turbo_moc package#

TurboMoC: Method of Characteristics solvers for nozzle/stator design.

Two solver variants share one thermodynamic/characteristics core (turbo_moc.core.classes):

  • turbo_moc.solvers.conventional.MOCSolver is the rounded-arc kernel. It supports subcooled-liquid flashing inlets (with a flat post-jump throat front), ordinary single-phase inlets, and saturated two-phase inlets.

  • turbo_moc.solvers.mln.MOCSolverMLN is the minimum-length, sharp-corner nozzle with a centred expansion fan from the throat corner.

Both accept the same style of config dict and export_full_result_data() schema, so results are directly comparable.

turbo_moc.ConventionalSolver#

alias of MOCSolver

class turbo_moc.FluidManager(fluid_name: str, backend: str, P0, T0, Q0)[source]#

Bases: object

Manages the single instance of the jaxprop fluid handler and stores the persistent stagnation properties (s0, h0) and critical state.

Methods

V_of_nu(nu_val)

Inverse of nu_of_V: velocity at a given real-gas Prandtl-Meyer angle (radians).

a_of_V(V)

Speed of sound at velocity V along the same s=s0 isentrope used for nu_of_V (cheap table lookup, no fluid call).

calculate_static_properties(V, s0, h0)

Calculates static properties for a single scalar velocity V.

nu_of_V(V)

Real-gas Prandtl-Meyer angle (radians) at velocity V, referenced to the critical/jump state.

rho_of_V(V)

Density at velocity V along the same s=s0 isentrope used for nu_of_V (cheap table lookup, no fluid call).

calculate_stagnation_state

V_of_nu(nu_val)[source]#

Inverse of nu_of_V: velocity at a given real-gas Prandtl-Meyer angle (radians).

a_of_V(V)[source]#

Speed of sound at velocity V along the same s=s0 isentrope used for nu_of_V (cheap table lookup, no fluid call).

calculate_stagnation_state()[source]#
calculate_static_properties(V, s0, h0)[source]#

Calculates static properties for a single scalar velocity V.

Calls the raw (non-JAX) Fluid object directly instead of routing through jitted_calculate_static_properties + FluidJAX.get_state’s nested JIT/pure_callback layers. This is the sole caller of that path and V is always a single scalar here (ThermoFlowProp, called once per point throughout Stage 2/3 marching – never batched), so the JIT layer was paying a full XLA trace+compile tax on nearly every call for zero reuse benefit (same diagnosis and fix as FluidManager._build_nu_table’s per-point loop).

nu_of_V(V)[source]#

Real-gas Prandtl-Meyer angle (radians) at velocity V, referenced to the critical/jump state.

rho_of_V(V)[source]#

Density at velocity V along the same s=s0 isentrope used for nu_of_V (cheap table lookup, no fluid call).

class turbo_moc.InternalPoint(fluid_manager: FluidManager, x=None, y=None)[source]#

Bases: object

Class representing a single point in a Method of Characteristics (MoC) grid. Stores geometry and flow properties. Includes a method to compute the sonic condition (Sauer real-gas approach) using a 1D root finder and the main Sauer function for the sonic line.

Methods

Inv_Wall_pnt_vertical(pwall, p33, p11, wall_ang)

Wall-point unit process for an initial data line that is VERTICAL (constant x -- e.g. the flat flashing throat front, x=0 for every y).

ReflexLine(n, l)

Reflex Line position calculator (ReflexZone).

SauerMain()

ThermoFlowProp()

compute_from_characteristics(paramP, paramM)

Computes a new MOC point.

compute_sonic_state()

Calculates the critical (sonic, M=1) state.

corner_point_prop(x_corner, y_corner, theta_rad)

Direct (search-free) sharp-corner unit process for a centred Prandtl-Meyer expansion fan.

ensure_fresh()

Recompute thermo/flow properties only if u or v changed since the last ThermoFlowProp call -- see _thermo_fresh above.

euler_predictor_corrector(paramP, paramM[, ...])

Euler Predictor-Corrector optimized with Object Reuse and Thermophysical Properties (a, theta) calculated ONLY ONCE before the iteration loop.

interior_point_prop(paramP, paramM)

Exact planar (delta=0) interior-point unit process.

solve_centerline_point(paramP, paramM)

Calculates the properties of a new point on the Axis of Symmetry (y=0).

to_dict()

Inv_Wall_pnt

copy_from

Inv_Wall_pnt(pwall, p33, p11, wall_ang)[source]#
Inv_Wall_pnt_vertical(pwall, p33, p11, wall_ang)[source]#

