Source code for boundlab.zono.exp
"""Exp linearizer for zonotope abstract interpretation.
Implements the DeepT minimal-area relaxation for the exponential function.
"""
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("Exp")
def exp_linearizer(ub: torch.Tensor, lb: torch.Tensor) -> ZonoBounds:
"""Minimal-area exp relaxation (DeepT, Section 4.5).
The returned zonotope ``y = slope·x + mu + beta·ε`` (with ε ∈ [-1, 1])
over-approximates ``exp(x)`` on ``[lb, ub]`` and guarantees
``mu - beta ≥ 0`` so downstream handlers (``reciprocal`` / softmax)
never see a non-positive lower bound.
Strategy:
- For each element, try the single-slope "minimal area" relaxation:
tangent at ``t_opt`` as the lower envelope, parallel secant line
through ``(ub_c, exp(ub_c))`` as the upper envelope.
- If the tangent gives a non-positive offset (e.g. ``lb > 0``) or the
fp32 precision would lose that offset against ``exp(ub)``, fall back
to the interval relaxation ``slope = 0``, ``mu = beta = exp(ub)/2``,
which yields ``[0, exp(ub)]`` — loose but guaranteed ``mu - beta = 0``
bit-exactly (so downstream ``ublb`` cannot go negative).
- Clamp ``lb``/``ub`` to ±30 to keep ``exp`` numerically safe; the
underflow/overflow branches use the interval fallback.
Examples
--------
>>> import torch
>>> import boundlab.expr as expr
>>> from boundlab.zono.exp import exp_linearizer
>>> x = expr.ConstVal(torch.tensor([0.0])) + 0.1 * expr.LpEpsilon([1])
>>> ub, lb = x.ublb()
>>> b = exp_linearizer(ub, lb)
>>> b.bias.shape
torch.Size([1])
"""
exp_lb = torch.exp(lb)
exp_ub = torch.exp(ub)
slope = (exp_ub - exp_lb) / (ub - lb)
slope = torch.where(torch.isfinite(slope), slope, torch.exp((ub + lb) / 2))
# assert torch.isfinite(slope).all(), f"Expected finite slope point for exp linearizer {slope.max().item()} {ub.max().item()} {lb.max().item()}"
slope_point = torch.log(slope)
# assert torch.isfinite(slope_point).all(), f"Expected finite slope point for exp linearizer {slope_point.max().item()}"
U = torch.max(exp_ub - slope * ub, exp_lb - slope * lb)
L = slope * (1 - slope_point)
# assert torch.isfinite(U).all() and torch.isfinite(L).all(), "Expected finite envelopes for exp linearizer"
beta = (U - L) / 2
mu = (U + L) / 2
# mu = torch.where(large_cases, torch.inf, mu)
error_op = EinsumOp.from_hardmard(beta, len(ub.shape))
return ZonoBounds(bias=mu, error_coeffs=error_op, input_weights=[slope])