one(measurement) should return 1, not 1 ± 0
stevengj opened this issue · comments
I would suggest defining:
Base.one(::Type{Measurement{T}}) where T = one(T)
which still a correct answer since it is still a multiplicative identity for a Measurement
(type promotion takes care of the rest). In general, one(x)
need not return the same type as x
in Julia — if the caller wants the same type, they should use oneunit(x)
.
one(x)
is commonly used to "strip the units" from a quantity, and it would provide a nice generic way to extract the underlying scalar type from a measurement. For example, this code in QuadGK.jl should work as-is for Measurement
if you define one
, and you should no longer need to implement specialized QuadGK methods. (Hopefully you can eliminate your QuadGK dependency completely and it will just work.)
Uhm, if I define
Base.one(::Type{Measurement{T}}) where T = one(T)
Base.one(::Type{Complex{Measurement{T}}}) where T = one(Complex{T})
then some operations are no longer inferred correctly:
julia> @code_warntype(complex(measurement(3)) ^ 2.5)
MethodInstance for ^(::Complex{Measurement{Float64}}, ::Float64)
from ^(z::Complex{T}, p::S) where {T<:Real, S<:Real} @ Base complex.jl:873
Static Parameters
T = Measurement{Float64}
S = Float64
Arguments
#self#::Core.Const(^)
z::Complex{Measurement{Float64}}
p::Float64
Locals
P::Type{Measurement{Float64}}
Body::Union{Complex{Measurement{Float64}}, ComplexF64}
1 ─ (P = Base.promote_type($(Expr(:static_parameter, 1)), $(Expr(:static_parameter, 2))))
│ %2 = Core.apply_type(Base.Complex, P::Core.Const(Measurement{Float64}))::Core.Const(Complex{Measurement{Float64}})
│ %3 = (%2)(z)::Complex{Measurement{Float64}}
│ %4 = (P::Core.Const(Measurement{Float64}))(p)::Core.PartialStruct(Measurement{Float64}, Any[Float64, Core.Const(0.0), Core.Const(0x0000000000000000), Core.Const(Measurements.Derivatives{Float64}())])
│ %5 = (%3 ^ %4)::Union{Complex{Measurement{Float64}}, ComplexF64}
└── return %5
julia> A = [(14 ± 0.1) (23 ± 0.2); (-12 ± 0.3) (29 ± 0.4)];
julia> using LinearAlgebra
julia> @code_warntype det(A)
MethodInstance for LinearAlgebra.det(::Matrix{Measurement{Float64}})
from det(A::AbstractMatrix{T}) where T @ LinearAlgebra ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/LinearAlgebra/src/generic.jl:1617
Static Parameters
T = Measurement{Float64}
Arguments
#self#::Core.Const(LinearAlgebra.det)
A::Matrix{Measurement{Float64}}
Locals
S::Type{Measurement{Float64}}
Body::Union{Float64, Measurement{Float64}}
1 ─ Core.NewvarNode(:(S))
│ %2 = LinearAlgebra.istriu(A)::Bool
└── goto #3 if not %2
2 ─ goto #4
3 ─ %5 = LinearAlgebra.istril(A)::Bool
└── goto #5 if not %5
4 ┄ %7 = $(Expr(:static_parameter, 1))::Core.Const(Measurement{Float64})
│ %8 = LinearAlgebra.one($(Expr(:static_parameter, 1)))::Core.Const(1.0)
│ %9 = LinearAlgebra.zero($(Expr(:static_parameter, 1)))::Core.Const(0.0 ± 0.0)
│ %10 = (%8 * %9)::Core.Const(0.0 ± 0.0)
│ %11 = LinearAlgebra.zero($(Expr(:static_parameter, 1)))::Core.Const(0.0 ± 0.0)
│ %12 = (%10 + %11)::Core.PartialStruct(Measurement{Float64}, Any[Core.Const(0.0), Float64, Core.Const(0x0000000000000000), Measurements.Derivatives{Float64}])
│ %13 = LinearAlgebra.one($(Expr(:static_parameter, 1)))::Core.Const(1.0)
│ %14 = (%12 / %13)::Core.PartialStruct(Measurement{Float64}, Any[Core.Const(0.0), Float64, Core.Const(0x0000000000000000), Measurements.Derivatives{Float64}])
│ %15 = LinearAlgebra.typeof(%14)::Core.Const(Measurement{Float64})
│ (S = LinearAlgebra.promote_type(%7, %15))
│ %17 = S::Core.Const(Measurement{Float64})
│ %18 = LinearAlgebra.UpperTriangular(A)::UpperTriangular{Measurement{Float64}, Matrix{Measurement{Float64}}}
│ %19 = LinearAlgebra.det(%18)::Core.PartialStruct(Measurement{Float64}, Any[Float64, Float64, Core.Const(0x0000000000000000), Measurements.Derivatives{Float64}])
│ %20 = LinearAlgebra.convert(%17, %19)::Core.PartialStruct(Measurement{Float64}, Any[Float64, Float64, Core.Const(0x0000000000000000), Measurements.Derivatives{Float64}])
└── return %20
5 ─ %22 = (:check,)::Core.Const((:check,))
│ %23 = Core.apply_type(Core.NamedTuple, %22)::Core.Const(NamedTuple{(:check,)})
│ %24 = Core.tuple(false)::Core.Const((false,))
│ %25 = (%23)(%24)::Core.Const((check = false,))
│ %26 = Core.kwcall(%25, LinearAlgebra.lu, A)::LU{Measurement{Float64}, Matrix{Measurement{Float64}}, Vector{Int64}}
│ %27 = LinearAlgebra.det(%26)::Union{Float64, Measurement{Float64}}
└── return %27
which breaks tests at
Measurements.jl/test/runtests.jl
Lines 288 to 289 in daa1ad2
Measurements.jl/test/runtests.jl
Line 732 in daa1ad2
And the accuracy of a bunch of QuadGK tests gets worse:
QuadGK: Test Failed at /home/mose/.julia/dev/Measurements/test/runtests.jl:852
Expression: ≈((QuadGK.quadgk(sin, -y, y))[1], cos(-y) - cos(y), atol = eps(Float64))
Evaluated: -2.23e-16 ± 2.7e-17 ≈ 0.0 ± 0.0 (atol=2.220446049250313e-16)
Stacktrace:
[1] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:477 [inlined]
[2] macro expansion
@ ~/.julia/dev/Measurements/test/runtests.jl:852 [inlined]
[3] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:1496 [inlined]
[4] top-level scope
@ ~/.julia/dev/Measurements/test/runtests.jl:851
QuadGK: Test Failed at /home/mose/.julia/dev/Measurements/test/runtests.jl:855
Expression: ≈((QuadGK.quadgk((t->(cos(x - t);)), 0, 2pi))[1], measurement(0), atol = 7.0e-16)
Evaluated: -1.063e-13 ± 2.9e-15 ≈ 0.0 ± 0.0 (atol=7.0e-16)
Stacktrace:
[1] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:477 [inlined]
[2] macro expansion
@ ~/.julia/dev/Measurements/test/runtests.jl:855 [inlined]
[3] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:1496 [inlined]
[4] top-level scope
@ ~/.julia/dev/Measurements/test/runtests.jl:851
QuadGK: Test Failed at /home/mose/.julia/dev/Measurements/test/runtests.jl:869
Expression: ≈((QuadGK.quadgk(sin, -y, y))[1], #= /home/mose/.julia/dev/Measurements/test/runtests.jl:870 =# @uncertain(((x->begin
(QuadGK.quadgk(sin, -x, x))[1]
end))(y)), atol = 1.0e-10)
Evaluated: -2.23e-16 ± 2.7e-17 ≈ -2.2e-16 ± 4.6e-10 (atol=1.0e-10)
Stacktrace:
[1] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:477 [inlined]
[2] macro expansion
@ ~/.julia/dev/Measurements/test/runtests.jl:869 [inlined]
[3] macro expansion
@ ~/.julia/juliaup/julia-1.9.0-beta4+0.x64.linux.gnu/share/julia/stdlib/v1.9/Test/src/Test.jl:1496 [inlined]
[4] top-level scope
@ ~/.julia/dev/Measurements/test/runtests.jl:851
Test Summary: | Pass Fail Total Time
QuadGK | 16 3 19 14.0s
although the prospect of deleting the src/quadgk.jl
file is quite appealing.
I presume the problem with the complex power is the use of one
at https://github.com/JuliaLang/julia/blob/afeda9f8cf0776461eaf565333e2159122d70bc4/base/complex.jl#L789?
Edit: no, or not only that: replacing one
with oneunit
on that line doesn't fix the inference failure.
And the accuracy of a bunch of QuadGK tests gets worse:
Actually, that's independent from this change, accuracy got worse in v2.8.1 of QuadGK, it's restored if I downgraded it to v2.8.0.
In particular, with 2.8.0:
julia> QuadGK.quadgk(t -> cos(3 - t), 0, 2pi)
(6.106226635438361e-16, 0.0)
with 2.8.1:
julia> QuadGK.quadgk(t -> cos(3 - t), 0, 2pi)
(-1.0625509660110193e-13, 1.1749641421160047e-23)
This has nothing to do with Measurements.jl
@stevengj Additionally, v2.6.0 of QuadGK broke another test here:
Measurements.jl/test/runtests.jl
Lines 925 to 930 in daa1ad2
julia> using Unitful, Measurements, QuadGK
julia> f(x) = x^2
f (generic function with 1 method)
julia> F(x) = x^3 / 3
F (generic function with 1 method)
julia> a = (5 ± 0.1)u"m"
5.0 ± 0.1 m
julia> b = (10 ± 1)u"m"
10.0 ± 1.0 m
julia> QuadGK.quadgk(f, a, b)
ERROR: MethodError: no method matching Float64(::Measurement{Float64})
Closest candidates are:
(::Type{T})(::Real, ::RoundingMode) where T<:AbstractFloat at rounding.jl:200
(::Type{T})(::T) where T<:Number at boot.jl:772
(::Type{T})(::AbstractChar) where T<:Union{AbstractChar, Number} at char.jl:50
...
Stacktrace:
[1] convert(#unused#::Type{Float64}, x::Measurement{Float64})
@ Base ./number.jl:7
[2] setindex!(A::Vector{Float64}, x::Measurement{Float64}, i1::Int64)
@ Base ./array.jl:966
[3] eignewt(b::Vector{Measurement{Float64}}, m::Int64, n::Int64)
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/gausskronrod.jl:43
[4] kronrod(#unused#::Type{Measurement{Float64}}, n::Int64)
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/gausskronrod.jl:197
[5] macro expansion
@ ~/.julia/packages/QuadGK/gxzkm/src/gausskronrod.jl:259 [inlined]
[6] _cachedrule(#unused#::Type{Measurement{Float64}}, n::Int64)
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/gausskronrod.jl:259
[7] cachedrule
@ ~/.julia/packages/QuadGK/gxzkm/src/gausskronrod.jl:264 [inlined]
[8] do_quadgk(f::typeof(f), s::Tuple{Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}, n::Int64, atol::Nothing, rtol::Nothing, maxevals::Int64, nrm::typeof(LinearAlgebra.norm), segbuf::Nothing)
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/adapt.jl:7
[9] #46
@ ~/.julia/packages/QuadGK/gxzkm/src/adapt.jl:219 [inlined]
[10] handle_infinities(workfunc::QuadGK.var"#46#47"{Nothing, Nothing, Int64, Int64, typeof(LinearAlgebra.norm), Nothing}, f::typeof(f), s::Tuple{Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}})
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/adapt.jl:118
[11] quadgk(::Function, ::Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, ::Vararg{Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}; atol::Nothing, rtol::Nothing, maxevals::Int64, order::Int64, norm::Function, segbuf::Nothing)
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/adapt.jl:218
[12] quadgk(::Function, ::Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}, ::Quantity{Measurement{Float64}, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}})
@ QuadGK ~/.julia/packages/QuadGK/BYxcx/src/adapt.jl:216
[13] top-level scope
@ REPL[36]:1
Although this would be fixed in #135 by defining the one
method you suggested.
QuadGK.quadgk(t -> cos(3 - t), 0, 2pi)
The basic problem with this test is that you are asking it to compute an integral that is zero to a relative tolerance of ≈ 1e-8
(the default), which is basically impossible. So, the convergence criterion in practice depends on arbitrary details of roundoff errors, so e.g. slightly changing the order of the integrand evaluation (as we did in JuliaMath/QuadGK.jl@962c801 for 2.8.1) causes it to stop at a very different point.
The solution, as explained in the QuadGK manual, is that whenever you have an integral that might be zero, you should pass in a nonzero atol
. Then it works fine in both 2.8.1, though it still can't converge to better than machine precision:
julia> quadgk_count(t -> cos(3 - t), 0, 2pi, atol=1e-15)
(2.220446049250313e-16, 5.551115123125783e-16, 105)
and 2.8.0:
julia> quadgk_count(t -> cos(3 - t), 0, 2pi, atol=1e-15)
(3.3306690738754696e-16, 2.220446049250313e-16, 105)
though the answers are very slightly different due to changes in roundoff.
For powers, oneunit
is not correct since if you have a unitful number x
then x^0
should be unitless, i.e. the x^n
function is inherently type-unstable for dimensionful numbers.
The alternative would be to define some other method to get the "scalar" type from a numeric type. Maybe call it scalarone(x)
or something like that, then define a package ScalarOne.jl and try to get all of the packages (Unitful, ForwardDiff, etcetera) to adopt it. It could call scalarone(x::Number) = one(x)
as a fallback, so it would be no worse than what we have now, I guess.