/
maths.py
379 lines (310 loc) · 13.7 KB
/
maths.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
from .functions import _DEPRECATION_ERROR_FUNCTION_KWARGS
from . import Units
from .data.data import Data
from .regrid import get_cartesian_coords
def relative_vorticity(
u, v, wrap=None, one_sided_at_boundary=False, radius=6371229.0, cyclic=None
):
"""Calculate the relative vorticity using centred finite
differences.
The relative vorticity of wind defined on a Cartesian domain (such
as a plane projection) is defined as
ζcartesian = δv/δx − δu/δy
where x and y are points on along the 'X' and 'Y' Cartesian
dimensions respectively; and u and v denote the 'X' and 'Y'
components of the horizontal winds.
If the wind field field is defined on a spherical
latitude-longitude domain then a correction factor is included:
ζspherical = δv/δx − δu/δy + (u/a)tan(ϕ)
where u and v denote the longitudinal and latitudinal components
of the horizontal wind field; a is the radius of the Earth; and ϕ
is the latitude at each point.
The relative vorticity is calculated using centred finite
differences (see the *one_sided_at_boundary* parameter).
The grid may be global or limited area. If missing values are
present then missing values will be returned at points where the
centred finite difference could not be calculated. The boundary
conditions may be cyclic in longitude. The non-cyclic boundaries
may either be filled with missing values or calculated with
off-centre finite differences.
Reference: H.B. Bluestein, Synoptic-Dynamic Meteorology in
Midlatitudes, 1992, Oxford Univ. Press p113-114
:Parameters:
u: `Field`
A field containing the x-wind. Must be on the same grid as
the y-wind.
v: `Field`
A field containing the y-wind. Must be on the same grid as
the x-wind.
radius: optional
The radius of the sphere when the winds are on a spherical
polar coordinate domain. May be any numeric scalar object
that can be converted to a `Data` object (which includes
numpy array and `Data` objects). By default *radius* has a
value of 6371229.0 metres, representing the Earth's
radius. If units are not specified then units of metres
are assumed.
*Parameter example:*
Five equivalent ways to set a radius of 6371200 metres:
``radius=6371200``, ``radius=numpy.array(6371200)``,
``radius=cf.Data(6371200)``, ``radius=cf.Data(6371200,
'm')``, ``radius=cf.Data(6371.2, 'km')``.
wrap: `bool`, optional
Whether the longitude is cyclic or not. By default this is
autodetected.
one_sided_at_boundary: `bool`, optional
If True then if the field is not cyclic off-centre finite
differences are calculated at the boundaries, otherwise
missing values are used at the boundaries.
:Returns:
`Field`
The relative vorticity calculated with centred finite
differences.
"""
if cyclic:
_DEPRECATION_ERROR_FUNCTION_KWARGS(
"relative_vorticity",
{"cyclic": cyclic},
"Use the 'wrap' keyword instead",
) # pragma: no cover
# Get the standard names of u and v
u_std_name = u.get_property("standard_name", None)
v_std_name = v.get_property("standard_name", None)
# Copy u and v
u = u.copy()
v = v.copy()
# Get the X and Y coordinates
(u_x_key, u_y_key), (u_x, u_y) = get_cartesian_coords(u, "u", ("X", "Y"))
(v_x_key, v_y_key), (v_x, v_y) = get_cartesian_coords(v, "v", ("X", "Y"))
if not u_x.equals(v_x) or not u_y.equals(v_y):
raise ValueError("u and v must be on the same grid.")
# Check for lat/long
is_latlong = (u_x.Units.islongitude and u_y.Units.islatitude) or (
u_x.units == "degrees" and u_y.units == "degrees"
)
# Check for cyclicity
if wrap is None:
if is_latlong:
wrap = u.iscyclic(u_x_key)
else:
wrap = False
# Find the relative vorticity
if is_latlong:
# Save the units of the X and Y coordinates
x_units = u_x.Units
y_units = u_y.Units
# Change the units of the lat/longs to radians
radians = Units("radians")
u_x.Units = radians
u_y.Units = radians
v_x.Units = radians
v_y.Units = radians
# Find cos and tan of latitude
cos_lat = u_y.cos()
tan_lat = u_y.tan()
# Reshape for broadcasting
u_shape = [1] * u.ndim
u_y_index = u.get_data_axes().index(u_y_key)
u_shape[u_y_index] = u_y.size
v_shape = [1] * v.ndim
v_y_index = v.get_data_axes().index(v_y_key)
v_shape[v_y_index] = v_y.size
# Calculate the correction term
corr = u.copy()
corr *= tan_lat.array.reshape(u_shape)
# Calculate the derivatives
v.derivative(
v_x_key,
wrap=wrap,
one_sided_at_boundary=one_sided_at_boundary,
inplace=True,
)
v.data /= cos_lat.array.reshape(v_shape)
u.derivative(
u_y_key, one_sided_at_boundary=one_sided_at_boundary, inplace=True
)
radius = Data.asdata(radius).squeeze()
radius.dtype = float
if radius.size != 1:
raise ValueError(f"Multiple radii: radius={radius!r}")
if not radius.Units:
radius.override_units(Units("metres"), inplace=True)
elif not radius.Units.equivalent(Units("metres")):
raise ValueError(f"Invalid units for radius: {radius.Units!r}")
# Calculate the relative vorticity. Do v-(u-corr) rather than
# v-u+corr to be nice with coordinate reference corner cases.
rv = v - (u - corr)
rv.data /= radius
# Convert the units of latitude and longitude to canonical units
rv.dimension_coordinate("X").Units = x_units
rv.dimension_coordinate("Y").Units = y_units
else:
v.derivative(
v_x_key, one_sided_at_boundary=one_sided_at_boundary, inplace=True
)
u.derivative(
u_y_key, one_sided_at_boundary=one_sided_at_boundary, inplace=True
)
rv = v - u
# Convert the units of relative vorticity to canonical units
rv.Units = Units("s-1")
# Set the standard name if appropriate and delete the long_name
if (u_std_name == "eastward_wind" and v_std_name == "northward_wind") or (
u_std_name == "x_wind" and v_std_name == "y_wind"
):
rv.standard_name = "atmosphere_relative_vorticity"
else:
rv.del_property("standard_name", None)
rv.del_property("long_name", None)
return rv
def histogram(*digitized):
"""Return the distribution of a set of variables in the form of an
N-dimensional histogram.
The number of dimensions of the histogram is equal to the number
of field constructs provided by the *digitized* argument. Each
such field construct defines a sequence of bins and provides
indices to the bins that each value of one of the variables
belongs. There is no upper limit to the number of dimensions of
the histogram.
The output histogram bins are defined by the exterior product of
the one-dimensional bins of each digitized field construct. For
example, if only one digitized field construct is provided then
the histogram bins simply comprise its one-dimensional bins; if
there are two digitized field constructs then the histogram bins
comprise the two-dimensional matrix formed by all possible
combinations of the two sets of one-dimensional bins; etc.
An output value for an histogram bin is formed by counting the
number cells for which the digitized field constructs, taken
together, index that bin. Note that it may be the case that not
all output bins are indexed by the digitized field constructs, and
for these bins missing data is returned.
The returned field construct will have a domain axis construct for
each dimension of the histogram, with a corresponding dimension
coordinate construct that defines the bin boundaries.
.. versionadded:: 3.0.2
.. seealso:: `cf.Field.bin`, `cf.Field.collapse`,
`cf.Field.digitize`, `cf.Field.percentile`,
`cf.Field.where`
:Parameters:
digitized: one or more `Field`
One or more field constructs that contain digitized data
with corresponding metadata, as would be output by
`cf.Field.digitize`. Each field construct contains indices
to the one-dimensional bins to which each value of an
original field construct belongs; and there must be
``bin_count`` and ``bin_bounds`` properties as defined by
the `cf.Field.digitize` method (and any of the extra
properties defined by that method are also recommended).
The bins defined by the ``bin_count`` and ``bin_bounds``
properties are used to create a dimension coordinate
construct for the output field construct.
Each digitized field construct must be transformable so
that its data is broadcastable to any other digitized
field contruct's data. This is done by using the metadata
constructs of the to create a mapping of physically
compatible dimensions between the fields, and then
manipulating the dimensions of the digitized field
construct's data to ensure that broadcasting can occur.
:Returns:
`Field`
The field construct containing the histogram.
**Examples:**
Create a one-dimensional histogram based on 10 equally-sized bins
that exactly span the data range:
>>> f = cf.example_field(0)
>>> print(f)
Field: specific_humidity (ncvar%q)
----------------------------------
Data : specific_humidity(latitude(5), longitude(8)) 1
Cell methods : area: mean
Dimension coords: latitude(5) = [-75.0, ..., 75.0] degrees_north
: longitude(8) = [22.5, ..., 337.5] degrees_east
: time(1) = [2019-01-01 00:00:00]
>>> print(f.array)
[[0.007 0.034 0.003 0.014 0.018 0.037 0.024 0.029]
[0.023 0.036 0.045 0.062 0.046 0.073 0.006 0.066]
[0.11 0.131 0.124 0.146 0.087 0.103 0.057 0.011]
[0.029 0.059 0.039 0.07 0.058 0.072 0.009 0.017]
[0.006 0.036 0.019 0.035 0.018 0.037 0.034 0.013]]
>>> indices, bins = f.digitize(10, return_bins=True)
>>> print(indices)
Field: long_name=Bin index to which each 'specific_humidity' value belongs (ncvar%q)
------------------------------------------------------------------------------------
Data : long_name=Bin index to which each 'specific_humidity' value belongs(latitude(5), longitude(8))
Cell methods : area: mean
Dimension coords: latitude(5) = [-75.0, ..., 75.0] degrees_north
: longitude(8) = [22.5, ..., 337.5] degrees_east
: time(1) = [2019-01-01 00:00:00]
>>> print(bins.array)
[[0.003 0.0173]
[0.0173 0.0316]
[0.0316 0.0459]
[0.0459 0.0602]
[0.0602 0.0745]
[0.0745 0.0888]
[0.0888 0.1031]
[0.1031 0.1174]
[0.1174 0.1317]
[0.1317 0.146 ]]
>>> h = cf.histogram(indices)
>>> rint(h)
Field: number_of_observations
-----------------------------
Data : number_of_observations(specific_humidity(10)) 1
Cell methods : latitude: longitude: point
Dimension coords: specific_humidity(10) = [0.01015, ..., 0.13885] 1
>>> print(h.array)
[9 7 9 4 5 1 1 1 2 1]
>>> print(h.coordinate('specific_humidity').bounds.array)
[[0.003 0.0173]
[0.0173 0.0316]
[0.0316 0.0459]
[0.0459 0.0602]
[0.0602 0.0745]
[0.0745 0.0888]
[0.0888 0.1031]
[0.1031 0.1174]
[0.1174 0.1317]
[0.1317 0.146 ]]
Create a two-dimensional histogram based on specific humidity and
temperature bins. The temperature bins in this example are derived
from a dummy temperature field construct with the same shape as
the specific humidity field construct already in use:
>>> g = f.copy()
>>> g.standard_name = 'air_temperature'
>>> import numpy
>>> g[...] = numpy.random.normal(loc=290, scale=10, size=40).reshape(5, 8)
>>> g.override_units('K', inplace=True)
>>> print(g)
Field: air_temperature (ncvar%q)
--------------------------------
Data : air_temperature(latitude(5), longitude(8)) K
Cell methods : area: mean
Dimension coords: latitude(5) = [-75.0, ..., 75.0] degrees_north
: longitude(8) = [22.5, ..., 337.5] degrees_east
: time(1) = [2019-01-01 00:00:00]
>>> indices_t = g.digitize(5)
>>> h = cf.histogram(indices, indices_t)
>>> print(h)
Field: number_of_observations
-----------------------------
Data : number_of_observations(air_temperature(5), specific_humidity(10)) 1
Cell methods : latitude: longitude: point
Dimension coords: air_temperature(5) = [281.1054839143287, ..., 313.9741786365939] K
: specific_humidity(10) = [0.01015, ..., 0.13885] 1
>>> print(h.array)
[[2 1 5 3 2 -- -- -- -- --]
[1 1 2 -- 1 -- 1 1 -- --]
[4 4 2 1 1 1 -- -- 1 1]
[1 1 -- -- 1 -- -- -- 1 --]
[1 -- -- -- -- -- -- -- -- --]]
>>> h.sum()
<CF Data(): 40 1>
"""
if not digitized:
raise ValueError(
"Must provide at least one 'digitized' field construct"
)
f = digitized[0].copy()
f.clear_properties()
return f.bin("sample_size", digitized=digitized)