# Mesh Creation¶

## Uniform, Planar Meshes¶

The most basic tool for creating an MPAS mesh is the function
`mpas_tools.planar_hex.make_planar_hex_mesh()`

or the associated
command-line tool `planar_hex`

. These tools create a uniform, planar mesh
of hexagons that may optionally be periodic in either or both of the x and
y directions. A typical call to `make_planar_hex_mesh()`

might look like
the following:

```
from mpas_tools.planar_hex import make_planar_hex_mesh
dsMesh = make_planar_hex_mesh(nx=10, ny=20, dc=1000., nonperiodic_x=False,
nonperiodic_y=False)
```

This creates a periodic mesh in both x and y that is 10 grid cells by 20 grid
cells in size with 1-km resolution. The mesh is not save to a file in this
example but is instead returned as an `xarray.Dataset`

object.

The command-line approach for generating the same mesh would be:

```
$ planar_hex --nx=10 --ny=20 --dc=1000. \
--outFileName=periodic_mesh_10x20_1km.nc
```

In this case, the resulting mesh is written to a file.

The default is to make a periodic mesh (awkwardly, it’s “not non-periodic”). If you want a mesh that is not periodic in one or both directions, you first need to create a mesh and then cull the cells along the periodic boundary. It doesn’t hurt to run the mesh converter as well, just to be sure the mesh conforms correctly to the MPAS mesh specification:

```
from mpas_tools.planar_hex import make_planar_hex_mesh
from mpas_tools.mesh.conversion import cull, convert
dsMesh = make_planar_hex_mesh(nx=10, ny=20, dc=1000., nonperiodic_x=True,
nonperiodic_y=True)
dsMesh = cull(dsMesh)
dsMesh = convert(dsMesh)
```

On the command line, this looks like:

```
$ planar_hex --nx=10 --ny=20 --dc=1000. --npx --npy \
--outFileName=mesh_to_cull.nc
$ MpasCellCuller.x mesh_to_cull.nc culled_mesh.nc
$ MpasMeshConverter.x culled_mesh.nc nonperiodic_mesh_10x20_1km.nc
```

## Building a JIGSAW Mesh¶

The `mpas_tools.mesh.creation.build_mesh`

module is used
create an MPAS mesh using the JIGSAW
and JIGSAW-Python (jigsawpy)
packages.

### Spherical Meshes¶

Spherical meshes are constructed with the function
`mpas_tools.mesh.creation.build_mesh.build_spherical_mesh()`

.
The user provides a 2D array `cellWidth`

of cell sizes in kilometers along
1D arrays for the longitude and latitude (the cell widths must be on a lon/lat
tensor grid) and the radius of the earth in meters.

The result is an MPAS mesh file, called `base_mesh.nc`

by default, as well as
several intermediate files: `mesh.log`

, `mesh-HFUN.msh`

, `mesh.jig`

,
`mesh-MESH.msh`

, `mesh.msh`

, and `mesh_triangles.nc`

.

Here is a simple example script for creating a uniform MPAS mesh with 240-km resolution:

```
#!/usr/bin/env python
import numpy as np
from mpas_tools.ocean import build_spherical_mesh
def cellWidthVsLatLon():
"""
Create cell width array for this mesh on a regular latitude-longitude grid.
Returns
-------
cellWidth : ndarray
m x n array of cell width in km
lon : ndarray
longitude in degrees (length n and between -180 and 180)
lat : ndarray
longitude in degrees (length m and between -90 and 90)
"""
dlat = 10
dlon = 10
constantCellWidth = 240
nlat = int(180/dlat) + 1
nlon = int(360/dlon) + 1
lat = np.linspace(-90., 90., nlat)
lon = np.linspace(-180., 180., nlon)
cellWidth = constantCellWidth * np.ones((lat.size, lon.size))
return cellWidth, lon, lat
def main():
cellWidth, lon, lat = cellWidthVsLatLon()
build_spherical_mesh(cellWidth, lon, lat, out_filename='base_mesh.nc')
if __name__ == '__main__':
main()
```

We define the resolution on a coarse (10 degree by 10 degree) grid because it is uniform. Meshes with more complex variation may require higher resolution grids to cell widths.

### Planar Meshes¶

Planar meshes can be constructed with the function
`mpas_tools.mesh.creation.build_mesh.build_planar_mesh()`

. Provide
this function with a 2D array `cellWidth`

of cell sizes in kilometers and
1D arrays for x and y (the cell widths must be on a 2D tensor grid). Planar
meshes also require `geom_points`

, a list of point coordinates for bounding
polygon for the planar mesh, and `geom_edges`

, a list of edges between points
in `geom_points`

that define the bounding polygon.

As for spehrical meshes, the result is an MPAS mesh file, called
`base_mesh.nc`

by default, as well as several intermediate files:
`mesh.log`

