Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# 4. Grain Regression
# Grain Regression

The thrust generated by a solid motor or hybrid engine depends on how much propellant surface is burning.
As the propellant burns, combustion products are released (mostly in gaseous form) and the grain recedes.
The burn direction is always perpendicular to the uninhibited surface (Piobert's Law), so the shape of that surface determines how much area is burning at any moment.
Determining the shape of the burning surface as a function of how far it has receded is called regression analysis.

![Animated grain regression](../assets/theory/grain_regression/regression_animation.svg)
![Animated grain regression](../assets/explanations/grain_regression/regression_animation.svg)

For a tubular or BATES geometry, the burn area can be easily determined analytically as a function of the web distance traveled.
However, for more complex geometries, the burn area may need to be determined through numerical methods.

![Complex port geometries regressing](../assets/theory/grain_regression/geometry_animations.svg)
![Complex port geometries regressing](../assets/explanations/grain_regression/geometry_animations.svg)

Machwave uses the fast marching method (FMM) to compute the burn area for complex grain geometries.
The FMM is a numerical algorithm for solving the Eikonal equation, which describes the evolution of a wavefront as it propagates through a medium.
Expand All @@ -26,9 +26,9 @@ The result is a single regression map, that can be used to determine the perimet

Machwave currently supports both 2D and 3D FMM regression, for constant and varying cross-section grain geometries.

Section 4.1 walks through the 2D FMM implementation step by step, showing the arrays and visualizations at each stage. Section 4.2 shows the differences between the 2D and 3D implementations, and how Machwave handles varying cross-section geometries. Section 4.3 maps the full call flow for both.
[Step by Step](#step-by-step) walks through the 2D FMM implementation, showing the arrays and visualizations at each stage. [2D vs 3D FMM](#2d-vs-3d-fmm) shows the differences between the 2D and 3D implementations, and how Machwave handles varying cross-section geometries. [Call Flow](#call-flow) maps the full call flow for both.

## 4.1 Step by Step
## Step by Step

This section walks through a single tubular grain regression with a grid resolution of `n=13`, but the same steps apply to any other geometry.
The map is kept this small for documentation only, real simulations have a lower limit of `n=100`.
Expand All @@ -41,8 +41,8 @@ x by column: -1.00 -0.83 -0.67 -0.50 -0.33 -0.17 0.00 0.17 0.33 0.50 0.67
y by row: -1.00 -0.83 -0.67 -0.50 -0.33 -0.17 0.00 0.17 0.33 0.50 0.67 0.83 1.00
```

![map_x gradient](../assets/theory/grain_regression/coord_x.svg)
![map_y gradient](../assets/theory/grain_regression/coord_y.svg)
![map_x gradient](../assets/explanations/grain_regression/coord_x.svg)
![map_y gradient](../assets/explanations/grain_regression/coord_y.svg)

**Mask the outer diameter: [`get_outer_diameter_mask()`][machwave.models.grain.fmm._2d.FMMGrainSegment2D.get_outer_diameter_mask].**
A `1` marks a cell outside the outer diameter ($x^2 + y^2 > 1$); the `0` cells are propellant.
Expand All @@ -63,7 +63,7 @@ A `1` marks a cell outside the outer diameter ($x^2 + y^2 > 1$); the `0` cells a
1 1 1 1 1 1 0 1 1 1 1 1 1
```

![casing mask](../assets/theory/grain_regression/mask.svg)
![casing mask](../assets/explanations/grain_regression/mask.svg)

**Carve the core: [`generate_initial_face_map()`][machwave.models.grain.fmm.base.FMMGrainSegment.generate_initial_face_map].**
Each grain geometry draws its own shape here.
Expand All @@ -85,7 +85,7 @@ This example uses a circular core. Empty cells are `0`, solid propellant is `1`.
1 1 1 1 1 1 1 1 1 1 1 1 1
```

![initial port](../assets/theory/grain_regression/initial_face.svg)
![initial port](../assets/explanations/grain_regression/initial_face.svg)

**Apply the inhibitors: [`get_masked_face()`][machwave.models.grain.fmm.base.FMMGrainSegment.get_masked_face].**
Lays the initial face over the outer-diameter mask, then `_apply_surface_inhibition` decides which of the grain's four surfaces are allowed to burn (an inhibited surface cannot burn).
Expand Down Expand Up @@ -113,7 +113,7 @@ By default only the outer surface is inhibited.
· · · · · · 1 · · · · · ·
```

![masked face](../assets/theory/grain_regression/masked_face.svg)
![masked face](../assets/explanations/grain_regression/masked_face.svg)

**Compute the regression map: [`get_regression_map()`][machwave.models.grain.fmm.base.FMMGrainSegment.get_regression_map].**
Now the fast marching method runs. `skfmm.distance` fills every propellant cell with its distance from the burning surface, as a fraction of the grain radius. The map's largest value is the web thickness ([`get_web_thickness()`][machwave.models.grain.fmm.base.FMMGrainSegment.get_web_thickness]).
Expand All @@ -134,7 +134,7 @@ Now the fast marching method runs. `skfmm.distance` fills every propellant cell
· · · · · · 0.5 · · · · · ·
```

![regression field gradient](../assets/theory/grain_regression/regression.svg)
![regression field gradient](../assets/explanations/grain_regression/regression.svg)

*Yellow cells represent empty space. The color darkens with depth into the web.*

Expand All @@ -157,7 +157,7 @@ The face map at a web distance is obtained by thresholding the regression map at
· · · · · · 1 · · · · · ·
```

![regressed face map](../assets/theory/grain_regression/face_map.svg)
![regressed face map](../assets/explanations/grain_regression/face_map.svg)

The mass-property reads, volume in 3D plus the center of gravity and moment of inertia in both 2D and 3D, need only the solid (`1`) cells of this map. They take those as a plain boolean mask from `_get_solid_mask`, cheaper to build and store than the masked integer map. The 2D reads pull the solid-cell indices from that mask, while the 3D reads reduce it straight to mass-property moments, and the mask, its indices, and its moments are each cached per web distance. Several of these reads share one web distance on a single timestep, so the regression map is thresholded once and every consumer reuses the result. The web distance only grows over a burn, so a single-entry cache is enough.

Expand All @@ -170,7 +170,7 @@ Then, `get_length` sums the curve to calculate the burning perimeter. Cells outs
... (6.0, 2.4) (7.0, 2.4) (8.0, 2.9) (9.0, 3.8) (9.6, 5.0) (9.6, 6.0) ... (closed)
```

![burning front contour](../assets/theory/grain_regression/contours.svg)
![burning front contour](../assets/explanations/grain_regression/contours.svg)

**Burn area across the web: [`get_burn_area(w)`][machwave.models.grain.fmm._2d.FMMGrainSegment2D.get_burn_area].**
The contour gives the burning perimeter at a single web distance.
Expand All @@ -185,7 +185,7 @@ The volume is the current grain length times the face area, reusing the same cac
**Port area: [`get_port_area(w)`][machwave.models.grain.fmm._2d.FMMGrainSegment2D.get_port_area].**
The port is the empty space inside the casing: the casing cross-section minus the solid face area. It comes straight from the cached face area, so it needs no extra work.

## 4.2 2D vs 3D FMM
## 2D vs 3D FMM

[`FMMGrainSegment2D`][machwave.models.grain.fmm._2d.FMMGrainSegment2D] describes a grain by a single cross-section extruded along its length.
[`FMMGrainSegment3D`][machwave.models.grain.fmm._3d.FMMGrainSegment3D] adds a `z` axis to the maps, split into `get_axial_resolution()` slices, so each slice can carve its own core, needed for varying geometries like finocyl or conical grains.
Expand All @@ -203,7 +203,7 @@ The two reads share one cached set of moments per web distance, so the per-times

Since the port area in 3D varies along the grain, [`get_port_area(w, z)`][machwave.models.grain.fmm._3d.FMMGrainSegment3D.get_port_area] slices the cross-section at axial height `z` and subtracts the solid area from the outer diameter exterior.

## 4.3 Call Flow
## Call Flow

The setup pipeline is shared by both implementations. They diverge only at how the burn area is measured and how volume and port area are read back. Methods marked `*` are overridden in 3D: `get_coordinate_grids` adds a `z` axis, `_apply_surface_inhibition` works slice by slice, and `_compute_regression_distance` uses a per-axis grid spacing.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 2. Mass Balance
# Mass Balance

Machwave applies the **conservation of mass** to an open control volume (the combustion chamber).
This means that the rate of change of **mass stored** equals the difference between the **mass inflow** and the **mass outflow**:
Expand Down Expand Up @@ -85,7 +85,7 @@ $$

where:

- $k$ is the isentropic exponent of the combustion products, evaluated at chamber conditions — the chamber value, distinct from the exhaust value $k_e$ used for the thrust coefficient in §1 (dimensionless)
- $k$ is the isentropic exponent of the combustion products, evaluated at chamber conditions — the chamber value, distinct from the exhaust value $k_e$ used for the thrust coefficient in [Thrust and Performance](thrust.md) (dimensionless)
- $P_r = P_\text{ext} / P_0$ is the back-pressure ratio, formed with the external (ambient) pressure $P_\text{ext}$ (dimensionless)
- $P^* = \left(\dfrac{2}{k+1}\right)^{k/(k-1)}$ is the critical pressure ratio (dimensionless)

Expand All @@ -105,7 +105,7 @@ It is evaluated by [`compute_chamber_pressure_mass_balance`][machwave.core.mass_
Both source terms are evaluated at the chamber pressure of each Runge–Kutta stage: $\dot{m}_{out}$ is linear in $P_0$, and $\dot{m}_{in}$ carries the pressure dependence of the burn rate (solid motor) or of the injector pressure drop (biliquid engine), so the integrator advances the true coupled $(\dot{m}_{in} - \dot{m}_{out})$ slope instead of freezing the inflow at the start of the step.
The next sections derive the specific forms of $\dot{m}_{in}$ for different categories of motors/engines.

## 2.1 Solid Rocket Motor
## Solid Rocket Motor

For a solid motor, the mass inflow is generated by the combustion of the solid propellant grain.
The grain burn rate $r$ is dependent on the chamber pressure and is modeled by Saint Robert's law:
Expand Down Expand Up @@ -143,7 +143,7 @@ $$

Implemented in [`SolidMotorState`][machwave.simulation.solid.states.SolidMotorState].

## 2.2 Biliquid Rocket Engine
## Biliquid Rocket Engine

For a biliquid engine, the mass inflow is set by the **injector mass flow** of two independent propellant streams.

Expand Down
12 changes: 6 additions & 6 deletions docs/theory/thrust.md → docs/explanations/thrust.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# 1. Thrust and Performance
# Thrust and Performance

Machwave calculates the **thrust** of a rocket motor/engine using the **thrust coefficient formulation**, which is derived from the idealized flow through a rocket nozzle.
The ideal thrust coefficient has two terms, a momentum term which dominates and a pressure term.
Each term is calculated and multiplied by the **nozzle efficiency** to obtain the real thrust coefficient.
The nozzle efficiency can be applied independently to each term, and accounts for the deviations from the idealized assumptions, such as boundary layer losses, chemical kinetic losses, and more.

## 1.1 Thrust Coefficient
## Thrust Coefficient

$$
F = C_f\, P_0\, A_t
Expand All @@ -22,7 +22,7 @@ where:

Implemented in [`get_thrust_from_thrust_coefficient`][machwave.core.compressible_flow.nozzle.get_thrust_from_thrust_coefficient].

## 1.2 Ideal Thrust Coefficient
## Ideal Thrust Coefficient

The **ideal thrust coefficient** is given by:

Expand All @@ -42,7 +42,7 @@ where:
The momentum term and the pressure term are returned separately by [`get_ideal_thrust_coefficient_terms`][machwave.core.compressible_flow.nozzle.get_ideal_thrust_coefficient_terms].
This way, nozzle losses can act on either term separately.

## 1.3 Flow Separation
## Flow Separation

The ideal thrust coefficient above assumes the nozzle flows full, with the exhaust attached to the wall all the way to the geometric exit.
As the chamber pressure decays during tail-off the nozzle becomes increasingly overexpanded ($P_e \ll P_\text{ext}$), and below a threshold the boundary layer can no longer sustain the adverse pressure gradient.
Expand All @@ -65,7 +65,7 @@ where:

Implemented in [`get_separated_exit_conditions`][machwave.core.compressible_flow.nozzle.get_separated_exit_conditions].

## 1.4 Nozzle Efficiency
## Nozzle Efficiency

Real nozzles deviate from the ideal assumptions baked into the ideal thrust coefficient.
Machwave lumps these deviations into a single **nozzle efficiency** $\eta_\text{nozzle}$ applied multiplicatively to the ideal coefficient:
Expand All @@ -84,7 +84,7 @@ Which and how many losses are included depends on the motor/engine category.
Each loss is composed by the `Motor`'s nozzle loss model and may derate the momentum term, the pressure term, or both of the thrust coefficient.
The equation above is the case where every loss derates both terms.

## 1.5 Total Impulse and Specific Impulse
## Total Impulse and Specific Impulse

After the thrust is calculated, the total impulse is obtained by integrating it over the thrust time:

Expand Down
10 changes: 5 additions & 5 deletions machwave/models/grain/fmm/NAMING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ and the parameters `web_distance`, `length`, `outer_diameter`, `density_ratio`,
- **`get_regression_map` / `regression_map`** — the adversarial review favoured
`get_arrival_time_field` on FMM-canon grounds (correct: under unit burn speed
`T(x)` *is* the regression distance). **Decision: keep `regression_map`.**
It is the term used throughout this module's `README.md` and the `docs/theory`
pages; renaming the code alone would split the vocabulary between code and
docs. Instead, enrich the docstring: *"the FMM arrival-time field `T(x)`; under
unit burn speed it equals the regression distance."*
It is the term used throughout this module's `README.md` and the
`docs/explanations` pages; renaming the code alone would split the vocabulary
between code and docs. Instead, enrich the docstring: *"the FMM arrival-time
field `T(x)`; under unit burn speed it equals the regression distance."*
- Short, clear locals in tight scopes: `inside`, `eroded`, `element_mass`,
`total_mass`, `total_volume`, `volume_per_element`, `voxels`, `voxel_map`,
`mesh`, `_resample_nearest`, `get_core_perimeter` (`core` is the project-wide
Expand Down Expand Up @@ -269,7 +269,7 @@ must update it in the same PR: this module's [`README.md`](README.md) (which
walks through `generate_initial_face_map`, `get_outer_diameter_mask`,
`get_regression_map`, `get_face_map`, `get_contours`,
`get_face_area_interpolator`, `get_burn_area_interpolator`, …), the
`docs/theory/grain_regression.md` walkthrough
`docs/explanations/grain_regression.md` walkthrough
and call-flow diagrams, the `docs/api/models/grain/` reference pages, and the
generated `site/`. After a docs-affecting rename, rebuild the site so the
generated HTML matches.
Expand Down
8 changes: 4 additions & 4 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ plugins:
nav:
- Home: index.md
- Quick Start: quickstart.md
- Rocket Theory:
- 1. Thrust and Performance: theory/thrust.md
- 2. Mass Balance: theory/mass_balance.md
- 3. Grain Regression: theory/grain_regression.md
- Explanations:
- Thrust and Performance: explanations/thrust.md
- Mass Balance: explanations/mass_balance.md
- Grain Regression: explanations/grain_regression.md
- Modules:
- adapters:
- Overview: api/adapters.md
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading