From 3c1e588c5058859fc7a418f98c6bdc5162f6cfb0 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Wed, 5 Jun 2024 17:23:16 +0100 Subject: [PATCH 01/15] Reapply "Enforce minimum service factor limits" This reverts commit 320738cf8fa6c6b8bcc16a84cec7771b9eba091f. --- src/muse/constraints.py | 14 +++++++------- src/muse/readers/csv.py | 15 +++++++++++++-- tests/test_minimum_service.py | 29 +++++++++++++++-------------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 81f76b251..b75c08f30 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -1,6 +1,6 @@ r"""Investment constraints. -Constraints on investements ensure that investements match some given criteria. For +Constraints on investments ensure that investments match some given criteria. For instance, the constraints could ensure that only so much of a new asset can be built every year. @@ -8,7 +8,7 @@ :py:meth:`~muse.constraints.register_constraints`. This registration step makes it possible for constraints to be declared in the TOML file. -Generally, LP solvers accept linear constraint defined as: +Generally, LP solvers accept linear constraints defined as: .. math:: @@ -16,8 +16,8 @@ with :math:`A` a matrix, :math:`x` the decision variables, and :math:`b` a vector. However, these quantities are dimensionless. They do no have timeslices, assets, or -replacement technologies, or any other dimensions that users have set-up in their model. -The crux is to translates from MUSE's data-structures to a consistent dimensionless +replacement technologies, or any other dimensions that users have set up in their model. +The crux is to translate from MUSE's data-structures to a consistent dimensionless format. In MUSE, users can register constraints functions that return fully dimensional @@ -44,8 +44,8 @@ - Any dimension in :math:`A_c .* x_c` (:math:`A_p .* x_p`) that is also in :math:`b` defines diagonal entries into the left (right) submatrix of :math:`A`. - Any dimension in :math:`A_c .* x_c` (:math:`A_p .* x_b`) and missing from - :math:`b` is reduce by summation over a row in the left (right) submatrix of - :math:`A`. In other words, those dimension do become part of a standard tensor + :math:`b` is reduced by summation over a row in the left (right) submatrix of + :math:`A`. In other words, those dimensions become part of a standard tensor reduction or matrix multiplication. There are two additional rules. However, they are likely to be the result of an @@ -281,7 +281,7 @@ def max_capacity_expansion( :math:`y=y_1` is the year marking the end of the investment period. Let :math:`\mathcal{A}^{i, r}_{t, \iota}(y)` be the current assets, before - invesment, and let :math:`\Delta\mathcal{A}^{i,r}_t` be the future investements. + investment, and let :math:`\Delta\mathcal{A}^{i,r}_t` be the future investments. The the constraint on agent :math:`i` are given as: .. math:: diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index d84468376..24e907bfa 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -130,6 +130,9 @@ def to_agent_share(name): if "year" in result.dims and len(result.year) == 1: result = result.isel(year=0, drop=True) + + check_minimum_service_factors_in_range(data, filename) + return result @@ -145,7 +148,7 @@ def read_technodata_timeslices(filename: Union[str, Path]) -> xr.Dataset: data = csv[csv.technology != "Unit"] data = data.apply(to_numeric) - data = check_utilization_not_all_zero(data, filename) + check_utilization_not_all_zero(data, filename) ts = pd.MultiIndex.from_frame( data.drop( @@ -934,4 +937,12 @@ def check_utilization_not_all_zero(data, filename): f"""A technology can not have a utilization factor of 0 for every timeslice. Please check file {filename}.""" ) - return data + + +def check_minimum_service_factors_in_range(data, filename): + min_service_factor = data["minimum_service_factor"] + if not np.all((0 <= min_service_factor) & (min_service_factor <= 1)): + raise ValueError( + f"""Minimum service factor values must all be between 0 and 1 inclusive. + Please check file {filename}.""" + ) diff --git a/tests/test_minimum_service.py b/tests/test_minimum_service.py index ded2b50d1..2064cf869 100644 --- a/tests/test_minimum_service.py +++ b/tests/test_minimum_service.py @@ -1,8 +1,11 @@ +from itertools import permutations + +import numpy as np from pytest import mark def modify_minimum_service_factors( - model_path, sector, process_name, minimum_service_factor + model_path, sector, processes, minimum_service_factors ): import pandas as pd @@ -10,27 +13,25 @@ def modify_minimum_service_factors( model_path / "technodata" / sector / "TechnodataTimeslices.csv" ) - technodata_timeslices.loc[ - technodata_timeslices["ProcessName"] == process_name[0], "MinimumServiceFactor" - ] = minimum_service_factor[0] - - technodata_timeslices.loc[ - technodata_timeslices["ProcessName"] == process_name[1], "MinimumServiceFactor" - ] = minimum_service_factor[1] + for process, minimum in zip(processes, minimum_service_factors): + technodata_timeslices.loc[ + technodata_timeslices["ProcessName"] == process, "MinimumServiceFactor" + ] = minimum return technodata_timeslices -@mark.parametrize("process_name", [("gasCCGT", "windturbine")]) @mark.parametrize( - "minimum_service_factor", [([1, 2, 3, 4, 5, 6], [0] * 6), ([0], [1, 2, 3, 4, 5, 6])] + "minimum_service_factors", + permutations((np.linspace(0, 1, 6), [0] * 6)), ) -def test_minimum_service_factor(tmpdir, minimum_service_factor, process_name): +def test_minimum_service_factor(tmpdir, minimum_service_factors): import pandas as pd from muse import examples from muse.mca import MCA sector = "power" + processes = ("gasCCGT", "windturbine") # Copy the model inputs to tmpdir model_path = examples.copy_model( @@ -40,8 +41,8 @@ def test_minimum_service_factor(tmpdir, minimum_service_factor, process_name): technodata_timeslices = modify_minimum_service_factors( model_path=model_path, sector=sector, - process_name=process_name, - minimum_service_factor=minimum_service_factor, + processes=processes, + minimum_service_factors=minimum_service_factors, ) technodata_timeslices.to_csv( @@ -53,7 +54,7 @@ def test_minimum_service_factor(tmpdir, minimum_service_factor, process_name): supply_timeslice = pd.read_csv(tmpdir / "Results/MCAMetric_Supply.csv") - for process, service_factor in zip(process_name, minimum_service_factor): + for process, service_factor in zip(processes, minimum_service_factors): for i, factor in enumerate(service_factor): assert ( supply_timeslice[ From 5418078f68133ff3d5f13f651d3014ab09e01a05 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 15:52:44 +0100 Subject: [PATCH 02/15] Fix: Skip minimum service factor check if not used --- src/muse/readers/csv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 24e907bfa..ad2ab5189 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -940,7 +940,12 @@ def check_utilization_not_all_zero(data, filename): def check_minimum_service_factors_in_range(data, filename): - min_service_factor = data["minimum_service_factor"] + try: + min_service_factor = data["minimum_service_factor"] + except KeyError: + # Minimum service factor not specified, so we don't need to check it + return + if not np.all((0 <= min_service_factor) & (min_service_factor <= 1)): raise ValueError( f"""Minimum service factor values must all be between 0 and 1 inclusive. From 61d6352600230cd80f6b7048e8ec30a30ac9665a Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 18 Jun 2024 16:10:54 +0100 Subject: [PATCH 03/15] Typo --- src/muse/readers/csv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index ad2ab5189..17cccbc42 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -272,7 +272,7 @@ def read_technologies( Arguments: technodata_path_or_sector: If `comm_out_path` and `comm_in_path` are not given, then this argument refers to the name of the sector. The three paths are - then determined using standard locations and name. Specifically, thechnodata + then determined using standard locations and name. Specifically, technodata looks for a "technodataSECTORNAME.csv" file in the standard location for that sector. However, if `comm_out_path` and `comm_in_path` are given, then this should be the path to the the technodata file. From c5f779d52bc0fb272aff54eecd08971c757681b3 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 18 Jun 2024 17:35:46 +0100 Subject: [PATCH 04/15] read_technodictionary(): Also check factor utilization is non-zero --- src/muse/readers/csv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 17cccbc42..67ecbe8c8 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -98,9 +98,10 @@ def to_agent_share(name): data.columns.name = "technodata" data.index.name = "technology" data = data.drop(["process_name", "region_name", "time"], axis=1) - data = data.apply(to_numeric, axis=0) + check_utilization_not_all_zero(data, filename) + result = xr.Dataset.from_dataframe(data.sort_index()) if "fuel" in result.variables: result["fuel"] = result.fuel.isel(region=0, year=0) From 2d40a615c325b386f28f1322973d583f4c0f41f6 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 15:56:21 +0100 Subject: [PATCH 05/15] Check minimum service factors in read_technodata_timeslices too --- src/muse/readers/csv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 67ecbe8c8..8bfd7d1fa 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -150,6 +150,7 @@ def read_technodata_timeslices(filename: Union[str, Path]) -> xr.Dataset: data = data.apply(to_numeric) check_utilization_not_all_zero(data, filename) + check_minimum_service_factors_in_range(data, filename) ts = pd.MultiIndex.from_frame( data.drop( From 454c86d96ee36ca9ce9d5d6198e6dcb96f213201 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 15:56:50 +0100 Subject: [PATCH 06/15] Add test for minimum service factor check in read_technodata_timeslices --- tests/test_minimum_service.py | 36 +++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_minimum_service.py b/tests/test_minimum_service.py index 2064cf869..89c7bc09f 100644 --- a/tests/test_minimum_service.py +++ b/tests/test_minimum_service.py @@ -1,7 +1,7 @@ -from itertools import permutations +from itertools import chain, permutations import numpy as np -from pytest import mark +from pytest import mark, raises def modify_minimum_service_factors( @@ -64,3 +64,35 @@ def test_minimum_service_factor(tmpdir, minimum_service_factors): ].supply >= factor ).all() + + +@mark.parametrize( + "minimum_service_factors", + chain.from_iterable(map(permutations, ((-1, 0), (2, 0), (float("nan"), 0)))), +) +def test_minimum_service_factor_invalid_input(tmpdir, minimum_service_factors): + from muse import examples + from muse.mca import MCA + + sector = "power" + processes = ("gasCCGT", "windturbine") + + # Copy the model inputs to tmpdir + model_path = examples.copy_model( + name="default_timeslice", path=tmpdir, overwrite=True + ) + + technodata_timeslices = modify_minimum_service_factors( + model_path=model_path, + sector=sector, + processes=processes, + minimum_service_factors=minimum_service_factors, + ) + + technodata_timeslices.to_csv( + model_path / "technodata" / sector / "TechnodataTimeslices.csv", index=False + ) + + with raises(ValueError): + with tmpdir.as_cwd(): + MCA.factory(model_path / "settings.toml").run() From fc963bc089b8ec2c7fcc767f0a94950b6ad093f2 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 16:10:18 +0100 Subject: [PATCH 07/15] Add tests for check_utilization_not_all_zero --- tests/test_readers.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_readers.py b/tests/test_readers.py index ddf986276..d9500c5de 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -410,3 +410,52 @@ def test_read_trade_technodata(tmp_path): "max_capacity_growth", "total_capacity_limit", } + + +def test_check_utilization_not_all_zero_success(): + import pandas as pd + from muse.readers.csv import check_utilization_not_all_zero + + df = pd.DataFrame( + { + "utilization_factor": (0, 1, 1), + "technology": ("gas", "gas", "solar"), + "region": ("GB", "GB", "FR"), + "year": (2010, 2010, 2011), + } + ) + check_utilization_not_all_zero(df, "file.csv") + + +def test_check_utilization_not_all_zero_fail_all_zero(): + import pandas as pd + from muse.readers.csv import check_utilization_not_all_zero + + df = pd.DataFrame( + { + "utilization_factor": (0, 0, 1), + "technology": ("gas", "gas", "solar"), + "region": ("GB", "GB", "FR"), + "year": (2010, 2010, 2011), + } + ) + + with raises(ValueError): + check_utilization_not_all_zero(df, "file.csv") + + +def test_check_utilization_not_all_zero_fail_missing_column(): + import pandas as pd + from muse.readers.csv import check_utilization_not_all_zero + + # NB: Required utilization_factor column is missing + df = pd.DataFrame( + { + "technology": ("gas", "gas", "solar"), + "region": ("GB", "GB", "FR"), + "year": (2010, 2010, 2011), + } + ) + + with raises(ValueError): + check_utilization_not_all_zero(df, "file.csv") From 4c10462d3764353b584e37c58b4838ea582bb299 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 16:18:10 +0100 Subject: [PATCH 08/15] Add tests for check_minimum_service_factors_in_range --- tests/test_readers.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_readers.py b/tests/test_readers.py index d9500c5de..e70c68f91 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -1,3 +1,4 @@ +from itertools import chain, permutations from pathlib import Path import toml @@ -459,3 +460,34 @@ def test_check_utilization_not_all_zero_fail_missing_column(): with raises(ValueError): check_utilization_not_all_zero(df, "file.csv") + + +def test_check_minimum_service_factors_in_range_success(): + import pandas as pd + from muse.readers.csv import check_minimum_service_factors_in_range + + df = pd.DataFrame({"minimum_service_factor": (0, 1)}) + check_minimum_service_factors_in_range(df, "file.csv") + + +def test_check_minimum_service_factors_in_range_column_missing(): + import pandas as pd + from muse.readers.csv import check_minimum_service_factors_in_range + + # If the minimum_service_factor column is missing, the function should just return + # without raising an error + df = pd.DataFrame() + check_minimum_service_factors_in_range(df, "file.csv") + + +@mark.parametrize( + "values", chain.from_iterable(permutations((0, bad)) for bad in (-1, 2)) +) +def test_check_minimum_service_factors_in_range_fail(values): + import pandas as pd + from muse.readers.csv import check_minimum_service_factors_in_range + + df = pd.DataFrame({"minimum_service_factor": values}) + + with raises(ValueError): + check_minimum_service_factors_in_range(df, "file.csv") From f4199af145576a516660a4e68959cd349e87fb82 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 16:28:46 +0100 Subject: [PATCH 09/15] Make higher-level function for utilisation and minimum service factor checks --- src/muse/readers/csv.py | 16 +++++----- tests/test_readers.py | 69 +++++++++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 8bfd7d1fa..699318f2f 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -100,7 +100,7 @@ def to_agent_share(name): data = data.drop(["process_name", "region_name", "time"], axis=1) data = data.apply(to_numeric, axis=0) - check_utilization_not_all_zero(data, filename) + check_utilization_and_minimum_service_factors(data, filename) result = xr.Dataset.from_dataframe(data.sort_index()) if "fuel" in result.variables: @@ -132,8 +132,6 @@ def to_agent_share(name): if "year" in result.dims and len(result.year) == 1: result = result.isel(year=0, drop=True) - check_minimum_service_factors_in_range(data, filename) - return result @@ -149,8 +147,7 @@ def read_technodata_timeslices(filename: Union[str, Path]) -> xr.Dataset: data = csv[csv.technology != "Unit"] data = data.apply(to_numeric) - check_utilization_not_all_zero(data, filename) - check_minimum_service_factors_in_range(data, filename) + check_utilization_and_minimum_service_factors(data, filename) ts = pd.MultiIndex.from_frame( data.drop( @@ -925,13 +922,18 @@ def read_finite_resources(path: Union[str, Path]) -> xr.DataArray: return xr.Dataset.from_dataframe(data).to_array(dim="commodity") -def check_utilization_not_all_zero(data, filename): +def check_utilization_and_minimum_service_factors(data, filename): if "utilization_factor" not in data.columns: raise ValueError( f"""A technology needs to have a utilization factor defined for every timeslice. Please check file {filename}.""" ) + _check_utilization_not_all_zero(data, filename) + _check_minimum_service_factors_in_range(data, filename) + + +def _check_utilization_not_all_zero(data, filename): utilization_sum = data.groupby(["technology", "region", "year"]).sum() if (utilization_sum.utilization_factor == 0).any(): @@ -941,7 +943,7 @@ def check_utilization_not_all_zero(data, filename): ) -def check_minimum_service_factors_in_range(data, filename): +def _check_minimum_service_factors_in_range(data, filename): try: min_service_factor = data["minimum_service_factor"] except KeyError: diff --git a/tests/test_readers.py b/tests/test_readers.py index e70c68f91..3d589275d 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -1,5 +1,6 @@ from itertools import chain, permutations from pathlib import Path +from unittest.mock import patch import toml import xarray as xr @@ -415,7 +416,7 @@ def test_read_trade_technodata(tmp_path): def test_check_utilization_not_all_zero_success(): import pandas as pd - from muse.readers.csv import check_utilization_not_all_zero + from muse.readers.csv import _check_utilization_not_all_zero df = pd.DataFrame( { @@ -425,12 +426,12 @@ def test_check_utilization_not_all_zero_success(): "year": (2010, 2010, 2011), } ) - check_utilization_not_all_zero(df, "file.csv") + _check_utilization_not_all_zero(df, "file.csv") def test_check_utilization_not_all_zero_fail_all_zero(): import pandas as pd - from muse.readers.csv import check_utilization_not_all_zero + from muse.readers.csv import _check_utilization_not_all_zero df = pd.DataFrame( { @@ -442,42 +443,25 @@ def test_check_utilization_not_all_zero_fail_all_zero(): ) with raises(ValueError): - check_utilization_not_all_zero(df, "file.csv") - - -def test_check_utilization_not_all_zero_fail_missing_column(): - import pandas as pd - from muse.readers.csv import check_utilization_not_all_zero - - # NB: Required utilization_factor column is missing - df = pd.DataFrame( - { - "technology": ("gas", "gas", "solar"), - "region": ("GB", "GB", "FR"), - "year": (2010, 2010, 2011), - } - ) - - with raises(ValueError): - check_utilization_not_all_zero(df, "file.csv") + _check_utilization_not_all_zero(df, "file.csv") def test_check_minimum_service_factors_in_range_success(): import pandas as pd - from muse.readers.csv import check_minimum_service_factors_in_range + from muse.readers.csv import _check_minimum_service_factors_in_range df = pd.DataFrame({"minimum_service_factor": (0, 1)}) - check_minimum_service_factors_in_range(df, "file.csv") + _check_minimum_service_factors_in_range(df, "file.csv") def test_check_minimum_service_factors_in_range_column_missing(): import pandas as pd - from muse.readers.csv import check_minimum_service_factors_in_range + from muse.readers.csv import _check_minimum_service_factors_in_range # If the minimum_service_factor column is missing, the function should just return # without raising an error df = pd.DataFrame() - check_minimum_service_factors_in_range(df, "file.csv") + _check_minimum_service_factors_in_range(df, "file.csv") @mark.parametrize( @@ -485,9 +469,40 @@ def test_check_minimum_service_factors_in_range_column_missing(): ) def test_check_minimum_service_factors_in_range_fail(values): import pandas as pd - from muse.readers.csv import check_minimum_service_factors_in_range + from muse.readers.csv import _check_minimum_service_factors_in_range df = pd.DataFrame({"minimum_service_factor": values}) with raises(ValueError): - check_minimum_service_factors_in_range(df, "file.csv") + _check_minimum_service_factors_in_range(df, "file.csv") + + +@patch("muse.readers.csv._check_minimum_service_factors_in_range") +@patch("muse.readers.csv._check_utilization_not_all_zero") +def test_check_utilization_and_minimum_service_factors(*mocks): + import pandas as pd + from muse.readers.csv import check_utilization_and_minimum_service_factors + + df = pd.DataFrame({"utilization_factor": (0, 0, 1)}) + check_utilization_and_minimum_service_factors(df, "file.csv") + for mock in mocks: + mock.assert_called_once_with(df, "file.csv") + + +@patch("muse.readers.csv._check_minimum_service_factors_in_range") +@patch("muse.readers.csv._check_utilization_not_all_zero") +def test_check_utilization_and_minimum_service_factors_missing_column(*mocks): + import pandas as pd + from muse.readers.csv import check_utilization_and_minimum_service_factors + + # NB: Required utilization_factor column is missing + df = pd.DataFrame( + { + "technology": ("gas", "gas", "solar"), + "region": ("GB", "GB", "FR"), + "year": (2010, 2010, 2011), + } + ) + + with raises(ValueError): + check_utilization_and_minimum_service_factors(df, "file.csv") From 593a43bc94d5f948ff6d070a0b9d717ab158b7b8 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 16:53:50 +0100 Subject: [PATCH 10/15] Check that utilisation factors are in range --- src/muse/readers/csv.py | 10 ++++++++++ tests/test_readers.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 699318f2f..5cf80c2eb 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -930,6 +930,7 @@ def check_utilization_and_minimum_service_factors(data, filename): ) _check_utilization_not_all_zero(data, filename) + _check_utilization_in_range(data, filename) _check_minimum_service_factors_in_range(data, filename) @@ -943,6 +944,15 @@ def _check_utilization_not_all_zero(data, filename): ) +def _check_utilization_in_range(data, filename): + utilization = data["utilization_factor"] + if not np.all((0 <= utilization) & (utilization <= 1)): + raise ValueError( + f"""Utilization factor values must all be between 0 and 1 inclusive. + Please check file {filename}.""" + ) + + def _check_minimum_service_factors_in_range(data, filename): try: min_service_factor = data["minimum_service_factor"] diff --git a/tests/test_readers.py b/tests/test_readers.py index 3d589275d..fdf000408 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -429,6 +429,26 @@ def test_check_utilization_not_all_zero_success(): _check_utilization_not_all_zero(df, "file.csv") +def test_check_utilization_in_range_success(): + import pandas as pd + from muse.readers.csv import _check_utilization_in_range + + df = pd.DataFrame({"utilization_factor": (0, 1)}) + _check_utilization_in_range(df, "file.csv") + + +@mark.parametrize( + "values", chain.from_iterable(permutations((0, bad)) for bad in (-1, 2)) +) +def test_check_utilization_in_range_fail(values): + import pandas as pd + from muse.readers.csv import _check_utilization_in_range + + df = pd.DataFrame({"utilization_factor": values}) + with raises(ValueError): + _check_utilization_in_range(df, "file.csv") + + def test_check_utilization_not_all_zero_fail_all_zero(): import pandas as pd from muse.readers.csv import _check_utilization_not_all_zero @@ -478,6 +498,7 @@ def test_check_minimum_service_factors_in_range_fail(values): @patch("muse.readers.csv._check_minimum_service_factors_in_range") +@patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") def test_check_utilization_and_minimum_service_factors(*mocks): import pandas as pd @@ -490,6 +511,7 @@ def test_check_utilization_and_minimum_service_factors(*mocks): @patch("muse.readers.csv._check_minimum_service_factors_in_range") +@patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") def test_check_utilization_and_minimum_service_factors_missing_column(*mocks): import pandas as pd From 9f14629ea7e9e9229420e2464760d48cef2e08de Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 17:04:56 +0100 Subject: [PATCH 11/15] Do check for min service factor column in check_utilization_and_minimum_service_factors --- src/muse/readers/csv.py | 10 ++++------ tests/test_readers.py | 34 +++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 5cf80c2eb..606f400f3 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -931,7 +931,9 @@ def check_utilization_and_minimum_service_factors(data, filename): _check_utilization_not_all_zero(data, filename) _check_utilization_in_range(data, filename) - _check_minimum_service_factors_in_range(data, filename) + + if "minimum_service_factor" in data.columns: + _check_minimum_service_factors_in_range(data, filename) def _check_utilization_not_all_zero(data, filename): @@ -954,11 +956,7 @@ def _check_utilization_in_range(data, filename): def _check_minimum_service_factors_in_range(data, filename): - try: - min_service_factor = data["minimum_service_factor"] - except KeyError: - # Minimum service factor not specified, so we don't need to check it - return + min_service_factor = data["minimum_service_factor"] if not np.all((0 <= min_service_factor) & (min_service_factor <= 1)): raise ValueError( diff --git a/tests/test_readers.py b/tests/test_readers.py index fdf000408..7b59808f5 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -474,16 +474,6 @@ def test_check_minimum_service_factors_in_range_success(): _check_minimum_service_factors_in_range(df, "file.csv") -def test_check_minimum_service_factors_in_range_column_missing(): - import pandas as pd - from muse.readers.csv import _check_minimum_service_factors_in_range - - # If the minimum_service_factor column is missing, the function should just return - # without raising an error - df = pd.DataFrame() - _check_minimum_service_factors_in_range(df, "file.csv") - - @mark.parametrize( "values", chain.from_iterable(permutations((0, bad)) for bad in (-1, 2)) ) @@ -497,23 +487,41 @@ def test_check_minimum_service_factors_in_range_fail(values): _check_minimum_service_factors_in_range(df, "file.csv") -@patch("muse.readers.csv._check_minimum_service_factors_in_range") @patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") +@patch("muse.readers.csv._check_minimum_service_factors_in_range") def test_check_utilization_and_minimum_service_factors(*mocks): import pandas as pd from muse.readers.csv import check_utilization_and_minimum_service_factors - df = pd.DataFrame({"utilization_factor": (0, 0, 1)}) + df = pd.DataFrame( + {"utilization_factor": (0, 0, 1), "minimum_service_factor": (0, 0, 0)} + ) check_utilization_and_minimum_service_factors(df, "file.csv") for mock in mocks: mock.assert_called_once_with(df, "file.csv") +@patch("muse.readers.csv._check_utilization_in_range") +@patch("muse.readers.csv._check_utilization_not_all_zero") @patch("muse.readers.csv._check_minimum_service_factors_in_range") +def test_check_utilization_and_minimum_service_factors_no_min( + min_service_factor_mock, *mocks +): + import pandas as pd + from muse.readers.csv import check_utilization_and_minimum_service_factors + + df = pd.DataFrame({"utilization_factor": (0, 0, 1)}) + check_utilization_and_minimum_service_factors(df, "file.csv") + for mock in mocks: + mock.assert_called_once_with(df, "file.csv") + min_service_factor_mock.assert_not_called() + + @patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") -def test_check_utilization_and_minimum_service_factors_missing_column(*mocks): +@patch("muse.readers.csv._check_minimum_service_factors_in_range") +def test_check_utilization_and_minimum_service_factors_fail_missing_utilization(*mocks): import pandas as pd from muse.readers.csv import check_utilization_and_minimum_service_factors From 5dba9f248327f3e6838230f2dd1d2ab5ee449ebc Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 17:18:13 +0100 Subject: [PATCH 12/15] Check whether utilization factors are below minimums --- src/muse/readers/csv.py | 8 ++++++++ tests/test_readers.py | 25 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 606f400f3..bc63f991b 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -934,6 +934,7 @@ def check_utilization_and_minimum_service_factors(data, filename): if "minimum_service_factor" in data.columns: _check_minimum_service_factors_in_range(data, filename) + _check_utilization_not_below_minimum(data, filename) def _check_utilization_not_all_zero(data, filename): @@ -955,6 +956,13 @@ def _check_utilization_in_range(data, filename): ) +def _check_utilization_not_below_minimum(data, filename): + if (data["utilization_factor"] < data["minimum_service_factor"]).any(): + raise ValueError(f"""Utilization factors must all be greater than or equal to + their corresponding minimum service factors. Please check + {filename}.""") + + def _check_minimum_service_factors_in_range(data, filename): min_service_factor = data["minimum_service_factor"] diff --git a/tests/test_readers.py b/tests/test_readers.py index 7b59808f5..0cec9c022 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -449,6 +449,25 @@ def test_check_utilization_in_range_fail(values): _check_utilization_in_range(df, "file.csv") +def test_check_utilization_not_below_minimum_success(): + import pandas as pd + from muse.readers.csv import _check_utilization_not_below_minimum + + df = pd.DataFrame({"utilization_factor": (0, 1), "minimum_service_factor": (0, 0)}) + _check_utilization_not_below_minimum(df, "file.csv") + + +def test_check_utilization_not_below_minimum_fail(): + import pandas as pd + from muse.readers.csv import _check_utilization_not_below_minimum + + df = pd.DataFrame( + {"utilization_factor": (0, 1), "minimum_service_factor": (0.1, 0)} + ) + with raises(ValueError): + _check_utilization_not_below_minimum(df, "file.csv") + + def test_check_utilization_not_all_zero_fail_all_zero(): import pandas as pd from muse.readers.csv import _check_utilization_not_all_zero @@ -489,6 +508,7 @@ def test_check_minimum_service_factors_in_range_fail(values): @patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") +@patch("muse.readers.csv._check_utilization_not_below_minimum") @patch("muse.readers.csv._check_minimum_service_factors_in_range") def test_check_utilization_and_minimum_service_factors(*mocks): import pandas as pd @@ -504,9 +524,10 @@ def test_check_utilization_and_minimum_service_factors(*mocks): @patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") +@patch("muse.readers.csv._check_utilization_not_below_minimum") @patch("muse.readers.csv._check_minimum_service_factors_in_range") def test_check_utilization_and_minimum_service_factors_no_min( - min_service_factor_mock, *mocks + min_service_factor_mock, utilization_below_min_mock, *mocks ): import pandas as pd from muse.readers.csv import check_utilization_and_minimum_service_factors @@ -516,10 +537,12 @@ def test_check_utilization_and_minimum_service_factors_no_min( for mock in mocks: mock.assert_called_once_with(df, "file.csv") min_service_factor_mock.assert_not_called() + utilization_below_min_mock.assert_not_called() @patch("muse.readers.csv._check_utilization_in_range") @patch("muse.readers.csv._check_utilization_not_all_zero") +@patch("muse.readers.csv._check_utilization_not_below_minimum") @patch("muse.readers.csv._check_minimum_service_factors_in_range") def test_check_utilization_and_minimum_service_factors_fail_missing_utilization(*mocks): import pandas as pd From 78c9b7f14a738f638d2ddbcdfb834e0f86c4bce1 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 20 Jun 2024 17:32:27 +0100 Subject: [PATCH 13/15] Replace test_minimum_service_factor_invalid_input with mocking --- tests/test_minimum_service.py | 41 +++++------------------------------ 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/tests/test_minimum_service.py b/tests/test_minimum_service.py index 89c7bc09f..5188517fd 100644 --- a/tests/test_minimum_service.py +++ b/tests/test_minimum_service.py @@ -1,7 +1,8 @@ -from itertools import chain, permutations +from itertools import permutations +from unittest.mock import patch import numpy as np -from pytest import mark, raises +from pytest import mark def modify_minimum_service_factors( @@ -25,7 +26,8 @@ def modify_minimum_service_factors( "minimum_service_factors", permutations((np.linspace(0, 1, 6), [0] * 6)), ) -def test_minimum_service_factor(tmpdir, minimum_service_factors): +@patch("muse.readers.csv.check_utilization_and_minimum_service_factors") +def test_minimum_service_factor(check_mock, tmpdir, minimum_service_factors): import pandas as pd from muse import examples from muse.mca import MCA @@ -51,6 +53,7 @@ def test_minimum_service_factor(tmpdir, minimum_service_factors): with tmpdir.as_cwd(): MCA.factory(model_path / "settings.toml").run() + check_mock.assert_called_once() supply_timeslice = pd.read_csv(tmpdir / "Results/MCAMetric_Supply.csv") @@ -64,35 +67,3 @@ def test_minimum_service_factor(tmpdir, minimum_service_factors): ].supply >= factor ).all() - - -@mark.parametrize( - "minimum_service_factors", - chain.from_iterable(map(permutations, ((-1, 0), (2, 0), (float("nan"), 0)))), -) -def test_minimum_service_factor_invalid_input(tmpdir, minimum_service_factors): - from muse import examples - from muse.mca import MCA - - sector = "power" - processes = ("gasCCGT", "windturbine") - - # Copy the model inputs to tmpdir - model_path = examples.copy_model( - name="default_timeslice", path=tmpdir, overwrite=True - ) - - technodata_timeslices = modify_minimum_service_factors( - model_path=model_path, - sector=sector, - processes=processes, - minimum_service_factors=minimum_service_factors, - ) - - technodata_timeslices.to_csv( - model_path / "technodata" / sector / "TechnodataTimeslices.csv", index=False - ) - - with raises(ValueError): - with tmpdir.as_cwd(): - MCA.factory(model_path / "settings.toml").run() From 013d6a4650daaf68abe4c2aa29a5eba7f0be1d12 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Fri, 21 Jun 2024 11:57:38 +0100 Subject: [PATCH 14/15] Fix: Check may be run more than once --- tests/test_minimum_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_minimum_service.py b/tests/test_minimum_service.py index 5188517fd..4efe8a7e9 100644 --- a/tests/test_minimum_service.py +++ b/tests/test_minimum_service.py @@ -53,7 +53,7 @@ def test_minimum_service_factor(check_mock, tmpdir, minimum_service_factors): with tmpdir.as_cwd(): MCA.factory(model_path / "settings.toml").run() - check_mock.assert_called_once() + check_mock.assert_called() supply_timeslice = pd.read_csv(tmpdir / "Results/MCAMetric_Supply.csv") From b444f6807300260dfa99f221008789333bfbef96 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Fri, 21 Jun 2024 12:34:48 +0100 Subject: [PATCH 15/15] Fix minimum_service example: Reduce utilization factor to be less than minimum Fixes #362. --- .../example/minimum_service/technodata/industry/Technodata.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/muse/data/example/minimum_service/technodata/industry/Technodata.csv b/src/muse/data/example/minimum_service/technodata/industry/Technodata.csv index 1822d7858..ca9f1de4c 100644 --- a/src/muse/data/example/minimum_service/technodata/industry/Technodata.csv +++ b/src/muse/data/example/minimum_service/technodata/industry/Technodata.csv @@ -1,6 +1,6 @@ ProcessName,RegionName,Time,Level,cap_par,cap_exp,fix_par,fix_exp,var_par,var_exp,MaxCapacityAddition,MaxCapacityGrowth,TotalCapacityLimit,TechnicalLife,UtilizationFactor,InterestRate,ScalingSize,Agent2,Type,Fuel,MinimumServiceFactor,Enduse Unit,-,Year,-,MUS$2010/Mt,-,MUS$2010/Mt,-,MUS$2010/Mt,-,Mt,-,Mt,Years,-,-,-,Retrofit,-,-,-,- procammonia_1,R1,2010,fixed,100,1,0.5,1,0,1,5,0.03,100,20,0.85,0.1,0.1,1,energy,fuel1,0.01,ammonia -procammonia_1,R1,2050,fixed,100,1,0.5,1,0,1,5,0.03,100,20,0.85,0.1,0.1,1,energy,fuel1,0.9,ammonia +procammonia_1,R1,2050,fixed,100,1,0.5,1,0,1,5,0.03,100,20,0.85,0.1,0.1,1,energy,fuel1,0.85,ammonia procammonia_2,R1,2010,fixed,97.5,1,0.4875,1,0,1,5,0.03,100,20,0.85,0.1,0.1,1,energy,fuel2,0,ammonia procammonia_2,R1,2050,fixed,97.5,1,0.4875,1,0,1,5,0.03,100,20,0.85,0.1,0.1,1,energy,fuel2,0,ammonia