STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
reconcile.py
Go to the documentation of this file.
1"""Resize the injector so the design point holds under the drawing's feed system.
2
3EngineDesign sizes the injector at the tank pressure less one loss per side (``feed_system.K0``).
4The drawing's feed delivers something else: the tank sags below lockup while it flows, the lines
5have their own valves and bends, and in flight every liquid column carries the vehicle's
6acceleration. On the 6.8 kN stand the injector sees ~15 psi less LOX and ~13 psi less fuel than it
7was sized for, so it under-flows and the chamber runs below its design point.
8
9The reconciler holds everything that is built (chamber, throat, plate rings, channels, tank
10pressure) and changes only what the plate's holes set:
11
12* **hole diameters** (both rings), solved so the engine makes the target thrust at the target
13 O/F through the drawing's feed. That restores the design flows, so chamber pressure and Isp come
14 back with them. Each passage keeps its drilled length, so its L/d (and with it Cd) moves with the
15 diameter;
16* **jet angles** (optional, whole degrees, included angle held): the spray resultant's tilt depends
17 on the two streams' momenta, which change with the hole areas. The angles bring it back.
18
19The momentum-flux ratio R = (Cd_O/Cd_F) sqrt(dp_O/dp_F) is reported but not targeted: at fixed
20flows through a fixed feed, the two injector drops are set, and hole size cannot move them apart.
21
22The feed is the drawing's, as Layer X measures it (``engine.layerx.feedfit``): K0 per side fitted to
23a burn. Because the fit depends on the flow (the regulator droops more at higher flow), the
24reconciler burns, fits, resizes and burns again until the holes stop moving. The last burn is the
25reconciled injector on the drawing, and its numbers are the ones reported.
26
27Forward mode is the inner model: ``PintleEngineRunner.evaluate`` at the lockup, with the fitted K0.
28"""
29
30from __future__ import annotations
31
32import copy
33import math
34import time
35from dataclasses import dataclass, replace
36from typing import Any, Callable, Dict, List, Optional, Tuple
37
38from engine.layerx.prepare import PSI
39
40Progress = Callable[[str, float], None]
41
42#: Number drills, ASME B94.11M (diameter in inches): the range injector holes are drilled in.
43NUMBER_DRILLS_IN = {
44 40: 0.0980, 41: 0.0960, 42: 0.0935, 43: 0.0890, 44: 0.0860, 45: 0.0820, 46: 0.0810, 47: 0.0785,
45 48: 0.0760, 49: 0.0730, 50: 0.0700, 51: 0.0670, 52: 0.0635, 53: 0.0595, 54: 0.0550, 55: 0.0520,
46 56: 0.0465, 57: 0.0430, 58: 0.0420, 59: 0.0410, 60: 0.0400, 61: 0.0390, 62: 0.0380, 63: 0.0370,
47 64: 0.0360, 65: 0.0350, 66: 0.0330, 67: 0.0320, 68: 0.0310, 69: 0.0292, 70: 0.0280,
48}
49#: Metric drills in the small-hole range come in 0.05 mm steps.
50METRIC_STEP_MM = 0.05
51
52SIDES = (("oxidizer", "O", "LOX"), ("fuel", "F", "Fuel"))
53
54
55@dataclass
57 thrust_N: Optional[float] = None # None: the design point's thrust
58 of: Optional[float] = None # None: the design point's O/F
59 hold_spray_direction: bool = True
60 max_passes: int = 4
61 tolerance: float = 1.0e-3 # relative hole-diameter change between passes
62
63 @classmethod
64 def from_dict(cls, d: Dict[str, Any]) -> "ReconcileRequest":
65 return cls(**{k: d[k] for k in cls.__dataclass_fields__ if k in d and d[k] is not None})
66
67
68# ------------------------------------------------------------------ the engine at one point
69
70
71def _forward(config: Any, p_tank: Any, ambient: float) -> Dict[str, Any]:
72 """Forward mode at the lockup: performance, the injector's split, and the spray tilt.
73 ``p_tank`` is one pressure for both tanks or ``(p_O, p_F)`` [Pa]."""
74 from engine.core.injectors.impinging import momentum_ratio_R_from_bulk_velocities
75 from engine.core.runner import PintleEngineRunner
76 from engine.optimizer.layers.layer1_static_optimization import _impinging_resultant_tilt_deg
77
78 p_O, p_F = p_tank if isinstance(p_tank, tuple) else (p_tank, p_tank)
79 r = PintleEngineRunner(copy.deepcopy(config)).evaluate(p_O, p_F, P_ambient=ambient, silent=True)
80 g = config.injector.geometry
81 rho_O = float(config.fluids["oxidizer"].density)
82 rho_F = float(config.fluids["fuel"].density)
83 n = float(g.oxidizer.n_elements)
84 A_O = n * math.pi * float(g.oxidizer.d_jet) ** 2 / 4.0
85 A_F = float(g.fuel.n_elements) * math.pi * float(g.fuel.d_jet) ** 2 / 4.0
86 mO, mF, pc = float(r["mdot_O"]), float(r["mdot_F"]), float(r["Pc"])
87 inj = r.get("injector_pressure") or {}
88 dpO, dpF = inj.get("delta_p_injector_O"), inj.get("delta_p_injector_F")
89 return {
90 "thrust_N": float(r["F"]), "of": float(r["MR"]), "pc_psia": pc / PSI, "isp_s": float(r["Isp"]),
91 "mdot_O": mO, "mdot_F": mF,
92 "dp_O_psi": dpO / PSI if dpO is not None else None, "dp_F_psi": dpF / PSI if dpF is not None else None,
93 "stiffness_O": dpO / pc if dpO is not None and pc > 0 else None,
94 "stiffness_F": dpF / pc if dpF is not None and pc > 0 else None,
95 "momentum_ratio": momentum_ratio_R_from_bulk_velocities(rho_O, rho_F, mO / (rho_O * A_O), mF / (rho_F * A_F)),
96 "tilt_deg": _impinging_resultant_tilt_deg(
97 mO, mF, rho_O, rho_F, n, float(g.oxidizer.d_jet), float(g.fuel.d_jet),
98 float(g.oxidizer.impingement_angle), float(g.fuel.impingement_angle),
99 spacing_O_m=float(g.oxidizer.spacing), spacing_F_m=float(g.fuel.spacing)),
100 "d_O_mm": float(g.oxidizer.d_jet) * 1e3, "d_F_mm": float(g.fuel.d_jet) * 1e3,
101 "angle_O_deg": float(g.oxidizer.impingement_angle), "angle_F_deg": float(g.fuel.impingement_angle),
102 }
103
104
105# ------------------------------------------------------------------ editing a config copy
106
107
108def _with_feed(config: Any, fit: Dict[str, Any]) -> Any:
109 """``config`` with each side's feed_system replaced by the fitted K0 (feedfit.design_update)."""
110 from engine.layerx.feedfit import design_update
111
112 cfg = copy.deepcopy(config)
113 for side, upd in design_update(fit, run_id="")["feed_system"].items():
114 cfg.feed_system[side] = cfg.feed_system[side].model_copy(update=upd)
115 return cfg
116
117
118def _passage_length(config: Any, side: str) -> Optional[float]:
119 """The drilled passage length [m] (declared L/d x d), which a resized hole keeps."""
120 dc = config.discharge.get(side) if isinstance(config.discharge, dict) else getattr(config.discharge, side, None)
121 lod = getattr(dc, "orifice_l_over_d", None) if dc is not None else None
122 if lod is None or getattr(dc, "l_over_d_source", "declared") != "declared":
123 return None
124 return float(lod) * float(getattr(config.injector.geometry, side).d_jet)
125
126
127def _with_holes(config: Any, d: Dict[str, float], lengths: Dict[str, Optional[float]],
128 angles: Optional[Dict[str, float]] = None) -> Any:
129 cfg = copy.deepcopy(config)
130 for side, _, _ in SIDES:
131 geo = getattr(cfg.injector.geometry, side)
132 setattr(geo, "d_jet", float(d[side]))
133 if angles is not None:
134 setattr(geo, "impingement_angle", float(angles[side]))
135 L = lengths.get(side)
136 if L is not None:
137 cfg.discharge[side] = cfg.discharge[side].model_copy(update={"orifice_l_over_d": L / float(d[side])})
138 return cfg
139
140
141# ------------------------------------------------------------------ the inner solve
142
143
144def _solve_holes(config: Any, lengths: Dict[str, Optional[float]], target: Tuple[float, float],
145 p_tank: Any, ambient: float, angles: Optional[Dict[str, float]] = None,
146 tol: float = 2.0e-5, max_iter: int = 12) -> Tuple[Dict[str, float], Dict[str, Any], int]:
147 """Hole diameters that make (thrust, O/F) = ``target`` through ``config``'s feed: Newton on
148 (ln d_O, ln d_F) with a finite-difference Jacobian, steps capped at 5 % per iteration."""
149 F_t, of_t = target
150 g = config.injector.geometry
151 x = [math.log(float(g.oxidizer.d_jet)), math.log(float(g.fuel.d_jet))]
152
153 def evaluate(xx: List[float]) -> Tuple[List[float], Dict[str, Any]]:
154 d = {"oxidizer": math.exp(xx[0]), "fuel": math.exp(xx[1])}
155 point = _forward(_with_holes(config, d, lengths, angles), p_tank, ambient)
156 return [math.log(point["thrust_N"] / F_t), math.log(point["of"] / of_t)], point
157
158 r, point = evaluate(x)
159 it = 0
160 for it in range(1, max_iter + 1):
161 if max(abs(v) for v in r) < tol:
162 break
163 h = 1.0e-3
164 J = [[0.0, 0.0], [0.0, 0.0]]
165 for j in range(2):
166 xp = list(x)
167 xp[j] += h
168 rp, _ = evaluate(xp)
169 for i in range(2):
170 J[i][j] = (rp[i] - r[i]) / h
171 det = J[0][0] * J[1][1] - J[0][1] * J[1][0]
172 if not math.isfinite(det) or abs(det) < 1e-12:
173 raise ValueError("the hole sizes do not move thrust and O/F independently here")
174 dx = [-(J[1][1] * r[0] - J[0][1] * r[1]) / det, -(-J[1][0] * r[0] + J[0][0] * r[1]) / det]
175 cap = max(abs(v) for v in dx) / 0.05
176 if cap > 1.0:
177 dx = [v / cap for v in dx]
178 x = [x[0] + dx[0], x[1] + dx[1]]
179 r, point = evaluate(x)
180 if max(abs(v) for v in r) >= 1e-3:
181 raise ValueError(f"hole sizing did not converge (thrust {r[0] * 100:+.2f} %, O/F {r[1] * 100:+.2f} %)")
182 return {"oxidizer": math.exp(x[0]), "fuel": math.exp(x[1])}, point, it
183
184
185def _angles_for_tilt(config: Any, target_tilt: float, p_tank: Any, ambient: float,
186 point: Dict[str, Any]) -> Tuple[Dict[str, float], float]:
187 """Whole-degree jet angles, the included angle held, that bring the resultant tilt nearest
188 ``target_tilt`` at this point's flows. Returns the angles and the tilt they give."""
189 from engine.optimizer.layers.layer1_static_optimization import _impinging_resultant_tilt_deg
190
191 g = config.injector.geometry
192 req = getattr(config, "design_requirements", None)
193 a_O, a_F = float(g.oxidizer.impingement_angle), float(g.fuel.impingement_angle)
194 included = a_O + a_F
195 lo = float(getattr(req, "layer1_impinging_jet_angle_min_deg", None) or 0.0)
196 asym = getattr(req, "layer1_impinging_jet_angle_max_asym_deg", None)
197 rho_O, rho_F = float(config.fluids["oxidizer"].density), float(config.fluids["fuel"].density)
198
199 def tilt(aO: float, aF: float) -> float:
200 return _impinging_resultant_tilt_deg(
201 point["mdot_O"], point["mdot_F"], rho_O, rho_F, float(g.oxidizer.n_elements),
202 float(g.oxidizer.d_jet), float(g.fuel.d_jet), aO, aF,
203 spacing_O_m=float(g.oxidizer.spacing), spacing_F_m=float(g.fuel.spacing))
204
205 best = (a_O, a_F, tilt(a_O, a_F))
206 for step in range(-8, 9):
207 aO = round(a_O) + step
208 aF = included - aO
209 if aO < lo or aF < lo or (asym is not None and abs(aO - aF) > float(asym)):
210 continue
211 t = tilt(aO, aF)
212 if math.isfinite(t) and abs(t - target_tilt) < abs(best[2] - target_tilt) - 0.25:
213 best = (aO, aF, t)
214 return {"oxidizer": best[0], "fuel": best[1]}, best[2]
215
216
217# ------------------------------------------------------------------ fabrication
218
219
220def drills(d_mm: float) -> Dict[str, Any]:
221 """Nearest number drill (ASME B94.11M, #40-#70) and nearest 0.05 mm metric drill, with the
222 flow-area error each gives against ``d_mm``."""
223 out: Dict[str, Any] = {}
224 no, inch = min(NUMBER_DRILLS_IN.items(), key=lambda kv: abs(kv[1] * 25.4 - d_mm))
225 if abs(inch * 25.4 - d_mm) < 0.15:
226 out["number"] = {"drill": f"#{no}", "d_mm": inch * 25.4, "area_error": (inch * 25.4 / d_mm) ** 2 - 1.0}
227 m = round(d_mm / METRIC_STEP_MM) * METRIC_STEP_MM
228 out["metric"] = {"drill": f"{m:.2f} mm", "d_mm": m, "area_error": (m / d_mm) ** 2 - 1.0}
229 return out
230
231
232def drill_candidates(d_mm: float, n: int = 4) -> List[Dict[str, Any]]:
233 """The drills nearest ``d_mm``: number drills (ASME B94.11M #40-#70) and 0.05 mm metric, each
234 with the flow-area error it gives, nearest first."""
235 out = [{"drill": f"#{no}", "d_mm": inch * 25.4} for no, inch in NUMBER_DRILLS_IN.items()]
236 m0 = round(d_mm / METRIC_STEP_MM)
237 out += [{"drill": f"{k * METRIC_STEP_MM:.2f} mm", "d_mm": k * METRIC_STEP_MM} for k in range(m0 - 2, m0 + 3) if k > 0]
238 for c in out:
239 c["area_error"] = (c["d_mm"] / d_mm) ** 2 - 1.0
240 out.sort(key=lambda c: abs(c["d_mm"] - d_mm))
241 seen, picked = set(), []
242 for c in out:
243 key = round(c["d_mm"], 4)
244 if key not in seen:
245 seen.add(key)
246 picked.append(c)
247 return picked[:n]
248
249
250def drill_grid(fed: Any, lengths: Dict[str, Optional[float]], p_tank: Any, ambient: float,
251 options: Dict[str, List[Dict[str, Any]]], angles: Optional[Dict[str, float]]) -> List[Dict[str, Any]]:
252 """Forward mode through the fitted feed for every pairing of the drill options: what each pair
253 of drills makes, before it is burned."""
254 rows = []
255 for o in options["oxidizer"]:
256 for f in options["fuel"]:
257 pt = _forward(_with_holes(fed, {"oxidizer": o["d_mm"] * 1e-3, "fuel": f["d_mm"] * 1e-3}, lengths, angles),
258 p_tank, ambient)
259 rows.append({"oxidizer": o["drill"], "fuel": f["drill"], "d_O_mm": o["d_mm"], "d_F_mm": f["d_mm"],
260 **{k: pt[k] for k in ("thrust_N", "of", "pc_psia", "stiffness_O", "stiffness_F",
261 "momentum_ratio", "tilt_deg")}})
262 return rows
263
264
265def _changes(config: Any, final: Any, lengths: Dict[str, Optional[float]]) -> List[Dict[str, Any]]:
266 rows: List[Dict[str, Any]] = []
267 for side, k, label in SIDES:
268 g0, g1 = getattr(config.injector.geometry, side), getattr(final.injector.geometry, side)
269 d0, d1 = float(g0.d_jet) * 1e3, float(g1.d_jet) * 1e3
270 rows.append({
271 "item": f"{label} orifice diameter", "unit": "mm", "from": d0, "to": d1, "delta": d1 - d0,
272 "fabrication": "re-drill larger" if d1 > d0 + 1e-4 else ("new plate (smaller hole)" if d1 < d0 - 1e-4 else "no change"),
273 "drills": drills(d1),
274 })
275 L = lengths.get(side)
276 if L is not None:
277 rows.append({"item": f"{label} passage L/d", "unit": "", "from": L / (d0 * 1e-3), "to": L / (d1 * 1e-3),
278 "delta": L / (d1 * 1e-3) - L / (d0 * 1e-3),
279 "fabrication": f"passage stays {L * 1e3:.2f} mm long"})
280 a0, a1 = float(g0.impingement_angle), float(g1.impingement_angle)
281 if abs(a1 - a0) > 1e-9:
282 rows.append({"item": f"{label} jet angle", "unit": "deg", "from": a0, "to": a1, "delta": a1 - a0,
283 "fabrication": "new plate"})
284 return rows
285
286
287def design_update(final: Any, fit_update: Dict[str, Any], lengths: Dict[str, Optional[float]]) -> Dict[str, Any]:
288 """What 'write into the design' sends: holes, angles, passage L/d, and the fitted feed that
289 makes Forward mode agree with the burn."""
290 geo: Dict[str, Any] = {}
291 dis: Dict[str, Any] = {}
292 for side, _, _ in SIDES:
293 g = getattr(final.injector.geometry, side)
294 geo[side] = {"d_jet": float(g.d_jet), "impingement_angle": float(g.impingement_angle)}
295 if lengths.get(side) is not None:
296 dis[side] = {"orifice_l_over_d": round(float(final.discharge[side].orifice_l_over_d), 4)}
297 out: Dict[str, Any] = {"injector": {"geometry": geo}, **fit_update}
298 if dis:
299 out["discharge"] = dis
300 return out
301
302
303# ------------------------------------------------------------------ the loop
304
305
306def _burn_figures(res: Dict[str, Any]) -> Dict[str, Any]:
307 s = res.get("summary") or {}
308 dv = (res.get("delivered") or {}).get("summary") or {}
309 return {
310 "mean_thrust_N": dv.get("mean_thrust_N") or s.get("mean_thrust_N"),
311 "pc_mean_psia": dv.get("pc_mean_psia") or s.get("pc_mean_psia"),
312 "of_mean": s.get("of_mean"),
313 "total_impulse_Ns": dv.get("total_impulse_Ns") or s.get("total_impulse_Ns"),
314 "burn_time_s": s.get("burn_time_s"),
315 }
316
317
318def run_reconcile(config: Any, drawing: Any, settings: Any, overrides: List[Any], request: ReconcileRequest,
319 progress: Optional[Progress] = None, cancelled: Callable[[], bool] = lambda: False) -> Dict[str, Any]:
320 from engine.layerx.analysis import run_prepared
321 from engine.layerx.prepare import prepare
322
323 started = time.perf_counter()
324
325 def say(stage: str, frac: float) -> None:
326 if progress is not None:
327 progress(stage, frac)
328
329 settings = replace(settings, card_center_psia=None)
330 prep = prepare(config, None, drawing, settings, overrides)
331 if not prep.ok:
332 raise ValueError("Preflight failed: " + "; ".join(c.label for c in prep.checks if c.status == "fail"))
333 lockup_psia = float(prep.derived["target_lockup_psia"])
334 p_tank, ambient = lockup_psia * PSI, float(prep.ambient_pa)
335 lengths = {side: _passage_length(config, side) for side, _, _ in SIDES}
336
337 say("Design point", 0.02)
338 design = _forward(config, p_tank, ambient)
339 target = (float(request.thrust_N or design["thrust_N"]), float(request.of or design["of"]))
340 source = "custom" if request.thrust_N or request.of else "design point"
341
342 cfg = copy.deepcopy(config)
343 passes: List[Dict[str, Any]] = []
344 before: Optional[Dict[str, Any]] = None
345 before_burn: Optional[Dict[str, Any]] = None
346 final_cfg, fit_update, final_point, last_burn = cfg, {}, None, None
347 converged = False
348 n = max(1, int(request.max_passes))
349 for k in range(n):
350 if cancelled():
351 from engine.layerx.pool import Cancelled
352
353 raise Cancelled()
354 base = 0.05 + 0.9 * k / n
355 say(f"Burning, pass {k + 1}", base)
356 prep_k = prep if k == 0 else prepare(cfg, None, drawing, settings, overrides)
357 if not prep_k.ok:
358 raise ValueError("Preflight failed on pass " + str(k + 1) + ": "
359 + "; ".join(c.label for c in prep_k.checks if c.status == "fail"))
360 res = run_prepared(prep_k, cancelled=cancelled, replay=settings.replay is not False, config=cfg)
361 trip = res.get("tripped")
362 if trip:
363 # A burn stopped by a vessel trip is not the feed the holes would see: nothing to fit.
364 raise ValueError(f"the burn tripped on pass {k + 1}: {trip.get('message') or trip.get('vessel')} "
365 f"(t = {float(trip.get('t') or 0.0):.2f} s); lower the lockup or restate the rating")
366 fit = res.get("feed_fit") or {}
367 if not fit.get("available"):
368 raise ValueError(f"the feed could not be fitted on pass {k + 1}: {fit.get('error', 'no fit')}")
369 last_burn = _burn_figures(res)
370 if k == 0:
371 before_burn = last_burn
372 say(f"Sizing holes, pass {k + 1}", base + 0.6 / n)
373 fed = _with_feed(cfg, fit)
374 # The fit's sag is measured from each tank's own T-0 pressure: solve from there.
375 p_fit = (fit["sides"]["oxidizer"]["lockup_psia"] * PSI, fit["sides"]["fuel"]["lockup_psia"] * PSI)
376 if k == 0:
377 before = _forward(fed, p_fit, ambient)
378 d, point, iters = _solve_holes(fed, lengths, target, p_fit, ambient)
379 angles = None
380 if request.hold_spray_direction:
381 trial = _with_holes(fed, d, lengths)
382 angles, tilt = _angles_for_tilt(trial, design["tilt_deg"], p_fit, ambient, point)
383 g = fed.injector.geometry
384 if (angles["oxidizer"], angles["fuel"]) != (float(g.oxidizer.impingement_angle), float(g.fuel.impingement_angle)):
385 # Angles move the mixing, and the mixing moves c*: size the holes again at the new angles.
386 d, point, more = _solve_holes(fed, lengths, target, p_fit, ambient, angles)
387 iters += more
388 else:
389 angles = None
390 nxt = _with_holes(fed, d, lengths, angles)
391 last_fed, last_p, last_angles = fed, p_fit, angles
392 g0 = cfg.injector.geometry
393 change = max(abs(d["oxidizer"] / float(g0.oxidizer.d_jet) - 1.0), abs(d["fuel"] / float(g0.fuel.d_jet) - 1.0))
394 passes.append({
395 "pass": k + 1, "burn": last_burn,
396 "K0_O": fit["sides"]["oxidizer"]["K0"], "K0_F": fit["sides"]["fuel"]["K0"],
397 # The burn against the target: the holes are sized to the burn-MEAN feed, so the burn's
398 # mean thrust is the measure of whether they made the design point on the drawing (with
399 # the erosion replay on, it also carries the eroding throat, ~+0.6 % at 3.5 s). A fit's
400 # manifold gap is not a measure: from pass 2 the config carries the previous fit.
401 "thrust_error": ((last_burn.get("mean_thrust_N") or float("nan")) / target[0] - 1.0),
402 "d_O_mm": d["oxidizer"] * 1e3, "d_F_mm": d["fuel"] * 1e3, "change": change, "newton_iterations": iters,
403 })
404 from engine.layerx.feedfit import design_update as feed_update
405
406 final_cfg, fit_update, final_point = nxt, feed_update(fit, run_id=""), point
407 # The burn this pass made was of the injector it started with: once the holes stop moving,
408 # that injector is the reconciled one and the burn verifies it.
409 if change < request.tolerance:
410 converged = True
411 break
412 cfg = nxt
413
414 say("Drill options", 0.97)
415 g = final_cfg.injector.geometry
416 options = {"oxidizer": drill_candidates(float(g.oxidizer.d_jet) * 1e3),
417 "fuel": drill_candidates(float(g.fuel.d_jet) * 1e3)}
418 grid = drill_grid(last_fed, lengths, last_p, ambient, options, last_angles)
419 say("Done", 1.0)
420 fit_update["feed_system"] = {side: {**v, "derived_from": {**v["derived_from"], "by": "Layer X injector reconcile"}}
421 for side, v in fit_update.get("feed_system", {}).items()}
422 req = getattr(config, "design_requirements", None)
423 band = {
424 "stiffness_O": [getattr(req, "injector_dp_ratio_O_min", None), getattr(req, "injector_dp_ratio_O_max", None)],
425 "stiffness_F": [getattr(req, "injector_dp_ratio_F_min", None), getattr(req, "injector_dp_ratio_F_max", None)],
426 "momentum_ratio": [getattr(req, "impinging_momentum_R_min", None), getattr(req, "impinging_momentum_R_max", None)],
427 }
428 notes = []
429 if not converged:
430 notes.append(f"The holes still moved {passes[-1]['change'] * 100:.2f} % on the last of {n} passes; "
431 "the last burn is of the injector before that step.")
432 if any(r["fabrication"].startswith("new plate (smaller") for r in _changes(config, final_cfg, lengths)):
433 notes.append("A hole gets smaller: a drilled plate cannot be corrected; it needs a new plate.")
434 result = {
435 # The design these holes were sized from: writing them into a different one is refused.
436 "config_sha256": prep.config_sha256,
437 "condition": "in flight" if settings.flight else "on the pad",
438 "lockup_psia": lockup_psia,
439 "target": {"thrust_N": target[0], "of": target[1], "source": source},
440 "design": design,
441 "before": before,
442 "after": final_point,
443 "before_burn": before_burn,
444 "after_burn": last_burn,
445 "passes": passes,
446 "converged": converged,
447 "changes": _changes(config, final_cfg, lengths),
448 "band": band,
449 "design_update": design_update(final_cfg, fit_update, lengths),
450 "drill_options": options,
451 "drill_grid": grid,
452 "passage_length_m": lengths,
453 "angles": {side: float(getattr(g, side).impingement_angle) for side, _, _ in SIDES},
454 "notes": notes,
455 "wall_s": time.perf_counter() - started,
456 "basis": ("Holes sized in Forward mode at the lockup through the drawing's feed (K0 fitted to a Layer X "
457 "burn), then burned again on the drawing until they stop moving. Chamber, throat, rings, channels "
458 "and tank pressure held."),
459 }
460 # The same answer in the change-list format Optimize uses (engine/layerx/diff.py). The keys above
461 # stay: saved reconciles and the Injector page read them.
462 from engine.layerx.diff import from_reconcile
463
464 try:
465 result["change_list"] = from_reconcile(result)
466 except Exception as exc: # noqa: BLE001 - the reconcile stands without its change list, and says so
467 result["change_list"] = {"available": False, "error": f"{type(exc).__name__}: {exc}"}
468 return result
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
"ReconcileRequest" from_dict(cls, Dict[str, Any] d)
Definition reconcile.py:64
List[Dict[str, Any]] drill_grid(Any fed, Dict[str, Optional[float]] lengths, Any p_tank, float ambient, Dict[str, List[Dict[str, Any]]] options, Optional[Dict[str, float]] angles)
Definition reconcile.py:251
Any _with_feed(Any config, Dict[str, Any] fit)
Definition reconcile.py:108
Optional[float] _passage_length(Any config, str side)
Definition reconcile.py:118
Dict[str, Any] drills(float d_mm)
Definition reconcile.py:220
Dict[str, Any] _forward(Any config, Any p_tank, float ambient)
Definition reconcile.py:71
Tuple[Dict[str, float], Dict[str, Any], int] _solve_holes(Any config, Dict[str, Optional[float]] lengths, Tuple[float, float] target, Any p_tank, float ambient, Optional[Dict[str, float]] angles=None, float tol=2.0e-5, int max_iter=12)
Definition reconcile.py:146
Dict[str, Any] design_update(Any final, Dict[str, Any] fit_update, Dict[str, Optional[float]] lengths)
Definition reconcile.py:287
Dict[str, Any] _burn_figures(Dict[str, Any] res)
Definition reconcile.py:306
Dict[str, Any] run_reconcile(Any config, Any drawing, Any settings, List[Any] overrides, ReconcileRequest request, Optional[Progress] progress=None, Callable[[], bool] cancelled=lambda:False)
Definition reconcile.py:319
List[Dict[str, Any]] drill_candidates(float d_mm, int n=4)
Definition reconcile.py:232
Any _with_holes(Any config, Dict[str, float] d, Dict[str, Optional[float]] lengths, Optional[Dict[str, float]] angles=None)
Definition reconcile.py:128
List[Dict[str, Any]] _changes(Any config, Any final, Dict[str, Optional[float]] lengths)
Definition reconcile.py:265
Tuple[Dict[str, float], float] _angles_for_tilt(Any config, float target_tilt, Any p_tank, float ambient, Dict[str, Any] point)
Definition reconcile.py:186