1"""The injector as a network branch, and the engine as a network boundary.
3Two objects, and the split matters.
5An **injector leg** is an ordinary hydraulic component: an orifice of known area
6and discharge coefficient, one per propellant, sitting at the downstream end of
7each feed line. Nothing about it is special except that its area came out of a
8Layer-1 optimisation rather than off a drawing.
10The **engine** is not a component at all. It is a boundary condition, and a
11circular one: chamber pressure depends on the flows, and the flows depend on
12chamber pressure through the injector's pressure difference. The session closes
13that loop each coupling step (``Session._close_chamber``), root-finding the
14chamber pressure the network and the engine agree on -- the same coupled
15injector-to-chamber solve EngineDesign performs, done here around a whole feed
16system instead of around two pressures.
18Why the loop is worth having
19----------------------------
20It is the reason a pressure-fed engine is stable in the large. Raise tank
21pressure and flow rises, so chamber pressure rises, so the injector's pressure
22difference rises by *less* than the tank did -- the engine absorbs part of every
23upstream change. That is also why injector stiffness is a design rule rather
24than a preference: as the injector's share of the total pressure drop falls, the
25loop gain rises, and below roughly 20% the engine starts responding to its own
26chamber more than to its tanks.
29from __future__
import annotations
32from dataclasses
import replace
33from typing
import TYPE_CHECKING, Mapping
38 InfeasibleOperatingPoint,
50 """One propellant's path through the injector face.
52 ``dp = mdot^2 / (2 rho (Cd A)^2)`` -- the orifice relation, with ``Cd``
53 coming from the Reynolds-dependent model in the engine config rather than
54 being a constant. That dependence is not decoration: at low flow during
55 startup the discharge coefficient is materially below its high-Reynolds
56 value, and an injector modelled at a fixed ``Cd`` opens too fast.
58 Built from an :class:`~feedtwin.engine.design.InjectorSide` rather than from
59 a component config, because the whole point of Phase 08 is that these
60 numbers arrive from Layer 1 and are not retyped.
63 def __init__(self, instance: ComponentInstance, side: InjectorSide) ->
None:
72 """``Cd . A`` at this operating point [m^2].
74 With an engine card it is recovered from the card's pressure drop at the
75 density this leg sees, so the diagnostics and the mixture balance read
76 the same injector the solve used."""
79 phi = card.capacity(abs(mdot), flow.p_upstream)
80 return phi / math.sqrt(2.0 * flow.rho)
if flow.rho > 0.0
else 0.0
81 cd = self.
side.cd_at(mdot, flow.rho, flow.mu, pressure=flow.p_upstream)
82 return cd * self.
side.area
95 return card.pressure_drop(abs(mdot), flow.p_upstream)
100 if area <= 0.0
or flow.rho <= 0.0:
102 return magnitude * magnitude / (2.0 * flow.rho * area * area)
104 def off_card(self, mdot: float, flow: FlowConditions) -> bool:
105 """True when this lookup is outside the hull the leg's card was fitted to.
107 The card's table clamps outside its box, which keeps the number
108 physical and says nothing; the hull is how a trace learns it left what
109 the card knows (:func:`fold_injectors`). No card, nothing to be off.
111 card = self.
side.card
112 if card
is None or mdot == 0.0:
114 return not card.covers(mdot, flow.p_upstream)
116 def diagnostics(self, mdot: float, flow: FlowConditions) -> dict[str, float]:
118 velocity = abs(mdot) / (flow.rho * self.
side.area)
if flow.rho > 0.0
else 0.0
120 "area_geometric": self.
side.area,
121 "area_effective": area,
122 "Cd": area / self.
side.area
if self.
side.area > 0.0
else 0.0,
123 "velocity": velocity,
125 flow.rho * velocity * self.
side.hydraulic_diameter / flow.mu
129 "elements": float(self.
side.element_count),
131 "extrapolated": 1.0
if self.
off_card(mdot, flow)
else 0.0,
135 out: list[Violation] = []
136 if self.
side.area <= 0.0:
138 Violation(self.
id,
"area",
"injector area is zero and cannot flow")
140 if self.
side.hydraulic_diameter <= 0.0:
144 "hydraulic_diameter",
145 "no hydraulic diameter, so the Reynolds-dependent Cd is "
146 "pinned at its floor",
154 design: EngineDesign,
155) -> tuple[InjectorLeg, InjectorLeg]:
156 """Build the two injector branches from an imported engine design.
158 Returns ``(oxidiser, fuel)``. The component ids are derived from the design
159 name so a report can trace a branch back to the config it came from.
163 def leg(side: InjectorSide, tag: str) -> InjectorLeg:
168 bore = math.sqrt(4.0 * side.area / math.pi)
169 instance = ComponentInstance.build(
173 "bore": Param(bore,
"m", Provenance.MANUFACTURER, design.name),
174 "pipe_bore": Param(bore,
"m", Provenance.MANUFACTURER, design.name),
176 side.discharge.cd_inf,
"-", Provenance.MANUFACTURER, design.name
182 return leg(design.oxidiser,
"OX"), leg(design.fuel,
"FUEL")
186 chamber: ChamberResult,
188 ports: Mapping[str, str],
189 flows: Mapping[str, float],
190 pressures: Mapping[str, float],
192 """The chamber's answer, flagged extrapolated if either injector leg's card
193 lookup was off its hull.
195 A step's ``extrapolated`` flag is the chamber's, which only ever asked the
196 chamber table; an injector card read below its sampled inlet pressures (a
197 vent-down, a low-pressure firing) was clamped and never counted. The flag
198 means "outside what the card knows", so the injectors fold into it.
200 if chamber.combustion.extrapolated:
202 for side
in (
"oxidiser",
"fuel"):
203 branch_id = ports.get(side,
"")
204 branch = network.branches.get(branch_id)
if branch_id
else None
205 if branch
is None or not isinstance(branch.component, InjectorLeg):
207 mdot = flows.get(branch_id, 0.0)
208 p_inlet = pressures.get(branch.upstream)
209 if mdot <= 0.0
or p_inlet
is None:
211 if branch.component.off_card(
215 chamber, combustion=replace(chamber.combustion, extrapolated=
True)
float pressure_drop(self, float mdot, FlowConditions flow)
None __init__(self, ComponentInstance instance, InjectorSide side)
dict[str, float] diagnostics(self, float mdot, FlowConditions flow)
list[Violation] check(self)
float pressure_drop(self, float mdot, FlowConditions flow)
bool off_card(self, float mdot, FlowConditions flow)
float effective_area(self, float mdot, FlowConditions flow)
ChamberResult fold_injectors(ChamberResult chamber, Network network, Mapping[str, str] ports, Mapping[str, float] flows, Mapping[str, float] pressures)
tuple[InjectorLeg, InjectorLeg] injector_legs(EngineDesign design)