Source code for calliope.backend.pyomo.constraints.milp
"""
Copyright (C) 2013-2018 Calliope contributors listed in AUTHORS.
Licensed under the Apache 2.0 License (see LICENSE file).
milp.py
~~~~~~~~~~~~~~~~~
Constraints for binary and integer decision variables
"""
import pyomo.core as po # pylint: disable=import-error
import numpy as np
from calliope.backend.pyomo.util import \
get_param, \
get_timestep_weight, \
get_loc_tech, \
split_comma_list, \
loc_tech_is_in
from calliope.backend.pyomo.constraints.capacity import get_capacity_constraint
def load_constraints(backend_model):
sets = backend_model.__calliope_model_data__['sets']
if 'loc_techs_milp' in sets:
backend_model.unit_commitment_constraint = po.Constraint(
backend_model.loc_techs_unit_commitment_constraint, backend_model.timesteps,
rule=unit_commitment_constraint_rule
)
backend_model.unit_capacity_constraint = po.Constraint(
backend_model.loc_techs_unit_capacity_constraint,
rule=unit_capacity_constraint_rule
)
if 'loc_tech_carriers_carrier_production_max_milp_constraint' in sets:
backend_model.carrier_production_max_milp_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_production_max_milp_constraint,
backend_model.timesteps,
rule=carrier_production_max_milp_constraint_rule
)
if 'loc_techs_carrier_production_max_conversion_plus_milp_constraint' in sets:
backend_model.carrier_production_max_conversion_plus_milp_constraint = po.Constraint(
backend_model.loc_techs_carrier_production_max_conversion_plus_milp_constraint,
backend_model.timesteps,
rule=carrier_production_max_conversion_plus_milp_constraint_rule
)
if 'loc_tech_carriers_carrier_consumption_max_milp_constraint' in sets:
backend_model.carrier_consumption_max_milp_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_consumption_max_milp_constraint,
backend_model.timesteps,
rule=carrier_consumption_max_milp_constraint_rule
)
if 'loc_tech_carriers_carrier_production_min_milp_constraint' in sets:
backend_model.carrier_production_min_milp_constraint = po.Constraint(
backend_model.loc_tech_carriers_carrier_production_min_milp_constraint,
backend_model.timesteps,
rule=carrier_production_min_milp_constraint_rule
)
if 'loc_techs_carrier_production_min_conversion_plus_milp_constraint' in sets:
backend_model.carrier_production_min_conversion_plus_milp_constraint = po.Constraint(
backend_model.loc_techs_carrier_production_min_conversion_plus_milp_constraint,
backend_model.timesteps,
rule=carrier_production_min_conversion_plus_milp_constraint_rule
)
if 'loc_techs_storage_capacity_units_constraint' in sets:
backend_model.storage_capacity_units_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_units_constraint,
rule=storage_capacity_units_constraint_rule
)
if 'loc_techs_energy_capacity_units_constraint' in sets:
backend_model.energy_capacity_units_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_units_constraint,
rule=energy_capacity_units_constraint_rule
)
if 'loc_techs_update_costs_investment_units_constraint' in sets and backend_model.mode != 'operate':
for loc_tech, cost in (
backend_model.loc_techs_update_costs_investment_units_constraint
* backend_model.costs):
update_costs_investment_units_constraint(backend_model, cost, loc_tech,)
if 'loc_techs_update_costs_investment_purchase_constraint' in sets and backend_model.mode != 'operate':
for loc_tech, cost in (
backend_model.loc_techs_update_costs_investment_purchase_constraint
* backend_model.costs):
update_costs_investment_purchase_constraint(backend_model, cost, loc_tech,)
if 'loc_techs_energy_capacity_max_purchase_constraint' in sets:
backend_model.energy_capacity_max_purchase_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_max_purchase_constraint,
rule=energy_capacity_max_purchase_constraint_rule
)
if 'loc_techs_energy_capacity_min_purchase_constraint' in sets:
backend_model.energy_capacity_min_purchase_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_min_purchase_constraint,
rule=energy_capacity_min_purchase_constraint_rule
)
if 'loc_techs_storage_capacity_max_purchase_constraint' in sets:
backend_model.storage_capacity_max_purchase_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_max_purchase_constraint,
rule=storage_capacity_max_purchase_constraint_rule
)
if 'loc_techs_storage_capacity_min_purchase_constraint' in sets:
backend_model.storage_capacity_min_purchase_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_min_purchase_constraint,
rule=storage_capacity_min_purchase_constraint_rule
)
if 'techs_unit_capacity_systemwide_constraint' in sets:
backend_model.unit_capacity_systemwide_constraint = po.Constraint(
backend_model.techs_unit_capacity_systemwide_constraint,
rule=unit_capacity_systemwide_constraint_rule
)
[docs]def unit_commitment_constraint_rule(backend_model, loc_tech, timestep):
"""
Constraining the number of integer units
:math:`operating_units(loc_tech, timestep)` of a technology which
can operate in a given timestep, based on maximum purchased units
:math:`units(loc_tech)`
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{operating\_units}(loc::tech, timestep) \\leq
\\boldsymbol{units}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp},
\\forall timestep \\in timesteps
"""
return (backend_model.operating_units[loc_tech, timestep]
<= backend_model.units[loc_tech])
[docs]def unit_capacity_constraint_rule(backend_model, loc_tech):
"""
Add upper and lower bounds for purchased units of a technology
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{units}(loc::tech)
\\begin{cases}
= units_{equals}(loc::tech),& \\text{if } units_{equals}(loc::tech)\\\\
\\leq units_{max}(loc::tech),& \\text{if } units_{max}(loc::tech)\\\\
\\text{unconstrained},& \\text{otherwise}
\\end{cases}
\\quad \\forall loc::tech \\in loc::techs_{milp}
and (if ``equals`` not enforced):
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{units}(loc::tech) \\geq units_{min}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp}
"""
return get_capacity_constraint(backend_model, 'units', loc_tech)
[docs]def carrier_production_max_milp_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set maximum carrier production of MILP techs that aren't conversion plus
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep)
\\leq energy_{cap, per unit}(loc::tech) \\times timestep\_resolution(timestep)
\\times \\boldsymbol{operating\_units}(loc::tech, timestep)
\\times \\eta_{parasitic}(loc::tech, timestep)
\\quad \\forall loc::tech \\in loc::techs_{milp}, \\forall timestep \\in timesteps
:math:`\\eta_{parasitic}` is only activated for `supply_plus` technologies
"""
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))
energy_cap = get_param(backend_model, 'energy_cap_per_unit', loc_tech)
return carrier_prod <= (
backend_model.operating_units[loc_tech, timestep] *
timestep_resolution * energy_cap * parasitic_eff
)
[docs]def carrier_production_max_conversion_plus_milp_constraint_rule(backend_model, loc_tech, timestep):
"""
Set maximum carrier production of conversion_plus MILP techs
.. container:: scrolling-wrapper
.. math::
\sum_{loc::tech::carrier \\in loc::tech::carriers_{out}}
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep)
\\leq energy_{cap, per unit}(loc::tech) \\times timestep\_resolution(timestep)
\\times \\boldsymbol{operating\_units}(loc::tech, timestep)
\\times \\eta_{parasitic}(loc::tech, timestep)
\\quad \\forall loc::tech \\in loc::techs_{milp, conversion^{+}}, \\forall timestep \\in timesteps
"""
model_data_dict = backend_model.__calliope_model_data__['data']
timestep_resolution = backend_model.timestep_resolution[timestep]
energy_cap = get_param(backend_model, 'energy_cap_per_unit', loc_tech)
loc_tech_carriers_out = (
split_comma_list(model_data_dict['lookup_loc_techs_conversion_plus']['out', loc_tech])
)
carrier_prod = sum(backend_model.carrier_prod[loc_tech_carrier, timestep]
for loc_tech_carrier in loc_tech_carriers_out)
return carrier_prod <= (
backend_model.operating_units[loc_tech, timestep] *
timestep_resolution * energy_cap
)
[docs]def carrier_production_min_milp_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set minimum carrier production of MILP techs that aren't conversion plus
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep)
\\geq energy_{cap, per unit}(loc::tech) \\times timestep\_resolution(timestep)
\\times \\boldsymbol{operating\_units}(loc::tech, timestep)
\\times energy_{cap, min use}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp}, \\forall timestep \\in timesteps
"""
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))
energy_cap = get_param(backend_model, 'energy_cap_per_unit', loc_tech)
return carrier_prod >= (
backend_model.operating_units[loc_tech, timestep] *
timestep_resolution * energy_cap * min_use
)
[docs]def carrier_production_min_conversion_plus_milp_constraint_rule(backend_model, loc_tech, timestep):
"""
Set minimum carrier production of conversion_plus MILP techs
.. container:: scrolling-wrapper
.. math::
\sum_{loc::tech::carrier \\in loc::tech::carriers_{out}}
\\boldsymbol{carrier_{prod}}(loc::tech::carrier, timestep)
\\geq energy_{cap, per unit}(loc::tech) \\times timestep\_resolution(timestep)
\\times \\boldsymbol{operating\_units}(loc::tech, timestep)
\\times energy_{cap, min use}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp, conversion^{+}},
\\forall timestep \\in timesteps
"""
model_data_dict = backend_model.__calliope_model_data__['data']
timestep_resolution = backend_model.timestep_resolution[timestep]
energy_cap = get_param(backend_model, 'energy_cap_per_unit', loc_tech)
min_use = get_param(backend_model, 'energy_cap_min_use', (loc_tech, timestep))
loc_tech_carriers_out = (
split_comma_list(model_data_dict['lookup_loc_techs_conversion_plus']['out', loc_tech])
)
carrier_prod = sum(backend_model.carrier_prod[loc_tech_carrier, timestep]
for loc_tech_carrier in loc_tech_carriers_out)
return carrier_prod >= (
backend_model.operating_units[loc_tech, timestep] *
timestep_resolution * energy_cap * min_use
)
[docs]def carrier_consumption_max_milp_constraint_rule(backend_model, loc_tech_carrier, timestep):
"""
Set maximum carrier consumption of demand, storage, and transmission MILP techs
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_{con}}(loc::tech::carrier, timestep)
\\geq -1 * energy_{cap, per unit}(loc::tech) \\times timestep\_resolution(timestep)
\\times \\boldsymbol{operating\_units}(loc::tech, timestep)
\\times \\eta_{parasitic}(loc::tech, timestep)
\\quad \\forall loc::tech \\in loc::techs_{milp, con}, \\forall timestep \\in timesteps
"""
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]
energy_cap = get_param(backend_model, 'energy_cap_per_unit', loc_tech)
return carrier_con >= (-1 *
backend_model.operating_units[loc_tech, timestep] *
timestep_resolution * energy_cap
)
[docs]def energy_capacity_units_constraint_rule(backend_model, loc_tech):
"""
Set energy capacity decision variable as a function of purchased units
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{energy_{cap}}(loc::tech) =
\\boldsymbol{units}(loc::tech) \\times energy_{cap, per unit}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp}
"""
return backend_model.energy_cap[loc_tech] == (
backend_model.units[loc_tech] *
get_param(backend_model, 'energy_cap_per_unit', loc_tech)
)
[docs]def storage_capacity_units_constraint_rule(backend_model, loc_tech):
"""
Set storage capacity decision variable as a function of purchased units
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage_{cap}}(loc::tech) =
\\boldsymbol{units}(loc::tech) \\times storage_{cap, per unit}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{milp, store}
"""
return backend_model.storage_cap[loc_tech] == (
backend_model.units[loc_tech] *
get_param(backend_model, 'storage_cap_per_unit', loc_tech)
)
[docs]def energy_capacity_max_purchase_constraint_rule(backend_model, loc_tech):
"""
Set maximum energy capacity decision variable upper bound as a function of
binary purchase variable
The first valid case is applied:
.. container:: scrolling-wrapper
.. math::
\\frac{\\boldsymbol{energy_{cap}}(loc::tech)}{energy_{cap, scale}(loc::tech)}
\\begin{cases}
= energy_{cap, equals}(loc::tech) \\times \\boldsymbol{purchased}(loc::tech),&
\\text{if } energy_{cap, equals}(loc::tech)\\\\
\\leq energy_{cap, max}(loc::tech) \\times \\boldsymbol{purchased}(loc::tech),&
\\text{if } energy_{cap, max}(loc::tech)\\\\
\\text{unconstrained},& \\text{otherwise}
\\end{cases}
\\forall loc::tech \\in loc::techs_{purchase}
"""
energy_cap_max = get_param(backend_model, 'energy_cap_max', loc_tech)
energy_cap_equals = get_param(backend_model, 'energy_cap_equals', loc_tech)
energy_cap_scale = get_param(backend_model, 'energy_cap_scale', loc_tech)
if po.value(energy_cap_equals):
return backend_model.energy_cap[loc_tech] == (
energy_cap_equals * energy_cap_scale * backend_model.purchased[loc_tech]
)
else:
return backend_model.energy_cap[loc_tech] <= (
energy_cap_max * energy_cap_scale * backend_model.purchased[loc_tech]
)
[docs]def energy_capacity_min_purchase_constraint_rule(backend_model, loc_tech):
"""
Set minimum energy capacity decision variable upper bound as a function of
binary purchase variable
and (if ``equals`` not enforced):
.. container:: scrolling-wrapper
.. math::
\\frac{\\boldsymbol{energy_{cap}}(loc::tech)}{energy_{cap, scale}(loc::tech)}
\\geq energy_{cap, min}(loc::tech) \\times \\boldsymbol{purchased}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs
"""
energy_cap_min = get_param(backend_model, 'energy_cap_min', loc_tech)
energy_cap_scale = get_param(backend_model, 'energy_cap_scale', loc_tech)
return backend_model.energy_cap[loc_tech] >= (
energy_cap_min * energy_cap_scale * backend_model.purchased[loc_tech]
)
[docs]def storage_capacity_max_purchase_constraint_rule(backend_model, loc_tech):
"""
Set maximum storage capacity.
The first valid case is applied:
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage_{cap}}(loc::tech)
\\begin{cases}
= storage_{cap, equals}(loc::tech) \\times \\boldsymbol{purchased},&
\\text{if } storage_{cap, equals} \\\\
\\leq storage_{cap, max}(loc::tech) \\times \\boldsymbol{purchased},&
\\text{if } storage_{cap, max}(loc::tech)\\\\
\\text{unconstrained},& \\text{otherwise}
\\end{cases}
\\forall loc::tech \\in loc::techs_{purchase, store}
"""
storage_cap_max = get_param(backend_model, 'storage_cap_max', loc_tech)
storage_cap_equals = get_param(backend_model, 'storage_cap_equals', loc_tech)
if po.value(storage_cap_equals):
return backend_model.storage_cap[loc_tech] == (
storage_cap_equals * backend_model.purchased[loc_tech]
)
elif po.value(storage_cap_max):
return backend_model.storage_cap[loc_tech] <= (
storage_cap_max * backend_model.purchased[loc_tech]
)
else:
return po.Constraint.Skip
[docs]def storage_capacity_min_purchase_constraint_rule(backend_model, loc_tech):
"""
Set minimum storage capacity decision variable as a function of
binary purchase variable
if ``equals`` not enforced for storage_cap:
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{storage_{cap}}(loc::tech)
\\geq storage_{cap, min}(loc::tech) \\times \\boldsymbol{purchased}(loc::tech)
\\quad \\forall loc::tech \\in loc::techs_{purchase, store}
"""
storage_cap_min = get_param(backend_model, 'storage_cap_min', loc_tech)
if po.value(storage_cap_min):
return backend_model.storage_cap[loc_tech] >= (
storage_cap_min * backend_model.purchased[loc_tech]
)
else:
return po.Constraint.Skip
[docs]def update_costs_investment_units_constraint(backend_model, cost, loc_tech):
"""
Add MILP investment costs (cost * number of units purchased)
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{cost_{investment}}(cost, loc::tech) += \\boldsymbol{units}(loc::tech)
\\times cost_{purchase}(cost, loc::tech) * timestep_{weight} * depreciation
\\quad \\forall cost \\in costs, \\forall loc::tech \\in loc::techs_{cost_{investment}, milp}
"""
model_data_dict = backend_model.__calliope_model_data__
ts_weight = get_timestep_weight(backend_model)
depreciation_rate = model_data_dict['data']['cost_depreciation_rate'][(cost, loc_tech)]
cost_purchase = get_param(backend_model, 'cost_purchase', (cost, loc_tech))
cost_of_purchase = (
backend_model.units[loc_tech] * cost_purchase * ts_weight * depreciation_rate
)
if loc_tech_is_in(backend_model, loc_tech, 'loc_techs_transmission'):
cost_of_purchase = cost_of_purchase / 2
backend_model.cost_investment_rhs[cost, loc_tech].expr += cost_of_purchase
return None
[docs]def update_costs_investment_purchase_constraint(backend_model, cost, loc_tech):
"""
Add binary investment costs (cost * binary_purchased_unit)
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{cost_{investment}}(cost, loc::tech) += \\boldsymbol{purchased}(loc::tech)
\\times cost_{purchase}(cost, loc::tech) * timestep_{weight} * depreciation
\\quad \\forall cost \\in costs, \\forall loc::tech \\in loc::techs_{cost_{investment}, purchase}
"""
model_data_dict = backend_model.__calliope_model_data__
ts_weight = get_timestep_weight(backend_model)
depreciation_rate = model_data_dict['data']['cost_depreciation_rate'][(cost, loc_tech)]
cost_purchase = get_param(backend_model, 'cost_purchase', (cost, loc_tech))
cost_of_purchase = (
backend_model.purchased[loc_tech] * cost_purchase * ts_weight * depreciation_rate
)
if loc_tech_is_in(backend_model, loc_tech, 'loc_techs_transmission'):
cost_of_purchase = cost_of_purchase / 2
backend_model.cost_investment_rhs[cost, loc_tech].expr += cost_of_purchase
return None
[docs]def unit_capacity_systemwide_constraint_rule(backend_model, tech):
"""
Set constraints to limit the number of purchased units of a single technology
type across all locations in the model.
The first valid case is applied:
.. container:: scrolling-wrapper
.. math::
\\sum_{loc}\\boldsymbol{units}(loc::tech) + \\boldsymbol{purchased}(loc::tech)
\\begin{cases}
= units_{equals, systemwide}(tech),&
\\text{if } units_{equals, systemwide}(tech)\\\\
\\leq units_{max, systemwide}(tech),&
\\text{if } units_{max, systemwide}(tech)\\\\
\\text{unconstrained},& \\text{otherwise}
\\end{cases}
\\forall tech \\in techs
"""
if tech in backend_model.techs_transmission_names:
all_loc_techs = [
i for i in backend_model.loc_techs_transmission
if i.split('::')[1].split(':')[0] == tech
]
multiplier = 2 # there are always two technologies associated with one link
else:
all_loc_techs = [
i for i in backend_model.loc_techs
if i.split('::')[1] == tech
]
multiplier = 1
max_systemwide = get_param(backend_model, 'units_max_systemwide', tech)
equals_systemwide = get_param(backend_model, 'units_equals_systemwide', tech)
if np.isinf(po.value(max_systemwide)) and not equals_systemwide:
return po.Constraint.NoConstraint
elif equals_systemwide and np.isinf(po.value(equals_systemwide)):
raise ValueError(
'Cannot use inf for energy_cap_equals_systemwide for tech `{}`'.format(tech)
)
sum_expr_units = sum(
backend_model.units[loc_tech] for loc_tech in all_loc_techs
if loc_tech_is_in(backend_model, loc_tech, 'loc_techs_milp')
)
sum_expr_purchase = sum(
backend_model.purchased[loc_tech] for loc_tech in all_loc_techs
if loc_tech_is_in(backend_model, loc_tech, 'loc_techs_purchase')
)
if equals_systemwide:
return sum_expr_units + sum_expr_purchase == equals_systemwide * multiplier
else:
return sum_expr_units + sum_expr_purchase <= max_systemwide * multiplier