pygeoinf 1.2.0__tar.gz → 1.2.1__tar.gz

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