Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ While retaining the core concepts of "custom" and "cmip" modes, ACCESS-MOPPy uni
getting_started
batch_processing
esmvaltool_integration
regridding
CMORise_ILAMB_workflow
mapping_reference
compliance_testing
Expand Down
79 changes: 79 additions & 0 deletions docs/source/regridding.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Optional variable-aware regridding
==================================

ACCESS-MOPPy writes native-grid CMIP-style output by default. Optional
regridding is intended for evaluation and analysis workflows such as ILAMB,
ESMValTool and REF, where a common comparison grid is useful. It should not be
applied blindly to all variables for publication outputs.

Design
------

The regridding hook runs after the CMOR variable has been derived on the native
grid and after metadata, units and time handling have been applied. It then:

* selects a method per variable;
* reuses a cached ESMF/xESMF weight file when present;
* otherwise generates weights with xESMF when the optional dependency is
installed;
* updates ``grid_label``, regular ``lat``/``lon`` coordinates and bounds; and
* removes stale native-grid ``cell_measures`` metadata.

Cached weights are reusable across variables, years, experiments and batch jobs
when the source grid, target grid, method and mask policy are identical. Weight
files are normally too large and grid-version-specific to ship inside the Python
package, so the recommended location is a shared data area.

Configuration
-------------

Example batch configuration::

regrid:
enabled: true
target_grid: cmip7-1x1
grid_label: gr
method: auto
weights:
mode: reuse_or_create
cache_dir: /g/data/xp65/public/apps/moppy/regrid_weights
mask_policy: nomask
variable_methods:
pr: conservative
tos: bilinear
sftlf: nearest_s2d
variable_classes:
uo: vector

``method: auto`` uses a deliberately conservative first-pass policy:

* flux-like and extensive quantities use ``conservative``;
* smooth scalar state variables use ``bilinear``;
* masks and categorical fields use ``nearest_s2d``; and
* vector/staggered-grid fields are refused unless explicitly handled by a
vector-aware workflow.

Weights command
---------------

Weights can also be generated explicitly before CMORisation::

moppy-regrid-weights create \
--source-grid ACCESS-ESM1-6-ocean-native-grid.nc \
--target-grid cmip7-1x1 \
--method conservative \
--output ACCESS-ESM1-6_gn_to_cmip7-1x1_conservative_nomask_a82f13.nc

Conservative regridding requires valid source cell bounds/corners, either
``lat_bnds``/``lon_bnds`` on rectilinear grids or
``vertices_latitude``/``vertices_longitude`` on curvilinear grids. Missing
bounds produce a clear error rather than silently generating unsafe weights.

Limitations
-----------

