A heat pump from a weather-compensated log¶
bestest_hydronic_heat_pump is BOPTEST's emulator of a single-zone house heated by a modulating
heat pump. Here prescribe drives it closed loop, planning from a log that a weather-compensated
controller left behind. That controller's outdoor-reset curve raised the modulation on cold days,
and cold days are the days the house loses heat fastest, so a model fitted on the log as it stands
mixes the heat pump's effect with the weather's.
This case study is not a script in this repository. It runs in
causaldyn-bench, for hours, against a live
emulator, and it was pre-registered: the module's docstring, committed before any scored episode.
just boptest-prescribe-live # in causaldyn-bench, with the BOPTEST stack up
At every half-hour step both arms make the same call from the zone temperature just measured, and the heat pump gets the first action of the schedule:
prescribe(
panel,
levers=[Lever("modulation", 0, 1, unit_cost=0)],
target=Target("zone", value=target),
horizon=16,
dt=0.5,
tolerance=0.5,
x0=[zone],
adjustment=graph,
) # the naive arm passes adjustment=()
The graph says what the logging controller and the house do: the reset curve reads the outdoor
temperature, its comfort feedback reads the bound, and occupancy, which nothing measures, sets both
the bound and the internal gains. prescribe resolves it to {outdoor, solar, bound}. The target
is the highest lower comfort bound BOPTEST forecasts over the plan's eight hours, plus an offset
swept over seven values from -0.5 to +1.5 K. The lever is free, so the offset is the only knob that
trades comfort for energy. Each arm's seven weeks trace a front, and the gate reads its energy on
that front at the discomfort of BOPTEST's own baseline controller in the same week, so neither arm
can win by heating less.
What it found¶
Six replicates, each a 20-day log of the reset controller and the week after it, the weeks running
from day 20 to day 62 of the year. Energy is BOPTEST's ener_tot in kWh/m2, read on each arm's
front at the baseline's tdis_tot in K h, and excess = E_naive / E_adjusted - 1:
| replicate | baseline discomfort | baseline energy | adjusted energy | naive energy | excess |
|---|---|---|---|---|---|
| 0 | 5.1359 | 1.7260 | 1.5813 | 1.4436 | -0.0871 |
| 1 | 3.4514 | 1.8867 | -- | 1.4391 | -- |
| 2 | 5.9027 | 1.7812 | 1.5764 | 1.4143 | -0.1028 |
| 3 | 5.6058 | 1.6424 | 1.4856 | 1.3987 | -0.0585 |
| 4 | 4.2318 | 1.6473 | 1.4717 | 1.1515 | -0.2176 |
| 5 | 3.7969 | 1.4327 | 1.2583 | -- | -- |
The mean excess is -0.1165, with a 95% t-interval of [-0.2276, -0.0054] over the four replicates where both arms have a reading. So the decision reads REFUTED: at the baseline's comfort the naive call was the cheaper one, in every replicate that could be read. The verdict does not stand, because both validity checks failed:
- V1: two fronts have no reading at the baseline's discomfort. In replicate 1 the adjusted arm's cheapest week was also free of discomfort, so its front is a single point more comfortable than the baseline. In replicate 5 the naive arm never got as comfortable as the baseline.
- V2: one naive week had 15.2% of its solves stopped by the iteration budget, against a limit of 1%.
Beside the gate, not gated: at the baseline's discomfort the adjusted arm needed 10.45% less energy than the baseline itself (95% t-interval [8.57%, 12.33%]) and the naive arm 21.12% less ([13.52%, 28.72%]), each over the five replicates where it has a reading, which are not the same five.
Where the naive call breaks¶
Fitted on the log as it stands, the heat pump's channel is linear in the zone temperature, and it
changes sign inside the range a heating target can reach. In replicate 2 it is 3.241 - 0.1413 T,
zero at 22.94 C. Above that temperature the model reads more heat as cooling, so once the zone is
above both the crossing and the target, the plan's answer to a room that is too warm is more heat.
The naive loop ran away in eight weeks, all in the three replicates whose crossing sat lowest (2, 4
and 5). Over those weeks it held mean modulations of 0.63 to 0.97 and used 2.35 to 3.75 kWh/m2
against the baseline's 1.43 to 1.78, and its discomfort reached 522 to 1403 K h. BOPTEST's
tdis_tot counts excursions above the upper comfort bound as well as below the lower one, and at
those modulations these were overheating. In replicate 5 it happened at every offset from -0.25 K
up, which is why that replicate's naive front never reaches the baseline. Track D-causal found the
same sign change on the same emulator, at 22.62 C on one of its seeds, where it pinned the command
at zero instead. The adjusted arm never ran away.
What the certificate said, and what the plant did¶
Every step is one prescribe call and only its first action is applied, so the step ahead is the
part of each plan the plant can check. Over every week of an arm: the range of the trustworthy
prefix, the one-step figures pooled, the largest finite regret bound and the share of calls where
it was infinite, and the largest share of any one week's calls stopped by the iteration budget or
left at the zero guess:
| arm | trustworthy steps | tube one step ahead K | one-step error rms K | largest error K | inside the tube | inside the tolerance | regret bound | bound infinite | stopped by budget | no progress |
|---|---|---|---|---|---|---|---|---|---|---|
| adjusted | 1-1 | 0.2926 | 0.1305 | 0.7931 | 0.959 | 0.991 | 2.15e-06 | 0.034 | 0.000 | 0.193 |
| naive | 0-0 | -- | 0.3263 | 1.6513 | -- | 0.872 | 5.34e-05 | 0.121 | 0.152 | 0.247 |
Where they disagree:
- The adjusted certificate trusted one step on every call, with an error tube of 0.29 K on average. The plant's one-step error left that tube on 4.1% of steps, reaching 0.79 K. The tube priced the channel's standard error alone, which is a scale and not a coverage guarantee.
- That error has been read three other ways since, and the run was not repeated under any of them.
It was the homoskedastic one; from 0.8.0 it is robust to noise that moves with the state. It was
the root mean of the variances of the channel's two coefficients, and one of them is the
channel's value at 0 C, which a log in Celsius reaches only by extrapolation; from 0.13.0 it is
the error of the channel's value at the log's states. It took each half-hour as independent;
from 0.16.0 one unit's error is read across its periods by cosines
(ADR 0075),
wider where the scores persist. The run's logs were not kept, so they cannot be refitted. On 600
logs simulated with this case's logging policy, size and units, the noise drawn afresh or
persisting up to AR(0.99) a half-hour
(
scripts/bench_boptest_tube.py), the run's library read the tube one step ahead at 0.25 K on average and 0.16.0 at 0.051 K, 0.11 to 0.43 of it on nine logs in ten. The cosines widened it, 1.0 times where the noise was drawn afresh and 1.8 times where it was AR(0.99), and the error at the log's states narrowed it to 0.12 to 0.23 of what it was. Those ratios put the run's 0.29 K at 0.033 to 0.125 K, under the plant's one-step error of 0.1305 K rms, and how often the plant would have left such a tube is not known. On the simulated logs 0.16.0's certificate trusted 6 to 13 steps on nine logs in ten, where the run's library trusted 1 to 5. - None of those tubes held the plant's own noise. From 0.17.0 the tube carries it
(ADR 0080):
it adds the radius that holds the plan's one-step error on the log with probability 0.95 at
each step, the error read off the fit's defect with its lag-1 correlation. The same 600 logs
were each run on from their last row 200 times under the plan, the noise and the weather going
on from the log's (
cover). The plant's error one step ahead reached 0.16 to 0.22 K at 95%, cell by cell, and it passed the tolerance of 0.5 K at 95% after 2 or 3 steps. 0.16.0's tube read 0.051 K one step ahead on average and held the plant at the first step on 40% of draws. The new tube reads 0.21 to 0.29 K and holds it at the first step on 97% to 99% of draws, cell by cell, and on 98.8% of the steps it trusts. Its certificate trusts 1 or 2 steps on nine logs in ten and 3 at most, and more than the log's own plant allows on 0 to 2% of the logs in a cell. From 0.18.0 the tube is carried by the fitted field's slope at each planned action and adds the drift's own error to the channel's (ADR 0096). Measured the same way before and after, the tube one step ahead does not move, the mean trusted steps move from 1.56 to 2.54 by cell to 1.59 to 2.71, at most 4 where it was 3, and the logs trusted past their own plant's horizon rise by one in one cell and by two in another, of 100. - The regret bound stayed small wherever it was finite, the runaway weeks included. It is a gap in the planning objective on the fitted model, not a statement about the plant. It was infinite, the objective not convex over the box, on 3.4% of the adjusted calls and 12.1% of the naive ones.
- Nothing in the certificate bounds comfort, which is what the gate matches on and what the runaway
weeks lost:
prescriberefused a constraint on its target column, so there was no barrier and nogamma*. From 0.7.0 it accepts one (ADR 0005). Whether its audit would have flagged these weeks is open: it prices the fitted model, under which more heat cools a zone above the crossing.
Where they agree: the naive certificate trusted nothing, because an asserted identification never
earns a step whatever the model predicts, and the runaway weeks bore that out. They are also what
pushed the naive arm past the V2 limit.
Scope, and it bounds what the numbers mean:
- The verdict is void, so this page makes no claim about what adjustment costs or saves here. The four readings that exist all favour the naive call.
- The lever is priced at zero, so each plan minimises distance to the target, not energy. In
replicate 4 at offset 0 the naive arm held a higher mean modulation than the adjusted one (0.296
against 0.253) and used less energy (1.1529 against 1.4770 kWh/m2). Energy use here is not
proportional to modulation, and neither arm was optimising what
ener_totmeasures. - The façade could not state three things the problem has: a bound on the steered state, as above;
a weather term in the drift, so the weather the adjusted arm partials out never enters its
forecast; and a target that changes within the horizon, so the comfort schedule reaches the plan
only through re-planning, which pre-heats up to one horizon early. From 0.7.0 a
Targettakes a schedule (ADR 0006) andprescribetakes a forecast of the weather as a driver in the drift (ADR 0007). - Six replicates, sized to the emulator time available rather than for power. Neighbouring logs share 13 of their 20 days, so the interval treats replicates as more independent than they are.
- Every number is float64, from the library at commit
7796c69: after 0.5.1, which is the version it reports, and before 0.6.0.
Results:
results/boptest_prescribe/results.md.
Code:
causaldyn_bench/boptest_prescribe.py,
through prescribe.
The rerun, with the band, the weather and the schedule stated¶
From 0.7.0 prescribe states all three things the first run could not. A second run asked the same
question with them stated. It was pre-registered the same way, on the same six logs and weeks, with
the library at f3f9eae, in float64:
prescribe(
panel,
levers=[Lever("modulation", 0, 1, unit_cost=0)],
target=Target("zone", value=lower[1:] + offset),
constraints=[Constraint("zone", hi=min(upper[1:]))],
hold_constraints=True,
drivers=[Driver("outdoor", outdoor), Driver("solar", solar)],
horizon=16,
dt=0.5,
tolerance=0.5,
x0=[zone],
adjustment=graph,
) # the naive arm passes adjustment=()
- The target now follows the lower comfort bound step by step, plus an offset swept from +1 to +2.5 K.
- The band's upper edge is held inside the solve.
- BOPTEST's weather forecast enters the drift. The fit puts every driver into the channel's
nuisance covariates, whatever
adjustmentsays, so both arms are adjusted for the weather. The arms now differ only inbound: the logging controller's comfort feedback, and the occupancy behind it.
| replicate | baseline discomfort | baseline energy | adjusted energy | naive energy | excess |
|---|---|---|---|---|---|
| 0 | 5.1359 | 1.7260 | 1.4627 | 1.4627 | -0.0000 |
| 1 | 3.4514 | 1.8867 | -- | -- | -- |
| 2 | 5.9027 | 1.7812 | 1.5339 | 1.5429 | +0.0059 |
| 3 | 5.6058 | 1.6424 | 1.4973 | 1.4967 | -0.0004 |
| 4 | 4.2318 | 1.6473 | 1.3024 | 1.3414 | +0.0299 |
| 5 | 3.7969 | 1.4327 | -- | -- | -- |
The mean excess is +0.0088, with a 95% t-interval of [-0.0140, +0.0317] over the four replicates where both arms have a reading, so the decision reads INCONCLUSIVE. This verdict does not stand either:
- V1: in replicates 1 and 5, neither arm got as comfortable as the baseline at any of the offsets. The pilot had ended them at its own least discomfort, and these weeks needed more.
- V2: counting only calls made from inside the band, the iteration budget stopped up to 7.1% of an adjusted week's calls and 5.5% of a naive week's, against a limit of 1%.
Beside the gate, not gated: at the baseline's discomfort, the adjusted arm needed 14.73% less energy than the baseline (95% t-interval [6.81%, 22.65%]) and the naive arm 14.02% less ([7.58%, 20.46%]). Each is over the four replicates where it has a reading.
What the held band did. The naive arm, now adjusted for the weather, no longer ran away: in no week did the zone end a step above the held edge more than 3.3% of the time.
The adjusted arm ran away once, in replicate 1 at the two highest offsets. There its fitted
channel, 3.579 - 0.1505 T, crosses zero at 23.78 C. It is the only one of the twelve fits under
which more heat cools a zone inside the band. The zone ended above the edge on 17.6% and 24.4% of
steps, and discomfort reached 114 and 194 K h.
The band holds the fitted model's zone inside it, and under that model more heat cools a zone above 23.78 C. Once the plant's zone is above the edge, no call can meet the barrier. So a held constraint is a statement about the model, and it protects the plant only where the model's channel has the right sign.
What the certificate said. The adjusted plans were trusted for between none and two steps, where the first run's were trusted for one step on every call. The tube one step ahead averaged 0.31 K, and the plant stayed inside it on 98.1% of steps, against 95.9% in the first run. The tube now prices a standard error that is robust to noise moving with the state. The naive plans were trusted for no step, as before.
That error was still the root mean of the coefficients' variances, each half-hour taken as independent, and the rerun was not repeated either. On the same simulated logs, under the rerun's call, 0.16.0 read the tube one step ahead at 0.10 to 0.42 of the rerun's library's on nine logs in ten, which puts the rerun's 0.31 K at 0.032 to 0.131 K, and its certificate trusted 6 to 13 steps on nine in ten, where the rerun's library trusted 1 to 5.
From 0.17.0 the tube carries the plant's own noise here too. Run on as above, with the drivers held at their last logged values in the plan and drawn on in the plant, the plant's error stays within 0.5 K at 95% for 2 to 5 steps, cell by cell. 0.16.0's tube held the plant at the first step on 35% to 51% of draws. The new one holds it there on 98% to 99% of draws, and on 98.6% of the steps it trusts. Its certificate trusts 2 or 3 steps on nine logs in ten, 4 at most, and more than the log's own plant allows on 0 to 2% of the logs in a cell. The held forecast's own error is not in the tube, and over these few steps the weather moves little.
Scope: the same six replicates, the same shared log days, and the same zero lever price as above. Neither verdict stands, so this page makes no claim about what adjustment costs or saves on this building.
Results:
results/boptest_rerun/results.md.
Code:
causaldyn_bench/boptest_rerun.py.
Before prescribe: forecast-MPC against the baseline¶
The first run on this emulator did not go through prescribe. It identified a thermal model from a
slow pseudo-random exploration, T_next = 0.980·T + 0.243·u + 5.76 with u the heat pump's
modulation in [0, 1], and drove the house with a forecast-driven comfort MPC, over a one-day
episode at a 30-minute step:
| KPI | BOPTEST's baseline | forecast-MPC |
|---|---|---|
tdis_tot (thermal discomfort, K·h) |
8.01 | 7.32 |
ener_tot (energy) |
0.393 | 0.354 |
cost_tot |
0.100 | 0.090 |
emis_tot (emissions) |
0.066 | 0.059 |
pele_tot (peak electrical demand) |
0.0190 | 0.0194 |
It beat the tuned baseline on discomfort, energy, cost and emissions at once, four KPIs of five; its
peak electrical demand was 2.0 % above the baseline's. Results:
results/boptest.md.