Skip to content

models.propulsion.propellants

machwave.models.propulsion.propellants

Propellant models and components.

BiliquidPropellant

Bases: Propellant

Biliquid propellant with separate oxidizer and fuel.

Source code in machwave/models/propulsion/propellants/categories/biliquid.py
class BiliquidPropellant(Propellant):
    """Biliquid propellant with separate oxidizer and fuel."""

    mixture_type = MixtureType.BILIQUID

    def __init__(
        self,
        name: str,
        components: list[PropellantComponent] | None = None,
        combustion_efficiency: float = 0.95,
        properties: ThermochemicalProperties | None = None,
        of_ratio: float | None = None,
    ):
        """Initialize biliquid propellant.

        Args:
            name: Propellant name.
            components: Chemical components (should be exactly 2: oxidizer and fuel).
            combustion_efficiency: Efficiency factor (0-1).
            properties: Pre-defined thermochemical properties (optional).
            of_ratio: Oxidizer-to-fuel mass ratio.
        """
        super().__init__(
            name=name,
            components=components,
            combustion_efficiency=combustion_efficiency,
        )
        self.properties = properties
        self.of_ratio = of_ratio
        self.oxidizer_tank_density: float = 0.0
        self.fuel_tank_density: float = 0.0

    def _validate_components(self):
        """Validate biliquid has exactly 2 components: oxidizer and fuel.

        Raises:
            PropellantValidationError: If validation fails.
        """
        if not self.components or len(self.components) != 2:
            raise PropellantValidationError(
                f"Biliquid propellant '{self.name}' requires exactly two components"
            )

        _ = self._get_fuel()  # check fuel
        _ = self._get_oxidizer()  # check oxidizer

    def _get_fuel(self) -> PropellantComponent:
        """Get the fuel component."""
        for c in self.components:
            if c.role == ComponentRole.FUEL:
                return c
        raise PropellantValidationError(
            f"Biliquid propellant '{self.name}' has no fuel component"
        )

    def _get_oxidizer(self) -> PropellantComponent:
        """Get the oxidizer component."""
        for c in self.components:
            if c.role == ComponentRole.OXIDIZER:
                return c
        raise PropellantValidationError(
            f"Biliquid propellant '{self.name}' has no oxidizer component"
        )

    def _get_thermochemical_service(self):
        """Create RocketCEA service for biliquid propellant.

        Returns:
            RocketCEAService instance.
        """
        fuel = self._get_fuel()
        oxidizer = self._get_oxidizer()

        return create_cea_service(
            oxidizer_name=oxidizer.name,
            fuel_name=fuel.name,
            oxidizer_to_fuel_ratio=self.of_ratio,
        )

__init__(name, components=None, combustion_efficiency=0.95, properties=None, of_ratio=None)

Initialize biliquid propellant.

Parameters:

Name Type Description Default
name str

Propellant name.

required
components list[PropellantComponent] | None

Chemical components (should be exactly 2: oxidizer and fuel).

None
combustion_efficiency float

Efficiency factor (0-1).

0.95
properties ThermochemicalProperties | None

Pre-defined thermochemical properties (optional).

None
of_ratio float | None

Oxidizer-to-fuel mass ratio.

None
Source code in machwave/models/propulsion/propellants/categories/biliquid.py
def __init__(
    self,
    name: str,
    components: list[PropellantComponent] | None = None,
    combustion_efficiency: float = 0.95,
    properties: ThermochemicalProperties | None = None,
    of_ratio: float | None = None,
):
    """Initialize biliquid propellant.

    Args:
        name: Propellant name.
        components: Chemical components (should be exactly 2: oxidizer and fuel).
        combustion_efficiency: Efficiency factor (0-1).
        properties: Pre-defined thermochemical properties (optional).
        of_ratio: Oxidizer-to-fuel mass ratio.
    """
    super().__init__(
        name=name,
        components=components,
        combustion_efficiency=combustion_efficiency,
    )
    self.properties = properties
    self.of_ratio = of_ratio
    self.oxidizer_tank_density: float = 0.0
    self.fuel_tank_density: float = 0.0

