Skip to main content

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.
  • null means OpenPH does not calculate that quantity. It never means zero, and it is why peak_w is null today.
  • cooling is sensible useful cooling demand and excludes dehumidification. It differs from the combined PHI Verification!I39 figure.
  • schema_version is independent of the package version. A reader accepts any matching major version, so fields added in 1.x stay 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.19.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openph-demand 0.19.0
File Size Uploaded
openph_demand-0.19.0.tar.gz 107.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for openph-demand 0.19.0
File Interpreter ABI Platform
openph_demand-0.19.0-py3-none-any.whl Python 3 none any Details

Total release size: 218.3 kB

Release files / openph_demand-0.19.0.tar.gz

Download URL openph_demand-0.19.0.tar.gz
Size 107.7 kB
Tags Source
SHA-256 checksum
How to use checksums
fde5150c2d06a2d100a8df7eb34be9cf41265a268eb9c2a3b0ba224c20d97bba
BLAKE2b-256 checksum
How to use checksums
f7a1dbd2ca88d3343ca225e5eebaf3b09d9ccda54dfaf558f767fb3aa269e5b7
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

Release files / openph_demand-0.19.0-py3-none-any.whl

Download URL openph_demand-0.19.0-py3-none-any.whl
Size 110.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d11f6f6f28f5f3fef9b6b1b9232ebbc70418e48c4a7949ba2ddabbbb90ebe2ce
BLAKE2b-256 checksum
How to use checksums
c42afe457ce7d2e793cdc9d2da7d689e3df41988f963d7235694029fd5d93ac1
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

Release history Release notifications | RSS feed

0.20.0

2 release files

This release

0.19.0 This release

2 release files

0.17.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page