pygeoinf 1.2.0__py3-none-any.whl → 1.2.2__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.
@@ -1,22 +1,20 @@
1
1
  """
2
- Defines a class hierarchy for operators between Hilbert spaces.
2
+ Provides classes for linear operators between Hilbert spaces.
3
3
 
4
- This module provides the tools for defining and manipulating mappings between
5
- `HilbertSpace` objects. It distinguishes between general non-linear operators
6
- and the more structured linear operators, which are the primary focus and
7
- support a rich algebra.
4
+ This module is the primary tool for defining and manipulating linear mappings
5
+ between `HilbertSpace` objects. It provides a powerful `LinearOperator` class
6
+ that supports a rich algebra and includes numerous factory methods for
7
+ convenient construction from matrices, forms, or tensor products.
8
8
 
9
9
  Key Classes
10
10
  -----------
11
- - `Operator`: A general, potentially non-linear operator defined by a simple
12
- mapping function.
13
11
  - `LinearOperator`: The main workhorse for linear algebra. It represents a
14
- linear map and provides rich functionality, including composition (`@`),
15
- adjoints (`.adjoint`), duals (`.dual`), and matrix representations (`.matrix`).
16
- It includes numerous factory methods for convenient construction.
12
+ linear map `L(x) = Ax` and provides rich functionality, including composition
13
+ (`@`), adjoints (`.adjoint`), duals (`.dual`), and matrix representations
14
+ (`.matrix`).
17
15
  - `DiagonalLinearOperator`: A specialized, efficient implementation for linear
18
- operators that are diagonal in their component representation, which supports
19
- functional calculus.
16
+ operators that are diagonal in their component representation, notable for
17
+ supporting functional calculus (e.g., `.inverse`, `.sqrt`).
20
18
  """
21
19
 
22
20
  from __future__ import annotations
@@ -26,10 +24,11 @@ import numpy as np
26
24
  from scipy.sparse.linalg import LinearOperator as ScipyLinOp
27
25
  from scipy.sparse import diags
28
26
 
27
+ # from .operators import Operator
28
+ from .nonlinear_operators import NonLinearOperator
29
29
 
