Skip to content

Commit

Permalink
Add OverlapSpace strategy, reskin UI, other things
Browse files Browse the repository at this point in the history
  • Loading branch information
rec0de committed Feb 7, 2022
1 parent c55e9a8 commit de07c5b
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class LocalSearch<Problem, Solution, Move>(private val strategy: LocalSearchStra
//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
11 changes: 10 additions & 1 deletion src/commonMain/kotlin/binpack/BinPackProblem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ class PlacedBox(w: Int, h: Int, val x: Int, val y: Int) : Box(w, h) {
return fragments
}

fun intersects(box: PlacedBox) = !((endX <= box.x) || (box.endX <= x) || (endY <= box.y) || (box.endY <= y))
fun outOfBounds(size: Int) = x < 0 || y < 0 || endX > size || endY > size
fun intersects(box: PlacedBox) = !((endX <= box.x) || (box.endX <= x) || (endY <= box.y) || (box.endY <= y))

fun intersection(box: PlacedBox) : Int {
val xOverlap = max(0, min(endX, box.endX) - max(x, box.x));
val yOverlap = max(0, min(endY, box.endY) - max(y, box.y));
return xOverlap * yOverlap
}
fun relativeOverlap(box: PlacedBox) = intersection(box).toDouble() / max(area, box.area)


}
19 changes: 17 additions & 2 deletions src/commonMain/kotlin/binpack/SpaceContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,23 @@ open class SpaceContainer(
spaces.addAll(space.shatter(placed))
}

