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

TOWER-0006 — Cooling-tower basin freeze-protection failure

Statusverified — engine e2ff2f8, cxf:fnv1a128:6a926c5a2ff88dc03e2cfebe65d131c0, 2026-08-20
Severity2
Methodrule
Phase2
CategoryPROTECTIVE
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed by
Related
Playbookscooling-tower-performance
SourceEVAPCO, Cooling Towers Operation and Maintenance Instructions (2017), p.21 — remote sump is the most effective idle freeze strategy; basin heaters protect basin water but not external piping and are for idle/no-flow operation; SPX Cooling Technologies, Cooling Tower Fundamentals, 2nd ed., p.90 — basin heater systems use a thermostat and low-water protection; the example maintains at least 40 °F and warns of fire or heater burnout without water; SPX Cooling Technologies, Basin Heater System Engineering Data TECH-BH-19 — packaged basin heater control includes temperature control and low-water-level protection; Library-authored watchdog thresholds/timers; OEM/site freeze plan remains authoritative
Operating stateswet cooling tower with water intentionally present in its basin/sump and a monitored heater or equivalent freeze-protection device under automatic control

Preconditions (host-enforced): Adopt only where the site/OEM freeze plan intentionally retains basin water and makes this monitored heater or equivalent device responsible for protection. Exclude drained-down towers, remote-sump strategies with an empty outdoor basin, dry coolers/fluid coolers, seasonal shutdown, maintenance, manual operation, and any interval in which basin level is below the heater’s safe operating requirement. Never use this diagnostic to bypass low-water cutoff, thermostat, over-temperature, fire, electrical, or OEM safeties, and never energize a heater manually from an alarm. Bind tower_basin_heater_cmd to the final post-safety request and status to independent current, power, contactor, or thermal proof rather than a command echo. Basin temperature must represent the bulk water, away from the local heater plume; OAT must represent the tower exposure. Points must be fresh and time-aligned. Configure both minimum_basin_temp and thermal_response_time from the actual OEM/site plan before evaluation; both shipped values are NO_PORTABLE_DEFAULT and otherwise require NO_EVAL. Basin heaters do not protect external piping, pumps, or heat exchangers.

Points: oat, tower_basin_temp, tower_basin_heater_cmd, tower_basin_heater_status

Outputs:

  • yFault — True while either the independent heater-proof lane or the freeze-exposed low-basin-temperature lane has matured
  • yHeaterFailToRun — True after a final heater command lacks independent proof for heater_proof_time
  • yLowBasinTemp — True after representative basin water remains below its configured minimum during freeze exposure for thermal_response_time
  • yFreezeExposure — True while OAT is strictly below freeze_exposure_oat; a diagnostic subcondition only, not a rule-wide data-quality flag (false does not mean NO_EVAL)

Parameters:

NameDefaultUnitCXF pathDescription
freeze_exposure_oat2.0°CfreezeExposure.tOutdoor exposure threshold. ADOPTED_TUNABLE: review against site climate, sensor bias, wind exposure, and OEM freeze strategy.
minimum_basin_temp4.0°CbasinLow.tMinimum representative basin temperature during exposure. NO_PORTABLE_DEFAULT: 4 °C is an adoption-blocking placeholder; replace it with the OEM/site freeze-plan value before evaluation.
heater_proof_time120.0sheaterProof.delayTimeAllowed independent electrical/operating proof delay. ADOPTED_TUNABLE: exceed contactor/current sensing and telemetry latency.
thermal_response_time1800.0sthermalProof.delayTimeContinuous low bulk-water temperature during exposure required for the thermal alarm. NO_PORTABLE_DEFAULT: 1800 s is an adoption-blocking placeholder; configure for basin volume, heater capacity, circulation, sensor location, and OEM response requirements.

Description

This watchdog exposes two different failures without pretending either is a complete freeze-control system. The electrical lane finds a final heater request that lacks independent proof. The thermal lane finds bulk basin water that remains below a site/OEM minimum while outdoor conditions meet the configured freeze-exposure threshold. Either is a prompt to follow the approved freeze plan—not permission to defeat safeties or energize equipment manually.

Detection Logic

freeze_exposure = oat < freeze_exposure_oat
heater_mismatch = tower_basin_heater_cmd AND NOT tower_basin_heater_status
low_condition   = freeze_exposure AND tower_basin_temp < minimum_basin_temp

