diff --git a/docs/temp_control_calibration.md b/docs/temp_control_calibration.md index 6c7ef32..c25d070 100644 --- a/docs/temp_control_calibration.md +++ b/docs/temp_control_calibration.md @@ -117,3 +117,88 @@ A replay panel added to `utils/analyze_log.py` would show: - `temp_ist` (blue) vs `sim_temp` (red dashed) — model accuracy at a glance - Residual `temp_ist − sim_temp` in a separate subplot — reveals systematic parameter errors vs noise floor + + +## 5. Heat rate noise from stirring + +Stirring creates low-frequency temperature fluctuations at the sensor. The heat +rate is computed in both controllers (`temp_controller_smith.py:71-74`, +identical in `temp_controller.py`) as: + +```python +heatrate = (self.theta_ist - self.last_theta_ist) / self.dt * 60 +alpha = 0.1 +self.heatrate_ist = (1-alpha) * self.heatrate_ist + alpha * heatrate +``` + +Two problems compound: + +1. **Differentiation amplifies noise.** A temperature fluctuation δT at + frequency f becomes a rate fluctuation of `2π·f·δT·60` K/min. Low-frequency + stirrer noise (e.g. 0.05 Hz, δT = 0.05 °C) → ≈ 0.9 K/min rate noise, the + same order as a typical `rate_soll`. + +2. **The IIR filter comes after differentiation.** With α=0.1 and dt=1 s the + time constant is τ = 9 s (cutoff ≈ 0.018 Hz). Filtering *after* + differentiating is the least effective arrangement: noise has already been + amplified before the filter sees it. + +The noisy `heatrate_err` feeds into `pid_heat.process()` as the proportional +error. With `kp=0.08`, ±1 K/min rate ripple produces ±8% power output noise. + +### Options + +#### Option A — Pre-filter theta_ist before differentiating (best return for effort) + +Apply a separate, heavier IIR to the raw temperature *before* the derivative: + +```python +beta = 0.05 # smaller = more smoothing, more lag +self.theta_ist_filtered = (1-beta)*self.theta_ist_filtered + beta*self.theta_ist +heatrate = (self.theta_ist_filtered - self.last_theta_ist_filtered) / self.dt * 60 +``` + +This is fundamentally better than post-filtering: the derivative of a smooth +signal stays smooth. `beta` can be tuned independently of `alpha`. + +#### Option B — Sliding-window linear regression + +Fit a line through the last N samples of `theta_ist` and use the slope: + +```python +slope, _ = np.polyfit(t_window, theta_window, 1) +heatrate_ist = slope * 60 # K/s → K/min +``` + +Gives the minimum-variance rate estimate for additive white noise, with a clean +constant lag of N/2 seconds. N ≈ 20–30 s is a reasonable starting point. + +#### Option C — Model-based rate (Smith predictor only) + +`Pot.process()` already computes `p_pot = delayed_power_in − L·M·(temp − theta_amb)` +and integrates `d(temp)/dt = p_pot / (M·C)`. This can be used directly: + +```python +rate_model = self.model.get_p_pot() / (self.model.M * self.model.C) * 60 +``` + +(`get_p_pot()` is not yet public on `Pot` but trivially added.) The rate is +completely noise-free because it is derived from the heater power signal, not +from differentiating the sensor. Accuracy depends on model calibration — see +sections 1–2 above. + +#### Option D — Reduce kp of the Heat/Cool PID + +Halving `kp` from 0.08 to 0.04 halves the power noise at the cost of slower +rate convergence. Treats the symptom rather than the source. + +### Recommendation + +Start with **option A**: one extra IIR state, localised to the rate +computation, no other structural changes. Set `beta` so that +`τ = (1−beta)/beta · dt` is 2–3× the dominant stirrer-noise period (visible +as the ripple frequency in `rate_ist` in the log). + +For the Smith predictor, **option C** is the most principled long-term +solution — it replaces a noisy numerical derivative with the model's own +analytical rate, which the Smith predictor is already tracking.