Skip to content

models.feed_systems.cycles

Concrete feed-system cycle implementations. A cycle is the topology that determines how propellant is pressurised on its way from the tanks to the injector — pressure stored in the tanks themselves, electrically driven pumps, turbine-driven pumps powered by a small bleed-off combustor, and so on. Each module in this sub-package implements one topology and conforms to the FeedSystem contract so the simulation loop can treat them interchangeably.

Available cycles

  • StackedTankPressureFedFeedSystem — Pressure-fed engine with the oxidizer tank stacked directly above a piston-separated fuel tank. Tank pressure provides both propellants' driving head.
  • SingleLinePressureFedFeedSystem — Pressure-fed engine feeding one line, which the tank pressurizes itself. The one-line case of the contract: an oxidizer-only hybrid feed or a monoliquid.

The cycle reads tank state through the PropellantLine instances it was constructed with, and combines that state with any component specs it owns to say what reaches the injector face. The simulation step lives in BiliquidEngineState.run_timestep, which calls get_inlet_states once per integration step and hands the results to the Injector at each chamber pressure the solver tries.


StackedTankPressureFedFeedSystem

A pressure-fed engine whose propellant tanks are arranged as a single vertical stack separated by a piston. The tank at the top of the stack — pressurizing_line — pressurises every propellant: directly for its own line, and through the piston for every line below it. Pressure loss across the piston is captured by piston_loss, and each feedline takes what line_losses states for it.

Construction

The two-line case a biliquid engine runs on has a convenience constructor, which names the lines "oxidizer" and "fuel":

from machwave.models.feed_systems import StackedTankPressureFedFeedSystem
from machwave.models.feed_systems.tank import Tank

feed_system = StackedTankPressureFedFeedSystem.from_oxidizer_and_fuel(
    oxidizer_tank=Tank("N2O", volume=0.010, temperature=298.0, initial_fluid_mass=5.0),
    fuel_tank=Tank("Ethanol", volume=0.008, temperature=298.0, initial_fluid_mass=3.0),
    piston_loss=0.0,          # Pa
    oxidizer_line_loss=2e5,   # Pa
    fuel_line_loss=2e5,       # Pa
)

Any other propellant count is built from the lines themselves — here a triliquid with a diluent below the piston:

from machwave.models.feed_systems import PropellantLine, StackedTankPressureFedFeedSystem
from machwave.models.propellants import ComponentRole

feed_system = StackedTankPressureFedFeedSystem(
    lines=[
        PropellantLine(name="oxidizer", role=ComponentRole.OXIDIZER, tank=oxidizer_tank),
        PropellantLine(name="fuel", role=ComponentRole.FUEL, tank=fuel_tank),
        PropellantLine(name="diluent", role=ComponentRole.ADDITIVE, tank=diluent_tank),
    ],
    pressurizing_line="oxidizer",
    piston_loss=1e5,
    line_losses={"oxidizer": 2e5, "fuel": 2e5, "diluent": 1e5},
)

Mass-flow model. The feed system supplies the inlet state of every line and the injector does the orifice dispatch. Inlet pressure is the pressurizing tank pressure for its own line and that pressure less piston_loss for every line below the piston, each less its own feedline loss; downstream pressure is chamber_pressure. Inlet density comes from Tank.get_density. The injector picks the orifice model per line from its MassFlowModel.

line_losses is a fixed pressure drop per line, not a function of flow.


SingleLinePressureFedFeedSystem

One propellant line, pressurized by its own tank: a self-pressurized propellant rides its vapor pressure while liquid remains, then blows down on the real-gas equation of state. What reaches the injector is that pressure less line_loss. An empty tank delivers nothing, which stops the flow at the injector element.

Construction

from machwave.models.feed_systems import SingleLinePressureFedFeedSystem
from machwave.models.feed_systems.tank import Tank

feed_system = SingleLinePressureFedFeedSystem.from_oxidizer_tank(
    oxidizer_tank=Tank("N2O", volume=0.010, temperature=298.0, initial_fluid_mass=5.0),
    line_loss=2e5,  # Pa
)

A monoliquid names its own line and role instead, through the PropellantLine constructor.


