rogermt commited on
Commit
e54021c
·
verified ·
1 Parent(s): ad22281

Remove original files from root (now in own-solver/)

Browse files
neurogolf_solver/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ARC-AGI NeuroGolf Championship - Complete Solver v5
4
- Refactored into modular components.
5
- """
6
-
7
- __version__ = '5.0.0'
 
 
 
 
 
 
 
 
neurogolf_solver/config.py DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Runtime configuration management."""
3
-
4
- from onnx import helper
5
-
6
- def get_providers(device='auto'):
7
- """Get ONNX Runtime execution providers based on device."""
8
- if device == 'cuda':
9
- return ['CUDAExecutionProvider', 'CPUExecutionProvider']
10
- return ['CPUExecutionProvider']
11
-
12
- def make_opset(version=17):
13
- """Create ONNX opset identifier."""
14
- return [helper.make_opsetid("", version)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/constants.py DELETED
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Constants and configuration values for ARC-AGI NeuroGolf Championship."""
3
-
4
- import numpy as np
5
- from onnx import TensorProto
6
-
7
- # Grid dimensions
8
- BATCH, CH, GH, GW = 1, 10, 30, 30
9
- GRID_SHAPE = [BATCH, CH, GH, GW]
10
-
11
- # ONNX settings
12
- DT = TensorProto.FLOAT
13
- IR = 8
14
- OPSET_VERSION = 17
15
-
16
- # Limits
17
- INT64_MIN = int(np.iinfo(np.int64).min)
18
- BANNED_OPS = {'Loop', 'Scan', 'NonZero', 'Unique', 'Script', 'Function'}
19
- MAX_ONNX_FILESIZE = int(1.44 * 1024 * 1024) # per .onnx file, NOT submission zip
20
-
21
- # Task exclusions — NONE. All 400 tasks count.
22
- EXCLUDED_TASKS = set()
23
-
24
- # ARC-GEN limits
25
- MAX_ARCGEN_VALIDATE = 30
26
- MAX_ARCGEN_FIT = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/data_loader.py DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Data loading utilities for ARC-AGI tasks."""
3
-
4
- import json
5
- import os
6
- import numpy as np
7
- from .constants import CH, GH, GW
8
-
9
-
10
- def load_tasks_dir(data_dir, arcgen_dir=None):
11
- """Load tasks from directory structure."""
12
- files = sorted(f for f in os.listdir(data_dir) if f.endswith('.json'))
13
- tasks = {}
14
- for i, f in enumerate(files):
15
- with open(os.path.join(data_dir, f)) as fh:
16
- data = json.load(fh)
17
- hex_id = f.replace('.json', '')
18
- if arcgen_dir and os.path.exists(os.path.join(arcgen_dir, f)):
19
- with open(os.path.join(arcgen_dir, f)) as fh:
20
- arcgen_examples = json.load(fh)
21
- if isinstance(arcgen_examples, list):
22
- data['arc-gen'] = arcgen_examples
23
- if 'arc-gen' not in data:
24
- data['arc-gen'] = []
25
- tasks[i + 1] = {'hex': hex_id, 'data': data}
26
- return tasks
27
-
28
-
29
- def load_tasks_kaggle(data_dir):
30
- """Load tasks from Kaggle format."""
31
- tasks = {}
32
- for tn in range(1, 401):
33
- path = os.path.join(data_dir, f"task{tn:03d}.json")
34
- if os.path.exists(path):
35
- with open(path) as f:
36
- data = json.load(f)
37
- if 'arc-gen' not in data:
38
- data['arc-gen'] = []
39
- tasks[tn] = {'hex': f'task{tn:03d}', 'data': data}
40
- return tasks
41
-
42
-
43
- def to_onehot(grid):
44
- """Convert grid to one-hot encoding."""
45
- arr = np.zeros((1, CH, GH, GW), dtype=np.float32)
46
- for r, row in enumerate(grid):
47
- for c, v in enumerate(row):
48
- if r < GH and c < GW and 0 <= v < CH:
49
- arr[0, v, r, c] = 1.0
50
- return arr
51
-
52
-
53
- def get_exs(td):
54
- """Get examples as numpy arrays."""
55
- return [(np.array(ex['input'], dtype=np.int64), np.array(ex['output'], dtype=np.int64))
56
- for ex in td['train'] + td['test']]
57
-
58
-
59
- def get_exs_for_fitting(td):
60
- """Get examples for fitting with ARC-GEN augmentation."""
61
- base_exs = [(np.array(ex['input'], dtype=np.int64), np.array(ex['output'], dtype=np.int64))
62
- for ex in td['train'] + td['test']]
63
- if not base_exs:
64
- return base_exs
65
- base_shapes = {inp.shape for inp, _ in base_exs}
66
- if len(base_shapes) != 1:
67
- return base_exs
68
- base_shape = list(base_shapes)[0]
69
- ag_exs = []
70
- for ex in td.get('arc-gen', []):
71
- inp = np.array(ex['input'], dtype=np.int64)
72
- out = np.array(ex['output'], dtype=np.int64)
73
- if inp.shape == base_shape and out.shape == base_exs[0][1].shape:
74
- ag_exs.append((inp, out))
75
- return base_exs + ag_exs[:10]
76
-
77
-
78
- def get_exs_for_fitting_variable(td):
79
- """Get examples for variable-shape fitting."""
80
- base_exs = [(np.array(ex['input'], dtype=np.int64), np.array(ex['output'], dtype=np.int64))
81
- for ex in td['train'] + td['test']]
82
- ag_exs = []
83
- for ex in td.get('arc-gen', []):
84
- inp = np.array(ex['input'], dtype=np.int64)
85
- out = np.array(ex['output'], dtype=np.int64)
86
- if inp.shape == out.shape and inp.shape[0] <= 30 and inp.shape[1] <= 30:
87
- ag_exs.append((inp, out))
88
- return base_exs + ag_exs[:20]
89
-
90
-
91
- def fixed_shapes(td):
92
- """Check if task has fixed input/output shapes."""
93
- shapes = set()
94
- for inp, out in get_exs(td):
95
- shapes.add((inp.shape, out.shape))
96
- return list(shapes)[0] if len(shapes) == 1 else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/gather_helpers.py DELETED
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Gather-based model building utilities."""
3
-
4
- import numpy as np
5
- from onnx import numpy_helper, helper
6
- from .onnx_helpers import mk
7
- from .constants import GH, GW
8
-
9
-
10
- def _build_gather_model(OH, OW, idx):
11
- """Build gather model from index mapping."""
12
- flat_idx = np.zeros((GH * GW,), dtype=np.int64)
13
- mask = np.zeros((1, 1, GH, GW), dtype=np.float32)
14
- for oi in range(OH):
15
- for oj in range(OW):
16
- flat_idx[oi * GW + oj] = idx[oi, oj, 0] * GW + idx[oi, oj, 1]
17
- mask[0, 0, oi, oj] = 1.0
18
- inits = [
19
- numpy_helper.from_array(np.array([1, 10, GH * GW], dtype=np.int64), 'fs'),
20
- numpy_helper.from_array(flat_idx, 'idx'),
21
- numpy_helper.from_array(np.array([1, 10, GH, GW], dtype=np.int64), 'os'),
22
- numpy_helper.from_array(mask, 'mask'),
23
- ]
24
- nodes = [
25
- helper.make_node('Reshape', ['input', 'fs'], ['flat']),
26
- helper.make_node('Gather', ['flat', 'idx'], ['g'], axis=2),
27
- helper.make_node('Reshape', ['g', 'os'], ['raw']),
28
- helper.make_node('Mul', ['raw', 'mask'], ['output']),
29
- ]
30
- return mk(nodes, inits)
31
-
32
-
33
- def _build_gather_model_with_const(IH, IW, OH, OW, idx, cst):
34
- """Build gather model with constant values."""
35
- flat_idx = np.zeros((GH * GW,), dtype=np.int64)
36
- gather_mask = np.zeros((1, 1, GH, GW), dtype=np.float32)
37
- const_oh = np.zeros((1, 10, GH, GW), dtype=np.float32)
38
- for oi in range(OH):
39
- for oj in range(OW):
40
- if idx[oi, oj, 0] >= 0:
41
- flat_idx[oi * GW + oj] = idx[oi, oj, 0] * GW + idx[oi, oj, 1]
42
- gather_mask[0, 0, oi, oj] = 1.0
43
- elif cst[oi, oj] >= 0:
44
- const_oh[0, cst[oi, oj], oi, oj] = 1.0
45
- has_const = np.any(const_oh > 0)
46
- inits = [
47
- numpy_helper.from_array(np.array([1, 10, GH * GW], dtype=np.int64), 'fs'),
48
- numpy_helper.from_array(flat_idx, 'idx'),
49
- numpy_helper.from_array(np.array([1, 10, GH, GW], dtype=np.int64), 'os'),
50
- numpy_helper.from_array(gather_mask, 'gmask'),
51
- ]
52
- nodes = [
53
- helper.make_node('Reshape', ['input', 'fs'], ['flat']),
54
- helper.make_node('Gather', ['flat', 'idx'], ['g'], axis=2),
55
- helper.make_node('Reshape', ['g', 'os'], ['raw']),
56
- helper.make_node('Mul', ['raw', 'gmask'], ['masked']),
57
- ]
58
- if has_const:
59
- inits.append(numpy_helper.from_array(const_oh, 'cst'))
60
- nodes.append(helper.make_node('Add', ['masked', 'cst'], ['output']))
61
- else:
62
- nodes[-1] = helper.make_node('Mul', ['raw', 'gmask'], ['output'])
63
- return mk(nodes, inits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/main.py DELETED
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ARC-AGI NeuroGolf Championship - Main Entry Point
4
-
5
- Usage:
6
- python -m neurogolf_solver.main --data_dir ARC-AGI/data/training/ --output_dir submission
7
- python -m neurogolf_solver.main --kaggle --output_dir /kaggle/working/submission
8
- python -m neurogolf_solver.main --data_dir ARC-AGI/data/training/ --arcgen_dir ARC-GEN-100K/ --use_wandb
9
- """
10
-
11
- import argparse
12
- import os
13
- import sys
14
- import time
15
- import onnxruntime as ort
16
- from .config import get_providers
17
- from .data_loader import load_tasks_dir, load_tasks_kaggle
18
- from .submission import run_tasks, generate_submission, print_summary
19
- from .profiler import score_network
20
- from .constants import EXCLUDED_TASKS, MAX_ONNX_FILESIZE
21
-
22
- try:
23
- import wandb
24
- except ImportError:
25
- wandb = None
26
-
27
-
28
- def check_all_models(output_dir, strict_size, strict_score):
29
- """Check all .onnx files for size limit and scoreability.
30
- strict_size=True → halt on oversized files.
31
- strict_score=False → warn on unscorable but don't halt."""
32
- size_problems = []
33
- score_problems = []
34
- for f in sorted(os.listdir(output_dir)):
35
- if not f.endswith('.onnx'):
36
- continue
37
- fpath = os.path.join(output_dir, f)
38
- fsize = os.path.getsize(fpath)
39
-
40
- if fsize > MAX_ONNX_FILESIZE:
41
- size_problems.append((f, fsize))
42
-
43
- macs, memory, params = score_network(fpath)
44
- if macs is None or memory is None or params is None:
45
- score_problems.append(f)
46
-
47
- if size_problems:
48
- print(f"\n{'!'*70}")
49
- print(f"FATAL: {len(size_problems)} .onnx files exceed 1.44MB limit:")
50
- for f, sz in size_problems:
51
- print(f" {f}: {sz:,} bytes ({sz/1024:.1f} KB)")
52
- print(f"{'!'*70}")
53
- if strict_size:
54
- sys.exit(1)
55
-
56
- if score_problems:
57
- print(f"\nWARNING: {len(score_problems)} .onnx files unscorable by onnx_tool:")
58
- for f in score_problems:
59
- print(f" {f}")
60
- if strict_score:
61
- print("Stopping (--strict_score is on).")
62
- sys.exit(1)
63
-
64
- if not size_problems and not score_problems:
65
- print(f"\nAll .onnx files pass size and score checks.")
66
-
67
-
68
- def main():
69
- parser = argparse.ArgumentParser(description='NeuroGolf Solver v5')
70
- parser.add_argument('--data_dir', default='ARC-AGI/data/training/')
71
- parser.add_argument('--arcgen_dir', default='', help='Path to ARC-GEN-100K/ directory')
72
- parser.add_argument('--output_dir', default='/kaggle/working/submission')
73
- parser.add_argument('--kaggle', action='store_true', help='Use Kaggle task format')
74
- parser.add_argument('--conv_budget', type=float, default=30.0, help='Seconds per conv solver per task')
75
- parser.add_argument('--tasks', type=str, default='', help='Comma-separated task numbers')
76
- parser.add_argument('--device', type=str, default='auto', choices=['auto', 'cpu', 'cuda'])
77
- parser.add_argument('--use_wandb', action='store_true', help='Enable W&B logging')
78
- parser.add_argument('--strict_size', type=bool, default=True, help='Halt if any .onnx > 1.44MB (default: True)')
79
- parser.add_argument('--strict_score', type=bool, default=False, help='Halt if any model unscorable (default: False)')
80
- args = parser.parse_args()
81
-
82
- providers = get_providers(args.device)
83
-
84
- config = {
85
- "device": args.device,
86
- "conv_budget": args.conv_budget,
87
- "data_dir": args.data_dir,
88
- "arcgen_dir": args.arcgen_dir,
89
- "tasks": args.tasks,
90
- }
91
-
92
- ort.set_default_logger_severity(3)
93
- print(f"Using providers: {providers}")
94
- print(f"Strict size: {args.strict_size} | Strict score: {args.strict_score}")
95
- print(f"Max .onnx file size: {MAX_ONNX_FILESIZE:,} bytes")
96
-
97
- # Load tasks
98
- if args.kaggle:
99
- tasks = load_tasks_kaggle(args.data_dir)
100
- else:
101
- arcgen = args.arcgen_dir if args.arcgen_dir else None
102
- tasks = load_tasks_dir(args.data_dir, arcgen_dir=arcgen)
103
-
104
- total_arcgen = sum(len(t['data'].get('arc-gen', [])) for t in tasks.values())
105
- print(f"Loaded {len(tasks)} tasks ({total_arcgen} ARC-GEN examples)")
106
-
107
- task_nums = [int(t) for t in args.tasks.split(',')] if args.tasks else sorted(tasks.keys())
108
- print(f"Solving {len(task_nums)} tasks")
109
- print(f"Conv budget: {args.conv_budget}s per task")
110
- print("=" * 70)
111
-
112
- t0 = time.time()
113
-
114
- if args.use_wandb and wandb is not None:
115
- with wandb.init(project="neurogolf", name="solver_run", config=config):
116
- results, costs_dict, total_score = run_tasks(
117
- task_nums, tasks, args.output_dir, providers,
118
- args.conv_budget, EXCLUDED_TASKS, use_wandb=True
119
- )
120
- else:
121
- results, costs_dict, total_score = run_tasks(
122
- task_nums, tasks, args.output_dir, providers,
123
- args.conv_budget, EXCLUDED_TASKS, use_wandb=False
124
- )
125
-
126
- elapsed = time.time() - t0
127
-
128
- # Check all output files BEFORE generating submission
129
- check_all_models(args.output_dir, args.strict_size, args.strict_score)
130
-
131
- submission_info = generate_submission(args.output_dir, results, costs_dict, task_nums)
132
- print_summary(results, submission_info, elapsed)
133
-
134
-
135
- if __name__ == '__main__':
136
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/onnx_helpers.py DELETED
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env python3
2
- """ONNX model building helper functions (opset 17)."""
3
-
4
- import numpy as np
5
- from onnx import helper, TensorProto, numpy_helper
6
- from .constants import DT, IR, GRID_SHAPE, INT64_MIN, GH, GW
7
- from .config import make_opset
8
-
9
-
10
- def _make_int64_init(name, values):
11
- """Create int64 initializer."""
12
- return numpy_helper.from_array(np.array(values, dtype=np.int64), name)
13
-
14
-
15
- def _build_pad_node(input_name, output_name, pad_h, pad_w, inits, suffix=''):
16
- """Pad with tensor-based pads input (opset 11+)."""
17
- pads_name = f'pads{suffix}'
18
- cv_name = f'pad_cv{suffix}'
19
- pads_arr = np.array([0, 0, 0, 0, 0, 0, pad_h, pad_w], dtype=np.int64)
20
- inits.append(numpy_helper.from_array(pads_arr, pads_name))
21
- inits.append(numpy_helper.from_array(np.array(0.0, dtype=np.float32), cv_name))
22
- return helper.make_node('Pad', [input_name, pads_name, cv_name], [output_name], mode='constant')
23
-
24
-
25
- def _build_slice_crop(input_name, output_name, IH, IW, inits, suffix=''):
26
- """Slice to crop [1,10,30,30] to [1,10,IH,IW]."""
27
- st_name = f'crop_st{suffix}'
28
- en_name = f'crop_en{suffix}'
29
- inits.append(_make_int64_init(st_name, [0, 0, 0, 0]))
30
- inits.append(_make_int64_init(en_name, [1, 10, IH, IW]))
31
- return helper.make_node('Slice', [input_name, st_name, en_name], [output_name])
32
-
33
-
34
- def _build_slice_reverse(input_name, output_name, axis, dim_size, inits, suffix=''):
35
- """Slice(step=-1) to reverse one axis. Zero MACs."""
36
- st_name = f'rev_st{suffix}'
37
- en_name = f'rev_en{suffix}'
38
- ax_name = f'rev_ax{suffix}'
39
- sp_name = f'rev_sp{suffix}'
40
- inits.append(_make_int64_init(st_name, [dim_size - 1]))
41
- inits.append(_make_int64_init(en_name, [INT64_MIN]))
42
- inits.append(_make_int64_init(ax_name, [axis]))
43
- inits.append(_make_int64_init(sp_name, [-1]))
44
- return helper.make_node('Slice', [input_name, st_name, en_name, ax_name, sp_name], [output_name])
45
-
46
-
47
- def _build_reducesum(input_name, output_name, axes_list, inits, suffix=''):
48
- """ReduceSum with axes as tensor input (opset 13+). keepdims=1."""
49
- axes_name = f'rs_axes{suffix}'
50
- inits.append(_make_int64_init(axes_name, axes_list))
51
- return helper.make_node('ReduceSum', [input_name, axes_name], [output_name], keepdims=1)
52
-
53
-
54
- def mk(nodes, inits=None, opset_version=17):
55
- """Create ONNX model from nodes and initializers."""
56
- x = helper.make_tensor_value_info("input", DT, GRID_SHAPE)
57
- y = helper.make_tensor_value_info("output", DT, GRID_SHAPE)
58
- g = helper.make_graph(nodes, "g", [x], [y], initializer=inits or [])
59
- return helper.make_model(g, ir_version=IR, opset_imports=make_opset(opset_version))
60
-
61
-
62
- def add_onehot_block(nodes, inits, am_name, oh_name):
63
- """Add ArgMax one-hot conversion block."""
64
- classes = np.arange(10, dtype=np.int64).reshape(1, 10, 1, 1)
65
- inits.append(numpy_helper.from_array(classes, 'classes'))
66
- nodes.append(helper.make_node('Equal', [am_name, 'classes'], ['eq']))
67
- nodes.append(helper.make_node('Cast', ['eq'], [oh_name], to=TensorProto.FLOAT))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/profiler.py DELETED
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Static profiling for ONNX models.
3
-
4
- Uses neurogolf_utils.score_network() (onnx_tool) when available — this is
5
- the ONLY scoring that matches Kaggle. The static fallback is approximate
6
- and prints a WARNING. If onnx_tool returns (None, None, None), the model
7
- is REJECTED — do not submit it.
8
- """
9
-
10
- import onnx
11
- from onnx import numpy_helper
12
- from .constants import BANNED_OPS, GH, GW
13
-
14
- try:
15
- from neurogolf_utils import score_network as _score_network_official
16
- HAS_ONNX_TOOL = True
17
- except ImportError:
18
- HAS_ONNX_TOOL = False
19
-
20
- _WARNED_NO_ONNX_TOOL = False
21
-
22
-
23
- def score_network(path):
24
- """Score network. Returns (macs, memory, params) or (None, None, None).
25
-
26
- If onnx_tool is available: uses official scorer. (None,None,None) = REJECTED.
27
- If onnx_tool is NOT available: uses static fallback with WARNING.
28
- """
29
- global _WARNED_NO_ONNX_TOOL
30
- if HAS_ONNX_TOOL:
31
- # Official scorer — trust its result. Do NOT catch exceptions silently.
32
- try:
33
- result = _score_network_official(path)
34
- except Exception as e:
35
- print(f"WARNING: onnx_tool score_network failed on {path}: {e}")
36
- return None, None, None
37
- return result
38
- else:
39
- if not _WARNED_NO_ONNX_TOOL:
40
- print("WARNING: onnx_tool not installed. Scores are APPROXIMATE and may not match Kaggle.")
41
- print("WARNING: Models that fail onnx_tool profiling will be REJECTED on Kaggle.")
42
- print("WARNING: Run neurogolf_utils.verify_network() in a Kaggle notebook before submitting.")
43
- _WARNED_NO_ONNX_TOOL = True
44
- return _static_profile(path)
45
-
46
-
47
- def _static_profile(path):
48
- """Static profiling fallback. APPROXIMATE — does not match Kaggle scoring.
49
- Only used when onnx_tool is not installed."""
50
- try:
51
- model = onnx.load(path)
52
- except:
53
- return None, None, None
54
- tensors = {}
55
- params = 0
56
- nbytes = 0
57
- macs = 0
58
- for init in model.graph.initializer:
59
- a = numpy_helper.to_array(init)
60
- tensors[init.name] = a
61
- params += a.size
62
- nbytes += a.nbytes
63
- for nd in model.graph.node:
64
- if nd.op_type == 'Constant':
65
- for attr in nd.attribute:
66
- if attr.t and attr.t.ByteSize() > 0:
67
- try:
68
- a = numpy_helper.to_array(attr.t)
69
- if nd.output:
70
- tensors[nd.output[0]] = a
71
- params += a.size
72
- nbytes += a.nbytes
73
- except:
74
- pass
75
- # Banned op check — UPPERCASE to match Kaggle
76
- if nd.op_type.upper() in {op.upper() for op in BANNED_OPS}:
77
- print(f"WARNING: Banned op '{nd.op_type}' found in {path}")
78
- return None, None, None
79
- if nd.op_type == 'Conv' and len(nd.input) >= 2 and nd.input[1] in tensors:
80
- w = tensors[nd.input[1]]
81
- if w.ndim == 4:
82
- co, ci, kh, kw = w.shape
83
- macs += co * ci * kh * kw * GH * GW
84
- return int(macs), int(nbytes), int(params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/WAVE2_SCAN.md DELETED
@@ -1,48 +0,0 @@
1
- # Wave 2 + Flood Fill Scan Results (2026-04-27)
2
-
3
- ## Wave 2 — Composition & Mode Extensions
4
-
5
- | Pattern | Matches |
6
- |---------|---------|
7
- | transform_then_recolor (flip/rot/transpose + color map) | 0 |
8
- | recolor_then_transform (reverse order) | 0 |
9
- | row_mode_fill (each row → dominant color) | 0 |
10
- | col_mode_fill (each col → dominant color) | 0 |
11
- | fill_bg_with_mode (zeros → global mode) | 0 |
12
- | fill_bg_with_color (zeros → fixed color, all examples) | 0 |
13
-
14
- ## Flood Fill
15
-
16
- | Pattern | Matches |
17
- |---------|---------|
18
- | flood_fill_replace (seed spreads into passable, all become fill_color) | 0 |
19
- | flood_fill_keep_seed (seed stays, passable neighbors become fill_color) | 0 |
20
-
21
- ## Pattern Inpainting
22
-
23
- | Pattern | Matches |
24
- |---------|---------|
25
- | Tile inpainting (output = perfect tile, input = tile with holes) | 0 |
26
-
27
- ## What the tasks ACTUALLY need (from manual inspection):
28
-
29
- - **Task 5**: Pattern stamping at positions indicated by markers
30
- - **Task 17**: Wallpaper defect restoration (NOT simple tile inpainting)
31
- - **Task 20**: Diamond symmetry completion with color-specific rules
32
- - **Task 27**: Shape-relative region filling (notch detection)
33
-
34
- These require **object-level reasoning**: detect shapes, understand spatial relationships
35
- between objects, apply context-dependent rules. Cannot be solved by pixel-level operations
36
- (flood fill, mode fill, color mapping) alone.
37
-
38
- ## Conclusion:
39
-
40
- Simple analytical solvers (Waves 1-2) and pixel-level propagation (flood fill)
41
- have reached their ceiling. The remaining 349 tasks need:
42
- 1. Object detection/segmentation
43
- 2. Spatial relationship reasoning
44
- 3. Context-dependent rule application
45
- 4. Pattern recognition beyond tiling
46
-
47
- These are fundamentally in the domain of learned models (conv lstsq already does this
48
- for some tasks) or much more complex hand-crafted solvers.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/__init__.py DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Solvers package for ARC-AGI NeuroGolf Championship."""
3
-
4
- from .solver_registry import ANALYTICAL_SOLVERS, solve_task
5
-
6
- __all__ = ['ANALYTICAL_SOLVERS', 'solve_task']
 
 
 
 
 
 
 
neurogolf_solver/solvers/analytical.py DELETED
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Basic analytical solvers: identity, constant, color_map, transpose."""
3
-
4
- import numpy as np
5
- from onnx import helper, numpy_helper, TensorProto
6
- from ..onnx_helpers import mk, _make_int64_init
7
- from ..data_loader import get_exs, fixed_shapes
8
-
9
-
10
- def s_identity(td):
11
- """Identity solver."""
12
- for ex in td['train'] + td['test']:
13
- if ex['input'] != ex['output']:
14
- return None
15
- return mk([helper.make_node('Identity', ['input'], ['output'])])
16
-
17
-
18
- def s_color_map(td):
19
- """Color mapping solver."""
20
- cm = {}
21
- for ex in td['train'] + td['test']:
22
- inp, out = np.array(ex['input']), np.array(ex['output'])
23
- if inp.shape != out.shape:
24
- return None
25
- for iv, ov in zip(inp.flat, out.flat):
26
- iv, ov = int(iv), int(ov)
27
- if iv in cm and cm[iv] != ov:
28
- return None
29
- cm[iv] = ov
30
- is_permutation = (set(cm.keys()) == set(cm.values()))
31
- if is_permutation:
32
- gather_ch = np.arange(10, dtype=np.int32)
33
- for src, dst in cm.items():
34
- if 0 <= src < 10 and 0 <= dst < 10:
35
- gather_ch[dst] = src
36
- inits = [numpy_helper.from_array(gather_ch, 'gi')]
37
- nodes = [helper.make_node('Gather', ['input', 'gi'], ['output'], axis=1)]
38
- return mk(nodes, inits)
39
- else:
40
- W = np.zeros((10, 10, 1, 1), dtype=np.float32)
41
- for ic in range(10):
42
- W[cm.get(ic, ic), ic, 0, 0] = 1.0
43
- return mk([helper.make_node('Conv', ['input', 'W'], ['output'], kernel_shape=[1, 1])],
44
- [numpy_helper.from_array(W, 'W')])
45
-
46
-
47
- def s_transpose(td):
48
- """Transpose solver."""
49
- for ex in td['train'] + td['test']:
50
- if not np.array_equal(np.array(ex['output']), np.array(ex['input']).T):
51
- return None
52
- return mk([helper.make_node('Transpose', ['input'], ['output'], perm=[0, 1, 3, 2])])
53
-
54
-
55
- def s_constant(td):
56
- """Constant output solver using opset 17 ReduceSum."""
57
- sp = fixed_shapes(td)
58
- if sp is None:
59
- return None
60
- exs = get_exs(td)
61
- outs = [out for _, out in exs]
62
- if not all(np.array_equal(outs[0], o) for o in outs[1:]):
63
- return None
64
- const = np.zeros((1, 10, 30, 30), dtype=np.float32)
65
- for r, row in enumerate(outs[0]):
66
- for c, v in enumerate(row):
67
- const[0, int(v), r, c] = 1.0
68
- inits = [
69
- numpy_helper.from_array(np.array(0.0, dtype=np.float32), 'z'),
70
- numpy_helper.from_array(const, 'c'),
71
- _make_int64_init('rs_axes_cst', [1, 2, 3]),
72
- ]
73
- nodes = [
74
- helper.make_node('Mul', ['input', 'z'], ['zd']),
75
- helper.make_node('ReduceSum', ['zd', 'rs_axes_cst'], ['s'], keepdims=1),
76
- helper.make_node('Add', ['s', 'c'], ['output']),
77
- ]
78
- return mk(nodes, inits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/conv.py DELETED
@@ -1,544 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Convolutional solvers with least squares fitting.
3
-
4
- v5.1: Refactored into composable primitives (_build_patch_matrix, _solve_weights,
5
- _extract_weights) + PCR (PCA regression) fallback via _solve_weights_pcr.
6
- PCR tested on 400 tasks: 0 new solves but no regressions. Code kept for
7
- future experiments (Lasso, Ridge can reuse the same _solve_weights interface).
8
- """
9
-
10
- import time
11
- import numpy as np
12
- import onnx
13
- from onnx import helper, numpy_helper
14
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node, add_onehot_block
15
- from ..data_loader import get_exs, get_exs_for_fitting, get_exs_for_fitting_variable, fixed_shapes
16
- from ..validators import validate
17
- from ..constants import GH, GW
18
-
19
-
20
- # ---------------------------------------------------------------------------
21
- # Core fitting primitives (composable: mix _build_patch_matrix with any solver)
22
- # ---------------------------------------------------------------------------
23
-
24
- def _build_patch_matrix(exs_raw, ks, use_bias, use_full_30=False):
25
- """Build patch matrix P and target matrix T_oh from examples.
26
- Returns (P, T, T_oh) or None if infeasible."""
27
- pad = ks // 2
28
- feat = 10 * ks * ks + (1 if use_bias else 0)
29
- if feat > 20000:
30
- return None
31
- patches, targets = [], []
32
- for inp_g, out_g in exs_raw:
33
- ih, iw = inp_g.shape
34
- if use_full_30:
35
- oh_full = np.zeros((10, GH, GW), dtype=np.float64)
36
- for c in range(10):
37
- oh_full[c, :ih, :iw] = (inp_g == c)
38
- oh_pad = np.pad(oh_full, ((0, 0), (pad, pad), (pad, pad)))
39
- else:
40
- oh_enc = np.zeros((10, ih, iw), dtype=np.float64)
41
- for c in range(10):
42
- oh_enc[c] = (inp_g == c)
43
- oh_pad = np.pad(oh_enc, ((0, 0), (pad, pad), (pad, pad)))
44
- oh, ow = out_g.shape
45
- for r in range(oh):
46
- for c in range(ow):
47
- p = oh_pad[:, r:r + ks, c:c + ks].flatten()
48
- if use_bias:
49
- p = np.append(p, 1.0)
50
- patches.append(p)
51
- targets.append(int(out_g[r, c]))
52
- n_patches = len(patches)
53
- if feat > 5000 and n_patches > 2000:
54
- return None
55
- P = np.array(patches, dtype=np.float64)
56
- T = np.array(targets, dtype=np.int64)
57
- T_oh = np.zeros((len(T), 10), dtype=np.float64)
58
- for i, t in enumerate(T):
59
- T_oh[i, t] = 1.0
60
- return P, T, T_oh
61
-
62
-
63
- def _solve_weights(P, T, T_oh):
64
- """Raw lstsq solve. Returns WT (p×10) or None."""
65
- try:
66
- WT = np.linalg.lstsq(P, T_oh, rcond=None)[0]
67
- except (np.linalg.LinAlgError, ValueError):
68
- return None
69
- if not np.array_equal(np.argmax(P @ WT, axis=1), T):
70
- return None
71
- return WT
72
-
73
-
74
- def _solve_weights_pcr(P, T, T_oh, var_thresholds=(0.999, 0.99, 0.95)):
75
- """PCA/Truncated SVD regression. Try multiple variance thresholds.
76
- Returns WT (p×10) or None.
77
- Only attempted when p/n > 0.5 (potential overfitting zone).
78
-
79
- Tested 2026-04-26: improves arc-gen accuracy by 3-9% on 4/345 unsolved
80
- tasks but never reaches 100% required for validation. Kept as fallback
81
- for marginal cases and for future combination with more arc-gen data."""
82
- n, p = P.shape
83
- if p / max(n, 1) <= 0.5:
84
- return None # lstsq is safe here, no need for PCR
85
- try:
86
- U, s, Vt = np.linalg.svd(P, full_matrices=False)
87
- except (np.linalg.LinAlgError, ValueError):
88
- return None
89
- cumvar = np.cumsum(s**2) / np.sum(s**2)
90
- for thresh in var_thresholds:
91
- k = int(np.searchsorted(cumvar, thresh)) + 1
92
- k = max(k, 5)
93
- k = min(k, min(n, p))
94
- P_red = U[:, :k] * s[:k]
95
- try:
96
- w_red = np.linalg.lstsq(P_red, T_oh, rcond=None)[0]
97
- except (np.linalg.LinAlgError, ValueError):
98
- continue
99
- if not np.array_equal(np.argmax(P_red @ w_red, axis=1), T):
100
- continue
101
- # Map back to full p-dimensional weights for ONNX conv
102
- WT = Vt[:k].T @ w_red
103
- # Verify full-space predictions match
104
- if np.array_equal(np.argmax(P @ WT, axis=1), T):
105
- return WT
106
- return None
107
-
108
-
109
- def _extract_weights(WT, ks, use_bias):
110
- """Extract Wconv and B from weight matrix WT."""
111
- if use_bias:
112
- Wconv = WT[:-1].T.reshape(10, 10, ks, ks).astype(np.float32)
113
- B = WT[-1].astype(np.float32)
114
- else:
115
- Wconv = WT.T.reshape(10, 10, ks, ks).astype(np.float32)
116
- B = None
117
- return Wconv, B
118
-
119
-
120
- # ---------------------------------------------------------------------------
121
- # Convenience wrappers (combine primitives into single-call fitting)
122
- # ---------------------------------------------------------------------------
123
-
124
- def _lstsq_conv(exs_raw, ks, use_bias, use_full_30=False):
125
- """Least squares convolutional weight fitting.
126
- Returns (Wconv, B) or None."""
127
- ptm = _build_patch_matrix(exs_raw, ks, use_bias, use_full_30)
128
- if ptm is None:
129
- return None
130
- P, T, T_oh = ptm
131
- WT = _solve_weights(P, T, T_oh)
132
- if WT is None:
133
- return None
134
- return _extract_weights(WT, ks, use_bias)
135
-
136
-
137
- def _lstsq_conv_pcr(exs_raw, ks, use_bias, use_full_30=False):
138
- """PCA regression convolutional weight fitting.
139
- Returns (Wconv, B) or None. Fallback when raw lstsq overfits."""
140
- ptm = _build_patch_matrix(exs_raw, ks, use_bias, use_full_30)
141
- if ptm is None:
142
- return None
143
- P, T, T_oh = ptm
144
- WT = _solve_weights_pcr(P, T, T_oh)
145
- if WT is None:
146
- return None
147
- return _extract_weights(WT, ks, use_bias)
148
-
149
-
150
- # ---------------------------------------------------------------------------
151
- # Solver functions (called from solver_registry.py)
152
- # ---------------------------------------------------------------------------
153
-
154
- def _build_and_validate_conv_fixed(fit_fn, fit_exs, ks, use_bias, IH, IW, td, path, providers):
155
- """Build ONNX model with given fit function, validate it. Returns (tag, model) or None."""
156
- result = fit_fn(fit_exs, ks, use_bias, use_full_30=False)
157
- if result is None:
158
- return None
159
- Wconv, B = result
160
- pad = ks // 2
161
- pad_h, pad_w = GH - IH, GW - IW
162
- inits = [
163
- _make_int64_init('sl_st', [0, 0, 0, 0]),
164
- _make_int64_init('sl_en', [1, 10, IH, IW]),
165
- numpy_helper.from_array(Wconv, 'W'),
166
- ]
167
- conv_inputs = ['grid', 'W']
168
- if B is not None:
169
- inits.append(numpy_helper.from_array(B, 'B'))
170
- conv_inputs.append('B')
171
- nodes = [
172
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['grid']),
173
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
174
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
175
- ]
176
- add_onehot_block(nodes, inits, 'am', 'oh_out')
177
- nodes.append(_build_pad_node('oh_out', 'output', pad_h, pad_w, inits))
178
- model = mk(nodes, inits)
179
- onnx.save(model, path)
180
- if validate(path, td, providers):
181
- tag = 'conv_fixed' if fit_fn == _lstsq_conv else 'conv_fixed_pcr'
182
- return tag, model
183
- return None
184
-
185
-
186
- def solve_conv_fixed(td, path, providers, time_budget=30.0):
187
- """Fixed-shape convolutional solver. Tries lstsq first, PCR as second pass."""
188
- exs = get_exs(td)
189
- for inp, out in exs:
190
- if inp.shape != out.shape:
191
- return None
192
- shapes = set(inp.shape for inp, _ in exs)
193
- if len(shapes) != 1:
194
- return None
195
- IH, IW = shapes.pop()
196
- fit_exs = get_exs_for_fitting(td)
197
- fit_exs = [(i, o) for i, o in fit_exs if i.shape == o.shape and i.shape == (IH, IW)]
198
- t_start = time.time()
199
- # Pass 1: raw lstsq (same as baseline)
200
- failed_ks = [] # (ks, use_bias) pairs where lstsq fit train but failed validation
201
- for use_bias in [False, True]:
202
- for ks in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]:
203
- if time.time() - t_start > time_budget:
204
- return None
205
- result = _lstsq_conv(fit_exs, ks, use_bias, use_full_30=False)
206
- if result is None:
207
- continue
208
- Wconv, B = result
209
- pad = ks // 2
210
- pad_h, pad_w = GH - IH, GW - IW
211
- inits = [
212
- _make_int64_init('sl_st', [0, 0, 0, 0]),
213
- _make_int64_init('sl_en', [1, 10, IH, IW]),
214
- numpy_helper.from_array(Wconv, 'W'),
215
- ]
216
- conv_inputs = ['grid', 'W']
217
- if B is not None:
218
- inits.append(numpy_helper.from_array(B, 'B'))
219
- conv_inputs.append('B')
220
- nodes = [
221
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['grid']),
222
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
223
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
224
- ]
225
- add_onehot_block(nodes, inits, 'am', 'oh_out')
226
- nodes.append(_build_pad_node('oh_out', 'output', pad_h, pad_w, inits))
227
- model = mk(nodes, inits)
228
- onnx.save(model, path)
229
- if validate(path, td, providers):
230
- return 'conv_fixed', model
231
- # lstsq fit train but failed validation — candidate for PCR
232
- failed_ks.append((ks, use_bias))
233
- # Pass 2: PCR on failed ks values (only if time remains)
234
- for ks, use_bias in failed_ks:
235
- if time.time() - t_start > time_budget:
236
- return None
237
- r = _build_and_validate_conv_fixed(_lstsq_conv_pcr, fit_exs, ks, use_bias, IH, IW, td, path, providers)
238
- if r is not None:
239
- return r
240
- return None
241
-
242
-
243
- def _build_and_validate_conv_var(fit_fn, fit_exs, ks, use_bias, td, path, providers):
244
- """Build variable-shape ONNX model with given fit function. Returns (tag, model) or None."""
245
- result = fit_fn(fit_exs, ks, use_bias, use_full_30=True)
246
- if result is None:
247
- return None
248
- Wconv, B = result
249
- pad = ks // 2
250
- inits = [
251
- numpy_helper.from_array(Wconv, 'W'),
252
- _make_int64_init('rs_axes_var', [1]),
253
- ]
254
- conv_inputs = ['input', 'W']
255
- if B is not None:
256
- inits.append(numpy_helper.from_array(B, 'B'))
257
- conv_inputs.append('B')
258
- nodes = [
259
- helper.make_node('ReduceSum', ['input', 'rs_axes_var'], ['mask'], keepdims=1),
260
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
261
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
262
- ]
263
- add_onehot_block(nodes, inits, 'am', 'oh_out')
264
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
265
- model = mk(nodes, inits)
266
- onnx.save(model, path)
267
- if validate(path, td, providers):
268
- tag = 'conv_var' if fit_fn == _lstsq_conv else 'conv_var_pcr'
269
- return tag, model
270
- return None
271
-
272
-
273
- def solve_conv_variable(td, path, providers, time_budget=30.0):
274
- """Variable-shape conv. Tries lstsq first, PCR as second pass."""
275
- exs = get_exs(td)
276
- for inp, out in exs:
277
- if inp.shape != out.shape:
278
- return None
279
- fit_exs = get_exs_for_fitting_variable(td)
280
- fit_exs = [(i, o) for i, o in fit_exs if i.shape == o.shape]
281
- t_start = time.time()
282
- # Pass 1: raw lstsq
283
- failed_ks = []
284
- for use_bias in [False, True]:
285
- for ks in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]:
286
- if time.time() - t_start > time_budget:
287
- return None
288
- result = _lstsq_conv(fit_exs, ks, use_bias, use_full_30=True)
289
- if result is None:
290
- continue
291
- Wconv, B = result
292
- pad = ks // 2
293
- inits = [
294
- numpy_helper.from_array(Wconv, 'W'),
295
- _make_int64_init('rs_axes_var', [1]),
296
- ]
297
- conv_inputs = ['input', 'W']
298
- if B is not None:
299
- inits.append(numpy_helper.from_array(B, 'B'))
300
- conv_inputs.append('B')
301
- nodes = [
302
- helper.make_node('ReduceSum', ['input', 'rs_axes_var'], ['mask'], keepdims=1),
303
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
304
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
305
- ]
306
- add_onehot_block(nodes, inits, 'am', 'oh_out')
307
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
308
- model = mk(nodes, inits)
309
- onnx.save(model, path)
310
- if validate(path, td, providers):
311
- return 'conv_var', model
312
- failed_ks.append((ks, use_bias))
313
- # Pass 2: PCR on failed ks values
314
- for ks, use_bias in failed_ks:
315
- if time.time() - t_start > time_budget:
316
- return None
317
- r = _build_and_validate_conv_var(_lstsq_conv_pcr, fit_exs, ks, use_bias, td, path, providers)
318
- if r is not None:
319
- return r
320
- return None
321
-
322
-
323
- def solve_conv_diffshape(td, path, providers, time_budget=30.0):
324
- """Different-shape convolutional solver. Tries lstsq first, PCR as second pass."""
325
- sp = fixed_shapes(td)
326
- if sp is None:
327
- return None
328
- (IH, IW), (OH, OW) = sp
329
- if IH == OH and IW == OW:
330
- return None
331
- if OH > IH or OW > IW:
332
- return None
333
- if OH > 30 or OW > 30:
334
- return None
335
- exs = get_exs(td)
336
- t_start = time.time()
337
- failed_configs = [] # (P, T, T_oh, ks, use_bias, dr_off, dc_off) for PCR retry
338
- for dr_off, dc_off in [(0, 0), ((IH - OH) // 2, (IW - OW) // 2)]:
339
- for use_bias in [False, True]:
340
- for ks in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]:
341
- if time.time() - t_start > time_budget:
342
- break
343
- pad = ks // 2
344
- feat = 10 * ks * ks + (1 if use_bias else 0)
345
- if feat > 10000:
346
- continue
347
- patches, targets = [], []
348
- valid = True
349
- for inp_g, out_g in exs:
350
- oh_enc = np.zeros((10, IH, IW), dtype=np.float64)
351
- for c in range(10):
352
- oh_enc[c] = (inp_g == c)
353
- oh_pad = np.pad(oh_enc, ((0, 0), (pad, pad), (pad, pad)))
354
- for r in range(OH):
355
- for c in range(OW):
356
- sr, sc = r + dr_off, c + dc_off
357
- if sr < 0 or sr >= IH or sc < 0 or sc >= IW:
358
- valid = False
359
- break
360
- p = oh_pad[:, sr:sr + ks, sc:sc + ks].flatten()
361
- if use_bias:
362
- p = np.append(p, 1.0)
363
- patches.append(p)
364
- targets.append(int(out_g[r, c]))
365
- if not valid:
366
- break
367
- if not valid:
368
- break
369
- if not valid:
370
- continue
371
- n_patches = len(patches)
372
- if feat > 5000 and n_patches > 2000:
373
- continue
374
- P = np.array(patches, dtype=np.float64)
375
- T = np.array(targets, dtype=np.int64)
376
- T_oh = np.zeros((len(T), 10), dtype=np.float64)
377
- for i, t in enumerate(T):
378
- T_oh[i, t] = 1.0
379
- # Pass 1: raw lstsq
380
- WT = _solve_weights(P, T, T_oh)
381
- if WT is None:
382
- continue
383
- Wconv, B = _extract_weights(WT, ks, use_bias)
384
- pad_h, pad_w = GH - OH, GW - OW
385
- inits = [
386
- _make_int64_init('sl_st', [0, 0, 0, 0]),
387
- _make_int64_init('sl_en', [1, 10, IH, IW]),
388
- numpy_helper.from_array(Wconv, 'W'),
389
- _make_int64_init('cr_st', [0, 0, dr_off, dc_off]),
390
- _make_int64_init('cr_en', [1, 10, dr_off + OH, dc_off + OW]),
391
- ]
392
- conv_inputs = ['grid', 'W']
393
- if B is not None:
394
- inits.append(numpy_helper.from_array(B, 'B'))
395
- conv_inputs.append('B')
396
- nodes = [
397
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['grid']),
398
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
399
- helper.make_node('Slice', ['co', 'cr_st', 'cr_en'], ['co_crop']),
400
- helper.make_node('ArgMax', ['co_crop'], ['am'], axis=1, keepdims=1),
401
- ]
402
- add_onehot_block(nodes, inits, 'am', 'oh_out')
403
- nodes.append(_build_pad_node('oh_out', 'output', pad_h, pad_w, inits))
404
- model = mk(nodes, inits)
405
- onnx.save(model, path)
406
- if validate(path, td, providers):
407
- return 'conv_diff', model
408
- # Failed validation — save for PCR retry
409
- failed_configs.append((P, T, T_oh, ks, use_bias, dr_off, dc_off))
410
- # Pass 2: PCR on failed configs
411
- for P, T, T_oh, ks, use_bias, dr_off, dc_off in failed_configs:
412
- if time.time() - t_start > time_budget:
413
- return None
414
- WT = _solve_weights_pcr(P, T, T_oh)
415
- if WT is None:
416
- continue
417
- Wconv, B = _extract_weights(WT, ks, use_bias)
418
- pad_h, pad_w = GH - OH, GW - OW
419
- inits = [
420
- _make_int64_init('sl_st', [0, 0, 0, 0]),
421
- _make_int64_init('sl_en', [1, 10, IH, IW]),
422
- numpy_helper.from_array(Wconv, 'W'),
423
- _make_int64_init('cr_st', [0, 0, dr_off, dc_off]),
424
- _make_int64_init('cr_en', [1, 10, dr_off + OH, dc_off + OW]),
425
- ]
426
- conv_inputs = ['grid', 'W']
427
- if B is not None:
428
- inits.append(numpy_helper.from_array(B, 'B'))
429
- conv_inputs.append('B')
430
- nodes = [
431
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['grid']),
432
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
433
- helper.make_node('Slice', ['co', 'cr_st', 'cr_en'], ['co_crop']),
434
- helper.make_node('ArgMax', ['co_crop'], ['am'], axis=1, keepdims=1),
435
- ]
436
- add_onehot_block(nodes, inits, 'am', 'oh_out')
437
- nodes.append(_build_pad_node('oh_out', 'output', pad_h, pad_w, inits))
438
- model = mk(nodes, inits)
439
- onnx.save(model, path)
440
- if validate(path, td, providers):
441
- return 'conv_diff_pcr', model
442
- return None
443
-
444
-
445
- def solve_conv_var_diff(td, path, providers, time_budget=30.0):
446
- """Variable diff-shape conv. Tries lstsq first, PCR as second pass."""
447
- exs = get_exs(td)
448
- t_start = time.time()
449
- failed_configs = [] # (P, T, T_oh, ks, use_bias) for PCR retry
450
- for use_bias in [False, True]:
451
- for ks in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]:
452
- if time.time() - t_start > time_budget:
453
- break
454
- pad = ks // 2
455
- feat = 10 * ks * ks + (1 if use_bias else 0)
456
- if feat > 20000:
457
- continue
458
- patches, targets = [], []
459
- for inp_g, out_g in exs:
460
- ih, iw = inp_g.shape
461
- oh, ow = out_g.shape
462
- oh_full = np.zeros((10, GH, GW), dtype=np.float64)
463
- for c in range(10):
464
- oh_full[c, :ih, :iw] = (inp_g == c)
465
- oh_pad = np.pad(oh_full, ((0, 0), (pad, pad), (pad, pad)))
466
- for r in range(oh):
467
- for c in range(ow):
468
- p = oh_pad[:, r:r + ks, c:c + ks].flatten()
469
- if use_bias:
470
- p = np.append(p, 1.0)
471
- patches.append(p)
472
- targets.append(int(out_g[r, c]))
473
- n_patches = len(patches)
474
- if feat > 5000 and n_patches > 2000:
475
- continue
476
- P = np.array(patches, dtype=np.float64)
477
- T = np.array(targets, dtype=np.int64)
478
- T_oh = np.zeros((len(T), 10), dtype=np.float64)
479
- for i, t in enumerate(T):
480
- T_oh[i, t] = 1.0
481
- # Pass 1: raw lstsq
482
- WT = _solve_weights(P, T, T_oh)
483
- if WT is None:
484
- continue
485
- Wconv, B = _extract_weights(WT, ks, use_bias)
486
- all_output_within_input = all(
487
- out_g.shape[0] <= inp_g.shape[0] and out_g.shape[1] <= inp_g.shape[1]
488
- for inp_g, out_g in exs
489
- )
490
- if all_output_within_input:
491
- inits = [
492
- numpy_helper.from_array(Wconv, 'W'),
493
- _make_int64_init('rs_axes_vd', [1]),
494
- ]
495
- conv_inputs = ['input', 'W']
496
- if B is not None:
497
- inits.append(numpy_helper.from_array(B, 'B'))
498
- conv_inputs.append('B')
499
- nodes = [
500
- helper.make_node('ReduceSum', ['input', 'rs_axes_vd'], ['mask'], keepdims=1),
501
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
502
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
503
- ]
504
- add_onehot_block(nodes, inits, 'am', 'oh_out')
505
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
506
- model = mk(nodes, inits)
507
- onnx.save(model, path)
508
- if validate(path, td, providers):
509
- return 'conv_var_diff', model
510
- # Failed validation — save for PCR
511
- failed_configs.append((P, T, T_oh, ks, use_bias))
512
- # Pass 2: PCR on failed configs
513
- for P, T, T_oh, ks, use_bias in failed_configs:
514
- if time.time() - t_start > time_budget:
515
- return None
516
- WT = _solve_weights_pcr(P, T, T_oh)
517
- if WT is None:
518
- continue
519
- Wconv, B = _extract_weights(WT, ks, use_bias)
520
- all_output_within_input = all(
521
- out_g.shape[0] <= inp_g.shape[0] and out_g.shape[1] <= inp_g.shape[1]
522
- for inp_g, out_g in exs
523
- )
524
- if all_output_within_input:
525
- inits = [
526
- numpy_helper.from_array(Wconv, 'W'),
527
- _make_int64_init('rs_axes_vd', [1]),
528
- ]
529
- conv_inputs = ['input', 'W']
530
- if B is not None:
531
- inits.append(numpy_helper.from_array(B, 'B'))
532
- conv_inputs.append('B')
533
- nodes = [
534
- helper.make_node('ReduceSum', ['input', 'rs_axes_vd'], ['mask'], keepdims=1),
535
- helper.make_node('Conv', conv_inputs, ['co'], kernel_shape=[ks, ks], pads=[pad] * 4),
536
- helper.make_node('ArgMax', ['co'], ['am'], axis=1, keepdims=1),
537
- ]
538
- add_onehot_block(nodes, inits, 'am', 'oh_out')
539
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
540
- model = mk(nodes, inits)
541
- onnx.save(model, path)
542
- if validate(path, td, providers):
543
- return 'conv_var_diff_pcr', model
544
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/edge.py DELETED
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Edge/boundary detection solver — Laplacian Conv.
3
-
4
- v5.2: 0 matches in current task set (edge definition too strict).
5
- Kept for future variants (per-color edges, interior-only edges, etc.).
6
- """
7
-
8
- import numpy as np
9
- from onnx import helper, numpy_helper
10
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node
11
- from ..data_loader import get_exs, fixed_shapes
12
- from ..constants import GH, GW
13
-
14
-
15
- def _has_edges(inp, out, edge_color, bg_color=0):
16
- """Check if output is edge detection of input."""
17
- h, w = inp.shape
18
- for r in range(h):
19
- for c in range(w):
20
- pix = inp[r, c]
21
- is_edge = False
22
- if pix != bg_color:
23
- for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
24
- nr, nc = r + dr, c + dc
25
- if 0 <= nr < h and 0 <= nc < w:
26
- if inp[nr, nc] != pix:
27
- is_edge = True
28
- break
29
- else:
30
- is_edge = True
31
- break
32
- expected = edge_color if is_edge else bg_color
33
- if out[r, c] != expected:
34
- return False
35
- return True
36
-
37
-
38
- def _build_edge_model(IH, IW, edge_color, bg_color=0):
39
- """Build ONNX model for edge detection via Laplacian conv."""
40
- pad_h, pad_w = GH - IH, GW - IW
41
-
42
- ch_sel = np.zeros((1, 10, 1, 1), dtype=np.float32)
43
- for c in range(10):
44
- if c != bg_color:
45
- ch_sel[0, c, 0, 0] = 1.0
46
-
47
- lap_k = np.array([[0, -1, 0],
48
- [-1, 4, -1],
49
- [0, -1, 0]], dtype=np.float32).reshape(1, 1, 3, 3)
50
-
51
- edge_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
52
- edge_oh[0, edge_color, 0, 0] = 1.0
53
- bg_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
54
- bg_oh[0, bg_color, 0, 0] = 1.0
55
-
56
- inits = [
57
- _make_int64_init('sl_st', [0, 0, 0, 0]),
58
- _make_int64_init('sl_en', [1, 10, IH, IW]),
59
- numpy_helper.from_array(ch_sel, 'ch_sel'),
60
- numpy_helper.from_array(lap_k, 'lap_k'),
61
- numpy_helper.from_array(np.float32(0.5), 'thresh'),
62
- numpy_helper.from_array(edge_oh, 'edge_oh'),
63
- numpy_helper.from_array(bg_oh, 'bg_oh'),
64
- ]
65
-
66
- nodes = [
67
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cropped']),
68
- helper.make_node('Conv', ['cropped', 'ch_sel'], ['occ'], kernel_shape=[1, 1]),
69
- helper.make_node('Conv', ['occ', 'lap_k'], ['lap_out'], kernel_shape=[3, 3], pads=[1, 1, 1, 1]),
70
- helper.make_node('Abs', ['lap_out'], ['lap_abs']),
71
- helper.make_node('Greater', ['lap_abs', 'thresh'], ['is_edge_raw']),
72
- helper.make_node('Greater', ['occ', 'thresh'], ['is_occ']),
73
- helper.make_node('And', ['is_edge_raw', 'is_occ'], ['is_edge']),
74
- helper.make_node('Where', ['is_edge', 'edge_oh', 'bg_oh'], ['result_small']),
75
- ]
76
- nodes.append(_build_pad_node('result_small', 'output', pad_h, pad_w, inits))
77
- return mk(nodes, inits)
78
-
79
-
80
- def s_edge_detect(td):
81
- """Edge detection solver: output = boundary pixels of input shapes."""
82
- exs = get_exs(td)
83
- sp = fixed_shapes(td)
84
- if sp is None:
85
- return None
86
- (IH, IW), (OH, OW) = sp
87
- if (IH, IW) != (OH, OW):
88
- return None
89
-
90
- for bg_color in [0]:
91
- out_colors = set()
92
- for _, out in exs:
93
- out_colors.update(out.flatten())
94
- for edge_color in out_colors:
95
- if edge_color == bg_color:
96
- continue
97
- if all(_has_edges(inp, out, edge_color, bg_color) for inp, out in exs):
98
- return _build_edge_model(IH, IW, edge_color, bg_color)
99
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/geometric.py DELETED
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Geometric transformation solvers: flip, rotate, shift, crop."""
3
-
4
- import numpy as np
5
- from onnx import helper
6
- from ..onnx_helpers import mk, _build_slice_crop, _build_slice_reverse, _build_pad_node
7
- from ..data_loader import get_exs, fixed_shapes
8
- from ..gather_helpers import _build_gather_model, _build_gather_model_with_const
9
- from ..constants import GH, GW
10
-
11
-
12
- def s_flip(td):
13
- """Flip using Slice(step=-1) — zero MACs."""
14
- exs = get_exs(td)
15
- sp = fixed_shapes(td)
16
- if sp is None:
17
- return None
18
- (IH, IW), (OH, OW) = sp
19
- if (IH, IW) != (OH, OW):
20
- return None
21
- for axis, flip_fn in [(0, np.flipud), (1, np.fliplr)]:
22
- if all(np.array_equal(out, flip_fn(inp)) for inp, out in exs):
23
- onnx_axis = 2 if axis == 0 else 3
24
- dim_size = IH if axis == 0 else IW
25
- pad_h, pad_w = GH - IH, GW - IW
26
- inits = []
27
- nodes = []
28
- nodes.append(_build_slice_crop('input', 'cropped', IH, IW, inits))
29
- nodes.append(_build_slice_reverse('cropped', 'flipped', onnx_axis, dim_size, inits))
30
- nodes.append(_build_pad_node('flipped', 'output', pad_h, pad_w, inits))
31
- return mk(nodes, inits)
32
- return None
33
-
34
-
35
- def s_rotate(td):
36
- """Rotate using Slice+Transpose — zero MACs for square grids and k=2.
37
- Gather fallback for non-square k=1,3."""
38
- exs = get_exs(td)
39
- sp = fixed_shapes(td)
40
- if sp is None:
41
- return None
42
- (IH, IW), (OH, OW) = sp
43
- for k in [1, 2, 3]:
44
- if not all(np.array_equal(out, np.rot90(inp, k)) for inp, out in exs):
45
- continue
46
- if k == 2:
47
- pad_h, pad_w = GH - OH, GW - OW
48
- inits = []
49
- nodes = []
50
- nodes.append(_build_slice_crop('input', 'cropped', IH, IW, inits))
51
- nodes.append(_build_slice_reverse('cropped', 'flip_h', 2, IH, inits, suffix='_h'))
52
- nodes.append(_build_slice_reverse('flip_h', 'rotated', 3, IW, inits, suffix='_w'))
53
- nodes.append(_build_pad_node('rotated', 'output', pad_h, pad_w, inits))
54
- return mk(nodes, inits)
55
- elif k == 1 and IH == IW:
56
- pad_h, pad_w = GH - IH, GW - IW
57
- inits = []
58
- nodes = []
59
- nodes.append(_build_slice_crop('input', 'cropped', IH, IW, inits))
60
- nodes.append(helper.make_node('Transpose', ['cropped'], ['transposed'], perm=[0, 1, 3, 2]))
61
- nodes.append(_build_slice_reverse('transposed', 'rotated', 2, IH, inits))
62
- nodes.append(_build_pad_node('rotated', 'output', pad_h, pad_w, inits))
63
- return mk(nodes, inits)
64
- elif k == 3 and IH == IW:
65
- pad_h, pad_w = GH - IH, GW - IW
66
- inits = []
67
- nodes = []
68
- nodes.append(_build_slice_crop('input', 'cropped', IH, IW, inits))
69
- nodes.append(_build_slice_reverse('cropped', 'flipped', 2, IH, inits))
70
- nodes.append(helper.make_node('Transpose', ['flipped'], ['rotated'], perm=[0, 1, 3, 2]))
71
- nodes.append(_build_pad_node('rotated', 'output', pad_h, pad_w, inits))
72
- return mk(nodes, inits)
73
- else:
74
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
75
- for r in range(OH):
76
- for c in range(OW):
77
- if k == 1:
78
- sr, sc = c, IH - 1 - r
79
- elif k == 3:
80
- sr, sc = IW - 1 - c, r
81
- idx[r, c] = [sr, sc]
82
- return _build_gather_model(OH, OW, idx)
83
- return None
84
-
85
-
86
- def s_shift(td):
87
- """Shift transformation solver."""
88
- exs = get_exs(td)
89
- sp = fixed_shapes(td)
90
- if sp is None:
91
- return None
92
- (IH, IW), (OH, OW) = sp
93
- if (IH, IW) != (OH, OW):
94
- return None
95
- for dr in range(-5, 6):
96
- for dc in range(-5, 6):
97
- if dr == 0 and dc == 0:
98
- continue
99
- ok = True
100
- for inp, out in exs:
101
- shifted = np.zeros_like(inp)
102
- r0, r1 = max(0, dr), min(IH, IH + dr)
103
- c0, c1 = max(0, dc), min(IW, IW + dc)
104
- if r1 > r0 and c1 > c0:
105
- sr0, sc0 = max(0, -dr), max(0, -dc)
106
- shifted[r0:r1, c0:c1] = inp[sr0:sr0 + (r1 - r0), sc0:sc0 + (c1 - c0)]
107
- if not np.array_equal(shifted, out):
108
- ok = False
109
- break
110
- if not ok:
111
- continue
112
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
113
- cst = np.full((OH, OW), 0, dtype=np.int64)
114
- for r in range(OH):
115
- for c in range(OW):
116
- sr, sc = r - dr, c - dc
117
- if 0 <= sr < IH and 0 <= sc < IW:
118
- idx[r, c] = [sr, sc]
119
- else:
120
- idx[r, c] = [-1, -1]
121
- return _build_gather_model_with_const(IH, IW, OH, OW, idx, cst)
122
- return None
123
-
124
-
125
- def s_fixed_crop(td):
126
- """Fixed crop solver."""
127
- exs = get_exs(td)
128
- sp = fixed_shapes(td)
129
- if sp is None:
130
- return None
131
- (IH, IW), (OH, OW) = sp
132
- if OH > IH or OW > IW or (OH == IH and OW == IW):
133
- return None
134
- for r0 in range(IH - OH + 1):
135
- for c0 in range(IW - OW + 1):
136
- if all(np.array_equal(inp[r0:r0 + OH, c0:c0 + OW], out) for inp, out in exs):
137
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
138
- for r in range(OH):
139
- for c in range(OW):
140
- idx[r, c] = [r0 + r, c0 + c]
141
- return _build_gather_model(OH, OW, idx)
142
- return None
143
-
144
-
145
- def s_gravity(td):
146
- """Detect gravity-like compaction (detection only, no ONNX model built)."""
147
- exs = get_exs(td)
148
- sp = fixed_shapes(td)
149
- if sp is None:
150
- return None
151
- (IH, IW), (OH, OW) = sp
152
- if (IH, IW) != (OH, OW):
153
- return None
154
-
155
- def _gravity(grid, direction):
156
- r = np.zeros_like(grid)
157
- h, w = grid.shape
158
- if direction in ('down', 'up'):
159
- for c in range(w):
160
- nz = grid[:, c][grid[:, c] != 0]
161
- if direction == 'down':
162
- r[h - len(nz):h, c] = nz
163
- else:
164
- r[:len(nz), c] = nz
165
- else:
166
- for rr in range(h):
167
- nz = grid[rr, :][grid[rr, :] != 0]
168
- if direction == 'right':
169
- r[rr, w - len(nz):w] = nz
170
- else:
171
- r[rr, :len(nz)] = nz
172
- return r
173
-
174
- for d in ('down', 'up', 'left', 'right'):
175
- if all(np.array_equal(_gravity(inp, d), out) for inp, out in exs):
176
- return None
177
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/gravity.py DELETED
@@ -1,140 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Gravity solver — unrolled bubble-sort via Conv + Where.
3
-
4
- v5.2: Solves Task 78 (direction=up, bg=0, score 8.399).
5
- Tries all 4 directions × 10 bg colors. Fixed-shape only.
6
- """
7
-
8
- import numpy as np
9
- from onnx import helper, numpy_helper
10
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node, add_onehot_block
11
- from ..data_loader import get_exs, fixed_shapes
12
- from ..constants import GH, GW
13
-
14
-
15
- def _gravity_np(grid, direction, bg_color=0):
16
- """Apply gravity in numpy for verification."""
17
- r = np.full_like(grid, bg_color)
18
- h, w = grid.shape
19
- if direction == 'down':
20
- for c in range(w):
21
- nz = grid[:, c][grid[:, c] != bg_color]
22
- r[h - len(nz):h, c] = nz
23
- elif direction == 'up':
24
- for c in range(w):
25
- nz = grid[:, c][grid[:, c] != bg_color]
26
- r[:len(nz), c] = nz
27
- elif direction == 'right':
28
- for rr in range(h):
29
- nz = grid[rr, :][grid[rr, :] != bg_color]
30
- r[rr, w - len(nz):w] = nz
31
- elif direction == 'left':
32
- for rr in range(h):
33
- nz = grid[rr, :][grid[rr, :] != bg_color]
34
- r[rr, :len(nz)] = nz
35
- return r
36
-
37
-
38
- def _build_gravity_model(IH, IW, direction, bg_color=0):
39
- """Build ONNX model for gravity via unrolled bubble-sort.
40
-
41
- Each step compares adjacent cells and swaps if needed:
42
- - If current cell is bg AND source neighbor is non-bg → fill with source
43
- - If current cell is non-bg AND destination neighbor is bg → vacate to bg
44
- After max(IH,IW) passes, all non-bg pixels settle in the gravity direction.
45
- """
46
- pad_h, pad_w = GH - IH, GW - IW
47
- n_steps = max(IH, IW)
48
-
49
- pull_above = np.zeros((10, 10, 3, 3), dtype=np.float32)
50
- pull_below = np.zeros((10, 10, 3, 3), dtype=np.float32)
51
- for ch in range(10):
52
- if direction == 'down':
53
- pull_above[ch, ch, 0, 1] = 1.0
54
- pull_below[ch, ch, 2, 1] = 1.0
55
- elif direction == 'up':
56
- pull_above[ch, ch, 2, 1] = 1.0
57
- pull_below[ch, ch, 0, 1] = 1.0
58
- elif direction == 'right':
59
- pull_above[ch, ch, 1, 0] = 1.0
60
- pull_below[ch, ch, 1, 2] = 1.0
61
- elif direction == 'left':
62
- pull_above[ch, ch, 1, 2] = 1.0
63
- pull_below[ch, ch, 1, 0] = 1.0
64
-
65
- bg_sel = np.zeros((1, 10, 1, 1), dtype=np.float32)
66
- bg_sel[0, bg_color, 0, 0] = 1.0
67
- bg_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
68
- bg_oh[0, bg_color, 0, 0] = 1.0
69
-
70
- inits = [
71
- _make_int64_init('sl_st', [0, 0, 0, 0]),
72
- _make_int64_init('sl_en', [1, 10, IH, IW]),
73
- numpy_helper.from_array(pull_above, 'pull_src'),
74
- numpy_helper.from_array(pull_below, 'pull_dst'),
75
- numpy_helper.from_array(bg_sel, 'bg_sel'),
76
- numpy_helper.from_array(bg_oh, 'bg_oh'),
77
- numpy_helper.from_array(np.float32(0.5), 'half'),
78
- ]
79
-
80
- nodes = [
81
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cur_0']),
82
- ]
83
-
84
- cur = 'cur_0'
85
- for i in range(n_steps):
86
- src = f'src_{i}'
87
- nodes.append(helper.make_node('Conv', [cur, 'pull_src'], [src],
88
- kernel_shape=[3, 3], pads=[1, 1, 1, 1]))
89
-
90
- nodes.append(helper.make_node('Mul', [cur, 'bg_sel'], [f'cbg_{i}']))
91
- inits.append(_make_int64_init(f'ax1_{i}', [1]))
92
- nodes.append(helper.make_node('ReduceSum', [f'cbg_{i}', f'ax1_{i}'], [f'cbgsum_{i}'], keepdims=1))
93
- nodes.append(helper.make_node('Greater', [f'cbgsum_{i}', 'half'], [f'cur_is_bg_{i}']))
94
-
95
- nodes.append(helper.make_node('Mul', [src, 'bg_sel'], [f'sbg_{i}']))
96
- inits.append(_make_int64_init(f'ax2_{i}', [1]))
97
- nodes.append(helper.make_node('ReduceSum', [f'sbg_{i}', f'ax2_{i}'], [f'sbgsum_{i}'], keepdims=1))
98
- nodes.append(helper.make_node('Not', [f'cur_is_bg_{i}'], [f'cur_not_bg_{i}']))
99
-
100
- nodes.append(helper.make_node('Greater', [f'sbgsum_{i}', 'half'], [f'src_is_bg_{i}']))
101
- nodes.append(helper.make_node('Not', [f'src_is_bg_{i}'], [f'src_not_bg_{i}']))
102
- nodes.append(helper.make_node('And', [f'cur_is_bg_{i}', f'src_not_bg_{i}'], [f'fill_{i}']))
103
-
104
- dst = f'dst_{i}'
105
- nodes.append(helper.make_node('Conv', [cur, 'pull_dst'], [dst],
106
- kernel_shape=[3, 3], pads=[1, 1, 1, 1]))
107
- nodes.append(helper.make_node('Mul', [dst, 'bg_sel'], [f'dbg_{i}']))
108
- inits.append(_make_int64_init(f'ax3_{i}', [1]))
109
- nodes.append(helper.make_node('ReduceSum', [f'dbg_{i}', f'ax3_{i}'], [f'dbgsum_{i}'], keepdims=1))
110
- nodes.append(helper.make_node('Greater', [f'dbgsum_{i}', 'half'], [f'dst_is_bg_{i}']))
111
- nodes.append(helper.make_node('And', [f'cur_not_bg_{i}', f'dst_is_bg_{i}'], [f'vacate_{i}']))
112
-
113
- nxt = f'cur_{i+1}'
114
- nodes.append(helper.make_node('Where', [f'fill_{i}', src, cur], [f'tmp_{i}']))
115
- nodes.append(helper.make_node('Where', [f'vacate_{i}', 'bg_oh', f'tmp_{i}'], [nxt]))
116
- cur = nxt
117
-
118
- nodes.append(helper.make_node('ArgMax', [cur], ['grav_am'], axis=1, keepdims=1))
119
- add_onehot_block(nodes, inits, 'grav_am', 'grav_oh')
120
- nodes.append(_build_pad_node('grav_oh', 'output', pad_h, pad_w, inits))
121
- return mk(nodes, inits)
122
-
123
-
124
- def s_gravity_unrolled(td):
125
- """Gravity solver with unrolled Conv+Where steps.
126
- Tries all 4 directions × bg colors 0-9."""
127
- exs = get_exs(td)
128
- sp = fixed_shapes(td)
129
- if sp is None:
130
- return None
131
- (IH, IW), (OH, OW) = sp
132
- if (IH, IW) != (OH, OW):
133
- return None
134
-
135
- for bg_color in range(10):
136
- for direction in ('down', 'up', 'left', 'right'):
137
- if all(np.array_equal(_gravity_np(inp, direction, bg_color), out)
138
- for inp, out in exs):
139
- return _build_gravity_model(IH, IW, direction, bg_color)
140
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/mode.py DELETED
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Mode fill solver — output = solid fill of most common input color.
3
-
4
- v5.2: Solves Task 129 (score 19.451).
5
- Uses runtime ReduceSum→ArgMax→Expand for variable mode across inputs.
6
- Falls through to s_constant when mode is fixed across all examples.
7
- """
8
-
9
- import numpy as np
10
- from onnx import helper, numpy_helper, TensorProto
11
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node
12
- from ..data_loader import get_exs, fixed_shapes
13
- from ..constants import GH, GW
14
-
15
-
16
- def s_mode_fill(td):
17
- """Mode fill: output is entirely the most common color from input.
18
- Uses runtime ArgMax to handle variable mode across inputs."""
19
- exs = get_exs(td)
20
-
21
- for inp, out in exs:
22
- if inp.shape != out.shape:
23
- return None
24
- vals, counts = np.unique(inp, return_counts=True)
25
- mode = vals[np.argmax(counts)]
26
- if not np.all(out == mode):
27
- return None
28
-
29
- # Check if mode is always the same color
30
- modes = set()
31
- for inp, out in exs:
32
- vals, counts = np.unique(inp, return_counts=True)
33
- modes.add(vals[np.argmax(counts)])
34
-
35
- if len(modes) == 1:
36
- return None # Let s_constant handle it
37
-
38
- sp = fixed_shapes(td)
39
- if sp is None:
40
- return None
41
- (IH, IW), (OH, OW) = sp
42
- if (IH, IW) != (OH, OW):
43
- return None
44
-
45
- pad_h, pad_w = GH - IH, GW - IW
46
-
47
- inits = [
48
- _make_int64_init('sl_st', [0, 0, 0, 0]),
49
- _make_int64_init('sl_en', [1, 10, IH, IW]),
50
- _make_int64_init('rs_axes_mode', [2, 3]),
51
- numpy_helper.from_array(np.arange(10, dtype=np.int64).reshape(1, 10, 1, 1), 'classes'),
52
- ]
53
-
54
- nodes = [
55
- helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cropped']),
56
- helper.make_node('ReduceSum', ['cropped', 'rs_axes_mode'], ['hist'], keepdims=1),
57
- helper.make_node('ArgMax', ['hist'], ['mode_idx'], axis=1, keepdims=1),
58
- helper.make_node('Equal', ['mode_idx', 'classes'], ['eq']),
59
- helper.make_node('Cast', ['eq'], ['mode_oh'], to=TensorProto.FLOAT),
60
- helper.make_node('Expand', ['mode_oh', 'sl_en'], ['expanded']),
61
- ]
62
- nodes.append(_build_pad_node('expanded', 'output', pad_h, pad_w, inits))
63
- return mk(nodes, inits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/solver_registry.py DELETED
@@ -1,163 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Solver registry and task solving orchestration."""
3
-
4
- import os
5
- import time
6
- import onnx
7
- from .analytical import s_identity, s_constant, s_color_map, s_transpose
8
- from .geometric import s_flip, s_rotate, s_shift, s_fixed_crop, s_gravity
9
- from .tiling import (s_tile, s_upscale, s_kronecker, s_nonuniform_scale, s_diagonal_tile,
10
- s_mirror_h, s_mirror_v, s_quad_mirror, s_concat, s_concat_enhanced,
11
- s_spatial_gather, s_varshape_spatial_gather)
12
- from .gravity import s_gravity_unrolled
13
- from .edge import s_edge_detect
14
- from .mode import s_mode_fill
15
- from .wave1 import (s_downsample_stride, s_symmetry_complete, s_extract_inner,
16
- s_add_border, s_sparse_fill, s_channel_filter)
17
- from .conv import solve_conv_fixed, solve_conv_variable, solve_conv_diffshape, solve_conv_var_diff
18
- from ..data_loader import get_exs, fixed_shapes
19
- from ..validators import validate
20
- from ..profiler import score_network
21
- from ..constants import EXCLUDED_TASKS, MAX_ONNX_FILESIZE
22
-
23
- # Analytical solvers registry — order matters (cheaper first)
24
- ANALYTICAL_SOLVERS = [
25
- ('identity', s_identity),
26
- ('constant', s_constant),
27
- ('color_map', s_color_map),
28
- ('transpose', s_transpose),
29
- ('flip', s_flip),
30
- ('rotate', s_rotate),
31
- ('shift', s_shift),
32
- ('tile', s_tile),
33
- ('upscale', s_upscale),
34
- ('kronecker', s_kronecker),
35
- ('nonuniform_scale', s_nonuniform_scale),
36
- ('mirror_h', s_mirror_h),
37
- ('mirror_v', s_mirror_v),
38
- ('quad_mirror', s_quad_mirror),
39
- ('concat', s_concat),
40
- ('concat_enhanced', s_concat_enhanced),
41
- ('diagonal_tile', s_diagonal_tile),
42
- ('fixed_crop', s_fixed_crop),
43
- ('spatial_gather', s_spatial_gather),
44
- ('varshape_spatial_gather', s_varshape_spatial_gather),
45
- ('gravity_unrolled', s_gravity_unrolled),
46
- ('edge_detect', s_edge_detect),
47
- ('mode_fill', s_mode_fill),
48
- ('downsample_stride', s_downsample_stride),
49
- ('symmetry_complete', s_symmetry_complete),
50
- ('extract_inner', s_extract_inner),
51
- ('add_border', s_add_border),
52
- ('sparse_fill', s_sparse_fill),
53
- ('channel_filter', s_channel_filter),
54
- ]
55
-
56
-
57
- def _check_size(path):
58
- """Return True if file is within 1.44MB limit."""
59
- try:
60
- return os.path.getsize(path) <= MAX_ONNX_FILESIZE
61
- except OSError:
62
- return False
63
-
64
-
65
- def _check_scoreable(path):
66
- """Return True if score_network returns valid scores (not None).
67
- A model that can't be scored will be REJECTED by Kaggle."""
68
- macs, memory, params = score_network(path)
69
- if macs is None or memory is None or params is None:
70
- return False
71
- return True
72
-
73
-
74
- def _accept_model(path, td, providers):
75
- """Full acceptance check: size + validate (outputs) + scoreable.
76
- Returns True only if model would be accepted by Kaggle."""
77
- if not _check_size(path):
78
- return False
79
- if not validate(path, td, providers):
80
- return False
81
- if not _check_scoreable(path):
82
- return False
83
- return True
84
-
85
-
86
- def _cleanup_failed(path):
87
- """Delete leftover .onnx file from failed solve attempts.
88
- Prevents bad files from ending up in submission zip."""
89
- try:
90
- if os.path.exists(path):
91
- os.remove(path)
92
- except OSError:
93
- pass
94
-
95
-
96
- def solve_task(tn, td, outdir, providers, conv_budget=30.0, excluded_tasks=None):
97
- """Solve a single ARC-AGI task.
98
-
99
- Returns: (ok, solver_name, file_size, elapsed, model_path)
100
- If unsolved, deletes any leftover .onnx file.
101
- """
102
- if excluded_tasks is None:
103
- excluded_tasks = EXCLUDED_TASKS
104
-
105
- t_start = time.time()
106
- os.makedirs(outdir, exist_ok=True)
107
- path = os.path.join(outdir, f"task{tn:03d}.onnx")
108
-
109
- if tn in excluded_tasks:
110
- return False, 'excluded', None, time.time() - t_start, path
111
-
112
- # 1. Try analytical solvers (fast, tiny models)
113
- for sname, sfn in ANALYTICAL_SOLVERS:
114
- try:
115
- model = sfn(td)
116
- if model is None:
117
- continue
118
- onnx.save(model, path)
119
- if _accept_model(path, td, providers):
120
- return True, sname, os.path.getsize(path), time.time() - t_start, path
121
- except:
122
- pass
123
-
124
- # 2. Determine task shape category and try conv solvers
125
- exs = get_exs(td)
126
- same_shape = all(inp.shape == out.shape for inp, out in exs)
127
- shapes = set(inp.shape for inp, _ in exs)
128
- fixed_in = len(shapes) == 1
129
-
130
- conv_time = conv_budget
131
-
132
- if same_shape:
133
- if fixed_in:
134
- result = solve_conv_fixed(td, path, providers, time_budget=conv_time / 2)
135
- if result is not None:
136
- if _check_size(path) and _check_scoreable(path):
137
- sname, model = result
138
- return True, sname, os.path.getsize(path), time.time() - t_start, path
139
- result = solve_conv_variable(td, path, providers, time_budget=conv_time)
140
- if result is not None:
141
- if _check_size(path) and _check_scoreable(path):
142
- sname, model = result
143
- return True, sname, os.path.getsize(path), time.time() - t_start, path
144
- else:
145
- sp = fixed_shapes(td)
146
- if sp is not None:
147
- (IH, IW), (OH, OW) = sp
148
- if OH <= IH and OW <= IW:
149
- result = solve_conv_diffshape(td, path, providers, time_budget=conv_time)
150
- if result is not None:
151
- if _check_size(path) and _check_scoreable(path):
152
- sname, model = result
153
- return True, sname, os.path.getsize(path), time.time() - t_start, path
154
-
155
- result = solve_conv_var_diff(td, path, providers, time_budget=conv_time)
156
- if result is not None:
157
- if _check_size(path) and _check_scoreable(path):
158
- sname, model = result
159
- return True, sname, os.path.getsize(path), time.time() - t_start, path
160
-
161
- # All solvers failed — delete leftover .onnx file
162
- _cleanup_failed(path)
163
- return False, None, None, time.time() - t_start, path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/tiling.py DELETED
@@ -1,429 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Tiling, upscaling, mirror, concat, and spatial gather solvers."""
3
-
4
- import numpy as np
5
- from onnx import helper
6
- from itertools import product as iproduct
7
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node
8
- from ..data_loader import get_exs, fixed_shapes
9
- from ..gather_helpers import _build_gather_model, _build_gather_model_with_const
10
-
11
-
12
- def s_tile(td):
13
- """Tiling solver."""
14
- exs = get_exs(td)
15
- in_shapes = set(inp.shape for inp, _ in exs)
16
- if len(in_shapes) != 1:
17
- return None
18
- IH, IW = in_shapes.pop()
19
- tiles = set()
20
- for inp, out in exs:
21
- OH, OW = out.shape
22
- if OH % IH or OW % IW:
23
- return None
24
- rH, rW = OH // IH, OW // IW
25
- if rH < 1 or rW < 1 or (rH == 1 and rW == 1):
26
- return None
27
- tiles.add((rH, rW))
28
- if len(tiles) != 1:
29
- return None
30
- rH, rW = tiles.pop()
31
- OH, OW = IH * rH, IW * rW
32
- if OH > 30 or OW > 30:
33
- return None
34
- for inp, out in exs:
35
- if not np.array_equal(out, np.tile(inp, (rH, rW))):
36
- return None
37
- pad_h, pad_w = 30 - OH, 30 - OW
38
- inits = [
39
- _make_int64_init('st', [0, 0, 0, 0]),
40
- _make_int64_init('en', [1, 10, IH, IW]),
41
- _make_int64_init('rp', [1, 1, rH, rW]),
42
- ]
43
- nodes = [
44
- helper.make_node('Slice', ['input', 'st', 'en'], ['cr']),
45
- helper.make_node('Tile', ['cr', 'rp'], ['tl']),
46
- ]
47
- nodes.append(_build_pad_node('tl', 'output', pad_h, pad_w, inits))
48
- return mk(nodes, inits)
49
-
50
-
51
- def s_upscale(td):
52
- """Upscaling solver."""
53
- exs = get_exs(td)
54
- in_shapes = set(inp.shape for inp, _ in exs)
55
- if len(in_shapes) != 1:
56
- return None
57
- IH, IW = in_shapes.pop()
58
- scales = set()
59
- for inp, out in exs:
60
- OH, OW = out.shape
61
- if OH % IH or OW % IW:
62
- return None
63
- sH, sW = OH // IH, OW // IW
64
- if sH < 2 or sW < 2:
65
- return None
66
- scales.add((sH, sW))
67
- if len(scales) != 1:
68
- return None
69
- sH, sW = scales.pop()
70
- OH, OW = IH * sH, IW * sW
71
- if OH > 30 or OW > 30:
72
- return None
73
- for inp, out in exs:
74
- if not np.array_equal(out, np.repeat(np.repeat(inp, sH, 0), sW, 1)):
75
- return None
76
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
77
- for r in range(OH):
78
- for c in range(OW):
79
- idx[r, c] = [r // sH, c // sW]
80
- return _build_gather_model(OH, OW, idx)
81
-
82
-
83
- def s_kronecker(td):
84
- """Kronecker product solver."""
85
- exs = get_exs(td)
86
- sp = fixed_shapes(td)
87
- if sp is None:
88
- return None
89
- (IH, IW), (OH, OW) = sp
90
- if OH % IH != 0 or OW % IW != 0:
91
- return None
92
- sH, sW = OH // IH, OW // IW
93
- if sH < 2 or sW < 2:
94
- return None
95
- if OH > 30 or OW > 30:
96
- return None
97
- for inp, out in exs:
98
- if not np.array_equal(out, np.kron(inp, np.ones((sH, sW), dtype=np.int64))):
99
- return None
100
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
101
- for r in range(OH):
102
- for c in range(OW):
103
- idx[r, c] = [r // sH, c // sW]
104
- return _build_gather_model(OH, OW, idx)
105
-
106
-
107
- def s_nonuniform_scale(td):
108
- """Non-uniform scaling solver."""
109
- exs = get_exs(td)
110
- sp = fixed_shapes(td)
111
- if sp is None:
112
- return None
113
- (IH, IW), (OH, OW) = sp
114
- for fh, fw in [(1, 2), (2, 1), (1, 3), (3, 1), (2, 3), (3, 2), (1, 4), (4, 1), (2, 4), (4, 2)]:
115
- if OH != IH * fh or OW != IW * fw:
116
- continue
117
- if OH > 30 or OW > 30:
118
- continue
119
- if all(np.array_equal(np.repeat(np.repeat(inp, fh, 0), fw, 1), out) for inp, out in exs):
120
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
121
- for r in range(OH):
122
- for c in range(OW):
123
- idx[r, c] = [r // fh, c // fw]
124
- return _build_gather_model(OH, OW, idx)
125
- return None
126
-
127
-
128
- def s_diagonal_tile(td):
129
- """Diagonal tiling solver."""
130
- exs = get_exs(td)
131
- sp = fixed_shapes(td)
132
- if sp is None:
133
- return None
134
- (IH, IW), (OH, OW) = sp
135
- if OH % IH != 0 or OW % IW != 0:
136
- return None
137
- rH, rW = OH // IH, OW // IW
138
- if rH != rW or rH < 2:
139
- return None
140
- if OH > 30 or OW > 30:
141
- return None
142
- for inp, out in exs:
143
- for bi in range(rH):
144
- for bj in range(rW):
145
- block = out[bi * IH:(bi + 1) * IH, bj * IW:(bj + 1) * IW]
146
- if bi == bj:
147
- if not np.array_equal(block, inp):
148
- return None
149
- else:
150
- if not np.all(block == 0):
151
- return None
152
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
153
- cst = np.full((OH, OW), -1, dtype=np.int64)
154
- for bi in range(rH):
155
- for bj in range(rW):
156
- for lr in range(IH):
157
- for lc in range(IW):
158
- oi, oj = bi * IH + lr, bj * IW + lc
159
- if bi == bj:
160
- idx[oi, oj] = [lr, lc]
161
- else:
162
- idx[oi, oj] = [-1, -1]
163
- cst[oi, oj] = 0
164
- return _build_gather_model_with_const(IH, IW, OH, OW, idx, cst)
165
-
166
-
167
- def s_mirror_h(td):
168
- """Horizontal mirror solver."""
169
- exs = get_exs(td)
170
- sp = fixed_shapes(td)
171
- if sp is None:
172
- return None
173
- (IH, IW), (OH, OW) = sp
174
- if OH != IH or OW != 2 * IW:
175
- return None
176
- if OW > 30:
177
- return None
178
- for inp, out in exs:
179
- if not np.array_equal(np.concatenate([inp, np.flip(inp, 1)], 1), out):
180
- return None
181
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
182
- for r in range(OH):
183
- for c in range(OW):
184
- sc = c if c < IW else 2 * IW - 1 - c
185
- idx[r, c] = [r, sc]
186
- return _build_gather_model(OH, OW, idx)
187
-
188
-
189
- def s_mirror_v(td):
190
- """Vertical mirror solver."""
191
- exs = get_exs(td)
192
- sp = fixed_shapes(td)
193
- if sp is None:
194
- return None
195
- (IH, IW), (OH, OW) = sp
196
- if OW != IW or OH != 2 * IH:
197
- return None
198
- if OH > 30:
199
- return None
200
- for inp, out in exs:
201
- if not np.array_equal(np.concatenate([inp, np.flip(inp, 0)], 0), out):
202
- return None
203
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
204
- for r in range(OH):
205
- for c in range(OW):
206
- sr = r if r < IH else 2 * IH - 1 - r
207
- idx[r, c] = [sr, c]
208
- return _build_gather_model(OH, OW, idx)
209
-
210
-
211
- def s_quad_mirror(td):
212
- """Quad mirror solver."""
213
- exs = get_exs(td)
214
- sp = fixed_shapes(td)
215
- if sp is None:
216
- return None
217
- (IH, IW), (OH, OW) = sp
218
- if OH != 2 * IH or OW != 2 * IW:
219
- return None
220
- if OH > 30 or OW > 30:
221
- return None
222
- for inp, out in exs:
223
- expected = np.block([[inp, np.flip(inp, 1)],
224
- [np.flip(inp, 0), np.flip(np.flip(inp, 0), 1)]])
225
- if not np.array_equal(expected, out):
226
- return None
227
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
228
- for r in range(OH):
229
- for c in range(OW):
230
- sr = r if r < IH else 2 * IH - 1 - r
231
- sc = c if c < IW else 2 * IW - 1 - c
232
- idx[r, c] = [sr, sc]
233
- return _build_gather_model(OH, OW, idx)
234
-
235
-
236
- def s_concat(td):
237
- """Concatenation solver with transformations."""
238
- exs = get_exs(td)
239
- sp = fixed_shapes(td)
240
- if sp is None:
241
- return None
242
- (IH, IW), (OH, OW) = sp
243
- transforms = [
244
- ('id', lambda x: x), ('fliplr', lambda x: np.fliplr(x)),
245
- ('flipud', lambda x: np.flipud(x)), ('rot180', lambda x: np.rot90(x, 2)),
246
- ]
247
- if OH == IH and OW % IW == 0 and OW > IW:
248
- n = OW // IW
249
- if 2 <= n <= 4:
250
- for combo in iproduct(range(4), repeat=n):
251
- if all(np.array_equal(out, np.concatenate([transforms[t][1](inp) for t in combo], axis=1))
252
- for inp, out in exs):
253
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
254
- for oi in range(OH):
255
- for oj in range(OW):
256
- bj = oj // IW
257
- lr, lc = oi, oj % IW
258
- t = transforms[combo[bj]][0]
259
- if t == 'id':
260
- sr, sc = lr, lc
261
- elif t == 'fliplr':
262
- sr, sc = lr, IW - 1 - lc
263
- elif t == 'flipud':
264
- sr, sc = IH - 1 - lr, lc
265
- elif t == 'rot180':
266
- sr, sc = IH - 1 - lr, IW - 1 - lc
267
- idx[oi, oj] = [sr, sc]
268
- return _build_gather_model(OH, OW, idx)
269
- if OW == IW and OH % IH == 0 and OH > IH:
270
- n = OH // IH
271
- if 2 <= n <= 4:
272
- for combo in iproduct(range(4), repeat=n):
273
- if all(np.array_equal(out, np.concatenate([transforms[t][1](inp) for t in combo], axis=0))
274
- for inp, out in exs):
275
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
276
- for oi in range(OH):
277
- for oj in range(OW):
278
- bi = oi // IH
279
- lr, lc = oi % IH, oj
280
- t = transforms[combo[bi]][0]
281
- if t == 'id':
282
- sr, sc = lr, lc
283
- elif t == 'fliplr':
284
- sr, sc = lr, IW - 1 - lc
285
- elif t == 'flipud':
286
- sr, sc = IH - 1 - lr, lc
287
- elif t == 'rot180':
288
- sr, sc = IH - 1 - lr, IW - 1 - lc
289
- idx[oi, oj] = [sr, sc]
290
- return _build_gather_model(OH, OW, idx)
291
- return None
292
-
293
-
294
- def s_concat_enhanced(td):
295
- """Enhanced concatenation with all 8 dihedral transforms."""
296
- exs = get_exs(td)
297
- sp = fixed_shapes(td)
298
- if sp is None:
299
- return None
300
- (IH, IW), (OH, OW) = sp
301
- if IH == OH and IW == OW:
302
- return None
303
- if OH % IH != 0 or OW % IW != 0:
304
- return None
305
- rH, rW = OH // IH, OW // IW
306
- if rH * rW > 16 or rH * rW < 2:
307
- return None
308
- if OH > 30 or OW > 30:
309
- return None
310
- transforms = [
311
- ('id', lambda x: x), ('fliplr', lambda x: np.fliplr(x)),
312
- ('flipud', lambda x: np.flipud(x)), ('rot180', lambda x: np.rot90(x, 2)),
313
- ('rot90', lambda x: np.rot90(x, 1)), ('rot270', lambda x: np.rot90(x, 3)),
314
- ('T', lambda x: x.T), ('T_fliplr', lambda x: np.fliplr(x.T)),
315
- ]
316
- block_transforms = {}
317
- for bi in range(rH):
318
- for bj in range(rW):
319
- found = None
320
- for tidx, (tname, tfn) in enumerate(transforms):
321
- ok = True
322
- for inp, out in exs:
323
- block = out[bi * IH:(bi + 1) * IH, bj * IW:(bj + 1) * IW]
324
- expected = tfn(inp)
325
- if expected.shape != (IH, IW) or not np.array_equal(block, expected):
326
- ok = False
327
- break
328
- if ok:
329
- found = (tidx, tname)
330
- break
331
- if found is None:
332
- return None
333
- block_transforms[(bi, bj)] = found
334
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
335
- for bi in range(rH):
336
- for bj in range(rW):
337
- _, tname = block_transforms[(bi, bj)]
338
- for lr in range(IH):
339
- for lc in range(IW):
340
- oi, oj = bi * IH + lr, bj * IW + lc
341
- if tname == 'id':
342
- sr, sc = lr, lc
343
- elif tname == 'fliplr':
344
- sr, sc = lr, IW - 1 - lc
345
- elif tname == 'flipud':
346
- sr, sc = IH - 1 - lr, lc
347
- elif tname == 'rot180':
348
- sr, sc = IH - 1 - lr, IW - 1 - lc
349
- elif tname == 'rot90':
350
- sr, sc = IW - 1 - lc, lr
351
- elif tname == 'rot270':
352
- sr, sc = lc, IH - 1 - lr
353
- elif tname == 'T':
354
- sr, sc = lc, lr
355
- elif tname == 'T_fliplr':
356
- sr, sc = IW - 1 - lc, lr
357
- idx[oi, oj] = [sr, sc]
358
- for inp, out in exs:
359
- reconstructed = np.zeros_like(out)
360
- for oi in range(OH):
361
- for oj in range(OW):
362
- reconstructed[oi, oj] = inp[idx[oi, oj, 0], idx[oi, oj, 1]]
363
- if not np.array_equal(reconstructed, out):
364
- return None
365
- return _build_gather_model(OH, OW, idx)
366
-
367
-
368
- def s_spatial_gather(td):
369
- """Spatial gather solver."""
370
- sp = fixed_shapes(td)
371
- if sp is None:
372
- return None
373
- (IH, IW), (OH, OW) = sp
374
- exs = get_exs(td)
375
- idx = np.full((OH, OW, 2), -1, dtype=np.int64)
376
- cst = np.full((OH, OW), -1, dtype=np.int64)
377
- for oi in range(OH):
378
- for oj in range(OW):
379
- vals = set(int(out[oi, oj]) for _, out in exs)
380
- if len(vals) == 1:
381
- cst[oi, oj] = vals.pop()
382
- found = False
383
- for ri in range(IH):
384
- for rj in range(IW):
385
- if all(int(inp[ri, rj]) == int(out[oi, oj]) for inp, out in exs):
386
- idx[oi, oj] = [ri, rj]
387
- found = True
388
- break
389
- if found:
390
- break
391
- if not found and cst[oi, oj] < 0:
392
- return None
393
- return _build_gather_model_with_const(IH, IW, OH, OW, idx, cst)
394
-
395
-
396
- def s_varshape_spatial_gather(td):
397
- """Variable shape spatial gather solver."""
398
- sp = fixed_shapes(td)
399
- if sp is not None:
400
- return None
401
- exs = get_exs(td)
402
- exs_30 = []
403
- for inp, out in exs:
404
- ih, iw = inp.shape
405
- oh, ow = out.shape
406
- inp30 = np.zeros((30, 30), dtype=np.int64)
407
- out30 = np.zeros((30, 30), dtype=np.int64)
408
- inp30[:ih, :iw] = inp
409
- out30[:oh, :ow] = out
410
- exs_30.append((inp30, out30))
411
- idx = np.full((30, 30, 2), -1, dtype=np.int64)
412
- cst = np.full((30, 30), -1, dtype=np.int64)
413
- for oi in range(30):
414
- for oj in range(30):
415
- vals = set(int(out30[oi, oj]) for _, out30 in exs_30)
416
- if len(vals) == 1:
417
- cst[oi, oj] = vals.pop()
418
- found = False
419
- for ri in range(30):
420
- for rj in range(30):
421
- if all(int(inp30[ri, rj]) == int(out30[oi, oj]) for inp30, out30 in exs_30):
422
- idx[oi, oj] = [ri, rj]
423
- found = True
424
- break
425
- if found:
426
- break
427
- if not found and cst[oi, oj] < 0:
428
- return None
429
- return _build_gather_model_with_const(30, 30, 30, 30, idx, cst)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/solvers/wave1.py DELETED
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Wave 1 static spatial remapping solvers.
3
-
4
- A4: downsample_stride — strided sampling of input
5
- A7: symmetry_complete — mirror to complete L-R or T-B symmetry
6
- A1: extract_inner — remove border frame
7
- A2: add_border — add constant border
8
- A6: sparse_fill — pixel to block expansion
9
- B1: channel_filter — keep only certain colors
10
-
11
- Scan results (2026-04-27): 0 arc-gen validated matches.
12
- Kept for future tasks and as building blocks.
13
- """
14
-
15
- import numpy as np
16
- from ..data_loader import get_exs, fixed_shapes
17
- from ..gather_helpers import _build_gather_model, _build_gather_model_with_const
18
- from ..onnx_helpers import mk, _make_int64_init, _build_pad_node, add_onehot_block
19
- from ..constants import GH, GW
20
-
21
-
22
- def s_downsample_stride(td):
23
- """out[r,c] = inp[r*sH + oH, c*sW + oW] for integer strides."""
24
- exs = get_exs(td)
25
- sp = fixed_shapes(td)
26
- if sp is None:
27
- return None
28
- (IH, IW), (OH, OW) = sp
29
- if OH >= IH or OW >= IW:
30
- return None
31
-
32
- for sh in range(2, 6):
33
- for sw in range(2, 6):
34
- for oh_off in range(sh):
35
- for ow_off in range(sw):
36
- ok = True
37
- for inp, out in exs:
38
- sampled = inp[oh_off::sh, ow_off::sw]
39
- if sampled.shape != out.shape or not np.array_equal(sampled, out):
40
- ok = False
41
- break
42
- if ok:
43
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
44
- for r in range(OH):
45
- for c in range(OW):
46
- idx[r, c] = [r * sh + oh_off, c * sw + ow_off]
47
- return _build_gather_model(OH, OW, idx)
48
- return None
49
-
50
-
51
- def s_symmetry_complete(td):
52
- """Complete partial T-B symmetry by adding mirrored + original via Gather."""
53
- from onnx import helper, numpy_helper
54
-
55
- exs = get_exs(td)
56
- sp = fixed_shapes(td)
57
- if sp is None:
58
- return None
59
- (IH, IW), (OH, OW) = sp
60
- if (IH, IW) != (OH, OW):
61
- return None
62
- if IH < 2:
63
- return None
64
-
65
- # T-B symmetry: out[r,c] = max(inp[r,c], inp[IH-1-r,c])
66
- ok = True
67
- for inp, out in exs:
68
- exp = inp.copy()
69
- for r in range(IH // 2):
70
- for c in range(IW):
71
- v = max(int(inp[r, c]), int(inp[IH - 1 - r, c]))
72
- exp[r, c] = v
73
- exp[IH - 1 - r, c] = v
74
- if not np.array_equal(out, exp):
75
- ok = False
76
- break
77
-
78
- if ok:
79
- # Build: Gather(self) + Gather(mirror) → Add → ArgMax → one-hot
80
- pad_h, pad_w = GH - OH, GW - OW
81
- mirror_idx = np.zeros((GH * GW,), dtype=np.int64)
82
- mask = np.zeros((1, 1, GH, GW), dtype=np.float32)
83
- self_idx = np.zeros((GH * GW,), dtype=np.int64)
84
- for r in range(OH):
85
- for c in range(OW):
86
- self_idx[r * GW + c] = r * GW + c
87
- mirror_idx[r * GW + c] = (IH - 1 - r) * GW + c
88
- mask[0, 0, r, c] = 1.0
89
-
90
- inits = [
91
- numpy_helper.from_array(np.array([1, 10, GH * GW], dtype=np.int64), 'fs'),
92
- numpy_helper.from_array(self_idx, 'self_idx'),
93
- numpy_helper.from_array(mirror_idx, 'mirror_idx'),
94
- numpy_helper.from_array(np.array([1, 10, GH, GW], dtype=np.int64), 'os'),
95
- numpy_helper.from_array(mask, 'mask'),
96
- ]
97
- nodes = [
98
- helper.make_node('Reshape', ['input', 'fs'], ['flat']),
99
- helper.make_node('Gather', ['flat', 'self_idx'], ['g_self'], axis=2),
100
- helper.make_node('Gather', ['flat', 'mirror_idx'], ['g_mirror'], axis=2),
101
- helper.make_node('Add', ['g_self', 'g_mirror'], ['combined']),
102
- helper.make_node('Reshape', ['combined', 'os'], ['combined_2d']),
103
- helper.make_node('ArgMax', ['combined_2d'], ['am'], axis=1, keepdims=1),
104
- ]
105
- add_onehot_block(nodes, inits, 'am', 'oh_out')
106
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
107
- return mk(nodes, inits)
108
-
109
- # L-R symmetry: out[r,c] = max(inp[r,c], inp[r,IW-1-c])
110
- if IW < 2:
111
- return None
112
- ok = True
113
- for inp, out in exs:
114
- exp = inp.copy()
115
- for r in range(IH):
116
- for c in range(IW // 2):
117
- v = max(int(inp[r, c]), int(inp[r, IW - 1 - c]))
118
- exp[r, c] = v
119
- exp[r, IW - 1 - c] = v
120
- if not np.array_equal(out, exp):
121
- ok = False
122
- break
123
-
124
- if ok:
125
- mirror_idx = np.zeros((GH * GW,), dtype=np.int64)
126
- mask = np.zeros((1, 1, GH, GW), dtype=np.float32)
127
- self_idx = np.zeros((GH * GW,), dtype=np.int64)
128
- for r in range(OH):
129
- for c in range(OW):
130
- self_idx[r * GW + c] = r * GW + c
131
- mirror_idx[r * GW + c] = r * GW + (IW - 1 - c)
132
- mask[0, 0, r, c] = 1.0
133
-
134
- inits = [
135
- numpy_helper.from_array(np.array([1, 10, GH * GW], dtype=np.int64), 'fs'),
136
- numpy_helper.from_array(self_idx, 'self_idx'),
137
- numpy_helper.from_array(mirror_idx, 'mirror_idx'),
138
- numpy_helper.from_array(np.array([1, 10, GH, GW], dtype=np.int64), 'os'),
139
- numpy_helper.from_array(mask, 'mask'),
140
- ]
141
- nodes = [
142
- helper.make_node('Reshape', ['input', 'fs'], ['flat']),
143
- helper.make_node('Gather', ['flat', 'self_idx'], ['g_self'], axis=2),
144
- helper.make_node('Gather', ['flat', 'mirror_idx'], ['g_mirror'], axis=2),
145
- helper.make_node('Add', ['g_self', 'g_mirror'], ['combined']),
146
- helper.make_node('Reshape', ['combined', 'os'], ['combined_2d']),
147
- helper.make_node('ArgMax', ['combined_2d'], ['am'], axis=1, keepdims=1),
148
- ]
149
- add_onehot_block(nodes, inits, 'am', 'oh_out')
150
- nodes.append(helper.make_node('Mul', ['oh_out', 'mask'], ['output']))
151
- return mk(nodes, inits)
152
-
153
- return None
154
-
155
-
156
- def s_extract_inner(td):
157
- """Remove N-pixel border frame → smaller output."""
158
- exs = get_exs(td)
159
- sp = fixed_shapes(td)
160
- if sp is None:
161
- return None
162
- (IH, IW), (OH, OW) = sp
163
-
164
- for b in range(1, min(IH, IW) // 2):
165
- if OH != IH - 2 * b or OW != IW - 2 * b:
166
- continue
167
- if all(np.array_equal(inp[b:IH-b, b:IW-b], out) for inp, out in exs):
168
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
169
- for r in range(OH):
170
- for c in range(OW):
171
- idx[r, c] = [r + b, c + b]
172
- return _build_gather_model(OH, OW, idx)
173
- return None
174
-
175
-
176
- def s_add_border(td):
177
- """Add constant-color border frame → larger output."""
178
- exs = get_exs(td)
179
- sp = fixed_shapes(td)
180
- if sp is None:
181
- return None
182
- (IH, IW), (OH, OW) = sp
183
-
184
- for b in range(1, 5):
185
- if OH != IH + 2 * b or OW != IW + 2 * b:
186
- continue
187
- if OH > 30 or OW > 30:
188
- continue
189
- for bc in range(10):
190
- ok = True
191
- for inp, out in exs:
192
- exp = np.full((OH, OW), bc, dtype=np.int64)
193
- exp[b:b+IH, b:b+IW] = inp
194
- if not np.array_equal(out, exp):
195
- ok = False
196
- break
197
- if ok:
198
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
199
- cst = np.full((OH, OW), -1, dtype=np.int64)
200
- for r in range(OH):
201
- for c in range(OW):
202
- if b <= r < b + IH and b <= c < b + IW:
203
- idx[r, c] = [r - b, c - b]
204
- else:
205
- idx[r, c] = [-1, -1]
206
- cst[r, c] = bc
207
- return _build_gather_model_with_const(IH, IW, OH, OW, idx, cst)
208
- return None
209
-
210
-
211
- def s_sparse_fill(td):
212
- """Each input pixel becomes an NxN block in output."""
213
- exs = get_exs(td)
214
- sp = fixed_shapes(td)
215
- if sp is None:
216
- return None
217
- (IH, IW), (OH, OW) = sp
218
-
219
- for bh in range(2, 10):
220
- for bw in range(2, 10):
221
- if OH != IH * bh or OW != IW * bw:
222
- continue
223
- if OH > 30 or OW > 30:
224
- continue
225
- ok = True
226
- for inp, out in exs:
227
- exp = np.zeros((OH, OW), dtype=np.int64)
228
- for r in range(IH):
229
- for c in range(IW):
230
- exp[r*bh:(r+1)*bh, c*bw:(c+1)*bw] = inp[r, c]
231
- if not np.array_equal(out, exp):
232
- ok = False
233
- break
234
- if ok:
235
- idx = np.zeros((OH, OW, 2), dtype=np.int64)
236
- for r in range(OH):
237
- for c in range(OW):
238
- idx[r, c] = [r // bh, c // bw]
239
- return _build_gather_model(OH, OW, idx)
240
- return None
241
-
242
-
243
- def s_channel_filter(td):
244
- """Keep only certain colors, rest → background (0)."""
245
- from onnx import helper, numpy_helper
246
-
247
- exs = get_exs(td)
248
- sp = fixed_shapes(td)
249
- if sp is None:
250
- return None
251
- (IH, IW), (OH, OW) = sp
252
- if (IH, IW) != (OH, OW):
253
- return None
254
-
255
- in_colors = set()
256
- out_colors = set()
257
- for inp, out in exs:
258
- in_colors.update(inp.flatten())
259
- out_colors.update(out.flatten())
260
-
261
- if not (out_colors < in_colors):
262
- return None
263
-
264
- keep = out_colors
265
- for inp, out in exs:
266
- exp = np.where(np.isin(inp, list(keep)), inp, 0)
267
- if not np.array_equal(out, exp):
268
- return None
269
-
270
- ch_mask = np.zeros((1, 10, 1, 1), dtype=np.float32)
271
- for c in keep:
272
- if 0 <= c < 10:
273
- ch_mask[0, c, 0, 0] = 1.0
274
-
275
- inits = [numpy_helper.from_array(ch_mask, 'ch_mask')]
276
- nodes = [helper.make_node('Mul', ['input', 'ch_mask'], ['output'])]
277
- return mk(nodes, inits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/submission.py DELETED
@@ -1,150 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Submission file generation and task running with W&B logging."""
3
-
4
- import os
5
- import csv
6
- import io
7
- import math
8
- import zipfile
9
- from collections import Counter
10
- from .profiler import score_network
11
- from .constants import MAX_ONNX_FILESIZE, EXCLUDED_TASKS
12
-
13
- try:
14
- import wandb
15
- except ImportError:
16
- wandb = None
17
-
18
-
19
- def run_tasks(task_nums, tasks, output_dir, providers, conv_budget, excluded_tasks, use_wandb):
20
- """Run all tasks and collect results.
21
-
22
- Returns: (results, costs_dict, total_score)
23
- """
24
- from .solvers.solver_registry import solve_task
25
-
26
- results = {}
27
- costs_dict = {}
28
- total_score = 0
29
-
30
- for tn in task_nums:
31
- if tn not in tasks:
32
- continue
33
-
34
- td = tasks[tn]['data']
35
- ok, sname, sz, t_task, model_path = solve_task(
36
- tn, td, output_dir, providers, conv_budget, excluded_tasks
37
- )
38
-
39
- if ok:
40
- macs, memory, params = score_network(model_path)
41
- if macs is None:
42
- macs, memory, params = 0, 0, 0
43
- cost = macs + memory + params
44
- score = max(1.0, 25.0 - math.log(max(1, cost)))
45
- total_score += score
46
-
47
- # Check per-file size limit
48
- if sz and sz > MAX_ONNX_FILESIZE:
49
- print(f"Task {tn:3d}: {sname:25s} OVER SIZE LIMIT ({sz:,} > {MAX_ONNX_FILESIZE:,})")
50
- continue
51
-
52
- results[tn] = (sname, t_task, sz)
53
- costs_dict[tn] = cost
54
- print(f"Task {tn:3d}: {sname:25s} {score:7.3f} {cost:>12} {t_task:7.3f}s ({sz:>8,} bytes)")
55
- else:
56
- score = 0
57
- cost = 0
58
- print(f"Task {tn:3d}: UNSOLVED {t_task:7.3f}s")
59
-
60
- if use_wandb and wandb is not None:
61
- wandb.log({
62
- "task_id": tn,
63
- "solver": sname if ok else "unsolved",
64
- "onnx_bytes": sz if ok else 0,
65
- "task_time_sec": t_task,
66
- "cost": cost,
67
- "score": score,
68
- })
69
-
70
- return results, costs_dict, total_score
71
-
72
-
73
- def generate_submission(output_dir, results, costs_dict, active_tasks):
74
- """Generate submission.zip and submission.csv.
75
-
76
- Returns dict with submission info.
77
- """
78
- n_files = len([f for f in os.listdir(output_dir) if f.endswith('.onnx')])
79
- total_size = sum(os.path.getsize(os.path.join(output_dir, f))
80
- for f in os.listdir(output_dir) if f.endswith('.onnx'))
81
-
82
- # Check per-file size limits
83
- oversized = []
84
- for f in os.listdir(output_dir):
85
- if f.endswith('.onnx'):
86
- fsize = os.path.getsize(os.path.join(output_dir, f))
87
- if fsize > MAX_ONNX_FILESIZE:
88
- oversized.append((f, fsize))
89
-
90
- # Create submission.zip
91
- parent_dir = os.path.dirname(output_dir) or '/kaggle/working/'
92
- zip_path = os.path.join(parent_dir, 'submission.zip')
93
- buf = io.BytesIO()
94
- with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
95
- for f in sorted(os.listdir(output_dir)):
96
- if f.endswith('.onnx'):
97
- zf.write(os.path.join(output_dir, f), f)
98
- zip_bytes = buf.getvalue()
99
- with open(zip_path, 'wb') as f:
100
- f.write(zip_bytes)
101
- zip_size = len(zip_bytes)
102
-
103
- # Create submission.csv
104
- csv_path = os.path.join(parent_dir, 'submission.csv')
105
- with open(csv_path, 'w', newline='') as f:
106
- w = csv.writer(f)
107
- w.writerow(['task_id', 'total_cost'])
108
- for tn in sorted(costs_dict.keys()):
109
- w.writerow([f'task{tn:03d}', costs_dict[tn]])
110
-
111
- unsolved_count = len(active_tasks) - len(results)
112
- total_score = sum(max(1.0, 25.0 - math.log(max(1, cost))) for cost in costs_dict.values())
113
- total_cost = sum(costs_dict.values())
114
-
115
- return {
116
- 'n_files': n_files,
117
- 'total_size': total_size,
118
- 'zip_path': zip_path,
119
- 'zip_size': zip_size,
120
- 'csv_path': csv_path,
121
- 'total_score': total_score,
122
- 'total_cost': total_cost,
123
- 'unsolved_count': unsolved_count,
124
- 'oversized': oversized,
125
- }
126
-
127
-
128
- def print_summary(results, submission_info, elapsed):
129
- """Print summary statistics."""
130
- active_count = submission_info['unsolved_count'] + len(results)
131
-
132
- print(f"\n{'=' * 70}")
133
- print(f"Solved: {len(results)}/{active_count} tasks in {elapsed:.0f}s")
134
- solver_names = [v[0] for v in results.values()]
135
- sc = Counter(solver_names)
136
- for s, c in sc.most_common():
137
- print(f" {s}: {c}")
138
-
139
- print(f"\n{submission_info['n_files']} ONNX files, {submission_info['total_size'] / 1024:.1f} KB uncompressed")
140
- print(f"ZIP size: {submission_info['zip_size'] / 1024:.1f} KB")
141
-
142
- if submission_info['oversized']:
143
- print(f"WARNING: {len(submission_info['oversized'])} files exceed 1.44MB limit:")
144
- for f, sz in submission_info['oversized']:
145
- print(f" {f}: {sz / 1024:.1f} KB")
146
-
147
- print(f"\nEstimated LB score: {submission_info['total_score']:.1f}")
148
- print(f"Total cost: {submission_info['total_cost']:,}")
149
- print(f"Solved: {len(results)} | Unsolved: {submission_info['unsolved_count']}")
150
- print(f"Written: {submission_info['zip_path']} | {submission_info['csv_path']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
neurogolf_solver/validators.py DELETED
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Model validation utilities.
3
-
4
- Validation order (matches Kaggle's verify_network):
5
- 1. File size ≤ 1.44MB
6
- 2. onnx.checker.check_model() — catches malformed graphs
7
- 3. No banned ops (UPPERCASE check to match Kaggle)
8
- 4. All tensor shapes are static (no dynamic dims)
9
- 5. onnxruntime.InferenceSession loads successfully
10
- 6. Correct outputs on train + test + arc-gen
11
- """
12
-
13
- import os
14
- import numpy as np
15
- import onnx
16
- import onnxruntime as ort
17
- from .data_loader import to_onehot
18
- from .constants import MAX_ARCGEN_VALIDATE, MAX_ONNX_FILESIZE, BANNED_OPS
19
-
20
- _BANNED_OPS_UPPER = {op.upper() for op in BANNED_OPS}
21
-
22
-
23
- def check_model_structure(path):
24
- """Check ONNX model structure: size, valid graph, no banned ops, static shapes.
25
- Returns (ok, error_message)."""
26
- # 1. File size
27
- try:
28
- fsize = os.path.getsize(path)
29
- except OSError:
30
- return False, f"File not found: {path}"
31
- if fsize > MAX_ONNX_FILESIZE:
32
- return False, f"File size {fsize} exceeds {MAX_ONNX_FILESIZE} ({fsize/1024:.1f} KB)"
33
-
34
- # 2. ONNX checker
35
- try:
36
- model = onnx.load(path)
37
- onnx.checker.check_model(model)
38
- except Exception as e:
39
- return False, f"onnx.checker failed: {e}"
40
-
41
- # 3. Banned ops (UPPERCASE comparison — matches Kaggle)
42
- for node in model.graph.node:
43
- if node.op_type.upper() in _BANNED_OPS_UPPER:
44
- return False, f"Banned op: {node.op_type}"
45
-
46
- # 4. Static shapes — all tensors must have fully defined shapes
47
- for inp in model.graph.input:
48
- if inp.type.HasField('tensor_type'):
49
- shape = inp.type.tensor_type.shape
50
- if shape:
51
- for dim in shape.dim:
52
- if not dim.dim_value and dim.dim_value != 0:
53
- if not dim.dim_param: # symbolic dim is also not static
54
- pass # dim_value=0 is valid (means unknown in some contexts)
55
- return False, f"Dynamic shape on input '{inp.name}': {[d.dim_value or d.dim_param for d in shape.dim]}"
56
-
57
- for out in model.graph.output:
58
- if out.type.HasField('tensor_type'):
59
- shape = out.type.tensor_type.shape
60
- if shape:
61
- for dim in shape.dim:
62
- if not dim.dim_value and dim.dim_value != 0:
63
- if not dim.dim_param:
64
- pass
65
- return False, f"Dynamic shape on output '{out.name}': {[d.dim_value or d.dim_param for d in shape.dim]}"
66
-
67
- return True, None
68
-
69
-
70
- def validate(path, td, providers):
71
- """Full validation: structure check + correct outputs on all splits.
72
- Returns False immediately if any check fails."""
73
- # Structure checks first
74
- ok, err = check_model_structure(path)
75
- if not ok:
76
- return False
77
-
78
- # Load and run inference
79
- try:
80
- opts = ort.SessionOptions()
81
- opts.log_severity_level = 3
82
- sess = ort.InferenceSession(path, sess_options=opts, providers=providers)
83
- except:
84
- return False
85
-
86
- examples = td['train'] + td['test']
87
- if 'arc-gen' in td:
88
- examples = examples + td['arc-gen'][:MAX_ARCGEN_VALIDATE]
89
- for ex in examples:
90
- inp = to_onehot(ex['input'])
91
- exp = to_onehot(ex['output'])
92
- try:
93
- out = sess.run(['output'], {'input': inp})[0]
94
- out = (out > 0.0).astype(np.float32)
95
- except:
96
- return False
97
- if not np.array_equal(out, exp):
98
- return False
99
- return True
100
-
101
-
102
- def validate_raw(raw_bytes, td, providers):
103
- """Validate ONNX model from raw bytes."""
104
- if len(raw_bytes) > MAX_ONNX_FILESIZE:
105
- return False
106
- try:
107
- opts = ort.SessionOptions()
108
- opts.log_severity_level = 3
109
- sess = ort.InferenceSession(raw_bytes, sess_options=opts, providers=providers)
110
- except:
111
- return False
112
- examples = td['train'] + td['test']
113
- if 'arc-gen' in td:
114
- examples = examples + td['arc-gen'][:MAX_ARCGEN_VALIDATE]
115
- for ex in examples:
116
- inp = to_onehot(ex['input'])
117
- exp = to_onehot(ex['output'])
118
- try:
119
- out = sess.run(['output'], {'input': inp})[0]
120
- out = (out > 0.0).astype(np.float32)
121
- except:
122
- return False
123
- if not np.array_equal(out, exp):
124
- return False
125
- return True