Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HW-0010 — Hot-water supply temperature tracking failure

Statusverified — engine e2ff2f8, cxf:fnv1a128:d680c1142ee5617cde72f0dbcc028046, 2026-08-20
Severity3
Methodrule
Phase2
CategoryCOMFORT_ENERGY
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed by
RelatedHW-0002, HW-0004, HW-0007, HW-0008, HW-0009, HW-0011, FPB-0006
Playbookshot-water-plant-faults
SourceNIST, Automatically Detecting Faulty Regulation in HVAC Controls (2013), pp. 412 and 416-419 — regulated-variable allowance bands, transient exclusions, and field-tuned alarm parameters; LBNL Simulated Boiler Plant dataset inventory, PDF pp.4-8 — hot-water loop supply temperature/setpoint/status channels and separate sensor-bias, fouling, and poor-PI fault cases; the dataset is a future replay target, not a completed validation claim; Library precedent CHW-0007 — verified strict mirrored hydronic tracking-error topology and continuous 900 s persistence
Operating statesnormal automatic hot-water operation after startup, with at least one boiler proven firing and distribution circulation established

Preconditions (host-enforced): hws_temp and hws_temp_sp must describe the same controlled outlet or common header and final active target. A common header plus an OR of firing statuses is valid only when all points belong to the same configured plant; it is not an individual-boiler outlet comparison. Exclude warm-up and setback recovery, setpoint/reset ramps, stage or pump changes, minimum-flow transitions, tuning tests, and intentional demand, high-limit, fuel, flame-safeguard, freeze, emissions, or other capacity limits until the plant has settled. boiler_status must represent firing rather than enable, and hw_pump_status must establish distribution circulation. Temperature, status, and setpoint signals must be fresh, aligned, calibrated, and in the declared units. When any obligation is unmet the verdict is NO_EVAL, not healthy.

Points: hws_temp, hws_temp_sp, boiler_status, hw_pump_status

Outputs:

  • yFault — True after the active plant remains more than tracking_error above or below its final HWS target continuously for sustained_duration
  • yTooCold — Immediate direction flag; true while the active plant is more than tracking_error below setpoint
  • yTooHot — Immediate direction flag; true while the active plant is more than tracking_error above setpoint

Parameters:

NameDefaultUnitCXF pathDescription
tracking_error2.0KtooHot.t, tooCold.tADOPTED_TUNABLE symmetric settled tracking allowance. Commission above combined measurement error, setpoint resolution, and the controller’s normal deadband; exact equality is clear.
sustained_duration900.0spersist.delayTimeLIBRARY_PRECEDENT from CHW-0007’s identical hydronic tracking form. Confirm it exceeds ordinary plant response after every excluded transition.

Description

This rule reports a firing, circulating hot-water plant that cannot hold the same target its supply-temperature point is meant to control. The direction is diagnostic, not causal: cold water may reflect capacity, flow, fouling, staging, or an intentional limit the host failed to exclude; hot water may reflect overshoot, aggressive staging, a bad target, sensor bias, or the wrong header.

Detection Logic

error       = hws_temp - hws_temp_sp
too_hot     = error > tracking_error
too_cold    = -error > tracking_error
plant_active = boiler_status AND hw_pump_status

yTooHot  = plant_active AND too_hot
yTooCold = plant_active AND too_cold
yFault   = TrueDelay(yTooHot OR yTooCold, sustained_duration)

Block graph (rule.cxf.jsonld):

HW-0010 block graph

Both comparisons are strict, so exactly +/-2 K is clear. The single delay is after their OR: a directly sampled hot-to-cold jump without an in-band tick preserves persistence, while an in-band, boiler-off, or pump-off tick resets it. delayOnInit=true requires the complete interval after evaluator startup.

Possible Diagnoses

  1. Boiler capacity, fuel input, heat exchanger, or minimum-flow limitation.
  2. Poor temperature-loop tuning, excessive integral action, or plant delay.
  3. Stage command, firing proof, distribution pump, or control-valve problem.
  4. Active setpoint not reaching the local boiler or mixing controller.
  5. Temperature sensor bias, poor placement, stale delivery, or wrong header.
  6. A real demand, reset, safety, emissions, or high-limit condition omitted from the host gate.

Energy Impact

