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

Add NetCDF C version of mesh conversion tools #514

Merged
merged 14 commits into from
Aug 7, 2023
Merged
45 changes: 24 additions & 21 deletions conda_package/mpas_tools/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import subprocess


def check_call(args, logger, log_command=True, timeout=None, **kwargs):
def check_call(args, logger=None, log_command=True, timeout=None, **kwargs):
"""
Call the given subprocess with logging to the given logger.

Expand All @@ -13,7 +13,7 @@ def check_call(args, logger, log_command=True, timeout=None, **kwargs):
A list or string of argument to the subprocess. If ``args`` is a
string, you must pass ``shell=True`` as one of the ``kwargs``.

logger : logging.Logger
logger : logging.Logger, optional
The logger to write output to

log_command : bool, optional
Expand All @@ -36,25 +36,28 @@ def check_call(args, logger, log_command=True, timeout=None, **kwargs):
print_args = args
else:
print_args = ' '.join(args)
if log_command:
logger.info(f'Running: {print_args}')

process = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, **kwargs)
stdout, stderr = process.communicate(timeout=timeout)

if stdout:
stdout = stdout.decode('utf-8')
for line in stdout.split('\n'):
logger.info(line)
if stderr:
stderr = stderr.decode('utf-8')
for line in stderr.split('\n'):
logger.error(line)

if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode,
print_args)

# make a logger if there isn't already one
with LoggingContext(print_args, logger=logger) as logger:
if log_command:
logger.info(f'Running: {print_args}')

process = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, **kwargs)
stdout, stderr = process.communicate(timeout=timeout)

if stdout:
stdout = stdout.decode('utf-8')
for line in stdout.split('\n'):
logger.info(line)
if stderr:
stderr = stderr.decode('utf-8')
for line in stderr.split('\n'):
logger.error(line)

if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode,
print_args)


class LoggingContext(object):
Expand Down
80 changes: 29 additions & 51 deletions conda_package/mpas_tools/mesh/conversion.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import os
import xarray
import subprocess
from tempfile import TemporaryDirectory
import shutil

import mpas_tools.io
from mpas_tools.io import write_netcdf
from mpas_tools.logging import check_call


def convert(dsIn, graphInfoFileName=None, logger=None, dir=None):
Expand Down Expand Up @@ -48,9 +49,9 @@ def convert(dsIn, graphInfoFileName=None, logger=None, dir=None):

outDir = os.path.dirname(outFileName)

_call_subprocess(['MpasMeshConverter.x', inFileName, outFileName],
check_call(['MpasMeshConverter.x', inFileName, outFileName],
logger)

dsOut = xarray.open_dataset(outFileName)
dsOut.load()

Expand Down Expand Up @@ -144,8 +145,8 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None,

outDir = os.path.dirname(outFileName)

_call_subprocess(args, logger)
check_call(args=args, logger=logger)

dsOut = xarray.open_dataset(outFileName)
dsOut.load()

Expand All @@ -156,10 +157,10 @@ def cull(dsIn, dsMask=None, dsInverse=None, dsPreserve=None,
return dsOut


def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None):
def mask(dsMesh, fcMask=None, logger=None, dir=None, cores=1):
"""
Use ``MpasMaskCreator.x`` to create a set of region masks either from
mask feature collections or from seed points to be used to flood fill
Use ``compute_mpas_region_masks`` to create a set of region masks either
from mask feature collections

Parameters
----------
Expand All @@ -169,17 +170,16 @@ def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None):
fcMask : geometric_features.FeatureCollection, optional
A feature collection containing features to use to create the mask

fcSeed : geometric_features.FeatureCollection, optional
A feature collection with points to use a seeds for a flood fill that
will create a mask of all cells connected to the seed points

logger : logging.Logger, optional
A logger for the output if not stdout

dir : str, optional
A directory in which a temporary directory will be added with files
produced during mask creation and then deleted upon completion.

cores : int, optional
The number of cores to use for python multiprocessing