30
30
  from .random_matrix import (
31
- fixed_rank_random_range,
32
- variable_rank_random_range,
31
+ random_range,
33
32
  random_svd as rm_svd,
34
33
  random_cholesky as rm_chol,
35
34
  random_eig as rm_eig,
@@ -43,94 +42,18 @@ if TYPE_CHECKING:
43
42
  from .linear_forms import LinearForm
44
43
 
45
44
 
46
- def _parallel_scipy_op_col(scipy_op: ScipyLinOp, j: int, domain_dim: int) -> np.ndarray:
47
- """
48
- A top-level helper that applies a scipy.LinearOperator to a basis vector.
49
-
50
- This function is simple and serializable ("picklable").
51
-
52
- Args:
53
- scipy_op: The SciPy LinearOperator wrapper for the matrix action.
54
- j: The index of the basis vector (column) to compute.
55
- domain_dim: The dimension of the domain space.
56
-
57
- Returns:
58
- The j-th column of the dense matrix as a NumPy array.
59
- """
60
- # Create the j-th component basis vector
61
- cx = np.zeros(domain_dim)
62
- cx[j] = 1.0
63
-
64
- # Apply the SciPy wrapper, which handles all necessary conversions
65
- return scipy_op @ cx
45
+ class LinearOperator(NonLinearOperator):
46
+ """A linear operator between two Hilbert spaces.
66
47
 
48
+ This class represents a linear map `L(x) = Ax` and provides rich
49
+ functionality for linear algebraic operations. It specializes
50
+ `NonLinearOperator`, correctly defining its derivative as the operator
51
+ itself.
67
52
 
68
- class Operator:
69
- """
70
- A general, potentially non-linear operator between two Hilbert spaces.
71
- """
72
-
73
- def __init__(
74
- self,
75
- domain: HilbertSpace,
76
- codomain: HilbertSpace,
77
- mapping: Callable[[Any], Any],
78
- ) -> None:
79
- """
80
- Initializes the Operator.
81
-
82
- Args:
83
- domain (HilbertSpace): Domain of the operator.
84
- codomain (HilbertSpace): Codomain of the operator.
85
- mapping (callable): The function defining the mapping from the
86
- domain to the codomain.
87
- """
88
- self._domain: HilbertSpace = domain
89
- self._codomain: HilbertSpace = codomain
90
- self.__mapping: Callable[[Any], Any] = mapping
91
-
92
- @property
93
- def domain(self) -> HilbertSpace:
94
- """The domain of the operator."""
95
- return self._domain
96
-
97
- @property
98
- def codomain(self) -> HilbertSpace:
99
- """The codomain of the operator."""
100
- return self._codomain
101
-
102
- @property
103
- def is_automorphism(self) -> bool:
104
- """True if the operator maps a space into itself."""
105
- return self.domain == self.codomain
106
-
107
- @property
108
- def is_square(self) -> bool:
109
- """True if the operator's domain and codomain have the same dimension."""
110
- return self.domain.dim == self.codomain.dim
111
-
112
- @property
113
- def linear(self) -> bool:
114
- """False for a general operator. Overridden by LinearOperator."""
115
- return False
116
-
117
- def __call__(self, x: Any) -> Any:
118
- """Applies the operator's mapping to a vector."""
119
- return self.__mapping(x)
120
-
121
-
122
- class LinearOperator(Operator):
123
- """
124
- A linear operator between two Hilbert spaces.
125
-
126
- This class is the primary workhorse for linear algebraic operations. An
127
- operator can be defined "on the fly" from a callable mapping. The class
128
- automatically derives the associated `adjoint` and `dual` operators,
129
- which are fundamental for solving linear systems and for optimization.
130
-
131
- It supports a rich algebra, including composition (`@`), addition (`+`),
132
- and scalar multiplication (`*`). Operators can also be represented as
133
- dense or matrix-free (`scipy`) matrices for use with numerical solvers.
53
+ Key features include operator algebra (`@`, `+`, `*`), automatic
54
+ derivation of adjoint (`.adjoint`) and dual (`.dual`) operators, and
55
+ multiple matrix representations (`.matrix()`) for use with numerical
56
+ solvers.
134
57
  """
135
58
 
136
59
  def __init__(
@@ -159,7 +82,10 @@ class LinearOperator(Operator):
159
82
  dual_base (LinearOperator, optional): Internal use for duals.
160
83
  adjoint_base (LinearOperator, optional): Internal use for adjoints.
161
84
  """
162
- super().__init__(domain, codomain, mapping)
85
+ super().__init__(
86
+ domain, codomain, self._mapping_impl, derivative=self._derivative_impl
87
+ )
88
+ self._mapping = mapping
163
89
  self._dual_base: Optional[LinearOperator] = dual_base
164
90
  self._adjoint_base: Optional[LinearOperator] = adjoint_base
165
91
  self._thread_safe: bool = thread_safe
@@ -329,47 +255,25 @@ class LinearOperator(Operator):
329
255
  """
330
256
  Creates a LinearOperator from its matrix representation.
331
257
 
332
- This factory method allows you to define a `LinearOperator` using a
333
- concrete matrix (like a `numpy.ndarray`) that acts on the component
334
- vectors of the abstract Hilbert space vectors. The `galerkin` flag
335
- determines how this matrix action is interpreted.
258
+ This factory defines a `LinearOperator` using a concrete matrix that
259
+ acts on the component vectors of the abstract Hilbert space vectors.
336
260
 
337
261
  Args:
338
- domain (HilbertSpace): The operator's domain.
339
- codomain (HilbertSpace): The operator's codomain.
340
- matrix (MatrixLike): The matrix representation, which can be a dense
341
- NumPy array or a SciPy LinearOperator. Its shape must be
342
- (codomain.dim, domain.dim).
343
- galerkin (bool): Specifies the interpretation of the matrix.
344
-
345
- - **`galerkin=False` (Default): Standard Component Mapping**
346
- This is the most direct interpretation. The matrix `M` maps the
347
- component vector `c_x` of an input vector `x` directly to the
348
- component vector `c_y` of the output vector `y`.
349
-
350
- - **`galerkin=True`: Galerkin (or "Weak Form") Representation**
351
- This interpretation is standard in the finite element method (FEM)
352
- and other variational techniques. The matrix `M` maps the component
353
- vector `c_x` of an input `x` to the component vector `c_yp` of the
354
- *dual* of the output vector `y`.
355
-
356
- - **Matrix Entries**: The matrix elements are defined by inner
357
- products with basis vectors: `M_ij = inner_product(A(b_j), b_i)`,
358
- where `b_j` are domain basis vectors and `b_i` are codomain
359
- basis vectors.
360
- - **Use Case**: This is critically important for preserving the
361
- mathematical properties of an operator. For example, if an operator
362
- `A` is self-adjoint, its Galerkin matrix `M` will be **symmetric**
363
- (`M.T == M`). This allows the use of highly efficient numerical
364
- methods like the Conjugate Gradient solver or Cholesky
365
- factorization, which rely on symmetry. The standard component
366
- matrix of a self-adjoint operator is generally not symmetric
367
- unless the basis is orthonormal.
262
+ domain: The operator's domain space.
263
+ codomain: The operator's codomain space.
264
+ matrix: The matrix representation (NumPy array or SciPy
265
+ LinearOperator). Shape must be `(codomain.dim, domain.dim)`.
266
+ galerkin: If `True`, the matrix is interpreted in its "weak form"
267
+ or Galerkin representation (`M_ij = <basis_j, A(basis_i)>`),
268
+ which maps a vector's components to the components of its
269
+ *dual*. This is crucial as it ensures a self-adjoint
270
+ operator is represented by a symmetric matrix. If `False`
271
+ (default), it's a standard component-to-component map.
368
272
 
369
273
  Returns:
370
- LinearOperator: A new `LinearOperator` instance whose action is
371
- defined by the provided matrix and interpretation.
274
+ A new `LinearOperator` defined by the matrix action.
372
275
  """
276
+
373
277
  assert matrix.shape == (codomain.dim, domain.dim)
374
278
 
375
279
  if galerkin:
@@ -521,53 +425,28 @@ class LinearOperator(Operator):
521
425
  parallel: bool = False,
522
426
  n_jobs: int = -1,
523
427
  ) -> Union[ScipyLinOp, np.ndarray]:
524
- """
525
- Returns a matrix representation of the operator.
428
+ """Returns a matrix representation of the operator.
526
429
 
527
- This method provides a concrete matrix that represents the abstract
528
- linear operator's action on the underlying component vectors.
430
+ This provides a concrete matrix that represents the operator's action
431
+ on the underlying component vectors.
529
432
 
530
433
  Args:
531
- dense (bool): Determines the format of the returned matrix.
532
- - If `True`, this method computes and returns a dense `numpy.ndarray`.
533
- Be aware that this can be very memory-intensive for
534
- high-dimensional spaces.
535
- - If `False` (default), it returns a matrix-free
536
- `scipy.sparse.linalg.LinearOperator`. This object encapsulates
537
- the operator's action (`matvec`) and its transpose action
538
- (`rmatvec`) without ever explicitly forming the full matrix in memory,
539
- making it ideal for large-scale problems.
540
-
541
- galerkin (bool): Specifies the interpretation of the matrix representation. This
542
- flag is crucial for correctly using the matrix with numerical solvers.
543
-
544
- - **`galerkin=False` (Default): Standard Component Mapping**
545
- The returned matrix `M` performs a standard component-to-component
546
- mapping.
547
- - **`matvec` action**: Takes the component vector `c_x` of an input `x`
548
- and returns the component vector `c_y` of the output `y`.
549
- - **`rmatvec` action**: Corresponds to the matrix of the **dual operator**, `A'`.
550
-
551
- - **`galerkin=True`: Galerkin (or "Weak Form") Representation**
552
- The returned matrix `M` represents the operator in a weak form, mapping
553
- components of a vector to components of a dual vector.
554
- - **`matvec` action**: Takes the component vector `c_x` of an input `x`
555
- and returns the component vector `c_yp` of the *dual* of the output `y`.
556
- - **`rmatvec` action**: Corresponds to the matrix of the **adjoint operator**, `A*`.
557
- - **Key Property**: This representation is designed to preserve fundamental
558
- mathematical properties. For instance, if the `LinearOperator` is
559
- self-adjoint, its Galerkin matrix will be **symmetric**, which is a
560
- prerequisite for algorithms like the Conjugate Gradient method.
561
-
562
- parallel (bool): If True, use parallel computing. Defaults to False.
563
- This is only relevant for dense matrices.
564
- n_jobs (int): Number of parallel jobs. Defaults to -1.
565
- This is only relevant for dense matrices.
434
+ dense: If `True`, returns a dense `numpy.ndarray`. If `False`
435
+ (default), returns a memory-efficient, matrix-free
436
+ `scipy.sparse.linalg.LinearOperator`.
437
+ galerkin: If `True`, the returned matrix is the Galerkin
438
+ representation, whose `rmatvec` corresponds to the
439
+ **adjoint** operator. If `False` (default), the `rmatvec`
440
+ corresponds to the **dual** operator. The Galerkin form is
441
+ essential for algorithms that rely on symmetry/self-adjointness.
442
+ parallel: If `True` and `dense=True`, computes the matrix columns
443
+ in parallel.
444
+ n_jobs: Number of parallel jobs to use. `-1` uses all available cores.
566
445
 
567
446
  Returns:
568
- Union[ScipyLinOp, np.ndarray]: The matrix representation of the
569
- operator, either as a dense array or a matrix-free object.
447
+ The matrix representation, either dense or matrix-free.
570
448
  """
449
+
571
450
  if dense:
572
451
  return self._compute_dense_matrix(galerkin, parallel, n_jobs)
573
452
  else:
@@ -625,31 +504,38 @@ class LinearOperator(Operator):
625
504
 
626
505
  def random_svd(
627
506
  self,
628
- rank: int,
507
+ size_estimate: int,
629
508
  /,
630
509
  *,
631
- power: int = 0,
632
510
  galerkin: bool = False,
633
- rtol: float = 1e-3,
634
- method: str = "fixed",
511
+ method: str = "variable",
512
+ max_rank: int = None,
513
+ power: int = 2,
514
+ rtol: float = 1e-4,
515
+ block_size: int = 10,
635
516
  parallel: bool = False,
636
517
  n_jobs: int = -1,
637
- ) -> Tuple[LinearOperator, "DiagonalLinearOperator", LinearOperator]:
518
+ ) -> Tuple[LinearOperator, DiagonalLinearOperator, LinearOperator]:
638
519
  """
639
520
  Computes an approximate SVD using a randomized algorithm.
640
521
 
641
522
  Args:
642
- rank (int): The desired rank of the SVD.
643
- power (int): The power of the random matrix.
523
+ size_estimate: For 'fixed' method, the exact target rank. For 'variable'
524
+ method, this is the initial rank to sample.
644
525
  galerkin (bool): If True, use the Galerkin representation.
645
- rtol (float): The relative tolerance for the SVD.
646
- method (str): The method to use for the SVD.
647
- - "fixed": Use a fixed rank SVD.
648
- - "variable": Use a variable rank SVD.
649
- parallel (bool): If True, use parallel computing. Defaults to False.
650
- Only used with fixed rank method.
651
- n_jobs (int): Number of parallel jobs. Defaults to -1.
652
- Only used with fixed rank method.
526
+ method ({'variable', 'fixed'}): The algorithm to use.
527
+ - 'variable': (Default) Progressively samples to find the rank needed
528
+ to meet tolerance `rtol`, stopping at `max_rank`.
529
+ - 'fixed': Returns a basis with exactly `size_estimate` columns.
530
+ max_rank: For 'variable' method, a hard limit on the rank. Ignored if
531
+ method='fixed'. Defaults to min(m, n).
532
+ power: Number of power iterations to improve accuracy.
533
+ rtol: Relative tolerance for the 'variable' method. Ignored if
534
+ method='fixed'.
535
+ block_size: Number of new vectors to sample per iteration in 'variable'
536
+ method. Ignored if method='fixed'.
537
+ parallel: Whether to use parallel matrix multiplication.
538
+ n_jobs: Number of jobs for parallelism.
653
539
 
654
540
  Returns:
655
541
  left (LinearOperator): The left singular vector matrix.
@@ -659,25 +545,25 @@ class LinearOperator(Operator):
659
545
  Notes:
660
546
  The right factor is in transposed form. This means the original
661
547
  operator can be approximated as:
662
- A = left @ singular_values @ right
663
-
548
+ A = left @ singular_values @ right
664
549
  """
665
550
  from .hilbert_space import EuclideanSpace
666
551
 
667
552
  matrix = self.matrix(galerkin=galerkin)
668
553
  m, n = matrix.shape
669
554
  k = min(m, n)
670
- rank = rank if rank <= k else k
671
555
 
672
- qr_factor: np.ndarray
673
- if method == "fixed":
674
- qr_factor = fixed_rank_random_range(
675
- matrix, rank, power=power, parallel=parallel, n_jobs=n_jobs
676
- )
677
- elif method == "variable":
678
- qr_factor = variable_rank_random_range(matrix, rank, power=power, rtol=rtol)
679
- else:
680
- raise ValueError("Invalid method selected")
556
+ qr_factor = random_range(
557
+ matrix,
558
+ size_estimate if size_estimate < k else k,
559
+ method=method,
560
+ max_rank=max_rank,
561
+ power=power,
562
+ rtol=rtol,
563
+ block_size=block_size,
564
+ parallel=parallel,
565
+ n_jobs=n_jobs,
566
+ )
681
567
 
682
568
  left_factor_mat, singular_values, right_factor_transposed = rm_svd(
683
569
  matrix, qr_factor
@@ -705,39 +591,40 @@ class LinearOperator(Operator):
705
591
 
706
592
  def random_eig(
707
593
  self,
708
- rank: int,
594
+ size_estimate: int,
709
595
  /,
710
596
  *,
711
- power: int = 0,
712
- rtol: float = 1e-3,
713
- method: str = "fixed",
597
+ method: str = "variable",
598
+ max_rank: int = None,
599
+ power: int = 2,
600
+ rtol: float = 1e-4,
601
+ block_size: int = 10,
714
602
  parallel: bool = False,
715
603
  n_jobs: int = -1,
716
- ) -> Tuple[LinearOperator, "DiagonalLinearOperator"]:
604
+ ) -> Tuple[LinearOperator, DiagonalLinearOperator]:
717
605
  """
718
- Computes an approximate eigendecomposition for a self-adjoint
719
- operator using a randomized algorithm.
606
+ Computes an approximate eigen-decomposition using a randomized algorithm.
720
607
 
721
608
  Args:
722
- rank (int): The desired rank of the eigendecomposition.
723
- power (int): The power of the random matrix.
724
- rtol (float): The relative tolerance for the eigendecomposition.
725
- method (str): The method to use for the eigendecomposition.
726
- - "fixed": Use a fixed rank eigendecomposition.
727
- - "variable": Use a variable rank eigendecomposition.
728
- parallel (bool): If True, use parallel computing. Defaults to False.
729
- Only used with fixed rank method.
730
- n_jobs (int): Number of parallel jobs. Defaults to -1.
731
- Only used with fixed rank method.
609
+ size_estimate: For 'fixed' method, the exact target rank. For 'variable'
610
+ method, this is the initial rank to sample.
611
+ method ({'variable', 'fixed'}): The algorithm to use.
612
+ - 'variable': (Default) Progressively samples to find the rank needed
613
+ to meet tolerance `rtol`, stopping at `max_rank`.
614
+ - 'fixed': Returns a basis with exactly `size_estimate` columns.
615
+ max_rank: For 'variable' method, a hard limit on the rank. Ignored if
616
+ method='fixed'. Defaults to min(m, n).
617
+ power: Number of power iterations to improve accuracy.
618
+ rtol: Relative tolerance for the 'variable' method. Ignored if
619
+ method='fixed'.
620
+ block_size: Number of new vectors to sample per iteration in 'variable'
621
+ method. Ignored if method='fixed'.
622
+ parallel: Whether to use parallel matrix multiplication.
623
+ n_jobs: Number of jobs for parallelism.
732
624
 
733
625
  Returns:
734
- expansion (LinearOperator): A linear operator that maps coefficients
735
- in the eigen-basis to the resulting vector.
736
- eigenvalues (DiagonalLinearOperator): The eigenvalues.
737
-
738
- Notes:
739
- The original operator can be approximated as:
740
- A = expansion @ eigenvalues @ expansion.adjoint
626
+ expansion (LinearOperator): Mapping from coefficients in eigen-basis to vectors.
627
+ eigenvaluevalues (DiagonalLinearOperator): The eigenvalues values.
741
628
 
742
629
  """
743
630
  from .hilbert_space import EuclideanSpace
@@ -746,17 +633,18 @@ class LinearOperator(Operator):
746
633
  matrix = self.matrix(galerkin=True)
747
634
  m, n = matrix.shape
748
635
  k = min(m, n)
749
- rank = rank if rank <= k else k
750
636
 
751
- qr_factor: np.ndarray
752
- if method == "fixed":
753
- qr_factor = fixed_rank_random_range(
754
- matrix, rank, power=power, parallel=parallel, n_jobs=n_jobs
755
- )
756
- elif method == "variable":
757
- qr_factor = variable_rank_random_range(matrix, rank, power=power, rtol=rtol)
758
- else:
759
- raise ValueError("Invalid method selected")
637
+ qr_factor = random_range(
638
+ matrix,
639
+ size_estimate if size_estimate < k else k,
640
+ method=method,
641
+ max_rank=max_rank,
642
+ power=power,
643
+ rtol=rtol,
644
+ block_size=block_size,
645
+ parallel=parallel,
646
+ n_jobs=n_jobs,
647
+ )
760
648
 
761
649
  eigenvectors, eigenvalues = rm_eig(matrix, qr_factor)
762
650
  euclidean = EuclideanSpace(qr_factor.shape[1])
@@ -770,12 +658,14 @@ class LinearOperator(Operator):
770
658
 
771
659
  def random_cholesky(
772
660
  self,
773
- rank: int,
661
+ size_estimate: int,
774
662
  /,
775
663
  *,
776
- power: int = 0,
777
- rtol: float = 1e-3,
778
- method: str = "fixed",
664
+ method: str = "variable",
665
+ max_rank: int = None,
666
+ power: int = 2,
667
+ rtol: float = 1e-4,
668
+ block_size: int = 10,
779
669
  parallel: bool = False,
780
670
  n_jobs: int = -1,
781
671
  ) -> LinearOperator:
@@ -784,16 +674,21 @@ class LinearOperator(Operator):
784
674
  self-adjoint operator using a randomized algorithm.
785
675
 
786
676
  Args:
787
- rank (int): The desired rank of the Cholesky decomposition.
788
- power (int): The power of the random matrix.
789
- rtol (float): The relative tolerance for the Cholesky decomposition.
790
- method (str): The method to use for the Cholesky decomposition.
791
- - "fixed": Use a fixed rank Cholesky decomposition.
792
- - "variable": Use a variable rank Cholesky decomposition.
793
- parallel (bool): If True, use parallel computing. Defaults to False.
794
- Only used with fixed rank method.
795
- n_jobs (int): Number of parallel jobs. Defaults to -1.
796
- Only used with fixed rank method.
677
+ size_estimate: For 'fixed' method, the exact target rank. For 'variable'
678
+ method, this is the initial rank to sample.
679
+ method ({'variable', 'fixed'}): The algorithm to use.
680
+ - 'variable': (Default) Progressively samples to find the rank needed
681
+ to meet tolerance `rtol`, stopping at `max_rank`.
682
+ - 'fixed': Returns a basis with exactly `size_estimate` columns.
683
+ max_rank: For 'variable' method, a hard limit on the rank. Ignored if
684
+ method='fixed'. Defaults to min(m, n).
685
+ power: Number of power iterations to improve accuracy.
686
+ rtol: Relative tolerance for the 'variable' method. Ignored if
687
+ method='fixed'.
688
+ block_size: Number of new vectors to sample per iteration in 'variable'
689
+ method. Ignored if method='fixed'.
690
+ parallel: Whether to use parallel matrix multiplication.
691
+ n_jobs: Number of jobs for parallelism.
797
692
 
798
693
  Returns:
799
694
  factor (LinearOperator): A linear operator from a Euclidean space
@@ -810,17 +705,18 @@ class LinearOperator(Operator):
810
705
  matrix = self.matrix(galerkin=True)
811
706
  m, n = matrix.shape
812
707
  k = min(m, n)
813
- rank = rank if rank <= k else k
814
708
 
815
- qr_factor: np.ndarray
816
- if method == "fixed":
817
- qr_factor = fixed_rank_random_range(
818
- matrix, rank, power=power, parallel=parallel, n_jobs=n_jobs
819
- )
820
- elif method == "variable":
821
- qr_factor = variable_rank_random_range(matrix, rank, power=power, rtol=rtol)
822
- else:
823
- raise ValueError("Invalid method selected")
709
+ qr_factor = random_range(
710
+ matrix,
711
+ size_estimate if size_estimate < k else k,
712
+ method=method,
713
+ max_rank=max_rank,
714
+ power=power,
715
+ rtol=rtol,
716
+ block_size=block_size,
717
+ parallel=parallel,
718
+ n_jobs=n_jobs,
719
+ )
824
720
 
825
721
  cholesky_factor = rm_chol(matrix, qr_factor)
826
722
 
@@ -831,6 +727,12 @@ class LinearOperator(Operator):
831
727
  galerkin=True,
832
728
  )
