diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46a5197..850c042 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ The changes listed in this file are categorised as follows: ### Fixed +- ``Cmip6MeteorDataGetter.validate_pattern_scaling_cache`` now accepts an optional ``variable`` argument matching the per-variable suffix ``MeteorPatternScaling`` saves under (``cmip6-{model}-aer-{variable}``). Without it, a cache that ``MeteorPatternScaling.__init__`` loads successfully by filename was reported as "invalid" by the outer validator, causing ``MeteorInterface`` to run ``prepare_pattern_scaling_training_data`` (which fetches and assembles CMIP6 data over the network) on every generation call and immediately discard the result. Threading ``variable=`` through ``_train_pattern_scaling`` fixes the mismatch and eliminates the redundant work (~13–18 s per generation call in the NorESM2-MM profiling workloads). - Variable length timeseries now works also when don't have "year" as time dimension. - Fixes to generate annual and monthly gridded ensembles with unified noise and preserving more of the variance. diff --git a/src/meteor/cmip6_meteor_data_getter.py b/src/meteor/cmip6_meteor_data_getter.py index a119625..5f605a6 100644 --- a/src/meteor/cmip6_meteor_data_getter.py +++ b/src/meteor/cmip6_meteor_data_getter.py @@ -1083,7 +1083,9 @@ def prepare_pattern_scaling_training_data( ) return training_data - def validate_pattern_scaling_cache(self, cache_file, model_name, scenario="aer"): + def validate_pattern_scaling_cache( + self, cache_file, model_name, scenario="aer", variable=None + ): """ Validate a cached pattern scaling model file. @@ -1098,6 +1100,11 @@ def validate_pattern_scaling_cache(self, cache_file, model_name, scenario="aer") Expected model name scenario : str, optional Scenario suffix for expected model name. Default is "aer". + variable : str, optional + Variable suffix appended to the expected model name (e.g. "tas"). + Must match the name ``MeteorPatternScaling`` uses when saving the + per-variable model. When omitted, the legacy (no-suffix) form is + used, but this will mismatch caches saved with per-variable names. Returns ------- @@ -1121,7 +1128,14 @@ def validate_pattern_scaling_cache(self, cache_file, model_name, scenario="aer") ... print(f"✅ {info['message']}") """ expected_name = f"cmip6-{model_name}-{scenario}" - expected_vars = set(self.flds) + if variable is not None: + expected_name = f"{expected_name}-{variable}" + # Per-variable caches only carry that one variable; asking for all + # of self.flds here would spuriously reject a valid single-variable + # cache and force a re-fit. + expected_vars = {variable} + else: + expected_vars = set(self.flds) info = { "expected_name": expected_name, diff --git a/src/meteor/meteor_interface.py b/src/meteor/meteor_interface.py index 3da2183..3d9af58 100644 --- a/src/meteor/meteor_interface.py +++ b/src/meteor/meteor_interface.py @@ -405,9 +405,14 @@ def _train_pattern_scaling(self, variable, config, verbose=True): self.model, variable=variable ) - # Check cache + # Check cache. The variable suffix must match the name + # MeteorPatternScaling saves under (``cmip6-{model}-aer-{variable}``); + # without it the validator returns "invalid" for a cache that then + # loads successfully by filename, causing wasted training-data prep. is_valid, cached_model, info = ( # pylint: disable=unused-variable - self.data_getter.validate_pattern_scaling_cache(cache_file, self.model) + self.data_getter.validate_pattern_scaling_cache( + cache_file, self.model, variable=variable + ) ) if is_valid: diff --git a/tests/unit/test_cmip6_meteor_data_getter.py b/tests/unit/test_cmip6_meteor_data_getter.py index 8798cec..0ae5d02 100644 --- a/tests/unit/test_cmip6_meteor_data_getter.py +++ b/tests/unit/test_cmip6_meteor_data_getter.py @@ -773,6 +773,35 @@ def test_caching(tmp_path): assert cached_model is None assert "Error reading cached file" in info["message"] + # variable= must be honored so that per-variable caches (which + # MeteorPatternScaling saves under `cmip6-{model}-aer-{variable}` and which + # only carry that one variable in patternflds) round-trip. + # Without this, MeteorInterface prepares training data on every call because + # this check returns "invalid" even when the pkl loads fine by filename. + per_var_file = tmp_path / "per_variable.pkl" + per_var_name = "cmip6-ModelA-aer-tas" + with open(per_var_file, "wb") as handle: + pickle.dump( + {"name": per_var_name, "patternflds": {"tas": None}}, + handle, + ) + + # No variable= arg: legacy validator, mismatches per-variable name. + is_valid, _, info = data_getter.validate_pattern_scaling_cache( + str(per_var_file), "ModelA" + ) + assert is_valid is False + assert "Model name mismatch" in info["message"] + + # variable="tas": matches name AND expected_vars is narrowed to just + # {"tas"} instead of self.flds, so the single-variable pkl is accepted. + is_valid, cached_model, info = data_getter.validate_pattern_scaling_cache( + str(per_var_file), "ModelA", variable="tas" + ) + assert is_valid is True + assert cached_model is not None + assert "Cache valid" in info["message"] + # def test_error_handling_for_invalid_experiments_and_fields(): # """Test error handling for invalid experiments and fields to hit lines 585-593."""