Metadata-Version: 2.4
Name: synload-de
Version: 0.1.0
Summary: Synthetic 15-minute electricity load profiles for German households, labeled by building type, occupancy, and sector-coupled loads (heat pump, EV, electric water heating)
Author-email: Christoph Komanns <komanns@fh-aachen.de>
License-Expression: MIT
Project-URL: Homepage, https://github.com/NOWUM/synload-de
Project-URL: Repository, https://github.com/NOWUM/synload-de
Project-URL: Issues, https://github.com/NOWUM/synload-de/issues
Keywords: load profiles,households,heat pump,electric vehicle,synthetic data,germany,synpro
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24.0
Requires-Dist: pandas>=2.0.0
Requires-Dist: pyarrow>=14.0.0
Requires-Dist: tables>=3.8.0
Provides-Extra: test
Requires-Dist: ruff>=0.4.9; extra == "test"
Requires-Dist: pytest>=7.2.2; extra == "test"
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
Dynamic: license-file

<!--
SPDX-FileCopyrightText: NOWUM Developers

SPDX-License-Identifier: MIT
-->

# synload-de

Synthetic 15-minute electricity load profiles for German households, labeled by building type, occupancy, and sector-coupled loads (heat pump, EV, electric water heating).

Every profile is one point in a full factorial over five dimensions, so each combination of household constellation, building type, heat pump, electric water heating and electric vehicle exists exactly once and carries the labels that produced it. The four load components stay in separate columns, so consumers can decompose a profile, drive flexibility models from a single component, or recombine them differently.

