fix: stamp PlantParams with each logger's own elapsed, not Sud.elapsed

log_plant_params() stored whatever elapsed it was handed - always
Sud.elapsed - regardless of which logger's own Samples.t timeline it
needed to match. SudLogTask._elapsed() is Sud.elapsed, so its PlantParams
lined up fine, but ServerLogTask._elapsed() is a separate tick counter
since server startup - using Sud.elapsed there put PlantParams.t on a
different scale than its own Samples.t entirely, causing
utils/replay_sim.py to apply grain/water-mass changes at the wrong point
when replaying a server-session log (spotted by comparing its output for
log_latest.json vs log_latest_sud.json from the same session).

Fixed by dropping the elapsed parameter - log_plant_params(params) now
stamps with self._elapsed(), so each logger always uses its own correct
timeline. Cascades into SudTask.set_on_plant_params()'s callback signature
and its wiring in server/brewpi.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDeoTKBDkXRhPfnyDEoBt
This commit is contained in:
2026-07-01 20:53:50 +02:00
co-authored by Claude Sonnet 5
parent a495e758fd
commit 0382bb01aa
3 changed files with 22 additions and 9 deletions
+4 -4
View File
@@ -148,7 +148,7 @@ if __name__ == '__main__':
# and every log_interval seconds in between.
server_log_task = ServerLogTask(tc, heater, heater_task, DT_TASK, args.logdir, config=config, dt=DT, log_interval=log_interval, sim_warp_factor=sim_warp_factor)
if startup_params is not None:
server_log_task.log_plant_params(0, startup_params)
server_log_task.log_plant_params(startup_params)
taskmgr.add(server_log_task)
# Per-run log - only records while a Sud is actually playing; written to
@@ -157,9 +157,9 @@ if __name__ == '__main__':
sud_log_task = SudLogTask(tc, heater, heater_task, sud, DT_TASK, args.logdir, config=config, dt=DT, log_interval=log_interval, sim_warp_factor=sim_warp_factor)
taskmgr.add(sud_log_task)
sud_task.set_on_plant_params(lambda elapsed, params: (
server_log_task.log_plant_params(elapsed, params),
sud_log_task.log_plant_params(elapsed, params)))
sud_task.set_on_plant_params(lambda params: (
server_log_task.log_plant_params(params),
sud_log_task.log_plant_params(params)))
sud_task.set_on_reanchor(sud_log_task.log_reanchor)
# Assign data flow
+10 -2
View File
@@ -32,8 +32,16 @@ class ServerLogTask(ATask):
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
self._last_write = time.monotonic()
def log_plant_params(self, elapsed, params):
self._param_events.append({'t': elapsed, 'params': params})
def log_plant_params(self, params):
# Stamped with this logger's *own* _elapsed() - not, say, Sud.elapsed
# passed in from the caller - so PlantParams' 't' always lines up
# with this same log's own Samples' 't'. SudLogTask's _elapsed() IS
# Sud.elapsed, but ServerLogTask's is its own independent counter
# since server startup, on a completely different timeline (the
# server usually starts well before any Sud does) - using the
# wrong one made utils/replay_sim.py apply plant-param changes at
# the wrong point in ServerLogTask's own Samples entirely.
self._param_events.append({'t': self._elapsed(), 'params': params})
async def on_process(self):
while True:
+8 -3
View File
@@ -117,8 +117,13 @@ class SudTask(ATask):
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."""
"""Register a callback invoked with (params) whenever
apply_plant_params() fires, so external observers (e.g.
SudLogTask/ServerLogTask) can record each change. Deliberately
doesn't pass elapsed - Sud.elapsed is meaningful for a SudLogTask
(its own Samples' 't' timeline) but not a ServerLogTask (a
separate timeline since server startup); each logger stamps its
own _elapsed() itself (see ServerLogTask.log_plant_params())."""
self._on_plant_params = callback
def set_on_reanchor(self, callback):
@@ -148,7 +153,7 @@ class SudTask(ATask):
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)
self._on_plant_params(params)
def apply_stirrer(self, phase):
stirrer_cfg = phase.get('stirrer', {})