Building a Three-Country Electricity Market Model
Overview
This tutorial demonstrates how to use pyconvexity to programmatically build and solve an electricity market model. We'll create a three-country system (South Africa, Mozambique, and Eswatini) with power plants, loads, and cross-border transmission links.
What you'll learn
- How to create and work with
pyconvexitydatabase files - Setting up networks, time periods, and carriers
- Creating components (buses, generators, loads, links)
- Working with static and time series attributes
- Solving optimization models
Key concept:
pyconvexity stores your entire model in a SQLite database (.db file). This approach allows you to version control your models, share them easily, and programmatically manipulate them outside of the Convexity app.
Prerequisites
pip install pyconvexityDownload Example Files
Step 1: Define your system parameters
First, let's define the characteristics of our electricity market. We have three countries with different generation mixes and costs:
import pyconvexity as px
import numpy as np
from datetime import datetime, timedelta
import os
# Marginal costs for each technology (EUR/MWh)
marginal_costs = {"Wind": 0, "Hydro": 0, "Coal": 30, "Gas": 60, "Oil": 80}
# Power plant capacities in MW for each country
power_plant_p_nom = {
"South Africa": {"Coal": 35000, "Wind": 3000, "Gas": 8000, "Oil": 2000},
"Mozambique": {"Hydro": 1200},
"Eswatini": {"Hydro": 600},
}
# Transmission capacities between countries (MW)
transmission = {
"South Africa": {"Mozambique": 500, "Eswatini": 250},
"Mozambique": {"Eswatini": 100},
}
# Baseload demand for each country (MW)
baseload = {"South Africa": 22000, "Mozambique": 250, "Eswatini": 150}
# Geographic coordinates for visualization
country_coords = {
"South Africa": {"latitude": -25.7, "longitude": 28.2},
"Mozambique": {"latitude": -25.9, "longitude": 32.6},
"Eswatini": {"latitude": -26.3, "longitude": 31.1},
}Step 2: Create the database
pyconvexity uses SQLite databases to store models. Create a new database with the required schema:
# Define where to save the database file
db_path = "three_country_market.db"
# Create the database with `pyconvexity` schema
px.create_database_with_schema(db_path)This creates an empty database with all the necessary tables for storing networks, components, and time series data.
Step 3: Create the network and time periods
Now we'll create a network that represents our 24-hour electricity market:
with px.database_context(db_path) as conn:
# Define time range (24 hours)
start_time = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
end_time = start_time + timedelta(hours=23)
# Create network
network_req = px.CreateNetworkRequest(
name="Three Country Electricity Market",
description="Three bidding zones connected by transmission",
start_time=start_time.strftime("%Y-%m-%d %H:%M:%S"),
end_time=end_time.strftime("%Y-%m-%d %H:%M:%S"),
time_resolution="PT1H", # Hourly resolution (ISO 8601 format)
)
px.create_network(conn, network_req)Note: The database_context ensures the database connection is properly managed and changes are committed.
Step 4: Create carriers (energy types)
Carriers represent different types of energy or commodities. We need to create carriers before creating components:
# Define carriers with colors for visualization
carriers = {}
carrier_names = ["AC", "Wind", "Hydro", "Coal", "Gas", "Oil"]
carrier_colors = {
"AC": "#1f77b4",
"Wind": "#2ca02c",
"Hydro": "#17becf",
"Coal": "#8C8C8C",
"Gas": "#CCCCCC",
"Oil": "#d62728",
}
# Create each carrier
for carrier_name in carrier_names:
carrier_id = px.create_carrier(
conn,
name=carrier_name,
co2_emissions=0.0, # Could set actual emissions here
color=carrier_colors.get(carrier_name, "#808080"),
nice_name=carrier_name,
)
carriers[carrier_name] = carrier_idStep 5: Create buses (network nodes)
Buses represent locations in the network where energy is produced, consumed, or transmitted:
countries = ["Eswatini", "Mozambique", "South Africa"]
bus_ids = {}
# Create a bus for each country
for country in countries:
coords = country_coords[country]
bus_id = px.create_component(
conn,
component_type="BUS",
name=country,
carrier_id=carriers["AC"],
latitude=coords["latitude"],
longitude=coords["longitude"],
)
bus_ids[country] = bus_id
# Set bus voltage (optional)
px.set_static_attribute(conn, bus_id, "v_nom", px.StaticValue(400.0))Step 6: Create generators
Now we add power plants for each country with their capacities and costs:
# Track generator positions for spacing
generator_counter = {}
for country in countries:
generator_counter[country] = 0
coords = country_coords[country]
for tech in power_plant_p_nom[country]:
gen_name = f"{country} {tech}"
# Offset generators slightly for visualization
offset_lat = (generator_counter[country] % 3 - 1) * 0.3
offset_lon = (generator_counter[country] // 3) * 0.3
gen_lat = coords["latitude"] + offset_lat
gen_lon = coords["longitude"] + offset_lon
generator_counter[country] += 1
# Create generator
gen_id = px.create_component(
conn,
component_type="GENERATOR",
name=gen_name,
bus_id=bus_ids[country],
carrier_id=carriers[tech],
latitude=gen_lat,
longitude=gen_lon,
)
# Set capacity and cost
px.set_static_attribute(
conn, gen_id, "p_nom",
px.StaticValue(float(power_plant_p_nom[country][tech]))
)
px.set_static_attribute(
conn, gen_id, "marginal_cost",
px.StaticValue(float(marginal_costs[tech]))
)Step 7: Create loads with time series
Loads represent electricity demand. We'll create varying demand profiles over 24 hours:
for country in countries:
load_name = f"{country} load"
coords = country_coords[country]
# Offset load location slightly from bus
load_lat = coords["latitude"] + 0.1
load_lon = coords["longitude"] + 0.1
# Create load component
load_id = px.create_component(
conn,
component_type="LOAD",
name=load_name,
bus_id=bus_ids[country],
carrier_id=carriers["AC"],
latitude=load_lat,
longitude=load_lon,
)
# Generate 24-hour demand profile with ±20% variation
load_variation = baseload[country] * (1 + np.random.uniform(-0.2, 0.2, size=24))
# Set time series attribute
px.set_timeseries_attribute(conn, load_id, "p_set", load_variation.tolist())Key difference from static attributes: Time series attributes vary over time, so we pass a list of 24 values (one per hour).
Step 8: Create transmission links
Links represent controllable power transfer between regions (like HVDC or NTC):
for country in countries:
if country not in transmission:
continue
for other_country in countries:
if other_country not in transmission[country]:
continue
link_name = f"{country} - {other_country} link"
# Position link at midpoint between countries
coords0 = country_coords[country]
coords1 = country_coords[other_country]
link_lat = (coords0["latitude"] + coords1["latitude"]) / 2.0
link_lon = (coords0["longitude"] + coords1["longitude"]) / 2.0
# Create link
link_id = px.create_component(
conn,
component_type="LINK",
name=link_name,
bus0_id=bus_ids[country],
bus1_id=bus_ids[other_country],
carrier_id=carriers["AC"],
latitude=link_lat,
longitude=link_lon,
)
# Set transmission capacity
px.set_static_attribute(
conn, link_id, "p_nom",
px.StaticValue(float(transmission[country][other_country]))
)
# Allow bidirectional flow
px.set_static_attribute(conn, link_id, "p_min_pu", px.StaticValue(-1.0))
# Commit all changes
conn.commit()
print("Model created successfully!")Step 9: Solve the optimization
Now that our model is stored in the database, we can solve it:
# Solve the network
print("Solving network...")
result = px.solve_network(
db_path=db_path,
solver_name="highs", # Free open-source solver
progress_callback=lambda progress, msg: print(f" {progress}%: {msg}"),
)
print(f"Optimization complete!")
print(f"Success: {result.get('success', False)}")
print(f"Objective: {result.get('objective_value', 'N/A')}")The solver will minimize total system cost while meeting demand constraints, respecting transmission limits, and generator capacities.
Export to Excel
After solving, you can export the complete model and results to Excel for analysis or sharing:
from pyconvexity.io.excel_exporter import ExcelModelExporter
xlsx_path = db_path.replace('.db', '.xlsx')
exporter = ExcelModelExporter()
exporter.export_model_to_excel(db_path=db_path, output_path=xlsx_path)
print(f"Exported to: {xlsx_path}")The Excel workbook includes sheets for each component type, timeseries data, carriers, network configuration, and solve statistics.
Next steps
- View results: Open the
.dbfile in Convexity to visualize dispatch, prices, and flows - Analyze in Excel: Open the
.xlsxfile to explore results in a spreadsheet - Modify parameters: Change capacities, costs, or demand and re-solve
- Extract results programmatically: Use
pyconvexityto read optimization results and create custom analyses - Automate scenarios: Run multiple scenarios by varying parameters in a loop

