Skip to content

Commit 29454c1

Browse files
authored
Merge pull request #78 from SymbolicML/more-functions-3
Add many other numeric methods
2 parents cd30223 + 40521a1 commit 29454c1

File tree

4 files changed

+253
-16
lines changed

4 files changed

+253
-16
lines changed

src/math.jl

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES
22
@eval begin
33
Base.:*(l::$type, r::$type) = new_quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r))
44
Base.:/(l::$type, r::$type) = new_quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r))
5+
Base.div(x::$type, y::$type, r::RoundingMode=RoundToZero) = new_quantity(typeof(x), div(ustrip(x), ustrip(y), r), dimension(x) / dimension(y))
56

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

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

1215
Base.:*(l::$type, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) * r)
1316
Base.:/(l::$type, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) / r)
@@ -41,7 +44,7 @@ end
4144
Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l))
4245

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

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

100-
Base.abs(q::UnionAbstractQuantity) = new_quantity(typeof(q), abs(ustrip(q)), dimension(q))
101103
Base.abs2(q::UnionAbstractQuantity) = new_quantity(typeof(q), abs2(ustrip(q)), dimension(q)^2)
102104
Base.angle(q::UnionAbstractQuantity{T}) where {T<:Complex} = angle(ustrip(q))
105+
106+
############################## Require dimensionless input ##############################
107+
# Note that :clamp, :cmp, :sign already work
108+
# We skip :rad2deg, :deg2rad in case the user defines a rad or deg unit
109+
for f in (
110+
:sin, :cos, :tan, :sinh, :cosh, :tanh, :asin, :acos,
111+
:asinh, :acosh, :atanh, :sec, :csc, :cot, :asec, :acsc, :acot, :sech, :csch,
112+
:coth, :asech, :acsch, :acoth, :sinc, :cosc, :cosd, :cotd, :cscd, :secd,
113+
:sinpi, :cospi, :sind, :tand, :acosd, :acotd, :acscd, :asecd, :asind,
114+
:log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1, :frexp, :exponent,
115+
)
116+
@eval function Base.$f(q::UnionAbstractQuantity)
117+
iszero(dimension(q)) || throw(DimensionError(q))
118+
return $f(ustrip(q))
119+
end
120+
end
121+
for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:atan, :atand)
122+
@eval begin
123+
function Base.$f(x::$type)
124+
iszero(dimension(x)) || throw(DimensionError(x))
125+
return $f(ustrip(x))
126+
end
127+
function Base.$f(y::$type, x::$type)
128+
dimension(y) == dimension(x) || throw(DimensionError(y, x))
129+
return $f(ustrip(y), ustrip(x))
130+
end
131+
function Base.$f(y::$type, x::$base_type)
132+
iszero(dimension(y)) || throw(DimensionError(y))
133+
return $f(ustrip(y), x)
134+
end
135+
function Base.$f(y::$base_type, x::$type)
136+
iszero(dimension(x)) || throw(DimensionError(x))
137+
return $f(y, ustrip(x))
138+
end
139+
end
140+
end
141+
#########################################################################################
142+
143+
############################## Same dimension as input ##################################
144+
for f in (
145+
:float, :abs, :real, :imag, :conj, :adjoint, :unsigned,
146+
:nextfloat, :prevfloat, :identity, :transpose, :significand
147+
)
148+
@eval function Base.$f(q::UnionAbstractQuantity)
149+
return new_quantity(typeof(q), $f(ustrip(q)), dimension(q))
150+
end
151+
end
152+
for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, :mod)
153+
# These treat the x as the magnitude, so we take the dimensions from there,
154+
# and ignore any dimensions on y, since those will cancel out.
155+
@eval begin
156+
function Base.$f(x::$type, y::$type)
157+
return new_quantity(typeof(x), $f(ustrip(x), ustrip(y)), dimension(x))
158+
end
159+
function Base.$f(x::$type, y::$base_type)
160+
return new_quantity(typeof(x), $f(ustrip(x), y), dimension(x))
161+
end
162+
function Base.$f(x::$base_type, y::$type)
163+
return $f(x, ustrip(y))
164+
end
165+
end
166+
end
167+
function Base.ldexp(x::UnionAbstractQuantity, n::Integer)
168+
return new_quantity(typeof(x), ldexp(ustrip(x), n), dimension(x))
169+
end
170+
function Base.round(q::UnionAbstractQuantity, r::RoundingMode=RoundNearest)
171+
return new_quantity(typeof(q), round(ustrip(q), r), dimension(q))
172+
end
173+
function Base.round(::Type{Ti}, q::UnionAbstractQuantity, r::RoundingMode=RoundNearest) where {Ti<:Integer}
174+
return new_quantity(typeof(q), round(Ti, ustrip(q), r), dimension(q))
175+
end
176+
for f in (:floor, :trunc, :ceil)
177+
@eval begin
178+
function Base.$f(q::UnionAbstractQuantity)
179+
return new_quantity(typeof(q), $f(ustrip(q)), dimension(q))
180+
end
181+
function Base.$f(::Type{Ti}, q::UnionAbstractQuantity) where {Ti<:Integer}
182+
return new_quantity(typeof(q), $f(Ti, ustrip(q)), dimension(q))
183+
end
184+
end
185+
end
186+
function Base.modf(q::UnionAbstractQuantity)
187+
output = modf(ustrip(q))
188+
return ntuple(i -> new_quantity(typeof(q), output[i], dimension(q)), Val(2))
189+
end
190+
#########################################################################################

