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

RTU-0008 — Refrigerant undercharge — superheat/subcooling divergence

Statusverified — engine e2ff2f8, cxf:fnv1a128:6185ddcde7a0e68b845e3af5bcea5b05, 2026-08-18
Severity3
Methodrule
Phase2
CategoryEFFICIENCY_LOSS
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedHP-0004, RTU-0009, RTU-0007, RTU-0001, RTU-0010, RTU-0011
Playbooksrtu-compressor-refrigerant
SourceLibrary-authored — HVAC FDD Reference v1.0 §11 specifies no refrigerant-charge rule for packaged units; its playbook reaches charge only as a cause of short-cycling (Step 2.1.2); NIST SP 1087, Kim, Yoon, Payne & Domanski, Cooling Mode Fault Detection and Diagnosis Method for a Residential Heat Pump, NIST, October 2008: §5.4.3 Table 5.2 (undercharge fault-direction rows, zones A and B), §5.4.2 and §5.4.4 (the 0.5 °C upstream-subcooling test and its role in selecting a chart), §5.5.1 Figs. 5.16-5.17 (fault level versus EER degradation), §5.5.2 Table 5.16 (undercharge diagnosis results) — a residential heat-pump study restated here for unitary packaged equipment (see Deviations); Breuker & Braun 1998b and Rossi & Braun 1997, reproduced as SP 1087 Table 5.1(a) — the fixed-orifice refrigerant-leakage row, which is the half of the packaged population SP 1087’s own TXV rig does not represent; Li, H. & Braun, J.E. (2009), Decoupling features and virtual sensors for diagnosis of faults in vapor compression air conditioners, HVAC&R Research 15(1) — the virtual-refrigerant-charge sensor built from four surface-mounted temperatures and validated across seven unitary systems; the lineage for reading charge off exactly the temperatures this rule reads; Kim, W. & Braun, J.E. (2020), Energy and Buildings 225 — integrated virtual sensors demonstrated on rooftop units, the packaged-equipment continuation of that work; Hu, Y. et al. (2021), Energy and Buildings 248 — single-feature charge inference degrades when other faults are present; the simultaneous-fault caveat, carried in Deviations as on HP-0004; Sibling precedent: HP-0004 (the heat-pump original this card mirrors, including the sub-condition-flag resolution), RTU-0002 (fixed bands named as a simplification of a regressed baseline), RTU-0007 (settled-compressor and stage-change gating as host preconditions)
Operating statesmechanical cooling, compressor running and settled — one instance per refrigerant circuit. Cooling-only packaged equipment, so there is no mode split and no defrost state to exclude; a unit in economizer free cooling has its compressors off and is covered by the compressor gate.

Preconditions (host-enforced): The compressor must be running and must have held its current stage for min_runtime_for_eval (15 min). With the compressor off all four temperatures equalise and both differences collapse to zero; after a start or a stage change superheat overshoots for minutes while the metering device catches up, and on a multi-stage unit the whole refrigerant-side operating point moves (RTU-0007’s stage-change precondition, same reason). Short-cycling units may never present a settled window at all — that silence is RTU-0001’s finding, not a healthy charge. evap_sat_temp and cond_sat_temp are host-derived P-T lookups and each lookup MUST be configured for the refrigerant actually in the machine: a wrong refrigerant biases both differences at once and in opposite directions, which is this rule’s exact fault pattern. The suction and liquid probes must have good pipe contact and be insulated from ambient air — on a rooftop an unshaded, uninsulated liquid-line probe reads solar gain and fabricates collapsed subcooling. Head-pressure control must be at its normal control point; a unit deliberately flooding the condenser on a cool day moves subcooling by design. Read yTxvSaturated as diagnostic context, NOT as an evaluability gate: this rule has no in-graph NO_EVAL test and false never means healthy (see Deviations).

Points: suction_temp, evap_sat_temp, cond_sat_temp, liquid_temp

Outputs:

  • yFault — True while suction superheat has stayed above superheat_high_band and liquid subcooling below subcooling_low_band, both continuously for at least alarm_delay
  • yTxvSaturated — Sub-condition flag (NOT an evaluability flag; false never means NO_EVAL) — true when liquid subcooling has fallen below subcooling_two_phase_floor, meaning the liquid line is no longer measurably subcooled and the metering device is being fed two-phase refrigerant

Parameters:

