Skip to content

v9.13.0

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Nov 07:25
· 2 commits to main since this release
a7cbc3f

This release replaces the simulator's model of the heater
(nwp500.sim, nwp500.sim_calibrate and nwp-cli simulate).
The cloud API, the MQTT and BLE clients and their models are unchanged.

  • The tank is stacked layers (sim.physics.stratified), in place
    of the two-zone tank: draws, losses, circulation and each heat source
    act on the layers they reach, and the probes read the layers at their
    heights.
  • The heat pump is a performance map fitted on the heater's own
    runs, its heat and electricity each a plane in the air and the
    condenser's water, in place of the refrigerant cycle and the heat
    book.
  • Calibration fits the layered tank and the map from a recorded
    history and writes heater-params/2; a heater-params/1
    document is refused and must be calibrated again.
  • Parameters can be scored on recorded days they were not fitted on
    (nwp500.sim_score), and two parameter sets compared on the same
    events.
  • A simulation starts from the layers a history leaves, and can go
    on from where another ended (HeaterState.resume).

These changes break callers of the simulator: see Upgrading below.

Upgrading

The simulator's tank is stacked layers and its heat pump a fitted
performance map; the two-zone tank, the refrigerant cycle, the heat
book and the element-mode planner are gone. To move a caller over:

  • Parameters. A heater-params/1 document is refused: calibrate
    again with nwp-cli simulate calibrate (or
    sim_calibrate.calibrate), which writes heater-params/2. A
    /2 document with removed fields, such as the heat book's, still
    reads: they are left out and named by params.unknown_fields. One
    written before the layered tank also reads its tank as the priors,
    with tank unfitted, until it is calibrated again.
  • Removed parameters. PowerParams and HeaterParams.power;
    BookParams and HeaterParams.book; the two zones' tank, loss
    and draw terms (TankParams.lower_zone_share and
    stratification_gap_tau_h, LossParams.ua_btu_hr_f,
    ua_exponent, bottom_loss_btu_hr, the DrawParams reach and
    observability terms); the refrigerant cycle's CompressorParams
    fields; ElementParams.upper_f_per_min and lower_f_per_min;
    the readiness evidence and outlet lag (ReadinessParams.evidence,
    delivered_lag_gal, fall_f_per_gal); the circulation's book
    terms (CirculationParams.loss_btu_per_gal, book_circuit_gal).
    The Removed and Changed lists below name each.
  • simulate no longer takes ledger, lead_draws,
    physics_usable or book, and readiness and
    time_to_ready no longer take book. extensions.HeatLedger
    and its types are gone.
  • The tank's state. HeaterState.layers_f carries the layers a
    walk starts from; without it every answer reads the profile the two
    probes stand for and says tank_profile_estimated.
    sim_adapters.layers_from_history and layers_from_changes, and
    sim_home_assistant.HomeAssistantTank (was HomeAssistantBook),
    give it from a recorded history; book_from_history and
    book_from_changes are gone.
  • element_mode(scenario, params, *, mode, ...) takes a
    Scenario in place of state, settings and at, and no
    hp_lower_minutes; ElementModeResult has no plan.
    physics.element.mode_plan and mode_recovery are gone.
  • draw_forecast.draws_first gives each placed draw as
    (at, gallons, per-draw gallons, seconds), its time drawing added.
  • A custom Physics implements estimate, observe,
    advance and draw on a TankState.
  • The circulation's terms. CirculationParams.displaced_gal is
    the pipe's measured volume, no longer an effective one: measure it
    and the new pipe_ua_btu_hr_f on one run after a rest
    (calibration.measure_pipe), then fit the return's rest and
    cooling with calibration.fit_return(runs, displaced_gal, pipe_ua_btu_hr_f), which returns those two. fit_displaced_gal
    is gone.

