Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/access_moppy/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,16 @@ def __init__(
elif table in ("Oyr", "Oday", "Omon", "Ofx") or table.startswith(
_mip_ocean_prefixes
):
# Ocean yearly tables can legitimately aggregate from higher-frequency
# inputs (for example monthly -> yearly). Keep explicit user control
# via enable_resampling, but auto-enable for yearly ocean requests.
ocean_enable_resampling = self.enable_resampling or table in (
"Oyr",
"OPyr",
"OPyrLev",
"OByr",
"OByrLev",
)
if self.model_id in ("ACCESS-OM3", "ACCESS-CM3"):
# ACCESS-OM3 uses MOM6 (C-grid) — requires dedicated CMORiser implementation
# that handles C-grid supergrid logic, MOM6 metadata, and OM3-specific conventions
Expand All @@ -510,6 +520,9 @@ def __init__(
vocab=self.vocab,
variable_mapping=self.variable_mapping.to_dict(),
drs_root=drs_root if drs_root else None,
validate_frequency=self.validate_frequency,
enable_resampling=ocean_enable_resampling,
resampling_method=self.resampling_method,
)
else:
# ACCESS-OM2 uses MOM5 (B-grid) — handled by a separate CMORiser class
Expand All @@ -523,6 +536,9 @@ def __init__(
vocab=self.vocab,
variable_mapping=self.variable_mapping.to_dict(),
drs_root=drs_root if drs_root else None,
validate_frequency=self.validate_frequency,
enable_resampling=ocean_enable_resampling,
resampling_method=self.resampling_method,
)
elif table in ("SImon", "SIday") or table.startswith(_mip_seaice_prefixes):
self.cmoriser = SeaIce_CMORiser(
Expand Down
12 changes: 10 additions & 2 deletions src/access_moppy/file_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,16 @@ def _build_patterns(
# --- Per-variable explicit override ---
explicit = var_entry.get("file_pattern")
if explicit:
# Explicit patterns are already relative to input_root
return [explicit] if isinstance(explicit, str) else list(explicit)
# Explicit patterns are already relative to input_root.
# They can be:
# - a string: one pattern for all frequencies
# - a list: several patterns for all frequencies
# - a dict: frequency-specific override, e.g. {"yr": [...], "default": ...}
if isinstance(explicit, dict):
explicit = explicit.get(freq, explicit.get("default"))

if explicit:
return [explicit] if isinstance(explicit, str) else list(explicit)

# --- Component-level config ---
comp_cfg = file_discovery_cfg.get("components", {}).get(component)
Expand Down
6 changes: 6 additions & 0 deletions src/access_moppy/mappings/ACCESS-ESM1-6_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4940,6 +4940,12 @@
"model_variables": [
"salt_vdiffuse_impl"
],
"file_pattern": {
"yr": [
"output[0-9][0-9][0-9]/ocean/ocean-*-salt_vdiffuse_impl-1mon-mean-y_*.nc",
"output[0-9][0-9][0-9]/ocean/ocean-*-salt_vdiffuse_impl-1monthly-*-ym_*.nc"
]
},
"calculation": {
"type": "direct",
"formula": "salt_vdiffuse_impl"
Expand Down
12 changes: 12 additions & 0 deletions src/access_moppy/ocean.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ def __init__(
vocab: CMIP6Vocabulary,
variable_mapping: Dict[str, Any],
drs_root: Optional[Path] = None,
validate_frequency: bool = True,
enable_resampling: bool = False,
resampling_method: str = "auto",
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand All @@ -338,6 +341,9 @@ def __init__(
vocab=vocab,
variable_mapping=variable_mapping,
drs_root=drs_root,
validate_frequency=validate_frequency,
enable_resampling=enable_resampling,
resampling_method=resampling_method,
)

nominal_resolution = vocab._get_nominal_resolution(target_realm="ocean")
Expand Down Expand Up @@ -399,6 +405,9 @@ def __init__(
vocab: CMIP6Vocabulary,
variable_mapping: Dict[str, Any],
drs_root: Optional[Path] = None,
validate_frequency: bool = True,
enable_resampling: bool = False,
resampling_method: str = "auto",
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand All @@ -410,6 +419,9 @@ def __init__(
vocab=vocab,
variable_mapping=variable_mapping,
drs_root=drs_root,
validate_frequency=validate_frequency,
enable_resampling=enable_resampling,
resampling_method=resampling_method,
)

nominal_resolution = vocab._get_nominal_resolution(target_realm="ocean")
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,58 @@ def test_ocean_table_selects_om2_for_non_om3_source(self, valid_config, temp_dir

mock_om2.assert_called_once()

@pytest.mark.unit
def test_ocean_yearly_auto_enables_resampling(self, valid_config, temp_dir):
with (
patch("access_moppy.driver.load_model_mappings") as mock_load,
patch("access_moppy.driver.Ocean_CMORiser_OM2") as mock_om2,
):
mock_load.return_value = {"osaltdiff": {"units": "kg m-2 s-1"}}
mock_om2_instance = MagicMock()
mock_om2_instance.ds = xr.Dataset()
mock_om2.return_value = mock_om2_instance

ACCESS_ESM_CMORiser(
input_paths=["test.nc"],
compound_name="Oyr.osaltdiff",
source_id="ACCESS-ESM1-5",
output_path=temp_dir,
experiment_id=valid_config["experiment_id"],
variant_label=valid_config["variant_label"],
grid_label=valid_config["grid_label"],
activity_id=valid_config["activity_id"],
)

kwargs = mock_om2.call_args.kwargs
assert kwargs["enable_resampling"] is True

@pytest.mark.unit
def test_ocean_monthly_does_not_auto_enable_resampling(
self, valid_config, temp_dir
):
with (
patch("access_moppy.driver.load_model_mappings") as mock_load,
patch("access_moppy.driver.Ocean_CMORiser_OM2") as mock_om2,
):
mock_load.return_value = {"tos": {"units": "K"}}
mock_om2_instance = MagicMock()
mock_om2_instance.ds = xr.Dataset()
mock_om2.return_value = mock_om2_instance

ACCESS_ESM_CMORiser(
input_paths=["test.nc"],
compound_name="Omon.tos",
source_id="ACCESS-ESM1-5",
output_path=temp_dir,
experiment_id=valid_config["experiment_id"],
variant_label=valid_config["variant_label"],
grid_label=valid_config["grid_label"],
activity_id=valid_config["activity_id"],
)

kwargs = mock_om2.call_args.kwargs
assert kwargs["enable_resampling"] is False

@pytest.mark.unit
def test_unsupported_table_raises_value_error_with_supported_list(
self, valid_config, temp_dir
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_file_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,34 @@ def test_per_variable_file_pattern_overrides_component_config(self):
patterns = _build_patterns(var_entry, "ocean", "mon", self.fd_cfg)
assert patterns == ["output*/ocean/ocean-2d-surface_temp-1mon-mean-y_*.nc"]

def test_per_variable_file_pattern_freq_specific_override(self):
var_entry = {
"model_variables": ["salt_vdiffuse_impl"],
"file_pattern": {
"yr": [
"output*/ocean/ocean-*-salt_vdiffuse_impl-1mon-mean-y_*.nc",
]
},
}
patterns = _build_patterns(var_entry, "ocean", "yr", self.fd_cfg)
assert patterns == [
"output*/ocean/ocean-*-salt_vdiffuse_impl-1mon-mean-y_*.nc",
]

def test_per_variable_file_pattern_freq_specific_falls_back(self):
var_entry = {
"model_variables": ["salt_vdiffuse_impl"],
"file_pattern": {
"yr": [
"output*/ocean/ocean-*-salt_vdiffuse_impl-1mon-mean-y_*.nc",
]
},
}
patterns = _build_patterns(var_entry, "ocean", "mon", self.fd_cfg)
assert len(patterns) == 2
assert all("salt_vdiffuse_impl" in p for p in patterns)
assert all("1mon" in p or "1monthly" in p for p in patterns)

def test_unknown_component_raises(self):
with pytest.raises(FileDiscoveryError, match="No file_discovery config"):
_build_patterns({}, "nonexistent_component", "mon", self.fd_cfg)
Expand Down