Skip to content

Commit 889d020

Browse files
authored
Merge pull request #107 from ven-k/vkb/fix_register_units
Allow External Unit Registration
2 parents 9681769 + 92df2f4 commit 889d020

8 files changed

+274
-75
lines changed

docs/src/units.md

+8
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,11 @@ Units.T
4242
Units.L
4343
Units.bar
4444
```
45+
46+
## Custom Units
47+
48+
You can define custom units with the `@register_unit` macro:
49+
50+
```@docs
51+
@register_unit
52+
```

src/DynamicQuantities.jl

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ export QuantityArray
99
export DimensionError
1010
export ustrip, dimension
1111
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
12-
export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert
12+
export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert, @register_unit
13+
1314

1415
include("internal_utils.jl")
1516
include("fixed_rational.jl")
17+
include("write_once_read_many.jl")
1618
include("types.jl")
1719
include("utils.jl")
1820
include("math.jl")
@@ -22,6 +24,7 @@ include("constants.jl")
2224
include("uparse.jl")
2325
include("symbolic_dimensions.jl")
2426
include("complex.jl")
27+
include("register_units.jl")
2528
include("disambiguities.jl")
2629

2730
include("deprecated.jl")
@@ -38,12 +41,11 @@ using .Units: UNIT_SYMBOLS
3841
let _units_import_expr = :(using .Units: m, g)
3942
append!(
4043
_units_import_expr.args[1].args,
41-
map(s -> Expr(:(.), s), filter(s -> s (:m, :g), UNIT_SYMBOLS))
44+
Expr(:(.), s) for s in UNIT_SYMBOLS if s (:m, :g)
4245
)
4346
eval(_units_import_expr)
4447
end
4548

46-
4749
function __init__()
4850
@require_extensions
4951
end

src/register_units.jl

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
2+
import .SymbolicUnits: update_external_symbolic_unit_value
3+
4+
# Update the unit collections
5+
const UNIT_UPDATE_LOCK = Threads.SpinLock()
6+
7+
function update_all_values(name_symbol, unit)
8+
lock(UNIT_UPDATE_LOCK) do
9+
push!(ALL_SYMBOLS, name_symbol)
10+
push!(ALL_VALUES, unit)
11+
i = lastindex(ALL_VALUES)
12+
ALL_MAPPING[name_symbol] = i
13+
UNIT_MAPPING[name_symbol] = i
14+
update_external_symbolic_unit_value(name_symbol)
15+
end
16+
end
17+
18+
"""
19+
@register_unit symbol value
20+
21+
Register a new unit under the given symbol to have
22+
a particular value.
23+
24+
# Example
25+
26+
```julia
27+
julia> @register_unit MyVolt 1.5u"V"
28+
```
29+
30+
This will register a new unit `MyVolt` with a value of `1.5u"V"`.
31+
You can then use this unit in your calculations:
32+
33+
```julia
34+
julia> x = 20us"MyVolt^2"
35+
20.0 MyVolt²
36+
37+
julia> y = 2.5us"A"
38+
2.5 A
39+
40+
julia> x * y^2 |> uconvert(us"W^2")
41+
281.25 W²
42+
43+
julia> x * y^2 |> uconvert(us"W^2") |> sqrt |> uexpand
44+
16.77050983124842 m² kg s⁻³
45+
```
46+
47+
"""
48+
macro register_unit(symbol, value)
49+
return esc(_register_unit(symbol, value))
50+
end
51+
52+
function _register_unit(name::Symbol, value)
53+
name_symbol = Meta.quot(name)
54+
index = get(ALL_MAPPING, name, INDEX_TYPE(0))
55+
if !iszero(index)
56+
unit = ALL_VALUES[index]
57+
# When a utility function to expand `value` to its final form becomes
58+
# available, enable the following check. This will avoid throwing an error
59+
# if user is trying to register an existing unit with matching values.
60+
# unit.value != value && throw("Unit $name is already defined as $unit")
61+
error("Unit `$name` is already defined as `$unit`")
62+
end
63+
reg_expr = _lazy_register_unit(name, value)
64+
push!(
65+
reg_expr.args,
66+
quote
67+
$update_all_values($name_symbol, $value)
68+
nothing
69+
end
70+
)
71+
return reg_expr
72+
end

src/symbolic_dimensions.jl

+35-26
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1+
import ..WriteOnceReadMany
12
import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
23
import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
34

45

5-
const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)
6+
disambiguate_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s
67

7-
disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s
8-
9-
const INDEX_TYPE = UInt8
8+
const INDEX_TYPE = UInt16
109
# Prefer units over constants:
1110
# For example, this means we can't have a symbolic Planck's constant,
1211
# as it is just "hours" (h), which is more common.
13-
const ALL_SYMBOLS = (
14-
UNIT_SYMBOLS...,
15-
disambiguate_symbol.(CONSTANT_SYMBOLS)...
16-
)
17-
const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...)
18-
const ALL_MAPPING = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS)))
12+
const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_constant_symbol.(CONSTANT_SYMBOLS)...])
13+
const ALL_VALUES = WriteOnceReadMany([UNIT_VALUES..., CONSTANT_VALUES...])
14+
const ALL_MAPPING = WriteOnceReadMany(Dict(s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)))
1915

2016
"""
2117
AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
@@ -169,7 +165,7 @@ uexpand(q::QuantityArray) = uexpand.(q)
169165
uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
170166
171167
Convert a quantity `q` with base SI units to the symbolic units of `qout`, for `q` and `qout` with compatible units.
172-
Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`.
168+
Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`.
173169
"""
174170
function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
175171
@assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert."
@@ -224,7 +220,7 @@ end
224220
"""
225221
uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions})
226222
227-
Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e
223+
Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e
228224
a function equivalent to `q -> uconvert(qout, q)`.
229225
"""
230226
uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout)
@@ -371,29 +367,30 @@ module SymbolicUnits
371367
import ..UNIT_SYMBOLS
372368
import ..CONSTANT_SYMBOLS
373369
import ..SymbolicDimensionsSingleton
374-
import ...constructorof
375-
import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
376-
import ...DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
377-
import ...DEFAULT_VALUE_TYPE
378-
import ...DEFAULT_DIM_BASE_TYPE
370+
import ..constructorof
371+
import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE
372+
import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
373+
import ..DEFAULT_VALUE_TYPE
374+
import ..DEFAULT_DIM_BASE_TYPE
375+
import ..WriteOnceReadMany
379376

380377
# Lazily create unit symbols (since there are so many)
381378
module Constants
382379
import ...CONSTANT_SYMBOLS
383380
import ...SymbolicDimensionsSingleton
384381
import ...constructorof
385-
import ...disambiguate_symbol
386-
import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE
387-
import ....DEFAULT_VALUE_TYPE
388-
import ....DEFAULT_DIM_BASE_TYPE
382+
import ...disambiguate_constant_symbol
383+
import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
384+
import ...DEFAULT_VALUE_TYPE
385+
import ...DEFAULT_DIM_BASE_TYPE
389386

390387
const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
391388

392389
for unit in CONSTANT_SYMBOLS
393390
@eval begin
394391
const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
395392
DEFAULT_VALUE_TYPE(1.0),
396-
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
393+
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_constant_symbol(unit))))
397394
)
398395
push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
399396
end
@@ -404,18 +401,30 @@ module SymbolicUnits
404401
import .Constants as SymbolicConstants
405402
import .Constants: SYMBOLIC_CONSTANT_VALUES
406403

407-
const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
408-
for unit in UNIT_SYMBOLS
404+
const SYMBOLIC_UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_SYMBOLIC_QUANTITY_TYPE}}()
405+
406+
function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES)
409407
@eval begin
410408
const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
411409
DEFAULT_VALUE_TYPE(1.0),
412410
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit)))
413411
)
414-
push!(_SYMBOLIC_UNIT_VALUES, $unit)
412+
push!($symbolic_unit_values, $unit)
415413
end
416414
end
417-
const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES)
418415

416+
update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data)
417+
update_symbolic_unit_values!(UNIT_SYMBOLS)
418+
419+
# Non-eval version of `update_symbolic_unit_values!` for registering units in
420+
# an external module.
421+
function update_external_symbolic_unit_value(unit)
422+
unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
423+
DEFAULT_VALUE_TYPE(1.0),
424+
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(unit)
425+
)
426+
push!(SYMBOLIC_UNIT_VALUES, unit)
427+
end
419428

420429
"""
421430
sym_uparse(raw_string::AbstractString)

src/units.jl

+44-45
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
module Units
22

3+
import ..WriteOnceReadMany
34
import ..DEFAULT_DIM_TYPE
45
import ..DEFAULT_VALUE_TYPE
56
import ..DEFAULT_QUANTITY_TYPE
67

78
@assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type."
89

9-
const _UNIT_SYMBOLS = Symbol[]
10-
const _UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
10+
const UNIT_SYMBOLS = WriteOnceReadMany{Vector{Symbol}}()
11+
const UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_QUANTITY_TYPE}}()
1112

12-
macro register_unit(name, value)
13-
return esc(_register_unit(name, value))
13+
macro _lazy_register_unit(name, value)
14+
return esc(_lazy_register_unit(name, value))
1415
end
1516

1617
macro add_prefixes(base_unit, prefixes)
1718
@assert prefixes.head == :tuple
18-
return esc(_add_prefixes(base_unit, prefixes.args, _register_unit))
19+
return esc(_add_prefixes(base_unit, prefixes.args, _lazy_register_unit))
1920
end
2021

21-
function _register_unit(name::Symbol, value)
22-
s = string(name)
23-
return quote
22+
function _lazy_register_unit(name::Symbol, value)
23+
name_symbol = Meta.quot(name)
24+
quote
2425
const $name = $value
25-
push!(_UNIT_SYMBOLS, Symbol($s))
26-
push!(_UNIT_VALUES, $name)
26+
push!($UNIT_SYMBOLS, $name_symbol)
27+
push!($UNIT_VALUES, $name)
2728
end
2829
end
2930

@@ -42,13 +43,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function)
4243
end
4344

4445
# SI base units
45-
@register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1)
46-
@register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1)
47-
@register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1)
48-
@register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1)
49-
@register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1)
50-
@register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1)
51-
@register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1)
46+
@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1)
47+
@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1)
48+
@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1)
49+
@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1)
50+
@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1)
51+
@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1)
52+
@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1)
5253

5354
@add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G)
5455
@add_prefixes g (p, n, μ, u, m, k)
@@ -88,17 +89,17 @@ end
8889
)
8990

9091
# SI derived units
91-
@register_unit Hz inv(s)
92-
@register_unit N kg * m / s^2
93-
@register_unit Pa N / m^2
94-
@register_unit J N * m
95-
@register_unit W J / s
96-
@register_unit C A * s
97-
@register_unit V W / A
98-
@register_unit F C / V
99-
@register_unit Ω V / A
100-
@register_unit ohm Ω
101-
@register_unit T N / (A * m)
92+
@_lazy_register_unit Hz inv(s)
93+
@_lazy_register_unit N kg * m / s^2
94+
@_lazy_register_unit Pa N / m^2
95+
@_lazy_register_unit J N * m
96+
@_lazy_register_unit W J / s
97+
@_lazy_register_unit C A * s
98+
@_lazy_register_unit V W / A
99+
@_lazy_register_unit F C / V
100+
@_lazy_register_unit Ω V / A
101+
@_lazy_register_unit ohm Ω
102+
@_lazy_register_unit T N / (A * m)
102103

103104
@add_prefixes Hz (n, μ, u, m, k, M, G)
104105
@add_prefixes N ()
@@ -156,17 +157,17 @@ end
156157

157158
# Common assorted units
158159
## Time
159-
@register_unit min 60 * s
160-
@register_unit minute min
161-
@register_unit h 60 * min
162-
@register_unit hr h
163-
@register_unit day 24 * h
164-
@register_unit d day
165-
@register_unit wk 7 * day
166-
@register_unit yr 365.25 * day
167-
@register_unit inch 2.54 * cm
168-
@register_unit ft 12 * inch
169-
@register_unit mi 5280 * ft
160+
@_lazy_register_unit min 60 * s
161+
@_lazy_register_unit minute min
162+
@_lazy_register_unit h 60 * min
163+
@_lazy_register_unit hr h
164+
@_lazy_register_unit day 24 * h
165+
@_lazy_register_unit d day
166+
@_lazy_register_unit wk 7 * day
167+
@_lazy_register_unit yr 365.25 * day
168+
@_lazy_register_unit inch 2.54 * cm
169+
@_lazy_register_unit ft 12 * inch
170+
@_lazy_register_unit mi 5280 * ft
170171

171172
@add_prefixes min ()
172173
@add_prefixes minute ()
@@ -178,7 +179,7 @@ end
178179
@add_prefixes yr (k, M, G)
179180

180181
## Volume
181-
@register_unit L dm^3
182+
@_lazy_register_unit L dm^3
182183

183184
@add_prefixes L (μ, u, m, c, d)
184185

@@ -188,7 +189,7 @@ end
188189
)
189190

190191
## Pressure
191-
@register_unit bar 100 * kPa
192+
@_lazy_register_unit bar 100 * kPa
192193

193194
@add_prefixes bar (m,)
194195

@@ -203,9 +204,7 @@ end
203204
# Do not wish to define physical constants, as the number of symbols might lead to ambiguity.
204205
# The user should define these instead.
205206

206-
"""A tuple of all possible unit symbols."""
207-
const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS)
208-
const UNIT_VALUES = Tuple(_UNIT_VALUES)
209-
const UNIT_MAPPING = NamedTuple([s => i for (i, s) in enumerate(UNIT_SYMBOLS)])
207+
# Update `UNIT_MAPPING` with all internally defined unit symbols.
208+
const UNIT_MAPPING = WriteOnceReadMany(Dict(s => i for (i, s) in enumerate(UNIT_SYMBOLS)))
210209

211210
end

0 commit comments

Comments
 (0)