-
Notifications
You must be signed in to change notification settings - Fork 1
Make edge/face z-coordinate calculations dask-compatible #8
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
Open
aesgeorges
wants to merge
7
commits into
main
Choose a base branch
from
BDS-181-edge-face-z-to-dask
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
2fbfbad
Rewrites _calculate_edge_z and _calculate_face_z in _schismgrid.py to…
a97a800
Add tests for face and edge z-coordinate calculations in test_schismg…
24b75ec
Removed leftover commented-out implementation
aesgeorges 91e48d6
Fix return type annotation and docstring for _calculate_face_z
Copilot 49a8260
Fix return type annotation and docstring for _calculate_edge_z
Copilot a81aafe
Remove .values calls in assign_coords to keep z-coordinates lazy
Copilot 36a1e39
Fix z-coordinate shape order in _assign_z_coords, _calculate_edge_z, …
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| """Tests for suxarray.io._schismgrid face/edge z-coordinate calculations. | ||
|
|
||
| Uses real SCHISM test data (out2d_1.nc / zCoordinates_1.nc). | ||
|
|
||
| Focuses on: | ||
| - _calculate_face_z: correct averaging for triangular (3-node) and | ||
| quadrilateral (4-node) cells, with fill-value masking for tri cells. | ||
| - _calculate_edge_z: correct averaging over the two endpoint nodes. | ||
| """ | ||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| import xarray as xr | ||
|
|
||
| from suxarray.io._schismgrid import ( | ||
| _calculate_edge_z, | ||
| _calculate_face_z, | ||
| _rename_coords, | ||
| ) | ||
| from uxarray.io._ugrid import _read_ugrid | ||
|
|
||
| TESTDATA = Path(__file__).parent / "testdata" | ||
|
|
||
| # Expected face/edge counts for out2d_1.nc | ||
| N_NODES = 823 | ||
| N_FACES = 791 | ||
| N_EDGES = 1613 | ||
| N_TRI = 87 | ||
| N_QUAD = 704 | ||
| N_LAYERS = 23 | ||
| N_TIME = 48 | ||
|
|
||
| # First tri face in the test mesh: index 64, nodes 63/80/79 (0-based) | ||
| TRI_IDX = 64 | ||
| TRI_NODES = [63, 80, 79] | ||
|
|
||
| # First quad face in the test mesh: index 0, nodes 0/1/6/3 (0-based) | ||
| QUAD_IDX = 0 | ||
| QUAD_NODES = [0, 1, 6, 3] | ||
|
|
||
| # First edge: index 0, nodes 0/1 (0-based) | ||
| EDGE_IDX = 0 | ||
| EDGE_NODES = [0, 1] | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def calc_inputs(): | ||
| """Prepare processed ds_out2d and ds_zcoords ready for the calculation | ||
| functions, mirroring the pre-processing done inside _read_schism_grid.""" | ||
| ds_out2d_raw = xr.open_dataset(str(TESTDATA / "out2d_1.nc")) | ||
| ds_zcoords_raw = xr.open_dataset(str(TESTDATA / "zCoordinates_1.nc")) | ||
|
|
||
| # Process out2d the same way _read_schism_grid does | ||
| ds_out2d = ds_out2d_raw.drop_dims("time") | ||
| ds_out2d, _ = _read_ugrid(ds_out2d) | ||
| ds_out2d = _rename_coords(ds_out2d) | ||
|
|
||
| # Rename zcoords dims the same way _read_schism_grid does | ||
| ds_zcoords = ds_zcoords_raw.swap_dims( | ||
| {"nSCHISM_hgrid_node": "n_node", "nSCHISM_vgrid_layers": "n_layer"} | ||
| ) | ||
|
|
||
| return ds_out2d, ds_zcoords, ds_zcoords_raw | ||
|
|
||
|
|
||
| # Connectivity – sanity checks on the test mesh | ||
|
|
||
| def test_mesh_tri_and_quad_counts(calc_inputs): | ||
| """Test mesh has the expected 87 tri and 704 quad cells.""" | ||
| ds_out2d, _, _ = calc_inputs | ||
| fnc = ds_out2d.face_node_connectivity.values | ||
| fill = ds_out2d.face_node_connectivity._FillValue | ||
| assert int(np.sum(fnc[:, -1] == fill)) == N_TRI | ||
| assert int(np.sum(fnc[:, -1] != fill)) == N_QUAD | ||
| assert ds_out2d.face_node_connectivity.sizes["n_face"] == N_FACES | ||
|
|
||
|
|
||
| # _calculate_face_z | ||
|
|
||
| def test_calculate_face_z_output_shape(calc_inputs): | ||
| """_calculate_face_z returns (n_face, time, n_layer).""" | ||
| ds_out2d, ds_zcoords, _ = calc_inputs | ||
| face_z = _calculate_face_z(ds_zcoords, ds_out2d) | ||
| assert face_z.dims == ("n_face", "time", "n_layer") | ||
| assert face_z.sizes == {"n_face": N_FACES, | ||
| "time": N_TIME, | ||
| "n_layer": N_LAYERS} | ||
|
|
||
|
|
||
| def test_calculate_face_z_tri_cell(calc_inputs): | ||
| """face_z for a tri cell equals the mean of exactly its 3 node z-values.""" | ||
| ds_out2d, ds_zcoords, ds_zcoords_raw = calc_inputs | ||
| face_z = _calculate_face_z(ds_zcoords, ds_out2d) | ||
| z_raw = ds_zcoords_raw.zCoordinates.values # (time, n_node, n_layer) | ||
| expected = float(z_raw[0, TRI_NODES, -1].mean()) | ||
| assert float(face_z.values[TRI_IDX, 0, -1]) == pytest.approx(expected, | ||
| rel=1e-5) | ||
|
|
||
|
|
||
| def test_calculate_face_z_tri_ignores_fill_node(calc_inputs): | ||
| """Guard that the fill node is NOT included in the tri-cell average. | ||
|
|
||
| If node index -1 (wrapping to the last node) were naively included, | ||
| the result would match a 4-node mean instead of the correct 3-node mean. | ||
| """ | ||
| ds_out2d, ds_zcoords, ds_zcoords_raw = calc_inputs | ||
| face_z = _calculate_face_z(ds_zcoords, ds_out2d) | ||
| z_raw = ds_zcoords_raw.zCoordinates.values | ||
| # Wrong result if fill wraps to node[-1] = node[N_NODES - 1] | ||
| wrong_mean = float(z_raw[0, TRI_NODES + [N_NODES - 1], -1].mean()) | ||
| assert float(face_z.values[TRI_IDX, 0, -1]) != pytest.approx(wrong_mean, | ||
| rel=1e-5) | ||
|
|
||
|
|
||
| def test_calculate_face_z_quad_cell(calc_inputs): | ||
| """face_z for a quad cell equals the mean of all 4 node z-values.""" | ||
| ds_out2d, ds_zcoords, ds_zcoords_raw = calc_inputs | ||
| face_z = _calculate_face_z(ds_zcoords, ds_out2d) | ||
| z_raw = ds_zcoords_raw.zCoordinates.values | ||
| expected = float(z_raw[0, QUAD_NODES, -1].mean()) | ||
| assert float(face_z.values[QUAD_IDX, 0, -1]) == pytest.approx(expected, | ||
| rel=1e-5) | ||
|
|
||
|
|
||
| def test_calculate_face_z_tri_and_quad_differ(calc_inputs): | ||
| """ | ||
| Tri and quad cells produce distinct z-values, | ||
| confirming correct node counts. | ||
| """ | ||
| ds_out2d, ds_zcoords, _ = calc_inputs | ||
| face_z = _calculate_face_z(ds_zcoords, ds_out2d) | ||
| assert float(face_z.values[TRI_IDX, 0, -1]) != pytest.approx( | ||
| float(face_z.values[QUAD_IDX, 0, -1]), rel=1e-5 | ||
| ) | ||
|
|
||
|
|
||
| # _calculate_edge_z | ||
|
|
||
| def test_calculate_edge_z_output_shape(calc_inputs): | ||
| """_calculate_edge_z returns (n_edge, time, n_layer).""" | ||
| ds_out2d, ds_zcoords, _ = calc_inputs | ||
| edge_z = _calculate_edge_z(ds_zcoords, ds_out2d) | ||
| assert edge_z.dims == ("n_edge", "time", "n_layer") | ||
| assert edge_z.sizes == {"n_edge": N_EDGES, | ||
| "time": N_TIME, | ||
| "n_layer": N_LAYERS} | ||
|
|
||
|
|
||
| def test_calculate_edge_z_values(calc_inputs): | ||
| """edge_z equals the two-node mean at every time step.""" | ||
| ds_out2d, ds_zcoords, ds_zcoords_raw = calc_inputs | ||
| edge_z = _calculate_edge_z(ds_zcoords, ds_out2d) | ||
| z_raw = ds_zcoords_raw.zCoordinates.values # (time, n_node, n_layer) | ||
| expected = z_raw[:, EDGE_NODES, -1].mean(axis=1) # (time,) | ||
| np.testing.assert_allclose(edge_z.values[EDGE_IDX, :, -1], | ||
| expected, rtol=1e-5) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These values are 1D static coordinates (from the first timestep in bottom layer). They should be light enough to call eagerly. But as we're moving towards lazy loading, dropping .values brings consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit that removes the
.valuescalls —assign_coordsnow receives the DataArray slices directly, keeping the z-coordinate calculations lazy.