Skip to content

Commit 9e2b54e

Browse files
committed
Add algorithms
1 parent a13119f commit 9e2b54e

File tree

3 files changed

+107
-20
lines changed

3 files changed

+107
-20
lines changed

src/calliope/model.py

+83-17
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from __future__ import annotations
66

77
import logging
8+
import random
9+
from collections.abc import Callable
810
from pathlib import Path
911
from typing import TYPE_CHECKING
1012

13+
import numpy as np
1114
import pandas as pd
1215
import xarray as xr
1316

@@ -611,7 +614,7 @@ def _solve_spores(self, solver_config: config_schema.Solve) -> xr.Dataset:
611614
spore_range = range(1, spores_config.number + 1)
612615
for spore in spore_range:
613616
LOGGER.info(f"Optimisation model | Running SPORE {spore}.")
614-
self._spores_update_model(baseline_results, results_list[-1], spores_config)
617+
self._spores_update_model(baseline_results, results_list, spores_config)
615618

616619
iteration_results = self.backend._solve(solver_config, warmstart=False)
617620
results_list.append(iteration_results)
@@ -633,9 +636,86 @@ def _solve_spores(self, solver_config: config_schema.Solve) -> xr.Dataset:
633636
def _spores_update_model(
634637
self,
635638
baseline_results: xr.Dataset,
636-
previous_results: xr.Dataset,
639+
all_previous_results: list[xr.Dataset],
637640
spores_config: config_schema.SolveSpores,
638641
):
642+
def _score_integer() -> xr.DataArray:
643+
# Look at capacity deployment in the previous iteration
644+
previous_cap = latest_results["flow_cap"].where(spores_techs)
645+
646+
# Make sure that penalties are applied only to non-negligible deployments of capacity
647+
min_relevant_size = spores_config.score_threshold_factor * previous_cap.max(
648+
["nodes", "techs"]
649+
)
650+
651+
new_score = (
652+
# Where capacity was deployed more than the minimal relevant size, assign an integer penalty (score)
653+
previous_cap.where(previous_cap > min_relevant_size)
654+
.clip(min=1, max=1)
655+
.fillna(0)
656+
.where(spores_techs)
657+
)
658+
return new_score
659+
660+
def _score_relative_deployment() -> xr.DataArray:
661+
# Look at capacity deployment in the previous iteration
662+
previous_cap = latest_results["flow_cap"].where(spores_techs)
663+
664+
# Look at capacity deployment in the previous iteration
665+
relative_cap = previous_cap / self.inputs["flow_cap_max"].where(
666+
spores_techs
667+
)
668+
669+
# Make sure that penalties are applied only to non-negligible deployments of capacity
670+
min_relevant_size = spores_config.score_threshold_factor * relative_cap
671+
672+
new_score = (
673+
# Where capacity was deployed more than the minimal relevant size, assign the relative deployment as a penalty (score)
674+
relative_cap.where(previous_cap > min_relevant_size)
675+
.fillna(0)
676+
.where(spores_techs)
677+
)
678+
return new_score.to_pandas()
679+
680+
def _score_random() -> xr.DataArray:
681+
previous_cap = latest_results["flow_cap"].where(spores_techs)
682+
new_score = (
683+
previous_cap.fillna(0)
684+
.where(previous_cap.isnull(), other=lambda x: random.random())
685+
.where(spores_techs)
686+
)
687+
688+
return new_score
689+
690+
def _score_evolving_average() -> xr.DataArray:
691+
previous_cap = latest_results["flow_cap"]
692+
evolving_average = sum(
693+
results["flow_cap"] for results in all_previous_results
694+
) / len(all_previous_results)
695+
696+
relative_change = abs(evolving_average - previous_cap) / evolving_average
697+
# first iteration
698+
if relative_change.sum() == 0:
699+
# first iteration
700+
new_score = _score_integer()
701+
else:
702+
# If capacity is exactly the same as the average, we give the relative difference an arbitrarily small value
703+
relative_change = (
704+
relative_change.clip(min=0.001)
705+
.where(relative_change != np.inf, other=0)
706+
.where(spores_techs)
707+
)
708+
new_score = relative_change**-1
709+
710+
return new_score
711+
712+
latest_results = all_previous_results[-1]
713+
allowed_methods: dict[str, Callable[[], xr.DataArray]] = {
714+
"integer": _score_integer,
715+
"relative_deployment": _score_relative_deployment,
716+
"random": _score_random,
717+
"evolving_average": _score_evolving_average,
718+
}
639719
# Update the slack-cost backend parameter based on the calculated minimum feasible system design cost
640720
constraining_cost = baseline_results.cost.groupby("costs").sum(..., min_count=1)
641721
self.backend.update_parameter("spores_baseline_cost", constraining_cost)
@@ -647,22 +727,8 @@ def _spores_update_model(
647727
).notnull()
648728
& self.inputs.definition_matrix
649729
)
730+
new_score = allowed_methods[spores_config.scoring_algorithm]()
650731

