Configuration Guide¶
Top-Level Structure¶
YAML configuration files are validated by Pydantic v2. The configuration drives the two-stage decomposition [25]: capacity expansion via the master problem [24] followed by operational dispatch using a rolling horizon approach. A complete file has the following top-level keys:
# Simulation settings
simulation_mode: development # "development" or "unit_commitment"
unit_commitment_hours: 24 # Hours for UC window (UC mode only)
date_start: "01/01/2025 00:00" # Simulation start date (DD/MM/YYYY HH:MM)
# Module configurations
temporal: { ... } # Time resolution settings
solver: { ... } # Solver settings
n1_security: { ... } # N-1 contingency settings
master_problem: { ... } # Capacity expansion settings
enable_primary_energy: true # Enable fuel supply chain
# Network definition
meta_network:
systems: [system_a, system_b] # List of system names
systems_links: { ... } # Inter-system connections
# System definitions
systems:
system_a: { ... } # Full system configuration
system_b: { ... }
Simulation Modes¶
| Mode | Description | Model Type | Use Case |
|---|---|---|---|
development |
Economic dispatch (ED) | Pure LP | Long-term planning, capacity expansion |
unit_commitment |
Unit commitment (UC) | MIP | Short-term scheduling with binary start/stop |
development mode formulates a pure LP with continuous generator output -- recommended for multi-year capacity expansion studies. unit_commitment mode introduces binary commitment variables, producing a MIP that models startup/shutdown costs and minimum up/down time constraints.
Temporal Configuration¶
temporal:
resolution_hours: 1 # Time step (hours): 1, 2, 3, 4, 6
use_rolling_horizon: true # Enable rolling horizon dispatch
rolling_horizon_hours: 48 # Window size (hours)
overlap_hours: 6 # Overlap between windows
# Sub-model resolutions (upscaling for faster solve)
investment_resolution: 0 # Master problem (0 = full resolution)
primary_energy_resolution: 24 # Fuel model (hours per period)
battery_soc_resolution: 0 # SOC tracking (0 = full)
ev_resolution: 0 # EV model (0 = full)
reserve_resolution: 0 # Reserve model (0 = full)
The rolling horizon splits each year into overlapping windows. Only the non-overlapping portion contributes to the final solution; the overlap ensures continuity of storage SOC and ramp trajectories.
Sub-model resolutions allow individual components to operate at coarser time steps. For example, primary_energy_resolution: 24 uses daily time blocks for the fuel supply chain, reducing variables while retaining hourly generator dispatch. A value of 0 uses the same resolution as the main model.
Solver Configuration¶
solver:
name: highs # highs, cbc, glpk, gurobi, cplex, scip, xpress
threads: 4 # Number of solver threads
time_limit: 3600 # Max solve time (seconds)
gap: 0.01 # MIP optimality gap (1%)
verbose: false # Show solver output
options: {} # Solver-specific key-value pairs
The options dictionary passes solver-specific parameters directly to the JuMP optimizer:
solver:
name: highs
threads: 4
options:
presolve: "on"
solver_method: "ipm"
simplex_scale_strategy: "max_equilibration"
primal_feasibility_tolerance: 1.0e-7
dual_feasibility_tolerance: 1.0e-7
See the Solver Guide for solver-specific options and tuning.
N-1 Security¶
N-1 contingency analysis ensures the system remains secure if any single transmission line or generator fails.
n1_security:
enabled: false # Enable N-1 constraints
transmission_enabled: false # Transmission contingency
generation_enabled: false # Generation contingency
transmission_reserve_factor: 0.8 # Line loading limit under N-1
generation_reserve_factor: 1.0 # Generation margin
When transmission_enabled is true, the model identifies the critical N-1 element at each time step and enforces that remaining lines can carry the redistributed flow. The transmission_reserve_factor limits line loading to 80% of thermal capacity for contingency headroom.
Master Problem¶
master_problem:
stochastic: false # Enable stochastic programming
representative_days_per_year: 5 # Days for operational validation
min_day_separation: 7 # Min days between representatives
# MGA / SPORES: Near-optimal alternative exploration
mga:
enabled: false # Master toggle
method: mga # "mga" (HSJ loop) | "spores" (per-objective sweep)
# MGA-only knob (ignored under SPORES):
num_alternatives: 10 # K alternatives for the HSJ loop
# SPORES-only knob (must be empty under MGA):
objectives: [] # e.g. [min_total_build, max_regional_equity]
# Shared knobs:
slack_fraction: 0.05 # Cost slack (0.05 = 5% above optimal)
investment_threshold: 0.1 # MW threshold for HSJ frequency scoring
ESFEX offers two near-optimal exploration methods. Both share the cost-slack envelope \(Z \leq (1+\varepsilon) C^*\) but differ in how alternatives are produced:
method: mga(default) — runs the classical Hop-Skip-Jump (HSJ) loopnum_alternativestimes. Each iteration maximises a single diversity objective that penalises investment variables seen in previous alternatives.method: spores— solves one alternative per declared objective (see SporesObjective). The alternative count equalslen(objectives);num_alternativesis ignored. Example:
master_problem:
mga:
enabled: true
method: spores
objectives:
- min_total_build # smallest near-optimal portfolio
- max_tech_equity # technology-diversified plan
- max_regional_equity # spatially-spread plan
- evolutionary_dist # maximally different from cost-optimal
slack_fraction: 0.05
Results are exported under /mga/ in the HDF5 file with /mga.attrs["method"] and a per-alternative objective tag (see HDF5 Output Schema § MGA Results). Alternative 0 (cost-optimal) is used for operational dispatch.
System Configuration¶
Demand¶
systems:
my_system:
name: my_system
demand_path: demand.xlsx # Path to demand data file
demand_scale: 1.0 # Demand multiplier
demand_growth: 0.02 # 2% annual growth rate
Demand growth is applied multiplicatively: year y demand equals D_base * (1 + demand_growth)^(y-1). See Demand Data for file format details.
Nodes¶
nodes:
adjacency_matrix: # N x N connectivity (0 or capacity MW)
- [0, 100, 0]
- [100, 0, 50]
- [0, 50, 0]
coordinates: # [longitude, latitude] per node
- [-82.38, 23.13]
- [-81.95, 22.40]
- [-80.45, 22.07]
names: ["Havana", "Cienfuegos", "Camaguey"]
The adjacency matrix is symmetric: entry (i, j) gives transmission capacity in MW. A value of 0 means no direct connection. Node coordinates are used for distance calculations (zone interconnection costs, GUI map display).
Generators¶
Each generator is defined as a named entry with per-node arrays:
generators:
solar_pv:
name: Solar PV
type: Renewable # "Renewable" or "Non-renewable"
fuel: Solar
rated_power: [50.0, 30.0, 20.0] # MW per node
min_power: [0.0, 0.0, 0.0] # Fraction of rated
invest_cost: [800000, 800000, 800000] # $/MW
invest_max_power: [500, 300, 200] # MW max investment
fuel_cost: [0.0, 0.0, 0.0] # $/MWh
fixed_cost: [5.0, 5.0, 5.0] # $/MWh (fixed O&M)
maintenance_cost: [2.0, 2.0, 2.0] # $/MWh (variable O&M)
start_up_cost: [0.0, 0.0, 0.0] # $/start
decommissioning_cost: [0, 0, 0] # $/MW
ramp_up: [1.0, 1.0, 1.0] # Fraction/hour
ramp_down: [1.0, 1.0, 1.0]
min_up_time: [0, 0, 0] # Hours (UC mode only)
min_down_time: [0, 0, 0]
eff_at_rated: [1.0, 1.0, 1.0] # Efficiency at rated power
eff_at_min: [1.0, 1.0, 1.0] # Efficiency at min power
life_time: [25, 25, 25] # Years
initial_age: [0, 0, 0] # Years
degradation_rate: [0.005, 0.005, 0.005] # Annual capacity degradation
inertia: [0.0, 0.0, 0.0] # MVA*s
Availability: solar_profile.csv # Availability file path
Every per-node parameter must have exactly num_nodes entries; a mismatch causes a validation error.
Generator Age and Retirement¶
Generators are retired when age exceeds lifetime:
- Existing units:
age = initial_age + (year_idx - 1) - Invested units:
age = year_idx - investment_year
Setting initial_age: [20, 20, 20] with life_time: [25, 25, 25] means the generator retires after 5 simulation years.
Cost Curves and Bidding Curves¶
Piecewise cost curves replace the flat fuel_cost parameter. Four curve types are available:
generators:
gas_turbine:
name: Gas Turbine
type: Non-renewable
fuel: Natural Gas
# ... other fields ...
fuel_cost_curve:
- curve_type: stepwise # Node 0: stepwise blocks
blocks:
- { fraction: 0.4, price: 45.0 } # First 40% at $45/MWh
- { fraction: 0.3, price: 55.0 } # Next 30% at $55/MWh
- { fraction: 0.3, price: 75.0 } # Last 30% at $75/MWh
- curve_type: linear # Node 1: linear ramp
price_at_zero: 40.0 # $/MWh at minimum output
price_at_max: 80.0 # $/MWh at rated power
- curve_type: flat # Node 2: constant price
flat_price: 50.0 # $/MWh at all output levels
Supported curve_type values:
| Curve Type | Parameters | Description |
|---|---|---|
flat |
flat_price |
Constant marginal cost at all output levels |
linear |
price_at_zero, price_at_max |
Linear interpolation from min to max output |
stepwise |
blocks (list of {fraction, price}) |
Piecewise constant blocks; fractions must sum to 1.0 |
exponential |
base_price, scale_factor |
price(P) = base_price * exp(scale_factor * P / P_max) |
fuel_cost_curve overrides the flat fuel_cost array. Each list entry corresponds to one node. All curves are internally normalized to stepwise blocks for the optimizer.
Technologies (Investment Candidates)¶
Technologies define investment options for new capacity. Unlike generators (existing physical units), these are candidates evaluated by the master problem.
technologies:
solar_pv_new:
name: Solar PV (New)
type: Renewable
fuel: Solar
invest_cost: [700000, 700000, 700000] # $/MW per node
invest_max_power: [500, 300, 200] # MW max per node
Availability: solar_profile.csv
eff_at_rated: [1.0, 1.0, 1.0]
degradation_rate: [0.005, 0.005, 0.005]
lifetime: 25
fuel_cost: [0.0, 0.0, 0.0]
fixed_cost: [5.0, 5.0, 5.0]
maintenance_cost: [2.0, 2.0, 2.0]
wind_onshore:
name: Wind Onshore
type: Renewable
fuel: Wind
invest_cost: [1200000, 1200000, 1200000]
invest_max_power: [200, 100, 100]
Availability: wind_profile.csv
eff_at_rated: [1.0, 1.0, 1.0]
degradation_rate: [0.002, 0.002, 0.002]
lifetime: 20
fuel_cost: [0.0, 0.0, 0.0]
fuel_cost_curve: # Optional: per-node bidding curves
- curve_type: flat
flat_price: 0.0
- curve_type: flat
flat_price: 0.0
- curve_type: flat
flat_price: 0.0
Selected technologies produce virtual generator instances that participate in operational dispatch.
Battery Technologies¶
battery_technologies:
li_ion_4h:
name: Li-Ion 4h
invest_cost_power: [180000, 180000, 180000] # $/MW per node
invest_cost_energy: [120000, 120000, 120000] # $/MWh per node
invest_max_power: [200, 100, 100] # MW max per node
invest_max_capacity: [800, 400, 400] # MWh max per node
min_duration_hours: 2.0 # Min E/P ratio (hours)
max_duration_hours: 8.0 # Max E/P ratio (hours)
efficiency_charge: [0.95, 0.95, 0.95]
efficiency_discharge: [0.95, 0.95, 0.95]
degradation_rate: [0.01, 0.01, 0.01]
lifetime: 15
soc_initial: [0.5, 0.5, 0.5]
max_DoD: [0.9, 0.9, 0.9]
maintenance_cost: [1.0, 1.0, 1.0]
decommissioning_cost: [5000, 5000, 5000]
Batteries (Existing)¶
batteries:
li_ion:
name: Li-Ion Storage
capacity: [0.0, 0.0, 0.0] # MWh per node
max_charge_power: [0.0, 0.0, 0.0] # MW
max_discharge_power: [0.0, 0.0, 0.0] # MW
charge_efficiency: [0.95, 0.95, 0.95]
discharge_efficiency: [0.95, 0.95, 0.95]
soc_min: [0.10, 0.10, 0.10] # Fraction
soc_max: [0.95, 0.95, 0.95]
soc_initial: [0.50, 0.50, 0.50]
self_discharge: [0.0001, 0.0001, 0.0001] # Per hour
invest_cost_power: [200000, 200000, 200000] # $/MW
invest_cost_capacity: [150000, 150000, 150000] # $/MWh
invest_max_power: [200, 100, 100] # MW
invest_max_capacity: [800, 400, 400] # MWh
min_duration_hours: 2.0 # Min E/P ratio
max_duration_hours: 8.0 # Max E/P ratio
life_time: [15, 15, 15]
maintenance_cost: [1.0, 1.0, 1.0] # $/MWh
spillage: false # Allow energy spillage
Battery SOC is subject to a cyclic constraint: SOC(t_last) == SOC(t_initial) at the end of each operational day, preventing batteries from acting as free energy sources.
Penalties¶
Penalties are costs for violating soft constraints. They guide the optimizer toward feasible solutions and provide shadow prices for constraint relaxation.
penalties:
loss_of_load: 10000000.0 # $/MW not supplied (VOLL)
curtailment: 100.0 # $/MWh curtailed energy
loss_of_reserve_static: 100.0 # $/MW static reserve deficit
loss_of_reserve_dynamic: 100.0 # $/MW dynamic reserve deficit
loss_of_inertia: 200.0 # $/MW-s inertia deficit
transfer_margin: 100.0 # $/MW transfer margin violation
fre_penetration_loss: 100.0 # $/MWh RE shortfall
co2_cost: 10.0 # $/tCO2 carbon price
co2_budget_violation: 500.0 # $/tCO2 over budget
max_curtailment_ratio: 0.05 # Max curtailment as fraction of RE gen
ev_loss: 10.0 # $/MWh EV demand not met
loss_of_fuel_supply: 100.0 # $/MW-eq fuel deficit
transport_congestion: 100.0 # $/MW congestion penalty
storage_violation: 100.0 # $/MW storage constraint violation
non_electric_demand_loss: 100.0 # $/unit fuel demand unmet
soc_violation: 1000000.0 # $/MWh SOC limit violation
delay_retirement_per_mw: 50000.0 # $/MW retirement delay penalty
rooftop_curtailment: 5.0 # $/MWh rooftop solar curtailed
loss_of_load (VOLL) must be significantly higher than any generation cost so the optimizer prefers expensive dispatch over load shedding. Typical VOLL values: \(1,000-\)50,000/MWh depending on jurisdiction.
max_curtailment_ratio limits RE curtailment to a fraction of total RE generation (e.g., 0.05 = 5% maximum). Enforced as a constraint, not a cost.
Targets and Constraints¶
co2_budget:
enabled: true
annual_budget: 1000000.0 # tonnes CO2/year
target_re_penetration: 0.80 # 80% RE target
initial_re_penetration: 0.0 # Starting RE fraction (auto-calculated if 0)
min_annual_increment: 0.0 # Min RE increase per year
max_annual_increment: 1.0 # Max RE increase per year
max_curtailment_ratio: 0.05 # 5% curtailment limit
# Economic parameters
discount_rate: 0.05 # 5% discount rate
demand_growth: 0.02 # 2% annual demand growth
MAX_ANNUAL_SYSTEM_COST: 1000000000.0 # Annual investment budget ($/year)
When initial_re_penetration is 0.0, ESFEX auto-calculates it from existing renewable capacities. The RE target is enforced as a master problem constraint, with fre_penetration_loss as the shortfall penalty.
DC Power Flow¶
dc_power_flow:
enabled: true
base_impedance: 100.0 # Ohms
max_angle_diff_deg: 30.0 # Max voltage angle difference (degrees)
transmission_lines_geo: # Per-line data
- line_id: line_0
from_node: 0
to_node: 1
capacity_mw: 100.0
reactance_pu: 0.05
resistance_pu: 0.01
length_km: 200.0
voltage_kv: 220.0
- line_id: line_1
from_node: 1
to_node: 2
capacity_mw: 50.0
reactance_pu: 0.08
resistance_pu: 0.015
length_km: 150.0
voltage_kv: 110.0
With DC power flow enabled, transmission uses linearized power flow equations: P_ij = (theta_i - theta_j) / x_ij. The max_angle_diff_deg constraint limits angle differences between connected nodes.
With DC power flow disabled, a transport model applies: flows are limited only by line capacity without angle constraints.
EV Configuration¶
ev_categories:
sedan:
battery_capacity_kwh: 60.0
max_charge_power_kw: 11.0
max_discharge_power_kw: 7.0
charge_efficiency: 0.95
discharge_efficiency: 0.92
min_soc: 0.20
max_soc: 0.90
ev_quantity:
sedan: [1000, 500, 300] # Per node
EV_initial_soc: [0.5, 0.5, 0.5] # Per node
base_patterns:
commuter: [0.1, 0.1, ..., 0.8, 0.9] # 24 hourly availability values
EV charging demand uses an S-curve growth model. When EV optimization is enabled, the optimizer decides charging/V2G schedules; when disabled, EV demand is added to total_demand.
Sectoral Demand¶
electric_demand:
residential:
criticality: 0.7 # 0 = fully flexible, 1 = critical
flexibility: 0.3
industrial:
criticality: 0.9
flexibility: 0.1
commercial:
criticality: 0.5
flexibility: 0.5
sector_distribution:
0: # Node 0
residential: 0.40 # 40% residential
industrial: 0.35
commercial: 0.25
1: # Node 1
residential: 0.50
industrial: 0.30
commercial: 0.20
Fractions per node must sum to 1.0. Higher criticality sectors are shed last.
Multi-System Configuration¶
meta_network:
systems: [cuba_main, isla_juventud]
dynamic_transfer_pricing: false
systems_links:
- systems: [cuba_main, isla_juventud]
from_nodes: [2] # Node in system A
to_nodes: [0] # Node in system B
capacity: [50.0] # MW existing
invest_cost: [500000.0] # $/MW for expansion
invest_max: [200.0] # MW max expansion
Each system is solved independently for operational dispatch but shares investment decisions through the master problem.
External System Files¶
# main_config.yaml
meta_network:
systems: [mainland, island]
systems:
mainland: mainland_system.yaml # Reference to external file
island: island_system.yaml
Paths are resolved relative to the main configuration file.
Validation¶
Pydantic v2 validates all configuration at load time:
- Type checking: All fields must match expected types
- Range validation: Values must be within acceptable ranges (e.g., efficiencies in [0, 1])
- Cross-field validation: Array lengths must match
num_nodes - File existence: Referenced data files (demand, availability) are checked
- Enum validation: Fields like
typemust be one of the allowed values
Common validation errors:
| Error | Cause | Fix |
|---|---|---|
list length must match num_nodes |
Per-node array wrong size | Add/remove entries to match node count |
field required |
Missing required field | Add the field to YAML |
value is not a valid float |
String where number expected | Remove quotes from numeric values |
Input should be 'Renewable' or 'Non-renewable' |
Invalid generator type | Use exact case: Renewable or Non-renewable |
ensure this value is greater than or equal to 0 |
Negative value where positive required | Check for typos in numeric values |
Validating Before Running¶
Performs all checks without starting the optimization.
Complete Example¶
simulation_mode: development
date_start: "01/01/2025 00:00"
temporal:
resolution_hours: 1
use_rolling_horizon: true
rolling_horizon_hours: 48
overlap_hours: 6
solver:
name: highs
threads: 4
time_limit: 3600
gap: 0.01
verbose: false
n1_security:
enabled: false
master_problem:
representative_days_per_year: 5
enable_primary_energy: false
meta_network:
systems: [island]
systems:
island:
name: island
demand_path: demand.xlsx
demand_scale: 1.0
demand_growth: 0.02
discount_rate: 0.05
target_re_penetration: 0.80
max_curtailment_ratio: 0.05
MAX_ANNUAL_SYSTEM_COST: 1000000000.0
nodes:
adjacency_matrix:
- [0, 100]
- [100, 0]
coordinates:
- [-82.38, 23.13]
- [-81.95, 22.40]
names: ["Node_A", "Node_B"]
generators:
diesel:
name: Diesel
type: Non-renewable
fuel: Diesel
rated_power: [50.0, 30.0]
min_power: [0.3, 0.3]
invest_cost: [0, 0]
invest_max_power: [0, 0]
fuel_cost: [85.0, 85.0]
fixed_cost: [5.0, 5.0]
maintenance_cost: [3.0, 3.0]
start_up_cost: [500, 500]
decommissioning_cost: [10000, 10000]
ramp_up: [0.5, 0.5]
ramp_down: [0.5, 0.5]
min_up: [2, 2]
min_down: [2, 2]
eff_at_rated: [0.38, 0.38]
eff_at_min: [0.30, 0.30]
life_time: [30, 30]
initial_age: [10, 10]
degradation_rate: [0.01, 0.01]
inertia: [5.0, 5.0]
technologies:
solar_pv:
name: Solar PV
type: Renewable
fuel: Solar
invest_cost: [700000, 700000]
invest_max_power: [200, 100]
Availability: solar_profile.csv
eff_at_rated: [1.0, 1.0]
degradation_rate: [0.005, 0.005]
lifetime: 25
battery_technologies:
li_ion:
name: Li-Ion
invest_cost_power: [180000, 180000]
invest_cost_energy: [120000, 120000]
invest_max_power: [100, 50]
invest_max_capacity: [400, 200]
min_duration_hours: 2.0
max_duration_hours: 8.0
efficiency_charge: [0.95, 0.95]
efficiency_discharge: [0.95, 0.95]
degradation_rate: [0.01, 0.01]
lifetime: 15
penalties:
loss_of_load: 10000000.0
fre_penetration_loss: 100.0
curtailment: 100.0
max_curtailment_ratio: 0.05
See the Config Reference for a complete field listing.