Skip to content

models.propellants

Propellant definition and thermochemical evaluation. A propellant is built from PropellantComponent instances (each with a chemical formula, density, enthalpy, and role), then evaluated at operating conditions via NASA CEA to obtain γ, molecular weight, adiabatic flame temperature, Isp, and condensed-phase fractions.

Submodules:

  • categoriesSolidPropellant (with St. Robert’s burn rate law) and BiliquidPropellant (with O/F ratio)
  • formulations — Ready-to-use propellant instances and JSON loader

machwave.models.propellants

Propellant models and components.

BiliquidPropellant

Bases: Propellant

Biliquid propellant with separate oxidizer and fuel.

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

    mixture_type = propellant_base.MixtureType.BILIQUID

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

        Args:
            name: Propellant name.
            components: Chemical components (should be exactly 2: oxidizer and fuel).
            oxidizer_to_fuel_ratio: Oxidizer-to-fuel mass ratio for this
                formulation. Used as the default mixture_ratio at the
                thermochemical service layer; callers can override per-call
                via ``evaluate(mixture_ratio=...)``.

        Raises:
            PropellantValidationError: If components do not include exactly
                one oxidizer and one fuel.
        """
        super().__init__(
            name=name,
            components=components,
        )

        self.oxidizer_to_fuel_ratio = oxidizer_to_fuel_ratio
        self._validate_components()

    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 propellant_base.PropellantValidationError(
                f"Biliquid propellant '{self.name}' requires exactly two components"
            )

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

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

    def _get_oxidizer(self) -> propellant_components.PropellantComponent:
        """Get the oxidizer component."""
        for c in self.components:
            if c.role == propellant_components.ComponentRole.OXIDIZER:
                return c
        raise propellant_base.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 cea_service.create_cea_service(
            oxidizer_name=oxidizer.name,
            fuel_name=fuel.name,
            oxidizer_to_fuel_ratio=self.oxidizer_to_fuel_ratio,
            has_condensed_phase=self.has_condensed_phase,
        )

__init__(name, components=None, oxidizer_to_fuel_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
oxidizer_to_fuel_ratio float | None

Oxidizer-to-fuel mass ratio for this formulation. Used as the default mixture_ratio at the thermochemical service layer; callers can override per-call via evaluate(mixture_ratio=...).

None

Raises:

Type Description
PropellantValidationError

If components do not include exactly one oxidizer and one fuel.

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

    Args:
        name: Propellant name.
        components: Chemical components (should be exactly 2: oxidizer and fuel).
        oxidizer_to_fuel_ratio: Oxidizer-to-fuel mass ratio for this
            formulation. Used as the default mixture_ratio at the
            thermochemical service layer; callers can override per-call
            via ``evaluate(mixture_ratio=...)``.

    Raises:
        PropellantValidationError: If components do not include exactly
            one oxidizer and one fuel.
    """
    super().__init__(
        name=name,
        components=components,
    )

    self.oxidizer_to_fuel_ratio = oxidizer_to_fuel_ratio
    self._validate_components()

ComponentRole

Bases: StrEnum

Role of a chemical component in the propellant formulation.

Source code in machwave/models/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"

MixtureType

Bases: StrEnum

Type of propellant mixture.

Source code in machwave/models/propellants/categories/base.py
class MixtureType(enum.StrEnum):
    """Type of propellant mixture."""

    SOLID = "solid"
    BILIQUID = "biliquid"

Propellant

Bases: ABC