ComponentRole

Bases: StrEnum

Role of a chemical component in the propellant formulation.

Source code in machwave/models/propulsion/propellants/components.py
class ComponentRole(enum.StrEnum):
    """Role of a chemical component in the propellant formulation."""

    OXIDIZER = "oxidizer"
    FUEL = "fuel"  # Includes binders (HTPB, PBAN, etc.)
    ADDITIVE = "additive"

Propellant

Bases: ABC

Base class for propellant formulations.

Source code in machwave/models/propulsion/propellants/categories/base.py
class Propellant(abc.ABC):
    """Base class for propellant formulations."""

    mixture_type: MixtureType

    def __init__(
        self,
        name: str,
        components: list[PropellantComponent] | None = None,
        combustion_efficiency: float = 0.95,
    ):
        """Initialize propellant.

        Args:
            name: Propellant name.
            components: Chemical components. If None, defaults to empty list.
            combustion_efficiency: Efficiency factor (0-1).
        """
        self.name = name
        self.components = list(components) if components is not None else []
        self.combustion_efficiency = combustion_efficiency

    @functools.cached_property
    def thermochemical_service(self) -> cea_service.RocketCEAService:
        """Get thermochemical service, cached."""
        return self._get_thermochemical_service()

    @abc.abstractmethod
    def _validate_components(self):
        """Validate components meet propellant type requirements.

        Raises:
            PropellantValidationError: If validation fails.
        """
        pass

    @abc.abstractmethod
    def _get_thermochemical_service(self) -> cea_service.RocketCEAService:
        """Create thermochemical service for this propellant.

        Implemented for every subclass of propellant category, based on how the CEA
        object is constructed (from components, from properties, others).

        Returns:
            RocketCEAService instance.

        Raises:
            PropellantValidationError: If service creation fails.
        """
        pass

    def evaluate(
        self, chamber_pressure: float, expansion_ratio: float = 8.0
    ) -> ThermochemicalProperties:
        """Evaluate thermochemical properties at given conditions.

        Args:
            chamber_pressure: Chamber pressure [Pa].
            expansion_ratio: Nozzle area ratio (Ae/At).

        Returns:
            ThermochemicalProperties.

        Raises:
            ValueError: If evaluation fails.
        """
        self._validate_components()
        service = self.thermochemical_service

        adiabatic_flame_temperature = service.get_adiabatic_flame_temperature(
            chamber_pressure=chamber_pressure
        )

        molecular_weight_chamber, gamma_chamber = service.get_chamber_properties(
            chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
        )

        molecular_weight_exhaust, gamma_exhaust = service.get_exhaust_properties(
            chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
        )

        i_sp_frozen, i_sp_shifting = service.get_specific_impulse(
            chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
        )
        qsi_chamber, qsi_exhaust = service.get_condensed_phase_fractions(
            chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
        )

        properties = ThermochemicalProperties(
            gamma_chamber=gamma_chamber,
            gamma_exhaust=gamma_exhaust,
            adiabatic_flame_temperature=adiabatic_flame_temperature,
            molecular_weight_chamber=molecular_weight_chamber,
            molecular_weight_exhaust=molecular_weight_exhaust,
            i_sp_frozen=i_sp_frozen,
            i_sp_shifting=i_sp_shifting,
            qsi_chamber=qsi_chamber,
            qsi_exhaust=qsi_exhaust,
        )
        return properties

thermochemical_service cached property

Get thermochemical service, cached.

__init__(name, components=None, combustion_efficiency=0.95)

Initialize propellant.

Parameters:

Name Type Description Default
name str

Propellant name.

required
components list[PropellantComponent] | None

Chemical components. If None, defaults to empty list.

None
combustion_efficiency float

Efficiency factor (0-1).

