Skip to content

Commit 1c837b3

Browse files
authored
Merge pull request #710 from xylar/fix-mesh-converter-angle-edge
Fix computation of `angleEdge` in NetCDF-C mesh converter
2 parents b8bb61a + 5f8f272 commit 1c837b3

11 files changed

Lines changed: 224 additions & 57 deletions

File tree

conda_package/docs/api.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ Mesh conversion
7676
compute_mpas_flood_fill_mask
7777
compute_lon_lat_region_masks
7878

79+
.. currentmodule:: mpas_tools.mesh.spherical
80+
81+
.. autosummary::
82+
:toctree: generated/
83+
84+
recompute_angle_edge
85+
calc_edge_normal_vector
86+
calc_vector_east_north
87+
7988
.. currentmodule:: mpas_tools.merge_grids
8089

8190
.. autosummary::

conda_package/docs/authors.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ Contributors
2323
* Phillip J. Wolfram
2424
* Tong Zhang
2525

26-
For a list of all the contributions:
27-
https://github.com/MPAS-Dev/MPAS-Tools/graphs/contributors
26+
For a list of all contributions, see the
27+
`contributors graph <https://github.com/MPAS-Dev/MPAS-Tools/graphs/contributors>`_.

conda_package/docs/building_docs.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ To preview the documentation locally, open the ``index.html`` file in the
2626
cd _build/html
2727
python -m http.server 8000
2828
29-
Then, open http://0.0.0.0:8000/master/ in your browser.
29+
Then, open `<http://0.0.0.0:8000/master/>`_ in your browser.

conda_package/docs/making_changes.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ things like whitespace at the end of lines.
2121
The first time you set up the ``mpas_tools_dev`` environment, you will need to set up
2222
``pre-commit``. This is done by running:
2323

24-
```bash
25-
pre-commit install
26-
```
24+
.. code-block:: bash
25+
26+
pre-commit install
2727
2828
You only need to do this once when you create the ``mpas_tools_dev``
2929
environment. If you create a new version of ``mpas_tools_dev``, then you will
@@ -43,9 +43,9 @@ PEP8 compliance, as well as sort, check and format imports,
4343
f-strings, and `mypy <https://mypy-lang.org/>` to check for consistent variable
4444
types. An example error might be:
4545

46-
```bash
47-
example.py:77:1: E302 expected 2 blank lines, found 1
48-
```
46+
.. code-block:: bash
47+
48+
example.py:77:1: E302 expected 2 blank lines, found 1
4949
5050
For this example, we would just add an additional blank line after line 77 and
5151
try the commit again to make sure we've resolved the issue.

conda_package/docs/mesh_conversion.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ The converter also generates a ``graph.info`` file for graph partitioning
8383
tools (e.g., Metis). In Python, this file is only written if the
8484
``graphInfoFileName`` argument is provided.
8585

86+
For spherical meshes, the :py:mod:`mpas_tools.mesh.spherical` module provides
87+
Python utilities for recomputing ``angleEdge`` and related local east/north
88+
geometry directly from the mesh coordinates. This can be useful for
89+
verification and diagnostics after conversion.
90+
8691
.. _cell_culler:
8792

8893
Cell Culler

conda_package/docs/releasing.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ Version Bump and Dependency Updates
1717
- ``conda_package/mpas_tools/__init__.py``
1818
- ``conda_package/recipe/recipe.yaml``
1919

