Add greedy stacker: overlay(T1(x), T2(x)) composition after depth-1 beam search
Browse files- itt_solver/beam_logging.py +77 -24
itt_solver/beam_logging.py
CHANGED
|
@@ -26,12 +26,8 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 26 |
enable_layer_minus_one=False,
|
| 27 |
boundary_source='target'):
|
| 28 |
"""
|
| 29 |
-
Beam search with gate validation
|
| 30 |
-
|
| 31 |
-
Uses a dual-strategy approach: each atomic transform is tried on BOTH the
|
| 32 |
-
current (resized) field AND the original input. This is critical for
|
| 33 |
-
shape-changing transforms (e.g. Kronecker) that must operate on the raw
|
| 34 |
-
input rather than the tiled/resized intermediate.
|
| 35 |
"""
|
| 36 |
phi_in = np.array(phi_in, dtype=float)
|
| 37 |
phi_target = np.array(phi_target, dtype=float)
|
|
@@ -55,7 +51,6 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 55 |
|
| 56 |
def _try_candidate(phi_after_atomic, T_atomic, T_cur, cur_field_resized,
|
| 57 |
path_states, path_sigmas, depth_log, candidates, source_tag=""):
|
| 58 |
-
"""Score one candidate, check gates, and append to candidates if accepted."""
|
| 59 |
phi_new_resized = _resize_to_target(phi_after_atomic, phi_target)
|
| 60 |
|
| 61 |
if enable_layer_minus_one and layer_mask is not None:
|
|
@@ -78,12 +73,8 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 78 |
|
| 79 |
if not gates_info.get('passed', False):
|
| 80 |
depth_log.append({
|
| 81 |
-
'atomic': label,
|
| 82 |
-
'
|
| 83 |
-
'residue': residue,
|
| 84 |
-
'energy': energy,
|
| 85 |
-
'gates': gates_info,
|
| 86 |
-
'accepted': False,
|
| 87 |
'shape': phi_candidate.shape,
|
| 88 |
})
|
| 89 |
return
|
|
@@ -91,16 +82,10 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 91 |
new_states = path_states + [phi_candidate]
|
| 92 |
new_sigmas = path_sigmas + [residue]
|
| 93 |
T_new = T_cur.compose(T_atomic)
|
| 94 |
-
|
| 95 |
candidates.append((T_new, phi_candidate, score, new_states, new_sigmas))
|
| 96 |
-
|
| 97 |
depth_log.append({
|
| 98 |
-
'atomic': label,
|
| 99 |
-
'
|
| 100 |
-
'residue': residue,
|
| 101 |
-
'energy': energy,
|
| 102 |
-
'gates': gates_info,
|
| 103 |
-
'accepted': True,
|
| 104 |
'shape': phi_candidate.shape,
|
| 105 |
})
|
| 106 |
|
|
@@ -111,7 +96,7 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 111 |
base_field_for_apply = path_states[-1]
|
| 112 |
|
| 113 |
for idx, T_atomic in enumerate(atomic_library):
|
| 114 |
-
#
|
| 115 |
try:
|
| 116 |
phi_after_atomic = T_atomic.apply(base_field_for_apply)
|
| 117 |
_try_candidate(phi_after_atomic, T_atomic, T_cur,
|
|
@@ -120,8 +105,7 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 120 |
except Exception:
|
| 121 |
pass
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
# shape-changing transforms like Kronecker) ---
|
| 125 |
try:
|
| 126 |
phi_after_original = T_atomic.apply(phi_in)
|
| 127 |
if phi_after_original.shape != base_field_for_apply.shape or \
|
|
@@ -144,8 +128,77 @@ def beam_minimize_with_log(phi_in, phi_target, atomic_library,
|
|
| 144 |
if sigma_l1(best[1], phi_target) == 0:
|
| 145 |
break
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
if best is None:
|
| 148 |
return identity, phi0, [phi0], [sigma_l1(phi0, phi_target)], logs
|
| 149 |
|
| 150 |
T_best, phi_best, _, states_best, sigmas_best = best
|
| 151 |
return T_best, phi_best, states_best, sigmas_best, logs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
enable_layer_minus_one=False,
|
| 27 |
boundary_source='target'):
|
| 28 |
"""
|
| 29 |
+
Beam search with gate validation, dual-strategy (resized + original input),
|
| 30 |
+
and greedy stacker (overlay composition of depth-1 pieces).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
phi_in = np.array(phi_in, dtype=float)
|
| 33 |
phi_target = np.array(phi_target, dtype=float)
|
|
|
|
| 51 |
|
| 52 |
def _try_candidate(phi_after_atomic, T_atomic, T_cur, cur_field_resized,
|
| 53 |
path_states, path_sigmas, depth_log, candidates, source_tag=""):
|
|
|
|
| 54 |
phi_new_resized = _resize_to_target(phi_after_atomic, phi_target)
|
| 55 |
|
| 56 |
if enable_layer_minus_one and layer_mask is not None:
|
|
|
|
| 73 |
|
| 74 |
if not gates_info.get('passed', False):
|
| 75 |
depth_log.append({
|
| 76 |
+
'atomic': label, 'score': score, 'residue': residue,
|
| 77 |
+
'energy': energy, 'gates': gates_info, 'accepted': False,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
'shape': phi_candidate.shape,
|
| 79 |
})
|
| 80 |
return
|
|
|
|
| 82 |
new_states = path_states + [phi_candidate]
|
| 83 |
new_sigmas = path_sigmas + [residue]
|
| 84 |
T_new = T_cur.compose(T_atomic)
|
|
|
|
| 85 |
candidates.append((T_new, phi_candidate, score, new_states, new_sigmas))
|
|
|
|
| 86 |
depth_log.append({
|
| 87 |
+
'atomic': label, 'score': score, 'residue': residue,
|
| 88 |
+
'energy': energy, 'gates': gates_info, 'accepted': True,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
'shape': phi_candidate.shape,
|
| 90 |
})
|
| 91 |
|
|
|
|
| 96 |
base_field_for_apply = path_states[-1]
|
| 97 |
|
| 98 |
for idx, T_atomic in enumerate(atomic_library):
|
| 99 |
+
# Strategy 1: apply to current (resized) field
|
| 100 |
try:
|
| 101 |
phi_after_atomic = T_atomic.apply(base_field_for_apply)
|
| 102 |
_try_candidate(phi_after_atomic, T_atomic, T_cur,
|
|
|
|
| 105 |
except Exception:
|
| 106 |
pass
|
| 107 |
|
| 108 |
+
# Strategy 2: apply to ORIGINAL input
|
|
|
|
| 109 |
try:
|
| 110 |
phi_after_original = T_atomic.apply(phi_in)
|
| 111 |
if phi_after_original.shape != base_field_for_apply.shape or \
|
|
|
|
| 128 |
if sigma_l1(best[1], phi_target) == 0:
|
| 129 |
break
|
| 130 |
|
| 131 |
+
# --- Greedy stacker: try overlay(T1(x), T2(x)) for top candidates ---
|
| 132 |
+
if best is not None and sigma_l1(best[1], phi_target) > 0:
|
| 133 |
+
depth1_pieces = []
|
| 134 |
+
for T_atomic in atomic_library:
|
| 135 |
+
try:
|
| 136 |
+
piece = T_atomic.apply(phi_in)
|
| 137 |
+
piece_resized = _resize_to_target(piece, phi_target)
|
| 138 |
+
piece_sigma = sigma_l1(piece_resized, phi_target)
|
| 139 |
+
depth1_pieces.append((T_atomic, piece_resized, piece_sigma))
|
| 140 |
+
except Exception:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
depth1_pieces.sort(key=lambda x: x[2])
|
| 144 |
+
top_n = min(len(depth1_pieces), beam_width * 2)
|
| 145 |
+
stacker_log = []
|
| 146 |
+
|
| 147 |
+
for i in range(top_n):
|
| 148 |
+
T1, p1, s1 = depth1_pieces[i]
|
| 149 |
+
for j in range(top_n):
|
| 150 |
+
if i == j:
|
| 151 |
+
continue
|
| 152 |
+
T2, p2, s2 = depth1_pieces[j]
|
| 153 |
+
|
| 154 |
+
overlaid = p1.copy()
|
| 155 |
+
mask = (p2 != 0)
|
| 156 |
+
overlaid[mask] = p2[mask]
|
| 157 |
+
|
| 158 |
+
residue = sigma_l1(overlaid, phi_target)
|
| 159 |
+
|
| 160 |
+
if residue < sigma_l1(best[1], phi_target):
|
| 161 |
+
gates_info = validate_gates(overlaid, phi_in, phi_target,
|
| 162 |
+
boundary_mask=boundary_mask_resized,
|
| 163 |
+
max_fraction=max_fraction,
|
| 164 |
+
allowed_symbols=allowed_symbols)
|
| 165 |
+
label = f"overlay({repr(T1)},{repr(T2)})"
|
| 166 |
+
if gates_info.get('passed', False):
|
| 167 |
+
energy = dirichlet_energy(overlaid)
|
| 168 |
+
score = residue + lock_coeff * energy
|
| 169 |
+
T_composed = Transform(lambda p, _p1=p1, _p2=p2: _overlay(_p1, _p2),
|
| 170 |
+
f"overlay({T1.name},{T2.name})")
|
| 171 |
+
_, _, _, best_states, best_sigmas = best
|
| 172 |
+
new_states = best_states + [overlaid]
|
| 173 |
+
new_sigmas = best_sigmas + [residue]
|
| 174 |
+
if score < best[2]:
|
| 175 |
+
best = (T_composed, overlaid, score, new_states, new_sigmas)
|
| 176 |
+
|
| 177 |
+
stacker_log.append({
|
| 178 |
+
'atomic': label, 'score': score,
|
| 179 |
+
'residue': residue, 'energy': energy,
|
| 180 |
+
'gates': gates_info, 'accepted': True,
|
| 181 |
+
'shape': overlaid.shape,
|
| 182 |
+
})
|
| 183 |
+
|
| 184 |
+
if residue == 0:
|
| 185 |
+
break
|
| 186 |
+
if best is not None and sigma_l1(best[1], phi_target) == 0:
|
| 187 |
+
break
|
| 188 |
+
|
| 189 |
+
if stacker_log:
|
| 190 |
+
logs.append(stacker_log)
|
| 191 |
+
|
| 192 |
if best is None:
|
| 193 |
return identity, phi0, [phi0], [sigma_l1(phi0, phi_target)], logs
|
| 194 |
|
| 195 |
T_best, phi_best, _, states_best, sigmas_best = best
|
| 196 |
return T_best, phi_best, states_best, sigmas_best, logs
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _overlay(base, fg):
|
| 200 |
+
"""Transparent overlay helper: fg non-zero pixels overwrite base."""
|
| 201 |
+
result = base.copy()
|
| 202 |
+
mask = (fg != 0)
|
| 203 |
+
result[mask] = fg[mask]
|
| 204 |
+
return result
|