From d75ad63d4113e43b5167e28cee9762a2f678e79b Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Mon, 28 Jul 2025 12:24:30 +0200 Subject: [PATCH 1/6] Added furlongs test --- test/DerivativeTest.jl | 13 +++++ test/Furlongs.jl | 110 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 test/Furlongs.jl diff --git a/test/DerivativeTest.jl b/test/DerivativeTest.jl index 4de1a6de..206a2d68 100644 --- a/test/DerivativeTest.jl +++ b/test/DerivativeTest.jl @@ -8,6 +8,7 @@ using Random using ForwardDiff using DiffTests +include(joinpath(dirname(@__FILE__), "Furlongs.jl")) include(joinpath(dirname(@__FILE__), "utils.jl")) Random.seed!(1) @@ -113,4 +114,16 @@ end @test ForwardDiff.derivative(x -> (1+im)*x, 0) == (1+im) end +@testset "non-standard numbers" begin + Base.exp(f::Furlongs.Furlong) = exp(f.val) + Base.cos(f::Furlongs.Furlong) = cos(f.val) + Base.sin(f::Furlongs.Furlong) = sin(f.val) + + furlong = Furlongs.Furlong{2}(1.0) + + f(x) = exp(x) + 4*sin(x)*oneunit(x) + + @test ForwardDiff.derivative(f, furlong) == exp(furlong) + 4*cos(furlong) +end + end # module diff --git a/test/Furlongs.jl b/test/Furlongs.jl new file mode 100644 index 00000000..3fef6683 --- /dev/null +++ b/test/Furlongs.jl @@ -0,0 +1,110 @@ +module Furlongs + +export Furlong + +const TNumber = Real # ForwardDiff only supports Real numbers + +# Here we implement a minimal dimensionful type Furlong, which is used +# to test dimensional correctness of various functions in Base. + +# represents a quantity in furlongs^p +struct Furlong{p,T<:TNumber} <: TNumber + val::T + Furlong{p,T}(v::TNumber) where {p,T} = new(v) +end +Furlong(x::T) where {T<:TNumber} = Furlong{1,T}(x) +Furlong(x::Furlong) = x +(::Type{T})(x::Furlong{0}) where {T<:TNumber} = T(x.val)::T +(::Type{T})(x::Furlong{0}) where {T<:Furlong{0}} = T(x.val)::T +(::Type{T})(x::Furlong{0}) where {T<:Furlong} = typeassert(x, T) +Furlong{p}(v::TNumber) where {p} = Furlong{p,typeof(v)}(v) +Furlong{p}(x::Furlong{q}) where {p,q} = (typeassert(x, Furlong{p}); Furlong{p,typeof(x.val)}(x.val)) +Furlong{p,T}(x::Furlong{q}) where {T,p,q} = (typeassert(x, Furlong{p}); Furlong{p,T}(T(x.val))) + +Base.promote_rule(::Type{Furlong{p,T}}, ::Type{Furlong{p,S}}) where {p,T,S} = + Furlong{p,promote_type(T,S)} +Base.promote_rule(::Type{Furlong{0,T}}, ::Type{S}) where {T,S<:Union{Real,Complex}} = + Furlong{0,promote_type(T,S)} +# only Furlong{0} forms a ring and isa TNumber +Base.convert(::Type{T}, y::TNumber) where {T<:Furlong{0}} = T(y)::T +Base.convert(::Type{Furlong}, y::TNumber) = Furlong{0}(y) +Base.convert(::Type{Furlong{<:Any,T}}, y::TNumber) where {T<:TNumber} = Furlong{0,T}(y) +Base.convert(::Type{T}, y::TNumber) where {T<:Furlong} = typeassert(y, T) # throws, since cannot convert a Furlong{0} to a Furlong{p} +# other Furlong{p} form a group +Base.convert(::Type{T}, y::Furlong) where {T<:Furlong{0}} = T(y)::T +Base.convert(::Type{Furlong}, y::Furlong) = y +Base.convert(::Type{Furlong{<:Any,T}}, y::Furlong{p}) where {p,T<:TNumber} = Furlong{p,T}(y) +Base.convert(::Type{T}, y::Furlong) where {T<:Furlong} = T(y)::T + +Base.one(::Furlong{p,T}) where {p,T} = one(T) +Base.one(::Type{Furlong{p,T}}) where {p,T} = one(T) +Base.oneunit(::Furlong{p,T}) where {p,T} = Furlong{p,T}(one(T)) +Base.oneunit(::Type{Furlong{p,T}}) where {p,T} = Furlong{p,T}(one(T)) +Base.zero(::Furlong{p,T}) where {p,T} = Furlong{p,T}(zero(T)) +Base.zero(::Type{Furlong{p,T}}) where {p,T} = Furlong{p,T}(zero(T)) +Base.iszero(x::Furlong) = iszero(x.val) +Base.float(x::Furlong{p}) where {p} = Furlong{p}(float(x.val)) +Base.eps(::Type{Furlong{p,T}}) where {p,T<:AbstractFloat} = Furlong{p}(eps(T)) +Base.eps(::Furlong{p,T}) where {p,T<:AbstractFloat} = eps(Furlong{p,T}) +Base.floatmin(::Type{Furlong{p,T}}) where {p,T<:AbstractFloat} = Furlong{p}(floatmin(T)) +Base.floatmin(::Furlong{p,T}) where {p,T<:AbstractFloat} = floatmin(Furlong{p,T}) +Base.floatmax(::Type{Furlong{p,T}}) where {p,T<:AbstractFloat} = Furlong{p}(floatmax(T)) +Base.floatmax(::Furlong{p,T}) where {p,T<:AbstractFloat} = floatmax(Furlong{p,T}) +Base.conj(x::Furlong{p,T}) where {p,T} = Furlong{p,T}(conj(x.val)) + +# convert Furlong exponent p to a canonical form +canonical_p(p) = isinteger(p) ? Int(p) : Rational{Int}(p) + +Base.abs(x::Furlong{p}) where {p} = Furlong{p}(abs(x.val)) +Base.abs2(x::Furlong{p}) where {p} = Furlong{canonical_p(2p)}(abs2(x.val)) +Base.inv(x::Furlong{p}) where {p} = Furlong{canonical_p(-p)}(inv(x.val)) + +for f in (:isfinite, :isnan, :isreal, :isinf) + @eval Base.$f(x::Furlong) = $f(x.val) +end +for f in (:real,:imag,:complex,:+,:-) + @eval Base.$f(x::Furlong{p}) where {p} = Furlong{p}($f(x.val)) +end + +import Base: +, -, ==, !=, <, <=, isless, isequal, *, /, //, div, rem, mod, ^ +for op in (:+, :-) + @eval function $op(x::Furlong{p}, y::Furlong{p}) where {p} + v = $op(x.val, y.val) + Furlong{p}(v) + end +end +for op in (:(==), :(!=), :<, :<=, :isless, :isequal) + @eval $op(x::Furlong{p}, y::Furlong{p}) where {p} = $op(x.val, y.val)::Bool +end +for (f,op) in ((:_plus,:+),(:_minus,:-),(:_times,:*),(:_div,://)) + @eval function $f(v::T, ::Furlong{p}, ::Union{Furlong{q},Val{q}}) where {T,p,q} + s = $op(p, q) + Furlong{canonical_p(s),T}(v) + end +end +for (op,eop) in ((:*, :_plus), (:/, :_minus), (://, :_minus), (:div, :_minus)) + @eval begin + $op(x::Furlong{p}, y::Furlong{q}) where {p,q} = + $eop($op(x.val, y.val),x,y) + $op(x::Furlong{p}, y::S) where {p,S<:TNumber} = $op(x,Furlong{0,S}(y)) + $op(x::S, y::Furlong{p}) where {p,S<:TNumber} = $op(Furlong{0,S}(x),y) + end +end +# to fix an ambiguity +//(x::Furlong, y::Complex) = x // Furlong{0,typeof(y)}(y) +for op in (:rem, :mod) + @eval begin + $op(x::Furlong{p}, y::Furlong) where {p} = Furlong{p}($op(x.val, y.val)) + $op(x::Furlong{p}, y::TNumber) where {p} = Furlong{p}($op(x.val, y)) + end +end +Base.sqrt(x::Furlong) = _div(sqrt(x.val), x, Val(2)) +Base.muladd(x::Furlong, y::Furlong, z::Furlong) = x*y + z +Base.muladd(x::Furlong, y::TNumber, z::TNumber) = x*y + z +Base.muladd(x::Furlong, y::Furlong, z::TNumber) = x*y + z +Base.muladd(x::TNumber, y::Furlong, z::TNumber) = x*y + z +Base.muladd(x::TNumber, y::TNumber, z::Furlong) = x*y + z +Base.muladd(x::TNumber, y::Furlong, z::Furlong) = x*y + z +Base.muladd(x::Furlong, y::TNumber, z::Furlong) = x*y + z + +end From b932157a8dda8bbbc4e1f4874b12f35bf0036f5b Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Tue, 19 Aug 2025 16:11:58 +0200 Subject: [PATCH 2/6] Added comment for module Furlongs --- test/Furlongs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Furlongs.jl b/test/Furlongs.jl index 3fef6683..e06b49b4 100644 --- a/test/Furlongs.jl +++ b/test/Furlongs.jl @@ -1,4 +1,4 @@ -module Furlongs +module Furlongs # Provides a non-standard number type, Furlong, for testing purposes. Adapted from https://github.com/JuliaLang/julia/blob/v1.11.6/test/testhelpers/Furlongs.jl export Furlong From 3ef7c377508e2f43a0f04ebf22b1874091ffdc5d Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Tue, 19 Aug 2025 16:13:11 +0200 Subject: [PATCH 3/6] Limited exp, cos, and sin to only be defined for dimensionless furlongs Co-authored-by: David Widmann --- test/DerivativeTest.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/DerivativeTest.jl b/test/DerivativeTest.jl index 206a2d68..6461add3 100644 --- a/test/DerivativeTest.jl +++ b/test/DerivativeTest.jl @@ -115,9 +115,9 @@ end end @testset "non-standard numbers" begin - Base.exp(f::Furlongs.Furlong) = exp(f.val) - Base.cos(f::Furlongs.Furlong) = cos(f.val) - Base.sin(f::Furlongs.Furlong) = sin(f.val) + Base.exp(f::Furlongs.Furlong{0}) = exp(f.val) + Base.cos(f::Furlongs.Furlong{0}) = cos(f.val) + Base.sin(f::Furlongs.Furlong{0}) = sin(f.val) furlong = Furlongs.Furlong{2}(1.0) From f88637ed1dc117aa4cbf1641571d580232b56041 Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Sun, 21 Sep 2025 04:47:49 +0000 Subject: [PATCH 4/6] Moved Base.exp, Base.cos, and Base.sin definitions to Furlongs module --- test/DerivativeTest.jl | 4 ---- test/Furlongs.jl | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/DerivativeTest.jl b/test/DerivativeTest.jl index 6461add3..190de923 100644 --- a/test/DerivativeTest.jl +++ b/test/DerivativeTest.jl @@ -115,10 +115,6 @@ end end @testset "non-standard numbers" begin - Base.exp(f::Furlongs.Furlong{0}) = exp(f.val) - Base.cos(f::Furlongs.Furlong{0}) = cos(f.val) - Base.sin(f::Furlongs.Furlong{0}) = sin(f.val) - furlong = Furlongs.Furlong{2}(1.0) f(x) = exp(x) + 4*sin(x)*oneunit(x) diff --git a/test/Furlongs.jl b/test/Furlongs.jl index e06b49b4..4bedf230 100644 --- a/test/Furlongs.jl +++ b/test/Furlongs.jl @@ -107,4 +107,8 @@ Base.muladd(x::TNumber, y::TNumber, z::Furlong) = x*y + z Base.muladd(x::TNumber, y::Furlong, z::Furlong) = x*y + z Base.muladd(x::Furlong, y::TNumber, z::Furlong) = x*y + z +Base.exp(f::Furlongs.Furlong{0}) = exp(f.val) +Base.cos(f::Furlongs.Furlong{0}) = cos(f.val) +Base.sin(f::Furlongs.Furlong{0}) = sin(f.val) + end From 2d905d45bfdf43b6e2a03fd8afbad6bff68bebb5 Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Sun, 21 Sep 2025 06:54:42 +0200 Subject: [PATCH 5/6] Reverted limitation of exp, cos, and sin to only be defined for dimensionless Furlongs --- test/Furlongs.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Furlongs.jl b/test/Furlongs.jl index 4bedf230..a4578c41 100644 --- a/test/Furlongs.jl +++ b/test/Furlongs.jl @@ -107,8 +107,8 @@ Base.muladd(x::TNumber, y::TNumber, z::Furlong) = x*y + z Base.muladd(x::TNumber, y::Furlong, z::Furlong) = x*y + z Base.muladd(x::Furlong, y::TNumber, z::Furlong) = x*y + z -Base.exp(f::Furlongs.Furlong{0}) = exp(f.val) -Base.cos(f::Furlongs.Furlong{0}) = cos(f.val) -Base.sin(f::Furlongs.Furlong{0}) = sin(f.val) +Base.exp(f::Furlongs.Furlong) = exp(f.val) +Base.cos(f::Furlongs.Furlong) = cos(f.val) +Base.sin(f::Furlongs.Furlong) = sin(f.val) end From 41999eb3aaeda1b92db837a61903c23a5074ac3d Mon Sep 17 00:00:00 2001 From: Jesper Stemann Andersen Date: Sun, 21 Sep 2025 07:30:19 +0200 Subject: [PATCH 6/6] Added test that demonstrates issue with non-standard number types --- test/PartialsTest.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/PartialsTest.jl b/test/PartialsTest.jl index 8372a53e..5d30360e 100644 --- a/test/PartialsTest.jl +++ b/test/PartialsTest.jl @@ -5,6 +5,8 @@ using Random using ForwardDiff using ForwardDiff: Partials +include(joinpath(dirname(@__FILE__), "Furlongs.jl")) + samerng() = MersenneTwister(1) @testset "Partials{$N,$T}" for N in (0, 3), T in (Int, Float32, Float64) @@ -131,6 +133,10 @@ samerng() = MersenneTwister(1) @test ForwardDiff._mul_partials(ZEROS, ZEROS, NaN, Inf).values == ZEROS.values end end + + @testset "non-standard numbers" begin # Will be fixed by changing single_seed to use oneunit rather than one + @test_throws MethodError ForwardDiff.construct_seeds(ForwardDiff.Partials{3, Furlongs.Furlong{1, Float64}}) + end end io = IOBuffer()