OpenPH-Demand
PHPP energy demand calculations.
openph-demand is one package in the OpenPH UV workspace. It implements the PHPP energy demand calculations and reproduces the numbers in Excel PHPP exactly.
Purpose
OpenPH-Demand calculates:
- Annual heating demand: transmission and ventilation heat losses, internal and solar gains.
- Annual cooling demand: solar gains, internal gains, cooling strategies.
- Solar radiation on windows and opaque surfaces for the heating and cooling periods.
- Ground heat transfer: temperatures for ground-coupled surfaces.
It does not yet implement the PHPP Heating load and Cooling load worksheets. It covers only
the peak-load climate inputs those worksheets use. There is no design-load (W) result, so the
energy demand summary reports peak_w: null, because a zero would be misleading.
Structure
openph-demand/
├── src/
│ └── openph_demand/
│ ├── heating_demand/ # Heating demand calculations
│ ├── cooling_demand/ # Cooling demand calculations
│ ├── ground/ # Ground heat transfer
│ ├── solar/ # Solar radiation calculations
│ ├── to_table/ # Table view renderers
│ ├── summary.py # EnergyDemandSummary + build_energy_demand_summary
│ ├── get_solvers.py # Type-safe solver accessors
│ └── solvers.py # Solver classes (Energy, Ground, Solar)
├── tests/
└── pyproject.toml
Usage
from openph.phpp import OpPhPHPP
from openph_demand.get_solvers import (
get_openph_energy_demand_solver,
get_openph_ground_solver,
get_openph_solar_solver,
)
# Create PHPP model
phpp = OpPhPHPP()
# Get solvers (registered via entry points)
energy_solver = get_openph_energy_demand_solver(phpp)
ground_solver = get_openph_ground_solver(phpp)
solar_solver = get_openph_solar_solver(phpp)
# Access demand calculations
heating_demand = energy_solver.heating_demand
cooling_demand = energy_solver.cooling_demand
# Get results. Read the canonical annual scalars; never sum a monthly row.
annual_heating_kwh = heating_demand.total_yearly_heating_demand # Heating!AF117
annual_heating_kwh_m2a = heating_demand.total_yearly_specific_heating_demand # Heating!Q78
annual_cooling_kwh = cooling_demand.total_annual_cooling_demand_kwh # Cooling!AG155
ground_temps = ground_solver.periods
window_radiation = solar_solver.annual_demand
Energy demand summary
For an application such as a web request, a batch parametric run, or a stored record, use the compact summary instead of reading solver properties one at a time:
from openph_demand import build_energy_demand_summary
summary = build_energy_demand_summary(phpp)
payload = summary.to_dict() # JSON-ready
text = summary.to_json() # stable ordering, no NaN/Infinity
{
"schema_version": "1.0",
"floor_area_m2": 307.40378,
"heating": {
"annual_kwh": 7092.797,
"annual_kwh_m2a": 23.073228,
"monthly_kwh": [{"period": "jan", "value": 1552.683}, "…"],
"peak_w": null
},
"cooling": {"annual_kwh": 1769.475, "annual_kwh_m2a": 5.756192, "…": "…"},
"warnings": [],
"engine_versions": {"openph": "0.6.0", "openph-demand": "0.6.0", "PHX": "1.56.82"}
}
Rules for the summary:
- It adds to
openph.results.collect_results, the complete PHPP-addressable audit document, and leaves that document whole. The summary is about 200x smaller and has no worksheet addresses. - Annual values are canonical PHPP results that the builder copies as-is. Callers must not
re-aggregate
monthly_kwh. - Monthly values carry
"jan"through"dec"period labels. Match on the label, never on array position. nullmeans OpenPH does not calculate that quantity. It never means zero, and it is whypeak_wisnulltoday.coolingis sensible useful cooling demand and excludes dehumidification. It differs from the combined PHIVerification!I39figure.schema_versionis independent of the package version. A reader accepts any matching major version, so fields added in1.xstay readable.- The summary is core output and needs neither pandas nor rich.
A subset of the audit document
If you need PHPP-addressable demand records, for QA against a workbook for example, narrow the
audit document with openph.results.ResultSelection instead of collecting everything:
from openph.results import ResultSelection, collect_results
results = collect_results(phpp, selection=ResultSelection(key_prefixes={"energy_demand"}, tiers={"final"}))
The openph README and help(ResultSelection) document the four selectors, how they combine,
and which ones raise instead of matching nothing. This README doesn't repeat them.
One point applies to this package in particular. Selecting energy_demand still constructs
ground and solar_radiation, because the demand solvers call them as calculation
dependencies. The collector won't instantiate a solver you excluded. A selected solver can
still build one.
For a per-request application payload, use build_energy_demand_summary above. It is the
output designed for that job.
Registered solvers
This package registers three solver plugins with OpenPH:
energy_demand→OpPhEnergyDemandSolver(heating + cooling)ground→OpPhGroundSolver(ground heat transfer)solar_radiation→OpPhSolarRadiationSolver(solar gains)
Concurrency
These solvers follow the core contract in ## Concurrency of the openph README and add no
restriction to it. Independent models can be converted, solved, and collected concurrently.
After one thread has solved a model, other threads can read it concurrently. Sharing an
unsolved model is unsupported.
This package is the reason sharing an unsolved model is unsupported instead of only slow.
OpPhGroundSolver runs a ten-iteration solve and assigns each period's heat
flow only when the solve finishes. The heating and cooling calculations read those periods.
Every such read forces the solve first, so a caller always gets solved values. That guarantee
works at read time and uses no lock.
The ground solve must also never move into object construction. The solve reads back into
energy_demand.heating_demand, so forcing it during construction would create a cycle.
openph-demand/tests/test_shared_model_boundary.py tests this behavior.
Development
This package is part of the UV workspace. See context/ENVIRONMENT.md at the workspace root:
uv sync # Install all workspace packages
uv run pytest openph-demand/tests/ # Run tests
Metadata
Release files for openph-demand 0.20.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openph_demand-0.20.0.tar.gz | 107.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openph_demand-0.20.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 218.3 kB
Release files / openph_demand-0.20.0.tar.gz
| Download URL | openph_demand-0.20.0.tar.gz |
|---|---|
| Size | 107.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b09ae003fc606f8b3730925fc4b103bbfaf0abec74df0498532d93cdba107371
|
|
BLAKE2b-256 checksum How to use checksums |
4b986bf9e9c244bcf11ee3ccad09e3187182ad780817e773d59006d403d421c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / openph_demand-0.20.0-py3-none-any.whl
| Download URL | openph_demand-0.20.0-py3-none-any.whl |
|---|---|
| Size | 110.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
37a11a74a47301ff7a936dba1bf2890141a557962b8fdfbd1d3356cf716373a5
|
|
BLAKE2b-256 checksum How to use checksums |
4cba407a751944df14ee800b59bd6fa43cb9d8838909f17a2c21e6f5d363edd0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log