Model/domain/sap10_calculator/worksheet/utilisation_factor.py
Khalim Conn-Kowlessar 29ac35ccbe refactor: lift-and-shift packages/domain/src/domain/sap → domain/sap10_calculator
Migration of the SAP 10.2 calculator package from the uv-workspace
src-layout (`packages/domain/src/domain/sap`) to the root-level layout
(`domain/sap10_calculator`), matching the pattern already used by
`domain.addresses` / `domain.tasks` / `domain.postcode`.

Changes:

- `git mv packages/domain/src/domain/sap → domain/sap10_calculator`
  (92 files; git auto-detected all as renames so blame/history is
  preserved).
- Subpackage rename: `domain.sap` → `domain.sap10_calculator`. 48
  Python files rewritten (`from domain.sap.X` → `from domain.sap10_
  calculator.X`); zero remaining `domain.sap` refs after the sed pass.
- Path-string updates: 3 .py files (test fixtures + xlsx loader) +
  6 markdown docs (CONTEXT.md, 2 ADRs, 3 sap-spec docs, sap10_
  calculator/README.md) had hard-coded `packages/domain/src/domain/
  sap/...` paths rewritten to `domain/sap10_calculator/...`.
- `Path(__file__).parents[N]` rebasing: the old tree was 3 levels
  deeper than the new one (`packages/domain/src/`), so 4× `parents[7]`
  became `parents[4]` and 1× `parents[6]` became `parents[3]` across
  `tables/pcdb/{__init__.py, postcode_weather.py, etl.py}`,
  `worksheet/tests/_xlsx_loader.py`, and `tests/test_pcdb_etl.py`.
- PEP 420 namespace package: deleted both `domain/__init__.py`
  (root + workspace, both load-bearing only as empty/docstring) so
  Python combines `domain.sap10_calculator` (root) and `domain.ml`
  (workspace) into one namespace package. Confirmed via
  `domain.__path__ == ['/workspaces/model/domain',
  '/workspaces/model/packages/domain/src/domain']`. Without this,
  the root `domain/__init__.py` shadowed the workspace one and
  `domain.ml` was unreachable.

Verified:

- Full sweep (`backend/documents_parser/tests/test_summary_pdf_
  mapper_chain.py + domain/sap10_calculator/worksheet/tests/test_
  e2e_elmhurst_sap_score.py + domain/sap10_calculator/rdsap/tests/
  test_golden_fixtures.py`): 99 passed / 19 failed — exact same
  counts as pre-refactor. All 19 failures pre-existing (9 hand-built
  001479 + 6 cohort diff + 4 cohort chain non-spec).
- Wider sweep (all sap10_calculator + domain.ml): 1654 passed /
  20 failed (the +1 vs the focused sweep is the pre-existing
  `test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_
  section_5_11_4` which was already failing on the previous baseline).
- Pyright net-zero on the three load-bearing baselines:
  `heat_transmission.py` 13, `cert_to_inputs.py` 35, `mapper.py` 33.

Lift-and-shift only — no semantic renames (`Sap10Calculator` stays
`Sap10Calculator`), no testpaths edits in pytest.ini (sap tests
continue to be invoked by explicit pytest paths).

Note: `domain.ml` still lives at `packages/domain/src/domain/ml/`.
Migrating it would close out the dual-`domain/` layout but is
out of scope for this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:22:37 +00:00

43 lines
1.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SAP 10.2 Table 9a — heating utilisation factor η.
η reduces the contribution of internal + solar gains when they outpace the
dwelling's heat-loss rate. A well-insulated dwelling with large solar gains
in October can't fully use those gains — it's already warm enough.
Formula per Table 9a:
a = 1 + τ / 15 where τ is the dwelling time constant (h)
γ = G / L gain-to-loss ratio (W / W)
if γ > 0 and γ ≠ 1: η = (1 γ^a) / (1 γ^(a+1))
if γ = 1: η = a / (a + 1)
if γ ≤ 0: η = 1
The time constant τ = TMP / (3.6 × HLP) comes from the dwelling's thermal
mass parameter and heat-loss parameter; computed by the orchestrator and
passed in here.
Reference: SAP 10.2 specification (14-03-2025) Table 9a (page 184).
"""
from __future__ import annotations
def utilisation_factor(
*,
total_gains_w: float,
heat_loss_rate_w: float,
time_constant_h: float,
) -> float:
"""SAP 10.2 Table 9a heating utilisation factor η.
γ = total_gains_w / heat_loss_rate_w; η ∈ (0, 1]. When the heat-loss
rate is non-positive (dwelling already balanced or gaining), η = 1
so the gains are fully credited.
"""
if heat_loss_rate_w <= 0:
return 1.0
gamma = total_gains_w / heat_loss_rate_w
a = 1.0 + time_constant_h / 15.0
if gamma == 1.0:
return a / (a + 1.0)
return (1.0 - gamma**a) / (1.0 - gamma ** (a + 1.0))