-
Notifications
You must be signed in to change notification settings - Fork 183
/
pack.py
289 lines (244 loc) · 10.5 KB
/
pack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""pack a list of components into as few components as possible.
adapted from phidl.geometry.
"""
import warnings
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
from pydantic import validate_arguments
import gdsfactory as gf
from gdsfactory.component import Component
from gdsfactory.name import get_name_short
from gdsfactory.types import Anchor, ComponentSpec, Float2, Number
def _pack_single_bin(
rect_dict: Dict[int, Tuple[Number, Number]],
aspect_ratio: Tuple[Number, Number],
max_size: Tuple[float, float],
sort_by_area: bool,
density: float,
) -> Tuple[Dict[int, Tuple[Number, Number, Number, Number]], Dict[Any, Any]]:
"""Packs a dict of rectangles {id:(w,h)} and tries to.
Pack it into a bin as small as possible with aspect ratio `aspect_ratio`
Will iteratively grow the bin size until everything fits or the bin size
reaches `max_size`.
Args:
rect_dict: dict of rectangles {id: (w, h)} to pack.
aspect_ratio: x, y.
max_size: tuple of max X, Y size.
sort_by_area: sorts components by area.
density: of packing, closer to 1 packs tighter (more compute heavy).
Returns:
packed rectangles dict {id:(x,y,w,h)}.
dict of remaining unpacked rectangles.
"""
import rectpack
# Compute total area and use it for an initial estimate of the bin size
total_area = sum(r[0] * r[1] for r in rect_dict.values())
aspect_ratio = np.asarray(aspect_ratio) / np.linalg.norm(aspect_ratio) # Normalize
# Setup variables
box_size = np.asarray(aspect_ratio * np.sqrt(total_area), dtype=np.float64)
box_size = np.clip(box_size, None, max_size)
rp_sort = rectpack.SORT_AREA if sort_by_area else rectpack.SORT_NONE
# Repeatedly run the rectangle-packing algorithm with increasingly larger
# areas until everything fits or we've reached the maximum size
while True:
# Create the pack object
rect_packer = rectpack.newPacker(
mode=rectpack.PackingMode.Offline,
pack_algo=rectpack.MaxRectsBlsf,
sort_algo=rp_sort,
bin_algo=rectpack.PackingBin.BBF,
rotation=False,
)
# Add each rectangle to the pack, create a single bin, and pack
for rid, r in rect_dict.items():
rect_packer.add_rect(width=r[0], height=r[1], rid=rid)
rect_packer.add_bin(width=box_size[0], height=box_size[1])
rect_packer.pack()
# Adjust the box size for next time
box_size *= density # Increase area to try to fit
box_size = np.clip(box_size, None, max_size)
# Quit the loop if we've packed all the rectangles or reached the max size
if len(rect_packer.rect_list()) == len(rect_dict):
break
if all(box_size >= max_size):
break
# Separate packed from unpacked rectangles, make dicts of form {id:(x,y,w,h)}
packed_rect_dict = {r[-1]: r[:-1] for r in rect_packer[0].rect_list()}
unpacked_rect_dict = {
k: v for k, v in rect_dict.items() if k not in packed_rect_dict
}
return packed_rect_dict, unpacked_rect_dict
@validate_arguments
def pack(
component_list: List[ComponentSpec],
spacing: float = 10.0,
aspect_ratio: Float2 = (1.0, 1.0),
max_size: Tuple[Optional[float], Optional[float]] = (None, None),
sort_by_area: bool = True,
density: float = 1.1,
precision: float = 1e-2,
text: Optional[ComponentSpec] = None,
text_prefix: str = "",
text_offsets: Tuple[Float2, ...] = ((0, 0),),
text_anchors: Tuple[Anchor, ...] = ("cc",),
name_prefix: Optional[str] = None,
rotation: int = 0,
h_mirror: bool = False,
v_mirror: bool = False,
) -> List[Component]:
"""Pack a list of components into as few Components as possible.
Adapted from phidl.geometry
Args:
component_list: list or tuple.
spacing: Minimum distance between adjacent shapes.
aspect_ratio: (width, height) ratio of the rectangular bin.
max_size: Limits the size into which the shapes will be packed.
sort_by_area: Pre-sorts the shapes by area.
density: Values closer to 1 pack tighter but require more computation.
precision: Desired precision for rounding vertex coordinates.
text: Optional function to add text labels.
text_prefix: for labels. For example. 'A' will produce 'A1', 'A2', ...
text_offsets: relative to component size info anchor. Defaults to center.
text_anchors: relative to component (ce cw nc ne nw sc se sw center cc).
name_prefix: for each packed component (avoids the Unnamed cells warning).
Note that the suffix contains a uuid so the name will not be deterministic.
rotation: for each component in degrees.
h_mirror: horizontal mirror in y axis (x, 1) (1, 0). This is the most common.
v_mirror: vertical mirror using x axis (1, y) (0, y).
.. plot::
:include-source:
import gdsfactory as gf
components = [gf.components.triangle(x=i) for i in range(1, 10)]
c = gf.pack(
components,
spacing=20.0,
max_size=(100, 100),
text=gf.partial(gf.components.text, justify="center"),
text_prefix="R",
name_prefix="demo",
text_anchors=["nc"],
text_offsets=[(-10, 0)],
v_mirror=True,
)
c[0].plot()
"""
if density < 1.01:
raise ValueError(
"pack() `density` argument is too small. "
"The density argument must be >= 1.01"
)
# Sanitize max_size variable
max_size = [np.inf if v is None else v for v in max_size]
max_size = np.asarray(max_size, dtype=np.float64) # In case it's integers
max_size = max_size / precision
component_list = [gf.get_component(component) for component in component_list]
# Convert Components to rectangles
rect_dict = {}
for n, D in enumerate(component_list):
w, h = (D.size + spacing) / precision
w, h = int(w), int(h)
if (w > max_size[0]) or (h > max_size[1]):
raise ValueError(
f"pack() failed because Component {D.name!r} has x or y "
"dimension larger than `max_size` and cannot be packed.\n"
f"size = {w*precision, h*precision}, max_size = {max_size*precision}"
)
rect_dict[n] = (w, h)
packed_list = []
while rect_dict:
(packed_rect_dict, rect_dict) = _pack_single_bin(
rect_dict,
aspect_ratio=aspect_ratio,
max_size=max_size,
sort_by_area=sort_by_area,
density=density,
)
packed_list.append(packed_rect_dict)
components_packed_list = []
index = 0
for i, rect_dict in enumerate(packed_list):
name = get_name_short(f"{name_prefix or 'pack'}_{i}")
packed = Component(name, with_uuid=True)
packed.info["components"] = {}
for n, rect in rect_dict.items():
x, y, w, h = rect
xcenter = x + w / 2 + spacing / 2
ycenter = y + h / 2 + spacing / 2
component = component_list[n]
d = component.ref(rotation=rotation, h_mirror=h_mirror, v_mirror=v_mirror)
packed.add(d)
if hasattr(component, "settings"):
packed.info["components"][component.name] = dict(component.settings)
d.center = (xcenter * precision, ycenter * precision)
packed.add_ports(d.ports, prefix=f"{component.name}_{index}_")
index += 1
if text:
for text_offset, text_anchor in zip(text_offsets, text_anchors):
label = packed << text(f"{text_prefix}{index}")
label.move(
np.array(text_offset) + getattr(d.size_info, text_anchor)
)
components_packed_list.append(packed)
if len(components_packed_list) > 1:
groups = len(components_packed_list)
warnings.warn(f"unable to pack in one component, creating {groups} components")
return components_packed_list
def test_pack() -> Component:
"""Test packing function."""
component_list = [
gf.components.ellipse(radii=tuple(np.random.rand(2) * n + 2)) for n in range(2)
]
component_list += [
gf.components.rectangle(size=tuple(np.random.rand(2) * n + 2)) for n in range(2)
]
components_packed_list = pack(
component_list, # Must be a list or tuple of Components
spacing=1.25, # Minimum distance between adjacent shapes
aspect_ratio=(2, 1), # (width, height) ratio of the rectangular bin
max_size=(None, None), # Limits the size into which the shapes will be packed
density=1.05, # Values closer to 1 pack tighter but require more computation
sort_by_area=True, # Pre-sorts the shapes by area
)
c = components_packed_list[0] # Only one bin was created, so we plot that
assert len(c.get_dependencies()) == 4
return c
def test_pack_with_settings() -> Component:
"""Test packing function with custom settings."""
component_list = [
gf.components.rectangle(size=(i, i), port_type=None) for i in range(1, 10)
]
component_list += [
gf.components.rectangle(size=(i, i), port_type=None) for i in range(1, 10)
]
components_packed_list = pack(
component_list, # Must be a list or tuple of Components
spacing=1.25, # Minimum distance between adjacent shapes
aspect_ratio=(2, 1), # (width, height) ratio of the rectangular bin
# max_size=(None, None), # Limits the size into which the shapes will be packed
max_size=(20, 20), # Limits the size into which the shapes will be packed
density=1.05, # Values closer to 1 pack tighter but require more computation
sort_by_area=True, # Pre-sorts the shapes by area
precision=1e-3,
)
return components_packed_list[0]
if __name__ == "__main__":
# test_pack()
# c = test_pack_with_settings()
# c = test_pack()
# c.show(show_ports=True)
# c.pprint()
# c.write_gds_with_metadata("mask.gds")
p = pack(
[gf.components.straight(length=i) for i in [1, 1]],
spacing=20.0,
max_size=(100, 100),
text=gf.partial(gf.components.text, justify="center"),
text_prefix="R",
name_prefix="demo",
text_anchors=["nc"],
text_offsets=[(-10, 0)],
v_mirror=True,
)
c = p[0]
print(c.name)
c.show(show_ports=True)