|
| 1 | +import Compat: allequal |
| 2 | + |
| 3 | +""" |
| 4 | + QuantityArray{T,N,D<:AbstractDimensions,Q<:AbstractQuantity,V<:AbstractArray} |
| 5 | +
|
| 6 | +An array of quantities with value `value` of type `V` and dimensions `dimensions` of type `D` |
| 7 | +(which are shared across all elements of the array). This is a subtype of `AbstractArray{Q,N}`, |
| 8 | +and so can be used in most places where a normal array would be used, including broadcasting operations. |
| 9 | +
|
| 10 | +# Fields |
| 11 | +
|
| 12 | +- `value`: The underlying array of values. Access with `ustrip(a)`. |
| 13 | +- `dimensions`: The dimensions of the array. Access with `dimension(a)`. |
| 14 | +
|
| 15 | +# Constructors |
| 16 | +
|
| 17 | +- `QuantityArray(value::AbstractArray, dimensions::AbstractDimensions)`: Create a `QuantityArray` with value `value` and dimensions `dimensions`. |
| 18 | +- `QuantityArray(value::AbstractArray, quantity::Quantity)`: Create a `QuantityArray` with value `value` and dimensions inferred |
| 19 | + with `dimension(quantity)`. This is so that you can easily create an array with the units module, like so: |
| 20 | + ```julia |
| 21 | + julia> A = QuantityArray(randn(32), 1u"m") |
| 22 | + ``` |
| 23 | +- `QuantityArray(v::AbstractArray{<:AbstractQuantity})`: Create a `QuantityArray` from an array of quantities. This means the following |
| 24 | + syntax works: |
| 25 | + ```julia |
| 26 | + julia> A = QuantityArray(randn(32) .* 1u"km/s") |
| 27 | + ``` |
| 28 | +""" |
| 29 | +struct QuantityArray{T,N,D<:AbstractDimensions,Q<:AbstractQuantity{T,D},V<:AbstractArray{T,N}} <: AbstractArray{Q,N} |
| 30 | + value::V |
| 31 | + dimensions::D |
| 32 | + |
| 33 | + function QuantityArray(v::_V, d::_D, ::Type{_Q}) where {_T,_N,_D<:AbstractDimensions,_Q<:AbstractQuantity,_V<:AbstractArray{_T,_N}} |
| 34 | + Q_out = constructor_of(_Q){_T,_D} |
| 35 | + return new{_T,_N,_D,Q_out,_V}(v, d) |
| 36 | + end |
| 37 | +end |
| 38 | + |
| 39 | +# Construct with a Quantity (easier, as you can use the units): |
| 40 | +QuantityArray(v::AbstractArray; kws...) = QuantityArray(v, DEFAULT_DIM_TYPE(; kws...)) |
| 41 | +QuantityArray(v::AbstractArray, d::AbstractDimensions) = QuantityArray(v, d, Quantity) |
| 42 | +QuantityArray(v::AbstractArray, q::AbstractQuantity) = QuantityArray(v .* ustrip(q), dimension(q), typeof(q)) |
| 43 | +QuantityArray(v::QA) where {Q<:AbstractQuantity,QA<:AbstractArray{Q}} = |
| 44 | + let |
| 45 | + allequal(dimension.(v)) || throw(DimensionError(first(v), v)) |
| 46 | + QuantityArray(ustrip.(v), dimension(first(v)), Q) |
| 47 | + end |
| 48 | + |
| 49 | +function Base.promote_rule(::Type{QA1}, ::Type{QA2}) where {QA1<:QuantityArray,QA2<:QuantityArray} |
| 50 | + D = promote_type(dim_type.((QA1, QA2))...) |
| 51 | + Q = promote_type(quantity_type.((QA1, QA2))...) |
| 52 | + T = promote_type(value_type.((QA1, QA2))...) |
| 53 | + V = promote_type(array_type.((QA1, QA2))...) |
| 54 | + N = ndims(QA1) |
| 55 | + |
| 56 | + @assert( |
| 57 | + N == ndims(QA2), |
| 58 | + "Cannot promote quantity arrays with different dimensions." |
| 59 | + ) |
| 60 | + @assert( |
| 61 | + Q <: AbstractQuantity{T,D} && V <: AbstractArray{T}, |
| 62 | + "Incompatible promotion rules between\n $(QA1)\nand\n $(QA2)\nPlease convert to a common quantity type first." |
| 63 | + ) |
| 64 | + |
| 65 | + return QuantityArray{T,N,D,Q,V} |
| 66 | +end |
| 67 | + |
| 68 | +function Base.convert(::Type{QA}, A::QA) where {QA<:QuantityArray} |
| 69 | + return A |
| 70 | +end |
| 71 | +function Base.convert(::Type{QA1}, A::QA2) where {QA1<:QuantityArray,QA2<:QuantityArray} |
| 72 | + Q = quantity_type(QA1) |
| 73 | + V = array_type(QA1) |
| 74 | + N = ndims(QA1) |
| 75 | + |
| 76 | + raw_array = Base.Fix1(convert, Q).(A) |
| 77 | + output = QuantityArray(convert(constructor_of(V){Q,N}, raw_array)) |
| 78 | + # TODO: This will mess with static arrays |
| 79 | + |
| 80 | + return output::QA1 |
| 81 | +end |
| 82 | + |
| 83 | +@inline ustrip(A::QuantityArray) = A.value |
| 84 | +@inline dimension(A::QuantityArray) = A.dimensions |
| 85 | + |
| 86 | +array_type(::Type{<:QuantityArray{T,N,D,Q,V}}) where {T,N,D,Q,V} = V |
| 87 | +array_type(A::QuantityArray) = array_type(typeof(A)) |
| 88 | + |
| 89 | +quantity_type(::Type{<:QuantityArray{T,N,D,Q}}) where {T,N,D,Q} = Q |
| 90 | +quantity_type(A::QuantityArray) = quantity_type(typeof(A)) |
| 91 | + |
| 92 | +dim_type(::Type{<:QuantityArray{T,N,D}}) where {T,N,D} = D |
| 93 | +dim_type(A::QuantityArray) = dim_type(typeof(A)) |
| 94 | + |
| 95 | +value_type(::Type{<:AbstractQuantity{T}}) where {T} = T |
| 96 | +value_type(::Type{<:QuantityArray{T}}) where {T} = T |
| 97 | +value_type(A::Union{<:QuantityArray,<:AbstractQuantity}) = value_type(typeof(A)) |
| 98 | + |
| 99 | +# One field: |
| 100 | +for f in (:size, :length, :axes) |
| 101 | + @eval Base.$f(A::QuantityArray) = $f(ustrip(A)) |
| 102 | +end |
| 103 | + |
| 104 | +function Base.getindex(A::QuantityArray, i...) |
| 105 | + output_value = getindex(ustrip(A), i...) |
| 106 | + if isa(output_value, AbstractArray) |
| 107 | + return QuantityArray(output_value, dimension(A), quantity_type(A)) |
| 108 | + else |
| 109 | + return new_quantity(quantity_type(A), output_value, dimension(A)) |
| 110 | + end |
| 111 | +end |
| 112 | +function Base.setindex!(A::QuantityArray{T,N,D,Q}, v::Q, i...) where {T,N,D,Q<:AbstractQuantity} |
| 113 | + dimension(A) == dimension(v) || throw(DimensionError(A, v)) |
| 114 | + return unsafe_setindex!(A, v, i...) |
| 115 | +end |
| 116 | +function Base.setindex!(A::QuantityArray{T,N,D,Q}, v::AbstractQuantity, i...) where {T,N,D,Q<:AbstractQuantity} |
| 117 | + return setindex!(A, convert(Q, v), i...) |
| 118 | +end |
| 119 | + |
| 120 | +unsafe_setindex!(A, v, i...) = setindex!(ustrip(A), ustrip(v), i...) |
| 121 | + |
| 122 | +Base.IndexStyle(::Type{Q}) where {Q<:QuantityArray} = IndexStyle(array_type(Q)) |
| 123 | + |
| 124 | + |
| 125 | +Base.similar(A::QuantityArray) = QuantityArray(similar(ustrip(A)), dimension(A), quantity_type(A)) |
| 126 | +Base.similar(A::QuantityArray, ::Type{S}) where {S} = QuantityArray(similar(ustrip(A), S), dimension(A), quantity_type(A)) |
| 127 | + |
| 128 | +# Unfortunately this mess of `similar` is required to avoid ambiguous methods. |
| 129 | +# c.f. base/abstractarray.jl |
| 130 | +for dim_type in (:(Dims), :(Tuple{Union{Integer,Base.OneTo},Vararg{Union{Integer,Base.OneTo}}}), :(Tuple{Integer, Vararg{Integer}})) |
| 131 | + @eval Base.similar(A::QuantityArray, dims::$dim_type) = QuantityArray(similar(ustrip(A), dims), dimension(A), quantity_type(A)) |
| 132 | + @eval Base.similar(A::QuantityArray, ::Type{S}, dims::$dim_type) where {S} = QuantityArray(similar(ustrip(A), S, dims), dimension(A), quantity_type(A)) |
| 133 | +end |
| 134 | + |
| 135 | +Base.BroadcastStyle(::Type{QA}) where {QA<:QuantityArray} = Broadcast.ArrayStyle{QA}() |
| 136 | + |
| 137 | +function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{QA}}, ::Type{ElType}) where {QA<:QuantityArray,ElType<:AbstractQuantity} |
| 138 | + T = value_type(ElType) |
| 139 | + output_array = similar(bc, T) |
| 140 | + first_output::ElType = materialize_first(bc) |
| 141 | + return QuantityArray(output_array, dimension(first_output)::dim_type(ElType), ElType) |
| 142 | +end |
| 143 | +function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{QuantityArray{T,N,D,Q,V}}}, ::Type{ElType}) where {T,N,D,Q,V<:Array{T,N},ElType} |
| 144 | + return similar(Array{ElType}, axes(bc)) |
| 145 | +end |
| 146 | +function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{QuantityArray{T,N,D,Q,V}}}, ::Type{ElType}) where {T,N,D,Q,V,ElType} |
| 147 | + # To deal with things like StaticArrays, we need to rely on |
| 148 | + # only `similar(::Type{ArrayType}, axes)`. We can't specify the |
| 149 | + # element type in `similar` if we only give the array type. |
| 150 | + # TODO: However, this results in a redundant allocation. |
| 151 | + return (_ -> zero(ElType)).(similar(V, axes(bc))) |
| 152 | +end |
| 153 | + |
| 154 | +# Basically, we want to solve a single element to find the output dimension. |
| 155 | +# Then we can put results in the output `QuantityArray`. |
| 156 | +materialize_first(bc::Base.Broadcast.Broadcasted) = bc.f(materialize_first.(bc.args)...) |
| 157 | + |
| 158 | +# Base cases |
| 159 | +materialize_first(q::AbstractQuantity{<:AbstractArray}) = new_quantity(typeof(q), first(ustrip(q)), dimension(q)) |
| 160 | +materialize_first(q::AbstractQuantity) = q |
| 161 | +materialize_first(q::QuantityArray) = first(q) |
| 162 | +materialize_first(q::AbstractArray{Q}) where {Q<:AbstractQuantity} = first(q) |
| 163 | + |
| 164 | +# Derived calls |
| 165 | +materialize_first(r::Base.RefValue) = materialize_first(r.x) |
| 166 | +materialize_first(x::Base.Broadcast.Extruded) = materialize_first(x.x) |
| 167 | +materialize_first(args::Tuple) = materialize_first(first(args)) |
| 168 | +materialize_first(args::AbstractArray) = |
| 169 | + let |
| 170 | + length(args) >= 1 || error("Unexpected broadcast format. Please submit a bug report.") |
| 171 | + materialize_first(args[begin]) |
| 172 | + end |
| 173 | +materialize_first(::Tuple{}) = error("Unexpected broadcast format. Please submit a bug report.") |
| 174 | + |
| 175 | +# Everything else: |
| 176 | +materialize_first(x) = x |
| 177 | + |
| 178 | +function _print_array_type(io::IO, ::Type{QA}) where {QA<:QuantityArray} |
| 179 | + return print(io, "QuantityArray(::", array_type(QA), ", ::", quantity_type(QA), ")") |
| 180 | +end |
| 181 | +Base.showarg(io::IO, v::QuantityArray, _) = _print_array_type(io, typeof(v)) |
| 182 | +Base.show(io::IO, ::MIME"text/plain", ::Type{QA}) where {QA<:QuantityArray} = _print_array_type(io, QA) |
| 183 | + |
| 184 | +# Other array operations: |
| 185 | +Base.copy(A::QuantityArray) = QuantityArray(copy(ustrip(A)), copy(dimension(A)), quantity_type(A)) |
| 186 | +for f in (:cat, :hcat, :vcat) |
| 187 | + preamble = quote |
| 188 | + allequal(dimension.(A)) || throw(DimensionError(A[begin], A[begin+1:end])) |
| 189 | + A = promote(A...) |
| 190 | + dimensions = dimension(A[begin]) |
| 191 | + Q = quantity_type(A[begin]) |
| 192 | + end |
| 193 | + if f == :cat |
| 194 | + @eval function Base.$f(A::QuantityArray...; dims) |
| 195 | + $preamble |
| 196 | + return QuantityArray($f(ustrip.(A)...; dims), dimensions, Q) |
| 197 | + end |
| 198 | + else |
| 199 | + @eval function Base.$f(A::QuantityArray...) |
| 200 | + $preamble |
| 201 | + return QuantityArray($f(ustrip.(A)...), dimensions, Q) |
| 202 | + end |
| 203 | + end |
| 204 | +end |
| 205 | +Base.fill(x::AbstractQuantity, dims::Dims...) = QuantityArray(fill(ustrip(x), dims...), dimension(x), typeof(x)) |
| 206 | +Base.fill(x::AbstractQuantity, t::Tuple{}) = QuantityArray(fill(ustrip(x), t), dimension(x), typeof(x)) |
| 207 | + |
| 208 | +ulength(q::QuantityArray) = ulength(dimension(q)) |
| 209 | +umass(q::QuantityArray) = umass(dimension(q)) |
| 210 | +utime(q::QuantityArray) = utime(dimension(q)) |
| 211 | +ucurrent(q::QuantityArray) = ucurrent(dimension(q)) |
| 212 | +utemperature(q::QuantityArray) = utemperature(dimension(q)) |
| 213 | +uluminosity(q::QuantityArray) = uluminosity(dimension(q)) |
| 214 | +uamount(q::QuantityArray) = uamount(dimension(q)) |
0 commit comments