833
729
 
730
+ def _mapping_impl(self, x: Any) -> Any:
731
+ return self._mapping(x)
732
+
733
+ def _derivative_impl(self, _: Any) -> LinearOperator:
734
+ return self
735
+
834
736
  def _dual_mapping_default(self, yp: Any) -> LinearForm:
835
737
  from .linear_forms import LinearForm
836
738
 
@@ -899,55 +801,126 @@ class LinearOperator(Operator):
899
801
  def __truediv__(self, a: float) -> LinearOperator:
900
802
  return self * (1.0 / a)
901
803
 
902
- def __add__(self, other: LinearOperator) -> LinearOperator:
903
- domain = self.domain
904
- codomain = self.codomain
804
+ def __add__(
805
+ self, other: NonLinearOperator | LinearOperator
806
+ ) -> NonLinearOperator | LinearOperator:
807
+ """Returns the sum of this operator and another.
905
808
 
906
- def mapping(x: Any) -> Any:
907
- return codomain.add(self(x), other(x))
809
+ If `other` is also a `LinearOperator`, this performs an optimized
810
+ addition that preserves linearity and correctly defines the new
811
+ operator's `adjoint`. Otherwise, it delegates to the general
812
+ implementation in the `NonLinearOperator` base class.
908
813
 
909
- def adjoint_mapping(y: Any) -> Any:
910
- return domain.add(self.adjoint(y), other.adjoint(y))
814
+ Args:
815
+ other: The operator to add to this one.
911
816
 
912
- return LinearOperator(
913
- domain, codomain, mapping, adjoint_mapping=adjoint_mapping
914
- )
817
+ Returns:
818
+ A new `LinearOperator` if adding two linear operators, otherwise
819
+ a `NonLinearOperator`.
820
+ """
915
821
 
