Git Product home page Git Product logo

dynamicquantities.jl's Introduction

Dev Build Status Coverage Aqua QA

DynamicQuantities defines a simple statically-typed Quantity type for Julia. Physical dimensions are stored as a value, as opposed to a parametric type, as in Unitful.jl. This can greatly improve both runtime performance, by avoiding type instabilities, and startup time, as it avoids overspecializing methods.

Performance

DynamicQuantities can greatly outperform Unitful when the compiler cannot infer dimensions in a function:

julia> using BenchmarkTools, DynamicQuantities; import Unitful

julia> dyn_uni = 0.2u"m/s"
0.2 m s⁻¹

julia> unitful = convert(Unitful.Quantity, dyn_uni)
0.2 m s⁻¹

julia> f(x, i) = x ^ i * 0.3;

julia> @btime f($dyn_uni, 1);
  2.708 ns (0 allocations: 0 bytes)

julia> @btime f($unitful, 1);
  2.597 μs (30 allocations: 1.33 KiB)

Note the μ and n: this is a 1000x speedup! Here, the DynamicQuantities quantity object allows the compiler to build a function that is type stable, while the Unitful quantity object, which stores its dimensions in the type, requires type inference at runtime.

However, if the dimensions in your function can be inferred by the compiler, then you can get better speeds with Unitful:

julia> g(x) = x ^ 2 * 0.3;

julia> @btime g($dyn_uni);
  1.791 ns (0 allocations: 0 bytes)

julia> @btime g($unitful);
  1.500 ns (0 allocations: 0 bytes)

While both of these are type stable, because Unitful parametrizes the type on the dimensions, functions can specialize to units and the compiler can optimize away units from the code.

Usage

You can create a Quantity object by using the convenience macro u"...":

julia> x = 0.3u"km/s"
300.0 m s⁻¹

julia> y = 42 * u"kg"
42.0 kg

or by importing explicitly:

julia> using DynamicQuantities: kPa

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

You can also construct values explicitly with the Quantity type, with a value and keyword arguments for the powers of the physical dimensions (mass, length, time, current, temperature, luminosity, amount):

julia> x = Quantity(300.0, length=1, time=-1)
300.0 m s⁻¹

Elementary calculations with +, -, *, /, ^, sqrt, cbrt, abs are supported:

julia> x * y
12600.0 m kg s⁻¹

julia> x / y
7.142857142857143 m kg⁻¹ s⁻¹

julia> x ^ 3
2.7e7 m³ s⁻³

julia> x ^ -1
0.0033333333333333335 m⁻¹ s

julia> sqrt(x)
17.320508075688775 m¹ᐟ² s⁻¹ᐟ²

julia> x ^ 1.5
5196.152422706632 m³ᐟ² s⁻³ᐟ²

Each of these values has the same type, which means we don't need to perform type inference at runtime.

Furthermore, we can do dimensional analysis by detecting DimensionError:

julia> x + 3 * x
1.2 m¹ᐟ² kg

julia> x + y
ERROR: DimensionError: 0.3 m¹ᐟ² kg and 10.2 kg² s⁻² have incompatible dimensions

The dimensions of a Quantity can be accessed either with dimension(quantity) for the entire Dimensions object:

julia> dimension(x)
m¹ᐟ² kg

or with umass, ulength, etc., for the various dimensions:

julia> umass(x)
1//1

julia> ulength(x)
1//2

Finally, you can strip units with ustrip:

julia> ustrip(x)
0.2

Constants

There are a variety of physical constants accessible via the Constants submodule:

julia> Constants.c
2.99792458e8 m s⁻¹

which you may like to define as

julia> const C = Constants

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

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

Similarly, you can just import each individual constant:

julia> using DynamicQuantities.Constants: h

For the full list, see the docs.

Symbolic Units

You can also choose to not eagerly convert to SI base units, instead leaving the units as the user had written them. For example:

julia> q = 100us"cm * kPa"
100.0 cm kPa

julia> q^2
10000.0 cm² kPa²

You can convert to regular SI base units with uexpand:

julia> uexpand(q^2)
1.0e6 kg² s⁻⁴

This also works with constants:

julia> x = us"Constants.c * Hz"
1.0 Hz c

julia> x^2
1.0 Hz² c²

julia> uexpand(x^2)
8.987551787368176e16 m² s⁻⁴

You can also convert a quantity in regular base SI units to symbolic units with the |> infix operator

julia> 5e-9u"m" |> us"nm"
5.0 nm

You can also convert between different symbolic units. (Note that you can write this more explicitly with uconvert(us"nm", 5e-9u"m").)

Finally, you can also import these directly:

julia> using DynamicQuantities.SymbolicUnits: cm

or constants:

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.

Custom Units

You can create custom units with the @register_unit macro:

julia> @register_unit OneFiveV 1.5u"V"

and then use it in calculations normally:

julia> x = us"OneFiveV"
1.0 OneFiveV

julia> x * 10u"A" |> us"W"
15.0 W

julia> 3us"V" |> us"OneFiveV"
2.0 OneFiveV

Arrays

For working with an array of quantities that have the same dimensions, you can use a QuantityArray:

julia> ar = QuantityArray(rand(3), u"m/s")
3-element QuantityArray(::Vector{Float64}, ::Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}):
 0.2729202669351497 m s⁻¹
 0.992546340360901 m s⁻¹
 0.16863543422972482 m s⁻¹

This QuantityArray is a subtype <:AbstractArray{Quantity{Float64,Dimensions{...}},1}, meaning that indexing a specific element will return a Quantity:

julia> ar[2]
0.992546340360901 m s⁻¹

julia> ar[2] *= 2
1.985092680721802 m s⁻¹

julia> ar[2] += 0.5u"m/s"
2.485092680721802 m s⁻¹

This also has a custom broadcasting interface which allows the compiler to avoid redundant dimension calculations, relative to if you had simply used an array of quantities:

julia> f(v) = v^2 * 1.5;

julia> @btime $f.(xa) setup=(xa = randn(100000) .* u"km/s");
  109.500 μs (2 allocations: 3.81 MiB)

julia> @btime $f.(qa) setup=(xa = randn(100000) .* u"km/s"; qa = QuantityArray(xa));
  50.917 μs (3 allocations: 781.34 KiB)

So we can see the QuantityArray version saves on both time and memory.

Unitful

DynamicQuantities allows you to convert back and forth from Unitful.jl:

julia> using Unitful: Unitful, @u_str; import DynamicQuantities

julia> x = 0.5u"km/s"
0.5 km s⁻¹

julia> y = convert(DynamicQuantities.Quantity, x)
500.0 m s⁻¹

julia> y2 = y^2 * 0.3
75000.0 m² s⁻²

julia> x2 = convert(Unitful.Quantity, y2)
75000.0 m² s⁻²

julia> x^2*0.3 == x2
true

Types

Both a Quantity's values and dimensions are of arbitrary type. The default Dimensions (for the u"..." macro) performs exponent tracking for SI units, and SymbolicDimensions (for the us"..." macro) performs exponent tracking for all known unit and constant symbols, using a sparse array.

You can create custom spaces dimension spaces by simply creating a Julia struct subtyped to AbstractDimensions:

julia> struct CookiesAndMilk{R} <: AbstractDimensions{R}
           cookies::R
           milk::R
       end

julia> cookie_rate = Quantity(0.9, CookiesAndMilk(cookies=1, milk=-1))
0.9 cookies milk⁻¹

julia> total_milk = Quantity(103, CookiesAndMilk(milk=1))
103 milk

julia> total_cookies = cookie_rate * total_milk
92.7 cookies

Exponents are tracked by default with the type R = FixedRational{Int32,C}, which represents rational numbers with a fixed denominator C. This is much faster than Rational.

julia> typeof(0.5u"kg")
Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}

You can change the type of the value field by initializing with a value explicitly of the desired type.

julia> typeof(Quantity(Float16(0.5), mass=1, length=1))
Quantity{Float16, Dimensions{FixedRational{Int32, 25200}}}

