Skip to content

Commit 58b560e

Browse files
koldunovnclaude
andcommitted
Add MPAS dual-mesh connectivity support for YAC interpolation
Read native cellsOnVertex(nVertices, vertexDegree=3) from MPAS files instead of auto-generating lat-band triangulation, which produced degenerate/duplicate triangles causing YAC to abort. Boundary triangles referencing invalid cell index 0 are filtered out automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20029ad commit 58b560e

File tree

4 files changed

+311
-15
lines changed

4 files changed

+311
-15
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ MPAS unstructured data:
262262
```bash
263263
./ushow data.nc grid.nc # MPAS UGRID (face_lon/face_lat)
264264
./ushow mpas_output.nc # Native MPAS (lonCell/latCell)
265+
./ushow mpas_output.nc --yac -r 0.1 # MPAS with YAC interpolation (uses native cellsOnVertex connectivity)
265266
```
266267

267268
YAC interpolation (requires `make WITH_YAC=1`):
@@ -367,7 +368,7 @@ By default, ushow uses a fast KDTree nearest-neighbor lookup to map unstructured
367368
| `avg_dist` | Cell averaging, distance-weighted |
368369
| `avg_bary` | Cell averaging, barycentric |
369370

370-
NNN methods work with any grid type. Averaging methods require element connectivity — for grids that lack it (reduced Gaussian, HEALPix, etc.), ushow auto-generates a triangulation from latitude bands.
371+
NNN methods work with any grid type. Averaging methods require element connectivity — for grids that lack it (reduced Gaussian, HEALPix, etc.), ushow auto-generates a triangulation from latitude bands. For MPAS grids, native dual-mesh connectivity (`cellsOnVertex`) is read directly from the file.
371372

372373
### Per-Depth Masking (`--yac-3d`)
373374