Wall-point unit process for an initial data line that is VERTICAL (constant x – e.g. the flat flashing throat front, x=0 for every y). Inv_Wall_pnt interpolates position along the “line 3-1” by x, which is degenerate when x33 == x11; here the same shooting method is redone parametrizing by y instead (x is fixed along the line, so any y between p33.y and p11.y is a valid position). Otherwise identical to Inv_Wall_pnt.

ReflexLine(n, l)[source]#

Reflex Line position calculator (ReflexZone). Calculates the position of the new wall point (or reflection point) a distance ‘l’ away, projected along the Mach angle plus flow angle.

Parameters:

n (float): Step multiplier (1 in the case of wall projection, n_step in others). l (float): Calculated distance based on mass flow ratio.

SauerMain()[source]#
ThermoFlowProp()[source]#
compute_from_characteristics(paramP, paramM, EPC=0)[source]#

Computes a new MOC point.

EPC defaults to 0 (no corrector): interior_point_prop / solve_centerline_point now solve the EXACT planar (theta, nu) compatibility relations directly (see their docstrings), so there is no thermodynamic linearisation error left for a corrector step to reduce. euler_predictor_corrector still uses the old linearised Q/R1/S algebra and would only reintroduce that error if applied here, so it is skipped unless explicitly requested.

compute_sonic_state()[source]#

Calculates the critical (sonic, M=1) state. FIX: Reads the pre-calculated critical state properties from the Manager.

copy_from(other)[source]#
corner_point_prop(x_corner, y_corner, theta_rad)[source]#

Direct (search-free) sharp-corner unit process for a centred Prandtl-Meyer expansion fan.

Within a centred simple wave emanating from a sharp corner, theta and nu are related EXACTLY by theta - theta_ref = nu(V) - nu_ref (Zebbiche & Youbi 2007, eqs. 1-2 restricted to a simple wave). Since the corner’s reference state (the throat/post-jump flow, before any turning) has theta_ref=0 and is exactly where this class’s nu(V) table is zeroed (see FluidManager._build_nu_table), this collapses to theta = nu(V) at the corner itself for ANY prescribed turning angle theta – no dependence on neighbouring front data at all.

This replaces Inv_Wall_pnt / Inv_Wall_pnt_vertical’s iterative shooting search (which interpolates against bracketing front points to locate a wall intersection) for the MLN sharp-corner case, where the wall position is fixed and the flow angle at each fan angle is prescribed by construction – there is nothing to search for. Used by moc_solver_mln.py’s run_stage_3_kernel_mln.

ensure_fresh()[source]#

Recompute thermo/flow properties only if u or v changed since the last ThermoFlowProp call – see _thermo_fresh above.

euler_predictor_corrector(paramP, paramM, max_iter=1000, tol_x=1e-06, tol_v=1e-06)[source]#

Euler Predictor-Corrector optimized with Object Reuse and Thermophysical Properties (a, theta) calculated ONLY ONCE before the iteration loop.

interior_point_prop(paramP, paramM)[source]#

Exact planar (delta=0) interior-point unit process.

Zebbiche & Youbi (2007), eqs. 1-2, 9-23: for planar flow the characteristic compatibility relations are the exact invariants:

  • theta + nu(V) = const along C- (paramM’s characteristic)

  • theta - nu(V) = const along C+ (paramP’s characteristic)

so theta3 and nu3 (hence V3) at the new point follow directly from the two foot points’ theta and nu, with no linearisation at all in the thermodynamic part. This replaces the general (axisymmetric- capable) linearised Q/R1/S solve, which is only approximate for this delta=0 problem and was the source of accumulating error in earlier attempts. Geometry (the (x, y) intersection) still needs the standard linearised treatment, unchanged from before.

solve_centerline_point(paramP, paramM)[source]#

Calculates the properties of a new point on the Axis of Symmetry (y=0).

Exact planar (delta=0) compatibility: theta + nu(V) = const along the C- characteristic through paramM (Zebbiche & Youbi 2007, eqs. 1, 9, 27-28). With the axis reflection boundary condition theta=0, this gives nu(V_axis) = paramM.theta + nu(paramM.V) directly – no linearised Q/R1/S algebra needed. Geometry (intersection) is unchanged: paramP supplies the mirror-image C+ line used to locate the axis point.

to_dict()[source]#
class turbo_moc.MOCSolverMLN(config: dict, show_plot=False, fluid_manager: FluidManager = None)[source]#

Bases: object

Minimum-Length Nozzle solver. Stages 1, 2, 4 are identical to MOCSolver in turbo_moc/. Stage 3 uses a centred expansion fan from the throat corner.

Methods

build_result_data([extra_meta])

Returns the full result dict without touching the filesystem -- see turbo_moc.solvers.conventional.MOCSolver.build_result_data (same schema, reused as-is by turbo_moc.api).

plot_results()

run_stage_1_sauer([progress_callback])

run_stage_2_ivp_march([progress_callback])

