Add task277 ONNX build script (266/266 PASS, score 14.85 vs base 13.30, gain +1.55)
Browse files
medal-solvers/build_task277_onnx.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build ONNX model for Task 277 — Smallest CC recoloring. V2 compact.
|
| 2 |
+
|
| 3 |
+
Key facts: Always exactly 3 CCs, gap≥2, no ties.
|
| 4 |
+
|
| 5 |
+
Approach:
|
| 6 |
+
1. MaxPool label propagation → 3 unique labels
|
| 7 |
+
2. Find 3 labels: max, second max, third max
|
| 8 |
+
3. Count cells per label
|
| 9 |
+
4. Min count → color 2, others → color 1
|
| 10 |
+
"""
|
| 11 |
+
import numpy as np
|
| 12 |
+
import onnx
|
| 13 |
+
from onnx import helper, numpy_helper, TensorProto
|
| 14 |
+
import math
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def build_task277():
|
| 19 |
+
nodes, inits, vis = [], [], []
|
| 20 |
+
counter = [0]
|
| 21 |
+
|
| 22 |
+
def nm():
|
| 23 |
+
counter[0] += 1
|
| 24 |
+
return f"t{counter[0]}"
|
| 25 |
+
|
| 26 |
+
def const(name, val, dtype='f'):
|
| 27 |
+
arr = np.array(val, dtype=np.float32 if dtype == 'f' else np.int64)
|
| 28 |
+
inits.append(numpy_helper.from_array(arr, name))
|
| 29 |
+
|
| 30 |
+
def vi(name, shape, dt=TensorProto.FLOAT):
|
| 31 |
+
vis.append(helper.make_tensor_value_info(name, dt, shape))
|
| 32 |
+
|
| 33 |
+
def nd(op, ins, outs_shapes, **kwargs):
|
| 34 |
+
out_names = []
|
| 35 |
+
for sd in outs_shapes:
|
| 36 |
+
if isinstance(sd, tuple):
|
| 37 |
+
shape, dt = sd
|
| 38 |
+
else:
|
| 39 |
+
shape, dt = sd, TensorProto.FLOAT
|
| 40 |
+
n = nm()
|
| 41 |
+
vi(n, shape, dt)
|
| 42 |
+
out_names.append(n)
|
| 43 |
+
nodes.append(helper.make_node(op, ins, out_names, **kwargs))
|
| 44 |
+
return out_names[0] if len(out_names) == 1 else out_names
|
| 45 |
+
|
| 46 |
+
# === CONSTANTS ===
|
| 47 |
+
const('c_one', [1.0])
|
| 48 |
+
const('c_zero', [0.0])
|
| 49 |
+
const('c_half', [0.5])
|
| 50 |
+
const('c_big', [200.0])
|
| 51 |
+
|
| 52 |
+
const('sl_start', [0, 0, 0, 0], 'i')
|
| 53 |
+
const('sl_end', [1, 10, 10, 10], 'i')
|
| 54 |
+
const('sl_axes', [0, 1, 2, 3], 'i')
|
| 55 |
+
const('sl_ch8_start', [0, 8, 0, 0], 'i')
|
| 56 |
+
const('sl_ch8_end', [1, 9, 10, 10], 'i')
|
| 57 |
+
|
| 58 |
+
# Label grid: unique per cell (1-100)
|
| 59 |
+
const('label_init', np.arange(1, 101, dtype=np.float32).reshape(1, 1, 10, 10))
|
| 60 |
+
const('axes23', [2, 3], 'i')
|
| 61 |
+
|
| 62 |
+
const('pad_10_to_30', [0, 0, 0, 0, 0, 0, 20, 20], 'i')
|
| 63 |
+
const('pad_val_zero', [0.0])
|
| 64 |
+
const('zeros_7ch', np.zeros((1, 7, 10, 10), dtype=np.float32))
|
| 65 |
+
|
| 66 |
+
# === STEP 1: Get mask ===
|
| 67 |
+
inp10 = nd('Slice', ['input', 'sl_start', 'sl_end', 'sl_axes'], [[1, 10, 10, 10]])
|
| 68 |
+
mask8 = nd('Slice', [inp10, 'sl_ch8_start', 'sl_ch8_end', 'sl_axes'], [[1, 1, 10, 10]])
|
| 69 |
+
|
| 70 |
+
# === STEP 2: Label propagation ===
|
| 71 |
+
labels = nd('Mul', [mask8, 'label_init'], [[1, 1, 10, 10]])
|
| 72 |
+
for _ in range(10):
|
| 73 |
+
pooled = nd('MaxPool', [labels], [[1, 1, 10, 10]], kernel_shape=[3, 3], pads=[1, 1, 1, 1])
|
| 74 |
+
labels = nd('Mul', [pooled, mask8], [[1, 1, 10, 10]])
|
| 75 |
+
|
| 76 |
+
# === STEP 3: Find 3 unique labels ===
|
| 77 |
+
# L1 = max of all labels
|
| 78 |
+
L1 = nd('ReduceMax', [labels, 'axes23'], [[1, 1, 1, 1]], keepdims=1)
|
| 79 |
+
|
| 80 |
+
# Mask out L1: set cells with label==L1 to 0
|
| 81 |
+
diff_L1 = nd('Abs', [nd('Sub', [labels, L1], [[1, 1, 10, 10]])], [[1, 1, 10, 10]])
|
| 82 |
+
is_L1_b = nd('Less', [diff_L1, 'c_half'], [([1, 1, 10, 10], TensorProto.BOOL)])
|
| 83 |
+
is_L1 = nd('Cast', [is_L1_b], [[1, 1, 10, 10]], to=1)
|
| 84 |
+
not_L1 = nd('Sub', ['c_one', is_L1], [[1, 1, 10, 10]])
|
| 85 |
+
labels_no_L1 = nd('Mul', [labels, not_L1], [[1, 1, 10, 10]])
|
| 86 |
+
|
| 87 |
+
# L2 = max of remaining labels
|
| 88 |
+
L2 = nd('ReduceMax', [labels_no_L1, 'axes23'], [[1, 1, 1, 1]], keepdims=1)
|
| 89 |
+
|
| 90 |
+
# Mask out L2
|
| 91 |
+
diff_L2 = nd('Abs', [nd('Sub', [labels, L2], [[1, 1, 10, 10]])], [[1, 1, 10, 10]])
|
| 92 |
+
is_L2_b = nd('Less', [diff_L2, 'c_half'], [([1, 1, 10, 10], TensorProto.BOOL)])
|
| 93 |
+
is_L2 = nd('Cast', [is_L2_b], [[1, 1, 10, 10]], to=1)
|
| 94 |
+
not_L2 = nd('Sub', ['c_one', is_L2], [[1, 1, 10, 10]])
|
| 95 |
+
labels_no_L1_L2 = nd('Mul', [labels_no_L1, not_L2], [[1, 1, 10, 10]])
|
| 96 |
+
|
| 97 |
+
# L3 mask is just: mask8 - is_L1 - is_L2
|
| 98 |
+
is_L3 = nd('Sub', [mask8, nd('Add', [is_L1, is_L2], [[1, 1, 10, 10]])], [[1, 1, 10, 10]])
|
| 99 |
+
|
| 100 |
+
# === STEP 4: Count cells per CC ===
|
| 101 |
+
count_L1 = nd('ReduceSum', [is_L1, 'axes23'], [[1, 1, 1, 1]], keepdims=1)
|
| 102 |
+
count_L2 = nd('ReduceSum', [is_L2, 'axes23'], [[1, 1, 1, 1]], keepdims=1)
|
| 103 |
+
count_L3 = nd('ReduceSum', [is_L3, 'axes23'], [[1, 1, 1, 1]], keepdims=1)
|
| 104 |
+
|
| 105 |
+
# === STEP 5: Find minimum count ===
|
| 106 |
+
min_12 = nd('Min', [count_L1, count_L2], [[1, 1, 1, 1]])
|
| 107 |
+
min_count = nd('Min', [min_12, count_L3], [[1, 1, 1, 1]])
|
| 108 |
+
|
| 109 |
+
# Which CC has this count? (no ties guaranteed)
|
| 110 |
+
diff_c1 = nd('Abs', [nd('Sub', [count_L1, min_count], [[1, 1, 1, 1]])], [[1, 1, 1, 1]])
|
| 111 |
+
smallest_is_L1_b = nd('Less', [diff_c1, 'c_half'], [([1, 1, 1, 1], TensorProto.BOOL)])
|
| 112 |
+
smallest_is_L1 = nd('Cast', [smallest_is_L1_b], [[1, 1, 1, 1]], to=1)
|
| 113 |
+
|
| 114 |
+
diff_c2 = nd('Abs', [nd('Sub', [count_L2, min_count], [[1, 1, 1, 1]])], [[1, 1, 1, 1]])
|
| 115 |
+
smallest_is_L2_b = nd('Less', [diff_c2, 'c_half'], [([1, 1, 1, 1], TensorProto.BOOL)])
|
| 116 |
+
smallest_is_L2 = nd('Cast', [smallest_is_L2_b], [[1, 1, 1, 1]], to=1)
|
| 117 |
+
|
| 118 |
+
diff_c3 = nd('Abs', [nd('Sub', [count_L3, min_count], [[1, 1, 1, 1]])], [[1, 1, 1, 1]])
|
| 119 |
+
smallest_is_L3_b = nd('Less', [diff_c3, 'c_half'], [([1, 1, 1, 1], TensorProto.BOOL)])
|
| 120 |
+
smallest_is_L3 = nd('Cast', [smallest_is_L3_b], [[1, 1, 1, 1]], to=1)
|
| 121 |
+
|
| 122 |
+
# === STEP 6: Build smallest mask and other mask ===
|
| 123 |
+
sm1 = nd('Mul', [is_L1, smallest_is_L1], [[1, 1, 10, 10]])
|
| 124 |
+
sm2 = nd('Mul', [is_L2, smallest_is_L2], [[1, 1, 10, 10]])
|
| 125 |
+
sm3 = nd('Mul', [is_L3, smallest_is_L3], [[1, 1, 10, 10]])
|
| 126 |
+
smallest_mask = nd('Add', [sm1, nd('Add', [sm2, sm3], [[1, 1, 10, 10]])], [[1, 1, 10, 10]])
|
| 127 |
+
other_mask = nd('Sub', [mask8, smallest_mask], [[1, 1, 10, 10]])
|
| 128 |
+
|
| 129 |
+
# === STEP 7: Build output ===
|
| 130 |
+
bg_mask = nd('Sub', ['c_one', mask8], [[1, 1, 10, 10]])
|
| 131 |
+
out_10 = nd('Concat', [bg_mask, other_mask, smallest_mask, 'zeros_7ch'], [[1, 10, 10, 10]], axis=1)
|
| 132 |
+
|
| 133 |
+
# === STEP 8: Pad — output directly ===
|
| 134 |
+
nodes.append(helper.make_node('Pad', [out_10, 'pad_10_to_30', 'pad_val_zero'], ['output']))
|
| 135 |
+
|
| 136 |
+
x = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1, 10, 30, 30])
|
| 137 |
+
y = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1, 10, 30, 30])
|
| 138 |
+
|
| 139 |
+
graph = helper.make_graph(nodes, 'task277', [x], [y], initializer=inits, value_info=vis)
|
| 140 |
+
model = helper.make_model(graph, ir_version=10, opset_imports=[helper.make_opsetid('', 18)])
|
| 141 |
+
return model
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
if __name__ == '__main__':
|
| 145 |
+
import onnxruntime as ort
|
| 146 |
+
import json
|
| 147 |
+
|
| 148 |
+
print("Building Task 277 ONNX model v2...")
|
| 149 |
+
model = build_task277()
|
| 150 |
+
|
| 151 |
+
output_path = '/app/task277.onnx'
|
| 152 |
+
del model.graph.value_info[:]
|
| 153 |
+
model = onnx.shape_inference.infer_shapes(model, strict_mode=True)
|
| 154 |
+
onnx.save(model, output_path)
|
| 155 |
+
|
| 156 |
+
print(f" Nodes: {len(model.graph.node)}")
|
| 157 |
+
print(f" File size: {os.path.getsize(output_path):,} bytes")
|
| 158 |
+
|
| 159 |
+
sess = ort.InferenceSession(output_path)
|
| 160 |
+
with open('/app/task-data/task277.json') as f:
|
| 161 |
+
data = json.load(f)
|
| 162 |
+
|
| 163 |
+
all_examples = data['train'] + data['test'] + data.get('arc-gen', [])
|
| 164 |
+
right_count, wrong_count = 0, 0
|
| 165 |
+
for i, ex in enumerate(all_examples):
|
| 166 |
+
inp = np.zeros((1, 10, 30, 30), dtype=np.float32)
|
| 167 |
+
for r, row in enumerate(ex['input']):
|
| 168 |
+
for c, v in enumerate(row):
|
| 169 |
+
if r < 30 and c < 30:
|
| 170 |
+
inp[0][v][r][c] = 1.0
|
| 171 |
+
result = sess.run(['output'], {'input': inp})
|
| 172 |
+
out = (result[0] > 0.0).astype(float)
|
| 173 |
+
exp = np.zeros((1, 10, 30, 30), dtype=np.float32)
|
| 174 |
+
for r, row in enumerate(ex['output']):
|
| 175 |
+
for c, v in enumerate(row):
|
| 176 |
+
if r < 30 and c < 30:
|
| 177 |
+
exp[0][v][r][c] = 1.0
|
| 178 |
+
if np.array_equal(out, exp):
|
| 179 |
+
right_count += 1
|
| 180 |
+
else:
|
| 181 |
+
wrong_count += 1
|
| 182 |
+
if wrong_count <= 5:
|
| 183 |
+
print(f" FAIL {i}: {len(np.where(out != exp)[0])} diffs")
|
| 184 |
+
|
| 185 |
+
print(f" Results: {right_count}/{right_count+wrong_count} pass ({wrong_count} fail)")
|
| 186 |
+
|
| 187 |
+
if wrong_count == 0:
|
| 188 |
+
params = sum(int(np.prod(init.dims)) for init in model.graph.initializer)
|
| 189 |
+
mem_est = sum(int(np.prod([d.dim_value for d in vi.type.tensor_type.shape.dim])) *
|
| 190 |
+
np.dtype(onnx.helper.tensor_dtype_to_np_dtype(vi.type.tensor_type.elem_type)).itemsize
|
| 191 |
+
for vi in model.graph.value_info
|
| 192 |
+
if vi.type.HasField('tensor_type') and vi.type.tensor_type.HasField('shape'))
|
| 193 |
+
score_est = max(1.0, 25.0 - math.log(max(1.0, mem_est + params)))
|
| 194 |
+
print(f" Params: {params:,}, Memory: {mem_est:,}, Score: {score_est:.3f}")
|
| 195 |
+
print(f" Base: 13.30. Gain: {score_est - 13.30:+.3f}")
|