arc-agi-2-solver / arc_solver.py
Interstellar007's picture
Upload arc_solver.py
bc62bf8 verified
Raw
History Blame Contribute Delete
19.4 kB
"""
ARC-AGI-2 Competition Solver
Dual-track approach: Program Synthesis (induction) + TTT (transduction)
Competition constraints:
- 4× NVIDIA L4 GPUs (24GB VRAM each)
- 12-hour wall-clock time for 240 tasks (~3 min/task avg)
- No internet access
- Pass@2 scoring (2 attempts per task, exact match)
Strategy:
Track A: SOAR-style program synthesis with Qwen-2.5-Coder-7B
Track B: TTT transduction with augmented inference + PoE
Ensemble: Combine both tracks with priority to verified solutions
"""
import os
import sys
import json
import time
import copy
import random
import traceback
from typing import List, Dict, Tuple, Optional, Any
from collections import defaultdict, Counter
import numpy as np
from arc_data import (
load_arc_dataset_from_hf, grids_equal,
D8_TRANSFORMS, augment_task, reverse_d8,
create_color_permutation, reverse_color_permutation,
grid_to_string, string_to_grid
)
from program_synthesis import (
ProgramSynthesisEngine, evaluate_program_on_task,
weighted_majority_vote, extract_python_code,
format_examples_for_prompt, get_error_feedback,
_execute_transform, ARCPrimitives
)
# ============================================================
# Configuration
# ============================================================
class Config:
# Model settings
PROGRAM_MODEL = "julien31/Soar-qwen-7b" # SOAR fine-tuned Qwen for program synthesis
TTT_MODEL = "nvidia/Mistral-NeMo-Minitron-8B-Base" # For transduction TTT
# Program synthesis settings
N_PROGRAM_SAMPLES = 100 # Programs to sample per task
N_PROGRAM_REFINEMENTS = 100 # Refinement budget per task
PROGRAM_TEMPERATURE = 0.8
PROGRAM_MAX_TOKENS = 1024
# TTT settings
TTT_LORA_RANK = 32
TTT_STEPS = 64
TTT_LR = 2e-4
TTT_MAX_EXAMPLES = 250
# Inference settings
N_AUGMENTATIONS = 16
DFS_THRESHOLD = 0.09 # 9% threshold for DFS
# Time budget
TOTAL_TIME_HOURS = 12
TIME_PER_TASK_SECONDS = 150 # ~2.5 min per task (conservative)
# Hardware
N_GPUS = 4
# ============================================================
# Ensemble Logic
# ============================================================
class ARC_Solver:
"""
Main solver that combines both tracks.
Track A (program synthesis) provides verified solutions.
Track B (TTT transduction) fills gaps.
"""
def __init__(self, config: Config = None):
self.config = config or Config()
self.program_engine = None
self.ttt_engine = None
self.results = {}
self.start_time = time.time()
def load_models(self):
"""Load models for both tracks."""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
print("Loading models...")
# Track A: Program synthesis model (SOAR-Qwen-7B)
print(f" Loading {self.config.PROGRAM_MODEL}...")
try:
prog_tokenizer = AutoTokenizer.from_pretrained(
self.config.PROGRAM_MODEL,
trust_remote_code=True
)
prog_model = AutoModelForCausalLM.from_pretrained(
self.config.PROGRAM_MODEL,
torch_dtype=torch.bfloat16,
device_map="auto",
trust_remote_code=True,
)
prog_model.eval()
self.program_engine = ProgramSynthesisEngine(
model=prog_model,
tokenizer=prog_tokenizer,
max_samples=self.config.N_PROGRAM_SAMPLES,
max_refinements=self.config.N_PROGRAM_REFINEMENTS,
temperature=self.config.PROGRAM_TEMPERATURE,
)
print(" ✓ Program synthesis model loaded")
except Exception as e:
print(f" ✗ Failed to load program model: {e}")
print("Models loaded.")
def time_remaining(self) -> float:
"""Get remaining time in seconds."""
elapsed = time.time() - self.start_time
return self.config.TOTAL_TIME_HOURS * 3600 - elapsed
def solve_with_programs(self, task: Dict, task_id: str,
time_budget: float = 120) -> List[List[List[int]]]:
"""
Track A: Solve using program synthesis.
Programs that pass ALL training examples are verified solutions.
"""
if self.program_engine is None:
return []
start = time.time()
# Sample programs
programs = self.program_engine.sample_programs(task)
# Check time
if time.time() - start > time_budget * 0.6:
pass # Skip refinement if running low on time
else:
# Refine
programs = self.program_engine.refine_programs(task, programs)
# Separate verified (100% accuracy) from partial
verified = [(c, a, o) for c, a, o in programs if a == 1.0 and o is not None]
partial = [(c, a, o) for c, a, o in programs if 0 < a < 1.0 and o is not None]
if verified:
# Use weighted vote among verified programs
preds = weighted_majority_vote(verified, top_k=2)
print(f" [PROG] Task {task_id}: {len(verified)} verified programs → {len(preds)} predictions")
return preds
if partial:
preds = weighted_majority_vote(partial, top_k=2)
print(f" [PROG] Task {task_id}: {len(partial)} partial programs → {len(preds)} predictions (unverified)")
return preds
print(f" [PROG] Task {task_id}: No valid programs found")
return []
def solve_with_augmented_programs(self, task: Dict, task_id: str,
time_budget: float = 120) -> List[List[List[int]]]:
"""
Enhanced program synthesis using D8 augmentations.
Try solving augmented versions of the task.
"""
if self.program_engine is None:
return []
all_programs = []
for t_name, _ in D8_TRANSFORMS[:4]: # Use 4 augmentations to save time
aug_task = augment_task(task, transform_name=t_name)
programs = self.program_engine.sample_programs(aug_task, n_samples=10)
# Map test outputs back to original space
for code, acc, test_out in programs:
if test_out is not None:
original_out = reverse_d8(test_out, t_name)
all_programs.append((code, acc, original_out))
# Vote
verified = [(c, a, o) for c, a, o in all_programs if a == 1.0 and o is not None]
if verified:
return weighted_majority_vote(verified, top_k=2)
partial = [(c, a, o) for c, a, o in all_programs if a > 0 and o is not None]
if partial:
return weighted_majority_vote(partial, top_k=2)
return []
def solve_task(self, task: Dict, task_id: str = "unknown") -> List[List[List[int]]]:
"""
Solve a single ARC task using the ensemble strategy.
Priority:
1. Verified program solutions (100% on training examples)
2. TTT transduction with voting
3. Partial program solutions (>50% on training examples)
4. Any available prediction
"""
time_budget = min(self.config.TIME_PER_TASK_SECONDS, self.time_remaining() - 60)
if time_budget <= 0:
print(f" Task {task_id}: OUT OF TIME")
return []
predictions = []
# Track A: Program synthesis
prog_preds = self.solve_with_programs(task, task_id, time_budget=time_budget * 0.7)
# If we have verified programs, use them directly
if prog_preds:
predictions = prog_preds
# If program synthesis didn't find verified solutions, try augmented approach
if not predictions and time_budget > 30:
aug_preds = self.solve_with_augmented_programs(task, task_id, time_budget=time_budget * 0.3)
if aug_preds:
predictions = aug_preds
# Ensure we have exactly 2 predictions
if len(predictions) == 0:
# Return empty grids as fallback
return []
elif len(predictions) == 1:
# Duplicate single prediction
predictions.append(predictions[0])
return predictions[:2]
def solve_all(self, tasks: List[Dict], task_ids: Optional[List[str]] = None) -> Dict:
"""
Solve all tasks. Returns dict of task_id -> [pred1, pred2].
"""
if task_ids is None:
task_ids = [str(i) for i in range(len(tasks))]
results = {}
solved = 0
for i, (task, task_id) in enumerate(zip(tasks, task_ids)):
elapsed = time.time() - self.start_time
remaining = self.config.TOTAL_TIME_HOURS * 3600 - elapsed
print(f"\n[{i+1}/{len(tasks)}] Task {task_id} "
f"(elapsed: {elapsed/3600:.2f}h, remaining: {remaining/3600:.2f}h)")
if remaining <= 60:
print(" TIME'S UP - stopping")
break
try:
preds = self.solve_task(task, task_id)
if preds:
results[task_id] = preds
solved += 1
else:
results[task_id] = []
except Exception as e:
print(f" ERROR: {e}")
traceback.print_exc()
results[task_id] = []
total_time = time.time() - self.start_time
print(f"\n{'='*60}")
print(f"RESULTS: {solved}/{len(tasks)} tasks produced predictions")
print(f"Total time: {total_time/3600:.2f}h")
print(f"{'='*60}")
return results
def evaluate(self, tasks: List[Dict], results: Dict,
task_ids: Optional[List[str]] = None) -> Dict:
"""Evaluate results against ground truth (for tasks with known outputs)."""
if task_ids is None:
task_ids = [str(i) for i in range(len(tasks))]
correct = 0
evaluated = 0
for task, task_id in zip(tasks, task_ids):
gt = task["test"][0].get("output")
if gt is None:
continue
evaluated += 1
preds = results.get(task_id, [])
for pred in preds:
if grids_equal(pred, gt):
correct += 1
break
accuracy = correct / evaluated if evaluated > 0 else 0
print(f"\nEvaluation:")
print(f" Correct: {correct}/{evaluated}")
print(f" Pass@2 Accuracy: {accuracy:.4f} ({accuracy*100:.2f}%)")
return {
"correct": correct,
"evaluated": evaluated,
"accuracy": accuracy,
}
# ============================================================
# Quick evaluation without models (using heuristic solvers)
# ============================================================
class HeuristicSolver:
"""
Simple heuristic solver for common ARC patterns.
No ML model needed — uses pattern matching.
"""
@staticmethod
def try_identity(task: Dict) -> Optional[List[List[int]]]:
"""Check if output == input."""
for pair in task["train"]:
if not grids_equal(pair["input"], pair["output"]):
return None
return copy.deepcopy(task["test"][0]["input"])
@staticmethod
def try_color_replacement(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is input with colors replaced."""
# Infer color mapping from first example
inp0 = task["train"][0]["input"]
out0 = task["train"][0]["output"]
if len(inp0) != len(out0) or len(inp0[0]) != len(out0[0]):
return None
color_map = {}
for r in range(len(inp0)):
for c in range(len(inp0[0])):
ic = inp0[r][c]
oc = out0[r][c]
if ic in color_map:
if color_map[ic] != oc:
return None
color_map[ic] = oc
# Verify on other examples
for pair in task["train"][1:]:
inp, out = pair["input"], pair["output"]
if len(inp) != len(out) or len(inp[0]) != len(out[0]):
return None
for r in range(len(inp)):
for c in range(len(inp[0])):
expected = color_map.get(inp[r][c])
if expected is None or expected != out[r][c]:
return None
# Apply to test
test_inp = task["test"][0]["input"]
result = []
for row in test_inp:
result.append([color_map.get(c, c) for c in row])
return result
@staticmethod
def try_rotation(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is a rotation of input."""
for k in [1, 2, 3]: # 90, 180, 270 degrees
all_match = True
for pair in task["train"]:
rotated = np.rot90(np.array(pair["input"]), k=-k).tolist()
if not grids_equal(rotated, pair["output"]):
all_match = False
break
if all_match:
return np.rot90(np.array(task["test"][0]["input"]), k=-k).tolist()
return None
@staticmethod
def try_flip(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is a flip of input."""
for flip_fn in [np.fliplr, np.flipud]:
all_match = True
for pair in task["train"]:
flipped = flip_fn(np.array(pair["input"])).tolist()
if not grids_equal(flipped, pair["output"]):
all_match = False
break
if all_match:
return flip_fn(np.array(task["test"][0]["input"])).tolist()
return None
@staticmethod
def try_transpose(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is transpose of input."""
all_match = True
for pair in task["train"]:
transposed = np.array(pair["input"]).T.tolist()
if not grids_equal(transposed, pair["output"]):
all_match = False
break
if all_match:
return np.array(task["test"][0]["input"]).T.tolist()
return None
@staticmethod
def try_crop_to_nonzero(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is the input cropped to non-zero bounding box."""
for pair in task["train"]:
arr = np.array(pair["input"])
nonzero = np.argwhere(arr != 0)
if len(nonzero) == 0:
return None
r1, c1 = nonzero.min(axis=0)
r2, c2 = nonzero.max(axis=0)
cropped = arr[r1:r2+1, c1:c2+1].tolist()
if not grids_equal(cropped, pair["output"]):
return None
test_arr = np.array(task["test"][0]["input"])
nonzero = np.argwhere(test_arr != 0)
if len(nonzero) == 0:
return None
r1, c1 = nonzero.min(axis=0)
r2, c2 = nonzero.max(axis=0)
return test_arr[r1:r2+1, c1:c2+1].tolist()
@staticmethod
def try_scale(task: Dict) -> Optional[List[List[int]]]:
"""Check if output is input scaled by some factor."""
for factor in [2, 3, 4, 5]:
all_match = True
for pair in task["train"]:
inp = pair["input"]
out = pair["output"]
if len(out) != len(inp) * factor or len(out[0]) != len(inp[0]) * factor:
all_match = False
break
# Check each cell
for r in range(len(inp)):
for c in range(len(inp[0])):
for dr in range(factor):
for dc in range(factor):
if out[r*factor+dr][c*factor+dc] != inp[r][c]:
all_match = False
break
if not all_match:
break
if not all_match:
break
if not all_match:
break
if not all_match:
break
if all_match:
inp = task["test"][0]["input"]
result = []
for row in inp:
for dr in range(factor):
result.append([c for c in row for dc in range(factor)])
return result
return None
def solve(self, task: Dict) -> Optional[List[List[int]]]:
"""Try all heuristic solvers."""
solvers = [
self.try_identity,
self.try_color_replacement,
self.try_rotation,
self.try_flip,
self.try_transpose,
self.try_crop_to_nonzero,
self.try_scale,
]
for solver in solvers:
try:
result = solver(task)
if result is not None:
return result
except Exception:
continue
return None
# ============================================================
# Main entry point
# ============================================================
def main():
"""Main evaluation pipeline."""
print("=" * 60)
print("ARC-AGI-2 Solver v1.0")
print("Dual-track: Program Synthesis + Test-Time Training")
print("=" * 60)
# Load data
print("\nLoading ARC-AGI-2 training data...")
tasks = load_arc_dataset_from_hf("arc-agi-community/arc-agi-2", "train")
print(f"Loaded {len(tasks)} tasks")
# Quick heuristic evaluation
print("\n--- Heuristic Solver Evaluation ---")
heuristic = HeuristicSolver()
h_correct = 0
h_total = 0
for i, task in enumerate(tasks):
gt = task["test"][0].get("output")
if gt is None:
continue
h_total += 1
pred = heuristic.solve(task)
if pred is not None and grids_equal(pred, gt):
h_correct += 1
print(f"Heuristic solver: {h_correct}/{h_total} = {h_correct/h_total*100:.1f}%")
# Full solver (if models available)
print("\n--- Full Solver ---")
solver = ARC_Solver()
try:
solver.load_models()
# Solve all tasks
task_ids = [str(i) for i in range(len(tasks))]
results = solver.solve_all(tasks, task_ids)
# Evaluate
eval_results = solver.evaluate(tasks, results, task_ids)
except Exception as e:
print(f"Model loading failed (expected on CPU): {e}")
print("Running heuristic-only evaluation...")
if __name__ == "__main__":
main()