open fun remove(box: PlacedBox) {
open fun remove(box: PlacedBox, overlapPossible: Boolean = false) {
boxes.remove(box)
spaces.add(box)
if(overlapPossible) {
val candidateSpaces = mutableListOf(box)
val newSpaces = mutableListOf<PlacedBox>()

while(candidateSpaces.isNotEmpty()) {
val space = candidateSpaces.removeAt(0)
val intersecting = boxes.firstOrNull { it.intersects(space) }
if(intersecting == null)
newSpaces.add(space)
else
candidateSpaces.addAll(space.shatter(intersecting))
}
spaces.addAll(newSpaces)
}
else
spaces.add(box)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package binpack.configurations
import algorithms.greedy.GreedyPacker
import binpack.*
import binpack.greedy.*
import ui.UIState

abstract class GenericGreedyConfig : Algorithm() {
protected lateinit var packer: GreedyPacker<Box,Int,BinPackSolution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package binpack.configurations
import algorithms.localsearch.LocalSearch
import binpack.*
import binpack.greedy.NormalPosCircTouchPacker
import binpack.localsearch.LSSMove
import binpack.localsearch.LocalSequenceStrategy
import binpack.localsearch.MSSMove
import binpack.localsearch.MaximalSpaceStrategy
import binpack.localsearch.*

object LocalSearchLocalSequence : Algorithm() {
override val name = "LocalSearch-LocalSequence"
Expand All @@ -26,9 +23,9 @@ object LocalSearchLocalSequence : Algorithm() {
}
}

object LocalSearchMaximalSpace : Algorithm() {
override val name = "LocalSearch-MaxSpace"
override val shortName = "LS MaxSpace"
object LocalSearchRepackSpace : Algorithm() {
override val name = "LocalSearch-RepackSpace"
override val shortName = "LS RepackSpace"
private lateinit var localsearch: LocalSearch<BinPackProblem, SpaceContainerSolution, MSSMove>

override fun optimize(): BinPackSolution {
Expand All @@ -40,6 +37,24 @@ object LocalSearchMaximalSpace : Algorithm() {
}

override fun init(instance: BinPackProblem) {
localsearch = LocalSearch(MaximalSpaceStrategy(), instance)
localsearch = LocalSearch(RepackSpaceStrategy(), instance)
}
}

object LocalSearchRelaxedSpace : Algorithm() {
override val name = "LocalSearch-RelaxedSpace"
override val shortName = "LS RelaxedSpace"
private lateinit var localsearch: LocalSearch<BinPackProblem, SpaceContainerSolution, MSSMove>

override fun optimize(): BinPackSolution {
return localsearch.optimize()
}

override fun optimizeStep(limit: Int): Pair<SpaceContainerSolution, Boolean> {
return localsearch.optimizeStep(limit)
}

override fun init(instance: BinPackProblem) {
localsearch = LocalSearch(OverlapSpaceStrategy(), instance)
}
}

This file was deleted.

248 changes: 248 additions & 0 deletions src/commonMain/kotlin/binpack/localsearch/OverlapSpaceStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package binpack.localsearch

import Logger
import binpack.SpaceContainer
import binpack.SpaceContainerSolution
import kotlin.math.max

class OverlapSpaceStrategy : RepackSpaceStrategy() {
override val estimateFactor = 1.0
private var allowedOverlap = 1.0
private var overlapPenalty = 1.0
private var consecutivePlaceboMoves = 0
private var forceLateNeighborhood = false

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

override fun perIterationSharedSetup(solution: SpaceContainerSolution) {
if(forceLateNeighborhood) {
repackEstimationAvailableSpaces = solution.containerObjs.flatMap { c -> c.spaces.map { Pair(c.ci, it) } }
}
}

override fun neighboringSolutions(solution: SpaceContainerSolution): List<MSSMove> {
val containers = solution.containerObjs

// For fast progress early on, emulate the greedy algorithm, producing as few candidate moves as possible
// later on (once we have a halfway decent solution), produce more higher-effort moves
return if(!forceLateNeighborhood)
earlyNeighborhood(containers)
else
lateNeighborhood(containers)
}

override fun earlyNeighborhood(containers: List<SpaceContainer>) : List<MSSMove> {
val cramMoves = mutableListOf<MSSMove>()

val target = containers.firstOrNull { it.hasAccessibleSpace }

target?.spaces?.forEachIndexed { si, space ->

val box = containers.subList(target.ci+1, containers.size).flatMap { c ->
c.boxes.mapIndexedNotNull { bi, b ->
val placed = optimalPlacement(b, space, target.size)
val condition = (placed == null) // || (allowedOverlap < 1.0 && c.boxes.any { it.relativeOverlap(placed) > allowedOverlap })
if(condition) null else Triple(c.ci, bi, placed!!)
}
}.maxByOrNull { space.relativeOverlap(it.third) }

if(box != null)
cramMoves.add(CramMove(box.first, box.second, target.ci, si))
}

return if(cramMoves.size == 0) {
forceLateNeighborhood = true
lateNeighborhood(containers)
}
else
listOf(PlaceboMove) + cramMoves
}

override fun lateNeighborhood(containers: List<SpaceContainer>) : List<MSSMove> {
val baseMoves = mutableListOf<MSSMove>()
var cramMoves = mutableListOf<MSSMove>()
var localMoves = mutableListOf<MSSMove>()

// Ensure that local search never stops before all overlaps are removed
if(containers.any { c -> c.boxes.any{ b1 -> c.boxes.any{ b2 -> b1 != b2 && b1.intersects(b2) } } })
baseMoves.add(PlaceboMove)

// In the worst case, we have to add overlapping boxes to a new container
val escapeMoves = containers.flatMap { c ->
c.boxes.mapIndexedNotNull { bi, box -> if(c.boxes.any { box != it && box.relativeOverlap(it) > allowedOverlap }) EscapeMove(c.ci, bi) else null }
}

val sourceCandidates = containers.filter { it.freeSpace.toDouble() / it.area > 0.1 }

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 % 5) {
container.spaces.indices.forEach { si ->
localMoves.add(LocalMove(ci, bi, si))
}
}

// Generate cross-container cram moves
/*(0 until ci).forEach { tci ->
containers[tci].spaces.forEachIndexed { si, space ->
if(box.area * allowedOverlap < space.area)
cramMoves.add(CramMove(ci, bi, tci, si))
}
}*/
// Generate cross-container repack moves
(0 until ci).forEach { tci ->
containers[tci].spaces.indices.forEach { si ->
cramMoves.add(RepackMove(ci, bi, tci, si))
}
}
}
}

//Logger.log("Move count: ${cramMoves.size} cram; ${localMoves.size} local; ${escapeMoves.size} escape;")

if(localMoves.size > moveBudget * 0.2) {
localMoves.shuffle()
localMoves = localMoves.subList(0, (moveBudget * 0.2).toInt())
}

if(cramMoves.size > moveBudget - localMoves.size) {
cramMoves.shuffle()
cramMoves = cramMoves.subList(0, moveBudget - localMoves.size)
}

moveIndex += 1
return baseMoves + escapeMoves + localMoves + cramMoves
}

override fun deltaScoreMove(solution: SpaceContainerSolution, currentScore: Double, move: MSSMove): Double {
return when(move) {
is PlaceboMove -> -0.00001
is LocalMove -> super.deltaScoreMove(solution, currentScore, move)
is RepackMove -> super.deltaScoreMove(solution, currentScore, move)
is EscapeMove -> {
val source = solution.containerObjs[move.sourceContainer]
val box = source.boxes[move.sourceBox]
val newContainerCost = (source.area - box.area).toDouble() / (1 + solution.containerObjs.size)
val overlapCost = - source.boxes.sumOf { b -> if(box.relativeOverlap(b) > allowedOverlap && box != b) box.intersection(b) else 0 } * overlapPenalty

newContainerCost + overlapCost
}
is CramMove -> {
val source = solution.containerObjs[move.sourceContainer]
val target = solution.containerObjs[move.targetContainer]
val box = source.boxes[move.sourceBox]
val space = target.spaces[move.targetSpace]

val placed = optimalPlacement(box, space, target.size)

// box doesn't fit target space, like, _at all_
if(placed == null || target.boxes.any { placed.relativeOverlap(it) > allowedOverlap })
0.0
else {
val emptiesSource = source.boxes.size == 1
val overlap = target.boxes.sumOf { it.intersection(placed) }
val overlapCost = - source.boxes.sumOf { b -> if(box.relativeOverlap(b) > allowedOverlap && box != b) box.intersection(b) else 0 }

val targetCost = - max(1.0, (box.area.toDouble() - overlap))
val sourceCost = if(emptiesSource) box.area - source.area.toDouble() else if(move.targetContainer < move.sourceContainer) 0.0 else box.area.toDouble()

overlapCost * overlapPenalty + sourceCost / (move.sourceContainer + 1) + targetCost / (move.targetContainer + 1)
}
}
else -> 0.0
}
}

override fun applyMove(solution: SpaceContainerSolution, move: MSSMove): SpaceContainerSolution {
return when(move) {
is PlaceboMove -> {
// Accelerate overlap cost increase
consecutivePlaceboMoves += 1
overlapPenalty += 0.3 * consecutivePlaceboMoves
allowedOverlap = max(0.0, allowedOverlap - 0.007 * consecutivePlaceboMoves)
//Logger.log("Allowed overlap now $allowedOverlap ($consecutivePlaceboMoves consecutive)")
solution
}
is RepackMove -> {
allowedOverlap = max(0.0, allowedOverlap - 0.001)
applyRepackMove(solution, move)
}
is LocalMove -> applyLocalMove(solution, move)
is CramMove -> applyCramMove(solution, move)
is EscapeMove -> applyEscapeMove(solution, move)
else -> throw Exception("Unknown or unsupported move $move")
}
}

private fun applyCramMove(solution: SpaceContainerSolution, move: CramMove): SpaceContainerSolution {
consecutivePlaceboMoves = 0
val source = solution.containerObjs[move.sourceContainer]
val target = solution.containerObjs[move.targetContainer]
val box = source.boxes[move.sourceBox]
val space = target.spaces[move.targetSpace]

val placed = optimalPlacement(box, space, target.size) ?: throw Exception("No valid placement for $box")

val newContainers = solution.containerObjs.toMutableList()

// Remove box from source
source.remove(box, overlapPossible = true)
consolidateSpaces(source)

// Place box and adjust spaces
target.boxes.add(placed)
target.spaces.filter { it.intersects(placed) }.forEach {
target.spaces.remove(it)
target.spaces.addAll(it.shatter(placed))
}

consolidateSpaces(target)

// Remove container if now empty
if(source.boxes.isEmpty()) {
newContainers.removeAt(move.sourceContainer)
newContainers.subList(move.sourceContainer, newContainers.size).forEach { container -> container.ci -= 1 }
}

return SpaceContainerSolution(solution.containerSize, newContainers)
}

private fun applyEscapeMove(solution: SpaceContainerSolution, move: EscapeMove): SpaceContainerSolution {
consecutivePlaceboMoves = 0
val source = solution.containerObjs[move.sourceContainer]
val box = source.boxes[move.sourceBox]

val newContainers = solution.containerObjs.toMutableList()
source.remove(box, overlapPossible = true)

// Try a shallow global repack first before creating a new container
if(!shallowGlobalRepack(newContainers, move.sourceContainer, listOf(box))) {
val target = SpaceContainer(solution.containerObjs.size, source.size)
target.add(box)
newContainers.add(target)
}

return SpaceContainerSolution(solution.containerSize, newContainers)
}

override fun scoreSolution(solution: SpaceContainerSolution): Double {
//renderDebugSpaces(solution.containerObjs)
return 0.0
}
}

object PlaceboMove : MSSMove {
override fun toString() = "PlaceboMove"
}
data class CramMove(val sourceContainer: Int, val sourceBox: Int, val targetContainer: Int, val targetSpace: Int) : MSSMove
data class EscapeMove(val sourceContainer: Int, val sourceBox: Int) : MSSMove
Loading

0 comments on commit de07c5b

Please sign in to comment.