#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This module defines the PREP-SHOT model. The model is created using
the pyoptinterface library.
"""
from prepshot._model.demand import AddDemandConstraints
from prepshot._model.generation import AddGenerationConstraints
from prepshot._model.cost import AddCostObjective
from prepshot._model.co2 import AddCo2EmissionConstraints
from prepshot._model.hydro import AddHydropowerConstraints
from prepshot._model.storage import AddStorageConstraints
from prepshot._model.transmission import AddTransmissionConstraints
from prepshot._model.investment import AddInvestmentConstraints
from prepshot._model.finance import AddFinanceConstraints
from prepshot._model.reserve import AddReserveConstraints
from prepshot._model.dc_flow import AddDCFlowConstraints
from prepshot._model.unit_commitment import AddUnitCommitmentConstraints
from prepshot._model.heat_rate import AddHeatRateConstraints
from prepshot.logs import timer
from prepshot.solver import get_solver
from prepshot.solver import set_solver_parameters
[文档]def define_model(
params : dict
) -> object:
"""This function creates the model class depending on predefined solver.
Parameters
----------
params : dict
parameters for the model
Returns
-------
object
A pyoptinterface Model object depending on the solver
Raises
------
ValueError
Unsupported or undefined solver
"""
solver = get_solver(params)
model = solver.Model()
model.params = params
set_solver_parameters(model)
return model
[文档]def define_basic_sets(model : object) -> None:
"""Define sets for the model.
Parameters
----------
model : object
Model object to be solved.
"""
params = model.params
basic_sets = ["year", "zone", "tech", "hour", "month"]
for set_name in basic_sets:
setattr(model, set_name, params[set_name])
# hour_p is the hour set augmented with one "previous" hour at the
# front, used as the anchor for storage / reservoir balances:
# storage[h] = storage[h-1] + ... links each hour to the prior one,
# and storage[hour_p[0]] = initial_storage closes the chain.
# For CEM (hour starts at 1) hour_p[0] = 0; for PCM windows
# starting at h_first, hour_p[0] = h_first - 1, so the prior-hour
# anchor lives just outside the window.
hours_list = list(params['hour'])
model.hour_p = [hours_list[0] - 1] + hours_list
# PyPSA-style: a free-form `carrier` string plus per-tech behavior
# flags. Hydropower is a special-cased carrier (carrier == 'hydro')
# so the existing reservoir / water-flow constraints continue to
# apply unchanged. Other behaviors (VRE, storage, must-run) are
# driven by the boolean flag columns rather than a fixed type enum.
techs_df = params['technologies']
def _tech_filter(mask):
return techs_df.loc[mask, 'tech'].tolist()
model.hydro_tech = _tech_filter(techs_df['carrier'] == 'hydro')
model.storage_tech = _tech_filter(techs_df['is_storage'].astype(bool))
# PyPSA-style: variable / must-run / curtailable behaviors are no
# longer flag-driven. Any tech can have a time-varying max/min
# generation profile via tech_max_gen_profile.csv /
# tech_min_gen_profile.csv. The unified per-tech generation bound
# constraint lives in AddGenerationConstraints.
# `dispatchable_tech` = anything that's not hydro and not storage
# (i.e. its generation is bounded by cap × p_max_pu).
special_mask = (
(techs_df['carrier'] == 'hydro')
| techs_df['is_storage'].astype(bool)
)
model.dispatchable_tech = _tech_filter(~special_mask)
model.tech_types = ['dispatchable_tech', 'hydro_tech', 'storage_tech']
if params['isinflow']:
# Hydro plants are first-class techs (carrier='hydro');
# model.station is the list of those tech names for
# backwards-compat with the hydro module.
model.station = model.hydro_tech
[文档]def define_active_zone_tech(model : object) -> None:
"""Pull the sparse ``(zone, tech)`` set from ``model.params`` (built
once at file-read time by ``load_data.compute_active_zone_tech``)
and derive the time-indexed key lists used by the constraint
builders.
The (zone, tech) set itself is data-property (depends only on
``existing_fleet`` + ``expansion_candidates``), so we compute it
in ``load_data`` and reuse it unchanged across every PCM window.
The (h, m, y, z, te) lists DO depend on the per-window time
index sets, so they're materialised here.
"""
from prepshot.load_data import compute_active_zone_tech
if 'active_zt' not in model.params:
# PCM windows go through ``_build_window_params`` which copies
# params and possibly overrides ``existing_fleet`` -- recompute
# the sparse set on the fly so the override is respected.
compute_active_zone_tech(model.params)
model.active_zt = model.params['active_zt']
model.tech_zones = model.params['tech_zones']
model.zone_techs = model.params['zone_techs']
model.active_zt_storage = model.params['active_zt_storage']
model.active_lines = model.params.get('active_lines') or []
model.out_neighbours = model.params.get('out_neighbours') or {}
model.in_neighbours = model.params.get('in_neighbours') or {}
# Time-indexed key lists used by generation.py, heat_rate.py,
# unit_commitment.py, demand.py, etc.
model.active_hmyzte = [
(h, m, y, z, te)
for h in model.hour
for m in model.month
for y in model.year
for (z, te) in model.active_zt
]
model.active_hmyzte_storage = [
(h, m, y, z, te)
for h in model.hour
for m in model.month
for y in model.year
for (z, te) in model.active_zt_storage
]
# (h, m, y, z1, z2) over real lines only -- used by transmission.py
# and demand.py.
model.active_hmyz1z2 = [
(h, m, y, z1, z2)
for h in model.hour
for m in model.month
for y in model.year
for (z1, z2) in model.active_lines
]
model.active_yz1z2 = [
(y, z1, z2)
for y in model.year
for (z1, z2) in model.active_lines
]
[文档]def define_complex_sets(model : object) -> None:
"""Pad ``transmission_line_efficiency`` for the active line set.
A few constraint rules read ``efficiency[z1, z2]`` for every line
in ``active_lines``; if the input CSV omits a line that DOES
appear in ``transmission_existing`` (rare but possible), default
its efficiency to 0. We no longer pad the dense ``zone x zone``
product -- the active-line set is sparse, so padding only
touches the real lines.
"""
# Several line-param dicts only carry the rows explicitly in
# their CSV, but the cost / capacity rules read every active line
# by index. Pad each with 0 (or +inf for capacity_max-style upper
# bounds) on lines missing from the CSV. We pad the *active*
# set, not the dense zone**2 product.
# Default to 1.0 (lossless) when the CSV omits a line that's in
# the active set. The historical 0.0 fallback silently blocks
# those lines -- not what you want if the CSV is just incomplete.
# Users can still set efficiency=0 explicitly to disable a line.
eff = model.params['transmission_line_efficiency']
for (z1, z2) in model.active_lines:
if (z1, z2) not in eff:
eff[z1, z2] = 1.0
# Padding defaults: 0 for cost/distance/susceptance (no penalty,
# no flow contribution from a missing entry), but a large value
# for lifetime so the line is "in service" through the whole
# planning horizon when the CSV is absent. Padding lifetime to 0
# collapses ``cap_lines_existing`` to 0 -- silently blocks every
# line whose lifetime row is missing.
line_param_zero = (
'transmission_line_variable_OM_cost',
'transmission_line_fixed_OM_cost',
'transmission_line_investment_cost',
'distance',
'transmission_line_susceptance',
)
for k in line_param_zero:
d = model.params.get(k)
if d is None:
continue
for (z1, z2) in model.active_lines:
if (z1, z2) not in d:
d[z1, z2] = 0
lt = model.params.get('transmission_line_lifetime')
if lt is not None:
for (z1, z2) in model.active_lines:
if (z1, z2) not in lt:
lt[z1, z2] = 9999
[文档]def define_variables(model : object) -> None:
"""Define variables for the model.
Parameters
----------
model : object
Model to be solved.
"""
from prepshot.utils import sparse_tupledict
model.cap_newtech = model.add_variables(
model.year, model.zone, model.tech, lb=0
)
# Sparsify cap_newline / trans_export over real (z1, z2) lines
# only. Thai PCM goes from 222k dense pairs to ~615 lines; that
# propagates into 6 transmission constraint families and the
# demand power-balance neighbour quicksums.
_new_var = lambda *_: model.add_variable(lb=0)
model.cap_newline = sparse_tupledict(model.active_yz1z2, _new_var)
# Sparsify gen / charge / storage variable creation: only build
# variables for (z, te) pairs that have either existing capacity
# at the zone or a candidate row allowing build there. Inactive
# pairs would just be lb=ub=0 for every (h, m, y) anyway -- on
# full-nodal Thai PCM this drops gen / storage / charge from
# ~5M variables each to ~10k each.
model.gen = sparse_tupledict(model.active_hmyzte, _new_var)
# storage has hour_p anchor (hour - 1) for the cycle close.
active_h_p_myzte = [
(h, m, y, z, te)
for h in model.hour_p
for m in model.month
for y in model.year
for (z, te) in model.active_zt_storage
]
model.storage = sparse_tupledict(active_h_p_myzte, _new_var)
model.charge = sparse_tupledict(model.active_hmyzte_storage, _new_var)
model.trans_export = sparse_tupledict(model.active_hmyz1z2, _new_var)
# Load-not-served slack, one per (h, m, y, z). Created only when
# the user opts in; demand.power_balance_rule and the cost
# objective check ``hasattr(model, 'lns')``.
if model.params.get('allow_load_shedding', False):
model.lns = model.add_variables(
model.hour, model.month, model.year, model.zone, lb=0
)
# Reserve variables are created INSIDE AddReserveConstraints (v1.16)
# because the product set comes from the eligibility CSV at runtime
# rather than being a fixed list here. Skip if reserve is off.
if model.params['isinflow']:
model.genflow = model.add_variables(
model.station, model.hour, model.month, model.year, lb=0
)
model.spillflow = model.add_variables(
model.station, model.hour, model.month, model.year, lb=0
)
model.withdraw = model.add_variables(
model.station, model.hour, model.month, model.year, lb=0
)
model.storage_reservoir = model.add_variables(
model.station, model.hour_p, model.month, model.year, lb=0
)
model.output = model.add_variables(
model.station, model.hour, model.month, model.year, lb=0
)
[文档]def define_constraints(model : object) -> None:
"""Define constraints for the model.
Parameters
----------
model : object
Model to be solved.
"""
AddInvestmentConstraints(model)
AddGenerationConstraints(model)
AddTransmissionConstraints(model)
AddDCFlowConstraints(model)
AddCo2EmissionConstraints(model)
AddStorageConstraints(model)
AddHydropowerConstraints(model)
AddReserveConstraints(model)
AddUnitCommitmentConstraints(model)
AddHeatRateConstraints(model)
AddDemandConstraints(model)
[文档]@timer
def create_model(params : dict) -> object:
"""Create the PREP-SHOT model.
Parameters
----------
params : dict
Dictionary of parameters for the model.
Returns
-------
object
Model object.
"""
model = define_model(params)
define_basic_sets(model)
define_active_zone_tech(model)
define_complex_sets(model)
define_variables(model)
define_constraints(model)
AddCostObjective(model)
# Optional public-debt accounting: only wired up when the user
# supplies a finance dataset (public_debt_ratio + cost-of-capital
# tables). Reads cost_newtech_breakdown built by AddCostObjective.
if params.get('public_debt_ratio'):
AddFinanceConstraints(model)
return model