STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_burn_report.py
Go to the documentation of this file.
1"""One way to total a burn (feedtwin.session.report), and one way to put a card on an engine.
2
3The cockpit finds its burns in a session's history; Layer X records one burn as columns. The
4totals have to be the same arithmetic on the same steps, or a disagreement between the tools
5could be bookkeeping. Checked against hand sums, and against the burn's own trace.
6"""
7
8from __future__ import annotations
9
10from types import SimpleNamespace
11
12import pytest
13from test_engine_card import constant_card
14from test_session_burn import CEA, ENGINE, STAND, TABLES, needs_engine, needs_stand
15
16from feedtwin.engine.chamber import GRAVITY
17from feedtwin.session.report import burns, summarise
18
19
21 t: float, thrust: float, ox: float, fuel: float, pc: float = 2.5e6
22) -> SimpleNamespace:
23 chamber = None
24 if ox + fuel > 0.0:
25 chamber = SimpleNamespace(
26 thrust=thrust,
27 pressure=pc,
28 mdot_total=ox + fuel,
29 mdot_oxidiser=ox,
30 mdot_fuel=fuel,
31 combustion=SimpleNamespace(cstar=1500.0, extrapolated=False),
32 )
33 return SimpleNamespace(t=t, chamber=chamber, balance=None)
34
35
37 # A start ramp, then full flow at a drifting O/F; uneven steps on purpose.
38 t = [0.1, 0.15, 0.25, 0.4]
39 thrust = [1000.0, 2000.0, 2100.0, 1900.0]
40 ox = [0.3, 0.6, 0.6, 0.66]
41 fuel = [0.2, 0.4, 0.4, 0.34]
42 r = summarise(t, thrust, [2e6] * 4, ox, fuel, before=0.0)
43 dt = [0.1, 0.05, 0.1, 0.15]
44 impulse = sum(f * d for f, d in zip(thrust, dt))
45 assert r.impulse_Ns == pytest.approx(impulse, rel=1e-12)
46 assert r.end_s - r.start_s == pytest.approx(0.4)
47 assert r.thrust_mean_N == pytest.approx(impulse / 0.4)
48 burned_ox = sum(m * d for m, d in zip(ox, dt))
49 burned_fuel = sum(m * d for m, d in zip(fuel, dt))
50 # Mass ratio of what burned -- not the mean of the step ratios, which differs here.
51 assert r.of_mean == pytest.approx(burned_ox / burned_fuel)
52 assert r.of_mean != pytest.approx(
53 sum(o / f for o, f in zip(ox, fuel)) / 4, rel=1e-3
54 )
55 assert r.isp_s == pytest.approx(impulse / (GRAVITY * (burned_ox + burned_fuel)))
56 # The ramp step is outside full flow: the minimum is the full-flow one.
57 assert r.thrust_min_N == 1900.0
58 assert r.of_min == pytest.approx(1.5) and r.of_max == pytest.approx(0.66 / 0.34)
59
60
62 history = [_sample(0.0, 0, 0, 0)]
63 history += [_sample(0.02 * k, 2000.0, 0.6, 0.4) for k in range(1, 6)]
64 history += [_sample(0.12, 0, 0, 0), _sample(0.14, 0, 0, 0)]
65 history += [_sample(0.14 + 0.02 * k, 1000.0, 0.3, 0.2) for k in range(1, 4)]
66 found = burns(history) # type: ignore[arg-type]
67 assert len(found) == 2
68 # The LOX runs out first: fuel alone through the chamber is not a burn, however
69 # much "thrust" a clamped combustion table reports for it.
70 history += [_sample(0.22, 2000.0, 0.0, 0.4), _sample(0.24, 2000.0, 0.0, 0.4)]
71 assert burns(history)[-1].impulse_Ns == pytest.approx(found[-1].impulse_Ns) # type: ignore[arg-type]
72 assert not burns(history)[-1].burning # type: ignore[arg-type]
73 first, second = found
74 assert first.start_s == 0.0 and first.end_s == pytest.approx(0.10)
75 assert first.impulse_Ns == pytest.approx(2000.0 * 0.10)
76 assert not first.burning
77 assert second.start_s == pytest.approx(0.14)
78 assert second.impulse_Ns == pytest.approx(1000.0 * 0.06)
79 assert second.burning, "the history ends mid-burn: running totals, said so"
80
81
82@needs_stand
83@needs_engine
85 """The cockpit's way (find the burn in the samples) against the burn's own trace,
86 step for step: same impulse, same propellant, to rounding."""
87 from test_session_burn import _model
88
89 from feedtwin.session import load_machine
90 from feedtwin.session.burn import BurnPlan, burn_setup, open_session, run_burn
91
92 session = open_session(
93 _model(engine=True), load_machine(tables=TABLES), setup=burn_setup()
94 )
95 seen: list = []
96 plan = BurnPlan(
97 tank_psi=550.0,
98 loads={"OXT": 6.0, "FUT": 4.0},
99 settle=False,
100 lead_in_s=0.1,
101 horizon_s=0.3,
102 )
103 trace = run_burn(
104 session, plan, also=lambda clock, sample, firing: seen.append(sample)
105 )
106 inlets = trace.probes.injector_inlet
107 (found,) = burns(seen, inlets)
108
109 firing = [i for i, f in enumerate(trace.firing) if f]
110 impulse = sum(
111 trace.chamber["thrust_N"][i] * (trace.t[i] - trace.t[i - 1]) for i in firing
112 )
113 ox = sum(
114 trace.chamber["mdot_oxidiser"][i] * (trace.t[i] - trace.t[i - 1])
115 for i in firing
116 )
117 assert found.steps == len(firing)
118 assert found.impulse_Ns == pytest.approx(impulse, rel=1e-9)
119 assert found.oxidiser_kg == pytest.approx(ox, rel=1e-9)
120 ch = trace.chamber
121 total = [ch["mdot_oxidiser"][i] + ch["mdot_fuel"][i] for i in firing]
122 median = sorted(total)[len(total) // 2]
123 full = [i for i, m in zip(firing, total) if m >= 0.8 * median]
124 for side, key in (
125 ("oxidiser", "stiffness_oxidiser_min"),
126 ("fuel", "stiffness_fuel_min"),
127 ):
128 pc, inlet = ch["pressure_Pa"], trace.pressure[inlets[side]]
129 lowest = min((inlet[i] - pc[i]) / pc[i] for i in full)
130 assert getattr(found, key) == pytest.approx(lowest, rel=1e-12), side
131 assert getattr(found, key) > 0.05, side
132
133
135 import yaml
136 from dataclasses import replace
137
138 from feedtwin.engine.card import CardChamber
139 from feedtwin.engine.importer import engine_from_config
140
141 if not ENGINE.exists():
142 pytest.skip("engine config absent")
143 design = engine_from_config(yaml.safe_load(ENGINE.read_text()), name="7000N")
144 card = replace(
145 constant_card(throat=design.throat_area),
146 provenance={"ambient_pa_sampled": 94070.0},
147 )
148 installed, chamber = card.install(design)
149 assert installed.oxidiser.card is card.oxidiser and installed.fuel.card is card.fuel
150 assert isinstance(chamber, CardChamber)
151 assert chamber.ambient_pressure == 94070.0
152 assert card.install(design, ambient_pressure=1e5)[1].ambient_pressure == 1e5
153 with pytest.raises(ValueError, match="throat"):
154 constant_card(throat=design.throat_area * 1.1).install(design)
155 assert CEA # the stand's combustion table is what the burn above used
#define min(a, b)
Definition Arduino.h:42
None test_a_burn_found_in_a_sessions_history_totals_what_its_trace_totals()
SimpleNamespace _sample(float t, float thrust, float ox, float fuel, float pc=2.5e6)
None test_a_history_splits_into_burns_at_every_quiet_step()
None test_totals_are_right_endpoint_sums_over_each_steps_own_length()
None test_install_is_attach_plus_the_cards_chamber_at_its_own_site()