From 2923c917b23e269f65078497300dd7f5b945a2e7 Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Thu, 4 Jan 2024 21:49:09 +0000 Subject: [PATCH 01/25] Attempt basic fix to precompilation --- src/uparse.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uparse.jl b/src/uparse.jl index e913d044..2d4f5064 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -54,7 +54,7 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ macro u_str(s) - return esc(uparse(s)) + return esc(Meta.parse(s)) end end From 25542fde933b0b2b12504facad159a04d434d868 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:07:26 +0000 Subject: [PATCH 02/25] Unitful-like unit parsing macro --- src/uparse.jl | 52 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/uparse.jl b/src/uparse.jl index 2d4f5064..771fee88 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -4,7 +4,8 @@ import ..constructorof import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE -import ..Units: UNIT_SYMBOLS +import ..Units: UNIT_SYMBOLS, UNIT_VALUES +import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants function _generate_units_import() @@ -34,11 +35,11 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::DEFAULT_QUANTITY_TYPE + return as_quantity(eval(map_to_scope(Meta.parse(s))))::DEFAULT_QUANTITY_TYPE end as_quantity(q::DEFAULT_QUANTITY_TYPE) = q -as_quantity(x::Number) = convert(DEFAULT_QUANTITY_TYPE, x) +as_quantity(x::Number) = convert(DEFAULT_QUANTITY_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") """ @@ -54,7 +55,50 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ macro u_str(s) - return esc(Meta.parse(s)) + ex = Meta.parse(s) + return esc(map_to_scope(ex)) +end + +function map_to_scope(ex::Expr) + if ex.head == :call + ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + return ex + elseif ex.head == :tuple + ex.args[:] = map(map_to_scope, ex.args) + return ex + elseif ex.head == :. + if ex.args[1] == :Constants + @assert ex.args[2] isa QuoteNode + return lookup_constant(ex.args[2].value) + else + return ex + end + else + throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` are expected.")) + return ex + end +end +function map_to_scope(sym::Symbol) + if sym in UNIT_SYMBOLS + return lookup_unit(sym) + elseif sym in CONSTANT_SYMBOLS + throw(ArgumentError("Found the symbol $sym. To access constants in a unit expression, access the `Constants` module. For example, `u\"Constants.h\"`.")) + return sym + else + throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + return sym + end +end +function map_to_scope(ex) + return ex +end +function lookup_unit(ex::Symbol) + i = findfirst(==(ex), UNIT_SYMBOLS)::Int + return UNIT_VALUES[i] +end +function lookup_constant(ex::Symbol) + i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int + return CONSTANT_VALUES[i] end end From 4a28973c6c5eb7f8879d24097c3a6c94e317cef8 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:09:31 +0000 Subject: [PATCH 03/25] Update tests to user-passed version --- test/unittests.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unittests.jl b/test/unittests.jl index 59b31bb0..540d2e61 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -462,16 +462,16 @@ end @test utime(z) == 1 @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 - # Test type stability of extreme range of units - @test typeof(u"1") == DEFAULT_QUANTITY_TYPE - @test typeof(u"1f0") == DEFAULT_QUANTITY_TYPE + # Test that `u_str` respects original type: + @test typeof(u"1") == Int + @test typeof(u"1f0") == Float32 @test typeof(u"s"^2) == DEFAULT_QUANTITY_TYPE @test typeof(u"Ω") == DEFAULT_QUANTITY_TYPE @test typeof(u"Gyr") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE - @test_throws LoadError eval(:(u":x")) + @test_throws ArgumentError eval(:(u":x")) end @testset "Constants" begin From 02142b26da3cb55486793bcda89071266fa458b8 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:13:30 +0000 Subject: [PATCH 04/25] Test `u_str` error messages --- src/uparse.jl | 2 +- test/unittests.jl | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/uparse.jl b/src/uparse.jl index 771fee88..39d25d7c 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -82,7 +82,7 @@ function map_to_scope(sym::Symbol) if sym in UNIT_SYMBOLS return lookup_unit(sym) elseif sym in CONSTANT_SYMBOLS - throw(ArgumentError("Found the symbol $sym. To access constants in a unit expression, access the `Constants` module. For example, `u\"Constants.h\"`.")) + throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please access the `Constants` module. For example, `u\"Constants.$sym\"`.")) return sym else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) diff --git a/test/unittests.jl b/test/unittests.jl index 540d2e61..d25edfce 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -471,7 +471,9 @@ end @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE - @test_throws ArgumentError eval(:(u":x")) + @test_throws LoadError eval(:(u"x")) + VERSION >= v"1.9" && @test_throws "Symbol x not found" uparse("x") + VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" uparse("c") end @testset "Constants" begin From 8b22c1e67f6c694ece3b208ad23d09e33d837955 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:23:16 +0000 Subject: [PATCH 05/25] Test additional branches in `@u_str` --- src/uparse.jl | 15 ++++----------- test/unittests.jl | 4 ++++ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/uparse.jl b/src/uparse.jl index 39d25d7c..695a0bf2 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -66,16 +66,11 @@ function map_to_scope(ex::Expr) elseif ex.head == :tuple ex.args[:] = map(map_to_scope, ex.args) return ex - elseif ex.head == :. - if ex.args[1] == :Constants - @assert ex.args[2] isa QuoteNode - return lookup_constant(ex.args[2].value) - else - return ex - end + elseif ex.head == :. && ex.args[1] == :Constants + @assert ex.args[2] isa QuoteNode + return lookup_constant(ex.args[2].value) else - throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` are expected.")) - return ex + throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` (for `Constants`) are expected.")) end end function map_to_scope(sym::Symbol) @@ -83,10 +78,8 @@ function map_to_scope(sym::Symbol) return lookup_unit(sym) elseif sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please access the `Constants` module. For example, `u\"Constants.$sym\"`.")) - return sym else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) - return sym end end function map_to_scope(ex) diff --git a/test/unittests.jl b/test/unittests.jl index d25edfce..5310f4f7 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -471,9 +471,13 @@ end @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE + # Can also use tuples: + @test typeof(u"(m, s)") == Tuple{DEFAULT_QUANTITY_TYPE, DEFAULT_QUANTITY_TYPE} + @test_throws LoadError eval(:(u"x")) VERSION >= v"1.9" && @test_throws "Symbol x not found" uparse("x") VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" uparse("c") + VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("import ..Units") end @testset "Constants" begin From 20b485ba16f9c3de359e3c197e9a1c65d4e93a43 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:57:01 +0000 Subject: [PATCH 06/25] Make Unitful conversion compatible with singletons --- ext/DynamicQuantitiesUnitfulExt.jl | 2 +- src/symbolic_dimensions.jl | 70 +++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 16f09420..a8252a1a 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -30,7 +30,7 @@ for (_, _, Q) in ABSTRACT_QUANTITY_TYPES validate_upreferred() cumulator = DynamicQuantities.ustrip(x) dims = DynamicQuantities.dimension(x) - if dims isa DynamicQuantities.SymbolicDimensions + if dims isa DynamicQuantities.AbstractSymbolicDimensions throw(ArgumentError("Conversion of a `DynamicQuantities." * string($Q) * "` to a `Unitful.Quantity` is not defined with dimensions of type `SymbolicDimensions`. Instead, you can first use the `uexpand` function to convert the dimensions to their base SI form of type `Dimensions`, then convert this quantity to a `Unitful.Quantity`.")) end equiv = unitful_equivalences() diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index a71df4cd..43bb5d3c 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -336,6 +336,7 @@ to enable pretty-printing of units. module SymbolicUnits import ..UNIT_SYMBOLS + import ..CONSTANT_SYMBOLS import ..SymbolicDimensionsSingleton import ...constructorof import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE @@ -353,22 +354,34 @@ module SymbolicUnits import ....DEFAULT_VALUE_TYPE import ....DEFAULT_DIM_BASE_TYPE + const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_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)))) - ) + @eval begin + const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) + ) + push!(_SYMBOLIC_CONSTANT_VALUES, $unit) + end end + const SYMBOLIC_CONSTANT_VALUES = Tuple(_SYMBOLIC_CONSTANT_VALUES) end import .Constants import .Constants as SymbolicConstants + import .Constants: SYMBOLIC_CONSTANT_VALUES + const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] 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))) - ) + @eval begin + const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) + ) + push!(_SYMBOLIC_UNIT_VALUES, $unit) + end end + const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES) """ @@ -388,17 +401,53 @@ module SymbolicUnits namespace collisions, a few physical constants are automatically converted. """ function sym_uparse(raw_string::AbstractString) - raw_result = eval(Meta.parse(raw_string)) + raw_result = eval(map_to_scope(Meta.parse(raw_string))) return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE end 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))") + + function map_to_scope(ex::Expr) + if ex.head == :call + ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + return ex + elseif ex.head == :tuple + ex.args[:] = map(map_to_scope, ex.args) + return ex + elseif ex.head == :. && ex.args[1] == :Constants + @assert ex.args[2] isa QuoteNode + return lookup_constant(ex.args[2].value) + else + throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` (for `SymbolicConstants`) are expected.")) + end + end + function map_to_scope(sym::Symbol) + if sym in UNIT_SYMBOLS + return lookup_unit(sym) + elseif sym in CONSTANT_SYMBOLS + throw(ArgumentError("Symbol $sym found in `SymbolicConstants` but not `SymbolicUnits`. Please access the `SymbolicConstants` module. For example, `u\"SymbolicConstants.$sym\"`.")) + else + throw(ArgumentError("Symbol $sym not found in `SymbolicUnits` or `SymbolicConstants`.")) + end + end + function map_to_scope(ex) + return ex + end + function lookup_unit(ex::Symbol) + i = findfirst(==(ex), UNIT_SYMBOLS)::Int + return SYMBOLIC_UNIT_VALUES[i] + end + function lookup_constant(ex::Symbol) + i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int + return SYMBOLIC_CONSTANT_VALUES[i] + end end import .SymbolicUnits: sym_uparse import .SymbolicUnits: SymbolicConstants +import .SymbolicUnits: map_to_scope """ us"[unit expression]" @@ -416,7 +465,8 @@ 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(SymbolicUnits.sym_uparse(s)) + ex = Meta.parse(s) + return esc(map_to_scope(ex)) end function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{SymbolicDimensionsSingleton{R2}}) where {R1,R2} From e0c58c7013ad708ea2d2e486275fb02c66ecea2c Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:57:13 +0000 Subject: [PATCH 07/25] Ensure to always convert away from singletons when parsing --- 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 43bb5d3c..c60c95f2 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -437,11 +437,11 @@ module SymbolicUnits end function lookup_unit(ex::Symbol) i = findfirst(==(ex), UNIT_SYMBOLS)::Int - return SYMBOLIC_UNIT_VALUES[i] + return as_quantity(SYMBOLIC_UNIT_VALUES[i]) end function lookup_constant(ex::Symbol) i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int - return SYMBOLIC_CONSTANT_VALUES[i] + return as_quantity(SYMBOLIC_CONSTANT_VALUES[i]) end end From 8c870a871f2bdf0e3fa7dca7f962db97556894bb Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:57:59 +0000 Subject: [PATCH 08/25] Introduce `NoDims` type for working around non-quantities --- src/types.jl | 14 ++++++++++++++ src/utils.jl | 9 +++++++++ test/unittests.jl | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/src/types.jl b/src/types.jl index 68f519b0..02423100 100644 --- a/src/types.jl +++ b/src/types.jl @@ -122,6 +122,20 @@ end const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE} +""" + NoDims{R} + +A type representing the dimensions of a non-quantity. + +For any `getproperty` call on this type, the result is `zero(R)`. +""" +struct NoDims{R<:Real} <: AbstractDimensions{R} +end + +Base.getproperty(::NoDims{R}, ::Symbol) where {R} = zero(R) + +const DEFAULT_DIMENSIONLESS_TYPE = NoDims{DEFAULT_DIM_BASE_TYPE} + """ Quantity{T<:Number,D<:AbstractDimensions} <: AbstractQuantity{T,D} <: Number diff --git a/src/utils.jl b/src/utils.jl index c7fde640..0326f12d 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -28,6 +28,13 @@ end function Base.promote_rule(::Type{Dimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end +function Base.promote_rule(::Type{NoDims{R1}}, ::Type{NoDims{R2}}) where {R1,R2} + return NoDims{promote_type(R1,R2)} +end +function Base.promote_rule(::Type{NoDims{R1}}, ::Type{D}) where {R1,R2,D<:AbstractDimensions{R2}} + # The `R1` type is "unused" so we ignore it + return D +end # Define all the quantity x quantity promotion rules """ @@ -340,12 +347,14 @@ ustrip(::AbstractDimensions) = error("Cannot remove units from an `AbstractDimen """ dimension(q::AbstractQuantity) dimension(q::AbstractGenericQuantity) + dimension(x) Get the dimensions of a quantity, returning an `AbstractDimensions` object. """ dimension(q::UnionAbstractQuantity) = q.dimensions dimension(d::AbstractDimensions) = d dimension(aq::AbstractArray{<:UnionAbstractQuantity}) = allequal(dimension.(aq)) ? dimension(first(aq)) : throw(DimensionError(aq[begin], aq[begin+1:end])) +dimension(_) = DEFAULT_DIMENSIONLESS_TYPE() """ ulength(q::AbstractQuantity) diff --git a/test/unittests.jl b/test/unittests.jl index 5310f4f7..19840442 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -478,6 +478,10 @@ end VERSION >= v"1.9" && @test_throws "Symbol x not found" uparse("x") VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" uparse("c") VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("import ..Units") + @test_throws LoadError eval(:(us"x")) + VERSION >= v"1.9" && @test_throws "Symbol x not found" sym_uparse("x") + VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" sym_uparse("c") + VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("import ..Units") end @testset "Constants" begin From c80113dc74b67e9b05da00290e623e5feb4df0a3 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:59:34 +0000 Subject: [PATCH 09/25] Ensure `iszero(::NoDims)` is true --- src/utils.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.jl b/src/utils.jl index 0326f12d..407b1552 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -254,6 +254,7 @@ for f in ( @eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q)) end Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) +Base.iszero(::NoDims) = true Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) From 8d646d1b2d87061d0287a01bba34725e16dd9c22 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 4 Jan 2024 23:59:59 +0000 Subject: [PATCH 10/25] Fix error test --- test/unittests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index 19840442..e19a64e1 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -480,7 +480,7 @@ end VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("import ..Units") @test_throws LoadError eval(:(us"x")) VERSION >= v"1.9" && @test_throws "Symbol x not found" sym_uparse("x") - VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" sym_uparse("c") + VERSION >= v"1.9" && @test_throws "Symbol c found in `SymbolicConstants` but not `SymbolicUnits`" sym_uparse("c") VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("import ..Units") end From 7b99fed62220037b7ca9626dcd6d4e964fa347f0 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 5 Jan 2024 00:06:28 +0000 Subject: [PATCH 11/25] Extra tests of `NoDims` --- test/unittests.jl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index e19a64e1..77a3052c 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,5 +1,5 @@ using DynamicQuantities -using DynamicQuantities: FixedRational +using DynamicQuantities: FixedRational, NoDims using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using DynamicQuantities: array_type, value_type, dim_type, quantity_type using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof @@ -1633,6 +1633,18 @@ end @eval @test all($f.($qx_real_dimensions, $qy_dimensions) .== $ground_truth) @eval @test all($f.($qx_dimensions, $qy_real_dimensions) .== $ground_truth) end + + # Should be able to compare against `NoDims`: + @test Quantity(1.0) >= 1.0 + @test !(Quantity(1.0) > 1.0) +end + +@testset "Extra tests of `NoDims`" begin + @test promote_type(NoDims{Int16}, NoDims{Int32}) === NoDims{Int32} + + # Prefer other types, always: + @test promote_type(Dimensions{Int16}, NoDims{Int32}) === Dimensions{Int16} + @test promote_type(MyDimensions{Int16}, NoDims{Int32}) === MyDimensions{Int16} end @testset "Test div" begin From 56e47a001f3e64d8dd65dfcb2e0fa42ea0670edd Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 11 Jan 2024 22:00:38 +0000 Subject: [PATCH 12/25] Enforce type stability for `u_str` --- src/symbolic_dimensions.jl | 16 ++++++++-------- src/uparse.jl | 9 ++++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index c60c95f2..8be8831b 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -400,9 +400,10 @@ module SymbolicUnits `Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ - function sym_uparse(raw_string::AbstractString) - raw_result = eval(map_to_scope(Meta.parse(raw_string))) - return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE + function sym_uparse(s::AbstractString) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return copy(eval(ex))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE end as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE) = q @@ -445,9 +446,7 @@ module SymbolicUnits end end -import .SymbolicUnits: sym_uparse -import .SymbolicUnits: SymbolicConstants -import .SymbolicUnits: map_to_scope +import .SymbolicUnits: as_quantity, sym_uparse, SymbolicConstants, map_to_scope """ us"[unit expression]" @@ -465,8 +464,9 @@ 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) - ex = Meta.parse(s) - return esc(map_to_scope(ex)) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return esc(ex) end function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{SymbolicDimensionsSingleton{R2}}) where {R1,R2} diff --git a/src/uparse.jl b/src/uparse.jl index 695a0bf2..01a01e8b 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -35,7 +35,9 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ function uparse(s::AbstractString) - return as_quantity(eval(map_to_scope(Meta.parse(s))))::DEFAULT_QUANTITY_TYPE + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return eval(ex)::DEFAULT_QUANTITY_TYPE end as_quantity(q::DEFAULT_QUANTITY_TYPE) = q @@ -55,8 +57,9 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ macro u_str(s) - ex = Meta.parse(s) - return esc(map_to_scope(ex)) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return esc(ex) end function map_to_scope(ex::Expr) From a3f839a3350c4a72946a2503c48c91010bc462eb Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 11 Jan 2024 22:06:17 +0000 Subject: [PATCH 13/25] Fix parsing issue --- src/symbolic_dimensions.jl | 5 +---- src/uparse.jl | 5 +---- test/unittests.jl | 12 ++++++------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 8be8831b..853c2150 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -414,14 +414,11 @@ module SymbolicUnits if ex.head == :call ex.args[2:end] = map(map_to_scope, ex.args[2:end]) return ex - elseif ex.head == :tuple - ex.args[:] = map(map_to_scope, ex.args) - return ex elseif ex.head == :. && ex.args[1] == :Constants @assert ex.args[2] isa QuoteNode return lookup_constant(ex.args[2].value) else - throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` (for `SymbolicConstants`) are expected.")) + throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `SymbolicConstants`) are expected.")) end end function map_to_scope(sym::Symbol) diff --git a/src/uparse.jl b/src/uparse.jl index 01a01e8b..fa90c751 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -66,14 +66,11 @@ function map_to_scope(ex::Expr) if ex.head == :call ex.args[2:end] = map(map_to_scope, ex.args[2:end]) return ex - elseif ex.head == :tuple - ex.args[:] = map(map_to_scope, ex.args) - return ex elseif ex.head == :. && ex.args[1] == :Constants @assert ex.args[2] isa QuoteNode return lookup_constant(ex.args[2].value) else - throw(ArgumentError("Unexpected expression: $ex. Only `:call`, `:tuple`, and `:.` (for `Constants`) are expected.")) + throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `Constants`) are expected.")) end end function map_to_scope(sym::Symbol) diff --git a/test/unittests.jl b/test/unittests.jl index 77a3052c..403e5c82 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -462,26 +462,26 @@ end @test utime(z) == 1 @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 - # Test that `u_str` respects original type: - @test typeof(u"1") == Int - @test typeof(u"1f0") == Float32 + # Test type stability of extreme range of units + @test typeof(u"1") == DEFAULT_QUANTITY_TYPE + @test typeof(u"1f0") == DEFAULT_QUANTITY_TYPE @test typeof(u"s"^2) == DEFAULT_QUANTITY_TYPE @test typeof(u"Ω") == DEFAULT_QUANTITY_TYPE @test typeof(u"Gyr") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE - # Can also use tuples: - @test typeof(u"(m, s)") == Tuple{DEFAULT_QUANTITY_TYPE, DEFAULT_QUANTITY_TYPE} + @test_throws ErrorException eval(:(u":x")) - @test_throws LoadError eval(:(u"x")) VERSION >= v"1.9" && @test_throws "Symbol x not found" uparse("x") VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" uparse("c") VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("import ..Units") + VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("(m, m)") @test_throws LoadError eval(:(us"x")) VERSION >= v"1.9" && @test_throws "Symbol x not found" sym_uparse("x") VERSION >= v"1.9" && @test_throws "Symbol c found in `SymbolicConstants` but not `SymbolicUnits`" sym_uparse("c") VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("import ..Units") + VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("(m, m)") end @testset "Constants" begin From 03242c19bf11f4db8d3ef4b2a17a88300b1b25bd Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 00:41:24 +0000 Subject: [PATCH 14/25] Expand test coverage --- src/DynamicQuantities.jl | 5 ++- src/symbolic_dimensions.jl | 9 ++--- test/unittests.jl | 68 +++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 9f49b04c..e25bfce3 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -2,7 +2,10 @@ module DynamicQuantities export Units, Constants, SymbolicUnits, SymbolicConstants export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity -export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError +export Quantity, GenericQuantity, RealQuantity +export Dimensions, SymbolicDimensions, SymbolicDimensionsSingleton, NoDims +export QuantityArray +export DimensionError export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 853c2150..3ed57234 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -99,15 +99,16 @@ 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} +constructorof(::Type{<:SymbolicDimensionsSingleton}) = SymbolicDimensionsSingleton 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 + +# Need to construct with `R` if available, as can't figure it out otherwise: +constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R} # Conversion: function SymbolicDimensions(d::SymbolicDimensionsSingleton{R}) where {R} @@ -197,7 +198,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) = constructorof(d)(getfield(d, :dim)) +Base.copy(d::SymbolicDimensionsSingleton) = constructorof(typeof(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 403e5c82..55efefe7 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -156,6 +156,8 @@ end @test abs(x) == abs(Quantity(1.2, length=2 // 5)) @test abs2(x) == Quantity(abs2(-1.2), length=4 // 5) + @test copy(x) == x + @test iszero(x) == false @test iszero(x * 0) == true @test isfinite(x) == true @@ -461,6 +463,7 @@ end z = u"yr" @test utime(z) == 1 @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 + @test z == uparse("yr") # Test type stability of extreme range of units @test typeof(u"1") == DEFAULT_QUANTITY_TYPE @@ -694,6 +697,7 @@ end @inferred f2(5) @test uexpand(f2(5)) == u"s"^5 + @test_throws ErrorException uparse("'c'") @test_throws ErrorException sym_uparse("'c'") # For constants which have a namespace collision, the numerical expansion is used: @@ -734,6 +738,17 @@ end @test qa isa Vector{Quantity{Float64,SymbolicDimensions{Rational{Int}}}} DynamicQuantities.with_type_parameters(SymbolicDimensions{Float64}, Rational{Int}) == SymbolicDimensions{Rational{Int}} + # Many symbols in one: + x = us"pm * fm * nm * μm * mm * cm * dm * m * km" + y = us"s" + @test inv(x) != x + @test dimension(inv(x)).pm == -1 + @test x != y + @test y != x + @test dimension(uexpand(x * y)) == dimension(u"m^9 * s") + z = uexpand(x) + @test x == z + @testset "Promotion with Dimensions" begin x = 0.5u"cm" y = -0.03u"m" @@ -1639,12 +1654,63 @@ end @test !(Quantity(1.0) > 1.0) end -@testset "Extra tests of `NoDims`" begin +@testset "Tests of `NoDims`" begin @test promote_type(NoDims{Int16}, NoDims{Int32}) === NoDims{Int32} # Prefer other types, always: @test promote_type(Dimensions{Int16}, NoDims{Int32}) === Dimensions{Int16} @test promote_type(MyDimensions{Int16}, NoDims{Int32}) === MyDimensions{Int16} + + # Always zero dimensions + @test iszero(dimension(1.0)) + @test iszero(dimension([1.0, 2.0])) + @test dimension(1.0) * u"1" == u"1" + @test typeof(dimension(1.0) * u"1") === typeof(u"1") + + # Even when accessed: + @test NoDims().km == 0 + @test NoDims().m != 1 + + # Even weird user-defined dimensions: + @test NoDims().cookie == 0 + + # Always returns the same type: + @test NoDims{Int32}().cookie isa Int32 + @test NoDims{Int32}().cookie == 0 +end + +@testset "Tests of SymbolicDimensionsSingleton" begin + km = SymbolicUnits.km + @test km isa Quantity{T,SymbolicDimensionsSingleton{R}} where {T,R} + @test dimension(km) isa SymbolicDimensionsSingleton + @test dimension(km) isa AbstractSymbolicDimensions + + @test dimension(km).km == 1 + @test dimension(km).m == 0 + VERSION >= v"1.9" && + @test_throws "is not available as a symbol" dimension(km).γ + @test !iszero(km) + @test inv(km) == us"km^-1" + @test inv(km) == u"km^-1" + + # Constructors + @test SymbolicDimensionsSingleton(:cm) isa SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE} + @test constructorof(SymbolicDimensionsSingleton) === SymbolicDimensionsSingleton + + @test with_type_parameters( + SymbolicDimensionsSingleton{Int64}, + Int32 + ) === SymbolicDimensionsSingleton{Int32} + + @test convert( + SymbolicDimensions, + SymbolicDimensionsSingleton{Int32}(:cm) + ) isa SymbolicDimensions{Int32} + + @test copy(km) == km + + # Any operation should immediately convert it: + @test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R} end @testset "Test div" begin From f89203d4d07d318f6cfbdf2db21e23824cd7c69f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 00:45:08 +0000 Subject: [PATCH 15/25] Improve docs --- docs/src/types.md | 13 +++++++++++++ test/unittests.jl | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/src/types.md b/docs/src/types.md index 2a0bc17d..102fd5a2 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -40,6 +40,19 @@ which is subtyped to `Any`. ```@docs GenericQuantity AbstractGenericQuantity +``` + +In the other direction, there is also `RealQuantity`, +which is subtyped to `Real`. + +```@docs +RealQuantity +AbstractRealQuantity +``` + +More general, these are each contained in the following: + +```@docs UnionAbstractQuantity DynamicQuantities.ABSTRACT_QUANTITY_TYPES ``` diff --git a/test/unittests.jl b/test/unittests.jl index 55efefe7..dfeb3120 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,5 +1,5 @@ using DynamicQuantities -using DynamicQuantities: FixedRational, NoDims +using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using DynamicQuantities: array_type, value_type, dim_type, quantity_type using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof From 7f1997f2dd11ef06658b3d06328f20a246372bbf Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 00:51:21 +0000 Subject: [PATCH 16/25] Improve docs --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 63f7ae13..6f417995 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,9 @@ julia> room_temp = 100kPa 100000.0 m⁻¹ kg s⁻² ``` +Note that `Units` is an exported submodule, so you can +also access this as `Units.kPa`. + This supports a wide range of SI base and derived units, with common prefixes. @@ -176,6 +179,12 @@ julia> u"Constants.c * Hz" 2.99792458e8 m s⁻² ``` +Similarly, you can just import these: + +```julia +julia> using DynamicQuantities.Constants: c +``` + For the full list, see the [docs](https://symbolicml.org/DynamicQuantities.jl/dev/constants/). @@ -220,6 +229,23 @@ julia> uconvert(us"nm", 5e-9u"m") # can also write 5e-9u"m" |> uconvert(us"nm") 5.0 nm ``` +Finally, you can also import these directly: + +```julia +julia> using DynamicQuantities.SymbolicUnits: cm +``` + +or constants: + +```julia +julia> using DynamicQuantities.SymbolicConstants: h +``` + +Note that `SymbolicUnits` and `SymbolicConstants` are exported, +so you can simply access these as `SymbolicUnits.cm` and `SymbolicConstants.h`, +respectively. + + ### Arrays For working with an array of quantities that have the same dimensions, From ea0447b71c8c28b1e0a35ccc3a556a2fab594566 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 00:52:52 +0000 Subject: [PATCH 17/25] More test coverage --- test/unittests.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index dfeb3120..266217d9 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1689,10 +1689,13 @@ end @test dimension(km).m == 0 VERSION >= v"1.9" && @test_throws "is not available as a symbol" dimension(km).γ - @test !iszero(km) + @test !iszero(dimension(km)) @test inv(km) == us"km^-1" @test inv(km) == u"km^-1" + @test !iszero(dimension(SymbolicConstants.c)) + @test SymbolicConstants.c isa Quantity{T,SymbolicDimensionsSingleton{R}} where {T,R} + # Constructors @test SymbolicDimensionsSingleton(:cm) isa SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE} @test constructorof(SymbolicDimensionsSingleton) === SymbolicDimensionsSingleton From 1658ebb3475cbfb1f34fedb484c2808f9d655cf2 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 00:55:20 +0000 Subject: [PATCH 18/25] More tests --- test/unittests.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index 266217d9..c9253c57 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1714,6 +1714,22 @@ end # Any operation should immediately convert it: @test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R} + + # Test promotion explicitly for coverage: + @test promote_type( + SymbolicDimensionsSingleton{Int16}, + SymbolicDimensionsSingleton{Int32} + ) === SymbolicDimensions{Int32} + # ^ Note how we ALWAYS convert to SymbolicDimensions, even + # if the types are the same. + @test promote_type( + SymbolicDimensionsSingleton{Int16}, + SymbolicDimensions{Int32} + ) === SymbolicDimensions{Int32} + @test promote_type( + SymbolicDimensionsSingleton{Int64}, + Dimensions{Int16} + ) === Dimensions{Int64} end @testset "Test div" begin From d173a25bc9285f0a475308822dd02d5de9967164 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:11:26 +0000 Subject: [PATCH 19/25] More coverage --- README.md | 14 +++++++++++++- docs/src/types.md | 11 +++++++++++ test/unittests.jl | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f417995..649cfb4e 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,19 @@ julia> using DynamicQuantities.SymbolicConstants: h Note that `SymbolicUnits` and `SymbolicConstants` are exported, so you can simply access these as `SymbolicUnits.cm` and `SymbolicConstants.h`, -respectively. +respectively. I like to work with these like so: + +```julia +julia> const u = DynamicQuantities.SymbolicUnits; + +julia> const C = DynamicQuantities.SymbolicConstants; +``` + +so that I can simply write things like: + +```julia +julia> u.yr * C.c |> uexpand |> uconvert(u.km) +``` ### Arrays diff --git a/docs/src/types.md b/docs/src/types.md index 102fd5a2..30d5f9be 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -25,6 +25,17 @@ Another type which subtypes `AbstractDimensions` is `SymbolicDimensions`: SymbolicDimensions ``` +Just note that all of the symbolic units and constants are stored using the +immutable `SymbolicDimensionsSingleton`, which shares the same +supertype `AbstractSymbolicDimensions <: SymbolicDimensions`. These get immediately +converted to the mutable `SymbolicDimensions` when used in any +calculation. + +```@docs +SymbolicDimensionsSingleton +AbstractSymbolicDimensions +``` + ## Arrays ```@docs diff --git a/test/unittests.jl b/test/unittests.jl index c9253c57..db8e6f2c 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1681,6 +1681,7 @@ end @testset "Tests of SymbolicDimensionsSingleton" begin km = SymbolicUnits.km + m = SymbolicUnits.m @test km isa Quantity{T,SymbolicDimensionsSingleton{R}} where {T,R} @test dimension(km) isa SymbolicDimensionsSingleton @test dimension(km) isa AbstractSymbolicDimensions @@ -1730,6 +1731,19 @@ end SymbolicDimensionsSingleton{Int64}, Dimensions{Int16} ) === Dimensions{Int64} + + # Test map_dimensions explicitly for coverage: + @test map_dimensions(-, dimension(km)).km == -1 + @test map_dimensions(-, dimension(km)) isa SymbolicDimensions + @test map_dimensions(+, dimension(km), dimension(m)).km == 1 + @test map_dimensions(+, dimension(km), dimension(m)).m == 1 + @test map_dimensions(+, dimension(km), dimension(m)).cm == 0 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).km == 1 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).m == 1 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).cm == 0 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).km == 1 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).m == 1 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).cm == 0 end @testset "Test div" begin From 76737ff34abef7ddfb8f3dde37e9da2125283285 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:16:07 +0000 Subject: [PATCH 20/25] More coverage --- test/unittests.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index db8e6f2c..c85cfb94 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -4,6 +4,7 @@ using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_D using DynamicQuantities: array_type, value_type, dim_type, quantity_type using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value +using DynamicQuantities: map_dimensions using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using StaticArrays: SArray, MArray @@ -749,6 +750,14 @@ end z = uexpand(x) @test x == z + # Trigger part of map_dimensions missed elsewhere + x = us"km" + y = us"km" + @test x * y |> uexpand == u"km^2" + @test x / y |> uexpand == u"1" + @test map_dimensions(+, dimension(us"km"), dimension(us"km")) == dimension(us"km^2") + @test map_dimensions(-, dimension(us"km"), dimension(us"km")) == dimension(us"1") + @testset "Promotion with Dimensions" begin x = 0.5u"cm" y = -0.03u"m" From bf357433bf19c2e5311d2292eeb902ce207ab857 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:38:27 +0000 Subject: [PATCH 21/25] Constraint uconvert --- README.md | 23 ++++++++--------------- src/symbolic_dimensions.jl | 38 +++++++++++++++++++++++++++++++++++--- test/unittests.jl | 10 ++++++++++ 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 649cfb4e..df8d0379 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,13 @@ julia> room_temp = 100kPa ``` Note that `Units` is an exported submodule, so you can -also access this as `Units.kPa`. +also access this as `Units.kPa`. You may like to define + +```julia +julia> const U = Units; const C = Constants; +``` + +so that you can simply write, say, `U.kPa` or `C.m_e`. This supports a wide range of SI base and derived units, with common prefixes. @@ -243,20 +249,7 @@ julia> using DynamicQuantities.SymbolicConstants: h Note that `SymbolicUnits` and `SymbolicConstants` are exported, so you can simply access these as `SymbolicUnits.cm` and `SymbolicConstants.h`, -respectively. I like to work with these like so: - -```julia -julia> const u = DynamicQuantities.SymbolicUnits; - -julia> const C = DynamicQuantities.SymbolicConstants; -``` - -so that I can simply write things like: - -```julia -julia> u.yr * C.c |> uexpand |> uconvert(u.km) -``` - +respectively. ### Arrays diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 3ed57234..42f56286 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -171,7 +171,7 @@ uexpand(q::QuantityArray) = uexpand.(q) 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, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, 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)) @@ -179,7 +179,7 @@ function uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimension new_dim = dimension(qout) return new_quantity(typeof(q), new_val, new_dim) end -function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}, 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)) @@ -187,7 +187,39 @@ function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions new_dim = dimension(qout) return QuantityArray(new_array, new_dim, quantity_type(q)) end -# TODO: Method for converting SymbolicDimensions -> SymbolicDimensions + +# Ensure we always do operations with SymbolicDimensions: +function uconvert( + qout::UnionAbstractQuantity{T,<:SymbolicDimensionsSingleton{R}}, + q::Union{ + <:UnionAbstractQuantity{<:Any,<:Dimensions}, + <:QuantityArray{<:Any,<:Any,<:Dimensions}, + }, +) where {T,R} + return uconvert( + convert( + with_type_parameters( + typeof(qout), + T, + with_type_parameters(SymbolicDimensions, R), + ), + qout, + ), + q, + ) +end + +# Allow user to convert SymbolicDimensions -> SymbolicDimensions +function uconvert( + qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions{R}}, + q::Union{ + <:UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, + <:QuantityArray{<:Any,<:Any,<:AbstractSymbolicDimensions}, + }, +) where {R} + return uconvert(qout, uexpand(q)) +end + """ uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}) diff --git a/test/unittests.jl b/test/unittests.jl index c85cfb94..1c1d1e72 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1753,6 +1753,16 @@ end @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).km == 1 @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).m == 1 @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).cm == 0 + + # Note that we avoid converting to SymbolicDimensionsSingleton for uconvert: + @test km |> uconvert(us"m") == 1000m + @test km |> uconvert(us"m") isa Quantity{T,SymbolicDimensions{R}} where {T,R} + @test [km, km] isa Vector{Quantity{T,SymbolicDimensionsSingleton{R}}} where {T,R} + @test [km^2, km] isa Vector{Quantity{T,SymbolicDimensions{R}}} where {T,R} + + # Symbolic dimensions retain symbols: + @test QuantityArray([km, km]) |> uconvert(us"m") == [1000m, 1000m] + @test QuantityArray([km, km]) |> uconvert(us"m") != [km, km] end @testset "Test div" begin From fe1060028cb5dbdf3235f8f401455267b5f9cda8 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:39:21 +0000 Subject: [PATCH 22/25] More exports for working with dimensions --- src/DynamicQuantities.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index e25bfce3..46e83662 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,9 +1,10 @@ module DynamicQuantities export Units, Constants, SymbolicUnits, SymbolicConstants -export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity +export AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity export Quantity, GenericQuantity, RealQuantity -export Dimensions, SymbolicDimensions, SymbolicDimensionsSingleton, NoDims +export AbstractDimensions, Dimensions, NoDims +export AbstractSymbolicDimensions, SymbolicDimensions, SymbolicDimensionsSingleton export QuantityArray export DimensionError export ustrip, dimension From 133445eb00188843943735644d94de4e0a614b2d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:40:21 +0000 Subject: [PATCH 23/25] Update docs --- docs/src/types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/types.md b/docs/src/types.md index 30d5f9be..65a82599 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -27,7 +27,7 @@ SymbolicDimensions Just note that all of the symbolic units and constants are stored using the immutable `SymbolicDimensionsSingleton`, which shares the same -supertype `AbstractSymbolicDimensions <: SymbolicDimensions`. These get immediately +supertype `AbstractSymbolicDimensions <: AbstractDimensions`. These get immediately converted to the mutable `SymbolicDimensions` when used in any calculation. From 5ff5e706906e5b7d934bd73cbefa18f97690c0c6 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:44:15 +0000 Subject: [PATCH 24/25] Improve readme --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df8d0379..d3b711b2 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Note that `Units` is an exported submodule, so you can also access this as `Units.kPa`. You may like to define ```julia -julia> const U = Units; const C = Constants; +julia> const U = Units ``` so that you can simply write, say, `U.kPa` or `C.m_e`. @@ -178,6 +178,12 @@ julia> Constants.c 2.99792458e8 m s⁻¹ ``` +which you may like to define as + +```julia +julia> const C = Constants +``` + These can also be used inside the `u"..."` macro: ```julia @@ -185,10 +191,10 @@ julia> u"Constants.c * Hz" 2.99792458e8 m s⁻² ``` -Similarly, you can just import these: +Similarly, you can just import each individual constant: ```julia -julia> using DynamicQuantities.Constants: c +julia> using DynamicQuantities.Constants: h ``` For the full list, see the [docs](https://symbolicml.org/DynamicQuantities.jl/dev/constants/). From 06365063fbfeabc3d668d86e57c500722fb01538 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 12 Jan 2024 01:49:47 +0000 Subject: [PATCH 25/25] Fix coverage --- test/unittests.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index 1c1d1e72..57074e1a 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1760,6 +1760,11 @@ end @test [km, km] isa Vector{Quantity{T,SymbolicDimensionsSingleton{R}}} where {T,R} @test [km^2, km] isa Vector{Quantity{T,SymbolicDimensions{R}}} where {T,R} + # No issue when converting to SymbolicDimensionsSingleton (gets + # converted) + @test uconvert(km, u"m") == 0.001km + @test uconvert(km, u"m") isa Quantity{T,SymbolicDimensions{R}} where {T,R} + # Symbolic dimensions retain symbols: @test QuantityArray([km, km]) |> uconvert(us"m") == [1000m, 1000m] @test QuantityArray([km, km]) |> uconvert(us"m") != [km, km]