Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow External Unit Registration #107

Merged
merged 17 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/src/units.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
8 changes: 5 additions & 3 deletions src/DynamicQuantities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ 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")
include("fixed_rational.jl")
include("write_once_read_many.jl")
include("types.jl")
include("utils.jl")
include("math.jl")
Expand All @@ -22,6 +24,7 @@ include("constants.jl")
include("uparse.jl")
include("symbolic_dimensions.jl")
include("complex.jl")
include("register_units.jl")
include("disambiguities.jl")

include("deprecated.jl")
Expand All @@ -38,12 +41,11 @@ 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


function __init__()
@require_extensions
end
Expand Down
72 changes: 72 additions & 0 deletions src/register_units.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
import .SymbolicUnits: update_external_symbolic_unit_value

# Update the unit collections
const UNIT_UPDATE_LOCK = Threads.SpinLock()

MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
function update_all_values(name_symbol, unit)
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
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_external_symbolic_unit_value(name_symbol)
end
end

"""
@register_unit symbol value

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))
end

function _register_unit(name::Symbol, value)
name_symbol = Meta.quot(name)
index = get(ALL_MAPPING, name, INDEX_TYPE(0))
if !iszero(index)
unit = ALL_VALUES[index]
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
# 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")
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
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
61 changes: 35 additions & 26 deletions src/symbolic_dimensions.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import ..WriteOnceReadMany
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_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s

disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s

const INDEX_TYPE = UInt8
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 = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_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)))

"""
AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
Expand Down Expand Up @@ -169,7 +165,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."
Expand Down Expand Up @@ -224,7 +220,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)
Expand Down Expand Up @@ -371,29 +367,30 @@ 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 ..constructorof
import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE
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
import ...CONSTANT_SYMBOLS
import ...SymbolicDimensionsSingleton
import ...constructorof
import ...disambiguate_symbol
import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE
import ....DEFAULT_VALUE_TYPE
import ....DEFAULT_DIM_BASE_TYPE
import ...disambiguate_constant_symbol
import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
import ...DEFAULT_VALUE_TYPE
import ...DEFAULT_DIM_BASE_TYPE

const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]

for unit in CONSTANT_SYMBOLS
@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
Expand All @@ -404,18 +401,30 @@ 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 = WriteOnceReadMany{Vector{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)))
)
push!(_SYMBOLIC_UNIT_VALUES, $unit)
push!($symbolic_unit_values, $unit)
end
end
const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES)

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)
Expand Down
89 changes: 44 additions & 45 deletions src/units.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
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_SYMBOLS = WriteOnceReadMany{Vector{Symbol}}()
const UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_QUANTITY_TYPE}}()

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
const $name = $value
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
push!(_UNIT_SYMBOLS, Symbol($s))
push!(_UNIT_VALUES, $name)
push!($UNIT_SYMBOLS, $name_symbol)
push!($UNIT_VALUES, $name)
end
end

Expand All @@ -42,13 +43,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)
Expand Down Expand Up @@ -88,17 +89,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 ()
Expand Down Expand Up @@ -156,17 +157,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 ()
Expand All @@ -178,7 +179,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)

Expand All @@ -188,7 +189,7 @@ end
)

## Pressure
@register_unit bar 100 * kPa
@_lazy_register_unit bar 100 * kPa

@add_prefixes bar (m,)

Expand All @@ -203,9 +204,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.
const UNIT_MAPPING = WriteOnceReadMany(Dict(s => i for (i, s) in enumerate(UNIT_SYMBOLS)))

end
Loading
Loading