{ "cells": [ { "cell_type": "markdown", "id": "7659bd4fd436", "metadata": {}, "source": [ "# EnergyDataModel — Quickstart\n", "\n", "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." ] }, { "cell_type": "markdown", "id": "190bcf8ab1f2", "metadata": {}, "source": [ "## 1. Setup\n", "\n", "Pure Python — `pip install` and import. Runs locally or on Colab." ] }, { "cell_type": "code", "execution_count": 1, "id": "1967d749600f", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:07.574636Z", "iopub.status.busy": "2026-05-06T10:58:07.574495Z", "iopub.status.idle": "2026-05-06T10:58:07.578374Z", "shell.execute_reply": "2026-05-06T10:58:07.577825Z" } }, "outputs": [], "source": [ "try:\n", " import google.colab # noqa: F401\n", "\n", " !pip install -q energydatamodel\n", "except ImportError:\n", " pass" ] }, { "cell_type": "code", "execution_count": 2, "id": "1747be5604bc", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:07.580060Z", "iopub.status.busy": "2026-05-06T10:58:07.579928Z", "iopub.status.idle": "2026-05-06T10:58:08.424047Z", "shell.execute_reply": "2026-05-06T10:58:08.423525Z" } }, "outputs": [], "source": [ "import json\n", "\n", "import energydatamodel as edm\n", "from shapely.geometry import Polygon" ] }, { "cell_type": "markdown", "id": "d3cd3729f827", "metadata": {}, "source": [ "## 2. Declare your portfolio\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 3, "id": "c8e1a6298602", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.425910Z", "iopub.status.busy": "2026-05-06T10:58:08.425572Z", "iopub.status.idle": "2026-05-06T10:58:08.430821Z", "shell.execute_reply": "2026-05-06T10:58:08.430249Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Portfolio('Nordic')\n", "├── Site('Lillgrund')\n", "│ └── WindFarm('Lillgrund-WF')\n", "│ ├── WindTurbine('T01')\n", "│ └── WindTurbine('T02')\n", "└── Site('Stockholm-Rooftop')\n", " ├── PVSystem('PV-01')\n", " │ └── PVArray()\n", " └── Battery('B-01')\n" ] } ], "source": [ "portfolio = edm.Portfolio(\n", " name=\"Nordic\",\n", " members=[\n", " edm.Site(\n", " name=\"Lillgrund\",\n", " lat=55.51,\n", " lon=12.78,\n", " members=[\n", " edm.wind.WindFarm(\n", " name=\"Lillgrund-WF\",\n", " capacity=110,\n", " members=[\n", " edm.wind.WindTurbine(name=\"T01\", capacity=2.3, hub_height=68.5, lat=55.510, lon=12.780),\n", " edm.wind.WindTurbine(name=\"T02\", capacity=2.3, hub_height=68.5, lat=55.515, lon=12.790),\n", " ],\n", " ),\n", " ],\n", " ),\n", " edm.Site(\n", " name=\"Stockholm-Rooftop\",\n", " lat=59.33,\n", " lon=18.07,\n", " members=[\n", " edm.solar.PVSystem(\n", " name=\"PV-01\",\n", " capacity=12.0,\n", " surface_tilt=25,\n", " surface_azimuth=180,\n", " members=[edm.solar.PVArray(capacity=12.0, surface_tilt=25, surface_azimuth=180)],\n", " ),\n", " edm.battery.Battery(name=\"B-01\", storage_capacity=20, max_charge=10, max_discharge=10),\n", " ],\n", " ),\n", " ],\n", ")\n", "\n", "print(portfolio.to_tree())" ] }, { "cell_type": "markdown", "id": "33e4ce050cea", "metadata": {}, "source": [ "## 3. Time series in plain English\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 4, "id": "73867bf6add5", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.432840Z", "iopub.status.busy": "2026-05-06T10:58:08.432638Z", "iopub.status.idle": "2026-05-06T10:58:08.440933Z", "shell.execute_reply": "2026-05-06T10:58:08.440166Z" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", "
TimeSeries
\n", "
\n", "\n", "\n", "\n", "\n", "
Nameelectricity.supply
TimezoneUTC
UnitMW
Data typeACTUAL
\n", "
" ], "text/plain": [ "TimeSeries\n", "┌────────────────────────────────────────┐\n", "│ Name: electricity.supply │\n", "│ Timezone: UTC │\n", "│ Unit: MW │\n", "│ Data type: ACTUAL │\n", "└────────────────────────────────────────┘" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "turbine = portfolio.members[0].members[0].members[0] # WindTurbine T01\n", "pv = portfolio.members[1].members[0] # PVSystem PV-01\n", "\n", "turbine.timeseries = [\n", " edm.electricity_supply(unit=\"MW\", data_type=edm.DataType.ACTUAL),\n", " edm.electricity_supply(unit=\"MW\", data_type=edm.DataType.FORECAST, frequency=edm.Frequency.PT1H),\n", "]\n", "pv.timeseries = [\n", " edm.electricity_supply(unit=\"kW\", data_type=edm.DataType.ACTUAL),\n", "]\n", "\n", "turbine.timeseries[0]" ] }, { "cell_type": "markdown", "id": "d872076db25e", "metadata": {}, "source": [ "## 4. Areas, geometry, interconnections\n", "\n", "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.\n", "\n", "Connect two areas with an `Interconnection`. Endpoints are captured by UUID — rename or restructure the tree, the link still resolves." ] }, { "cell_type": "code", "execution_count": 5, "id": "93d591bb4193", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.442862Z", "iopub.status.busy": "2026-05-06T10:58:08.442707Z", "iopub.status.idle": "2026-05-06T10:58:08.448729Z", "shell.execute_reply": "2026-05-06T10:58:08.448150Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "SE4 area: 12.0\n", "SE4 centroid: (14.50, 56.50)\n", "icx links: SE4 → DK2\n" ] } ], "source": [ "se4 = edm.BiddingZone(\n", " name=\"SE4\",\n", " geometry=Polygon([(12.5, 55.0), (16.5, 55.0), (16.5, 58.0), (12.5, 58.0)]),\n", " timeseries=[edm.spot_price(unit=\"EUR/MWh\")],\n", ")\n", "dk2 = edm.BiddingZone(\n", " name=\"DK2\",\n", " geometry=Polygon([(11.0, 54.5), (12.8, 54.5), (12.8, 56.0), (11.0, 56.0)]),\n", " timeseries=[edm.spot_price(unit=\"EUR/MWh\")],\n", ")\n", "icx = edm.grid.Interconnection(\n", " name=\"SE4-DK2\",\n", " from_element=se4,\n", " to_element=dk2, # bare Element auto-wrapped in a Reference\n", " capacity_forward=1700,\n", " capacity_backward=1300,\n", " timeseries=[edm.cross_border_flow(unit=\"MW\")],\n", ")\n", "\n", "print(f\"SE4 area: {se4.geometry.area}\")\n", "print(f\"SE4 centroid: ({se4.geometry.centroid.x:.2f}, {se4.geometry.centroid.y:.2f})\")\n", "print(f\"icx links: {icx.from_element.get().name} → {icx.to_element.get().name}\")" ] }, { "cell_type": "markdown", "id": "ce2d8a06b1a8", "metadata": {}, "source": [ "## 5. One tree, everything together\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 6, "id": "46e6e6cbc50c", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.454994Z", "iopub.status.busy": "2026-05-06T10:58:08.454829Z", "iopub.status.idle": "2026-05-06T10:58:08.457833Z", "shell.execute_reply": "2026-05-06T10:58:08.457180Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Portfolio('Nordic')\n", "├── Site('Lillgrund')\n", "│ └── WindFarm('Lillgrund-WF')\n", "│ ├── WindTurbine('T01')\n", "│ └── WindTurbine('T02')\n", "├── Site('Stockholm-Rooftop')\n", "│ ├── PVSystem('PV-01')\n", "│ │ └── PVArray()\n", "│ └── Battery('B-01')\n", "├── BiddingZone('SE4')\n", "├── BiddingZone('DK2')\n", "└── Interconnection('SE4-DK2')\n" ] } ], "source": [ "portfolio = edm.Portfolio(\n", " name=\"Nordic\",\n", " members=[*portfolio.members, se4, dk2, icx],\n", ")\n", "print(portfolio.to_tree())" ] }, { "cell_type": "markdown", "id": "e58e47c67274", "metadata": {}, "source": [ "## 6. Save and reload\n", "\n", "`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." ] }, { "cell_type": "code", "execution_count": 7, "id": "3e8190e12c49", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.461166Z", "iopub.status.busy": "2026-05-06T10:58:08.459233Z", "iopub.status.idle": "2026-05-06T10:58:08.467706Z", "shell.execute_reply": "2026-05-06T10:58:08.467299Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Portfolio('Nordic')\n", "├── Site('Lillgrund')\n", "│ └── WindFarm('Lillgrund-WF')\n", "│ ├── WindTurbine('T01')\n", "│ └── WindTurbine('T02')\n", "├── Site('Stockholm-Rooftop')\n", "│ ├── PVSystem('PV-01')\n", "│ │ └── PVArray()\n", "│ └── Battery('B-01')\n", "├── BiddingZone('SE4')\n", "├── BiddingZone('DK2')\n", "└── Interconnection('SE4-DK2')\n" ] } ], "source": [ "raw = portfolio.to_json()\n", "restored = edm.Portfolio.from_json(raw)\n", "\n", "assert restored.id == portfolio.id\n", "assert json.dumps(raw, default=str) == json.dumps(restored.to_json(), default=str)\n", "\n", "print(restored.to_tree())" ] }, { "cell_type": "markdown", "id": "cab5eeea2eb1", "metadata": {}, "source": [ "## 7. Extend it\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 8, "id": "09b98ca404c5", "metadata": { "execution": { "iopub.execute_input": "2026-05-06T10:58:08.468956Z", "iopub.status.busy": "2026-05-06T10:58:08.468811Z", "iopub.status.idle": "2026-05-06T10:58:08.473316Z", "shell.execute_reply": "2026-05-06T10:58:08.472679Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Site('H2-Site')\n", "└── Electrolyzer('EL-1')\n", "\n", "Electrolyzer: 5000 kW @ η=0.65\n" ] } ], "source": [ "from dataclasses import dataclass\n", "\n", "\n", "@dataclass(repr=False, kw_only=True)\n", "class Electrolyzer(edm.NodeAsset):\n", " capacity_kw: float | None = None\n", " efficiency: float | None = None\n", "\n", "\n", "h2_site = edm.Site(\n", " name=\"H2-Site\",\n", " members=[Electrolyzer(name=\"EL-1\", capacity_kw=5000, efficiency=0.65)],\n", ")\n", "\n", "reloaded = edm.Element.from_json(h2_site.to_json())\n", "print(reloaded.to_tree())\n", "\n", "el = reloaded.members[0]\n", "print(f\"\\n{type(el).__name__}: {el.capacity_kw} kW @ η={el.efficiency}\")" ] }, { "cell_type": "markdown", "id": "348f9a3d1091", "metadata": {}, "source": "## What's next\n\n- **Persist it.** [EnergyDB](https://github.com/rebase-energy/energydb) takes any EDM tree and stores it — hierarchy in Postgres, 3-dimensional time series in ClickHouse — same UUIDs end-to-end.\n- **Explore the model.** Full class catalogue at [docs.energydatamodel.org](https://docs.energydatamodel.org/en/latest/). Visual map: [zoomhub.net/Zxa5x](https://zoomhub.net/Zxa5x).\n- **Join the community.** [Slack](https://dub.sh/yTqMriJ) — energy modellers building open-source together." } ], "metadata": { "kernelspec": { "display_name": "backend (3.13.12)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.12" } }, "nbformat": 4, "nbformat_minor": 5 }