Source code for lpspline.constraints.concavity
import cvxpy as cp
import numpy as np
from .base import Constraint
[docs]
class Concave(Constraint):
"""
Concavity constraint enforcing a negative second derivative globally or locally.
"""
def __init__(self, start: float = None, end: float = None) -> None:
"""
Initialize the Concavity constraint.
Parameters
----------
start : float, default=None
The domain starting coordinate to bound the constraint enforcement region.
end : float, default=None
The domain ending coordinate for the constraint region.
"""
self.start = start
self.end = end
[docs]
def build_constraint(self, s) -> list:
"""
Constructs the appropriate CVXPY concave formulations according to the basis type.
Parameters
----------
s : Spline
The parent Spline applying this restriction.
Returns
-------
list
A sequence containing formulation rules as CVXPY boolean expressions.
Raises
------
NotImplementedError
If the supplied Spline instance functionally lacks concavity restrictions.
"""
from ..spline import Linear, PiecewiseLinear, BSpline
variables = s._build_variables()
if isinstance(s, BSpline):
return self._constraint_BSpline(s)
elif isinstance(s, PiecewiseLinear):
return self._constraint_PiecewiseLinear(s)
else:
raise NotImplementedError(f"Concavity constraint not implemented for spline type '{type(s).__name__}'")
def _constraint_BSpline(self, s):
"""
Bounds sequential B-Splines evaluating directly on the recursive knot control points natively.
Parameters
----------
s : BSpline
B-spline formulation component.
Returns
-------
list
Resultant list enforcing sequential second-order numerical negative bounds.
"""
variables = s._build_variables()
constraints = []
M = len(s._by_classes) if s.by is not None else 1
dim_base = len(s.knots) + s.degree - 1
for c in range(M):
v_chunk = variables[:, c] if s.by is not None else variables
if self.start is not None and self.end is not None:
knots = np.array(s.knots)
indices = np.where((knots >= self.start) & (knots <= self.end))[0]
max_idx = len(knots) - 3
indices = indices[indices <= max_idx]
if len(indices) > 0:
constraints.extend([v_chunk[indices+2] - 2 * v_chunk[indices+1] + v_chunk[indices] <= 0])
else:
constraints.extend([v_chunk[2:] - 2 * v_chunk[1:-1] + v_chunk[:-2] <= 0])
return constraints
def _constraint_PiecewiseLinear(self, s):
"""
Enforces piecewise linear negative second differences tracking changes in slopes natively.
Parameters
----------
s : PiecewiseLinear
Approximation basis component.
Returns
-------
list
Formulations targeting piecewise basis differences targeting negative knot adjustments.
"""
variables = s._build_variables()
constraints = []
M = len(s._by_classes) if s.by is not None else 1
dim_base = 2 + len(s.knots)
for c in range(M):
v_chunk = variables[:, c] if s.by is not None else variables
if self.start is not None and self.end is not None:
knots = np.array(s.knots)
indices = np.where((knots >= self.start) & (knots <= self.end))[0]
if len(indices) > 0:
constraints.extend([v_chunk[indices+2] <= 0])
else:
constraints.extend([v_chunk[2:] <= 0])
return constraints