Skip to content

Commit d08e6f1

Browse files
authored
Merge pull request #56 from devmotion/dw/sparsearrays
Remove SparseArrays dependency
2 parents eeba43f + 0bcc134 commit d08e6f1

File tree

3 files changed

+214
-45
lines changed

3 files changed

+214
-45
lines changed

Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ version = "0.7.2"
66
[deps]
77
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
88
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
9-
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
109
Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775"
1110

1211
[weakdeps]

src/symbolic_dimensions.jl

Lines changed: 162 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
22
import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
3-
import SparseArrays as SA
43

54
const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)
65

6+
const INDEX_TYPE = UInt8
77
# Prefer units over constants:
88
# For example, this means we can't have a symbolic Planck's constant,
99
# as it is just "hours" (h), which is more common.
@@ -19,7 +19,7 @@ const ALL_VALUES = vcat(
1919
if k SYMBOL_CONFLICTS
2020
)...
2121
)
22-
const ALL_MAPPING = NamedTuple([s => i for (i, s) in enumerate(ALL_SYMBOLS)])
22+
const ALL_MAPPING = NamedTuple([s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)])
2323

2424
"""
2525
SymbolicDimensions{R} <: AbstractDimensions{R}
@@ -35,49 +35,60 @@ to one which uses `Dimensions` as its dimensions (i.e., base SI units)
3535
`expand_units`.
3636
"""
3737
struct SymbolicDimensions{R} <: AbstractDimensions{R}
38-
_data::SA.SparseVector{R}
39-
40-
SymbolicDimensions(data::SA.SparseVector) = new{eltype(data)}(data)
41-
SymbolicDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data)
38+
nzdims::Vector{INDEX_TYPE}
39+
nzvals::Vector{R}
4240
end
4341

4442
static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS
45-
data(d::SymbolicDimensions) = getfield(d, :_data)
46-
Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[ALL_MAPPING[s]]
47-
Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k)
43+
function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R}
44+
nzdims = getfield(d, :nzdims)
45+
i = get(ALL_MAPPING, s, INDEX_TYPE(0))
46+
iszero(i) && error("$s is not available as a symbol in SymbolicDimensions. Symbols available: $(ALL_SYMBOLS).")
47+
ii = searchsortedfirst(nzdims, i)
48+
if ii <= length(nzdims) && nzdims[ii] == i
49+
return getfield(d, :nzvals)[ii]
50+
else
51+
return zero(R)
52+
end
53+
end
54+
Base.propertynames(::SymbolicDimensions) = ALL_SYMBOLS
55+
Base.getindex(d::SymbolicDimensions, k::Symbol) = getproperty(d, k)
4856
constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions
4957

50-
SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(data(d))
51-
(::Type{D})(; kws...) where {D<:SymbolicDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...)
52-
(::Type{D})(::Type{R}; kws...) where {R,D<:SymbolicDimensions} =
53-
let constructor=constructor_of(D){R}
54-
length(kws) == 0 && return constructor(SA.spzeros(R, length(ALL_SYMBOLS)))
55-
I = [ALL_MAPPING[s] for s in keys(kws)]
56-
V = [tryrationalize(R, v) for v in values(kws)]
57-
data = SA.sparsevec(I, V, length(ALL_SYMBOLS))
58-
return constructor(data)
58+
SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(getfield(d, :nzdims), convert(Vector{R}, getfield(d, :nzvals)))
59+
SymbolicDimensions(; kws...) = SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}(; kws...)
60+
function SymbolicDimensions{R}(; kws...) where {R}
61+
if isempty(kws)
62+
return SymbolicDimensions{R}(Vector{INDEX_TYPE}(undef, 0), Vector{R}(undef, 0))
5963
end
64+
I = INDEX_TYPE[ALL_MAPPING[s] for s in keys(kws)]
65+
p = sortperm(I)
66+
V = R[tryrationalize(R, kws[i]) for i in p]
67+
return SymbolicDimensions{R}(permute!(I, p), V)
68+
end
69+
(::Type{<:SymbolicDimensions})(::Type{R}; kws...) where {R} = SymbolicDimensions{R}(; kws...)
6070

