diff --git a/CHANGELOG.md b/CHANGELOG.md index f88cf93..3c57943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `default_scaler` to the configuration for the scaler to be used if no specific scaler configuration is provided [#9](https://github.com/climate-resource/spaemis/pull/9) - Move test configuration to `test-data` directory [#8](https://github.com/climate-resource/spaemis/pull/8) -- Add functionality to write out an xarray dataset as a set of CSVs that are formatted the same as the input emissions inventory data [#7](https://github.com/climate-resource/spaemis/pull/7) +- Add functionality to write out a xarray dataset as a set of CSVs that are formatted the same as the input emissions inventory data [#7](https://github.com/climate-resource/spaemis/pull/7) - Add relative_change scaler and reading of Input4MIPs data [#3](https://github.com/climate-resource/spaemis/pull/3) - Added CLI command `project` and a framework for scalers [#2](https://github.com/climate-resource/spaemis/pull/2) -- Initial commit and repository setup \ No newline at end of file +- Initial commit and repository setup diff --git a/src/spaemis/commands/project_command.py b/src/spaemis/commands/project_command.py index d6f463f..6a87200 100644 --- a/src/spaemis/commands/project_command.py +++ b/src/spaemis/commands/project_command.py @@ -4,12 +4,14 @@ import logging import os.path +from itertools import product +from typing import Dict, Tuple import click import xarray as xr from spaemis.commands.base import cli -from spaemis.config import DownscalingScenarioConfig, VariableConfig, load_config +from spaemis.config import DownscalingScenarioConfig, VariableScalerConfig, load_config from spaemis.inventory import EmissionsInventory, load_inventory, write_inventory_csvs from spaemis.scaling import get_scaler_by_config @@ -17,7 +19,7 @@ def scale_inventory( - cfg: VariableConfig, inventory: EmissionsInventory, target_year: int + cfg: VariableScalerConfig, inventory: EmissionsInventory, target_year: int ) -> xr.Dataset: """ Scale a given variable/sector @@ -68,10 +70,35 @@ def calculate_projections( The dimensionality of the output variables is (sector, year, lat, lon) """ + scaling_configs: Dict[Tuple[str, str], VariableScalerConfig] = { + (cfg.variable, cfg.sector): cfg for cfg in config.scalers + } + + if config.default_scaler: + # Add in additional scalers for each missing variable/sector + variables = inventory.data.data_vars.keys() + sectors = inventory.data["sector"].values + + for variable, sector in product(variables, sectors): + scaling_configs.setdefault( + (variable, sector), + VariableScalerConfig( + variable=variable, + sector=sector, + method=config.default_scaler, + ), + ) + projections = [] - for variable_config in config.variables: + + for variable_config in scaling_configs.values(): for slice_year in config.timeslices: - logger.info(f"Processing year={slice_year}") + logger.info( + "Processing variable=%s sector=%s year=%i", + variable_config.variable, + variable_config.sector, + slice_year, + ) res = scale_inventory(variable_config, inventory, slice_year) projections.append(res) diff --git a/src/spaemis/config.py b/src/spaemis/config.py index 003803a..5b1cbed 100644 --- a/src/spaemis/config.py +++ b/src/spaemis/config.py @@ -2,7 +2,7 @@ Description of the configuration """ -from typing import Any, ClassVar, Literal, Type, Union, get_args +from typing import Any, ClassVar, Literal, Optional, Type, Union, get_args from attrs import define from cattrs.preconf.pyyaml import make_converter @@ -42,7 +42,7 @@ def _discriminate_scaler(value: Any, _klass: Type) -> ScalerMethod: @define -class VariableConfig: +class VariableScalerConfig: variable: str sector: str method: ScalerMethod @@ -57,7 +57,8 @@ class DownscalingScenarioConfig: inventory_name: str inventory_year: int timeslices: list[int] - variables: list[VariableConfig] + scalers: list[VariableScalerConfig] + default_scaler: Optional[ScalerMethod] = None def load_config(config_file: str) -> DownscalingScenarioConfig: diff --git a/src/spaemis/config/scenarios/ssp245.yaml b/src/spaemis/config/scenarios/ssp245.yaml index 9af6870..4d7e781 100644 --- a/src/spaemis/config/scenarios/ssp245.yaml +++ b/src/spaemis/config/scenarios/ssp245.yaml @@ -5,26 +5,30 @@ timeslices: - 2020 - 2040 - 2060 + - 2080 -variables: +default_scaler: + name: constant + +scalers: - variable: NOx sector: industry method: name: relative_change - source_id: IAMC-MESSAGE-GLOBIOM-ssp245-1-1 + source_id: &source IAMC-MESSAGE-GLOBIOM-ssp245-1-1 variable_id: NOx-em-anthro sector: Industrial Sector - variable: NOx sector: motor_vehicles method: name: relative_change - source_id: IAMC-MESSAGE-GLOBIOM-ssp245-1-1 + source_id: *source variable_id: NOx-em-anthro sector: Transportation Sector - variable: CO sector: industry method: name: relative_change - source_id: IAMC-MESSAGE-GLOBIOM-ssp245-1-1 + source_id: *source variable_id: CO-em-anthro sector: Industrial Sector \ No newline at end of file diff --git a/tests/test-data/config/test-config.yaml b/tests/test-data/config/test-config.yaml index 9af6870..c2c32ae 100644 --- a/tests/test-data/config/test-config.yaml +++ b/tests/test-data/config/test-config.yaml @@ -6,7 +6,7 @@ timeslices: - 2040 - 2060 -variables: +scalers: - variable: NOx sector: industry method: diff --git a/tests/unit/commands/test_project.py b/tests/unit/commands/test_project.py index 0062535..d52b01c 100644 --- a/tests/unit/commands/test_project.py +++ b/tests/unit/commands/test_project.py @@ -6,7 +6,12 @@ from spaemis.commands import cli from spaemis.commands.project_command import calculate_projections, scale_inventory -from spaemis.config import VariableConfig, converter, load_config +from spaemis.config import ( + ConstantScaleMethod, + VariableScalerConfig, + converter, + load_config, +) def test_cli_project(runner, config_file, tmpdir, mocker, inventory): @@ -59,7 +64,7 @@ def test_scale_inventory_missing_variable(inventory): "sector": "Industrial", "method": {"name": "constant"}, }, - VariableConfig, + VariableScalerConfig, ) with pytest.raises(ValueError, match="Variable missing not available in inventory"): scale_inventory(config, inventory, 2040) @@ -72,7 +77,7 @@ def test_scale_inventory_missing_sector(inventory): "sector": "unknown", "method": {"name": "constant"}, }, - VariableConfig, + VariableScalerConfig, ) with pytest.raises(ValueError, match="Sector unknown not available in inventory"): scale_inventory(config, inventory, 2040) @@ -85,7 +90,7 @@ def test_scale_inventory_constant(inventory): "sector": "rail", "method": {"name": "constant"}, }, - VariableConfig, + VariableScalerConfig, ) res = scale_inventory(config, inventory, 2040) assert isinstance(res, xr.Dataset) @@ -111,7 +116,7 @@ def test_scale_inventory_relative(inventory): "sector": "Transportation Sector", }, }, - VariableConfig, + VariableScalerConfig, ) res = scale_inventory(config, inventory, 2040) assert isinstance(res, xr.Dataset) @@ -143,3 +148,25 @@ def test_calculate_projections(config, inventory): assert res["CO"].sel(sector="motor_vehicles").isnull().all() # but CO|industry should have data assert not res["CO"].sel(sector="industry").isnull().all() + + +def test_calculate_projections_with_default(config, inventory): + config.default_scaler = ConstantScaleMethod() + + res = calculate_projections(config, inventory) + + assert (res["sector"] == inventory.data["sector"]).all() + + # CO|architect_coating should be held constant + xr.testing.assert_allclose( + res["CO"] + .sel(sector="architect_coating", year=2040) + .reset_coords("year", drop=True), + inventory.data["CO"].sel(sector="architect_coating"), + ) + # CO|industry should be scaled + with pytest.raises(AssertionError): + xr.testing.assert_allclose( + res["CO"].sel(sector="industry", year=2040).reset_coords("year", drop=True), + inventory.data["CO"].sel(sector="industry"), + )