Skip to content

Commit 1ad4391

Browse files
committed
slight speedup
1 parent fedcc49 commit 1ad4391

1 file changed

Lines changed: 130 additions & 125 deletions

File tree

src/guillotine/core/dp_solver.py

Lines changed: 130 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import numpy as np
44
from guillotine.core.constants import (
5-
NOT_COMPUTED,
65
DECISION_EMPTY,
76
DECISION_FILL,
87
DECISION_CUT_X,
@@ -13,109 +12,75 @@
1312

1413

1514
class GuillotineDP:
16-
"""Optimized DP solver with bottom-up pure computation and sparse defect cache."""
15+
"""Optimized DP solver v7."""
1716

1817
def __init__(self, item_sizes, geometry, patterns):
19-
"""Initialize solver and precompute pure rectangle values."""
2018
self.geom = geometry
2119
self.patterns = patterns
2220
self.W0 = geometry.W0
2321
self.H0 = geometry.H0
2422

25-
# Convert items to numpy for fast g computation
2623
self.item_w = np.array(item_sizes[0], dtype=np.int32)
2724
self.item_h = np.array(item_sizes[1], dtype=np.int32)
2825
self.item_area = self.item_w * self.item_h
2926
self.n_items = len(self.item_w)
3027

31-
# Precompute g(w,h) for all dimensions
32-
# g_values[w,h] = best area achievable by tiling with single item type
33-
# g_indices[w,h] = which item achieves that (for reconstruction)
34-
self._precompute_g()
28+
# Pure rectangles: numpy arrays for O(1) access (no hashing)
29+
self.F_values = np.zeros((self.W0 + 1, self.H0 + 1), dtype=np.int32)
30+
self.F_type = np.zeros((self.W0 + 1, self.H0 + 1), dtype=np.int8)
31+
self.F_param = np.zeros((self.W0 + 1, self.H0 + 1), dtype=np.int32)
3532

36-
# Precompute F(w,h) bottom-up for all pure rectangles
37-
# This eliminates ALL recursion for pure rectangles
38-
self._precompute_F()
33+
# Defected rectangles: dict (4D space too sparse for array)
34+
# Stores (value, decision_type, decision_param)
35+
self.cache = {}
3936

40-
# Sparse cache for defected rectangles: (x,y,w,h) -> (value, type, param)
41-
# Much more memory-efficient than dense 4D array
42-
self.cache_Fd = {}
37+
self._precompute_g()
38+
self._precompute_F()
4339

4440
def _precompute_g(self):
45-
"""Precompute best single-item-type tiling for all rectangle sizes.
46-
47-
For each (w,h), find which item type gives maximum coverage.
48-
49-
Time: O(W * H * n_items)
50-
Space: O(W * H)
51-
"""
41+
"""Precompute best tiling for each size."""
5242
W0, H0 = self.W0, self.H0
43+
item_w, item_h, item_area = self.item_w, self.item_h, self.item_area
5344
n_items = self.n_items
54-
item_w = self.item_w
55-
item_h = self.item_h
56-
item_area = self.item_area
5745

58-
# Arrays to store results
5946
g_values = np.zeros((W0 + 1, H0 + 1), dtype=np.int32)
6047
g_indices = np.full((W0 + 1, H0 + 1), -1, dtype=np.int32)
6148

62-
# For each rectangle size
6349
for w in range(1, W0 + 1):
6450
for h in range(1, H0 + 1):
6551
best_val = 0
6652
best_idx = -1
67-
68-
# Try each item type
6953
for i in range(n_items):
70-
# How many items fit in each direction?
7154
nx = w // item_w[i]
7255
ny = h // item_h[i]
73-
7456
if nx > 0 and ny > 0:
7557
val = item_area[i] * nx * ny
7658
if val > best_val:
7759
best_val = val
7860
best_idx = i
79-
8061
g_values[w, h] = best_val
8162
g_indices[w, h] = best_idx
8263

8364
self.g_values = g_values
8465
self.g_indices = g_indices
8566

8667
def _precompute_F(self):
87-
"""Precompute optimal values for all pure rectangles bottom-up.
88-
89-
F(w,h) = max of:
90-
- g(w,h): tile with single item type
91-
- max over z: F(z,h) + F(w-z,h) [vertical cut]
92-
- max over z: F(w,z) + F(w,h-z) [horizontal cut]
93-
94-
By processing in order of increasing w and h, all subproblems
95-
are solved before we need them.
96-
97-
Time: O(W * H * max_cuts)
98-
Space: O(W * H)
99-
"""
68+
"""Bottom-up DP for pure rectangles."""
10069
W0, H0 = self.W0, self.H0
10170
patterns = self.patterns
10271
g_values = self.g_values
10372
g_indices = self.g_indices
73+
F_values = self.F_values
74+
F_type = self.F_type
75+
F_param = self.F_param
10476

105-
# Arrays for F values and decisions
106-
F_values = np.zeros((W0 + 1, H0 + 1), dtype=np.int32)
107-
F_type = np.zeros((W0 + 1, H0 + 1), dtype=np.int8)
108-
F_param = np.zeros((W0 + 1, H0 + 1), dtype=np.int32)
109-
110-
# Process in order of increasing dimensions
11177
for w in range(1, W0 + 1):
11278
for h in range(1, H0 + 1):
113-
# Start with tiling option
114-
best_val = g_values[w, h]
79+
best_val = int(g_values[w, h])
11580
best_type = DECISION_FILL if g_indices[w, h] >= 0 else DECISION_EMPTY
116-
best_param = g_indices[w, h] if g_indices[w, h] >= 0 else 0
81+
best_param = int(g_indices[w, h]) if g_indices[w, h] >= 0 else 0
11782

118-
# Try vertical cuts (exploit symmetry: only z <= w/2)
83+
# Vertical cuts (use symmetry)
11984
half_w = w >> 1
12085
for z in patterns.cuts_pure_x(w):
12186
if z > half_w:
@@ -126,7 +91,7 @@ def _precompute_F(self):
12691
best_type = DECISION_CUT_X
12792
best_param = z
12893

129-
# Try horizontal cuts (exploit symmetry: only z <= h/2)
94+
# Horizontal cuts (use symmetry)
13095
half_h = h >> 1
13196
for z in patterns.cuts_pure_y(h):
13297
if z > half_h:
@@ -140,129 +105,169 @@ def _precompute_F(self):
140105
F_values[w, h] = best_val
141106
F_type[w, h] = best_type
142107
F_param[w, h] = best_param
143-
144-
self.F_values = F_values
145-
self.F_type = F_type
146-
self.F_param = F_param
147108

148109
def F(self, w, h):
149-
"""Get precomputed pure rectangle value. O(1)."""
110+
"""Get pure rectangle value."""
150111
if w <= 0 or h <= 0:
151112
return 0
152113
return int(self.F_values[w, h])
153114

154115
def F_d(self, x, y, w, h):
155-
"""Compute optimal value for potentially defected rectangle.
156-
157-
Uses recursion only for defected regions (sparse).
158-
Pure regions use precomputed F values directly.
159-
"""
116+
"""DP for defected rectangles with inlined child lookups."""
160117
if w <= 0 or h <= 0:
161118
return 0
162119

163-
# Check cache
164120
key = (x, y, w, h)
165-
cached = self.cache_Fd.get(key)
121+
cached = self.cache.get(key)
166122
if cached is not None:
167123
return cached[0]
168124

169-
# If pure, use precomputed value
125+
# Check purity
170126
if self.geom.is_pure(x, y, w, h):
171-
val = self.F_values[w, h]
172-
self.cache_Fd[key] = (int(val), DECISION_PURE, 0)
173-
return int(val)
127+
val = int(self.F_values[w, h])
128+
self.cache[key] = (val, DECISION_PURE, 0)
129+
return val
174130

175-
# Defected: must cut around defects
176131
best_val = 0
177132
best_type = DECISION_DEFECT
178133
best_param = 0
179134

180-
# Get cuts including defect boundaries
135+
# Local refs for speed
136+
cache = self.cache
137+
is_pure = self.geom.is_pure
138+
F_values = self.F_values
139+
181140
X_cuts, Y_cuts = self.patterns.cuts_defected(x, y, w, h)
182141

183-
# Try vertical cuts
142+
# Vertical cuts with inlined lookups
184143
for z in X_cuts:
185-
total = self.F_d(x, y, z, h) + self.F_d(x + z, y, w - z, h)
144+
# Left child
145+
lw, lh = z, h
146+
if lw > 0:
147+
lkey = (x, y, lw, lh)
148+
lc = cache.get(lkey)
149+
if lc is not None:
150+
lv = lc[0]
151+
elif is_pure(x, y, lw, lh):
152+
lv = int(F_values[lw, lh])
153+
cache[lkey] = (lv, DECISION_PURE, 0)
154+
else:
155+
lv = self.F_d(x, y, lw, lh)
156+
else:
157+
lv = 0
158+
159+
# Right child
160+
rx, rw, rh = x + z, w - z, h
161+
if rw > 0:
162+
rkey = (rx, y, rw, rh)
163+
rc = cache.get(rkey)
164+
if rc is not None:
165+
rv = rc[0]
166+
elif is_pure(rx, y, rw, rh):
167+
rv = int(F_values[rw, rh])
168+
cache[rkey] = (rv, DECISION_PURE, 0)
169+
else:
170+
rv = self.F_d(rx, y, rw, rh)
171+
else:
172+
rv = 0
173+
174+
total = lv + rv
186175
if total > best_val:
187176
best_val = total
188177
best_type = DECISION_CUT_X
189178
best_param = z
190179

191-
# Try horizontal cuts
180+
# Horizontal cuts with inlined lookups
192181
for z in Y_cuts:
193-
total = self.F_d(x, y, w, z) + self.F_d(x, y + z, w, h - z)
182+
# Bottom child
183+
bw, bh = w, z
184+
if bh > 0:
185+
bkey = (x, y, bw, bh)
186+
bc = cache.get(bkey)
187+
if bc is not None:
188+
bv = bc[0]
189+
elif is_pure(x, y, bw, bh):
190+
bv = int(F_values[bw, bh])
191+
cache[bkey] = (bv, DECISION_PURE, 0)
192+
else:
193+
bv = self.F_d(x, y, bw, bh)
194+
else:
195+
bv = 0
196+
197+
# Top child
198+
ty, tw, th = y + z, w, h - z
199+
if th > 0:
200+
tkey = (x, ty, tw, th)
201+
tc = cache.get(tkey)
202+
if tc is not None:
203+
tv = tc[0]
204+
elif is_pure(x, ty, tw, th):
205+
tv = int(F_values[tw, th])
206+
cache[tkey] = (tv, DECISION_PURE, 0)
207+
else:
208+
tv = self.F_d(x, ty, tw, th)
209+
else:
210+
tv = 0
211+
212+
total = bv + tv
194213
if total > best_val:
195214
best_val = total
196215
best_type = DECISION_CUT_Y
197216
best_param = z
198217

199-
self.cache_Fd[key] = (best_val, best_type, best_param)
218+
cache[key] = (best_val, best_type, best_param)
200219
return best_val
201220

202221
def solve(self):
203-
"""Solve the cutting problem and return (value, sequence)."""
204-
value = self.F_d(0, 0, self.W0, self.H0)
205-
sequence = self._reconstruct_Fd(0, 0, self.W0, self.H0)
206-
return value, sequence
222+
"""Solve and return (value, sequence)."""
223+
# Fast path: entirely pure sheet
224+
if self.geom.is_pure(0, 0, self.W0, self.H0):
225+
val = int(self.F_values[self.W0, self.H0])
226+
seq = self._reconstruct_F(self.W0, self.H0)
227+
return val, seq
228+
229+
val = self.F_d(0, 0, self.W0, self.H0)
230+
seq = self._reconstruct_Fd(0, 0, self.W0, self.H0)
231+
return int(val), seq
207232

208233
def _reconstruct_F(self, w, h):
209-
"""Reconstruct cutting sequence for pure rectangle."""
234+
"""Reconstruct sequence for pure rectangle."""
210235
if w <= 0 or h <= 0:
211236
return 'empty'
212237

213-
dec_type = self.F_type[w, h]
214-
dec_param = self.F_param[w, h]
238+
t = self.F_type[w, h]
239+
p = int(self.F_param[w, h])
215240

216-
if dec_type == DECISION_EMPTY:
241+
if t == DECISION_EMPTY:
217242
return 'empty'
218-
219-
if dec_type == DECISION_FILL:
220-
return f'g_{dec_param}'
221-
222-
if dec_type == DECISION_CUT_X:
223-
z = int(dec_param)
224-
return ('X', z, self._reconstruct_F(z, h), self._reconstruct_F(w - z, h))
225-
226-
if dec_type == DECISION_CUT_Y:
227-
z = int(dec_param)
228-
return ('Y', z, self._reconstruct_F(w, z), self._reconstruct_F(w, h - z))
229-
243+
if t == DECISION_FILL:
244+
return f'g_{p}'
245+
if t == DECISION_CUT_X:
246+
return ('X', p, self._reconstruct_F(p, h), self._reconstruct_F(w - p, h))
247+
if t == DECISION_CUT_Y:
248+
return ('Y', p, self._reconstruct_F(w, p), self._reconstruct_F(w, h - p))
230249
return 'empty'
231250

232251
def _reconstruct_Fd(self, x, y, w, h):
233-
"""Reconstruct cutting sequence for potentially defected rectangle."""
252+
"""Reconstruct sequence for defected rectangle."""
234253
if w <= 0 or h <= 0:
235254
return 'empty'
236255

237-
cached = self.cache_Fd.get((x, y, w, h))
238-
if cached is None:
239-
# Should not happen if solve() was called
256+
c = self.cache.get((x, y, w, h))
257+
if c is None:
240258
return 'empty'
241259

242-
_, dec_type, dec_param = cached
260+
_, t, p = c
261+
p = int(p)
243262

244-
if dec_type == DECISION_PURE:
263+
if t == DECISION_PURE:
245264
return self._reconstruct_F(w, h)
246-
247-
if dec_type == DECISION_EMPTY:
265+
if t == DECISION_EMPTY:
248266
return 'empty'
249-
250-
if dec_type == DECISION_DEFECT:
267+
if t == DECISION_DEFECT:
251268
return 'defect'
252-
253-
if dec_type == DECISION_FILL:
254-
return f'g_{dec_param}'
255-
256-
if dec_type == DECISION_CUT_X:
257-
z = int(dec_param)
258-
left = self._reconstruct_Fd(x, y, z, h)
259-
right = self._reconstruct_Fd(x + z, y, w - z, h)
260-
return ('X', z, left, right)
261-
262-
if dec_type == DECISION_CUT_Y:
263-
z = int(dec_param)
264-
bot = self._reconstruct_Fd(x, y, w, z)
265-
top = self._reconstruct_Fd(x, y + z, w, h - z)
266-
return ('Y', z, bot, top)
267-
268-
return 'empty'
269+
if t == DECISION_CUT_X:
270+
return ('X', p, self._reconstruct_Fd(x, y, p, h), self._reconstruct_Fd(x + p, y, w - p, h))
271+
if t == DECISION_CUT_Y:
272+
return ('Y', p, self._reconstruct_Fd(x, y, w, p), self._reconstruct_Fd(x, y + p, w, h - p))
273+
return 'empty'

0 commit comments

Comments
 (0)