916
- def __sub__(self, other: LinearOperator) -> LinearOperator:
917
- domain = self.domain
918
- codomain = self.codomain
822
+ if isinstance(other, LinearOperator):
823
+ domain = self.domain
824
+ codomain = self.codomain
919
825
 
920
- def mapping(x: Any) -> Any:
921
- return codomain.subtract(self(x), other(x))
826
+ def mapping(x: Any) -> Any:
827
+ return codomain.add(self(x), other(x))
922
828
 
923
- def adjoint_mapping(y: Any) -> Any:
924
- return domain.subtract(self.adjoint(y), other.adjoint(y))
829
+ def adjoint_mapping(y: Any) -> Any:
830
+ return domain.add(self.adjoint(y), other.adjoint(y))
925
831
 
926
- return LinearOperator(
927
- domain, codomain, mapping, adjoint_mapping=adjoint_mapping
928
- )
832
+ return LinearOperator(
833
+ domain, codomain, mapping, adjoint_mapping=adjoint_mapping
834
+ )
835
+ else:
836
+ return super().__add__(other)
929
837
 
930
- def __matmul__(self, other: LinearOperator) -> LinearOperator:
931
- domain = other.domain
932
- codomain = self.codomain
838
+ def __sub__(
839
+ self, other: NonLinearOperator | LinearOperator
840
+ ) -> NonLinearOperator | LinearOperator:
841
+ """Returns the difference between this operator and another.
933
842
 
934
- def mapping(x: Any) -> Any:
935
- return self(other(x))
843
+ If `other` is also a `LinearOperator`, this performs an optimized
844
+ subtraction that preserves linearity and correctly defines the new
845
+ operator's `adjoint`. Otherwise, it delegates to the general
846
+ implementation in the `NonLinearOperator` base class.
936
847
 
937
- def adjoint_mapping(y: Any) -> Any:
938
- return other.adjoint(self.adjoint(y))
848
+ Args:
849
+ other: The operator to subtract from this one.
939
850
 
940
- return LinearOperator(
941
- domain, codomain, mapping, adjoint_mapping=adjoint_mapping
942
- )
851
+ Returns:
852
+ A new `LinearOperator` if subtracting two linear operators,
853
+ otherwise a `NonLinearOperator`.
854
+ """
855
+
856
+ if isinstance(other, LinearOperator):
857
+
858
+ domain = self.domain
859
+ codomain = self.codomain
860
+
861
+ def mapping(x: Any) -> Any:
862
+ return codomain.subtract(self(x), other(x))
863
+
864
+ def adjoint_mapping(y: Any) -> Any:
865
+ return domain.subtract(self.adjoint(y), other.adjoint(y))
866
+
867
+ return LinearOperator(
868
+ domain, codomain, mapping, adjoint_mapping=adjoint_mapping
869
+ )
870
+ else:
871
+ return super().__sub__(other)
872
+
873
+ def __matmul__(
874
+ self, other: NonLinearOperator | LinearOperator
875
+ ) -> NonLinearOperator | LinearOperator:
876
+ """Composes this operator with another using the @ symbol.
877
+
878
+ The composition `(self @ other)` results in a new operator that
879
+ first applies `other` and then applies `self`, i.e.,
880
+ `(self @ other)(x) = self(other(x))`.
881
+
882
+ If `other` is also a `LinearOperator`, this creates a new `LinearOperator`
883
+ whose adjoint is correctly defined using the composition rule:
884
+ `(L1 @ L2)* = L2* @ L1*`. Otherwise, it delegates to the general
885
+ `NonLinearOperator` implementation.
886
+
887
+ Args:
888
+ other: The operator to compose with (the right-hand operator).
889
+
890
+ Returns:
891
+ A new `LinearOperator` if composing two linear operators,
892
+ otherwise a `NonLinearOperator`.
893
+ """
894
+
895
+ if isinstance(other, LinearOperator):
896
+ domain = other.domain
897
+ codomain = self.codomain
898
+
899
+ def mapping(x: Any) -> Any:
900
+ return self(other(x))
901
+
902
+ def adjoint_mapping(y: Any) -> Any:
903
+ return other.adjoint(self.adjoint(y))
904
+
905
+ return LinearOperator(
906
+ domain, codomain, mapping, adjoint_mapping=adjoint_mapping
907
+ )
908
+
909
+ else:
910
+ return super().__matmul__(other)
943
911
 
