Skip to content

Improve @u_str implementation to fix downstream precompilation #106

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

Merged
merged 26 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2923c91
Attempt basic fix to precompilation
MilesCranmer Jan 4, 2024
25542fd
Unitful-like unit parsing macro
MilesCranmer Jan 4, 2024
4a28973
Update tests to user-passed version
MilesCranmer Jan 4, 2024
02142b2
Test `u_str` error messages
MilesCranmer Jan 4, 2024
8b22c1e
Test additional branches in `@u_str`
MilesCranmer Jan 4, 2024
20b485b
Make Unitful conversion compatible with singletons
MilesCranmer Jan 4, 2024
e0c58c7
Ensure to always convert away from singletons when parsing
MilesCranmer Jan 4, 2024
8c870a8
Introduce `NoDims` type for working around non-quantities
MilesCranmer Jan 4, 2024
c80113d
Ensure `iszero(::NoDims)` is true
MilesCranmer Jan 4, 2024
8d646d1
Fix error test
MilesCranmer Jan 4, 2024
7b99fed
Extra tests of `NoDims`
MilesCranmer Jan 5, 2024
56e47a0
Enforce type stability for `u_str`
MilesCranmer Jan 11, 2024
a3f839a
Fix parsing issue
MilesCranmer Jan 11, 2024
03242c1
Expand test coverage
MilesCranmer Jan 12, 2024
f89203d
Improve docs
MilesCranmer Jan 12, 2024
7f1997f
Improve docs
MilesCranmer Jan 12, 2024
ea0447b
More test coverage
MilesCranmer Jan 12, 2024
1658ebb
More tests
MilesCranmer Jan 12, 2024
d173a25
More coverage
MilesCranmer Jan 12, 2024
76737ff
More coverage
MilesCranmer Jan 12, 2024
519356b
Merge tag 'v0.11.1' into fix-precomp
MilesCranmer Jan 12, 2024
bf35743
Constraint uconvert
MilesCranmer Jan 12, 2024
fe10600
More exports for working with dimensions
MilesCranmer Jan 12, 2024
133445e
Update docs
MilesCranmer Jan 12, 2024
5ff5e70
Improve readme
MilesCranmer Jan 12, 2024
0636506
Fix coverage
MilesCranmer Jan 12, 2024
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ julia> room_temp = 100kPa
100000.0 m⁻¹ kg s⁻²
```

Note that `Units` is an exported submodule, so you can
also access this as `Units.kPa`. You may like to define

```julia
julia> const U = Units
```

so that you can simply write, say, `U.kPa` or `C.m_e`.

This supports a wide range of SI base and derived units, with common
prefixes.

Expand Down Expand Up @@ -169,13 +178,25 @@ julia> Constants.c
2.99792458e8 m s⁻¹
```

which you may like to define as

```julia
julia> const C = Constants
```

These can also be used inside the `u"..."` macro:

```julia
julia> u"Constants.c * Hz"
2.99792458e8 m s⁻²
```

Similarly, you can just import each individual constant:

```julia
julia> using DynamicQuantities.Constants: h
```

