tilingPuzzles 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. tilingpuzzles/__init__.py +6 -0
  2. tilingpuzzles/benchmark/README.md +10 -0
  3. tilingpuzzles/benchmark/__init__.py +0 -0
  4. tilingpuzzles/benchmark/data/timingResulsts.csv +193 -0
  5. tilingpuzzles/benchmark/git_state.py +22 -0
  6. tilingpuzzles/benchmark/run_benchmark.py +105 -0
  7. tilingpuzzles/examples/README.md +3 -0
  8. tilingpuzzles/examples/__init__.py +0 -0
  9. tilingpuzzles/examples/rectangularPentomino.py +40 -0
  10. tilingpuzzles/examples/scaledStones.py +163 -0
  11. tilingpuzzles/examples/tests/__init__.py +0 -0
  12. tilingpuzzles/examples/tests/test_rectangularPentomino.py +24 -0
  13. tilingpuzzles/examples/tests/test_scaledStones.py +8 -0
  14. tilingpuzzles/games/__init__.py +2 -0
  15. tilingpuzzles/games/game.py +20 -0
  16. tilingpuzzles/games/generic.py +7 -0
  17. tilingpuzzles/games/komino.py +147 -0
  18. tilingpuzzles/games/realisations.py +78 -0
  19. tilingpuzzles/games/stone.py +536 -0
  20. tilingpuzzles/games/stone_core.py +48 -0
  21. tilingpuzzles/games/tests/__init__.py +0 -0
  22. tilingpuzzles/games/tests/test_game.py +7 -0
  23. tilingpuzzles/games/tests/test_komino.py +30 -0
  24. tilingpuzzles/games/tests/test_realisations.py +28 -0
  25. tilingpuzzles/games/tests/test_stone.py +172 -0
  26. tilingpuzzles/games/tests/test_tile.py +19 -0
  27. tilingpuzzles/games/tile.py +39 -0
  28. tilingpuzzles/logUtils/__init__.py +0 -0
  29. tilingpuzzles/logUtils/callGraph.py +47 -0
  30. tilingpuzzles/logger.py +39 -0
  31. tilingpuzzles/solvers/__init__.py +0 -0
  32. tilingpuzzles/solvers/hights.py +3 -0
  33. tilingpuzzles/solvers/kominoSolver.py +191 -0
  34. tilingpuzzles/solvers/tests/test_komino_solver.py +30 -0
  35. tilingpuzzles/visualize/__init__.py +0 -0
  36. tilingpuzzles/visualize/visualize.py +61 -0
  37. tilingpuzzles-0.2.0.dist-info/METADATA +44 -0
  38. tilingpuzzles-0.2.0.dist-info/RECORD +40 -0
  39. tilingpuzzles-0.2.0.dist-info/WHEEL +4 -0
  40. tilingpuzzles-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,536 @@
1
+
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ from tilingpuzzles.visualize.visualize import Visualize
6
+ from tilingpuzzles.logUtils.callGraph import GTracker
7
+ import queue
8
+
9
+ from . import stone,tile
10
+
11
+ from logging import info
12
+
13
+
14
+
15
+ class Stone_Symetries(list):
16
+
17
+ def __init__(self,symertries=None):
18
+ super().__init__()
19
+
20
+ if symertries is None:
21
+ symertries=[
22
+
23
+ Stone_Symetries.rotate,
24
+ Stone_Symetries.flip
25
+ ]
26
+ self+=symertries
27
+
28
+
29
+ def __call__(self,stone:Stone):
30
+ res=set()
31
+ checkStack=[stone.shift_positive()]
32
+ while checkStack:
33
+ front=checkStack.pop()
34
+ if front in res:
35
+ continue
36
+ res.add(front)
37
+ for sym in self:
38
+ new_st:Stone=sym(front)
39
+ checkStack.append(new_st.shift_positive())
40
+ return res
41
+
42
+ pass
43
+
44
+
45
+ def rotate(stone:Stone) -> Stone:
46
+ new_tiles=[ (ty,-tx) for (tx,ty) in stone]
47
+ return Stone(new_tiles)
48
+
49
+ def flip(stone:Stone) -> Stone:
50
+ new_tiles=[(tx,-ty) for (tx,ty) in stone]
51
+ return Stone(new_tiles)
52
+
53
+ pass
54
+
55
+ class Stone_config():
56
+ K_STONE_MAX_LAYER_SIZE=1000
57
+ # Maximum number of stones in a layer until a assertion error is trown
58
+ SYMS=Stone_Symetries()
59
+ MaxCacheStoneSize:int=0
60
+
61
+ class Stone(frozenset):
62
+ """
63
+ Frozen set of tiles
64
+ All sets of tiles are considered as a stone
65
+ """
66
+ # change to frozenset to make it hashable
67
+
68
+ _NormalizeCache={}
69
+
70
+ def __new__(cls,*tiles):
71
+ tiles=map(tile.Tile,*tiles)
72
+ return super(Stone,cls).__new__(cls,tiles)
73
+
74
+ def __init__(self,*tiles):
75
+ super().__init__()
76
+ pass
77
+
78
+
79
+ def normalize(self) -> Stone:
80
+ #TODO Optimize, use bounding first, avoid generating all symetries at all cost
81
+ # Use dictionary to cache
82
+
83
+ #WARNING slow
84
+ if len(self)<=Stone_config.MaxCacheStoneSize:
85
+ positive=self.shift_positive()
86
+ if positive in Stone._NormalizeCache:
87
+ return Stone._NormalizeCache[positive]
88
+ #BUG not done jet
89
+ syms=self.get_symetries()
90
+ syms=list(syms)
91
+ syms.sort(key=hash)
92
+ res=syms[0]
93
+ if len(self)<Stone_config.MaxCacheStoneSize:
94
+ Stone._NormalizeCache.update({sym:res for sym in syms})
95
+ assert syms
96
+ return res
97
+
98
+
99
+ #@GTracker.track_calls
100
+ def shift_positive(self):
101
+ # TODO use self.shift
102
+ sx=[t[0] for t in self]
103
+ sx=np.array(sx)
104
+ sy=[t[1] for t in self]
105
+ sy=np.array(sy)
106
+
107
+ sy-=min(sy)
108
+ sx-=min(sx)
109
+
110
+ return Stone(zip(sx,sy))
111
+
112
+ def shift(self,dx,dy) -> Stone :
113
+ # TODO ditch numpy
114
+ """
115
+ Creates a new stone that is shiftet dx and dy
116
+ """
117
+ sx=[t[0] for t in self]
118
+ sx=np.array(sx)
119
+ sy=[t[1] for t in self]
120
+ sy=np.array(sy)
121
+ sx+=dx
122
+ sy+=dy
123
+ return Stone(zip(sx,sy))
124
+
125
+ @property
126
+ def bounding_Box(self):
127
+ """
128
+ upper bounds not included
129
+ (xmin,ymin),(xmax,ymax)
130
+ """
131
+ assert self
132
+ for t in self:
133
+ min_vals=list(t)
134
+ max_vals=list(t)
135
+ break
136
+
137
+ for tile in self:
138
+ for i,cord in enumerate(tile):
139
+ min_vals[i]=min(min_vals[i],cord)
140
+ max_vals[i]=max(max_vals[i],cord)
141
+
142
+ return tuple(min_vals),tuple(max_vals)
143
+
144
+
145
+ def to_mask(self,n=None ,m=None):
146
+ sx=[t[0] for t in self]
147
+ sy=[t[1] for t in self]
148
+
149
+ if n==None:
150
+ n=max(sx)+1
151
+ if m== None:
152
+ m=max(sy)+1
153
+ mask=np.zeros((n,m))
154
+ mask[sx,sy]=1
155
+ return mask
156
+
157
+
158
+ def display(self):
159
+ Visualize.draw_stone(st=self)
160
+ # make it default for ipynb
161
+ def _ipython_display_(self):
162
+ self.display()
163
+
164
+
165
+ def splitConnected(self) -> tuple[Stone,Stone]:
166
+ # Flood fill
167
+ #TODO Stone -> isConected
168
+
169
+ toCheck:list[tile.Tile]=[self.get_any_tile()]
170
+ found:set[tile.Tile]=set()
171
+
172
+ while toCheck:
173
+ front=toCheck.pop()
174
+ if front in found:
175
+ continue
176
+ found.add(front)
177
+ toCheck+=list(front.get_neighbores() & self)
178
+
179
+ connected=Stone(found)
180
+ reminder=Stone(self - connected)
181
+
182
+ return connected, reminder
183
+
184
+ pass
185
+
186
+ def isConected(self) -> bool:
187
+ if not self:
188
+ return True
189
+ _,reminder=self.splitConnected()
190
+ return not reminder
191
+
192
+ def ConectedComponents(self) -> set[Stone]:
193
+ reminder=self
194
+ res=set()
195
+ while reminder:
196
+ split,reminder=reminder.splitConnected()
197
+ res.add(split)
198
+ return res
199
+
200
+
201
+
202
+ #@GTracker.track_calls
203
+ def outer_bound(self,allow_diag=False) -> Stone:
204
+ res=set()
205
+ for t in self:
206
+ res.update(stone.Stone.get_neighbores(t,allow_diag=allow_diag))
207
+
208
+ res -=self
209
+ return Stone(res)
210
+
211
+ #@GTracker.track_calls
212
+ def inner_bound(self) -> Stone:
213
+
214
+ res=set()
215
+
216
+ for t in self:
217
+ t:tile.Tile
218
+ if not t.get_neighbores() <= self:
219
+ res.add(t)
220
+
221
+ return Stone(res)
222
+
223
+
224
+ #@GTracker.track_calls
225
+ def get_k_stone_on_tile(self,t,k=5) -> set[Stone]:
226
+ res = self._k_stone_subtree(Stone({t}),Stone(tuple()),Stone(tuple()),k=k)
227
+ return set(res)
228
+
229
+
230
+
231
+ def _layer_wise_expansion(self,t,k=5):
232
+
233
+ # coplexity replaced by _k_stone_subtree
234
+ # Solution recursive Decision Tree
235
+ # - in Substone, not in Substone
236
+
237
+ res=set()
238
+ if not t in self:
239
+ return res
240
+
241
+ cur_layer={Stone((t,))}
242
+
243
+ for i in range(1,k):
244
+ next_layer=set()
245
+ for cur in cur_layer:
246
+ cur:Stone
247
+ for boundTile in cur.outer_bound():
248
+ if boundTile in self:
249
+ next_layer.add(Stone(cur | {boundTile} ))
250
+
251
+ assert len(next_layer)<=Stone_config.K_STONE_MAX_LAYER_SIZE,'COMPLEXITY !!! To many possible stones'
252
+ cur_layer=next_layer
253
+
254
+
255
+ return cur_layer
256
+
257
+ def _k_stone_subtree(self,inSubtree:Stone,notInSutree:Stone,bound:Stone,k=5):
258
+ if len(inSubtree)==k:
259
+ return [inSubtree]
260
+ if not bound:
261
+ bound=inSubtree.outer_bound()
262
+ bound&=self
263
+ bound-=notInSutree
264
+ bound=Stone(bound)
265
+ if not bound:
266
+ return []
267
+
268
+ expPoint=bound.get_any_tile()
269
+ res=[]
270
+ new_bound=Stone(bound-{expPoint})
271
+
272
+ # All Stones that contain the expansion Point
273
+ res+=self._k_stone_subtree(
274
+ Stone(inSubtree |{expPoint} ),
275
+ notInSutree,
276
+ new_bound,
277
+ k
278
+ )
279
+ # All Stones that dont contain the expansion Point
280
+ res += self._k_stone_subtree(
281
+ inSubtree,
282
+ Stone(notInSutree | {expPoint}),
283
+ new_bound,
284
+ k
285
+ )
286
+
287
+
288
+ return res
289
+
290
+
291
+
292
+
293
+
294
+ def get_symetries(self):
295
+ return Stone_config.SYMS(self)
296
+
297
+ def get_any_tile(self):
298
+ for tile in self:
299
+ return tile
300
+
301
+ def getMinTile(self):
302
+ assert self
303
+ minTile= self.get_any_tile()
304
+
305
+ for t in self:
306
+ if t<minTile:
307
+ minTile= t
308
+ return minTile
309
+
310
+ def from_string(s:str) -> Stone:
311
+ lines=s.splitlines()
312
+ res=set()
313
+
314
+ for i, l in enumerate(lines):
315
+ for j,c in enumerate(l):
316
+ if c.strip():
317
+ res.add((i,j))
318
+ return Stone(res)
319
+
320
+
321
+ def good_cut_point(self,epsi=0.1,perc=0.1,offset=1)-> tuple[int,int]:
322
+ #TODO
323
+ #if len(self)<15:
324
+ # return self.getMinTile()
325
+
326
+ outerBound=self.outer_bound(allow_diag=True)
327
+ delta=lambda a,b: int(abs(a-b)*perc/2+offset)
328
+ split=lambda a,b: (a+delta(a,b),b-delta(a,b))
329
+
330
+ ((xmin,ymin),(xmax,ymax))=outerBound.bounding_Box
331
+ x_self_bound,y_self_bound=self.bounding_Box
332
+
333
+ cases=(
334
+ (split(xmin,xmax),(ymin,ymax)),
335
+ ((xmin,xmax),split(ymin,ymax))
336
+ )
337
+ points_and_costs=[]
338
+ for c in cases:
339
+ (x1,x2),(y1,y2)=c
340
+ #info(f" bounds = {c =} ")
341
+ if not (x1<= x2 and y1 <= y2):
342
+ continue
343
+ substone=outerBound.clip_to_bounds(x1,x2,y1,y2)
344
+ #substone.display()
345
+ components =list(substone.ConectedComponents())
346
+ assert components
347
+ max_edge=[ (comp._max_edge_value() ,comp) for comp in components ]
348
+ min_edge=[ (comp._min_edge_value() ,comp) for comp in components ]
349
+
350
+ _,starts=max(max_edge)
351
+ _,ends=min(min_edge)
352
+ starts-=ends
353
+ ends -= starts
354
+ if not starts:
355
+ #starts=stone.Stone(starts)
356
+ info(f"{substone = }")
357
+ substone.display()
358
+ info(f"{self = }")
359
+ self.display()
360
+ assert starts
361
+ assert ends
362
+
363
+
364
+ #starts.display()
365
+ #ends.display()
366
+ pac=self._nearest_endpoint_from_inner_AStar(
367
+ starts=starts,
368
+ ends=ends,
369
+ epsi_cost=epsi
370
+ )
371
+ cost,t =pac
372
+ #happends somehow
373
+ assert t in self
374
+ points_and_costs.append(pac)
375
+ #info(f"{points_and_costs = }")
376
+ if not points_and_costs:
377
+ return self.getMinTile()
378
+ elif len(points_and_costs)==1:
379
+ cost,t= points_and_costs[0]
380
+ return t
381
+ else:
382
+ cost,t=min(*points_and_costs)
383
+ return t
384
+
385
+
386
+
387
+
388
+
389
+ def _nearest_endpoint_from_inner_AStar(
390
+ self,
391
+ starts:stone.Stone,
392
+ ends:stone.Stone,
393
+ epsi_cost=0.3,
394
+ phase_change_cost=0.75,
395
+ dist_center_of_mass_cost=1
396
+ ) -> tuple[float,tuple]:
397
+ """
398
+ returns
399
+ - end_tile
400
+ - optimal tile in ends
401
+ - cost
402
+ - cost for making a cut from starts to ends
403
+ epsi_cost: cost for moving outside the tile >0
404
+ phase_change_cost: cost for going from outside to inside or inside to outside
405
+ """
406
+ assert starts
407
+ assert ends
408
+
409
+ xCenter=sum([x for x,y in self])
410
+ yCenter=sum([y for x,y in self])
411
+ xCenter/=len(self)
412
+ yCenter/=len(self)
413
+ (xmin,ymin),(xmax,ymax)=self.bounding_Box
414
+ diameter=xmax-xmin+ymax-ymin
415
+
416
+
417
+
418
+
419
+ pq=queue.PriorityQueue()
420
+
421
+ for start in starts:
422
+ pq.put((0,0,start,start))
423
+ visited=set()
424
+ min_dist= lambda x,y: epsi_cost*min([ abs(x -x_) +abs(y-y_) for x_,y_ in ends ])
425
+ distance_com_cost=lambda x,y: (abs(x -xCenter)+abs(y-yCenter))*dist_center_of_mass_cost/diameter
426
+ min_steps=lambda min_dist,t: min(min_dist,int(abs(t[0]-xCenter))+int(abs(t[1]-yCenter)))
427
+ heuristic_distance_com_cost=lambda steps: steps*(steps+1)/2*dist_center_of_mass_cost/diameter
428
+ while(pq.not_empty):
429
+ front=pq.get()
430
+
431
+ _,front_cost,front_item,prev=front # _ = heuristic
432
+ if front_item in ends and tile.Tile(prev) in self:
433
+ #TODO remove Tile this is why this Tile/tuple has to be split
434
+ x,y=prev
435
+ return front_cost,(x,y)
436
+ if front_item in ends:
437
+ continue
438
+
439
+ if front_item in visited:
440
+ continue
441
+ #info(f"{front}")
442
+ visited.add(front_item)
443
+
444
+ neighbores=stone.Stone.get_neighbores(front_item)
445
+ for neigbore in neighbores:
446
+ if neigbore in visited:
447
+ continue
448
+ front_in_stone=front_item in self
449
+ next_in_stone=neigbore in self
450
+
451
+ #TODO add distance cost, reduces number of stones visited
452
+
453
+ next_cost=front_cost + distance_com_cost(*neigbore)
454
+ match (front_in_stone,next_in_stone):
455
+ case (True,True):
456
+ next_cost+=1
457
+ case (False,False):
458
+ next_cost+=epsi_cost
459
+ case (True,False):
460
+ next_cost+=phase_change_cost+epsi_cost
461
+ case (False,True):
462
+ next_cost+=1
463
+ case _:
464
+ assert False, "Unreachable"
465
+ dst=min_dist(*neigbore)
466
+ steps=min_steps(dst,neigbore)
467
+ next_heuristic=next_cost+dst+ heuristic_distance_com_cost(dst)
468
+ #Todo min cost for distance cost
469
+ assert next_heuristic>=0
470
+ pq.put((next_heuristic,next_cost,neigbore,front_item))
471
+
472
+ assert False ,"Unreachable"
473
+
474
+
475
+
476
+ def clip_to_bounds(self,xmin,xmax,ymin,ymax)-> Stone:
477
+ """
478
+ returns a stone cliped to bounds
479
+ """
480
+ res =set()
481
+ #TODO
482
+
483
+ for t in self:
484
+ t:tile.Tile
485
+ x,y=t
486
+ inBounds=xmin <= x and x <= xmax
487
+ inBounds &= ymin <= y and y <= ymax
488
+ if inBounds:
489
+ res.add(t)
490
+ return Stone(res)
491
+
492
+
493
+
494
+ def get_neighbores(tile:tuple,lowerB=None,upperB=None,allow_diag=False) -> stone.Stone:
495
+ res=[]
496
+ x,y=tile
497
+ if not allow_diag:
498
+ delta_neig=(
499
+ (1,0),(-1,0),(0,1),(0,-1)
500
+ )
501
+ else:
502
+ delta_neig=(
503
+ (1,0),(-1,0),(0,1),(0,-1),
504
+ (1,1),(-1,-1),(1,-1),(-1,1)
505
+ )
506
+
507
+ for dx,dy in delta_neig:
508
+ res.append((x+dx,y+dy))
509
+
510
+ return stone.Stone(res)
511
+
512
+ def _min_edge_value(self):
513
+ x,y =self.get_any_tile()
514
+ res=x+y
515
+
516
+ for x,y in self:
517
+ res=min(res,x+y)
518
+ return res
519
+
520
+ def _max_edge_value(self):
521
+ x,y =self.get_any_tile()
522
+ res=x+y
523
+
524
+ for x,y in self:
525
+ res=max(res,x+y)
526
+ return res
527
+
528
+
529
+
530
+
531
+
532
+
533
+
534
+
535
+
536
+
@@ -0,0 +1,48 @@
1
+
2
+
3
+ # New core data structure
4
+ # Wrapp Frozenset
5
+
6
+
7
+ class Core():
8
+
9
+ def __init__(self,tiles):
10
+
11
+ self.tiles=frozenset(tiles)
12
+ pass
13
+
14
+ def __hash__(self):
15
+ return hash(self.tiles)
16
+ pass
17
+
18
+ def __eq__(self, other):
19
+ return self.tiles ==other.tiles
20
+ pass
21
+
22
+ def __add__(self,other):
23
+ pass
24
+
25
+ def __or__(self, value):
26
+ pass
27
+
28
+ def __and__(self,other):
29
+ pass
30
+
31
+ def __le__(self,other):
32
+ pass
33
+
34
+ def __ge__(self,other):
35
+ pass
36
+
37
+ def __sub__(self,other):
38
+ pass
39
+
40
+ def __repr__(self):
41
+ return f"Stone({self.tiles})"
42
+ pass
43
+
44
+
45
+
46
+
47
+
48
+
File without changes
@@ -0,0 +1,7 @@
1
+ from tilingpuzzles.games.game import Game
2
+
3
+
4
+ def test_Game():
5
+ Game(((1,0),(0,1)))
6
+ pass
7
+
@@ -0,0 +1,30 @@
1
+
2
+ from tilingpuzzles.games.komino import Komino
3
+
4
+ import logging
5
+
6
+ def test_komino():
7
+ Komino(TilesToFill={(1,0),(0,1)})
8
+ Komino.generate(M=10)
9
+ pass
10
+
11
+
12
+
13
+ def test_generate():
14
+
15
+ for i in range(2,4):
16
+ for j in range(2,4):
17
+ game,used =Komino.generate(i,j)
18
+ assert len(game.T)==i*j
19
+
20
+
21
+ def test_mask():
22
+
23
+ komi,used=Komino.generate(5,5)
24
+
25
+ assert komi.to_mask().sum().sum() == len(komi.T), "tiles should be preserved"
26
+
27
+
28
+ def test_unique_stones():
29
+ k =Komino(())
30
+ assert len(k.unique_stones())==12
@@ -0,0 +1,28 @@
1
+
2
+ from tilingpuzzles.games.komino import Komino
3
+ from tilingpuzzles.games.realisations import Realisations
4
+ from tilingpuzzles.games.stone import Stone
5
+
6
+ from logging import info
7
+
8
+
9
+
10
+ def test_realisations():
11
+
12
+ M=5
13
+ k=5
14
+ komino,used=Komino.generate(M,k)
15
+
16
+ # Single tile stone
17
+ s=Stone(((1,1),))
18
+
19
+ r=Realisations(komino.T)
20
+ r.add_stone(s)
21
+
22
+ # should equal the number of tiles
23
+ info(f"{komino.T = }")
24
+ info(f"{r.indexToReal.values() = }")
25
+ assert len(set(r.indexToReal.values()))== len(komino.T)
26
+ assert len(r.indexToReal) == len(komino.T)
27
+
28
+ pass