diff --git a/README.md b/README.md index f2d7685a..6d33571f 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,33 @@ julia> @btime $f.(qa) setup=(xa = randn(100000) .* u"km/s"; qa = QuantityArray(x So we can see the `QuantityArray` version saves on both time and memory. +By default, DynamicQuantities will create a `QuantityArray` from an `AbstractArray`, similarly to how a `Quantity` is created from a scalar in the [Usage](@ref) examples: + +```julia +julia> x = [0.3, 0.4, 0.5]u"km/s" +3-element QuantityArray(::Vector{Float64}, ::Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}): + 300.0 m s⁻¹ + 400.0 m s⁻¹ + 500.0 m s⁻¹ + +julia> y = (42:45) * u"kg" +4-element QuantityArray(::StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}, ::Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}): + 42.0 kg + 43.0 kg + 44.0 kg + 45.0 kg +``` + +This can be overridden to produce a vector of `Quantity`s by explicitly broadcasting the unit: + +```julia +julia> z = [0.3, 0.4, 0.5] .* u"km/s" +3-element Vector{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}}: + 300.0 m s⁻¹ + 400.0 m s⁻¹ + 500.0 m s⁻¹ +``` + ### Unitful DynamicQuantities allows you to convert back and forth from Unitful.jl: diff --git a/docs/src/examples.md b/docs/src/examples.md index e5dc7d1f..19b5a72c 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -238,6 +238,7 @@ the same dimension) by passing an array and a single quantity: ```julia x = QuantityArray(randn(32), u"km/s") +# or x = randn(32)u"km/s" ``` or, by passing an array of individual quantities: @@ -273,6 +274,8 @@ f_square(v) = v^2 * 1.5 - v^2 println("Applying function to y_q: ", sum(f_square.(y_q))) ``` +See [Home > Arrays](index.md#Arrays) for more. + ### Fill We can also make `QuantityArray` using `fill`: diff --git a/ext/DynamicQuantitiesLinearAlgebraExt.jl b/ext/DynamicQuantitiesLinearAlgebraExt.jl index 1b2b5699..08d37c12 100644 --- a/ext/DynamicQuantitiesLinearAlgebraExt.jl +++ b/ext/DynamicQuantitiesLinearAlgebraExt.jl @@ -2,7 +2,7 @@ module DynamicQuantitiesLinearAlgebraExt using LinearAlgebra: LinearAlgebra as LA using DynamicQuantities -using DynamicQuantities: DynamicQuantities as DQ, quantity_type, new_quantity, DimensionError +using DynamicQuantities: DynamicQuantities as DQ, quantity_type, new_quantity, DimensionError, ABSTRACT_QUANTITY_TYPES using TestItems: @testitem DQ.is_ext_loaded(::Val{:LinearAlgebra}) = true @@ -35,6 +35,30 @@ for op in (:(Base.:*), :(Base.:/), :(Base.:\)), @eval $op(l::$L, r::$R) = DQ.array_op($op, l, r) end +for ARRAY_TYPE in ( + LA.Bidiagonal, + LA.Diagonal, + LA.Hermitian, + LA.LowerTriangular, + LA.LowerTriangular{<:Any, <:Union{LA.Adjoint{<:Any, <:StridedMatrix{T}}, LA.Transpose{<:Any, <:StridedMatrix{T}}, StridedArray{T, 2}} where T}, + LA.Symmetric, + LA.SymTridiagonal, + LA.Tridiagonal, + LA.UnitLowerTriangular, + LA.UnitUpperTriangular, + LA.UpperTriangular, + LA.UpperTriangular{<:Any, <:Union{LA.Adjoint{<:Any, <:StridedMatrix{T}}, LA.Transpose{<:Any, <:StridedMatrix{T}}, StridedArray{T, 2}} where T}, + LA.UpperHessenberg, + ), + (type, base_type, ) in ABSTRACT_QUANTITY_TYPES + + @eval begin + Base.:*(A::$ARRAY_TYPE, q::$type) = QuantityArray(A, q) + Base.:*(q::$type, A::$ARRAY_TYPE) = QuantityArray(A, q) + Base.:/(A::$ARRAY_TYPE, q::$type) = A * inv(q) + end +end + function Base.:*( l::LA.Transpose{Q,<:AbstractVector}, r::DQ.QuantityArray{T2,1,D,Q,<:AbstractVector{T2}} diff --git a/src/arrays.jl b/src/arrays.jl index 53a1522d..984584d1 100644 --- a/src/arrays.jl +++ b/src/arrays.jl @@ -14,6 +14,14 @@ and so can be used in most places where a normal array would be used, including # Constructors +The most convenient way to create a `QuantityArray` is by multiplying your array-like object by the desired units, e.g., + +```julia +x = [3, 4, 5]u"km/s" +``` + +For more control, the following constructors are available: + - `QuantityArray(v::AbstractArray, d::AbstractDimensions)`: Create a `QuantityArray` with value `v` and dimensions `d`, using `Quantity` if the eltype of `v` is numeric, and `GenericQuantity` otherwise. @@ -71,6 +79,12 @@ for (type, base_type, default_type) in ABSTRACT_QUANTITY_TYPES if type in (AbstractQuantity, AbstractGenericQuantity) @eval QuantityArray(v::AbstractArray{<:$base_type}, d::AbstractDimensions) = QuantityArray(v, d, $default_type) end + + @eval begin + Base.:*(A::AbstractArray{T}, q::$type) where {T<:Number} = QuantityArray(A, q) + Base.:*(q::$type, A::AbstractArray{T}) where {T<:Number} = A * q + Base.:/(A::AbstractArray{T}, q::$type) where {T<:Number} = A * inv(q) + end end QuantityArray(v::QA) where {Q<:UnionAbstractQuantity,QA<:AbstractArray{Q}} = let diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 7896c555..7d822b91 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -110,3 +110,9 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES, numeric_type in (Bool, BigFloat) end end end +# Cover method ambiguities from, e.g., op(::Array, ::Quantity)::QuantityArray` +Base.:*(A::StepRangeLen{<:Real, <:Base.TwicePrecision}, q::AbstractRealQuantity) = QuantityArray(A, q) +Base.:*(q::AbstractRealQuantity, A::StepRangeLen{<:Real, <:Base.TwicePrecision}) = A * q +Base.:/(A::BitArray, q::AbstractRealQuantity) = A * inv(q) +Base.:/(A::BitArray, q::AbstractQuantity) = A * inv(q) +Base.:/(A::StepRangeLen{<:Real, <:Base.TwicePrecision}, q::AbstractRealQuantity) = A * inv(q) diff --git a/test/unittests.jl b/test/unittests.jl index 189dab09..ebf0a548 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -265,6 +265,7 @@ end end @testset "Arrays" begin + T_QA_GenericQuantity(T, N) = QuantityArray{T, N, D, Q, V} where {T, D<:Dimensions, Q<:UnionAbstractQuantity, V<:AbstractArray{T, N}} for T in [Float16, Float32, Float64], R in [Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] D = Dimensions{R} @@ -288,9 +289,9 @@ end @test ustrip(x + ones(T, 32))[32] == 2 @test typeof(x + ones(T, 32)) <: GenericQuantity{Vector{T}} @test typeof(x - ones(T, 32)) <: GenericQuantity{Vector{T}} - @test typeof(ones(T, 32) * GenericQuantity(T(1), D, length=1)) <: GenericQuantity{Vector{T}} - @test typeof(ones(T, 32) / GenericQuantity(T(1), D, length=1)) <: GenericQuantity{Vector{T}} - @test ones(T, 32) / GenericQuantity(T(1), length=1) == GenericQuantity(ones(T, 32), length=-1) + @test typeof(ones(T, 32) * GenericQuantity(T(1), D, length=1)) <: T_QA_GenericQuantity(T, 1) + @test typeof(ones(T, 32) / GenericQuantity(T(1), D, length=1)) <: T_QA_GenericQuantity(T, 1) + @test ones(T, 32) / GenericQuantity(T(1), length=1) == QuantityArray(ones(T, 32), GenericQuantity(T(1), length=-1)) end @testset "isapprox" begin @@ -351,6 +352,14 @@ end @test norm(GenericQuantity(ustrip.(x), length=1, time=-1), 2) ≈ norm(ustrip.(x), 2) * u"m/s" @test ustrip(x') == ustrip(x)' + + # With BitArray and RealQuantity: + T_QA_AbstractArray = QuantityArray{T, 2, D, Q, V} where {T, D<:Dimensions, Q<:UnionAbstractQuantity, V<:AbstractArray} + T_QA_s_AbstractArray = QuantityArray{T, 2, D, Q, V} where {T, D<:SymbolicDimensions, Q<:UnionAbstractQuantity, V<:AbstractArray} + + @test BitArray([1 0 1 0]) / u"m" isa T_QA_AbstractArray + @test BitArray([1 0 1 0]) / us"m" isa T_QA_s_AbstractArray + @test BitArray([1 0 1 0]) / RealQuantity(u"m") isa T_QA_AbstractArray end @testset "Ranges" begin @@ -395,9 +404,11 @@ end end @testset "Multiplying ranges with units" begin + T_QA_StepRangeLen = QuantityArray{T, 1, D, Q, V} where {T, D<:Dimensions, Q<:UnionAbstractQuantity, V<:StepRangeLen} + T_QA_s_StepRangeLen = QuantityArray{T, 1, D, Q, V} where {T, D<:SymbolicDimensions, Q<:UnionAbstractQuantity, V<:StepRangeLen} # Test multiplying ranges with units x = (1:0.25:4)u"inch" - @test x isa StepRangeLen + @test x isa T_QA_StepRangeLen @test first(x) == 1u"inch" @test x[2] == 1.25u"inch" @test last(x) == 4u"inch" @@ -405,7 +416,7 @@ end # Integer range (but real-valued unit) x = (1:4)u"inch" - @test x isa StepRangeLen + @test x isa T_QA_StepRangeLen @test first(x) == 1u"inch" @test x[2] == 2u"inch" @test last(x) == 4u"inch" @@ -414,7 +425,7 @@ end # Test with floating point range x = (1.0:0.5:3.0)u"m" - @test x isa StepRangeLen + @test x isa T_QA_StepRangeLen @test first(x) == 1.0u"m" @test x[2] == 1.5u"m" @test last(x) == 3.0u"m" @@ -427,7 +438,7 @@ end # Test with symbolic units x = (1:0.25:4)us"inch" - @test x isa StepRangeLen{<:Quantity{Float64,<:SymbolicDimensions}} + @test x isa T_QA_s_StepRangeLen @test first(x) == us"inch" @test x[2] == 1.25us"inch" @test last(x) == 4us"inch" @@ -435,7 +446,7 @@ end # Test that symbolic units preserve their symbolic nature x = (0:0.1:1)us"km/h" - @test x isa AbstractRange + @test x isa QuantityArray @test first(x) == 0us"km/h" @test x[2] == 0.1us"km/h" @test last(x) == 1us"km/h" @@ -443,7 +454,7 @@ end # Similarly, integers should stay integers: x = (1:4)us"inch" - @test_skip x isa StepRangeLen{<:Quantity{Int64,<:SymbolicDimensions}} + @test_skip x isa T_QA_s_StepRangeLen @test first(x) == us"inch" @test x[2] == 2us"inch" @test last(x) == 4us"inch" @@ -453,6 +464,13 @@ end @test_skip (1.0:4.0) * RealQuantity(u"inch") isa StepRangeLen{<:RealQuantity{Float64,<:SymbolicDimensions}} # TODO: This is not available as TwicePrecision interacts with Real in a way # that demands many other functions to be defined. + @test (1.0:4.0) / RealQuantity(u"inch") isa T_QA_StepRangeLen + @test (1.0:4.0) * RealQuantity(u"inch") isa T_QA_StepRangeLen + @test RealQuantity(u"inch") * (1.0:4.0) isa T_QA_StepRangeLen + + @test (1.0:4.0) / RealQuantity(us"inch") isa T_QA_s_StepRangeLen + @test (1.0:4.0) * RealQuantity(us"inch") isa T_QA_s_StepRangeLen + @test RealQuantity(us"inch") * (1.0:4.0) isa T_QA_s_StepRangeLen end end