Skip to content

Commit f781eeb

Browse files
committed
Modified radius setting so default is EARTH_RADIUS (specified in mesh.py and imported wherever else needed). Radius = None leads to a crash in flat mesh cases, so still kept an if/else to set deg2m = 1.0 in xgrid/uxgrid if no radius is specified, i.e. if SphericalMesh or spherical are not set.
1 parent ce76ee3 commit f781eeb

4 files changed

Lines changed: 47 additions & 35 deletions

File tree

src/parcels/_core/mesh.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
11
import numpy as np
22

3+
EARTH_RADIUS = 6366707.019493707
4+
35

46
class SphericalMesh:
57
"""Spherical mesh object with configurable planetary radius.
68
79
Pass to FieldSet object as ``mesh=SphericalMesh(radius=...)``.
8-
radius is in meters; None reverts to default for Earth, where
9-
arcdegree to meter conversion is defined as 1852 * 60
10-
(1852 meters per arcminute * 60 arcminutes per arcdegree).
10+
radius is in meters; defaults to Earth radius.
1111
"""
1212

13-
def __init__(self, radius: float | None = None):
14-
if radius is not None and not isinstance(radius, (int, float, np.number)):
15-
raise TypeError(f"radius must be a number or None, got {type(radius).__name__}")
16-
if radius is not None and radius <= 0:
13+
def __init__(self, radius: float = EARTH_RADIUS):
14+
if not isinstance(radius, (int, float, np.number)):
15+
raise TypeError(f"radius must be a number, got {type(radius).__name__}")
16+
if radius <= 0:
1717
raise ValueError(f"radius must be positive, got {radius}")
1818
self.radius = radius
1919

2020
@property
2121
def deg2m(self) -> float:
2222
"""Meters per degree of arc."""
23-
if self.radius is None:
24-
return 1852 * 60.0
25-
else:
26-
return self.radius * np.pi / 180.0
23+
return self.radius * np.pi / 180.0
2724

2825
def __repr__(self) -> str:
2926
return f"SphericalMesh(radius={self.radius})"

src/parcels/_core/uxgrid.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from parcels._core.basegrid import BaseGrid
99
from parcels._core.index_search import GRID_SEARCH_ERROR, _search_1d_array, uxgrid_point_in_cell
10-
from parcels._core.mesh import SphericalMesh
10+
from parcels._core.mesh import SphericalMesh, EARTH_RADIUS
1111
from parcels._typing import assert_valid_mesh
1212

1313
_UXGRID_AXES = Literal["Z", "FACE"]
@@ -47,7 +47,7 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh) -> None:
4747
self._radius = mesh.radius
4848
else:
4949
self._mesh = mesh
50-
self._radius = None
50+
self._radius = EARTH_RADIUS if mesh == "spherical" else None
5151
self._spatialhash = None
5252

5353
assert_valid_mesh(mesh)
@@ -82,10 +82,9 @@ def get_axis_dim(self, axis: _UXGRID_AXES) -> int:
8282
@property
8383
def deg2m(self) -> float:
8484
"""Metres per arcdegree for this grid's mesh."""
85-
if self._radius is None:
86-
return 1852 * 60.0
87-
else:
88-
return self._radius * np.pi / 180.0
85+
if self._radius is None: # flat mesh; None causes crash in advection
86+
return 1.0
87+
return self._radius * np.pi / 180.0
8988

9089
def search(self, z, y, x, ei=None, tol=1e-6):
9190
"""

src/parcels/_core/xgrid.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import parcels._typing as ptyping
1313
from parcels._core.basegrid import BaseGrid
1414
from parcels._core.index_search import _search_1d_array, _search_indices_curvilinear_2d
15-
from parcels._core.mesh import SphericalMesh
15+
from parcels._core.mesh import SphericalMesh, EARTH_RADIUS
1616
from parcels._sgrid.accessor import _get_dim_to_axis_mapping
1717
from parcels._sgrid.core import SGRID_PADDING_TO_XGCM_POSITION
1818

@@ -175,7 +175,7 @@ def __init__(self, model_data: xr.Dataset, mesh):
175175
self._radius = mesh.radius
176176
else:
177177
self._mesh = mesh
178-
self._radius = None
178+
self._radius = EARTH_RADIUS if mesh == "spherical" else None
179179
self._spatialhash = None
180180
ds = model_data
181181

@@ -258,10 +258,9 @@ def time(self):
258258
@property
259259
def deg2m(self) -> float:
260260
"""Metres per degree of arc for this grid's mesh."""
261-
if self._radius is None:
262-
return 1852 * 60.0
263-
else:
264-
return self._radius * np.pi / 180.0
261+
if self._radius is None: # flat mesh; None causes crash in advection
262+
return 1.0
263+
return self._radius * np.pi / 180.0
265264

266265
@cached_property
267266
def xdim(self) -> int:

tests/test_mesh.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@
22
import pytest
33

44
from parcels import FieldSet, ParticleSet, SphericalMesh
5+
from parcels._core.mesh import EARTH_RADIUS
56
from parcels._datasets.structured.generated import simple_UV_dataset
67
from parcels.kernels import AdvectionRK4
78

8-
EARTH_DEG2M = 1852 * 60.0
9-
9+
EARTH_DEG2M = EARTH_RADIUS * np.pi / 180
1010

1111
def test_spherical_mesh_deg2m():
12-
assert SphericalMesh().radius is None
12+
assert SphericalMesh().radius == EARTH_RADIUS
1313
assert SphericalMesh().deg2m == EARTH_DEG2M
1414
r = 3389500.0 # Mars radius
1515
assert SphericalMesh(radius=r).deg2m == pytest.approx(r * np.pi / 180)
1616

17-
1817
@pytest.mark.parametrize(
1918
"mesh, exp_radius, exp_deg2m",
2019
[
21-
("spherical", None, EARTH_DEG2M),
22-
(SphericalMesh(), None, EARTH_DEG2M),
20+
("spherical", EARTH_RADIUS, EARTH_DEG2M),
21+
(SphericalMesh(), EARTH_RADIUS, EARTH_DEG2M),
2322
(SphericalMesh(radius=3389500.0), 3389500.0, 3389500.0 * np.pi / 180),
2423
],
2524
)
@@ -29,31 +28,49 @@ def test_xgrid_radius_and_deg2m(mesh, exp_radius, exp_deg2m):
2928
assert grid._radius == exp_radius
3029
assert grid.deg2m == pytest.approx(exp_deg2m)
3130

32-
33-
@pytest.mark.parametrize("radius", [None, 3389500.0, 6051800.0, 6371000.0]) # Mars, Venus, Earth
34-
def test_advection_uses_custom_radius(radius, npart=10):
31+
@pytest.mark.parametrize("mesh, deg2m",
32+
[
33+
(SphericalMesh(), EARTH_DEG2M), # No radius entered
34+
(SphericalMesh(radius=3389500.0), 3389500.0 * np.pi / 180), # Mars
35+
(SphericalMesh(radius=6051800.0), 6051800.0 * np.pi / 180), # Venus
36+
(SphericalMesh(radius=EARTH_RADIUS), EARTH_DEG2M), # Explicit Earth
37+
],
38+
)
39+
def test_advection_uses_custom_radius(mesh, deg2m, npart=10):
3540
ds = simple_UV_dataset()
3641
ds["U"].data[:] = 1.0
37-
fieldset = FieldSet.from_sgrid_conventions(ds, mesh=SphericalMesh(radius=radius))
42+
fieldset = FieldSet.from_sgrid_conventions(ds, mesh=mesh)
3843

3944
runtime = 7200
4045
startlat = np.linspace(0, 80, npart)
4146
startlon = 20.0 + np.zeros(npart)
4247
pset = ParticleSet(fieldset, x=startlon, y=startlat)
4348
pset.execute(AdvectionRK4, runtime=runtime, dt=np.timedelta64(15, "m"))
4449

45-
deg2m = EARTH_DEG2M if radius is None else radius * np.pi / 180
4650
expected_dlon = runtime / (deg2m * np.cos(np.deg2rad(pset.y)))
4751
np.testing.assert_allclose(pset.x - startlon, expected_dlon, atol=1e-5)
4852
np.testing.assert_allclose(pset.y, startlat, atol=1e-5)
4953

54+
def test_advection_flat_mesh(npart=10):
55+
ds = simple_UV_dataset(mesh="flat")
56+
ds["U"].data[:] = 1.0
57+
fieldset = FieldSet.from_sgrid_conventions(ds, mesh="flat")
58+
59+
runtime = 7200
60+
startlat = np.linspace(0, 80, npart)
61+
startlon = 20.0 + np.zeros(npart)
62+
pset = ParticleSet(fieldset, x=startlon, y=startlat)
63+
pset.execute(AdvectionRK4, runtime=runtime, dt=np.timedelta64(15, "m"))
64+
65+
assert fieldset.U.grid.deg2m == 1.0 # flat mesh deg2m
66+
np.testing.assert_allclose(pset.x - startlon, runtime, atol=1e-5)
67+
np.testing.assert_allclose(pset.y, startlat, atol=1e-5)
5068

5169
@pytest.mark.parametrize("bad_radius", ["6371000", [6371000], (1, 2), {}])
5270
def test_spherical_mesh_rejects_non_numeric_radius(bad_radius):
5371
with pytest.raises(TypeError):
5472
SphericalMesh(radius=bad_radius)
5573

56-
5774
@pytest.mark.parametrize("bad_radius", [0, -1.0, -6371000])
5875
def test_spherical_mesh_rejects_nonpos_radius(bad_radius):
5976
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)