0.95
Source code in machwave/models/propulsion/propellants/categories/base.py
def __init__(
    self,
    name: str,
    components: list[PropellantComponent] | None = None,
    combustion_efficiency: float = 0.95,
):
    """Initialize propellant.

    Args:
        name: Propellant name.
        components: Chemical components. If None, defaults to empty list.
        combustion_efficiency: Efficiency factor (0-1).
    """
    self.name = name
    self.components = list(components) if components is not None else []
    self.combustion_efficiency = combustion_efficiency

evaluate(chamber_pressure, expansion_ratio=8.0)

Evaluate thermochemical properties at given conditions.

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required
expansion_ratio float

Nozzle area ratio (Ae/At).

8.0

Returns:

Type Description
ThermochemicalProperties

ThermochemicalProperties.

Raises:

Type Description
ValueError

If evaluation fails.

Source code in machwave/models/propulsion/propellants/categories/base.py
def evaluate(
    self, chamber_pressure: float, expansion_ratio: float = 8.0
) -> ThermochemicalProperties:
    """Evaluate thermochemical properties at given conditions.

    Args:
        chamber_pressure: Chamber pressure [Pa].
        expansion_ratio: Nozzle area ratio (Ae/At).

    Returns:
        ThermochemicalProperties.

    Raises:
        ValueError: If evaluation fails.
    """
    self._validate_components()
    service = self.thermochemical_service

    adiabatic_flame_temperature = service.get_adiabatic_flame_temperature(
        chamber_pressure=chamber_pressure
    )

    molecular_weight_chamber, gamma_chamber = service.get_chamber_properties(
        chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
    )

    molecular_weight_exhaust, gamma_exhaust = service.get_exhaust_properties(
        chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
    )

    i_sp_frozen, i_sp_shifting = service.get_specific_impulse(
        chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
    )
    qsi_chamber, qsi_exhaust = service.get_condensed_phase_fractions(
        chamber_pressure=chamber_pressure, expansion_ratio=expansion_ratio
    )

    properties = ThermochemicalProperties(
        gamma_chamber=gamma_chamber,
        gamma_exhaust=gamma_exhaust,
        adiabatic_flame_temperature=adiabatic_flame_temperature,
        molecular_weight_chamber=molecular_weight_chamber,
        molecular_weight_exhaust=molecular_weight_exhaust,
        i_sp_frozen=i_sp_frozen,
        i_sp_shifting=i_sp_shifting,
        qsi_chamber=qsi_chamber,
        qsi_exhaust=qsi_exhaust,
    )
    return properties

PropellantComponent dataclass

Chemical component of a propellant formulation.

Attributes:

Name Type Description
name str

Component name (i.e., "KNO3", "LOX", "HTPB").

role ComponentRole

Component role (oxidizer, fuel, additive).

density float

Component density [kg/m³].

chemical_formula dict[str, int]

Element symbols to atom counts (i.e., {"H": 2, "O": 1}).

enthalpy float

Standard enthalpy of formation [J/mol].

initial_temperature float

Initial component temperature before combustion [K].

Source code in machwave/models/propulsion/propellants/components.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class PropellantComponent:
    """Chemical component of a propellant formulation.

    Attributes:
        name: Component name (i.e., "KNO3", "LOX", "HTPB").
        role: Component role (oxidizer, fuel, additive).
        density: Component density [kg/m³].
        chemical_formula: Element symbols to atom counts (i.e., {"H": 2, "O": 1}).
        enthalpy: Standard enthalpy of formation [J/mol].
        initial_temperature: Initial component temperature before combustion [K].
    """

    name: str
    role: ComponentRole
    density: float
    chemical_formula: dict[str, int]
    enthalpy: float
    initial_temperature: float = 298.15

    def to_cea_dict(self, *, weight_percent: float) -> dict:
        """Convert component to CEA-compatible dictionary format.

        Args:
            weight_percent: Component weight percent in the mixture [%].

        Returns:
            CEA format with name, formula, weight_percent,
            heat_of_formation (cal/mol), temperature [K], and density [g/cc].
        """
        heat_of_formation_cal = convert_joules_per_mol_to_cal_per_mol(self.enthalpy)
        density_gcc = convert_kgm3_to_gcc(self.density)

        return {
            "name": self.name,
            "formula": self.chemical_formula,
            "weight_percent": weight_percent,
            "heat_of_formation": heat_of_formation_cal,
            "temperature": self.initial_temperature,
            "density": density_gcc,
        }

