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

HP-0004 — Refrigerant undercharge — superheat/subcooling divergence

Statusverified — engine e2ff2f8, cxf:fnv1a128:456b99147ff742525fe015d642f727d9, 2026-08-18
Severity3
Methodrule
Phase2
CategoryEFFICIENCY_LOSS
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedHP-0001, HP-0005, RTU-0002, RTU-0008, HP-0007
Playbooksheat-pump-faults
SourceLibrary-authored — no reference card exists for this fault; 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 (TXV control limits; the 0.5 °C upstream-subcooling test and its role in the method), §5.5.1 Figures 5.16-5.17 (fault level versus EER degradation), §5.5.2 Table 5.16 (undercharge diagnosis results); Breuker & Braun 1998b and Rossi & Braun 1997, reproduced as SP 1087 Table 5.1(a) — the fixed-orifice refrigerant-leakage row this rule’s pattern reproduces; the corroboration lineage HP-0001 draws on; Barandier 2023, Barandier & Mendes 2024 — undercharge prevalence on heat pumps (cited by HP-0001); Li & Braun 2007, HVAC&R Research 13(2) — the multiple-simultaneous-fault caveat on single-fault pattern charts; Sibling precedent: HP-0001 (commissioning-set placeholder parameters), RTU-0002 (fixed bands named as a simplification of a regressed baseline), HP-0002 and HP-0003 (defrost and compressor gating as host preconditions)
Operating statesmechanical cooling, compressor running, unit not in defrost — all three host-gated

Preconditions (host-enforced): The compressor must be running and the unit must not be in defrost. Both are host gates on comp_status and defrost_status: with the compressor off all four temperatures equalise and both differences collapse to zero, and a defrost cycle deliberately reverses the circuit, which scrambles every refrigerant-side temperature this rule reads (HP-0002/HP-0003 precedent). The compressor must also have held its current capacity for min_runtime_for_eval (15 min); superheat overshoots for minutes after a start or a capacity step while the expansion valve catches up. evap_sat_temp and cond_sat_temp are host-derived P-T lookups and the 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 be in good contact with the line and insulated from ambient air; an uninsulated liquid-line probe reads high in a hot plant room and fabricates collapsed subcooling. 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 expansion valve is at its control limit

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, against an 11.1 °C compressor-map standard). Set it from the unit’s own charging chart plus a tolerance; 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 — published charging-chart targets for a TXV unit run roughly 8-11 °C at design, and this band is set well under the target so normal load swings do not reach it.
subcooling_two_phase_floor0.5°CtxvSat.tSubcooling below which the expansion-valve inlet is taken to be two-phase and the valve at its control limit. 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 thermostatic valve 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 expansion valve opens further to keep the evaporator fed, and once it runs out of travel the evaporator starves while the condenser loses its liquid seal: superheat climbs and subcooling collapses at the same time. That divergence is the signature — capacity loss on its own says nothing about cause. NIST SP 1087 imposed graded charge faults on a TXV-equipped R410A heat pump in cooling and recorded exactly this pair once the valve saturated (§5.4.3, Table 5.2, zone B), matching the fixed-orifice charts of Breuker & Braun (1998b). This rule forms the two differences from four refrigerant-side temperatures and alarms when both 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):

HP-0004 block graph

The conjunction is the diagnosis, not a noise filter. High superheat with subcooling high is the liquid-line-restriction pattern (SP 1087 Table 5.2, zone B), where refrigerant backs up ahead of the restriction; low subcooling with superheat still normal is a valve that is 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; delayOnInit = true holds that window across a controller restart rather than alarming on the first tick. 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 charge top-up without a leak search buys months, not years
  2. The unit was charged short, at commissioning or after a repair that vented the circuit and was recharged by pressure rather than by weight
  3. Severe condenser airflow restriction — SP 1087’s zone-B chart moves superheat up and subcooling down for this fault too, and it separates only on condensing temperature, which rises rather than falls; this rule cannot see that (see Deviations)
  4. Instrumentation: a P-T derivation configured for the wrong refrigerant, or a liquid-line probe with poor contact or missing insulation. Both fabricate 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 heat pump, all of it compressor electricity, so the avoided-emissions basis is the marginal operating emissions rate (MOER). 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. A site 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 choose which fault-direction chart applies, never to suppress evaluation, and the zone-B chart this rule encodes 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 rather than y…Ok.
  • Fixed bands replace SP 1087’s regressed no-fault baseline. It compares each feature against a third-order polynomial regression on three variables (outdoor and indoor drybulb, indoor dew point); this library’s only regression primitive is a host-fitted line (HP-0001). Charging-chart-style nominal targets stand in — a real simplification, named as one on RTU-0002’s precedent, and the reason bands left at their defaults can alarm on a healthy machine forever.
  • Only the TXV-saturated row of the chart is encoded. In SP 1087’s zone A (valve still in control) undercharge moves no superheat at all — only subcooling, condensing temperature and the two air-side deltas — so a mild charge loss the valve is absorbing reads healthy here. Earlier investigators reported difficulty detecting undercharge reliably below about 40% charge loss (Breuker & Braun 1998b; Stylianou & Nikanpour 1996, via SP 1087 §2).
  • Two features, so severe condenser airflow restriction is not excluded. It shares the zone-B superheat-up/subcooling-down pair and separates on condensing temperature moving up rather than down — a direction test that needs a baseline for the current operating condition — the host-fitted-baseline convention that later shipped RTU-0007. The card names it as diagnosis 3 rather than pretending to rule it out.
  • Grounding is cooling-mode only. SP 1087 tested cooling exclusively, and in heating the coils swap roles, so the sensors sit on different heat exchangers even though the arithmetic is unchanged. operating_states says cooling; a heating-mode instance is an extrapolation from charge-diagnosis practice and needs its own commissioned bands.
  • 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 by vectors.
  • Superheat is measured at the compressor suction, not the evaporator exit. The point dictionary’s suction_temp is a suction-line probe, and SP 1087 notes suction superheat reads higher than evaporator-exit superheat on the same machine, which it attributes to heat transfer within the valve. Its 9 °C zone boundary is an evaporator-exit number fitted to one test rig and is deliberately not carried into this card as a parameter.
  • Compressor, defrost and steady-state gating stay host preconditions. The graph computes the fault given valid data, per SCHEMA.md; comp_status, defrost_status and time-since-capacity-change are not among its inputs. 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 Li & Braun (2007) showed this whole family of charts misreads simultaneous faults, whose feature residuals superpose. Two faults at once can cancel this rule’s pattern or fake it; the diagnosis text is a ranking, not a verdict.
  • persist.delayOnInit = true (CDL default is false), the library’s standing choice. Severity 3 and category: EFFICIENCY_LOSS follow HP-0001 and RTU-0002, the library’s other charge-and-capacity cards; no reference card exists to inherit them from.

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 — the falling edge means “no longer diverging”, nothing more. When yTxvSaturated is true the technician should expect flash gas at the sight glass and should weigh the recovered charge rather than trusting subcooling to confirm the fix. HP-0001 sees the same fault as a COP shortfall without naming it, and HP-0005 is the overcharge branch, which is why the three carry each other in related. The heat-pump-faults playbook orders the on-site work; its step 1 already sends the technician to superheat and subcooling, which is the measurement this rule automates.