or by conversion:

julia> typeof(convert(Quantity{Float16}, 0.5u"m/s"))
Quantity{Float16, Dimensions{FixedRational{Int32, 25200}}}

For many applications, FixedRational{Int8,6} will suffice, and can be faster as it means the entire Dimensions struct will fit into 64 bits. You can change the type of the dimensions field by passing the type you wish to use as the second argument to Quantity:

julia> using DynamicQuantities

julia> R8 = Dimensions{FixedRational{Int8,6}};

julia> R32 = Dimensions{FixedRational{Int32,2^4 * 3^2 * 5^2 * 7}};  # Default

julia> q8 = [Quantity{Float64,R8}(randn(), length=rand(-2:2)) for i in 1:1000];

julia> q32 = [Quantity{Float64,R32}(randn(), length=rand(-2:2)) for i in 1:1000];

julia> f(x) = @. x ^ 2 * 0.5;

julia> @btime f($q8);
  1.433 μs (3 allocations: 15.77 KiB)

julia> @btime f($q32);
  1.883 μs (4 allocations: 39.12 KiB)

dynamicquantities.jl's People

Contributors

dependabot[bot] avatar devmotion avatar gaurav-arya avatar glenhertz avatar j-fu avatar jkrumbiegel avatar kristofferc avatar michaelhatherly avatar mikeingold avatar milescranmer avatar mo-gul avatar oscardssmith avatar sweep-ai[bot] avatar torkele avatar ven-k avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

dynamicquantities.jl's Issues

What is the recommended way of dealing with non-SI units?

For example, if one wants to be able to use miles/hour, we can do
mile = 5280 * 12 * 0.0254u"m"
followed by
60 * mile/u"hr"
or because the latter is clumsy, maybe we use
mph = mile/u"hr"
after which we can use mph.

Is there thought about whether the user should be allowed to register new units which could then be used within the u"..." strings?

Beware of `preferunits`

preferunits allows to change the preferred units such that they will deviate from the default SI base units:

julia> using DynamicQuantities, Unitful

julia> Unitful.preferunits(u"km")

julia> convert(DynamicQuantities.Quantity,3u"km")
3 𝐋 ¹

I think users shall be made aware of this possibility.
In LessUnitful.jl I tried to provide an API function to allow a consistency check.

Unitful conversion fails for symbolic units

julia> convert(Unitful.Quantity, DynamicQuantities.u"s")
1.0 s

julia> convert(Unitful.Quantity, DynamicQuantities.us"s")
ERROR: type NamedTuple has no field s
Stacktrace:
 [1] getindex
   @ ./namedtuple.jl:137 [inlined]
 [2] convert(#unused#::Type{Quantity}, x::DynamicQuantities.Quantity{Float64, DynamicQuantities.SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}})
   @ DynamicQuantitiesUnitfulExt ~/.julia/packages/DynamicQuantities/81OKc/ext/DynamicQuantitiesUnitfulExt.jl:35
 [3] top-level scope
   @ REPL[66]:1

Quantity currently incompatible with QuadGK

I've been testing out DynamicQuantities as a replacement for Unitful in some of my packages and I discovered that it currently doesn't work with QuadGK's integral solver like Unitful does. MWE (issue also opened here).

julia> using DynamicQuantities  # [06fc5a27] DynamicQuantities v0.7.0

julia> using QuadGK  # [1fd47b50] QuadGK v2.8.2

julia> quadgk(t -> 5u"m/s"*t, 0u"s", 10u"s")
ERROR: MethodError: no method matching cachedrule(::Type{Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}, ::Int64)

Closest candidates are:
  cachedrule(::Union{Type{Complex{BigFloat}}, Type{BigFloat}}, ::Integer)
   @ QuadGK C:\Users\*\.julia\packages\QuadGK\XqIlh\src\gausskronrod.jl:253
  cachedrule(::Type{T}, ::Integer) where T<:Number
   @ QuadGK C:\Users\*\.julia\packages\QuadGK\XqIlh\src\gausskronrod.jl:264

It looks like the issue probably spawns from here where Base.one(::Quantity) produces a new_quantity.

julia> typeof(one(typeof(1u"s")))
Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}

QuadGK would expect Base.one for the above example to return simply a Float64 instead. This seems consistent with the docstring for Base.one, excerpted:

If possible, one(x) returns a value of the same type as x, and one(T) returns a value of type T. However,
this may not be the case for types representing dimensionful quantities (e.g. time in days), since the
multiplicative identity must be dimensionless. In that case, one(x) should return an identity value of
the same precision (and shape, for matrices) as x.

If you want a quantity that is of the same type as x, or of type T, even if x is dimensionful, use
oneunit instead.

...

  Examples
  ≡≡≡≡≡≡≡≡≡≡

...

  julia> import Dates; one(Dates.Day(1))
  1

It also seems consistent with how Unitful handles this

julia> typeof(one(typeof(1.0u"s")))  # Unitful @u_str
Float64

Was the current definition of Base.one here intended to return the dimensioned type?

Add a scale factor dimension?

We're looking into adding this to JuMP: trulsf/UnitJuMP.jl#18

One kicker is that it'd be nice to be able to talk about micro/milli/kilo/mega etc.

Have you thought about adding a scale::Int dimension? Hopefully it'd support something like:

km = Dimension(length = 1, scale = 3)
us = Dimension(time = -1, scale = -6)
km / us == Dimension(length = 1, time = -2, scale = 9)

Exponential should work with dimensionless quantities

We should be able to exponentiate dimensionless quantitites. For a motivated example:

using DynamicQuantities.Constants: a_0

R10(r) = 2 / a_0^(3/2) * exp(-r/a_0)
R10(3 * a_0)

And a minimal case:

using DynamicQuantities
exp(1u"m" / 1u"m")

With error:

ERROR: MethodError: no method matching exp(::Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}})

Happy to put in a PR, although it may be a few days due to schoolwork.

Registering custom unit propagation rules

It could be useful for users (especially package authors) to write custom unit propagation rules for their functions, that instruct DynamicQuantities on how units propagate from the input to the output without having to analyze the whole function. Writing a generic rule for abstract quantities seems nearly already possible with a simple dispatch, for example:

julia> using DynamicQuantities

julia> function mycube(x)
           # this function does not like DynamicQuantities
           x isa AbstractQuantity && sleep(1)
           return x^3
       end
mycube (generic function with 1 method)

julia> function mycube(x::AbstractQuantity)
           return DynamicQuantities.new_quantity(typeof(x), mycube(ustrip(x)), dimension(x)^3)
       end
mycube (generic function with 2 methods)

julia> @time mycube(1u"m")
  0.000001 seconds
1.0

The "nearly" is because DynamicQuantities.new_quantity is not currently public API: should it be?

One could imagine using this pattern for performance on a complicated function (e.g. with many inner loops, scalar calculations, etc.), in cases where exists a performance overhead of DynamicQuantities and a package author wants to ensure zero-overhead performance while avoiding the everything-is-a-new-type approach of unitful, e.g. #55.

We could make the process even easier than the above, too. For example, we could automate the process of ustrip'ing and feeding into the original functions, handling general inputs via Functors.fmap(ustrip, args), so that the user only has to specify how the dimension is propagated.

Unexpected sum Performance Delta vs Unitful

I was running some testing to see if I could boost performance in a package of mine by converting from Unitful but ran into some unexpected benchmark results where Unitful seems to perform about an order-of-magnitude faster when suming vectors of Quantities. Here's a MWE with benchmark results from my system (versioninfo at bottom). I'm basically just instantiating a 1000-length vector of Cartesian length vectors and then suming it. Any ideas about what's going on here?

I first defined a generic struct for Cartesian vectors:

using Unitful
using DynamicQuantities
using BenchmarkTools
using StaticArrays

struct CoordinateCartesian{L}
	x::L
	y::L
	z::L
end

Base.:+(u::CoordinateCartesian, v::CoordinateCartesian) = CoordinateCartesian(u.x+v.x, u.y+v.y, u.z+v.z)

