Skip to content

Commit

Permalink
Merge branch 'main' into feature-attrdict-declutter
Browse files Browse the repository at this point in the history
  • Loading branch information
irm-codebase authored Dec 2, 2024
2 parents 9f978d9 + f2e4f43 commit 5eafc04
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

|changed| `template:` can now be used anywhere within YAML definition files, not just in the `nodes`, `techs` and `data_tables` sections.

|fixed| Area-based parameters have appropriate documented units of `area` rather than `area^2` (#701).

|fixed| Technology capacity lower bound constraints so that `[cap-type]_min` (e.g., `flow_cap_min`) is not always enforced if the `purchased_units` variable is active (#643).

|changed| Single data entries defined in YAML indexed parameters will not be automatically broadcast along indexed dimensions.
To achieve the same functionality as in `<v0.7.dev4`, the user must set the new `init` configuration option `broadcast_param_data` to True (#615).

Expand Down
16 changes: 10 additions & 6 deletions src/calliope/config/model_def_schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ properties:
description: >-
Sets `area_use` to a parameter in operate mode.
NOTE: this parameter cannot be used in `plan` mode as it clashes with the decision variable of the same name.
x-unit: $\text{area}^{2}$.
x-unit: $\text{area}$.

area_use_max:
$ref: "#/$defs/TechParamNullNumber"
Expand All @@ -529,7 +529,7 @@ properties:
title: Maximum usable area.
description: >-
If set to a finite value, limits the upper bound of the `area_use` decision variable to this value.
x-unit: $\text{area}^{2}$.
x-unit: $\text{area}$.

area_use_min:
$ref: "#/$defs/TechParamNullNumber"
Expand All @@ -538,7 +538,7 @@ properties:
title: Minimum usable area.
description: >-
Limits the lower bound of the `area_use` decision variable to this value.
x-unit: $\text{area}^{2}$.
x-unit: $\text{area}$.

area_use_per_flow_cap:
$ref: "#/$defs/TechParamNullNumber"
Expand All @@ -547,7 +547,7 @@ properties:
title: Area use per flow capacity
description: >-
If set, forces `area_use` to follow `flow_cap` with the given numerical ratio (e.g. setting to 1.5 means that `area_use == 1.5 * flow_cap`).
x-unit: $\frac{\text{area}^{2}}{\text{power}}$.
x-unit: $\frac{\text{area}}{\text{power}}$.

storage_cap:
$ref: "#/$defs/TechParamNullNumber"
Expand All @@ -558,7 +558,7 @@ properties:
description: >-
Sets `storage_cap` to a parameter in operate mode.
NOTE: this parameter cannot be used in `plan` mode as it clashes with the decision variable of the same name.
x-unit: $\text{area}^{2}$.
x-unit: $\text{area}$.

storage_cap_max:
$ref: "#/$defs/TechParamNullNumber"
Expand Down Expand Up @@ -950,7 +950,7 @@ properties:
title: Cost of area use.
description: >-
Cost per unit `area_use`.
x-unit: $\text{area}^{-2}$.
x-unit: $\text{area}^{-1}$.

cost_source_cap:
$ref: "#/$defs/TechCostNullNumber"
Expand Down Expand Up @@ -1016,3 +1016,7 @@ properties:
minimum: 0
default: .inf
x-resample_method: mean
title: Available area at the given node.
description: >-
Limits the total area that can be occupied by all technologies which have the `area_use` decision variable activated.
x-unit: $\text{area}$.
68 changes: 53 additions & 15 deletions src/calliope/math/plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ constraints:
storage_capacity_units_milp:
description: >-
Fix the storage capacity of any technology using integer units to define its capacity.
foreach: [nodes, techs, carriers]
foreach: [nodes, techs]
where: "storage AND purchased_units AND storage_cap_per_unit"
equations:
- expression: storage_cap == purchased_units * storage_cap_per_unit
Expand All @@ -378,7 +378,7 @@ constraints:
description: >-
Fix the flow capacity of any technology using integer units to define its capacity.
foreach: [nodes, techs, carriers]
where: "operating_units AND flow_cap_per_unit"
where: "purchased_units AND flow_cap_per_unit"
equations:
- expression: flow_cap == purchased_units * flow_cap_per_unit

Expand All @@ -394,14 +394,18 @@ constraints:
- where: NOT flow_cap_max
expression: flow_cap <= bigM * purchased_units

flow_capacity_min_purchase_milp:
flow_capacity_minimum:
description: >-
Set the lower bound on a technology's flow capacity,
for any technology with integer capacity purchasing.
for any technology with a non-zero lower bound,
with or without integer capacity purchasing.
foreach: [nodes, techs, carriers]
where: "purchased_units AND flow_cap_min"
where: "flow_cap_min"
equations:
- expression: flow_cap >= flow_cap_min * purchased_units
- where: NOT purchased_units
expression: flow_cap >= flow_cap_min
- where: purchased_units
expression: flow_cap >= flow_cap_min * purchased_units

storage_capacity_max_purchase_milp:
description: >-
Expand All @@ -412,14 +416,44 @@ constraints:
equations:
- expression: storage_cap <= storage_cap_max * purchased_units

storage_capacity_min_purchase_milp:
storage_capacity_minimum:
description: >-
Set the lower bound on a technology's storage capacity,
for any technology with integer capacity purchasing.
Set the lower bound on a technology's storage capacity
for any technology with a non-zero lower bound,
with or without integer capacity purchasing.
foreach: [nodes, techs]
where: "storage_cap_min"
equations:
- where: NOT purchased_units
expression: storage_cap >= storage_cap_min
- where: purchased_units
expression: storage_cap >= storage_cap_min * purchased_units

area_use_minimum:
description: >-
Set the lower bound on a technology's area use
for any technology with a non-zero lower bound,
with or without integer capacity purchasing.
foreach: [nodes, techs]
where: "area_use_min"
equations:
- where: NOT purchased_units
expression: area_use >= area_use_min
- where: purchased_units
expression: area_use >= area_use_min * purchased_units

source_capacity_minimum:
description: >-
Set the lower bound on a technology's source capacity
for any supply technology with a non-zero lower bound,
with or without integer capacity purchasing.
foreach: [nodes, techs]
where: "purchased_units AND storage_cap_min"
where: "base_tech=supply AND source_cap_min"
equations:
- expression: storage_cap >= storage_cap_min * purchased_units
- where: NOT purchased_units
expression: source_cap >= source_cap_min
- where: purchased_units
expression: source_cap >= source_cap_min * purchased_units

unit_capacity_max_systemwide_milp:
description: >-
Expand Down Expand Up @@ -496,7 +530,7 @@ variables:
unit: power
foreach: [nodes, techs, carriers]
bounds:
min: flow_cap_min
min: 0 # set in a distinct constraint to handle the integer purchase variable
max: flow_cap_max

link_flow_cap:
Expand Down Expand Up @@ -561,7 +595,7 @@ variables:
foreach: [nodes, techs]
where: "(area_use_min OR area_use_max OR area_use_per_flow_cap OR sink_unit=per_area OR source_unit=per_area)"
bounds:
min: area_use_min
min: 0 # set in a distinct constraint to handle the integer purchase variable
max: area_use_max

source_use:
Expand All @@ -586,7 +620,7 @@ variables:
foreach: [nodes, techs]
where: "base_tech=supply"
bounds:
min: source_cap_min
min: 0 # set in a distinct constraint to handle the integer purchase variable
max: source_cap_max

# --8<-- [start:variable]
Expand All @@ -601,7 +635,7 @@ variables:
where: "include_storage=True OR base_tech=storage"
domain: real # optional; defaults to real.
bounds:
min: storage_cap_min
min: 0 # set in a distinct constraint to handle the integer purchase variable
max: storage_cap_max
active: true # optional; defaults to true.
# --8<-- [end:variable]
Expand Down Expand Up @@ -662,6 +696,10 @@ variables:
description: >-
Flow capacity that will be set to zero if the technology is not operating in a given
timestep and will be set to the value of the decision variable `flow_cap` otherwise.
This is useful when you want to set a minimum flow capacity for any technology investment, but also want to allow the model to decide the capacity.
It is expected to only be used when `purchased_units_max == 1`,
i.e., the `purchased_units` decision variable is binary.
If `purchased_units_max > 1`, you may get strange results and should instead use the less flexible `flow_cap_per_unit`.
default: 0
unit: power
foreach: [nodes, techs, carriers, timesteps]
Expand Down
17 changes: 17 additions & 0 deletions tests/common/lp_files/area_use.lp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
\* Source Pyomo model name=None *\

min
objectives(foo)(0):
+1 variables(area_use)(a__test_supply_elec)
+1 variables(area_use)(b__test_supply_elec)

s.t.

c_l_constraints(area_use_minimum)(a__test_supply_elec)_:
+1 variables(area_use)(a__test_supply_elec)
>= 1.0

bounds
0 <= variables(area_use)(a__test_supply_elec) <= +inf
0 <= variables(area_use)(b__test_supply_elec) <= 100.0
end
9 changes: 4 additions & 5 deletions tests/common/lp_files/flow_cap.lp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ objectives(foo)(0):

s.t.

c_e_ONE_VAR_CONSTANT:
+1 ONE_VAR_CONSTANT
= 1
c_l_constraints(flow_capacity_minimum)(a__test_supply_elec__electricity)_:
+1 variables(flow_cap)(a__test_supply_elec__electricity)
>= 1.0

bounds
1 <= ONE_VAR_CONSTANT <= 1
1.0 <= variables(flow_cap)(a__test_supply_elec__electricity) <= +inf
0 <= variables(flow_cap)(a__test_supply_elec__electricity) <= +inf
0 <= variables(flow_cap)(b__test_supply_elec__electricity) <= 100.0
end
2 changes: 1 addition & 1 deletion tests/common/lp_files/flow_out_max.lp
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ bounds
0 <= variables(flow_cap)(a__test_link_a_b_heat__heat) <= 5.0
0 <= variables(flow_out)(a__test_link_a_b_heat__heat__2005_01_01_01_00) <= +inf
0 <= variables(flow_out)(a__test_supply_elec__electricity__2005_01_01_00_00) <= +inf
100.0 <= variables(flow_cap)(a__test_supply_elec__electricity) <= 100.0
0 <= variables(flow_cap)(a__test_supply_elec__electricity) <= 10.0
0 <= variables(flow_out)(a__test_supply_elec__electricity__2005_01_01_01_00) <= +inf
0 <= variables(flow_out)(b__test_link_a_b_elec__electricity__2005_01_01_00_00) <= +inf
0 <= variables(flow_cap)(b__test_link_a_b_elec__electricity) <= 10.0
Expand Down
17 changes: 17 additions & 0 deletions tests/common/lp_files/source_cap.lp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
\* Source Pyomo model name=None *\

min
objectives(foo)(0):
+1 variables(source_cap)(a__test_supply_elec)
+1 variables(source_cap)(b__test_supply_elec)

s.t.

c_l_constraints(source_capacity_minimum)(a__test_supply_elec)_:
+1 variables(source_cap)(a__test_supply_elec)
>= 1.0

bounds
0 <= variables(source_cap)(a__test_supply_elec) <= +inf
0 <= variables(source_cap)(b__test_supply_elec) <= 100.0
end
17 changes: 17 additions & 0 deletions tests/common/lp_files/storage_cap.lp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
\* Source Pyomo model name=None *\

min
objectives(foo)(0):
+1 variables(storage_cap)(a__test_supply_elec)
+1 variables(storage_cap)(b__test_supply_elec)

s.t.

c_l_constraints(storage_capacity_minimum)(a__test_supply_elec)_:
+1 variables(storage_cap)(a__test_supply_elec)
>= 1.0

bounds
0 <= variables(storage_cap)(a__test_supply_elec) <= +inf
0 <= variables(storage_cap)(b__test_supply_elec) <= 100.0
end
4 changes: 2 additions & 2 deletions tests/test_backend_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,11 @@ def test_update_variable_single_bound_multi_val(self, caplog, solved_model_func)
def test_update_variable_error_update_parameter_instead(self, solved_model_func):
"""Check that expected error is raised if trying to update a variable bound that was set by a parameter."""
with pytest.raises(calliope.exceptions.BackendError) as excinfo:
solved_model_func.backend.update_variable_bounds("flow_cap", min=1)
solved_model_func.backend.update_variable_bounds("flow_cap", max=1)
assert check_error_or_warning(
excinfo,
"Cannot update variable bounds that have been set by parameters."
" Use `update_parameter('flow_cap_min')` to update the min bound of flow_cap.",
" Use `update_parameter('flow_cap_max')` to update the max bound of flow_cap.",
)

def test_fix_variable_before_solve(self, built_model_cls_longnames):
Expand Down
68 changes: 42 additions & 26 deletions tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,54 @@ class TestBaseMath:
def base_math(self):
return read_rich_yaml(CALLIOPE_DIR / "math" / "plan.yaml")

def test_flow_cap(self, compare_lps):
self.TEST_REGISTER.add("variables.flow_cap")
@pytest.mark.parametrize(
("variable", "constraint", "overrides"),
[
("flow_cap", "flow_capacity_minimum", {}),
(
"storage_cap",
"storage_capacity_minimum",
{"techs.test_supply_elec.include_storage": True},
),
("area_use", "area_use_minimum", {}),
("source_cap", "source_capacity_minimum", {}),
],
)
def test_capacity_variables_and_bounds(
self, compare_lps, variable, constraint, overrides
):
"""Check that variables are initiated with the appropriate bounds,
and that the lower bound is updated from zero via a separate constraint if required.
"""
constraint_full = f"constraints.{constraint}"
self.TEST_REGISTER.add(f"variables.{variable}")
self.TEST_REGISTER.add(constraint_full)
model = build_test_model(
{
"nodes.b.techs.test_supply_elec.flow_cap_max": 100,
"nodes.a.techs.test_supply_elec.flow_cap_min": 1,
"nodes.a.techs.test_supply_elec.flow_cap_max": np.nan,
f"nodes.b.techs.test_supply_elec.{variable}_max": 100,
f"nodes.a.techs.test_supply_elec.{variable}_min": 1,
f"nodes.a.techs.test_supply_elec.{variable}_max": np.nan,
**overrides,
},
"simple_supply,two_hours,investment_costs",
)
custom_math = {
# need the variable defined in a constraint/objective for it to appear in the LP file bounds
"objectives": {
"foo": {
"equations": [
{
"expression": "sum(flow_cap[techs=test_supply_elec], over=[nodes, carriers])"
}
],
"sense": "minimise",
}
# Custom objective ensures that all variables appear in the LP file.
# Variables not found in either an objective or constraint will never appear in the LP.
sum_in_objective = "[nodes]" if variable != "flow_cap" else "[nodes, carriers]"
custom_objective = {
"objectives.foo": {
"equations": [
{
"expression": f"sum({variable}[techs=test_supply_elec], over={sum_in_objective})"
}
],
"sense": "minimise",
}
}
compare_lps(model, custom_math, "flow_cap")

# "flow_cap" is the name of the lp file
custom_math = AttrDict(
{constraint_full: PLAN_MATH.get_key(constraint_full), **custom_objective}
)
compare_lps(model, custom_math, variable)

def test_storage_max(self, compare_lps):
self.TEST_REGISTER.add("constraints.storage_max")
Expand All @@ -87,13 +109,7 @@ def test_storage_max(self, compare_lps):

def test_flow_out_max(self, compare_lps):
self.TEST_REGISTER.add("constraints.flow_out_max")
model = build_test_model(
{
"nodes.a.techs.test_supply_elec.flow_cap_min": 100,
"nodes.a.techs.test_supply_elec.flow_cap_max": 100,
},
"simple_supply,two_hours,investment_costs",
)
model = build_test_model({}, "simple_supply,two_hours,investment_costs")

custom_math = {
"constraints": {"flow_out_max": PLAN_MATH.constraints.flow_out_max}
Expand Down

0 comments on commit 5eafc04

Please sign in to comment.