Skip to content

Commit c307cac

Browse files
authored
Merge pull request #33 from SymbolicML/arrays-2
Create `QuantityArray <: AbstractArray`
2 parents 1d62cec + c1bdf62 commit c307cac

13 files changed

+732
-52
lines changed

Project.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
name = "DynamicQuantities"
22
uuid = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
33
authors = ["MilesCranmer <[email protected]> and contributors"]
4-
version = "0.6.4"
4+
version = "0.7.0"
55

66
[deps]
77
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
8-
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
98
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
109
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
10+
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
1111
Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775"
1212

1313
[weakdeps]
14+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1415
ScientificTypes = "321657f4-b219-11e9-178b-2701a2544e81"
1516
ScientificTypesBase = "30f210dd-8aff-4c5f-94ba-8e64358c1161"
1617
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
1718

1819
[extensions]
20+
DynamicQuantitiesLinearAlgebraExt = "LinearAlgebra"
1921
DynamicQuantitiesScientificTypesExt = ["ScientificTypes", "ScientificTypesBase"]
2022
DynamicQuantitiesUnitfulExt = "Unitful"
2123

@@ -30,13 +32,15 @@ julia = "1.6"
3032

3133
[extras]
3234
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
35+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
3336
Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
3437
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
3538
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"
3639
ScientificTypes = "321657f4-b219-11e9-178b-2701a2544e81"
3740
ScientificTypesBase = "30f210dd-8aff-4c5f-94ba-8e64358c1161"
41+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
3842
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3943
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
4044

4145
[targets]
42-
test = ["Test", "Aqua", "Ratios", "SaferIntegers", "SafeTestsets", "ScientificTypes", "ScientificTypesBase", "Unitful"]
46+
test = ["Test", "Aqua", "LinearAlgebra", "Ratios", "SaferIntegers", "SafeTestsets", "ScientificTypes", "ScientificTypesBase", "StaticArrays", "Unitful"]

benchmark/benchmarks.jl

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ using DynamicQuantities
33

44
const SUITE = BenchmarkGroup()
55

6-
SUITE["creation"] = let s = BenchmarkGroup()
6+
SUITE["Quantity"] = BenchmarkGroup()
7+
8+
SUITE["Quantity"]["creation"] = let s = BenchmarkGroup()
79
s["Quantity(x)"] = @benchmarkable Quantity(x) setup = (x = randn()) evals = 1000
810
s["Quantity(x, length=y)"] = @benchmarkable Quantity(x, length=y) setup = (x = randn(); y = rand(1:5)) evals = 1000
911
s
1012
end
1113

1214
default() = Quantity(rand(), length=rand(1:5), mass=rand(1:5) // 2)
1315

14-
SUITE["with_numbers"] = let s = BenchmarkGroup()
16+
SUITE["Quantity"]["with_numbers"] = let s = BenchmarkGroup()
1517
f1(x, i) = x^i
1618
s["^int"] = @benchmarkable $f1(x, i) setup = (x = default(); i = rand(1:5)) evals = 1000
1719
f2(x, y) = x * y
@@ -21,7 +23,7 @@ SUITE["with_numbers"] = let s = BenchmarkGroup()
2123
s
2224
end
2325

24-
SUITE["with_self"] = let s = BenchmarkGroup()
26+
SUITE["Quantity"]["with_self"] = let s = BenchmarkGroup()
2527
f4(x) = inv(x)
2628
s["inv"] = @benchmarkable $f4(x) setup = (x = default()) evals = 1000
2729
f7(x) = ustrip(x)
@@ -31,10 +33,31 @@ SUITE["with_self"] = let s = BenchmarkGroup()
3133
s
3234
end
3335

34-
SUITE["with_quantity"] = let s = BenchmarkGroup()
36+
SUITE["Quantity"]["with_quantity"] = let s = BenchmarkGroup()
3537
f5(x, y) = x / y
3638
s["/y"] = @benchmarkable $f5(x, y) setup = (x = default(); y = default()) evals = 1000
3739
f6(x, y) = x + y
3840
s["+y"] = @benchmarkable $f6(x, y) setup = (x = default(); y = x + rand() * x) evals = 1000
3941
s
4042
end
43+
44+
if @isdefined QuantityArray
45+
SUITE["QuantityArray"] = BenchmarkGroup()
46+
47+
SUITE["QuantityArray"]["broadcasting"] = let s = BenchmarkGroup()
48+
N = 10000
49+
f9(x) = x^2
50+
s["x^2_normal_array"] = @benchmarkable $f9.(arr) setup = (arr = randn($N))
51+
s["x^2_quantity_array"] = @benchmarkable $f9.(arr) setup = (arr = QuantityArray(randn($N), u"km/s"))
52+
s["x^2_array_of_quantities"] = @benchmarkable $f9.(arr) setup = (arr = randn($N) .* u"km/s")
53+
f10(x) = x^4
54+
s["x^4_normal_array"] = @benchmarkable $f10.(arr) setup = (arr = randn($N))
55+
s["x^4_quantity_array"] = @benchmarkable $f10.(arr) setup = (arr = QuantityArray(randn($N), u"km/s"))
56+
s["x^4_array_of_quantities"] = @benchmarkable $f10.(arr) setup = (arr = randn($N) .* u"km/s")
57+
f11(x) = x^4 * 0.9 - x * x / 0.3 * x * 0.9 * x
58+
s["multi_normal_array"] = @benchmarkable $f11.(arr) setup = (arr = randn($N))
59+
s["multi_quantity_array"] = @benchmarkable $f11.(arr) setup = (arr = QuantityArray(randn($N), u"km/s"))
60+
s["multi_array_of_quantities"] = @benchmarkable $f11.(arr) setup = (arr = randn($N) .* u"km/s")
61+
s
62+
end
63+
end

docs/src/types.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@ AbstractQuantity
1717
Note also that the `Quantity` object can take a custom `AbstractDimensions`
1818
as input, so there is often no need to subtype `AbstractQuantity` separately.
1919

20+
## Symbolic dimensions
21+
2022
Another type which subtypes `AbstractDimensions` is `SymbolicDimensions`:
2123

2224
```@docs
2325
SymbolicDimensions
2426
```
27+
28+
## Arrays
29+
30+
```@docs
31+
QuantityArray
32+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module DynamicQuantitiesLinearAlgebraExt
2+
3+
import LinearAlgebra: norm
4+
import DynamicQuantities: AbstractQuantity, ustrip, dimension, new_quantity
5+
6+
norm(q::AbstractQuantity, p::Real=2) = new_quantity(typeof(q), norm(ustrip(q), p), dimension(q))
7+
8+
end

src/DynamicQuantities.jl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
module DynamicQuantities
22

3+
import TOML: parsefile
4+
5+
const PACKAGE_VERSION = try
6+
let project = parsefile(joinpath(pkgdir(@__MODULE__), "Project.toml"))
7+
VersionNumber(project["version"])
8+
end
9+
catch
10+
VersionNumber(0, 0, 0)
11+
end
12+
313
export Units, Constants
414
export AbstractQuantity, AbstractDimensions
5-
export Quantity, Dimensions, SymbolicDimensions, DimensionError
15+
export Quantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError
616
export ustrip, dimension
717
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
818
export uparse, @u_str, sym_uparse, @us_str, expand_units
@@ -11,6 +21,7 @@ include("fixed_rational.jl")
1121
include("types.jl")
1222
include("utils.jl")
1323
include("math.jl")
24+
include("arrays.jl")
1425
include("units.jl")
1526
include("constants.jl")
1627
include("uparse.jl")

src/arrays.jl

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)