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
|
# Records every Sud run's measured data and forecast to their own
|
||||||
# JSON files under logs/, for later offline analysis - see
|
# JSON files under logs/, for later offline analysis - see
|
||||||
# tasks/sud_log.py.
|
# 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
|
# Continuous server-session log - records from startup to shutdown
|
||||||
# regardless of Sud state; written to logs/log_{date_time}.json on exit.
|
# 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).
|
# when the current step has temperature=None (inherits from a prior step).
|
||||||
self._paused_temp_soll = None
|
self._paused_temp_soll = None
|
||||||
self._on_end = None
|
self._on_end = None
|
||||||
|
self._on_plant_params = None
|
||||||
msg_handler.set_recv_handler(self.recv)
|
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):
|
def apply_plant_params(self, grain_mass, water_mass):
|
||||||
"""Keeps the real plant's and the controller's internal model's
|
"""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
|
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)
|
params = self.sud.derive_plant_params(grain_mass, water_mass)
|
||||||
self.pot.set_plant_params(params)
|
self.pot.set_plant_params(params)
|
||||||
self.tc.set_model_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):
|
def apply_stirrer(self, phase):
|
||||||
stirrer_cfg = phase.get('stirrer', {})
|
stirrer_cfg = phase.get('stirrer', {})
|
||||||
|
|||||||
+22
-2
@@ -43,7 +43,7 @@ class SudLogTask(ATask):
|
|||||||
and the real controller keeps actively driving toward its target
|
and the real controller keeps actively driving toward its target
|
||||||
throughout WAIT_USER too)."""
|
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)
|
ATask.__init__(self, interval)
|
||||||
self.sud = sud
|
self.sud = sud
|
||||||
self.tc = tc
|
self.tc = tc
|
||||||
@@ -51,8 +51,12 @@ class SudLogTask(ATask):
|
|||||||
self.heater_task = heater_task
|
self.heater_task = heater_task
|
||||||
self.sud_task = sud_task
|
self.sud_task = sud_task
|
||||||
self.path = path
|
self.path = path
|
||||||
|
self.config = config
|
||||||
# None while no run is in progress - see on_sud_state_changed().
|
# None while no run is in progress - see on_sud_state_changed().
|
||||||
self._samples = None
|
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
|
# Identifies this run's pair of files (log_<run_id>_<name>.json
|
||||||
# and forecast_<run_id>_<name>.json) - fixed at the moment the run
|
# and forecast_<run_id>_<name>.json) - fixed at the moment the run
|
||||||
# starts, not when it's written out, so the name reflects when
|
# 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
|
was_running = self._samples is not None
|
||||||
if self._is_running(state) and not was_running:
|
if self._is_running(state) and not was_running:
|
||||||
self._samples = []
|
self._samples = []
|
||||||
|
self._param_events = []
|
||||||
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
||||||
elif not self._is_running(state) and was_running:
|
elif not self._is_running(state) and was_running:
|
||||||
self._write()
|
self._write()
|
||||||
self._samples = None
|
self._samples = None
|
||||||
|
self._param_events = None
|
||||||
self._run_id = 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):
|
def _write(self):
|
||||||
if not self._samples:
|
if not self._samples:
|
||||||
return
|
return
|
||||||
os.makedirs(self.path, exist_ok=True)
|
os.makedirs(self.path, exist_ok=True)
|
||||||
name_part = _safe_filename_part(self.sud.name)
|
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)
|
samples_filename = "log_{}_{}.json".format(self._run_id, name_part)
|
||||||
with open(os.path.join(self.path, samples_filename), "w") as f:
|
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))
|
print("Sud log: wrote {} samples to {}".format(len(self._samples), samples_filename))
|
||||||
|
|
||||||
forecast_filename = "forecast_{}_{}.json".format(self._run_id, name_part)
|
forecast_filename = "forecast_{}_{}.json".format(self._run_id, name_part)
|
||||||
|
|||||||
Reference in New Issue
Block a user