Added

  • A run under way at a walk's start, from the water under its
    probes.
    CompressorParams.running_probes_warm_f ((0, 0) unless
    set; defaults.SITE_RUNNING_PROBES_WARM_F, (0.51, 0.91) degF, in
    site_example_params and so default_params): how much higher
    the upper and lower probes read than the water at them while the
    compressor heats. A scenario that starts with the compressor running,
    not starting there (running_starts) nor resumed from a walk's end
    (HeaterState.resume), takes its tank as the probes less these, its
    layers given moved so much at the probes (TankPhysics.at_probes),
    in simulate, forecast (and so next_recovery and
    readiness's recovery) and active_cycle, whose checks of the
    water at the target are of it too. A walk's end state
    (HeaterState.resume given, carried or dropped) holds the walk's
    water and is taken as it is. The values are the probes' step down in
    the 5 minutes after 47 quiet stops of runs heating 40 minutes or more
    (measured 2026-10-28, recovery-speed). On the 29 held-out days at
    DrawParams.meter_reads 0.85 (measured 2026-11-06), the five runs
    under way at a day's start on a hot tank, 22 to 128 minutes from their
    stop, came 4.7 to 15.2 minutes short (8.5 on average), reaching the
    stop on probes read warm, and now 2.9 short on average; walked from
    their recorded starts, four of them come 3.0 short to 10.2 long. The
    day's electricity comes 11.41 % off and 0.72 % low against 12.66 and
    2.15, the tank's metrics within 0.02 degF.

  • The evaporator fan's high speed in the compressor's power.
    CompressorParams.fan_high_below_air_f (None unless set) and
    fan_high_w (0): a walk's compressor run puts its fan at the high
    speed from the first instant the air is under fan_high_below_air_f
    to the run's end (TankPhysics.fan_high,
    physics.heat_pump.fan_high_from,
    decay.AmbientSampler.first_under,
    stratified.Compressor.fan_high_at), and the power map's
    electricity is its plane's plus fan_high_w while it is
    (physics.heat_pump.power_w, its fan_high; without it the speed
    is read from the air alone, as the record's replay reads it); the heat
    is unchanged. A run a walk ends under way keeps its fan's speed in the
    resume (CompressorRun.fan_high_at, fan_low_through), so the
    walk that goes on from SimulationResult.end_state holds it; a run
    under way at a scenario's start without one reads its fan from that
    start. heat_pump_fit.power_blocks gives each block
    run_air_min_f, the least air of its run's every minute to the
    block's end; heat_pump_fit.fit_power_map takes the air
    (fit_heat_pump the base's), takes a block for the high speed where
    that is under it (heat_pump_fit.fan_high), fits the plane on the
    rest and the step, at least 0, as the mean the plane leaves of the
    high speed's (Plane.fan_high), fitting no plane with too few or
    too alike at the low speed and keeping the base's step with none at
    the high; power_map_error reads the blocks so; the calibration
    report shows the step, and violations checks the low speed at the
    fan's air and over and the high at every air, unless the fan's air is
    at or under the envelope's coldest. On one NWP500-65 (measured
    2026-11-05) the fan ran at about 543 rpm or 895, went to the high
    speed inside a run as the heater's own air reading fell to about 58
    degF and held it to the run's end, and its high speed drew 23.7 W more
    on the 10-minute metered climb blocks with one speed throughout. Read
    from the air at the heater and held so, at 59.0 degF and +20.68 W
    fitted on the blocks before 2026-09-18 with the plane kept (the
    blocks' speed so taken agreeing with the fan's measured one on 97.3 %
    of them), the 1,807 winter blocks under 60 degF read 1.1 W over (in
    the fit) against 19.2 low with the plane alone, the winter's at 60 to
    80 degF 1.6 W low against 6.4, and the 50 held out under 60 degF 9.3 W
    low against 26.3. The 29 held-out days at DrawParams.meter_reads
    0.85 move only in their electricity, the day's 12.66 % off against
    12.80, 2.15 % low against 2.48.

  • The house meter's reading of a gallon. DrawParams.meter_reads
    (1, the meter as it reads, unless set; over 0 and at most 2) takes
    each metered gallon as 1 / meter_reads delivered
    (DrawParams.delivered_gal) where the tank takes it or is judged
    against it: a Draw's gallons in a walk, a DrawWindow's
    expected gallons, a bath's and a shower's in time_to_ready (and
    ReadinessParams.bath_gal and shower_gal), a record's draws in
    its replay and the tank fit, a delivery's gallons in the score. A
    record still holds the meter's gallons. The flows a draw is judged by,
    and its domain volume, stay the meter's. On the site's 29 held-out
    days (measured 2026-11-03 on the parameters in use) 0.85, a known
    volume's reading on that day, puts the day's electricity 2.4 % low
    against 9.8 % at 1, 12.9 % off against 14.3, and the false starts at
    0.28 a day against 0.17. A meter whose reading moved takes dated
    readings (DrawParams.meter_reads_since, (from, share) in
    order, meter_reads before the first; DrawParams.reads_at):
    each metered gallon is taken at the reading at its own instant, a
    bath's and a shower's at the scenario's start, a window's at its
    start. On the site, 1 before the meter's step of 2026-09-12 19:51
    UTC and 0.85 from it scores the held-out days as 0.85 throughout
    does, as they fall after the step; site_example_params carries
    it (defaults.SITE_METER_READS_SINCE), and default_params
    reads as the meter reads.

  • How near a walk came to its start triggers.
    sim.start_margins(result.points, hp_lower_on_f, tou_windows, loads, params) gives each coast of a walk and its nearest approach to each
    heat-pump trigger in force over it (StartMargin): the lower zone
    over its trigger (hp_lower_on, no nearer than the setpoint), held
    as the heater holds it and in Energy Saver released by a drop, a
    draw's or the circulation's; inside the TOU windows the upper zone
    over the setpoint less UPPER_DEFICIT_F as well; where, and whether
    the coast ended in a start. A caller can weigh a start the walk did
    not call by how nearly it called it. On the site's 29 held-out days, 3
    of the 11 recorded starts the walk did not place fell on coasts it did
    not start: inside the windows at the smallest and fourth smallest
    upper margins of 23 (1.8 and 7.8 °F), and for the lower trigger at the
    second smallest of 24 (7.0 °F). Each carries the gallons drawn in the
    hour up to its point (StartMargin.drawn_gal) and whether they came
    to 8 gal or more (after_large_draw), where a lower margin is the
    least sure: on the site's held-out single draws of that size the
    walk's lower probe fell at once, a median 12.8 °F under the record's a
    minute after, and an hour after was 3.4 to 16.5 °F off, either way, on
    the 9 it missed by 3 °F or more. simulate gives them on its result
    (SimulationResult.margins), read on the lower trigger, the TOU
    windows, the draws it took, the circulation and the
    parameters the walk had. start_margins counts no draw before the
    walk's first point, nor, given until, at or after it.

  • PathPoint.heating_disabled: heating was disabled at the point
    (Vacation, or powered off with no entry having turned the heater on).

  • A slower draw takes more of the tank than the valve's share.
    Each delivered gallon takes s + undiluted_share · (1 − s) of the
    tank's water, s the valve's share
    (DrawParams.undiluted_share, 0-1, 0 by default), in the walk, the
    deliverable gallons and a history's metered tank gallons;
    physics.draw.hot_fraction and withdrawn_gal take it.
    site_example_params sets 0.25 (SITE_UNDILUTED_SHARE): after
    the site's 32 held-out draws of 8 gal or more it put the lower
    probe's fall within 4.86 °F RMS of the record's, against 5.92 with
    the valve's share alone. On the 29 held-out days the
    electricity came out 14.3 % off against 16.0, the recoveries 14.5 min
    against 17.1, and one day whose start at the TOU window's close was
    missed is now called.

  • A fast draw takes the tank's water whole. A draw at
    DrawParams.undiluted_gpm or faster (its gallons over its
    duration_s, Draw.flow_gpm) takes as much of the tank's water
    as it delivers, as with no valve. site_example_params sets 2.0
    gal/min (SITE_UNDILUTED_GPM); the default, None, takes every draw
    through the valve. On the site's record the four draws of 20 gal or
    more at 2.1-2.7 gal/min took about 1.3 times the valve's share. The
    record times each draw by the history's draw detector
    (TankRecord.draw_seconds, tank_fit.draw_durations,
    draw_spans), and sim_score walks one at that flow or faster
    over its time, one under way at an event's start replayed as far as
    it ran and walked on for the rest. On the 29 held-out days the two
    the tank ran out on are called, where neither was. The upper probe's
    lowest is 2.9 degF off against 3.8, and the electricity 16.0 % off
    against 16.5. The 16 held-out recoveries with water in use are 25.0
    min off against 31.4.

  • Candidates weighed over many households. sim.draw_ensemble
    samples members from a weekly draw forecast (Poisson counts at each
    slot's rate, long draws' durations lognormal on the published mean
    and 90th percentile, volume with duration) and weigh walks every
    candidate on the same members: cold_share, each member's lowest
    delivery over its long draws, and its electricity, for a cost and its
    spread.

  • What a change leaves behind. sim.carry_on walks each walk's
    end on through one tail, the heater in its standing settings, so
    changes are weighed with the tank each leaves rather than over their
    own span alone. sim.Carried holds the walk and its tail, their
    electricity by slot, and how far the tail's end is from the first
    walk's (residual_f, residual_wh, heating_alike,
    settled). Whether the tails go on alike is walked past every
    instant still due and every TOU window's close, up to 8 days
    (sim.carry.GO_ON_MAX); tails owing one past that are not counted
    alike.

  • The scoreboard walks whole days. sim_score.day_events takes
    each local day a record observed throughout, and sim_score.score
    walks it from its start as the heater was told over it: the recorded
    setpoint and mode writes, the TOU windows the heater marked, the
    metered draws, the circulation and the air. It is scored on what a
    scheduler compares plans by: the day's electricity and when in the
    day it fell, the order of days by their electricity, the
    compressor's and the elements' starts, the upper probe's lowest and
    the tank at the day's end (Events.days, the day_* metrics).
    sim.record.Record carries the lower probe's stop threshold
    (hp_lower_off_f), which sim_adapters.record reads.

  • A simulation goes on from where another ended. A scheduler that
    stands the simulator in for a heater calls it once a step, and each
    call began as a mode entry: a running compressor's hold, lockout and
    pending start, an element's dead time and the heat pump's
    unavailability were taken afresh, and the heating intervals running
    at the cut were cut. SimulationResult.end_state, end_history
    and end_settings are the heater at the end: the probes and
    layers, what runs, and HeaterState.resume, the JSON text of a
    versioned document of the controller's state. Passed as the next
    Scenario's state, history and settings, the walk goes
    on: chained at marks of the clock's 10-minute grid, the calls give
    the path and heating of one walk with marks at the cuts, through
    codec and JSON too, with TOU and the climate's mains across
    midnight. A
    cut off the grid splits a walk's step (0.002 °F after two days of
    15-minute calls). A setpoint or mode other than the one the last
    call left is a write at the start (serves="settings"), its
    setpoint held and range-checked as an app's write is. Into or out of
    Vacation or Power Off, which the simulation holds no rule for as a
    write, the call starts afresh from the tank and the history and says
    resume_dropped_heating_disabled_changed. engine.simulate
    takes resume and Simulation.resume gives it
    (engine.Resume, to_document, from_document); one from
    another instant, or of another version, is refused.

  • The mains by the day, from the climate. cold_supply.mains_f
    is Burch and Christensen's correlation of a utility's water with the
    year's outdoor air (its mean, and its warmest monthly mean less its
    coldest), and cold_supply.climate_of reads that climate off a
    year of outdoor readings. DrawParams.climate_f carries it:
    DrawParams.mains_on(day) gives that day's mains, else
    inlet_temp_f. A scenario without an inlet meets each draw with
    the mains on its day, as a draw window's deliverable gallons and
    time_to_ready's walk read them, and defaults_used names the
    start's; the history's replay, the
    tank's fits and readiness(..., on=day) take each day's. At one
    installation (mean air 61.3 °F, swing 19.9 °F, its weather
    station's 2025-11 to 2026-10) the correlation read 1.5 °F under its
    inlet probe's plateaus on 25 days of 2026-09-20 to 10-28, and 0.8 °F
    under from 10-20 on; September's 72.4 °F held all year was 4.4 °F
    over by 10-28. site_example_params carries that climate
    (SITE_CLIMATE).

  • The tank's layers from a recorded history, for any caller.
    sim_adapters.layers_from_history and layers_from_changes give
    HeaterState.layers_f at an instant from a heater's statuses or its
    values' changes, the household's flow, the circulation's runs and the
    air: the last 24 hours walked through the tank (nwp500.sim.history:
    record, replay), corrected to the probes as they were read.
    sim_home_assistant.HomeAssistantTank.layers gives them from Home
    Assistant, and
    nwp-cli simulate run starts a scenario that starts now from them,
    with the scenario's probes laid over them: before, it started from
    the two probes alone, as tank_profile_estimated said.

  • A tank's layers as where a simulation starts
    (HeaterState.layers_f, as many as TankParams.layers, the
    bottom first). Two probes do not say how the water between and below
    them is layered: without the layers, the simulator makes them of the
    probes and says tank_profile_estimated. tank_fit.start_state
    gives them from a recorded history, and sim_score.score(..., record=...) starts each event from them.

  • Where the condenser heats, fitted. tank_fit.CONDENSER_TERMS
    (TankParams.condenser_span's low end and width) is fitted on
    recoveries (sim_score.layer_recoveries: the compressor from its
    start, its hold by time off, and the circulation through it) together
    with the tank's other terms on the quiet windows and draws
    (sim_score.fit_layers). Fitted apart, the span that times the
    recoveries left the bottom of the tank each window starts from wrong:
    held out, a coast's lower probe went from 0.52 to 0.86 °F.

  • The layered tank fitted to a history (nwp500.sim.tank_fit).
    Two probes do not say where the water below the lower one is, so
    each event starts from the layers the history before it leaves
    (replay): a day of the recorded heat pump, element, draws and
    circulation walked through the layers, corrected to the probes every
    ten minutes. fit fits the tank's terms (TERMS) by robust least
    squares on the probes over the record's quiet coasts and the hours
    after its draws; it takes a map_ to spread the events over
    processes. nwp500.sim_score.fit_layers and score_layers fit
    and score it on a history's events with score's metrics.

    On the held-out days, both models given the heater's mixing valve and
    circulation, the fitted layered tank tracks a coast's upper probe to
    0.189 °F (the two-zone model 0.299) and its lower to 0.519 °F
    (0.630), each interval clear of the two-zone value, and the probes
    after a draw to 0.310 and 2.379 °F (0.281 and 2.161, inside the
    intervals).

  • The layered tank's probes and ends. Each probe reads the layers
    around its height, weighted by distance over its spread
    (TankParams.upper_probe_spread_gal, lower_probe_spread_gal),
    and the lower one reads lower_probe_offset_f above them. The top
    layer loses heat through LossParams.ua_top_btu_hr_f, and the
    sides through ua_side_btu_hr_f.

  • nwp500.sim.physics.stratified: the tank as stacked layers.
    Twelve layers by default (HPWHsim's model), the bottom first, each
    mixed within itself, with operators that conserve energy and never
    take a layer past what drives it:

    • mix turns inversions over;
    • lose relaxes each layer toward the air exactly, the bottom
      layer through the bottom's UA too;
    • conduct conducts between neighbours;
    • add_heat, with the condenser's heat spread over the span it
      wraps and an element's into its layer;
    • flow pushes water in at the bottom, through an inlet zone that
      mixes it in as it comes, and out of the top as a plug;
    • draw takes a delivered draw through the mixing valve.

    The probes read the layers (observe); estimate and
    assimilate build or correct a profile from them, and
    deliverable_gal is the tap's gallons to the line. walk steps
    the tank through time on a clock-anchored grid, cuts it where a stop
    holds, and books every term's energy, which closes. A coast never
    falls below the air, an element's heat is its electricity, and 100
    gal drawn in an hour empties the top. Its parameters are new
    TankParams fields (layers, the probes' and elements'
    heights, condenser_span, inlet_mix_gal,
    conductance_btu_hr_f) and LossParams.ua_bottom_btu_hr_f,
    priors until fitted (tank_fit).

  • The heat pump's operating range. It runs only with the air at
    41-113 °F and the tank's lower zone at 50-149 °F
    (ControllerParams.hp_air_range_f, hp_water_range_f, from the
    user's manual). Outside them a running compressor stops and the
    elements heat the tank by Electric's rule; back inside, a recovery the
    elements were carrying returns to the heat pump when it has room to
    heat. A recovery stops at 149 °F, and the elements finish a setpoint
    above it. forecast, active_cycle and time_to_ready say
    heat_pump_unavailable; next_recovery is unavailable
    (Recovery.hp_unavailable), and time_to_ready projects every
    mode as Electric, Heat Pump mode from the heater's own trigger. A walk
    that relied on the elements' takeover says
    envelope_fallback_unverified, since none is on the record. A
    recovery with its air outside the span its heat was fitted on says
    heat_map_out_of_domain. AmbientSampler.outside finds the air's
    spans outside a range, on the readings given
    (TankPhysics(air_readings=...)). See
    :doc:/explanation/simulator-methodology.

  • nwp500.sim_score: parameters scored on recorded events they
    were not fitted on.
    It takes the events from the record without a
    model (recoveries that reached their target, quiet windows, a meter's
    isolated draws, and its clean draws with the compressor off, against
    the gallons each delivered before the top fell below 105 °F), predicts
    each through the public simulator, and reports each metric's mean
    with a 90 % interval from a bootstrap of local days. Two parameter
    sets scored on one history are scored on the same events, so models
    that share no terms can be compared (compare). It does no I/O.
    See :doc:/how-to/calibrate-simulator.

  • nwp500.sim.heat_pump_fit: a heat pump's recoveries measured
    from a history, without a model of the cycle.
    recovery_frame
    puts each recovery (sim.calibration.heat_pump_cycles) on a minute
    grid in three phases: the hold after a start, the climb, and the run
    past the target. start_holds, run_ons (with a fixed run-on and
    a stop at an overshoot judged against each other), climb_heat
    (the heat a mixed, draw-free climb puts into the tank) and
    power_blocks (the metered power in 10-minute blocks) measure
    from it. evidence gathers them, and fit_heat_pump fits the
    heat's and the power's maps and the run-on on them
    (with_heat_pump applies it). The helpers that read a history
    frame moved to sim.history_frame. physics.heat_pump reads the
    map.

  • nwp-cli simulate calibrate --holdout-from DATE fits on the
    history before DATE and prints the base's and the fit's scores on
    the events after it.

  • sim_calibrate.prepare and prepare_changes: a history as
    the fits read it, the frame a scorer reads.

  • The setpoint in a recorded history. sim_adapters.history and
    change_history carry a setpoint_f column;
    HeaterEntities.setpoint names its entity, and ChangeRows has
    a setpoint_f series.

  • ChangeRows.until and HeaterHistory.until, the mirror of
    since: a history split at an instant is the two.

Changed

  • The tank is calibrated on the history's last seven weeks.
    sim_calibrate.calibrate and calibrate_changes fit the tank's
    terms on the quiet windows, draws and recoveries starting in the last
    tank_weeks of the history (TANK_WEEKS, 7), the history before
    them read only as their replay's lead-in; tank_weeks=None fits on
    every event, as before. The heat pump is still fitted on the whole
    history. nwp-cli simulate calibrate --tank-weeks WEEKS sets it (0
    for the whole history), counted back from the history's last status
    (with --holdout-from, the last before it). On one site's record,
    the tank fitted on its events in the seven weeks before 2026-09-18
    scored better on the next 29 days than fitted on the five months
    before: 13.4 against 16.1 minutes off a recovery, 3.1 against 3.8 °F
    off the day's end, 10.1 against 11.5 % off its electricity, 0.82
    against 0.79 of the compressor's starts placed; the probes after a
    draw and in standby 0.02 to 0.13 °F worse, the day's lowest upper
    probe 2.5 against 2.2 °F off and a recovery's electricity 4.9 against
    4.3 % (calibrate-simulator).

  • The probes' lean stops at 0.75 of the height. A profile built from
    the two probes (stratified.estimate) and the replay's correction
    toward them (assimilate) ran linear from the lower probe's height
    to the upper's; they now run linear to
    TankParams.correction_top_height (0.75, not fitted; the upper
    probe's height if lower) and level above it. It must be above the
    lower probe; the tank fit (sim.tank_fit.fit) keeps the lower
    probe's height at least 0.01 under it. At the upper probe's height the
    water just under that probe took a cold lower probe's correction:
    three large held-out draws in an evening window without heating, two
    from tanks with the probes 22 and 40 °F apart, dropped the walk's
    upper probe 2.9-9.8 °F against the record's 1.1-3.1. Over the site's
    29 held-out days (measured 2026-11-05 at DrawParams.meter_reads
    0.85 on the parameters in use, not refitted) the upper probe through
    draws reads 0.33 °F RMS against 0.37, the recoveries 13.4 min off
    against 14.1 (the draw-free 3.5 against 4.2), the day's electricity
    12.8 % off against 12.9, the compressor's starts as before; the
    elements start twice more over the 29 days, the recoveries'
    electricity is a mean 2.7 % low against 1.6, and the lower probe
    through draws reads 1.71 °F RMS against 1.68. A walk without layers
    reads the water above the lean's top level, a little above the upper
    probe (0.03 °F per °F between the probes), so it crosses the line at
    once: on the defaults, with the upper probe 0.5 °F under the line,
    nothing is promised with the probes up to 16 °F apart and the 15.8 gal
    above 0.75 from 18 °F, against 6.6-9.2 gal before.

  • A gallon of hot water holds less heat. Every heat in the
    simulator took water at 8.34 BTU per gallon per °F, the mains'
    value; hot water is lighter, 8.24 at 120 °F and 8.20 at 140, so the
    tank held up to 2 % more heat than it does. The new
    physics.water has the heat per gallon by temperature
    (btu_per_gal_f, Kell's density and IAPWS-95's heat capacity to
    0.002) and keeps each layer's heat as heat_f, the temperature
    water of a constant 8.34 would have with it, so mixing, flows and
    heat put in stay linear and conserve energy. The loss, the
    column's conduction, the delivered water's mix, the pipe's loss
    (stratified.Flowing.pipe_ua_btu_hr_f replaces share;
    circulation.steady_share takes the water's temperature) and the
    heat fits (heat_pump_fit.climb_heat, calibration's run heat
    and measure_pipe) read it. The 65's heat map and loss, fitted in
    BTU against the constant, are restated at their reference water
    (defaults._restated): 4,445 BTU/h at 65 °F air and 120 °F water
    (from 4,499), -16.7 per °F of water (from -15.9); refitted under the
    water's heat the map lands within 0.1 % of that. The site's pipe
    measures 14.4 BTU/h·°F (from 14.6). The element's metered power
    stands, so it heats hot water faster: the goldens' element runs end
    4 s to 2.2 min sooner, and the golden 8 h coast starts its heat
    7 min sooner. simulate takes about 20 % longer.

  • A simulation's cost grows with its span. A coast walked the tank
    to the next fixed event, often the horizon, to find the heater's own
    start, then again to the start; and every step of a walk scanned all
    its draws, flows and change instants, and the trigger search every
    drop for every segment. A coast now walks 2 hours, then each time
    twice as far (to a 10-minute mark of the clock, where a split walk is
    the walk whole), and stops once the heater's own start is triggered,
    which is then the start the whole walk finds; the draws, flows,
    instants and drops are found by bisection. On one NWP500-65's
    parameters with about 30 draws a day, simulate over 14 days takes
    392 ms against 2.6 s in Heat Pump mode and 441 ms against 4.8 s in
    High Demand. A coast of 224 days with draws and a circulation run
    four times a day takes 1.9 s against 46 s. The answers are the same.

  • A circulation run returns its pipe, then the tank's own water a
    little cooler.
    A run was priced on an effective volume (19.36 gal
    at the site) back at the return's rest, then the tank's own water as
    hot as it left; a run held on for 35.7 min after a 4 h rest on
    2026-10-29 brought the pipe's water back for 5 gal, rose half way to
    its steady value at 6.86 gal and held 1.6 °F under the upper probe at
    1.08 gal/min. CirculationParams.displaced_gal is now that measured
    volume and the new pipe_ua_btu_hr_f the pipe's loss: the run's
    own water comes back at the rest plus circulation.steady_share
    of the top's lift over it (exp(−UA / ṁc)), in the walk
    (tank_physics.circulation_flows, stratified.Flowing's new
    rest_f and pipe_ua_btu_hr_f), the tank's fit and
    run_heat_btu.
    calibration.measure_pipe measures the two on such a run, and
    fit_return fits the return's rest and cooling given them (3.74 °F
    and 0.72 h at the site, on 122 runs). The run's readings are a test
    fixture, circulation_run_2026-10-29.json. Fitted before
    2026-09-18 and scored after, a recovery's duration comes out 27.2
    min off against 30.9 (5.45 against 5.69 with no draw) and its energy
    4.4 % against 4.5. Refitted on the whole record with it, the site's
    tank fits to 2.26 °F RMS (from 2.50): the bottom's loss 0.445 to
    0.559 BTU/h·°F, the condenser's span 0.148-0.323 to 0.154-0.289, the
    lower probe 0.469 to 0.476; its power map takes +1.02 W per °F of
    air and +2.72 per °F of the condenser's water (12.7 W RMS, from
    13.0).

  • Water warmer than the tank's inlet zone rises past it. The
    layered tank took every flow in at the bottom and pushed the column
    up as a plug, so a run's warm return lifted the cold water the pipe
    had brought back through the whole tank, and the upper probe of the
    run above read 2.0 °F RMS off. Water warmer than the inlet zone now
    goes in where the column is as warm and pushes out only the water
    above (stratified.flow); on that run the upper probe reads
    0.8 °F RMS off, and the tank gives up the heat the return line
    measured but 3.2 %. Only circulation flows this way; draws do not. The
    lower probe, at its fitted height (0.48), cannot read the 105.8 °F
    it fell to on that run: no stable column with a probe that high
    holds so little heat below it, which places the probe lower than
    the tank's fit does.

  • The upper element's height, documented. An element's heat turns
    its layer over into the ones above, so its height acts in whole
    layers: of twelve, the upper element's 0.72 heats 21.1 gal and climbs
    the upper probe 1.59 °F/min, where the record's 29 clean runs climb it
    2.01 (0.75-0.833, 15.8 gal, gives 2.00). The record's 24 element runs
    from 2026-04-16, most with water drawn or the compressor running,
    each walked from its replayed layers, read the upper probe to 7.9 °F
    RMS at 0.72 against 10.2 at 0.79, so 0.72 is kept;
    TankParams.upper_element_height and the simulator's docs say so,
    and no longer call it fitted.

  • The heat pump's maps are read along their planes. The heat's and
    the power's maps were held at the edges of the span each was fitted
    on: a 45 °F air heated as 61 °F does, the condenser's water below 114
    °F got the heat at 114 °F, and the heat and the power, held at
    different water, gave a COP that bent where neither was measured.
    physics.heat_pump now reads each along its plane wherever the
    heat pump runs, the walk holding its reads to the controller's
    envelope (stratified.Layout's new map_air_f and
    map_water_f: a cold bottom can leave the condenser's water under
    the envelope the controller judges at the lower probe); heat_map_out_of_domain still marks a recovery
    whose air is outside the heat's span, its heat the plane carried past
    the record. A fit's slopes take the sign the cycle gives them (the
    heat rising with the air and falling with the water, the power rising
    with the water; heat_pump_fit.HEAT_SLOPE_SIGNS and
    POWER_SLOPE_SIGNS): the plane is the least-squares one so
    constrained, the best of those with each slope free or pinned at 0
    that keep every sign. heat_pump_fit.violations checks a map over
    the controller's envelope (41 to 113 °F air, 50 to 149 °F water; its
    new air_f and water_f, and fit_heat_pump's, which both
    sim_calibrate and calibration.calibrate pass from the
    parameters' controller) rather than its
    domains, its COP against the ideal heat pump's between the
    evaporator's coil and the water (heat_pump.refrigerant_carnot_cop,
    the coil EVAPORATOR_APPROACH_F, 20.5 °F, under the air: as close
    as it ran in 90 % of 145,286 steady running minutes on the record).
    Against the coil as read, the site's map runs a steady 0.43 of that
    ideal and never over 0.46; the check is still 0.6. The site's tank is
    refitted with the maps so read (the bottom's loss 0.168 to 0.445
    BTU/h·°F, the condenser's span 0.143-0.351 to 0.148-0.323, 2.52 to
    2.50 °F RMS over the events), and its power map on that tank (408.9 W,
    +1.03 per °F of air, +2.70 per °F of water). Held out after
    2026-09-18, the 11 draw-free recoveries' duration error went from 6.9
    to 5.7 min and their electricity's from 5.0 % to 4.5 %, its bias from
    0.4 % high to 0.6 % low; the 16 with draws went from 47.2 to 48.2 min,
    and the lower probe after a draw stayed at 2.16 °F RMS.

  • The power is fitted where it is read. The walk reads the power's
    map at the condenser's water, which is below the lower probe the map
    was fitted on (4.7 °F on the record's climbs on average): read there,
    the map priced them at 407.2 W against the 418.2 W metered.
    sim_calibrate now reads each metered block at the condenser's
    water on the record's replay (heat_pump_fit.in_condenser_water,
    over tank_fit.condenser_water, one replay through the record: the
    water each step read the map at, after its draws and the start's
    stir, weighted by the hours it ran, stratified.Books' new
    compressor_h and compressor_water_f_h), and the power's plane
    is fitted there; power_blocks' new
    water_f is the water a block is read at, the lower probe's with
    no record to replay, and calibration.calibrate takes the
    evidence to fit on. The site's map is refitted so (408.6 W at 65
    °F air and 120 °F water, +1.04 per °F of air, +2.70 per °F of water,
    3,562 blocks, 2026-04-16 to 10-26, 12.9 W RMS); its air span is 53
    to 90 °F, the runs before 2026-04-16 having no record to replay. On
    the 11 held-out draw-free recoveries the electricity went from 2.7 %
    low to 0.4 % high on average and its mean absolute error from 4.5 %
    to 5.0 %. The heat's map is left on the mixed climbs: on the record's
    replay a draw-free recovery's layers gain 1.006 times the heat the
    map credits, and a refit on the probes through the replayed layers
    lengthened the held-out draw-free recoveries' error from 6.9 to 7.6
    min or more.

  • What a recovery gets wrong, documented.
    :doc:/explanation/simulator gives two errors in a heat-pump recovery
    that partly cancel. After 40 minutes or more of heating the upper and
    lower probes read about 0.5 and 0.9 °F higher than at rest, and the
    model's probes carry nothing for it, so the stop, judged against an
    overshoot measured on the reading, comes late. The power was read at
    the condenser's water: 392 W against the 417 W metered, pooled over
    the 11 held-out draw-free recoveries. Modelling both cut those
    recoveries' bias from 6.6 to 2.5 min but raised their mean absolute
    error from 11.9 to 12.7 min and the electricity's from 5.7 % to 9.9 %,
    so the offset is not applied; the power is now fitted at the
    condenser's water (above). :doc:/explanation/recovery-speed gives the
    probes' step at a quiet stop, and that the heat pump cools its room a
    little: the air rose 0.49 °F less on average over a run than over the
    same hours on nearby days with nothing running (90 % interval
    0.04-0.95 less).

  • Another installation's mains are the rating test's 58 °F
    (defaults.RATING_INLET_F, 10 CFR 430, Subpart B, Appendix E), in
    default_params, in place of one installation's September 72.4 °F:
    colder than most of the year's mains away from the northern tier, so
    its draws err the safe way. Give DrawParams.climate_f to place
    them by the day.

  • Hot water at the tap from the tank's layers (breaking).
    Deliverable gallons (delivery.deliverable), the delivered
    temperature along a walk (delivery_along), draw windows and
    readiness read the tank's layers (HeaterState.layers_f):
    the water leaving the top in turn, each gallon through the mixing
    valve, to the first the valve delivers under the line, the mains at
    the scenario's inlet. Without layers they read the profile the two
    probes stand for and say tank_profile_estimated.
    Deliverable.basis is "layers" or "probes".
    readiness takes inlet_f. time_to_ready walks the scenario
    in each mode, the mode written at the start, for up to a day
    (READY_HORIZON), to the first instant its layers deliver the
    bath, the energy the walk's to then, under the TOU windows the heater
    keeps; the heat pump's walk's reasons are the answer's, and an
    element mode's row carries what its walk adds to them. It raises
    ValueError for a setpoint written out of range, as simulate
    does. On 34 deliveries held out of the fit, none of
    which crossed the line, the replayed layers under-promised 3 % of
    them, by 1.17 gal on average, against 3 % and 0.91 gal from the
    probes alone. Of the three draws in the record that took the upper
    probe across the line, the layered tank as first fitted crossed the
    probe at 26.1 and 31.3 gal against the recorded 23.3 and 31.3 (the
    third started below the line).
    sim_score scores deliveries from the replayed layers.

  • The tank's keeper from Home Assistant is HomeAssistantTank
    (was HomeAssistantBook): it holds the replay's 24 hours
    (TANK_WINDOW) and no longer reads the inlet or the draw demand.
    nwp-cli simulate run reads it with read_tank and says when a
    scenario that does not start now walks from the two probes.

  • A history's steps are steps_from_history and
    steps_from_changes
    (were book_steps_from_*), on
    STEP_GRID_S (was BOOK_GRID_S). A draw is a flow over
    DrawParams.draw_gpm (0.3 gal/min).

  • A parameters document fitted with the heat book still reads. Its
    book part and its readiness evidence, delivered_lag_gal
    and fall_f_per_gal are named by unknown_fields and left out,
    its book's draw_gpm read as DrawParams.draw_gpm; book and
    readiness in its unfitted are dropped (RETIRED_TERMS).
    A result document holds unbounded gallons (a mains at or above the
    line) as null.

  • A recorded history's Step lives in nwp500.sim.history,
    with house_gpm, FlowVolume, AMBIENT_DEFAULT_F and the
    replay's span, REPLAY (from sim.tank_fit, which still imports
    them). A step says when it starts (Step.at), which
    steps_from_history and steps_from_changes fill.

  • Calibration fits the layered tank (breaking).
    sim_calibrate.calibrate and calibrate_changes fit the tank's
    terms (tank_fit.TERMS: the sides', bottom's and top's losses, the
    column's conductance, each probe's height and spread, the lower
    probe's offset, the inlet's mixing) on the record's quiet windows and,
    given a meter's draws, its draws, with where the condenser heats
    (CONDENSER_TERMS) on 5 recoveries or more; then the heat pump, its
    heat priced against the fitted tank's loss. The tank needs 10 windows
    and draws (MIN_TANK_EVENTS) and the air. Calibrated.tank
    (TankCalibration) says what its fit saw, and map_ spreads its
    trials over processes (nwp-cli simulate calibrate uses one per
    core). keep_better judges the tank as one group, tank, on the
    latest fifth of its events (tank_fit.event_rmse), in place of
    standby and gap_tau, and HeldOut holds those events
    (tank, record). The report's tank rows replace the loss law's
    and the gap's. HeaterParams.unfitted and the reasons name
    tank (tank_uncalibrated) in place of standby and
    gap_tau (standby_uncalibrated); a document of the two-zone
    tank's reads with tank unfitted, its tank the priors'.
    sim.calibration.calibrate fits the heat pump alone, and
    Calibration has no standby. heat_pump_fit.climb_heat and
    evidence take the HeaterParams whose tank prices the loss.

    The defaults' tank is that fit on one NWP500-65's record, 2026-04-16
    to 2026-10-26 (266 quiet windows, 254 metered draws, 118 recoveries):
    the sides 2.89, the bottom 0.89 and the top 0.68 BTU/hr per °F, the
    probes at 0.890 and 0.478 of the volume. Calibrated on the record to
    2026-09-18 and scored after it, a draw-free recovery's duration is
    out a mean 11.8 min (7.3-16.6; the two zones 7.9), its electricity
    5.8 % (7.2 %), a coast's upper probe 0.255 °F (0.299) and its lower
    0.548 °F (0.630), the probes after a draw 0.290 and 2.09 °F (0.281
    and 2.16). Started from the priors, the fit went from 6.92 to 2.68 °F
    RMS in 25 trials and scored within each of those intervals. The
    record does not split the bottom's loss from the sides' well: the
    bottom came out 0.89 to 2.35 across the fits. Refitted against the
    layered loss, the heat map moves 0.05 % at 65 °F air and 120 °F
    water and under 0.2 % across its span, and the shipped one stays.

  • The simulator walks the layered tank (breaking).
    tank_physics.TankPhysics is the stratified tank
    (physics.stratified) its parameters lay out, and the engine walks
    it through everything at once: Physics.advance takes the tank from
    one instant to another with what is running (Running: the
    compressor from its start, heating after its hold; the elements, their
    heat on its way for their dead time after they stop; credited none of
    it inside a withheld draw window), taking each draw at its instant and
    the circulation's runs as water through the tank, and stops where the
    engine asks. A recovery runs until the lower probe reaches its stop;
    the engine no longer places a recovery's integrated climb on a draw's
    response. Elements put their electricity into the water: the two
    zones had credited them 1.81 and 1.24 times it. A draw takes its
    water's heat, and 100 gal in an hour now empties the top.

    A RecoveryModel's duration is served as the pace of the heat
    pump's heat for that recovery (heat_pump_recovery.rate_scale,
    corrected once on a walk at that pace), so its path is the layers'
    at the model's speed. RecoveryContext carries the layers
    (layers_f) and has no draw_load; PhysicsRecovery has no
    path. TankPhysics.recovery is the physics' climb,
    recover the served one with its trajectory
    (heating_trajectory.from_walk), scale the pace alone and
    layers a state of given layers. forecast and active_cycle
    walk the layers too; a recovery short of its target after
    RECOVERY_BOUND (16 h) is refused, and simulate runs a heater
    that cannot reach its target on, as a real one does.

    Held out from 2026-09-18 on one NWP500-65, through simulate and
    standby, each event started from the layers its history left and
    the layered terms fitted on the days before: a draw-free recovery's
    duration is out a mean 12.3 min (7.6-17.3; the two zones 7.9), a mean
    7.2 min long, its electricity 5.8 % (7.2 %), one with draws in it
    47.0 min (43.6); a coast's upper probe 0.261 °F (0.299) and its lower
    0.552 °F (0.630); the probes after a draw 0.289 and 2.078 °F (0.281
    and 2.161). Started from the two probes alone, a draw-free recovery is
    out 15 min.

    The golden walks moved: a coast holds its upper probe longer, so the
    TOU opening's start comes later; an element runs until its own heat
    brings its zone to the off point, longer than the two zones'
    credited heat did, and Electric's elements run upper then lower; a
    shower's draws take the lower probe further, so a start inside the
    TOU window waits for its close, and a 26 gal draw at a window's close
    starts a recovery.

  • The engine carries the tank as its physics' state (breaking for
    a custom Physics). Physics gains estimate (the state two
    probe readings stand for) and observe (the probes on a state),
    and its advance and draw take and give a state
    (TankState) in place of the two zones' temperatures; each
    Point carries the state it reached. The engine reads the tank
    only through observe.

  • The heat pump is a performance map (breaking). The heat into the
    tank and the electricity drawn are each a plane in the air at the
    heater and the lower tank's water (CompressorParams.heat_btu_h,
    power_w and their slopes), fitted on the heater's own recoveries
    and each read inside the span it was fitted over
    (heat_domain_*, power_domain_*). The condenser gives what
    the evaporator takes plus the electricity, by construction; a fit
    claiming a COP over 0.6 of Carnot's, heat given to the air, or under
    50 W anywhere in that span is not applied. It replaces the R-134a
    cycle: physics.refrigerant, the compressor constant, the
    evaporator's approach and slope, the condenser approach, clearance,
    exponent, airflow and latent load, and two_zone. Held out from
    2026-09-18 on one NWP500-65, a draw-free recovery's duration is out a
    mean 7.9 min (13.6 before) and its electricity 7.2 % (12.8 % before,
    a mean 8.7 % low; now 1.5 % low). See
    :doc:/explanation/simulator-methodology.

  • A recovery holds at its start and runs on past its target. At
    the instant the condenser's heating begins the walk stirs the water
    it wraps with the cold water below it (physics.stratified.stir,
    by Layout.stir_weights), the same heat spread over the pool, and
    the lower probe holds until the condenser has heated the pool back
    past its reading: longer the longer the compressor was off and the
    colder the bottom has grown, as the record's holds are (0 to 22 min),
    with nothing fitted to them. A compressor already running at the
    walk's start is not stirred again (in simulate, active_cycle
    and forecast) unless some of a running_hold is left or
    simulate's new running_starts says it starts there (a
    recorded start replayed, as sim_score does), nor one that stops
    before its heating begins. forecast keeps a running compressor
    going, ready early, where stopping it and starting again later (a
    start, stirred) could not be ready by the target. On the record's 84 clean starts the
    model holds a median 3, 23, 18, 18 and 21 min after under 15, 60,
    180 and 720 min off and beyond, against 0, 5, 10, 18 and 22; the
    upper probe holds still through the record's holds, as the model's
    does. CompressorParams.start_hold_min imposes no hold by default
    (it was 0, 5, 10, 18 and 22 min, credited no heat, and cost a start
    after a day off 165 Wh and 3 °F of tank); a caller's start_hold
    or running_hold still applies, and simulate uses it with or
    without a RecoveryModel. Fitted and scored on the record
    (2026-04-16 to 10-26) with the stir and no hold, against the model
    with the hold: the held-out draw-free recoveries' mean absolute error
    11.9 to 8.7 min and their electricity's 5.7 to 4.4 %, the standby
    probes' RMS 0.254/0.564 to 0.202/0.534 °F, the draws' lower probe
    2.08 to 2.28 °F, the recoveries with draws unchanged. The climb
    runs to ControllerParams.hp_stop_overshoot_f (0.9 °F) past the
    target in simulate and active_cycle, and in forecast to
    the heater's own threshold (not a partial charge or a TOU cap's
    level); a RecoveryModel is asked with that stop as its
    target_f. Recorded durations already ran from start to stop, so the old
    compressor constant had absorbed both.

  • Parameters are heater-params/2 (breaking). A
    heater-params/1 document is refused: calibrate again with
    nwp-cli simulate calibrate.

  • sim_calibrate's groups are heat_map, power_map and
    run_on in place of compressor, power_scale and cop, each refitted and judged on the latest
    fifth with keep_better. Calibration.heat_pump
    (heat_pump_fit.HeatPumpFit) replaces compressor and power.

  • A recovery's trajectory and COP are the map's. The electricity is
    the map's power over the run; the COP is the map's heat over it.

  • element_mode(scenario, params, *, mode, setpoint_f=None, ready_upper_f=None, switch_back_after_minutes=None, slot_minutes=10.0, recovery=None) walks the scenario with the mode written at its
    start, as time_to_ready walks each mode: to the end of the
    recovery the write sets off (every element and the heat pump stopped)
    within READY_HORIZON, or to a switch-back. It takes a Scenario in place of
    state, settings and at, and no hp_lower_minutes: the
    heat pump heats the layers in the same walk. ElementModeResult
    carries the walk's times, the elements' minutes and kWh, the heat
    pump's kWh (heat_pump_kwh), a profile of ElementInterval
    slots with both, the tank at the end with its layers, and
    reasons; plan is gone. A recovery part of which a later mode
    write or the heater's drop at a window stops, or whose compressor a
    setpoint write stops short, has no full_minutes
    (recovery_cut_short), and a write of the mode already in force
    says mode_already_in_force. From 125/110 °F at a 140.9 °F
    setpoint the High Demand and Energy Saver recoveries end 127 and 128
    minutes out, with the heat pump's, which the element model left
    unknown; Electric's in 45.

  • calibration.fit_element_rates and ElementRateFit are
    fit_element_w and ElementPowerFit, the element's power alone;
    ElementLagFit.upper_f_per_min is rise_f_per_min, a
    measurement no parameter reads.