yHeaterFailToRun = heater_mismatch sustained for heater_proof_time
yLowBasinTemp     = low_condition sustained for thermal_response_time
yFreezeExposure   = freeze_exposure
yFault            = yHeaterFailToRun OR yLowBasinTemp

TOWER-0006 block graph

Both comparators are strict: exactly 2 °C OAT is not exposure and exactly 4 °C basin temperature is not low under the shipped graph. Each lane has an independent TrueDelay(delayOnInit=true) and clears immediately when its own condition clears.

Possible Diagnoses

yHeaterFailToRun:

  1. Open disconnect, breaker/fuse, contactor, control transformer, or heater element
  2. Low-water, thermostat, over-temperature, or OEM safety correctly blocking operation
  3. Failed current/power proof or status mapped to the wrong heater circuit
  4. Final command is not actually downstream of local thermostat/safety logic

yLowBasinTemp:

  1. Heater is undersized, failed, staged incorrectly, or not receiving voltage
  2. Basin temperature sensor is biased, poorly located, or in a stagnant pocket
  3. Basin level, wind exposure, leakage, or unintended circulation exceeds the design basis
  4. Site intended drain-down/remote-sump operation but the host failed to suppress evaluation
  5. The configured minimum or exposure threshold does not match the OEM freeze plan

Energy Impact

No energy savings estimate. This is a protective finding whose value is avoided freeze damage, loss of heat rejection, water release, and unsafe inspection conditions. The opposite electrical direction—heater proven on with command off—is deliberately not included and may become a future energy rule.

Emissions Impact

Scope 2, qualitative only. Avoided repair and refrigerant/water consequences are real but outside the point set and cannot be defensibly converted to operational emissions here.

Deviations

  • minimum_basin_temp = 4 °C is not portable. OEM examples commonly describe about 40 °F protection, but strategy, equipment, ambient design, circulation, glycol, sensor location, and basin geometry differ. The shipped value is an adoption-blocking placeholder, not a universal safety setpoint.
  • The 2 °C exposure threshold and 120 s electrical timer are adopted. The 1800 s thermal response is NO_PORTABLE_DEFAULT because basin volume, heater capacity, circulation, and sensor placement dominate it. No cited source publishes this complete watchdog algorithm or these limits.
  • The graph does not encode water level. Low-water cutoff is a mandatory independent safety and a host precondition. Never infer that yFault=false makes heater operation safe.
  • A proven heater does not suppress low basin temperature. Electrical proof is not thermal adequacy, which is why the two lanes remain independent.
  • Command false/status true is intentionally silent in this graph, not safe. It may indicate stale proof, a stuck contactor, or unintended heater operation with property/fire consequences—especially without water. Route it to the OEM/site safety workflow immediately; a future rule may separately quantify the excess-energy direction.
  • Basin protection is not plant protection. OEM literature explicitly warns that a basin heater does not protect external piping, pumps, or heat exchangers; the full site drain-down/circulation plan remains authoritative.
  • No EnergyPlus validation is claimed. Simulated basin-heater electricity is neither an independent command/proof pair nor a validation of the physical safety strategy.

Notes

Follow the site/OEM freeze procedure before field inspection. Confirm basin level and electrical isolation from a safe location. Do not bypass low-water or other safeties, and do not manually energize a heater based on this diagnostic.

Test Vectors

28 scenarios, clock step 60 s over 4200 s.

