pygeoinf 1.2.0__py3-none-any.whl → 1.2.1__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 CHANGED
@@ -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
+ """
pygeoinf/direct_sum.py CHANGED
@@ -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(
pygeoinf/hilbert_space.py CHANGED
@@ -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
 
pygeoinf/linear_forms.py CHANGED
@@ -1,33 +1,36 @@
1
1
  """
2
- Provides the `LinearForm` class to represent linear functionals.
2
+ Provides the `LinearForm` class for representing linear functionals.
3
3
 
4
4
  A linear form is a linear mapping from a vector in a Hilbert space to a
5
- scalar (a real number). This class provides a concrete representation for
6
- elements of the dual space of a `HilbertSpace`.
7
-
8
- A `LinearForm` can be thought of as a dual vector and is a fundamental component
9
- for defining inner products and adjoint operators within the library.
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.
10
8
  """
11
9
 
12
10
  from __future__ import annotations
13
11
  from typing import Callable, Optional, Any, TYPE_CHECKING
14
12
 
13
+ from joblib import Parallel, delayed
14
+
15
15
  import numpy as np
16
16
 
17
+ from .nonlinear_forms import NonLinearForm
18
+
17
19
  # This block only runs for type checkers, not at runtime
18
20
  if TYPE_CHECKING:
19
- from .hilbert_space import HilbertSpace, EuclideanSpace
20
- from .operators import LinearOperator
21
+ from .hilbert_space import HilbertSpace, EuclideanSpace, Vector
22
+ from .linear_operators import LinearOperator
21
23
 
22
24
 
23
- class LinearForm:
25
+ class LinearForm(NonLinearForm):
24
26
  """
25
- Represents a linear form, a functional that maps vectors to scalars.
27
+ Represents a linear form as an efficient, component-based functional.
26
28
 
27
- A `LinearForm` is an element of a dual `HilbertSpace`. It is defined by its
28
- action on vectors from its `domain` space. Internally, this action is
29
- represented by a component vector, which when dotted with the component
30
- vector of a primal space element, produces the scalar result.
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.
31
34
  """
32
35
 
33
36
  def __init__(
@@ -35,29 +38,49 @@ class LinearForm:
35
38
  domain: HilbertSpace,
36
39
  /,
37
40
  *,
38
- mapping: Optional[Callable[[Any], float]] = None,
39
41
  components: Optional[np.ndarray] = None,
42
+ mapping: Optional[Callable[[Vector], float]] = None,
43
+ parallel: bool = False,
44
+ n_jobs: int = -1,
40
45
  ) -> None:
41
46
  """
42
- Initializes the LinearForm.
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.
43
53
 
44
- A form can be defined either by its functional mapping or directly
45
- by its component vector. If a mapping is provided without components,
46
- the components will be computed by evaluating the mapping on the
47
- basis vectors of the domain.
48
54
 
49
55
  Args:
50
56
  domain: The Hilbert space on which the form is defined.
51
- mapping: A function `f(x)` defining the action of the form.
52
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.
53
71
  """
54
72
 
55
- self._domain: HilbertSpace = domain
73
+ super().__init__(
74
+ domain,
75
+ self._mapping_impl,
76
+ gradient=self._gradient_impl,
77
+ hessian=self._hessian_impl,
78
+ )
56
79
 
57
80
  if components is None:
58
81
  if mapping is None:
59
82
  raise AssertionError("Neither mapping nor components specified.")
60
- self._compute_components(mapping)
83
+ self._compute_components(mapping, parallel, n_jobs)
61
84
  else:
62
85
  self._components: np.ndarray = components
63
86
 
@@ -93,7 +116,7 @@ class LinearForm:
93
116
  is the scalar result of the form's action.
94
117
  """
95
118
  from .hilbert_space import EuclideanSpace
96
- from .operators import LinearOperator
119
+ from .linear_operators import LinearOperator
97
120
 
98
121
  return LinearOperator(
99
122
  self.domain,
@@ -108,10 +131,6 @@ class LinearForm:
108
131
  """
109
132
  return LinearForm(self.domain, components=self.components.copy())
110
133
 
111
- def __call__(self, x: Any) -> float:
112
- """Applies the linear form to a vector."""
113
- return np.dot(self._components, self.domain.to_components(x))
114
-
115
134
  def __neg__(self) -> LinearForm:
116
135
  """Returns the additive inverse of the form."""
117
136
  return LinearForm(self.domain, components=-self._components)
@@ -128,13 +147,47 @@ class LinearForm:
128
147
  """Returns the division of the form by a scalar."""
129
148
  return self * (1.0 / a)
130
149
 
131
- def __add__(self, other: LinearForm) -> LinearForm:
132
- """Returns the sum of this form and another."""
133
- return LinearForm(self.domain, components=self.components + other.components)
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.
134
181
 
135
- def __sub__(self, other: LinearForm) -> LinearForm:
136
- """Returns the difference between this form and another."""
137
- return LinearForm(self.domain, components=self.components - other.components)
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)
138
191
 
139
192
  def __imul__(self, a: float) -> "LinearForm":
140
193
  """
@@ -156,14 +209,55 @@ class LinearForm:
156
209
  """Returns the string representation of the form's components."""
157
210
  return self.components.__str__()
158
211
 
159
- def _compute_components(self, mapping: Callable[[Any], float]):
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:
160
260
  """
161
- Computes the component vector of the form.
261
+ Computes the Hessian of the form at a point.
162
262
  """
163
- self._components = np.zeros(self.domain.dim)
164
- cx = np.zeros(self.domain.dim)
165
- for i in range(self.domain.dim):
166
- cx[i] = 1
167
- x = self.domain.from_components(cx)
168
- self._components[i] = mapping(x)
169
- cx[i] = 0
263
+ return self.domain.zero_operator()