Skip to content

core.compressible_flow

Compressible flow relations for convergent-divergent nozzle analysis.

  • Isentropic flow — Critical pressure ratio, exit Mach from expansion ratio, exit pressure, choked-flow detection.
  • Nozzle performance — Ideal and corrected thrust coefficient (Cf), optimal expansion ratio for a given altitude, thrust from Cf.
  • Loss models — Percentage-based correction factors for divergent angle, finite-rate kinetics, boundary layer, and two-phase flow losses. These combine into an overall nozzle efficiency applied to the ideal Cf.

All loss functions return values in the 0–100% range and follow standard empirical correlations from Humble, Henry & Larson.

machwave.core.compressible_flow

Compressible flow theory and analysis.

apply_thrust_coefficient_correction(ideal_thrust_coefficient, nozzle_correction_factor)

Apply nozzle efficiency correction to thrust coefficient.

Parameters:

Name Type Description Default
ideal_thrust_coefficient float

Ideal thrust coefficient.

required
nozzle_correction_factor float

Nozzle efficiency (0-1).

required

Returns:

Type Description
float

Corrected thrust coefficient.

Source code in machwave/core/compressible_flow/nozzle.py
def apply_thrust_coefficient_correction(
    ideal_thrust_coefficient: float,
    nozzle_correction_factor: float,
) -> float:
    """Apply nozzle efficiency correction to thrust coefficient.

    Args:
        ideal_thrust_coefficient: Ideal thrust coefficient.
        nozzle_correction_factor: Nozzle efficiency (0-1).

    Returns:
        Corrected thrust coefficient.
    """
    return ideal_thrust_coefficient * nozzle_correction_factor

get_boundary_layer_loss_fraction(chamber_pressure_psi, throat_diameter_inch, expansion_ratio, time, c_1, c_2)

Boundary layer loss accounts for the decrement in performance due to the viscous and heat transfer effects in the nozzle walls. It is time dependent.

Valid for liquid, solid, and hybrid propellants.

The time dependence is exponential due to the transient heat up, important in motors with short burn durations (less than 4 seconds). Dependence on expansion ratio represents the effect of a the amount of nozzle surface area.

Time constant C2 comes from analysis of a the transient heating of a BATES motor.

Time constant C1 was obtained from a direct measurement of the heat loss in a BATES motor, among other things.

Ordinary nozzle: C1 = 0.003650 C2 = 0.000937

Solid steel nozzle with relatively thick walls: C1 = 0.005060 C2 = 0.000000

The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a fraction [0, 1].

Parameters:

Name Type Description Default
chamber_pressure_psi float

The chamber pressure [psi].

required
throat_diameter_inch float

The throat diameter [in].

required
expansion_ratio float

The expansion ratio of the nozzle.

required
time float

The time in seconds [s].

required
c_1 float

Coefficient for the boundary layer loss.

required
c_2 float

Coefficient for the boundary layer loss.

required

Returns: The boundary layer loss fraction in [0, 1].

Source code in machwave/core/compressible_flow/losses.py
@decorators.check_bounds(lower=0.0, upper=1.0)
@decorators.warn_if_outside_range(**TYPICAL_RANGES["boundary_layer_loss"])
def get_boundary_layer_loss_fraction(
    chamber_pressure_psi: float,
    throat_diameter_inch: float,
    expansion_ratio: float,
    time: float,
    c_1: float,
    c_2: float,
) -> float:
    """
    Boundary layer loss accounts for the decrement in performance due to
    the viscous and heat transfer effects in the nozzle walls. It is time dependent.

    Valid for liquid, solid, and hybrid propellants.

    The time dependence is exponential due to the transient heat up, important in
    motors with short burn durations (less than 4 seconds). Dependence on expansion
    ratio represents the effect of a the amount of nozzle surface area.

    Time constant C2 comes from analysis of a the transient heating of a BATES motor.

    Time constant C1 was obtained from a direct measurement of the heat loss in a BATES
    motor, among other things.

    Ordinary nozzle:
    C1 = 0.003650
    C2 = 0.000937

    Solid steel nozzle with relatively thick walls:
    C1 = 0.005060
    C2 = 0.000000

    The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a
    fraction [0, 1].

    Args:
        chamber_pressure_psi: The chamber pressure [psi].
        throat_diameter_inch: The throat diameter [in].
        expansion_ratio: The expansion ratio of the nozzle.
        time: The time in seconds [s].
        c_1: Coefficient for the boundary layer loss.
        c_2: Coefficient for the boundary layer loss.
    Returns:
        The boundary layer loss fraction in [0, 1].
    """
    term_1 = c_1 * (chamber_pressure_psi**0.8) / (throat_diameter_inch**0.2)
    term_2 = 1 + 2 * np.exp(
        (-c_2 * chamber_pressure_psi**0.8 * time) / (throat_diameter_inch**0.2)
    )
    term_3 = 1 + 0.016 * (expansion_ratio - 9)

    return 0.01 * term_1 * term_2 * term_3

