-
Notifications
You must be signed in to change notification settings - Fork 182
Add support for defining planetary radius #2739
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
Changes from 12 commits
0c4fa45
3ddff60
ffa6d85
f566fe1
69fa293
6ec7382
b10e32c
493ef8f
08fec2c
a277913
da32a50
72be313
0c3c1f0
ddac215
d28fa4c
ce76ee3
f781eeb
9ac08fc
93a364b
d466ccd
066aeb1
27e6456
a50c0ff
a884bf8
c82a69e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import numpy as np | ||
|
|
||
|
|
||
| class SphericalMesh: | ||
| """Spherical mesh object with configurable planetary radius. | ||
|
|
||
| Pass to FieldSet object as ``mesh=SphericalMesh(radius=...)``. | ||
| radius is in meters; None reverts degree to meter conversion | ||
| to 1852 * 60 . | ||
| """ | ||
|
|
||
| def __init__(self, radius: float | None = None): | ||
| if radius is not None and not isinstance(radius, (int, float, np.number)): | ||
| raise TypeError(f"radius must be a number of None, got {type(radius).__name__}") | ||
|
maureenjcohen marked this conversation as resolved.
Outdated
|
||
| if radius is not None and radius <= 0: | ||
| raise ValueError(f"radius must be positive, got {radius}") | ||
| self.radius = radius | ||
|
|
||
| @property | ||
| def deg2m(self) -> float: | ||
| """Meters per degree of arc.""" | ||
| if self.radius is None: | ||
| return 1852 * 60.0 | ||
| else: | ||
| return self.radius * np.pi / 180.0 | ||
|
|
||
| def __repr__(self) -> str: | ||
| return f"SphericalMesh(radius={self.radius})" | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||
|
|
||||
| from parcels._core.basegrid import BaseGrid | ||||
| from parcels._core.index_search import GRID_SEARCH_ERROR, _search_1d_array, uxgrid_point_in_cell | ||||
| from parcels._core.mesh import SphericalMesh | ||||
| from parcels._typing import assert_valid_mesh | ||||
|
|
||||
| _UXGRID_AXES = Literal["Z", "FACE"] | ||||
|
|
@@ -41,7 +42,12 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh) -> None: | |||
| if z.ndim != 1: | ||||
| raise ValueError("z must be a 1D array of vertical coordinates") | ||||
| self.z = z | ||||
| self._mesh = mesh | ||||
| if isinstance(mesh, SphericalMesh): | ||||
| self._mesh = "spherical" | ||||
| self._radius = mesh.radius | ||||
| else: | ||||
| self._mesh = mesh | ||||
| self._radius = None | ||||
| self._spatialhash = None | ||||
|
|
||||
| assert_valid_mesh(mesh) | ||||
|
|
@@ -73,6 +79,14 @@ def get_axis_dim(self, axis: _UXGRID_AXES) -> int: | |||
| elif axis == "FACE": | ||||
| return self.uxgrid.n_face | ||||
|
|
||||
| @property | ||||
| def deg2m(self) -> float: | ||||
| """Metres per degree of arc for this grid's mesh.""" | ||||
|
maureenjcohen marked this conversation as resolved.
Outdated
|
||||
| if self._radius is None: | ||||
| return 1852 * 60.0 | ||||
| else: | ||||
| return self._radius * np.pi / 180.0 | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may be a bit overcomplicated. Why not simply use Earth radius as default (
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An advantage would then also be that a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure if the exact 1852 * 60 was needed, or if using Earth radius might lead to small inconsistencies when switching between v3 and v4. I think your suggestion is good.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if the user doesn't specify a radius, should radius default to None (flat mesh) or to Earth's? Most people are working on Earth and it might be more convenient for users not to have to put in a radius every time.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep the default as it is now (spherical Earth), but the idea is that the correct mesh is discovered when creating the FieldSet, see below Parcels/src/parcels/_core/model.py Line 320 in a6bb153
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated SphericalMesh to have Earth's radius as default. However, I was getting crashes from the advection tests for flat mesh cases because the advection still calls grid.deg2m, which has radius = None for flat meshes. I kept an if/else for xgrid and uxgrid which sets deg2m = 1.0 if there's no radius (i.e. the mesh is flat). |
||||
|
|
||||
| def search(self, z, y, x, ei=None, tol=1e-6): | ||||
| """ | ||||
| Search for the grid cell (face) and vertical layer that contains the given points. | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| import parcels._typing as ptyping | ||
| from parcels._core.basegrid import BaseGrid | ||
| from parcels._core.index_search import _search_1d_array, _search_indices_curvilinear_2d | ||
| from parcels._core.mesh import SphericalMesh | ||
| from parcels._sgrid.accessor import _get_dim_to_axis_mapping | ||
| from parcels._sgrid.core import SGRID_PADDING_TO_XGCM_POSITION | ||
|
|
||
|
|
@@ -169,7 +170,12 @@ def __init__(self, model_data: xr.Dataset, mesh): | |
| self._ds = model_data | ||
| grid = XgcmLikeGrid(self.sgrid_metadata, model_data) | ||
| self.xgcm_grid = grid | ||
| self._mesh = mesh | ||
| if isinstance(mesh, SphericalMesh): | ||
| self._mesh = "spherical" | ||
| self._radius = mesh.radius | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR looks good to me! The only question I have for @VeckoTheGecko is if this combination of |
||
| else: | ||
| self._mesh = mesh | ||
| self._radius = None | ||
| self._spatialhash = None | ||
| ds = model_data | ||
|
|
||
|
|
@@ -249,6 +255,14 @@ def _datetimes(self): | |
| def time(self): | ||
| return self._datetimes.astype(np.float64) / 1e9 | ||
|
|
||
| @property | ||
| def deg2m(self) -> float: | ||
| """Metres per degree of arc for this grid's mesh.""" | ||
| if self._radius is None: | ||
| return 1852 * 60.0 | ||
| else: | ||
| return self._radius * np.pi / 180.0 | ||
|
|
||
| @cached_property | ||
| def xdim(self) -> int: | ||
| return self.get_axis_dim("X") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ | |
| import numpy as np | ||
| from cftime import datetime as cftime_datetime | ||
|
|
||
| from parcels._core.mesh import SphericalMesh | ||
|
|
||
| if TYPE_CHECKING: | ||
| import xgcm | ||
|
|
||
|
|
@@ -73,4 +75,6 @@ def _validate_against_pure_literal(value, typing_literal): | |
|
|
||
| # Assertion functions to clean user input | ||
| def assert_valid_mesh(value: Any): | ||
| if isinstance(value, SphericalMesh): | ||
| return | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @VeckoTheGecko, can you check if this is correct behaviour for typing? |
||
| _validate_against_pure_literal(value, Mesh) | ||
Uh oh!
There was an error while loading. Please reload this page.