machwave.models.feed_systems.cycles

Concrete feed-system cycle implementations.

Each module in this package implements one cycle topology (pressure-fed, electric-pump, gas-generator, expander, staged combustion, ...). Cycles consume the shared component specifications in machwave.models.feed_systems.components and conform to the machwave.models.feed_systems.base.FeedSystem contract.

SingleLinePressureFedFeedSystem

Bases: FeedSystem

Pressure-fed feed system delivering a single propellant line.

The tank pressurizes itself: a self-pressurized propellant rides its own vapor pressure while liquid remains, and blows down on the real-gas equation of state once none does. This is the one-line case of the feed system contract, which an oxidizer-only hybrid feed and a monoliquid engine both run on.

Source code in machwave/models/feed_systems/cycles/single_line_pressure_fed.py
class SingleLinePressureFedFeedSystem(feed_system_base.FeedSystem):
    """
    Pressure-fed feed system delivering a single propellant line.

    The tank pressurizes itself: a self-pressurized propellant rides its own
    vapor pressure while liquid remains, and blows down on the real-gas
    equation of state once none does. This is the one-line case of the feed
    system contract, which an oxidizer-only hybrid feed and a monoliquid engine
    both run on.
    """

    def __init__(
        self,
        line: line_models.PropellantLine,
        line_loss: float = 0.0,
    ):
        """
        Initialize the SingleLinePressureFedFeedSystem.

        Args:
            line: The propellant line the system feeds.
            line_loss: Pressure loss along the feedline [Pa], stated for the
                design flow rather than computed from it.

        Raises:
            ValueError: If the pressure loss is negative.
        """
        super().__init__([line])

        self.line_loss = line_loss

        if line_loss < 0.0:
            raise ValueError(f"line_loss must be non-negative, got {line_loss}")

    @classmethod
    def from_oxidizer_tank(
        cls,
        *,
        oxidizer_tank: tank.Tank,
        line_loss: float = 0.0,
    ) -> "SingleLinePressureFedFeedSystem":
        """
        Build the oxidizer-only system a hybrid motor runs on.

        Args:
            oxidizer_tank: Tank the oxidizer line draws from.
            line_loss: Pressure loss along the feedline [Pa].
        """
        return cls(
            line=line_models.PropellantLine(
                name="oxidizer",
                role=propellant_components.ComponentRole.OXIDIZER,
                tank=oxidizer_tank,
            ),
            line_loss=line_loss,
        )

    @property
    def line(self) -> line_models.PropellantLine:
        """The one line the system feeds."""
        return next(iter(self.lines.values()))

    def get_inlet_pressures(
        self, line_states: Mapping[str, line_models.LineState]
    ) -> dict[str, float]:
        """
        Return the tank pressure less the feedline loss [Pa], keyed by line name.

        Args:
            line_states: Fluid mass and internal energy of the line, keyed by
                line name.
        """
        line_state = line_states[self.line.name]

        return {
            self.line.name: self.line.tank.get_pressure(
                line_state.fluid_mass, line_state.internal_energy
            )
            - self.line_loss
        }

line property

The one line the system feeds.

__init__(line, line_loss=0.0)

Initialize the SingleLinePressureFedFeedSystem.

Parameters:

Name Type Description Default
line PropellantLine

The propellant line the system feeds.

required
line_loss float

Pressure loss along the feedline [Pa], stated for the design flow rather than computed from it.

0.0

Raises:

Type Description
ValueError

If the pressure loss is negative.

Source code in machwave/models/feed_systems/cycles/single_line_pressure_fed.py
def __init__(
    self,
    line: line_models.PropellantLine,
    line_loss: float = 0.0,
):
    """
    Initialize the SingleLinePressureFedFeedSystem.

    Args:
        line: The propellant line the system feeds.
        line_loss: Pressure loss along the feedline [Pa], stated for the
            design flow rather than computed from it.

    Raises:
        ValueError: If the pressure loss is negative.
    """
    super().__init__([line])

    self.line_loss = line_loss

    if line_loss < 0.0:
        raise ValueError(f"line_loss must be non-negative, got {line_loss}")

