STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
analyze_controller_run.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Analyze controller run logs and generate plots.
3
4Loads controller logs and plots:
5- F_ref vs F_hat
6- MR
7- P_copv, P_reg, P_u_i, P_d_i
8- duty / valve states
9- constraint margins
10"""
11
12import argparse
13import sys
14from pathlib import Path
15import numpy as np
16import matplotlib.pyplot as plt
17from matplotlib.gridspec import GridSpec
18
19# Add parent directory to path for imports
20sys.path.insert(0, str(Path(__file__).parent.parent))
21
22from engine.control.robust_ddp.logging import ControllerLogger
23
24
25def load_logs(filepath: str):
26 """Load logs from file."""
27 return ControllerLogger.load(filepath)
28
29
30def extract_data(logs):
31 """Extract data arrays from logs."""
32 n = len(logs)
33
34 data = {
35 "tick": np.array([log["tick"] for log in logs]),
36 "timestamp": np.array([log["timestamp"] for log in logs]),
37 "P_copv": np.array([log["pressures"]["P_copv"] for log in logs]),
38 "P_reg": np.array([log["pressures"]["P_reg"] for log in logs]),
39 "P_u_fuel": np.array([log["pressures"]["P_u_fuel"] for log in logs]),
40 "P_u_ox": np.array([log["pressures"]["P_u_ox"] for log in logs]),
41 "P_d_fuel": np.array([log["pressures"]["P_d_fuel"] for log in logs]),
42 "P_d_ox": np.array([log["pressures"]["P_d_ox"] for log in logs]),
43 "P_ch": np.array([log["engine"]["P_ch"] if log["engine"]["P_ch"] is not None else np.nan for log in logs]),
44 "F": np.array([log["engine"]["F"] if log["engine"]["F"] is not None else np.nan for log in logs]),
45 "MR": np.array([log["engine"]["MR"] if log["engine"]["MR"] is not None else np.nan for log in logs]),
46 "duty_F": np.array([log["control"]["duty_F"] for log in logs]),
47 "duty_O": np.array([log["control"]["duty_O"] for log in logs]),
48 "u_F_onoff": np.array([log["control"]["u_F_onoff"] for log in logs]),
49 "u_O_onoff": np.array([log["control"]["u_O_onoff"] for log in logs]),
50 "F_ref": np.array([log["reference"]["F_ref"] if log["reference"]["F_ref"] is not None else np.nan for log in logs]),
51 "MR_ref": np.array([log["reference"]["MR_ref"] if log["reference"]["MR_ref"] is not None else np.nan for log in logs]),
52 }
53
54 # Extract constraint margins
55 constraint_keys = set()
56 for log in logs:
57 constraint_keys.update(log["constraints"].keys())
58
59 for key in constraint_keys:
60 data[f"constraint_{key}"] = np.array([
61 log["constraints"].get(key, np.nan) for log in logs
62 ])
63
64 # Extract w_bar (use first component as example)
65 if logs and "robustness" in logs[0] and "w_bar" in logs[0]["robustness"]:
66 w_bar = logs[0]["robustness"]["w_bar"]
67 if len(w_bar) > 0:
68 data["w_bar_0"] = np.array([
69 log["robustness"]["w_bar"][0] if len(log["robustness"]["w_bar"]) > 0 else np.nan
70 for log in logs
71 ])
72
73 return data
74
75
76def plot_controller_run(data, output_file: str = None):
77 """Plot controller run data."""
78 fig = plt.figure(figsize=(16, 12))
79 gs = GridSpec(4, 2, figure=fig, hspace=0.3, wspace=0.3)
80
81 time = data["timestamp"]
82
83 # Plot 1: F_ref vs F_hat
84 ax1 = fig.add_subplot(gs[0, 0])
85 ax1.plot(time, data["F_ref"] / 1000, 'b--', label='F_ref', linewidth=2)
86 ax1.plot(time, data["F"] / 1000, 'r-', label='F_hat', linewidth=1.5)
87 ax1.set_xlabel('Time [s]')
88 ax1.set_ylabel('Thrust [kN]')
89 ax1.set_title('Thrust Tracking')
90 ax1.legend()
91 ax1.grid(True, alpha=0.3)
92
93 # Plot 2: MR
94 ax2 = fig.add_subplot(gs[0, 1])
95 ax2.plot(time, data["MR_ref"], 'b--', label='MR_ref', linewidth=2)
96 ax2.plot(time, data["MR"], 'r-', label='MR_hat', linewidth=1.5)
97 ax2.set_xlabel('Time [s]')
98 ax2.set_ylabel('Mixture Ratio (O/F)')
99 ax2.set_title('Mixture Ratio')
100 ax2.legend()
101 ax2.grid(True, alpha=0.3)
102
103 # Plot 3: Pressures (COPV, Regulator)
104 ax3 = fig.add_subplot(gs[1, 0])
105 ax3.plot(time, data["P_copv"] / 1e6, 'b-', label='P_copv', linewidth=1.5)
106 ax3.plot(time, data["P_reg"] / 1e6, 'g-', label='P_reg', linewidth=1.5)
107 ax3.set_xlabel('Time [s]')
108 ax3.set_ylabel('Pressure [MPa]')
109 ax3.set_title('COPV and Regulator Pressures')
110 ax3.legend()
111 ax3.grid(True, alpha=0.3)
112
113 # Plot 4: Ullage Pressures
114 ax4 = fig.add_subplot(gs[1, 1])
115 ax4.plot(time, data["P_u_fuel"] / 1e6, 'b-', label='P_u_fuel', linewidth=1.5)
116 ax4.plot(time, data["P_u_ox"] / 1e6, 'r-', label='P_u_ox', linewidth=1.5)
117 ax4.set_xlabel('Time [s]')
118 ax4.set_ylabel('Pressure [MPa]')
119 ax4.set_title('Ullage Pressures')
120 ax4.legend()
121 ax4.grid(True, alpha=0.3)
122
123 # Plot 5: Feed Pressures
124 ax5 = fig.add_subplot(gs[2, 0])
125 ax5.plot(time, data["P_d_fuel"] / 1e6, 'b-', label='P_d_fuel', linewidth=1.5)
126 ax5.plot(time, data["P_d_ox"] / 1e6, 'r-', label='P_d_ox', linewidth=1.5)
127 ax5.plot(time, data["P_ch"] / 1e6, 'g-', label='P_ch', linewidth=1.5)
128 ax5.set_xlabel('Time [s]')
129 ax5.set_ylabel('Pressure [MPa]')
130 ax5.set_title('Feed and Chamber Pressures')
131 ax5.legend()
132 ax5.grid(True, alpha=0.3)
133
134 # Plot 6: Duty Cycles
135 ax6 = fig.add_subplot(gs[2, 1])
136 ax6.plot(time, data["duty_F"], 'b-', label='duty_F', linewidth=1.5)
137 ax6.plot(time, data["duty_O"], 'r-', label='duty_O', linewidth=1.5)
138 ax6.set_xlabel('Time [s]')
139 ax6.set_ylabel('Duty Cycle [0-1]')
140 ax6.set_title('Solenoid Duty Cycles')
141 ax6.set_ylim([-0.1, 1.1])
142 ax6.legend()
143 ax6.grid(True, alpha=0.3)
144
145 # Plot 7: Constraint Margins
146 ax7 = fig.add_subplot(gs[3, :])
147 constraint_keys = [k for k in data.keys() if k.startswith("constraint_")]
148 if constraint_keys:
149 for key in constraint_keys:
150 constraint_name = key[len("constraint_"):]
151 ax7.plot(time, data[key], label=constraint_name, linewidth=1.5, alpha=0.7)
152 ax7.axhline(y=0, color='k', linestyle='--', linewidth=1, alpha=0.5)
153 ax7.set_xlabel('Time [s]')
154 ax7.set_ylabel('Constraint Margin (positive = violation)')
155 ax7.set_title('Constraint Margins')
156 ax7.legend(ncol=3, fontsize=8)
157 ax7.grid(True, alpha=0.3)
158 else:
159 ax7.text(0.5, 0.5, 'No constraint data', ha='center', va='center', transform=ax7.transAxes)
160 ax7.set_title('Constraint Margins')
161
162 plt.suptitle('Controller Run Analysis', fontsize=16, y=0.995)
163
164 if output_file:
165 plt.savefig(output_file, dpi=150, bbox_inches='tight')
166 print(f"Plot saved to {output_file}")
167 else:
168 plt.show()
169
170
171def main():
172 """Main function."""
173 parser = argparse.ArgumentParser(description='Analyze controller run logs')
174 parser.add_argument('log_file', type=str, help='Path to log file (JSON or CSV)')
175 parser.add_argument('-o', '--output', type=str, default=None, help='Output plot file (default: show)')
176
177 args = parser.parse_args()
178
179 # Load logs
180 print(f"Loading logs from {args.log_file}...")
181 logs = load_logs(args.log_file)
182 print(f"Loaded {len(logs)} log entries")
183
184 # Extract data
185 data = extract_data(logs)
186
187 # Plot
188 plot_controller_run(data, args.output)
189
190
191if __name__ == "__main__":
192 main()
193
194
195
plot_controller_run(data, str output_file=None)