NameDefaultUnitCXF pathDescription
superheat_high_band15.0°CshHigh.tSuction superheat above which the evaporator is judged starved. COMMISSIONING-SET PLACEHOLDER — the shipped 15.0 sits just above the no-fault compressor-suction superheats NIST SP 1087 reports for its test unit (10.2-13.7 °C). Set it from this unit’s own charging chart plus a tolerance, and on a fixed-orifice machine set it at the chart’s high-superheat corner — low indoor wet-bulb against a high outdoor drybulb (see Deviations); a suction-line probe reads higher than an evaporator-exit probe on the same machine.
subcooling_low_band3.0°CscLow.tLiquid subcooling below which the condenser is judged short of liquid. COMMISSIONING-SET PLACEHOLDER on the same terms — charging-chart targets run roughly 8-11 °C at design on a TXV unit and lower on a fixed-orifice one, and this band sits well under either so normal load swings do not reach it.
subcooling_two_phase_floor0.5°CtxvSat.tSubcooling below which the metering-device inlet is taken to be two-phase. Drives yTxvSaturated only. 0.5 °C is SP 1087’s own single-phase/two-phase test (§5.4.2), used there to pick which fault-direction chart applies; unlike the bands above it is a physical boundary, not a per-unit tuning.
alarm_delay1800.0spersist.delayTimeContinuous divergence required before the alarm asserts (30 min). Long enough to outlast a metering device hunting after a load step, short enough that a real charge loss is reported within the hour

Description

A machine short of refrigerant runs short of liquid. The metering device runs out of authority trying to keep the evaporator fed, so the evaporator starves while the condenser loses its liquid seal: superheat climbs and subcooling collapses at once. That divergence is the signature — capacity loss alone says nothing about cause, and a rooftop unit loses capacity quietly for a season before anyone calls it in. NIST SP 1087 imposed graded charge faults on a TXV-equipped R410A machine in cooling and recorded this pair once the valve saturated (§5.4.3, Table 5.2, zone B); Breuker & Braun’s fixed-orifice charts show it from the first pound lost. This rule forms both differences from four refrigerant-side temperatures and alarms when they sit past their commissioned bands for half an hour.

Detection Logic

suction_superheat = suction_temp  − evap_sat_temp
liquid_subcooling = cond_sat_temp − liquid_temp

yTxvSaturated = liquid_subcooling < subcooling_two_phase_floor
                (sub-condition flag; false does NOT mean NO_EVAL)

yFault        = suction_superheat > superheat_high_band
                AND liquid_subcooling < subcooling_low_band,
                sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

RTU-0008 block graph

The conjunction is the diagnosis, not a noise filter. High superheat with subcooling high is the liquid-line or filter-drier restriction pattern, where refrigerant backs up ahead of the restriction; with subcooling merely normal it is a fixed-orifice unit at low indoor load, which is no fault at all. Low subcooling with superheat still normal is a valve compensating successfully. Only the two together indict the charge.

Both comparisons are strict, so a unit exactly on either band reads healthy, and both bands are per-unit commissioning values — the shipped defaults are placeholders, not thresholds anyone measured on the machine in front of you. persist requires 30 continuous minutes and clocks them from the conjunction, not from the first symptom; delayOnInit = true holds that window across a controller restart. yTxvSaturated reports whether the liquid line is still measurably subcooled; it does not gate the alarm, and the reason it must not is the card’s main deviation.

Possible Diagnoses

  1. Refrigerant leak — brazed joints, Schrader cores, service-valve packing and flare connections, in that order of prevalence; a top-up without a leak search buys months, not years
  2. The unit was charged short, at commissioning or after a rooftop repair that vented the circuit and was recharged by pressure rather than by weight
  3. Condenser airflow restriction (RTU-0007) — a fouled coil moves this pair the same way and separates only on condensing temperature, which rises rather than falls. This rule reads differences, not levels, so RTU-0007’s split is the discriminator and neither card suppresses the other
  4. Instrumentation: a P-T derivation configured for the wrong refrigerant, or a liquid-line probe with poor contact, missing insulation or sun on it. Each fabricates the pattern on a correctly charged machine

Energy Impact

EFFICIENCY_LOSS, MEDIUM confidence, PROXY_ESTIMATION. waste_kw = compressor_kw × d / (1 − d), the extra compressor runtime needed to deliver the same cooling at a degraded EER. NIST SP 1087 sizes d directly: every fault it tested except compressor leakage needed a fault level above 10% to cost 5% of EER, and a 20% charge shortfall cost 6.5-13% of EER, the largest hit in its Figure 5.17. Confidence is MEDIUM because the rule fires on a pattern, not a severity — it reports that the charge is low, not by how much, so d is a population number until the technician’s gauge set supplies a real one.