61-
function Base.convert(::Type{Qout}, q::Quantity{<:Any,<:Dimensions}) where {T,D<:SymbolicDimensions,Qout<:Quantity{T,D}}
62-
output = Qout(
63-
convert(T, ustrip(q)),
64-
D;
65-
m=ulength(q),
66-
kg=umass(q),
67-
s=utime(q),
68-
A=ucurrent(q),
69-
K=utemperature(q),
70-
cd=uluminosity(q),
71-
mol=uamount(q),
72-
)
73-
SA.dropzeros!(data(dimension(output)))
74-
return output
71+
function Base.convert(::Type{Quantity{T,SymbolicDimensions}}, q::Quantity{<:Any,<:Dimensions}) where {T}
72+
return convert(Quantity{T,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}}, q)
73+
end
74+
function Base.convert(::Type{Quantity{T,SymbolicDimensions{R}}}, q::Quantity{<:Any,<:Dimensions}) where {T,R}
75+
syms = (:m, :kg, :s, :A, :K, :cd, :mol)
76+
vals = (ulength(q), umass(q), utime(q), ucurrent(q), utemperature(q), uluminosity(q), uamount(q))
77+
I = INDEX_TYPE[ALL_MAPPING[s] for (s, v) in zip(syms, vals) if !iszero(v)]
78+
V = R[tryrationalize(R, v) for v in vals if !iszero(v)]
79+
p = sortperm(I)
80+
permute!(I, p)
81+
permute!(V, p)
82+
dims = SymbolicDimensions{R}(I, V)
83+
return Quantity(convert(T, ustrip(q)), dims)
7584
end
76-
function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}}
77-
result = one(Q) * ustrip(q)
85+
function Base.convert(::Type{Quantity{T,D}}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions}
86+
result = Quantity(T(ustrip(q)), D())
7887
d = dimension(q)
79-
for (idx, value) in zip(SA.findnz(data(d))...)
80-
result = result * convert(Q, ALL_VALUES[idx]) ^ value
88+
for (idx, value) in zip(getfield(d, :nzdims), getfield(d, :nzvals))
89+
if !iszero(value)
90+
result = result * convert(Quantity{T,D}, ALL_VALUES[idx]) ^ value
91+
end
8192
end
8293
return result
8394
end
@@ -95,15 +106,122 @@ end
95106
expand_units(q::QuantityArray) = expand_units.(q)
96107

97108

98-
Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(data(d)))
99-
Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) = data(l) == data(r)
100-
Base.iszero(d::SymbolicDimensions) = iszero(data(d))
101-
Base.:*(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) + data(r))
102-
Base.:/(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) - data(r))
103-
Base.inv(d::SymbolicDimensions) = SymbolicDimensions(-data(d))
104-
Base.:^(l::SymbolicDimensions{R}, r::Integer) where {R} = SymbolicDimensions(data(l) * r)
105-
Base.:^(l::SymbolicDimensions{R}, r::Number) where {R} = SymbolicDimensions(data(l) * tryrationalize(R, r))
109+
Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(getfield(d, :nzdims)), copy(getfield(d, :nzvals)))
110+
function Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions)
111+
nzdims_l = getfield(l, :nzdims)
112+
nzvals_l = getfield(l, :nzvals)
113+
nzdims_r = getfield(r, :nzdims)
114+
nzvals_r = getfield(r, :nzvals)
115+
nl = length(nzdims_l)
116+
nr = length(nzdims_r)
117+
il = ir = 1
118+
while il <= nl && ir <= nr
119+
dim_l = nzdims_l[il]
120+
dim_r = nzdims_r[ir]
121+
if dim_l == dim_r
122+
if nzvals_l[il] != nzvals_r[ir]
123+
return false
124+
end
125+
il += 1
126+
ir += 1
127+
elseif dim_l < dim_r
128+
if !iszero(nzvals_l[il])
129+
return false
130+
end
131+
il += 1
132+
else
133+
if !iszero(nzvals_r[ir])
134+
return false
135+
end
136+
ir += 1
137+
end
138+
end
139+
140+
while il <= nl
141+
if !iszero(nzvals_l[il])
142+
return false
143+
end
144+
il += 1
145+
end
146+
147+
while ir <= nr
148+
if !iszero(nzvals_r[ir])
149+
return false
150+
end
151+
ir += 1
152+
end
153+
154+
return true
155+
end
156+
Base.iszero(d::SymbolicDimensions) = iszero(getfield(d, :nzvals))
157+
158+
# Defines `inv(::SymbolicDimensions)` and `^(::SymbolicDimensions, ::Number)`
159+
function map_dimensions(op::Function, d::SymbolicDimensions)
160+
return SymbolicDimensions(copy(getfield(d, :nzdims)), map(op, getfield(d, :nzvals)))
161+
end
106162

