STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_engine.py
Go to the documentation of this file.
1"""Phase 08: a Layer-1 engine dropped onto the end of a feed system.
2
3The promise being tested is narrow and checkable: **point it at what the
4optimizer wrote, and nothing gets retyped.** So most of these tests read real
5EngineDesign configs off disk when the sibling repo is present, and fall back to
6inline fixtures when it is not -- the fixtures are copied from the real schema,
7so a schema change breaks the on-disk tests loudly rather than leaving the
8inline ones passing against a format nobody uses any more.
9
10The two things that fail silently get the most attention. An **injector area**
11from the wrong formula is a wrong mass flow and every number downstream stays
12plausible, so each injector type's area is checked against its closed form. And
13a **combustion table read outside its range** returns a clamped number that
14looks like a physical plateau, so extrapolation is surfaced rather than absorbed.
15"""
16
17from __future__ import annotations
18
19import math
20from pathlib import Path
21
22import pytest
23
24from feedtwin.comps import FlowConditions
25from feedtwin.engine import (
26 CEATable,
27 Chamber,
28 ConstantCStar,
29 EngineImportError,
30 UnknownPropellant,
31 engine_from_config,
32 injector_areas,
33 injector_legs,
34 load_engine,
35 register_injector_type,
36 register_propellant_alias,
37 registered_injector_types,
38 species_for,
39)
40
41# The sibling app in this monorepo. It was a path on one laptop, so these tests
42# skipped everywhere else, CI included.
43ENGINE_DESIGN = Path(__file__).resolve().parents[3] / "EngineDesign"
44REAL_CONFIG = ENGINE_DESIGN / "configs" / "impinging_lox_ch4_8000N_optimal.yaml"
45REAL_CACHE = ENGINE_DESIGN / "output" / "cache" / "cea_cache_LOX_CH4_3D.npz"
46
47needs_engine_design = pytest.mark.skipif(
48 not REAL_CONFIG.exists(), reason="EngineDesign configs not present"
49)
50needs_cea = pytest.mark.skipif(
51 not REAL_CACHE.exists(), reason="EngineDesign CEA cache not present"
52)
53
54
55def impinging_config() -> dict[str, object]:
56 """The real schema, trimmed to what the importer reads."""
57 return {
58 "injector": {
59 "type": "impinging",
60 "geometry": {
61 "oxidizer": {
62 "n_elements": 18,
63 "d_jet": 0.0029682419398978075,
64 "impingement_angle": 76.4,
65 "spacing": 0.0115,
66 },
67 "fuel": {
68 "n_elements": 18,
69 "d_jet": 0.0020731739365082054,
70 "impingement_angle": 71.7,
71 "spacing": 0.0051,
72 },
73 },
74 },
75 "discharge": {
76 "oxidizer": {"Cd_inf": 0.4, "a_Re": 0.15, "Cd_min": 0.15},
77 "fuel": {"Cd_inf": 0.4, "a_Re": 0.15, "Cd_min": 0.15},
78 },
79 "fluids": {
80 "oxidizer": {"name": "LOX", "temperature": 90.0},
81 "fuel": {"name": "Methane", "temperature": 112.0},
82 },
83 "chamber_geometry": {
84 "A_throat": 0.0013982470588664619,
85 "expansion_ratio": 7.01837178880855,
86 "volume": 0.002081806580056309,
87 "design_pressure": 2413166.0,
88 "design_MR": 3.5,
89 "design_thrust": 8000.0,
90 },
91 }
92
93
94# ------------------------------------------------------------- the import
95
96
97@needs_engine_design
99 """The Phase 08 promise, against the file the optimizer actually wrote."""
100 design = load_engine(REAL_CONFIG)
101 assert design.injector_type == "impinging"
102 assert design.oxidiser.propellant == "oxygen"
103 assert design.fuel.propellant == "methane"
104 assert design.throat_area > 0.0
105 assert design.design_thrust == 8000.0
106 assert design.design_mixture_ratio == 3.5
107 # Every quantity crossing the boundary says where it came from.
108 assert "fluids.oxidizer.name" in design.provenance["oxidizer.propellant"]
109 assert "discharge.oxidizer" in design.provenance["oxidizer.Cd"]
110
111
112@needs_engine_design
114 """A schema drift in EngineDesign should break here, loudly."""
115 configs = sorted((ENGINE_DESIGN / "configs").glob("*.yaml"))
116 assert configs, "no configs found to check against"
117 imported = 0
118 for path in configs:
119 try:
120 design = load_engine(path)
121 except EngineImportError:
122 continue # not every yaml in that folder is a full engine config
123 assert design.throat_area > 0.0
124 assert design.oxidiser.area > 0.0 and design.fuel.area > 0.0
125 imported += 1
126 assert imported >= 3
127
128
130 design = engine_from_config(impinging_config(), name="inline")
131 assert design.injector_type == "impinging"
132 assert design.oxidiser.element_count == 18
133
134
136 """An import failure saying only 'missing field' sends someone hunting."""
137 config = impinging_config()
138 del config["chamber_geometry"]["A_throat"] # type: ignore[index]
139 with pytest.raises(EngineImportError, match="A_throat"):
140 engine_from_config(config, name="inline")
141
142 config = impinging_config()
143 del config["injector"]["geometry"]["fuel"] # type: ignore[index]
144 with pytest.raises(EngineImportError, match="fuel"):
145 engine_from_config(config, name="inline")
146
147
149 config = impinging_config()
150 config["fluids"]["fuel"]["name"] = "Methaine" # type: ignore[index]
151 with pytest.raises(EngineImportError, match="methane"):
152 engine_from_config(config, name="inline")
153
154
156 """A wrong area is a wrong mass flow, and everything after it looks fine."""
157 config = impinging_config()
158 config["injector"]["type"] = "swirl" # type: ignore[index]
159 with pytest.raises(EngineImportError, match="no area extractor"):
160 engine_from_config(config, name="inline")
161
162
164 assert species_for("LOX") == species_for("oxygen") == "oxygen"
165 assert species_for("CH4") == species_for("Methane") == "methane"
166 assert species_for("EtOH") == "ethanol"
167 with pytest.raises(UnknownPropellant):
168 species_for("unobtainium")
169 register_propellant_alias("unobtainium", "helium")
170 assert species_for("Unobtainium") == "helium"
171
172
173# -------------------------------------------------------- injector geometry
174
175
177 geometry = impinging_config()["injector"]["geometry"] # type: ignore[index]
178 area, hydraulic, count = injector_areas("impinging", geometry, "oxidizer")
179 d = 0.0029682419398978075
180 assert area == pytest.approx(18 * math.pi * d * d / 4.0, rel=1e-12)
181 assert hydraulic == pytest.approx(d)
182 assert count == 18
183
184
186 """The asymmetry is the trap: one formula applied to both is silently wrong."""
187 geometry = {
188 "lox": {"n_orifices": 14, "d_orifice": 0.00272},
189 "fuel": {"d_pintle_tip": 0.030, "h_gap": 0.0012},
190 }
191 ox_area, ox_d, ox_n = injector_areas("pintle", geometry, "oxidizer")
192 assert ox_area == pytest.approx(14 * math.pi * 0.00272**2 / 4.0, rel=1e-12)
193 assert ox_n == 14
194
195 fuel_area, fuel_d, _ = injector_areas("pintle", geometry, "fuel")
196 r_in, r_out = 0.015, 0.015 + 0.0012
197 assert fuel_area == pytest.approx(math.pi * (r_out**2 - r_in**2), rel=1e-12)
198 # An annulus's hydraulic diameter is twice the gap, not the tip diameter.
199 assert fuel_d == pytest.approx(2 * 0.0012)
200
201 # And the two sides genuinely differ -- the orifice formula on the fuel
202 # annulus would give something else entirely.
203 assert fuel_area != pytest.approx(ox_area)
204
205
207 geometry = {
208 "core": {"n_ports": 7, "d_port": 0.0018},
209 "annulus": {"inner_diameter": 0.006, "gap_thickness": 0.0004},
210 }
211 ox_area, _, ox_n = injector_areas("coaxial", geometry, "oxidizer")
212 assert ox_area == pytest.approx(7 * math.pi * 0.0018**2 / 4.0, rel=1e-12)
213 assert ox_n == 7
214 fuel_area, fuel_d, _ = injector_areas("coaxial", geometry, "fuel")
215 assert fuel_area == pytest.approx(math.pi * (0.0034**2 - 0.003**2), rel=1e-12)
216 assert fuel_d == pytest.approx(0.0008)
217
218
220 assert set(registered_injector_types()) >= {"impinging", "pintle", "coaxial"}
221 register_injector_type("slot", lambda geometry, side: (1e-5, 1e-3, 1))
222 assert "slot" in registered_injector_types()
223 assert injector_areas("slot", {}, "fuel") == (1e-5, 1e-3, 1)
224 with pytest.raises(KeyError, match="unknown injector type"):
225 injector_areas("vortex", {}, "fuel")
226
227
228# --------------------------------------------------------------- discharge
229
230
232 """Must agree with EngineDesign's own answer or the two model different hardware."""
233 design = engine_from_config(impinging_config(), name="inline")
234 model = design.oxidiser.discharge
235 for reynolds in (1e3, 1e4, 1e5, 1e6):
236 expected = 0.4 - 0.15 / math.sqrt(reynolds)
237 assert model.cd(reynolds) == pytest.approx(
238 min(max(expected, 0.15), 0.4), rel=1e-12
239 )
240
241
243 design = engine_from_config(impinging_config(), name="inline")
244 model = design.oxidiser.discharge
245 # Cd = 0.4 - 0.15/sqrt(Re) only reaches the 0.15 floor below Re ~ 0.36,
246 # which is deep creeping flow. The floor exists for exactly that region.
247 assert model.cd(0.2) == pytest.approx(model.cd_min)
248 assert model.cd(1.0) == pytest.approx(0.25) # above the floor, unclamped
249 assert model.cd(1e12) <= model.cd_inf
250 assert model.cd(0.0) == model.cd_min
251 assert model.cd(-5.0) == model.cd_min
252
253
255 """Low-Re Cd is materially below its high-Re value, which matters at startup."""
256 design = engine_from_config(impinging_config(), name="inline")
257 side = design.oxidiser
258 low = side.cd_at(0.01, rho=1140.0, mu=1.8e-4)
259 high = side.cd_at(2.0, rho=1140.0, mu=1.8e-4)
260 assert low < high <= side.discharge.cd_inf
261
262
263# ------------------------------------------------------------- the injector
264
265
267 design = engine_from_config(impinging_config(), name="inline")
268 ox, _ = injector_legs(design)
269 flow = FlowConditions(rho=1140.0, mu=1.8e-4, p_upstream=4.0e6)
270 mdot = 1.8
271 area = ox.effective_area(mdot, flow)
272 assert ox.pressure_drop(mdot, flow) == pytest.approx(
273 mdot * mdot / (2.0 * 1140.0 * area * area), rel=1e-12
274 )
275
276
278 design = engine_from_config(impinging_config(), name="inline")
279 ox, _ = injector_legs(design)
280 flow = FlowConditions(rho=1140.0, mu=1.8e-4, p_upstream=4.0e6)
281 assert ox.pressure_drop(1.0, flow) == pytest.approx(ox.pressure_drop(-1.0, flow))
282 # Not exactly 4x, because Cd rises with Reynolds -- and that is the point.
283 ratio = ox.pressure_drop(2.0, flow) / ox.pressure_drop(1.0, flow)
284 assert 3.5 < ratio < 4.0
285
286
288 """A sanity check on the imported areas, in the units a designer thinks in."""
289 design = engine_from_config(impinging_config(), name="inline")
290 ox, fuel = injector_legs(design)
291 lox = FlowConditions(rho=1140.0, mu=1.8e-4, p_upstream=4.0e6)
292 ch4 = FlowConditions(rho=422.6, mu=1.2e-4, p_upstream=4.0e6)
293
294 mdot_total = 1.9
295 mr = design.design_mixture_ratio
296 mdot_ox = mdot_total * mr / (1.0 + mr)
297 mdot_fuel = mdot_total / (1.0 + mr)
298
299 pc = design.design_chamber_pressure
300 for leg, mdot, conditions in ((ox, mdot_ox, lox), (fuel, mdot_fuel, ch4)):
301 stiffness = leg.pressure_drop(mdot, conditions) / pc
302 assert 0.05 < stiffness < 1.0, f"{leg.id} stiffness {stiffness:.3f}"
303
304
305# ----------------------------------------------------------------- chamber
306
307
309 chamber = Chamber(1.4e-3, ConstantCStar(cstar=1800.0, thrust_coefficient=1.5))
310 result = chamber.evaluate(1.5, 0.4)
311 assert result.pressure == pytest.approx(1.9 * 1800.0 / 1.4e-3, rel=1e-12)
312 assert result.mixture_ratio == pytest.approx(1.5 / 0.4)
313 assert result.thrust == pytest.approx(1.5 * result.pressure * 1.4e-3, rel=1e-12)
314
315
317 """Not a numerical guard: a chamber is open to atmosphere through its nozzle.
318
319 Without this the loop drives chamber pressure toward zero when nothing is
320 flowing, and then asks the feed system to solve a 650 psi drop into 0.5 psi
321 -- a state no hardware occupies and no solver enjoys.
322 """
323 chamber = Chamber(1.4e-3, ConstantCStar(cstar=1800.0, thrust_coefficient=1.5))
324 assert chamber.evaluate(0.0, 0.0).pressure == pytest.approx(101325.0)
325 trickle = chamber.evaluate(1e-6, 1e-6)
326 assert trickle.pressure == pytest.approx(101325.0)
327 assert trickle.thrust == 0.0 # and no thrust is reported for a shut engine
328 assert not chamber.is_firing(trickle)
329 assert chamber.is_firing(chamber.evaluate(1.5, 0.4))
330
331
333 vacuum = Chamber(1.4e-3, ConstantCStar(cstar=1800.0), ambient_pressure=1.0)
334 assert vacuum.evaluate(0.0, 0.0).pressure == pytest.approx(1.0)
335
336
338 """Why the chamber is quasi-steady: it responds 1000x faster than the feed."""
339 chamber = Chamber(1.4e-3, ConstantCStar(cstar=1800.0), volume=2.08e-3)
340 tau = chamber.fill_time(3.0e6, 3.5)
341 assert 1e-4 < tau < 5e-3
342
343
344@needs_cea
346 table = CEATable(REAL_CACHE, expansion_ratio=7.0)
347 assert table.propellants == ("LOX", "CH4")
348 state = table.combustion(3.0e6, 3.5)
349 assert 1700 < state.cstar < 1900
350 assert 3200 < state.temperature < 3600
351 assert 1.10 < state.gamma < 1.20
352 assert 1.3 < state.thrust_coefficient < 1.8
353 assert not state.extrapolated
354
355
356@needs_cea
358 """A real physical shape, not a monotone fit: LOX/CH4 peaks near O/F 2.8."""
359 table = CEATable(REAL_CACHE, expansion_ratio=7.0)
360 ratios = [2.5, 2.8, 3.2, 3.6, 4.0]
361 cstars = [table.combustion(3.0e6, mr).cstar for mr in ratios]
362 peak = cstars.index(max(cstars))
363 assert 0 <= peak <= 2, f"c* peaked at O/F {ratios[peak]}"
364 assert cstars[-1] < max(cstars)
365
366
367@needs_cea
369 """A clamped c* looks like a physical plateau on a plot. It is a table edge."""
370 table = CEATable(REAL_CACHE, expansion_ratio=7.0)
371 inside = table.combustion(3.0e6, 3.5)
372 assert not inside.extrapolated
373 assert table.contains(3.0e6, 3.5)
374
375 outside = table.combustion(3.0e6, 6.0)
376 assert outside.extrapolated
377 assert not table.contains(3.0e6, 6.0)
378 # Clamped to the edge rather than extrapolated into fiction.
379 edge = table.combustion(3.0e6, float(table.mixture_ratios[-1]))
380 assert outside.cstar == pytest.approx(edge.cstar)
381
382
383@needs_cea
385 """The output that makes O/F drift visible instead of merely tracked."""
386 table = CEATable(REAL_CACHE, expansion_ratio=7.0)
387 cold = table.combustion(3.0e6, 2.5).temperature
388 hot = table.combustion(3.0e6, 3.6).temperature
389 assert hot > cold + 100.0
390
391
392# ------------------------------------------------------- the coupled engine
393
394
395@needs_cea
396@needs_engine_design
398 """Why a pressure-fed engine is stable in the large.
399
400 Raise the feed pressure and chamber pressure rises, but by *less*, because
401 the rise eats into the injector's own pressure difference. If chamber
402 pressure tracked feed pressure one-for-one there would be no loop.
403 """
404 design = load_engine(REAL_CONFIG)
405 chamber = Chamber(
406 design.throat_area, CEATable(REAL_CACHE, expansion_ratio=design.expansion_ratio)
407 )
408 ox, fuel = injector_legs(design)
409
410 def fire(feed: float) -> tuple[float, float]:
411 pc = 2.0e6
412 for _ in range(200):
413 lox = FlowConditions(rho=1140.0, mu=1.8e-4, p_upstream=feed)
414 ch4 = FlowConditions(rho=422.6, mu=1.2e-4, p_upstream=feed)
415 dp = max(feed - pc, 1.0)
416 mdot_ox = math.sqrt(2.0 * 1140.0 * dp) * ox.effective_area(1.5, lox)
417 mdot_fuel = math.sqrt(2.0 * 422.6 * dp) * fuel.effective_area(0.4, ch4)
418 new = chamber.evaluate(mdot_ox, mdot_fuel).pressure
419 if abs(new - pc) < 1.0:
420 pc = new
421 break
422 pc += 0.3 * (new - pc)
423 return pc, feed - pc
424
425 low_pc, low_dp = fire(4.0e6)
426 high_pc, high_dp = fire(5.0e6)
427
428 assert high_pc > low_pc
429 # The chamber absorbed part of the 1 MPa: it did not move the full amount.
430 assert (high_pc - low_pc) < 1.0e6
431 # And the rest went into injector stiffness.
432 assert high_dp > low_dp
433
434
435# ------------------------------------------------- the vehicle's own engine
436
437
438ETHALOX = ENGINE_DESIGN / "configs" / "ethalox_doublet_7000N.yaml"
439ETHALOX_CACHE = ENGINE_DESIGN / "output" / "cache" / "cea_cache_LOX_Ethanol_3D.npz"
440
441needs_ethalox = pytest.mark.skipif(
442 not ETHALOX.exists() or not ETHALOX_CACHE.exists(),
443 reason="the ethalox config or its CEA cache is not present",
444)
445
446
447@needs_ethalox
449 """LOX/ethanol, impinging doublet. The engine actually being built."""
450 design = load_engine(ETHALOX)
451 assert design.injector_type == "impinging"
452 assert design.oxidiser.propellant == "oxygen"
453 assert design.fuel.propellant == "ethanol"
454 assert design.oxidiser.element_count == 26
455 assert design.fuel.element_count == 26
456 # 7200, not the 7000 in `chamber_geometry.design_thrust`. That key holds the
457 # last optimiser run's achievement; `design_requirements.target_thrust` holds
458 # what was asked for, and intent wins here exactly as it already did for O/F
459 # and chamber pressure. This assertion used to encode the gap.
460 assert design.design_thrust == 7200.0
461
462
463@needs_ethalox
465 """A cross-check between the two tools, not a restatement of one of them.
466
467 The config asks for ``optimal_of_ratio: 1.65``. Reading c* straight off the
468 CEA table the optimiser used, the peak should land there — and if it does
469 not, one of the two is looking at the wrong propellant pair.
470 """
471 design = load_engine(ETHALOX)
472 table = CEATable(ETHALOX_CACHE, expansion_ratio=design.expansion_ratio)
473 assert table.propellants == ("LOX", "Ethanol")
474
475 ratios = [1.2, 1.4, 1.65, 1.9, 2.2]
476 cstars = [table.combustion(2.9e6, mr).cstar for mr in ratios]
477 peak = ratios[cstars.index(max(cstars))]
478 assert peak == pytest.approx(1.65, abs=0.3)
479
480
481@needs_ethalox
483 """The config asks for 20–30% injector stiffness on both legs."""
484 design = load_engine(ETHALOX)
485 ox, fuel = injector_legs(design)
486 lox = FlowConditions(rho=1140.0, mu=1.8e-4, p_upstream=4.0e6)
487 ethanol = FlowConditions(rho=789.0, mu=1.2e-3, p_upstream=4.0e6)
488
489 # At the design point: 7000 N, Pc 420 psi, O/F 1.65.
490 pc = 420.0 * 6894.757293168361
491 table = CEATable(ETHALOX_CACHE, expansion_ratio=design.expansion_ratio)
492 cstar = table.combustion(pc, 1.65).cstar
493 mdot_total = pc * design.throat_area / cstar
494 mdot_ox = mdot_total * 1.65 / 2.65
495 mdot_fuel = mdot_total / 2.65
496
497 for leg, mdot, conditions in ((ox, mdot_ox, lox), (fuel, mdot_fuel, ethanol)):
498 stiffness = leg.pressure_drop(mdot, conditions) / pc
499 assert 0.10 < stiffness < 0.60, f"{leg.id} stiffness {stiffness:.3f}"
500
501
502@needs_ethalox
504 None
505):
506 """The ethalox config carries its design point twice, and they disagree.
507
508 ``chamber_geometry`` is what the geometry was last sized at:
509 ``design_MR`` 2.55, a leftover from when this config was LOX/methane, and
510 ``design_pressure`` 350 psi. ``design_requirements`` is what the optimiser
511 was told to hit: ``optimal_of_ratio`` 1.65, where the LOX/ethanol c*
512 actually peaks, and ``target_chamber_pressure_psi`` 420.
513
514 **Intent wins**, for both. The geometry block goes stale every time a
515 config is re-run or switches propellant pair, so a disagreement is the
516 normal state of a config being worked on -- and this used to raise a
517 warning on it, which is how a healthy config came to shout at its operator
518 twice on every import.
519
520 Not silently, though. Which field was used, and what the other one said, is
521 recorded in provenance, so the choice is auditable without being noise.
522 """
523 design = load_engine(ETHALOX)
524
525 assert design.design_mixture_ratio == pytest.approx(1.65)
526 assert design.design_chamber_pressure / 6894.757293168361 == pytest.approx(
527 420.0, abs=0.5
528 )
529 assert not design.warnings, "a config disagreeing with itself is not a warning"
530
531 for key, stale in (
532 ("mixture_ratio", "2.55"),
533 ("design_chamber_pressure", "350"),
534 ):
535 note = design.provenance[key]
536 assert "intent wins" in note, f"{key} does not say which field won"
537 assert stale in note, f"{key} does not record what the stale field said"
538
539
540@needs_engine_design
542 """A warning that fires on healthy input is noise, and noise gets ignored."""
543 assert load_engine(REAL_CONFIG).warnings == ()
544
545
546@needs_ethalox
548 """`use_geometry_cd` is on in this config, so hole size moves Cd_inf."""
549 design = load_engine(ETHALOX)
550 model = design.oxidiser.discharge
551 assert model.use_geometry_cd
552
553 # 2 mm is the reference: no adjustment there.
554 assert model.cd_infinite(2.0e-3) == pytest.approx(model.cd_inf, rel=1e-9)
555 # Smaller holes lose a little, larger ones gain a little, both clamped.
556 assert model.cd_infinite(1.0e-3) < model.cd_inf < model.cd_infinite(4.0e-3)
557 assert model.cd_infinite(1.0e-6) >= model.cd_inf_min_geom
558 assert model.cd_infinite(1.0) <= model.cd_inf_max
559
560
561def _config_with(tmp_path, **overrides):
562 """The shipped ethalox config with a couple of keys changed.
563
564 Built from the real file rather than a hand-rolled dict: the importer
565 legitimately requires a full injector geometry, and a synthetic fixture
566 would be testing the fixture.
567 """
568 import yaml
569 from feedtwin.engine.importer import load_engine
570
571 raw = yaml.safe_load(ETHALOX.read_text())
572 for dotted, value in overrides.items():
573 section, key = dotted.split(".")
574 if value is None:
575 raw.get(section, {}).pop(key, None)
576 else:
577 raw.setdefault(section, {})[key] = value
578 path = tmp_path / "e.yaml"
579 path.write_text(yaml.safe_dump(raw))
580 return load_engine(path)
581
582
583@needs_ethalox
585 """Intent wins for all three, or the rule is not a rule.
586
587 `chamber_geometry.design_thrust` is the last optimiser run's achievement;
588 `design_requirements.target_thrust` is what was asked for. The importer
589 already preferred intent for O/F and Pc and read thrust bare, so the shipped
590 ethalox config imported as 7000 N against a delivered 7200 N -- silently.
591 """
592 engine = _config_with(tmp_path)
593 assert engine.design_thrust == pytest.approx(7200.0)
594 assert "target_thrust" in engine.provenance["design_thrust"]
595 assert "7000" in engine.provenance["design_thrust"], "name the value it overrode"
596
597
598@needs_ethalox
600 engine = _config_with(tmp_path, **{"design_requirements.target_thrust": None})
601 assert engine.design_thrust == pytest.approx(7000.0)
602 assert "chamber_geometry.design_thrust" in engine.provenance["design_thrust"]
603
604
605@needs_ethalox
607 """A warning that fires on healthy input teaches people to skim past it."""
608 engine = _config_with(tmp_path, **{"chamber_geometry.design_thrust": 7200.0})
609 assert "that is the last optimiser run" not in engine.provenance["design_thrust"]
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
TOML_PURE_GETTER bool contains(std::string_view key) const noexcept
Definition toml.hpp:8266
None test_every_shipped_config_imports()
None test_chamber_pressure_is_mdot_cstar_over_throat()
None test_cstar_peaks_below_the_stoichiometric_ratio()
None test_impinging_area_is_n_round_jets()
None test_the_geometry_cd_model_is_honoured_when_the_config_asks_for_it()
None test_pintle_uses_orifices_on_one_side_and_an_annulus_on_the_other()
dict[str, object] impinging_config()
None test_thrust_resolves_like_mixture_ratio_and_chamber_pressure(tmp_path)
( None) test_a_config_that_disagrees_with_itself_resolves_to_intent_and_records_it()
None test_chamber_fill_time_is_milliseconds()
None test_agreeing_thrust_values_are_not_flagged(tmp_path)
None test_thrust_falls_back_when_no_target_is_stated(tmp_path)
None test_chamber_at_altitude_takes_a_different_floor()
None test_cd_rises_with_flow()
None test_ethalox_injector_stiffness_is_in_the_design_band()
None test_injector_leg_follows_the_orifice_relation()
None test_leaving_the_table_is_reported_not_absorbed()
None test_an_unknown_propellant_is_refused_with_a_suggestion()
None test_cd_follows_the_engine_design_correlation()
None test_the_chamber_loop_absorbs_part_of_an_upstream_change()
None test_a_missing_key_names_itself_and_where_it_looked()
None test_an_unknown_injector_type_is_refused_not_guessed()
None test_injector_dp_is_symmetric_and_quadratic()
None test_injector_types_are_registered_not_hardcoded()
None test_ethalox_cstar_peaks_where_the_optimiser_put_the_mixture_ratio()
None test_a_shut_engine_sits_at_ambient_not_at_vacuum()
None test_cea_table_reproduces_real_combustion()
None test_injector_stiffness_at_the_design_point_is_sane()
None test_a_consistent_config_warns_about_nothing()
None test_chamber_temperature_moves_with_mixture_ratio()
None test_import_from_a_parsed_config()
_config_with(tmp_path, **overrides)
None test_the_current_vehicle_engine_imports()
None test_a_real_layer1_config_imports_with_nothing_retyped()
None test_cd_is_clamped_at_both_ends()
None test_coaxial_uses_core_ports_and_an_outer_annulus()
None test_propellant_aliases_cover_both_naming_conventions()