From 049ef29d1cd6d5ab2d6adbe9ebbb8cbc29e6c394 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 3 Dec 2023 20:04:11 +0000 Subject: [PATCH 1/8] Refactor symbolic units module - Now eagerly creates symbolic units as immutable objects, rather than at first call - This uses a new type `SymbolicDimensionsSingleton` that stores a single dimension - Exports `SymbolicUnits` and `SymbolicConstants` for direct imports --- src/DynamicQuantities.jl | 2 +- src/symbolic_dimensions.jl | 257 +++++++++++++++++++++++-------------- src/utils.jl | 3 +- test/unittests.jl | 4 +- 4 files changed, 164 insertions(+), 102 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 37436f59..9f49b04c 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,6 +1,6 @@ module DynamicQuantities -export Units, Constants +export Units, Constants, SymbolicUnits, SymbolicConstants export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError export ustrip, dimension diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 46435978..d026a2e8 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -1,26 +1,46 @@ import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES + const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) +function disambiguate_symbol(s) + if s in SYMBOL_CONFLICTS + return Symbol(string(s) * "_constant") + else + return s + end +end +function reambiguate_symbol(s) + str_s = string(s) + if endswith(str_s, "_constant") + return Symbol(str_s[1:end-9]) + else + return s + end +end + 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. const ALL_SYMBOLS = ( UNIT_SYMBOLS..., - setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS)... -) -const ALL_VALUES = vcat( - UNIT_VALUES..., - ( - v - for (k, v) in zip(CONSTANT_SYMBOLS, CONSTANT_VALUES) - if k ∉ SYMBOL_CONFLICTS - )... + disambiguate_symbol.(CONSTANT_SYMBOLS)... ) +const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...) const ALL_MAPPING = NamedTuple([s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)]) +""" + AbstractSymbolicDimensions{R} <: AbstractDimensions{R} + +Abstract type to allow for custom types of symbolic dimensions. +In defining this abstract type we allow for units to declare themselves +as a special type of symbolic dimensions which are immutable, whereas +the regular `SymbolicDimensions` type has mutable storage. +""" +abstract type AbstractSymbolicDimensions{R} <: AbstractDimensions{R} end + """ SymbolicDimensions{R} <: AbstractDimensions{R} @@ -34,31 +54,47 @@ You can convert a quantity using `SymbolicDimensions` as its dimensions to one which uses `Dimensions` as its dimensions (i.e., base SI units) `uexpand`. """ -struct SymbolicDimensions{R} <: AbstractDimensions{R} +struct SymbolicDimensions{R} <: AbstractSymbolicDimensions{R} nzdims::Vector{INDEX_TYPE} nzvals::Vector{R} end -@inline dimension_names(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS +""" + SymbolicDimensionsSingleton{R} <: AbstractSymbolicDimensions{R} + +This special symbolic dimensions types stores a single unit or constant, and can +be used for constructing symbolic units and constants without needing to allocate mutable storage. +""" +struct SymbolicDimensionsSingleton{R} <: AbstractSymbolicDimensions{R} + dim::INDEX_TYPE + + SymbolicDimensionsSingleton(dim::INDEX_TYPE, ::Type{_R}) where {_R} = new{_R}(dim) +end + +# Access: function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} - nzdims = getfield(d, :nzdims) + _nzdims = nzdims(d) 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] + 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 nzvals(d)[ii] else return zero(R) end end -Base.propertynames(::SymbolicDimensions) = ALL_SYMBOLS -Base.getindex(d::SymbolicDimensions, k::Symbol) = getproperty(d, k) +function Base.getproperty(d::SymbolicDimensionsSingleton{R}, s::Symbol) where {R} + i = get(ALL_MAPPING, s, INDEX_TYPE(0)) + iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).") + return i == getfield(d, :dim) ? one(R) : zero(R) +end -constructorof(::Type{<:SymbolicDimensions}) = SymbolicDimensions -with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R} -SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(getfield(d, :nzdims), convert(Vector{R}, getfield(d, :nzvals))) +# Constructors: +(::Type{<:SymbolicDimensions})(::Type{R}; kws...) where {R} = SymbolicDimensions{R}(; kws...) +SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(nzdims(d), convert(Vector{R}, nzvals(d))) SymbolicDimensions(; kws...) = SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}(; kws...) +SymbolicDimensionsSingleton(s::Symbol) = SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(s) function SymbolicDimensions{R}(; kws...) where {R} if isempty(kws) return SymbolicDimensions{R}(Vector{INDEX_TYPE}(undef, 0), Vector{R}(undef, 0)) @@ -68,7 +104,36 @@ function SymbolicDimensions{R}(; kws...) where {R} 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 SymbolicDimensionsSingleton{R}(s::Symbol) where {R} + i = get(ALL_MAPPING, s, INDEX_TYPE(0)) + iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).") + return SymbolicDimensionsSingleton(i, R) +end + +# Traits: +dimension_names(::Type{<:AbstractSymbolicDimensions}) = ALL_SYMBOLS +Base.propertynames(::AbstractSymbolicDimensions) = ALL_SYMBOLS +Base.getindex(d::AbstractSymbolicDimensions, k::Symbol) = getproperty(d, k) +constructorof(::Type{<:SymbolicDimensions}) = SymbolicDimensions +constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R} +with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R} +with_type_parameters(::Type{<:SymbolicDimensionsSingleton}, ::Type{R}) where {R} = SymbolicDimensionsSingleton{R} +nzdims(d::SymbolicDimensions) = getfield(d, :nzdims) +nzdims(d::SymbolicDimensionsSingleton) = [getfield(d, :dim)] +nzvals(d::SymbolicDimensions) = getfield(d, :nzvals) +nzvals(::SymbolicDimensionsSingleton{R}) where {R} = [one(R)] +Base.eltype(::AbstractSymbolicDimensions{R}) where {R} = R +Base.eltype(::Type{<:AbstractSymbolicDimensions{R}}) where {R} = R + +# Conversion: +function SymbolicDimensions(d::SymbolicDimensionsSingleton{R}) where {R} + return SymbolicDimensions{R}(nzdims(d), nzvals(d)) +end +function SymbolicDimensions{R}(d::SymbolicDimensionsSingleton) where {R} + return SymbolicDimensions{R}(nzdims(d), nzvals(d)) +end +Base.convert(::Type{SymbolicDimensions}, d::SymbolicDimensionsSingleton) = SymbolicDimensions(d) +Base.convert(::Type{SymbolicDimensions{R}}, d::SymbolicDimensionsSingleton) where {R} = SymbolicDimensions{R}(d) for (type, _, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -86,10 +151,10 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES dims = SymbolicDimensions{R}(I, V) return constructorof(Q)(convert(T, ustrip(q)), dims) end - function Base.convert(::Type{Q}, q::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:$type{T,D}} + function Base.convert(::Type{Q}, q::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) where {T,D<:Dimensions,Q<:$type{T,D}} result = constructorof(Q)(convert(T, ustrip(q)), D()) d = dimension(q) - for (idx, value) in zip(getfield(d, :nzdims), getfield(d, :nzvals)) + for (idx, value) in zip(nzdims(d), nzvals(d)) if !iszero(value) result = result * convert(with_type_parameters(Q, T, D), ALL_VALUES[idx]) ^ value end @@ -101,27 +166,27 @@ end """ - uexpand(q::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}) + uexpand(q::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) Expand the symbolic units in a quantity to their base SI form. -In other words, this converts a quantity with `SymbolicDimensions` +In other words, this converts a quantity with `AbstractSymbolicDimensions` to one with `Dimensions`. The opposite of this function is `uconvert`, -for converting to specific symbolic units, or, e.g., `convert(Quantity{<:Any,<:SymbolicDimensions}, q)`, +for converting to specific symbolic units, or, e.g., `convert(Quantity{<:Any,<:AbstractSymbolicDimensions}, q)`, for assuming SI units as the output symbols. """ -function uexpand(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:UnionAbstractQuantity{T,D}} +function uexpand(q::Q) where {T,R,D<:AbstractSymbolicDimensions{R},Q<:UnionAbstractQuantity{T,D}} return convert(with_type_parameters(Q, T, Dimensions{R}), q) end uexpand(q::QuantityArray) = uexpand.(q) # TODO: Make the array-based one more efficient """ - uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) + uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) Convert a quantity `q` with base SI units to the symbolic units of `qout`, for `q` and `qout` with compatible units. Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`. """ -function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) @assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert." qout_expanded = uexpand(qout) dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded)) @@ -129,7 +194,7 @@ function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::U new_dim = dimension(qout) return new_quantity(typeof(q), new_val, new_dim) end -function uconvert(qout::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions}) @assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert." qout_expanded = uexpand(qout) dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded)) @@ -140,19 +205,21 @@ end # TODO: Method for converting SymbolicDimensions -> SymbolicDimensions """ - uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}) + uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}) Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e a function equivalent to `q -> uconvert(qout, q)`. """ -uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}) = Base.Fix1(uconvert, qout) - -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) +uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout) + +Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(nzdims(d)), copy(nzvals(d))) +Base.copy(d::SymbolicDimensionsSingleton) = SymbolicDimensionsSingleton(getfield(d, :dim), eltype(d)) + +function Base.:(==)(l::AbstractSymbolicDimensions, r::AbstractSymbolicDimensions) + nzdims_l = nzdims(l) + nzvals_l = nzvals(l) + nzdims_r = nzdims(r) + nzvals_r = nzvals(r) nl = length(nzdims_l) nr = length(nzdims_r) il = ir = 1 @@ -194,12 +261,15 @@ function Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) return true end -Base.iszero(d::SymbolicDimensions) = iszero(getfield(d, :nzvals)) +Base.iszero(d::AbstractSymbolicDimensions) = iszero(nzvals(d)) +Base.iszero(d::SymbolicDimensionsSingleton) = false # Defines `inv(::SymbolicDimensions)` and `^(::SymbolicDimensions, ::Number)` function map_dimensions(op::Function, d::SymbolicDimensions) - return SymbolicDimensions(copy(getfield(d, :nzdims)), map(op, getfield(d, :nzvals))) + return SymbolicDimensions(copy(nzdims(d)), map(op, nzvals(d))) end +# Ensure we always do operations with SymbolicDimensions: +map_dimensions(op::Function, d::SymbolicDimensionsSingleton) = map_dimensions(op, SymbolicDimensions(d)) # Defines `*(::SymbolicDimensions, ::SymbolicDimensions)` and `/(::SymbolicDimensions, ::SymbolicDimensions)` function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{R}) where {O<:Function,L,R} @@ -208,10 +278,10 @@ function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{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) + nzdims_l = nzdims(l) + nzvals_l = nzvals(l) + nzdims_r = nzdims(r) + nzvals_r = nzvals(r) nl = length(nzdims_l) nr = length(nzdims_r) il = ir = 1 @@ -263,72 +333,60 @@ function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{R return SymbolicDimensions(I, V) end +# Ensure we always do operations with SymbolicDimensions: +map_dimensions(op::Function, l::SymbolicDimensionsSingleton, r::SymbolicDimensionsSingleton) = map_dimensions(op, SymbolicDimensions(l), SymbolicDimensions(r)) +map_dimensions(op::Function, l::SymbolicDimensions, r::SymbolicDimensionsSingleton) = map_dimensions(op, l, SymbolicDimensions(r)) +map_dimensions(op::Function, l::SymbolicDimensionsSingleton, r::SymbolicDimensions) = map_dimensions(op, SymbolicDimensions(l), r) -const DEFAULT_SYMBOLIC_QUANTITY_TYPE = with_type_parameters(DEFAULT_QUANTITY_TYPE, DEFAULT_VALUE_TYPE, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) +# Units are stored using SymbolicDimensionsSingleton +const DEFAULT_SYMBOLIC_QUANTITY_TYPE = with_type_parameters(DEFAULT_QUANTITY_TYPE, DEFAULT_VALUE_TYPE, SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}) +# However, we output units from `us_str` using SymbolicDimensions, for type stability +const DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE = with_type_parameters(DEFAULT_QUANTITY_TYPE, DEFAULT_VALUE_TYPE, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) """ - SymbolicUnitsParse + SymbolicUnits A separate module where each unit is treated as a separate dimension, to enable pretty-printing of units. """ -module SymbolicUnitsParse +module SymbolicUnits import ..UNIT_SYMBOLS - import ..CONSTANT_SYMBOLS - import ..SYMBOL_CONFLICTS - import ..SymbolicDimensions - + import ..SymbolicDimensionsSingleton import ...constructorof import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE + import ...DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE # Lazily create unit symbols (since there are so many) module Constants - import ..CONSTANT_SYMBOLS - import ..SYMBOL_CONFLICTS - import ..SymbolicDimensions - - import ..constructorof - import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE - import ..DEFAULT_VALUE_TYPE - import ..DEFAULT_DIM_BASE_TYPE - - import ...Constants as EagerConstants - - const CONSTANT_SYMBOLS_EXIST = Ref{Bool}(false) - const CONSTANT_SYMBOLS_LOCK = Threads.SpinLock() - function _generate_unit_symbols() - CONSTANT_SYMBOLS_EXIST[] || lock(CONSTANT_SYMBOLS_LOCK) do - CONSTANT_SYMBOLS_EXIST[] && return nothing - for unit in setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS) - @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) - end - # Evaluate conflicting symbols to non-symbolic form: - for unit in SYMBOL_CONFLICTS - @eval const $unit = convert(DEFAULT_SYMBOLIC_QUANTITY_TYPE, EagerConstants.$unit) - end - CONSTANT_SYMBOLS_EXIST[] = true - end - return nothing + import ...CONSTANT_SYMBOLS + import ...SymbolicDimensionsSingleton + import ...constructorof + import ...disambiguate_symbol + import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE + import ....DEFAULT_VALUE_TYPE + import ....DEFAULT_DIM_BASE_TYPE + + for unit in CONSTANT_SYMBOLS + @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) + ) end end import .Constants + import .Constants as SymbolicConstants - const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) - const UNIT_SYMBOLS_LOCK = Threads.SpinLock() - function _generate_unit_symbols() - UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do - UNIT_SYMBOLS_EXIST[] && return nothing - for unit in UNIT_SYMBOLS - @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) - end - UNIT_SYMBOLS_EXIST[] = true - end - return nothing + for unit in UNIT_SYMBOLS + @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) + ) end + """ sym_uparse(raw_string::AbstractString) @@ -346,18 +404,17 @@ module SymbolicUnitsParse namespace collisions, a few physical constants are automatically converted. """ function sym_uparse(raw_string::AbstractString) - _generate_unit_symbols() - Constants._generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) - return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_TYPE + return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE end - as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_TYPE) = q - as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_TYPE, x) + as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE) = q + as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") end -import .SymbolicUnitsParse: sym_uparse +import .SymbolicUnits: sym_uparse +import .SymbolicUnits: SymbolicConstants """ us"[unit expression]" @@ -375,9 +432,15 @@ module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to namespace collisions, a few physical constants are automatically converted. """ macro us_str(s) - return esc(SymbolicUnitsParse.sym_uparse(s)) + return esc(SymbolicUnits.sym_uparse(s)) end +function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{SymbolicDimensionsSingleton{R2}}) where {R1,R2} + return SymbolicDimensions{promote_type(R1,R2)} +end +function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{D}) where {R1,D<:AbstractDimensions} + return promote_type(SymbolicDimensions{R1}, D) +end function Base.promote_rule(::Type{SymbolicDimensions{R1}}, ::Type{SymbolicDimensions{R2}}) where {R1,R2} return SymbolicDimensions{promote_type(R1,R2)} end diff --git a/src/utils.jl b/src/utils.jl index 2cb32ac1..ab933161 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -319,7 +319,8 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES, (type2, _, _) in ABSTRACT_QUANTITY_ # with the type for the dimensions being inferred. end -Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d +Base.convert(::Type{D}, d::D) where {R,D<:AbstractDimensions{R}} = d +Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = D(d) Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) Base.copy(d::D) where {D<:AbstractDimensions} = map_dimensions(copy, d) diff --git a/test/unittests.jl b/test/unittests.jl index 948aabf4..65346c5b 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -695,9 +695,7 @@ end @test uexpand(us"Constants.h") == u"Constants.h" # Actually expands to: - @test dimension(us"Constants.h")[:m] == 2 - @test dimension(us"Constants.h")[:s] == -1 - @test dimension(us"Constants.h")[:kg] == 1 + @test string(dimension(us"Constants.h")) == "h_constant" # So the numerical value is different from other constants: @test ustrip(us"Constants.h") == ustrip(u"Constants.h") From 63f4a401af9edc455e0704280112d79eba4e7445 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 3 Dec 2023 20:25:41 +0000 Subject: [PATCH 2/8] Remove unnecessary method --- src/symbolic_dimensions.jl | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index d026a2e8..e0047b9a 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -4,21 +4,7 @@ import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) -function disambiguate_symbol(s) - if s in SYMBOL_CONFLICTS - return Symbol(string(s) * "_constant") - else - return s - end -end -function reambiguate_symbol(s) - str_s = string(s) - if endswith(str_s, "_constant") - return Symbol(str_s[1:end-9]) - else - return s - end -end +disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(string(s) * "_constant") : s const INDEX_TYPE = UInt8 # Prefer units over constants: From 286e84918f6c974b1556ee7bd7852f30cd53e5cd Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 3 Dec 2023 20:39:39 +0000 Subject: [PATCH 3/8] Fix up some array issues --- src/utils.jl | 2 +- test/unittests.jl | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ab933161..1891f456 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -320,7 +320,7 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES, (type2, _, _) in ABSTRACT_QUANTITY_ end Base.convert(::Type{D}, d::D) where {R,D<:AbstractDimensions{R}} = d -Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = D(d) +Base.convert(::Type{D}, d::AbstractDimensions{R}) where {R,D<:AbstractDimensions} = with_type_parameters(D, R)(d) Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) Base.copy(d::D) where {D<:AbstractDimensions} = map_dimensions(copy, d) diff --git a/test/unittests.jl b/test/unittests.jl index 65346c5b..b435c1a0 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -697,8 +697,8 @@ end # Actually expands to: @test string(dimension(us"Constants.h")) == "h_constant" - # So the numerical value is different from other constants: - @test ustrip(us"Constants.h") == ustrip(u"Constants.h") + # Constants have different numerical value from non-symbolic one: + @test ustrip(us"Constants.h") != ustrip(u"Constants.h") @test ustrip(us"Constants.au") != ustrip(u"Constants.au") # Test conversion @@ -912,10 +912,10 @@ end @test x[5] == Q(5, length=1, time=-1) y = randn(32) - @test ustrip(QuantityArray(y, Q(u"m"))) == y + @test ustrip(QuantityArray(y, Q(u"m"))) ≈ y f_square(v) = v^2 * 1.5 - v^2 - @test sum(f_square.(QuantityArray(y, Q(u"m")))) == sum(f_square.(y) .* Q(u"m^2")) + @test sum(f_square.(QuantityArray(y, Q(u"m")))) ≈ sum(f_square.(y) .* Q(u"m^2")) y_q = QuantityArray(y, Q(u"m * cd / s")) @test typeof(f_square.(y_q)) == typeof(y_q) @@ -1057,7 +1057,7 @@ end y_q = QuantityArray(y, Q(u"m")) f4(v) = v^4 * 0.3 - @test sum(f4.(QuantityArray(y, Q(u"m")))) == sum(f4.(y) .* Q(u"m^4")) + @test sum(f4.(QuantityArray(y, Q(u"m")))) ≈ sum(f4.(y) .* Q(u"m^4")) f4v(v) = f4.(v) @inferred f4v(y_q) @@ -1600,9 +1600,8 @@ end y = 5randn(10) .- 2.5 for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions ground_truth = @eval $f.($x, $y) - dim = convert(D, dimension(u"m/s")) - qx_dimensions = [Q(xi, dim) for xi in x] - qy_dimensions = [Q(yi, dim) for yi in y] + qx_dimensions = [with_type_parameters(Q, Float64, D)(xi, dim) for xi in x] + qy_dimensions = [with_type_parameters(Q, Float64, D)(yi, dim) for yi in y] @eval @test all($f.($qx_dimensions, $qy_dimensions) .== $ground_truth) if f in (:isequal, :(==)) # These include a dimension check in the result, rather than From a52ab278275da2a7199bb0216f2cc6d7fb89a848 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 3 Dec 2023 21:11:38 +0000 Subject: [PATCH 4/8] Fix conversion rules --- src/symbolic_dimensions.jl | 2 +- src/utils.jl | 1 + test/unittests.jl | 55 ++++++++++++++++++-------------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index e0047b9a..dd64c2ad 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -142,7 +142,7 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES d = dimension(q) for (idx, value) in zip(nzdims(d), nzvals(d)) if !iszero(value) - result = result * convert(with_type_parameters(Q, T, D), ALL_VALUES[idx]) ^ value + result *= convert(with_type_parameters(Q, T, D), ALL_VALUES[idx] ^ value) end end return result diff --git a/src/utils.jl b/src/utils.jl index 1891f456..c7fde640 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -215,6 +215,7 @@ for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES base_type = true_base_type <: Number ? true_base_type : Number @eval begin function Base.isapprox(l::$type, r::$type; kws...) + l, r = promote_except_value(l, r) dimension(l) == dimension(r) || throw(DimensionError(l, r)) return isapprox(ustrip(l), ustrip(r); kws...) end diff --git a/test/unittests.jl b/test/unittests.jl index b435c1a0..8eb5157d 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1487,13 +1487,13 @@ end ) for x in valid_inputs[1:3] qx_dimensionless = Q(x, D) - qx_dimensions = Q(x, convert(D, dimension(u"m/s"))) + qx_dimensions = convert(with_type_parameters(Q, Float64, D), Q(x, 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 = Q(y, D) - qy_dimensions = Q(y, convert(D, dimension(u"m/s"))) + qy_dimensions = convert(with_type_parameters(Q, Float64, D), Q(y, 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) @@ -1521,24 +1521,25 @@ end T <: Complex && Q == RealQuantity && continue 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) + qx_dimensions = convert(with_type_parameters(Q, T, D), Q(x, dimension(u"m/s"))) num_outputs = 2 for i=1:num_outputs - @eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $dim) + @eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $(dimension(u"m/s"))) 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) + # dim = convert(D, dimension(u"m/s")) + # qx_dimensions = Q(x, dim) + # qy_dimensions = Q(y, dim) + qx_dimensions = convert(with_type_parameters(Q, T, D), Q(x, dimension(u"m/s"))) + qy_dimensions = convert(with_type_parameters(Q, T, D), Q(y, dimension(u"m/s"))) + @eval @test $f($qx_dimensions, $qy_dimensions) == $Q($f($x, $y), dimension(u"m/s")) if f in (:copysign, :flipsign) # 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) + @eval @test $f($qx_dimensions, $y) == $Q($f($x, $y), dimension(u"m/s")) elseif f in (:rem, :mod) # Also do test without dimensions (need dimensionless) qx_dimensionless = Q(x, D) @@ -1550,31 +1551,28 @@ end if f == :rem && VERSION >= v"1.9" # Can also do other rounding modes for r in (:RoundFromZero, :RoundNearest, :RoundUp, :RoundDown) - @eval @test $f($qx_dimensions, $qy_dimensions, $r) ≈ $Q($f($x, $y, $r), $dim) + @eval @test $f($qx_dimensions, $qy_dimensions, $r) ≈ $Q($f($x, $y, $r), dimension(u"m/s")) end end 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) + for x in 5rand(10:50, 3) + qx_dimensions = convert(with_type_parameters(Q, typeof(x), D), Q(x, dimension(u"m/s"))) + @eval @test $f($qx_dimensions) == $Q($f($x), dimension(u"m/s")) 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) + qx_dimensions = convert(with_type_parameters(Q, T, D), Q(x, dimension(u"m/s"))) + @eval @test $f($qx_dimensions) == $Q($f($x), dimension(u"m/s")) + @eval @test $f(Int32, $qx_dimensions) == $Q($f(Int32, $x), dimension(u"m/s")) end elseif f == :ldexp for x in 5rand(T, 3) .- 2.5 - dim = convert(D, dimension(u"m/s")) - qx_dimensions = Q(x, dim) + qx_dimensions = convert(with_type_parameters(Q, T, D), Q(x, dimension(u"m/s"))) for i=1:3 - @eval @test $f($qx_dimensions, $i) == $Q($f($x, $i), $dim) + @eval @test $f($qx_dimensions, $i) == $Q($f($x, $i), dimension(u"m/s")) end end else @@ -1584,9 +1582,8 @@ end 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) + qx_dimensions = convert(with_type_parameters(Q, T, D), Q(x, dimension(u"m/s"))) + @eval @test $f($qx_dimensions) == $Q($f($x), dimension(u"m/s")) end end end @@ -1600,8 +1597,8 @@ end y = 5randn(10) .- 2.5 for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions ground_truth = @eval $f.($x, $y) - qx_dimensions = [with_type_parameters(Q, Float64, D)(xi, dim) for xi in x] - qy_dimensions = [with_type_parameters(Q, Float64, D)(yi, dim) for yi in y] + qx_dimensions = [convert(with_type_parameters(Q, Float64, D), Q(xi, dimension(u"m/s"))) for xi in x] + qy_dimensions = [convert(with_type_parameters(Q, Float64, D), Q(yi, dimension(u"m/s"))) for yi in y] @eval @test all($f.($qx_dimensions, $qy_dimensions) .== $ground_truth) if f in (:isequal, :(==)) # These include a dimension check in the result, rather than @@ -1617,8 +1614,8 @@ end @eval @test all($f.($qx_dimensionless, $y) .== $ground_truth) @eval @test all($f.($x, $qy_dimensionless) .== $ground_truth) - qx_real_dimensions = [RealQuantity(xi, dim) for xi in x] - qy_real_dimensions = [RealQuantity(yi, dim) for yi in y] + qx_real_dimensions = [convert(RealQuantity{Float64,D}, Quantity(xi, dimension(u"m/s"))) for xi in x] + qy_real_dimensions = [convert(RealQuantity{Float64,D}, Quantity(yi, dimension(u"m/s"))) for yi in y] # Mixed quantity input @eval @test all($f.($qx_real_dimensions, $qy_dimensions) .== $ground_truth) @eval @test all($f.($qx_dimensions, $qy_real_dimensions) .== $ground_truth) From 490b630d195cf5269d1b4e39b14379a38d28ae64 Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Fri, 15 Dec 2023 01:11:25 -0600 Subject: [PATCH 5/8] Apply suggestions from code review Co-authored-by: David Widmann --- src/symbolic_dimensions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index dd64c2ad..8cabefdd 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -4,7 +4,7 @@ import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) -disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(string(s) * "_constant") : s +disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s const INDEX_TYPE = UInt8 # Prefer units over constants: @@ -15,7 +15,7 @@ const ALL_SYMBOLS = ( disambiguate_symbol.(CONSTANT_SYMBOLS)... ) const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...) -const ALL_MAPPING = NamedTuple([s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)]) +const ALL_MAPPING = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS))) """ AbstractSymbolicDimensions{R} <: AbstractDimensions{R} From b74a86dd8be9a83f00fa3bce9fba538f67329e7d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 15 Dec 2023 01:29:53 -0600 Subject: [PATCH 6/8] More suggestions from code review --- src/symbolic_dimensions.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 8cabefdd..4a3e8f22 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -59,11 +59,11 @@ end # Access: function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} - _nzdims = nzdims(d) + nzdims = DynamicQuantities.nzdims(d) 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 + ii = searchsortedfirst(nzdims, i) + if ii <= length(nzdims) && nzdims[ii] == i return nzvals(d)[ii] else return zero(R) @@ -105,18 +105,18 @@ constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDime with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R} with_type_parameters(::Type{<:SymbolicDimensionsSingleton}, ::Type{R}) where {R} = SymbolicDimensionsSingleton{R} nzdims(d::SymbolicDimensions) = getfield(d, :nzdims) -nzdims(d::SymbolicDimensionsSingleton) = [getfield(d, :dim)] +nzdims(d::SymbolicDimensionsSingleton) = (getfield(d, :dim),) nzvals(d::SymbolicDimensions) = getfield(d, :nzvals) -nzvals(::SymbolicDimensionsSingleton{R}) where {R} = [one(R)] +nzvals(::SymbolicDimensionsSingleton{R}) where {R} = (one(R),) Base.eltype(::AbstractSymbolicDimensions{R}) where {R} = R Base.eltype(::Type{<:AbstractSymbolicDimensions{R}}) where {R} = R # Conversion: function SymbolicDimensions(d::SymbolicDimensionsSingleton{R}) where {R} - return SymbolicDimensions{R}(nzdims(d), nzvals(d)) + return SymbolicDimensions{R}([nzdims(d)...], [nzvals(d)...]) end function SymbolicDimensions{R}(d::SymbolicDimensionsSingleton) where {R} - return SymbolicDimensions{R}(nzdims(d), nzvals(d)) + return SymbolicDimensions{R}([nzdims(d)...], [nzvals(d)...]) end Base.convert(::Type{SymbolicDimensions}, d::SymbolicDimensionsSingleton) = SymbolicDimensions(d) Base.convert(::Type{SymbolicDimensions{R}}, d::SymbolicDimensionsSingleton) where {R} = SymbolicDimensions{R}(d) From fb5e340db3412b2a3033e5af9411cdddac81388b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 15 Dec 2023 01:45:21 -0600 Subject: [PATCH 7/8] Add disambiguities --- src/disambiguities.jl | 3 +++ src/symbolic_dimensions.jl | 6 ++---- test/unittests.jl | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 49a3cf2b..57ff677d 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -23,6 +23,9 @@ Base.:*(l::Number, r::AbstractDimensions) = error("Please use an `UnionAbstractQ Base.:/(l::AbstractDimensions, r::Number) = error("Please use an `UnionAbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") Base.:/(l::Number, r::AbstractDimensions) = error("Please use an `UnionAbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") +SymbolicDimensionsSingleton{R}(::D) where {R,D<:AbstractDimensions} = error("SymbolicDimensionsSingleton must be constructed explicitly rather than converted to.") +SymbolicDimensionsSingleton{R}(::Type{R2}) where {R,R2} = error("SymbolicDimensionsSingleton requires a dimension to be specified.") + # Promotion ambiguities function Base.promote_rule(::Type{F}, ::Type{Bool}) where {F<:FixedRational} return F diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 4a3e8f22..a71df4cd 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -53,8 +53,6 @@ be used for constructing symbolic units and constants without needing to allocat """ struct SymbolicDimensionsSingleton{R} <: AbstractSymbolicDimensions{R} dim::INDEX_TYPE - - SymbolicDimensionsSingleton(dim::INDEX_TYPE, ::Type{_R}) where {_R} = new{_R}(dim) end # Access: @@ -93,7 +91,7 @@ end function SymbolicDimensionsSingleton{R}(s::Symbol) where {R} i = get(ALL_MAPPING, s, INDEX_TYPE(0)) iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).") - return SymbolicDimensionsSingleton(i, R) + return SymbolicDimensionsSingleton{R}(i) end # Traits: @@ -199,7 +197,7 @@ a function equivalent to `q -> uconvert(qout, q)`. uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout) Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(nzdims(d)), copy(nzvals(d))) -Base.copy(d::SymbolicDimensionsSingleton) = SymbolicDimensionsSingleton(getfield(d, :dim), eltype(d)) +Base.copy(d::SymbolicDimensionsSingleton) = constructorof(d)(getfield(d, :dim)) function Base.:(==)(l::AbstractSymbolicDimensions, r::AbstractSymbolicDimensions) nzdims_l = nzdims(l) diff --git a/test/unittests.jl b/test/unittests.jl index 8eb5157d..59b31bb0 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -824,12 +824,15 @@ end @test promote_rule(typeof(π), typeof(x)) == promote_type(Rational{Int32}, typeof(π)) end - @testset "Weakref" begin + @testset "Unimplemented on purpose" begin x = 1.0u"m" s = "test" y = WeakRef(s) @test_throws ErrorException x == y @test_throws ErrorException y == x + + @test_throws ErrorException DynamicQuantities.SymbolicDimensionsSingleton{Int}(Dimensions()) + @test_throws ErrorException DynamicQuantities.SymbolicDimensionsSingleton{Int}(UInt32) end @testset "Arrays" begin From 212626fdd9c16977d15b68a1ddfd007bab0d672a Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:28:35 +0000 Subject: [PATCH 8/8] Bump version with refactored symbolic units --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 94828005..4823fe10 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DynamicQuantities" uuid = "06fc5a27-2a28-4c7c-a15d-362465fb6821" authors = ["MilesCranmer and contributors"] -version = "0.10.3" +version = "0.11.0" [deps] Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"