3. Read across the whole portfolio in one fluent call.
July 31, 2026 ยท View on GitHub
โก EnergyDB
Persistent storage for energy portfolios โ assets, grid topology, and 3-dimensional time series, in one connected database.
๐๏ธ What is EnergyDB?
EnergyDB is a database for energy portfolios. It stores three things together in one connected system:
| Layer | Description | Real-World Example |
|---|---|---|
| ๐ณย Asset hierarchy | Arbitrary-depth tree of portfolios, sites, and assets | "Offshore-1 โ WindTurbine T01 โ power" |
| ๐ย Grid topology | Typed edges (lines, transformers, pipes, interconnections) connecting any two assets | "Cable-1: BusA โ BusB" |
| โฑ๏ธย 3-dimensional time series | Actuals and versioned forecasts attached to any node or edge, queryable as-of any point in time | "power_flow on Cable-1, valid Wed 12:00, known Mon 18:00" |
Structure lives in PostgreSQL, values live in ClickHouse, and stable UUID identity lets Python objects round-trip to the database without losing any structural state.
EnergyDB extends TimeDB with persistent storage for EnergyDataModel hierarchies.
โจ Why EnergyDB?
Most time-series systems are agnostic about what their series represent โ they treat data as opaque (series_id, timestamp, value) triples. EnergyDB knows it is a portfolio, and links every series back to the asset or grid edge it describes.
- ๐ Round-trip persistence: Every
Elementkeeps its UUID7 from in-memory object to row primary key โ renames, moves, and property edits become silentUPDATEs, never delete-then-insert. - ๐ Diffable structural changes:
dry_run=Truepreviews every insert, rename, move, and delete as aTreeDiffbefore you apply โ no surprise mutations, and the same preview is available across a wholetransaction(). - โฑ๏ธ Time-of-knowledge queries: Forecast revisions, corrections, and as-of backtests, powered by TimeDB.
- ๐งญ Lazy fluent navigation:
client.get_node("Portfolio", "Site", "T01").read(...)resolves to one indexed SQL query, regardless of subtree size. - โ๏ธ Unit conversion at the boundary: Declare canonical units once; pint rescales every read and write automatically.
- ๐งน Idempotent writes:
write(..., skip_unchanged=True)drops rows that only duplicate the latest stored value before insert, so writing the same window repeatedly doesn't bloat storage. The comparison key is chosen per series โ actuals dedupe pervalid_time, versioned forecasts per(valid_time, knowledge_time)โ so a republished forecast is never mistaken for a duplicate. Opt-in; the write still returns itsrun_id(aWriteResultcarrying.written/.skippedcounts). - ๐ฏ Partial reads that don't fail:
read(manifest, on_missing="skip")returns every series that resolved plus the triples that didn't, so one unregistered series in a 1,500-series manifest costs you that series โ not the whole batch. - ๐งฏ Typed errors:
NodeNotFoundError,SeriesNotFoundError(carrying every unresolved triple),ManifestError, โฆ all under oneEnergyDBErrorbase โ so you branch on types and structured fields instead of matching message text. Every class also subclassesValueError, so broad handlers keep working.
๐ Quick Start
1. Installation
pip install energydb
Requires Python 3.12+, PostgreSQL (asset hierarchy + series catalog), and ClickHouse (time-series values).
Need a local Postgres + ClickHouse? One command brings both up:
cd local-db && docker compose up -d(seelocal-db/, or DEVELOPMENT.md for the full setup).
2. Usage Example
from datetime import UTC, datetime
import energydb as edb
import pandas as pd
client = edb.Client() # reads TIMEDB_PG_DSN / TIMEDB_CH_URL
client.create()
# 1. Declare your portfolio: a tree of typed assets with their series.
t01 = edb.wind.WindTurbine(
name="T01", capacity=3.5, hub_height=80,
timeseries=[edb.TimeSeries(name="power", unit="MW",
data_type=edb.DataType.ACTUAL)],
)
portfolio = edb.Portfolio(
name="my-portfolio",
members=[edb.Site(name="Offshore-1", members=[t01])],
)
client.register_tree(portfolio) # create-only; edit existing nodes via scope mutators
# 2. Write hourly power for that turbine.
start = datetime(2026, 1, 1, tzinfo=UTC)
df = pd.DataFrame({
"valid_time": pd.date_range(start, periods=24, freq="1h", tz="UTC"),
"value": [2.5 + 0.05 * i for i in range(24)],
})
client.get_node("my-portfolio", "Offshore-1", "T01").write(
df, name="power", data_type="actual",
)
# 3. Read across the whole portfolio in one fluent call.
client.get_node("my-portfolio").read(name="power", data_type="actual")
Async?
edb.Clientis a synchronous facade overedb.AsyncClient. Forasync/awaitcode, useAsyncClientdirectly โawait client.open()once, thenawaitevery method shown above.
๐งช Try it in Google Colab
Want to try EnergyDB without a local setup? Open our Quickstart in Colab โ the first cell automatically installs PostgreSQL + ClickHouse inside the VM.
Note: Data persists only within the active Colab session. Additional notebooks are available in the
examples/directory.
๐ Documentation & Resources
- ๐ Official Documentation
- โ๏ธ Installation Guide
- ๐ Python SDK Documentation
- ๐ Reference
- ๐ก Examples & Notebooks
๐ฆ Related Projects
| Project | Description |
|---|---|
| TimeDB | 3-dimensional time-series storage on ClickHouse with auditability and overlapping-forecast support |
| TimeDataModel | Pythonic data model for time series |
| EnergyDataModel | Data model for energy assets (solar, wind, battery, grid, ...) |
๐ค Contributing
Contributions are welcome! If you're interested in improving EnergyDB, please see our Development Guide for local setup instructions.
Licensed under the Apache-2.0 License.
Find a bug or have a feature request? Open an Issue.