41 """Exponential blowdown from each tank's configured initial pressure to 70% of it."""
44 P_O0 = float(config.lox_tank.initial_pressure_psi
or config.design_requirements.max_lox_tank_pressure_psi)
45 P_F0 = float(config.fuel_tank.initial_pressure_psi
or config.design_requirements.max_fuel_tank_pressure_psi)
46 times, lox_psi = generate_pressure_profile(
"exponential", P_O0, 0.7 * P_O0, duration_s, n_points, decay_constant=3.0)
47 _, fuel_psi = generate_pressure_profile(
"exponential", P_F0, 0.7 * P_F0, duration_s, n_points, decay_constant=3.0)
48 return times, lox_psi, fuel_psi
55 runner = PintleEngineRunner(config)
57 return compute_timeseries_results(runner, times, lox_psi, fuel_psi, run_copv=
False)
63 cfg = copy.deepcopy(config)
64 cfg.lox_tank.mass = lox_kg
65 cfg.fuel_tank.mass = fuel_kg
66 times = np.asarray(data[
"time"], dtype=float)
67 times = times - times[0]
70 "thrust": np.asarray(data[
"thrust_kN"], dtype=float) * 1000.0,
71 "mdot_O": np.asarray(data[
"mdot_O_kg_s"], dtype=float),
72 "mdot_F": np.asarray(data[
"mdot_F_kg_s"], dtype=float),
74 return run_flight_simulation(cfg, pressure_curves, float(times[-1]))
78 """The flight router caps a load the tank cannot hold, matches the shared resolver, does not
79 touch a load that fits, and honours an explicit design_requirements capacity over geometry."""
84 rho_F = float(config.fluids[
"fuel"].density)
85 base = config.model_dump()
88 small = copy.deepcopy(base)
89 small[
"fuel_tank"].update(mass=requested, tank_volume_m3=
None, rp1_h=0.3, rp1_radius=0.05)
90 adj, _, fuel_max, fill_factor = _apply_propellant_mass_caps(small, config)
91 expected_max, _, expected_ff, explicit = resolve_fuel_tank_limits(PintleEngineConfig(**small), rho_F)
93 assert fuel_max == pytest.approx(expected_max)
and fill_factor == pytest.approx(expected_ff)
94 assert fuel_max < requested,
"test premise: this tank must be too small for the load"
95 assert adj[
"fuel"][
"was_capped"]
is True
96 assert small[
"fuel_tank"][
"mass"] == pytest.approx(fuel_max),
"router must write the capped mass back"
97 assert adj[
"fuel"][
"original"] == pytest.approx(requested)
and adj[
"fuel"][
"capped"] == pytest.approx(fuel_max)
99 big = copy.deepcopy(base)
100 big[
"fuel_tank"].update(mass=requested, tank_volume_m3=
None, rp1_h=2.0, rp1_radius=0.15)
101 adj, _, fuel_max, _ = _apply_propellant_mass_caps(big, config)
102 assert fuel_max > requested
103 assert adj[
"fuel"][
"was_capped"]
is False
104 assert big[
"fuel_tank"][
"mass"] == pytest.approx(requested),
"a load that fits must not be touched"
106 capped = copy.deepcopy(big)
107 capped[
"design_requirements"][
"fuel_tank_capacity_kg"] = 3.0
108 adj, _, fuel_max, _ = _apply_propellant_mass_caps(capped, config)
109 assert fuel_max == pytest.approx(3.0)
and adj[
"fuel"][
"explicit_capacity_kg"] == pytest.approx(3.0)
110 assert adj[
"fuel"][
"was_capped"]
is True and capped[
"fuel_tank"][
"mass"] == pytest.approx(3.0)
116 lox_required = float(summary.get(
"lox_propellant_kg")
or 0)
117 fuel_required = float(summary.get(
"fuel_propellant_kg")
or 0)
118 assert lox_required > 0
and fuel_required > 0
120 low =
_run_flight(config, data,
max(0.5, lox_required * 0.45),
max(0.3, fuel_required * 0.45))
121 high =
_run_flight(config, data, lox_required * 1.05, fuel_required * 1.05)
123 assert low.get(
"success"), low.get(
"error")
124 assert high.get(
"success"), high.get(
"error")
125 assert low[
"truncation_info"].get(
"truncated")
is True
126 assert high[
"apogee"] > low[
"apogee"] + 5.0, (
127 f
"Expected higher propellant to raise apogee when truncated: low={low['apogee']:.1f}m high={high['apogee']:.1f}m"
134 lox_required = float(summary[
"lox_propellant_kg"])
135 fuel_required = float(summary[
"fuel_propellant_kg"])
137 optimal =
_run_flight(config, data, lox_required * 1.02, fuel_required * 1.02)
138 heavy =
_run_flight(config, data, lox_required * 1.35, fuel_required * 1.35)
140 assert optimal.get(
"success"), optimal.get(
"error")
141 assert heavy.get(
"success"), heavy.get(
"error")
142 assert optimal[
"truncation_info"].get(
"truncated")
is False
143 assert heavy[
"apogee"] <= optimal[
"apogee"] + 15.0, (
144 f
"Excess propellant should not increase apogee: optimal={optimal['apogee']:.1f}m heavy={heavy['apogee']:.1f}m"
152 short_imp = float(short_summary[
"total_impulse_kNs"])
153 long_imp = float(long_summary[
"total_impulse_kNs"])
154 assert long_imp > short_imp * 1.15,
"Longer burn should deliver materially more impulse"
158 float(short_summary[
"lox_propellant_kg"]) * 1.1, float(short_summary[
"fuel_propellant_kg"]) * 1.1,
162 float(long_summary[
"lox_propellant_kg"]) * 1.1, float(long_summary[
"fuel_propellant_kg"]) * 1.1,
165 assert short_flight.get(
"success"), short_flight.get(
"error")
166 assert long_flight.get(
"success"), long_flight.get(
"error")
167 assert long_flight[
"apogee"] > short_flight[
"apogee"] + 20.0, (
168 f
"Longer burn + enough propellant should raise apogee: short={short_flight['apogee']:.1f}m "
169 f
"long={long_flight['apogee']:.1f}m"