Alongside the dated log files, ServerLogTask/SudLogTask now also write (and overwrite) a fixed-name copy on every write() - logs/log_latest.json and logs/log_latest_sud.json - so a dashboard/tail-style consumer can point at one unchanging path instead of tracking the current run's timestamped filename. Also switches SudLogTask's own dated/latest filenames from .log to .json, matching what they've always actually contained. This incidentally fixes utils/analyze_log.py's two-argument CLI form, which already assumed a .json extension for sud logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JFDeoTKBDkXRhPfnyDEoBt
87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
import time
|
|
from tasks.server_log import ServerLogTask
|
|
from components import APid, AHeater
|
|
|
|
|
|
def _safe_filename_part(text):
|
|
return "".join(c if c.isalnum() or c in "-_" else "_" for c in text) or "Sud"
|
|
|
|
|
|
class SudLogTask(ServerLogTask):
|
|
"""Records temp/power samples for a single Sud run - same sample format
|
|
as ServerLogTask, but only while a run is active: recording starts on
|
|
Play and is written out on Stop (or natural completion), to
|
|
logs/log_{date_time}_{sud_name}.json. 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, 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
|
|
|
|
def _filename(self):
|
|
name_part = _safe_filename_part(self.sud.name or 'Sud')
|
|
return 'log_{}_{}.json'.format(self._run_id, name_part)
|
|
|
|
def _latest_filename(self):
|
|
# Fixed regardless of which schedule is running, unlike _filename()
|
|
# above - a dashboard/tail-style consumer can point at one
|
|
# unchanging path across different Suds, not just different runs
|
|
# of the same one.
|
|
return 'log_latest_sud.json'
|
|
|
|
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._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):
|
|
if not self._active:
|
|
return
|
|
self._active = False
|
|
self.write()
|
|
self._param_events = []
|
|
self._reanchors = []
|