Running SDOM and Understanding Outputs#

This guide covers how to run SDOM optimizations and the outputs/results it provides.

Running an Optimization#

Basic Workflow#

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"
    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}")

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 xpress package (license required)
pip install 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 generation results aggregated by technology, curtailment, imports/exports and Load.

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).

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.summary_df is intentionally empty in the zonal path; 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

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#