Orchestrators
Orchestrators are beginner-friendly entry points grouped by data domain.
Instead of memorising dataset IDs, you import a domain module (for example
ltds or gis) and call a named method.
Why this matters for beginners
- Lower cognitive load: call descriptive functions rather than raw endpoint URLs
- Fewer mistakes: common parameters and response structures are consistent
- Easier learning curve: start with one domain, then expand gradually
Available orchestrators
ltds- Long Term Development Statement tables and related assetsdfes- Distributed Future Energy Scenarios data accessdnoa- Distribution Network Options Assessment data accessnetwork- Network-level circuit and demand profile datasetsflexibility- Flexibility dispatches and curtailment-related recordscurtailment- Curtailment event datasetsders(resourcesalias) - Distributed energy resource / embedded capacity datagis- Geospatial assets (substations, lines, poles, sites)powerflow- Time-series network loading and transformer/circuit flows
First orchestrator call (recommended)
from ukpyn import ltds
table_3a = ltds.get_table_3a(licence_area="EPN")
print(table_3a.total_count)
print(len(table_3a.records))
Domain examples (copy/paste)
from ukpyn import flexibility, gis, powerflow, network, dfes, dnoa, curtailment, ders
dispatches = flexibility.get_dispatches(start_date="2024-01-01")
substations = gis.get_primary_substations(licence_area="SPN")
flows = powerflow.get_circuit_timeseries(voltage="132kv", granularity="half_hourly")
profiles = network.get_demand_profiles()
headroom = dfes.get_headroom()
assessment = dnoa.get_assessment()
events = curtailment.get_events(start_date="2024-01-01")
embedded = ders.get_embedded_capacity()
Sync and async patterns
For most beginners, start with sync functions like get_table_3a() or
get_dispatches().
When you need higher throughput or integration into async applications, use each
orchestrator's get_async(...) method.
Choosing the right orchestrator
- Planning and LTDS tables →
ltds - Scenario-style capacity/headroom views →
dfes - Options assessment and planning decisions →
dnoa - Operational flexibility activity →
flexibilityorcurtailment - Asset location and spatial workflows →
gis - Time-series loading and flows →
powerflow - Embedded generation / demand resources →
ders
GIS orchestrator — geometry handling
UKPN's ODP stores geometry in several different field formats across datasets. ukpyn normalises all of these so you don't have to.
Normalised geometry on every record
Every Record object exposes a .geometry property that returns a standard
GeoJSON geometry dict (or None if the record has no spatial data):
from ukpyn import gis
result = gis.get_primary_substations(licence_area="EPN", limit=5)
for rec in result.records:
print(rec.geometry) # {"type": "Point", "coordinates": [-0.12, 51.51]}
The property resolves geometry from whichever raw field the ODP happens to use,
in priority order: geo_shape → spatial_coordinates → geo_point_2d →
geo_point → geopoint. Point dicts like {"lat": 51.5, "lon": -0.1} are
automatically converted to GeoJSON Point format.
Feature unwrapping
Some datasets (e.g. poles) return geo_shape as a full GeoJSON Feature:
{"type": "Feature", "geometry": {"type": "Point", "coordinates": [...]}, "properties": {}}
ukpyn automatically unwraps this to the bare geometry dict during parsing.
NaN sanitisation
The ODP occasionally returns NaN for missing values in fields like
grid_site or grid_site_floc. Python's float('nan') is invalid JSON and
will break PostgreSQL, json.dumps(), and any serialisation step. ukpyn
replaces NaN and Infinity with None on every record automatically.
Coordinate dimensions (2D vs 3D)
The GeoJSON export path returns 3D geometries (with Z elevation), while the
records API returns 2D points. Use the dimensions parameter to normalise:
# Strip Z values for a flat 2D map
geojson_bytes = gis.export_geojson("hv_overhead_lines", dimensions="2d")
# Ensure all coordinates have Z (defaults to 0.0 where missing)
geojson_bytes = gis.export_geojson("hv_overhead_lines", dimensions="3d")
# Pass through as-is (default)
geojson_bytes = gis.export_geojson("hv_overhead_lines", dimensions="raw")
Practical beginner workflow
- Pick one orchestrator tied to your question.
- Run one minimal call with no optional parameters.
- Print
total_countandlen(records). - Add one filter (for example
licence_area). - Compare outputs and iterate.
Next steps
- Follow Tutorials for full notebook workflows
- Revisit Getting Started if environment or key setup fails