src/types.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,7 @@ end
252252
struct DimensionError{Q1,Q2} <: Exception
253253
q1::Q1
254254
q2::Q2
255+
256+
DimensionError(q1, q2) = new{typeof(q1),typeof(q2)}(q1, q2)
257+
DimensionError(q1) = DimensionError(q1, nothing)
255258
end

src/utils.jl

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ end
2525
return output
2626
end
2727

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

132131
# Simple flags:
133-
for f in (:iszero, :isfinite, :isinf, :isnan, :isreal)
132+
for f in (
133+
:iszero, :isfinite, :isinf, :isnan, :isreal, :signbit,
134+
:isempty, :iseven, :isodd, :isinteger, :ispow2
135+
)
134136
@eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q))
135137
end
136138

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

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

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

199198
Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {Q<:UnionAbstractQuantity} = q
200199
Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {T,Q<:UnionAbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q))

test/unittests.jl

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ using StaticArrays: SArray, MArray
99
using LinearAlgebra: norm
1010
using Test
1111

12+
function record_show(s, f=show)
13+
io = IOBuffer()
14+
f(io, s)
15+
return String(take!(io))
16+
end
17+
1218
@testset "Basic utilities" begin
1319

1420
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}]
@@ -149,6 +155,21 @@ using Test
149155
@test isinf(x * Inf) == true
150156
@test isnan(x) == false
151157
@test isnan(x * NaN) == true
158+
@test isreal(x) == true
159+
@test isreal(x * (1 + 2im)) == false
160+
@test signbit(x) == true
161+
@test signbit(-x) == false
162+
@test isempty(x) == false
163+
@test isempty(GenericQuantity([0.0, 1.0])) == false
164+
@test isempty(GenericQuantity(Float64[])) == true
165+
@test iseven(Quantity(2, length=1)) == true
166+
@test iseven(Quantity(3, length=1)) == false
167+
@test isodd(Quantity(2, length=1)) == false
168+
@test isodd(Quantity(3, length=1)) == true
169+
@test isinteger(Quantity(2, length=1)) == true
170+
@test isinteger(Quantity(2.1, length=1)) == false
171+
@test ispow2(Quantity(2, length=1)) == true
172+
@test ispow2(Quantity(3, length=1)) == false
152173

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