Emissions Impact

Scope 2, PROXY_EMISSIONS, MEDIUM confidence; typically 300-2,000 kg CO₂e/yr for a commercial packaged unit, the same order as RTU-0002 and RTU-0007 and all of it compressor electricity, so the avoided-emissions basis is the marginal operating emissions rate (MOER) and the waste peaks on the hot afternoons when the grid is dirtiest. A leaking circuit also vents refrigerant, and R410A carries a GWP near 2,000; that release is a scope 1 emission this card does not estimate, because the leak rate is not observable from any point the rule reads. Sites with refrigerant-tracking obligations should account for it separately.

Deviations

  • The 0.5 °C subcooling floor is a sub-condition flag, not an evaluability gate. SP 1087 uses it (§5.4.4) to select which fault-direction chart applies, never to suppress evaluation, and that chart still lists falling subcooling as an undercharge symptom. Gating yFault on it would silence the rule exactly when the liquid line has flashed to two-phase — severe undercharge, not missing data — so the flag is named yTxvSaturated, not y…Ok.
  • The flag keeps HP-0004’s name on a family where half the population has no TXV. Nothing saturates on a fixed-orifice unit, but the measurement is identical — the liquid line is no longer measurably subcooled — and outputs says so. A per-family rename would cost hosts binding both cards a shared signal name for one word of accuracy.
  • The rule does not know which expansion device it is watching, and does not need to. SP 1087 splits its charts on that device — Table 5.2 for TXV, Table 5.1(a) for fixed orifice — but both list superheat up with subcooling down for undercharge, so the pattern is common ground and no point or parameter records the device. The split moves into commissioning instead, and it lands on the superheat band: on a fixed-orifice unit superheat follows indoor wet-bulb and outdoor drybulb rather than being controlled (charging charts for that population are two-dimensional for that reason), so the band belongs at the chart’s high-superheat corner — low indoor wet-bulb against a high outdoor drybulb — or a dry-climate unit alarms on its hottest afternoons (fixed_orifice_unit_at_low_indoor_load pins the case).
  • Fixed bands replace SP 1087’s regressed no-fault baseline. SP 1087 compares each feature against a third-order regression on three variables; this library’s only regression primitive is a host-fitted line, so charging-chart nominal targets stand in — a real simplification, named as one on RTU-0002’s precedent, and the reason untouched defaults can alarm forever.
  • Sensitivity runs opposite between the two populations. On a TXV unit undercharge moves no superheat until the valve saturates (SP 1087’s zone A), so a mild loss the valve absorbs reads healthy here; earlier investigators reported difficulty detecting undercharge below roughly 40% charge loss (Breuker & Braun 1998b; Stylianou & Nikanpour 1996, via SP 1087 §2). A fixed-orifice unit is caught earlier and pays for it in false-alarm exposure at low load.
  • Two features, so condenser airflow restriction is not excluded. It shares the superheat-up/subcooling-down pair and separates on condensing temperature moving up rather than down — a level test needing a baseline, which RTU-0007 has and this rule does not. Diagnosis 3 names it and condenser_restriction_reads_as_undercharge pins that this rule fires on it; no suppression either way, because neither rule adjudicates the other’s evidence.
  • The grounding transfers from a residential heat pump to packaged equipment. A starved evaporator and an unsealed condenser are circuit-level physics, and Li & Braun (2009) validated charge inference from these same four surface temperatures across seven unitary systems, Kim & Braun (2020) carrying it onto rooftop units. What transfers is the pattern, not the numbers: no SP 1087 threshold is adopted except the physical two-phase floor.
  • Cooling-only, which is where this card stands on firmer ground than HP-0004. SP 1087 tested cooling exclusively, so the heat-pump reading has to carry a caveat about the coils swapping roles in heating. A cooling-only packaged unit has no reversing valve and no defrost cycle: the sensors keep their heat exchangers year-round, operating_states needs no mode split and preconditions no defrost gate. Bind HP-0004 for a heat-pump rooftop.
  • Compressor and steady-state gating stay host preconditions. The graph computes the fault given valid data, per SCHEMA.md; comp_status and time-since-stage-change are not among its inputs, and the 30-minute alarm_delay does not substitute — a post-start superheat overshoot starts the persistence timer rather than being excluded from it.
  • The pattern chart is a single-fault chart. SP 1087 imposed one fault at a time, and Hu et al. (2021) show single-feature charge inference degrading when other faults are present, their residuals superposing. Two faults at once can cancel this rule’s pattern or fake it; the diagnosis list is a ranking, not a verdict.
  • Strict > and < at both bands. CDL Reals has no GreaterEqual or LessEqual, so a unit sitting exactly on a band reads healthy. The disagreement is measure-zero on real-valued signals; both sides of both bands, and of the two-phase floor, are pinned bit-exactly by vectors.
  • The playbook covers the work but does not yet list this rule. Its Step 2.1.2 already sends the technician to superheat and subcooling against manufacturer specs, the measurement this rule automates; the Applies-To row and a charge-specific Step 1 entry are the index owner’s to add.
  • persist.delayOnInit = true (CDL default is false), the library’s standing choice. Severity 3 and category: EFFICIENCY_LOSS follow HP-0004 and RTU-0002; the namespace urn:cxf-library:rtu-0008# is SCHEMA.md’s normative form, as on RTU-0007 and VAV-0010. No reference card exists to inherit any of this from and none publishes test vectors, so every scenario in vectors.json is authored from the equation and replayed against the pinned engine rev.

