Skip to content

Config Schema

Module: esfex.config.schema

Model Hierarchy

ESFEXConfig
 ├── TemporalConfig
 ├── SolverConfig
 ├── N1SecurityConfig
 ├── MasterProblemConfig
 │    └── MGAConfig
 ├── MetaNetworkConfig
 │    └── SystemLinkConfig
 └── systems: dict[str, SystemConfig]
      ├── NodeConfig
      │    └── GeoCoordinate
      ├── BusConfig
      ├── TransmissionLineGeo
      ├── TransformerConfig
      ├── ACDCConverterConfig
      ├── FrequencyConverterConfig
      ├── DevelopmentZoneConfig
      ├── DCPowerFlowConfig
      ├── ACPowerFlowConfig
      ├── GeneratorConfig
      │    └── CostCurveConfig / CostCurveBlock
      ├── BatteryConfig
      ├── TechnologyConfig
      ├── BatteryTechnologyConfig
      ├── ElectrolyzerConfig
      ├── FuelConfig
      ├── PrimaryEnergySourceConfig
      ├── FuelEntryPointConfig
      ├── FuelInfrastructureConfig
      ├── PenaltiesConfig
      ├── CO2BudgetConfig
      ├── CriticalityPenalties
      ├── DemandSectorConfig
      ├── NonElectricDemandConfig
      ├── EVCategoryConfig
      ├── RooftopSolarConfig
      ├── StochasticScenarioConfig
      │    └── ScenarioMultipliers
      └── ConversionTechnologyConfig

ESFEXConfig

Top-level configuration container.

class ESFEXConfig(BaseModel):
    simulation_mode: Literal["development", "unit_commitment"]
    unit_commitment_hours: int = 24
    date_start: str = "01/01/2025 00:00"
    temporal: TemporalConfig
    solver: SolverConfig
    n1_security: N1SecurityConfig
    master_problem: MasterProblemConfig
    enable_primary_energy: bool = True
    meta_network: MetaNetworkConfig
    systems: dict[str, SystemConfig]
    plugins: dict[str, Any] = {}

Key Fields:

Field Type Default Description
simulation_mode Literal["development", "unit_commitment"] (required) "development" = LP capacity expansion + dispatch; "unit_commitment" = MIP dispatch only
date_start str "01/01/2025 00:00" Simulation start date (DD/MM/YYYY HH:MM)
enable_primary_energy bool True Enable fuel supply chain optimization

Properties:

  • primary_system -> SystemConfig: Returns the first system in the meta-network
  • get_system(name: str) -> SystemConfig: Retrieve a system by name

SystemConfig

Per-system configuration with all equipment, constraints, and parameters.

class SystemConfig(BaseModel):
    name: str
    demand_path: Optional[str] = None
    demand_scale: float = 1.0
    target_re_penetration: float = 1.0
    discount_rate: float = 0.05
    max_annual_system_cost: float = 20e9
    ...

Key Fields:

Field Type Default Description
name str (required) System identifier
demand_path Optional[str] None Path to demand data file (Excel/CSV)
demand_scale float 1.0 Demand scaling factor
target_re_penetration float 1.0 Target RE share (0-1)
discount_rate float 0.05 Economic discount rate
max_annual_system_cost float 20e9 Annual investment budget cap ($)
loss_demand_threshold float 0.05 Load shedding threshold fraction
sim_rooftop bool False Enable rooftop solar simulation
reserve_margin float 1.15 Capacity adequacy margin (1.15 = 15%)
soc_end_tolerance float 0.05 Battery end-of-horizon SOC tolerance

Properties:

  • num_nodes -> int: Number of nodes (derived from adjacency matrix)
  • num_buses -> int: Number of buses (0 if none defined)

GeneratorConfig

Generator unit configuration. All per-node fields are lists of length num_nodes.

class GeneratorConfig(BaseModel):
    name: str
    type: Literal["Renewable", "Non-renewable", "Storage", "Electrolyzer"]
    fuel: str
    technology: Optional[str] = None
    reservable: bool = True
    rated_power: list[float]           # MW per node
    min_power: list[float]             # Fraction of rated
    fuel_cost: list[float]             # $/MWh
    invest_cost: list[float]           # $/MW (DEPRECATED: use technologies)
    invest_max_power: list[float]      # MW max (DEPRECATED: use technologies)
    life_time: list[int]               # Years
    initial_age: list[int]             # Years
    ...