get_critical_pressure_ratio(k)

Get critical pressure ratio for choked flow.

Parameters:

Name Type Description Default
k float

Isentropic exponent.

required

Returns:

Type Description
float

Critical pressure ratio.

Source code in machwave/core/compressible_flow/isentropic.py
def get_critical_pressure_ratio(k: float) -> float:
    """Get critical pressure ratio for choked flow.

    Args:
        k: Isentropic exponent.

    Returns:
        Critical pressure ratio.
    """
    return (2 / (k + 1)) ** (k / (k - 1))

get_exit_mach_from_expansion_ratio(k, expansion_ratio)

Get exit Mach number from expansion ratio.

Parameters:

Name Type Description Default
k float

Isentropic exponent.

required
expansion_ratio float

Expansion ratio (A_exit / A_throat).

required

Returns:

Type Description
float

Exit Mach number.

Raises:

Type Description
ValueError

If solver fails to converge.

Source code in machwave/core/compressible_flow/isentropic.py
def get_exit_mach_from_expansion_ratio(k: float, expansion_ratio: float) -> float:
    """Get exit Mach number from expansion ratio.

    Args:
        k: Isentropic exponent.
        expansion_ratio: Expansion ratio (A_exit / A_throat).

    Returns:
        Exit Mach number.

    Raises:
        ValueError: If solver fails to converge.
    """
    try:
        exit_mach = cast(
            float,
            scipy.optimize.brentq(
                lambda m: get_expansion_ratio_from_exit_mach(m, k) - expansion_ratio,
                a=1.001,  # Just above sonic
                b=20.0,  # High supersonic
            ),
        )
        return exit_mach
    except ValueError as e:
        raise ValueError(
            f"Failed to converge for expansion_ratio={expansion_ratio}, k={k}"
        ) from e

get_exit_pressure(k_exhaust, expansion_ratio, chamber_pressure)

Get exit pressure from isentropic relations.

Parameters:

Name Type Description Default
k_exhaust float

Isentropic exponent at exit.

required
expansion_ratio float

Expansion ratio.

required
chamber_pressure float

Chamber pressure [Pa].

required

Returns:

Type Description
float

Exit pressure [Pa].

Source code in machwave/core/compressible_flow/isentropic.py
def get_exit_pressure(
    k_exhaust: float,
    expansion_ratio: float,
    chamber_pressure: float,
) -> float:
    """Get exit pressure from isentropic relations.

    Args:
        k_exhaust: Isentropic exponent at exit.
        expansion_ratio: Expansion ratio.
        chamber_pressure: Chamber pressure [Pa].

    Returns:
        Exit pressure [Pa].
    """
    exit_mach = get_exit_mach_from_expansion_ratio(k_exhaust, expansion_ratio)
    return chamber_pressure * (1 + 0.5 * (k_exhaust - 1) * exit_mach**2) ** (
        -k_exhaust / (k_exhaust - 1)
    )

get_expansion_ratio_from_exit_mach(mach, k)

Get expansion ratio from exit Mach number.

Parameters:

Name Type Description Default
mach float

Mach number.

required
k float

Isentropic exponent.

required

Returns:

Type Description
float

Expansion ratio (A / A_throat).

Source code in machwave/core/compressible_flow/isentropic.py
def get_expansion_ratio_from_exit_mach(mach: float, k: float) -> float:
    """Get expansion ratio from exit Mach number.

    Args:
        mach: Mach number.
        k: Isentropic exponent.

    Returns:
        Expansion ratio (A / A_throat).
    """
    term1 = (2 / (k + 1)) * (1 + 0.5 * (k - 1) * mach**2)
    term2 = (k + 1) / (2 * (k - 1))
    return (1 / mach) * (term1**term2)

get_ideal_thrust_coefficient(chamber_pressure, exit_pressure, external_pressure, expansion_ratio, k_exhaust)

Get ideal thrust coefficient for DeLaval nozzle.

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required
exit_pressure float

Exit pressure [Pa].

required
external_pressure float

External pressure [Pa].

required
expansion_ratio float

