pygeoinf 1.0.8__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 +113 -85
- 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 -435
- pygeoinf/symmetric_space/symmetric_space.py +330 -142
- {pygeoinf-1.0.8.dist-info → pygeoinf-1.1.0.dist-info}/METADATA +1 -1
- pygeoinf-1.1.0.dist-info/RECORD +20 -0
- pygeoinf/forms.py +0 -168
- pygeoinf/symmetric_space/line.py +0 -384
- pygeoinf-1.0.8.dist-info/RECORD +0 -21
- {pygeoinf-1.0.8.dist-info → pygeoinf-1.1.0.dist-info}/LICENSE +0 -0
- {pygeoinf-1.0.8.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
|
-
)
|
|
118
|
-
|
|
119
|
-
# ===============================================#
|
|
120
|
-
# Static methods #
|
|
121
|
-
# ===============================================#
|
|
122
|
-
|
|
123
|
-
@staticmethod
|
|
124
|
-
def from_sobolev_parameters(
|
|
125
|
-
order: float,
|
|
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")
|
|
86
|
+
self._grid = grid
|
|
87
|
+
self._sampling = 1
|
|
157
88
|
|
|
158
|
-
|
|
159
|
-
l = 0
|
|
160
|
-
err = 1.0
|
|
161
|
-
sobolev_func = lambda deg: (1.0 + scale**2 * deg * (deg + 1)) ** order
|
|
89
|
+
self._extend: bool = extend
|
|
162
90
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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,181 +139,70 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
217
139
|
"""The Condon-Shortley phase convention used (1)."""
|
|
218
140
|
return self._csphase
|
|
219
141
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
142
|
+
@property
|
|
143
|
+
def spatial_dimension(self) -> int:
|
|
144
|
+
"""The dimension of the space."""
|
|
145
|
+
return 2
|
|
223
146
|
|
|
224
147
|
def random_point(self) -> List[float]:
|
|
225
148
|
"""Returns a random point as `[latitude, longitude]`."""
|
|
226
|
-
latitude = np.random.uniform(-
|
|
149
|
+
latitude = np.rad2deg(np.arcsin(np.random.uniform(-1.0, 1.0)))
|
|
227
150
|
longitude = np.random.uniform(0.0, 360.0)
|
|
228
151
|
return [latitude, longitude]
|
|
229
152
|
|
|
230
|
-
def
|
|
231
|
-
self,
|
|
232
|
-
truncation_degree: int,
|
|
233
|
-
/,
|
|
234
|
-
*,
|
|
235
|
-
smoother: Optional[Callable[[int], float]] = None,
|
|
236
|
-
) -> "LinearOperator":
|
|
153
|
+
def laplacian_eigenvalue(self, k: [int, int]) -> float:
|
|
237
154
|
"""
|
|
238
|
-
Returns
|
|
239
|
-
|
|
240
|
-
This can be used for truncating or smoothing a field in the spherical
|
|
241
|
-
harmonic domain.
|
|
155
|
+
Returns the (l.m)-th eigenvalue of the Laplacian.
|
|
242
156
|
|
|
243
157
|
Args:
|
|
244
|
-
|
|
245
|
-
smoother: An optional callable `f(l)` that applies a degree-dependent
|
|
246
|
-
weighting factor during projection.
|
|
247
|
-
|
|
248
|
-
Returns:
|
|
249
|
-
A `LinearOperator` that maps from this space to a new, lower-degree
|
|
250
|
-
`Sobolev` space.
|
|
158
|
+
k = (l,m): The index of the eigenvalue to return.
|
|
251
159
|
"""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
# Default smoother is an identity mapping.
|
|
256
|
-
f: Callable[[int], float] = smoother if smoother is not None else lambda l: 1.0
|
|
257
|
-
|
|
258
|
-
row_dim = (truncation_degree + 1) ** 2
|
|
259
|
-
col_dim = (self.lmax + 1) ** 2
|
|
260
|
-
|
|
261
|
-
# Construct the sparse matrix that performs the coordinate projection.
|
|
262
|
-
row, col = 0, 0
|
|
263
|
-
rows, cols, data = [], [], []
|
|
264
|
-
for l in range(self.lmax + 1):
|
|
265
|
-
fac = f(l)
|
|
266
|
-
for _ in range(l + 1):
|
|
267
|
-
if l <= truncation_degree:
|
|
268
|
-
rows.append(row)
|
|
269
|
-
row += 1
|
|
270
|
-
cols.append(col)
|
|
271
|
-
data.append(fac)
|
|
272
|
-
col += 1
|
|
273
|
-
|
|
274
|
-
for l in range(truncation_degree + 1):
|
|
275
|
-
fac = f(l)
|
|
276
|
-
for _ in range(1, l + 1):
|
|
277
|
-
rows.append(row)
|
|
278
|
-
row += 1
|
|
279
|
-
cols.append(col)
|
|
280
|
-
data.append(fac)
|
|
281
|
-
col += 1
|
|
282
|
-
|
|
283
|
-
smat = coo_array(
|
|
284
|
-
(data, (rows, cols)), shape=(row_dim, col_dim), dtype=float
|
|
285
|
-
).tocsc()
|
|
286
|
-
|
|
287
|
-
codomain = Sobolev(
|
|
288
|
-
truncation_degree,
|
|
289
|
-
self.order,
|
|
290
|
-
self.scale,
|
|
291
|
-
vector_as_SHGrid=self._vector_as_SHGrid,
|
|
292
|
-
radius=self.radius,
|
|
293
|
-
grid=self._grid,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
def mapping(u: Any) -> Any:
|
|
297
|
-
uc = self.to_components(u)
|
|
298
|
-
vc = smat @ uc
|
|
299
|
-
return codomain.from_components(vc)
|
|
300
|
-
|
|
301
|
-
def adjoint_mapping(v: Any) -> Any:
|
|
302
|
-
vc = codomain.to_components(v)
|
|
303
|
-
uc = smat.T @ vc
|
|
304
|
-
return self.from_components(uc)
|
|
160
|
+
l = k[0]
|
|
161
|
+
return l * (l + 1) / self.radius**2
|
|
305
162
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def dirac(self, point: List[float]) -> "LinearForm":
|
|
163
|
+
def trace_of_invariant_automorphism(self, f: Callable[[float], float]) -> float:
|
|
309
164
|
"""
|
|
310
|
-
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.
|
|
311
167
|
|
|
312
168
|
Args:
|
|
313
|
-
|
|
169
|
+
f: A real-valued function that is well-defined on the spectrum
|
|
170
|
+
of the Laplacian.
|
|
314
171
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
colatitude,
|
|
321
|
-
longitude,
|
|
322
|
-
normalization=self.normalization,
|
|
323
|
-
degrees=True,
|
|
324
|
-
)
|
|
325
|
-
c = self._to_components_from_coeffs(coeffs)
|
|
326
|
-
# Note: The user confirmed the logic for this return statement is correct
|
|
327
|
-
# for their use case, despite the base class returning a LinearForm.
|
|
328
|
-
return self.dual.from_components(c)
|
|
329
|
-
|
|
330
|
-
def invariant_automorphism(self, f: Callable[[float], float]) -> "LinearOperator":
|
|
331
|
-
matrix = self._degree_dependent_scaling_to_diagonal_matrix(f)
|
|
332
|
-
|
|
333
|
-
def mapping(x: Any) -> Any:
|
|
334
|
-
return self.from_components(matrix @ self.to_components(x))
|
|
335
|
-
|
|
336
|
-
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
|
|
337
177
|
|
|
338
|
-
def
|
|
339
|
-
self, f: Callable[[float], float], /, *, expectation: Optional[Any] = None
|
|
340
|
-
) -> "GaussianMeasure":
|
|
178
|
+
def project_function(self, f: Callable[[(float, float)], float]) -> np.ndarray:
|
|
341
179
|
"""
|
|
342
|
-
Returns
|
|
180
|
+
Returns an element of the space by projecting a given function.
|
|
343
181
|
|
|
344
182
|
Args:
|
|
345
|
-
f:
|
|
346
|
-
expectation: The mean of the measure. Defaults to zero.
|
|
183
|
+
f: A function that takes a point `(lat, lon)` and returns a value.
|
|
347
184
|
"""
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return np.sqrt(f(l) / (self.radius**2 * self._sobolev_function(l)))
|
|
351
|
-
|
|
352
|
-
def h(l: int) -> float:
|
|
353
|
-
return np.sqrt(self.radius**2 * self._sobolev_function(l) * f(l))
|
|
354
|
-
|
|
355
|
-
matrix = self._degree_dependent_scaling_to_diagonal_matrix(g)
|
|
356
|
-
adjoint_matrix = self._degree_dependent_scaling_to_diagonal_matrix(h)
|
|
357
|
-
domain = EuclideanSpace(self.dim)
|
|
358
|
-
|
|
359
|
-
def mapping(c: np.ndarray) -> Any:
|
|
360
|
-
return self.from_components(matrix @ c)
|
|
361
|
-
|
|
362
|
-
def adjoint_mapping(u: Any) -> np.ndarray:
|
|
363
|
-
return adjoint_matrix @ self.to_components(u)
|
|
364
|
-
|
|
365
|
-
inverse_matrix = self._degree_dependent_scaling_to_diagonal_matrix(
|
|
366
|
-
lambda l: 1.0 / g(l)
|
|
367
|
-
)
|
|
368
|
-
inverse_adjoint_matrix = self._degree_dependent_scaling_to_diagonal_matrix(
|
|
369
|
-
lambda l: 1.0 / h(l)
|
|
185
|
+
u = sh.SHGrid.from_zeros(
|
|
186
|
+
self.lmax, grid=self.grid, extend=self.extend, sampling=self._sampling
|
|
370
187
|
)
|
|
188
|
+
for j, lon in enumerate(u.lons()):
|
|
189
|
+
for i, lat in enumerate(u.lats()):
|
|
190
|
+
u.data[i, j] = f((lat, lon))
|
|
371
191
|
|
|
372
|
-
|
|
373
|
-
return inverse_matrix @ self.to_components(u)
|
|
192
|
+
return u
|
|
374
193
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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)
|
|
378
197
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self, domain, inverse_mapping, adjoint_mapping=inverse_adjoint_mapping
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
return GaussianMeasure(
|
|
387
|
-
covariance_factor=covariance_factor,
|
|
388
|
-
inverse_covariance_factor=inverse_covariance_factor,
|
|
389
|
-
expectation=expectation,
|
|
390
|
-
)
|
|
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)
|
|
391
202
|
|
|
392
203
|
def plot(
|
|
393
204
|
self,
|
|
394
|
-
|
|
205
|
+
u: sh.SHGrid,
|
|
395
206
|
/,
|
|
396
207
|
*,
|
|
397
208
|
projection: "Projection" = ccrs.PlateCarree(),
|
|
@@ -409,7 +220,7 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
409
220
|
Creates a map plot of a function on the sphere using `cartopy`.
|
|
410
221
|
|
|
411
222
|
Args:
|
|
412
|
-
|
|
223
|
+
u: The element to be plotted.
|
|
413
224
|
projection: A `cartopy.crs` projection. Defaults to `PlateCarree`.
|
|
414
225
|
contour: If True, creates a filled contour plot. Otherwise, a `pcolormesh` plot.
|
|
415
226
|
cmap: The colormap name.
|
|
@@ -425,14 +236,9 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
425
236
|
Returns:
|
|
426
237
|
A tuple `(figure, axes, image)` containing the Matplotlib and Cartopy objects.
|
|
427
238
|
"""
|
|
428
|
-
field: sh.SHGrid = (
|
|
429
|
-
f
|
|
430
|
-
if self._vector_as_SHGrid
|
|
431
|
-
else f.expand(normalization=self.normalization, csphase=self.csphase)
|
|
432
|
-
)
|
|
433
239
|
|
|
434
|
-
lons =
|
|
435
|
-
lats =
|
|
240
|
+
lons = u.lons()
|
|
241
|
+
lats = u.lats()
|
|
436
242
|
|
|
437
243
|
figsize: Tuple[int, int] = kwargs.pop("figsize", (10, 8))
|
|
438
244
|
fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": projection})
|
|
@@ -448,7 +254,7 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
448
254
|
|
|
449
255
|
kwargs.setdefault("cmap", cmap)
|
|
450
256
|
if symmetric:
|
|
451
|
-
data_max = 1.2 * np.nanmax(np.abs(
|
|
257
|
+
data_max = 1.2 * np.nanmax(np.abs(u.data))
|
|
452
258
|
kwargs.setdefault("vmin", -data_max)
|
|
453
259
|
kwargs.setdefault("vmax", data_max)
|
|
454
260
|
|
|
@@ -458,14 +264,14 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
458
264
|
im = ax.contourf(
|
|
459
265
|
lons,
|
|
460
266
|
lats,
|
|
461
|
-
|
|
267
|
+
u.data,
|
|
462
268
|
transform=ccrs.PlateCarree(),
|
|
463
269
|
levels=levels,
|
|
464
270
|
**kwargs,
|
|
465
271
|
)
|
|
466
272
|
else:
|
|
467
273
|
im = ax.pcolormesh(
|
|
468
|
-
lons, lats,
|
|
274
|
+
lons, lats, u.data, transform=ccrs.PlateCarree(), **kwargs
|
|
469
275
|
)
|
|
470
276
|
|
|
471
277
|
if gridlines:
|
|
@@ -485,28 +291,12 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
485
291
|
|
|
486
292
|
return fig, ax, im
|
|
487
293
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
294
|
+
# --------------------------------------------------------------- #
|
|
295
|
+
# private methods #
|
|
296
|
+
# ----------------------------------------------------------------#
|
|
491
297
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"""
|
|
495
|
-
if not isinstance(other, Sobolev):
|
|
496
|
-
return NotImplemented
|
|
497
|
-
|
|
498
|
-
return (
|
|
499
|
-
self.lmax == other.lmax
|
|
500
|
-
and self.order == other.order
|
|
501
|
-
and self.scale == other.scale
|
|
502
|
-
and self.radius == other.radius
|
|
503
|
-
and self.grid == other.grid
|
|
504
|
-
and self._vector_as_SHGrid == other._vector_as_SHGrid
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
# ==============================================#
|
|
508
|
-
# Private methods #
|
|
509
|
-
# ==============================================#
|
|
298
|
+
def _grid_name(self):
|
|
299
|
+
return self.grid if self._sampling == 1 else "DH2"
|
|
510
300
|
|
|
511
301
|
def _coefficient_to_component_mapping(self) -> coo_array:
|
|
512
302
|
"""Builds a sparse matrix to map `pyshtools` coeffs to component vectors."""
|
|
@@ -536,38 +326,10 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
536
326
|
(data, (rows, cols)), shape=(row_dim, col_dim), dtype=float
|
|
537
327
|
).tocsc()
|
|
538
328
|
|
|
539
|
-
def
|
|
540
|
-
"""Returns a component vector from a `pyshtools` coefficient array."""
|
|
541
|
-
f = coeffs.flatten(order="C")
|
|
542
|
-
return self._sparse_coeffs_to_component @ f
|
|
543
|
-
|
|
544
|
-
def _to_components_from_SHCoeffs(self, ulm: sh.SHCoeffs) -> np.ndarray:
|
|
545
|
-
"""Returns a component vector from an `SHCoeffs` object."""
|
|
546
|
-
return self._to_components_from_coeffs(ulm.coeffs)
|
|
547
|
-
|
|
548
|
-
def _to_components_from_SHGrid(self, u: sh.SHGrid) -> np.ndarray:
|
|
549
|
-
"""Returns a component vector from an `SHGrid` object."""
|
|
550
|
-
ulm = u.expand(normalization=self.normalization, csphase=self.csphase)
|
|
551
|
-
return self._to_components_from_SHCoeffs(ulm)
|
|
552
|
-
|
|
553
|
-
def _from_components_to_SHCoeffs(self, c: np.ndarray) -> sh.SHCoeffs:
|
|
554
|
-
"""Returns an `SHCoeffs` object from its component vector."""
|
|
555
|
-
f = self._sparse_coeffs_to_component.T @ c
|
|
556
|
-
coeffs = f.reshape((2, self.lmax + 1, self.lmax + 1))
|
|
557
|
-
return sh.SHCoeffs.from_array(
|
|
558
|
-
coeffs, normalization=self.normalization, csphase=self.csphase
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
def _from_components_to_SHGrid(self, c: np.ndarray) -> sh.SHGrid:
|
|
562
|
-
"""Returns an `SHGrid` object from its component vector."""
|
|
563
|
-
ulm = self._from_components_to_SHCoeffs(c)
|
|
564
|
-
return ulm.expand(grid=self.grid, extend=self.extend)
|
|
565
|
-
|
|
566
|
-
def _degree_dependent_scaling_to_diagonal_matrix(
|
|
567
|
-
self, f: Callable[[int], float]
|
|
568
|
-
) -> diags:
|
|
329
|
+
def _degree_dependent_scaling_values(self, f: Callable[[int], float]) -> diags:
|
|
569
330
|
"""Creates a diagonal sparse matrix from a function of degree `l`."""
|
|
570
|
-
|
|
331
|
+
dim = (self.lmax + 1) ** 2
|
|
332
|
+
values = np.zeros(dim)
|
|
571
333
|
i = 0
|
|
572
334
|
for l in range(self.lmax + 1):
|
|
573
335
|
j = i + l + 1
|
|
@@ -577,124 +339,262 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
577
339
|
j = i + l
|
|
578
340
|
values[i:j] = f(l)
|
|
579
341
|
i = j
|
|
580
|
-
return
|
|
342
|
+
return values
|
|
581
343
|
|
|
582
|
-
def
|
|
583
|
-
"""
|
|
584
|
-
|
|
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
|
|
585
348
|
|
|
586
|
-
def
|
|
587
|
-
"""
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
590
355
|
)
|
|
591
356
|
|
|
592
|
-
def _to_dual_impl(self, u: Any) -> "LinearForm":
|
|
593
|
-
"""Implements the mapping to the dual space."""
|
|
594
|
-
c = self._metric_tensor @ self.to_components(u) * self.radius**2
|
|
595
|
-
return self.dual.from_components(c)
|
|
596
357
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
):
|
|
377
|
+
|
|
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)
|
|
601
397
|
|
|
602
|
-
def
|
|
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:
|
|
603
409
|
"""
|
|
604
410
|
Custom in-place ax implementation for pyshtools objects.
|
|
605
411
|
x := a*x
|
|
606
412
|
"""
|
|
607
|
-
|
|
608
|
-
# For SHGrid objects, modify the .data array
|
|
609
|
-
x.data *= a
|
|
610
|
-
else:
|
|
611
|
-
# For SHCoeffs objects, modify the .coeffs array
|
|
612
|
-
x.coeffs *= a
|
|
413
|
+
x.data *= a
|
|
613
414
|
|
|
614
|
-
def
|
|
415
|
+
def axpy(self, a: float, x: sh.SHGrid, y: sh.SHGrid) -> None:
|
|
615
416
|
"""
|
|
616
417
|
Custom in-place axpy implementation for pyshtools objects.
|
|
617
418
|
y := a*x + y
|
|
618
419
|
"""
|
|
619
|
-
|
|
620
|
-
# For SHGrid objects, modify the .data array
|
|
621
|
-
y.data += a * x.data
|
|
622
|
-
else:
|
|
623
|
-
# For SHCoeffs objects, modify the .coeffs array
|
|
624
|
-
y.coeffs += a * x.coeffs
|
|
420
|
+
y.data += a * x.data
|
|
625
421
|
|
|
626
|
-
def
|
|
627
|
-
"""
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
u1_field = u1.expand(grid=self.grid, extend=self.extend)
|
|
632
|
-
u2_field = u2.expand(grid=self.grid, extend=self.extend)
|
|
633
|
-
u3_field = u1_field * u2_field
|
|
634
|
-
return u3_field.expand(
|
|
635
|
-
normalization=self.normalization, csphase=self.csphase
|
|
636
|
-
)
|
|
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
|
|
637
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
|
+
)
|
|
638
434
|
|
|
639
|
-
|
|
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
|
+
# ================================================================ #
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class Sobolev(SphereHelper, MassWeightedHilbertModule, AbstractInvariantSobolevSpace):
|
|
640
454
|
"""
|
|
641
|
-
|
|
455
|
+
Implementation of the Sobolev space Hˢ on the sphere.
|
|
642
456
|
|
|
643
|
-
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).
|
|
644
462
|
"""
|
|
645
463
|
|
|
646
464
|
def __init__(
|
|
647
465
|
self,
|
|
648
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,
|
|
649
496
|
/,
|
|
650
497
|
*,
|
|
651
|
-
vector_as_SHGrid: bool = True,
|
|
652
498
|
radius: float = 1.0,
|
|
499
|
+
vector_as_SHGrid: bool = True,
|
|
653
500
|
grid: str = "DH",
|
|
654
|
-
|
|
501
|
+
rtol: float = 1e-8,
|
|
502
|
+
power_of_two: bool = False,
|
|
503
|
+
) -> "Sobolev":
|
|
655
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
|
+
|
|
656
512
|
Args:
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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`.
|
|
662
522
|
"""
|
|
663
|
-
|
|
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(
|
|
664
545
|
lmax,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
vector_as_SHGrid=vector_as_SHGrid,
|
|
546
|
+
order,
|
|
547
|
+
scale,
|
|
668
548
|
radius=radius,
|
|
669
549
|
grid=grid,
|
|
670
550
|
)
|
|
671
551
|
|
|
552
|
+
def __eq__(self, other: object) -> bool:
|
|
553
|
+
"""
|
|
554
|
+
Checks for mathematical equality with another Sobolev space on a sphere.
|
|
672
555
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
+
)
|
|
568
|
+
|
|
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)
|
|
677
575
|
|
|
678
|
-
def
|
|
576
|
+
def dirac(self, point: (float, float)) -> LinearForm:
|
|
679
577
|
"""
|
|
578
|
+
Returns the linear functional for point evaluation (Dirac measure).
|
|
579
|
+
|
|
680
580
|
Args:
|
|
681
|
-
|
|
682
|
-
upper_degree: Above this degree `l`, the filter gain is 0.
|
|
581
|
+
point: A tuple containing `(latitude, longitude)`.
|
|
683
582
|
"""
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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)
|