The flashing throat front is flat (x=0 for every y), so it carries no geometric spread in x.

run_stage_2_sauer_direct([progress_callback])

For the MLN, the expansion fan originates entirely from the sharp throat corner: within a centred simple wave, the corner's own (theta, nu) at any fan angle follows directly from that angle alone (see InternalPoint.corner_point_prop), with no dependence on the rest of the throat line -- PROVIDED the corner's own reference state has theta=0 exactly (the nu(V) table's zero point).

run_stage_3_kernel_mln([progress_callback])

Minimum-length nozzle kernel.

export_full_result_data

run_stage_4_reflex

build_result_data(extra_meta=None)[source]#

Returns the full result dict without touching the filesystem – see turbo_moc.solvers.conventional.MOCSolver.build_result_data (same schema, reused as-is by turbo_moc.api).

export_full_result_data(filename='turbo_moc_result_data.json', extra_meta=None)[source]#
plot_results()[source]#
run_stage_1_sauer(progress_callback=None)[source]#
run_stage_2_ivp_march(progress_callback=None)[source]#

The flashing throat front is flat (x=0 for every y), so it carries no geometric spread in x. Stage 3’s Inv_Wall_pnt needs two IVP points with different x to define the line 3-1 slope — feeding the flat front to it directly divides by zero. Marching the front forward via interior-point compatibility (mixing C+/C- characteristics, always at different slopes since alpha != 0) builds a proper x-spread characteristics mesh before Stage 3 runs, exactly as moc_tool_flashing does for the arc-wall kernel.

NOTE: mutates self.Sauer in place; run_stage_1_sauer already cached the true throat-corner x in self.throat_corner_x before this runs.

run_stage_2_sauer_direct(progress_callback=None)[source]#

For the MLN, the expansion fan originates entirely from the sharp throat corner: within a centred simple wave, the corner’s own (theta, nu) at any fan angle follows directly from that angle alone (see InternalPoint.corner_point_prop), with no dependence on the rest of the throat line – PROVIDED the corner’s own reference state has theta=0 exactly (the nu(V) table’s zero point). This holds exactly for the flashing front (_stage1_flashing_front sets v=0, i.e. theta=0, at every point, including the corner), but NOT for the smooth Sauer sonic line (_stage1_sauer_smooth): the transonic curvature formula (SauerMain) gives a small but genuinely nonzero flow angle even at the wall point, so collapsing to a single point would silently drop that curvature. Only the flashing case is therefore simplified to a single-point IVP; the smooth case keeps the full multi-point front as before.

For the flashing case, dropping the other (redundant – all identical state, just spread over y) front points also makes the first fan angle collapse exactly to the reference algorithm’s ray 1 (corner -> axis directly, zero interior points, see Zebbiche & Youbi 2007 Fig. 3a) – previously ray 1 was seeded from the full multi-point front, adding “interior” points that carried no independent new information.

run_stage_3_kernel_mln(progress_callback=None)[source]#

Minimum-length nozzle kernel.

Identical logic to the standard Stage 3 kernel EXCEPT that the wall position is fixed at the throat corner (x_corner, y_t) for every fan angle, instead of moving along a circular arc.

For flashing cases, the corner point at each fan angle is computed DIRECTLY via InternalPoint.corner_point_prop (no iterative wall search), and self.IVP is seeded (in run_stage_2_sauer_direct) with just the single corner point rather than the full flat front – together these make ray 1 collapse exactly to corner -> axis directly (zero interior points), matching Zebbiche & Youbi (2007) Fig. 3a’s reference algorithm.

The corner position is taken from IVP[0] after Stage 2 (the wall-side point on the IVP line, which is already supersonic — avoiding the M=1 singularity that caused NaN in the previous implementation).

All C+ characteristics thus originate from this single corner point. The flow angle at the corner grows step-by-step through the wall compatibility equation (Inv_Wall_pnt), which is the standard procedure for any wall boundary.

FIXED TARGET, NOT A SEARCH. self.tau_mln is expected to already span [0, theta*] where theta* = 0.5*nu(M_f) is the classical SSL-MLN corner turn angle (Argrow & Emanuel 1991, eq. for the straight sonic line MLN), computed from the same real-gas Prandtl-Meyer integral used in NICFD_2026/python_codes/04_moc_flashing.py, referenced from the post-jump state (nu=0 there) rather than the classical M=1 point – exactly the origin of this corner’s fan. Sweeping a KNOWN, bounded angle range (rather than an open-ended Mach/pressure-crossing search) avoids accumulating turning far past where the design actually ends, which is what previously drove the axis-point solve into the near-M=1 singularity and the fluid table’s valid range.

run_stage_4_reflex(refine_near_corner=False, n_sub=8, n_segments=3, progress_callback=None)[source]#
exception turbo_moc.NozzleDesignError[source]#

Bases: Exception

Raised when a nozzle design request is invalid or the solve fails before reaching a usable result (as opposed to the solver’s own graceful early-stop, which still returns a partial-but-usable design – see the ‘converged’ flag in design_nozzle’s returned dict).

turbo_moc.design_nozzle(fluid_name: str, P0: float, T0: float = None, Q0: float = nan, p_back: float = None, Noz_Mach: float = None, solver: str = 'conventional', y_t: float = 0.01, n: int = 15, rho_t: float = 0.2, rho_d: float = 0.1, delta_flow: float = 0.0, tau_max: float = 90.0, tau_step: float = 1.0, n_tau_mln: int = 60, use_true_sauer_line: bool = False, backend: str = 'HEOS', show_plot: bool = False, progress_callback=None, auto_nudge_q0: bool = True) → dict[source]#

Run every stage of a method-of-characteristics nozzle design.

The function accepts ordinary values and returns no solver or fluid-manager objects, so its result can be passed directly to a UI callback or HTTP handler.

Parameters:
fluid_namestr

CoolProp fluid name.

P0float

Stagnation pressure in pascals.

T0float, optional

Stagnation temperature in kelvin for a single-phase or flashing inlet.

Q0float, optional

Inlet quality for a saturated two-phase state. Leave as NaN when using T0.

p_backfloat, optional

Back pressure in pascals. This is the recommended design target and is required by the minimum-length solver.

Noz_Machfloat, optional

Direct nozzle Mach-number target. At least one of p_back and Noz_Mach is required.

solver{“conventional”, “mln”}

Rounded-arc conventional solver or sharp-corner minimum-length solver.

y_tfloat

Throat half-height.

nint

Number of points used to discretize the initial characteristic line.

rho_t, rho_dfloat

Geometric radii used by the nozzle construction.

delta_flowfloat

Axisymmetric-flow switch or coefficient passed to the solver.

tau_max, tau_stepfloat

Angular extent and increment for the conventional solver.

n_tau_mlnint

Number of angular samples for the minimum-length expansion fan.

use_true_sauer_linebool

Use the classical parabolic Sauer sonic line instead of the default flat post-jump throat front.

backendstr

Thermodynamic backend passed to the fluid manager.

show_plotbool

Enable the solver’s diagnostic plotting.

progress_callbackcallable, optional

Called as progress_callback(fraction, stage_label) throughout the solver stages.

auto_nudge_q0bool

If a saturated-liquid classical Sauer solve fails, retry with small positive inlet qualities and retain the first converged result.

Returns:
dict

JSON-safe solver data. It includes wall, kernel, Sauer-line, mesh, and axis data together with the resolved inputs, convergence state, and runtime.

turbo_moc.get_fluid_manager(fluid_name: str, backend: str, P0, T0, Q0) → FluidManager[source]#

Returns a cached FluidManager for (fluid_name, backend, P0, T0, Q0), building it only on first use for that combination.

FluidManager depends only on the fluid and inlet condition, not on nozzle geometry – building it (notably its Prandtl-Meyer table, see _build_nu_table) is the dominant one-time cost per solver instantiation. A design/optimization loop that sweeps geometry at a FIXED operating point should call this once and pass the result to MOCSolver/ MOCSolverMLN’s fluid_manager= argument, instead of letting each one rebuild it from scratch. Changing the fluid or the operating point is a different cache key – it does NOT reuse a stale manager.

Q0 = float(‘nan’) marks a subcooled-liquid/single-phase inlet (use P0,T0 instead) – this is the common case here, so NaN needs explicit handling: two separately-constructed float(‘nan’) objects are neither is nor == to each other (NaN != NaN by IEEE 754), so using Q0 directly as a dict-key component would silently never hit the cache for this case (every call would look like a distinct, never-before-seen key). Collapsed to a fixed sentinel string instead so all NaN-Q0 calls at the same (fluid, P0, T0) correctly share one cached manager.

turbo_moc.list_supported_fluids() → list[source]#

CoolProp’s fluid list, for populating a fluid-selection UI control.

turbo_moc.make_progress_reporter(set_progress, weights=None)[source]#

Create a callback that combines progress from all solver stages.

Parameters:
set_progresscallable

Receives the (percent_string, message_string) tuple expected by a Dash background callback.

weightsdict, optional

Relative weights for solver stages 1 through 4. The defaults reflect their approximate runtime.

Returns:
callable

A reporter accepting (fraction, stage_label), suitable for a solver’s progress_callback argument.

Examples

Use the returned reporter inside a Dash background callback:

reporter = make_progress_reporter(set_progress)
data = design_nozzle(..., progress_callback=reporter)

Subpackages#

Submodules#