Returns
-------
dsMask : xarray.Dataset
Expand All @@ -189,48 +189,26 @@ def mask(dsMesh, fcMask=None, fcSeed=None, logger=None, dir=None):
if dir is not None:
dir = os.path.abspath(dir)
with TemporaryDirectory(dir=dir) as tempdir:
inFileName = '{}/mesh_in.nc'.format(tempdir)
inFileName = f'{tempdir}/mesh_in.nc'
write_netcdf(dsMesh, inFileName)
outFileName = '{}/mesh_out.nc'.format(tempdir)

args = ['MpasMaskCreator.x', inFileName, outFileName]

if fcMask is not None:
fileName = '{}/mask.geojson'.format(tempdir)
fcMask.to_geojson(fileName)
args.extend(['-f', fileName])

if fcSeed is not None:
fileName = '{}/seed.geojson'.format(tempdir)
fcSeed.to_geojson(fileName)
args.extend(['-s', fileName])

_call_subprocess(args, logger)
outFileName = f'{tempdir}/mask_out.nc'

geojsonFileName = f'{tempdir}/mask.geojson'
fcMask.to_geojson(geojsonFileName)
args = ['compute_mpas_region_masks',
'-m', inFileName,
'-o', outFileName,
'-g', geojsonFileName,
'-t', 'cell',
'--process_count', f'{cores}',
'--format', mpas_tools.io.default_format,
]
if mpas_tools.io.default_engine is not None:
args.extend(['--engine', mpas_tools.io.default_engine])

check_call(args=args, logger=logger)

dsOut = xarray.open_dataset(outFileName)
dsOut.load()

return dsOut


def _call_subprocess(args, logger):
"""Call the given subprocess and send the output to the logger"""
if logger is None:
subprocess.check_call(args)
else:
process = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate()

if stdout:
stdout = stdout.decode('utf-8')
for line in stdout.split('\n'):
logger.info(line)
if stderr:
stderr = stderr.decode('utf-8')
for line in stderr.split('\n'):
logger.error(line)

if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode,
' '.join(args))
18 changes: 9 additions & 9 deletions conda_package/mpas_tools/mesh/creation/build_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import cartopy.crs as ccrs
import cartopy

from mpas_tools.mesh.conversion import convert
from mpas_tools.io import write_netcdf
from mpas_tools.logging import check_call

from mpas_tools.mesh.creation.jigsaw_driver import jigsaw_driver
from mpas_tools.mesh.creation.jigsaw_to_netcdf import jigsaw_to_netcdf
Expand Down Expand Up @@ -101,9 +100,10 @@ def build_spherical_mesh(cellWidth, lon, lat, earth_radius,
sphere_radius=earth_radius)

logger.info('Step 3. Convert from triangles to MPAS mesh')
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'), dir=dir,
logger=logger),
out_filename)
args = ['MpasMeshConverter.x',
'mesh_triangles.nc',
out_filename]
check_call(args=args, logger=logger)


def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges,
Expand Down Expand Up @@ -152,7 +152,7 @@ def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges,
output_name='mesh_triangles.nc', on_sphere=False)

logger.info('Step 3. Convert from triangles to MPAS mesh')
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'),
logger=logger),
out_filename)

args = ['MpasMeshConverter.x',
'mesh_triangles.nc',
out_filename]
check_call(args=args, logger=logger)
4 changes: 2 additions & 2 deletions conda_package/mpas_tools/mesh/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def compute_mpas_region_masks(dsMesh, fcMask, maskTypes=('cell', 'vertex'),
maskTypes : tuple of {'cell', 'edge', 'vertex'}, optional
Which type(s) of masks to make. Masks are created based on whether
the latitude and longitude associated with each of these locations
(e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cells'``) are
(e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cell'``) are
inside or outside of the regions in ``fcMask``.

logger : logging.Logger, optional
Expand Down Expand Up @@ -213,7 +213,7 @@ def compute_mpas_transect_masks(dsMesh, fcMask, earthRadius,
maskTypes : tuple of {'cell', 'edge', 'vertex'}, optional
Which type(s) of masks to make. Masks are created based on whether
the latitude and longitude associated with each of these locations
(e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cells'``) are
(e.g. ``dsMesh.latCell`` and ``dsMesh.lonCell`` for ``'cell'``) are
inside or outside of the transects in ``fcMask``.

logger : logging.Logger, optional
Expand Down
13 changes: 7 additions & 6 deletions conda_package/mpas_tools/viz/paraview_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
from progressbar import ProgressBar, Percentage, Bar, ETA
from mpas_tools.conversion import mask, cull
from mpas_tools.io import write_netcdf
from mpas_tools.logging import check_call


def extract_vtk(filename_pattern, variable_list='all', dimension_list=None,
Expand Down Expand Up @@ -2146,12 +2147,14 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names,
print('Making a region mask file')
ds_mask = mask(dsMesh=ds_mesh, fcMask=fc_region_mask, logger=logger,
dir=temp_dir)
write_netcdf(ds_mask, '{}/mask.nc'.format(temp_dir))
write_netcdf(ds_mask, f'{temp_dir}/mask.nc')
print('Cropping mesh to region')
out_mesh_filename = '{}/mesh.nc'.format(temp_dir)
ds_culled = cull(dsIn=ds_mesh, dsInverse=ds_mask, logger=logger,
dir=temp_dir)
write_netcdf(ds_culled, out_mesh_filename)
args = ['MpasCellCuller.x',
mesh_filename,
out_mesh_filename,
'-i', f'{temp_dir}/mask.nc']
check_call(args=args, logger=logger)
Comment on lines -2152 to +2157
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@matthewhoffman, I saw a question about this change (that maybe got deleted).

In general, I'm moving away from the wrapper function calls in any case where we want to read in and write out files anyway. The wrapper functions have to write out temporary files before calling the tools under the hood, so there's an unnecessary extra level of reading in and writing back out files with the wrapper functions if they already exist.


region_masks = dict()
cell_mask = ds_mask.regionCellMasks.sum(dim='nRegions') > 0
Expand Down Expand Up @@ -2201,5 +2204,3 @@ def _cull_files(fc_region_mask, temp_dir, mesh_filename, time_file_names,
handler.close()

return out_mesh_filename, out_time_file_names

# vim: set expandtab:
2 changes: 1 addition & 1 deletion conda_package/recipe/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cmake --build .
cmake --install .

# build and install mesh conversion tools
cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools
cd ${SRC_DIR}/conda_package/mesh_tools/mesh_conversion_tools_netcdf_c
mkdir build
cd build
cmake \
Expand Down
2 changes: 1 addition & 1 deletion conda_package/recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test:
- translate_planar_grid -f 'periodic_mesh_10x20_1km.nc' -d 'periodic_mesh_20x40_1km.nc'
- MpasMeshConverter.x mesh_tools/mesh_conversion_tools/test/mesh.QU.1920km.151026.nc mesh.nc
- MpasCellCuller.x mesh.nc culled_mesh.nc -m mesh_tools/mesh_conversion_tools/test/land_mask_final.nc
- MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson
# - MpasMaskCreator.x mesh.nc arctic_mask.nc -f mesh_tools/mesh_conversion_tools/test/Arctic_Ocean.geojson
Copy link
Member

Choose a reason for hiding this comment

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

Is this intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, as I said, this tool has been removed from the conda package.

- planar_hex --nx=30 --ny=20 --dc=1000. --npx --npy --outFileName='nonperiodic_mesh_30x20_1km.nc'
- MpasCellCuller.x nonperiodic_mesh_30x20_1km.nc culled_nonperiodic_mesh_30x20_1km.nc
- python -m pytest conda_package/mpas_tools/tests
Expand Down
15 changes: 15 additions & 0 deletions mesh_tools/mesh_conversion_tools_netcdf_c/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cmake_minimum_required (VERSION 3.0.2)
project (mesh_conversion_tools)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

add_executable (MpasMeshConverter.x mpas_mesh_converter.cpp)
target_link_libraries (MpasMeshConverter.x netcdf)

add_executable (MpasCellCuller.x mpas_cell_culler.cpp)
target_link_libraries (MpasCellCuller.x netcdf)

#add_executable (MpasMaskCreator.x mpas_mask_creator.cpp jsoncpp.cpp)
#target_link_libraries (MpasMaskCreator.x netcdf)

install (TARGETS MpasMeshConverter.x MpasCellCuller.x DESTINATION bin)
Loading