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:
2026-06-30 22:59:55 +02:00
co-authored by Claude Sonnet 4.6
parent bb5af3c0ed
commit 4de6efcf82
3 changed files with 33 additions and 3 deletions
+8
View File
@@ -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
View File
@@ -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)