This is a first pass. Applying existing sparse weight files is intentionally
lightweight and testable without ESMF, but generating weights still requires the
optional ``xesmf``/ESMF stack. Vector rotation, staggered-grid location-aware
interpolation, bounded fraction methods and richer target-grid registries remain
future work.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ test-esmval = [
"access_moppy[test]",
"esmvaltool>=2.14",
]
regrid = [
"xesmf>=0.8",
]
docs = [
"sphinx>=7.0.0",
"sphinx-rtd-theme>=1.3.0",
Expand All @@ -93,6 +96,7 @@ moppy-example-config = "access_moppy.examples.show_config:main"
moppy-calc-ab-coeffts = "access_moppy.legacy_utilities.calc_hybrid_height_coeffs:main"
moppy-esmval-prepare = "access_moppy.esmval.cli_commands:main_prepare"
moppy-esmval-run = "access_moppy.esmval.cli_commands:main_run"
moppy-regrid-weights = "access_moppy.regrid:main"

[project.entry-points."esmvaltool_commands"]
cmorise = "access_moppy.esmval.cli_commands:CMORiseCommand"
Expand Down
1 change: 1 addition & 0 deletions src/access_moppy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from . import _version
from ._config import _creator
from .driver import ACCESS_ESM_CMORiser
from .regrid import RegridConfig, RegridError, select_regrid_method
from .utilities import check_for_updates

__version__ = _version.get_versions()["version"]
Expand Down
16 changes: 16 additions & 0 deletions src/access_moppy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import xarray as xr
from cftime import date2num

from access_moppy.regrid import RegridConfig, apply_optional_regridding
from access_moppy.utilities import (
FrequencyMismatchError,
IncompatibleFrequencyError,
Expand Down Expand Up @@ -201,6 +202,7 @@ def __init__(
chunk_size_mb: float = 4.0,
enable_compression: bool = True,
compression_level: int = 4,
regrid: dict | RegridConfig | None = None,
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand Down Expand Up @@ -253,6 +255,11 @@ def __init__(
self.enable_chunking = enable_chunking
self.enable_compression = enable_compression
self.compression_level = compression_level
self.regrid_config = (
regrid
if isinstance(regrid, RegridConfig)
else RegridConfig.from_config(regrid)
)
self.chunker = (
DatasetChunker(
target_chunk_size_mb=chunk_size_mb,
Expand Down Expand Up @@ -1380,6 +1387,15 @@ def run(self, write_output: bool = False):
# Standardize missing values to CMIP6 requirements after processing
self.standardize_missing_values()
self.update_attributes()
if self.regrid_config.requires_regridding:
self.ds = apply_optional_regridding(
self.ds,
self.cmor_name,
self.regrid_config,
source_id=getattr(self.vocab, "source_id", None),
source_grid_label=getattr(self.vocab, "grid_label", None),
)
self.vocab.grid_label = self.regrid_config.grid_label
self.reorder()
# Final rechunking before writing for optimal I/O performance
if write_output:
Expand Down
7 changes: 7 additions & 0 deletions src/access_moppy/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def __init__(
enable_resampling: bool = False,
enable_chunking: bool = False,
resampling_method: str = "auto",
regrid: Optional[Dict[str, Any]] = None,
# Backward compatibility
input_paths: Optional[Union[str, list]] = None,
):
Expand All @@ -124,6 +125,7 @@ def __init__(
:param validate_frequency: Whether to validate temporal frequency consistency across input files (default: True).
:param enable_resampling: Whether to enable automatic temporal resampling when frequency mismatches occur (default: False).
:param resampling_method: Method for temporal resampling ('auto', 'mean', 'sum', 'min', 'max', 'first', 'last') (default: 'auto').
:param regrid: Optional regridding configuration. Disabled by default.
:param input_paths: [DEPRECATED] Use input_data instead. Kept for backward compatibility.
"""

Expand Down Expand Up @@ -239,6 +241,7 @@ def __init__(
self.enable_chunking = enable_chunking
self.resampling_method = resampling_method
self.output_path = Path(output_path)
self.regrid = regrid
self.experiment_id = experiment_id
self.source_id = source_id
self.variant_label = variant_label
Expand Down Expand Up @@ -329,6 +332,7 @@ def __init__(
enable_resampling=self.enable_resampling,
resampling_method=self.resampling_method,
enable_chunking=self.enable_chunking,
regrid=self.regrid,
)
elif table in ("Oyr", "Oday", "Omon", "Ofx"):
if self.source_id == "ACCESS-OM3" or self.model_id == "ACCESS-CM3":
Expand All @@ -343,6 +347,7 @@ def __init__(
vocab=self.vocab,
variable_mapping=self.variable_mapping.to_dict(),
drs_root=drs_root if drs_root else None,
regrid=self.regrid,
)
else:
# ACCESS-OM2 uses MOM5 (B-grid) — handled by a separate CMORiser class
Expand All @@ -356,6 +361,7 @@ def __init__(
vocab=self.vocab,
variable_mapping=self.variable_mapping.to_dict(),
drs_root=drs_root if drs_root else None,
regrid=self.regrid,
)
elif table in ("SImon", "SIday"):
self.cmoriser = SeaIce_CMORiser(
Expand All @@ -367,6 +373,7 @@ def __init__(
vocab=self.vocab,
variable_mapping=self.variable_mapping,
drs_root=drs_root if drs_root else None,
regrid=self.regrid,
)
else:
_atmos_tables = (
Expand Down
19 changes: 19 additions & 0 deletions src/access_moppy/examples/batch_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,22 @@ worker_init: |

# Optional: Wait for all jobs to complete before exiting
wait_for_completion: false

# Optional: variable-aware regridding for evaluation workflows (disabled by default).
# Native grid remains the recommended default for CMIP publication unless a
# secondary grid product is explicitly required.
# regrid:
# enabled: true
# target_grid: cmip7-1x1
# grid_label: gr
# method: auto # conservative | bilinear | nearest_s2d | auto
# weights:
# mode: reuse_or_create # reuse | create | reuse_or_create
# cache_dir: /g/data/xp65/public/apps/moppy/regrid_weights
# mask_policy: nomask
# variable_methods:
# pr: conservative
# tos: bilinear
# sftlf: nearest_s2d
# variable_classes:
# uo: vector # refuse scalar regridding for vector fields
6 changes: 6 additions & 0 deletions src/access_moppy/ocean.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
validate_frequency: bool = True,
enable_resampling: bool = False,
resampling_method: str = "auto",
regrid: dict | None = None,
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand All @@ -41,6 +42,7 @@ def __init__(
validate_frequency=validate_frequency,
enable_resampling=enable_resampling,
resampling_method=resampling_method,
regrid=regrid,
)

self.supergrid = None # To be defined in subclasses
Expand Down Expand Up @@ -319,6 +321,7 @@ def __init__(
vocab: CMIP6Vocabulary,
variable_mapping: Dict[str, Any],
drs_root: Optional[Path] = None,
regrid: dict | None = None,
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand All @@ -330,6 +333,7 @@ def __init__(
vocab=vocab,
variable_mapping=variable_mapping,
drs_root=drs_root,
regrid=regrid,
)

nominal_resolution = vocab._get_nominal_resolution(target_realm="ocean")
Expand Down Expand Up @@ -391,6 +395,7 @@ def __init__(
vocab: CMIP6Vocabulary,
variable_mapping: Dict[str, Any],
drs_root: Optional[Path] = None,
regrid: dict | None = None,
# Backward compatibility
input_paths: Optional[Union[str, List[str]]] = None,
):
Expand All @@ -402,6 +407,7 @@ def __init__(
vocab=vocab,
variable_mapping=variable_mapping,
drs_root=drs_root,
regrid=regrid,
)

nominal_resolution = vocab._get_nominal_resolution(target_realm="ocean")
Expand Down
Loading
Loading