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

ratioGap

via solve_keywords["timelimit"]

Requires executable path

HiGHS

mip_rel_gap

via solve_keywords["timelimit"]

Uses appsi_highs interface

Xpress

miprelstop

maxtime

Uses xpress_direct interface

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 (line_id, from_area, to_area, hour, signed and directional flows, directional capacity and utilization).

marginal_prices.csv

Fixed-decision LP hourly marginal prices. Copperplate has one area_id=copperplate row per hour; zonal runs have one row per area and hour.

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_zonal

  • results.areas, results.lines

  • results.area_generation_df, results.area_storage_df, results.area_thermal_generation_df, results.area_installed_plants_df, results.area_summary_df

  • results.interregional_exchanges_df

  • results.marginal_prices_df, or its defensive-copy accessor results.get_marginal_prices_dataframe()

  • results.line_congestion_duals_df, or its defensive-copy accessor results.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.01

  • Set 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

capacity_donut.png

Installed capacity by technology (donut chart)

capacity_generation_donuts.png

Side-by-side capacity and total generation donuts

heatmap_<column>.png

One 365×24 hourly dispatch heatmap per generation technology

duration_curve_total_thermal_generation.png

Descending hourly total thermal generation (copperplate)

duration_curve_load.png

Descending hourly load (copperplate)

duration_curve_net_load.png

Descending hourly net load (copperplate)

duration_curve_imports.png, duration_curve_exports.png

Descending imports or exports when the corresponding generation column is available

duration_curve_marginal_price.png

Descending available copperplate marginal prices; omitted when pricing is unavailable

heatmap_marginal_price.png

Available copperplate marginal prices by actual hour; omitted for fewer than 24 priced hours

duration_curve_system_total_thermal_generation.png, duration_curve_system_load.png, duration_curve_system_net_load.png

Descending system-level thermal generation, load, and net load for zonal results

duration_curve_interregional_signed_flows.png

Independently ranked signed flows for each interregional line when zonal exchange data is available

duration_curve_zonal_marginal_prices.png

Independently ranked available marginal prices for each zone

area_generation_stacks.png

Annual generation by area, stacked by technology, in zonal results

area_capacity_stacks_power.png

Installed power capacity by area in zonal results

line_flow_heatmap.png

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

output_dir="./results/"

Plots saved to ./results/plots/

plots_dir="./figures/"

Plots saved directly to ./figures/

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

Next Steps