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]:
TimeSeries
Nameelectricity.supply
TimezoneUTC
UnitMW
Data typeACTUAL

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.