Source code for pygeoinf.nonlinear_forms
"""
Provides the `NonLinearForm` base class to represent non-linear functionals.
A non-linear form, or functional, is a mapping from a vector in a Hilbert
space to a scalar. This class provides a foundational structure for these
functionals, equipping them with algebraic operations and an interface for
derivatives like gradients and Hessians.
For non-smooth convex functions, the class also supports subgradients,
which generalize gradients to non-differentiable points.
"""
from __future__ import annotations
from typing import Callable, Optional, Any, TYPE_CHECKING
# This block only runs for type checkers, not at runtime
if TYPE_CHECKING:
from .hilbert_space import HilbertSpace, Vector
from .linear_forms import LinearForm
from .linear_operators import LinearOperator
from .nonlinear_operators import NonLinearOperator
[docs]
class NonLinearForm:
"""
Represents a general non-linear functional that maps vectors to scalars.
This class serves as the foundation for all forms. It defines the basic
callable interface `form(x)` and overloads arithmetic operators
(`+`, `-`, `*`) to create new forms. It also provides an optional
framework for specifying a form's gradient, Hessian, and subgradient.
For smooth functions, use gradient and hessian.
For non-smooth convex functions, use subgradient.
"""
def __init__(
self,
domain: HilbertSpace,
mapping: Callable[[Vector], float],
/,
*,
gradient: Optional[Callable[[Vector], Vector]] = None,
subgradient: Optional[Callable[[Vector], Vector]] = None,
hessian: Optional[Callable[[Vector], LinearOperator]] = None,
) -> None:
"""
Initializes the NonLinearForm.
Args:
domain: The Hilbert space on which the form is defined.
mapping: The function `f(x)` that defines the action of the form.
gradient: An optional function that computes the gradient of
the form.
subgradient: An optional function that computes a subgradient
of the form. For non-smooth convex functions, this returns
an element of the subdifferential ∂f(x). If both gradient
and subgradient are provided, gradient is preferred for
smooth optimization algorithms.
hessian: An optional function that computes the Hessian of
the form.
"""
self._domain: HilbertSpace = domain
self._mapping = mapping
self._gradient = gradient
self._subgradient = subgradient
self._hessian = hessian
@property
def domain(self) -> HilbertSpace:
"""The Hilbert space on which the form is defined."""
return self._domain
@property
def has_gradient(self) -> bool:
"""True if the form has a gradient."""
return self._gradient is not None
@property
def has_hessian(self) -> bool:
"""True if the form has a Hessian."""
return self._hessian is not None
@property
def has_subgradient(self) -> bool:
"""True if the form has a subgradient."""
return self._subgradient is not None
def __call__(self, x: Any) -> float:
"""Applies the linear form to a vector."""
return self._mapping(x)
[docs]
def gradient(self, x: Any) -> Vector:
"""
Computes the gradient of the form at a given point.
Args:
x: The vector at which to evaluate the gradient.
Returns:
The gradient of the form as a vector in the domain space.
Raises:
NotImplementedError: If a gradient function was not provided
during initialization.
"""
if self._gradient is None:
raise NotImplementedError(
"Gradient not implemented for this form."
)
return self._gradient(x)
[docs]
def derivative(self, x: Vector) -> LinearForm:
"""
Computes the derivative of the form at a given point.
Args:
x: The vector at which to evaluate the derivative.
Returns:
The derivative of the form as a `LinearForm`.
Raises:
NotImplementedError: If a gradient function was not provided
during initialization.
"""
return self.domain.to_dual(self.gradient(x))
[docs]
def hessian(self, x: Any) -> LinearOperator:
"""
Computes the Hessian of the form at a given point.
Args:
x: The vector at which to evaluate the Hessian.
Returns:
The Hessian of the form as a LinearOperator mapping the
domain to itself.
Raises:
NotImplementedError: If a Hessian function was not provided
during initialization.
"""
if self._hessian is None:
raise NotImplementedError("Hessian not implemented for this form.")
return self._hessian(x)
[docs]
def subgradient(self, x: Any) -> Vector:
"""
Computes a subgradient of the form at a given point.
For convex functions, a subgradient g ∈ ∂f(x) satisfies:
f(y) ≥ f(x) + ⟨g, y - x⟩ for all y
At points where the function is differentiable, the subgradient
equals the gradient. At non-smooth points, there may be multiple
subgradients; this method returns one of them.
Args:
x: The vector at which to evaluate the subgradient.
Returns:
A subgradient of the form as a vector in the domain space.
Raises:
NotImplementedError: If a subgradient function was not provided
during initialization.
"""
if self._subgradient is None:
raise NotImplementedError(
"Subgradient not implemented for this form."
)
return self._subgradient(x)
def __neg__(self) -> NonLinearForm:
"""Returns the additive inverse of the form."""
if self._gradient is None:
gradient = None
else:
def gradient(x: Vector) -> Vector:
return self.domain.negative(self.gradient(x))
if self._subgradient is None:
subgradient = None
else:
def subgradient(x: Vector) -> Vector:
return self.domain.negative(self.subgradient(x))
if self._hessian is None:
hessian = None
else:
def hessian(x: Vector) -> LinearOperator:
return -self.hessian(x)
return NonLinearForm(
self.domain,
lambda x: -self(x),
gradient=gradient,
subgradient=subgradient,
hessian=hessian,
)
def __mul__(self, a: float) -> NonLinearForm:
"""Returns the product of the form and a scalar."""
if self._gradient is None:
gradient = None
else:
def gradient(x: Vector) -> Vector:
return self.domain.multiply(a, self.gradient(x))
if self._subgradient is None:
subgradient = None
else:
def subgradient(x: Vector) -> Vector:
return self.domain.multiply(a, self.subgradient(x))
if self._hessian is None:
hessian = None
else:
def hessian(x: Vector) -> LinearOperator:
return a * self.hessian(x)
return NonLinearForm(
self.domain,
lambda x: a * self(x),
gradient=gradient,
subgradient=subgradient,
hessian=hessian,
)
def __rmul__(self, a: float) -> NonLinearForm:
"""Returns the product of the form and a scalar."""
return self * a
def __truediv__(self, a: float) -> NonLinearForm:
"""Returns the division of the form by a scalar."""
return self * (1.0 / a)
def __add__(self, other: NonLinearForm) -> NonLinearForm:
"""Returns the sum of this form and another."""
if self._gradient is None or other._gradient is None:
gradient = None
else:
def gradient(x: Vector) -> Vector:
return self.domain.add(self.gradient(x), other.gradient(x))
if self._subgradient is None or other._subgradient is None:
subgradient = None
else:
def subgradient(x: Vector) -> Vector:
return self.domain.add(
self.subgradient(x), other.subgradient(x)
)
if self._hessian is None or other._hessian is None:
hessian = None
else:
def hessian(x: Vector) -> LinearOperator:
return self.hessian(x) + other.hessian(x)
return NonLinearForm(
self.domain,
lambda x: self(x) + other(x),
gradient=gradient,
subgradient=subgradient,
hessian=hessian,
)
def __sub__(self, other: NonLinearForm) -> NonLinearForm:
"""Returns the difference between this form and another."""
if self._gradient is None or other._gradient is None:
gradient = None
else:
def gradient(x: Vector) -> Vector:
return self.domain.subtract(
self.gradient(x), other.gradient(x)
)
if self._subgradient is None or other._subgradient is None:
subgradient = None
else:
def subgradient(x: Vector) -> Vector:
return self.domain.subtract(
self.subgradient(x), other.subgradient(x)
)
if self._hessian is None or other._hessian is None:
hessian = None
else:
def hessian(x: Vector) -> LinearOperator:
return self.hessian(x) - other.hessian(x)
return NonLinearForm(
self.domain,
lambda x: self(x) - other(x),
gradient=gradient,
subgradient=subgradient,
hessian=hessian,
)
def __matmul__(self, other: NonLinearOperator) -> NonLinearForm:
"""
Composes this form with a non-linear operator: `(self @ other)(x) = self(other(x))`.
The gradient is computed using the adjoint of the operator's Fréchet derivative:
grad(F o G)(x) = G'(x)* (grad_F(G(x)))
"""
domain = other.domain
def mapping(x: Any) -> float:
return self(other(x))
if self.has_gradient and other.has_derivative:
def gradient(x: Vector) -> Vector:
inner_val = other(x)
grad_f = self.gradient(inner_val)
D_g = other.derivative(x)
return D_g.adjoint(grad_f)
else:
gradient = None
return NonLinearForm(domain, mapping, gradient=gradient)