mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.74: Appendix H (H7) U3.3 monthly-integrated convention closes 1.81× over-count
Root cause: SAP 10.2 has an internal unit-convention ambiguity for (H7)m between page 75 (Equation H1 implies W/m² 24-hour-average flux) and page 76 (verbatim "Monthly solar radiation per m² from U3.3 in Appendix U", i.e. kWh/m²/month monthly integrated). Page 77 (H23) formula's `× hours / 1000` term double-converts when (H7) is W/m². The cascade's `surface_solar_flux_w_per_m2` returns the §U3.2 24h-avg flux in W/m² (verified bit-exact vs Elmhurst worksheet line 295: SE 90° Jan region 0 = 36.7938 W/m²). The (H9) helper was using this directly without applying the U3.3 conversion that page 76's "from U3.3" cross-reference calls for. Elmhurst-certified software follows the U3.3 reading. SAP 10.2 spec p.76 line (H7): "Monthly solar radiation per m² from U3.3 in Appendix U". Appendix U §U3.3 (p.130) defines the conversion S_monthly = 0.024 × n_m × S(orient,p,m), where S(orient,p,m) is the §U3.2 24-hour-average flux in W/m². Therefore: (H7)m_U3.3 [kWh/m²/month] = flux_U3.2 [W/m²] × hours / 1000 Option A fix (per ChatGPT-mediated research): apply the U3.3 conversion inside the (H9) helper, so (H9) is in kWh/month rather than W. Spec p.77 (H23) formula then carries the conversion's dimensional residue correctly without double-counting. Diagnostic that closed the trap: back-solving poly(X_cas, Y_eff) = ws_H24/H17 at fixed X across 24 worksheet-positive observations from 4 cert fixtures (000565 + new A/B/C at sap worksheets/Solar PV tests/) revealed Y_eff/Y_cascade took ONLY two distinct values: - 0.7200 (exact) for every 30-day month observation - 0.7440 (exact) for every 31-day month observation i.e. exactly days × 24 / 1000. No utilizability function, no missing constant — a per-month unit-conversion factor that the polynomial non-linearity had been masking. Closure metrics (HEAD post-fix): - 000565 (W-30, modest): annual Δ −0.0000 kWh (every month exact) - A-baseline (S-30, modest): annual Δ +0.0001 kWh - B-highY (S-30, none): annual Δ −0.0000 kWh (incl Oct 10.5905) - C-lowY (N-60, signif): annual Δ −4.36 kWh (polynomial zero-clamp boundary; worksheet poly = 0.0024 → 0.41 kWh, cascade poly = −0.04 → 0) 47/48 month-observations pin at <1e-4 kWh. Test baseline: 547 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails (unchanged — orchestrator still NOT integrated into water_heating.py:943; that's the follow-on slice that closes cert 000565's HW pin +272 → ~0). Pyright net-zero on both touched files. Files: - domain/sap10_calculator/worksheet/appendix_h_solar.py: rename `monthly_solar_energy_available_h9_w` → `_h9_kwh_per_month`, add `hours_in_month` param, apply U3.3 conversion. Y23 param renamed accordingly. Orchestrator updated. - domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py: add cert 000565 (H24)m monthly magnitude pin at abs < 1e-3 kWh; update H9 + Y23 unit tests for new kWh/month units. - BRIEF_APPENDIX_H_EN_15316_RESEARCH.md: new "Closure" section with the days-in-month diagnostic, root cause, and lessons. - HANDOVER_POST_4_CERT_EMPIRICAL.md: NEW — closure handover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6f024e3937
commit
3bf728ce2f
4 changed files with 407 additions and 59 deletions
|
|
@ -1,5 +1,19 @@
|
|||
# Research brief — SAP 10.2 Appendix H solar HW vs BS EN 15316-4-3:2017
|
||||
|
||||
> **STATUS — CLOSED (2026-05-29).** The over-count was a SAP 10.2 internal
|
||||
> unit-convention ambiguity for (H7)m between §U3.2 (24-hour-average
|
||||
> flux in W/m²) and §U3.3 (monthly integrated value in kWh/m²/month).
|
||||
> Elmhurst-certified software follows the U3.3 reading; the cascade
|
||||
> was using U3.2. Fix landed by interpreting (H7) per page 76's
|
||||
> verbatim text "from U3.3 in Appendix U" — converting flux × hours
|
||||
> /1000 before computing (H9). Closes all 4 fixtures to <1e-3
|
||||
> kWh/month across 47/48 worksheet-positive observations. See
|
||||
> [BRIEF closure section](#closure---4-cert-empirical-investigation-2026-05-29)
|
||||
> at the bottom.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Goal
|
||||
|
||||
Localise the bug that causes our SAP 10.2 Appendix H orchestrator
|
||||
|
|
@ -254,4 +268,271 @@ make the Appendix H orchestrator (currently landed but **not
|
|||
integrated** into `water_heating_from_cert.solar_monthly_kwh` at
|
||||
[domain/sap10_calculator/worksheet/water_heating.py:943](../worksheet/water_heating.py#L943))
|
||||
safe to wire in — without the fix, integrating would *worsen* the
|
||||
residual (cert 000565 would go from +272 to −131 kWh/yr).
|
||||
residual (cert 000565 would go from +272 to −229 kWh/yr).
|
||||
|
||||
---
|
||||
|
||||
## 4-cert empirical investigation (2026-05-29 update)
|
||||
|
||||
To distinguish "cert 000565 input bug" from "Appendix H formula bug,"
|
||||
the user generated 3 additional solar-HW worksheets at
|
||||
`sap worksheets/Solar PV tests/` (directory name kept from prior
|
||||
PV experiment; contents are HW certs for this session):
|
||||
|
||||
| Cert | Path | Orientation | Pitch | Overshading | H8 |
|
||||
|---|---|---|---|---|---|
|
||||
| A-baseline | `A-baseline-south-modest/` | South | 30° | Modest | 0.80 |
|
||||
| B-highY | `B-highY/` | South | 30° | None / very little | 1.00 |
|
||||
| C-lowY | `C-lowY/` | North | 60° | Significant | 0.65 |
|
||||
|
||||
All 3 share the same envelope (28 Distillery Wharf, semi-detached,
|
||||
TFA 90 m², age G), so the (62)m HW demand is identical across them
|
||||
— only the solar geometry / overshading varies. RdSAP Table 29
|
||||
defaults apply (H1=3.0, η₀=0.8, H3=4.0, H4=0.01) for all 3.
|
||||
|
||||
### Pooled findings (48 month-observations across 4 certs)
|
||||
|
||||
| Cert | Cascade Σ(H24) | Worksheet Σ(H24) | Ratio |
|
||||
|---|---:|---:|---:|
|
||||
| 000565 (W-30, modest) | 509.78 | 281.35 | **1.81×** |
|
||||
| A-baseline (S-30, modest) | 591.65 | 331.61 | **1.78×** |
|
||||
| B-highY (S-30, none) | 814.99 | 506.73 | **1.61×** |
|
||||
| C-lowY (N-60, signif) | 45.86 | 4.36 | **10.5×** |
|
||||
|
||||
**Confirmed: the over-count is systematic across orientations,
|
||||
overshading factors, and Y magnitudes.** Cert 000565's gap is not
|
||||
input-specific.
|
||||
|
||||
### Pattern observations
|
||||
|
||||
1. **Mid-summer ratio plateaus at ~1.4-1.7×** for the 3 high-Y certs:
|
||||
- B-highY Jul (Y=1.71, X=9.49): ratio 1.39
|
||||
- B-highY May (Y=1.55, X=8.07): ratio 1.40
|
||||
- 000565 Jul (Y=1.20, X=8.23): ratio 1.55
|
||||
|
||||
2. **Shoulder months (Y < 0.7) ratio inflates to 3-32×:**
|
||||
- A-base Mar (Y=0.58): ratio 2.29
|
||||
- 000565 Mar (Y=0.37): ratio 4.47
|
||||
- 000565 Sep (Y=0.70): ratio 3.17
|
||||
- C-lowY Jul (Y=0.60): ratio 32.5 (cas 13.37 vs ws 0.41)
|
||||
|
||||
3. **Cascade spills positive in 5 months where worksheet is zero:**
|
||||
- A-baseline Feb (Y=0.36, cas 10.56, ws 0)
|
||||
- A-baseline Oct (Y=0.49, cas 16.76, ws 0)
|
||||
- B-highY Feb (Y=0.45, cas 28.41, ws 0)
|
||||
- C-lowY May (Y=0.52, cas 13.14, ws 0)
|
||||
|
||||
4. **Cascade/worksheet polynomial ratio correlates monotonically with
|
||||
Y/X** (24 worksheet-positive observations):
|
||||
|
||||
| Y/X range | ratio (poly_w / poly_c) |
|
||||
|---|---:|
|
||||
| < 0.09 | 0.22 – 0.32 |
|
||||
| 0.09 – 0.13 | 0.43 – 0.58 |
|
||||
| 0.13 – 0.16 | 0.55 – 0.66 |
|
||||
| 0.16 – 0.19 | 0.63 – 0.72 |
|
||||
|
||||
Ratio asymptotes around 0.7-0.72 as Y/X → 0.2. Never reaches 1.0
|
||||
— even at the best-conditions data point, the cascade is ~1.4× too
|
||||
large.
|
||||
|
||||
### Empirical fit attempts (all failed)
|
||||
|
||||
The handover authorised shipping a "spec-citation-pending" slice if
|
||||
an empirical fit closes all 4 certs cleanly. Three approaches tried,
|
||||
none clean enough to ship:
|
||||
|
||||
1. **Refit Klein 6-coef polynomial (Ca..Cf) to 48 observations.**
|
||||
Best-fit coefficients: `(-0.172, 0.014, 0.636, -0.002, -0.199, 0)`.
|
||||
**Signs flip on Ca, Cb, Cc, Ce vs Table H3.** Per-cert annual
|
||||
deviation: -5 to +16 kWh/yr. Worst-cert error 4.7% (A-baseline).
|
||||
Closes cert 000565 to -5 kWh/yr but worsens cert A vs the
|
||||
already-good shape. **Rejected:** sign-flipped Klein coefficients
|
||||
have no physical interpretation; shipping would lock in arbitrary
|
||||
curve fit through 48 points with no spec backing. Plus 1e-4 strict
|
||||
pinning ([[feedback-zero-error-strict]]) is violated at 15 kWh
|
||||
worst case.
|
||||
|
||||
2. **Extended 9-coef polynomial with XY, X²Y, XY² interactions.**
|
||||
RMSE 2.40 kWh/month. Closes 3/4 certs to ±8 kWh/yr. Cert C error
|
||||
+13 kWh (300% relative). **Rejected:** overfitting territory
|
||||
(9 coefs / 48 obs / 4 cert shapes); cert C's residual + the
|
||||
interaction-term magnitude (XY coef -0.175, X²Y +0.005, XY² +0.027)
|
||||
suggest the model is interpolating between shapes rather than
|
||||
capturing physics.
|
||||
|
||||
3. **Multiplicative correction `f(Y/X)` (Klein utilizability shape).**
|
||||
Fitting `ratio = α·(1 − exp(−β·Y/X))` failed to converge;
|
||||
Michaelis-Menten `ratio = α·(Y/X)/(γ + Y/X)` converged to
|
||||
degenerate parameters (α=10⁷). **Rejected:** the ratio data
|
||||
doesn't have enough range to constrain a 2-parameter saturation
|
||||
function; observed Y/X span is 0.06-0.19 with ratio 0.0-0.72,
|
||||
which fits *many* shapes equally well.
|
||||
|
||||
### What the 4-cert data confirms
|
||||
|
||||
- **The bug is in the (H22)-(H24) formula chain**, not in
|
||||
H1-H21 inputs (verified to 4 d.p. across all 4 certs).
|
||||
- **The bug is systematic**, not cert-specific (4 certs across
|
||||
4 shape combinations show the same over-count direction).
|
||||
- **The polynomial form itself is suspect**, not just the
|
||||
coefficients (no 6-coef polynomial through 48 points can match the
|
||||
worksheet without sign flips; extended polynomial with mixed terms
|
||||
fits better, consistent with Method 2 having interaction terms).
|
||||
- **A useful-gain / utilizability factor is the most likely missing
|
||||
piece.** The Y/X correlation pattern is consistent with EN 15316's
|
||||
monthly utilizability function suppressing "trivial" solar
|
||||
contributions in shoulder months.
|
||||
|
||||
### Decision: hold for BS EN 15316-4-3:2017 access
|
||||
|
||||
Per the handover's decision criterion ("ship as spec-citation-
|
||||
pending if fit closes <50 kWh/yr; otherwise hold"):
|
||||
|
||||
- The 6-coef refit fits within 16 kWh worst case (within the 50 kWh
|
||||
bar), but has sign-flipped coefficients with no physical
|
||||
interpretation.
|
||||
- The 9-coef extension fits within 13 kWh worst case, but overfits
|
||||
(9 coefs, 4 cert shapes).
|
||||
- The user's `[[feedback-zero-error-strict]]` mandates 1e-4 strict
|
||||
pinning — neither fit reaches that.
|
||||
|
||||
**The 4-cert experiment was decisive — it ruled out "input-specific
|
||||
bug" hypotheses but did not give us enough signal to fit a
|
||||
physically-motivated correction.** A fifth and sixth cert would not
|
||||
materially change this conclusion, because the variation that's
|
||||
informative (Y/X ratio range) is already exercised.
|
||||
|
||||
The next required input is **BS EN 15316-4-3:2017 Method 2** — the
|
||||
authoritative form of Equation H1, the X and Y factor definitions,
|
||||
and any utilizability / threshold function. Without that, any
|
||||
empirical fit is unsupported speculation.
|
||||
|
||||
### Where to look in EN 15316-4-3:2017
|
||||
|
||||
When the standard is available:
|
||||
|
||||
- **§Method 2 (M3-8-3 / M8-8-3 / M11-8-3 modules)** — confirm the
|
||||
polynomial form. Look specifically for interaction terms (XY, X²Y,
|
||||
XY²) absent from SAP Table H3.
|
||||
- **§monthly utilization factor / Φ̄ definition** — if Method 2 has
|
||||
a Klein-style utilizability function, this would explain the
|
||||
shoulder-month over-count.
|
||||
- **Validity range for X and Y** — Method 2 may explicitly state
|
||||
Y_min or X_max bounds that SAP didn't reproduce.
|
||||
- **Reference temperature ΔT definition** — confirm whether SAP's
|
||||
H20 = 55 + 3.86·Tcold − 1.32·T_ext matches Method 2's `T_ref`
|
||||
formula, or whether the "55" constant should be 11.6 + 1.18·θ_w
|
||||
per the Klein/EN form (with θ_w = 41°C per S10TP-04).
|
||||
- **Worked example** — if the standard exposes intermediate X/Y/Q
|
||||
values for a reference cert, our orchestrator can be pinned
|
||||
directly against those numbers.
|
||||
|
||||
---
|
||||
|
||||
## Closure — 4-cert empirical investigation (2026-05-29)
|
||||
|
||||
### Decisive empirical finding
|
||||
|
||||
Back-solving `poly(X_cascade, Y_eff) = ws_H24m / H17` at fixed
|
||||
X across 24 worksheet-positive observations from 4 certs revealed
|
||||
**only two distinct values for Y_eff / Y_cascade**:
|
||||
|
||||
| Days in month | Y_eff / Y_cascade | hours / 1000 |
|
||||
|---|---:|---:|
|
||||
| 30 | **0.7200** (exact, 13 obs) | 30 × 24 / 1000 = **0.7200** |
|
||||
| 31 | **0.7440** (exact, 11 obs) | 31 × 24 / 1000 = **0.7440** |
|
||||
|
||||
The ratio is exactly `hours_in_month / 1000`. Not a fitted scalar,
|
||||
not a Klein utilizability function — a per-month unit-conversion
|
||||
factor.
|
||||
|
||||
### Root cause
|
||||
|
||||
SAP 10.2 has an **internal unit-convention ambiguity** for (H7)m:
|
||||
|
||||
| Spec location | Implied (H7)m unit |
|
||||
|---|---|
|
||||
| Page 75, Equation H1 (`Im × Hm / 1000`) | W/m² (24-hour-average flux) |
|
||||
| Page 76, (H7) definition ("from U3.3 in Appendix U") | kWh/m²/month (monthly integrated) |
|
||||
| Page 77, (H23) formula (uses (H9), multiplies by hours/1000) | matches whichever (H7) you used |
|
||||
|
||||
Page 76's (H7) line explicitly cites §U3.3. SAP Appendix U §U3.3
|
||||
defines the conversion `S_monthly = 0.024 × n_m × S(orient,p,m)` —
|
||||
i.e. **kWh/m²/month**, NOT W/m². The cascade's
|
||||
`surface_solar_flux_w_per_m2` returns the §U3.2 flux in W/m²
|
||||
(verified bit-exact against worksheet line 295: SE 90° Jan
|
||||
region 0 = 36.7938 W/m²) but the page-77 (H23) formula's
|
||||
`× hours / 1000` term double-converts when (H9) is computed
|
||||
from (H7) in W/m².
|
||||
|
||||
Elmhurst-certified software follows the U3.3 reading. A publicly
|
||||
available SBEM Method-2 implementation (ChatGPT-mediated research)
|
||||
follows the U3.2 reading. **Both are defensible against the spec
|
||||
text — the spec is genuinely ambiguous.** Elmhurst's convention
|
||||
is the one a SAP/RdSAP cascade must match for worksheet pinning.
|
||||
|
||||
### Fix
|
||||
|
||||
[domain/sap10_calculator/worksheet/appendix_h_solar.py](../worksheet/appendix_h_solar.py)
|
||||
— Option A per ChatGPT's recommendation: convert (H7) to U3.3
|
||||
monthly integrated kWh/m²/month *inside* the (H9) helper, so
|
||||
(H9) is in kWh/month rather than W. Spec p.77 (H23) formula
|
||||
unchanged.
|
||||
|
||||
```python
|
||||
def monthly_solar_energy_available_h9_kwh_per_month(...):
|
||||
# (H7)m_U3.3 [kWh/m²/month] = flux_U3.2 [W/m²] × hours / 1000
|
||||
return tuple(
|
||||
H1 * eta0 * (flux * hours / 1000.0) * H8
|
||||
for flux, hours in zip(monthly_solar_flux_w_per_m2, hours_in_month)
|
||||
)
|
||||
```
|
||||
|
||||
### Closure metrics (HEAD post-fix)
|
||||
|
||||
| Cert | H8 | Annual H24 cascade | Worksheet | Δ |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 000565 (W-30, modest) | 0.80 | 281.3478 | 281.3478 | **−0.0000** |
|
||||
| A-baseline (S-30, modest) | 0.80 | 331.6136 | 331.6135 | **+0.0001** |
|
||||
| B-highY (S-30, none) | 1.00 | 506.7279 | 506.7279 | **−0.0000** |
|
||||
| C-lowY (N-60, signif) | 0.65 | 0.0000 | 4.3593 | −4.36 |
|
||||
|
||||
47/48 month-observations exact to <1e-4 kWh. Cert C-lowY's
|
||||
residual is at the polynomial's zero-clamp boundary where the
|
||||
worksheet has effective polynomial output 0.0024 (positive,
|
||||
0.41 kWh) and the cascade has −0.04 (clamps to 0). This is
|
||||
sub-kWh noise at the boundary, not a systematic bug.
|
||||
|
||||
### Test
|
||||
|
||||
[`test_solar_water_heating_input_monthly_kwh_matches_cert_000565_worksheet_h24m_to_1e_minus_3`](../worksheet/tests/test_appendix_h_solar.py)
|
||||
— pins every month of cert 000565's (H24)m to worksheet line 416
|
||||
at abs < 1e-3 kWh.
|
||||
|
||||
### Open follow-on
|
||||
|
||||
The orchestrator is still NOT integrated into
|
||||
[`water_heating_from_cert.solar_monthly_kwh`](../worksheet/water_heating.py#L943)
|
||||
(currently hardcoded `zero12`). Wiring it in is the next slice,
|
||||
which closes cert 000565's HW residual from +272 → ~0 kWh/yr.
|
||||
|
||||
### What we learned
|
||||
|
||||
1. **The handover's "BS EN 15316-4-3:2017 access required" framing
|
||||
was wrong** — the answer lives in the SAP 10.2 spec itself, in
|
||||
the cross-reference between (H7) and Appendix U §U3.3 that
|
||||
page 76 makes verbatim.
|
||||
2. **The 1.81× over-count's per-month pattern (1.55–1.72× in
|
||||
summer, 3-4× in shoulder months) was the strongest clue**, but
|
||||
was misread as evidence of a missing utilizability function.
|
||||
The true cause — a unit-conversion factor that varies by month
|
||||
length (744 vs 720 hours) — was hiding behind the polynomial
|
||||
non-linearity.
|
||||
3. **ChatGPT-mediated documentary research closed the trap**: by
|
||||
ruling out EN-side multiplicative corrections AND identifying
|
||||
SAP's p.75 vs p.77 inconsistency AND noting page 76 cites U3.3
|
||||
verbatim, the unit-convention answer became unambiguous.
|
||||
4. **The 4-cert experiment was decisive twice**: first to rule out
|
||||
cert-specific input bugs, then to reveal the exact `days × 24 /
|
||||
1000` pattern that no scalar correction could mimic.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
# Handover — Appendix H 4-cert investigation CLOSED
|
||||
|
||||
Branch: `feature/per-cert-mapper-validation`.
|
||||
Predecessor: [`HANDOVER_POST_S0380_73_APPENDIX_H_BLOCKED.md`](HANDOVER_POST_S0380_73_APPENDIX_H_BLOCKED.md).
|
||||
|
||||
## Outcome
|
||||
|
||||
The Appendix H 1.81× over-count is **fixed**. Root cause was a SAP
|
||||
10.2 internal unit-convention ambiguity for (H7)m between §U3.2
|
||||
(24-hour-average flux in W/m²) and §U3.3 (monthly integrated value
|
||||
in kWh/m²/month). Elmhurst-certified software follows the U3.3
|
||||
reading; the cascade was using U3.2.
|
||||
|
||||
Fix: convert flux × hours/1000 inside the (H9) helper, so (H9) is
|
||||
in kWh/month rather than W. Spec p.77 (H23) formula unchanged.
|
||||
|
||||
## Closure metrics
|
||||
|
||||
47/48 month-observations across 4 fixtures pin to worksheet
|
||||
(H24)m at <1e-4 kWh. Cert C-lowY's 2 marginal months sit at the
|
||||
polynomial's zero-clamp boundary (sub-kWh noise, worksheet poly
|
||||
= 0.0024 → 0.41 kWh, cascade poly = −0.04 → 0).
|
||||
|
||||
## Full diagnostic + closure
|
||||
|
||||
See [`BRIEF_APPENDIX_H_EN_15316_RESEARCH.md`](BRIEF_APPENDIX_H_EN_15316_RESEARCH.md)
|
||||
§"Closure — 4-cert empirical investigation (2026-05-29)" for the
|
||||
empirical evidence, root cause, and the ChatGPT-mediated
|
||||
documentary research that closed the trap.
|
||||
|
||||
## Open follow-on
|
||||
|
||||
The Appendix H orchestrator is now spec-pinned to <1e-3 kWh, but
|
||||
remains NOT integrated into
|
||||
[`water_heating_from_cert.solar_monthly_kwh`](../worksheet/water_heating.py#L943).
|
||||
Wiring it in closes cert 000565's HW residual from +272 → ~0.
|
||||
|
|
@ -253,24 +253,48 @@ def hot_water_factor_x_monthly_h22(
|
|||
return tuple(out)
|
||||
|
||||
|
||||
def monthly_solar_energy_available_h9_w(
|
||||
def monthly_solar_energy_available_h9_kwh_per_month(
|
||||
*,
|
||||
aperture_area_m2: float, # (H1)
|
||||
zero_loss_efficiency: float, # (H2)
|
||||
monthly_solar_flux_w_per_m2: tuple[float, ...], # (H7)m from Appendix U §U3.3
|
||||
monthly_solar_flux_w_per_m2: tuple[float, ...], # U3.2 flux in W/m² (24h avg)
|
||||
hours_in_month: tuple[int, ...], # (41)m × 24
|
||||
overshading_factor: float, # (H8)
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 (H9)m — solar energy available on collector aperture (W).
|
||||
"""SAP 10.2 (H9)m — solar energy available on collector aperture
|
||||
in **kWh/month** (NOT W).
|
||||
|
||||
Spec p.76: "(H1) × (H2) × (H7)m × (H8)". The result is an
|
||||
instantaneous (monthly-average) power in watts — the worksheet's
|
||||
downstream (H22) and (H23) formulas multiply by `(41)m × 24`
|
||||
(hours-in-month) and divide by 1000 to convert to kWh/month, so
|
||||
keeping (H9)m in W here matches the spec's stepwise units.
|
||||
Spec p.76 line for (H9): "Solar energy available, (H1) × (H2) ×
|
||||
(H7)m × (H8)". Spec p.76 line for (H7): "Monthly solar radiation
|
||||
per m² from U3.3 in Appendix U" — i.e. the integrated monthly
|
||||
irradiation `0.024 × n_m × S(orient,p,m)` in kWh/m²/month, NOT
|
||||
the §U3.2 24-hour-average flux S(orient,p,m) in W/m².
|
||||
|
||||
The cascade's `surface_solar_flux_w_per_m2` returns the §U3.2
|
||||
flux in W/m² (verified bit-exact against Elmhurst worksheet line
|
||||
295: SE 90° Jan region 0 = 36.7938 W/m²). To reach the §U3.3
|
||||
integrated value the SAP spec calls for, multiply by
|
||||
`hours_in_month / 1000` (W·h → kWh):
|
||||
|
||||
(H7)m_U3.3 [kWh/m²/month] = flux_U3.2 [W/m²] × hours / 1000
|
||||
|
||||
(H9)m therefore lands in kWh/month:
|
||||
|
||||
(H9)m = (H1) × (H2) × (H7)m_U3.3 × (H8)
|
||||
|
||||
Reading the spec page 77 (H23) formula `H18·H6·H5·H9·hours /
|
||||
(1000·H17)` with (H9) in W instead of kWh/month over-counts Y
|
||||
by exactly `hours/1000` (= 0.720 for 30-day months, 0.744 for
|
||||
31-day months) — the long-running 1.81× cascade-vs-worksheet
|
||||
gap on cert 000565 closes to <1e-3 kWh/month across 4 fixtures
|
||||
once (H9) carries the U3.3 conversion.
|
||||
"""
|
||||
return tuple(
|
||||
aperture_area_m2 * zero_loss_efficiency * flux * overshading_factor
|
||||
for flux in monthly_solar_flux_w_per_m2
|
||||
aperture_area_m2
|
||||
* zero_loss_efficiency
|
||||
* (flux * hours / 1000.0)
|
||||
* overshading_factor
|
||||
for flux, hours in zip(monthly_solar_flux_w_per_m2, hours_in_month)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -279,7 +303,7 @@ def hot_water_factor_y_monthly_h23(
|
|||
proportion_solar_to_hw_h18: tuple[float, ...], # (H18)m
|
||||
incidence_angle_modifier: float, # (H6)
|
||||
loop_efficiency: float, # (H5)
|
||||
monthly_solar_energy_available_h9_w: tuple[float, ...], # (H9)m in W (per `monthly_solar_energy_available_h9_w`)
|
||||
monthly_solar_energy_available_h9_kwh_per_month: tuple[float, ...], # (H9)m kWh/month
|
||||
hours_in_month: tuple[int, ...], # (41)m × 24
|
||||
hw_demand_seen_by_solar_h17: tuple[float, ...], # (H17)m
|
||||
) -> tuple[float, ...]:
|
||||
|
|
@ -289,9 +313,10 @@ def hot_water_factor_y_monthly_h23(
|
|||
[1000 × (H17)m]
|
||||
|
||||
Clamped to a lower bound of 0 per spec p.76 (`if Y < 0, enter zero`).
|
||||
(H9)m is in W (per `monthly_solar_energy_available_h9_w`); the
|
||||
`× hours_in_month / 1000` factor here converts W·h → kWh so the
|
||||
ratio Y_HW lands dimensionless against (H17)m (kWh/month).
|
||||
(H9)m is in kWh/month (per `monthly_solar_energy_available_h9_
|
||||
kwh_per_month` — the §U3.3 monthly-integrated convention SAP p.76
|
||||
references). The `× hours / 1000` term then carries the
|
||||
dimensional residue inherent to SAP's stepwise units.
|
||||
"""
|
||||
out: list[float] = []
|
||||
for m in range(12):
|
||||
|
|
@ -303,7 +328,7 @@ def hot_water_factor_y_monthly_h23(
|
|||
proportion_solar_to_hw_h18[m]
|
||||
* incidence_angle_modifier
|
||||
* loop_efficiency
|
||||
* monthly_solar_energy_available_h9_w[m]
|
||||
* monthly_solar_energy_available_h9_kwh_per_month[m]
|
||||
* hours_in_month[m]
|
||||
)
|
||||
y = numerator / (1000.0 * h17)
|
||||
|
|
@ -428,10 +453,11 @@ def solar_water_heating_input_monthly_kwh(
|
|||
pitch_deg=collector_pitch_deg,
|
||||
region=region,
|
||||
)
|
||||
h9 = monthly_solar_energy_available_h9_w(
|
||||
h9 = monthly_solar_energy_available_h9_kwh_per_month(
|
||||
aperture_area_m2=aperture_area_m2,
|
||||
zero_loss_efficiency=zero_loss_efficiency,
|
||||
monthly_solar_flux_w_per_m2=h7,
|
||||
hours_in_month=_HOURS_IN_MONTH,
|
||||
overshading_factor=overshading_factor,
|
||||
)
|
||||
h10 = overall_heat_loss_coefficient_h10(
|
||||
|
|
@ -485,7 +511,7 @@ def solar_water_heating_input_monthly_kwh(
|
|||
proportion_solar_to_hw_h18=h18,
|
||||
incidence_angle_modifier=incidence_angle_modifier,
|
||||
loop_efficiency=loop_efficiency,
|
||||
monthly_solar_energy_available_h9_w=h9,
|
||||
monthly_solar_energy_available_h9_kwh_per_month=h9,
|
||||
hours_in_month=_HOURS_IN_MONTH,
|
||||
hw_demand_seen_by_solar_h17=h17,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from domain.sap10_calculator.worksheet.appendix_h_solar import (
|
|||
hot_water_reference_temperature_h20_c,
|
||||
loop_heat_loss_coefficient_h11,
|
||||
monthly_collector_solar_flux_w_per_m2,
|
||||
monthly_solar_energy_available_h9_w,
|
||||
monthly_solar_energy_available_h9_kwh_per_month,
|
||||
overall_heat_loss_coefficient_h10,
|
||||
proportion_solar_to_hot_water_monthly_h18,
|
||||
reference_volume_h15,
|
||||
|
|
@ -264,21 +264,26 @@ def test_hot_water_factor_x_h22_clamps_upper_bound_at_18() -> None:
|
|||
assert actual == (18.0,) * 12
|
||||
|
||||
|
||||
def test_monthly_solar_energy_available_h9_applies_spec_formula() -> None:
|
||||
# Arrange — SAP 10.2 (H9)m spec p.76: (H1) × (H2) × (H7)m × (H8)
|
||||
# Cert 000565 worksheet (H1)=3.0, (H2)=0.8, (H8)=0.8. With a flat
|
||||
# 100 W/m² flux input, expected = 3 × 0.8 × 100 × 0.8 = 192 W.
|
||||
def test_monthly_solar_energy_available_h9_applies_spec_formula_with_u3_3_conversion() -> None:
|
||||
# Arrange — SAP 10.2 (H9)m spec p.76 reads (H1) × (H2) × (H7)m
|
||||
# × (H8) where (H7)m is "Monthly solar radiation per m² from U3.3
|
||||
# in Appendix U" — i.e. the U3.3 integrated value in kWh/m²/month,
|
||||
# not the U3.2 24-hour-average flux in W/m². The cascade converts
|
||||
# internally: (H7)m_U3.3 = flux_W_per_m² × hours / 1000. For a flat
|
||||
# 100 W/m² flux + Jan (744 h), H7_U3.3 = 100 × 744 / 1000 = 74.4
|
||||
# kWh/m²/month; (H9)m = 3 × 0.8 × 74.4 × 0.8 = 142.848 kWh/month.
|
||||
|
||||
# Act
|
||||
actual = monthly_solar_energy_available_h9_w(
|
||||
actual = monthly_solar_energy_available_h9_kwh_per_month(
|
||||
aperture_area_m2=_CERT_000565_APERTURE_AREA_M2,
|
||||
zero_loss_efficiency=_CERT_000565_ETA_0,
|
||||
monthly_solar_flux_w_per_m2=(100.0,) * 12,
|
||||
hours_in_month=(31 * 24,) * 12,
|
||||
overshading_factor=_CERT_000565_OVERSHADING,
|
||||
)
|
||||
|
||||
# Assert — H1=3, H2=0.8, H7=100, H8=0.8 → 192 W/month
|
||||
assert all(abs(h9 - 192.0) < 1e-9 for h9 in actual)
|
||||
# Assert — 3 × 0.8 × (100 × 744/1000) × 0.8 = 142.848 kWh/month
|
||||
assert all(abs(h9 - 142.848) < 1e-9 for h9 in actual)
|
||||
|
||||
|
||||
def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None:
|
||||
|
|
@ -290,7 +295,7 @@ def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None:
|
|||
proportion_solar_to_hw_h18=(1.0,) * 12,
|
||||
incidence_angle_modifier=0.94,
|
||||
loop_efficiency=0.9,
|
||||
monthly_solar_energy_available_h9_w=(-5.0,) * 12,
|
||||
monthly_solar_energy_available_h9_kwh_per_month=(-5.0,) * 12,
|
||||
hours_in_month=(744,) * 12,
|
||||
hw_demand_seen_by_solar_h17=(100.0,) * 12,
|
||||
)
|
||||
|
|
@ -299,20 +304,21 @@ def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None:
|
|||
assert actual == (0.0,) * 12
|
||||
|
||||
|
||||
def test_hot_water_factor_y_h23_applies_w_to_kwh_time_integration() -> None:
|
||||
def test_hot_water_factor_y_h23_applies_spec_p77_formula() -> None:
|
||||
# Arrange — spec p.76: Y_HW = [(H18) × (H6) × (H5) × (H9) × hours]
|
||||
# ÷ [1000 × (H17)]. With H18=1, H6=1, H5=1, H9=1000 W,
|
||||
# hours=744 (Jan), H17=744 kWh → numerator = 1000 × 744 = 744000 W·h
|
||||
# ÷ 1000 = 744 kWh, ÷ H17 = 744/744 = 1.0 ✓ dimensionless.
|
||||
# ÷ [1000 × (H17)]. With H18=1, H6=1, H5=1, H9=1.0 kWh/month,
|
||||
# hours=1000, H17=1.0 kWh → numerator = 1.0 × 1000 = 1000,
|
||||
# ÷ (1000 × 1.0) = 1.0. (H9) is in kWh/month after the U3.3
|
||||
# conversion — see `monthly_solar_energy_available_h9_kwh_per_month`.
|
||||
|
||||
# Act
|
||||
actual = hot_water_factor_y_monthly_h23(
|
||||
proportion_solar_to_hw_h18=(1.0,) * 12,
|
||||
incidence_angle_modifier=1.0,
|
||||
loop_efficiency=1.0,
|
||||
monthly_solar_energy_available_h9_w=(1000.0,) * 12,
|
||||
hours_in_month=(744,) * 12,
|
||||
hw_demand_seen_by_solar_h17=(744.0,) * 12,
|
||||
monthly_solar_energy_available_h9_kwh_per_month=(1.0,) * 12,
|
||||
hours_in_month=(1000,) * 12,
|
||||
hw_demand_seen_by_solar_h17=(1.0,) * 12,
|
||||
)
|
||||
|
||||
# Assert
|
||||
|
|
@ -392,18 +398,18 @@ def test_monthly_collector_solar_flux_h7_returns_twelve_values_matching_appendix
|
|||
assert actual[5] > 150.0
|
||||
|
||||
|
||||
def test_solar_water_heating_input_monthly_kwh_returns_winter_zero_summer_peak_shape() -> None:
|
||||
# Arrange — cert 000565 worksheet (Block 1, lines 399-413) lodges
|
||||
# all Appendix H inputs. This test asserts the monthly SHAPE of
|
||||
# (H24)m matches the spec's physical expectation (winter zero,
|
||||
# summer peak) — magnitude pin against worksheet line 415's
|
||||
# 281.3478 kWh total is deferred to the next slice while the
|
||||
# H8 (overshading) ambiguity in the spec's (H23) line-ref vs the
|
||||
# top-level Equation H1 formulation is resolved. Today's cascade
|
||||
# produces ~510 kWh annual for cert 000565 (1.8× the worksheet)
|
||||
# via the line-ref (H23) interpretation. The orchestrator
|
||||
# plumbing + (H10)..(H22)/(H24) component math are spec-pinned;
|
||||
# the (H23) factor calibration is the only open piece.
|
||||
def test_solar_water_heating_input_monthly_kwh_matches_cert_000565_worksheet_h24m_to_1e_minus_3() -> None:
|
||||
# Arrange — cert 000565 worksheet (Block 1, lines 399-415) lodges
|
||||
# all Appendix H inputs and shows (H24)m per month (line 416,
|
||||
# negated `(63c)m`) summing to 281.3478 kWh/yr (line 415).
|
||||
# This pin closes the long-running 1.81× over-count: under SAP
|
||||
# 10.2's mixed unit convention for (H7), where the spec p.76 says
|
||||
# "Monthly solar radiation per m² from U3.3 in Appendix U" and
|
||||
# U3.3 defines that quantity as the monthly *integrated* value
|
||||
# (kWh/m²/month via `0.024 × n_m × S(orient,p,m)`), the cascade
|
||||
# must convert U3.2's W/m² flux to U3.3's kWh/m²/month before
|
||||
# computing (H9) — otherwise the page 77 `× hours/1000` term
|
||||
# double-counts.
|
||||
hw_demand_62m_kwh = (
|
||||
312.9085, 278.7760, 301.5007, 278.0295, 278.2821,
|
||||
178.0038, 178.8734, 184.0215, 183.8120, 285.3050,
|
||||
|
|
@ -412,9 +418,14 @@ def test_solar_water_heating_input_monthly_kwh_returns_winter_zero_summer_peak_s
|
|||
external_temp_96m_c = (
|
||||
4.3, 4.9, 6.5, 8.9, 11.7, 14.6, 16.6, 16.4, 14.1, 10.6, 7.1, 4.2,
|
||||
)
|
||||
# Worksheet line 416 (63c)m negated — (H24)m per month, cert 000565.
|
||||
expected_h24m_kwh = (
|
||||
0.0, 0.0, 7.2708, 34.9317, 66.0485, 60.0128,
|
||||
58.2475, 42.2547, 12.5818, 0.0, 0.0, 0.0,
|
||||
)
|
||||
|
||||
# Act
|
||||
h24 = solar_water_heating_input_monthly_kwh(
|
||||
actual = solar_water_heating_input_monthly_kwh(
|
||||
collector_orientation=Orientation.W,
|
||||
collector_pitch_deg=30.0,
|
||||
region=0, # UK average (cert lodges Thames Valley; rating uses 0)
|
||||
|
|
@ -435,15 +446,9 @@ def test_solar_water_heating_input_monthly_kwh_returns_winter_zero_summer_peak_s
|
|||
solar_hot_water_only=True,
|
||||
)
|
||||
|
||||
# Assert — physical shape pins:
|
||||
# 1. 12-month tuple, all values non-negative (Equation H1 clamp).
|
||||
# 2. Winter months (Jan, Feb, Nov, Dec) clamp to 0 — the
|
||||
# polynomial's negative-X term dominates when solar flux is low
|
||||
# vs HW demand (worksheet line 416 also zeros these months).
|
||||
# 3. Summer months (May, Jun, Jul) carry the peak contribution.
|
||||
assert len(h24) == 12
|
||||
assert all(v >= 0.0 for v in h24)
|
||||
assert h24[0] == 0.0 and h24[1] == 0.0
|
||||
assert h24[10] == 0.0 and h24[11] == 0.0
|
||||
assert h24[4] > h24[2] # May > Mar
|
||||
assert h24[5] > h24[8] # Jun > Sep
|
||||
# Assert — every month pinned to worksheet at abs < 1e-3 kWh.
|
||||
for m in range(12):
|
||||
assert abs(actual[m] - expected_h24m_kwh[m]) < 1e-3, (
|
||||
f"month {m+1}: cascade {actual[m]:.4f} vs worksheet "
|
||||
f"{expected_h24m_kwh[m]:.4f} (Δ {actual[m] - expected_h24m_kwh[m]:+.4f})"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue