kofdai commited on
Commit
c16db1d
Β·
verified Β·
1 Parent(s): 3378f21

Upload arc/pattern_atoms.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. arc/pattern_atoms.py +332 -0
arc/pattern_atoms.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ arc/pattern_atoms.py β€” Transform pattern detection as Atoms
3
+
4
+ ARC-AGI tasks encode grid transformations. We detect these as "TransformAtoms"
5
+ and synthesize rules by composing them.
6
+
7
+ TransformAtom categories:
8
+ - geometric: rotate, flip, translate, scale, tile
9
+ - color: recolor, fill, swap, gradient
10
+ - structural: crop, extract, overlay, merge, split
11
+ - logical: mask, conditional, count-based
12
+ - topological: flood_fill, boundary, connectivity
13
+ """
14
+
15
+ from __future__ import annotations
16
+ from typing import List, Optional, Tuple, Dict
17
+ from dataclasses import dataclass
18
+ from arc.grid import (
19
+ Grid, grid_shape, grid_eq, grid_colors,
20
+ rotate_90, rotate_180, rotate_270,
21
+ flip_h, flip_v, transpose,
22
+ tile, tile_with_flip, tile_checkerboard,
23
+ recolor, most_common_color,
24
+ extract_subgrid, flood_fill_regions, analyze,
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class TransformAtom:
30
+ """An atomic transformation detected between input→output"""
31
+ category: str # geometric, color, structural, logical, topological
32
+ operation: str # specific operation name
33
+ params: dict # operation parameters
34
+ confidence: float # how well this explains the transform (0-1)
35
+ explanation: str # human-readable description
36
+
37
+
38
+ def detect_transforms(inp: Grid, out: Grid) -> List[TransformAtom]:
39
+ """Detect all possible transform atoms between input and output grids."""
40
+ atoms = []
41
+
42
+ atoms.extend(_detect_geometric(inp, out))
43
+ atoms.extend(_detect_tiling(inp, out))
44
+ atoms.extend(_detect_color_mapping(inp, out))
45
+ atoms.extend(_detect_scaling(inp, out))
46
+ atoms.extend(_detect_crop_or_extract(inp, out))
47
+ atoms.extend(_detect_overlay(inp, out))
48
+
49
+ # Sort by confidence
50
+ atoms.sort(key=lambda a: a.confidence, reverse=True)
51
+ return atoms
52
+
53
+
54
+ # ── Geometric transforms ──
55
+
56
+ def _detect_geometric(inp: Grid, out: Grid) -> List[TransformAtom]:
57
+ atoms = []
58
+ ih, iw = grid_shape(inp)
59
+ oh, ow = grid_shape(out)
60
+
61
+ # Identity
62
+ if grid_eq(inp, out):
63
+ atoms.append(TransformAtom('geometric', 'identity', {}, 1.0, 'Output is identical to input'))
64
+
65
+ # Same size transforms
66
+ if (ih, iw) == (oh, ow):
67
+ if grid_eq(flip_h(inp), out):
68
+ atoms.append(TransformAtom('geometric', 'flip_h', {}, 1.0, 'Horizontal flip'))
69
+ if grid_eq(flip_v(inp), out):
70
+ atoms.append(TransformAtom('geometric', 'flip_v', {}, 1.0, 'Vertical flip'))
71
+ if grid_eq(rotate_180(inp), out):
72
+ atoms.append(TransformAtom('geometric', 'rotate_180', {}, 1.0, '180Β° rotation'))
73
+
74
+ # Rotated (dimensions swap)
75
+ if (ih, iw) == (ow, oh):
76
+ if grid_eq(rotate_90(inp), out):
77
+ atoms.append(TransformAtom('geometric', 'rotate_90', {}, 1.0, '90Β° clockwise rotation'))
78
+ if grid_eq(rotate_270(inp), out):
79
+ atoms.append(TransformAtom('geometric', 'rotate_270', {}, 1.0, '270Β° clockwise rotation'))
80
+ if grid_eq(transpose(inp), out):
81
+ atoms.append(TransformAtom('geometric', 'transpose', {}, 1.0, 'Transpose'))
82
+
83
+ return atoms
84
+
85
+
86
+ # ── Tiling ──
87
+
88
+ def _detect_tiling(inp: Grid, out: Grid) -> List[TransformAtom]:
89
+ atoms = []
90
+ ih, iw = grid_shape(inp)
91
+ oh, ow = grid_shape(out)
92
+
93
+ if ih == 0 or iw == 0:
94
+ return atoms
95
+
96
+ # Check if output is exact tiling of input
97
+ if oh % ih == 0 and ow % iw == 0:
98
+ rh = oh // ih
99
+ rw = ow // iw
100
+ if rh >= 1 and rw >= 1 and (rh > 1 or rw > 1):
101
+ # Simple tile
102
+ if grid_eq(tile(inp, rh, rw), out):
103
+ atoms.append(TransformAtom(
104
+ 'geometric', 'tile', {'repeat_h': rh, 'repeat_w': rw},
105
+ 1.0, f'Tile {rh}Γ—{rw}'
106
+ ))
107
+ # Tile with alternating row flips
108
+ if grid_eq(tile_with_flip(inp, rh, rw), out):
109
+ atoms.append(TransformAtom(
110
+ 'geometric', 'tile_flip', {'repeat_h': rh, 'repeat_w': rw},
111
+ 1.0, f'Tile with row flip {rh}Γ—{rw}'
112
+ ))
113
+ # Tile with checkerboard flips
114
+ if grid_eq(tile_checkerboard(inp, rh, rw), out):
115
+ atoms.append(TransformAtom(
116
+ 'geometric', 'tile_checkerboard', {'repeat_h': rh, 'repeat_w': rw},
117
+ 1.0, f'Tile checkerboard {rh}Γ—{rw}'
118
+ ))
119
+
120
+ return atoms
121
+
122
+
123
+ # ── Color mapping ──
124
+
125
+ def _detect_color_mapping(inp: Grid, out: Grid) -> List[TransformAtom]:
126
+ atoms = []
127
+ ih, iw = grid_shape(inp)
128
+ oh, ow = grid_shape(out)
129
+
130
+ if (ih, iw) != (oh, ow):
131
+ return atoms
132
+
133
+ # Try to find a consistent color mapping
134
+ cmap = {}
135
+ consistent = True
136
+ for r in range(ih):
137
+ for c in range(iw):
138
+ ic = inp[r][c]
139
+ oc = out[r][c]
140
+ if ic in cmap:
141
+ if cmap[ic] != oc:
142
+ consistent = False
143
+ break
144
+ else:
145
+ cmap[ic] = oc
146
+ if not consistent:
147
+ break
148
+
149
+ if consistent and cmap:
150
+ # Check it's not identity
151
+ non_identity = any(k != v for k, v in cmap.items())
152
+ if non_identity:
153
+ atoms.append(TransformAtom(
154
+ 'color', 'recolor', {'map': cmap},
155
+ 1.0, f'Color mapping: {cmap}'
156
+ ))
157
+
158
+ # Check for color swap (A↔B)
159
+ swaps = [(k, v) for k, v in cmap.items() if cmap.get(v) == k and k != v]
160
+ if swaps:
161
+ atoms.append(TransformAtom(
162
+ 'color', 'color_swap', {'swaps': swaps},
163
+ 0.95, f'Color swap: {swaps}'
164
+ ))
165
+
166
+ return atoms
167
+
168
+
169
+ # ── Scaling ──
170
+
171
+ def _detect_scaling(inp: Grid, out: Grid) -> List[TransformAtom]:
172
+ atoms = []
173
+ ih, iw = grid_shape(inp)
174
+ oh, ow = grid_shape(out)
175
+
176
+ if ih == 0 or iw == 0:
177
+ return atoms
178
+
179
+ # Integer scaling
180
+ if oh % ih == 0 and ow % iw == 0:
181
+ sh = oh // ih
182
+ sw = ow // iw
183
+ if sh == sw and sh > 1:
184
+ # Check pixel scaling
185
+ scaled = True
186
+ for r in range(ih):
187
+ for c in range(iw):
188
+ for dr in range(sh):
189
+ for dc in range(sw):
190
+ if out[r * sh + dr][c * sw + dc] != inp[r][c]:
191
+ scaled = False
192
+ break
193
+ if not scaled:
194
+ break
195
+ if not scaled:
196
+ break
197
+ if not scaled:
198
+ break
199
+
200
+ if scaled:
201
+ atoms.append(TransformAtom(
202
+ 'geometric', 'scale', {'factor': sh},
203
+ 1.0, f'Scale {sh}x'
204
+ ))
205
+
206
+ return atoms
207
+
208
+
209
+ # ── Crop / Extract ──
210
+
211
+ def _detect_crop_or_extract(inp: Grid, out: Grid) -> List[TransformAtom]:
212
+ atoms = []
213
+ ih, iw = grid_shape(inp)
214
+ oh, ow = grid_shape(out)
215
+
216
+ # Output smaller β†’ could be crop/extract
217
+ if oh <= ih and ow <= iw and (oh < ih or ow < iw):
218
+ # Try all positions
219
+ for r in range(ih - oh + 1):
220
+ for c in range(iw - ow + 1):
221
+ sub = extract_subgrid(inp, r, c, r + oh - 1, c + ow - 1)
222
+ if grid_eq(sub, out):
223
+ atoms.append(TransformAtom(
224
+ 'structural', 'extract', {'r': r, 'c': c, 'h': oh, 'w': ow},
225
+ 0.9, f'Extract subgrid at ({r},{c}) size {oh}Γ—{ow}'
226
+ ))
227
+
228
+ # Extract by region (non-bg bounding box)
229
+ regions = flood_fill_regions(inp)
230
+ if regions:
231
+ for reg in regions[:5]:
232
+ r1, c1, r2, c2 = reg['bbox']
233
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
234
+ if grid_eq(sub, out):
235
+ atoms.append(TransformAtom(
236
+ 'structural', 'extract_region',
237
+ {'color': reg['color'], 'bbox': reg['bbox']},
238
+ 0.95, f'Extract region color={reg["color"]} bbox={reg["bbox"]}'
239
+ ))
240
+
241
+ return atoms
242
+
243
+
244
+ # ── Overlay / Merge ──
245
+
246
+ def _detect_overlay(inp: Grid, out: Grid) -> List[TransformAtom]:
247
+ atoms = []
248
+ ih, iw = grid_shape(inp)
249
+ oh, ow = grid_shape(out)
250
+
251
+ if (ih, iw) != (oh, ow):
252
+ return atoms
253
+
254
+ # Check which cells changed
255
+ changed = []
256
+ unchanged = []
257
+ for r in range(ih):
258
+ for c in range(iw):
259
+ if inp[r][c] != out[r][c]:
260
+ changed.append((r, c, inp[r][c], out[r][c]))
261
+ else:
262
+ unchanged.append((r, c))
263
+
264
+ if not changed:
265
+ return atoms
266
+
267
+ # Check if all changes are to the same color (fill)
268
+ new_colors = set(nc for _, _, _, nc in changed)
269
+ if len(new_colors) == 1:
270
+ fill_color = new_colors.pop()
271
+ old_colors = set(oc for _, _, oc, _ in changed)
272
+ atoms.append(TransformAtom(
273
+ 'color', 'fill_cells',
274
+ {'count': len(changed), 'new_color': fill_color, 'old_colors': list(old_colors)},
275
+ 0.7, f'Fill {len(changed)} cells β†’ color {fill_color}'
276
+ ))
277
+
278
+ # Check if changes only affect background cells
279
+ bg = most_common_color(inp)
280
+ if all(oc == bg for _, _, oc, _ in changed):
281
+ atoms.append(TransformAtom(
282
+ 'structural', 'fill_background',
283
+ {'bg_color': bg, 'new_colors': list(new_colors)},
284
+ 0.75, f'Fill background (color {bg}) with new patterns'
285
+ ))
286
+
287
+ return atoms
288
+
289
+
290
+ # ── Cross-matching transforms across train examples ──
291
+
292
+ def find_common_transforms(train_pairs: List[Tuple[Grid, Grid]]) -> List[TransformAtom]:
293
+ """Find transforms that appear in ALL training pairs"""
294
+ if not train_pairs:
295
+ return []
296
+
297
+ # Get transforms for each pair
298
+ per_pair = [detect_transforms(inp, out) for inp, out in train_pairs]
299
+
300
+ if not per_pair:
301
+ return []
302
+
303
+ # Find operations that appear in all pairs
304
+ # Match by (category, operation)
305
+ first_ops = {(a.category, a.operation) for a in per_pair[0]}
306
+
307
+ common_ops = first_ops
308
+ for pair_atoms in per_pair[1:]:
309
+ pair_ops = {(a.category, a.operation) for a in pair_atoms}
310
+ common_ops = common_ops & pair_ops
311
+
312
+ # Collect common atoms with averaged confidence
313
+ result = []
314
+ for cat, op in common_ops:
315
+ atoms_per_pair = []
316
+ for pair_atoms in per_pair:
317
+ matching = [a for a in pair_atoms if a.category == cat and a.operation == op]
318
+ if matching:
319
+ atoms_per_pair.append(matching[0])
320
+
321
+ if len(atoms_per_pair) == len(train_pairs):
322
+ # Check if params are consistent
323
+ avg_conf = sum(a.confidence for a in atoms_per_pair) / len(atoms_per_pair)
324
+ result.append(TransformAtom(
325
+ cat, op,
326
+ atoms_per_pair[0].params, # use first pair's params
327
+ avg_conf,
328
+ f'{atoms_per_pair[0].explanation} (consistent across {len(train_pairs)} examples)'
329
+ ))
330
+
331
+ result.sort(key=lambda a: a.confidence, reverse=True)
332
+ return result