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
4 changes: 2 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//| mill-version: 1.1.9
//| mill-jvm-version: 25
//| mill-jvm-opts: [ "--add-modules", "jdk.incubator.vector"]
//| mill-jvm-opts: [ "--add-modules", "jdk.incubator.vector", -XX:TrimNativeHeapInterval=60000]
//| mvnDeps:
//| - io.github.quafadas:millSite_mill1_3.8:0.0.57
//| - com.goyeau:mill-scalafix_mill1_3:0.6.0
Expand Down Expand Up @@ -37,7 +37,7 @@ object V:
val spire: Dep = mvn"org.typelevel::spire::0.18.0"
val laminar: Dep = mvn"com.raquo::laminar::17.2.1"
val scalaTags: Dep = mvn"com.lihaoyi::scalatags::0.13.1"
val scalaVersion = "3.9.0"
val scalaVersion = "3.8.4"
val munitVersion = "1.3.6"
val blas: Dep = mvn"dev.ludovic.netlib:blas:3.2.0"
val lapack: Dep = mvn"dev.ludovic.netlib:lapack:3.2.0"
Expand Down
8 changes: 4 additions & 4 deletions jitAudit/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import mill.*, scalalib.*
*
* ==What was deliberately not built: D2 and D5==
*
* The plan specified two further checks in this tier, both of which work by running with
* {@code -XX:+LogCompilation} and parsing the XML: D2 confirms Vector API intrinsics were applied by looking for
* The plan specified two further checks in this tier, both of which work by running with {@code -XX:+LogCompilation}
* and parsing the XML: D2 confirms Vector API intrinsics were applied by looking for
* {@code <intrinsic id='_Vector...'>} entries, and D5 counts {@code <uncommon_trap>} and recompilation events. Neither
* is implemented, and the decision is not "not yet" — it is that the cost/benefit does not work. Recorded here rather
* than left as an absence, because an unexplained gap in a checklist reads as an oversight.
Expand Down Expand Up @@ -101,8 +101,8 @@ object `package` extends ScalaModule:
* ==How this relates to the unbuilt D2==
*
* It covers part of the same ground, from an observable consequence and a product-grade {@code -XX:} flag rather
* than from a JDK-internal XML vocabulary. D1 with EA on cannot distinguish "the SIMD intrinsics were applied"
* from "they were not, and EA scalarised the software-path objects instead" — both read as zero. This scope can.
* than from a JDK-internal XML vocabulary. D1 with EA on cannot distinguish "the SIMD intrinsics were applied" from
* "they were not, and EA scalarised the software-path objects instead" — both read as zero. This scope can.
*
* Only part, though, and the earlier framing of it as "what replaces D2" was too strong. It relies on EA being what
* rescues a software-path kernel, and that is not always so: D6's canary is a software-path kernel that allocates
Expand Down
132 changes: 132 additions & 0 deletions vecxt/src/BooleanMatrix.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package vecxt

import vecxt.matrix.*

/** Logical operations on `Matrix[Boolean]`, mirroring [[BooleanArrays]].
*
* Each op has the same two branches as the other matrix element-wise ops: when the backing array holds exactly the
* matrix's elements in order, it delegates to the (SIMD on the JVM) array op and reuses `m.layout`; otherwise it walks
* the view through `linearIndex`, and any new result is dense column-major.
*/
object BooleanMatrix:

extension (m: Matrix[Boolean])

/** True if every element of the matrix is true. True for an empty matrix. */
def allTrue: Boolean =
if m.hasSimpleContiguousMemoryLayout then BooleanArrays.allTrue(m.raw)
else
var out = true
var j = 0
while out && j < m.cols do
var i = 0
while out && i < m.rows do
if !m.raw(m.layout.linearIndex(i, j)) then out = false
end if
i += 1
end while
j += 1
end while
out
end allTrue

/** True if at least one element of the matrix is true. */
def any: Boolean =
if m.hasSimpleContiguousMemoryLayout then BooleanArrays.any(m.raw)
else
var out = false
var j = 0
while !out && j < m.cols do
var i = 0
while !out && i < m.rows do
if m.raw(m.layout.linearIndex(i, j)) then out = true
end if
i += 1
end while
j += 1
end while
out
end any

/** The number of true elements in the matrix. */
def trues: Int =
if m.hasSimpleContiguousMemoryLayout then BooleanArrays.trues(m.raw)
else
var sum = 0
m.layout.foreach2D { (i, j) =>
if m.raw(m.layout.linearIndex(i, j)) then sum += 1
end if
}
sum
end trues

