Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sparse floorplan problem for example_large #28

Merged
merged 1 commit into from
Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions example/example_large.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import random

import rectangle_packing_solver as rps


def main():
random.seed(123456)

# Define a problem
problem = rps.Problem(rectangles=[(0.1 * i, 0.1 * i) for i in range(100, 200, 5)])
print("problem:", problem)

# Find a solution
print("Solving... It may take a few minutes...")
solution = rps.Solver().solve(problem=problem, simanneal_minutes=1.0, simanneal_steps=500)
print("solution:", solution)

# Visualization (to floorplan_large.png)
rps.Visualizer().visualize(solution=solution, path="./figs/floorplan_large.png")

# Find a solution (with limit)
print("Solving... It may take a few minutes...")
solution = rps.Solver().solve(problem=problem, simanneal_minutes=1.0, simanneal_steps=500, height_limit=50.0)
print("solution (with limit):", solution)
rps.Visualizer().visualize(solution=solution, path="./figs/floorplan_large_limit.png")
Expand Down
Binary file modified figs/floorplan_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified figs/floorplan_example_limit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified figs/floorplan_large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified figs/floorplan_large_limit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
234 changes: 211 additions & 23 deletions rectangle_packing_solver/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import random
import signal
import sys
from contextlib import redirect_stderr
from typing import List, Optional, Tuple

