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/section.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import xarray as xr
|
|
3
|
+
|
|
4
|
+
from .gridutils import get_geo_corners, check_symmetric
|
|
5
|
+
|
|
6
|
+
class Section():
|
|
7
|
+
"""A named hydrographic section"""
|
|
8
|
+
def __init__(self, name, coords, children = {}, parent = None):
|
|
9
|
+
"""Initiate named hydrographic section
|
|
10
|
+
|
|
11
|
+
Arguments
|
|
12
|
+
---------
|
|
13
|
+
name [str] -- name of the section
|
|
14
|
+
coords [list or tuple] -- coordinates that define the section
|
|
15
|
+
|
|
16
|
+
If type is list, elements of the list must all be 2-tuples
|
|
17
|
+
of the form (lon, lat).
|
|
18
|
+
|
|
19
|
+
If type is tuple, it must be of the form (lons, lats), where
|
|
20
|
+
lons and lats are lists of np.ndarray instances of the same
|
|
21
|
+
length with elements of type float.
|
|
22
|
+
|
|
23
|
+
Keyword Arguments
|
|
24
|
+
-----------------
|
|
25
|
+
children [mapping from str to Section (default: {})] -- dictionary
|
|
26
|
+
mapping the names of child sections to their Section instances.
|
|
27
|
+
This attribute will generally be populated automatically from
|
|
28
|
+
the function `join_sections`.
|
|
29
|
+
|
|
30
|
+
parent [Section (default: None)] -- TO DO
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
instance of Section
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
>>> sec.Section("Bering Strait", [(-170.3, 66.1), (-167.6,65.7)])
|
|
39
|
+
Section(Bering Strait, [(-170.3, 66.1), (-167.6, 65.7)])
|
|
40
|
+
"""
|
|
41
|
+
self.name = name
|
|
42
|
+
if type(coords) is tuple:
|
|
43
|
+
if len(coords) == 2:
|
|
44
|
+
self.update_coords(coords_from_lonlat(coords[0], coords[1]))
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("If coords is a tuple, must be (lons, lats)")
|
|
47
|
+
elif type(coords) is list:
|
|
48
|
+
if all([(type(c) is tuple) and (len(c)==2) for c in coords]):
|
|
49
|
+
self.coords = coords.copy()
|
|
50
|
+
self.lons_c, self.lats_c = lonlat_from_coords(self.coords)
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError("If coords is a list, its elements must be (lon,lat) 2-tuples")
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("coords must be a 2-tuple of lists/arrays or a list of 2-tuples")
|
|
55
|
+
|
|
56
|
+
self.children = children.copy() # need this to be a copy or get a recursion error in __repr__...
|
|
57
|
+
self.parent = parent
|
|
58
|
+
self.save = {}
|
|
59
|
+
|
|
60
|
+
def reverse(self):
|
|
61
|
+
"""Reverse the section's direction"""
|
|
62
|
+
self.update_coords(self.coords[::-1])
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def update_coords(self, coords):
|
|
66
|
+
"""Update coordinates (including longitude and latitude arrrays)"""
|
|
67
|
+
self.coords = coords
|
|
68
|
+
self.lons_c, self.lats_c = lonlat_from_coords(self.coords)
|
|
69
|
+
|
|
70
|
+
def copy(self):
|
|
71
|
+
"""Create a deep copy of the Section instance"""
|
|
72
|
+
section = Section(
|
|
73
|
+
self.name,
|
|
74
|
+
self.coords,
|
|
75
|
+
children=self.children,
|
|
76
|
+
parent=self.parent
|
|
77
|
+
)
|
|
78
|
+
section.save = self.save.copy()
|
|
79
|
+
return section
|
|
80
|
+
|
|
81
|
+
def __repr__(self, indent=0, show_attributes=True):
|
|
82
|
+
indent_str = " " * indent
|
|
83
|
+
summary = f"{indent_str}Section({self.name}, {self.coords})"
|
|
84
|
+
|
|
85
|
+
# Automatically extract and add attributes
|
|
86
|
+
if show_attributes:
|
|
87
|
+
summary += f"\n{indent_str}attributes:"
|
|
88
|
+
for attr, value in vars(self).items():
|
|
89
|
+
if attr not in ['children', 'save'] and not attr.startswith('_'):
|
|
90
|
+
summary += f"\n{indent_str} {attr}"
|
|
91
|
+
|
|
92
|
+
if len(self.children) > 0:
|
|
93
|
+
summary += f"\n{indent_str} children:"
|
|
94
|
+
for child in self.children.values():
|
|
95
|
+
child_repr = child.__repr__(indent + 3, show_attributes=False)
|
|
96
|
+
summary += f"\n{indent_str} - {child_repr.lstrip()}"
|
|
97
|
+
return summary
|
|
98
|
+
|
|
99
|
+
class GriddedSection(Section):
|
|
100
|
+
"""Initiate named hydrographic section specific to an ocean model grid
|
|
101
|
+
|
|
102
|
+
Arguments
|
|
103
|
+
---------
|
|
104
|
+
section [sectionate.Section] -- named Sectionate section
|
|
105
|
+
grid [xgcm.Grid] -- ocean model grid object
|
|
106
|
+
|
|
107
|
+
Keyword Arguments
|
|
108
|
+
-----------------
|
|
109
|
+
i_c [list or np.ndarray] -- x corner indices
|
|
110
|
+
j_c [list or np.ndarray] -- y corner indices
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
instance of GriddedSection
|
|
115
|
+
"""
|
|
116
|
+
def __init__(self, section, grid, i_c=None, j_c=None):
|
|
117
|
+
super().__init__(
|
|
118
|
+
section.name,
|
|
119
|
+
section.coords,
|
|
120
|
+
children = section.children,
|
|
121
|
+
parent = section.parent
|
|
122
|
+
)
|
|
123
|
+
self.grid = grid
|
|
124
|
+
if isinstance(i_c, (list, np.ndarray)) & isinstance(j_c, (list, np.ndarray)):
|
|
125
|
+
self.i_c = i_c
|
|
126
|
+
self.j_c = j_c
|
|
127
|
+
else:
|
|
128
|
+
self.grid_section()
|
|
129
|
+
|
|
130
|
+
def grid_section(self, **kwargs):
|
|
131
|
+
"""Pass this Section's coordinates to sectionate.grid_section
|
|
132
|
+
|
|
133
|
+
Arguments
|
|
134
|
+
---------
|
|
135
|
+
grid
|
|
136
|
+
|
|
137
|
+
Keyword Arguments
|
|
138
|
+
-----------------
|
|
139
|
+
**kwargs passed directly to sectionate.grid_section
|
|
140
|
+
"""
|
|
141
|
+
self.i_c, self.j_c, self.lons_c, self.lats_c = grid_section(
|
|
142
|
+
self.grid,
|
|
143
|
+
self.lons_c,
|
|
144
|
+
self.lats_c,
|
|
145
|
+
**kwargs
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return self.i_c, self.j_c, self.lons_c, self.lats_c
|
|
149
|
+
|
|
150
|
+
def copy(self):
|
|
151
|
+
"""Creates a copy of a GriddedSection, with deep copies of all attributes except the grid."""
|
|
152
|
+
super().copy()
|
|
153
|
+
self.i_c = self.i_c.copy()
|
|
154
|
+
self.j_c = self.j_c.copy()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def join_sections(name, *sections, **kwargs):
|
|
158
|
+
"""
|
|
159
|
+
Joins child Sections together to create a parent Section.
|
|
160
|
+
|
|
161
|
+
Arguments
|
|
162
|
+
---------
|
|
163
|
+
name [str] -- name of the parent section
|
|
164
|
+
*sections [Section] -- the sequence of child sections to be joined
|
|
165
|
+
|
|
166
|
+
Keyword Arguments
|
|
167
|
+
-----------------
|
|
168
|
+
align [bool (Default : True)] -- reverse sections as needed to minimize
|
|
169
|
+
the distance between end/start points of consecutive sections
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
instance of Section
|
|
174
|
+
|
|
175
|
+
Example
|
|
176
|
+
-------
|
|
177
|
+
>>> section1 = sec.Section("section1", ([0., 100.], [0., 0.]))
|
|
178
|
+
>>> section2 = sec.Section("section2", ([100., 200.], [0., 0.]))
|
|
179
|
+
>>> section = sec.join_sections("section", section1, section2)
|
|
180
|
+
>>> section
|
|
181
|
+
Section(section, [(0.0, 0.0), (100.0, 0.0), (100.0, 0.0), (200.0, 0.0)])
|
|
182
|
+
Children:
|
|
183
|
+
- Section(section1, [(0.0, 0.0), (100.0, 0.0)])
|
|
184
|
+
- Section(section2, [(100.0, 0.0), (200.0, 0.0)])
|
|
185
|
+
"""
|
|
186
|
+
if type(name) is not str:
|
|
187
|
+
raise ValueError("first argument (name) must be a str.")
|
|
188
|
+
elif any([not(isinstance(s, Section)) for s in sections]):
|
|
189
|
+
raise ValueError("all positional arguments after the first must be instances of Section")
|
|
190
|
+
align = kwargs["align"] if "align" in kwargs else True
|
|
191
|
+
extend = kwargs["extend"] if "extend" in kwargs else False
|
|
192
|
+
|
|
193
|
+
section = Section(name, sections[0].coords)
|
|
194
|
+
if len(sections) > 1:
|
|
195
|
+
for i, s in enumerate(sections[1:], start=1):
|
|
196
|
+
if not(align):
|
|
197
|
+
coords1, coords2 = section.coords, s.coords
|
|
198
|
+
else:
|
|
199
|
+
coords1, coords2 = align_coords(
|
|
200
|
+
section.coords,
|
|
201
|
+
s.coords,
|
|
202
|
+
extend=extend
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
s.update_coords(coords2)
|
|
206
|
+
section.update_coords(coords1 + coords2)
|
|
207
|
+
|
|
208
|
+
if i == 1:
|
|
209
|
+
sections[0].update_coords(coords1)
|
|
210
|
+
section.children[sections[0].name] = sections[0]
|
|
211
|
+
section.children[s.name] = s
|
|
212
|
+
|
|
213
|
+
return section
|
|
214
|
+
|
|
215
|
+
def grid_section(grid, lons, lats, topology="latlon"):
|
|
216
|
+
"""
|
|
217
|
+
Compute composite section along model `grid` velocity faces that approximates geodesic paths
|
|
218
|
+
between consecutive points defined by (lons, lats).
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
grid: xgcm.Grid
|
|
223
|
+
Object describing the geometry of the ocean model grid, including metadata about variable names for
|
|
224
|
+
the staggered C-grid dimensions and c oordinates.
|
|
225
|
+
lons: list or np.ndarray
|
|
226
|
+
Longitudes, in degrees, of consecutive vertices defining a piece-wise geodesic section.
|
|
227
|
+
lats: list or np.ndarray
|
|
228
|
+
Latitudes, in degrees (in range [-90, 90]), of consecutive vertices defining a piece-wise geodesic section.
|
|
229
|
+
topology: str
|
|
230
|
+
Default: "latlon". Currently only supports the following options: ["latlon", "cartesian", "MOM-tripolar"].
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
i_c, j_c, lons_c, lats_c: `np.ndarray` of types (int, int, float, float)
|
|
235
|
+
(i_c, j_c) correspond to indices of vorticity points that define velocity faces.
|
|
236
|
+
(lons_c, lats_c) are the corresponding longitude and latitudes.
|
|
237
|
+
"""
|
|
238
|
+
geocorners = get_geo_corners(grid)
|
|
239
|
+
return create_section_composite(
|
|
240
|
+
geocorners["X"],
|
|
241
|
+
geocorners["Y"],
|
|
242
|
+
lons,
|
|
243
|
+
lats,
|
|
244
|
+
check_symmetric(grid),
|
|
245
|
+
boundary={ax:grid.axes[ax]._boundary for ax in grid.axes},
|
|
246
|
+
topology=topology
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def create_section_composite(
|
|
250
|
+
gridlon,
|
|
251
|
+
gridlat,
|
|
252
|
+
lons,
|
|
253
|
+
lats,
|
|
254
|
+
symmetric,
|
|
255
|
+
boundary={"X":"periodic", "Y":"extend"},
|
|
256
|
+
topology="latlon"
|
|
257
|
+
):
|
|
258
|
+
"""
|
|
259
|
+
Compute composite section along velocity faces, as defined by coordinates of vorticity points (gridlon, gridlat),
|
|
260
|
+
that most closely approximates geodesic paths between consecutive points defined by (lons, lats).
|
|
261
|
+
|
|
262
|
+
PARAMETERS:
|
|
263
|
+
-----------
|
|
264
|
+
|
|
265
|
+
gridlon: np.ndarray
|
|
266
|
+
2d array of longitude (with dimensions ("Y", "X")), in degrees
|
|
267
|
+
gridlat: np.ndarray
|
|
268
|
+
2d array of latitude (with dimensions ("Y", "X")), in degrees
|
|
269
|
+
lons: list of float
|
|
270
|
+
longitude of section starting, intermediate and end points, in degrees
|
|
271
|
+
lats: list of float
|
|
272
|
+
latitude of section starting, intermediate and end points, in degrees
|
|
273
|
+
symmetric: bool
|
|
274
|
+
True if symmetric (vorticity on "outer" positions); False if non-symmetric (assuming "right" positions).
|
|
275
|
+
boundary: dictionary mapping grid axis to boundary condition
|
|
276
|
+
Default: {"X":"periodic", "Y":"extend"}. Set to {"X":"extend", "Y":"extend"} if using a non-periodic regional domain.
|
|
277
|
+
topology: str
|
|
278
|
+
Default: "latlon". Currently only supports the following options: ["latlon", "cartesian", "MOM-tripolar"].
|
|
279
|
+
|
|
280
|
+
RETURNS:
|
|
281
|
+
-------
|
|
282
|
+
|
|
283
|
+
i_c, j_c, lons_c, lats_c: `np.ndarray` of types (int, int, float, float)
|
|
284
|
+
(i_c, j_c) correspond to indices of vorticity points that define velocity faces.
|
|
285
|
+
(lons_c, lats_c) are the corresponding longitude and latitudes.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
i_c = np.array([], dtype=np.int64)
|
|
289
|
+
j_c = np.array([], dtype=np.int64)
|
|
290
|
+
lons_c = np.array([], dtype=np.float64)
|
|
291
|
+
lats_c = np.array([], dtype=np.float64)
|
|
292
|
+
|
|
293
|
+
if len(lons) != len(lats):
|
|
294
|
+
raise ValueError("lons and lats should have the same length")
|
|
295
|
+
|
|
296
|
+
for k in range(len(lons) - 1):
|
|
297
|
+
i_c_seg, j_c_seg, lons_c_seg, lats_c_seg = create_section(
|
|
298
|
+
gridlon,
|
|
299
|
+
gridlat,
|
|
300
|
+
lons[k],
|
|
301
|
+
lats[k],
|
|
302
|
+
lons[k + 1],
|
|
303
|
+
lats[k + 1],
|
|
304
|
+
symmetric,
|
|
305
|
+
boundary=boundary,
|
|
306
|
+
topology=topology
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
i_c = np.concatenate([i_c, i_c_seg[:-1]], axis=0)
|
|
310
|
+
j_c = np.concatenate([j_c, j_c_seg[:-1]], axis=0)
|
|
311
|
+
lons_c = np.concatenate([lons_c, lons_c_seg[:-1]], axis=0)
|
|
312
|
+
lats_c = np.concatenate([lats_c, lats_c_seg[:-1]], axis=0)
|
|
313
|
+
|
|
314
|
+
i_c = np.concatenate([i_c, [i_c_seg[-1]]], axis=0)
|
|
315
|
+
j_c = np.concatenate([j_c, [j_c_seg[-1]]], axis=0)
|
|
316
|
+
lons_c = np.concatenate([lons_c, [lons_c_seg[-1]]], axis=0)
|
|
317
|
+
lats_c = np.concatenate([lats_c, [lats_c_seg[-1]]], axis=0)
|
|
318
|
+
|
|
319
|
+
return i_c.astype(np.int64), j_c.astype(np.int64), lons_c, lats_c
|
|
320
|
+
|
|
321
|
+
def create_section(gridlon, gridlat, lonstart, latstart, lonend, latend, symmetric, boundary={"X":"periodic", "Y":"extend"}, topology="latlon"):
|
|
322
|
+
"""
|
|
323
|
+
Compute a section segment along velocity faces, as defined by coordinates of vorticity points (gridlon, gridlat),
|
|
324
|
+
that most closely approximates the geodesic path between points (lonstart, latstart) and (lonend, latend).
|
|
325
|
+
|
|
326
|
+
PARAMETERS:
|
|
327
|
+
-----------
|
|
328
|
+
|
|
329
|
+
gridlon: np.ndarray
|
|
330
|
+
2d array of longitude (with dimensions ("Y", "X")), in degrees
|
|
331
|
+
gridlat: np.ndarray
|
|
332
|
+
2d array of latitude (with dimensions ("Y", "X")), in degrees
|
|
333
|
+
lonstart: float
|
|
334
|
+
longitude of starting point, in degrees
|
|
335
|
+
lonend: float
|
|
336
|
+
longitude of end point, in degrees
|
|
337
|
+
latstart: float
|
|
338
|
+
latitude of starting point, in degrees
|
|
339
|
+
latend: float
|
|
340
|
+
latitude of end point, in degrees
|
|
341
|
+
symmetric: bool
|
|
342
|
+
True if symmetric (vorticity on "outer" positions); False if non-symmetric (assuming "right" positions).
|
|
343
|
+
boundary: dictionary mapping grid axis to boundary condition
|
|
344
|
+
Default: {"X":"periodic", "Y":"extend"}. Set to {"X":"extend", "Y":"extend"} if using a non-periodic regional domain.
|
|
345
|
+
topology: str
|
|
346
|
+
Default: "latlon". Currently only supports the following options: ["latlon", "cartesian", "MOM-tripolar"].
|
|
347
|
+
|
|
348
|
+
RETURNS:
|
|
349
|
+
-------
|
|
350
|
+
|
|
351
|
+
i_c, j_c, lons_c, lats_c: `np.ndarray` of types (int, int, float, float)
|
|
352
|
+
(i_c, j_c) correspond to indices of vorticity points that define velocity faces.
|
|
353
|
+
(lons_c, lats_c) are the corresponding longitude and latitudes.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if symmetric and boundary["X"] == "periodic":
|
|
357
|
+
gridlon=gridlon[:,:-1]
|
|
358
|
+
gridlat=gridlat[:,:-1]
|
|
359
|
+
|
|
360
|
+
i_c_seg, j_c_seg, lons_c_seg, lats_c_seg = infer_grid_path_from_geo(
|
|
361
|
+
lonstart,
|
|
362
|
+
latstart,
|
|
363
|
+
lonend,
|
|
364
|
+
latend,
|
|
365
|
+
gridlon,
|
|
366
|
+
gridlat,
|
|
367
|
+
boundary=boundary,
|
|
368
|
+
topology=topology
|
|
369
|
+
)
|
|
370
|
+
return (
|
|
371
|
+
i_c_seg,
|
|
372
|
+
j_c_seg,
|
|
373
|
+
lons_c_seg,
|
|
374
|
+
lats_c_seg
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def infer_grid_path_from_geo(lonstart, latstart, lonend, latend, gridlon, gridlat, boundary={"X":"periodic", "Y":"extend"}, topology="latlon"):
|
|
378
|
+
"""
|
|
379
|
+
Find the grid indices (and coordinates) of vorticity points that most closely approximates
|
|
380
|
+
the geodesic path between points (lonstart, latstart) and (lonend, latend).
|
|
381
|
+
|
|
382
|
+
PARAMETERS:
|
|
383
|
+
-----------
|
|
384
|
+
|
|
385
|
+
lonstart: float
|
|
386
|
+
longitude of section starting point, in degrees
|
|
387
|
+
latstart: float
|
|
388
|
+
latitude of section starting point, in degrees
|
|
389
|
+
lonend: float
|
|
390
|
+
longitude of section end point, in degrees
|
|
391
|
+
latend: float
|
|
392
|
+
latitude of section end point, in degrees
|
|
393
|
+
gridlon: np.ndarray
|
|
394
|
+
2d array of longitude, in degrees
|
|
395
|
+
gridlat: np.ndarray
|
|
396
|
+
2d array of latitude, in degrees
|
|
397
|
+
boundary: dictionary mapping grid axis to boundary condition
|
|
398
|
+
Default: {"X":"periodic", "Y":"extend"}. Set to {"X":"extend", "Y":"extend"} if using a non-periodic regional domain.
|
|
399
|
+
topology: str
|
|
400
|
+
Default: "latlon". Currently only supports the following options: ["latlon", "cartesian", "MOM-tripolar"].
|
|
401
|
+
|
|
402
|
+
RETURNS:
|
|
403
|
+
-------
|
|
404
|
+
|
|
405
|
+
i_c, j_c, lons_c, lats_c: `np.ndarray` of types (int, int, float, float)
|
|
406
|
+
(i_c, j_c) correspond to indices of vorticity points that define velocity faces.
|
|
407
|
+
(lons_c, lats_c) are the corresponding longitude and latitudes.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
istart, jstart = find_closest_grid_point(
|
|
411
|
+
lonstart,
|
|
412
|
+
latstart,
|
|
413
|
+
gridlon,
|
|
414
|
+
gridlat
|
|
415
|
+
)
|
|
416
|
+
iend, jend = find_closest_grid_point(
|
|
417
|
+
lonend,
|
|
418
|
+
latend,
|
|
419
|
+
gridlon,
|
|
420
|
+
gridlat
|
|
421
|
+
)
|
|
422
|
+
i_c_seg, j_c_seg, lons_c_seg, lats_c_seg = infer_grid_path(
|
|
423
|
+
istart,
|
|
424
|
+
jstart,
|
|
425
|
+
iend,
|
|
426
|
+
jend,
|
|
427
|
+
gridlon,
|
|
428
|
+
gridlat,
|
|
429
|
+
boundary=boundary,
|
|
430
|
+
topology=topology
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return i_c_seg, j_c_seg, lons_c_seg, lats_c_seg
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def infer_grid_path(i1, j1, i2, j2, gridlon, gridlat, boundary={"X":"periodic", "Y":"extend"}, topology="latlon"):
|
|
437
|
+
"""
|
|
438
|
+
Find the grid indices (and coordinates) of vorticity points that most closely approximate
|
|
439
|
+
the geodesic path between points (gridlon[j1,i1], gridlat[j1,i1]) and
|
|
440
|
+
(gridlon[j2,i2], gridlat[j2,i2]).
|
|
441
|
+
|
|
442
|
+
PARAMETERS:
|
|
443
|
+
-----------
|
|
444
|
+
|
|
445
|
+
i1: integer
|
|
446
|
+
i-coord of point1
|
|
447
|
+
j1: integer
|
|
448
|
+
j-coord of point1
|
|
449
|
+
i2: integer
|
|
450
|
+
i-coord of point2
|
|
451
|
+
j2: integer
|
|
452
|
+
j-coord of point2
|
|
453
|
+
gridlon: np.ndarray
|
|
454
|
+
2d array of longitude, in degrees
|
|
455
|
+
gridlat: np.ndarray
|
|
456
|
+
2d array of latitude, in degrees
|
|
457
|
+
boundary: dictionary mapping grid axis to boundary condition
|
|
458
|
+
Default: {"X":"periodic", "Y":"extend"}. Set to {"X":"extend", "Y":"extend"} if using a non-periodic regional domain.
|
|
459
|
+
topology: str
|
|
460
|
+
Default: "latlon". Currently only supports the following options: ["latlon", "cartesian", "MOM-tripolar"].
|
|
461
|
+
|
|
462
|
+
RETURNS:
|
|
463
|
+
-------
|
|
464
|
+
|
|
465
|
+
i_c_seg, j_c_seg: list of int
|
|
466
|
+
list of (i,j) pairs bounded by (i1, j1) and (i2, j2)
|
|
467
|
+
lons_c_seg, lats_c_seg: list of float
|
|
468
|
+
corresponding longitude and latitude for i_c_seg, j_c_seg
|
|
469
|
+
"""
|
|
470
|
+
ny, nx = gridlon.shape
|
|
471
|
+
|
|
472
|
+
if isinstance(gridlon, xr.core.dataarray.DataArray):
|
|
473
|
+
gridlon = gridlon.values
|
|
474
|
+
if isinstance(gridlat, xr.core.dataarray.DataArray):
|
|
475
|
+
gridlat = gridlat.values
|
|
476
|
+
|
|
477
|
+
# target coordinates
|
|
478
|
+
lon1, lat1 = gridlon[j1, i1], gridlat[j1, i1]
|
|
479
|
+
lon2, lat2 = gridlon[j2, i2], gridlat[j2, i2]
|
|
480
|
+
|
|
481
|
+
# init loop index to starting position
|
|
482
|
+
i = i1
|
|
483
|
+
j = j1
|
|
484
|
+
|
|
485
|
+
i_c_seg = [i] # add first point to list of points
|
|
486
|
+
j_c_seg = [j] # add first point to list of points
|
|
487
|
+
|
|
488
|
+
# iterate through the grid path steps until we reach end of section
|
|
489
|
+
ct = 0 # grid path step counter
|
|
490
|
+
|
|
491
|
+
# Grid-agnostic algorithm:
|
|
492
|
+
# First, find all four neighbors (subject to grid topology)
|
|
493
|
+
# Second, throw away any that are further from the destination than the current point
|
|
494
|
+
# Third, go to the valid neighbor that has the smallest angle from the arc path between the
|
|
495
|
+
# start and end points (the shortest geodesic path)
|
|
496
|
+
j_prev, i_prev = j,i
|
|
497
|
+
while (i%nx != i2) or (j != j2):
|
|
498
|
+
|
|
499
|
+
# safety precaution: exit after taking enough steps to have crossed the entire model grid
|
|
500
|
+
if ct > (nx+ny+1):
|
|
501
|
+
raise RuntimeError(f"Should have reached the endpoint by now.")
|
|
502
|
+
|
|
503
|
+
d_current = distance_on_unit_sphere(
|
|
504
|
+
gridlon[j,i],
|
|
505
|
+
gridlat[j,i],
|
|
506
|
+
lon2,
|
|
507
|
+
lat2
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if d_current < 1.e-12:
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
if boundary["X"] == "periodic":
|
|
514
|
+
right = (j, (i+1)%nx)
|
|
515
|
+
left = (j, (i-1)%nx)
|
|
516
|
+
else:
|
|
517
|
+
right = (j, np.clip(i+1, 0, nx-1))
|
|
518
|
+
left = (j, np.clip(i-1, 0, nx-1))
|
|
519
|
+
down = (np.clip(j-1, 0, ny-1), i)
|
|
520
|
+
|
|
521
|
+
if topology=="MOM-tripolar":
|
|
522
|
+
if j!=ny-1:
|
|
523
|
+
up = (j+1, i%nx)
|
|
524
|
+
else:
|
|
525
|
+
up = (j-1, (nx-1) - (i%nx))
|
|
526
|
+
|
|
527
|
+
elif topology=="cartesian" or topology=="latlon":
|
|
528
|
+
up = (np.clip(j+1, 0, ny-1), i)
|
|
529
|
+
else:
|
|
530
|
+
raise ValueError("Only 'cartesian', 'latlon', and 'MOM-tripolar' grid topologies are currently supported.")
|
|
531
|
+
|
|
532
|
+
neighbors = [right, left, down, up]
|
|
533
|
+
|
|
534
|
+
j_next, i_next = None, None
|
|
535
|
+
smallest_angle = np.inf
|
|
536
|
+
d_list = []
|
|
537
|
+
for (_j, _i) in neighbors:
|
|
538
|
+
d = distance_on_unit_sphere(
|
|
539
|
+
gridlon[_j,_i],
|
|
540
|
+
gridlat[_j,_i],
|
|
541
|
+
lon2,
|
|
542
|
+
lat2
|
|
543
|
+
)
|
|
544
|
+
d_list.append(d/d_current)
|
|
545
|
+
if d < d_current:
|
|
546
|
+
if d==0.: # We're done!
|
|
547
|
+
j_next, i_next = _j, _i
|
|
548
|
+
smallest_angle = 0.
|
|
549
|
+
break
|
|
550
|
+
# Instead of simply moving to the point that gets us closest to the target,
|
|
551
|
+
# a more robust approach is to pick, among the points that do get us closer,
|
|
552
|
+
# the one that most closely follows the great circle between the start and
|
|
553
|
+
# end points of the section. We average the angles relative to both end
|
|
554
|
+
# points so that the shortest path is unique and insensitive to which direction
|
|
555
|
+
# the section is traveled.
|
|
556
|
+
else:
|
|
557
|
+
angle1 = spherical_angle(
|
|
558
|
+
lon2,
|
|
559
|
+
lat2,
|
|
560
|
+
lon1,
|
|
561
|
+
lat1,
|
|
562
|
+
gridlon[_j,_i],
|
|
563
|
+
gridlat[_j,_i],
|
|
564
|
+
)
|
|
565
|
+
angle2 = spherical_angle(
|
|
566
|
+
lon1,
|
|
567
|
+
lat1,
|
|
568
|
+
lon2,
|
|
569
|
+
lat2,
|
|
570
|
+
gridlon[_j,_i],
|
|
571
|
+
gridlat[_j,_i],
|
|
572
|
+
)
|
|
573
|
+
angle = (angle1+angle2)/2.
|
|
574
|
+
if angle < smallest_angle:
|
|
575
|
+
j_next, i_next = _j, _i
|
|
576
|
+
smallest_angle = angle
|
|
577
|
+
|
|
578
|
+
# There can be some strange edge cases in which none of the neighboring points
|
|
579
|
+
# actually get us closer to the target (e.g. when closing folds in the grid).
|
|
580
|
+
# In these cases, simply pick the adjacent point that gets us closest, as long as
|
|
581
|
+
# it was not our previous point (to avoid endless loops). This algorithm should be
|
|
582
|
+
# guaranteed to always get us to the target point.
|
|
583
|
+
if (smallest_angle == np.inf) or (j_next, i_next) == (j_prev, i_prev):
|
|
584
|
+
if (j_prev, i_prev) in neighbors:
|
|
585
|
+
idx = neighbors.index((j_prev, i_prev))
|
|
586
|
+
del neighbors[idx]
|
|
587
|
+
del d_list[idx]
|
|
588
|
+
|
|
589
|
+
(j_next, i_next) = neighbors[np.argmin(d_list)]
|
|
590
|
+
|
|
591
|
+
j_prev, i_prev = j,i
|
|
592
|
+
|
|
593
|
+
j = j_next
|
|
594
|
+
i = i_next
|
|
595
|
+
|
|
596
|
+
i_c_seg.append(i)
|
|
597
|
+
j_c_seg.append(j)
|
|
598
|
+
|
|
599
|
+
ct+=1
|
|
600
|
+
|
|
601
|
+
# create lat/lon vectors from i,j pairs
|
|
602
|
+
lons_c_seg = []
|
|
603
|
+
lats_c_seg = []
|
|
604
|
+
for jj, ji in zip(j_c_seg, i_c_seg):
|
|
605
|
+
lons_c_seg.append(gridlon[jj, ji])
|
|
606
|
+
lats_c_seg.append(gridlat[jj, ji])
|
|
607
|
+
return np.array(i_c_seg), np.array(j_c_seg), np.array(lons_c_seg), np.array(lats_c_seg)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def find_closest_grid_point(lon, lat, gridlon, gridlat):
|
|
611
|
+
"""
|
|
612
|
+
Find integer indices of closest grid point in grid of coordinates
|
|
613
|
+
(gridlon, gridlat), for a given point (lon, at).
|
|
614
|
+
|
|
615
|
+
PARAMETERS:
|
|
616
|
+
-----------
|
|
617
|
+
lon (float): longitude of point to find, in degrees
|
|
618
|
+
lat (float): latitude of point to find, in degrees
|
|
619
|
+
gridlon (numpy.ndarray): grid longitudes, in degrees
|
|
620
|
+
gridlat (numpy.ndarray): grid latitudes, in degrees
|
|
621
|
+
|
|
622
|
+
RETURNS:
|
|
623
|
+
--------
|
|
624
|
+
|
|
625
|
+
iclose, jclose: integer
|
|
626
|
+
grid indices for geographical point of interest
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
if isinstance(gridlon, xr.core.dataarray.DataArray):
|
|
630
|
+
gridlon = gridlon.values
|
|
631
|
+
if isinstance(gridlat, xr.core.dataarray.DataArray):
|
|
632
|
+
gridlat = gridlat.values
|
|
633
|
+
dist = distance_on_unit_sphere(lon, lat, gridlon, gridlat)
|
|
634
|
+
jclose, iclose = np.unravel_index(np.nanargmin(dist), gridlon.shape)
|
|
635
|
+
return iclose, jclose
|
|
636
|
+
|
|
637
|
+
def distance_on_unit_sphere(lon1, lat1, lon2, lat2, R=6.371e6, method="vincenty"):
|
|
638
|
+
"""
|
|
639
|
+
Calculate geodesic arc distance between points (lon1, lat1) and (lon2, lat2).
|
|
640
|
+
|
|
641
|
+
PARAMETERS:
|
|
642
|
+
-----------
|
|
643
|
+
lon1 : float
|
|
644
|
+
Start longitude(s), in degrees
|
|
645
|
+
lat1 : float
|
|
646
|
+
Start latitude(s), in degrees
|
|
647
|
+
lon2 : float
|
|
648
|
+
End longitude(s), in degrees
|
|
649
|
+
lat2 : float
|
|
650
|
+
End latitude(s), in degrees
|
|
651
|
+
R : float
|
|
652
|
+
Radius of sphere. Default: 6.371e6 (realistic Earth value). Set to 1 for
|
|
653
|
+
arc distance in radius.
|
|
654
|
+
method : str
|
|
655
|
+
Name of method. Supported methods: ["vincenty", "haversine", "law of cosines"].
|
|
656
|
+
Default: "vincenty", which is the most robust. Note, however, that it still can result in
|
|
657
|
+
vanishingly small (but crucially non-zero) errors; such as that the distance between (0., 0.)
|
|
658
|
+
and (360., 0.) is 1.e-16 meters when it should be identically zero.
|
|
659
|
+
|
|
660
|
+
RETURNS:
|
|
661
|
+
--------
|
|
662
|
+
|
|
663
|
+
dist : float
|
|
664
|
+
Geodesic distance between points (lon1, lat1) and (lon2, lat2).
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
phi1 = np.deg2rad(lat1)
|
|
668
|
+
phi2 = np.deg2rad(lat2)
|
|
669
|
+
dphi = np.abs(phi2-phi1)
|
|
670
|
+
|
|
671
|
+
lam1 = np.deg2rad(lon1)
|
|
672
|
+
lam2 = np.deg2rad(lon2)
|
|
673
|
+
dlam = np.abs(lam2-lam1)
|
|
674
|
+
|
|
675
|
+
if method=="vincenty":
|
|
676
|
+
numerator = np.sqrt(
|
|
677
|
+
(np.cos(phi2)*np.sin(dlam))**2 +
|
|
678
|
+
(np.cos(phi1)*np.sin(phi2) - np.sin(phi1)*np.cos(phi2)*np.cos(dlam))**2
|
|
679
|
+
)
|
|
680
|
+
denominator = np.sin(phi1)*np.sin(phi2) + np.cos(phi1)*np.cos(phi2)*np.cos(dlam)
|
|
681
|
+
arc = np.arctan2(numerator, denominator)
|
|
682
|
+
|
|
683
|
+
elif method=="haversine":
|
|
684
|
+
arc = 2*np.arcsin(np.sqrt(
|
|
685
|
+
np.sin(dphi/2.)**2 + (1. - np.sin(dphi/2.)**2 - np.sin((phi1+phi2)/2.)**2)*np.sin(dlam/2.)**2
|
|
686
|
+
))
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
elif method=="law of cosines":
|
|
690
|
+
arc = np.arccos(
|
|
691
|
+
np.sin(phi1)*np.sin(phi2) + np.cos(phi1)*np.cos(phi2)*np.cos(dlam)
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
return R * arc
|
|
695
|
+
|
|
696
|
+
def spherical_angle(lonA, latA, lonB, latB, lonC, latC):
|
|
697
|
+
"""
|
|
698
|
+
Calculate the spherical triangle angle alpha between geodesic arcs AB and AC defined by
|
|
699
|
+
[(lonA, latA), (lonB, latB)] and [(lonA, latA), (lonC, latC)], respectively.
|
|
700
|
+
|
|
701
|
+
PARAMETERS:
|
|
702
|
+
-----------
|
|
703
|
+
lonA : float
|
|
704
|
+
Longitude of point A, in degrees
|
|
705
|
+
latA : float
|
|
706
|
+
Latitude of point A, in degrees
|
|
707
|
+
lonB : float
|
|
708
|
+
Longitude of point B, in degrees
|
|
709
|
+
latB : float
|
|
710
|
+
Latitude of point B, in degrees
|
|
711
|
+
lonC : float
|
|
712
|
+
Longitude of point C, in degrees
|
|
713
|
+
latC : float
|
|
714
|
+
Latitude of point C, in degrees
|
|
715
|
+
|
|
716
|
+
RETURNS:
|
|
717
|
+
--------
|
|
718
|
+
|
|
719
|
+
angle : float
|
|
720
|
+
Spherical absolute value of triangle angle alpha, in radians.
|
|
721
|
+
"""
|
|
722
|
+
a = distance_on_unit_sphere(lonB, latB, lonC, latC, R=1.)
|
|
723
|
+
b = distance_on_unit_sphere(lonC, latC, lonA, latA, R=1.)
|
|
724
|
+
c = distance_on_unit_sphere(lonA, latA, lonB, latB, R=1.)
|
|
725
|
+
|
|
726
|
+
return np.arccos(np.clip((np.cos(a) - np.cos(b)*np.cos(c))/(np.sin(b)*np.sin(c)), -1., 1.))
|
|
727
|
+
|
|
728
|
+
def align_coords(coords1, coords2, extend=False):
|
|
729
|
+
"""Align coords1 and coords2 by minimizing distance between coords[-1] and coords[0]
|
|
730
|
+
|
|
731
|
+
Arguments
|
|
732
|
+
---------
|
|
733
|
+
coords1 [list of (lon,lat) tuples]
|
|
734
|
+
coords2 [list of (lon,lat) tuples]
|
|
735
|
+
|
|
736
|
+
Keyword Arguments
|
|
737
|
+
-----------------
|
|
738
|
+
extend [bool (Default : False)] -- extends coords1 so that its starting point is
|
|
739
|
+
equal to the end point of coords2 and its end point is the starting point of
|
|
740
|
+
coords2.
|
|
741
|
+
|
|
742
|
+
Returns
|
|
743
|
+
-------
|
|
744
|
+
(coords1, coords2)
|
|
745
|
+
|
|
746
|
+
Examples
|
|
747
|
+
--------
|
|
748
|
+
>>> coords1 = [(-100, 0), (-50, 0)]
|
|
749
|
+
>>> coords2 = [( 0, 0), (-40, 0)]
|
|
750
|
+
>>> sec.align_coords(coords1, coords2)
|
|
751
|
+
|
|
752
|
+
"""
|
|
753
|
+
coords_options = [
|
|
754
|
+
[coords1 , coords2 ],
|
|
755
|
+
[coords1[::-1], coords2 ],
|
|
756
|
+
[coords1 , coords2[::-1]],
|
|
757
|
+
[coords1[::-1], coords2[::-1]]
|
|
758
|
+
]
|
|
759
|
+
dists = np.array([
|
|
760
|
+
coord_distance(c1[-1], c2[0])
|
|
761
|
+
for (c1,c2) in coords_options
|
|
762
|
+
])
|
|
763
|
+
coords1, coords2 = coords_options[np.argmin(dists)]
|
|
764
|
+
if extend:
|
|
765
|
+
coords1 = [coords2[-1]] + coords1 + [coords2[0]]
|
|
766
|
+
return coords1, coords2
|
|
767
|
+
|
|
768
|
+
def coord_distance(coord1, coord2):
|
|
769
|
+
"""Spherical distance between coord1 and coord2"""
|
|
770
|
+
return distance_on_unit_sphere(
|
|
771
|
+
coord1[0],
|
|
772
|
+
coord1[1],
|
|
773
|
+
coord2[0],
|
|
774
|
+
coord2[1]
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
def lonlat_from_coords(coords):
|
|
778
|
+
"""Turns list of coordinate pairs into arrays of longitudes and latitudes"""
|
|
779
|
+
return (
|
|
780
|
+
np.array([lon for (lon, lat) in coords]),
|
|
781
|
+
np.array([lat for (lon, lat) in coords])
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
def coords_from_lonlat(lons, lats):
|
|
785
|
+
"""Turns iterable longitudes and latitudes into a list of coordinate pairs"""
|
|
786
|
+
return [(lon, lat) for (lon, lat) in zip(lons, lats)]
|