Seed the forecast estimator's throwaway controller with start_theta

SudForecastEstimator.estimate() builds a fresh Pot/temperature
controller per call and never set its initial theta_soll, leaving it
at TempControllerBase's default of 0. For the upfront, full-schedule
Forecast this was harmless - the first real step always carries its
own 'temperature', overwriting it instantly. But the dynamic
RemainingForecast is recomputed from wherever the live run currently
is, and since ramping is no longer gated by a 'ramp' key, the
remaining schedule's first step often has no 'temperature' of its
own (e.g. resuming from a WAIT_USER pause into a hold-only step) - a
fresh tc has no persisted target to inherit it from like the real
run's tc does, so it span chasing 0 degrees, hit MAX_TICKS, and
produced a ~200,000-point result instead of the expected ~10,000.

That oversized payload (~6MB) is what was actually tripping
websockets' default 1MB max_size and disconnecting clients with
"1009 (message too big)" - not the GUI threading issue fixed
separately. Verified live: the same remaining-schedule case that
previously hit MAX_TICKS (200001 points) now resolves in 8348, and a
full driven run (load, start, auto-confirm both WAIT_USER steps,
completion) produces no oversized messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhiQe64F74uHV8jzuoSa5K
This commit is contained in:
2026-06-22 10:22:36 +02:00
co-authored by Claude Sonnet 4.6
parent 7c95a3bde3
commit 76d3a6047c
+11
View File
@@ -51,6 +51,17 @@ class SudForecastEstimator:
tc = PidFactory.create(self.pid_type, self.dt, self.tempctrl_params, self.plant_params, theta_amb=self.theta_amb)
tc.set_enabled(True)
tc.set_theta_ist(pot.get_temperature())
# Seed the target at start_theta - this tc is a fresh, throwaway
# instance (unlike the real run's persistent one), so without this
# its theta_soll_set defaults to 0 until a step pushes its own.
# Steps without their own 'temperature' (common now that ramping
# isn't gated by a 'ramp' key - see components/sud.py) rely on
# inheriting whatever target was already running, which for a
# schedule starting mid-brew (the dynamic remaining forecast) is
# start_theta, not 0 - without this, such a schedule's first step
# would have the simulated controller chase 0 degrees indefinitely,
# hitting MAX_TICKS and producing a needlessly huge result.
tc.set_theta_soll(start_theta)
def on_step_changed(step):
if step is None: