Source code for sdplab.regularization.entropy
# Copyright 2026 Pavlo Pelikh
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from spacecore import jax_pytree_class, DenseArray
from ._base import NEG_EIG_TOL, Regularizer
[docs]
@jax_pytree_class
class EntropyReg(Regularizer):
r"""Entropy regularizer, :math:`\varphi(t) = t(\log t - 1)`.
Its conjugate is the exponential :math:`\psi(s) = e^{s}`. The *free*
smoothed dual term (``legendre(..., normalized=False)``) is the separable
:math:`\varepsilon \sum_i e^{s_i/\varepsilon}`, which is **unbounded**
away from dual feasibility and overflows at small :math:`\varepsilon`. Set
``normalized=True`` (on the regularizer methods, or on
:class:`~sdplab.regularization.BoundDualFunctional`) to select the
fixed-trace form: the globally bounded log-partition
:math:`\varepsilon \log \operatorname{Tr}\exp(X/\varepsilon)` with the
unit-trace Gibbs-state gradient. Because :math:`\log\psi'(x) = x` is affine
here, that fixed-trace form is the *exact* log-partition dual — the target
for the entropy dual at small :math:`\varepsilon`.
"""
[docs]
def phi(self, x: DenseArray) -> DenseArray:
r"""Return :math:`x(\log x - 1)` on :math:`x\ge0`, else :math:`+\infty`.
Round-off-negative eigenvalues (down to ``-NEG_EIG_TOL``) evaluate at
the limit :math:`\varphi(0)=0` rather than out of domain.
"""
ops = self.ops
safe_x = ops.where(x > 0., x, 1.)
positive_values = safe_x * (ops.log(safe_x) - 1.)
return ops.where(
x > 0.,
positive_values,
ops.where(x >= -NEG_EIG_TOL, 0., float("inf")),
)
[docs]
def phi_star(self, x: DenseArray) -> DenseArray:
r"""Return :math:`\psi(x) = \exp(x)` elementwise."""
ops = self.ctx.ops
return ops.exp(x)
[docs]
def log_phi_star_prime(self, x: DenseArray) -> DenseArray:
r"""Return :math:`\log(\psi'(x))` elementwise.
For entropy regularization, :math:`\psi'(x) = \exp(x)`, hence
:math:`\log(\psi'(x)) = x`.
"""
return x
[docs]
def phi_star_prime(self, x: DenseArray) -> DenseArray:
r"""Return :math:`\psi'(x) = \exp(x)` elementwise."""
ops = self.ctx.ops
return ops.exp(x)
def _normalized_legendre(self, scaled: DenseArray, val: float) -> DenseArray:
r"""Return the fixed-trace conjugate
:math:`\varepsilon\big(\log\operatorname{Tr}e^{S/\varepsilon} + 1\big)`.
Because :math:`\log\psi'(x) = x` is affine, the chemical potential is
available in closed form: :math:`\sum_i e^{(s_i-\theta)/\varepsilon} = 1`
gives :math:`\theta = \varepsilon\log\sum_i e^{s_i/\varepsilon}`, and
substituting into
:math:`\theta + \varepsilon\operatorname{Tr}\psi((S-\theta)/\varepsilon)`
leaves the trailing term equal to :math:`\varepsilon` exactly. That
constant is what makes this the conjugate of the *same*
:math:`\varphi(t) = t(\log t - 1)` that :meth:`phi` uses -- the
:math:`-t` contributes :math:`-\varepsilon` on the unit-trace face -- so
primal and dual values here form a Fenchel pair and duality gaps come
out right. It does not affect the gradient, which is the Gibbs state
:math:`\operatorname{softmax}(S/\varepsilon)`.
"""
return (self._log_partition(scaled) + 1.0) * val