feat: include full config and plant param changes in Sud run log
Each log_*.json now carries a "Config" key (the full server config) and
a "PlantParams" list of {t, params} entries — one per step transition —
so offline analysis tools have the PID gains and plant model that were
active at every point of the run without having to cross-reference a
separate config file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -131,7 +131,9 @@ if __name__ == '__main__':
|
||||
# Records every Sud run's measured data and forecast to their own
|
||||
# JSON files under logs/, for later offline analysis - see
|
||||
# tasks/sud_log.py.
|
||||
taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK))
|
||||
sud_log_task = SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK, config=config)
|
||||
sud_task.set_on_plant_params(sud_log_task.log_plant_params)
|
||||
taskmgr.add(sud_log_task)
|
||||
|
||||
# Continuous server-session log - records from startup to shutdown
|
||||
# regardless of Sud state; written to logs/log_{date_time}.json on exit.
|
||||
|
||||
@@ -111,8 +111,14 @@ class SudTask(ATask):
|
||||
# when the current step has temperature=None (inherits from a prior step).
|
||||
self._paused_temp_soll = None
|
||||
self._on_end = None
|
||||
self._on_plant_params = None
|
||||
msg_handler.set_recv_handler(self.recv)
|
||||
|
||||
def set_on_plant_params(self, callback):
|
||||
"""Register a callback invoked whenever apply_plant_params() fires,
|
||||
so external observers (e.g. SudLogTask) can record each change."""
|
||||
self._on_plant_params = callback
|
||||
|
||||
def apply_plant_params(self, grain_mass, water_mass):
|
||||
"""Keeps the real plant's and the controller's internal model's
|
||||
plant params in sync with this Sud's own doc - L/Td come straight
|
||||
@@ -129,6 +135,8 @@ class SudTask(ATask):
|
||||
params = self.sud.derive_plant_params(grain_mass, water_mass)
|
||||
self.pot.set_plant_params(params)
|
||||
self.tc.set_model_plant_params(params)
|
||||
if self._on_plant_params:
|
||||
self._on_plant_params(self.sud.elapsed, params)
|
||||
|
||||
def apply_stirrer(self, phase):
|
||||
stirrer_cfg = phase.get('stirrer', {})
|
||||
|
||||
+22
-2
@@ -43,7 +43,7 @@ class SudLogTask(ATask):
|
||||
and the real controller keeps actively driving toward its target
|
||||
throughout WAIT_USER too)."""
|
||||
|
||||
def __init__(self, sud: Sud, tc: APid, heater: AHeater, heater_task, sud_task, interval, path='./logs'):
|
||||
def __init__(self, sud: Sud, tc: APid, heater: AHeater, heater_task, sud_task, interval, path='./logs', config=None):
|
||||
ATask.__init__(self, interval)
|
||||
self.sud = sud
|
||||
self.tc = tc
|
||||
@@ -51,8 +51,12 @@ class SudLogTask(ATask):
|
||||
self.heater_task = heater_task
|
||||
self.sud_task = sud_task
|
||||
self.path = path
|
||||
self.config = config
|
||||
# None while no run is in progress - see on_sud_state_changed().
|
||||
self._samples = None
|
||||
# Plant param changes during a run: [{t, params}, ...] - recorded
|
||||
# whenever apply_plant_params() fires (once per step transition).
|
||||
self._param_events = None
|
||||
# Identifies this run's pair of files (log_<run_id>_<name>.json
|
||||
# and forecast_<run_id>_<name>.json) - fixed at the moment the run
|
||||
# starts, not when it's written out, so the name reflects when
|
||||
@@ -67,21 +71,37 @@ class SudLogTask(ATask):
|
||||
was_running = self._samples is not None
|
||||
if self._is_running(state) and not was_running:
|
||||
self._samples = []
|
||||
self._param_events = []
|
||||
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
||||
elif not self._is_running(state) and was_running:
|
||||
self._write()
|
||||
self._samples = None
|
||||
self._param_events = None
|
||||
self._run_id = None
|
||||
|
||||
def log_plant_params(self, elapsed, params):
|
||||
"""Called by SudTask.apply_plant_params() on every step transition
|
||||
so the log captures which plant params were active at each point
|
||||
in the run."""
|
||||
if self._param_events is not None:
|
||||
self._param_events.append({'t': elapsed, 'params': params})
|
||||
|
||||
def _write(self):
|
||||
if not self._samples:
|
||||
return
|
||||
os.makedirs(self.path, exist_ok=True)
|
||||
name_part = _safe_filename_part(self.sud.name)
|
||||
|
||||
log_data = {'Name': self.sud.name}
|
||||
if self.config is not None:
|
||||
log_data['Config'] = self.config
|
||||
if self._param_events:
|
||||
log_data['PlantParams'] = self._param_events
|
||||
log_data['Samples'] = self._samples
|
||||
|
||||
samples_filename = "log_{}_{}.json".format(self._run_id, name_part)
|
||||
with open(os.path.join(self.path, samples_filename), "w") as f:
|
||||
json.dump({'Name': self.sud.name, 'Samples': self._samples}, f, indent='\t')
|
||||
json.dump(log_data, f, indent='\t')
|
||||
print("Sud log: wrote {} samples to {}".format(len(self._samples), samples_filename))
|
||||
|
||||
forecast_filename = "forecast_{}_{}.json".format(self._run_id, name_part)
|
||||
|
||||
Reference in New Issue
Block a user