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 2 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
4 changes: 2 additions & 2 deletions src/DynamicQuantities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,7 +44,6 @@ let _units_import_expr = :(using .Units: m, g)
eval(_units_import_expr)
end


function __init__()
@require_extensions
end
Expand Down
35 changes: 35 additions & 0 deletions src/register_units.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

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
push!(ALL_SYMBOLS, name_symbol)
push!(ALL_VALUES, unit)
ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1)
end
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

# 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)
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
$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

44 changes: 22 additions & 22 deletions src/symbolic_dimensions.jl
Original file line number Diff line number Diff line change
@@ -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))))
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

"""
AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -371,21 +370,21 @@ 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
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 ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
import ...DEFAULT_VALUE_TYPE
import ...DEFAULT_DIM_BASE_TYPE

const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]

Expand All @@ -404,18 +403,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)))
)
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)
Expand Down
91 changes: 46 additions & 45 deletions src/units.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}()
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

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


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,
Expand All @@ -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)
Expand Down Expand Up @@ -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 ()
Expand Down Expand Up @@ -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 ()
Expand All @@ -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)

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

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

@add_prefixes bar (m,)

Expand All @@ -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)))
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

end
36 changes: 34 additions & 2 deletions test/unittests.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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"
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
@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
Loading