Expansion ratio.

required
k_exhaust float

Isentropic exponent at exit.

required

Returns:

Type Description
float

Ideal thrust coefficient.

References

https://www.nakka-rocketry.net/th_thrst.html

Source code in machwave/core/compressible_flow/nozzle.py
def get_ideal_thrust_coefficient(
    chamber_pressure: float,
    exit_pressure: float,
    external_pressure: float,
    expansion_ratio: float,
    k_exhaust: float,
) -> float:
    """Get ideal thrust coefficient for DeLaval nozzle.

    Args:
        chamber_pressure: Chamber pressure [Pa].
        exit_pressure: Exit pressure [Pa].
        external_pressure: External pressure [Pa].
        expansion_ratio: Expansion ratio.
        k_exhaust: Isentropic exponent at exit.

    Returns:
        Ideal thrust coefficient.

    References:
        https://www.nakka-rocketry.net/th_thrst.html
    """
    pressure_ratio = exit_pressure / chamber_pressure
    return (
        np.sqrt(
            (2 * (k_exhaust**2) / (k_exhaust - 1))
            * ((2 / (k_exhaust + 1)) ** ((k_exhaust + 1) / (k_exhaust - 1)))
            * (1 - (pressure_ratio ** ((k_exhaust - 1) / k_exhaust)))
        )
        + expansion_ratio * (exit_pressure - external_pressure) / chamber_pressure
    )

get_kinetics_loss_fraction(i_sp_th_frozen, i_sp_th_shifting, chamber_pressure_psi)

The kinetics loss accounts for the decrement in performance due to incomplete heat transfer of latent heat to sensible heat caused by the finite time required for the gas phase chemical reactions to occur.

Valid for liquid, solid, and hybrid propellants. The expansion ratio of the i_sp_th_frozen and i_sp_th_shifting should be the same.

Pressure correction is applied for chamber pressures above 1.379 MPa (200 psi), in order to dampen the effect of the kinetics loss.

The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a fraction [0, 1].

Parameters:

Name Type Description Default
i_sp_th_frozen float

The specific impulse of the frozen flow [s].

required
i_sp_th_shifting float

The specific impulse of the shifting flow [s].

required
chamber_pressure_psi float

The chamber pressure [psi].

required

Returns: The kinetics loss fraction in [0, 1].

Source code in machwave/core/compressible_flow/losses.py
@decorators.check_bounds(lower=0.0, upper=1.0)
@decorators.warn_if_outside_range(**TYPICAL_RANGES["kinetics_loss"])
def get_kinetics_loss_fraction(
    i_sp_th_frozen: float, i_sp_th_shifting: float, chamber_pressure_psi: float
) -> float:
    """
    The kinetics loss accounts for the decrement in performance due to
    incomplete heat transfer of latent heat to sensible heat caused by the finite time
    required for the gas phase chemical reactions to occur.

    Valid for liquid, solid, and hybrid propellants.
    The expansion ratio of the i_sp_th_frozen and i_sp_th_shifting should be the same.

    Pressure correction is applied for chamber pressures above 1.379 MPa (200 psi), in
    order to dampen the effect of the kinetics loss.

    The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a
    fraction [0, 1].

    Args:
        i_sp_th_frozen: The specific impulse of the frozen flow [s].
        i_sp_th_shifting: The specific impulse of the shifting flow [s].
        chamber_pressure_psi: The chamber pressure [psi].
    Returns:
        The kinetics loss fraction in [0, 1].
    """
    i_sp_th_ratio = i_sp_th_frozen / i_sp_th_shifting

    if chamber_pressure_psi < KINETICS_LOSS_PRESSURE_THRESHOLD_PSI:
        pressure_correction = 1.0
    else:
        pressure_correction = (
            KINETICS_LOSS_PRESSURE_THRESHOLD_PSI / chamber_pressure_psi
        )

    return 0.333 * (1 - i_sp_th_ratio) * pressure_correction

get_nozzle_divergent_loss_fraction(divergent_angle)

Calculates the divergent nozzle loss fraction given the half angle. NOTE: only applicable for a conical convergent-divergent nozzle.

Parameters:

Name Type Description Default
divergent_angle float

The half angle of the divergent nozzle [degrees].

required

Returns:

Type Description
float

The divergent loss fraction in [0, 1].

