PyFishPack 0.1.0__cp310-cp310-win_amd64.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.
- PyFishPack/__init__.py +86 -0
- PyFishPack/__pycache__/__init__.cpython-310.pyc +0 -0
- PyFishPack/__pycache__/apps.cpython-310.pyc +0 -0
- PyFishPack/_dummy.c +23 -0
- PyFishPack/_dummy.cp310-win_amd64.pyd +0 -0
- PyFishPack/apps.py +3640 -0
- PyFishPack/fishpack.cp310-win_amd64.dll.a +0 -0
- PyFishPack/fishpack.cp310-win_amd64.pyd +0 -0
- PyFishPack/meson.build +213 -0
- PyFishPack/src/archive/f77/Makefile +19 -0
- PyFishPack/src/archive/f77/blktri.f +1404 -0
- PyFishPack/src/archive/f77/cblktri.f +1414 -0
- PyFishPack/src/archive/f77/cmgnbn.f +1592 -0
- PyFishPack/src/archive/f77/comf.f +186 -0
- PyFishPack/src/archive/f77/fftpack.f +2968 -0
- PyFishPack/src/archive/f77/genbun.f +1335 -0
- PyFishPack/src/archive/f77/gnbnaux.f +314 -0
- PyFishPack/src/archive/f77/hstcrt.f +443 -0
- PyFishPack/src/archive/f77/hstcsp.f +683 -0
- PyFishPack/src/archive/f77/hstcyl.f +485 -0
- PyFishPack/src/archive/f77/hstplr.f +538 -0
- PyFishPack/src/archive/f77/hstssp.f +634 -0
- PyFishPack/src/archive/f77/hw3crt.f +687 -0
- PyFishPack/src/archive/f77/hwscrt.f +512 -0
- PyFishPack/src/archive/f77/hwscsp.f +728 -0
- PyFishPack/src/archive/f77/hwscyl.f +538 -0
- PyFishPack/src/archive/f77/hwsplr.f +602 -0
- PyFishPack/src/archive/f77/hwsssp.f +780 -0
- PyFishPack/src/archive/f77/pois3d.f +550 -0
- PyFishPack/src/archive/f77/poistg.f +875 -0
- PyFishPack/src/archive/f77/sepaux.f +361 -0
- PyFishPack/src/archive/f77/sepeli.f +1029 -0
- PyFishPack/src/archive/f77/sepx4.f +958 -0
- PyFishPack/src/centered_axisymmetric_spherical_solver.f90 +1002 -0
- PyFishPack/src/centered_cartesian_helmholtz_solver_3d.f90 +819 -0
- PyFishPack/src/centered_cartesian_solver.f90 +583 -0
- PyFishPack/src/centered_cylindrical_solver.f90 +634 -0
- PyFishPack/src/centered_helmholtz_solvers.f90 +156 -0
- PyFishPack/src/centered_polar_solver.f90 +746 -0
- PyFishPack/src/centered_real_linear_systems_solver.f90 +280 -0
- PyFishPack/src/centered_spherical_solver.f90 +928 -0
- PyFishPack/src/complex_block_tridiagonal_linear_systems_solver.f90 +1947 -0
- PyFishPack/src/complex_linear_systems_solver.f90 +1787 -0
- PyFishPack/src/fftpack_c_api.f90 +86 -0
- PyFishPack/src/fishpack.f90 +191 -0
- PyFishPack/src/fishpack.pyf +504 -0
- PyFishPack/src/fishpack_c_api.f90 +365 -0
- PyFishPack/src/fishpack_original.pyf +2119 -0
- PyFishPack/src/fishpack_precision.f90 +53 -0
- PyFishPack/src/general_linear_systems_solver_3d.f90 +296 -0
- PyFishPack/src/iterative_solvers.f90 +969 -0
- PyFishPack/src/main.f90 +10 -0
- PyFishPack/src/pyfishpack_module.c +1302 -0
- PyFishPack/src/real_block_tridiagonal_linear_systems_solver.f90 +319 -0
- PyFishPack/src/sepeli.f90 +1454 -0
- PyFishPack/src/sepx4.f90 +1338 -0
- PyFishPack/src/staggered_axisymmetric_spherical_solver.f90 +908 -0
- PyFishPack/src/staggered_cartesian_solver.f90 +553 -0
- PyFishPack/src/staggered_cylindrical_solver.f90 +630 -0
- PyFishPack/src/staggered_helmholtz_solvers.f90 +172 -0
- PyFishPack/src/staggered_polar_solver.f90 +651 -0
- PyFishPack/src/staggered_real_linear_systems_solver.f90 +258 -0
- PyFishPack/src/staggered_spherical_solver.f90 +758 -0
- PyFishPack/src/three_dimensional_solvers.f90 +602 -0
- PyFishPack/src/type_CenteredCyclicReductionUtility.f90 +1714 -0
- PyFishPack/src/type_CyclicReductionUtility.f90 +472 -0
- PyFishPack/src/type_FishpackWorkspace.f90 +290 -0
- PyFishPack/src/type_GeneralizedCyclicReductionUtility.f90 +1980 -0
- PyFishPack/src/type_PeriodicFastFourierTransform.f90 +3789 -0
- PyFishPack/src/type_SepAux.f90 +586 -0
- PyFishPack/src/type_StaggeredCyclicReductionUtility.f90 +893 -0
- pyfishpack-0.1.0.dist-info/DELVEWHEEL +2 -0
- pyfishpack-0.1.0.dist-info/METADATA +81 -0
- pyfishpack-0.1.0.dist-info/RECORD +81 -0
- pyfishpack-0.1.0.dist-info/WHEEL +5 -0
- pyfishpack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyfishpack-0.1.0.dist-info/top_level.txt +1 -0
- pyfishpack.libs/libgcc_s_seh-1-25d59ccffa1a9009644065b069829e07.dll +0 -0
- pyfishpack.libs/libgfortran-5-08f2195cfa0d823e13371c5c3186a82a.dll +0 -0
- pyfishpack.libs/libquadmath-0-c5abb9113f1ee64b87a889958e4b7418.dll +0 -0
- pyfishpack.libs/libwinpthread-1-83908d14abfafb8b3bfa38cf51ecee56.dll +0 -0
PyFishPack/apps.py
ADDED
|
@@ -0,0 +1,3640 @@
|
|
|
1
|
+
"""Equation-level inversion helpers built on the compiled Fishpack backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from . import fishpack
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_SUPPORTED_BCS = {"fixed", "periodic"}
|
|
14
|
+
_SUPPORTED_SOR_BCS = {"fixed", "extend", "periodic"}
|
|
15
|
+
_UNDEF = -9.99e8
|
|
16
|
+
_DEFAULT_MPARAMS = {
|
|
17
|
+
"f0": 1e-5,
|
|
18
|
+
"beta": 2e-11,
|
|
19
|
+
"N2": 2e-4,
|
|
20
|
+
"D": 100.0,
|
|
21
|
+
"depth": 100.0,
|
|
22
|
+
"lambda": 1e-8,
|
|
23
|
+
"c0": 8e-9,
|
|
24
|
+
"c1": 8e-5,
|
|
25
|
+
"A": 1.0,
|
|
26
|
+
"B": 0.0,
|
|
27
|
+
"C": 1.0,
|
|
28
|
+
"R": 1.0,
|
|
29
|
+
"A4": 0.0,
|
|
30
|
+
"rho0": 1025.0,
|
|
31
|
+
"epsilon": 1e-5,
|
|
32
|
+
"Phi": 1.0,
|
|
33
|
+
"k": 1.0,
|
|
34
|
+
"ang0": 2e5,
|
|
35
|
+
"Gamma": 1.0,
|
|
36
|
+
"M0": 0.0,
|
|
37
|
+
"C0": 1.0,
|
|
38
|
+
"Rearth": 6371200.0,
|
|
39
|
+
"Omega": 7.292e-5,
|
|
40
|
+
"g": 9.80665,
|
|
41
|
+
}
|
|
42
|
+
_DEFAULT_IPARAMS = {
|
|
43
|
+
"BCs": ("fixed", "fixed"),
|
|
44
|
+
"undef": np.nan,
|
|
45
|
+
"mxLoop": 5000,
|
|
46
|
+
"tolerance": 1e-8,
|
|
47
|
+
"optArg": None,
|
|
48
|
+
"printInfo": False,
|
|
49
|
+
"debug": False,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def invert_Poisson(
|
|
54
|
+
F: Any,
|
|
55
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
56
|
+
coords: str = "cartesian",
|
|
57
|
+
icbc: Any = None,
|
|
58
|
+
mParams: dict[str, Any] | None = None,
|
|
59
|
+
iParams: dict[str, Any] | None = None,
|
|
60
|
+
*,
|
|
61
|
+
BCs: Sequence[str] | None = None,
|
|
62
|
+
spacing: Sequence[float] | None = None,
|
|
63
|
+
raise_on_error: bool = True,
|
|
64
|
+
) -> Any:
|
|
65
|
+
r"""Invert the Cartesian Poisson equation on a uniform grid.
|
|
66
|
+
|
|
67
|
+
This is the equation-level xinvert-style wrapper around the modern Fortran
|
|
68
|
+
Fishpack ``genbun`` backend. It supports only Cartesian, uniform-grid,
|
|
69
|
+
constant-coefficient Poisson solves. Lat-lon, variable-coefficient, and
|
|
70
|
+
other non-Cartesian formulations are not supported.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
F : array-like or xarray.DataArray
|
|
75
|
+
Forcing field to invert. NumPy inputs are solved along ``dims`` as
|
|
76
|
+
axis indices; xarray inputs use named dimensions.
|
|
77
|
+
dims : sequence of str or int, optional
|
|
78
|
+
Two inversion dimensions. Defaults to the last two dimensions.
|
|
79
|
+
coords : str, default "cartesian"
|
|
80
|
+
Coordinate system. Only ``"cartesian"`` is supported.
|
|
81
|
+
icbc : any, optional
|
|
82
|
+
Accepted for xinvert-style compatibility and ignored.
|
|
83
|
+
mParams, iParams : dict, optional
|
|
84
|
+
Compatibility mappings for equation and inversion parameters. Boundary
|
|
85
|
+
conditions may be supplied through ``iParams["BCs"]`` when ``BCs`` is
|
|
86
|
+
not passed explicitly.
|
|
87
|
+
BCs : sequence of {"fixed", "periodic"}, optional
|
|
88
|
+
Boundary conditions for the two inversion dimensions. Defaults to
|
|
89
|
+
``("fixed", "fixed")``.
|
|
90
|
+
spacing : sequence of float, optional
|
|
91
|
+
Grid spacing ``(dy, dx)`` for the inversion dimensions. Defaults to
|
|
92
|
+
unit spacing.
|
|
93
|
+
raise_on_error : bool, default True
|
|
94
|
+
Raise ``RuntimeError`` if Fishpack reports a nonzero solver error code.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
array-like or xarray.DataArray
|
|
99
|
+
The inverted field with the same array type as the input.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
del icbc, mParams # Kept for API compatibility with xinvert-style calls.
|
|
103
|
+
|
|
104
|
+
if coords != "cartesian":
|
|
105
|
+
raise NotImplementedError(
|
|
106
|
+
"invert_Poisson currently supports uniform Cartesian coordinates only"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
params = dict(iParams or {})
|
|
110
|
+
bcs = _normalize_bcs(BCs if BCs is not None else params.get("BCs", None))
|
|
111
|
+
|
|
112
|
+
if _is_dataarray(F):
|
|
113
|
+
return _invert_poisson_labeled(
|
|
114
|
+
F, dims=dims, bcs=bcs, spacing=spacing, raise_on_error=raise_on_error
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return _invert_poisson_ndarray(
|
|
118
|
+
F, axes=dims, bcs=bcs, spacing=spacing, raise_on_error=raise_on_error
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def invert_RefState(
|
|
123
|
+
PV: Any,
|
|
124
|
+
dims: Sequence[str] | None,
|
|
125
|
+
coords: str = "z-lat",
|
|
126
|
+
icbc: Any = None,
|
|
127
|
+
mParams: dict[str, Any] | None = None,
|
|
128
|
+
iParams: dict[str, Any] | None = None,
|
|
129
|
+
*,
|
|
130
|
+
BCs: Sequence[str] | None = None,
|
|
131
|
+
) -> Any:
|
|
132
|
+
r"""Invert xinvert's balanced symmetric-vortex reference-state equation.
|
|
133
|
+
|
|
134
|
+
The solve is backed by the modern Fortran SOR kernel exposed as
|
|
135
|
+
``fishpack.sor_standard2d``. The wrapper follows xinvert's coefficient
|
|
136
|
+
construction for the ``"z-lat"`` and ``"cartesian"`` coordinate forms and
|
|
137
|
+
preserves xarray coordinates and metadata.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
PV : xarray.DataArray
|
|
142
|
+
Two-dimensional potential-vorticity distribution, optionally with
|
|
143
|
+
non-core dimensions.
|
|
144
|
+
dims : sequence of str
|
|
145
|
+
Two inversion dimensions. For ``"z-lat"``, the second dimension is
|
|
146
|
+
interpreted as latitude in degrees. For ``"cartesian"``, the second
|
|
147
|
+
dimension is interpreted as radius.
|
|
148
|
+
coords : {"z-lat", "cartesian"}, default "z-lat"
|
|
149
|
+
Coordinate form used for xinvert-compatible coefficients.
|
|
150
|
+
icbc : xarray.DataArray, optional
|
|
151
|
+
Initial guess and fixed boundary values.
|
|
152
|
+
mParams : dict, optional
|
|
153
|
+
Model parameters. Uses ``ang0``, ``Gamma``, ``g``, and ``Rearth``.
|
|
154
|
+
iParams : dict, optional
|
|
155
|
+
Iteration parameters including ``BCs``, ``undef``, ``mxLoop``,
|
|
156
|
+
``tolerance``, and optional ``optArg``.
|
|
157
|
+
BCs : sequence of {"fixed", "extend", "periodic"}, optional
|
|
158
|
+
Boundary conditions for the two inversion dimensions.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
xarray.DataArray
|
|
163
|
+
Inverted angular-momentum field named ``"inverted"``.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
_require_dataarray(PV, "invert_RefState")
|
|
167
|
+
if dims is None or len(dims) != 2:
|
|
168
|
+
raise ValueError("invert_RefState requires two xarray dimension names")
|
|
169
|
+
if coords.lower() not in {"z-lat", "cartesian"}:
|
|
170
|
+
raise NotImplementedError("invert_RefState supports only 'z-lat' and 'cartesian'")
|
|
171
|
+
|
|
172
|
+
params = _merged_mparams(mParams)
|
|
173
|
+
iparams = _merged_iparams(iParams, ndim=2, BCs=BCs)
|
|
174
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 2)
|
|
175
|
+
mask_f, init_s, zero = _mask_labeled_field(PV, dims, iparams, bcs, icbc)
|
|
176
|
+
ydim, xdim = dims
|
|
177
|
+
xcoord = mask_f.coords[xdim]
|
|
178
|
+
|
|
179
|
+
if coords.lower() == "z-lat":
|
|
180
|
+
acoef = zero + np.sin(np.deg2rad(xcoord))
|
|
181
|
+
ccoef = zero + params["Gamma"] * float(params["g"]) / mask_f / xcoord
|
|
182
|
+
dy, dx = _sor_spacing2d(mask_f, (ydim, xdim), coords, float(params["Rearth"]))
|
|
183
|
+
else:
|
|
184
|
+
acoef = zero + 2.0 * params["ang0"] / (xcoord**3.0)
|
|
185
|
+
ccoef = zero + params["Gamma"] * float(params["g"]) / mask_f / xcoord
|
|
186
|
+
dy, dx = _sor_spacing2d(mask_f, (ydim, xdim), coords, float(params["Rearth"]))
|
|
187
|
+
|
|
188
|
+
bcoef = zero
|
|
189
|
+
solved = _solve_sor2d_labeled(
|
|
190
|
+
init_s,
|
|
191
|
+
acoef,
|
|
192
|
+
bcoef,
|
|
193
|
+
ccoef,
|
|
194
|
+
mask_f,
|
|
195
|
+
dims=dims,
|
|
196
|
+
dy=dy,
|
|
197
|
+
dx=dx,
|
|
198
|
+
bcs=bcs,
|
|
199
|
+
iparams=iparams,
|
|
200
|
+
)
|
|
201
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def invert_RefStateSWM(
|
|
205
|
+
Q: Any,
|
|
206
|
+
dims: Sequence[str] | None,
|
|
207
|
+
coords: str = "lat",
|
|
208
|
+
icbc: Any = None,
|
|
209
|
+
mParams: dict[str, Any] | None = None,
|
|
210
|
+
iParams: dict[str, Any] | None = None,
|
|
211
|
+
*,
|
|
212
|
+
BCs: Sequence[str] | None = None,
|
|
213
|
+
) -> Any:
|
|
214
|
+
r"""Invert xinvert's one-dimensional shallow-water reference state.
|
|
215
|
+
|
|
216
|
+
The variable-coefficient 1-D solve is performed by the modern Fortran SOR
|
|
217
|
+
kernel ``fishpack.sor_standard1d``. Coefficients follow xinvert's
|
|
218
|
+
``invert_RefStateSWM`` construction for latitude coordinates.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
Q : xarray.DataArray
|
|
223
|
+
Potential-vorticity contour field with one inversion dimension.
|
|
224
|
+
dims : sequence of str
|
|
225
|
+
Single latitude dimension.
|
|
226
|
+
coords : {"lat"}, default "lat"
|
|
227
|
+
Coordinate form. Only xinvert's latitude form is supported.
|
|
228
|
+
icbc : xarray.DataArray, optional
|
|
229
|
+
Initial guess and fixed boundary values.
|
|
230
|
+
mParams : dict, optional
|
|
231
|
+
Model parameters. Uses ``M0``, ``C0``, ``g``, ``Rearth``, and
|
|
232
|
+
``Omega``.
|
|
233
|
+
iParams : dict, optional
|
|
234
|
+
Iteration parameters including ``BCs``, ``undef``, ``mxLoop``,
|
|
235
|
+
``tolerance``, and optional ``optArg``.
|
|
236
|
+
BCs : sequence of {"fixed", "extend", "periodic"}, optional
|
|
237
|
+
Boundary condition for the latitude dimension.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
xarray.DataArray
|
|
242
|
+
Inverted mass-correction field named ``"inverted"``.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
_require_dataarray(Q, "invert_RefStateSWM")
|
|
246
|
+
if dims is None or len(dims) != 1:
|
|
247
|
+
raise ValueError("invert_RefStateSWM requires one xarray dimension name")
|
|
248
|
+
if coords.lower() != "lat":
|
|
249
|
+
raise NotImplementedError("invert_RefStateSWM supports only 'lat' coordinates")
|
|
250
|
+
|
|
251
|
+
params = _merged_mparams(mParams)
|
|
252
|
+
iparams = _merged_iparams(iParams, ndim=1, BCs=BCs)
|
|
253
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 1)
|
|
254
|
+
mask_f, init_s, zero = _mask_labeled_field(Q, dims, iparams, bcs, icbc)
|
|
255
|
+
dim = dims[0]
|
|
256
|
+
lat = np.deg2rad(mask_f.coords[dim])
|
|
257
|
+
cos_g = np.cos(lat)
|
|
258
|
+
sin_g = np.sin(lat)
|
|
259
|
+
cos_h = np.cos((lat + lat.shift({dim: 1})) / 2.0)
|
|
260
|
+
asin = float(params["Rearth"]) * sin_g
|
|
261
|
+
acos = float(params["Rearth"]) * cos_g
|
|
262
|
+
acos = acos.where(acos >= 0.0, other=-acos * 0.1)
|
|
263
|
+
|
|
264
|
+
m0 = _as_labeled_like(params["M0"], mask_f)
|
|
265
|
+
c0 = _as_labeled_like(params["C0"], mask_f)
|
|
266
|
+
delx = _sor_spacing1d(mask_f, dim, coords, float(params["Rearth"]))
|
|
267
|
+
diff = _second_diff_swm(m0, cos_h, delx, dim)
|
|
268
|
+
|
|
269
|
+
acoef = zero + 1.0 / cos_h
|
|
270
|
+
bcoef = zero - c0 * mask_f * asin / (np.pi * float(params["g"]) * acos**3.0)
|
|
271
|
+
force = (
|
|
272
|
+
zero
|
|
273
|
+
- (asin * c0**2.0 / (2.0 * np.pi * float(params["g"]) * acos**3.0))
|
|
274
|
+
+ (2.0 * np.pi * float(params["Omega"]) ** 2.0 * asin * acos) / float(params["g"])
|
|
275
|
+
- diff
|
|
276
|
+
)
|
|
277
|
+
solved = _solve_sor1d_labeled(
|
|
278
|
+
init_s,
|
|
279
|
+
acoef,
|
|
280
|
+
bcoef,
|
|
281
|
+
force,
|
|
282
|
+
dims=dims,
|
|
283
|
+
dx=delx,
|
|
284
|
+
bcs=bcs,
|
|
285
|
+
iparams=iparams,
|
|
286
|
+
)
|
|
287
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def invert_geostrophic(
|
|
291
|
+
lapPhi: Any,
|
|
292
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
293
|
+
coords: str = "cartesian",
|
|
294
|
+
icbc: Any = None,
|
|
295
|
+
mParams: dict[str, Any] | None = None,
|
|
296
|
+
iParams: dict[str, Any] | None = None,
|
|
297
|
+
*,
|
|
298
|
+
BCs: Sequence[str] | None = None,
|
|
299
|
+
spacing: Sequence[float] | None = None,
|
|
300
|
+
raise_on_error: bool = True,
|
|
301
|
+
) -> Any:
|
|
302
|
+
r"""Invert the Cartesian geostrophic balance equation.
|
|
303
|
+
|
|
304
|
+
The constant-Coriolis Cartesian subset uses the direct modern Fortran
|
|
305
|
+
Fishpack path. Cartesian beta-plane inputs use the modern Fortran
|
|
306
|
+
``sor_standard2d`` backend with xinvert-style flux coefficients. Lat-lon
|
|
307
|
+
formulations remain unsupported.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
lapPhi : array-like or xarray.DataArray
|
|
312
|
+
Laplacian forcing field to divide by ``f0`` before the 2-D inversion.
|
|
313
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
314
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``f0`` and
|
|
315
|
+
``beta``; the direct ``beta = 0`` path requires nonzero ``f0``.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
array-like or xarray.DataArray
|
|
320
|
+
The geostrophic streamfunction or velocity potential field.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
if coords != "cartesian":
|
|
324
|
+
raise NotImplementedError(
|
|
325
|
+
"invert_geostrophic currently supports Cartesian coordinates only"
|
|
326
|
+
)
|
|
327
|
+
params = _merged_mparams(mParams)
|
|
328
|
+
beta = _scalar_param(params, "beta")
|
|
329
|
+
f0 = _scalar_param(params, "f0")
|
|
330
|
+
if beta != 0.0:
|
|
331
|
+
return _invert_standard_2d(
|
|
332
|
+
lapPhi,
|
|
333
|
+
dims=dims,
|
|
334
|
+
coords=coords,
|
|
335
|
+
iParams=iParams,
|
|
336
|
+
BCs=BCs,
|
|
337
|
+
spacing=spacing,
|
|
338
|
+
icbc=icbc,
|
|
339
|
+
coefficients=_cartesian_geostrophic_coefficients(
|
|
340
|
+
lapPhi,
|
|
341
|
+
dims=dims,
|
|
342
|
+
spacing=spacing,
|
|
343
|
+
f0=f0,
|
|
344
|
+
beta=beta,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
if f0 == 0.0:
|
|
348
|
+
raise ValueError("f0 must be non-zero")
|
|
349
|
+
return _invert_constant_helmholtz(
|
|
350
|
+
np.asarray(lapPhi, dtype=np.float64) / f0 if not _is_dataarray(lapPhi) else lapPhi / f0,
|
|
351
|
+
dims=dims,
|
|
352
|
+
coords=coords,
|
|
353
|
+
iParams=iParams,
|
|
354
|
+
BCs=BCs,
|
|
355
|
+
spacing=spacing,
|
|
356
|
+
helmholtz=0.0,
|
|
357
|
+
raise_on_error=raise_on_error,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def invert_PV2D(
|
|
362
|
+
PV: Any,
|
|
363
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
364
|
+
coords: str = "cartesian",
|
|
365
|
+
icbc: Any = None,
|
|
366
|
+
mParams: dict[str, Any] | None = None,
|
|
367
|
+
iParams: dict[str, Any] | None = None,
|
|
368
|
+
*,
|
|
369
|
+
BCs: Sequence[str] | None = None,
|
|
370
|
+
spacing: Sequence[float] | None = None,
|
|
371
|
+
raise_on_error: bool = True,
|
|
372
|
+
) -> Any:
|
|
373
|
+
r"""Invert the Cartesian scalar-``N2`` QG potential-vorticity equation.
|
|
374
|
+
|
|
375
|
+
Only the Cartesian, uniform-grid, constant-coefficient subset is
|
|
376
|
+
supported.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
PV : array-like or xarray.DataArray
|
|
381
|
+
Potential-vorticity forcing field.
|
|
382
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
383
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``f0`` and
|
|
384
|
+
``N2``; ``N2`` must be a positive scalar.
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
array-like or xarray.DataArray
|
|
389
|
+
The inverted field with the same array type as the input.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
del icbc
|
|
393
|
+
if coords != "cartesian":
|
|
394
|
+
raise NotImplementedError("invert_PV2D currently supports Cartesian coordinates only")
|
|
395
|
+
params = _merged_mparams(mParams)
|
|
396
|
+
f0 = _scalar_param(params, "f0")
|
|
397
|
+
n2 = _scalar_param(params, "N2")
|
|
398
|
+
if n2 <= 0.0:
|
|
399
|
+
raise ValueError("N2 must be a positive scalar")
|
|
400
|
+
return _invert_constant_2d(
|
|
401
|
+
PV,
|
|
402
|
+
dims=dims,
|
|
403
|
+
coords=coords,
|
|
404
|
+
iParams=iParams,
|
|
405
|
+
BCs=BCs,
|
|
406
|
+
spacing=spacing,
|
|
407
|
+
coefficients=(f0 * f0 / n2, 1.0),
|
|
408
|
+
helmholtz=0.0,
|
|
409
|
+
raise_on_error=raise_on_error,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def invert_Eliassen(
|
|
414
|
+
F: Any,
|
|
415
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
416
|
+
coords: str = "cartesian",
|
|
417
|
+
icbc: Any = None,
|
|
418
|
+
mParams: dict[str, Any] | None = None,
|
|
419
|
+
iParams: dict[str, Any] | None = None,
|
|
420
|
+
*,
|
|
421
|
+
BCs: Sequence[str] | None = None,
|
|
422
|
+
spacing: Sequence[float] | None = None,
|
|
423
|
+
raise_on_error: bool = True,
|
|
424
|
+
) -> Any:
|
|
425
|
+
r"""Invert the Cartesian Eliassen equation.
|
|
426
|
+
|
|
427
|
+
Cartesian inputs are supported for both NumPy arrays and
|
|
428
|
+
:class:`xarray.DataArray` objects. Non-Cartesian formulations remain
|
|
429
|
+
unsupported. The separable ``B = 0`` case uses the direct Fishpack /
|
|
430
|
+
``genbun`` path, while constant-coefficient ``B != 0`` cases route to the
|
|
431
|
+
modern Fortran ``sor_general2d`` backend for the equivalent
|
|
432
|
+
cross-derivative form.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
F : array-like or xarray.DataArray
|
|
437
|
+
Forcing field to invert.
|
|
438
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
439
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``A``, ``B``, and
|
|
440
|
+
``C``.
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
array-like or xarray.DataArray
|
|
445
|
+
The inverted field with the same array type as the input.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
if coords != "cartesian":
|
|
449
|
+
raise NotImplementedError("invert_Eliassen currently supports Cartesian coordinates only")
|
|
450
|
+
params = _merged_mparams(mParams)
|
|
451
|
+
cross = _scalar_param(params, "B")
|
|
452
|
+
if cross != 0.0:
|
|
453
|
+
return _invert_general_2d(
|
|
454
|
+
F,
|
|
455
|
+
dims=dims,
|
|
456
|
+
coords=coords,
|
|
457
|
+
iParams=iParams,
|
|
458
|
+
BCs=BCs,
|
|
459
|
+
spacing=spacing,
|
|
460
|
+
icbc=icbc,
|
|
461
|
+
coefficients=(
|
|
462
|
+
_scalar_param(params, "A"),
|
|
463
|
+
2.0 * cross,
|
|
464
|
+
_scalar_param(params, "C"),
|
|
465
|
+
0.0,
|
|
466
|
+
0.0,
|
|
467
|
+
0.0,
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
return _invert_constant_2d(
|
|
471
|
+
F,
|
|
472
|
+
dims=dims,
|
|
473
|
+
coords=coords,
|
|
474
|
+
iParams=iParams,
|
|
475
|
+
BCs=BCs,
|
|
476
|
+
spacing=spacing,
|
|
477
|
+
coefficients=(_scalar_param(params, "A"), _scalar_param(params, "C")),
|
|
478
|
+
helmholtz=0.0,
|
|
479
|
+
raise_on_error=raise_on_error,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def invert_Fofonoff(
|
|
484
|
+
F: Any,
|
|
485
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
486
|
+
coords: str = "cartesian",
|
|
487
|
+
icbc: Any = None,
|
|
488
|
+
mParams: dict[str, Any] | None = None,
|
|
489
|
+
iParams: dict[str, Any] | None = None,
|
|
490
|
+
*,
|
|
491
|
+
BCs: Sequence[str] | None = None,
|
|
492
|
+
spacing: Sequence[float] | None = None,
|
|
493
|
+
raise_on_error: bool = True,
|
|
494
|
+
) -> Any:
|
|
495
|
+
r"""Invert the Cartesian Fofonoff Helmholtz equation.
|
|
496
|
+
|
|
497
|
+
Cartesian uniform-grid inputs are backed by the modern Fortran Fishpack
|
|
498
|
+
Helmholtz path. ``beta = 0`` uses a constant right-hand side; beta-plane
|
|
499
|
+
inputs use the Cartesian y coordinate from xarray coordinates or, for
|
|
500
|
+
NumPy arrays, from ``dims`` and ``spacing``.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
F : array-like or xarray.DataArray
|
|
505
|
+
Forcing field to invert.
|
|
506
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
507
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``f0``, ``beta``,
|
|
508
|
+
``c0``, and ``c1``.
|
|
509
|
+
|
|
510
|
+
Returns
|
|
511
|
+
-------
|
|
512
|
+
array-like or xarray.DataArray
|
|
513
|
+
The inverted field with the same array type as the input.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
del icbc
|
|
517
|
+
if coords != "cartesian":
|
|
518
|
+
raise NotImplementedError("invert_Fofonoff currently supports Cartesian coordinates only")
|
|
519
|
+
params = _merged_mparams(mParams)
|
|
520
|
+
forcing = _cartesian_coriolis_forcing(
|
|
521
|
+
F,
|
|
522
|
+
dims,
|
|
523
|
+
spacing,
|
|
524
|
+
params,
|
|
525
|
+
sign=-1.0,
|
|
526
|
+
offset=float(params["c1"]),
|
|
527
|
+
)
|
|
528
|
+
return _invert_constant_helmholtz(
|
|
529
|
+
forcing,
|
|
530
|
+
dims=dims,
|
|
531
|
+
coords=coords,
|
|
532
|
+
iParams=iParams,
|
|
533
|
+
BCs=BCs,
|
|
534
|
+
spacing=spacing,
|
|
535
|
+
helmholtz=-float(params["c0"]),
|
|
536
|
+
raise_on_error=raise_on_error,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def invert_GillMatsuno(
|
|
541
|
+
Q: Any,
|
|
542
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
543
|
+
coords: str = "cartesian",
|
|
544
|
+
icbc: Any = None,
|
|
545
|
+
mParams: dict[str, Any] | None = None,
|
|
546
|
+
iParams: dict[str, Any] | None = None,
|
|
547
|
+
*,
|
|
548
|
+
BCs: Sequence[str] | None = None,
|
|
549
|
+
spacing: Sequence[float] | None = None,
|
|
550
|
+
raise_on_error: bool = True,
|
|
551
|
+
) -> Any:
|
|
552
|
+
r"""Invert the Cartesian Gill-Matsuno Helmholtz subset.
|
|
553
|
+
|
|
554
|
+
Cartesian inputs are supported for both NumPy arrays and
|
|
555
|
+
:class:`xarray.DataArray` objects. Non-Cartesian formulations remain
|
|
556
|
+
unsupported. The ``beta = 0`` case uses the direct Fishpack / ``genbun``
|
|
557
|
+
path, while the Cartesian beta-plane case routes to the modern Fortran
|
|
558
|
+
``sor_general2d`` backend.
|
|
559
|
+
|
|
560
|
+
Parameters
|
|
561
|
+
----------
|
|
562
|
+
Q : array-like or xarray.DataArray
|
|
563
|
+
Forcing field to invert.
|
|
564
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
565
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``epsilon``,
|
|
566
|
+
``Phi``, ``f0``, and ``beta``.
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
array-like or xarray.DataArray
|
|
571
|
+
The inverted field with the same array type as the input.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
if coords != "cartesian":
|
|
575
|
+
raise NotImplementedError("invert_GillMatsuno currently supports Cartesian coordinates only")
|
|
576
|
+
params = _merged_mparams(mParams)
|
|
577
|
+
beta = _scalar_param(params, "beta")
|
|
578
|
+
epsilon = _scalar_param(params, "epsilon")
|
|
579
|
+
phi = _scalar_param(params, "Phi")
|
|
580
|
+
f0 = _scalar_param(params, "f0")
|
|
581
|
+
denom = epsilon * epsilon + f0 * f0
|
|
582
|
+
if denom == 0.0:
|
|
583
|
+
raise ValueError("epsilon and f0 cannot both be zero")
|
|
584
|
+
if beta != 0.0:
|
|
585
|
+
return _invert_general_2d(
|
|
586
|
+
Q,
|
|
587
|
+
dims=dims,
|
|
588
|
+
coords=coords,
|
|
589
|
+
iParams=iParams,
|
|
590
|
+
BCs=BCs,
|
|
591
|
+
spacing=spacing,
|
|
592
|
+
icbc=icbc,
|
|
593
|
+
coefficients=_cartesian_beta_general_coefficients(
|
|
594
|
+
Q,
|
|
595
|
+
dims=dims,
|
|
596
|
+
spacing=spacing,
|
|
597
|
+
epsilon=epsilon,
|
|
598
|
+
f0=f0,
|
|
599
|
+
beta=beta,
|
|
600
|
+
scale=phi,
|
|
601
|
+
helmholtz=-epsilon,
|
|
602
|
+
),
|
|
603
|
+
)
|
|
604
|
+
alpha = epsilon * phi / denom
|
|
605
|
+
return _invert_constant_2d(
|
|
606
|
+
Q,
|
|
607
|
+
dims=dims,
|
|
608
|
+
coords=coords,
|
|
609
|
+
iParams=iParams,
|
|
610
|
+
BCs=BCs,
|
|
611
|
+
spacing=spacing,
|
|
612
|
+
coefficients=(alpha, alpha),
|
|
613
|
+
helmholtz=-epsilon,
|
|
614
|
+
raise_on_error=raise_on_error,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def invert_GillMatsuno_test(
|
|
619
|
+
Q: Any,
|
|
620
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
621
|
+
coords: str = "cartesian",
|
|
622
|
+
icbc: Any = None,
|
|
623
|
+
mParams: dict[str, Any] | None = None,
|
|
624
|
+
iParams: dict[str, Any] | None = None,
|
|
625
|
+
*,
|
|
626
|
+
BCs: Sequence[str] | None = None,
|
|
627
|
+
spacing: Sequence[float] | None = None,
|
|
628
|
+
raise_on_error: bool = True,
|
|
629
|
+
) -> Any:
|
|
630
|
+
r"""Invert the xinvert Gill-Matsuno test-form subset for ``beta = 0``.
|
|
631
|
+
|
|
632
|
+
This wrapper exposes the Cartesian, uniform-grid, constant-coefficient test
|
|
633
|
+
subset backed by the modern Fortran Fishpack implementation. Beta-plane,
|
|
634
|
+
non-Cartesian, and other nonseparable cases are not supported here and
|
|
635
|
+
raise through the delegated implementation.
|
|
636
|
+
|
|
637
|
+
Parameters
|
|
638
|
+
----------
|
|
639
|
+
Q : array-like or xarray.DataArray
|
|
640
|
+
Forcing field to invert.
|
|
641
|
+
dims : sequence of str or int, optional
|
|
642
|
+
Inversion dimensions. Defaults to the last two dimensions.
|
|
643
|
+
coords : str, default "cartesian"
|
|
644
|
+
Coordinate system. Only ``"cartesian"`` is supported.
|
|
645
|
+
icbc : any, optional
|
|
646
|
+
Accepted for xinvert-style compatibility and ignored.
|
|
647
|
+
mParams : dict, optional
|
|
648
|
+
Equation-parameter mapping. This subset expects the usual
|
|
649
|
+
Gill-Matsuno parameters and requires ``beta = 0``.
|
|
650
|
+
iParams : dict, optional
|
|
651
|
+
Inversion-parameter mapping. Boundary conditions may be supplied
|
|
652
|
+
through ``iParams["BCs"]`` when ``BCs`` is not passed explicitly.
|
|
653
|
+
BCs : sequence of {"fixed", "periodic"}, optional
|
|
654
|
+
Boundary conditions for the two inversion dimensions.
|
|
655
|
+
spacing : sequence of float, optional
|
|
656
|
+
Grid spacing for the inversion dimensions. Defaults to unit spacing.
|
|
657
|
+
raise_on_error : bool, default True
|
|
658
|
+
Raise ``RuntimeError`` if Fishpack reports a nonzero solver error code.
|
|
659
|
+
|
|
660
|
+
Returns
|
|
661
|
+
-------
|
|
662
|
+
array-like or xarray.DataArray
|
|
663
|
+
The inverted field with the same array type as the input.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
return invert_GillMatsuno(
|
|
667
|
+
Q,
|
|
668
|
+
dims=dims,
|
|
669
|
+
coords=coords,
|
|
670
|
+
icbc=icbc,
|
|
671
|
+
mParams=mParams,
|
|
672
|
+
iParams=iParams,
|
|
673
|
+
BCs=BCs,
|
|
674
|
+
spacing=spacing,
|
|
675
|
+
raise_on_error=raise_on_error,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def invert_BrethertonHaidvogel(
|
|
680
|
+
h: Any,
|
|
681
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
682
|
+
coords: str = "cartesian",
|
|
683
|
+
icbc: Any = None,
|
|
684
|
+
mParams: dict[str, Any] | None = None,
|
|
685
|
+
iParams: dict[str, Any] | None = None,
|
|
686
|
+
*,
|
|
687
|
+
BCs: Sequence[str] | None = None,
|
|
688
|
+
spacing: Sequence[float] | None = None,
|
|
689
|
+
raise_on_error: bool = True,
|
|
690
|
+
) -> Any:
|
|
691
|
+
r"""Invert the Cartesian constant-depth Bretherton-Haidvogel equation.
|
|
692
|
+
|
|
693
|
+
The constant-depth Cartesian subset is backed by the modern Fortran
|
|
694
|
+
Fishpack Helmholtz path. ``beta = 0`` uses a constant Coriolis parameter;
|
|
695
|
+
beta-plane inputs use the Cartesian y coordinate from xarray coordinates
|
|
696
|
+
or, for NumPy arrays, from ``dims`` and ``spacing``.
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
h : array-like or xarray.DataArray
|
|
701
|
+
Layer-thickness forcing field to invert.
|
|
702
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
703
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``f0``, ``beta``,
|
|
704
|
+
and ``lambda``; ``D`` or ``depth`` must be nonzero.
|
|
705
|
+
|
|
706
|
+
Returns
|
|
707
|
+
-------
|
|
708
|
+
array-like or xarray.DataArray
|
|
709
|
+
The inverted field with the same array type as the input.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
del icbc
|
|
713
|
+
if coords != "cartesian":
|
|
714
|
+
raise NotImplementedError(
|
|
715
|
+
"invert_BrethertonHaidvogel currently supports Cartesian coordinates only"
|
|
716
|
+
)
|
|
717
|
+
params = _merged_mparams(mParams)
|
|
718
|
+
depth = float(params.get("D", params["depth"]))
|
|
719
|
+
if depth == 0.0:
|
|
720
|
+
raise ValueError("D/depth must be non-zero")
|
|
721
|
+
coriolis = _cartesian_coriolis_field(h, dims, spacing, params)
|
|
722
|
+
if _is_dataarray(h):
|
|
723
|
+
forcing = -(h * coriolis) / depth
|
|
724
|
+
else:
|
|
725
|
+
forcing = -(np.asarray(h, dtype=np.float64) * coriolis) / depth
|
|
726
|
+
return _invert_constant_helmholtz(
|
|
727
|
+
forcing,
|
|
728
|
+
dims=dims,
|
|
729
|
+
coords=coords,
|
|
730
|
+
iParams=iParams,
|
|
731
|
+
BCs=BCs,
|
|
732
|
+
spacing=spacing,
|
|
733
|
+
helmholtz=-float(params["lambda"]) * depth,
|
|
734
|
+
raise_on_error=raise_on_error,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def invert_Stommel(
|
|
739
|
+
curl: Any,
|
|
740
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
741
|
+
coords: str = "cartesian",
|
|
742
|
+
icbc: Any = None,
|
|
743
|
+
mParams: dict[str, Any] | None = None,
|
|
744
|
+
iParams: dict[str, Any] | None = None,
|
|
745
|
+
*,
|
|
746
|
+
BCs: Sequence[str] | None = None,
|
|
747
|
+
spacing: Sequence[float] | None = None,
|
|
748
|
+
raise_on_error: bool = True,
|
|
749
|
+
) -> Any:
|
|
750
|
+
r"""Invert the Cartesian Stommel equation on the supported subset.
|
|
751
|
+
|
|
752
|
+
The Cartesian ``beta = 0`` subset uses the direct Fishpack / ``genbun``
|
|
753
|
+
solve path. When ``beta != 0``, the solve is routed to the modern
|
|
754
|
+
Fortran ``sor_general2d`` backend. Non-Cartesian formulations remain
|
|
755
|
+
unsupported.
|
|
756
|
+
|
|
757
|
+
Parameters
|
|
758
|
+
----------
|
|
759
|
+
curl : array-like or xarray.DataArray
|
|
760
|
+
Wind-stress curl forcing field to invert.
|
|
761
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
762
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``D``, ``rho0``,
|
|
763
|
+
``R``, and ``beta``; ``D`` and ``rho0`` must be nonzero.
|
|
764
|
+
|
|
765
|
+
Returns
|
|
766
|
+
-------
|
|
767
|
+
array-like or xarray.DataArray
|
|
768
|
+
The inverted field with the same array type as the input.
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
if coords != "cartesian":
|
|
772
|
+
raise NotImplementedError("invert_Stommel currently supports Cartesian coordinates only")
|
|
773
|
+
params = _merged_mparams(mParams)
|
|
774
|
+
beta = _scalar_param(params, "beta")
|
|
775
|
+
depth = _scalar_param(params, "D")
|
|
776
|
+
rho0 = _scalar_param(params, "rho0")
|
|
777
|
+
resistance = _scalar_param(params, "R")
|
|
778
|
+
if depth == 0.0 or rho0 == 0.0:
|
|
779
|
+
raise ValueError("D and rho0 must be non-zero")
|
|
780
|
+
alpha = -resistance / depth
|
|
781
|
+
forcing = curl * (-1.0 / (depth * rho0)) if _is_dataarray(curl) else np.asarray(curl, dtype=np.float64) * (-1.0 / (depth * rho0))
|
|
782
|
+
if beta != 0.0:
|
|
783
|
+
return _invert_general_2d(
|
|
784
|
+
forcing,
|
|
785
|
+
dims=dims,
|
|
786
|
+
coords=coords,
|
|
787
|
+
iParams=iParams,
|
|
788
|
+
BCs=BCs,
|
|
789
|
+
spacing=spacing,
|
|
790
|
+
icbc=icbc,
|
|
791
|
+
coefficients=(alpha, 0.0, alpha, 0.0, -beta, 0.0),
|
|
792
|
+
)
|
|
793
|
+
return _invert_constant_2d(
|
|
794
|
+
forcing,
|
|
795
|
+
dims=dims,
|
|
796
|
+
coords=coords,
|
|
797
|
+
iParams=iParams,
|
|
798
|
+
BCs=BCs,
|
|
799
|
+
spacing=spacing,
|
|
800
|
+
coefficients=(alpha, alpha),
|
|
801
|
+
helmholtz=0.0,
|
|
802
|
+
raise_on_error=raise_on_error,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def invert_Stommel_test(
|
|
807
|
+
curl: Any,
|
|
808
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
809
|
+
coords: str = "cartesian",
|
|
810
|
+
icbc: Any = None,
|
|
811
|
+
mParams: dict[str, Any] | None = None,
|
|
812
|
+
iParams: dict[str, Any] | None = None,
|
|
813
|
+
*,
|
|
814
|
+
BCs: Sequence[str] | None = None,
|
|
815
|
+
spacing: Sequence[float] | None = None,
|
|
816
|
+
raise_on_error: bool = True,
|
|
817
|
+
) -> Any:
|
|
818
|
+
r"""Invert the xinvert Stommel test-form subset.
|
|
819
|
+
|
|
820
|
+
This wrapper preserves xinvert's Cartesian, uniform-grid,
|
|
821
|
+
constant-coefficient test form and delegates to :func:`invert_Stommel`.
|
|
822
|
+
As a result, ``beta = 0`` uses the direct Fishpack / ``genbun`` path while
|
|
823
|
+
``beta != 0`` uses the modern Fortran ``sor_general2d`` backend.
|
|
824
|
+
Non-Cartesian formulations remain unsupported.
|
|
825
|
+
|
|
826
|
+
Parameters
|
|
827
|
+
----------
|
|
828
|
+
curl : array-like or xarray.DataArray
|
|
829
|
+
Wind-stress curl forcing field to invert.
|
|
830
|
+
dims : sequence of str or int, optional
|
|
831
|
+
Inversion dimensions. Defaults to the last two dimensions.
|
|
832
|
+
coords : str, default "cartesian"
|
|
833
|
+
Coordinate system. Only ``"cartesian"`` is supported.
|
|
834
|
+
icbc : any, optional
|
|
835
|
+
Accepted for xinvert-style compatibility and ignored.
|
|
836
|
+
mParams : dict, optional
|
|
837
|
+
Equation-parameter mapping. This subset expects the usual Stommel
|
|
838
|
+
parameters; ``D`` and ``rho0`` must be nonzero, and the ``beta``
|
|
839
|
+
handling follows :func:`invert_Stommel`.
|
|
840
|
+
iParams : dict, optional
|
|
841
|
+
Inversion-parameter mapping. Boundary conditions may be supplied
|
|
842
|
+
through ``iParams["BCs"]`` when ``BCs`` is not passed explicitly.
|
|
843
|
+
BCs : sequence of {"fixed", "periodic"}, optional
|
|
844
|
+
Boundary conditions for the two inversion dimensions.
|
|
845
|
+
spacing : sequence of float, optional
|
|
846
|
+
Grid spacing for the inversion dimensions. Defaults to unit spacing.
|
|
847
|
+
raise_on_error : bool, default True
|
|
848
|
+
Raise ``RuntimeError`` if Fishpack reports a nonzero solver error code.
|
|
849
|
+
|
|
850
|
+
Returns
|
|
851
|
+
-------
|
|
852
|
+
array-like or xarray.DataArray
|
|
853
|
+
The inverted field with the same array type as the input.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
return invert_Stommel(
|
|
857
|
+
curl,
|
|
858
|
+
dims=dims,
|
|
859
|
+
coords=coords,
|
|
860
|
+
icbc=icbc,
|
|
861
|
+
mParams=mParams,
|
|
862
|
+
iParams=iParams,
|
|
863
|
+
BCs=BCs,
|
|
864
|
+
spacing=spacing,
|
|
865
|
+
raise_on_error=raise_on_error,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def invert_StommelMunk(
|
|
870
|
+
curl: Any,
|
|
871
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
872
|
+
coords: str = "cartesian",
|
|
873
|
+
icbc: Any = None,
|
|
874
|
+
mParams: dict[str, Any] | None = None,
|
|
875
|
+
iParams: dict[str, Any] | None = None,
|
|
876
|
+
*,
|
|
877
|
+
BCs: Sequence[str] | None = None,
|
|
878
|
+
spacing: Sequence[float] | None = None,
|
|
879
|
+
raise_on_error: bool = True,
|
|
880
|
+
) -> Any:
|
|
881
|
+
r"""Invert the Cartesian Stommel-Munk equation in xinvert form.
|
|
882
|
+
|
|
883
|
+
The ``A4 = 0`` subset delegates to :func:`invert_Stommel`. Nonzero
|
|
884
|
+
``A4`` uses the modern Fortran ``sor_biharmonic2d`` backend for the
|
|
885
|
+
Cartesian fourth-order Stommel-Munk equation. Lat-lon and other
|
|
886
|
+
non-Cartesian formulations remain unsupported.
|
|
887
|
+
|
|
888
|
+
Parameters
|
|
889
|
+
----------
|
|
890
|
+
curl : array-like or xarray.DataArray
|
|
891
|
+
Wind-stress curl forcing field to invert.
|
|
892
|
+
dims : sequence of str or int, optional
|
|
893
|
+
Inversion dimensions. Defaults to the last two dimensions.
|
|
894
|
+
coords : str, default "cartesian"
|
|
895
|
+
Coordinate system. Only ``"cartesian"`` is supported.
|
|
896
|
+
icbc : any, optional
|
|
897
|
+
Accepted for xinvert-style compatibility and ignored.
|
|
898
|
+
mParams : dict, optional
|
|
899
|
+
Equation-parameter mapping. This wrapper uses ``A4``, ``beta``,
|
|
900
|
+
``D``, ``rho0``, and ``R``.
|
|
901
|
+
iParams : dict, optional
|
|
902
|
+
Inversion-parameter mapping. Boundary conditions may be supplied
|
|
903
|
+
through ``iParams["BCs"]`` when ``BCs`` is not passed explicitly.
|
|
904
|
+
BCs : sequence of {"fixed", "periodic"}, optional
|
|
905
|
+
Boundary conditions for the two inversion dimensions.
|
|
906
|
+
spacing : sequence of float, optional
|
|
907
|
+
Grid spacing for the inversion dimensions. Defaults to unit spacing.
|
|
908
|
+
raise_on_error : bool, default True
|
|
909
|
+
Raise ``RuntimeError`` if Fishpack reports a nonzero solver error code.
|
|
910
|
+
|
|
911
|
+
Returns
|
|
912
|
+
-------
|
|
913
|
+
array-like or xarray.DataArray
|
|
914
|
+
The inverted field with the same array type as the input.
|
|
915
|
+
"""
|
|
916
|
+
|
|
917
|
+
params = _merged_mparams(mParams)
|
|
918
|
+
a4 = _scalar_param(params, "A4")
|
|
919
|
+
if a4 != 0.0:
|
|
920
|
+
if coords != "cartesian":
|
|
921
|
+
raise NotImplementedError("invert_StommelMunk currently supports Cartesian coordinates only")
|
|
922
|
+
depth = _scalar_param(params, "D")
|
|
923
|
+
rho0 = _scalar_param(params, "rho0")
|
|
924
|
+
if depth == 0.0 or rho0 == 0.0:
|
|
925
|
+
raise ValueError("D and rho0 must be non-zero")
|
|
926
|
+
forcing = (
|
|
927
|
+
curl * (-1.0 / (depth * rho0))
|
|
928
|
+
if _is_dataarray(curl)
|
|
929
|
+
else np.asarray(curl, dtype=np.float64) * (-1.0 / (depth * rho0))
|
|
930
|
+
)
|
|
931
|
+
resistance = _scalar_param(params, "R")
|
|
932
|
+
beta = _scalar_param(params, "beta")
|
|
933
|
+
return _invert_biharmonic_2d(
|
|
934
|
+
forcing,
|
|
935
|
+
dims=dims,
|
|
936
|
+
coords=coords,
|
|
937
|
+
iParams=iParams,
|
|
938
|
+
BCs=BCs,
|
|
939
|
+
spacing=spacing,
|
|
940
|
+
icbc=icbc,
|
|
941
|
+
coefficients=(a4, 0.0, a4, -resistance / depth, 0.0, -resistance / depth, 0.0, -beta, 0.0),
|
|
942
|
+
)
|
|
943
|
+
return invert_Stommel(
|
|
944
|
+
curl,
|
|
945
|
+
dims=dims,
|
|
946
|
+
coords=coords,
|
|
947
|
+
icbc=icbc,
|
|
948
|
+
mParams=params,
|
|
949
|
+
iParams=iParams,
|
|
950
|
+
BCs=BCs,
|
|
951
|
+
spacing=spacing,
|
|
952
|
+
raise_on_error=raise_on_error,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def invert_StommelArons(
|
|
957
|
+
Q: Any,
|
|
958
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
959
|
+
coords: str = "cartesian",
|
|
960
|
+
icbc: Any = None,
|
|
961
|
+
mParams: dict[str, Any] | None = None,
|
|
962
|
+
iParams: dict[str, Any] | None = None,
|
|
963
|
+
*,
|
|
964
|
+
BCs: Sequence[str] | None = None,
|
|
965
|
+
spacing: Sequence[float] | None = None,
|
|
966
|
+
raise_on_error: bool = True,
|
|
967
|
+
) -> Any:
|
|
968
|
+
r"""Invert the Cartesian constant-Coriolis Stommel-Arons subset.
|
|
969
|
+
|
|
970
|
+
Cartesian inputs are supported for both NumPy arrays and
|
|
971
|
+
:class:`xarray.DataArray` objects. Non-Cartesian formulations remain
|
|
972
|
+
unsupported. The ``beta = 0`` case uses the direct Fishpack / ``genbun``
|
|
973
|
+
path, while the Cartesian beta-plane case routes to the modern Fortran
|
|
974
|
+
``sor_general2d`` backend.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
Q : array-like or xarray.DataArray
|
|
979
|
+
Forcing field to invert.
|
|
980
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
981
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``epsilon``,
|
|
982
|
+
``f0``, and ``beta``.
|
|
983
|
+
|
|
984
|
+
Returns
|
|
985
|
+
-------
|
|
986
|
+
array-like or xarray.DataArray
|
|
987
|
+
The inverted field with the same array type as the input.
|
|
988
|
+
"""
|
|
989
|
+
|
|
990
|
+
if coords != "cartesian":
|
|
991
|
+
raise NotImplementedError("invert_StommelArons currently supports Cartesian coordinates only")
|
|
992
|
+
params = _merged_mparams(mParams)
|
|
993
|
+
beta = _scalar_param(params, "beta")
|
|
994
|
+
epsilon = _scalar_param(params, "epsilon")
|
|
995
|
+
f0 = _scalar_param(params, "f0")
|
|
996
|
+
denom = epsilon * epsilon + f0 * f0
|
|
997
|
+
if denom == 0.0:
|
|
998
|
+
raise ValueError("epsilon and f0 cannot both be zero")
|
|
999
|
+
if beta != 0.0:
|
|
1000
|
+
return _invert_general_2d(
|
|
1001
|
+
Q,
|
|
1002
|
+
dims=dims,
|
|
1003
|
+
coords=coords,
|
|
1004
|
+
iParams=iParams,
|
|
1005
|
+
BCs=BCs,
|
|
1006
|
+
spacing=spacing,
|
|
1007
|
+
icbc=icbc,
|
|
1008
|
+
coefficients=_cartesian_beta_general_coefficients(
|
|
1009
|
+
Q,
|
|
1010
|
+
dims=dims,
|
|
1011
|
+
spacing=spacing,
|
|
1012
|
+
epsilon=epsilon,
|
|
1013
|
+
f0=f0,
|
|
1014
|
+
beta=beta,
|
|
1015
|
+
scale=1.0,
|
|
1016
|
+
helmholtz=0.0,
|
|
1017
|
+
),
|
|
1018
|
+
)
|
|
1019
|
+
alpha = epsilon / denom
|
|
1020
|
+
return _invert_constant_2d(
|
|
1021
|
+
Q,
|
|
1022
|
+
dims=dims,
|
|
1023
|
+
coords=coords,
|
|
1024
|
+
iParams=iParams,
|
|
1025
|
+
BCs=BCs,
|
|
1026
|
+
spacing=spacing,
|
|
1027
|
+
coefficients=(alpha, alpha),
|
|
1028
|
+
helmholtz=0.0,
|
|
1029
|
+
raise_on_error=raise_on_error,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def invert_omega(
|
|
1034
|
+
F: Any,
|
|
1035
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
1036
|
+
coords: str = "cartesian",
|
|
1037
|
+
icbc: Any = None,
|
|
1038
|
+
mParams: dict[str, Any] | None = None,
|
|
1039
|
+
iParams: dict[str, Any] | None = None,
|
|
1040
|
+
*,
|
|
1041
|
+
BCs: Sequence[str] | None = None,
|
|
1042
|
+
spacing: Sequence[float] | None = None,
|
|
1043
|
+
raise_on_error: bool = True,
|
|
1044
|
+
) -> Any:
|
|
1045
|
+
r"""Invert the Cartesian QG omega equation.
|
|
1046
|
+
|
|
1047
|
+
The constant-Coriolis subset uses the direct Fishpack 3-D solver. The
|
|
1048
|
+
Cartesian beta-plane subset uses the modern Fortran ``sor_standard3d``
|
|
1049
|
+
backend with xinvert-style flux coefficients.
|
|
1050
|
+
|
|
1051
|
+
Parameters
|
|
1052
|
+
----------
|
|
1053
|
+
F : array-like or xarray.DataArray
|
|
1054
|
+
Forcing field to invert.
|
|
1055
|
+
dims, coords, icbc, mParams, iParams, BCs, spacing, raise_on_error
|
|
1056
|
+
See :func:`invert_Poisson`. ``mParams`` may provide ``f0``, ``beta``,
|
|
1057
|
+
and ``N2``; this subset requires ``beta = 0`` and positive ``N2``.
|
|
1058
|
+
|
|
1059
|
+
Returns
|
|
1060
|
+
-------
|
|
1061
|
+
array-like or xarray.DataArray
|
|
1062
|
+
The inverted field with the same array type as the input.
|
|
1063
|
+
"""
|
|
1064
|
+
|
|
1065
|
+
if coords != "cartesian":
|
|
1066
|
+
raise NotImplementedError("invert_omega currently supports Cartesian coordinates only")
|
|
1067
|
+
params = _merged_mparams(mParams)
|
|
1068
|
+
beta = float(params["beta"])
|
|
1069
|
+
n2 = float(params["N2"])
|
|
1070
|
+
if n2 <= 0.0:
|
|
1071
|
+
raise ValueError("N2 must be a positive scalar")
|
|
1072
|
+
f0 = float(params["f0"])
|
|
1073
|
+
if beta != 0.0:
|
|
1074
|
+
return _invert_standard_3d(
|
|
1075
|
+
F,
|
|
1076
|
+
dims=dims,
|
|
1077
|
+
coords=coords,
|
|
1078
|
+
iParams=iParams,
|
|
1079
|
+
BCs=BCs,
|
|
1080
|
+
spacing=spacing,
|
|
1081
|
+
icbc=icbc,
|
|
1082
|
+
coefficients=_cartesian_omega_coefficients(
|
|
1083
|
+
F, dims=dims, spacing=spacing, f0=f0, beta=beta, n2=n2
|
|
1084
|
+
),
|
|
1085
|
+
)
|
|
1086
|
+
del icbc
|
|
1087
|
+
rhs = F / n2 if _is_dataarray(F) else np.asarray(F, dtype=np.float64) / n2
|
|
1088
|
+
return _invert_constant_3d(
|
|
1089
|
+
rhs,
|
|
1090
|
+
dims=dims,
|
|
1091
|
+
coords=coords,
|
|
1092
|
+
iParams=iParams,
|
|
1093
|
+
BCs=BCs,
|
|
1094
|
+
spacing=spacing,
|
|
1095
|
+
coefficients=(f0 * f0 / n2, 1.0, 1.0),
|
|
1096
|
+
raise_on_error=raise_on_error,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def invert_3DOcean(
|
|
1101
|
+
F: Any,
|
|
1102
|
+
dims: Sequence[str] | Sequence[int] | None = None,
|
|
1103
|
+
coords: str = "cartesian",
|
|
1104
|
+
icbc: Any = None,
|
|
1105
|
+
mParams: dict[str, Any] | None = None,
|
|
1106
|
+
iParams: dict[str, Any] | None = None,
|
|
1107
|
+
*,
|
|
1108
|
+
BCs: Sequence[str] | None = None,
|
|
1109
|
+
spacing: Sequence[float] | None = None,
|
|
1110
|
+
raise_on_error: bool = True,
|
|
1111
|
+
) -> Any:
|
|
1112
|
+
r"""Invert the Cartesian constant-coefficient 3-D ocean equation subset.
|
|
1113
|
+
|
|
1114
|
+
Only the Cartesian, uniform-grid, constant-coefficient Fishpack subset is
|
|
1115
|
+
supported. Beta-plane and other nonseparable formulations intentionally
|
|
1116
|
+
raise ``NotImplementedError``.
|
|
1117
|
+
|
|
1118
|
+
Parameters
|
|
1119
|
+
----------
|
|
1120
|
+
F : array-like or xarray.DataArray
|
|
1121
|
+
Forcing field to invert.
|
|
1122
|
+
dims : sequence of str or int, optional
|
|
1123
|
+
Three inversion dimensions. Defaults to the last three dimensions.
|
|
1124
|
+
coords : str, default "cartesian"
|
|
1125
|
+
Coordinate system. Only ``"cartesian"`` is supported.
|
|
1126
|
+
icbc : any, optional
|
|
1127
|
+
Accepted for xinvert-style compatibility and ignored.
|
|
1128
|
+
mParams, iParams : dict, optional
|
|
1129
|
+
Compatibility mappings for equation and inversion parameters. Boundary
|
|
1130
|
+
conditions may be supplied through ``iParams["BCs"]`` when ``BCs`` is
|
|
1131
|
+
not passed explicitly.
|
|
1132
|
+
BCs : sequence of {"fixed", "periodic"}, optional
|
|
1133
|
+
Boundary conditions for the three inversion dimensions. Defaults to
|
|
1134
|
+
``("fixed", "fixed", "fixed")``.
|
|
1135
|
+
spacing : sequence of float, optional
|
|
1136
|
+
Grid spacing ``(dz, dy, dx)`` for the inversion dimensions. Defaults
|
|
1137
|
+
to unit spacing.
|
|
1138
|
+
raise_on_error : bool, default True
|
|
1139
|
+
Raise ``RuntimeError`` if Fishpack reports a nonzero solver error code.
|
|
1140
|
+
|
|
1141
|
+
Returns
|
|
1142
|
+
-------
|
|
1143
|
+
array-like or xarray.DataArray
|
|
1144
|
+
The inverted field with the same array type as the input.
|
|
1145
|
+
"""
|
|
1146
|
+
|
|
1147
|
+
if coords != "cartesian":
|
|
1148
|
+
raise NotImplementedError("invert_3DOcean currently supports Cartesian coordinates only")
|
|
1149
|
+
params = _merged_mparams(mParams)
|
|
1150
|
+
beta = _scalar_param(params, "beta")
|
|
1151
|
+
epsilon = _scalar_param(params, "epsilon")
|
|
1152
|
+
f0 = _scalar_param(params, "f0")
|
|
1153
|
+
n2 = _scalar_param(params, "N2")
|
|
1154
|
+
buoyancy_damping = _scalar_param(params, "k")
|
|
1155
|
+
if n2 <= 0.0:
|
|
1156
|
+
raise ValueError("N2 must be a positive scalar")
|
|
1157
|
+
denom = epsilon * epsilon + f0 * f0
|
|
1158
|
+
if denom == 0.0:
|
|
1159
|
+
raise ValueError("epsilon and f0 cannot both be zero")
|
|
1160
|
+
if beta != 0.0:
|
|
1161
|
+
return _invert_general_3d(
|
|
1162
|
+
F,
|
|
1163
|
+
dims=dims,
|
|
1164
|
+
coords=coords,
|
|
1165
|
+
iParams=iParams,
|
|
1166
|
+
BCs=BCs,
|
|
1167
|
+
spacing=spacing,
|
|
1168
|
+
icbc=icbc,
|
|
1169
|
+
coefficients=_cartesian_3d_ocean_coefficients(
|
|
1170
|
+
F,
|
|
1171
|
+
dims=dims,
|
|
1172
|
+
spacing=spacing,
|
|
1173
|
+
epsilon=epsilon,
|
|
1174
|
+
f0=f0,
|
|
1175
|
+
beta=beta,
|
|
1176
|
+
n2=n2,
|
|
1177
|
+
buoyancy_damping=buoyancy_damping,
|
|
1178
|
+
),
|
|
1179
|
+
)
|
|
1180
|
+
del icbc
|
|
1181
|
+
horizontal = epsilon / denom
|
|
1182
|
+
vertical = buoyancy_damping / n2
|
|
1183
|
+
return _invert_constant_3d(
|
|
1184
|
+
F,
|
|
1185
|
+
dims=dims,
|
|
1186
|
+
coords=coords,
|
|
1187
|
+
iParams=iParams,
|
|
1188
|
+
BCs=BCs,
|
|
1189
|
+
spacing=spacing,
|
|
1190
|
+
coefficients=(vertical, horizontal, horizontal),
|
|
1191
|
+
raise_on_error=raise_on_error,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def invert_MultiGrid(
|
|
1196
|
+
invert_func: Any,
|
|
1197
|
+
*args: Any,
|
|
1198
|
+
ratio: int = 3,
|
|
1199
|
+
gridNo: int = 3,
|
|
1200
|
+
**kwargs: Any,
|
|
1201
|
+
) -> tuple[Any, list[list[Any]], list[Any]]:
|
|
1202
|
+
"""Compatibility helper for xinvert's ``MultiGrid`` entry point.
|
|
1203
|
+
|
|
1204
|
+
This is not a separate equation solver. PyFishPack uses direct Fishpack
|
|
1205
|
+
solvers, so this wrapper delegates once to ``invert_func`` and returns a
|
|
1206
|
+
xinvert-style ``(solution, grids, history)`` tuple.
|
|
1207
|
+
|
|
1208
|
+
Parameters
|
|
1209
|
+
----------
|
|
1210
|
+
invert_func : callable
|
|
1211
|
+
Solver function to call once with ``*args`` and ``**kwargs``.
|
|
1212
|
+
*args : Any
|
|
1213
|
+
Positional arguments passed through to ``invert_func``.
|
|
1214
|
+
ratio : int, optional
|
|
1215
|
+
Accepted for API compatibility with xinvert and currently ignored.
|
|
1216
|
+
gridNo : int, optional
|
|
1217
|
+
Accepted for API compatibility with xinvert and currently ignored.
|
|
1218
|
+
**kwargs : Any
|
|
1219
|
+
Keyword arguments passed through to ``invert_func``.
|
|
1220
|
+
|
|
1221
|
+
Returns
|
|
1222
|
+
-------
|
|
1223
|
+
tuple[Any, list[list[Any]], list[Any]]
|
|
1224
|
+
``(solution, grids, history)`` where ``solution`` is the direct
|
|
1225
|
+
result from ``invert_func``, ``grids`` records the input arguments, and
|
|
1226
|
+
``history`` records the single returned solution.
|
|
1227
|
+
"""
|
|
1228
|
+
|
|
1229
|
+
del ratio, gridNo
|
|
1230
|
+
if not callable(invert_func):
|
|
1231
|
+
raise TypeError("invert_func must be callable")
|
|
1232
|
+
solution = invert_func(*args, **kwargs)
|
|
1233
|
+
return solution, [list(args)], [solution]
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def spectral_transform(
|
|
1237
|
+
data: Any,
|
|
1238
|
+
*,
|
|
1239
|
+
kind: str = "rfft",
|
|
1240
|
+
direction: str = "forward",
|
|
1241
|
+
axis: int = -1,
|
|
1242
|
+
normalize: bool = False,
|
|
1243
|
+
) -> np.ndarray:
|
|
1244
|
+
"""Apply a lightweight one-dimensional FFTPACK transform along an array axis."""
|
|
1245
|
+
|
|
1246
|
+
transform = kind.lower()
|
|
1247
|
+
direct = direction.lower()
|
|
1248
|
+
if direct not in {"forward", "backward", "inverse"}:
|
|
1249
|
+
raise ValueError("direction must be 'forward', 'backward', or 'inverse'")
|
|
1250
|
+
inverse = direct in {"backward", "inverse"}
|
|
1251
|
+
|
|
1252
|
+
real_methods = {
|
|
1253
|
+
("rfft", False): fishpack.rfftf,
|
|
1254
|
+
("rfft", True): fishpack.rfftb,
|
|
1255
|
+
("sint", False): fishpack.sint,
|
|
1256
|
+
("sint", True): fishpack.sint,
|
|
1257
|
+
("cost", False): fishpack.cost,
|
|
1258
|
+
("cost", True): fishpack.cost,
|
|
1259
|
+
("sinq", False): fishpack.sinqf,
|
|
1260
|
+
("sinq", True): fishpack.sinqb,
|
|
1261
|
+
("cosq", False): fishpack.cosqf,
|
|
1262
|
+
("cosq", True): fishpack.cosqb,
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if transform == "cfft":
|
|
1266
|
+
arr = np.asarray(data, dtype=np.complex128)
|
|
1267
|
+
if arr.ndim == 0:
|
|
1268
|
+
raise ValueError("spectral_transform expects at least one-dimensional input")
|
|
1269
|
+
axis = axis % arr.ndim
|
|
1270
|
+
moved = np.moveaxis(arr, axis, -1)
|
|
1271
|
+
flat = moved.reshape((-1, moved.shape[-1]))
|
|
1272
|
+
out = np.empty_like(flat)
|
|
1273
|
+
method = fishpack.cfftb if inverse else fishpack.cfftf
|
|
1274
|
+
for idx, row in enumerate(flat):
|
|
1275
|
+
interleaved = np.ascontiguousarray(row, dtype=np.complex128).view(np.float64)
|
|
1276
|
+
transformed = np.asarray(method(interleaved), dtype=np.float64).view(np.complex128)
|
|
1277
|
+
out[idx] = transformed
|
|
1278
|
+
result = out.reshape(moved.shape)
|
|
1279
|
+
if normalize and inverse:
|
|
1280
|
+
result = result / moved.shape[-1]
|
|
1281
|
+
return np.moveaxis(result, -1, axis)
|
|
1282
|
+
|
|
1283
|
+
method = real_methods.get((transform, inverse))
|
|
1284
|
+
if method is None:
|
|
1285
|
+
raise ValueError("kind must be one of 'rfft', 'cfft', 'sint', 'cost', 'sinq', or 'cosq'")
|
|
1286
|
+
|
|
1287
|
+
arr = np.asarray(data, dtype=np.float64)
|
|
1288
|
+
if arr.ndim == 0:
|
|
1289
|
+
raise ValueError("spectral_transform expects at least one-dimensional input")
|
|
1290
|
+
axis = axis % arr.ndim
|
|
1291
|
+
moved = np.moveaxis(arr, axis, -1)
|
|
1292
|
+
flat = moved.reshape((-1, moved.shape[-1]))
|
|
1293
|
+
out = np.empty_like(flat)
|
|
1294
|
+
for idx, row in enumerate(flat):
|
|
1295
|
+
out[idx] = method(np.ascontiguousarray(row, dtype=np.float64))
|
|
1296
|
+
result = out.reshape(moved.shape)
|
|
1297
|
+
if normalize and inverse and transform == "rfft":
|
|
1298
|
+
result = result / moved.shape[-1]
|
|
1299
|
+
return np.moveaxis(result, -1, axis)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _require_dataarray(field: Any, func_name: str) -> None:
|
|
1303
|
+
if not _is_dataarray(field):
|
|
1304
|
+
raise NotImplementedError(f"{func_name} currently requires an xarray.DataArray input")
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _merged_iparams(
|
|
1308
|
+
params: dict[str, Any] | None,
|
|
1309
|
+
*,
|
|
1310
|
+
ndim: int,
|
|
1311
|
+
BCs: Sequence[str] | None,
|
|
1312
|
+
) -> dict[str, Any]:
|
|
1313
|
+
merged = dict(_DEFAULT_IPARAMS)
|
|
1314
|
+
merged["BCs"] = tuple("fixed" for _ in range(ndim))
|
|
1315
|
+
if params is not None:
|
|
1316
|
+
merged.update(params)
|
|
1317
|
+
if BCs is not None:
|
|
1318
|
+
merged["BCs"] = tuple(BCs)
|
|
1319
|
+
return merged
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def _mask_labeled_field(
|
|
1323
|
+
field: Any,
|
|
1324
|
+
dims: Sequence[str],
|
|
1325
|
+
iparams: dict[str, Any],
|
|
1326
|
+
bcs: tuple[str, ...],
|
|
1327
|
+
icbc: Any,
|
|
1328
|
+
) -> tuple[Any, Any, Any]:
|
|
1329
|
+
missing = [dim for dim in dims if dim not in field.dims]
|
|
1330
|
+
if missing:
|
|
1331
|
+
raise ValueError(f"dims not present in input DataArray: {missing}")
|
|
1332
|
+
|
|
1333
|
+
undef = iparams["undef"]
|
|
1334
|
+
if np.isnan(undef):
|
|
1335
|
+
mask_f = field.fillna(_UNDEF)
|
|
1336
|
+
else:
|
|
1337
|
+
mask_f = field.where(field != undef, other=_UNDEF)
|
|
1338
|
+
zero = mask_f - mask_f
|
|
1339
|
+
|
|
1340
|
+
if icbc is None:
|
|
1341
|
+
init_s = zero.copy()
|
|
1342
|
+
else:
|
|
1343
|
+
mask = mask_f == _UNDEF
|
|
1344
|
+
for dim, bc in zip(dims, bcs):
|
|
1345
|
+
if bc != "periodic":
|
|
1346
|
+
coord = mask_f.coords[dim]
|
|
1347
|
+
cond = coord.isin([coord[0], coord[-1]])
|
|
1348
|
+
mask = np.logical_or(mask, cond)
|
|
1349
|
+
init_s = field.__class__(icbc, coords=field.coords, dims=field.dims) if not _is_dataarray(icbc) else icbc
|
|
1350
|
+
init_s = init_s.where(mask, other=0)
|
|
1351
|
+
return mask_f, init_s.load(), zero
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _restore_labeled_result(result: Any, mask_f: Any, iparams: dict[str, Any], icbc: Any) -> Any:
|
|
1355
|
+
result = result.rename("inverted")
|
|
1356
|
+
if icbc is None:
|
|
1357
|
+
return result.where(mask_f != _UNDEF, other=iparams["undef"])
|
|
1358
|
+
return result
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def _solve_sor2d_labeled(
|
|
1362
|
+
init_s: Any,
|
|
1363
|
+
acoef: Any,
|
|
1364
|
+
bcoef: Any,
|
|
1365
|
+
ccoef: Any,
|
|
1366
|
+
force: Any,
|
|
1367
|
+
*,
|
|
1368
|
+
dims: Sequence[str],
|
|
1369
|
+
dy: float,
|
|
1370
|
+
dx: float,
|
|
1371
|
+
bcs: tuple[str, ...],
|
|
1372
|
+
iparams: dict[str, Any],
|
|
1373
|
+
) -> Any:
|
|
1374
|
+
import xarray as xr
|
|
1375
|
+
|
|
1376
|
+
ydim, xdim = dims
|
|
1377
|
+
acoef, bcoef, ccoef, force, init_s = xr.broadcast(acoef, bcoef, ccoef, force, init_s)
|
|
1378
|
+
outer_dims = tuple(dim for dim in force.dims if dim not in dims)
|
|
1379
|
+
order = (*outer_dims, ydim, xdim)
|
|
1380
|
+
a_t = acoef.transpose(*order)
|
|
1381
|
+
b_t = bcoef.transpose(*order)
|
|
1382
|
+
c_t = ccoef.transpose(*order)
|
|
1383
|
+
f_t = force.transpose(*order)
|
|
1384
|
+
s_t = init_s.transpose(*order)
|
|
1385
|
+
values = np.empty(s_t.shape, dtype=np.float64)
|
|
1386
|
+
optarg = _sor_optarg(iparams, s_t.shape[-2:])
|
|
1387
|
+
|
|
1388
|
+
for index in np.ndindex(s_t.shape[:-2] or ()):
|
|
1389
|
+
key = index if s_t.ndim > 2 else (...,)
|
|
1390
|
+
if s_t.ndim == 2:
|
|
1391
|
+
solved, relerr, overflow, loops = fishpack.sor_standard2d(
|
|
1392
|
+
s_t.values,
|
|
1393
|
+
a_t.values,
|
|
1394
|
+
b_t.values,
|
|
1395
|
+
c_t.values,
|
|
1396
|
+
f_t.values,
|
|
1397
|
+
dy,
|
|
1398
|
+
dx,
|
|
1399
|
+
bcs[0],
|
|
1400
|
+
bcs[1],
|
|
1401
|
+
optarg,
|
|
1402
|
+
_UNDEF,
|
|
1403
|
+
int(iparams["mxLoop"]),
|
|
1404
|
+
float(iparams["tolerance"]),
|
|
1405
|
+
)
|
|
1406
|
+
values[...] = solved
|
|
1407
|
+
if overflow:
|
|
1408
|
+
raise RuntimeError("Fortran SOR standard2d overflowed")
|
|
1409
|
+
break
|
|
1410
|
+
solved, relerr, overflow, loops = fishpack.sor_standard2d(
|
|
1411
|
+
s_t.values[key],
|
|
1412
|
+
a_t.values[key],
|
|
1413
|
+
b_t.values[key],
|
|
1414
|
+
c_t.values[key],
|
|
1415
|
+
f_t.values[key],
|
|
1416
|
+
dy,
|
|
1417
|
+
dx,
|
|
1418
|
+
bcs[0],
|
|
1419
|
+
bcs[1],
|
|
1420
|
+
optarg,
|
|
1421
|
+
_UNDEF,
|
|
1422
|
+
int(iparams["mxLoop"]),
|
|
1423
|
+
float(iparams["tolerance"]),
|
|
1424
|
+
)
|
|
1425
|
+
values[key] = solved
|
|
1426
|
+
if overflow:
|
|
1427
|
+
raise RuntimeError("Fortran SOR standard2d overflowed")
|
|
1428
|
+
|
|
1429
|
+
result = force.__class__(
|
|
1430
|
+
values,
|
|
1431
|
+
coords=s_t.coords,
|
|
1432
|
+
dims=s_t.dims,
|
|
1433
|
+
attrs=dict(force.attrs),
|
|
1434
|
+
name="inverted",
|
|
1435
|
+
)
|
|
1436
|
+
return result.transpose(*force.dims)
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _solve_sor3d_labeled(
|
|
1440
|
+
init_s: Any,
|
|
1441
|
+
acoef: Any,
|
|
1442
|
+
bcoef: Any,
|
|
1443
|
+
ccoef: Any,
|
|
1444
|
+
force: Any,
|
|
1445
|
+
*,
|
|
1446
|
+
dims: Sequence[str],
|
|
1447
|
+
dz: float,
|
|
1448
|
+
dy: float,
|
|
1449
|
+
dx: float,
|
|
1450
|
+
bcs: tuple[str, ...],
|
|
1451
|
+
iparams: dict[str, Any],
|
|
1452
|
+
) -> Any:
|
|
1453
|
+
import xarray as xr
|
|
1454
|
+
|
|
1455
|
+
zdim, ydim, xdim = dims
|
|
1456
|
+
acoef, bcoef, ccoef, force, init_s = xr.broadcast(acoef, bcoef, ccoef, force, init_s)
|
|
1457
|
+
outer_dims = tuple(dim for dim in force.dims if dim not in dims)
|
|
1458
|
+
order = (*outer_dims, zdim, ydim, xdim)
|
|
1459
|
+
a_t = acoef.transpose(*order)
|
|
1460
|
+
b_t = bcoef.transpose(*order)
|
|
1461
|
+
c_t = ccoef.transpose(*order)
|
|
1462
|
+
f_t = force.transpose(*order)
|
|
1463
|
+
s_t = init_s.transpose(*order)
|
|
1464
|
+
values = np.empty(s_t.shape, dtype=np.float64)
|
|
1465
|
+
optarg = _sor_optarg(iparams, s_t.shape[-3:])
|
|
1466
|
+
|
|
1467
|
+
if s_t.ndim == 3:
|
|
1468
|
+
solved, relerr, overflow, loops = fishpack.sor_standard3d(
|
|
1469
|
+
s_t.values,
|
|
1470
|
+
a_t.values,
|
|
1471
|
+
b_t.values,
|
|
1472
|
+
c_t.values,
|
|
1473
|
+
f_t.values,
|
|
1474
|
+
dz,
|
|
1475
|
+
dy,
|
|
1476
|
+
dx,
|
|
1477
|
+
bcs[0],
|
|
1478
|
+
bcs[1],
|
|
1479
|
+
bcs[2],
|
|
1480
|
+
optarg,
|
|
1481
|
+
_UNDEF,
|
|
1482
|
+
int(iparams["mxLoop"]),
|
|
1483
|
+
float(iparams["tolerance"]),
|
|
1484
|
+
)
|
|
1485
|
+
values[...] = solved
|
|
1486
|
+
if overflow:
|
|
1487
|
+
raise RuntimeError("Fortran SOR standard3d overflowed")
|
|
1488
|
+
else:
|
|
1489
|
+
for index in np.ndindex(s_t.shape[:-3]):
|
|
1490
|
+
solved, relerr, overflow, loops = fishpack.sor_standard3d(
|
|
1491
|
+
s_t.values[index],
|
|
1492
|
+
a_t.values[index],
|
|
1493
|
+
b_t.values[index],
|
|
1494
|
+
c_t.values[index],
|
|
1495
|
+
f_t.values[index],
|
|
1496
|
+
dz,
|
|
1497
|
+
dy,
|
|
1498
|
+
dx,
|
|
1499
|
+
bcs[0],
|
|
1500
|
+
bcs[1],
|
|
1501
|
+
bcs[2],
|
|
1502
|
+
optarg,
|
|
1503
|
+
_UNDEF,
|
|
1504
|
+
int(iparams["mxLoop"]),
|
|
1505
|
+
float(iparams["tolerance"]),
|
|
1506
|
+
)
|
|
1507
|
+
values[index] = solved
|
|
1508
|
+
if overflow:
|
|
1509
|
+
raise RuntimeError("Fortran SOR standard3d overflowed")
|
|
1510
|
+
|
|
1511
|
+
result = force.__class__(
|
|
1512
|
+
values,
|
|
1513
|
+
coords=s_t.coords,
|
|
1514
|
+
dims=s_t.dims,
|
|
1515
|
+
attrs=dict(force.attrs),
|
|
1516
|
+
name="inverted",
|
|
1517
|
+
)
|
|
1518
|
+
return result.transpose(*force.dims)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def _invert_standard_2d(
|
|
1522
|
+
F: Any,
|
|
1523
|
+
*,
|
|
1524
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1525
|
+
coords: str,
|
|
1526
|
+
iParams: dict[str, Any] | None,
|
|
1527
|
+
BCs: Sequence[str] | None,
|
|
1528
|
+
spacing: Sequence[float] | None,
|
|
1529
|
+
icbc: Any,
|
|
1530
|
+
coefficients: tuple[Any, Any, Any],
|
|
1531
|
+
) -> Any:
|
|
1532
|
+
if coords != "cartesian":
|
|
1533
|
+
raise NotImplementedError("standard-form SOR currently supports Cartesian coordinates only")
|
|
1534
|
+
iparams = _merged_iparams(iParams, ndim=2, BCs=BCs)
|
|
1535
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 2)
|
|
1536
|
+
if _is_dataarray(F):
|
|
1537
|
+
return _invert_standard_2d_labeled(
|
|
1538
|
+
F,
|
|
1539
|
+
dims=dims,
|
|
1540
|
+
bcs=bcs,
|
|
1541
|
+
spacing=spacing,
|
|
1542
|
+
iparams=iparams,
|
|
1543
|
+
icbc=icbc,
|
|
1544
|
+
coefficients=coefficients,
|
|
1545
|
+
)
|
|
1546
|
+
return _invert_standard_2d_ndarray(
|
|
1547
|
+
F,
|
|
1548
|
+
axes=dims,
|
|
1549
|
+
bcs=bcs,
|
|
1550
|
+
spacing=spacing,
|
|
1551
|
+
iparams=iparams,
|
|
1552
|
+
icbc=icbc,
|
|
1553
|
+
coefficients=coefficients,
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _invert_standard_2d_labeled(
|
|
1558
|
+
field: Any,
|
|
1559
|
+
*,
|
|
1560
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1561
|
+
bcs: tuple[str, ...],
|
|
1562
|
+
spacing: Sequence[float] | None,
|
|
1563
|
+
iparams: dict[str, Any],
|
|
1564
|
+
icbc: Any,
|
|
1565
|
+
coefficients: tuple[Any, Any, Any],
|
|
1566
|
+
) -> Any:
|
|
1567
|
+
if dims is None:
|
|
1568
|
+
if field.ndim < 2:
|
|
1569
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
1570
|
+
dims = field.dims[-2:]
|
|
1571
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
1572
|
+
raise TypeError("xarray standard-form inversion requires two dimension names")
|
|
1573
|
+
mask_f, init_s, _zero = _mask_labeled_field(field, dims, iparams, bcs, icbc)
|
|
1574
|
+
dy, dx = _spacing_for_labeled(mask_f, (dims[0], dims[1]), spacing)
|
|
1575
|
+
coefs = tuple(_as_labeled_like(coef, mask_f) for coef in coefficients)
|
|
1576
|
+
solved = _solve_sor2d_labeled(
|
|
1577
|
+
init_s,
|
|
1578
|
+
coefs[0],
|
|
1579
|
+
coefs[1],
|
|
1580
|
+
coefs[2],
|
|
1581
|
+
mask_f,
|
|
1582
|
+
dims=dims,
|
|
1583
|
+
dy=dy,
|
|
1584
|
+
dx=dx,
|
|
1585
|
+
bcs=bcs,
|
|
1586
|
+
iparams=iparams,
|
|
1587
|
+
)
|
|
1588
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
def _invert_standard_2d_ndarray(
|
|
1592
|
+
field: Any,
|
|
1593
|
+
*,
|
|
1594
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
1595
|
+
bcs: tuple[str, ...],
|
|
1596
|
+
spacing: Sequence[float] | None,
|
|
1597
|
+
iparams: dict[str, Any],
|
|
1598
|
+
icbc: Any,
|
|
1599
|
+
coefficients: tuple[Any, Any, Any],
|
|
1600
|
+
) -> np.ndarray:
|
|
1601
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
1602
|
+
if arr.ndim < 2:
|
|
1603
|
+
raise ValueError("standard-form inversion requires at least a two-dimensional array")
|
|
1604
|
+
if axes is None:
|
|
1605
|
+
axes_tuple = (arr.ndim - 2, arr.ndim - 1)
|
|
1606
|
+
else:
|
|
1607
|
+
if len(axes) != 2 or not all(isinstance(axis, int) for axis in axes):
|
|
1608
|
+
raise TypeError("NumPy standard-form inversion requires two integer axes")
|
|
1609
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
1610
|
+
if axes_tuple[0] == axes_tuple[1]:
|
|
1611
|
+
raise ValueError("inversion axes must be distinct")
|
|
1612
|
+
dy, dx = _normalize_spacing(spacing)
|
|
1613
|
+
moved = np.moveaxis(arr, axes_tuple, (-2, -1))
|
|
1614
|
+
init = (
|
|
1615
|
+
np.zeros_like(moved)
|
|
1616
|
+
if icbc is None
|
|
1617
|
+
else np.moveaxis(np.asarray(icbc, dtype=np.float64), axes_tuple, (-2, -1))
|
|
1618
|
+
)
|
|
1619
|
+
moved_coefficients = []
|
|
1620
|
+
for coef in coefficients:
|
|
1621
|
+
coef_arr = np.asarray(coef, dtype=np.float64)
|
|
1622
|
+
if coef_arr.ndim == arr.ndim and coef_arr.shape == arr.shape:
|
|
1623
|
+
coef_arr = np.moveaxis(coef_arr, axes_tuple, (-2, -1))
|
|
1624
|
+
moved_coefficients.append(coef_arr)
|
|
1625
|
+
coef_arrays = tuple(
|
|
1626
|
+
_broadcast_ndarray_coefficient(coef, moved.shape) for coef in moved_coefficients
|
|
1627
|
+
)
|
|
1628
|
+
solved = _solve_sor_standard2d_batched(
|
|
1629
|
+
init,
|
|
1630
|
+
coef_arrays[0],
|
|
1631
|
+
coef_arrays[1],
|
|
1632
|
+
coef_arrays[2],
|
|
1633
|
+
moved,
|
|
1634
|
+
dy=dy,
|
|
1635
|
+
dx=dx,
|
|
1636
|
+
bcs=bcs,
|
|
1637
|
+
iparams=iparams,
|
|
1638
|
+
)
|
|
1639
|
+
return np.moveaxis(solved, (-2, -1), axes_tuple)
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def _invert_standard_3d(
|
|
1643
|
+
F: Any,
|
|
1644
|
+
*,
|
|
1645
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1646
|
+
coords: str,
|
|
1647
|
+
iParams: dict[str, Any] | None,
|
|
1648
|
+
BCs: Sequence[str] | None,
|
|
1649
|
+
spacing: Sequence[float] | None,
|
|
1650
|
+
icbc: Any,
|
|
1651
|
+
coefficients: tuple[Any, Any, Any],
|
|
1652
|
+
) -> Any:
|
|
1653
|
+
if coords != "cartesian":
|
|
1654
|
+
raise NotImplementedError("standard-form 3D SOR currently supports Cartesian coordinates only")
|
|
1655
|
+
iparams = _merged_iparams(iParams, ndim=3, BCs=BCs)
|
|
1656
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 3)
|
|
1657
|
+
if bcs[0] == "periodic" or bcs[1] == "periodic":
|
|
1658
|
+
raise NotImplementedError("standard-form 3D SOR currently supports periodic boundaries only in x")
|
|
1659
|
+
if _is_dataarray(F):
|
|
1660
|
+
return _invert_standard_3d_labeled(
|
|
1661
|
+
F,
|
|
1662
|
+
dims=dims,
|
|
1663
|
+
bcs=bcs,
|
|
1664
|
+
spacing=spacing,
|
|
1665
|
+
iparams=iparams,
|
|
1666
|
+
icbc=icbc,
|
|
1667
|
+
coefficients=coefficients,
|
|
1668
|
+
)
|
|
1669
|
+
return _invert_standard_3d_ndarray(
|
|
1670
|
+
F,
|
|
1671
|
+
axes=dims,
|
|
1672
|
+
bcs=bcs,
|
|
1673
|
+
spacing=spacing,
|
|
1674
|
+
iparams=iparams,
|
|
1675
|
+
icbc=icbc,
|
|
1676
|
+
coefficients=coefficients,
|
|
1677
|
+
)
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _invert_standard_3d_labeled(
|
|
1681
|
+
field: Any,
|
|
1682
|
+
*,
|
|
1683
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1684
|
+
bcs: tuple[str, ...],
|
|
1685
|
+
spacing: Sequence[float] | None,
|
|
1686
|
+
iparams: dict[str, Any],
|
|
1687
|
+
icbc: Any,
|
|
1688
|
+
coefficients: tuple[Any, Any, Any],
|
|
1689
|
+
) -> Any:
|
|
1690
|
+
if dims is None:
|
|
1691
|
+
if field.ndim < 3:
|
|
1692
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 3 dimensions")
|
|
1693
|
+
dims = field.dims[-3:]
|
|
1694
|
+
if len(dims) != 3 or not all(isinstance(dim, str) for dim in dims):
|
|
1695
|
+
raise TypeError("xarray standard-form 3D inversion requires three dimension names")
|
|
1696
|
+
mask_f, init_s, _zero = _mask_labeled_field(field, dims, iparams, bcs, icbc)
|
|
1697
|
+
dz, dy, dx = _spacing_for_labeled3d(mask_f, (dims[0], dims[1], dims[2]), spacing)
|
|
1698
|
+
coefs = tuple(_as_labeled_like(coef, mask_f) for coef in coefficients)
|
|
1699
|
+
solved = _solve_sor3d_labeled(
|
|
1700
|
+
init_s,
|
|
1701
|
+
coefs[0],
|
|
1702
|
+
coefs[1],
|
|
1703
|
+
coefs[2],
|
|
1704
|
+
mask_f,
|
|
1705
|
+
dims=dims,
|
|
1706
|
+
dz=dz,
|
|
1707
|
+
dy=dy,
|
|
1708
|
+
dx=dx,
|
|
1709
|
+
bcs=bcs,
|
|
1710
|
+
iparams=iparams,
|
|
1711
|
+
)
|
|
1712
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
def _invert_standard_3d_ndarray(
|
|
1716
|
+
field: Any,
|
|
1717
|
+
*,
|
|
1718
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
1719
|
+
bcs: tuple[str, ...],
|
|
1720
|
+
spacing: Sequence[float] | None,
|
|
1721
|
+
iparams: dict[str, Any],
|
|
1722
|
+
icbc: Any,
|
|
1723
|
+
coefficients: tuple[Any, Any, Any],
|
|
1724
|
+
) -> np.ndarray:
|
|
1725
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
1726
|
+
if arr.ndim < 3:
|
|
1727
|
+
raise ValueError("standard-form 3D inversion requires at least a three-dimensional array")
|
|
1728
|
+
if axes is None:
|
|
1729
|
+
axes_tuple = (arr.ndim - 3, arr.ndim - 2, arr.ndim - 1)
|
|
1730
|
+
else:
|
|
1731
|
+
if len(axes) != 3 or not all(isinstance(axis, int) for axis in axes):
|
|
1732
|
+
raise TypeError("NumPy standard-form 3D inversion requires three integer axes")
|
|
1733
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
1734
|
+
if len(set(axes_tuple)) != 3:
|
|
1735
|
+
raise ValueError("inversion axes must be distinct")
|
|
1736
|
+
|
|
1737
|
+
dz, dy, dx = _normalize_spacing3d(spacing)
|
|
1738
|
+
moved = np.moveaxis(arr, axes_tuple, (-3, -2, -1))
|
|
1739
|
+
init = (
|
|
1740
|
+
np.zeros_like(moved)
|
|
1741
|
+
if icbc is None
|
|
1742
|
+
else np.moveaxis(np.asarray(icbc, dtype=np.float64), axes_tuple, (-3, -2, -1))
|
|
1743
|
+
)
|
|
1744
|
+
moved_coefficients = []
|
|
1745
|
+
for coef in coefficients:
|
|
1746
|
+
coef_arr = np.asarray(coef, dtype=np.float64)
|
|
1747
|
+
if coef_arr.ndim == arr.ndim and coef_arr.shape == arr.shape:
|
|
1748
|
+
coef_arr = np.moveaxis(coef_arr, axes_tuple, (-3, -2, -1))
|
|
1749
|
+
moved_coefficients.append(coef_arr)
|
|
1750
|
+
coef_arrays = tuple(
|
|
1751
|
+
_broadcast_ndarray_coefficient(coef, moved.shape) for coef in moved_coefficients
|
|
1752
|
+
)
|
|
1753
|
+
solved = _solve_sor_standard3d_batched(
|
|
1754
|
+
init,
|
|
1755
|
+
coef_arrays[0],
|
|
1756
|
+
coef_arrays[1],
|
|
1757
|
+
coef_arrays[2],
|
|
1758
|
+
moved,
|
|
1759
|
+
dz=dz,
|
|
1760
|
+
dy=dy,
|
|
1761
|
+
dx=dx,
|
|
1762
|
+
bcs=bcs,
|
|
1763
|
+
iparams=iparams,
|
|
1764
|
+
)
|
|
1765
|
+
return np.moveaxis(solved, (-3, -2, -1), axes_tuple)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _solve_sor_standard2d_batched(
|
|
1769
|
+
init_s: np.ndarray,
|
|
1770
|
+
acoef: np.ndarray,
|
|
1771
|
+
bcoef: np.ndarray,
|
|
1772
|
+
ccoef: np.ndarray,
|
|
1773
|
+
force: np.ndarray,
|
|
1774
|
+
*,
|
|
1775
|
+
dy: float,
|
|
1776
|
+
dx: float,
|
|
1777
|
+
bcs: tuple[str, ...],
|
|
1778
|
+
iparams: dict[str, Any],
|
|
1779
|
+
) -> np.ndarray:
|
|
1780
|
+
arrays = [np.asarray(item, dtype=np.float64) for item in (init_s, acoef, bcoef, ccoef, force)]
|
|
1781
|
+
shape = np.broadcast_shapes(*(item.shape for item in arrays))
|
|
1782
|
+
arrays = [np.broadcast_to(item, shape) for item in arrays]
|
|
1783
|
+
if len(shape) < 2:
|
|
1784
|
+
raise ValueError("standard-form SOR expects the last two dimensions to be spatial")
|
|
1785
|
+
values = np.empty(shape, dtype=np.float64)
|
|
1786
|
+
optarg = _sor_optarg(iparams, shape[-2:])
|
|
1787
|
+
if len(shape) == 2:
|
|
1788
|
+
solved, relerr, overflow, loops = fishpack.sor_standard2d(
|
|
1789
|
+
arrays[0],
|
|
1790
|
+
arrays[1],
|
|
1791
|
+
arrays[2],
|
|
1792
|
+
arrays[3],
|
|
1793
|
+
arrays[4],
|
|
1794
|
+
dy,
|
|
1795
|
+
dx,
|
|
1796
|
+
bcs[0],
|
|
1797
|
+
bcs[1],
|
|
1798
|
+
optarg,
|
|
1799
|
+
_UNDEF,
|
|
1800
|
+
int(iparams["mxLoop"]),
|
|
1801
|
+
float(iparams["tolerance"]),
|
|
1802
|
+
)
|
|
1803
|
+
if overflow:
|
|
1804
|
+
raise RuntimeError("Fortran SOR standard2d overflowed")
|
|
1805
|
+
values[...] = solved
|
|
1806
|
+
return values
|
|
1807
|
+
for index in np.ndindex(shape[:-2]):
|
|
1808
|
+
solved, relerr, overflow, loops = fishpack.sor_standard2d(
|
|
1809
|
+
arrays[0][index],
|
|
1810
|
+
arrays[1][index],
|
|
1811
|
+
arrays[2][index],
|
|
1812
|
+
arrays[3][index],
|
|
1813
|
+
arrays[4][index],
|
|
1814
|
+
dy,
|
|
1815
|
+
dx,
|
|
1816
|
+
bcs[0],
|
|
1817
|
+
bcs[1],
|
|
1818
|
+
optarg,
|
|
1819
|
+
_UNDEF,
|
|
1820
|
+
int(iparams["mxLoop"]),
|
|
1821
|
+
float(iparams["tolerance"]),
|
|
1822
|
+
)
|
|
1823
|
+
if overflow:
|
|
1824
|
+
raise RuntimeError("Fortran SOR standard2d overflowed")
|
|
1825
|
+
values[index] = solved
|
|
1826
|
+
return values
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def _solve_sor_standard3d_batched(
|
|
1830
|
+
init_s: np.ndarray,
|
|
1831
|
+
acoef: np.ndarray,
|
|
1832
|
+
bcoef: np.ndarray,
|
|
1833
|
+
ccoef: np.ndarray,
|
|
1834
|
+
force: np.ndarray,
|
|
1835
|
+
*,
|
|
1836
|
+
dz: float,
|
|
1837
|
+
dy: float,
|
|
1838
|
+
dx: float,
|
|
1839
|
+
bcs: tuple[str, ...],
|
|
1840
|
+
iparams: dict[str, Any],
|
|
1841
|
+
) -> np.ndarray:
|
|
1842
|
+
arrays = [np.asarray(item, dtype=np.float64) for item in (init_s, acoef, bcoef, ccoef, force)]
|
|
1843
|
+
shape = np.broadcast_shapes(*(item.shape for item in arrays))
|
|
1844
|
+
arrays = [np.broadcast_to(item, shape) for item in arrays]
|
|
1845
|
+
if len(shape) < 3:
|
|
1846
|
+
raise ValueError("standard-form 3D SOR expects the last three dimensions to be spatial")
|
|
1847
|
+
values = np.empty(shape, dtype=np.float64)
|
|
1848
|
+
optarg = _sor_optarg(iparams, shape[-3:])
|
|
1849
|
+
if len(shape) == 3:
|
|
1850
|
+
solved, relerr, overflow, loops = fishpack.sor_standard3d(
|
|
1851
|
+
arrays[0],
|
|
1852
|
+
arrays[1],
|
|
1853
|
+
arrays[2],
|
|
1854
|
+
arrays[3],
|
|
1855
|
+
arrays[4],
|
|
1856
|
+
dz,
|
|
1857
|
+
dy,
|
|
1858
|
+
dx,
|
|
1859
|
+
bcs[0],
|
|
1860
|
+
bcs[1],
|
|
1861
|
+
bcs[2],
|
|
1862
|
+
optarg,
|
|
1863
|
+
_UNDEF,
|
|
1864
|
+
int(iparams["mxLoop"]),
|
|
1865
|
+
float(iparams["tolerance"]),
|
|
1866
|
+
)
|
|
1867
|
+
if overflow:
|
|
1868
|
+
raise RuntimeError("Fortran SOR standard3d overflowed")
|
|
1869
|
+
values[...] = solved
|
|
1870
|
+
return values
|
|
1871
|
+
for index in np.ndindex(shape[:-3]):
|
|
1872
|
+
solved, relerr, overflow, loops = fishpack.sor_standard3d(
|
|
1873
|
+
arrays[0][index],
|
|
1874
|
+
arrays[1][index],
|
|
1875
|
+
arrays[2][index],
|
|
1876
|
+
arrays[3][index],
|
|
1877
|
+
arrays[4][index],
|
|
1878
|
+
dz,
|
|
1879
|
+
dy,
|
|
1880
|
+
dx,
|
|
1881
|
+
bcs[0],
|
|
1882
|
+
bcs[1],
|
|
1883
|
+
bcs[2],
|
|
1884
|
+
optarg,
|
|
1885
|
+
_UNDEF,
|
|
1886
|
+
int(iparams["mxLoop"]),
|
|
1887
|
+
float(iparams["tolerance"]),
|
|
1888
|
+
)
|
|
1889
|
+
if overflow:
|
|
1890
|
+
raise RuntimeError("Fortran SOR standard3d overflowed")
|
|
1891
|
+
values[index] = solved
|
|
1892
|
+
return values
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
def _invert_general_2d(
|
|
1896
|
+
G: Any,
|
|
1897
|
+
*,
|
|
1898
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1899
|
+
coords: str,
|
|
1900
|
+
iParams: dict[str, Any] | None,
|
|
1901
|
+
BCs: Sequence[str] | None,
|
|
1902
|
+
spacing: Sequence[float] | None,
|
|
1903
|
+
icbc: Any,
|
|
1904
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any],
|
|
1905
|
+
) -> Any:
|
|
1906
|
+
if coords != "cartesian":
|
|
1907
|
+
raise NotImplementedError("general-form SOR currently supports Cartesian coordinates only")
|
|
1908
|
+
iparams = _merged_iparams(iParams, ndim=2, BCs=BCs)
|
|
1909
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 2)
|
|
1910
|
+
if _is_dataarray(G):
|
|
1911
|
+
return _invert_general_2d_labeled(
|
|
1912
|
+
G,
|
|
1913
|
+
dims=dims,
|
|
1914
|
+
bcs=bcs,
|
|
1915
|
+
spacing=spacing,
|
|
1916
|
+
iparams=iparams,
|
|
1917
|
+
icbc=icbc,
|
|
1918
|
+
coefficients=coefficients,
|
|
1919
|
+
)
|
|
1920
|
+
return _invert_general_2d_ndarray(
|
|
1921
|
+
G,
|
|
1922
|
+
axes=dims,
|
|
1923
|
+
bcs=bcs,
|
|
1924
|
+
spacing=spacing,
|
|
1925
|
+
iparams=iparams,
|
|
1926
|
+
icbc=icbc,
|
|
1927
|
+
coefficients=coefficients,
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _invert_general_2d_labeled(
|
|
1932
|
+
field: Any,
|
|
1933
|
+
*,
|
|
1934
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
1935
|
+
bcs: tuple[str, ...],
|
|
1936
|
+
spacing: Sequence[float] | None,
|
|
1937
|
+
iparams: dict[str, Any],
|
|
1938
|
+
icbc: Any,
|
|
1939
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any],
|
|
1940
|
+
) -> Any:
|
|
1941
|
+
if dims is None:
|
|
1942
|
+
if field.ndim < 2:
|
|
1943
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
1944
|
+
dims = field.dims[-2:]
|
|
1945
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
1946
|
+
raise TypeError("xarray general-form inversion requires two dimension names")
|
|
1947
|
+
mask_f, init_s, zero = _mask_labeled_field(field, dims, iparams, bcs, icbc)
|
|
1948
|
+
dy, dx = _spacing_for_labeled(mask_f, (dims[0], dims[1]), spacing)
|
|
1949
|
+
coefs = tuple(_as_labeled_like(coef, mask_f) for coef in coefficients)
|
|
1950
|
+
solved = _solve_sor_general2d_labeled(
|
|
1951
|
+
init_s,
|
|
1952
|
+
*coefs,
|
|
1953
|
+
mask_f,
|
|
1954
|
+
dims=dims,
|
|
1955
|
+
dy=dy,
|
|
1956
|
+
dx=dx,
|
|
1957
|
+
bcs=bcs,
|
|
1958
|
+
iparams=iparams,
|
|
1959
|
+
)
|
|
1960
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def _invert_general_2d_ndarray(
|
|
1964
|
+
field: Any,
|
|
1965
|
+
*,
|
|
1966
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
1967
|
+
bcs: tuple[str, ...],
|
|
1968
|
+
spacing: Sequence[float] | None,
|
|
1969
|
+
iparams: dict[str, Any],
|
|
1970
|
+
icbc: Any,
|
|
1971
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any],
|
|
1972
|
+
) -> np.ndarray:
|
|
1973
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
1974
|
+
if arr.ndim < 2:
|
|
1975
|
+
raise ValueError("general-form inversion requires at least a two-dimensional array")
|
|
1976
|
+
if axes is None:
|
|
1977
|
+
axes_tuple = (arr.ndim - 2, arr.ndim - 1)
|
|
1978
|
+
else:
|
|
1979
|
+
if len(axes) != 2 or not all(isinstance(axis, int) for axis in axes):
|
|
1980
|
+
raise TypeError("NumPy general-form inversion requires two integer axes")
|
|
1981
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
1982
|
+
if axes_tuple[0] == axes_tuple[1]:
|
|
1983
|
+
raise ValueError("inversion axes must be distinct")
|
|
1984
|
+
dy, dx = _normalize_spacing(spacing)
|
|
1985
|
+
moved = np.moveaxis(arr, axes_tuple, (-2, -1))
|
|
1986
|
+
init = np.zeros_like(moved) if icbc is None else np.moveaxis(np.asarray(icbc, dtype=np.float64), axes_tuple, (-2, -1))
|
|
1987
|
+
moved_coefficients = []
|
|
1988
|
+
for coef in coefficients:
|
|
1989
|
+
coef_arr = np.asarray(coef, dtype=np.float64)
|
|
1990
|
+
if coef_arr.ndim == arr.ndim and coef_arr.shape == arr.shape:
|
|
1991
|
+
coef_arr = np.moveaxis(coef_arr, axes_tuple, (-2, -1))
|
|
1992
|
+
moved_coefficients.append(coef_arr)
|
|
1993
|
+
coef_arrays = tuple(_broadcast_ndarray_coefficient(coef, moved.shape) for coef in moved_coefficients)
|
|
1994
|
+
solved = _solve_sor_general2d_batched(
|
|
1995
|
+
init,
|
|
1996
|
+
*coef_arrays,
|
|
1997
|
+
moved,
|
|
1998
|
+
dy=dy,
|
|
1999
|
+
dx=dx,
|
|
2000
|
+
bcs=bcs,
|
|
2001
|
+
iparams=iparams,
|
|
2002
|
+
)
|
|
2003
|
+
return np.moveaxis(solved, (-2, -1), axes_tuple)
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
def _invert_general_3d(
|
|
2007
|
+
H: Any,
|
|
2008
|
+
*,
|
|
2009
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2010
|
+
coords: str,
|
|
2011
|
+
iParams: dict[str, Any] | None,
|
|
2012
|
+
BCs: Sequence[str] | None,
|
|
2013
|
+
spacing: Sequence[float] | None,
|
|
2014
|
+
icbc: Any,
|
|
2015
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any],
|
|
2016
|
+
) -> Any:
|
|
2017
|
+
if coords != "cartesian":
|
|
2018
|
+
raise NotImplementedError("general-form 3D SOR currently supports Cartesian coordinates only")
|
|
2019
|
+
iparams = _merged_iparams(iParams, ndim=3, BCs=BCs)
|
|
2020
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 3)
|
|
2021
|
+
if bcs[0] == "periodic" or bcs[1] == "periodic":
|
|
2022
|
+
raise NotImplementedError("general-form 3D SOR currently supports periodic boundaries only in x")
|
|
2023
|
+
if _is_dataarray(H):
|
|
2024
|
+
return _invert_general_3d_labeled(
|
|
2025
|
+
H,
|
|
2026
|
+
dims=dims,
|
|
2027
|
+
bcs=bcs,
|
|
2028
|
+
spacing=spacing,
|
|
2029
|
+
iparams=iparams,
|
|
2030
|
+
icbc=icbc,
|
|
2031
|
+
coefficients=coefficients,
|
|
2032
|
+
)
|
|
2033
|
+
return _invert_general_3d_ndarray(
|
|
2034
|
+
H,
|
|
2035
|
+
axes=dims,
|
|
2036
|
+
bcs=bcs,
|
|
2037
|
+
spacing=spacing,
|
|
2038
|
+
iparams=iparams,
|
|
2039
|
+
icbc=icbc,
|
|
2040
|
+
coefficients=coefficients,
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
def _invert_general_3d_labeled(
|
|
2045
|
+
field: Any,
|
|
2046
|
+
*,
|
|
2047
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2048
|
+
bcs: tuple[str, ...],
|
|
2049
|
+
spacing: Sequence[float] | None,
|
|
2050
|
+
iparams: dict[str, Any],
|
|
2051
|
+
icbc: Any,
|
|
2052
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any],
|
|
2053
|
+
) -> Any:
|
|
2054
|
+
if dims is None:
|
|
2055
|
+
if field.ndim < 3:
|
|
2056
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 3 dimensions")
|
|
2057
|
+
dims = field.dims[-3:]
|
|
2058
|
+
if len(dims) != 3 or not all(isinstance(dim, str) for dim in dims):
|
|
2059
|
+
raise TypeError("xarray general-form 3D inversion requires three dimension names")
|
|
2060
|
+
mask_f, init_s, _zero = _mask_labeled_field(field, dims, iparams, bcs, icbc)
|
|
2061
|
+
dz, dy, dx = _spacing_for_labeled3d(mask_f, (dims[0], dims[1], dims[2]), spacing)
|
|
2062
|
+
coefs = tuple(_as_labeled_like(coef, mask_f) for coef in coefficients)
|
|
2063
|
+
solved = _solve_sor_general3d_labeled(
|
|
2064
|
+
init_s,
|
|
2065
|
+
*coefs,
|
|
2066
|
+
mask_f,
|
|
2067
|
+
dims=dims,
|
|
2068
|
+
dz=dz,
|
|
2069
|
+
dy=dy,
|
|
2070
|
+
dx=dx,
|
|
2071
|
+
bcs=bcs,
|
|
2072
|
+
iparams=iparams,
|
|
2073
|
+
)
|
|
2074
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def _invert_general_3d_ndarray(
|
|
2078
|
+
field: Any,
|
|
2079
|
+
*,
|
|
2080
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
2081
|
+
bcs: tuple[str, ...],
|
|
2082
|
+
spacing: Sequence[float] | None,
|
|
2083
|
+
iparams: dict[str, Any],
|
|
2084
|
+
icbc: Any,
|
|
2085
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any],
|
|
2086
|
+
) -> np.ndarray:
|
|
2087
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
2088
|
+
if arr.ndim < 3:
|
|
2089
|
+
raise ValueError("general-form 3D inversion requires at least a three-dimensional array")
|
|
2090
|
+
if axes is None:
|
|
2091
|
+
axes_tuple = (arr.ndim - 3, arr.ndim - 2, arr.ndim - 1)
|
|
2092
|
+
else:
|
|
2093
|
+
if len(axes) != 3 or not all(isinstance(axis, int) for axis in axes):
|
|
2094
|
+
raise TypeError("NumPy general-form 3D inversion requires three integer axes")
|
|
2095
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
2096
|
+
if len(set(axes_tuple)) != 3:
|
|
2097
|
+
raise ValueError("inversion axes must be distinct")
|
|
2098
|
+
|
|
2099
|
+
dz, dy, dx = _normalize_spacing3d(spacing)
|
|
2100
|
+
moved = np.moveaxis(arr, axes_tuple, (-3, -2, -1))
|
|
2101
|
+
init = (
|
|
2102
|
+
np.zeros_like(moved)
|
|
2103
|
+
if icbc is None
|
|
2104
|
+
else np.moveaxis(np.asarray(icbc, dtype=np.float64), axes_tuple, (-3, -2, -1))
|
|
2105
|
+
)
|
|
2106
|
+
moved_coefficients = []
|
|
2107
|
+
for coef in coefficients:
|
|
2108
|
+
coef_arr = np.asarray(coef, dtype=np.float64)
|
|
2109
|
+
if coef_arr.ndim == arr.ndim and coef_arr.shape == arr.shape:
|
|
2110
|
+
coef_arr = np.moveaxis(coef_arr, axes_tuple, (-3, -2, -1))
|
|
2111
|
+
moved_coefficients.append(coef_arr)
|
|
2112
|
+
coef_arrays = tuple(
|
|
2113
|
+
_broadcast_ndarray_coefficient(coef, moved.shape) for coef in moved_coefficients
|
|
2114
|
+
)
|
|
2115
|
+
solved = _solve_sor_general3d_batched(
|
|
2116
|
+
init,
|
|
2117
|
+
*coef_arrays,
|
|
2118
|
+
moved,
|
|
2119
|
+
dz=dz,
|
|
2120
|
+
dy=dy,
|
|
2121
|
+
dx=dx,
|
|
2122
|
+
bcs=bcs,
|
|
2123
|
+
iparams=iparams,
|
|
2124
|
+
)
|
|
2125
|
+
return np.moveaxis(solved, (-3, -2, -1), axes_tuple)
|
|
2126
|
+
|
|
2127
|
+
|
|
2128
|
+
def _solve_sor_general2d_labeled(
|
|
2129
|
+
init_s: Any,
|
|
2130
|
+
acoef: Any,
|
|
2131
|
+
bcoef: Any,
|
|
2132
|
+
ccoef: Any,
|
|
2133
|
+
dcoef: Any,
|
|
2134
|
+
ecoef: Any,
|
|
2135
|
+
fcoef: Any,
|
|
2136
|
+
force: Any,
|
|
2137
|
+
*,
|
|
2138
|
+
dims: Sequence[str],
|
|
2139
|
+
dy: float,
|
|
2140
|
+
dx: float,
|
|
2141
|
+
bcs: tuple[str, ...],
|
|
2142
|
+
iparams: dict[str, Any],
|
|
2143
|
+
) -> Any:
|
|
2144
|
+
import xarray as xr
|
|
2145
|
+
|
|
2146
|
+
ydim, xdim = dims
|
|
2147
|
+
acoef, bcoef, ccoef, dcoef, ecoef, fcoef, force, init_s = xr.broadcast(
|
|
2148
|
+
acoef, bcoef, ccoef, dcoef, ecoef, fcoef, force, init_s
|
|
2149
|
+
)
|
|
2150
|
+
outer_dims = tuple(dim for dim in force.dims if dim not in dims)
|
|
2151
|
+
order = (*outer_dims, ydim, xdim)
|
|
2152
|
+
arrays = [item.transpose(*order).values for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, force)]
|
|
2153
|
+
values = _solve_sor_general2d_batched(
|
|
2154
|
+
arrays[0],
|
|
2155
|
+
arrays[1],
|
|
2156
|
+
arrays[2],
|
|
2157
|
+
arrays[3],
|
|
2158
|
+
arrays[4],
|
|
2159
|
+
arrays[5],
|
|
2160
|
+
arrays[6],
|
|
2161
|
+
arrays[7],
|
|
2162
|
+
dy=dy,
|
|
2163
|
+
dx=dx,
|
|
2164
|
+
bcs=bcs,
|
|
2165
|
+
iparams=iparams,
|
|
2166
|
+
)
|
|
2167
|
+
template = force.transpose(*order)
|
|
2168
|
+
result = force.__class__(
|
|
2169
|
+
values,
|
|
2170
|
+
coords=template.coords,
|
|
2171
|
+
dims=template.dims,
|
|
2172
|
+
attrs=dict(force.attrs),
|
|
2173
|
+
name="inverted",
|
|
2174
|
+
)
|
|
2175
|
+
return result.transpose(*force.dims)
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def _solve_sor_general2d_batched(
|
|
2179
|
+
init_s: np.ndarray,
|
|
2180
|
+
acoef: np.ndarray,
|
|
2181
|
+
bcoef: np.ndarray,
|
|
2182
|
+
ccoef: np.ndarray,
|
|
2183
|
+
dcoef: np.ndarray,
|
|
2184
|
+
ecoef: np.ndarray,
|
|
2185
|
+
fcoef: np.ndarray,
|
|
2186
|
+
force: np.ndarray,
|
|
2187
|
+
*,
|
|
2188
|
+
dy: float,
|
|
2189
|
+
dx: float,
|
|
2190
|
+
bcs: tuple[str, ...],
|
|
2191
|
+
iparams: dict[str, Any],
|
|
2192
|
+
) -> np.ndarray:
|
|
2193
|
+
arrays = [np.asarray(item, dtype=np.float64) for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, force)]
|
|
2194
|
+
shape = np.broadcast_shapes(*(item.shape for item in arrays))
|
|
2195
|
+
arrays = [np.broadcast_to(item, shape) for item in arrays]
|
|
2196
|
+
if len(shape) < 2:
|
|
2197
|
+
raise ValueError("general-form SOR expects the last two dimensions to be spatial")
|
|
2198
|
+
values = np.empty(shape, dtype=np.float64)
|
|
2199
|
+
optarg = _sor_optarg(iparams, shape[-2:])
|
|
2200
|
+
if len(shape) == 2:
|
|
2201
|
+
solved, relerr, overflow, loops = fishpack.sor_general2d(
|
|
2202
|
+
arrays[0], arrays[1], arrays[2], arrays[3], arrays[4], arrays[5], arrays[6], arrays[7],
|
|
2203
|
+
dy, dx, bcs[0], bcs[1], optarg, _UNDEF, int(iparams["mxLoop"]), float(iparams["tolerance"])
|
|
2204
|
+
)
|
|
2205
|
+
if overflow:
|
|
2206
|
+
raise RuntimeError("Fortran SOR general2d overflowed")
|
|
2207
|
+
values[...] = solved
|
|
2208
|
+
return values
|
|
2209
|
+
for index in np.ndindex(shape[:-2]):
|
|
2210
|
+
solved, relerr, overflow, loops = fishpack.sor_general2d(
|
|
2211
|
+
arrays[0][index], arrays[1][index], arrays[2][index], arrays[3][index],
|
|
2212
|
+
arrays[4][index], arrays[5][index], arrays[6][index], arrays[7][index],
|
|
2213
|
+
dy, dx, bcs[0], bcs[1], optarg, _UNDEF, int(iparams["mxLoop"]), float(iparams["tolerance"])
|
|
2214
|
+
)
|
|
2215
|
+
if overflow:
|
|
2216
|
+
raise RuntimeError("Fortran SOR general2d overflowed")
|
|
2217
|
+
values[index] = solved
|
|
2218
|
+
return values
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
def _solve_sor_general3d_labeled(
|
|
2222
|
+
init_s: Any,
|
|
2223
|
+
acoef: Any,
|
|
2224
|
+
bcoef: Any,
|
|
2225
|
+
ccoef: Any,
|
|
2226
|
+
dcoef: Any,
|
|
2227
|
+
ecoef: Any,
|
|
2228
|
+
fcoef: Any,
|
|
2229
|
+
gcoef: Any,
|
|
2230
|
+
force: Any,
|
|
2231
|
+
*,
|
|
2232
|
+
dims: Sequence[str],
|
|
2233
|
+
dz: float,
|
|
2234
|
+
dy: float,
|
|
2235
|
+
dx: float,
|
|
2236
|
+
bcs: tuple[str, ...],
|
|
2237
|
+
iparams: dict[str, Any],
|
|
2238
|
+
) -> Any:
|
|
2239
|
+
import xarray as xr
|
|
2240
|
+
|
|
2241
|
+
zdim, ydim, xdim = dims
|
|
2242
|
+
arrays = xr.broadcast(acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, force, init_s)
|
|
2243
|
+
acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, force, init_s = arrays
|
|
2244
|
+
outer_dims = tuple(dim for dim in force.dims if dim not in dims)
|
|
2245
|
+
order = (*outer_dims, zdim, ydim, xdim)
|
|
2246
|
+
transposed_force = force.transpose(*order)
|
|
2247
|
+
arrays_np = [
|
|
2248
|
+
item.transpose(*order).values
|
|
2249
|
+
for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, force)
|
|
2250
|
+
]
|
|
2251
|
+
values = _solve_sor_general3d_batched(
|
|
2252
|
+
arrays_np[0],
|
|
2253
|
+
arrays_np[1],
|
|
2254
|
+
arrays_np[2],
|
|
2255
|
+
arrays_np[3],
|
|
2256
|
+
arrays_np[4],
|
|
2257
|
+
arrays_np[5],
|
|
2258
|
+
arrays_np[6],
|
|
2259
|
+
arrays_np[7],
|
|
2260
|
+
arrays_np[8],
|
|
2261
|
+
dz=dz,
|
|
2262
|
+
dy=dy,
|
|
2263
|
+
dx=dx,
|
|
2264
|
+
bcs=bcs,
|
|
2265
|
+
iparams=iparams,
|
|
2266
|
+
)
|
|
2267
|
+
result = force.__class__(
|
|
2268
|
+
values,
|
|
2269
|
+
coords=transposed_force.coords,
|
|
2270
|
+
dims=transposed_force.dims,
|
|
2271
|
+
attrs=dict(force.attrs),
|
|
2272
|
+
name="inverted",
|
|
2273
|
+
)
|
|
2274
|
+
return result.transpose(*force.dims)
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
def _solve_sor_general3d_batched(
|
|
2278
|
+
init_s: np.ndarray,
|
|
2279
|
+
acoef: np.ndarray,
|
|
2280
|
+
bcoef: np.ndarray,
|
|
2281
|
+
ccoef: np.ndarray,
|
|
2282
|
+
dcoef: np.ndarray,
|
|
2283
|
+
ecoef: np.ndarray,
|
|
2284
|
+
fcoef: np.ndarray,
|
|
2285
|
+
gcoef: np.ndarray,
|
|
2286
|
+
force: np.ndarray,
|
|
2287
|
+
*,
|
|
2288
|
+
dz: float,
|
|
2289
|
+
dy: float,
|
|
2290
|
+
dx: float,
|
|
2291
|
+
bcs: tuple[str, ...],
|
|
2292
|
+
iparams: dict[str, Any],
|
|
2293
|
+
) -> np.ndarray:
|
|
2294
|
+
arrays = [
|
|
2295
|
+
np.asarray(item, dtype=np.float64)
|
|
2296
|
+
for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, force)
|
|
2297
|
+
]
|
|
2298
|
+
shape = np.broadcast_shapes(*(item.shape for item in arrays))
|
|
2299
|
+
arrays = [np.broadcast_to(item, shape) for item in arrays]
|
|
2300
|
+
if len(shape) < 3:
|
|
2301
|
+
raise ValueError("general-form 3D SOR expects the last three dimensions to be spatial")
|
|
2302
|
+
values = np.empty(shape, dtype=np.float64)
|
|
2303
|
+
optarg = _sor_optarg(iparams, shape[-3:])
|
|
2304
|
+
if len(shape) == 3:
|
|
2305
|
+
solved, relerr, overflow, loops = fishpack.sor_general3d(
|
|
2306
|
+
arrays[0],
|
|
2307
|
+
arrays[1],
|
|
2308
|
+
arrays[2],
|
|
2309
|
+
arrays[3],
|
|
2310
|
+
arrays[4],
|
|
2311
|
+
arrays[5],
|
|
2312
|
+
arrays[6],
|
|
2313
|
+
arrays[7],
|
|
2314
|
+
arrays[8],
|
|
2315
|
+
dz,
|
|
2316
|
+
dy,
|
|
2317
|
+
dx,
|
|
2318
|
+
bcs[0],
|
|
2319
|
+
bcs[1],
|
|
2320
|
+
bcs[2],
|
|
2321
|
+
optarg,
|
|
2322
|
+
_UNDEF,
|
|
2323
|
+
int(iparams["mxLoop"]),
|
|
2324
|
+
float(iparams["tolerance"]),
|
|
2325
|
+
)
|
|
2326
|
+
if overflow:
|
|
2327
|
+
raise RuntimeError("Fortran SOR general3d overflowed")
|
|
2328
|
+
values[...] = solved
|
|
2329
|
+
return values
|
|
2330
|
+
for index in np.ndindex(shape[:-3]):
|
|
2331
|
+
solved, relerr, overflow, loops = fishpack.sor_general3d(
|
|
2332
|
+
arrays[0][index],
|
|
2333
|
+
arrays[1][index],
|
|
2334
|
+
arrays[2][index],
|
|
2335
|
+
arrays[3][index],
|
|
2336
|
+
arrays[4][index],
|
|
2337
|
+
arrays[5][index],
|
|
2338
|
+
arrays[6][index],
|
|
2339
|
+
arrays[7][index],
|
|
2340
|
+
arrays[8][index],
|
|
2341
|
+
dz,
|
|
2342
|
+
dy,
|
|
2343
|
+
dx,
|
|
2344
|
+
bcs[0],
|
|
2345
|
+
bcs[1],
|
|
2346
|
+
bcs[2],
|
|
2347
|
+
optarg,
|
|
2348
|
+
_UNDEF,
|
|
2349
|
+
int(iparams["mxLoop"]),
|
|
2350
|
+
float(iparams["tolerance"]),
|
|
2351
|
+
)
|
|
2352
|
+
if overflow:
|
|
2353
|
+
raise RuntimeError("Fortran SOR general3d overflowed")
|
|
2354
|
+
values[index] = solved
|
|
2355
|
+
return values
|
|
2356
|
+
|
|
2357
|
+
|
|
2358
|
+
def _invert_biharmonic_2d(
|
|
2359
|
+
J: Any,
|
|
2360
|
+
*,
|
|
2361
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2362
|
+
coords: str,
|
|
2363
|
+
iParams: dict[str, Any] | None,
|
|
2364
|
+
BCs: Sequence[str] | None,
|
|
2365
|
+
spacing: Sequence[float] | None,
|
|
2366
|
+
icbc: Any,
|
|
2367
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any, Any, Any],
|
|
2368
|
+
) -> Any:
|
|
2369
|
+
if coords != "cartesian":
|
|
2370
|
+
raise NotImplementedError("biharmonic SOR currently supports Cartesian coordinates only")
|
|
2371
|
+
iparams = _merged_iparams(iParams, ndim=2, BCs=BCs)
|
|
2372
|
+
bcs = _normalize_sor_bcs(iparams["BCs"], 2)
|
|
2373
|
+
if _is_dataarray(J):
|
|
2374
|
+
return _invert_biharmonic_2d_labeled(
|
|
2375
|
+
J,
|
|
2376
|
+
dims=dims,
|
|
2377
|
+
bcs=bcs,
|
|
2378
|
+
spacing=spacing,
|
|
2379
|
+
iparams=iparams,
|
|
2380
|
+
icbc=icbc,
|
|
2381
|
+
coefficients=coefficients,
|
|
2382
|
+
)
|
|
2383
|
+
return _invert_biharmonic_2d_ndarray(
|
|
2384
|
+
J,
|
|
2385
|
+
axes=dims,
|
|
2386
|
+
bcs=bcs,
|
|
2387
|
+
spacing=spacing,
|
|
2388
|
+
iparams=iparams,
|
|
2389
|
+
icbc=icbc,
|
|
2390
|
+
coefficients=coefficients,
|
|
2391
|
+
)
|
|
2392
|
+
|
|
2393
|
+
|
|
2394
|
+
def _invert_biharmonic_2d_labeled(
|
|
2395
|
+
field: Any,
|
|
2396
|
+
*,
|
|
2397
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2398
|
+
bcs: tuple[str, ...],
|
|
2399
|
+
spacing: Sequence[float] | None,
|
|
2400
|
+
iparams: dict[str, Any],
|
|
2401
|
+
icbc: Any,
|
|
2402
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any, Any, Any],
|
|
2403
|
+
) -> Any:
|
|
2404
|
+
if dims is None:
|
|
2405
|
+
if field.ndim < 2:
|
|
2406
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
2407
|
+
dims = field.dims[-2:]
|
|
2408
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
2409
|
+
raise TypeError("xarray biharmonic inversion requires two dimension names")
|
|
2410
|
+
mask_f, init_s, _zero = _mask_labeled_field(field, dims, iparams, bcs, icbc)
|
|
2411
|
+
dy, dx = _spacing_for_labeled(mask_f, (dims[0], dims[1]), spacing)
|
|
2412
|
+
coefs = tuple(_as_labeled_like(coef, mask_f) for coef in coefficients)
|
|
2413
|
+
solved = _solve_sor_biharmonic2d_labeled(
|
|
2414
|
+
init_s,
|
|
2415
|
+
*coefs,
|
|
2416
|
+
mask_f,
|
|
2417
|
+
dims=dims,
|
|
2418
|
+
dy=dy,
|
|
2419
|
+
dx=dx,
|
|
2420
|
+
bcs=bcs,
|
|
2421
|
+
iparams=iparams,
|
|
2422
|
+
)
|
|
2423
|
+
return _restore_labeled_result(solved, mask_f, iparams, icbc)
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
def _invert_biharmonic_2d_ndarray(
|
|
2427
|
+
field: Any,
|
|
2428
|
+
*,
|
|
2429
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
2430
|
+
bcs: tuple[str, ...],
|
|
2431
|
+
spacing: Sequence[float] | None,
|
|
2432
|
+
iparams: dict[str, Any],
|
|
2433
|
+
icbc: Any,
|
|
2434
|
+
coefficients: tuple[Any, Any, Any, Any, Any, Any, Any, Any, Any],
|
|
2435
|
+
) -> np.ndarray:
|
|
2436
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
2437
|
+
if arr.ndim < 2:
|
|
2438
|
+
raise ValueError("biharmonic inversion requires at least a two-dimensional array")
|
|
2439
|
+
if axes is None:
|
|
2440
|
+
axes_tuple = (arr.ndim - 2, arr.ndim - 1)
|
|
2441
|
+
else:
|
|
2442
|
+
if len(axes) != 2 or not all(isinstance(axis, int) for axis in axes):
|
|
2443
|
+
raise TypeError("NumPy biharmonic inversion requires two integer axes")
|
|
2444
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
2445
|
+
if axes_tuple[0] == axes_tuple[1]:
|
|
2446
|
+
raise ValueError("inversion axes must be distinct")
|
|
2447
|
+
dy, dx = _normalize_spacing(spacing)
|
|
2448
|
+
moved = np.moveaxis(arr, axes_tuple, (-2, -1))
|
|
2449
|
+
init = np.zeros_like(moved) if icbc is None else np.moveaxis(np.asarray(icbc, dtype=np.float64), axes_tuple, (-2, -1))
|
|
2450
|
+
moved_coefficients = []
|
|
2451
|
+
for coef in coefficients:
|
|
2452
|
+
coef_arr = np.asarray(coef, dtype=np.float64)
|
|
2453
|
+
if coef_arr.ndim == arr.ndim and coef_arr.shape == arr.shape:
|
|
2454
|
+
coef_arr = np.moveaxis(coef_arr, axes_tuple, (-2, -1))
|
|
2455
|
+
moved_coefficients.append(coef_arr)
|
|
2456
|
+
coef_arrays = tuple(_broadcast_ndarray_coefficient(coef, moved.shape) for coef in moved_coefficients)
|
|
2457
|
+
solved = _solve_sor_biharmonic2d_batched(
|
|
2458
|
+
init,
|
|
2459
|
+
*coef_arrays,
|
|
2460
|
+
moved,
|
|
2461
|
+
dy=dy,
|
|
2462
|
+
dx=dx,
|
|
2463
|
+
bcs=bcs,
|
|
2464
|
+
iparams=iparams,
|
|
2465
|
+
)
|
|
2466
|
+
return np.moveaxis(solved, (-2, -1), axes_tuple)
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def _solve_sor_biharmonic2d_labeled(
|
|
2470
|
+
init_s: Any,
|
|
2471
|
+
acoef: Any,
|
|
2472
|
+
bcoef: Any,
|
|
2473
|
+
ccoef: Any,
|
|
2474
|
+
dcoef: Any,
|
|
2475
|
+
ecoef: Any,
|
|
2476
|
+
fcoef: Any,
|
|
2477
|
+
gcoef: Any,
|
|
2478
|
+
hcoef: Any,
|
|
2479
|
+
icoef: Any,
|
|
2480
|
+
force: Any,
|
|
2481
|
+
*,
|
|
2482
|
+
dims: Sequence[str],
|
|
2483
|
+
dy: float,
|
|
2484
|
+
dx: float,
|
|
2485
|
+
bcs: tuple[str, ...],
|
|
2486
|
+
iparams: dict[str, Any],
|
|
2487
|
+
) -> Any:
|
|
2488
|
+
import xarray as xr
|
|
2489
|
+
|
|
2490
|
+
ydim, xdim = dims
|
|
2491
|
+
arrays = xr.broadcast(acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, hcoef, icoef, force, init_s)
|
|
2492
|
+
acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, hcoef, icoef, force, init_s = arrays
|
|
2493
|
+
outer_dims = tuple(dim for dim in force.dims if dim not in dims)
|
|
2494
|
+
order = (*outer_dims, ydim, xdim)
|
|
2495
|
+
arrays_np = [
|
|
2496
|
+
item.transpose(*order).values
|
|
2497
|
+
for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, hcoef, icoef, force)
|
|
2498
|
+
]
|
|
2499
|
+
values = _solve_sor_biharmonic2d_batched(
|
|
2500
|
+
arrays_np[0],
|
|
2501
|
+
arrays_np[1],
|
|
2502
|
+
arrays_np[2],
|
|
2503
|
+
arrays_np[3],
|
|
2504
|
+
arrays_np[4],
|
|
2505
|
+
arrays_np[5],
|
|
2506
|
+
arrays_np[6],
|
|
2507
|
+
arrays_np[7],
|
|
2508
|
+
arrays_np[8],
|
|
2509
|
+
arrays_np[9],
|
|
2510
|
+
arrays_np[10],
|
|
2511
|
+
dy=dy,
|
|
2512
|
+
dx=dx,
|
|
2513
|
+
bcs=bcs,
|
|
2514
|
+
iparams=iparams,
|
|
2515
|
+
)
|
|
2516
|
+
template = force.transpose(*order)
|
|
2517
|
+
result = force.__class__(
|
|
2518
|
+
values,
|
|
2519
|
+
coords=template.coords,
|
|
2520
|
+
dims=template.dims,
|
|
2521
|
+
attrs=dict(force.attrs),
|
|
2522
|
+
name="inverted",
|
|
2523
|
+
)
|
|
2524
|
+
return result.transpose(*force.dims)
|
|
2525
|
+
|
|
2526
|
+
|
|
2527
|
+
def _solve_sor_biharmonic2d_batched(
|
|
2528
|
+
init_s: np.ndarray,
|
|
2529
|
+
acoef: np.ndarray,
|
|
2530
|
+
bcoef: np.ndarray,
|
|
2531
|
+
ccoef: np.ndarray,
|
|
2532
|
+
dcoef: np.ndarray,
|
|
2533
|
+
ecoef: np.ndarray,
|
|
2534
|
+
fcoef: np.ndarray,
|
|
2535
|
+
gcoef: np.ndarray,
|
|
2536
|
+
hcoef: np.ndarray,
|
|
2537
|
+
icoef: np.ndarray,
|
|
2538
|
+
force: np.ndarray,
|
|
2539
|
+
*,
|
|
2540
|
+
dy: float,
|
|
2541
|
+
dx: float,
|
|
2542
|
+
bcs: tuple[str, ...],
|
|
2543
|
+
iparams: dict[str, Any],
|
|
2544
|
+
) -> np.ndarray:
|
|
2545
|
+
arrays = [
|
|
2546
|
+
np.asarray(item, dtype=np.float64)
|
|
2547
|
+
for item in (init_s, acoef, bcoef, ccoef, dcoef, ecoef, fcoef, gcoef, hcoef, icoef, force)
|
|
2548
|
+
]
|
|
2549
|
+
shape = np.broadcast_shapes(*(item.shape for item in arrays))
|
|
2550
|
+
arrays = [np.broadcast_to(item, shape) for item in arrays]
|
|
2551
|
+
if len(shape) < 2:
|
|
2552
|
+
raise ValueError("biharmonic SOR expects the last two dimensions to be spatial")
|
|
2553
|
+
values = np.empty(shape, dtype=np.float64)
|
|
2554
|
+
optarg = _sor_optarg(iparams, shape[-2:])
|
|
2555
|
+
if len(shape) == 2:
|
|
2556
|
+
solved, relerr, overflow, loops = fishpack.sor_biharmonic2d(
|
|
2557
|
+
arrays[0], arrays[1], arrays[2], arrays[3], arrays[4], arrays[5],
|
|
2558
|
+
arrays[6], arrays[7], arrays[8], arrays[9], arrays[10],
|
|
2559
|
+
dy, dx, bcs[0], bcs[1], optarg, _UNDEF, int(iparams["mxLoop"]), float(iparams["tolerance"])
|
|
2560
|
+
)
|
|
2561
|
+
if overflow:
|
|
2562
|
+
raise RuntimeError("Fortran SOR biharmonic2d overflowed")
|
|
2563
|
+
values[...] = solved
|
|
2564
|
+
return values
|
|
2565
|
+
for index in np.ndindex(shape[:-2]):
|
|
2566
|
+
solved, relerr, overflow, loops = fishpack.sor_biharmonic2d(
|
|
2567
|
+
arrays[0][index], arrays[1][index], arrays[2][index], arrays[3][index],
|
|
2568
|
+
arrays[4][index], arrays[5][index], arrays[6][index], arrays[7][index],
|
|
2569
|
+
arrays[8][index], arrays[9][index], arrays[10][index],
|
|
2570
|
+
dy, dx, bcs[0], bcs[1], optarg, _UNDEF, int(iparams["mxLoop"]), float(iparams["tolerance"])
|
|
2571
|
+
)
|
|
2572
|
+
if overflow:
|
|
2573
|
+
raise RuntimeError("Fortran SOR biharmonic2d overflowed")
|
|
2574
|
+
values[index] = solved
|
|
2575
|
+
return values
|
|
2576
|
+
|
|
2577
|
+
|
|
2578
|
+
def _broadcast_ndarray_coefficient(coef: Any, shape: tuple[int, ...]) -> np.ndarray:
|
|
2579
|
+
arr = np.asarray(coef, dtype=np.float64)
|
|
2580
|
+
if arr.ndim == 0:
|
|
2581
|
+
return np.full(shape, float(arr), dtype=np.float64)
|
|
2582
|
+
return np.broadcast_to(arr, shape).astype(np.float64, copy=False)
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
def _solve_sor1d_labeled(
|
|
2586
|
+
init_s: Any,
|
|
2587
|
+
acoef: Any,
|
|
2588
|
+
bcoef: Any,
|
|
2589
|
+
force: Any,
|
|
2590
|
+
*,
|
|
2591
|
+
dims: Sequence[str],
|
|
2592
|
+
dx: float,
|
|
2593
|
+
bcs: tuple[str, ...],
|
|
2594
|
+
iparams: dict[str, Any],
|
|
2595
|
+
) -> Any:
|
|
2596
|
+
import xarray as xr
|
|
2597
|
+
|
|
2598
|
+
(dim,) = dims
|
|
2599
|
+
acoef, bcoef, force, init_s = xr.broadcast(acoef, bcoef, force, init_s)
|
|
2600
|
+
outer_dims = tuple(item for item in force.dims if item != dim)
|
|
2601
|
+
order = (*outer_dims, dim)
|
|
2602
|
+
a_t = acoef.transpose(*order)
|
|
2603
|
+
b_t = bcoef.transpose(*order)
|
|
2604
|
+
f_t = force.transpose(*order)
|
|
2605
|
+
s_t = init_s.transpose(*order)
|
|
2606
|
+
values = np.empty(s_t.shape, dtype=np.float64)
|
|
2607
|
+
optarg = _sor_optarg(iparams, (s_t.shape[-1],))
|
|
2608
|
+
|
|
2609
|
+
for index in np.ndindex(s_t.shape[:-1] or ()):
|
|
2610
|
+
key = index if s_t.ndim > 1 else (...,)
|
|
2611
|
+
if s_t.ndim == 1:
|
|
2612
|
+
solved, relerr, overflow, loops = fishpack.sor_standard1d(
|
|
2613
|
+
s_t.values,
|
|
2614
|
+
a_t.values,
|
|
2615
|
+
b_t.values,
|
|
2616
|
+
f_t.values,
|
|
2617
|
+
dx,
|
|
2618
|
+
bcs[0],
|
|
2619
|
+
optarg,
|
|
2620
|
+
_UNDEF,
|
|
2621
|
+
int(iparams["mxLoop"]),
|
|
2622
|
+
float(iparams["tolerance"]),
|
|
2623
|
+
)
|
|
2624
|
+
values[...] = solved
|
|
2625
|
+
if overflow:
|
|
2626
|
+
raise RuntimeError("Fortran SOR standard1d overflowed")
|
|
2627
|
+
break
|
|
2628
|
+
solved, relerr, overflow, loops = fishpack.sor_standard1d(
|
|
2629
|
+
s_t.values[key],
|
|
2630
|
+
a_t.values[key],
|
|
2631
|
+
b_t.values[key],
|
|
2632
|
+
f_t.values[key],
|
|
2633
|
+
dx,
|
|
2634
|
+
bcs[0],
|
|
2635
|
+
optarg,
|
|
2636
|
+
_UNDEF,
|
|
2637
|
+
int(iparams["mxLoop"]),
|
|
2638
|
+
float(iparams["tolerance"]),
|
|
2639
|
+
)
|
|
2640
|
+
values[key] = solved
|
|
2641
|
+
if overflow:
|
|
2642
|
+
raise RuntimeError("Fortran SOR standard1d overflowed")
|
|
2643
|
+
|
|
2644
|
+
result = force.__class__(
|
|
2645
|
+
values,
|
|
2646
|
+
coords=s_t.coords,
|
|
2647
|
+
dims=s_t.dims,
|
|
2648
|
+
attrs=dict(force.attrs),
|
|
2649
|
+
name="inverted",
|
|
2650
|
+
)
|
|
2651
|
+
return result.transpose(*force.dims)
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
def _sor_optarg(iparams: dict[str, Any], shape: tuple[int, ...]) -> float:
|
|
2655
|
+
if iparams.get("optArg") is not None:
|
|
2656
|
+
return float(iparams["optArg"])
|
|
2657
|
+
if len(shape) == 1:
|
|
2658
|
+
epsilon = np.sin(np.pi / (2.0 * shape[0] + 2.0)) ** 2
|
|
2659
|
+
elif len(shape) == 2:
|
|
2660
|
+
epsilon = (
|
|
2661
|
+
np.sin(np.pi / (2.0 * shape[1] + 2.0)) ** 2
|
|
2662
|
+
+ np.sin(np.pi / (2.0 * shape[0] + 2.0)) ** 2
|
|
2663
|
+
)
|
|
2664
|
+
elif len(shape) == 3:
|
|
2665
|
+
epsilon = (
|
|
2666
|
+
np.sin(np.pi / (2.0 * shape[2] + 2.0)) ** 2
|
|
2667
|
+
+ np.sin(np.pi / (2.0 * shape[1] + 2.0)) ** 2
|
|
2668
|
+
+ np.sin(np.pi / (2.0 * shape[0] + 2.0)) ** 2
|
|
2669
|
+
)
|
|
2670
|
+
else:
|
|
2671
|
+
raise ValueError("SOR optArg supports one, two, or three dimensions")
|
|
2672
|
+
return float(2.0 / (1.0 + np.sqrt((2.0 - epsilon) * epsilon)))
|
|
2673
|
+
|
|
2674
|
+
|
|
2675
|
+
def _sor_spacing2d(field: Any, dims: tuple[str, str], coords: str, rearth: float) -> tuple[float, float]:
|
|
2676
|
+
ydim, xdim = dims
|
|
2677
|
+
dy = _uniform_coord_spacing(field.coords[ydim].values, ydim)
|
|
2678
|
+
dx = _uniform_coord_spacing(field.coords[xdim].values, xdim)
|
|
2679
|
+
if coords.lower() == "z-lat":
|
|
2680
|
+
dx = np.deg2rad(dx) * rearth
|
|
2681
|
+
return dy, dx
|
|
2682
|
+
|
|
2683
|
+
|
|
2684
|
+
def _sor_spacing1d(field: Any, dim: str, coords: str, rearth: float) -> float:
|
|
2685
|
+
dx = _uniform_coord_spacing(field.coords[dim].values, dim)
|
|
2686
|
+
if coords.lower() == "lat":
|
|
2687
|
+
dx = np.deg2rad(dx) * rearth
|
|
2688
|
+
return dx
|
|
2689
|
+
|
|
2690
|
+
|
|
2691
|
+
def _as_labeled_like(value: Any, template: Any) -> Any:
|
|
2692
|
+
if _is_dataarray(value):
|
|
2693
|
+
return value
|
|
2694
|
+
return (template * 0.0) + value
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
def _second_diff_swm(m0: Any, cos_h: Any, delx: float, dim: str) -> Any:
|
|
2698
|
+
shifted_forward = m0.shift({dim: -1})
|
|
2699
|
+
shifted_backward = m0.shift({dim: 1})
|
|
2700
|
+
flux_forward = (shifted_forward - m0) / cos_h.shift({dim: -1})
|
|
2701
|
+
flux_backward = (m0 - shifted_backward) / cos_h
|
|
2702
|
+
diff = (flux_forward - flux_backward) / (delx * delx)
|
|
2703
|
+
return diff.fillna(0.0)
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
def _invert_constant_helmholtz(
|
|
2707
|
+
F: Any,
|
|
2708
|
+
*,
|
|
2709
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2710
|
+
coords: str,
|
|
2711
|
+
iParams: dict[str, Any] | None,
|
|
2712
|
+
BCs: Sequence[str] | None,
|
|
2713
|
+
spacing: Sequence[float] | None,
|
|
2714
|
+
helmholtz: float,
|
|
2715
|
+
raise_on_error: bool,
|
|
2716
|
+
) -> Any:
|
|
2717
|
+
return _invert_constant_2d(
|
|
2718
|
+
F,
|
|
2719
|
+
dims=dims,
|
|
2720
|
+
coords=coords,
|
|
2721
|
+
iParams=iParams,
|
|
2722
|
+
BCs=BCs,
|
|
2723
|
+
spacing=spacing,
|
|
2724
|
+
coefficients=(1.0, 1.0),
|
|
2725
|
+
helmholtz=helmholtz,
|
|
2726
|
+
raise_on_error=raise_on_error,
|
|
2727
|
+
)
|
|
2728
|
+
|
|
2729
|
+
|
|
2730
|
+
def _invert_constant_2d(
|
|
2731
|
+
F: Any,
|
|
2732
|
+
*,
|
|
2733
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2734
|
+
coords: str,
|
|
2735
|
+
iParams: dict[str, Any] | None,
|
|
2736
|
+
BCs: Sequence[str] | None,
|
|
2737
|
+
spacing: Sequence[float] | None,
|
|
2738
|
+
coefficients: tuple[float, float],
|
|
2739
|
+
helmholtz: float,
|
|
2740
|
+
raise_on_error: bool,
|
|
2741
|
+
) -> Any:
|
|
2742
|
+
if coords != "cartesian":
|
|
2743
|
+
raise NotImplementedError("only Cartesian constant-coefficient equations are supported")
|
|
2744
|
+
params = dict(iParams or {})
|
|
2745
|
+
bcs = _normalize_bcs(BCs if BCs is not None else params.get("BCs", None))
|
|
2746
|
+
if _is_dataarray(F):
|
|
2747
|
+
return _invert_constant_2d_labeled(
|
|
2748
|
+
F,
|
|
2749
|
+
dims=dims,
|
|
2750
|
+
bcs=bcs,
|
|
2751
|
+
spacing=spacing,
|
|
2752
|
+
coefficients=coefficients,
|
|
2753
|
+
helmholtz=helmholtz,
|
|
2754
|
+
raise_on_error=raise_on_error,
|
|
2755
|
+
)
|
|
2756
|
+
return _invert_constant_2d_ndarray(
|
|
2757
|
+
F,
|
|
2758
|
+
axes=dims,
|
|
2759
|
+
bcs=bcs,
|
|
2760
|
+
spacing=spacing,
|
|
2761
|
+
coefficients=coefficients,
|
|
2762
|
+
helmholtz=helmholtz,
|
|
2763
|
+
raise_on_error=raise_on_error,
|
|
2764
|
+
)
|
|
2765
|
+
|
|
2766
|
+
|
|
2767
|
+
def _invert_constant_3d(
|
|
2768
|
+
F: Any,
|
|
2769
|
+
*,
|
|
2770
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2771
|
+
coords: str,
|
|
2772
|
+
iParams: dict[str, Any] | None,
|
|
2773
|
+
BCs: Sequence[str] | None,
|
|
2774
|
+
spacing: Sequence[float] | None,
|
|
2775
|
+
coefficients: tuple[float, float, float],
|
|
2776
|
+
raise_on_error: bool,
|
|
2777
|
+
) -> Any:
|
|
2778
|
+
if coords != "cartesian":
|
|
2779
|
+
raise NotImplementedError("only Cartesian constant-coefficient 3D equations are supported")
|
|
2780
|
+
params = dict(iParams or {})
|
|
2781
|
+
bcs = _normalize_bcs_n(BCs if BCs is not None else params.get("BCs", None), 3)
|
|
2782
|
+
if _is_dataarray(F):
|
|
2783
|
+
return _invert_constant_3d_labeled(
|
|
2784
|
+
F,
|
|
2785
|
+
dims=dims,
|
|
2786
|
+
bcs=bcs,
|
|
2787
|
+
spacing=spacing,
|
|
2788
|
+
coefficients=coefficients,
|
|
2789
|
+
raise_on_error=raise_on_error,
|
|
2790
|
+
)
|
|
2791
|
+
return _invert_constant_3d_ndarray(
|
|
2792
|
+
F,
|
|
2793
|
+
axes=dims,
|
|
2794
|
+
bcs=bcs,
|
|
2795
|
+
spacing=spacing,
|
|
2796
|
+
coefficients=coefficients,
|
|
2797
|
+
raise_on_error=raise_on_error,
|
|
2798
|
+
)
|
|
2799
|
+
|
|
2800
|
+
|
|
2801
|
+
def _invert_poisson_labeled(
|
|
2802
|
+
field: Any,
|
|
2803
|
+
*,
|
|
2804
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2805
|
+
bcs: tuple[str, str],
|
|
2806
|
+
spacing: Sequence[float] | None,
|
|
2807
|
+
raise_on_error: bool,
|
|
2808
|
+
) -> Any:
|
|
2809
|
+
if dims is None:
|
|
2810
|
+
if field.ndim < 2:
|
|
2811
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
2812
|
+
dims = field.dims[-2:]
|
|
2813
|
+
if len(dims) != 2:
|
|
2814
|
+
raise ValueError("invert_Poisson requires exactly two inversion dimensions")
|
|
2815
|
+
if not all(isinstance(dim, str) for dim in dims):
|
|
2816
|
+
raise TypeError("xarray inputs require dimension names in dims")
|
|
2817
|
+
missing = [dim for dim in dims if dim not in field.dims]
|
|
2818
|
+
if missing:
|
|
2819
|
+
raise ValueError(f"dims not present in input DataArray: {missing}")
|
|
2820
|
+
|
|
2821
|
+
ydim, xdim = dims
|
|
2822
|
+
dy, dx = _spacing_for_labeled(field, (ydim, xdim), spacing)
|
|
2823
|
+
outer_dims = tuple(dim for dim in field.dims if dim not in dims)
|
|
2824
|
+
transposed = field.transpose(*outer_dims, ydim, xdim)
|
|
2825
|
+
|
|
2826
|
+
values = _solve_poisson_batched(
|
|
2827
|
+
transposed.values,
|
|
2828
|
+
dy=dy,
|
|
2829
|
+
dx=dx,
|
|
2830
|
+
bcs=bcs,
|
|
2831
|
+
raise_on_error=raise_on_error,
|
|
2832
|
+
)
|
|
2833
|
+
|
|
2834
|
+
result = field.__class__(
|
|
2835
|
+
values,
|
|
2836
|
+
coords=transposed.coords,
|
|
2837
|
+
dims=transposed.dims,
|
|
2838
|
+
attrs=dict(field.attrs),
|
|
2839
|
+
name="inverted" if field.name is None else f"{field.name}_inverted",
|
|
2840
|
+
)
|
|
2841
|
+
return result.transpose(*field.dims)
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
def _invert_poisson_ndarray(
|
|
2845
|
+
field: Any,
|
|
2846
|
+
*,
|
|
2847
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
2848
|
+
bcs: tuple[str, str],
|
|
2849
|
+
spacing: Sequence[float] | None,
|
|
2850
|
+
raise_on_error: bool,
|
|
2851
|
+
) -> np.ndarray:
|
|
2852
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
2853
|
+
if arr.ndim < 2:
|
|
2854
|
+
raise ValueError("invert_Poisson requires at least a two-dimensional array")
|
|
2855
|
+
if axes is None:
|
|
2856
|
+
axes_tuple = (arr.ndim - 2, arr.ndim - 1)
|
|
2857
|
+
else:
|
|
2858
|
+
if len(axes) != 2:
|
|
2859
|
+
raise ValueError("invert_Poisson requires exactly two inversion axes")
|
|
2860
|
+
if not all(isinstance(axis, int) for axis in axes):
|
|
2861
|
+
raise TypeError("NumPy inputs require integer axes or axes=None")
|
|
2862
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
2863
|
+
if axes_tuple[0] == axes_tuple[1]:
|
|
2864
|
+
raise ValueError("inversion axes must be distinct")
|
|
2865
|
+
|
|
2866
|
+
dy, dx = _normalize_spacing(spacing)
|
|
2867
|
+
moved = np.moveaxis(arr, axes_tuple, (-2, -1))
|
|
2868
|
+
solved = _solve_poisson_batched(
|
|
2869
|
+
moved, dy=dy, dx=dx, bcs=bcs, raise_on_error=raise_on_error
|
|
2870
|
+
)
|
|
2871
|
+
return np.moveaxis(solved, (-2, -1), axes_tuple)
|
|
2872
|
+
|
|
2873
|
+
|
|
2874
|
+
def _invert_constant_2d_labeled(
|
|
2875
|
+
field: Any,
|
|
2876
|
+
*,
|
|
2877
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2878
|
+
bcs: tuple[str, str],
|
|
2879
|
+
spacing: Sequence[float] | None,
|
|
2880
|
+
coefficients: tuple[float, float],
|
|
2881
|
+
helmholtz: float,
|
|
2882
|
+
raise_on_error: bool,
|
|
2883
|
+
) -> Any:
|
|
2884
|
+
if dims is None:
|
|
2885
|
+
if field.ndim < 2:
|
|
2886
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
2887
|
+
dims = field.dims[-2:]
|
|
2888
|
+
if len(dims) != 2:
|
|
2889
|
+
raise ValueError("constant-coefficient inversion requires exactly two dimensions")
|
|
2890
|
+
if not all(isinstance(dim, str) for dim in dims):
|
|
2891
|
+
raise TypeError("xarray inputs require dimension names in dims")
|
|
2892
|
+
missing = [dim for dim in dims if dim not in field.dims]
|
|
2893
|
+
if missing:
|
|
2894
|
+
raise ValueError(f"dims not present in input DataArray: {missing}")
|
|
2895
|
+
|
|
2896
|
+
ydim, xdim = dims
|
|
2897
|
+
dy, dx = _spacing_for_labeled(field, (ydim, xdim), spacing)
|
|
2898
|
+
outer_dims = tuple(dim for dim in field.dims if dim not in dims)
|
|
2899
|
+
transposed = field.transpose(*outer_dims, ydim, xdim)
|
|
2900
|
+
values = _solve_constant_2d_batched(
|
|
2901
|
+
transposed.values,
|
|
2902
|
+
dy=dy,
|
|
2903
|
+
dx=dx,
|
|
2904
|
+
bcs=bcs,
|
|
2905
|
+
coefficients=coefficients,
|
|
2906
|
+
helmholtz=helmholtz,
|
|
2907
|
+
raise_on_error=raise_on_error,
|
|
2908
|
+
)
|
|
2909
|
+
result = field.__class__(
|
|
2910
|
+
values,
|
|
2911
|
+
coords=transposed.coords,
|
|
2912
|
+
dims=transposed.dims,
|
|
2913
|
+
attrs=dict(field.attrs),
|
|
2914
|
+
name="inverted" if field.name is None else f"{field.name}_inverted",
|
|
2915
|
+
)
|
|
2916
|
+
return result.transpose(*field.dims)
|
|
2917
|
+
|
|
2918
|
+
|
|
2919
|
+
def _invert_constant_2d_ndarray(
|
|
2920
|
+
field: Any,
|
|
2921
|
+
*,
|
|
2922
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
2923
|
+
bcs: tuple[str, str],
|
|
2924
|
+
spacing: Sequence[float] | None,
|
|
2925
|
+
coefficients: tuple[float, float],
|
|
2926
|
+
helmholtz: float,
|
|
2927
|
+
raise_on_error: bool,
|
|
2928
|
+
) -> np.ndarray:
|
|
2929
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
2930
|
+
if arr.ndim < 2:
|
|
2931
|
+
raise ValueError("constant-coefficient inversion requires at least a two-dimensional array")
|
|
2932
|
+
if axes is None:
|
|
2933
|
+
axes_tuple = (arr.ndim - 2, arr.ndim - 1)
|
|
2934
|
+
else:
|
|
2935
|
+
if len(axes) != 2:
|
|
2936
|
+
raise ValueError("constant-coefficient inversion requires exactly two inversion axes")
|
|
2937
|
+
if not all(isinstance(axis, int) for axis in axes):
|
|
2938
|
+
raise TypeError("NumPy inputs require integer axes or axes=None")
|
|
2939
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
2940
|
+
if axes_tuple[0] == axes_tuple[1]:
|
|
2941
|
+
raise ValueError("inversion axes must be distinct")
|
|
2942
|
+
|
|
2943
|
+
dy, dx = _normalize_spacing(spacing)
|
|
2944
|
+
moved = np.moveaxis(arr, axes_tuple, (-2, -1))
|
|
2945
|
+
solved = _solve_constant_2d_batched(
|
|
2946
|
+
moved,
|
|
2947
|
+
dy=dy,
|
|
2948
|
+
dx=dx,
|
|
2949
|
+
bcs=bcs,
|
|
2950
|
+
coefficients=coefficients,
|
|
2951
|
+
helmholtz=helmholtz,
|
|
2952
|
+
raise_on_error=raise_on_error,
|
|
2953
|
+
)
|
|
2954
|
+
return np.moveaxis(solved, (-2, -1), axes_tuple)
|
|
2955
|
+
|
|
2956
|
+
|
|
2957
|
+
def _invert_constant_3d_labeled(
|
|
2958
|
+
field: Any,
|
|
2959
|
+
*,
|
|
2960
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
2961
|
+
bcs: tuple[str, str, str],
|
|
2962
|
+
spacing: Sequence[float] | None,
|
|
2963
|
+
coefficients: tuple[float, float, float],
|
|
2964
|
+
raise_on_error: bool,
|
|
2965
|
+
) -> Any:
|
|
2966
|
+
if dims is None:
|
|
2967
|
+
if field.ndim < 3:
|
|
2968
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 3 dimensions")
|
|
2969
|
+
dims = field.dims[-3:]
|
|
2970
|
+
if len(dims) != 3:
|
|
2971
|
+
raise ValueError("constant-coefficient 3D inversion requires exactly three dimensions")
|
|
2972
|
+
if not all(isinstance(dim, str) for dim in dims):
|
|
2973
|
+
raise TypeError("xarray inputs require dimension names in dims")
|
|
2974
|
+
missing = [dim for dim in dims if dim not in field.dims]
|
|
2975
|
+
if missing:
|
|
2976
|
+
raise ValueError(f"dims not present in input DataArray: {missing}")
|
|
2977
|
+
|
|
2978
|
+
zdim, ydim, xdim = dims
|
|
2979
|
+
dz, dy, dx = _spacing_for_labeled3d(field, (zdim, ydim, xdim), spacing)
|
|
2980
|
+
outer_dims = tuple(dim for dim in field.dims if dim not in dims)
|
|
2981
|
+
transposed = field.transpose(*outer_dims, zdim, ydim, xdim)
|
|
2982
|
+
values = _solve_constant_3d_batched(
|
|
2983
|
+
transposed.values,
|
|
2984
|
+
dz=dz,
|
|
2985
|
+
dy=dy,
|
|
2986
|
+
dx=dx,
|
|
2987
|
+
bcs=bcs,
|
|
2988
|
+
coefficients=coefficients,
|
|
2989
|
+
raise_on_error=raise_on_error,
|
|
2990
|
+
)
|
|
2991
|
+
result = field.__class__(
|
|
2992
|
+
values,
|
|
2993
|
+
coords=transposed.coords,
|
|
2994
|
+
dims=transposed.dims,
|
|
2995
|
+
attrs=dict(field.attrs),
|
|
2996
|
+
name="inverted" if field.name is None else f"{field.name}_inverted",
|
|
2997
|
+
)
|
|
2998
|
+
return result.transpose(*field.dims)
|
|
2999
|
+
|
|
3000
|
+
|
|
3001
|
+
def _invert_constant_3d_ndarray(
|
|
3002
|
+
field: Any,
|
|
3003
|
+
*,
|
|
3004
|
+
axes: Sequence[str] | Sequence[int] | None,
|
|
3005
|
+
bcs: tuple[str, str, str],
|
|
3006
|
+
spacing: Sequence[float] | None,
|
|
3007
|
+
coefficients: tuple[float, float, float],
|
|
3008
|
+
raise_on_error: bool,
|
|
3009
|
+
) -> np.ndarray:
|
|
3010
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3011
|
+
if arr.ndim < 3:
|
|
3012
|
+
raise ValueError("constant-coefficient 3D inversion requires at least a three-dimensional array")
|
|
3013
|
+
if axes is None:
|
|
3014
|
+
axes_tuple = (arr.ndim - 3, arr.ndim - 2, arr.ndim - 1)
|
|
3015
|
+
else:
|
|
3016
|
+
if len(axes) != 3:
|
|
3017
|
+
raise ValueError("constant-coefficient 3D inversion requires exactly three inversion axes")
|
|
3018
|
+
if not all(isinstance(axis, int) for axis in axes):
|
|
3019
|
+
raise TypeError("NumPy inputs require integer axes or axes=None")
|
|
3020
|
+
axes_tuple = tuple(axis % arr.ndim for axis in axes)
|
|
3021
|
+
if len(set(axes_tuple)) != 3:
|
|
3022
|
+
raise ValueError("inversion axes must be distinct")
|
|
3023
|
+
|
|
3024
|
+
dz, dy, dx = _normalize_spacing3d(spacing)
|
|
3025
|
+
moved = np.moveaxis(arr, axes_tuple, (-3, -2, -1))
|
|
3026
|
+
solved = _solve_constant_3d_batched(
|
|
3027
|
+
moved,
|
|
3028
|
+
dz=dz,
|
|
3029
|
+
dy=dy,
|
|
3030
|
+
dx=dx,
|
|
3031
|
+
bcs=bcs,
|
|
3032
|
+
coefficients=coefficients,
|
|
3033
|
+
raise_on_error=raise_on_error,
|
|
3034
|
+
)
|
|
3035
|
+
return np.moveaxis(solved, (-3, -2, -1), axes_tuple)
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
def _solve_poisson_batched(
|
|
3039
|
+
values: np.ndarray,
|
|
3040
|
+
*,
|
|
3041
|
+
dy: float,
|
|
3042
|
+
dx: float,
|
|
3043
|
+
bcs: tuple[str, str],
|
|
3044
|
+
raise_on_error: bool,
|
|
3045
|
+
) -> np.ndarray:
|
|
3046
|
+
arr = np.asarray(values, dtype=np.float64)
|
|
3047
|
+
if arr.ndim < 2:
|
|
3048
|
+
raise ValueError("Poisson solve expects the last two dimensions to be spatial")
|
|
3049
|
+
return _solve_helmholtz_batched(
|
|
3050
|
+
arr,
|
|
3051
|
+
dy=dy,
|
|
3052
|
+
dx=dx,
|
|
3053
|
+
bcs=bcs,
|
|
3054
|
+
helmholtz=0.0,
|
|
3055
|
+
raise_on_error=raise_on_error,
|
|
3056
|
+
)
|
|
3057
|
+
|
|
3058
|
+
|
|
3059
|
+
def _solve_helmholtz_batched(
|
|
3060
|
+
values: np.ndarray,
|
|
3061
|
+
*,
|
|
3062
|
+
dy: float,
|
|
3063
|
+
dx: float,
|
|
3064
|
+
bcs: tuple[str, str],
|
|
3065
|
+
helmholtz: float,
|
|
3066
|
+
raise_on_error: bool,
|
|
3067
|
+
) -> np.ndarray:
|
|
3068
|
+
return _solve_constant_2d_batched(
|
|
3069
|
+
values,
|
|
3070
|
+
dy=dy,
|
|
3071
|
+
dx=dx,
|
|
3072
|
+
bcs=bcs,
|
|
3073
|
+
coefficients=(1.0, 1.0),
|
|
3074
|
+
helmholtz=helmholtz,
|
|
3075
|
+
raise_on_error=raise_on_error,
|
|
3076
|
+
)
|
|
3077
|
+
|
|
3078
|
+
|
|
3079
|
+
def _solve_constant_2d_batched(
|
|
3080
|
+
values: np.ndarray,
|
|
3081
|
+
*,
|
|
3082
|
+
dy: float,
|
|
3083
|
+
dx: float,
|
|
3084
|
+
bcs: tuple[str, str],
|
|
3085
|
+
coefficients: tuple[float, float],
|
|
3086
|
+
helmholtz: float,
|
|
3087
|
+
raise_on_error: bool,
|
|
3088
|
+
) -> np.ndarray:
|
|
3089
|
+
arr = np.asarray(values, dtype=np.float64)
|
|
3090
|
+
if arr.ndim < 2:
|
|
3091
|
+
raise ValueError("2D constant-coefficient solve expects the last two dimensions to be spatial")
|
|
3092
|
+
m, n = arr.shape[-2:]
|
|
3093
|
+
if m <= 2 or n <= 2:
|
|
3094
|
+
raise ValueError("Fishpack genbun requires both spatial dimensions to be > 2")
|
|
3095
|
+
|
|
3096
|
+
result = np.empty(arr.shape, dtype=np.float64, order="C")
|
|
3097
|
+
if arr.ndim == 2:
|
|
3098
|
+
result[...] = _solve_constant_2d(
|
|
3099
|
+
arr,
|
|
3100
|
+
dy=dy,
|
|
3101
|
+
dx=dx,
|
|
3102
|
+
bcs=bcs,
|
|
3103
|
+
coefficients=coefficients,
|
|
3104
|
+
helmholtz=helmholtz,
|
|
3105
|
+
raise_on_error=raise_on_error,
|
|
3106
|
+
)
|
|
3107
|
+
else:
|
|
3108
|
+
for index in np.ndindex(arr.shape[:-2]):
|
|
3109
|
+
result[index] = _solve_constant_2d(
|
|
3110
|
+
arr[index],
|
|
3111
|
+
dy=dy,
|
|
3112
|
+
dx=dx,
|
|
3113
|
+
bcs=bcs,
|
|
3114
|
+
coefficients=coefficients,
|
|
3115
|
+
helmholtz=helmholtz,
|
|
3116
|
+
raise_on_error=raise_on_error,
|
|
3117
|
+
)
|
|
3118
|
+
return result
|
|
3119
|
+
|
|
3120
|
+
|
|
3121
|
+
def _solve_poisson_2d(
|
|
3122
|
+
force: np.ndarray,
|
|
3123
|
+
*,
|
|
3124
|
+
dy: float,
|
|
3125
|
+
dx: float,
|
|
3126
|
+
bcs: tuple[str, str],
|
|
3127
|
+
raise_on_error: bool,
|
|
3128
|
+
) -> np.ndarray:
|
|
3129
|
+
return _solve_helmholtz_2d(
|
|
3130
|
+
force,
|
|
3131
|
+
dy=dy,
|
|
3132
|
+
dx=dx,
|
|
3133
|
+
bcs=bcs,
|
|
3134
|
+
helmholtz=0.0,
|
|
3135
|
+
raise_on_error=raise_on_error,
|
|
3136
|
+
)
|
|
3137
|
+
|
|
3138
|
+
|
|
3139
|
+
def _solve_helmholtz_2d(
|
|
3140
|
+
force: np.ndarray,
|
|
3141
|
+
*,
|
|
3142
|
+
dy: float,
|
|
3143
|
+
dx: float,
|
|
3144
|
+
bcs: tuple[str, str],
|
|
3145
|
+
helmholtz: float,
|
|
3146
|
+
raise_on_error: bool,
|
|
3147
|
+
) -> np.ndarray:
|
|
3148
|
+
return _solve_constant_2d(
|
|
3149
|
+
force,
|
|
3150
|
+
dy=dy,
|
|
3151
|
+
dx=dx,
|
|
3152
|
+
bcs=bcs,
|
|
3153
|
+
coefficients=(1.0, 1.0),
|
|
3154
|
+
helmholtz=helmholtz,
|
|
3155
|
+
raise_on_error=raise_on_error,
|
|
3156
|
+
)
|
|
3157
|
+
|
|
3158
|
+
|
|
3159
|
+
def _solve_constant_2d(
|
|
3160
|
+
force: np.ndarray,
|
|
3161
|
+
*,
|
|
3162
|
+
dy: float,
|
|
3163
|
+
dx: float,
|
|
3164
|
+
bcs: tuple[str, str],
|
|
3165
|
+
coefficients: tuple[float, float],
|
|
3166
|
+
helmholtz: float,
|
|
3167
|
+
raise_on_error: bool,
|
|
3168
|
+
) -> np.ndarray:
|
|
3169
|
+
alpha_y, alpha_x = (float(item) for item in coefficients)
|
|
3170
|
+
if not np.isfinite(alpha_y) or not np.isfinite(alpha_x):
|
|
3171
|
+
raise ValueError("2D coefficients must be finite")
|
|
3172
|
+
if alpha_x == 0.0 or alpha_y == 0.0:
|
|
3173
|
+
raise ValueError("2D coefficients must be non-zero")
|
|
3174
|
+
if alpha_y / alpha_x <= 0.0:
|
|
3175
|
+
raise NotImplementedError(
|
|
3176
|
+
"Fishpack genbun path requires elliptic 2D coefficients with the same sign"
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3179
|
+
m, n = force.shape
|
|
3180
|
+
ratio = (alpha_y / alpha_x) * (dx / dy) ** 2
|
|
3181
|
+
a = np.full(m, ratio, dtype=np.float64)
|
|
3182
|
+
b = np.full(m, -2.0 * ratio + float(helmholtz) * dx * dx / alpha_x, dtype=np.float64)
|
|
3183
|
+
c = np.full(m, ratio, dtype=np.float64)
|
|
3184
|
+
|
|
3185
|
+
if bcs[0] == "periodic":
|
|
3186
|
+
mperod = 0
|
|
3187
|
+
else:
|
|
3188
|
+
mperod = 1
|
|
3189
|
+
a[0] = 0.0
|
|
3190
|
+
c[-1] = 0.0
|
|
3191
|
+
|
|
3192
|
+
nperod = 0 if bcs[1] == "periodic" else 1
|
|
3193
|
+
rhs = np.array(force, dtype=np.float64, order="F", copy=True) * (dx * dx / alpha_x)
|
|
3194
|
+
solution, ierror = fishpack.genbun(nperod, n, mperod, m, a, b, c, rhs)
|
|
3195
|
+
if raise_on_error and ierror != 0:
|
|
3196
|
+
raise RuntimeError(f"Fishpack genbun failed with ierror={ierror}")
|
|
3197
|
+
return np.asarray(solution)
|
|
3198
|
+
|
|
3199
|
+
|
|
3200
|
+
def _solve_constant_3d_batched(
|
|
3201
|
+
values: np.ndarray,
|
|
3202
|
+
*,
|
|
3203
|
+
dz: float,
|
|
3204
|
+
dy: float,
|
|
3205
|
+
dx: float,
|
|
3206
|
+
bcs: tuple[str, str, str],
|
|
3207
|
+
coefficients: tuple[float, float, float],
|
|
3208
|
+
raise_on_error: bool,
|
|
3209
|
+
) -> np.ndarray:
|
|
3210
|
+
arr = np.asarray(values, dtype=np.float64)
|
|
3211
|
+
if arr.ndim < 3:
|
|
3212
|
+
raise ValueError("3D solve expects the last three dimensions to be spatial")
|
|
3213
|
+
if min(arr.shape[-3:]) <= 2:
|
|
3214
|
+
raise ValueError("Fishpack pois3d requires all spatial dimensions to be > 2")
|
|
3215
|
+
|
|
3216
|
+
result = np.empty(arr.shape, dtype=np.float64, order="C")
|
|
3217
|
+
if arr.ndim == 3:
|
|
3218
|
+
result[...] = _solve_constant_3d(
|
|
3219
|
+
arr,
|
|
3220
|
+
dz=dz,
|
|
3221
|
+
dy=dy,
|
|
3222
|
+
dx=dx,
|
|
3223
|
+
bcs=bcs,
|
|
3224
|
+
coefficients=coefficients,
|
|
3225
|
+
raise_on_error=raise_on_error,
|
|
3226
|
+
)
|
|
3227
|
+
else:
|
|
3228
|
+
for index in np.ndindex(arr.shape[:-3]):
|
|
3229
|
+
result[index] = _solve_constant_3d(
|
|
3230
|
+
arr[index],
|
|
3231
|
+
dz=dz,
|
|
3232
|
+
dy=dy,
|
|
3233
|
+
dx=dx,
|
|
3234
|
+
bcs=bcs,
|
|
3235
|
+
coefficients=coefficients,
|
|
3236
|
+
raise_on_error=raise_on_error,
|
|
3237
|
+
)
|
|
3238
|
+
return result
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
def _solve_constant_3d(
|
|
3242
|
+
force_zyx: np.ndarray,
|
|
3243
|
+
*,
|
|
3244
|
+
dz: float,
|
|
3245
|
+
dy: float,
|
|
3246
|
+
dx: float,
|
|
3247
|
+
bcs: tuple[str, str, str],
|
|
3248
|
+
coefficients: tuple[float, float, float],
|
|
3249
|
+
raise_on_error: bool,
|
|
3250
|
+
) -> np.ndarray:
|
|
3251
|
+
alpha_z, alpha_y, alpha_x = (float(item) for item in coefficients)
|
|
3252
|
+
nz, ny, nx = force_zyx.shape
|
|
3253
|
+
f_xyz = np.asfortranarray(np.transpose(force_zyx, (2, 1, 0)))
|
|
3254
|
+
|
|
3255
|
+
xperod = 0 if bcs[2] == "periodic" else 1
|
|
3256
|
+
yperod = 0 if bcs[1] == "periodic" else 1
|
|
3257
|
+
zperod = 0 if bcs[0] == "periodic" else 1
|
|
3258
|
+
c1 = alpha_x / (dx * dx)
|
|
3259
|
+
c2 = alpha_y / (dy * dy)
|
|
3260
|
+
zcoef = alpha_z / (dz * dz)
|
|
3261
|
+
a = np.full(nz, zcoef, dtype=np.float64)
|
|
3262
|
+
b = np.full(nz, -2.0 * zcoef, dtype=np.float64)
|
|
3263
|
+
c = np.full(nz, zcoef, dtype=np.float64)
|
|
3264
|
+
if bcs[0] != "periodic":
|
|
3265
|
+
a[0] = 0.0
|
|
3266
|
+
c[-1] = 0.0
|
|
3267
|
+
|
|
3268
|
+
solution_xyz, ierror = fishpack.pois3d(
|
|
3269
|
+
xperod, nx, c1, yperod, ny, c2, zperod, nz, a, b, c, f_xyz
|
|
3270
|
+
)
|
|
3271
|
+
if raise_on_error and ierror != 0:
|
|
3272
|
+
raise RuntimeError(f"Fishpack pois3d failed with ierror={ierror}")
|
|
3273
|
+
return np.asarray(solution_xyz).transpose(2, 1, 0)
|
|
3274
|
+
|
|
3275
|
+
|
|
3276
|
+
def _normalize_bcs(bcs: Sequence[str] | None) -> tuple[str, str]:
|
|
3277
|
+
return _normalize_bcs_n(bcs, 2) # type: ignore[return-value]
|
|
3278
|
+
|
|
3279
|
+
|
|
3280
|
+
def _normalize_bcs_n(bcs: Sequence[str] | None, ndim: int) -> tuple[str, ...]:
|
|
3281
|
+
if bcs is None:
|
|
3282
|
+
bcs = tuple("fixed" for _ in range(ndim))
|
|
3283
|
+
if len(bcs) != ndim:
|
|
3284
|
+
raise ValueError(f"BCs must contain {ndim} entries for the inversion dimensions")
|
|
3285
|
+
normalized = tuple(str(bc).lower() for bc in bcs)
|
|
3286
|
+
unsupported = [bc for bc in normalized if bc not in _SUPPORTED_BCS]
|
|
3287
|
+
if unsupported:
|
|
3288
|
+
raise NotImplementedError(
|
|
3289
|
+
"Only 'fixed' and 'periodic' boundary conditions are supported"
|
|
3290
|
+
)
|
|
3291
|
+
return normalized
|
|
3292
|
+
|
|
3293
|
+
|
|
3294
|
+
def _normalize_sor_bcs(bcs: Sequence[str] | None, ndim: int) -> tuple[str, ...]:
|
|
3295
|
+
if bcs is None:
|
|
3296
|
+
bcs = tuple("fixed" for _ in range(ndim))
|
|
3297
|
+
if len(bcs) != ndim:
|
|
3298
|
+
raise ValueError(f"BCs must contain {ndim} entries for the inversion dimensions")
|
|
3299
|
+
normalized = tuple(str(bc).lower() for bc in bcs)
|
|
3300
|
+
unsupported = [bc for bc in normalized if bc not in _SUPPORTED_SOR_BCS]
|
|
3301
|
+
if unsupported:
|
|
3302
|
+
raise NotImplementedError(
|
|
3303
|
+
"SOR-backed inversions support 'fixed', 'extend', and 'periodic' boundary conditions"
|
|
3304
|
+
)
|
|
3305
|
+
return normalized
|
|
3306
|
+
|
|
3307
|
+
|
|
3308
|
+
def _merged_mparams(params: dict[str, Any] | None) -> dict[str, Any]:
|
|
3309
|
+
merged = dict(_DEFAULT_MPARAMS)
|
|
3310
|
+
if params is not None:
|
|
3311
|
+
merged.update(params)
|
|
3312
|
+
return merged
|
|
3313
|
+
|
|
3314
|
+
|
|
3315
|
+
def _scalar_param(params: dict[str, Any], name: str) -> float:
|
|
3316
|
+
value = params[name]
|
|
3317
|
+
array = np.asarray(value)
|
|
3318
|
+
if array.size != 1:
|
|
3319
|
+
raise NotImplementedError(f"{name} must be a scalar for the current Fishpack-backed subset")
|
|
3320
|
+
scalar = float(array.reshape(()))
|
|
3321
|
+
if not np.isfinite(scalar):
|
|
3322
|
+
raise ValueError(f"{name} must be finite")
|
|
3323
|
+
return scalar
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def _cartesian_coriolis_forcing(
|
|
3327
|
+
field: Any,
|
|
3328
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3329
|
+
spacing: Sequence[float] | None,
|
|
3330
|
+
params: dict[str, Any],
|
|
3331
|
+
*,
|
|
3332
|
+
sign: float,
|
|
3333
|
+
offset: float,
|
|
3334
|
+
) -> Any:
|
|
3335
|
+
coriolis = _cartesian_coriolis_field(field, dims, spacing, params)
|
|
3336
|
+
if _is_dataarray(field):
|
|
3337
|
+
return (field * 0.0) + offset + sign * coriolis
|
|
3338
|
+
return offset + sign * np.asarray(coriolis, dtype=np.float64)
|
|
3339
|
+
|
|
3340
|
+
|
|
3341
|
+
def _cartesian_coriolis_field(
|
|
3342
|
+
field: Any,
|
|
3343
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3344
|
+
spacing: Sequence[float] | None,
|
|
3345
|
+
params: dict[str, Any],
|
|
3346
|
+
) -> Any:
|
|
3347
|
+
f0 = float(params["f0"])
|
|
3348
|
+
beta = float(params["beta"])
|
|
3349
|
+
if beta == 0.0:
|
|
3350
|
+
return _like_field(field, f0)
|
|
3351
|
+
if _is_dataarray(field):
|
|
3352
|
+
if dims is None:
|
|
3353
|
+
if field.ndim < 2:
|
|
3354
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
3355
|
+
dims = field.dims[-2:]
|
|
3356
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
3357
|
+
raise TypeError("xarray beta-plane forcing requires two dimension names")
|
|
3358
|
+
ydim = dims[0]
|
|
3359
|
+
y = field.coords[ydim]
|
|
3360
|
+
return f0 + beta * y
|
|
3361
|
+
|
|
3362
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3363
|
+
if arr.ndim < 2:
|
|
3364
|
+
raise ValueError("beta-plane forcing requires at least a two-dimensional array")
|
|
3365
|
+
if dims is None:
|
|
3366
|
+
yaxis = arr.ndim - 2
|
|
3367
|
+
else:
|
|
3368
|
+
if len(dims) != 2 or not all(isinstance(axis, int) for axis in dims):
|
|
3369
|
+
raise TypeError("NumPy beta-plane forcing requires two integer axes")
|
|
3370
|
+
yaxis = int(dims[0]) % arr.ndim
|
|
3371
|
+
dy, _ = _normalize_spacing(spacing)
|
|
3372
|
+
y = np.arange(arr.shape[yaxis], dtype=np.float64) * dy
|
|
3373
|
+
shape = [1] * arr.ndim
|
|
3374
|
+
shape[yaxis] = arr.shape[yaxis]
|
|
3375
|
+
coriolis = f0 + beta * y.reshape(shape)
|
|
3376
|
+
return np.broadcast_to(coriolis, arr.shape)
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
def _cartesian_geostrophic_coefficients(
|
|
3380
|
+
field: Any,
|
|
3381
|
+
*,
|
|
3382
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3383
|
+
spacing: Sequence[float] | None,
|
|
3384
|
+
f0: float,
|
|
3385
|
+
beta: float,
|
|
3386
|
+
) -> tuple[Any, Any, Any]:
|
|
3387
|
+
if _is_dataarray(field):
|
|
3388
|
+
if dims is None:
|
|
3389
|
+
if field.ndim < 2:
|
|
3390
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
3391
|
+
dims = field.dims[-2:]
|
|
3392
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
3393
|
+
raise TypeError("xarray geostrophic beta-plane inversion requires two dimension names")
|
|
3394
|
+
y = field.coords[dims[0]]
|
|
3395
|
+
f_center = f0 + beta * y
|
|
3396
|
+
f_half = f0 + beta * ((y + y.shift({dims[0]: 1})) / 2.0).fillna(y)
|
|
3397
|
+
return f_half, 0.0, f_center
|
|
3398
|
+
|
|
3399
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3400
|
+
if arr.ndim < 2:
|
|
3401
|
+
raise ValueError("geostrophic beta-plane inversion requires at least a two-dimensional array")
|
|
3402
|
+
if dims is None:
|
|
3403
|
+
yaxis = arr.ndim - 2
|
|
3404
|
+
else:
|
|
3405
|
+
if len(dims) != 2 or not all(isinstance(axis, int) for axis in dims):
|
|
3406
|
+
raise TypeError("NumPy geostrophic beta-plane inversion requires two integer axes")
|
|
3407
|
+
yaxis = int(dims[0]) % arr.ndim
|
|
3408
|
+
dy, _ = _normalize_spacing(spacing)
|
|
3409
|
+
y = np.arange(arr.shape[yaxis], dtype=np.float64) * dy
|
|
3410
|
+
y_half = y.copy()
|
|
3411
|
+
if y_half.size > 1:
|
|
3412
|
+
y_half[1:] = 0.5 * (y[1:] + y[:-1])
|
|
3413
|
+
shape = [1] * arr.ndim
|
|
3414
|
+
shape[yaxis] = arr.shape[yaxis]
|
|
3415
|
+
f_center = f0 + beta * y.reshape(shape)
|
|
3416
|
+
f_half = f0 + beta * y_half.reshape(shape)
|
|
3417
|
+
return (
|
|
3418
|
+
np.broadcast_to(f_half, arr.shape),
|
|
3419
|
+
0.0,
|
|
3420
|
+
np.broadcast_to(f_center, arr.shape),
|
|
3421
|
+
)
|
|
3422
|
+
|
|
3423
|
+
|
|
3424
|
+
def _cartesian_beta_general_coefficients(
|
|
3425
|
+
field: Any,
|
|
3426
|
+
*,
|
|
3427
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3428
|
+
spacing: Sequence[float] | None,
|
|
3429
|
+
epsilon: float,
|
|
3430
|
+
f0: float,
|
|
3431
|
+
beta: float,
|
|
3432
|
+
scale: float,
|
|
3433
|
+
helmholtz: float,
|
|
3434
|
+
) -> tuple[Any, Any, Any, Any, Any, Any]:
|
|
3435
|
+
if _is_dataarray(field):
|
|
3436
|
+
if dims is None:
|
|
3437
|
+
if field.ndim < 2:
|
|
3438
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 2 dimensions")
|
|
3439
|
+
dims = field.dims[-2:]
|
|
3440
|
+
if len(dims) != 2 or not all(isinstance(dim, str) for dim in dims):
|
|
3441
|
+
raise TypeError("xarray beta-plane inversion requires two dimension names")
|
|
3442
|
+
y = field.coords[dims[0]]
|
|
3443
|
+
f = f0 + beta * y
|
|
3444
|
+
denom = epsilon * epsilon + f * f
|
|
3445
|
+
c1 = epsilon / denom
|
|
3446
|
+
c2_dy = beta * (epsilon * epsilon - f * f) / (denom * denom)
|
|
3447
|
+
c1_dy = -2.0 * epsilon * f * beta / (denom * denom)
|
|
3448
|
+
return (
|
|
3449
|
+
scale * c1,
|
|
3450
|
+
0.0,
|
|
3451
|
+
scale * c1,
|
|
3452
|
+
scale * c1_dy,
|
|
3453
|
+
-scale * c2_dy,
|
|
3454
|
+
helmholtz,
|
|
3455
|
+
)
|
|
3456
|
+
|
|
3457
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3458
|
+
if arr.ndim < 2:
|
|
3459
|
+
raise ValueError("beta-plane inversion requires at least a two-dimensional array")
|
|
3460
|
+
if dims is None:
|
|
3461
|
+
yaxis = arr.ndim - 2
|
|
3462
|
+
else:
|
|
3463
|
+
if len(dims) != 2 or not all(isinstance(axis, int) for axis in dims):
|
|
3464
|
+
raise TypeError("NumPy beta-plane inversion requires two integer axes")
|
|
3465
|
+
yaxis = int(dims[0]) % arr.ndim
|
|
3466
|
+
dy, _ = _normalize_spacing(spacing)
|
|
3467
|
+
y = np.arange(arr.shape[yaxis], dtype=np.float64) * dy
|
|
3468
|
+
shape = [1] * arr.ndim
|
|
3469
|
+
shape[yaxis] = arr.shape[yaxis]
|
|
3470
|
+
f = f0 + beta * y.reshape(shape)
|
|
3471
|
+
denom = epsilon * epsilon + f * f
|
|
3472
|
+
c1 = epsilon / denom
|
|
3473
|
+
c2_dy = beta * (epsilon * epsilon - f * f) / (denom * denom)
|
|
3474
|
+
c1_dy = -2.0 * epsilon * f * beta / (denom * denom)
|
|
3475
|
+
return (
|
|
3476
|
+
np.broadcast_to(scale * c1, arr.shape),
|
|
3477
|
+
0.0,
|
|
3478
|
+
np.broadcast_to(scale * c1, arr.shape),
|
|
3479
|
+
np.broadcast_to(scale * c1_dy, arr.shape),
|
|
3480
|
+
np.broadcast_to(-scale * c2_dy, arr.shape),
|
|
3481
|
+
helmholtz,
|
|
3482
|
+
)
|
|
3483
|
+
|
|
3484
|
+
|
|
3485
|
+
def _cartesian_omega_coefficients(
|
|
3486
|
+
field: Any,
|
|
3487
|
+
*,
|
|
3488
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3489
|
+
spacing: Sequence[float] | None,
|
|
3490
|
+
f0: float,
|
|
3491
|
+
beta: float,
|
|
3492
|
+
n2: float,
|
|
3493
|
+
) -> tuple[Any, Any, Any]:
|
|
3494
|
+
if _is_dataarray(field):
|
|
3495
|
+
if dims is None:
|
|
3496
|
+
if field.ndim < 3:
|
|
3497
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 3 dimensions")
|
|
3498
|
+
dims = field.dims[-3:]
|
|
3499
|
+
if len(dims) != 3 or not all(isinstance(dim, str) for dim in dims):
|
|
3500
|
+
raise TypeError("xarray omega beta-plane inversion requires three dimension names")
|
|
3501
|
+
y = field.coords[dims[1]]
|
|
3502
|
+
f = f0 + beta * y
|
|
3503
|
+
return f * f, n2, n2
|
|
3504
|
+
|
|
3505
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3506
|
+
if arr.ndim < 3:
|
|
3507
|
+
raise ValueError("omega beta-plane inversion requires at least a three-dimensional array")
|
|
3508
|
+
if dims is None:
|
|
3509
|
+
yaxis = arr.ndim - 2
|
|
3510
|
+
else:
|
|
3511
|
+
if len(dims) != 3 or not all(isinstance(axis, int) for axis in dims):
|
|
3512
|
+
raise TypeError("NumPy omega beta-plane inversion requires three integer axes")
|
|
3513
|
+
yaxis = int(dims[1]) % arr.ndim
|
|
3514
|
+
_, dy, _ = _normalize_spacing3d(spacing)
|
|
3515
|
+
y = np.arange(arr.shape[yaxis], dtype=np.float64) * dy
|
|
3516
|
+
shape = [1] * arr.ndim
|
|
3517
|
+
shape[yaxis] = arr.shape[yaxis]
|
|
3518
|
+
acoef = (f0 + beta * y.reshape(shape)) ** 2
|
|
3519
|
+
return (
|
|
3520
|
+
np.broadcast_to(acoef, arr.shape),
|
|
3521
|
+
n2,
|
|
3522
|
+
n2,
|
|
3523
|
+
)
|
|
3524
|
+
|
|
3525
|
+
|
|
3526
|
+
def _cartesian_3d_ocean_coefficients(
|
|
3527
|
+
field: Any,
|
|
3528
|
+
*,
|
|
3529
|
+
dims: Sequence[str] | Sequence[int] | None,
|
|
3530
|
+
spacing: Sequence[float] | None,
|
|
3531
|
+
epsilon: float,
|
|
3532
|
+
f0: float,
|
|
3533
|
+
beta: float,
|
|
3534
|
+
n2: float,
|
|
3535
|
+
buoyancy_damping: float,
|
|
3536
|
+
) -> tuple[Any, Any, Any, Any, Any, Any, Any]:
|
|
3537
|
+
vertical = buoyancy_damping / n2
|
|
3538
|
+
if _is_dataarray(field):
|
|
3539
|
+
if dims is None:
|
|
3540
|
+
if field.ndim < 3:
|
|
3541
|
+
raise ValueError("dims must be supplied for xarray inputs with fewer than 3 dimensions")
|
|
3542
|
+
dims = field.dims[-3:]
|
|
3543
|
+
if len(dims) != 3 or not all(isinstance(dim, str) for dim in dims):
|
|
3544
|
+
raise TypeError("xarray 3DOcean beta-plane inversion requires three dimension names")
|
|
3545
|
+
y = field.coords[dims[1]]
|
|
3546
|
+
f = f0 + beta * y
|
|
3547
|
+
denom = epsilon * epsilon + f * f
|
|
3548
|
+
c1 = epsilon / denom
|
|
3549
|
+
c1_dy = -2.0 * epsilon * f * beta / (denom * denom)
|
|
3550
|
+
c2_dy = beta * (epsilon * epsilon - f * f) / (denom * denom)
|
|
3551
|
+
return vertical, c1, c1, 0.0, c1_dy, -c2_dy, 0.0
|
|
3552
|
+
|
|
3553
|
+
arr = np.asarray(field, dtype=np.float64)
|
|
3554
|
+
if arr.ndim < 3:
|
|
3555
|
+
raise ValueError("3DOcean beta-plane inversion requires at least a three-dimensional array")
|
|
3556
|
+
if dims is None:
|
|
3557
|
+
yaxis = arr.ndim - 2
|
|
3558
|
+
else:
|
|
3559
|
+
if len(dims) != 3 or not all(isinstance(axis, int) for axis in dims):
|
|
3560
|
+
raise TypeError("NumPy 3DOcean beta-plane inversion requires three integer axes")
|
|
3561
|
+
yaxis = int(dims[1]) % arr.ndim
|
|
3562
|
+
_, dy, _ = _normalize_spacing3d(spacing)
|
|
3563
|
+
y = np.arange(arr.shape[yaxis], dtype=np.float64) * dy
|
|
3564
|
+
shape = [1] * arr.ndim
|
|
3565
|
+
shape[yaxis] = arr.shape[yaxis]
|
|
3566
|
+
f = f0 + beta * y.reshape(shape)
|
|
3567
|
+
denom = epsilon * epsilon + f * f
|
|
3568
|
+
c1 = epsilon / denom
|
|
3569
|
+
c1_dy = -2.0 * epsilon * f * beta / (denom * denom)
|
|
3570
|
+
c2_dy = beta * (epsilon * epsilon - f * f) / (denom * denom)
|
|
3571
|
+
return (
|
|
3572
|
+
vertical,
|
|
3573
|
+
np.broadcast_to(c1, arr.shape),
|
|
3574
|
+
np.broadcast_to(c1, arr.shape),
|
|
3575
|
+
0.0,
|
|
3576
|
+
np.broadcast_to(c1_dy, arr.shape),
|
|
3577
|
+
np.broadcast_to(-c2_dy, arr.shape),
|
|
3578
|
+
0.0,
|
|
3579
|
+
)
|
|
3580
|
+
|
|
3581
|
+
|
|
3582
|
+
def _like_field(field: Any, value: float) -> Any:
|
|
3583
|
+
if _is_dataarray(field):
|
|
3584
|
+
return (field * 0.0) + value
|
|
3585
|
+
return np.zeros_like(np.asarray(field, dtype=np.float64)) + value
|
|
3586
|
+
|
|
3587
|
+
|
|
3588
|
+
def _normalize_spacing(spacing: Sequence[float] | None) -> tuple[float, float]:
|
|
3589
|
+
if spacing is None:
|
|
3590
|
+
return 1.0, 1.0
|
|
3591
|
+
if len(spacing) != 2:
|
|
3592
|
+
raise ValueError("spacing must contain dy and dx")
|
|
3593
|
+
dy, dx = (float(spacing[0]), float(spacing[1]))
|
|
3594
|
+
if dy <= 0.0 or dx <= 0.0:
|
|
3595
|
+
raise ValueError("spacing values must be positive")
|
|
3596
|
+
return dy, dx
|
|
3597
|
+
|
|
3598
|
+
|
|
3599
|
+
def _normalize_spacing3d(spacing: Sequence[float] | None) -> tuple[float, float, float]:
|
|
3600
|
+
if spacing is None:
|
|
3601
|
+
return 1.0, 1.0, 1.0
|
|
3602
|
+
if len(spacing) != 3:
|
|
3603
|
+
raise ValueError("spacing must contain dz, dy, and dx")
|
|
3604
|
+
dz, dy, dx = (float(spacing[0]), float(spacing[1]), float(spacing[2]))
|
|
3605
|
+
if dz <= 0.0 or dy <= 0.0 or dx <= 0.0:
|
|
3606
|
+
raise ValueError("spacing values must be positive")
|
|
3607
|
+
return dz, dy, dx
|
|
3608
|
+
|
|
3609
|
+
|
|
3610
|
+
def _spacing_for_labeled(
|
|
3611
|
+
field: Any, dims: tuple[str, str], spacing: Sequence[float] | None
|
|
3612
|
+
) -> tuple[float, float]:
|
|
3613
|
+
if spacing is not None:
|
|
3614
|
+
return _normalize_spacing(spacing)
|
|
3615
|
+
return tuple(_uniform_coord_spacing(field.coords[dim].values, dim) for dim in dims) # type: ignore[return-value]
|
|
3616
|
+
|
|
3617
|
+
|
|
3618
|
+
def _spacing_for_labeled3d(
|
|
3619
|
+
field: Any, dims: tuple[str, str, str], spacing: Sequence[float] | None
|
|
3620
|
+
) -> tuple[float, float, float]:
|
|
3621
|
+
if spacing is not None:
|
|
3622
|
+
return _normalize_spacing3d(spacing)
|
|
3623
|
+
return tuple(_uniform_coord_spacing(field.coords[dim].values, dim) for dim in dims) # type: ignore[return-value]
|
|
3624
|
+
|
|
3625
|
+
|
|
3626
|
+
def _uniform_coord_spacing(coord: Any, dim: str) -> float:
|
|
3627
|
+
values = np.asarray(coord, dtype=np.float64)
|
|
3628
|
+
if values.ndim != 1 or values.size < 2:
|
|
3629
|
+
raise ValueError(f"coordinate {dim!r} must be one-dimensional with at least two points")
|
|
3630
|
+
deltas = np.diff(values)
|
|
3631
|
+
delta = float(deltas[0])
|
|
3632
|
+
if not np.allclose(deltas, delta):
|
|
3633
|
+
raise ValueError(f"coordinate {dim!r} must be uniformly spaced")
|
|
3634
|
+
if delta == 0.0:
|
|
3635
|
+
raise ValueError(f"coordinate {dim!r} has zero spacing")
|
|
3636
|
+
return abs(delta)
|
|
3637
|
+
|
|
3638
|
+
|
|
3639
|
+
def _is_dataarray(obj: Any) -> bool:
|
|
3640
|
+
return obj.__class__.__module__.startswith("xarray.") and hasattr(obj, "dims")
|