Test Vectors

14 scenarios, clock step 300 s over 7200 s.

ScenarioDescription
correctly_charged_unitNominal cooling operation: 11 °C of suction superheat against a 15 °C band and 9 °C of subcooling against a 3 °C band. Neither conjunct holds, and the liquid line is comfortably single-phase.
undercharge_with_liquid_still_subcooledThe signature: 20 °C superheat with subcooling collapsed to 1.5 °C. Both conjuncts hold from t=0, so delayOnInit puts the alarm one alarm_delay in. Subcooling is still above the two-phase floor, so yTxvSaturated stays false — the fault does not depend on it.
high_superheat_with_high_subcooling_is_not_underchargeThe discriminator the subcooling conjunct exists for: 20 °C superheat with subcooling raised to 12 °C 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.
low_subcooling_alone_while_the_valve_compensatesSubcooling down to 1 °C with superheat held at a normal 11 °C — the expansion valve is still doing its job. This is the in-control-zone undercharge pattern, and this 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 °C of superheat, a quarter of a degree over the band, and the alarm matures after 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 °C of subcooling clears the band and the alarm matures.
subcooling_exactly_at_the_two_phase_floorEvaluability-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 °C of subcooling means the liquid line is no longer measurably subcooled, the valve is at its limit, and this is 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.
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 a thermostatic valve hunting after a capacity 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": 300,
    "horizon_s": 7200
  },
  "scenarios": [
    {
      "name": "correctly_charged_unit",
      "description": "Nominal cooling operation: 11 \u00b0C of suction superheat against a 15 \u00b0C band and 9 \u00b0C of subcooling against a 3 \u00b0C 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 \u00b0C superheat with subcooling collapsed to 1.5 \u00b0C. Both conjuncts hold from t=0, so delayOnInit puts the alarm one alarm_delay in. Subcooling is still above the two-phase floor, so yTxvSaturated stays false \u2014 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "high_superheat_with_high_subcooling_is_not_undercharge",
      "description": "The discriminator the subcooling conjunct exists for: 20 \u00b0C superheat with subcooling raised to 12 \u00b0C 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": "low_subcooling_alone_while_the_valve_compensates",
      "description": "Subcooling down to 1 \u00b0C with superheat held at a normal 11 \u00b0C \u2014 the expansion valve is still doing its job. This is the in-control-zone undercharge pattern, and this 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 \u2212 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 \u00b0C of superheat, a quarter of a degree over the band, and the alarm matures after 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "subcooling_exactly_at_the_band",
      "description": "Threshold edge: subcooling is exactly subcooling_low_band (33.0 \u2212 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 \u00b0C 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "subcooling_exactly_at_the_two_phase_floor",
      "description": "Evaluability-flag edge: subcooling is exactly subcooling_two_phase_floor (30.5 \u2212 30.0 = 0.5, binary-exact). The floor test is a strict `<`, so yTxvSaturated is false \u2014 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "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 \u00b0C of subcooling means the liquid line is no longer measurably subcooled, the valve is at its limit, and this is a severe charge loss. yTxvSaturated goes true and yFault still asserts \u2014 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "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": 2700,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3300,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "transient_superheat_excursion_never_alarms",
      "description": "Superheat spikes over its band for 1500 s \u2014 a load step, or a thermostatic valve hunting after a capacity change \u2014 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 \u2014 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": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 3300,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 3900,
          "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
        }
      ]
    }
  ]
}