435-
# Showing rationals
436-
function show_string(i)
437-
io = IOBuffer()
438-
show(io, i)
439-
return String(take!(io))
440-
end
441-
@test show_string(FixedRational{Int,10}(2)) == "2"
442-
@test show_string(FixedRational{Int,10}(11//10)) == "11//10"
456+
@test record_show(FixedRational{Int,10}(2)) == "2"
457+
@test record_show(FixedRational{Int,10}(11//10)) == "11//10"
443458

444459
# Promotion rules
445460
@test promote_type(FixedRational{Int64,10},FixedRational{BigInt,10}) == FixedRational{BigInt,10}
@@ -1104,3 +1119,135 @@ end
11041119
@test convert(typeof(qx), qy)[1] == convert(Quantity{Float64}, qy[1])
11051120
end
11061121
end
1122+
1123+
function is_input_valid(f, x)
1124+
try
1125+
f(x)
1126+
catch e
1127+
e isa DomainError && return false
1128+
rethrow(e)
1129+
end
1130+
return true
1131+
end
1132+
1133+
@testset "Assorted dimensionless functions" begin
1134+
functions = (
1135+
:sin, :cos, :tan, :sinh, :cosh, :tanh, :asin, :acos,
1136+
:asinh, :acosh, :atanh, :sec, :csc, :cot, :asec, :acsc, :acot, :sech, :csch,
1137+
:coth, :asech, :acsch, :acoth, :sinc, :cosc, :cosd, :cotd, :cscd, :secd,
1138+
:sinpi, :cospi, :sind, :tand, :acosd, :acotd, :acscd, :asecd, :asind,
1139+
:log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1, :frexp, :exponent,
1140+
:atan, :atand
1141+
)
1142+
for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions
1143+
# Only test on valid domain
1144+
valid_inputs = filter(
1145+
x -> is_input_valid(eval(f), x),
1146+
5rand(100) .- 2.5
1147+
)
1148+
for x in valid_inputs[1:3]
1149+
qx_dimensionless = Quantity(x, D)
1150+
qx_dimensions = Quantity(x, convert(D, dimension(u"m/s")))
1151+
@eval @test $f($qx_dimensionless) == $f($x)
1152+
@eval @test_throws DimensionError $f($qx_dimensions)
1153+
if f in (:atan, :atand)
1154+
for y in valid_inputs[end-3:end]
1155+
qy_dimensionless = Quantity(y, D)
1156+
qy_dimensions = Quantity(y, convert(D, dimension(u"m/s")))
1157+
@eval @test $f($y, $qx_dimensionless) == $f($y, $x)
1158+
@eval @test $f($qy_dimensionless, $x) == $f($y, $x)
1159+
@eval @test $f($qy_dimensionless, $qx_dimensionless) == $f($y, $x)
1160+
@eval @test $f($qy_dimensions, $qx_dimensions) == $f($y, $x)
1161+
@eval @test_throws DimensionError $f($qy_dimensions, $x)
1162+
@eval @test_throws DimensionError $f($y, $qx_dimensions)
1163+
end
1164+
end
1165+
end
1166+
end
1167+
s = record_show(DimensionError(u"km/s"), showerror)
1168+
@test occursin("not dimensionless", s)
1169+
end
1170+
1171+
@testset "Assorted dimensionful functions" begin
1172+
functions = (
1173+
:float, :abs, :real, :imag, :conj, :adjoint, :unsigned,
1174+
:nextfloat, :prevfloat, :identity, :transpose,
1175+
:copysign, :flipsign, :mod, :modf,
1176+
:floor, :trunc, :ceil, :significand,
1177+
:ldexp, :round,
1178+
)
1179+
for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions
1180+
T = f in (:abs, :real, :imag, :conj) ? ComplexF64 : Float64
1181+
if f == :modf # Functions that return multiple outputs
1182+
for x in 5rand(T, 3) .- 2.5
1183+
dim = convert(D, dimension(u"m/s"))
1184+
qx_dimensions = Q(x, dim)
1185+
num_outputs = 2
1186+
for i=1:num_outputs
1187+
@eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $dim)
1188+
end
1189+
end
1190+
elseif f in (:copysign, :flipsign, :rem, :mod) # Functions that need multiple inputs
1191+
for x in 5rand(T, 3) .- 2.5
1192+
for y in 5rand(T, 3) .- 2.5
1193+
dim = convert(D, dimension(u"m/s"))
1194+
qx_dimensions = Q(x, dim)
1195+
qy_dimensions = Q(y, dim)
1196+
@eval @test $f($qx_dimensions, $qy_dimensions) == $Q($f($x, $y), $dim)
1197+
if f in (:copysign, :flipsign, :mod)
1198+
# Also do test without dimensions
1199+
@eval @test $f($x, $qy_dimensions) == $f($x, $y)
1200+
@eval @test $f($qx_dimensions, $y) == $Q($f($x, $y), $dim)
1201+
end
1202+
end
1203+
end
1204+
elseif f == :unsigned
1205+
for x in 5rand(-10:10, 3)
1206+
dim = convert(D, dimension(u"m/s"))
1207+
qx_dimensions = Q(x, dim)
1208+
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
1209+
end
1210+
elseif f in (:round, :floor, :trunc, :ceil)
1211+
for x in 5rand(T, 3) .- 2.5
1212+
dim = convert(D, dimension(u"m/s"))
1213+
qx_dimensions = Q(x, dim)
1214+
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
1215+
@eval @test $f(Int32, $qx_dimensions) == $Q($f(Int32, $x), $dim)
1216+
end
1217+
elseif f == :ldexp
1218+
for x in 5rand(T, 3) .- 2.5
1219+
dim = convert(D, dimension(u"m/s"))
1220+
qx_dimensions = Q(x, dim)
1221+
for i=1:3
1222+
@eval @test $f($qx_dimensions, $i) == $Q($f($x, $i), $dim)
1223+
end
1224+
end
1225+
else
1226+
# Only test on valid domain
1227+
valid_inputs = filter(
1228+
x -> is_input_valid(eval(f), x),
1229+
5rand(T, 100) .- 2.5
1230+
)
1231+
for x in valid_inputs[1:3]
1232+
dim = convert(D, dimension(u"m/s"))
1233+
qx_dimensions = Q(x, dim)
1234+
@eval @test $f($qx_dimensions) == $Q($f($x), $dim)
1235+
end
1236+
end
1237+
end
1238+
end
1239+
1240+
@testset "Test div" begin
1241+
for Q in (Quantity, GenericQuantity)
1242+
x = Q{Int}(10, length=1)
1243+
y = Q{Int}(3, mass=-1)
1244+
@test div(x, y) == Q{Int}(3, length=1, mass=1)
1245+
@test div(x, 3) == Q{Int}(3, length=1)
1246+
@test div(10, y) == Q{Int}(3, mass=1)
1247+
if VERSION >= v"1.9"
1248+
@test div(x, y, RoundFromZero) == Q{Int}(4, length=1, mass=1)
1249+
@test div(x, 3, RoundFromZero) == Q{Int}(4, length=1)
1250+
@test div(10, y, RoundFromZero) == Q{Int}(4, mass=1)
1251+
end
1252+
end
1253+
end

0 commit comments

Comments
 (0)