For the full list, see the [docs](https://symbolicml.org/DynamicQuantities.jl/dev/constants/).


Expand Down Expand Up @@ -220,6 +241,22 @@ julia> uconvert(us"nm", 5e-9u"m") # can also write 5e-9u"m" |> uconvert(us"nm")
5.0 nm
```

Finally, you can also import these directly:

```julia
julia> using DynamicQuantities.SymbolicUnits: cm
```

or constants:

```julia
julia> using DynamicQuantities.SymbolicConstants: h
```

Note that `SymbolicUnits` and `SymbolicConstants` are exported,
so you can simply access these as `SymbolicUnits.cm` and `SymbolicConstants.h`,
respectively.

### Arrays

For working with an array of quantities that have the same dimensions,
Expand Down
24 changes: 24 additions & 0 deletions docs/src/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ Another type which subtypes `AbstractDimensions` is `SymbolicDimensions`:
SymbolicDimensions
```

Just note that all of the symbolic units and constants are stored using the
immutable `SymbolicDimensionsSingleton`, which shares the same
supertype `AbstractSymbolicDimensions <: AbstractDimensions`. These get immediately
converted to the mutable `SymbolicDimensions` when used in any
calculation.

```@docs
SymbolicDimensionsSingleton
AbstractSymbolicDimensions
```

## Arrays

```@docs
Expand All @@ -40,6 +51,19 @@ which is subtyped to `Any`.
```@docs
GenericQuantity
AbstractGenericQuantity
```

In the other direction, there is also `RealQuantity`,
which is subtyped to `Real`.

```@docs
RealQuantity
AbstractRealQuantity
```

More general, these are each contained in the following:

```@docs
UnionAbstractQuantity
DynamicQuantities.ABSTRACT_QUANTITY_TYPES
```
Expand Down
2 changes: 1 addition & 1 deletion ext/DynamicQuantitiesUnitfulExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ for (_, _, Q) in ABSTRACT_QUANTITY_TYPES
validate_upreferred()
cumulator = DynamicQuantities.ustrip(x)
dims = DynamicQuantities.dimension(x)
if dims isa DynamicQuantities.SymbolicDimensions
if dims isa DynamicQuantities.AbstractSymbolicDimensions
throw(ArgumentError("Conversion of a `DynamicQuantities." * string($Q) * "` to a `Unitful.Quantity` is not defined with dimensions of type `SymbolicDimensions`. Instead, you can first use the `uexpand` function to convert the dimensions to their base SI form of type `Dimensions`, then convert this quantity to a `Unitful.Quantity`."))
end
equiv = unitful_equivalences()
Expand Down
8 changes: 6 additions & 2 deletions src/DynamicQuantities.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
module DynamicQuantities

export Units, Constants, SymbolicUnits, SymbolicConstants
export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity
export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError
export AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity
export Quantity, GenericQuantity, RealQuantity
export AbstractDimensions, Dimensions, NoDims
export AbstractSymbolicDimensions, SymbolicDimensions, SymbolicDimensionsSingleton
export QuantityArray
export DimensionError
export ustrip, dimension
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert
Expand Down
122 changes: 101 additions & 21 deletions src/symbolic_dimensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,16 @@ dimension_names(::Type{<:AbstractSymbolicDimensions}) = ALL_SYMBOLS
Base.propertynames(::AbstractSymbolicDimensions) = ALL_SYMBOLS
Base.getindex(d::AbstractSymbolicDimensions, k::Symbol) = getproperty(d, k)
constructorof(::Type{<:SymbolicDimensions}) = SymbolicDimensions
constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R}
constructorof(::Type{<:SymbolicDimensionsSingleton}) = SymbolicDimensionsSingleton
with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R}
with_type_parameters(::Type{<:SymbolicDimensionsSingleton}, ::Type{R}) where {R} = SymbolicDimensionsSingleton{R}
nzdims(d::SymbolicDimensions) = getfield(d, :nzdims)
nzdims(d::SymbolicDimensionsSingleton) = (getfield(d, :dim),)
nzvals(d::SymbolicDimensions) = getfield(d, :nzvals)
nzvals(::SymbolicDimensionsSingleton{R}) where {R} = (one(R),)
Base.eltype(::AbstractSymbolicDimensions{R}) where {R} = R
Base.eltype(::Type{<:AbstractSymbolicDimensions{R}}) where {R} = R

# Need to construct with `R` if available, as can't figure it out otherwise:
constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R}

# Conversion:
function SymbolicDimensions(d::SymbolicDimensionsSingleton{R}) where {R}
Expand Down Expand Up @@ -170,23 +171,55 @@ uexpand(q::QuantityArray) = uexpand.(q)
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)`.
"""
function uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
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."
qout_expanded = uexpand(qout)
dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded))
new_val = ustrip(q) / ustrip(qout_expanded)
new_dim = dimension(qout)
return new_quantity(typeof(q), new_val, new_dim)
end
function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions})
function uconvert(qout::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions})
@assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert."
qout_expanded = uexpand(qout)
dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded))
new_array = ustrip(q) ./ ustrip(qout_expanded)
new_dim = dimension(qout)
return QuantityArray(new_array, new_dim, quantity_type(q))
end
# TODO: Method for converting SymbolicDimensions -> SymbolicDimensions

# Ensure we always do operations with SymbolicDimensions:
function uconvert(
qout::UnionAbstractQuantity{T,<:SymbolicDimensionsSingleton{R}},
q::Union{
<:UnionAbstractQuantity{<:Any,<:Dimensions},
<:QuantityArray{<:Any,<:Any,<:Dimensions},
},
) where {T,R}
return uconvert(
convert(
with_type_parameters(
typeof(qout),
T,
with_type_parameters(SymbolicDimensions, R),
),
qout,
),
q,
)
end

# Allow user to convert SymbolicDimensions -> SymbolicDimensions
function uconvert(
qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions{R}},
q::Union{
<:UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions},
<:QuantityArray{<:Any,<:Any,<:AbstractSymbolicDimensions},
},
) where {R}
return uconvert(qout, uexpand(q))
end


"""
uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions})
Expand All @@ -197,7 +230,7 @@ a function equivalent to `q -> uconvert(qout, q)`.
uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout)

Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(nzdims(d)), copy(nzvals(d)))
Base.copy(d::SymbolicDimensionsSingleton) = constructorof(d)(getfield(d, :dim))
Base.copy(d::SymbolicDimensionsSingleton) = constructorof(typeof(d))(getfield(d, :dim))

function Base.:(==)(l::AbstractSymbolicDimensions, r::AbstractSymbolicDimensions)
nzdims_l = nzdims(l)
Expand Down Expand Up @@ -336,6 +369,7 @@ to enable pretty-printing of units.
module SymbolicUnits

import ..UNIT_SYMBOLS
import ..CONSTANT_SYMBOLS
import ..SymbolicDimensionsSingleton
import ...constructorof
import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
Expand All @@ -353,22 +387,34 @@ module SymbolicUnits
import ....DEFAULT_VALUE_TYPE
import ....DEFAULT_DIM_BASE_TYPE

const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]

for unit in CONSTANT_SYMBOLS
@eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
DEFAULT_VALUE_TYPE(1.0),
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
)
@eval begin
const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
DEFAULT_VALUE_TYPE(1.0),
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
)
push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
end
end
const SYMBOLIC_CONSTANT_VALUES = Tuple(_SYMBOLIC_CONSTANT_VALUES)
end
import .Constants
import .Constants as SymbolicConstants
import .Constants: SYMBOLIC_CONSTANT_VALUES

const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
for unit in UNIT_SYMBOLS
@eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
DEFAULT_VALUE_TYPE(1.0),
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit)))
)
@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)
end
end
const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES)


"""
Expand All @@ -387,18 +433,50 @@ module SymbolicUnits
`Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to
namespace collisions, a few physical constants are automatically converted.
"""
function sym_uparse(raw_string::AbstractString)
raw_result = eval(Meta.parse(raw_string))
return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
function sym_uparse(s::AbstractString)
ex = map_to_scope(Meta.parse(s))
ex = :($as_quantity($ex))
return copy(eval(ex))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
end

as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE) = q
as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, x)
as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))")

function map_to_scope(ex::Expr)
if ex.head == :call
ex.args[2:end] = map(map_to_scope, ex.args[2:end])
return ex
elseif ex.head == :. && ex.args[1] == :Constants
@assert ex.args[2] isa QuoteNode
return lookup_constant(ex.args[2].value)
else
throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `SymbolicConstants`) are expected."))
end
end
function map_to_scope(sym::Symbol)
if sym in UNIT_SYMBOLS
return lookup_unit(sym)
elseif sym in CONSTANT_SYMBOLS
throw(ArgumentError("Symbol $sym found in `SymbolicConstants` but not `SymbolicUnits`. Please access the `SymbolicConstants` module. For example, `u\"SymbolicConstants.$sym\"`."))
else
throw(ArgumentError("Symbol $sym not found in `SymbolicUnits` or `SymbolicConstants`."))
end
end
function map_to_scope(ex)
return ex
end
function lookup_unit(ex::Symbol)
i = findfirst(==(ex), UNIT_SYMBOLS)::Int
return as_quantity(SYMBOLIC_UNIT_VALUES[i])
end
function lookup_constant(ex::Symbol)
i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int
return as_quantity(SYMBOLIC_CONSTANT_VALUES[i])
end
end

import .SymbolicUnits: sym_uparse
import .SymbolicUnits: SymbolicConstants
import .SymbolicUnits: as_quantity, sym_uparse, SymbolicConstants, map_to_scope

"""
us"[unit expression]"
Expand All @@ -416,7 +494,9 @@ module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to
namespace collisions, a few physical constants are automatically converted.
"""
macro us_str(s)
return esc(SymbolicUnits.sym_uparse(s))
ex = map_to_scope(Meta.parse(s))
ex = :($as_quantity($ex))
return esc(ex)
end

function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{SymbolicDimensionsSingleton{R2}}) where {R1,R2}
Expand Down
14 changes: 14 additions & 0 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ end

const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE}

"""
NoDims{R}

A type representing the dimensions of a non-quantity.

For any `getproperty` call on this type, the result is `zero(R)`.
"""
struct NoDims{R<:Real} <: AbstractDimensions{R}
end

Base.getproperty(::NoDims{R}, ::Symbol) where {R} = zero(R)

const DEFAULT_DIMENSIONLESS_TYPE = NoDims{DEFAULT_DIM_BASE_TYPE}

"""
Quantity{T<:Number,D<:AbstractDimensions} <: AbstractQuantity{T,D} <: Number

Expand Down
Loading