# Alias for each package's @u_str for a unit meter
m_uf = Unitful.@u_str("m")
m_dq = DynamicQuantities.@u_str("m")

Using the CoordinateCartesian struct with Unitful Quantities:

u = CoordinateCartesian(1.0m_uf, 2.0m_uf, 3.0m_uf)
u_arr = repeat([u], 1000)
    """
    1000-element Vector{CoordinateCartesian{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}}
    """
	
@benchmark sum($u_arr)
    """
    BenchmarkTools.Trial: 10000 samples with 259 evaluations.
     Range (min … max):  286.486 ns … 563.320 ns  ┊ GC (min … max): 0.00% … 0.00%
     Time  (median):     298.069 ns               ┊ GC (median):    0.00%
     Time  (mean ± σ):   298.142 ns ±  12.318 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
    
      ▁▆▂ ▃▂▂ ▂█▄              ▁                                    ▁
      ███▆███▆█████▇▆▆▇█▇█▅▇▅▆███▄▃▁▄▄▁▅▅▅▁▁▃▃▁▃▁▁▄▃▃▃▃▅▅▁▅▁▇▃▇▆▄▆▆ █
      286 ns        Histogram: log(frequency) by time        363 ns <
    
     Memory estimate: 0 bytes, allocs estimate: 0.
    """

Using the CoordinateCartesian struct with DynamicQuantities Quantities is roughly an order-of-magnitude slower:

v = CoordinateCartesian(1.0m_dq, 2.0m_dq, 3.0m_dq)
v_arr = repeat([v], 1000)
    """
    1000-element Vector{CoordinateCartesian{DynamicQuantities.Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}}
    """

@benchmark sum($v_arr)
    """
    BenchmarkTools.Trial: 10000 samples with 9 evaluations.
    Range (min … max):  2.333 μs …   7.833 μs  ┊ GC (min … max): 0.00% … 0.00%
    Time  (median):     2.344 μs               ┊ GC (median):    0.00%
    Time  (mean ± σ):   2.354 μs ± 141.301 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
    
    ▇█                                                          ▁
    ██▁▆▁█▇▁▅▁▅▁▅▅▁▅▁▄▁▄▅▁▅▁▄▁▁▄▁▁▁▅▅▁▅▁▄▁▅▃▁▅▁▇▁█▆▁▅▁▄▁▆▄▁▃▁▅▅ █
    2.33 μs      Histogram: log(frequency) by time      2.71 μs <
    
    Memory estimate: 0 bytes, allocs estimate: 0.
    """

Next I tried replacing the struct with the built-in QuantityArray type, which was much slower still, apparently due to allocations.

a = DynamicQuantities.QuantityArray([1.0m_dq, 2.0m_dq, 3.0m_dq])
a_arr = repeat([a], 1000)
    """
    1000-element Vector{QuantityArray{Float64, 1, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}}}
    """

@benchmark sum($a_arr)
    """
    BenchmarkTools.Trial: 10000 samples with 1 evaluation.
     Range (min … max):  151.800 μs …  1.280 ms  ┊ GC (min … max): 0.00% … 85.76%
     Time  (median):     159.400 μs              ┊ GC (median):    0.00%
     Time  (mean ± σ):   162.371 μs ± 43.999 μs  ┊ GC (mean ± σ):  1.06% ±  3.42%
    
			     ▁▃▃▅▇█▆▄▄▃▃▃▂▂▂▂▂                 ▁  ▁            ▂
      ▄▁▄▄▆▄▆▇███████████████████████▇▇▇█▇▇▆▆▅▆▆▇▇███████▇▆▇▆▆▆▅▆▅ █
      152 μs        Histogram: log(frequency) by time       180 μs <
    
     Memory estimate: 78.05 KiB, allocs estimate: 999.
    """

How about a simple StaticVector of Quantities? This gets us back to the neighborhood of the struct with DynamicQuantities, but still slower than the Unitful version of the same.

b = SVector(1.0m_dq, 2.0m_dq, 3.0m_dq)
b_arr = repeat([b], 1000)
    """
    1000-element Vector{SVector{3, DynamicQuantities.Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}}
    """

@benchmark sum($b_arr)
    """
    BenchmarkTools.Trial: 10000 samples with 9 evaluations.
    Range (min … max):  2.333 μs …   8.067 μs  ┊ GC (min … max): 0.00% … 0.00%
    Time  (median):     2.344 μs               ┊ GC (median):    0.00%
    Time  (mean ± σ):   2.407 μs ± 188.850 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
    
    ██                                        ▁▅▃ ▁   ▁         ▂
    ██▆▆▇▃▄▄▅▄▅▁▅▄▅▅▅▅▅▅▅▄▃▁▆▅▅█▇▅▅▅▅▅▅▁▄▇▆▅▅▇█████▁▅▇██▆▄█▇█▅▄ █
    2.33 μs      Histogram: log(frequency) by time      2.93 μs <
    
    Memory estimate: 0 bytes, allocs estimate: 0.
    """

Again, how does this SVector version compare to one with Unitful?

c = SVector(1.0m_uf, 2.0m_uf, 3.0m_uf)
c_arr = repeat([c], 1000)
    """
    1000-element Vector{SVector{3, Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}}}
    """

@benchmark sum($c_arr)
    """
    BenchmarkTools.Trial: 10000 samples with 259 evaluations.
     Range (min … max):  298.069 ns … 495.367 ns  ┊ GC (min … max): 0.00% … 0.00%
     Time  (median):     298.456 ns               ┊ GC (median):    0.00%
     Time  (mean ± σ):   301.102 ns ±  12.656 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
    
      █▂▁    ▁     ▁▂                                               ▁
      ███▇▇▆██▇█▇▇███▆▃▄▄▄▆▇▆▆▄▄▄▄▄▄▄▄▄▃▄▁▃▄▄▄▄▄▄▄▄▃▃▄▃▁▅▃▅▆▄▄▃▄▄▃▄ █
      298 ns        Histogram: log(frequency) by time        384 ns <
    
     Memory estimate: 0 bytes, allocs estimate: 0.
    """

System info:

julia> versioninfo()
  Julia Version 1.9.3
  Commit bed2cd540a (2023-08-24 14:43 UTC)
  Build Info:
    Official https://julialang.org/ release
  Platform Info:
    OS: Windows (x86_64-w64-mingw32)
    CPU: 32 × AMD Ryzen 9 3950X 16-Core Processor
    WORD_SIZE: 64
    LIBM: libopenlibm
    LLVM: libLLVM-14.0.6 (ORCJIT, znver2)
    Threads: 16 on 32 virtual cores
  Environment:
    JULIA_NUM_THREADS = 16

(@v1.9) pkg> st
  Status `C:\Users\mikei\.julia\environments\v1.9\Project.toml`
    [6e4b80f9] BenchmarkTools v1.3.2
    [06fc5a27] DynamicQuantities v0.7.1
    [90137ffa] StaticArrays v1.6.5
    [1986cc42] Unitful v1.17.0

Sweep: Define `NoDims` for indicating something is not a quantity

We should define NoDims as an abstract type in src/types.jl which gets returned by dimension by default, when there is no quantity input. Thus, e.g., iszero(dimension(::Float32)) should be true by default, and dimension(::Float32) == dimension(::UnionAbstractQuantity) should always be true when iszero(dimension(::UnionAbstractQuantity)) is true.

Should write unit tests for this change.

Can also use this to simplify some of the operators in src/math.jl. Instead of having them write out iszero(...), they can simply write dimension(l) == dimension(r) to let us refactor parts of the code.

Checklist
  • Modify src/types.jl ! No changes made
  • Modify src/math.jla8ab0c0
  • Ran sandbox for src/math.jl. ✓
  • Create test/nodims.jlaf498af
  • Ran sandbox for test/nodims.jl. ✓

Flowchart

Pretty print element types of `QuantityArray`

So far QuantityArray shows this kind of output

