Skip to content

Commit

Permalink
Move move budget management into LS strategy, tweak LS MaxSpace
Browse files Browse the repository at this point in the history
  • Loading branch information
rec0de committed Jan 26, 2022
1 parent 95d92fd commit c55e9a8
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 50 deletions.
11 changes: 5 additions & 6 deletions src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ class LocalSearch<Problem, Solution, Move>(private val strategy: LocalSearchStra
fun optimizeStep(stepLimit: Int): Pair<Solution, Boolean> {
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)
Expand All @@ -39,9 +37,10 @@ class LocalSearch<Problem, Solution, Move>(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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package algorithms.localsearch
interface LocalSearchStrategy<Problem, Solution, Move> {
fun init(instance: Problem)
fun initialSolution(): Solution
fun neighboringSolutions(solution: Solution): Iterable<Move>
fun neighboringSolutions(solution: Solution): List<Move>
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
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/binpack/Container.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlin.math.max

class LocalSequenceStrategy(private val packer: GenericBinPacker<SegmentContainer>) : LocalSearchStrategy<BinPackProblem, ContainerSolution<SegmentContainer>, LSSMove> {
private lateinit var instance: BinPackProblem
private val moveBudget = 500

override fun init(instance: BinPackProblem) {
this.instance = instance
Expand All @@ -21,7 +22,7 @@ class LocalSequenceStrategy(private val packer: GenericBinPacker<SegmentContaine
return packer.getSolution()
}

override fun neighboringSolutions(solution: ContainerSolution<SegmentContainer>): Iterable<LSSMove> {
override fun neighboringSolutions(solution: ContainerSolution<SegmentContainer>): List<LSSMove> {
val containers = solution.containerObjs

// Try reflowing boxes into containers with accessible space
Expand All @@ -43,8 +44,9 @@ class LocalSequenceStrategy(private val packer: GenericBinPacker<SegmentContaine
}
}

Logger.log("Move count: ${reflows.size} reflow ops, ${swaps.size} swap ops, ${inserts.size} insert ops")
return reflows + swaps + inserts
//Logger.log("Move count: ${reflows.size} reflow ops, ${swaps.size} swap ops, ${inserts.size} insert ops")
val combined = reflows + swaps + inserts
return if(combined.size > moveBudget) combined.shuffled().subList(0, moveBudget) else combined
}

override fun deltaScoreMove(solution: ContainerSolution<SegmentContainer>, currentScore: Double, move: LSSMove): Double {
Expand Down
147 changes: 107 additions & 40 deletions src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
package binpack.localsearch

import algorithms.localsearch.LocalSearchStrategy
import binpack.BinPackProblem
import binpack.PlacedBox
import binpack.SpaceContainer
import binpack.SpaceContainerSolution
import binpack.*

class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerSolution, MSSMove> {
private lateinit var instance: BinPackProblem
private val estimateFactor: Double = 1.5
private val moveBudget = 2000
private var moveIndex = 0
private var repackEstimationAvailableSpaces: List<Pair<Int,PlacedBox>>? = null

override fun init(instance: BinPackProblem) {
this.instance = instance
}

override fun initialSolution(): SpaceContainerSolution {
// Place every box in its own container
val containers = instance.boxes.mapIndexed { i, box ->
val containers = instance.boxes.sortedByDescending { it.area }.mapIndexed { i, box ->
val c = SpaceContainer(i, instance.containerSize)
c.add(box, c.spaces[0])
c
}
return SpaceContainerSolution(instance.containerSize, containers)
}

override fun neighboringSolutions(solution: SpaceContainerSolution): Iterable<MSSMove> {
override fun neighboringSolutions(solution: SpaceContainerSolution): List<MSSMove> {
val containers = solution.containerObjs
val estimatedRange = (instance.lowerBound * estimateFactor).toInt()

Expand Down Expand Up @@ -59,36 +59,55 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
}

private fun lateNeighborhood(containers: List<SpaceContainer>) : List<MSSMove> {
val moves = mutableListOf<MSSMove>()

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<MSSMove>()
var localMoves = mutableListOf<MSSMove>()

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 {
return when(move) {
is CrossContainerMove -> {
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()

Expand All @@ -108,7 +127,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
else {
val cloned = container.clone()
cloned.remove(box)
val success = localRepack(cloned, box.asPlaced(space.x, space.y))
val success = localRepack(cloned, box.asPlaced(space.x, space.y)).isEmpty()
if(success)
cloned.spaces.size.toDouble() - container.spaces.size
else
Expand All @@ -126,18 +145,27 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
0.0
else {
val cloned = target.clone()
val emptiesSource = source.boxes.size == 1
val spillover = localRepack(cloned, box.asPlaced(space.x, space.y))

val success = localRepack(cloned, box.asPlaced(space.x, space.y))
if(success)
box.area.toDouble() / (move.sourceContainer + 1) - box.area.toDouble() / (move.targetContainer + 1)
else
0.0
// If the source would be emptied, we have to explicitly forbid packing spillover there
val globalRepackCost = estimateShallowGlobalRepack(solution, target.ci, if(emptiesSource) source.ci else null, spillover)
val targetCost = - box.area.toDouble()
val sourceCost = if(emptiesSource) box.area - source.area.toDouble() else box.area.toDouble()

globalRepackCost + sourceCost / (move.sourceContainer + 1) + targetCost / (move.targetContainer + 1)
}
}
else -> 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)
Expand All @@ -157,7 +185,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
// rotation?
val placed = box.asPlaced(space.x, space.y)

val repackSuccess = localRepack(container, placed)
val repackSuccess = localRepack(container, placed).isEmpty()

if(!repackSuccess)
throw Exception("Local repack failed, not enough space available")
Expand All @@ -179,14 +207,17 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS

// rotation?
val placed = box.asPlaced(space.x, space.y)
val spillover = localRepack(target, placed)

val repackSuccess = localRepack(target, placed)

if(!repackSuccess)
throw Exception("Repack failed, not enough space available")

if(source.boxes.isEmpty())
// Remove container if now empty
if(source.boxes.isEmpty()) {
newContainers.removeAt(move.sourceContainer)
newContainers.subList(move.sourceContainer, newContainers.size).forEach { container -> 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)
}
Expand All @@ -204,8 +235,10 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS

newContainers[move.targetContainer] = newTarget

if(newSource.boxes.isEmpty())
if(newSource.boxes.isEmpty()) {
newContainers.removeAt(move.sourceContainer)
newContainers.subList(move.sourceContainer, newContainers.size).forEach { container -> container.ci -= 1 }
}
else {
consolidateSpaces(newSource)
newContainers[move.sourceContainer] = newSource
Expand All @@ -222,10 +255,38 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
}

override fun scoreSolution(solution: SpaceContainerSolution): Double {
return solution.containerObjs.mapIndexed { index, container -> 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<Box>): Double {
if(boxes.isEmpty())
return 0.0

var delta = boxes.sumOf { it.area }.toDouble() / (1 + sourceContainer)
val availableSpaces = repackEstimationAvailableSpaces!!
val usedSpaces: MutableSet<Pair<Int,PlacedBox>> = 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<SpaceContainer>, sourceContainer: Int, boxes: List<Box>) : 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<Box> {
// Check bounds
if(placed.outOfBounds(container.size))
throw Exception("Local repack box $placed is out of bounds")
Expand All @@ -245,16 +306,22 @@ class MaximalSpaceStrategy : LocalSearchStrategy<BinPackProblem, SpaceContainerS
return repackIntoContainer(container, toRepack)
}

private fun repackIntoContainer(c: SpaceContainer, boxes: Collection<PlacedBox>) : Boolean {
private fun repackIntoContainer(c: SpaceContainer, boxes: Collection<PlacedBox>) : List<Box> {
val overflow = mutableListOf<Box>()
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) {
Expand Down

0 comments on commit c55e9a8

Please sign in to comment.