/** Element-wise negation, returning a new matrix. */
def not: Matrix[Boolean] =
if m.hasSimpleContiguousMemoryLayout then Matrix[Boolean](BooleanArrays.not(m.raw), m.layout)
else
val newArr = Array.ofDim[Boolean](m.numel)
m.layout.foreach2D { (i, j) =>
newArr(i + j * m.rows) = !m.raw(m.layout.linearIndex(i, j))
}
Matrix[Boolean](newArr, m.rows, m.cols)
end not

/** Element-wise negation in place, writing through to the backing array (so any other view sharing it sees the
* change).
*
* Throws [[UnsupportedLayoutException]] for a view whose strides alias one backing element to several positions
* (e.g. a broadcast with a zero stride): each alias would flip the same element again, so the result would depend
* on how many times it is repeated.
*/
def `not!`: Unit =
if m.hasSimpleContiguousMemoryLayout then BooleanArrays.`not!`(m.raw)
else
if (m.rowStride == 0 && m.rows > 1) || (m.colStride == 0 && m.cols > 1) then
throw UnsupportedLayoutException(
s"not! cannot be applied in place to a view that aliases elements, got layout: ${m.layoutString}"
)
end if
m.layout.foreach2D { (i, j) =>
val idx = m.layout.linearIndex(i, j)
m.raw(idx) = !m.raw(idx)
}
end `not!`

/** Element-wise logical and. Both matrices must have the same shape. */
def &&(that: Matrix[Boolean]): Matrix[Boolean] =
sameDimMatCheck(m, that)
if sameDenseElementWiseMemoryLayoutCheck(m, that) then
Matrix[Boolean](BooleanArrayOps.and(m.raw, that.raw), m.layout)
else
val newArr = Array.ofDim[Boolean](m.numel)
m.layout.foreach2D { (i, j) =>
newArr(i + j * m.rows) = m.raw(m.layout.linearIndex(i, j)) && that.raw(that.layout.linearIndex(i, j))
}
Matrix[Boolean](newArr, m.rows, m.cols)
end if
end &&

/** Element-wise logical or. Both matrices must have the same shape. */
def ||(that: Matrix[Boolean]): Matrix[Boolean] =
sameDimMatCheck(m, that)
if sameDenseElementWiseMemoryLayoutCheck(m, that) then
Matrix[Boolean](BooleanArrayOps.or(m.raw, that.raw), m.layout)
else
val newArr = Array.ofDim[Boolean](m.numel)
m.layout.foreach2D { (i, j) =>
newArr(i + j * m.rows) = m.raw(m.layout.linearIndex(i, j)) || that.raw(that.layout.linearIndex(i, j))
}
Matrix[Boolean](newArr, m.rows, m.cols)
end if
end ||
end extension
end BooleanMatrix

/** The array `&&` / `||` live in `BooleanArrays` on the JVM but in `arrayUtil` on JS and Native, so resolve them
* through `all` rather than naming either. Kept outside [[BooleanMatrix]], whose own `&&` / `||` would shadow them.
*/
private object BooleanArrayOps:
import vecxt.all.*
def and(a: Array[Boolean], b: Array[Boolean]): Array[Boolean] = a && b
def or(a: Array[Boolean], b: Array[Boolean]): Array[Boolean] = a || b
end BooleanArrayOps
1 change: 1 addition & 0 deletions vecxt/src/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object all:
export vecxt.JsFloatMatrix.*
export vecxt.NativeFloatMatrix.*
export vecxt.JvmIntMatrix.*
export vecxt.BooleanMatrix.*
export vecxt.JsDoubleMatrix.*
export vecxt.NativeDoubleMatrix.*
export vecxt.DoubleMatrix.*
Expand Down
5 changes: 5 additions & 0 deletions vecxt/src/doublematrix.scala
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ object DoubleMatrix:
reduceAlongDimension(dim, _ * _, 1.0)
end product

def mean(dim: DimensionExtender): Matrix[Double] =
val s = sum(dim)
s / (m.numel / s.numel).toDouble
end mean

// inline def - : Matrix[Double] =
// Matrix(vecxt.doublearrays.*(m.raw)(-1), m.shape)

Expand Down
146 changes: 146 additions & 0 deletions vecxt/test/src/MatrixBoolean.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,150 @@ class MatrixBooleanSuite extends FunSuite:
assertVecEquals[Double](calc.raw, result.raw)
}

