Skip to content

Commit 807f740

Browse files
authored
Merge pull request #808 from davidhassell/curl
Fix incorrect result units for some differential operators
2 parents 5645730 + bdfcd88 commit 807f740

File tree

5 files changed

+112
-33
lines changed

5 files changed

+112
-33
lines changed

Changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ version NEXTVERSION
77
(https://github.com/NCAS-CMS/cf-python/issues/784)
88
* Include the UM version as a field property when reading UM files
99
(https://github.com/NCAS-CMS/cf-python/issues/777)
10+
* New keyword parameter to `cf.Field.derivative`:
11+
``ignore_coordinate_units``
12+
(https://github.com/NCAS-CMS/cf-python/issues/807)
13+
* Fix bug that sometimes puts an incorrect ``radian-1`` or
14+
``radian-2`` in the returned units of the differential operator
15+
methods and functions
16+
(https://github.com/NCAS-CMS/cf-python/issues/807)
1017
* Fix bug where `cf.example_fields` returned a `list` of Fields rather
1118
than a `Fieldlist`
1219
(https://github.com/NCAS-CMS/cf-python/issues/725)

cf/field.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2982,7 +2982,7 @@ def laplacian_xy(
29822982
[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]]
29832983
>>> lp = f.laplacian_xy(radius='earth')
29842984
>>> lp
2985-
<CF Field: long_name=X-Y Laplacian of specific_humidity(latitude(5), longitude(8)) m-2.rad-2>
2985+
<CF Field: long_name=X-Y Laplacian of specific_humidity(latitude(5), longitude(8)) m-2>
29862986
>>> print(lp.array)
29872987
[[-- -- -- -- -- -- -- --]
29882988
[-- -- -- -- -- -- -- --]
@@ -3050,19 +3050,31 @@ def laplacian_xy(
30503050
r2_sin_theta = sin_theta * r**2
30513051

30523052
d2f_dphi2 = f.derivative(
3053-
x_key, wrap=x_wrap, one_sided_at_boundary=one_sided_at_boundary
3053+
x_key,
3054+
wrap=x_wrap,
3055+
one_sided_at_boundary=one_sided_at_boundary,
3056+
ignore_coordinate_units=True,
30543057
).derivative(
3055-
x_key, wrap=x_wrap, one_sided_at_boundary=one_sided_at_boundary
3058+
x_key,
3059+
wrap=x_wrap,
3060+
one_sided_at_boundary=one_sided_at_boundary,
3061+
ignore_coordinate_units=True,
30563062
)
30573063

30583064
term1 = d2f_dphi2 / (r2_sin_theta * sin_theta)
30593065

30603066
df_dtheta = f.derivative(
3061-
y_key, wrap=None, one_sided_at_boundary=one_sided_at_boundary
3067+
y_key,
3068+
wrap=None,
3069+
one_sided_at_boundary=one_sided_at_boundary,
3070+
ignore_coordinate_units=True,
30623071
)
30633072

30643073
term2 = (df_dtheta * sin_theta).derivative(
3065-
y_key, wrap=None, one_sided_at_boundary=one_sided_at_boundary
3074+
y_key,
3075+
wrap=None,
3076+
one_sided_at_boundary=one_sided_at_boundary,
3077+
ignore_coordinate_units=True,
30663078
) / r2_sin_theta
30673079

30683080
f = term1 + term2
@@ -11607,8 +11619,8 @@ def grad_xy(self, x_wrap=None, one_sided_at_boundary=False, radius=None):
1160711619
[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]]
1160811620
>>> fx, fy = f.grad_xy(radius='earth')
1160911621
>>> fx, fy
11610-
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>,
11611-
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>)
11622+
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1>,
11623+
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1>)
1161211624
>>> print(fx.array)
1161311625
[[0. 0. 0. 0. 0. 0. 0. 0.]
1161411626
[0. 0. 0. 0. 0. 0. 0. 0.]
@@ -11679,14 +11691,18 @@ def grad_xy(self, x_wrap=None, one_sided_at_boundary=False, radius=None):
1167911691
r = f.radius(default=radius)
1168011692

1168111693
X = f.derivative(
11682-
x_key, wrap=x_wrap, one_sided_at_boundary=one_sided_at_boundary
11694+
x_key,
11695+
wrap=x_wrap,
11696+
one_sided_at_boundary=one_sided_at_boundary,
11697+
ignore_coordinate_units=True,
1168311698
) / (theta.sin() * r)
1168411699

1168511700
Y = (
1168611701
f.derivative(
1168711702
y_key,
1168811703
wrap=None,
1168911704
one_sided_at_boundary=one_sided_at_boundary,
11705+
ignore_coordinate_units=True,
1169011706
)
1169111707
/ r
1169211708
)
@@ -14226,9 +14242,10 @@ def derivative(
1422614242
axis,
1422714243
wrap=None,
1422814244
one_sided_at_boundary=False,
14245+
cyclic=None,
14246+
ignore_coordinate_units=False,
1422914247
inplace=False,
1423014248
i=False,
14231-
cyclic=None,
1423214249
):
1423314250
"""Calculate the derivative along the specified axis.
1423414251

@@ -14262,6 +14279,28 @@ def derivative(
1426214279
at the non-cyclic boundaries. By default missing
1426314280
values are set at non-cyclic boundaries.
1426414281

14282+
ignore_coordinate_units: `bool`, optional
14283+
If True then the coordinates providing the cell
14284+
spacings along the specified axis are assumed to be
14285+
dimensionless, even if they do in fact have
14286+
units. This does not change the magnitude of the
14287+
returned numerical values, but the units of the
14288+
returned field construct will be identical to the
14289+
original units.
14290+
14291+
If False (the default) then the coordinate units will
14292+
propagate through to the result. i.e. the units of the
14293+
returned field construct will be the original units
14294+
divided by the coordinate units.
14295+
14296+
For example, for a field construct with units of
14297+
``m.s-1`` and X coordinate units of ``radians``, the
14298+
units of the X derivative will be ``m.s-1.radians-1``
14299+
by default, or ``m.s-1`` if *ignore_coordinate_units*
14300+
is True.
14301+
14302+
.. versionadded:: NEXTVERSION
14303+
1426514304
{{inplace: `bool`, optional}}
1426614305

1426714306
{{i: deprecated at version 3.0.0}}
@@ -14393,6 +14432,11 @@ def derivative(
1439314432
for _ in range(self.ndim - 1 - axis_index):
1439414433
d.insert_dimension(position=1, inplace=True)
1439514434

14435+
if ignore_coordinate_units:
14436+
# Remove the coordinate units from the coordinate
14437+
# differences, before we calculate the derivative.
14438+
d.override_units(None, inplace=True)
14439+
1439614440
# Find the derivative
1439714441
f.data /= d
1439814442

cf/maths.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,11 @@ def curl_xy(fx, fy, x_wrap=None, one_sided_at_boundary=False, radius=None):
372372
[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]]
373373
>>> fx, fy = f.grad_xy(radius='earth', one_sided_at_boundary=True)
374374
>>> fx, fy
375-
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>,
376-
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>)
375+
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1>,
376+
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1>)
377377
>>> c = cf.curl_xy(fx, fy, radius='earth')
378378
>>> c
379-
<CF Field: long_name=Divergence of (long_name=X gradient of specific_humidity, long_name=Y gradient of specific_humidity)(latitude(5), longitude(8)) m-2.rad-2>
379+
<CF Field: long_name=Horizontal curl of (long_name=X gradient of specific_humidity, long_name=Y gradient of specific_humidity)(latitude(5), longitude(8)) m-2>
380380
>>> print(c.array)
381381
[[-- -- -- -- -- -- -- --]
382382
[0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0]
@@ -437,8 +437,7 @@ def curl_xy(fx, fy, x_wrap=None, one_sided_at_boundary=False, radius=None):
437437
# --------------------------------------------------------
438438
# Spherical polar coordinates
439439
# --------------------------------------------------------
440-
# Convert latitude and longitude units to radians, so that the
441-
# units of the result are nice.
440+
# Convert latitude and longitude units to radians
442441
radians = Units("radians")
443442
fx_x_coord.Units = radians
444443
fx_y_coord.Units = radians
@@ -461,10 +460,16 @@ def curl_xy(fx, fy, x_wrap=None, one_sided_at_boundary=False, radius=None):
461460
r = fx.radius(default=radius)
462461

463462
term1 = (fx * sin_theta).derivative(
464-
fx_y_key, wrap=None, one_sided_at_boundary=one_sided_at_boundary
463+
fx_y_key,
464+
wrap=None,
465+
one_sided_at_boundary=one_sided_at_boundary,
466+
ignore_coordinate_units=True,
465467
)
466468
term2 = fy.derivative(
467-
fy_x_key, wrap=x_wrap, one_sided_at_boundary=one_sided_at_boundary
469+
fy_x_key,
470+
wrap=x_wrap,
471+
one_sided_at_boundary=one_sided_at_boundary,
472+
ignore_coordinate_units=True,
468473
)
469474

470475
c = (term1 - term2) / (sin_theta * r)
@@ -597,11 +602,12 @@ def div_xy(fx, fy, x_wrap=None, one_sided_at_boundary=False, radius=None):
597602
[0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]]
598603
>>> fx, fy = f.grad_xy(radius='earth', one_sided_at_boundary=True)
599604
>>> fx, fy
600-
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>,
601-
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1.rad-1>)
605+
(<CF Field: long_name=X gradient of specific_humidity(latitude(5), longitude(8)) m-1>,
606+
<CF Field: long_name=Y gradient of specific_humidity(latitude(5), longitude(8)) m-1>)
602607
>>> d = cf.div_xy(fx, fy, radius='earth')
603608
>>> d
604-
<CF Field: long_name=Divergence of (long_name=X gradient of specific_humidity, long_name=Y gradient of specific_humidity)(latitude(5), longitude(8)) m-2.rad-2>
609+
<CF Field: long_name=Horizontal divergence of (long_name=X gradient of specific_humidity, long_name=Y gradient of specific_humidity)(latitude(5), longitude(8)) m-2>
610+
605611
>>> print(d.array)
606612
[[-- -- -- -- -- -- -- --]
607613
[0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0]
@@ -686,11 +692,17 @@ def div_xy(fx, fy, x_wrap=None, one_sided_at_boundary=False, radius=None):
686692
r = fx.radius(default=radius)
687693

688694
term1 = fx.derivative(
689-
fx_x_key, wrap=x_wrap, one_sided_at_boundary=one_sided_at_boundary
695+
fx_x_key,
696+
wrap=x_wrap,
697+
one_sided_at_boundary=one_sided_at_boundary,
698+
ignore_coordinate_units=True,
690699
)
691700

692701
term2 = (fy * sin_theta).derivative(
693-
fy_y_key, wrap=None, one_sided_at_boundary=one_sided_at_boundary
702+
fy_y_key,
703+
wrap=None,
704+
one_sided_at_boundary=one_sided_at_boundary,
705+
ignore_coordinate_units=True,
694706
)
695707

696708
d = (term1 + term2) / (sin_theta * r)

cf/test/test_Field.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,17 +2018,23 @@ def test_Field_moving_window(self):
20182018
def test_Field_derivative(self):
20192019
f = cf.example_field(0)
20202020
f[...] = np.arange(9)[1:] * 45
2021+
x = f.dimension_coordinate("X")
2022+
2023+
# Ignore coordinate units
2024+
d = f.derivative("X", ignore_coordinate_units=True)
2025+
self.assertEqual(d.Units, f.Units)
20212026

20222027
# Check a cyclic periodic axis
20232028
d = f.derivative("X")
2029+
self.assertEqual(d.Units, f.Units / x.Units)
20242030
self.assertTrue(np.allclose(d[:, 1:-1].array, 1))
20252031
self.assertTrue(np.allclose(d[:, [0, -1]].array, -3))
20262032

20272033
# The reversed field should contain the same gradients in this
20282034
# case
20292035
f1 = f[:, ::-1]
20302036
d1 = f1.derivative("X")
2031-
self.assertTrue(d1.data.equals(d.data))
2037+
self.assertTrue(d1.data.equals(d.data, verbose=-1))
20322038

20332039
# Check non-cyclic
20342040
d = f.derivative("X", wrap=False)
@@ -2490,12 +2496,21 @@ def test_Field_grad_xy(self):
24902496
radius=radius, x_wrap=wrap, one_sided_at_boundary=one_sided
24912497
)
24922498

2493-
self.assertTrue(x.Units == y.Units == cf.Units("m-1 rad-1"))
2499+
self.assertEqual(x.Units, y.Units)
2500+
self.assertEqual(y.Units, cf.Units("m-1"))
24942501

24952502
x0 = f.derivative(
2496-
"X", wrap=wrap, one_sided_at_boundary=one_sided
2503+
"X",
2504+
wrap=wrap,
2505+
one_sided_at_boundary=one_sided,
24972506
) / (sin_theta * r)
2498-
y0 = f.derivative("Y", one_sided_at_boundary=one_sided) / r
2507+
y0 = (
2508+
f.derivative(
2509+
"Y",
2510+
one_sided_at_boundary=one_sided,
2511+
)
2512+
/ r
2513+
)
24992514

25002515
# Check the data
25012516
with cf.rtol(1e-10):
@@ -2528,7 +2543,8 @@ def test_Field_grad_xy(self):
25282543
for one_sided in (True, False):
25292544
x, y = f.grad_xy(x_wrap=wrap, one_sided_at_boundary=one_sided)
25302545

2531-
self.assertTrue(x.Units == y.Units == cf.Units("m-1"))
2546+
self.assertEqual(x.Units, y.Units)
2547+
self.assertEqual(y.Units, cf.Units("m-1"))
25322548

25332549
x0 = f.derivative(
25342550
"X", wrap=wrap, one_sided_at_boundary=one_sided
@@ -2572,15 +2588,15 @@ def test_Field_laplacian_xy(self):
25722588
radius=radius, x_wrap=wrap, one_sided_at_boundary=one_sided
25732589
)
25742590

2575-
self.assertTrue(lp.Units == cf.Units("m-2 rad-2"))
2591+
self.assertEqual(lp.Units, cf.Units("m-2"))
25762592

25772593
lp0 = cf.div_xy(
25782594
*f.grad_xy(
25792595
radius=radius,
25802596
x_wrap=wrap,
25812597
one_sided_at_boundary=one_sided,
25822598
),
2583-
radius=2,
2599+
radius=radius,
25842600
x_wrap=wrap,
25852601
one_sided_at_boundary=one_sided,
25862602
)
@@ -2604,7 +2620,7 @@ def test_Field_laplacian_xy(self):
26042620
x_wrap=wrap, one_sided_at_boundary=one_sided
26052621
)
26062622

2607-
self.assertTrue(lp.Units == cf.Units("m-2"))
2623+
self.assertEqual(lp.Units, cf.Units("m-2"))
26082624

26092625
lp0 = cf.div_xy(
26102626
*f.grad_xy(x_wrap=wrap, one_sided_at_boundary=one_sided),

cf/test/test_Maths.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_curl_xy(self):
3232
one_sided_at_boundary=one_sided,
3333
)
3434

35-
self.assertTrue(c.Units == cf.Units("m-2 rad-2"))
35+
self.assertEqual(c.Units, cf.Units("m-2"))
3636

3737
term1 = (x * sin_theta).derivative(
3838
"Y", one_sided_at_boundary=one_sided
@@ -68,7 +68,7 @@ def test_curl_xy(self):
6868
x, y, x_wrap=wrap, one_sided_at_boundary=one_sided
6969
)
7070

71-
self.assertTrue(d.Units == cf.Units("m-2"))
71+
self.assertEqual(d.Units, cf.Units("m-2"))
7272

7373
term1 = x.derivative(
7474
"X", wrap=wrap, one_sided_at_boundary=one_sided
@@ -121,7 +121,7 @@ def test_div_xy(self):
121121
one_sided_at_boundary=one_sided,
122122
)
123123

124-
self.assertTrue(d.Units == cf.Units("m-2 rad-2"), d.Units)
124+
self.assertEqual(d.Units, cf.Units("m-2"))
125125

126126
term1 = x.derivative(
127127
"X", wrap=wrap, one_sided_at_boundary=one_sided
@@ -157,7 +157,7 @@ def test_div_xy(self):
157157
x, y, x_wrap=wrap, one_sided_at_boundary=one_sided
158158
)
159159

160-
self.assertTrue(d.Units == cf.Units("m-2"))
160+
self.assertEqual(d.Units, cf.Units("m-2"))
161161

162162
term1 = x.derivative(
163163
"X", wrap=wrap, one_sided_at_boundary=one_sided

0 commit comments

Comments
 (0)