Create build_task363_onnx.py
Browse files
medal-solvers/build_tasks/build_task363_onnx.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
Compact ONNX model for ARC task 363.
|
| 4 |
+
|
| 5 |
+
Rule (verified on all 265 train/test/arc-gen examples; grids are all 10x10):
|
| 6 |
+
The input has a "template" drawn in colour 2 (within a box of colour-5 background) and
|
| 7 |
+
the rest of the grid is a colour-0 / colour-5 texture. Stamp the template (its 2-cells)
|
| 8 |
+
at every placement where the 2-cells land on colour-0 cells, subject to filtering:
|
| 9 |
+
valid : all template-2 cells align with colour-0 cells.
|
| 10 |
+
has_overlap : the placement's painted cells overlap another placement's.
|
| 11 |
+
touch_edge : the placement reaches the grid border.
|
| 12 |
+
dense : (fg dilated by the template-shape) count > 16 at that offset.
|
| 13 |
+
top2 : the placement lies entirely within the top two rows.
|
| 14 |
+
keep = valid AND (no_overlap OR touch_edge)
|
| 15 |
+
AND NOT (no_overlap AND not touch_edge AND dense AND top2)
|
| 16 |
+
Painted cells become colour 2; everything else unchanged.
|
| 17 |
+
|
| 18 |
+
Memory strategy (few intermediates, small dtypes, work in 10x10 / 19x19 offset space):
|
| 19 |
+
All channel reads come from a single colour-value map produced by one 1x1 Conv.
|
| 20 |
+
Placement search / overlap / edge / density / top2 are Conv / ConvTranspose in the
|
| 21 |
+
19x19 offset space (float16). Output is composed with one Equal at the end.
|
| 22 |
+
"""
|
| 23 |
+
import numpy as np
|
| 24 |
+
import onnx
|
| 25 |
+
from onnx import helper, TensorProto, numpy_helper
|
| 26 |
+
|
| 27 |
+
C = 10
|
| 28 |
+
G = 10 # actual grid size (all examples are 10x10)
|
| 29 |
+
F = TensorProto.FLOAT16
|
| 30 |
+
I64 = TensorProto.INT64
|
| 31 |
+
BOOL = TensorProto.BOOL
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def f16(name, arr):
|
| 35 |
+
return numpy_helper.from_array(np.asarray(arr, dtype=np.float16), name=name)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def i64(name, arr):
|
| 39 |
+
return numpy_helper.from_array(np.asarray(arr, dtype=np.int64), name=name)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def build_model():
|
| 43 |
+
n = []
|
| 44 |
+
I = []
|
| 45 |
+
|
| 46 |
+
# ---- constants ----
|
| 47 |
+
# read channel 2 and channel 5 (the only non-zero colours) as thin 10x10 slices
|
| 48 |
+
I.append(i64("sp_ax", [2, 3]))
|
| 49 |
+
I.append(i64("ch_ax", [1, 2, 3]))
|
| 50 |
+
I.append(i64("c2s", [2, 0, 0])); I.append(i64("c2e", [3, G, G]))
|
| 51 |
+
I.append(i64("c5s", [5, 0, 0])); I.append(i64("c5e", [6, G, G]))
|
| 52 |
+
I.append(f16("five16", [5.0]))
|
| 53 |
+
I.append(f16("v2", [2.0]))
|
| 54 |
+
I.append(f16("half", [0.5]))
|
| 55 |
+
I.append(f16("d16", [16.0]))
|
| 56 |
+
# colour-value 1x1 conv: cval = 1*ch0 + 2*ch2 + 5*ch5 ... actually we need: is2, is0, isFG.
|
| 57 |
+
# Use weights so cval encodes: ch0->1, ch2->2, ch5->5 (others 0). Then is2=(cval==2),
|
| 58 |
+
# is0=(cval==1)? no. Simpler: produce 3 derived maps with one Conv of 3 output channels:
|
| 59 |
+
# out0 = ch2 (template), out1 = ch0 (zero), out2 = sum(ch1..ch9) (foreground).
|
| 60 |
+
# 1x1 Conv reads only channels 2..5 (cval = 2*ch2 + 5*ch5; bg/zero => cval==0).
|
| 61 |
+
I.append(f16("one16", [1.0]))
|
| 62 |
+
|
| 63 |
+
# k3 ones for dilation
|
| 64 |
+
I.append(f16("k3", np.ones((1, 1, 3, 3), np.float16)))
|
| 65 |
+
# edge ring (10x10) and top-2-rows (10x10)
|
| 66 |
+
edge = np.zeros((1, 1, G, G), np.float16)
|
| 67 |
+
edge[0, 0, 0, :] = 1; edge[0, 0, -1, :] = 1; edge[0, 0, :, 0] = 1; edge[0, 0, :, -1] = 1
|
| 68 |
+
I.append(f16("edge10", edge))
|
| 69 |
+
top2 = np.zeros((1, 1, G, G), np.float16); top2[0, 0, 0:2, :] = 1
|
| 70 |
+
I.append(f16("top2_10", top2))
|
| 71 |
+
# pad 10 -> 28 (9 each side) for "full" convs with a 10x10 kernel
|
| 72 |
+
# crop 28 -> 10 (centre) for the painted result
|
| 73 |
+
# pad 10 -> 30 to restore full output grid
|
| 74 |
+
# output one-hot: uint8 colour index vs channel range
|
| 75 |
+
U8 = TensorProto.UINT8
|
| 76 |
+
I.append(numpy_helper.from_array(np.arange(C, dtype=np.uint8).reshape(1, C, 1, 1), "chan_vals_u8"))
|
| 77 |
+
I.append(numpy_helper.from_array(np.array(2, np.uint8).reshape(1, 1, 1, 1), "u2"))
|
| 78 |
+
I.append(numpy_helper.from_array(np.array(255, np.uint8).reshape(1, 1, 1, 1), "u255"))
|
| 79 |
+
# pad 10 -> 30 with sentinel 255 for the colour-index map
|
| 80 |
+
I.append(i64("pad_to30_idx", [0, 0, 0, 0, 0, 0, 20, 20]))
|
| 81 |
+
|
| 82 |
+
# ---- read only channel-2 (template) and channel-5 (texture) as thin 10x10 slices ----
|
| 83 |
+
n.append(helper.make_node("Slice", ["input", "c2s", "c2e", "ch_ax"], ["T_f32"])) # [1,1,10,10] f32 ch2
|
| 84 |
+
n.append(helper.make_node("Slice", ["input", "c5s", "c5e", "ch_ax"], ["F5_f32"])) # [1,1,10,10] f32 ch5
|
| 85 |
+
n.append(helper.make_node("Cast", ["T_f32"], ["T"], to=F)) # template (colour 2)
|
| 86 |
+
n.append(helper.make_node("Cast", ["F5_f32"], ["F5"], to=F)) # texture (colour 5)
|
| 87 |
+
n.append(helper.make_node("Add", ["T", "F5"], ["FG"])) # foreground = ch2|ch5 (mutually excl.)
|
| 88 |
+
n.append(helper.make_node("Sub", ["one16", "FG"], ["Z"])) # zero = 1 - fg
|
| 89 |
+
# colour value for output: 2*T + 5*F5
|
| 90 |
+
n.append(helper.make_node("Mul", ["F5", "five16"], ["c5v"]))
|
| 91 |
+
n.append(helper.make_node("Mul", ["T", "v2"], ["c2v"]))
|
| 92 |
+
n.append(helper.make_node("Add", ["c2v", "c5v"], ["cval"])) # 0 / 2 / 5
|
| 93 |
+
n.append(helper.make_node("ReduceSum", ["T", "sp_ax"], ["nt"], keepdims=1)) # template size
|
| 94 |
+
|
| 95 |
+
# ---- valid placements: Conv(zero, T, pad9) == nt (19x19 offset space) ----
|
| 96 |
+
n.append(helper.make_node("Conv", ["Z", "T"], ["match"], pads=[9, 9, 9, 9])) # [1,1,19,19]
|
| 97 |
+
n.append(helper.make_node("Equal", ["match", "nt"], ["valid_b"]))
|
| 98 |
+
n.append(helper.make_node("Cast", ["valid_b"], ["valid"], to=F))
|
| 99 |
+
|
| 100 |
+
# ---- overlap: ConvTranspose(valid, T, pads=9) -> cover[10x10]; Conv(cover, T, pad9) -> ov[19]; >nt ----
|
| 101 |
+
n.append(helper.make_node("ConvTranspose", ["valid", "T"], ["cover"], pads=[9, 9, 9, 9])) # [1,1,10,10]
|
| 102 |
+
n.append(helper.make_node("Conv", ["cover", "T"], ["ov"], pads=[9, 9, 9, 9])) # [1,1,19,19]
|
| 103 |
+
n.append(helper.make_node("Greater", ["ov", "nt"], ["has_ov"]))
|
| 104 |
+
n.append(helper.make_node("Not", ["has_ov"], ["no_ov"]))
|
| 105 |
+
|
| 106 |
+
# ---- touch edge: Conv(edge, T, pad9) > 0.5 ----
|
| 107 |
+
n.append(helper.make_node("Conv", ["edge10", "T"], ["ec"], pads=[9, 9, 9, 9]))
|
| 108 |
+
n.append(helper.make_node("Greater", ["ec", "half"], ["touch"]))
|
| 109 |
+
n.append(helper.make_node("Not", ["touch"], ["not_touch"]))
|
| 110 |
+
n.append(helper.make_node("Or", ["no_ov", "touch"], ["keep_ov"]))
|
| 111 |
+
|
| 112 |
+
# ---- dense: template_dilated = Conv(T, k3, pad1); Conv(fg_pad, Td) > 16 ----
|
| 113 |
+
n.append(helper.make_node("Conv", ["T", "k3"], ["Td"], pads=[1, 1, 1, 1])) # [1,1,10,10]
|
| 114 |
+
n.append(helper.make_node("Conv", ["FG", "Td"], ["nz8"], pads=[9, 9, 9, 9])) # [1,1,19,19]
|
| 115 |
+
n.append(helper.make_node("Greater", ["nz8", "d16"], ["dense"]))
|
| 116 |
+
|
| 117 |
+
# ---- top2: Conv(top2, T, pad9) == nt ----
|
| 118 |
+
n.append(helper.make_node("Conv", ["top2_10", "T"], ["t2c"], pads=[9, 9, 9, 9]))
|
| 119 |
+
n.append(helper.make_node("Equal", ["t2c", "nt"], ["all_top2"]))
|
| 120 |
+
|
| 121 |
+
# ---- top_false = (no_ov & not_touch) & (dense & all_top2) ; kept = valid & keep_ov & ~top_false
|
| 122 |
+
n.append(helper.make_node("And", ["dense", "all_top2"], ["dat"]))
|
| 123 |
+
n.append(helper.make_node("And", ["no_ov", "not_touch"], ["nont"]))
|
| 124 |
+
n.append(helper.make_node("And", ["dat", "nont"], ["top_false"]))
|
| 125 |
+
n.append(helper.make_node("Not", ["top_false"], ["not_tf"]))
|
| 126 |
+
n.append(helper.make_node("And", ["valid_b", "keep_ov"], ["kept_a"]))
|
| 127 |
+
n.append(helper.make_node("And", ["kept_a", "not_tf"], ["kept_b"]))
|
| 128 |
+
n.append(helper.make_node("Cast", ["kept_b"], ["kept"], to=F))
|
| 129 |
+
|
| 130 |
+
# ---- paint: ConvTranspose(kept, T, pads=9) -> 10x10 directly (no 28x28, no crop) ----
|
| 131 |
+
n.append(helper.make_node("ConvTranspose", ["kept", "T"], ["paint10"], pads=[9, 9, 9, 9]))
|
| 132 |
+
n.append(helper.make_node("Greater", ["paint10", "half"], ["paint_b"])) # [1,1,10,10] bool
|
| 133 |
+
|
| 134 |
+
# ---- compose output via a uint8 colour-index map (one small tensor + one Equal) ----
|
| 135 |
+
# colour_idx (10x10) = 2 where painted, else cval (0/2/5). Pad to 30x30 with 255
|
| 136 |
+
# (out-of-grid) so the final Equal yields all-False there. output one-hot = Equal.
|
| 137 |
+
n.append(helper.make_node("Cast", ["cval"], ["cval_u8"], to=U8)) # [1,1,10,10] u8
|
| 138 |
+
n.append(helper.make_node("Where", ["paint_b", "u2", "cval_u8"], ["cidx10"])) # [1,1,10,10] u8
|
| 139 |
+
n.append(helper.make_node("Pad", ["cidx10", "pad_to30_idx", "u255"], ["cidx30"])) # [1,1,30,30] u8
|
| 140 |
+
n.append(helper.make_node("Equal", ["chan_vals_u8", "cidx30"], ["output"])) # [1,C,30,30] bool
|
| 141 |
+
|
| 142 |
+
inp = helper.make_tensor_value_info("input", TensorProto.FLOAT, [1, C, 30, 30])
|
| 143 |
+
out = helper.make_tensor_value_info("output", BOOL, [1, C, 30, 30])
|
| 144 |
+
g = helper.make_graph(n, "task363", [inp], [out], I)
|
| 145 |
+
m = helper.make_model(g, opset_imports=[helper.make_operatorsetid("", 18)], ir_version=10)
|
| 146 |
+
onnx.checker.check_model(m)
|
| 147 |
+
return m
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def encode_grid(grid):
|
| 151 |
+
g = np.array(grid, dtype=np.int64)
|
| 152 |
+
h, w = g.shape
|
| 153 |
+
oh = np.zeros((1, C, 30, 30), dtype=np.float32)
|
| 154 |
+
for r in range(h):
|
| 155 |
+
for c in range(w):
|
| 156 |
+
oh[0, int(g[r, c]), r, c] = 1.0
|
| 157 |
+
return oh
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def verify(onnx_path, task_json):
|
| 161 |
+
import json, onnxruntime as ort
|
| 162 |
+
data = json.load(open(task_json))
|
| 163 |
+
sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
|
| 164 |
+
total = passed = 0
|
| 165 |
+
fails = []
|
| 166 |
+
for split in ("train", "test", "arc-gen"):
|
| 167 |
+
for i, pair in enumerate(data.get(split, [])):
|
| 168 |
+
inp, tgt = pair["input"], pair["output"]
|
| 169 |
+
if max(len(inp), max((len(r) for r in inp), default=0)) > 30:
|
| 170 |
+
continue
|
| 171 |
+
y = sess.run(["output"], {"input": encode_grid(inp)})[0]
|
| 172 |
+
if np.array_equal((y > 0.0).astype(np.float32), encode_grid(tgt)):
|
| 173 |
+
passed += 1
|
| 174 |
+
else:
|
| 175 |
+
fails.append((split, i))
|
| 176 |
+
total += 1
|
| 177 |
+
print(f"correctness: {passed}/{total} pass" + (f" FAILS={fails[:10]}" if fails else ""))
|
| 178 |
+
return passed == total
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
import argparse, os
|
| 183 |
+
ap = argparse.ArgumentParser(description="Build and verify Task 363 ONNX model.")
|
| 184 |
+
ap.add_argument("--input-json", default="/home/user/uploads/task363.json")
|
| 185 |
+
ap.add_argument("--output-onnx", default="task363.onnx")
|
| 186 |
+
ap.add_argument("--no-verify", action="store_true")
|
| 187 |
+
args = ap.parse_args()
|
| 188 |
+
m = build_model()
|
| 189 |
+
onnx.save(m, args.output_onnx)
|
| 190 |
+
print(f"saved {args.output_onnx}: {os.path.getsize(args.output_onnx)} bytes, "
|
| 191 |
+
f"{len(m.graph.node)} nodes")
|
| 192 |
+
if not args.no_verify and os.path.exists(args.input_json):
|
| 193 |
+
verify(args.output_onnx, args.input_json)
|
| 194 |
+
|