private def assertLogical(actual: Matrix[Boolean], expectedRows: Array[Boolean]*)(implicit
loc: munit.Location
): Unit =
assertEquals(actual.rows, expectedRows.length)
for i <- 0 until actual.rows do
for j <- 0 until actual.cols do assertEquals(actual(i, j), expectedRows(i)(j), clue = s"at ($i, $j)")
end for
end for
end assertLogical

// 2 x 2 view on the middle of a 3 x 3 col-major array (rows 1-2, cols 1-2). Non-contiguous: offset 4, colStride 3.
// col-major raw: col0 = (T, F, F), col1 = (T, T, F), col2 = (F, T, F)
// view: [ T, T ]
// [ F, F ]
private def strided: Matrix[Boolean] =
Matrix(Array[Boolean](true, false, false, true, true, false, false, true, false), 2, 2, 1, 3, 4)

private def rowMajor: Matrix[Boolean] =
// [ T, F, T ]
// [ F, F, T ]
Matrix(Array[Boolean](true, false, true, false, false, true), 2, 3, 3, 1, 0)

test("allTrue") {
assert(Matrix.fromRows[Boolean](Array(true, true), Array(true, true)).allTrue)
assert(!Matrix.fromRows[Boolean](Array(true, true), Array(false, true)).allTrue)
assert(!rowMajor.allTrue)
assert(!strided.allTrue)

val big = Matrix(Array.fill(1025 * 3)(true), 1025, 3)
assert(big.allTrue)
big.raw(2000) = false
assert(!big.allTrue)

val allTrueView = Matrix(Array(false, true, true, false, true, true), 2, 2, 1, 3, 1)
assert(allTrueView.allTrue)
}

test("any") {
assert(!Matrix.zeros[Boolean]((3, 3)).any)
assert(Matrix.eye[Boolean](3).any)
assert(rowMajor.any)
assert(strided.any)

val big = Matrix(Array.fill(1025 * 3)(false), 1025, 3)
assert(!big.any)
big.raw(2000) = true
assert(big.any)

// Only elements outside the view are true.
val noneInView = Matrix(Array(true, false, false, true, false, false), 2, 2, 1, 3, 1)
assert(!noneInView.any)
}

test("trues") {
assertEquals(Matrix.eye[Boolean](4).trues, 4)
assertEquals(Matrix.zeros[Boolean]((2, 5)).trues, 0)
assertEquals(rowMajor.trues, 3)
assertEquals(strided.trues, 2)

val big = Matrix(Array.fill(1025 * 3)(true), 1025, 3)
assertEquals(big.trues, 3075)
}

test("not - dense") {
val m = Matrix.fromRows[Boolean](Array(true, false, true), Array(false, true, false))
val n = m.not
assertLogical(n, Array(false, true, false), Array(true, false, true))
assertLogical(m, Array(true, false, true), Array(false, true, false)) // original untouched

assertLogical(rowMajor.not, Array(false, true, false), Array(true, true, false))
}

test("not - strided view") {
val v = strided
val n = v.not
assertLogical(n, Array(false, false), Array(true, true))
assert(n.isDenseColMajor)
assertLogical(v, Array(true, true), Array(false, false)) // view untouched
}

test("not! - dense") {
val m = Matrix.fromRows[Boolean](Array(true, false), Array(false, false))
m.`not!`
assertLogical(m, Array(false, true), Array(true, true))

val r = rowMajor
r.`not!`
assertLogical(r, Array(false, true, false), Array(true, true, false))
}

test("not! - strided view writes through and leaves the rest of the array alone") {
val v = strided
v.`not!`
assertLogical(v, Array(false, false), Array(true, true))
assertVecEquals[Boolean](v.raw, Array(true, false, false, true, false, true, false, false, true))
}

test("not! - aliased (broadcast) view throws") {
val broadcastCol = Matrix(Array(true, false), 2, 3, 1, 0, 0)
intercept[UnsupportedLayoutException](broadcastCol.`not!`)
}

test("&& and || - dense col-major") {
val a = Matrix.fromRows[Boolean](Array(true, true, false), Array(false, true, false))
val b = Matrix.fromRows[Boolean](Array(true, false, false), Array(true, true, true))
assertLogical(a && b, Array(true, false, false), Array(false, true, false))
assertLogical(a || b, Array(true, true, false), Array(true, true, true))
}

