Skip to content

Commit

Permalink
Update cost expression names & split out cost expressions (#685)
Browse files Browse the repository at this point in the history
* + remove volatile CoinCBC link from docs
  • Loading branch information
brynpickering authored Oct 15, 2024
1 parent c00486b commit b2d2dd1
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 64 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 has been removed from `model.math`, and can now be accessed via `model.math.data` (#639).

|new| (non-NaN) Default values and data types for parameters appear in math documentation (if they appear in the model definition schema) (#677).
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ This list is not exhaustive; any solvers [supported by Pyomo](https://pyomo.read

[CBC](https://github.com/coin-or/Cbc) is our recommended option if you want a free and open-source solver.
CBC can be installed via conda on Linux and macOS by running `mamba install -c conda-forge coin-or-cbc`.
Windows binary packages are somewhat more difficult to install, due to limited information on [the CBC website](https://github.com/coin-or/Cbc), but can be found within their [binary archive](https://www.coin-or.org/download/binary/Cbc/) and are included in their [package releases on GitHub](https://github.com/coin-or/Cbc/releases).
Windows binary packages are somewhat more difficult to install, due to limited information on [the CBC website](https://github.com/coin-or/Cbc), but are included in their [package releases on GitHub](https://github.com/coin-or/Cbc/releases).
The GitHub releases are more up-to-date. We recommend you download the relevant binary for [CBC 2.10.11](https://github.com/coin-or/Cbc/releases/download/releases%2F2.10.11/Cbc-releases.2.10.11-w64-msvc17-md.zip) and add `cbc.exe` to a directory known to PATH (e.g. an Anaconda environment 'bin' directory).

### GLPK
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_tables-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
10 changes: 8 additions & 2 deletions src/calliope/math/operate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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)
88 changes: 55 additions & 33 deletions src/calliope/math/plan.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,34 @@ 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.
If the modeling period is not equal to one full year, this will be scaled accordingly.
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 +912,24 @@ 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.
If the modeling period is not equal to one full year, this will be scaled accordingly.
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 +939,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: {}
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 @@ -152,12 +152,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 @@ -224,7 +223,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 @@ -154,7 +154,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 @@ -2001,7 +2006,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 b2d2dd1

Please sign in to comment.