Skip to content

Commit 0fae9f3

Browse files
authored
Merge pull request #12 from andrewdnolan/improved_patch_creation
- Improve edge/vertex patch creation along culled mesh boundaries - Replaced numpy masked arrays, with ragged array to better integrate with cartopy - Improved and optimized support datasets comprised of dask arrays - Fixed bug in Polar Stereographic projections for spherical meshes
2 parents d16d63f + a34cf82 commit 0fae9f3

File tree

2 files changed

+124
-66
lines changed

2 files changed

+124
-66
lines changed

mosaic/descriptor.py

Lines changed: 111 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
connectivity_arrays = ["cellsOnEdge",
1414
"cellsOnVertex",
1515
"verticesOnEdge",
16-
"verticesOnCell"]
16+
"verticesOnCell",
17+
"edgesOnVertex"]
1718

1819

1920
class Descriptor:
@@ -48,6 +49,7 @@ def __init__(self, ds, projection=None, transform=None, use_latlon=False):
4849
self.latlon = use_latlon
4950
self.projection = projection
5051
self.transform = transform
52+
self._pre_projected = False
5153

5254
# if mesh is on a sphere, force the use of lat lon coords
5355
if ds.attrs["on_a_sphere"].strip().upper() == 'YES':
@@ -60,6 +62,7 @@ def __init__(self, ds, projection=None, transform=None, use_latlon=False):
6062
# reproject the minimal dataset, even for non-spherical meshes
6163
if projection and transform:
6264
self._transform_coordinates(projection, transform)
65+
self._pre_projected = True
6366

