Stochastic occupancy and electricity-demand profile modeling for households and service buildings, for UU-BUEM. Feeds UU-BUEM/buem (a pure envelope/thermal model) with the occupancy/internal-gains/electricity input it doesn't generate itself.
This package generates stochastic hourly occupancy states and derives total hourly electricity demand from those states, for:
- Households — via composition archetypes (
generic,working_couple,family_with_children,retired_single,student_shared, ...). - Service buildings —
supermarket,office,restaurant,school,hotel,bakery,warehouse,clinic, easily extended with more types.
Core models:
- Household occupancy:
HouseholdProfile(aliased asOccupancyProfile) - Household electricity:
ElectricityConsumptionProfile - Service building occupancy + electricity:
ServiceBuildingProfile
Everything — occupancy probabilities, household archetypes, appliance/
equipment specs, service-building schedules — is config-driven JSON, not
hardcoded Python. Adding a new appliance, household archetype, or building
type is a config addition; see CLAUDE.md and
docs/plans/equipment-service-buildings-architecture.md for the extension
points and design rationale.
occupancy/
├── src/
│ └── occupancy/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py
│ ├── config/ # CLI-facing ScenarioConfig
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── data/default_scenario.json
│ ├── core/ # shared engine (households + services_buildings)
│ │ ├── equipment.py # EquipmentSpec + trigger-strategy registry
│ │ ├── occupancy_engine.py # generator-strategy registry
│ │ ├── loader.py
│ │ └── result.py # OccupancyResult
│ ├── households/
│ │ ├── __init__.py # HOUSEHOLD_ARCHETYPES registry
│ │ ├── archetypes.py
│ │ ├── household_profile.py
│ │ ├── electricity.py
│ │ └── data/
│ │ ├── archetypes/*.json
│ │ └── equipment.json
│ ├── services_buildings/
│ │ ├── __init__.py # SERVICE_BUILDING_TYPES registry
│ │ ├── building_profile.py
│ │ ├── building_types.py
│ │ ├── supermarket.py
│ │ ├── office.py
│ │ ├── restaurant.py
│ │ ├── school.py
│ │ ├── hotel.py
│ │ ├── bakery.py
│ │ ├── warehouse.py
│ │ ├── clinic.py
│ │ └── data/<type>/{schedule,equipment}.json
│ └── visualization/
│ ├── __init__.py
│ └── plots.py
├── infrastructure/
│ ├── env/
│ │ └── occupancy_env.yml
│ └── container/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── occupancy.def
├── tests/
├── pyproject.toml
├── meta.yaml
├── setup.ps1
└── setup.bat
# 1. Create/update the conda environment and install the 'occupancy' command:
.\setup.ps1 # PowerShell
setup.bat # CMD
# 2. Activate and verify:
conda activate occupancy_env
occupancy --help
conda develop srcis not used here.setup.ps1creates the conda environment and runspip install -e . --no-depsfrom the repo root so the editable install works correctly.
Generate a household occupancy profile (defaults to the generic archetype):
occupancy --year 2026 --persons 3 --seed 42 --output outputs/occupancy.csvGenerate occupancy + electricity for a specific household archetype:
occupancy --year 2026 --persons 2 --archetype working_couple --seed 42 \
--include-electricity --output outputs/working_couple.csvGenerate a service building profile:
occupancy --building-type supermarket --year 2026 --seed 42 \
--include-electricity --output outputs/supermarket.csvRun a fully reproducible scenario from config:
occupancy --config path/to/scenario.jsonEvery numerical default — occupancy probabilities, household archetypes,
appliance/equipment specs, service-building schedules — is bundled JSON
under each subpackage's own data/ folder (config/data/,
households/data/, services_buildings/data/), loaded via
importlib.resources. No hardcoded arrays in Python source.
| Location | Contents |
|---|---|
config/data/default_scenario.json |
Year, persons/capacity, seed, building type, appliance flags, output path |
households/data/archetypes/*.json |
Per-archetype occupancy probabilities, generator strategy, equipment overrides |
households/data/equipment.json |
37 household appliances as EquipmentSpec rows (29 sourced from the CREST Domestic Electricity Demand Model; 8 added for 2026 NL households, mostly estimated — see CHANGELOG.md) |
services_buildings/data/<type>/schedule.json |
Capacity, generator strategy + params (open hours, etc.) |
services_buildings/data/<type>/equipment.json |
That building type's EquipmentSpec rows |
Pass a custom scenario JSON via --config to override any subset of the
scenario/occupancy/electricity fields without touching the bundled defaults.
Adding a new appliance, household archetype, or service-building type is a
JSON addition — see CLAUDE.md for the registry mechanism.
UU-BUEM/buem's ModelBUEM requires four
pd.Series in its cfg dict — Q_ig (internal gains, kW), elecLoad
(electric load, kW), occ_nothome (fraction of occupants away, 0-1),
occ_sleeping (fraction asleep, 0-1) — and raises ValueError if any is
missing (buem.thermal.model_buem.ModelBUEM._addPara/
_addConstraints_sequential). occupancy.to_buem_profiles() builds all
four from an OccupancyResult:
from occupancy import (
ElectricityConsumptionProfile,
HouseholdProfile,
ServiceBuildingProfile,
to_buem_profiles,
)
# Service buildings include equipment power by default.
office = ServiceBuildingProfile(building_type="office", year=2025, seed=1)
buem_inputs = to_buem_profiles(office.to_result())
# Households: equipment lives in ElectricityConsumptionProfile, not
# HouseholdProfile itself -- use its `.to_result()`, not the bare profile's.
household = HouseholdProfile(num_persons=3, year=2025, seed=1)
elec = ElectricityConsumptionProfile(occupancy_profile=household, seed=1)
buem_inputs = to_buem_profiles(elec.to_result())
cfg = {**cfg, **buem_inputs} # merge into buem's cfg dictbuem_inputs is {"Q_ig": pd.Series, "elecLoad": pd.Series, "occ_nothome": pd.Series, "occ_sleeping": pd.Series}, indexed the same as result.profile.
Notes:
Q_igis derived fromn_present/n_active, split into present-but- inactive vs. active occupants, each with its own per-occupant heat-gain constant. Those constants come fromheat_gain_present_kw/heat_gain_active_kwon the profile's originating archetype (households/data/archetypes/*.json) or building type (services_buildings/data/<type>/schedule.json) — e.g. office (0.100/0.130 kW) vs. supermarket (0.100/0.180 kW) vs. school (0.085/0.120 kW) — illustrative, ISO 7730 / ASHRAE Fundamentals Ch. 18-informed, not yet survey-calibrated. Passgain_present_kw/gain_active_kwexplicitly to override.occ_sleepingis real generator output:core/occupancy_engine.py's generators emit ann_asleepcolumn (drawn from the profile'sasleep_probabilities), andocc_sleeping = n_asleep / num_persons— for any building type, not just households. Most service-building types never setasleep_probabilities(they're never occupied overnight) son_asleepstays0for them, buthoteldoes and gets genuine sleeping occupants through the same mechanism. A fixed 23:00–07:00 window heuristic is used only as a household-only fallback for profiles generated before this column existed.- buem (
UU-BUEM/buem)'scfg_attribute.pynow importsfrom occupancy import ElectricityConsumptionProfile, HouseholdProfile, to_buem_profilesand calls them in exactly the pattern shown above — the earlierbuem_occupancypackage-name mismatch this section used to document has been fixed on buem's side.
# Build the image (bind-mounts src/ at runtime, including package data):
docker compose -f infrastructure/container/docker-compose.yml build
# Run (src/ is bind-mounted for live development):
docker compose -f infrastructure/container/docker-compose.yml up
# Standalone run:
docker run --rm occupancy:latest python -m occupancy --helpOutput CSV is written to outputs/ on the host via bind mount.
ruff format .
ruff check .
pytest
mypy src
python validate.py