From 6fc41bd78e6081e9f3ca869431bd7ee6d0326698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Tue, 20 Oct 2026 11:06:27 +0200 Subject: [PATCH 01/17] add softplus transformation --- docs/src/index.md | 15 ++++++++------- src/scalar.jl | 16 +++++++++++++++- test/runtests.jl | 6 +++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index f21e0db..31d87a3 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -118,8 +118,8 @@ t = as((; e = Identity(), foo = tf)) ``` where each instance of `Identity()` here could be replaced with an arbitrary scalar transform. -This relies on [`constructorof` from ConstructionBase](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.constructorof). If a `Tuple` transform is wrapped in a type this way, transform results will be unpacked and passed to the constructor, hence in the same order as the transform. -If a `NamedTuple` transform is used, it will be unpacked as keyword arguments to the constructor of the type. +This relies on [`constructorof` from ConstructionBase](https://juliaobjects.github.io/ConstructionBase.jl/stable/#ConstructionBase.constructorof). If a `Tuple` transform is wrapped in a type this way, transform results will be unpacked and passed to the constructor, hence in the same order as the transform. +If a `NamedTuple` transform is used, it will be unpacked as keyword arguments to the constructor of the type. `Tuple` or `NamedTuple` transforms that do not provide proper arguments to the constructor of a given type will simply result in `MethodError`s. For structs that are constructed from a single argument, wrap a single transform in a `Tuple`, like so: @@ -128,8 +128,8 @@ struct Baz; f; end tbaz = as(Baz, (Identity(),)) ``` -Inverting these transforms would, in the most general case, require inverting the constructor of the given type, which may itself have several valid dispatches. -Rather than conduct a close inspection of user types and their available constructors, inverting a `Tuple`-based struct transform will check that the struct has exactly `n` fields, where `n` is the length of the `Tuple` transform, and use those fields in their struct order; if there are not `n` fields the inversion will error. This will work for any structs with only the default constructor. Inverting a `NamedTuple`-based struct transform will attempt to use struct fields with names matching the names in the `NamedTuple` transform, which fails if the struct does not have matching fields. +Inverting these transforms would, in the most general case, require inverting the constructor of the given type, which may itself have several valid dispatches. +Rather than conduct a close inspection of user types and their available constructors, inverting a `Tuple`-based struct transform will check that the struct has exactly `n` fields, where `n` is the length of the `Tuple` transform, and use those fields in their struct order; if there are not `n` fields the inversion will error. This will work for any structs with only the default constructor. Inverting a `NamedTuple`-based struct transform will attempt to use struct fields with names matching the names in the `NamedTuple` transform, which fails if the struct does not have matching fields. ## Scalar transforms @@ -162,6 +162,7 @@ TVLogistic TVScale TVShift TVNeg +TVSoftPlus ``` Consistent with common notation, transforms are applied right-to-left; for example, `as(Real, -∞, 3)` is equivalent to `TVShift(3) ∘ TVNeg() ∘ TVExp()`. @@ -173,16 +174,16 @@ This is useful especially for making sure that values near 0, when transformed, In addition, the `TVScale` transform accepts arbitrary types. It can be used as the outermost transform (so leftmost in the composition) to add, for example, `Unitful` units to a number (or to create other exotic number types which can be constructed by multiplying, such as a `ForwardDiff.Dual`). However, note that calculating log Jacobian determinants may error for types that are not real numbers. -For example, +For example, ```julia using Unitful t = TVScale(5u"m") ∘ TVExp() ``` -produces positive quantities with the dimension of length. +produces positive quantities with the dimension of length. !!! note Because the log-Jacobian of a transform that adds units is not defined, `transform_and_logjac` and `inverse_and_logjac` - only have methods defined for `TVScale{T} where {T<:Real}`. + only have methods defined for `TVScale{T} where {T<:Real}`. !!! note The inverse transform of `TVScale(scale)` divides by `scale`, which is the correct inverse for adding units to a number, but may be inappropriate for other custom number types. A transform that doesn't just multiply or an inverse that extracts a float from an exotic number type could be defined by adding methods to `transform` and `inverse` like the following: ``` diff --git a/src/scalar.jl b/src/scalar.jl index cbf1d49..c4df282 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -1,4 +1,4 @@ -export TVExp, TVScale, TVShift, TVLogistic, TVNeg +export TVExp, TVScale, TVShift, TVLogistic, TVNeg, TVSoftPlus export ∞, asℝ, asℝ₊, asℝ₋, as𝕀, as_real, as_positive_real, as_negative_real, as_unit_interval @@ -157,6 +157,20 @@ inverse_eltype(::TVNeg, ::Type{T}) where T = typeof(-oneunit(T)) inverse(::TVNeg, x::Number) = -x inverse_and_logjac(::TVNeg, x::Number) = -x, logjac_zero(LogJac(), typeof(x)) +""" +$(TYPEDEF) + +“Softplus” transformation `x ↦ log(1+exp(x))`. +""" +struct TVSoftPlus <: ScalarTransform end + +transform(::TVSoftPlus, x::Real) = log1pexp(x) +transform_and_logjac(t::TVSoftPlus, x::Real) = transform(t, x), -log1pexp(-x) + +inverse_eltype(::TVSoftPlus, ::Type{T}) where T = _ensure_float(T) +inverse(::TVSoftPlus, y::Number) = logexpm1(y) +inverse_and_logjac(::TVSoftPlus, y::Number) = logexpm1(y), -log1mexp(-y) + #### #### composite scalar transforms #### diff --git a/test/runtests.jl b/test/runtests.jl index 79c3ee7..c130fcd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1145,9 +1145,14 @@ end @test_throws InexactError inverse(t, fill(Complex(0, 1), 3)) end +@testset "TVSoftPlus" begin + test_transformation(TransformVariables.TVSoftPlus(), y -> y > 0) +end + ### ### Reactant compat tests ### + @testset "Reactant.jl" begin @testset "Scalar transforms" begin a = 3.1 @@ -1156,7 +1161,6 @@ end @test @jit(transform(asℝ₊, ar)) ≈ transform(asℝ₊, a) @test @jit(transform(asℝ₋, ar)) ≈ transform(asℝ₋, a) @test @jit(transform(as𝕀, ar)) ≈ transform(as𝕀, a) - end @testset "Array transforms begin" begin From b9517a9f9ece041f1f9ad3b0b55ca14a886c7dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Tue, 20 Oct 2026 13:40:07 +0200 Subject: [PATCH 02/17] add TVCauchy transform, minor doc changes --- docs/src/index.md | 3 +++ src/scalar.jl | 43 +++++++++++++++++++++++++++++++++++++++++-- test/runtests.jl | 5 +++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 31d87a3..04823ef 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -163,8 +163,11 @@ TVScale TVShift TVNeg TVSoftPlus +TVCauchy ``` +The above include additional transformations that don't have a short syntax with `as`. + Consistent with common notation, transforms are applied right-to-left; for example, `as(Real, -∞, 3)` is equivalent to `TVShift(3) ∘ TVNeg() ∘ TVExp()`. If you are working in an editor where typing Unicode is difficult, `TransformVariables.compose` is also available, as in `TransformVariables.compose(TVScale(5.0), TVNeg(), TVExp())`. diff --git a/src/scalar.jl b/src/scalar.jl index c4df282..b40857b 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -1,4 +1,5 @@ -export TVExp, TVScale, TVShift, TVLogistic, TVNeg, TVSoftPlus +export TVExp, TVScale, TVShift, TVLogistic, TVNeg, TVSoftPlus, TVCauchy + export ∞, asℝ, asℝ₊, asℝ₋, as𝕀, as_real, as_positive_real, as_negative_real, as_unit_interval @@ -160,7 +161,8 @@ inverse_and_logjac(::TVNeg, x::Number) = -x, logjac_zero(LogJac(), typeof(x)) """ $(TYPEDEF) -“Softplus” transformation `x ↦ log(1+exp(x))`. +“Softplus” transformation ``x ↦ log(1+exp(x))``, mapping the real line to positive +numbers. """ struct TVSoftPlus <: ScalarTransform end @@ -171,6 +173,43 @@ inverse_eltype(::TVSoftPlus, ::Type{T}) where T = _ensure_float(T) inverse(::TVSoftPlus, y::Number) = logexpm1(y) inverse_and_logjac(::TVSoftPlus, y::Number) = logexpm1(y), -log1mexp(-y) +""" +$(TYPEDEF) + +Transformation ``x ↦ 1/2 + arctan(x)/π``, mapping the real line to `[0,1]`, which is +also the CDF of the Cauchy distribution. +""" +struct TVCauchy <: ScalarTransform end + +function transform(::TVCauchy, x::Number) + _x = float(x) + _π = oftype(_x, π) + if _x > -1 + one(_x) / 2 + atan(_x) / _π + else + # avoid 1/2 - 1/2 cancellations + atan(inv(-_x)) / _π + end +end + +function transform_and_logjac(t::TVCauchy, x::Number) + y = transform(t, x) + y, -log(oftype(y, π)) - log1psq(x) +end + +inverse_eltype(::TVCauchy, ::Type{T}) where T = _ensure_float(T) + +function inverse(::TVCauchy, y::Number) + @argcheck 0 <= y <= 1 DomainError + cotpi(p) = cospi(p) / sinpi(p) + y > 0.5 ? cotpi(1 - y) : -cotpi(y) +end + +function inverse_and_logjac(t::TVCauchy, y::Number) + x = inverse(t, y) + x, log(oftype(x, π)) - 2 * log(sinpi(y)) +end + #### #### composite scalar transforms #### diff --git a/test/runtests.jl b/test/runtests.jl index c130fcd..77f0f7e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1149,6 +1149,11 @@ end test_transformation(TransformVariables.TVSoftPlus(), y -> y > 0) end +@testset "TVCauchy" begin + test_transformation(TransformVariables.TVCauchy(), y -> 0 < y < 1) +end + + ### ### Reactant compat tests ### From 34bb92035ba4aab3f463ae939fda7905d36c2804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Tue, 20 Oct 2026 17:15:08 +0200 Subject: [PATCH 03/17] widen to Number, tests for Reactant --- ext/ReactantExt.jl | 27 +++++++++++++++++---------- src/scalar.jl | 10 ++++++---- test/runtests.jl | 4 +++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/ext/ReactantExt.jl b/ext/ReactantExt.jl index 339abf7..0933cb4 100644 --- a/ext/ReactantExt.jl +++ b/ext/ReactantExt.jl @@ -1,5 +1,5 @@ module ReactantExt -using TransformVariables: TransformVariables, ArrayTransformation, LogJacFlag, +using TransformVariables: TransformVariables, ArrayTransformation, LogJacFlag, logjac_zero, transform_with, _ensure_float, dimension using Reactant using Reactant: TracedRNumber, AnyTracedRArray @@ -22,12 +22,19 @@ Base.@nospecialize function Reactant.traced_type_inner( return TransformVariables.ArrayTransformation{T_traced, M} end -TransformVariables._ensure_float(x::Type{TracedRNumber{T}}) where T = TracedRNumber{_ensure_float(T)} +function TransformVariables._ensure_float(x::Type{TracedRNumber{T}}) where T + TracedRNumber{_ensure_float(T)} +end + +function TransformVariables._cauchy_cdf(x::TracedRNumber{T}) where {T<:AbstractFloat} + _π = T(π) + ifelse(x > -1, one(x) / 2 + atan(x) / _π, atan(inv(-x)) / _π) +end @noinline function TransformVariables.transform_with( - flag::TransformVariables.LogJacFlag, - transformation::TransformVariables.ArrayTransformation, - x::AnyTracedRArray, + flag::TransformVariables.LogJacFlag, + transformation::TransformVariables.ArrayTransformation, + x::AnyTracedRArray, index ) (; inner_transformation, dims) = transformation @@ -48,18 +55,18 @@ TransformVariables._ensure_float(x::Type{TracedRNumber{T}}) where T = TracedRNum throw(ArgumentError("only `Number` and `AbstractArray` transformations are supported in Reactant compilation mode")) end - # Reactant can't handle arrays of non-primitive types so we have to be more explicit about - # the output and use a `@trace` for loop. + # Reactant can't handle arrays of non-primitive types so we have to be more explicit about + # the output and use a `@trace` for loop. @trace track_numbers=false for i in eachindex(𝐼) idx = 𝐼[i] y, ℓ, _ = transform_with(flag, inner_transformation, x, idx) if !isempty(y) - ℓa += ℓ + ℓa += ℓ end if tmp isa Number Reactant.@allowscalar yℓ[i] = y else - idxs = ntuple(n-> Colon(), ndims(tmp)) + idxs = ntuple(n-> Colon(), ndims(tmp)) yℓ[idxs..., i] = y end end @@ -83,4 +90,4 @@ function TransformVariables.transform_with(flag::LogJacFlag, t::ArrayTransformat y, logjac_zero(flag, _ensure_float(eltype(x))), index′ end -end \ No newline at end of file +end diff --git a/src/scalar.jl b/src/scalar.jl index b40857b..d1477c4 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -166,8 +166,8 @@ numbers. """ struct TVSoftPlus <: ScalarTransform end -transform(::TVSoftPlus, x::Real) = log1pexp(x) -transform_and_logjac(t::TVSoftPlus, x::Real) = transform(t, x), -log1pexp(-x) +transform(::TVSoftPlus, x::Number) = log1pexp(x) +transform_and_logjac(t::TVSoftPlus, x::Number) = transform(t, x), -log1pexp(-x) inverse_eltype(::TVSoftPlus, ::Type{T}) where T = _ensure_float(T) inverse(::TVSoftPlus, y::Number) = logexpm1(y) @@ -181,9 +181,9 @@ also the CDF of the Cauchy distribution. """ struct TVCauchy <: ScalarTransform end -function transform(::TVCauchy, x::Number) +function _cauchy_cdf(x::T) where {T<:Real} + _π = T(π) _x = float(x) - _π = oftype(_x, π) if _x > -1 one(_x) / 2 + atan(_x) / _π else @@ -192,6 +192,8 @@ function transform(::TVCauchy, x::Number) end end +transform(::TVCauchy, x::Number) = _cauchy_cdf(float(x)) + function transform_and_logjac(t::TVCauchy, x::Number) y = transform(t, x) y, -log(oftype(y, π)) - log1psq(x) diff --git a/test/runtests.jl b/test/runtests.jl index 77f0f7e..c1f0e97 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -470,7 +470,7 @@ end @test tn[1] == tn.a == inner[1] @test tn[2] == tn.b == inner[2] @test tn[3] == tn.c == inner[3] - @test_throws ErrorException tn.d + @test_throws (VERSION < v"1.12" ? ErrorException : FieldError) tn.d @test_throws BoundsError tn[4] @test propertynames(tn) == propertynames(inner) @test (@set tn[3] = t2) == (@set tn.c = t2) == as((a = t1, b = t2, c = t2)) @@ -1166,6 +1166,8 @@ end @test @jit(transform(asℝ₊, ar)) ≈ transform(asℝ₊, a) @test @jit(transform(asℝ₋, ar)) ≈ transform(asℝ₋, a) @test @jit(transform(as𝕀, ar)) ≈ transform(as𝕀, a) + @test @jit(transform(TVSoftPlus(), ar)) ≈ transform(TVSoftPlus(), a) + @test @jit(transform(TVCauchy(), ar)) ≈ transform(TVCauchy(), a) end @testset "Array transforms begin" begin From 99bfe01d650ff089a7f5ebb06154fdcadd18d5e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Tue, 20 Oct 2026 19:50:25 +0200 Subject: [PATCH 04/17] don't promote pi --- ext/ReactantExt.jl | 3 +-- src/scalar.jl | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ext/ReactantExt.jl b/ext/ReactantExt.jl index 0933cb4..805a04b 100644 --- a/ext/ReactantExt.jl +++ b/ext/ReactantExt.jl @@ -27,8 +27,7 @@ function TransformVariables._ensure_float(x::Type{TracedRNumber{T}}) where T end function TransformVariables._cauchy_cdf(x::TracedRNumber{T}) where {T<:AbstractFloat} - _π = T(π) - ifelse(x > -1, one(x) / 2 + atan(x) / _π, atan(inv(-x)) / _π) + ifelse(x > -1, one(x) / 2 + atan(x) / π, atan(inv(-x)) / π) end @noinline function TransformVariables.transform_with( diff --git a/src/scalar.jl b/src/scalar.jl index d1477c4..487283d 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -182,13 +182,12 @@ also the CDF of the Cauchy distribution. struct TVCauchy <: ScalarTransform end function _cauchy_cdf(x::T) where {T<:Real} - _π = T(π) _x = float(x) if _x > -1 - one(_x) / 2 + atan(_x) / _π + one(_x) / 2 + atan(_x) / π else # avoid 1/2 - 1/2 cancellations - atan(inv(-_x)) / _π + atan(inv(-_x)) / π end end From e449839ff1565e40df911d945c3bfc80d25f5b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Wed, 21 Oct 2026 08:54:26 +0200 Subject: [PATCH 05/17] use sincospi --- src/scalar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalar.jl b/src/scalar.jl index 487283d..4dfd658 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -202,7 +202,7 @@ inverse_eltype(::TVCauchy, ::Type{T}) where T = _ensure_float(T) function inverse(::TVCauchy, y::Number) @argcheck 0 <= y <= 1 DomainError - cotpi(p) = cospi(p) / sinpi(p) + cotpi(p) = ((s, c) = sincospi(p); c / s) y > 0.5 ? cotpi(1 - y) : -cotpi(y) end From b7282088276bbb0607c52c83b9e8899d228b1815 Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Wed, 21 Oct 2026 08:54:53 +0200 Subject: [PATCH 06/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/scalar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalar.jl b/src/scalar.jl index 4dfd658..15c016e 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -208,7 +208,7 @@ end function inverse_and_logjac(t::TVCauchy, y::Number) x = inverse(t, y) - x, log(oftype(x, π)) - 2 * log(sinpi(y)) + x, IrrationalConstants.logπ - 2 * log(sinpi(y)) end #### From 37d33ccb3c1e26e0817368b23734e61e3afb7921 Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Wed, 21 Oct 2026 08:55:02 +0200 Subject: [PATCH 07/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/scalar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalar.jl b/src/scalar.jl index 15c016e..82568b7 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -195,7 +195,7 @@ transform(::TVCauchy, x::Number) = _cauchy_cdf(float(x)) function transform_and_logjac(t::TVCauchy, x::Number) y = transform(t, x) - y, -log(oftype(y, π)) - log1psq(x) + y, -(log1psq(x) + IrrationalConstants.logπ) end inverse_eltype(::TVCauchy, ::Type{T}) where T = _ensure_float(T) From 94e5252996f416e223b5e326d16430bf5a181b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Wed, 21 Oct 2026 10:04:58 +0200 Subject: [PATCH 08/17] add IrrationalConstants --- Project.toml | 2 ++ src/TransformVariables.jl | 1 + 2 files changed, 3 insertions(+) diff --git a/Project.toml b/Project.toml index 1d77727..bba608d 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ CompositionsBase = "a33af91c-f02d-484b-be07-31d278c5ca2b" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -40,6 +41,7 @@ ConstructionBase = "1.6.0" DocStringExtensions = "0.8, 0.9" ForwardDiff = "0.10, 1" InverseFunctions = "0.1" +IrrationalConstants = "0.2.6" LinearAlgebra = "1.6" LogExpFunctions = "0.3, 1" Random = "1.6" diff --git a/src/TransformVariables.jl b/src/TransformVariables.jl index a9140c9..9f7c8a5 100644 --- a/src/TransformVariables.jl +++ b/src/TransformVariables.jl @@ -4,6 +4,7 @@ using ArgCheck: @argcheck import Compat using DocStringExtensions: FUNCTIONNAME, SIGNATURES, TYPEDEF import ForwardDiff +import IrrationalConstants using LogExpFunctions using LinearAlgebra: UpperTriangular, logabsdet, norm, rmul! using Random: AbstractRNG, GLOBAL_RNG From 380712511bdb53f0ff24025ad08ede08f91620cb Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Tue, 27 Oct 2026 14:21:10 +0100 Subject: [PATCH 09/17] Update Project.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loosen compat bounds to minor version Co-authored-by: David Müller-Widmann --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index bba608d..a949879 100644 --- a/Project.toml +++ b/Project.toml @@ -41,7 +41,7 @@ ConstructionBase = "1.6.0" DocStringExtensions = "0.8, 0.9" ForwardDiff = "0.10, 1" InverseFunctions = "0.1" -IrrationalConstants = "0.2.6" +IrrationalConstants = "0.2" LinearAlgebra = "1.6" LogExpFunctions = "0.3, 1" Random = "1.6" From 5d46d137c621293dbfbbd4ec9ac3dbbee76be9cb Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Tue, 27 Oct 2026 14:22:27 +0100 Subject: [PATCH 10/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use open interval. Co-authored-by: David Müller-Widmann --- src/scalar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalar.jl b/src/scalar.jl index 82568b7..7d78168 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -176,7 +176,7 @@ inverse_and_logjac(::TVSoftPlus, y::Number) = logexpm1(y), -log1mexp(-y) """ $(TYPEDEF) -Transformation ``x ↦ 1/2 + arctan(x)/π``, mapping the real line to `[0,1]`, which is +Transformation ``x ↦ 1/2 + arctan(x)/π``, mapping the real line to `(0, 1)`, which is also the CDF of the Cauchy distribution. """ struct TVCauchy <: ScalarTransform end From 17d3eda4a0a58647f5ce058be780ee5f12f2b58b Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Tue, 27 Oct 2026 14:23:43 +0100 Subject: [PATCH 11/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/scalar.jl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/scalar.jl b/src/scalar.jl index 7d78168..3571a07 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -202,13 +202,20 @@ inverse_eltype(::TVCauchy, ::Type{T}) where T = _ensure_float(T) function inverse(::TVCauchy, y::Number) @argcheck 0 <= y <= 1 DomainError - cotpi(p) = ((s, c) = sincospi(p); c / s) - y > 0.5 ? cotpi(1 - y) : -cotpi(y) + first(_cauchy_quantile_and_sinpi(y)) end -function inverse_and_logjac(t::TVCauchy, y::Number) - x = inverse(t, y) - x, IrrationalConstants.logπ - 2 * log(sinpi(y)) +function inverse_and_logjac(::TVCauchy, y::Number) + @argcheck 0 <= y <= 1 DomainError + x, s = _cauchy_quantile_and_sinpi(y) + x, IrrationalConstants.logπ - 2 * log(s) +end + +# returns the quantile and sinpi(y) +function _cauchy_quantile_and_sinpi(y) + flip = y > 0.5 + s, c = sincospi(flip ? 1 - y : y) + (flip ? c / s : -c / s), s end #### From d6e8505ad700e8876a21f7be8458c322da0d3bee Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Tue, 27 Oct 2026 14:24:33 +0100 Subject: [PATCH 12/17] Update test/runtests.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/runtests.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index c1f0e97..b72f13b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1146,7 +1146,13 @@ end end @testset "TVSoftPlus" begin - test_transformation(TransformVariables.TVSoftPlus(), y -> y > 0) + t = TVSoftPlus() + test_transformation(t, y -> y > 0) + @test transform(t, 1000.0) == 1000.0 + @test inverse(t, 1000.0) == 1000.0 + @test transform(t, -30.0) ≈ exp(-30.0) + @test inverse(t, exp(-30.0)) ≈ -30.0 + @test @inferred(transform(t, 1f0)) isa Float32 end @testset "TVCauchy" begin From eb1809f042f4f8c2d8272032101cb2b7ce7c685a Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Thu, 5 Nov 2026 11:39:01 +0100 Subject: [PATCH 13/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/scalar.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scalar.jl b/src/scalar.jl index 3571a07..6e7fc42 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -195,7 +195,7 @@ transform(::TVCauchy, x::Number) = _cauchy_cdf(float(x)) function transform_and_logjac(t::TVCauchy, x::Number) y = transform(t, x) - y, -(log1psq(x) + IrrationalConstants.logπ) + y, -(log1psq(float(x)) + IrrationalConstants.logπ) end inverse_eltype(::TVCauchy, ::Type{T}) where T = _ensure_float(T) From 7599d32183b754fb64169cffeae58d05029151aa Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Thu, 5 Nov 2026 11:39:21 +0100 Subject: [PATCH 14/17] Update src/scalar.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- src/scalar.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/scalar.jl b/src/scalar.jl index 6e7fc42..e64ddc5 100644 --- a/src/scalar.jl +++ b/src/scalar.jl @@ -181,13 +181,12 @@ also the CDF of the Cauchy distribution. """ struct TVCauchy <: ScalarTransform end -function _cauchy_cdf(x::T) where {T<:Real} - _x = float(x) - if _x > -1 - one(_x) / 2 + atan(_x) / π +function _cauchy_cdf(x::Real) + if x > -1 + one(x) / 2 + atan(x) / π else # avoid 1/2 - 1/2 cancellations - atan(inv(-_x)) / π + atan(inv(-x)) / π end end From 8830db37fabad688f0565d315c0970df9f2fd10e Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Thu, 5 Nov 2026 11:39:47 +0100 Subject: [PATCH 15/17] Update test/runtests.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/runtests.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index b72f13b..9518a42 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1156,7 +1156,17 @@ end end @testset "TVCauchy" begin - test_transformation(TransformVariables.TVCauchy(), y -> 0 < y < 1) + t = TVCauchy() + test_transformation(t, y -> 0 < y < 1) + @test transform(t, -1e300) ≈ inv(π * 1e300) + @test transform(t, -Inf) == 0 + @test transform(t, Inf) == 1 + @test inverse(t, 0.0) == -Inf + @test inverse(t, 1.0) == Inf + @test_throws DomainError inverse(t, 1.5) + @test @inferred(transform(t, 1f0)) isa Float32 + @test @inferred(inverse(t, 0.3f0)) isa Float32 + @test transform_and_logjac(t, 2^40)[2] ≈ transform_and_logjac(t, 2.0^40)[2] end From 860829a0838bf71ff2a5296078522056b2fc1adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Thu, 5 Nov 2026 12:16:11 +0100 Subject: [PATCH 16/17] tests for negative values, Reactant array tests for cauchy and softplus --- test/runtests.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9518a42..e027d6e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1169,7 +1169,6 @@ end @test transform_and_logjac(t, 2^40)[2] ≈ transform_and_logjac(t, 2.0^40)[2] end - ### ### Reactant compat tests ### @@ -1183,7 +1182,9 @@ end @test @jit(transform(asℝ₋, ar)) ≈ transform(asℝ₋, a) @test @jit(transform(as𝕀, ar)) ≈ transform(as𝕀, a) @test @jit(transform(TVSoftPlus(), ar)) ≈ transform(TVSoftPlus(), a) + @test @jit(transform(TVSoftPlus(), -ar)) ≈ transform(TVSoftPlus(), -a) @test @jit(transform(TVCauchy(), ar)) ≈ transform(TVCauchy(), a) + @test @jit(transform(TVCauchy(), -ar)) ≈ transform(TVCauchy(), -a) end @testset "Array transforms begin" begin @@ -1200,7 +1201,7 @@ end end end @testset "Test inner transformations" begin - for T in (asℝ, asℝ₊, asℝ₋, as𝕀) + for T in (asℝ, asℝ₊, asℝ₋, as𝕀, TVSoftPlus(), TVCauchy()) tr = as(Array, as(Array, T, 3), 3) a = randn(dimension(tr)) ar = Reactant.to_rarray(a) From 9f6709785429f98cf9664c927e8a71623245aa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20K=2E=20Papp?= Date: Thu, 5 Nov 2026 12:32:21 +0100 Subject: [PATCH 17/17] test cauchy zero with reactant --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index e027d6e..cd86446 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1185,6 +1185,7 @@ end @test @jit(transform(TVSoftPlus(), -ar)) ≈ transform(TVSoftPlus(), -a) @test @jit(transform(TVCauchy(), ar)) ≈ transform(TVCauchy(), a) @test @jit(transform(TVCauchy(), -ar)) ≈ transform(TVCauchy(), -a) + @test @jit(transform(TVCauchy(), ConcreteRNumber(0.0))) ≈ transform(TVCauchy(), 0.0) end @testset "Array transforms begin" begin