pygeoinf 1.2.7__py3-none-any.whl → 1.2.8__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 +9 -1
- pygeoinf/gaussian_measure.py +24 -35
- pygeoinf/linear_operators.py +659 -173
- pygeoinf/linear_solvers.py +39 -7
- pygeoinf/random_matrix.py +114 -0
- {pygeoinf-1.2.7.dist-info → pygeoinf-1.2.8.dist-info}/METADATA +8 -15
- {pygeoinf-1.2.7.dist-info → pygeoinf-1.2.8.dist-info}/RECORD +9 -9
- {pygeoinf-1.2.7.dist-info → pygeoinf-1.2.8.dist-info}/LICENSE +0 -0
- {pygeoinf-1.2.7.dist-info → pygeoinf-1.2.8.dist-info}/WHEEL +0 -0
pygeoinf/linear_operators.py
CHANGED
|
@@ -23,9 +23,11 @@ from typing import Callable, List, Optional, Any, Union, Tuple, TYPE_CHECKING, D
|
|
|
23
23
|
from collections import defaultdict
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
|
+
import scipy.sparse as sp
|
|
26
27
|
from scipy.sparse.linalg import LinearOperator as ScipyLinOp
|
|
27
28
|
from scipy.sparse import diags
|
|
28
29
|
|
|
30
|
+
|
|
29
31
|
from joblib import Parallel, delayed
|
|
30
32
|
|
|
31
33
|
# from .operators import Operator
|
|
@@ -53,8 +55,8 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
53
55
|
|
|
54
56
|
This class represents a linear map `L(x) = Ax` and provides rich
|
|
55
57
|
functionality for linear algebraic operations. It specializes
|
|
56
|
-
`NonLinearOperator`,
|
|
57
|
-
itself.
|
|
58
|
+
`NonLinearOperator`, with the derivative mapping taking the
|
|
59
|
+
required form (i.e., the derivative is just the operator itself).
|
|
58
60
|
|
|
59
61
|
Key features include operator algebra (`@`, `+`, `*`), automatic
|
|
60
62
|
derivation of adjoint (`.adjoint`) and dual (`.dual`) operators, and
|
|
@@ -71,7 +73,6 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
71
73
|
*,
|
|
72
74
|
dual_mapping: Optional[Callable[[Any], Any]] = None,
|
|
73
75
|
adjoint_mapping: Optional[Callable[[Any], Any]] = None,
|
|
74
|
-
thread_safe: bool = False,
|
|
75
76
|
dual_base: Optional[LinearOperator] = None,
|
|
76
77
|
adjoint_base: Optional[LinearOperator] = None,
|
|
77
78
|
) -> None:
|
|
@@ -84,9 +85,14 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
84
85
|
mapping (callable): The function defining the linear mapping.
|
|
85
86
|
dual_mapping (callable, optional): The action of the dual operator.
|
|
86
87
|
adjoint_mapping (callable, optional): The action of the adjoint.
|
|
87
|
-
thread_safe (bool, optional): True if the mapping is thread-safe.
|
|
88
88
|
dual_base (LinearOperator, optional): Internal use for duals.
|
|
89
89
|
adjoint_base (LinearOperator, optional): Internal use for adjoints.
|
|
90
|
+
|
|
91
|
+
Notes:
|
|
92
|
+
If neither the dual or adjoint mappings are provided, an they are
|
|
93
|
+
deduced internally using a correction but very inefficient method.
|
|
94
|
+
In general this functionality should not be relied on other than
|
|
95
|
+
for operators between low-dimensional spaces.
|
|
90
96
|
"""
|
|
91
97
|
super().__init__(
|
|
92
98
|
domain, codomain, self._mapping_impl, derivative=self._derivative_impl
|
|
@@ -94,7 +100,6 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
94
100
|
self._mapping = mapping
|
|
95
101
|
self._dual_base: Optional[LinearOperator] = dual_base
|
|
96
102
|
self._adjoint_base: Optional[LinearOperator] = adjoint_base
|
|
97
|
-
self._thread_safe: bool = thread_safe
|
|
98
103
|
self.__adjoint_mapping: Callable[[Any], Any]
|
|
99
104
|
self.__dual_mapping: Callable[[Any], Any]
|
|
100
105
|
|
|
@@ -253,83 +258,107 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
253
258
|
def from_matrix(
|
|
254
259
|
domain: HilbertSpace,
|
|
255
260
|
codomain: HilbertSpace,
|
|
256
|
-
matrix: Union[np.ndarray, ScipyLinOp],
|
|
261
|
+
matrix: Union[np.ndarray, sp.sparray, ScipyLinOp],
|
|
257
262
|
/,
|
|
258
263
|
*,
|
|
259
264
|
galerkin: bool = False,
|
|
260
|
-
) ->
|
|
265
|
+
) -> MatrixLinearOperator:
|
|
261
266
|
"""
|
|
262
|
-
Creates
|
|
267
|
+
Creates the most appropriate LinearOperator from a matrix representation.
|
|
263
268
|
|
|
264
|
-
This factory
|
|
265
|
-
|
|
269
|
+
This factory method acts as a dispatcher, inspecting the type of the
|
|
270
|
+
input matrix and returning the most specialized and optimized operator
|
|
271
|
+
subclass (e.g., Dense, Sparse, or DiagonalSparse). It also handles
|
|
272
|
+
matrix-free `scipy.sparse.linalg.LinearOperator` objects.
|
|
266
273
|
|
|
267
274
|
Args:
|
|
268
275
|
domain: The operator's domain space.
|
|
269
276
|
codomain: The operator's codomain space.
|
|
270
|
-
matrix: The matrix representation (NumPy
|
|
271
|
-
|
|
272
|
-
galerkin: If `True`, the matrix is interpreted in
|
|
273
|
-
or Galerkin representation (`M_ij = <basis_j, A(basis_i)>`),
|
|
274
|
-
which maps a vector's components to the components of its
|
|
275
|
-
*dual*. This is crucial as it ensures a self-adjoint
|
|
276
|
-
operator is represented by a symmetric matrix. If `False`
|
|
277
|
-
(default), it's a standard component-to-component map.
|
|
277
|
+
matrix: The matrix representation (NumPy ndarray, SciPy sparray,
|
|
278
|
+
or SciPy LinearOperator).
|
|
279
|
+
galerkin: If `True`, the matrix is interpreted in Galerkin form.
|
|
278
280
|
|
|
279
281
|
Returns:
|
|
280
|
-
|
|
282
|
+
An instance of the most appropriate MatrixLinearOperator subclass.
|
|
281
283
|
"""
|
|
284
|
+
# The order of these checks is important: from most specific to most general.
|
|
282
285
|
|
|
283
|
-
|
|
286
|
+
# 1. Check for the most specific diagonal-sparse format
|
|
287
|
+
if isinstance(matrix, sp.dia_array):
|
|
288
|
+
diagonals_tuple = (matrix.data, matrix.offsets)
|
|
289
|
+
return DiagonalSparseMatrixLinearOperator(
|
|
290
|
+
domain, codomain, diagonals_tuple, galerkin=galerkin
|
|
291
|
+
)
|
|
284
292
|
|
|
285
|
-
|
|
293
|
+
# 2. Check for any other modern sparse format
|
|
294
|
+
elif isinstance(matrix, sp.sparray):
|
|
295
|
+
return SparseMatrixLinearOperator(
|
|
296
|
+
domain, codomain, matrix, galerkin=galerkin
|
|
297
|
+
)
|
|
286
298
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
299
|
+
# 3. Check for a dense NumPy array
|
|
300
|
+
elif isinstance(matrix, np.ndarray):
|
|
301
|
+
return DenseMatrixLinearOperator(
|
|
302
|
+
domain, codomain, matrix, galerkin=galerkin
|
|
303
|
+
)
|
|
292
304
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return domain.from_dual(xp)
|
|
305
|
+
# 4. Check for a matrix-free SciPy LinearOperator
|
|
306
|
+
elif isinstance(matrix, ScipyLinOp):
|
|
307
|
+
# This is matrix-free, so the general MatrixLinearOperator is the correct wrapper.
|
|
308
|
+
return MatrixLinearOperator(domain, codomain, matrix, galerkin=galerkin)
|
|
298
309
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
# 5. Handle legacy sparse matrix formats (optional but robust)
|
|
311
|
+
elif sp.issparse(matrix):
|
|
312
|
+
modern_array = sp.csr_array(matrix)
|
|
313
|
+
return SparseMatrixLinearOperator(
|
|
314
|
+
domain, codomain, modern_array, galerkin=galerkin
|
|
304
315
|
)
|
|
305
316
|
|
|
317
|
+
# 6. Raise an error for unsupported types
|
|
306
318
|
else:
|
|
307
|
-
|
|
308
|
-
def mapping(x: Any) -> Any:
|
|
309
|
-
cx = domain.to_components(x)
|
|
310
|
-
cy = matrix @ cx
|
|
311
|
-
return codomain.from_components(cy)
|
|
312
|
-
|
|
313
|
-
def dual_mapping(yp: Any) -> Any:
|
|
314
|
-
cyp = codomain.dual.to_components(yp)
|
|
315
|
-
cxp = matrix.T @ cyp
|
|
316
|
-
return domain.dual.from_components(cxp)
|
|
317
|
-
|
|
318
|
-
return LinearOperator(domain, codomain, mapping, dual_mapping=dual_mapping)
|
|
319
|
+
raise TypeError(f"Unsupported matrix type: {type(matrix)}")
|
|
319
320
|
|
|
320
321
|
@staticmethod
|
|
321
322
|
def self_adjoint_from_matrix(
|
|
322
|
-
domain: HilbertSpace,
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
domain: HilbertSpace,
|
|
324
|
+
matrix: Union[np.ndarray, sp.sparray, ScipyLinOp],
|
|
325
|
+
) -> MatrixLinearOperator:
|
|
326
|
+
"""
|
|
327
|
+
Creates the most appropriate self-adjoint LinearOperator from a matrix.
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
cyp = matrix @ cx
|
|
329
|
-
yp = domain.dual.from_components(cyp)
|
|
330
|
-
return domain.from_dual(yp)
|
|
329
|
+
This factory acts as a dispatcher, returning the most specialized
|
|
330
|
+
subclass for the given matrix type (e.g., Dense, Sparse).
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
It ALWAYS assumes the provided matrix is the **Galerkin** representation
|
|
333
|
+
of the operator. The user is responsible for ensuring the input matrix
|
|
334
|
+
is symmetric (or self-adjoint for ScipyLinOp).
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
domain: The operator's domain and codomain space.
|
|
338
|
+
matrix: The symmetric matrix representation.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
An instance of the most appropriate MatrixLinearOperator subclass.
|
|
342
|
+
"""
|
|
343
|
+
# Dispatch to the appropriate subclass, always with galerkin=True
|
|
344
|
+
if isinstance(matrix, sp.dia_array):
|
|
345
|
+
diagonals_tuple = (matrix.data, matrix.offsets)
|
|
346
|
+
return DiagonalSparseMatrixLinearOperator(
|
|
347
|
+
domain, domain, diagonals_tuple, galerkin=True
|
|
348
|
+
)
|
|
349
|
+
elif isinstance(matrix, sp.sparray):
|
|
350
|
+
return SparseMatrixLinearOperator(domain, domain, matrix, galerkin=True)
|
|
351
|
+
elif isinstance(matrix, np.ndarray):
|
|
352
|
+
return DenseMatrixLinearOperator(domain, domain, matrix, galerkin=True)
|
|
353
|
+
elif isinstance(matrix, ScipyLinOp):
|
|
354
|
+
return MatrixLinearOperator(domain, domain, matrix, galerkin=True)
|
|
355
|
+
elif sp.issparse(matrix):
|
|
356
|
+
modern_array = sp.csr_array(matrix)
|
|
357
|
+
return SparseMatrixLinearOperator(
|
|
358
|
+
domain, domain, modern_array, galerkin=True
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
raise TypeError(f"Unsupported matrix type: {type(matrix)}")
|
|
333
362
|
|
|
334
363
|
@staticmethod
|
|
335
364
|
def from_tensor_product(
|
|
@@ -417,11 +446,6 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
417
446
|
else:
|
|
418
447
|
return self._adjoint_base
|
|
419
448
|
|
|
420
|
-
@property
|
|
421
|
-
def thread_safe(self) -> bool:
|
|
422
|
-
"""True if the operator's mapping is thread-safe."""
|
|
423
|
-
return self._thread_safe
|
|
424
|
-
|
|
425
449
|
def matrix(
|
|
426
450
|
self,
|
|
427
451
|
/,
|
|
@@ -483,22 +507,34 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
483
507
|
return self.domain.dual.to_components(xp)
|
|
484
508
|
|
|
485
509
|
def matmat(xmat: np.ndarray) -> np.ndarray:
|
|
486
|
-
|
|
487
|
-
assert
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
510
|
+
_n, k = xmat.shape
|
|
511
|
+
assert _n == self.domain.dim
|
|
512
|
+
|
|
513
|
+
if not parallel:
|
|
514
|
+
ymat = np.zeros((self.codomain.dim, k))
|
|
515
|
+
for j in range(k):
|
|
516
|
+
ymat[:, j] = matvec(xmat[:, j])
|
|
517
|
+
return ymat
|
|
518
|
+
else:
|
|
519
|
+
result_cols = Parallel(n_jobs=n_jobs)(
|
|
520
|
+
delayed(matvec)(xmat[:, j]) for j in range(k)
|
|
521
|
+
)
|
|
522
|
+
return np.column_stack(result_cols)
|
|
493
523
|
|
|
494
524
|
def rmatmat(ymat: np.ndarray) -> np.ndarray:
|
|
495
|
-
|
|
496
|
-
assert
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
525
|
+
_m, k = ymat.shape
|
|
526
|
+
assert _m == self.codomain.dim
|
|
527
|
+
|
|
528
|
+
if not parallel:
|
|
529
|
+
xmat = np.zeros((self.domain.dim, k))
|
|
530
|
+
for j in range(k):
|
|
531
|
+
xmat[:, j] = rmatvec(ymat[:, j])
|
|
532
|
+
return xmat
|
|
533
|
+
else:
|
|
534
|
+
result_cols = Parallel(n_jobs=n_jobs)(
|
|
535
|
+
delayed(rmatvec)(ymat[:, j]) for j in range(k)
|
|
536
|
+
)
|
|
537
|
+
return np.column_stack(result_cols)
|
|
502
538
|
|
|
503
539
|
return ScipyLinOp(
|
|
504
540
|
(self.codomain.dim, self.domain.dim),
|
|
@@ -512,7 +548,7 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
512
548
|
self,
|
|
513
549
|
/,
|
|
514
550
|
*,
|
|
515
|
-
galerkin: bool =
|
|
551
|
+
galerkin: bool = False,
|
|
516
552
|
parallel: bool = False,
|
|
517
553
|
n_jobs: int = -1,
|
|
518
554
|
) -> np.ndarray:
|
|
@@ -554,7 +590,7 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
554
590
|
offsets: List[int],
|
|
555
591
|
/,
|
|
556
592
|
*,
|
|
557
|
-
galerkin: bool =
|
|
593
|
+
galerkin: bool = False,
|
|
558
594
|
parallel: bool = False,
|
|
559
595
|
n_jobs: int = -1,
|
|
560
596
|
) -> Tuple[np.ndarray, List[int]]:
|
|
@@ -639,7 +675,11 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
639
675
|
block_size: int = 10,
|
|
640
676
|
parallel: bool = False,
|
|
641
677
|
n_jobs: int = -1,
|
|
642
|
-
) -> Tuple[
|
|
678
|
+
) -> Tuple[
|
|
679
|
+
DenseMatrixLinearOperator,
|
|
680
|
+
DiagonalSparseMatrixLinearOperator,
|
|
681
|
+
DenseMatrixLinearOperator,
|
|
682
|
+
]:
|
|
643
683
|
"""
|
|
644
684
|
Computes an approximate SVD using a randomized algorithm.
|
|
645
685
|
|
|
@@ -662,9 +702,9 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
662
702
|
n_jobs: Number of jobs for parallelism.
|
|
663
703
|
|
|
664
704
|
Returns:
|
|
665
|
-
left (
|
|
666
|
-
singular_values (
|
|
667
|
-
right (
|
|
705
|
+
left (DenseMatrixLinearOperator): The left singular vector matrix.
|
|
706
|
+
singular_values (DiagonalSparseMatrixLinearOperator): The singular values.
|
|
707
|
+
right (DenseMatrixLinearOperator): The right singular vector matrix.
|
|
668
708
|
|
|
669
709
|
Notes:
|
|
670
710
|
The right factor is in transposed form. This means the original
|
|
@@ -694,7 +734,9 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
694
734
|
)
|
|
695
735
|
|
|
696
736
|
euclidean = EuclideanSpace(qr_factor.shape[1])
|
|
697
|
-
diagonal =
|
|
737
|
+
diagonal = DiagonalSparseMatrixLinearOperator.from_diagonal_values(
|
|
738
|
+
euclidean, euclidean, singular_values
|
|
739
|
+
)
|
|
698
740
|
|
|
699
741
|
if galerkin:
|
|
700
742
|
right = LinearOperator.from_matrix(
|
|
@@ -725,7 +767,7 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
725
767
|
block_size: int = 10,
|
|
726
768
|
parallel: bool = False,
|
|
727
769
|
n_jobs: int = -1,
|
|
728
|
-
) -> Tuple[
|
|
770
|
+
) -> Tuple[DenseMatrixLinearOperator, DiagonalSparseMatrixLinearOperator]:
|
|
729
771
|
"""
|
|
730
772
|
Computes an approximate eigen-decomposition using a randomized algorithm.
|
|
731
773
|
|
|
@@ -747,8 +789,8 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
747
789
|
n_jobs: Number of jobs for parallelism.
|
|
748
790
|
|
|
749
791
|
Returns:
|
|
750
|
-
expansion (
|
|
751
|
-
eigenvaluevalues (
|
|
792
|
+
expansion (DenseMatrixLinearOperator): Mapping from coefficients in eigen-basis to vectors.
|
|
793
|
+
eigenvaluevalues (DiagonalSparseMatrixLinearOperator): The eigenvalues values.
|
|
752
794
|
|
|
753
795
|
"""
|
|
754
796
|
from .hilbert_space import EuclideanSpace
|
|
@@ -772,7 +814,9 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
772
814
|
|
|
773
815
|
eigenvectors, eigenvalues = rm_eig(matrix, qr_factor)
|
|
774
816
|
euclidean = EuclideanSpace(qr_factor.shape[1])
|
|
775
|
-
diagonal =
|
|
817
|
+
diagonal = DiagonalSparseMatrixLinearOperator.from_diagonal_values(
|
|
818
|
+
euclidean, euclidean, eigenvalues
|
|
819
|
+
)
|
|
776
820
|
|
|
777
821
|
expansion = LinearOperator.from_matrix(
|
|
778
822
|
euclidean, self.domain, eigenvectors, galerkin=True
|
|
@@ -792,7 +836,7 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
792
836
|
block_size: int = 10,
|
|
793
837
|
parallel: bool = False,
|
|
794
838
|
n_jobs: int = -1,
|
|
795
|
-
) ->
|
|
839
|
+
) -> DenseMatrixLinearOperator:
|
|
796
840
|
"""
|
|
797
841
|
Computes an approximate Cholesky decomposition for a positive-definite
|
|
798
842
|
self-adjoint operator using a randomized algorithm.
|
|
@@ -815,7 +859,7 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
815
859
|
n_jobs: Number of jobs for parallelism.
|
|
816
860
|
|
|
817
861
|
Returns:
|
|
818
|
-
factor (
|
|
862
|
+
factor (DenseMatrixLinearOperator): A linear operator from a Euclidean space
|
|
819
863
|
into the domain of the operator.
|
|
820
864
|
|
|
821
865
|
Notes:
|
|
@@ -1037,40 +1081,81 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
|
1037
1081
|
return self.matrix(dense=True).__str__()
|
|
1038
1082
|
|
|
1039
1083
|
|
|
1040
|
-
class
|
|
1041
|
-
"""
|
|
1084
|
+
class MatrixLinearOperator(LinearOperator):
|
|
1085
|
+
"""
|
|
1086
|
+
A sub-class of LinearOperator for which the operator's action is
|
|
1087
|
+
defined internally through its matrix representation.
|
|
1042
1088
|
|
|
1043
|
-
This
|
|
1044
|
-
|
|
1045
|
-
direct computation of operator functions like its inverse (`.inverse`) or
|
|
1046
|
-
square root (`.sqrt`).
|
|
1089
|
+
This matrix can be either a dense numpy matrix or a
|
|
1090
|
+
scipy LinearOperator.
|
|
1047
1091
|
"""
|
|
1048
1092
|
|
|
1049
1093
|
def __init__(
|
|
1050
1094
|
self,
|
|
1051
1095
|
domain: HilbertSpace,
|
|
1052
1096
|
codomain: HilbertSpace,
|
|
1053
|
-
|
|
1097
|
+
matrix: Union[np.ndarray, ScipyLinOp],
|
|
1054
1098
|
/,
|
|
1055
|
-
|
|
1099
|
+
*,
|
|
1100
|
+
galerkin=False,
|
|
1101
|
+
):
|
|
1056
1102
|
"""
|
|
1057
|
-
|
|
1058
|
-
|
|
1103
|
+
Args:
|
|
1104
|
+
domain: The domain of the operator.
|
|
1105
|
+
codomain: The codomain of the operator.
|
|
1106
|
+
matrix: matrix representation of the linear operator in either standard
|
|
1107
|
+
or Galerkin form.
|
|
1108
|
+
galerkin: If True, galerkin representation used. Default is false.
|
|
1059
1109
|
"""
|
|
1110
|
+
assert matrix.shape == (codomain.dim, domain.dim)
|
|
1060
1111
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
self.
|
|
1112
|
+
self._matrix = matrix
|
|
1113
|
+
self._is_dense = isinstance(matrix, np.ndarray)
|
|
1114
|
+
self._galerkin = galerkin
|
|
1064
1115
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1116
|
+
if galerkin:
|
|
1117
|
+
|
|
1118
|
+
def mapping(x: Any) -> Any:
|
|
1119
|
+
cx = domain.to_components(x)
|
|
1120
|
+
cyp = matrix @ cx
|
|
1121
|
+
yp = codomain.dual.from_components(cyp)
|
|
1122
|
+
return codomain.from_dual(yp)
|
|
1123
|
+
|
|
1124
|
+
def adjoint_mapping(y: Any) -> Any:
|
|
1125
|
+
cy = codomain.to_components(y)
|
|
1126
|
+
cxp = matrix.T @ cy
|
|
1127
|
+
xp = domain.dual.from_components(cxp)
|
|
1128
|
+
return domain.from_dual(xp)
|
|
1129
|
+
|
|
1130
|
+
super().__init__(domain, codomain, mapping, adjoint_mapping=adjoint_mapping)
|
|
1131
|
+
|
|
1132
|
+
else:
|
|
1133
|
+
|
|
1134
|
+
def mapping(x: Any) -> Any:
|
|
1135
|
+
cx = domain.to_components(x)
|
|
1136
|
+
cy = matrix @ cx
|
|
1137
|
+
return codomain.from_components(cy)
|
|
1138
|
+
|
|
1139
|
+
def dual_mapping(yp: Any) -> Any:
|
|
1140
|
+
cyp = codomain.dual.to_components(yp)
|
|
1141
|
+
cxp = matrix.T @ cyp
|
|
1142
|
+
return domain.dual.from_components(cxp)
|
|
1143
|
+
|
|
1144
|
+
super().__init__(domain, codomain, mapping, dual_mapping=dual_mapping)
|
|
1145
|
+
|
|
1146
|
+
@property
|
|
1147
|
+
def is_dense(self) -> bool:
|
|
1148
|
+
"""
|
|
1149
|
+
Returns True if the matrix representation is stored internally in dense form.
|
|
1150
|
+
"""
|
|
1151
|
+
return self._is_dense
|
|
1152
|
+
|
|
1153
|
+
@property
|
|
1154
|
+
def is_galerkin(self) -> bool:
|
|
1155
|
+
"""
|
|
1156
|
+
Returns True if the matrix representation is stored in Galerkin form.
|
|
1157
|
+
"""
|
|
1158
|
+
return self._galerkin
|
|
1074
1159
|
|
|
1075
1160
|
def _compute_dense_matrix(
|
|
1076
1161
|
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
@@ -1078,48 +1163,26 @@ class DiagonalLinearOperator(LinearOperator):
|
|
|
1078
1163
|
"""
|
|
1079
1164
|
Overloaded method to efficiently compute the dense matrix.
|
|
1080
1165
|
"""
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
return
|
|
1166
|
+
|
|
1167
|
+
if galerkin == self.is_galerkin and self.is_dense:
|
|
1168
|
+
return self._matrix
|
|
1084
1169
|
else:
|
|
1085
|
-
# The user wants the standard matrix, which may differ from the
|
|
1086
|
-
# diagonal Galerkin matrix in non-Euclidean spaces. Fall back
|
|
1087
|
-
# to the safe, general method for this conversion.
|
|
1088
1170
|
return super()._compute_dense_matrix(galerkin, parallel, n_jobs)
|
|
1089
1171
|
|
|
1090
|
-
@property
|
|
1091
|
-
def diagonal_values(self) -> np.ndarray:
|
|
1092
|
-
"""The diagonal entries of the operator's Galerkin matrix."""
|
|
1093
|
-
return self._diagonal_values
|
|
1094
|
-
|
|
1095
|
-
def function(self, f: Callable[[float], float]) -> "DiagonalLinearOperator":
|
|
1096
|
-
"""Applies a function to the operator via functional calculus."""
|
|
1097
|
-
new_diagonal_values = np.array([f(x) for x in self.diagonal_values])
|
|
1098
|
-
return DiagonalLinearOperator(self.domain, self.codomain, new_diagonal_values)
|
|
1099
|
-
|
|
1100
|
-
@property
|
|
1101
|
-
def inverse(self) -> "DiagonalLinearOperator":
|
|
1102
|
-
"""The inverse of the operator, computed via functional calculus."""
|
|
1103
|
-
assert all(val != 0 for val in self.diagonal_values)
|
|
1104
|
-
return self.function(lambda x: 1 / x)
|
|
1105
|
-
|
|
1106
|
-
@property
|
|
1107
|
-
def sqrt(self) -> "DiagonalLinearOperator":
|
|
1108
|
-
"""The square root of the operator, computed via functional calculus."""
|
|
1109
|
-
assert all(val >= 0 for val in self._diagonal_values)
|
|
1110
|
-
return self.function(np.sqrt)
|
|
1111
|
-
|
|
1112
1172
|
def extract_diagonal(
|
|
1113
1173
|
self,
|
|
1114
1174
|
/,
|
|
1115
1175
|
*,
|
|
1116
|
-
galerkin: bool =
|
|
1176
|
+
galerkin: bool = False,
|
|
1117
1177
|
parallel: bool = False,
|
|
1118
1178
|
n_jobs: int = -1,
|
|
1119
1179
|
) -> np.ndarray:
|
|
1120
|
-
"""
|
|
1121
|
-
|
|
1122
|
-
|
|
1180
|
+
"""
|
|
1181
|
+
Overload for efficiency.
|
|
1182
|
+
"""
|
|
1183
|
+
|
|
1184
|
+
if galerkin == self.is_galerkin and self.is_dense:
|
|
1185
|
+
return self._matrix.diagonal()
|
|
1123
1186
|
else:
|
|
1124
1187
|
return super().extract_diagonal(
|
|
1125
1188
|
galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
@@ -1130,23 +1193,460 @@ class DiagonalLinearOperator(LinearOperator):
|
|
|
1130
1193
|
offsets: List[int],
|
|
1131
1194
|
/,
|
|
1132
1195
|
*,
|
|
1133
|
-
galerkin: bool =
|
|
1196
|
+
galerkin: bool = False,
|
|
1134
1197
|
parallel: bool = False,
|
|
1135
1198
|
n_jobs: int = -1,
|
|
1136
1199
|
) -> Tuple[np.ndarray, List[int]]:
|
|
1137
|
-
"""
|
|
1138
|
-
|
|
1200
|
+
"""
|
|
1201
|
+
Overrides the base method for efficiency by extracting diagonals directly
|
|
1202
|
+
from the stored dense matrix when possible.
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
if self.is_dense and galerkin == self.is_galerkin:
|
|
1139
1206
|
dim = self.domain.dim
|
|
1207
|
+
|
|
1140
1208
|
diagonals_array = np.zeros((len(offsets), dim))
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1209
|
+
|
|
1210
|
+
for i, k in enumerate(offsets):
|
|
1211
|
+
diag_k = np.diag(self._matrix, k=k)
|
|
1212
|
+
|
|
1213
|
+
if k >= 0:
|
|
1214
|
+
diagonals_array[i, k : k + len(diag_k)] = diag_k
|
|
1215
|
+
else:
|
|
1216
|
+
diagonals_array[i, : len(diag_k)] = diag_k
|
|
1217
|
+
|
|
1144
1218
|
return diagonals_array, offsets
|
|
1219
|
+
|
|
1220
|
+
else:
|
|
1221
|
+
return super().extract_diagonals(
|
|
1222
|
+
offsets, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
class DenseMatrixLinearOperator(MatrixLinearOperator):
|
|
1227
|
+
"""
|
|
1228
|
+
A specialisation of the MatrixLinearOperator class to instances where
|
|
1229
|
+
the matrix representation is always provided as a numpy array.
|
|
1230
|
+
|
|
1231
|
+
This is a class provides some additional methods for component-wise access.
|
|
1232
|
+
"""
|
|
1233
|
+
|
|
1234
|
+
def __init__(
|
|
1235
|
+
self,
|
|
1236
|
+
domain: HilbertSpace,
|
|
1237
|
+
codomain: HilbertSpace,
|
|
1238
|
+
matrix: np.ndarray,
|
|
1239
|
+
/,
|
|
1240
|
+
*,
|
|
1241
|
+
galerkin=False,
|
|
1242
|
+
):
|
|
1243
|
+
"""
|
|
1244
|
+
domain: The domain of the operator.
|
|
1245
|
+
codomain: The codomain of the operator.
|
|
1246
|
+
matrix: matrix representation of the linear operator in either standard
|
|
1247
|
+
or Galerkin form.
|
|
1248
|
+
galerkin: If True, galerkin representation used. Default is false.
|
|
1249
|
+
"""
|
|
1250
|
+
|
|
1251
|
+
if not isinstance(matrix, np.ndarray):
|
|
1252
|
+
raise ValueError("Matrix must be input in dense form.")
|
|
1253
|
+
|
|
1254
|
+
super().__init__(domain, codomain, matrix, galerkin=galerkin)
|
|
1255
|
+
|
|
1256
|
+
@staticmethod
|
|
1257
|
+
def from_linear_operator(
|
|
1258
|
+
operator: LinearOperator,
|
|
1259
|
+
/,
|
|
1260
|
+
*,
|
|
1261
|
+
galerkin: bool = False,
|
|
1262
|
+
parallel: bool = False,
|
|
1263
|
+
n_jobs: int = -1,
|
|
1264
|
+
) -> DenseMatrixLinearOperator:
|
|
1265
|
+
"""
|
|
1266
|
+
Converts a LinearOperator into a DenseMatrixLinearOperator by forming its dense matrix representation.
|
|
1267
|
+
|
|
1268
|
+
Args:
|
|
1269
|
+
operator: The operator to be converted.
|
|
1270
|
+
galerkin: If True, the Galerkin representation is used. Default is False.
|
|
1271
|
+
parallel: If True, dense matrix calculation is done in parallel. Default is False.
|
|
1272
|
+
n_jobs: Number of jobs used for parallel calculations. Default is False.
|
|
1273
|
+
"""
|
|
1274
|
+
|
|
1275
|
+
if isinstance(operator, DenseMatrixLinearOperator):
|
|
1276
|
+
return operator
|
|
1277
|
+
|
|
1278
|
+
domain = operator.domain
|
|
1279
|
+
codomain = operator.codomain
|
|
1280
|
+
|
|
1281
|
+
matrix = operator.matrix(
|
|
1282
|
+
dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
return DenseMatrixLinearOperator(domain, codomain, matrix, galerkin=galerkin)
|
|
1286
|
+
|
|
1287
|
+
def __getitem__(self, key: tuple[int, int] | int | slice) -> float | np.ndarray:
|
|
1288
|
+
"""
|
|
1289
|
+
Provides direct, component-wise access to the underlying matrix.
|
|
1290
|
+
|
|
1291
|
+
This allows for intuitive slicing and indexing, like `op[i, j]` or `op[0, :]`.
|
|
1292
|
+
Note: The access is on the stored matrix, which may be in either
|
|
1293
|
+
standard or Galerkin form depending on how the operator was initialized.
|
|
1294
|
+
"""
|
|
1295
|
+
return self._matrix[key]
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
class SparseMatrixLinearOperator(MatrixLinearOperator):
|
|
1299
|
+
"""
|
|
1300
|
+
A specialization for operators represented by a modern SciPy sparse array.
|
|
1301
|
+
|
|
1302
|
+
This class requires a `scipy.sparse.sparray` object (e.g., csr_array)
|
|
1303
|
+
and provides optimized methods that delegate to efficient SciPy routines.
|
|
1304
|
+
|
|
1305
|
+
Upon initialization, the internal array is converted to the CSR
|
|
1306
|
+
(Compressed Sparse Row) format to ensure consistently fast matrix-vector
|
|
1307
|
+
products and row-slicing operations.
|
|
1308
|
+
"""
|
|
1309
|
+
|
|
1310
|
+
def __init__(
|
|
1311
|
+
self,
|
|
1312
|
+
domain: HilbertSpace,
|
|
1313
|
+
codomain: HilbertSpace,
|
|
1314
|
+
matrix: sp.sparray,
|
|
1315
|
+
/,
|
|
1316
|
+
*,
|
|
1317
|
+
galerkin: bool = False,
|
|
1318
|
+
):
|
|
1319
|
+
"""
|
|
1320
|
+
Args:
|
|
1321
|
+
domain: The domain of the operator.
|
|
1322
|
+
codomain: The codomain of the operator.
|
|
1323
|
+
matrix: The sparse array representation of the linear operator.
|
|
1324
|
+
Must be a modern sparray object (e.g., csr_array).
|
|
1325
|
+
galerkin: If True, the matrix is in Galerkin form. Defaults to False.
|
|
1326
|
+
"""
|
|
1327
|
+
# Strict check for the modern sparse array type
|
|
1328
|
+
if not isinstance(matrix, sp.sparray):
|
|
1329
|
+
raise TypeError(
|
|
1330
|
+
"Matrix must be a modern SciPy sparray object (e.g., csr_array)."
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
super().__init__(domain, codomain, matrix, galerkin=galerkin)
|
|
1334
|
+
self._matrix = self._matrix.asformat("csr")
|
|
1335
|
+
|
|
1336
|
+
def __getitem__(self, key):
|
|
1337
|
+
"""Provides direct component access using SciPy's sparse indexing."""
|
|
1338
|
+
return self._matrix[key]
|
|
1339
|
+
|
|
1340
|
+
def _compute_dense_matrix(
|
|
1341
|
+
self, galerkin: bool, parallel: bool, n_jobs: int
|
|
1342
|
+
) -> np.ndarray:
|
|
1343
|
+
"""
|
|
1344
|
+
Overrides the base method to efficiently compute the dense matrix.
|
|
1345
|
+
"""
|
|
1346
|
+
# ⚡️ Fast path: Use the highly optimized .toarray() method.
|
|
1347
|
+
if galerkin == self.is_galerkin:
|
|
1348
|
+
return self._matrix.toarray()
|
|
1349
|
+
|
|
1350
|
+
# Fallback path for when a basis conversion is needed.
|
|
1145
1351
|
else:
|
|
1352
|
+
return super()._compute_dense_matrix(galerkin, parallel, n_jobs)
|
|
1353
|
+
|
|
1354
|
+
def extract_diagonal(
|
|
1355
|
+
self,
|
|
1356
|
+
/,
|
|
1357
|
+
*,
|
|
1358
|
+
galerkin: bool = False,
|
|
1359
|
+
parallel: bool = False,
|
|
1360
|
+
n_jobs: int = -1,
|
|
1361
|
+
) -> np.ndarray:
|
|
1362
|
+
"""
|
|
1363
|
+
Overrides the base method to efficiently extract the main diagonal.
|
|
1364
|
+
"""
|
|
1365
|
+
if galerkin == self.is_galerkin:
|
|
1366
|
+
return self._matrix.diagonal(k=0)
|
|
1367
|
+
else:
|
|
1368
|
+
return super().extract_diagonal(
|
|
1369
|
+
galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
def extract_diagonals(
|
|
1373
|
+
self,
|
|
1374
|
+
offsets: List[int],
|
|
1375
|
+
/,
|
|
1376
|
+
*,
|
|
1377
|
+
galerkin: bool = False,
|
|
1378
|
+
parallel: bool = False,
|
|
1379
|
+
n_jobs: int = -1,
|
|
1380
|
+
) -> Tuple[np.ndarray, List[int]]:
|
|
1381
|
+
"""
|
|
1382
|
+
Overrides the base method for efficiency by extracting diagonals
|
|
1383
|
+
directly from the stored sparse array.
|
|
1384
|
+
"""
|
|
1385
|
+
if galerkin != self.is_galerkin:
|
|
1146
1386
|
return super().extract_diagonals(
|
|
1147
1387
|
offsets, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
|
|
1148
1388
|
)
|
|
1149
1389
|
|
|
1390
|
+
dim = self.domain.dim
|
|
1391
|
+
diagonals_array = np.zeros((len(offsets), dim))
|
|
1392
|
+
|
|
1393
|
+
for i, k in enumerate(offsets):
|
|
1394
|
+
# Use the sparse array's fast .diagonal() method
|
|
1395
|
+
diag_k = self._matrix.diagonal(k=k)
|
|
1396
|
+
|
|
1397
|
+
# Place the raw diagonal into the padded output array
|
|
1398
|
+
if k >= 0:
|
|
1399
|
+
diagonals_array[i, k : k + len(diag_k)] = diag_k
|
|
1400
|
+
else:
|
|
1401
|
+
diagonals_array[i, : len(diag_k)] = diag_k
|
|
1402
|
+
|
|
1403
|
+
return diagonals_array, offsets
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
class DiagonalSparseMatrixLinearOperator(SparseMatrixLinearOperator):
|
|
1407
|
+
"""
|
|
1408
|
+
A highly specialized operator for matrices defined purely by a set of
|
|
1409
|
+
non-zero diagonals.
|
|
1410
|
+
|
|
1411
|
+
This class internally stores the operator using a `scipy.sparse.dia_array`
|
|
1412
|
+
for maximum efficiency in storage and matrix-vector products. It provides
|
|
1413
|
+
extremely fast methods for extracting diagonals, as this is its native
|
|
1414
|
+
storage format.
|
|
1415
|
+
|
|
1416
|
+
A key feature of this class is its support for **functional calculus**. It
|
|
1417
|
+
dynamically proxies element-wise mathematical functions (e.g., `.sqrt()`,
|
|
1418
|
+
`.log()`, `abs()`, `**`) to the underlying sparse array. For reasons of
|
|
1419
|
+
mathematical correctness, these operations are restricted to operators that
|
|
1420
|
+
are **strictly diagonal** (i.e., have only a non-zero main diagonal) and
|
|
1421
|
+
will raise a `NotImplementedError` otherwise.
|
|
1422
|
+
|
|
1423
|
+
Aggregation methods that do not return a new operator (e.g., `.sum()`)
|
|
1424
|
+
are not restricted and can be used on any multi-diagonal operator.
|
|
1425
|
+
|
|
1426
|
+
Class Methods
|
|
1427
|
+
-------------
|
|
1428
|
+
from_diagonal_values:
|
|
1429
|
+
Constructs a strictly diagonal operator from a 1D array of values.
|
|
1430
|
+
from_operator:
|
|
1431
|
+
Creates a diagonal approximation of another LinearOperator.
|
|
1432
|
+
|
|
1433
|
+
Properties
|
|
1434
|
+
----------
|
|
1435
|
+
offsets:
|
|
1436
|
+
The array of stored diagonal offsets.
|
|
1437
|
+
is_strictly_diagonal:
|
|
1438
|
+
True if the operator only has a non-zero main diagonal.
|
|
1439
|
+
inverse:
|
|
1440
|
+
The inverse of a strictly diagonal operator.
|
|
1441
|
+
sqrt:
|
|
1442
|
+
The square root of a strictly diagonal operator.
|
|
1443
|
+
"""
|
|
1444
|
+
|
|
1445
|
+
def __init__(
|
|
1446
|
+
self,
|
|
1447
|
+
domain: HilbertSpace,
|
|
1448
|
+
codomain: HilbertSpace,
|
|
1449
|
+
diagonals: Tuple[np.ndarray, List[int]],
|
|
1450
|
+
/,
|
|
1451
|
+
*,
|
|
1452
|
+
galerkin: bool = False,
|
|
1453
|
+
):
|
|
1454
|
+
"""
|
|
1455
|
+
Args:
|
|
1456
|
+
domain: The domain of the operator.
|
|
1457
|
+
codomain: The codomain of the operator.
|
|
1458
|
+
diagonals: A tuple `(data, offsets)` where `data` is a 2D array
|
|
1459
|
+
of diagonal values and `offsets` is a list of their
|
|
1460
|
+
positions. This is the native format for a dia_array.
|
|
1461
|
+
galerkin: If True, the matrix is in Galerkin form. Defaults to False.
|
|
1462
|
+
"""
|
|
1463
|
+
shape = (codomain.dim, domain.dim)
|
|
1464
|
+
dia_array = sp.dia_array(diagonals, shape=shape)
|
|
1465
|
+
|
|
1466
|
+
MatrixLinearOperator.__init__(
|
|
1467
|
+
self, domain, codomain, dia_array, galerkin=galerkin
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
@classmethod
|
|
1471
|
+
def from_operator(
|
|
1472
|
+
cls, operator: LinearOperator, offsets: List[int], /, *, galerkin: bool = True
|
|
1473
|
+
) -> DiagonalSparseMatrixLinearOperator:
|
|
1474
|
+
"""
|
|
1475
|
+
Creates a diagonal approximation of another LinearOperator.
|
|
1476
|
+
|
|
1477
|
+
This factory method works by calling the source operator's
|
|
1478
|
+
`.extract_diagonals()` method and using the result to construct a
|
|
1479
|
+
new, highly efficient DiagonalSparseMatrixLinearOperator.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
operator: The source operator to approximate.
|
|
1483
|
+
offsets: The list of diagonal offsets to extract and keep.
|
|
1484
|
+
galerkin: Specifies which matrix representation to use.
|
|
1485
|
+
|
|
1486
|
+
Returns:
|
|
1487
|
+
A new DiagonalSparseMatrixLinearOperator.
|
|
1488
|
+
"""
|
|
1489
|
+
diagonals_data, extracted_offsets = operator.extract_diagonals(
|
|
1490
|
+
offsets, galerkin=galerkin
|
|
1491
|
+
)
|
|
1492
|
+
return cls(
|
|
1493
|
+
operator.domain,
|
|
1494
|
+
operator.codomain,
|
|
1495
|
+
(diagonals_data, extracted_offsets),
|
|
1496
|
+
galerkin=galerkin,
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
@classmethod
|
|
1500
|
+
def from_diagonal_values(
|
|
1501
|
+
cls,
|
|
1502
|
+
domain: HilbertSpace,
|
|
1503
|
+
codomain: HilbertSpace,
|
|
1504
|
+
diagonal_values: np.ndarray,
|
|
1505
|
+
/,
|
|
1506
|
+
*,
|
|
1507
|
+
galerkin: bool = False,
|
|
1508
|
+
) -> "DiagonalSparseMatrixLinearOperator":
|
|
1509
|
+
"""
|
|
1510
|
+
Constructs a purely diagonal operator from a 1D array of values.
|
|
1511
|
+
|
|
1512
|
+
This provides a convenient way to create an operator with non-zero
|
|
1513
|
+
entries only on its main diagonal (offset k=0).
|
|
1514
|
+
|
|
1515
|
+
Args:
|
|
1516
|
+
domain: The domain of the operator.
|
|
1517
|
+
codomain: The codomain of the operator. Must have the same dimension.
|
|
1518
|
+
diagonal_values: A 1D NumPy array of the values for the main diagonal.
|
|
1519
|
+
galerkin: If True, the operator is in Galerkin form.
|
|
1520
|
+
|
|
1521
|
+
Returns:
|
|
1522
|
+
A new DiagonalSparseMatrixLinearOperator.
|
|
1523
|
+
"""
|
|
1524
|
+
if domain.dim != codomain.dim or domain.dim != len(diagonal_values):
|
|
1525
|
+
raise ValueError(
|
|
1526
|
+
"Domain, codomain, and diagonal_values must all have the same dimension."
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
# Reshape the 1D array of values into the 2D `data` array format
|
|
1530
|
+
diagonals_data = diagonal_values.reshape(1, -1)
|
|
1531
|
+
offsets = [0]
|
|
1532
|
+
|
|
1533
|
+
return cls(domain, codomain, (diagonals_data, offsets), galerkin=galerkin)
|
|
1534
|
+
|
|
1535
|
+
@property
|
|
1536
|
+
def offsets(self) -> np.ndarray:
|
|
1537
|
+
"""Returns the array of stored diagonal offsets."""
|
|
1538
|
+
return self._matrix.offsets
|
|
1539
|
+
|
|
1540
|
+
@property
|
|
1541
|
+
def is_strictly_diagonal(self) -> bool:
|
|
1542
|
+
"""
|
|
1543
|
+
True if the operator only has a non-zero main diagonal (offset=0).
|
|
1544
|
+
"""
|
|
1545
|
+
return len(self.offsets) == 1 and self.offsets[0] == 0
|
|
1546
|
+
|
|
1547
|
+
@property
|
|
1548
|
+
def inverse(self) -> "DiagonalSparseMatrixLinearOperator":
|
|
1549
|
+
"""
|
|
1550
|
+
The inverse of the operator, computed via functional calculus.
|
|
1551
|
+
Requires the operator to be strictly diagonal with no zero entries.
|
|
1552
|
+
"""
|
|
1553
|
+
if not self.is_strictly_diagonal:
|
|
1554
|
+
raise NotImplementedError(
|
|
1555
|
+
"Inverse is only implemented for strictly diagonal operators."
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
if np.any(self._matrix.diagonal(k=0) == 0):
|
|
1559
|
+
raise ValueError("Cannot invert an operator with zeros on the diagonal.")
|
|
1560
|
+
|
|
1561
|
+
return self**-1
|
|
1562
|
+
|
|
1563
|
+
@property
|
|
1564
|
+
def sqrt(self) -> "DiagonalSparseMatrixLinearOperator":
|
|
1565
|
+
"""
|
|
1566
|
+
The square root of the operator, computed via functional calculus.
|
|
1567
|
+
Requires the operator to be strictly diagonal with non-negative entries.
|
|
1568
|
+
"""
|
|
1569
|
+
|
|
1570
|
+
if np.any(self._matrix.data < 0):
|
|
1571
|
+
raise ValueError(
|
|
1572
|
+
"Cannot take the square root of an operator with negative entries."
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
return self.__getattr__("sqrt")()
|
|
1576
|
+
|
|
1577
|
+
def extract_diagonals(
|
|
1578
|
+
self,
|
|
1579
|
+
offsets: List[int],
|
|
1580
|
+
/,
|
|
1581
|
+
*,
|
|
1582
|
+
galerkin: bool = True,
|
|
1583
|
+
# parallel and n_jobs are ignored but kept for signature consistency
|
|
1584
|
+
parallel: bool = False,
|
|
1585
|
+
n_jobs: int = -1,
|
|
1586
|
+
) -> Tuple[np.ndarray, List[int]]:
|
|
1587
|
+
"""
|
|
1588
|
+
Overrides the base method for extreme efficiency.
|
|
1589
|
+
|
|
1590
|
+
This operation is nearly free, as it involves selecting the requested
|
|
1591
|
+
diagonals from the data already stored in the native format.
|
|
1592
|
+
"""
|
|
1593
|
+
if galerkin != self.is_galerkin:
|
|
1594
|
+
return super().extract_diagonals(offsets, galerkin=galerkin)
|
|
1595
|
+
|
|
1596
|
+
# Create a result array and fill it with the requested stored diagonals
|
|
1597
|
+
result_diagonals = np.zeros((len(offsets), self.domain.dim))
|
|
1598
|
+
|
|
1599
|
+
# Create a mapping from stored offset to its data row for quick lookup
|
|
1600
|
+
stored_diagonals = dict(zip(self.offsets, self._matrix.data))
|
|
1601
|
+
|
|
1602
|
+
for i, k in enumerate(offsets):
|
|
1603
|
+
if k in stored_diagonals:
|
|
1604
|
+
result_diagonals[i, :] = stored_diagonals[k]
|
|
1605
|
+
|
|
1606
|
+
return result_diagonals, offsets
|
|
1607
|
+
|
|
1608
|
+
def __getattr__(self, name: str):
|
|
1609
|
+
"""
|
|
1610
|
+
Dynamically proxies method calls to the underlying dia_array.
|
|
1611
|
+
|
|
1612
|
+
For element-wise mathematical functions that return a new operator,
|
|
1613
|
+
this method enforces that the operator must be strictly diagonal.
|
|
1614
|
+
"""
|
|
1615
|
+
attr = getattr(self._matrix, name)
|
|
1616
|
+
|
|
1617
|
+
if callable(attr):
|
|
1618
|
+
|
|
1619
|
+
def wrapper(*args, **kwargs):
|
|
1620
|
+
result = attr(*args, **kwargs)
|
|
1621
|
+
|
|
1622
|
+
if isinstance(result, sp.sparray):
|
|
1623
|
+
if not self.is_strictly_diagonal:
|
|
1624
|
+
raise NotImplementedError(
|
|
1625
|
+
f"Element-wise function '{name}' is only defined for "
|
|
1626
|
+
"strictly diagonal operators."
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
return DiagonalSparseMatrixLinearOperator(
|
|
1630
|
+
self.domain,
|
|
1631
|
+
self.codomain,
|
|
1632
|
+
(result.data, result.offsets),
|
|
1633
|
+
galerkin=self.is_galerkin,
|
|
1634
|
+
)
|
|
1635
|
+
else:
|
|
1636
|
+
return result
|
|
1637
|
+
|
|
1638
|
+
return wrapper
|
|
1639
|
+
else:
|
|
1640
|
+
return attr
|
|
1641
|
+
|
|
1642
|
+
def __abs__(self):
|
|
1643
|
+
"""Explicitly handle the built-in abs() function."""
|
|
1644
|
+
return self.__getattr__("__abs__")()
|
|
1645
|
+
|
|
1646
|
+
def __pow__(self, power):
|
|
1647
|
+
"""Explicitly handle the power operator (**)."""
|
|
1648
|
+
return self.__getattr__("__pow__")(power)
|
|
1649
|
+
|
|
1150
1650
|
|
|
1151
1651
|
class NormalSumOperator(LinearOperator):
|
|
1152
1652
|
"""
|
|
@@ -1169,15 +1669,12 @@ class NormalSumOperator(LinearOperator):
|
|
|
1169
1669
|
B: Optional[LinearOperator] = None,
|
|
1170
1670
|
) -> None:
|
|
1171
1671
|
|
|
1172
|
-
# This operator's domain is the codomain of A.
|
|
1173
1672
|
op_domain = A.codomain
|
|
1174
1673
|
|
|
1175
1674
|
if Q is None:
|
|
1176
|
-
# Q must be an operator on the domain of A.
|
|
1177
1675
|
Q = A.domain.identity_operator()
|
|
1178
1676
|
|
|
1179
1677
|
if B is None:
|
|
1180
|
-
# B must be an operator on the codomain of A.
|
|
1181
1678
|
B = op_domain.zero_operator()
|
|
1182
1679
|
|
|
1183
1680
|
if A.domain != Q.domain:
|
|
@@ -1189,7 +1686,6 @@ class NormalSumOperator(LinearOperator):
|
|
|
1189
1686
|
self._Q = Q
|
|
1190
1687
|
self._B = B
|
|
1191
1688
|
|
|
1192
|
-
# The compositional definition now works with correct dimensions.
|
|
1193
1689
|
composite_op = self._A @ self._Q @ self._A.adjoint + self._B
|
|
1194
1690
|
|
|
1195
1691
|
super().__init__(
|
|
@@ -1207,40 +1703,30 @@ class NormalSumOperator(LinearOperator):
|
|
|
1207
1703
|
implementation leveraging the base class's methods.
|
|
1208
1704
|
"""
|
|
1209
1705
|
if not galerkin:
|
|
1210
|
-
|
|
1211
|
-
return super()._compute_dense_matrix(galerkin, parallel, n_jobs) #
|
|
1706
|
+
return super()._compute_dense_matrix(galerkin, parallel, n_jobs)
|
|
1212
1707
|
|
|
1213
|
-
domain_Y = self._A.codomain
|
|
1214
|
-
dim = self.domain.dim
|
|
1708
|
+
domain_Y = self._A.codomain
|
|
1709
|
+
dim = self.domain.dim
|
|
1215
1710
|
jobs = n_jobs if parallel else 1
|
|
1216
1711
|
|
|
1217
|
-
# Step 1: Get component vectors c(v_j) where v_j = A*(e_j)
|
|
1218
|
-
print("Step 1: Computing components of v_j = A*(e_j)...")
|
|
1219
1712
|
a_star_mat = self._A.adjoint.matrix(
|
|
1220
1713
|
dense=True, galerkin=False, parallel=parallel, n_jobs=n_jobs
|
|
1221
|
-
)
|
|
1222
|
-
|
|
1223
|
-
# Step 2: Reconstruct v_j vectors and compute w_j = Q(v_j)
|
|
1224
|
-
print("Step 2: Computing w_j = Q(v_j)...")
|
|
1225
|
-
v_vectors = [domain_Y.from_components(a_star_mat[:, j]) for j in range(dim)] #
|
|
1226
|
-
w_vectors = Parallel(n_jobs=jobs)(delayed(self._Q)(v_j) for v_j in v_vectors) #
|
|
1714
|
+
)
|
|
1227
1715
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1716
|
+
v_vectors = [domain_Y.from_components(a_star_mat[:, j]) for j in range(dim)]
|
|
1717
|
+
w_vectors = Parallel(n_jobs=jobs)(delayed(self._Q)(v_j) for v_j in v_vectors)
|
|
1230
1718
|
|
|
1231
1719
|
def compute_row(i: int) -> np.ndarray:
|
|
1232
1720
|
"""Computes the i-th row of the inner product matrix."""
|
|
1233
1721
|
v_i = v_vectors[i]
|
|
1234
|
-
return np.array([domain_Y.inner_product(v_i, w_j) for w_j in w_vectors])
|
|
1722
|
+
return np.array([domain_Y.inner_product(v_i, w_j) for w_j in w_vectors])
|
|
1235
1723
|
|
|
1236
1724
|
rows = Parallel(n_jobs=jobs)(delayed(compute_row)(i) for i in range(dim))
|
|
1237
1725
|
m_aqa_mat = np.vstack(rows)
|
|
1238
1726
|
|
|
1239
|
-
# Step 4: Compute B_mat
|
|
1240
|
-
print("Step 4: Computing matrix for B...")
|
|
1241
1727
|
b_mat = self._B.matrix(
|
|
1242
1728
|
dense=True, galerkin=True, parallel=parallel, n_jobs=n_jobs
|
|
1243
|
-
)
|
|
1729
|
+
)
|
|
1244
1730
|
|
|
1245
1731
|
return m_aqa_mat + b_mat
|
|
1246
1732
|
|
|
@@ -1248,7 +1734,7 @@ class NormalSumOperator(LinearOperator):
|
|
|
1248
1734
|
self,
|
|
1249
1735
|
/,
|
|
1250
1736
|
*,
|
|
1251
|
-
galerkin: bool =
|
|
1737
|
+
galerkin: bool = False,
|
|
1252
1738
|
parallel: bool = False,
|
|
1253
1739
|
n_jobs: int = -1,
|
|
1254
1740
|
) -> np.ndarray:
|
|
@@ -1282,7 +1768,7 @@ class NormalSumOperator(LinearOperator):
|
|
|
1282
1768
|
offsets: List[int],
|
|
1283
1769
|
/,
|
|
1284
1770
|
*,
|
|
1285
|
-
galerkin: bool =
|
|
1771
|
+
galerkin: bool = False,
|
|
1286
1772
|
parallel: bool = False,
|
|
1287
1773
|
n_jobs: int = -1,
|
|
1288
1774
|
) -> Tuple[np.ndarray, List[int]]:
|