22
33import numpy as np
44from guillotine .core .constants import (
5- NOT_COMPUTED ,
65 DECISION_EMPTY ,
76 DECISION_FILL ,
87 DECISION_CUT_X ,
1312
1413
1514class 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