-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Broadcasting functions #3624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Broadcasting functions #3624
Changes from all commits
6b0ea89
cd83def
f2a2943
756aa83
a804f9d
cfe6490
5224a45
b212e6f
4315651
c99c48a
d749405
c267a8c
23bc583
26367b5
54338f5
5e13b30
d593869
41d2db1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| export const broadcastMatricesDocs = { | ||
| name: 'broadcastMatrices', | ||
| category: 'Matrix', | ||
| syntax: [ | ||
| 'broadcastMatrices(A, B)' | ||
| ], | ||
| description: 'Broadcast any number of arrays or matrices against each other.', | ||
| examples: [ | ||
| 'broadcastMatrices([1, 2, 3], [[1], [2], [3]])', | ||
| 'broadcastMatrices([1, 2; 3, 4], [5, 6; 7, 8])', | ||
| 'broadcastMatrices([1, 2, 3], [5], [[10], [20], [30]])' | ||
| ], | ||
| seealso: [ | ||
| 'size', 'reshape', 'broadcastSizes', 'broadcastTo' | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| export const broadcastSizesDocs = { | ||
| name: 'broadcastSizes', | ||
| category: 'Matrix', | ||
| syntax: [ | ||
| 'broadcastSizes(sizeA, sizeB)' | ||
| ], | ||
| description: 'Broadcast the sizes of matrices to a compatible size', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, I am concerned about at least the grammatical correctness here. It's not that any sizes are being broadcast here, per se, is it, but rather that the size resulting from a broadcast is being computed, right? So shouldn't the description be something more like "Compute the size that would result from broadcasting a list of matrices of the given sizes, if possible"? (Again, this function can throw an error if the sizes are incompatible, correct?) This also observation also, for me, calls into question the name of the function. Would
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The terminology used by numpy is
I think I understand where are you coming from, because the original sizes are kept intact. But maybe it's an implicit definition, because when one adds numbers, nothing happens to the numbers, we could say is to compute the result from adding numbers. Yes this function will throw an error for incompatible sizes.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I meant that it's the matrices that are broadcast, not their sizes. This function does not actually do any broadcasting. It just computes a size, and so it should be named accordingly, I think. Your thoughts?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any further thoughts here? I am recommending:
because, as a matter of the plain meanings of the words, a matrix/array can be broadcast, but a size cannot be broadcast.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, even though I had a response planned from the first comment I was debating myself on how to present the response. Broadcasting, when applied to a matrix's shape, does not imply treating the shape itself as a data structure to be broadcasted. Much like the phrase 'running water' describes flow rather than the physical stride of a 'running person,' broadcasting a shape is a functional convention. I don't know if the convention all comes from numpy, but it's a common trend stdlib: broadcast-shapes I think the name of the function is ok as is, but it could be described as you suggest to avoid a circular definition.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, we should just strive for both the clearest name of the function and the clearest description of what it does. If you think that "broadcastSizes(s, t, u)" is clearer than "broadcastSize(s, t, u)", I am not going to quibble. But I do prefer a more explicit description, thanks. |
||
| examples: [ | ||
| 'broadcastSizes([3, 1, 3], [3, 3])', | ||
| 'broadcastSizes([2, 1], [2, 2])', | ||
| 'broadcastSizes([1, 3], [2, 3], [4, 2, 3])' | ||
| ], | ||
| seealso: [ | ||
| 'size', 'reshape', 'broadcastTo', 'broadcastMatrices' | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export const broadcastToDocs = { | ||
| name: 'broadcastTo', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry about the name of this function. It seems to me that since sizes look like matrices, visually,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand. Many of these are taken from numpy and have counterparts in jax / mlx / pytorch and maybe others.
I don't have a strong opinion on this, just please review if it makes sense to follow that convention.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am the one less familiar with the territory here. That's why this was couched as a suggestion. Please select the name you think is best, including leaving it be, unless @josdejong weighs in otherwise. Please just post your final decision here.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, to land this PR we need a decision on the final name here. If you are on the fence, I recommend switching to
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree about the differences between shape and size. I'm considering |
||
| category: 'Matrix', | ||
| syntax: [ | ||
| 'broadcastTo(A, size)' | ||
| ], | ||
| description: 'Broadcast a matrix to a compatible size', | ||
| examples: [ | ||
| 'broadcastTo([1, 2, 3], [3, 3])', | ||
| 'broadcastTo([1, 2; 3, 4], [2, 2])' | ||
| ], | ||
| seealso: [ | ||
| 'size', 'reshape', 'broadcastSizes', 'broadcastMatrices' | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { broadcastArrays } from '../../utils/array.js' | ||
| import { factory } from '../../utils/factory.js' | ||
| import { isMatrix } from '../../utils/is.js' | ||
|
|
||
| const name = 'broadcastMatrices' | ||
| const dependencies = ['typed'] | ||
|
|
||
| export const createBroadcastMatrices = /* #__PURE__ */ factory(name, dependencies, ({ typed }) => { | ||
| /** | ||
| * Broadcast any number of arrays or matrices against each other. | ||
| * | ||
| * Syntax: | ||
| * | ||
| * math.broadcastMatrices(x, y) | ||
| * math.broadcastMatrices(x, y, ...) | ||
| * | ||
| * Examples: | ||
| * | ||
| * math.broadcastMatrices([1, 2], [[3], [4]]) // returns [[[1, 2], [1, 2]], [[3, 3], [4, 4]]] | ||
| * math.broadcastMatrices([2, 3]) // returns [[2, 3]] | ||
| * math.broadcastMatrices([2, 3], [3, 1]) // returns [[2, 3], [3, 1]] | ||
| * | ||
| * See also: | ||
| * | ||
| * size, reshape, broadcastSizes, broadcastTo | ||
| * | ||
| * History: | ||
| * | ||
| * v15.1.1 created | ||
| * @param {...(Array|Matrix)} x One or more matrices or arrays | ||
| * @return {Array[Array|Matrix]} An array of matrices with the broadcasted sizes. | ||
| */ | ||
| return typed(name, { | ||
| '...Array|Matrix': collections => { | ||
| const areMatrices = collections.map(isMatrix) | ||
| if (areMatrices.includes(true)) { | ||
| const arrays = collections.map((c, i) => areMatrices[i] ? c.valueOf() : c) | ||
| const broadcastedArrays = broadcastArrays(...arrays) | ||
| const broadcastedCollections = broadcastedArrays.map((arr, i) => areMatrices[i] ? collections[i].create(arr) : arr) | ||
| return broadcastedCollections | ||
| } | ||
| return broadcastArrays(...collections) | ||
| } | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { broadcastSizes } from '../../utils/array.js' | ||
| import { factory } from '../../utils/factory.js' | ||
|
|
||
| const name = 'broadcastSizes' | ||
| const dependencies = ['typed'] | ||
|
|
||
| export const createBroadcastSizes = /* #__PURE__ */ factory(name, dependencies, ({ typed }) => { | ||
| /** | ||
| * Calculate the broadcasted size of one or more matrices or arrays. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per my comments on the internal docs, shouldn't this be something more like "Calculate the size that would result from broadcasting one or more matrices or arrays, given the sizes of the input collections."? The same comments about having documentation on the operation of broadcasting either here or linked here apply to this function as well. Also mention of what happens with incompatible sizes.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still needs further editing/documentation. |
||
| * Always returns an Array containing numbers. | ||
| * | ||
| * Syntax: | ||
| * | ||
| * math.broadcastSizes(x, y) | ||
| * math.broadcastSizes(x, y, ...) | ||
| * | ||
| * Examples: | ||
| * | ||
| * math.broadcastSizes([2, 3]) // returns [2, 3] | ||
| * math.broadcastSizes([2, 3], [3]) // returns [2, 3] | ||
| * math.broadcastSizes([1, 2, 3], [1, 2, 1]) // returns [1, 2, 3] | ||
| * | ||
| * See also: | ||
| * | ||
| * size, reshape, squeeze, broadcastTo | ||
| * | ||
| * History: | ||
| * | ||
| * v15.1.1 created | ||
| * @param {...(Array|Matrix)} x One or more matrices or arrays | ||
| * @return {Array} A vector with the broadcasted size. | ||
| */ | ||
| return typed(name, { | ||
| '...Array|Matrix': collections => broadcastSizes(...collections.map(collection => collection.valueOf())) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { broadcastTo } from '../../utils/array.js' | ||
| import { factory } from '../../utils/factory.js' | ||
|
|
||
| const name = 'broadcastTo' | ||
| const dependencies = ['typed'] | ||
|
|
||
| export const createBroadcastTo = /* #__PURE__ */ factory(name, dependencies, ({ typed }) => { | ||
| /** | ||
| * Broadcast an array to a specified size. | ||
| * | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically the same documentation comments: needs to be clear (how to find out) what broadcasting is, and there should be mention of what happens with incompatible size. Presumably that also means that (for all three functions) there should be an example with incompatible size.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still needs further editing/documentation. |
||
| * Syntax: | ||
| * | ||
| * math.broadcastTo(x, size) | ||
| * | ||
| * Examples: | ||
| * | ||
| * math.broadcastTo([1, 2, 3], [2, 3]) // returns [[1, 2, 3], [1, 2, 3]] | ||
| * math.broadcastTo([2, 3], [2, 2]) // returns [[2, 3], [2, 3]] | ||
| * | ||
| * See also: | ||
| * | ||
| * size, reshape, squeeze, broadcastSizes | ||
| * | ||
| * History: | ||
| * | ||
| * v15.1.1 created | ||
| * | ||
| * @param {Array|Matrix} x The array or matrix to broadcast | ||
| * @param {Array|Matrix} size The target size | ||
| * @return {Array} The broadcasted array | ||
| */ | ||
| return typed(name, { | ||
| 'Array, Array': broadcastTo, | ||
| 'Array, Matrix': (arr, size) => broadcastTo(arr, size.valueOf()), | ||
| 'Matrix, Array|Matrix': (M, size) => { | ||
| const result = M.create() | ||
| result._size = size.valueOf() | ||
| result._data = broadcastTo(M.valueOf(), size.valueOf()) | ||
| result._datatype = M.datatype() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't want to be breaking encapsulation of the internal format of matrices here. In particular, this implementation is supposed to work with an arbitrary Matrix implementation, based on its typed-function signature. Hence it can't delve into the internal fields, as they are assuming M is a DenseMatrix, which it might not be. Therefore, this should be
Of course I am open to other ideas. But we don't want DenseMatrix-internals-specific code here. This observation also suggests there should be unit tests for the broadcasting functions on SparseMatrix arguments. Please add some if they aren't there.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this case I think it can be left as you mention |
||
| return result | ||
| } | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import assert from 'assert' | ||
| import math from '../../../../src/defaultInstance.js' | ||
|
|
||
| describe('broadcastMatrices', function () { | ||
| const matrix = math.matrix | ||
| const broadcastMatrices = math.broadcastMatrices | ||
| const A = [[1], [2], [3]] | ||
| const B = [[10, 20, 30]] | ||
| const C = [100] | ||
| const broadcastedA = [[1, 1, 1], [2, 2, 2], [3, 3, 3]] | ||
| const broadcastedB = [[10, 20, 30], [10, 20, 30], [10, 20, 30]] | ||
| const broadcastedC = [[100, 100, 100], [100, 100, 100], [100, 100, 100]] | ||
|
|
||
| it('should broadcast matrices', function () { | ||
| assert.deepStrictEqual(broadcastMatrices(matrix(A), matrix(B)), [matrix(broadcastedA), matrix(broadcastedB)]) | ||
| assert.deepStrictEqual(broadcastMatrices(matrix(A), matrix(C)), [matrix(A), matrix([[100], [100], [100]])]) | ||
| assert.deepStrictEqual(broadcastMatrices(matrix(B), matrix(A)), [matrix(broadcastedB), matrix(broadcastedA)]) | ||
| assert.deepStrictEqual(broadcastMatrices(matrix(A), matrix(B), matrix(C)), [matrix(broadcastedA), matrix(broadcastedB), matrix(broadcastedC)]) | ||
| }) | ||
|
|
||
| it('should broadcast arrays', function () { | ||
| assert.deepStrictEqual(broadcastMatrices(A, B), [broadcastedA, broadcastedB]) | ||
| assert.deepStrictEqual(broadcastMatrices(B, A), [broadcastedB, broadcastedA]) | ||
| assert.deepStrictEqual(broadcastMatrices(A, B, C), [broadcastedA, broadcastedB, broadcastedC]) | ||
| assert.deepStrictEqual(broadcastMatrices(A), [A]) | ||
| assert.deepStrictEqual(broadcastMatrices(B, C, A), [broadcastedB, broadcastedC, broadcastedA]) | ||
| }) | ||
|
|
||
| it('should throw an error if sizes are not compatible', function () { | ||
| assert.throws(function () { broadcastMatrices(matrix([[1, 2], [3, 4]]), matrix([[1, 2, 3]])) }, /Error: shape mismatch: /) | ||
| assert.throws(function () { broadcastMatrices([[1, 2], [3, 4]], matrix([[1, 2, 3]])) }, /Error: shape mismatch: /) | ||
| assert.throws(function () { broadcastMatrices(matrix([[1, 2], [3, 4]]), [[1, 2, 3]]) }, /Error: shape mismatch: /) | ||
| assert.throws(function () { broadcastMatrices([[1, 2], [3, 4]], [[1, 2, 3]]) }, /Error: shape mismatch: /) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import assert from 'assert' | ||
| import math from '../../../../src/defaultInstance.js' | ||
|
|
||
| describe('broadcastSizes', function () { | ||
| const broadcastSizes = math.broadcastSizes | ||
| const matrix = math.matrix | ||
|
|
||
| it('should broadcast sizes', function () { | ||
| assert.deepStrictEqual(broadcastSizes([2, 3]), [2, 3]) | ||
| assert.deepStrictEqual(broadcastSizes([3, 3], [3, 1]), [3, 3]) | ||
| assert.deepStrictEqual(broadcastSizes([2, 1], [1, 3]), [2, 3]) | ||
| assert.deepStrictEqual(broadcastSizes([5, 4, 3], [1, 4, 1]), [5, 4, 3]) | ||
| assert.deepStrictEqual(broadcastSizes([3], [2, 3]), [2, 3]) | ||
| assert.deepStrictEqual(broadcastSizes([1, 3], [2, 1]), [2, 3]) | ||
| }) | ||
|
|
||
| it('should throw an error if sizes are not compatible', function () { | ||
| assert.throws(function () { broadcastSizes([2, 3], [3, 2]) }, /Error: shape mismatch: /) | ||
| assert.throws(function () { broadcastSizes([2, 3], [2, 3, 4]) }, /Error: shape mismatch: /) | ||
| }) | ||
|
|
||
| it('should broadcast sizes of mixed arrays and matrices', function () { | ||
| assert.deepStrictEqual(broadcastSizes([3, 3], matrix([3, 1])), [3, 3]) | ||
| assert.deepStrictEqual(broadcastSizes(matrix([2, 1]), [1, 3]), [2, 3]) | ||
| assert.deepStrictEqual(broadcastSizes(matrix([5, 4, 3]), matrix([1, 4, 1])), [5, 4, 3]) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import assert from 'assert' | ||
| import math from '../../../../src/defaultInstance.js' | ||
|
|
||
| describe('broadcastTo', function () { | ||
| const broadcastTo = math.broadcastTo | ||
| const matrix = math.matrix | ||
|
|
||
| it('should broadcast arrays to a given size', function () { | ||
| assert.deepStrictEqual(broadcastTo([1, 2, 3], [2, 3]), [[1, 2, 3], [1, 2, 3]]) | ||
| assert.deepStrictEqual(broadcastTo([2, 3], [2, 2]), [[2, 3], [2, 3]]) | ||
| }) | ||
|
|
||
| it('should broadcast matrices to a given size', function () { | ||
| assert.deepStrictEqual(broadcastTo(matrix([1, 2, 3]), [2, 3]), matrix([[1, 2, 3], [1, 2, 3]])) | ||
| assert.deepStrictEqual(broadcastTo(matrix([2, 3]), [2, 2]), matrix([[2, 3], [2, 3]])) | ||
| }) | ||
|
|
||
| it('should broadcast mixed arrays and matrices to a given size', function () { | ||
| assert.deepStrictEqual(broadcastTo([1, 2, 3], matrix([2, 3])), [[1, 2, 3], [1, 2, 3]]) | ||
| assert.deepStrictEqual(broadcastTo(matrix([2, 3]), [2, 2]), matrix([[2, 3], [2, 3]])) | ||
| assert.deepStrictEqual(broadcastTo(matrix([1, 2, 3]), matrix([2, 3])), matrix([[1, 2, 3], [1, 2, 3]])) | ||
| assert.deepStrictEqual(broadcastTo([2, 3], matrix([2, 2])), [[2, 3], [2, 3]]) | ||
| }) | ||
|
|
||
| it('should throw an error if sizes are not compatible', function () { | ||
| assert.throws(function () { broadcastTo([1, 2], [2, 3]) }, /Error: shape mismatch: /) | ||
| assert.throws(function () { broadcastTo(matrix([1, 2]), [2, 3]) }, /Error: shape mismatch: /) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the syntax read
'broadcastMatrices(A, B, ...)'since any number of arguments are allowed?