From 4e2f2bdcc7272dd1998a4e2121e3a212dca21d83 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 24 Jun 2026 09:07:16 +0000 Subject: [PATCH] =?UTF-8?q?test(worksheet):=20pin=20simulated=20case=2050?= =?UTF-8?q?=20=E2=80=94=20MVHR=20+=20dual-immersion=20all-electric?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the mapper-driven e2e cascade pin for "simulated case 50" (000565 semi, electric storage main SAP 402 + portable electric secondary + MVHR + whc-903 DUAL electric immersion + 160 L cylinder, Economy-7). Routes the Summary PDF through extractor + mapper + calculator like the other 000565 fixtures. Locks in two off-peak fixes this case ground-truthed: - the Table 13 HW high/low split applied to CO2/PE (commit 39ae2cf0), and - the Table 12a Grid 2 MVHR fan fraction 0.71/0.58 (commit cd5113ab). All 11 SAP-result fields reconcile to the U985 worksheet EXACTLY, including the (272) rating CO2 2397.1237 — SAP 38.8426 (=39), cost £1317.0116, water 1668.0788 kWh, fans 315.6384 kWh. Summary mirrored to the tracked fixtures dir so the test doesn't depend on the unstaged `sap worksheets/` workspace. pyright strict gate not run locally (pyright not installed in this container). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/fixtures/Summary_000565_case50.pdf | Bin 0 -> 67909 bytes .../_elmhurst_worksheet_000565_case50.py | 113 ++++++++++++++++++ .../worksheet/test_e2e_elmhurst_sap_score.py | 18 +++ 3 files changed, 131 insertions(+) create mode 100644 backend/documents_parser/tests/fixtures/Summary_000565_case50.pdf create mode 100644 tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case50.py diff --git a/backend/documents_parser/tests/fixtures/Summary_000565_case50.pdf b/backend/documents_parser/tests/fixtures/Summary_000565_case50.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ab23b99832f0c41720bb9ca96f01336fef6ae094 GIT binary patch literal 67909 zcmeFa1ymeew&( z-nnnh|IPf@oAuVc-C4z{sEJstfA(LY2OL~IMjn#@nRDUmR< z{9C~GZvn?sp#9fuo+AH4HlGyq&5aB|49brBpr;y0SwV_s0GJtqOi7p-85tyv%uGx{ zBup%sdkaVPp_D zvjiF0Gl*O2fs90r3~UUI7-WsCO&}Swu`_e>^E-g-jr6RLJ_Q_TBRZ%^#5`o9UJjJ8 z|JZj4bVRpAU$ff}BOK`J*mvpvq$?;aT!CgtNT3gTCX5?Ij!c*dL%7|f@#5R$gn8ja zV+K%TLrHaV&&~50^MzOdjKD-u>GvyDHDki0-W7<+SB?{Ubl7x&--zV`(|{zQ_SWr|r@KozFFm;MVW;-wc&ts@(87c& z^nyvx)(Nfo*3vvs>2~|I*TQD!#i#w&v*>1(1r)OtO!)-0MVB#_+H-RbzbFEd?_*}y zT;4ih%1oJzFu5=6H~CFFGk7bx2^JP_y0Bbf8n~xy0Y@GwpuA!v&zMdPYQgUNn>L&c ziAOER_Q#Tok`jjRHtOu(^0Gr6Dbg@4KflV!9O^q)N zhQ(E1!J+WD`Kh_}>Ufu9>3f6k%Tw3frhJBQO4cicGYrCoMRpuVflMCi<6XecX~@{p z9=_A650!Ik(OA$yq8r)U6xt>;riwZT+h^9@YoXC>T(A1x%)J`>$_0MEA#4k`p$c4U z{JHno8#hL1(K^AyW%VJyjCJJhDPHVFpHwUD?Jv?=X|KleoeVC`2H|9{N`eBj!du># zV469vfwG5@T{>E)s;Z%Ug7*T?XA>!aq$QeD#HQTaKNcwOul0QwQ6m~Z8mv`~Z$ejy8E4KfiMX75fnu(ltgDZn0!=E0HpS%)H^&}cbK9RD z&p+&oM%PhM@^n*v=c#)*(EqlRKPL{Hb}D7FcDztpVQ32lPfl)Rv_IZ7^qltT;2=28 zI1$!=N(??JZHWOm^wU}zUHZD?O9yJ*{ZSY`Rnz<`PgG8rJ-IrI0nCF{xj z55%~rzbp-_@EBfO_q|H9G52ES{m$!OVKUeC())*D2cx;vp8g^-Ef!~PsJ zwuW=8svY~Vn*D9m@~82u_y+yKmftwEZr#q#FrA#^gYOFvTuK2gN)xgvfEJGM!J8ws zO6OUQZg?cHe4N&g`5%%gfZ|&1@lhk4t&%iCQ@gcL8O4V7XjLQZPp7+Qe$lwp8~QxY zF1XZHO7GC={C9E5PUswy-W;1d{jOZJEf`FyR015SHp% z&+YKZI)`Z7WUJoy1~zc%-iDw|wPh1u<|S?Oi~7%Q>Invru6X)u5+Zo@M7wcCvAp6f zGw>McyI7|E_+}iYMzWIKp21`FS4O2gQG3B^PY_T@U`po|{?Pz@UEar=%y4tDBV*^vAdR%CX@c|4pRS#y_zNCm27QtIcNJYuTWCACRYQ+7_dwTzvp!K3 z8JvF1vL6vXLkbLndZSfiM25FrbTCgY42?J(k(p5-p z-be)N4W82IFDjc>0uN+^x#T(f@%O{2`1*9LEXRt6{Ybo>wuH7DcS^K#x{Df5`sqgK z-U5qVQg`@6g8_ni{UXX<=Pc{un}qiZUa+jueZ-ahZ>9?wbCVMg!Ff9Ld(AJA$^6ue zv35?RiPK4KXnh8GYtQ?FXcRBpDf8fsx46Z-AFE4AQGr(7Yt)D{*FOcU-eK5FF(}Tw zf)n9yQps7Ds(qG@2!1$niUraZi0o7{?Y*p9jF4yVMHp|b5>Qk5@%v=_1|EskNgHso zr8>rAnh>KQsWZVQR0eyHH5yO`{0el?F7N(zq+>tZu$z%?^!5uW!B0o77yU(jRm4o5 zx-}ooiQo7!n!R~_I+MeG;4bWKe0IA(tRKUtYEOp_j#)HMd_}qeswmJZm}h@q^C(FD ze7r85uA$BH{5IoUCK7Oka?5Gz74K}vELb(fQY@Zr`McQIvFftQyJWJu%J($H7Hl(8 z(Z=YIHCJdNKBDUF>=Dg{HYgwPKy{CDI|MhJXWWISgEll2%F5f!$jE~5R$V}ZR_Vx) z8{wOSfAO(7Q$-){Q0d2aGqU0wTsZU;;28J4(v#K6?szuhR5?2`jM)0qzG%PrOz$*O z7f(Y;+~MBo^MMr6z=oURtXT=-h_1wU_4(2+7eFCHmSm|TAMNV;j9Jzd$RL_w|4^EQ z$?tBvuTefs(nkhf9FW6xH*BLFH;;Q(q@_OlVgDy4NJUt!W%{n<_rPn4akh*Lx=rki z;Uv_%`R;^Ym3=qq{2A=V0RSJg(55$rZAHj(SNF)o$(kce6#^(=;v~rEtIqXz;8>sOg=bYbxq%;Q5TIl z+#Ge3n-J|=tteTsF^kZ(ZS}RsmLu{t+lMe5{4B%N+lxg6AFs$3jU2t$p8|JX*_^yL zn`i+9`e5}D|}rq9!+PF+^7%My6{~{rUM3C9ugkQ03!hWzmbzQX9kM- z;!f##eHztL>44?=Rfa_&3ljVv6;IX0E_TMG@BHMh$CFNJ;-iYg*IXzvwJtHJ2MY7{ zHGCpHVq0hRDS<+J==uAW$8V~Qs2QG@Tj*Rm%Rg>)HJ?v8iv2o~4lI>_=c(=xIyfL5 z7^BiH{cV?E0!=5$1t+qb`b-DBbjd^pD_bGyA?L*TiGB<7Q~TM>udtXwNWz^Vh9cnx z#!t}?-PG#r$u7D3$dD&=OCT0{(4mf@L7A4XKOreo8CBNnI0|r-lVsKB!C^a zL)ezSoZFGU!k52rq{> zI?@7TAE9BlE6T z9KpSgE!-h-U!@@R+@WEt+<^1GKb=$m75jDmk0~2G)*_8~{#W$2`8zLiBS!6N1t;-z zMx>&i@dz_WH|ZU?bY@wt!`U;=w?dyHKomKrMcQE~bgOf-AeiAe8W%37 z#-BKr--&a>jBl8b&T`QIa$n_r2Cnga=mWD0WG;isgSM77Hg(}V^i_2xW6LM-U9&tz z=AK6>XTZB6?|SuNFN?dMmIh zm-1>j2*=Uz;Q}cguZp(=yqzC`13aL_TwBS_%fBDI_hSLaI}XBEUYtar(F$@IG?(LG z;Yzd>g{@M9*zm31ycR}1xAes$l0yiKi-(dLSt8+LDVeh*s2 z_4HjtXDAh`z60|vgNw5rS*5R%E`@+i_67kPI03Pe_Q2o>?GN)O1a_H@b!VR_Oh252 zG(-xAe`F@+az-3XILjzmsP{CuX)55=#>Kk+StDGSe|+Kd=%wx54Prp_p2()~a%m#T z#gDApIs7W3QT7>!0fcR4?WskAIC($x(wVASs~#XcMH;(Eg?r^;bo72XN2w?H4iiP{ z)L77!X6}*zIZ@W}#pCHk5aRWk0LfLTICCkh{iB0D zkDp!8V{R6%8X4jd3!;Em#jmVgA@Nb2exk~3fwdFP5Z^l#vsS@gyr@^;k?_-4Wr`_& za52TjttpUSyx*;47__NuRU;0d@z6|vbQA_H;?zQC?dq$`^ae>;S9rrYtaak!jkHyXJ(#smiYJFg- zzkju@C?HtE^V4>YT;k^r1RRHVV6EaLd;8bx$X*d_MDGpLo`<#3M(s{l2V`K!(~Q!; zFUIVsR1#jzb6B$H zgHz zJbpY*a}lVH6TQtj{j$ZVLnQG5ylu~P z>)C6y&?9u8DyWntyoOUOP?bx5XXK0|qR6xce+yE9%%D7baku)4a?DL@?pjl?|sAq%ieNgV~VAN?_C`Ymt4O*nui{)F)?v>!EfysrjIJmSQL`|0|6Kik9I{V6Epfr;Dcy?$6kf3>j$sk;BD;RxI;4ir$r50=xi=&`3g8e&zph485odF3QY+Ss7a}uK3DBHkXQoYx3lV82sAs$3J z{4*q4znAvK;Bo~dw@%sdpD)G+Jllr)NH!D6zqQe>`t=$_qG5q0$&!SrVn^NXPE|L! z?5D70KW6tHp2yL~do_?R#EY%~NiaG|QuEDLUD#dcWO>fXIaVHeD zlo@99fxG|$o^`P9zQO{>G_8B@yRzRd#2uXrv}W=rCq&AV<_q$NwyP+x58#Ri6%$62 zW+%4WgaQmL(FW<>b7@{S8dLj?cF>-q%;sL9)n4Ar0OCxL_XitrOB;L|x{PCA`0c1p zqC)21{_rUDY6yZHeY0O*fjiu4Dqu+``91-ovzI+=)CaurhzP~#LrE{ci76^%QDU

