Source code for calliope.backend.pyomo.constraints.dispatch
"""
Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
Licensed under the Apache 2.0 License (see LICENSE file).
dispatch.py
~~~~~~~~~~~~~~~~~
Energy dispatch constraints, limiting production/consumption to the capacities
of the technologies
"""
import pyomo.core as po # pylint: disable=import-error
from calliope.backend.pyomo.util import get_param, get_loc_tech, get_previous_timestep
ORDER = 10 # order in which to invoke constraints relative to other constraint files
def load_constraints(backend_model):
sets = backend_model.__calliope_model_data["sets"]
if "loc_tech_carriers_carrier_production_max_constraint" in sets:
backend_model.carrier_production_max_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_production_max_constraint,
backend_model.timesteps,
rule=carrier_production_max_constraint_rule,
)
if "loc_tech_carriers_carrier_production_min_constraint" in sets:
backend_model.carrier_production_min_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_production_min_constraint,
backend_model.timesteps,
rule=carrier_production_min_constraint_rule,
)
if "loc_tech_carriers_carrier_consumption_max_constraint" in sets:
backend_model.carrier_consumption_max_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_consumption_max_constraint,
backend_model.timesteps,
rule=carrier_consumption_max_constraint_rule,
)
if "loc_techs_resource_max_constraint" in sets:
backend_model.resource_max_constraint = po.Constraint(
backend_model.loc_techs_resource_max_constraint,
backend_model.timesteps,
rule=resource_max_constraint_rule,
)
if "loc_techs_storage_max_constraint" in sets:
backend_model.storage_max_constraint = po.Constraint(
backend_model.loc_techs_storage_max_constraint,
backend_model.timesteps,
rule=storage_max_constraint_rule,
)
if "loc_techs_storage_discharge_depth" in sets:
backend_model.storage_discharge_depth_constraint = po.Constraint(
backend_model.loc_techs_storage_discharge_depth,
backend_model.timesteps,
rule=storage_discharge_depth_constraint_rule,
)
if "loc_tech_carriers_ramping_constraint" in sets:
backend_model.ramping_up_constraint = po.Constraint(
backend_model.loc_tech_carriers_ramping_constraint,
backend_model.timesteps,
rule=ramping_up_constraint_rule,
)
backend_model.ramping_down_constraint = po.Constraint(
backend_model.loc_tech_carriers_ramping_constraint,
backend_model.timesteps,
rule=ramping_down_constraint_rule,
)
if "loc_techs_storage_intra_max_constraint" in sets:
backend_model.storage_intra_max_constraint = po.Constraint(
backend_model.loc_techs_storage_intra_max_constraint,
backend_model.timesteps,
rule=storage_intra_max_rule,
)
if "loc_techs_storage_intra_min_constraint" in sets:
backend_model.storage_intra_min_constraint = po.Constraint(
backend_model.loc_techs_storage_intra_min_constraint,
backend_model.timesteps,
rule=storage_intra_min_rule,
)
if "loc_techs_storage_inter_max_constraint" in sets:
backend_model.storage_inter_max_constraint = po.Constraint(
backend_model.loc_techs_storage_inter_max_constraint,
backend_model.datesteps,
rule=storage_inter_max_rule,
)
if "loc_techs_storage_inter_min_constraint" in sets:
backend_model.storage_inter_min_constraint = po.Constraint(
backend_model.loc_techs_storage_inter_min_constraint,
backend_model.datesteps,
rule=storage_inter_min_rule,
)
[docs]def carrier_production_max_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set maximum carrier production. All technologies.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep) \\leq energy_{cap}(loc::tech)
\\times timestep\\_resolution(timestep) \\times parasitic\\_eff(loc::tec)
"""
loc_tech = get_loc_tech(loc_tech_carrier)
carrier_prod = backend_model.carrier_prod[loc_tech_carrier, timestep]
timestep_resolution = backend_model.timestep_resolution[timestep]
parasitic_eff = get_param(backend_model, "parasitic_eff", (loc_tech, timestep))
return carrier_prod <= (
backend_model.energy_cap[loc_tech] * timestep_resolution * parasitic_eff
)
[docs]def carrier_production_min_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set minimum carrier production. All technologies except ``conversion_plus``.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep) \\geq energy_{cap}(loc::tech)
\\times timestep\\_resolution(timestep) \\times energy_{cap,min\\_use}(loc::tec)
"""
loc_tech = get_loc_tech(loc_tech_carrier)
carrier_prod = backend_model.carrier_prod[loc_tech_carrier, timestep]
timestep_resolution = backend_model.timestep_resolution[timestep]
min_use = get_param(backend_model, "energy_cap_min_use", (loc_tech, timestep))
return carrier_prod >= (
backend_model.energy_cap[loc_tech] * timestep_resolution * min_use
)
[docs]def carrier_consumption_max_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set maximum carrier consumption for demand, storage, and transmission techs.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{con}}(loc::tech::carrier, timestep) \\geq
-1 \\times energy_{cap}(loc::tech)
\\times timestep\\_resolution(timestep)
"""
loc_tech = get_loc_tech(loc_tech_carrier)
carrier_con = backend_model.carrier_con[loc_tech_carrier, timestep]
timestep_resolution = backend_model.timestep_resolution[timestep]
return carrier_con >= (
-1 * backend_model.energy_cap[loc_tech] * timestep_resolution
)
[docs]def resource_max_constraint_rule(backend_model, loc_tech, timestep):
"""
Set maximum resource consumed by supply_plus techs.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{resource_{con}}(loc::tech, timestep) \\leq
timestep\\_resolution(timestep) \\times resource_{cap}(loc::tech)
"""
timestep_resolution = backend_model.timestep_resolution[timestep]
return backend_model.resource_con[loc_tech, timestep] <= (
timestep_resolution * backend_model.resource_cap[loc_tech]
)
[docs]def storage_max_constraint_rule(backend_model, loc_tech, timestep):
"""
Set maximum stored energy. Supply_plus & storage techs only.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage}(loc::tech, timestep) \\leq
storage_{cap}(loc::tech)
"""
return (
backend_model.storage[loc_tech, timestep] <= backend_model.storage_cap[loc_tech]
)
[docs]def storage_discharge_depth_constraint_rule(backend_model, loc_tech, timestep):
"""
Forces storage state of charge to be greater than the allowed depth of discharge.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage}(loc::tech, timestep) >=
\\boldsymbol{storage_discharge_depth}\\forall loc::tech \\in loc::techs_{storage}, \\forall timestep \\in timesteps
"""
storage_discharge_depth = get_param(
backend_model, "storage_discharge_depth", loc_tech
)
return (
backend_model.storage[loc_tech, timestep]
>= storage_discharge_depth * backend_model.storage_cap[loc_tech]
)
[docs]def ramping_up_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Ramping up constraint.
.. container:: scrolling-wrapper
.. math::
diff(loc::tech::carrier, timestep) \\leq max\\_ramping\\_rate(loc::tech::carrier, timestep)
"""
return ramping_constraint(backend_model, loc_tech_carrier, timestep, direction=0)
[docs]def ramping_down_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Ramping down constraint.
.. container:: scrolling-wrapper
.. math::
-1 \\times max\\_ramping\\_rate(loc::tech::carrier, timestep) \\leq diff(loc::tech::carrier, timestep)
"""
return ramping_constraint(backend_model, loc_tech_carrier, timestep, direction=1)
[docs]def ramping_constraint(backend_model, loc_tech_carrier, timestep, direction=0):
"""
Ramping rate constraints.
Direction: 0 is up, 1 is down.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{max\\_ramping\\_rate}(loc::tech::carrier, timestep) =
energy_{ramping}(loc::tech, timestep) \\times energy_{cap}(loc::tech)
\\boldsymbol{diff}(loc::tech::carrier, timestep) =
(carrier_{prod}(loc::tech::carrier, timestep) + carrier_{con}(loc::tech::carrier, timestep))
/ timestep\\_resolution(timestep) -
(carrier_{prod}(loc::tech::carrier, timestep-1) + carrier_{con}(loc::tech::carrier, timestep-1))
/ timestep\\_resolution(timestep-1)
"""
# No constraint for first timestep
# Pyomo returns the order 1-indexed, but we want 0-indexing
if backend_model.timesteps.ord(timestep) - 1 == 0:
return po.Constraint.NoConstraint
else:
previous_step = get_previous_timestep(backend_model.timesteps, timestep)
time_res = backend_model.timestep_resolution[timestep]
time_res_prev = backend_model.timestep_resolution[previous_step]
loc_tech = loc_tech_carrier.rsplit("::", 1)[0]
# Ramping rate (fraction of installed capacity per hour)
ramping_rate = get_param(backend_model, "energy_ramping", (loc_tech, timestep))
try:
prod_this = backend_model.carrier_prod[loc_tech_carrier, timestep]
prod_prev = backend_model.carrier_prod[loc_tech_carrier, previous_step]
except KeyError:
prod_this = 0
prod_prev = 0
try:
con_this = backend_model.carrier_con[loc_tech_carrier, timestep]
con_prev = backend_model.carrier_con[loc_tech_carrier, previous_step]
except KeyError:
con_this = 0
con_prev = 0
diff = (prod_this + con_this) / time_res - (
prod_prev + con_prev
) / time_res_prev
max_ramping_rate = ramping_rate * backend_model.energy_cap[loc_tech]
if direction == 0:
return diff <= max_ramping_rate
else:
return -1 * max_ramping_rate <= diff
[docs]def storage_intra_max_rule(backend_model, loc_tech, timestep):
"""
When clustering days, to reduce the timeseries length, set limits on
intra-cluster auxiliary maximum storage decision variable.
`Ref: DOI 10.1016/j.apenergy.2018.01.023 <https://doi.org/10.1016/j.apenergy.2018.01.023>`_
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage}(loc::tech, timestep) \\leq
\\boldsymbol{storage_{intra\\_cluster, max}}(loc::tech, cluster(timestep))
\\quad \\forall loc::tech \\in loc::techs_{store}, \\forall timestep \\in timesteps
Where :math:`cluster(timestep)` is the cluster number in which the timestep
is located.
"""
cluster = backend_model.__calliope_model_data["data"]["timestep_cluster"][timestep]
return (
backend_model.storage[loc_tech, timestep]
<= backend_model.storage_intra_cluster_max[loc_tech, cluster]
)
[docs]def storage_intra_min_rule(backend_model, loc_tech, timestep):
"""
When clustering days, to reduce the timeseries length, set limits on
intra-cluster auxiliary minimum storage decision variable.
`Ref: DOI 10.1016/j.apenergy.2018.01.023 <https://doi.org/10.1016/j.apenergy.2018.01.023>`_
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage}(loc::tech, timestep) \\geq
\\boldsymbol{storage_{intra\\_cluster, min}}(loc::tech, cluster(timestep))
\\quad \\forall loc::tech \\in loc::techs_{store}, \\forall timestep \\in timesteps
Where :math:`cluster(timestep)` is the cluster number in which the timestep
is located.
"""
cluster = backend_model.__calliope_model_data["data"]["timestep_cluster"][timestep]
return (
backend_model.storage[loc_tech, timestep]
>= backend_model.storage_intra_cluster_min[loc_tech, cluster]
)
[docs]def storage_inter_max_rule(backend_model, loc_tech, datestep):
"""
When clustering days, to reduce the timeseries length, set maximum limit on
the intra-cluster and inter-date stored energy.
intra-cluster = all timesteps in a single cluster
datesteps = all dates in the unclustered timeseries (each has a corresponding cluster)
`Ref: DOI 10.1016/j.apenergy.2018.01.023 <https://doi.org/10.1016/j.apenergy.2018.01.023>`_
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage_{inter\\_cluster}}(loc::tech, datestep) +
\\boldsymbol{storage_{intra\\_cluster, max}}(loc::tech, cluster(datestep))
\\leq \\boldsymbol{storage_{cap}}(loc::tech) \\quad \\forall
loc::tech \\in loc::techs_{store}, \\forall datestep \\in datesteps
Where :math:`cluster(datestep)` is the cluster number in which the datestep
is located.
"""
cluster = backend_model.__calliope_model_data["data"]["lookup_datestep_cluster"][
datestep
]
return (
backend_model.storage_inter_cluster[loc_tech, datestep]
+ backend_model.storage_intra_cluster_max[loc_tech, cluster]
<= backend_model.storage_cap[loc_tech]
)
[docs]def storage_inter_min_rule(backend_model, loc_tech, datestep):
"""
When clustering days, to reduce the timeseries length, set minimum limit on
the intra-cluster and inter-date stored energy.
intra-cluster = all timesteps in a single cluster
datesteps = all dates in the unclustered timeseries (each has a corresponding cluster)
`Ref: DOI 10.1016/j.apenergy.2018.01.023 <https://doi.org/10.1016/j.apenergy.2018.01.023>`_
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage_{inter\\_cluster}}(loc::tech, datestep)
\\times (1 - storage\\_loss(loc::tech, timestep))^{24} +
\\boldsymbol{storage_{intra\\_cluster, min}}(loc::tech, cluster(datestep))
\\geq 0 \\quad \\forall loc::tech \\in loc::techs_{store},
\\forall datestep \\in datesteps
Where :math:`cluster(datestep)` is the cluster number in which the datestep
is located.
"""
cluster = backend_model.__calliope_model_data["data"]["lookup_datestep_cluster"][
datestep
]
storage_loss = get_param(backend_model, "storage_loss", loc_tech)
return (
backend_model.storage_inter_cluster[loc_tech, datestep]
* ((1 - storage_loss) ** 24)
+ backend_model.storage_intra_cluster_min[loc_tech, cluster]
>= 0
)