Take Kalman tuning from config, not from the saved state

q and r were written into station_state.json and restored over the configured
values, so tuning was effectively immutable in the field. This was found the
expensive way: the retune in the previous commit was deployed, the service
restarted cleanly, and the filters carried on with q = 2e-6 because that is
what the state file said. Measured median rate afterwards was 14.4 C/h against
12.4 before, which is to say nothing happened.

Only the estimate is state. x, P and initialised are restored; q and r now
come from config every time. P may be momentarily inconsistent with a changed
q, which costs a few hundred samples of reconvergence and is far cheaper than
a configuration change that appears to work and does not.

load_dict also now skips filters this build no longer has, rather than
resurrecting them from an older state file.
This commit is contained in:
2026-08-19 19:22:49 +01:00
parent b9a468542d
commit 3f41881c64
2 changed files with 57 additions and 4 deletions
+31
View File
@@ -333,3 +333,34 @@ def test_kalman_rate_is_physical_in_a_still_room():
assert np.median(settled) < 3.0, (
f"median |rate| {np.median(settled):.1f} C/h in a room drifting 0.4 C/h")
assert np.percentile(settled, 95) < 10.0
def test_retuning_q_survives_a_reload():
"""Tuning lives in config, not in the state file.
q and r were persisted and restored, so a retune deployed to a running
station did nothing: the service restarted and the filters carried on with
whatever tuning was in force when the state was last written. The symptom
is a config change that appears to work and does not, which is the worst
kind.
"""
from ashvale.config import load_config
from ashvale.estimation import SignalTracker
cfg = load_config()
cfg.sensor.kalman_q_temp = 2.0e-6 # an old, badly tuned state file
old = SignalTracker(cfg)
for i in range(50):
old.step(1.7554e9 + i * 2.0, 24.0, 50.0, 1013.0, 43.0)
saved = old.to_dict()
assert saved["filters"]["temperature"]["q"] == 2.0e-6
cfg.sensor.kalman_q_temp = 1.0e-9 # the retune
fresh = SignalTracker(cfg)
fresh.load_dict(saved)
assert fresh.filters["temperature"].q == 1.0e-9, \
"the state file overrode the configured tuning"
# the estimate itself must still be carried across
assert fresh.filters["temperature"].initialised
assert fresh.filters["temperature"].x[0] == pytest.approx(
old.filters["temperature"].x[0])