Skip to content

Commit

Permalink
Prevent frontogenesis from returning nans
Browse files Browse the repository at this point in the history
Fixes #3768 by making sure the argument to the arcsin function is
valid.

Previously, frontogenesis could return nans when there was a constant
theta field (division by zero would occur) or if round-off error
resulted in the argument to arcsin being slightly outside the valid
domain of the function (-1 to 1).  In this commit, edits are made to
set points to zero where nans occur due to division by zero (the
frontogenesis is zero when the magnitude of the theta gradient is zero
anyway) and to use np.clip to ensure the argument to arcsin is valid.

I could not come up with a simplified test case that triggers the
round-off error issue with arcsin, but I do include a test case for
the constant theta situation.  Because the test case results in a
division by zero by design, it is currently failing since that
triggers a RuntimeWarning.
  • Loading branch information
sgdecker committed Nov 19, 2024
1 parent ab1b956 commit c9d8dc5
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 1 deletion.
13 changes: 12 additions & 1 deletion src/metpy/calc/kinematics.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,18 @@ def frontogenesis(potential_temperature, u, v, dx=None, dy=None, x_dim=-1, y_dim

# Compute the angle (beta) between the wind field and the gradient of potential temperature
psi = 0.5 * np.arctan2(shrd, strd)
beta = np.arcsin((-ddx_theta * np.cos(psi) - ddy_theta * np.sin(psi)) / mag_theta)
arg = (-ddx_theta * np.cos(psi) - ddy_theta * np.sin(psi)) / mag_theta

# A few problems may occur when calculating the argument to the arcsin function.
# First, we may have divided by zero, since a constant theta field would mean
# mag_theta is zero. To counter this, we set the argument to zero in this case.
# Second, due to round-off error, the argument may be slightly outside the domain
# of arcsin. To counter this, we use np.clip to force the argument to be an
# acceptable value. With these adjustments, we can make sure beta doesn't end up
# with nans somewhere.
arg[mag_theta==0] = 0
arg = np.clip(arg, -1, 1)
beta = np.arcsin(arg)

return 0.5 * mag_theta * (tdef * np.cos(2 * beta) - div)

Expand Down
47 changes: 47 additions & 0 deletions tests/calc/test_kinematics.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,53 @@ def test_frontogenesis_asym():
assert_almost_equal(fronto, true_fronto)


def test_frontogenesis_nan():
"""Test that frontogenesis calculation does not result in nan."""
x = np.array([-4142340.8, -4061069.8, -3979798.8, -3898527.8, -3817256.8],
dtype=np.float32)
y = np.array([-832207.56, -750936.56, -669665.56, -588394.5, -507123.56],
dtype=np.float32)
lat = np.array([[12.38805122, 12.58268367, 12.77387305, 12.96159447, 13.14582354],
[13.07545967, 13.27159197, 13.46425116, 13.65341235, 13.83905111],
[13.76423766, 13.96186003, 14.15597929, 14.34657051, 14.53360928],
[14.45429168, 14.65339375, 14.84896275, 15.04097373, 15.22940228],
[15.14552468, 15.3460955 , 15.54310332, 15.73652321, 15.92633074]])
lon = np.array([[-132.75696788, -132.05286812, -131.34671228, -130.63852983, -129.92835084],

Check failure on line 321 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:321:96: E501 Line too long (96 > 95)
[-132.9590417 , -132.25156385, -131.54199837, -130.83037505, -130.11672431],

Check failure on line 322 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:322:96: E501 Line too long (96 > 95)
[-133.16323241, -132.45234731, -131.73934239, -131.02424779, -130.30709426],

Check failure on line 323 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:323:96: E501 Line too long (96 > 95)
[-133.36957268, -132.65525085, -131.93877637, -131.22017972, -130.49949199],

Check failure on line 324 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:324:96: E501 Line too long (96 > 95)
[-133.57809517, -132.86030681, -132.14033233, -131.41820252, -130.69394884]])

Check failure on line 325 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:325:96: E501 Line too long (97 > 95)
uvals = np.array([[ 0.165024, 0.055023, -0.454977, -1.534977, -2.744976],
[ 0.155024, -0.434977, -2.114977, -3.474977, -4.034977],
[-1.554977, -2.714977, -2.084976, -5.274977, -3.334976],
[-3.424976, -7.644977, -7.654977, -5.384976, -3.224977],
[-9.564977, -9.934977, -7.454977, -6.004977, -4.144977]]) * units('m/s')
vvals = np.array([[ 2.6594982, 1.9194984, 2.979498, 2.149498, 2.6394978],
[ 3.4994984, 4.0794983, 4.8494987, 5.2594986, 3.1694984],
[ 5.159498, 6.4594975, 6.559498, 5.9694977, 3.189499],
[ 6.5994987, 9.799498, 7.4594975, 4.2894993, 3.729498],
[11.3394985, 6.779499, 4.0994987, 4.819498, 4.9994984]]) * units('m/s')

Check failure on line 335 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E501)

tests/calc/test_kinematics.py:335:96: E501 Line too long (99 > 95)
Tvals = np.array([[290.2, 290.1, 290.2, 290.30002, 290.30002],

Check failure on line 336 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (N806)

tests/calc/test_kinematics.py:336:5: N806 Variable `Tvals` in function should be lowercase
[290.5, 290.30002, 290.30002, 290.30002, 290.2],
[290.80002, 290.40002, 290.2, 290.40002, 289.90002],
[290.7, 290.90002, 290.7, 290.1, 289.7],
[290.90002, 290.40002, 289.7, 289.7, 289.30002]]) * units('degK')

x = xr.DataArray(data=x, coords={'x': x}, dims='x', attrs={'units': 'meters'})
y = xr.DataArray(data=y, coords={'y': y}, dims='y', attrs={'units': 'meters'})

dims = ['y', 'x']
coords = {'x': x, 'y': y, 'latitude': (dims, lat), 'longitude': (dims, lon)}

u = xr.DataArray(data=uvals, coords=coords, dims=dims)
v = xr.DataArray(data=vvals, coords=coords, dims=dims)
T = xr.DataArray(data=Tvals, coords=coords, dims=dims)

Check failure on line 350 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (N806)

tests/calc/test_kinematics.py:350:5: N806 Variable `T` in function should be lowercase

th = potential_temperature(850*units('hPa'), T)

Check failure on line 352 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (E226)

tests/calc/test_kinematics.py:352:35: E226 Missing whitespace around arithmetic operator
F = frontogenesis(th, u, v)

Check failure on line 353 in tests/calc/test_kinematics.py

View workflow job for this annotation

GitHub Actions / Run Lint Tools

Ruff (N806)

tests/calc/test_kinematics.py:353:5: N806 Variable `F` in function should be lowercase
assert not np.any(np.isnan(F))


def test_advection_uniform():
"""Test advection calculation for a uniform 1D field."""
u = np.ones((3,)) * units('m/s')
Expand Down

0 comments on commit c9d8dc5

Please sign in to comment.