Source code for boundlab.zono.reciprocal

"""Reciprocal linearizer for zonotope abstract interpretation.

Implements the DeepT minimal-area relaxation for the reciprocal function (1/x).
"""

import torch

from boundlab.linearop._base import LinearOpFlags
from boundlab.linearop._einsum import EinsumOp
from boundlab.linearop._indices import SetIndicesOp

from . import ZonoBounds, _register_linearizer


[docs] @_register_linearizer("Reciprocal") def reciprocal_linearizer(ub: torch.Tensor, lb: torch.Tensor) -> ZonoBounds: """Minimal-area reciprocal relaxation with positive-output constraint. Matches DeepT (Bonaert et al., 2021, Section 4.6) with the ``y_positive_constraint`` that prevents bound explosion when the input lower bound is near zero (as happens inside softmax when ``sum(exp(x_j - x_i))`` has a loose lower bound). The key change from the naive relaxation: the tangent point is clamped to ``t_opt >= ub/2 + 0.01``, ensuring the reciprocal output remains strictly positive and the relaxation stays tight. """ output_shape = ub.shape # Clamp to positive # lb = torch.clamp(lb, min=1e-9) # ub = torch.clamp(ub, min=lb + 1e-12) degen = torch.abs(ub - lb) < 1e-12 # Optimal tangent point: geometric mean minimizes area, but # clamp to ub/2 + 0.01 to ensure strictly positive output t_crit = torch.sqrt(ub * lb) t_crit2 = 0.5 * ub + 0.01 t_opt = torch.maximum(t_crit, t_crit2) slope = -1.0 / (t_opt ** 2) val_at_t = 1.0 / t_opt # Lower bound line (tangent at t_opt) c_lower = val_at_t - slope * t_opt # = 2/t_opt # Upper bound line: connects through whichever endpoint gives # the tighter (lower) intercept c_upper = torch.maximum(1.0 / lb - slope * lb, 1.0 / ub - slope * ub) mu = 0.5 * (c_upper + c_lower) beta = 0.5 * (c_upper - c_lower) slope = torch.where(degen, torch.zeros_like(slope), slope) mu = torch.where(degen, 1.0 / lb, mu) beta = torch.where(degen, torch.zeros_like(beta), torch.abs(beta)) error_op = EinsumOp.from_hardmard(beta, len(ub.shape)) return ZonoBounds(bias=mu, error_coeffs=error_op, input_weights=[slope])