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/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)]