julia> ar = QuantityArray(rand(3), u"m/s")
3-element QuantityArray(::Vector{Float64}, ::Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
0.2729202669351497 m s⁻¹
0.992546340360901 m s⁻¹
0.16863543422972482 m s⁻¹

So obviously it is possible to print the unit string from the dimension.
As far as I understand the design of QuantityArrays it is not possible to concatenate two arrays of different dimensions, meaning that the unit is shared among all array values.
With that in mind, does it make sense to print the unit of each element of the array given that it is shared by all elements, and takes quite a bit of space for matrices such as

julia> A = hcat(QuantityArray(randn(32,5) .* 1u"km/s"),QuantityArray(rand(32,2) .* 1u"m/s")
)
32×7 QuantityArray(::Matrix{Float64}, ::Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
386.672 m s⁻¹ -81.0539 m s⁻¹ 424.889 m s⁻¹ -39.8638 m s⁻¹ -504.45 m s⁻¹ 0.0555176 m s⁻¹ 0.245589 m s⁻¹
-381.83 m s⁻¹ -2759.87 m s⁻¹ 647.776 m s⁻¹ -298.789 m s⁻¹ -1403.23 m s⁻¹ 0.2663 m s⁻¹ 0.198563 m s⁻¹
940.253 m s⁻¹ -600.701 m s⁻¹ -1039.45 m s⁻¹ -1567.83 m s⁻¹ -9.59547 m s⁻¹ 0.660183 m s⁻¹ 0.459871 m s⁻¹
344.404 m s⁻¹ 241.106 m s⁻¹ 150.618 m s⁻¹ -1498.66 m s⁻¹ -1043.98 m s⁻¹ 0.975485 m s⁻¹ 0.117091 m s⁻¹
-2350.84 m s⁻¹ -1049.62 m s⁻¹ 517.911 m s⁻¹ 727.556 m s⁻¹ -1440.38 m s⁻¹ 0.762756 m s⁻¹ 0.0765941 m s⁻¹
1444.87 m s⁻¹ -264.519 m s⁻¹ -1044.43 m s⁻¹ 608.438 m s⁻¹ -1093.79 m s⁻¹ 0.484282 m s⁻¹ 0.274533 m s⁻¹
778.131 m s⁻¹ 625.476 m s⁻¹ 331.268 m s⁻¹ -438.879 m s⁻¹ 393.874 m s⁻¹ 0.305781 m s⁻¹ 0.165897 m s⁻¹
269.81 m s⁻¹ 436.029 m s⁻¹ -2178.13 m s⁻¹ -972.365 m s⁻¹ 34.1768 m s⁻¹ 0.370966 m s⁻¹ 0.0336445 m s⁻¹
-454.505 m s⁻¹ -1477.05 m s⁻¹ 1048.72 m s⁻¹ 842.635 m s⁻¹ -970.589 m s⁻¹ 0.142977 m s⁻¹ 0.459108 m s⁻¹
580.788 m s⁻¹ -2201.46 m s⁻¹ -1862.64 m s⁻¹ -885.331 m s⁻¹ -658.453 m s⁻¹ 0.357363 m s⁻¹ 0.282407 m s⁻¹
-1192.77 m s⁻¹ 1020.91 m s⁻¹ 334.605 m s⁻¹ -892.718 m s⁻¹ 213.223 m s⁻¹ 0.805413 m s⁻¹ 0.439469 m s⁻¹
307.255 m s⁻¹ -1208.88 m s⁻¹ -1137.83 m s⁻¹ 787.486 m s⁻¹ 1893.14 m s⁻¹ 0.249205 m s⁻¹ 0.393297 m s⁻¹
2256.44 m s⁻¹ -380.859 m s⁻¹ -601.359 m s⁻¹ 1517.48 m s⁻¹ -1265.31 m s⁻¹ 0.388709 m s⁻¹ 0.303767 m s⁻¹
-282.792 m s⁻¹ 10.4678 m s⁻¹ 388.6 m s⁻¹ 520.606 m s⁻¹ 1000.6 m s⁻¹ 0.747662 m s⁻¹ 0.565991 m s⁻¹
-42.5849 m s⁻¹ 2128.23 m s⁻¹ 3.97641 m s⁻¹ -740.586 m s⁻¹ -721.475 m s⁻¹ 0.964792 m s⁻¹ 0.740287 m s⁻¹

Wouldn't it make more sense to show the unit in the type with something like

32×7 QuantityArray(::Matrix{Float64}, ::Quantity{Float64, Dimensions{m s-1)):

Molar units not supported

Molar (different from moles) are commonly used in chemistry and biology. It would be useful to support it:

Amount in molar. Available variant: pM, nM, μM (/uM), mM.

(Molarity is defined as moles per litre, e.g. M = mol/L)

Compatibility with `zero(::Type{T})`

It seems like a reason DynamicQuantities.jl is incompatible with many libraries right now is due to their usage of zero(::Type{T}), which works for Unitful but not for DynamicQuantities.jl.

Therefore I wonder if there is a way of defining something like:

zero(::Type{Q}) where {Q<:AbstractQuantity} = AutoQuantityZero{Q}()

which could avoid checking for any dimension errors in operations, and act as a neutral zero.

The problem with this is type instability, but perhaps that's better than simply throwing an error...? Then as packages adapt they would get type stability again. (And of course any package already using zero(::T) would maintain type stability).

Thoughts @gaurav-arya?

We could even think of adding a special boolean field to Quantity to indicate this... thus making everything type stable. But it's a bit worrisome...

Use `Int64 / C` as default

cc @oscardssmith

I think it would probably be much faster to use Int64 / C, for some choice of constant C, than to use Rational{Int16} as is currently done – as it would avoid repeated gcd calls.

We would want to pick some constant C such that most rationals expressed by Rational{Int16} that people would typically want to use could also be expressed this way.

Even more rigorous testing with PropCheck

100% test coverage is not enough!

I think we should add PropCheck.jl testing to probe more edge cases.

Here's a look at how this could work (https://seelengrab.github.io/PropCheck.jl/stable/Examples/structs.html). The following code builds a PropCheck generator of Quantity{T, R}s:

using Test
using DynamicQuantities
using DynamicQuantities: DEFAULT_DIM_BASE_TYPE
using PropCheck: itype, isample, interleave, check, generate

DEFAULT_R = DEFAULT_DIM_BASE_TYPE

function ifixedrational(R; limit=100)
    inumerator = isample(-limit:limit)
    idenominator = isample(1:limit)
    iargs = interleave(inumerator, idenominator)
    return map(R, map(splat(Rational), iargs))
end

function idimensions(R; dimension_limit=100)
    iargs = interleave(
        ntuple(
            _ -> ifixedrational(R, limit=dimension_limit),
            fieldcount(Dimensions{R})
        )...
    )
    return map(splat(Dimensions{R}), iargs)
end

function iquantity(T, R; dimension_limit=100)
    iargs = interleave(itype(T), idimensions(R; dimension_limit))
    return map(splat(Quantity), iargs)
end

with this, we can see that the combination of these iterators gives us random quantities:

generate(iquantity(Float32, DEFAULT_R))
# Tree(-2.802293e-10 m³³²³ᐟ¹²⁶⁰⁰ kg²⁰²⁶³ᐟ²⁵²⁰⁰ s⁸⁹⁰⁹ᐟ¹²⁶⁰⁰ A⁻⁷⁰⁶ᐟ¹⁵⁷⁵ K⁻⁹²ᐟ⁶³ cd¹ᐟ⁶ mol⁸⁰⁴⁷ᐟ³⁶⁰⁰)

with this iterator, we can test different properties, like commutativity of operators, via PropCheck's shrinkage approach:

@test check(interleave(iquantity(Float32, DEFAULT_R), iquantity(Float32, DEFAULT_R))) do (q1, q2)
    q1 * q2 == q2 * q1
end

This is more powerful than writing out test cases by hand since every time it runs, it will randomly check a different part of the code. Manual test cases might keep missing a particular combination of code branches and we may never know about it.

INVALID vs error

Hi,

playing around with linear system solution with DynamicQuantities. Getting stuck a the point I already anticipated, so opening this issue:

Adding a meter and a kg in the moment results in INVALID.
But this is quite hard to debug - one would dive into the code to figure out where this happens.

Wouldn't it be better to just throw an error on such an operation ? Or can throwing an error be made optional ?

Also it would be possible to remove the valid flag and to use its space for other purposes.

Round tripping Quantities through String

Hi, thanks for the package, nice to have an alternative approach to the Unitful type-based one.

Is there a way in this package to convert back and fourth between Quantities and String representations so that quantities could be serialised to strings and parsed back again? Like in Unitful, string(q::Quantity) prints a space between the last numeral and the unit, which yields an error with uparse.

`map` fails with `QuantityArray`

julia> map(identity, [us"hr"])
1-element Vector{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}:
 1.0 hr

julia> map(identity, QuantityArray([us"hr"]))
ERROR: TypeError: in typeassert, expected AbstractVector{<:DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}, got a value of type QuantityArray{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}}
Stacktrace:
 [1] _collect(c::QuantityArray{Float64, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}}, itr::Base.Generator{QuantityArray{Float64, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}}, typeof(identity)}, #unused#::Base.EltypeUnknown, isz::Base.HasShape{1})
   @ Base ./array.jl:812
 [2] collect_similar(cont::QuantityArray{Float64, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}}, itr::Base.Generator{QuantityArray{Float64, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}}, typeof(identity)})
   @ Base ./array.jl:711
 [3] map(f::Function, A::QuantityArray{Float64, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{Float64}})
   @ Base ./abstractarray.jl:3263
 [4] top-level scope
   @ REPL[92]:1

