WIP: Implement broadcasting with AxisArrays on Julia 0.7 #131
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR proposes an implementation of broadcasting for AxisArrays that will be possible using Julia 0.7. I'm getting a bit ahead of myself because not all AxisArrays tests pass on 0.7, and I'm also aware that the new broadcasting API may continue to change (e.g. JuliaLang/julia#25377). However, broadcasting is important enough for how I intend to use AxisArrays that I want to give an early demo, and also want to solicit some feedback before I sink too much time into this approach.
High-level description
Axis{:row}(Base.OneTo(1))
in a 1xN matrix.Algorithm description
The following discussion relies upon understanding the new broadcasting API described in the interfaces section of the latest Julia docs. Broadcasting is intercepted after styles are combined, but before eltypes and indices are computed.
combine_indices
from any AxisArrays (but not other kinds of arrays) in the broadcasting operation. AxisArray axis names and values are returned from a newbroadcast_indices
method. As currently implemented, this demands exact equality of axis values, so tiny floating-point differences count. This returns a tuple of AxisArrays.Axis that we'll callaxesAs
.Provided that was successful, do broadcasting over all broadcast args using the underlying arrays (
array.data
ifarray
is an AxisArray). Call the resultbroadcasted
.Compare the axes in
axesAs
with thedefault_axes
forbroadcasted
(which is not an AxisArray). We'll call the tuple of default axesdefaxesBs
. Note thatlength(axesAs) <= length(defaxesBs)
. Process these two tuplesaxesAs
anddefaxesBs
taking pairs of elementsaxA
,axB
from each usingBase.tail
, etc.3a. If the axis names match, then you need to see if you believe the axis from
axA
was originally a default axis. This PR makes the decision that if you have an axis likeAxis{:row, <:Base.OneTo}
, then it was a default axis. If so, return e.g.Axis{:row}(Base.OneTo(length(axB))
so that you resize the default axis to match the size required forbroadcasted
. If the values are not from Base.OneTo then it is not a default axis, and the arrays cannot be broadcasted.3b. If the axis names don't match, then there's no need to worry about default axes, just return
axA
.broadcasted
into an AxisArray using the axes obtained from step 3. The number of axes you obtain from step 3 may be less than the number of dimensions ofbroadcasted
, in which case the AxisArray constructor will usedefault_axes
for the remainder.Examples
See
test/broadcast.jl
, more tests/examples to come.Relation to previous AxisArrays.jl issues and PRs concerning broadcasting
Issue 128
This PR satisfies what @omus considers an ideal solution in #128 (comment) (I've sanitized some deprecation warnings):
PR 54
This PR also doesn't care about argument order, which was a limitation in PR #54:
It also doesn't care if the eltypes are Real, another limitation in #54:
Note that broadcasting is not oblivious to the underlying storage order, as mentioned in the high-level description, and there are differing opinions on that [1] [2]. However, this PR is very conservative, in that you can do strictly more with broadcasting while preserving the AxisArray wrapper. If there were another PR that paid no attention to the underlying storage order / did auto alignment, I think you would again have strictly more functionality, for some sense of the word strictly :) I'm not sure how broadcasting should be treated when combining both AxisArrays and AbstractArrays in that case; there you kind of need to pay attention to the storage order.
Known limitations
Not everything is inferable yet, trying to identify why.
Some of the error messages are opaque when broadcasting doesn't work for AxisArray-specific reasons. I don't think this is insurmountable but it would require some more boiler-plate to fix.
Axis info can get lost when using wrappers around AxisArrays, like with adjoint.This has been resolved as follows:Note that as a consequence of requiring unique axis names for each dimension,
A + A'
fails. This is because the result array would have the same axis name for both column and row (:asdf). At first I wondered if Adjoint should really wrap AxisArrays like it does now, but that's actually consistent with this PR in that the underlying storage order is important in broadcasting. I think I'm fine with that— perhaps the README should say specifically that indexing can be oblivious to the storage order of the underlying array.transpose(A)
:Probably AxisArrays should be updated to use the Transpose type that was introduced.