The underlying series come from [synPRO](https://synpro-lastprofile.de/) (Fraunhofer ISE). This repository does the assembly: unit conversion, per-household normalisation, superposition and plausibility checking.

**These are synthetic profiles, not measurements.** See [Known limitations](#known-limitations) before using them for anything that depends on realistic diversity between households.

## Install

```sh
pip install -e .
```

With the test suite:

```sh
pip install -e .[test]
pytest
```

## Quick start

```sh
synload-de build
```

The synPRO exports ship inside the package, so this works from any directory and there is nothing to download.

This writes `output/profiles.csv` (192 rows of metadata) and `output/timeseries.parquet` (6.7 million rows, 56 MB on disk), then prints a benchmark report.

```sh
synload-de build --realizations 3 --target-year 2023 --format csv
synload-de validate
synload-de build --help
```

**Output formats.** `--format` takes `parquet` (the default), `csv` or `hdf5`; all three work out of the box. HDF5 is written as a queryable PyTables table under the key `timeseries`, zlib compressed, with `profile_id` indexed, so a single profile comes out of the file without reading the rest:

```py
one = pd.read_hdf("output/timeseries.h5", "timeseries", where="profile_id == 7")
```

**Memory.** The time series is assembled in memory, at roughly 32 bytes per row: about 215 MB for the default build and 650 MB with `--realizations 3`. Writing needs roughly twice the peak. Each build logs its projected size and warns above 750 MB, so check that line before starting a large run on a small machine. `--restrict-family-houses` cuts the row count by 38 %, and building in several runs with different `--seed` values is always an option.

Library use:

```py
from synload_de import (
    build_combinations,
    build_dataset,
    load_inputs,
    PACKAGED_RAW_DATA,
    report,
    validate,
)

inputs = load_inputs(PACKAGED_RAW_DATA)
profiles, timeseries = build_dataset(inputs, combinations=build_combinations())

print(report(validate(profiles)))

# Energy per component on a winter day, in kWh
one = timeseries[timeseries["profile_id"] == 0].set_index("datetime")
components = ["base_load_kw", "hp_load_kw", "dhw_load_kw", "ev_load_kw"]
print(one.loc["2021-01-15", components].sum() * 0.25)  # 0.25 h per step
```

## What the dataset contains

`profiles.csv`, one row per profile:

| Column | Meaning |
| --- | --- |
| `profile_id` | Row id, and the join key to the time series |
| `realization` | 0 for the base case, higher for diversified copies |
| `constellation`, `n_persons` | `single_person_under30`, `2_fulltime_employees`, `2_persons_over65`, `family` |
| `building_type`, `building_class`, `single_family` | `{old_building, passive}_{single_family, multi_party}_house`. `old_building` means unrenovated 1979 to 2001 stock, not pre-war; `passive` is post-2002 stock brought to passive house standard |
| `heat_pump`, `electric_water_heating`, `electric_vehicle`, `electric_vehicle_type` | Consumer labels; the vehicle type is `tesla`, `vw_eup` or the explicit string `none` |
| `n_apartments`, `floor_area_m2` | Apartments the source building was simulated with, and the floor area of one of them |
| `specific_heat_demand_kwh_m2` | Heat demand per square metre of apartment |
| `annual_{base,hp,dhw,ev,total}_kwh` | Annual electricity per component |
| `hp_thermal_kwh`, `hp_spf` | Delivered heat and the resulting seasonal performance factor |
| `peak_load_kw` | Highest 15-minute total load |

`timeseries.parquet`, long format, 35 040 rows per profile:

| Column | Unit |
| --- | --- |
| `datetime` | UTC, 15-minute grid, 2021 unless `--target-year` is given |
| `profile_id` | Join key to `profiles.csv` |
| `base_load_kw` | Household appliances and lighting |
| `hp_load_kw` | Heat pump electricity |
| `dhw_load_kw` | Electric instantaneous water heater |
| `ev_load_kw` | Home charging |
| `total_load_kw` | Sum of the four components |

Resulting spread across the 192 profiles: 1 320 to 19 880 kWh per year, peak load 2.8 to 26.1 kW.

## How the raw series are converted

Three normalisation steps do the real work, and each corrects a mistake that is easy to make and hard to see in a plot.

**Thermal to electric.** `Q_htg` and `Q_dhw` in the synPRO export are *thermal* power, not electric. Space heating is divided by a temperature-dependent COP,

```
COP = 0.45 * (T_flow + 273.15) / (T_flow - T_amb),  clipped to [1.5, 5.5]
```

with `T_flow` at 55 °C for unrenovated buildings (radiators) and 35 °C for passive houses (underfloor heating). A constant COP would flatten exactly the winter peak that matters for grid studies. Hot water is assumed to come from a resistive instantaneous heater, so its COP is 1.

The resulting seasonal performance factors are 2.82 for old buildings and 4.0 to 4.1 for passive houses, inside the 2.6 to 5.4 band measured across the Fraunhofer ISE heat pump field trials. `--cop-mode constant` replaces the curve with a flat COP, as an escape hatch rather than a recommendation.

**The ambient temperature is the one synPRO used.** The packaged `heat/` directory contains `meta_info_used_weather_data_TRY2010_6_Jahr.dat`, the DWD test reference year the heat demand was simulated against — region 6, representative station Bad Marienberg, a synthetic mean year of 7.79 °C assembled from 1988 to 2007 observations. It is the only temperature source, which is the point: heat demand and COP see the same atmosphere, and the two correlate at -0.79. Substituting observed weather of some calendar year would drop that to about -0.60.

The file is found by glob, so re-exporting the profiles for a different climate region works without a code change as long as exactly one such file sits in the `heat/` directory. The simulated year is read from the profiles' own date column rather than assumed, so a re-export for a different year works too — except a leap year, which is rejected with an explanation, because a reference year has only 8760 hours.

**Whole building to one household.** synPRO reports multi-party demand for the entire building. The multi-party files cover four apartments, so heat and hot water are divided by `number of apartments` read from the file header.

`meta_info_used_building_data.txt` documents the reference buildings the simulations were configured from, and it names a different apartment count: 6 where the profile header records 4. The header wins, and its own numbers show why. The header also states a specific heating load of 151 kWh/m², which is per apartment, so it fixes the apartment size:

| Apartments assumed | kWh per apartment | implied apartment area |
| --- | --- | --- |
| 4 | 12 099 | **80.1 m²** |
| 6 | 8 066 | 53.4 m² |

The reference apartment in that file is 74.5 m², so 4 fits and 6 does not. The hot water file agrees independently: 8 occupants across 4 apartments is 2.0 people per flat, against 1.33 at six. `synload-de validate` recomputes this chain on every build, so a wrong divisor shows up as a 50 % deviation instead of the 2 % tolerance.

The metadata file is read by nothing in the package; the normalisation works entirely from the profile headers.

**Household total to household size.** The hot water file is the total for the occupancy it was simulated with, not a per-person series. Scaling to a different household size means multiplying by `n_persons / occupants_per_apartment`. Multiplying by `n_persons` alone overstates demand by up to a factor of four.

Only the `p_evse_home_w` column of the vehicle files enters a household profile; those files also record charging at work and at public chargers.

Superposition is done positionally on numpy arrays with explicit length checks. Adding pandas Series would align on the index and silently produce NaN.

## Known limitations

**Coincidence factor of 1.0 by default.** The raw data holds a single series per constellation, building type and vehicle, so all households of one combination are bit-for-bit identical and their peaks align perfectly. Aggregating them to a feeder or portfolio will badly overstate the peak, because real households never charge or cook in lockstep. `--realizations N` applies a seeded circular time shift and energy scaling to break the coherence. That is diversification of one simulation, not independent resampling of synPRO, and it does not reproduce the diversity of a real household population.

**Temperature is hourly, the profiles are quarter-hourly.** The reference year gives one value per hour, interpolated onto the 15-minute grid, so sub-hourly temperature swings do not reach the COP. The residual mismatch between heat demand and temperature (-0.79 rather than closer to -1.0) is mostly physics rather than data: thermal inertia, solar gains, night setback and the summer-zero floor all break the linear relation. Passive houses sit between -0.49 and -0.54 because gains dominate their demand.

**Demand only.** The dataset models electricity consumption and nothing else. There is no on-site generation, no storage and no sizing attributes for either, so every load column is gross consumption at the meter. Anyone wanting net load has to bring their own generation series and subtract it.

**Not representative of the building stock.** The factorial weights every combination equally. It is a coverage grid, not a sample: do not read the mean over all profiles as a German average. By default any constellation may occupy any building type, because one and two person households, especially over 65, are the most common occupants of German single-family houses. `--restrict-family-houses` reverts to placing only four-person households there.

**Relabelling the year does not change the weather.** `--target-year` shifts the axis while preserving weekdays, so a 2023 run starts on 30 December 2022. The load still carries the reference year's weather; combining it with another year's spot prices or generation data mixes weather years.

## Data provenance

`synload_de/raw_data/` holds unmodified synPRO exports, including the two documentation files Fraunhofer ships with the heat profiles: `meta_info_used_weather_data_TRY2010_6_Jahr.dat`, the DWD test reference year the build reads, and `meta_info_used_building_data.txt`, the reference building parameters, which nothing in the package reads. They are distributed with the package so that an installation can build the dataset without a separate download.

Fraunhofer ISE states in the file headers that it accepts no liability for the correctness or completeness of the data, and that liability claims for damage caused by using it are rejected. That notice applies to everything in that directory.

## Licensing

Two licences apply to this repository, and they cover different things.

**The code** — everything under `synload_de/` except `raw_data/`, plus the tests — is MIT licensed. See [LICENSE](./LICENSE).

**The data** in `synload_de/raw_data/` comes from Fraunhofer ISE and is accompanied by the Community Data License Agreement – Permissive, Version 2.0, whose full text ships alongside it as `CDLA-Permissive-2.0.pdf`. CDLA-Permissive-2.0 allows the data to be used, modified and shared, provided the agreement text and the attribution notices travel with it — which is why the licence file is packaged together with the profiles rather than left behind.

Anyone redistributing this package or the data in it should read that agreement, and should confirm the current terms with [Fraunhofer ISE](https://synpro-lastprofile.de/) rather than relying on this summary.