from_oxidizer_tank(*, oxidizer_tank, line_loss=0.0) classmethod

Build the oxidizer-only system a hybrid motor runs on.

Parameters:

Name Type Description Default
oxidizer_tank Tank

Tank the oxidizer line draws from.

required
line_loss float

Pressure loss along the feedline [Pa].

0.0
Source code in machwave/models/feed_systems/cycles/single_line_pressure_fed.py
@classmethod
def from_oxidizer_tank(
    cls,
    *,
    oxidizer_tank: tank.Tank,
    line_loss: float = 0.0,
) -> "SingleLinePressureFedFeedSystem":
    """
    Build the oxidizer-only system a hybrid motor runs on.

    Args:
        oxidizer_tank: Tank the oxidizer line draws from.
        line_loss: Pressure loss along the feedline [Pa].
    """
    return cls(
        line=line_models.PropellantLine(
            name="oxidizer",
            role=propellant_components.ComponentRole.OXIDIZER,
            tank=oxidizer_tank,
        ),
        line_loss=line_loss,
    )

get_inlet_pressures(line_states)

Return the tank pressure less the feedline loss [Pa], keyed by line name.

Parameters:

Name Type Description Default
line_states Mapping[str, LineState]

Fluid mass and internal energy of the line, keyed by line name.

required
Source code in machwave/models/feed_systems/cycles/single_line_pressure_fed.py
def get_inlet_pressures(
    self, line_states: Mapping[str, line_models.LineState]
) -> dict[str, float]:
    """
    Return the tank pressure less the feedline loss [Pa], keyed by line name.

    Args:
        line_states: Fluid mass and internal energy of the line, keyed by
            line name.
    """
    line_state = line_states[self.line.name]

    return {
        self.line.name: self.line.tank.get_pressure(
            line_state.fluid_mass, line_state.internal_energy
        )
        - self.line_loss
    }

StackedTankPressureFedFeedSystem

Bases: FeedSystem

Pressure-fed feed system whose propellant tanks are stacked vertically.

The tank at the top of the stack pressurizes every tank below it through a piston, so one tank sets the pressure the whole system runs on.