v7W&q#sj{ooLLMo+piFNT5yjbrij^@!6JDpO`;I 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 literal 0 HcmV?d00001 diff --git a/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case50.py b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case50.py new file mode 100644 index 00000000..e051922b --- /dev/null +++ b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case50.py @@ -0,0 +1,113 @@ +"""Mapper-driven cascade pin against the Elmhurst U985-0001-000565 +"simulated case 50" worksheet — an all-electric Economy-7 dwelling with +MVHR + electric storage heaters + a DUAL-immersion hot-water cylinder. + +Like 000565 / the _rr cases, this fixture does NOT hand-build the +EpcPropertyData: it routes the Summary PDF through +ElmhurstSiteNotesExtractor + from_elmhurst_site_notes so the SAP-result +pin grid exercises the WHOLE extractor + mapper + calculator pipeline. + +This case was hand-built (Khalim) to ground-truth the dual-immersion +cylinder water-heating path, and it exercises two distinct off-peak +tariff mechanics that the gas/standard-tariff fixtures don't reach: + + - The Table 13 HW high-rate fraction for a whc-903 DUAL electric + immersion (0.1009 high / 0.8991 low) — the COST (6.4878 p/kWh) AND + the CO2/PE split ("Water heating - high/low rate cost", fixed in + `_electric_immersion_hw_high_rate_fraction`). + - The Table 12a Grid 2 fan fraction (0.71/0.58) for MVHR fan + electricity (315.64 kWh, 100% MVHR per worksheet line 230a) — billed + distinct from "all other uses" 0.90/0.80 (fixed by including the MVHR + fan in `mev_kwh_for_cost_split`). + +Unknown meter + dual electric immersion resolves to 7-hour off-peak via +the §12 trigger. After both fixes the existing-dwelling rating reconciles +to the U985 worksheet EXACTLY, including the (272) rating CO2. + +Cert shape: 000565 semi shell, main = electric storage heaters (SAP 402, +manual charge control), portable electric secondary (SAP 693), water +heating from a whc-903 dual electric immersion + 160 L foam cylinder (no +cylinder stat), MVHR (Vent Axia, PCDB 500140), one instantaneous electric +shower, no PV, Economy-7. + +Source: user-simulated PDFs at `sap worksheets/golden fixture debugging/ +simulated case 50/`. The Summary is mirrored into the tracked +`backend/documents_parser/tests/fixtures/Summary_000565_case50.pdf` so the +test runs without depending on the unstaged workspace. + +Worksheet pin targets (U985-0001-000565 block 1 — existing dwelling SAP): +- SAP rating 39 (258); continuous 38.8426; ECF 4.4252 (257) +- Total fuel cost £1317.0116 (255) +- Total CO2 2397.1237 kg/year (272) +- Space heating 14318.4904 kWh/year ((98c)) +- Main 1 fuel 12170.7169 kWh/year (211) +- Secondary fuel 2147.7736 kWh/year (215) +- Hot water fuel 1668.0788 kWh/year (219) +- Lighting 435.3204 kWh/year (232) +- Pumps/fans 315.6384 kWh/year (231) + +Per [[feedback-zero-error-strict]] + [[feedback-e2e-validation- +philosophy]]: pins are abs=1e-4 against the worksheet PDF. The pin +values live in `test_e2e_elmhurst_sap_score._FIXTURE_PINS`. +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path +from typing import Final + +from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor +from datatypes.epc.domain.epc_property_data import EpcPropertyData +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + +# parents[0]=worksheet/, [1]=sap10_calculator/, [2]=domain/, [3]=tests/, +# [4]=repo root. +_SUMMARY_PDF: Final[Path] = ( + Path(__file__).resolve().parents[4] + / "backend" / "documents_parser" / "tests" / "fixtures" + / "Summary_000565_case50.pdf" +) + + +def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]: + """Convert a Summary PDF into the per-page text format the + ElmhurstSiteNotesExtractor expects (label\\nvalue sequences). Mirror + of the helper in the other `_elmhurst_worksheet_*` fixtures. + """ + info = subprocess.run( + ["pdfinfo", str(pdf_path)], capture_output=True, text=True, check=True, + ).stdout + m = re.search(r"Pages:\s+(\d+)", info) + if m is None: + raise RuntimeError(f"Could not parse page count from {pdf_path}") + page_count = int(m.group(1)) + pages: list[str] = [] + for i in range(1, page_count + 1): + layout = subprocess.run( + [ + "pdftotext", "-layout", "-f", str(i), "-l", str(i), + str(pdf_path), "-", + ], + capture_output=True, text=True, check=True, + ).stdout + tokens: list[str] = [] + for line in layout.splitlines(): + if not line.strip(): + tokens.append("") + continue + parts = [p for p in re.split(r"\s{2,}", line.strip()) if p] + tokens.extend(parts) + pages.append("\n".join(tokens)) + return pages + + +def build_epc() -> EpcPropertyData: + """Route the simulated case-50 Summary through extractor + mapper. + No hand-built EpcPropertyData — the extractor and mapper are part of + the test target. + """ + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + return EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) diff --git a/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py b/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py index 13b1b1bb..918329d9 100644 --- a/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py +++ b/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py @@ -48,6 +48,7 @@ from tests.domain.sap10_calculator.worksheet import ( _elmhurst_worksheet_001431_case6 as _w001431_case6, _elmhurst_worksheet_001431_case7 as _w001431_case7, _elmhurst_worksheet_001431_case20 as _w001431_case20, + _elmhurst_worksheet_000565_case50 as _w000565_case50, _elmhurst_worksheet_000565_case52 as _w000565_case52, ) from tests.domain.sap10_calculator.worksheet._elmhurst_fixtures import ( @@ -297,6 +298,22 @@ _FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = { lighting_kwh_per_yr=246.3083, pumps_fans_kwh_per_yr=0.0, ), + # Mapper-driven — Summary_000565_case50.pdf → extractor → mapper → + # calculator. All-electric Economy-7: storage-heater main (SAP 402) + + # MVHR + whc-903 DUAL electric immersion + 160 L cylinder. Exercises + # the Table 13 HW high/low split (cost + CO2/PE) AND the Table 12a + # Grid 2 MVHR fan fraction (0.71/0.58). Reconciles to the worksheet + # EXACTLY after both off-peak fixes (incl. the (272) rating CO2). + "000565_case50": FixtureCascadePins( + sap_score=39, sap_score_continuous=38.8426, ecf=4.4252, + total_fuel_cost_gbp=1317.0116, co2_kg_per_yr=2397.1237, + space_heating_kwh_per_yr=14318.4904, + main_heating_fuel_kwh_per_yr=12170.7169, + secondary_heating_fuel_kwh_per_yr=2147.7736, + hot_water_kwh_per_yr=1668.0788, + lighting_kwh_per_yr=435.3204, + pumps_fans_kwh_per_yr=315.6384, + ), # Mapper-driven — Summary_000565_case52.pdf → extractor → mapper → # calculator. Regular (non-combi) mains-gas boiler (SAP 102) + a # 160 L foam cylinder heated from the main (WHC 901), no cylinder @@ -333,6 +350,7 @@ _FIXTURE_MODULES: Final[dict[str, ModuleType]] = { "001431_case6": _w001431_case6, "001431_case7": _w001431_case7, "001431_case20": _w001431_case20, + "000565_case50": _w000565_case50, "000565_case52": _w000565_case52, }