Skip to content
Merged
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
2 changes: 1 addition & 1 deletion laws/src/instances/DoubleInstances.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 0 additions & 6 deletions vecxt/src-jvm/floatmatrix.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 58 additions & 8 deletions vecxt/src/doublematrix.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 0 additions & 21 deletions vecxt/test/src-jvm/TODO.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions vecxt/test/src-jvm/floatmatrix.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions vecxt/test/src/layoutCorpus.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
48 changes: 9 additions & 39 deletions vecxt/test/src/matrix.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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") {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
1 change: 0 additions & 1 deletion vecxt_re/test/src/tower.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package vecxt_re

import vecxt_re.*
import vecxt.all.*
import vecxt.all.given
import SplitLosses.*

class TowerSuite extends munit.FunSuite:
Expand Down
Loading