🔋 Represent energy systems as Python data classes for improved modularity and readability
EnergyDataModel provides an open-source, Python-based data model that enables energy data scientists and modellers to write more modular and readable code. EnergyDataModel lets you:
- 🧱 Modularity - Represent energy assets, energy systems and other relevant concepts as object-oriented building blocks;
- 🏗️ Relationships - Structure your energy assets in graphs and hierarchies representing energy systems that can be serialized to files (e.g. .csv, .json, and .geojson files);
- 🤓 Readability - Write more explicit Python code through human-readable expressions and built-in convenience methods;
- 🧩 Interoperability - Convert data format to other energy-relevant data models and ontologies; and
- 💬 Communicate - Communicate effectively in teams with a common energy system data vocabulary.
⬇️ Installation | 📖 Documentation | 🚀 Try out now in Colab | 👋 Join Slack Community
Class hierarchy
Everything in EnergyDataModel inherits from a single root, Element, which carries identity (id: UUID, name), a list of attached TimeSeries (typically metadata-only — declarations of the series this element exposes), an optional shapely geometry, and an extra dict for ad-hoc JSON-scalar fields. Identity is a UUID7 generated at construction time — stable across renames, round-trips through JSON, and (when paired with EnergyDB) sits as the row primary key in PostgreSQL.
Three sibling subtrees specialize Element, plus an Asset mixin:
Node— graph vertices (equipment, areas, sensors, grid topology points). Addsmembersandtz.Edge— relationships between two nodes (lines, transformers, interconnectors). Addsfrom_element,to_element,directed. Endpoints areReference[T]objects holding the target's UUID.Collection— logical groupings (Portfolio, Site, ...). Addsmembersandtz. Not a graph vertex —isinstance(portfolio, Node)is False.Asset— cross-cutting mixin marking physical equipment. Addscommissioning_date. Mixed into Node viaNodeAssetand into Edge viaEdgeAsset; never used as a leaf type.
System-structural classes (containers, areas, networks, bases, utilities) live flat at edm.X. Technology-specific equipment lives under sub-namespaces — edm.solar.PVSystem, edm.wind.WindTurbine, edm.grid.Line, etc.
| Namespace | Data Classes |
|---|---|
🧱 edm (core) |
Element, Node, Edge |
🏷️ edm (bases) |
Asset, NodeAsset, GridNode, Sensor |
☀️ edm.solar |
FixedMount, SingleAxisTrackerMount, PVArray, PVSystem, SolarPowerArea |
🌬️ edm.wind |
WindTurbine, WindFarm, WindPowerArea |
🔋 edm.battery |
Battery |
💦 edm.hydro |
Reservoir, HydroTurbine, HydroPowerPlant |
♻️ edm.heatpump |
HeatPump |
🏠 edm.building |
Building, House |
🌡️ edm.weather |
TemperatureSensor, WindSpeedSensor, RadiationSensor, RainSensor, HumiditySensor |
⚡ edm.grid |
nodes: JunctionPoint, Meter, DeliveryPoint, Transformeredges: Line, Link, Pipe, Interconnection, EdgeAssetother: Carrier, SubNetwork, Network |
🗺️ edm (area) |
Area, BiddingZone, Country, ControlArea, WeatherCell, SynchronousArea |
📦 edm (containers) |
Collection, Portfolio, Site, MultiSite, Region, EnergyCommunity, VirtualPowerPlant |
📈 edm (constructors) |
electricity_supply, electricity_demand, electricity_supply_area, electricity_demand_area, spot_price, cross_border_flow, grid_frequency, temperature, gas_supply, gas_demand, heating_demand |
Explore the data model visually here.
Read the full documentation here.
Purpose and Philosphy
The aim of EnergyDataModel is to provide the energy data and modelling community with a Python-based open-source tool to enable improvement of software engineering aspects like code quality, maintainability, modularity, reusability and interoperability. We believe that bringing more rigorous software engineering practices to the energy data community has the potential to radically improve productivity, collaboration and usefulness of software tools, utimately leading to better energy decisions.
Our philosophy is aligned on usefulness and practicality over maximizing execution performance or some kind of esoteric theoretical rigor. A well-know quote by Abelson & Sussman comes to mind:
"Programs [software] are meant to be read by humans and only incidentally for computers to execute"
Making code explicit, readable and intuitive counts.
If you are interested in joining our mission to build open-source tools that improve productiveness and workflow of energy modellers worldwide - then join our Slack!
Basic usage
Create an energy system made up of two sites with co-located solar, wind and batteries and save as a JSON-file.
import energydatamodel as edm
from shapely.geometry import Point
pvsystem_1 = edm.solar.PVSystem(name="PV-1", capacity=2400, surface_azimuth=180, surface_tilt=25)
windturbine_1 = edm.wind.WindTurbine(name="WT-1", capacity=3200, hub_height=120, rotor_diameter=100)
battery_1 = edm.battery.Battery(name="B-1", storage_capacity=1000, min_soc=150, max_charge=500, max_discharge=500)
site_1 = edm.Site(name="Site-1",
geometry=Point(64, 46), # (lon, lat)
members=[pvsystem_1, windturbine_1, battery_1])
pvsystem_2 = edm.solar.PVSystem(name="PV-2", capacity=2400, surface_azimuth=180, surface_tilt=25)
windturbine_2 = edm.wind.WindTurbine(name="WT-2", capacity=3200, hub_height=120, rotor_diameter=100)
battery_2 = edm.battery.Battery(name="B-2", storage_capacity=1000, min_soc=150, max_charge=500, max_discharge=500)
site_2 = edm.Site(name="Site-2",
geometry=Point(58, 51),
members=[pvsystem_2, windturbine_2, battery_2])
portfolio = edm.Portfolio(name="My Portfolio", members=[site_1, site_2])
portfolio.to_json()
Modeling a synchronous area with grid frequency
import energydatamodel as edm
nsa = edm.SynchronousArea(
name="NSA",
nominal_frequency=50.0,
members=[
edm.BiddingZone(name="SE-SE1"),
edm.BiddingZone(name="SE-SE2"),
edm.BiddingZone(name="NO1"),
edm.BiddingZone(name="FI"),
],
timeseries=[edm.grid_frequency()],
)
For more examples on usage and applications of EnergyDataModel see the documentation examples page here.
Extending EnergyDataModel
EDM is built to be extended. To add your own asset type, just inherit from the closest base — usually edm.NodeAsset for physical equipment, edm.grid.EdgeAsset for things connecting two nodes, or edm.Element for anything else. Use @dataclass(repr=False, kw_only=True) to match the rest of the model.
from dataclasses import dataclass
import energydatamodel as edm
@dataclass(repr=False, kw_only=True)
class Electrolyzer(edm.NodeAsset):
capacity_kw: float | None = None
efficiency: float | None = None
Your class is automatically registered for JSON round-trips — no decorator, no extra setup:
e = Electrolyzer(name="EL-1", capacity_kw=5000, efficiency=0.65)
site = edm.Site(name="H2 Site", members=[e])
raw = site.to_json()
restored = edm.Element.from_json(raw)
assert isinstance(restored.members[0], Electrolyzer)
The only requirement is that the module defining your class is imported before from_json is called. EDM looks up types by name in a process-wide registry, so the class must be defined for the loader to find it. In practice this means a single import my_assets (or from my_assets import Electrolyzer) at the top of any script that loads saved models — the same pattern used by every Python registry library. If the loader can't find a type, it raises ValueError: Unknown Element type 'Electrolyzer'.
For one-off custom fields that don't justify a new class, every Element carries an extra: dict[str, JSONScalar] you can populate freely:
pv = edm.solar.PVSystem(name="PV-1", capacity=2400, extra={"owner": "Acme", "asset_id_legacy": 17})
Values in extra are restricted to JSON-native scalars (str, int, float, bool, None) plus nested dict / list of the same. EDM types, enums, and shapely geometries are rejected — for typed values, define a typed subclass instead. This restriction is validated at to_json time and surfaces a clear TypeError when violated.
Cross-tree references
Edges and other cross-cutting links use Reference[T] to point at another Element by its UUID. References are valid the moment they're constructed and resolve lazily against an Index:
se4 = edm.BiddingZone(name="SE4")
dk2 = edm.BiddingZone(name="DK2")
icx = edm.grid.Interconnection(
name="SE4-DK2",
from_element=edm.Reference(se4), # captures se4.id
to_element=edm.Reference(dk2),
capacity_forward=1700,
capacity_backward=1300,
)
portfolio = edm.Portfolio(name="Nordic", members=[se4, dk2, icx])
# Resolve a Reference against the tree root (or pre-built Index).
icx.from_element.resolve(portfolio)
icx.from_element.get().name # "SE4"
A bare Element passed as from_element / to_element on an Edge is auto-wrapped in a Reference at construction. JSON round-trip emits refs as {"__ref__": "<uuid>"} — single-pass deserialize, no two-pass walk.
Installation
We recommend installing using a virtual environment like venv, poetry or uv.
Install the stable release:
pip install energydatamodel
PVSystem.to_pvlib() requires the optional solar extra:
pip install energydatamodel[solar]
Install the latest release:
pip install git+https://github.com/rebase-energy/energydatamodel.git
Install in editable mode for development:
git clone https://github.com/rebase-energy/energydatamodel.git
cd EnergyDataModel
pip install -e .[dev]
Ways to Contribute
We welcome contributions from anyone interested in this project! Here are some ways to contribute to EnergyDataModel:
- Add a new energy system assets and concepts;
- Propose updates to existing energy assets and concepts;
- Create a converter to new data format; or
- Create a converter to another energy data model.
If you are interested in contributing, then feel free to join our Slack Community so that we can discuss it. 👋
Contributors
This project uses allcontributors.org to recognize all contributors, including those that don't push code.
Sebastian Haglund 💻 |
Nelson 🤔 |
Licence
This project uses the MIT Licence.
Acknowledgement
The authors of this project would like to thank the Swedish Energy Agency for their financial support under the E2B2 program (project number P2022-00903)
Metadata
Release files for energydatamodel 0.12.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 | |
|---|---|---|---|
| energydatamodel-0.12.0.tar.gz | 43.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| energydatamodel-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.7 kB
Release files / energydatamodel-0.12.0.tar.gz
| Download URL | energydatamodel-0.12.0.tar.gz |
|---|---|
| Size | 43.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fd887dc96abbc6a7b0a25cd6b70d2d050ce6bb05c5e262d342f83c8cc610ecc6
|
|
BLAKE2b-256 checksum How to use checksums |
652587e6f125e8266c73586ffb65c329c7cbece219d689433b4b62049a399297
|
| 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 Aug 28, 2026.
Transparency logRelease files / energydatamodel-0.12.0-py3-none-any.whl
| Download URL | energydatamodel-0.12.0-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0de97933fc0641fe6fca23f85a2d5ecf08c3375c4b0c25c160617f1ca3a0002a
|
|
BLAKE2b-256 checksum How to use checksums |
57a45e643993d9128faa501ba91b6e0dcb8cc9e8d6f43da97a4292657c84c0fb
|
| 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 Aug 28, 2026.
Transparency log