Source code for calliope.backend.pyomo.constraints.milp
"""
Copyright (C) since 2013 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,
apply_equals,
get_depreciation_rate,
)
from calliope.backend.pyomo.constraints.capacity import get_capacity_constraint
ORDER = 30 # order in which to invoke constraints relative to other constraint files
def load_constraints(backend_model):
sets = backend_model.__calliope_model_data["sets"]
run_config = backend_model.__calliope_run_config
if "loc_techs_unit_commitment_milp_constraint" in sets:
backend_model.unit_commitment_milp_constraint = po.Constraint(
backend_model.loc_techs_unit_commitment_milp_constraint,
backend_model.timesteps,
rule=unit_commitment_milp_constraint_rule,
)
if "loc_techs_unit_capacity_milp_constraint" in sets:
backend_model.unit_capacity_milp_constraint = po.Constraint(
backend_model.loc_techs_unit_capacity_milp_constraint,
rule=unit_capacity_milp_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_milp_constraint" in sets:
backend_model.storage_capacity_units_milp_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_units_milp_constraint,
rule=storage_capacity_units_milp_constraint_rule,
)
if "loc_techs_energy_capacity_units_milp_constraint" in sets:
backend_model.energy_capacity_units_milp_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_units_milp_constraint,
rule=energy_capacity_units_milp_constraint_rule,
)
if (
"loc_techs_update_costs_investment_units_milp_constraint" in sets
and run_config["mode"] != "operate"
):
for loc_tech, cost in (
backend_model.loc_techs_update_costs_investment_units_milp_constraint
* backend_model.costs
):
update_costs_investment_units_milp_constraint(
backend_model,
cost,
loc_tech,
)
if (
"loc_techs_update_costs_investment_purchase_milp_constraint" in sets
and run_config["mode"] != "operate"
):
for loc_tech, cost in (
backend_model.loc_techs_update_costs_investment_purchase_milp_constraint
* backend_model.costs
):
update_costs_investment_purchase_milp_constraint(
backend_model,
cost,
loc_tech,
)
if "loc_techs_energy_capacity_max_purchase_milp_constraint" in sets:
backend_model.energy_capacity_max_purchase_milp_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_max_purchase_milp_constraint,
rule=energy_capacity_max_purchase_milp_constraint_rule,
)
if "loc_techs_energy_capacity_min_purchase_milp_constraint" in sets:
backend_model.energy_capacity_min_purchase_milp_constraint = po.Constraint(
backend_model.loc_techs_energy_capacity_min_purchase_milp_constraint,
rule=energy_capacity_min_purchase_milp_constraint_rule,
)
if "loc_techs_storage_capacity_max_purchase_milp_constraint" in sets:
backend_model.storage_capacity_max_purchase_milp_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_max_purchase_milp_constraint,
rule=storage_capacity_max_purchase_milp_constraint_rule,
)
if "loc_techs_storage_capacity_min_purchase_milp_constraint" in sets:
backend_model.storage_capacity_min_purchase_milp_constraint = po.Constraint(
backend_model.loc_techs_storage_capacity_min_purchase_milp_constraint,
rule=storage_capacity_min_purchase_milp_constraint_rule,
)
if "techs_unit_capacity_systemwide_milp_constraint" in sets:
backend_model.unit_capacity_systemwide_milp_constraint = po.Constraint(
backend_model.techs_unit_capacity_systemwide_milp_constraint,
rule=unit_capacity_systemwide_milp_constraint_rule,
)
if "loc_techs_asynchronous_prod_con_milp_constraint" in sets:
backend_model.asynchronous_con_milp_constraint = po.Constraint(
backend_model.loc_techs_asynchronous_prod_con_milp_constraint,
backend_model.timesteps,
rule=asynchronous_con_milp_constraint_rule,
)
backend_model.asynchronous_prod_milp_constraint = po.Constraint(
backend_model.loc_techs_asynchronous_prod_con_milp_constraint,
backend_model.timesteps,
rule=asynchronous_prod_milp_constraint_rule,
)
[docs]def unit_commitment_milp_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_milp_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_milp_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_milp_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_milp_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 apply_equals(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_milp_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_milp_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 apply_equals(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_milp_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_milp_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}
"""
ts_weight = get_timestep_weight(backend_model)
depreciation_rate = get_depreciation_rate(backend_model, (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_milp_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}
"""
ts_weight = get_timestep_weight(backend_model)
depreciation_rate = get_depreciation_rate(backend_model, (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_milp_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 getattr(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 apply_equals(equals_systemwide):
return po.Constraint.NoConstraint
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 apply_equals(equals_systemwide):
return sum_expr_units + sum_expr_purchase == equals_systemwide * multiplier
else:
return sum_expr_units + sum_expr_purchase <= max_systemwide * multiplier
[docs]def asynchronous_con_milp_constraint_rule(backend_model, loc_tech, timestep):
"""
BigM limit set on `carrier_con`, forcing it to either be zero or non-zero,
depending on whether `con` is zero or one, respectively.
.. container:: scrolling-wrapper
.. math::
- \\boldsymbol{carrier_con}[loc::tech::carrier, timestep] \\leq
\\text{bigM} \\times (1 - \\boldsymbol{prod_con_switch}[loc::tech, timestep])
\\forall loc::tech \\in loc::techs_{asynchronous_prod_con},
\\forall timestep \\in timesteps
"""
model_dict = backend_model.__calliope_model_data
loc_tech_carrier = model_dict["data"]["lookup_loc_techs"][loc_tech]
return (
-1 * backend_model.carrier_con[loc_tech_carrier, timestep]
<= (1 - backend_model.prod_con_switch[loc_tech, timestep]) * backend_model.bigM
)
[docs]def asynchronous_prod_milp_constraint_rule(backend_model, loc_tech, timestep):
"""
BigM limit set on `carrier_prod`, forcing it to either be zero or non-zero,
depending on whether `prod` is zero or one, respectively.
.. container:: scrolling-wrapper
.. math::
\\boldsymbol{carrier_prod}[loc::tech::carrier, timestep] \\leq
\\text{bigM} \\times \\boldsymbol{prod_con_switch}[loc::tech, timestep]
\\forall loc::tech \\in loc::techs_{asynchronous_prod_con},
\\forall timestep \\in timesteps
"""
model_dict = backend_model.__calliope_model_data
loc_tech_carrier = model_dict["data"]["lookup_loc_techs"][loc_tech]
return (
backend_model.carrier_prod[loc_tech_carrier, timestep]
<= backend_model.prod_con_switch[loc_tech, timestep] * backend_model.bigM
)