Source code for rlaopt.atoms.halfspace
"""Halfspace constraint atom for optimization."""
import torch
from rlaopt.atoms.polyhedron import Polyhedron
from rlaopt.expression import Variable
from rlaopt.ext_tensordict import TensorDict
[docs]
class Halfspace(Polyhedron):
"""Halfspace constraint atom representing a linear inequality.
A halfspace constraint restricts a variable to satisfy a linear inequality:
c^T x <= upper, which defines a half-space in the variable space.
This is a special case of Polyhedra with a single linear inequality
constraint, but with an efficient closed-form proximal operator (projection
onto the halfspace).
Args:
x: Variable to constrain.
c: Normal vector defining the halfspace orientation.
upper: Upper bound for the linear form c^T x.
Examples:
>>> # Constraint: c^T x <= 1
>>> x = Variable((5,), name='x')
>>> c = torch.randn(5)
>>> halfspace = Halfspace(x, c=c, upper=1.0)
>>> # Non-negativity for first coordinate: x[0] >= 0
>>> # Rewritten as: -x[0] <= 0, so c = [-1, 0, 0, ...], upper = 0
>>> c = torch.zeros(5)
>>> c[0] = -1.0
>>> nonneg = Halfspace(x, c=c, upper=torch.tensor(0.0))
>>> # Use proximal operator for projection
>>> violating_point = torch.randn(5)
>>> projected = halfspace.prox(violating_point, prox_scaling=1.0)
"""
[docs]
def __init__(self, x: Variable, c: torch.Tensor, upper: float):
"""Initialize the halfspace constraint atom.
Args:
x: Variable to constrain.
c: Normal vector defining the halfspace orientation.
upper: Upper bound for the linear form c^T x.
"""
super().__init__(x, A=None, b=None, C=c, lower=None, upper=upper)
[docs]
def is_proxable(self) -> bool:
"""Check if the halfspace constraint has a computable proximal operator.
Returns:
bool: Always True, as halfspace constraints have a closed-form
proximal operator (projection onto the halfspace).
"""
return True
def _prox(
self, relevant_variable_values: TensorDict, prox_scaling: float
) -> TensorDict:
"""Compute the proximal operator of the halfspace constraint.
The proximal operator projects onto the halfspace by moving the point
perpendicular to the boundary until it satisfies c^T x <= upper.
"""
c = self.get_buffer("C")
upper = self.get_buffer("upper")
def project_onto_halfspace(x: torch.Tensor) -> torch.Tensor:
r = torch.dot(c, x) - upper
# x is feasible
if r <= 0:
return x
# x is infeasible
else:
c_norm = torch.linalg.norm(c, 2)
return x - torch.nn.functional.relu(r) * c / c_norm**2
return relevant_variable_values.apply(project_onto_halfspace)