60def build_ledger(result: Mapping[str, Any], prep: Any =
None, config: Any =
None, *,
61 forward: Optional[Mapping[str, Any]] =
None, runner: Any =
None,
62 injector: Optional[Mapping[str, Any]] =
None) -> Any:
63 """``diagnostics.ledger`` (a list of rows). Never raises: a failure is ``{available: false, error}``."""
69 series = result.get(
"series")
or {}
70 dl = result.get(
"delivered")
or {}
71 rep = result.get(
"replay")
or {}
74 fire = firing_mask(result, n)
75 dt = arr(series.get(
"dt"), n)
if series.get(
"dt")
is not None and len(series[
"dt"]) == n
else np.ones(n)
80 fwd = forward_design_point(prep, config, runner, forward)
81 except Exception
as exc:
82 fwd_err = f
"{type(exc).__name__}: {exc}"
83 fd = (fwd
or {}).get(
"diagnostics")
or {}
84 basis = (fwd
or {}).get(
"_basis")
or {}
85 lockup = basis.get(
"tank_psia")
or ((getattr(prep,
"derived",
None)
or
86 (result.get(
"provenance")
or {}).get(
"derived")
or {})
87 .get(
"target_lockup_psia"))
88 fwd_note = (f
"EngineDesign Forward at {lockup} psia both tanks" if fwd
else
89 f
"no Forward solve ({fwd_err or 'pass forward= or runner='})")
91 def fwd_val(key: str, scale: float = 1.0) -> Optional[float]:
98 return None if v
is None else v / scale
100 def ser(path: str) -> np.ndarray:
101 return arr(
_get(series, path), n)
103 def firing_stats(v: np.ndarray) -> Optional[Dict[str, Optional[float]]]:
104 return _stats(np.where(fire, v, np.nan), np.where(fire, dt, np.nan))
106 def dl_stats(key: str, scale: float = 1.0) -> Optional[Dict[str, Optional[float]]]:
110 d_t = arr(dl.get(
"t"))
112 w = np.diff(d_t, prepend=d_t[0] - (d_t[1] - d_t[0]
if d_t.size > 1
else 1.0))
if d_t.size
else None
115 rows: List[Dict[str, Any]] = []
118 no_replay =
not dl.get(
"t")
119 replay_off = ((result.get(
"provenance")
or {}).get(
"settings")
or {}).get(
"replay")
is False
120 why = (
"not computed: nozzle erosion off" if replay_off
else "not computed: the run has no erosion replay") \
123 def row(key: str, label: str, design: Optional[float], unit: str, delivered: Any, series_ref: str,
124 replaced: str, note: str =
"", design_source: str =
"") ->
None:
125 rows.append({
"key": key,
"label": label,
"design_value": scalar(design)
if design
is not None else None,
126 "design_source": design_source
or fwd_note,
"unit": unit,
"delivered": delivered,
127 "series_ref": series_ref,
"replaced": replaced,
"note": note})
128 if delivered
is None and why
and series_ref.startswith((
"delivered.",
"replay.")):
129 rows[-1][
"missing"] =
"erosion off" if replay_off
else "no replay"
131 sides = ((
"ox",
"O",
"LOX",
"oxidizer"), (
"fuel",
"F",
"Fuel",
"fuel"))
132 for s, k, lab, _
in sides:
133 row(f
"tank_pressure_{s}", f
"{lab} tank pressure", lockup,
"psia", firing_stats(ser(f
"{s}.tank_psia")),
134 f
"series.{s}.tank_psia",
"yes",
135 "EngineDesign holds the tank at the lockup; the twin's ullage dips at ignition and rides the "
136 "regulator's supply-pressure effect", design_source=
"Forward input (the burn's lockup)")
137 regs = series.get(
"regulators")
or {}
138 for rid, reg
in regs.items():
139 row(f
"regulator_outlet_{rid}", f
"Regulator {reg.get('label') or rid} outlet", lockup,
"psia",
140 firing_stats(arr(reg.get(
"outlet_psia"), n)), f
"series.regulators.{rid}.outlet_psia",
"yes",
141 "Forward has no regulator (a flat setpoint); the drawing's supply-pressure effect and droop act "
142 "here. Sources disagree on the supply effect (AUDIT D9).",
143 design_source=
"flat setpoint at the lockup (no regulator in Forward)")
144 for s, k, lab, _
in sides:
145 row(f
"feed_loss_{s}", f
"{lab} feed loss, tank to manifold", fwd_val(f
"delta_p_feed_{k}", PSI),
"psi",
146 firing_stats(ser(f
"{s}.tank_psia") - ser(f
"{s}.manifold_psia")),
147 f
"series.{s}.tank_psia - series.{s}.manifold_psia",
"yes",
148 "design: the config's lumped K0 + Borda dump; delivered: the drawing's lines, head and the dump")
151 twin_note =
"the twin's chamber on the as-built throat (no erosion replay)" if no_replay
else ""
153 def engine_row(key: str, label: str, design: Optional[float], unit: str, dl_key: str, twin_path: str) ->
None:
155 row(key, label, design, unit, dl_stats(dl_key), f
"delivered.{dl_key}",
"yes")
157 row(key, label, design, unit, firing_stats(ser(twin_path)), f
"series.{twin_path}",
"yes", twin_note)
159 engine_row(
"pc",
"Chamber pressure", fwd_val(
"Pc", PSI),
"psia",
"pc_psia",
"chamber.pc_psia")
160 engine_row(
"thrust",
"Thrust", fwd_val(
"F"),
"N",
"thrust_N",
"chamber.thrust_N")
161 engine_row(
"of",
"O/F", fwd_val(
"MR"),
"-",
"mr",
"chamber.mr")
162 for s, k, lab, _
in sides:
163 engine_row(f
"mdot_{s}", f
"{lab} flow", fwd_val(f
"mdot_{k}"),
"kg/s", f
"mdot_{k}", f
"{s}.mdot")
164 engine_row(
"isp",
"Isp", fwd_val(
"Isp"),
"s",
"isp_s",
"chamber.isp_s")
165 pc_f = fwd_val(
"Pc", PSI)
166 for s, k, lab, _
in sides:
167 dpi = fwd_val(f
"delta_p_injector_{k}", PSI)
168 row(f
"stiffness_{s}", f
"{lab} injector dP/Pc", (dpi / pc_f)
if dpi
is not None and pc_f
else None,
"-",
169 firing_stats(ser(f
"{s}.stiffness")), f
"series.{s}.stiffness",
"yes",
170 "design band from design_requirements.injector_dp_ratio_*")
171 row(f
"dp_injector_{s}", f
"{lab} injector dP", dpi,
"psi", firing_stats(ser(f
"{s}.dp_injector_psi")),
172 f
"series.{s}.dp_injector_psi",
"yes")
173 row(f
"manifold_{s}", f
"{lab} manifold pressure", fwd_val(f
"P_injector_{k}", PSI),
"psia",
174 firing_stats(ser(f
"{s}.manifold_psia")), f
"series.{s}.manifold_psia",
"yes")
177 at0 = scalar(
_get(config,
"chamber_geometry.A_throat"))
if config
is not None else None
178 at0 = at0
or fwd_val(
"A_throat")
179 ratio = dl.get(
"throat_area_ratio")
180 row(
"throat_area",
"Throat area", at0 * 1e6
if at0
else None,
"mm^2",
181 dl_stats(
"throat_area_ratio", at0 * 1e6)
if (at0
and ratio
is not None)
else None,
182 "delivered.throat_area_ratio x A_t0",
"yes", why
or "recession rates unvalidated (AUDIT 1 #10)",
183 design_source=
"config chamber_geometry.A_throat (as built)")
184 eps0 = scalar(
_get(config,
"chamber_geometry.expansion_ratio"))
if config
is not None else None
185 row(
"eps",
"Expansion ratio", eps0,
"-", dl_stats(
"eps"),
"delivered.eps",
"yes",
186 design_source=
"config chamber_geometry.expansion_ratio")
187 lstar0 = scalar(
_get(config,
"chamber_geometry.Lstar"))
if config
is not None else None
188 row(
"lstar",
"L*", lstar0,
"m",
_stats(arr(rep.get(
"Lstar_m")))
if rep.get(
"Lstar_m")
else None,
189 "replay.Lstar_m",
"partly",
"the twin's card stays as built (AUDIT 3 #10)",
190 design_source=
"config chamber_geometry.Lstar")
191 row(
"eta_cstar",
"eta_c*", fwd_val(
"eta_cstar"),
"-",
192 _stats(arr(rep.get(
"eta_cstar")))
if rep.get(
"eta_cstar")
else None,
"replay.eta_cstar",
"partly",
193 why
or "the same model at each replay point; its inputs (E_m, SMD) stay assumed")
194 if config
is not None:
195 for s, k, lab, ck
in sides:
196 geo = orifice_geometry(config, s)
197 cd = cd_effective(ser(f
"{s}.mdot"), ser(f
"{s}.dp_injector_psi"), geo[
"area"], geo[
"rho"])
198 row(f
"cd_{s}", f
"{lab} orifice Cd", fwd_val(f
"Cd_{k}"),
"-", firing_stats(cd),
199 f
"derived: series.{s}.mdot / (A sqrt(2 rho series.{s}.dp_injector_psi))",
"no",
200 "the card's capacity is EngineDesign's Cd model (Lichtarowicz); the burn does not measure it")
201 for s, k, lab, ck
in sides:
202 rho_cfg = scalar(
_get(config, f
"fluids.{ck}.density"))
if isinstance(config.fluids, Mapping)
else None
203 fluid = side_fluid(prep, result, s)
or (
"oxygen" if s ==
"ox" else "ethanol")
204 T_l = ser(f
"{s}.liquid_K")
205 rho_twin = np.array([saturated_liquid_density(fluid, x)
if math.isfinite(x)
else math.nan
207 row(f
"density_{s}", f
"{lab} density (engine)", rho_cfg,
"kg/m^3", firing_stats(rho_twin),
208 f
"CoolProp saturated liquid at series.{s}.liquid_K (the twin's basis)",
"no",
209 "the twin prices liquid on the saturated line at T; the engine card keeps the config density "
210 "(AUDIT ledger #27)", design_source=f
"config fluids.{ck}.density")
211 zeta = scalar(
_get(config,
"chamber_geometry.nozzle_efficiency"))
212 row(
"nozzle_efficiency",
"Nozzle efficiency", zeta,
"-",
_one(zeta),
"config (unchanged by the burn)",
213 "no",
"schema default with no source (AUDIT D16)",
214 design_source=
"config chamber_geometry.nozzle_efficiency")
215 amb_d = fwd_val(
"P_ambient", PSI)
216 row(
"ambient",
"Ambient pressure", amb_d,
"psia", dl_stats(
"ambient_psia"),
"delivered.ambient_psia",
217 "partly",
"site ambient on the pad; altitude only inside RocketPy")
219 flight = result.get(
"flight")
or {}
220 sched = flight.get(
"schedule")
if flight.get(
"ok")
else None
221 if sched
and sched.get(
"t"):
222 a = np.interp(t, arr(sched[
"t"]), arr(sched[
"accel_m_s2"])) / G0
223 a_ref, a_note =
"flight.schedule.accel_m_s2 / g0",
"the flown pass's axial specific force"
226 a_ref, a_note =
"Setup.body_acceleration (pad: 1 g)",
"on the pad: one g"
227 row(
"specific_force",
"Axial specific force",
None,
"g", firing_stats(a), a_ref,
"partly",
228 f
"EngineDesign has no head term at all; {a_note}", design_source=
"none (no head term)")
229 for s, k, lab, _
in sides:
230 row(f
"tank_head_{s}", f
"{lab} tank head", 0.0,
"psi",
231 firing_stats(ser(f
"{s}.outlet_psia") - ser(f
"{s}.tank_psia")),
232 f
"series.{s}.outlet_psia - series.{s}.tank_psia",
"partly",
233 "no line heights on the drawing: only the tanks' own liquid feels the acceleration",
234 design_source=
"EngineDesign: no hydrostatic head")
235 if injector
and injector.get(
"momentum_ratio")
is not None:
236 row(
"momentum_ratio",
"Momentum ratio R", injector.get(
"design_momentum_ratio"),
"-",
237 firing_stats(arr(injector[
"momentum_ratio"], n)),
"diagnostics.injector.momentum_ratio",
238 "partly",
"derived on the twin's flows at the config densities")
239 summ = (dl.get(
"summary")
or {})
240 burn_cfg = scalar(
_get(config,
"thrust.burn_time"))
if config
is not None else None
241 row(
"burn_time",
"Burn time", burn_cfg,
"s",
_one((result.get(
"summary")
or {}).get(
"burn_time_s")),
242 "summary.burn_time_s",
"yes",
"the config's burn time is a stale field (AUDIT 4 #22)",
243 design_source=
"config thrust.burn_time")
245 if config
is not None:
246 load = (scalar(
_get(config,
"design_requirements.lox_tank_capacity_kg"))
or 0.0) + \
247 (scalar(
_get(config,
"design_requirements.fuel_tank_capacity_kg"))
or 0.0)
248 isp_f = fwd_val(
"Isp")
249 row(
"impulse",
"Total impulse", (load * isp_f * G0)
if (load
and isp_f)
else None,
"N s",
250 _one(summ.get(
"total_impulse_Ns")
if not no_replay
251 else (result.get(
"summary")
or {}).get(
"total_impulse_Ns")),
252 "delivered.summary.total_impulse_Ns" if not no_replay
else "summary.total_impulse_Ns",
"yes",
253 "design: propellant load x Forward Isp x g0",
254 design_source=
"config load x Forward Isp")
256 except Exception
as exc:
257 return unavailable(f
"{type(exc).__name__}: {exc}")
261 runner: Any =
None, forward: Optional[Mapping[str, Any]] =
None,
262 target_N: Optional[float] =
None, sampler: Any =
None) -> Dict[str, Any]:
263 """Every feed-diagnostic block of DATA-CONTRACT 3 this package builds, keyed as the contract
264 keys them under ``result["diagnostics"]``: ladder, regulator, solenoids, pressurant,
265 saturation, cavitation, injector, thrust_shape, ledger. One Forward solve at the lockup is
266 shared by the injector and the ledger. Each block fails on its own (``{available: false,
267 error}``); this never raises."""
275 out: Dict[str, Any] = {}
277 fwd = forward_design_point(prep, config, runner, forward)
280 out[
"ladder"] = build_ladder(result, prep, config)
281 out[
"regulator"] = build_regulator(result, prep, config)
282 out[
"solenoids"] = build_solenoids(result, prep, config)
283 reg = out[
"regulator"]
if isinstance(out[
"regulator"], Mapping)
and "error" not in out[
"regulator"]
else None
284 out[
"pressurant"] = build_pressurant(result, prep, config, regulator=reg)
285 out[
"saturation"] = build_saturation(result, prep, config)
286 out[
"cavitation"] = build_cavitation(result, prep, config)
287 out[
"injector"] = build_injector(result, prep, config, forward=fwd)
288 out[
"thrust_shape"] = build_thrust_shape(result, prep, config, sampler=sampler, target_N=target_N)
289 inj = out[
"injector"]
if isinstance(out[
"injector"], Mapping)
and "error" not in out[
"injector"]
else None
290 out[
"ledger"] =
build_ledger(result, prep, config, forward=fwd, injector=inj)
Any build_ledger(Mapping[str, Any] result, Any prep=None, Any config=None, *, Optional[Mapping[str, Any]] forward=None, Any runner=None, Optional[Mapping[str, Any]] injector=None)
Dict[str, Any] build_feed_diagnostics(Mapping[str, Any] result, Any prep=None, Any config=None, *, Any runner=None, Optional[Mapping[str, Any]] forward=None, Optional[float] target_N=None, Any sampler=None)