STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
logging.py
Go to the documentation of this file.
1"""Structured logging for robust DDP controller."""
2
3from __future__ import annotations
4
5import json
6import csv
7from pathlib import Path
8from typing import Optional, Dict, Any, List
9from datetime import datetime
10import numpy as np
11
12from .data_models import Measurement
13from .actuation import ActuationCommand
14from .engine_wrapper import EngineEstimate
15from .constraints import constraint_values
16
17
19 """Structured logger for controller runs."""
20
21 def __init__(self, log_file: Optional[str] = None, format: str = "json"):
22 """
23 Initialize logger.
24
25 Parameters:
26 -----------
27 log_file : str, optional
28 Path to log file. If None, logs to stdout.
29 format : str
30 Log format: "json" or "csv"
31 """
32 self.log_file = log_file
33 self.format = format
34 self.logs: List[Dict[str, Any]] = []
35
36 if log_file:
37 self.log_path = Path(log_file)
38 self.log_path.parent.mkdir(parents=True, exist_ok=True)
39 else:
40 self.log_path = None
41
43 self,
44 tick: int,
45 timestamp: float,
46 meas: Measurement,
47 eng_est: Optional[EngineEstimate],
48 constraints: Dict[str, float],
49 u_proposed: np.ndarray,
50 u_filtered: np.ndarray,
51 actuation_cmd: ActuationCommand,
52 w_bar: np.ndarray,
53 F_ref: Optional[float] = None,
54 MR_ref: Optional[float] = None,
55 ) -> None:
56 """
57 Log one control tick.
58
59 Parameters:
60 -----------
61 tick : int
62 Tick number
63 timestamp : float
64 Timestamp [s]
65 meas : Measurement
66 Sensor measurements
67 eng_est : EngineEstimate, optional
68 Engine performance estimate
69 constraints : Dict[str, float]
70 Constraint margins
71 u_proposed : np.ndarray
72 Proposed relaxed control
73 u_filtered : np.ndarray
74 Filtered (safe) relaxed control
75 actuation_cmd : ActuationCommand
76 Actuation command
77 w_bar : np.ndarray
78 Residual bounds
79 F_ref : float, optional
80 Reference thrust [N]
81 MR_ref : float, optional
82 Reference mixture ratio
83 """
84 log_entry = {
85 "tick": int(tick),
86 "timestamp": float(timestamp),
87 "pressures": {
88 "P_copv": float(meas.P_copv),
89 "P_reg": float(meas.P_reg),
90 "P_u_fuel": float(meas.P_u_fuel),
91 "P_u_ox": float(meas.P_u_ox),
92 "P_d_fuel": float(meas.P_d_fuel),
93 "P_d_ox": float(meas.P_d_ox),
94 },
95 "engine": {
96 "P_ch": float(eng_est.P_ch) if eng_est else None,
97 "F": float(eng_est.F) if eng_est else None,
98 "MR": float(eng_est.MR) if eng_est else None,
99 "mdot_F": float(eng_est.mdot_F) if eng_est else None,
100 "mdot_O": float(eng_est.mdot_O) if eng_est else None,
101 },
102 "constraints": {k: float(v) for k, v in constraints.items()},
103 "control": {
104 "u_proposed_F": float(u_proposed[0]),
105 "u_proposed_O": float(u_proposed[1]),
106 "u_filtered_F": float(u_filtered[0]),
107 "u_filtered_O": float(u_filtered[1]),
108 "duty_F": float(actuation_cmd.duty_F),
109 "duty_O": float(actuation_cmd.duty_O),
110 "u_F_onoff": bool(actuation_cmd.u_F_onoff),
111 "u_O_onoff": bool(actuation_cmd.u_O_onoff),
112 },
113 "robustness": {
114 "w_bar": w_bar.tolist(),
115 },
116 "reference": {
117 "F_ref": float(F_ref) if F_ref is not None else None,
118 "MR_ref": float(MR_ref) if MR_ref is not None else None,
119 },
120 }
121
122 self.logs.append(log_entry)
123
124 # Write immediately if file specified
125 if self.log_path:
126 if self.format == "json":
127 self._write_json(log_entry)
128 elif self.format == "csv":
129 self._write_csv(log_entry)
130
131 def _write_json(self, entry: Dict[str, Any]) -> None:
132 """Write JSON log entry."""
133 with open(self.log_path, 'a') as f:
134 json.dump(entry, f)
135 f.write('\n')
136
137 def _write_csv(self, entry: Dict[str, Any]) -> None:
138 """Write CSV log entry."""
139 # Flatten entry for CSV
140 flat_entry = {
141 "tick": entry["tick"],
142 "timestamp": entry["timestamp"],
143 "P_copv": entry["pressures"]["P_copv"],
144 "P_reg": entry["pressures"]["P_reg"],
145 "P_u_fuel": entry["pressures"]["P_u_fuel"],
146 "P_u_ox": entry["pressures"]["P_u_ox"],
147 "P_d_fuel": entry["pressures"]["P_d_fuel"],
148 "P_d_ox": entry["pressures"]["P_d_ox"],
149 "P_ch": entry["engine"]["P_ch"],
150 "F": entry["engine"]["F"],
151 "MR": entry["engine"]["MR"],
152 "mdot_F": entry["engine"]["mdot_F"],
153 "mdot_O": entry["engine"]["mdot_O"],
154 "u_proposed_F": entry["control"]["u_proposed_F"],
155 "u_proposed_O": entry["control"]["u_proposed_O"],
156 "u_filtered_F": entry["control"]["u_filtered_F"],
157 "u_filtered_O": entry["control"]["u_filtered_O"],
158 "duty_F": entry["control"]["duty_F"],
159 "duty_O": entry["control"]["duty_O"],
160 "u_F_onoff": entry["control"]["u_F_onoff"],
161 "u_O_onoff": entry["control"]["u_O_onoff"],
162 "F_ref": entry["reference"]["F_ref"],
163 "MR_ref": entry["reference"]["MR_ref"],
164 }
165
166 # Add constraint margins
167 for key, value in entry["constraints"].items():
168 flat_entry[f"constraint_{key}"] = value
169
170 # Add w_bar components
171 w_bar = entry["robustness"]["w_bar"]
172 for i in range(len(w_bar)):
173 flat_entry[f"w_bar_{i}"] = w_bar[i]
174
175 # Write header if file is new
176 file_exists = self.log_path.exists()
177 with open(self.log_path, 'a', newline='') as f:
178 writer = csv.DictWriter(f, fieldnames=flat_entry.keys())
179 if not file_exists:
180 writer.writeheader()
181 writer.writerow(flat_entry)
182
183 def save(self, filepath: Optional[str] = None) -> None:
184 """
185 Save all logs to file.
186
187 Parameters:
188 -----------
189 filepath : str, optional
190 Path to save file. If None, uses self.log_file.
191 """
192 if filepath:
193 path = Path(filepath)
194 elif self.log_path:
195 path = self.log_path
196 else:
197 raise ValueError("No filepath specified")
198
199 path.parent.mkdir(parents=True, exist_ok=True)
200
201 if self.format == "json":
202 with open(path, 'w') as f:
203 json.dump(self.logs, f, indent=2)
204 elif self.format == "csv":
205 if self.logs:
206 with open(path, 'w', newline='') as f:
207 # Flatten first entry to get fieldnames
208 first_entry = self.logs[0]
209 flat_entry = self._flatten_entry(first_entry)
210 writer = csv.DictWriter(f, fieldnames=flat_entry.keys())
211 writer.writeheader()
212 for entry in self.logs:
213 writer.writerow(self._flatten_entry(entry))
214
215 def _flatten_entry(self, entry: Dict[str, Any]) -> Dict[str, Any]:
216 """Flatten nested entry for CSV."""
217 flat = {
218 "tick": entry["tick"],
219 "timestamp": entry["timestamp"],
220 "P_copv": entry["pressures"]["P_copv"],
221 "P_reg": entry["pressures"]["P_reg"],
222 "P_u_fuel": entry["pressures"]["P_u_fuel"],
223 "P_u_ox": entry["pressures"]["P_u_ox"],
224 "P_d_fuel": entry["pressures"]["P_d_fuel"],
225 "P_d_ox": entry["pressures"]["P_d_ox"],
226 "P_ch": entry["engine"]["P_ch"] or "",
227 "F": entry["engine"]["F"] or "",
228 "MR": entry["engine"]["MR"] or "",
229 "mdot_F": entry["engine"]["mdot_F"] or "",
230 "mdot_O": entry["engine"]["mdot_O"] or "",
231 "u_proposed_F": entry["control"]["u_proposed_F"],
232 "u_proposed_O": entry["control"]["u_proposed_O"],
233 "u_filtered_F": entry["control"]["u_filtered_F"],
234 "u_filtered_O": entry["control"]["u_filtered_O"],
235 "duty_F": entry["control"]["duty_F"],
236 "duty_O": entry["control"]["duty_O"],
237 "u_F_onoff": entry["control"]["u_F_onoff"],
238 "u_O_onoff": entry["control"]["u_O_onoff"],
239 "F_ref": entry["reference"]["F_ref"] or "",
240 "MR_ref": entry["reference"]["MR_ref"] or "",
241 }
242
243 # Add constraint margins
244 for key, value in entry["constraints"].items():
245 flat[f"constraint_{key}"] = value
246
247 # Add w_bar components
248 w_bar = entry["robustness"]["w_bar"]
249 for i in range(len(w_bar)):
250 flat[f"w_bar_{i}"] = w_bar[i]
251
252 return flat
253
254 @classmethod
255 def load(cls, filepath: str) -> List[Dict[str, Any]]:
256 """
257 Load logs from file.
258
259 Parameters:
260 -----------
261 filepath : str
262 Path to log file
263
264 Returns:
265 --------
266 logs : List[Dict[str, Any]]
267 List of log entries
268 """
269 path = Path(filepath)
270
271 if path.suffix == '.json':
272 with open(path, 'r') as f:
273 return json.load(f)
274 elif path.suffix == '.csv':
275 logs = []
276 with open(path, 'r') as f:
277 reader = csv.DictReader(f)
278 for row in reader:
279 # Reconstruct nested structure
280 entry = {
281 "tick": int(row["tick"]),
282 "timestamp": float(row["timestamp"]),
283 "pressures": {
284 "P_copv": float(row["P_copv"]),
285 "P_reg": float(row["P_reg"]),
286 "P_u_fuel": float(row["P_u_fuel"]),
287 "P_u_ox": float(row["P_u_ox"]),
288 "P_d_fuel": float(row["P_d_fuel"]),
289 "P_d_ox": float(row["P_d_ox"]),
290 },
291 "engine": {
292 "P_ch": float(row["P_ch"]) if row["P_ch"] else None,
293 "F": float(row["F"]) if row["F"] else None,
294 "MR": float(row["MR"]) if row["MR"] else None,
295 "mdot_F": float(row["mdot_F"]) if row["mdot_F"] else None,
296 "mdot_O": float(row["mdot_O"]) if row["mdot_O"] else None,
297 },
298 "control": {
299 "u_proposed_F": float(row["u_proposed_F"]),
300 "u_proposed_O": float(row["u_proposed_O"]),
301 "u_filtered_F": float(row["u_filtered_F"]),
302 "u_filtered_O": float(row["u_filtered_O"]),
303 "duty_F": float(row["duty_F"]),
304 "duty_O": float(row["duty_O"]),
305 "u_F_onoff": bool(int(row["u_F_onoff"])) if row["u_F_onoff"] else False,
306 "u_O_onoff": bool(int(row["u_O_onoff"])) if row["u_O_onoff"] else False,
307 },
308 "reference": {
309 "F_ref": float(row["F_ref"]) if row["F_ref"] else None,
310 "MR_ref": float(row["MR_ref"]) if row["MR_ref"] else None,
311 },
312 "constraints": {},
313 "robustness": {"w_bar": []},
314 }
315
316 # Extract constraints
317 for key in row.keys():
318 if key.startswith("constraint_"):
319 constraint_name = key[len("constraint_"):]
320 entry["constraints"][constraint_name] = float(row[key])
321
322 # Extract w_bar
323 w_bar = []
324 i = 0
325 while f"w_bar_{i}" in row:
326 w_bar.append(float(row[f"w_bar_{i}"]))
327 i += 1
328 entry["robustness"]["w_bar"] = w_bar
329
330 logs.append(entry)
331 return logs
332 else:
333 raise ValueError(f"Unsupported file format: {path.suffix}")
334
Dict[str, Any] _flatten_entry(self, Dict[str, Any] entry)
Definition logging.py:215
None _write_csv(self, Dict[str, Any] entry)
Definition logging.py:137
None log_tick(self, int tick, float timestamp, Measurement meas, Optional[EngineEstimate] eng_est, Dict[str, float] constraints, np.ndarray u_proposed, np.ndarray u_filtered, ActuationCommand actuation_cmd, np.ndarray w_bar, Optional[float] F_ref=None, Optional[float] MR_ref=None)
Definition logging.py:55
None _write_json(self, Dict[str, Any] entry)
Definition logging.py:131
None save(self, Optional[str] filepath=None)
Definition logging.py:183
__init__(self, Optional[str] log_file=None, str format="json")
Definition logging.py:21
List[Dict[str, Any]] load(cls, str filepath)
Definition logging.py:255