From e7eab0b2bf16e0fbb7876f929ff43b48886168cd Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 22:35:16 +0100 Subject: [PATCH 1/5] Use A42 instead of ITP in find_alpha Roots 3.0.2 changed `decide_convergence` for bracketing methods to return `NaN` (raising `ConvergenceFailed`) when the algorithm terminates in a `:not_converged` state, instead of returning the best endpoint. `Roots.ITP` can stall a few floats short of the (unreachable, `eps^3`) x-tolerance and hits exactly this path, so `find_alpha` errored for many inputs. `Roots.A42()` (Alefeld-Potra-Shi) is superlinear like ITP but narrows the bracket down to adjacent floats and converges via the exact-x path, so it works identically on both old and new Roots. The analytic AD rules are unchanged: they depend only on the returned value being a root of the same equation. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/bijectors/planar_layer.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bijectors/planar_layer.jl b/src/bijectors/planar_layer.jl index 08dfca51..02e68d68 100644 --- a/src/bijectors/planar_layer.jl +++ b/src/bijectors/planar_layer.jl @@ -172,10 +172,12 @@ function find_alpha(wt_y::T, wt_u_hat::T, b::T) where {T<:Real} return lower end - # Solve the root-finding problem - # A value of `κ₁ = 0.2 / (upper - lower)` is suggested - # Ref: https://docs.rs/kurbo/0.11.1/kurbo/common/fn.solve_itp.html - α0 = Roots.find_zero((lower, upper), Roots.ITP(; κ₁=inv(10 * Δ))) do α + # Solve the root-finding problem with the Alefeld-Potra-Shi method, which is + # superlinear and narrows the bracket down to adjacent floating point values. + # We used to use `Roots.ITP`, but its bracket can stall a few floats short of + # convergence, and as of Roots 3.0.2 `find_zero` then returns `NaN` instead of + # the best endpoint, causing a `ConvergenceFailed` error. + α0 = Roots.find_zero((lower, upper), Roots.A42()) do α return α + wt_u_hat * tanh(α + b) - wt_y end From c467de33b1dc4cd3ce9b0805605bd717286186dd Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 22:41:17 +0100 Subject: [PATCH 2/5] Bump version to 0.16.2 Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9776f2c8..516feb1f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Bijectors" uuid = "76274a88-744f-5084-9051-94815aaf08c4" -version = "0.16.1" +version = "0.16.2" [deps] AbstractPPL = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" From 6c64772d5a6f426881da05e899b7ff52a925db23 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 23:10:52 +0100 Subject: [PATCH 3/5] Resample non-finite draws in univariate interface tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Distributions 0.25.129's bounded kernel-density samplers (Biweight, Cosine, …) can return NaN, which propagated into the transform/logpdf/AD comparisons and failed the `Biweight` interface test. Draw via a `randfinite` helper that resamples until finite so the tests exercise Bijectors, not the upstream bug. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/interface.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/interface.jl b/test/interface.jl index bb40407b..c09a2122 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -6,6 +6,11 @@ using ForwardDiff using Bijectors Random.seed!(123) +# Distributions 0.25.129's bounded kernel-density samplers (Biweight, Cosine, …) can +# return NaN; resample until finite so we test Bijectors, not the upstream bug. +randfinite(d) = (x = rand(d); isfinite(x) ? x : randfinite(d)) +randfinite(d, n::Int) = (x = rand(d, n); all(isfinite, x) ? x : randfinite(d, n)) + @testset "Univariate" begin # Tests with scalar-valued distributions. uni_dists = [ @@ -44,20 +49,20 @@ Random.seed!(123) td = @inferred transformed(dist) # single sample - y = @inferred rand(td) + y = @inferred randfinite(td) x = @inferred inverse(td.transform)(y) @test y ≈ @inferred td.transform(x) @test @inferred(logpdf(td, y)) ≈ @inferred(logpdf_with_trans(dist, x, true)) # multi-sample - y = @inferred rand(td, 10) + y = @inferred randfinite(td, 10) x = inverse(td.transform).(y) @test logpdf.(td, y) ≈ logpdf_with_trans.(dist, x, true) # logpdf corresponds to logpdf_with_trans d = dist b = @inferred bijector(d) - x = rand(d) + x = randfinite(d) y = @inferred b(x) @test logpdf(d, inverse(b)(y)) + logabsdetjacinv(b, y) ≈ logpdf_with_trans(d, x, true) @@ -66,7 +71,7 @@ Random.seed!(123) # verify against AD d = dist b = bijector(d) - x = rand(d) + x = randfinite(d) y = b(x) # `ForwardDiff.derivative` can lead to some numerical inaccuracy, # so we use a slightly higher `atol` than default. From c0a0c3516b45a07267eb07560435e9a1f955dba3 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 23:13:01 +0100 Subject: [PATCH 4/5] Drop stale runic job name from Format workflow Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Format.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/Format.yml b/.github/workflows/Format.yml index e9c0966f..50c1f319 100644 --- a/.github/workflows/Format.yml +++ b/.github/workflows/Format.yml @@ -16,7 +16,6 @@ concurrency: jobs: check: - name: runic runs-on: ubuntu-latest steps: From 42f472c696ca12aa4630fe023943caa86daebae2 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 23:37:24 +0100 Subject: [PATCH 5/5] Resample non-finite/failed draws in VectorBijectors test_all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recent Distributions releases break some samplers: the bounded kernel-density distributions (Biweight, Triweight, Epanechnikov) return NaN, and Cosine's quantile-based sampler throws `Roots.ConvergenceFailed` under Roots 3. Both propagated into `test_all` (roundtrip, logjac, …) and failed the Vector and ReverseDiff suites. Draw samples via a `randfinite` helper that resamples until finite (retrying on NaN and on a thrown sampler error), matching the `randfinite` approach used in the univariate interface tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/vector/test_utils.jl | 43 +++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/vector/test_utils.jl b/src/vector/test_utils.jl index 60544d15..32c184f9 100644 --- a/src/vector/test_utils.jl +++ b/src/vector/test_utils.jl @@ -26,13 +26,32 @@ function _name(d::D.JointOrderStatistics) return "joint order statistic $(_name(d.dist)) with length $(length(d))" end +# Some Distributions samplers are broken in recent releases: the kernel-density ones +# (Biweight, Triweight, Epanechnikov) return NaN, and Cosine's quantile-based sampler throws +# under Roots 3. Resample until we get a finite draw so `test_all` exercises Bijectors rather +# than the upstream bugs. +_all_finite(x::Real) = isfinite(x) +_all_finite(x::AbstractArray{<:Real}) = all(isfinite, x) +_all_finite(x) = true # non-Real samples (Cholesky, NamedTuple) are unaffected +function randfinite(d) + for _ in 1:10_000 + x = try + rand(d) + catch + continue + end + _all_finite(x) && return x + end + return error("randfinite: no finite sample from $(d) after 10000 attempts") +end + # AD will give nonsense results at the limits of censored distributions (since the gradient # is not well-defined), so we avoid generating samples that are exactly at the limits. -_rand_safe_ad(d::D.Distribution) = rand(d) +_rand_safe_ad(d::D.Distribution) = randfinite(d) _rand_safe_ad(d::D.Censored) = begin a, b = d.lower, d.upper while true - x = rand(d) + x = randfinite(d) if x != a && x != b return x end @@ -267,7 +286,7 @@ function test_roundtrip(d::D.Distribution) # generate random parameters across the support @testset "roundtrip: $(_name(d))" begin for _ in 1:1000 - @testset let x = rand(d), d = d + @testset let x = randfinite(d), d = d ffwd = to_vec(d) frvs = from_vec(d) @test _isapprox_safe(x, frvs(ffwd(x))) @@ -276,7 +295,7 @@ function test_roundtrip(d::D.Distribution) end @testset "roundtrip (linked): $(_name(d))" begin for _ in 1:1000 - @testset let x = rand(d), d = d + @testset let x = randfinite(d), d = d ffwd = to_linked_vec(d) frvs = from_linked_vec(d) xnew = frvs(ffwd(x)) @@ -312,7 +331,7 @@ function test_roundtrip_inverse(d::D.Distribution, test_in_support, atol, rtol) # Check that Distributions.jl can actually run insupport. Sometimes it can't, e.g. # with product_distribution(MvNormal(), MvNormal()), even though that function is # well-defined. - x = rand(d) + x = randfinite(d) if test_in_support && (!hasmethod(D.insupport, Tuple{typeof(d),typeof(x)})) @info "No method for Distributions.insupport($(typeof(d)), $(typeof(x))), skipping in-support test" test_in_support = false @@ -364,7 +383,7 @@ conversions should be type stable. To disable type stability checks for the cons set `test_construction_type_stable=false`. """ function test_type_stability(d::D.Distribution, test_construction_type_stable=true) - x = rand(d) + x = randfinite(d) @testset "type stability: $(_name(d))" begin @testset let x = x, d = d if test_construction_type_stable @@ -400,7 +419,7 @@ values produced by `to_vec`. function test_optics(d::D.Distribution) @testset "optic_vec: $(_name(d))" begin o = optic_vec(d) - x = rand(d) + x = randfinite(d) v = to_vec(d)(x) for (optic, value) in zip(o, v) if optic !== nothing @@ -423,7 +442,7 @@ function test_optics(d::D.Distribution) # Jacobian of the link transform, row `i` should have nonzeros only in the columns # corresponding to `lo[i]`. This is a bit finicky to do because `x` might not be a # vector(!) so we need to flatten everything first, using `to_vec`. - x = rand(d) + x = randfinite(d) xvec = to_vec(d)(x) yvec = to_linked_vec(d)(x) f = to_linked_vec(d) ∘ from_vec(d) @@ -461,7 +480,7 @@ vector forms for the given distribution `d` match those reported by `vec_length` function test_vec_lengths(d::D.Distribution) @testset "vector lengths: $(_name(d))" begin for _ in 1:10 - @testset let x = rand(d), d = d + @testset let x = randfinite(d), d = d y = to_vec(d)(x) @test length(y) == vec_length(d) end @@ -469,7 +488,7 @@ function test_vec_lengths(d::D.Distribution) end @testset "vector lengths (linked): $(_name(d))" begin for _ in 1:10 - @testset let x = rand(d), d = d + @testset let x = randfinite(d), d = d y = to_linked_vec(d)(x) @test length(y) == linked_vec_length(d) end @@ -490,7 +509,7 @@ function test_allocations(d::D.Distribution, expected_zero_allocs=()) # For univariates, to_vec and to_linked_vec always cause allocations because they have # to create a new vector. # TODO: Generalise to multivariates etc - x = rand(d) + x = randfinite(d) @testset "allocations: $(_name(d))" begin @testset let x = x, d = d if to_vec in expected_zero_allocs @@ -531,7 +550,7 @@ function test_logjac(d::D.Distribution, atol, rtol) # Vectorisation logjacs should be zero because they are just reshapes. @testset "logjac: $(_name(d))" begin for _ in 1:100 - @testset let x = rand(d), d = d + @testset let x = randfinite(d), d = d ffwd = to_vec(d) y, logjac = with_logabsdet_jacobian(ffwd, x) @test _isapprox_safe(y, ffwd(x); atol=atol, rtol=rtol)