diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index c68f8dc5..8653bb29 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -1,6 +1,6 @@ package vecxt.laws.instances -import vecxt.all.{*, given} +import vecxt.all.* import vecxt.laws.Dimension import vecxt.laws.VectorCommutativeGroup import vecxt.laws.VectorCommutativeMonoid diff --git a/vecxt/src-jvm/floatmatrix.scala b/vecxt/src-jvm/floatmatrix.scala index 4b416715..6ade50ab 100644 --- a/vecxt/src-jvm/floatmatrix.scala +++ b/vecxt/src-jvm/floatmatrix.scala @@ -578,12 +578,6 @@ object JvmFloatMatrix: end -= - /** In-place elementwise scalar multiply. The non-contiguous branch used to be `???`, which made the scalar-left - * `d *= m` below (which delegates here) throw for any strided or offset matrix even though nothing about the - * operation needs contiguity. Same shape as the `Double` twin in `src/doublematrix.scala`: SIMD over the whole - * backing array when dense contiguous, element-by-element via `linearIndex` otherwise — which skips padding - * instead of scaling it. - */ def *=(d: Float): Unit = if m.hasSimpleContiguousMemoryLayout then floatarrays.*=(m.raw)(d) else diff --git a/vecxt/src/doublematrix.scala b/vecxt/src/doublematrix.scala index f5431b1e..4ddb6277 100644 --- a/vecxt/src/doublematrix.scala +++ b/vecxt/src/doublematrix.scala @@ -342,7 +342,13 @@ object DoubleMatrix: def unary_- : Matrix[Double] = if m.hasSimpleContiguousMemoryLayout then Matrix[Double](vecxt.doublearrays.unary_-(m.raw), m.layout) - else ??? + else + val newArr = Array.ofDim[Double](m.numel) + m.layout.foreach2D { (i, j) => + val srcIdx = m.layout.linearIndex(i, j) + newArr(i + j * m.rows) = -m.raw(srcIdx) + } + Matrix[Double](newArr, m.rows, m.cols) def `exp!`: Unit = if m.hasSimpleContiguousMemoryLayout then vecxt.doublearrays.`exp!`(m.raw) @@ -443,19 +449,47 @@ object DoubleMatrix: def tan = if m.hasSimpleContiguousMemoryLayout then Matrix[Double](vecxt.all.tan(m.raw), m.layout) - else ??? + else + val newArr = Array.ofDim[Double](m.numel) + m.layout.foreach2D { (i, j) => + val srcIdx = m.layout.linearIndex(i, j) + newArr(i + j * m.rows) = Math.tan(m.raw(srcIdx)) + } + Matrix[Double](newArr, m.rows, m.cols) def `tan!` = if m.hasSimpleContiguousMemoryLayout then vecxt.doublearrays.`tan!`(m.raw) - else ??? + else + m.layout.foreach2D { (i, j) => + val idx = m.layout.linearIndex(i, j) + m.raw(idx) = Math.tan(m.raw(idx)) + } + end if + end `tan!` def mean: Double = if m.hasSimpleContiguousMemoryLayout then m.sumSIMD / (m.rows * m.cols) - else ??? + else + var acc = 0.0 + m.layout.foreach2D { (i, j) => + val idx = m.layout.linearIndex(i, j) + acc += m.raw(idx) + } + acc / (m.rows * m.cols) + end if + end mean def **(power: Double): Matrix[Double] = if m.hasSimpleContiguousMemoryLayout then Matrix[Double](vecxt.all.**(m.raw)(power), m.layout) - else ??? + else + val newArr = Array.ofDim[Double](m.numel) + m.layout.foreach2D { (i, j) => + val srcIdx = m.layout.linearIndex(i, j) + newArr(i + j * m.rows) = Math.pow(m.raw(srcIdx), power) + } + Matrix[Double](newArr, m.rows, m.cols) + end if + end ** /** Reads every element through `m.layout.linearIndex`, which is just `offset + row * rowStride + col * colStride` — * valid for any layout, dense or strided, row-major or column-major. So unlike the element-wise SIMD ops in this @@ -513,15 +547,31 @@ object DoubleMatrix: m.diag.sum end trace - def sum: Double = sumSIMD + inline def sum: Double = sumSIMD def sumSIMD: Double = if m.hasSimpleContiguousMemoryLayout then vecxt.doublearrays.sum(m.raw) - else ??? + else + var acc = 0.0 + m.layout.foreach2D { (i, j) => + val idx = m.layout.linearIndex(i, j) + acc += m.raw(idx) + } + acc + end if + end sumSIMD def norm: Double = if m.hasSimpleContiguousMemoryLayout then vecxt.all.norm(m.raw) - else ??? + else + var acc = 0.0 + m.layout.foreach2D { (i, j) => + val idx = m.layout.linearIndex(i, j) + acc += m.raw(idx) * m.raw(idx) + } + Math.sqrt(acc) + end if + end norm // Note: det method is provided by platform-specific implementations // See: vecxt.JvmDeterminant (JVM with SIMD) and vecxt.JsNativeDeterminant (JS/Native) diff --git a/vecxt/test/src-jvm/TODO.test.scala b/vecxt/test/src-jvm/TODO.test.scala index 763dbac6..849f1c4e 100644 --- a/vecxt/test/src-jvm/TODO.test.scala +++ b/vecxt/test/src-jvm/TODO.test.scala @@ -29,25 +29,4 @@ class TODO extends FunSuite: left.`matmulInPlace!`(right, out, alpha = 1.0f, beta = 0.0f) } - // Removed: "matrix-vector multiply throws for non-column-major Float matrices". It asserted a NotImplementedError - // for a dense row-major operand, which is precisely the `???` this branch replaced — `*` now picks TRANS and lda - // from the strides and handles that layout, so the test was pinning the limitation rather than any behaviour worth - // keeping. The positive case is covered in the shared FloatMatVecSuite, which runs the same 2x3 row-major shape - // (among five layouts) and checks the values rather than just that something happens. - - // Was: "*= scalar throws for unsupported non-contiguous Float layouts", asserting a NotImplementedError for the - // fixture below. That `???` is now a `foreach2D` fallback (needed so the scalar-left `d *= m` works for anything but - // dense contiguous matrices), so the test asserts the values instead of the limitation. - test("*= scalar scales every element of a non-contiguous Float layout, and nothing else"): - // rows=2, cols=2, rowStride=2, colStride=5 over a length-8 array: neither stride is 1, so this is also the - // `unitStrideAxis == -1` case. Elements live at raw(0), raw(2), raw(5), raw(7); the 90s are padding. - val raw = Array[Float](1.0f, 90.0f, 2.0f, 91.0f, 92.0f, 3.0f, 93.0f, 4.0f) - val mat = Matrix[Float](raw, 2, 2, 2, 5, 0) - assert(!mat.hasSimpleContiguousMemoryLayout) - - mat *= 2.0f - - assertMatrixEquals(mat, Matrix.fromRows[Float](Array(2.0f, 6.0f), Array(4.0f, 8.0f))) - assertVecEquals(raw, Array[Float](2.0f, 90.0f, 4.0f, 91.0f, 92.0f, 6.0f, 93.0f, 8.0f)) - end TODO diff --git a/vecxt/test/src-jvm/floatmatrix.test.scala b/vecxt/test/src-jvm/floatmatrix.test.scala index 25e46bef..0ceecdd0 100644 --- a/vecxt/test/src-jvm/floatmatrix.test.scala +++ b/vecxt/test/src-jvm/floatmatrix.test.scala @@ -130,6 +130,18 @@ class FloatMatrixJvmSuite extends FunSuite: ) ) + test("*= scalar scales every element of a non-contiguous Float layout, and nothing else"): + // rows=2, cols=2, rowStride=2, colStride=5 over a length-8 array: neither stride is 1, so this is also the + // `unitStrideAxis == -1` case. Elements live at raw(0), raw(2), raw(5), raw(7); the 90s are padding. + val raw = Array[Float](1.0f, 90.0f, 2.0f, 91.0f, 92.0f, 3.0f, 93.0f, 4.0f) + val mat = Matrix[Float](raw, 2, 2, 2, 5, 0) + assert(!mat.hasSimpleContiguousMemoryLayout) + + mat *= 2.0f + + assertMatrixEquals(mat, Matrix.fromRows[Float](Array(2.0f, 6.0f), Array(4.0f, 8.0f))) + assertVecEquals(raw, Array[Float](2.0f, 90.0f, 4.0f, 91.0f, 92.0f, 6.0f, 93.0f, 8.0f)) + test("*:*= on offset Float view uses general layout path"): // (vecxt/src-jvm/floatmatrix.scala) `sub` has a // nonzero offset (it's `base`'s columns 1..2), so it misses the dense/offset-0 fast path and exercises the diff --git a/vecxt/test/src/layoutCorpus.test.scala b/vecxt/test/src/layoutCorpus.test.scala index 2a72ede7..6424484b 100644 --- a/vecxt/test/src/layoutCorpus.test.scala +++ b/vecxt/test/src/layoutCorpus.test.scala @@ -289,6 +289,38 @@ class LayoutCorpusSuite extends FunSuite: end for } + test("tan — op(view) == op(copy) over the full layout-kind corpus") { + for m <- corpus do + val copy = denseCopy(m) + assertLogicallyEqual(m.tan, copy.tan, s"tan on $m") + end for + } + + test("unary_- — op(view) == op(copy), source matrix unchanged") { + for m <- corpus do + val copy = denseCopy(m) + val before = m.raw.clone() + assertLogicallyEqual(-m, copy.*(-1.0), s"unary_- on $m") + assertVecEquals(m.raw, before) + end for + } + + test("**(scalar) — op(view) == op(copy) over the full layout-kind corpus") { + for m <- corpus do + val copy = denseCopy(m) + assertLogicallyEqual(m.**(2.0), copy.**(2.0), s"**(2.0) on $m") + end for + } + + test("mean/sum/norm reductions on views match dense copy") { + for m <- corpus do + val copy = denseCopy(m) + assertEqualsDouble(m.mean, copy.mean, 1e-9, s"mean on $m") + assertEqualsDouble(m.sum, copy.sum, 1e-9, s"sum on $m") + assertEqualsDouble(m.norm, copy.norm, 1e-9, s"norm on $m") + end for + } + /** The oracle for `Matrix[Boolean]` results — mirrors `model` above but for comparison-operator output. */ private def modelBool(m: Matrix[Boolean])(i: Int, j: Int): Boolean = m.raw(m.layout.offset + i * m.layout.rowStride + j * m.layout.colStride) @@ -414,6 +446,17 @@ class LayoutCorpusSuite extends FunSuite: end for } + test("tan! in-place — in-view matches op(copy), out-of-view untouched") { + for m <- corpus do + val copy = denseCopy(m) + val before = m.raw.clone() + copy.`tan!` + m.`tan!` + assertLogicallyEqual(m, copy, s"tan! on $m") + assertOutsideViewUntouched(m, before, s"tan! on $m") + end for + } + test("update(fct, value) in-place — in-view matches op(copy), out-of-view untouched") { for m <- corpus do val copy = denseCopy(m) diff --git a/vecxt/test/src/matrix.test.scala b/vecxt/test/src/matrix.test.scala index 991d704b..eb5aa254 100644 --- a/vecxt/test/src/matrix.test.scala +++ b/vecxt/test/src/matrix.test.scala @@ -9,27 +9,6 @@ import MatrixInstance.update class MatrixExtensionSuite extends FunSuite: - // TODO will fail on JS, grrr. - // test("print") { - // val mat1 = Matrix[Double](Array(1.0, 4.0, 2.0, 5.0), (2, 2)) - // assert(mat1.printMat.contains("4")) - - // } - - // test("transpose etc".only) { - // val mat1 = Matrix(Array(1.0, 4.0, 2.0, 5.0, 3.0, 6.0), (2, 3)) - // val mat2 = Matrix(NArray(7.0, 9.0, 11.0, 8.0, 10, 12.0), (3, 2)) - // val result2 = mat1 @@ mat2 - - // result2.printMat - // val result3 = Matrix.eye(2) + mat1 @@ mat2 - // result3.printMat - // val mat3 = mat2.transpose + mat1 - // println(mat2.transpose.printMat) - // mat3.raw.printArr - // mat3.printMat - // } - def mat1to9 = Matrix.fromRows[Double]( Array(1.0, 2.0, 3.0), Array(4.0, 5.0, 6.0), @@ -39,7 +18,14 @@ class MatrixExtensionSuite extends FunSuite: def raw1to9 = mat1to9.raw test("pow") { - mat1to9 ** 2.0 + assertVecEquals[Double]((mat1to9 ** 2.0).raw, raw1to9.map(x => x * x)) + + val dontMutate = mat1to9 + assertMatrixEquals( + dontMutate(1 to 2, 1 to 2) ** 2.0, + Matrix.fromRows[Double](Array(25.0, 36.0), Array(64.0, 81.0)) + ) + assertEqualsDouble(dontMutate(1, 0), 4.0, 0.01) } test("from rows") { @@ -283,17 +269,6 @@ class MatrixExtensionSuite extends FunSuite: assertMatrixEquals(result, expected) } - test("Some urnary ops") { - val checkThis = mat1to9.exp - mat1to9.log - mat1to9.sqrt - mat1to9.sin - mat1to9.cos - - assertVecEquals[Double](checkThis.raw, raw1to9.exp) - - } - test("log on submatrix (non-contiguous layout) yields correct new matrix") { val base = Matrix[Double](Array.tabulate[Double](9)(_.toDouble), 3, 3) val sub = base(::, Array(1, 2)) // cols 1&2 of col-major 3x3: values (3,4,5) and (6,7,8) @@ -583,16 +558,11 @@ class MatrixExtensionSuite extends FunSuite: val arr2 = Array[Double](1.0, 2.0) assertVecEquals(mat1 * arr1, Array[Double](14.0, 32.0)) - - // Was commented out because `*` threw `???` for anything that was not dense column-major, and mat1.transpose is - // row-major. It now runs — but note the value it was written with, Array(6.0, 30.0), was never right: the - // transpose is 3x2, so the product with a length-2 vector has three entries, not two. - // mat1.transpose is [[1,4],[2,5],[3,6]]; against [1,2] that is [1+8, 2+10, 3+12]. assertVecEquals(mat1.transpose * arr2, Array[Double](9.0, 12.0, 15.0)) } // ─── matrix-vector product across layouts ──────────────────────────────────────────────────────────────────── - // `*` used to be `if m.isDenseColMajor then dgemv(...) else ???`. It now picks TRANS/lda from the strides, the + // Picks TRANS/lda from the strides, the // same way matmulInPlace! does for dgemm, and falls back to an elementwise loop for layouts no single leading // dimension can describe. Every fixture below is the same logical 2x3 [[1,2,3],[4,5,6]], so all must agree. diff --git a/vecxt_re/test/src/tower.test.scala b/vecxt_re/test/src/tower.test.scala index f8f929d7..44b46d28 100644 --- a/vecxt_re/test/src/tower.test.scala +++ b/vecxt_re/test/src/tower.test.scala @@ -2,7 +2,6 @@ package vecxt_re import vecxt_re.* import vecxt.all.* -import vecxt.all.given import SplitLosses.* class TowerSuite extends munit.FunSuite: