rogermt commited on
Commit
f7d4497
·
verified ·
1 Parent(s): 1a9b50e

Add build_submission.py - single script to generate submission from pre-built models

Browse files
Files changed (1) hide show
  1. medal-solvers/build_submission.py +342 -0
medal-solvers/build_submission.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ build_submission.py — Generates the full submission zip for NeuroGolf.
4
+
5
+ Usage (Kaggle notebook):
6
+ python build_submission.py \
7
+ --base /kaggle/input/competitions/neurogolf-2026/submission-6043.zip \
8
+ --task-data-dir /kaggle/input/competitions/neurogolf-2026 \
9
+ --wave22 /path/to/wave22.py \
10
+ --optimized-dir /path/to/medal-solvers/optimized \
11
+ --output /kaggle/working/submission.zip
12
+
13
+ What it does:
14
+ 1. Loads pre-built .onnx files from --optimized-dir (the 7 hand-crafted + 16 wave22)
15
+ 2. Optionally builds MORE wave22 models on-the-fly (if --wave22 given)
16
+ 3. Validates every replacement model (100% pass required)
17
+ 4. Creates submission zip: base + all valid replacements
18
+
19
+ Arguments:
20
+ --base Path to the base submission zip (submission-6043.zip)
21
+ --task-data-dir Directory containing taskNNN.json files
22
+ --optimized-dir Directory with pre-built .onnx files to swap in
23
+ --wave22 Path to wave22.py to build additional models (optional)
24
+ --output Output zip path (default: submission.zip)
25
+ --skip-validation Skip example validation (not recommended)
26
+
27
+ Minimal Kaggle usage (just swap pre-built models):
28
+ python build_submission.py \
29
+ --base submission-6043.zip \
30
+ --task-data-dir /kaggle/input/competitions/neurogolf-2026 \
31
+ --optimized-dir optimized \
32
+ --output /kaggle/working/submission.zip
33
+ """
34
+ import argparse
35
+ import os
36
+ import sys
37
+ import json
38
+ import zipfile
39
+ import math
40
+ import numpy as np
41
+ import onnx
42
+ from onnx import helper as oh, numpy_helper as onh, TensorProto
43
+ import onnxruntime as ort
44
+ import zlib
45
+ import base64
46
+ import re
47
+
48
+
49
+ def _d(b64, shape, dtype):
50
+ """Decode base64+zlib compressed numpy array (used by wave22)."""
51
+ return np.frombuffer(zlib.decompress(base64.b64decode(b64)), dtype=dtype).reshape(shape)
52
+
53
+
54
+ def validate_model(model_path, task_data_dir, task_num):
55
+ """Validate model passes ALL examples. Returns (right, wrong)."""
56
+ task_file = os.path.join(task_data_dir, f'task{task_num:03d}.json')
57
+ if not os.path.exists(task_file):
58
+ return None, None
59
+
60
+ with open(task_file) as f:
61
+ data = json.load(f)
62
+
63
+ sess = ort.InferenceSession(model_path)
64
+ all_ex = data.get('train', []) + data.get('test', []) + data.get('arc-gen', [])
65
+
66
+ right, wrong = 0, 0
67
+ for ex in all_ex:
68
+ inp_grid = ex['input']
69
+ if max(len(inp_grid), max((len(r) for r in inp_grid), default=0)) > 30:
70
+ continue
71
+ inp = np.zeros((1, 10, 30, 30), dtype=np.float32)
72
+ for r, row in enumerate(inp_grid):
73
+ for c, v in enumerate(row):
74
+ if r < 30 and c < 30:
75
+ inp[0][v][r][c] = 1.0
76
+ exp = np.zeros((1, 10, 30, 30), dtype=np.float32)
77
+ for r, row in enumerate(ex['output']):
78
+ for c, v in enumerate(row):
79
+ if r < 30 and c < 30:
80
+ exp[0][v][r][c] = 1.0
81
+
82
+ result = sess.run(['output'], {'input': inp})
83
+ out = (result[0] > 0.0).astype(float)
84
+ if np.array_equal(out, exp):
85
+ right += 1
86
+ else:
87
+ wrong += 1
88
+ return right, wrong
89
+
90
+
91
+ def score_model_static(model):
92
+ """Static score approximation from value_info + initializers."""
93
+ try:
94
+ mi = onnx.shape_inference.infer_shapes(model, strict_mode=False)
95
+ except:
96
+ mi = model
97
+ g = mi.graph
98
+ ini = {i.name for i in g.initializer}
99
+ mem = 0
100
+ for vi in g.value_info:
101
+ if vi.name in ini:
102
+ continue
103
+ if not vi.type.HasField('tensor_type'):
104
+ continue
105
+ tt = vi.type.tensor_type
106
+ if not tt.HasField('shape'):
107
+ continue
108
+ n = 1
109
+ ok = True
110
+ for d in tt.shape.dim:
111
+ if d.HasField('dim_value') and d.dim_value > 0:
112
+ n *= d.dim_value
113
+ else:
114
+ ok = False
115
+ break
116
+ if not ok:
117
+ continue
118
+ dt = onnx.helper.tensor_dtype_to_np_dtype(tt.elem_type)
119
+ mem += n * np.dtype(dt).itemsize
120
+ par = 0
121
+ for i in g.initializer:
122
+ if any(d <= 0 for d in i.dims):
123
+ return None
124
+ par += math.prod(i.dims) if i.dims else 1
125
+ for nd in g.node:
126
+ if nd.op_type == 'Constant':
127
+ for a in nd.attribute:
128
+ if a.name == 'value':
129
+ par += math.prod(a.t.dims) if a.t.dims else 1
130
+ elif a.name == 'value_floats':
131
+ par += len(a.floats)
132
+ elif a.name == 'value_ints':
133
+ par += len(a.ints)
134
+ if mem + par == 0:
135
+ return None
136
+ return max(1.0, 25.0 - math.log(max(1.0, mem + par)))
137
+
138
+
139
+ def load_task_data(task_data_dir, task_num):
140
+ """Load task JSON data."""
141
+ path = os.path.join(task_data_dir, f'task{task_num:03d}.json')
142
+ if not os.path.exists(path):
143
+ return None
144
+ with open(path) as f:
145
+ data = json.load(f)
146
+ if 'arc-gen' not in data:
147
+ data['arc-gen'] = []
148
+ return data
149
+
150
+
151
+ def build_wave22_models(wave22_path, task_data_dir, output_dir, base_zip_path):
152
+ """Build wave22 models that improve over base. Returns dict {task_num: path}."""
153
+ if not os.path.exists(wave22_path):
154
+ print(f" WARNING: wave22.py not found at {wave22_path}, skipping")
155
+ return {}
156
+
157
+ with open(wave22_path) as f:
158
+ wave22_code = f.read()
159
+
160
+ wave22_ns = {
161
+ 'np': np, 'zlib': zlib, 'base64': base64,
162
+ 'oh': oh, 'onh': onh, 'TensorProto': TensorProto,
163
+ '_d': _d
164
+ }
165
+ exec(wave22_code, wave22_ns)
166
+
167
+ funcs = re.findall(r'def (s_\w+)\(td\):\s*\n\s*"""Task (\d+)', wave22_code)
168
+ print(f" Found {len(funcs)} wave22 functions")
169
+
170
+ # Score base models
171
+ base_scores = {}
172
+ with zipfile.ZipFile(base_zip_path, 'r') as zf:
173
+ for name in zf.namelist():
174
+ if name.startswith('task') and name.endswith('.onnx'):
175
+ try:
176
+ tn = int(name[4:7])
177
+ data = zf.read(name)
178
+ tmp = os.path.join(output_dir, f'_base_{name}')
179
+ with open(tmp, 'wb') as f:
180
+ f.write(data)
181
+ m = onnx.load(tmp)
182
+ s = score_model_static(m)
183
+ if s:
184
+ base_scores[tn] = s
185
+ os.remove(tmp)
186
+ except:
187
+ pass
188
+
189
+ skip_tasks = {53, 77, 98, 100, 291, 307, 398}
190
+ models = {}
191
+ file_limit = 1.44 * 1024 * 1024
192
+
193
+ for func_name, task_str in funcs:
194
+ tn = int(task_str)
195
+ if tn in skip_tasks:
196
+ continue
197
+
198
+ td = load_task_data(task_data_dir, tn)
199
+ if td is None:
200
+ continue
201
+
202
+ try:
203
+ model = wave22_ns[func_name](td)
204
+ if model is None:
205
+ continue
206
+
207
+ out_path = os.path.join(output_dir, f'task{tn:03d}.onnx')
208
+ onnx.save(model, out_path)
209
+ fsize = os.path.getsize(out_path)
210
+
211
+ if fsize > file_limit:
212
+ os.remove(out_path)
213
+ continue
214
+
215
+ wave_score = score_model_static(model)
216
+ base_score = base_scores.get(tn)
217
+ if wave_score is None or base_score is None:
218
+ os.remove(out_path)
219
+ continue
220
+
221
+ if wave_score <= base_score:
222
+ os.remove(out_path)
223
+ continue
224
+
225
+ right, wrong = validate_model(out_path, task_data_dir, tn)
226
+ if wrong != 0 or right is None:
227
+ os.remove(out_path)
228
+ continue
229
+
230
+ gain = wave_score - base_score
231
+ models[tn] = out_path
232
+ print(f" task{tn:03d}: +{gain:.3f} ({base_score:.3f} -> {wave_score:.3f}), "
233
+ f"{len(model.graph.node)} nodes, {right}/{right+wrong} PASS")
234
+
235
+ except Exception:
236
+ p = os.path.join(output_dir, f'task{tn:03d}.onnx')
237
+ if os.path.exists(p):
238
+ os.remove(p)
239
+
240
+ return models
241
+
242
+
243
+ def create_submission(base_zip_path, replacements, output_path):
244
+ """Create submission zip with replacements."""
245
+ os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
246
+
247
+ with zipfile.ZipFile(base_zip_path, 'r') as base_zip:
248
+ with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as out_zip:
249
+ replaced = []
250
+ for item in base_zip.namelist():
251
+ basename = os.path.basename(item)
252
+ if basename.startswith('task') and basename.endswith('.onnx'):
253
+ try:
254
+ tn = int(basename[4:7])
255
+ if tn in replacements:
256
+ out_zip.write(replacements[tn], basename)
257
+ replaced.append(tn)
258
+ continue
259
+ except ValueError:
260
+ pass
261
+ data = base_zip.read(item)
262
+ out_zip.writestr(basename, data)
263
+
264
+ print(f"\n Submission: {output_path}")
265
+ print(f" Size: {os.path.getsize(output_path):,} bytes")
266
+ print(f" Replaced {len(replaced)} models: {sorted(replaced)}")
267
+
268
+
269
+ def main():
270
+ parser = argparse.ArgumentParser(description="Build NeuroGolf submission")
271
+ parser.add_argument('--base', required=True,
272
+ help='Path to base submission zip (submission-6043.zip)')
273
+ parser.add_argument('--task-data-dir', required=True,
274
+ help='Directory with taskNNN.json files')
275
+ parser.add_argument('--wave22', default=None,
276
+ help='Path to wave22.py (optional, builds additional models)')
277
+ parser.add_argument('--optimized-dir', default=None,
278
+ help='Directory with pre-built optimized .onnx files')
279
+ parser.add_argument('--output', default='submission.zip',
280
+ help='Output submission zip path')
281
+ parser.add_argument('--skip-validation', action='store_true',
282
+ help='Skip validation (not recommended)')
283
+ args = parser.parse_args()
284
+
285
+ if not os.path.exists(args.base):
286
+ print(f"ERROR: Base zip not found: {args.base}")
287
+ sys.exit(1)
288
+ if not os.path.exists(args.task_data_dir):
289
+ print(f"ERROR: Task data dir not found: {args.task_data_dir}")
290
+ sys.exit(1)
291
+
292
+ replacements = {}
293
+ file_limit = 1.44 * 1024 * 1024
294
+
295
+ # --- Step 1: Load pre-built optimized models ---
296
+ if args.optimized_dir and os.path.isdir(args.optimized_dir):
297
+ print(f"[1] Loading pre-built models from {args.optimized_dir}")
298
+ for f in sorted(os.listdir(args.optimized_dir)):
299
+ if f.startswith('task') and f.endswith('.onnx'):
300
+ tn = int(f[4:7])
301
+ path = os.path.join(args.optimized_dir, f)
302
+ fsize = os.path.getsize(path)
303
+ if fsize > file_limit:
304
+ print(f" SKIP {f}: exceeds size limit ({fsize:,} bytes)")
305
+ continue
306
+ if not args.skip_validation:
307
+ right, wrong = validate_model(path, args.task_data_dir, tn)
308
+ if wrong != 0 or right is None:
309
+ print(f" SKIP {f}: validation failed ({wrong} wrong)")
310
+ continue
311
+ print(f" {f}: {right}/{right+wrong} PASS")
312
+ else:
313
+ print(f" {f}: loaded (validation skipped)")
314
+ replacements[tn] = path
315
+ print(f" Total from optimized-dir: {len(replacements)}")
316
+ else:
317
+ print("[1] No --optimized-dir provided, skipping pre-built models")
318
+
319
+ # --- Step 2: Build wave22 models ---
320
+ if args.wave22:
321
+ print(f"\n[2] Building wave22 models from {args.wave22}")
322
+ tmp_dir = os.path.join(os.path.dirname(args.output) or '.', '_wave22_tmp')
323
+ os.makedirs(tmp_dir, exist_ok=True)
324
+ wave22_models = build_wave22_models(
325
+ args.wave22, args.task_data_dir, tmp_dir, args.base)
326
+ added = 0
327
+ for tn, path in wave22_models.items():
328
+ if tn not in replacements:
329
+ replacements[tn] = path
330
+ added += 1
331
+ print(f" Added from wave22: {added} new models")
332
+ else:
333
+ print("\n[2] No --wave22 provided, skipping wave22 extraction")
334
+
335
+ # --- Step 3: Create submission ---
336
+ print(f"\n[3] Creating submission zip")
337
+ create_submission(args.base, replacements, args.output)
338
+ print("\nDone!")
339
+
340
+
341
+ if __name__ == '__main__':
342
+ main()