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]."""
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")
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),
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."""
150 g = config.injector.geometry
151 x = [math.log(float(g.oxidizer.d_jet)), math.log(float(g.fuel.d_jet))]
153 def evaluate(xx: List[float]) -> Tuple[List[float], Dict[str, Any]]:
154 d = {
"oxidizer": math.exp(xx[0]),
"fuel": math.exp(xx[1])}
156 return [math.log(point[
"thrust_N"] / F_t), math.log(point[
"of"] / of_t)], point
158 r, point = evaluate(x)
160 for it
in range(1, max_iter + 1):
161 if max(abs(v)
for v
in r) < tol:
164 J = [[0.0, 0.0], [0.0, 0.0]]
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
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
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."""
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)
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)
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))
205 best = (a_O, a_F, tilt(a_O, a_F))
206 for step
in range(-8, 9):
207 aO = round(a_O) + step
209 if aO < lo
or aF < lo
or (asym
is not None and abs(aO - aF) > float(asym)):
212 if math.isfinite(t)
and abs(t - target_tilt) < abs(best[2] - target_tilt) - 0.25:
214 return {
"oxidizer": best[0],
"fuel": best[1]}, best[2]
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
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"),
275 L = lengths.get(side)
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"})
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]:
323 started = time.perf_counter()
325 def say(stage: str, frac: float) ->
None:
326 if progress
is not None:
327 progress(stage, frac)
329 settings = replace(settings, card_center_psia=
None)
330 prep = prepare(config,
None, drawing, settings, overrides)
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}
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"
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
348 n =
max(1, int(request.max_passes))
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)
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")
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')}")
371 before_burn = last_burn
372 say(f
"Sizing holes, pass {k + 1}", base + 0.6 / n)
375 p_fit = (fit[
"sides"][
"oxidizer"][
"lockup_psia"] * PSI, fit[
"sides"][
"fuel"][
"lockup_psia"] * PSI)
377 before =
_forward(fed, p_fit, ambient)
378 d, point, iters =
_solve_holes(fed, lengths, target, p_fit, ambient)
380 if request.hold_spray_direction:
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)):
386 d, point, more =
_solve_holes(fed, lengths, target, p_fit, ambient, 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))
395 "pass": k + 1,
"burn": last_burn,
396 "K0_O": fit[
"sides"][
"oxidizer"][
"K0"],
"K0_F": fit[
"sides"][
"fuel"][
"K0"],
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,
406 final_cfg, fit_update, final_point = nxt, feed_update(fit, run_id=
""), point
409 if change < request.tolerance:
414 say(
"Drill options", 0.97)
415 g = final_cfg.injector.geometry
418 grid =
drill_grid(last_fed, lengths, last_p, ambient, options, last_angles)
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)
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)],
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.")
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},
442 "after": final_point,
443 "before_burn": before_burn,
444 "after_burn": last_burn,
446 "converged": converged,
447 "changes":
_changes(config, final_cfg, lengths),
449 "design_update":
design_update(final_cfg, fit_update, lengths),
450 "drill_options": options,
452 "passage_length_m": lengths,
453 "angles": {side: float(getattr(g, side).impingement_angle)
for side, _, _
in SIDES},
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."),
465 result[
"change_list"] = from_reconcile(result)
466 except Exception
as exc:
467 result[
"change_list"] = {
"available":
False,
"error": f
"{type(exc).__name__}: {exc}"}
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)