Flexibility Markets with ukpyn¶
This tutorial covers working with UK Power Networks flexibility market data using the flexibility orchestrator.
What you'll learn:
- What flexibility markets are and why dispatches matter
- Using the
flexibilitymodule with simple imports - Listing available datasets
- Getting dispatch events with filtering
- Using the generic
flexibility.get()function - Exporting data to different formats
Prerequisites:
- Complete 01-getting-started.ipynb first
- UKPN_API_KEY environment variable configured
- These tutorials require additional dependencies. Install them with
pip install "ukpyn[all]"— see Tutorial 01 for full setup instructions
1. Introduction to Flexibility Markets¶
What are Flexibility Markets?¶
Flexibility markets are mechanisms that allow UK Power Networks to procure services from distributed energy resources (DERs) to manage network constraints. Instead of traditional network reinforcement (building new infrastructure), flexibility services offer a more cost-effective and faster solution.
What are Dispatches?¶
A dispatch event occurs when UK Power Networks calls upon a flexibility provider to deliver their contracted service. This could involve:
- Demand reduction: Reducing electricity consumption during peak periods
- Generation increase: Increasing local generation to relieve network stress
- Storage discharge: Releasing stored energy when needed
Why Do Dispatches Matter?¶
Dispatch data is valuable for:
- Researchers: Understanding how flexibility services are utilized across the network
- Flexibility providers: Analyzing market activity and opportunities
- Policy makers: Evaluating the effectiveness of flexibility markets
- Network planners: Assessing flexibility's role in managing constraints
Service Types¶
Common flexibility service types include:
| Service Type | Description |
|---|---|
| Sustain | Long-duration services (typically 2+ hours) |
| Secure | Response services for network security |
| Dynamic | Fast-response flexibility |
| Restore | Services for network restoration |
import ukpyn
ukpyn.check_api_key()
print("API key configured!")
from ukpyn import flexibility
print("Flexibility module loaded successfully!")
print(f"Module: {flexibility.__name__}")
# Expected output:
# Flexibility module loaded successfully!
# Module: ukpyn.orchestrators.flexibility
Available Functions¶
The flexibility module provides several convenience functions:
| Function | Description |
|---|---|
flexibility.get() |
Generic function to get any flexibility dataset |
flexibility.get_dispatches() |
Get dispatch events with specific filters |
flexibility.get_curtailment() |
Get curtailment events |
flexibility.export() |
Export data to CSV, JSON, or other formats |
Each function has both sync and async versions (e.g., get_async(), get_dispatches_async()).
3. Listing Available Datasets¶
Before fetching data, let's see what datasets are available in the flexibility module.
# List available datasets
print("Available flexibility datasets:")
print("-" * 40)
for dataset in flexibility.available_datasets:
print(f" - {dataset}")
# Expected output:
# Available flexibility datasets:
# ----------------------------------------
# - dispatches
# - dispatch_events
# You can also access this as a module-level attribute
print(f"Number of datasets: {len(flexibility.available_datasets)}")
print(f"\nDatasets: {flexibility.available_datasets}")
# Expected output:
# Number of datasets: 3
#
# Datasets: ['dispatches', 'dispatch_events', 'tenders']
Dataset Descriptions¶
| Dataset Name | Alias | Description |
|---|---|---|
dispatches |
dispatch_events |
Flexibility dispatch events - instances where flexibility services were called upon |
Note: dispatch_events is an alias for dispatches - they return the same data.
4. Getting Dispatch Events¶
The get_dispatches() function provides a convenient way to fetch dispatch events with built-in filtering options.
# Fetch recent dispatch events (no filters)
dispatches = flexibility.get_dispatches(limit=10)
print(f"Total dispatch events: {dispatches.total_count}")
print(f"Records returned: {len(dispatches.records)}")
# Expected output:
# Total dispatch events: 1234 (actual number will vary)
# Records returned: 10
# Display the first few records
print("Sample dispatch events:")
print("=" * 60)
for i, record in enumerate(dispatches.records[:3], 1):
print(f"\nRecord {i} (ID: {record.id})")
print("-" * 40)
if record.fields:
for key, value in record.fields.items():
print(f" {key}: {value}")
# Expected output:
# Sample dispatch events:
# ============================================================
#
# Record 1 (ID: abc123...)
# ----------------------------------------
# start_time_local: 2024-06-15T10:00:00
# product: Sustain
# zone: EPN
# ...
Filtering by Date Range¶
Use start_date and end_date to filter dispatches within a specific time period.
Date formats accepted:
- ISO format string:
'2024-01-01' - Python
dateobject:date(2024, 1, 1) - Python
datetimeobject:datetime(2024, 1, 1)
# Filter by date range using string format
q1_2024 = flexibility.get_dispatches(
start_date="2024-01-01", end_date="2024-03-31", limit=50
)
print(f"Q1 2024 dispatches: {q1_2024.total_count}")
print(f"Records returned: {len(q1_2024.records)}")
# Expected output:
# Q1 2024 dispatches: 287 (actual number will vary)
# Records returned: 50
# Filter using Python date objects
from datetime import date, datetime
# Using date object
jan_dispatches = flexibility.get_dispatches(
start_date=date(2024, 1, 1), end_date=date(2024, 1, 31), limit=20
)
print(f"January 2024 dispatches: {jan_dispatches.total_count}")
# Using datetime object
recent_dispatches = flexibility.get_dispatches(
start_date=datetime(2024, 6, 1), limit=10
)
print(f"Dispatches since June 2024: {recent_dispatches.total_count}")
# Expected output:
# January 2024 dispatches: 95 (actual numbers will vary)
# Dispatches since June 2024: 450
Filtering by Service Type¶
Use product to filter dispatches by the type of flexibility service.
# Get dispatches for a specific service type
sustain_dispatches = flexibility.get_dispatches(product="Peak Reduction", limit=20)
print(f"Peak Reduction service dispatches: {sustain_dispatches.total_count}")
print(f"Records returned: {len(sustain_dispatches.records)}")
# Expected output:
# Peak Reduction service dispatches: 678 (actual number will vary)
# Records returned: 20
# Combine date range and service type filters
filtered_dispatches = flexibility.get_dispatches(
start_date="2024-01-01", end_date="2024-06-30", product="Secure", limit=100
)
print(f"Secure dispatches in H1 2024: {filtered_dispatches.total_count}")
# Display summary
if filtered_dispatches.records:
print("\nSample records:")
for record in filtered_dispatches.records[:3]:
if record.fields:
date_val = record.fields.get("start_time_local", "N/A")
product = record.fields.get("product", "N/A")
print(f" - {date_val}: {product}")
# Expected output:
# Secure dispatches in H1 2024: 312 (actual number will vary)
#
# Sample records:
# - 2024-06-28T...: Secure
# - 2024-06-27T...: Secure
# - 2024-06-25T...: Secure
Pagination¶
For large result sets, use limit and offset for pagination.
# Paginate through dispatch records
page_size = 25
# Page 1
page1 = flexibility.get_dispatches(limit=page_size, offset=0)
print(f"Total records: {page1.total_count}")
print(f"Page 1: records 1-{len(page1.records)}")
# Page 2
page2 = flexibility.get_dispatches(limit=page_size, offset=25)
print(f"Page 2: records 26-{25 + len(page2.records)}")
# Calculate total pages
total_pages = (page1.total_count + page_size - 1) // page_size
print(f"\nTotal pages: {total_pages}")
# Expected output:
# Total records: 1234 (actual number will vary)
# Page 1: records 1-25
# Page 2: records 26-50
#
# Total pages: 50
5. Using the Generic get() Function¶
The flexibility.get() function provides a generic way to fetch any flexibility dataset.
# Use the generic get() function
data = flexibility.get("dispatches", limit=10)
print("Dataset: dispatches")
print(f"Total records: {data.total_count}")
print(f"Records returned: {len(data.records)}")
# Expected output:
# Dataset: dispatches
# Total records: 1234 (actual number will vary)
# Records returned: 10
# Using the alias 'dispatch_events' works the same way
data_alias = flexibility.get("dispatch_events", limit=5)
print("Using 'dispatch_events' alias:")
print(f"Total records: {data_alias.total_count}")
# Expected output:
# Using 'dispatch_events' alias:
# Total records: 1234 (same as 'dispatches')
# Add custom ODSQL filters with the where parameter
# This gives you full control over filtering
# Note: Use actual field names from the ODP schema
data_filtered = flexibility.get(
"dispatches",
limit=20,
where="start_time_local >= '2024-01-01' AND start_time_local <= '2024-06-30'",
order_by="-start_time_local", # Sort by date descending (newest first)
)
print(f"Filtered dispatches (H1 2024): {data_filtered.total_count}")
print("\nFirst 3 records (sorted by date, newest first):")
for record in data_filtered.records[:3]:
if record.fields:
date_val = record.fields.get("start_time_local", "N/A")
print(f" - {date_val}")
# Expected output:
# Filtered dispatches (H1 2024): 567 (actual number will vary)
#
# First 3 records (sorted by date, newest first):
# - 2024-06-30T...
# - 2024-06-29T...
# - 2024-06-28T...
# Select specific fields to reduce response size
# Note: Use actual field names from the ODP schema
data_selected = flexibility.get(
"dispatches", limit=10, select="start_time_local, product, zone"
)
print("Selected fields only (start_time_local, product, zone):")
print("-" * 50)
for record in data_selected.records[:5]:
if record.fields:
print(f" {record.fields}")
# Expected output:
# Selected fields only (start_time_local, product, zone):
# --------------------------------------------------
# {'start_time_local': '2024-06-30T...', 'product': 'Sustain', 'zone': 'EPN'}
# ...
Error Handling¶
Handle errors gracefully when working with the API.
# Handle invalid dataset names
try:
invalid_data = flexibility.get("invalid_dataset")
except ValueError as e:
print(f"Error: {e}")
print(f"\nAvailable datasets: {flexibility.available_datasets}")
# Expected output:
# Error: Unknown dataset: 'invalid_dataset'. Available datasets: dispatches, dispatch_events
#
# Available datasets: ['dispatches', 'dispatch_events']
# Handle invalid date formats
try:
bad_date = flexibility.get_dispatches(start_date="not-a-date")
except ValueError as e:
print(f"Date format error: {e}")
# Expected output:
# Date format error: Invalid date format: 'not-a-date'. Expected ISO format (YYYY-MM-DD) or datetime string.
6. Exporting Data¶
Export flexibility data to various formats for analysis in other tools.
# Export dispatches to CSV
csv_data = flexibility.export("dispatches", format="csv", limit=100)
print(f"Exported {len(csv_data)} bytes of CSV data")
print("\nPreview (first 500 characters):")
print("-" * 60)
print(csv_data.decode("utf-8")[:500])
# Expected output:
# Exported 15234 bytes of CSV data
#
# Preview (first 500 characters):
# ------------------------------------------------------------
# date;product;zone;mw_dispatched;...
# 2024-06-30;Secure;EPN;5.2;...
# ...
# Save exported data to a file (optional)
from pathlib import Path
csv_data = flexibility.export("dispatches", format="csv", limit=100)
save_dir = None # Set to a directory (e.g. "exports") to enable writing files.
if save_dir:
output_file = Path(save_dir) / "dispatches_export.csv"
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "wb") as f:
f.write(csv_data)
print(f"Saved to {output_file}")
else:
print("File save skipped; set save_dir to enable writing.")
# Expected output:
# Saved dispatches_export.csv
# Export to JSON format
import json
json_data = flexibility.export("dispatches", format="json", limit=10)
# Parse and display
data = json.loads(json_data)
print(f"Exported {len(data)} records as JSON")
print("\nFirst record:")
print(json.dumps(data[0], indent=2))
# Expected output:
# Exported 10 records as JSON
#
# First record:
# {
# "date": "2024-06-30",
# "product": "Secure",
# ...
# }
# Export to Excel format (optional)
from pathlib import Path
xlsx_data = flexibility.export("dispatches", format="xlsx", limit=100)
save_dir = None # Set to a directory (e.g. "exports") to enable writing files.
if save_dir:
output_file = Path(save_dir) / "dispatches_export.xlsx"
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "wb") as f:
f.write(xlsx_data)
print(f"Saved {len(xlsx_data)} bytes to {output_file}")
else:
print(
f"Exported {len(xlsx_data)} bytes (file save skipped; set save_dir to enable writing)."
)
# Expected output:
# Saved 24567 bytes to dispatches_export.xlsx
Working with pandas (Optional)¶
Load exported data into pandas for analysis.
# Load into pandas DataFrame
try:
from io import BytesIO
import pandas as pd
# Export and load into pandas
csv_data = flexibility.export("dispatches", format="csv", limit=200)
# OpenDataSoft CSV uses semicolon separator
df = pd.read_csv(BytesIO(csv_data), sep=";")
print(f"DataFrame shape: {df.shape}")
print(f"\nColumns: {list(df.columns)}")
print("\nFirst 5 rows:")
display(df.head())
except ImportError:
print("pandas not installed. Install with: pip install pandas")
# Expected output:
# DataFrame shape: (200, 8)
#
# Columns: ['date', 'product', 'zone', 'mw_dispatched', ...]
#
# First 5 rows:
# date product zone mw_dispatched ...
# 0 2024-06-30 Secure EPN 5.2 ...
# ...
# Quick analysis with pandas
try:
from io import BytesIO
import pandas as pd
csv_data = flexibility.export("dispatches", format="csv", limit=500)
df = pd.read_csv(BytesIO(csv_data), sep=";")
# Count by product (service type)
if "product" in df.columns:
print("Dispatches by product type:")
print(df["product"].value_counts())
except ImportError:
print("pandas not installed")
except Exception as e:
print(f"Analysis error: {e}")
# Expected output:
# Dispatches by product type:
# product
# Sustain 287
# Secure 156
# Dynamic 57
# Name: count, dtype: int64
Async Usage (Advanced)¶
For applications requiring async/await, use the async versions of the functions.
# Async usage in Jupyter (await works directly in notebooks)
# Get dispatches asynchronously
async_dispatches = await flexibility.get_async("dispatches", limit=10)
print(f"Async fetch: {async_dispatches.total_count} total records")
print(f"Retrieved: {len(async_dispatches.records)} records")
# Expected output:
# Async fetch: 1234 total records
# Retrieved: 10 records
Clean Up¶
Remove any exported files created during this tutorial.
# Clean up exported files (optional)
import os
files_to_remove = ["dispatches_export.csv", "dispatches_export.xlsx"]
for filename in files_to_remove:
if os.path.exists(filename):
os.remove(filename)
print(f"Removed {filename}")
print("\nCleanup complete!")
# Expected output:
# Removed dispatches_export.csv
# Removed dispatches_export.xlsx
#
# Cleanup complete!
Summary¶
You've learned how to:
- Understand flexibility markets and why dispatch data matters
- Import the flexibility module with a simple
from ukpyn import flexibility - List available datasets using
flexibility.available_datasets - Get dispatch events with filtering by date range and service type
- Use the generic get() function with custom ODSQL filters
- Export data to CSV, JSON, and Excel formats
Key Functions¶
| Function | Description |
|---|---|
flexibility.available_datasets |
List available dataset names |
flexibility.get(dataset, ...) |
Generic data fetch |
flexibility.get_dispatches(...) |
Fetch dispatches with filtering |
flexibility.export(dataset, format, ...) |
Export data to file format |
Date Filter Parameters¶
| Parameter | Type | Example |
|---|---|---|
start_date |
str, date, datetime | '2024-01-01' |
end_date |
str, date, datetime | date(2024, 12, 31) |
product |
str | 'Secure' |
Next Steps¶
- Explore 03-analysis-patterns.ipynb for data analysis techniques
- Check the examples folder for community contributions
- Visit the UK Power Networks Open Data Portal for more datasets