AttributeChange (the common base of every ATask and component ABC) now
sets self.log = logging.getLogger(type(self).__name__), so components
no longer need to hand-type their own name into each message. Wired
logging.basicConfig() in server/brewpi.py with a bare "%(name)s:
%(message)s" formatter - Tee (see prior commit) still supplies the
"<date>T<time>:" prefix, so lines read "<date>T<time>:<component>:
<message>" without double-stamping, and third-party loggers
(websockets, asyncio) now get the same formatting for free.
Converted the print() call sites that were standing in for this in
tasks/ and components/ (leaving __main__ demo blocks and explicit
debug-dump helpers alone).
Renames pid_hold/pid_heat/pid_cool to pid_outer/pid_inner/pid_inner_cool
to match what actually runs when, and splits inner-loop config into
Inner.Heat/Inner.Hold/Inner.Cool so the same PID instance gets a tight
yi_max ceiling only while HOLD drives it, without capping legitimate
1.5 K/min ramps. Fixes the overshoot from docs/overshoot_hold_windup.md
where a cold-water disturbance during HOLD wound up pid_heat's integral
term with no anti-windup engagement, taking ~35s+ to unwind naturally.
Breaking config change: Hold/Heat/Cool -> Outer/Inner.{Heat,Hold,Cool}
in config.json, both .tpl templates, the pid/sud demo scripts, and
replay_sim.py's CLI flags. Adds tests/components/pid/ (stdlib unittest)
covering the Pid clamp/recovery behavior and closed-loop disturbance,
ramp, and HOLD<->HEAT transition cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGQhVQ2Y3yXAQTXhrxVd5u
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
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>
ServerLogTask now embeds the full config in its JSON output, matching
SudLogTask — so replay_sim and analyze_log work on server logs without
needing a separate config file.
replay_sim.py switches from two positional args (date_time, sud_name) to
a single log file path, with config and plant params resolved from the
log's embedded "Config"/"PlantParams" sections when present and falling
back to config.json / hardcoded defaults for older logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The two-argument form (date_time sud_name) is unchanged. When called
with a single argument it is treated as a direct path to any log_*.json
(e.g. a server-session log without a Sud name); the forecast panel is
shown only if a matching forecast_*.json exists alongside it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the open-loop (feed real temp_ist) approach with a proper
closed-loop simulation: a Pot plant model is driven by the controller's
power output each tick, generating sim_temp_ist that diverges from the
real log's real_temp_ist whenever the PID params produce different
behaviour. Temperature panel now shows real_temp_ist vs sim_temp_ist
vs temp_soll so the effect of candidate gains is immediately visible.
Plant params (M, C, L, Td) default to 20 kg water / L=0.2 / Td=30 s
and can be overridden via --plant-* flags. Ambient defaults from config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EY449HByk6UpKhDN2HTnfK
Feeds real temp_ist/temp_soll readings from a run log back into a fresh
TempController (Normal mode) tick by tick, then plots a 3-panel comparison
of heat rate and normalized power against the original logged values. Any
PID gain can be overridden via --<section>-<gain> flags so candidate params
can be evaluated against a past run without touching hardware.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EY449HByk6UpKhDN2HTnfK
Renders the same two plots the PyQt GUI shows (realtime 3-panel strip
chart and forecast-vs-actual comparison) from a log/forecast JSON pair,
specified by date_time and sud_name on the command line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NSo8R6GjoBQdStB3j67zSs