pygeoinf 1.0.9__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeoinf/operators.py CHANGED
@@ -1,5 +1,22 @@
1
1
  """
2
- Module defining the Operator, LinearOperator, and DiagonalLinearOperator classes.
2
+ Defines a class hierarchy for operators between Hilbert spaces.
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.
8
+
9
+ Key Classes
10
+ -----------
11
+ - `Operator`: A general, potentially non-linear operator defined by a simple
12
+ mapping function.
13
+ - `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.
17
+ - `DiagonalLinearOperator`: A specialized, efficient implementation for linear
18
+ operators that are diagonal in their component representation, which supports
19
+ functional calculus.
3
20
  """
4
21
 
5
22
  from __future__ import annotations
@@ -9,8 +26,7 @@ import numpy as np
9
26
  from scipy.sparse.linalg import LinearOperator as ScipyLinOp
10
27
  from scipy.sparse import diags
11
28
 
12
- # This import path assumes `pygeoinf` is on the python path.
13
- # For a local package structure, you might use from ..random_matrix import ...
29
+
14
30
  from .random_matrix import (
15
31
  fixed_rank_random_range,
16
32
  variable_rank_random_range,
@@ -22,7 +38,7 @@ from .random_matrix import (
22
38
  # This block only runs for type checkers, not at runtime
23
39
  if TYPE_CHECKING:
24
40
  from .hilbert_space import HilbertSpace, EuclideanSpace
25
- from .forms import LinearForm
41
+ from .linear_forms import LinearForm
26
42
 
27
43
 
28
44
  class Operator:
@@ -83,8 +99,14 @@ class LinearOperator(Operator):
83
99
  """
84
100
  A linear operator between two Hilbert spaces.
85
101
 
86
- This class is the primary workhorse for linear algebraic operations,
87
- supporting composition, adjoints, duals, and matrix representations.
102
+ This class is the primary workhorse for linear algebraic operations. An
103
+ operator can be defined "on the fly" from a callable mapping. The class
104
+ automatically derives the associated `adjoint` and `dual` operators,
105
+ which are fundamental for solving linear systems and for optimization.
106
+
107
+ It supports a rich algebra, including composition (`@`), addition (`+`),
108
+ and scalar multiplication (`*`). Operators can also be represented as
109
+ dense or matrix-free (`scipy`) matrices for use with numerical solvers.
88
110
  """
89
111
 
90
112
  def __init__(
@@ -96,7 +118,6 @@ class LinearOperator(Operator):
96
118
  *,
97
119
  dual_mapping: Optional[Callable[[Any], Any]] = None,
98
120
  adjoint_mapping: Optional[Callable[[Any], Any]] = None,
99
- formal_adjoint_mapping: Optional[Callable[[Any], Any]] = None,
100
121
  thread_safe: bool = False,
101
122
  dual_base: Optional[LinearOperator] = None,
102
123
  adjoint_base: Optional[LinearOperator] = None,
@@ -110,7 +131,6 @@ class LinearOperator(Operator):
110
131
  mapping (callable): The function defining the linear mapping.
111
132
  dual_mapping (callable, optional): The action of the dual operator.
112
133
  adjoint_mapping (callable, optional): The action of the adjoint.
113
- formal_adjoint_mapping (callable, optional): The formal adjoint.
114
134
  thread_safe (bool, optional): True if the mapping is thread-safe.
115
135
  dual_base (LinearOperator, optional): Internal use for duals.
116
136
  adjoint_base (LinearOperator, optional): Internal use for adjoints.
@@ -119,17 +139,12 @@ class LinearOperator(Operator):
119
139
  self._dual_base: Optional[LinearOperator] = dual_base
120
140
  self._adjoint_base: Optional[LinearOperator] = adjoint_base
121
141
  self._thread_safe: bool = thread_safe
122
- self.__formal_adjoint_mapping: Optional[Callable[[Any], Any]]
123
142
  self.__adjoint_mapping: Callable[[Any], Any]
124
143
  self.__dual_mapping: Callable[[Any], Any]
125
144
 
126
145
  if dual_mapping is None:
127
146
  if adjoint_mapping is None:
128
- if formal_adjoint_mapping is None:
129
- self.__dual_mapping = self._dual_mapping_default
130
- else:
131
- self.__formal_adjoint_mapping = formal_adjoint_mapping
132
- self.__dual_mapping = self._dual_mapping_from_formal_adjoint
147
+ self.__dual_mapping = self._dual_mapping_default
133
148
  self.__adjoint_mapping = self._adjoint_mapping_from_dual
134
149
  else:
135
150
  self.__adjoint_mapping = adjoint_mapping
@@ -156,11 +171,76 @@ class LinearOperator(Operator):
156
171
  return LinearOperator(domain, domain, mapping, adjoint_mapping=mapping)
157
172
 
158
173
  @staticmethod
159
- def formally_self_adjoint(
160
- domain: "HilbertSpace", mapping: Callable[[Any], Any]
174
+ def from_formal_adjoint(
175
+ domain: "HilbertSpace", codomain: "HilbertSpace", operator: LinearOperator
161
176
  ) -> LinearOperator:
162
- """Creates a formally self-adjoint operator."""
163
- return LinearOperator(domain, domain, mapping, formal_adjoint_mapping=mapping)
177
+ """
178
+ Constructs an operator on weighted spaces from one on the underlying spaces.
179
+
180
+ This is a key method for working with `MassWeightedHilbertSpace`. It takes
181
+ an operator `A` that is defined on the simple, unweighted underlying spaces
182
+ and "lifts" it to be a proper operator on the mass-weighted spaces. It
183
+ correctly defines the new operator's adjoint with respect to the
184
+ weighted inner products.
185
+
186
+ Args:
187
+ domain: The (potentially) mass-weighted domain of the new operator.
188
+ codomain: The (potentially) mass-weighted codomain of the new operator.
189
+ operator: The original operator defined on the underlying,
190
+ unweighted spaces.
191
+
192
+ Returns:
193
+ A new `LinearOperator` that acts between the mass-weighted spaces.
194
+ """
195
+ from .hilbert_space import MassWeightedHilbertSpace
196
+
197
+ if isinstance(domain, MassWeightedHilbertSpace):
198
+ domain_base = domain.underlying_space
199
+ domain_inverse_mass_operator = domain.inverse_mass_operator
200
+ else:
201
+ domain_base = domain
202
+ domain_inverse_mass_operator = domain.identity_operator()
203
+
204
+ if isinstance(codomain, MassWeightedHilbertSpace):
205
+ codomain_base = codomain.underlying_space
206
+ codomain_mass_operator = codomain.mass_operator
207
+ else:
208
+ codomain_base = codomain
209
+ codomain_mass_operator = codomain.identity_operator()
210
+
211
+ if domain_base != operator.domain:
212
+ raise ValueError("Domain mismatch")
213
+ if codomain_base != operator.codomain:
214
+ raise ValueError("Codomain mismatch")
215
+
216
+ return LinearOperator(
217
+ domain,
218
+ codomain,
219
+ operator,
220
+ adjoint_mapping=domain_inverse_mass_operator
221
+ @ operator.adjoint
222
+ @ codomain_mass_operator,
223
+ )
224
+
225
+ @staticmethod
226
+ def from_formally_self_adjoint(
227
+ domain: "HilbertSpace", operator: LinearOperator
228
+ ) -> LinearOperator:
229
+ """
230
+ Method to construct LinearOperators on MassWeightedHilbertSpaces
231
+ that are self-adjoint on the underlying space.
232
+
233
+ Args:
234
+ domain (MassWeightedHilbertSpace): The domain of the operator.
235
+ operator (LinearOperator): The operator to be converted
236
+
237
+ Notes:
238
+ If the domain is not a MassWeightedHilbertSpace, the underlying
239
+ domain is taken to be the domain, and the mass operator the identity.
240
+ In such cases the method is well-defined but pointless as the output
241
+ operator is identical to the input operator.
242
+ """
243
+ return LinearOperator.from_formal_adjoint(domain, domain, operator)
164
244
 
165
245
  @staticmethod
166
246
  def from_linear_forms(forms: List["LinearForm"]) -> LinearOperator:
@@ -203,15 +283,48 @@ class LinearOperator(Operator):
203
283
  galerkin: bool = False,
204
284
  ) -> LinearOperator:
205
285
  """
206
- Creates an operator from its matrix representation.
286
+ Creates a LinearOperator from its matrix representation.
287
+
288
+ This factory method allows you to define a `LinearOperator` using a
289
+ concrete matrix (like a `numpy.ndarray`) that acts on the component
290
+ vectors of the abstract Hilbert space vectors. The `galerkin` flag
291
+ determines how this matrix action is interpreted.
207
292
 
208
293
  Args:
209
- domain: The operator's domain.
210
- codomain: The operator's codomain.
211
- matrix: The matrix representation.
212
- galerkin: If False (default), matrix maps components to components.
213
- If True, matrix maps components of the domain to dual
214
- components of the codomain.
294
+ domain (HilbertSpace): The operator's domain.
295
+ codomain (HilbertSpace): The operator's codomain.
296
+ matrix (MatrixLike): The matrix representation, which can be a dense
297
+ NumPy array or a SciPy LinearOperator. Its shape must be
298
+ (codomain.dim, domain.dim).
299
+ galerkin (bool): Specifies the interpretation of the matrix.
300
+
301
+ - **`galerkin=False` (Default): Standard Component Mapping**
302
+ This is the most direct interpretation. The matrix `M` maps the
303
+ component vector `c_x` of an input vector `x` directly to the
304
+ component vector `c_y` of the output vector `y`.
305
+
306
+ - **`galerkin=True`: Galerkin (or "Weak Form") Representation**
307
+ This interpretation is standard in the finite element method (FEM)
308
+ and other variational techniques. The matrix `M` maps the component
309
+ vector `c_x` of an input `x` to the component vector `c_yp` of the
310
+ *dual* of the output vector `y`.
311
+
312
+ - **Matrix Entries**: The matrix elements are defined by inner
313
+ products with basis vectors: `M_ij = inner_product(A(b_j), b_i)`,
314
+ where `b_j` are domain basis vectors and `b_i` are codomain
315
+ basis vectors.
316
+ - **Use Case**: This is critically important for preserving the
317
+ mathematical properties of an operator. For example, if an operator
318
+ `A` is self-adjoint, its Galerkin matrix `M` will be **symmetric**
319
+ (`M.T == M`). This allows the use of highly efficient numerical
320
+ methods like the Conjugate Gradient solver or Cholesky
321
+ factorization, which rely on symmetry. The standard component
322
+ matrix of a self-adjoint operator is generally not symmetric
323
+ unless the basis is orthonormal.
324
+
325
+ Returns:
326
+ LinearOperator: A new `LinearOperator` instance whose action is
327
+ defined by the provided matrix and interpretation.
215
328
  """
216
329
  assert matrix.shape == (codomain.dim, domain.dim)
217
330
 
@@ -361,11 +474,44 @@ class LinearOperator(Operator):
361
474
  """
362
475
  Returns a matrix representation of the operator.
363
476
 
364
- By default, returns a matrix-free `scipy.sparse.linalg.LinearOperator`.
477
+ This method provides a concrete matrix that represents the abstract
478
+ linear operator's action on the underlying component vectors.
365
479
 
366
480
  Args:
367
- dense: If True, returns a dense `numpy.ndarray`.
368
- galerkin: If True, returns the Galerkin representation.
481
+ dense (bool): Determines the format of the returned matrix.
482
+ - If `True`, this method computes and returns a dense `numpy.ndarray`.
483
+ Be aware that this can be very memory-intensive for
484
+ high-dimensional spaces.
485
+ - If `False` (default), it returns a matrix-free
486
+ `scipy.sparse.linalg.LinearOperator`. This object encapsulates
487
+ the operator's action (`matvec`) and its transpose action
488
+ (`rmatvec`) without ever explicitly forming the full matrix in memory,
489
+ making it ideal for large-scale problems.
490
+
491
+ galerkin (bool): Specifies the interpretation of the matrix representation. This
492
+ flag is crucial for correctly using the matrix with numerical solvers.
493
+
494
+ - **`galerkin=False` (Default): Standard Component Mapping**
495
+ The returned matrix `M` performs a standard component-to-component
496
+ mapping.
497
+ - **`matvec` action**: Takes the component vector `c_x` of an input `x`
498
+ and returns the component vector `c_y` of the output `y`.
499
+ - **`rmatvec` action**: Corresponds to the matrix of the **dual operator**, `A'`.
500
+
501
+ - **`galerkin=True`: Galerkin (or "Weak Form") Representation**
502
+ The returned matrix `M` represents the operator in a weak form, mapping
503
+ components of a vector to components of a dual vector.
504
+ - **`matvec` action**: Takes the component vector `c_x` of an input `x`
505
+ and returns the component vector `c_yp` of the *dual* of the output `y`.
506
+ - **`rmatvec` action**: Corresponds to the matrix of the **adjoint operator**, `A*`.
507
+ - **Key Property**: This representation is designed to preserve fundamental
508
+ mathematical properties. For instance, if the `LinearOperator` is
509
+ self-adjoint, its Galerkin matrix will be **symmetric**, which is a
510
+ prerequisite for algorithms like the Conjugate Gradient method.
511
+
512
+ Returns:
513
+ Union[ScipyLinOp, np.ndarray]: The matrix representation of the
514
+ operator, either as a dense array or a matrix-free object.
369
515
  """
370
516
  if dense:
371
517
  return self._compute_dense_matrix(galerkin)
@@ -552,7 +698,7 @@ class LinearOperator(Operator):
552
698
  )
553
699
 
554
700
  def _dual_mapping_default(self, yp: Any) -> "LinearForm":
555
- from .forms import LinearForm
701
+ from .linear_forms import LinearForm
556
702
 
557
703
  return LinearForm(self.domain, mapping=lambda x: yp(self(x)))
558
704
 
@@ -561,13 +707,6 @@ class LinearOperator(Operator):
561
707
  x = self.__adjoint_mapping(y)
562
708
  return self.domain.to_dual(x)
563
709
 
564
- def _dual_mapping_from_formal_adjoint(self, yp: Any) -> Any:
565
- cyp = self.codomain.dual.to_components(yp)
566
- y = self.codomain.from_components(cyp)
567
- x = self.__formal_adjoint_mapping(y)
568
- cx = self.domain.to_components(x)
569
- return self.domain.dual.from_components(cx)
570
-
571
710
  def _adjoint_mapping_from_dual(self, y: Any) -> Any:
572
711
  yp = self.codomain.to_dual(y)
573
712
  xp = self.__dual_mapping(yp)
pygeoinf/random_matrix.py CHANGED
@@ -1,12 +1,20 @@
1
1
  """
2
- Module for random matrix factorisations.
3
-
4
- This module provides functions for computing low-rank matrix factorisations
5
- using randomized algorithms. These methods are particularly effective for large
6
- matrices where deterministic methods would be too slow. The implementations
7
- are based on the work of Halko, Martinsson, and Tropp (2011).
2
+ Implements randomized algorithms for low-rank matrix factorizations.
3
+
4
+ This module provides functions for computing approximate, low-rank matrix
5
+ factorizations (SVD, Cholesky, Eigendecomposition) using randomized methods.
6
+ These algorithms are particularly effective for large, high-dimensional matrices
7
+ where deterministic methods would be computationally prohibitive. They work by
8
+ finding a low-dimensional subspace that captures most of the "action" of the
9
+ matrix.
10
+
11
+ The implementations are based on the seminal work of Halko, Martinsson, and
12
+ Tropp, "Finding structure with randomness: Probabilistic algorithms for
13
+ constructing approximate matrix decompositions" (2011).
8
14
  """
9
15
 
16
+ from typing import Tuple, Union
17
+
10
18
  import numpy as np
11
19
  from scipy.linalg import (
12
20
  cho_factor,
@@ -16,7 +24,7 @@ from scipy.linalg import (
16
24
  qr,
17
25
  )
18
26
  from scipy.sparse.linalg import LinearOperator as ScipyLinOp
19
- from typing import Tuple, Union
27
+
20
28
 
21
29
  # A type for objects that act like matrices (numpy arrays or SciPy LinearOperators)
22
30
  MatrixLike = Union[np.ndarray, ScipyLinOp]
@@ -26,29 +34,24 @@ def fixed_rank_random_range(
26
34
  matrix: MatrixLike, rank: int, power: int = 0
27
35
  ) -> np.ndarray:
28
36
  """
29
- Computes an orthonormal basis for a fixed-rank approximation to the
30
- range of a matrix using a randomized method.
37
+ Computes an orthonormal basis for a fixed-rank approximation of a matrix's range.
31
38
 
32
- This is a two-stage algorithm that finds a low-dimensional subspace that
33
- captures most of the action of the matrix.
39
+ This randomized algorithm finds a low-dimensional subspace that captures
40
+ most of the action of the matrix.
34
41
 
35
42
  Args:
36
- matrix (matrix-like): An (m, n) matrix or LinearOperator whose range
37
- is to be approximated.
38
- rank (int): The desired rank for the approximation. Must be
39
- greater than 1.
40
- power (int): The exponent for power iterations, used to improve the
41
- accuracy of the approximation.
43
+ matrix: An (m, n) matrix or scipy.LinearOperator whose range is to be approximated.
44
+ rank: The desired rank for the approximation.
45
+ power: The number of power iterations to perform. Power iterations
46
+ (multiplying by `A*A`) improves the accuracy of the approximation by
47
+ amplifying the dominant singular values, but adds to the computational cost.
42
48
 
43
49
  Returns:
44
- numpy.ndarray: An (m, rank) matrix with orthonormal columns whose
45
- span approximates the range of the input matrix.
50
+ An (m, rank) matrix with orthonormal columns whose span approximates
51
+ the range of the input matrix.
46
52
 
47
53
  Notes:
48
- If the input matrix is a scipy LinearOperator, it must have the
49
- `matmat` and `rmatmat` methods implemented.
50
-
51
- This method is based on Algorithm 4.4 in Halko et al. 2011.
54
+ Based on Algorithm 4.4 in Halko et al. 2011.
52
55
  """
53
56
 
54
57
  m, n = matrix.shape
@@ -143,19 +146,20 @@ def random_svd(
143
146
  matrix: MatrixLike, qr_factor: np.ndarray
144
147
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
145
148
  """
146
- Computes an approximate Singular Value Decomposition (SVD) from a
147
- low-rank range approximation.
149
+ Computes an approximate SVD from a low-rank range approximation.
150
+
151
+ This function takes the original matrix and an orthonormal basis for its
152
+ approximate range (the `qr_factor`) and projects the problem into a smaller
153
+ subspace where a deterministic SVD is cheap to compute.
148
154
 
149
155
  Args:
150
- matrix (matrix-like): The original (m, n) matrix or LinearOperator.
151
- qr_factor (numpy.ndarray): An (m, k) orthonormal basis for the
152
- approximate range of the matrix, typically from a
153
- `random_range` function.
156
+ matrix: The original (m, n) matrix or LinearOperator.
157
+ qr_factor: An (m, k) orthonormal basis for the approximate range,
158
+ typically from a `random_range` function.
154
159
 
155
160
  Returns:
156
- (numpy.ndarray, numpy.ndarray, numpy.ndarray): A tuple (U, S, Vh)
157
- containing the approximate SVD factors, such that A ~= U @ S @ Vh.
158
- S is a 1D array of singular values.
161
+ A tuple `(U, S, Vh)` containing the approximate SVD factors, where S is
162
+ a 1D array of singular values.
159
163
 
160
164
  Notes:
161
165
  Based on Algorithm 5.1 of Halko et al. 2011.