From 04d2b14a9bf4b290dbfb076334e8c1e56a93b700 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:10:53 +0530 Subject: [PATCH 01/17] feat: allow registering units externally - By interpolating `Units.UNIT_SYMBOLS` while registering units and updating vectors-and-maps-of-units, new units can be registered even from outside the `Units` module - Internally, units are registered lazily with `_lazy_register_unit` aka `UNIT_MAPPING` is updated after all units are defined. --- src/DynamicQuantities.jl | 4 +- src/register_units.jl | 37 ++++++++++++++++ src/symbolic_dimensions.jl | 43 +++++++++--------- src/units.jl | 91 +++++++++++++++++++------------------- test/runtests.jl | 3 -- test/unittests.jl | 40 +++++++++++++++-- 6 files changed, 144 insertions(+), 74 deletions(-) create mode 100644 src/register_units.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 46e83662..bf81cd6d 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -23,9 +23,10 @@ include("uparse.jl") include("symbolic_dimensions.jl") include("complex.jl") include("disambiguities.jl") - +include("register_units.jl") include("deprecated.jl") export expand_units +export @register_unit import PackageExtensionCompat: @require_extensions import .Units @@ -43,7 +44,6 @@ let _units_import_expr = :(using .Units: m, g) eval(_units_import_expr) end - function __init__() @require_extensions end diff --git a/src/register_units.jl b/src/register_units.jl new file mode 100644 index 00000000..3c6d5b86 --- /dev/null +++ b/src/register_units.jl @@ -0,0 +1,37 @@ + +import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit +import .SymbolicUnits: + SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values! + +# Update the unit collections +function update_unit_mapping(name, value, unit_mapping::Dict{Symbol,Int} = UNIT_MAPPING) + unit_mapping[name] = length(unit_mapping) + 1 +end + +function update_all_values(name_symbol, unit) + push!(ALL_SYMBOLS, name_symbol) + push!(ALL_VALUES, unit) + ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1) +end + + +# Register +macro register_unit(name, value) + return esc(_register_unit(name, value)) +end + +function _register_unit(name::Symbol, value) + name_symbol = Meta.quot(name) + reg_expr = _lazy_register_unit(name, value) + push!(reg_expr.args, + quote + $update_unit_mapping($name_symbol, $value) + $update_all_values($name_symbol, $value) + $update_symbolic_unit_values!($name_symbol) + # suppress the print of `SYMBOLIC_UNIT_VALUES` + nothing + end + ) + return reg_expr +end + diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 42f56286..2b0d00ea 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -1,21 +1,20 @@ import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES - const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s -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..., - disambiguate_symbol.(CONSTANT_SYMBOLS)... -) -const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...) -const ALL_MAPPING = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS))) +const INDEX_TYPE = UInt16 +# 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..., disambiguate_symbol.(CONSTANT_SYMBOLS)...] +const ALL_VALUES = [UNIT_VALUES..., CONSTANT_VALUES...] +const ALL_MAPPING = Dict(ALL_SYMBOLS .=> (INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS)))) """ AbstractSymbolicDimensions{R} <: AbstractDimensions{R} @@ -91,7 +90,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{R}(i) + SymbolicDimensionsSingleton{R}(i) end # Traits: @@ -169,7 +168,7 @@ uexpand(q::QuantityArray) = uexpand.(q) 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)`. +Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`. """ 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." @@ -224,7 +223,7 @@ end """ 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 +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,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout) @@ -368,6 +367,7 @@ to enable pretty-printing of units. """ module SymbolicUnits + import ..UNIT_SYMBOLS import ..CONSTANT_SYMBOLS import ..SymbolicDimensionsSingleton @@ -383,9 +383,9 @@ module SymbolicUnits import ...SymbolicDimensionsSingleton import ...constructorof import ...disambiguate_symbol - import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE - import ....DEFAULT_VALUE_TYPE - import ....DEFAULT_DIM_BASE_TYPE + import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE + import ...DEFAULT_VALUE_TYPE + import ...DEFAULT_DIM_BASE_TYPE const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] @@ -393,7 +393,9 @@ module SymbolicUnits @eval begin const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}( + $(QuoteNode(disambiguate_symbol(unit))), + ), ) push!(_SYMBOLIC_CONSTANT_VALUES, $unit) end @@ -404,18 +406,19 @@ module SymbolicUnits import .Constants as SymbolicConstants import .Constants: SYMBOLIC_CONSTANT_VALUES - const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] - for unit in UNIT_SYMBOLS + const SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] + + function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES) @eval begin const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))), ) - push!(_SYMBOLIC_UNIT_VALUES, $unit) + push!($symbolic_unit_values, $unit) end end - const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES) + update_symbolic_unit_values!.(UNIT_SYMBOLS) """ sym_uparse(raw_string::AbstractString) diff --git a/src/units.jl b/src/units.jl index 7c016d67..0bc9743d 100644 --- a/src/units.jl +++ b/src/units.jl @@ -6,27 +6,30 @@ import ..DEFAULT_QUANTITY_TYPE @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." -const _UNIT_SYMBOLS = Symbol[] -const _UNIT_VALUES = DEFAULT_QUANTITY_TYPE[] +const UNIT_SYMBOLS = Symbol[] +const UNIT_VALUES = DEFAULT_QUANTITY_TYPE[] +const UNIT_MAPPING = Dict{Symbol,Int}() -macro register_unit(name, value) - return esc(_register_unit(name, value)) +macro _lazy_register_unit(name, value) + return esc(_lazy_register_unit(name, value)) end macro add_prefixes(base_unit, prefixes) @assert prefixes.head == :tuple - return esc(_add_prefixes(base_unit, prefixes.args, _register_unit)) + return esc(_add_prefixes(base_unit, prefixes.args, _lazy_register_unit)) end -function _register_unit(name::Symbol, value) - s = string(name) - return quote +function _lazy_register_unit(name::Symbol, value) + name_symbol = Meta.quot(name) + quote + haskey($UNIT_MAPPING, $name_symbol) && throw("Unit $($name_symbol) already exists.") const $name = $value - push!(_UNIT_SYMBOLS, Symbol($s)) - push!(_UNIT_VALUES, $name) + push!($UNIT_SYMBOLS, $name_symbol) + push!($UNIT_VALUES, $name) end end + function _add_prefixes(base_unit::Symbol, prefixes, register_function) all_prefixes = ( f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1, @@ -42,13 +45,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function) end # SI base units -@register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1) -@register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1) -@register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1) -@register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1) -@register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1) -@register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1) -@register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1) +@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length = 1) +@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass = 1) +@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time = 1) +@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current = 1) +@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature = 1) +@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity = 1) +@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount = 1) @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G) @add_prefixes g (p, n, μ, u, m, k) @@ -88,17 +91,17 @@ end ) # SI derived units -@register_unit Hz inv(s) -@register_unit N kg * m / s^2 -@register_unit Pa N / m^2 -@register_unit J N * m -@register_unit W J / s -@register_unit C A * s -@register_unit V W / A -@register_unit F C / V -@register_unit Ω V / A -@register_unit ohm Ω -@register_unit T N / (A * m) +@_lazy_register_unit Hz inv(s) +@_lazy_register_unit N kg * m / s^2 +@_lazy_register_unit Pa N / m^2 +@_lazy_register_unit J N * m +@_lazy_register_unit W J / s +@_lazy_register_unit C A * s +@_lazy_register_unit V W / A +@_lazy_register_unit F C / V +@_lazy_register_unit Ω V / A +@_lazy_register_unit ohm Ω +@_lazy_register_unit T N / (A * m) @add_prefixes Hz (n, μ, u, m, k, M, G) @add_prefixes N () @@ -156,17 +159,17 @@ end # Common assorted units ## Time -@register_unit min 60 * s -@register_unit minute min -@register_unit h 60 * min -@register_unit hr h -@register_unit day 24 * h -@register_unit d day -@register_unit wk 7 * day -@register_unit yr 365.25 * day -@register_unit inch 2.54 * cm -@register_unit ft 12 * inch -@register_unit mi 5280 * ft +@_lazy_register_unit min 60 * s +@_lazy_register_unit minute min +@_lazy_register_unit h 60 * min +@_lazy_register_unit hr h +@_lazy_register_unit day 24 * h +@_lazy_register_unit d day +@_lazy_register_unit wk 7 * day +@_lazy_register_unit yr 365.25 * day +@_lazy_register_unit inch 2.54 * cm +@_lazy_register_unit ft 12 * inch +@_lazy_register_unit mi 5280 * ft @add_prefixes min () @add_prefixes minute () @@ -178,7 +181,7 @@ end @add_prefixes yr (k, M, G) ## Volume -@register_unit L dm^3 +@_lazy_register_unit L dm^3 @add_prefixes L (μ, u, m, c, d) @@ -188,7 +191,7 @@ end ) ## Pressure -@register_unit bar 100 * kPa +@_lazy_register_unit bar 100 * kPa @add_prefixes bar (m,) @@ -203,9 +206,7 @@ end # Do not wish to define physical constants, as the number of symbols might lead to ambiguity. # The user should define these instead. -"""A tuple of all possible unit symbols.""" -const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS) -const UNIT_VALUES = Tuple(_UNIT_VALUES) -const UNIT_MAPPING = NamedTuple([s => i for (i, s) in enumerate(UNIT_SYMBOLS)]) +# Update `UNIT_MAPPING` with all internally defined unit symbols. +merge!(UNIT_MAPPING, Dict(UNIT_SYMBOLS .=> 1:lastindex(UNIT_SYMBOLS))) end diff --git a/test/runtests.jl b/test/runtests.jl index 70709e92..85c73883 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,9 +19,6 @@ else @safetestset "Measurements.jl integration tests" begin include("test_measurements.jl") end - @safetestset "Meshes.jl integration tests" begin - include("test_meshes.jl") - end @safetestset "Unit tests" begin include("unittests.jl") end diff --git a/test/unittests.jl b/test/unittests.jl index c7a9cf19..a896b2e6 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,9 +1,12 @@ using DynamicQuantities using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions -using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +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 using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value +using DynamicQuantities: UNIT_VALUES, UNIT_MAPPING, UNIT_SYMBOLS, ALL_MAPPING, ALL_SYMBOLS, ALL_VALUES +using DynamicQuantities.SymbolicUnits: SYMBOLIC_UNIT_VALUES using DynamicQuantities: map_dimensions using Ratios: SimpleRatio using SaferIntegers: SafeInt16 @@ -686,8 +689,8 @@ end @test !iszero(sym) end - q = 1.5us"km/s" - @test q == 1.5 * us"km" / us"s" + q = 1.5us"km/s" + @test q == 1.5 * us"km" / us"s" @test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) @test string(dimension(q)) == "s⁻¹ km" @test uexpand(q) == 1.5u"km/s" @@ -1727,7 +1730,7 @@ end ) 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} @@ -1848,3 +1851,32 @@ end y = Quantity(2.0im, mass=1) @test_throws DimensionError x^y end + +# `@testset` rewrites the test block with a `let...end`, resulting in an invalid +# local `const` (ref: src/units.jl:26). To avoid it, register units outside the +# test block. +map_count_before_registering = length(UNIT_MAPPING) +all_map_count_before_registering = length(ALL_MAPPING) +@register_unit MyV u"V" +@register_unit MySV us"V" +@register_unit MySV2 us"km/h" + +@testset "Register Unit" begin + @test MyV === u"V" + @test MyV == us"V" + @test MySV == us"V" + @test MySV2 == us"km/h" + + @test length(UNIT_MAPPING) == map_count_before_registering + 3 + @test length(ALL_MAPPING) == all_map_count_before_registering + 3 + + for my_unit in (MySV, MyV) + @test my_unit in UNIT_VALUES + @test my_unit in ALL_VALUES + @test my_unit in SYMBOLIC_UNIT_VALUES + end + for my_unit in (:MySV, :MyV) + @test my_unit in UNIT_SYMBOLS + @test my_unit in ALL_SYMBOLS + end +end From 2c869e303ac46e5db6692ab198b8a39f507e564d Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 2 Feb 2024 17:34:20 +0530 Subject: [PATCH 02/17] fix: import of vars into SymbolicUnits sybmodule --- src/register_units.jl | 2 -- src/symbolic_dimensions.jl | 19 ++++++++----------- test/runtests.jl | 3 +++ test/unittests.jl | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/register_units.jl b/src/register_units.jl index 3c6d5b86..d0722b5f 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -1,4 +1,3 @@ - import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit import .SymbolicUnits: SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values! @@ -14,7 +13,6 @@ function update_all_values(name_symbol, unit) ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1) end - # Register macro register_unit(name, value) return esc(_register_unit(name, value)) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 2b0d00ea..4f9c184d 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -90,7 +90,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).") - SymbolicDimensionsSingleton{R}(i) + return SymbolicDimensionsSingleton{R}(i) end # Traits: @@ -367,15 +367,14 @@ to enable pretty-printing of units. """ module SymbolicUnits - import ..UNIT_SYMBOLS import ..CONSTANT_SYMBOLS import ..SymbolicDimensionsSingleton - import ...constructorof - import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE - import ...DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE - import ...DEFAULT_VALUE_TYPE - import ...DEFAULT_DIM_BASE_TYPE + import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE + import ..constructorof + 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 @@ -393,9 +392,7 @@ module SymbolicUnits @eval begin const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}( - $(QuoteNode(disambiguate_symbol(unit))), - ), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) ) push!(_SYMBOLIC_CONSTANT_VALUES, $unit) end @@ -412,7 +409,7 @@ module SymbolicUnits @eval begin const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) ) push!($symbolic_unit_values, $unit) end diff --git a/test/runtests.jl b/test/runtests.jl index 85c73883..70709e92 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,9 @@ else @safetestset "Measurements.jl integration tests" begin include("test_measurements.jl") end + @safetestset "Meshes.jl integration tests" begin + include("test_meshes.jl") + end @safetestset "Unit tests" begin include("unittests.jl") end diff --git a/test/unittests.jl b/test/unittests.jl index a896b2e6..84b3751a 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -689,8 +689,8 @@ end @test !iszero(sym) end - q = 1.5us"km/s" - @test q == 1.5 * us"km" / us"s" + q = 1.5us"km/s" + @test q == 1.5 * us"km" / us"s" @test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) @test string(dimension(q)) == "s⁻¹ km" @test uexpand(q) == 1.5u"km/s" From 5c00b031ee32ae1820d094404a7277ac13a0b9b0 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Sun, 4 Feb 2024 18:45:16 +0530 Subject: [PATCH 03/17] feat: add `WriteOnceReadMany` and utils `UNIT_SYMBOLS`, `UNIT_VALUES`, `UNIT_MAPPING`, `ALL_VALUES`,`ALL_SYMBOLS`, ALL_MAPPING`, `SYMBOLIC_UNIT_VALUES` are instances of this collection type. With this data type only a certain set of operations are permitted on these collections. --- src/DynamicQuantities.jl | 5 ++++- src/register_units.jl | 39 ++++++++++++++++++++-------------- src/symbolic_dimensions.jl | 18 ++++++++-------- src/units.jl | 9 ++++---- src/write_once_read_many.jl | 42 +++++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 src/write_once_read_many.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index bf81cd6d..bb5973d7 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -11,8 +11,11 @@ export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert +const INDEX_TYPE = UInt16 + include("internal_utils.jl") include("fixed_rational.jl") +include("write_once_read_many.jl") include("types.jl") include("utils.jl") include("math.jl") @@ -39,7 +42,7 @@ using .Units: UNIT_SYMBOLS let _units_import_expr = :(using .Units: m, g) append!( _units_import_expr.args[1].args, - map(s -> Expr(:(.), s), filter(s -> s ∉ (:m, :g), UNIT_SYMBOLS)) + Expr(:(.), s) for s in UNIT_SYMBOLS if s ∉ (:m, :g) ) eval(_units_import_expr) end diff --git a/src/register_units.jl b/src/register_units.jl index d0722b5f..905726ae 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -3,14 +3,17 @@ import .SymbolicUnits: SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values! # Update the unit collections -function update_unit_mapping(name, value, unit_mapping::Dict{Symbol,Int} = UNIT_MAPPING) - unit_mapping[name] = length(unit_mapping) + 1 -end +const UNIT_UPDATE_LOCK = Threads.SpinLock() function update_all_values(name_symbol, unit) - push!(ALL_SYMBOLS, name_symbol) - push!(ALL_VALUES, unit) - ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1) + lock(UNIT_UPDATE_LOCK) do + push!(ALL_SYMBOLS, name_symbol) + push!(ALL_VALUES, unit) + i = lastindex(ALL_VALUES) + ALL_MAPPING[name_symbol] = i + UNIT_MAPPING[name_symbol] = i + update_symbolic_unit_values!(name_symbol) + end end # Register @@ -20,16 +23,20 @@ end function _register_unit(name::Symbol, value) name_symbol = Meta.quot(name) - reg_expr = _lazy_register_unit(name, value) - push!(reg_expr.args, - quote - $update_unit_mapping($name_symbol, $value) + index = get(ALL_MAPPING, name, INDEX_TYPE(0)) + if iszero(index) + reg_expr = _lazy_register_unit(name, value) + push!(reg_expr.args, quote $update_all_values($name_symbol, $value) - $update_symbolic_unit_values!($name_symbol) - # suppress the print of `SYMBOLIC_UNIT_VALUES` nothing - end - ) - return reg_expr + end) + return reg_expr + else + unit = ALL_VALUES[index] + # When a utility function to expand `value` to its final form becomes + # available, enable the following check. This will avoid throwing an error + # if user is trying to register an existing unit with matching values. + # unit.value != value && throw("Unit $name is already defined as $unit") + throw("Unit `$name` is already defined as `$unit`") + end end - diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 4f9c184d..84f2069c 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -1,3 +1,5 @@ +import ..WriteOnceReadMany +import ..INDEX_TYPE import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES @@ -8,13 +10,9 @@ disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s # 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 INDEX_TYPE = UInt16 -# 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..., disambiguate_symbol.(CONSTANT_SYMBOLS)...] -const ALL_VALUES = [UNIT_VALUES..., CONSTANT_VALUES...] -const ALL_MAPPING = Dict(ALL_SYMBOLS .=> (INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS)))) +const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...]) +const ALL_VALUES = WriteOnceReadMany([UNIT_VALUES..., CONSTANT_VALUES...]) +const ALL_MAPPING = WriteOnceReadMany(Dict(s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS))) """ AbstractSymbolicDimensions{R} <: AbstractDimensions{R} @@ -375,6 +373,7 @@ module SymbolicUnits import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE + import ..WriteOnceReadMany # Lazily create unit symbols (since there are so many) module Constants @@ -403,7 +402,7 @@ module SymbolicUnits import .Constants as SymbolicConstants import .Constants: SYMBOLIC_CONSTANT_VALUES - const SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] + const SYMBOLIC_UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_SYMBOLIC_QUANTITY_TYPE}}() function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES) @eval begin @@ -415,7 +414,8 @@ module SymbolicUnits end end - update_symbolic_unit_values!.(UNIT_SYMBOLS) + update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data) + update_symbolic_unit_values!(UNIT_SYMBOLS) """ sym_uparse(raw_string::AbstractString) diff --git a/src/units.jl b/src/units.jl index 0bc9743d..3cdba464 100644 --- a/src/units.jl +++ b/src/units.jl @@ -1,14 +1,14 @@ module Units +import ..WriteOnceReadMany import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_QUANTITY_TYPE @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." -const UNIT_SYMBOLS = Symbol[] -const UNIT_VALUES = DEFAULT_QUANTITY_TYPE[] -const UNIT_MAPPING = Dict{Symbol,Int}() +const UNIT_SYMBOLS = WriteOnceReadMany{Vector{Symbol}}() +const UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_QUANTITY_TYPE}}() macro _lazy_register_unit(name, value) return esc(_lazy_register_unit(name, value)) @@ -22,7 +22,6 @@ end function _lazy_register_unit(name::Symbol, value) name_symbol = Meta.quot(name) quote - haskey($UNIT_MAPPING, $name_symbol) && throw("Unit $($name_symbol) already exists.") const $name = $value push!($UNIT_SYMBOLS, $name_symbol) push!($UNIT_VALUES, $name) @@ -207,6 +206,6 @@ end # The user should define these instead. # Update `UNIT_MAPPING` with all internally defined unit symbols. -merge!(UNIT_MAPPING, Dict(UNIT_SYMBOLS .=> 1:lastindex(UNIT_SYMBOLS))) +const UNIT_MAPPING = WriteOnceReadMany(Dict(s => i for (i, s) in enumerate(UNIT_SYMBOLS))) end diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl new file mode 100644 index 00000000..5245b968 --- /dev/null +++ b/src/write_once_read_many.jl @@ -0,0 +1,42 @@ +""" + WriteOnceReadMany() + +Used for storing units, values, symbolic-units. +""" +struct WriteOnceReadMany{V} + _raw_data::V + + WriteOnceReadMany(_raw_data) = new{typeof(_raw_data)}(_raw_data) + WriteOnceReadMany{T}() where T = WriteOnceReadMany(T()) +end + +# Utility functions +for f in (:enumerate, :length, :lastindex) + @eval begin + Base.$f(w::WriteOnceReadMany) = $f(w._raw_data) + end +end + +Base.getindex(w::WriteOnceReadMany, i::Union{Int, INDEX_TYPE, Symbol}) = getindex(w._raw_data, i) + +Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) +Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i) + +Base.intersect(w::WriteOnceReadMany, v::AbstractSet) = intersect(w._raw_data, v) +Base.intersect(v::AbstractSet, w::WriteOnceReadMany) = intersect(v, w._raw_data) + +Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...) + +for f in (:findfirst, :filter) + @eval begin + Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data) + end +end + +Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, i::Int, s::Symbol) = setindex!(w, INDEX_TYPE(i), s) +function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, T}}, i::T, s::Symbol) where T <: Union{Int, INDEX_TYPE} + haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])") + setindex!(w._raw_data, i, s) +end + +Base.get(w::WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, a, b) = get(w._raw_data, a, b) From cbd071c4c9efa1a71efa3608830ba61ce8c3cfba Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Sun, 11 Feb 2024 12:48:02 +0530 Subject: [PATCH 04/17] test: precompile `@register_unit` in an external module. --- src/register_units.jl | 5 ++--- src/symbolic_dimensions.jl | 12 ++++++++++- .../ExternalUnitRegistration.jl | 21 +++++++++++++++++++ test/unittests.jl | 13 ++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 test/precompile_test/ExternalUnitRegistration.jl diff --git a/src/register_units.jl b/src/register_units.jl index 905726ae..bbd9e7c2 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -1,6 +1,5 @@ import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit -import .SymbolicUnits: - SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values! +import .SymbolicUnits: update_external_symbolic_unit_value # Update the unit collections const UNIT_UPDATE_LOCK = Threads.SpinLock() @@ -12,7 +11,7 @@ function update_all_values(name_symbol, unit) i = lastindex(ALL_VALUES) ALL_MAPPING[name_symbol] = i UNIT_MAPPING[name_symbol] = i - update_symbolic_unit_values!(name_symbol) + update_external_symbolic_unit_value(name_symbol) end end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 84f2069c..dcd19772 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -417,7 +417,17 @@ module SymbolicUnits update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data) update_symbolic_unit_values!(UNIT_SYMBOLS) - """ + # Non-eval version of `update_symbolic_unit_values!` for registering units in + # an external module. + function update_external_symbolic_unit_value(unit) + unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(unit) + ) + push!(SYMBOLIC_UNIT_VALUES, unit) + end + +""" sym_uparse(raw_string::AbstractString) Parse a string containing an expression of units and return the diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl new file mode 100644 index 00000000..48314590 --- /dev/null +++ b/test/precompile_test/ExternalUnitRegistration.jl @@ -0,0 +1,21 @@ +module ExternalUnitRegistration + +using DynamicQuantities: @register_unit, @u_str, @us_str, + ALL_MAPPING, ALL_SYMBOLS, DEFAULT_QUANTITY_TYPE, + DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING +using Test + +@register_unit Wb u"m^2*kg*s^-2*A^-1" + +@testset " Register Unit Inside a Module" begin + for collection in (UNIT_SYMBOLS, ALL_SYMBOLS, keys(ALL_MAPPING._raw_data), keys(UNIT_MAPPING._raw_data)) + @test :Wb ∈ collection + end + + w = u"Wb" + ws = us"Wb" + @test w isa DEFAULT_QUANTITY_TYPE + @test ws isa DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE +end + +end diff --git a/test/unittests.jl b/test/unittests.jl index 84b3751a..150e6705 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -8,6 +8,7 @@ using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value using DynamicQuantities: UNIT_VALUES, UNIT_MAPPING, UNIT_SYMBOLS, ALL_MAPPING, ALL_SYMBOLS, ALL_VALUES using DynamicQuantities.SymbolicUnits: SYMBOLIC_UNIT_VALUES using DynamicQuantities: map_dimensions +using DynamicQuantities: _register_unit using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using StaticArrays: SArray, MArray @@ -1861,6 +1862,8 @@ all_map_count_before_registering = length(ALL_MAPPING) @register_unit MySV us"V" @register_unit MySV2 us"km/h" +@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s")) + @testset "Register Unit" begin @test MyV === u"V" @test MyV == us"V" @@ -1880,3 +1883,13 @@ all_map_count_before_registering = length(ALL_MAPPING) @test my_unit in ALL_SYMBOLS end end + +push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) + +using ExternalUnitRegistration: Wb +@testset "Type of Extenral Unit" begin + @test Wb isa DEFAULT_QUANTITY_TYPE + @test Wb/u"m^2*kg*s^-2*A^-1" == 1.0 +end + +pop!(LOAD_PATH) From b44749f297a22e3f79f603229aa08220443942e6 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Sun, 11 Feb 2024 23:48:26 +0000 Subject: [PATCH 05/17] Clean up `write_once_read_many.jl` --- src/write_once_read_many.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index 5245b968..7e44fa59 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -17,7 +17,14 @@ for f in (:enumerate, :length, :lastindex) end end -Base.getindex(w::WriteOnceReadMany, i::Union{Int, INDEX_TYPE, Symbol}) = getindex(w._raw_data, i) +Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i) +Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b) + +# Only define setindex! for Dicts, and throw an error if the key already exists +function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol) + haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])") + setindex!(w._raw_data, i, s) +end Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i) @@ -33,10 +40,3 @@ for f in (:findfirst, :filter) end end -Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, i::Int, s::Symbol) = setindex!(w, INDEX_TYPE(i), s) -function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, T}}, i::T, s::Symbol) where T <: Union{Int, INDEX_TYPE} - haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])") - setindex!(w._raw_data, i, s) -end - -Base.get(w::WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, a, b) = get(w._raw_data, a, b) From 464d5a793892538fe9f5c033c8a4310071010c5a Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Sun, 11 Feb 2024 23:54:26 +0000 Subject: [PATCH 06/17] Clean up ambiguity calculation --- src/symbolic_dimensions.jl | 10 ++++------ src/write_once_read_many.jl | 3 --- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index dcd19772..d59c1ff1 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -3,14 +3,12 @@ import ..INDEX_TYPE import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES -const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) - -disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s +disambiguate_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s # 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 = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...]) +const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_constant_symbol.(CONSTANT_SYMBOLS)...]) const ALL_VALUES = WriteOnceReadMany([UNIT_VALUES..., CONSTANT_VALUES...]) const ALL_MAPPING = WriteOnceReadMany(Dict(s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS))) @@ -380,7 +378,7 @@ module SymbolicUnits import ...CONSTANT_SYMBOLS import ...SymbolicDimensionsSingleton import ...constructorof - import ...disambiguate_symbol + import ...disambiguate_constant_symbol import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE @@ -391,7 +389,7 @@ module SymbolicUnits @eval begin const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_constant_symbol(unit)))) ) push!(_SYMBOLIC_CONSTANT_VALUES, $unit) end diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index 7e44fa59..943c16c4 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -29,9 +29,6 @@ end Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i) -Base.intersect(w::WriteOnceReadMany, v::AbstractSet) = intersect(w._raw_data, v) -Base.intersect(v::AbstractSet, w::WriteOnceReadMany) = intersect(v, w._raw_data) - Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...) for f in (:findfirst, :filter) From f51b5547d3b6e385112fcfb75e48c1035c492f38 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Sun, 11 Feb 2024 23:59:28 +0000 Subject: [PATCH 07/17] Simplify WriteOnceReadMany --- src/write_once_read_many.jl | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index 943c16c4..cf2ff604 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -12,9 +12,10 @@ end # Utility functions for f in (:enumerate, :length, :lastindex) - @eval begin - Base.$f(w::WriteOnceReadMany) = $f(w._raw_data) - end + @eval Base.$f(w::WriteOnceReadMany) = $f(w._raw_data) +end +for f in (:findfirst, :filter) + @eval Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data) end Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i) @@ -27,13 +28,7 @@ function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Sy end Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) -Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i) +Base.iterate(w::WriteOnceReadMany, i) = iterate(w._raw_data, i) Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...) -for f in (:findfirst, :filter) - @eval begin - Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data) - end -end - From 6a75e3ca27da479b93eade2664e3e70f3e21638f Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:00:37 +0000 Subject: [PATCH 08/17] Fix return value from `Base.push!` --- src/write_once_read_many.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index cf2ff604..dc801d84 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -30,5 +30,5 @@ end Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) Base.iterate(w::WriteOnceReadMany, i) = iterate(w._raw_data, i) -Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...) +Base.push!(w::WriteOnceReadMany, val...) = (push!(w._raw_data, val...); w) From 1fc82900444bb2f26c117f43b606815b9312276f Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:02:57 +0000 Subject: [PATCH 09/17] Fix return value from `Base.setindex!` --- src/write_once_read_many.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index dc801d84..d2f1c29c 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -25,6 +25,7 @@ Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b) function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol) haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])") setindex!(w._raw_data, i, s) + return w end Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data) From 8f2cc240d1488589b65198f6e46b58b352fb1724 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:05:13 +0000 Subject: [PATCH 10/17] Move INDEX_TYPE back to symbolic_dimensions --- src/DynamicQuantities.jl | 1 - src/symbolic_dimensions.jl | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index bb5973d7..45bf3788 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -11,7 +11,6 @@ export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert -const INDEX_TYPE = UInt16 include("internal_utils.jl") include("fixed_rational.jl") diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index d59c1ff1..ef46ee78 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -1,10 +1,11 @@ import ..WriteOnceReadMany -import ..INDEX_TYPE import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES + disambiguate_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s +const INDEX_TYPE = UInt16 # 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. From 9b1cba432fbca6658352948721081eabee6a7089 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:09:34 +0000 Subject: [PATCH 11/17] Add docstring for `@register_unit` --- src/register_units.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/register_units.jl b/src/register_units.jl index bbd9e7c2..225facac 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -15,9 +15,14 @@ function update_all_values(name_symbol, unit) end end -# Register -macro register_unit(name, value) - return esc(_register_unit(name, value)) +""" + @register_unit symbol value + +Register a new unit under the given symbol to have +a particular value. +""" +macro register_unit(symbol, value) + return esc(_register_unit(symbol, value)) end function _register_unit(name::Symbol, value) From abbcdec2412aa969c28041a716d6b41da6532520 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:16:38 +0000 Subject: [PATCH 12/17] Improve @register_unit docstring and add to docs --- docs/src/units.md | 8 ++++++++ src/register_units.jl | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/src/units.md b/docs/src/units.md index 098bdbc5..218b94dd 100644 --- a/docs/src/units.md +++ b/docs/src/units.md @@ -42,3 +42,11 @@ Units.T Units.L Units.bar ``` + +## Custom Units + +You can define custom units with the `@register_unit` macro: + +```@docs +@register_unit +``` diff --git a/src/register_units.jl b/src/register_units.jl index 225facac..83d326d9 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -20,6 +20,30 @@ end Register a new unit under the given symbol to have a particular value. + +# Example + +```julia +julia> @register_unit MyVolt 1.5u"V" +``` + +This will register a new unit `MyVolt` with a value of `1.5u"V"`. +You can then use this unit in your calculations: + +```julia +julia> x = 20us"MyVolt^2" +20.0 MyVolt² + +julia> y = 2.5us"A" +2.5 A + +julia> x * y^2 |> uconvert(us"W^2") +281.25 W² + +julia> x * y^2 |> uconvert(us"W^2") |> sqrt |> uexpand +16.77050983124842 m² kg s⁻³ +``` + """ macro register_unit(symbol, value) return esc(_register_unit(symbol, value)) From ac1889aac1424ffdcb2ba4787d997483636c3d80 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:19:48 +0000 Subject: [PATCH 13/17] Type stability --- src/register_units.jl | 20 +++++++++++--------- src/write_once_read_many.jl | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/register_units.jl b/src/register_units.jl index 83d326d9..80955fcd 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -52,19 +52,21 @@ end function _register_unit(name::Symbol, value) name_symbol = Meta.quot(name) index = get(ALL_MAPPING, name, INDEX_TYPE(0)) - if iszero(index) - reg_expr = _lazy_register_unit(name, value) - push!(reg_expr.args, quote - $update_all_values($name_symbol, $value) - nothing - end) - return reg_expr - else + if !iszero(index) unit = ALL_VALUES[index] # When a utility function to expand `value` to its final form becomes # available, enable the following check. This will avoid throwing an error # if user is trying to register an existing unit with matching values. # unit.value != value && throw("Unit $name is already defined as $unit") - throw("Unit `$name` is already defined as `$unit`") + error("Unit `$name` is already defined as `$unit`") end + reg_expr = _lazy_register_unit(name, value) + push!( + reg_expr.args, + quote + $update_all_values($name_symbol, $value) + nothing + end + ) + return reg_expr end diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index d2f1c29c..d4b54074 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -23,7 +23,7 @@ Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b) # Only define setindex! for Dicts, and throw an error if the key already exists function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol) - haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])") + haskey(w._raw_data, s) && error("Unit $s already exists at index $(w[s])") setindex!(w._raw_data, i, s) return w end From d6f5cc11c91f6d7da957495850f807a01c8afc97 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:28:46 +0000 Subject: [PATCH 14/17] Clean up diff --- src/DynamicQuantities.jl | 6 +++--- src/symbolic_dimensions.jl | 4 ++-- src/units.jl | 15 +++++++-------- test/unittests.jl | 4 +--- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 45bf3788..b9ff34b1 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -9,7 +9,7 @@ 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 +export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert, @register_unit include("internal_utils.jl") @@ -24,11 +24,11 @@ include("constants.jl") include("uparse.jl") include("symbolic_dimensions.jl") include("complex.jl") -include("disambiguities.jl") include("register_units.jl") +include("disambiguities.jl") + include("deprecated.jl") export expand_units -export @register_unit import PackageExtensionCompat: @require_extensions import .Units diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index ef46ee78..3b4ccf6f 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -367,8 +367,8 @@ module SymbolicUnits import ..UNIT_SYMBOLS import ..CONSTANT_SYMBOLS import ..SymbolicDimensionsSingleton - import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE import ..constructorof + import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE @@ -426,7 +426,7 @@ module SymbolicUnits push!(SYMBOLIC_UNIT_VALUES, unit) end -""" + """ sym_uparse(raw_string::AbstractString) Parse a string containing an expression of units and return the diff --git a/src/units.jl b/src/units.jl index 3cdba464..226ace39 100644 --- a/src/units.jl +++ b/src/units.jl @@ -28,7 +28,6 @@ function _lazy_register_unit(name::Symbol, value) end end - function _add_prefixes(base_unit::Symbol, prefixes, register_function) all_prefixes = ( f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1, @@ -44,13 +43,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function) end # SI base units -@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length = 1) -@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass = 1) -@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time = 1) -@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current = 1) -@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature = 1) -@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity = 1) -@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount = 1) +@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1) +@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1) +@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1) +@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1) +@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1) +@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1) +@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1) @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G) @add_prefixes g (p, n, μ, u, m, k) diff --git a/test/unittests.jl b/test/unittests.jl index 150e6705..eb4affd3 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,7 +1,6 @@ using DynamicQuantities using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions -using DynamicQuantities: - DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +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 using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value @@ -1731,7 +1730,6 @@ end ) 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} From 81321a0f343d9da907ed135a338f5f24b11323dc Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:38:45 +0000 Subject: [PATCH 15/17] Improve docstring of WriteOnceReadMany --- src/write_once_read_many.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index d4b54074..12e956df 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -1,7 +1,12 @@ """ - WriteOnceReadMany() + WriteOnceReadMany{V}(container::V) -Used for storing units, values, symbolic-units. +A wrapper type for container that only defines methods +for appending to and reading to, but not modifying the container. + +This is so that we can safely define a `@register_unit` interface +without needing to worry about the user overwriting previously +defined units and voiding the indexing of symbolic dimensions. """ struct WriteOnceReadMany{V} _raw_data::V From a1ba7e7d5faf3994bf38af58113ac6298f43a7a4 Mon Sep 17 00:00:00 2001 From: MilesCranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:40:41 +0000 Subject: [PATCH 16/17] Stylistic tweaks --- src/write_once_read_many.jl | 4 ++-- test/precompile_test/ExternalUnitRegistration.jl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl index 12e956df..e97f190d 100644 --- a/src/write_once_read_many.jl +++ b/src/write_once_read_many.jl @@ -24,10 +24,10 @@ for f in (:findfirst, :filter) end Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i) -Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b) +Base.get(w::WriteOnceReadMany{<:AbstractDict}, a, b) = get(w._raw_data, a, b) # Only define setindex! for Dicts, and throw an error if the key already exists -function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol) +function Base.setindex!(w::WriteOnceReadMany{<:AbstractDict}, i, s::Symbol) haskey(w._raw_data, s) && error("Unit $s already exists at index $(w[s])") setindex!(w._raw_data, i, s) return w diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl index 48314590..c9c0e6c4 100644 --- a/test/precompile_test/ExternalUnitRegistration.jl +++ b/test/precompile_test/ExternalUnitRegistration.jl @@ -1,13 +1,13 @@ module ExternalUnitRegistration -using DynamicQuantities: @register_unit, @u_str, @us_str, - ALL_MAPPING, ALL_SYMBOLS, DEFAULT_QUANTITY_TYPE, - DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING +using DynamicQuantities: @register_unit, @u_str, @us_str +using DynamicQuantities: ALL_MAPPING, ALL_SYMBOLS, DEFAULT_QUANTITY_TYPE +using DynamicQuantities: DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING using Test @register_unit Wb u"m^2*kg*s^-2*A^-1" -@testset " Register Unit Inside a Module" begin +@testset "Register Unit Inside a Module" begin for collection in (UNIT_SYMBOLS, ALL_SYMBOLS, keys(ALL_MAPPING._raw_data), keys(UNIT_MAPPING._raw_data)) @test :Wb ∈ collection end From 92df2f428564fb3823f30a4390c1792b0effdb4a Mon Sep 17 00:00:00 2001 From: Miles Cranmer <miles.cranmer@gmail.com> Date: Mon, 12 Feb 2024 00:48:51 +0000 Subject: [PATCH 17/17] Only test error string on 1.9+ --- test/unittests.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index eb4affd3..11b12bf8 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1860,7 +1860,12 @@ all_map_count_before_registering = length(ALL_MAPPING) @register_unit MySV us"V" @register_unit MySV2 us"km/h" -@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s")) +if VERSION >= v"1.9" + @test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s")) + + # Constants as well: + @test_throws "Unit `Ryd` is already defined" esc(_register_unit(:Ryd, u"Constants.Ryd")) +end @testset "Register Unit" begin @test MyV === u"V"