Base class for propellant formulations.

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

    mixture_type: MixtureType

    # Chamber pressure is quantized to this width [Pa] before evaluation.
    CHAMBER_PRESSURE_QUANTIZATION_PA: float = 200.0

    # Mixture ratio (O/F) is quantized to this width before evaluation.
    MIXTURE_RATIO_QUANTIZATION: float = 1e-3

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

        Args:
            name: Propellant name.
            components: Chemical components. If None, defaults to empty list.
        """
        self.name = name
        self.components = list(components or [])
        self._evaluation_cache: dict = {}

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

    @property
    def has_condensed_phase(self) -> bool:
        """
        Whether this propellant can form a condensed combustion phase.

        A propellant built solely from elements in `GASEOUS_ONLY_ELEMENTS` cannot form a
        condensed phase.
        """
        return any(
            element.strip().upper() not in GASEOUS_ONLY_ELEMENTS
            for component in self.components
            for element in component.chemical_formula
        )

    @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 _compute_thermochemical_properties(
        self,
        chamber_pressure: float,
        expansion_ratio: float,
        mixture_ratio: float | None,
    ) -> propellant_properties.ThermochemicalProperties:
        """Query the thermochemical service at the given conditions."""
        service = self.thermochemical_service

        adiabatic_flame_temperature = service.get_adiabatic_flame_temperature(
            chamber_pressure=chamber_pressure, mixture_ratio=mixture_ratio
        )

        molecular_weight_chamber, k_chamber = service.get_chamber_properties(
            chamber_pressure=chamber_pressure,
            expansion_ratio=expansion_ratio,
            mixture_ratio=mixture_ratio,
        )

        molecular_weight_exhaust, k_exhaust = service.get_exhaust_properties(
            chamber_pressure=chamber_pressure,
            expansion_ratio=expansion_ratio,
            mixture_ratio=mixture_ratio,
        )

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

        return propellant_properties.ThermochemicalProperties(
            k_chamber=k_chamber,
            k_exhaust=k_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,
        )

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

        Chamber pressure is quantized to `CHAMBER_PRESSURE_QUANTIZATION_PA` and
        mixture ratio to `MIXTURE_RATIO_QUANTIZATION`; the result is cached per
        `(chamber_pressure, expansion_ratio, mixture_ratio)`.

        Args:
            chamber_pressure: Chamber pressure [Pa].
            expansion_ratio: Nozzle area ratio (Ae/At).
            mixture_ratio: Ratio of the propellant mixture.

        Returns:
            ThermochemicalProperties.

        Raises:
            ValueError: If evaluation fails.
        """
        quantized_chamber_pressure = (
            round(chamber_pressure / self.CHAMBER_PRESSURE_QUANTIZATION_PA)
            * self.CHAMBER_PRESSURE_QUANTIZATION_PA
        )
        quantized_mixture_ratio = (
            round(mixture_ratio / self.MIXTURE_RATIO_QUANTIZATION)
            * self.MIXTURE_RATIO_QUANTIZATION
            if mixture_ratio is not None
            else None
        )
        cache_key = (
            quantized_chamber_pressure,
            expansion_ratio,
            quantized_mixture_ratio,
        )
        cached_properties = self._evaluation_cache.get(cache_key)
        if cached_properties is not None:
            return cached_properties

        properties = self._compute_thermochemical_properties(
            chamber_pressure=quantized_chamber_pressure,
            expansion_ratio=expansion_ratio,
            mixture_ratio=quantized_mixture_ratio,
        )
        self._evaluation_cache[cache_key] = properties
        return properties

has_condensed_phase property

Whether this propellant can form a condensed combustion phase.

A propellant built solely from elements in GASEOUS_ONLY_ELEMENTS cannot form a condensed phase.

thermochemical_service cached property

Get thermochemical service, cached.

__init__(name, components=None)

Initialize a propellant.

Parameters:

Name Type Description Default
name str

Propellant name.

required
components list[PropellantComponent] | None

Chemical components. If None, defaults to empty list.

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

    Args:
        name: Propellant name.
        components: Chemical components. If None, defaults to empty list.
    """
    self.name = name
    self.components = list(components or [])
    self._evaluation_cache: dict = {}

evaluate(chamber_pressure, expansion_ratio=8.0, mixture_ratio=None)

Evaluate thermochemical properties at given conditions.

Chamber pressure is quantized to CHAMBER_PRESSURE_QUANTIZATION_PA and mixture ratio to MIXTURE_RATIO_QUANTIZATION; the result is cached per (chamber_pressure, expansion_ratio, mixture_ratio).

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure [Pa].

required
expansion_ratio float

Nozzle area ratio (Ae/At).

8.0
mixture_ratio float | None

Ratio of the propellant mixture.

None

Returns:

Type Description
ThermochemicalProperties

ThermochemicalProperties.

Raises:

Type Description
ValueError

If evaluation fails.

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

    Chamber pressure is quantized to `CHAMBER_PRESSURE_QUANTIZATION_PA` and
    mixture ratio to `MIXTURE_RATIO_QUANTIZATION`; the result is cached per
    `(chamber_pressure, expansion_ratio, mixture_ratio)`.

    Args:
        chamber_pressure: Chamber pressure [Pa].
        expansion_ratio: Nozzle area ratio (Ae/At).
        mixture_ratio: Ratio of the propellant mixture.

    Returns:
        ThermochemicalProperties.

    Raises:
        ValueError: If evaluation fails.
    """
    quantized_chamber_pressure = (
        round(chamber_pressure / self.CHAMBER_PRESSURE_QUANTIZATION_PA)
        * self.CHAMBER_PRESSURE_QUANTIZATION_PA
    )
    quantized_mixture_ratio = (
        round(mixture_ratio / self.MIXTURE_RATIO_QUANTIZATION)
        * self.MIXTURE_RATIO_QUANTIZATION
        if mixture_ratio is not None
        else None
    )
    cache_key = (
        quantized_chamber_pressure,
        expansion_ratio,
        quantized_mixture_ratio,
    )
    cached_properties = self._evaluation_cache.get(cache_key)
    if cached_properties is not None:
        return cached_properties

    properties = self._compute_thermochemical_properties(
        chamber_pressure=quantized_chamber_pressure,
        expansion_ratio=expansion_ratio,
        mixture_ratio=quantized_mixture_ratio,
    )
    self._evaluation_cache[cache_key] = properties
    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^3].

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/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^3].
        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 = conversions.convert_joules_per_mol_to_cal_per_mol(
            self.enthalpy
        )
        density_gcc = conversions.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/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 = conversions.convert_joules_per_mol_to_cal_per_mol(
        self.enthalpy
    )
    density_gcc = conversions.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/propellants/categories/solid.py
