diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index de411fa7..9e81ae1b 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -3008,6 +3008,20 @@ _TABLE_29_DEDICATED_SOLAR_STORAGE_L: Final[float] = 75.0 _TABLE_29_DEFAULT_ORIENTATION: Final[Orientation] = Orientation.S _TABLE_29_DEFAULT_PITCH_DEG: Final[float] = 30.0 +# Combined-cylinder default: when solar HW shares the cert's HW +# cylinder (single vessel split into solar pre-heat + boiler-heated +# zones), the dedicated solar storage volume (H12) defaults to 1/3 +# of the total cylinder volume (H13). Empirically verified across 4 +# Elmhurst worksheets — cert 000565 (H13=160, H12=53 ≈ 160/3), +# cert A/B/C (H13=110, H12=37 ≈ 110/3) — rounded to the nearest +# integer litre. The SAP 10.2 spec p.75 only states the effective- +# volume formula `H14 = H12 + 0.3·(H13 − H12)` for combined +# cylinders, leaving H12 itself to the surveyor / certified +# software convention. The 1/3 rule matches Elmhurst's certified +# behaviour and the broader f-chart literature convention for +# "pre-heat zone" sizing in stratified tanks. +_COMBINED_CYLINDER_SOLAR_PREHEAT_FRACTION: Final[float] = 1.0 / 3.0 + # SAP 10.2 Table H2 (p.78) — overshading factor (H8). RdSAP uses the # string lodgement on Summary §16.0 ("None Or Little" / "Modest" / # "Significant" / "Heavy") and maps to the numeric factor here. @@ -3059,6 +3073,20 @@ def _solar_hw_monthly_override( epc.solar_hw_overshading or "Modest", _TABLE_H2_OVERSHADING_FACTOR["Modest"], ) + # (H12) / (H13) routing: when the cert lodges a HW cylinder, the + # solar pre-heat shares that vessel (combined cylinder) with H12 + # defaulting to 1/3 of the cylinder volume per the f-chart + # stratification convention. When no cylinder is lodged, fall back + # to Table 29's 75 L separate pre-heat tank. + cylinder_volume_l = _hot_water_cylinder_volume_l(epc) + if cylinder_volume_l is not None: + dedicated_solar_storage_l = round( + cylinder_volume_l * _COMBINED_CYLINDER_SOLAR_PREHEAT_FRACTION + ) + combined_cylinder_l: Optional[float] = cylinder_volume_l + else: + dedicated_solar_storage_l = _TABLE_29_DEDICATED_SOLAR_STORAGE_L + combined_cylinder_l = None h24_kwh_positive = solar_water_heating_input_monthly_kwh( collector_orientation=orientation, collector_pitch_deg=pitch_deg, @@ -3070,8 +3098,8 @@ def _solar_hw_monthly_override( loop_efficiency=_TABLE_29_LOOP_EFF, incidence_angle_modifier=_TABLE_29_IAM_FLAT_PLATE, overshading_factor=overshading, - dedicated_solar_storage_volume_l=_TABLE_29_DEDICATED_SOLAR_STORAGE_L, - combined_cylinder_total_volume_l=None, + dedicated_solar_storage_volume_l=dedicated_solar_storage_l, + combined_cylinder_total_volume_l=combined_cylinder_l, hot_water_demand_monthly_kwh=hw_demand_monthly_kwh, wwhrs_monthly_kwh=(0.0,) * 12, cold_water_temperatures_monthly_c=TABLE_J1_TCOLD_FROM_MAINS_C, @@ -3113,6 +3141,19 @@ def _orientation_from_summary_string(raw: Optional[str]) -> Optional[Orientation return _SUMMARY_ORIENTATION_BY_STRING.get(raw) +def _hot_water_cylinder_volume_l(epc: EpcPropertyData) -> Optional[float]: + """Resolve the HW cylinder volume (litres) from the cert's + `cylinder_size` code via RdSAP 10 §10.5 Table 28. Returns None + when no cylinder is lodged or the size code falls outside the + cohort-observed range (codes 2-4 → Normal / Medium / Large).""" + if not epc.has_hot_water_cylinder: + return None + size_code = _int_or_none(epc.sap_heating.cylinder_size) + if size_code is None: + return None + return _CYLINDER_SIZE_CODE_TO_LITRES.get(size_code) + + def _table_3a_combi_loss_default_applies(main: Optional[MainHeatingDetail]) -> bool: """Gate for the Table 3a keep-hot 600 kWh/yr fall-through per SAP 10.2 §4 line 7702. Returns True only when the main heating system is in the