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.
Files changed (81) hide show
  1. PyFishPack/__init__.py +86 -0
  2. PyFishPack/__pycache__/__init__.cpython-310.pyc +0 -0
  3. PyFishPack/__pycache__/apps.cpython-310.pyc +0 -0
  4. PyFishPack/_dummy.c +23 -0
  5. PyFishPack/_dummy.cp310-win_amd64.pyd +0 -0
  6. PyFishPack/apps.py +3640 -0
  7. PyFishPack/fishpack.cp310-win_amd64.dll.a +0 -0
  8. PyFishPack/fishpack.cp310-win_amd64.pyd +0 -0
  9. PyFishPack/meson.build +213 -0
  10. PyFishPack/src/archive/f77/Makefile +19 -0
  11. PyFishPack/src/archive/f77/blktri.f +1404 -0
  12. PyFishPack/src/archive/f77/cblktri.f +1414 -0
  13. PyFishPack/src/archive/f77/cmgnbn.f +1592 -0
  14. PyFishPack/src/archive/f77/comf.f +186 -0
  15. PyFishPack/src/archive/f77/fftpack.f +2968 -0
  16. PyFishPack/src/archive/f77/genbun.f +1335 -0
  17. PyFishPack/src/archive/f77/gnbnaux.f +314 -0
  18. PyFishPack/src/archive/f77/hstcrt.f +443 -0
  19. PyFishPack/src/archive/f77/hstcsp.f +683 -0
  20. PyFishPack/src/archive/f77/hstcyl.f +485 -0
  21. PyFishPack/src/archive/f77/hstplr.f +538 -0
  22. PyFishPack/src/archive/f77/hstssp.f +634 -0
  23. PyFishPack/src/archive/f77/hw3crt.f +687 -0
  24. PyFishPack/src/archive/f77/hwscrt.f +512 -0
  25. PyFishPack/src/archive/f77/hwscsp.f +728 -0
  26. PyFishPack/src/archive/f77/hwscyl.f +538 -0
  27. PyFishPack/src/archive/f77/hwsplr.f +602 -0
  28. PyFishPack/src/archive/f77/hwsssp.f +780 -0
  29. PyFishPack/src/archive/f77/pois3d.f +550 -0
  30. PyFishPack/src/archive/f77/poistg.f +875 -0
  31. PyFishPack/src/archive/f77/sepaux.f +361 -0
  32. PyFishPack/src/archive/f77/sepeli.f +1029 -0
  33. PyFishPack/src/archive/f77/sepx4.f +958 -0
  34. PyFishPack/src/centered_axisymmetric_spherical_solver.f90 +1002 -0
  35. PyFishPack/src/centered_cartesian_helmholtz_solver_3d.f90 +819 -0
  36. PyFishPack/src/centered_cartesian_solver.f90 +583 -0
  37. PyFishPack/src/centered_cylindrical_solver.f90 +634 -0
  38. PyFishPack/src/centered_helmholtz_solvers.f90 +156 -0
  39. PyFishPack/src/centered_polar_solver.f90 +746 -0
  40. PyFishPack/src/centered_real_linear_systems_solver.f90 +280 -0
  41. PyFishPack/src/centered_spherical_solver.f90 +928 -0
  42. PyFishPack/src/complex_block_tridiagonal_linear_systems_solver.f90 +1947 -0
  43. PyFishPack/src/complex_linear_systems_solver.f90 +1787 -0
  44. PyFishPack/src/fftpack_c_api.f90 +86 -0
  45. PyFishPack/src/fishpack.f90 +191 -0
  46. PyFishPack/src/fishpack.pyf +504 -0
  47. PyFishPack/src/fishpack_c_api.f90 +365 -0
  48. PyFishPack/src/fishpack_original.pyf +2119 -0
  49. PyFishPack/src/fishpack_precision.f90 +53 -0
  50. PyFishPack/src/general_linear_systems_solver_3d.f90 +296 -0
  51. PyFishPack/src/iterative_solvers.f90 +969 -0
  52. PyFishPack/src/main.f90 +10 -0
  53. PyFishPack/src/pyfishpack_module.c +1302 -0
  54. PyFishPack/src/real_block_tridiagonal_linear_systems_solver.f90 +319 -0
  55. PyFishPack/src/sepeli.f90 +1454 -0
  56. PyFishPack/src/sepx4.f90 +1338 -0
  57. PyFishPack/src/staggered_axisymmetric_spherical_solver.f90 +908 -0
  58. PyFishPack/src/staggered_cartesian_solver.f90 +553 -0
  59. PyFishPack/src/staggered_cylindrical_solver.f90 +630 -0
  60. PyFishPack/src/staggered_helmholtz_solvers.f90 +172 -0
  61. PyFishPack/src/staggered_polar_solver.f90 +651 -0
  62. PyFishPack/src/staggered_real_linear_systems_solver.f90 +258 -0
  63. PyFishPack/src/staggered_spherical_solver.f90 +758 -0
  64. PyFishPack/src/three_dimensional_solvers.f90 +602 -0
  65. PyFishPack/src/type_CenteredCyclicReductionUtility.f90 +1714 -0
  66. PyFishPack/src/type_CyclicReductionUtility.f90 +472 -0
  67. PyFishPack/src/type_FishpackWorkspace.f90 +290 -0
  68. PyFishPack/src/type_GeneralizedCyclicReductionUtility.f90 +1980 -0
  69. PyFishPack/src/type_PeriodicFastFourierTransform.f90 +3789 -0
  70. PyFishPack/src/type_SepAux.f90 +586 -0
  71. PyFishPack/src/type_StaggeredCyclicReductionUtility.f90 +893 -0
  72. pyfishpack-0.1.0.dist-info/DELVEWHEEL +2 -0
  73. pyfishpack-0.1.0.dist-info/METADATA +81 -0
  74. pyfishpack-0.1.0.dist-info/RECORD +81 -0
  75. pyfishpack-0.1.0.dist-info/WHEEL +5 -0
  76. pyfishpack-0.1.0.dist-info/licenses/LICENSE +21 -0
  77. pyfishpack-0.1.0.dist-info/top_level.txt +1 -0
  78. pyfishpack.libs/libgcc_s_seh-1-25d59ccffa1a9009644065b069829e07.dll +0 -0
  79. pyfishpack.libs/libgfortran-5-08f2195cfa0d823e13371c5c3186a82a.dll +0 -0
  80. pyfishpack.libs/libquadmath-0-c5abb9113f1ee64b87a889958e4b7418.dll +0 -0
  81. 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")