TOWER-0006 — Cooling-tower basin freeze-protection failure
| Status | verified — engine e2ff2f8, cxf:fnv1a128:6a926c5a2ff88dc03e2cfebe65d131c0, 2026-08-20 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | MEDIUM |
| Estimation | QUALITATIVE_ONLY |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | — |
| Playbooks | cooling-tower-performance |
| Source | EVAPCO, 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 states | wet 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 maturedyHeaterFailToRun— True after a final heater command lacks independent proof for heater_proof_timeyLowBasinTemp— True after representative basin water remains below its configured minimum during freeze exposure for thermal_response_timeyFreezeExposure— 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:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
freeze_exposure_oat | 2.0 | °C | freezeExposure.t | Outdoor exposure threshold. ADOPTED_TUNABLE: review against site climate, sensor bias, wind exposure, and OEM freeze strategy. |
minimum_basin_temp | 4.0 | °C | basinLow.t | Minimum 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_time | 120.0 | s | heaterProof.delayTime | Allowed independent electrical/operating proof delay. ADOPTED_TUNABLE: exceed contactor/current sensing and telemetry latency. |
thermal_response_time | 1800.0 | s | thermalProof.delayTime | Continuous 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
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:
- Open disconnect, breaker/fuse, contactor, control transformer, or heater element
- Low-water, thermostat, over-temperature, or OEM safety correctly blocking operation
- Failed current/power proof or status mapped to the wrong heater circuit
- Final command is not actually downstream of local thermostat/safety logic
yLowBasinTemp:
- Heater is undersized, failed, staged incorrectly, or not receiving voltage
- Basin temperature sensor is biased, poorly located, or in a stagnant pocket
- Basin level, wind exposure, leakage, or unintended circulation exceeds the design basis
- Site intended drain-down/remote-sump operation but the host failed to suppress evaluation
- 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 °Cis 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_DEFAULTbecause 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=falsemakes 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.
| Scenario | Description |
|---|---|
warm_ambient_heater_off | A warm wet tower with no heater request is healthy. |
freeze_exposure_safe_basin | Exposure alone is diagnostic context and does not alarm while basin temperature is safe. |
heater_mismatch_at_initialization | A standing heater command/proof mismatch serves the full electrical proof delay. |
low_basin_at_initialization | A cold basin during exposure serves the complete initialization-safe thermal delay. |
oat_exact_boundary_clear | Exactly 2 C is outside the strict freeze-exposure condition. |
oat_just_below_boundary | One test increment below 2 C enters freeze exposure. |
basin_exact_boundary_clear | Exactly 4 C is outside the strict low-basin condition. |
basin_just_below_boundary | One test increment below the site-configured minimum matures the thermal branch. |
heater_mismatch_ends_one_tick_before_proof | Independent proof arriving at 60 seconds prevents the 120-second timer from maturing. |
mature_heater_alarm_recovers | A mature electrical proof alarm clears immediately when independent proof arrives. |
low_condition_ends_one_tick_before_thermal_proof | A 1740-second cold-basin episode clears one evaluator tick before maturity. |
mature_low_basin_alarm_recovers | An already-mature thermal alarm clears when representative basin temperature recovers. |
exposure_ends_before_thermal_duration | Outdoor exposure ending at 1740 seconds resets the thermal candidate. |
basin_recovers_before_thermal_duration | Representative basin recovery before the duration prevents a protective alarm. |
heater_proven_but_basin_stays_low | Electrical heater proof cannot mask a basin that remains thermally unsafe. |
uncommanded_heater_status_is_uncovered_hazard | 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. |
low_basin_without_exposure_clear | A low reading without configured outdoor exposure does not enter the thermal protection lane. |
both_branches_mature_independently | Electrical proof and low-basin protection can both mature without masking one another. |
heater_command_chatter_never_matures | Short command/proof mismatches cannot accumulate across healthy intervals. |
two_short_low_basin_episodes_do_not_accumulate | Separated thermal episodes each require a complete 1800-second duration. |
heater_status_chatter_never_matures | Independent proof repeatedly restores agreement before the electrical timer can mature. |
oat_threshold_chatter_resets_thermal_lane | Outdoor temperature alternating across the strict exposure threshold prevents continuous thermal maturity. |
basin_threshold_chatter_resets_thermal_lane | Basin temperature alternating across the strict configured minimum prevents continuous maturity. |
electrical_to_thermal_branch_handoff | Heater proof clears a mature electrical alarm, then the independently pending cold-basin lane matures later. |
mature_exposure_alarm_recovers | A mature low-basin alarm clears when outdoor exposure ends even if the basin reading remains low. |
mature_heater_alarm_clears_on_command_withdrawal | The electrical lane clears when its final heater request is withdrawn. |
drained_tower_raw_false_positive | A 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_spot | An 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
}
]
}
]
}