Source code in machwave/models/feed_systems/cycles/stacked_tank_pressure_fed.py
class StackedTankPressureFedFeedSystem(feed_system_base.FeedSystem):
    """
    Pressure-fed feed system whose propellant tanks are stacked vertically.

    The tank at the top of the stack pressurizes every tank below it through a
    piston, so one tank sets the pressure the whole system runs on.
    """

    def __init__(
        self,
        lines: Sequence[line_models.PropellantLine],
        pressurizing_line: str,
        piston_loss: float = 0.0,
        line_losses: Mapping[str, float] | None = None,
    ):
        """
        Initialize the StackedTankPressureFedFeedSystem.

        Args:
            lines: Propellant lines the system feeds, one per propellant.
            pressurizing_line: Name of the line at the top of the stack, whose
                tank pressure drives every line.
            piston_loss: Pressure loss across the piston [Pa], taken by every
                line below the top of the stack.
            line_losses: Pressure loss along each feedline [Pa], keyed by line
                name and stated for the design flow rather than computed from
                it. A line left out takes no loss.

        Raises:
            ValueError: If the pressurizing line is not one of the lines, if a
                loss is keyed by an unknown line, or if any pressure loss is
                negative.
        """
        super().__init__(lines)

        self.pressurizing_line = pressurizing_line
        self.piston_loss = piston_loss
        self.line_losses = {name: 0.0 for name in self.lines} | dict(line_losses or {})

        self._validate_pressure_losses()

    @classmethod
    def from_oxidizer_and_fuel(
        cls,
        *,
        oxidizer_tank: tank.Tank,
        fuel_tank: tank.Tank,
        piston_loss: float = 0.0,
        oxidizer_line_loss: float = 0.0,
        fuel_line_loss: float = 0.0,
    ) -> "StackedTankPressureFedFeedSystem":
        """
        Build the two-line system a biliquid engine runs on.

        The oxidizer sits at the top of the stack and pressurizes the fuel
        through the piston. The lines are named "oxidizer" and "fuel", which is
        how the injector and the simulation state key them.

        Args:
            oxidizer_tank: Tank the oxidizer line draws from.
            fuel_tank: Tank the fuel line draws from.
            piston_loss: Pressure loss across the piston [Pa].
            oxidizer_line_loss: Pressure loss along the oxidizer feedline [Pa].
            fuel_line_loss: Pressure loss along the fuel feedline [Pa].
        """
        return cls(
            lines=[
                line_models.PropellantLine(
                    name="oxidizer",
                    role=propellant_components.ComponentRole.OXIDIZER,
                    tank=oxidizer_tank,
                ),
                line_models.PropellantLine(
                    name="fuel",
                    role=propellant_components.ComponentRole.FUEL,
                    tank=fuel_tank,
                ),
            ],
            pressurizing_line="oxidizer",
            piston_loss=piston_loss,
            line_losses={"oxidizer": oxidizer_line_loss, "fuel": fuel_line_loss},
        )

    def _validate_pressure_losses(self) -> None:
        if self.pressurizing_line not in self.lines:
            raise ValueError(
                f"pressurizing_line {self.pressurizing_line!r} is not one of the "
                f"lines, got {list(self.lines)}"
            )

        unknown = [name for name in self.line_losses if name not in self.lines]
        if unknown:
            raise ValueError(f"line_losses names {unknown} are not lines of the system")

        if self.piston_loss < 0.0:
            raise ValueError(
                f"piston_loss must be non-negative, got {self.piston_loss}"
            )
        for name, loss in self.line_losses.items():
            if loss < 0.0:
                raise ValueError(
                    f"line_losses[{name!r}] must be non-negative, got {loss}"
                )

    def get_inlet_pressures(
        self, line_states: Mapping[str, line_models.LineState]
    ) -> dict[str, float]:
        """
        Return the pressure delivered to the injector on every line [Pa].

        The line at the top of the stack loses only what its own feedline
        takes; every line below the piston loses the piston pressure loss too.

        Args:
            line_states: Fluid mass and internal energy of every line, keyed by
                line name.

        Returns:
            Injector inlet pressure of every line [Pa], keyed by line name.
        """
        pressurizing_state = line_states[self.pressurizing_line]
        stack_pressure = self.lines[self.pressurizing_line].tank.get_pressure(
            pressurizing_state.fluid_mass, pressurizing_state.internal_energy
        )

        return {
            name: stack_pressure
            - (0.0 if name == self.pressurizing_line else self.piston_loss)
            - self.line_losses[name]
            for name in self.lines
        }

__init__(lines, pressurizing_line, piston_loss=0.0, line_losses=None)

Initialize the StackedTankPressureFedFeedSystem.

Parameters:

Name Type Description Default
lines Sequence[PropellantLine]

Propellant lines the system feeds, one per propellant.

required
pressurizing_line str

Name of the line at the top of the stack, whose tank pressure drives every line.

required
piston_loss float

Pressure loss across the piston [Pa], taken by every line below the top of the stack.

0.0
line_losses Mapping[str, float] | None

Pressure loss along each feedline [Pa], keyed by line name and stated for the design flow rather than computed from it. A line left out takes no loss.

None

Raises:

Type Description
ValueError

If the pressurizing line is not one of the lines, if a loss is keyed by an unknown line, or if any pressure loss is negative.

Source code in machwave/models/feed_systems/cycles/stacked_tank_pressure_fed.py
def __init__(
    self,
    lines: Sequence[line_models.PropellantLine],
    pressurizing_line: str,
    piston_loss: float = 0.0,
    line_losses: Mapping[str, float] | None = None,
):
    """
    Initialize the StackedTankPressureFedFeedSystem.

    Args:
        lines: Propellant lines the system feeds, one per propellant.
        pressurizing_line: Name of the line at the top of the stack, whose
            tank pressure drives every line.
        piston_loss: Pressure loss across the piston [Pa], taken by every
            line below the top of the stack.
        line_losses: Pressure loss along each feedline [Pa], keyed by line
            name and stated for the design flow rather than computed from
            it. A line left out takes no loss.

    Raises:
        ValueError: If the pressurizing line is not one of the lines, if a
            loss is keyed by an unknown line, or if any pressure loss is
            negative.
    """
    super().__init__(lines)

    self.pressurizing_line = pressurizing_line
    self.piston_loss = piston_loss
    self.line_losses = {name: 0.0 for name in self.lines} | dict(line_losses or {})

    self._validate_pressure_losses()