944
912
  def __str__(self) -> str:
945
913
  return self.matrix(dense=True).__str__()
946
914
 
947
915
 
948
916
  class DiagonalLinearOperator(LinearOperator):
949
- """
950
- A LinearOperator that is diagonal in its component representation.
917
+ """A LinearOperator that is diagonal in its component representation.
918
+
919
+ This provides an efficient implementation for diagonal linear operators.
920
+ Its key feature is support for **functional calculus**, allowing for the
921
+ direct computation of operator functions like inverse (`.inverse`) or
922
+
923
+ square root (`.sqrt`) by applying the function to the diagonal entries.
951
924
  """
952
925
 
953
926
  def __init__(
@@ -989,21 +962,24 @@ class DiagonalLinearOperator(LinearOperator):
989
962
  """The diagonal entries of the operator's matrix representation."""
990
963
  return self._diagonal_values
991
964
 
992
- def function(self, f: Callable[[float], float]) -> "DiagonalLinearOperator":
993
- """
994
- Applies a function to the operator via functional calculus.
965
+ def function(self, f: Callable[[float], float]) -> DiagonalLinearOperator:
966
+ """Applies a function to the operator via functional calculus.
995
967
 
996
- This creates a new DiagonalLinearOperator where each diagonal entry `d_i`
997
- is replaced by `f(d_i)`.
968
+ This creates a new `DiagonalLinearOperator` where the function `f` has
969
+ been applied to each of the diagonal entries. For example,
970
+ `op.function(lambda x: 1/x)` computes the inverse.
998
971
 
999
972
  Args:
1000
- f: A function that maps a float to a float.
973
+ f: A scalar function to apply to the diagonal values.
974
+
975
+ Returns:
976
+ A new `DiagonalLinearOperator` with the transformed diagonal.
1001
977
  """
1002
978
  diagonal_values = np.array([f(x) for x in self.diagonal_values])
1003
979
  return DiagonalLinearOperator(self.domain, self.codomain, diagonal_values)
1004
980
 
1005
981
  @property
1006
- def inverse(self) -> "DiagonalLinearOperator":
982
+ def inverse(self) -> DiagonalLinearOperator:
1007
983
  """
1008
984
  The inverse of the operator, computed via functional calculus.
1009
985
  Requires all diagonal values to be non-zero.
@@ -1012,7 +988,7 @@ class DiagonalLinearOperator(LinearOperator):
1012
988
  return self.function(lambda x: 1 / x)
1013
989
 
1014
990
  @property
1015
- def sqrt(self) -> "DiagonalLinearOperator":
991
+ def sqrt(self) -> DiagonalLinearOperator:
1016
992
  """
1017
993
  The square root of the operator, computed via functional calculus.
1018
994
  Requires all diagonal values to be non-negative.