Skip to content

models.propellants.categories

Propellant type classes.

  • SolidPropellant — Defined by component mass fractions, pre-computed or CEA-evaluated thermochemical properties, and a burn_rate_map encoding St. Robert’s law coefficients (\(r = a \cdot P_0^n\)) across pressure ranges. Call get_burn_rate(P_0) to obtain instantaneous regression rate.
  • BiliquidPropellant — Defined by exactly two components (oxidizer + fuel) and an O/F mass ratio. Thermochemical properties are evaluated at the specified O/F.

Both extend the Propellant base class and implement evaluate(chamber_pressure, expansion_ratio, mixture_ratio=None) to return a ThermochemicalProperties dataclass. The optional mixture_ratio overrides the propellant's stored ratio per-call — BiliquidEngineState uses this to feed the live \(\dot{m}_{ox} / \dot{m}_{fuel}\) each step.

machwave.models.propellants.categories

Propellant categories or mixture types.

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()

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

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)