Skip to content

Commit 89ccaff

Browse files
authored
feat(hx): add hydronic heat-exchanger family (#57)
* feat(hx): add hydronic heat-exchanger family * test(hx): complete boundary and flow matrix coverage * docs(hx): make related fault links reciprocal
1 parent 41f7d56 commit 89ccaff

20 files changed

Lines changed: 3576 additions & 7 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ Here's a real one — AHU-0016, simultaneous heating and cooling:
4040

4141
## What's inside
4242

43-
- **134 verified fault rules** across thirteen equipment families — air handlers,
43+
- **137 verified fault rules** across fourteen equipment families — air handlers,
4444
VAV boxes, fan-powered terminals, rooftop units, heat pumps, chillers, cooling
45-
towers, hot-water plants, fan coils, energy recovery ventilators, pumps, VFDs, and
46-
cross-equipment sensor-health rules. The
45+
towers, hot-water plants, hydronic heat exchangers, fan coils, energy recovery
46+
ventilators, pumps, VFDs, and cross-equipment sensor-health rules. The
4747
[Fault Code Map](https://jscott3201.github.io/open-control-library/registry.html)
4848
lists them all.
4949
- **Point dictionaries** (`points/`) — every canonical point name grounded in

SCHEMA.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ cxf-library/
2222
├── tools/verify/ # Rust harness: loads each rule into the engine, runs vectors
2323
```
2424

25-
Equipment family keys: `ahu`, `vav`, `fpb`, `rtu`, `hp`, `fcu`, `chw`, `hw`, `erv`,
26-
`pmp`, `vfd`, `sys`, `tower`. Fault IDs live in a general namespace:
25+
Equipment family keys: `ahu`, `vav`, `fpb`, `rtu`, `hp`, `fcu`, `chw`, `hw`, `hx`,
26+
`erv`, `pmp`, `vfd`, `sys`, `tower`. Fault IDs live in a general namespace:
2727
`{EQUIP}-{NNNN}` — uppercase family key, four digits, contiguous from `0001`
2828
per family in authoring order. The folder name is the fault ID. The number
2929
carries no semantic meaning; provenance lives in each card's `source:` list.

faults/hx/HX-0001/card.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
schema: cxf-library/fault-card/v1
3+
id: HX-0001
4+
name: Hydronic heat-exchanger effectiveness degradation
5+
equipment: hx
6+
status: verified
7+
phase: 2
8+
method: statistical
9+
severity: 3
10+
category: EFFICIENCY_LOSS
11+
confidence: MEDIUM
12+
estimation_method: BASELINE_COMPARISON
13+
source:
14+
- "EnergyPlus 25.1 Engineering Reference, Heat Exchangers — epsilon-NTU model using both flow-capacity rates and inlet temperatures: https://bigladdersoftware.com/epx/docs/25-1/engineering-reference/heat-exchangers.html"
15+
- "Guelpa and Verda, Applied Energy 258 (2020), DOI 10.1016/j.apenergy.2019.114059 — field fouling detection on 325 district-heating HX substations from primary mass flow and temperatures on both sides"
16+
- "DOE FEMP, Energy Management Information System Capabilities — reduced HX heat transfer from temperature sensors as a condition-based maintenance signal: https://www.energy.gov/cmei/femp/energy-management-information-system-capabilities"
17+
g36: null
18+
clusters: []
19+
suppresses: []
20+
suppressed_by: []
21+
related: [HX-0002, HX-0003]
22+
playbooks: [hydronic-heat-exchanger-faults]
23+
operating_states: "One indirect liquid-to-liquid HX exchanging heat in a settled heating or cooling state, with both branch flows established and a frozen clean/design expected-effectiveness model ready and in domain"
24+
preconditions: "All six physical derivation inputs must describe the same HX: primary/secondary entering/leaving temperatures plus individual branch flows, aligned in time and correctly scaled. The host computes effectiveness only after proving positive finite thermal capacity rates, sufficient entering-temperature separation, configured density/cp for each fluid (including glycol concentration), and agreement of independently calculated side heat rates within commissioned uncertainty. The expected model must be frozen, independently fitted/commissioned, ready, fresh, and in domain for the current flow-capacity ratio, entering temperatures, direction, and control state. Suspend and re-warm after starts, direction/setpoint/pump/valve/stage changes. A common-header flow, duplicated side point, same-window fitted target, imbalance, or invalid denominator means NO_EVAL, not healthy. Steam/phase-change, air/refrigerant, potable, direct-contact, and aggregate-bank service are excluded."
25+
points:
26+
- effectiveness
27+
- effectiveness_expected
28+
outputs:
29+
- name: yFault
30+
description: True after actual effectiveness remains more than effectiveness_allowance below the valid expected value for alarm_delay
31+
- name: yEffectivenessLow
32+
description: Diagnostic sub-condition flag; true when expected minus actual effectiveness strictly exceeds the allowance. False never means NO_EVAL
33+
params:
34+
effectiveness_allowance:
35+
default: 0.125
36+
unit: "1"
37+
description: "NO_PORTABLE_DEFAULT executable placeholder: 0.125 effectiveness points is a binary-exact vector fixture, not a field recommendation. Commission from clean-model error, sensor/fluid-property uncertainty, and the minimum actionable degradation before enabling this rule."
38+
cxf: shortfallHigh.t
39+
alarm_delay:
40+
default: 900.0
41+
unit: s
42+
description: "ADOPTED_TUNABLE 15-minute persistence after the host's independent settling/re-warm gate. Retune to the installation time constant and data cadence; no cited source establishes a universal duration."
43+
cxf: persist.delayTime
44+
energy_impact:
45+
affected_subsystem: Hydronic exchange plus upstream heating/cooling and pumping needed to replace lost transfer
46+
savings_range: Site-specific; the field source estimates about 1.6% primary-energy reduction across its whole district network from cleaning detected fouling, not a per-HX savings claim
47+
climate_sensitivity: both
48+
runtime_estimation: "lost_kw = max(effectiveness_expected - effectiveness, 0) × min(C_primary, C_secondary) × abs(primary_entering_temp - secondary_entering_temp), evaluated only with the same validated host derivation"
49+
emissions:
50+
scope: "1+2"
51+
method: PROXY_EMISSIONS
52+
verified:
53+
engine_rev: e2ff2f8
54+
content_id: "cxf:fnv1a128:95d59df8187626eb12cc97061e4b7f9b"
55+
date: 2026-08-20
56+
---
57+
58+
## Description
59+
60+
An indirect liquid heat exchanger loses effectiveness when fouling, scale,
61+
blocked channels, internal bypass, wrong fluid properties, or hydraulic changes
62+
reduce the heat it moves for the opportunity available. This rule compares a
63+
host-validated actual thermal effectiveness with a frozen clean/design expected
64+
value for the same operating condition. It reports degradation, not a root
65+
cause and not a raw "approach" temperature.
66+
67+
The four-port point identity matters as much as the arithmetic. Primary and
68+
secondary are fixed topology labels; heating usually makes signed transfer
69+
positive and cooling negative. The host converts both directions to a positive
70+
effectiveness before the graph sees them.
71+
72+
## Detection Logic
73+
74+
```text
75+
shortfall = effectiveness_expected - effectiveness
76+
yEffectivenessLow = shortfall > effectiveness_allowance
77+
yFault = yEffectivenessLow continuously for alarm_delay
78+
```
79+
80+
![HX-0001 block graph](diagram.svg)
81+
82+
The graph has no `Divide`. The host publishes `effectiveness` only after safe
83+
denominator, fluid-property, timestamp, and side-energy-balance checks. A
84+
denominator guard downstream of a division would not prevent that division
85+
from evaluating; moving the validated thermodynamic derivation to the host
86+
also supports water/glycol properties the CXF graph does not carry.
87+
88+
Both comparisons use finite dimensionless scalars and the threshold is strict.
89+
`yEffectivenessLow` is immediate diagnostic evidence; only `yFault` is delayed.
90+
91+
## Possible Diagnoses
92+
93+
1. Plate/tube fouling, scale, biological film, or blocked channels.
94+
2. Internal gasket/bypass leakage or incorrect HX piping.
95+
3. Insufficient or maldistributed flow not caught by the commissioned floors.
96+
4. Degraded or misconfigured glycol concentration/fluid properties.
97+
5. Temperature/flow sensor bias, time misalignment, or swapped side/location.
98+
6. Expected model drift, wrong domain, or baseline trained on abnormal data.
99+
100+
## Energy Impact
101+
102+
EFFICIENCY_LOSS with BASELINE_COMPARISON and MEDIUM confidence. Lost transfer
103+
must be replaced by upstream boilers, chillers, heat pumps, district energy, or
104+
longer pumping. The estimator uses the same validated available-rate basis as
105+
the effectiveness calculation; this two-point graph alone cannot produce kW.
106+
Guelpa and Verda's 1.6% is a network-wide expected benefit from a cleaning
107+
program across 325 substations, not a savings range to assign to one alarm.
108+
109+
## Emissions Impact
110+
111+
Scope 1+2, PROXY_EMISSIONS. Apply the marginal emissions rate of the actual
112+
replacement heat source and electricity used while the fault is active. Do not
113+
infer fuel/electric split from transfer direction alone.
114+
115+
## Deviations
116+
117+
- **The thermodynamic ratio is host-derived.** EnergyPlus documents the
118+
epsilon-NTU physics, but the repository graph intentionally compares two safe
119+
scalars instead of dividing inside CXF. This is a safety and fluid-property
120+
adaptation, not a claim that the host model is standardized.
121+
- **`effectiveness_allowance = 0.125` is not portable.** No source supplies a
122+
universal threshold. The exact binary value makes strict-boundary vectors
123+
unambiguous; deployment must replace it before enabling evaluation.
124+
- **The field method is precedent, not a transcribed algorithm.** Guelpa and
125+
Verda use a calibrated fouling workflow under variable district-heating
126+
conditions. This card keeps the baseline/error-domain obligation but does not
127+
claim to reproduce their full method.
128+
- **No in-graph readiness flag.** Baseline/domain, denominator, and balance
129+
validity depend on provenance and configuration beyond two boundary points;
130+
they are mandatory host NO_EVAL gates.
131+
- **No suppression.** HX-0002 may explain why HX-0001 is unevaluable, but rule
132+
IDs are not equipment-instance scoped. A host gates the same instance rather
133+
than globally suppressing every HX-0001 when any HX-0002 is active.
134+
- **Initial scope excludes steam.** Phase change needs a different capacity and
135+
topology contract even though some trade usage calls it hydronic.

faults/hx/HX-0001/diagram.svg

Lines changed: 30 additions & 0 deletions
Loading

faults/hx/HX-0001/rule.cxf.jsonld

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
{
2+
"@context": {
3+
"S231": "http://data.ashrae.org/S231P#",
4+
"base": "urn:cxf-library:hx-0001#"
5+
},
6+
"@graph": [
7+
{
8+
"@id": "urn:cxf-library:hx-0001#hx_0001",
9+
"@type": "S231:Block",
10+
"S231:label": "hx_0001",
11+
"S231:containsBlock": [
12+
{
13+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall"
14+
},
15+
{
16+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh"
17+
},
18+
{
19+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist"
20+
}
21+
],
22+
"S231:hasInput": [
23+
{
24+
"@id": "urn:cxf-library:hx-0001#hx_0001.effectiveness"
25+
},
26+
{
27+
"@id": "urn:cxf-library:hx-0001#hx_0001.effectiveness_expected"
28+
}
29+
],
30+
"S231:hasOutput": [
31+
{
32+
"@id": "urn:cxf-library:hx-0001#hx_0001.yFault"
33+
},
34+
{
35+
"@id": "urn:cxf-library:hx-0001#hx_0001.yEffectivenessLow"
36+
}
37+
]
38+
},
39+
{
40+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall",
41+
"@type": "urn:cxf-library:hx-0001#Buildings.Controls.OBC.CDL.Reals.Subtract",
42+
"S231:label": "shortfall",
43+
"S231:hasInput": [
44+
{
45+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u1"
46+
},
47+
{
48+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u2"
49+
}
50+
],
51+
"S231:hasOutput": {
52+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.y"
53+
}
54+
},
55+
{
56+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u1",
57+
"@type": "S231:RealInput",
58+
"S231:isOfDataType": {
59+
"@id": "S231:Real"
60+
}
61+
},
62+
{
63+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u2",
64+
"@type": "S231:RealInput",
65+
"S231:isOfDataType": {
66+
"@id": "S231:Real"
67+
}
68+
},
69+
{
70+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.y",
71+
"@type": "S231:RealOutput",
72+
"S231:isOfDataType": {
73+
"@id": "S231:Real"
74+
},
75+
"S231:isConnectedTo": {
76+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.u"
77+
}
78+
},
79+
{
80+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh",
81+
"@type": "urn:cxf-library:hx-0001#Buildings.Controls.OBC.CDL.Reals.GreaterThreshold",
82+
"S231:label": "shortfallHigh",
83+
"S231:hasParameter": {
84+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.t"
85+
},
86+
"S231:hasInput": {
87+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.u"
88+
},
89+
"S231:hasOutput": {
90+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.y"
91+
}
92+
},
93+
{
94+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.t",
95+
"S231:value": {
96+
"@value": "0.125",
97+
"@type": "http://www.w3.org/2001/XMLSchema#double"
98+
}
99+
},
100+
{
101+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.u",
102+
"@type": "S231:RealInput",
103+
"S231:isOfDataType": {
104+
"@id": "S231:Real"
105+
}
106+
},
107+
{
108+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfallHigh.y",
109+
"@type": "S231:BooleanOutput",
110+
"S231:isOfDataType": {
111+
"@id": "S231:Boolean"
112+
},
113+
"S231:isConnectedTo": [
114+
{
115+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.u"
116+
},
117+
{
118+
"@id": "urn:cxf-library:hx-0001#hx_0001.yEffectivenessLow"
119+
}
120+
]
121+
},
122+
{
123+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist",
124+
"@type": "urn:cxf-library:hx-0001#Buildings.Controls.OBC.CDL.Logical.TrueDelay",
125+
"S231:label": "persist",
126+
"S231:hasParameter": [
127+
{
128+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.delayTime"
129+
},
130+
{
131+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.delayOnInit"
132+
}
133+
],
134+
"S231:hasInput": {
135+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.u"
136+
},
137+
"S231:hasOutput": {
138+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.y"
139+
}
140+
},
141+
{
142+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.delayTime",
143+
"S231:value": {
144+
"@value": "900.0",
145+
"@type": "http://www.w3.org/2001/XMLSchema#double"
146+
}
147+
},
148+
{
149+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.delayOnInit",
150+
"S231:value": {
151+
"@value": "true",
152+
"@type": "http://www.w3.org/2001/XMLSchema#boolean"
153+
}
154+
},
155+
{
156+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.u",
157+
"@type": "S231:BooleanInput",
158+
"S231:isOfDataType": {
159+
"@id": "S231:Boolean"
160+
}
161+
},
162+
{
163+
"@id": "urn:cxf-library:hx-0001#hx_0001.persist.y",
164+
"@type": "S231:BooleanOutput",
165+
"S231:isOfDataType": {
166+
"@id": "S231:Boolean"
167+
},
168+
"S231:isConnectedTo": {
169+
"@id": "urn:cxf-library:hx-0001#hx_0001.yFault"
170+
}
171+
},
172+
{
173+
"@id": "urn:cxf-library:hx-0001#hx_0001.effectiveness",
174+
"@type": "S231:RealInput",
175+
"S231:isOfDataType": {
176+
"@id": "S231:Real"
177+
},
178+
"S231:isConnectedTo": {
179+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u2"
180+
}
181+
},
182+
{
183+
"@id": "urn:cxf-library:hx-0001#hx_0001.effectiveness_expected",
184+
"@type": "S231:RealInput",
185+
"S231:isOfDataType": {
186+
"@id": "S231:Real"
187+
},
188+
"S231:isConnectedTo": {
189+
"@id": "urn:cxf-library:hx-0001#hx_0001.shortfall.u1"
190+
}
191+
},
192+
{
193+
"@id": "urn:cxf-library:hx-0001#hx_0001.yFault",
194+
"@type": "S231:BooleanOutput",
195+
"S231:isOfDataType": {
196+
"@id": "S231:Boolean"
197+
}
198+
},
199+
{
200+
"@id": "urn:cxf-library:hx-0001#hx_0001.yEffectivenessLow",
201+
"@type": "S231:BooleanOutput",
202+
"S231:isOfDataType": {
203+
"@id": "S231:Boolean"
204+
}
205+
}
206+
]
207+
}

0 commit comments

Comments
 (0)