The finding is qualitative. Sustained over-temperature can increase pipe loss and keep a condensing plant above its efficient return-temperature region; under-temperature can increase terminal/pump effort or shift load to other heat. The graph has no fuel or delivered-load model, so it does not invent savings.

Emissions Impact

Any scope-1 effect follows the change in boiler fuel use and cannot be inferred from error direction alone. Quantification requires measured fuel or a validated load-and-efficiency model; this rule reports no generic emissions reduction.

Deviations

  • This is a library-authored application of NIST regulation concepts and the CHW-0007 graph, not a source-transcribed boiler rule.
  • The shipped 2 K allowance is adopted and tunable; neither NIST nor LBNL publishes it as a portable boiler threshold.
  • The LBNL dataset is cited for point/fault coverage only. No LBNL replay was available locally for this slice, so no LBNL-derived TPR or FPR claim is recorded; the frontmatter separately records limited healthy EnergyPlus FPR evidence.
  • Confidence is MEDIUM rather than the brief’s proposed HIGH because common- header topology, sensor bias, and intentional plant limits remain material confounders even with the stated host gates.
  • Direction flags include the active-plant gate. When the plant stops, they and yFault clear immediately; the host must report NO_EVAL outside the stated operating state.

Notes

Read HW-0009 when command and proof disagree, HW-0008 when the target itself is not resetting, and HW-0002/HW-0004 when temperature tracking coexists with efficiency or low-delta-T evidence. These rules may co-occur and do not suppress one another.

Test Vectors

19 scenarios, clock step 60 s over 3600 s.