version is [06fc5a27] DynamicQuantities v0.10.0

Physical constants

We could have a submodule that delivers common physical constants. Already I am finding myself having to type out the speed of light by hand; it would be useful if it were just built-in.

This could work as follows:

using DynamicQuantities
import DynamicQuantities: Units as U, Constants as C

Then, C would let you access constants, and U the units. This is similar to how astropy does it, to avoid mixing namespaces.

e.g., the energy of a 500nm photon

wavelength = 500*U.nm
energy = C.c*C.h/wavelength

This Constants namespace could also be exposed within the @u_str macro, so you could also write this as

energy = (1/500) * u"C.c*C.h/nm"

I'd be interested to know if @odow or @ChrisRackauckas have any thoughts or ideas in this direction.

`bionomial` and `factorial` functions not defined?

I'm coming here indirectly via ModelingToolkit.jl (which my package, Catalyst.jl, depends on) which recently started using DynamicQuantities.

Catalyst currently crashed whenever you try to use the binomial or factorial functions. I have traced this to ModelingToolkit: SciML/ModelingToolkit.jl#2554. I am not familiar with DynamicQuantities (only ModelingToolkit), but it seems like this issue might be traced here? (see conversation)

Sorry for the non-exhaustive description. This sounds like the error (and how to fix it) might be directly obvious for someone familiar with the package. If not, I will try to figure out more about what's going on.

Meshes.jl test failure

Not sure what this error is from... @mikeingold any idea?

julia> using DynamicQuantities, Meshes

julia> v = Meshes.Vec(1.0u"m/s", 2.0u"m/s", 3.0u"m/s")
ERROR: StackOverflowError:

This worked fine like a couple of weeks ago (included in test/test_meshes.jl). And DynamicQuantities.jl doesn't implement any special code for Meshes.jl.

If I pass a tuple instead it gives me more info:

julia> v = Meshes.Vec((1.0u"m/s", 2.0u"m/s", 3.0u"m/s"))
ERROR: StackOverflowError:
Stacktrace:
 [1] ntuple
   @ ./ntuple.jl:50 [inlined]
 [2] copy
   @ ./broadcast.jl:1097 [inlined]
 [3] materialize
   @ ./broadcast.jl:867 [inlined]
 [4] Vec(coords::Tuple{Quantity{Float64, Dimensions{…}}, Quantity{Float64, Dimensions{…}}, Quantity{Float64, Dimensions{…}}}) (repeats 58052 times)
   @ Meshes ~/.julia/packages/Meshes/BNwtP/src/vectors.jl:62
Some type information was truncated. Use `show(err)` to see complete types.

@juliohm @eliascarv do you know why this might be? Are there any breaking changes recently that might have changed this syntax?

Why `one(1u"kg")` no dimension?

Example below:

julia> using DynamicQuantities
julia> zero(1u"kg")
0.0 kg
julia> one(1u"kg")
1.0
julia> zero(1u"kg") |> dimension
kg
julia> one(1u"kg") |> dimension
# nothing displayed

I wonder why dimensions for the results of zero and one functions are different. Is it intentional or a bug?

+ and - should propagate `Quantity.valid`

Also, might be worth re-evaluating tradeoff of errors vs passing a flag.

Another option is to lazily compute a hash value of the dimensions, and only look at that when checking dimension equality. Then frequent sums would only have to check ==(::UInt, ::UInt) rather than on a whole tuple of rational ints.

`reduce(vcat` returns corrupted result

