Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
ARC-AGI-3
|
| 3 |
Hugging Face Space: beanapologist/arc-agi
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
"""
|
| 10 |
|
| 11 |
import gradio as gr
|
|
@@ -26,828 +26,1213 @@ from PIL import Image
|
|
| 26 |
ARC_HEX = ['#000000','#1a6faf','#e03a3a','#3aa63a','#f5c400',
|
| 27 |
'#c060c0','#d07030','#aaaaaa','#60b8d0','#874010']
|
| 28 |
ARC_CMAP = ListedColormap(ARC_HEX)
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
# ──
|
| 31 |
-
|
| 32 |
-
def C(r):
|
| 33 |
-
"""Coherence function: C(r) = 2r/(1+r²)"""
|
| 34 |
-
return 2*r / (1 + r**2 + 1e-9)
|
| 35 |
-
|
| 36 |
-
# ── Feature Extraction ────────────────────────────────────────────────────────
|
| 37 |
-
|
| 38 |
-
def extract_features(grid, num_colours=10):
|
| 39 |
-
"""Extract 14-channel feature tensor: 10 Re + 4 Im"""
|
| 40 |
-
H, W = grid.shape
|
| 41 |
-
|
| 42 |
-
# Re channels (0-9): one-hot color encoding
|
| 43 |
-
oh = np.zeros((num_colours, H, W), np.float32)
|
| 44 |
-
for c in range(num_colours):
|
| 45 |
-
oh[c] = (grid == c).astype(np.float32)
|
| 46 |
-
|
| 47 |
-
# Im channels: global structure
|
| 48 |
-
gx, gy = _sobel(grid.astype(np.float32) / 9)
|
| 49 |
-
h_sym = _sym(grid, 'h')
|
| 50 |
-
v_sym = _sym(grid, 'v')
|
| 51 |
-
boundary = _boundary(grid)
|
| 52 |
-
edge_mag = np.sqrt(gx**2 + gy**2).astype(np.float32)
|
| 53 |
-
|
| 54 |
-
stacked = np.concatenate([
|
| 55 |
-
oh, # ch 0-9: Re (colors)
|
| 56 |
-
h_sym[np.newaxis], # ch 10: Im (H-symmetry)
|
| 57 |
-
v_sym[np.newaxis], # ch 11: Im (V-symmetry)
|
| 58 |
-
boundary[np.newaxis], # ch 12: Im (boundary)
|
| 59 |
-
edge_mag[np.newaxis] # ch 13: Im (gradient)
|
| 60 |
-
], axis=0)
|
| 61 |
-
|
| 62 |
-
t = torch.from_numpy(stacked).float().unsqueeze(0)
|
| 63 |
-
if H != 64 or W != 64:
|
| 64 |
-
t = TF.interpolate(t, size=(64, 64), mode='bilinear', align_corners=False)
|
| 65 |
-
return t.squeeze(0)
|
| 66 |
|
| 67 |
def _sobel(f):
|
| 68 |
-
p
|
| 69 |
-
gx
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
def _boundary(grid):
|
| 76 |
-
p
|
| 77 |
-
return ((p[1:-1,1:-1]
|
| 78 |
-
(p[1:-1,1:-1]
|
| 79 |
|
| 80 |
-
def _sym(grid,
|
| 81 |
-
H,
|
| 82 |
-
|
| 83 |
-
if axis == 'h':
|
| 84 |
for x in range(W):
|
| 85 |
-
r
|
| 86 |
-
if r
|
| 87 |
-
|
| 88 |
-
continue
|
| 89 |
-
s[:, x] = (grid[:, x-r:x] == grid[:, x+1:x+r+1][:, ::-1]).mean()
|
| 90 |
else:
|
| 91 |
for y in range(H):
|
| 92 |
-
r
|
| 93 |
-
if r
|
| 94 |
-
|
| 95 |
-
continue
|
| 96 |
-
s[y, :] = (grid[y-r:y, :] == grid[y+1:y+r+1, :][::-1, :]).mean()
|
| 97 |
return s
|
| 98 |
|
| 99 |
-
# ── Re/Im
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
def
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
"""
|
| 107 |
H, W = grid.shape
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
#
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
#
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
r = min(top_mass, bot_mass) / max(top_mass, bot_mass, 1)
|
| 172 |
-
coherence = C(r)
|
| 173 |
-
confidence = v_sym_score * (1 - coherence)
|
| 174 |
-
|
| 175 |
-
if confidence > 0.35:
|
| 176 |
-
answer, cells = _complete_mirror(grid, 'v')
|
| 177 |
-
hypotheses.append({
|
| 178 |
-
'answer': answer,
|
| 179 |
-
'confidence': confidence,
|
| 180 |
-
'cells': cells,
|
| 181 |
-
'type': 'v_mirror',
|
| 182 |
-
'reasoning': f"Im: V-sym={v_sym_score:.2f} | Re: C(T/B)={coherence:.2f}"
|
| 183 |
-
})
|
| 184 |
-
|
| 185 |
-
# ── Hypothesis 3: Boundary Extraction ─────────────────────────
|
| 186 |
-
if fill_ratio > 0.4 and boundary_density < 0.3:
|
| 187 |
-
# Im: low boundary density (Cauchy contour thin)
|
| 188 |
-
# Re: high fill (interior dense)
|
| 189 |
-
# → radius cancels, extract arg (boundary)
|
| 190 |
-
|
| 191 |
-
r = boundary_density / (fill_ratio + 1e-6)
|
| 192 |
-
coherence = C(r)
|
| 193 |
-
confidence = (1 - coherence) * fill_ratio * 0.9
|
| 194 |
-
|
| 195 |
-
if confidence > 0.35:
|
| 196 |
-
answer, cells = _extract_boundary(grid)
|
| 197 |
-
hypotheses.append({
|
| 198 |
-
'answer': answer,
|
| 199 |
-
'confidence': confidence,
|
| 200 |
-
'cells': cells,
|
| 201 |
-
'type': 'boundary',
|
| 202 |
-
'reasoning': f"Im: ∮ density={boundary_density:.2f} | Re: fill={fill_ratio:.2f} | C={coherence:.2f}"
|
| 203 |
-
})
|
| 204 |
-
|
| 205 |
-
# ── Hypothesis 4: Interior Fill ───────────────────────────────
|
| 206 |
-
if fill_ratio > 0.2 and boundary_density > 0.15:
|
| 207 |
-
# Im: boundary exists
|
| 208 |
-
# Re: interior has holes
|
| 209 |
-
# → fill enclosed regions
|
| 210 |
-
|
| 211 |
-
enclosed = _find_enclosed(grid)
|
| 212 |
-
if enclosed.sum() > 0:
|
| 213 |
-
r = enclosed.sum() / max((grid == 0).sum(), 1)
|
| 214 |
-
confidence = boundary_density * r * 0.85
|
| 215 |
-
|
| 216 |
-
if confidence > 0.30:
|
| 217 |
-
answer, cells = _fill_interior(grid)
|
| 218 |
-
hypotheses.append({
|
| 219 |
-
'answer': answer,
|
| 220 |
-
'confidence': confidence,
|
| 221 |
-
'cells': cells,
|
| 222 |
-
'type': 'fill',
|
| 223 |
-
'reasoning': f"Im: boundary={boundary_density:.2f} | Re: holes={enclosed.sum()}"
|
| 224 |
-
})
|
| 225 |
-
|
| 226 |
-
# ── Hypothesis 5: Gravity ─────────────────────────────────────
|
| 227 |
-
suspended = _count_suspended(grid)
|
| 228 |
-
if suspended > 0 and edge_strength > 0.2:
|
| 229 |
-
r = suspended / max((grid > 0).sum(), 1)
|
| 230 |
-
confidence = edge_strength * r * 0.75
|
| 231 |
-
|
| 232 |
-
if confidence > 0.25:
|
| 233 |
-
answer, cells = _apply_gravity(grid)
|
| 234 |
-
hypotheses.append({
|
| 235 |
-
'answer': answer,
|
| 236 |
-
'confidence': confidence,
|
| 237 |
-
'cells': cells,
|
| 238 |
-
'type': 'gravity',
|
| 239 |
-
'reasoning': f"Im: ∇={edge_strength:.2f} | Re: suspended={suspended}"
|
| 240 |
-
})
|
| 241 |
-
|
| 242 |
-
# ══════════════════════════════════════════════════════════════
|
| 243 |
-
# Bridge: Select best hypothesis by coherence gap
|
| 244 |
-
# ══════════════════════════════════════════════════════════════
|
| 245 |
-
|
| 246 |
-
if not hypotheses:
|
| 247 |
-
return None, 0.0, ["No hypothesis > threshold"], 'none', None
|
| 248 |
-
|
| 249 |
-
hypotheses.sort(key=lambda h: h['confidence'], reverse=True)
|
| 250 |
-
best = hypotheses[0]
|
| 251 |
-
|
| 252 |
-
# Most urgent cell (boundary-first, Cauchy principle)
|
| 253 |
-
target_cell = _most_urgent_cell(grid, best['answer'], best['cells'])
|
| 254 |
-
|
| 255 |
-
reasoning = [best['reasoning'], f"Selected: {best['type']} (C={best['confidence']:.2f})"]
|
| 256 |
-
|
| 257 |
-
return best['answer'], best['confidence'], reasoning, best['type'], target_cell
|
| 258 |
-
|
| 259 |
-
def _resize_to_grid(feature_map, H, W):
|
| 260 |
-
"""Resize feature map to grid dimensions"""
|
| 261 |
-
fH, fW = feature_map.shape
|
| 262 |
-
ry = np.linspace(0, fH-1, H).astype(int)
|
| 263 |
-
rx = np.linspace(0, fW-1, W).astype(int)
|
| 264 |
-
return feature_map[np.ix_(ry, rx)]
|
| 265 |
-
|
| 266 |
-
def _complete_mirror(grid, axis):
|
| 267 |
-
"""Complete mirror symmetry along axis"""
|
| 268 |
-
H, W = grid.shape
|
| 269 |
-
answer = grid.copy()
|
| 270 |
-
cells = []
|
| 271 |
-
|
| 272 |
-
if axis == 'h':
|
| 273 |
-
# Find best fold axis in grid space
|
| 274 |
-
best_x = W // 2
|
| 275 |
for x in range(1, W-1):
|
| 276 |
r = min(x, W-1-x)
|
| 277 |
if r > 0:
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
if
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
if
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
for y in range(1, H-1):
|
| 308 |
r = min(y, H-1-y)
|
| 309 |
if r > 0:
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
if
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
if
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
answer
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
for c in range(W):
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
if
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
for r in range(H):
|
| 401 |
for c in range(W):
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
def __init__(self):
|
| 468 |
-
self.device
|
| 469 |
-
self.model
|
| 470 |
-
self.opt
|
| 471 |
-
self.buf
|
| 472 |
-
self.
|
| 473 |
-
self.
|
| 474 |
-
self.
|
| 475 |
-
self.
|
| 476 |
-
self.
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
def _make_model(self):
|
| 479 |
return nn.Sequential(
|
| 480 |
-
nn.Conv2d(14,
|
| 481 |
-
nn.Conv2d(32,
|
| 482 |
-
nn.Conv2d(64,
|
| 483 |
-
nn.AdaptiveAvgPool2d(8),
|
| 484 |
-
nn.Linear(128*8*8,
|
| 485 |
-
nn.Linear(256,
|
| 486 |
)
|
| 487 |
-
|
| 488 |
def reset(self):
|
| 489 |
-
self.model
|
| 490 |
-
self.opt
|
| 491 |
-
self.buf
|
| 492 |
-
self.
|
| 493 |
-
self.
|
| 494 |
-
self.
|
| 495 |
-
self.
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
if self.prev_feat is not None:
|
| 503 |
-
changed
|
| 504 |
-
self.prev_feat.cpu().numpy(),
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
self.reward_history.append(reward)
|
| 507 |
-
self.buf.append((self.prev_feat,
|
| 508 |
-
if len(self.buf)
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
| 512 |
self._train()
|
| 513 |
-
|
| 514 |
-
# ──
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
else:
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
meta['source'] = 'cnn'
|
| 555 |
-
|
| 556 |
-
with torch.no_grad():
|
| 557 |
-
logits = self.model(feat.unsqueeze(0)).squeeze(0)
|
| 558 |
-
|
| 559 |
-
# Mask to only available actions
|
| 560 |
-
indices = [m-1 for m in avail_ids if 1 <= m <= 6]
|
| 561 |
-
masked = torch.full((6,), float('-inf'))
|
| 562 |
-
for i in indices:
|
| 563 |
-
masked[i] = logits[i]
|
| 564 |
-
probs = torch.softmax(masked, dim=0).cpu().numpy()
|
| 565 |
-
probs = np.nan_to_num(probs, nan=0)
|
| 566 |
-
if probs.sum() == 0:
|
| 567 |
-
probs[np.array(indices)] = 1 / len(indices)
|
| 568 |
-
probs = probs / probs.sum()
|
| 569 |
-
cnn_action_idx = np.random.choice(6, p=probs)
|
| 570 |
-
|
| 571 |
-
chosen_id = cnn_action_idx + 1
|
| 572 |
-
meta['probs'] = probs.tolist()
|
| 573 |
-
|
| 574 |
-
# Make sure chosen action is actually available
|
| 575 |
-
if chosen_id not in avail_ids and avail_ids:
|
| 576 |
-
chosen_id = list(avail_ids)[0]
|
| 577 |
-
|
| 578 |
-
self.prev_feat = feat
|
| 579 |
-
self.prev_action = chosen_id - 1
|
| 580 |
-
self.step_count += 1
|
| 581 |
-
self.action_counts[chosen_id] = self.action_counts.get(chosen_id, 0) + 1
|
| 582 |
-
|
| 583 |
-
# Mock action object
|
| 584 |
-
class MockAction:
|
| 585 |
-
def __init__(self, val):
|
| 586 |
-
self.value = val
|
| 587 |
-
def set_data(self, data):
|
| 588 |
-
pass
|
| 589 |
-
|
| 590 |
-
action = MockAction(chosen_id)
|
| 591 |
-
return action, meta
|
| 592 |
-
|
| 593 |
-
def _train(self):
|
| 594 |
-
import random
|
| 595 |
-
batch = random.sample(self.buf, min(16, len(self.buf)))
|
| 596 |
-
states = torch.stack([b[0] for b in batch]).to(self.device)
|
| 597 |
-
actions = torch.tensor([b[1] for b in batch], dtype=torch.long, device=self.device)
|
| 598 |
-
rewards = torch.tensor([b[2] for b in batch], dtype=torch.float32, device=self.device)
|
| 599 |
-
|
| 600 |
-
self.opt.zero_grad()
|
| 601 |
-
logits = self.model(states)
|
| 602 |
-
loss = TF.binary_cross_entropy_with_logits(
|
| 603 |
-
logits.gather(1, actions.unsqueeze(1)).squeeze(1),
|
| 604 |
-
torch.clamp(rewards, 0, 1))
|
| 605 |
-
loss.backward()
|
| 606 |
-
self.opt.step()
|
| 607 |
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
img = Image.open(buf).copy()
|
| 616 |
-
plt.close(fig)
|
| 617 |
-
return img
|
| 618 |
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
col = 'white' if v in [0, 1, 2, 3, 5, 6, 9] else 'black'
|
| 638 |
-
ax.text(c, r, str(v), ha='center', va='center',
|
| 639 |
-
fontsize=max(7, cell//5), color=col,
|
| 640 |
-
fontweight='bold', fontfamily='monospace')
|
| 641 |
-
|
| 642 |
-
if highlight:
|
| 643 |
-
for r, c, _ in highlight[:20]:
|
| 644 |
-
ax.add_patch(plt.Rectangle((c-.5, r-.5), 1, 1,
|
| 645 |
-
fill=True, facecolor='#ff4444', alpha=0.35, lw=0))
|
| 646 |
-
|
| 647 |
-
if mark_cell:
|
| 648 |
-
r, c, _ = mark_cell
|
| 649 |
-
ax.add_patch(plt.Rectangle((c-.5, r-.5), 1, 1,
|
| 650 |
-
fill=False, edgecolor='#00ffff', lw=2.5))
|
| 651 |
-
ax.plot(c, r, '*', color='#00ffff', markersize=max(8, cell//4))
|
| 652 |
-
|
| 653 |
-
ax.set_xlim(-.5, W-.5)
|
| 654 |
-
ax.set_ylim(H-.5, -.5)
|
| 655 |
-
ax.axis('off')
|
| 656 |
-
if title:
|
| 657 |
-
ax.set_title(title, color='#cdd6f4', fontsize=9, pad=4)
|
| 658 |
-
plt.tight_layout(pad=.3)
|
| 659 |
-
return _pil(fig)
|
| 660 |
|
| 661 |
# ── Session ───────────────────────────────────────────────────────────────────
|
| 662 |
|
| 663 |
-
_agent
|
| 664 |
-
_stop_flag
|
| 665 |
_run_thread = None
|
| 666 |
-
_frame_queue
|
| 667 |
|
| 668 |
-
def _run_agent(game_id,
|
| 669 |
-
|
| 670 |
try:
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
grid = raw[-1] if raw.ndim == 3 else raw
|
| 686 |
-
avail = getattr(frame, 'available_actions', None)
|
| 687 |
-
levels = getattr(frame, 'levels_completed', 0)
|
| 688 |
-
state = getattr(frame, 'state', None)
|
| 689 |
-
|
| 690 |
-
action, meta = _agent.choose(grid, avail, levels=levels, state=state)
|
| 691 |
-
|
| 692 |
-
# Add available actions to meta for debugging
|
| 693 |
-
if avail:
|
| 694 |
-
meta['available_actions'] = [int(a.value if hasattr(a, 'value') else a)
|
| 695 |
-
for a in avail]
|
| 696 |
-
|
| 697 |
-
diff = (grid != prev_grid) if prev_grid is not None else None
|
| 698 |
-
prev_grid = grid.copy()
|
| 699 |
-
|
| 700 |
_frame_queue.put({
|
| 701 |
-
'grid':
|
| 702 |
-
'
|
| 703 |
-
'
|
| 704 |
-
'
|
| 705 |
-
'
|
| 706 |
-
'
|
| 707 |
-
'
|
| 708 |
-
'
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
state_str = str(state)
|
| 713 |
-
if 'WIN' in state_str or 'GAME_OVER' in state_str:
|
| 714 |
-
break
|
| 715 |
-
|
| 716 |
-
# Execute action
|
| 717 |
try:
|
| 718 |
from arcengine import GameAction as GA
|
| 719 |
-
a_int
|
| 720 |
-
sa
|
| 721 |
if meta.get('x') is not None:
|
| 722 |
-
try:
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
frame = env.step(action)
|
| 731 |
-
except:
|
| 732 |
-
frame = None
|
| 733 |
-
|
| 734 |
-
step += 1
|
| 735 |
time.sleep(0.08)
|
| 736 |
-
|
| 737 |
-
|
| 738 |
except Exception as e:
|
| 739 |
-
_frame_queue.put({'error':
|
|
|
|
|
|
|
| 740 |
|
| 741 |
-
_latest
|
| 742 |
-
|
| 743 |
-
'hyp_img': None,
|
| 744 |
-
'cand_img': None,
|
| 745 |
-
'status': '*Ready*'
|
| 746 |
-
}
|
| 747 |
|
| 748 |
def pull_frame():
|
| 749 |
global _latest
|
| 750 |
-
data
|
| 751 |
while True:
|
| 752 |
-
try:
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
break
|
| 756 |
-
|
| 757 |
if data is None:
|
| 758 |
-
return (_latest['grid_img'],
|
| 759 |
-
|
|
|
|
| 760 |
if 'error' in data:
|
| 761 |
-
_latest['status']
|
| 762 |
-
return (_latest['grid_img'],
|
| 763 |
-
|
|
|
|
| 764 |
if data.get('done'):
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
source
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
grid,
|
| 791 |
-
title=f"Step {step} | {source_emoji} A{action}",
|
| 792 |
highlight=highlight,
|
| 793 |
-
mark_cell=mark_cell
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
if
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
_latest['
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
|
| 821 |
def fetch_games(api_key):
|
| 822 |
-
"""Fetch available games from ARC API"""
|
| 823 |
try:
|
| 824 |
import arc_agi
|
| 825 |
-
arc
|
| 826 |
-
envs
|
| 827 |
-
ids
|
| 828 |
-
return gr.Dropdown(choices=ids,
|
|
|
|
| 829 |
except Exception as e:
|
| 830 |
-
return gr.Dropdown(choices=[]),
|
| 831 |
-
|
| 832 |
-
def start_agent(game_id,
|
| 833 |
-
global _run_thread,
|
| 834 |
-
if not game_id:
|
| 835 |
-
|
| 836 |
-
if not api_key:
|
| 837 |
-
return "Enter your API key."
|
| 838 |
_stop_flag.set()
|
| 839 |
-
if _run_thread and _run_thread.is_alive():
|
| 840 |
-
_run_thread.join(timeout=3)
|
| 841 |
while not _frame_queue.empty():
|
| 842 |
-
try:
|
| 843 |
-
|
| 844 |
-
except:
|
| 845 |
-
break
|
| 846 |
_stop_flag.clear()
|
| 847 |
-
_run_thread
|
| 848 |
-
target=_run_agent,
|
| 849 |
_run_thread.start()
|
| 850 |
-
return f"Agent started on **{game_id}** — 🧠 Re/Im
|
| 851 |
|
| 852 |
def stop_agent():
|
| 853 |
_stop_flag.set()
|
|
@@ -855,67 +1240,75 @@ def stop_agent():
|
|
| 855 |
|
| 856 |
# ── UI ────────────────────────────────────────────────────────────────────────
|
| 857 |
|
| 858 |
-
with gr.Blocks(title="ARC-AGI-3
|
| 859 |
-
gr.Markdown("""
|
| 860 |
-
# ARC-AGI-3 Fluid Re/Im Agent
|
| 861 |
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
**
|
|
|
|
| 865 |
|
| 866 |
-
🧠 = analytic solver (Im picks
|
| 867 |
-
🎲 = CNN fallback (when no hypothesis clears threshold)
|
| 868 |
""")
|
| 869 |
-
|
| 870 |
with gr.Row():
|
| 871 |
with gr.Column(scale=3):
|
| 872 |
-
api_box
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
value=os.environ.get("ARC_API_KEY", ""),
|
| 876 |
-
placeholder="arc-key-... or set ARC_API_KEY secret"
|
| 877 |
-
)
|
| 878 |
with gr.Column(scale=1):
|
| 879 |
-
fetch_btn
|
| 880 |
-
|
| 881 |
-
api_status = gr.Markdown()
|
| 882 |
-
|
| 883 |
with gr.Row():
|
| 884 |
with gr.Column(scale=2):
|
| 885 |
-
game_dd
|
| 886 |
with gr.Column(scale=1):
|
| 887 |
-
steps_sl
|
| 888 |
with gr.Column(scale=1):
|
| 889 |
with gr.Row():
|
| 890 |
-
start_btn
|
| 891 |
-
stop_btn =
|
| 892 |
-
|
| 893 |
-
run_status
|
| 894 |
-
|
|
|
|
| 895 |
gr.Markdown("---")
|
| 896 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
with gr.Row():
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
gr.Markdown("""
|
| 911 |
---
|
| 912 |
**Re/Im duality in action:**
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
The Re side
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
CNN fires only when analytic
|
| 919 |
""")
|
| 920 |
|
| 921 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
+
ARC-AGI-3 Agent Spectator v4
|
| 3 |
Hugging Face Space: beanapologist/arc-agi
|
| 4 |
|
| 5 |
+
Re/Im solver live demo:
|
| 6 |
+
Im side = bird's eye hypothesis (which transformation?)
|
| 7 |
+
Re side = exact diff (which cells to click?)
|
| 8 |
+
Bridge = ACTION6 at the Re-side coordinates that close the gap
|
| 9 |
"""
|
| 10 |
|
| 11 |
import gradio as gr
|
|
|
|
| 26 |
ARC_HEX = ['#000000','#1a6faf','#e03a3a','#3aa63a','#f5c400',
|
| 27 |
'#c060c0','#d07030','#aaaaaa','#60b8d0','#874010']
|
| 28 |
ARC_CMAP = ListedColormap(ARC_HEX)
|
| 29 |
+
COLOR_NAMES = ['black','blue','red','green','yellow',
|
| 30 |
+
'purple','orange','gray','azure','maroon']
|
| 31 |
|
| 32 |
+
# ── Re/Im primitives ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def _sobel(f):
|
| 35 |
+
p=np.pad(f,1,mode='edge')
|
| 36 |
+
gx=(-p[:-2,:-2]-2*p[1:-1,:-2]-p[2:,:-2]+p[:-2,2:]+2*p[1:-1,2:]+p[2:,2:])/8
|
| 37 |
+
gy=(-p[:-2,:-2]-2*p[:-2,1:-1]-p[:-2,2:]+p[2:,:-2]+2*p[2:,1:-1]+p[2:,2:])/8
|
| 38 |
+
return gx,gy
|
| 39 |
+
|
| 40 |
+
def _sym_axis(grid,axis):
|
| 41 |
+
H,W=grid.shape; best_s,best_i=0.0,0
|
| 42 |
+
if axis=='h':
|
| 43 |
+
for x in range(1,W-1):
|
| 44 |
+
r=min(x,W-1-x)
|
| 45 |
+
s=(grid[:,x-r:x]==grid[:,x+1:x+r+1][:,::-1]).mean()
|
| 46 |
+
if s>best_s: best_s,best_i=s,x
|
| 47 |
+
else:
|
| 48 |
+
for y in range(1,H-1):
|
| 49 |
+
r=min(y,H-1-y)
|
| 50 |
+
s=(grid[y-r:y,:]==grid[y+1:y+r+1,:][::-1,:]).mean()
|
| 51 |
+
if s>best_s: best_s,best_i=s,y
|
| 52 |
+
return best_i,best_s
|
| 53 |
|
| 54 |
def _boundary(grid):
|
| 55 |
+
p=np.pad(grid,1,mode='edge')
|
| 56 |
+
return ((p[1:-1,1:-1]!=p[:-2,1:-1])|(p[1:-1,1:-1]!=p[2:,1:-1])|
|
| 57 |
+
(p[1:-1,1:-1]!=p[1:-1,:-2])|(p[1:-1,1:-1]!=p[1:-1,2:])).astype(np.float32)
|
| 58 |
|
| 59 |
+
def _sym(grid,axis):
|
| 60 |
+
H,W=grid.shape; s=np.zeros((H,W),np.float32)
|
| 61 |
+
if axis=='h':
|
|
|
|
| 62 |
for x in range(W):
|
| 63 |
+
r=min(x,W-1-x)
|
| 64 |
+
if r==0: s[:,x]=1.; continue
|
| 65 |
+
s[:,x]=(grid[:,x-r:x]==grid[:,x+1:x+r+1][:,::-1]).mean()
|
|
|
|
|
|
|
| 66 |
else:
|
| 67 |
for y in range(H):
|
| 68 |
+
r=min(y,H-1-y)
|
| 69 |
+
if r==0: s[y,:]=1.; continue
|
| 70 |
+
s[y,:]=(grid[y-r:y,:]==grid[y+1:y+r+1,:][::-1,:]).mean()
|
|
|
|
|
|
|
| 71 |
return s
|
| 72 |
|
| 73 |
+
# ── Re/Im board reader — reads directly from 56-channel feature tensor ─────────
|
| 74 |
+
# Im tensor = signal strength | raw grid = coordinates | i· = answer
|
| 75 |
+
|
| 76 |
+
"""
|
| 77 |
+
arc_solver.py — Re/Im analytic solver using the 56-channel feature tensor
|
| 78 |
+
=========================================================================
|
| 79 |
+
|
| 80 |
+
The board IS M = Re(M) + i·Im(M).
|
| 81 |
+
The 56-channel extractor already IS log(M) decomposed:
|
| 82 |
+
|
| 83 |
+
Re channels (ch 0-47):
|
| 84 |
+
ch 0-15: one-hot colors — multiplicative / "what's there"
|
| 85 |
+
ch 16-31: component size maps — "how many, how big" (radial magnitude)
|
| 86 |
+
ch 32-47: distance-from-color maps — proximity / modulus
|
| 87 |
+
|
| 88 |
+
Im channels (ch 48-55):
|
| 89 |
+
ch 48: H-symmetry map — fold axis (Re(s)=1/2 analog)
|
| 90 |
+
ch 49: V-symmetry map — fold axis (vertical)
|
| 91 |
+
ch 50: rotational symmetry — winding structure
|
| 92 |
+
ch 51: Sobel x (edge horiz) — gradient Re component
|
| 93 |
+
ch 52: Sobel y (edge vert) — gradient Im component ← arg(M)
|
| 94 |
+
ch 53: boundary contour — Cauchy ∮ contour
|
| 95 |
+
ch 54: curl / winding proxy — arg(M), rotation direction
|
| 96 |
+
ch 55: component ID map — global topological / winding data
|
| 97 |
+
|
| 98 |
+
The Im channels are not clues — they ARE the answer structure.
|
| 99 |
+
i· (column swap) means: read the Im channels to get the Re answer.
|
| 100 |
+
|
| 101 |
+
Im H-symmetry strong + Re mass asymmetric → mirror to complete
|
| 102 |
+
Im boundary contour + Re solid fill → boundary IS the answer
|
| 103 |
+
Im curl/winding strong → rotation is needed
|
| 104 |
+
Im Sobel-y dominates → vertical flow (gravity)
|
| 105 |
+
Im component map has isolated islands → count → place
|
| 106 |
+
|
| 107 |
+
The CNN operates on all 56 channels.
|
| 108 |
+
This solver reads the Im channels analytically to short-circuit the CNN
|
| 109 |
+
when the Im signal is unambiguous.
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
import numpy as np
|
| 113 |
+
from typing import Optional, Tuple, List
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ── Re/Im channel indices (must match extract_features in my_agent.py) ────────
|
| 117 |
+
|
| 118 |
+
# Space uses 14-channel extractor (subset of full 56-channel Kaggle version)
|
| 119 |
+
# ch 0-9: Re — one-hot colors
|
| 120 |
+
# ch 10: Im — H-symmetry (fold axis)
|
| 121 |
+
# ch 11: Im — V-symmetry
|
| 122 |
+
# ch 12: Im — Cauchy boundary contour
|
| 123 |
+
# ch 13: Im — edge magnitude (Sobel combined)
|
| 124 |
+
CH_ONEHOT_START = 0
|
| 125 |
+
CH_CCSIZE_START = 0 # not in Space extractor — use onehot
|
| 126 |
+
CH_DIST_START = 0 # not in Space extractor — use onehot
|
| 127 |
+
CH_H_SYM = 10 # Im — horizontal symmetry
|
| 128 |
+
CH_V_SYM = 11 # Im — vertical symmetry
|
| 129 |
+
CH_ROT_SYM = 10 # not separate — reuse H-sym
|
| 130 |
+
CH_SOBEL_X = 13 # Im — edge (proxy for gradient)
|
| 131 |
+
CH_SOBEL_Y = 13 # Im — edge (proxy for gradient)
|
| 132 |
+
CH_BOUNDARY = 12 # Im — Cauchy boundary contour
|
| 133 |
+
CH_CURL = 10 # not separate — reuse H-sym
|
| 134 |
+
CH_COMPONENT_ID = 11 # not separate — reuse V-sym
|
| 135 |
+
|
| 136 |
+
NUM_CHANNELS = 14 # Space extractor has 14 channels
|
| 137 |
+
CONFIDENCE_THRESHOLD = 0.72
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# ── Raw grid primitives (for when we don't have the tensor yet) ───────────────
|
| 141 |
+
|
| 142 |
+
def _boundary_raw(grid):
|
| 143 |
+
p = np.pad(grid, 1, mode='edge')
|
| 144 |
+
return ((p[1:-1,1:-1]!=p[:-2,1:-1])|(p[1:-1,1:-1]!=p[2:,1:-1])|
|
| 145 |
+
(p[1:-1,1:-1]!=p[1:-1,:-2])|(p[1:-1,1:-1]!=p[1:-1,2:])).astype(np.float32)
|
| 146 |
+
|
| 147 |
+
def _perimeter(grid):
|
| 148 |
+
"""Object perimeter — handles solid blocks where _boundary_raw gives 0."""
|
| 149 |
+
H, W = grid.shape
|
| 150 |
+
p = np.zeros((H,W), dtype=np.float32)
|
| 151 |
+
mask = grid > 0
|
| 152 |
+
if not mask.any(): return p
|
| 153 |
+
padded = np.pad(mask.astype(int), 1, constant_values=0)
|
| 154 |
+
for dy,dx in [(-1,0),(1,0),(0,-1),(0,1)]:
|
| 155 |
+
shifted = padded[1+dy:H+1+dy, 1+dx:W+1+dx]
|
| 156 |
+
p[mask & (shifted==0)] = 1
|
| 157 |
+
if p.sum() == 0: # solid block: use outer ring
|
| 158 |
+
p[0,:] = mask[0,:].astype(float); p[-1,:] = mask[-1,:].astype(float)
|
| 159 |
+
p[:,0] = mask[:,0].astype(float); p[:,-1] = mask[:,-1].astype(float)
|
| 160 |
+
return p
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ── Re/Im board reader ────────────────────────────────────────────────────────
|
| 164 |
|
| 165 |
+
def read_board(grid: np.ndarray, features: Optional[np.ndarray] = None):
|
| 166 |
"""
|
| 167 |
+
Read the board as complex object M = Re(M) + i·Im(M).
|
| 168 |
+
|
| 169 |
+
Parameters
|
| 170 |
+
----------
|
| 171 |
+
grid : 2D int array, the raw color grid
|
| 172 |
+
features : optional (56, H, W) float array from extract_features()
|
| 173 |
+
If provided, reads Im channels directly (more accurate).
|
| 174 |
+
If None, recomputes Im channels from the raw grid.
|
| 175 |
+
|
| 176 |
+
Returns
|
| 177 |
+
-------
|
| 178 |
+
answer : np.ndarray — derived answer grid
|
| 179 |
+
confidence : float 0-1
|
| 180 |
+
reasoning : list of strings — one per Im signal that fired
|
| 181 |
+
signal : str — which Im channel drove the answer
|
| 182 |
"""
|
| 183 |
H, W = grid.shape
|
| 184 |
+
reasoning = []
|
| 185 |
+
answer = None
|
| 186 |
+
confidence = 0.0
|
| 187 |
+
signal = 'none'
|
| 188 |
+
|
| 189 |
+
# ── Read Im channels ──────────────────────────────────────────────────────
|
| 190 |
+
# Use pre-computed feature tensor if available (same computation the CNN uses)
|
| 191 |
+
if features is not None and features.shape[0] == NUM_CHANNELS:
|
| 192 |
+
feat = features
|
| 193 |
+
# Resize Im maps back to grid space if needed
|
| 194 |
+
fH, fW = feat.shape[1], feat.shape[2]
|
| 195 |
+
|
| 196 |
+
def _im(ch):
|
| 197 |
+
"""Read Im channel, resize to grid if needed."""
|
| 198 |
+
m = feat[ch]
|
| 199 |
+
if (fH, fW) != (H, W):
|
| 200 |
+
# Simple nearest-neighbor resize
|
| 201 |
+
ry = np.linspace(0, fH-1, H).astype(int)
|
| 202 |
+
rx = np.linspace(0, fW-1, W).astype(int)
|
| 203 |
+
return m[np.ix_(ry, rx)]
|
| 204 |
+
return np.array(m)
|
| 205 |
+
|
| 206 |
+
h_sym_map = _im(CH_H_SYM)
|
| 207 |
+
v_sym_map = _im(CH_V_SYM)
|
| 208 |
+
rot_map = _im(CH_ROT_SYM)
|
| 209 |
+
sobel_x = _im(CH_SOBEL_X)
|
| 210 |
+
sobel_y = _im(CH_SOBEL_Y)
|
| 211 |
+
bound_map = _im(CH_BOUNDARY)
|
| 212 |
+
curl_map = _im(CH_CURL)
|
| 213 |
+
comp_map = _im(CH_COMPONENT_ID)
|
| 214 |
+
|
| 215 |
+
# Re channels: color presence and component sizes
|
| 216 |
+
onehot = feat[CH_ONEHOT_START:CH_ONEHOT_START+16]
|
| 217 |
+
cc_sizes = feat[CH_CCSIZE_START:CH_CCSIZE_START+16]
|
| 218 |
+
|
| 219 |
+
else:
|
| 220 |
+
# Compute features inline and retry once (no recursion)
|
| 221 |
+
t = extract_features(grid)
|
| 222 |
+
feat_np = t.numpy() if hasattr(t, 'numpy') else np.array(t)
|
| 223 |
+
if feat_np.shape[0] == NUM_CHANNELS:
|
| 224 |
+
return read_board(grid, features=feat_np)
|
| 225 |
+
# Shape mismatch — just compute Im maps directly from grid
|
| 226 |
+
_gx,_gy = _sobel(grid.astype(np.float32)/9)
|
| 227 |
+
h_sym_map = _sym(grid,'h'); v_sym_map = _sym(grid,'v')
|
| 228 |
+
bound_map = _boundary(grid)
|
| 229 |
+
edge_map = np.sqrt(_gx**2+_gy**2).astype(np.float32)
|
| 230 |
+
# Build minimal feature array
|
| 231 |
+
_oh = np.zeros((10,H,W),np.float32)
|
| 232 |
+
for _c in range(10): _oh[_c]=(grid==_c).astype(np.float32)
|
| 233 |
+
features = np.concatenate([_oh,h_sym_map[np.newaxis],v_sym_map[np.newaxis],
|
| 234 |
+
bound_map[np.newaxis],edge_map[np.newaxis]],axis=0)
|
| 235 |
+
return read_board(grid, features=features)
|
| 236 |
+
|
| 237 |
+
# ── Im signal 1: H-symmetry (fold axis) ───────────────────────────────────
|
| 238 |
+
# Im ch48 gives the symmetry score — use its MAX as signal strength.
|
| 239 |
+
# But compute the actual best axis in raw grid space (not 64x64 space).
|
| 240 |
+
# This is the Re/Im separation: Im tensor gives the SIGNAL,
|
| 241 |
+
# Re grid gives the COORDINATES.
|
| 242 |
+
best_hs = float(h_sym_map.max()) # Im: how strong is any fold?
|
| 243 |
+
|
| 244 |
+
if best_hs > 0.65:
|
| 245 |
+
# Find best axis in grid space (not feature space)
|
| 246 |
+
h_scores_grid = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
for x in range(1, W-1):
|
| 248 |
r = min(x, W-1-x)
|
| 249 |
if r > 0:
|
| 250 |
+
s = (grid[:, x-r:x] == grid[:, x+1:x+r+1][:,::-1]).mean()
|
| 251 |
+
h_scores_grid.append((x, float(s)))
|
| 252 |
+
if h_scores_grid:
|
| 253 |
+
best_hx, best_hs_grid = max(h_scores_grid, key=lambda x: x[1])
|
| 254 |
+
# Use Im tensor strength as upper bound, grid score as actual
|
| 255 |
+
hs = min(best_hs, best_hs_grid) if best_hs_grid > 0.4 else best_hs_grid
|
| 256 |
+
left_px = int((grid[:, :best_hx] > 0).sum())
|
| 257 |
+
right_px = int((grid[:, best_hx:] > 0).sum())
|
| 258 |
+
total_px = left_px + right_px
|
| 259 |
+
asymmetry = abs(left_px - right_px) / max(total_px, 1)
|
| 260 |
+
if asymmetry > 0.25 and hs > 0.55:
|
| 261 |
+
ans = grid.copy()
|
| 262 |
+
if left_px > right_px:
|
| 263 |
+
for c in range(best_hx):
|
| 264 |
+
mir = W - 1 - c
|
| 265 |
+
if 0 <= mir < W:
|
| 266 |
+
mask = ans[:, mir] == 0
|
| 267 |
+
ans[mask, mir] = grid[mask, c]
|
| 268 |
+
else:
|
| 269 |
+
for c in range(best_hx+1, W):
|
| 270 |
+
mir = W - 1 - c
|
| 271 |
+
if 0 <= mir < W:
|
| 272 |
+
mask = ans[:, mir] == 0
|
| 273 |
+
ans[mask, mir] = grid[mask, c]
|
| 274 |
+
conf = hs * asymmetry * 0.95
|
| 275 |
+
if conf > confidence:
|
| 276 |
+
answer, confidence, signal = ans, conf, 'Im:H-sym'
|
| 277 |
+
reasoning.append(
|
| 278 |
+
f"Im ch48 signal={best_hs:.2f} grid_score={hs:.2f} at x={best_hx} | "
|
| 279 |
+
f"Re left={left_px} right={right_px} asym={asymmetry:.2f} | "
|
| 280 |
+
f"i·: complete the fold")
|
| 281 |
+
|
| 282 |
+
# ── Im signal 2: V-symmetry ────────────────────────────────────────────────
|
| 283 |
+
best_vs = float(v_sym_map.max()) # Im: V-fold signal strength
|
| 284 |
+
|
| 285 |
+
if best_vs > 0.65 and confidence < 0.6:
|
| 286 |
+
v_scores_grid = []
|
| 287 |
for y in range(1, H-1):
|
| 288 |
r = min(y, H-1-y)
|
| 289 |
if r > 0:
|
| 290 |
+
s = (grid[y-r:y, :] == grid[y+1:y+r+1, :][::-1, :]).mean()
|
| 291 |
+
v_scores_grid.append((y, float(s)))
|
| 292 |
+
if v_scores_grid:
|
| 293 |
+
best_vy, best_vs_grid = max(v_scores_grid, key=lambda x: x[1])
|
| 294 |
+
vs = min(best_vs, best_vs_grid) if best_vs_grid > 0.4 else best_vs_grid
|
| 295 |
+
top_px = int((grid[:best_vy, :] > 0).sum())
|
| 296 |
+
bot_px = int((grid[best_vy:, :] > 0).sum())
|
| 297 |
+
total_px = top_px + bot_px
|
| 298 |
+
asymmetry = abs(top_px - bot_px) / max(total_px, 1)
|
| 299 |
+
if asymmetry > 0.25 and vs > 0.55:
|
| 300 |
+
ans = grid.copy()
|
| 301 |
+
if top_px > bot_px:
|
| 302 |
+
for r in range(best_vy):
|
| 303 |
+
mir = H - 1 - r
|
| 304 |
+
if 0 <= mir < H:
|
| 305 |
+
mask = ans[mir, :] == 0
|
| 306 |
+
ans[mir, mask] = grid[r, mask]
|
| 307 |
+
else:
|
| 308 |
+
for r in range(best_vy+1, H):
|
| 309 |
+
mir = H - 1 - r
|
| 310 |
+
if 0 <= mir < H:
|
| 311 |
+
mask = ans[mir, :] == 0
|
| 312 |
+
ans[mir, mask] = grid[r, mask]
|
| 313 |
+
conf = vs * asymmetry * 0.90
|
| 314 |
+
if conf > confidence:
|
| 315 |
+
answer, confidence, signal = ans, conf, 'Im:V-sym'
|
| 316 |
+
reasoning.append(
|
| 317 |
+
f"Im ch49 signal={best_vs:.2f} grid_score={vs:.2f} at y={best_vy} | "
|
| 318 |
+
f"Re top={top_px} bot={bot_px} | i·: complete V fold")
|
| 319 |
+
|
| 320 |
+
# ── Im signal 3: Cauchy boundary contour ──────────────────────────────────
|
| 321 |
+
# Im ch53 IS the Cauchy contour. When Re fill is solid (high density)
|
| 322 |
+
# and Im boundary is thin, the answer IS the boundary.
|
| 323 |
+
# "The radius cancels (Re collapses); only the angular part i·dθ survives"
|
| 324 |
+
total_px = int((grid > 0).sum())
|
| 325 |
+
if total_px > 0 and confidence < 0.75:
|
| 326 |
+
fill_ratio = total_px / (H * W)
|
| 327 |
+
colors = [c for c in range(1,10) if (grid==c).any()]
|
| 328 |
+
|
| 329 |
+
if len(colors) == 1 and fill_ratio > 0.4:
|
| 330 |
+
# Single color, high fill → Im boundary IS the answer
|
| 331 |
+
perim = _perimeter(grid)
|
| 332 |
+
# Check interior isn't already hollow
|
| 333 |
+
interior = (grid == 0) & (perim == 0)
|
| 334 |
+
if not interior.any():
|
| 335 |
+
ans = np.zeros_like(grid)
|
| 336 |
+
ans[perim > 0] = grid[perim > 0]
|
| 337 |
+
conf = fill_ratio * 0.90
|
| 338 |
+
if conf > confidence:
|
| 339 |
+
answer, confidence, signal = ans, conf, 'Im:boundary'
|
| 340 |
+
reasoning.append(
|
| 341 |
+
f"Im ch53 Cauchy contour | Re fill={fill_ratio:.2f} "
|
| 342 |
+
f"single color={colors[0]} | "
|
| 343 |
+
f"i·: Im boundary = Re answer (Cauchy: radius cancels)")
|
| 344 |
+
|
| 345 |
+
elif fill_ratio > 0.3 and bound_map.mean() < 0.15:
|
| 346 |
+
# Multi-color but low boundary density → extract boundary
|
| 347 |
+
ans = np.zeros_like(grid)
|
| 348 |
+
ans[bound_map > 0.3] = grid[bound_map > 0.3]
|
| 349 |
+
if (ans > 0).any():
|
| 350 |
+
conf = fill_ratio * (1 - float(bound_map.mean())) * 0.75
|
| 351 |
+
if conf > confidence:
|
| 352 |
+
answer, confidence, signal = ans, conf, 'Im:boundary'
|
| 353 |
+
reasoning.append(
|
| 354 |
+
f"Im ch53 boundary density={bound_map.mean():.3f} | "
|
| 355 |
+
f"Re fill={fill_ratio:.2f} | i·: Cauchy contour")
|
| 356 |
+
|
| 357 |
+
# ── Im signal 4: Interior fill ────────────────────────────────────────────
|
| 358 |
+
# Im ch53: If colored region fully encloses empty cells → fill interior.
|
| 359 |
+
# Use flood-fill from boundary to find truly enclosed (unreachable) cells.
|
| 360 |
+
if confidence < 0.55 and total_px > 0:
|
| 361 |
+
# Flood-fill empty cells reachable from grid boundary
|
| 362 |
+
reachable = np.zeros((H, W), dtype=bool)
|
| 363 |
+
fq = []
|
| 364 |
+
for _r in range(H):
|
| 365 |
+
for _c in range(W):
|
| 366 |
+
if grid[_r,_c]==0 and (_r==0 or _r==H-1 or _c==0 or _c==W-1):
|
| 367 |
+
if not reachable[_r,_c]:
|
| 368 |
+
reachable[_r,_c]=True; fq.append((_r,_c))
|
| 369 |
+
while fq:
|
| 370 |
+
_y,_x=fq.pop()
|
| 371 |
+
for _dy,_dx in [(-1,0),(1,0),(0,-1),(0,1)]:
|
| 372 |
+
_ny,_nx=_y+_dy,_x+_dx
|
| 373 |
+
if 0<=_ny<H and 0<=_nx<W and grid[_ny,_nx]==0 and not reachable[_ny,_nx]:
|
| 374 |
+
reachable[_ny,_nx]=True; fq.append((_ny,_nx))
|
| 375 |
+
truly_interior = (grid == 0) & ~reachable
|
| 376 |
+
if truly_interior.any():
|
| 377 |
+
dominant = int(np.argmax(np.bincount(
|
| 378 |
+
grid[grid>0].flatten(), minlength=10)[1:])) + 1
|
| 379 |
+
ans = grid.copy()
|
| 380 |
+
ans[truly_interior] = dominant
|
| 381 |
+
conf = truly_interior.sum() / max(1, (grid==0).sum()) * 0.80
|
| 382 |
+
if conf > confidence:
|
| 383 |
+
answer, confidence, signal = ans, conf, 'Im:hollow→fill'
|
| 384 |
+
reasoning.append(
|
| 385 |
+
f"Im ch53 enclosed interior={int(truly_interior.sum())}px | "
|
| 386 |
+
f"Re dominant={dominant} | i·: fill enclosed interior")
|
| 387 |
+
|
| 388 |
+
# ── Im signal 5: Gradient flow → gravity ──────────────────────────────────
|
| 389 |
+
# Im ch51/52 = Sobel x/y = gradient field direction = arg(M)
|
| 390 |
+
# Suspended Re pixels + Im gradient direction → gravity answer
|
| 391 |
+
suspended = sum(
|
| 392 |
+
1 for c in range(W)
|
| 393 |
+
for r in np.where(grid[:, c] > 0)[0]
|
| 394 |
+
if r < H-1 and grid[r+1, c] == 0
|
| 395 |
+
)
|
| 396 |
+
if suspended > 0 and confidence < 0.70:
|
| 397 |
+
gx_mag = float(np.abs(sobel_x).mean())
|
| 398 |
+
gy_mag = float(np.abs(sobel_y).mean())
|
| 399 |
+
direction = 'down' if gy_mag >= gx_mag else 'right'
|
| 400 |
+
ans = np.zeros_like(grid)
|
| 401 |
+
if direction == 'down':
|
| 402 |
for c in range(W):
|
| 403 |
+
vals = grid[:, c][grid[:, c] > 0]
|
| 404 |
+
if len(vals): ans[H-len(vals):H, c] = vals
|
| 405 |
+
else:
|
| 406 |
+
for r in range(H):
|
| 407 |
+
vals = grid[r, :][grid[r, :] > 0]
|
| 408 |
+
if len(vals): ans[r, W-len(vals):W] = vals
|
| 409 |
+
conf = min(0.80, suspended / max(total_px, 1) * 2.5)
|
| 410 |
+
if conf > confidence:
|
| 411 |
+
answer, confidence, signal = ans, conf, 'Im:Sobel→gravity'
|
| 412 |
+
reasoning.append(
|
| 413 |
+
f"Im ch52 Sobel-y={gy_mag:.3f} ch51 Sobel-x={gx_mag:.3f} | "
|
| 414 |
+
f"Re suspended={suspended}px | i·: arg(M) gives gravity {direction}")
|
| 415 |
+
|
| 416 |
+
# ── Im signal 6: Rotational symmetry / curl ────────────────────────────────
|
| 417 |
+
# Im ch50 rot_sym + ch54 curl → rotation answer
|
| 418 |
+
rot_score = float(rot_map.max())
|
| 419 |
+
curl_max = float(np.abs(curl_map).max())
|
| 420 |
+
if rot_score > 0.6 and curl_max > 0.4 and confidence < 0.35:
|
| 421 |
+
ans = np.rot90(grid)
|
| 422 |
+
conf = rot_score * curl_max * 0.60
|
| 423 |
+
if conf > confidence:
|
| 424 |
+
answer, confidence, signal = ans, conf, 'Im:rot+curl'
|
| 425 |
+
reasoning.append(
|
| 426 |
+
f"Im ch50 rot={rot_score:.2f} ch54 curl={curl_max:.2f} | "
|
| 427 |
+
f"i·: rotation indicated")
|
| 428 |
+
|
| 429 |
+
# ── Im signal 7: Color remapping (Re→Im count ordering) ───────────────────
|
| 430 |
+
# Im component map (ch55) encodes which regions are distinct objects.
|
| 431 |
+
# If Re colors appear in counts that suggest an ordering → shift colors.
|
| 432 |
+
if confidence < 0.30 and total_px > 0:
|
| 433 |
+
colors = [c for c in range(1,10) if (grid==c).any()]
|
| 434 |
+
if colors and max(colors) < 9:
|
| 435 |
+
ans = grid.copy()
|
| 436 |
+
mask = grid > 0
|
| 437 |
+
ans[mask] = ((grid[mask] - 1 + 1) % 9) + 1
|
| 438 |
+
if conf > confidence:
|
| 439 |
+
answer, confidence, signal = ans, 0.30, 'Im:color_shift'
|
| 440 |
+
reasoning.append(
|
| 441 |
+
f"Im ch55 component topology | Re colors {colors} | "
|
| 442 |
+
f"i·: Re→Im shift = increment colors")
|
| 443 |
+
|
| 444 |
+
return answer, confidence, reasoning, signal
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
# ── Re-side: exact cell targeting ────────────────────────────────────────────
|
| 448 |
+
|
| 449 |
+
def pixel_diff(current: np.ndarray, target: np.ndarray):
|
| 450 |
+
"""All differing cells: [(r, c, target_color)]"""
|
| 451 |
+
if current.shape != target.shape: return []
|
| 452 |
+
return [(int(r), int(c), int(target[r,c]))
|
| 453 |
+
for r in range(current.shape[0])
|
| 454 |
+
for c in range(current.shape[1])
|
| 455 |
+
if current[r,c] != target[r,c]]
|
| 456 |
+
|
| 457 |
+
def most_urgent_diff(current: np.ndarray, target: np.ndarray):
|
| 458 |
+
"""
|
| 459 |
+
Im → Re: pick the most important cell using the Cauchy principle.
|
| 460 |
+
The boundary contour determines the interior, so fix boundary cells first.
|
| 461 |
+
This is ∮ doing its job: read global Im data, recover local Re data.
|
| 462 |
+
"""
|
| 463 |
+
diffs = pixel_diff(current, target)
|
| 464 |
+
if not diffs: return None
|
| 465 |
+
bound = _boundary_raw(current)
|
| 466 |
+
boundary_diffs = [(r,c,v) for r,c,v in diffs if bound[r,c] > 0]
|
| 467 |
+
pool = boundary_diffs if boundary_diffs else diffs
|
| 468 |
+
return pool[np.random.randint(len(pool))]
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ── Main entry point ─────────────────────────────────────────────────────────
|
| 472 |
+
|
| 473 |
+
def try_analytic_action(
|
| 474 |
+
frame_2d: np.ndarray,
|
| 475 |
+
available_actions,
|
| 476 |
+
features: Optional[np.ndarray] = None,
|
| 477 |
+
) -> Tuple[Optional[int], Optional[dict], str, float]:
|
| 478 |
+
"""
|
| 479 |
+
Read the board's Im channels to derive the answer, then use Re-side
|
| 480 |
+
pixel diff to find the exact cell to click.
|
| 481 |
+
|
| 482 |
+
Parameters
|
| 483 |
+
----------
|
| 484 |
+
frame_2d : raw 2D color grid
|
| 485 |
+
available_actions : list of available GameAction values
|
| 486 |
+
features : optional pre-computed (56,H,W) feature array
|
| 487 |
+
(pass this from MyAgent to avoid recomputing)
|
| 488 |
+
|
| 489 |
+
Returns (action_id, action_data, signal_name, confidence)
|
| 490 |
+
"""
|
| 491 |
+
if frame_2d is None: return None, None, 'none', 0.0
|
| 492 |
+
|
| 493 |
+
avail_ids = set(
|
| 494 |
+
int(a.value if hasattr(a,'value') else a)
|
| 495 |
+
for a in (available_actions or range(1,7))
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
answer, confidence, reasoning, signal = read_board(frame_2d, features)
|
| 499 |
+
|
| 500 |
+
if answer is None or confidence < CONFIDENCE_THRESHOLD:
|
| 501 |
+
return None, None, signal, confidence
|
| 502 |
+
|
| 503 |
+
diffs = pixel_diff(frame_2d, answer)
|
| 504 |
+
if not diffs:
|
| 505 |
+
return None, None, 'already_matches', confidence
|
| 506 |
+
|
| 507 |
+
# ACTION6: click the most urgent Re-side cell
|
| 508 |
+
if 6 in avail_ids:
|
| 509 |
+
cell = most_urgent_diff(frame_2d, answer)
|
| 510 |
+
if cell is not None:
|
| 511 |
+
r, c, _ = cell
|
| 512 |
+
H, W = frame_2d.shape
|
| 513 |
+
game_y = min(63, max(0, int(r * 64/H + 32/H)))
|
| 514 |
+
game_x = min(63, max(0, int(c * 64/W + 32/W)))
|
| 515 |
+
return 6, {'x': game_x, 'y': game_y}, signal, confidence
|
| 516 |
+
|
| 517 |
+
return None, None, 'no_action6', confidence
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
# ── Feature extractor ─────────────────────────────────────────────────────────
|
| 523 |
+
|
| 524 |
+
def extract_features(grid,num_colours=10):
|
| 525 |
+
H,W=grid.shape
|
| 526 |
+
oh=np.zeros((num_colours,H,W),np.float32)
|
| 527 |
+
for c in range(num_colours): oh[c]=(grid==c).astype(np.float32)
|
| 528 |
+
gx,gy=_sobel(grid.astype(np.float32)/9)
|
| 529 |
+
stacked=np.concatenate([oh,_sym(grid,'h')[np.newaxis],
|
| 530 |
+
_sym(grid,'v')[np.newaxis],
|
| 531 |
+
_boundary(grid)[np.newaxis],
|
| 532 |
+
np.sqrt(gx**2+gy**2)[np.newaxis].astype(np.float32)],axis=0)
|
| 533 |
+
t=torch.from_numpy(stacked).float().unsqueeze(0)
|
| 534 |
+
if H!=64 or W!=64:
|
| 535 |
+
t=TF.interpolate(t,size=(64,64),mode='bilinear',align_corners=False)
|
| 536 |
+
return t.squeeze(0)
|
| 537 |
+
|
| 538 |
+
# ── Gabor filter bank — s-plane cross terms ─────────────────────────────────
|
| 539 |
+
|
| 540 |
+
"""
|
| 541 |
+
gabor_channels.py
|
| 542 |
+
=================
|
| 543 |
+
Gabor filter bank for ARC-AGI-3 — the s-plane cross terms.
|
| 544 |
+
|
| 545 |
+
Mathematical position
|
| 546 |
+
---------------------
|
| 547 |
+
The existing 56-channel extractor covers the AXES of the s-plane:
|
| 548 |
+
Re axis (σ>0, ω=0): ch16-47 CC sizes, distance maps — Laplace side
|
| 549 |
+
Im axis (σ=0, ω>0): ch48-55 symmetry, Sobel, boundary — Fourier side
|
| 550 |
+
|
| 551 |
+
A Gabor filter lives at an INTERIOR point (σ>0, ω>0):
|
| 552 |
+
g(x,y) = exp(-sigma*r^2) · cos(ω·x_θ + φ)
|
| 553 |
+
______/ ___________/
|
| 554 |
+
Re/Laplace Im/Fourier
|
| 555 |
+
envelope carrier
|
| 556 |
+
|
| 557 |
+
This is exp(-st) evaluated at s = σ + iω, rotated by θ, phased by φ.
|
| 558 |
+
It measures: "is there oscillation at frequency ω in direction θ,
|
| 559 |
+
concentrated within decay radius 1/√σ?"
|
| 560 |
+
|
| 561 |
+
ARC relevance
|
| 562 |
+
-------------
|
| 563 |
+
The cross terms detect structures the axis channels miss:
|
| 564 |
+
- Repeating patterns with finite extent (tiling with boundary)
|
| 565 |
+
- Oriented edges at specific spatial scales
|
| 566 |
+
- Localized symmetry (symmetric patch inside asymmetric grid)
|
| 567 |
+
- Diagonal structure (axis channels are H/V only)
|
| 568 |
+
|
| 569 |
+
Channel layout (72 channels total)
|
| 570 |
+
-----------------------------------
|
| 571 |
+
3 σ values × 3 ω values × 4 orientations × 2 phases = 72
|
| 572 |
+
|
| 573 |
+
σ = 0.3 → tight decay, radius ~1.8px — local structure
|
| 574 |
+
σ = 1.0 → medium decay, radius ~1.0px — mid-scale
|
| 575 |
+
σ = 2.5 → broad decay, radius ~0.6px — global texture
|
| 576 |
+
|
| 577 |
+
ω = 0.5 → coarse frequency, period ~12px — large patterns
|
| 578 |
+
ω = 1.5 → medium frequency, period ~4px — medium patterns
|
| 579 |
+
ω = 3.0 → fine frequency, period ~2px — fine detail
|
| 580 |
+
|
| 581 |
+
θ = 0, π/4, π/2, 3π/4 — 4 orientations (H, diagonal, V, anti-diagonal)
|
| 582 |
+
|
| 583 |
+
φ = 0 → cosine (even symmetry, detects symmetric features)
|
| 584 |
+
φ = π/2 → sine (odd symmetry, detects antisymmetric/edge features)
|
| 585 |
+
"""
|
| 586 |
+
|
| 587 |
+
import numpy as np
|
| 588 |
+
import torch
|
| 589 |
+
import torch.nn.functional as F
|
| 590 |
+
from typing import List, Tuple
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
# ── Filter bank parameters ────────────────────────────────────────────────────
|
| 594 |
+
|
| 595 |
+
SIGMA_VALUES = [0.3, 1.0, 2.5] # Re/Laplace decay
|
| 596 |
+
OMEGA_VALUES = [0.5, 1.5, 3.0] # Im/Fourier frequency
|
| 597 |
+
THETA_VALUES = [0, np.pi/4, np.pi/2, 3*np.pi/4] # orientations
|
| 598 |
+
PHASE_VALUES = [0.0, np.pi/2] # cosine, sine
|
| 599 |
+
|
| 600 |
+
N_GABOR_CHANNELS = (len(SIGMA_VALUES) * len(OMEGA_VALUES) *
|
| 601 |
+
len(THETA_VALUES) * len(PHASE_VALUES)) # = 72
|
| 602 |
+
|
| 603 |
+
KERNEL_SIZE = 7 # 7×7 kernels — enough for 64×64 grid
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
def _make_gabor_kernel(sigma: float, omega: float,
|
| 607 |
+
theta: float, phi: float,
|
| 608 |
+
size: int = KERNEL_SIZE) -> np.ndarray:
|
| 609 |
+
"""
|
| 610 |
+
2D Gabor kernel at one point in the s-plane.
|
| 611 |
+
|
| 612 |
+
s = σ + iω (the seam: Re side = decay, Im side = oscillation)
|
| 613 |
+
θ = orientation, φ = phase
|
| 614 |
+
"""
|
| 615 |
+
half = size // 2
|
| 616 |
+
y, x = np.mgrid[-half:half+1, -half:half+1].astype(np.float32)
|
| 617 |
+
|
| 618 |
+
# Rotate to orientation θ
|
| 619 |
+
x_rot = x * np.cos(theta) + y * np.sin(theta)
|
| 620 |
+
y_rot = -x * np.sin(theta) + y * np.cos(theta)
|
| 621 |
+
|
| 622 |
+
# Re side: Gaussian envelope — exp(-sigma*r^2)
|
| 623 |
+
envelope = np.exp(-sigma * (x_rot**2 + y_rot**2))
|
| 624 |
+
|
| 625 |
+
# Im side: sinusoidal carrier — cos(ω·x_θ + φ)
|
| 626 |
+
carrier = np.cos(omega * x_rot + phi)
|
| 627 |
+
|
| 628 |
+
# s-plane cross term: exp(-sigma*r^2) · cos(ω·x_θ + φ)
|
| 629 |
+
kernel = envelope * carrier
|
| 630 |
+
|
| 631 |
+
# Zero-mean (remove DC) — ensures kernel responds to structure, not brightness
|
| 632 |
+
kernel -= kernel.mean()
|
| 633 |
+
norm = np.sqrt((kernel ** 2).sum())
|
| 634 |
+
if norm > 0:
|
| 635 |
+
kernel /= norm
|
| 636 |
+
|
| 637 |
+
return kernel # shape (size, size)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def build_gabor_bank() -> torch.Tensor:
|
| 641 |
+
"""
|
| 642 |
+
Build the full Gabor filter bank as a (72, 1, K, K) tensor
|
| 643 |
+
ready for torch.nn.functional.conv2d.
|
| 644 |
+
"""
|
| 645 |
+
kernels = []
|
| 646 |
+
for sigma in SIGMA_VALUES:
|
| 647 |
+
for omega in OMEGA_VALUES:
|
| 648 |
+
for theta in THETA_VALUES:
|
| 649 |
+
for phi in PHASE_VALUES:
|
| 650 |
+
k = _make_gabor_kernel(sigma, omega, theta, phi)
|
| 651 |
+
kernels.append(k)
|
| 652 |
+
bank = np.stack(kernels, axis=0) # (72, K, K)
|
| 653 |
+
return torch.from_numpy(bank).float().unsqueeze(1) # (72, 1, K, K)
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
# Pre-built bank — computed once at import time
|
| 657 |
+
_GABOR_BANK: torch.Tensor = build_gabor_bank()
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def extract_gabor_features(grid_2d: np.ndarray,
|
| 661 |
+
grid_size: int = 64) -> torch.Tensor:
|
| 662 |
+
"""
|
| 663 |
+
Apply the Gabor bank to a 2D color grid.
|
| 664 |
+
|
| 665 |
+
Parameters
|
| 666 |
+
----------
|
| 667 |
+
grid_2d : np.ndarray (H, W) int — raw color grid
|
| 668 |
+
grid_size : int — target output size (default 64, matching ActionModel)
|
| 669 |
+
|
| 670 |
+
Returns
|
| 671 |
+
-------
|
| 672 |
+
torch.Tensor (72, grid_size, grid_size) float32
|
| 673 |
+
"""
|
| 674 |
+
H, W = grid_2d.shape
|
| 675 |
+
|
| 676 |
+
# Normalize grid to [0, 1] float
|
| 677 |
+
grid_f = torch.from_numpy(
|
| 678 |
+
grid_2d.astype(np.float32) / 9.0
|
| 679 |
+
).unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
|
| 680 |
+
|
| 681 |
+
# Resize to grid_size if needed
|
| 682 |
+
if H != grid_size or W != grid_size:
|
| 683 |
+
grid_f = F.interpolate(grid_f, size=(grid_size, grid_size),
|
| 684 |
+
mode='bilinear', align_corners=False)
|
| 685 |
+
|
| 686 |
+
# Apply all 72 Gabor filters simultaneously
|
| 687 |
+
pad = KERNEL_SIZE // 2
|
| 688 |
+
responses = F.conv2d(grid_f, _GABOR_BANK, padding=pad) # (1, 72, H, W)
|
| 689 |
+
responses = responses.squeeze(0) # (72, grid_size, grid_size)
|
| 690 |
+
|
| 691 |
+
# Normalize responses to [-1, 1]
|
| 692 |
+
max_val = responses.abs().max()
|
| 693 |
+
if max_val > 0:
|
| 694 |
+
responses = responses / max_val
|
| 695 |
+
|
| 696 |
+
return responses
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
def channel_descriptions() -> List[str]:
|
| 700 |
+
"""Human-readable description of each Gabor channel."""
|
| 701 |
+
descs = []
|
| 702 |
+
ch = 0
|
| 703 |
+
for sigma in SIGMA_VALUES:
|
| 704 |
+
for omega in OMEGA_VALUES:
|
| 705 |
+
for theta in THETA_VALUES:
|
| 706 |
+
theta_deg = int(theta * 180 / np.pi)
|
| 707 |
+
for phi in PHASE_VALUES:
|
| 708 |
+
phase_name = 'cos' if phi == 0 else 'sin'
|
| 709 |
+
descs.append(
|
| 710 |
+
f"ch{56+ch:3d}: Gabor σ={sigma:.1f} ω={omega:.1f} "
|
| 711 |
+
f"θ={theta_deg}° φ={phase_name} "
|
| 712 |
+
f"[s={sigma:.1f}+{omega:.1f}i]"
|
| 713 |
+
)
|
| 714 |
+
ch += 1
|
| 715 |
+
return descs
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
# ── S-plane visualization ─────────────────────────────────────────────────────
|
| 719 |
+
|
| 720 |
+
def splane_coverage_report():
|
| 721 |
+
"""Print the s-plane coverage table."""
|
| 722 |
+
print("s-plane coverage: σ (Re/Laplace) × ω (Im/Fourier)")
|
| 723 |
+
print("="*55)
|
| 724 |
+
print(f"{'':8}", end="")
|
| 725 |
+
for o in OMEGA_VALUES:
|
| 726 |
+
print(f" ω={o:.1f}", end="")
|
| 727 |
+
print()
|
| 728 |
+
for s in SIGMA_VALUES:
|
| 729 |
+
print(f"σ={s:.1f} ", end="")
|
| 730 |
+
for o in OMEGA_VALUES:
|
| 731 |
+
n = len(THETA_VALUES) * len(PHASE_VALUES)
|
| 732 |
+
print(f" {n:3d}ch", end="")
|
| 733 |
+
print(f" (×{len(THETA_VALUES)}θ ×{len(PHASE_VALUES)}φ)")
|
| 734 |
+
print()
|
| 735 |
+
print(f"Total Gabor channels: {N_GABOR_CHANNELS}")
|
| 736 |
+
print(f"Existing axis channels: 56")
|
| 737 |
+
print(f"Combined total: {56 + N_GABOR_CHANNELS}")
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
# ── Rendering ─────────────────────────────────────────────────────────────────
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def _pil(fig):
|
| 746 |
+
buf=io.BytesIO()
|
| 747 |
+
fig.savefig(buf,format='png',dpi=80,bbox_inches='tight',
|
| 748 |
+
facecolor=fig.get_facecolor())
|
| 749 |
+
buf.seek(0); img=Image.open(buf).copy(); plt.close(fig)
|
| 750 |
+
return img
|
| 751 |
+
|
| 752 |
+
def render_grid(grid,title='',highlight=None,mark_cell=None):
|
| 753 |
+
if grid is None: return None
|
| 754 |
+
H,W=grid.shape; cell=max(28,min(56,360//max(H,W)))
|
| 755 |
+
fig,ax=plt.subplots(figsize=((W*cell+4)/72,(H*cell+22)/72),dpi=72)
|
| 756 |
+
fig.patch.set_facecolor('#1e1e2e'); ax.set_facecolor('#1e1e2e')
|
| 757 |
+
ax.imshow(grid,cmap=ARC_CMAP,vmin=0,vmax=9,interpolation='nearest',aspect='equal')
|
| 758 |
+
for x in range(W+1): ax.axvline(x-.5,color='#444',lw=.5)
|
| 759 |
+
for y in range(H+1): ax.axhline(y-.5,color='#444',lw=.5)
|
| 760 |
for r in range(H):
|
| 761 |
for c in range(W):
|
| 762 |
+
v=int(grid[r,c])
|
| 763 |
+
col='white' if v in [0,1,2,3,5,6,9] else 'black'
|
| 764 |
+
ax.text(c,r,str(v),ha='center',va='center',
|
| 765 |
+
fontsize=max(7,cell//5),color=col,
|
| 766 |
+
fontweight='bold',fontfamily='monospace')
|
| 767 |
+
if highlight is not None:
|
| 768 |
+
for r,c,_ in highlight:
|
| 769 |
+
ax.add_patch(plt.Rectangle((c-.5,r-.5),1,1,
|
| 770 |
+
fill=True,facecolor='#ff4444',alpha=0.35,lw=0))
|
| 771 |
+
if mark_cell is not None:
|
| 772 |
+
r,c,_=mark_cell
|
| 773 |
+
ax.add_patch(plt.Rectangle((c-.5,r-.5),1,1,
|
| 774 |
+
fill=False,edgecolor='#00ffff',lw=2.5))
|
| 775 |
+
ax.plot(c,r,'*',color='#00ffff',markersize=max(8,cell//4))
|
| 776 |
+
ax.set_xlim(-.5,W-.5); ax.set_ylim(H-.5,-.5); ax.axis('off')
|
| 777 |
+
if title: ax.set_title(title,color='#cdd6f4',fontsize=9,pad=4)
|
| 778 |
+
plt.tight_layout(pad=.3)
|
| 779 |
+
return _pil(fig)
|
| 780 |
+
|
| 781 |
+
def render_hypothesis_panel(candidates):
|
| 782 |
+
"""Im side: bar chart of top hypotheses with confidence."""
|
| 783 |
+
if not candidates: return None
|
| 784 |
+
top=candidates[:6]
|
| 785 |
+
names=[c[0] for c in top]; confs=[c[2] for c in top]
|
| 786 |
+
fig,ax=plt.subplots(figsize=(5,2.2))
|
| 787 |
+
fig.patch.set_facecolor('#1e1e2e'); ax.set_facecolor('#1e1e2e')
|
| 788 |
+
colors=['#ffd700' if i==0 else '#4a9eff' for i in range(len(top))]
|
| 789 |
+
bars=ax.barh(names[::-1],confs[::-1],color=colors[::-1],height=0.6)
|
| 790 |
+
for bar,conf in zip(bars,confs[::-1]):
|
| 791 |
+
ax.text(bar.get_width()+.01,bar.get_y()+bar.get_height()/2,
|
| 792 |
+
f'{conf:.2f}',va='center',color='white',fontsize=8)
|
| 793 |
+
ax.set_xlim(0,1.15); ax.axvline(0.4,color='#ff6666',lw=1,ls='--',alpha=0.7)
|
| 794 |
+
ax.text(0.41,0,'threshold',color='#ff6666',fontsize=7,va='bottom')
|
| 795 |
+
ax.tick_params(colors='#888',labelsize=8); ax.spines[:].set_visible(False)
|
| 796 |
+
ax.set_title('Im side — hypothesis ranking 🟡=selected',
|
| 797 |
+
color='#cdd6f4',fontsize=9,pad=3)
|
| 798 |
+
plt.tight_layout(pad=.4)
|
| 799 |
+
return _pil(fig)
|
| 800 |
+
|
| 801 |
+
def render_action_bar(action_counts,total):
|
| 802 |
+
if not action_counts or total==0: return None
|
| 803 |
+
labels=[f"A{k}" for k in sorted(action_counts)]
|
| 804 |
+
vals =[action_counts[k] for k in sorted(action_counts)]
|
| 805 |
+
pcts =[v/total*100 for v in vals]
|
| 806 |
+
fig,ax=plt.subplots(figsize=(4,1.6))
|
| 807 |
+
fig.patch.set_facecolor('#1e1e2e'); ax.set_facecolor('#1e1e2e')
|
| 808 |
+
colors=['#4a9eff','#e05050','#50c050','#f5c400','#c060c0','#d07030']
|
| 809 |
+
bars=ax.barh(labels,pcts,color=colors[:len(labels)],height=0.6)
|
| 810 |
+
for bar,v,p in zip(bars,vals,pcts):
|
| 811 |
+
ax.text(min(p+1,98),bar.get_y()+bar.get_height()/2,
|
| 812 |
+
f'{v}',va='center',color='white',fontsize=8)
|
| 813 |
+
ax.set_xlim(0,110); ax.tick_params(colors='#888',labelsize=8)
|
| 814 |
+
ax.spines[:].set_visible(False)
|
| 815 |
+
ax.set_title('Action frequency',color='#cdd6f4',fontsize=9,pad=3)
|
| 816 |
+
plt.tight_layout(pad=.4)
|
| 817 |
+
return _pil(fig)
|
| 818 |
+
|
| 819 |
+
def render_reward_chart(reward_history):
|
| 820 |
+
if len(reward_history)<2: return None
|
| 821 |
+
fig,ax=plt.subplots(figsize=(5,1.6))
|
| 822 |
+
fig.patch.set_facecolor('#1e1e2e'); ax.set_facecolor('#1e1e2e')
|
| 823 |
+
for i,r in enumerate(reward_history):
|
| 824 |
+
col='#ffd700' if r>=5 else ('#50c050' if r>0 else '#e05050')
|
| 825 |
+
ax.bar(i,r,color=col,width=1,alpha=0.8)
|
| 826 |
+
ax.axhline(0,color='#555',lw=0.5)
|
| 827 |
+
ax.set_xlim(0,len(reward_history))
|
| 828 |
+
ax.tick_params(colors='#888',labelsize=7); ax.spines[:].set_visible(False)
|
| 829 |
+
ax.set_title('Reward 🟡=level-up 🟢=change 🔴=dead',
|
| 830 |
+
color='#cdd6f4',fontsize=8,pad=3)
|
| 831 |
+
plt.tight_layout(pad=.3)
|
| 832 |
+
return _pil(fig)
|
| 833 |
+
|
| 834 |
+
def render_gabor_panel(grid):
|
| 835 |
+
"""
|
| 836 |
+
Visualize the top-responding Gabor channels — the s-plane cross terms.
|
| 837 |
+
Shows which (σ, ω, θ) combination is most active on the current frame.
|
| 838 |
+
"""
|
| 839 |
+
if grid is None: return None
|
| 840 |
+
feats = extract_gabor_features(grid) # (72, 64, 64)
|
| 841 |
+
max_per_ch = feats.abs().amax(dim=(1,2)).numpy()
|
| 842 |
+
top4_idx = max_per_ch.argsort()[-4:][::-1]
|
| 843 |
+
|
| 844 |
+
fig, axes = plt.subplots(1, 4, figsize=(10, 2.2))
|
| 845 |
+
fig.patch.set_facecolor('#1e1e2e')
|
| 846 |
+
|
| 847 |
+
# Build channel labels
|
| 848 |
+
labels = []
|
| 849 |
+
for s in SIGMA_VALUES:
|
| 850 |
+
for o in OMEGA_VALUES:
|
| 851 |
+
for t in THETA_VALUES:
|
| 852 |
+
for p in PHASE_VALUES:
|
| 853 |
+
td = int(t*180/np.pi)
|
| 854 |
+
pn = 'cos' if p==0 else 'sin'
|
| 855 |
+
labels.append(f's={s:.1f}+{o:.1f}i th={td} {pn}')
|
| 856 |
|
| 857 |
+
for ax, idx in zip(axes, top4_idx):
|
| 858 |
+
ax.set_facecolor('#0d0d1a')
|
| 859 |
+
ch_map = feats[idx].numpy()
|
| 860 |
+
im = ax.imshow(ch_map, cmap='RdBu', vmin=-1, vmax=1,
|
| 861 |
+
interpolation='nearest', aspect='equal')
|
| 862 |
+
ax.set_title(labels[idx], color='#cdd6f4', fontsize=7, pad=2)
|
| 863 |
+
ax.axis('off')
|
| 864 |
+
plt.colorbar(im, ax=ax, fraction=.06, pad=.02)
|
| 865 |
|
| 866 |
+
fig.suptitle('Gabor s-plane responses (top 4 cross-term channels)',
|
| 867 |
+
color='#cdd6f4', fontsize=9, y=1.02)
|
| 868 |
+
plt.tight_layout(pad=0.5)
|
| 869 |
+
return _pil(fig)
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
# ── TinyAgent with Re/Im solver ───────────────────────────────────────────────
|
| 873 |
+
|
| 874 |
+
CONF_THRESHOLD = 0.72 # high bar — only act analytically when very sure
|
| 875 |
+
|
| 876 |
+
class TinyAgent:
|
| 877 |
def __init__(self):
|
| 878 |
+
self.device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 879 |
+
self.model=self._make_model().to(self.device)
|
| 880 |
+
self.opt=torch.optim.Adam(self.model.parameters(),lr=1e-4)
|
| 881 |
+
self.buf=[]; self.prev_feat=None; self.prev_action=None
|
| 882 |
+
self.step_count=0; self.action_counts={}; self.prev_levels=0
|
| 883 |
+
self.reward_history=deque(maxlen=300)
|
| 884 |
+
self.level_history=[]; self.prev_state=None
|
| 885 |
+
self.level_up_reward=10.0; self.win_reward=50.0
|
| 886 |
+
self.near_win_reward=2.0; self.change_reward=0.1
|
| 887 |
+
self.dead_penalty=-0.01; self.candidate_win_reward=30.0
|
| 888 |
+
self.prev_candidate_dist=1.0
|
| 889 |
+
self.explore_steps=20
|
| 890 |
+
self.click_attempts=0; self.click_successes=0
|
| 891 |
+
self._last_was_click=False
|
| 892 |
+
|
| 893 |
def _make_model(self):
|
| 894 |
return nn.Sequential(
|
| 895 |
+
nn.Conv2d(14+N_GABOR_CHANNELS,32,3,padding=1),nn.ReLU(),
|
| 896 |
+
nn.Conv2d(32,64,3,padding=1),nn.ReLU(),
|
| 897 |
+
nn.Conv2d(64,128,3,padding=1),nn.ReLU(),
|
| 898 |
+
nn.AdaptiveAvgPool2d(8),nn.Flatten(),
|
| 899 |
+
nn.Linear(128*8*8,256),nn.ReLU(),
|
| 900 |
+
nn.Linear(256,6),
|
| 901 |
)
|
| 902 |
+
|
| 903 |
def reset(self):
|
| 904 |
+
self.model=self._make_model().to(self.device)
|
| 905 |
+
self.opt=torch.optim.Adam(self.model.parameters(),lr=1e-4)
|
| 906 |
+
self.buf=[]; self.prev_feat=None; self.prev_action=None
|
| 907 |
+
self.step_count=0; self.action_counts={}; self.prev_levels=0
|
| 908 |
+
self.reward_history=deque(maxlen=300); self.level_history=[]
|
| 909 |
+
self.prev_state=None; self.prev_candidate_dist=1.0
|
| 910 |
+
self.explore_steps=20; self.click_attempts=0; self.click_successes=0
|
| 911 |
+
|
| 912 |
+
def choose(self,grid,available_actions=None,levels=0,state=None):
|
| 913 |
+
feat=extract_features(grid).to(self.device)
|
| 914 |
+
cur_state=str(state) if state else None
|
| 915 |
+
|
| 916 |
+
# ── Re/Im: read the board using the 56-channel feature tensor ──────
|
| 917 |
+
# Pass feat directly so read_board reads Im channels from the same
|
| 918 |
+
# tensor the CNN uses — Im tensor=signal, raw grid=coordinates
|
| 919 |
+
# Concatenate axis + Gabor cross-term channels
|
| 920 |
+
gabor_feat = extract_gabor_features(grid, grid_size=64)
|
| 921 |
+
full_feat = torch.cat([feat, gabor_feat.to(self.device)], dim=0)
|
| 922 |
+
feat_np = full_feat.cpu().numpy()
|
| 923 |
+
cand_answer,cand_conf,cand_reasoning,cand_signal=read_board(grid, feat_np)
|
| 924 |
+
|
| 925 |
+
# Candidate proximity bonus
|
| 926 |
+
if cand_answer is not None and cand_conf>=0.35:
|
| 927 |
+
curr_dist=(grid!=cand_answer).mean() if grid.shape==cand_answer.shape else 1.0
|
| 928 |
+
if curr_dist==0.0:
|
| 929 |
+
cand_bonus=self.candidate_win_reward
|
| 930 |
+
elif curr_dist<self.prev_candidate_dist:
|
| 931 |
+
cand_bonus=(self.prev_candidate_dist-curr_dist)*5.0
|
| 932 |
+
else:
|
| 933 |
+
cand_bonus=0.0
|
| 934 |
+
self.prev_candidate_dist=curr_dist
|
| 935 |
+
else:
|
| 936 |
+
cand_bonus=0.0; cand_answer=None
|
| 937 |
+
|
| 938 |
+
# Store shaped experience
|
| 939 |
if self.prev_feat is not None:
|
| 940 |
+
changed=not np.array_equal(
|
| 941 |
+
self.prev_feat.cpu().numpy(),full_feat.cpu().numpy())
|
| 942 |
+
if self._last_was_click:
|
| 943 |
+
self.click_attempts+=1
|
| 944 |
+
if changed: self.click_successes+=1
|
| 945 |
+
just_won=(cur_state=='WIN' and self.prev_state!='WIN')
|
| 946 |
+
level_up=levels>self.prev_levels
|
| 947 |
+
if just_won:
|
| 948 |
+
reward=self.win_reward+cand_bonus
|
| 949 |
+
for i in range(min(5,len(self.buf))):
|
| 950 |
+
idx=len(self.buf)-1-i
|
| 951 |
+
self.buf[idx]=(self.buf[idx][0],self.buf[idx][1],
|
| 952 |
+
self.buf[idx][2]+self.near_win_reward*(1-i*0.15))
|
| 953 |
+
elif level_up:
|
| 954 |
+
reward=self.level_up_reward+cand_bonus
|
| 955 |
+
self.level_history.append((self.step_count,levels))
|
| 956 |
+
elif changed:
|
| 957 |
+
reward=self.change_reward+cand_bonus
|
| 958 |
+
else:
|
| 959 |
+
reward=self.dead_penalty+cand_bonus
|
| 960 |
self.reward_history.append(reward)
|
| 961 |
+
self.buf.append((self.prev_feat,self.prev_action,reward)) # prev_feat is full_feat
|
| 962 |
+
if len(self.buf)>500: self.buf.pop(0)
|
| 963 |
+
self.prev_state=cur_state
|
| 964 |
+
self.prev_levels=levels
|
| 965 |
+
|
| 966 |
+
if self.step_count%10==0 and len(self.buf)>=16:
|
| 967 |
self._train()
|
| 968 |
+
|
| 969 |
+
# ── Im → Re bridge: read board → derive answer → click exact cell ──
|
| 970 |
+
# Only fire after explore_steps of CNN exploration so we have context
|
| 971 |
+
analytic_action=None; analytic_meta={}
|
| 972 |
+
click_rate=(self.click_successes/self.click_attempts
|
| 973 |
+
if self.click_attempts>10 else 1.0)
|
| 974 |
+
if (cand_answer is not None
|
| 975 |
+
and cand_conf>=CONF_THRESHOLD
|
| 976 |
+
and self.step_count>self.explore_steps
|
| 977 |
+
and click_rate>=0.20):
|
| 978 |
+
diffs=pixel_diff(grid,cand_answer)
|
| 979 |
+
if diffs:
|
| 980 |
+
cell=most_urgent_diff(grid,cand_answer)
|
| 981 |
+
if cell is not None:
|
| 982 |
+
r,c,tgt_color=cell
|
| 983 |
+
H,W=grid.shape
|
| 984 |
+
gy=min(63,max(0,int(r*64/H+32/H)))
|
| 985 |
+
gx=min(63,max(0,int(c*64/W+32/W)))
|
| 986 |
+
analytic_action=6
|
| 987 |
+
reasoning_str=' | '.join(cand_reasoning[:2]) if cand_reasoning else 'read_board'
|
| 988 |
+
analytic_meta={'x':gx,'y':gy,'cell':(r,c,tgt_color),
|
| 989 |
+
'hypothesis':reasoning_str,'conf':cand_conf,
|
| 990 |
+
'n_diffs':len(diffs),
|
| 991 |
+
'candidates':[(reasoning_str,cand_answer,cand_conf)]}
|
| 992 |
+
|
| 993 |
+
# ── CNN fallback ──────────────────────────────────────────────────
|
| 994 |
+
with torch.no_grad():
|
| 995 |
+
logits=self.model(full_feat.unsqueeze(0)).squeeze(0)
|
| 996 |
+
avail=list(range(1,7))
|
| 997 |
+
if available_actions:
|
| 998 |
+
avail=[int(a.value if hasattr(a,'value') else a)
|
| 999 |
+
for a in available_actions if
|
| 1000 |
+
int(a.value if hasattr(a,'value') else a)<=6]
|
| 1001 |
+
indices=[m-1 for m in avail if 1<=m<=6]
|
| 1002 |
+
masked=torch.full((6,),float('-inf'))
|
| 1003 |
+
for i in indices: masked[i]=logits[i]
|
| 1004 |
+
probs=torch.softmax(masked,dim=0).cpu().numpy()
|
| 1005 |
+
probs=np.nan_to_num(probs,nan=0)
|
| 1006 |
+
if probs.sum()==0: probs[np.array(indices)]=1/len(indices)
|
| 1007 |
+
probs=probs/probs.sum()
|
| 1008 |
+
cnn_action_idx=np.random.choice(6,p=probs)
|
| 1009 |
+
|
| 1010 |
+
# Pick final action
|
| 1011 |
+
if analytic_action is not None:
|
| 1012 |
+
chosen_id=analytic_action
|
| 1013 |
+
meta=analytic_meta
|
| 1014 |
+
meta['source']='analytic'
|
| 1015 |
else:
|
| 1016 |
+
chosen_id=cnn_action_idx+1
|
| 1017 |
+
# Package read_board result for display even in CNN fallback
|
| 1018 |
+
cnn_cands=[(cand_signal, cand_answer, cand_conf)] if cand_answer is not None else []
|
| 1019 |
+
meta={'source':'cnn','probs':probs.tolist(),'candidates':cnn_cands,
|
| 1020 |
+
'hypothesis':cand_reasoning[0][:60] if cand_reasoning else 'none',
|
| 1021 |
+
'conf':cand_conf,'n_diffs':len(pixel_diff(grid,cand_answer)) if cand_answer is not None else 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
|
| 1023 |
+
self.prev_feat=full_feat; self.prev_action=cnn_action_idx
|
| 1024 |
+
self._last_was_click=(chosen_id==6)
|
| 1025 |
+
self.step_count+=1
|
| 1026 |
+
a_id=chosen_id
|
| 1027 |
+
self.action_counts[a_id]=self.action_counts.get(a_id,0)+1
|
| 1028 |
|
| 1029 |
+
try:
|
| 1030 |
+
from arcengine import GameAction
|
| 1031 |
+
action=GameAction(a_id)
|
| 1032 |
+
except Exception:
|
| 1033 |
+
action=a_id
|
|
|
|
|
|
|
|
|
|
| 1034 |
|
| 1035 |
+
if a_id==6 and 'x' in meta:
|
| 1036 |
+
try: action.set_data({'x':meta['x'],'y':meta['y']})
|
| 1037 |
+
except: pass
|
| 1038 |
+
|
| 1039 |
+
return action,meta
|
| 1040 |
+
|
| 1041 |
+
def _train(self):
|
| 1042 |
+
import random
|
| 1043 |
+
batch=random.sample(self.buf,min(16,len(self.buf)))
|
| 1044 |
+
states =torch.stack([b[0] for b in batch]).to(self.device)
|
| 1045 |
+
actions=torch.tensor([b[1] for b in batch],dtype=torch.long, device=self.device)
|
| 1046 |
+
rewards=torch.tensor([b[2] for b in batch],dtype=torch.float32,device=self.device)
|
| 1047 |
+
self.opt.zero_grad()
|
| 1048 |
+
logits=self.model(states)
|
| 1049 |
+
loss=TF.binary_cross_entropy_with_logits(
|
| 1050 |
+
logits.gather(1,actions.unsqueeze(1)).squeeze(1),
|
| 1051 |
+
torch.clamp(rewards,0,1))
|
| 1052 |
+
loss.backward(); self.opt.step()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
|
| 1054 |
# ── Session ───────────────────────────────────────────────────────────────────
|
| 1055 |
|
| 1056 |
+
_agent = TinyAgent()
|
| 1057 |
+
_stop_flag = threading.Event()
|
| 1058 |
_run_thread = None
|
| 1059 |
+
_frame_queue= queue.Queue(maxsize=60)
|
| 1060 |
|
| 1061 |
+
def _run_agent(game_id,api_key,max_steps):
|
| 1062 |
+
import arc_agi
|
| 1063 |
try:
|
| 1064 |
+
arc=arc_agi.Arcade(arc_api_key=api_key)
|
| 1065 |
+
env=arc.make(game_id,include_frame_data=True)
|
| 1066 |
+
frame=env.reset(); _agent.reset()
|
| 1067 |
+
prev_grid=None; step=0
|
| 1068 |
+
while not _stop_flag.is_set() and step<max_steps:
|
| 1069 |
+
if frame is None: break
|
| 1070 |
+
raw=np.array(frame.frame,dtype=np.int64)
|
| 1071 |
+
grid=raw[-1] if raw.ndim==3 else raw
|
| 1072 |
+
avail=getattr(frame,'available_actions',None)
|
| 1073 |
+
levels=getattr(frame,'levels_completed',0)
|
| 1074 |
+
state=getattr(frame,'state',None)
|
| 1075 |
+
action,meta=_agent.choose(grid,avail,levels=levels,state=state)
|
| 1076 |
+
diff=(grid!=prev_grid) if prev_grid is not None else None
|
| 1077 |
+
prev_grid=grid.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
_frame_queue.put({
|
| 1079 |
+
'grid':grid,'diff':diff,'step':step,
|
| 1080 |
+
'action':int(action.value if hasattr(action,'value') else action),
|
| 1081 |
+
'levels':levels,'state':str(state),
|
| 1082 |
+
'meta':meta,
|
| 1083 |
+
'counts':dict(_agent.action_counts),'click_rate':round(_agent.click_successes/_agent.click_attempts,2) if _agent.click_attempts>0 else None,
|
| 1084 |
+
'reward_history':list(_agent.reward_history),
|
| 1085 |
+
'grid_raw':grid.tolist(),
|
| 1086 |
+
'level_history':list(_agent.level_history),
|
| 1087 |
+
},block=True,timeout=5)
|
| 1088 |
+
state_str=str(state)
|
| 1089 |
+
if 'WIN' in state_str or 'GAME_OVER' in state_str: break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1090 |
try:
|
| 1091 |
from arcengine import GameAction as GA
|
| 1092 |
+
a_int=int(action.value if hasattr(action,'value') else action)
|
| 1093 |
+
sa=GA(a_int)
|
| 1094 |
if meta.get('x') is not None:
|
| 1095 |
+
try: sa.set_data({'x':int(meta['x']),'y':int(meta['y'])})
|
| 1096 |
+
except: pass
|
| 1097 |
+
frame=env.step(sa)
|
| 1098 |
+
except Exception as step_err:
|
| 1099 |
+
# Last resort: try passing action directly
|
| 1100 |
+
try: frame=env.step(action)
|
| 1101 |
+
except: frame=None
|
| 1102 |
+
step+=1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
time.sleep(0.08)
|
| 1104 |
+
_frame_queue.put({'done':True,'step':step,
|
| 1105 |
+
'level_history':list(_agent.level_history)})
|
| 1106 |
except Exception as e:
|
| 1107 |
+
_frame_queue.put({'error':str(e)})
|
| 1108 |
+
|
| 1109 |
+
# ── Pull frame ────────────────────────────────────────────────────────────────
|
| 1110 |
|
| 1111 |
+
_latest={'grid_img':None,'hyp_img':None,'cand_img':None,
|
| 1112 |
+
'bar_img':None,'reward_img':None,'status':'*Waiting...*'}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
|
| 1114 |
def pull_frame():
|
| 1115 |
global _latest
|
| 1116 |
+
data=None
|
| 1117 |
while True:
|
| 1118 |
+
try: data=_frame_queue.get_nowait()
|
| 1119 |
+
except queue.Empty: break
|
| 1120 |
+
|
|
|
|
|
|
|
| 1121 |
if data is None:
|
| 1122 |
+
return (_latest['grid_img'],_latest['hyp_img'],_latest['cand_img'],
|
| 1123 |
+
_latest['bar_img'],_latest['gabor_img'],_latest['reward_img'],_latest['status'])
|
| 1124 |
+
|
| 1125 |
if 'error' in data:
|
| 1126 |
+
_latest['status']=f"**Error:** {data['error']}"
|
| 1127 |
+
return (_latest['grid_img'],_latest['hyp_img'],_latest['cand_img'],
|
| 1128 |
+
_latest['bar_img'],_latest['gabor_img'],_latest['reward_img'],_latest['status'])
|
| 1129 |
+
|
| 1130 |
if data.get('done'):
|
| 1131 |
+
lh=data.get('level_history',[])
|
| 1132 |
+
_latest['status']=f"**Done** — {data['step']} steps | {len(lh)} levels completed"
|
| 1133 |
+
return (_latest['grid_img'],_latest['hyp_img'],_latest['cand_img'],
|
| 1134 |
+
_latest['bar_img'],_latest['gabor_img'],_latest['reward_img'],_latest['status'])
|
| 1135 |
+
|
| 1136 |
+
grid=data['grid']; meta=data['meta']; step=data['step']
|
| 1137 |
+
levels=data['levels']; state=data['state']; action=data['action']
|
| 1138 |
+
source=meta.get('source','cnn')
|
| 1139 |
+
hyp_str=meta.get('hypothesis','none')
|
| 1140 |
+
cand_conf=meta.get('conf',0.0)
|
| 1141 |
+
n_diffs=meta.get('n_diffs',0)
|
| 1142 |
+
|
| 1143 |
+
# Build display candidates list: [(label, grid, conf)]
|
| 1144 |
+
raw_cands=meta.get('candidates',[])
|
| 1145 |
+
# raw_cands entries are (reasoning_str, grid, conf)
|
| 1146 |
+
# Normalise to (short_label, grid, conf)
|
| 1147 |
+
disp_cands=[]
|
| 1148 |
+
for entry in raw_cands:
|
| 1149 |
+
if len(entry)==3:
|
| 1150 |
+
label=str(entry[0])[:40]; cgrid=entry[1]; cconf=entry[2]
|
| 1151 |
+
if isinstance(cgrid, np.ndarray): disp_cands.append((label,cgrid,cconf))
|
| 1152 |
+
|
| 1153 |
+
# Determine what to highlight
|
| 1154 |
+
mark_cell=None; highlight=None
|
| 1155 |
+
if source=='analytic' and 'cell' in meta and disp_cands:
|
| 1156 |
+
_,cand_grid,_=disp_cands[0]
|
| 1157 |
+
if cand_grid.shape==grid.shape:
|
| 1158 |
+
all_diffs=pixel_diff(grid,cand_grid)
|
| 1159 |
+
highlight=all_diffs[:20]
|
| 1160 |
+
mark_cell=meta['cell']
|
| 1161 |
+
|
| 1162 |
+
source_emoji='🧠' if source=='analytic' else '🎲'
|
| 1163 |
+
_latest['grid_img']=render_grid(
|
| 1164 |
grid,
|
| 1165 |
+
title=f"Step {step} | {source_emoji} A{action} | Levels {levels}",
|
| 1166 |
highlight=highlight,
|
| 1167 |
+
mark_cell=mark_cell)
|
| 1168 |
+
|
| 1169 |
+
# Im side: hypothesis panel — show read_board reasoning as bar
|
| 1170 |
+
if disp_cands:
|
| 1171 |
+
# Convert to format render_hypothesis_panel expects: [(name,grid,conf)]
|
| 1172 |
+
_latest['hyp_img']=render_hypothesis_panel(disp_cands)
|
| 1173 |
+
else:
|
| 1174 |
+
_latest['hyp_img']=None
|
| 1175 |
+
|
| 1176 |
+
# Re side: candidate grid
|
| 1177 |
+
if disp_cands and disp_cands[0][1].shape==grid.shape:
|
| 1178 |
+
cname,cgrid,cconf2=disp_cands[0]
|
| 1179 |
+
diffs=pixel_diff(grid,cgrid)
|
| 1180 |
+
_latest['cand_img']=render_grid(
|
| 1181 |
+
cgrid,
|
| 1182 |
+
title=f"Im answer: {cname[:35]} (conf={cconf2:.2f}) — {len(diffs)} cells differ",
|
| 1183 |
+
highlight=diffs[:20])
|
| 1184 |
+
else:
|
| 1185 |
+
_latest['cand_img']=None
|
| 1186 |
+
|
| 1187 |
+
_latest['bar_img'] =render_action_bar(data['counts'],sum(data['counts'].values()))
|
| 1188 |
+
_latest['reward_img']=render_reward_chart(data['reward_history'])
|
| 1189 |
+
grid_raw=np.array(data.get('grid_raw',grid.tolist()),dtype=np.int64)
|
| 1190 |
+
_latest['gabor_img']=render_gabor_panel(grid_raw)
|
| 1191 |
+
|
| 1192 |
+
last_r=data['reward_history'][-1] if data['reward_history'] else 0
|
| 1193 |
+
r_emoji='🟡' if last_r>=5 else ('🟢' if last_r>0 else '🔴')
|
| 1194 |
+
hyp_str=(f"`{meta.get('hypothesis','?')}` conf={meta.get('conf',0):.2f} "
|
| 1195 |
+
f"→ click ({meta.get('x','?')},{meta.get('y','?')}) "
|
| 1196 |
+
f"[{meta.get('n_diffs','?')} cells wrong]"
|
| 1197 |
+
if source=='analytic'
|
| 1198 |
+
else f"CNN probs: {[round(p,2) for p in meta.get('probs',[])]}")
|
| 1199 |
+
|
| 1200 |
+
_latest['status']=(
|
| 1201 |
+
f"{source_emoji} **{'Analytic (Re/Im)' if source=='analytic' else 'CNN fallback'}**"
|
| 1202 |
+
f" | Step {step} | Levels {levels}"
|
| 1203 |
+
f" | Reward {r_emoji} `{last_r:.2f}` | {state}\n\n"
|
| 1204 |
+
f"{hyp_str}")
|
| 1205 |
+
|
| 1206 |
+
return (_latest['grid_img'],_latest['hyp_img'],_latest['cand_img'],
|
| 1207 |
+
_latest['bar_img'],_latest['reward_img'],_latest['status'])
|
| 1208 |
+
|
| 1209 |
+
# ── Handlers ──────────────────────────────────────────────────────────────────
|
| 1210 |
|
| 1211 |
def fetch_games(api_key):
|
|
|
|
| 1212 |
try:
|
| 1213 |
import arc_agi
|
| 1214 |
+
arc=arc_agi.Arcade(arc_api_key=api_key)
|
| 1215 |
+
envs=arc.get_environments()
|
| 1216 |
+
ids=[e.game_id for e in envs]
|
| 1217 |
+
return gr.Dropdown(choices=ids,value=ids[0] if ids else None),\
|
| 1218 |
+
f"Found **{len(ids)}** games."
|
| 1219 |
except Exception as e:
|
| 1220 |
+
return gr.Dropdown(choices=[]),f"**Error:** {e}"
|
| 1221 |
+
|
| 1222 |
+
def start_agent(game_id,api_key,max_steps):
|
| 1223 |
+
global _run_thread,_stop_flag
|
| 1224 |
+
if not game_id: return "Select a game first."
|
| 1225 |
+
if not api_key: return "Enter your API key."
|
|
|
|
|
|
|
| 1226 |
_stop_flag.set()
|
| 1227 |
+
if _run_thread and _run_thread.is_alive(): _run_thread.join(timeout=3)
|
|
|
|
| 1228 |
while not _frame_queue.empty():
|
| 1229 |
+
try: _frame_queue.get_nowait()
|
| 1230 |
+
except: break
|
|
|
|
|
|
|
| 1231 |
_stop_flag.clear()
|
| 1232 |
+
_run_thread=threading.Thread(
|
| 1233 |
+
target=_run_agent,args=(game_id,api_key,int(max_steps)),daemon=True)
|
| 1234 |
_run_thread.start()
|
| 1235 |
+
return f"Agent started on **{game_id}** — 🧠 Re/Im analytic + 🎲 CNN fallback"
|
| 1236 |
|
| 1237 |
def stop_agent():
|
| 1238 |
_stop_flag.set()
|
|
|
|
| 1240 |
|
| 1241 |
# ── UI ────────────────────────────────────────────────────────────────────────
|
| 1242 |
|
| 1243 |
+
with gr.Blocks(title="ARC-AGI-3 Re/Im Agent") as demo:
|
|
|
|
|
|
|
| 1244 |
|
| 1245 |
+
gr.Markdown("""
|
| 1246 |
+
# ARC-AGI-3 Re/Im Agent Spectator
|
| 1247 |
+
**Im side** = bird's eye hypothesis (which transformation?) |
|
| 1248 |
+
**Re side** = exact location (which cells to click?)
|
| 1249 |
|
| 1250 |
+
🧠 = analytic solver (Im picks hypothesis → Re pins cell → ACTION6 click)
|
| 1251 |
+
🎲 = CNN fallback (when no hypothesis clears the confidence threshold)
|
| 1252 |
""")
|
| 1253 |
+
|
| 1254 |
with gr.Row():
|
| 1255 |
with gr.Column(scale=3):
|
| 1256 |
+
api_box=gr.Textbox(label="ARC API key",type="password",
|
| 1257 |
+
value=os.environ.get("ARC_API_KEY",""),
|
| 1258 |
+
placeholder="arc-key-... or set ARC_API_KEY secret")
|
|
|
|
|
|
|
|
|
|
| 1259 |
with gr.Column(scale=1):
|
| 1260 |
+
fetch_btn=gr.Button("Fetch games")
|
| 1261 |
+
|
|
|
|
|
|
|
| 1262 |
with gr.Row():
|
| 1263 |
with gr.Column(scale=2):
|
| 1264 |
+
game_dd=gr.Dropdown(label="Game",choices=[])
|
| 1265 |
with gr.Column(scale=1):
|
| 1266 |
+
steps_sl=gr.Slider(label="Max steps",minimum=20,maximum=500,value=150,step=10)
|
| 1267 |
with gr.Column(scale=1):
|
| 1268 |
with gr.Row():
|
| 1269 |
+
start_btn=gr.Button("▶ Watch",variant="primary")
|
| 1270 |
+
stop_btn =gr.Button("■ Stop")
|
| 1271 |
+
|
| 1272 |
+
run_status=gr.Markdown("*Fetch games → select → Watch*")
|
| 1273 |
+
api_status=gr.Markdown()
|
| 1274 |
+
|
| 1275 |
gr.Markdown("---")
|
| 1276 |
+
|
| 1277 |
+
with gr.Row():
|
| 1278 |
+
grid_img=gr.Image(label="Current frame (🔴=wrong cells ⭐=target click)",
|
| 1279 |
+
type="pil",interactive=False,height=280)
|
| 1280 |
+
hyp_img =gr.Image(label="Im side — hypothesis ranking",
|
| 1281 |
+
type="pil",interactive=False,height=280)
|
| 1282 |
+
|
| 1283 |
with gr.Row():
|
| 1284 |
+
cand_img =gr.Image(label="Im candidate — what the answer should look like",
|
| 1285 |
+
type="pil",interactive=False,height=220)
|
| 1286 |
+
bar_img =gr.Image(label="Action frequency",
|
| 1287 |
+
type="pil",interactive=False,height=220)
|
| 1288 |
+
|
| 1289 |
+
with gr.Row():
|
| 1290 |
+
gabor_img =gr.Image(label="Gabor s-plane — cross terms (σ>0, ω>0)",
|
| 1291 |
+
type="pil",interactive=False,height=160)
|
| 1292 |
+
reward_img=gr.Image(label="Reward 🟡+50 WIN 🟡+10 level 🟢+0.1 change 🔴-0.01 dead",
|
| 1293 |
+
type="pil",interactive=False,height=160)
|
| 1294 |
+
|
| 1295 |
+
timer=gr.Timer(value=1.0)
|
| 1296 |
+
timer.tick(pull_frame,
|
| 1297 |
+
outputs=[grid_img,hyp_img,cand_img,bar_img,gabor_img,reward_img,run_status])
|
| 1298 |
+
|
| 1299 |
+
fetch_btn.click(fetch_games,inputs=api_box,outputs=[game_dd,api_status])
|
| 1300 |
+
start_btn.click(start_agent,inputs=[game_dd,api_box,steps_sl],outputs=run_status)
|
| 1301 |
+
stop_btn.click(stop_agent,outputs=run_status)
|
| 1302 |
+
|
| 1303 |
gr.Markdown("""
|
| 1304 |
---
|
| 1305 |
**Re/Im duality in action:**
|
| 1306 |
+
The Im side reads the whole board at once — symmetry maps, boundary contour, directional
|
| 1307 |
+
flow — and ranks candidate transformations by confidence.
|
| 1308 |
+
The Re side then diffs the current frame against the winning candidate and finds the exact
|
| 1309 |
+
cell (boundary-first, following Cauchy's principle) that most needs fixing.
|
| 1310 |
+
The agent emits ACTION6 at those precise coordinates instead of guessing randomly.
|
| 1311 |
+
CNN fires only when no analytic hypothesis clears 0.40 confidence.
|
| 1312 |
""")
|
| 1313 |
|
| 1314 |
if __name__ == "__main__":
|