diff --git a/.github/workflows/CI-test.yaml b/.github/workflows/CI-test.yaml index a97c76c5..7cdbd553 100644 --- a/.github/workflows/CI-test.yaml +++ b/.github/workflows/CI-test.yaml @@ -17,6 +17,16 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + - name: Checkout LFS files + run: git lfs pull + - name: Checkout LFS files in submodules + run: git submodule foreach --recursive git lfs pull + - name: Check Mesh + run: | + ls tests/data/test_experiments/piControl_on_PI/mesh/pi/ + cat tests/data/test_experiments/piControl_on_PI/mesh/pi/aux3d.out + cat tests/data/test_experiments/piControl_on_PI/mesh/pi/elem2d.out + cat tests/data/test_experiments/piControl_on_PI/mesh/pi/nod2d.out - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -28,7 +38,7 @@ jobs: if ${{ matrix.python-version == '3.12' }}; then pip install --upgrade setuptools; fi - name: Install package run: | - python -m pip install .[dev] + python -m pip install ".[dev, fesom]" - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.gitmodules b/.gitmodules index 7c18222c..540a447c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "cmip6-cmor-tables"] path = cmip6-cmor-tables url = https://github.com/PCMDI/cmip6-cmor-tables.git +[submodule "tests/data/test_experiments/piControl_on_PI/mesh/pi"] + path = tests/data/test_experiments/piControl_on_PI/mesh/pi + url = https://gitlab.awi.de/fesom/pi.git diff --git a/conftest.py b/conftest.py index 669b3596..0bbedf2e 100644 --- a/conftest.py +++ b/conftest.py @@ -7,13 +7,14 @@ from tests.utils.constants import TEST_ROOT pytest_plugins = [ + "tests.fixtures.CMIP_Tables_Dir", + "tests.fixtures.CV_Dir", + "tests.fixtures.config_files", "tests.fixtures.configs", + "tests.fixtures.datasets", "tests.fixtures.environment", "tests.fixtures.fake_filesystem", "tests.fixtures.sample_rules", - "tests.fixtures.config_files", - "tests.fixtures.CV_Dir", - "tests.fixtures.CMIP_Tables_Dir", ] diff --git a/doc/index.rst b/doc/index.rst index d57f8236..d1fa7579 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,6 +15,7 @@ Contents installation pymorize_building_blocks pymorize_config_file + pymorize_fesom including_subcommand_plugins developer_guide API diff --git a/doc/pymorize_fesom.rst b/doc/pymorize_fesom.rst new file mode 100644 index 00000000..44ed6895 --- /dev/null +++ b/doc/pymorize_fesom.rst @@ -0,0 +1,33 @@ +=============================================== +Usage: ``pymorize`` functionality for ``fesom`` +=============================================== + +In addition to the generic pipeline steps, we also include a few that are specific for ``FESOM``. + +Regridding to a regular grid +---------------------------- + +If you would like to regrid your output to a regular 1x1 degree grid, you can use a pipeline step that +will do this. It is automatically checked with data on the ``pi`` mesh, so it should also handle bigger +regridding tasks, but you may still run into memory issues for very large datasets. Open an issue with a +reproducible mimimal example if you run into this! + +In your ``Rule`` specification, you need to point to the ``mesh_file`` that you would like to use: + +.. code-block:: yaml + + rules: + - name: regrid + mesh_file: /path/to/mesh_folder/with/nod2d # Note, just the folder, not the actual file! + pipelines: + - my_pipeline + +Then, in your pipeline, you can use the step ``pymorize.fesom.regrid_to_regular``: + +.. code-block:: yaml + + pipelines: + - name: my_pipeline + steps: + - pymorize.fesom.regrid_to_regular + diff --git a/setup.py b/setup.py index 9e44e146..4db3f3f6 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,9 @@ def read(filename): "sphinx_rtd_theme", ], "doc": docs_require, + "fesom": [ + "pyfesom2 @ git+https://github.com/fesom/pyfesom2.git@0.3.0", # FIXME(PG): We should talk with Nikolay, this is not optimal... + ], }, entry_points={ "console_scripts": [ diff --git a/src/pymorize/fesom.py b/src/pymorize/fesom.py new file mode 100644 index 00000000..aad8b94e --- /dev/null +++ b/src/pymorize/fesom.py @@ -0,0 +1,378 @@ +import numpy as np +import xarray as xr +from pyfesom2.load_mesh_data import load_mesh + +from .pipeline import FrozenPipeline + + +############################################################################## +# The following is taken directly from pyfesom2, the version that can be +# downloaded via PyPI is out of date and incorrect... +import logging +import os +from collections import namedtuple + +import joblib +import numpy as np +import scipy.spatial.qhull as qhull +import xarray as xr +from numba import jit +from scipy.interpolate import CloughTocher2DInterpolator, LinearNDInterpolator +from scipy.spatial import cKDTree +import scipy + + +def lon_lat_to_cartesian(lon, lat, R=6371000): + """ + calculates lon, lat coordinates of a point on a sphere with + radius R. Taken from http://earthpy.org/interpolation_between_grids_with_ckdtree.html + """ + lon_r = np.radians(lon) + lat_r = np.radians(lat) + + x = R * np.cos(lat_r) * np.cos(lon_r) + y = R * np.cos(lat_r) * np.sin(lon_r) + z = R * np.sin(lat_r) + return x, y, z + + +def create_indexes_and_distances(mesh, lons, lats, k=1, n_jobs=2): + """ + Creates KDTree object and query it for indexes of points in FESOM mesh that are close to the + points of the target grid. Also return distances of the original points to target points. + + Parameters + ---------- + mesh : fesom_mesh object + pyfesom mesh representation + lons/lats : array + 2d arrays with target grid values. + k : int + k-th nearest neighbors to return. + n_jobs : int, optional + Number of jobs to schedule for parallel processing. If -1 is given + all processors are used. Default: 1. + + Returns + ------- + distances : array of floats + The distances to the nearest neighbors. + inds : ndarray of ints + The locations of the neighbors in data. + + """ + xs, ys, zs = lon_lat_to_cartesian(mesh.x2, mesh.y2) + xt, yt, zt = lon_lat_to_cartesian(lons.flatten(), lats.flatten()) + + tree = cKDTree(list(zip(xs, ys, zs))) + + border_version = "1.6.0" + current_version = scipy.__version__ + v1_parts = list(map(int, border_version.split("."))) + v2_parts = list(map(int, current_version.split("."))) + + if v2_parts > v1_parts: + distances, inds = tree.query(list(zip(xt, yt, zt)), k=k, workers=n_jobs) + else: + distances, inds = tree.query(list(zip(xt, yt, zt)), k=k, n_jobs=n_jobs) + + return distances, inds + + +def fesom2regular( + data, + mesh, + lons, + lats, + distances_path=None, + inds_path=None, + qhull_path=None, + how="nn", + k=5, + radius_of_influence=100000, + n_jobs=2, + dumpfile=True, + basepath=None, +): + """ + Interpolates data from FESOM mesh to target (usually regular) mesh. + + Parameters + ---------- + data : array + 1d array that represents FESOM data at one + mesh : fesom_mesh object + pyfesom mesh representation + lons/lats : array + 2d arrays with target grid values. + distances_path : string + Path to the file with distances. If not provided and dumpfile=True, it will be created. + inds_path : string + Path to the file with inds. If not provided and dumpfile=True, it will be created. + qhull_path : str + Path to the file with qhull (needed for linear and cubic interpolations). If not provided and dumpfile=True, it will be created. + how : str + Interpolation method. Options are 'nn' (nearest neighbor), 'idist' (inverce distance), "linear" and "cubic". + k : int + k-th nearest neighbors to use. Only used when how==idist + radius_of_influence : int + Cut off distance in meters, only used in nn and idist. + n_jobs : int, optional + Number of jobs to schedule for parallel processing. If -1 is given + all processors are used. Default: 1. Only used for nn and idist. + dumpfile: bool + wether to dump resulted distances and inds to the file. + basepath: str + path where to store additional interpolation files. If None (default), + the path of the mesh will be used. + + Returns + ------- + data_interpolated : 2d array + array with data interpolated to the target grid. + + """ + if isinstance(data, xr.DataArray): + data = data.data + data = data.squeeze() + + left, right, down, up = np.min(lons), np.max(lons), np.min(lats), np.max(lats) + lonNumber, latNumber = lons.shape[1], lats.shape[0] + + if how == "nn": + kk = 1 + else: + kk = k + + distances_paths = [] + inds_paths = [] + qhull_paths = [] + + MESH_BASE = os.path.basename(mesh.path) + MESH_DIR = mesh.path + CACHE_DIR = os.environ.get("PYFESOM_CACHE", os.path.join(os.getcwd(), "MESH_cache")) + CACHE_DIR = os.path.join(CACHE_DIR, MESH_BASE) + + if not os.path.isdir(CACHE_DIR): + os.makedirs(CACHE_DIR) + + distances_file = "distances_{}_{}_{}_{}_{}_{}_{}_{}".format( + mesh.n2d, left, right, down, up, lonNumber, latNumber, kk + ) + inds_file = "inds_{}_{}_{}_{}_{}_{}_{}_{}".format( + mesh.n2d, left, right, down, up, lonNumber, latNumber, kk + ) + qhull_file = "qhull_{}".format(mesh.n2d) + + distances_paths.append(os.path.join(mesh.path, distances_file)) + distances_paths.append(os.path.join(CACHE_DIR, distances_file)) + + inds_paths.append(os.path.join(mesh.path, inds_file)) + inds_paths.append(os.path.join(CACHE_DIR, inds_file)) + + qhull_paths.append(os.path.join(mesh.path, qhull_file)) + qhull_paths.append(os.path.join(CACHE_DIR, qhull_file)) + + # if distances_path is provided, use it first + if distances_path is not None: + distances_paths.insert(0, distances_path) + + if inds_path is not None: + inds_paths.insert(0, inds_path) + + if qhull_path is not None: + qhull_paths.insert(0, qhull_path) + + loaded_distances = False + loaded_inds = False + loaded_qhull = False + if how == "nn": + for distances_path in distances_paths: + if os.path.isfile(distances_path): + logging.info( + "Note: using precalculated file from {}".format(distances_path) + ) + try: + distances = joblib.load(distances_path) + loaded_distances = True + break + except PermissionError: + # who knows, something didn't work. Try the next path: + continue + for inds_path in inds_paths: + if os.path.isfile(inds_path): + logging.info("Note: using precalculated file from {}".format(inds_path)) + try: + inds = joblib.load(inds_path) + loaded_inds = True + break + except PermissionError: + # Same as above...something is wrong + continue + if not (loaded_distances and loaded_inds): + distances, inds = create_indexes_and_distances( + mesh, lons, lats, k=kk, n_jobs=n_jobs + ) + if dumpfile: + for distances_path in distances_paths: + try: + joblib.dump(distances, distances_path) + break + except PermissionError: + # Couldn't dump the file, try next path + continue + for inds_path in inds_paths: + try: + joblib.dump(inds, inds_path) + break + except PermissionError: + # Couldn't dump inds file, try next + continue + + data_interpolated = data[inds] + data_interpolated[distances >= radius_of_influence] = np.nan + data_interpolated = data_interpolated.reshape(lons.shape) + data_interpolated = np.ma.masked_invalid(data_interpolated) + return data_interpolated + + elif how == "idist": + for distances_path in distances_paths: + if os.path.isfile(distances_path): + logging.info( + "Note: using precalculated file from {}".format(distances_path) + ) + try: + distances = joblib.load(distances_path) + loaded_distances = True + break + except PermissionError: + # who knows, something didn't work. Try the next path: + continue + for inds_path in inds_paths: + if os.path.isfile(inds_path): + logging.info("Note: using precalculated file from {}".format(inds_path)) + try: + inds = joblib.load(inds_path) + loaded_inds = True + break + except PermissionError: + # Same as above...something is wrong + continue + if not (loaded_distances and loaded_inds): + distances, inds = create_indexes_and_distances( + mesh, lons, lats, k=kk, n_jobs=n_jobs + ) + if dumpfile: + for distances_path in distances_paths: + try: + joblib.dump(distances, distances_path) + break + except PermissionError: + # Couldn't dump the file, try next path + continue + for inds_path in inds_paths: + try: + joblib.dump(inds, inds_path) + break + except PermissionError: + # Couldn't dump inds file, try next + continue + + distances_ma = np.ma.masked_greater(distances, radius_of_influence) + + w = 1.0 / distances_ma**2 + data_interpolated = np.ma.sum(w * data[inds], axis=1) / np.ma.sum(w, axis=1) + data_interpolated.shape = lons.shape + data_interpolated = np.ma.masked_invalid(data_interpolated) + return data_interpolated + + elif how == "linear": + for qhull_path in qhull_paths: + if os.path.isfile(qhull_path): + logging.info( + "Note: using precalculated file from {}".format(qhull_path) + ) + try: + qh = joblib.load(qhull_path) + loaded_qhull = True + break + except PermissionError: + # who knows, something didn't work. Try the next path: + continue + if not loaded_qhull: + points = np.vstack((mesh.x2, mesh.y2)).T + qh = qhull.Delaunay(points) + if dumpfile: + for qhull_path in qhull_paths: + try: + joblib.dump(qh, qhull_path) + break + except PermissionError: + continue + data_interpolated = LinearNDInterpolator(qh, data)((lons, lats)) + data_interpolated = np.ma.masked_invalid(data_interpolated) + return data_interpolated + + elif how == "cubic": + for qhull_path in qhull_paths: + if os.path.isfile(qhull_path): + logging.info( + "Note: using precalculated file from {}".format(qhull_path) + ) + logging.info( + "Note: using precalculated file from {}".format(qhull_path) + ) + try: + qh = joblib.load(qhull_path) + loaded_qhull = True + break + except PermissionError: + # who knows, something didn't work. Try the next path: + continue + if not loaded_qhull: + points = np.vstack((mesh.x2, mesh.y2)).T + qh = qhull.Delaunay(points) + if dumpfile: + for qhull_path in qhull_paths: + try: + joblib.dump(qh, qhull_path) + break + except PermissionError: + continue + data_interpolated = CloughTocher2DInterpolator(qh, data)((lons, lats)) + else: + raise ValueError("Interpolation method is not supported") + + +############################################################################## + + +def attach_mesh_to_rule(data, rule): + rule.mesh = load_mesh(rule.mesh_file) + return data + + +def regrid_to_regular(data, rule): + mesh = load_mesh(rule.mesh_file) + box = rule.get("box", "-180, 180, -90, 90") + x_min, x_max, y_min, y_max = map(float, box.split(",")) + x = np.linspace(x_min, x_max, int(x_max - x_min)) + y = np.linspace(y_min, y_max, int(y_max - y_min)) + lon, lat = np.meshgrid(x, y) + # This works on a timestep-by-timestep basis, so we need to + # run an apply here... + # Apply `fesom2regular` function to each time step + interpolated = xr.map_blocks( + fesom2regular, + data, + kwargs={"mesh": mesh, "lons": lon, "lats": lat}, + template=xr.DataArray( + np.empty((len(data["time"]), 360, 180)), dims=["time", "lon", "lat"] + ), + ) + return interpolated + + +class FESOMRegridPipeline(FrozenPipeline): + STEPS = ("pymorize.fesom.regrid_to_regular",) + NAME = "pymorize.fesom.FESOMRegridPipeline" diff --git a/src/pymorize/pipeline.py b/src/pymorize/pipeline.py index b957c15b..20521ec1 100644 --- a/src/pymorize/pipeline.py +++ b/src/pymorize/pipeline.py @@ -179,6 +179,9 @@ class FrozenPipeline(Pipeline): A tuple containing the steps of the pipeline. This is a class-level attribute and cannot be modified. """ + NAME = "FrozenPipeline" + STEPS = () + @property def steps(self): return self._steps @@ -187,6 +190,10 @@ def steps(self): def steps(self, value): raise AttributeError("Cannot set steps on a FrozenPipeline") + def __init__(self, name=NAME, **kwargs): + steps = [get_callable_by_name(name) for name in self.STEPS] + super().__init__(*steps, name=name, **kwargs) + class DefaultPipeline(FrozenPipeline): """ @@ -210,10 +217,7 @@ class DefaultPipeline(FrozenPipeline): "pymorize.generic.show_data", "pymorize.files.save_dataset", ) - - def __init__(self, name="pymorize.pipeline.DefaultPipeline", **kwargs): - steps = [get_callable_by_name(name) for name in self.STEPS] - super().__init__(*steps, name=name, **kwargs) + NAME = "pymorize.pipeline.DefaultPipeline" class TestingPipeline(FrozenPipeline): @@ -239,7 +243,4 @@ class TestingPipeline(FrozenPipeline): "pymorize.generic.dummy_logic_step", "pymorize.generic.dummy_save_data", ) - - def __init__(self, name="pymorize.pipeline.TestingPipeline", **kwargs): - steps = [get_callable_by_name(name) for name in self.STEPS] - super().__init__(*steps, name=name, **kwargs) + NAME = "pymorize.pipeline.TestingPipeline" diff --git a/tests/configs/fesom_pi_mesh_run.yaml b/tests/configs/fesom_pi_mesh_run.yaml new file mode 100644 index 00000000..de98d557 --- /dev/null +++ b/tests/configs/fesom_pi_mesh_run.yaml @@ -0,0 +1,12 @@ +general: + CMIP_TABLES_DIR: ./cmip6-cmor-tables/Tables/ +inherit: + mesh_file: ./tests/data/test_experiments/piControl_on_PI/mesh/pi/ +rules: + - model_variable: sst + inputs: + - path: ./tests/data/test_experiments/piControl_on_PI/output_pi/ + pattern: sst.fesom.*.nc + cmor_variable: tos + output_directory: ./sandbox/ + pipelines: ["pymorize.fesom.FESOMRegridPipeline"] diff --git a/tests/data/example_1.yaml b/tests/data/ls_results/example_1.yaml similarity index 100% rename from tests/data/example_1.yaml rename to tests/data/ls_results/example_1.yaml diff --git a/tests/data/example_2.yaml b/tests/data/ls_results/example_2.yaml similarity index 100% rename from tests/data/example_2.yaml rename to tests/data/ls_results/example_2.yaml diff --git a/tests/data/test_experiments/piControl_on_PI/mesh/pi b/tests/data/test_experiments/piControl_on_PI/mesh/pi new file mode 160000 index 00000000..087f3cca --- /dev/null +++ b/tests/data/test_experiments/piControl_on_PI/mesh/pi @@ -0,0 +1 @@ +Subproject commit 087f3cca7f41590f66031eef1d105ee1291b2845 diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/a_ice.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/a_ice.fesom.1948.nc new file mode 100644 index 00000000..28d57cbd Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/a_ice.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.clock b/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.clock new file mode 100644 index 00000000..901f18fe --- /dev/null +++ b/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.clock @@ -0,0 +1,2 @@ + 85500.000000000000 1 1948 + 86400.000000000000 1 1948 diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.mesh.diag.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.mesh.diag.nc new file mode 100644 index 00000000..749ebe3a Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/fesom.mesh.diag.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/salt.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/salt.fesom.1948.nc new file mode 100644 index 00000000..2bf059bd Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/salt.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/sst.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/sst.fesom.1948.nc new file mode 100644 index 00000000..19ad77b5 Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/sst.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/temp.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/temp.fesom.1948.nc new file mode 100644 index 00000000..5d72207c Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/temp.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/u.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/u.fesom.1948.nc new file mode 100644 index 00000000..d9e98696 Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/u.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/uice.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/uice.fesom.1948.nc new file mode 100644 index 00000000..3695b6e2 Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/uice.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/v.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/v.fesom.1948.nc new file mode 100644 index 00000000..b2584fd4 Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/v.fesom.1948.nc differ diff --git a/tests/data/test_experiments/piControl_on_PI/output_pi/vice.fesom.1948.nc b/tests/data/test_experiments/piControl_on_PI/output_pi/vice.fesom.1948.nc new file mode 100644 index 00000000..6354dffe Binary files /dev/null and b/tests/data/test_experiments/piControl_on_PI/output_pi/vice.fesom.1948.nc differ diff --git a/tests/externals/__init__.py b/tests/externals/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/externals/test_pyfesom2.py b/tests/externals/test_pyfesom2.py new file mode 100644 index 00000000..76b47b99 --- /dev/null +++ b/tests/externals/test_pyfesom2.py @@ -0,0 +1,16 @@ +""" +Tests for pyfesom2 functionality as used in pymorize +""" + +from pyfesom2.load_mesh_data import load_mesh + +import pymorize.rule + + +def test_read_grid_from_rule(fesom_pi_mesh_config): + config = fesom_pi_mesh_config + mesh_path = config["inherit"]["mesh_file"] + rule = pymorize.rule.Rule.from_dict(config["rules"][0]) + rule.mesh_file = config["inherit"]["mesh_file"] + + load_mesh(mesh_path) diff --git a/tests/fixtures/config_files.py b/tests/fixtures/config_files.py index 1eb52f14..010a3f7b 100644 --- a/tests/fixtures/config_files.py +++ b/tests/fixtures/config_files.py @@ -6,3 +6,8 @@ @pytest.fixture def test_config(): return TEST_ROOT / "configs" / "test_config.yaml" + + +@pytest.fixture +def fesom_pi_mesh_config_file(): + return TEST_ROOT / "configs/fesom_pi_mesh_run.yaml" diff --git a/tests/fixtures/configs.py b/tests/fixtures/configs.py index 418ad19e..9ded68cc 100644 --- a/tests/fixtures/configs.py +++ b/tests/fixtures/configs.py @@ -1,4 +1,5 @@ import pytest +import ruamel.yaml @pytest.fixture @@ -37,3 +38,9 @@ def config_pattern_env_var_name_and_value(): "pattern_env_var_value": "other_test.*nc", } } + + +@pytest.fixture +def fesom_pi_mesh_config(fesom_pi_mesh_config_file): + yaml = ruamel.yaml.YAML() + return yaml.load(fesom_pi_mesh_config_file.open()) diff --git a/tests/fixtures/datasets.py b/tests/fixtures/datasets.py new file mode 100644 index 00000000..9b73bbf8 --- /dev/null +++ b/tests/fixtures/datasets.py @@ -0,0 +1,11 @@ +import pytest +import xarray as xr + +from tests.utils.constants import TEST_ROOT + + +@pytest.fixture +def fesom_pi_sst_ds(): + return xr.open_dataset( + TEST_ROOT / "data/test_experiments/piControl_on_PI/output_pi/sst.fesom.1948.nc" + ) diff --git a/tests/unit/test_fesom.py b/tests/unit/test_fesom.py new file mode 100644 index 00000000..c821f812 --- /dev/null +++ b/tests/unit/test_fesom.py @@ -0,0 +1,22 @@ +import pymorize +import pymorize.fesom + + +def test_regridding(fesom_pi_mesh_config, fesom_pi_sst_ds): + config = fesom_pi_mesh_config + rule = pymorize.rule.Rule.from_dict(config["rules"][0]) + rule.mesh_file = config["inherit"]["mesh_file"] + da = fesom_pi_sst_ds.sst + da = pymorize.fesom.regrid_to_regular(da, rule) + assert da.shape == (180, 360) + + +def test_attach_mesh_to_rule(fesom_pi_mesh_config): + config = fesom_pi_mesh_config + rule = pymorize.rule.Rule.from_dict(config["rules"][0]) + rule.mesh_file = config["inherit"]["mesh_file"] + data = None # Not important for this test + assert not hasattr(rule, "mesh") + # _ symbolizes just any return value, which we never use + _ = pymorize.fesom.attach_mesh_to_rule(data, rule) + assert hasattr(rule, "mesh")