diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e876ca..560bb23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/lib/src/matrix.dart b/lib/src/matrix.dart index 8e7883c..c475e5d 100644 --- a/lib/src/matrix.dart +++ b/lib/src/matrix.dart @@ -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( @@ -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); diff --git a/lib/src/matrix32.dart b/lib/src/matrix32.dart index 9184db5..bb484ed 100644 --- a/lib/src/matrix32.dart +++ b/lib/src/matrix32.dart @@ -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())); @@ -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); diff --git a/lib/src/vector.dart b/lib/src/vector.dart index 03c8238..aae7c0a 100644 --- a/lib/src/vector.dart +++ b/lib/src/vector.dart @@ -108,6 +108,14 @@ class Vector64 extends IterableBase { 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); diff --git a/lib/src/vector32.dart b/lib/src/vector32.dart index 939db28..e0df39c 100644 --- a/lib/src/vector32.dart +++ b/lib/src/vector32.dart @@ -80,6 +80,14 @@ class Vector32 extends IterableBase { 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); diff --git a/test/matrix_api_test.dart b/test/matrix_api_test.dart index bdb8649..5adbcf2 100644 --- a/test/matrix_api_test.dart +++ b/test/matrix_api_test.dart @@ -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, + ); + }); }