, `mesh-HFUN.msh`

, `mesh.jig`

, `mesh-MESH.msh`

, `mesh.msh`

,
and `mesh_triangles.nc`

.

### JIGSAW Driver¶

Underlying both spherical and planar mesh creation is the JIGSAW driver
function `mpas_tools.mesh.creation.jigsaw_driver.jigsaw_driver()`

. This
function is used to setup data structures and then build a JIGSAW mesh using
`jigsawpy`

.

## Mesh Definition Tools¶

The `mpas_tools.mesh.creation.mesh_definition_tools`

module includes
several tools for defining the `cellWidth`

variable.

### Merging Cell Widths¶

The function
`mpas_tools.mesh.creation.mesh_definition_tools.mergeCellWidthVsLat()`

is used to combine two cell-width distributions that are functions of latitude
only and which asymptote to different constant values north and south of a given
transition latitude with a `tanh`

function of a given characteristic width.

For example, the following code snippet will produce cell widths as a function of latitude of about 30 km south of the Arctic Circle and 10 km north of that latitude, transitioning over a characteristic “distance” of about 5 degrees.

```
import numpy
from mpas_tools.mesh.creation.mesh_definition_tools import \
mergeCellWidthVsLat
lat = numpy.linspace(-90., 90., 181)
cellWidthInSouth = 30.
cellWidthInNorth = 10.
latTransition = 66.5
latWidthTransition = 5.
cellWidths = mergeCellWidthVsLat(lat, cellWidthInSouth, cellWidthInNorth,
latTransition, latWidthTransition)
```

### Defining an Eddy-closure Mesh¶

One of the commonly used flavor of MPAS-Ocean and MPAS-Seaice meshes is designed
with relatively coarse resolution in mind (requiring parameterization of ocean
eddies with an “eddy closure”). This flavor of mesh has resolution that is
purely a function of latitude, with 5 regions of relatively uniform resolution
(north polar, northern mid-latitudes, equatorial, southern mid-latitudes and
south polar) with smooth (`tanh`

) transitions between these resolutions.

The default EC mesh has resolutions of 35 km at the poles, 60 km at
mid-latitudes and 30 km at the equator. Transitions between equatorial and
mid-latitude regions are at 15 degrees N/S latitude and transitions between
mid-latitude and polar regions are at 73 degrees N/S latitude. The
transition near the equator is somewhat more abrupt (~6 degrees) than near the
poles (~9 degrees). The switch between the transitional `tanh`

functions is
made at 40 degrees N/S latitude, where the resolution is nearly constant and no
appreciable discontinuity arises. The default EC mesh can be obtained with the
function
`mpas_tools.mesh.creation.mesh_definition_tools.EC_CellWidthVsLat()`

:

```
import numpy
from mpas_tools.mesh.creation.mesh_definition_tools import \
EC_CellWidthVsLat
lat = numpy.linspace(-90., 90., 181)
cellWidths = EC_CellWidthVsLat(lat)
```

### Defining a Rossby-radius Mesh¶

Another common flavor of MPAS-Ocean and MPAS-Seaice meshes is designed for higher resolutions, where the Rossby radius of deformation can be (at least partially) resolved. These meshes approximately scale their resolution in proportion to the Rossby radius.

A typical Rossby Radius Scaling (RRS) mesh has a resolution at the poles that is
three times finer than the resolution at the equator. For example, the RRS mesh
used in E3SMv1 high resolution simulations would be defined, using the function
`mpas_tools.mesh.creation.mesh_definition_tools.RRS_CellWidthVsLat()`

by:

```
import numpy
from mpas_tools.mesh.creation.mesh_definition_tools import \
RRS_CellWidthVsLat
lat = numpy.linspace(-90., 90., 181)
cellWidths = RRS_CellWidthVsLat(lat, cellWidthEq=18., cellWidthPole=6.)
```

### Defining an Atlantic/Pacific Mesh¶

The function
`mpas_tools.mesh.creation.mesh_definition_tools.AtlanticPacificGrid()`

can be used to define a mesh that has two different, constant resolutions in the
Atlantic and Pacific Oceans.

## Signed Distance Functions¶

The `mpas_tools.mesh.creation.signed_distance`

module includes several
functions for creating `cellWidth`

variables based on the signed distance from
a boundary curve on the sphere. A signed distance function is positive outside
the bounding shape and negative inside, with a value proportional to the
distance to the nearest point on the curve (so the function is equal to zero on
the curve). Signed distance functions provide a useful way ot define
transitions in resolution based on complex shapes that can be defined using
geojson files. These files can be created by hand,
e.g. at geojson.io or in python using libraries like
shapely.

Calls to the functions in this module require a
FeatureCollection
object from the
geometric_features
package. The `FeatureColleciton`

must define one or more regions on the
sphere from which the distance, mask, or signed distance will be computed.
The `FeatureColleciton`