20-
- Make sure the version follows semantic versioning (see
21-
https://semver.org/).
20+
- Make sure the version follows
21+
`semantic versioning <https://semver.org/>`_.
2222
For release candidates, use versions like ``1.3.0rc1`` (no ``v`` prefix).
2323

2424
2. **Check and Update Dependencies**
@@ -38,7 +38,7 @@ Version Bump and Dependency Updates
3838
- The dependencies in ``recipe.yaml`` are the ones that will be used for the
3939
released package on conda-forge. The dependencies in ``pyproject.toml``
4040
are for PyPI and should be kept in sync as much as possible but are only
41-
there as a sanity check when we run ```pip check``. The ``dev-spec.txt``
41+
there as a sanity check when we run ``pip check``. The ``dev-spec.txt``
4242
file should include all dependencies needed for development and testing,
4343
and ``pixi.toml`` should remain equivalent for pixi users.
4444

@@ -100,8 +100,7 @@ Tagging and Publishing a Release Candidate
100100
- Update dependencies if needed
101101

102102
- Commit, push to a new branch, and open a PR **against the ``dev`` branch**
103-
of the feedstock:
104-
https://github.com/conda-forge/mpas_tools-feedstock
103+
of the `mpas_tools-feedstock <https://github.com/conda-forge/mpas_tools-feedstock>`_.
105104

106105
- Follow any instructions in the PR template and merge once approved
107106

@@ -112,7 +111,7 @@ Publishing a Stable Release
112111

113112
- For stable releases, create a GitHub release page as follows:
114113

115-
- Go to https://github.com/MPAS-Dev/MPAS-Tools/releases
114+
- Go to `the GitHub releases page <https://github.com/MPAS-Dev/MPAS-Tools/releases>`_
116115

117116
- Click "Draft a new release"
118117

@@ -127,8 +126,8 @@ Publishing a Stable Release
127126

128127
7. **Updating the conda-forge Feedstock for a Stable Release**
129128

130-
- Wait for the ``regro-cf-autotick-bot`` to open a PR at:
131-
https://github.com/conda-forge/mpas_tools-feedstock
129+
- Wait for the ``regro-cf-autotick-bot`` to open a PR at the
130+
`mpas_tools-feedstock repository <https://github.com/conda-forge/mpas_tools-feedstock>`_.
132131

133132
- This may take several hours to a day.
134133

@@ -137,7 +136,8 @@ Publishing a Stable Release
137136
- Merge once CI checks pass
138137

139138
**Note:** If you are impatient, you can accelerate this process by creating
140-
a bot issue at: https://github.com/conda-forge/mpas_tools-feedstock/issues
139+
a bot issue at the
140+
`mpas_tools-feedstock issues page <https://github.com/conda-forge/mpas_tools-feedstock/issues>`_
141141
with the subject ``@conda-forge-admin, please update version``. This
142142
will open a new PR with the version within a few minutes.
143143

conda_package/docs/testing_changes.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,30 @@ command:
2525
cd conda_package
2626
pixi install
2727
pixi shell
28-
rattler-build build -m ci/linux_64_python3.14.____cpython.yaml -r recipe/ --output-dir ../output
28+
rattler-build build -m ci/linux_64_python3.14.____cpython.yaml -r recipe/ --output-dir output
2929
30-
This writes package artifacts to ``output/`` in the repository root.
30+
This writes package artifacts to ``output/`` under ``conda_package``.
3131

3232
To install the locally built package into the pixi environment, add the local
3333
build output as a channel and then add ``mpas_tools`` from that channel:
3434

3535
.. code-block:: bash
3636
3737
cd conda_package
38-
pixi workspace channel add "file://$PWD/../output"
39-
pixi add --platform linux-64 "mpas_tools [channel='file://$PWD/../output']"
38+
pixi workspace channel add "file://$PWD/output"
39+
pixi add --platform linux-64 "mpas_tools [channel='file://$PWD/output']"
40+
41+
.. important::
42+
43+
pixi, like other conda-family package managers, identifies a package by
44+
its name, version and build string. If you rebuild a local package without
45+
changing that identity, pixi may continue using an older cached artifact
46+
even if the file in ``output/`` has changed.
47+
48+
If you rebuild ``mpas_tools`` locally and need pixi to pick up the new
49+
package contents reliably, bump the conda recipe build number in
50+
``conda_package/recipe/recipe.yaml`` before rebuilding. For Python-only
51+
development, ``pixi run install-editable`` is often more convenient.
4052

4153
.. warning::
4254

@@ -75,7 +87,8 @@ Then run tools within the pixi shell (for example ``pytest``).
7587

7688
A useful hybrid workflow is to install the latest release conda package
7789
first (to get compiled tools), then install your branch in editable mode on
78-
top for Python development.
90+
top for Python development. This also avoids the need to bump the conda
91+
build number for every local Python-only rebuild.
7992

8093
Legacy Method: Conda Editable Install
8194
*************************************

conda_package/docs/visualization.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ centers to triangle nodes. ``dsTris`` includes variables ``triCellIndices``,
4949
the cell that each triangle is part of; ``nodeCellIndices`` and
5050
``nodeCellWeights``, the indices and weights used to interpolate from MPAS cell
5151
centers to triangle nodes; Cartesian coordinates ``xNode``, ``yNode``, and
52-
``zNode``; and ``lonNode``` and ``latNode`` in radians. ``lonNode`` is
52+
``zNode``; and ``lonNode`` and ``latNode`` in radians. ``lonNode`` is
5353
guaranteed to be within 180 degrees of the cell center corresponding to
5454
``triCellIndices``. Nodes always have a counterclockwise winding.
5555

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import numpy as np
2+
import xarray as xr
3+
4+
from mpas_tools.transects import lon_lat_to_cartesian
5+
6+
7+
def recompute_angle_edge(ds_mesh):
8+
"""
9+
Recompute ``angleEdge`` from edge and vertex locations on the sphere.
10+
11+
Parameters
12+
----------
13+
ds_mesh : xarray.Dataset
14+
An MPAS spherical mesh dataset containing edge and vertex locations.
15+
16+
Returns
17+
-------
18+
angle_edge : xarray.DataArray
19+
``angleEdge`` recomputed from spherical geometry.
20+
"""
21+
normal_east_north = calc_edge_normal_vector(ds_mesh)
22+
angle_edge = xr.zeros_like(ds_mesh.angleEdge)
23+
angle_edge.values = np.atan2(
24+
normal_east_north[:, 1], normal_east_north[:, 0]
25+
)
26+
return angle_edge
27+
28+
29+
def calc_edge_normal_vector(ds_mesh):
30+
"""
31+
Compute edge-normal vectors projected onto local east/north coordinates.
32+
33+
Parameters
34+
----------
35+
ds_mesh : xarray.Dataset
36+
An MPAS spherical mesh dataset containing edge and vertex locations.
37+
38+
Returns
39+
-------
40+
normal_east_north : numpy.ndarray
41+
A ``(nEdges, 2)`` array of unit normal vectors in local east/north
42+
coordinates.
43+
"""
44+
edge_cartesian = np.array(
45+
lon_lat_to_cartesian(
46+
ds_mesh.lonEdge, ds_mesh.latEdge, 1.0, degrees=False
47+
)
48+
)
49+
50+
vertex_1 = ds_mesh.verticesOnEdge.isel(TWO=0).values - 1
51+
vertex_2 = ds_mesh.verticesOnEdge.isel(TWO=1).values - 1
52+
53+
lon_vertex_1 = ds_mesh.lonVertex.isel(nVertices=vertex_1)
54+
lat_vertex_1 = ds_mesh.latVertex.isel(nVertices=vertex_1)
55+
lon_vertex_2 = ds_mesh.lonVertex.isel(nVertices=vertex_2)
56+
lat_vertex_2 = ds_mesh.latVertex.isel(nVertices=vertex_2)
57+
58+
vertex_1_cartesian = np.array(
59+
lon_lat_to_cartesian(lon_vertex_1, lat_vertex_1, 1.0, degrees=False)
60+
)
61+
vertex_2_cartesian = np.array(
62+
lon_lat_to_cartesian(lon_vertex_2, lat_vertex_2, 1.0, degrees=False)
63+
)
64+
65+
dvertex_cartesian = vertex_2_cartesian - vertex_1_cartesian
66+
normal_cartesian = np.cross(dvertex_cartesian, edge_cartesian, axis=0)
67+
68+
edge_east, edge_north = calc_vector_east_north(
69+
edge_cartesian[0, :], edge_cartesian[1, :], edge_cartesian[2, :]
70+
)
71+
72+
normal_east_north = np.zeros((ds_mesh.sizes['nEdges'], 2))
73+
normal_east_north[:, 0] = np.sum(edge_east * normal_cartesian, axis=0)
74+
normal_east_north[:, 1] = np.sum(edge_north * normal_cartesian, axis=0)
75+
76+
norm = np.linalg.norm(normal_east_north, axis=1)
77+
nonzero = norm > 0.0
78+
normal_east_north[nonzero, :] /= norm[nonzero, np.newaxis]
79+
80+
return normal_east_north
81+
82+
83+
def calc_vector_east_north(x, y, z):
84+
"""
85+
Compute local east and north unit vectors on the sphere.
86+
87+
Parameters
88+
----------
89+
x, y, z : numpy.ndarray
90+
Cartesian coordinates of points on the unit sphere.
91+
92+
Returns
93+
-------
94+
east, north : tuple of numpy.ndarray
95+
Local east and north unit vectors, each with shape ``(3, nPoints)``.
96+
"""
97+
axis = np.array([0.0, 0.0, 1.0])
98+
xyz = np.stack((x, y, z), axis=1)
99+
east = np.cross(axis, np.transpose(xyz), axis=0)
100+
north = np.cross(np.transpose(xyz), east, axis=0)
101+
102+
east /= np.linalg.norm(east, axis=0)
103+
north /= np.linalg.norm(north, axis=0)
104+
105+
return east, north

conda_package/tests/test_conversion.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#!/usr/bin/env python
22

33
import matplotlib
4+
import numpy as np
45

56
from mpas_tools.io import write_netcdf
67
from mpas_tools.mesh.conversion import convert, cull, mask
8+
from mpas_tools.mesh.spherical import recompute_angle_edge
79

810
from .util import get_test_data_file
911

@@ -30,5 +32,21 @@ def test_conversion():
3032
write_netcdf(dsMask, 'antarctic_mask.nc')
3133

3234

35+
def test_conversion_angle_edge():
36+
ds_mesh = xarray.open_dataset(
37+
get_test_data_file('mesh.QU.1920km.151026.nc')
38+
)
39+
ds_mesh = convert(dsIn=ds_mesh)
40+
41+
angle_edge_python = recompute_angle_edge(ds_mesh)
42+
angle_diff = np.angle(
43+
np.exp(1j * (angle_edge_python.values - ds_mesh.angleEdge.values))
44+
)
45+
46+
assert np.all(np.isfinite(angle_diff))
47+
assert np.max(np.abs(angle_diff)) < 1.0e-10
48+
49+
3350
if __name__ == '__main__':
3451
test_conversion()
52+
test_conversion_angle_edge()

0 commit comments

Comments
 (0)