Building_eload
Version: 0.10.0
District-level building energy simulation at hourly resolution.
This model was first presented in the research article : https://doi.org/10.1016/j.enbuild.2026.117409
building_eload is developed as a library, released on PyPI. The Snakemake
workflow reproducing the paper's results lives in a separate repository
(building_eload_paper), pinned to the published 0.4.x releases of this
package.
Overview
The model is a two-stage pipeline, split across two packages since 0.9.0:
- Static simulation — annual building-level energy estimates and district
calibration. This stage now lives in the separate
buildingcalibrationpackage (buildingcalibration.calibration), together with the validation (.validation), the representative-district clustering (.clustering) and their figures (.plots). - Dynamic simulation — hourly electricity load profiles from those static outputs. This package.
building_eload is therefore a pure dynamic-simulation library: the hourly
end-use models (models/), the simulation orchestrator
(core/dynamic_simulation) and the load-curve plots. It does not import
buildingcalibration, and buildingcalibration does not import it — the
handover is either a set of parquet files on disk or an in-memory object
matching core.dynamic_simulation.StaticResultsLike (see
DynamicSimulation.from_static_results below).
Main dependencies:
buildingdata(reference datasets: ELMAS non-residential load curves, occupant activity diaries, ERA5-derived climate)heatpumpmodel(the opt-in heat-pump performance core)
Installation
git clone https://git.persee.minesparis.psl.eu/planeterr/building_eload.git
cd building_eload
pip install -e .
The static-calibration half of the pipeline is a separate install:
pip install buildingcalibration
Reproducibility
With a fixed DynamicParameters.seed, a run is deterministic to float
accumulation noise: every stochastic decision (occupant profile draw,
vacation schedule, heating-season detection, and — since 0.11.0 — the
dwelling occupancy state, which is now a physical rule with no tie-break) is
reproducible, and two runs of the same code differ only by floating-point
summation order (≤1.6e-4 absolute on per-building hourly electricity,
independent of the thread count; annual totals and validation metrics
unchanged to ~1e-5 relative). The rerun-comparison contract used by the
compute1 equivalence study is 1e-3 absolute on float32 columns and 1e-10 on
float64. Bit-identity across runs is not a claim this library makes.
Time Basis: the Axis is UTC, the Schedules are Local
Every frame the dynamic simulation produces — weather, dwelling_model,
energy_model_static, energy_model_hourly and the result parquets — is
indexed by a timezone-naive datetime holding UTC instants. The axis comes
from the ERA5-derived EPW file, whose hours are UTC. Joining these profiles to
metered data labelled in local time (Enedis, a French feeder) therefore needs a
conversion on the consumer's side; the labels are not wall-clock hours.
Two inputs are on a local clock instead, and are converted at their own
boundary (building_eload.utils.time_alignment, issue #14):
- the DHW production windows (
hot_water_modes'hour_map, paper Tab. 4) — a "22–01 h" off-peak tank runs 22–01 h on the tenant's clock; - the ELMAS non-residential load curves, whose hour labels are French legal time.
DynamicParameters(schedule_time_zone=...) selects that clock. It defaults to
"Europe/Paris"; "UTC" applies the two schedules to the axis verbatim, which
is what the library did before 0.15.0 — their load landing 1 h late in winter
and 2 h late in summer.
Four inputs are on that local clock, and all four are converted at their own
boundary: the DHW windows and the ELMAS curves (0.15.0), and the occupant
activity diaries and the cooking meal windows keyed off them (next release).
The diaries' basis is readable from the book — Hygiene peaks at 07:00,
Cooking at 11:50, Sleep rises at 23:00 — and the weather axis was checked
the same way: irradiance-weighted solar noon sits at 12.2–12.9 h in January
and July, season-invariant, so it really is UTC.
Validating against metered data needs the matching
buildingcalibration. Its validation stage used to reindex the measured Enedis curve positionally onto the year's hours, which put it on a fixed UTC+1 clock; it now joins on the true instant. The two corrections only make sense together — on the paper's national validation, either alone is worse than neither (11.614 → 15.181 model-only, → 13.417 axis-only) while the pair lands at 12.551. On 0.15.0 without thatbuildingcalibrationfix,schedule_time_zone="UTC"is the coherent setting. See issue #14.
Data: the Caller Owns the Layout
Since 0.10.0 this package has no path registry at all. The data_path
dict and plot_path constant that used to live in building_eload/__init__.py
— eight <checkout>/data/... paths resolved at import time — are gone
(improvement plan, task G8), which completes for the dynamic core the doctrine
buildingcalibration was extracted under in 0.9.0:
- External open data is never a path. It comes from a
buildingdatagetter, which owns download, cache, vintage and schema. Nothing needs a localdata/tree out of the box. - Pipeline intermediates are frames first, explicit paths second, and have
no default. A missing one raises a
ValueErrornaming the parameter, instead of silently reading from someone else's checkout.
What that means per input:
| Input | How you supply it |
|---|---|
| Occupant activity diaries | nothing — defaults to buildingdata.get_occupant_diaries(). Or pass DynamicParameters(activity_file=...): a parquet path, or an already-loaded polars.DataFrame (cheaper across many districts). |
| ELMAS non-residential load curves | nothing — buildingdata.get_elmas(table=...), called by NonResidentialModel. |
| Climate (EPW) | the static results name the file; DynamicParameters(climate_path=...) is the root a bare name resolves against, and an absolute path bypasses it. Fetch one with buildingdata.get_era5_climate(lat, lon, year) and pass its path — the dynamic stage carries no district coordinates, so it cannot fetch one for you. |
| Static-stage results (this stage's inputs) | DynamicSimulation.from_static_results(results, ...) (in memory), or DynamicParameters(input_path=...) + DynamicSimulation.from_files(...) (parquet). |
| Dynamic results (this stage's outputs) | DynamicParameters(output_path=...), required by save_results() only — a run that keeps its results in memory needs no path at all. output_folder is an optional run sub-directory of it. |
| Figures | save_plot(..., save_path=...); unset, figures go to <cwd>/plot. |
The paper workflow's conventional tree (nothing in the library knows about it;
it is data/ in the repo root, untracked and multi-GB):
data/
├── climate/ # ERA5-derived weather files (EPW)
└── simulation/
├── static_simulation/<year>/ # -> DynamicParameters(input_path=...)
└── dynamic_simulation/<year>/ # -> DynamicParameters(output_path=...)
The hermetic test suite (pytest -m "not integration") never touches it; the
integration tests do, and they declare that layout themselves in
building_eload/tests/conftest.py (BUILDING_ELOAD_DATA overrides the root)
— in the caller, where it belongs.
Current Core API
Dynamic simulation (building_eload.core.dynamic_simulation)
Main classes:
DynamicParametersDynamicSimulation
Primary methods used by users:
DynamicSimulation.from_files(district_id, parameters)DynamicSimulation.from_static_results(static_results, parameters)DynamicSimulation.run()DynamicSimulation.save_results()DynamicSimulation.plot_results(...)
The static-stage seam (StaticResultsLike)
from_static_results accepts any object carrying the seven attributes the
dynamic stage reads — district_id, climate_year, climate_path,
climate_file, residential_buildings, dwellings,
non_residential_buildings. That contract is published as a
typing.Protocol:
from buildingcalibration.calibration import StaticSimulation, StaticParameters
from building_eload.core.dynamic_simulation import (
DynamicParameters, DynamicSimulation, StaticResultsLike,
)
static_results = StaticSimulation("262320000", StaticParameters(...)).run()
assert isinstance(static_results, StaticResultsLike) # runtime-checkable
sim = DynamicSimulation.from_static_results(static_results, DynamicParameters(year=2023))
Neither package imports the other; the isinstance check is the whole
coupling. Passing files instead (from_files) needs no adapter at all.
Static calibration, validation, clustering
Moved to buildingcalibration in 0.9.0 — same public names, new import paths:
buildingcalibration.calibration (StaticParameters, StaticSimulation,
StaticResults, BuildingModelResults, StaticProcessor, BuildingLoader,
the representative-district selectors), buildingcalibration.validation
(Validation), buildingcalibration.clustering (cluster_districts,
screen_unreliable_iris*) and buildingcalibration.plots.
Minimal Usage
Tutorials for the dynamic simulation are available in /doc/tutorials; the
static/validation ones moved with their domain (see
doc/tutorials/moved_to_buildingcalibration.md).
1) Dynamic simulation
from building_eload.core.dynamic_simulation import DynamicParameters, DynamicSimulation
district_id = "262320000"
params = DynamicParameters(
year=2023,
run_non_residential=True,
run_again=True,
)
sim = DynamicSimulation.from_files(district_id=district_id, parameters=params)
sim.run()
sim.save_results()
2) Dynamic simulation with the heat-pump model (opt-in)
The conversion of hourly heating demand into heating electricity happens in
a single place, building_eload.models.heating_system. Leaving
DynamicSimulation.heating_system at None keeps the published
constant-efficiency (static-COP) behaviour; assigning a converter swaps the
model without touching anything else:
from building_eload.models import heat_pump as hp
from building_eload.models.heat_pump_system import HeatPumpHeatingSystem
# Defaults: air-to-water, medium-temperature radiators with weather
# compensation, inverter, monovalent with an electric-resistance backup --
# the reference configuration of Rogeau et al. (2024). Applied to the
# buildings whose `heating_system` is "electric heat pump"; every other
# building keeps the published conversion.
sim.heating_system = HeatPumpHeatingSystem(
hp.HeatPumpConfig(
system=hp.System.A_W,
mode=hp.Mode.M,
emitter=hp.Emitter.FH, # floor heating; the biggest SCOP lever
technology=hp.Technology.INVERTER,
)
)
sim.run()
The hourly results then carry heat_pump_electricity_need and
heating_backup_electricity_need alongside the usual
heating_electricity_need (their sum), plus heat_pump_heat_delivered.
Air-source configurations require the weather frame's humidity column and
fail loudly without it, rather than silently disabling the defrost derate.
Each building runs the heat pump its own EPC describes. When the static
output carries the DPE heat-pump detail (heat_pump_type,
heating_emitter_type, heat_pump_installation_period — written by
buildingmodel >= 1.3.0 from buildingdata >= 0.6.0), the converter resolves
system / emitter / technology per building through
heatpumpmodel.dpe.config_from_dpe — the same mapping buildingmodel's
static stage resolves its per-archetype SCOPs with, so a building is the same
machine in both stages — and runs the physics once per distinct archetype. The
config above is then the fallback for buildings whose DPE match carries no
heat-pump detail, and the source of every axis the DPE does not carry (mode,
T_lim, the sub-models). Nothing changes for a static output produced before
those columns existed. A scenario that means to impose one machine on the whole
district ("every heat pump on floor heating") opts out explicitly:
sim.heating_system = HeatPumpHeatingSystem(config, use_dpe_detail=False)
Which archetype each building actually ran as is readable afterwards from
sim.heating_system.config_by_building.
Running Simulations
The former building_eload.scripts entry points were removed in 0.8.0; use
the public API instead (the tutorials in doc/tutorials/ walk through each
step):
building_eload.core.dynamic_simulation.DynamicSimulation— dynamic hourly simulation (this package)buildingcalibration.calibration.StaticSimulation— static annual calibration (moved out in 0.9.0)buildingcalibration.validation.Validation— validation against measured consumption (moved out in 0.9.0)buildingcalibration.clustering.cluster_districts— representative-district selection (moved out in 0.9.0)- EPW climate generation lives in the
buildingdatapackage (buildingdata.prefetch_era5for the bulk ERA5 download,buildingdata.get_era5_climatefor per-point EPW files)
For an end-to-end orchestrated pipeline, see the Snakemake workflow in the
building_eload_paper repository.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file building_eload-0.16.0-py3-none-any.whl.
File metadata
- Download URL: building_eload-0.16.0-py3-none-any.whl
- Upload date:
- Size: 130.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15d8ab5898512f82e7a9bfdd0ae19a3d54a9daad31119e2e6e1d50082493558c
|
|
| MD5 |
808a4113f6eca99231d401e0b531748d
|
|
| BLAKE2b-256 |
4da65411abb89591b6185be0d9542ee0280d2bdbb8f801e0fb11883fe71a53df
|