Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symbolics.jl support #144

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Rebase Measurement's internal type to be Real-based, not `Abstrac…
…tFloat`

This is a fairly mechanical change that only changes `AbstractFloat`
where it is used as `T` of `Measurement{T}`.
LebedevRI committed Nov 10, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 98c0ac34a7ab3a472a61fc3a20654bcef969deaa
4 changes: 2 additions & 2 deletions docs/src/appendix.md
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ The `Measurement` Type
`Measurement` is a
[composite](https://docs.julialang.org/en/v1/manual/types/#Composite-Types-1)
[parametric](https://docs.julialang.org/en/v1/manual/types/#Parametric-Types-1)
type, whose parameter is the `AbstractFloat` subtype of the nominal value and
type, whose parameter is the `Real` subtype of the nominal value and
the uncertainty of the measurement. `Measurement` type itself is subtype of
`AbstractFloat`, thus `Measurement` objects can be used in any function taking
`AbstractFloat` arguments without redefining it, and calculation of uncertainty
@@ -20,7 +20,7 @@ will be exact.
In detail, this is the definition of the type:

```julia
struct Measurement{T<:AbstractFloat} <: AbstractFloat
struct Measurement{T<:Real} <: AbstractFloat
val::T
err::T
tag::UInt64
14 changes: 7 additions & 7 deletions ext/MeasurementsSpecialFunctionsExt.jl
Original file line number Diff line number Diff line change
@@ -30,40 +30,40 @@ else
end
# Error function: erf, erfinv, erfc, erfcinv, erfcx, erfi, dawson

