spharmgrid 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spharmgrid/__init__.py +63 -0
- spharmgrid/_ducc.py +202 -0
- spharmgrid/_vector.py +248 -0
- spharmgrid/_xarray.py +232 -0
- spharmgrid/accessors.py +811 -0
- spharmgrid/cli.py +348 -0
- spharmgrid/grids.py +453 -0
- spharmgrid/kinematics.py +843 -0
- spharmgrid/metadata.py +248 -0
- spharmgrid/operators.py +286 -0
- spharmgrid/regrid.py +156 -0
- spharmgrid/spectral.py +242 -0
- spharmgrid-0.1.0.dist-info/METADATA +110 -0
- spharmgrid-0.1.0.dist-info/RECORD +17 -0
- spharmgrid-0.1.0.dist-info/WHEEL +4 -0
- spharmgrid-0.1.0.dist-info/entry_points.txt +2 -0
- spharmgrid-0.1.0.dist-info/licenses/LICENSE +28 -0
spharmgrid/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""xarray-first spherical-harmonic atmospheric operations using DUCC0.
|
|
2
|
+
|
|
3
|
+
Importing :mod:`spharmgrid` registers the ``.sg`` accessors on xarray
|
|
4
|
+
``DataArray`` and ``Dataset`` objects. DUCC0 supplies all numerical
|
|
5
|
+
spherical-harmonic transforms; spharmgrid supplies the GL/CC, xarray, and CF
|
|
6
|
+
operations layer around them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Import solely for xarray accessor registration after direct functions exist.
|
|
10
|
+
from . import accessors as _accessors # noqa: F401
|
|
11
|
+
from .grids import Grid, clenshaw_curtis_grid, detect_grid, gaussian_grid
|
|
12
|
+
from .kinematics import (
|
|
13
|
+
divergence,
|
|
14
|
+
divergent_wind,
|
|
15
|
+
helmholtz,
|
|
16
|
+
inverse_vector_laplacian,
|
|
17
|
+
kinematics,
|
|
18
|
+
potentials,
|
|
19
|
+
rotational_wind,
|
|
20
|
+
streamfunction,
|
|
21
|
+
vector_laplacian,
|
|
22
|
+
velocity_potential,
|
|
23
|
+
vorticity,
|
|
24
|
+
wind,
|
|
25
|
+
)
|
|
26
|
+
from .operators import (
|
|
27
|
+
EARTH_RADIUS_M,
|
|
28
|
+
gradient,
|
|
29
|
+
inverse_gradient,
|
|
30
|
+
inverse_laplacian,
|
|
31
|
+
laplacian,
|
|
32
|
+
)
|
|
33
|
+
from .regrid import regrid, regrid_vector
|
|
34
|
+
from .spectral import SpectralRange, filter, parse_spectral
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"EARTH_RADIUS_M",
|
|
38
|
+
"Grid",
|
|
39
|
+
"SpectralRange",
|
|
40
|
+
"clenshaw_curtis_grid",
|
|
41
|
+
"detect_grid",
|
|
42
|
+
"divergent_wind",
|
|
43
|
+
"divergence",
|
|
44
|
+
"filter",
|
|
45
|
+
"gaussian_grid",
|
|
46
|
+
"gradient",
|
|
47
|
+
"helmholtz",
|
|
48
|
+
"inverse_gradient",
|
|
49
|
+
"inverse_laplacian",
|
|
50
|
+
"inverse_vector_laplacian",
|
|
51
|
+
"kinematics",
|
|
52
|
+
"laplacian",
|
|
53
|
+
"parse_spectral",
|
|
54
|
+
"potentials",
|
|
55
|
+
"regrid",
|
|
56
|
+
"regrid_vector",
|
|
57
|
+
"rotational_wind",
|
|
58
|
+
"streamfunction",
|
|
59
|
+
"velocity_potential",
|
|
60
|
+
"vector_laplacian",
|
|
61
|
+
"vorticity",
|
|
62
|
+
"wind",
|
|
63
|
+
]
|
spharmgrid/_ducc.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Small internal adapters around DUCC0's spherical-harmonic functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import cache
|
|
7
|
+
from typing import Literal, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
12
|
+
from .grids import Grid
|
|
13
|
+
|
|
14
|
+
Geometry = Literal["CC", "GL"]
|
|
15
|
+
|
|
16
|
+
DUCC_THREADS = 4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class TransformSpec:
|
|
21
|
+
"""A scalar/vector transform bandwidth supported by its source geometry."""
|
|
22
|
+
|
|
23
|
+
lmax: int
|
|
24
|
+
mmax: int
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
if self.lmax < 0 or self.mmax < 0 or self.mmax > self.lmax:
|
|
28
|
+
raise ValueError("transform limits must satisfy 0 <= mmax <= lmax")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def geometry_for(grid: Grid) -> Geometry:
|
|
32
|
+
"""Map the public lowercase grid label to DUCC's geometry label."""
|
|
33
|
+
return "GL" if grid.kind == "gl" else "CC"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cache
|
|
37
|
+
def alm_degrees(lmax: int, mmax: int) -> NDArray[np.int64]:
|
|
38
|
+
"""Return total degree for each contiguous healpy-ordered coefficient."""
|
|
39
|
+
degrees = np.concatenate(
|
|
40
|
+
[np.arange(m, lmax + 1, dtype=np.int64) for m in range(mmax + 1)]
|
|
41
|
+
)
|
|
42
|
+
degrees.setflags(write=False)
|
|
43
|
+
return degrees
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def scalar_analysis(
|
|
47
|
+
frame: NDArray[np.generic],
|
|
48
|
+
*,
|
|
49
|
+
spec: TransformSpec,
|
|
50
|
+
geometry: Geometry,
|
|
51
|
+
phi0: float,
|
|
52
|
+
) -> NDArray[np.complex128]:
|
|
53
|
+
"""Analyze one north-to-south, cyclic-eastward scalar map."""
|
|
54
|
+
_require_frame(frame)
|
|
55
|
+
import ducc0
|
|
56
|
+
|
|
57
|
+
values = np.ascontiguousarray(frame, dtype=np.float64)
|
|
58
|
+
result = ducc0.sht.analysis_2d(
|
|
59
|
+
map=values[np.newaxis, :, :],
|
|
60
|
+
spin=0,
|
|
61
|
+
lmax=spec.lmax,
|
|
62
|
+
mmax=spec.mmax,
|
|
63
|
+
geometry=geometry,
|
|
64
|
+
phi0=phi0,
|
|
65
|
+
nthreads=DUCC_THREADS,
|
|
66
|
+
)
|
|
67
|
+
return cast(NDArray[np.complex128], result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def scalar_synthesis(
|
|
71
|
+
alm: NDArray[np.complexfloating],
|
|
72
|
+
*,
|
|
73
|
+
spec: TransformSpec,
|
|
74
|
+
geometry: Geometry,
|
|
75
|
+
ntheta: int,
|
|
76
|
+
nphi: int,
|
|
77
|
+
phi0: float,
|
|
78
|
+
) -> NDArray[np.float64]:
|
|
79
|
+
"""Synthesize one scalar map on a north-to-south DUCC geometry."""
|
|
80
|
+
import ducc0
|
|
81
|
+
|
|
82
|
+
values = np.ascontiguousarray(alm)
|
|
83
|
+
result = ducc0.sht.synthesis_2d(
|
|
84
|
+
alm=values,
|
|
85
|
+
spin=0,
|
|
86
|
+
lmax=spec.lmax,
|
|
87
|
+
mmax=spec.mmax,
|
|
88
|
+
geometry=geometry,
|
|
89
|
+
ntheta=ntheta,
|
|
90
|
+
nphi=nphi,
|
|
91
|
+
phi0=phi0,
|
|
92
|
+
nthreads=DUCC_THREADS,
|
|
93
|
+
)
|
|
94
|
+
return cast(NDArray[np.float64], result[0])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def scalar_derivative_synthesis(
|
|
98
|
+
alm: NDArray[np.complexfloating],
|
|
99
|
+
*,
|
|
100
|
+
spec: TransformSpec,
|
|
101
|
+
geometry: Geometry,
|
|
102
|
+
ntheta: int,
|
|
103
|
+
nphi: int,
|
|
104
|
+
phi0: float,
|
|
105
|
+
) -> NDArray[np.float64]:
|
|
106
|
+
"""Synthesize theta and eastward angular derivatives of a scalar field.
|
|
107
|
+
|
|
108
|
+
DUCC's ``DERIV1`` mode returns ``(d/dtheta, (1/sin(theta)) d/dphi)``.
|
|
109
|
+
The caller maps the first component from southward theta to northward
|
|
110
|
+
latitude and applies the physical-radius factor.
|
|
111
|
+
"""
|
|
112
|
+
import ducc0
|
|
113
|
+
|
|
114
|
+
result = ducc0.sht.synthesis_2d(
|
|
115
|
+
alm=np.ascontiguousarray(alm),
|
|
116
|
+
spin=1,
|
|
117
|
+
lmax=spec.lmax,
|
|
118
|
+
mmax=spec.mmax,
|
|
119
|
+
geometry=geometry,
|
|
120
|
+
ntheta=ntheta,
|
|
121
|
+
nphi=nphi,
|
|
122
|
+
phi0=phi0,
|
|
123
|
+
nthreads=DUCC_THREADS,
|
|
124
|
+
mode="DERIV1",
|
|
125
|
+
)
|
|
126
|
+
return cast(NDArray[np.float64], result)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def vector_analysis(
|
|
130
|
+
u: NDArray[np.generic],
|
|
131
|
+
v: NDArray[np.generic],
|
|
132
|
+
*,
|
|
133
|
+
spec: TransformSpec,
|
|
134
|
+
geometry: Geometry,
|
|
135
|
+
phi0: float,
|
|
136
|
+
) -> NDArray[np.complex128]:
|
|
137
|
+
"""Analyze geographic eastward/northward wind into DUCC E/B coefficients.
|
|
138
|
+
|
|
139
|
+
DUCC's spin-1 map components are ``(v_theta, v_phi)``. Geographic wind
|
|
140
|
+
uses northward ``v``, while increasing theta points south, so spharmgrid
|
|
141
|
+
maps the components as ``(-v, u)``.
|
|
142
|
+
"""
|
|
143
|
+
_require_frame(u)
|
|
144
|
+
_require_frame(v)
|
|
145
|
+
if u.shape != v.shape:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"wind frames have incompatible shapes: {u.shape} and {v.shape}"
|
|
148
|
+
)
|
|
149
|
+
import ducc0
|
|
150
|
+
|
|
151
|
+
eastward = np.asarray(u, dtype=np.float64)
|
|
152
|
+
northward = np.asarray(v, dtype=np.float64)
|
|
153
|
+
vector_map = np.stack((-northward, eastward), axis=0)
|
|
154
|
+
result = ducc0.sht.analysis_2d(
|
|
155
|
+
map=np.ascontiguousarray(vector_map),
|
|
156
|
+
spin=1,
|
|
157
|
+
lmax=spec.lmax,
|
|
158
|
+
mmax=spec.mmax,
|
|
159
|
+
geometry=geometry,
|
|
160
|
+
phi0=phi0,
|
|
161
|
+
nthreads=DUCC_THREADS,
|
|
162
|
+
)
|
|
163
|
+
return cast(NDArray[np.complex128], result)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def vector_synthesis(
|
|
167
|
+
alm_e: NDArray[np.complexfloating],
|
|
168
|
+
alm_b: NDArray[np.complexfloating],
|
|
169
|
+
*,
|
|
170
|
+
spec: TransformSpec,
|
|
171
|
+
geometry: Geometry,
|
|
172
|
+
ntheta: int,
|
|
173
|
+
nphi: int,
|
|
174
|
+
phi0: float,
|
|
175
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
176
|
+
"""Synthesize DUCC E/B coefficients into geographic eastward/northward wind."""
|
|
177
|
+
import ducc0
|
|
178
|
+
|
|
179
|
+
coefficients = np.stack((alm_e, alm_b), axis=0)
|
|
180
|
+
result = ducc0.sht.synthesis_2d(
|
|
181
|
+
alm=np.ascontiguousarray(coefficients),
|
|
182
|
+
spin=1,
|
|
183
|
+
lmax=spec.lmax,
|
|
184
|
+
mmax=spec.mmax,
|
|
185
|
+
geometry=geometry,
|
|
186
|
+
ntheta=ntheta,
|
|
187
|
+
nphi=nphi,
|
|
188
|
+
phi0=phi0,
|
|
189
|
+
nthreads=DUCC_THREADS,
|
|
190
|
+
)
|
|
191
|
+
# This is the inverse of ``(-v, u)`` in ``vector_analysis``.
|
|
192
|
+
return (
|
|
193
|
+
cast(NDArray[np.float64], result[1]),
|
|
194
|
+
cast(NDArray[np.float64], -result[0]),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _require_frame(frame: NDArray[np.generic]) -> None:
|
|
199
|
+
if frame.ndim != 2:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"DUCC kernel requires a two-dimensional frame, got {frame.shape}"
|
|
202
|
+
)
|
spharmgrid/_vector.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Private xarray adapters for paired tangent-vector SHT operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import xarray as xr
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
12
|
+
from ._xarray import (
|
|
13
|
+
FieldLayout,
|
|
14
|
+
apply_ufunc_options,
|
|
15
|
+
exact_align,
|
|
16
|
+
field_layout,
|
|
17
|
+
require_dataarray,
|
|
18
|
+
restore_output,
|
|
19
|
+
)
|
|
20
|
+
from .grids import grids_equivalent
|
|
21
|
+
|
|
22
|
+
VectorScalarKernel = Callable[
|
|
23
|
+
[NDArray[np.generic], NDArray[np.generic]], NDArray[np.float64]
|
|
24
|
+
]
|
|
25
|
+
VectorPairKernel = Callable[
|
|
26
|
+
[NDArray[np.generic], NDArray[np.generic]],
|
|
27
|
+
tuple[NDArray[np.float64], NDArray[np.float64]],
|
|
28
|
+
]
|
|
29
|
+
VectorQuadKernel = Callable[
|
|
30
|
+
[NDArray[np.generic], NDArray[np.generic]],
|
|
31
|
+
tuple[
|
|
32
|
+
NDArray[np.float64],
|
|
33
|
+
NDArray[np.float64],
|
|
34
|
+
NDArray[np.float64],
|
|
35
|
+
NDArray[np.float64],
|
|
36
|
+
],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def vector_inputs(
|
|
41
|
+
u: xr.DataArray, v: xr.DataArray
|
|
42
|
+
) -> tuple[FieldLayout, xr.DataArray, xr.DataArray]:
|
|
43
|
+
"""Validate, canonicalize, and exactly align a geographic vector pair."""
|
|
44
|
+
u = require_dataarray(u, "u")
|
|
45
|
+
v = require_dataarray(v, "v")
|
|
46
|
+
layout = field_layout(u)
|
|
47
|
+
v_layout = field_layout(v)
|
|
48
|
+
_require_matching_layouts(layout, v_layout, "u", "v")
|
|
49
|
+
canonical_u = layout.canonicalize(u)
|
|
50
|
+
canonical_v = v_layout.canonicalize(v)
|
|
51
|
+
return layout, *exact_align(canonical_u, canonical_v, names=("u", "v"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def vector_scalar_transform(
|
|
55
|
+
original: xr.DataArray,
|
|
56
|
+
source: FieldLayout,
|
|
57
|
+
canonical_u: xr.DataArray,
|
|
58
|
+
canonical_v: xr.DataArray,
|
|
59
|
+
transform: VectorScalarKernel,
|
|
60
|
+
) -> xr.DataArray:
|
|
61
|
+
"""Apply one paired-vector kernel that returns a scalar map."""
|
|
62
|
+
dask_field = _dask_input(canonical_u, canonical_v)
|
|
63
|
+
|
|
64
|
+
def kernel(
|
|
65
|
+
frame_u: NDArray[np.generic], frame_v: NDArray[np.generic]
|
|
66
|
+
) -> NDArray[np.float64]:
|
|
67
|
+
return transform(frame_u, frame_v)
|
|
68
|
+
|
|
69
|
+
output = xr.apply_ufunc(
|
|
70
|
+
kernel,
|
|
71
|
+
canonical_u,
|
|
72
|
+
canonical_v,
|
|
73
|
+
input_core_dims=[
|
|
74
|
+
[source.latitude_dim, source.longitude_dim],
|
|
75
|
+
[source.latitude_dim, source.longitude_dim],
|
|
76
|
+
],
|
|
77
|
+
output_core_dims=[[source.latitude_dim, source.longitude_dim]],
|
|
78
|
+
vectorize=True,
|
|
79
|
+
output_dtypes=[np.float64],
|
|
80
|
+
**apply_ufunc_options(dask_field),
|
|
81
|
+
)
|
|
82
|
+
return restore_output(
|
|
83
|
+
cast(xr.DataArray, output),
|
|
84
|
+
source=source,
|
|
85
|
+
target=source,
|
|
86
|
+
original_dims=original.dims,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def vector_pair_transform(
|
|
91
|
+
original: xr.DataArray,
|
|
92
|
+
source: FieldLayout,
|
|
93
|
+
target: FieldLayout,
|
|
94
|
+
canonical_u: xr.DataArray,
|
|
95
|
+
canonical_v: xr.DataArray,
|
|
96
|
+
transform: VectorPairKernel,
|
|
97
|
+
) -> tuple[xr.DataArray, xr.DataArray]:
|
|
98
|
+
"""Apply one paired-vector kernel that returns two target-grid maps."""
|
|
99
|
+
dask_field = _dask_input(canonical_u, canonical_v)
|
|
100
|
+
output_changes_shape = _output_changes_shape(source, target)
|
|
101
|
+
output_sizes = (
|
|
102
|
+
{target.latitude_dim: target.grid.nlat, target.longitude_dim: target.grid.nlon}
|
|
103
|
+
if output_changes_shape
|
|
104
|
+
else None
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def kernel(
|
|
108
|
+
frame_u: NDArray[np.generic], frame_v: NDArray[np.generic]
|
|
109
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
110
|
+
return transform(frame_u, frame_v)
|
|
111
|
+
|
|
112
|
+
outputs = xr.apply_ufunc(
|
|
113
|
+
kernel,
|
|
114
|
+
canonical_u,
|
|
115
|
+
canonical_v,
|
|
116
|
+
input_core_dims=[
|
|
117
|
+
[source.latitude_dim, source.longitude_dim],
|
|
118
|
+
[source.latitude_dim, source.longitude_dim],
|
|
119
|
+
],
|
|
120
|
+
output_core_dims=[
|
|
121
|
+
[target.latitude_dim, target.longitude_dim],
|
|
122
|
+
[target.latitude_dim, target.longitude_dim],
|
|
123
|
+
],
|
|
124
|
+
vectorize=True,
|
|
125
|
+
output_dtypes=[np.float64, np.float64],
|
|
126
|
+
exclude_dims=(
|
|
127
|
+
{source.latitude_dim, source.longitude_dim}
|
|
128
|
+
if output_changes_shape
|
|
129
|
+
else set()
|
|
130
|
+
),
|
|
131
|
+
**apply_ufunc_options(dask_field, output_sizes=output_sizes),
|
|
132
|
+
)
|
|
133
|
+
first, second = cast(tuple[xr.DataArray, xr.DataArray], outputs)
|
|
134
|
+
return (
|
|
135
|
+
restore_output(
|
|
136
|
+
first,
|
|
137
|
+
source=source,
|
|
138
|
+
target=target,
|
|
139
|
+
original_dims=original.dims,
|
|
140
|
+
),
|
|
141
|
+
restore_output(
|
|
142
|
+
second,
|
|
143
|
+
source=source,
|
|
144
|
+
target=target,
|
|
145
|
+
original_dims=original.dims,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def vector_quad_transform(
|
|
151
|
+
original: xr.DataArray,
|
|
152
|
+
source: FieldLayout,
|
|
153
|
+
canonical_u: xr.DataArray,
|
|
154
|
+
canonical_v: xr.DataArray,
|
|
155
|
+
transform: VectorQuadKernel,
|
|
156
|
+
) -> tuple[xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray]:
|
|
157
|
+
"""Apply one paired-vector kernel that returns four source-grid maps."""
|
|
158
|
+
dask_field = _dask_input(canonical_u, canonical_v)
|
|
159
|
+
|
|
160
|
+
def kernel(
|
|
161
|
+
frame_u: NDArray[np.generic], frame_v: NDArray[np.generic]
|
|
162
|
+
) -> tuple[
|
|
163
|
+
NDArray[np.float64],
|
|
164
|
+
NDArray[np.float64],
|
|
165
|
+
NDArray[np.float64],
|
|
166
|
+
NDArray[np.float64],
|
|
167
|
+
]:
|
|
168
|
+
return transform(frame_u, frame_v)
|
|
169
|
+
|
|
170
|
+
outputs = xr.apply_ufunc(
|
|
171
|
+
kernel,
|
|
172
|
+
canonical_u,
|
|
173
|
+
canonical_v,
|
|
174
|
+
input_core_dims=[
|
|
175
|
+
[source.latitude_dim, source.longitude_dim],
|
|
176
|
+
[source.latitude_dim, source.longitude_dim],
|
|
177
|
+
],
|
|
178
|
+
output_core_dims=[
|
|
179
|
+
[source.latitude_dim, source.longitude_dim],
|
|
180
|
+
[source.latitude_dim, source.longitude_dim],
|
|
181
|
+
[source.latitude_dim, source.longitude_dim],
|
|
182
|
+
[source.latitude_dim, source.longitude_dim],
|
|
183
|
+
],
|
|
184
|
+
vectorize=True,
|
|
185
|
+
output_dtypes=[np.float64, np.float64, np.float64, np.float64],
|
|
186
|
+
**apply_ufunc_options(dask_field),
|
|
187
|
+
)
|
|
188
|
+
first, second, third, fourth = cast(
|
|
189
|
+
tuple[xr.DataArray, xr.DataArray, xr.DataArray, xr.DataArray], outputs
|
|
190
|
+
)
|
|
191
|
+
return (
|
|
192
|
+
restore_output(
|
|
193
|
+
first,
|
|
194
|
+
source=source,
|
|
195
|
+
target=source,
|
|
196
|
+
original_dims=original.dims,
|
|
197
|
+
),
|
|
198
|
+
restore_output(
|
|
199
|
+
second,
|
|
200
|
+
source=source,
|
|
201
|
+
target=source,
|
|
202
|
+
original_dims=original.dims,
|
|
203
|
+
),
|
|
204
|
+
restore_output(
|
|
205
|
+
third,
|
|
206
|
+
source=source,
|
|
207
|
+
target=source,
|
|
208
|
+
original_dims=original.dims,
|
|
209
|
+
),
|
|
210
|
+
restore_output(
|
|
211
|
+
fourth,
|
|
212
|
+
source=source,
|
|
213
|
+
target=source,
|
|
214
|
+
original_dims=original.dims,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _dask_input(first: xr.DataArray, second: xr.DataArray) -> xr.DataArray:
|
|
220
|
+
return first if first.chunks is not None else second
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _output_changes_shape(source: FieldLayout, target: FieldLayout) -> bool:
|
|
224
|
+
return (
|
|
225
|
+
source.latitude_dim != target.latitude_dim
|
|
226
|
+
or source.longitude_dim != target.longitude_dim
|
|
227
|
+
or source.grid.nlat != target.grid.nlat
|
|
228
|
+
or source.grid.nlon != target.grid.nlon
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _require_matching_layouts(
|
|
233
|
+
first: FieldLayout, second: FieldLayout, first_name: str, second_name: str
|
|
234
|
+
) -> None:
|
|
235
|
+
if (
|
|
236
|
+
first.latitude_dim != second.latitude_dim
|
|
237
|
+
or first.longitude_dim != second.longitude_dim
|
|
238
|
+
or first.coordinates.latitude_name != second.coordinates.latitude_name
|
|
239
|
+
or first.coordinates.longitude_name != second.coordinates.longitude_name
|
|
240
|
+
):
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"{first_name} and {second_name} must use the same horizontal "
|
|
243
|
+
"coordinate names and dimensions"
|
|
244
|
+
)
|
|
245
|
+
if not grids_equivalent(first.grid, second.grid):
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"{first_name} and {second_name} must describe equivalent supported grids"
|
|
248
|
+
)
|