from_oxidizer_and_fuel(*, oxidizer_tank, fuel_tank, piston_loss=0.0, oxidizer_line_loss=0.0, fuel_line_loss=0.0) classmethod

Build the two-line system a biliquid engine runs on.

The oxidizer sits at the top of the stack and pressurizes the fuel through the piston. The lines are named "oxidizer" and "fuel", which is how the injector and the simulation state key them.

Parameters:

Name Type Description Default
oxidizer_tank Tank

Tank the oxidizer line draws from.

required
fuel_tank Tank

Tank the fuel line draws from.

required
piston_loss float

Pressure loss across the piston [Pa].

0.0
oxidizer_line_loss float

Pressure loss along the oxidizer feedline [Pa].

0.0
fuel_line_loss float

Pressure loss along the fuel feedline [Pa].

0.0
Source code in machwave/models/feed_systems/cycles/stacked_tank_pressure_fed.py
@classmethod
def from_oxidizer_and_fuel(
    cls,
    *,
    oxidizer_tank: tank.Tank,
    fuel_tank: tank.Tank,
    piston_loss: float = 0.0,
    oxidizer_line_loss: float = 0.0,
    fuel_line_loss: float = 0.0,
) -> "StackedTankPressureFedFeedSystem":
    """
    Build the two-line system a biliquid engine runs on.

    The oxidizer sits at the top of the stack and pressurizes the fuel
    through the piston. The lines are named "oxidizer" and "fuel", which is
    how the injector and the simulation state key them.

    Args:
        oxidizer_tank: Tank the oxidizer line draws from.
        fuel_tank: Tank the fuel line draws from.
        piston_loss: Pressure loss across the piston [Pa].
        oxidizer_line_loss: Pressure loss along the oxidizer feedline [Pa].
        fuel_line_loss: Pressure loss along the fuel feedline [Pa].
    """
    return cls(
        lines=[
            line_models.PropellantLine(
                name="oxidizer",
                role=propellant_components.ComponentRole.OXIDIZER,
                tank=oxidizer_tank,
            ),
            line_models.PropellantLine(
                name="fuel",
                role=propellant_components.ComponentRole.FUEL,
                tank=fuel_tank,
            ),
        ],
        pressurizing_line="oxidizer",
        piston_loss=piston_loss,
        line_losses={"oxidizer": oxidizer_line_loss, "fuel": fuel_line_loss},
    )

get_inlet_pressures(line_states)

Return the pressure delivered to the injector on every line [Pa].

The line at the top of the stack loses only what its own feedline takes; every line below the piston loses the piston pressure loss too.

Parameters:

Name Type Description Default
line_states Mapping[str, LineState]

Fluid mass and internal energy of every line, keyed by line name.

required

Returns:

Type Description
dict[str, float]

Injector inlet pressure of every line [Pa], keyed by line name.

Source code in machwave/models/feed_systems/cycles/stacked_tank_pressure_fed.py
def get_inlet_pressures(
    self, line_states: Mapping[str, line_models.LineState]
) -> dict[str, float]:
    """
    Return the pressure delivered to the injector on every line [Pa].

    The line at the top of the stack loses only what its own feedline
    takes; every line below the piston loses the piston pressure loss too.

    Args:
        line_states: Fluid mass and internal energy of every line, keyed by
            line name.

    Returns:
        Injector inlet pressure of every line [Pa], keyed by line name.
    """
    pressurizing_state = line_states[self.pressurizing_line]
    stack_pressure = self.lines[self.pressurizing_line].tank.get_pressure(
        pressurizing_state.fluid_mass, pressurizing_state.internal_energy
    )

    return {
        name: stack_pressure
        - (0.0 if name == self.pressurizing_line else self.piston_loss)
        - self.line_losses[name]
        for name in self.lines
    }