Skip to content

Remove SparseArrays dependency #56

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
Oct 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
1 change: 0 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ version = "0.7.2"
[deps]
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775"

[weakdeps]
Expand Down
206 changes: 162 additions & 44 deletions src/symbolic_dimensions.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
import SparseArrays as SA

const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)

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

"""
SymbolicDimensions{R} <: AbstractDimensions{R}
Expand All @@ -35,49 +35,60 @@ to one which uses `Dimensions` as its dimensions (i.e., base SI units)
`expand_units`.
"""
struct SymbolicDimensions{R} <: AbstractDimensions{R}
_data::SA.SparseVector{R}

SymbolicDimensions(data::SA.SparseVector) = new{eltype(data)}(data)
SymbolicDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data)
nzdims::Vector{INDEX_TYPE}
nzvals::Vector{R}
end

static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS
data(d::SymbolicDimensions) = getfield(d, :_data)
Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[ALL_MAPPING[s]]
Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k)
function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R}
nzdims = getfield(d, :nzdims)
i = get(ALL_MAPPING, s, INDEX_TYPE(0))
iszero(i) && error("$s is not available as a symbol in SymbolicDimensions. Symbols available: $(ALL_SYMBOLS).")
ii = searchsortedfirst(nzdims, i)
if ii <= length(nzdims) && nzdims[ii] == i
return getfield(d, :nzvals)[ii]
else
return zero(R)
end
end
Base.propertynames(::SymbolicDimensions) = ALL_SYMBOLS
Base.getindex(d::SymbolicDimensions, k::Symbol) = getproperty(d, k)
constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions

SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(data(d))
(::Type{D})(; kws...) where {D<:SymbolicDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...)
(::Type{D})(::Type{R}; kws...) where {R,D<:SymbolicDimensions} =
let constructor=constructor_of(D){R}
length(kws) == 0 && return constructor(SA.spzeros(R, length(ALL_SYMBOLS)))
I = [ALL_MAPPING[s] for s in keys(kws)]
V = [tryrationalize(R, v) for v in values(kws)]
data = SA.sparsevec(I, V, length(ALL_SYMBOLS))
return constructor(data)
SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(getfield(d, :nzdims), convert(Vector{R}, getfield(d, :nzvals)))
SymbolicDimensions(; kws...) = SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}(; kws...)
function SymbolicDimensions{R}(; kws...) where {R}
if isempty(kws)
return SymbolicDimensions{R}(Vector{INDEX_TYPE}(undef, 0), Vector{R}(undef, 0))
end
I = INDEX_TYPE[ALL_MAPPING[s] for s in keys(kws)]
p = sortperm(I)
V = R[tryrationalize(R, kws[i]) for i in p]
return SymbolicDimensions{R}(permute!(I, p), V)
end
(::Type{<:SymbolicDimensions})(::Type{R}; kws...) where {R} = SymbolicDimensions{R}(; kws...)

