fix: make sim_warp_factor real-hardware-safe and sud logs forecast-reproducible
sim_warp_factor was scaling DT_TASK unconditionally, including against real hardware, and was leaking into recorded sample timestamps via wall-clock time - both violate its intended meaning (a reciprocal scale on task wait time only). Clamp it to 1.0 whenever Heater.type isn't "sim", and track elapsed time via tick-counted accumulators (Sud.elapsed for SudLogTask, a matching one for ServerLogTask) instead of time.monotonic(). Sud/server logs now also carry HeaterPowers, the effective SimWarpFactor, the raw Doc, a correct Name, and ForecastAnchors (one entry per real _reanchor_forecast() trigger) - enough for utils/analyze_log.py to rebuild a SudForecastEstimator and replay the exact same splice-per-transition forecast correction offline that a live client sees, replacing the old (always-dead) forecast_*.json sidecar lookup. Two bugs found and fixed along the way: - ServerLogTask's periodic wall-clock checkpoint had no way to know a SudLogTask run had just ended, so it would immediately re-write the just-completed log with PlantParams/ForecastAnchors cleared back to empty. Gated the checkpoint on _should_sample(). - _reanchor_forecast() truncated the old forecast against real elapsed time, which can end up numerically less than the old forecast's own (very wrong) speculative timestamps once a WAIT_USER confirmation runs long - leaving a "ghost" segment instead of a clean cut. Truncate against forecast_step_starts[index] instead, which lives in the same coordinate space as the forecast being cut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFDeoTKBDkXRhPfnyDEoBt
This commit is contained in:
+40
-4
@@ -14,10 +14,12 @@ class SudLogTask(ServerLogTask):
|
||||
logs/log_{date_time}_{sud_name}.log. A Pause/resume doesn't start a new
|
||||
file - only a fresh Play (from IDLE/DONE) does."""
|
||||
|
||||
def __init__(self, tc: APid, heater: AHeater, heater_task, sud, interval, path='./logs', config=None, dt=None, log_interval=None):
|
||||
ServerLogTask.__init__(self, tc, heater, heater_task, interval, path, config, dt, log_interval)
|
||||
def __init__(self, tc: APid, heater: AHeater, heater_task, sud, interval, path='./logs', config=None, dt=None, log_interval=None, sim_warp_factor=None):
|
||||
ServerLogTask.__init__(self, tc, heater, heater_task, interval, path, config, dt, log_interval, sim_warp_factor)
|
||||
self.sud = sud
|
||||
self._active = False
|
||||
self._doc = None
|
||||
self._reanchors = []
|
||||
|
||||
def _should_sample(self):
|
||||
return self._active
|
||||
@@ -26,13 +28,46 @@ class SudLogTask(ServerLogTask):
|
||||
name_part = _safe_filename_part(self.sud.name or 'Sud')
|
||||
return 'log_{}_{}.log'.format(self._run_id, name_part)
|
||||
|
||||
def _log_name(self):
|
||||
return self.sud.name
|
||||
|
||||
def _elapsed(self):
|
||||
# The Sud's own tick-counted ground truth (see components/sud.py's
|
||||
# Sud.elapsed) rather than the base class's own tick counter -
|
||||
# already resets to 0 on every fresh Play (Sud.start()), so samples
|
||||
# line up with SudForecastEstimator's t=0 the same way Sud.elapsed
|
||||
# does everywhere else it's consumed (e.g. SudTask's forecast
|
||||
# re-anchoring).
|
||||
return self.sud.elapsed
|
||||
|
||||
def log_reanchor(self, elapsed, index, theta_ist):
|
||||
# One entry per real _reanchor_forecast() trigger (see SudTask.
|
||||
# set_on_reanchor()) - lets utils/analyze_log.py replay the exact
|
||||
# same splice-per-transition forecast correction a live client
|
||||
# would have seen, instead of just the single, uncorrected
|
||||
# cold-start estimate() run - see build_forecast() there.
|
||||
self._reanchors.append({'t': elapsed, 'index': index, 'theta_ist': theta_ist})
|
||||
|
||||
def _extra_log_data(self):
|
||||
# The raw sud.json doc this run was playing, exactly as loaded (see
|
||||
# Sud.save()) - lets utils/analyze_log.py re-simulate this run's own
|
||||
# SudForecastEstimator forecast for comparison against Samples,
|
||||
# without depending on the sude/*.json file it came from still
|
||||
# existing or being unchanged.
|
||||
data = {}
|
||||
if self._doc is not None:
|
||||
data['Doc'] = self._doc
|
||||
if self._reanchors:
|
||||
data['ForecastAnchors'] = self._reanchors
|
||||
return data
|
||||
|
||||
def start_run(self):
|
||||
if self._active:
|
||||
return
|
||||
self._samples = []
|
||||
self._t0 = time.monotonic()
|
||||
self._last_write = self._t0
|
||||
self._last_write = time.monotonic()
|
||||
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
|
||||
self._doc = self.sud.save()
|
||||
self._active = True
|
||||
|
||||
def stop_run(self):
|
||||
@@ -41,3 +76,4 @@ class SudLogTask(ServerLogTask):
|
||||
self._active = False
|
||||
self.write()
|
||||
self._param_events = []
|
||||
self._reanchors = []
|
||||
|
||||
Reference in New Issue
Block a user