Per-Node Array Fields:

Field Type Unit Description
rated_power list[float] MW Installed capacity
min_power list[float] fraction Minimum output as fraction of rated
fuel_cost list[float] $/MWh Fuel cost
fixed_cost list[float] $/MWh Fixed O&M cost
maintenance_cost list[float] $/MWh Maintenance cost
start_up_cost list[float] $ Startup cost per event
invest_cost list[float] $/MW Investment cost (deprecated)
invest_max_power list[float] MW Max investment capacity (deprecated)
eff_at_rated list[float] -- Efficiency at rated power
eff_at_min list[float] -- Efficiency at minimum power
ramp_up list[float] pu/min Ramp-up rate
ramp_down list[float] pu/min Ramp-down rate
min_up list[int] hours Minimum up time
min_down list[int] hours Minimum down time
life_time list[int] years Economic lifetime
initial_age list[int] years Age at simulation start
degradation_rate list[float] %/year Annual capacity degradation
decommissioning_cost list[float] $/MW Decommissioning cost
inertia list[float] s Inertia constant H

Scalar Fields:

Field Type Default Description
frequency_hz float 50.0 Operating frequency (Hz)
current_type Literal["AC","DC","AC_DC"] "AC" Current type
availability_file Optional[str] None Path to availability profile (CSV/Excel)

Reservoir Hydroelectric Fields (optional):

Field Type Description
reservoir_capacity list[float] Reservoir capacity per node (MWh)
reservoir_initial_level list[float] Initial level fraction (0-1)
reservoir_inflow_file Optional[str] Inflow time series file
reservoir_pump_capacity list[float] Pump capacity per node (MW)

BatteryConfig

Battery/storage unit configuration.

class BatteryConfig(BaseModel):
    name: str
    type: Literal["Storage"] = "Storage"
    capacity: list[float]              # MWh per node
    MaxChargePower: list[float]        # MW
    MaxDischargePower: list[float]     # MW
    efficiency_charge: list[float]     # [0,1]
    efficiency_discharge: list[float]
    soc_initial: list[float]           # Fraction
    max_DoD: list[float]              # Max depth of discharge
    ...

Storage-Specific Fields:

Field Type Unit Description
capacity list[float] MWh Energy capacity per node
MaxChargePower list[float] MW Max charge power per node
MaxDischargePower list[float] MW Max discharge power per node
efficiency_charge list[float] -- Charging efficiency (0-1)
efficiency_discharge list[float] -- Discharging efficiency (0-1)
soc_initial list[float] fraction Initial SOC fraction
max_DoD list[float] fraction Maximum depth of discharge
spillage bool -- Allow battery spillage
min_duration_hours Optional[int] hours Min energy-to-power ratio
max_duration_hours Optional[int] hours Max energy-to-power ratio
throughput_degradation_cost Optional[list[float]] $/MWh Cycling wear cost

TechnologyConfig

Candidate technology for new generation investment. Unlike GeneratorConfig (existing units), technologies define what can be built.

class TechnologyConfig(BaseModel):
    name: str
    type: Literal["Renewable", "Non-renewable"]
    fuel: str
    invest_cost: list[float]           # $/MW per node
    invest_max_power: list[float]      # MW per node
    lifetime: int                      # Years
    eff_at_rated: list[float]
    degradation_rate: list[float]
    availability_file: Optional[str] = None
    ...

BatteryTechnologyConfig

Candidate technology for new battery/storage investment.

class BatteryTechnologyConfig(BaseModel):
    name: str
    invest_cost_power: list[float]     # $/MW per node
    invest_cost_energy: list[float]    # $/MWh per node
    invest_max_power: list[float]      # MW per node
    invest_max_capacity: list[float]   # MWh per node
    min_duration_hours: float = 1.0
    max_duration_hours: float = 24.0
    efficiency_charge: list[float]
    efficiency_discharge: list[float]
    lifetime: int
    ...

TemporalConfig