Notes

Do not read a cleared alarm as a repaired machine: the host gates this rule on the compressor running, so every stop drops yFault for the same reason a recharge does, and a short-cycling unit may never complete a settled window at all. When yTxvSaturated is true, expect flash gas at the sight glass and weigh the recovered charge rather than trusting subcooling to confirm the fix. Check RTU-0007 before opening the gauges — a fouled condenser makes this same pair and costs a coil wash rather than a leak search. RTU-0009 is the overcharge branch, HP-0004 the heat-pump original, and rtu-compressor-refrigerant orders the on-site work.

Test Vectors

16 scenarios, clock step 60 s over 7200 s.

ScenarioDescription
correctly_charged_unitNominal mechanical cooling: 11 degC of suction superheat against a 15 degC band and 9 degC of subcooling against a 3 degC band. Neither conjunct holds and the liquid line is comfortably single-phase.
undercharge_with_liquid_still_subcooledThe signature: 20 degC superheat with subcooling collapsed to 1.5 degC. Both conjuncts hold from t=0, so delayOnInit puts the alarm exactly one alarm_delay in. Subcooling is still above the two-phase floor, so yTxvSaturated stays false and the fault does not depend on it.
high_superheat_with_high_subcooling_is_not_underchargeThe discriminator the subcooling conjunct exists for: 20 degC superheat with subcooling raised to 12 degC is the liquid-line-restriction pattern, where refrigerant backs up ahead of the restriction. Superheat alone would call this a charge fault; the AND does not.
fixed_orifice_unit_at_low_indoor_loadThe RTU false positive the conjunction blocks: a fixed-orifice machine at the high-superheat corner of its charging chart - low indoor wet-bulb against a hot outdoor day - runs 18 degC of superheat with its charge intact, because on that population superheat follows load rather than being controlled. Subcooling stays at a healthy 8 degC and nothing asserts.
low_subcooling_alone_while_the_valve_compensatesSubcooling down to 1 degC with superheat held at a normal 11 degC - a TXV still in control. This is the in-control-zone undercharge pattern and the rule deliberately does not fire on it (see Deviations); the superheat conjunct blocks alone.
superheat_exactly_at_the_bandThreshold edge: superheat is exactly superheat_high_band (20.0 - 5.0 = 15.0, binary-exact) with subcooling already low. The comparison is a strict >, so a unit sitting precisely on the band reads healthy.
superheat_just_above_the_bandThreshold edge from the other side: 15.25 degC of superheat, a quarter of a degree over the band, and the alarm matures at alarm_delay.
subcooling_exactly_at_the_bandThreshold edge: subcooling is exactly subcooling_low_band (33.0 - 30.0 = 3.0, binary-exact) with superheat well over its band. The comparison is a strict <, so a unit precisely on the band reads healthy.
subcooling_just_below_the_bandThreshold edge from the other side: 2.75 degC of subcooling clears the band and the alarm matures.
subcooling_exactly_at_the_two_phase_floorSub-condition-flag edge: subcooling is exactly subcooling_two_phase_floor (30.5 - 30.0 = 0.5, binary-exact). The floor test is a strict <, so yTxvSaturated is false - and the fault asserts anyway, because the flag is a sub-condition, not a gate.
subcooling_below_the_two_phase_floorThe card’s design point, and the other side of the floor edge: 0.25 degC of subcooling means the liquid line is no longer measurably subcooled and the metering device is being fed two-phase refrigerant - a severe charge loss. yTxvSaturated goes true and yFault still asserts; a host that treated the flag as an evaluability gate would go silent on the worst case this rule exists for.
condenser_restriction_reads_as_underchargeThe RTU-0007 interaction, pinned rather than hidden: a fouled condenser on a correctly charged unit also drives superheat up (18 degC) and subcooling down (2 degC), so this rule alarms. The one thing that separates the two - condensing temperature climbing to 50 degC rather than falling - is present in the inputs and invisible to the graph, which reads only the difference. Nothing suppresses either rule; RTU-0007’s condenser split is the discriminator and the two cards carry each other in related.
timer_starts_when_the_second_conjunct_arrivesSubcooling is low from t=0 but superheat only crosses its band at t=1200 s, as the charge loss deepens past the point where the valve can keep holding superheat. The persistence window runs from the conjunction, not from the first symptom, so the alarm lands at 3000 s.
transient_superheat_excursion_never_alarmsSuperheat spikes over its band for 1500 s - a load step, or an expansion valve hunting after a stage change - and settles back 300 s short of alarm_delay. The timer resets on the recovery and nothing is reported.
alarm_clears_after_the_charge_is_restoredRecovery: the divergence alarms at 1800 s, a technician weighs in the missing charge at t = 3600 s, and both readings return to nominal. yFault drops on that same tick - TrueDelay only delays the rising edge.
idle_unit_reads_healthy_and_the_flag_reads_saturatedWhy the compressor-running precondition is host-enforced rather than optional: with the unit off, all four temperatures equalise, superheat and subcooling both collapse to zero, and the graph reports no fault while yTxvSaturated reads true. Neither answer means anything about the refrigerant charge.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 7200
  },
  "scenarios": [
    {
      "name": "correctly_charged_unit",
      "description": "Nominal mechanical cooling: 11 degC of suction superheat against a 15 degC band and 9 degC of subcooling against a 3 degC band. Neither conjunct holds and the liquid line is comfortably single-phase.",
      "inputs": {
        "suction_temp": 16.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 39.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "undercharge_with_liquid_still_subcooled",
      "description": "The signature: 20 degC superheat with subcooling collapsed to 1.5 degC. Both conjuncts hold from t=0, so delayOnInit puts the alarm exactly one alarm_delay in. Subcooling is still above the two-phase floor, so yTxvSaturated stays false and the fault does not depend on it.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.5,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "high_superheat_with_high_subcooling_is_not_undercharge",
      "description": "The discriminator the subcooling conjunct exists for: 20 degC superheat with subcooling raised to 12 degC is the liquid-line-restriction pattern, where refrigerant backs up ahead of the restriction. Superheat alone would call this a charge fault; the AND does not.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 42.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "fixed_orifice_unit_at_low_indoor_load",
      "description": "The RTU false positive the conjunction blocks: a fixed-orifice machine at the high-superheat corner of its charging chart - low indoor wet-bulb against a hot outdoor day - runs 18 degC of superheat with its charge intact, because on that population superheat follows load rather than being controlled. Subcooling stays at a healthy 8 degC and nothing asserts.",
      "inputs": {
        "suction_temp": 23.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 52.0,
        "liquid_temp": 44.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "low_subcooling_alone_while_the_valve_compensates",
      "description": "Subcooling down to 1 degC with superheat held at a normal 11 degC - a TXV still in control. This is the in-control-zone undercharge pattern and the rule deliberately does not fire on it (see Deviations); the superheat conjunct blocks alone.",
      "inputs": {
        "suction_temp": 16.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "superheat_exactly_at_the_band",
      "description": "Threshold edge: superheat is exactly superheat_high_band (20.0 - 5.0 = 15.0, binary-exact) with subcooling already low. The comparison is a strict `>`, so a unit sitting precisely on the band reads healthy.",
      "inputs": {
        "suction_temp": 20.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "superheat_just_above_the_band",
      "description": "Threshold edge from the other side: 15.25 degC of superheat, a quarter of a degree over the band, and the alarm matures at alarm_delay.",
      "inputs": {
        "suction_temp": 20.25,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "subcooling_exactly_at_the_band",
      "description": "Threshold edge: subcooling is exactly subcooling_low_band (33.0 - 30.0 = 3.0, binary-exact) with superheat well over its band. The comparison is a strict `<`, so a unit precisely on the band reads healthy.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 33.0,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "subcooling_just_below_the_band",
      "description": "Threshold edge from the other side: 2.75 degC of subcooling clears the band and the alarm matures.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 32.75,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "subcooling_exactly_at_the_two_phase_floor",
      "description": "Sub-condition-flag edge: subcooling is exactly subcooling_two_phase_floor (30.5 - 30.0 = 0.5, binary-exact). The floor test is a strict `<`, so yTxvSaturated is false - and the fault asserts anyway, because the flag is a sub-condition, not a gate.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 30.5,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "subcooling_below_the_two_phase_floor",
      "description": "The card's design point, and the other side of the floor edge: 0.25 degC of subcooling means the liquid line is no longer measurably subcooled and the metering device is being fed two-phase refrigerant - a severe charge loss. yTxvSaturated goes true and yFault still asserts; a host that treated the flag as an evaluability gate would go silent on the worst case this rule exists for.",
      "inputs": {
        "suction_temp": 25.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 30.25,
        "liquid_temp": 30.0
      },
      "expect": [
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "condenser_restriction_reads_as_undercharge",
      "description": "The RTU-0007 interaction, pinned rather than hidden: a fouled condenser on a correctly charged unit also drives superheat up (18 degC) and subcooling down (2 degC), so this rule alarms. The one thing that separates the two - condensing temperature climbing to 50 degC rather than falling - is present in the inputs and invisible to the graph, which reads only the difference. Nothing suppresses either rule; RTU-0007's condenser split is the discriminator and the two cards carry each other in `related`.",
      "inputs": {
        "suction_temp": 23.0,
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 50.0,
        "liquid_temp": 48.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "timer_starts_when_the_second_conjunct_arrives",
      "description": "Subcooling is low from t=0 but superheat only crosses its band at t=1200 s, as the charge loss deepens past the point where the valve can keep holding superheat. The persistence window runs from the conjunction, not from the first symptom, so the alarm lands at 3000 s.",
      "inputs": {
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.0,
        "liquid_temp": 30.0,
        "suction_temp": [
          {
            "t": 0,
            "value": 16.0
          },
          {
            "t": 1200,
            "value": 25.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2940,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3000,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "transient_superheat_excursion_never_alarms",
      "description": "Superheat spikes over its band for 1500 s - a load step, or an expansion valve hunting after a stage change - and settles back 300 s short of alarm_delay. The timer resets on the recovery and nothing is reported.",
      "inputs": {
        "evap_sat_temp": 5.0,
        "cond_sat_temp": 31.0,
        "liquid_temp": 30.0,
        "suction_temp": [
          {
            "t": 0,
            "value": 16.0
          },
          {
            "t": 900,
            "value": 25.0
          },
          {
            "t": 2400,
            "value": 16.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "alarm_clears_after_the_charge_is_restored",
      "description": "Recovery: the divergence alarms at 1800 s, a technician weighs in the missing charge at t = 3600 s, and both readings return to nominal. yFault drops on that same tick - TrueDelay only delays the rising edge.",
      "inputs": {
        "evap_sat_temp": 5.0,
        "liquid_temp": 30.0,
        "suction_temp": [
          {
            "t": 0,
            "value": 25.0
          },
          {
            "t": 3600,
            "value": 16.0
          }
        ],
        "cond_sat_temp": [
          {
            "t": 0,
            "value": 31.0
          },
          {
            "t": 3600,
            "value": 39.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1800,
          "to_s": 3540,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "idle_unit_reads_healthy_and_the_flag_reads_saturated",
      "description": "Why the compressor-running precondition is host-enforced rather than optional: with the unit off, all four temperatures equalise, superheat and subcooling both collapse to zero, and the graph reports no fault while yTxvSaturated reads true. Neither answer means anything about the refrigerant charge.",
      "inputs": {
        "suction_temp": 22.0,
        "evap_sat_temp": 22.0,
        "cond_sat_temp": 22.0,
        "liquid_temp": 22.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yTxvSaturated",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    }
  ]
}