Something weird happens to the units in the reduce(vcat version, you can see already in the printout that the values differ. 1.0 hr vs (1.0 ) hr. I only noticed this later though, because uexpand then throws an error on these values.

julia> using DynamicQuantities

julia> arr1 = QuantityArray([us"hr"])
1-element QuantityArray(::Vector{Float64}, ::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
 1.0 hr

julia> arr2 = QuantityArray([us"hr"])
1-element QuantityArray(::Vector{Float64}, ::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
 1.0 hr

julia> arr3 = vcat(arr1, arr2)
2-element QuantityArray(::Vector{Float64}, ::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
 1.0 hr
 1.0 hr

julia> DynamicQuantities.uexpand(arr3)
2-element QuantityArray(::Vector{Float64}, ::DynamicQuantities.Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
 3600.0 s
 3600.0 s

julia> arr4 = reduce(vcat, [arr1, arr2])
2-element QuantityArray(::Vector{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}, ::DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
 (1.0 ) hr
 (1.0 ) hr

julia> DynamicQuantities.uexpand(arr4)
ERROR: MethodError: no method matching unsafe_fixed_rational(::DynamicQuantities.Quantity{Int32, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, ::Type{Int32}, ::Val{25200})

Closest candidates are:
  unsafe_fixed_rational(::Integer, ::Type{T}, ::Val{den}) where {T, den}
   @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/fixed_rational.jl:15

Stacktrace:
  [1] tryrationalize(#unused#::Type{DynamicQuantities.FixedRational{Int32, 25200}}, x::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/fixed_rational.jl:116
  [2] _pow(l::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, r::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/math.jl:119
  [3] ^(l::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, r::DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/math.jl:127
  [4] _pow(l::DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, r::DynamicQuantities.FixedRational{Int32, 25200})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/math.jl:122
  [5] ^(l::DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, r::DynamicQuantities.FixedRational{Int32, 25200})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/math.jl:127
  [6] convert(#unused#::Type{DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}, q::DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/symbolic_dimensions.jl:94
  [7] uexpand
    @ ~/.julia/packages/DynamicQuantities/kwSjp/src/symbolic_dimensions.jl:113 [inlined]
  [8] materialize_first
    @ ~/.julia/packages/DynamicQuantities/kwSjp/src/arrays.jl:177 [inlined]
  [9] similar
    @ ~/.julia/packages/DynamicQuantities/kwSjp/src/arrays.jl:161 [inlined]
 [10] similar
    @ ~/.julia/packages/DynamicQuantities/kwSjp/src/arrays.jl:160 [inlined]
 [11] copy
    @ ./broadcast.jl:898 [inlined]
 [12] materialize
    @ ./broadcast.jl:873 [inlined]
 [13] uexpand(q::QuantityArray{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, 1, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}, DynamicQuantities.Quantity{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}, Vector{DynamicQuantities.Quantity{Float64, SymbolicDimensions{DynamicQuantities.FixedRational{Int32, 25200}}}}})
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/kwSjp/src/symbolic_dimensions.jl:115
 [14] top-level scope
    @ REPL[63]:1

This was run on [06fc5a27] DynamicQuantities v0.10.0

Make SparseArrays a weak dependency?

In Julia >= 1.10, SparseArrays won't be included in the system image anymore. To reduce loading times, therefore SparseArrays was or is about to be demoted from a regular to a weak dependency for many packages in the ecosystem such as FillArrays, Distances and PDMats. However, if a package in the dependency stack loads SparseArrays all these extensions will be compiled and loaded. Could the SparseArrays dependency of DynamicQuantities also be moved to an extension or removed alltogether to avoid that it triggers all these extensions?

Immutable constants for symbolic dimensions

Right now the units and constants for symbolic dimensions, like the one you get from us"km", are stored with mutable arrays (note that non-symbolic versions like u"km" are fine as-is).

This is part of the reason it takes so long to generate all of the constants, which is why they are created at first call to sym_uparse, rather than at precompilation time. This is why the first time you call us"km", it takes a little longer. It's also why you can't precompile things with symbolic dimensions (see #58).

I think it might make sense instead to have an immutable version of each of the symbolic units and constants. I'm not quite sure how this would work. Maybe you could have another SymbolicImmutableDimensions <: AbstractDimensions that is explicitly for storing symbolic dimensions constants, and stores the symbol.

Then you could have promotion rules set up so it will convert itself to SymbolicDimensions when operated on. But maybe there is a simpler way.

Thoughts @gaurav-arya @devmotion?

Logo

Played around with Luxor yesterday, thought this package is still missing some kind of logo.
Here's one idea, something like a "decoder ring". I thought about one being able to change between different units dynamically, "dialing them in", sort of.

Here's the Luxor code
using Luxor
using Luxor.Colors


image_width = 500

@drawsvg begin
    circle_radius = 0.85 * image_width / 2
    scale(circle_radius, circle_radius)

    offsets_deg = [20, 160, -2.5]
    colors = Base.splat(RGB).([Luxor.julia_green, Luxor.julia_red, Luxor.julia_purple])
    inner_radius = 0.25
    ring_thicknesses = [0.25, 0.25, 0.25]
    ringgap = 0.02
    sectorgap_deg = 5
    sector_whiten = 0.25
    casing_opacity = 0.8
    casing_overhang = 0.05

    prefixes = ["p", "n", "μ", "m", "", "k"]
    units = ["g", "s", "m"]

    whiten(c, fraction) = Colors.weighted_color_mean(fraction, c, colorant"white")

    sector_width = 360 / length(prefixes)

    for i in 1:3
        color = colors[i]

        r1 = inner_radius + (i-1) * ringgap + sum(ring_thicknesses[1:i-1], init = 0.0)
        r2 = r1 + ring_thicknesses[i]

        for (j, pref) in enumerate(prefixes)

            ang = offsets_deg[i] + ((j - 1) / length(prefixes) * 360)

            angstart = ang - 0.5 * sector_width
            angstop = ang + 0.5 * sector_width

            sethue(iseven(j) ? colors[i] : whiten(colors[i], 1 - sector_whiten))
            sector(O, r1, r2, deg2rad(angstart), deg2rad(angstop), action = :fill)

            radius = 0.5 * (r1 + r2)
            center = Point(reverse(sincosd(ang))) * radius
            sethue("white")
            s = pref * units[i]
            @show s
            
            # textcurvecentered is buggy when scaling is active
            @layer begin
                scale(1/circle_radius, 1/circle_radius)
                fs = 25
                fontsize(fs)
                fontface("Helvetica Bold")
                textcurvecentered(s, deg2rad(ang), radius * circle_radius, O, letter_spacing = 0, baselineshift = -0.3 * fs)
            end
        end
    end

    r1 = inner_radius - casing_overhang
    r2 = r1 + sum(ring_thicknesses) + ((length(ring_thicknesses) - 1) * ringgap) + (2 * casing_overhang)
    sethue(Luxor.julia_blue)
    setopacity(casing_opacity)
    sector(O, r1, r2, deg2rad(270 + 0.5 * sector_width), deg2rad(270 - 0.5 * sector_width), action = :fill)
end image_width image_width
image

CGS units type / other type systems

Some domains use non-SI unit systems. For example, astronomers like to use CGS units (also known as Gaussian units). Although CGS units are falling out of use (and have been dead in most fields for a long time) it could be nice if these other domains could use DynamicQuantities.jl.

One should generally not mix cgs units with SI because:

  1. There are only 3 base dimensions: length, mass, and time. All others (even charge) are derived based on these.
  2. Some equations need to be rewritten. i.e., you could not write one version of Maxwell's equations and have it work for both SI units and cgs units – there is an extra $4\pi$ factor in the cgs version.

I think all that would be required is a CGSQuantity <: AbstractQuantity and CGSDimensions <: AbstractQuantity (requiring #24), with one fewer dimension.

However, maybe it's just easier to define erg as one of the available units, and be clear that the SI convention is used. We could even throw a warning when someone uses erg – letting them know that it is being converted to the SI equivalent.

Radian/Steradian

Radians and steradians are dimensionless, but really should be treated like units. So far they have been excluded from the library (along with their derived units, like lumens and lux), but it could be useful to include them somehow. Maybe once #24 merges, there could be an alternate RadQuantity, RadDimensions objects for propagating these units, with promotion properties from regular Quantity objects so the units submodule can be used as normal.

Choosing type of quantity value based on type used in construction

Arguably, when we do 1.0f0u"m", the floating point backing type should remain Float32 rather than being converted to Float64:

julia> (1.0f0u"m").value
1.0

In general, there's a question of what type we should choose for the quantity value. In theory, we could even keep integers as integers, i.e. 1u"m" would remain 1 m. But perhaps we would want to enforce floating point, in which case something like float(T) might make sense.

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

Adding "missing" units

Symbolic units have milligrams and micrograms, but for example not nanograms.

julia> us"mg"
1.0 mg

julia> us"ng"
ERROR: LoadError: UndefVarError: `ng` not defined
Stacktrace:
 [1] top-level scope
   @ :0
 [2] eval
   @ ./boot.jl:370 [inlined]
 [3] eval
   @ ~/.julia/packages/DynamicQuantities/ZX9NZ/src/symbolic_dimensions.jl:266 [inlined]
 [4] sym_uparse(raw_string::String)
   @ DynamicQuantities.SymbolicUnitsParse ~/.julia/packages/DynamicQuantities/ZX9NZ/src/symbolic_dimensions.jl:340
 [5] var"@us_str"(__source__::LineNumberNode, __module__::Module, s::Any)
   @ DynamicQuantities ~/.julia/packages/DynamicQuantities/ZX9NZ/src/symbolic_dimensions.jl:367
in expression starting at REPL[5]:

I could not determine from the docs if this system is extensible, or whether one would have to define their own units to add nanograms (and how to do that).

Behavior with `missing`

When trying to integrate DynamicQuantities support into an existing code base, I noticed that one difficulty came from its different behavior regarding missing.

julia> missing * DynamicQuantities.us"hr"
(missing) hr

julia> missing * u"hr"
missing

The lower version is Unitful. Because missings are wrapped within Quantity in DynamicQuantities, a whole host of missing-handling functions doesn't work and quantities have to be unwrapped manually. As the simplest example, ismissing(missing * DynamicQuantities.us"hr") === false. What was the rationale for this behavior, could it still be changed?

Apparent Issue with Precompilation for Dependent Packages

Edit: Actually, this issue happens even if I don't use Revise.jl! I think the @eval line somehow breaks precompilation?

I'm developing a new package, AstrophysicalCalculations, which depends on DynamicQuantities, and I'm using Revise to assist with that development. When I enter using Revise, AstrophysicalCalculations, I get the error shown below. I believe that the @eval line in symbolic_dimensions.jl:215 makes using Revise impossible for dependent packages. I could be wrong / using this incorrectly, and this issue may be unresolvable, but I wanted to post this issue for awareness. Thank you for DynamicQuantities.jl!

The line where my package fails: src/src/StellarObservations.jl:111.

ERROR: LoadError: Evaluation into the closed module `SymbolicUnitsParse` breaks incremental compilation because the side effects will not be permanent. This is likely due to some other module mutating `SymbolicUnitsParse` with `eval` during precompilation - don't do this.
Stacktrace:
  [1] eval
    @ ./boot.jl:370 [inlined]
  [2] (::DynamicQuantities.SymbolicUnitsParse.var"#1#2")()
    @ DynamicQuantities.SymbolicUnitsParse ~/.julia/packages/DynamicQuantities/AS62k/src/symbolic_dimensions.jl:162
  [3] lock(f::DynamicQuantities.SymbolicUnitsParse.var"#1#2", l::Base.Threads.SpinLock)
    @ Base ./lock.jl:229
  [4] _generate_unit_symbols
    @ ~/.julia/packages/DynamicQuantities/AS62k/src/symbolic_dimensions.jl:159 [inlined]
  [5] sym_uparse(raw_string::String)
    @ DynamicQuantities.SymbolicUnitsParse ~/.julia/packages/DynamicQuantities/AS62k/src/symbolic_dimensions.jl:186
  [6] var"@us_str"(__source__::LineNumberNode, __module__::Module, s::Any)
    @ DynamicQuantities ~/.julia/packages/DynamicQuantities/AS62k/src/symbolic_dimensions.jl:215
  [7] #macroexpand#63
    @ ./expr.jl:119 [inlined]
  [8] macroexpand
    @ ./expr.jl:117 [inlined]
  [9] docm(source::LineNumberNode, mod::Module, meta::Any, ex::Any, define::Bool)
    @ Base.Docs ./docs/Docs.jl:539
 [10] docm(source::LineNumberNode, mod::Module, meta::Any, ex::Any)
    @ Base.Docs ./docs/Docs.jl:539
 [11] (::DocStringExtensions.var"#35#36"{typeof(DocStringExtensions.template_hook)})(::LineNumberNode, ::Vararg{Any})
    @ DocStringExtensions ~/.julia/packages/DocStringExtensions/JVu77/src/templates.jl:11
 [12] var"@doc"(::LineNumberNode, ::Module, ::String, ::Vararg{Any})
    @ Core ./boot.jl:539
 [13] include(mod::Module, _path::String)
    @ Base ./Base.jl:457
 [14] include(x::String)
    @ AstrophysicalCalculations ~/Projects/Science/AstrophysicalCalculations.jl/src/AstrophysicalCalculations.jl:36
 [15] top-level scope
    @ ~/Projects/Science/AstrophysicalCalculations.jl/src/AstrophysicalCalculations.jl:61
 [16] include
    @ ./Base.jl:457 [inlined]
 [17] include_package_for_output(pkg::Base.PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::Vector{Pair{Base.PkgId, UInt128}}, source::Nothing)
    @ Base ./loading.jl:2049
 [18] top-level scope
    @ stdin:3
in expression starting at /Users/joey/Projects/Science/AstrophysicalCalculations.jl/src/src/StellarObservations.jl:31
in expression starting at /Users/joey/Projects/Science/AstrophysicalCalculations.jl/src/src/StellarObservations.jl:1
in expression starting at /Users/joey/Projects/Science/AstrophysicalCalculations.jl/src/AstrophysicalCalculations.jl:1
in expression starting at stdin:3
ERROR: Failed to precompile AstrophysicalCalculations [fd9cccc2-a1d0-4073-bc0f-e265a877cbf5] to "/Users/joey/.julia/compiled/v1.9/AstrophysicalCalculations/jl_DJD5DI".
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] compilecache(pkg::Base.PkgId, path::String, internal_stderr::IO, internal_stdout::IO, keep_loaded_modules::Bool)
   @ Base ./loading.jl:2300
 [3] compilecache
   @ ./loading.jl:2167 [inlined]
 [4] _require(pkg::Base.PkgId, env::String)
   @ Base ./loading.jl:1805
 [5] _require_prelocked(uuidkey::Base.PkgId, env::String)
   @ Base ./loading.jl:1660
 [6] macro expansion
   @ ./loading.jl:1648 [inlined]
 [7] macro expansion
   @ ./lock.jl:267 [inlined]
 [8] require(into::Module, mod::Symbol)
   @ Base ./loading.jl:1611

Intended behavior for dimension errors on `SymbolicDimensions`

I would like to hear the opinions of @odow @trulsf for JuMP and maybe @ChrisRackauckas or @YingboMa for ModelingToolkit.jl.

What would you prefer the behavior to be when you try to add two quantities that have incompatible SymbolicDimensions?

First, recall that SymbolicDimensions is a way to avoid automatically converting to SI base units: For example:
julia> q = 4us"kPa * Constants.c"
4.0 kPa c

julia> sqrt(q)
2.0 kPa¹ᐟ² c¹ᐟ²

Only when you call expand_units would it convert from SymbolicDimensions to Dimensions:

julia> expand_units(q)
1.199169832e12 kg s⁻³

Right now when you try to add two incompatible quantities, the behavior is:

julia> 1us"km/s" + 1us"m/s"
ERROR: DimensionError: 1.0 s⁻¹ km and 1.0 m s⁻¹ have incompatible dimensions

whereas with regular Dimensions, you get:

julia> 1u"km/s" + 1u"m/s"
1001.0 m s⁻¹

Questions for you: is this behavior good as-is? Or should there be some way to automatically convert to SI units when this happens, but stay in SymbolicDimensions (to avoid type instability)?


Another option would be to perform all computations with Dimensions, and then display by converting back to SymbolicDimensions. For example:

julia> output_units = us"km/hr";

julia> a = 15us"km/hr"; b = 30us"m/s";

julia> raw_output = expand_units(a) + expand_units(b)
34.166666666666664 m s⁻¹

julia> as_units(q, units) = Quantity(ustrip(q / expand_units(units)) * ustrip(units), dimension(units));

julia> as_units(raw_output, output_units)
122.99999999999999 km hr⁻¹

Further improvements to startup time

cc @devmotion

The current startup time is about 40 ms on v1.10. This is pretty good! For comparison, Unitful.jl is 150 ms.

But I think we can do even better. Following along with https://sciml.ai/news/2022/09/21/compile_time/, it seems like a good way for improving startup time is looking at invalidations. The current list of invalidations, on my startup file, is:

julia> using SnoopCompileCore;

julia> invalidations = @snoopr begin
           using DynamicQuantities
       end;

julia> using SnoopCompile;

julia> trees = invalidation_trees(invalidations)
12-element Vector{SnoopCompile.MethodInvalidations}:
 inserting deleteat!(t::BenchmarkTools.Trial, i) @ BenchmarkTools ~/.julia/packages/BenchmarkTools/0owsb/src/trials.jl:33 invalidated:
   mt_backedges: 1: signature Tuple{typeof(deleteat!), Any, Int64} triggered MethodInstance for DynamicQuantities.UnitsParse._generate_units_import() (0 children)

 inserting deleteat!(listStore::Gtk.GtkListStoreLeaf, index::Int64) @ Gtk ~/.julia/packages/Gtk/oo3cW/src/lists.jl:182 invalidated:
   mt_backedges: 1: signature Tuple{typeof(deleteat!), Any, Int64} triggered MethodInstance for DynamicQuantities.UnitsParse._generate_units_import() (0 children)

 inserting deleteat!(w::Gtk.GtkContainer, i::Integer) @ Gtk ~/.julia/packages/Gtk/oo3cW/src/container.jl:10 invalidated:
   mt_backedges: 1: signature Tuple{typeof(deleteat!), Any, Int64} triggered MethodInstance for DynamicQuantities.UnitsParse._generate_units_import() (0 children)

 inserting promote(x::Integer, y::F) where F<:FixedRational @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/fixed_rational.jl:86 invalidated:
   backedges: 1: superseding promote(x, y) @ Base promotion.jl:391 with MethodInstance for promote(::Int64, ::Real) (1 children)

 inserting isless(l::AbstractQuantity, r) @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/utils.jl:78 invalidated:
   mt_backedges: 1: signature Tuple{typeof(isless), Any, Char} triggered MethodInstance for <(::Any, ::Char) (1 children)

 inserting isless(l, r::AbstractQuantity) @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/utils.jl:82 invalidated:
   mt_backedges: 1: signature Tuple{typeof(isless), Char, Any} triggered MethodInstance for <(::Char, ::Any) (1 children)

 inserting convert(::Type{F}, x::Integer) where F<:FixedRational @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/fixed_rational.jl:58 invalidated:
   backedges: 1: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{T} where T<:Real, ::Int64) (2 children)

 inserting convert(::Type{I}, x::F) where {I<:Integer, F<:FixedRational} @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/fixed_rational.jl:63 invalidated:
   backedges: 1: superseding convert(::Type{T}, x::Number) where T<:Number @ Base number.jl:7 with MethodInstance for convert(::Type{Int64}, ::Real) (3 children)

 inserting promote(x::F, y::Integer) where F<:FixedRational @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/fixed_rational.jl:87 invalidated:
   backedges: 1: superseding promote(x, y) @ Base promotion.jl:391 with MethodInstance for promote(::Real, ::Int64) (1 children)
              2: superseding promote(x, y) @ Base promotion.jl:391 with MethodInstance for promote(::Any, ::Any) (14 children)

 inserting +(l, r::AbstractQuantity) @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/math.jl:32 invalidated:
   mt_backedges:  1: signature Tuple{typeof(+), Ptr{UInt8}, Any} triggered MethodInstance for Base.GMP.var"#string#4"(::Int64, ::Integer, ::typeof(string), ::BigInt) (0 children)
                  2: signature Tuple{typeof(+), Ptr{Nothing}, Any} triggered MethodInstance for Gtk.GLib.GClosureMarshal(::Ptr{Nothing}, ::Ptr{Gtk.GLib.GValue}, ::UInt32, ::Ptr{Gtk.GLib.GValue}, ::Ptr{Nothing}, ::Ptr{Nothing}) (0 children)
               ...
                 73: signature Tuple{typeof(+), Ptr{Nothing}, Any} triggered MethodInstance for Gtk.GLib._signal_connect(::Function, ::Gtk.GtkEntryLeaf, ::String, ::Bool, ::Bool, ::Nothing, ::Nothing) (31 children)

 inserting ==(l, r::AbstractQuantity) @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/utils.jl:72 invalidated:
   backedges:  1: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Core.MethodInstance, ::Any) (6 children)
               2: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Gtk.GLib.GObject, ::Any) (6 children)
               ...
              40: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Core.TypeName, ::Any) (120 children)

 inserting ==(l::AbstractQuantity, r) @ DynamicQuantities ~/Documents/DynamicQuantities.jl/src/utils.jl:73 invalidated:
   backedges: 1: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Any, ::Core.Compiler.InferenceResult) (5 children)
              2: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Any, ::Cthulhu.DInfo.DebugInfo) (6 children)
              ...
              7: superseding ==(x, y) @ Base Base.jl:159 with MethodInstance for ==(::Any, ::Gtk.GLib.GObject) (446 children)

I think many of these can be deleted, getting rid of the invalidation. Although it might make sense to wait for #49 first, since that will change how we go about this. (e.g., dispatching on Number for the == methods).

Support dispatches on `Number`, `Real`, etc.

Inspired by #40 (comment):

It's too bad I can't do

abstract type AbstractQuantity{T,D} <: T end

which would make quantity containing a Number also a type Number.

While we can't do that, a practical solution could be to get this to work on a finite set of types T, e.g. Number and Real, Perhaps wrapper types could be a nice solution here, e.g. something like

struct NumberQuantityWrapper{Q} <: Number
    quantity::Q
end

struct RealQuantityWrapper{Q} <: Real
    quantity::Q
end

const QuantityWrapper = Union{NumberQuantityWrapper, RealQuantityWrapper}
ustrip(q::QuantityWrapper) = ustrip(q.quantity)
dimension(q::QuantityWrapper) = dimension(q.quantity)

function new_quantity(::Type{Q}, l, r) where {Q<:AbstractQuantity}
    quantity = constructor_of(Q)(l, r)
    if l isa Number 
        return NumberQuantityWrapper(quantity)
    elseif l isa Real
        return RealQuantityWrapper(quantity)
    else
        return quantity
    end
end

One difficulty here is that NumberQuantityWrapper and RealQuantityWrapper no longer live in the same type hierarchy as AbstractQuantity, but I think we could come up with a clean design using union types and/or metaprogramming that loops over the wrappers for easily defining dispatch rules on quantities. What do you think @MilesCranmer?

Addendum: Although in the wrapper type approach proposed here, AbstractQuantity would again be generic, I am in support of subtyping Number as in #40 as an initial step-- as long as it does not cause any concrete immediate issues, matching Unitful seems like the right thing. This issue is about having our cake and eating it too:) (Supporting a few carefully selected generic types like Real would hopefully go a long way to increasing composability of DynamicQuantities.jl even further, as compared to Unitful.jl.)

Edit: We also have to think about the consequences of breaking the AbstractQuantity hierarchy on possible extension rules in downstream packages. Perhaps the current AbstractQuantity should become AbstractGenericQuantity, and AbstractQuantity should be exported and equal to Union{AbstractGenericQuantity, QuantityWrapper}.

Base.isapprox fails for Vector{Quantity}

Extending this from a comment in #76, I reduced the issue to a MWE. The issue seems to occur specifically when running Base.isapprox on a Vector{Quantity}.

using DynamicQuantities

a = 1.0u"m"
b = 2.0u"m"

a  b
# Works ok: returns False

[a]  [b]
#=
ERROR: Cannot create an additive identity for a `UnionAbstractQuantity` type, as the dimensions are unknown. Please use `zero(::UnionAbstractQuantity)` instead.
Stacktrace:
 [1] error(s::String)
   @ Base .\error.jl:35
 [2] zero(::Type{Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}})
   @ DynamicQuantities C:\Users\mikei\.julia\packages\DynamicQuantities\jkfTz\src\utils.jl:280
 [3] real(T::Type)
   @ Base .\complex.jl:120
 [4] rtoldefault(x::Type{Quantity{Float64, Dimensions{…}}}, y::Type{Quantity{Float64, Dimensions{…}}}, atol::Int64)
   @ Base .\floatfuncs.jl:347
 [5] isapprox(x::Vector{Quantity{Float64, Dimensions{…}}}, y::Vector{Quantity{Float64, Dimensions{…}}})
   @ LinearAlgebra C:\Users\mikei\.julia\juliaup\julia-1.10.0+0.x64.w64.mingw32\share\julia\stdlib\v1.10\LinearAlgebra\src\generic.jl:1785
 [6] top-level scope
   @ REPL[8]:1
=#

u"Ω" errors in precompilation

MWE: write the literalu"Ω" in a package that precompiles.

caused by: LoadError: Evaluation into the closed module `UnitsParse` breaks incremental compilation because the side effects will not be permanent. This is likely due to some other module mutating `UnitsParse` with `eval` during precompilation - don't do this.
Stacktrace:
  [1] eval
    @ Core ./boot.jl:385 [inlined]
  [2] eval
    @ DynamicQuantities.UnitsParse ~/.julia/packages/DynamicQuantities/5QflN/src/uparse.jl:1 [inlined]
  [3] uparse(s::String)
    @ DynamicQuantities.UnitsParse ~/.julia/packages/DynamicQuantities/5QflN/src/uparse.jl:37
  [4] var"@u_str"(__source__::LineNumberNode, __module__::Module, s::Any)
    @ DynamicQuantities.UnitsParse ~/.julia/packages/DynamicQuantities/5QflN/src/uparse.jl:57
...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.