Skip to content

Commit

Permalink
Update cost expression names & split out expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
brynpickering committed Sep 17, 2024
1 parent 3cd8caf commit 9910b0e
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 63 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### User-facing changes

|changed| cost expressions in math, to split out investment costs into the capital cost (`cost_investment`), annualised capital cost (`cost_investment_annualised`), fixed operation costs (`cost_operation_fixed`) and variable operation costs (`cost_operation_variable`, previously `cost_var`) (#645).

|new| Math component cross-references in both directions ("uses" and "used in") in Markdown math documentation (#643).

|fixed| Duplicated links in math documentation (#651).
Expand Down
6 changes: 6 additions & 0 deletions docs/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ Here are the main changes to parameter/decision variable names that are not link
* `energy_cap_min_use``flow_out_min_relative` (i.e., the value is relative to `flow_cap`).
* `parasitic_eff``flow_out_parasitic_eff`.
* `force_asynchronous_prod_con``force_async_flow`.
* `cost_var``cost_operation_variable`.
* `exists``active`.

!!! info "See also"
Expand Down Expand Up @@ -365,6 +366,11 @@ You can find an example of this change [above](#filedf-→-data_sources-section)
We have rolled the integer decision variable `units` and the binary `purchased` into one decision variable `purchased_units`.
To achieve the same functionality for `purchased`, set `purchased_units_max: 1`.

### `cost_investment``cost_investment_annualised` + `cost_operation_fixed`

Investment costs are split out into the component caused by annual operation and maintenance (`cost_operation_fixed`) and an annualised equivalent of the initial capital investment (`cost_investment_annualised`).
`cost_investment` still exists in the model results and represents the initial capital investment, i.e., without applying the economic depreciation rate.

### Explicitly triggering MILP and storage decision variables/constraints

In v0.6, we inferred that a mixed-integer linear model was desired based on the user defining certain parameters.
Expand Down
17 changes: 6 additions & 11 deletions docs/user_defined_math/examples/piecewise_linear_costs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,9 @@ global_expressions:
OR cost_investment_purchase OR piecewise_cost_investment)
equations:
- expression: >
$annualisation_weight * (
$depreciation_rate * (
sum(default_if_empty(cost_investment_flow_cap, 0), over=carriers) +
default_if_empty(cost_investment_storage_cap, 0) +
default_if_empty(cost_investment_source_cap, 0) +
default_if_empty(cost_investment_area_use, 0) +
default_if_empty(cost_investment_purchase, 0) +
default_if_empty(piecewise_cost_investment, 0)
) * (1 + cost_om_annual_investment_fraction)
+ sum(cost_om_annual * default_if_empty(flow_cap, 0), over=carriers)
)
sum(default_if_empty(cost_investment_flow_cap, 0), over=carriers) +
default_if_empty(cost_investment_storage_cap, 0) +
default_if_empty(cost_investment_source_cap, 0) +
default_if_empty(cost_investment_area_use, 0) +
default_if_empty(cost_investment_purchase, 0) +
default_if_empty(piecewise_cost_investment, 0)
3 changes: 2 additions & 1 deletion src/calliope/backend/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@ def as_array(self, array: xr.DataArray, *, over: str | list[str]) -> xr.DataArra
NaNs are ignored (xarray.DataArray.sum arg: `skipna: True`) and if all values along the dimension(s) are NaN,
the summation will lead to a NaN (xarray.DataArray.sum arg: `min_count=1`).
"""
return array.sum(over, min_count=1, skipna=True)
filtered_over = set(self._listify(over)).intersection(array.dims)
return array.sum(filtered_over, min_count=1, skipna=True)


class ReduceCarrierDim(ParsingHelperFunction):
Expand Down
86 changes: 53 additions & 33 deletions src/calliope/math/base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ constraints:
slices:
final_step:
- expression: get_val_at_index(timesteps=-1)
active: true # optional; defaults to true.
active: true # optional; defaults to true.
# --8<-- [end:constraint]

balance_transmission:
Expand All @@ -279,7 +279,7 @@ constraints:

symmetric_transmission:
description: >-
Fix the flow capacity of two `transmission` technologies representing the same link in the system.
Fix the flow capacity of two `transmission` technologies representing the same link in the system.
foreach: [nodes, techs]
where: "base_tech=transmission"
equations:
Expand Down Expand Up @@ -599,11 +599,11 @@ variables:
unit: energy
foreach: [nodes, techs]
where: "include_storage=True OR base_tech=storage"
domain: real # optional; defaults to real.
domain: real # optional; defaults to real.
bounds:
min: storage_cap_min
max: storage_cap_max
active: true # optional; defaults to true.
active: true # optional; defaults to true.
# --8<-- [end:variable]

storage:
Expand Down Expand Up @@ -721,7 +721,6 @@ variables:
min: -.inf
max: 0


objectives:
# --8<-- [start:objective]
min_cost_optimisation:
Expand Down Expand Up @@ -754,7 +753,7 @@ objectives:
- where: "NOT config.ensure_feasibility=True"
expression: "0"
sense: minimise
active: true # optional; defaults to true.
active: true # optional; defaults to true.
# --8<-- [end:objective]

global_expressions:
Expand Down Expand Up @@ -788,7 +787,7 @@ global_expressions:
- where: NOT base_tech=transmission
expression: flow_in * flow_in_eff

cost_var:
cost_operation_variable:
title: Variable operating costs
description: >-
The operating costs per timestep of a technology.
Expand Down Expand Up @@ -874,27 +873,33 @@ global_expressions:
cost_investment:
title: Total investment costs
description: >-
The installation costs of a technology, including annualised investment costs and annual maintenance costs.
The installation costs of a technology, including those linked to the nameplate capacity, land use, storage size, and binary/integer unit purchase.
default: 0
unit: cost
foreach: [nodes, techs, costs]
where: >-
cost_investment_flow_cap OR cost_investment_storage_cap OR cost_investment_source_cap OR
cost_investment_area_use OR cost_investment_purchase
equations:
- expression: >
$annualisation_weight * (
($depreciation_rate + cost_om_annual_investment_fraction) * (
sum(default_if_empty(cost_investment_flow_cap, 0), over=carriers) +
default_if_empty(cost_investment_storage_cap, 0) +
default_if_empty(cost_investment_source_cap, 0) +
default_if_empty(cost_investment_area_use, 0) +
default_if_empty(cost_investment_purchase, 0)
)
+ sum(cost_om_annual * flow_cap, over=carriers)
)
- expression: >-
sum(default_if_empty(cost_investment_flow_cap, 0), over=carriers) +
default_if_empty(cost_investment_storage_cap, 0) +
default_if_empty(cost_investment_source_cap, 0) +
default_if_empty(cost_investment_area_use, 0) +
default_if_empty(cost_investment_purchase, 0)
cost_investment_annualised:
title: Equivalent annual investment costs
description: >-
An annuity factor has been applied to scale lifetime investment costs to annual values that can be directly compared to operation costs.
default: 0
unit: cost
foreach: [nodes, techs, costs]
where: cost_investment
equations:
- expression: $annualisation_weight * $depreciation_rate * cost_investment
sub_expressions:
annualisation_weight:
annualisation_weight: &annualisation_weight
- expression: sum(timestep_resolution * timestep_weights, over=timesteps) / 8760
depreciation_rate:
- where: cost_depreciation_rate
Expand All @@ -906,6 +911,23 @@ global_expressions:
(cost_interest_rate * ((1 + cost_interest_rate) ** lifetime)) /
(((1 + cost_interest_rate) ** lifetime) - 1)
cost_operation_fixed:
title: Total fixed operation costs
description: >-
The fixed, annual operation costs of a technology, which are calculated relative to investment costs.
default: 0
unit: cost
foreach: [nodes, techs, costs]
where: cost_investment AND (cost_om_annual OR cost_om_annual_investment_fraction)
equations:
- expression: >-
$annualisation_weight * (
sum(cost_om_annual * flow_cap, over=carriers) +
cost_investment * cost_om_annual_investment_fraction
)
sub_expressions:
annualisation_weight: *annualisation_weight

# --8<-- [start:expression]
cost:
title: Total costs
Expand All @@ -915,21 +937,19 @@ global_expressions:
default: 0
unit: cost
foreach: [nodes, techs, costs]
where: "cost_investment OR cost_var"
where: "cost_investment_annualised OR cost_operation_variable OR cost_operation_fixed"
equations:
- expression: $cost_investment + $cost_var_sum
- expression: >-
default_if_empty(cost_investment_annualised, 0) +
$cost_operation_sum +
default_if_empty(cost_operation_fixed, 0)
sub_expressions:
cost_investment:
- where: "cost_investment"
expression: cost_investment
- where: "NOT cost_investment"
expression: "0"
cost_var_sum:
- where: "cost_var"
expression: sum(cost_var, over=timesteps)
- where: "NOT cost_var"
cost_operation_sum:
- where: "cost_operation_variable"
expression: sum(cost_operation_variable, over=timesteps)
- where: "NOT cost_operation_variable"
expression: "0"
active: true # optional; defaults to true.
active: true # optional; defaults to true.
# --8<-- [end:expression]

piecewise_constraints: {}
piecewise_constraints: {}
10 changes: 8 additions & 2 deletions src/calliope/math/operate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ variables:

global_expressions:
cost_investment.active: false
cost_investment_annualised.active: false
cost_investment_flow_cap.active: false
cost_investment_storage_cap.active: false
cost_investment_source_cap.active: false
cost_investment_area_use.active: false
cost_investment_purchase.active: false
cost:
where: "cost_export OR cost_flow_in OR cost_flow_out"
where: "cost_operation_variable"
equations:
- expression: $cost_var_sum
- expression: sum(cost_operation_variable, over=timesteps)
File renamed without changes.
5 changes: 2 additions & 3 deletions tests/test_backend_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,11 @@ def test_get_variable_obj_type(self, variable):
"""Check a decision variable has the correct obj_type."""
assert variable.attrs["obj_type"] == "variables"

def test_get_variable_refs(self, variable):
def test_get_variable_refs(self, variable, solved_model_cls):
"""Check a decision variable has all expected references to other math components."""
assert variable.attrs["references"] == {
"flow_in_max",
"flow_out_max",
"cost_investment",
"cost_investment_flow_cap",
"symmetric_transmission",
}
Expand Down Expand Up @@ -223,7 +222,7 @@ def test_get_global_expression_obj_type(self, global_expression):

def test_get_global_expression_refs(self, global_expression):
"""Check a global expression has all expected math component refs."""
assert global_expression.attrs["references"] == {"cost"}
assert global_expression.attrs["references"] == {"cost_investment_annualised"}

def test_get_global_expression_default(self, global_expression):
"""Check a global expression has expected default."""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_backend_gurobi.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_verbose_strings_expression(self, simple_supply_longnames):

assert "flow_cap[a, test_supply_elec, electricity]" in obj.sel(dims).item()
# parameters are not gurobi objects, so we don't get their names in our strings
assert "parameters[cost_interest_rate]" not in obj.sel(dims).item()
assert "cost_flow_cap" not in obj.sel(dims).item()

assert not obj.coords_in_name

Expand Down
24 changes: 16 additions & 8 deletions tests/test_backend_pyomo.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ def test_loc_techs_cost_investment_milp_constraint(self):

def test_loc_techs_not_cost_var_constraint(self, simple_conversion):
"""I for i in sets.loc_techs_om_cost if i not in sets.loc_techs_conversion_plus + sets.loc_techs_conversion"""
assert "cost_var" not in simple_conversion.backend.expressions
assert "cost_operation_variable" not in simple_conversion.backend.expressions

@pytest.mark.parametrize(
("tech", "scenario", "cost"),
Expand All @@ -502,7 +502,7 @@ def test_loc_techs_cost_var_constraint(self, tech, scenario, cost):
{f"techs.{tech}.costs.monetary.{cost}": 1}, f"{scenario},two_hours"
)
m.build()
assert "cost_var" in m.backend.expressions
assert "cost_operation_variable" in m.backend.expressions

def test_one_way_om_cost(self):
"""With one_way transmission, it should still be possible to set an flow_out cost."""
Expand All @@ -521,13 +521,17 @@ def test_one_way_om_cost(self):
"timesteps": m.backend._dataset.timesteps[1],
}
assert check_variable_exists(
m.backend.get_expression("cost_var", as_backend_objs=False), "flow_out", idx
m.backend.get_expression("cost_operation_variable", as_backend_objs=False),
"flow_out",
idx,
)

idx["nodes"] = "a"
idx["techs"] = "test_transmission_elec:b"
assert not check_variable_exists(
m.backend.get_expression("cost_var", as_backend_objs=False), "flow_out", idx
m.backend.get_expression("cost_operation_variable", as_backend_objs=False),
"flow_out",
idx,
)


Expand Down Expand Up @@ -560,17 +564,18 @@ def test_loc_tech_carriers_export_balance_constraint(self, supply_export):

def test_loc_techs_update_costs_var_constraint(self, supply_export):
"""I for i in sets.loc_techs_om_cost if i in sets.loc_techs_export"""
assert "cost_var" in supply_export.backend.expressions
assert "cost_operation_variable" in supply_export.backend.expressions

m = build_model(
{"techs.test_supply_elec.costs.monetary.flow_out": 0.1},
"supply_export,two_hours,investment_costs",
)
m.build()
assert "cost_var" in m.backend.expressions
assert "cost_operation_variable" in m.backend.expressions

assert check_variable_exists(
m.backend.get_expression("cost_var", as_backend_objs=False), "flow_export"
m.backend.get_expression("cost_operation_variable", as_backend_objs=False),
"flow_export",
)

def test_loc_tech_carriers_export_max_constraint(self):
Expand Down Expand Up @@ -2026,7 +2031,10 @@ def test_verbose_strings_expression(self, simple_supply_longnames):
"variables[flow_cap][a, test_supply_elec, electricity]"
in obj.sel(dims).item()
)
assert "parameters[cost_interest_rate]" in obj.sel(dims).item()
assert (
"parameters[cost_flow_cap][test_supply_elec, monetary]"
in obj.sel(dims).item()
)

assert not obj.coords_in_name

Expand Down
8 changes: 4 additions & 4 deletions tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ def test_balance_storage(self, compare_lps):
compare_lps(model, custom_math, "balance_storage")

@pytest.mark.parametrize("with_export", [True, False])
def test_cost_var_with_export(self, compare_lps, with_export):
def test_cost_operation_variable(self, compare_lps, with_export):
"""Test variable costs in the objective."""
self.TEST_REGISTER.add("global_expressions.cost_var")
self.TEST_REGISTER.add("global_expressions.cost_operation_variable")
override = {
"techs.test_conversion_plus.cost_flow_out": {
"data": [1, 2],
Expand Down Expand Up @@ -195,15 +195,15 @@ def test_cost_var_with_export(self, compare_lps, with_export):
"foo": {
"equations": [
{
"expression": "sum(cost_var, over=[nodes, techs, costs, timesteps])"
"expression": "sum(cost_operation_variable, over=[nodes, techs, costs, timesteps])"
}
],
"sense": "minimise",
}
}
}
suffix = "_with_export" if with_export else ""
compare_lps(model, custom_math, f"cost_var{suffix}")
compare_lps(model, custom_math, f"cost_operation_variable{suffix}")

@pytest.mark.xfail(reason="not all base math is in the test config dict yet")
def test_all_math_registered(self, base_math):
Expand Down

0 comments on commit 9910b0e

Please sign in to comment.