Source code for pygeoinf.linear_forms

"""
Provides the `LinearForm` class for representing linear functionals.

A linear form is a linear mapping from a vector in a Hilbert space to a
scalar. This class provides a concrete, component-based representation for
elements of the dual space of a `HilbertSpace`. It inherits from `NonLinearForm`,
specializing it for the linear case.
"""

from __future__ import annotations
from typing import Callable, Optional, Any, TYPE_CHECKING

from joblib import Parallel, delayed

import numpy as np

from .nonlinear_forms import NonLinearForm

# This block only runs for type checkers, not at runtime
if TYPE_CHECKING:
    from .hilbert_space import HilbertSpace, Vector
    from .linear_operators import LinearOperator


[docs] class LinearForm(NonLinearForm): """ Represents a linear form as an efficient, component-based functional. A `LinearForm` is an element of a dual `HilbertSpace` and is defined by its action on vectors from its `domain`. Internally, this action is represented by a component vector. This class provides optimized arithmetic operations and correctly defines the gradient (a constant vector) and the Hessian (the zero operator) for any linear functional. """ def __init__( self, domain: HilbertSpace, /, *, components: Optional[np.ndarray] = None, mapping: Optional[Callable[[Vector], float]] = None, parallel: bool = False, n_jobs: int = -1, ) -> None: """ Initializes the LinearForm from a mapping or component vector. A form must be defined by exactly one of two methods: 1. **components**: The explicit component vector representing the form. 2. **mapping**: A function `f(x)` that defines the form's action. The components will be automatically computed from this mapping. Args: domain: The Hilbert space on which the form is defined. components: The component representation of the form. mapping: The functional mapping `f(x)`. Used if `components` is None. parallel: Whether to use parallel computing components from the mapping. n_jobs: The number of jobs to use for parallel computing. Raises: AssertionError: If neither or both `mapping` and `components` are specified. Notes: Parallel options only relevant if the form is defined by a mapping. If both `components` and `mapping` are specified, `components` will take precedence. """ super().__init__( domain, self._mapping_impl, gradient=self._gradient_impl, hessian=self._hessian_impl, ) if components is None: if mapping is None: raise AssertionError("Neither mapping nor components specified.") self._compute_components(mapping, parallel, n_jobs) else: self._components: np.ndarray = components
[docs] @staticmethod def from_linear_operator(operator: "LinearOperator") -> LinearForm: """ Creates a LinearForm from an operator that maps to a 1D Euclidean space. """ from .hilbert_space import EuclideanSpace assert operator.codomain == EuclideanSpace(1) return LinearForm(operator.domain, mapping=lambda x: operator(x)[0])
@property def domain(self) -> HilbertSpace: """The Hilbert space on which the form is defined.""" return self._domain @property def components(self) -> np.ndarray: """ The component vector of the form. """ return self._components @property def as_linear_operator(self) -> "LinearOperator": """ Represents the linear form as a `LinearOperator`. The resulting operator maps from the form's original domain to a 1-dimensional `EuclideanSpace`, where the single component of the output is the scalar result of the form's action. """ from .hilbert_space import EuclideanSpace from .linear_operators import LinearOperator return LinearOperator( self.domain, EuclideanSpace(1), lambda x: np.array([self(x)]), dual_mapping=lambda y: y * self, )
[docs] def copy(self) -> LinearForm: """ Creates a deep copy of the linear form. """ return LinearForm(self.domain, components=self.components.copy())
def __neg__(self) -> LinearForm: """Returns the additive inverse of the form.""" return LinearForm(self.domain, components=-self._components) def __mul__(self, a: float) -> LinearForm: """Returns the product of the form and a scalar.""" return LinearForm(self.domain, components=a * self._components) def __rmul__(self, a: float) -> LinearForm: """Returns the product of the form and a scalar.""" return self * a def __truediv__(self, a: float) -> LinearForm: """Returns the division of the form by a scalar.""" return self * (1.0 / a) def __add__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm: """ Returns the sum of this form and another. If `other` is also a `LinearForm`, this performs an optimized, component-wise addition. Otherwise, it delegates to the general implementation in the `NonLinearForm` base class. Args: other: The form to add to this one. Returns: A `LinearForm` if adding two `LinearForm`s, otherwise a `NonLinearForm`. """ if isinstance(other, LinearForm): return LinearForm( self.domain, components=self.components + other.components ) else: return super().__add__(other) def __sub__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm: """ Returns the difference of this form and another. If `other` is also a `LinearForm`, this performs an optimized, component-wise subtraction. Otherwise, it delegates to the general implementation in the `NonLinearForm` base class. Args: other: The form to subtract from this one. Returns: A `LinearForm` if subtracting two `LinearForm`s, otherwise a `NonLinearForm`. """ if isinstance(other, LinearForm): return LinearForm( self.domain, components=self.components - other.components ) else: return super().__sub__(other) def __imul__(self, a: float) -> "LinearForm": """ Performs in-place scalar multiplication: self *= a. """ self._components *= a return self def __iadd__(self, other: "LinearForm") -> "LinearForm": """ Performs in-place addition with another form: self += other. """ if self.domain != other.domain: raise ValueError("Linear forms must share the same domain for addition.") self._components += other.components return self def __str__(self) -> str: """Returns the string representation of the form's components.""" return self.components.__str__() def _compute_components( self, mapping: Callable[[Any], float], parallel: bool, n_jobs: Optional[int], ): """Computes the component vector of the form, with an optional parallel backend.""" if not parallel: self._components = np.zeros(self.domain.dim) cx = np.zeros(self.domain.dim) for i in range(self.domain.dim): cx[i] = 1.0 x = self.domain.from_components(cx) self._components[i] = mapping(x) cx[i] = 0.0 else: def compute_one_component(i: int) -> float: """ Computes a single component for a given basis vector index. This function is sent to each parallel worker. """ # cx = np.zeros(self.domain.dim) # cx[i] = 1.0 # x = self.domain.from_components(cx) x = self.domain.basis_vector(i) return mapping(x) # Run the helper function in parallel for each dimension results = Parallel(n_jobs=n_jobs)( delayed(compute_one_component)(i) for i in range(self.domain.dim) ) self._components = np.array(results) def _mapping_impl(self, x: Vector) -> float: """ Maps a vector to its scalar value. """ return np.dot(self.components, self.domain.to_components(x)) def _gradient_impl(self, _: Vector) -> Vector: """ Computes the gradient of the form at a point. """ return self.domain.from_dual(self) def _hessian_impl(self, _: Vector) -> LinearOperator: """ Computes the Hessian of the form at a point. """ return self.domain.zero_operator()