function Base.convert(::Type{Qout}, q::Quantity{<:Any,<:Dimensions}) where {T,D<:SymbolicDimensions,Qout<:Quantity{T,D}}
output = Qout(
convert(T, ustrip(q)),
D;
m=ulength(q),
kg=umass(q),
s=utime(q),
A=ucurrent(q),
K=utemperature(q),
cd=uluminosity(q),
mol=uamount(q),
)
SA.dropzeros!(data(dimension(output)))
return output
function Base.convert(::Type{Quantity{T,SymbolicDimensions}}, q::Quantity{<:Any,<:Dimensions}) where {T}
return convert(Quantity{T,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}}, q)
end
function Base.convert(::Type{Quantity{T,SymbolicDimensions{R}}}, q::Quantity{<:Any,<:Dimensions}) where {T,R}
syms = (:m, :kg, :s, :A, :K, :cd, :mol)
vals = (ulength(q), umass(q), utime(q), ucurrent(q), utemperature(q), uluminosity(q), uamount(q))
I = INDEX_TYPE[ALL_MAPPING[s] for (s, v) in zip(syms, vals) if !iszero(v)]
V = R[tryrationalize(R, v) for v in vals if !iszero(v)]
p = sortperm(I)
permute!(I, p)
permute!(V, p)
dims = SymbolicDimensions{R}(I, V)
return Quantity(convert(T, ustrip(q)), dims)
end
function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}}
result = one(Q) * ustrip(q)
function Base.convert(::Type{Quantity{T,D}}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions}
result = Quantity(T(ustrip(q)), D())
d = dimension(q)
for (idx, value) in zip(SA.findnz(data(d))...)
result = result * convert(Q, ALL_VALUES[idx]) ^ value
for (idx, value) in zip(getfield(d, :nzdims), getfield(d, :nzvals))
if !iszero(value)
result = result * convert(Quantity{T,D}, ALL_VALUES[idx]) ^ value
end
end
return result
end
Expand All @@ -95,15 +106,122 @@ end
expand_units(q::QuantityArray) = expand_units.(q)


Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(data(d)))
Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) = data(l) == data(r)
Base.iszero(d::SymbolicDimensions) = iszero(data(d))
Base.:*(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) + data(r))
Base.:/(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) - data(r))
Base.inv(d::SymbolicDimensions) = SymbolicDimensions(-data(d))
Base.:^(l::SymbolicDimensions{R}, r::Integer) where {R} = SymbolicDimensions(data(l) * r)
Base.:^(l::SymbolicDimensions{R}, r::Number) where {R} = SymbolicDimensions(data(l) * tryrationalize(R, r))
Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(getfield(d, :nzdims)), copy(getfield(d, :nzvals)))
function Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions)
nzdims_l = getfield(l, :nzdims)
nzvals_l = getfield(l, :nzvals)
nzdims_r = getfield(r, :nzdims)
nzvals_r = getfield(r, :nzvals)
nl = length(nzdims_l)
nr = length(nzdims_r)
il = ir = 1
while il <= nl && ir <= nr
dim_l = nzdims_l[il]
dim_r = nzdims_r[ir]
if dim_l == dim_r
if nzvals_l[il] != nzvals_r[ir]
return false
end
il += 1
ir += 1
elseif dim_l < dim_r
if !iszero(nzvals_l[il])
return false
end
il += 1
else
if !iszero(nzvals_r[ir])
return false
end
ir += 1
end
end

while il <= nl
if !iszero(nzvals_l[il])
return false
end
il += 1
end

while ir <= nr
if !iszero(nzvals_r[ir])
return false
end
ir += 1
end

return true
end
Base.iszero(d::SymbolicDimensions) = iszero(getfield(d, :nzvals))

# Defines `inv(::SymbolicDimensions)` and `^(::SymbolicDimensions, ::Number)`
function map_dimensions(op::Function, d::SymbolicDimensions)
return SymbolicDimensions(copy(getfield(d, :nzdims)), map(op, getfield(d, :nzvals)))
end

# Defines `*(::SymbolicDimensions, ::SymbolicDimensions)` and `/(::SymbolicDimensions, ::SymbolicDimensions)`
function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{R}) where {O<:Function,L,R}
zero_L = zero(L)
zero_R = zero(R)
T = typeof(op(zero(L), zero(R)))
I = Vector{INDEX_TYPE}(undef, 0)
V = Vector{T}(undef, 0)
nzdims_l = getfield(l, :nzdims)
nzvals_l = getfield(l, :nzvals)
nzdims_r = getfield(r, :nzdims)
nzvals_r = getfield(r, :nzvals)
nl = length(nzdims_l)
nr = length(nzdims_r)
il = ir = 1
while il <= nl && ir <= nr
dim_l = nzdims_l[il]
dim_r = nzdims_r[ir]
if dim_l == dim_r
s = op(nzvals_l[il], nzvals_r[ir])
if !iszero(s)
push!(I, dim_l)
push!(V, s)
end
il += 1
ir += 1
elseif dim_l < dim_r
s = op(nzvals_l[il], zero_R)
if !iszero(s)
push!(I, dim_l)
push!(V, s)
end
il += 1
else
s = op(zero_L, nzvals_r[ir])
if !iszero(s)
push!(I, dim_r)
push!(V, s)
end
ir += 1
end
end

