Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor land-ice mesh generation test cases #590

Merged
merged 19 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/build_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
python-version: "3.10"
- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
id: file_changes
uses: trilom/file-changes-action@v1.2.3
uses: trilom/file-changes-action@v1.2.4
with:
output: ' '
- if: ${{ steps.skip_check.outputs.should_skip != 'true' }}
Expand Down
305 changes: 293 additions & 12 deletions compass/landice/mesh.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import time

import jigsawpy
import mpas_tools.io
import numpy as np
import xarray
from geometric_features import FeatureCollection, GeometricFeatures
from mpas_tools.io import write_netcdf
from mpas_tools.logging import check_call
from mpas_tools.mesh.conversion import convert, cull
from mpas_tools.mesh.creation import build_planar_mesh
from netCDF4 import Dataset


def gridded_flood_fill(field, iStart=None, jStart=None):
Expand Down Expand Up @@ -122,8 +130,9 @@ def set_cell_width(self, section, thk, bed=None, vx=None, vy=None,
:py:func:`mpas_tools.mesh.creation.build_mesh.build_planar_mesh()`.
Requires the following options to be set in the given config section:
``min_spac``, ``max_spac``, ``high_log_speed``, ``low_log_speed``,
``high_dist``, ``low_dist``,``cull_distance``, ``use_speed``,
``use_dist_to_edge``, and ``use_dist_to_grounding_line``.
``high_dist``, ``low_dist``, ``high_dist_bed``, ``low_dist_bed``,
``high_bed``, ``low_bed``, ``cull_distance``, ``use_speed``,
``use_dist_to_edge``, ``use_dist_to_grounding_line``, and ``use_bed``.

Parameters
----------
Expand Down Expand Up @@ -167,16 +176,16 @@ def set_cell_width(self, section, thk, bed=None, vx=None, vy=None,
section = self.config[section]

# Get config inputs for cell spacing functions
min_spac = float(section.get('min_spac'))
max_spac = float(section.get('max_spac'))
high_log_speed = float(section.get('high_log_speed'))
low_log_speed = float(section.get('low_log_speed'))
high_dist = float(section.get('high_dist'))
low_dist = float(section.get('low_dist'))
high_dist_bed = float(section.get('high_dist_bed'))
low_dist_bed = float(section.get('low_dist_bed'))
low_bed = float(section.get('low_bed'))
high_bed = float(section.get('high_bed'))
min_spac = section.getfloat('min_spac')
max_spac = section.getfloat('max_spac')
high_log_speed = section.getfloat('high_log_speed')
low_log_speed = section.getfloat('low_log_speed')
high_dist = section.getfloat('high_dist')
low_dist = section.getfloat('low_dist')
high_dist_bed = section.getfloat('high_dist_bed')
low_dist_bed = section.getfloat('low_dist_bed')
low_bed = section.getfloat('low_bed')
high_bed = section.getfloat('high_bed')

# convert km to m
cull_distance = float(section.get('cull_distance')) * 1.e3
Expand Down Expand Up @@ -411,3 +420,275 @@ def get_dist_to_edge_and_GL(self, thk, topg, x, y, section, window_size=None):
'seconds'.format(toc - tic))

return dist_to_edge, dist_to_grounding_line


def build_cell_width(self, section_name, gridded_dataset,
flood_fill_start=[None, None]):
"""
Determine MPAS mesh cell size based on user-defined density function.

Parameters
----------
section : str
section of config file used to define mesh parameters
xylar marked this conversation as resolved.
Show resolved Hide resolved
gridded_dataset : str
name of .nc file used to define cell spacing
flood_fill_start : list of ints
i and j indices used to define starting location for flood fill.
Most cases will use [None, None], which will just start the flood
fill in the center of the gridded dataset.
xylar marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
cell_width : numpy.ndarray
Desired width of MPAS cells based on mesh desnity functions to pass to
:py:func:`mpas_tools.mesh.creation.build_mesh.build_planar_mesh()`.
x1 : float
x coordinates from gridded dataset
y1 : float
y coordinates from gridded dataset
geom_points : jigsawpy.jigsaw_msh_t.VERT2_t
xy node coordinates to pass to build_planar_mesh()
geom_edges : jigsawpy.jigsaw_msh_t.EDGE2_t
xy edge coordinates between nodes to pass to build_planar_mesh()
flood_mask : numpy.ndarray
mask calculated by the flood fill routine,
where cells connected to the ice sheet (or main feature)
are 1 and everything else is 0.
xylar marked this conversation as resolved.
Show resolved Hide resolved
"""

section = self.config[section_name]
# get needed fields from gridded dataset
f = Dataset(gridded_dataset, 'r')
f.set_auto_mask(False) # disable masked arrays

x1 = f.variables['x1'][:]
y1 = f.variables['y1'][:]
thk = f.variables['thk'][0, :, :]
topg = f.variables['topg'][0, :, :]
vx = f.variables['vx'][0, :, :]
vy = f.variables['vy'][0, :, :]

f.close()

# Define extent of region to mesh.
xx0 = section.get('x_min')
xx1 = section.get('x_max')
yy0 = section.get('y_min')
yy1 = section.get('y_max')

if 'None' in [xx0, xx1, yy0, yy1]:
xx0 = np.min(x1)
xx1 = np.max(x1)
yy0 = np.min(y1)
yy1 = np.max(y1)
else:
xx0 = float(xx0)
xx1 = float(xx1)
yy0 = float(yy0)
yy1 = float(yy1)
xylar marked this conversation as resolved.
Show resolved Hide resolved

geom_points, geom_edges = set_rectangular_geom_points_and_edges(
xx0, xx1, yy0, yy1)

# Remove ice not connected to the ice sheet.
flood_mask = gridded_flood_fill(thk)
thk[flood_mask == 0] = 0.0
vx[flood_mask == 0] = 0.0
vy[flood_mask == 0] = 0.0
trhille marked this conversation as resolved.
Show resolved Hide resolved

# Calculate distance from each grid point to ice edge
# and grounding line, for use in cell spacing functions.
distToEdge, distToGL = get_dist_to_edge_and_GL(
self, thk, topg, x1,
y1, section=section_name)

# Set cell widths based on mesh parameters set in config file
cell_width = set_cell_width(self, section=section_name,
thk=thk, bed=topg, vx=vx, vy=vy,
dist_to_edge=distToEdge,
dist_to_grounding_line=distToGL,
flood_fill_iStart=flood_fill_start[0],
flood_fill_jStart=flood_fill_start[1])

return (cell_width.astype('float64'), x1.astype('float64'),
y1.astype('float64'), geom_points, geom_edges, flood_mask)


def build_MALI_mesh(self, cell_width, x1, y1, geom_points,
xylar marked this conversation as resolved.
Show resolved Hide resolved
geom_edges, mesh_name, section_name,
gridded_dataset, projection, geojson_file=None):
"""
Create the MALI mesh based on final cell widths determined by
:py:func:`compass.landice.mesh.build_cell_width()`, using Jigsaw and
MPAS-Tools functions. Culls the mesh based on config options, interpolates
all available fields from the gridded dataset to the MALI mesh using the
bilinear method, and marks domain boundaries as Dirichlet cells.

Parameters
----------
cell_width : numpy.ndarray
Desired width of MPAS cells calculated by :py:func:`build_cell_width()`
based on mesh density functions define in :py:func:`set_cell_width()`
to pass to
:py:func:`mpas_tools.mesh.creation.build_mesh.build_planar_mesh()`.
x1 : float
x coordinates from gridded dataset
y1 : float
y coordinates from gridded dataset
geom_points : jigsawpy.jigsaw_msh_t.VERT2_t
xy node coordinates to pass to build_planar_mesh()
geom_edges : jigsawpy.jigsaw_msh_t.EDGE2_t
xy edge coordinates between nodes to pass to build_planar_mesh()
mesh_name : str
Filename to be used for final MALI .nc mesh file.
section_name : str
Name of config section containing mesh creation options.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, clarify somewhere what config options are expected in this section.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in ae88c40 and 95af2ff

gridded_dataset : str
Name of gridded dataset file to be used for interpolation to MALI mesh
projection : str
Projection to be used for setting lat-long fields.
Likely 'gis-gimp' or 'ais-bedmap2'
geojson_file : str
Name of geojson file that defines regional domain extent.
"""

logger = self.logger
section = self.config[section_name]

logger.info('calling build_planar_mesh')
build_planar_mesh(cell_width, x1, y1, geom_points,
geom_edges, logger=logger)
dsMesh = xarray.open_dataset('base_mesh.nc')
logger.info('culling mesh')
dsMesh = cull(dsMesh, logger=logger)
logger.info('converting to MPAS mesh')
dsMesh = convert(dsMesh, logger=logger)
logger.info('writing grid_converted.nc')
write_netcdf(dsMesh, 'grid_converted.nc')
levels = section.get('levels')
logger.info('calling create_landice_grid_from_generic_MPAS_grid.py')
args = ['create_landice_grid_from_generic_MPAS_grid.py',
'-i', 'grid_converted.nc',
'-o', 'grid_preCull.nc',
'-l', levels, '-v', 'glimmer']
check_call(args, logger=logger)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in similar places, I think it would help debugging more if you give the full command, e.g:

Suggested change
logger.info('calling create_landice_grid_from_generic_MPAS_grid.py')
args = ['create_landice_grid_from_generic_MPAS_grid.py',
'-i', 'grid_converted.nc',
'-o', 'grid_preCull.nc',
'-l', levels, '-v', 'glimmer']
check_call(args, logger=logger)
args = ['create_landice_grid_from_generic_MPAS_grid.py',
'-i', 'grid_converted.nc',
'-o', 'grid_preCull.nc',
'-l', levels, '-v', 'glimmer']
logger.info(f'Running: {" ".join(args}')
check_call(args, logger=logger)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like check_call already takes care of that, but I've removed the redundant logger.info() calls in 342856f.


logger.info('calling interpolate_to_mpasli_grid.py')
args = ['interpolate_to_mpasli_grid.py', '-s',
gridded_dataset, '-d',
'grid_preCull.nc', '-m', 'b', '-t']

check_call(args, logger=logger)

cullDistance = section.get('cull_distance')
if float(cullDistance) > 0.:
logger.info('calling define_cullMask.py')
args = ['define_cullMask.py', '-f',
'grid_preCull.nc', '-m',
'distance', '-d', cullDistance]

check_call(args, logger=logger)
else:
logger.info('cullDistance <= 0 in config file. '
'Will not cull by distance to margin. \n')

if geojson_file is not None:
# This step is only necessary because the GeoJSON region
# is defined by lat-lon.
logger.info('calling set_lat_lon_fields_in_planar_grid.py')
args = ['set_lat_lon_fields_in_planar_grid.py', '-f',
'grid_preCull.nc', '-p', projection]

check_call(args, logger=logger)

logger.info('calling MpasMaskCreator.x')
args = ['MpasMaskCreator.x', 'grid_preCull.nc',
'mask.nc', '-f', geojson_file]

check_call(args, logger=logger)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, the python-based mask creator I wrote is typically faster for large meshes because it can use threading:
http://mpas-dev.github.io/MPAS-Tools/stable/mesh_conversion.html#mask-creation-with-python-multiprocessing

There are examples of using it in the global_ocean test group and in MPAS-Analysis.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you're using it in make_region_masks() below. I'd use it here, too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, great! That's probably outside the scope of this PR, but I'll make a note to do that in the future.


logger.info('culling to geojson file')

dsMesh = xarray.open_dataset('grid_preCull.nc')
if geojson_file is not None:
mask = xarray.open_dataset('mask.nc')
else:
mask = None

dsMesh = cull(dsMesh, dsInverse=mask, logger=logger)
write_netcdf(dsMesh, 'culled.nc')

logger.info('Marking horns for culling')
args = ['mark_horns_for_culling.py', '-f', 'culled.nc']
check_call(args, logger=logger)

logger.info('culling and converting')
dsMesh = xarray.open_dataset('culled.nc')
dsMesh = cull(dsMesh, logger=logger)
dsMesh = convert(dsMesh, logger=logger)
write_netcdf(dsMesh, 'dehorned.nc')

logger.info('calling create_landice_grid_from_generic_MPAS_grid.py')
args = ['create_landice_grid_from_generic_MPAS_grid.py', '-i',
'dehorned.nc', '-o',
mesh_name, '-l', levels, '-v', 'glimmer',
'--beta', '--thermal', '--obs', '--diri']

check_call(args, logger=logger)

logger.info('calling interpolate_to_mpasli_grid.py')
args = ['interpolate_to_mpasli_grid.py', '-s',
gridded_dataset, '-d', mesh_name, '-m', 'b']
check_call(args, logger=logger)

logger.info('Marking domain boundaries dirichlet')
args = ['mark_domain_boundaries_dirichlet.py',
'-f', mesh_name]
check_call(args, logger=logger)

logger.info('calling set_lat_lon_fields_in_planar_grid.py')
args = ['set_lat_lon_fields_in_planar_grid.py', '-f',
mesh_name, '-p', projection]
check_call(args, logger=logger)


def make_region_masks(self, mesh_filename, mask_filename, cores, tags):
"""
Create masks for ice-sheet subregions based on data
in MPAS-Dev/geometric_fatures.

Parameters
----------
mesh_filename : str
name of .nc mesh file for which to create region masks
mask_filename : str
name of .nc file to contain region masks
cores : int
number of processors used to create region masks
tags : list of str
Groups of regions for which masks are to be defined
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mesh_filename : str
name of .nc mesh file for which to create region masks
mask_filename : str
name of .nc file to contain region masks
cores : int
number of processors used to create region masks
tags : list of str
Groups of regions for which masks are to be defined
mesh_filename : str
name of NetCDF mesh file for which to create region masks
mask_filename : str
name of NetCDF file to contain region masks
cores : int
number of processors used to create region masks
tags : list of str
Groups of regions for which masks are to be defined

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in ae88c40

"""

logger = self.logger
logger.info('creating region masks')
gf = GeometricFeatures()
fcMask = FeatureCollection()

for tag in tags:
fc = gf.read(componentName='landice', objectType='region',
tags=[tag])
fcMask.merge(fc)

geojson_filename = 'regionMask.geojson'
fcMask.to_geojson(geojson_filename)

args = ['compute_mpas_region_masks',
'-m', mesh_filename,
'-g', geojson_filename,
'-o', mask_filename,
'-t', 'cell',
'--process_count', f'{cores}',
'--format', mpas_tools.io.default_format,
'--engine', mpas_tools.io.default_engine]
check_call(args, logger=logger)
Loading