import simanneal
Expand Down Expand Up @@ -52,21 +50,100 @@ def solve(
if not isinstance(problem, Problem):
raise TypeError("Invalid argument: 'problem' must be an instance of Problem.")

# If width/height limits are not given...
if (width_limit is None) and (height_limit is None):
return self._solve_with_constraints_strategy(
problem, width_limit, height_limit, simanneal_minutes, simanneal_steps, constraints_strategy="soft"
)

# If width/height limits are given...
if width_limit is None:
width_limit = sys.float_info.max
if height_limit is None:
height_limit = sys.float_info.max
max_width = max([min(r["width"], r["height"]) if r["rotatable"] else r["width"] for r in problem.rectangles])
max_height = max([min(r["width"], r["height"]) if r["rotatable"] else r["height"] for r in problem.rectangles])
if width_limit < max_width:
raise ValueError(
f"'width_limit' must be greater than or equal to {max_width} "
+ "(= the largest width of the given problem)."
)
if height_limit < max_height:
raise ValueError(
f"'height_limit' must be greater than or equal to {max_height} "
+ "(= the largest height of the given problem)."
)

# If constraints of width and/or hight are given,
# we will use two kinds of annealer in a hybrid way.
# - 1) Soft constraints strategy:
# Find a solution with smallest area as possible, the width/height limits may not be met.
# - 2) Hard constraints strategy:
# Find a solution so that the width/height limits must be met. Sometimes no solutions will be found.
solution_soft = self._solve_with_constraints_strategy(
problem, width_limit, height_limit, simanneal_minutes, simanneal_steps, constraints_strategy="soft"
)
try:
solution_hard = self._solve_with_constraints_strategy(
problem, width_limit, height_limit, simanneal_minutes, simanneal_steps, constraints_strategy="hard"
)
except HardToFindSolutionException:
return solution_soft

is_constraints_met_soft = (solution_soft.floorplan.bounding_box[0] <= width_limit) and (
solution_soft.floorplan.bounding_box[1] <= height_limit
)
is_constraints_met_hard = (solution_hard.floorplan.bounding_box[0] <= width_limit) and (
solution_hard.floorplan.bounding_box[1] <= height_limit
)
if solution_soft.floorplan.area < solution_hard.floorplan.area:
solution_smaller = solution_soft
else:
solution_smaller = solution_hard
if is_constraints_met_soft and is_constraints_met_hard:
return solution_smaller
elif is_constraints_met_soft:
return solution_soft
elif is_constraints_met_hard:
return solution_hard
else:
return solution_smaller

def _solve_with_constraints_strategy(
self,
problem: Problem,
width_limit: Optional[float] = None,
height_limit: Optional[float] = None,
simanneal_minutes: float = 0.1,
simanneal_steps: int = 100,
constraints_strategy: str = None,
) -> Solution:
# Initial state (= G_{+} + G_{-} + rotations)
init_gp = list(range(problem.n))
init_gn = list(range(problem.n))
if width_limit and (width_limit < sys.float_info.max):
# Flat along with vertical line
init_gn = list(reversed(list(range(problem.n))))
else:
# Flat along with horizontal line
init_gn = list(range(problem.n))
init_rot = [0 for _ in range(problem.n)]
init_state = init_gp + init_gn + init_rot

# Get rid of output (stderr) from simanneal in this "with" block
rpp = RectanglePackingProblemAnnealer(
state=init_state, problem=problem, width_limit=width_limit, height_limit=height_limit
)
if constraints_strategy == "soft":
rpp = RectanglePackingProblemAnnealerSoft(
state=init_state, problem=problem, width_limit=width_limit, height_limit=height_limit
)
elif constraints_strategy == "hard":
rpp = RectanglePackingProblemAnnealerHard(
state=init_state, problem=problem, width_limit=width_limit, height_limit=height_limit
)
else:
raise ValueError("'constraints_strategy' must be either of ['soft', 'hard'].")

signal.signal(signal.SIGINT, exit_handler)
with redirect_stderr(open(os.devnull, "w")):
rpp.copy_strategy = "slice" # We use "slice" since the state is a list
rpp.set_schedule(rpp.auto(minutes=simanneal_minutes, steps=simanneal_steps))
final_state, _ = rpp.anneal()
rpp.copy_strategy = "slice" # We use "slice" since the state is a list
rpp.set_schedule(rpp.auto(minutes=simanneal_minutes, steps=simanneal_steps))
final_state, _ = rpp.anneal()

# Convert simanneal's final_state to a Solution object
gp, gn, rotations = rpp.retrieve_pairs(n=problem.n, state=final_state)
Expand All @@ -77,8 +154,26 @@ def solve(


class RectanglePackingProblemAnnealer(simanneal.Annealer):
"""
The base class of the Annealer for the rectangle packing problem.
"""

@classmethod
def retrieve_pairs(cls, n: int, state: List[int]) -> Tuple[List[int], List[int], List[int]]:
"""
Retrieve G_{+}, G_{-}, and rotations from a state.
"""
gp = state[0:n]
gn = state[n : 2 * n]
rotations = state[2 * n : 3 * n]
return (gp, gn, rotations)


class RectanglePackingProblemAnnealerSoft(RectanglePackingProblemAnnealer):
"""
Annealer for the rectangle packing problem.
This annealer is based on Soft constraints strategy. In other words, it can't be helped that it may find a
solution violating constraints.
"""

def __init__(
Expand All @@ -99,20 +194,27 @@ def __init__(
[max(r["width"], r["height"]) if r["rotatable"] else r["height"] for r in problem.rectangles]
)

self.width_limit = sys.float_info.max
self.width_limit: float = sys.float_info.max
if width_limit:
self.width_limit = width_limit
self.height_limit = sys.float_info.max
self.height_limit: float = sys.float_info.max
if height_limit:
self.height_limit = height_limit
self.state: List[int] = []
super(RectanglePackingProblemAnnealer, self).__init__(state)
self._step: int = 0
super(RectanglePackingProblemAnnealerSoft, self).__init__(state)

def update(self, step: int, T: int, E: float, acceptance: float, improvement: float) -> None:
"""
Override the default_update method.
Purpose: Introduce steps, and disable stderr output.
"""
self._step = step

def move(self) -> float:
"""
Move state (sequence-pair) and return the energy diff.
"""

initial_energy: float = self.energy()
initial_state: List[int] = self.state[:]

Expand All @@ -129,7 +231,7 @@ def move(self) -> float:
self.state[i + 2 * self.problem.n] = initial_state[i + 2 * self.problem.n] + 1

# A solution whose width/height limit is not satisfied has a larger energy.
# We adopt a valid solution as the annealing steps proceeds.
# We would like to adopt a valid solution as the annealing steps proceeds.
energy = self.energy()

return energy - initial_energy
Expand All @@ -154,12 +256,98 @@ def energy(self) -> float:

return float(floorplan.area)

@classmethod
def retrieve_pairs(cls, n: int, state: List[int]) -> Tuple[List[int], List[int], List[int]]:

class RectanglePackingProblemAnnealerHard(RectanglePackingProblemAnnealer):
"""
Annealer for the rectangle packing problem.
This annealer is based on Hard constraints strategy. In other words, it must find only a solution satisfying
constraints. If it is hard to find a solution satisfying hard constraints, the annealer raises
HardToFindSolutionException.
"""

def __init__(
self,
state: List[int],
problem: Problem,
width_limit: Optional[float] = None,
height_limit: Optional[float] = None,
) -> None:
self.seqpair = SequencePair()
self.problem = problem

self.width_limit: float = sys.float_info.max
if width_limit:
self.width_limit = width_limit
self.height_limit: float = sys.float_info.max
if height_limit:
self.height_limit = height_limit
self.state: List[int] = []
self._step: int = 0
super(RectanglePackingProblemAnnealerHard, self).__init__(state)

def update(self, step: int, T: int, E: float, acceptance: float, improvement: float) -> None:
"""
Retrieve G_{+}, G_{-}, and rotations from a state.
Override the default_update method.
Purpose: Introduce steps, disable stderr output.
"""
gp = state[0:n]
gn = state[n : 2 * n]
rotations = state[2 * n : 3 * n]
return (gp, gn, rotations)
self._step = step

def move(self) -> float:
"""
Move state (sequence-pair) and return the energy diff.
"""
initial_energy: float = self.energy()
initial_state: List[int] = self.state[:]

# Maximum the number of trial: 100
for _ in range(100):
# Choose two indices and swap them
i, j = random.sample(range(self.problem.n), k=2) # The first and second index
offset = random.randint(0, 1) * self.problem.n # Choose G_{+} (=0) or G_{-} (=1)

# Swap them (i != j always holds true)
self.state[i + offset], self.state[j + offset] = initial_state[j + offset], initial_state[i + offset]

# Random rotation
if self.problem.rectangles[i]["rotatable"]:
if random.randint(0, 1) == 1:
self.state[i + 2 * self.problem.n] = initial_state[i + 2 * self.problem.n] + 1

# We adopt solution if the solution width/height limit is satisfied
energy = self.energy()
if energy < sys.float_info.max:
break

# Restore the state
self.state = initial_state[:]

else:
raise HardToFindSolutionException

return energy - initial_energy

def energy(self) -> float:
"""
Calculates the area of bounding box.
"""

# Pick up sequence-pair and rotations from state
gp, gn, rotations = self.retrieve_pairs(n=self.problem.n, state=self.state)
seqpair = SequencePair(pair=(gp, gn))
floorplan = seqpair.decode(problem=self.problem, rotations=rotations)

# Returns float max, if width/height limit is not satisfied
if floorplan.bounding_box[0] > self.width_limit:
return sys.float_info.max
if floorplan.bounding_box[1] > self.height_limit:
return sys.float_info.max

return float(floorplan.area)


class HardToFindSolutionException(Exception):
"""
When it is hard to find a solution, this exception is raised.
"""

pass
6 changes: 6 additions & 0 deletions tests/example_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def example_problem():
return rectangles


@pytest.fixture
def example_large_problem():
rectangles = [(0.1 * i, 0.1 * i) for i in range(100, 200, 5)]
return rectangles


@pytest.fixture
def example_pair_horizontally():
# By this pair of sequences, all of the four rectangles are aligned in a strait line horizontally.
Expand Down
Loading