src/mesh.c

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -666,10 +666,115 @@ USMesh *mesh_create_from_zarr(USFile *file) {
666666

667667
#endif /* HAVE_ZARR */
668668

669+
/* Try to load MPAS dual-mesh connectivity (cellsOnVertex).
670+
* In MPAS, each Voronoi vertex connects exactly 3 cells.
671+
* cellsOnVertex(nVertices, vertexDegree=3) gives triangle connectivity
672+
* where the "vertices" of each triangle are cell centers (our mesh points).
673+
* Boundary triangles referencing cell 0 (invalid in 1-based) are skipped.
674+
*/
675+
static int load_mpas_connectivity(USMesh *mesh, int ncid) {
676+
int varid, status;
677+
678+
status = nc_inq_varid(ncid, "cellsOnVertex", &varid);
679+
if (status != NC_NOERR) return -1;
680+
681+
int ndims;
682+
nc_inq_varndims(ncid, varid, &ndims);
683+
if (ndims != 2) return -1;
684+
685+
int dimids[2];
686+
size_t dim_sizes[2];
687+
nc_inq_vardimid(ncid, varid, dimids);
688+
nc_inq_dimlen(ncid, dimids[0], &dim_sizes[0]);
689+
nc_inq_dimlen(ncid, dimids[1], &dim_sizes[1]);
690+
691+
/* Expect (nVertices, vertexDegree=3) */
692+
size_t n_tri_raw, vdeg;
693+
if (dim_sizes[1] == 3) {
694+
n_tri_raw = dim_sizes[0];
695+
vdeg = dim_sizes[1];
696+
} else if (dim_sizes[0] == 3) {
697+
n_tri_raw = dim_sizes[1];
698+
vdeg = dim_sizes[0];
699+
} else {
700+
return -1; /* Not a triangle dual mesh */
701+
}
702+
703+
int *raw = malloc(n_tri_raw * vdeg * sizeof(int));
704+
if (!raw) return -1;
705+
706+
status = nc_get_var_int(ncid, varid, raw);
707+
if (status != NC_NOERR) {
708+
free(raw);
709+
return -1;
710+
}
711+
712+
int transpose = (dim_sizes[0] == 3);
713+
714+
/* First pass: count valid triangles (skip those referencing cell 0 = boundary) */
715+
size_t n_valid = 0;
716+
for (size_t i = 0; i < n_tri_raw; i++) {
717+
int v0, v1, v2;
718+
if (transpose) {
719+
v0 = raw[0 * n_tri_raw + i];
720+
v1 = raw[1 * n_tri_raw + i];
721+
v2 = raw[2 * n_tri_raw + i];
722+
} else {
723+
v0 = raw[i * 3 + 0];
724+
v1 = raw[i * 3 + 1];
725+
v2 = raw[i * 3 + 2];
726+
}
727+
if (v0 > 0 && v1 > 0 && v2 > 0) n_valid++;
728+
}
729+
730+
int *elem_nodes = malloc(n_valid * 3 * sizeof(int));
731+
if (!elem_nodes) {
732+
free(raw);
733+
return -1;
734+
}
735+
736+
/* Second pass: store valid triangles, convert to 0-based */
737+
size_t out = 0;
738+
for (size_t i = 0; i < n_tri_raw; i++) {
739+
int v0, v1, v2;
740+
if (transpose) {
741+
v0 = raw[0 * n_tri_raw + i];
742+
v1 = raw[1 * n_tri_raw + i];
743+
v2 = raw[2 * n_tri_raw + i];
744+
} else {
745+
v0 = raw[i * 3 + 0];
746+
v1 = raw[i * 3 + 1];
747+
v2 = raw[i * 3 + 2];
748+
}
749+
if (v0 > 0 && v1 > 0 && v2 > 0) {
750+
/* Also validate indices are within range */
751+
if ((size_t)(v0 - 1) < mesh->n_points &&
752+
(size_t)(v1 - 1) < mesh->n_points &&
753+
(size_t)(v2 - 1) < mesh->n_points) {
754+
elem_nodes[out * 3 + 0] = v0 - 1;
755+
elem_nodes[out * 3 + 1] = v1 - 1;
756+
elem_nodes[out * 3 + 2] = v2 - 1;
757+
out++;
758+
}
759+
}
760+
}
761+
762+
free(raw);
763+
764+
mesh->n_elements = out;
765+
mesh->n_vertices = 3;
766+
mesh->elem_nodes = elem_nodes;
767+
768+
printf("Loaded MPAS dual-mesh connectivity: %zu triangles from cellsOnVertex "
769+
"(%zu raw, %zu boundary skipped)\n",
770+
out, n_tri_raw, n_tri_raw - out);
771+
return 0;
772+
}
773+
669774
/* Load element connectivity from mesh file for polygon rendering */
670775
static int load_element_connectivity(USMesh *mesh, int ncid) {
671776
int status, varid;
672-
777+
673778
/* Try to find face_nodes variable (UGRID convention) */
674779
static const char *CONN_NAMES[] = {
675780
"face_nodes", "face_node_connectivity", "elem", NULL
@@ -683,23 +788,24 @@ static int load_element_connectivity(USMesh *mesh, int ncid) {
683788
}
684789
}
685790
if (!found_conn) {
686-
return -1; /* No connectivity found - not an error, just no polygon mode */
791+
/* Try MPAS dual-mesh connectivity (cellsOnVertex) */
792+
return load_mpas_connectivity(mesh, ncid);
687793
}
688-
794+
689795
/* Get dimensions */
690796
int ndims;
691797
nc_inq_varndims(ncid, varid, &ndims);
692798
if (ndims != 2) {
693799
fprintf(stderr, "face_nodes: expected 2D, got %dD\n", ndims);
694800
return -1;
695801
}
696-
802+
697803
int dimids[2];
698804
size_t dim_sizes[2];
699805
nc_inq_vardimid(ncid, varid, dimids);
700806
nc_inq_dimlen(ncid, dimids[0], &dim_sizes[0]);
701807
nc_inq_dimlen(ncid, dimids[1], &dim_sizes[1]);
702-
808+
703809
/* Determine which dimension is vertices (n3=3) vs elements */
704810
size_t n_vertices, n_elements;
705811
int transpose = 0;
@@ -715,32 +821,32 @@ static int load_element_connectivity(USMesh *mesh, int ncid) {
715821
fprintf(stderr, "face_nodes: cannot identify vertex dimension\n");
716822
return -1;
717823
}
718-
824+
719825
printf("Loading element connectivity: %zu elements, %zu vertices each\n",
720826
n_elements, n_vertices);
721-
827+
722828
/* Allocate and read connectivity */
723829
int *raw_data = malloc(n_elements * n_vertices * sizeof(int));
724830
if (!raw_data) return -1;
725-
831+
726832
status = nc_get_var_int(ncid, varid, raw_data);
727833
if (status != NC_NOERR) {
728834
fprintf(stderr, "Failed to read face_nodes: %s\n", nc_strerror(status));
729835
free(raw_data);
730836
return -1;
731837
}
732-
838+
733839
/* Get start_index attribute (1-based or 0-based indexing) */
734840
int start_index = 1; /* Default to 1-based (Fortran) */
735841
nc_get_att_int(ncid, varid, "start_index", &start_index);
736-
842+
737843
/* Allocate final array and transpose if needed */
738844
mesh->elem_nodes = malloc(n_elements * n_vertices * sizeof(int));
739845
if (!mesh->elem_nodes) {
740846
free(raw_data);
741847
return -1;
742848
}
743-
849+
744850
if (transpose) {
745851
/* Data is (n3, elem), transpose to (elem, n3) and convert to 0-based */
746852
for (size_t e = 0; e < n_elements; e++) {
@@ -755,12 +861,12 @@ static int load_element_connectivity(USMesh *mesh, int ncid) {
755861
mesh->elem_nodes[i] = raw_data[i] - start_index;
756862
}
757863
}
758-
864+
759865
free(raw_data);
760-
866+
761867
mesh->n_elements = n_elements;
762868
mesh->n_vertices = (int)n_vertices;
763-
869+
764870
printf("Loaded %zu triangular elements for polygon rendering\n", n_elements);
765871
return 0;
766872
}

tests/test_mesh.c

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
#include "test_framework.h"
6+
#include "test_utils.h"
67
#define _USE_MATH_DEFINES
78
#include <math.h>
89
#include "../src/ushow.defines.h"
@@ -362,4 +363,92 @@ TEST(meters_to_chord_half_circumference) {
362363
return 1;
363364
}
364365

366+
/* Test MPAS cellsOnVertex connectivity loading */
367+
TEST(mpas_connectivity_loads) {
368+
int n_cells = 20, n_vertices = 30, n_boundary = 5;
369+
const char *filename = create_test_netcdf_mpas_with_connectivity(
370+
n_cells, n_vertices, n_boundary);
371+
ASSERT_NOT_NULL(filename);
372+
373+
int ncid;
374+
int status = nc_open(filename, NC_NOWRITE, &ncid);
375+
ASSERT_EQ_INT(status, NC_NOERR);
376+
377+
USMesh *mesh = mesh_create_from_netcdf(ncid, NULL);
378+
ASSERT_NOT_NULL(mesh);
379+
nc_close(ncid);
380+
381+
/* Should have loaded MPAS connectivity */
382+
ASSERT_GT(mesh->n_elements, 0);
383+
ASSERT_EQ_INT(mesh->n_vertices, 3);
384+
ASSERT_NOT_NULL(mesh->elem_nodes);
385+
386+
/* Boundary vertices (referencing cell 0) should be skipped */
387+
ASSERT_EQ_SIZET(mesh->n_elements, (size_t)(n_vertices - n_boundary));
388+
389+
/* All indices must be in range [0, n_cells) */
390+
for (size_t i = 0; i < mesh->n_elements; i++) {
391+
int a = mesh->elem_nodes[i * 3 + 0];
392+
int b = mesh->elem_nodes[i * 3 + 1];
393+
int c = mesh->elem_nodes[i * 3 + 2];
394+
ASSERT_GE(a, 0);
395+
ASSERT_LT(a, n_cells);
396+
ASSERT_GE(b, 0);
397+
ASSERT_LT(b, n_cells);
398+
ASSERT_GE(c, 0);
399+
ASSERT_LT(c, n_cells);
400+
}
401+
402+
mesh_free(mesh);
403+
cleanup_test_file(filename);
404+
return 1;
405+
}
406+
407+
/* Test MPAS connectivity: all-boundary file produces zero elements */
408+
TEST(mpas_connectivity_all_boundary) {
409+
int n_cells = 10, n_vertices = 8, n_boundary = 8;
410+
const char *filename = create_test_netcdf_mpas_with_connectivity(
411+
n_cells, n_vertices, n_boundary);
412+
ASSERT_NOT_NULL(filename);
413+
414+
int ncid;
415+
int status = nc_open(filename, NC_NOWRITE, &ncid);
416+
ASSERT_EQ_INT(status, NC_NOERR);
417+
418+
USMesh *mesh = mesh_create_from_netcdf(ncid, NULL);
419+
ASSERT_NOT_NULL(mesh);
420+
nc_close(ncid);
421+
422+
/* All vertices are boundary -> all triangles skipped */
423+
ASSERT_EQ_SIZET(mesh->n_elements, 0);
424+
425+
mesh_free(mesh);
426+
cleanup_test_file(filename);
427+
return 1;
428+
}
429+
430+
/* Test MPAS connectivity: no boundary vertices */
431+
TEST(mpas_connectivity_no_boundary) {
432+
int n_cells = 15, n_vertices = 20, n_boundary = 0;
433+
const char *filename = create_test_netcdf_mpas_with_connectivity(
434+
n_cells, n_vertices, n_boundary);
435+
ASSERT_NOT_NULL(filename);
436+
437+
int ncid;
438+
int status = nc_open(filename, NC_NOWRITE, &ncid);
439+
ASSERT_EQ_INT(status, NC_NOERR);
440+
441+
USMesh *mesh = mesh_create_from_netcdf(ncid, NULL);
442+
ASSERT_NOT_NULL(mesh);
443+
nc_close(ncid);
444+
445+
/* No boundary -> all triangles kept */
446+
ASSERT_EQ_SIZET(mesh->n_elements, (size_t)n_vertices);
447+
ASSERT_EQ_INT(mesh->n_vertices, 3);
448+
449+
mesh_free(mesh);
450+
cleanup_test_file(filename);
451+
return 1;
452+
}
453+
365454
RUN_TESTS("Mesh")

0 commit comments

Comments
 (0)