Skip to content

models.propulsion.propellants.categories

machwave.models.propulsion.propellants.categories

Propellant categories or mixture types.

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

BurnRateOutOfBoundsError

Bases: Exception

Raised when chamber pressure is outside burn rate model valid range.

Source code in machwave/models/propulsion/propellants/categories/solid.py
class BurnRateOutOfBoundsError(Exception):
    """Raised when chamber pressure is outside burn rate model valid range."""

    def __init__(self, chamber_pressure: float):
        """
        Args:
            chamber_pressure: Chamber pressure that is out of bounds [Pa].
        """
        super().__init__(
            f"Chamber pressure {chamber_pressure:.2e} Pa is outside the valid range "
            f"for this burn rate model."
        )

__init__(chamber_pressure)

Parameters:

Name Type Description Default
chamber_pressure float

Chamber pressure that is out of bounds [Pa].

required
Source code in machwave/models/propulsion/propellants/categories/solid.py
def __init__(self, chamber_pressure: float):
    """
    Args:
        chamber_pressure: Chamber pressure that is out of bounds [Pa].
    """
    super().__init__(
        f"Chamber pressure {chamber_pressure:.2e} Pa is outside the valid range "
        f"for this burn rate model."
    )

MixtureType

Bases: StrEnum

Type of propellant mixture.

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

PropellantValidationError

Bases: Exception

Raised when propellant validation fails.

Source code in machwave/models/propulsion/propellants/categories/base.py
class PropellantValidationError(Exception):
    """Raised when propellant validation fails."""

    def __init__(self, message: str):
        """
        Args:
            message: Description of the validation error.
        """
        super().__init__(f"Propellant validation error: {message}")

__init__(message)

Parameters:

Name Type Description Default
message str

Description of the validation error.

required
Source code in machwave/models/propulsion/propellants/categories/base.py
def __init__(self, message: str):
    """
    Args:
        message: Description of the validation error.
    """
    super().__init__(f"Propellant validation error: {message}")

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)