Skip to content

Add many other numeric methods #78

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

Merged
merged 14 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
92 changes: 90 additions & 2 deletions src/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES
@eval begin
Base.:*(l::$type, r::$type) = new_quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r))
Base.:/(l::$type, r::$type) = new_quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r))
Base.div(x::$type, y::$type, r::RoundingMode=RoundToZero) = new_quantity(typeof(x), div(ustrip(x), ustrip(y), r), dimension(x) / dimension(y))

Base.:*(l::$type, r::$base_type) = new_quantity(typeof(l), ustrip(l) * r, dimension(l))
Base.:/(l::$type, r::$base_type) = new_quantity(typeof(l), ustrip(l) / r, dimension(l))
Base.div(x::$type, y::Number, r::RoundingMode=RoundToZero) = new_quantity(typeof(x), div(ustrip(x), y, r), dimension(x))

Base.:*(l::$base_type, r::$type) = new_quantity(typeof(r), l * ustrip(r), dimension(r))
Base.:/(l::$base_type, r::$type) = new_quantity(typeof(r), l / ustrip(r), inv(dimension(r)))
Base.div(x::Number, y::$type, r::RoundingMode=RoundToZero) = new_quantity(typeof(y), div(x, ustrip(y), r), inv(dimension(y)))

Base.:*(l::$type, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) * r)
Base.:/(l::$type, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) / r)
Expand Down Expand Up @@ -41,7 +44,7 @@ end
Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l))

# Combining different abstract types
for op in (:*, :/, :+, :-),
for op in (:*, :/, :+, :-, :div, :atan, :atand, :copysign, :flipsign, :mod),
(t1, _, _) in ABSTRACT_QUANTITY_TYPES,
(t2, _, _) in ABSTRACT_QUANTITY_TYPES

Expand Down Expand Up @@ -97,6 +100,91 @@ Base.sqrt(q::UnionAbstractQuantity) = new_quantity(typeof(q), sqrt(ustrip(q)), s
Base.cbrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 3))
Base.cbrt(q::UnionAbstractQuantity) = new_quantity(typeof(q), cbrt(ustrip(q)), cbrt(dimension(q)))

Base.abs(q::UnionAbstractQuantity) = new_quantity(typeof(q), abs(ustrip(q)), dimension(q))
Base.abs2(q::UnionAbstractQuantity) = new_quantity(typeof(q), abs2(ustrip(q)), dimension(q)^2)
Base.angle(q::UnionAbstractQuantity{T}) where {T<:Complex} = angle(ustrip(q))

############################## Require dimensionless input ##############################
# Note that :clamp, :cmp, :sign already work
# We skip :rad2deg, :deg2rad in case the user defines a rad or deg unit
for f in (
:sin, :cos, :tan, :sinh, :cosh, :tanh, :asin, :acos,
:asinh, :acosh, :atanh, :sec, :csc, :cot, :asec, :acsc, :acot, :sech, :csch,
:coth, :asech, :acsch, :acoth, :sinc, :cosc, :cosd, :cotd, :cscd, :secd,
:sinpi, :cospi, :sind, :tand, :acosd, :acotd, :acscd, :asecd, :asind,
:log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1, :frexp, :exponent,
)
@eval function Base.$f(q::UnionAbstractQuantity)
iszero(dimension(q)) || throw(DimensionError(q))
return $f(ustrip(q))
end
end
for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:atan, :atand)
@eval begin
function Base.$f(x::$type)
iszero(dimension(x)) || throw(DimensionError(x))
return $f(ustrip(x))
end
function Base.$f(y::$type, x::$type)
dimension(y) == dimension(x) || throw(DimensionError(y, x))
return $f(ustrip(y), ustrip(x))
end
function Base.$f(y::$type, x::$base_type)
iszero(dimension(y)) || throw(DimensionError(y))
return $f(ustrip(y), x)
end
function Base.$f(y::$base_type, x::$type)
iszero(dimension(x)) || throw(DimensionError(x))
return $f(y, ustrip(x))
end
end
end
#########################################################################################