Source code in machwave/core/compressible_flow/losses.py
@decorators.check_bounds(lower=0.0, upper=1.0)
@decorators.warn_if_outside_range(**TYPICAL_RANGES["divergent_loss"])
def get_nozzle_divergent_loss_fraction(divergent_angle: float) -> float:
    """
    Calculates the divergent nozzle loss fraction given the half angle.
    NOTE: only applicable for a conical convergent-divergent nozzle.

    Args:
        divergent_angle: The half angle of the divergent nozzle [degrees].

    Returns:
        The divergent loss fraction in [0, 1].
    """
    return 0.5 * (1 - np.cos(np.deg2rad(divergent_angle)))

get_optimal_expansion_ratio(k, chamber_pressure, atmospheric_pressure)

Get optimal expansion ratio for DeLaval nozzle.

Parameters:

Name Type Description Default
k float

Isentropic exponent.

required
chamber_pressure float

Chamber pressure [Pa].

required
atmospheric_pressure float

External pressure [Pa].

required

Returns:

Type Description
float

Optimal expansion ratio.

Source code in machwave/core/compressible_flow/nozzle.py
def get_optimal_expansion_ratio(
    k: float, chamber_pressure: float, atmospheric_pressure: float
) -> float:
    """Get optimal expansion ratio for DeLaval nozzle.

    Args:
        k: Isentropic exponent.
        chamber_pressure: Chamber pressure [Pa].
        atmospheric_pressure: External pressure [Pa].

    Returns:
        Optimal expansion ratio.
    """
    return (
        (((k + 1) / 2) ** (1 / (k - 1)))
        * ((atmospheric_pressure / chamber_pressure) ** (1 / k))
        * np.sqrt(
            ((k + 1) / (k - 1))
            * (1 - (atmospheric_pressure / chamber_pressure) ** ((k - 1) / k))
        )
    ) ** -1

get_overall_nozzle_efficiency(divergent_loss, kinetics_loss, boundary_layer_loss, two_phase_loss, other_losses)

Calculates the overall nozzle efficiency by combining the loss fractions.

All inputs are loss fractions in [0, 1] (not percentages).

Parameters:

Name Type Description Default
divergent_loss float

The divergent nozzle loss fraction.

required
kinetics_loss float

The kinetics loss fraction.

required
boundary_layer_loss float

The boundary layer loss fraction.

required
two_phase_loss float

The two-phase flow loss fraction.

required
other_losses float

Additional losses, as a fraction in [0, 1].

required

Returns:

Type Description
float

The overall nozzle efficiency, as a fraction in [0, 1].

Source code in machwave/core/compressible_flow/losses.py
@decorators.check_bounds(lower=0.0, upper=1.0)
def get_overall_nozzle_efficiency(
    divergent_loss: float,
    kinetics_loss: float,
    boundary_layer_loss: float,
    two_phase_loss: float,
    other_losses: float,
) -> float:
    """
    Calculates the overall nozzle efficiency by combining the loss fractions.

    All inputs are loss fractions in [0, 1] (not percentages).

    Args:
        divergent_loss: The divergent nozzle loss fraction.
        kinetics_loss: The kinetics loss fraction.
        boundary_layer_loss: The boundary layer loss fraction.
        two_phase_loss: The two-phase flow loss fraction.
        other_losses: Additional losses, as a fraction in [0, 1].

    Returns:
        The overall nozzle efficiency, as a fraction in [0, 1].
    """
    return 1.0 - (
        divergent_loss
        + kinetics_loss
        + boundary_layer_loss
        + two_phase_loss
        + other_losses
    )

get_thrust_from_thrust_coefficient(thrust_coefficient, chamber_pressure, nozzle_throat_area)

Get thrust from thrust coefficient.

Parameters:

Name Type Description Default
thrust_coefficient float

Thrust coefficient.

required
chamber_pressure float

Chamber stagnation pressure [Pa].

required
nozzle_throat_area float

Nozzle throat area [m^2].

required

Returns:

Type Description
float

Thrust [N].

Source code in machwave/core/compressible_flow/nozzle.py
def get_thrust_from_thrust_coefficient(
    thrust_coefficient: float, chamber_pressure: float, nozzle_throat_area: float
) -> float:
    """Get thrust from thrust coefficient.

    Args:
        thrust_coefficient: Thrust coefficient.
        chamber_pressure: Chamber stagnation pressure [Pa].
        nozzle_throat_area: Nozzle throat area [m^2].

    Returns:
        Thrust [N].
    """
    return thrust_coefficient * chamber_pressure * nozzle_throat_area

get_two_phase_flow_loss_fraction(chamber_pressure_psi, mass_fraction_of_condensed_phase, expansion_ratio, throat_diameter_inch, characteristic_length_inch)

