From c55e9a81f8fc759f62c542935a6dd5d83e5ba717 Mon Sep 17 00:00:00 2001 From: rec0de Date: Wed, 26 Jan 2022 21:40:37 +0100 Subject: [PATCH] Move move budget management into LS strategy, tweak LS MaxSpace --- .../algorithms/localsearch/LocalSearch.kt | 11 +- .../localsearch/LocalSearchStrategy.kt | 3 +- src/commonMain/kotlin/binpack/Container.kt | 3 + .../localsearch/LocalSequenceStrategy.kt | 8 +- .../localsearch/MaximalSpaceStrategy.kt | 147 +++++++++++++----- 5 files changed, 122 insertions(+), 50 deletions(-) diff --git a/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt b/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt index 18aa043..1071cb2 100644 --- a/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt +++ b/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt @@ -16,15 +16,13 @@ class LocalSearch(private val strategy: LocalSearchStra fun optimizeStep(stepLimit: Int): Pair { var step = 0 var noImprovement = 0 - val explorationLimit = 500 val noImprovementLimit = 5 while(step < stepLimit) { - // find best neighboring solution - var consideredMoves = strategy.neighboringSolutions(solution).shuffled() + strategy.perIterationSharedSetup(solution) - if(consideredMoves.size > explorationLimit) - consideredMoves = consideredMoves.subList(0, explorationLimit) + // find best neighboring solution + val consideredMoves = strategy.neighboringSolutions(solution) val bestNextStep = consideredMoves.minByOrNull { strategy.deltaScoreMove(solution, bestCost, it) } val bestNextStepDelta = if(bestNextStep == null) 0.0 else strategy.deltaScoreMove(solution, bestCost, bestNextStep) @@ -39,9 +37,10 @@ class LocalSearch(private val strategy: LocalSearchStra continue } else { - Logger.log("Applied move: $bestNextStep for delta of $bestNextStepDelta") + //Logger.log("Applied move: $bestNextStep for delta of $bestNextStepDelta") solution = strategy.applyMove(solution, bestNextStep!!) bestCost = strategy.scoreSolution(solution) + Logger.log("Best cost: $bestCost") noImprovement = 0 } diff --git a/src/commonMain/kotlin/algorithms/localsearch/LocalSearchStrategy.kt b/src/commonMain/kotlin/algorithms/localsearch/LocalSearchStrategy.kt index d24913c..952a5d7 100644 --- a/src/commonMain/kotlin/algorithms/localsearch/LocalSearchStrategy.kt +++ b/src/commonMain/kotlin/algorithms/localsearch/LocalSearchStrategy.kt @@ -3,7 +3,8 @@ package algorithms.localsearch interface LocalSearchStrategy { fun init(instance: Problem) fun initialSolution(): Solution - fun neighboringSolutions(solution: Solution): Iterable + fun neighboringSolutions(solution: Solution): List + fun perIterationSharedSetup(solution: Solution) {} fun deltaScoreMove(solution: Solution, currentScore: Double, move: Move): Double fun applyMove(solution: Solution, move: Move): Solution fun scoreSolution(solution: Solution): Double diff --git a/src/commonMain/kotlin/binpack/Container.kt b/src/commonMain/kotlin/binpack/Container.kt index 8899693..b031858 100644 --- a/src/commonMain/kotlin/binpack/Container.kt +++ b/src/commonMain/kotlin/binpack/Container.kt @@ -7,6 +7,9 @@ interface Container { val hasAccessibleSpace: Boolean val freeSpace: Int + val area: Int + get() = size * size + fun add(box: PlacedBox) fun clone(): Container } \ No newline at end of file diff --git a/src/commonMain/kotlin/binpack/localsearch/LocalSequenceStrategy.kt b/src/commonMain/kotlin/binpack/localsearch/LocalSequenceStrategy.kt index 7b8c273..a293eca 100644 --- a/src/commonMain/kotlin/binpack/localsearch/LocalSequenceStrategy.kt +++ b/src/commonMain/kotlin/binpack/localsearch/LocalSequenceStrategy.kt @@ -10,6 +10,7 @@ import kotlin.math.max class LocalSequenceStrategy(private val packer: GenericBinPacker) : LocalSearchStrategy, LSSMove> { private lateinit var instance: BinPackProblem + private val moveBudget = 500 override fun init(instance: BinPackProblem) { this.instance = instance @@ -21,7 +22,7 @@ class LocalSequenceStrategy(private val packer: GenericBinPacker): Iterable { + override fun neighboringSolutions(solution: ContainerSolution): List { val containers = solution.containerObjs // Try reflowing boxes into containers with accessible space @@ -43,8 +44,9 @@ class LocalSequenceStrategy(private val packer: GenericBinPacker moveBudget) combined.shuffled().subList(0, moveBudget) else combined } override fun deltaScoreMove(solution: ContainerSolution, currentScore: Double, move: LSSMove): Double { diff --git a/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt b/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt index 415d430..ad29f7c 100644 --- a/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt +++ b/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt @@ -1,14 +1,14 @@ package binpack.localsearch import algorithms.localsearch.LocalSearchStrategy -import binpack.BinPackProblem -import binpack.PlacedBox -import binpack.SpaceContainer -import binpack.SpaceContainerSolution +import binpack.* class MaximalSpaceStrategy : LocalSearchStrategy { private lateinit var instance: BinPackProblem private val estimateFactor: Double = 1.5 + private val moveBudget = 2000 + private var moveIndex = 0 + private var repackEstimationAvailableSpaces: List>? = null override fun init(instance: BinPackProblem) { this.instance = instance @@ -16,7 +16,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy + val containers = instance.boxes.sortedByDescending { it.area }.mapIndexed { i, box -> val c = SpaceContainer(i, instance.containerSize) c.add(box, c.spaces[0]) c @@ -24,7 +24,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy { + override fun neighboringSolutions(solution: SpaceContainerSolution): List { val containers = solution.containerObjs val estimatedRange = (instance.lowerBound * estimateFactor).toInt() @@ -59,25 +59,47 @@ class MaximalSpaceStrategy : LocalSearchStrategy) : List { - val moves = mutableListOf() - - containers.forEachIndexed { ci, container -> - container.boxes.indices.forEach{ bi -> - // Generate local moves - container.spaces.indices.forEach { si -> - moves.add(LocalMove(ci, bi, si)) + var repackMoves = mutableListOf() + var localMoves = mutableListOf() + + val sourceCandidates = containers.filter { it.freeSpace.toDouble() / it.area > 0.2 } + + sourceCandidates.forEach { container -> + val ci = container.ci + container.boxes.forEachIndexed { bi, box -> + // Generate local moves only for a subset of containers + if(ci % 5 == moveIndex) { + container.spaces.indices.forEach { si -> + localMoves.add(LocalMove(ci, bi, si)) + } } // Generate cross-container repack moves (0 until ci).forEach { tci -> - containers[tci].spaces.indices.forEach { si -> - moves.add(RepackMove(ci, bi, tci, si)) + val target = containers[tci] + if(target.hasAccessibleSpace) { + target.spaces.indices.forEach { si -> + repackMoves.add(RepackMove(ci, bi, tci, si)) + } } } } } - return moves + //Logger.log("Move count: ${repackMoves.size} repack; ${localMoves.size} local; ${sourceCandidates.size} sc out of ${containers.size}") + + if(localMoves.size > moveBudget * 0.2) { + localMoves.shuffle() + localMoves = localMoves.subList(0, (moveBudget * 0.2).toInt()) + } + + if(repackMoves.size > moveBudget - localMoves.size) { + repackMoves.shuffle() + repackMoves = repackMoves.subList(0, moveBudget - localMoves.size) + } + + moveIndex = (moveIndex + 1) % 5 + return localMoves + repackMoves } override fun deltaScoreMove(solution: SpaceContainerSolution, currentScore: Double, move: MSSMove): Double { @@ -85,10 +107,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy { val sourceContainer = solution.containerObjs[move.sourceContainer] val box = sourceContainer.boxes[move.sourceBox] - val space = solution.containerObjs[move.targetContainer].spaces[move.targetSpace] - //box.area * (1.0 / move.sourceContainer - 1.0 / move.targetContainer) - //val targetCostDelta = shatter(space, box).sumOf { it.area }.toDouble() - space.area val targetCostDelta = - box.area.toDouble() val sourceCostDelta = if(sourceContainer.boxes.size == 1) 0.0 else box.area.toDouble() @@ -108,7 +127,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy 0.0 } } + override fun perIterationSharedSetup(solution: SpaceContainerSolution) { + if(solution.containerObjs.size <= (instance.lowerBound * estimateFactor).toInt()) { + repackEstimationAvailableSpaces = solution.containerObjs.flatMap { c -> c.spaces.map { Pair(c.ci, it) } } + } + } + override fun applyMove(solution: SpaceContainerSolution, move: MSSMove): SpaceContainerSolution { return when(move) { is CrossContainerMove -> applyCrossContainerMove(solution, move) @@ -157,7 +185,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy container.ci -= 1 } + } + + // Re-use of move.targetContainer index is safe because targetContainer is guaranteed to be before source container, so index is unchanged + if(spillover.isNotEmpty() && !shallowGlobalRepack(newContainers, move.targetContainer, spillover)) + throw Exception("Local repack produced spillover and global shallow repack failed") return SpaceContainerSolution(solution.containerSize, newContainers) } @@ -204,8 +235,10 @@ class MaximalSpaceStrategy : LocalSearchStrategy container.ci -= 1 } + } else { consolidateSpaces(newSource) newContainers[move.sourceContainer] = newSource @@ -222,10 +255,38 @@ class MaximalSpaceStrategy : LocalSearchStrategy container.spaces.sumOf{ it.area } * (1.0 / (index+1))}.sum() + return solution.containerObjs.sumOf { container -> + container.spaces.sumOf { it.area }.toDouble() / (container.ci + 1) + } + } + + private fun estimateShallowGlobalRepack(solution: SpaceContainerSolution, sourceContainer: Int, additionalAvoid: Int?, boxes: List): Double { + if(boxes.isEmpty()) + return 0.0 + + var delta = boxes.sumOf { it.area }.toDouble() / (1 + sourceContainer) + val availableSpaces = repackEstimationAvailableSpaces!! + val usedSpaces: MutableSet> = mutableSetOf() + + boxes.sortedByDescending { it.area }.forEach { box -> + val space = availableSpaces.firstOrNull { it.first != sourceContainer && it.first != additionalAvoid && it.second.fitsRotated(box) && !usedSpaces.contains(it) } ?: return Double.POSITIVE_INFINITY + usedSpaces.add(space) + delta -= box.area / (1 + space.first) + } + + return delta } - private fun localRepack(container: SpaceContainer, placed: PlacedBox) : Boolean { + private fun shallowGlobalRepack(containers: List, sourceContainer: Int, boxes: List) : Boolean { + boxes.sortedByDescending { it.area }.forEach { box -> + val target = containers.firstOrNull { it.ci != sourceContainer && it.freeSpace >= box.area && it.spaces.any { space -> space.fitsRotated(box) } } ?: return false + val space = target.spaces.first{ it.fitsRotated(box) } + target.add(box, space) + } + return true + } + + private fun localRepack(container: SpaceContainer, placed: PlacedBox) : List { // Check bounds if(placed.outOfBounds(container.size)) throw Exception("Local repack box $placed is out of bounds") @@ -245,16 +306,22 @@ class MaximalSpaceStrategy : LocalSearchStrategy) : Boolean { + private fun repackIntoContainer(c: SpaceContainer, boxes: Collection) : List { + val overflow = mutableListOf() consolidateSpaces(c) boxes.forEach { repackBox -> - val fit = c.spaces.firstOrNull { it.fitsRotated(repackBox) } ?: return false - c.add(repackBox, fit) - consolidateSpaces(c) + val fit = c.spaces.firstOrNull { it.fitsRotated(repackBox) } + + if(fit == null) + overflow.add(repackBox) + else { + c.add(repackBox, fit) + consolidateSpaces(c) + } } - return true + return overflow } private fun consolidateSpaces(c: SpaceContainer) {