EnergyDataModel — Quickstart
Describe an energy portfolio — assets, sites, areas, the lines that connect them — as plain Python objects. Time series stay with the asset, geometry is Shapely, and every element carries a stable UUID that rides through JSON. One expression to declare it, one line to save it, one subclass to extend it.
1. Setup
Pure Python — pip install and import. Runs locally or on Colab.
[1]:
try:
import google.colab # noqa: F401
!pip install -q energydatamodel
except ImportError:
pass
[2]:
import json
import energydatamodel as edm
from shapely.geometry import Polygon
2. Declare your portfolio
A Portfolio is a Python expression: sites, wind farms, turbines, PV systems, batteries — each a typed dataclass with the domain fields you’d expect. members=[...] nests them; lat= / lon= places them. No YAML. No schema files.
[3]:
portfolio = edm.Portfolio(
name="Nordic",
members=[
edm.Site(
name="Lillgrund",
lat=55.51,
lon=12.78,
members=[
edm.wind.WindFarm(
name="Lillgrund-WF",
capacity=110,
members=[
edm.wind.WindTurbine(name="T01", capacity=2.3, hub_height=68.5, lat=55.510, lon=12.780),
edm.wind.WindTurbine(name="T02", capacity=2.3, hub_height=68.5, lat=55.515, lon=12.790),
],
),
],
),
edm.Site(
name="Stockholm-Rooftop",
lat=59.33,
lon=18.07,
members=[
edm.solar.PVSystem(
name="PV-01",
capacity=12.0,
surface_tilt=25,
surface_azimuth=180,
members=[edm.solar.PVArray(capacity=12.0, surface_tilt=25, surface_azimuth=180)],
),
edm.battery.Battery(name="B-01", storage_capacity=20, max_charge=10, max_discharge=10),
],
),
],
)
print(portfolio.to_tree())
Portfolio('Nordic')
├── Site('Lillgrund')
│ └── WindFarm('Lillgrund-WF')
│ ├── WindTurbine('T01')
│ └── WindTurbine('T02')
└── Site('Stockholm-Rooftop')
├── PVSystem('PV-01')
│ └── PVArray()
└── Battery('B-01')
3. Time series in plain English
Attach metadata-only TimeSeries declarations with the energy vocabulary — electricity_supply, electricity_demand, spot_price, cross_border_flow, grid_frequency, … Each carries unit, data type (actual / forecast / capacity / …), and optional frequency. They stay with the asset and ride through JSON.
[4]:
turbine = portfolio.members[0].members[0].members[0] # WindTurbine T01
pv = portfolio.members[1].members[0] # PVSystem PV-01
turbine.timeseries = [
edm.electricity_supply(unit="MW", data_type=edm.DataType.ACTUAL),
edm.electricity_supply(unit="MW", data_type=edm.DataType.FORECAST, frequency=edm.Frequency.PT1H),
]
pv.timeseries = [
edm.electricity_supply(unit="kW", data_type=edm.DataType.ACTUAL),
]
turbine.timeseries[0]
[4]:
4. Areas, geometry, interconnections
Bidding zones, control areas, weather cells, countries — all proper subclasses of Area, distinguished by isinstance. Pass any Shapely shape via geometry= and .area, .bounds, .centroid come along for free; polygons round-trip to GeoJSON.
Connect two areas with an Interconnection. Endpoints are captured by UUID — rename or restructure the tree, the link still resolves.
[5]:
se4 = edm.BiddingZone(
name="SE4",
geometry=Polygon([(12.5, 55.0), (16.5, 55.0), (16.5, 58.0), (12.5, 58.0)]),
timeseries=[edm.spot_price(unit="EUR/MWh")],
)
dk2 = edm.BiddingZone(
name="DK2",
geometry=Polygon([(11.0, 54.5), (12.8, 54.5), (12.8, 56.0), (11.0, 56.0)]),
timeseries=[edm.spot_price(unit="EUR/MWh")],
)
icx = edm.grid.Interconnection(
name="SE4-DK2",
from_element=se4,
to_element=dk2, # bare Element auto-wrapped in a Reference
capacity_forward=1700,
capacity_backward=1300,
timeseries=[edm.cross_border_flow(unit="MW")],
)
print(f"SE4 area: {se4.geometry.area}")
print(f"SE4 centroid: ({se4.geometry.centroid.x:.2f}, {se4.geometry.centroid.y:.2f})")
print(f"icx links: {icx.from_element.get().name} → {icx.to_element.get().name}")
SE4 area: 12.0
SE4 centroid: (14.50, 56.50)
icx links: SE4 → DK2
5. One tree, everything together
Equipment, areas, and edges live in one root. The interconnection knows the zones it links by UUID — no path lookup, no order-of-construction games.
[6]:
portfolio = edm.Portfolio(
name="Nordic",
members=[*portfolio.members, se4, dk2, icx],
)
print(portfolio.to_tree())
Portfolio('Nordic')
├── Site('Lillgrund')
│ └── WindFarm('Lillgrund-WF')
│ ├── WindTurbine('T01')
│ └── WindTurbine('T02')
├── Site('Stockholm-Rooftop')
│ ├── PVSystem('PV-01')
│ │ └── PVArray()
│ └── Battery('B-01')
├── BiddingZone('SE4')
├── BiddingZone('DK2')
└── Interconnection('SE4-DK2')
6. Save and reload
to_json() emits a JSON-compatible dict — every element keeps its UUID. from_json() rebuilds the tree, identity intact. Write it to disk, ship it over a queue, hand it to a teammate; the model is the same on the other side.
[7]:
raw = portfolio.to_json()
restored = edm.Portfolio.from_json(raw)
assert restored.id == portfolio.id
assert json.dumps(raw, default=str) == json.dumps(restored.to_json(), default=str)
print(restored.to_tree())
Portfolio('Nordic')
├── Site('Lillgrund')
│ └── WindFarm('Lillgrund-WF')
│ ├── WindTurbine('T01')
│ └── WindTurbine('T02')
├── Site('Stockholm-Rooftop')
│ ├── PVSystem('PV-01')
│ │ └── PVArray()
│ └── Battery('B-01')
├── BiddingZone('SE4')
├── BiddingZone('DK2')
└── Interconnection('SE4-DK2')
7. Extend it
EDM is built to be extended. Inherit from the closest base — NodeAsset for equipment, edm.grid.EdgeAsset for things connecting two nodes — and your class is automatically registered for JSON round-trip. No decorator. No registry to update.
[8]:
from dataclasses import dataclass
@dataclass(repr=False, kw_only=True)
class Electrolyzer(edm.NodeAsset):
capacity_kw: float | None = None
efficiency: float | None = None
h2_site = edm.Site(
name="H2-Site",
members=[Electrolyzer(name="EL-1", capacity_kw=5000, efficiency=0.65)],
)
reloaded = edm.Element.from_json(h2_site.to_json())
print(reloaded.to_tree())
el = reloaded.members[0]
print(f"\n{type(el).__name__}: {el.capacity_kw} kW @ η={el.efficiency}")
Site('H2-Site')
└── Electrolyzer('EL-1')
Electrolyzer: 5000 kW @ η=0.65
What’s next
Persist it. EnergyDB takes any EDM tree and stores it — hierarchy in Postgres, 3-dimensional time series in ClickHouse — same UUIDs end-to-end.
Explore the model. Full class catalogue at docs.energydatamodel.org. Visual map: zoomhub.net/Zxa5x.
Join the community. Slack — energy modellers building open-source together.