could come from the predefined features included in
`geometric_features`

, could be read in from a `geojson`

file (see
Reading in Features),
or could be created as part of a python script with `shapely`

or other tools.

In this example, we first define a base resolution using the default EC mesh
(see Defining an Eddy-closure Mesh) and then use
`mpas_tools.mesh.creation.signed_distance.signed_distance_from_geojson()`

to create a signed distance function from a `FeatureCollection`

read in from
this geojson file.
The signed distance function is used to define a region of high resolution (12
km) around Antarctica.

```
import numpy as np
import mpas_tools.mesh.creation.mesh_definition_tools as mdt
from mpas_tools.mesh.creation.signed_distance import \
signed_distance_from_geojson
from geometric_features import read_feature_collection
from mpas_tools.cime.constants import constants
dlon = 0.1
dlat = dlon
earth_radius = constants['SHR_CONST_REARTH']
nlon = int(360./dlon) + 1
nlat = int(180./dlat) + 1
lon = np.linspace(-180., 180., nlon)
lat = np.linspace(-90., 90., nlat)
cellWidth = mdt.EC_CellWidthVsLat(lat)
# broadcast cellWidth to 2D
_, cellWidth = np.meshgrid(lon, cellWidthVsLat)
# now, add the high-res region
fc = read_feature_collection('high_res_region.geojson')
so_signed_distance = signed_distance_from_geojson(fc, lon, lat,
earth_radius,
max_length=0.25)
# Equivalent to 20 degrees latitude
trans_width = 1600e3
trans_start = -500e3
dx_min = 12.
weights = 0.5 * (1 + np.tanh((so_signed_distance - trans_start) /
trans_width))
cellWidth = dx_min * (1 - weights) + cellWidth * weights
```

Sometimes it can be useful to extract just the mask of the region of interest
(defined as `0`

outside the the region and `1`

inside it) or the unsigned
distance. For these purposes, use the functions
`mpas_tools.mesh.creation.signed_distance.mask_from_geojson()`

and
`mpas_tools.mesh.creation.signed_distance.distance_from_geojson()`

,
respectively.

The grid does not necessarily have to be uniform in resolution. Here is a
relatively complicated example where the highest resolution (1/100 degree) of
the lon/lat grid is concentrated within +/- 10 degrees of 0 degrees longitude
and 0 degrees latitude and the resolution is ~2 degrees elsewhere. The shape
`test.geojson`

is given below, and is in the high-res region:

```
import numpy
import matplotlib.pyplot as plt
from geometric_features import read_feature_collection
from mpas_tools.cime.constants import constants
from mpas_tools.mesh.creation.signed_distance import \
distance_from_geojson, mask_from_geojson, signed_distance_from_geojson
def gaussian(dcoarse, dfine, cmin, cmax, center, width, iter_count=10):
def get_res(xval):
res = dcoarse + \
(dfine - dcoarse) * numpy.exp(-(xval - center) ** 2 /
(2. * width**2))
return res
x = [cmin]
while x[-1] < cmax:
d = get_res(x[-1])
for index in range(iter_count):
x_mid = x[-1] + 0.5*d
d = get_res(x_mid)
x.append(x[-1] + d)
x = numpy.array(x)
factor = (cmax - cmin)/(x[-1] - x[0])
x = cmin + factor*(x - x[0])
return x
def main():
dcoarse = 2.
dfine = 0.01
width = 10.
lon = gaussian(dcoarse=dcoarse, dfine=dfine, cmin=-180., cmax=180.,
center=0., width=width)
lat = gaussian(dcoarse=dcoarse, dfine=dfine, cmin=-90., cmax=90.,
center=0., width=width)
earth_radius = constants['SHR_CONST_REARTH']
fc = read_feature_collection('test.geojson')
distance = distance_from_geojson(fc, lon, lat, earth_radius,
max_length=dfine)
mask = mask_from_geojson(fc, lon, lat, max_length=dfine)
signed_distance = signed_distance_from_geojson(fc, lon, lat, earth_radius,
max_length=dfine)
```

Here is `test.geojson`

:

```
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "weird shape",
"component": "ocean",
"object": "region",
"author": "Xylar Asay-Davis"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
3.8232421875000173,
3.283113991917228
],
[
1.5600585937500142,
3.8752158957336085
],
[
0.9448242187500171,
1.8453839885731744
],
[
-0.3186035156249841,
0.8239462091017558
],
[
0.6372070312500188,
-0.37353251022881745
],
[
2.3181152343750147,
0.03295898255728466
],
[
3.636474609375017,
-0.3405741662837591
],
[
4.163818359375015,
1.5159363834516735
],
[
2.9443359375000164,
1.9771465537125645
],
[
3.8232421875000173,
3.283113991917228
]
]
]
}
}
]
}
```