Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Unreleased

- Add statically-typed `scale()` to `Matrix64`, `Matrix32`, `Vector64`, and
`Vector32`, and explicit axis broadcasting (`addVector`, `subtractVector`,
`multiplyVector`, `divideVector`) on the matrix types to give a fully typed
multiply path and remove the square-matrix broadcast ambiguity.

## 2.0.0

- Redesign the package around high performance and script-friendly ergonomics,
Expand Down
47 changes: 47 additions & 0 deletions lib/src/matrix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,29 @@ class Matrix64 {

Matrix64 hadamard(Object other) => _binary(other, ElementwiseOp.multiply);

/// Scalar multiply that preserves the static [Matrix64] type.
///
/// `operator *` returns `dynamic` because it also dispatches to
/// matrix/vector products; use [scale] when the result must stay
/// statically typed for chaining.
Matrix64 scale(num factor) => _binary(factor, ElementwiseOp.multiply);

/// Add [vector] to every line along [axis] with an explicit
/// orientation. `Axis.columns` expects `vector.length == columns`,
/// `Axis.rows` expects `vector.length == rows` — removing the
/// square-matrix ambiguity of the broadcasting operators.
Matrix64 addVector(Vector64 vector, Axis axis) =>
_broadcastAxis(vector, axis, ElementwiseOp.add);

Matrix64 subtractVector(Vector64 vector, Axis axis) =>
_broadcastAxis(vector, axis, ElementwiseOp.subtract);

Matrix64 multiplyVector(Vector64 vector, Axis axis) =>
_broadcastAxis(vector, axis, ElementwiseOp.multiply);

Matrix64 divideVector(Vector64 vector, Axis axis) =>
_broadcastAxis(vector, axis, ElementwiseOp.divide);

Matrix64 matmul(Matrix64 other) {
if (columns != other.rows) {
throw ArgumentError(
Expand Down Expand Up @@ -941,6 +964,30 @@ class Matrix64 {
);
}

Matrix64 _broadcastAxis(Vector64 vector, Axis axis, ElementwiseOp op) {
final expected = axis == Axis.columns ? columns : rows;
if (vector.length != expected) {
throw ArgumentError(
'Vector64 length ${vector.length} must equal '
'${axis == Axis.columns ? 'columns $columns' : 'rows $rows'} '
'to broadcast along $axis.',
);
}
final vectorData = vector.unsafeValuesView;
final result = Float64List(count);
for (var row = 0; row < rows; row++) {
final offset = row * columns;
for (var column = 0; column < columns; column++) {
result[offset + column] = _apply(
_data[offset + column],
vectorData[axis == Axis.columns ? column : row],
op,
);
}
}
return Matrix64.storage(rows, columns, result, copy: false);
}

Matrix64 _broadcastVector(Vector64 vector, ElementwiseOp op) {
final vectorData = vector.values;
final result = Float64List(count);
Expand Down
47 changes: 47 additions & 0 deletions lib/src/matrix32.dart
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,29 @@ class Matrix32 {

Matrix32 hadamard(Object other) => _binary(other, ElementwiseOp.multiply);

/// Scalar multiply that preserves the static [Matrix32] type.
///
/// `operator *` returns `dynamic` because it also dispatches to
/// matrix/vector products; use [scale] when the result must stay
/// statically typed for chaining.
Matrix32 scale(num factor) => _binary(factor, ElementwiseOp.multiply);

/// Add [vector] to every line along [axis] with an explicit
/// orientation. `Axis.columns` expects `vector.length == columns`,
/// `Axis.rows` expects `vector.length == rows` — removing the
/// square-matrix ambiguity of the broadcasting operators.
Matrix32 addVector(Vector32 vector, Axis axis) =>
_broadcastAxis32(vector, axis, ElementwiseOp.add);

Matrix32 subtractVector(Vector32 vector, Axis axis) =>
_broadcastAxis32(vector, axis, ElementwiseOp.subtract);

Matrix32 multiplyVector(Vector32 vector, Axis axis) =>
_broadcastAxis32(vector, axis, ElementwiseOp.multiply);

Matrix32 divideVector(Vector32 vector, Axis axis) =>
_broadcastAxis32(vector, axis, ElementwiseOp.divide);

Matrix32 vstack(Matrix32 other) =>
Matrix32.fromMatrix(toFloat64().vstack(other.toFloat64()));

Expand Down Expand Up @@ -719,6 +742,30 @@ class Matrix32 {
);
}

Matrix32 _broadcastAxis32(Vector32 vector, Axis axis, ElementwiseOp op) {
final expected = axis == Axis.columns ? columns : rows;
if (vector.length != expected) {
throw ArgumentError(
'Vector32 length ${vector.length} must equal '
'${axis == Axis.columns ? 'columns $columns' : 'rows $rows'} '
'to broadcast along $axis.',
);
}
final vectorData = vector.unsafeValuesView;
final result = Float32List(count);
for (var row = 0; row < rows; row++) {
final offset = row * columns;
for (var column = 0; column < columns; column++) {
result[offset + column] = _apply(
_data[offset + column],
vectorData[axis == Axis.columns ? column : row],
op,
);
}
}
return Matrix32.storage(rows, columns, result, copy: false);
}

Matrix32 _broadcastVector32(Vector32 vector, ElementwiseOp op) {
final vectorData = vector.unsafeValuesView;
final result = Float32List(count);
Expand Down
8 changes: 8 additions & 0 deletions lib/src/vector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ class Vector64 extends IterableBase<double> {

dynamic operator /(Object other) => _binary(other, ElementwiseOp.divide);

/// Scalar multiply that preserves the static [Vector64] type.
///
/// `operator *` returns `dynamic` because it also dispatches to the
/// vector/matrix product; use [scale] when the result must stay
/// statically typed for chaining.
Vector64 scale(num factor) =>
Vector64.storage(Kernels.scale(_data, factor.toDouble()), copy: false);

double dot(Vector64 other) {
_checkLength(other);
return Kernels.dot(_data, 0, other._data, 0, length);
Expand Down
8 changes: 8 additions & 0 deletions lib/src/vector32.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ class Vector32 extends IterableBase<double> {

dynamic operator /(Object other) => _binary(other, ElementwiseOp.divide);

/// Scalar multiply that preserves the static [Vector32] type.
///
/// `operator *` returns `dynamic` because it also dispatches to the
/// vector/matrix product; use [scale] when the result must stay
/// statically typed for chaining.
Vector32 scale(num factor) =>
Vector32.storage(Kernels.scale32(_data, factor.toDouble()), copy: false);

double dot(Vector32 other) {
_checkLength(other);
return Kernels.dot32(_data, 0, other._data, 0, length);
Expand Down
63 changes: 63 additions & 0 deletions test/matrix_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,67 @@ void main() {
throwsArgumentError,
);
});

test('typed arithmetic methods preserve static types', () {
final Matrix64 a = mat([
[1, 2, 3],
[4, 5, 6],
]);

final Matrix64 scaled = a.scale(2);
expect(scaled.transpose.toRows(), [
[2, 8],
[4, 10],
[6, 12],
]);

final Vector64 x = vec([1, 2, 3]);
final Vector64 doubled = x.scale(2);
expect(doubled.toList(), [2, 4, 6]);

final Matrix32 a32 = mat32([
[1, 2],
[3, 4],
]);
expect(a32.scale(3).transpose.toRows(), [
[3, 9],
[6, 12],
]);
expect(vec32([1, 2]).scale(4).toList(), [4, 8]);
});

test('explicit axis broadcasting removes square-matrix ambiguity', () {
final a = mat([
[1, 2],
[3, 4],
]);
final v = vec([10, 20]);

expect(a.addVector(v, Axis.columns).toRows(), [
[11, 22],
[13, 24],
]);
expect(a.addVector(v, Axis.rows).toRows(), [
[11, 12],
[23, 24],
]);
expect(a.subtractVector(v, Axis.columns).toRows(), [
[-9, -18],
[-7, -16],
]);

final a32 = mat32([
[1, 2],
[3, 4],
]);
expect(a32.multiplyVector(vec32([2, 3]), Axis.rows).toRows(), [
[2, 4],
[9, 12],
]);

expect(
() => a.addVector(vec([1, 2, 3]), Axis.columns),
throwsArgumentError,
);
});
}