1"""Structured logging for robust DDP controller."""
3from __future__
import annotations
7from pathlib
import Path
8from typing
import Optional, Dict, Any, List
9from datetime
import datetime
12from .data_models
import Measurement
13from .actuation
import ActuationCommand
14from .engine_wrapper
import EngineEstimate
15from .constraints
import constraint_values
19 """Structured logger for controller runs."""
21 def __init__(self, log_file: Optional[str] =
None, format: str =
"json"):
27 log_file : str, optional
28 Path to log file. If None, logs to stdout.
30 Log format: "json" or "csv"
34 self.
logs: List[Dict[str, Any]] = []
38 self.
log_path.parent.mkdir(parents=
True, exist_ok=
True)
47 eng_est: Optional[EngineEstimate],
48 constraints: Dict[str, float],
49 u_proposed: np.ndarray,
50 u_filtered: np.ndarray,
51 actuation_cmd: ActuationCommand,
53 F_ref: Optional[float] =
None,
54 MR_ref: Optional[float] =
None,
67 eng_est : EngineEstimate, optional
68 Engine performance estimate
69 constraints : Dict[str, float]
71 u_proposed : np.ndarray
72 Proposed relaxed control
73 u_filtered : np.ndarray
74 Filtered (safe) relaxed control
75 actuation_cmd : ActuationCommand
79 F_ref : float, optional
81 MR_ref : float, optional
82 Reference mixture ratio
86 "timestamp": float(timestamp),
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),
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,
102 "constraints": {k: float(v)
for k, v
in constraints.items()},
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),
114 "w_bar": w_bar.tolist(),
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,
122 self.
logs.append(log_entry)
128 elif self.
format ==
"csv":
132 """Write JSON log entry."""
138 """Write CSV log 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"],
167 for key, value
in entry[
"constraints"].items():
168 flat_entry[f
"constraint_{key}"] = value
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]
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())
181 writer.writerow(flat_entry)
183 def save(self, filepath: Optional[str] =
None) ->
None:
185 Save all logs to file.
189 filepath : str, optional
190 Path to save file. If None, uses self.log_file.
193 path = Path(filepath)
197 raise ValueError(
"No filepath specified")
199 path.parent.mkdir(parents=
True, exist_ok=
True)
202 with open(path,
'w')
as f:
203 json.dump(self.
logs, f, indent=2)
204 elif self.
format ==
"csv":
206 with open(path,
'w', newline=
'')
as f:
208 first_entry = self.
logs[0]
210 writer = csv.DictWriter(f, fieldnames=flat_entry.keys())
212 for entry
in self.
logs:
216 """Flatten nested entry for CSV."""
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 "",
244 for key, value
in entry[
"constraints"].items():
245 flat[f
"constraint_{key}"] = value
248 w_bar = entry[
"robustness"][
"w_bar"]
249 for i
in range(len(w_bar)):
250 flat[f
"w_bar_{i}"] = w_bar[i]
255 def load(cls, filepath: str) -> List[Dict[str, Any]]:
266 logs : List[Dict[str, Any]]
269 path = Path(filepath)
271 if path.suffix ==
'.json':
272 with open(path,
'r')
as f:
274 elif path.suffix ==
'.csv':
276 with open(path,
'r')
as f:
277 reader = csv.DictReader(f)
281 "tick": int(row[
"tick"]),
282 "timestamp": float(row[
"timestamp"]),
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"]),
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,
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,
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,
313 "robustness": {
"w_bar": []},
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])
325 while f
"w_bar_{i}" in row:
326 w_bar.append(float(row[f
"w_bar_{i}"]))
328 entry[
"robustness"][
"w_bar"] = w_bar
333 raise ValueError(f
"Unsupported file format: {path.suffix}")
Dict[str, Any] _flatten_entry(self, Dict[str, Any] entry)
None _write_csv(self, Dict[str, Any] entry)
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)
None _write_json(self, Dict[str, Any] entry)
None save(self, Optional[str] filepath=None)
__init__(self, Optional[str] log_file=None, str format="json")
List[Dict[str, Any]] load(cls, str filepath)