EnergyDB — Quickstart
Declare your energy portfolio — sites, turbines, arrays, batteries — as plain Python objects. One call persists the whole tree; one fluent line reads it back across every asset.
1. Setup
Client reads connection settings from TIMEDB_PG_DSN (Postgres for hierarchy + series catalog) and TIMEDB_CH_URL (ClickHouse for time-series values).
[1]:
try:
import urllib.request
import google.colab # noqa: F401
urllib.request.urlretrieve(
"https://raw.githubusercontent.com/rebase-energy/energydb/main/examples/colab_setup.py", "/tmp/colab_setup.py"
)
exec(open("/tmp/colab_setup.py").read())
except ImportError:
pass
[2]:
from datetime import UTC, datetime
import energydb as edb
import pandas as pd
client = edb.Client()
client.delete() # clean slate
client.create()
2. Declare your portfolio
A Portfolio is just a Python object — sites, turbines, arrays, batteries — with metadata-only TimeSeries declarations naming the series each node will hold. Build it top-down; the tree printout below is your structural deliverable.
[3]:
# Step 1 — declare the series this turbine will hold (metadata only; data lands later via write).
power_actual = edb.TimeSeries(name="power", unit="MW", data_type=edb.DataType.ACTUAL)
wind_speed = edb.TimeSeries(name="wind_speed", unit="m/s", data_type=edb.DataType.ACTUAL)
power_forecast = edb.TimeSeries(
name="power",
unit="MW",
data_type=edb.DataType.FORECAST,
timeseries_type=edb.TimeSeriesType.OVERLAPPING,
)
# Step 2 — wrap them on a WindTurbine (and a sibling with a single declaration inline).
t01 = edb.wind.WindTurbine(
name="T01",
lat=55.01,
lon=3.02,
capacity=3.5,
hub_height=80,
timeseries=[power_actual, wind_speed, power_forecast],
)
t02 = edb.wind.WindTurbine(
name="T02",
lat=55.01,
lon=3.04,
capacity=3.5,
hub_height=80,
timeseries=[edb.TimeSeries(name="power", unit="MW", data_type=edb.DataType.ACTUAL)],
)
# Step 3 — group the turbines under a Site that carries its own series too.
offshore_1 = edb.Site(
name="Offshore-1",
lat=55.0,
lon=3.0,
timeseries=[edb.TimeSeries(name="demand", unit="MW", data_type=edb.DataType.ACTUAL)],
members=[t01, t02],
)
# Step 4 — same bottom-up pattern for the second site: array → system, plus a battery, then site.
pv_array = edb.solar.PVArray(
name="Array-1",
capacity=10,
surface_tilt=25,
surface_azimuth=180,
timeseries=[edb.TimeSeries(name="power", unit="MW", data_type=edb.DataType.ACTUAL)],
)
pv_system = edb.solar.PVSystem(name="PV01", members=[pv_array])
battery = edb.battery.Battery(name="B01", storage_capacity=1000, max_charge=500)
rooftop_1 = edb.Site(name="Rooftop-1", lat=52.0, lon=4.5, members=[pv_system, battery])
# Step 5 — assemble the portfolio.
portfolio = edb.Portfolio(name="my-portfolio", members=[offshore_1, rooftop_1])
print(portfolio.to_tree())
Portfolio('my-portfolio')
├── Site('Offshore-1')
│ ├── WindTurbine('T01')
│ └── WindTurbine('T02')
└── Site('Rooftop-1')
├── PVSystem('PV01')
│ └── PVArray('Array-1')
└── Battery('B01')
3. Persist and write data
register_tree creates every node, edge, and series declaration in one PG transaction — the UUIDs you set in Python become the row primary keys. Then write streams in the actual values, one short call per series.
[4]:
client.register_tree(portfolio)
# A day of synthetic hourly values, varied by base level per series.
start = datetime(2026, 1, 1, tzinfo=UTC)
end = datetime(2026, 1, 2, tzinfo=UTC)
hours = pd.date_range(start, end, freq="1h", inclusive="left", tz="UTC")
def synthetic_dataframe(base):
return pd.DataFrame({"valid_time": hours, "value": [base + 0.05 * i for i in range(len(hours))]})
offshore = client.get_node("my-portfolio", "Offshore-1")
offshore.write(synthetic_dataframe(12.0), name="demand", data_type="actual")
offshore.get_node("T01").write(synthetic_dataframe(2.5), name="power", data_type="actual")
offshore.get_node("T01").write(synthetic_dataframe(7.5), name="wind_speed", data_type="actual")
offshore.get_node("T02").write(synthetic_dataframe(2.7), name="power", data_type="actual")
rooftop = client.get_node("my-portfolio", "Rooftop-1", "PV01", "Array-1")
rooftop.write(synthetic_dataframe(5.0), name="power", data_type="actual")
[4]:
58297317239439162
4. Read across the portfolio
The pay-off. One fluent scope, many routings — full subtree, single-asset drill-down, series-name filter, node-type filter. Each line below is one indexed query.
[5]:
portfolio = client.get_node("my-portfolio")
portfolio.read(data_type="actual", start_valid=start) # everything — every series on every node
df = portfolio.read(data_type="actual", name="power", start_valid=start) # only 'power' values
portfolio.where(type="WindTurbine").read(data_type="actual", name="power") # turbines only
portfolio.get_node("Offshore-1", "T01").read(data_type="actual", name="wind_speed") # one asset, one series
df
[5]:
| valid_time | value | path | data_type | name |
|---|---|---|---|---|
| datetime[μs, UTC] | f64 | str | str | str |
| 2026-01-01 00:00:00 UTC | 2.5 | "my-portfolio/Offshore-1/T01" | "actual" | "power" |
| 2026-01-01 01:00:00 UTC | 2.55 | "my-portfolio/Offshore-1/T01" | "actual" | "power" |
| 2026-01-01 02:00:00 UTC | 2.6 | "my-portfolio/Offshore-1/T01" | "actual" | "power" |
| 2026-01-01 03:00:00 UTC | 2.65 | "my-portfolio/Offshore-1/T01" | "actual" | "power" |
| 2026-01-01 04:00:00 UTC | 2.7 | "my-portfolio/Offshore-1/T01" | "actual" | "power" |
| … | … | … | … | … |
| 2026-01-01 19:00:00 UTC | 5.95 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 20:00:00 UTC | 6.0 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 21:00:00 UTC | 6.05 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 22:00:00 UTC | 6.1 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 23:00:00 UTC | 6.15 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
5. Reconstruct the tree
Pull the whole portfolio back as an EDM tree — same UUIDs, ready for inspection or in-memory edits.
[6]:
tree = client.get_tree("my-portfolio", include_series=True)
print(tree)
Portfolio('my-portfolio')
├── Site('Offshore-1')
│ ├── WindTurbine('T01')
│ └── WindTurbine('T02')
└── Site('Rooftop-1')
├── PVSystem('PV01')
│ └── PVArray('Array-1')
└── Battery('B01')
6. Modify existing nodes and edges
register_tree is create-only — to edit what’s already persisted, use scope mutators (rename, update, delete, move_to, add) addressed by path or uuid. The node keeps its uuid, so attached series stay attached. Every mutator accepts dry_run=True to return a TreeDiff preview without committing. client.transaction() batches several mutations into one atomic commit; .commit() is required, exiting without it raises.
[7]:
client.get_node("my-portfolio", "Offshore-1").rename("Offshore-Renamed")
client.get_node("my-portfolio", "Offshore-Renamed", "T01").update({"capacity": 4.0})
client.get_node("my-portfolio", "Offshore-Renamed", "T02").delete()
# Add a new turbine under Offshore-Renamed; .add() returns a scope at the new node.
client.get_node("my-portfolio", "Offshore-Renamed").add(edb.wind.WindTurbine(name="T03", capacity=4.5))
renamed = client.get_node("my-portfolio", "Offshore-Renamed").get()
print(f"Renamed: {renamed.name} (uuid={renamed.id})")
Renamed: Offshore-Renamed (uuid=019e3a33-c9cd-7bfa-a262-1ba2f3cafbd5)
[8]:
diff = client.get_node("my-portfolio", "Offshore-Renamed", "T01").update({"hub_height": 95}, dry_run=True)
diff.render()
# DB is unchanged after a dry_run.
print("hub_height still:", client.get_node("my-portfolio", "Offshore-Renamed", "T01").get().hub_height)
~ WindTurbine 'T01' [hub_height: 80 → 95]
hub_height still: 80
[9]:
with client.transaction() as txn:
txn.get_node("my-portfolio", "Offshore-Renamed", "T01").update({"hub_height": 95})
txn.get_node("my-portfolio", "Offshore-Renamed", "T03").rename("T02")
txn.get_node("my-portfolio", "Rooftop-1", "B01").move_to(("my-portfolio", "Offshore-Renamed"))
txn.preview().render()
txn.commit()
~ WindTurbine 'T01' [hub_height: 80 → 95]
~ WindTurbine 'T02' [rename 'T03' → 'T02']
~ Battery 'B01' [moved (parent 019e3a33-c9d1-7c29-ad0e-cc9d87e6152e → 019e3a33-c9cd-7bfa-a262-1ba2f3cafbd5)]
7. Bulk manifest I/O
A single manifest DataFrame fans out a read across many series at once — routing column is path, node_uuid, or edge_uuid, detected automatically.
[10]:
manifest = pd.DataFrame(
[
{"path": "my-portfolio/Offshore-Renamed/T01", "data_type": "actual", "name": "power"},
{"path": "my-portfolio/Rooftop-1/PV01/Array-1", "data_type": "actual", "name": "power"},
]
)
df = client.read(manifest, start_valid=start)
df
[10]:
| valid_time | value | path | data_type | name |
|---|---|---|---|---|
| datetime[μs, UTC] | f64 | str | str | str |
| 2026-01-01 00:00:00 UTC | 2.5 | "my-portfolio/Offshore-Renamed/… | "actual" | "power" |
| 2026-01-01 01:00:00 UTC | 2.55 | "my-portfolio/Offshore-Renamed/… | "actual" | "power" |
| 2026-01-01 02:00:00 UTC | 2.6 | "my-portfolio/Offshore-Renamed/… | "actual" | "power" |
| 2026-01-01 03:00:00 UTC | 2.65 | "my-portfolio/Offshore-Renamed/… | "actual" | "power" |
| 2026-01-01 04:00:00 UTC | 2.7 | "my-portfolio/Offshore-Renamed/… | "actual" | "power" |
| … | … | … | … | … |
| 2026-01-01 19:00:00 UTC | 5.95 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 20:00:00 UTC | 6.0 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 21:00:00 UTC | 6.05 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 22:00:00 UTC | 6.1 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
| 2026-01-01 23:00:00 UTC | 6.15 | "my-portfolio/Rooftop-1/PV01/Ar… | "actual" | "power" |
8. Edges
Lines, links, and pipes link two nodes. Pass the endpoint nodes directly — the edge captures their .id. Edges hold their own time series the same way nodes do (register_series → write → read).
[11]:
bus_a = edb.grid.JunctionPoint(name="BusA")
bus_b = edb.grid.JunctionPoint(name="BusB")
line = edb.grid.Line(name="Cable-1", capacity=500, from_element=bus_a, to_element=bus_b)
offshore = client.get_node("my-portfolio", "Offshore-Renamed")
offshore.add(bus_a)
offshore.add(bus_b)
client.create_edge(line)
found = client.get_edge(
from_path=("my-portfolio", "Offshore-Renamed", "BusA"),
to_path=("my-portfolio", "Offshore-Renamed", "BusB"),
type="Line",
).get()
print(f"{type(found).__name__}: {found.name} (uuid={found.id})")
Line: Cable-1 (uuid=019e3a33-cbe2-7189-8e27-5df23845b82f)
9. Cleanup
[12]:
client.delete()