while il <= nl
s = op(nzvals_l[il], zero_R)
if !iszero(s)
push!(I, nzdims_l[il])
push!(V, s)
end
il += 1
end

while ir <= nr
s = op(zero_L, nzvals_r[ir])
if !iszero(s)
push!(I, nzdims_r[ir])
push!(V, s)
end
ir += 1
end

return SymbolicDimensions(I, V)
end

"""
SymbolicUnitsParse
Expand Down
52 changes: 52 additions & 0 deletions test/unittests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,49 @@ end
end

@testset "Symbolic dimensions" begin
# TODO: Remove constructors for sym3 and sym4s?
sym1 = @inferred(SymbolicDimensions(; m=3, s=-1))
sym2 = @inferred(SymbolicDimensions{Rational{Int}}(; m=3, s=-1))
sym3 = @inferred(SymbolicDimensions(Rational{Int}; m=3, s=-1))
sym4 = @inferred(SymbolicDimensions{Int}(Rational{Int}; m=3, s=-1))
for (sym, T) in (
(sym1, DynamicQuantities.DEFAULT_DIM_BASE_TYPE), (sym2, Rational{Int}), (sym3, Rational{Int}), (sym4, Rational{Int}),
)
@test sym isa SymbolicDimensions{T}

# Properties
@test sym.m == 3
@test sym.s == -1
@test propertynames(sym) == DynamicQuantities.ALL_SYMBOLS
@test issubset((:m, :s), propertynames(sym))
@test all(propertynames(sym)) do x
val = getproperty(sym, x)
return x === :m ? val == 3 : (x === :s ? val == -1 : iszero(val))
end

# Internal constructor
@test DynamicQuantities.constructor_of(typeof(sym)) === SymbolicDimensions

# Equality comparisons
@test sym == sym
@test sym == copy(sym)
@test sym !== copy(sym)
@test sym == SymbolicDimensions{Int}(; m=3, s=-1)
@test SymbolicDimensions{Int}(; m=3, s=-1) == sym
@test sym == SymbolicDimensions(; m=3, g=0, s=-1)
@test SymbolicDimensions(; m=3, g=0, s=-1) == sym
@test sym == SymbolicDimensions(; m=3, s=-1, K=0)
@test SymbolicDimensions(; m=3, s=-1, K=0) == sym
@test sym != SymbolicDimensions(; m=2, s=-1)
@test SymbolicDimensions(; m=2, s=-1) != sym
@test sym != SymbolicDimensions(; m=3, g=1, s=-1)
@test SymbolicDimensions(; m=3, g=1, s=-1) != sym
@test sym != SymbolicDimensions(; m=3, s=-1, K=1)
@test SymbolicDimensions(; m=3, s=-1, K=1) != sym

@test !iszero(sym)
end

q = 1.5us"km/s"
@test q == 1.5 * us"km" / us"s"
@test typeof(q) <: Quantity{Float64,<:SymbolicDimensions}
Expand Down Expand Up @@ -537,6 +580,15 @@ end

# Test conversion
@test typeof(SymbolicDimensions{Rational{Int}}(dimension(us"km/s"))) == SymbolicDimensions{Rational{Int}}
@test convert(Quantity{Float64,SymbolicDimensions}, u"kg") == 1.0us"kg"
@test convert(Quantity{Float64,SymbolicDimensions}, u"cm") == 1e-2us"m"
@test convert(Quantity{Float64,Dimensions}, 3.5us"kg/s") == 3.5u"kg/s"
@test convert(Quantity{Float64,Dimensions}, 3.5us"Constants.pc") == 3.5u"Constants.pc"

# Helpful error if symbol not found:
sym5 = dimension(us"km/s")
VERSION >= v"1.8" &&
@test_throws "rad is not available as a symbol" sym5.rad
end

@testset "Test ambiguities" begin
Expand Down