STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layer2_pc_constraint.py
Go to the documentation of this file.
2import numpy as np
3import logging
4from unittest.mock import MagicMock, patch
5import sys
6import os
7import glob
8import time
9
10# Add project root to path
11sys.path.append(os.getcwd())
12
13from engine.optimizer.layers.layer2_pressure import run_layer2_pressure
14
15# Mock evaluation function
16def mock_evaluate_factory(drift_type="stable"):
17 def mock_evaluate(time_array, P_tank_O, P_tank_F, *args, **kwargs):
18 n = len(time_array)
19 Pc_initial = 3e6
20
21 # NOTE: This mock provides the result of the solver.
22 # But the PRUNING check happens BEFORE the solver, based on P_tank_O and P_tank_F.
23 # If P_tank drops too low, this function should NOT be called.
24
25 if drift_type == "stable":
26 decay = np.linspace(1.0, 0.76, n)
27 Pc_hist = Pc_initial * decay
28 elif drift_type == "extreme_drift":
29 decay = np.linspace(1.0, 0.40, n)
30 Pc_hist = Pc_initial * decay
31 else:
32 Pc_hist = np.full(n, Pc_initial)
33
34 return {
35 "Pc": Pc_hist,
36 "F": np.full(n, 1000.0),
37 "mdot_O": np.full(n, 1.0),
38 "mdot_F": np.full(n, 0.5),
39 "MR": np.full(n, 2.0),
40 "stability_score": np.full(n, 0.9),
41 "chugging_stability_margin": np.full(n, 1.0)
42 }
43 return mock_evaluate
44
45# Helper to mock decoding segments to force low pressure for pruning test
46def mock_decode_low_pressure(x, n_seg, init_lox, init_fuel, min_lox_p, min_fuel_p):
47 # This mocks `decode_segments_from_x` to return segments that produce VERY low pressure
48 # forcing the pruning check to trigger
49 from engine.optimizer.layers.layer2_pressure import generate_pressure_curve_from_segments
50
51 # Return valid structure but with low end pressures
52 # Target 50% of initial
53 seg_lox = [{"length_ratio": 1.0, "type": "blowdown", "start_pressure": init_lox, "end_pressure": init_lox * 0.5, "k": 0.3}]
54 seg_fuel = [{"length_ratio": 1.0, "type": "blowdown", "start_pressure": init_fuel, "end_pressure": init_fuel * 0.5, "k": 0.3}]
55 return seg_lox, seg_fuel
56
57
59 print("\n=== Testing Layer 2 Chamber Pressure Constraint ===")
60
61 config_mock = MagicMock()
62 config_mock.ablative_cooling = None
63 config_mock.graphite_insert = None
64
65 # Common mocks
66 with patch("engine.optimizer.layers.layer2_pressure.PintleEngineRunner") as MockRunner, \
67 patch("engine.optimizer.layers.layer2_pressure.logging.getLogger") as mock_get_logger:
68
69 mock_logger = MagicMock()
70 mock_get_logger.return_value = mock_logger
71
72 # Setup Runner instance
73 instance = MockRunner.return_value
74 # Mock single-point evaluate for Pc_initial check (3 MPa)
75 instance.evaluate.return_value = {"Pc": 3e6}
76
77 # --- Test Case 1: Stable Pressure (Safe) ---
78 print("\n--- Test Case 1: Stable Pressure (Safe) ---")
79 instance.evaluate_arrays_with_time.side_effect = mock_evaluate_factory("stable")
80
81 objs_case1 = []
82 def capture_case1(eval_num, obj, best):
83 objs_case1.append(obj)
84
85 try:
86 run_layer2_pressure(
87 optimized_config=config_mock,
88 initial_lox_pressure_pa=3e6,
89 initial_fuel_pressure_pa=2e6,
90 peak_thrust=1000.0,
91 target_apogee_m=1000.0,
92 rocket_dry_mass_kg=100.0,
93 max_lox_tank_capacity_kg=50.0,
94 max_fuel_tank_capacity_kg=50.0,
95 target_burn_time=10.0,
96 n_time_points=20,
97 max_iterations=1,
98 optimal_of_ratio=2.0,
99 objective_callback=capture_case1
100 )
101 except Exception:
102 pass
103
104 # Check logs for breakdown too
105 found_safe = False
106 for call in mock_logger.info.call_args_list:
107 if "Pc_stab=0.00" in str(call):
108 found_safe = True
109
110 # If objective is reasonable (around 100-200) and Pc_stab=0 in logs
111 if found_safe:
112 print("PASS: Verified Pc_stab=0.00 for stable pressure.")
113 else:
114 print(f"FAIL: Did not find zero Pc penalty (Objs: {objs_case1}).")
115
116 # --- Test Case 2: Extreme Drift in Output (Objective Penalty) ---
117 print("\n--- Test Case 2: Extreme Drift (>25%) ---")
118 mock_logger.reset_mock()
119 instance.evaluate_arrays_with_time.side_effect = mock_evaluate_factory("extreme_drift")
120
121 objs_case2 = []
122 def capture_case2(eval_num, obj, best):
123 objs_case2.append(obj)
124
125 try:
126 run_layer2_pressure(
127 optimized_config=config_mock,
128 initial_lox_pressure_pa=3e6,
129 initial_fuel_pressure_pa=2e6,
130 peak_thrust=1000.0,
131 target_apogee_m=1000.0,
132 rocket_dry_mass_kg=100.0,
133 max_lox_tank_capacity_kg=50.0,
134 max_fuel_tank_capacity_kg=50.0,
135 target_burn_time=10.0,
136 n_time_points=20,
137 max_iterations=1,
138 optimal_of_ratio=2.0,
139 objective_callback=capture_case2
140 )
141 except Exception:
142 pass
143
144 found_penalty = False
145 # Check if objective is elevated (> 200, typically 100 base + 100 penalty)
146 # 40% drift -> 60% of initial. Wait, mock returns 0.40.
147 # Deviation 60%. Threshold 25%. Excess 35%. Penalty mean * 1000.
148 # Mean depends on curve. Linear to 0.40.
149 # Should be penalty ~ 100s.
150
151 if any(obj > 200.0 for obj in objs_case2):
152 found_penalty = True
153
154 if found_penalty:
155 print(f"PASS: Verified high Pc_stab penalty (Max obj: {max(objs_case2)}).")
156 else:
157 # Check logs as fallback
158 for call in mock_logger.info.call_args_list:
159 msg = str(call)
160 if "Pc_stab=" in msg and "Pc_stab=0.00" not in msg:
161 found_penalty = True
162 if found_penalty:
163 print("PASS: Verified high Pc_stab penalty via logs.")
164 else:
165 print(f"FAIL: Did not find high Pc penalty (Objs: {objs_case2}).")
166
167 # --- Test Case 3: Severe Pressure Drop (Pre-Solver Pruning) ---
168 print("\n--- Test Case 3: Severe Pressure Drop (<75% of Initial) ---")
169 mock_logger.reset_mock()
170 # To test pruning, we must force the generated pressure curve (INPUT) to be low.
171 # We patch `generate_pressure_curve_from_segments` to return a low constant pressure.
172
173 def mock_generate_low_pressure(*args, **kwargs):
174 return np.full(20, 1.0e6) # 1 MPa, much lower than 0.75 * 3MPa (2.25 MPa)
175
176 objs_case3 = []
177 def capture_case3(eval_num, obj, best):
178 objs_case3.append(obj)
179
180 with patch("engine.optimizer.layers.layer2_pressure.generate_pressure_curve_from_segments", side_effect=mock_generate_low_pressure):
181 try:
182 run_layer2_pressure(
183 optimized_config=config_mock,
184 initial_lox_pressure_pa=3e6,
185 initial_fuel_pressure_pa=3e6,
186 peak_thrust=1000.0,
187 target_apogee_m=1000.0,
188 rocket_dry_mass_kg=100.0,
189 max_lox_tank_capacity_kg=50.0,
190 max_fuel_tank_capacity_kg=50.0,
191 target_burn_time=10.0,
192 n_time_points=20,
193 max_iterations=1,
194 optimal_of_ratio=2.0,
195 objective_callback=capture_case3
196 )
197 except Exception:
198 pass
199
200 found_pruning = False
201 # Now that pruning is removed and replaced by generation constraints,
202 # if we force low pressure via mocking, it should just be caught by the standard
203 # stability penalty in the objective function.
204 # So we look for high objective value, not a log message.
205
206 if any(obj > 200.0 for obj in objs_case3):
207 found_pruning = True
208
209 if found_pruning:
210 print(f"PASS: Verified high penalty for low pressure (generation constraints bypassed by mock).")
211 else:
212 print(f"FAIL: Did not find high penalty for low pressure (Objs: {objs_case3}).")
213
214if __name__ == "__main__":
mock_evaluate_factory(drift_type="stable")
mock_decode_low_pressure(x, n_seg, init_lox, init_fuel, min_lox_p, min_fuel_p)