From b3923fb1c0bcac48804457f0ff10dcd921af4ff1 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Tue, 30 Jun 2026 20:33:12 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20smooth=20HOLD=E2=86=92HEAT=20power=20tra?= =?UTF-8?q?nsition=20(bumpless=20transfer=20+=20cascade=20timing)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes for the visible power dip at every ramp-to-ramp step boundary: 1. One-tick cascade delay: heatrate_soll was computed from pid_hold.get_y() before pid_hold.process() ran in that tick, so the first tick of a new ramp used the stale hold-phase output (≈0) as the rate setpoint, driving pid_heat to near zero. Fix: move heatrate_soll computation inside process_pid(), after pid_hold.process(). 2. Cold-start reset: pid_heat.reset() at HOLD→HEAT discarded the hold-phase integral, so pid_heat had to ramp up from zero on every new step. Fix: remove the reset (bumpless transfer) — the hold-phase integral is a warm starting point that carries the baseline hold power into the new ramp. Together these eliminate the ~1s dip to near-zero and replace the slow 20–30s integral ramp-up with an immediate start at roughly hold-level power, climbing monotonically to the target. Co-Authored-By: Claude Sonnet 4.6 --- components/pid/temp_controller.py | 5 +---- components/pid/temp_controller_base.py | 8 +++++++- components/pid/temp_controller_fsm.py | 4 +++- components/pid/temp_controller_smith.py | 5 +---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/components/pid/temp_controller.py b/components/pid/temp_controller.py index 6474359..9cde7f4 100644 --- a/components/pid/temp_controller.py +++ b/components/pid/temp_controller.py @@ -16,10 +16,7 @@ class TempController(TempControllerBase): # 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 - self.heatrate_soll = self.heatrate_soll_set * self.pid_hold.get_y() theta_err = self.theta_soll_set - self.theta_ist - heatrate_err = self.heatrate_soll - self.heatrate_ist - diff = self.theta_soll_set - self.theta_ist self.process_fsm(diff) - self.process_pid(theta_err, heatrate_err, hold_scale) + self.process_pid(theta_err, hold_scale) diff --git a/components/pid/temp_controller_base.py b/components/pid/temp_controller_base.py index 20bbf20..f58d68d 100644 --- a/components/pid/temp_controller_base.py +++ b/components/pid/temp_controller_base.py @@ -130,8 +130,14 @@ class TempControllerBase(TempControllerFsm, APid): def get_heatrate_soll_set(self): return self.heatrate_soll_set - def process_pid(self, theta_err, heatrate_err, hold_scale=1.0): + def process_pid(self, theta_err, hold_scale=1.0): self.pid_hold.process(theta_err, -self.theta_ist, hold_scale) + # Compute heatrate_soll HERE, after pid_hold ticks, so it uses + # this tick's output rather than last tick's — eliminates the + # one-tick cascade delay that caused p_set to dip near zero on + # the first tick of a new ramp (HOLD→HEAT transition). + self.heatrate_soll = self.heatrate_soll_set * self.pid_hold.get_y() + heatrate_err = self.heatrate_soll - self.heatrate_ist # Only the PID actually driving y is advanced - otherwise the # inactive one (e.g. pid_heat while COOL has pid_cool driving) diff --git a/components/pid/temp_controller_fsm.py b/components/pid/temp_controller_fsm.py index 08b386f..f641d43 100644 --- a/components/pid/temp_controller_fsm.py +++ b/components/pid/temp_controller_fsm.py @@ -96,7 +96,9 @@ class TempControllerFsm: elif self.state == States.HOLD: if diff >= self.thresholds['HoldHeat']: state_next = States.HEAT - self.pid_heat.reset() + # No pid_heat.reset() here — bumpless transfer: carry the + # hold-phase integral into the new ramp so power doesn't + # drop to near-zero and crawl back up from scratch. elif diff <= -self.thresholds['HoldCool']: state_next = States.COOL self.pid_cool.reset() diff --git a/components/pid/temp_controller_smith.py b/components/pid/temp_controller_smith.py index bae6e97..3ed2604 100755 --- a/components/pid/temp_controller_smith.py +++ b/components/pid/temp_controller_smith.py @@ -64,10 +64,7 @@ class TempController(TempControllerBase): # 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 - self.heatrate_soll = self.heatrate_soll_set * self.pid_hold.get_y() theta_err = self.theta_soll_set - self.theta_ist - heatrate_err = self.heatrate_soll - self.heatrate_ist - diff = self.theta_soll_set - self.theta_ist self.process_fsm(diff) - self.process_pid(theta_err, heatrate_err, hold_scale) + self.process_pid(theta_err, hold_scale)