rogermt commited on
Commit
e3216be
·
verified ·
1 Parent(s): 05ef2ec

Add gravity, edge detect, mode fill solvers (v5.2)

Browse files

New solvers in new_solvers.py:
- gravity_unrolled: bubble-sort via Conv+Where, 4 directions, any bg color
Validated: Task 78 solved (score 8.399)
- edge_detect: Laplacian conv + threshold (0 matches in current task set)
- mode_fill: ReduceSum histogram + ArgMax + Expand
Validated: Task 129 solved (score 19.451)

Full 400-task run: 52 solved (was 49), score 709.5, est LB 1057.5
No regressions on existing 49 tasks."

neurogolf_solver/solvers/new_solvers.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """New solver architectures: gravity, edge detection, mode fill.
3
+
4
+ These use ONNX ops beyond Conv+lstsq to handle tasks that require
5
+ non-local operations (directional propagation, boundary detection,
6
+ global aggregation).
7
+
8
+ v5.2 (2026-04-26): gravity_unrolled solves Task 78, mode_fill solves Task 129.
9
+ """
10
+
11
+ import numpy as np
12
+ from onnx import helper, numpy_helper, TensorProto
13
+ from ..onnx_helpers import mk, _make_int64_init, _build_pad_node, _build_slice_crop, add_onehot_block
14
+ from ..data_loader import get_exs, fixed_shapes
15
+ from ..constants import GH, GW
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Gravity solver — unrolled bubble-sort via Conv + Where
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def _gravity_np(grid, direction, bg_color=0):
23
+ """Apply gravity in numpy for verification."""
24
+ r = np.full_like(grid, bg_color)
25
+ h, w = grid.shape
26
+ if direction == 'down':
27
+ for c in range(w):
28
+ nz = grid[:, c][grid[:, c] != bg_color]
29
+ r[h - len(nz):h, c] = nz
30
+ elif direction == 'up':
31
+ for c in range(w):
32
+ nz = grid[:, c][grid[:, c] != bg_color]
33
+ r[:len(nz), c] = nz
34
+ elif direction == 'right':
35
+ for rr in range(h):
36
+ nz = grid[rr, :][grid[rr, :] != bg_color]
37
+ r[rr, w - len(nz):w] = nz
38
+ elif direction == 'left':
39
+ for rr in range(h):
40
+ nz = grid[rr, :][grid[rr, :] != bg_color]
41
+ r[rr, :len(nz)] = nz
42
+ return r
43
+
44
+
45
+ def _build_gravity_model(IH, IW, direction, bg_color=0):
46
+ """Build ONNX model for gravity via unrolled bubble-sort.
47
+
48
+ Each step compares adjacent cells and swaps if needed:
49
+ - If current cell is bg AND source neighbor is non-bg → fill with source
50
+ - If current cell is non-bg AND destination neighbor is bg → vacate to bg
51
+ After max(IH,IW) passes, all non-bg pixels settle in the gravity direction.
52
+ """
53
+ pad_h, pad_w = GH - IH, GW - IW
54
+ n_steps = max(IH, IW)
55
+
56
+ # Two shift kernels: pull from source and destination directions
57
+ pull_above = np.zeros((10, 10, 3, 3), dtype=np.float32)
58
+ pull_below = np.zeros((10, 10, 3, 3), dtype=np.float32)
59
+ for ch in range(10):
60
+ if direction == 'down':
61
+ pull_above[ch, ch, 0, 1] = 1.0
62
+ pull_below[ch, ch, 2, 1] = 1.0
63
+ elif direction == 'up':
64
+ pull_above[ch, ch, 2, 1] = 1.0
65
+ pull_below[ch, ch, 0, 1] = 1.0
66
+ elif direction == 'right':
67
+ pull_above[ch, ch, 1, 0] = 1.0
68
+ pull_below[ch, ch, 1, 2] = 1.0
69
+ elif direction == 'left':
70
+ pull_above[ch, ch, 1, 2] = 1.0
71
+ pull_below[ch, ch, 1, 0] = 1.0
72
+
73
+ bg_sel = np.zeros((1, 10, 1, 1), dtype=np.float32)
74
+ bg_sel[0, bg_color, 0, 0] = 1.0
75
+ bg_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
76
+ bg_oh[0, bg_color, 0, 0] = 1.0
77
+
78
+ inits = [
79
+ _make_int64_init('sl_st', [0, 0, 0, 0]),
80
+ _make_int64_init('sl_en', [1, 10, IH, IW]),
81
+ numpy_helper.from_array(pull_above, 'pull_src'),
82
+ numpy_helper.from_array(pull_below, 'pull_dst'),
83
+ numpy_helper.from_array(bg_sel, 'bg_sel'),
84
+ numpy_helper.from_array(bg_oh, 'bg_oh'),
85
+ numpy_helper.from_array(np.float32(0.5), 'half'),
86
+ ]
87
+
88
+ nodes = [
89
+ helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cur_0']),
90
+ ]
91
+
92
+ cur = 'cur_0'
93
+ for i in range(n_steps):
94
+ src = f'src_{i}'
95
+ nodes.append(helper.make_node('Conv', [cur, 'pull_src'], [src],
96
+ kernel_shape=[3, 3], pads=[1, 1, 1, 1]))
97
+
98
+ nodes.append(helper.make_node('Mul', [cur, 'bg_sel'], [f'cbg_{i}']))
99
+ inits.append(_make_int64_init(f'ax1_{i}', [1]))
100
+ nodes.append(helper.make_node('ReduceSum', [f'cbg_{i}', f'ax1_{i}'], [f'cbgsum_{i}'], keepdims=1))
101
+ nodes.append(helper.make_node('Greater', [f'cbgsum_{i}', 'half'], [f'cur_is_bg_{i}']))
102
+
103
+ nodes.append(helper.make_node('Mul', [src, 'bg_sel'], [f'sbg_{i}']))
104
+ inits.append(_make_int64_init(f'ax2_{i}', [1]))
105
+ nodes.append(helper.make_node('ReduceSum', [f'sbg_{i}', f'ax2_{i}'], [f'sbgsum_{i}'], keepdims=1))
106
+ nodes.append(helper.make_node('Not', [f'cur_is_bg_{i}'], [f'cur_not_bg_{i}']))
107
+
108
+ nodes.append(helper.make_node('Greater', [f'sbgsum_{i}', 'half'], [f'src_is_bg_{i}']))
109
+ nodes.append(helper.make_node('Not', [f'src_is_bg_{i}'], [f'src_not_bg_{i}']))
110
+ nodes.append(helper.make_node('And', [f'cur_is_bg_{i}', f'src_not_bg_{i}'], [f'fill_{i}']))
111
+
112
+ dst = f'dst_{i}'
113
+ nodes.append(helper.make_node('Conv', [cur, 'pull_dst'], [dst],
114
+ kernel_shape=[3, 3], pads=[1, 1, 1, 1]))
115
+ nodes.append(helper.make_node('Mul', [dst, 'bg_sel'], [f'dbg_{i}']))
116
+ inits.append(_make_int64_init(f'ax3_{i}', [1]))
117
+ nodes.append(helper.make_node('ReduceSum', [f'dbg_{i}', f'ax3_{i}'], [f'dbgsum_{i}'], keepdims=1))
118
+ nodes.append(helper.make_node('Greater', [f'dbgsum_{i}', 'half'], [f'dst_is_bg_{i}']))
119
+ nodes.append(helper.make_node('And', [f'cur_not_bg_{i}', f'dst_is_bg_{i}'], [f'vacate_{i}']))
120
+
121
+ nxt = f'cur_{i+1}'
122
+ nodes.append(helper.make_node('Where', [f'fill_{i}', src, cur], [f'tmp_{i}']))
123
+ nodes.append(helper.make_node('Where', [f'vacate_{i}', 'bg_oh', f'tmp_{i}'], [nxt]))
124
+ cur = nxt
125
+
126
+ # Re-encode as clean one-hot via ArgMax + Equal+Cast, then pad
127
+ nodes.append(helper.make_node('ArgMax', [cur], ['grav_am'], axis=1, keepdims=1))
128
+ add_onehot_block(nodes, inits, 'grav_am', 'grav_oh')
129
+ nodes.append(_build_pad_node('grav_oh', 'output', pad_h, pad_w, inits))
130
+ return mk(nodes, inits)
131
+
132
+
133
+ def s_gravity_unrolled(td):
134
+ """Gravity solver with unrolled Conv+Where steps.
135
+ Tries all 4 directions × bg colors 0-9."""
136
+ exs = get_exs(td)
137
+ sp = fixed_shapes(td)
138
+ if sp is None:
139
+ return None
140
+ (IH, IW), (OH, OW) = sp
141
+ if (IH, IW) != (OH, OW):
142
+ return None
143
+
144
+ for bg_color in range(10):
145
+ for direction in ('down', 'up', 'left', 'right'):
146
+ if all(np.array_equal(_gravity_np(inp, direction, bg_color), out)
147
+ for inp, out in exs):
148
+ return _build_gravity_model(IH, IW, direction, bg_color)
149
+ return None
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Edge/boundary detection — Laplacian Conv
154
+ # ---------------------------------------------------------------------------
155
+
156
+ def _has_edges(inp, out, edge_color, bg_color=0):
157
+ """Check if output is edge detection of input."""
158
+ h, w = inp.shape
159
+ for r in range(h):
160
+ for c in range(w):
161
+ pix = inp[r, c]
162
+ is_edge = False
163
+ if pix != bg_color:
164
+ for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
165
+ nr, nc = r+dr, c+dc
166
+ if 0 <= nr < h and 0 <= nc < w:
167
+ if inp[nr, nc] != pix:
168
+ is_edge = True
169
+ break
170
+ else:
171
+ is_edge = True
172
+ break
173
+ expected = edge_color if is_edge else bg_color
174
+ if out[r, c] != expected:
175
+ return False
176
+ return True
177
+
178
+
179
+ def s_edge_detect(td):
180
+ """Edge detection solver: output = boundary pixels of input shapes."""
181
+ exs = get_exs(td)
182
+ sp = fixed_shapes(td)
183
+ if sp is None:
184
+ return None
185
+ (IH, IW), (OH, OW) = sp
186
+ if (IH, IW) != (OH, OW):
187
+ return None
188
+
189
+ for bg_color in [0]:
190
+ out_colors = set()
191
+ for _, out in exs:
192
+ out_colors.update(out.flatten())
193
+ for edge_color in out_colors:
194
+ if edge_color == bg_color:
195
+ continue
196
+ if all(_has_edges(inp, out, edge_color, bg_color) for inp, out in exs):
197
+ return _build_edge_model(IH, IW, edge_color, bg_color)
198
+ return None
199
+
200
+
201
+ def _build_edge_model(IH, IW, edge_color, bg_color=0):
202
+ """Build ONNX model for edge detection via Laplacian conv."""
203
+ pad_h, pad_w = GH - IH, GW - IW
204
+
205
+ ch_sel = np.zeros((1, 10, 1, 1), dtype=np.float32)
206
+ for c in range(10):
207
+ if c != bg_color:
208
+ ch_sel[0, c, 0, 0] = 1.0
209
+
210
+ lap_k = np.array([[0, -1, 0],
211
+ [-1, 4, -1],
212
+ [0, -1, 0]], dtype=np.float32).reshape(1, 1, 3, 3)
213
+
214
+ edge_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
215
+ edge_oh[0, edge_color, 0, 0] = 1.0
216
+ bg_oh = np.zeros((1, 10, 1, 1), dtype=np.float32)
217
+ bg_oh[0, bg_color, 0, 0] = 1.0
218
+
219
+ inits = [
220
+ _make_int64_init('sl_st', [0, 0, 0, 0]),
221
+ _make_int64_init('sl_en', [1, 10, IH, IW]),
222
+ numpy_helper.from_array(ch_sel, 'ch_sel'),
223
+ numpy_helper.from_array(lap_k, 'lap_k'),
224
+ numpy_helper.from_array(np.float32(0.5), 'thresh'),
225
+ numpy_helper.from_array(edge_oh, 'edge_oh'),
226
+ numpy_helper.from_array(bg_oh, 'bg_oh'),
227
+ ]
228
+
229
+ nodes = [
230
+ helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cropped']),
231
+ helper.make_node('Conv', ['cropped', 'ch_sel'], ['occ'], kernel_shape=[1, 1]),
232
+ helper.make_node('Conv', ['occ', 'lap_k'], ['lap_out'], kernel_shape=[3, 3], pads=[1, 1, 1, 1]),
233
+ helper.make_node('Abs', ['lap_out'], ['lap_abs']),
234
+ helper.make_node('Greater', ['lap_abs', 'thresh'], ['is_edge_raw']),
235
+ helper.make_node('Greater', ['occ', 'thresh'], ['is_occ']),
236
+ helper.make_node('And', ['is_edge_raw', 'is_occ'], ['is_edge']),
237
+ helper.make_node('Where', ['is_edge', 'edge_oh', 'bg_oh'], ['result_small']),
238
+ ]
239
+ nodes.append(_build_pad_node('result_small', 'output', pad_h, pad_w, inits))
240
+ return mk(nodes, inits)
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Mode fill solver — output = solid fill of most common input color
245
+ # ---------------------------------------------------------------------------
246
+
247
+ def s_mode_fill(td):
248
+ """Mode fill: output is entirely the most common color from input.
249
+ Uses runtime ArgMax to handle variable mode across inputs."""
250
+ exs = get_exs(td)
251
+
252
+ for inp, out in exs:
253
+ if inp.shape != out.shape:
254
+ return None
255
+ vals, counts = np.unique(inp, return_counts=True)
256
+ mode = vals[np.argmax(counts)]
257
+ if not np.all(out == mode):
258
+ return None
259
+
260
+ # Check if mode is always the same color
261
+ modes = set()
262
+ for inp, out in exs:
263
+ vals, counts = np.unique(inp, return_counts=True)
264
+ modes.add(vals[np.argmax(counts)])
265
+
266
+ if len(modes) == 1:
267
+ return None # Let s_constant handle it
268
+
269
+ sp = fixed_shapes(td)
270
+ if sp is None:
271
+ return None
272
+ (IH, IW), (OH, OW) = sp
273
+ if (IH, IW) != (OH, OW):
274
+ return None
275
+
276
+ pad_h, pad_w = GH - IH, GW - IW
277
+
278
+ inits = [
279
+ _make_int64_init('sl_st', [0, 0, 0, 0]),
280
+ _make_int64_init('sl_en', [1, 10, IH, IW]),
281
+ _make_int64_init('rs_axes_mode', [2, 3]),
282
+ numpy_helper.from_array(np.arange(10, dtype=np.int64).reshape(1, 10, 1, 1), 'classes'),
283
+ ]
284
+
285
+ nodes = [
286
+ helper.make_node('Slice', ['input', 'sl_st', 'sl_en'], ['cropped']),
287
+ helper.make_node('ReduceSum', ['cropped', 'rs_axes_mode'], ['hist'], keepdims=1),
288
+ helper.make_node('ArgMax', ['hist'], ['mode_idx'], axis=1, keepdims=1),
289
+ helper.make_node('Equal', ['mode_idx', 'classes'], ['eq']),
290
+ helper.make_node('Cast', ['eq'], ['mode_oh'], to=TensorProto.FLOAT),
291
+ helper.make_node('Expand', ['mode_oh', 'sl_en'], ['expanded']),
292
+ ]
293
+ nodes.append(_build_pad_node('expanded', 'output', pad_h, pad_w, inits))
294
+ return mk(nodes, inits)