ScenarioDescription
inactive_plant_with_errorA stopped plant is outside the rule’s operating premise even with a large temperature error.
active_on_setpointA firing boiler and running pump holding the active target are healthy.
too_cold_at_initializationA standing cold-side violation exposes direction immediately: yFault is clear one tick before 900 s, asserts exactly at 900 s, and remains true after the initialization-safe delay.
too_hot_at_initializationThe mirror hot-side violation uses the same continuous persistence.
hot_error_exact_boundary_clearExactly +2 K is clear under the strict comparator.
hot_error_just_beyond_boundaryOne hundredth kelvin above the hot boundary follows the full delay.
cold_error_exact_boundary_clearExactly -2 K is clear under the mirrored strict comparator.
cold_error_just_beyond_boundaryOne hundredth kelvin past the cold boundary follows the full delay.
boiler_only_is_inactiveBoiler firing without established distribution-pump proof gates off both directions.
pump_only_is_inactivePump circulation without a firing heat source is not tracking evidence for this rule.
violation_ends_one_tick_before_persistenceThe cold error returns to the band one evaluator step before 900 s and never matures.
mature_alarm_recoversA mature hot-side alarm and its diagnostic clear immediately after temperature recovers.
two_short_episodes_do_not_accumulateTwo cold episodes separated by an in-band interval each start a fresh timer.
pump_proof_drop_resets_timerLoss and restoration of pump proof gives the continuing temperature error a fresh 900-second clock.
boiler_proof_drop_resets_timerA firing-status interruption independently resets persistence.
direct_direction_handoff_preserves_timerA sampled hot-to-cold handoff never re-enters the band, so the shared out-of-band timer continues while diagnostics swap.
setpoint_step_settles_before_delayA reset step creates raw cold error, but temperature catches the new target before persistence.
temperature_threshold_chatter_resets_delayAlternating just inside and outside the hot boundary never accumulates continuous persistence.
common_header_binding_is_host_excludedThe graph alarms on a coherent raw tuple even if this header is not the firing boiler’s controlled target; topology proof is a mandatory host gate.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 3600
  },
  "scenarios": [
    {
      "name": "inactive_plant_with_error",
      "description": "A stopped plant is outside the rule's operating premise even with a large temperature error.",
      "inputs": {
        "hws_temp": 50.0,
        "hws_temp_sp": 60.0,
        "boiler_status": false,
        "hw_pump_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "active_on_setpoint",
      "description": "A firing boiler and running pump holding the active target are healthy.",
      "inputs": {
        "hws_temp": 60.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "too_cold_at_initialization",
      "description": "A standing cold-side violation exposes direction immediately: yFault is clear one tick before 900 s, asserts exactly at 900 s, and remains true after the initialization-safe delay.",
      "inputs": {
        "hws_temp": 57.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 900,
          "to_s": 900,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "too_hot_at_initialization",
      "description": "The mirror hot-side violation uses the same continuous persistence.",
      "inputs": {
        "hws_temp": 63.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "hot_error_exact_boundary_clear",
      "description": "Exactly +2 K is clear under the strict comparator.",
      "inputs": {
        "hws_temp": 62.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "hot_error_just_beyond_boundary",
      "description": "One hundredth kelvin above the hot boundary follows the full delay.",
      "inputs": {
        "hws_temp": 62.01,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "cold_error_exact_boundary_clear",
      "description": "Exactly -2 K is clear under the mirrored strict comparator.",
      "inputs": {
        "hws_temp": 58.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "cold_error_just_beyond_boundary",
      "description": "One hundredth kelvin past the cold boundary follows the full delay.",
      "inputs": {
        "hws_temp": 57.99,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "boiler_only_is_inactive",
      "description": "Boiler firing without established distribution-pump proof gates off both directions.",
      "inputs": {
        "hws_temp": 50.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "pump_only_is_inactive",
      "description": "Pump circulation without a firing heat source is not tracking evidence for this rule.",
      "inputs": {
        "hws_temp": 50.0,
        "hws_temp_sp": 60.0,
        "boiler_status": false,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "violation_ends_one_tick_before_persistence",
      "description": "The cold error returns to the band one evaluator step before 900 s and never matures.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 57.0
          },
          {
            "t": 840,
            "value": 60.0
          }
        ],
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_alarm_recovers",
      "description": "A mature hot-side alarm and its diagnostic clear immediately after temperature recovers.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 63.0
          },
          {
            "t": 1800,
            "value": 60.0
          }
        ],
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 1740,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooHot",
          "from_s": 1860,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "two_short_episodes_do_not_accumulate",
      "description": "Two cold episodes separated by an in-band interval each start a fresh timer.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 57.0
          },
          {
            "t": 600,
            "value": 60.0
          },
          {
            "t": 900,
            "value": 57.0
          },
          {
            "t": 1500,
            "value": 60.0
          }
        ],
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "pump_proof_drop_resets_timer",
      "description": "Loss and restoration of pump proof gives the continuing temperature error a fresh 900-second clock.",
      "inputs": {
        "hws_temp": 57.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 600,
            "value": false
          },
          {
            "t": 900,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "boiler_proof_drop_resets_timer",
      "description": "A firing-status interruption independently resets persistence.",
      "inputs": {
        "hws_temp": 63.0,
        "hws_temp_sp": 60.0,
        "boiler_status": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 600,
            "value": false
          },
          {
            "t": 900,
            "value": true
          }
        ],
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "direct_direction_handoff_preserves_timer",
      "description": "A sampled hot-to-cold handoff never re-enters the band, so the shared out-of-band timer continues while diagnostics swap.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 63.0
          },
          {
            "t": 600,
            "value": 57.0
          }
        ],
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooHot",
          "from_s": 0,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yTooCold",
          "from_s": 660,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "setpoint_step_settles_before_delay",
      "description": "A reset step creates raw cold error, but temperature catches the new target before persistence.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 60.0
          },
          {
            "t": 600,
            "value": 64.0
          }
        ],
        "hws_temp_sp": [
          {
            "t": 0,
            "value": 60.0
          },
          {
            "t": 300,
            "value": 64.0
          }
        ],
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "temperature_threshold_chatter_resets_delay",
      "description": "Alternating just inside and outside the hot boundary never accumulates continuous persistence.",
      "inputs": {
        "hws_temp": [
          {
            "t": 0,
            "value": 62.01
          },
          {
            "t": 180,
            "value": 61.99
          },
          {
            "t": 360,
            "value": 62.01
          },
          {
            "t": 540,
            "value": 61.99
          },
          {
            "t": 720,
            "value": 62.01
          },
          {
            "t": 900,
            "value": 61.99
          }
        ],
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "common_header_binding_is_host_excluded",
      "description": "The graph alarms on a coherent raw tuple even if this header is not the firing boiler's controlled target; topology proof is a mandatory host gate.",
      "inputs": {
        "hws_temp": 55.0,
        "hws_temp_sp": 60.0,
        "boiler_status": true,
        "hw_pump_status": true
      },
      "expect": [
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    }
  ]
}