Source code for decent_bench.costs._base._quadratic_cost

from __future__ import annotations

from functools import cached_property

import numpy as np
from numpy import float64
from numpy.typing import NDArray

import decent_bench.utils.interoperability as iop
from decent_bench.costs._base._cost import Cost
from decent_bench.utils.array import Array
from decent_bench.utils.types import SupportedDevices, SupportedFrameworks


[docs] class QuadraticCost(Cost): r""" Quadratic cost function. .. math:: f(\mathbf{x}) = \frac{1}{2} \mathbf{x}^T \mathbf{Ax} + \mathbf{b}^T \mathbf{x} + c """
[docs] def __init__( self, A: Array, # noqa: N803 b: Array, c: float = 0, ): self.A: NDArray[float64] = iop.to_numpy(A) self.b: NDArray[float64] = iop.to_numpy(b) if self.A.ndim != 2: raise ValueError("Matrix A must be 2D") if self.A.shape[0] != self.A.shape[1]: raise ValueError("Matrix A must be square") if self.b.ndim != 1: raise ValueError("Vector b must be 1D") if self.A.shape[0] != self.b.shape[0]: raise ValueError(f"Dimension mismatch: A has shape {self.A.shape} but b has length {self.b.shape[0]}") self.A_sym = 0.5 * (self.A + self.A.T) self.c = c
@property def shape(self) -> tuple[int, ...]: return self.b.shape @property def framework(self) -> SupportedFrameworks: return SupportedFrameworks.NUMPY @property def device(self) -> SupportedDevices: return SupportedDevices.CPU
[docs] @cached_property def m_smooth(self) -> float: # pyright: ignore[reportIncompatibleMethodOverride] r""" The cost function's smoothness constant. .. math:: \max_{i} \left| \lambda_i \right| where :math:`\lambda_i` are the eigenvalues of :math:`\frac{1}{2} (\mathbf{A}+\mathbf{A}^T)`. For the general definition, see :attr:`Cost.m_smooth <decent_bench.costs.Cost.m_smooth>`. """ eigs = np.linalg.eigvalsh(self.A_sym) return float(np.max(np.abs(eigs)))
[docs] @cached_property def m_cvx(self) -> float: # pyright: ignore[reportIncompatibleMethodOverride] r""" The cost function's convexity constant. .. math:: \begin{array}{ll} \min_i \lambda_i, & \text{if } \min_i \lambda_i > 0, \\ 0, & \text{if } \min_i \lambda_i = 0, \\ \text{NaN}, & \text{if } \min_i \lambda_i < 0 \end{array} where :math:`\lambda_i` are the eigenvalues of :math:`\frac{1}{2} (\mathbf{A}+\mathbf{A}^T)`. For the general definition, see :attr:`Cost.m_cvx <decent_bench.costs.Cost.m_cvx>`. """ eigs = np.linalg.eigvalsh(self.A_sym) l_min = float(np.min(eigs)) tol = 1e-12 if l_min > tol: return l_min if abs(l_min) <= tol: return 0 return np.nan
[docs] @iop.autodecorate_cost_method(Cost.function) def function(self, x: NDArray[float64]) -> float: r""" Evaluate function at x. .. math:: \frac{1}{2} \mathbf{x}^T \mathbf{Ax} + \mathbf{b}^T \mathbf{x} + c """ return float(0.5 * x.dot(self.A.dot(x)) + self.b.dot(x) + self.c)
[docs] @iop.autodecorate_cost_method(Cost.gradient) def gradient(self, x: NDArray[float64]) -> NDArray[float64]: r""" Gradient at x. .. math:: \frac{1}{2} (\mathbf{A}+\mathbf{A}^T)\mathbf{x} + \mathbf{b} """ return self.A_sym @ x + self.b
[docs] @iop.autodecorate_cost_method(Cost.hessian) def hessian(self, x: NDArray[float64]) -> NDArray[float64]: # noqa: ARG002 r""" Hessian at x. .. math:: \frac{1}{2} (\mathbf{A}+\mathbf{A}^T) """ ret: NDArray[float64] = self.A_sym.copy() return ret
[docs] @iop.autodecorate_cost_method(Cost.proximal) def proximal(self, x: NDArray[float64], penalty: float) -> NDArray[float64]: r""" Proximal at x. .. math:: (\frac{\rho}{2} (\mathbf{A} + \mathbf{A}^T) + \mathbf{I})^{-1} (\mathbf{x} - \rho \mathbf{b}) where :math:`\rho > 0` is the penalty. This is a closed form solution, see :meth:`Cost.proximal() <decent_bench.costs.Cost.proximal>` for the general proximal definition. """ lhs = penalty * self.A_sym + np.eye(self.A.shape[1]) rhs = x - self.b * penalty return np.asarray(np.linalg.solve(lhs, rhs), dtype=float64)
[docs] def __add__(self, other: Cost) -> Cost: """Add another cost function.""" self._validate_cost_operation(other) if isinstance(other, QuadraticCost): return QuadraticCost( A=iop.to_array(self.A + other.A, self.framework, self.device), b=iop.to_array(self.b + other.b, self.framework, self.device), c=self.c + other.c, ) return super().__add__(other)
def __sub__(self, other: Cost) -> Cost: """ Subtract another cost function. Preserves :class:`QuadraticCost` when subtracting another quadratic cost. """ self._validate_cost_operation(other) if isinstance(other, QuadraticCost): return self.__add__( QuadraticCost( A=iop.to_array(-other.A, self.framework, self.device), b=iop.to_array(-other.b, self.framework, self.device), c=-other.c, ) ) return super().__sub__(other)