pygeoinf 1.2.0__tar.gz → 1.2.1__tar.gz
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-1.2.0 → pygeoinf-1.2.1}/PKG-INFO +1 -1
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/__init__.py +17 -4
- pygeoinf-1.2.1/pygeoinf/backus_gilbert.py +3 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/direct_sum.py +24 -77
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/forward_problem.py +4 -1
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/gaussian_measure.py +35 -10
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/hilbert_space.py +7 -9
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/linear_bayesian.py +1 -1
- pygeoinf-1.2.1/pygeoinf/linear_forms.py +263 -0
- pygeoinf-1.2.0/pygeoinf/operators.py → pygeoinf-1.2.1/pygeoinf/linear_operators.py +279 -303
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/linear_optimisation.py +4 -4
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/linear_solvers.py +74 -7
- pygeoinf-1.2.1/pygeoinf/nonlinear_forms.py +225 -0
- pygeoinf-1.2.1/pygeoinf/nonlinear_operators.py +209 -0
- pygeoinf-1.2.1/pygeoinf/nonlinear_optimisation.py +211 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/parallel.py +1 -1
- pygeoinf-1.2.1/pygeoinf/random_matrix.py +387 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/symmetric_space/circle.py +1 -1
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/symmetric_space/sphere.py +1 -1
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/symmetric_space/symmetric_space.py +1 -1
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pyproject.toml +1 -1
- pygeoinf-1.2.0/pygeoinf/linear_forms.py +0 -169
- pygeoinf-1.2.0/pygeoinf/random_matrix.py +0 -247
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/LICENSE +0 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/README.md +0 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/inversion.py +0 -0
- {pygeoinf-1.2.0 → pygeoinf-1.2.1}/pygeoinf/symmetric_space/__init__.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .random_matrix import (
|
|
2
2
|
fixed_rank_random_range,
|
|
3
3
|
variable_rank_random_range,
|
|
4
|
+
random_range,
|
|
4
5
|
random_svd,
|
|
5
6
|
random_eig,
|
|
6
7
|
random_cholesky,
|
|
@@ -16,16 +17,22 @@ from .hilbert_space import (
|
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
from .
|
|
20
|
-
|
|
21
|
-
LinearOperator,
|
|
22
|
-
DiagonalLinearOperator,
|
|
20
|
+
from .nonlinear_forms import (
|
|
21
|
+
NonLinearForm,
|
|
23
22
|
)
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
from .linear_forms import (
|
|
26
26
|
LinearForm,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
from .nonlinear_operators import NonLinearOperator
|
|
30
|
+
|
|
31
|
+
from .linear_operators import (
|
|
32
|
+
LinearOperator,
|
|
33
|
+
DiagonalLinearOperator,
|
|
34
|
+
)
|
|
35
|
+
|
|
29
36
|
|
|
30
37
|
from .gaussian_measure import (
|
|
31
38
|
GaussianMeasure,
|
|
@@ -45,7 +52,9 @@ from .linear_solvers import (
|
|
|
45
52
|
DirectLinearSolver,
|
|
46
53
|
LUSolver,
|
|
47
54
|
CholeskySolver,
|
|
55
|
+
EigenSolver,
|
|
48
56
|
IterativeLinearSolver,
|
|
57
|
+
ScipyIterativeSolver,
|
|
49
58
|
CGMatrixSolver,
|
|
50
59
|
BICGMatrixSolver,
|
|
51
60
|
BICGStabMatrixSolver,
|
|
@@ -61,3 +70,7 @@ from .linear_optimisation import (
|
|
|
61
70
|
)
|
|
62
71
|
|
|
63
72
|
from .linear_bayesian import LinearBayesianInversion, LinearBayesianInference
|
|
73
|
+
|
|
74
|
+
from .nonlinear_optimisation import (
|
|
75
|
+
ScipyUnconstrainedOptimiser,
|
|
76
|
+
)
|
|
@@ -29,7 +29,7 @@ from scipy.linalg import block_diag
|
|
|
29
29
|
from joblib import Parallel, delayed
|
|
30
30
|
|
|
31
31
|
from .hilbert_space import HilbertSpace
|
|
32
|
-
from .
|
|
32
|
+
from .linear_operators import LinearOperator
|
|
33
33
|
from .linear_forms import LinearForm
|
|
34
34
|
from .parallel import parallel_compute_dense_matrix_from_scipy_op
|
|
35
35
|
|
|
@@ -307,35 +307,18 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
|
|
|
307
307
|
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
308
308
|
) -> np.ndarray:
|
|
309
309
|
"""Overloaded method to efficiently compute the dense matrix for a block operator."""
|
|
310
|
-
if not parallel:
|
|
311
|
-
block_matrices = [
|
|
312
|
-
[
|
|
313
|
-
self.block(i, j).matrix(
|
|
314
|
-
dense=True, galerkin=galerkin, parallel=False
|
|
315
|
-
)
|
|
316
|
-
for j in range(self.col_dim)
|
|
317
|
-
]
|
|
318
|
-
for i in range(self.row_dim)
|
|
319
|
-
]
|
|
320
|
-
return np.block(block_matrices)
|
|
321
310
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
self.block(i, j).matrix(
|
|
325
|
-
|
|
311
|
+
block_matrices = [
|
|
312
|
+
[
|
|
313
|
+
self.block(i, j).matrix(
|
|
314
|
+
dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
315
|
+
)
|
|
326
316
|
for j in range(self.col_dim)
|
|
327
317
|
]
|
|
318
|
+
for i in range(self.row_dim)
|
|
319
|
+
]
|
|
328
320
|
|
|
329
|
-
|
|
330
|
-
delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
|
|
331
|
-
for op in block_scipy_ops
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
block_matrices = [
|
|
335
|
-
computed_blocks[i * self.col_dim : (i + 1) * self.col_dim]
|
|
336
|
-
for i in range(self.row_dim)
|
|
337
|
-
]
|
|
338
|
-
return np.block(block_matrices)
|
|
321
|
+
return np.block(block_matrices)
|
|
339
322
|
|
|
340
323
|
def __mapping(self, xs: List[Any]) -> List[Any]:
|
|
341
324
|
|
|
@@ -420,23 +403,11 @@ class ColumnLinearOperator(LinearOperator, BlockStructure):
|
|
|
420
403
|
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
421
404
|
) -> np.ndarray:
|
|
422
405
|
"""Overloaded method to efficiently compute the dense matrix for a column operator."""
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return np.vstack(block_matrices)
|
|
429
|
-
else:
|
|
430
|
-
block_scipy_ops = [
|
|
431
|
-
op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
|
|
432
|
-
]
|
|
433
|
-
|
|
434
|
-
computed_blocks = Parallel(n_jobs=n_jobs)(
|
|
435
|
-
delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
|
|
436
|
-
for op in block_scipy_ops
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
return np.vstack(computed_blocks)
|
|
406
|
+
block_matrices = [
|
|
407
|
+
op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
|
|
408
|
+
for op in self._operators
|
|
409
|
+
]
|
|
410
|
+
return np.vstack(block_matrices)
|
|
440
411
|
|
|
441
412
|
|
|
442
413
|
class RowLinearOperator(LinearOperator, BlockStructure):
|
|
@@ -496,23 +467,11 @@ class RowLinearOperator(LinearOperator, BlockStructure):
|
|
|
496
467
|
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
497
468
|
) -> np.ndarray:
|
|
498
469
|
"""Overloaded method to efficiently compute the dense matrix for a row operator."""
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
return np.hstack(block_matrices)
|
|
505
|
-
else:
|
|
506
|
-
block_scipy_ops = [
|
|
507
|
-
op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
|
|
508
|
-
]
|
|
509
|
-
|
|
510
|
-
computed_blocks = Parallel(n_jobs=n_jobs)(
|
|
511
|
-
delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
|
|
512
|
-
for op in block_scipy_ops
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
return np.hstack(computed_blocks)
|
|
470
|
+
block_matrices = [
|
|
471
|
+
op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
|
|
472
|
+
for op in self._operators
|
|
473
|
+
]
|
|
474
|
+
return np.hstack(block_matrices)
|
|
516
475
|
|
|
517
476
|
|
|
518
477
|
class BlockDiagonalLinearOperator(LinearOperator, BlockStructure):
|
|
@@ -559,20 +518,8 @@ class BlockDiagonalLinearOperator(LinearOperator, BlockStructure):
|
|
|
559
518
|
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
560
519
|
) -> np.ndarray:
|
|
561
520
|
"""Overloaded method to efficiently compute the dense matrix for a block-diagonal operator."""
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
return block_diag(*block_matrices)
|
|
568
|
-
else:
|
|
569
|
-
block_scipy_ops = [
|
|
570
|
-
op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
|
|
571
|
-
]
|
|
572
|
-
|
|
573
|
-
computed_blocks = Parallel(n_jobs=n_jobs)(
|
|
574
|
-
delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
|
|
575
|
-
for op in block_scipy_ops
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
return block_diag(*computed_blocks)
|
|
521
|
+
block_matrices = [
|
|
522
|
+
op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
|
|
523
|
+
for op in self._operators
|
|
524
|
+
]
|
|
525
|
+
return block_diag(*block_matrices)
|
|
@@ -29,7 +29,7 @@ from .direct_sum import ColumnLinearOperator
|
|
|
29
29
|
# circular import errors while still allowing type hints.
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from .hilbert_space import HilbertSpace, Vector
|
|
32
|
-
from .
|
|
32
|
+
from .linear_operators import LinearOperator
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class ForwardProblem:
|
|
@@ -221,9 +221,11 @@ class LinearForwardProblem(ForwardProblem):
|
|
|
221
221
|
Calculates the chi-squared statistic for a given model and data.
|
|
222
222
|
|
|
223
223
|
This measures the misfit between the predicted and observed data.
|
|
224
|
+
|
|
224
225
|
- If a data error measure with an inverse covariance `C_e^-1` is defined,
|
|
225
226
|
this is the weighted misfit: `(d - A(u))^T * C_e^-1 * (d - A(u))`.
|
|
226
227
|
- Otherwise, it is the squared L2 norm of the data residual: `||d - A(u)||^2`.
|
|
228
|
+
|
|
227
229
|
Args:
|
|
228
230
|
model: A vector from the model space.
|
|
229
231
|
data: An observed data vector from the data space.
|
|
@@ -231,6 +233,7 @@ class LinearForwardProblem(ForwardProblem):
|
|
|
231
233
|
Returns:
|
|
232
234
|
The chi-squared statistic.
|
|
233
235
|
"""
|
|
236
|
+
|
|
234
237
|
residual = self.data_space.subtract(data, self.forward_operator(model))
|
|
235
238
|
|
|
236
239
|
if self.data_error_measure_set:
|
|
@@ -30,7 +30,7 @@ from scipy.stats import multivariate_normal
|
|
|
30
30
|
|
|
31
31
|
from .hilbert_space import EuclideanSpace, HilbertModule
|
|
32
32
|
|
|
33
|
-
from .
|
|
33
|
+
from .linear_operators import (
|
|
34
34
|
LinearOperator,
|
|
35
35
|
DiagonalLinearOperator,
|
|
36
36
|
)
|
|
@@ -491,12 +491,16 @@ class GaussianMeasure:
|
|
|
491
491
|
|
|
492
492
|
def low_rank_approximation(
|
|
493
493
|
self,
|
|
494
|
-
|
|
494
|
+
size_estimate: int,
|
|
495
495
|
/,
|
|
496
496
|
*,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
method: str = "variable",
|
|
498
|
+
max_rank: int = None,
|
|
499
|
+
power: int = 2,
|
|
500
|
+
rtol: float = 1e-4,
|
|
501
|
+
block_size: int = 10,
|
|
502
|
+
parallel: bool = False,
|
|
503
|
+
n_jobs: int = -1,
|
|
500
504
|
) -> GaussianMeasure:
|
|
501
505
|
"""
|
|
502
506
|
Constructs a low-rank approximation of the measure.
|
|
@@ -505,16 +509,37 @@ class GaussianMeasure:
|
|
|
505
509
|
can be much more efficient for sampling and storage.
|
|
506
510
|
|
|
507
511
|
Args:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
method (
|
|
511
|
-
|
|
512
|
+
size_estimate: For 'fixed' method, the exact target rank. For 'variable'
|
|
513
|
+
method, this is the initial rank to sample.
|
|
514
|
+
method ({'variable', 'fixed'}): The algorithm to use.
|
|
515
|
+
- 'variable': (Default) Progressively samples to find the rank needed
|
|
516
|
+
to meet tolerance `rtol`, stopping at `max_rank`.
|
|
517
|
+
- 'fixed': Returns a basis with exactly `size_estimate` columns.
|
|
518
|
+
max_rank: For 'variable' method, a hard limit on the rank. Ignored if
|
|
519
|
+
method='fixed'. Defaults to min(m, n).
|
|
520
|
+
power: Number of power iterations to improve accuracy.
|
|
521
|
+
rtol: Relative tolerance for the 'variable' method. Ignored if
|
|
522
|
+
method='fixed'.
|
|
523
|
+
block_size: Number of new vectors to sample per iteration in 'variable'
|
|
524
|
+
method. Ignored if method='fixed'.
|
|
525
|
+
parallel: Whether to use parallel matrix multiplication.
|
|
526
|
+
n_jobs: Number of jobs for parallelism.
|
|
512
527
|
|
|
513
528
|
Returns:
|
|
514
529
|
GaussianMeasure: The new, low-rank Gaussian measure.
|
|
530
|
+
|
|
531
|
+
Notes:
|
|
532
|
+
Parallel implemention only currently possible with fixed-rank decompositions.
|
|
515
533
|
"""
|
|
516
534
|
covariance_factor = self.covariance.random_cholesky(
|
|
517
|
-
|
|
535
|
+
size_estimate,
|
|
536
|
+
method=method,
|
|
537
|
+
max_rank=max_rank,
|
|
538
|
+
power=power,
|
|
539
|
+
rtol=rtol,
|
|
540
|
+
block_size=block_size,
|
|
541
|
+
parallel=parallel,
|
|
542
|
+
n_jobs=n_jobs,
|
|
518
543
|
)
|
|
519
544
|
|
|
520
545
|
return GaussianMeasure(
|
|
@@ -38,7 +38,7 @@ import numpy as np
|
|
|
38
38
|
|
|
39
39
|
# This block only runs for type checkers, not at runtime
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
|
-
from .
|
|
41
|
+
from .linear_operators import LinearOperator
|
|
42
42
|
from .linear_forms import LinearForm
|
|
43
43
|
|
|
44
44
|
# Define a generic type for vectors in a Hilbert space
|
|
@@ -223,15 +223,13 @@ class HilbertSpace(ABC):
|
|
|
223
223
|
# Final (Non-Overridable) Methods #
|
|
224
224
|
# ------------------------------------------------------------------- #
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
228
226
|
@final
|
|
229
227
|
@property
|
|
230
228
|
def coordinate_inclusion(self) -> LinearOperator:
|
|
231
229
|
"""
|
|
232
230
|
The linear operator mapping R^n component vectors into this space.
|
|
233
231
|
"""
|
|
234
|
-
from .
|
|
232
|
+
from .linear_operators import LinearOperator
|
|
235
233
|
|
|
236
234
|
domain = EuclideanSpace(self.dim)
|
|
237
235
|
|
|
@@ -257,7 +255,7 @@ class HilbertSpace(ABC):
|
|
|
257
255
|
"""
|
|
258
256
|
The linear operator projecting vectors from this space to R^n.
|
|
259
257
|
"""
|
|
260
|
-
from .
|
|
258
|
+
from .linear_operators import LinearOperator
|
|
261
259
|
|
|
262
260
|
codomain = EuclideanSpace(self.dim)
|
|
263
261
|
|
|
@@ -281,7 +279,7 @@ class HilbertSpace(ABC):
|
|
|
281
279
|
@property
|
|
282
280
|
def riesz(self) -> LinearOperator:
|
|
283
281
|
"""The Riesz map (dual to primal) as a `LinearOperator`."""
|
|
284
|
-
from .
|
|
282
|
+
from .linear_operators import LinearOperator
|
|
285
283
|
|
|
286
284
|
return LinearOperator.self_dual(self.dual, self.from_dual)
|
|
287
285
|
|
|
@@ -289,7 +287,7 @@ class HilbertSpace(ABC):
|
|
|
289
287
|
@property
|
|
290
288
|
def inverse_riesz(self) -> LinearOperator:
|
|
291
289
|
"""The inverse Riesz map (primal to dual) as a `LinearOperator`."""
|
|
292
|
-
from .
|
|
290
|
+
from .linear_operators import LinearOperator
|
|
293
291
|
|
|
294
292
|
return LinearOperator.self_dual(self, self.to_dual)
|
|
295
293
|
|
|
@@ -412,7 +410,7 @@ class HilbertSpace(ABC):
|
|
|
412
410
|
@final
|
|
413
411
|
def identity_operator(self) -> LinearOperator:
|
|
414
412
|
"""Returns the identity operator `I` on the space."""
|
|
415
|
-
from .
|
|
413
|
+
from .linear_operators import LinearOperator
|
|
416
414
|
|
|
417
415
|
return LinearOperator(
|
|
418
416
|
self,
|
|
@@ -433,7 +431,7 @@ class HilbertSpace(ABC):
|
|
|
433
431
|
Returns:
|
|
434
432
|
The zero linear operator.
|
|
435
433
|
"""
|
|
436
|
-
from .
|
|
434
|
+
from .linear_operators import LinearOperator
|
|
437
435
|
|
|
438
436
|
codomain = self if codomain is None else codomain
|
|
439
437
|
return LinearOperator(
|
|
@@ -28,7 +28,7 @@ from .gaussian_measure import GaussianMeasure
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
from .forward_problem import LinearForwardProblem
|
|
31
|
-
from .
|
|
31
|
+
from .linear_operators import LinearOperator
|
|
32
32
|
from .linear_solvers import LinearSolver, IterativeLinearSolver
|
|
33
33
|
from .hilbert_space import HilbertSpace, Vector
|
|
34
34
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides the `LinearForm` class for representing linear functionals.
|
|
3
|
+
|
|
4
|
+
A linear form is a linear mapping from a vector in a Hilbert space to a
|
|
5
|
+
scalar. This class provides a concrete, component-based representation for
|
|
6
|
+
elements of the dual space of a `HilbertSpace`. It inherits from `NonLinearForm`,
|
|
7
|
+
specializing it for the linear case.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Callable, Optional, Any, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from joblib import Parallel, delayed
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .nonlinear_forms import NonLinearForm
|
|
18
|
+
|
|
19
|
+
# This block only runs for type checkers, not at runtime
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .hilbert_space import HilbertSpace, EuclideanSpace, Vector
|
|
22
|
+
from .linear_operators import LinearOperator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LinearForm(NonLinearForm):
|
|
26
|
+
"""
|
|
27
|
+
Represents a linear form as an efficient, component-based functional.
|
|
28
|
+
|
|
29
|
+
A `LinearForm` is an element of a dual `HilbertSpace` and is defined by its
|
|
30
|
+
action on vectors from its `domain`. Internally, this action is represented
|
|
31
|
+
by a component vector. This class provides optimized arithmetic operations
|
|
32
|
+
and correctly defines the gradient (a constant vector) and the Hessian
|
|
33
|
+
(the zero operator) for any linear functional.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
domain: HilbertSpace,
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
components: Optional[np.ndarray] = None,
|
|
42
|
+
mapping: Optional[Callable[[Vector], float]] = None,
|
|
43
|
+
parallel: bool = False,
|
|
44
|
+
n_jobs: int = -1,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Initializes the LinearForm from a mapping or component vector.
|
|
48
|
+
|
|
49
|
+
A form must be defined by exactly one of two methods:
|
|
50
|
+
1. **components**: The explicit component vector representing the form.
|
|
51
|
+
2. **mapping**: A function `f(x)` that defines the form's action.
|
|
52
|
+
The components will be automatically computed from this mapping.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
domain: The Hilbert space on which the form is defined.
|
|
57
|
+
components: The component representation of the form.
|
|
58
|
+
mapping: The functional mapping `f(x)`. Used if `components` is None.
|
|
59
|
+
parallel: Whether to use parallel computing components from the mapping.
|
|
60
|
+
n_jobs: The number of jobs to use for parallel computing.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
AssertionError: If neither or both `mapping` and `components`
|
|
64
|
+
are specified.
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
Parallel options only relevant if the form is defined by a mapping.
|
|
68
|
+
|
|
69
|
+
If both `components` and `mapping` are specified, `components`
|
|
70
|
+
will take precedence.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
super().__init__(
|
|
74
|
+
domain,
|
|
75
|
+
self._mapping_impl,
|
|
76
|
+
gradient=self._gradient_impl,
|
|
77
|
+
hessian=self._hessian_impl,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if components is None:
|
|
81
|
+
if mapping is None:
|
|
82
|
+
raise AssertionError("Neither mapping nor components specified.")
|
|
83
|
+
self._compute_components(mapping, parallel, n_jobs)
|
|
84
|
+
else:
|
|
85
|
+
self._components: np.ndarray = components
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def from_linear_operator(operator: "LinearOperator") -> LinearForm:
|
|
89
|
+
"""
|
|
90
|
+
Creates a LinearForm from an operator that maps to a 1D Euclidean space.
|
|
91
|
+
"""
|
|
92
|
+
from .hilbert_space import EuclideanSpace
|
|
93
|
+
|
|
94
|
+
assert operator.codomain == EuclideanSpace(1)
|
|
95
|
+
return LinearForm(operator.domain, mapping=lambda x: operator(x)[0])
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def domain(self) -> HilbertSpace:
|
|
99
|
+
"""The Hilbert space on which the form is defined."""
|
|
100
|
+
return self._domain
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def components(self) -> np.ndarray:
|
|
104
|
+
"""
|
|
105
|
+
The component vector of the form.
|
|
106
|
+
"""
|
|
107
|
+
return self._components
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def as_linear_operator(self) -> "LinearOperator":
|
|
111
|
+
"""
|
|
112
|
+
Represents the linear form as a `LinearOperator`.
|
|
113
|
+
|
|
114
|
+
The resulting operator maps from the form's original domain to a
|
|
115
|
+
1-dimensional `EuclideanSpace`, where the single component of the output
|
|
116
|
+
is the scalar result of the form's action.
|
|
117
|
+
"""
|
|
118
|
+
from .hilbert_space import EuclideanSpace
|
|
119
|
+
from .linear_operators import LinearOperator
|
|
120
|
+
|
|
121
|
+
return LinearOperator(
|
|
122
|
+
self.domain,
|
|
123
|
+
EuclideanSpace(1),
|
|
124
|
+
lambda x: np.array([self(x)]),
|
|
125
|
+
dual_mapping=lambda y: y * self,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def copy(self) -> LinearForm:
|
|
129
|
+
"""
|
|
130
|
+
Creates a deep copy of the linear form.
|
|
131
|
+
"""
|
|
132
|
+
return LinearForm(self.domain, components=self.components.copy())
|
|
133
|
+
|
|
134
|
+
def __neg__(self) -> LinearForm:
|
|
135
|
+
"""Returns the additive inverse of the form."""
|
|
136
|
+
return LinearForm(self.domain, components=-self._components)
|
|
137
|
+
|
|
138
|
+
def __mul__(self, a: float) -> LinearForm:
|
|
139
|
+
"""Returns the product of the form and a scalar."""
|
|
140
|
+
return LinearForm(self.domain, components=a * self._components)
|
|
141
|
+
|
|
142
|
+
def __rmul__(self, a: float) -> LinearForm:
|
|
143
|
+
"""Returns the product of the form and a scalar."""
|
|
144
|
+
return self * a
|
|
145
|
+
|
|
146
|
+
def __truediv__(self, a: float) -> LinearForm:
|
|
147
|
+
"""Returns the division of the form by a scalar."""
|
|
148
|
+
return self * (1.0 / a)
|
|
149
|
+
|
|
150
|
+
def __add__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm:
|
|
151
|
+
"""
|
|
152
|
+
Returns the sum of this form and another.
|
|
153
|
+
|
|
154
|
+
If `other` is also a `LinearForm`, this performs an optimized,
|
|
155
|
+
component-wise addition. Otherwise, it delegates to the general
|
|
156
|
+
implementation in the `NonLinearForm` base class.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
other: The form to add to this one.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A `LinearForm` if adding two `LinearForm`s, otherwise a `NonLinearForm`.
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(other, LinearForm):
|
|
165
|
+
return LinearForm(
|
|
166
|
+
self.domain, components=self.components + other.components
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
return super().__add__(other)
|
|
170
|
+
|
|
171
|
+
def __sub__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm:
|
|
172
|
+
"""
|
|
173
|
+
Returns the difference of this form and another.
|
|
174
|
+
|
|
175
|
+
If `other` is also a `LinearForm`, this performs an optimized,
|
|
176
|
+
component-wise subtraction. Otherwise, it delegates to the general
|
|
177
|
+
implementation in the `NonLinearForm` base class.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
other: The form to subtract from this one.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A `LinearForm` if subtracting two `LinearForm`s, otherwise a `NonLinearForm`.
|
|
184
|
+
"""
|
|
185
|
+
if isinstance(other, LinearForm):
|
|
186
|
+
return LinearForm(
|
|
187
|
+
self.domain, components=self.components - other.components
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
return super().__sub__(other)
|
|
191
|
+
|
|
192
|
+
def __imul__(self, a: float) -> "LinearForm":
|
|
193
|
+
"""
|
|
194
|
+
Performs in-place scalar multiplication: self *= a.
|
|
195
|
+
"""
|
|
196
|
+
self._components *= a
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def __iadd__(self, other: "LinearForm") -> "LinearForm":
|
|
200
|
+
"""
|
|
201
|
+
Performs in-place addition with another form: self += other.
|
|
202
|
+
"""
|
|
203
|
+
if self.domain != other.domain:
|
|
204
|
+
raise ValueError("Linear forms must share the same domain for addition.")
|
|
205
|
+
self._components += other.components
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
"""Returns the string representation of the form's components."""
|
|
210
|
+
return self.components.__str__()
|
|
211
|
+
|
|
212
|
+
def _compute_components(
|
|
213
|
+
self,
|
|
214
|
+
mapping: Callable[[Any], float],
|
|
215
|
+
parallel: bool,
|
|
216
|
+
n_jobs: Optional[int],
|
|
217
|
+
):
|
|
218
|
+
"""Computes the component vector of the form, with an optional parallel backend."""
|
|
219
|
+
if not parallel:
|
|
220
|
+
self._components = np.zeros(self.domain.dim)
|
|
221
|
+
cx = np.zeros(self.domain.dim)
|
|
222
|
+
for i in range(self.domain.dim):
|
|
223
|
+
cx[i] = 1.0
|
|
224
|
+
x = self.domain.from_components(cx)
|
|
225
|
+
self._components[i] = mapping(x)
|
|
226
|
+
cx[i] = 0.0
|
|
227
|
+
else:
|
|
228
|
+
|
|
229
|
+
def compute_one_component(i: int) -> float:
|
|
230
|
+
"""
|
|
231
|
+
Computes a single component for a given basis vector index.
|
|
232
|
+
This function is sent to each parallel worker.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# cx = np.zeros(self.domain.dim)
|
|
236
|
+
# cx[i] = 1.0
|
|
237
|
+
# x = self.domain.from_components(cx)
|
|
238
|
+
x = self.domain.basis_vector(i)
|
|
239
|
+
return mapping(x)
|
|
240
|
+
|
|
241
|
+
# Run the helper function in parallel for each dimension
|
|
242
|
+
results = Parallel(n_jobs=n_jobs)(
|
|
243
|
+
delayed(compute_one_component)(i) for i in range(self.domain.dim)
|
|
244
|
+
)
|
|
245
|
+
self._components = np.array(results)
|
|
246
|
+
|
|
247
|
+
def _mapping_impl(self, x: Vector) -> float:
|
|
248
|
+
"""
|
|
249
|
+
Maps a vector to its scalar value.
|
|
250
|
+
"""
|
|
251
|
+
return np.dot(self.components, self.domain.to_components(x))
|
|
252
|
+
|
|
253
|
+
def _gradient_impl(self, _: Vector) -> Vector:
|
|
254
|
+
"""
|
|
255
|
+
Computes the gradient of the form at a point.
|
|
256
|
+
"""
|
|
257
|
+
return self.domain.from_dual(self)
|
|
258
|
+
|
|
259
|
+
def _hessian_impl(self, _: Vector) -> LinearOperator:
|
|
260
|
+
"""
|
|
261
|
+
Computes the Hessian of the form at a point.
|
|
262
|
+
"""
|
|
263
|
+
return self.domain.zero_operator()
|