SortingNetworksSmokeTest.kt
src/jvmMain/kotlin/afield/examples/SortingNetworksSmokeTest.kt · 226 lines
package afield.examples
import afield.data.sorting.SortingComparator
import afield.data.sorting.SortingNetwork
FIELD_TEST id: stdlib.sorting-networks.smoke runner: jvm-main entrypoint: afield.examples.SortingNetworksSmokeTest tags: [smoke, stdlib, sorting-networks] timeoutSec: 60 expect: exitCode: 0 stdoutContains: - "✅ SortingNetworks smoke tests passed"
object SortingNetworksSmokeTest
Smoke tests for SortingNetwork: build, notate, verify, visualise (FLine), and simulated-annealing optimisation.
object SortingNetworksSmokeTest {
fun runTests(): Boolean
fun runTests(): Boolean {
return try {
testBubbleIsValid()
testOddEvenMergeIsValid()
testSortProducesCorrectOrder()
testLayerTrace()
testNotationRoundTrip()
testFromNotationRejectsInvalid()
testLayersAreDisjoint()
testDepth()
testAsciiDiagramNonEmpty()
testToFLinesNonEmpty()
testAnnealImprovesBubble()
testAnnealResultIsValid()
println("✅ SortingNetworks smoke tests passed")
true
} catch (t: Throwable) {
println("❌ SortingNetworks smoke tests FAILED: ${t.message}")
t.printStackTrace()
false
}
}
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
check(runTests()) { "SortingNetworks smoke tests failed" }
}
bubble
private fun testBubbleIsValid() {
for (n in 2..8) {
val net = SortingNetwork.bubble(n)
require(net.isSorting()) { "bubble($n) must be a valid sorting network" }
require(net.comparators.size == n * (n - 1) / 2) {
"bubble($n) should have ${n*(n-1)/2} comparators, got ${net.comparators.size}"
}
}
}
oddEvenMerge
private fun testOddEvenMergeIsValid() {
// Known optimal comparator counts: n=2→1, n=3→3, n=4→5, n=5→9, n=6→12, n=7→16, n=8→19
val knownOptima = mapOf(2 to 1, 3 to 3, 4 to 5, 5 to 9, 6 to 12, 7 to 16, 8 to 19)
for (n in 2..9) {
val net = SortingNetwork.oddEvenMerge(n)
require(net.isSorting()) { "oddEvenMerge($n) must be a valid sorting network" }
// Should be at most bubble sort size for n >= 4
if (n >= 4) {
val bubbleSize = n * (n - 1) / 2
require(net.comparators.size <= bubbleSize) {
"oddEvenMerge($n) has ${net.comparators.size} comparators but bubble has $bubbleSize"
}
}
// For n ≤ 8, Batcher's algorithm produces optimal counts
knownOptima[n]?.let { expected ->
require(net.comparators.size == expected) {
"oddEvenMerge($n) should have $expected comparators (optimal), got ${net.comparators.size}"
}
}
}
}
sort
private fun testSortProducesCorrectOrder() {
val net = SortingNetwork.oddEvenMerge(5)
val inputs = listOf(
intArrayOf(5, 3, 1, 4, 2),
intArrayOf(1, 2, 3, 4, 5),
intArrayOf(5, 4, 3, 2, 1),
intArrayOf(3, 3, 3, 3, 3),
intArrayOf(2, 1, 4, 3, 5),
)
inputs.forEach { input ->
val sorted = net.sort(input)
for (i in 0 until sorted.size - 1) {
require(sorted[i] <= sorted[i + 1]) {
"sort(${input.toList()}) produced unsorted output ${sorted.toList()}"
}
}
}
}
private fun testLayerTrace() {
val net = SortingNetwork.oddEvenMerge(5)
val trace = net.layerTrace(intArrayOf(5, 1, 4, 2, 3))
require(trace.size == net.depth() + 1) {
"layerTrace() should return depth+1 snapshots, got ${trace.size}"
}
val last = trace.last()
require(last.contentEquals(intArrayOf(1, 2, 3, 4, 5))) {
"layerTrace() final state should be sorted, got ${last.toList()}"
}
}
notation
private fun testNotationRoundTrip() {
for (n in 2..7) {
val net = SortingNetwork.oddEvenMerge(n)
val notation = net.notation()
require(notation.isNotBlank()) { "notation() must not be blank for n=$n" }
val parsed = SortingNetwork.fromNotation(n, notation)
// Parsed networks should have the same comparator set (possibly different order,
// but since fromNotation preserves layer order they should be equal).
require(parsed.isSorting()) { "Parsed network for n=$n must be valid" }
require(parsed.comparators.size == net.comparators.size) {
"Round-trip changed comparator count for n=$n"
}
}
}
private fun testFromNotationRejectsInvalid() {
// A hand-crafted invalid notation (missing comparators)
val invalid = SortingNetwork.fromNotation(4, "0:1")
require(!invalid.isSorting()) { "A single comparator for n=4 should NOT sort all inputs" }
}
layers
private fun testLayersAreDisjoint() {
val net = SortingNetwork.oddEvenMerge(6)
net.layers().forEach { layer ->
val wires = mutableSetOf<Int>()
layer.forEach { comp ->
require(wires.add(comp.lo)) { "Wire ${comp.lo} appears twice in the same layer" }
require(wires.add(comp.hi)) { "Wire ${comp.hi} appears twice in the same layer" }
}
}
}
private fun testDepth() {
// Under ASAP scheduling, the standard left-to-right bubble comparator order
// has depth 2n-3.
for (n in 2..6) {
val depth = SortingNetwork.bubble(n).depth()
val expected = 2 * n - 3
require(depth == expected) { "bubble($n) should have depth $expected, got $depth" }
}
// oddEvenMerge should have depth ≤ bubble depth
for (n in 4..8) {
val oemDepth = SortingNetwork.oddEvenMerge(n).depth()
val bubDepth = SortingNetwork.bubble(n).depth()
require(oemDepth <= bubDepth) {
"oddEvenMerge($n) depth $oemDepth should be ≤ bubble depth $bubDepth"
}
}
}
ASCII diagram
private fun testAsciiDiagramNonEmpty() {
val diag = SortingNetwork.oddEvenMerge(4).asciiDiagram()
require(diag.isNotBlank()) { "asciiDiagram() must not be blank" }
// Should contain at least one ● marker
require(diag.contains('●')) { "asciiDiagram() must contain ● endpoint markers" }
}
toFLines
private fun testToFLinesNonEmpty() {
val flines = SortingNetwork.oddEvenMerge(4).toFLines()
require(flines.isNotEmpty()) { "toFLines() must return a non-empty list" }
// Should have at least the wire FLine plus one FLine per layer
val net = SortingNetwork.oddEvenMerge(4)
val minExpected = 1 + net.depth() // wires + at least one FLine per layer
require(flines.size >= minExpected) {
"toFLines() returned ${flines.size} FLines, expected >= $minExpected"
}
// Every FLine must have at least one node
flines.forEach { f ->
require(f.nodes.isNotEmpty()) { "toFLines() returned an FLine with no nodes" }
}
}
simulated annealing
private fun testAnnealImprovesBubble() {
// SA should reduce bubble(4)=6 comparators; optimal for n=4 is 5.
val opt = SortingNetwork.anneal(n = 4, seed = 42L)
val bubbleSize = SortingNetwork.bubble(4).comparators.size // 6
require(opt.comparators.size <= bubbleSize) {
"anneal(4) should find ≤ $bubbleSize comparators, got ${opt.comparators.size}"
}
}
private fun testAnnealResultIsValid() {
for (n in 2..6) {
var callbackCount = 0
val opt = SortingNetwork.anneal(n = n, seed = 7L)
require(opt.isSorting()) { "anneal(n=$n) result must be a valid sorting network" }
SortingNetwork.anneal(n = n, seed = 7L, iterations = 2_500, progressEvery = 500) { _, _ ->
callbackCount++
}
require(callbackCount >= 5) { "anneal progress callback should have fired repeatedly, got $callbackCount" }
}
}
}