Resiliency Module
API reference for sdom.resiliency – the operational-resiliency evaluation
module. See the user-guide page Resiliency Evaluation for the
mathematical background, file-format requirements and worked examples.
Top-Level Convenience
- sdom.resiliency.evaluate_resiliency(snapshot_dir, *, inputs_dir, outage_spec, year=2030, scenario_id=1, n_hours=8760, hours=None, min_soc_per_tech=None, slack_penalty=10000.0, curtailment_penalty=0.0, soc_slack_penalty=1000.0, formulation_overrides=None, n_workers=None, solver='highs', solver_options=None, critical_load_MW=None, profile_baseline=False, profile_outages=False)[source]
End-to-end helper: load -> baseline dispatch -> outage evaluation.
- Parameters:
snapshot_dir (str or pathlib.Path) – Snapshot directory passed to
load_designed_system().inputs_dir (str or pathlib.Path) – Previous-stage inputs directory.
outage_spec (OutageSpec) – Outage scenario specification.
year (int, optional) – Calendar year of the snapshot. Default
2030.scenario_id (int, optional) – Scenario / Run id resolved from the snapshot CSVs. Default
1.n_hours (int, optional) – Baseline-dispatch horizon length. Default
8760.hours (iterable of int, optional) – Anchor hours to evaluate.
None(default) evaluates every hour1..n_hours.min_soc_per_tech (dict, optional) – Operational SOC floor per storage tech (fraction of
Cap_E); forwarded to both the baseline builder and the per-hour outage runner. DefaultNone.slack_penalty (float, optional) – Penalty (USD/MWh) applied to slack
u[t]in the outage LP. Default10_000.curtailment_penalty (float, optional) – Penalty applied to curtailed VRE energy (USD/MWh). Default
0.soc_slack_penalty (float, optional) – Penalty (USD/MWh) on the per-tech SOC recovery-target slack in the outage LP. Forwarded to
run_resiliency_evaluation()andbuild_outage_dispatch(). Default1_000.formulation_overrides (dict, optional) – Component formulation overrides forwarded to
load_designed_system().n_workers (int, optional) – Worker pool size for the per-hour evaluation.
None(default) resolves tomax(1, os.cpu_count() - 1)insiderun_resiliency_evaluation().solver (str, optional) – Pyomo solver name.
"highs"first triesappsi_highs. Default"highs".solver_options (dict, optional) – Solver options forwarded to both the baseline solve and every per-hour outage solve.
critical_load_MW (float, optional) – Constant critical load (MW) used in place of the hourly load series during the outage sub-horizon of each per-hour LP. Forwarded to
run_resiliency_evaluation()and through it tobuild_outage_dispatch().None(default) preserves the original behaviour. Must be non-negative.profile_baseline (bool, optional) – When
True, attach aModelInitProfilerto the baseline build/solve and print summary tables. DefaultFalse(opt-in to avoid runtime/logging overhead in the top-level helper).profile_outages (bool, optional) – When
Trueandn_workers == 1, profile every per-hour outage build. Ignored whenn_workers > 1(a per-worker summary is rarely useful). DefaultFalse(opt-in).
- Returns:
Per-hour records (sorted by anchor hour) plus run metadata including
n_workers_used,n_hours,solverand theoutage_specreference.- Return type:
See also
sdom.resiliency.load_designed_system,sdom.resiliency.build_baseline_dispatch,sdom.resiliency.run_baseline_dispatch,sdom.resiliency.run_resiliency_evaluationExamples
>>> from sdom.resiliency import OutageSpec, evaluate_resiliency >>> spec = OutageSpec( ... duration_hours=4, ... recovery_hours=4, ... outaged_assets={"imports": "all"}, ... ) >>> results = evaluate_resiliency( ... "snapshot/", ... inputs_dir="inputs/", ... outage_spec=spec, ... n_hours=24, ... hours=[1, 5, 10], ... n_workers=1, ... )
Data Loading
- sdom.resiliency.load_designed_system(snapshot_dir, *, inputs_dir, year=2030, scenario_id=1, formulation_overrides=None, attach_cem_data=True)[source]
Load a fixed-capacity designed system from SDOM output snapshots.
- Parameters:
snapshot_dir (str or pathlib.Path) – Directory containing
OutputSummary_*andOutputSelectedVRE_*CSVs from a prior SDOM design run.OutputGeneration_*andOutputStorage_*files are ignored at this stage; files containingPhase1in their names are also excluded.inputs_dir (str or pathlib.Path) – Directory holding the previous-stage time-series and parameter CSVs (
Load_hourly_*,CFSolar_*,CFWind_*,Data_Balancing_units_*,StorageData_*,Import_Cap_*,import_prices_*,Export_Cap_*,export_prices_*,fixed_dem_charges.csv,var_dem_charges.csv).year (int, optional) – Calendar year used in CSV filenames. Default
2030.scenario_id (int, optional) – Scenario / Run id to extract from snapshot CSVs. Default
1. If a snapshot file contains a single unique scenario, that one is used and a warning is emitted whenscenario_iddiffers. If multiple scenarios are present,scenario_idmust match one of them; otherwiseValueErroris raised listing the available ids.formulation_overrides (dict, optional) – Mapping
{component: formulation_name}overlaid on the default formulation map.attach_cem_data (bool, optional) – When
True(default), also load the CEM-shaped data dict viaload_cem_data()and attach it asDesignedSystem.cem_data. Required bysdom.resiliency.build_baseline_dispatch(). Set toFalseto skip this step in environments where the previous-stage inputs directory is not a valid CEM inputs folder.
- Return type:
- Raises:
FileNotFoundError – When the
OutputSummary_*snapshot is missing.ValueError – When
scenario_idcannot be resolved against the snapshot.
Examples
>>> ds = load_designed_system( ... "Data/resiliency_eval/3MW_critical_load_24hrs_outage_24hrs_recovery", ... inputs_dir="Data/resiliency_eval/inputs_previous_stage/Paper_PGnE/Paper", ... year=2030, ... scenario_id=1, ... ) >>> ds.scenario_id 1
- class sdom.resiliency.DesignedSystem(storage_caps: dict[str, dict[str, float]] = <factory>, thermal_caps: dict[str, dict[str, float]] = <factory>, solar_caps: dict[str, float] = <factory>, wind_caps: dict[str, float] = <factory>, solar_fom: dict[str, float] = <factory>, wind_fom: dict[str, float] = <factory>, load: ~pandas.core.series.Series | None = None, cf_solar: ~pandas.core.frame.DataFrame | None = None, cf_wind: ~pandas.core.frame.DataFrame | None = None, nuclear: ~pandas.core.series.Series | None = None, hydro: ~pandas.core.series.Series | None = None, other_renewables: ~pandas.core.series.Series | None = None, import_cap: ~pandas.core.series.Series | None = None, import_price: ~pandas.core.series.Series | None = None, export_cap: ~pandas.core.series.Series | None = None, export_price: ~pandas.core.series.Series | None = None, phi_fix_t: ~pandas.core.series.Series | None = None, phi_var_t: ~pandas.core.series.Series | None = None, month_of_hour: ~pandas.core.series.Series | None = None, scenario_id: int = 1, year: int = 2030, formulation_map: dict[str, str] = <factory>, cem_data: dict | None = None)[source]
Fixed-capacity designed system loaded from SDOM output snapshots.
- Parameters:
storage_caps (dict) – Mapping
{tech: {"Cap_Pch", "Cap_Pdis", "Cap_E", "eta_ch", "eta_dis", "soc_min_frac", "vom", "fom", "cost_ratio"}}for each storage technology with non-zero capacity. Capacities are in MW / MWh.fomis the fixed-O&M rate (USD/kW-yr) andcost_ratiois the share of that rate billed against the charge-side power capacity (the remainder is billed against the discharge-side power capacity), matching the CEM accounting insdom.models.formulations_storage.storage_fixed_om_cost_expr_rule().thermal_caps (dict) – Mapping
{tech: {"capacity_MW", "heat_rate", "fuel_cost", "vom", "var_cost", "fom"}}for each thermal technology with non-zero capacity.var_cost = heat_rate * fuel_cost + vom.fomis the fixed-O&M rate (USD/kW-yr) aggregated across plants.solar_caps (dict) – Mapping
{plant_id: capacity_MW}for selected solar plants.wind_caps (dict) – Mapping
{plant_id: capacity_MW}for selected wind plants.solar_fom (dict) – Mapping
{plant_id: fom_USD_per_kW_yr}for the selected solar / wind plants. Values come from theFOM_Mcolumn of theCapSolar_*.csv/CapWind_*.csvprevious-stage inputs. Carried for auditing and downstream reporting; the baseline-dispatch objective itself sources FOM from the CEM block expressions so the two paths cannot diverge.wind_fom (dict) – Mapping
{plant_id: fom_USD_per_kW_yr}for the selected solar / wind plants. Values come from theFOM_Mcolumn of theCapSolar_*.csv/CapWind_*.csvprevious-stage inputs. Carried for auditing and downstream reporting; the baseline-dispatch objective itself sources FOM from the CEM block expressions so the two paths cannot diverge.load (pandas.Series) – Hourly time-series (length 8760) indexed by hour-of-year (1..8760).
nuclear (pandas.Series) – Hourly time-series (length 8760) indexed by hour-of-year (1..8760).
hydro (pandas.Series) – Hourly time-series (length 8760) indexed by hour-of-year (1..8760).
other_renewables (pandas.Series) – Hourly time-series (length 8760) indexed by hour-of-year (1..8760).
cf_solar (pandas.DataFrame) – Hourly capacity factors with columns indexed by plant id.
cf_wind (pandas.DataFrame) – Hourly capacity factors with columns indexed by plant id.
import_cap (pandas.Series) – Hourly grid-exchange capacity and price series.
import_price (pandas.Series) – Hourly grid-exchange capacity and price series.
export_cap (pandas.Series) – Hourly grid-exchange capacity and price series.
export_price (pandas.Series) – Hourly grid-exchange capacity and price series.
phi_fix_t (pandas.Series) – Hourly fixed and variable demand-charge tariffs (USD/MW or USD/MWh).
phi_var_t (pandas.Series) – Hourly fixed and variable demand-charge tariffs (USD/MW or USD/MWh).
month_of_hour (pandas.Series) – Mapping from hour-of-year (1..8760) to calendar month (1..12) used to bill demand charges per month.
scenario_id (int) – Scenario / Run id resolved from the snapshot CSVs.
year (int) – Calendar year of the snapshot.
formulation_map (dict) – Mapping
{component: formulation_name}resolved from defaults plus user-provided overrides.cem_data (dict, optional) – CEM-shaped data dict (as returned by
sdom.io_manager.load_data()) used by the baseline dispatch builder to reuse the planning-model formulations insdom.models.Nonewhen the previous-stage inputs were not reloaded for that purpose.
- __init__(storage_caps: dict[str, dict[str, float]] = <factory>, thermal_caps: dict[str, dict[str, float]] = <factory>, solar_caps: dict[str, float] = <factory>, wind_caps: dict[str, float] = <factory>, solar_fom: dict[str, float] = <factory>, wind_fom: dict[str, float] = <factory>, load: ~pandas.core.series.Series | None = None, cf_solar: ~pandas.core.frame.DataFrame | None = None, cf_wind: ~pandas.core.frame.DataFrame | None = None, nuclear: ~pandas.core.series.Series | None = None, hydro: ~pandas.core.series.Series | None = None, other_renewables: ~pandas.core.series.Series | None = None, import_cap: ~pandas.core.series.Series | None = None, import_price: ~pandas.core.series.Series | None = None, export_cap: ~pandas.core.series.Series | None = None, export_price: ~pandas.core.series.Series | None = None, phi_fix_t: ~pandas.core.series.Series | None = None, phi_var_t: ~pandas.core.series.Series | None = None, month_of_hour: ~pandas.core.series.Series | None = None, scenario_id: int = 1, year: int = 2030, formulation_map: dict[str, str] = <factory>, cem_data: dict | None = None) None
- class sdom.resiliency.BaselineState(soc_trajectory: ~pandas.core.frame.DataFrame | None = None, solver_status: str | None = None, objective_value: float | None = None, metadata: dict[str, ~typing.Any] = <factory>)[source]
Placeholder container for baseline-dispatch outputs (Phase 2).
- Parameters:
soc_trajectory (pandas.DataFrame, optional) – Hourly state-of-charge per storage technology (hour x tech).
solver_status (str, optional) – Solver termination status from the baseline run.
objective_value (float, optional) – Baseline objective value (USD).
metadata (dict, optional) – Free-form solver / run metadata.
Outage Specification
- class sdom.resiliency.OutageSpec(duration_hours: int, recovery_hours: int | dict[str, int], outaged_assets: dict[str, str | ~typing.Iterable], derating_factors: dict[tuple[str, str], float] = <factory>, min_soc_recovery: dict[str, float] | None = None, per_asset_durations: dict[tuple[str, str], int] = <factory>)[source]
Specification of an outage / de-rating scenario.
- Parameters:
duration_hours (int) – Outage duration applied to all listed assets unless overridden by
per_asset_durations.recovery_hours (int or dict) – Hours allowed after the outage for storage devices to recover the SOC target. A single
intbroadcasts to every storage technology; adict{tech: hours}gives per-tech values.outaged_assets (dict) – Mapping
{component: asset_selector}wherecomponentis one ofVALID_COMPONENTSandasset_selectoris either the string"all"or an iterable of asset/plant IDs. The must-run components inMUST_RUN_COMPONENTSonly accept"all"in this iteration; iterables raiseNotImplementedErrorfromvalidate().derating_factors (dict, optional) – Mapping
{(component, asset_id): factor}with each factor in[0, 1]. Missing assets default to0(full outage) when listed inoutaged_assets, or1(no outage) otherwise.min_soc_recovery (dict, optional) – Per-tech target SOC fraction at the end of the recovery window.
None(default) -> baseline SOC at the end of the recovery window divided byCap_Eper tech.per_asset_durations (dict, optional) – Optional per-asset overrides
{(component, asset_id): hours}forduration_hours.
- Raises:
ValueError – If a derating factor is outside
[0, 1], a component name is not inVALID_COMPONENTS, or a string asset selector is not"all".
- validate(designed_system) None[source]
Validate the specification against
designed_system.- Parameters:
designed_system (DesignedSystem) – Source of truth for asset universes (storage techs, thermal plants, VRE plants).
- Raises:
ValueError – If an asset id in
outaged_assetsdoes not match the designed system, or ifrecovery_hoursis a dict missing techs present in the designed system.NotImplementedError – If a must-run component is given a per-asset iterable.
- resolve_recovery_hours(designed_system) dict[str, int][source]
Return
{tech: recovery_hours}for every storage tech.- Raises:
ValueError – If
recovery_hoursis a dict that does not cover every storage tech indesigned_system.
- resolve_duration(component: str, asset_id: str) int[source]
Return outage duration (hours) for a given (component, asset_id).
- resolve_derating(component: str, asset_id: str) float[source]
Return the derating multiplier
rhofor(component, asset_id).Returns
1.0when the asset is not selected for outage. Returns the user-provided factor (or the default0.0) when it is.
- resolve_outaged_asset_ids(component: str, designed_system) list[str][source]
Return the concrete list of outaged asset ids for
component.For must-run components and
importsthe returned list is the canonical universe (["all"]and["grid"]respectively).
- resolve_min_soc_recovery(baseline_results, designed_system, *, recovery_end_hour: dict[str, int]) dict[str, float][source]
Return
{tech: SOC_target_fraction}at the end of recovery.If
min_soc_recoveryisNone, fractions default toSOC_baseline[tech, recovery_end_hour[tech]] / Cap_E[tech]. Per-tech entries inmin_soc_recoveryoverride the baseline default.- Parameters:
baseline_results (BaselineDispatchResults) – Provides
soc_trajectory(DataFrame indexed by hour with one column per tech, MWh).designed_system (DesignedSystem) – Provides
storage_caps[tech]["Cap_E"].recovery_end_hour (dict) – Mapping
{tech: hour_of_year}of the recovery end-hour (already clipped to the baseline horizon).
- sdom.resiliency.VALID_COMPONENTS
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.
If the argument is a tuple, the return value is the same object.
- sdom.resiliency.MUST_RUN_COMPONENTS
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.
If the argument is a tuple, the return value is the same object.
Baseline Dispatch (Problem B)
- sdom.resiliency.build_baseline_dispatch(designed_system, *, n_hours=8760, min_soc_per_tech=None, curtailment_penalty=0.0, add_demand_charges=True, model_name='SDOM_BaselineDispatch', profile=False)[source]
Build the fixed-capacity annual baseline dispatch model.
The model is built by calling the planning-model constructor
sdom.optimization_main._initialize_model_copperplate()ondesigned_system.cem_dataand then:Fixing every capacity decision variable (
model.thermal.plant_installed_capacity,model.storage.{Pcha,Pdis,Ecap},model.{pv,wind}.capacity_fraction) to the values stored indesigned_system.Applying an optional operational SOC floor.
Layering monthly fixed and variable demand charges on top of the existing
model.imports.variableviaadd_demand_charges_to_existing_imports().Replacing the CEM planning objective with an operational-only objective (variable thermal + variable storage + imports/exports + demand charges + optional curtailment penalty).
- Parameters:
designed_system (DesignedSystem) – Output of
sdom.resiliency.load_designed_system(). Must carry a populatedcem_dataattribute.n_hours (int, optional) – Number of hours to simulate. Default
8760.min_soc_per_tech (dict, optional) –
{tech: fraction in [0, 1]}enforcing a SOC floor as a fraction of energy capacity. Missing techs default to whatever is stored ondesigned_system.storage_caps[s]['soc_min_frac'](or 0).curtailment_penalty (float, optional) – Per-MWh penalty applied to total VRE curtailment. Default
0.0.add_demand_charges (bool, optional) – When
True(default) anddesigned_systemcarriesphi_fix_t,phi_var_tandmonth_of_hour, monthly demand charges are added to the objective.model_name (str, optional) – Pyomo model name. Default
"SDOM_BaselineDispatch".profile (bool, optional) – Instrument every build step with a
ModelInitProfilerand print a summary. DefaultFalse.
- Return type:
pyomo.environ.ConcreteModel
- Raises:
TypeError – If
designed_systemis not aDesignedSystem.ValueError – If
n_hoursis not positive or ifdesigned_system.cem_datais missing.
- sdom.resiliency.run_baseline_dispatch(model, *, solver='highs', solver_options=None, tee=False, profile=False)[source]
Solve the baseline dispatch and collect per-hour trajectories.
- Parameters:
model (pyomo.environ.ConcreteModel) – Model returned by
build_baseline_dispatch().solver (str, optional) – Pyomo solver name.
"highs"first triesappsi_highsthen falls back tohighs. Default"highs".solver_options (dict, optional) – Extra options passed to
solver.solve(..., options=...).tee (bool, optional) – Stream solver output to the console. Default
False.profile (bool, optional) – Instrument solve + extraction with a profiler. Default
False.
- Returns:
Hourly dispatch trajectories plus solver metadata.
- Return type:
- Raises:
AttributeError – If
modelwas not produced bybuild_baseline_dispatch().
- class sdom.resiliency.BaselineDispatchResults(soc_trajectory: ~pandas.core.frame.DataFrame | None = None, pcha_trajectory: ~pandas.core.frame.DataFrame | None = None, pdis_trajectory: ~pandas.core.frame.DataFrame | None = None, pthermal_trajectory: ~pandas.core.frame.DataFrame | None = None, psolar_trajectory: ~pandas.core.frame.DataFrame | None = None, pwind_trajectory: ~pandas.core.frame.DataFrame | None = None, pimp: ~pandas.core.series.Series | None = None, pexp: ~pandas.core.series.Series | None = None, nuclear: ~pandas.core.series.Series | None = None, hydro: ~pandas.core.series.Series | None = None, other_renewables: ~pandas.core.series.Series | None = None, load: ~pandas.core.series.Series | None = None, month_of_hour: ~pandas.core.series.Series | None = None, objective_value: float | None = None, solver_status: str | None = None, metadata: dict[str, ~typing.Any] = <factory>, cost_breakdown: dict[str, float] = <factory>)[source]
Trajectories and metadata produced by
run_baseline_dispatch().- Parameters:
soc_trajectory (pandas.DataFrame) – Hourly state-of-charge per storage technology, indexed by hour and with one column per tech (MWh).
pcha_trajectory (pandas.DataFrame) – Hourly charge / discharge per storage tech (MW).
pdis_trajectory (pandas.DataFrame) – Hourly charge / discharge per storage tech (MW).
pthermal_trajectory (pandas.DataFrame) – Hourly thermal dispatch per balancing-unit Plant_id (MW). Empty
DataFramewhen no thermal units survive the snapshot filter.psolar_trajectory (pandas.DataFrame) – Hourly dispatched solar / wind power per plant id (MW).
pwind_trajectory (pandas.DataFrame) – Hourly dispatched solar / wind power per plant id (MW).
pimp (pandas.Series) – Hourly imports / exports (MW).
pexp (pandas.Series) – Hourly imports / exports (MW).
nuclear (pandas.Series) – Hourly time-series parameters echoed from the input system (MW).
hydro (pandas.Series) – Hourly time-series parameters echoed from the input system (MW).
other_renewables (pandas.Series) – Hourly time-series parameters echoed from the input system (MW).
load (pandas.Series) – Hourly time-series parameters echoed from the input system (MW).
month_of_hour (pandas.Series) – Hour -> month mapping used by the demand-charge billing.
objective_value (float) – Operational objective value (USD).
solver_status (str) – Solver termination condition (e.g.
"optimal").metadata (dict, optional) – Free-form solver / run metadata.
cost_breakdown (dict, optional) – Per-component USD totals reconciling to
objective_value. Keys:thermal_var_USD,storage_var_USD,imports_USD,exports_USD(positive; objective contribution is-exports_USD),demand_charges_USD,curtailment_USD,fom_USD,total_USD. Empty dict when the model carries no component metadata.
- __init__(soc_trajectory: ~pandas.core.frame.DataFrame | None = None, pcha_trajectory: ~pandas.core.frame.DataFrame | None = None, pdis_trajectory: ~pandas.core.frame.DataFrame | None = None, pthermal_trajectory: ~pandas.core.frame.DataFrame | None = None, psolar_trajectory: ~pandas.core.frame.DataFrame | None = None, pwind_trajectory: ~pandas.core.frame.DataFrame | None = None, pimp: ~pandas.core.series.Series | None = None, pexp: ~pandas.core.series.Series | None = None, nuclear: ~pandas.core.series.Series | None = None, hydro: ~pandas.core.series.Series | None = None, other_renewables: ~pandas.core.series.Series | None = None, load: ~pandas.core.series.Series | None = None, month_of_hour: ~pandas.core.series.Series | None = None, objective_value: float | None = None, solver_status: str | None = None, metadata: dict[str, ~typing.Any] = <factory>, cost_breakdown: dict[str, float] = <factory>) None
Outage Dispatch (Problem O)
- sdom.resiliency.build_outage_dispatch(baseline_results, *, start_hour, outage_spec, designed_system=None, slack_penalty=10000.0, curtailment_penalty=0.0, soc_slack_penalty=1000.0, min_soc_per_tech=None, n_hours=8760, critical_load_MW=None, model_name='SDOM_OutageDispatch', profile=False)[source]
Build the per-hour outage economic-dispatch Pyomo LP anchored at
start_hour.- Parameters:
baseline_results (BaselineDispatchResults) – Output of
sdom.resiliency.run_baseline_dispatch(). Used to seed the initial SOC and (optionally) the recovery SOC target. May carry the originatingDesignedSystemunderbaseline_results.metadata["designed_system"].start_hour (int) – Anchor hour
h. The outage horizon is[h, h + duration + max_recovery - 1], clipped to[1, n_hours].outage_spec (OutageSpec) – Outage / de-rating specification.
designed_system (DesignedSystem, optional) – Source of truth for capacities and time series. If
None, taken frombaseline_results.metadata["designed_system"].slack_penalty (float, optional) – Penalty \(\pi^{slack}\) (USD/MWh) on unserved-energy slack. Default
10_000.0.curtailment_penalty (float, optional) – Penalty on curtailed VRE energy (USD/MWh). Default
0.0.soc_slack_penalty (float, optional) – Penalty \(\pi^{soc}\) (USD/MWh) on the per-storage-tech slack variable that relaxes the SOC recovery-target constraint (see notes). Default
1_000.0. The operational SOC floor remains a hard bound; only the end-of-recovery target is relaxed.min_soc_per_tech (dict, optional) – Operational SOC floor per storage tech (fraction of
Cap_E). Same semantics asbuild_baseline_dispatch().n_hours (int, optional) – Length of the baseline horizon used for end-of-year clipping. Default
8760.critical_load_MW (float, optional) – Constant critical load (MW) used in place of
designed_system.load[t]for every hourtin the outage sub-horizon[start_hour, start_hour + duration_hours - 1](clipped to the LP end hour). Recovery-window hours continue to use the originalD_t.None(default) preserves the original behaviour: the hourly load series is used everywhere. Must be non-negative.model_name (str, optional) – Pyomo model name. Default
"SDOM_OutageDispatch".profile (bool, optional) – When
True, instrument the build stages with aModelInitProfiler, attach it asmodel.profilerand print a summary table. DefaultFalse. Note: enabling this from inside a parallelProcessPoolExecutorwill produce one summary per worker on each spawned process, which is rarely useful. Preferprofileonly in serial runs.
- Returns:
A Pyomo LP exposing
model.h(hour set),model.u(slack,NonNegativeReals), the standard dispatch sub-blocks (storage,thermal,solar,wind,imports,exports) without demand-charge variables,model.fom_cost_expr(constant fixed-O&M cost prorated to the outage horizon), and an objective that minimises operational cost plus slack and curtailment penalties plus the prorated FOM constant.- Return type:
pyomo.environ.ConcreteModel
- Raises:
ValueError – If
designed_systemis not provided and cannot be recovered frombaseline_results.metadata, or if validation ofoutage_specagainstdesigned_systemfails.TypeError – If
baseline_resultsordesigned_systemis the wrong type.
Notes
Initial SOC is seeded by setting the mutable parameter
model.storage.SOC_init[s]frombaseline_results.soc_trajectoryatstart_hour.SOC_initrepresents the SOC at the start of the outage horizon (i.e., the boundary value \(SOC_{s,h-1}\) that the dynamics equation at \(t = h\) reads as its prior state). The SOC dynamics constraint therefore covers every hour in \(\mathcal{T}^{out}_h\), including the anchor hourstart_hour, so that the charge and discharge variables at the anchor hour appear in a balance equation. Earlier versions fixedSOC[s, start_hour]viapyomo.environ.Var.fix()and skipped the dynamics equation at the anchor; under that formulationPcha[s, start_hour]andPdis[s, start_hour]for surviving (non-outaged) storage techs were unconstrained by any SOC balance.When
critical_load_MWis provided, the load parameter is overridden only over the outage sub-horizon \(\mathcal{T}^{out}_h = \{h, \ldots, h + \Delta^{out} - 1\}\); hours in the recovery sub-horizon retain the originalD_tso that storage replenishment toward the end-of-recovery target reflects realistic post-outage operations.
Imports Formulation with Demand Charges
The opt-in ImportsWithDemandChargesFormulation block builder used by the
baseline LP. Pure linear program; not registered in
io_manager.py.
- sdom.resiliency.add_imports_with_demand_charges(model, *, import_cap: Series, import_price: Series, phi_fix_t: Series, phi_var_t: Series, month_of_hour: Series, block_name: str = 'imports')[source]
Attach the
ImportsWithDemandChargesFormulationblock tomodel.The block adds hourly import variables
Pimp[t], monthly fixed and variable demand-charge variablesD_fix[m]/D_var[m], the capacity bound, the demand-charge linking inequalities, and an additive cost expressiontotal_cost_exprsuitable for inclusion in any objective.- Parameters:
model (pyomo.environ.ConcreteModel) – Host model. Must already define
model.h(a PyomoSetof hour indices).import_cap (pandas.Series) – Hourly import capacity \(\overline{P}^{imp}_{t}\) (MW), indexed by hour matching
model.h.import_price (pandas.Series) – Hourly import energy price \(c^{imp}_{t}\) (USD/MWh).
phi_fix_t (pandas.Series) – Hourly fixed demand-charge tariff \(\phi^{fix}_{t}\) (USD/MW). Must be constant within each calendar month.
phi_var_t (pandas.Series) – Hourly variable (time-of-use) demand-charge tariff \(\phi^{var}_{t}\) (USD/MW).
month_of_hour (pandas.Series) – Mapping hour -> month integer.
block_name (str, optional) – Name of the sub-block attached to
model. Default"imports".
- Returns:
The block that was attached to
model.- Return type:
pyomo.environ.Block
- Raises:
AttributeError – If
model.his not present.
Notes
The contributed cost is
\[Z_{imp,dc} = \sum_t c^{imp}_t\, p^{imp}_t + \sum_m \left( D^{fix}_m + D^{var}_m \right),\]with linking constraints \(D^{k}_{m} \ge \phi^{k}_{t}\, p^{imp}_{t}\) for all \(t \in \mathcal{T}_m\) and \(k \in \{fix, var\}\).
Examples
>>> import pyomo.environ as pyo >>> import pandas as pd >>> m = pyo.ConcreteModel() >>> m.h = pyo.RangeSet(1, 24) >>> idx = range(1, 25) >>> add_imports_with_demand_charges( ... m, ... import_cap=pd.Series(100.0, index=idx), ... import_price=pd.Series(1.0, index=idx), ... phi_fix_t=pd.Series(50.0, index=idx), ... phi_var_t=pd.Series(2.0, index=idx), ... month_of_hour=pd.Series(1, index=idx), ... )
Parallel Runner
- sdom.resiliency.run_resiliency_evaluation(baseline_results, *, outage_spec, designed_system=None, hours=None, slack_penalty=10000.0, curtailment_penalty=0.0, soc_slack_penalty=1000.0, min_soc_per_tech=None, n_hours=8760, n_workers=None, solver='highs', solver_options=None, critical_load_MW=None, profile_outages=False)[source]
Run the per-hour outage evaluation in parallel and aggregate metrics.
For every anchor hour
hinhoursthe runner builds the short-horizon outage LP viabuild_outage_dispatch(), solves it with HiGHS, and recordsEUE,USE_hours,max_unserved_MWand the objective value. Per-hour problems are independent and are solved in parallel viaconcurrent.futures.ProcessPoolExecutorwhenn_workers > 1;n_workers == 1(or a single-hour run) falls back to an in-process serial loop.- Parameters:
baseline_results (BaselineDispatchResults) – Output of
run_baseline_dispatch(). Must carry an SOC trajectory; if itsmetadatadoes not contain a"designed_system"entry,designed_systemmust be supplied.outage_spec (OutageSpec) – Outage / de-rating specification, broadcast to every anchor hour.
designed_system (DesignedSystem, optional) – Source of truth for capacities and time series. Takes precedence over
baseline_results.metadata['designed_system']when both are provided.hours (iterable of int, optional) – Anchor hours to evaluate.
None(default) evaluates every hour1..n_hours. Duplicates are removed; the result is sorted.slack_penalty (float, optional) – Penalty (USD/MWh) applied to slack
u[t]. Default10_000.curtailment_penalty (float, optional) – Penalty applied to curtailed VRE energy (USD/MWh). Default
0.soc_slack_penalty (float, optional) – Penalty (USD/MWh) applied to the per-tech SOC recovery-target slack variable in the outage LP. Forwarded to
build_outage_dispatch(). Default1_000.min_soc_per_tech (dict, optional) – Operational SOC floor per storage tech (fraction of
Cap_E); forwarded to the outage builder.n_hours (int, optional) – Length of the baseline horizon used for end-of-year clipping. Default
8760.n_workers (int, optional) – Worker pool size.
None->max(1, os.cpu_count() - 1);1forces serial mode. Always clamped tolen(hours).solver (str, optional) – Pyomo solver name.
"highs"first triesappsi_highs. Default"highs".solver_options (dict, optional) – Solver options forwarded to
solver.solve(..., options=...).critical_load_MW (float, optional) – Constant critical load (MW) used in place of the hourly load series during the outage sub-horizon of each per-hour LP. Forwarded to
build_outage_dispatch().None(default) preserves the original behaviour. Must be non-negative.profile_outages (bool, optional) – When
Trueandn_workers == 1, profile every per-hour outage build viaModelInitProfiler. Ignored (with a warning) whenn_workers > 1because each worker would emit its own summary on a separate process. DefaultFalse.
- Returns:
Per-hour records (sorted by
start_hour) plus run metadata includingn_workers_usedand a reference tooutage_spec.- Return type:
- Raises:
ValueError – If
designed_systemcannot be resolved from arguments or metadata, or ifn_workers < 1.TypeError – If
baseline_resultsis not aBaselineDispatchResultsinstance, oroutage_specis not anOutageSpec.
Notes
Failure isolation: if any worker raises an exception, that hour’s record is marked
solver_status="error"and itserror_messagefield carries the formatted traceback. Other hours continue normally.
Results Container
- class sdom.resiliency.ResiliencyResults(per_hour: ~pandas.core.frame.DataFrame, metadata: dict[str, ~typing.Any] = <factory>)[source]
Per-hour outage outcomes (lightweight, Phase 5).
Aggregate metrics (LOLP, LOLE, percentiles) and plotting are added in Phase 6.
- Parameters:
per_hour (pandas.DataFrame) – Indexed by
hour(anchorstart_hour). Columns include["EUE", "USE_hours", "max_unserved_MW", "objective_value", "solver_status", "solve_time_s", "truncated", "error_message"].metadata (dict) – Free-form run metadata. Conventionally includes
{"n_workers_used", "outage_spec", "n_hours", "solver"}.
- per_hour: DataFrame
- to_dataframe() DataFrame[source]
Return
per_hourwith the index promoted to ahourcolumn.- Returns:
A copy of
per_hourwithhouras a regular column, sorted byhour.- Return type:
- metrics(*, level: str = 'aggregate')[source]
Aggregate or per-hour resiliency metrics.
- Parameters:
level ({"aggregate", "per_hour"}, optional) –
"aggregate"(default) returns adictof scalar metrics computed over the evaluated hours (errored hours excluded)."per_hour"returns a copy ofper_hourwithhourpromoted to a column.- Return type:
- Raises:
ValueError – If
levelis not one of the supported values.
Notes
Aggregate metrics exclude rows with
solver_status == "error"; the count of excluded rows is reported asn_errors.
- lole() float[source]
Return the loss-of-load expectation (mean USE hours per scenario).
- Return type:
- eue(*, p: float | None = None) float[source]
Return the mean EUE or an empirical percentile of EUE.
- Parameters:
p (float, optional) – Quantile in
(0, 1). DefaultNonereturns the mean EUE.- Return type:
- Raises:
ValueError – If
pis provided and not in(0, 1).
- save(path: str | Path | None = None) Path[source]
Persist per-hour records and aggregate metrics to disk.
- Parameters:
path (str or pathlib.Path, optional) – Output directory. Default:
./results_resiliency/relative to the current working directory. The directory is created if it does not exist.- Returns:
The directory the artifacts were written to.
- Return type:
- Raises:
ImportError – If no Parquet engine (
pyarroworfastparquet) is available.
Notes
Writes two files to
path:per_hour.parquet- the per-hour DataFrame.summary.json- aggregate metrics + JSON-safe metadata.
- classmethod load(path: str | Path) ResiliencyResults[source]
Load a previously-saved
ResiliencyResultsfrompath.- Parameters:
path (str or pathlib.Path) – Directory that previously received
save().- Return type:
- Raises:
FileNotFoundError – If
per_hour.parquetorsummary.jsonis missing.
Plotting
- sdom.resiliency.plot_metric_distribution(results: ResiliencyResults, *, metric: str = 'EUE', kind: str = 'hist', ax: Axes | None = None, **plot_kwargs: Any) Axes[source]
Plot the empirical distribution of a per-hour metric.
- Parameters:
results (ResiliencyResults) – Container produced by
sdom.resiliency.run_resiliency_evaluation().metric (str, optional) – Numeric column of
results.per_hourto plot. Default"EUE".kind ({"hist", "ecdf", "exceedance"}, optional) –
Plot style. Default
"hist"."hist"- histogram of metric values."ecdf"- empirical CDF, monotonically non-decreasing in[0, 1]."exceedance"- exceedance curve1 - ECDF, monotonically non-increasing.
ax (matplotlib.axes.Axes, optional) – Existing axes to draw on. A new figure/axes is created when
None.**plot_kwargs – Forwarded to the underlying matplotlib call (
ax.histforkind="hist";ax.plototherwise).
- Return type:
matplotlib.axes.Axes
- Raises:
ImportError – If matplotlib is not importable.
ValueError – If
kindis not a supported value ormetricis not a numeric column ofresults.per_hour.