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.
@@ -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
@@ -0,0 +1,3 @@
1
+ """sectionate: version information"""
2
+
3
+ __version__ = "0.3.0"