to_cea_dict(*, weight_percent)

Convert component to CEA-compatible dictionary format.

Parameters:

Name Type Description Default
weight_percent float

Component weight percent in the mixture [%].

required

Returns:

Type Description
dict

CEA format with name, formula, weight_percent,

dict

heat_of_formation (cal/mol), temperature [K], and density [g/cc].

Source code in machwave/models/propulsion/propellants/components.py
def to_cea_dict(self, *, weight_percent: float) -> dict:
    """Convert component to CEA-compatible dictionary format.

    Args:
        weight_percent: Component weight percent in the mixture [%].

    Returns:
        CEA format with name, formula, weight_percent,
        heat_of_formation (cal/mol), temperature [K], and density [g/cc].
    """
    heat_of_formation_cal = convert_joules_per_mol_to_cal_per_mol(self.enthalpy)
    density_gcc = convert_kgm3_to_gcc(self.density)

    return {
        "name": self.name,
        "formula": self.chemical_formula,
        "weight_percent": weight_percent,
        "heat_of_formation": heat_of_formation_cal,
        "temperature": self.initial_temperature,
        "density": density_gcc,
    }

SolidPropellant

Bases: Propellant

Solid propellant with burn rate model.

Source code in machwave/models/propulsion/propellants/categories/solid.py
class SolidPropellant(Propellant):
    """Solid propellant with burn rate model."""

    mixture_type = MixtureType.SOLID

    def __init__(
        self,
        name: str,
        components: list[PropellantComponent] | None = None,
        mass_fractions: list[float] | None = None,
        combustion_efficiency: float = 0.95,
        properties: ThermochemicalProperties | None = None,
        burn_rate_map: list[dict[str, float | int]] | None = None,
    ):
        """Initialize solid propellant. If components are not provided,
        properties must be defined and vice-versa.

        Args:
            name: Propellant name.
            components: Chemical components (optional).
            combustion_efficiency: Efficiency factor (0-1).
            properties: Pre-defined thermochemical properties (optional).
            burn_rate: St. Robert's law coefficients by pressure range.
        """
        super().__init__(
            name=name,
            components=components,
            combustion_efficiency=combustion_efficiency,
        )
        self._properties = properties
        self.burn_rate_map = burn_rate_map if burn_rate_map is not None else []
        self.mass_fractions = mass_fractions if mass_fractions is not None else []

    @property
    def properties(self) -> ThermochemicalProperties | None:
        """Expose pre-defined thermochemical properties when present."""
        return self._properties

    def _validate_components(self):
        """Validate solid propellant has oxidizer and fuel.

        Raises:
            PropellantValidationError: If validation fails.
        """
        # Allow empty components if properties are pre-defined (for formulations)
        if not self.components:
            if self._properties is None:
                raise PropellantValidationError(
                    f"Solid propellant '{self.name}' has no components or pre-defined "
                    "properties"
                )
            return

        if not self.mass_fractions:
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' requires mass_fractions for its "
                "components"
            )
        if len(self.mass_fractions) != len(self.components):
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' mass_fractions length must match "
                "components length"
            )
        if any(mf < 0 for mf in self.mass_fractions):
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' mass_fractions must be non-negative"
            )
        mf_sum = sum(self.mass_fractions)
        if abs(mf_sum - 1.0) > MASS_FRACTION_SUM_TOLERANCE:
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' mass_fractions must sum to 1.0 (got "
                f"{mf_sum:.6f})"
            )

        has_oxidizer = any(c.role == ComponentRole.OXIDIZER for c in self.components)
        has_fuel = any(c.role == ComponentRole.FUEL for c in self.components)

        if not has_oxidizer:
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' requires at least one oxidizer "
                "component"
            )
        if not has_fuel:
            raise PropellantValidationError(
                f"Solid propellant '{self.name}' requires at least one fuel component"
            )

    def _get_thermochemical_service(self):
        """Create RocketCEA service from components.

        Returns:
            RocketCEAService instance.
        """
        if self.components:
            self._validate_components()
            # Convert components to CEA format
            components_data = [
                comp.to_cea_dict(weight_percent=mf * 100.0)
                for comp, mf in zip(self.components, self.mass_fractions)
            ]
            card_string = generate_card_string(components_data)
            return create_cea_service(
                propellant_name=normalize_custom_propellant_name(self.name),
                card_string=card_string,
            )
        else:
            raise PropellantValidationError(
                f"Cannot create thermochemical service without components for "
                f"propellant '{self.name}'"
            )

    def evaluate(
        self, chamber_pressure: float, expansion_ratio: float = 8.0
    ) -> ThermochemicalProperties:
        """Evaluate thermochemical properties.

        If properties are pre-defined, returns them directly.
        Otherwise, evaluates using the thermochemical service via parent
        class.

        Args:
            chamber_pressure: Chamber pressure [Pa].
            expansion_ratio: Nozzle area expansion ratio (Ae/At).

        Returns:
            Pre-defined or calculated properties.

        Raises:
            PropellantValidationError: If evaluation fails.
        """
        if self._properties is not None:
            return self._properties
        return super().evaluate(chamber_pressure, expansion_ratio)

    @property
    def ideal_density(self) -> float:
        """Calculate ideal propellant density [kg/m^3] for solid mixtures.

        Uses harmonic mean based on solid mixture mass fractions.
        """
        self._validate_components()
        reciprocal_sum = sum(
            mf / comp.density for comp, mf in zip(self.components, self.mass_fractions)
        )
        return 1.0 / reciprocal_sum

    def get_burn_rate(self, chamber_pressure: float) -> float:
        """Calculate instantaneous burn rate for solid propellants.

        Uses St. Robert's law: r = a * P^n, where r is burn rate [m/s],
        P is chamber pressure [MPa], and a, n are empirical coefficients.

        Args:
            chamber_pressure: Chamber pressure [Pa].

        Returns:
            Burn rate [m/s].

        Raises:
            BurnRateOutOfBoundsError: If pressure is outside valid range.
            ValueError: If burn_rate model is not defined.
        """
        if not self.burn_rate_map:
            raise PropellantValidationError(
                f"Burn rate model not defined for propellant '{self.name}'"
            )

        for item in self.burn_rate_map:
            if item["min"] <= chamber_pressure <= item["max"]:
                a = item["a"]
                n = item["n"]
                # Convert pressure from Pa to MPa, apply St. Robert's law,
                # then convert from mm/s to m/s
                return (a * (chamber_pressure * 1e-6) ** n) * 1e-3

        raise BurnRateOutOfBoundsError(chamber_pressure)