############################## Same dimension as input ##################################
for f in (
:float, :abs, :real, :imag, :conj, :adjoint, :unsigned,
:nextfloat, :prevfloat, :identity, :transpose, :significand
)
@eval function Base.$f(q::UnionAbstractQuantity)
return new_quantity(typeof(q), $f(ustrip(q)), dimension(q))
end
end
for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, :mod)
# These treat the x as the magnitude, so we take the dimensions from there,
# and ignore any dimensions on y, since those will cancel out.
@eval begin
function Base.$f(x::$type, y::$type)
return new_quantity(typeof(x), $f(ustrip(x), ustrip(y)), dimension(x))
end
function Base.$f(x::$type, y::$base_type)
return new_quantity(typeof(x), $f(ustrip(x), y), dimension(x))
end
function Base.$f(x::$base_type, y::$type)
return $f(x, ustrip(y))
end
end
end
function Base.ldexp(x::UnionAbstractQuantity, n::Integer)
return new_quantity(typeof(x), ldexp(ustrip(x), n), dimension(x))
end
function Base.round(q::UnionAbstractQuantity, r::RoundingMode=RoundNearest)
return new_quantity(typeof(q), round(ustrip(q), r), dimension(q))
end
function Base.round(::Type{Ti}, q::UnionAbstractQuantity, r::RoundingMode=RoundNearest) where {Ti<:Integer}
return new_quantity(typeof(q), round(Ti, ustrip(q), r), dimension(q))
end
for f in (:floor, :trunc, :ceil)
@eval begin
function Base.$f(q::UnionAbstractQuantity)
return new_quantity(typeof(q), $f(ustrip(q)), dimension(q))
end
function Base.$f(::Type{Ti}, q::UnionAbstractQuantity) where {Ti<:Integer}
return new_quantity(typeof(q), $f(Ti, ustrip(q)), dimension(q))
end
end
end
function Base.modf(q::UnionAbstractQuantity)
output = modf(ustrip(q))
return ntuple(i -> new_quantity(typeof(q), output[i], dimension(q)), Val(2))
end
#########################################################################################
3 changes: 3 additions & 0 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,7 @@ end
struct DimensionError{Q1,Q2} <: Exception
q1::Q1
q2::Q2

DimensionError(q1, q2) = new{typeof(q1),typeof(q2)}(q1, q2)
DimensionError(q1) = DimensionError(q1, nothing)
end
11 changes: 5 additions & 6 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ end
return output
end

Base.float(q::UnionAbstractQuantity) = new_quantity(typeof(q), float(ustrip(q)), dimension(q))
Base.convert(::Type{Number}, q::AbstractQuantity) = q
function Base.convert(::Type{T}, q::UnionAbstractQuantity) where {T<:Number}
@assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead."
Expand Down Expand Up @@ -130,14 +129,13 @@ function Base.isless(l::Number, r::UnionAbstractQuantity)
end

# Simple flags:
for f in (:iszero, :isfinite, :isinf, :isnan, :isreal)
for f in (
:iszero, :isfinite, :isinf, :isnan, :isreal, :signbit,
:isempty, :iseven, :isodd, :isinteger, :ispow2
)
@eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q))
end

# Simple operations which return a full quantity (same dimensions)
for f in (:real, :imag, :conj, :adjoint, :unsigned, :nextfloat, :prevfloat)
@eval Base.$f(q::UnionAbstractQuantity) = new_quantity(typeof(q), $f(ustrip(q)), dimension(q))
end

# Base.one, typemin, typemax
for f in (:one, :typemin, :typemax)
Expand Down Expand Up @@ -195,6 +193,7 @@ tryrationalize(::Type{R}, x::Union{Rational,Integer}) where {R} = convert(R, x)
tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x)) : convert(R, rationalize(Int, x))

Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: ", e.q1, " and ", e.q2, " have incompatible dimensions")
Base.showerror(io::IO, e::DimensionError{<:Any,Nothing}) = print(io, "DimensionError: ", e.q1, " is not dimensionless")

Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {Q<:UnionAbstractQuantity} = q
Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {T,Q<:UnionAbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q))
Expand Down
163 changes: 155 additions & 8 deletions test/unittests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ using StaticArrays: SArray, MArray
using LinearAlgebra: norm
using Test

function record_show(s, f=show)
io = IOBuffer()
f(io, s)
return String(take!(io))
end

@testset "Basic utilities" begin

for Q in [Quantity, GenericQuantity], T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}]
Expand Down Expand Up @@ -149,6 +155,21 @@ using Test
@test isinf(x * Inf) == true
@test isnan(x) == false
@test isnan(x * NaN) == true
@test isreal(x) == true
@test isreal(x * (1 + 2im)) == false
@test signbit(x) == true
@test signbit(-x) == false
@test isempty(x) == false
@test isempty(GenericQuantity([0.0, 1.0])) == false
@test isempty(GenericQuantity(Float64[])) == true
@test iseven(Quantity(2, length=1)) == true
@test iseven(Quantity(3, length=1)) == false
@test isodd(Quantity(2, length=1)) == false
@test isodd(Quantity(3, length=1)) == true
@test isinteger(Quantity(2, length=1)) == true
@test isinteger(Quantity(2.1, length=1)) == false
@test ispow2(Quantity(2, length=1)) == true
@test ispow2(Quantity(3, length=1)) == false