class SolidPropellant(propellant_base.Propellant):
    """Solid propellant with burn rate model."""

    mixture_type = propellant_base.MixtureType.SOLID

    def __init__(
        self,
        name: str,
        components: list[propellant_components.PropellantComponent] | None = None,
        mass_fractions: list[float] | None = None,
        properties: propellant_properties.ThermochemicalProperties | None = None,
        burn_rate_map: list[dict[str, float | int]] | None = None,
    ):
        """
        Initialize a solid propellant.

        Args:
            name: Propellant name.
            components: Chemical components. Required; must contain at least
                one oxidizer and one fuel.
            mass_fractions: Mass fractions aligned with `components`. Must
                sum to 1.0.
            properties: Pre-defined thermochemical properties. Optional
                override used by `evaluate()` to skip CEA when provided.
            burn_rate_map: Saint Robert's law coefficients by pressure range.

        Raises:
            PropellantValidationError: If components, mass_fractions, or
                their relationship is invalid.
        """
        super().__init__(
            name=name,
            components=components,
        )

        self._properties = properties
        self.burn_rate_map = burn_rate_map or []
        self.mass_fractions = mass_fractions or []
        self._validate_components()

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

    def _validate_components(self):
        """
        Validate solid propellant components and mass fractions.

        Raises:
            PropellantValidationError: If validation fails.
        """
        if not self.components:
            raise propellant_base.PropellantValidationError(
                f"Solid propellant '{self.name}' requires `components`"
            )

        if not self.mass_fractions:
            raise propellant_base.PropellantValidationError(
                f"Solid propellant '{self.name}' requires `mass_fractions` for its "
                "components"
            )
        if len(self.mass_fractions) != len(self.components):
            raise propellant_base.PropellantValidationError(
                f"Solid propellant '{self.name}' `mass_fractions` length must match "
                "components length"
            )
        if any(mf < 0 for mf in self.mass_fractions):
            raise propellant_base.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 propellant_base.PropellantValidationError(
                f"Solid propellant '{self.name}' `mass_fractions` must sum to 1.0 (got "
                f"{mf_sum:.6f})"
            )

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

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

    def _get_thermochemical_service(self):
        """
        Create a RocketCEA service from this propellant's components.

        Returns:
            RocketCEAService instance.
        """
        components_data = [
            comp.to_cea_dict(weight_percent=mf * 100.0)
            for comp, mf in zip(self.components, self.mass_fractions)
        ]
        card_string = cea_service.generate_card_string(components_data)
        return cea_service.create_cea_service(
            propellant_name=cea_service.normalize_custom_propellant_name(self.name),
            card_string=card_string,
            has_condensed_phase=self.has_condensed_phase,
        )

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

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

        Args:
            chamber_pressure: Chamber pressure [Pa].
            expansion_ratio: Nozzle area expansion ratio (Ae/At).
            mixture_ratio: Per-call mixture ratio override. Unused for solid
                formulations; accepted for parent-class compatibility.

        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, mixture_ratio)

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

        Uses a harmonic mean based on solid mixture mass fractions.
        """
        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:
        """
        Return the instantaneous burn rate of the solid propellant.

        Uses Saint 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
        drawn from `burn_rate_map`.

        Args:
            chamber_pressure: Chamber pressure [Pa].

        Returns:
            Burn rate [m/s].

        Raises:
            BurnRateOutOfBoundsError: If pressure is outside the valid range.
            PropellantValidationError: If the burn rate model is not defined.
        """
        if not self.burn_rate_map:
            raise propellant_base.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

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

Uses a harmonic mean based on solid mixture mass fractions.

properties property

Expose pre-defined thermochemical properties when present.

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

Initialize a solid propellant.

Parameters:

Name Type Description Default
name str

Propellant name.

required
components list[PropellantComponent] | None

Chemical components. Required; must contain at least one oxidizer and one fuel.

None
mass_fractions list[float] | None

Mass fractions aligned with components. Must sum to 1.0.

None
properties ThermochemicalProperties | None

Pre-defined thermochemical properties. Optional override used by evaluate() to skip CEA when provided.

None
burn_rate_map list[dict[str, float | int]] | None

Saint Robert's law coefficients by pressure range.

None

Raises:

Type Description
PropellantValidationError

If components, mass_fractions, or their relationship is invalid.

Source code in machwave/models/propellants/categories/solid.py
def __init__(
    self,
    name: str,
    components: list[propellant_components.PropellantComponent] | None = None,
    mass_fractions: list[float] | None = None,
    properties: propellant_properties.ThermochemicalProperties | None = None,
    burn_rate_map: list[dict[str, float | int]] | None = None,
):
    """
    Initialize a solid propellant.

    Args:
        name: Propellant name.
        components: Chemical components. Required; must contain at least
            one oxidizer and one fuel.
        mass_fractions: Mass fractions aligned with `components`. Must
            sum to 1.0.
        properties: Pre-defined thermochemical properties. Optional
            override used by `evaluate()` to skip CEA when provided.
        burn_rate_map: Saint Robert's law coefficients by pressure range.

    Raises:
        PropellantValidationError: If components, mass_fractions, or
            their relationship is invalid.
    """
    super().__init__(
        name=name,
        components=components,
    )

    self._properties = properties
    self.burn_rate_map = burn_rate_map or []
    self.mass_fractions = mass_fractions or []
    self._validate_components()

evaluate(chamber_pressure, expansion_ratio=8.0, mixture_ratio=None)

Evaluate thermochemical properties.

If properties are pre-defined, returns them directly. Otherwise, evaluates using the thermochemical service via the 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
mixture_ratio float | None

Per-call mixture ratio override. Unused for solid formulations; accepted for parent-class compatibility.

None

Returns:

Type Description
ThermochemicalProperties

Pre-defined or calculated properties.

Raises:

Type Description
PropellantValidationError

If evaluation fails.

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

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

    Args:
        chamber_pressure: Chamber pressure [Pa].
        expansion_ratio: Nozzle area expansion ratio (Ae/At).
        mixture_ratio: Per-call mixture ratio override. Unused for solid
            formulations; accepted for parent-class compatibility.

    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, mixture_ratio)

get_burn_rate(chamber_pressure)

Return the instantaneous burn rate of the solid propellant.

Uses Saint 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 drawn from burn_rate_map.

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 the valid range.

PropellantValidationError

If the burn rate model is not defined.

Source code in machwave/models/propellants/categories/solid.py
def get_burn_rate(self, chamber_pressure: float) -> float:
    """
    Return the instantaneous burn rate of the solid propellant.

    Uses Saint 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
    drawn from `burn_rate_map`.

    Args:
        chamber_pressure: Chamber pressure [Pa].

    Returns:
        Burn rate [m/s].

    Raises:
        BurnRateOutOfBoundsError: If pressure is outside the valid range.
        PropellantValidationError: If the burn rate model is not defined.
    """
    if not self.burn_rate_map:
        raise propellant_base.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 of chemical-rocket combustion products.

Attributes:

Name Type Description
k_chamber float

Isentropic exponent in chamber.

k_exhaust float

Isentropic exponent at nozzle exit.

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.

qsi_exhaust float

Condensed phase species content at exit.

Raises:

Type Description
ValueError

If any parameter is outside a valid range.

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

    Attributes:
        k_chamber: Isentropic exponent in chamber.
        k_exhaust: Isentropic exponent at nozzle exit.
        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.
        qsi_exhaust: Condensed phase species content at exit.

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

    k_chamber: float
    k_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/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})"
        )