xbudget 0.5.0__tar.gz

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,26 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Set up Python
13
+ uses: actions/setup-python@v2
14
+ with:
15
+ python-version: '3.x'
16
+ - name: Install dependencies
17
+ run: |
18
+ python -m pip install --upgrade pip
19
+ pip install build twine
20
+ - name: Build package
21
+ run: python -m build
22
+ - name: Publish package
23
+ env:
24
+ TWINE_USERNAME: __token__
25
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
26
+ run: twine upload dist/*
@@ -0,0 +1,2 @@
1
+ __pycache__
2
+ .ipynb_checkpoints
xbudget-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Henri F. Drake
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
xbudget-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: xbudget
3
+ Version: 0.5.0
4
+ Summary: Helper functions and meta-data conventions for wrangling finite-volume ocean model budgets
5
+ Project-URL: Homepage, https://github.com/hdrake/xbudget
6
+ Project-URL: Bugs/Issues/Features, https://github.com/hdrake/xbudget/issues
7
+ Author-email: "Henri F. Drake" <hfdrake@uci.edu>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: numpy
14
+ Requires-Dist: xarray
15
+ Requires-Dist: xgcm>=0.9.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # xbudget
19
+ Helper functions and meta-data conventions for wrangling finite-volume ocean model budgets.
20
+
21
+ Quick Start Guide
22
+ -----------------
23
+
24
+ **For users: minimal installation within an existing environment**
25
+ ```bash
26
+ pip install git+https://github.com/hdrake/xbudget.git@main
27
+ ```
28
+
29
+ **For developers: installing from scratch using `conda`**
30
+ ```bash
31
+ git clone git@github.com:hdrake/xbudget.git
32
+ cd xbudget
33
+ conda env create -f docs/environment.yml
34
+ conda activate docs_env_xbudget
35
+ pip install -e .
36
+ python -m ipykernel install --user --name docs_env_xbudget --display-name "docs_env_xbudget"
37
+ jupyter-lab
38
+ ```
@@ -0,0 +1,21 @@
1
+ # xbudget
2
+ Helper functions and meta-data conventions for wrangling finite-volume ocean model budgets.
3
+
4
+ Quick Start Guide
5
+ -----------------
6
+
7
+ **For users: minimal installation within an existing environment**
8
+ ```bash
9
+ pip install git+https://github.com/hdrake/xbudget.git@main
10
+ ```
11
+
12
+ **For developers: installing from scratch using `conda`**
13
+ ```bash
14
+ git clone git@github.com:hdrake/xbudget.git
15
+ cd xbudget
16
+ conda env create -f docs/environment.yml
17
+ conda activate docs_env_xbudget
18
+ pip install -e .
19
+ python -m ipykernel install --user --name docs_env_xbudget --display-name "docs_env_xbudget"
20
+ jupyter-lab
21
+ ```
@@ -0,0 +1,10 @@
1
+ name: test_env_xbudget
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.8
6
+ - cftime
7
+ - netcdf4
8
+ - pydap
9
+ - pytest
10
+ - pip
@@ -0,0 +1,13 @@
1
+ name: docs_env_xbudget
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python>=3.8
6
+ - cftime
7
+ - ipython
8
+ - jupyterlab
9
+ - matplotlib
10
+ - netcdf4
11
+ - pydap
12
+ - pytest
13
+ - pip
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "xbudget"
3
+ dynamic = ["version"]
4
+ authors = [
5
+ {name="Henri F. Drake", email="hfdrake@uci.edu"},
6
+ ]
7
+ description = "Helper functions and meta-data conventions for wrangling finite-volume ocean model budgets"
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ dependencies = [
16
+ "numpy",
17
+ "xarray",
18
+ "xgcm >= 0.9.0",
19
+ ]
20
+
21
+ [project.urls]
22
+ "Homepage" = "https://github.com/hdrake/xbudget"
23
+ "Bugs/Issues/Features" = "https://github.com/hdrake/xbudget/issues"
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [tool.hatch.metadata]
30
+ allow-direct-references = true
31
+
32
+ [tool.hatch.version]
33
+ path = "xbudget/version.py"
34
+
35
+ [tool.hatch.build]
36
+ exclude = ["examples/**", "data/**"]
@@ -0,0 +1,4 @@
1
+ """ xbudget: xarray and xgcm-based functions for evaluating finite-volume budgets"""
2
+ from .presets import *
3
+ from .collect import *
4
+ from .version import __version__
@@ -0,0 +1,381 @@
1
+ from operator import mul
2
+ from functools import reduce
3
+ import copy
4
+ import numpy as np
5
+ import numbers
6
+ import xarray as xr
7
+ import xgcm
8
+
9
+ import warnings
10
+
11
+ def aggregate(xbudget_dict, decompose=[]):
12
+ """Aggregate xbudget dictionary into simpler root-level budgets.
13
+
14
+ Parameters
15
+ ----------
16
+ xbudget_dict : dictionary in xbudget-compatible format
17
+ decompose : str or list (default: [])
18
+ Name of variable type(s) to decompose into the summed parts
19
+
20
+ Examples
21
+ --------
22
+ >>> xbudget_dict = {
23
+ "heat": {
24
+ "rhs": {
25
+ "sum": {
26
+ "advection": {
27
+ "var":"advective_tendency"
28
+ },
29
+ "var": "heat_rhs_sum"
30
+ },
31
+ "var": "heat_rhs",
32
+ }
33
+ }
34
+ }
35
+ >>> xbudget.aggregate(xbudget_dict)
36
+ {'heat': {'rhs': {'advection': 'advective_tendency'}}}
37
+
38
+ >>>xbudget_dict = {
39
+ "heat": {
40
+ "rhs": {
41
+ "sum": {
42
+ "advection": {
43
+ "var":"advective_tendency",
44
+ "sum": {
45
+ "horizontal": {
46
+ "var":"advective_tendency_h",
47
+ },
48
+ "vertical": {
49
+ "var":"advective_tendency_v"
50
+ },
51
+ "var":"heat_rhs_sum_advection_sum"
52
+ }
53
+ },
54
+ "var": "heat_rhs_sum"
55
+ },
56
+ "var": "heat_rhs",
57
+ }
58
+ }
59
+ }
60
+ >>> xbudget.aggregate(xbudget_dict)
61
+ {'heat': {'rhs': {'advection': 'advective_tendency'}}}
62
+
63
+ >>> xbudget.aggregate(xbudget_dict, decompose="advection")
64
+ {'heat': {'rhs': {'advection_horizontal': 'advective_tendency_h',
65
+ 'advection_vertical': 'advective_tendency_v'}}}
66
+
67
+ See also
68
+ --------
69
+ disaggregate, deep_search, _deep_search
70
+ """
71
+ new_budgets = copy.deepcopy(xbudget_dict)
72
+ for tr, tr_xbudget_dict in xbudget_dict.items():
73
+ for side,terms in tr_xbudget_dict.items():
74
+ if side in ["lhs", "rhs"]:
75
+ new_budgets[tr][side] = deep_search(
76
+ disaggregate(tr_xbudget_dict[side], decompose=decompose)
77
+ )
78
+ return new_budgets
79
+
80
+ def disaggregate(b, decompose=[]):
81
+ """Disaggregate variable's provenance dictionary into summed parts
82
+
83
+ Parameters
84
+ ----------
85
+ b : xbudget sub-dictionary for a variable
86
+ decompose : str or list (default: [])
87
+ Name of variable type(s) to decompose into the summed parts
88
+
89
+ Examples
90
+ --------
91
+ >>> b = {
92
+ "sum": {
93
+ "advection": {
94
+ "var":"advective_tendency",
95
+ "sum": {
96
+ "horizontal": {
97
+ "var":"advective_tendency_h",
98
+ },
99
+ "vertical": {
100
+ "var":"advective_tendency_v"
101
+ },
102
+ "var":"heat_rhs_sum_advection_sum"
103
+ }
104
+ },
105
+ "var": "heat_rhs_sum"
106
+ },
107
+ "var": "heat_rhs",
108
+ }
109
+ >>> {'advection': 'advective_tendency'}
110
+ {'advection': 'advective_tendency'}
111
+
112
+ >>> xbudget.disaggregate(b, decompose="advection")
113
+ {'advection': {'horizontal': 'advective_tendency_h',
114
+ 'vertical': 'advective_tendency_v'}}
115
+
116
+ See also
117
+ --------
118
+ aggregate
119
+ """
120
+ if "sum" in b:
121
+ bsum_novar = {k:v for (k,v) in b["sum"].items() if (k!="var") and (v is not None)}
122
+ sum_dict = dict((k,v["var"]) if ("var" in v) else (k,v) for k,v in bsum_novar.items())
123
+ b_recurse = {}
124
+ for (k,v) in sum_dict.items():
125
+ if k not in decompose:
126
+ b_recurse[k] = v
127
+ else:
128
+ v_dict = disaggregate(b["sum"][k], decompose=decompose)
129
+ if "product" in v_dict.keys():
130
+ b_recurse[k] = v_dict["var"]
131
+ else:
132
+ b_recurse[k] = v_dict
133
+ return b_recurse
134
+ return b
135
+
136
+ def deep_search(b):
137
+ """Utility function for searching for variables in xbudget dictionary.
138
+
139
+ See also
140
+ --------
141
+ aggregate, _deep_search
142
+ """
143
+ return _deep_search(b, new_b={}, k_last=None)
144
+
145
+ def _deep_search(b, new_b={}, k_last=None):
146
+ """Recursive function for searching for variables in xbudget dictionary.
147
+
148
+ See also
149
+ --------
150
+ aggregate, deep_search
151
+ """
152
+ if type(b) is str:
153
+ new_b[k_last] = b
154
+ elif type(b) is dict:
155
+ for (k, v) in b.items():
156
+ if k_last is not None:
157
+ k = f"{k_last}_{k}"
158
+ _deep_search(v, new_b=new_b, k_last=k)
159
+ return new_b
160
+
161
+ def collect_budgets(ds, xbudget_dict):
162
+ """Fills xbudget dictionary with all tracer content tendencies
163
+
164
+ Parameters
165
+ ----------
166
+ ds : xr.Dataset containing budget diagnostics
167
+ xbudget_dict : dictionary in xbudget-compatible format
168
+ Example format:
169
+ >>> xbudget_dict = {
170
+ "heat": {
171
+ "rhs": {
172
+ "sum": {
173
+ "advection": {
174
+ "var":"advective_tendency"
175
+ },
176
+ "var": "heat_rhs_sum"
177
+ },
178
+ "var": "heat_rhs",
179
+ }
180
+ }
181
+ }
182
+ """
183
+ for eq, v in xbudget_dict.items():
184
+ for side in ["lhs", "rhs"]:
185
+ if side in v:
186
+ budget_fill_dict(ds, v[side], f"{eq}_{side}")
187
+
188
+ def budget_fill_dict(data, xbudget_dict, namepath):
189
+ """Recursively fill xbudget dictionary
190
+
191
+ Parameters
192
+ ----------
193
+ data : xgcm.grid or xr.Dataset
194
+ xbudget_dict : dictionary in xbudget-compatible format containing variable in namepath
195
+ namepath : name of variable in dataset (data._ds or data)
196
+ """
197
+ if type(data)==xgcm.grid.Grid:
198
+ grid = data
199
+ ds = grid._ds
200
+ else:
201
+ ds = data
202
+ grid = None
203
+
204
+ var_pref = None
205
+
206
+ if ((xbudget_dict["var"] is not None) and
207
+ (xbudget_dict["var"] in ds) and
208
+ (namepath not in ds)):
209
+ var_rename = ds[xbudget_dict["var"]].rename(namepath)
210
+ var_rename.attrs['provenance'] = xbudget_dict["var"]
211
+ ds[namepath] = ds[xbudget_dict["var"]]
212
+ var_pref = ds[namepath]
213
+
214
+ for k,v in xbudget_dict.items():
215
+ if k in ['sum', 'product']:
216
+ op_list = []
217
+ for k_term, v_term in v.items():
218
+ if isinstance(v_term, dict): # recursive call to get this variable
219
+ v_term_recursive = budget_fill_dict(data, v_term, f"{namepath}_{k}_{k_term}")
220
+ if v_term_recursive is not None:
221
+ op_list.append(v_term_recursive)
222
+ elif isinstance(v_term, numbers.Number):
223
+ op_list.append(v_term)
224
+ elif isinstance(v_term, str):
225
+ if v_term in ds:
226
+ op_list.append(ds[v_term])
227
+ else:
228
+ warnings.warn(f"Variable {v_term} is missing from the dataset `ds`, so it is being skipped. To suppress this warning, remove {v_term} from the `xbudget_dict`.")
229
+ if k=="product":
230
+ op_list.append(0.)
231
+
232
+ # Compute variable from sum or product operation
233
+ if (
234
+ (len(op_list) == 0) |
235
+ all([e is None for e in op_list]) |
236
+ any([e is None for e in op_list])
237
+ ):
238
+ return None
239
+ else:
240
+ var = sum(op_list) if k=="sum" else reduce(mul, op_list, 1)
241
+ if not isinstance(var, xr.DataArray):
242
+ continue
243
+
244
+ # Variable metadata
245
+ var_name = f"{namepath}_{k}"
246
+ var = var.rename(var_name)
247
+ var_provenance = [o.name if isinstance(o, xr.DataArray) else o for o in op_list]
248
+ var.attrs["provenance"] = var_provenance
249
+ ds[var_name] = var
250
+ if (xbudget_dict[k]["var"] is None):
251
+ xbudget_dict[k]["var"] = var_name
252
+
253
+ if (xbudget_dict["var"] is None):
254
+ var_copy = var.copy()
255
+ var_copy.attrs["provenance"] = var_name
256
+ xbudget_dict["var"] = namepath
257
+ if namepath not in ds:
258
+ ds[namepath] = var_copy
259
+
260
+ # keep record of the first-listed variable
261
+ if var_pref is None:
262
+ var_pref = var.copy()
263
+
264
+ if k == "difference":
265
+ if grid is not None:
266
+ staggered_axes = {
267
+ axn:c for axn,ax in grid.axes.items()
268
+ for pos,c in ax.coords.items()
269
+ if pos!="center"
270
+ }
271
+ v_term = [v_term for k_term,v_term in v.items() if k_term!="var"][0]
272
+ if v_term not in ds:
273
+ warnings.warn(f"Variable {v_term} is missing from the dataset `ds`, so it is being skipped. To suppress this warning, remove {v_term} from the `xbudget_dict`.")
274
+ continue
275
+ candidate_axes = [axn for (axn,c) in staggered_axes.items() if c in ds[v_term].dims]
276
+ if len(candidate_axes) == 1:
277
+ axis = candidate_axes[0]
278
+ else:
279
+ raise ValueError("Flux difference inconsistent with finite volume discretization.")
280
+ var = grid.diff(ds[v_term].fillna(0.), axis)
281
+ var_name = f"{namepath}_difference"
282
+ var = var.rename(var_name)
283
+ var_provenance = v_term
284
+ var.attrs["provenance"] = var_provenance
285
+ ds[var_name] = var
286
+ if var_pref is None:
287
+ var_pref = var.copy()
288
+ else:
289
+ raise ValueError("Input `ds` must be `xgcm.Grid` instance if using `difference` operations.")
290
+
291
+ return var_pref
292
+
293
+ def get_vars(xbudget_dict, terms):
294
+ """Get xbudget sub-dictionaries for specified terms.
295
+
296
+ Parameters
297
+ ----------
298
+ xbudget_dict : dictionary in xbudget-compatible format
299
+ terms : str or list of str
300
+
301
+ Examples
302
+ -------
303
+ >>> xbudget_dict = {
304
+ "heat": {
305
+ "rhs": {
306
+ "sum": {
307
+ "advection": {
308
+ "var":"advective_tendency"
309
+ },
310
+ "var": "heat_rhs_sum"
311
+ },
312
+ "var": "heat_rhs",
313
+ }
314
+ }
315
+ }
316
+ >>> xbudget.get_vars(xbudget_dict, "heat_rhs_sum")
317
+ {'var': 'heat_rhs_sum', 'sum': ['advective_tendency']}
318
+ """
319
+ return _get_vars(xbudget_dict, terms)
320
+
321
+ def _get_vars(b, terms, k_long=""):
322
+ """Recursive version of _get_vars for determining variable provenance tree.
323
+
324
+ Parameters
325
+ ----------
326
+ b : dictionary
327
+ terms : str or list of str
328
+ k_long : variable name suffix
329
+
330
+ See also
331
+ --------
332
+ get_vars
333
+ """
334
+ if isinstance(terms, (list, np.ndarray)):
335
+ return [_get_vars(b, term) for term in terms]
336
+ elif type(terms) is str:
337
+ for k,v in b.items():
338
+ if type(v) is str:
339
+ k_short = k_long.replace("_sum", "").replace("_product", "")
340
+ if v==terms:
341
+ decomps = {"var": v}
342
+ if len(terms) > len("_sum"):
343
+ if (terms[-len("_sum"):] == "_sum") and ("sum" in b):
344
+ ts = {kk:vv for (kk,vv) in b["sum"].items() if kk!="var"}
345
+ decomps["sum"] = [vv["var"] if type(vv) is dict else vv for (kk,vv) in ts.items()]
346
+ elif (terms[-len("_sum"):] == "_sum"):
347
+ ts = {kk:vv for (kk,vv) in b.items() if kk!="var"}
348
+ decomps["sum"] = [vv["var"] if type(vv) is dict else vv for (kk,vv) in ts.items()]
349
+ if len(terms) > len("_product"):
350
+ if (terms[-len("_product"):] == "_product") and ("product" in b):
351
+ ts = {kk:vv for (kk,vv) in b["product"].items() if kk!="var"}
352
+ decomps["product"] = [vv["var"] if type(vv) is dict else vv for (kk,vv) in ts.items()]
353
+ elif (terms[-len("_product"):] == "_product"):
354
+ ts = {kk:vv for (kk,vv) in b.items() if kk!="var"}
355
+ decomps["product"] = [vv["var"] if type(vv) is dict else vv for (kk,vv) in ts.items()]
356
+ return decomps
357
+
358
+ if k!="var":
359
+ k_short+="_"+k
360
+ if k_short==terms:
361
+ return v
362
+ elif type(v) is dict:
363
+ if k_long=="":
364
+ new_k = k
365
+ elif len(k_long)>0:
366
+ new_k = f"{k_long}_{k}"
367
+ var = _get_vars(v, terms, k_long=new_k)
368
+ if var is not None:
369
+ return var
370
+
371
+ def flatten(container):
372
+ for i in container:
373
+ if isinstance(i, (list,tuple)):
374
+ for j in flatten(i):
375
+ yield j
376
+ else:
377
+ yield i
378
+
379
+ def flatten_lol(lol):
380
+ """Flatten a list of lists into a single list."""
381
+ return list(flatten(lol))