From 50a8026e1a2b3233ba3d3cce5d86c38af048bce4 Mon Sep 17 00:00:00 2001 From: Cambridge Yang Date: Fri, 29 May 2026 22:52:28 -0400 Subject: [PATCH] Fix StackOverflow in condition() for non-Float64 values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `condition(C, js, uⱼₛ)` (and the SklarDist method) typed `uⱼₛ` as `NTuple{p,Float64}`. The untyped entry point routes through `_process_tuples`, which calls `float.` — and `float(::BigFloat) === BigFloat`, `float(::Float32) === Float32` — so non-`Float64` reals miss the typed method, fall back to the untyped entry point, and recurse forever (StackOverflow). Reproduces on a plain `condition(ClaytonCopula(3, 2.0), (1, 2), (big"0.3", big"0.4"))`. Widen both typed methods to `NTuple{p,<:Real}`. `Float64` is still `<:Real`, so existing dispatch and behaviour are unchanged (the method is replaced, not added — no new ambiguity). Non-`Float64` values are converted to `Float64` by the downstream `DistortionFromCop`/`ConditionalCopula` fields, so the result is computed in `Float64` regardless of input precision. Adds a regression test covering Copula and SklarDist entry points, scalar and tuple, single- and multi-conditioned dims, with BigFloat and Float32 values. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Conditioning.jl | 10 ++++++-- test/ConditionalDistribution.jl | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/Conditioning.jl b/src/Conditioning.jl index 93eea625..140094a6 100644 --- a/src/Conditioning.jl +++ b/src/Conditioning.jl @@ -233,14 +233,20 @@ Notes of that conditional distribution. """ condition(C::Copula{D}, j, xⱼ) where D = condition(C, _process_tuples(Val{D}(), j, xⱼ)...) -function condition(C::Copula{D}, js::NTuple{p, Int}, uⱼₛ::NTuple{p, Float64}) where {D, p} +# Accept any real `uⱼₛ` (not only `Float64`): `_process_tuples` calls `float.`, +# which keeps `BigFloat`/`Float32` as-is, so a `Float64`-only signature here let +# such inputs fall back to the untyped entry point above and recurse forever +# (StackOverflow). The downstream `DistortionFromCop`/`ConditionalCopula` still +# store `Float64`, so non-`Float64` values are converted there — the conditioning +# result is computed in `Float64` regardless of input precision. +function condition(C::Copula{D}, js::NTuple{p, Int}, uⱼₛ::NTuple{p, <:Real}) where {D, p} margins = Tuple(DistortionFromCop(C, js, uⱼₛ, i) for i in setdiff(1:D, js)) p==D-1 && return margins[1] return SklarDist(ConditionalCopula(C, js, uⱼₛ), margins) end condition(C::SklarDist{<:Copula{D}}, j, xⱼ) where D = condition(C, _process_tuples(Val{D}(), j, xⱼ)...) -function condition(X::SklarDist{<:Copula{D}, Tpl}, js::NTuple{p, Int}, xⱼₛ::NTuple{p, Float64}) where {D, Tpl, p} +function condition(X::SklarDist{<:Copula{D}, Tpl}, js::NTuple{p, Int}, xⱼₛ::NTuple{p, <:Real}) where {D, Tpl, p} uⱼₛ = Tuple(Distributions.cdf(X.m[j], xⱼ) for (j,xⱼ) in zip(js, xⱼₛ)) margins = Tuple(DistortionFromCop(X.C, js, uⱼₛ, i)(X.m[i]) for i in setdiff(1:D, js)) p==D-1 && return margins[1] diff --git a/test/ConditionalDistribution.jl b/test/ConditionalDistribution.jl index 55132e29..11037bab 100644 --- a/test/ConditionalDistribution.jl +++ b/test/ConditionalDistribution.jl @@ -291,3 +291,45 @@ end @test isapprox(cdf(Y, q), α; atol=2e-3, rtol=2e-3) end end + +@testset "condition accepts non-Float64 reals (BigFloat StackOverflow regression)" begin + # Regression: condition(C, js, uⱼₛ) hardcoded NTuple{p,Float64}. Because + # _process_tuples calls float. (which keeps BigFloat/Float32 unchanged), such + # inputs missed the typed method, fell back to the untyped entry point, and + # recursed forever (StackOverflow). The typed methods now accept + # NTuple{p,<:Real}; non-Float64 values are converted to Float64 downstream, so + # the conditioning result matches the Float64-input result (tolerance allows + # for the fast-vs-generic distortion method difference on the converted path). + C3 = ClaytonCopula(3, 2.0) + C4 = ClaytonCopula(4, 2.0) + + # Copula entry, single conditioned dim (p == D-1) → univariate Distortion. + r1 = condition(C3, (1, 2), (0.3, 0.4)) + b1 = condition(C3, (1, 2), (big"0.3", big"0.4")) # must not StackOverflow + @test b1 isa Copulas.Distortion + for u in (0.1, 0.5, 0.9) + @test isapprox(cdf(b1, u), cdf(r1, u); atol=1e-6) + end + + # Copula entry, scalar BigFloat, multi remaining (p == 1 < D-1) → SklarDist. + r2 = condition(C3, 1, 0.3) + b2 = condition(C3, 1, big"0.3") + @test b2 isa SklarDist + @test isapprox(cdf(b2.C, [0.5, 0.6]), cdf(r2.C, [0.5, 0.6]); atol=1e-6) + + # Copula entry, tuple BigFloat, multi conditioned (p == 2 < D-1). + r3 = condition(C4, (1, 2), (0.3, 0.4)) + b3 = condition(C4, (1, 2), (big"0.3", big"0.4")) + @test b3 isa SklarDist + @test isapprox(cdf(b3.C, [0.5, 0.6]), cdf(r3.C, [0.5, 0.6]); atol=1e-6) + + # SklarDist entry, BigFloat data-scale conditioning value (3-dim → 2-dim cond). + X = SklarDist(C3, (Normal(), LogNormal(), Exponential())) + rS = condition(X, (1,), (0.2,)) + bS = condition(X, (1,), (big"0.2",)) + @test bS isa SklarDist + @test isapprox(cdf(bS, [0.3, 0.5]), cdf(rS, [0.3, 0.5]); atol=1e-6) + + # Float32 also previously recursed; confirm it is accepted too. + @test condition(C3, (1, 2), (0.3f0, 0.4f0)) isa Copulas.Distortion +end