pygeoinf 1.0.9__py3-none-any.whl → 1.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.
- pygeoinf/__init__.py +5 -1
- pygeoinf/direct_sum.py +101 -75
- pygeoinf/forward_problem.py +33 -32
- pygeoinf/gaussian_measure.py +97 -71
- pygeoinf/hilbert_space.py +517 -241
- pygeoinf/inversion.py +16 -4
- pygeoinf/linear_bayesian.py +57 -36
- pygeoinf/linear_forms.py +169 -0
- pygeoinf/linear_optimisation.py +34 -23
- pygeoinf/linear_solvers.py +74 -247
- pygeoinf/operators.py +175 -36
- pygeoinf/random_matrix.py +36 -32
- pygeoinf/symmetric_space/circle.py +347 -202
- pygeoinf/symmetric_space/sphere.py +335 -448
- pygeoinf/symmetric_space/symmetric_space.py +330 -142
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/METADATA +1 -2
- pygeoinf-1.1.0.dist-info/RECORD +20 -0
- pygeoinf/forms.py +0 -128
- pygeoinf/symmetric_space/line.py +0 -384
- pygeoinf-1.0.9.dist-info/RECORD +0 -21
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/LICENSE +0 -0
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/WHEEL +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Provides concrete implementations of function spaces on the two-sphere (S²).
|
|
3
|
+
|
|
4
|
+
This module uses the abstract framework from the symmetric space module to create
|
|
5
|
+
fully-featured `Lebesgue` (L²) and `Sobolev` (Hˢ) Hilbert spaces for functions
|
|
6
|
+
defined on the surface of a sphere.
|
|
7
|
+
|
|
8
|
+
It utilizes the `pyshtools` library for highly efficient and accurate spherical
|
|
9
|
+
harmonic transforms. Following a compositional design, this module first
|
|
10
|
+
defines a base `Lebesgue` space and then constructs the `Sobolev` space as a
|
|
11
|
+
`MassWeightedHilbertSpace` over it. The module also includes powerful plotting
|
|
12
|
+
utilities built on `cartopy` for professional-quality geospatial visualization.
|
|
13
|
+
|
|
14
|
+
Key Classes
|
|
15
|
+
-----------
|
|
16
|
+
- `SphereHelper`: A mixin class providing the core geometry, spherical harmonic
|
|
17
|
+
transform machinery, and `cartopy`-based plotting utilities.
|
|
18
|
+
- `Lebesgue`: A concrete implementation of the L²(S²) space of square-integrable
|
|
19
|
+
functions on the sphere.
|
|
20
|
+
- `Sobolev`: A concrete implementation of the Hˢ(S²) space of functions with a
|
|
21
|
+
specified degree of smoothness.
|
|
3
22
|
"""
|
|
4
23
|
|
|
5
24
|
from __future__ import annotations
|
|
@@ -7,185 +26,83 @@ from typing import Callable, Any, List, Optional, Tuple, TYPE_CHECKING
|
|
|
7
26
|
|
|
8
27
|
import matplotlib.pyplot as plt
|
|
9
28
|
import matplotlib.ticker as mticker
|
|
10
|
-
|
|
11
29
|
import numpy as np
|
|
12
|
-
|
|
13
30
|
from scipy.sparse import diags, coo_array
|
|
14
31
|
|
|
15
32
|
import pyshtools as sh
|
|
16
|
-
|
|
17
33
|
import cartopy.crs as ccrs
|
|
18
34
|
import cartopy.feature as cfeature
|
|
19
35
|
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
|
|
20
36
|
|
|
21
|
-
from pygeoinf.hilbert_space import
|
|
37
|
+
from pygeoinf.hilbert_space import (
|
|
38
|
+
HilbertModule,
|
|
39
|
+
MassWeightedHilbertModule,
|
|
40
|
+
)
|
|
22
41
|
from pygeoinf.operators import LinearOperator
|
|
23
|
-
from pygeoinf.
|
|
24
|
-
from
|
|
42
|
+
from pygeoinf.linear_forms import LinearForm
|
|
43
|
+
from .symmetric_space import (
|
|
44
|
+
AbstractInvariantLebesgueSpace,
|
|
45
|
+
AbstractInvariantSobolevSpace,
|
|
46
|
+
)
|
|
47
|
+
|
|
25
48
|
|
|
26
|
-
# This block only runs for type checkers, not at runtime
|
|
27
49
|
if TYPE_CHECKING:
|
|
28
50
|
from matplotlib.figure import Figure
|
|
29
|
-
from matplotlib.axes import Axes
|
|
30
51
|
from cartopy.mpl.geoaxes import GeoAxes
|
|
31
52
|
from cartopy.crs import Projection
|
|
32
|
-
from pygeoinf.forms import LinearForm
|
|
33
53
|
|
|
34
54
|
|
|
35
|
-
class
|
|
55
|
+
class SphereHelper:
|
|
36
56
|
"""
|
|
37
|
-
|
|
57
|
+
A mixin providing common functionality for function spaces on the sphere.
|
|
38
58
|
|
|
39
|
-
This
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
This helper is not intended for direct instantiation. It provides the core
|
|
60
|
+
geometry (radius, grid type), the spherical harmonic transform machinery via
|
|
61
|
+
`pyshtools`, and `cartopy`-based plotting utilities that are shared by the
|
|
62
|
+
`Lebesgue` and `Sobolev` space classes.
|
|
42
63
|
"""
|
|
43
64
|
|
|
44
65
|
def __init__(
|
|
45
66
|
self,
|
|
46
67
|
lmax: int,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
vector_as_SHGrid: bool = True,
|
|
52
|
-
radius: float = 1.0,
|
|
53
|
-
grid: str = "DH",
|
|
54
|
-
) -> None:
|
|
68
|
+
radius: float,
|
|
69
|
+
grid: str,
|
|
70
|
+
extend: bool,
|
|
71
|
+
):
|
|
55
72
|
"""
|
|
56
73
|
Args:
|
|
57
|
-
lmax: The maximum spherical harmonic degree
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`pyshtools.SHGrid` objects. Otherwise, they are `pyshtools.SHCoeffs`.
|
|
62
|
-
radius: The radius of the sphere. Defaults to 1.0.
|
|
63
|
-
grid: The `pyshtools` grid type (e.g., 'DH', 'DH2', 'GLQ').
|
|
64
|
-
Defaults to 'DH'.
|
|
74
|
+
lmax: The maximum spherical harmonic degree to be represented.
|
|
75
|
+
radius: Radius of the sphere.
|
|
76
|
+
grid: The `pyshtools` grid type.
|
|
77
|
+
extend: If True, the spatial grid includes both 0 and 360-degree longitudes.
|
|
65
78
|
"""
|
|
66
|
-
|
|
67
79
|
self._lmax: int = lmax
|
|
68
80
|
self._radius: float = radius
|
|
69
|
-
self._grid: str = grid
|
|
70
|
-
self._sampling: int = 2 if self.grid == "DH2" else 1
|
|
71
|
-
self._extend: bool = True
|
|
72
|
-
self._normalization: str = "ortho"
|
|
73
|
-
self._csphase: int = 1
|
|
74
|
-
self._sparse_coeffs_to_component: coo_array = (
|
|
75
|
-
self._coefficient_to_component_mapping()
|
|
76
|
-
)
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if vector_as_SHGrid:
|
|
82
|
-
super().__init__(
|
|
83
|
-
order,
|
|
84
|
-
scale,
|
|
85
|
-
dim,
|
|
86
|
-
self._to_components_from_SHGrid,
|
|
87
|
-
self._from_components_to_SHGrid,
|
|
88
|
-
self._inner_product_impl,
|
|
89
|
-
self._to_dual_impl,
|
|
90
|
-
self._from_dual_impl,
|
|
91
|
-
ax=self._ax_impl,
|
|
92
|
-
axpy=self._axpy_impl,
|
|
93
|
-
vector_multiply=self._vector_multiply_impl,
|
|
94
|
-
)
|
|
82
|
+
if grid == "DH2":
|
|
83
|
+
self._grid = "DH"
|
|
84
|
+
self._sampling = 2
|
|
95
85
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
scale,
|
|
99
|
-
dim,
|
|
100
|
-
self._to_components_from_SHCoeffs,
|
|
101
|
-
self._from_components_to_SHCoeffs,
|
|
102
|
-
self._inner_product_impl,
|
|
103
|
-
self._to_dual_impl,
|
|
104
|
-
self._from_dual_impl,
|
|
105
|
-
ax=self._ax_impl,
|
|
106
|
-
axpy=self._axpy_impl,
|
|
107
|
-
vector_multiply=self._vector_multiply_impl,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
self._metric_tensor: diags = self._degree_dependent_scaling_to_diagonal_matrix(
|
|
111
|
-
self._sobolev_function
|
|
112
|
-
)
|
|
113
|
-
self._inverse_metric_tensor: diags = (
|
|
114
|
-
self._degree_dependent_scaling_to_diagonal_matrix(
|
|
115
|
-
lambda l: 1.0 / self._sobolev_function(l)
|
|
116
|
-
)
|
|
117
|
-
)
|
|
86
|
+
self._grid = grid
|
|
87
|
+
self._sampling = 1
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
# Static methods #
|
|
121
|
-
# ===============================================#
|
|
89
|
+
self._extend: bool = extend
|
|
122
90
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
scale: float,
|
|
127
|
-
/,
|
|
128
|
-
*,
|
|
129
|
-
radius: float = 1.0,
|
|
130
|
-
vector_as_SHGrid: bool = True,
|
|
131
|
-
grid: str = "DH",
|
|
132
|
-
rtol: float = 1e-8,
|
|
133
|
-
power_of_two: bool = False,
|
|
134
|
-
) -> "Sobolev":
|
|
135
|
-
"""
|
|
136
|
-
Creates an instance with `lmax` chosen based on the Sobolev parameters.
|
|
137
|
-
|
|
138
|
-
This factory method estimates the spherical harmonic truncation degree
|
|
139
|
-
(`lmax`) required to represent the space while meeting a specified
|
|
140
|
-
relative tolerance for the truncation error. This is useful when the
|
|
141
|
-
required `lmax` is not known a priori.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
order: The order of the Sobolev space, controlling smoothness.
|
|
145
|
-
scale: The non-dimensional length-scale for the space.
|
|
146
|
-
radius: The radius of the sphere. Defaults to 1.0.
|
|
147
|
-
vector_as_SHGrid: If True (default), elements are `SHGrid` objects.
|
|
148
|
-
grid: The `pyshtools` grid type (e.g., 'DH'). Defaults to 'DH'.
|
|
149
|
-
rtol: The relative tolerance used to determine the `lmax`.
|
|
150
|
-
power_of_two: If True, `lmax` is set to the next power of two.
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
An instance of the Sobolev class with a calculated `lmax`.
|
|
154
|
-
"""
|
|
155
|
-
if order <= 1.0:
|
|
156
|
-
raise ValueError("This method is only applicable for orders > 1.0")
|
|
157
|
-
|
|
158
|
-
summation = 1.0
|
|
159
|
-
l = 0
|
|
160
|
-
err = 1.0
|
|
161
|
-
sobolev_func = lambda deg: (1.0 + scale**2 * deg * (deg + 1)) ** order
|
|
162
|
-
|
|
163
|
-
while err > rtol:
|
|
164
|
-
l += 1
|
|
165
|
-
term = (2 * l + 1) / sobolev_func(l)
|
|
166
|
-
summation += term
|
|
167
|
-
err = term / summation
|
|
168
|
-
print(l, err)
|
|
169
|
-
if l > 10000:
|
|
170
|
-
raise RuntimeError("Failed to converge on a stable lmax.")
|
|
171
|
-
|
|
172
|
-
if power_of_two:
|
|
173
|
-
n = int(np.log2(l))
|
|
174
|
-
l = 2 ** (n + 1)
|
|
91
|
+
# SH coefficient options fixed internally
|
|
92
|
+
self._normalization: str = "ortho"
|
|
93
|
+
self._csphase: int = 1
|
|
175
94
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
order,
|
|
180
|
-
scale,
|
|
181
|
-
vector_as_SHGrid=vector_as_SHGrid,
|
|
182
|
-
radius=radius,
|
|
183
|
-
grid=grid,
|
|
95
|
+
# Set up sparse matrix that maps SHCoeff data arrrays into reduced form
|
|
96
|
+
self._sparse_coeffs_to_component: coo_array = (
|
|
97
|
+
self._coefficient_to_component_mapping()
|
|
184
98
|
)
|
|
185
99
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
100
|
+
def orthonormalised(self) -> bool:
|
|
101
|
+
"""The space is orthonormalised."""
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _space(self):
|
|
105
|
+
return self
|
|
189
106
|
|
|
190
107
|
@property
|
|
191
108
|
def lmax(self) -> int:
|
|
@@ -202,6 +119,11 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
202
119
|
"""The `pyshtools` grid type used for spatial representations."""
|
|
203
120
|
return self._grid
|
|
204
121
|
|
|
122
|
+
@property
|
|
123
|
+
def sampling(self) -> int:
|
|
124
|
+
"""The sampling factor used for spatial representations."""
|
|
125
|
+
return self._sampling
|
|
126
|
+
|
|
205
127
|
@property
|
|
206
128
|
def extend(self) -> bool:
|
|
207
129
|
"""True if the spatial grid includes both 0 and 360-degree longitudes."""
|
|
@@ -217,194 +139,70 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
217
139
|
"""The Condon-Shortley phase convention used (1)."""
|
|
218
140
|
return self._csphase
|
|
219
141
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def project_function(self, f: Callable[[(float, float)], float]) -> np.ndarray:
|
|
225
|
-
"""
|
|
226
|
-
Returns an element of the space by projecting a given function.
|
|
227
|
-
|
|
228
|
-
Args:
|
|
229
|
-
f: A function that takes a point `(lat, lon)` and returns a value.
|
|
230
|
-
"""
|
|
231
|
-
u = self.zero
|
|
232
|
-
for j, lon in enumerate(u.lons()):
|
|
233
|
-
for i, lat in enumerate(u.lats()):
|
|
234
|
-
u.data[i, j] = f((lat, lon))
|
|
235
|
-
return u
|
|
142
|
+
@property
|
|
143
|
+
def spatial_dimension(self) -> int:
|
|
144
|
+
"""The dimension of the space."""
|
|
145
|
+
return 2
|
|
236
146
|
|
|
237
147
|
def random_point(self) -> List[float]:
|
|
238
148
|
"""Returns a random point as `[latitude, longitude]`."""
|
|
239
|
-
latitude = np.random.uniform(-
|
|
149
|
+
latitude = np.rad2deg(np.arcsin(np.random.uniform(-1.0, 1.0)))
|
|
240
150
|
longitude = np.random.uniform(0.0, 360.0)
|
|
241
151
|
return [latitude, longitude]
|
|
242
152
|
|
|
243
|
-
def
|
|
244
|
-
self,
|
|
245
|
-
truncation_degree: int,
|
|
246
|
-
/,
|
|
247
|
-
*,
|
|
248
|
-
smoother: Optional[Callable[[int], float]] = None,
|
|
249
|
-
) -> "LinearOperator":
|
|
153
|
+
def laplacian_eigenvalue(self, k: [int, int]) -> float:
|
|
250
154
|
"""
|
|
251
|
-
Returns
|
|
252
|
-
|
|
253
|
-
This can be used for truncating or smoothing a field in the spherical
|
|
254
|
-
harmonic domain.
|
|
155
|
+
Returns the (l.m)-th eigenvalue of the Laplacian.
|
|
255
156
|
|
|
256
157
|
Args:
|
|
257
|
-
|
|
258
|
-
smoother: An optional callable `f(l)` that applies a degree-dependent
|
|
259
|
-
weighting factor during projection.
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
A `LinearOperator` that maps from this space to a new, lower-degree
|
|
263
|
-
`Sobolev` space.
|
|
158
|
+
k = (l,m): The index of the eigenvalue to return.
|
|
264
159
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
268
|
-
# Default smoother is an identity mapping.
|
|
269
|
-
f: Callable[[int], float] = smoother if smoother is not None else lambda l: 1.0
|
|
270
|
-
|
|
271
|
-
row_dim = (truncation_degree + 1) ** 2
|
|
272
|
-
col_dim = (self.lmax + 1) ** 2
|
|
273
|
-
|
|
274
|
-
# Construct the sparse matrix that performs the coordinate projection.
|
|
275
|
-
row, col = 0, 0
|
|
276
|
-
rows, cols, data = [], [], []
|
|
277
|
-
for l in range(self.lmax + 1):
|
|
278
|
-
fac = f(l)
|
|
279
|
-
for _ in range(l + 1):
|
|
280
|
-
if l <= truncation_degree:
|
|
281
|
-
rows.append(row)
|
|
282
|
-
row += 1
|
|
283
|
-
cols.append(col)
|
|
284
|
-
data.append(fac)
|
|
285
|
-
col += 1
|
|
286
|
-
|
|
287
|
-
for l in range(truncation_degree + 1):
|
|
288
|
-
fac = f(l)
|
|
289
|
-
for _ in range(1, l + 1):
|
|
290
|
-
rows.append(row)
|
|
291
|
-
row += 1
|
|
292
|
-
cols.append(col)
|
|
293
|
-
data.append(fac)
|
|
294
|
-
col += 1
|
|
295
|
-
|
|
296
|
-
smat = coo_array(
|
|
297
|
-
(data, (rows, cols)), shape=(row_dim, col_dim), dtype=float
|
|
298
|
-
).tocsc()
|
|
299
|
-
|
|
300
|
-
codomain = Sobolev(
|
|
301
|
-
truncation_degree,
|
|
302
|
-
self.order,
|
|
303
|
-
self.scale,
|
|
304
|
-
vector_as_SHGrid=self._vector_as_SHGrid,
|
|
305
|
-
radius=self.radius,
|
|
306
|
-
grid=self._grid,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
def mapping(u: Any) -> Any:
|
|
310
|
-
uc = self.to_components(u)
|
|
311
|
-
vc = smat @ uc
|
|
312
|
-
return codomain.from_components(vc)
|
|
313
|
-
|
|
314
|
-
def adjoint_mapping(v: Any) -> Any:
|
|
315
|
-
vc = codomain.to_components(v)
|
|
316
|
-
uc = smat.T @ vc
|
|
317
|
-
return self.from_components(uc)
|
|
160
|
+
l = k[0]
|
|
161
|
+
return l * (l + 1) / self.radius**2
|
|
318
162
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def dirac(self, point: List[float]) -> "LinearForm":
|
|
163
|
+
def trace_of_invariant_automorphism(self, f: Callable[[float], float]) -> float:
|
|
322
164
|
"""
|
|
323
|
-
Returns the
|
|
165
|
+
Returns the trace of the automorphism of the form f(Δ) with f a function
|
|
166
|
+
that is well-defined on the spectrum of the Laplacian.
|
|
324
167
|
|
|
325
168
|
Args:
|
|
326
|
-
|
|
169
|
+
f: A real-valued function that is well-defined on the spectrum
|
|
170
|
+
of the Laplacian.
|
|
327
171
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
colatitude,
|
|
334
|
-
longitude,
|
|
335
|
-
normalization=self.normalization,
|
|
336
|
-
degrees=True,
|
|
337
|
-
)
|
|
338
|
-
c = self._to_components_from_coeffs(coeffs)
|
|
339
|
-
# Note: The user confirmed the logic for this return statement is correct
|
|
340
|
-
# for their use case, despite the base class returning a LinearForm.
|
|
341
|
-
return self.dual.from_components(c)
|
|
342
|
-
|
|
343
|
-
def invariant_automorphism(self, f: Callable[[float], float]) -> "LinearOperator":
|
|
344
|
-
matrix = self._degree_dependent_scaling_to_diagonal_matrix(f)
|
|
345
|
-
|
|
346
|
-
def mapping(x: Any) -> Any:
|
|
347
|
-
return self.from_components(matrix @ self.to_components(x))
|
|
348
|
-
|
|
349
|
-
return LinearOperator.self_adjoint(self, mapping)
|
|
172
|
+
trace = 0
|
|
173
|
+
for l in range(self.lmax + 1):
|
|
174
|
+
for m in range(-l, l + 1):
|
|
175
|
+
trace += f(self.laplacian_eigenvalue((l, m)))
|
|
176
|
+
return trace
|
|
350
177
|
|
|
351
|
-
def
|
|
352
|
-
self, f: Callable[[float], float], /, *, expectation: Optional[Any] = None
|
|
353
|
-
) -> "GaussianMeasure":
|
|
178
|
+
def project_function(self, f: Callable[[(float, float)], float]) -> np.ndarray:
|
|
354
179
|
"""
|
|
355
|
-
Returns
|
|
180
|
+
Returns an element of the space by projecting a given function.
|
|
356
181
|
|
|
357
182
|
Args:
|
|
358
|
-
f:
|
|
359
|
-
expectation: The mean of the measure. Defaults to zero.
|
|
183
|
+
f: A function that takes a point `(lat, lon)` and returns a value.
|
|
360
184
|
"""
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return np.sqrt(f(l) / (self.radius**2 * self._sobolev_function(l)))
|
|
364
|
-
|
|
365
|
-
def h(l: int) -> float:
|
|
366
|
-
return np.sqrt(self.radius**2 * self._sobolev_function(l) * f(l))
|
|
367
|
-
|
|
368
|
-
matrix = self._degree_dependent_scaling_to_diagonal_matrix(g)
|
|
369
|
-
adjoint_matrix = self._degree_dependent_scaling_to_diagonal_matrix(h)
|
|
370
|
-
domain = EuclideanSpace(self.dim)
|
|
371
|
-
|
|
372
|
-
def mapping(c: np.ndarray) -> Any:
|
|
373
|
-
return self.from_components(matrix @ c)
|
|
374
|
-
|
|
375
|
-
def adjoint_mapping(u: Any) -> np.ndarray:
|
|
376
|
-
return adjoint_matrix @ self.to_components(u)
|
|
377
|
-
|
|
378
|
-
inverse_matrix = self._degree_dependent_scaling_to_diagonal_matrix(
|
|
379
|
-
lambda l: 1.0 / g(l)
|
|
185
|
+
u = sh.SHGrid.from_zeros(
|
|
186
|
+
self.lmax, grid=self.grid, extend=self.extend, sampling=self._sampling
|
|
380
187
|
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def inverse_mapping(u: Any) -> np.ndarray:
|
|
386
|
-
return inverse_matrix @ self.to_components(u)
|
|
188
|
+
for j, lon in enumerate(u.lons()):
|
|
189
|
+
for i, lat in enumerate(u.lats()):
|
|
190
|
+
u.data[i, j] = f((lat, lon))
|
|
387
191
|
|
|
388
|
-
|
|
389
|
-
# Note: This is a formal adjoint mapping.
|
|
390
|
-
return self.from_components(inverse_adjoint_matrix @ c)
|
|
192
|
+
return u
|
|
391
193
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
inverse_covariance_factor = LinearOperator(
|
|
396
|
-
self, domain, inverse_mapping, adjoint_mapping=inverse_adjoint_mapping
|
|
397
|
-
)
|
|
194
|
+
def to_coefficient(self, u: sh.SHGrid) -> sh.SHCoeffs:
|
|
195
|
+
"""Maps a function vector to its spherical harmonic coefficients."""
|
|
196
|
+
return u.expand(normalization=self.normalization, csphase=self.csphase)
|
|
398
197
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
198
|
+
def from_coefficient(self, ulm: sh.SHCoeffs) -> sh.SHGrid:
|
|
199
|
+
"""Maps spherical harmonic coefficients to a function vector."""
|
|
200
|
+
grid = self.grid if self._sampling == 1 else "DH2"
|
|
201
|
+
return ulm.expand(grid=grid, extend=self.extend)
|
|
404
202
|
|
|
405
203
|
def plot(
|
|
406
204
|
self,
|
|
407
|
-
|
|
205
|
+
u: sh.SHGrid,
|
|
408
206
|
/,
|
|
409
207
|
*,
|
|
410
208
|
projection: "Projection" = ccrs.PlateCarree(),
|
|
@@ -422,7 +220,7 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
422
220
|
Creates a map plot of a function on the sphere using `cartopy`.
|
|
423
221
|
|
|
424
222
|
Args:
|
|
425
|
-
|
|
223
|
+
u: The element to be plotted.
|
|
426
224
|
projection: A `cartopy.crs` projection. Defaults to `PlateCarree`.
|
|
427
225
|
contour: If True, creates a filled contour plot. Otherwise, a `pcolormesh` plot.
|
|
428
226
|
cmap: The colormap name.
|
|
@@ -438,14 +236,9 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
438
236
|
Returns:
|
|
439
237
|
A tuple `(figure, axes, image)` containing the Matplotlib and Cartopy objects.
|
|
440
238
|
"""
|
|
441
|
-
field: sh.SHGrid = (
|
|
442
|
-
f
|
|
443
|
-
if self._vector_as_SHGrid
|
|
444
|
-
else f.expand(normalization=self.normalization, csphase=self.csphase)
|
|
445
|
-
)
|
|
446
239
|
|
|
447
|
-
lons =
|
|
448
|
-
lats =
|
|
240
|
+
lons = u.lons()
|
|
241
|
+
lats = u.lats()
|
|
449
242
|
|
|
450
243
|
figsize: Tuple[int, int] = kwargs.pop("figsize", (10, 8))
|
|
451
244
|
fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": projection})
|
|
@@ -461,7 +254,7 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
461
254
|
|
|
462
255
|
kwargs.setdefault("cmap", cmap)
|
|
463
256
|
if symmetric:
|
|
464
|
-
data_max = 1.2 * np.nanmax(np.abs(
|
|
257
|
+
data_max = 1.2 * np.nanmax(np.abs(u.data))
|
|
465
258
|
kwargs.setdefault("vmin", -data_max)
|
|
466
259
|
kwargs.setdefault("vmax", data_max)
|
|
467
260
|
|
|
@@ -471,14 +264,14 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
471
264
|
im = ax.contourf(
|
|
472
265
|
lons,
|
|
473
266
|
lats,
|
|
474
|
-
|
|
267
|
+
u.data,
|
|
475
268
|
transform=ccrs.PlateCarree(),
|
|
476
269
|
levels=levels,
|
|
477
270
|
**kwargs,
|
|
478
271
|
)
|
|
479
272
|
else:
|
|
480
273
|
im = ax.pcolormesh(
|
|
481
|
-
lons, lats,
|
|
274
|
+
lons, lats, u.data, transform=ccrs.PlateCarree(), **kwargs
|
|
482
275
|
)
|
|
483
276
|
|
|
484
277
|
if gridlines:
|
|
@@ -498,28 +291,12 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
498
291
|
|
|
499
292
|
return fig, ax, im
|
|
500
293
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
294
|
+
# --------------------------------------------------------------- #
|
|
295
|
+
# private methods #
|
|
296
|
+
# ----------------------------------------------------------------#
|
|
504
297
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"""
|
|
508
|
-
if not isinstance(other, Sobolev):
|
|
509
|
-
return NotImplemented
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
self.lmax == other.lmax
|
|
513
|
-
and self.order == other.order
|
|
514
|
-
and self.scale == other.scale
|
|
515
|
-
and self.radius == other.radius
|
|
516
|
-
and self.grid == other.grid
|
|
517
|
-
and self._vector_as_SHGrid == other._vector_as_SHGrid
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
# ==============================================#
|
|
521
|
-
# Private methods #
|
|
522
|
-
# ==============================================#
|
|
298
|
+
def _grid_name(self):
|
|
299
|
+
return self.grid if self._sampling == 1 else "DH2"
|
|
523
300
|
|
|
524
301
|
def _coefficient_to_component_mapping(self) -> coo_array:
|
|
525
302
|
"""Builds a sparse matrix to map `pyshtools` coeffs to component vectors."""
|
|
@@ -549,38 +326,10 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
549
326
|
(data, (rows, cols)), shape=(row_dim, col_dim), dtype=float
|
|
550
327
|
).tocsc()
|
|
551
328
|
|
|
552
|
-
def
|
|
553
|
-
"""Returns a component vector from a `pyshtools` coefficient array."""
|
|
554
|
-
f = coeffs.flatten(order="C")
|
|
555
|
-
return self._sparse_coeffs_to_component @ f
|
|
556
|
-
|
|
557
|
-
def _to_components_from_SHCoeffs(self, ulm: sh.SHCoeffs) -> np.ndarray:
|
|
558
|
-
"""Returns a component vector from an `SHCoeffs` object."""
|
|
559
|
-
return self._to_components_from_coeffs(ulm.coeffs)
|
|
560
|
-
|
|
561
|
-
def _to_components_from_SHGrid(self, u: sh.SHGrid) -> np.ndarray:
|
|
562
|
-
"""Returns a component vector from an `SHGrid` object."""
|
|
563
|
-
ulm = u.expand(normalization=self.normalization, csphase=self.csphase)
|
|
564
|
-
return self._to_components_from_SHCoeffs(ulm)
|
|
565
|
-
|
|
566
|
-
def _from_components_to_SHCoeffs(self, c: np.ndarray) -> sh.SHCoeffs:
|
|
567
|
-
"""Returns an `SHCoeffs` object from its component vector."""
|
|
568
|
-
f = self._sparse_coeffs_to_component.T @ c
|
|
569
|
-
coeffs = f.reshape((2, self.lmax + 1, self.lmax + 1))
|
|
570
|
-
return sh.SHCoeffs.from_array(
|
|
571
|
-
coeffs, normalization=self.normalization, csphase=self.csphase
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
def _from_components_to_SHGrid(self, c: np.ndarray) -> sh.SHGrid:
|
|
575
|
-
"""Returns an `SHGrid` object from its component vector."""
|
|
576
|
-
ulm = self._from_components_to_SHCoeffs(c)
|
|
577
|
-
return ulm.expand(grid=self.grid, extend=self.extend)
|
|
578
|
-
|
|
579
|
-
def _degree_dependent_scaling_to_diagonal_matrix(
|
|
580
|
-
self, f: Callable[[int], float]
|
|
581
|
-
) -> diags:
|
|
329
|
+
def _degree_dependent_scaling_values(self, f: Callable[[int], float]) -> diags:
|
|
582
330
|
"""Creates a diagonal sparse matrix from a function of degree `l`."""
|
|
583
|
-
|
|
331
|
+
dim = (self.lmax + 1) ** 2
|
|
332
|
+
values = np.zeros(dim)
|
|
584
333
|
i = 0
|
|
585
334
|
for l in range(self.lmax + 1):
|
|
586
335
|
j = i + l + 1
|
|
@@ -590,124 +339,262 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
590
339
|
j = i + l
|
|
591
340
|
values[i:j] = f(l)
|
|
592
341
|
i = j
|
|
593
|
-
return
|
|
342
|
+
return values
|
|
594
343
|
|
|
595
|
-
def
|
|
596
|
-
"""
|
|
597
|
-
|
|
344
|
+
def _coefficient_to_component(self, ulm: sh.SHCoeffs) -> np.ndarray:
|
|
345
|
+
"""Maps spherical harmonic coefficients to a component vector."""
|
|
346
|
+
flat_coeffs = ulm.coeffs.flatten(order="C")
|
|
347
|
+
return self._sparse_coeffs_to_component @ flat_coeffs
|
|
598
348
|
|
|
599
|
-
def
|
|
600
|
-
"""
|
|
601
|
-
|
|
602
|
-
|
|
349
|
+
def _component_to_coefficient(self, c: np.ndarray) -> sh.SHCoeffs:
|
|
350
|
+
"""Maps a component vector to spherical harmonic coefficients."""
|
|
351
|
+
flat_coeffs = self._sparse_coeffs_to_component.T @ c
|
|
352
|
+
coeffs = flat_coeffs.reshape((2, self.lmax + 1, self.lmax + 1))
|
|
353
|
+
return sh.SHCoeffs.from_array(
|
|
354
|
+
coeffs, normalization=self.normalization, csphase=self.csphase
|
|
603
355
|
)
|
|
604
356
|
|
|
605
|
-
def _to_dual_impl(self, u: Any) -> "LinearForm":
|
|
606
|
-
"""Implements the mapping to the dual space."""
|
|
607
|
-
c = self._metric_tensor @ self.to_components(u) * self.radius**2
|
|
608
|
-
return self.dual.from_components(c)
|
|
609
357
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
358
|
+
class Lebesgue(SphereHelper, HilbertModule, AbstractInvariantLebesgueSpace):
|
|
359
|
+
"""
|
|
360
|
+
Implementation of the Lebesgue space L² on the sphere.
|
|
361
|
+
|
|
362
|
+
This class represents square-integrable functions on a sphere. A function is
|
|
363
|
+
represented by a `pyshtools.SHGrid` object, which stores its values on a
|
|
364
|
+
regular grid in latitude and longitude. The L² inner product is defined
|
|
365
|
+
in the spherical harmonic domain.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(
|
|
369
|
+
self,
|
|
370
|
+
lmax: int,
|
|
371
|
+
/,
|
|
372
|
+
*,
|
|
373
|
+
radius: float = 1,
|
|
374
|
+
grid: str = "DH",
|
|
375
|
+
extend: bool = True,
|
|
376
|
+
):
|
|
614
377
|
|
|
615
|
-
|
|
378
|
+
if lmax < 0:
|
|
379
|
+
raise ValueError("lmax must be non-negative")
|
|
380
|
+
|
|
381
|
+
self._dim = (lmax + 1) ** 2
|
|
382
|
+
|
|
383
|
+
SphereHelper.__init__(self, lmax, radius, grid, extend)
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def dim(self) -> int:
|
|
387
|
+
"""The dimension of the space."""
|
|
388
|
+
return self._dim
|
|
389
|
+
|
|
390
|
+
def to_components(self, u: sh.SHGrid) -> np.ndarray:
|
|
391
|
+
coeff = self.to_coefficient(u)
|
|
392
|
+
return self._coefficient_to_component(coeff)
|
|
393
|
+
|
|
394
|
+
def from_components(self, c: np.ndarray) -> sh.SHGrid:
|
|
395
|
+
coeff = self._component_to_coefficient(c)
|
|
396
|
+
return self.from_coefficient(coeff)
|
|
397
|
+
|
|
398
|
+
def to_dual(self, u: sh.SHGrid) -> LinearForm:
|
|
399
|
+
coeff = self.to_coefficient(u)
|
|
400
|
+
cp = self._coefficient_to_component(coeff) * self.radius**2
|
|
401
|
+
return self.dual.from_components(cp)
|
|
402
|
+
|
|
403
|
+
def from_dual(self, up: LinearForm) -> sh.SHGrid:
|
|
404
|
+
cp = self.dual.to_components(up) / self.radius**2
|
|
405
|
+
coeff = self._component_to_coefficient(cp)
|
|
406
|
+
return self.from_coefficient(coeff)
|
|
407
|
+
|
|
408
|
+
def ax(self, a: float, x: sh.SHGrid) -> None:
|
|
616
409
|
"""
|
|
617
410
|
Custom in-place ax implementation for pyshtools objects.
|
|
618
411
|
x := a*x
|
|
619
412
|
"""
|
|
620
|
-
|
|
621
|
-
# For SHGrid objects, modify the .data array
|
|
622
|
-
x.data *= a
|
|
623
|
-
else:
|
|
624
|
-
# For SHCoeffs objects, modify the .coeffs array
|
|
625
|
-
x.coeffs *= a
|
|
413
|
+
x.data *= a
|
|
626
414
|
|
|
627
|
-
def
|
|
415
|
+
def axpy(self, a: float, x: sh.SHGrid, y: sh.SHGrid) -> None:
|
|
628
416
|
"""
|
|
629
417
|
Custom in-place axpy implementation for pyshtools objects.
|
|
630
418
|
y := a*x + y
|
|
631
419
|
"""
|
|
632
|
-
|
|
633
|
-
# For SHGrid objects, modify the .data array
|
|
634
|
-
y.data += a * x.data
|
|
635
|
-
else:
|
|
636
|
-
# For SHCoeffs objects, modify the .coeffs array
|
|
637
|
-
y.coeffs += a * x.coeffs
|
|
420
|
+
y.data += a * x.data
|
|
638
421
|
|
|
639
|
-
def
|
|
640
|
-
"""
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
422
|
+
def vector_multiply(self, x1: sh.SHGrid, x2: sh.SHGrid) -> sh.SHGrid:
|
|
423
|
+
"""
|
|
424
|
+
Computes the pointwise product of two functions.
|
|
425
|
+
"""
|
|
426
|
+
return x1 * x2
|
|
427
|
+
|
|
428
|
+
def eigenfunction_norms(self) -> np.ndarray:
|
|
429
|
+
"""Returns a list of the norms of the eigenfunctions."""
|
|
430
|
+
return np.fromiter(
|
|
431
|
+
[self.radius for i in range(self.dim)],
|
|
432
|
+
dtype=float,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def invariant_automorphism(self, f: Callable[[float], float]) -> LinearOperator:
|
|
436
|
+
values = self._degree_dependent_scaling_values(
|
|
437
|
+
lambda l: f(self.laplacian_eigenvalue((l, 0)))
|
|
438
|
+
)
|
|
439
|
+
matrix = diags([values], [0])
|
|
440
|
+
|
|
441
|
+
def mapping(u):
|
|
442
|
+
c = matrix @ (self.to_components(u))
|
|
443
|
+
coeff = self._component_to_coefficient(c)
|
|
444
|
+
return self.from_coefficient(coeff)
|
|
445
|
+
|
|
446
|
+
return LinearOperator.self_adjoint(self, mapping)
|
|
447
|
+
|
|
448
|
+
# ================================================================ #
|
|
449
|
+
# Private methods #
|
|
450
|
+
# ================================================================ #
|
|
650
451
|
|
|
651
452
|
|
|
652
|
-
class
|
|
453
|
+
class Sobolev(SphereHelper, MassWeightedHilbertModule, AbstractInvariantSobolevSpace):
|
|
653
454
|
"""
|
|
654
|
-
|
|
455
|
+
Implementation of the Sobolev space Hˢ on the sphere.
|
|
655
456
|
|
|
656
|
-
This
|
|
457
|
+
This class represents functions with a specified degree of smoothness. It is
|
|
458
|
+
constructed as a `MassWeightedHilbertModule` over the `Lebesgue` space, where
|
|
459
|
+
the mass operator weights the spherical harmonic coefficients to enforce
|
|
460
|
+
smoothness. This is the primary class for defining smooth, random function
|
|
461
|
+
fields (e.g., for geophysics or climate science).
|
|
657
462
|
"""
|
|
658
463
|
|
|
659
464
|
def __init__(
|
|
660
465
|
self,
|
|
661
466
|
lmax: int,
|
|
467
|
+
order: float,
|
|
468
|
+
scale: float,
|
|
469
|
+
/,
|
|
470
|
+
radius: float = 1,
|
|
471
|
+
grid: str = "DH",
|
|
472
|
+
extend: bool = True,
|
|
473
|
+
):
|
|
474
|
+
|
|
475
|
+
if lmax < 0:
|
|
476
|
+
raise ValueError("lmax must be non-negative")
|
|
477
|
+
|
|
478
|
+
SphereHelper.__init__(self, lmax, radius, grid, extend)
|
|
479
|
+
AbstractInvariantSobolevSpace.__init__(self, order, scale)
|
|
480
|
+
|
|
481
|
+
lebesgue = Lebesgue(lmax, radius=radius, grid=grid, extend=extend)
|
|
482
|
+
|
|
483
|
+
mass_operator = lebesgue.invariant_automorphism(self.sobolev_function)
|
|
484
|
+
inverse_mass_operator = lebesgue.invariant_automorphism(
|
|
485
|
+
lambda k: 1.0 / self.sobolev_function(k)
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
MassWeightedHilbertModule.__init__(
|
|
489
|
+
self, lebesgue, mass_operator, inverse_mass_operator
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def from_sobolev_parameters(
|
|
494
|
+
order: float,
|
|
495
|
+
scale: float,
|
|
662
496
|
/,
|
|
663
497
|
*,
|
|
664
|
-
vector_as_SHGrid: bool = True,
|
|
665
498
|
radius: float = 1.0,
|
|
499
|
+
vector_as_SHGrid: bool = True,
|
|
666
500
|
grid: str = "DH",
|
|
667
|
-
|
|
501
|
+
rtol: float = 1e-8,
|
|
502
|
+
power_of_two: bool = False,
|
|
503
|
+
) -> "Sobolev":
|
|
668
504
|
"""
|
|
505
|
+
Creates an instance with `lmax` chosen based on the Sobolev parameters.
|
|
506
|
+
|
|
507
|
+
This factory method estimates the spherical harmonic truncation degree
|
|
508
|
+
(`lmax`) required to represent the space while meeting a specified
|
|
509
|
+
relative tolerance for the truncation error. This is useful when the
|
|
510
|
+
required `lmax` is not known a priori.
|
|
511
|
+
|
|
669
512
|
Args:
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
513
|
+
order: The order of the Sobolev space, controlling smoothness.
|
|
514
|
+
scale: The non-dimensional length-scale for the space.
|
|
515
|
+
radius: The radius of the sphere. Defaults to 1.0.
|
|
516
|
+
grid: The `pyshtools` grid type (e.g., 'DH'). Defaults to 'DH'.
|
|
517
|
+
rtol: The relative tolerance used to determine the `lmax`.
|
|
518
|
+
power_of_two: If True, `lmax` is set to the next power of two.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
An instance of the Sobolev class with a calculated `lmax`.
|
|
675
522
|
"""
|
|
676
|
-
|
|
523
|
+
if order <= 1.0:
|
|
524
|
+
raise ValueError("This method is only applicable for orders > 1.0")
|
|
525
|
+
|
|
526
|
+
summation = 1.0
|
|
527
|
+
l = 0
|
|
528
|
+
err = 1.0
|
|
529
|
+
sobolev_func = lambda deg: (1.0 + scale**2 * deg * (deg + 1)) ** order
|
|
530
|
+
|
|
531
|
+
while err > rtol:
|
|
532
|
+
l += 1
|
|
533
|
+
term = 1 / sobolev_func(l)
|
|
534
|
+
summation += term
|
|
535
|
+
err = term / summation
|
|
536
|
+
if l > 10000:
|
|
537
|
+
raise RuntimeError("Failed to converge on a stable lmax.")
|
|
538
|
+
|
|
539
|
+
if power_of_two:
|
|
540
|
+
n = int(np.log2(l))
|
|
541
|
+
l = 2 ** (n + 1)
|
|
542
|
+
|
|
543
|
+
lmax = l
|
|
544
|
+
return Sobolev(
|
|
677
545
|
lmax,
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
vector_as_SHGrid=vector_as_SHGrid,
|
|
546
|
+
order,
|
|
547
|
+
scale,
|
|
681
548
|
radius=radius,
|
|
682
549
|
grid=grid,
|
|
683
550
|
)
|
|
684
551
|
|
|
552
|
+
def __eq__(self, other: object) -> bool:
|
|
553
|
+
"""
|
|
554
|
+
Checks for mathematical equality with another Sobolev space on a sphere.
|
|
685
555
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
556
|
+
Two spaces are considered equal if they are of the same type and have
|
|
557
|
+
the same defining parameters (kmax, order, scale, and radius).
|
|
558
|
+
"""
|
|
559
|
+
if not isinstance(other, Sobolev):
|
|
560
|
+
return NotImplemented
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
self.lmax == other.lmax
|
|
564
|
+
and self.radius == other.radius
|
|
565
|
+
and self.order == other.order
|
|
566
|
+
and self.scale == other.scale
|
|
567
|
+
)
|
|
690
568
|
|
|
691
|
-
def
|
|
569
|
+
def eigenfunction_norms(self) -> np.ndarray:
|
|
570
|
+
"""Returns a list of the norms of the eigenfunctions."""
|
|
571
|
+
values = self._degree_dependent_scaling_values(
|
|
572
|
+
lambda l: np.sqrt(self.sobolev_function(self.laplacian_eigenvalue((l, 0))))
|
|
573
|
+
)
|
|
574
|
+
return self.radius * np.fromiter(values, dtype=float)
|
|
575
|
+
|
|
576
|
+
def dirac(self, point: (float, float)) -> LinearForm:
|
|
692
577
|
"""
|
|
578
|
+
Returns the linear functional for point evaluation (Dirac measure).
|
|
579
|
+
|
|
693
580
|
Args:
|
|
694
|
-
|
|
695
|
-
upper_degree: Above this degree `l`, the filter gain is 0.
|
|
581
|
+
point: A tuple containing `(latitude, longitude)`.
|
|
696
582
|
"""
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
583
|
+
latitude, longitude = point
|
|
584
|
+
colatitude = 90.0 - latitude
|
|
585
|
+
|
|
586
|
+
coeffs = sh.expand.spharm(
|
|
587
|
+
self.lmax,
|
|
588
|
+
colatitude,
|
|
589
|
+
longitude,
|
|
590
|
+
normalization=self.normalization,
|
|
591
|
+
degrees=True,
|
|
592
|
+
)
|
|
593
|
+
ulm = sh.SHCoeffs.from_array(
|
|
594
|
+
coeffs,
|
|
595
|
+
normalization=self.normalization,
|
|
596
|
+
csphase=self.csphase,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
c = self._coefficient_to_component(ulm)
|
|
600
|
+
return self.dual.from_components(c)
|