diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ac1fc3..3216d16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: OM3Utils +name: esmgrids on: push: diff --git a/README.md b/README.md index fb7c7d9..aa66221 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ - -[![Code Health](https://landscape.io/github/DoublePrecision/esmgrids/master/landscape.svg?style=flat)](https://landscape.io/github/DoublePrecision/esmgrids/master) -[![Build Status](https://travis-ci.org/DoublePrecision/esmgrids.svg?branch=master)](https://travis-ci.org/DoublePrecision/esmgrids) +![Code Health](https://github.com/COSIMA/esmgrids/actions/workflows/ci.yml/badge.svg) # esmgrids @@ -19,10 +17,6 @@ Grids currently supported: ## To run the tests ``` -conda env create -n grids python3 -source activate grids -python -m pytest +pip install '.[tests]' +python -m pytest -m "not broken" ``` - -Warning: this will download a rather large tarball of test inputs. - diff --git a/esmgrids/__init__.py b/esmgrids/__init__.py index e69de29..a591580 100644 --- a/esmgrids/__init__.py +++ b/esmgrids/__init__.py @@ -0,0 +1,30 @@ +import re +import warnings +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("esmgrids") +except PackageNotFoundError: + # package is not installed + pass + + +def safe_version(): + """ + Returns the version, issuing a warning if there are revisions since the last tag + and an error if there are uncommitted changes + + This function assumes the setuptools_scm default versioning scheme - see + https://setuptools-scm.readthedocs.io/en/latest/usage/#default-versioning-scheme + """ + if re.match(r".*\d{8}$", __version__): + warnings.warn( + ( + "There are uncommitted changes! Commit, push and release these changes before " + "generating any production files." + ) + ) + elif re.match(r".*dev.*", __version__): + warnings.warn(("There are unreleased commits! Do a release before generating any production files.")) + + return __version__ diff --git a/esmgrids/base_grid.py b/esmgrids/base_grid.py index 4b79d3f..617ec24 100644 --- a/esmgrids/base_grid.py +++ b/esmgrids/base_grid.py @@ -1,7 +1,7 @@ import numpy as np import netCDF4 as nc -from .util import calc_area_of_polygons +from esmgrids.util import calc_area_of_polygons class BaseGrid(object): diff --git a/esmgrids/cice_grid.py b/esmgrids/cice_grid.py index 3f14925..6445cf8 100644 --- a/esmgrids/cice_grid.py +++ b/esmgrids/cice_grid.py @@ -1,13 +1,13 @@ import numpy as np import netCDF4 as nc -from .base_grid import BaseGrid +from esmgrids.base_grid import BaseGrid class CiceGrid(BaseGrid): def __init__(self, **kwargs): - self.type = "Arakawa B" + self.type = "Arakawa B / C" self.full_name = "CICE" super(CiceGrid, self).__init__(**kwargs) @@ -65,66 +65,95 @@ def fromfile(cls, h_grid_def, mask_file=None, description="CICE tripolar"): description=description, ) - def write(self, grid_filename, mask_filename): + def _create_2d_nc_var(self, f, name): + return f.createVariable( + name, + "f8", + dimensions=("ny", "nx"), + compression="zlib", + complevel=1, + ) + + def write(self, grid_filename, mask_filename, metadata=None): """ - Write out CICE grid to netcdf. + Write out CICE grid to netcdf + + Parameters + ---------- + grid_filename: str + The name of the grid file to write + mask_filename: str + The name of the mask file to write + metadata: dict + Any global or variable metadata attributes to add to the files being written """ + # Grid file f = nc.Dataset(grid_filename, "w") # Create dimensions. f.createDimension("nx", self.num_lon_points) + # nx is the grid_longitude but doesn't have a value other than its index f.createDimension("ny", self.num_lat_points) - f.createDimension("nc", 4) + # ny is the grid_latitude but doesn't have a value other than its index # Make all CICE grid variables. - ulat = f.createVariable("ulat", "f8", dimensions=("ny", "nx")) + # names are based on https://cfconventions.org/Data/cf-standard-names/current/build/cf-standard-name-table.html + f.Conventions = "CF-1.6" + + ulat = self._create_2d_nc_var(f, "ulat") ulat.units = "radians" - ulat.title = "Latitude of U points" - ulon = f.createVariable("ulon", "f8", dimensions=("ny", "nx")) + ulat.long_name = "Latitude of U points" + ulat.standard_name = "latitude" + ulon = self._create_2d_nc_var(f, "ulon") ulon.units = "radians" - ulon.title = "Longitude of U points" - tlat = f.createVariable("tlat", "f8", dimensions=("ny", "nx")) + ulon.long_name = "Longitude of U points" + ulon.standard_name = "longitude" + tlat = self._create_2d_nc_var(f, "tlat") tlat.units = "radians" - tlat.title = "Latitude of T points" - tlon = f.createVariable("tlon", "f8", dimensions=("ny", "nx")) + tlat.long_name = "Latitude of T points" + tlat.standard_name = "latitude" + tlon = self._create_2d_nc_var(f, "tlon") tlon.units = "radians" - tlon.title = "Longitude of T points" - - if self.clon_t is not None: - clon_t = f.createVariable("clon_t", "f8", dimensions=("nc", "ny", "nx")) - clon_t.units = "radians" - clon_t.title = "Longitude of T cell corners" - clat_t = f.createVariable("clat_t", "f8", dimensions=("nc", "ny", "nx")) - clat_t.units = "radians" - clat_t.title = "Latitude of T cell corners" - clon_u = f.createVariable("clon_u", "f8", dimensions=("nc", "ny", "nx")) - clon_u.units = "radians" - clon_u.title = "Longitude of U cell corners" - clat_u = f.createVariable("clat_u", "f8", dimensions=("nc", "ny", "nx")) - clat_u.units = "radians" - clat_u.title = "Latitude of U cell corners" - - htn = f.createVariable("htn", "f8", dimensions=("ny", "nx")) + tlon.long_name = "Longitude of T points" + tlon.standard_name = "longitude" + + htn = self._create_2d_nc_var(f, "htn") htn.units = "cm" - htn.title = "Width of T cells on North side." - hte = f.createVariable("hte", "f8", dimensions=("ny", "nx")) + htn.long_name = "Width of T cells on North side." + htn.coordinates = "ulat tlon" + htn.grid_mapping = "crs" + hte = self._create_2d_nc_var(f, "hte") hte.units = "cm" - hte.title = "Width of T cells on East side." + hte.long_name = "Width of T cells on East side." + hte.coordinates = "tlat ulon" + hte.grid_mapping = "crs" - angle = f.createVariable("angle", "f8", dimensions=("ny", "nx")) + angle = self._create_2d_nc_var(f, "angle") angle.units = "radians" - angle.title = "Rotation angle of U cells." - angleT = f.createVariable("angleT", "f8", dimensions=("ny", "nx")) + angle.long_name = "Rotation angle of U cells." + angle.standard_name = "angle_of_rotation_from_east_to_x" + angle.coordinates = "ulat ulon" + angle.grid_mapping = "crs" + angleT = self._create_2d_nc_var(f, "angleT") angleT.units = "radians" - angleT.title = "Rotation angle of T cells." + angleT.long_name = "Rotation angle of T cells." + angleT.standard_name = "angle_of_rotation_from_east_to_x" + angleT.coordinates = "tlat tlon" + angleT.grid_mapping = "crs" - area_t = f.createVariable("tarea", "f8", dimensions=("ny", "nx")) + area_t = self._create_2d_nc_var(f, "tarea") area_t.units = "m^2" - area_t.title = "Area of T cells." - area_u = f.createVariable("uarea", "f8", dimensions=("ny", "nx")) + area_t.long_name = "Area of T cells." + area_t.standard_name = "cell_area" + area_t.coordinates = "tlat tlon" + area_t.grid_mapping = "crs" + area_u = self._create_2d_nc_var(f, "uarea") area_u.units = "m^2" - area_u.title = "Area of U cells." + area_u.long_name = "Area of U cells." + area_u.standard_name = "cell_area" + area_u.coordinates = "ulat ulon" + area_u.grid_mapping = "crs" area_t[:] = self.area_t[:] area_u[:] = self.area_u[:] @@ -135,12 +164,6 @@ def write(self, grid_filename, mask_filename): ulat[:] = np.deg2rad(self.y_u) ulon[:] = np.deg2rad(self.x_u) - if self.clon_t is not None: - clon_t[:] = np.deg2rad(self.clon_t) - clat_t[:] = np.deg2rad(self.clat_t) - clon_u[:] = np.deg2rad(self.clon_u) - clat_u[:] = np.deg2rad(self.clat_u) - # Convert from m to cm. htn[:] = self.dx_tn[:] * 100.0 hte[:] = self.dy_te[:] * 100.0 @@ -148,11 +171,36 @@ def write(self, grid_filename, mask_filename): angle[:] = np.deg2rad(self.angle_u[:]) angleT[:] = np.deg2rad(self.angle_t[:]) + # Add the typical crs (i.e. WGS84/EPSG4326 , but in radians). + crs = f.createVariable("crs", "S1") + crs.crs_wkt = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["radians",1,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]' + + # Add global metadata + if metadata: + for attr, meta in metadata.items(): + f.setncattr(attr, meta) + f.close() - with nc.Dataset(mask_filename, "w") as f: - f.createDimension("nx", self.num_lon_points) - f.createDimension("ny", self.num_lat_points) - mask = f.createVariable("kmt", "f8", dimensions=("ny", "nx")) - # CICE uses 0 as masked, whereas internally we use 1 as masked. - mask[:] = 1 - self.mask_t + # Mask file + f = nc.Dataset(mask_filename, "w") + + f.createDimension("nx", self.num_lon_points) + f.createDimension("ny", self.num_lat_points) + mask = self._create_2d_nc_var(f, "kmt") + mask.grid_mapping = "crs" + mask.standard_name = "sea_binary_mask" + + # CICE uses 0 as masked, whereas internally we use 1 as masked. + mask[:] = 1 - self.mask_t + + # Add the typical crs (i.e. WGS84/EPSG4326 , but in radians). + crs = f.createVariable("crs", "S1") + crs.crs_wkt = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["radians",1,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]' + + # Add global metadata + if metadata: + for attr, meta in metadata.items(): + f.setncattr(attr, meta) + + f.close() diff --git a/esmgrids/cli.py b/esmgrids/cli.py new file mode 100644 index 0000000..08c853d --- /dev/null +++ b/esmgrids/cli.py @@ -0,0 +1,40 @@ +import os +import argparse + +from esmgrids import safe_version +from esmgrids.util import md5sum +from esmgrids.mom_grid import MomGrid +from esmgrids.cice_grid import CiceGrid + + +def cice_from_mom(): + """Script for creating CICE grid files from MOM grid files""" + + parser = argparse.ArgumentParser(description="Create CICE grid files from MOM grid files") + parser.add_argument("--ocean_hgrid", type=str, help="Input MOM ocean_hgrid.nc supergrid file") + parser.add_argument("--ocean_mask", type=str, help="Input MOM ocean_mask.nc mask file") + parser.add_argument("--cice_grid", type=str, default="grid.nc", help="Output CICE grid file") + parser.add_argument("--cice_kmt", type=str, default="kmt.nc", help="Output CICE kmt file") + + args = parser.parse_args() + ocean_hgrid = os.path.abspath(args.ocean_hgrid) + ocean_mask = os.path.abspath(args.ocean_mask) + cice_grid = os.path.abspath(args.cice_grid) + cice_kmt = os.path.abspath(args.cice_kmt) + + version = safe_version() + runcmd = ( + f"Created using https://github.com/COSIMA/esmgrids {version}: " + f"cice_from_mom --ocean_hgrid={ocean_hgrid} --ocean_mask={ocean_mask} " + f"--cice_grid={cice_grid} --cice_kmt={cice_kmt}" + ) + provenance_metadata = { + "inputfile": ( + f"{ocean_hgrid} (md5 hash: {md5sum(ocean_hgrid)}), " f"{ocean_mask} (md5 hash: {md5sum(ocean_mask)})" + ), + "history": runcmd, + } + + mom = MomGrid.fromfile(ocean_hgrid, mask_file=ocean_mask) + cice = CiceGrid.fromgrid(mom) + cice.write(cice_grid, cice_kmt, metadata=provenance_metadata) diff --git a/esmgrids/mom_grid.py b/esmgrids/mom_grid.py index c59d7dd..ec4297b 100644 --- a/esmgrids/mom_grid.py +++ b/esmgrids/mom_grid.py @@ -1,7 +1,7 @@ import numpy as np import netCDF4 as nc -from .base_grid import BaseGrid +from esmgrids.base_grid import BaseGrid class MomGrid(BaseGrid): @@ -86,9 +86,8 @@ def fromfile(cls, h_grid_def, v_grid_def=None, mask_file=None, calc_areas=True, dy_ue = dy_ext[1::2, 3::2] + dy_ext[2::2, 3::2] angle_dx = f.variables["angle_dx"][:] - # The angle of rotation is a corner cell centres and applies to - # both t and u cells. - angle_t = angle_dx[2::2, 2::2] + + angle_t = angle_dx[1::2, 1::2] angle_u = angle_dx[2::2, 2::2] area = f.variables["area"][:] diff --git a/esmgrids/util.py b/esmgrids/util.py index cb6b0b4..a0fdb68 100644 --- a/esmgrids/util.py +++ b/esmgrids/util.py @@ -2,6 +2,7 @@ import pyproj from shapely.geometry import shape + proj_str = "+proj=laea +lat_0={} +lon_0={} +ellps=sphere" @@ -39,3 +40,11 @@ def calc_area_of_polygons(clons, clats): assert np.min(areas) > 0 return areas + + +def md5sum(filename): + from hashlib import md5 + from mmap import mmap, ACCESS_READ + + with open(filename) as file, mmap(file.fileno(), 0, access=ACCESS_READ) as file: + return md5(file).hexdigest() diff --git a/pyproject.toml b/pyproject.toml index 4295a15..0eab91d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "numpy", "netcdf4", "shapely", - "pyproj", + "pyproj" ] [build-system] @@ -48,8 +48,13 @@ test = [ "pytest", "pytest-cov", "sh", + "xarray", + "ocean_model_grid_generator@git+https://github.com/nikizadehgfdl/ocean_model_grid_generator@790069b31f9791864ccd514a2b8f53f385a0452e" ] +[project.scripts] +cice_from_mom = "esmgrids.cli:cice_from_mom" + [tool.pytest.ini_options] addopts = ["--cov=esmgrids", "--cov-report=term", "--cov-report=xml"] testpaths = ["test"] diff --git a/test/test_cice_grid.py b/test/test_cice_grid.py new file mode 100644 index 0000000..77e2f45 --- /dev/null +++ b/test/test_cice_grid.py @@ -0,0 +1,231 @@ +import pytest +import xarray as xr +from numpy.testing import assert_allclose +from numpy import deg2rad +from subprocess import run +from pathlib import Path + +# from esmgrids.cli import cice_from_mom + +# create test grids at 4 degrees and 0.1 degrees +# 4 degress is the lowest tested in ocean_model_grid_generator +# going higher resolution than 0.1 has too much computational cost +_test_resolutions = [4, 0.1] + + +# so that our fixtures are only create once in this pytest module, we need this special version of 'tmp_path' +@pytest.fixture(scope="module") +def tmp_path(tmp_path_factory: pytest.TempdirFactory) -> Path: + return tmp_path_factory.mktemp("temp") + + +# ---------------- +# test data: +class MomGridFixture: + """Generate a sample tripole grid to use as test data""" + + def __init__(self, res, tmp_path): + self.path = str(tmp_path) + "/ocean_hgrid.nc" + self.mask_path = str(tmp_path) + "/ocean_mask.nc" + + # generate a tripolar grid as test data + run( + [ + "ocean_grid_generator.py", + "-r", + str(1 / res), + "--no_south_cap", + "--ensure_nj_even", + "-f", + self.path, + ] + ) + + # open ocean_hgrid.nc + self.ds = xr.open_dataset(self.path) + + # an ocean mask with a arbitrary mask + self.mask_ds = xr.Dataset() + self.mask_ds["mask"] = (self.ds.area.coarsen(ny=2).sum().coarsen(nx=2).sum()) > 5e9 + self.mask_ds.to_netcdf(self.mask_path) + + +class CiceGridFixture: + """Make the CICE grid, using script under test""" + + def __init__(self, mom_grid, tmp_path): + self.path = str(tmp_path) + "/grid.nc" + self.kmt_path = str(tmp_path) + "/kmt.nc" + run( + [ + "cice_from_mom", + "--ocean_hgrid", + mom_grid.path, + "--ocean_mask", + mom_grid.mask_path, + "--cice_grid", + self.path, + "--cice_kmt", + self.kmt_path, + ] + ) + self.ds = xr.open_dataset(self.path, decode_cf=False) + self.kmt_ds = xr.open_dataset(self.kmt_path, decode_cf=False) + + +# pytest doesn't support class fixtures, so we need these two constructor funcs +@pytest.fixture(scope="module", params=_test_resolutions) +def mom_grid(request, tmp_path): + return MomGridFixture(request.param, tmp_path) + + +@pytest.fixture(scope="module") +def cice_grid(mom_grid, tmp_path): + return CiceGridFixture(mom_grid, tmp_path) + + +@pytest.fixture(scope="module") +def test_grid_ds(mom_grid): + # this generates the expected answers + # In simple terms the MOM supergrid has four cells for each model grid cell. The MOM supergrid includes all edges (left and right) but CICE only uses right/east edges. (e.g. For points/edges of first cell: 0,0 is SW corner, 1,1 is middle of cell, 2,2, is NE corner/edges) + + ds = mom_grid.ds + + # u points are at top-right (NE) corner + u_points = ds.isel(nxp=slice(2, None, 2), nyp=slice(2, None, 2)) + + # t points are in middle of cell + t_points = ds.isel(nxp=slice(1, None, 2), nyp=slice(1, None, 2)) + + test_grid = xr.Dataset() + + test_grid["ulat"] = deg2rad(u_points.y) + test_grid["ulon"] = deg2rad(u_points.x) + test_grid["tlat"] = deg2rad(t_points.y) + test_grid["tlon"] = deg2rad(t_points.x) + + test_grid["angle"] = deg2rad(u_points.angle_dx) # angle at u point + test_grid["angleT"] = deg2rad(t_points.angle_dx) + + # length of top (northern) edge of cells + test_grid["htn"] = ds.dx.isel(nyp=slice(2, None, 2)).coarsen(nx=2).sum() * 100 + # length of right (eastern) edge of cells + test_grid["hte"] = ds.dy.isel(nxp=slice(2, None, 2)).coarsen(ny=2).sum() * 100 + + # area of cells + test_grid["tarea"] = ds.area.coarsen(ny=2).sum().coarsen(nx=2).sum() + + # uarea is area of a cell centred around the u point + # we need to fold on longitude and wrap in latitude to calculate this + # drop the bottom row, new top row is reverse of current top row + area_folded = xr.concat([ds.area.isel(ny=slice(1, None)), ds.area.isel(ny=-1, nx=slice(-1, None, -1))], dim="ny") + + # drop the first column, make the new last column the first column + area_wrapped = xr.concat([area_folded.isel(nx=slice(1, None)), area_folded.isel(nx=0)], dim="nx") + + test_grid["uarea"] = area_wrapped.coarsen(ny=2).sum().coarsen(nx=2).sum() + + return test_grid + + +# ---------------- +# the tests in earnest: + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_cice_var_list(cice_grid, test_grid_ds): + # Test : Are there missing vars in cice_grid? + assert set(test_grid_ds.variables).difference(cice_grid.ds.variables) == set() + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_cice_grid(cice_grid, test_grid_ds): + # Test : Is the data the same as the test_grid + for jVar in test_grid_ds.variables: + assert_allclose(cice_grid.ds[jVar], test_grid_ds[jVar], rtol=1e-13, verbose=True, err_msg=f"{jVar} mismatch") + + +def test_cice_kmt(mom_grid, cice_grid): + # Test : does the mask match + mask = mom_grid.mask_ds.mask + kmt = cice_grid.kmt_ds.kmt + + assert_allclose(mask, kmt, rtol=1e-13, verbose=True, err_msg="mask mismatch") + + +def test_cice_grid_attributes(cice_grid): + # Test: do the expected attributes to exist in the cice ds + cf_attributes = { + "ulat": {"standard_name": "latitude", "units": "radians"}, + "ulon": {"standard_name": "longitude", "units": "radians"}, + "tlat": {"standard_name": "latitude", "units": "radians"}, + "tlon": {"standard_name": "longitude", "units": "radians"}, + "uarea": { + "standard_name": "cell_area", + "units": "m^2", + "grid_mapping": "crs", + "coordinates": "ulat ulon", + }, + "tarea": { + "standard_name": "cell_area", + "units": "m^2", + "grid_mapping": "crs", + "coordinates": "tlat tlon", + }, + "angle": { + "standard_name": "angle_of_rotation_from_east_to_x", + "units": "radians", + "grid_mapping": "crs", + "coordinates": "ulat ulon", + }, + "angleT": { + "standard_name": "angle_of_rotation_from_east_to_x", + "units": "radians", + "grid_mapping": "crs", + "coordinates": "tlat tlon", + }, + "htn": {"units": "cm", "coordinates": "ulat tlon", "grid_mapping": "crs"}, + "hte": {"units": "cm", "coordinates": "tlat ulon", "grid_mapping": "crs"}, + } + + for iVar in cf_attributes.keys(): + print(cice_grid.ds[iVar]) + + for jAttr in cf_attributes[iVar].keys(): + assert cice_grid.ds[iVar].attrs[jAttr] == cf_attributes[iVar][jAttr] + + +def test_crs_exist(cice_grid): + # Test: has the crs been added ? + # todo: open with GDAL and rioxarray and confirm they find the crs? + assert hasattr(cice_grid.ds, "crs") + assert hasattr(cice_grid.kmt_ds, "crs") + + +def test_inputs_logged(cice_grid, mom_grid): + # Test: have the source data been logged ? + + input_md5 = run(["md5sum", cice_grid.ds.inputfile], capture_output=True, text=True) + input_md5 = input_md5.stdout.split(" ")[0] + mask_md5 = run(["md5sum", cice_grid.kmt_ds.inputfile], capture_output=True, text=True) + mask_md5 = mask_md5.stdout.split(" ")[0] + + for ds in [cice_grid.ds, cice_grid.kmt_ds]: + assert ( + ds.inputfile + == ( + mom_grid.path + + " (md5 hash: " + + input_md5 + + "), " + + mom_grid.mask_path + + " (md5 hash: " + + mask_md5 + + ")" + ), + "inputfile attribute incorrect ({ds.inputfile} != {mom_grid.path})", + ) + + assert hasattr(ds, "inputfile"), "inputfile attribute missing" + + assert hasattr(ds, "history"), "history attribute missing" diff --git a/test/test_grids.py b/test/test_grids.py index f16c636..410b034 100644 --- a/test/test_grids.py +++ b/test/test_grids.py @@ -89,22 +89,6 @@ def input_dir(self): return os.path.join(self.test_data_dir, "input") - @pytest.mark.broken - def test_convert_mom_to_cice(self, input_dir, output_dir): - """ - Read in a MOM grid and write out a cice grid at the same resolution. - """ - - mask = os.path.join(input_dir, "ocean_01_mask.nc") - hgrid = os.path.join(input_dir, "ocean_01_hgrid.nc") - mom = MomGrid.fromfile(hgrid, mask_file=mask) - cice = CiceGrid.fromgrid(mom) - grid_file = os.path.join(output_dir, "grid.nc") - mask_file = os.path.join(output_dir, "kmt.nc") - cice.write(grid_file, mask_file) - - # FIXME actually test that the CICE grid is good. - @pytest.mark.broken def test_corners(self, input_dir): """