Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gridpath/system/load_balance/load_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ def meet_load_rule(mod, z, tmp):
m.Meet_Load_Constraint = Constraint(m.LOAD_ZONES, m.TMPS, rule=meet_load_rule)

def use_limit_constraint_rule(mod, lz):
# No limit specified (defaults to +inf): skip the constraint entirely
# rather than build a row with an infinite (or huge) RHS, which would
# be a free row that only hurts solver scaling.
if mod.unserved_energy_limit_mwh[lz] == float("inf"):
return Constraint.Skip
return (
sum(
mod.Unserved_Energy_MW_Expression[lz, tmp]
Expand All @@ -160,6 +165,9 @@ def use_limit_constraint_rule(mod, lz):
)

def max_unserved_load_limit_constraint_rule(mod, lz, tmp):
# No limit specified (defaults to +inf): skip (see above).
if mod.max_unserved_load_limit_mw[lz] == float("inf"):
return Constraint.Skip
return (
mod.Unserved_Energy_MW_Expression[lz, tmp]
<= mod.max_unserved_load_limit_mw[lz]
Expand Down
70 changes: 66 additions & 4 deletions gridpath/transmission/capacity/capacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
again depend on the line's *capacity_type*.
"""

import math
import os.path
import pandas as pd
from pyomo.environ import Set, Expression, value
Expand Down Expand Up @@ -79,13 +80,28 @@ def add_model_components(
| | :code:`TX_OPR_TMPS` |
| |
| Two-dimensional set of the transmission lines and their operational |
| timepoints, derived from :code:`TX_OPR_PRDS` and the timepoitns in each |
| timepoints, derived from :code:`TX_OPR_PRDS` and the timepoints in each |
| period. |
+-------------------------------------------------------------------------+
| | :code:`TX_LINES_OPR_IN_TMP` |
| | *Defined over*: :code:`TIMEPOINTS` |
| |
| Indexed set of transmission lines operatoinal in each timepoint. |
| Indexed set of transmission lines operational in each timepoint. |
+-------------------------------------------------------------------------+
| | :code:`TX_OPR_PRDS_W_MIN_LIMIT` |
| |
| Subset of :code:`TX_OPR_PRDS` for line-periods that have a lower flow |
| limit. A capacity type may declare a line-period unconstrained (no |
| limit) via :code:`min_limit_is_unconstrained_rule`; capacity types |
| without that method are always constrained (the default). The |
| operational types build their minimum-flow constraints over this |
| subset, so unconstrained line-periods get no such constraint. |
+-------------------------------------------------------------------------+
| | :code:`TX_OPR_PRDS_W_MAX_LIMIT` |
| |
| Subset of :code:`TX_OPR_PRDS` for line-periods that have an upper flow |
| limit (analogous to :code:`TX_OPR_PRDS_W_MIN_LIMIT`, via |
| :code:`max_limit_is_unconstrained_rule`). |
+-------------------------------------------------------------------------+

|
Expand Down Expand Up @@ -192,6 +208,47 @@ def tx_max_capacity_rule(mod, tx, p):

m.Tx_Max_Capacity_MW = Expression(m.TX_OPR_PRDS, rule=tx_max_capacity_rule)

# Sets of line-periods that have a lower / upper flow limit. A capacity
# type may declare a line-period "unconstrained" (no flow limit) by
# defining min_limit_is_unconstrained_rule / max_limit_is_unconstrained_rule
# and returning True; capacity types without those methods are always
# constrained (the default), so no line is ever silently left unbounded.
# The operational types build their min/max flow constraints over these
# subsets, skipping unconstrained line-periods entirely.
def tx_min_limit_is_unconstrained(mod, tx, p):
cap_type = mod.tx_capacity_type[tx]
module = imported_tx_capacity_modules[cap_type]
if hasattr(module, "min_limit_is_unconstrained_rule"):
return module.min_limit_is_unconstrained_rule(mod, tx, p)
return False

def tx_max_limit_is_unconstrained(mod, tx, p):
cap_type = mod.tx_capacity_type[tx]
module = imported_tx_capacity_modules[cap_type]
if hasattr(module, "max_limit_is_unconstrained_rule"):
return module.max_limit_is_unconstrained_rule(mod, tx, p)
return False

m.TX_OPR_PRDS_W_MIN_LIMIT = Set(
dimen=2,
within=m.TX_OPR_PRDS,
initialize=lambda mod: [
(tx, p)
for (tx, p) in mod.TX_OPR_PRDS
if not tx_min_limit_is_unconstrained(mod, tx, p)
],
)

m.TX_OPR_PRDS_W_MAX_LIMIT = Set(
dimen=2,
within=m.TX_OPR_PRDS,
initialize=lambda mod: [
(tx, p)
for (tx, p) in mod.TX_OPR_PRDS
if not tx_max_limit_is_unconstrained(mod, tx, p)
],
)


# Input-Output
###############################################################################
Expand Down Expand Up @@ -224,12 +281,17 @@ def export_results(
"max_mw",
]

# An unconstrained line-period has an infinite capacity; report it as
# NULL rather than the literal "inf" so the results stay numeric.
def _finite_or_none(v):
return None if math.isinf(v) else v

data = [
[
tx_line,
prd,
value(m.Tx_Min_Capacity_MW[tx_line, prd]),
value(m.Tx_Max_Capacity_MW[tx_line, prd]),
_finite_or_none(value(m.Tx_Min_Capacity_MW[tx_line, prd])),
_finite_or_none(value(m.Tx_Max_Capacity_MW[tx_line, prd])),
]
for (tx_line, prd) in m.TX_OPR_PRDS
]
Expand Down
126 changes: 83 additions & 43 deletions gridpath/transmission/capacity/capacity_types/tx_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import csv
import os.path
import pandas as pd
from statistics import mean

from pyomo.environ import Set, Param, Reals, NonNegativeReals
Expand All @@ -42,10 +43,17 @@
write_validation_to_database,
validate_dtypes,
validate_idxs,
validate_missing_inputs,
validate_column_monotonicity,
)

# A specified transmission line whose min (max) capacity is left blank in the
# inputs is treated as having no lower (upper) flow limit. The capacity params
# default to these sentinels, and the operational types skip the corresponding
# flow-limit constraint when the capacity is infinite (see
# min/max_limit_is_unconstrained_rule below).
Negative_Infinity = float("-inf")
Infinity = float("inf")


def add_model_components(
m,
Expand Down Expand Up @@ -116,8 +124,13 @@ def add_model_components(
# Required Params
###########################################################################

m.tx_spec_min_cap_mw = Param(m.TX_SPEC_OPR_PRDS, within=Reals)
m.tx_spec_max_cap_mw = Param(m.TX_SPEC_OPR_PRDS, within=Reals)
# Optional caps: a blank min (max) in the inputs leaves the param at
# -inf (+inf), which the operational type reads as "no lower (upper)
# flow limit" and skips the corresponding constraint.
m.tx_spec_min_cap_mw = Param(
m.TX_SPEC_OPR_PRDS, within=Reals, default=Negative_Infinity
)
m.tx_spec_max_cap_mw = Param(m.TX_SPEC_OPR_PRDS, within=Reals, default=Infinity)
m.tx_spec_fixed_cost_per_mw_yr = Param(
m.TX_SPEC_OPR_PRDS, within=NonNegativeReals, default=0
)
Expand All @@ -140,12 +153,30 @@ def max_transmission_capacity_rule(mod, tx, p):
return mod.tx_spec_max_cap_mw[tx, p]


def min_limit_is_unconstrained_rule(mod, tx, p):
"""Whether this line-period has no lower flow limit (blank min in inputs)."""
return mod.tx_spec_min_cap_mw[tx, p] == Negative_Infinity


def max_limit_is_unconstrained_rule(mod, tx, p):
"""Whether this line-period has no upper flow limit (blank max in inputs)."""
return mod.tx_spec_max_cap_mw[tx, p] == Infinity


def fixed_cost_rule(mod, g, p):
"""
The fixed cost of Tx lines of the *tx_spec* capacity type is a
pre-specified number equal to the average capacity times the per-mw fixed
cost for each of the project's operational periods.

A line with no flow limit (infinite min or max capacity) has no
meaningful capacity to cost, so its fixed cost is zero.
"""
if (
mod.tx_spec_min_cap_mw[g, p] == Negative_Infinity
or mod.tx_spec_max_cap_mw[g, p] == Infinity
):
return 0
return (
mean([abs(mod.tx_spec_min_cap_mw[g, p]), abs(mod.tx_spec_max_cap_mw[g, p])])
* mod.tx_spec_fixed_cost_per_mw_yr[g, p]
Expand All @@ -167,32 +198,50 @@ def load_model_data(
subproblem,
stage,
):
data_portal.load(
filename=os.path.join(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
"inputs",
"specified_transmission_line_capacities.tab",
),
select=(
"transmission_line",
"period",
"specified_tx_min_mw",
"specified_tx_max_mw",
"fixed_cost_per_mw_yr",
),
index=m.TX_SPEC_OPR_PRDS,
param=(
m.tx_spec_min_cap_mw,
m.tx_spec_max_cap_mw,
m.tx_spec_fixed_cost_per_mw_yr,
),
# min and max capacities are optional (a blank cell means "no flow limit
# in that direction"), so we cannot use a single data_portal.load() that
# ties index membership to parsing every param column. Instead we read the
# file manually, build TX_SPEC_OPR_PRDS from *every* row, and populate the
# capacity params per-cell, skipping blanks so they fall back to the
# ±Infinity defaults. This mirrors
# transmission/operations/transmission_flow_limits.py.
capacities_file = os.path.join(
scenario_directory,
weather_iteration,
hydro_iteration,
availability_iteration,
subproblem,
stage,
"inputs",
"specified_transmission_line_capacities.tab",
)

df = pd.read_csv(capacities_file, sep="\t")

opr_prds = []
min_cap = {}
max_cap = {}
fixed_cost = {}
for _, row in df.iterrows():
tx = row["transmission_line"]
prd = int(row["period"])
opr_prds.append((tx, prd))
# "." (or a blank read as NaN) leaves the param at its ±inf default.
min_val = row["specified_tx_min_mw"]
if str(min_val) != "." and pd.notna(min_val):
min_cap[(tx, prd)] = float(min_val)
max_val = row["specified_tx_max_mw"]
if str(max_val) != "." and pd.notna(max_val):
max_cap[(tx, prd)] = float(max_val)
fc_val = row["fixed_cost_per_mw_yr"]
if str(fc_val) != "." and pd.notna(fc_val):
fixed_cost[(tx, prd)] = float(fc_val)

data_portal.data()["TX_SPEC_OPR_PRDS"] = {None: opr_prds}
data_portal.data()["tx_spec_min_cap_mw"] = min_cap
data_portal.data()["tx_spec_max_cap_mw"] = max_cap
data_portal.data()["tx_spec_fixed_cost_per_mw_yr"] = fixed_cost


# Database
###############################################################################
Expand Down Expand Up @@ -390,23 +439,14 @@ def validate_inputs(
),
)

# Check for missing values (vs. missing row entries above)
cols = ["min_mw", "max_mw"]
write_validation_to_database(
conn=conn,
scenario_id=scenario_id,
weather_iteration=weather_iteration,
hydro_iteration=hydro_iteration,
availability_iteration=availability_iteration,
subproblem_id=subproblem,
stage_id=stage,
gridpath_module=__name__,
db_table="inputs_transmission_specified_capacity",
severity="High",
errors=validate_missing_inputs(df, cols),
)
# Note: min_mw and max_mw are intentionally NOT checked for missing
# values here -- a blank in either column is a valid input meaning "no
# flow limit in that direction" (the capacity param falls back to its
# ±Infinity default).

# check that min <= max
# check that min <= max (validate_column_monotonicity drops NaN rows, so
# lines with a blank/unconstrained min or max are skipped)
cols = ["min_mw", "max_mw"]
write_validation_to_database(
conn=conn,
scenario_id=scenario_id,
Expand Down
43 changes: 41 additions & 2 deletions gridpath/transmission/operations/operational_types/tx_dcopf.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ def add_model_components(
| Two-dimensional set with transmission lines of the :code:`tx_dcopf` |
| operational type and their operational timepoints. |
+-------------------------------------------------------------------------+
| | :code:`TX_DCOPF_OPR_TMPS_W_MIN_CONSTRAINT` |
| |
| Subset of :code:`TX_DCOPF_OPR_TMPS` restricted to line-timepoints whose |
| line-period has a lower flow limit (i.e. is in the transmission |
| capacity module's :code:`TX_OPR_PRDS_W_MIN_LIMIT`). The minimum-flow |
| constraint is built over this subset, so a line left unconstrained by |
| its capacity type gets no such constraint. |
+-------------------------------------------------------------------------+
| | :code:`TX_DCOPF_OPR_TMPS_W_MAX_CONSTRAINT` |
| |
| Subset of :code:`TX_DCOPF_OPR_TMPS` restricted to line-timepoints whose |
| line-period has an upper flow limit (analogous to |
| :code:`TX_DCOPF_OPR_TMPS_W_MIN_CONSTRAINT`); scopes the maximum-flow |
| constraint. |
+-------------------------------------------------------------------------+

|

Expand Down Expand Up @@ -212,6 +227,30 @@ def add_model_components(
),
)

# Operational timepoints whose line-period has a lower / upper flow limit;
# lines left unconstrained by their capacity type are excluded so no
# min/max flow constraint is built for them (see the transmission
# capacity module's TX_OPR_PRDS_W_MIN_LIMIT / _W_MAX_LIMIT).
m.TX_DCOPF_OPR_TMPS_W_MIN_CONSTRAINT = Set(
dimen=2,
within=m.TX_DCOPF_OPR_TMPS,
initialize=lambda mod: [
(tx, tmp)
for (tx, tmp) in mod.TX_DCOPF_OPR_TMPS
if (tx, mod.period[tmp]) in mod.TX_OPR_PRDS_W_MIN_LIMIT
],
)

m.TX_DCOPF_OPR_TMPS_W_MAX_CONSTRAINT = Set(
dimen=2,
within=m.TX_DCOPF_OPR_TMPS,
initialize=lambda mod: [
(tx, tmp)
for (tx, tmp) in mod.TX_DCOPF_OPR_TMPS
if (tx, mod.period[tmp]) in mod.TX_OPR_PRDS_W_MAX_LIMIT
],
)

# Derived Sets
###########################################################################

Expand Down Expand Up @@ -264,11 +303,11 @@ def add_model_components(
###########################################################################

m.TxDcopf_Min_Transmit_Constraint = Constraint(
m.TX_DCOPF_OPR_TMPS, rule=min_transmit_rule
m.TX_DCOPF_OPR_TMPS_W_MIN_CONSTRAINT, rule=min_transmit_rule
)

m.TxDcopf_Max_Transmit_Constraint = Constraint(
m.TX_DCOPF_OPR_TMPS, rule=max_transmit_rule
m.TX_DCOPF_OPR_TMPS_W_MAX_CONSTRAINT, rule=max_transmit_rule
)

m.TxDcopf_Kirchhoff_Voltage_Law_Constraint = Constraint(
Expand Down
Loading
Loading