Source code for boundlab.poly.reciprocal
"""Reciprocal linearizer for polytope abstract interpretation."""
import torch
from . import PolyBounds, _register_linearizer
[docs]
@_register_linearizer("Reciprocal")
def reciprocal_linearizer(ub: torch.Tensor, lb: torch.Tensor) -> PolyBounds:
r"""CROWN relaxation of :math:`1/x` on a strictly positive domain.
``1/x`` is convex on :math:`(0, \infty)`:
- Upper envelope: secant through :math:`(\ell, 1/\ell)` and
:math:`(u, 1/u)`.
- Lower envelope: tangent at a point :math:`t_{\mathrm{opt}}`. The
minimum-area tangent point is :math:`\sqrt{\ell u}`; we additionally
clamp it to :math:`u/2 + 0.01` to keep the lower envelope strictly
positive (important when feeding softmax denominators).
"""
degen = torch.abs(ub - lb) < 1e-12
denom = (ub - lb).clamp(min=1e-30)
upper_lam = (1.0 / ub - 1.0 / lb) / denom
upper_bias = 1.0 / lb - upper_lam * lb
t_crit = torch.sqrt(ub.clamp(min=1e-30) * lb.clamp(min=1e-30))
t_opt = torch.maximum(t_crit, 0.5 * ub + 0.01)
lower_lam = -1.0 / (t_opt * t_opt)
lower_bias = 2.0 / t_opt # 1/t − lower_lam · t == 1/t + 1/t
exact_lam = -1.0 / (lb * lb)
exact_bias = 1.0 / lb - exact_lam * lb
upper_lam = torch.where(degen, exact_lam, upper_lam)
upper_bias = torch.where(degen, exact_bias, upper_bias)
lower_lam = torch.where(degen, exact_lam, lower_lam)
lower_bias = torch.where(degen, exact_bias, lower_bias)
return PolyBounds(
upper_lam=upper_lam,
upper_bias=upper_bias,
lower_lam=lower_lam,
lower_bias=lower_bias,
)