Two-phase flow loss accounts for the decrement in performance due to the presence of a condensed phase in the combustion products.

Valid for solid, and hybrid propellants.

The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a fraction [0, 1].

Parameters:

Name Type Description Default
chamber_pressure_psi float

The chamber pressure [psi].

required
mass_fraction_of_condensed_phase float

The mass fraction of the condensed phase.

required
expansion_ratio float

The expansion ratio of the nozzle.

required
throat_diameter_inch float

The throat diameter [in].

required
characteristic_length_inch float

The characteristic length [in].

required

Returns: The two-phase flow loss fraction in [0, 1].

Source code in machwave/core/compressible_flow/losses.py
@decorators.check_bounds(lower=0.0, upper=1.0)
@decorators.warn_if_outside_range(**TYPICAL_RANGES["two_phase_flow_loss"])
def get_two_phase_flow_loss_fraction(
    chamber_pressure_psi: float,
    mass_fraction_of_condensed_phase: float,
    expansion_ratio: float,
    throat_diameter_inch: float,
    characteristic_length_inch: float,
) -> float:
    """
    Two-phase flow loss accounts for the decrement in performance due to
    the presence of a condensed phase in the combustion products.

    Valid for solid, and hybrid propellants.

    The source AFRPL-TR-75-36 calculates it as a percentage, here it is converted to a
    fraction [0, 1].

    Args:
        chamber_pressure_psi: The chamber pressure [psi].
        mass_fraction_of_condensed_phase: The mass fraction of the condensed phase.
        expansion_ratio: The expansion ratio of the nozzle.
        throat_diameter_inch: The throat diameter [in].
        characteristic_length_inch: The characteristic length [in].
    Returns:
        The two-phase flow loss fraction in [0, 1].
    """
    particle_size_um: float = _get_two_phase_phase_loss_particle_size(
        chamber_pressure_psi,
        mass_fraction_of_condensed_phase,
        throat_diameter_inch,
        characteristic_length_inch,
    )

    if mass_fraction_of_condensed_phase >= 0.09:
        c_4 = 0.5
        if throat_diameter_inch < 1.0:
            c_3, c_5, c_6 = 9.0, 1.0, 1.0
        elif throat_diameter_inch < 2.0:
            c_3, c_5, c_6 = 9.0, 1.0, 0.8
        else:  # throat_diameter_inch >= 2
            if particle_size_um < 4.0:
                c_3, c_5, c_6 = 13.4, 0.8, 0.8
            elif particle_size_um <= 8.0:
                c_3, c_5, c_6 = 10.2, 0.8, 0.4
            else:
                c_3, c_5, c_6 = 7.58, 0.8, 0.33
    else:  # mass_fraction_of_condensed_phase < 0.09
        c_4 = 1.0
        if throat_diameter_inch < 1.0:
            c_3, c_5, c_6 = 30.0, 1.0, 1.0
        elif throat_diameter_inch < 2.0:
            c_3, c_5, c_6 = 30.0, 1.0, 0.8
        else:  # throat_diameter_inch >= 2
            if particle_size_um < 4.0:
                c_3, c_5, c_6 = 44.5, 0.8, 0.8
            elif particle_size_um <= 8.0:
                c_3, c_5, c_6 = 34.0, 0.8, 0.4
            else:
                c_3, c_5, c_6 = 25.2, 0.8, 0.33

    numerator = (mass_fraction_of_condensed_phase**c_4) * (particle_size_um**c_5)
    denominator = (
        (chamber_pressure_psi**0.15)
        * (expansion_ratio**0.08)
        * (throat_diameter_inch**c_6)
    )

    return 0.01 * c_3 * numerator / denominator

is_flow_choked(chamber_pressure, external_pressure, critical_pressure_ratio)

Check if flow is choked.

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required
external_pressure float

External pressure [Pa].

required
critical_pressure_ratio float

Critical pressure ratio.

required

Returns:

Type Description
bool

True if flow is choked, False otherwise.

Source code in machwave/core/compressible_flow/isentropic.py
def is_flow_choked(
    chamber_pressure: float,
    external_pressure: float,
    critical_pressure_ratio: float,
) -> bool:
    """Check if flow is choked.

    Args:
        chamber_pressure: Chamber pressure [Pa].
        external_pressure: External pressure [Pa].
        critical_pressure_ratio: Critical pressure ratio.

    Returns:
        True if flow is choked, False otherwise.
    """
    return chamber_pressure >= external_pressure / critical_pressure_ratio