651-
# Look at capacity deployment in the previous iteration
652-
previous_cap = previous_results["flow_cap"].where(spores_techs)
653-
654-
# Make sure that penalties are applied only to non-negligible deployments of capacity
655-
min_relevant_size = spores_config.score_threhsold_factor * previous_cap.max(
656-
["nodes", "techs"]
657-
)
658-
659-
new_score = (
660-
# Where capacity was deployed more than the minimal relevant size, assign an integer penalty (score)
661-
previous_cap.where(previous_cap > min_relevant_size)
662-
.clip(min=1, max=1)
663-
.fillna(0)
664-
.where(spores_techs)
665-
)
666732
new_score += self.backend.get_parameter(
667733
"spores_score", as_backend_objs=False
668734
).fillna(0)

src/calliope/schemas/config_schema.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ class SolveSpores(CalliopeBaseModel):
130130
"""SPORES configuration options used when solving a Calliope optimisation problem (`calliope.Model.solve`)."""
131131

132132
model_config = {"title": "Model solve SPORES mode configuration"}
133+
134+
scoring_algorithm: Literal[
135+
"integer", "relative_deployment", "random", "evolving_average"
136+
] = "integer"
137+
"""Algorithm to apply to update the SPORES score between iterations."""
138+
133139
number: int = Field(default=3)
134140
"""SPORES mode number of iterations after the initial base run."""
135141

@@ -145,7 +151,7 @@ class SolveSpores(CalliopeBaseModel):
145151
tracking_parameter: str | None = None
146152
"""If given, an input parameter name with which to filter technologies for consideration in SPORES scoring."""
147153

148-
score_threhsold_factor: float = Field(default=0.1, ge=0)
154+
score_threshold_factor: float = Field(default=0.1, ge=0)
149155
"""A factor to apply to flow capacities above which they will increment the SPORES score.
150156
E.g., if the previous iteration flow capacity was `100` then, with a threshold value of 0.1,
151157
only capacities above `10` in the current iteration will cause the SPORES score to increase for that technology at that node.

tests/test_core_model.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,21 @@ def spores_model_and_log(self, request):
193193

194194
return model, log
195195

196+
@pytest.fixture(
197+
scope="class",
198+
params=["integer", "relative_deployment", "random", "evolving_average"],
199+
)
200+
def spores_model_and_log_algorithms(self, request):
201+
"""Iterate 2 times in SPORES mode using different scoring algorithms."""
202+
model = build_model({}, "spores,investment_costs")
203+
model.build(mode="spores")
204+
with self.caplog_session(request) as caplog:
205+
with caplog.at_level(logging.INFO):
206+
model.solve(**{"spores.scoring_algorithm": request.param})
207+
log = caplog.text
208+
209+
return model, log
210+
196211
@pytest.fixture(scope="class")
197212
def spores_model_skip_baseline_and_log(self, request):
198213
"""Iterate 2 times in SPORES mode having pre-computed the baseline results."""
@@ -244,9 +259,9 @@ def test_backend_build_mode(self, spores_model_and_log):
244259
spores_model, _ = spores_model_and_log
245260
assert spores_model.backend.config.mode == "spores"
246261

247-
def test_spores_mode_success(self, spores_model_and_log):
262+
def test_spores_mode_success(self, spores_model_and_log_algorithms):
248263
"""Solving in spores mode should lead to an optimal solution."""
249-
spores_model, _ = spores_model_and_log
264+
spores_model, _ = spores_model_and_log_algorithms
250265
assert spores_model.results.attrs["termination_condition"] == "optimal"
251266

252267
def test_spores_mode_3_results(self, spores_model_and_log):

0 commit comments

Comments
 (0)