Sensitivity Analysis¶
Module: esfex.sensitivity
Submodules: esfex.sensitivity.engine, esfex.sensitivity.lp_parser, esfex.sensitivity.worker
Requires: SALib and scipy, both included in the core pip install esfex.
Overview¶
Sobol global sensitivity analysis [11], [12] (via SALib [19]) varies all parameters simultaneously and decomposes output variance into contributions from each parameter and their interactions.
Two analysis modes:
| Mode | Speed | Fidelity | Use Case |
|---|---|---|---|
LP-level ("lp") |
Fast (seconds per eval) | Approximate (linear model) | Screening, quick parameter importance ranking |
Config-level ("config") |
Slow (minutes per eval) | Exact (full nonlinear model) | Final analysis, publication-quality results |
SensitivityEngine¶
Orchestrates Sobol sensitivity analysis using SALib.
class SensitivityEngine:
def __init__(
self,
mode: str,
parameters: list[SensitivityParameter],
kpi_names: list[str] | None = None,
n_base_samples: int = 128,
)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
mode |
str |
required | Analysis mode: "lp" for LP-level or "config" for config-level. |
parameters |
list[SensitivityParameter] |
required | List of parameters to vary. |
kpi_names |
list[str] or None |
None |
KPI names to evaluate. If None, uses the default set: ["total_cost", "inv_gen_total", "inv_bat_total", "curtailment", "load_shedding"]. |
n_base_samples |
int |
128 |
Sobol N parameter. Total evaluations = N * (2D + 2) where D = number of parameters. |
Properties:
| Property | Type | Description |
|---|---|---|
problem |
dict |
SALib problem definition with num_vars, names, and bounds. |
n_evaluations |
int |
Total number of model evaluations required: N * (2D + 2). |
generate_samples¶
Generate Saltelli sample matrix using SALib.sample.saltelli.
Returns: Array of shape (N*(2D+2), D) where each row is a parameter combination.
run_lp_analysis¶
def run_lp_analysis(
self,
lp_path: str,
progress_callback: Callable[[int, int, str], None] | None = None,
) -> SobolResult
Run LP-level Sobol analysis. Parses the LP file once, then perturbs objective coefficients and/or RHS values for each sample, re-solving with scipy's HiGHS backend.
Parameters:
| Parameter | Type | Description |
|---|---|---|
lp_path |
str |
Path to a .lp file exported from JuMP (e.g., via MasterProblemAdapter.write_lp()). |
progress_callback |
callable or None |
Optional callback (current, total, message) for progress tracking. |
Returns: SobolResult with first-order (S1) and total-effect (ST) indices.
How it works:
- Parse the LP file into an
LPModel(variables, constraints, objective). - Auto-detect parameter groups from variable naming patterns (e.g.,
gen_inv_y*_g1_*->"inv_gen_1"). - For each Saltelli sample, multiply the relevant objective coefficients or RHS values by the sample multiplier.
- Re-solve the perturbed LP using
scipy.optimize.linprogwith HiGHS. - Extract KPIs from the solution.
- Compute Sobol indices using
SALib.analyze.sobol.
run_config_analysis¶
def run_config_analysis(
self,
base_config_path: str,
output_dir: str,
progress_callback: Callable[[int, int, str], None] | None = None,
) -> SobolResult
Run config-level Sobol analysis. Creates modified YAML configs and runs full ESFEX simulations per sample.
Parameters:
| Parameter | Type | Description |
|---|---|---|
base_config_path |
str |
Path to the base YAML configuration file. |
output_dir |
str |
Output directory for intermediate results and temp configs. |
progress_callback |
callable or None |
Optional progress callback. |
Returns: SobolResult with Sobol indices.
How it works:
- Load the base YAML config.
- For each Saltelli sample, apply parameter multipliers to create a modified config (investment costs, fuel costs, demand growth, etc.).
- Write the modified config to a temporary YAML file.
- Run a full ESFEX simulation via subprocess:
python -m esfex.cli run --config temp.yaml --output run_dir. - Extract KPIs from the HDF5 results.
- Compute Sobol indices.
Config-level parameter application:
_apply_config_multipliers() applies multipliers to each system's configuration:
- invest_cost_renewables: Multiplies invest_cost for solar/wind/PV generators.
- invest_cost_conventional: Multiplies invest_cost for non-renewable generators.
- invest_cost_storage: Multiplies invest_cost_power and invest_cost_capacity for batteries.
- invest_cost_transmission: Multiplies transmission investment costs.
- fuel_cost: Multiplies fuel_cost for all generators.
- maintenance_cost: Multiplies maintenance costs.
- demand_growth: Multiplies the growth_rate in demand configuration.
- fuel_price_growth: Multiplies fuel price escalation rates.
- carbon_price: Multiplies carbon pricing parameters.
SensitivityParameter¶
Defines a single parameter to vary in the analysis.
@dataclass
class SensitivityParameter:
name: str # Display name
key: str # Internal key for matching
lower_bound: float # Lower multiplier bound (default 0.5)
upper_bound: float # Upper multiplier bound (default 2.0)
category: str # "objective", "rhs", or "config"
Categories:
| Category | Used In | Description |
|---|---|---|
"objective" |
LP mode | Multiplier applied to objective coefficients in the LP model. |
"rhs" |
LP mode | Multiplier applied to constraint right-hand-side values. |
"config" |
Config mode | Multiplier applied to YAML config parameters. |
Example:
from esfex.sensitivity.engine import SensitivityParameter
params = [
SensitivityParameter(
name="Solar Investment Cost",
key="invest_cost_renewables",
lower_bound=0.5,
upper_bound=2.0,
category="config",
),
SensitivityParameter(
name="Diesel Fuel Cost",
key="fuel_cost",
lower_bound=0.5,
upper_bound=3.0,
category="config",
),
]
SobolResult¶
Results from a Sobol analysis run.
@dataclass
class SobolResult:
parameters: list[str] # Parameter names
kpi_names: list[str] # KPI names
S1: dict[str, np.ndarray] # First-order indices {kpi: array}
ST: dict[str, np.ndarray] # Total-effect indices {kpi: array}
S1_conf: dict[str, np.ndarray] # S1 confidence intervals
ST_conf: dict[str, np.ndarray] # ST confidence intervals
n_samples: int # Base Sobol N
n_evaluations: int # Total evaluations performed
Sobol Index Interpretation¶
First-Order Index (S1):
The fraction of output variance explained by varying this parameter alone:
| S1 Value | Interpretation |
|---|---|
| 0.0 - 0.05 | Negligible direct effect |
| 0.05 - 0.20 | Minor influence |
| 0.20 - 0.50 | Significant influence |
| 0.50 - 1.00 | Dominant parameter |
Total-Order Index (ST):
The fraction of output variance explained by this parameter including all interactions:
- ST ~ S1: The parameter acts independently (no interactions).
- ST >> S1: Significant interaction effects with other parameters.
- Sum of all ST > 1: Indicates strong parameter interactions.
to_csv¶
Export Sobol indices to a CSV file with columns: KPI, Parameter, S1, S1_conf, ST, ST_conf.
Available KPIs¶
| KPI Name | Description | Units |
|---|---|---|
total_cost |
NPV of total system cost (investment + operational + penalties) | $ |
inv_gen_total |
Total generation investment (MW capacity installed) | MW |
inv_bat_total |
Total storage investment (MW power capacity) | MW |
curtailment |
Total RE curtailment | MWh |
load_shedding |
Total unserved energy | MWh |
Predefined Config Parameters¶
Returns SensitivityParameter objects for:
| Key | Display Name | Bounds |
|---|---|---|
invest_cost_renewables |
RE Investment Cost | 0.5 - 2.0 |
invest_cost_storage |
Storage Investment Cost | 0.5 - 2.0 |
invest_cost_conventional |
Conv. Investment Cost | 0.5 - 2.0 |
invest_cost_transmission |
Transmission Inv. Cost | 0.5 - 2.0 |
fuel_cost |
Fuel Cost | 0.5 - 3.0 |
maintenance_cost |
Maintenance Cost | 0.5 - 2.0 |
demand_growth |
Demand Growth | 0.8 - 1.5 |
fuel_price_growth |
Fuel Price Growth | 0.5 - 2.0 |
carbon_price |
Carbon Price | 0.0 - 3.0 |
LP-Level Auto-Detection¶
Parameters can be auto-detected from the LP file:
from esfex.sensitivity.engine import get_lp_parameters
params = get_lp_parameters("master_problem.lp")
Returns SensitivityParameter objects for each detected group:
Objective coefficient groups (from variable name patterns):
| Pattern | Group Name | Description |
|---|---|---|
gen_inv_y*_gN_* |
inv_gen_N |
Generator N investment cost |
bat_pow_inv_y*_biN_* |
inv_bat_pow_N |
Battery N power investment |
bat_cap_inv_y*_biN_* |
inv_bat_cap_N |
Battery N capacity investment |
op_gen_y*_d*_N,* |
op_gen_N |
Operational cost generator N |
op_ll_* |
op_load_shedding |
Load shedding penalty |
op_fre_loss_* |
op_fre_penalty |
Frequency penalty |
RHS groups (from constraint name patterns):
| Pattern | Group Name | Description |
|---|---|---|
demand_bal_*, power_balance_* |
demand |
Demand balance RHS |
re_target_*, re_ratio_* |
re_target |
RE target RHS |
co2_*, carbon_* |
co2_budget |
CO2 budget RHS |
budget_* |
cost_budget |
Budget constraint RHS |
LPModel (LP Parser)¶
Parses CPLEX LP format files (exported by JuMP) into scipy-compatible structures for fast re-solving.
from esfex.sensitivity.lp_parser import parse_lp_file, solve_lp, perturb_and_solve
model = parse_lp_file("master_problem.lp")
print(f"Variables: {model.n_vars}, Constraints: {model.n_constraints}")
# Solve baseline
obj_val, solution = solve_lp(model)
# Perturb and re-solve
kpis = perturb_and_solve(
model,
obj_multipliers={"inv_gen_0": 1.5, "op_gen_0": 0.8},
rhs_multipliers={"demand": 1.1},
)
Reads CPLEX LP format sections (minimize/maximize, subject to, bounds, end) and builds sparse scipy matrices for efficient re-solving. Each perturbation creates modified coefficient/RHS arrays without re-parsing.
SensitivityWorker (GUI Integration)¶
Background QThread worker for running sensitivity analysis without blocking the GUI.
from esfex.sensitivity.worker import SensitivityWorker
worker = SensitivityWorker(engine, lp_path="model.lp")
worker.progressChanged.connect(on_progress) # (current, total, message)
worker.resultReady.connect(on_result) # SobolResult
worker.errorOccurred.connect(on_error) # error message
worker.start()
# To cancel:
worker.cancel()
Signals:
| Signal | Arguments | Description |
|---|---|---|
progressChanged |
(int, int, str) |
Current eval, total evals, status message. |
resultReady |
(SobolResult,) |
Analysis complete with result. |
errorOccurred |
(str,) |
Error or cancellation message. |
Computational Cost¶
Total model evaluations follow the Sobol sampling formula:
where N = n_base_samples and D = number of parameters.
| Parameters (D) | Samples (N) | Evaluations | Approx. Time (LP mode) | Approx. Time (Config mode, 4 workers) |
|---|---|---|---|---|
| 3 | 128 | 1,024 | ~1 minute | ~2 hours |
| 5 | 256 | 3,072 | ~5 minutes | ~8 hours |
| 9 | 128 | 2,560 | ~4 minutes | ~6 hours |
| 9 | 256 | 5,120 | ~8 minutes | ~13 hours |
Best Practices¶
- Start with LP mode: Use LP-level analysis for initial screening, then switch to config-level for important parameters.
- Start with few parameters: Begin with 3-5 most uncertain parameters.
- Use reasonable bounds: Bounds should reflect plausible uncertainty ranges (e.g., 0.5x to 2x for costs).
- Sufficient samples: Use at least N=128 for reliable first-order indices, N=512+ for convergence.
- Check convergence: Run with increasing N and verify indices stabilize.
- Prioritize results: Parameters with high ST values should be modeled carefully or treated stochastically.
Full Example¶
from esfex.sensitivity.engine import (
SensitivityEngine,
get_config_parameters,
)
# Use predefined config-level parameters
params = get_config_parameters()
# Create engine
engine = SensitivityEngine(
mode="config",
parameters=params,
kpi_names=["total_cost", "curtailment", "load_shedding"],
n_base_samples=64,
)
print(f"Total evaluations needed: {engine.n_evaluations}")
# Run analysis
result = engine.run_config_analysis(
base_config_path="configs/isla_juventud.yaml",
output_dir="results/sensitivity/",
progress_callback=lambda cur, tot, msg: print(f"[{cur}/{tot}] {msg}"),
)
# Export results
result.to_csv("results/sobol_indices.csv")
# Print most influential parameters for total cost
for i, param in enumerate(result.parameters):
print(f" {param}: S1={result.S1['total_cost'][i]:.3f}, "
f"ST={result.ST['total_cost'][i]:.3f}")