sectionate 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sectionate/__init__.py +6 -0
- sectionate/catalog/Atlantic_transport_arrays.json +348 -0
- sectionate/gridutils.py +95 -0
- sectionate/section.py +786 -0
- sectionate/tests/test_convergent_transport.py +120 -0
- sectionate/tests/test_section.py +92 -0
- sectionate/tests/test_section_class.py +59 -0
- sectionate/tests/test_section_cornercases.py +63 -0
- sectionate/tests/test_utils.py +4 -0
- sectionate/tracers.py +76 -0
- sectionate/transports.py +505 -0
- sectionate/utils.py +173 -0
- sectionate/version.py +3 -0
- sectionate-0.3.0.dist-info/METADATA +44 -0
- sectionate-0.3.0.dist-info/RECORD +17 -0
- sectionate-0.3.0.dist-info/WHEEL +4 -0
- sectionate-0.3.0.dist-info/licenses/LICENSE +674 -0
sectionate/transports.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import numpy as np
|
|
3
|
+
import xarray as xr
|
|
4
|
+
import dask
|
|
5
|
+
|
|
6
|
+
from .gridutils import check_symmetric, coord_dict, get_geo_corners
|
|
7
|
+
from .section import distance_on_unit_sphere
|
|
8
|
+
|
|
9
|
+
def uvindices_from_qindices(grid, i_c, j_c):
|
|
10
|
+
"""
|
|
11
|
+
Find the `grid` indices of the N-1 velocity points defined by the consecutive indices of
|
|
12
|
+
N vorticity points. Follows MOM6 conventions (https://mom6.readthedocs.io/en/main/api/generated/pages/Horizontal_Indexing.html),
|
|
13
|
+
automatically checking `grid` metadata to determine whether the grid is symmetric or non-symmetric.
|
|
14
|
+
|
|
15
|
+
PARAMETERS:
|
|
16
|
+
-----------
|
|
17
|
+
grid: xgcm.Grid
|
|
18
|
+
Grid object describing ocean model grid and containing data variables
|
|
19
|
+
i_c: int
|
|
20
|
+
vorticity point indices along "X" dimension
|
|
21
|
+
j_c: int
|
|
22
|
+
vorticity point indices along "Y" dimension
|
|
23
|
+
|
|
24
|
+
RETURNS:
|
|
25
|
+
--------
|
|
26
|
+
uvindices : dict
|
|
27
|
+
Dictionary containing:
|
|
28
|
+
- "var" : "U" if corresponding to "X"-direction velocity (usually nominally zonal), "V" otherwise
|
|
29
|
+
- "i" : "X"-dimension index of appropriate "U" or "V" velocity
|
|
30
|
+
- "j" : "Y"-dimension index of appropriate "U" or "V" velocity
|
|
31
|
+
- "nward" : True if point was passed through while going in positive "j"-index direction
|
|
32
|
+
- "eward" : True if point was passed through while going in positive "i"-index direction
|
|
33
|
+
"""
|
|
34
|
+
nsec = len(i_c)
|
|
35
|
+
uvindices = {
|
|
36
|
+
"var":np.zeros(nsec-1, dtype="<U2"),
|
|
37
|
+
"i":np.zeros(nsec-1, dtype=np.int64),
|
|
38
|
+
"j":np.zeros(nsec-1, dtype=np.int64),
|
|
39
|
+
"nward":np.zeros(nsec-1, dtype=bool),
|
|
40
|
+
"eward":np.zeros(nsec-1, dtype=bool)
|
|
41
|
+
}
|
|
42
|
+
symmetric = check_symmetric(grid)
|
|
43
|
+
for k in range(0, nsec-1):
|
|
44
|
+
zonal = not(j_c[k+1] != j_c[k])
|
|
45
|
+
eward = i_c[k+1] > i_c[k]
|
|
46
|
+
nward = j_c[k+1] > j_c[k]
|
|
47
|
+
# Handle corner cases for wrapping boundaries
|
|
48
|
+
if (i_c[k+1] - i_c[k])>1: eward = False
|
|
49
|
+
elif (i_c[k+1] - i_c[k])<-1: eward = True
|
|
50
|
+
uvindex = {
|
|
51
|
+
"var": "V" if zonal else "U",
|
|
52
|
+
"i": i_c[k+(1 if not(eward) and zonal else 0)],
|
|
53
|
+
"j": j_c[k+(1 if not(nward) and not(zonal) else 0)],
|
|
54
|
+
"nward": nward,
|
|
55
|
+
"eward": eward,
|
|
56
|
+
}
|
|
57
|
+
uvindex["i"] += (1 if not(symmetric) and zonal else 0)
|
|
58
|
+
uvindex["j"] += (1 if not(symmetric) and not(zonal) else 0)
|
|
59
|
+
for (key, v) in uvindices.items():
|
|
60
|
+
v[k] = uvindex[key]
|
|
61
|
+
return uvindices
|
|
62
|
+
|
|
63
|
+
def uvcoords_from_uvindices(grid, uvindices):
|
|
64
|
+
"""
|
|
65
|
+
Find the (lons,lats) coordinates of the N-1 velocity points defined by `uvindices` (returned by `uvindices_from_qindices`).
|
|
66
|
+
Assumes the names of longitude and latitude coordinates in `grid` contain the sub-strings "lon" and "lat", respectively,
|
|
67
|
+
but otherwise finds the names using `grid` metadata.
|
|
68
|
+
|
|
69
|
+
PARAMETERS:
|
|
70
|
+
-----------
|
|
71
|
+
grid: xgcm.Grid
|
|
72
|
+
Grid object describing ocean model grid and containing data variables
|
|
73
|
+
uvindices : dict
|
|
74
|
+
Dictionary returned by `sectionate.transports.uvindices_from_qindices`, containing:
|
|
75
|
+
- "var" : "U" if corresponding to "X"-direction velocity (usually nominally zonal), "V" otherwise
|
|
76
|
+
- "i" : "X"-dimension index of appropriate "U" or "V" velocity
|
|
77
|
+
- "j" : "Y"-dimension index of appropriate "U" or "V" velocity
|
|
78
|
+
- "nward" : True if point was passed through while going in positive "j"-index direction
|
|
79
|
+
- "eward" : True if point was passed through while going in positive "i"-index direction
|
|
80
|
+
|
|
81
|
+
RETURNS:
|
|
82
|
+
--------
|
|
83
|
+
lons : np.ndarray(float)
|
|
84
|
+
lats : np.ndarray(float)
|
|
85
|
+
"""
|
|
86
|
+
lons, lats = np.zeros(len(uvindices["var"])), np.zeros(len(uvindices["var"]))
|
|
87
|
+
|
|
88
|
+
ds = grid._ds
|
|
89
|
+
coords = coord_dict(grid)
|
|
90
|
+
geo_coords = [c for c in list(ds.coords) if ("lon" in c) or ("lat" in c)]
|
|
91
|
+
center_names = {f"geo{d}_center":c for d,c in
|
|
92
|
+
{d:c for d in ["lon", "lat"] for c in geo_coords
|
|
93
|
+
if ((coords["X"]["center"] in ds[c].coords) and
|
|
94
|
+
(coords["Y"]["center"] in ds[c].coords))
|
|
95
|
+
if d in c}.items()}
|
|
96
|
+
u_names = {f"geo{d}_u":c for d,c in
|
|
97
|
+
{d:c for d in ["lon", "lat"] for c in geo_coords
|
|
98
|
+
if ((coords["X"]["corner"] in ds[c].coords) and
|
|
99
|
+
(coords["Y"]["center"] in ds[c].coords))
|
|
100
|
+
if d in c}.items()}
|
|
101
|
+
v_names = {f"geo{d}_v":c for d,c in
|
|
102
|
+
{d:c for d in ["lon", "lat"] for c in geo_coords
|
|
103
|
+
if ((coords["X"]["center"] in ds[c].coords) and
|
|
104
|
+
(coords["Y"]["corner"] in ds[c].coords))
|
|
105
|
+
if d in c}.items()}
|
|
106
|
+
corner_names = {f"geo{d}_corner":c for d,c in
|
|
107
|
+
{d:c for d in ["lon", "lat"] for c in geo_coords
|
|
108
|
+
if ((coords["X"]["corner"] in ds[c].coords) and
|
|
109
|
+
(coords["Y"]["corner"] in ds[c].coords))
|
|
110
|
+
if d in c}.items()}
|
|
111
|
+
|
|
112
|
+
for p in range(len(uvindices["var"])):
|
|
113
|
+
var, i, j = uvindices["var"][p], uvindices["i"][p], uvindices["j"][p]
|
|
114
|
+
if var == "U":
|
|
115
|
+
if (f"geolon_u" in u_names) and (f"geolat_u" in u_names):
|
|
116
|
+
lon = ds[u_names[f"geolon_u"]].isel({
|
|
117
|
+
coords["X"]["corner"]:i,
|
|
118
|
+
coords["Y"]["center"]:j
|
|
119
|
+
}).values
|
|
120
|
+
lat = ds[u_names[f"geolat_u"]].isel({
|
|
121
|
+
coords["X"]["corner"]:i,
|
|
122
|
+
coords["Y"]["center"]:j
|
|
123
|
+
}).values
|
|
124
|
+
elif (f"geolon_corner" in corner_names) and (f"geolat_center" in center_names):
|
|
125
|
+
lon = ds[corner_names[f"geolon_corner"]].isel({
|
|
126
|
+
coords["X"]["corner"]:i,
|
|
127
|
+
coords["Y"]["corner"]:j
|
|
128
|
+
}).values
|
|
129
|
+
lat = ds[center_names[f"geolat_center"]].isel({
|
|
130
|
+
coords["X"]["center"]:wrap_idx(i, grid, "X"),
|
|
131
|
+
coords["Y"]["center"]:wrap_idx(j, grid, "Y")
|
|
132
|
+
}).values
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError("Cannot locate grid coordinates necessary to\
|
|
135
|
+
identify U-velociy faces.")
|
|
136
|
+
elif var == "V":
|
|
137
|
+
if (f"geolon_v" in v_names) and (f"geolat_v" in v_names):
|
|
138
|
+
lon = ds[v_names[f"geolon_v"]].isel({
|
|
139
|
+
coords["X"]["center"]:wrap_idx(i, grid, "X"),
|
|
140
|
+
coords["Y"]["corner"]:j
|
|
141
|
+
}).values
|
|
142
|
+
lat = ds[v_names[f"geolat_v"]].isel({
|
|
143
|
+
coords["X"]["center"]:wrap_idx(i, grid, "X"),
|
|
144
|
+
coords["Y"]["corner"]:j
|
|
145
|
+
}).values
|
|
146
|
+
elif (f"geolon_center" in center_names) and (f"geolat_corner" in corner_names):
|
|
147
|
+
lon = ds[center_names[f"geolon_center"]].isel({
|
|
148
|
+
coords["X"]["center"]:wrap_idx(i, grid, "X"),
|
|
149
|
+
coords["Y"]["center"]:wrap_idx(j, grid, "Y")
|
|
150
|
+
}).values
|
|
151
|
+
lat = ds[corner_names[f"geolat_corner"]].isel({
|
|
152
|
+
coords["X"]["corner"]:i,
|
|
153
|
+
coords["Y"]["corner"]:j
|
|
154
|
+
}).values
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError("Cannot locate grid coordinates necessary to\
|
|
157
|
+
identify V-velociy faces.")
|
|
158
|
+
lons[p] = lon
|
|
159
|
+
lats[p] = lat
|
|
160
|
+
return lons, lats
|
|
161
|
+
|
|
162
|
+
def uvcoords_from_qindices(grid, i_c, j_c):
|
|
163
|
+
"""
|
|
164
|
+
Directly finds coordinates of velocity points from vorticity point indices, wrapping other functions.
|
|
165
|
+
|
|
166
|
+
PARAMETERS:
|
|
167
|
+
-----------
|
|
168
|
+
grid: xgcm.Grid
|
|
169
|
+
Grid object describing ocean model grid and containing data variables
|
|
170
|
+
i_c: int
|
|
171
|
+
vorticity point indices along "X" dimension
|
|
172
|
+
j_c: int
|
|
173
|
+
vorticity point indices along "Y" dimension
|
|
174
|
+
|
|
175
|
+
RETURNS:
|
|
176
|
+
--------
|
|
177
|
+
lons : np.ndarray(float)
|
|
178
|
+
lats : np.ndarray(float)
|
|
179
|
+
"""
|
|
180
|
+
return uvcoords_from_uvindices(
|
|
181
|
+
grid,
|
|
182
|
+
uvindices_from_qindices(grid, i_c, j_c),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def convergent_transport(
|
|
186
|
+
grid,
|
|
187
|
+
i_c,
|
|
188
|
+
j_c,
|
|
189
|
+
utr="umo",
|
|
190
|
+
vtr="vmo",
|
|
191
|
+
layer="z_l",
|
|
192
|
+
interface="z_i",
|
|
193
|
+
outname="conv_mass_transport",
|
|
194
|
+
sect_coord="sect",
|
|
195
|
+
geometry="spherical",
|
|
196
|
+
positive_in=True,
|
|
197
|
+
cell_widths={"U":"dyCu", "V":"dxCv"},
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Lazily calculates extensive transports normal to a section, with the sign convention of positive into the spherical polygon
|
|
201
|
+
defined by the section, unless overridden by changing the "positive_in=True" keyword argument. Supports curvlinear geometries
|
|
202
|
+
and complicated grid topologies, as long as the grid is *locally* orthogonal (as in MOM6).
|
|
203
|
+
Lazily broadcasts the calculation in all dimensions except ("X", "Y").
|
|
204
|
+
|
|
205
|
+
PARAMETERS:
|
|
206
|
+
-----------
|
|
207
|
+
grid: xgcm.Grid
|
|
208
|
+
Grid object describing ocean model grid and containing data variables. Must include variables "utr" and "vtr" (see kwargs).
|
|
209
|
+
i_c: int
|
|
210
|
+
Vorticity point indices along "X" dimension.
|
|
211
|
+
j_c: int
|
|
212
|
+
Vorticity point indices along "Y" dimension.
|
|
213
|
+
utr: str
|
|
214
|
+
Name of "X"-direction tracer transport
|
|
215
|
+
vtr: str
|
|
216
|
+
Name of "Y"-direction tracer transport
|
|
217
|
+
layer : str or None
|
|
218
|
+
interface : str or None
|
|
219
|
+
outname : str
|
|
220
|
+
Name of output xr.DataArray variable. Default: "conv_mass_transport".
|
|
221
|
+
sect_coord: str
|
|
222
|
+
Name of the dimension describing along-section data in the output. Default: "sect".
|
|
223
|
+
geometry : str
|
|
224
|
+
Geometry to use to check orientation of the section. Supported geometries are ["cartesian", "spherical"].
|
|
225
|
+
Default: "spherical".
|
|
226
|
+
positive_in : bool or xr.DataArray of type bool
|
|
227
|
+
If True, convergence is defined as "inwards" with respect to the corresponding "geometry".
|
|
228
|
+
If False, convergence is defined as "outwards" (equivalently, negative the inward convergence).
|
|
229
|
+
If a boolean xr.DataArray, get value of positive_in by selecting the value of the mask on the inside
|
|
230
|
+
of an arbitrary velocity face.
|
|
231
|
+
cell_widths : dict
|
|
232
|
+
Values of "U" and "V" items in `cell_widths` correspond to the names of the coordinates describing the width of
|
|
233
|
+
velocity cells. If they are both present in `grid._ds`, accumulate distance along the section and add it to the
|
|
234
|
+
returned xr.DataArray.
|
|
235
|
+
|
|
236
|
+
RETURNS:
|
|
237
|
+
--------
|
|
238
|
+
dsout : xr.DataArray
|
|
239
|
+
Contains the calculated normal transport and the coordinates of the contributing velocity points (`lon`, `lat`),
|
|
240
|
+
as well as some useful metadata, such as whether each point corresponds to a "U" or "V" velocity and whether
|
|
241
|
+
the sign of the transport had to be flipped to make it point inwards.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
if (layer is not None) and (interface is not None):
|
|
245
|
+
if layer.replace("l", "i") != interface:
|
|
246
|
+
raise ValueError("Inconsistent layer and interface grid variables!")
|
|
247
|
+
|
|
248
|
+
uvindices = uvindices_from_qindices(grid, i_c, j_c)
|
|
249
|
+
uvcoords = uvcoords_from_qindices(grid, i_c, j_c)
|
|
250
|
+
|
|
251
|
+
sect = xr.Dataset()
|
|
252
|
+
sect = sect.assign_coords({
|
|
253
|
+
sect_coord: xr.DataArray(
|
|
254
|
+
np.arange(uvindices["i"].size),
|
|
255
|
+
dims=(sect_coord,)
|
|
256
|
+
)
|
|
257
|
+
})
|
|
258
|
+
sect["i"] = xr.DataArray(uvindices["i"], dims=sect_coord)
|
|
259
|
+
sect["j"] = xr.DataArray(uvindices["j"], dims=sect_coord)
|
|
260
|
+
sect["Usign"] = xr.DataArray(
|
|
261
|
+
np.array([1 if i else -1 for i in ~uvindices["nward"]]),
|
|
262
|
+
dims=sect_coord
|
|
263
|
+
)
|
|
264
|
+
sect["Vsign"] = xr.DataArray(
|
|
265
|
+
np.array([1 if i else -1 for i in uvindices["eward"]]),
|
|
266
|
+
dims=sect_coord
|
|
267
|
+
)
|
|
268
|
+
sect["var"] = xr.DataArray(uvindices["var"], dims=sect_coord)
|
|
269
|
+
sect["Umask"] = xr.DataArray(uvindices["var"]=="U", dims=sect_coord)
|
|
270
|
+
sect["Vmask"] = xr.DataArray(uvindices["var"]=="V", dims=sect_coord)
|
|
271
|
+
|
|
272
|
+
mask_types = (np.ndarray, dask.array.Array, xr.DataArray)
|
|
273
|
+
if isinstance(positive_in, mask_types):
|
|
274
|
+
positive_in = is_mask_inside(positive_in, grid, sect)
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
if (geometry == "cartesian") and (grid.axes["X"]._boundary == "periodic"):
|
|
278
|
+
raise ValueError("Periodic cartesian domains are not yet supported!")
|
|
279
|
+
coords = coord_dict(grid)
|
|
280
|
+
geo_corners = get_geo_corners(grid)
|
|
281
|
+
idx = {
|
|
282
|
+
coords["X"]["corner"]:xr.DataArray(i_c, dims=("pt",)),
|
|
283
|
+
coords["Y"]["corner"]:xr.DataArray(j_c, dims=("pt",)),
|
|
284
|
+
}
|
|
285
|
+
counterclockwise = is_section_counterclockwise(
|
|
286
|
+
geo_corners["X"].isel(idx).values,
|
|
287
|
+
geo_corners["Y"].isel(idx).values,
|
|
288
|
+
geometry=geometry
|
|
289
|
+
)
|
|
290
|
+
positive_in = positive_in ^ (not(counterclockwise))
|
|
291
|
+
orient_fact = 1 if positive_in else -1
|
|
292
|
+
|
|
293
|
+
coords = coord_dict(grid)
|
|
294
|
+
usel = {
|
|
295
|
+
coords["X"]["corner"]: sect["i"],
|
|
296
|
+
coords["Y"]["center"]: wrap_idx(sect["j"], grid, "Y")
|
|
297
|
+
}
|
|
298
|
+
vsel = {
|
|
299
|
+
coords["X"]["center"]: wrap_idx(sect["i"], grid, "X"),
|
|
300
|
+
coords["Y"]["corner"]: sect["j"]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
u = grid._ds[utr]
|
|
304
|
+
v = grid._ds[vtr]
|
|
305
|
+
|
|
306
|
+
conv_umo_masked = (
|
|
307
|
+
u.isel(usel).fillna(0.)
|
|
308
|
+
*sect["Usign"]*sect["Umask"]
|
|
309
|
+
)
|
|
310
|
+
conv_vmo_masked = (
|
|
311
|
+
v.isel(vsel).fillna(0.)
|
|
312
|
+
*sect["Vsign"]*sect["Vmask"]
|
|
313
|
+
)
|
|
314
|
+
conv_transport = xr.DataArray(
|
|
315
|
+
(conv_umo_masked + conv_vmo_masked)*orient_fact,
|
|
316
|
+
)
|
|
317
|
+
dsout = xr.Dataset({outname: conv_transport})
|
|
318
|
+
|
|
319
|
+
if ((cell_widths["U"] in grid._ds.coords) and
|
|
320
|
+
(cell_widths["V"] in grid._ds.coords)):
|
|
321
|
+
|
|
322
|
+
dsout = dsout.assign_coords({
|
|
323
|
+
"dl": xr.DataArray(
|
|
324
|
+
(
|
|
325
|
+
(grid._ds[cell_widths["U"]].isel(usel).fillna(0.)
|
|
326
|
+
*sect["Umask"])+
|
|
327
|
+
(grid._ds[cell_widths["V"]].isel(vsel).fillna(0.)
|
|
328
|
+
*sect["Vmask"])
|
|
329
|
+
),
|
|
330
|
+
dims=(sect_coord,),
|
|
331
|
+
attrs={"units":"m"}
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
dsout = dsout.assign_coords({
|
|
336
|
+
"sign": orient_fact*(
|
|
337
|
+
sect["Usign"]*sect["Umask"] +
|
|
338
|
+
sect["Vsign"]*sect["Vmask"]
|
|
339
|
+
),
|
|
340
|
+
"dir": xr.DataArray(
|
|
341
|
+
np.array(["U" if u else "V" for u in sect["Umask"]]),
|
|
342
|
+
coords=(dsout[sect_coord],),
|
|
343
|
+
dims=(sect_coord,)
|
|
344
|
+
),
|
|
345
|
+
"lon": xr.DataArray(
|
|
346
|
+
uvcoords[0],
|
|
347
|
+
coords=(dsout[sect_coord],),
|
|
348
|
+
dims=(sect_coord,)
|
|
349
|
+
),
|
|
350
|
+
"lat": xr.DataArray(
|
|
351
|
+
uvcoords[1],
|
|
352
|
+
coords=(dsout[sect_coord],),
|
|
353
|
+
dims=(sect_coord,)
|
|
354
|
+
),
|
|
355
|
+
})
|
|
356
|
+
dsout[outname].attrs = {**dsout[outname].attrs, **{
|
|
357
|
+
"orient_fact":orient_fact,
|
|
358
|
+
"positive_in":positive_in,
|
|
359
|
+
}}
|
|
360
|
+
dsout[outname].attrs
|
|
361
|
+
|
|
362
|
+
if layer is not None:
|
|
363
|
+
dsout[layer] = grid._ds[layer]
|
|
364
|
+
if interface is not None:
|
|
365
|
+
dsout[interface] = grid._ds[interface]
|
|
366
|
+
|
|
367
|
+
return dsout
|
|
368
|
+
|
|
369
|
+
def is_section_counterclockwise(lons_c, lats_c, geometry="spherical"):
|
|
370
|
+
"""
|
|
371
|
+
Check if the polygon defined by the consecutive (lons_c, lats_c) is `counterclockwise` (with respect to a given
|
|
372
|
+
`geometry`). Under the hood, it does this by checking whether the signed area (or determinant) of the polygon
|
|
373
|
+
is negative (counterclockwise) or positive (clockwise). This is only a meaningful calculation if the section
|
|
374
|
+
is closed, i.e. (lons_c[-1], lats_c[-1]) == (lons_c[0], lats_c[0]), and therefore defines a polygon.
|
|
375
|
+
|
|
376
|
+
For the case `geometry="spherical"`, the periodic nature of the longitude coordinate complicates things;
|
|
377
|
+
instead of working in spherical coordinates, we use a South-Pole stereographic projection of the surface of the sphere
|
|
378
|
+
and evaluate the orientation of the projected polygon with respect to the stereographic plane.
|
|
379
|
+
|
|
380
|
+
PARAMETERS:
|
|
381
|
+
-----------
|
|
382
|
+
lons_c : np.ndarray(float), longitude of corner points, in degrees
|
|
383
|
+
lats_c : np.ndarray(float), latitude of corner points, in degrees
|
|
384
|
+
geometry : str
|
|
385
|
+
Geometry to use to check orientation of the section. Supported geometries are ["cartesian", "spherical"].
|
|
386
|
+
Default: "spherical".
|
|
387
|
+
|
|
388
|
+
RETURNS:
|
|
389
|
+
--------
|
|
390
|
+
counterclockwise : bool
|
|
391
|
+
"""
|
|
392
|
+
if distance_on_unit_sphere(lons_c[0], lats_c[0], lons_c[-1], lats_c[-1]) > 10.:
|
|
393
|
+
warnings.warn("The orientation of open sections is ambiguous–verify that it matches expectations!")
|
|
394
|
+
lons_c = np.append(lons_c, lons_c[0])
|
|
395
|
+
lats_c = np.append(lats_c, lats_c[0])
|
|
396
|
+
|
|
397
|
+
if geometry == "spherical":
|
|
398
|
+
X, Y = stereographic_projection(lons_c, lats_c)
|
|
399
|
+
elif geometry == "cartesian":
|
|
400
|
+
X, Y = lons_c, lats_c
|
|
401
|
+
else:
|
|
402
|
+
raise ValueError("""Only "spherical" and "cartesian" geometries are currently supported.""")
|
|
403
|
+
|
|
404
|
+
signed_area = 0.
|
|
405
|
+
for i in range(X.size-1):
|
|
406
|
+
signed_area += (X[i+1]-X[i])*(Y[i+1]+Y[i])
|
|
407
|
+
return signed_area < 0.
|
|
408
|
+
|
|
409
|
+
def stereographic_projection(lons, lats):
|
|
410
|
+
"""
|
|
411
|
+
Projects longitudes and latitudes onto the South-Polar stereographic plane.
|
|
412
|
+
|
|
413
|
+
PARAMETERS:
|
|
414
|
+
-----------
|
|
415
|
+
lons : np.ndarray(float), in degrees
|
|
416
|
+
lats : np.ndarray(float), in degrees
|
|
417
|
+
|
|
418
|
+
RETURNS:
|
|
419
|
+
--------
|
|
420
|
+
X : np.ndarray(float)
|
|
421
|
+
Y : np.ndarray(float)
|
|
422
|
+
"""
|
|
423
|
+
lats = np.clip(lats, -90. +1.e-3, 90. -1.e-3)
|
|
424
|
+
varphi = np.deg2rad(-lats+90.)
|
|
425
|
+
theta = np.deg2rad(lons)
|
|
426
|
+
|
|
427
|
+
R = np.sin(varphi)/(1. - np.clip(np.cos(varphi), -1. +1.e-14, 1. -1.e-14))
|
|
428
|
+
Theta = -theta
|
|
429
|
+
|
|
430
|
+
X, Y = R*np.cos(Theta), R*np.sin(Theta)
|
|
431
|
+
|
|
432
|
+
return X, Y
|
|
433
|
+
|
|
434
|
+
def is_mask_inside(mask, grid, sect, idx=0):
|
|
435
|
+
"""
|
|
436
|
+
Find the `(i,j)` indices of the `grid` tracer-cell point "inside" of the velocity face at index `idx`,
|
|
437
|
+
and evaluate the value of the `mask` there.
|
|
438
|
+
|
|
439
|
+
PARAMETERS:
|
|
440
|
+
-----------
|
|
441
|
+
mask : xr.DataArray
|
|
442
|
+
grid : xgcm.Grid
|
|
443
|
+
sect : dict
|
|
444
|
+
Dictionary of uvindices (see `sectionate.transports.uvindices_from_qindices`).
|
|
445
|
+
|
|
446
|
+
RETURNS:
|
|
447
|
+
--------
|
|
448
|
+
positive_in : bool
|
|
449
|
+
"""
|
|
450
|
+
symmetric = check_symmetric(grid)
|
|
451
|
+
coords = coord_dict(grid)
|
|
452
|
+
if sect["var"][idx]=="U":
|
|
453
|
+
i = (
|
|
454
|
+
sect["i"][idx]
|
|
455
|
+
- (1 if sect["Usign"][idx].values==-1. else 0)
|
|
456
|
+
+ (1 if not(symmetric) else 0)
|
|
457
|
+
)
|
|
458
|
+
j = sect["j"][idx]
|
|
459
|
+
if 0<=i<=grid._ds[coords["X"]["center"]].size-1:
|
|
460
|
+
positive_in = mask.isel({
|
|
461
|
+
coords["X"]["center"]: i,
|
|
462
|
+
coords["Y"]["center"]: j
|
|
463
|
+
}).values
|
|
464
|
+
elif i==-1:
|
|
465
|
+
positive_in = not(mask.isel({
|
|
466
|
+
coords["X"]["center"]: i+1,
|
|
467
|
+
coords["Y"]["center"]: j
|
|
468
|
+
})).values
|
|
469
|
+
elif i==grid._ds[coords["X"]["center"]].size:
|
|
470
|
+
positive_in = not(mask.isel({
|
|
471
|
+
coords["X"]["center"]: i-1,
|
|
472
|
+
coords["Y"]["center"]: j
|
|
473
|
+
})).values
|
|
474
|
+
elif sect["var"][idx]=="V":
|
|
475
|
+
i = sect["i"][idx]
|
|
476
|
+
j = (
|
|
477
|
+
sect["j"][idx]
|
|
478
|
+
- (1 if sect["Vsign"][idx].values==-1. else 0)
|
|
479
|
+
+ (1 if not(symmetric) else 0)
|
|
480
|
+
)
|
|
481
|
+
if 0<=j<=grid._ds[coords["Y"]["center"]].size-1:
|
|
482
|
+
positive_in = mask.isel({
|
|
483
|
+
coords["X"]["center"]: i,
|
|
484
|
+
coords["Y"]["center"]: j
|
|
485
|
+
}).values
|
|
486
|
+
elif j==-1:
|
|
487
|
+
positive_in = not(mask.isel({
|
|
488
|
+
coords["X"]["center"]: i,
|
|
489
|
+
coords["Y"]["center"]: j+1,
|
|
490
|
+
})).values
|
|
491
|
+
elif j==grid._ds[coords["Y"]["center"]].size:
|
|
492
|
+
positive_in = not(mask.isel({
|
|
493
|
+
coords["X"]["center"]: i,
|
|
494
|
+
coords["Y"]["center"]: j-1
|
|
495
|
+
})).values
|
|
496
|
+
return positive_in
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def wrap_idx(idx, grid, axis):
|
|
500
|
+
coords = coord_dict(grid)
|
|
501
|
+
if grid.axes[axis]._boundary == "periodic":
|
|
502
|
+
idx = np.mod(idx, grid._ds[coords[axis]["center"]].size)
|
|
503
|
+
else:
|
|
504
|
+
idx = np.minimum(idx, grid._ds[coords[axis]["center"]].size-1)
|
|
505
|
+
return idx
|
sectionate/utils.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from sectionate import Section, GriddedSection
|
|
5
|
+
|
|
6
|
+
def load_sections_from_catalog(filename):
|
|
7
|
+
"""
|
|
8
|
+
Load all sections from a specified catalog JSON file.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
filename : str
|
|
13
|
+
The name of the JSON file in the catalog directory.
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
dict
|
|
18
|
+
A dictionary mapping section names to Section objects.
|
|
19
|
+
|
|
20
|
+
Examples
|
|
21
|
+
--------
|
|
22
|
+
>>> sectionate.utils.load_sections_from_catalog("Atlantic_transport_arrays.json")
|
|
23
|
+
"""
|
|
24
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
25
|
+
catalog_path = os.path.join(base_dir, 'catalog', filename)
|
|
26
|
+
with open(catalog_path, 'r') as f:
|
|
27
|
+
content = json.load(f)
|
|
28
|
+
section_dict = {
|
|
29
|
+
section_name: Section(
|
|
30
|
+
section_name,
|
|
31
|
+
(values["lon"], values["lat"])
|
|
32
|
+
)
|
|
33
|
+
for (section_name, values) in content.items()
|
|
34
|
+
}
|
|
35
|
+
return section_dict
|
|
36
|
+
|
|
37
|
+
def get_section_catalog():
|
|
38
|
+
"""
|
|
39
|
+
Get a list of all JSON catalog files in the catalog directory.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
list of str
|
|
44
|
+
List of JSON filenames found in the catalog directory.
|
|
45
|
+
Returns an empty list if the directory does not exist.
|
|
46
|
+
|
|
47
|
+
Example
|
|
48
|
+
-------
|
|
49
|
+
>>> sectionate.utils.get_section_catalog()
|
|
50
|
+
"""
|
|
51
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
52
|
+
catalog_dir = os.path.join(base_dir, 'catalog')
|
|
53
|
+
if not os.path.isdir(catalog_dir):
|
|
54
|
+
print("catalog directory does not exist.")
|
|
55
|
+
return []
|
|
56
|
+
json_files = [f for f in os.listdir(catalog_dir) if f.endswith('.json')]
|
|
57
|
+
return json_files
|
|
58
|
+
|
|
59
|
+
def get_all_section_names():
|
|
60
|
+
"""
|
|
61
|
+
Get a list of all section names available in all catalog JSON files.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
list of str
|
|
66
|
+
List of all unique section names found in the catalog.
|
|
67
|
+
|
|
68
|
+
Example
|
|
69
|
+
-------
|
|
70
|
+
>>> sectionate.utils.get_all_section_names()
|
|
71
|
+
"""
|
|
72
|
+
section_names = set()
|
|
73
|
+
json_files = get_section_catalog()
|
|
74
|
+
if not json_files:
|
|
75
|
+
return []
|
|
76
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
77
|
+
catalog_dir = os.path.join(base_dir, 'catalog')
|
|
78
|
+
for filename in json_files:
|
|
79
|
+
catalog_path = os.path.join(catalog_dir, filename)
|
|
80
|
+
with open(catalog_path, 'r') as f:
|
|
81
|
+
content = json.load(f)
|
|
82
|
+
section_names.update(content.keys())
|
|
83
|
+
return list(section_names)
|
|
84
|
+
|
|
85
|
+
def load_section(section_name):
|
|
86
|
+
"""
|
|
87
|
+
Load a section by name from the section catalog.
|
|
88
|
+
|
|
89
|
+
Searches through all JSON files in the section catalog directory for the specified
|
|
90
|
+
section name. If found, returns a Section object initialized with the section's
|
|
91
|
+
longitude and latitude values.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
section_name : str
|
|
96
|
+
The name of the section to load.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
Section or None
|
|
101
|
+
A Section object if the section is found, otherwise None.
|
|
102
|
+
|
|
103
|
+
Example
|
|
104
|
+
-------
|
|
105
|
+
>>> section = sectionate.load_section("OSNAP West")
|
|
106
|
+
"""
|
|
107
|
+
json_files = get_section_catalog()
|
|
108
|
+
if not json_files:
|
|
109
|
+
return None
|
|
110
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
111
|
+
catalog_dir = os.path.join(base_dir, 'catalog')
|
|
112
|
+
for filename in json_files:
|
|
113
|
+
catalog_path = os.path.join(catalog_dir, filename)
|
|
114
|
+
with open(catalog_path, 'r') as f:
|
|
115
|
+
content = json.load(f)
|
|
116
|
+
if section_name in content:
|
|
117
|
+
values = content[section_name]
|
|
118
|
+
return Section(section_name, (values["lon"], values["lat"]))
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def save_gridded_section(filepath, gridded_section):
|
|
122
|
+
"""
|
|
123
|
+
Save a GriddedSection object to a JSON file.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
filepath : str
|
|
128
|
+
Path to the JSON file to write.
|
|
129
|
+
gridded_section : GriddedSection
|
|
130
|
+
The GriddedSection object to save.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
None
|
|
135
|
+
"""
|
|
136
|
+
data = {
|
|
137
|
+
"name": gridded_section.name,
|
|
138
|
+
"lons_c": [float(x) for x in gridded_section.lons_c],
|
|
139
|
+
"lats_c": [float(x) for x in gridded_section.lats_c],
|
|
140
|
+
"i_c": [int(x) for x in gridded_section.i_c],
|
|
141
|
+
"j_c": [int(x) for x in gridded_section.j_c]
|
|
142
|
+
}
|
|
143
|
+
with open(filepath, "w") as f:
|
|
144
|
+
json.dump(data, f, indent=2)
|
|
145
|
+
|
|
146
|
+
def load_gridded_section(filepath, grid):
|
|
147
|
+
"""
|
|
148
|
+
Load a GriddedSection from a JSON file.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
filepath : str
|
|
153
|
+
Path to the JSON file containing the gridded section data.
|
|
154
|
+
grid : xgcm.Grid
|
|
155
|
+
The xgcm Grid object to associate with the section.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
GriddedSection
|
|
160
|
+
The loaded GriddedSection object.
|
|
161
|
+
"""
|
|
162
|
+
with open(filepath, "r") as f:
|
|
163
|
+
data = json.load(f)
|
|
164
|
+
|
|
165
|
+
name = data["name"]
|
|
166
|
+
coords = (data["lons_c"], data["lats_c"])
|
|
167
|
+
section = Section(name,coords)
|
|
168
|
+
return GriddedSection(
|
|
169
|
+
section,
|
|
170
|
+
grid,
|
|
171
|
+
i_c=data["i_c"],
|
|
172
|
+
j_c=data["j_c"],
|
|
173
|
+
)
|
sectionate/version.py
ADDED