Interstellar007 commited on
Commit
fb19269
·
verified ·
1 Parent(s): e408622

Upload enhanced_heuristics.py

Browse files
Files changed (1) hide show
  1. enhanced_heuristics.py +841 -0
enhanced_heuristics.py ADDED
@@ -0,0 +1,841 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Enhanced Heuristic Solvers for ARC-AGI
3
+ Covers many more common patterns than the basic heuristics.
4
+ These run instantly (no model) and serve as Track B of our pipeline.
5
+ """
6
+ import copy
7
+ import itertools
8
+ from typing import List, Dict, Optional, Tuple
9
+ from collections import Counter, defaultdict
10
+ import numpy as np
11
+
12
+
13
+ def grids_equal(g1, g2):
14
+ if g1 is None or g2 is None:
15
+ return False
16
+ if len(g1) != len(g2):
17
+ return False
18
+ for r1, r2 in zip(g1, g2):
19
+ if len(r1) != len(r2):
20
+ return False
21
+ if list(r1) != list(r2):
22
+ return False
23
+ return True
24
+
25
+
26
+ class EnhancedHeuristicSolver:
27
+ """Comprehensive heuristic solver covering ~10-15% of ARC tasks."""
28
+
29
+ def solve(self, task: Dict) -> Optional[List[List[int]]]:
30
+ """Try all heuristic solvers, return first match."""
31
+ solvers = [
32
+ self.try_identity,
33
+ self.try_color_map,
34
+ self.try_rotation,
35
+ self.try_flip,
36
+ self.try_transpose,
37
+ self.try_crop_nonzero,
38
+ self.try_crop_to_color,
39
+ self.try_scale,
40
+ self.try_tile,
41
+ self.try_gravity,
42
+ self.try_border,
43
+ self.try_fill_enclosed,
44
+ self.try_mirror_symmetric,
45
+ self.try_count_to_grid,
46
+ self.try_sort_rows,
47
+ self.try_sort_cols,
48
+ self.try_remove_color,
49
+ self.try_keep_largest_object,
50
+ self.try_overlay,
51
+ self.try_repeat_pattern,
52
+ self.try_extract_subgrid,
53
+ self.try_swap_colors,
54
+ self.try_mask_operation,
55
+ self.try_size_change_pattern,
56
+ ]
57
+
58
+ for solver in solvers:
59
+ try:
60
+ result = solver(task)
61
+ if result is not None:
62
+ # Validate result is reasonable
63
+ if len(result) > 0 and all(len(r) > 0 for r in result):
64
+ if all(all(isinstance(c, int) and 0 <= c <= 9 for c in row) for row in result):
65
+ return result
66
+ except Exception:
67
+ continue
68
+
69
+ return None
70
+
71
+ # --- Geometric Transforms ---
72
+
73
+ @staticmethod
74
+ def try_identity(task):
75
+ for p in task["train"]:
76
+ if p["input"] != p["output"]:
77
+ return None
78
+ return copy.deepcopy(task["test"][0]["input"])
79
+
80
+ @staticmethod
81
+ def try_rotation(task):
82
+ for k in [1, 2, 3]:
83
+ if all(np.rot90(np.array(p["input"]), k=-k).tolist() == p["output"] for p in task["train"]):
84
+ return np.rot90(np.array(task["test"][0]["input"]), k=-k).tolist()
85
+ return None
86
+
87
+ @staticmethod
88
+ def try_flip(task):
89
+ for fn in [np.fliplr, np.flipud]:
90
+ if all(fn(np.array(p["input"])).tolist() == p["output"] for p in task["train"]):
91
+ return fn(np.array(task["test"][0]["input"])).tolist()
92
+ return None
93
+
94
+ @staticmethod
95
+ def try_transpose(task):
96
+ if all(np.array(p["input"]).T.tolist() == p["output"] for p in task["train"]):
97
+ return np.array(task["test"][0]["input"]).T.tolist()
98
+ return None
99
+
100
+ @staticmethod
101
+ def try_mirror_symmetric(task):
102
+ """Check if output is input with added mirror symmetry."""
103
+ # Try completing horizontal/vertical symmetry
104
+ for axis in ['h', 'v']:
105
+ ok = True
106
+ for p in task["train"]:
107
+ arr = np.array(p["input"])
108
+ out = np.array(p["output"])
109
+ if arr.shape != out.shape:
110
+ ok = False
111
+ break
112
+ if axis == 'h':
113
+ mirrored = np.fliplr(arr)
114
+ else:
115
+ mirrored = np.flipud(arr)
116
+ # Check if output = combine input and its mirror (non-zero takes priority)
117
+ expected = arr.copy()
118
+ mask = arr == 0
119
+ expected[mask] = mirrored[mask]
120
+ if not np.array_equal(expected, out):
121
+ ok = False
122
+ break
123
+ if ok:
124
+ arr = np.array(task["test"][0]["input"])
125
+ if axis == 'h':
126
+ mirrored = np.fliplr(arr)
127
+ else:
128
+ mirrored = np.flipud(arr)
129
+ result = arr.copy()
130
+ mask = arr == 0
131
+ result[mask] = mirrored[mask]
132
+ return result.tolist()
133
+ return None
134
+
135
+ # --- Color Operations ---
136
+
137
+ @staticmethod
138
+ def try_color_map(task):
139
+ """Global color replacement: each color maps to another."""
140
+ inp0, out0 = task["train"][0]["input"], task["train"][0]["output"]
141
+ if len(inp0) != len(out0) or len(inp0[0]) != len(out0[0]):
142
+ return None
143
+ cmap = {}
144
+ for r in range(len(inp0)):
145
+ for c in range(len(inp0[0])):
146
+ k, v = inp0[r][c], out0[r][c]
147
+ if k in cmap and cmap[k] != v:
148
+ return None
149
+ cmap[k] = v
150
+ for p in task["train"][1:]:
151
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
152
+ return None
153
+ for r in range(len(p["input"])):
154
+ for c in range(len(p["input"][0])):
155
+ if cmap.get(p["input"][r][c]) != p["output"][r][c]:
156
+ return None
157
+ return [[cmap.get(c, c) for c in row] for row in task["test"][0]["input"]]
158
+
159
+ @staticmethod
160
+ def try_swap_colors(task):
161
+ """Swap two specific colors."""
162
+ for p in task["train"]:
163
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
164
+ return None
165
+
166
+ # Find changed cells
167
+ swaps = set()
168
+ for p in task["train"]:
169
+ for r in range(len(p["input"])):
170
+ for c in range(len(p["input"][0])):
171
+ if p["input"][r][c] != p["output"][r][c]:
172
+ swaps.add((p["input"][r][c], p["output"][r][c]))
173
+
174
+ if len(swaps) == 2:
175
+ s = list(swaps)
176
+ if s[0] == (s[1][1], s[1][0]):
177
+ a, b = s[0]
178
+ # Verify
179
+ ok = True
180
+ for p in task["train"]:
181
+ for r in range(len(p["input"])):
182
+ for c in range(len(p["input"][0])):
183
+ ic = p["input"][r][c]
184
+ oc = p["output"][r][c]
185
+ if ic == a:
186
+ if oc != b: ok = False
187
+ elif ic == b:
188
+ if oc != a: ok = False
189
+ else:
190
+ if oc != ic: ok = False
191
+ if not ok: break
192
+ if not ok: break
193
+ if not ok: break
194
+ if ok:
195
+ return [[b if c==a else (a if c==b else c) for c in row]
196
+ for row in task["test"][0]["input"]]
197
+ return None
198
+
199
+ @staticmethod
200
+ def try_remove_color(task):
201
+ """Remove a specific color (replace with background)."""
202
+ for bg in range(10):
203
+ for remove_color in range(10):
204
+ if remove_color == bg:
205
+ continue
206
+ ok = True
207
+ for p in task["train"]:
208
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
209
+ ok = False
210
+ break
211
+ for r in range(len(p["input"])):
212
+ for c in range(len(p["input"][0])):
213
+ ic = p["input"][r][c]
214
+ oc = p["output"][r][c]
215
+ if ic == remove_color:
216
+ if oc != bg: ok = False
217
+ else:
218
+ if oc != ic: ok = False
219
+ if not ok: break
220
+ if not ok: break
221
+ if not ok: break
222
+ if ok:
223
+ return [[bg if c==remove_color else c for c in row]
224
+ for row in task["test"][0]["input"]]
225
+ return None
226
+
227
+ # --- Cropping ---
228
+
229
+ @staticmethod
230
+ def try_crop_nonzero(task):
231
+ """Crop to bounding box of non-zero cells."""
232
+ for p in task["train"]:
233
+ arr = np.array(p["input"])
234
+ nz = np.argwhere(arr != 0)
235
+ if len(nz) == 0:
236
+ return None
237
+ r1, c1 = nz.min(0)
238
+ r2, c2 = nz.max(0)
239
+ if arr[r1:r2+1, c1:c2+1].tolist() != p["output"]:
240
+ return None
241
+ arr = np.array(task["test"][0]["input"])
242
+ nz = np.argwhere(arr != 0)
243
+ if len(nz) == 0:
244
+ return None
245
+ r1, c1 = nz.min(0)
246
+ r2, c2 = nz.max(0)
247
+ return arr[r1:r2+1, c1:c2+1].tolist()
248
+
249
+ @staticmethod
250
+ def try_crop_to_color(task):
251
+ """Crop to bounding box of specific non-background color."""
252
+ for bg_color in range(10):
253
+ ok = True
254
+ for p in task["train"]:
255
+ arr = np.array(p["input"])
256
+ nz = np.argwhere(arr != bg_color)
257
+ if len(nz) == 0:
258
+ ok = False
259
+ break
260
+ r1, c1 = nz.min(0)
261
+ r2, c2 = nz.max(0)
262
+ if arr[r1:r2+1, c1:c2+1].tolist() != p["output"]:
263
+ ok = False
264
+ break
265
+ if ok:
266
+ arr = np.array(task["test"][0]["input"])
267
+ nz = np.argwhere(arr != bg_color)
268
+ if len(nz) == 0:
269
+ return None
270
+ r1, c1 = nz.min(0)
271
+ r2, c2 = nz.max(0)
272
+ return arr[r1:r2+1, c1:c2+1].tolist()
273
+ return None
274
+
275
+ @staticmethod
276
+ def try_extract_subgrid(task):
277
+ """Extract a specific rectangular region."""
278
+ # Check if all outputs are same size
279
+ out_shapes = set()
280
+ for p in task["train"]:
281
+ out_shapes.add((len(p["output"]), len(p["output"][0])))
282
+ if len(out_shapes) != 1:
283
+ return None
284
+
285
+ oh, ow = out_shapes.pop()
286
+
287
+ # Try finding the subgrid at every possible position
288
+ # Check if there's a consistent position rule
289
+ for p in task["train"]:
290
+ inp = np.array(p["input"])
291
+ out = np.array(p["output"])
292
+ ih, iw = inp.shape
293
+
294
+ if oh > ih or ow > iw:
295
+ return None
296
+
297
+ found = False
298
+ for r in range(ih - oh + 1):
299
+ for c in range(iw - ow + 1):
300
+ if np.array_equal(inp[r:r+oh, c:c+ow], out):
301
+ found = True
302
+ break
303
+ if found:
304
+ break
305
+ if not found:
306
+ return None
307
+
308
+ return None # Too ambiguous without more logic
309
+
310
+ # --- Scaling ---
311
+
312
+ @staticmethod
313
+ def try_scale(task):
314
+ for factor in [2, 3, 4, 5]:
315
+ ok = True
316
+ for p in task["train"]:
317
+ inp, out = np.array(p["input"]), np.array(p["output"])
318
+ if out.shape[0] != inp.shape[0]*factor or out.shape[1] != inp.shape[1]*factor:
319
+ ok = False
320
+ break
321
+ expected = np.repeat(np.repeat(inp, factor, axis=0), factor, axis=1)
322
+ if not np.array_equal(expected, out):
323
+ ok = False
324
+ break
325
+ if ok:
326
+ inp = np.array(task["test"][0]["input"])
327
+ return np.repeat(np.repeat(inp, factor, axis=0), factor, axis=1).tolist()
328
+ return None
329
+
330
+ @staticmethod
331
+ def try_tile(task):
332
+ """Check if output is input tiled NxM times."""
333
+ for nr in range(1, 6):
334
+ for nc in range(1, 6):
335
+ if nr == 1 and nc == 1:
336
+ continue
337
+ ok = True
338
+ for p in task["train"]:
339
+ inp, out = np.array(p["input"]), np.array(p["output"])
340
+ ih, iw = inp.shape
341
+ if out.shape != (ih*nr, iw*nc):
342
+ ok = False
343
+ break
344
+ expected = np.tile(inp, (nr, nc))
345
+ if not np.array_equal(expected, out):
346
+ ok = False
347
+ break
348
+ if ok:
349
+ inp = np.array(task["test"][0]["input"])
350
+ return np.tile(inp, (nr, nc)).tolist()
351
+ return None
352
+
353
+ # --- Gravity / Movement ---
354
+
355
+ @staticmethod
356
+ def try_gravity(task):
357
+ """Check if colored cells fall down/up/left/right."""
358
+ for direction in ['down', 'up', 'left', 'right']:
359
+ ok = True
360
+ for p in task["train"]:
361
+ arr = np.array(p["input"])
362
+ out = np.array(p["output"])
363
+ if arr.shape != out.shape:
364
+ ok = False
365
+ break
366
+
367
+ # Get background color (most common)
368
+ bg = Counter(arr.flatten().tolist()).most_common(1)[0][0]
369
+
370
+ result = np.full_like(arr, bg)
371
+ h, w = arr.shape
372
+
373
+ if direction == 'down':
374
+ for c in range(w):
375
+ non_bg = [arr[r, c] for r in range(h) if arr[r, c] != bg]
376
+ for i, val in enumerate(non_bg):
377
+ result[h - len(non_bg) + i, c] = val
378
+ elif direction == 'up':
379
+ for c in range(w):
380
+ non_bg = [arr[r, c] for r in range(h) if arr[r, c] != bg]
381
+ for i, val in enumerate(non_bg):
382
+ result[i, c] = val
383
+ elif direction == 'right':
384
+ for r in range(h):
385
+ non_bg = [arr[r, c] for c in range(w) if arr[r, c] != bg]
386
+ for i, val in enumerate(non_bg):
387
+ result[r, w - len(non_bg) + i] = val
388
+ elif direction == 'left':
389
+ for r in range(h):
390
+ non_bg = [arr[r, c] for c in range(w) if arr[r, c] != bg]
391
+ for i, val in enumerate(non_bg):
392
+ result[r, i] = val
393
+
394
+ if not np.array_equal(result, out):
395
+ ok = False
396
+ break
397
+
398
+ if ok:
399
+ arr = np.array(task["test"][0]["input"])
400
+ bg = Counter(arr.flatten().tolist()).most_common(1)[0][0]
401
+ result = np.full_like(arr, bg)
402
+ h, w = arr.shape
403
+
404
+ if direction == 'down':
405
+ for c in range(w):
406
+ non_bg = [arr[r, c] for r in range(h) if arr[r, c] != bg]
407
+ for i, val in enumerate(non_bg):
408
+ result[h - len(non_bg) + i, c] = val
409
+ elif direction == 'up':
410
+ for c in range(w):
411
+ non_bg = [arr[r, c] for r in range(h) if arr[r, c] != bg]
412
+ for i, val in enumerate(non_bg):
413
+ result[i, c] = val
414
+ elif direction == 'right':
415
+ for r in range(h):
416
+ non_bg = [arr[r, c] for c in range(w) if arr[r, c] != bg]
417
+ for i, val in enumerate(non_bg):
418
+ result[r, w - len(non_bg) + i] = val
419
+ elif direction == 'left':
420
+ for r in range(h):
421
+ non_bg = [arr[r, c] for c in range(w) if arr[r, c] != bg]
422
+ for i, val in enumerate(non_bg):
423
+ result[r, i] = val
424
+
425
+ return result.tolist()
426
+ return None
427
+
428
+ # --- Border / Frame ---
429
+
430
+ @staticmethod
431
+ def try_border(task):
432
+ """Check if output adds a border around non-zero region."""
433
+ for p in task["train"]:
434
+ inp, out = np.array(p["input"]), np.array(p["output"])
435
+ if inp.shape != out.shape:
436
+ return None
437
+
438
+ # Check if the change is adding a border color around objects
439
+ # Try: output = input with border cells colored
440
+ for border_color in range(10):
441
+ ok = True
442
+ for p in task["train"]:
443
+ inp = np.array(p["input"])
444
+ out = np.array(p["output"])
445
+ h, w = inp.shape
446
+
447
+ expected = inp.copy()
448
+ bg = Counter(inp.flatten().tolist()).most_common(1)[0][0]
449
+
450
+ for r in range(h):
451
+ for c in range(w):
452
+ if inp[r, c] == bg:
453
+ # Check if adjacent to non-bg
454
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
455
+ nr, nc = r+dr, c+dc
456
+ if 0 <= nr < h and 0 <= nc < w and inp[nr, nc] != bg:
457
+ expected[r, c] = border_color
458
+ break
459
+
460
+ if not np.array_equal(expected, out):
461
+ ok = False
462
+ break
463
+
464
+ if ok:
465
+ inp = np.array(task["test"][0]["input"])
466
+ h, w = inp.shape
467
+ bg = Counter(inp.flatten().tolist()).most_common(1)[0][0]
468
+ result = inp.copy()
469
+ for r in range(h):
470
+ for c in range(w):
471
+ if inp[r, c] == bg:
472
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
473
+ nr, nc = r+dr, c+dc
474
+ if 0 <= nr < h and 0 <= nc < w and inp[nr, nc] != bg:
475
+ result[r, c] = border_color
476
+ break
477
+ return result.tolist()
478
+ return None
479
+
480
+ # --- Fill Operations ---
481
+
482
+ @staticmethod
483
+ def try_fill_enclosed(task):
484
+ """Fill enclosed regions with a specific color."""
485
+ for p in task["train"]:
486
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
487
+ return None
488
+
489
+ # Try flood fill from edges
490
+ for fill_color in range(10):
491
+ ok = True
492
+ for p in task["train"]:
493
+ inp = np.array(p["input"])
494
+ out = np.array(p["output"])
495
+ h, w = inp.shape
496
+
497
+ bg = Counter(inp.flatten().tolist()).most_common(1)[0][0]
498
+
499
+ # Find enclosed bg regions (not connected to border)
500
+ visited = np.zeros_like(inp, dtype=bool)
501
+
502
+ # BFS from border
503
+ from collections import deque
504
+ queue = deque()
505
+ for r in range(h):
506
+ for c in [0, w-1]:
507
+ if inp[r, c] == bg and not visited[r, c]:
508
+ queue.append((r, c))
509
+ visited[r, c] = True
510
+ for c in range(w):
511
+ for r in [0, h-1]:
512
+ if inp[r, c] == bg and not visited[r, c]:
513
+ queue.append((r, c))
514
+ visited[r, c] = True
515
+
516
+ while queue:
517
+ r, c = queue.popleft()
518
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
519
+ nr, nc = r+dr, c+dc
520
+ if 0 <= nr < h and 0 <= nc < w and not visited[nr, nc] and inp[nr, nc] == bg:
521
+ visited[nr, nc] = True
522
+ queue.append((nr, nc))
523
+
524
+ # Fill enclosed
525
+ expected = inp.copy()
526
+ for r in range(h):
527
+ for c in range(w):
528
+ if inp[r, c] == bg and not visited[r, c]:
529
+ expected[r, c] = fill_color
530
+
531
+ if not np.array_equal(expected, out):
532
+ ok = False
533
+ break
534
+
535
+ if ok:
536
+ from collections import deque
537
+ inp = np.array(task["test"][0]["input"])
538
+ h, w = inp.shape
539
+ bg = Counter(inp.flatten().tolist()).most_common(1)[0][0]
540
+ visited = np.zeros_like(inp, dtype=bool)
541
+ queue = deque()
542
+ for r in range(h):
543
+ for c in [0, w-1]:
544
+ if inp[r, c] == bg and not visited[r, c]:
545
+ queue.append((r, c))
546
+ visited[r, c] = True
547
+ for c in range(w):
548
+ for r in [0, h-1]:
549
+ if inp[r, c] == bg and not visited[r, c]:
550
+ queue.append((r, c))
551
+ visited[r, c] = True
552
+ while queue:
553
+ r, c = queue.popleft()
554
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
555
+ nr, nc = r+dr, c+dc
556
+ if 0 <= nr < h and 0 <= nc < w and not visited[nr, nc] and inp[nr, nc] == bg:
557
+ visited[nr, nc] = True
558
+ queue.append((nr, nc))
559
+ result = inp.copy()
560
+ for r in range(h):
561
+ for c in range(w):
562
+ if inp[r, c] == bg and not visited[r, c]:
563
+ result[r, c] = fill_color
564
+ return result.tolist()
565
+ return None
566
+
567
+ # --- Counting / Size ---
568
+
569
+ @staticmethod
570
+ def try_count_to_grid(task):
571
+ """Output is a small grid whose size encodes a count."""
572
+ # Check if output dimensions relate to counting something in input
573
+ return None # Complex, skip for now
574
+
575
+ # --- Sorting ---
576
+
577
+ @staticmethod
578
+ def try_sort_rows(task):
579
+ """Check if rows are sorted by some criterion."""
580
+ for p in task["train"]:
581
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
582
+ return None
583
+
584
+ # Try sorting rows by color values
585
+ for reverse in [False, True]:
586
+ ok = True
587
+ for p in task["train"]:
588
+ sorted_rows = sorted(p["input"], reverse=reverse)
589
+ if sorted_rows != p["output"]:
590
+ ok = False
591
+ break
592
+ if ok:
593
+ return sorted(task["test"][0]["input"], reverse=reverse)
594
+ return None
595
+
596
+ @staticmethod
597
+ def try_sort_cols(task):
598
+ """Check if columns are sorted."""
599
+ for p in task["train"]:
600
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
601
+ return None
602
+
603
+ for reverse in [False, True]:
604
+ ok = True
605
+ for p in task["train"]:
606
+ arr = np.array(p["input"])
607
+ sorted_arr = np.sort(arr, axis=0)
608
+ if reverse:
609
+ sorted_arr = sorted_arr[::-1]
610
+ if not np.array_equal(sorted_arr, np.array(p["output"])):
611
+ ok = False
612
+ break
613
+ if ok:
614
+ arr = np.array(task["test"][0]["input"])
615
+ sorted_arr = np.sort(arr, axis=0)
616
+ if reverse:
617
+ sorted_arr = sorted_arr[::-1]
618
+ return sorted_arr.tolist()
619
+ return None
620
+
621
+ # --- Object Operations ---
622
+
623
+ @staticmethod
624
+ def try_keep_largest_object(task):
625
+ """Keep only the largest connected component."""
626
+ from collections import deque
627
+
628
+ for p in task["train"]:
629
+ if len(p["input"]) != len(p["output"]) or len(p["input"][0]) != len(p["output"][0]):
630
+ return None
631
+
632
+ def get_objects(grid, bg=0):
633
+ arr = np.array(grid)
634
+ h, w = arr.shape
635
+ visited = np.zeros_like(arr, dtype=bool)
636
+ objects = []
637
+
638
+ for r in range(h):
639
+ for c in range(w):
640
+ if not visited[r, c] and arr[r, c] != bg:
641
+ cells = []
642
+ color = arr[r, c]
643
+ queue = deque([(r, c)])
644
+ visited[r, c] = True
645
+ while queue:
646
+ cr, cc = queue.popleft()
647
+ cells.append((cr, cc))
648
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
649
+ nr, nc = cr+dr, cc+dc
650
+ if 0 <= nr < h and 0 <= nc < w and not visited[nr, nc] and arr[nr, nc] != bg:
651
+ visited[nr, nc] = True
652
+ queue.append((nr, nc))
653
+ objects.append({"cells": cells, "color": color})
654
+
655
+ return objects
656
+
657
+ for bg in [0]:
658
+ ok = True
659
+ for p in task["train"]:
660
+ objects = get_objects(p["input"], bg)
661
+ if not objects:
662
+ ok = False
663
+ break
664
+ largest = max(objects, key=lambda o: len(o["cells"]))
665
+
666
+ expected = np.full_like(np.array(p["input"]), bg)
667
+ inp_arr = np.array(p["input"])
668
+ for r, c in largest["cells"]:
669
+ expected[r, c] = inp_arr[r, c]
670
+
671
+ if not np.array_equal(expected, np.array(p["output"])):
672
+ ok = False
673
+ break
674
+
675
+ if ok:
676
+ inp = task["test"][0]["input"]
677
+ objects = get_objects(inp, bg)
678
+ if not objects:
679
+ return None
680
+ largest = max(objects, key=lambda o: len(o["cells"]))
681
+ result = np.full_like(np.array(inp), bg)
682
+ inp_arr = np.array(inp)
683
+ for r, c in largest["cells"]:
684
+ result[r, c] = inp_arr[r, c]
685
+ return result.tolist()
686
+
687
+ return None
688
+
689
+ # --- Overlay / Combine ---
690
+
691
+ @staticmethod
692
+ def try_overlay(task):
693
+ """Check if output is OR/AND overlay of two halves of input."""
694
+ for split_type in ['horizontal', 'vertical']:
695
+ for op in ['or', 'and', 'xor']:
696
+ ok = True
697
+ for p in task["train"]:
698
+ inp = np.array(p["input"])
699
+ out = np.array(p["output"])
700
+ h, w = inp.shape
701
+
702
+ if split_type == 'horizontal' and h % 2 == 0:
703
+ top = inp[:h//2]
704
+ bot = inp[h//2:]
705
+ if out.shape != top.shape:
706
+ ok = False
707
+ break
708
+ elif split_type == 'vertical' and w % 2 == 0:
709
+ top = inp[:, :w//2]
710
+ bot = inp[:, w//2:]
711
+ if out.shape != top.shape:
712
+ ok = False
713
+ break
714
+ else:
715
+ ok = False
716
+ break
717
+
718
+ bg = 0
719
+ if op == 'or':
720
+ expected = np.where(top != bg, top, bot)
721
+ elif op == 'and':
722
+ expected = np.where((top != bg) & (bot != bg), top, bg)
723
+ elif op == 'xor':
724
+ expected = np.where((top != bg) ^ (bot != bg),
725
+ np.where(top != bg, top, bot), bg)
726
+
727
+ if not np.array_equal(expected, out):
728
+ ok = False
729
+ break
730
+
731
+ if ok:
732
+ inp = np.array(task["test"][0]["input"])
733
+ h, w = inp.shape
734
+
735
+ if split_type == 'horizontal':
736
+ top = inp[:h//2]
737
+ bot = inp[h//2:]
738
+ else:
739
+ top = inp[:, :w//2]
740
+ bot = inp[:, w//2:]
741
+
742
+ bg = 0
743
+ if op == 'or':
744
+ result = np.where(top != bg, top, bot)
745
+ elif op == 'and':
746
+ result = np.where((top != bg) & (bot != bg), top, bg)
747
+ elif op == 'xor':
748
+ result = np.where((top != bg) ^ (bot != bg),
749
+ np.where(top != bg, top, bot), bg)
750
+
751
+ return result.tolist()
752
+ return None
753
+
754
+ # --- Pattern Repetition ---
755
+
756
+ @staticmethod
757
+ def try_repeat_pattern(task):
758
+ """Check if output repeats a pattern found in input."""
759
+ return None # Complex pattern detection, skip
760
+
761
+ @staticmethod
762
+ def try_mask_operation(task):
763
+ """Check if one color acts as a mask for another."""
764
+ return None # Complex, skip
765
+
766
+ @staticmethod
767
+ def try_size_change_pattern(task):
768
+ """Detect systematic size change patterns."""
769
+ # Check if output is always a fixed size
770
+ out_sizes = set()
771
+ for p in task["train"]:
772
+ out_sizes.add((len(p["output"]), len(p["output"][0])))
773
+
774
+ if len(out_sizes) == 1:
775
+ oh, ow = out_sizes.pop()
776
+ # Check if it's always 1x1 (counting/classification)
777
+ if oh == 1 and ow == 1:
778
+ # Try: output color = number of unique non-bg colors
779
+ for bg in [0]:
780
+ ok = True
781
+ for p in task["train"]:
782
+ colors = set(c for row in p["input"] for c in row) - {bg}
783
+ if p["output"] != [[len(colors)]]:
784
+ ok = False
785
+ break
786
+ if ok:
787
+ bg = 0
788
+ colors = set(c for row in task["test"][0]["input"] for c in row) - {bg}
789
+ return [[len(colors)]]
790
+
791
+ return None
792
+
793
+
794
+ # ============================================================
795
+ # Test
796
+ # ============================================================
797
+
798
+ if __name__ == "__main__":
799
+ from datasets import load_dataset
800
+
801
+ print("Loading ARC-AGI-2...")
802
+ ds = load_dataset("arc-agi-community/arc-agi-2", split="train")
803
+ tasks = []
804
+ for row in ds:
805
+ tasks.append({"train": row["fewshots"], "test": row["question"]})
806
+
807
+ print("Loading ARC-AGI-1...")
808
+ ds_v1 = load_dataset("lordspline/arc-agi", split="training")
809
+ tasks_v1 = []
810
+ for row in ds_v1:
811
+ tasks_v1.append({"train": row["train"], "test": row["test"]})
812
+
813
+ solver = EnhancedHeuristicSolver()
814
+
815
+ # Eval ARC-AGI-2
816
+ correct_v2 = 0
817
+ total_v2 = 0
818
+ for task in tasks:
819
+ gt = task["test"][0].get("output")
820
+ if gt is None:
821
+ continue
822
+ total_v2 += 1
823
+ pred = solver.solve(task)
824
+ if pred is not None and grids_equal(pred, gt):
825
+ correct_v2 += 1
826
+
827
+ print(f"ARC-AGI-2: {correct_v2}/{total_v2} = {correct_v2/total_v2*100:.1f}%")
828
+
829
+ # Eval ARC-AGI-1
830
+ correct_v1 = 0
831
+ total_v1 = 0
832
+ for task in tasks_v1:
833
+ gt = task["test"][0].get("output")
834
+ if gt is None:
835
+ continue
836
+ total_v1 += 1
837
+ pred = solver.solve(task)
838
+ if pred is not None and grids_equal(pred, gt):
839
+ correct_v1 += 1
840
+
841
+ print(f"ARC-AGI-1: {correct_v1}/{total_v1} = {correct_v1/total_v1*100:.1f}%")