diff --git a/compass/ocean/inactive_top_cells.py b/compass/ocean/inactive_top_cells.py
new file mode 100644
index 0000000000..0f1ca7af62
--- /dev/null
+++ b/compass/ocean/inactive_top_cells.py
@@ -0,0 +1,45 @@
+import os
+
+import xarray
+from mpas_tools.io import write_netcdf
+
+
+def remove_inactive_top_cells_output(in_filename, out_filename=None,
+ mesh_filename=None):
+ """
+ Remove inactive top cells from the output netCDF file
+
+ Parameters
+ ----------
+ in_filename : str
+ Filename for the netCDF file to be cropped
+
+ out_filename : str, optional
+ Filename for the netCDF file after cropping. Tbe default name is the
+ original file name with ``_crop`` appended before the extension
+
+ mesh_filename : str, optional
+ Filename for an MPAS mesh if not included in the file to be cropped
+
+ """
+ if not os.path.exists(in_filename):
+ raise OSError(f'File {in_filename} does not exist.')
+
+ if out_filename is None:
+ basename, ext = os.path.splitext(in_filename)
+ out_filename = f'{basename}_crop{ext}'
+
+ with xarray.open_dataset(in_filename) as ds_in:
+ if mesh_filename is None:
+ ds_mesh = ds_in
+ else:
+ ds_mesh = xarray.open_dataset(mesh_filename)
+ minLevelCell = ds_mesh.minLevelCell
+ minval = minLevelCell.min().values
+ maxval = minLevelCell.max().values
+ if minval != maxval:
+ raise ValueError('Expected minLevelCell to have a constant '
+ 'value for inactive top cell tests')
+ ds_out = ds_in.isel(nVertLevels=slice(minval - 1, None))
+
+ write_netcdf(ds_out, out_filename)
diff --git a/compass/ocean/suites/inactive_top.txt b/compass/ocean/suites/inactive_top.txt
new file mode 100644
index 0000000000..2f8b40d60c
--- /dev/null
+++ b/compass/ocean/suites/inactive_top.txt
@@ -0,0 +1,7 @@
+ocean/global_ocean/QU240/mesh
+ocean/global_ocean/QU240/WOA23/init
+ocean/global_ocean/QU240/WOA23/init_inactive_top
+ocean/global_ocean/QU240/WOA23/performance_test
+ocean/global_ocean/QU240/WOA23/performance_test_inactive_top
+ocean/global_ocean/QU240/WOA23/RK4/performance_test
+ocean/global_ocean/QU240/WOA23/RK4/performance_test_inactive_top
diff --git a/compass/ocean/tests/global_ocean/__init__.py b/compass/ocean/tests/global_ocean/__init__.py
index b68a3f6d58..d81e7b5ac9 100644
--- a/compass/ocean/tests/global_ocean/__init__.py
+++ b/compass/ocean/tests/global_ocean/__init__.py
@@ -56,7 +56,8 @@ def __init__(self, mpas_core):
include_rk4=True,
include_regression=True,
include_phc=True,
- include_en4_1900=True)
+ include_en4_1900=True,
+ with_inactive_top_cells=True)
# for other meshes, we do fewer tests
self._add_tests(mesh_names=['QU', 'Icos', 'QUwISC', 'IcoswISC'],
@@ -88,7 +89,7 @@ def __init__(self, mpas_core):
def _add_tests(self, mesh_names, DynamicAdjustment,
high_res_topography=True, include_rk4=False,
include_regression=False, include_phc=False,
- include_en4_1900=False):
+ include_en4_1900=False, with_inactive_top_cells=False):
""" Add test cases for the given mesh(es) """
default_ic = 'WOA23'
@@ -100,7 +101,8 @@ def _add_tests(self, mesh_names, DynamicAdjustment,
self.add_test_case(mesh_test)
init_test = Init(test_group=self, mesh=mesh_test,
- initial_condition=default_ic)
+ initial_condition=default_ic,
+ with_inactive_top_cells=False)
self.add_test_case(init_test)
time_integrator = default_time_int
@@ -163,6 +165,21 @@ def _add_tests(self, mesh_names, DynamicAdjustment,
test_group=self, mesh=mesh_test, init=init_test,
time_integrator=time_integrator))
+ if with_inactive_top_cells:
+ init_test = Init(test_group=self, mesh=mesh_test,
+ initial_condition=default_ic,
+ with_inactive_top_cells=True)
+ self.add_test_case(init_test)
+ self.add_test_case(
+ PerformanceTest(
+ test_group=self, mesh=mesh_test, init=init_test,
+ time_integrator=default_time_int))
+ if include_rk4:
+ self.add_test_case(
+ PerformanceTest(
+ test_group=self, mesh=mesh_test, init=init_test,
+ time_integrator='RK4'))
+
initial_conditions = []
if include_phc:
initial_conditions.append('PHC')
diff --git a/compass/ocean/tests/global_ocean/forward.py b/compass/ocean/tests/global_ocean/forward.py
index bcb30eb0f3..2b11836c45 100644
--- a/compass/ocean/tests/global_ocean/forward.py
+++ b/compass/ocean/tests/global_ocean/forward.py
@@ -3,6 +3,7 @@
from importlib.resources import contents
from compass.model import run_model
+from compass.ocean.inactive_top_cells import remove_inactive_top_cells_output
from compass.ocean.tests.global_ocean.metadata import (
add_mesh_and_init_metadata,
)
@@ -155,6 +156,9 @@ def __init__(self, test_case, mesh, time_integrator, init=None,
filename='graph.info',
work_dir_target=f'{init.path}/initial_state/graph.info')
+ if init.with_inactive_top_cells:
+ self.add_output_file(filename='output_crop.nc')
+
self.add_model_as_input()
def setup(self):
@@ -214,6 +218,12 @@ def run(self):
update_pio = self.config.getboolean('global_ocean',
'forward_update_pio')
run_model(self, update_pio=update_pio)
+
+ if self.init.with_inactive_top_cells:
+ remove_inactive_top_cells_output(in_filename='output.nc',
+ out_filename='output_crop.nc',
+ mesh_filename='init.nc')
+
add_mesh_and_init_metadata(self.outputs, self.config,
init_filename='init.nc')
diff --git a/compass/ocean/tests/global_ocean/init/__init__.py b/compass/ocean/tests/global_ocean/init/__init__.py
index c59841abeb..8d6081de6e 100644
--- a/compass/ocean/tests/global_ocean/init/__init__.py
+++ b/compass/ocean/tests/global_ocean/init/__init__.py
@@ -24,8 +24,14 @@ class Init(TestCase):
init_subdir : str
The subdirectory within the test group for all test cases with this
initial condition
+
+ inactive_top_comp_subdir : str
+ If ``with_inactive_top_cells == True``, the subdirectory equivalent to
+ ``init_subdir`` for test cases without inactive top cells for use for
+ validation between tests with and without inactive top cells
"""
- def __init__(self, test_group, mesh, initial_condition):
+ def __init__(self, test_group, mesh, initial_condition,
+ with_inactive_top_cells=False):
"""
Create the test case
@@ -43,17 +49,25 @@ def __init__(self, test_group, mesh, initial_condition):
name = 'init'
mesh_name = mesh.mesh_name
ic_dir = initial_condition
- self.init_subdir = os.path.join(mesh_name, ic_dir)
+ init_subdir = os.path.join(mesh_name, ic_dir)
+ if with_inactive_top_cells:
+ # Add the name of the subdir without inactive top cells whether or
+ # not is has or will be run
+ self.inactive_top_comp_subdir = init_subdir
+ name = f'{name}_inactive_top'
+ self.init_subdir = init_subdir
subdir = os.path.join(self.init_subdir, name)
super().__init__(test_group=test_group, name=name, subdir=subdir)
self.mesh = mesh
self.initial_condition = initial_condition
+ self.with_inactive_top_cells = with_inactive_top_cells
self.add_step(
InitialState(
test_case=self, mesh=mesh,
- initial_condition=initial_condition))
+ initial_condition=initial_condition,
+ with_inactive_top_cells=with_inactive_top_cells))
if mesh.with_ice_shelf_cavities:
self.add_step(RemapIceShelfMelt(test_case=self, mesh=mesh))
@@ -84,6 +98,15 @@ def configure(self, config=None):
config.set('global_ocean', 'init_description',
descriptions[initial_condition])
+ if self.with_inactive_top_cells:
+ # Since we start at minLevelCell = 2, we need to increase the
+ # number of vertical levels in the cfg file to end up with the
+ # intended number in the initial state
+ vert_levels = config.getint('vertical_grid', 'vert_levels')
+ config.set('vertical_grid', 'vert_levels', f'{vert_levels + 1}',
+ comment='active vertical levels + inactive_top_cells')
+ config.set('vertical_grid', 'inactive_top_cells', '1')
+
def validate(self):
"""
Test cases can override this method to perform validation of variables
@@ -93,6 +116,27 @@ def validate(self):
compare_variables(test_case=self, variables=variables,
filename1='initial_state/initial_state.nc')
+ if self.with_inactive_top_cells:
+ # construct the work directory for the other test
+ filename2 = os.path.join(self.base_work_dir, self.mpas_core.name,
+ self.test_group.name,
+ self.inactive_top_comp_subdir,
+ 'init/initial_state/initial_state.nc')
+ if os.path.exists(filename2):
+ variables = ['temperature', 'salinity', 'layerThickness',
+ 'normalVelocity']
+ compare_variables(
+ test_case=self, variables=variables,
+ filename1='initial_state/initial_state_crop.nc',
+ filename2=filename2,
+ quiet=False, check_outputs=False,
+ skip_if_step_not_run=False)
+
+ else:
+ self.logger.warn('The version of "init" without inactive top '
+ 'cells was not run. Skipping\n'
+ 'validation.')
+
if self.mesh.with_ice_shelf_cavities:
variables = ['ssh', 'landIcePressure']
compare_variables(test_case=self, variables=variables,
diff --git a/compass/ocean/tests/global_ocean/init/init.cfg b/compass/ocean/tests/global_ocean/init/init.cfg
new file mode 100644
index 0000000000..3688d8510d
--- /dev/null
+++ b/compass/ocean/tests/global_ocean/init/init.cfg
@@ -0,0 +1,5 @@
+# Options related to the vertical grid
+[vertical_grid]
+
+# no inactive top cells by default
+inactive_top_cells = 0
diff --git a/compass/ocean/tests/global_ocean/init/initial_state.py b/compass/ocean/tests/global_ocean/init/initial_state.py
index d9ea96eedf..a47b0d3e05 100644
--- a/compass/ocean/tests/global_ocean/init/initial_state.py
+++ b/compass/ocean/tests/global_ocean/init/initial_state.py
@@ -1,7 +1,11 @@
import os
from importlib.resources import contents
+import xarray
+from mpas_tools.io import write_netcdf
+
from compass.model import run_model
+from compass.ocean.inactive_top_cells import remove_inactive_top_cells_output
from compass.ocean.plot import plot_initial_state, plot_vertical_grid
from compass.ocean.tests.global_ocean.metadata import (
add_mesh_and_init_metadata,
@@ -23,7 +27,8 @@ class InitialState(Step):
initial_condition : {'WOA23', 'PHC', 'EN4_1900'}
The initial condition dataset to use
"""
- def __init__(self, test_case, mesh, initial_condition):
+ def __init__(self, test_case, mesh, initial_condition,
+ with_inactive_top_cells):
"""
Create the step
@@ -44,6 +49,7 @@ def __init__(self, test_case, mesh, initial_condition):
super().__init__(test_case=test_case, name='initial_state')
self.mesh = mesh
self.initial_condition = initial_condition
+ self.with_inactive_top_cells = with_inactive_top_cells
package = 'compass.ocean.tests.global_ocean.init'
@@ -134,6 +140,9 @@ def __init__(self, test_case, mesh, initial_condition):
'graph.info']:
self.add_output_file(filename=file)
+ if with_inactive_top_cells:
+ self.add_output_file(filename='initial_state_crop.nc')
+
def setup(self):
"""
Get resources at setup from config options
@@ -162,6 +171,7 @@ def run(self):
Run this step of the testcase
"""
config = self.config
+ logger = self.logger
interfaces = generate_1d_grid(config=config)
write_1d_grid(interfaces=interfaces, out_filename='vertical_grid.nc')
@@ -171,6 +181,39 @@ def run(self):
update_pio = config.getboolean('global_ocean', 'init_update_pio')
run_model(self, update_pio=update_pio)
+ if self.with_inactive_top_cells:
+
+ logger.info(" * Updating minLevelCell for inactive top cells")
+
+ in_filename = 'initial_state.nc'
+ out_filename = in_filename
+
+ with xarray.open_dataset(in_filename) as ds:
+ ds.load()
+
+ # keep the data set with Time for output
+ ds_out = ds
+
+ ds = ds.isel(Time=0)
+
+ offset = config.getint('vertical_grid',
+ 'inactive_top_cells')
+
+ if 'minLevelCell' in ds:
+ minLevelCell = ds.minLevelCell + offset
+ ds_out['minLevelCell'] = minLevelCell
+ else:
+ logger.info(" - Variable minLevelCell, needed for "
+ "inactive top cells, is missing from the "
+ "initial condition")
+
+ write_netcdf(ds_out, out_filename)
+
+ remove_inactive_top_cells_output(
+ in_filename=in_filename, out_filename='initial_state_crop.nc')
+
+ logger.info(" - Complete")
+
add_mesh_and_init_metadata(self.outputs, config,
init_filename='initial_state.nc')
diff --git a/compass/ocean/tests/global_ocean/init/streams.init b/compass/ocean/tests/global_ocean/init/streams.init
index c105b6041e..aff8c8bc29 100644
--- a/compass/ocean/tests/global_ocean/init/streams.init
+++ b/compass/ocean/tests/global_ocean/init/streams.init
@@ -23,6 +23,7 @@
+
diff --git a/compass/ocean/tests/global_ocean/mesh/qu/icos.cfg b/compass/ocean/tests/global_ocean/mesh/qu/icos.cfg
index a1a448fea0..36774a8afc 100644
--- a/compass/ocean/tests/global_ocean/mesh/qu/icos.cfg
+++ b/compass/ocean/tests/global_ocean/mesh/qu/icos.cfg
@@ -9,3 +9,9 @@ prefix = Icos
mesh_description = MPAS subdivided icosahedral mesh for E3SM version
${e3sm_version} at ${min_res}-km global resolution with
<<>> vertical level
+
+# Options related to the vertical grid
+[vertical_grid]
+
+# Number of inactive top cell layers
+inactive_top_cells = 0
diff --git a/compass/ocean/tests/global_ocean/mesh/qu/qu.cfg b/compass/ocean/tests/global_ocean/mesh/qu/qu.cfg
index eb4a478d49..7b20022a59 100644
--- a/compass/ocean/tests/global_ocean/mesh/qu/qu.cfg
+++ b/compass/ocean/tests/global_ocean/mesh/qu/qu.cfg
@@ -20,6 +20,8 @@ max_layer_thickness = 250.0
# the min and max occurs
transition_levels = 28
+# Number of inactive top cell layers
+inactive_top_cells = 0
# options for global ocean testcases
[global_ocean]
diff --git a/compass/ocean/tests/global_ocean/mesh/qu240/qu240.cfg b/compass/ocean/tests/global_ocean/mesh/qu240/qu240.cfg
index de9bba14f9..4176455b76 100644
--- a/compass/ocean/tests/global_ocean/mesh/qu240/qu240.cfg
+++ b/compass/ocean/tests/global_ocean/mesh/qu240/qu240.cfg
@@ -16,6 +16,9 @@ min_layer_thickness = 3.0
# The maximum layer thickness
max_layer_thickness = 500.0
+# Number of inactive top cell layers
+inactive_top_cells = 0
+
# options for spherical meshes
[spherical_mesh]
diff --git a/compass/ocean/tests/global_ocean/performance_test/__init__.py b/compass/ocean/tests/global_ocean/performance_test/__init__.py
index 0487f4f9f3..990fe397d7 100644
--- a/compass/ocean/tests/global_ocean/performance_test/__init__.py
+++ b/compass/ocean/tests/global_ocean/performance_test/__init__.py
@@ -1,3 +1,6 @@
+import os
+
+from compass.ocean.inactive_top_cells import remove_inactive_top_cells_output
from compass.ocean.tests.global_ocean.forward import (
ForwardStep,
ForwardTestCase,
@@ -31,9 +34,18 @@ def __init__(self, test_group, mesh, init, time_integrator):
time_integrator : {'split_explicit', 'RK4'}
The time integrator to use for the forward run
"""
+ name = 'performance_test'
+ if init.with_inactive_top_cells:
+ if time_integrator == 'RK4':
+ self.inactive_top_comp_subdir = os.path.join(
+ mesh.mesh_name, init.initial_condition, time_integrator,
+ name)
+ else:
+ self.inactive_top_comp_subdir = os.path.join(
+ mesh.mesh_name, init.initial_condition, name)
+ name = f'{name}_inactive_top'
super().__init__(test_group=test_group, mesh=mesh, init=init,
- time_integrator=time_integrator,
- name='performance_test')
+ time_integrator=time_integrator, name=name)
if mesh.with_ice_shelf_cavities:
this_module = self.__module__
@@ -71,6 +83,24 @@ def validate(self):
compare_variables(test_case=self, variables=variables,
filename1=f'{step_subdir}/output.nc')
+ if self.init.with_inactive_top_cells:
+ filename2 = os.path.join(self.base_work_dir,
+ self.mpas_core.name,
+ self.test_group.name,
+ self.inactive_top_comp_subdir,
+ 'forward/output.nc')
+ if os.path.exists(filename2):
+ compare_variables(
+ test_case=self, variables=variables,
+ filename1=f'{step_subdir}/output_crop.nc',
+ filename2=filename2,
+ quiet=False, check_outputs=False,
+ skip_if_step_not_run=False)
+ else:
+ self.logger.warn('The version of "performance_test" '
+ 'without inactive top cells was not run. '
+ 'Skipping validation.')
+
if self.mesh.with_ice_shelf_cavities:
variables = [
'ssh', 'landIcePressure', 'landIceDraft',
diff --git a/compass/ocean/vertical/grid_1d/__init__.py b/compass/ocean/vertical/grid_1d/__init__.py
index fab57b5f23..5731da0c90 100644
--- a/compass/ocean/vertical/grid_1d/__init__.py
+++ b/compass/ocean/vertical/grid_1d/__init__.py
@@ -28,16 +28,21 @@ def generate_1d_grid(config):
A 1D array of positive depths for layer interfaces in meters
"""
section = config['vertical_grid']
+ offset = 0
+ if config.has_option('vertical_grid', 'inactive_top_cells'):
+ offset = section.getint('inactive_top_cells')
+ print(f'offset = {offset}')
+
grid_type = section.get('grid_type')
if grid_type == 'uniform':
vert_levels = section.getint('vert_levels')
- interfaces = _generate_uniform(vert_levels)
+ interfaces = _generate_uniform(vert_levels - offset)
elif grid_type == 'tanh_dz':
vert_levels = section.getint('vert_levels')
min_layer_thickness = section.getfloat('min_layer_thickness')
max_layer_thickness = section.getfloat('max_layer_thickness')
bottom_depth = section.getfloat('bottom_depth')
- interfaces = create_tanh_dz_grid(vert_levels,
+ interfaces = create_tanh_dz_grid(vert_levels - offset,
bottom_depth,
min_layer_thickness,
max_layer_thickness)
@@ -66,6 +71,9 @@ def generate_1d_grid(config):
# renormalize to the requested range
interfaces = (bottom_depth / interfaces[-1]) * interfaces
+ if config.has_option('vertical_grid', 'inactive_top_cells'):
+ interfaces = np.append(np.zeros((offset)), interfaces)
+
return interfaces
diff --git a/docs/developers_guide/ocean/test_groups/global_ocean.rst b/docs/developers_guide/ocean/test_groups/global_ocean.rst
index 91e28e8af4..5cc2646880 100644
--- a/docs/developers_guide/ocean/test_groups/global_ocean.rst
+++ b/docs/developers_guide/ocean/test_groups/global_ocean.rst
@@ -1013,6 +1013,11 @@ If a baseline is provided, prognostic variables and ice-shelf melt fluxes (if
ice-shelf cavities are included in the mesh) are compared with a baseline, and
the ``time integration`` timer is compared with that of the baseline.
+This test case can also serve as a verification of the inactive top cell
+implementation. If ``inactive_top_cells`` is given in the config file with
+values greater than zero, the vertical domain is shifted downward by the given
+number of layers.
+
.. _dev_ocean_global_ocean_restart_test:
restart_test test case