ideal_density property

Calculate ideal propellant density [kg/m^3] for solid mixtures.

Uses harmonic mean based on solid mixture mass fractions.

properties property

Expose pre-defined thermochemical properties when present.

__init__(name, components=None, mass_fractions=None, combustion_efficiency=0.95, properties=None, burn_rate_map=None)

Initialize solid propellant. If components are not provided, properties must be defined and vice-versa.

Parameters:

Name Type Description Default
name str

Propellant name.

required
components list[PropellantComponent] | None

Chemical components (optional).

None
combustion_efficiency float

Efficiency factor (0-1).

0.95
properties ThermochemicalProperties | None

Pre-defined thermochemical properties (optional).

None
burn_rate

St. Robert's law coefficients by pressure range.

required
Source code in machwave/models/propulsion/propellants/categories/solid.py
def __init__(
    self,
    name: str,
    components: list[PropellantComponent] | None = None,
    mass_fractions: list[float] | None = None,
    combustion_efficiency: float = 0.95,
    properties: ThermochemicalProperties | None = None,
    burn_rate_map: list[dict[str, float | int]] | None = None,
):
    """Initialize solid propellant. If components are not provided,
    properties must be defined and vice-versa.

    Args:
        name: Propellant name.
        components: Chemical components (optional).
        combustion_efficiency: Efficiency factor (0-1).
        properties: Pre-defined thermochemical properties (optional).
        burn_rate: St. Robert's law coefficients by pressure range.
    """
    super().__init__(
        name=name,
        components=components,
        combustion_efficiency=combustion_efficiency,
    )
    self._properties = properties
    self.burn_rate_map = burn_rate_map if burn_rate_map is not None else []
    self.mass_fractions = mass_fractions if mass_fractions is not None else []