class TemporalConfig(BaseModel):
    resolution_hours: int = 1          # 1 = hourly, 3 = 3-hourly, etc.
    rolling_horizon_hours: int = 48    # Window size
    overlap_hours: int = 6             # Overlap between windows
    use_rolling_horizon: bool = True
    investment_resolution: int = 8760  # Hours per investment period
    primary_energy_resolution: int = 24

SolverConfig

class SolverConfig(BaseModel):
    name: Literal["highs", "cbc", "glpk", "gurobi", "cplex", "scip", "xpress",
                  "clarabel", "scs", "ipopt"] = "highs"
    threads: int = 4
    time_limit: int = 10800            # Seconds (0 = unlimited)
    gap: float = 0.01                  # MIP optimality gap
    verbose: bool = False
    scale_constraints: bool = True
    options: dict[str, Any] = {}       # Solver-specific options

PenaltiesConfig

Penalty costs for constraint violations. All values in $/MW or $/MWh.

class PenaltiesConfig(BaseModel):
    loss_of_load: float = 10e6
    loss_of_reserve_static: float = 100
    loss_of_reserve_dynamic: float = 100
    loss_of_inertia: float = 200
    curtailment: float = 100
    max_curtailment_ratio: float = 0.05    # Constraint-based (5% of RE gen max)
    co2_cost: float = 10                   # $/tCO2
    co2_budget_violation: float = 500      # $/tCO2 over budget
    fre_penetration_loss: float = 100      # $/MWh RE shortfall
    ev_loss: float = 10                    # $/MWh EV demand not met
    soc_violation: float = 1e6             # $/MWh SOC limit violation
    delay_retirement_per_mw: float = 50000 # $/MW delay retirement
    ...

MasterProblemConfig

class MasterProblemConfig(BaseModel):
    stochastic: bool = False
    representative_days: int = 5
    min_day_separation: int = 5
    use_tsam: bool = False              # Enable TSAM clustering
    tsam_num_periods: int = 10
    tsam_method: Literal["kmedoids", "kmeans"] = "kmedoids"
    tsam_inter_period_linking: bool = True
    use_uc_in_dispatch: bool = False    # UC in operational dispatch
    planning_mode: Literal["perfect_foresight", "myopic"] = "perfect_foresight"
    mga: MGAConfig = MGAConfig()

MGAConfig

Configuration for near-optimal alternative generation. Two methods share most fields; the cross-field validator enforces that objectives and method agree (see Validation).

class MGAConfig(BaseModel):
    enabled: bool = False
    method: Literal["mga", "spores"] = "mga"
    objectives: list[SporesObjective] = []      # SPORES only
    num_alternatives: int = 10                  # 1-100, MGA only
    slack_fraction: float = 0.05                # 0-0.5
    investment_threshold: float = 0.1

MGAConfig validation

A @model_validator(mode="after") activates when enabled = True:

Condition Outcome
method = "spores" and objectives empty ValueError (choose at least one objective)
method = "mga" and objectives non-empty ValueError (objectives only valid with SPORES)
enabled = False validator is bypassed (YAML drafts can keep both populated)

SporesObjective

class SporesObjective(str, Enum):
    HSJ_DIVERSITY       = "hsj_diversity"
    MIN_TOTAL_BUILD     = "min_total_build"
    MAX_TECH_EQUITY     = "max_tech_equity"
    MAX_REGIONAL_EQUITY = "max_regional_equity"
    EVOLUTIONARY_DIST   = "evolutionary_dist"

The lowercase snake_case values match the Julia Symbol keys used by apply_spores_objective! in mga.jl (e.g. "min_total_build" \(\leftrightarrow\) :min_total_build) so round-tripping through YAML, the Pydantic schema, and the bridge layer is lossless. See Capacity Expansion §15.10–§15.13 for the LP formulation of each objective.


EVCategoryConfig

class EVCategoryConfig(BaseModel):
    battery_capacity: float             # kWh per vehicle
    charging_power: float               # kW per vehicle
    v2g_power: float                    # kW per vehicle for V2G
    v2g_participation: float            # Fraction (0-1)
    efficiency_charge: float            # 0-1
    efficiency_discharge: float         # 0-1
    min_soc: float                      # 0-1
    max_adoption: float = 35.0          # Growth multiplier
    growth_rate: float = 0.14           # Logistic growth rate
    mid_point_fraction: float = 0.5     # S-curve midpoint position

