Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Numerical mechanics of bijectors

Jitter, clamping, mixed-precision log-dets, and the round-trip test that keeps a deep destructor honest

05 — Numerical mechanics of bijectors

Gaussianization composes the map z=Φ−1(F(x))z = \Phi^{-1}(F(x)) hundreds of times. Each piece is innocent on paper, but in floating point the pipeline is a minefield: F(x)F(x) lives in [0,1][0, 1], and Φ−1(0)=−∞\Phi^{-1}(0) = -\infty, Φ−1(1)=+∞\Phi^{-1}(1) = +\infty. One saturated CDF value and a whole sample goes to infinity; a few thousand summed log-dets and the density quietly drifts. This notebook is the engineering that keeps the elegant math of notebooks 00–04 actually runnable.

What you will see

1. Where the round-trip error lives

In notebook 02 we saw a gauss_flows mixture-CDF round-trip to ~10-14, while rbig’s empirical-CDF marginal was far looser. The difference is entirely in the tails. An empirical CDF saturates — it returns values of (essentially) 0 and 1 beyond the observed data range — and Φ−1\Phi^{-1} maps those to ±∞\pm\infty, which no inverse can walk back. A smooth mixture CDF (here a logistic-mixture marginal with a robust inverse) never quite reaches 0 or 1, so it stays invertible across the data range. Let’s localise the error against ∣x∣|x|.

empirical-CDF (rbig)   max round-trip err = 1.64e-01
   in tails |x|>3: 9.84e-03   in bulk |x|<1: 1.31e-04
mixture-CDF (gauss_flows) max round-trip err = 2.02e-13
<Figure size 750x420 with 1 Axes>

The empirical-CDF error (blue) climbs by orders of magnitude as we move into the tail, while the smooth mixture CDF (green) stays at the float64 floor everywhere. The lesson: the marginal estimator’s tail behaviour is the whole ballgame for invertibility — which is why parametric flows prefer smooth CDFs (mixtures, splines) over a raw ECDF.

2. Jitter: clamping the CDF before the probit

Even a smooth CDF can round to exactly 0 or 1 in float arithmetic far enough into the tail. The standard guard is jitter: clamp the CDF into [ε,1−ε][\varepsilon, 1-\varepsilon] before applying Φ−1\Phi^{-1},

z=Φ−1 ⁣(clip(F(x), ε, 1−ε)).z = \Phi^{-1}\!\big(\mathrm{clip}(F(x),\,\varepsilon,\,1-\varepsilon)\big).

This caps the latent at ∣z∣≤Φ−1(1−ε)|z| \le \Phi^{-1}(1-\varepsilon), trading a tiny tail bias for guaranteed finiteness. Pick ε\varepsilon too small and you risk ±∞\pm\infty; too large and you flatten the tails. Watch the trade-off.

unclamped (eps=0): any non-finite latent? True
<Figure size 1100x420 with 2 Axes>

Left: smaller ε\varepsilon lets the latent reach further out (less tail bias) but the unclamped ε=0\varepsilon=0 curve runs to ±∞\pm\infty. Right: the jitter sets a hard ceiling on the latent — ε=10−3\varepsilon=10^{-3} caps ∣z∣|z| at ~3.1, while ε=10−7\varepsilon=10^{-7} allows ~5.2. gauss_flows applies this clamp inside its marginal bijectors, which is why its round-trips in §1 stayed finite.

3. Mixed precision: accumulating log-dets over deep stacks

A deep destructor sums one log-det per layer (notebook 01). Over hundreds of layers those sums grow large, and float32 — with ~7 significant digits — starts to lose the low-order bits. float64 (~16 digits) holds. The forward maps can run in float32 for speed, but the log-det accumulator should be float64. We sum the same per-layer log-det KK times in each precision and watch them diverge.

  K=    1 layers: |float32 - float64| log-det drift = 4.5137e-08
  K=   10 layers: |float32 - float64| log-det drift = 1.8819e-06
  K=  100 layers: |float32 - float64| log-det drift = 1.3097e-05
  K= 1000 layers: |float32 - float64| log-det drift = 1.0214e-02
  K=10000 layers: |float32 - float64| log-det drift = 1.3263e+00
<Figure size 700x400 with 1 Axes>

The drift grows with depth — by K=104K=10^4 layers the float32 accumulator is off by a non-trivial amount in the log-density, which corrupts likelihood comparisons and training gradients. The fix is free: keep the log-det accumulator in float64 (jax.config.update("jax_enable_x64", True), set at the top of every notebook in this series).

4. The round-trip invertibility test

A bijector’s defining contract is T−1(T(x))=xT^{-1}(T(x)) = x. In code that becomes a test with a tolerance: ∥T−1(T(x))−x∥∞<τ\lVert T^{-1}(T(x)) - x\rVert_\infty < \tau. Every flow layer should ship with one — it is the cheapest guard against a broken inverse, a missing clamp, or a precision bug. Here is the parametrised check applied across gauss_flows bijectors (the pattern that lives in tests/test_flow.py), with rbig’s empirical-CDF marginal included as a cautionary baseline.

round-trip max error (tolerance tau = 1e-05):
  PASS  MixtureGaussianCDF      8.91e-14
  PASS  MixtureLogisticCDF      9.24e-14
  PASS  RQSplineMarginal        3.33e-16
  PASS  HouseholderRotation     7.11e-15
  FAIL  rbig ECDF marginal      4.10e-01
<Figure size 800x400 with 1 Axes>

All four gauss_flows bijectors pass at τ=10−5\tau = 10^{-5} with room to spare (errors near the float64 floor); only rbig’s empirical-CDF marginal fails, on its tail samples from §1 — and that failure is fundamental (an ECDF genuinely has no information beyond the data range), not a bug to fix.

That MixtureGaussianCDF passes here is itself a small case study in why this test matters. An earlier draft of this notebook caught it failing — a too-narrow bisection bracket that could not walk back tail samples — which we filed as gauss_flows#108; it was fixed in gauss_flows 0.1.7, and the green bar above is that fix landing. A round-trip test run in CI on random inputs is exactly what surfaces such bugs and lets you then stack a hundred layers and trust the composition.

Recap

hazardsymptomfix
Φ−1(0/1)=±∞\Phi^{-1}(0/1) = \pm\inftytail samples explode / non-invertiblesmooth CDF + jitter clamp
jitter too small / largeoverflow / tail biastune ε\varepsilon (e.g. 10-6)
float32 log-det sumdensity drifts with depthfloat64 accumulator (jax_enable_x64)
broken inversesilent wrong samples/densitiesround-trip test <τ<\tau in CI

Next up. We can now build and trust a destructor — but how do we measure whether it actually reached N(0,I)\mathcal{N}(0, I)? 06 — Gaussianity diagnostics covers QQ-plots, skew/kurtosis, negentropy as a convergence signal, and multivariate normality tests.