evaluate(chamber_pressure, expansion_ratio=8.0)

Evaluate thermochemical properties.

If properties are pre-defined, returns them directly. Otherwise, evaluates using the thermochemical service via parent class.

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required
expansion_ratio float

Nozzle area expansion ratio (Ae/At).

8.0

Returns:

Type Description
ThermochemicalProperties

Pre-defined or calculated properties.

Raises:

Type Description
PropellantValidationError

If evaluation fails.

Source code in machwave/models/propulsion/propellants/categories/solid.py
def evaluate(
    self, chamber_pressure: float, expansion_ratio: float = 8.0
) -> ThermochemicalProperties:
    """Evaluate thermochemical properties.

    If properties are pre-defined, returns them directly.
    Otherwise, evaluates using the thermochemical service via parent
    class.

    Args:
        chamber_pressure: Chamber pressure [Pa].
        expansion_ratio: Nozzle area expansion ratio (Ae/At).

    Returns:
        Pre-defined or calculated properties.

    Raises:
        PropellantValidationError: If evaluation fails.
    """
    if self._properties is not None:
        return self._properties
    return super().evaluate(chamber_pressure, expansion_ratio)

get_burn_rate(chamber_pressure)

Calculate instantaneous burn rate for solid propellants.

Uses St. Robert's law: r = a * P^n, where r is burn rate [m/s], P is chamber pressure [MPa], and a, n are empirical coefficients.

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required

Returns:

Type Description
float

Burn rate [m/s].

Raises:

Type Description
BurnRateOutOfBoundsError

If pressure is outside valid range.

ValueError

If burn_rate model is not defined.

Source code in machwave/models/propulsion/propellants/categories/solid.py
def get_burn_rate(self, chamber_pressure: float) -> float:
    """Calculate instantaneous burn rate for solid propellants.

    Uses St. Robert's law: r = a * P^n, where r is burn rate [m/s],
    P is chamber pressure [MPa], and a, n are empirical coefficients.

    Args:
        chamber_pressure: Chamber pressure [Pa].

    Returns:
        Burn rate [m/s].

    Raises:
        BurnRateOutOfBoundsError: If pressure is outside valid range.
        ValueError: If burn_rate model is not defined.
    """
    if not self.burn_rate_map:
        raise PropellantValidationError(
            f"Burn rate model not defined for propellant '{self.name}'"
        )

    for item in self.burn_rate_map:
        if item["min"] <= chamber_pressure <= item["max"]:
            a = item["a"]
            n = item["n"]
            # Convert pressure from Pa to MPa, apply St. Robert's law,
            # then convert from mm/s to m/s
            return (a * (chamber_pressure * 1e-6) ** n) * 1e-3

    raise BurnRateOutOfBoundsError(chamber_pressure)

ThermochemicalProperties dataclass

Thermochemical properties for the combustion products of chemical rocket propellants.

Attributes:

Name Type Description
gamma_chamber float

Isentropic exponent in chamber (dimensionless).

gamma_exhaust float

Isentropic exponent at nozzle exit (dimensionless).

adiabatic_flame_temperature float

Ideal combustion temperature [K].

molecular_weight_chamber float

Molecular weight in chamber [kg/mol].

molecular_weight_exhaust float

Molecular weight at exit [kg/mol].

i_sp_frozen float

Frozen flow specific impulse [s].

i_sp_shifting float

Shifting equilibrium specific impulse [s].

qsi_chamber float

