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}.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, 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_{}_{}.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._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 = []