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
Both loggers previously only wrote their JSON log on their end trigger
(shutdown / Stop-or-DONE), losing the whole session/run's data on a
hard crash or power loss. Add a log_interval (config.json, default
300s) that rewrites the same file whole every N seconds in between.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
Pot config (water_mass, volumen) sent in System message at startup.
app.js stores potConfig and uses it in updatePotVisualization/
updateStatusLine when sudEmpty, showing startup fill level and
"No schedule – N L water" without requiring a sud to be loaded first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pot hardware constants (mass, material, L, Td) moved from sude/*.json
to config.json's Pot section; Sud/SudForecastEstimator accept pot_config
as baseline, per-brew pot sections may still override. Controller key
removed; pid_type moved into TempCtrl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PlantFactory.create() no longer takes plant_name; sim vs real is derived
from Heater.type. StirrerFactory is wired from Stirrer.type. HeaterFactory
registry key lowercased to "hendi" to match config. Controller.plant_name
removed from both config templates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PlantFactory.create() now accepts sensor_config dict; extracts type and
kwargs, falls back to sigma=0.05/temp_offset=-0.15 defaults. brewpi.py
passes config.get('TempSensor', {}). Templates updated with full
TempSensor sections.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a configurable IIR pre-filter (beta, default 0.05, τ≈19 s at dt=1 s)
applied to theta_ist before the derivative, so stirrer-induced temperature
noise is attenuated before being amplified by differentiation rather than
after. The shared filter/derivative/post-filter logic is factored into
TempControllerBase._compute_heatrate(); both Normal and Smith subclasses
call it, replacing their duplicated inline rate blocks.
beta is read from TempCtrl.beta in the config (default 0.05 if absent) and
added to config-sim.json.tpl and config-real.json.tpl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>