Condensed phase species content in chamber [mol/(100g)].

qsi_exhaust float

Condensed phase species content at exit [mol/(100g)].

Raises:

Type Description
ValueError

If any parameter is outside a valid range.

Source code in machwave/models/propulsion/propellants/properties.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class ThermochemicalProperties:
    """Thermochemical properties for the combustion products of chemical rocket
    propellants.

    Attributes:
        gamma_chamber: Isentropic exponent in chamber (dimensionless).
        gamma_exhaust: Isentropic exponent at nozzle exit (dimensionless).
        adiabatic_flame_temperature: Ideal combustion temperature [K].
        molecular_weight_chamber: Molecular weight in chamber [kg/mol].
        molecular_weight_exhaust: Molecular weight at exit [kg/mol].
        i_sp_frozen: Frozen flow specific impulse [s].
        i_sp_shifting: Shifting equilibrium specific impulse [s].
        qsi_chamber: Condensed phase species content in chamber [mol/(100g)].
        qsi_exhaust: Condensed phase species content at exit [mol/(100g)].

    Raises:
        ValueError: If any parameter is outside a valid range.
    """

    gamma_chamber: float
    gamma_exhaust: float
    adiabatic_flame_temperature: float
    molecular_weight_chamber: float
    molecular_weight_exhaust: float
    i_sp_frozen: float
    i_sp_shifting: float
    qsi_chamber: float
    qsi_exhaust: float

    def __post_init__(self) -> None:
        """Validate all properties are within physical bounds."""
        # Typical bounds in VALIDATION_BOUNDS
        for field_name, value in self.__dict__.items():
            if field_name in VALIDATION_BOUNDS:
                min_val, max_val = VALIDATION_BOUNDS[field_name]
                if not (min_val <= value <= max_val):
                    raise ValueError(
                        f"{field_name}={value} outside valid range "
                        f"({min_val}, {max_val}]"
                    )

        # Cross-property validation
        if self.i_sp_shifting < self.i_sp_frozen:
            raise ValueError(
                f"i_sp_shifting ({self.i_sp_shifting}) must be >= "
                f"i_sp_frozen ({self.i_sp_frozen})"
            )

    @functools.cached_property
    def R_chamber(self) -> float:
        """Specific gas constant for chamber [J/(kg-K)]."""
        return scipy.constants.R / self.molecular_weight_chamber

    @functools.cached_property
    def R_exhaust(self) -> float:
        """Specific gas constant for exhaust [J/(kg-K)]."""
        return scipy.constants.R / self.molecular_weight_exhaust

    @functools.cached_property
    def is_two_phase_flow(self) -> bool:
        """Check if combustion products have condensed phase species.

        Returns:
            True if qsi_chamber > 0 or qsi_exhaust > 0.
        """
        return self.qsi_chamber > 0.0 or self.qsi_exhaust > 0.0

R_chamber cached property

Specific gas constant for chamber [J/(kg-K)].

R_exhaust cached property

Specific gas constant for exhaust [J/(kg-K)].

is_two_phase_flow cached property

Check if combustion products have condensed phase species.

Returns:

Type Description
bool

True if qsi_chamber > 0 or qsi_exhaust > 0.

__post_init__()

Validate all properties are within physical bounds.

Source code in machwave/models/propulsion/propellants/properties.py
def __post_init__(self) -> None:
    """Validate all properties are within physical bounds."""
    # Typical bounds in VALIDATION_BOUNDS
    for field_name, value in self.__dict__.items():
        if field_name in VALIDATION_BOUNDS:
            min_val, max_val = VALIDATION_BOUNDS[field_name]
            if not (min_val <= value <= max_val):
                raise ValueError(
                    f"{field_name}={value} outside valid range "
                    f"({min_val}, {max_val}]"
                )

    # Cross-property validation
    if self.i_sp_shifting < self.i_sp_frozen:
        raise ValueError(
            f"i_sp_shifting ({self.i_sp_shifting}) must be >= "
            f"i_sp_frozen ({self.i_sp_frozen})"
        )