163+
# Defines `*(::SymbolicDimensions, ::SymbolicDimensions)` and `/(::SymbolicDimensions, ::SymbolicDimensions)`
164+
function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{R}) where {O<:Function,L,R}
165+
zero_L = zero(L)
166+
zero_R = zero(R)
167+
T = typeof(op(zero(L), zero(R)))
168+
I = Vector{INDEX_TYPE}(undef, 0)
169+
V = Vector{T}(undef, 0)
170+
nzdims_l = getfield(l, :nzdims)
171+
nzvals_l = getfield(l, :nzvals)
172+
nzdims_r = getfield(r, :nzdims)
173+
nzvals_r = getfield(r, :nzvals)
174+
nl = length(nzdims_l)
175+
nr = length(nzdims_r)
176+
il = ir = 1
177+
while il <= nl && ir <= nr
178+
dim_l = nzdims_l[il]
179+
dim_r = nzdims_r[ir]
180+
if dim_l == dim_r
181+
s = op(nzvals_l[il], nzvals_r[ir])
182+
if !iszero(s)
183+
push!(I, dim_l)
184+
push!(V, s)
185+
end
186+
il += 1
187+
ir += 1
188+
elseif dim_l < dim_r
189+
s = op(nzvals_l[il], zero_R)
190+
if !iszero(s)
191+
push!(I, dim_l)
192+
push!(V, s)
193+
end
194+
il += 1
195+
else
196+
s = op(zero_L, nzvals_r[ir])
197+
if !iszero(s)
198+
push!(I, dim_r)
199+
push!(V, s)
200+
end
201+
ir += 1
202+
end
203+
end
204+
205+
while il <= nl
206+
s = op(nzvals_l[il], zero_R)
207+
if !iszero(s)
208+
push!(I, nzdims_l[il])
209+
push!(V, s)
210+
end
211+
il += 1
212+
end
213+
214+
while ir <= nr
215+
s = op(zero_L, nzvals_r[ir])
216+
if !iszero(s)
217+
push!(I, nzdims_r[ir])
218+
push!(V, s)
219+
end
220+
ir += 1
221+
end
222+
223+
return SymbolicDimensions(I, V)
224+
end
107225

108226
"""
109227
SymbolicUnitsParse

test/unittests.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,49 @@ end
498498
end
499499

500500
@testset "Symbolic dimensions" begin
501+
# TODO: Remove constructors for sym3 and sym4s?
502+
sym1 = @inferred(SymbolicDimensions(; m=3, s=-1))
503+
sym2 = @inferred(SymbolicDimensions{Rational{Int}}(; m=3, s=-1))
504+
sym3 = @inferred(SymbolicDimensions(Rational{Int}; m=3, s=-1))
505+
sym4 = @inferred(SymbolicDimensions{Int}(Rational{Int}; m=3, s=-1))
506+
for (sym, T) in (
507+
(sym1, DynamicQuantities.DEFAULT_DIM_BASE_TYPE), (sym2, Rational{Int}), (sym3, Rational{Int}), (sym4, Rational{Int}),
508+
)
509+
@test sym isa SymbolicDimensions{T}
510+
511+
# Properties
512+
@test sym.m == 3
513+
@test sym.s == -1
514+
@test propertynames(sym) == DynamicQuantities.ALL_SYMBOLS
515+
@test issubset((:m, :s), propertynames(sym))
516+
@test all(propertynames(sym)) do x
517+
val = getproperty(sym, x)
518+
return x === :m ? val == 3 : (x === :s ? val == -1 : iszero(val))
519+
end
520+
521+
# Internal constructor
522+
@test DynamicQuantities.constructor_of(typeof(sym)) === SymbolicDimensions
523+
524+
# Equality comparisons
525+
@test sym == sym
526+
@test sym == copy(sym)
527+
@test sym !== copy(sym)
528+
@test sym == SymbolicDimensions{Int}(; m=3, s=-1)
529+
@test SymbolicDimensions{Int}(; m=3, s=-1) == sym
530+
@test sym == SymbolicDimensions(; m=3, g=0, s=-1)
531+
@test SymbolicDimensions(; m=3, g=0, s=-1) == sym
532+
@test sym == SymbolicDimensions(; m=3, s=-1, K=0)
533+
@test SymbolicDimensions(; m=3, s=-1, K=0) == sym
534+
@test sym != SymbolicDimensions(; m=2, s=-1)
535+
@test SymbolicDimensions(; m=2, s=-1) != sym
536+
@test sym != SymbolicDimensions(; m=3, g=1, s=-1)
537+
@test SymbolicDimensions(; m=3, g=1, s=-1) != sym
538+
@test sym != SymbolicDimensions(; m=3, s=-1, K=1)
539+
@test SymbolicDimensions(; m=3, s=-1, K=1) != sym
540+
541+
@test !iszero(sym)
542+
end
543+
501544
q = 1.5us"km/s"
502545
@test q == 1.5 * us"km" / us"s"
503546
@test typeof(q) <: Quantity{Float64,<:SymbolicDimensions}
@@ -537,6 +580,15 @@ end
537580

538581
# Test conversion
539582
@test typeof(SymbolicDimensions{Rational{Int}}(dimension(us"km/s"))) == SymbolicDimensions{Rational{Int}}
583+
@test convert(Quantity{Float64,SymbolicDimensions}, u"kg") == 1.0us"kg"
584+
@test convert(Quantity{Float64,SymbolicDimensions}, u"cm") == 1e-2us"m"
585+
@test convert(Quantity{Float64,Dimensions}, 3.5us"kg/s") == 3.5u"kg/s"
586+
@test convert(Quantity{Float64,Dimensions}, 3.5us"Constants.pc") == 3.5u"Constants.pc"
587+
588+
# Helpful error if symbol not found:
589+
sym5 = dimension(us"km/s")
590+
VERSION >= v"1.8" &&
591+
@test_throws "rad is not available as a symbol" sym5.rad
540592
end
541593

542594
@testset "Test ambiguities" begin

0 commit comments

Comments
 (0)