symbolicml / dynamicquantities.jl Goto Github PK
View Code? Open in Web Editor NEWEfficient and type-stable physical quantities in Julia
Home Page: https://symbolicml.org/DynamicQuantities.jl/dev/
License: Apache License 2.0
Efficient and type-stable physical quantities in Julia
Home Page: https://symbolicml.org/DynamicQuantities.jl/dev/
License: Apache License 2.0
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?
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.
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.
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
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:
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.
I'm building a utility to convert arbitrary Units.jl objects into DynamicQuantities, becuase I can't use DynamicQuantities to handle affine units like Fahrenheit or psig. However, when converting these units, I ran into some serious performance issues. When looking at @profview results, nearly all the samples fell into some form of the validate_upreferred() function. Looking at how this function is implemented, it's easy to see why this would be slow, and why validating it for every single call would be cumbersome. Is there any way we can opt out of this behaviour?
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!
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).
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.
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.
Would be good to evaluate performance tradeoff of this. Could even store a Bool for each dimension. It might be faster to avoid updating a 0 dimension entirely.
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?
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.
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)):
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.
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)
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.
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 m³
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.
Hi All,
Thanks for the great package!
I'm not sure if this behavior is expected, but it surprised me, so I thought I'd ask.
s = Set()
push!(s, 1us"day") # -> Set with 1 element
push!(s, 1us"day") # -> Set with 2 elements
I would have expected the set to only to have 1 element, because:
isequal(1us"day", 1us"day") # -> true
Am I mistaken in my intuition here?
Thanks for your time and work.
Cheers,
Max
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
)
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?
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
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
=#
With Unitful.jl, I can do
using Unitful: m, s
v = 3m/s
It would be great for feature-parity with Unitful if one could do
using DynamicQuantities: m, s
v = 3m/s
too, right?
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.
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
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.
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 missing
s 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?
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?
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 sum
ing 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 sum
ing 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
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
?
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⁻¹
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).
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.
Inspired by #40 (comment):
It's too bad I can't do
abstract type AbstractQuantity{T,D} <: T endwhich would make quantity containing a
Number
also a typeNumber
.
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}
.
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.
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?
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
If t
is a Quantity, the dimension is present in zero(t) but not in one(t) as in the following example:
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.3 (2024-04-30)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using DynamicQuantities
julia> t=1u"m"
1.0 m
julia> one(t)
1.0
julia> zero(t)
0.0 m
julia> oneunit(t)
1.0 m
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?
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
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...
Hi,
I am writing a paper and I reference DynamicQuantities.jl so I am wondering what would be the best way to cite the packge!
Thanks.
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
...
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
.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.