STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
feedfit.py
Go to the documentation of this file.
1"""The drawing's feed, in the form the injector is designed with.
2
3EngineDesign sizes the injector (Layer 1, forward mode) at a tank pressure, less one loss per side,
4``K0`` velocity heads of the line plus the exit dump (``engine/pipeline/feed_loss.py``). The drawing
5says otherwise in two places, measured on the 6.8 kN stand burn:
6
7* **The tank does not hold its lockup while firing.** It holds lockup less the regulator's droop
8 and the press line's loss into the ullage, partly given back by the supply-pressure effect as
9 the bottle falls: 13 psi below lockup on average on the LOX side, 25 psi at worst. EngineDesign's
10 tank pressure is the regulator's set point, flowing nothing.
11* **The lines lose more than their K0 says**, because the drawing has the valves, the bends and
12 the lengths: LOX 0.728 velocity heads against 0.643, fuel 2.16 against 2.02.
13
14Together the manifolds sit 13–15 psi below what the injector was sized for, about 11 % of its
15drop, and not by the same amount on the two sides.
16
17``fit_feed`` reads both from a Layer X burn, per side, against EngineDesign's own velocity head
18(its density, its ``A_hydraulic``) so the numbers drop straight into ``K0``:
19
20* ``K_line``: the drawing's lines, tank to line exit, least squares over the settled burn. A
21 velocity-head law is the lines' own form (Darcy plus fittings), so this one is physical.
22* ``K_supply``: the burn-mean fall of the tank below lockup, expressed in the same velocity heads at
23 the burn's mean flow. This one is not a velocity-head law (droop goes with flow, the
24 supply-pressure effect with time), so it is exact at the flow it was fitted at and approximate
25 away from it: a design that moves the flow 5 % moves it ~0.7 psi. Fit again after resizing the
26 injector, and the two converge.
27
28``K0 = K_line + K_supply`` is the whole loss from the lockup to the line exit at the design flow:
29the drawing's, in the form Layer 1's solver already reads. The exit dump stays EngineDesign's
30``K_exit``, which is the term the engine card uses too. Written into the design, it carries a
31``derived_from`` record saying which drawing, which run and which flow.
32"""
33
34from __future__ import annotations
35
36import time
37from typing import Any, Dict, List, Optional
38
39from engine.layerx.prepare import PSI, Prepared
40
41#: Steps after Fire left out of the fit: the ignition transient, as the other burn averages do.
42SETTLED_S = 0.3
43
44SIDES = (("ox", "oxidizer", "LOX"), ("fuel", "fuel", "Fuel"))
45
46
47def fit_feed(prep: Prepared, result: Dict[str, Any], config: Any) -> Dict[str, Any]:
48 """Per side: the drawing's feed against the design's, and the K0 that makes them agree."""
49 from engine.pipeline.feed_loss import delta_p_feed
50
51 series = result["series"]
52 fire = [i for i, f in enumerate(series["firing"]) if f]
53 if len(fire) < 4:
54 return {"available": False, "error": "the burn is too short to fit"}
55 t0 = series["t"][fire[0]]
56 idx = [i for i in fire[:-1] if series["t"][i] - t0 >= SETTLED_S]
57 if len(idx) < 3:
58 return {"available": False, "error": "too few settled steps to fit"}
59 flown = bool((result.get("flight") or {}).get("ok"))
60 sides: Dict[str, Any] = {}
61 for key, cside, label in SIDES:
62 # Each side's own tank at T-0: on a drawing with two regulators they differ.
63 lockup = float(result["summary"][key]["t0_psia"])
64 fs = config.feed_system[cside]
65 rho = float(config.fluids[cside].density)
66 mu = getattr(config.fluids[cside], "viscosity", None)
67 area = float(fs.A_hydraulic)
68 s = series[key]
69 rows = []
70 for i in idx:
71 m = float(s["mdot"][i])
72 q = 0.5 * rho * (m / (rho * area)) ** 2 / PSI # one velocity head of the design's line [psi]
73 rows.append((m, q, lockup, float(s["tank_psia"][i]), float(s["inlet_psia"][i]), float(s["manifold_psia"][i])))
74 m_mean = sum(r[0] for r in rows) / len(rows)
75 q_mean = sum(r[1] for r in rows) / len(rows)
76 sum_qq = sum(r[1] ** 2 for r in rows)
77 # The lines: tank to line exit is a velocity-head law; least squares through the origin.
78 k_line = sum((r[3] - r[4]) * r[1] for r in rows) / sum_qq
79 # The supply: lockup to tank, the burn's mean, in velocity heads at its mean flow.
80 deficit = sum(r[2] - r[3] for r in rows) / len(rows)
81 k_supply = deficit / q_mean
82 k0 = k_line + k_supply
83 # The design today, at the same flows: lockup less its feed loss, exit dump included.
84 def design_manifold(m: float, cfg: Any) -> float:
85 return lockup - delta_p_feed(m, rho, cfg, lockup * PSI, mu=mu) / PSI
86 fitted_cfg = fs.model_copy(update={"K0": k0, "roughness_m": None, "fittings": [], "K1": 0.0, "phi_type": "none"})
87 twin_man = [r[5] for r in rows]
88 today = [design_manifold(r[0], fs) for r in rows]
89 fitted = [design_manifold(r[0], fitted_cfg) for r in rows]
90 mean = lambda v: sum(v) / len(v) # noqa: E731
91 rms = lambda a, b: (sum((x - y) ** 2 for x, y in zip(a, b)) / len(a)) ** 0.5 # noqa: E731
92 line_loss = mean([r[3] - r[4] for r in rows])
93 line_loss_design = mean([delta_p_feed(r[0], rho, fs.model_copy(update={"K_exit": 0.0}), lockup * PSI, mu=mu) / PSI
94 for r in rows])
95 tank_firing = mean([r[3] for r in rows])
96 sides[cside] = {
97 "label": label,
98 "mdot_kg_s": m_mean,
99 "velocity_head_psi": q_mean,
100 "lockup_psia": lockup,
101 "tank_firing_psia": tank_firing,
102 "supply_deficit_psi": deficit,
103 "supply_deficit_worst_psi": max(r[2] - r[3] for r in rows),
104 "line_loss_psi": line_loss,
105 "line_loss_design_psi": line_loss_design,
106 "manifold_psia": mean(twin_man),
107 "manifold_design_psia": mean(today),
108 "manifold_gap_psi": mean(twin_man) - mean(today),
109 "manifold_fitted_psia": mean(fitted),
110 "fitted_rms_psi": rms(twin_man, fitted),
111 "K_line": k_line,
112 "K_supply": k_supply,
113 "K0": k0,
114 "K0_design": float(fs.K0),
115 "design_path": ("friction (roughness_m) + fittings" if getattr(fs, "roughness_m", None) is not None
116 else "K0" + (" + fittings" if getattr(fs, "fittings", None) else "")),
117 "write_refused": _refusal(label, k0, k_line, k_supply, line_loss, line_loss_design, tank_firing, lockup),
118 }
119 refused = [s["write_refused"] for s in sides.values() if s["write_refused"]]
120 return {
121 "available": True,
122 # A loss coefficient is never negative: a side whose fit comes out at or below zero is not
123 # written into the design (D-58), and the reason is said in words.
124 "writable": not refused,
125 "write_refused": " ".join(refused) or None,
126 "flown": flown,
127 "basis": (("In flight: every liquid column at the flight's acceleration. " if flown else "On the pad, one g. ")
128 + f"Settled firing steps ({len(idx)}, from {SETTLED_S:g} s after Fire, last step left out), "
129 "against the design's velocity head (its density, its A_hydraulic). Fit again after the "
130 "injector is resized: the supply term is exact only at the flow it was fitted at."),
131 "sides": sides,
132 "drawing": {"id": prep.drawing.id, "name": prep.drawing.name, "sha256": prep.drawing.sha256},
133 }
134
135
136def _refusal(label: str, k0: float, k_line: float, k_supply: float, line_psi: float, line_design_psi: float,
137 tank_psia: float, lockup_psia: float) -> Optional[str]:
138 """Why a side's fitted K0 cannot go into the design, or ``None`` when it can. K0 is a loss in
139 velocity heads; at or below zero the fit is saying the feed *adds* pressure, which a loss
140 coefficient cannot hold. Said by its parts: the lines (a side drawn with no line loses nothing
141 between the tank and the injector) and the supply (the regulator's outlet climbs as the bottle
142 empties, the supply-pressure effect, so the tank sits above its T-0 pressure while firing)."""
143 if k0 > 0.0:
144 return None
145 why = []
146 if k_line <= 0.0:
147 why.append(f"the drawing loses {line_psi:.1f} psi between the tank and the injector against the design's "
148 f"{line_design_psi:.1f}, so its lines are likely undrawn")
149 if k_supply < 0.0:
150 why.append(f"the tank sits above its T-0 pressure while firing ({tank_psia:.1f} against {lockup_psia:.1f} "
151 "psia: the regulator's outlet climbs as the bottle empties)")
152 return f"{label}: K0 {k0:.3f} is not a loss; " + (" and ".join(why) or "the fit adds pressure") + "."
153
154
155def design_update(fit: Dict[str, Any], run_id: str, when: Optional[float] = None) -> Dict[str, Any]:
156 """The ``feed_system`` update that writes the fit into the design: K0 per side on the lumped
157 path (the method ladder's "measured K", here fitted from the drawing), with its record.
158 Refused (``ValueError``) when a side's K0 is not a loss (:func:`_refusal`)."""
159 refused = [s.get("write_refused") or _refusal(s["label"], s["K0"], s["K_line"], s["K_supply"],
160 s.get("line_loss_psi", 0.0), s.get("line_loss_design_psi", 0.0),
161 s.get("tank_firing_psia", 0.0), s.get("lockup_psia", 0.0))
162 for s in fit["sides"].values()]
163 if any(refused):
164 raise ValueError(" ".join(r for r in refused if r))
165 stamp = time.strftime("%Y-%m-%d", time.localtime(when or time.time()))
166 out: Dict[str, Any] = {}
167 for cside, s in fit["sides"].items():
168 out[cside] = {
169 "K0": round(s["K0"], 4),
170 # The supply's part of K0, kept apart so the chug loop does not read the regulator's
171 # droop as line resistance (config FeedSystemConfig.supply_K).
172 "supply_K": round(max(s["K_supply"], 0.0), 4),
173 "K1": 0.0,
174 "phi_type": "none",
175 "roughness_m": None,
176 "fittings": [],
177 "derived_from": {
178 "by": "Layer X feed fit",
179 "drawing": fit["drawing"]["name"],
180 "drawing_sha256": fit["drawing"]["sha256"],
181 "run": run_id,
182 "date": stamp,
183 "condition": "in flight" if fit.get("flown") else "on the pad",
184 "K_line": round(s["K_line"], 4),
185 "K_supply": round(s["K_supply"], 4),
186 "supply_deficit_psi": round(s["supply_deficit_psi"], 2),
187 "mdot_kg_s": round(s["mdot_kg_s"], 4),
188 "lockup_psia": round(s["lockup_psia"], 2),
189 "replaced_K0": s["K0_design"],
190 "replaced_path": s["design_path"],
191 },
192 }
193 return {"feed_system": out}
194
195
196def summary_rows(fit: Dict[str, Any]) -> List[Dict[str, Any]]:
197 """Flat rows for a report."""
198 return [{"side": s["label"], **{k: v for k, v in s.items() if k != "label"}} for s in fit.get("sides", {}).values()]
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] fit_feed(Prepared prep, Dict[str, Any] result, Any config)
Definition feedfit.py:47
List[Dict[str, Any]] summary_rows(Dict[str, Any] fit)
Definition feedfit.py:196
Dict[str, Any] design_update(Dict[str, Any] fit, str run_id, Optional[float] when=None)
Definition feedfit.py:155
Optional[str] _refusal(str label, float k0, float k_line, float k_supply, float line_psi, float line_design_psi, float tank_psia, float lockup_psia)
Definition feedfit.py:137
TOML_NODISCARD_CTOR time() noexcept=default