772def prepare(config: Any, runner: Any, drawing: Drawing, settings: LayerXSettings,
773 overrides: Optional[List[Any]] =
None) -> Prepared:
774 """Read, match, size, calibrate and check. Never raises for a problem a
775 person can fix: that becomes a ``fail`` check naming it."""
782 prep =
Prepared(settings=settings, drawing=drawing, config_sha256=config_fingerprint(config),
783 measurements=list(overrides
or []))
785 add =
lambda key, label, status, detail=
"": checks.append(
Check(key, label, status, detail))
789 add(
"options",
"Run options",
"fail",
"; ".join(problems) +
".")
795 payload, applied, missing = apply_overrides(drawing.payload, list(overrides
or []))
796 prep.overrides = applied
798 measured = sum(1
for a
in applied
if a[
"provenance"] ==
"measured")
799 add(
"overrides",
"Restated drawing parameters",
"info",
800 f
"{len(applied)} parameter(s) restated ({measured} measured): "
801 +
", ".join(f
"{a['key']} = {a['value']:g} {a['unit']}".rstrip()
for a
in applied[:6])
802 + (
" …" if len(applied) > 6
else "") +
".")
804 add(
"overrides_missing",
"Restated parameters the drawing no longer has",
"warn",
805 "The drawing has changed since these were entered: " +
", ".join(missing) +
".")
809 payload, ground = vehicle_payload(payload)
813 add(
"ground_support",
"Ground support on the drawing",
"info",
814 f
"{', '.join(ground)} {'is' if len(ground) == 1 else 'are'} joined to the vehicle only through "
815 "paired disconnects: ground support. The burn runs the vehicle alone, primed by Layer X "
816 "(loads, bottle fill, regulator dome), with those disconnects capped.")
817 if settings.pressurant:
820 add(
"pressurant",
"Pressurant",
"info",
821 f
"{was} swapped for {settings.pressurant} on {swapped} component(s) for this burn. The drawing's "
822 "regulator droop and line data were measured with its own gas.")
824 diagram = read_diagram(payload, name=drawing.name)
825 except DiagramError
as exc:
826 add(
"drawing",
"Drawing reads",
"fail", str(exc))
831 native = engine_from_config(config.model_dump(mode=
"json"), name=
"engine")
832 except Exception
as exc:
833 add(
"engine",
"Engine import into the feed model",
"fail", str(exc))
835 species = {
"oxidiser": native.oxidiser.propellant,
"fuel": native.fuel.propellant}
839 vehicle = vehicle_ids(diagram)
840 tanks = on_vehicle([n
for n
in diagram.nodes
if n.type ==
"TANK"], vehicle)
841 roles: Dict[str, str] = {}
843 tag = {t.id: (t.label
or t.id)
for t
in tanks}
844 for side, sp
in species.items():
845 found = [t.id
for t
in tanks
if (t.fluid
or "").lower() == sp.lower()]
847 roles[side] = found[0]
849 add(f
"tank_{side}", f
"{side.capitalize()} tank on the drawing",
"fail",
850 f
"The engine burns {sp}; no tank on '{drawing.name}' holds it "
851 f
"(tanks: {', '.join(f'{tag[t.id]} [{t.fluid}]' for t in tanks) or 'none'}).")
853 add(f
"tank_{side}", f
"{side.capitalize()} tank on the drawing",
"fail",
854 f
"{len(found)} tanks on the vehicle hold {sp} ({', '.join(tag[f] for f in found)}); Layer X needs exactly one.")
856 add(
"tanks",
"Tanks match the engine's propellants",
"ok",
857 f
"{tag[roles['oxidiser']]} holds {species['oxidiser']}, {tag[roles['fuel']]} holds {species['fuel']}.")
860 cea = cea_table_path(config)
862 add(
"cea",
"Combustion table",
"warn",
863 "CEA cache not found; the feed model's chamber uses a constant c* and reports no thrust.")
865 add(
"cea",
"Combustion table",
"ok", cea.name)
870 elevation = float(getattr(getattr(config,
"environment",
None),
"elevation", 0.0)
or 0.0)
871 ambient = compute_ambient_pressure_from_elevation(elevation)
if elevation > 0
else 101325.0
872 prep.ambient_pa = ambient
873 add(
"ambient",
"Site ambient pressure",
"info",
874 f
"{ambient / 1e3:.2f} kPa at {elevation:.0f} m. Layer X reports absolute pressures; gauge "
875 "readings are referenced to 101.325 kPa, as on the DAQ.")
879 probe_model = assemble_model(diagram, diagram_id=drawing.id, engine=native,
880 cea_cache=str(cea)
if cea
else "")
881 except Exception
as exc:
882 add(
"assembly",
"Drawing assembles with the engine",
"fail", str(exc))
885 if set(inlets) != {
"oxidiser",
"fuel"}:
886 add(
"engine_ports",
"Engine attaches to the drawing",
"fail",
887 "The drawing's ENGINE symbol did not resolve to an oxidiser and a fuel injector inlet.")
889 prep.inlet_nodes = inlets
898 edges = {str(e.get(
"id")): e
for e
in payload.get(
"edges")
or []
if isinstance(e, dict)}
900 for side, cside
in ((
"oxidiser",
"oxidizer"), (
"fuel",
"fuel")):
901 path = _path_to_tank(probe_model, inlets.get(side,
""), roles.get(side,
""))
902 last = edges.get(path[-1])
if path
else None
903 bore = ((last
or {}).get(
"data")
or {}).get(
"params", {}).get(
"bore")
if last
else None
904 feed = (getattr(config,
"feed_system",
None)
or {}).get(cside)
905 if not isinstance(bore, dict)
or feed
is None:
907 drawn_mm = float(bore[
"value"]) * {
"mm": 1.0,
"m": 1e3,
"in": 25.4}.get(str(bore.get(
"unit",
"mm")), 1.0)
908 d_exit = getattr(feed,
"d_exit",
None)
909 design_mm = (float(d_exit)
if d_exit
else (4.0 * float(feed.A_hydraulic) / 3.141592653589793) ** 0.5) * 1e3
910 if design_mm > 0
and abs(drawn_mm / design_mm - 1.0) > 0.10:
911 mismatched.append(f
"{'LOX' if side == 'oxidiser' else 'fuel'}: drawn {drawn_mm:.2f} mm, design {design_mm:.2f} mm "
912 f
"(dump ×{(design_mm / drawn_mm) ** 4:.1f})")
914 add(
"exit_bore",
"Line into the injector",
"warn",
915 "The manifold dump is priced on the design's exit bore, not the drawing's last line: "
916 +
"; ".join(mismatched) +
". Set feed_system.<side>.d_exit to the drawn bore, or redraw the line.")
925 rows = line_paths(payload, probe_model, inlets, roles, [o.key()
for o
in (overrides
or [])])
926 except Exception
as exc:
927 add(
"vehicle_lines",
"Feed-line heights",
"fail", f
"{type(exc).__name__}: {exc}")
929 prep.vehicle_lines = rows
930 name = {
"oxidiser":
"LOX",
"fuel":
"Fuel"}
931 how = {
"drawing":
"as drawn",
"restated":
"as restated",
"none":
"no height on the drawing"}
932 missing = [r
for r
in rows
if r[
"used"] ==
"none"]
933 steep = [b
for r
in rows
for b
in r[
"too_steep"]]
934 add(
"vehicle_lines",
"Feed-line heights",
"warn" if missing
or steep
else "info",
935 "; ".join(f
"{name[r['side']]}: {r['length_m']:.2f} m of line from the tank, falls {r['drop_m']:.2f} m "
936 f
"({how[r['used']]})" for r
in rows) +
"."
937 + (
" In flight a line with no height carries no head; only the tank's own liquid feels the "
938 "acceleration. Put the heights on the drawing in pid-designer, or restate each line's "
939 "elevation_change under Enter measured values." if missing
else "")
940 + (f
" {', '.join(steep)} falls further than it is drawn long: check the drawing." if steep
else ""))
942 def fluid_at(side: str, pressure: float) -> Tuple[float, float]:
943 cond = probe_model.built.network.conditions(inlets[side], pressure)
944 return float(cond.rho), float(cond.mu)
947 atm_psia = ATMOSPHERE / PSI
948 lox_psia = float(getattr(config.lox_tank,
"initial_pressure_psi", 0.0)
or 0.0)
949 fuel_psia = float(getattr(config.fuel_tank,
"initial_pressure_psi", 0.0)
or 0.0)
952 bottles = on_vehicle([n
for n
in diagram.nodes
if n.type ==
"KBOTTLE"], vehicle)
954 if bottles
and "pressure" in bottles[0].params:
957 drawn_copv = psig_from_psia(bottles[0].params[
"pressure"].si / PSI)
959 add(
"bottle",
"Pressurant bottle",
"fail",
"The vehicle has no bottle to press from.")
961 if settings.copv_pressure_psia
is not None:
962 copv_psig = settings.copv_pressure_psia - atm_psia
964 copv_psig = settings.copv_pressure_psig
if settings.copv_pressure_psig
is not None else drawn_copv
965 if copv_psig
is None:
966 add(
"bottle_pressure",
"Bottle pressure at T-0",
"fail",
967 "Neither the rail nor the drawing's bottle states a fill pressure.")
969 supply_pa = from_psig(copv_psig)
977 knob = next((k
for k
in knobs
if k[
"id"] == reg_id),
None)
978 if settings.dome_regulator
and knob
is None:
979 add(
"dome_regulator",
"Dome regulator",
"fail",
980 f
"{settings.dome_regulator!r} is not a regulator a dial can set on the vehicle "
981 f
"({', '.join(k['label'] for k in knobs) or 'none'}).")
983 named = reg_id
if settings.dome_regulator
else None
984 drawn_dome =
None if knob
is None or knob[
"drawn_psia"]
is None else knob[
"drawn_psia"] - atm_psia
985 if settings.dome_psia
is not None:
986 dome, source = settings.dome_psia - atm_psia,
"set on the rail"
987 elif settings.tank_pressure_psia
is not None:
988 dome, source =
None,
"solved for the rail's tank pressure"
989 elif drawn_dome
is not None:
990 dome, source = drawn_dome,
"as drawn"
992 dome, source =
None,
"solved for the design's tank pressure"
997 add(
"dome",
"Regulator dome",
"fail",
998 "No regulator on the vehicle takes a dome dial, so the dial cannot set the tanks.")
1000 target_psia = lock / PSI
1002 target_psia = settings.tank_pressure_psia
or lox_psia
1004 add(
"tank_pressure",
"Tank pressure",
"fail",
1005 "Neither the rail, the drawing's dome nor the config (lox_tank.initial_pressure_psi) "
1006 "states a tank pressure.")
1008 if settings.tank_pressure_psia
is None and fuel_psia
and abs(fuel_psia - lox_psia) > 0.5:
1009 add(
"tank_pressures",
"One tank pressure for both tanks",
"warn",
1010 f
"The config sets LOX {lox_psia:.1f} and fuel {fuel_psia:.1f} psia; the drawing feeds both "
1011 f
"from one regulator, so both start at {target_psia:.1f} psia.")
1012 dome =
dome_for_lockup(probe_model, target_psia * PSI, supply_pa, named)
1013 target_pa = target_psia * PSI
1015 where = f
"on {knob['label']}" if knob
is not None else "on the control regulator"
1017 add(
"dome",
"Regulator dome",
"warn" if not regs
else "fail",
1018 "No dome-loaded regulator on the drawing: tank pressure is whatever its regulator's "
1019 "setpoint gives, and the target cannot be dialled." if not regs
else
1020 "Could not solve the dome setting for that lockup.")
1023 add(
"dome",
"Regulator dome",
"ok",
1024 f
"{dome + atm_psia:.1f} psia {where} ({source}) gives {target_psia:.1f} psia tank lockup "
1025 f
"with the bottle at {copv_psig + atm_psia:.0f} psia.")
1034 trips = {v.id: r
for v
in list(tanks) + list(bottles)
if (r :=
vessel_trip(v, safety_factor))
is not None}
1035 reliefs = {v.id: r
for v
in list(tanks) + list(bottles)
if (r :=
vessel_reliefs(diagram, v.id))}
1036 rated_tanks = {t.id: trips[t.id]
for t
in tanks
if t.id
in trips}
1038 trip = trips.get(t.id)
1039 if trip
is not None and target_psia > trip[
"trip_psia"]:
1040 add(f
"mawp_{t.id}", f
"{t.label or t.id} rating",
"fail",
1041 f
"Lockup {target_psia:.1f} psia is over the {trip['words']} {t.label or t.id} trips the stand at.")
1042 fill_psia = from_psig(copv_psig) / PSI
1044 trip = trips.get(b.id)
1045 if trip
is not None and fill_psia > trip[
"trip_psia"]:
1046 add(f
"mawp_{b.id}", f
"{b.label or b.id} rating",
"fail",
1047 f
"Bottle fill {fill_psia:.0f} psia is over the {trip['words']} {b.label or b.id} trips the stand at.")
1050 bottle_ids = {b.id
for b
in bottles}
1051 lifts = [f
"{'bottle fill' if v.id in bottle_ids else 'lockup'} {p:.0f} psia is over {label}'s "
1052 f
"{set_pa / PSI:.0f} psi set pressure on {v.label or v.id} ({(set_pa + ATMOSPHERE) / PSI:.1f} psia)"
1053 for v, p
in [(b, fill_psia)
for b
in bottles] + [(t, target_psia)
for t
in tanks
if t.id
in roles.values()]
1054 for label, set_pa
in reliefs.get(v.id, ())
if p * PSI > set_pa + ATMOSPHERE]
1056 said =
"; ".join(lifts)
1057 add(
"relief",
"Relief valves at T-0",
"warn",
1058 said[:1].upper() + said[1:] +
": it lifts during the settle to lockup, so the burn starts from what "
1059 "the relief leaves, not from the pressure asked (the run's T-0 event says what it was).")
1063 unrated = [v.label
or v.id
for v
in list(tanks) + list(bottles)
if v.id
not in trips]
1064 guessed = [f
"{v.label or v.id} trips at {trips[v.id]['words']}" for v
in list(tanks) + list(bottles)
1065 if v.id
in trips
and trips[v.id][
"source"].lower()
in (
"estimated",
"default")]
1066 if unrated
or guessed:
1067 add(
"ratings",
"Vessel ratings",
"warn",
1068 (f
"No MAWP or burst pressure on the drawing for {', '.join(unrated)}: nothing checks those vessels. "
1070 + (f
"Estimated, not measured or from a datasheet: {'; '.join(guessed)}. The rating checks and the "
1071 "optimiser's tank-pressure ceiling stand on these." if guessed
else ""))
1073 gn2 =
gn2_on_lox(bottles[0].fluid
if bottles
else None,
1074 next((t
for t
in tanks
if t.id == roles.get(
"oxidiser")),
None), target_pa,
1075 test_mode=settings.test_mode
or "hotfire",
1076 acknowledged=bool(settings.ack_gn2_condensation))
1078 add(
"gn2_condensation",
"Nitrogen over LOX", gn2[
"status"], gn2[
"detail"])
1081 loads: Optional[Dict[str, float]] =
None
1082 tank_volumes = {t.id: (t.params[
"volume"].si
if "volume" in t.params
else None)
for t
in tanks}
1085 for side
in (
"oxidiser",
"fuel"):
1086 rho_tank[side] = fluid_at(side, target_pa)[0]
1087 if settings.load ==
"config":
1088 loads = {roles[
"oxidiser"]: float(config.lox_tank.mass), roles[
"fuel"]: float(config.fuel_tank.mass)}
1089 for side, tank_id
in roles.items():
1090 vol = tank_volumes.get(tank_id)
1091 liquid = loads[tank_id] / rho_tank[side]
1096 add(f
"load_{side}", f
"{side.capitalize()} load fits",
"fail",
1097 f
"{loads[tank_id]:.3f} kg is {liquid * 1e3:.2f} L in a {vol * 1e3:.2f} L tank.")
1098 elif frac > 0.95
or frac < 0.25:
1099 add(f
"load_{side}", f
"{side.capitalize()} load fits",
"warn",
1100 f
"{loads[tank_id]:.3f} kg fills {frac * 100:.0f} % of the {vol * 1e3:.2f} L tank.")
1102 add(f
"load_{side}", f
"{side.capitalize()} load fits",
"ok",
1103 f
"{loads[tank_id]:.3f} kg fills {frac * 100:.0f} % of the {vol * 1e3:.2f} L tank.")
1105 for side, cfg_tank
in ((
"oxidiser", config.lox_tank), (
"fuel", config.fuel_tank)):
1106 cfg_vol = getattr(cfg_tank,
"tank_volume_m3",
None)
1107 vol = tank_volumes.get(roles[side])
1108 if cfg_vol
and vol
and abs(vol - cfg_vol) / cfg_vol > 0.05:
1109 add(f
"volume_{side}", f
"{side.capitalize()} tank volume: drawing vs config",
"warn",
1110 f
"The drawing's tank is {vol * 1e3:.2f} L; the config's is {cfg_vol * 1e3:.2f} L. "
1111 "The burn follows the drawing. A different ullage is a different blowdown.")
1112 press = getattr(config,
"press_tank",
None)
1113 cfg_copv_L = getattr(press,
"free_volume_L",
None)
if press
is not None else None
1114 if bottles
and cfg_copv_L
and "volume" in bottles[0].params:
1115 drawn_L = bottles[0].params[
"volume"].si * 1e3
1116 if abs(drawn_L - cfg_copv_L) / cfg_copv_L > 0.05:
1117 add(
"volume_copv",
"Bottle volume: drawing vs config",
"warn",
1118 f
"The drawing's bottle is {drawn_L:.3f} L; the config's is {cfg_copv_L:.3f} L.")
1125 rise =
_burnout_tank_check(probe_model, dome, bottles[0], copv_psig, target_pa, roles, tanks, tank_volumes,
1126 loads, rho_tank, settings, ambient, config, trips=trips, reliefs=reliefs)
1127 except Exception
as exc:
1128 rise = {
"status":
"info",
"detail": f
"Could not estimate the tanks' rise over the burn ({type(exc).__name__}: {exc})."}
1129 rise_estimate =
None
1130 if rise
is not None:
1131 add(
"tank_rise",
"Tanks at burnout (regulator supply effect)", rise[
"status"], rise[
"detail"])
1132 rise_estimate = rise.get(
"estimate")
1136 machine = load_machine(tables=machines_dir())
1137 except Exception
as exc:
1138 add(
"machine",
"Valve sequence (DAQ state machine)",
"fail", str(exc))
1142 labels = {n.id: n.label
for n
in diagram.nodes
if n.id
in probe_model.built.actuators}
1143 binding = bind(machine, labels, roles=probe_model.built.valve_roles)
1144 mains = [a
for a
in machine.actuators
if "main" in a.lower()]
1145 unbound_mains = [a
for a
in mains
if a
not in binding.to_symbol]
1147 add(
"mains",
"Main valves bound to the Fire state",
"fail",
1148 f
"{', '.join(unbound_mains)} has no valve on the drawing, so Fire would not open it.")
1150 add(
"mains",
"Main valves bound to the Fire state",
"ok",
1151 ", ".join(f
"{a} → {binding.to_symbol[a]}" for a
in mains))
1152 prep.machine = machine
1156 link = link_engine(config, tank_pa_O=target_pa, tank_pa_F=target_pa, cea_path=cea,
1157 ambient_pa=ambient, fluid_at=fluid_at, mode=settings.engine_model,
1158 card_center_pa=(settings.card_center_psia * PSI
if settings.card_center_psia
else None))
1159 except Exception
as exc:
1160 add(
"engine_link",
"EngineDesign solves at T-0",
"fail", f
"{type(exc).__name__}: {exc}")
1166 if link.mode !=
"card":
1167 add(
"card_eroded_nozzle",
"Card nozzle follows the eroding throat",
"info",
1168 f
"The engine is '{link.mode}', not the engine card: nothing to scale.")
1172 link.chamber = expansion_chamber(link.card, link.sampler.runner.cea_cache, ambient_pressure=ambient,
1173 volume=link.design.chamber_volume)
1174 add(
"card_eroded_nozzle",
"Card nozzle follows the eroding throat",
"info",
1175 "The engine card's vacuum velocity is scaled by Cf_vac(ε(t))/Cf_vac(ε0) from EngineDesign's CEA table "
1176 "as the replay's throat opens (AUDIT D4-B)"
1177 + (
"." if link.chamber.eps_axis
else "; this CEA table has no expansion-ratio axis, so the scale is 1."))
1178 cal = link.calibration
1179 if link.mode ==
"card":
1182 within = bool(prov.get(
"within_tolerance"))
1183 add(
"engine_card",
"Engine card from EngineDesign",
"ok" if within
else "warn",
1184 f
"{prov['samples']} EngineDesign solves at the line exit ({prov['sampler']}, "
1185 f
"{prov['built_s']:.1f} s). The table reproduces EngineDesign's own model to Pc "
1186 f
"{fit['envelope_closed_pc'] * 100:.2f} %, thrust {fit['envelope_closed_thrust'] * 100:.2f} %, flow "
1187 f
"{fit['envelope_closed_mdot'] * 100:.2f} % on {int(fit['envelope_points'])} held-out solves (tolerance "
1188 f
"{fit['tolerance'] * 100:.1f} %; whole scan Pc {fit['box_closed_pc'] * 100:.1f} %). That is agreement "
1189 "with the model, not accuracy: the model's own unknowns (Cd, mixing, nozzle efficiency) are the "
1190 "uncertainty sweep's.")
1195 add(
"engine_boundary",
"Engine boundary at the line exit",
"info",
1196 f
"EngineDesign at the {target_psia:.1f} psia lockup with the drawing's lines taken as lossless "
1197 f
"(line exit = tank): line exit to chamber LOX {link.reference['dp_line_O'] / PSI:.1f} psi, fuel "
1198 f
"{link.reference['dp_line_F'] / PSI:.1f} psi, its exit dump into the manifold included. The burn's "
1199 "own drops are lower by what the drawing's lines lose (the run's injector ΔP). The drawing's lines "
1200 "carry everything upstream; EngineDesign's own feed_system K is not used.")
1201 elif link.mode ==
"calibrated":
1202 add(
"engine_link",
"Engine fitted to EngineDesign at T-0",
"warn",
1203 f
"Cd {cal['cd_O']:.3f} / {cal['cd_F']:.3f} (twin's own law: {cal['native_cd_O']:.3f} / "
1204 f
"{cal['native_cd_F']:.3f}); η_c* {cal['eta_cstar']:.4f}, η_n {cal['eta_n']:.4f}, held constant "
1205 "through the burn. The engine card follows EngineDesign everywhere instead.")
1207 gap = twin_injector_check(link, fluid_at)
1208 add(
"engine_link",
"Engine: the twin's native model",
"warn",
1209 "Not EngineDesign. At EngineDesign's T-0 flows the twin's injector drops "
1210 +
", ".join(f
"{side} {tw / PSI:.0f} psi vs {ed / PSI:.0f}" for side, (tw, ed)
in gap.items())
1211 +
"; its chamber runs η_c* = 1, so Pc is an upper bound.")
1212 for note
in link.notes:
1213 add(
"engine_note",
"Engine link",
"warn", note)
1214 if link.mode ==
"card":
1218 for side, key
in ((
"oxidiser",
"oxidizer"), (
"fuel",
"fuel")):
1219 fluid = (config.fluids
or {}).get(key)
1220 rho_ed = float(getattr(fluid,
"density", 0.0)
or 0.0)
1222 rho_twin = fluid_at(side, target_pa)[0]
1223 gaps.append((side, rho_ed, rho_twin, rho_twin / rho_ed - 1.0))
1225 worst =
max(abs(g[3])
for g
in gaps)
1226 add(
"density",
"Propellant density: EngineDesign vs feed model",
"warn" if worst > 0.01
else "info",
1227 "; ".join(f
"{side} {ed:.1f} vs {tw:.1f} kg/m³ ({gap * 100:+.1f} %)" for side, ed, tw, gap
in gaps)
1228 +
". The engine card is tabulated at EngineDesign's densities; flow scales with √ρ, so the flow "
1229 "error is half the density gap.")
1232 def assembler() -> Any:
1233 return assemble_model(diagram, diagram_id=drawing.id, engine=link.design, chamber=link.chamber,
1234 engine_reference=f
"live config {prep.config_sha256[:12]}",
1235 meta={
"diagram_name": drawing.name,
"diagram_sha256": drawing.sha256})
1239 except Exception
as exc:
1240 add(
"assembly",
"Drawing assembles with the engine",
"fail", str(exc))
1242 defaults = model.report.unchecked
1243 add(
"assumptions",
"Unspecified drawing parameters",
"info" if not defaults
else "warn",
1244 f
"{defaults} library default(s), {len(model.report.assumptions)} assumed value(s) in total; "
1245 "listed under Details.")
1248 prep.assembler = assembler
1252 chamber_tolerance_psi=CHAMBER_TOLERANCE_PSI,
1253 network_tolerance=NETWORK_TOLERANCE,
1258 **{k: v
for k, v
in ((
"ullage_collapse", settings.ullage_collapse), (
"ullage_vapour", settings.ullage_vapour),
1259 (
"chilldown", settings.chilldown), (
"line_walls", settings.line_walls))
if v
is not None},
1261 prep.plan = BurnPlan(
1262 tank_psi=psig_from_psia(target_psia),
1264 fill_fraction=settings.fill_fraction,
1266 hold_s=settings.hold_s,
1267 settle=settings.settle,
1269 horizon_s=settings.horizon_s,
1270 tanks=(roles[
"oxidiser"], roles[
"fuel"])
if len(roles) == 2
else None,
1274 end_on_depletion=
True,
1275 dry_kg=settings.dry_kg,
1283 add(
"feed_lines",
"Feed lines on the drawing",
"warn",
1284 "; ".join(gaps) +
". The graded chug margin and the line losses rest on what is drawn: give these "
1285 "lines a length and bore in pid-designer, or restate them under Enter measured values.")
1286 lead = float(getattr(prep.plan,
"lead_in_s", 0.5))
1287 if abs(lead / settings.dt - round(lead / settings.dt)) > 1e-6:
1290 add(
"time_step",
"Time step",
"fail",
1291 f
"{settings.dt * 1e3:g} ms does not divide the {lead:g} s before Fire into whole steps. "
1292 "Use 10, 20, 50 or 100 ms.")
1294 add(
"residual",
"Burn ends with the tanks empty",
"info",
1295 f
"The burn ends when the first tank is down to {settings.dry_kg * 1e3:g} g"
1296 + (
": burnt dry. No unusable residual (sump, line, pull-through) is stated; enter a measured one "
1297 "to charge for it." if settings.dry_kg <= 0.002
else ", the unusable residual you stated."))
1298 req = getattr(config,
"design_requirements",
None)
1300 def band(side: str) -> Optional[List[float]]:
1301 lo = getattr(req, f
"injector_dp_ratio_{side}_min",
None)
if req
is not None else None
1302 hi = getattr(req, f
"injector_dp_ratio_{side}_max",
None)
if req
is not None else None
1303 return [float(lo), float(hi)]
if lo
is not None and hi
is not None else None
1308 "feed_twin_thermal": {
1309 k: getattr(prep.setup, k)
for k
in (
"ullage_collapse",
"ullage_vapour",
"chilldown",
"line_walls",
1310 "stratification",
"wall_boiling",
"chilldown_nucleate",
"boiling_onset_K")
1311 if hasattr(prep.setup, k)},
1312 "stiffness_band": {
"oxidiser": band(
"O"),
"fuel": band(
"F")},
1315 "pressurant_gas": (bottles[0].fluid
if bottles
else None),
1316 "target_lockup_psia": target_psia,
1318 "copv_psig": copv_psig,
1319 "copv_psia": (from_psig(copv_psig)) / PSI,
1320 "copv_id": bottles[0].id
if bottles
else None,
1323 "copv_volume_L": (
None if not bottles
else bottles[0].params[
"volume"].si * 1e3
if "volume" in bottles[0].params
1324 else getattr(prep.setup,
"bottle_volume_L",
None)),
1328 "copv_mawp_psi": (trips[bottles[0].id][
"trip_psia"]
if bottles
and bottles[0].id
in trips
else None),
1329 "copv_drawn_psig": drawn_copv,
1332 "dome_psia": dome + atm_psia,
1333 "dome_source": source,
1334 "dome_regulator": knob[
"id"]
if knob
is not None else None,
1335 "dome_regulator_label": knob[
"label"]
if knob
is not None else None,
1336 "dome_drawn_psia":
None if drawn_dome
is None else drawn_dome + atm_psia,
1337 "dome_candidates": knobs,
1338 "copv_drawn_psia":
None if drawn_copv
is None else drawn_copv + atm_psia,
1339 "tank_mawp_psi": {tank_id: r[
"trip_psia"]
for tank_id, r
in rated_tanks.items()},
1341 "vessel_ratings": {v.id: {
"label": v.label
or v.id,
"kind":
"bottle" if v.id
in bottle_ids
else "tank",
1343 for v
in list(tanks) + list(bottles)
if v.id
in trips},
1345 "fill_fraction":
None if loads
else settings.fill_fraction,
1346 "tank_volumes_L": {k: (v * 1e3
if v
else None)
for k, v
in tank_volumes.items()},
1347 "ambient_pa": ambient,
1348 "gauge_zero_pa": ATMOSPHERE,
1349 "elevation_m": elevation,
1350 "inlet_nodes": inlets,
1351 "engine_model": link.mode,
1352 "cea_table": cea.name
if cea
else None,
1353 "config_sha256": prep.config_sha256,
1354 "binding": dict(binding.to_symbol),
1355 "unbound": list(binding.unmatched),
1356 "machine_warnings": list(machine.warnings),
1357 "overrides": prep.overrides,
1358 "vehicle_lines": prep.vehicle_lines
or None,
1360 "tank_rise_estimate": rise_estimate,