From a02042bb97d435281c50a0fe3430c519a1da5702 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 02:52:23 -0400 Subject: [PATCH 01/27] Create physical constants submodule --- src/DynamicQuantities.jl | 5 ++- src/constants.jl | 89 ++++++++++++++++++++++++++++++++++++++++ src/units.jl | 5 ++- 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/constants.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 0fe164fc..ca754f42 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,5 +1,6 @@ module DynamicQuantities +export Units, Const export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str @@ -8,10 +9,10 @@ include("fixed_rational.jl") include("types.jl") include("utils.jl") include("math.jl") -include("units.jl") +include("units.jl") # < include("constants.jl") import Requires: @init, @require -import .Units: uparse, @u_str +import .Units: uparse, @u_str, Const if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/constants.jl b/src/constants.jl new file mode 100644 index 00000000..4e24d02d --- /dev/null +++ b/src/constants.jl @@ -0,0 +1,89 @@ +module Const + +import ..@u_str +import ..@add_prefixes + +# Source: http://physics.nist.gov/constants (2018) + +# Exact, base: +"Speed of light in a vacuum. Standard." +const c = 299792458u"m/s" +"Planck constant. Standard." +const h = 6.62607015e−34u"J/Hz" +"Reduced Planck constant (h/2π). Standard." +const hbar = h / (2π) +"Elementary charge. Standard." +const e = 1.602176634e−19u"C" +"Boltzmann constant. Standard." +const k_B = 1.380649e−23u"J/K" +"Avogadro constant. Standard." +const N_A = 6.02214076e+23u"mol^-1" + +# Exact, derived: +"Electron volt. Standard." +const eV = e * u"J/C" +@add_prefixes eV (m, k, M, G, T) +"Molar gas constant. Standard." +const R = N_A * k_B +"Faraday constant. Standard." +const F = N_A * e +"Stefan-Boltzmann constant. Standard." +const sigma_sb = (π^2/60) * k_B^4/(hbar^3 * c^2) + +# Measured +"Fine-structure constant. Measured." +const alpha = 7.2973525693e−3 +"Atomic mass unit (1/12th the mass of Carbon-12). Measured." +const u = 1.66053906660e-27u"kg" +"Newtonian constant of gravitation. Measured." +const G = 6.67430e-11u"m^3/(kg*s^2)" +"Vacuum magnetic permeability. Measured." +const mu_0 = 4π * alpha * hbar / (e^2 * c) +"Vacuum electric permittivity. Measured." +const eps_0 = 8.8541878128e-12u"F/m" +"Electron mass. Measured." +const m_e = 9.1093837015e−31u"kg" +"Proton mass. Measured." +const m_p = 1.67262192369e−27u"kg" +"Neutron mass. Measured." +const m_n = 1.67492749804e-27u"kg" +"Bohr radius. Measured." +const a_0 = hbar/(m_e * c * alpha) +"Coulomb constant (Note: SI units only!). Measured." +const k_e = 1/(4π * eps_0) +"Rydberg frequency. Measured." +const Ryd = alpha^2 * m_e * c^2 / (2 * h) + + +# Astro constants. +# Source: https://arxiv.org/abs/1510.07674 + +"Earth mass. Measured." +const M_earth = 5.97216787e+24u"kg" +"Solar mass. Measured." +const M_sun = 1.98840987e+30u"kg" +"Jupiter mass. Measured." +const M_jup = 1.8981246e+27u"kg" +"Nominal Earth equatorial radius. Standard." +const R_earth = 6.3781e+6u"m" +"Nominal Jupiter equatorial radius. Standard." +const R_jup = 7.1492e+7u"m" +"Nominal solar radius. Standard." +const R_sun = 6.957e+8u"m" +"Nominal solar luminosity. Standard." +const L_sun = 3.828e+26u"W" +"Standard luminosity at absolute bolometric magnitude 0. Standard." +const L_bol0 = 3.0128e+28u"W" +"Thomson scattering cross-section. Measured." +const sigma_T = 6.6524587321e-29u"m^2" +"Astronomical unit. Standard." +const au = 149597870700u"m" +"Parsec. Standard." +const pc = (648000/π) * au +@add_prefixes pc (k, M, G) +"Light year. Standard." +const ly = c * u"yr" +"Standard atmosphere. Standard." +const atm = 101325u"Pa" + +end diff --git a/src/units.jl b/src/units.jl index bbba5ffa..400014a8 100644 --- a/src/units.jl +++ b/src/units.jl @@ -17,7 +17,7 @@ end function _add_prefixes(base_unit::Symbol, prefixes) 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, - k=1e3, M=1e6, G=1e9 + k=1e3, M=1e6, G=1e9, T=1e12 ) expr = Expr(:block) for (prefix, value) in zip(keys(all_prefixes), values(all_prefixes)) @@ -145,4 +145,7 @@ macro u_str(s) return esc(uparse(s)) end +include("constants.jl") +import .Const + end From 3fccd674e2b305c0c8b5628c08ee9537edb99d41 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 03:28:19 -0400 Subject: [PATCH 02/27] Fix incorrect minus character --- src/constants.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/constants.jl b/src/constants.jl index 4e24d02d..1a1da7cb 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -9,13 +9,13 @@ import ..@add_prefixes "Speed of light in a vacuum. Standard." const c = 299792458u"m/s" "Planck constant. Standard." -const h = 6.62607015e−34u"J/Hz" +const h = 6.62607015e-34u"J/Hz" "Reduced Planck constant (h/2π). Standard." const hbar = h / (2π) "Elementary charge. Standard." -const e = 1.602176634e−19u"C" +const e = 1.602176634e-19u"C" "Boltzmann constant. Standard." -const k_B = 1.380649e−23u"J/K" +const k_B = 1.380649e-23u"J/K" "Avogadro constant. Standard." const N_A = 6.02214076e+23u"mol^-1" @@ -32,7 +32,7 @@ const sigma_sb = (π^2/60) * k_B^4/(hbar^3 * c^2) # Measured "Fine-structure constant. Measured." -const alpha = 7.2973525693e−3 +const alpha = 7.2973525693e-3 "Atomic mass unit (1/12th the mass of Carbon-12). Measured." const u = 1.66053906660e-27u"kg" "Newtonian constant of gravitation. Measured." @@ -42,9 +42,9 @@ const mu_0 = 4π * alpha * hbar / (e^2 * c) "Vacuum electric permittivity. Measured." const eps_0 = 8.8541878128e-12u"F/m" "Electron mass. Measured." -const m_e = 9.1093837015e−31u"kg" +const m_e = 9.1093837015e-31u"kg" "Proton mass. Measured." -const m_p = 1.67262192369e−27u"kg" +const m_p = 1.67262192369e-27u"kg" "Neutron mass. Measured." const m_n = 1.67492749804e-27u"kg" "Bohr radius. Measured." From 5391b09b9a87a88f41bdc974bafbbf446b7a3618 Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Fri, 16 Jun 2023 07:50:41 -0400 Subject: [PATCH 03/27] Apply suggestions from code review Co-authored-by: Oscar Dowson --- src/DynamicQuantities.jl | 4 ++-- src/constants.jl | 2 +- src/units.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index ca754f42..beda67a0 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,6 +1,6 @@ module DynamicQuantities -export Units, Const +export Units, Constants export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str @@ -12,7 +12,7 @@ include("math.jl") include("units.jl") # < include("constants.jl") import Requires: @init, @require -import .Units: uparse, @u_str, Const +import .Units: uparse, @u_str, Constants if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/constants.jl b/src/constants.jl index 1a1da7cb..be427220 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,4 +1,4 @@ -module Const +module Constants import ..@u_str import ..@add_prefixes diff --git a/src/units.jl b/src/units.jl index 400014a8..3cd478b9 100644 --- a/src/units.jl +++ b/src/units.jl @@ -146,6 +146,6 @@ macro u_str(s) end include("constants.jl") -import .Const +import .Constants end From 34433664acb44492b55287d03f3de5a7f627a1d1 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 29 Jun 2023 13:41:01 -0400 Subject: [PATCH 04/27] Create UnitDimensions struct for pretty-printing --- src/units.jl | 134 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/src/units.jl b/src/units.jl index bbba5ffa..4e9a4504 100644 --- a/src/units.jl +++ b/src/units.jl @@ -5,13 +5,27 @@ export uparse, @u_str import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Quantity +import ..AbstractDimensions + +const _UNIT_SYMBOLS = Symbol[] @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." +macro register_unit(name, value) + return esc(_register_unit(name, value)) +end + macro add_prefixes(base_unit, prefixes) @assert prefixes.head == :tuple - expr = _add_prefixes(base_unit, prefixes.args) - return expr |> esc + return esc(_add_prefixes(base_unit, prefixes.args)) +end + +function _register_unit(name::Symbol, value) + s = string(name) + return quote + const $name = $value + push!(_UNIT_SYMBOLS, Symbol($s)) + end end function _add_prefixes(base_unit::Symbol, prefixes) @@ -23,26 +37,19 @@ function _add_prefixes(base_unit::Symbol, prefixes) for (prefix, value) in zip(keys(all_prefixes), values(all_prefixes)) prefix in prefixes || continue new_unit = Symbol(prefix, base_unit) - push!(expr.args, :(const $new_unit = $value * $base_unit)) + push!(expr.args, _register_unit(new_unit, :($value * $base_unit))) end return expr end # SI base units -"Length in meters. Available variants: `fm`, `pm`, `nm`, `μm` (/`um`), `cm`, `dm`, `mm`, `km`, `Mm`, `Gm`." -const m = Quantity(1.0, length=1) -"Mass in grams. Available variants: `μg` (/`ug`), `mg`, `kg`." -const g = Quantity(1e-3, mass=1) -"Time in seconds. Available variants: `fs`, `ps`, `ns`, `μs` (/`us`), `ms`, `min`, `h` (/`hr`), `day`, `yr`, `kyr`, `Myr`, `Gyr`." -const s = Quantity(1.0, time=1) -"Current in Amperes. Available variants: `nA`, `μA` (/`uA`), `mA`, `kA`." -const A = Quantity(1.0, current=1) -"Temperature in Kelvin. Available variant: `mK`." -const K = Quantity(1.0, temperature=1) -"Luminosity in candela. Available variant: `mcd`." -const cd = Quantity(1.0, luminosity=1) -"Amount in moles. Available variant: `mmol`." -const mol = Quantity(1.0, amount=1) +@register_unit m Quantity(1.0, length=1) +@register_unit g Quantity(1e-3, mass=1) +@register_unit s Quantity(1.0, time=1) +@register_unit A Quantity(1.0, current=1) +@register_unit K Quantity(1.0, temperature=1) +@register_unit cd Quantity(1.0, luminosity=1) +@register_unit mol Quantity(1.0, amount=1) @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G) @add_prefixes g (μ, u, m, k) @@ -53,27 +60,17 @@ const mol = Quantity(1.0, amount=1) @add_prefixes mol (m,) # SI derived units -"Frequency in Hertz. Available variants: `kHz`, `MHz`, `GHz`." -const Hz = inv(s) -"Force in Newtons." -const N = kg * m / s^2 -"Pressure in Pascals. Available variant: `kPa`." -const Pa = N / m^2 -"Energy in Joules. Available variant: `kJ`." -const J = N * m -"Power in Watts. Available variants: `kW`, `MW`, `GW`." -const W = J / s -"Charge in Coulombs." -const C = A * s -"Voltage in Volts. Available variants: `kV`, `MV`, `GV`." -const V = W / A -"Capacitance in Farads." -const F = C / V -"Resistance in Ohms. Available variant: `mΩ`. Also available is ASCII `ohm` (with variant `mohm`)." -const Ω = V / A -const ohm = Ω -"Magnetic flux density in Teslas." -const T = N / (A * m) +@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) @add_prefixes Hz (k, M, G) @add_prefixes N () @@ -89,11 +86,11 @@ const T = N / (A * m) # Common assorted units ## Time -const min = 60 * s -const h = 60 * min -const hr = h -const day = 24 * h -const yr = 365.25 * day +@register_unit min 60 * s +@register_unit h 60 * min +@register_unit hr h +@register_unit day 24 * h +@register_unit yr 365.25 * day @add_prefixes min () @add_prefixes h () @@ -102,14 +99,12 @@ const yr = 365.25 * day @add_prefixes yr (k, M, G) ## Volume -"Volume in liters. Available variants: `mL`, `dL`." -const L = dm^3 +@register_unit L dm^3 @add_prefixes L (m, d) ## Pressure -"Pressure in bars." -const bar = 100 * kPa +@register_unit bar 100 * kPa @add_prefixes bar () @@ -145,4 +140,49 @@ macro u_str(s) return esc(uparse(s)) end +""" + UnitSymbols + +A separate module where each unit is treated as a separate dimension, +to enable pretty-printing of units. +""" +module UnitSymbols + + import ...Quantity + import ...AbstractDimensions + import ...DEFAULT_VALUE_TYPE + import ...DEFAULT_DIM_BASE_TYPE + + import .._UNIT_SYMBOLS + + """A tuple of all possible unit symbols.""" + const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS) + + function _create_unit_dimensions() + struct_def = :(struct UnitDimensions{R} <: AbstractDimensions{R}; end) + fields = struct_def.args[3].args + for unit in UNIT_SYMBOLS + push!(fields, :($(unit)::R)) + end + return struct_def + end + + # Create an AbstractDimensions containing all symbols: + @eval $(_create_unit_dimensions()) + + # Create all unit symbols + for unit in UNIT_SYMBOLS + @eval const $unit = Quantity(1.0, UnitDimensions, $(unit)=1) + end + + function uparse(s::AbstractString) + return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} + end + + as_quantity(q::Quantity) = q + as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), UnitDimensions{DEFAULT_DIM_BASE_TYPE}) + as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") + +end + end From 5b96c1a7cd407d06937057bba66dca922c49286b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 29 Jun 2023 14:09:45 -0400 Subject: [PATCH 05/27] Get conversion working --- src/units.jl | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/units.jl b/src/units.jl index 4e9a4504..9faf747c 100644 --- a/src/units.jl +++ b/src/units.jl @@ -5,6 +5,9 @@ export uparse, @u_str import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Quantity +import ..Dimensions +import ..ustrip +import ..dimension import ..AbstractDimensions const _UNIT_SYMBOLS = Symbol[] @@ -140,6 +143,10 @@ macro u_str(s) return esc(uparse(s)) end +"""A tuple of all possible unit symbols.""" +const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS) +const UNIT_VALUES = Tuple([eval(s) for s in UNIT_SYMBOLS]) + """ UnitSymbols @@ -153,22 +160,19 @@ module UnitSymbols import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE - import .._UNIT_SYMBOLS - - """A tuple of all possible unit symbols.""" - const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS) + import ..UNIT_SYMBOLS - function _create_unit_dimensions() - struct_def = :(struct UnitDimensions{R} <: AbstractDimensions{R}; end) + macro create_unit_dimensions(struct_name) + struct_def = :(struct $(struct_name){R} <: AbstractDimensions{R}; end) fields = struct_def.args[3].args - for unit in UNIT_SYMBOLS - push!(fields, :($(unit)::R)) + for symb in UNIT_SYMBOLS + push!(fields, :($(symb)::R)) end return struct_def end - # Create an AbstractDimensions containing all symbols: - @eval $(_create_unit_dimensions()) + # An AbstractDimensions containing all symbols: + @create_unit_dimensions UnitDimensions # Create all unit symbols for unit in UNIT_SYMBOLS @@ -185,4 +189,15 @@ module UnitSymbols end +function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitSymbols.UnitDimensions}) where {T,D,Q<:Quantity{T,D}} + result = one(Q) * ustrip(q) + d = dimension(q) + for (unit_symb, unit_val) in zip(UNIT_SYMBOLS, UNIT_VALUES) + dim = getproperty(d, unit_symb) + dim == 0 && continue + result = result * unit_val ^ dim + end + return result +end + end From 5e62cb6eb17f479202ad43e1b96fbd1410029eec Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 29 Jun 2023 17:04:16 -0400 Subject: [PATCH 06/27] Lazily generate unit symbol quantities --- src/units.jl | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/units.jl b/src/units.jl index 9faf747c..082fc765 100644 --- a/src/units.jl +++ b/src/units.jl @@ -10,10 +10,12 @@ import ..ustrip import ..dimension import ..AbstractDimensions -const _UNIT_SYMBOLS = Symbol[] @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." +const _UNIT_SYMBOLS = Symbol[] +const _UNIT_VALUES = Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE}[] + macro register_unit(name, value) return esc(_register_unit(name, value)) end @@ -28,6 +30,7 @@ function _register_unit(name::Symbol, value) return quote const $name = $value push!(_UNIT_SYMBOLS, Symbol($s)) + push!(_UNIT_VALUES, $name) end end @@ -145,7 +148,7 @@ end """A tuple of all possible unit symbols.""" const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS) -const UNIT_VALUES = Tuple([eval(s) for s in UNIT_SYMBOLS]) +const UNIT_VALUES = Tuple(_UNIT_VALUES) """ UnitSymbols @@ -168,19 +171,29 @@ module UnitSymbols for symb in UNIT_SYMBOLS push!(fields, :($(symb)::R)) end - return struct_def + return struct_def |> esc end - # An AbstractDimensions containing all symbols: @create_unit_dimensions UnitDimensions - # Create all unit symbols - for unit in UNIT_SYMBOLS - @eval const $unit = Quantity(1.0, UnitDimensions, $(unit)=1) + function generate_unit_symbols() + for unit in UNIT_SYMBOLS + @eval const $unit = Quantity(1.0, UnitDimensions; $(unit)=1) + end + return nothing end - function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} + const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) + const UNIT_SYMBOLS_LOCK = Threads.SpinLock() + + function uparse(raw_string::AbstractString) + UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do + UNIT_SYMBOLS_EXIST[] && return nothing + generate_unit_symbols() + UNIT_SYMBOLS_EXIST[] = true + end + raw_result = eval(Meta.parse(raw_string)) + return as_quantity(raw_result)::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} end as_quantity(q::Quantity) = q From 827b78b2aabf3ff46c5175cfac0df47d5d307cd1 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 30 Jun 2023 18:21:49 -0400 Subject: [PATCH 07/27] Refactor `UnitDimensions` to use a sparse vector for storage --- Project.toml | 1 + src/units.jl | 109 ++++++++++++++++++++++++++++++++++----------------- src/utils.jl | 2 + 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/Project.toml b/Project.toml index d5d9a3e3..d86ddd63 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.5.0" [deps] Requires = "ae029012-a4dd-5104-9daa-d747884805df" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775" [weakdeps] diff --git a/src/units.jl b/src/units.jl index 082fc765..1460f4fe 100644 --- a/src/units.jl +++ b/src/units.jl @@ -2,6 +2,9 @@ module Units export uparse, @u_str +import SparseArrays as SA +import Tricks: static_fieldnames + import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Quantity @@ -9,6 +12,9 @@ import ..Dimensions import ..ustrip import ..dimension import ..AbstractDimensions +import .._pow +import ..constructor_of +import ..tryrationalize @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." @@ -149,6 +155,58 @@ end """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)]) + + +""" + UnitDimensions{R} <: AbstractDimensions{R} + +An `AbstractDimensions` with one dimension for every unit symbol. +This is to allow for lazily reducing to SI base units, whereas +`Dimensions` is always in SI base units. Furthermore, `UnitDimensions` +stores dimensions using a sparse vector for efficiency (since there +are so many unit symbols). +""" +struct UnitDimensions{R} <: AbstractDimensions{R} + _data::SA.SparseVector{R} + + UnitDimensions(data::SA.SparseVector) = new{eltype(data)}(data) + UnitDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data) +end + +data(d::UnitDimensions) = getfield(d, :_data) +constructor_of(::Type{<:UnitDimensions}) = UnitDimensions + +UnitDimensions{R}(d::UnitDimensions) where {R} = UnitDimensions{R}(data(d)) +(::Type{D})(::Type{R}; kws...) where {R,D<:UnitDimensions} = + let constructor=constructor_of(D){R} + length(kws) == 0 && return constructor(SA.spzeros(R, length(UNIT_SYMBOLS))) + I = [UNIT_MAPPING[s] for s in keys(kws)] + V = [tryrationalize(R, v) for v in values(kws)] + data = SA.sparsevec(I, V, length(UNIT_SYMBOLS)) + return constructor(data) + end + +function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitDimensions}) where {T,D,Q<:Quantity{T,D}} + result = one(Q) * ustrip(q) + d = dimension(q) + for (idx, value) in zip(SA.findnz(data(d))...) + result = result * UNIT_VALUES[idx] ^ value + end + return result +end + +static_fieldnames(::Type{<:UnitDimensions}) = UNIT_SYMBOLS +Base.getproperty(d::UnitDimensions{R}, s::Symbol) where {R} = data(d)[UNIT_MAPPING[s]] +Base.getindex(d::UnitDimensions{R}, k::Symbol) where {R} = getproperty(d, k) +Base.copy(d::UnitDimensions) = UnitDimensions(copy(data(d))) +Base.:(==)(l::UnitDimensions, r::UnitDimensions) = data(l) == data(r) +Base.iszero(d::UnitDimensions) = iszero(data(d)) +Base.:*(l::UnitDimensions, r::UnitDimensions) = UnitDimensions(data(l) + data(r)) +Base.:/(l::UnitDimensions, r::UnitDimensions) = UnitDimensions(data(l) - data(r)) +Base.inv(d::UnitDimensions) = UnitDimensions(-data(d)) +_pow(l::UnitDimensions{R}, r::R) where {R} = UnitDimensions(data(l) * r) + """ UnitSymbols @@ -158,59 +216,36 @@ to enable pretty-printing of units. """ module UnitSymbols + import ..UNIT_SYMBOLS + import ..UnitDimensions + import ...Quantity - import ...AbstractDimensions import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE - import ..UNIT_SYMBOLS - - macro create_unit_dimensions(struct_name) - struct_def = :(struct $(struct_name){R} <: AbstractDimensions{R}; end) - fields = struct_def.args[3].args - for symb in UNIT_SYMBOLS - push!(fields, :($(symb)::R)) - end - return struct_def |> esc - end - - @create_unit_dimensions UnitDimensions - - function generate_unit_symbols() - for unit in UNIT_SYMBOLS - @eval const $unit = Quantity(1.0, UnitDimensions; $(unit)=1) - end - return nothing - end - + # Lazily create unit symbols (since there are so many) const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) const UNIT_SYMBOLS_LOCK = Threads.SpinLock() - - function uparse(raw_string::AbstractString) + function _generate_unit_symbols() UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do UNIT_SYMBOLS_EXIST[] && return nothing - generate_unit_symbols() + for unit in UNIT_SYMBOLS + @eval const $unit = Quantity(1.0, UnitDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + end UNIT_SYMBOLS_EXIST[] = true end + return nothing + end + + function uparse(raw_string::AbstractString) + _generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) - return as_quantity(raw_result)::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} + return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} end as_quantity(q::Quantity) = q as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), UnitDimensions{DEFAULT_DIM_BASE_TYPE}) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") - -end - -function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitSymbols.UnitDimensions}) where {T,D,Q<:Quantity{T,D}} - result = one(Q) * ustrip(q) - d = dimension(q) - for (unit_symb, unit_val) in zip(UNIT_SYMBOLS, UNIT_VALUES) - dim = getproperty(d, unit_symb) - dim == 0 && continue - result = result * unit_val ^ dim - end - return result end end diff --git a/src/utils.jl b/src/utils.jl index 0e89e8f4..d779f140 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -115,6 +115,8 @@ Base.convert(::Type{Q}, q::AbstractQuantity) where {T,D,Q<:AbstractQuantity{T,D} Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) +Base.copy(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, copy(ustrip(q)), copy(dimension(q))) + """ ustrip(q::AbstractQuantity) From 8b81ed4a1d0d8fb7bda4bfd9ba7e1d027b1725e2 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 30 Jun 2023 18:57:04 -0400 Subject: [PATCH 08/27] Export `sym_uparse`, `@us_str`, and `expand_units` --- src/DynamicQuantities.jl | 4 ++-- src/units.jl | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index a636b171..cf35e411 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -3,7 +3,7 @@ module DynamicQuantities export AbstractQuantity, AbstractDimensions export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount -export uparse, @u_str +export uparse, @u_str, sym_uparse, @us_str, expand_units include("fixed_rational.jl") include("types.jl") @@ -12,7 +12,7 @@ include("math.jl") include("units.jl") import Requires: @init, @require -import .Units: uparse, @u_str +import .Units: uparse, @u_str, sym_uparse, @us_str, expand_units if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/units.jl b/src/units.jl index 1460f4fe..55a34fdb 100644 --- a/src/units.jl +++ b/src/units.jl @@ -187,7 +187,7 @@ UnitDimensions{R}(d::UnitDimensions) where {R} = UnitDimensions{R}(data(d)) return constructor(data) end -function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitDimensions}) where {T,D,Q<:Quantity{T,D}} +function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}} result = one(Q) * ustrip(q) d = dimension(q) for (idx, value) in zip(SA.findnz(data(d))...) @@ -195,6 +195,10 @@ function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitDimensions}) where {T,D end return result end +function expand_units(q::Q) where {T,R,D<:UnitDimensions{R},Q<:Quantity{T,D}} + return convert(Quantity{T,Dimensions{R}}, q) +end + static_fieldnames(::Type{<:UnitDimensions}) = UNIT_SYMBOLS Base.getproperty(d::UnitDimensions{R}, s::Symbol) where {R} = data(d)[UNIT_MAPPING[s]] @@ -237,7 +241,7 @@ module UnitSymbols return nothing end - function uparse(raw_string::AbstractString) + function sym_uparse(raw_string::AbstractString) _generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} @@ -248,4 +252,10 @@ module UnitSymbols as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") end +import .UnitSymbols: sym_uparse + +macro us_str(s) + return esc(UnitSymbols.sym_uparse(s)) +end + end From 094b6d0d417c147e9f983c4cf822852f284427d3 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 8 Jul 2023 23:05:46 -0400 Subject: [PATCH 09/27] Restructure constants --- src/DynamicQuantities.jl | 7 ++++-- src/constants.jl | 51 ++++++++++++++++++++-------------------- src/units.jl | 31 ------------------------ test/unittests.jl | 8 +++++++ 4 files changed, 38 insertions(+), 59 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 311ebe4c..59a042f7 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -10,10 +10,13 @@ include("fixed_rational.jl") include("types.jl") include("utils.jl") include("math.jl") -include("units.jl") # < include("constants.jl") +include("units.jl") +include("constants.jl") +include("uparse.jl") import Requires: @init, @require -import .Units: uparse, @u_str, Constants +import .Constants: Constants +import .UnitsParse: uparse, @u_str if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/constants.jl b/src/constants.jl index be427220..ebf7193f 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,27 +1,26 @@ module Constants -import ..@u_str -import ..@add_prefixes +import ..Units as U # Source: http://physics.nist.gov/constants (2018) # Exact, base: "Speed of light in a vacuum. Standard." -const c = 299792458u"m/s" +const c = 299792458 * U.m/U.s "Planck constant. Standard." -const h = 6.62607015e-34u"J/Hz" +const h = 6.62607015e-34 * U.J/U.Hz "Reduced Planck constant (h/2π). Standard." const hbar = h / (2π) "Elementary charge. Standard." -const e = 1.602176634e-19u"C" +const e = 1.602176634e-19 * U.C "Boltzmann constant. Standard." -const k_B = 1.380649e-23u"J/K" +const k_B = 1.380649e-23 * U.J/U.K "Avogadro constant. Standard." -const N_A = 6.02214076e+23u"mol^-1" +const N_A = 6.02214076e+23 / U.mol # Exact, derived: "Electron volt. Standard." -const eV = e * u"J/C" +const eV = e * U.J/U.C @add_prefixes eV (m, k, M, G, T) "Molar gas constant. Standard." const R = N_A * k_B @@ -34,19 +33,19 @@ const sigma_sb = (π^2/60) * k_B^4/(hbar^3 * c^2) "Fine-structure constant. Measured." const alpha = 7.2973525693e-3 "Atomic mass unit (1/12th the mass of Carbon-12). Measured." -const u = 1.66053906660e-27u"kg" +const u = 1.66053906660e-27 * U.kg "Newtonian constant of gravitation. Measured." -const G = 6.67430e-11u"m^3/(kg*s^2)" +const G = 6.67430e-11 * U.m^3 / (U.kg * U.s^2) "Vacuum magnetic permeability. Measured." const mu_0 = 4π * alpha * hbar / (e^2 * c) "Vacuum electric permittivity. Measured." -const eps_0 = 8.8541878128e-12u"F/m" +const eps_0 = 8.8541878128e-12 * U.F/U.m "Electron mass. Measured." -const m_e = 9.1093837015e-31u"kg" +const m_e = 9.1093837015e-31 * U.kg "Proton mass. Measured." -const m_p = 1.67262192369e-27u"kg" +const m_p = 1.67262192369e-27 * U.kg "Neutron mass. Measured." -const m_n = 1.67492749804e-27u"kg" +const m_n = 1.67492749804e-27 * U.kg "Bohr radius. Measured." const a_0 = hbar/(m_e * c * alpha) "Coulomb constant (Note: SI units only!). Measured." @@ -59,31 +58,31 @@ const Ryd = alpha^2 * m_e * c^2 / (2 * h) # Source: https://arxiv.org/abs/1510.07674 "Earth mass. Measured." -const M_earth = 5.97216787e+24u"kg" +const M_earth = 5.97216787e+24 * U.kg "Solar mass. Measured." -const M_sun = 1.98840987e+30u"kg" +const M_sun = 1.98840987e+30 * U.kg "Jupiter mass. Measured." -const M_jup = 1.8981246e+27u"kg" +const M_jup = 1.8981246e+27 * U.kg "Nominal Earth equatorial radius. Standard." -const R_earth = 6.3781e+6u"m" +const R_earth = 6.3781e+6 * U.m "Nominal Jupiter equatorial radius. Standard." -const R_jup = 7.1492e+7u"m" +const R_jup = 7.1492e+7 * U.m "Nominal solar radius. Standard." -const R_sun = 6.957e+8u"m" +const R_sun = 6.957e+8 * U.m "Nominal solar luminosity. Standard." -const L_sun = 3.828e+26u"W" +const L_sun = 3.828e+26 * U.W "Standard luminosity at absolute bolometric magnitude 0. Standard." -const L_bol0 = 3.0128e+28u"W" +const L_bol0 = 3.0128e+28 * U.W "Thomson scattering cross-section. Measured." -const sigma_T = 6.6524587321e-29u"m^2" +const sigma_T = 6.6524587321e-29 * U.m^2 "Astronomical unit. Standard." -const au = 149597870700u"m" +const au = 149597870700 * U.m^2 "Parsec. Standard." const pc = (648000/π) * au @add_prefixes pc (k, M, G) "Light year. Standard." -const ly = c * u"yr" +const ly = c * U.yr "Standard atmosphere. Standard." -const atm = 101325u"Pa" +const atm = 101325 * U.Pa end diff --git a/src/units.jl b/src/units.jl index 3cd478b9..6dfdad61 100644 --- a/src/units.jl +++ b/src/units.jl @@ -1,7 +1,5 @@ module Units -export uparse, @u_str - import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Quantity @@ -119,33 +117,4 @@ const bar = 100 * kPa # Do not wish to define physical constants, as the number of symbols might lead to ambiguity. # The user should define these instead. -""" - uparse(s::AbstractString) - -Parse a string containing an expression of units and return the -corresponding `Quantity` object with `Float64` value. For example, -`uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. -""" -function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} -end - -as_quantity(q::Quantity) = q -as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) -as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") - -""" - u"[unit expression]" - -Parse a string containing an expression of units and return the -corresponding `Quantity` object with `Float64` value. For example, -`u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. -""" -macro u_str(s) - return esc(uparse(s)) -end - -include("constants.jl") -import .Constants - end diff --git a/test/unittests.jl b/test/unittests.jl index cea8a5e5..e0cd258d 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -346,6 +346,14 @@ end @test_throws LoadError eval(:(u":x")) end +@testset "Constants" begin + @test Constants.h * Constants.c / (1000.0u"nm") ≈ 1.9864458571489284e-19u"J" + + # Compute period of Earth based on solar mass and semi-major axis: + a = u"Constants.au" + @test isapprox(sqrt(4π^2 * a^3 / (Constants.G * Constants.M_sun)), 1u"yr"; rtol=1e-3) +end + @testset "Additional tests of FixedRational" begin @test convert(Int64, FixedRational{Int64,1000}(2 // 1)) == 2 @test convert(Int32, FixedRational{Int64,1000}(3 // 1)) == 3 From 2254e16e68c6dcf86b89e6e3bd895df9ae9742e4 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 8 Jul 2023 23:56:21 -0400 Subject: [PATCH 10/27] Refactor constants with `@register_constant` --- src/DynamicQuantities.jl | 5 +- src/constants.jl | 133 +++++++++++++++++++-------------------- src/symbolic_quantity.jl | 102 ++++++++++++++++++++++++++++++ src/types.jl | 2 + src/units.jl | 132 ++++---------------------------------- src/uparse.jl | 46 ++++++++++++++ 6 files changed, 230 insertions(+), 190 deletions(-) create mode 100644 src/symbolic_quantity.jl create mode 100644 src/uparse.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index d61d04c4..c9109741 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -13,11 +13,12 @@ include("math.jl") include("units.jl") include("constants.jl") include("uparse.jl") +include("symbolic_quantity.jl") import Requires: @init, @require -import .Constants: Constants +import .Units +import .Constants import .UnitsParse: uparse, @u_str -import .Units: uparse, @u_str, sym_uparse, @us_str, expand_units if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/constants.jl b/src/constants.jl index ebf7193f..07de5c91 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,88 +1,85 @@ module Constants +import ..DEFAULT_QUANTITY_TYPE +import ..Quantity import ..Units as U +import ..Units: _add_prefixes + +const _CONSTANT_SYMBOLS = Symbol[] +const _CONSTANT_VALUES = DEFAULT_QUANTITY_TYPE[] + +macro register_constant(name, value) + return esc(_register_constant(name, value)) +end + +macro add_prefixes(base_unit, prefixes) + @assert prefixes.head == :tuple + return esc(_add_prefixes(base_unit, prefixes.args, _register_constant)) +end + +function _register_constant(name::Symbol, value) + s = string(name) + return quote + const $name = $value + push!(_CONSTANT_SYMBOLS, Symbol($s)) + push!(_CONSTANT_VALUES, $name) + end +end # Source: http://physics.nist.gov/constants (2018) # Exact, base: -"Speed of light in a vacuum. Standard." -const c = 299792458 * U.m/U.s -"Planck constant. Standard." -const h = 6.62607015e-34 * U.J/U.Hz -"Reduced Planck constant (h/2π). Standard." -const hbar = h / (2π) -"Elementary charge. Standard." -const e = 1.602176634e-19 * U.C -"Boltzmann constant. Standard." -const k_B = 1.380649e-23 * U.J/U.K -"Avogadro constant. Standard." -const N_A = 6.02214076e+23 / U.mol +@register_constant c 299792458 * U.m/U.s +@register_constant h 6.62607015e-34 * U.J/U.Hz +@register_constant hbar h / (2π) +@register_constant e 1.602176634e-19 * U.C +@register_constant k_B 1.380649e-23 * U.J/U.K +@register_constant N_A 6.02214076e+23 / U.mol # Exact, derived: -"Electron volt. Standard." -const eV = e * U.J/U.C +@register_constant eV e * U.J/U.C +@register_constant R N_A * k_B +@register_constant F N_A * e +@register_constant sigma_sb (π^2/60) * k_B^4/(hbar^3 * c^2) + @add_prefixes eV (m, k, M, G, T) -"Molar gas constant. Standard." -const R = N_A * k_B -"Faraday constant. Standard." -const F = N_A * e -"Stefan-Boltzmann constant. Standard." -const sigma_sb = (π^2/60) * k_B^4/(hbar^3 * c^2) # Measured -"Fine-structure constant. Measured." -const alpha = 7.2973525693e-3 -"Atomic mass unit (1/12th the mass of Carbon-12). Measured." -const u = 1.66053906660e-27 * U.kg -"Newtonian constant of gravitation. Measured." -const G = 6.67430e-11 * U.m^3 / (U.kg * U.s^2) -"Vacuum magnetic permeability. Measured." -const mu_0 = 4π * alpha * hbar / (e^2 * c) -"Vacuum electric permittivity. Measured." -const eps_0 = 8.8541878128e-12 * U.F/U.m -"Electron mass. Measured." -const m_e = 9.1093837015e-31 * U.kg -"Proton mass. Measured." -const m_p = 1.67262192369e-27 * U.kg -"Neutron mass. Measured." -const m_n = 1.67492749804e-27 * U.kg -"Bohr radius. Measured." -const a_0 = hbar/(m_e * c * alpha) -"Coulomb constant (Note: SI units only!). Measured." -const k_e = 1/(4π * eps_0) -"Rydberg frequency. Measured." -const Ryd = alpha^2 * m_e * c^2 / (2 * h) +@register_constant alpha DEFAULT_QUANTITY_TYPE(7.2973525693e-3) +@register_constant u 1.66053906660e-27 * U.kg +@register_constant G 6.67430e-11 * U.m^3 / (U.kg * U.s^2) +@register_constant mu_0 4π * alpha * hbar / (e^2 * c) +@register_constant eps_0 8.8541878128e-12 * U.F/U.m +@register_constant m_e 9.1093837015e-31 * U.kg +@register_constant m_p 1.67262192369e-27 * U.kg +@register_constant m_n 1.67492749804e-27 * U.kg +@register_constant a_0 hbar/(m_e * c * alpha) +@register_constant k_e 1/(4π * eps_0) +@register_constant Ryd alpha^2 * m_e * c^2 / (2 * h) # Astro constants. # Source: https://arxiv.org/abs/1510.07674 -"Earth mass. Measured." -const M_earth = 5.97216787e+24 * U.kg -"Solar mass. Measured." -const M_sun = 1.98840987e+30 * U.kg -"Jupiter mass. Measured." -const M_jup = 1.8981246e+27 * U.kg -"Nominal Earth equatorial radius. Standard." -const R_earth = 6.3781e+6 * U.m -"Nominal Jupiter equatorial radius. Standard." -const R_jup = 7.1492e+7 * U.m -"Nominal solar radius. Standard." -const R_sun = 6.957e+8 * U.m -"Nominal solar luminosity. Standard." -const L_sun = 3.828e+26 * U.W -"Standard luminosity at absolute bolometric magnitude 0. Standard." -const L_bol0 = 3.0128e+28 * U.W -"Thomson scattering cross-section. Measured." -const sigma_T = 6.6524587321e-29 * U.m^2 -"Astronomical unit. Standard." -const au = 149597870700 * U.m^2 -"Parsec. Standard." -const pc = (648000/π) * au +@register_constant M_earth 5.97216787e+24 * U.kg +@register_constant M_sun 1.98840987e+30 * U.kg +@register_constant M_jup 1.8981246e+27 * U.kg +@register_constant R_earth 6.3781e+6 * U.m +@register_constant R_jup 7.1492e+7 * U.m +@register_constant R_sun 6.957e+8 * U.m +@register_constant L_sun 3.828e+26 * U.W +@register_constant L_bol0 3.0128e+28 * U.W +@register_constant sigma_T 6.6524587321e-29 * U.m^2 +@register_constant au 149597870700 * U.m^2 +@register_constant pc (648000/π) * au +@register_constant ly c * U.yr +@register_constant atm 101325 * U.Pa + @add_prefixes pc (k, M, G) -"Light year. Standard." -const ly = c * U.yr -"Standard atmosphere. Standard." -const atm = 101325 * U.Pa + +"""A tuple of all possible constants.""" +const CONSTANT_SYMBOLS = Tuple(_CONSTANT_SYMBOLS) +const CONSTANT_VALUES = Tuple(_CONSTANT_VALUES) +const CONSTANT_MAPPING = NamedTuple([s => i for (i, s) in enumerate(CONSTANT_SYMBOLS)]) end diff --git a/src/symbolic_quantity.jl b/src/symbolic_quantity.jl new file mode 100644 index 00000000..1cee5549 --- /dev/null +++ b/src/symbolic_quantity.jl @@ -0,0 +1,102 @@ +import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES +import SparseArrays as SA + +""" + SymbolicDimensions{R} <: AbstractDimensions{R} + +An `AbstractDimensions` with one dimension for every unit symbol. +This is to allow for lazily reducing to SI base units, whereas +`Dimensions` is always in SI base units. Furthermore, `SymbolicDimensions` +stores dimensions using a sparse vector for efficiency (since there +are so many unit symbols). +""" +struct SymbolicDimensions{R} <: AbstractDimensions{R} + _data::SA.SparseVector{R} + + SymbolicDimensions(data::SA.SparseVector) = new{eltype(data)}(data) + SymbolicDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data) +end + +data(d::SymbolicDimensions) = getfield(d, :_data) +constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions + +SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(data(d)) +(::Type{D})(::Type{R}; kws...) where {R,D<:SymbolicDimensions} = + let constructor=constructor_of(D){R} + length(kws) == 0 && return constructor(SA.spzeros(R, length(UNIT_SYMBOLS))) + I = [UNIT_MAPPING[s] for s in keys(kws)] + V = [tryrationalize(R, v) for v in values(kws)] + data = SA.sparsevec(I, V, length(UNIT_SYMBOLS)) + return constructor(data) + end + +function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}} + result = one(Q) * ustrip(q) + d = dimension(q) + for (idx, value) in zip(SA.findnz(data(d))...) + result = result * UNIT_VALUES[idx] ^ value + end + return result +end +function expand_units(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:Quantity{T,D}} + return convert(Quantity{T,Dimensions{R}}, q) +end + + +static_fieldnames(::Type{<:SymbolicDimensions}) = UNIT_SYMBOLS +Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[UNIT_MAPPING[s]] +Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k) +Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(data(d))) +Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) = data(l) == data(r) +Base.iszero(d::SymbolicDimensions) = iszero(data(d)) +Base.:*(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) + data(r)) +Base.:/(l::SymbolicDimensions, r::SymbolicDimensions) = SymbolicDimensions(data(l) - data(r)) +Base.inv(d::SymbolicDimensions) = SymbolicDimensions(-data(d)) +_pow(l::SymbolicDimensions{R}, r::R) where {R} = SymbolicDimensions(data(l) * r) + + +""" + SymbolicDimensionsModule + +A separate module where each unit is treated as a separate dimension, +to enable pretty-printing of units. +""" +module SymbolicDimensionsModule + + import ..UNIT_SYMBOLS + import ..SymbolicDimensions + + import ...Quantity + import ...DEFAULT_VALUE_TYPE + import ...DEFAULT_DIM_BASE_TYPE + + # Lazily create unit symbols (since there are so many) + const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) + const UNIT_SYMBOLS_LOCK = Threads.SpinLock() + function _generate_unit_symbols() + UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do + UNIT_SYMBOLS_EXIST[] && return nothing + for unit in UNIT_SYMBOLS + @eval const $unit = Quantity(1.0, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + end + UNIT_SYMBOLS_EXIST[] = true + end + return nothing + end + + function sym_uparse(raw_string::AbstractString) + _generate_unit_symbols() + raw_result = eval(Meta.parse(raw_string)) + return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}} + end + + as_quantity(q::Quantity) = q + as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) + as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") +end + +import .SymbolicDimensionsModule: sym_uparse + +macro us_str(s) + return esc(SymbolicDimensionsModule.sym_uparse(s)) +end diff --git a/src/types.jl b/src/types.jl index 78006836..7868d76c 100644 --- a/src/types.jl +++ b/src/types.jl @@ -121,6 +121,8 @@ end (::Type{Q})(q::AbstractQuantity) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = constructor_of(Q)(convert(T, ustrip(q)), convert(D, dimension(q))) (::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = constructor_of(Q)(convert(T, ustrip(q)), dimension(q)) +const DEFAULT_QUANTITY_TYPE = Quantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} + new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructor_of(D)(dims...) new_quantity(::Type{Q}, l, r) where {Q<:AbstractQuantity} = constructor_of(Q)(l, r) diff --git a/src/units.jl b/src/units.jl index fcda5b5b..85efc836 100644 --- a/src/units.jl +++ b/src/units.jl @@ -5,20 +5,13 @@ import Tricks: static_fieldnames import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE +import ..DEFAULT_QUANTITY_TYPE import ..Quantity -import ..Dimensions -import ..ustrip -import ..dimension -import ..AbstractDimensions -import .._pow -import ..constructor_of -import ..tryrationalize - @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." const _UNIT_SYMBOLS = Symbol[] -const _UNIT_VALUES = Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE}[] +const _UNIT_VALUES = DEFAULT_QUANTITY_TYPE[] macro register_unit(name, value) return esc(_register_unit(name, value)) @@ -26,7 +19,7 @@ end macro add_prefixes(base_unit, prefixes) @assert prefixes.head == :tuple - return esc(_add_prefixes(base_unit, prefixes.args)) + return esc(_add_prefixes(base_unit, prefixes.args, _register_unit)) end function _register_unit(name::Symbol, value) @@ -38,7 +31,7 @@ function _register_unit(name::Symbol, value) end end -function _add_prefixes(base_unit::Symbol, prefixes) +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, k=1e3, M=1e6, G=1e9, T=1e12 @@ -47,19 +40,19 @@ function _add_prefixes(base_unit::Symbol, prefixes) for (prefix, value) in zip(keys(all_prefixes), values(all_prefixes)) prefix in prefixes || continue new_unit = Symbol(prefix, base_unit) - push!(expr.args, _register_unit(new_unit, :($value * $base_unit))) + push!(expr.args, register_function(new_unit, :($value * $base_unit))) end return expr end # SI base units -@register_unit m Quantity(1.0, length=1) -@register_unit g Quantity(1e-3, mass=1) -@register_unit s Quantity(1.0, time=1) -@register_unit A Quantity(1.0, current=1) -@register_unit K Quantity(1.0, temperature=1) -@register_unit cd Quantity(1.0, luminosity=1) -@register_unit mol Quantity(1.0, amount=1) +@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) @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G) @add_prefixes g (μ, u, m, k) @@ -129,105 +122,4 @@ 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)]) - -""" - UnitDimensions{R} <: AbstractDimensions{R} - -An `AbstractDimensions` with one dimension for every unit symbol. -This is to allow for lazily reducing to SI base units, whereas -`Dimensions` is always in SI base units. Furthermore, `UnitDimensions` -stores dimensions using a sparse vector for efficiency (since there -are so many unit symbols). -""" -struct UnitDimensions{R} <: AbstractDimensions{R} - _data::SA.SparseVector{R} - - UnitDimensions(data::SA.SparseVector) = new{eltype(data)}(data) - UnitDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data) -end - -data(d::UnitDimensions) = getfield(d, :_data) -constructor_of(::Type{<:UnitDimensions}) = UnitDimensions - -UnitDimensions{R}(d::UnitDimensions) where {R} = UnitDimensions{R}(data(d)) -(::Type{D})(::Type{R}; kws...) where {R,D<:UnitDimensions} = - let constructor=constructor_of(D){R} - length(kws) == 0 && return constructor(SA.spzeros(R, length(UNIT_SYMBOLS))) - I = [UNIT_MAPPING[s] for s in keys(kws)] - V = [tryrationalize(R, v) for v in values(kws)] - data = SA.sparsevec(I, V, length(UNIT_SYMBOLS)) - return constructor(data) - end - -function Base.convert(::Type{Q}, q::Quantity{<:Any,<:UnitDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}} - result = one(Q) * ustrip(q) - d = dimension(q) - for (idx, value) in zip(SA.findnz(data(d))...) - result = result * UNIT_VALUES[idx] ^ value - end - return result -end -function expand_units(q::Q) where {T,R,D<:UnitDimensions{R},Q<:Quantity{T,D}} - return convert(Quantity{T,Dimensions{R}}, q) -end - - -static_fieldnames(::Type{<:UnitDimensions}) = UNIT_SYMBOLS -Base.getproperty(d::UnitDimensions{R}, s::Symbol) where {R} = data(d)[UNIT_MAPPING[s]] -Base.getindex(d::UnitDimensions{R}, k::Symbol) where {R} = getproperty(d, k) -Base.copy(d::UnitDimensions) = UnitDimensions(copy(data(d))) -Base.:(==)(l::UnitDimensions, r::UnitDimensions) = data(l) == data(r) -Base.iszero(d::UnitDimensions) = iszero(data(d)) -Base.:*(l::UnitDimensions, r::UnitDimensions) = UnitDimensions(data(l) + data(r)) -Base.:/(l::UnitDimensions, r::UnitDimensions) = UnitDimensions(data(l) - data(r)) -Base.inv(d::UnitDimensions) = UnitDimensions(-data(d)) -_pow(l::UnitDimensions{R}, r::R) where {R} = UnitDimensions(data(l) * r) - - -""" - UnitSymbols - -A separate module where each unit is treated as a separate dimension, -to enable pretty-printing of units. -""" -module UnitSymbols - - import ..UNIT_SYMBOLS - import ..UnitDimensions - - import ...Quantity - import ...DEFAULT_VALUE_TYPE - import ...DEFAULT_DIM_BASE_TYPE - - # Lazily create unit symbols (since there are so many) - const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) - const UNIT_SYMBOLS_LOCK = Threads.SpinLock() - function _generate_unit_symbols() - UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do - UNIT_SYMBOLS_EXIST[] && return nothing - for unit in UNIT_SYMBOLS - @eval const $unit = Quantity(1.0, UnitDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) - end - UNIT_SYMBOLS_EXIST[] = true - end - return nothing - end - - function sym_uparse(raw_string::AbstractString) - _generate_unit_symbols() - raw_result = eval(Meta.parse(raw_string)) - return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,UnitDimensions{DEFAULT_DIM_BASE_TYPE}} - end - - as_quantity(q::Quantity) = q - as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), UnitDimensions{DEFAULT_DIM_BASE_TYPE}) - as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") -end - -import .UnitSymbols: sym_uparse - -macro us_str(s) - return esc(UnitSymbols.sym_uparse(s)) -end - end diff --git a/src/uparse.jl b/src/uparse.jl new file mode 100644 index 00000000..0a29f887 --- /dev/null +++ b/src/uparse.jl @@ -0,0 +1,46 @@ +module UnitsParse + +import ..Quantity +import ..DEFAULT_DIM_TYPE +import ..DEFAULT_VALUE_TYPE +import ..Units: UNIT_SYMBOLS +import ..Constants + +function _generate_units_import() + import_expr = :(import ..Units: _) + deleteat!(first(import_expr.args).args, 2) + for symb in UNIT_SYMBOLS + push!(first(import_expr.args).args, Expr(:., symb)) + end + return import_expr +end + +eval(_generate_units_import()) + +""" + uparse(s::AbstractString) + +Parse a string containing an expression of units and return the +corresponding `Quantity` object with `Float64` value. For example, +`uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. +""" +function uparse(s::AbstractString) + return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} +end + +as_quantity(q::Quantity) = q +as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) +as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") + +""" + u"[unit expression]" + +Parse a string containing an expression of units and return the +corresponding `Quantity` object with `Float64` value. For example, +`u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. +""" +macro u_str(s) + return esc(uparse(s)) +end + +end \ No newline at end of file From 3a1de1b53aaa10575697e65fc0eb29c3bc706c4a Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 8 Jul 2023 23:59:15 -0400 Subject: [PATCH 11/27] Fix conversion --- src/symbolic_quantity.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/symbolic_quantity.jl b/src/symbolic_quantity.jl index 1cee5549..1358f67a 100644 --- a/src/symbolic_quantity.jl +++ b/src/symbolic_quantity.jl @@ -34,7 +34,7 @@ function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where result = one(Q) * ustrip(q) d = dimension(q) for (idx, value) in zip(SA.findnz(data(d))...) - result = result * UNIT_VALUES[idx] ^ value + result = result * convert(Q, UNIT_VALUES[idx]) ^ value end return result end From efb67f8079d3a8d566e7617e18ba6a32396856f7 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 00:00:40 -0400 Subject: [PATCH 12/27] Rename symbolic dimensions file --- src/DynamicQuantities.jl | 2 +- src/{symbolic_quantity.jl => symbolic_dimensions.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{symbolic_quantity.jl => symbolic_dimensions.jl} (100%) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index c9109741..bfd1b428 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -13,7 +13,7 @@ include("math.jl") include("units.jl") include("constants.jl") include("uparse.jl") -include("symbolic_quantity.jl") +include("symbolic_dimensions.jl") import Requires: @init, @require import .Units diff --git a/src/symbolic_quantity.jl b/src/symbolic_dimensions.jl similarity index 100% rename from src/symbolic_quantity.jl rename to src/symbolic_dimensions.jl From b3dce5671631dfa606f69401d4d9802c30c6ba8c Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 00:02:06 -0400 Subject: [PATCH 13/27] Fix definition of au --- src/constants.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.jl b/src/constants.jl index 07de5c91..21e9ce8c 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -70,7 +70,7 @@ end @register_constant L_sun 3.828e+26 * U.W @register_constant L_bol0 3.0128e+28 * U.W @register_constant sigma_T 6.6524587321e-29 * U.m^2 -@register_constant au 149597870700 * U.m^2 +@register_constant au 149597870700 * U.m @register_constant pc (648000/π) * au @register_constant ly c * U.yr @register_constant atm 101325 * U.Pa From 85b701e41a926a17bab582ecc2f4d3ceb2e974b2 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 00:49:02 -0400 Subject: [PATCH 14/27] Enable symbolic constants in SymbolicDimensions --- src/DynamicQuantities.jl | 3 +- src/symbolic_dimensions.jl | 62 +++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index bfd1b428..2e82cabb 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -2,7 +2,8 @@ module DynamicQuantities export Units, Constants export AbstractQuantity, AbstractDimensions -export Quantity, Dimensions, DimensionError, ustrip, dimension, valid +export Quantity, Dimensions, SymbolicDimensions, DimensionError +export ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, expand_units diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 1358f67a..23541c4d 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -1,6 +1,26 @@ import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES +import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES import SparseArrays as SA +const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS) + +# 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..., + setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS)... +) +const ALL_VALUES = vcat( + UNIT_VALUES..., + ( + v + for (k, v) in zip(CONSTANT_SYMBOLS, CONSTANT_VALUES) + if k ∉ SYMBOL_CONFLICTS + )... +) +const ALL_MAPPING = NamedTuple([s => i for (i, s) in enumerate(ALL_SYMBOLS)]) + """ SymbolicDimensions{R} <: AbstractDimensions{R} @@ -21,12 +41,13 @@ data(d::SymbolicDimensions) = getfield(d, :_data) constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(data(d)) +(::Type{D})(; kws...) where {D<:SymbolicDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...) (::Type{D})(::Type{R}; kws...) where {R,D<:SymbolicDimensions} = let constructor=constructor_of(D){R} - length(kws) == 0 && return constructor(SA.spzeros(R, length(UNIT_SYMBOLS))) - I = [UNIT_MAPPING[s] for s in keys(kws)] + length(kws) == 0 && return constructor(SA.spzeros(R, length(ALL_SYMBOLS))) + I = [ALL_MAPPING[s] for s in keys(kws)] V = [tryrationalize(R, v) for v in values(kws)] - data = SA.sparsevec(I, V, length(UNIT_SYMBOLS)) + data = SA.sparsevec(I, V, length(ALL_SYMBOLS)) return constructor(data) end @@ -34,7 +55,7 @@ function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where result = one(Q) * ustrip(q) d = dimension(q) for (idx, value) in zip(SA.findnz(data(d))...) - result = result * convert(Q, UNIT_VALUES[idx]) ^ value + result = result * convert(Q, ALL_VALUES[idx]) ^ value end return result end @@ -43,8 +64,8 @@ function expand_units(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:Quantity{T,D} end -static_fieldnames(::Type{<:SymbolicDimensions}) = UNIT_SYMBOLS -Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[UNIT_MAPPING[s]] +static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS +Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[ALL_MAPPING[s]] Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k) Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(data(d))) Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) = data(l) == data(r) @@ -64,6 +85,8 @@ to enable pretty-printing of units. module SymbolicDimensionsModule import ..UNIT_SYMBOLS + import ..CONSTANT_SYMBOLS + import ..SYMBOL_CONFLICTS import ..SymbolicDimensions import ...Quantity @@ -71,13 +94,37 @@ module SymbolicDimensionsModule import ...DEFAULT_DIM_BASE_TYPE # Lazily create unit symbols (since there are so many) + module Constants + import ..CONSTANT_SYMBOLS + import ..SYMBOL_CONFLICTS + import ..SymbolicDimensions + + import ..Quantity + import ..DEFAULT_VALUE_TYPE + import ..DEFAULT_DIM_BASE_TYPE + + const CONSTANT_SYMBOLS_EXIST = Ref{Bool}(false) + const CONSTANT_SYMBOLS_LOCK = Threads.SpinLock() + function _generate_unit_symbols() + CONSTANT_SYMBOLS_EXIST[] || lock(CONSTANT_SYMBOLS_LOCK) do + CONSTANT_SYMBOLS_EXIST[] && return nothing + for unit in setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS) + @eval const $unit = Quantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + end + CONSTANT_SYMBOLS_EXIST[] = true + end + return nothing + end + end + import .Constants + const UNIT_SYMBOLS_EXIST = Ref{Bool}(false) const UNIT_SYMBOLS_LOCK = Threads.SpinLock() function _generate_unit_symbols() UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do UNIT_SYMBOLS_EXIST[] && return nothing for unit in UNIT_SYMBOLS - @eval const $unit = Quantity(1.0, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + @eval const $unit = Quantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end UNIT_SYMBOLS_EXIST[] = true end @@ -86,6 +133,7 @@ module SymbolicDimensionsModule function sym_uparse(raw_string::AbstractString) _generate_unit_symbols() + Constants._generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}} end From 56d21fdba460154d56d2d7a884cf6cee18d3fd98 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 00:57:06 -0400 Subject: [PATCH 15/27] Add tests on symbolic dimensions --- test/unittests.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index e0cd258d..9a49d61e 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -414,3 +414,11 @@ end # But, we always need to use a quantity when mixing with mathematical operations: @test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions() end + +@testset "Symbolic dimensions" begin + q = 1.5us"km/s" + @test string(dimension(q)) == "s⁻¹ km" + @test expand_units(q) == 1.5u"km/s" + @test string(dimension(us"Constants.au^1.5")) == "au³ᐟ²" + @test expand_units(2.3us"Constants.au^1.5") ≈ 2.3u"Constants.au^1.5" +end From 7f2662c5a31c344f8c6ab92864a631f9b40d74c8 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 01:12:00 -0400 Subject: [PATCH 16/27] Additional unit tests for symbolic dimensions --- test/unittests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unittests.jl b/test/unittests.jl index 9a49d61e..95d284bb 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -420,5 +420,6 @@ end @test string(dimension(q)) == "s⁻¹ km" @test expand_units(q) == 1.5u"km/s" @test string(dimension(us"Constants.au^1.5")) == "au³ᐟ²" + @test string(dimension(expand_units(us"Constants.au^1.5"))) == "m³ᐟ²" @test expand_units(2.3us"Constants.au^1.5") ≈ 2.3u"Constants.au^1.5" end From 520f7e5555a9d0adafd38f694df6bcb97a329486 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 01:42:33 -0400 Subject: [PATCH 17/27] More symbolic dimensions tests --- src/symbolic_dimensions.jl | 8 ++++---- test/unittests.jl | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 23541c4d..5ead741d 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -77,12 +77,12 @@ _pow(l::SymbolicDimensions{R}, r::R) where {R} = SymbolicDimensions(data(l) * r) """ - SymbolicDimensionsModule + SymbolicUnitsParse A separate module where each unit is treated as a separate dimension, to enable pretty-printing of units. """ -module SymbolicDimensionsModule +module SymbolicUnitsParse import ..UNIT_SYMBOLS import ..CONSTANT_SYMBOLS @@ -143,8 +143,8 @@ module SymbolicDimensionsModule as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") end -import .SymbolicDimensionsModule: sym_uparse +import .SymbolicUnitsParse: sym_uparse macro us_str(s) - return esc(SymbolicDimensionsModule.sym_uparse(s)) + return esc(SymbolicUnitsParse.sym_uparse(s)) end diff --git a/test/unittests.jl b/test/unittests.jl index 95d284bb..033e1203 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -417,9 +417,16 @@ end @testset "Symbolic dimensions" begin q = 1.5us"km/s" + @test q == 1.5 * us"km" / us"s" + @test typeof(q) <: Quantity{Float64,<:SymbolicDimensions} @test string(dimension(q)) == "s⁻¹ km" @test expand_units(q) == 1.5u"km/s" @test string(dimension(us"Constants.au^1.5")) == "au³ᐟ²" @test string(dimension(expand_units(us"Constants.au^1.5"))) == "m³ᐟ²" @test expand_units(2.3us"Constants.au^1.5") ≈ 2.3u"Constants.au^1.5" + @test iszero(dimension(us"1.0")) == true + @test expand_units(inv(us"Constants.au")) ≈ 1/u"Constants.au" + @test dimension(inv(us"s") * us"km") == dimension(us"km/s") + @test dimension(inv(us"s") * us"m") != dimension(us"km/s") + @test dimension(expand_units(inv(us"s") * us"m")) == dimension(expand_units(us"km/s")) end From 455aefa6572c92a0fdf9b61d0b229b68c3ba8485 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 01:52:04 -0400 Subject: [PATCH 18/27] Add test for uparsing errors --- test/unittests.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index 033e1203..c07e883d 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -429,4 +429,6 @@ end @test dimension(inv(us"s") * us"km") == dimension(us"km/s") @test dimension(inv(us"s") * us"m") != dimension(us"km/s") @test dimension(expand_units(inv(us"s") * us"m")) == dimension(expand_units(us"km/s")) + + @test_throws ErrorException sym_uparse("'c'") end From c8342f6346e03206d6c74e1964eeacddd80ba92f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 02:02:48 -0400 Subject: [PATCH 19/27] Only measure coverage on version==1 --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f9238f68..edebb624 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -41,6 +41,7 @@ jobs: julia --color=yes --project=. coverage.jl - name: "Coveralls" uses: coverallsapp/github-action@v2 + if: matrix.version == '1' with: github-token: ${{ secrets.GITHUB_TOKEN }} file: lcov.info From da0085577ac80f72b86ce011dfe4234a424f3455 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 02:13:31 -0400 Subject: [PATCH 20/27] Add back docs for units --- src/units.jl | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/units.jl b/src/units.jl index 85efc836..228fb0c3 100644 --- a/src/units.jl +++ b/src/units.jl @@ -62,6 +62,35 @@ end @add_prefixes cd (m,) @add_prefixes mol (m,) +@doc( + "Length in meters. Available variants: `fm`, `pm`, `nm`, `μm` (/`um`), `cm`, `dm`, `mm`, `km`, `Mm`, `Gm`.", + m, +) +@doc( + "Mass in kilograms. Available variants: `μg` (/`ug`), `mg`, `g`.", + kg, +) +@doc( + "Time in seconds. Available variants: `fs`, `ps`, `ns`, `μs` (/`us`), `ms`, `min`, `h` (/`hr`), `day`, `yr`, `kyr`, `Myr`, `Gyr`.", + s, +) +@doc( + "Current in Amperes. Available variants: `nA`, `μA` (/`uA`), `mA`, `kA`.", + A, +) +@doc( + "Temperature in Kelvin. Available variant: `mK`.", + K, +) +@doc( + "Luminosity in candela. Available variant: `mcd`.", + cd, +) +@doc( + "Amount in moles. Available variant: `mmol`.", + mol, +) + # SI derived units @register_unit Hz inv(s) @register_unit N kg * m / s^2 @@ -87,6 +116,48 @@ end @add_prefixes ohm (m,) @add_prefixes T () +# SI derived units +@doc( + "Frequency in Hertz. Available variants: `kHz`, `MHz`, `GHz`.", + Hz, +) +@doc( + "Force in Newtons.", + N, +) +@doc( + "Pressure in Pascals. Available variant: `kPa`.", + Pa, +) +@doc( + "Energy in Joules. Available variant: `kJ`.", + J, +) +@doc( + "Power in Watts. Available variants: `kW`, `MW`, `GW`.", + W, +) +@doc( + "Charge in Coulombs.", + C, +) +@doc( + "Voltage in Volts. Available variants: `kV`, `MV`, `GV`.", + V, +) +@doc( + "Capacitance in Farads.", + F, +) +@doc( + "Resistance in Ohms. Available variant: `mΩ`. Also available is ASCII `ohm` (with variant `mohm`).", + Ω, +) +@doc( + "Magnetic flux density in Teslas.", + T, +) + # Common assorted units ## Time @register_unit min 60 * s @@ -106,11 +177,21 @@ end @add_prefixes L (m, d) +@doc( + "Volume in liters. Available variants: `mL`, `dL`.", + L, +) + ## Pressure @register_unit bar 100 * kPa @add_prefixes bar () +@doc( + "Pressure in bars.", + bar, +) + # Do not wish to define Gaussian units, as it changes # some formulas. Safer to force user to work exclusively in one unit system. From 2a7069777eea32536a986cbb117cfbd6bd655dca Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 02:21:57 -0400 Subject: [PATCH 21/27] Add docs for all constants --- src/constants.jl | 139 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/constants.jl b/src/constants.jl index 21e9ce8c..736a71b4 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -36,6 +36,31 @@ end @register_constant k_B 1.380649e-23 * U.J/U.K @register_constant N_A 6.02214076e+23 / U.mol +@doc( + "Speed of light in a vacuum. Standard.", + c, +) +@doc( + "Planck constant. Standard.", + h, +) +@doc( + "Reduced Planck constant (h/2π). Standard.", + hbar, +) +@doc( + "Elementary charge. Standard.", + e, +) +@doc( + "Boltzmann constant. Standard.", + k_B, +) +@doc( + "Avogadro constant. Standard.", + N_A, +) + # Exact, derived: @register_constant eV e * U.J/U.C @register_constant R N_A * k_B @@ -44,6 +69,23 @@ end @add_prefixes eV (m, k, M, G, T) +@doc( + "Electron volt. Standard.", + eV, +) +@doc( + "Molar gas constant. Standard.", + R, +) +@doc( + "Faraday constant. Standard.", + F, +) +@doc( + "Stefan-Boltzmann constant. Standard.", + sigma_sb, +) + # Measured @register_constant alpha DEFAULT_QUANTITY_TYPE(7.2973525693e-3) @register_constant u 1.66053906660e-27 * U.kg @@ -57,6 +99,50 @@ end @register_constant k_e 1/(4π * eps_0) @register_constant Ryd alpha^2 * m_e * c^2 / (2 * h) +@doc( + "Fine-structure constant. Measured.", + alpha, +) +@doc( + "Atomic mass unit (1/12th the mass of Carbon-12). Measured.", + u, +) +@doc( + "Newtonian constant of gravitation. Measured.", + G, +) +@doc( + "Vacuum magnetic permeability. Measured.", + mu_0, +) +@doc( + "Vacuum electric permittivity. Measured.", + eps_0, +) +@doc( + "Electron mass. Measured.", + m_e, +) +@doc( + "Proton mass. Measured.", + m_p, +) +@doc( + "Neutron mass. Measured.", + m_n, +) +@doc( + "Bohr radius. Measured.", + a_0, +) +@doc( + "Coulomb constant (Note: SI units only!). Measured.", + k_e, +) +@doc( + "Rydberg frequency. Measured.", + Ryd, +) # Astro constants. # Source: https://arxiv.org/abs/1510.07674 @@ -77,6 +163,59 @@ end @add_prefixes pc (k, M, G) +@doc( + "Earth mass. Measured.", + M_earth, +) +@doc( + "Solar mass. Measured.", + M_sun, +) +@doc( + "Jupiter mass. Measured.", + M_jup, +) +@doc( + "Nominal Earth equatorial radius. Standard.", + R_earth, +) +@doc( + "Nominal Jupiter equatorial radius. Standard.", + R_jup, +) +@doc( + "Nominal solar radius. Standard.", + R_sun, +) +@doc( + "Nominal solar luminosity. Standard.", + L_sun, +) +@doc( + "Standard luminosity at absolute bolometric magnitude 0. Standard.", + L_bol0, +) +@doc( + "Thomson scattering cross-section. Measured.", + sigma_T, +) +@doc( + "Astronomical unit. Standard.", + au, +) +@doc( + "Parsec. Standard.", + pc, +) +@doc( + "Light year. Standard.", + ly, +) +@doc( + "Standard atmosphere. Standard.", + atm, +) + """A tuple of all possible constants.""" const CONSTANT_SYMBOLS = Tuple(_CONSTANT_SYMBOLS) const CONSTANT_VALUES = Tuple(_CONSTANT_VALUES) From 67beece1f3c2877bb8edb8549a33ad9bee589b24 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 13:46:58 -0400 Subject: [PATCH 22/27] Put constant docstrings on docs page --- docs/make.jl | 1 + docs/src/constants.md | 46 +++++++++++++++++++++++++++++++++++++++++++ docs/src/units.md | 6 ++++-- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 docs/src/constants.md diff --git a/docs/make.jl b/docs/make.jl index da2dcedf..8dd3b581 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -41,6 +41,7 @@ makedocs(; "Home" => "index.md", "Utilities" => "api.md", "Units" => "units.md", + "Constants" => "constants.md", "Types" => "types.md", ] ) diff --git a/docs/src/constants.md b/docs/src/constants.md new file mode 100644 index 00000000..5bdf7e81 --- /dev/null +++ b/docs/src/constants.md @@ -0,0 +1,46 @@ + +# Units + +Many common physical constants are available as well: + +```@docs +Constants.c +Constants.h +Constants.hbar +Constants.e +Constants.k_B +Constants.N_A +Constants.eV +Constants.R +Constants.F +Constants.sigma_sb +Constants.alpha +Constants.u +Constants.G +Constants.mu_0 +Constants.eps_0 +Constants.m_e +Constants.m_p +Constants.m_n +Constants.a_0 +Constants.k_e +Constants.Ryd +``` + +## Astronomical constants + +```@docs +Constants.M_earth +Constants.M_sun +Constants.M_jup +Constants.R_earth +Constants.R_jup +Constants.R_sun +Constants.L_sun +Constants.L_bol0 +Constants.sigma_T +Constants.au +Constants.pc +Constants.ly +Constants.atm +``` diff --git a/docs/src/units.md b/docs/src/units.md index 3e231f84..098bdbc5 100644 --- a/docs/src/units.md +++ b/docs/src/units.md @@ -16,7 +16,7 @@ in a namespace with all the units available. ```@docs Units.m -Units.g +Units.kg Units.s Units.A Units.K @@ -24,6 +24,8 @@ Units.cd Units.mol ``` +### Derived units + Several derived SI units are available as well: ```@docs @@ -39,4 +41,4 @@ Units.Ω Units.T Units.L Units.bar -``` \ No newline at end of file +``` From a9f4625260f0a9b1c6ffa92b126e8d76330a2dd9 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 13:50:24 -0400 Subject: [PATCH 23/27] Add missing newline --- src/uparse.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uparse.jl b/src/uparse.jl index 0a29f887..eaa8e279 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -43,4 +43,4 @@ macro u_str(s) return esc(uparse(s)) end -end \ No newline at end of file +end From 1a6ff332f6ef047a9b66a199b104d29ed0860bda Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 14:12:17 -0400 Subject: [PATCH 24/27] Add documentation on symbolic units --- docs/make.jl | 1 + docs/src/types.md | 8 ++++++- src/symbolic_dimensions.jl | 49 +++++++++++++++++++++++++++++++++++--- src/uparse.jl | 10 ++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 8dd3b581..3e6f7114 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -42,6 +42,7 @@ makedocs(; "Utilities" => "api.md", "Units" => "units.md", "Constants" => "constants.md", + "Symbolic Units" => "symbolic_units.md", "Types" => "types.md", ] ) diff --git a/docs/src/types.md b/docs/src/types.md index 563e765d..1f8e2ff3 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -15,4 +15,10 @@ AbstractQuantity ``` Note also that the `Quantity` object can take a custom `AbstractDimensions` -as input, so there is often no need to subtype `AbstractQuantity` separately. \ No newline at end of file +as input, so there is often no need to subtype `AbstractQuantity` separately. + +Another type which subtypes `AbstractDimensions` is `SymbolicDimensions`: + +```@docs +SymbolicDimensions +``` diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 5ead741d..253deb60 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -29,6 +29,10 @@ This is to allow for lazily reducing to SI base units, whereas `Dimensions` is always in SI base units. Furthermore, `SymbolicDimensions` stores dimensions using a sparse vector for efficiency (since there are so many unit symbols). + +You can convert a quantity using `SymbolicDimensions` as its dimensions +to one which uses `Dimensions` as its dimensions (i.e., base SI units) +`expand_units`. """ struct SymbolicDimensions{R} <: AbstractDimensions{R} _data::SA.SparseVector{R} @@ -37,7 +41,10 @@ struct SymbolicDimensions{R} <: AbstractDimensions{R} SymbolicDimensions{_R}(data::SA.SparseVector) where {_R} = new{_R}(data) end +static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS data(d::SymbolicDimensions) = getfield(d, :_data) +Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[ALL_MAPPING[s]] +Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k) constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(data(d)) @@ -59,14 +66,19 @@ function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where end return result end + +""" + expand_units(q::Quantity{<:Any,<:SymbolicDimensions}) + +Expand the symbolic units in a quantity to their base SI form. +In other words, this converts a `Quantity` with `SymbolicDimensions` +to one with `Dimensions`. +""" function expand_units(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:Quantity{T,D}} return convert(Quantity{T,Dimensions{R}}, q) end -static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS -Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R} = data(d)[ALL_MAPPING[s]] -Base.getindex(d::SymbolicDimensions{R}, k::Symbol) where {R} = getproperty(d, k) Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(data(d))) Base.:(==)(l::SymbolicDimensions, r::SymbolicDimensions) = data(l) == data(r) Base.iszero(d::SymbolicDimensions) = iszero(data(d)) @@ -131,6 +143,22 @@ module SymbolicUnitsParse return nothing end + """ + sym_uparse(raw_string::AbstractString) + + Parse a string containing an expression of units and return the + corresponding `Quantity` object with `Float64` value. + However, that unlike the regular `u"..."` macro, this macro uses + `SymbolicDimensions` for the dimension type, which means that all units and + constants are stored symbolically and will not automatically expand to SI + units. For example, `sym_uparse("km/s^2")` would be parsed to + `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. + + Note that inside this expression, you also have access to the `Constants` + module. So, for example, `sym_uparse("Constants.c^2 * Hz^2")` would evaluate to + `Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to + namespace collisions, a few physical constants are not available. + """ function sym_uparse(raw_string::AbstractString) _generate_unit_symbols() Constants._generate_unit_symbols() @@ -145,6 +173,21 @@ end import .SymbolicUnitsParse: sym_uparse +""" + us"[unit expression]" + +Parse a string containing an expression of units and return the +corresponding `Quantity` object with `Float64` value. However, +unlike the regular `u"..."` macro, this macro uses `SymbolicDimensions` +for the dimension type, which means that all units and constants +are stored symbolically and will not automatically expand to SI units. +For example, `us"km/s^2"` would be parsed to `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. + +Note that inside this expression, you also have access to the `Constants` +module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to +`Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to +namespace collisions, a few physical constants are not available. +""" macro us_str(s) return esc(SymbolicUnitsParse.sym_uparse(s)) end diff --git a/src/uparse.jl b/src/uparse.jl index eaa8e279..e6f22854 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -23,6 +23,11 @@ eval(_generate_units_import()) Parse a string containing an expression of units and return the corresponding `Quantity` object with `Float64` value. For example, `uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. + +Note that inside this expression, you also have access to the `Constants` +module. So, for example, `uparse("Constants.c^2 * Hz^2")` would evaluate to +the quantity corresponding to the speed of light multiplied by Hertz, +squared. """ function uparse(s::AbstractString) return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} @@ -38,6 +43,11 @@ as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") Parse a string containing an expression of units and return the corresponding `Quantity` object with `Float64` value. For example, `u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. + +Note that inside this expression, you also have access to the `Constants` +module. So, for example, `u"Constants.c^2 * Hz^2"` would evaluate to +the quantity corresponding to the speed of light multiplied by Hertz, +squared. """ macro u_str(s) return esc(uparse(s)) From e259291a3c7d7cbe74a663b7a90dbd6779ad9850 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 14:35:16 -0400 Subject: [PATCH 25/27] Add constants with namespace collisions equal to expanded --- src/symbolic_dimensions.jl | 23 ++++++++++++++++++++++- test/unittests.jl | 17 +++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 253deb60..50797ffe 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -24,7 +24,7 @@ const ALL_MAPPING = NamedTuple([s => i for (i, s) in enumerate(ALL_SYMBOLS)]) """ SymbolicDimensions{R} <: AbstractDimensions{R} -An `AbstractDimensions` with one dimension for every unit symbol. +An `AbstractDimensions` with one dimension for every unit and constant symbol. This is to allow for lazily reducing to SI base units, whereas `Dimensions` is always in SI base units. Furthermore, `SymbolicDimensions` stores dimensions using a sparse vector for efficiency (since there @@ -58,6 +58,21 @@ SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(d return constructor(data) end +function Base.convert(::Type{Qout}, q::Quantity{<:Any,<:Dimensions}) where {T,D<:SymbolicDimensions,Qout<:Quantity{T,D}} + output = Qout( + convert(T, ustrip(q)), + D; + m=ulength(q), + kg=umass(q), + s=utime(q), + A=ucurrent(q), + K=utemperature(q), + cd=uluminosity(q), + mol=uamount(q), + ) + SA.dropzeros!(data(dimension(output))) + return output +end function Base.convert(::Type{Q}, q::Quantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:Quantity{T,D}} result = one(Q) * ustrip(q) d = dimension(q) @@ -115,6 +130,8 @@ module SymbolicUnitsParse import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE + import ...Constants as EagerConstants + const CONSTANT_SYMBOLS_EXIST = Ref{Bool}(false) const CONSTANT_SYMBOLS_LOCK = Threads.SpinLock() function _generate_unit_symbols() @@ -123,6 +140,10 @@ module SymbolicUnitsParse for unit in setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS) @eval const $unit = Quantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end + # Evaluate conflicting symbols to non-symbolic form: + for unit in SYMBOL_CONFLICTS + @eval const $unit = convert(Quantity{DEFAULT_VALUE_TYPE,SymbolicDimensions}, EagerConstants.$unit) + end CONSTANT_SYMBOLS_EXIST[] = true end return nothing diff --git a/test/unittests.jl b/test/unittests.jl index c07e883d..b3799309 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -431,4 +431,21 @@ end @test dimension(expand_units(inv(us"s") * us"m")) == dimension(expand_units(us"km/s")) @test_throws ErrorException sym_uparse("'c'") + + # For constants which have a namespace collision, the numerical expansion is used: + @test dimension(us"Constants.au")[:au] == 1 + @test dimension(us"Constants.h")[:h] == 0 + @test dimension(us"h")[:h] == 1 + + @test us"Constants.h" != us"h" + @test expand_units(us"Constants.h") == u"Constants.h" + + # Actually expands to: + @test dimension(us"Constants.h")[:m] == 2 + @test dimension(us"Constants.h")[:s] == -1 + @test dimension(us"Constants.h")[:kg] == 1 + + # So the numerical value is different from other constants: + @test ustrip(us"Constants.h") == ustrip(u"Constants.h") + @test ustrip(us"Constants.au") != ustrip(u"Constants.au") end From 82af824edec4d74bb54136df23d633da34940ba4 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 14:38:21 -0400 Subject: [PATCH 26/27] Add missing docs page --- docs/src/symbolic_units.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/src/symbolic_units.md diff --git a/docs/src/symbolic_units.md b/docs/src/symbolic_units.md new file mode 100644 index 00000000..6b17627a --- /dev/null +++ b/docs/src/symbolic_units.md @@ -0,0 +1,21 @@ +# Symbolic Dimensions + +Whereas `u"..."` will automatically convert all units to the same +base SI units, `us"..."` will not. This uses the `SymbolicDimensions` +type, which is a subtype of `AbstractDimensions` that stores the +dimensions symbolically. This is useful for keeping track of the +original units and constants in a user-entered expression. + +The two main functions for working with symbolic +units are `sym_uparse` and `us_str`: + +```@docs +@us_str +sym_uparse +``` + +To convert a quantity to its regular base SI units, use `expand_units`: + +```@docs +expand_units +``` From e6bd94015581d0753cac460c6d4cc46a4edc7de6 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 9 Jul 2023 14:49:51 -0400 Subject: [PATCH 27/27] Remove unused imports --- src/units.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/units.jl b/src/units.jl index 228fb0c3..644d7448 100644 --- a/src/units.jl +++ b/src/units.jl @@ -1,8 +1,5 @@ module Units -import SparseArrays as SA -import Tricks: static_fieldnames - import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_QUANTITY_TYPE