pid: option A heat-rate pre-filter (beta IIR before differentiation)

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>
This commit is contained in:
2026-06-30 17:02:14 +02:00
co-authored by Claude Sonnet 4.6
parent 8ca4e4d803
commit 5be1d4e862
5 changed files with 31 additions and 33 deletions
+3 -16
View File
@@ -5,17 +5,8 @@ from components.pid.temp_controller_base import TempControllerBase, States
class TempController(TempControllerBase):
def __init__(self, dt):
TempControllerBase.__init__(self, dt)
self.dt = dt
# None until the first process() tick - rather than hardcoding a
# starting point (e.g. 20), which would make that very first
# tick's heatrate come out as a bogus spike whenever the real
# starting temperature isn't actually 20 (see
# components/sud_forecast.py's SudForecastEstimator, which builds
# a fresh TempController on every call - unlike the real one,
# which only ever goes through this exactly once, it has no
# earlier ticks to have already settled past that spike by).
self.last_theta_ist = None
self.heatrate_ist = 0
# last_theta_ist / theta_ist_filtered / beta / dt all live in the base -
# see TempControllerBase.__init__ and _compute_heatrate().
# Fast model: same plant model but with zero transport delay, used
# to predict the current temperature without the dead time.
@@ -68,11 +59,7 @@ class TempController(TempControllerBase):
# so the dead time drops out of the feedback path.
self.theta_ist = self.theta_ist_model + (self.theta_ist_plant - self.theta_ist_model_delay)
heatrate = 0 if self.last_theta_ist is None else (self.theta_ist - self.last_theta_ist)/self.dt*60
alpha = 0.1
self.heatrate_ist = (1-alpha) * self.heatrate_ist + alpha*heatrate
self.last_theta_ist = self.theta_ist
self._compute_heatrate(self.theta_ist)
# Compensate for max heat rate to reduce overshoot
hold_scale = 1.0/self.heatrate_soll_set if self.heatrate_soll_set > 0 else 1.0