6467
def create_minimal_dataset(self, ds):
6568
"""
@@ -95,8 +98,8 @@ def fix_outofbounds_indices(ds, array_name):
9598
# list of coordinate / connectivity arrays needed to create patches
9699
mesh_arrays = coordinate_arrays + connectivity_arrays
97100

98-
# get the subset of arrays from the mesh dataset
99-
minimal_ds = ds[mesh_arrays]
101+
# get subset of arrays from mesh and load into memory if dask arrays
102+
minimal_ds = ds[mesh_arrays].load()
100103

101104
# delete the attributes in the minimal dataset to avoid confusion
102105
minimal_ds.attrs.clear()
@@ -138,6 +141,17 @@ def vertex_patches(self):
138141
patches = self._fix_antimeridian(patches, "Vertex")
139142
return patches
140143

144+
def get_transform(self):
145+
"""
146+
"""
147+
148+
if self._pre_projected:
149+
transform = self.projection
150+
else:
151+
transform = self.transform
152+
153+
return transform
154+
141155
def _transform_coordinates(self, projection, transform):
142156
"""
143157
"""
@@ -231,74 +245,128 @@ def transform_patches(self, patches, projection, transform):
231245

232246

233247
def _compute_cell_patches(ds):
248+
"""Create cell patches (i.e. Primary cells) for an MPAS mesh.
234249
250+
All cell patches have `ds.sizes["maxEdges"]` nodes, even if `nEdgesOnCell`
251+
for the given cell is less than maxEdges. We choosed to deal with ragged
252+
indices (i.e. node indices greater than `nEdgesOnCell` for a given cell)
253+
by repeating the first node of the patch. Nodes are ordered counter
254+
clockwise aroudn the cell center.
255+
"""
256+
# get the maximum number of edges on a cell
257+
maxEdges = ds.sizes["maxEdges"]
235258
# connectivity arrays have already been zero indexed
236259
verticesOnCell = ds.verticesOnCell
237260
# get a mask of the active vertices
238261
mask = verticesOnCell == -1
239262

240-
# get the coordinates needed to patch construction
241-
xVertex = ds.xVertex
242-
yVertex = ds.yVertex
263+
# tile the first vertices index
264+
firstVertex = np.tile(verticesOnCell[:, 0], (maxEdges, 1)).T
265+
# set masked vertices to the first vertex of the cell
266+
verticesOnCell = np.where(mask, firstVertex, verticesOnCell)
243267

244268
# reshape/expand the vertices coordinate arrays
245-
x_vert = np.ma.MaskedArray(xVertex[verticesOnCell], mask=mask)
246-
y_vert = np.ma.MaskedArray(yVertex[verticesOnCell], mask=mask)
269+
x_nodes = ds.xVertex.values[verticesOnCell]
270+
y_nodes = ds.yVertex.values[verticesOnCell]
247271

248-
verts = np.ma.stack((x_vert, y_vert), axis=-1)
272+
nodes = np.stack((x_nodes, y_nodes), axis=-1)
249273

250-
return verts
274+
return nodes
251275

252276

253-
def _compute_edge_patches(ds, latlon=False):
277+
def _compute_edge_patches(ds):
278+
"""Create edge patches for an MPAS mesh.
279+
280+
Edge patches have four nodes which typically correspond to the two cell
281+
centers of the `cellsOnEdge` and the two vertices which make up the edge.
282+
For an edge patch along a culled mesh boundary one of the cell centers
283+
usually used to construct the patch will be missing, so the corresponding
284+
node will be collapsed to the edge coordinate.
285+
"""
254286

255287
# connectivity arrays have already been zero indexed
256288
cellsOnEdge = ds.cellsOnEdge
257289
verticesOnEdge = ds.verticesOnEdge
258-
259-
# is this masking correct ??
290+
# condition should only be true once per row or else wouldn't be an edge
260291
cellMask = cellsOnEdge < 0
261-
vertexMask = verticesOnEdge < 0
262-
263-
# get the coordinates needed to patch construction
264-
xCell = ds.xCell
265-
yCell = ds.yCell
266-
xVertex = ds.xVertex
267-
yVertex = ds.yVertex
268292

269293
# get subset of cell coordinate arrays corresponding to edge patches
270-
xCell = np.ma.MaskedArray(xCell[cellsOnEdge], mask=cellMask)
271-
yCell = np.ma.MaskedArray(yCell[cellsOnEdge], mask=cellMask)
294+
xCell = ds.xCell.values[cellsOnEdge]
295+
yCell = ds.yCell.values[cellsOnEdge]
272296
# get subset of vertex coordinate arrays corresponding to edge patches
273-
xVertex = np.ma.MaskedArray(xVertex[verticesOnEdge], mask=vertexMask)
274-
yVertex = np.ma.MaskedArray(yVertex[verticesOnEdge], mask=vertexMask)
297+
xVertex = ds.xVertex.values[verticesOnEdge]
298+
yVertex = ds.yVertex.values[verticesOnEdge]
275299

276-
x_vert = np.ma.stack((xCell[:, 0], xVertex[:, 0],
277-
xCell[:, 1], xVertex[:, 1]), axis=-1)
300+
# if only one cell on edge (i.e. along a culled boundary), then collapse
301+
# the node corresponding to the missing cell back the edge location
302+
if np.any(cellMask):
303+
xCell = np.where(cellMask, ds.xEdge.values[:, np.newaxis], xCell)
304+
yCell = np.where(cellMask, ds.yEdge.values[:, np.newaxis], yCell)
278305

279-
y_vert = np.ma.stack((yCell[:, 0], yVertex[:, 0],
280-
yCell[:, 1], yVertex[:, 1]), axis=-1)
306+
x_nodes = np.stack((xCell[:, 0], xVertex[:, 0],
307+
xCell[:, 1], xVertex[:, 1]), axis=-1)
281308

282-
verts = np.ma.stack((x_vert, y_vert), axis=-1)
309+
y_nodes = np.stack((yCell[:, 0], yVertex[:, 0],
310+
yCell[:, 1], yVertex[:, 1]), axis=-1)
283311

284-
return verts
312+
nodes = np.stack((x_nodes, y_nodes), axis=-1)
285313

314+
return nodes
286315

287-
def _compute_vertex_patches(ds, latlon=False):
288316

289-
# connectivity arrays have already been zero indexed
290-
cellsOnVertex = ds.cellsOnVertex
291-
# get a mask of the active vertices
292-
mask = ds.cellsOnVertex == -1
317+
def _compute_vertex_patches(ds):
318+
"""Create vertex patches (i.e. Dual Cells) for an MPAS mesh.
293319
294-
# get the coordinates needed to patch construction
295-
xCell = ds.xCell
296-
yCell = ds.yCell
320+
Vertex patches have 6 nodes despite the typical dual cell only having
321+
three nodes (i.e. the cell centers of three cells on the vertex) in order
322+
ease the creation of vertex patches along culled mesh boundaries.
323+
The typical vertex patch will be comprised of the edges and cell centers
324+
of the `cellsOnVertex`. As the MPAS Mesh Specs (version 1.0: Section 5.3)
325+
outlines: "Edges lead cells as they move around vertex". So, the first node
326+
in a vertex patch will correspond to an edge (if present).
297327
298-
# reshape/expand the vertices coordinate arrays
299-
x_vert = np.ma.MaskedArray(xCell[cellsOnVertex], mask=mask)
300-
y_vert = np.ma.MaskedArray(yCell[cellsOnVertex], mask=mask)
328+
For patches along culled boundaries, if an edge and/or cell center is
329+
missing the corresponding node will be collapsed to the patches vertex
330+
position.
331+
"""
332+
nVertices = ds.sizes["nVertices"]
333+
vertexDegree = ds.sizes["vertexDegree"]
301334

302-
verts = np.ma.stack((x_vert, y_vert), axis=-1)
335+
nodes = np.zeros((nVertices, vertexDegree * 2, 2))
336+
# connectivity arrays have already been zero indexed
337+
cellsOnVertex = ds.cellsOnVertex.values
338+
edgesOnVertex = ds.edgesOnVertex.values
339+
# get a mask of active nodes
340+
cellMask = cellsOnVertex == -1
341+
edgeMask = edgesOnVertex == -1
342+
unionMask = cellMask & edgeMask
303343

304-
return verts
344+
# get the coordinates needed to patch construction
345+
xCell = ds.xCell.values
346+
yCell = ds.yCell.values
347+
xEdge = ds.xEdge.values
348+
yEdge = ds.yEdge.values
349+
# convert vertex coordinates to column vectors for broadcasting below
350+
xVertex = ds.xVertex.values[:, np.newaxis]
351+
yVertex = ds.yVertex.values[:, np.newaxis]
352+
353+
# if edge is missing collapse edge node to vertex, else leave at edge
354+
nodes[:, ::2, 0] = np.where(edgeMask, xVertex, xEdge[edgesOnVertex])
355+
nodes[:, ::2, 1] = np.where(edgeMask, yVertex, yEdge[edgesOnVertex])
356+
357+
# if cell is missing collapse cell node to vertex, else leave at cell
358+
nodes[:, 1::2, 0] = np.where(cellMask, xVertex, xCell[cellsOnVertex])
359+
nodes[:, 1::2, 1] = np.where(cellMask, yVertex, yCell[cellsOnVertex])
360+
361+
# -------------------------------------------------------------------------
362+
# NOTE: While the condition below probably only applies to the final edge
363+
# node we apply it to all, since the conditions above ensure the
364+
# patches will still be created correctly
365+
# -------------------------------------------------------------------------
366+
# if cell and edge missing collapse edge node to the first edge.
367+
# Because edges lead the vertices this ensures the patch encompasses
368+
# the full kite area and is propely closed.
369+
nodes[:, ::2, 0] = np.where(unionMask, nodes[:, 0:1, 0], nodes[:, ::2, 0])
370+
nodes[:, ::2, 1] = np.where(unionMask, nodes[:, 0:1, 1], nodes[:, ::2, 1])
371+
372+
return nodes

mosaic/polypcolor.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from cartopy.mpl.geoaxes import GeoAxes
12
from matplotlib.axes import Axes
23
from matplotlib.collections import PolyCollection
34
from matplotlib.colors import Normalize
@@ -56,34 +57,23 @@ def polypcolor(
5657
elif "nVertices" in c.dims:
5758
verts = descriptor.vertex_patches
5859

60+
transform = descriptor.get_transform()
61+
5962
collection = PolyCollection(verts, alpha=alpha, array=c,
6063
cmap=cmap, norm=norm, **kwargs)
6164

62-
collection._scale_norm(norm, vmin, vmax)
63-
64-
# get the limits of the **data**, which could exceed the valid
65-
# axis limits of the transform
66-
minx = verts[..., 0].min()
67-
maxx = verts[..., 0].max()
68-
miny = verts[..., 1].min()
69-
maxy = verts[..., 1].max()
65+
# only set the transform if GeoAxes
66+
if isinstance(ax, GeoAxes):
67+
collection.set_transform(transform)
7068

71-
corners = (minx, miny), (maxx, maxy)
72-
ax.update_datalim(corners)
73-
ax._request_autoscale_view()
69+
collection._scale_norm(norm, vmin, vmax)
7470
ax.add_collection(collection)
7571

76-
# if data has been transformed use the transforms x-limits.
77-
# patches have vertices that exceed the transfors x-limits to visually
78-
# correct the antimeridian problem
79-
if descriptor.projection:
80-
minx = descriptor.projection.x_limits[0]
81-
maxx = descriptor.projection.x_limits[1]
82-
83-
miny = descriptor.projection.y_limits[0]
84-
maxy = descriptor.projection.y_limits[1]
85-
86-
ax.set_xbound(minx, maxx)
87-
ax.set_ybound(miny, maxy)
72+
# Update the datalim for this polycollection
73+
limits = collection.get_datalim(ax.transData)
74+
# TODO: account for nodes of patches that lie outside of projection bounds
75+
# (i.e. as a result of patch wrapping at the antimeridian)
76+
ax.update_datalim(limits)
77+
ax.autoscale_view()
8878

8979
return collection

0 commit comments

Comments
 (0)