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

Add floorplan visualizer #14

Merged
merged 10 commits into from
May 23, 2021
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ jobs:
# example
- name: Run example/example.py
run: python example/example.py
- name: Run example/example_large.py
run: python example/example_large.py

# benchmark
- name: Run example/benchmark.py
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,5 @@ cython_debug/
################################
# Project specific settings
################################

floorplan.png
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ Sequence-pair [1] is used to represent a rectangle placement (floorplan).

## Features

- TBA
- Solution quality and execution time are tunable, since the solver is SA-based.
- Not only integers but also real numbers can be set as a rectangle width and height.
- A rectangle can rotate while optimizing.
- The built-in visualizer visualizes a floorplan solution.

## Installation

Expand All @@ -15,7 +18,8 @@ pip install rectangle-packing-solver

## Example Usage

Sample code:
### Sample code:

```python
import rectangle_packing_solver as rps

Expand All @@ -28,20 +32,29 @@ problem = rps.Problem(rectangles=[
])
print("problem:", problem)

# Get a solver
solver = rps.Solver()

# Find a solution
solution = solver.solve(problem)
solution = rpm.Solver().solve(problem=problem)
print("solution:", solution)

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

Output:
### Output:

```plaintext
problem: Problem({'n': 4, 'rectangles': [{'id': 0, 'width': 4, 'height': 6, 'rotatable': False}, {'id': 1, 'width': 4, 'height': 4, 'rotatable': False}, {'id': 2, 'width': 2.1, 'height': 3.2, 'rotatable': False}, {'id': 3, 'width': 1, 'height': 5, 'rotatable': True}]})
solution: Solution({'sequence_pair': SequencePair(([0, 1, 3, 2], [3, 0, 2, 1])), 'floorplan': Floorplan({'positions': [{'id': 0, 'x': 0, 'y': 1}, {'id': 1, 'x': 4, 'y': 3.2}, {'id': 2, 'x': 5.0, 'y': 0.0}, {'id': 3, 'x': 0, 'y': 0}], 'bounding_box': (8, 7.2), 'area': 57.6})})
```

### Floorplan (example):

![floorplan_example](./figs/floorplan_example.png)

### Floorplan (larger example):

![floorplan_large](./figs/floorplan_large.png)

## References

[1] H. Murata, K. Fujiyoshi, S. Nakatake, and Y. Kajitani, "VLSI module placement based on rectangle-packing by the sequence-pair," *IEEE Trans. on Computer-Aided Design of Integrated Circuits and Systems*, vol. 15, no. 12, pp. 1518--1524, Dec 1996.
8 changes: 4 additions & 4 deletions example/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ def main():
)
print("problem:", problem)

# Get a solver
solver = rps.Solver()

# Find a solution
solution = solver.solve(problem)
solution = rps.Solver().solve(problem=problem)
print("solution:", solution)

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


if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions example/example_large.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright 2021 Kotaro Terada
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import rectangle_packing_solver as rps


def main():
# 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
solution = rps.Solver().solve(problem=problem, simanneal_minutes=1.0, simanneal_steps=500)
print("solution:", solution)

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


if __name__ == "__main__":
main()
Binary file added 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 added 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.
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ warn_unused_configs = True
# Dependencies that don't have types.
[mypy-simanneal]
ignore_missing_imports = True

[mypy-matplotlib.*]
ignore_missing_imports = True
6 changes: 4 additions & 2 deletions rectangle_packing_solver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
# Classes
from .problem import Problem
from .solution import Solution

from .sequence_pair import SequencePair
from .floorplan import Floorplan

# Solvers
from .solver import Solver

# Visualizers
from .visualizer import Visualizer

from .__version__ import __version__, __version_info__

__all__ = ["Problem", "Solution", "SequencePair", "Floorplan", "Solver", "__version__", "__version_info__"]
__all__ = ["Problem", "Solution", "SequencePair", "Floorplan", "Solver", "Visualizer", "__version__", "__version_info__"]
2 changes: 2 additions & 0 deletions rectangle_packing_solver/sequence_pair.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def decode(self, problem: Problem, rotations: Optional[List] = None) -> Floorpla
"id": i,
"x": dist_h[i] - width_wrot[i], # distance from left edge
"y": dist_v[i] - height_wrot[i], # distande from bottom edge
"width": width_wrot[i],
"height": height_wrot[i],
}
)

Expand Down
91 changes: 91 additions & 0 deletions rectangle_packing_solver/visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2021 Kotaro Terada
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Tuple

import matplotlib.patches as patches
from matplotlib import pylab as plt

from .solution import Solution


class Visualizer:
"""
A floorplan visualizer.
"""

def __init__(self) -> None:
# Default font size is 12
plt.rcParams["font.size"] = 14

def visualize(self, solution: Solution, path: str = "floorplan.png", title: str = "Floorplan") -> None:
if not isinstance(solution, Solution):
raise TypeError("Invalid argument: 'solution' must be an instance of Solution.")

positions = solution.floorplan.positions
bounding_box = solution.floorplan.bounding_box

# Figure settings
fig = plt.figure(figsize=(10, 10))
ax = plt.axes()
ax.set_aspect("equal")
plt.xlim([0, bounding_box[0]])
plt.ylim([0, bounding_box[1]])
plt.xlabel("X")
plt.ylabel("Y")
plt.title(title)

# Plot every rectangle
for i, rectangle in enumerate(positions):
color, fontcolor = self.get_color(i)
r = patches.Rectangle(
xy=(rectangle["x"], rectangle["y"]),
width=rectangle["width"],
height=rectangle["height"],
edgecolor="#000000",
facecolor=color,
alpha=1.0,
fill=True,
)
ax.add_patch(r)

# Add text label
centering_offset = 0.011
center_x = rectangle["x"] + rectangle["width"] / 2 - bounding_box[0] * centering_offset
center_y = rectangle["y"] + rectangle["height"] / 2 - bounding_box[1] * centering_offset
ax.text(x=center_x, y=center_y, s=rectangle["id"], fontsize=18, color=fontcolor)

# Output
if path is None:
plt.show()
else:
fig.savefig(path)

plt.close()

@classmethod
def get_color(cls, i: int = 0) -> Tuple[str, str]:
"""
Gets rectangle face color (and its font color) from matplotlib cmap.
"""
cmap = plt.get_cmap("tab10")
color = cmap(i % cmap.N)
brightness = max(color[0], color[1], color[2])

if 0.85 < brightness:
fontcolor = "#000000"
else:
fontcolor = "#ffffff"

return (color, fontcolor)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
license="Apache 2.0",
install_requires=[
"simanneal>=0.5.0,<1.0.0",
"matplotlib>=3.3.4,<4.0.0",
"graphlib-backport>=1.0.3,<2.0.0", # TODO: Drop this when we drop 3.8 support
],
extras_require={
Expand Down
16 changes: 16 additions & 0 deletions tests/test_sequence_pair.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,26 @@ def test_sequence_pair_decode_horizontally(example_problem, example_pair_horizon
assert floorplan.positions[0]["id"] == 0
assert math.isclose(floorplan.positions[0]["x"], 0.0)
assert math.isclose(floorplan.positions[0]["y"], 0.0)
assert math.isclose(floorplan.positions[0]["width"], 4.0)
assert math.isclose(floorplan.positions[0]["height"], 6.0)

assert floorplan.positions[1]["id"] == 1
assert math.isclose(floorplan.positions[1]["x"], 4.0)
assert math.isclose(floorplan.positions[1]["y"], 0.0)
assert math.isclose(floorplan.positions[1]["width"], 4.0)
assert math.isclose(floorplan.positions[1]["height"], 4.0)

assert floorplan.positions[2]["id"] == 2
assert math.isclose(floorplan.positions[2]["x"], 8.0)
assert math.isclose(floorplan.positions[2]["y"], 0.0)
assert math.isclose(floorplan.positions[2]["width"], 2.1)
assert math.isclose(floorplan.positions[2]["height"], 3.2)

assert floorplan.positions[3]["id"] == 3
assert math.isclose(floorplan.positions[3]["x"], 10.1)
assert math.isclose(floorplan.positions[3]["y"], 0.0)
assert math.isclose(floorplan.positions[3]["width"], 1.0)
assert math.isclose(floorplan.positions[3]["height"], 5.0)

# Bounding box
assert floorplan.bounding_box == (11.1, 6.0)
Expand All @@ -93,18 +101,26 @@ def test_sequence_pair_decode_vertically(example_problem, example_pair_verticall
assert floorplan.positions[0]["id"] == 0
assert math.isclose(floorplan.positions[0]["x"], 0.0)
assert math.isclose(floorplan.positions[0]["y"], 12.2)
assert math.isclose(floorplan.positions[0]["width"], 4.0)
assert math.isclose(floorplan.positions[0]["height"], 6.0)

assert floorplan.positions[1]["id"] == 1
assert math.isclose(floorplan.positions[1]["x"], 0.0)
assert math.isclose(floorplan.positions[1]["y"], 8.2)
assert math.isclose(floorplan.positions[1]["width"], 4.0)
assert math.isclose(floorplan.positions[1]["height"], 4.0)

assert floorplan.positions[2]["id"] == 2
assert math.isclose(floorplan.positions[2]["x"], 0.0)
assert math.isclose(floorplan.positions[2]["y"], 5.0)
assert math.isclose(floorplan.positions[2]["width"], 2.1)
assert math.isclose(floorplan.positions[2]["height"], 3.2)

assert floorplan.positions[3]["id"] == 3
assert math.isclose(floorplan.positions[3]["x"], 0.0)
assert math.isclose(floorplan.positions[3]["y"], 0.0)
assert math.isclose(floorplan.positions[3]["width"], 1.0)
assert math.isclose(floorplan.positions[3]["height"], 5.0)

# Bounding box
assert floorplan.bounding_box == (4.0, 18.2)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
def test_solver(example_problem): # noqa: F811
problem = rps.Problem(rectangles=example_problem)
solver = rps.Solver()
solution = solver.solve(problem)
solution = solver.solve(problem=problem)

assert isinstance(solution, rps.Solution)
assert isinstance(solution.sequence_pair, rps.SequencePair)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2021 Kotaro Terada
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import mimetypes

from example_data import example_problem # noqa: F401

import rectangle_packing_solver as rps


def test_visualizer(example_problem): # noqa: F811
problem = rps.Problem(rectangles=example_problem)
solver = rps.Solver()
solution = solver.solve(problem=problem, simanneal_minutes=0.01, simanneal_steps=10)

rps.Visualizer().visualize(solution=solution, path="./floorplan.png")

mimetype = mimetypes.guess_type("./floorplan.png")[0]
assert mimetype == "image/png"