@test nextfloat(x) == Quantity(nextfloat(-1.2), length=2 // 5)
@test prevfloat(x) == Quantity(prevfloat(-1.2), length=2 // 5)
Expand Down Expand Up @@ -432,14 +453,8 @@ end
# Conversion to Rational without specifying type
@test convert(Rational, FixedRational{UInt8,6}(2)) === Rational{UInt8}(2)

# Showing rationals
function show_string(i)
io = IOBuffer()
show(io, i)
return String(take!(io))
end
@test show_string(FixedRational{Int,10}(2)) == "2"
@test show_string(FixedRational{Int,10}(11//10)) == "11//10"
@test record_show(FixedRational{Int,10}(2)) == "2"
@test record_show(FixedRational{Int,10}(11//10)) == "11//10"

# Promotion rules
@test promote_type(FixedRational{Int64,10},FixedRational{BigInt,10}) == FixedRational{BigInt,10}
Expand Down Expand Up @@ -1104,3 +1119,135 @@ end
@test convert(typeof(qx), qy)[1] == convert(Quantity{Float64}, qy[1])
end
end

function is_input_valid(f, x)
try
f(x)
catch e
e isa DomainError && return false
rethrow(e)
end
return true
end

@testset "Assorted dimensionless functions" begin
functions = (
:sin, :cos, :tan, :sinh, :cosh, :tanh, :asin, :acos,
:asinh, :acosh, :atanh, :sec, :csc, :cot, :asec, :acsc, :acot, :sech, :csch,
:coth, :asech, :acsch, :acoth, :sinc, :cosc, :cosd, :cotd, :cscd, :secd,
:sinpi, :cospi, :sind, :tand, :acosd, :acotd, :acscd, :asecd, :asind,
:log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1, :frexp, :exponent,
:atan, :atand
)
for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions
# Only test on valid domain
valid_inputs = filter(
x -> is_input_valid(eval(f), x),
5rand(100) .- 2.5
)
for x in valid_inputs[1:3]
qx_dimensionless = Quantity(x, D)
qx_dimensions = Quantity(x, convert(D, dimension(u"m/s")))
@eval @test $f($qx_dimensionless) == $f($x)
@eval @test_throws DimensionError $f($qx_dimensions)
if f in (:atan, :atand)
for y in valid_inputs[end-3:end]
qy_dimensionless = Quantity(y, D)
qy_dimensions = Quantity(y, convert(D, dimension(u"m/s")))
@eval @test $f($y, $qx_dimensionless) == $f($y, $x)
@eval @test $f($qy_dimensionless, $x) == $f($y, $x)
@eval @test $f($qy_dimensionless, $qx_dimensionless) == $f($y, $x)
@eval @test $f($qy_dimensions, $qx_dimensions) == $f($y, $x)
@eval @test_throws DimensionError $f($qy_dimensions, $x)
@eval @test_throws DimensionError $f($y, $qx_dimensions)
end
end
end
end
s = record_show(DimensionError(u"km/s"), showerror)
@test occursin("not dimensionless", s)
end

@testset "Assorted dimensionful functions" begin
functions = (
:float, :abs, :real, :imag, :conj, :adjoint, :unsigned,
:nextfloat, :prevfloat, :identity, :transpose,
:copysign, :flipsign, :mod, :modf,
:floor, :trunc, :ceil, :significand,
:ldexp, :round,
)
for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions
T = f in (:abs, :real, :imag, :conj) ? ComplexF64 : Float64
if f == :modf # Functions that return multiple outputs
for x in 5rand(T, 3) .- 2.5
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
num_outputs = 2
for i=1:num_outputs
@eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $dim)
end
end
elseif f in (:copysign, :flipsign, :rem, :mod) # Functions that need multiple inputs
for x in 5rand(T, 3) .- 2.5
for y in 5rand(T, 3) .- 2.5
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
qy_dimensions = Q(y, dim)
@eval @test $f($qx_dimensions, $qy_dimensions) == $Q($f($x, $y), $dim)
if f in (:copysign, :flipsign, :mod)
# Also do test without dimensions
@eval @test $f($x, $qy_dimensions) == $f($x, $y)
@eval @test $f($qx_dimensions, $y) == $Q($f($x, $y), $dim)
end
end
end
elseif f == :unsigned
for x in 5rand(-10:10, 3)
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
end
elseif f in (:round, :floor, :trunc, :ceil)
for x in 5rand(T, 3) .- 2.5
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
@eval @test $f(Int32, $qx_dimensions) == $Q($f(Int32, $x), $dim)
end
elseif f == :ldexp
for x in 5rand(T, 3) .- 2.5
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
for i=1:3
@eval @test $f($qx_dimensions, $i) == $Q($f($x, $i), $dim)
end
end
else
# Only test on valid domain
valid_inputs = filter(
x -> is_input_valid(eval(f), x),
5rand(T, 100) .- 2.5
)
for x in valid_inputs[1:3]
dim = convert(D, dimension(u"m/s"))
qx_dimensions = Q(x, dim)
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
end
end
end
end

@testset "Test div" begin
for Q in (Quantity, GenericQuantity)
x = Q{Int}(10, length=1)
y = Q{Int}(3, mass=-1)
@test div(x, y) == Q{Int}(3, length=1, mass=1)
@test div(x, 3) == Q{Int}(3, length=1)
@test div(10, y) == Q{Int}(3, mass=1)
if VERSION >= v"1.9"
@test div(x, y, RoundFromZero) == Q{Int}(4, length=1, mass=1)
@test div(x, 3, RoundFromZero) == Q{Int}(4, length=1)
@test div(10, y, RoundFromZero) == Q{Int}(4, mass=1)
end
end
end