ScenarioDescription
warm_ambient_heater_offA warm wet tower with no heater request is healthy.
freeze_exposure_safe_basinExposure alone is diagnostic context and does not alarm while basin temperature is safe.
heater_mismatch_at_initializationA standing heater command/proof mismatch serves the full electrical proof delay.
low_basin_at_initializationA cold basin during exposure serves the complete initialization-safe thermal delay.
oat_exact_boundary_clearExactly 2 C is outside the strict freeze-exposure condition.
oat_just_below_boundaryOne test increment below 2 C enters freeze exposure.
basin_exact_boundary_clearExactly 4 C is outside the strict low-basin condition.
basin_just_below_boundaryOne test increment below the site-configured minimum matures the thermal branch.
heater_mismatch_ends_one_tick_before_proofIndependent proof arriving at 60 seconds prevents the 120-second timer from maturing.
mature_heater_alarm_recoversA mature electrical proof alarm clears immediately when independent proof arrives.
low_condition_ends_one_tick_before_thermal_proofA 1740-second cold-basin episode clears one evaluator tick before maturity.
mature_low_basin_alarm_recoversAn already-mature thermal alarm clears when representative basin temperature recovers.
exposure_ends_before_thermal_durationOutdoor exposure ending at 1740 seconds resets the thermal candidate.
basin_recovers_before_thermal_durationRepresentative basin recovery before the duration prevents a protective alarm.
heater_proven_but_basin_stays_lowElectrical heater proof cannot mask a basin that remains thermally unsafe.
uncommanded_heater_status_is_uncovered_hazardCommand false and status true is intentionally silent in this graph but may be an uncontrolled-heater hazard; route it through the OEM/site safety workflow.
low_basin_without_exposure_clearA low reading without configured outdoor exposure does not enter the thermal protection lane.
both_branches_mature_independentlyElectrical proof and low-basin protection can both mature without masking one another.
heater_command_chatter_never_maturesShort command/proof mismatches cannot accumulate across healthy intervals.
two_short_low_basin_episodes_do_not_accumulateSeparated thermal episodes each require a complete 1800-second duration.
heater_status_chatter_never_maturesIndependent proof repeatedly restores agreement before the electrical timer can mature.
oat_threshold_chatter_resets_thermal_laneOutdoor temperature alternating across the strict exposure threshold prevents continuous thermal maturity.
basin_threshold_chatter_resets_thermal_laneBasin temperature alternating across the strict configured minimum prevents continuous maturity.
electrical_to_thermal_branch_handoffHeater proof clears a mature electrical alarm, then the independently pending cold-basin lane matures later.
mature_exposure_alarm_recoversA mature low-basin alarm clears when outdoor exposure ends even if the basin reading remains low.
mature_heater_alarm_clears_on_command_withdrawalThe electrical lane clears when its final heater request is withdrawn.
drained_tower_raw_false_positiveA meaningless low basin reading on an intentionally drained tower matures the raw thermal graph; applicability gating must report NO_EVAL.
heater_command_echo_raw_blind_spotAn echoed command presented as status keeps the electrical graph clear without proving current or heat; independent proof is mandatory.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 4200
  },
  "scenarios": [
    {
      "name": "warm_ambient_heater_off",
      "description": "A warm wet tower with no heater request is healthy.",
      "inputs": {
        "oat": 10.0,
        "tower_basin_temp": 10.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFreezeExposure",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "freeze_exposure_safe_basin",
      "description": "Exposure alone is diagnostic context and does not alarm while basin temperature is safe.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFreezeExposure",
          "from_s": 0,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yLowBasinTemp",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "heater_mismatch_at_initialization",
      "description": "A standing heater command/proof mismatch serves the full electrical proof delay.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 0,
          "to_s": 60,
          "equals": false
        },
        {
          "output": "yHeaterFailToRun",
          "from_s": 180,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 60,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 180,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "low_basin_at_initialization",
      "description": "A cold basin during exposure serves the complete initialization-safe thermal delay.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yLowBasinTemp",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "oat_exact_boundary_clear",
      "description": "Exactly 2 C is outside the strict freeze-exposure condition.",
      "inputs": {
        "oat": 2.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFreezeExposure",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "oat_just_below_boundary",
      "description": "One test increment below 2 C enters freeze exposure.",
      "inputs": {
        "oat": 1.99,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFreezeExposure",
          "from_s": 0,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "basin_exact_boundary_clear",
      "description": "Exactly 4 C is outside the strict low-basin condition.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 4.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "basin_just_below_boundary",
      "description": "One test increment below the site-configured minimum matures the thermal branch.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 3.99,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "heater_mismatch_ends_one_tick_before_proof",
      "description": "Independent proof arriving at 60 seconds prevents the 120-second timer from maturing.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 60,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_heater_alarm_recovers",
      "description": "A mature electrical proof alarm clears immediately when independent proof arrives.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 600,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 600,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "low_condition_ends_one_tick_before_thermal_proof",
      "description": "A 1740-second cold-basin episode clears one evaluator tick before maturity.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": [
          {
            "t": 0,
            "value": 3.0
          },
          {
            "t": 1740,
            "value": 6.0
          }
        ],
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_low_basin_alarm_recovers",
      "description": "An already-mature thermal alarm clears when representative basin temperature recovers.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": [
          {
            "t": 0,
            "value": 3.0
          },
          {
            "t": 2400,
            "value": 6.0
          }
        ],
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 2340,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 2340,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 2400,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "exposure_ends_before_thermal_duration",
      "description": "Outdoor exposure ending at 1740 seconds resets the thermal candidate.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 1740,
            "value": 5.0
          }
        ],
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "basin_recovers_before_thermal_duration",
      "description": "Representative basin recovery before the duration prevents a protective alarm.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": [
          {
            "t": 0,
            "value": 3.0
          },
          {
            "t": 1200,
            "value": 6.0
          }
        ],
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "heater_proven_but_basin_stays_low",
      "description": "Electrical heater proof cannot mask a basin that remains thermally unsafe.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": true
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "uncommanded_heater_status_is_uncovered_hazard",
      "description": "Command false and status true is intentionally silent in this graph but may be an uncontrolled-heater hazard; route it through the OEM/site safety workflow.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": true
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "low_basin_without_exposure_clear",
      "description": "A low reading without configured outdoor exposure does not enter the thermal protection lane.",
      "inputs": {
        "oat": 5.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFreezeExposure",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "both_branches_mature_independently",
      "description": "Electrical proof and low-basin protection can both mature without masking one another.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 180,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 180,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "heater_command_chatter_never_matures",
      "description": "Short command/proof mismatches cannot accumulate across healthy intervals.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 60,
            "value": false
          },
          {
            "t": 180,
            "value": true
          },
          {
            "t": 240,
            "value": false
          }
        ],
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "two_short_low_basin_episodes_do_not_accumulate",
      "description": "Separated thermal episodes each require a complete 1800-second duration.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": [
          {
            "t": 0,
            "value": 3.0
          },
          {
            "t": 1200,
            "value": 6.0
          },
          {
            "t": 1800,
            "value": 3.0
          },
          {
            "t": 3000,
            "value": 6.0
          }
        ],
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "heater_status_chatter_never_matures",
      "description": "Independent proof repeatedly restores agreement before the electrical timer can mature.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 60,
            "value": true
          },
          {
            "t": 180,
            "value": false
          },
          {
            "t": 240,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "oat_threshold_chatter_resets_thermal_lane",
      "description": "Outdoor temperature alternating across the strict exposure threshold prevents continuous thermal maturity.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 1.99
          },
          {
            "t": 600,
            "value": 2.0
          },
          {
            "t": 1200,
            "value": 1.99
          },
          {
            "t": 1800,
            "value": 2.0
          }
        ],
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "basin_threshold_chatter_resets_thermal_lane",
      "description": "Basin temperature alternating across the strict configured minimum prevents continuous maturity.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": [
          {
            "t": 0,
            "value": 3.99
          },
          {
            "t": 600,
            "value": 4.0
          },
          {
            "t": 1200,
            "value": 3.99
          },
          {
            "t": 1800,
            "value": 4.0
          }
        ],
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "electrical_to_thermal_branch_handoff",
      "description": "Heater proof clears a mature electrical alarm, then the independently pending cold-basin lane matures later.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 600,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 600,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "mature_exposure_alarm_recovers",
      "description": "A mature low-basin alarm clears when outdoor exposure ends even if the basin reading remains low.",
      "inputs": {
        "oat": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 2400,
            "value": 5.0
          }
        ],
        "tower_basin_temp": 3.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 2340,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 2340,
          "equals": true
        },
        {
          "output": "yFreezeExposure",
          "from_s": 2400,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2400,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_heater_alarm_clears_on_command_withdrawal",
      "description": "The electrical lane clears when its final heater request is withdrawn.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 600,
            "value": false
          }
        ],
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 180,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 600,
          "to_s": 4200,
          "equals": false
        }
      ]
    },
    {
      "name": "drained_tower_raw_false_positive",
      "description": "A meaningless low basin reading on an intentionally drained tower matures the raw thermal graph; applicability gating must report NO_EVAL.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 0.0,
        "tower_basin_heater_cmd": false,
        "tower_basin_heater_status": false
      },
      "expect": [
        {
          "output": "yLowBasinTemp",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 4200,
          "equals": true
        }
      ]
    },
    {
      "name": "heater_command_echo_raw_blind_spot",
      "description": "An echoed command presented as status keeps the electrical graph clear without proving current or heat; independent proof is mandatory.",
      "inputs": {
        "oat": 0.0,
        "tower_basin_temp": 6.0,
        "tower_basin_heater_cmd": true,
        "tower_basin_heater_status": true
      },
      "expect": [
        {
          "output": "yHeaterFailToRun",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4200,
          "equals": false
        }
      ]
    }
  ]
}