NodeConfig

class NodeConfig(BaseModel):
    num_nodes: Optional[int] = None     # Auto-inferred from adjacency matrix
    nodes_connections: list[float]      # Flattened NxN adjacency matrix (MW)
    reserve_static: list[float] = []    # Static reserve per node (MW)
    reserve_dynamic: list[float] = []   # Dynamic reserve per node (MW)
    losses: list[float] = []            # Transmission losses per node
    node_coordinates: Optional[list[GeoCoordinate]] = None
    node_names: Optional[list[str]] = None

DCPowerFlowConfig

class DCPowerFlowConfig(BaseModel):
    base_impedance: float = 100.0       # Ohm
    reactance_per_km: float = 0.4       # Ohm/km
    voltage_level_kv: float = 220.0     # kV
    enable_angle_limits: bool = True
    max_angle_diff_deg: float = 30.0    # Degrees (0-90)
    slack_bus: int = 0                  # 0-indexed
    loss_model: Literal["none", "linear", "pwl"] = "pwl"
    pwl_loss_segments: int = 3          # Operational dispatch (1-10)
    pwl_loss_segments_master: int = 2   # Master problem (1-5)

Other Configuration Classes

N1SecurityConfig

class N1SecurityConfig(BaseModel):
    enabled: bool = False
    apply_to_modes: list[str] = ["unit_commitment"]
    transmission_enabled: bool = True
    transmission_reserve_factor: float = 0.70
    generation_enabled: bool = True
    generation_reserve_type: Literal["largest_unit", "percentage"] = "largest_unit"
    generation_reserve_percentage: float = 0.15

MetaNetworkConfig

class MetaNetworkConfig(BaseModel):
    systems: list[str]                  # System names
    systems_links: dict[str, SystemLinkConfig] = {}
    dynamic_transfer_pricing: bool = False

StochasticScenarioConfig

class StochasticScenarioConfig(BaseModel):
    name: str
    probability: float                  # 0-1
    description: str = ""
    multipliers: ScenarioMultipliers = ScenarioMultipliers()

RooftopSolarConfig

class RooftopSolarConfig(BaseModel):
    adoption_scenario: Literal["low", "medium", "high"] = "medium"
    weather_variability: Literal["low", "normal", "high"] = "normal"
    simulation_seed: int = 42
    systems_per_node: list[int]
    avg_system_size: list[float]        # kW per system
    performance_ratio: float = 0.75
    cost_per_kw: float = 1200
    cost_reduction_rate: float = 0.08
    initial_adoption: list[float]
    max_adoption: dict[str, float]

CostCurveConfig

Bidding/offer curve configuration for generators and batteries.

class CostCurveConfig(BaseModel):
    curve_type: Literal["flat", "linear", "stepwise", "exponential"] = "flat"
    flat_price: Optional[float] = None
    blocks: Optional[list[CostCurveBlock]] = None
    price_at_zero: Optional[float] = None
    price_at_max: Optional[float] = None
    base_price: Optional[float] = None
    scale_factor: Optional[float] = None
    num_segments: int = 5

ElectrolyzerConfig

class ElectrolyzerConfig(BaseModel):
    name: str
    type: Literal["Electrolyzer"] = "Electrolyzer"
    technology: Literal["PEM", "Alkaline", "SOE"] = "PEM"
    energy_per_kg_h2: float = 50.0      # kWh/kg H2
    water_cost: float = 0.001           # $/kg H2
    ...

Validation Example

from esfex.config.schema import ESFEXConfig, SystemConfig, GeneratorConfig

# Pydantic validates all fields at construction
try:
    gen = GeneratorConfig(
        name="Solar PV",
        type="Renewable",
        fuel="Sun",
        rated_power=[50.0, 30.0],       # 2 nodes
        min_power=[0.0, 0.0],
        life_time=[25, 25],
        initial_age=[0, 0],
        # ... other required fields
    )
except ValidationError as e:
    print(e)  # Missing required fields are reported

See the Config Reference for a complete listing of all fields with YAML examples.