rogermt commited on
Commit
74cf870
·
verified ·
1 Parent(s): 81f74db

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
+