Update app.py
Browse files
app.py
CHANGED
|
@@ -193,6 +193,92 @@ def classify_path(path, d: int):
|
|
| 193 |
return "snake", True
|
| 194 |
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
def edge_dimension(a: int, b: int) -> int | None:
|
| 197 |
"""
|
| 198 |
Return the dimension index of the edge (a,b) if they are adjacent,
|
|
@@ -1255,14 +1341,13 @@ def render(d, show_labels_vals, path, mark_vals, mark_dist_vals, subsel, switchs
|
|
| 1255 |
)
|
| 1256 |
def path_info(d, path):
|
| 1257 |
path = path or []
|
|
|
|
| 1258 |
|
| 1259 |
if not path:
|
| 1260 |
return html.Span("Path: (empty)")
|
| 1261 |
|
| 1262 |
-
# Vertex list
|
| 1263 |
path_str = ", ".join(str(v) for v in path)
|
| 1264 |
|
| 1265 |
-
# Classification
|
| 1266 |
label, valid = classify_path(path, d)
|
| 1267 |
color = {
|
| 1268 |
"snake": "green",
|
|
@@ -1276,25 +1361,57 @@ def path_info(d, path):
|
|
| 1276 |
dims = []
|
| 1277 |
for i in range(len(path) - 1):
|
| 1278 |
dim = edge_dimension(path[i], path[i + 1])
|
| 1279 |
-
if dim is not None
|
| 1280 |
-
dims.append(dim)
|
| 1281 |
-
else:
|
| 1282 |
-
# non adjacent step, keep something explicit
|
| 1283 |
-
dims.append("?")
|
| 1284 |
-
|
| 1285 |
dims_str = ", ".join(str(x) for x in dims) if dims else "(none)"
|
| 1286 |
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
]
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
]
|
| 1296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
|
| 1299 |
@app.callback(
|
| 1300 |
Output("path_bits", "children"),
|
|
|
|
| 193 |
return "snake", True
|
| 194 |
|
| 195 |
|
| 196 |
+
def snake_violations(path: List[int], d: int):
|
| 197 |
+
"""
|
| 198 |
+
Return a dict with violating pairs for the snake/coil inducedness + adjacency rules.
|
| 199 |
+
|
| 200 |
+
We follow the same conventions as classify_path:
|
| 201 |
+
- If path is closed (path[0]==path[-1]), work with cycle = path[:-1]
|
| 202 |
+
- Consecutive edges must be adjacent
|
| 203 |
+
- Inducedness forbids any chord between non-consecutive vertices,
|
| 204 |
+
except we always allow the endpoints pair (0, n-1)
|
| 205 |
+
(closing edge for coil, or would-be closing edge for almost coil)
|
| 206 |
+
"""
|
| 207 |
+
if not path or len(path) <= 1:
|
| 208 |
+
return {
|
| 209 |
+
"dup_vertices": [],
|
| 210 |
+
"non_adjacent_steps": [],
|
| 211 |
+
"chords": [],
|
| 212 |
+
"bad_closing_edge": [],
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
is_closed = (path[0] == path[-1])
|
| 216 |
+
cycle = path[:-1] if is_closed else path[:]
|
| 217 |
+
n = len(cycle)
|
| 218 |
+
|
| 219 |
+
dup_vertices = []
|
| 220 |
+
non_adjacent_steps = []
|
| 221 |
+
chords = []
|
| 222 |
+
bad_closing_edge = []
|
| 223 |
+
|
| 224 |
+
# 1) duplicates (report as pairs of positions, but return as vertex pairs for display)
|
| 225 |
+
# We'll report every duplicate occurrence pair (v, v).
|
| 226 |
+
pos = {}
|
| 227 |
+
for i, v in enumerate(cycle):
|
| 228 |
+
if v in pos:
|
| 229 |
+
dup_vertices.append((v, v))
|
| 230 |
+
else:
|
| 231 |
+
pos[v] = i
|
| 232 |
+
|
| 233 |
+
# 2) consecutive non-adjacent steps (pairs of vertices)
|
| 234 |
+
for i in range(n - 1):
|
| 235 |
+
a, b = cycle[i], cycle[i + 1]
|
| 236 |
+
if hamming_dist(a, b) != 1:
|
| 237 |
+
non_adjacent_steps.append((a, b))
|
| 238 |
+
|
| 239 |
+
# closing edge required only if explicitly closed
|
| 240 |
+
if n >= 2:
|
| 241 |
+
closing_adjacent = (hamming_dist(cycle[0], cycle[-1]) == 1)
|
| 242 |
+
if is_closed and not closing_adjacent:
|
| 243 |
+
bad_closing_edge.append((cycle[-1], cycle[0]))
|
| 244 |
+
|
| 245 |
+
# 3) inducedness chords: any non-consecutive adjacent pair, skipping (0,n-1)
|
| 246 |
+
# To avoid O(n^2) scans on long paths, we use bit-neighbor checks.
|
| 247 |
+
# For each vertex v, check all its d neighbors u; if u appears in the path,
|
| 248 |
+
# and the indices are not consecutive (modulo nothing; this is snake logic),
|
| 249 |
+
# then it's a chord (unless it's endpoints pair).
|
| 250 |
+
idx_of = {v: i for i, v in enumerate(cycle)}
|
| 251 |
+
for v, i in idx_of.items():
|
| 252 |
+
for bit in range(d):
|
| 253 |
+
u = v ^ (1 << bit)
|
| 254 |
+
j = idx_of.get(u)
|
| 255 |
+
if j is None:
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
+
# skip actual path edges
|
| 259 |
+
if abs(i - j) == 1:
|
| 260 |
+
continue
|
| 261 |
+
|
| 262 |
+
# skip endpoints pair (0, n-1) always
|
| 263 |
+
if (i == 0 and j == n - 1) or (i == n - 1 and j == 0):
|
| 264 |
+
continue
|
| 265 |
+
|
| 266 |
+
# record chord once (as an unordered pair)
|
| 267 |
+
a, b = (v, u) if v < u else (u, v)
|
| 268 |
+
chords.append((a, b))
|
| 269 |
+
|
| 270 |
+
# de-duplicate chords list
|
| 271 |
+
chords = sorted(set(chords))
|
| 272 |
+
|
| 273 |
+
return {
|
| 274 |
+
"dup_vertices": dup_vertices,
|
| 275 |
+
"non_adjacent_steps": non_adjacent_steps,
|
| 276 |
+
"chords": chords,
|
| 277 |
+
"bad_closing_edge": bad_closing_edge,
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
|
| 282 |
def edge_dimension(a: int, b: int) -> int | None:
|
| 283 |
"""
|
| 284 |
Return the dimension index of the edge (a,b) if they are adjacent,
|
|
|
|
| 1341 |
)
|
| 1342 |
def path_info(d, path):
|
| 1343 |
path = path or []
|
| 1344 |
+
d = int(d)
|
| 1345 |
|
| 1346 |
if not path:
|
| 1347 |
return html.Span("Path: (empty)")
|
| 1348 |
|
|
|
|
| 1349 |
path_str = ", ".join(str(v) for v in path)
|
| 1350 |
|
|
|
|
| 1351 |
label, valid = classify_path(path, d)
|
| 1352 |
color = {
|
| 1353 |
"snake": "green",
|
|
|
|
| 1361 |
dims = []
|
| 1362 |
for i in range(len(path) - 1):
|
| 1363 |
dim = edge_dimension(path[i], path[i + 1])
|
| 1364 |
+
dims.append(dim if dim is not None else "?")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1365 |
dims_str = ", ".join(str(x) for x in dims) if dims else "(none)"
|
| 1366 |
|
| 1367 |
+
extra = None
|
| 1368 |
+
if label == "not snake":
|
| 1369 |
+
viol = snake_violations(path, d)
|
| 1370 |
+
|
| 1371 |
+
pairs = []
|
| 1372 |
+
# Keep categories separate so it's interpretable
|
| 1373 |
+
if viol["dup_vertices"]:
|
| 1374 |
+
pairs.extend(viol["dup_vertices"])
|
| 1375 |
+
if viol["non_adjacent_steps"]:
|
| 1376 |
+
pairs.extend(viol["non_adjacent_steps"])
|
| 1377 |
+
if viol["bad_closing_edge"]:
|
| 1378 |
+
pairs.extend(viol["bad_closing_edge"])
|
| 1379 |
+
if viol["chords"]:
|
| 1380 |
+
pairs.extend(viol["chords"])
|
| 1381 |
+
|
| 1382 |
+
# Format: (a,b), (c,d), ...
|
| 1383 |
+
# If there are many, don’t explode the UI
|
| 1384 |
+
MAX_SHOW = 30
|
| 1385 |
+
shown = pairs[:MAX_SHOW]
|
| 1386 |
+
pairs_str = ", ".join(f"({a},{b})" for a, b in shown)
|
| 1387 |
+
if len(pairs) > MAX_SHOW:
|
| 1388 |
+
pairs_str += f", ... (+{len(pairs) - MAX_SHOW} more)"
|
| 1389 |
+
|
| 1390 |
+
extra = html.Div(
|
| 1391 |
+
[
|
| 1392 |
+
html.Span("Violations: ", style={"fontWeight": "bold"}),
|
| 1393 |
+
html.Span(pairs_str if pairs_str else "(none)", style={"fontFamily": "monospace"}),
|
| 1394 |
+
],
|
| 1395 |
+
style={"marginTop": "4px"},
|
| 1396 |
+
)
|
| 1397 |
|
| 1398 |
+
return html.Div(
|
| 1399 |
+
[
|
| 1400 |
+
html.Div(
|
| 1401 |
+
[
|
| 1402 |
+
html.Span(f"Path: {path_str} "),
|
| 1403 |
+
html.Span(f"[{label}]", style={"color": color, "fontWeight": "bold"}),
|
| 1404 |
+
]
|
| 1405 |
+
),
|
| 1406 |
+
html.Div(
|
| 1407 |
+
[
|
| 1408 |
+
html.Span("Dimensions: "),
|
| 1409 |
+
html.Span(dims_str, style={"fontFamily": "monospace"}),
|
| 1410 |
+
]
|
| 1411 |
+
),
|
| 1412 |
+
extra if extra is not None else html.Span(),
|
| 1413 |
+
]
|
| 1414 |
+
)
|
| 1415 |
|
| 1416 |
@app.callback(
|
| 1417 |
Output("path_bits", "children"),
|