From 2b1b157a80ccd4af2889604111351b72dfd5b8b2 Mon Sep 17 00:00:00 2001 From: jedlimlx Date: Sun, 5 May 2024 16:49:12 +0800 Subject: [PATCH] Allow some profiling of the search --- src/commonMain/kotlin/search/cfind/CFind.kt | 149 +++++++++++++++----- src/commonMain/kotlin/search/cfind/Node.kt | 18 ++- src/commonMain/kotlin/search/cfind/Row.kt | 45 +++--- src/commonTest/kotlin/search/CFindTest.kt | 40 ++++-- src/jvmMain/kotlin/Main.jvm.kt | 24 +++- src/jvmTest/kotlin/Test.kt | 35 ++++- 6 files changed, 230 insertions(+), 81 deletions(-) diff --git a/src/commonMain/kotlin/search/cfind/CFind.kt b/src/commonMain/kotlin/search/cfind/CFind.kt index f514308..c9190fc 100644 --- a/src/commonMain/kotlin/search/cfind/CFind.kt +++ b/src/commonMain/kotlin/search/cfind/CFind.kt @@ -21,6 +21,7 @@ import simulation.Grid import kotlin.math.pow import kotlin.time.TimeSource import kotlin.random.Random +import kotlin.time.measureTime /** * Searches for spaceships using a method similar to the method used by gfind, which was described by David Eppstein in @@ -48,6 +49,7 @@ class CFind( val stdin: Boolean = false, val partialFiles: List = listOf(), partialFileFrequency: Int = -1, + val profiling: String? = null, verbosity: Int = 0 ): SearchProgram(verbosity) { override val searchResults: MutableList = mutableListOf() @@ -187,7 +189,6 @@ class CFind( BCs.reversed() } - val rightBC: List = run { val minX = baseCoordinates.last() val maxX = neighbourhood[0].maxBy { it.x } @@ -235,7 +236,7 @@ class CFind( // Initialising the transposition table @OptIn(ExperimentalUnsignedTypes::class) - val equivalentStates: LRUCache = LRUCache(transpositionTableSize) + val equivalentStates: LRUCache = LRUCache(transpositionTableSize) // Computing the rows that should be used in computing the next state val indices = Array(period) { phase -> @@ -247,6 +248,7 @@ class CFind( } else centralHeight * period - backOff[phase] } } + val minIndices = indices.map { it.min() } private val tempIndices = run { val lst = arrayListOf(indices) @@ -346,6 +348,7 @@ class CFind( listOf() } }.toList() + val minX = if (lookaheadNeighbourhood[0].isNotEmpty()) lookaheadNeighbourhood[0].minOf { (it, _) -> it.x } else 0 // Building lookup tables val numEquivalentStates: Int = rule.equivalentStates.distinct().size @@ -574,6 +577,21 @@ class CFind( else leftBC.size ).filter { it !in ignoreBCs } + // Split the boundary conditions by at which depth they apply + // TODO idk why this doesn't work +// val splitRightBCs = (0 ..< spacing).map { depth -> +// filteredRightBCs.filter { +// val coordinate = -it + lastBaseCoordinate +// coordinate.x.mod(spacing) == offsets[(depth - coordinate.y * period).mod(offsets.size)] +// } +// } +// val splitLeftBCs = (0 ..< spacing).map { depth -> +// filteredLeftBCs.filter { +// val coordinate = -it + Coordinate(width * spacing - 1, 0) + lastBaseCoordinate +// coordinate.x.mod(spacing) == offsets[(depth - coordinate.y * period).mod(offsets.size)] +// } +// } + // Opening the partial files val partialFileStreams = partialFiles.map { SystemFileSystem.sink(Path(it)).buffered() } @@ -622,7 +640,6 @@ class CFind( println((bold("Skipped Rows: ") + "${skippedRows.toList()}"), verbosity = 1) println((bold("Index to Row: ") + "${indexToRowMap.toList()}"), verbosity = 1) - println(brightRed(bold("\nLattice\n----------------")), verbosity = 1) println((bold("Basis Vectors: ") + "${basisVectors.first} / ${basisVectors.second}"), verbosity = 1) println((bold("Spacing: ") + "$spacing"), verbosity = 1) @@ -680,6 +697,14 @@ class CFind( var clearPartial = false var clearLines = 0 + // Some statistics that we will be keeping track of + var avgSuccessorTime = 0.0 + val queueSizes: ArrayList = arrayListOf() + val times: ArrayList = arrayListOf() + val numSuccessorList = ArrayList>() + + val numSuccessors = HashMap() + // Some common functions fun processSuccessors(successors: List): List = if (currentRow.successorSequence != null) { // This optimisation is possible because of the nature of depth-first search @@ -735,6 +760,20 @@ class CFind( "${green("$shipsFound")} ship${if (shipsFound == 1) "" else "s"} found." ) ) + + // Writing to the profiling file + if (profiling != null) { + val data = StringBuilder().apply { + append("queue_size,avg_time,num_successors\n") + for (i in 0.. + avgSuccessorTime = avgSuccessorTime * 0.999 + measureTime { + val (rows, lookaheadRows) = extractRows(currentRow) + successors = nextRow(currentRow, rows, lookaheadRows, depth = currentRow.depth + 1).first + }.inWholeNanoseconds * 0.001 + if (successors.size in numSuccessors) + numSuccessors[successors.size] = numSuccessors[successors.size]!! + 1 + else numSuccessors[successors.size] = 1 // Adding the new rows to the linked list val temp = processSuccessors(successors) @@ -776,6 +821,14 @@ class CFind( tail = it } + if (count.mod(1000) == 0) { + queueSizes.add(queueSize) + times.add(avgSuccessorTime) + numSuccessorList.add(numSuccessors.toMap()) + + numSuccessors.clear() + } + // Printing out the partials printPartials(bold("\nQueue Size: $queueSize / $maxQueueSize")) } @@ -998,6 +1051,20 @@ class CFind( "${green("$shipsFound")} ship${if (shipsFound == 1) "" else "s"} found." ) ) + + // Writing to the profiling file + if (profiling != null) { + val data = StringBuilder().apply { + append("queue_size,avg_time,num_successors\n") + for (i in 0.., List>> = Pair( indices[(row.depth + 1).mod(period)].map { row.getPredecessor(it - 1)!! }.toList(), @@ -1337,8 +1404,8 @@ class CFind( fun lookup(it: Int, row: IntArray? = null): IntArray { // Ensures that no effort is wasted if the row could never succeed if (memo[it] == null) { - if (lookaheadMemo != null && lookaheadMemo[it] == -1) { - memo[it] = successorTable[ + memo[it] = if (lookaheadMemo != null && lookaheadMemo[it] == -1) { + successorTable[ encodeNeighbourhood( translate(Coordinate(it, 0), depth) - lastBaseCoordinate, row, index = it, @@ -1346,7 +1413,7 @@ class CFind( ) ] } else { - memo[it] = successorTable[ + successorTable[ encodeNeighbourhood( translate(Coordinate(it, 0), depth) - lastBaseCoordinate, row, index = it, @@ -1354,11 +1421,9 @@ class CFind( ) ] } - return memo[it]!! - } else { - val temp = memo[it]!! - return temp } + + return memo[it]!! } // Running approximate lookahead for the current row @@ -1418,28 +1483,29 @@ class CFind( // Running another type of approximate lookahead val _lookaheadMemo: IntArray? val _lookaheadMemo2: IntArray? - val minX: Int // Only initialise these if they will actually be used if (lookaheadDepth < this.lookaheadDepth) { _lookaheadMemo = IntArray(width + leftBC.size) { -1 } if (approximateLookahead) { _lookaheadMemo2 = IntArray(width + leftBC.size) { -1 } - minX = lookaheadNeighbourhood[0].minOf { (it, _) -> it.x } } else { _lookaheadMemo2 = null - minX = 0 } } else { _lookaheadMemo = null _lookaheadMemo2 = null - minX = 0 } + val approximateLookaheadRows: List val lookaheadDepthDiff = if (lookaheadDepth < this.lookaheadDepth) { + approximateLookaheadRows = lookaheadRows[0] if (lookaheadDepth - 1 >= 0) lookaheadIndices[lookaheadDepth - 1][depth.mod(period)].filter { it > 0 }.min() - else indices[depth.mod(period)].min() - } else 0 + else minIndices[depth.mod(period)] + } else { + approximateLookaheadRows = listOf() + 0 + } fun approximateLookahead(index: Int, row: Int): Boolean { val index = index - additionalDepthArray[depth.mod(spacing)] if (index < 0) return true @@ -1451,26 +1517,28 @@ class CFind( var key = 0 if (_lookaheadMemo!![index] == -1) { // TODO change depth to the correct depth - for ((it, p) in memorisedlookaheadNeighbourhood[lookaheadDepth]) { - key += lookaheadRows[0][it + coordinate, 0, null, depth] * p - } + for ((it, p) in memorisedlookaheadNeighbourhood[lookaheadDepth]) + key += approximateLookaheadRows[it + coordinate, 0, null, depth] * p _lookaheadMemo[index] = key } else key = _lookaheadMemo[index] for ((it, p) in lookaheadNeighbourhood[0]) { if ((it + coordinate).x >= 0) // TODO consider different backgrounds - key += getDigit(row, pow(numEquivalentStates, (it.x + minX) / spacing), numEquivalentStates) * p + key += getDigit( + row, + pow(numEquivalentStates, (it.x + minX) / spacing), numEquivalentStates + ) * p } _lookaheadMemo2!![index] = key // Adding current cell state & next cell state var power = pow(rule.numStates, mainNeighbourhood.size) - key += lookaheadRows[0][coordinate, 0, null, depth] * power + key += approximateLookaheadRows[coordinate, 0, null, depth] * power power *= rule.numStates - key += lookaheadRows[0][coordinate, 1, null, depth] * power + key += approximateLookaheadRows[coordinate, 1, null, depth] * power return combinedSuccessorArray[key] } @@ -1496,7 +1564,8 @@ class CFind( // Getting the boundary state if (it.y == -centralHeight) { - val lookupTable = lookup(index) + val tempCoordinate = coordinate + lastBaseCoordinate + val lookupTable = lookup(tempCoordinate.x / spacing) val boundaryState = rows[tempCoordinate, 0, cells, depth] // Finally checking the boundary condition @@ -1513,7 +1582,7 @@ class CFind( satisfyBC = bcMemo[it]?.get( inverseBcNeighbourhood[memorisedBCsMap[it]!!].mapIndexed { index, it -> rule.equivalentStates[ - rows[it + offset, 0, node.completeRow, depth] + rows[it + offset, 0, cells, depth] ] * pow(numEquivalentStates, index) }.sum() ) ?: true @@ -1548,15 +1617,24 @@ class CFind( fun deadendNode(node: Node, x: Int) { val num = cacheWidths[x] node.applyOnPredecessor { - if (table[x][it.depth][it.cells.mod(num)] == -1) - table[x][it.depth][it.cells.mod(num)] = 0 + val temp = it.cells.mod(num) + if (table[x][it.depth][temp] == -1) { + table[x][it.depth][temp] = 0 + true + } else false } } // Indicates that this node can reach a completion fun completedNode(node: Node) { for (i in 0..if (approximateLookahead) this.lookaheadDepth - lookaheadDepth else 0) - node.applyOnPredecessor { table[i][it.depth][it.cells.mod(cacheWidths[i])] = 1 } + node.applyOnPredecessor { + val temp = it.cells.mod(cacheWidths[i]) + if (table[i][it.depth][temp] != 1) { + table[i][it.depth][temp] = 1 + true + } else false + } } // Computing the initial key for the inner lookup table @@ -1564,8 +1642,7 @@ class CFind( // Finally running the search val completedRows = arrayListOf() - val stack = ArrayList(10) - stack.add( + val stack = arrayListOf( Node( null, key, @@ -1617,7 +1694,7 @@ class CFind( if (checkBoundaryCondition(node, filteredLeftBCs, offset=Coordinate(width * spacing - 1, 0))) { // Running the lookahead - val row = Row(currentRow, node.completeRow, this) + val row = Row(currentRow, node.completeRow!!, this) if (lookaheadDepth < this.lookaheadDepth) { val newRows = lookaheadRows.mapIndexed { index, rows -> val temp = lookaheadIndices[lookaheadDepth][depth.mod(period)].min() // TODO may not be legit diff --git a/src/commonMain/kotlin/search/cfind/Node.kt b/src/commonMain/kotlin/search/cfind/Node.kt index 6b414cc..41ffc79 100644 --- a/src/commonMain/kotlin/search/cfind/Node.kt +++ b/src/commonMain/kotlin/search/cfind/Node.kt @@ -8,13 +8,21 @@ data class Node( val numStates: Int, val singleBaseCoordinate: Boolean = false ) { - val completeRow: IntArray by lazy { - predecessor?.completeRow?.plus(intArrayOf(prevCell)) ?: intArrayOf() + val completeRow: IntArray? by lazy { + var count = depth - 1 + var tempNode: Node? = this + val temp = IntArray(depth) { 0 } + while (count >= 0) { + temp[count--] = tempNode!!.prevCell + tempNode = tempNode.predecessor + } + + temp + //predecessor?.completeRow?.plus(intArrayOf(prevCell)) ?: intArrayOf() } - fun applyOnPredecessor(f: (Node) -> Unit) { - f(this) - predecessor?.applyOnPredecessor(f) + fun applyOnPredecessor(f: (Node) -> Boolean) { + if (f(this)) predecessor?.applyOnPredecessor(f) } override fun hashCode(): Int { diff --git a/src/commonMain/kotlin/search/cfind/Row.kt b/src/commonMain/kotlin/search/cfind/Row.kt index 9734f0a..e67dfdc 100644 --- a/src/commonMain/kotlin/search/cfind/Row.kt +++ b/src/commonMain/kotlin/search/cfind/Row.kt @@ -4,28 +4,38 @@ import simulation.Coordinate import simulation.DenseGrid import simulation.Grid -class Row(val predecessor: Row?, val cells: IntArray, var search: CFind? = null): Comparable { +class Row( + val predecessor: Row?, val cells: IntArray, var search: CFind? = null, + hash: Int? = null, reverseHash: Int? = null +): Comparable { companion object { var counter: Long = 0L } // unique id for each row val id = counter++ val hash = run { - var hash = 0 - for (i in cells.indices) { - hash += cells[i] * pow(search!!.rule.numStates, i) - } + if (hash == null) { + var _hash = 0 + for (i in cells.indices) + _hash += cells[i] * pow(search!!.rule.numStates, i) - hash + _hash + } else hash } val reverseHash = run { - var hash = 0 - for (i in cells.indices) { - hash += cells[cells.size - i - 1] * pow(search!!.rule.numStates, i) - } + if (reverseHash == null) { + var _hash = 0 + if ( + search!!.isotropic && + (search!!.symmetry == ShipSymmetry.GLIDE || search!!.symmetry == ShipSymmetry.ASYMMETRIC) + ) { + for (i in cells.indices) + _hash += cells[cells.size - i - 1] * pow(search!!.rule.numStates, i) + } - hash + _hash + } else reverseHash } // information about the row and its position within the larger ship @@ -53,10 +63,10 @@ class Row(val predecessor: Row?, val cells: IntArray, var search: CFind? = null) } operator fun get(index: Int): Int { - if (search!!.spacing != 1 && (index - offset).mod(search!!.spacing) != 0) { - println("crap $depth $index $offset") - return 0 - } +// if (search!!.spacing != 1 && (index - offset).mod(search!!.spacing) != 0) { +// println("crap $depth $index $offset") +// return 0 +// } if (search!!.spacing == 1) return cells[index] else return cells[index / search!!.spacing] } @@ -77,7 +87,10 @@ class Row(val predecessor: Row?, val cells: IntArray, var search: CFind? = null) while (predecessor != null) { if (depth - n == predecessor.depth) break - list.add(if (deepCopy) Row(null, predecessor.cells, search!!) else predecessor) + list.add( + if (deepCopy) Row(null, predecessor.cells, search!!, predecessor.hash, predecessor.reverseHash) + else predecessor + ) predecessor = predecessor.predecessor } diff --git a/src/commonTest/kotlin/search/CFindTest.kt b/src/commonTest/kotlin/search/CFindTest.kt index 613d240..378f723 100644 --- a/src/commonTest/kotlin/search/CFindTest.kt +++ b/src/commonTest/kotlin/search/CFindTest.kt @@ -97,7 +97,16 @@ class Test { @Test fun farEdgesTest() { + // TODO Figure out why no ships are found at width 4 + for (strategy in searchStrategies) { + val farEdges = CFind( + HROT("R3,C2,S2-3,B3,N@1000a4250008"), 2, 1, 5, ShipSymmetry.ODD, + verbosity = 1, searchStrategy = strategy + ) + farEdges.search() + assertEquals(farEdges.searchResults.size, 3) + } } // Test different rulespaces @@ -149,11 +158,6 @@ class Test { } } - @Test - fun ruletreeTest() { - - } - // Miscellaneous tests @Test fun dfsTest() { @@ -169,15 +173,15 @@ class Test { } @Test - fun diagonalTest() { // TODO get diagonal searches working for other neighbourhoods + fun diagonalTest() { for (strategy in searchStrategies) { -// val diagonalSearch = CFind( TODO figure out why this test keeps giving inconsistent results -// HROT("B34/S34"), 3, 1, 3, ShipSymmetry.ASYMMETRIC, -// verbosity = 1, direction = Coordinate(1, 1), searchStrategy = strategy -// ) -// diagonalSearch.search() -// -// assertEquals(diagonalSearch.searchResults.size, 2) + val diagonalSearch = CFind( + HROT("B34/S34"), 3, 1, 3, ShipSymmetry.ASYMMETRIC, + verbosity = 1, direction = Coordinate(1, 1), searchStrategy = strategy + ) + diagonalSearch.search() + + assertEquals(diagonalSearch.searchResults.size, 2) val glideDiagonalSearch = CFind( HROT("B3/S23"), 4, 1, 2, ShipSymmetry.GLIDE, @@ -185,7 +189,15 @@ class Test { ) glideDiagonalSearch.search() - assertEquals(glideDiagonalSearch.searchResults.size, 1) + assertEquals(glideDiagonalSearch.searchResults.size, 2) + + val minibugsSearch = CFind( + HROT("R2,C2,S6-9,B7-8,NM"), 3, 1, 2, ShipSymmetry.ODD, + verbosity = 1, searchStrategy = strategy, direction = Coordinate(1 ,1) + ) + minibugsSearch.search() + + assertEquals(minibugsSearch.searchResults.size, 1) } } diff --git a/src/jvmMain/kotlin/Main.jvm.kt b/src/jvmMain/kotlin/Main.jvm.kt index 754574a..b588b74 100644 --- a/src/jvmMain/kotlin/Main.jvm.kt +++ b/src/jvmMain/kotlin/Main.jvm.kt @@ -2,6 +2,10 @@ import it.skrape.core.* import it.skrape.fetcher.* import it.skrape.selects.* import it.skrape.selects.html5.* +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.writeString import rules.hrot.HROT import rules.nontotalistic.rules.INT import rules.nontotalistic.rules.INTGenerations @@ -108,13 +112,19 @@ actual fun main() { // ) // search.search() - val search = CFind( - HROT("R2,C2,S6-9,B7-8,NM"), 3, 1, 6, symmetry = ShipSymmetry.ODD, - verbosity = 1, searchStrategy = SearchStrategy.HYBRID_BFS, partialFrequency = 50000, - backupName = "dump", maxQueueSize = 1 shl 22, numThreads = 8, direction = Coordinate(1 ,1), - backupFrequency = 600//, lookaheadDepth = 0 - ) - search.search() +// val search = CFind( +// HROT("R2,C2,S6-9,B7-8,NM"), 2, 1, 8, +// symmetry = ShipSymmetry.ASYMMETRIC, verbosity = 1, +// searchStrategy = SearchStrategy.PRIORITY_QUEUE, partialFrequency = 50000, +// backupName = "dump", maxQueueSize = 1 shl 22, numThreads = 8, backupFrequency = 600 +// ) +// search.search() + + val search = CFind( + HROT("R2,C2,S6-9,B7-8,NM"), 3, 1, 7, ShipSymmetry.EVEN, + verbosity = 1, searchStrategy = SearchStrategy.HYBRID_BFS, numShips = 1 + ) + search.search() // val search = CFind( // HROT("R2,C2,S6-9,B7-8,NM"), 3, 2, 11, symmetry = ShipSymmetry.EVEN, diff --git a/src/jvmTest/kotlin/Test.kt b/src/jvmTest/kotlin/Test.kt index 1958a3e..e927176 100644 --- a/src/jvmTest/kotlin/Test.kt +++ b/src/jvmTest/kotlin/Test.kt @@ -1,6 +1,8 @@ import rules.hrot.HROT import search.cfind.CFind +import search.cfind.SearchStrategy import search.cfind.ShipSymmetry +import simulation.Coordinate import kotlin.test.Ignore import kotlin.test.Test import kotlin.time.TimeSource @@ -22,7 +24,8 @@ class Test { startTime = timeSource.markNow() val lifeSearchP4K1 = CFind( - HROT("B3/S23"), 4, 1, 7, ShipSymmetry.ODD, verbosity = -1, numShips = 1 + HROT("B3/S23"), 4, 1, 7, ShipSymmetry.ODD, + verbosity = -1, numShips = 1, searchStrategy = SearchStrategy.HYBRID_BFS ) lifeSearchP4K1.search() println("B3/S23, c/4o, width 7, odd: ${(timeSource.markNow() - startTime).inWholeMilliseconds / 1000.0}s") @@ -31,7 +34,8 @@ class Test { startTime = timeSource.markNow() val circularSearchP2K1 = CFind( - HROT("R2,C2,S5-8,B6-7,NC"), 2, 1, 7, ShipSymmetry.EVEN, verbosity = -1 + HROT("R2,C2,S5-8,B6-7,NC"), 2, 1, 7, ShipSymmetry.EVEN, + verbosity = -1, searchStrategy = SearchStrategy.HYBRID_BFS ) circularSearchP2K1.search() println("R2,C2,S5-8,B6-7,NC, c/2o, width 7, even: ${(timeSource.markNow() - startTime).inWholeMilliseconds / 1000.0}s") @@ -41,12 +45,37 @@ class Test { startTime = timeSource.markNow() val minibugsSearch = CFind( - HROT("R2,C2,S6-9,B7-8,NM"), 2, 1, 8, ShipSymmetry.ASYMMETRIC, verbosity = -1, numShips = 1 + HROT("R2,C2,S6-9,B7-8,NM"), 2, 1, 8, ShipSymmetry.ASYMMETRIC, + verbosity = -1, numShips = 1, searchStrategy = SearchStrategy.HYBRID_BFS ) minibugsSearch.search() assert(minibugsSearch.searchResults.size == 1) println("Minibugs, c/2o, width 8, asymmetric: ${(timeSource.markNow() - startTime).inWholeMilliseconds / 1000.0}s") + + startTime = timeSource.markNow() + + val farEdges = CFind( + HROT("R3,C2,S2-3,B3,N@1000a4250008"), 2, 1, 7, ShipSymmetry.ODD, + verbosity = -1, searchStrategy = SearchStrategy.HYBRID_BFS, numShips = 1 + ) + farEdges.search() + + println("Far Edges, c/2o, width 7, odd: ${(timeSource.markNow() - startTime).inWholeMilliseconds / 1000.0}s") + + assert(farEdges.searchResults.size == 1) + + startTime = timeSource.markNow() + + val search = CFind( + HROT("R2,C2,S2,B3,NN"), 4, 1, 8, ShipSymmetry.GLIDE, + verbosity = -1, searchStrategy = SearchStrategy.HYBRID_BFS, direction = Coordinate(1, 1) + ) + search.search() + + println("R2,C2,S2,B3,NN, c/4d, width 8, glide: ${(timeSource.markNow() - startTime).inWholeMilliseconds / 1000.0}s") + + assert(search.searchResults.size == 1) } } \ No newline at end of file