function SpecialFunctions.erf(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erf(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(erf(aval), 2*exp(-abs2(aval))/sqrt(T(pi)), a)
end

function SpecialFunctions.erfinv(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erfinv(a::Measurement{T}) where {T<:Real}
res = erfinv(a.val)
# For the derivative, see http://mathworld.wolfram.com/InverseErf.html
return result(res, sqrt(T(pi)) * exp(abs2(res)) / 2, a)
end

function SpecialFunctions.erfc(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erfc(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(erfc(aval), -2*exp(-abs2(aval))/sqrt(T(pi)), a)
end

function SpecialFunctions.erfcinv(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erfcinv(a::Measurement{T}) where {T<:Real}
res = erfcinv(a.val)
# For the derivative, see http://mathworld.wolfram.com/InverseErfc.html
return result(res, -sqrt(T(pi)) * exp(abs2(res)) / 2, a)
end

function SpecialFunctions.erfcx(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erfcx(a::Measurement{T}) where {T<:Real}
aval = a.val
res = erfcx(aval)
return result(res, 2 * (aval * res - inv(sqrt(T(pi)))), a)
end

function SpecialFunctions.erfi(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.erfi(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(erfi(aval), 2*exp(abs2(aval))/sqrt(T(pi)), a)
end

function SpecialFunctions.dawson(a::Measurement{T}) where {T<:AbstractFloat}
function SpecialFunctions.dawson(a::Measurement{T}) where {T<:Real}
aval = a.val
res = dawson(aval)
return result(res, one(T) - 2 * aval * res, a)
14 changes: 7 additions & 7 deletions src/Measurements.jl
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ include("derivatives-type.jl")
# measurement and propagate the uncertainty in the case of functions with
# more than one argument (in order to deal with correlation between
# arguments).
struct Measurement{T<:AbstractFloat} <: AbstractFloat
struct Measurement{T<:Real} <: AbstractFloat
val::T
err::T
tag::UInt64
@@ -72,16 +72,16 @@ function Measurement{T}(::S) where {T, S}
end

# Functions to quickly create an empty Derivatives object.
@generated empty_der1(x::Measurement{T}) where {T<:AbstractFloat} = Derivatives{T}()
@generated empty_der2(x::T) where {T<:AbstractFloat} = Derivatives{x}()
@generated empty_der1(x::Measurement{T}) where {T<:Real} = Derivatives{T}()
@generated empty_der2(x::T) where {T<:Real} = Derivatives{x}()

# Start from 1, 0 is reserved to derived quantities
const tag_counter = Threads.Atomic{UInt64}(1)

measurement(x::Measurement) = x
measurement(val::T) where {T<:AbstractFloat} = Measurement(val, zero(T), UInt64(0), empty_der2(val))
measurement(val::Real) = measurement(float(val))
function measurement(val::T, err::T) where {T<:AbstractFloat}
measurement(val::T) where {T<:AbstractFloat} = Measurement(val, zero(T), UInt64(0), empty_der2(val)) # FIXME
measurement(val::Real) = measurement(float(val)) # FIXME
function measurement(val::T, err::T) where {T<:AbstractFloat} # FIXME
newder = empty_der2(val)
if iszero(err)
Measurement{T}(val, err, UInt64(0), newder)
@@ -90,7 +90,7 @@ function measurement(val::T, err::T) where {T<:AbstractFloat}
return Measurement{T}(val, err, tag, Derivatives(newder, (val, err, tag)=>one(T)))
end
end
measurement(val::Real, err::Real) = measurement(promote(float(val), float(err))...)
measurement(val::Real, err::Real) = measurement(promote(float(val), float(err))...) # FIXME
measurement(::Missing, ::Union{Real,Missing} = missing) = missing
const ± = measurement

18 changes: 9 additions & 9 deletions src/conversions.jl
Original file line number Diff line number Diff line change
@@ -16,15 +16,15 @@
#
### Code:

Base.convert(::Type{Measurement{T}}, a::Irrational) where {T<:AbstractFloat} =
Base.convert(::Type{Measurement{T}}, a::Irrational) where {T<:Real} =
measurement(T(a))::Measurement{T}
Base.convert(::Type{Measurement{T}}, a::Rational{<:Integer}) where {T<:AbstractFloat} =
Base.convert(::Type{Measurement{T}}, a::Rational{<:Integer}) where {T<:Real} =
measurement(T(a))::Measurement{T}
Base.convert(::Type{Measurement{T}}, a::Real) where {T<:AbstractFloat} =
Base.convert(::Type{Measurement{T}}, a::Real) where {T<:Real} =
measurement(T(a))::Measurement{T}
Base.convert(::Type{Measurement{T}}, a::Base.TwicePrecision) where {T<:AbstractFloat} =
Base.convert(::Type{Measurement{T}}, a::Base.TwicePrecision) where {T<:Real} =
measurement(T(a))::Measurement{T}
Base.convert(::Type{Measurement{T}}, a::AbstractChar) where {T<:AbstractFloat} =
Base.convert(::Type{Measurement{T}}, a::AbstractChar) where {T<:Real} =
measurement(T(a))::Measurement{T}

function Base.convert(::Type{Measurement{T}}, a::Complex) where {T}
@@ -35,9 +35,9 @@ function Base.convert(::Type{Measurement{T}}, a::Complex) where {T}
end
end

Base.convert(::Type{Measurement{T}}, a::Measurement{T}) where {T<:AbstractFloat} = a
Base.convert(::Type{Measurement{T}}, a::Measurement{T}) where {T<:Real} = a
function Base.convert(::Type{Measurement{T}},
a::Measurement{<:AbstractFloat}) where {T<:AbstractFloat}
a::Measurement{<:Real}) where {T<:Real}
newder = empty_der2(zero(T))
for tag in keys(a.der)
newder = Derivatives(newder, (T(tag[1]), T(tag[2]), tag[3])=>T(a.der[tag]))
@@ -55,10 +55,10 @@ function Base.convert(::Type{Int}, a::Measurement)
return convert(Int, a.val)::Int
end

Base.promote_rule(::Type{Measurement{T}}, ::Type{S}) where {T<:AbstractFloat, S<:Real} =
Base.promote_rule(::Type{Measurement{T}}, ::Type{S}) where {T<:Real, S<:Real} =
Measurement{promote_type(T, S)}
Base.promote_rule(::Type{Measurement{T}},
::Type{Measurement{S}}) where {T<:AbstractFloat, S<:AbstractFloat} =
::Type{Measurement{S}}) where {T<:Real, S<:Real} =
Measurement{promote_type(T, S)}

# adaptation of JuliaLang/julia#30952
58 changes: 29 additions & 29 deletions src/math.jl
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ export @uncertain
# σ_G = |σ_a·∂G/∂a|
# The list of derivatives with respect to each measurement is updated with
# ∂G/∂a · previous_derivatives
@inline function result(val::T, der::Real, a::Measurement{<:AbstractFloat}) where {T<:Real}
@inline function result(val::T, der::Real, a::Measurement{<:Real}) where {T<:Real}
newder = empty_der1(a)
@inbounds for tag in keys(a.der)
if ! iszero(tag[2]) # Skip values with 0 uncertainty
@@ -56,9 +56,9 @@ end
# Get the common type parameter of a collection of Measurement objects. The first two
# methods are for the trivial cases of homogeneous tuples and arrays, the last, inefficient,
# method is for inhomogeneous collections (probably the least common case).
gettype(::Tuple{Measurement{T}, Vararg{Measurement{T}}}) where {T<:AbstractFloat} = T
gettype(::AbstractArray{Measurement{T}}) where {T<:AbstractFloat} = T
_eltype(::Measurement{T}) where {T<:AbstractFloat} = T
gettype(::Tuple{Measurement{T}, Vararg{Measurement{T}}}) where {T<:Real} = T
gettype(::AbstractArray{Measurement{T}}) where {T<:Real} = T
_eltype(::Measurement{T}) where {T<:Real} = T
gettype(collection) = promote_type(_eltype.(collection)...)

# This function is similar to the previous one, but applies to mathematical
@@ -249,7 +249,7 @@ function Base.:/(a::Measurement, b::Measurement)
return result(x / y, (oneovery, -x * abs2(oneovery)), (a, b))
end
Base.:/(a::Real, b::Measurement) = result(a/b.val, -a/abs2(b.val), b)
Base.:/(a::Measurement{T}, b::Real) where {T<:AbstractFloat} = result(a.val/b, 1/T(b), a)
Base.:/(a::Measurement{T}, b::Real) where {T<:Real} = result(a.val/b, 1/T(b), a)

# 0.0 as partial derivative for both arguments of "div", "fld", "cld" should be
# correct for most cases. This has been tested against "@uncertain" macro.
@@ -289,7 +289,7 @@ function Base.:^(a::Measurement, b::Integer)
return result(x ^ b, b * x ^ (b - 1), a)
end

function Base.:^(a::Measurement{T}, b::Rational) where {T<:AbstractFloat}
function Base.:^(a::Measurement{T}, b::Rational) where {T<:Real}
x = a.val
return result(x ^ b, b * x ^ (b - one(T)), a)
end
@@ -306,7 +306,7 @@ function Base.:^(a::Real, b::Measurement)
return result(res, res*log(a), b)
end

function Base.exp2(a::Measurement{T}) where {T<:AbstractFloat}
function Base.exp2(a::Measurement{T}) where {T<:Real}
pow = exp2(a.val)
return result(pow, pow*log(T(2)), a)
end
@@ -411,47 +411,47 @@ end
# Inverse trig functions: acos, acosd, acosh, asin, asind, asinh, atan, atand, atanh,
# asec, acsc, acot, asech, acsch, acoth

function Base.acos(a::Measurement{T}) where {T<:AbstractFloat}
function Base.acos(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(acos(aval), -inv(sqrt(one(T) - abs2(aval))), a)
end

function Base.acosd(a::Measurement{T}) where {T<:AbstractFloat}
function Base.acosd(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(acosd(aval), -rad2deg(inv(sqrt(one(T) - abs2(aval)))), a)
end

function Base.acosh(a::Measurement{T}) where {T<:AbstractFloat}
function Base.acosh(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(acosh(aval), inv(sqrt(abs2(aval) - one(T))), a)
end

function Base.asin(a::Measurement{T}) where {T<:AbstractFloat}
function Base.asin(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(asin(aval), inv(sqrt(one(T) - abs2(aval))), a)
end

function Base.asind(a::Measurement{T}) where {T<:AbstractFloat}
function Base.asind(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(asind(aval), rad2deg(inv(sqrt(one(T) - abs2(aval)))), a)
end

function Base.asinh(a::Measurement{T}) where {T<:AbstractFloat}
function Base.asinh(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(asinh(aval), inv(hypot(aval, one(T))), a)
end

function Base.atan(a::Measurement{T}) where {T<:AbstractFloat}
function Base.atan(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(atan(aval), inv(abs2(aval) + one(T)), a)
end

function Base.atand(a::Measurement{T}) where {T<:AbstractFloat}
function Base.atand(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(atand(aval), rad2deg(inv(abs2(aval) + one(T))), a)
end

function Base.atanh(a::Measurement{T}) where {T<:AbstractFloat}
function Base.atanh(a::Measurement{T}) where {T<:Real}
aval = a.val
return result(atanh(aval), inv(one(T) - abs2(aval)), a)
end
@@ -572,7 +572,7 @@ function Base.expm1(a::Measurement)
return result(expm1(aval), exp(aval), a)
end

function Base.exp10(a::Measurement{T}) where {T<:AbstractFloat}
function Base.exp10(a::Measurement{T}) where {T<:Real}
val = exp10(a.val)
return result(val, log(T(10))*val, a)
end
@@ -582,7 +582,7 @@ function Base.frexp(a::Measurement)
return (result(x, inv(exp2(y)), a), y)
end

Base.ldexp(a::Measurement{T}, e::Integer) where {T<:AbstractFloat} =
Base.ldexp(a::Measurement{T}, e::Integer) where {T<:Real} =
result(ldexp(a.val, e), ldexp(one(T), e), a)

# Logarithms
@@ -600,17 +600,17 @@ function Base.log(a::Measurement) # Special case
return result(log(aval), inv(aval), a)
end

function Base.log2(a::Measurement{T}) where {T<:AbstractFloat} # Special case
function Base.log2(a::Measurement{T}) where {T<:Real} # Special case
x = a.val
return result(log2(x), inv(log(T(2)) * x), a)
end

function Base.log10(a::Measurement{T}) where {T<:AbstractFloat} # Special case
function Base.log10(a::Measurement{T}) where {T<:Real} # Special case
aval = a.val
return result(log10(aval), inv(log(T(10)) * aval), a)
end

function Base.log1p(a::Measurement{T}) where {T<:AbstractFloat} # Special case
function Base.log1p(a::Measurement{T}) where {T<:Real} # Special case
aval = a.val
return result(log1p(aval), inv(aval + one(T)), a)
end
@@ -719,18 +719,18 @@ Base.rem2pi(a::Measurement, r::RoundingMode) = result(rem2pi(a.val, r), 1, a)

### Machine precision

Base.eps(::Type{Measurement{T}}) where {T<:AbstractFloat} = eps(T)
Base.eps(::Type{Measurement{T}}) where {T<:Real} = eps(T)
Base.eps(a::Measurement) = eps(a.val)

Base.nextfloat(a::Measurement) = result(nextfloat(a.val), 1, a)
Base.nextfloat(a::Measurement, n::Integer) = result(nextfloat(a.val, n), 1, a)

Base.maxintfloat(::Type{Measurement{T}}) where {T<:AbstractFloat} = maxintfloat(T)
Base.maxintfloat(::Type{Measurement{T}}) where {T<:Real} = maxintfloat(T)

Base.floatmin(::Type{Measurement{T}}) where {T<:AbstractFloat} = floatmin(T) ± zero(T)
Base.floatmax(::Type{Measurement{T}}) where {T<:AbstractFloat} = floatmax(T) ± zero(T)
Base.floatmin(::Type{Measurement{T}}) where {T<:Real} = floatmin(T) ± zero(T)
Base.floatmax(::Type{Measurement{T}}) where {T<:Real} = floatmax(T) ± zero(T)

Base.typemax(::Type{Measurement{T}}) where {T<:AbstractFloat} = typemax(T)
Base.typemax(::Type{Measurement{T}}) where {T<:Real} = typemax(T)

### Rounding

@@ -766,13 +766,13 @@ Base.trunc(::Type{Bool}, x::Measurement) = measurement(trunc(Bool, value(x)))

# Widening

Base.widen(::Type{Measurement{T}}) where {T<:AbstractFloat} = Measurement{widen(T)}
Base.widen(::Type{Measurement{T}}) where {T<:Real} = Measurement{widen(T)}

# To big float

Base.big(::Type{Measurement}) = Measurement{BigFloat}
Base.big(::Type{Measurement{T}}) where {T<:AbstractFloat} = Measurement{BigFloat}
Base.big(x::Measurement{<:AbstractFloat}) = convert(Measurement{BigFloat}, x)
Base.big(::Type{Measurement{T}}) where {T<:Real} = Measurement{BigFloat}
Base.big(x::Measurement{<:Real}) = convert(Measurement{BigFloat}, x)
Base.big(x::Complex{<:Measurement}) = convert(Complex{Measurement{BigFloat}}, x)

# Sum and prod
4 changes: 2 additions & 2 deletions src/parsing.jl
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ julia> measurement("-1234e-1")
"""
measurement(str::AbstractString) = parse(Measurement{Float64}, str)

function Base.tryparse(::Type{Measurement{T}}, str::S) where {T<:AbstractFloat, S<:AbstractString}
function Base.tryparse(::Type{Measurement{T}}, str::S) where {T<:Real, S<:AbstractString}
m = match(rxp_error_with_parentheses, str)
if m !== nothing # "123(45)e6"
val_str::S, val_dec, err_str::S, err_dec_str, expn = m.captures
@@ -128,7 +128,7 @@ function Base.tryparse(::Type{Measurement{T}}, str::S) where {T<:AbstractFloat,
return measurement(val, err)
end

function Base.parse(::Type{Measurement{T}}, str::S) where {T<:AbstractFloat, S<:AbstractString}
function Base.parse(::Type{Measurement{T}}, str::S) where {T<:Real, S<:AbstractString}
out = tryparse(Measurement{T}, str)
out === nothing && throw(ArgumentError("cannot parse $(repr(str)) as Measurement{$T}"))
return out
2 changes: 2 additions & 0 deletions src/show.jl
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
#
# This file defines the methods to represent `Measurement` objects in various places.
#
# FIXME: should special-handle non-AbstractFloat T's.
#
### Code:

import Printf
6 changes: 3 additions & 3 deletions src/utils.jl
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ computed as the standard score between their difference and 0:

stdscore(measure_1 - measure_2, 0)
"""
stdscore(a::Measurement{S}, b::Measurement{T}) where {S<:AbstractFloat,T<:AbstractFloat} =
stdscore(a::Measurement{S}, b::Measurement{T}) where {S<:Real,T<:Real} =
stdscore(a - b, zero(promote_type(S, T)))

# Weighted Average with Inverse-Variance Weighting
@@ -39,7 +39,7 @@ end

# Derivative and Gradient
derivative(a::Measurement{F},
tag::Tuple{T, T, UInt64}) where {F<:AbstractFloat, T<:AbstractFloat} =
tag::Tuple{T, T, UInt64}) where {F<:Real, T<:Real} =
get(a.der, tag, zero(F))

"""
@@ -98,7 +98,7 @@ of an independent `Measurement`, and the value is the absolute value of the
product between its uncertainty and the partial derivative of `x` with respect
to this `Measurement`.
"""
function uncertainty_components(x::Measurement{T}) where {T<:AbstractFloat}
function uncertainty_components(x::Measurement{T}) where {T<:Real}
out = Dict{Tuple{T, T, UInt64}, T}()
for var in keys(x.der)
out[var] = abs(var[2] * Measurements.derivative(x, var))
4 changes: 2 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -24,8 +24,8 @@ end
isapprox(x::Measurement, y::Measurement; rest...) =
isapprox(x.val, y.val; nans = true, rest...) &&
isapprox(x.err, y.err; nans = true, rest...)
isapprox(x::Complex{Measurement{<:AbstractFloat}},
y::Complex{Measurement{<:AbstractFloat}}; rest...) =
isapprox(x::Complex{Measurement{<:Real}},
y::Complex{Measurement{<:Real}}; rest...) =
isapprox(real(x), real(y); nans = true, rest...) &&
isapprox(imag(x), imag(y); nans = true, rest...)
# This is bit strict, but the idea is that in the tests we want `Measurement`s to be