Source code for waxmorph.jax.losses

"""JAX losses for ordered arrays and unordered point clouds.

``squared_loss`` uses row correspondence; Chamfer and Sinkhorn support unordered clouds.
The OTT factory provides debiased Sinkhorn divergence; PyTorch also provides GeomLoss
families.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

import jax.numpy as jnp

from waxmorph._validation import _validate_chamfer_shapes, _validate_same_shape


[docs] def squared_loss(X_pred: jnp.ndarray, X_target: jnp.ndarray) -> jnp.ndarray: r"""Squared Frobenius norm for row-aligned arrays of equal shape. Use :func:`chamfer_distance` or :func:`make_sinkhorn_loss` for unordered rows. .. math:: \mathcal{L} = \lVert X^f - X^T \rVert_F^2 Raises: ValueError: If the input shapes differ. Examples: >>> x = jnp.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) >>> y = jnp.array([[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]) >>> print(float(squared_loss(x, y))) 1.0 """ _validate_same_shape("squared_loss", X_pred.shape, X_target.shape) return jnp.sum((X_pred - X_target) ** 2)
[docs] def chamfer_distance(X_pred: jnp.ndarray, X_target: jnp.ndarray) -> jnp.ndarray: r"""Two directional mean distances between unordered, possibly unequal clouds. Euclidean nearest-neighbor terms provide permutation invariance and exchange symmetry: .. math:: \mathcal{L} = \frac{1}{N}\sum_i \min_j \lVert X^f_i - X^T_j \rVert + \frac{1}{M}\sum_j \min_i \lVert X^f_i - X^T_j \rVert JAX splits a ``min`` cotangent equally among tied minima; the derivative remains nonsmooth because the tied set changes under perturbation. Coincident distances use a finite zero-gradient branch. Raises: ValueError: If either cloud is empty, has another rank, or has a different width. Examples: >>> x = jnp.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) >>> y = jnp.array([[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]) >>> print(float(chamfer_distance(x, y))) 1.0 """ _validate_chamfer_shapes(X_pred.shape, X_target.shape) diff = X_pred[:, None, :] - X_target[None, :, :] sq = jnp.sum(diff * diff, axis=-1) dist = jnp.where(sq > 0.0, jnp.sqrt(jnp.where(sq > 0.0, sq, 1.0)), 0.0) return dist.min(axis=1).mean() + dist.min(axis=0).mean()
[docs] def make_sinkhorn_loss( blur: float = 0.05, *, p: int = 2, cost_fn: Any | None = None, **solve_kwargs: Any, ) -> Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray]: r"""Build OTT's debiased entropic-OT divergence for unordered clouds. .. math:: S_\varepsilon(\alpha,\beta)=\mathrm{OT}_\varepsilon(\alpha,\beta) -\tfrac12\mathrm{OT}_\varepsilon(\alpha,\alpha) -\tfrac12\mathrm{OT}_\varepsilon(\beta,\beta). ``blur`` sets ``epsilon = blur ** p``. With ``cost_fn=None``, ``p=1`` selects Euclidean cost and ``p=2`` selects half squared Euclidean cost; other exponents use an explicit cost. An explicit cost replaces that selection, while ``p`` still sets the epsilon exponent. ``solve_kwargs`` are forwarded to OTT. The factory provides debiased Sinkhorn divergence with OTT options. PyTorch's GeomLoss factory additionally provides MMD, Hausdorff, ``scaling``, and ``reach`` contracts. Numerical values follow each solver's options. OTT is imported lazily. Raises: ImportError: If importing ``ott-jax`` fails. """ from ott.geometry import costs, pointcloud from ott.tools import sinkhorn_divergence as sd if cost_fn is None: if p == 1: cost_fn = costs.Euclidean() elif p == 2: cost_fn = costs.PNormP(2) else: raise ValueError( f"make_sinkhorn_loss maps only p in {{1, 2}} to a built-in OTT " f"cost (got p={p!r}); pass an explicit `cost_fn` for other " "exponents." ) def loss_fn(X_pred: jnp.ndarray, X_target: jnp.ndarray) -> jnp.ndarray: return sd.sinkhorn_divergence( pointcloud.PointCloud, X_pred, X_target, cost_fn=cost_fn, epsilon=blur**p, solve_kwargs=solve_kwargs, )[0] return loss_fn