Fixed

  • The record's replay (tank_fit) heated every element minute in the
    upper element's layer, the lower element's too: TankRecord now
    holds the lower element's runs (lower_element, from a frame's
    lower_element_on, which the history frames and Step now
    carry), and the replay heats those in the lower element's layer. On
    one installation (2026-04-16 to 10-26) the lower element ran 11 times,
    62.8 min; the record's 24 element runs, each replayed from the layers
    at its start, read the upper probe to 4.4 degF RMS against 8.2 and the
    lower to 6.8 against 8.7. The recoveries' energy before 2026-09-18
    comes 20.1 % off against 20.9; nothing held out moved.

  • sim_score splits every draw the detector times that is under way
    at an event's start, where it split only one at
    DrawParams.undiluted_gpm or faster. A slower one was replayed
    whole at its onset, the start's probes laid over the replay, and the
    event's walk did not take the rest: a recovery starting 89 s into a
    10.9 gal draw was walked without 9 gal of it. The rest is now all at
    the start, as the score walks a slower draw at its onset. Measured
    2026-11-02 on the parameters fitted before 2026-09-18: of the 116
    recoveries 2026-04-17 to 10-26, 44 had a slower one under way. Held
    out, the 16 with water in use came out 18.3 min off (12.3-25.3)
    against 21.2, a mean 1.7 short against 13.1; before 2026-09-18 the 79
    came out 27.7 min off (21.5-35.3) against 25.3, a mean 25.2 long
    against 9.2. The draw-free and the electricity did not move, nor the
    days: no local midnight of the record falls inside a timed draw.

  • A recorded recovery is walked with each change of the setpoint inside
    it as a write at its instant (sim_score.Recovery.setpoints), where
    it was walked to the setpoint in force at its start; a restart a write
    causes before the recorded end continues the scored run, as the heater
    ran on through it. One raised from 110.3 to 141.8 °F 3.5 min in had
    been scored 284 min short; on the site's held-out record, the 16
    recoveries with water in use came out 31.4 min off against 42.5.

  • The 50 and 80 gal defaults resize the 65's tank to theirs
    (defaults.sized, defaults.tank_column_in), where they scaled
    only the volume. The manual's outside dimensions give each model's
    water column: the 50 is narrower than the 65 and as tall, the 80 as
    wide and taller. The sides' and bottom's loss, the axial conductance,
    the probes' spreads and the inlet zone follow the walls and the
    cross-section. The tanks lose 3.55 and 4.74 BTU/h·°F (each was
    4.12). The 50's first draw from a tank mixed at 125 °F to a 110 °F
    line is 37.5 gal (34.3 before), 14.8 under the 65's against the
    ratings' 15 gal of first-hour rating. The 80's is unchanged at 65.2
    gal: 12.9 over the 65's, where the ratings differ by 5.

  • The inlet zone is the same at every flow, and the methodology says
    why: the record's draws of 4 gal or more all ran at 0.7-1.3 gal/min,
    and with the gallons held the flow moves the lower probe's error
    within about two standard errors. The measurement is in
    :doc:/explanation/simulator-tried.

  • :doc:/how-to/calibrate-simulator says how a change to the
    simulator is scored: the record's pinned span and split, the site's
    installation (in code), and the code before the change scored the
    same way beside it. :doc:/explanation/simulator's accuracy says
    when it was measured. :doc:/explanation/simulator-tried adds the
    circulation's earlier return forms, the upper element at 0.79, the
    heat map fitted on the model's probes, the condenser's span fitted
    on recoveries alone, and events started from the two probes.

  • The new :doc:/explanation/simulator-tried page lists the simulator's changes
    tried against the record and not kept, each with what it found and
    where it is written up. The methodology says the layer count is part
    of the fitted model: with the 65's terms held, 6 to 96 layers move a
    recovery from 253 to 281 min and back, and 24 layers, refitted,
    scored no better than 12 on the held-out record (recoveries 38.2 min
    off against 37.7, both from the defaults' installation) and walk
    1.55 times as long. A test holds the
    default at 12.

  • The methodology says why the air is an input and not a state: the
    heat pump takes about 3,000 BTU/h from its room, which cooled 0.49 °F
    on average over a run at one installation; at the heat map's air
    slope that costs 0.6 % of the heat into the tank. A replay's air
    carries it, and a forecast's as its anchor does.

  • recovery-speed says why the heat map stops at 61 °F of air: the
    winter's 48,000 compressor minutes at 45-60 °F have no draw detector
    behind them, and the heater's own flag, the only stand-in, reads the
    heat 14 % high and its rise with the air a third flat on the months
    the detector covers. The record shows no frost either.

  • The methodology's "The air" said a recovery runs in its span's mean
    air; every step reads the air at its start, coast and recovery alike.
    The mean judges only the heat map's domain and is a learned
    duration's context.

  • A mark changes nothing. A mark, a resume cut or any event the walk
    stops at read the tank there by the controller's rules, and some of
    those rules read it otherwise along the path, so a mark moved 83 of
    600 random walks' heating. Each now reads the tank the same way:

    • The upper element does not start where it would stop at once:
      Energy Saver's 104.9 °F later start is over its off point, 1.62 °F
      under a setpoint below 106.6 °F, so at an event it started and
      stopped in the same instant and on the path never. There it starts
      as High Demand's does, 4.1 °F short of the setpoint, as Electric's
      does (controller.later_on); neither is measured there.
    • The compressor's lower trigger reads no nearer than the setpoint,
      hp_stop_overshoot_f short of where a recovery stops: over it,
      a recovery started at each event and stopped at once
      (lower_trigger_at_setpoint). The heater's own threshold is
      104.9 °F on the record for setpoints of 106.7 to 149 °F.
    • A recovery stopped short of the trigger (by a write, say) is read
      from just after its stop and starts again after the lockout. A
      coast from the stop started nothing to its end, and one from a
      later event did.
    • A start a coast's look-ahead finds past the upper element's own
      start is not kept as a pending start: the element changes the tank
      first. It fired at a TOU window's opening whatever the tank did.
    • The heat pump back in its range with too little left for it, the
      elements finish the recovery as judged where it came back
      (controller.Element.finishes), through the upper element's
      handover to the lower, not again at each event; a setpoint or mode
      write calls for heat anew and has it judged again.

    On 1,500 random walks, hourly marks now move no start or stop by a
    second or more (a crossing is interpolated a fraction of a second
    apart), and marks every 20 min one start in 300 by a minute, where a
    draw lands each minute and which of them crossed the trigger is read
    to the step. Marks off the minute grid split a walk step, which moves
    a slow crossing by up to 35 s.

  • A circulation known part way is known wherever a walk is cut. A
    span starting after Loads.circulation_known_until charged
    nothing, judged for each walk, so a run past it was charged by a walk
    begun before it and dropped by one begun after: a write that changed
    nothing, a draw or a mark inside a recovery took it away. With runs
    at 0.2 and 1 h known to 0.5 h, a write of the same setpoint at 0.7 h
    left the tank 3 °F warmer at 3 h. Which runs are known is now judged
    for each run: one starting by known_until is charged whole, one
    after it is not, and the answer says circulation_unknown as
    before. Circulation.over returns the known runs, no longer None.

  • A recovery from a cold tank says its heat is carried past the
    record.
    The heat's map was fitted on mixed climbs against the
    probes' mean, 114 to 144 °F on the site's record, and is read along
    its plane below that, but heat_map_out_of_domain judged only the
    air. A 58 °F tank at 67.5 °F air heats at a COP of 6.35 over its
    first hour and 4.45 to a 126 °F lower probe, its probes' mean 93 °F
    over its heating, and said nothing. A recovery whose probes' mean
    over its heating is outside the heat's water span now says
    heat_map_out_of_domain too, in simulate and in the recovery
    forecasts (forecast, active_cycle), as its mean air is judged
    against the air's (stratified.Books' new compressor_heat_h,
    the hours the condenser's heat went in, and compressor_mean_f_h).
    simulate judges it over the whole run where the compressor stops,
    not each stretch an event cuts it into, and a walk going on from one
    carries the run's heating so far (Resume); a walk that ends inside
    a run judges what it heated.
    From the heater's 104.9 °F lower trigger, at 67.5 °F air, a recovery
    says so at setpoints of 117 °F and under and not at 118 °F and over.
    The condenser's water the map is read at runs under the mean in a
    layered tank (from that trigger to a 130 °F setpoint it starts at
    102.1 °F and averages 114.3 °F, the probes' mean 121.9 °F), and is
    not what is judged. The map is not changed: what the heat pump does
    with water that cold is not on the record.

  • A draw runs over its time. A draw was taken all at its instant,
    and draw_forecast.forecast_loads dropped each expected draw's
    time drawing, so a forecast shower emptied the top of the tank at its
    onset. Draw takes duration_s: the walk takes it at its onset
    and at each whole minute before its end, each piece the gallons of
    its span, which walks exactly as a meter's per-minute draws, and
    forecast_loads (and draws_first, whose draws carry their
    seconds) gives each expected draw its own. A 60 gal shower over
    30 minutes from a full tank at a 130 °F setpoint now starts the
    compressor 12 minutes later than at once, and High Demand's upper
    element 17 minutes later. A draw with no duration is taken at its
    instant, as before. next_recovery and time_to_ready, whose
    recovery forecast takes no draws, take the rest of a draw under way
    at the recovery's start there, a recovery already running at the
    call included; that one left out a draw at its start before, one
    at an instant too.

  • Electric heats again after its first run. A walk in Electric
    heated only on entering the mode, so a tank left in it coasted to the
    room and drew nothing (element_later_starts_not_modelled). Its
    later starts are not on the record, so they follow the elements in the
    heat pump's place: the heater's own triggers start them by Electric's
    entry rule, the upper element then the lower, from the tank a draw at
    that instant leaves, and the upper element starts again 4.1 °F short
    of the setpoint, as High Demand's does (controller.later_start).
    Each walk that uses them says electric_later_starts_unverified,
    and so does one going on from such a run. Two days in Electric at a
    130 °F setpoint from 128 °F now keep the upper probe between 124.9
    and 132.05 °F, for 4.54 kWh with no draws and 13.55 kWh with a
    household's, where
    before they heated nothing and ended at 104.6 and 72.9 °F. Across 800
    random walks on one NWP500-65's parameters, 427 with Electric in
    force at some point change: 93 only in their reasons, 319 heat more
    (a median +8.6 kWh), and 15 less, by up to 6.5 kWh, where the tank
    was warmer when a later mode took over or the elements in the heat
    pump's place answered a low setpoint less often (below).

  • The elements answer the lower trigger no nearer than 4.1 °F under
    the setpoint.
    They stop at the setpoint, not past it as the
    compressor does, so with hp_lower_on_f at or near the setpoint
    the trigger started them again at once: in the heat pump's place,
    about 110 starts in two days at a 104 °F setpoint and a 104.9 °F
    threshold, most of them seconds long. They now answer it at the
    upper element's restart when that is lower (12 starts), and
    standby reads it there too, in Electric or with the heat pump
    outside its range where the trigger would fire, its
    lower_margin_f with it (controller.lower_trigger_f). Of the 800
    walks, 37 that never enter Electric change: 26 heat less and 10 more,
    the energy moving −7.5 to +0.5 kWh (a median −0.15) and the end tank
    up to 6.3 °F.

  • The upper element's later start is found on a recovery's path.
    High Demand's start of the upper element when the upper probe falls
    short, and Energy Saver's, were found along a coast but only read at
    the walk's events during a recovery, so a mark or a call's end moved
    them. Across 800 random walks on one NWP500-65's parameters, 66
    change: the element starts 0.2–60 min earlier in most and up to
    650 min in a few, starts in 6 that had none, and the energy moves
    −259 to +1249 Wh and the tank at the end up to 6.1 °F.

  • An element in the heat pump's place runs to the horizon. The air
    read past the walk's end ended its window there, so the element
    stopped as heat_pump, often with a compressor interval of no
    length after it; it now stops as horizon. 44 of the 800 walks
    change; the tank does not.

  • TOU windows past the end. A scenario's windows were laid out to
    its end, so a recovery running there was judged without a window
    opening after it; they now reach as far past the end as a recovery
    judges its cap (engine.RECOVERY_AHEAD, 16 h).

  • tou_cap_projected is said where the cap stops a recovery, in the
    walk it stops in. It was said where a recovery's cap was projected,
    so a walk ending before the cap, or one whose projection a draw
    undid, said it of a recovery the cap never stopped.

  • A start TOU defers past the walk's end stays pending, for a walk
    going on from there; controller.TriggerDeferred has no
    horizon. No single walk changes.

  • sim_score replays every draw before an event's start, and leaves
    a delivery's own draw out. The replay an event starts from stops at
    the start of the minute the event starts in.

    • A delivery: the meter's delivery starts and the detector's draw
      onsets run on different clocks, the onset 5–10 s earlier and, in 3
      of 34 held-out deliveries, in the minute before, so the replay took
      the delivery's own draw from the tank it then scored. A draw whose
      onset falls in the settle before a delivery
      (calibration.DRAW_SETTLE_S, when the meter saw no flow) is now
      left out.
    • A recovery: it starts at a compressor edge, any second of a
      minute, and keeps the draws from its start, so a draw earlier in
      that minute reached neither its replay nor its walk. It is now
      replayed at that minute's start, then corrected to the probes the
      recovery starts from, as one a minute earlier is.
    • A quiet window: a draw in its first minute, before its start,
      was dropped. It is now replayed the same way.

    No held-out metric moves on this heater's record.

  • In Electric, an upper element a lowered setpoint stops at its new off
    point hands over to the lower element, as one stopping at its own
    does; the lower zone was left where it was. Not when a mode write at
    the same instant changes the mode at once: the new mode's rule
    decides; a mode write held to a TOU window's end decides nothing
    then.

  • A setpoint raise in Energy Saver written with a mode that a TOU window
    holds to its end starts the upper element, as a raise alone does: the
    held mode leaves Energy Saver in force. It started nothing until the
    window closed.

  • An element heating in the heat pump's place stops with
    HeatingInterval.stop heat_pump when the heat pump takes the
    recovery back, not mode, so element_mode does not count that
    handover as cutting its recovery short.

  • An element stops where its zone reaches its off point. Its end
    was set from its fitted rate alone, so where something else lifted the
    zone too, it ran on at the setpoint, charged and lifting nothing. With
    the compressor running, High Demand's lower element ran 7 min at
    122 °F. An inverted tank, its lower zone above the upper, mixes in the
    coast's first step: the upper element, on at 110 °F over a 135 °F
    lower zone, ran 6 min at the setpoint, and a write or a boundary
    mid-run moved its end. The walk now stops it where the zone reaches
    its off point, taking an inverted tank's mix at once, with a book
    attached as without.

  • next_recovery takes the draw that trips the trigger. A draw
    at the heater's own start was left out of both the tank the recovery
    starts from and coast_draw_gal; one a second earlier was smeared
    across its coast step. The tank is now the one the draw leaves, from
    a coast run exactly to the start, and the draw's gallons are the
    coast's. recovery_as_of takes them off a copy kept past its start.

Removed

  • calibration.fit_displaced_gal, DISPLACED_BOUNDS and
    DISPLACED_GRID_POINTS: the pipe's volume is measured
    (measure_pipe), not fitted.
  • The element-mode planner: physics.element.mode_recovery,
    ModeRecovery, mode_plan, ModePlan, coast_f,
    upper_rise_f and MAX_PROFILE_SLOTS (api.MAX_PROFILE_SLOTS
    caps element_mode's profile); ElementParams.upper_f_per_min
    and lower_f_per_min, which a document still carrying them is
    read without (unknown_fields names them); the reasons
    energy_saver_later_starts_not_modelled,
    upper_zone_coasts_short_of_the_setpoint, hp_lower_not_given,
    lower_zone_not_projected_under_heat_pump,
    switch_back_lands_before_the_recovery_finishes and
    plan_ends_when_the_elements_stop_not_at_switch_back.
  • The heat book: sim.book, BookParams, Booked,
    full_tank_book, HeaterParams.book and book_terms,
    SimulationResult.booked, and the book arguments of
    simulate, readiness and time_to_ready;
    sim_adapters.book_from_history, book_from_changes and
    full_tank_book; sim_home_assistant.BookAnswer,
    BOOK_WINDOW and HomeAssistantBook.book;
    sim_calibrate.fit_book, BookTermsFit, MIN_BOOK_PAIRS and
    MIN_RUN_PAIRS; calibration's book, zone-mix, empty-point and
    evidence fits; cold_supply.on_the_mains;
    circulation.CirculationLoss; CirculationParams.loss_btu_per_gal
    and book_circuit_gal; Recovery.coast_btu, coast_draw_gal
    and hp_btu_per_h; the reasons no_heat_book,
    heat_book_aged, book_uncalibrated,
    readiness_uncalibrated, upper_probe_stand_in,
    capped_at_observed_volume and
    bath_ready_within_estimate_error.
  • The readiness evidence and the outlet lag: ReadinessParams.evidence,
    delivered_lag_gal and fall_f_per_gal;
    delivery.EXAMPLE_EVIDENCE, with_example_evidence,
    probes_gal, booked_deliverable, zone_delivery_f,
    demonstrated_gal, observed_max_gal and
    Deliverable.at_observed_max. The water above the upper probe is
    the layered tank's own.
  • The two-zone tank: physics.tank and physics.compressor;
    TankParams.lower_zone_share and stratification_gap_tau_h;
    LossParams.ua_btu_hr_f, ua_exponent, ua_ref_delta_t_f,
    ua_baseline_btu_hr_f, bottom_loss_btu_hr and
    bottom_gate_f (ua_side_btu_hr_f is the sides' own, 5.0
    BTU/hr per °F unfitted); params.UA_REF_DELTA_T_F,
    UA_MIN_DELTA_T_F and TAU_GAP_PRIOR_H.
  • The two zones' draw response: DrawParams.observability,
    reach_gate_f, reach_offset_f, reach_gal,
    upper_f_per_gal, lower_gate_f, pool_f_per_hour and
    ungated_observability; physics.draw.probes_after_draw,
    lower_zone_drop_f, draw_energy_btu, LEGACY_TMV_DELIVERY_F
    and the DEFAULT_ constants of those terms and of the draws'
    range.
  • The standby fits: calibration.fit_standby, StandbyFit,
    StandbyWindow, fit_window, fit_ua_law, fit_loss,
    LossFit, loss_windows, standby_ends, standby_error,
    with_standby, fit_gap_tau, gap_paths, gap_error,
    walk_tau, circulation_windows, CirculationWindow,
    fit_ungated_observability and their bounds and thresholds;
    calibrate's ua_exponent.
  • decay.project_decay, DecayProjection, Draw,
    draw_step and partitions.
  • simulate's ledger, lead_draws and physics_usable;
    History.heated_at; the reason tau_gap_prior; and the refusal
    of draws on parameters with no fitted draw response
    (draw_model_unsupported): the layers take any draw.
  • extensions.HeatLedger and its value types (Stand,
    LedgerZones, ZoneBound, DrawTaken, AfterDraw,
    DrawsAhead, UpperAhead); book.BookedHeatLedger,
    zone_after_draw_f, zone_mixed_f, zone_cannot_move and
    FALL_F_PER_GAL; walk_booked's carry and keep,
    WalkCarry and BookedPath.carry.
  • engine.DRAW_LEAD; TankPhysics' coast, heat,
    TwoZone, Coast, Heat, FlowingDraws, tau_gap_hours
    and physics_usable; REFINE_TOL_MINUTES;
    heat_pump_recovery.physics_recovery, recovery and
    Recovery; heating_trajectory.build_heating_trajectory;
    circulation.CirculationDraws and run_draws.
  • PowerParams and HeaterParams.power (power_scale,
    cop_scale, startup_wh, cop_lift_coeff);
    Forecast.startup_energy_wh and Recovery.startup_energy_wh;
    the startup_wh arguments of the engine; evaporator_f of
    active_cycle and sim_adapters.evaporator_f.
  • calibration.fit_compressor, CompressorFit,
    recovery_minutes, split_recoveries, with_compressor,
    PowerFit, fit_power, with_power, power_cycles,
    cop_cycles, CopCycle, path_to_end and the COP solvers;
    physics.compressor.predict_physics, heating_trajectory.path_cop
    and heat_pump_recovery.DEFAULT_HUMIDITY.