Running SDOM and Understanding Outputs
This guide covers how to run SDOM optimizations and the outputs/results it provides.
Important
For new work, use the Infrasys System interface. In SDOM v0.3.0 it becomes the only supported workflow; the dict-based load_data and initialize_model interface is deprecated. The current v0.2.XX release continues to support existing dict-based scripts, including the legacy example below.
Running an Optimization
System-first interface
Install the optional integration dependencies before using this interface:
uv pip install "sdom[infrasys]"
The following copperplate example loads a scenario into an Infrasys System,
builds its Pyomo model, solves it, and attaches the results to the System for
querying and plotting. See Infrasys System workflows
for the zonal and parametric variants.
from sdom import get_default_solver_config_dict, run_solver
from sdom.infrasys_integration import add_results_to_system, plot_system_results
from sdom.infrasys_integration.make_system import load_system
from sdom.infrasys_integration.pyomo_builder import (
initialize_copperplate_model_from_system,
)
system = load_system("Data/no_exchange_run_of_river", name="copperplate")
model = initialize_copperplate_model_from_system(system, n_hours=24).create_instance()
solver_config = get_default_solver_config_dict(solver_name="highs", executable_path="")
results = run_solver(model, solver_config, case_name="copperplate")
if results.is_optimal:
add_results_to_system(system, results, run_id="copperplate-24h")
plot_system_results(
system,
run_id="copperplate-24h",
output_dir="results/copperplate",
)
else:
print(f"Optimization failed: {results.termination_condition}")
flowchart TD
Csv[Scenario CSV files] --> System[Load Infrasys System]
System --> Model[Build Pyomo model]
Model --> Config[Configure solver]
Config --> Solve[Run solver]
Solve --> Optimal{Optimal solution}
Optimal -->|yes| Attach[Attach results to System]
Attach --> Plot[Query and plot results]
Optimal -->|no| Diagnose[Inspect termination condition]
Legacy interface
This dict-based load_data and initialize_model workflow remains available
in SDOM v0.2.XX for existing scripts. It is deprecated beginning with v0.3.0;
use the System-first interface above for new work.
from sdom import (
configure_logging,
load_data,
initialize_model,
run_solver,
get_default_solver_config_dict,
export_results
)
import logging
# 1. Configure logging (optional but recommended)
configure_logging(level=logging.INFO)
# 2. Load input data
data = load_data('./Data/my_scenario/')
# 3. Initialize the optimization model
model = initialize_model(
data=data,
n_hours=8760, # Full year
with_resilience_constraints=False,
model_name="SDOM_MyScenario"
)
# 4. Configure solver
solver_config = get_default_solver_config_dict(
solver_name="cbc", # or "highs" or "xpress"
executable_path="./Solver/bin/cbc.exe"
)
# 5. Run optimization - returns an OptimizationResults object
results = run_solver(model, solver_config)
# 6. Check results and export
if results.is_optimal:
export_results(results, case="scenario_1", output_dir="./results_pyomo/")
# 7. Access results directly from the OptimizationResults object
print(f"Optimization Status: {results.termination_condition}")
print(f"Total System Cost: ${results.total_cost:,.2f}")
print(f"Total Wind Capacity: {results.total_cap_wind:.2f} MW")
print(f"Total Solar Capacity: {results.total_cap_pv:.2f} MW")
# Access detailed DataFrames
generation_df = results.generation_df
storage_df = results.storage_df
summary_df = results.summary_df
else:
print(f"Optimization failed: {results.termination_condition}")
flowchart TD
Inputs[Load scenario data] --> Model[Build Pyomo model]
Model --> Config[Configure solver]
Config --> Solve[Run solver]
Solve --> Optimal{Optimal solution}
Optimal -->|yes| Results[Access results]
Results --> Outputs[Export tables and plots]
Optimal -->|no| Diagnose[Inspect termination condition]
Tip
The OptimizationResults object provides convenient properties like is_optimal,
total_cost, total_cap_wind, total_cap_pv, and dictionaries for storage capacities.
See the Results API Reference for full documentation.
Shorter Time Horizons
For testing or sensitivity analysis, you can run shorter simulations:
# 24-hour test run
model = initialize_model(data, n_hours=24)
# One week (168 hours)
model = initialize_model(data, n_hours=168)
# One month (~730 hours)
model = initialize_model(data, n_hours=730)
Warning
Budget formulations (monthly/daily hydro) require specific hour multiples. SDOM will automatically adjust and log a warning.
Solver Configuration
Currently SDOM python package has been tested with the following solvers:
CBC Solver (Open-Source)
This solver does not have a python package to make the interface, so you need to download the executable and indicate the path of such file:
solver_config = get_default_solver_config_dict(
solver_name="cbc",
executable_path="./Solver/bin/cbc.exe" # Windows
# executable_path="./Solver/bin/cbc" # Unix/MacOS
)
# Customize solver options
solver_config["options"]["ratioGap"] = 0.01 # 1% MIP gap
solver_config["solve_keywords"]["timelimit"] = 3600 # 1 hour limit
HiGHS Solver (Open-Source)
solver_config = get_default_solver_config_dict(
solver_name="highs",
executable_path="" # Does not require the path if you import the python package highspy
)
Xpress Solver (Commercial)
FICO Xpress is a high-performance commercial solver. Requires a valid license.
Installation:
# Install SDOM with the optional Xpress dependency (license required)
uv pip install "sdom[xpress]"
Configuration:
solver_config = get_default_solver_config_dict(
solver_name="xpress",
mip_gap=0.002, # MIP relative gap (0.2%)
time_limit=3600, # Time limit in seconds
)
Xpress-specific options:
# The configuration automatically uses Xpress control names:
# - miprelstop: MIP relative gap tolerance
# - maxtime: Maximum solve time (seconds)
# - outputlog: Solver output (0=off, 1=on)
# Additional Xpress controls can be added:
solver_config["options"]["threads"] = 4 # Number of threads
solver_config["options"]["presolve"] = 1 # Enable presolve
Note
Xpress requires a valid license. The license file (xpauth.xpr) should be in your Xpress installation directory or specified via environment variables.
Solver Option Reference
Solver |
MIP Gap Option |
Time Limit |
Notes |
|---|---|---|---|
CBC |
|
via |
Requires executable path |
HiGHS |
|
via |
Uses |
Xpress |
|
|
Uses |
Outputs/Results
In the path specified by “output_dir”, sdom will writhe the following output csv files:
File name |
Description |
|---|---|
OutputGeneration_CASENAME.csv |
Hourly system generation results aggregated by technology, curtailment, imports/exports and Load. In zonal runs, this has exactly one row per hour. |
OutputGenerationPerArea_CASENAME.csv |
Zonal-only hourly generation results with one row per area and hour. |
OutputStorage_CASENAME.csv |
Hourly storage operation results (charging/discharging and SOC). |
OutputSummary_CASENAME.csv |
Summary of key simulation results and statistics. |
OutputThermalGeneration_CASENAME.csv |
Hourly results for thermal generation plants. |
OutputInstalledPowerPlants_CASENAME.csv |
Installed capacity for each individual power plant (Solar PV, Wind, Thermal). |
OutputInterregionalExchanges_CASENAME.csv |
Zonal-only line flows ( |
marginal_prices.csv |
Fixed-decision LP hourly marginal prices. Copperplate has one |
Marginal Prices
After every successful planning solve, SDOM clones the solved model, fixes its
investment, capacity, and discrete decisions to the incumbent values, and
re-solves the operational problem as a continuous LP with the configured
solver. The resulting marginal_prices.csv contains the fixed-decision
operational LMPs rather than duals from the planning MIP.
The CSV columns are hour, area_id, marginal_price_USD_per_MWh,
generation_component_USD_per_MWh, congestion_component_USD_per_MWh,
supply_balance_dual, line_congestion_dual_sum, pricing_method, and
pricing_status. supply_balance_dual is the raw imported solver dual; the
reported marginal price is its demand-derivative sign convention. A missing
pricing solve or dual is reported by pricing_status and uses missing numeric
values, never zeros.
For a zonal connected component, the lexicographically first area is the
reference area. Its LMP is reported as the generation component for every area
in that component, and each congestion component is the local LMP less that
reference LMP. The directional f_upper and f_lower duals are available via
results.get_line_congestion_duals_dataframe() for KKT auditing. Copperplate
congestion components are zero.
Zonal Results Access
When using Network=AreaTransportationModelNetwork, run_solver populates zonal fields in OptimizationResults:
results.is_zonalresults.areas,results.linesresults.area_generation_df,results.area_storage_df,results.area_thermal_generation_df,results.area_installed_plants_df,results.area_summary_dfresults.interregional_exchanges_dfresults.marginal_prices_df, or its defensive-copy accessorresults.get_marginal_prices_dataframe()results.line_congestion_duals_df, or its defensive-copy accessorresults.get_line_congestion_duals_dataframe()
results.summary_df remains available in zonal runs for aggregate system-level
summary tables, exports, and plots. Use results.area_summary_df for
per-area summary tables.
Troubleshooting
Solver Performance
For large problems:
Increase MIP gap:
solver_config["options"]["mip_rel_gap"] = 0.01Set time limit:
solver_config["solve_keywords"]["timelimit"] = 7200
Infeasible Solutions
… in progress…
Visualising Results
After running a single optimisation, use plot_results() from the
analytic_tools sub-package to generate a standard set of publication-ready
figures in one call.
Generated figures
File |
Description |
|---|---|
|
Installed capacity by technology (donut chart) |
|
Side-by-side capacity and total generation donuts |
|
One 365×24 hourly dispatch heatmap per generation technology |
|
Descending hourly total thermal generation (copperplate) |
|
Descending hourly load (copperplate) |
|
Descending hourly net load (copperplate) |
|
Descending imports or exports when the corresponding generation column is available |
|
Descending available copperplate marginal prices; omitted when pricing is unavailable |
|
Available copperplate marginal prices by actual hour; omitted for fewer than 24 priced hours |
|
Descending system-level thermal generation, load, and net load for zonal results |
|
Independently ranked signed flows for each interregional line when zonal exchange data is available |
|
Independently ranked available marginal prices for each zone |
|
Annual generation by area, stacked by technology, in zonal results |
|
Installed power capacity by area in zonal results |
|
Signed interregional line flow by hour when zonal exchange data is available |
Duration curves use descending numeric observations with ranks starting at 1 on
the x-axis (Duration-curve position). Generation and flow curves use MW;
marginal-price curves use USD/MWh. Missing, empty, or non-numeric optional
data produces a warning and omits only the affected figure.
Basic usage
from sdom import load_data, initialize_model, run_solver, get_default_solver_config_dict
from sdom.analytic_tools import plot_results
data = load_data("./Data/no_exchange_run_of_river/")
model = initialize_model(data, n_hours=8760)
solver_config = get_default_solver_config_dict(solver_name="highs", executable_path="")
results = run_solver(model, solver_config)
if results.is_optimal:
# Save all plots to ./results_pyomo/my_scenario/plots/
plot_results(results, output_dir="./results_pyomo/my_scenario/")
Plots are saved to <output_dir>/plots/. To override the plots directory
explicitly, use the plots_dir parameter instead:
plot_results(results, plots_dir="./my_output_dir/figures/")
Note
plot_results() silently skips the run and logs a warning if the result is
not optimal — it never raises on infeasible solutions.
Controlling the output directory
Parameter |
Behaviour |
|---|---|
|
Plots saved to |
|
Plots saved directly to |
Both parameters are optional but at least one must be provided, otherwise a
ValueError is raised.
Full workflow example
from sdom import (
load_data, initialize_model, run_solver,
export_results, get_default_solver_config_dict,
)
from sdom.analytic_tools import plot_results
OUTPUT_DIR = "./results_pyomo/base_scenario/"
data = load_data("./Data/no_exchange_run_of_river/")
model = initialize_model(data, n_hours=8760)
solver_cfg = get_default_solver_config_dict(solver_name="highs", executable_path="")
results = run_solver(model, solver_cfg)
if results.is_optimal:
# Export CSV tables
export_results(results, case="base_scenario", output_dir=OUTPUT_DIR)
# Generate plots alongside the CSV outputs
plot_results(results, output_dir=OUTPUT_DIR)
print(f"Total cost : ${results.total_cost:,.0f}")
print(f"Solar PV : {results.total_cap_pv:.1f} MW")
print(f"Wind : {results.total_cap_wind:.1f} MW")
Running Parametric & Sensitivity Studies
To run multi-dimensional parameter sweeps in parallel (e.g., sweeping GenMix_Target, storage CAPEX, or load growth factors), use the built-in ParametricStudy API.
See the dedicated guide: Parametric & Sensitivity Analysis