fix: embed dt and startup plant params in server log; open-loop replay_sim

server/brewpi.py: pass physics DT to ServerLogTask (not wall-clock DT_TASK)
so logs carry the correct integration timestep; log startup plant params as
the first PlantParams event so replay_sim can reconstruct them even when no
Sud is ever loaded (previously fell back to hardcoded defaults, causing ~3°C
temperature offset in replay).

tasks/server_log.py: accept and embed dt in log JSON; switch
log_plant_params() to wall-clock monotonic time (same base as sample t),
since the elapsed argument was simulated time and not comparable.

utils/replay_sim.py: switch to open-loop replay (plant driven by logged
power_eff, not sim controller output); use PidFactory so Smith vs Normal
controller type is honoured; set Td=0 in replay plant for Smith mode (logged
temp_ist is already Smith-corrected); feed Smith predictor's internal model
with real power; apply embedded PlantParams mid-run; prefer embedded dt over
sample-based fallback; clamp sim power ≥ 0; replace meaningless sim-power
trace in power subplot with power_set vs power_eff from the log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 00:31:46 +02:00
co-authored by Claude Sonnet 4.6
parent 4a1c969ad0
commit 57c23f293a
3 changed files with 72 additions and 32 deletions
+5 -2
View File
@@ -16,20 +16,21 @@ class ServerLogTask(ATask):
Sud state — useful for verifying controller and heater behaviour
outside of a scheduled brew."""
def __init__(self, tc: APid, heater: AHeater, heater_task, interval, path='./logs', config=None):
def __init__(self, tc: APid, heater: AHeater, heater_task, interval, path='./logs', config=None, dt=None):
ATask.__init__(self, interval)
self.tc = tc
self.heater = heater
self.heater_task = heater_task
self.path = path
self.config = config
self.dt = dt
self._samples = []
self._param_events = []
self._t0 = time.monotonic()
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
def log_plant_params(self, elapsed, params):
self._param_events.append({'t': elapsed, 'params': params})
self._param_events.append({'t': time.monotonic() - self._t0, 'params': params})
async def on_process(self):
while True:
@@ -53,6 +54,8 @@ class ServerLogTask(ATask):
filename = 'log_{}.json'.format(self._run_id)
path = os.path.join(self.path, filename)
log_data = {'Name': ''}
if self.dt is not None:
log_data['dt'] = self.dt
if self.config is not None:
log_data['Config'] = self.config
if self._param_events: