RTU-0008 — Refrigerant undercharge — superheat/subcooling divergence
| Status | verified — engine e2ff2f8, cxf:fnv1a128:6185ddcde7a0e68b845e3af5bcea5b05, 2026-08-18 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | EFFICIENCY_LOSS |
| Confidence | MEDIUM |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | HP-0004, RTU-0009, RTU-0007, RTU-0001, RTU-0010, RTU-0011 |
| Playbooks | rtu-compressor-refrigerant |
| Source | Library-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 states | mechanical 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_delayyTxvSaturated— 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:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
superheat_high_band | 15.0 | °C | shHigh.t | Suction 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_band | 3.0 | °C | scLow.t | Liquid 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_floor | 0.5 | °C | txvSat.t | Subcooling 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_delay | 1800.0 | s | persist.delayTime | Continuous 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):
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
- 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
- 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
- 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
- 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
yFaulton it would silence the rule exactly when the liquid line has flashed to two-phase — severe undercharge, not missing data — so the flag is namedyTxvSaturated, noty…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
outputssays 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_loadpins 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_underchargepins 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_statesneeds no mode split andpreconditionsno 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_statusand time-since-stage-change are not among its inputs, and the 30-minutealarm_delaydoes 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. CDLRealshas noGreaterEqualorLessEqual, 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 isfalse), the library’s standing choice. Severity 3 andcategory: EFFICIENCY_LOSSfollow HP-0004 and RTU-0002; the namespaceurn: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 invectors.jsonis 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.
| Scenario | Description |
|---|---|
correctly_charged_unit | 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. |
undercharge_with_liquid_still_subcooled | 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. |
high_superheat_with_high_subcooling_is_not_undercharge | 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. |
fixed_orifice_unit_at_low_indoor_load | 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. |
low_subcooling_alone_while_the_valve_compensates | 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. |
superheat_exactly_at_the_band | 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. |
superheat_just_above_the_band | 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. |
subcooling_exactly_at_the_band | 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. |
subcooling_just_below_the_band | Threshold edge from the other side: 2.75 degC of subcooling clears the band and the alarm matures. |
subcooling_exactly_at_the_two_phase_floor | 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. |
subcooling_below_the_two_phase_floor | 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. |
condenser_restriction_reads_as_undercharge | 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. |
timer_starts_when_the_second_conjunct_arrives | 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. |
transient_superheat_excursion_never_alarms | 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. |
alarm_clears_after_the_charge_is_restored | 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. |
idle_unit_reads_healthy_and_the_flag_reads_saturated | 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. |
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
}
]
}
]
}