test("&& and || - large dense exercises the SIMD path") {
val n = 1025
val a = Matrix(Array.tabulate(n * 2)(_ % 2 == 0), n, 2)
val b = Matrix(Array.tabulate(n * 2)(_ % 3 == 0), n, 2)
val and = a && b
val or = a || b
for k <- 0 until n * 2 do
assertEquals(and.raw(k), k % 6 == 0, clue = s"and at $k")
assertEquals(or.raw(k), k % 2 == 0 || k % 3 == 0, clue = s"or at $k")
end for
}

test("&& and || - dense row-major") {
val other = Matrix(Array(true, true, false, true, false, true), 2, 3, 3, 1, 0)
// other: [ T, T, F ]
// [ T, F, T ]
assertLogical(rowMajor && other, Array(true, false, false), Array(false, false, true))
assertLogical(rowMajor || other, Array(true, true, true), Array(true, false, true))
}

test("&& and || - mixed layouts") {
val colMajor = Matrix.fromRows[Boolean](Array(true, true, false), Array(true, false, true))
assertLogical(rowMajor && colMajor, Array(true, false, false), Array(false, false, true))
assertLogical(colMajor || rowMajor, Array(true, true, true), Array(true, false, true))

val dense = Matrix.fromRows[Boolean](Array(false, true), Array(true, false))
assertLogical(strided && dense, Array(false, true), Array(false, false))
assertLogical(dense || strided, Array(true, true), Array(true, false))
}

test("&& and || - dimension mismatch throws") {
val a = Matrix.zeros[Boolean]((2, 2))
val b = Matrix.zeros[Boolean]((2, 3))
intercept[MatrixDimensionMismatch](a && b)
intercept[MatrixDimensionMismatch](a || b)
}

end MatrixBooleanSuite
39 changes: 39 additions & 0 deletions vecxt/test/src/matrix.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,45 @@ class MatrixExtensionSuite extends FunSuite:
assertMatrixEquals(prodC, Matrix[Double](Array[Double](8.0, 90.0), (1, 2)))
}

// mean(dim) divides sum(dim) by the length of the collapsed axis. Non-square matrices throughout, so dividing by
// the kept axis instead (rows vs cols) would give a different answer.

test("mean along dimension, column-major") {
// [[1,5],[4,3],[2,6]]
val mat1 = Matrix[Double](Array(1.0, 4.0, 2.0, 5.0, 3.0, 6.0), (3, 2))

assertMatrixEquals(mat1.mean(Rows), Matrix[Double](Array(3.0, 3.5, 4.0), (3, 1)))
assertMatrixEquals(mat1.mean(Cols), Matrix[Double](Array(7.0 / 3, 14.0 / 3), (1, 2)))
assertMatrixEquals(mat1.mean(0), mat1.mean(Rows))
assertMatrixEquals(mat1.mean(1), mat1.mean(Cols))
}

test("mean along dimension, row-major") {
val mat1 = Matrix[Double](Array(1.0, 5.0, 4.0, 3.0, 2.0, 6.0), 3, 2, 2, 1, 0)
assert(mat1.isDenseRowMajor)

assertMatrixEquals(mat1.mean(Rows), Matrix[Double](Array(3.0, 3.5, 4.0), (3, 1)))
assertMatrixEquals(mat1.mean(Cols), Matrix[Double](Array(7.0 / 3, 14.0 / 3), (1, 2)))
}

test("mean along dimension, strided view") {
// [[1,2,3],[4,5,6]] sliced out of the 3x3
val view = mat1to9(0 to 1, 0 to 2)

assertMatrixEquals(view.mean(Rows), Matrix[Double](Array(2.0, 5.0), (2, 1)))
assertMatrixEquals(view.mean(Cols), Matrix[Double](Array(2.5, 3.5, 4.5), (1, 3)))
}

test("mean along dimension agrees with overall mean") {
val mat1 = Matrix[Double](Array(1.0, 4.0, 2.0, 5.0, 3.0, 6.0), (3, 2))
assertEqualsDouble(mat1.mean(Rows).mean, mat1.mean, 1e-12)
assertEqualsDouble(mat1.mean(Cols).mean, mat1.mean, 1e-12)
}

test("mean along invalid dimension throws") {
intercept[InvalidDimensionException](mat1to9.mean(2))
}

// reduceAlongDimension (which backs sum/min/max/product along a dimension) has no dedicated test at all for
// Float or Int, row-major or column-major — this covers the same bug as the Double row-major tests above, for
// both remaining types it was fixed in, using the same logical matrix ([[1,5],[4,3],[2,6]]) throughout.
Expand Down
Loading
Loading