kofdai commited on
Commit
7eddc0f
Β·
verified Β·
1 Parent(s): c16db1d

Upload arc/advanced_detectors.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. arc/advanced_detectors.py +567 -0
arc/advanced_detectors.py ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ arc/advanced_detectors.py β€” Advanced ARC-AGI-2 transform detectors
3
+
4
+ 1. Conditional fill β€” color/position-based cell changes
5
+ 2. Region-based transforms β€” per-region operations
6
+ 3. Pattern completion β€” symmetry-based fill
7
+ 4. Count-based β€” color count / region size based transforms
8
+ 5. Composite rules β€” multi-atom compositions
9
+
10
+ Each detector returns List[TransformAtom] and has a corresponding apply function.
11
+ All detectors verify against ALL training pairs (Verantyx: verify before answer).
12
+ """
13
+
14
+ from __future__ import annotations
15
+ from typing import List, Dict, Tuple, Optional, Set
16
+ from arc.grid import (
17
+ Grid, grid_shape, grid_eq, grid_colors,
18
+ most_common_color, flood_fill_regions, extract_subgrid, place_subgrid,
19
+ analyze, flip_h, flip_v, rotate_90, rotate_180, rotate_270, transpose,
20
+ check_symmetry, GridInfo,
21
+ )
22
+ from arc.pattern_atoms import TransformAtom
23
+ import copy
24
+
25
+
26
+ # ═══════════════════════════════════════════════════════════
27
+ # 1. CONDITIONAL FILL
28
+ # ═══════════════════════════════════════════════════════════
29
+
30
+ def detect_conditional_fill(inp: Grid, out: Grid) -> List[TransformAtom]:
31
+ """Detect rules like: if cell==X, change to Y; if neighbor==Z, change to W"""
32
+ atoms = []
33
+ ih, iw = grid_shape(inp)
34
+ oh, ow = grid_shape(out)
35
+
36
+ if (ih, iw) != (oh, ow):
37
+ return atoms
38
+
39
+ # Collect all cell changes
40
+ changes = []
41
+ for r in range(ih):
42
+ for c in range(iw):
43
+ if inp[r][c] != out[r][c]:
44
+ neighbors = _get_neighbors(inp, r, c)
45
+ changes.append({
46
+ 'r': r, 'c': c,
47
+ 'old': inp[r][c], 'new': out[r][c],
48
+ 'neighbors': neighbors,
49
+ })
50
+
51
+ if not changes:
52
+ return atoms
53
+
54
+ # --- Rule 1: Simple color replacement (old→new, conditional on neighbors) ---
55
+ # Group by (old_color, new_color)
56
+ by_transition = {}
57
+ for ch in changes:
58
+ key = (ch['old'], ch['new'])
59
+ by_transition.setdefault(key, []).append(ch)
60
+
61
+ for (old_c, new_c), group in by_transition.items():
62
+ if len(group) == len(changes):
63
+ # All changes are same transition
64
+ atoms.append(TransformAtom(
65
+ 'conditional', 'color_replace',
66
+ {'old': old_c, 'new': new_c},
67
+ 0.85, f'Replace color {old_c} β†’ {new_c}'
68
+ ))
69
+
70
+ # --- Rule 2: Fill based on neighbor presence ---
71
+ # Check if changed cells all have a specific neighbor color
72
+ for (old_c, new_c), group in by_transition.items():
73
+ for check_color in range(10):
74
+ if all(check_color in ch['neighbors'].values() for ch in group):
75
+ # Verify: unchanged cells with same old_color do NOT have this neighbor
76
+ unchanged_with_old = []
77
+ for r in range(ih):
78
+ for c in range(iw):
79
+ if inp[r][c] == old_c and out[r][c] == old_c:
80
+ nb = _get_neighbors(inp, r, c)
81
+ unchanged_with_old.append(check_color in nb.values())
82
+
83
+ if unchanged_with_old and not any(unchanged_with_old):
84
+ atoms.append(TransformAtom(
85
+ 'conditional', 'neighbor_fill',
86
+ {'old': old_c, 'new': new_c, 'neighbor_color': check_color},
87
+ 0.9, f'If cell={old_c} and has neighbor={check_color}, fill with {new_c}'
88
+ ))
89
+
90
+ # --- Rule 3: Fill background between colored regions ---
91
+ bg = most_common_color(inp)
92
+ if all(ch['old'] == bg for ch in changes):
93
+ atoms.append(TransformAtom(
94
+ 'conditional', 'fill_between',
95
+ {'bg': bg, 'fill_positions': [(ch['r'], ch['c'], ch['new']) for ch in changes]},
96
+ 0.6, f'Fill background cells with pattern'
97
+ ))
98
+
99
+ # --- Rule 4: Gravity / flood fill direction ---
100
+ bg = most_common_color(inp)
101
+ non_bg_colors = grid_colors(inp) - {bg}
102
+ for color in non_bg_colors:
103
+ # Check if color "falls down" (gravity)
104
+ in_positions = [(r, c) for r in range(ih) for c in range(iw) if inp[r][c] == color]
105
+ out_positions = [(r, c) for r in range(oh) for c in range(ow) if out[r][c] == color]
106
+
107
+ if in_positions and out_positions:
108
+ # Same columns, rows shifted down?
109
+ in_cols = set(c for _, c in in_positions)
110
+ out_cols = set(c for _, c in out_positions)
111
+ if in_cols == out_cols:
112
+ # Check gravity: for each column, are output positions at bottom?
113
+ gravity = True
114
+ for col in in_cols:
115
+ in_rows = sorted(r for r, c in in_positions if c == col)
116
+ out_rows = sorted(r for r, c in out_positions if c == col)
117
+ if len(in_rows) != len(out_rows):
118
+ gravity = False
119
+ break
120
+ # Output should be at bottom
121
+ expected = list(range(ih - len(out_rows), ih))
122
+ if out_rows != expected:
123
+ gravity = False
124
+ break
125
+
126
+ if gravity and len(in_positions) > 1:
127
+ atoms.append(TransformAtom(
128
+ 'conditional', 'gravity_down',
129
+ {'color': color, 'bg': bg},
130
+ 0.9, f'Gravity: color {color} falls to bottom'
131
+ ))
132
+
133
+ return atoms
134
+
135
+
136
+ def apply_conditional_fill(atom: TransformAtom, inp: Grid) -> Optional[Grid]:
137
+ """Apply a conditional fill transform"""
138
+ op = atom.operation
139
+ params = atom.params
140
+ h, w = grid_shape(inp)
141
+ result = copy.deepcopy(inp)
142
+
143
+ if op == 'color_replace':
144
+ old, new = params['old'], params['new']
145
+ for r in range(h):
146
+ for c in range(w):
147
+ if result[r][c] == old:
148
+ result[r][c] = new
149
+ return result
150
+
151
+ elif op == 'neighbor_fill':
152
+ old, new, nc = params['old'], params['new'], params['neighbor_color']
153
+ for r in range(h):
154
+ for c in range(w):
155
+ if inp[r][c] == old:
156
+ neighbors = _get_neighbors(inp, r, c)
157
+ if nc in neighbors.values():
158
+ result[r][c] = new
159
+ return result
160
+
161
+ elif op == 'gravity_down':
162
+ color, bg = params['color'], params['bg']
163
+ for c in range(w):
164
+ count = sum(1 for r in range(h) if inp[r][c] == color)
165
+ for r in range(h):
166
+ if inp[r][c] == color:
167
+ result[r][c] = bg
168
+ for i in range(count):
169
+ result[h - 1 - i][c] = color
170
+ return result
171
+
172
+ return None
173
+
174
+
175
+ # ═══════════════════════════════════════════════════════════
176
+ # 2. REGION-BASED TRANSFORMS
177
+ # ═══════════════════════════════════════════════════════════
178
+
179
+ def detect_region_transforms(inp: Grid, out: Grid) -> List[TransformAtom]:
180
+ """Detect per-region operations: move, copy, transform individual regions"""
181
+ atoms = []
182
+ ih, iw = grid_shape(inp)
183
+ oh, ow = grid_shape(out)
184
+
185
+ bg = most_common_color(inp)
186
+ in_regions = flood_fill_regions(inp)
187
+ out_regions = flood_fill_regions(out)
188
+
189
+ if not in_regions:
190
+ return atoms
191
+
192
+ # --- Rule 1: Largest region extraction ---
193
+ if in_regions and (oh < ih or ow < iw):
194
+ largest = in_regions[0]
195
+ r1, c1, r2, c2 = largest['bbox']
196
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
197
+ if grid_eq(sub, out):
198
+ atoms.append(TransformAtom(
199
+ 'region', 'extract_largest',
200
+ {'color': largest['color']},
201
+ 0.9, f'Extract largest region (color={largest["color"]})'
202
+ ))
203
+
204
+ # --- Rule 2: Smallest region extraction ---
205
+ if len(in_regions) >= 2 and (oh < ih or ow < iw):
206
+ smallest = in_regions[-1]
207
+ r1, c1, r2, c2 = smallest['bbox']
208
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
209
+ if grid_eq(sub, out):
210
+ atoms.append(TransformAtom(
211
+ 'region', 'extract_smallest',
212
+ {'color': smallest['color']},
213
+ 0.9, f'Extract smallest region (color={smallest["color"]})'
214
+ ))
215
+
216
+ # --- Rule 3: Count regions β†’ output is count-sized ---
217
+ n_regions = len(in_regions)
218
+ if oh == 1 and ow == 1:
219
+ if out[0][0] == n_regions:
220
+ atoms.append(TransformAtom(
221
+ 'region', 'count_regions',
222
+ {},
223
+ 0.95, f'Output = number of regions ({n_regions})'
224
+ ))
225
+
226
+ # --- Rule 4: Sort regions by size ---
227
+ if (ih, iw) == (oh, ow) and len(in_regions) >= 2:
228
+ # Check if regions are recolored based on size order
229
+ sorted_regs = sorted(in_regions, key=lambda r: r['size'])
230
+ pass # Complex β€” defer to composite rules
231
+
232
+ # --- Rule 5: Per-region geometric transform ---
233
+ if (ih, iw) == (oh, ow):
234
+ for reg in in_regions:
235
+ r1, c1, r2, c2 = reg['bbox']
236
+ in_sub = extract_subgrid(inp, r1, c1, r2, c2)
237
+ out_sub = extract_subgrid(out, r1, c1, r2, c2)
238
+
239
+ # Check transforms on this region
240
+ for name, fn in [('flip_h', flip_h), ('flip_v', flip_v),
241
+ ('rotate_90', rotate_90), ('rotate_180', rotate_180)]:
242
+ if grid_shape(in_sub) == grid_shape(out_sub) and grid_eq(fn(in_sub), out_sub):
243
+ atoms.append(TransformAtom(
244
+ 'region', f'per_region_{name}',
245
+ {'color': reg['color'], 'bbox': reg['bbox']},
246
+ 0.85, f'{name} on region color={reg["color"]}'
247
+ ))
248
+
249
+ return atoms
250
+
251
+
252
+ def apply_region_transform(atom: TransformAtom, inp: Grid) -> Optional[Grid]:
253
+ op = atom.operation
254
+ params = atom.params
255
+ h, w = grid_shape(inp)
256
+
257
+ if op == 'extract_largest':
258
+ regions = flood_fill_regions(inp)
259
+ if regions:
260
+ r1, c1, r2, c2 = regions[0]['bbox']
261
+ return extract_subgrid(inp, r1, c1, r2, c2)
262
+
263
+ elif op == 'extract_smallest':
264
+ regions = flood_fill_regions(inp)
265
+ if regions:
266
+ r1, c1, r2, c2 = regions[-1]['bbox']
267
+ return extract_subgrid(inp, r1, c1, r2, c2)
268
+
269
+ elif op == 'count_regions':
270
+ regions = flood_fill_regions(inp)
271
+ return [[len(regions)]]
272
+
273
+ elif op.startswith('per_region_'):
274
+ transform_name = op.replace('per_region_', '')
275
+ fn_map = {'flip_h': flip_h, 'flip_v': flip_v,
276
+ 'rotate_90': rotate_90, 'rotate_180': rotate_180}
277
+ fn = fn_map.get(transform_name)
278
+ if not fn:
279
+ return None
280
+
281
+ color = params['color']
282
+ result = copy.deepcopy(inp)
283
+ regions = flood_fill_regions(inp)
284
+ for reg in regions:
285
+ if reg['color'] == color:
286
+ r1, c1, r2, c2 = reg['bbox']
287
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
288
+ transformed = fn(sub)
289
+ if grid_shape(sub) == grid_shape(transformed):
290
+ result = place_subgrid(result, transformed, r1, c1)
291
+ return result
292
+
293
+ return None
294
+
295
+
296
+ # ═══════════════════════════════════════════════════════════
297
+ # 3. PATTERN COMPLETION (symmetry-based)
298
+ # ═══════════════════════════════════════════════════════════
299
+
300
+ def detect_pattern_completion(inp: Grid, out: Grid) -> List[TransformAtom]:
301
+ """Detect symmetry completion: fill holes to make grid symmetric"""
302
+ atoms = []
303
+ ih, iw = grid_shape(inp)
304
+ oh, ow = grid_shape(out)
305
+
306
+ if (ih, iw) != (oh, ow):
307
+ return atoms
308
+
309
+ out_sym = check_symmetry(out)
310
+ inp_sym = check_symmetry(inp)
311
+
312
+ bg = most_common_color(inp)
313
+
314
+ # Check if output has symmetry that input doesn't
315
+ for sym_name in ['h_sym', 'v_sym', 'rot180']:
316
+ if out_sym.get(sym_name) and not inp_sym.get(sym_name):
317
+ # Verify: the non-bg cells in input are preserved in output
318
+ preserved = all(
319
+ inp[r][c] == out[r][c]
320
+ for r in range(ih) for c in range(iw)
321
+ if inp[r][c] != bg
322
+ )
323
+ if preserved:
324
+ atoms.append(TransformAtom(
325
+ 'completion', f'complete_{sym_name}',
326
+ {'bg': bg, 'symmetry': sym_name},
327
+ 0.85, f'Complete to {sym_name} symmetry (bg={bg})'
328
+ ))
329
+
330
+ return atoms
331
+
332
+
333
+ def apply_pattern_completion(atom: TransformAtom, inp: Grid) -> Optional[Grid]:
334
+ op = atom.operation
335
+ params = atom.params
336
+ h, w = grid_shape(inp)
337
+ bg = params['bg']
338
+ result = copy.deepcopy(inp)
339
+
340
+ if op == 'complete_h_sym':
341
+ for r in range(h):
342
+ for c in range(w):
343
+ mirror_c = w - 1 - c
344
+ if result[r][c] == bg and result[r][mirror_c] != bg:
345
+ result[r][c] = result[r][mirror_c]
346
+ elif result[r][mirror_c] == bg and result[r][c] != bg:
347
+ result[r][mirror_c] = result[r][c]
348
+ return result
349
+
350
+ elif op == 'complete_v_sym':
351
+ for r in range(h):
352
+ mirror_r = h - 1 - r
353
+ for c in range(w):
354
+ if result[r][c] == bg and result[mirror_r][c] != bg:
355
+ result[r][c] = result[mirror_r][c]
356
+ elif result[mirror_r][c] == bg and result[r][c] != bg:
357
+ result[mirror_r][c] = result[r][c]
358
+ return result
359
+
360
+ elif op == 'complete_rot180':
361
+ for r in range(h):
362
+ for c in range(w):
363
+ mr, mc = h - 1 - r, w - 1 - c
364
+ if result[r][c] == bg and result[mr][mc] != bg:
365
+ result[r][c] = result[mr][mc]
366
+ elif result[mr][mc] == bg and result[r][c] != bg:
367
+ result[mr][mc] = result[r][c]
368
+ return result
369
+
370
+ return None
371
+
372
+
373
+ # ═══════════════════════════════════════════════════════════
374
+ # 4. COUNT-BASED TRANSFORMS
375
+ # ════════════════════���══════════════════════════════════════
376
+
377
+ def detect_count_based(inp: Grid, out: Grid) -> List[TransformAtom]:
378
+ """Detect transforms based on counting: color frequency, region size, etc."""
379
+ atoms = []
380
+ ih, iw = grid_shape(inp)
381
+ oh, ow = grid_shape(out)
382
+
383
+ in_info = analyze(inp)
384
+
385
+ # --- Rule 1: Output dimensions based on color count ---
386
+ for color, count in in_info.color_counts.items():
387
+ if color == in_info.bg_color:
388
+ continue
389
+ if oh == count and ow == 1:
390
+ atoms.append(TransformAtom(
391
+ 'count', 'color_count_to_height',
392
+ {'color': color},
393
+ 0.85, f'Output height = count of color {color} ({count})'
394
+ ))
395
+ if ow == count and oh == 1:
396
+ atoms.append(TransformAtom(
397
+ 'count', 'color_count_to_width',
398
+ {'color': color},
399
+ 0.85, f'Output width = count of color {color} ({count})'
400
+ ))
401
+
402
+ # --- Rule 2: Number of colors β†’ output dimension ---
403
+ n_colors = len(in_info.colors - {in_info.bg_color})
404
+ if oh == n_colors or ow == n_colors:
405
+ atoms.append(TransformAtom(
406
+ 'count', 'num_colors_to_dim',
407
+ {'n_colors': n_colors, 'maps_to': 'height' if oh == n_colors else 'width'},
408
+ 0.7, f'Output dim = number of non-bg colors ({n_colors})'
409
+ ))
410
+
411
+ # --- Rule 3: Region count β†’ single cell output ---
412
+ n_regions = len(in_info.regions)
413
+ if oh == 1 and ow == 1 and out[0][0] == n_regions:
414
+ atoms.append(TransformAtom(
415
+ 'count', 'region_count',
416
+ {},
417
+ 0.95, f'Output = region count ({n_regions})'
418
+ ))
419
+
420
+ # --- Rule 4: Most/least frequent non-bg color as output ---
421
+ non_bg = {k: v for k, v in in_info.color_counts.items() if k != in_info.bg_color}
422
+ if non_bg and oh == 1 and ow == 1:
423
+ most = max(non_bg, key=non_bg.get)
424
+ least = min(non_bg, key=non_bg.get)
425
+ if out[0][0] == most:
426
+ atoms.append(TransformAtom(
427
+ 'count', 'most_frequent_color',
428
+ {},
429
+ 0.8, f'Output = most frequent non-bg color ({most})'
430
+ ))
431
+ if out[0][0] == least:
432
+ atoms.append(TransformAtom(
433
+ 'count', 'least_frequent_color',
434
+ {},
435
+ 0.8, f'Output = least frequent non-bg color ({least})'
436
+ ))
437
+
438
+ # --- Rule 5: Majority vote per row/column ---
439
+ if (ih, iw) == (oh, ow):
440
+ pass # Complex β€” defer
441
+
442
+ return atoms
443
+
444
+
445
+ def apply_count_based(atom: TransformAtom, inp: Grid) -> Optional[Grid]:
446
+ op = atom.operation
447
+ in_info = analyze(inp)
448
+
449
+ if op == 'region_count':
450
+ return [[len(in_info.regions)]]
451
+
452
+ elif op == 'most_frequent_color':
453
+ non_bg = {k: v for k, v in in_info.color_counts.items() if k != in_info.bg_color}
454
+ if non_bg:
455
+ return [[max(non_bg, key=non_bg.get)]]
456
+
457
+ elif op == 'least_frequent_color':
458
+ non_bg = {k: v for k, v in in_info.color_counts.items() if k != in_info.bg_color}
459
+ if non_bg:
460
+ return [[min(non_bg, key=non_bg.get)]]
461
+
462
+ elif op == 'color_count_to_height':
463
+ color = atom.params['color']
464
+ count = sum(1 for row in inp for c in row if c == color)
465
+ return [[color]] * count
466
+
467
+ elif op == 'color_count_to_width':
468
+ color = atom.params['color']
469
+ count = sum(1 for row in inp for c in row if c == color)
470
+ return [[color] * count]
471
+
472
+ return None
473
+
474
+
475
+ # ═══════════════════════════════════════════════════════════
476
+ # 5. COMPOSITE RULES
477
+ # ═══════════════════════════════════════════════════════════
478
+
479
+ def detect_composite(inp: Grid, out: Grid) -> List[TransformAtom]:
480
+ """Detect multi-step transforms (applied last, lowest priority)"""
481
+ atoms = []
482
+ ih, iw = grid_shape(inp)
483
+ oh, ow = grid_shape(out)
484
+
485
+ # --- Composite 1: Extract + Transform ---
486
+ if oh < ih or ow < iw:
487
+ regions = flood_fill_regions(inp)
488
+ for reg in regions[:5]:
489
+ r1, c1, r2, c2 = reg['bbox']
490
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
491
+ sh, sw = grid_shape(sub)
492
+
493
+ if (sh, sw) == (oh, ow):
494
+ for name, fn in [('flip_h', flip_h), ('flip_v', flip_v),
495
+ ('rotate_180', rotate_180)]:
496
+ if grid_eq(fn(sub), out):
497
+ atoms.append(TransformAtom(
498
+ 'composite', f'extract_then_{name}',
499
+ {'color': reg['color'], 'transform': name},
500
+ 0.85, f'Extract region {reg["color"]} then {name}'
501
+ ))
502
+ elif (sw, sh) == (oh, ow):
503
+ for name, fn in [('rotate_90', rotate_90), ('rotate_270', rotate_270),
504
+ ('transpose', transpose)]:
505
+ if grid_eq(fn(sub), out):
506
+ atoms.append(TransformAtom(
507
+ 'composite', f'extract_then_{name}',
508
+ {'color': reg['color'], 'transform': name},
509
+ 0.85, f'Extract region {reg["color"]} then {name}'
510
+ ))
511
+
512
+ return atoms
513
+
514
+
515
+ def apply_composite(atom: TransformAtom, inp: Grid) -> Optional[Grid]:
516
+ op = atom.operation
517
+ params = atom.params
518
+
519
+ if op.startswith('extract_then_'):
520
+ transform_name = params['transform']
521
+ fn_map = {
522
+ 'flip_h': flip_h, 'flip_v': flip_v,
523
+ 'rotate_90': rotate_90, 'rotate_180': rotate_180,
524
+ 'rotate_270': rotate_270, 'transpose': transpose,
525
+ }
526
+ fn = fn_map.get(transform_name)
527
+ if not fn:
528
+ return None
529
+
530
+ color = params['color']
531
+ regions = flood_fill_regions(inp)
532
+ for reg in regions:
533
+ if reg['color'] == color:
534
+ r1, c1, r2, c2 = reg['bbox']
535
+ sub = extract_subgrid(inp, r1, c1, r2, c2)
536
+ return fn(sub)
537
+
538
+ return None
539
+
540
+
541
+ # ═══════════════════════════════════════════════════════════
542
+ # UTILITIES
543
+ # ═══════════════════════════════════════════════════════════
544
+
545
+ def _get_neighbors(g: Grid, r: int, c: int) -> Dict[str, int]:
546
+ """Get 4-connected neighbor colors"""
547
+ h, w = grid_shape(g)
548
+ nb = {}
549
+ if r > 0: nb['up'] = g[r-1][c]
550
+ if r < h-1: nb['down'] = g[r+1][c]
551
+ if c > 0: nb['left'] = g[r][c-1]
552
+ if c < w-1: nb['right'] = g[r][c+1]
553
+ return nb
554
+
555
+
556
+ # ═══════════════════════════════════════════════════════════
557
+ # ALL DETECTORS (ordered by priority β€” Verantyx: simplest first)
558
+ # ═══════════════════════════════════════════════════════════
559
+
560
+ ALL_DETECTORS = [
561
+ # (detect_fn, apply_fn)
562
+ (detect_conditional_fill, apply_conditional_fill),
563
+ (detect_region_transforms, apply_region_transform),
564
+ (detect_pattern_completion, apply_pattern_completion),
565
+ (detect_count_based, apply_count_based),
566
+ (detect_composite, apply_composite),
567
+ ]