| import math |
| from typing import List |
|
|
| from dash import Dash, html, dcc, Input, Output, State, ctx, no_update |
| import plotly.graph_objects as go |
|
|
|
|
| |
| |
| |
| DEFAULTS = { |
| "dimension": 3, |
| "scale": 1600.0, |
| "node_radius": 6, |
| "edge_width": 4, |
| "show_labels": False, |
| "label_fontsize": 12, |
| "max_dimension": 12, |
| "min_dimension": 1, |
| } |
|
|
|
|
|
|
| |
|
|
|
|
| def consecutive_triple_start(indices: list[int], n: int, is_closed: bool) -> int | None: |
| """ |
| Given 3 indices, return the start index s such that {s,s+1,s+2} (mod n if closed) |
| equals the given set. If not possible, return None. |
| """ |
| if len(indices) != 3 or n <= 0: |
| return None |
|
|
| S = set(indices) |
|
|
| if is_closed: |
| for s in range(n): |
| if {(s) % n, (s + 1) % n, (s + 2) % n} == S: |
| return s |
| return None |
| else: |
| |
| mn = min(S) |
| if S == {mn, mn + 1, mn + 2} and (mn + 2) < n: |
| return mn |
| return None |
|
|
|
|
|
|
| def cycle_edge_dims(cycle: List[int], d: int) -> List[int] | None: |
| """ |
| cycle is vertex list WITHOUT repeating start at end. |
| Returns list of edge dimensions around the cycle (length n), |
| or None if any step is non-adjacent. |
| """ |
| n = len(cycle) |
| dims = [] |
| for i in range(n): |
| a = cycle[i] |
| b = cycle[(i + 1) % n] |
| dim = edge_dimension(a, b) |
| if dim is None: |
| return None |
| dims.append(dim) |
| return dims |
|
|
|
|
| def is_symmetric_coil(path: List[int], d: int, allow_reverse: bool = True) -> bool: |
| """ |
| A "doubled coil": valid coil whose second half repeats the structure of the first half. |
| We detect this via the cyclic edge-dimension sequence. |
| |
| allow_reverse=True also accepts that the second half is the reverse traversal |
| of the first half (often happens depending on where you cut the cycle). |
| """ |
| if not path or len(path) < 4: |
| return False |
| if path[0] != path[-1]: |
| return False |
|
|
| cycle = path[:-1] |
| n = len(cycle) |
| if n % 2 != 0: |
| return False |
|
|
| dims = cycle_edge_dims(cycle, d) |
| if dims is None: |
| return False |
|
|
| half = n // 2 |
| first = dims[:half] |
| second = dims[half:] |
|
|
| if second == first: |
| return True |
|
|
| if allow_reverse: |
| |
| |
| if second == list(reversed(first)): |
| return True |
|
|
| return False |
|
|
|
|
| def index_in_path(path: List[int], vid: int): |
| """Return the index of vid in path (first occurrence), or None.""" |
| try: |
| return path.index(vid) |
| except ValueError: |
| return None |
|
|
|
|
| def swap_dims_vertex(v: int, i: int, j: int) -> int: |
| """ |
| Swap bits i and j in the vertex id v. |
| (Coordinate permutation on Q_d.) |
| """ |
| if i == j: |
| return v |
| bi = (v >> i) & 1 |
| bj = (v >> j) & 1 |
| if bi != bj: |
| |
| v ^= (1 << i) | (1 << j) |
| return v |
|
|
|
|
| def flip_dim_vertex(v: int, k: int) -> int: |
| """ |
| Flip bit k in the vertex id v. |
| (Translation by the unit vector in coordinate k.) |
| """ |
| return v ^ (1 << k) |
|
|
|
|
| def swap_dims_path(path, d: int, i: int, j: int): |
| """Apply swap_dims_vertex to every vertex in the path.""" |
| if i < 0 or j < 0 or i >= d or j >= d: |
| return path |
| return [swap_dims_vertex(v, i, j) for v in path] |
|
|
|
|
| def flip_dim_path(path, d: int, k: int): |
| """Apply flip_dim_vertex to every vertex in the path.""" |
| if k < 0 or k >= d: |
| return path |
| return [flip_dim_vertex(v, k) for v in path] |
|
|
|
|
| def classify_path(path, d: int): |
| """ |
| Classify a path in Q_d as one of: |
| - "snake" (induced simple path) |
| - "coil" (induced simple cycle) |
| - "almost coil" (open path that would be a coil if closed) |
| - "not snake" otherwise |
| Returns: (label, is_valid) |
| """ |
|
|
| if not path or len(path) <= 1: |
| return "snake", True |
|
|
| |
| is_closed = (path[0] == path[-1]) |
|
|
| |
| cycle = path[:-1] if is_closed else path[:] |
| n = len(cycle) |
|
|
| |
| if len(set(cycle)) != n: |
| return "not snake", False |
|
|
| |
| for i in range(n - 1): |
| if hamming_dist(cycle[i], cycle[i + 1]) != 1: |
| return "not snake", False |
|
|
| |
| closing_adjacent = (hamming_dist(cycle[0], cycle[-1]) == 1) |
|
|
| |
| if is_closed and not closing_adjacent: |
| return "not snake", False |
|
|
| |
| |
| |
| |
| |
| for i in range(n): |
| for j in range(i + 1, n): |
| |
| if j == i + 1: |
| continue |
| |
| if i == 0 and j == n - 1: |
| continue |
| if hamming_dist(cycle[i], cycle[j]) == 1: |
| return "not snake", False |
|
|
| |
|
|
| if is_closed: |
| |
| |
| if is_symmetric_coil(path, d, allow_reverse=True): |
| return "symmetric coil", True |
| return "coil", True |
|
|
| |
| if closing_adjacent: |
| |
| return "almost coil", True |
|
|
| return "snake", True |
|
|
|
|
| def snake_violations(path: List[int], d: int): |
| """ |
| Return a dict with violating pairs for the snake/coil inducedness + adjacency rules. |
| |
| We follow the same conventions as classify_path: |
| - If path is closed (path[0]==path[-1]), work with cycle = path[:-1] |
| - Consecutive edges must be adjacent |
| - Inducedness forbids any chord between non-consecutive vertices, |
| except we always allow the endpoints pair (0, n-1) |
| (closing edge for coil, or would-be closing edge for almost coil) |
| """ |
| if not path or len(path) <= 1: |
| return { |
| "dup_vertices": [], |
| "non_adjacent_steps": [], |
| "chords": [], |
| "bad_closing_edge": [], |
| } |
|
|
| is_closed = (path[0] == path[-1]) |
| cycle = path[:-1] if is_closed else path[:] |
| n = len(cycle) |
|
|
| dup_vertices = [] |
| non_adjacent_steps = [] |
| chords = [] |
| bad_closing_edge = [] |
|
|
| |
| |
| pos = {} |
| for i, v in enumerate(cycle): |
| if v in pos: |
| dup_vertices.append((v, v)) |
| else: |
| pos[v] = i |
|
|
| |
| for i in range(n - 1): |
| a, b = cycle[i], cycle[i + 1] |
| if hamming_dist(a, b) != 1: |
| non_adjacent_steps.append((a, b)) |
|
|
| |
| if n >= 2: |
| closing_adjacent = (hamming_dist(cycle[0], cycle[-1]) == 1) |
| if is_closed and not closing_adjacent: |
| bad_closing_edge.append((cycle[-1], cycle[0])) |
|
|
| |
| |
| |
| |
| |
| idx_of = {v: i for i, v in enumerate(cycle)} |
| for v, i in idx_of.items(): |
| for bit in range(d): |
| u = v ^ (1 << bit) |
| j = idx_of.get(u) |
| if j is None: |
| continue |
|
|
| |
| if abs(i - j) == 1: |
| continue |
|
|
| |
| if (i == 0 and j == n - 1) or (i == n - 1 and j == 0): |
| continue |
|
|
| |
| a, b = (v, u) if v < u else (u, v) |
| chords.append((a, b)) |
|
|
| |
| chords = sorted(set(chords)) |
|
|
| return { |
| "dup_vertices": dup_vertices, |
| "non_adjacent_steps": non_adjacent_steps, |
| "chords": chords, |
| "bad_closing_edge": bad_closing_edge, |
| } |
|
|
|
|
|
|
| def edge_dimension(a: int, b: int) -> int | None: |
| """ |
| Return the dimension index of the edge (a,b) if they are adjacent, |
| otherwise return None. |
| """ |
| x = a ^ b |
| if x == 0 or (x & (x - 1)) != 0: |
| |
| return None |
| dim = 0 |
| while x > 1: |
| x >>= 1 |
| dim += 1 |
| return dim |
|
|
|
|
| def hamming_dist(a: int, b: int) -> int: |
| x, c = a ^ b, 0 |
| while x: |
| c += x & 1 |
| x >>= 1 |
| return c |
|
|
|
|
| def build_hypercube(d: int): |
| n = 1 << d |
| nodes = list(range(n)) |
| edges = [] |
| for u in range(n): |
| for bit in range(d): |
| v = u ^ (1 << bit) |
| if u < v: |
| edges.append((u, v, bit)) |
| return nodes, edges |
|
|
|
|
| def dim_color(k: int) -> str: |
| hues = [210, 20, 140, 80, 0, 260, 40, 180, 320, 120] |
| h = hues[k % len(hues)] |
| return f"hsl({h},65%,45%)" |
|
|
|
|
| def int_to_bin(n: int, d: int) -> str: |
| return format(n, f"0{d}b") |
|
|
| |
| |
| def layout_positions(d: int, base: float = 900.0, mode: str = "default"): |
| n = 1 << d |
|
|
| if mode == "bipartite": |
| |
| odds = [v for v in range(n) if (bin(v).count("1") % 2) == 1] |
| evens = [v for v in range(n) if (bin(v).count("1") % 2) == 0] |
|
|
| |
| odds.sort() |
| evens.sort() |
|
|
| col_gap = max(400.0, base * 0.9) |
| y_gap = max(12.0, base / max(1, (n // 2))) |
|
|
| pts = [] |
|
|
| |
| for idx, v in enumerate(odds): |
| x = 0.0 |
| y = idx * y_gap |
| pts.append((v, x, y)) |
|
|
| |
| for idx, v in enumerate(evens): |
| x = col_gap |
| y = idx * y_gap |
| pts.append((v, x, y)) |
|
|
| |
| minx = min(x for _, x, _ in pts) |
| miny = min(y for _, _, y in pts) |
| pts2 = [(vid, x - minx, y - miny) for vid, x, y in pts] |
|
|
| width = col_gap |
| height = (len(odds) - 1) * y_gap if odds else 0.0 |
| return pts2, width, height |
|
|
| |
| dx, dy = [0.0] * d, [0.0] * d |
| for k in range(d): |
| tier = k // 2 |
| mag = (2 ** max(0, (d - 1) - tier)) * (base / (2 ** d)) |
| if k % 2 == 0: |
| dx[k] = mag |
| else: |
| dy[k] = mag |
|
|
| pts = [] |
| minx = miny = float("inf") |
| maxx = maxy = float("-inf") |
| for vid in range(n): |
| x = sum(dx[k] for k in range(d) if (vid >> k) & 1) |
| y = sum(dy[k] for k in range(d) if (vid >> k) & 1) |
| if (vid >> 2) & 1: |
| x += 100 |
| y += 250 |
| if (vid >> 3) & 1: |
| x += 1800 |
| y -= 200 |
| if (vid >> 4) & 1: |
| x += 200 |
| y += 1800 |
| if (vid >> 5) & 1: |
| x += 3800 |
| y += 3200 |
| if (vid >> 6) & 1: |
| x += 3400 |
| y -= 200 |
| if (vid >> 7) & 1: |
| x += 8000 |
| y += 150 |
| if (vid >> 8) & 1: |
| x += 15000 |
| y -= 9000 |
|
|
|
|
| pts.append((vid, x, y)) |
| minx, maxx = min(minx, x), max(maxx, x) |
| miny, maxy = min(miny, y), max(maxy, y) |
|
|
| pts2 = [(vid, x - minx, y - miny) for vid, x, y in pts] |
| width = maxx - minx |
| height = maxy - miny |
| return pts2, width, height |
|
|
|
|
| def longest_cib(d: int): |
| """ |
| Return a predefined coil/snake for certain dimensions, |
| otherwise fall back to a standard Gray-code path. |
| """ |
| presets = { |
| 2: [0, 1, 3, 2, 0], |
| 3: [0, 1, 3, 7, 6, 4, 0], |
| 4: [0, 1, 3, 7, 15, 13, 12, 4, 0], |
| 5: [0, 1, 3, 7, 6, 14, 12, 13, 29, 31, 27, 26, 18, 16, 0], |
| 6: [0, 1, 3, 7, 15, 31, 29, 25, 24, 26, 10, 42, 43, 59, 51, 49, 53, 37, 45, 44, 60, 62, 54, 22, 20, 4, 0], |
| 7: [0, 1, 3, 7, 15, 13, 12, 28, 30, 26, 27, 25, 57, 56, 40, 104, 72, 73, 75, 107, 111, 110, 46, 38, 36, 52,116, 124, 125, 93, 95, 87, 119, 55, 51, 50, 114, 98, 66, 70, 68, 69, 101, 97, 113, 81, 80, 16, 0], |
| 8: [0, 1, 3, 7, 6, 14, 12, 13, 29, 31, 27, 26, 18, 50, 54, 62, 60, 56, 57, 49, 53, 37, 101, 69, 68, 196, 132, 133, 149, 151, 150, 158, 156, 220, 92, 94, 86, 87, 119, 115, 123, 122, 250, 254, 255, 191, 187, 179, 163, 167, 231, 230, 226, 98, 66, 74, 202, 200, 136, 137, 139, 143, 207, 205, 237, 173, 172, 174, 170, 42, 43, 47, 111, 110, 108, 104, 105, 73, 89, 217, 219, 211, 195, 193, 225, 241, 245, 244, 116, 112, 80, 208, 144, 176, 160, 32, 0], |
| 9: [0, 1, 3, 7, 15, 14, 30, 22, 18, 50, 34, 38, 36, 44, 60, 56, 24, 88, 80, 112, 116, 118, 126, 122, 106, 74, 66, 70, 68, 69, 101, 103, 99, 115, 83, 87, 95, 93, 125, 121, 105, 41, 43, 59, 63, 55, 53, 21, 149, 151, 147, 155, 153, 185, 189, 173, 175, 167, 163, 161, 160, 176, 180, 182, 190, 186, 170, 138, 130, 134, 132, 140, 156, 220, 222, 218, 210, 242, 226, 230, 228, 236, 232, 200, 192, 193, 209, 241, 245, 247, 255, 251, 235, 203, 459, 458, 450, 454, 452, 453, 485, 487, 483, 499, 467, 471, 479, 477, 509, 505, 489, 425, 427, 443, 447, 439, 437, 433, 401, 385, 387, 391, 399, 398, 414, 406, 402, 434, 418, 422, 420, 428, 444, 440, 408, 472, 464, 496, 500, 502, 510, 494, 366, 358, 354, 352, 360, 376, 380, 348, 340, 342, 338, 346, 347, 379, 383, 375, 373, 369, 337, 321, 323, 327, 335, 333, 365, 301, 293, 289, 291, 307, 275, 279, 287, 285, 281, 265, 267, 266, 298, 314, 318, 310, 308, 304, 272, 256, 0], |
| 10: [0, 1, 3, 7, 15, 14, 30, 62, 63, 55, 51, 50, 34, 42, 43, 41, 45, 37, 36, 52, 116, 117, 125, 121, 123, 122, 90, 74, 75, 73, 77, 76, 108, 104, 96, 97, 99, 103, 102, 70, 86, 87, 83, 81, 80, 208, 240, 241, 243, 247, 255, 254, 222, 206, 207, 199, 195, 194, 226, 234, 235, 233, 237, 229, 228, 196, 132, 133, 141, 137, 139, 138, 154, 186, 187, 185, 189, 188, 172, 168, 160, 161, 163, 167, 166, 182, 150, 151, 147, 403, 275, 279, 278, 310, 294, 295, 291, 289, 288, 296, 300, 316, 317, 313, 315, 314, 282, 266, 267, 265, 269, 261, 260, 324, 356, 357, 365, 361, 363, 362, 354, 322, 323, 327, 335, 334, 350, 382, 383, 375, 371, 369, 368, 376, 344, 345, 349, 341, 469, 471, 470, 502, 500, 508, 509, 505, 507, 506, 474, 458, 459, 457, 461, 460, 396, 412, 408, 440, 432, 433, 437, 421, 429, 425, 427, 426, 430, 494, 495, 487, 483, 481, 480, 448, 384, 386, 390, 391, 399, 415, 927, 919, 918, 950, 934, 935, 931, 929, 928, 936, 940, 956, 957, 953, 955, 954, 922, 906, 907, 905, 909, 901, 900, 964, 996, 997, 1005, 1001, 1003, 1002, 994, 962, 963, 967, 975, 974, 990, 1022, 1023, 1015, 1011, 1009, 1008, 976, 848, 849, 851, 855, 854, 838, 870, 871, 867, 865, 864, 872, 876, 844, 845, 841, 843, 842, 858, 890, 891, 889, 893, 885, 884, 820, 804, 805, 813, 809, 811, 810, 802, 818, 819, 823, 831, 830, 798, 782, 783, 775, 771, 769, 768, 776, 792, 793, 797, 789, 533, 661, 669, 665, 664, 656, 640, 641, 643, 647, 655, 654, 670, 702, 703, 695, 691, 690, 674, 682, 683, 681, 685, 677, 676, 692, 756, 757, 765, 761, 763, 762, 730, 714, 715, 713, 717, 716, 748, 744, 736, 737, 739, 743, 742, 710, 726, 727, 735, 607, 591, 583, 579, 578, 594, 530, 534, 518, 550, 551, 547, 545, 544, 552, 568, 632, 624, 625, 627, 631, 630, 638, 622, 618, 619, 617, 621, 613, 612, 580, 596, 604, 540, 524, 525, 521, 523, 539, 27, 25, 24, 8, 0], |
| } |
|
|
| if d in presets: |
| return presets[d][:] |
| |
| return [0] |
| |
| n = 1 << d |
| seq = [] |
| for i in range(n): |
| g = i ^ (i >> 1) |
| seq.append(g) |
| return seq |
|
|
| def longest_sym_cib(d: int): |
| """ |
| Return a predefined longest symmetric coil-in-the-box |
| for certain dimensions, otherwise fall back to a trivial path. |
| """ |
| presets = { |
| 4: [0, 1, 3, 7, 15, 14, 12, 8, 0], |
| 5: [0, 1, 3, 7, 15, 31, 29, 21, 20, 22, 18, 26, 10, 8, 0], |
| 6: [0, 1, 3, 7, 15, 13, 29, 28, 20, 22, 18, 50, 48, 56, 57, 59, 63, 55, 53, 37, 36, 44, 46, 42, 10, 8, 0], |
| 7: [0, 1, 3, 7, 15, 31, 63, 127, 119, 103, 99, 107, 75, 73, 77, 69, 68, 100, 116, 124, 120, 121, 113, 81, 80, 82, 86, 94, 78, 110, 46, 38, 54, 50, 58, 26, 24, 28, 20, 21, 53, 37, 45, 41, 40, 32, 0], |
| } |
|
|
| if d in presets: |
| return presets[d][:] |
|
|
| return [0] |
|
|
|
|
| def shorter_cib(d: int): |
| """ |
| Return a predefined coil/snake for certain dimensions, |
| otherwise fall back to a standard Gray-code path. |
| """ |
| presets = { |
| 2: [0, 1, 3, 2, 0], |
| 3: [0, 1, 3, 2, 0], |
| 4: [0, 1, 3, 7, 6, 4, 0], |
| 5: [0, 1, 3, 7, 6, 14, 12, 13, 29, 21, 20, 16, 0], |
| 6: [0, 1, 3, 35, 39, 38, 6, 14, 12, 44, 45, 61, 29, 31, 27, 59, 58, 50, 18, 16, 0], |
| |
| |
| 7: [0], |
| 8: [0], |
| } |
|
|
| if d in presets: |
| return presets[d][:] |
| |
| return [0] |
| |
| n = 1 << d |
| seq = [] |
| for i in range(n): |
| g = i ^ (i >> 1) |
| seq.append(g) |
| return seq |
|
|
|
|
|
|
| def parse_path(text: str, d: int) -> List[int]: |
| if not text: |
| return [] |
| out = [] |
| for tok in [t for t in text.replace(",", " ").split() if t]: |
| if False and all(c in "01" for c in tok): |
| val = int(tok, 2) |
| elif tok.isdigit(): |
| val = int(tok) |
| else: |
| continue |
| if 0 <= val < (1 << d): |
| out.append(val) |
| return out |
|
|
| |
|
|
| def make_figure(d: int, |
| show_bits: bool, |
| show_ints: bool, |
| mark_negations: bool, |
| mark_distances: bool, |
| node_r: int, |
| edge_w: int, |
| path: List[int], |
| scale_base: float, |
| subsel: dict | None = None, |
| switchsel: dict | None = None, |
| layout_mode: str = "default"): |
| nodes, edges = build_hypercube(d) |
| pts, width, height = layout_positions(d, base=scale_base, mode=layout_mode) |
| pos = {vid: (x, y) for vid, x, y in pts} |
|
|
| path = path or [] |
| in_path = set(path) |
|
|
| |
| subsel = subsel or {} |
| sel_active = bool(subsel.get("active")) |
| sel_s = subsel.get("start_idx") |
| sel_e = subsel.get("end_idx") |
|
|
| selected_vertices = set() |
| if sel_active and sel_s is not None and sel_e is not None and path: |
| selected_vertices = set(path[sel_s:sel_e + 1]) |
|
|
| |
| switchsel = switchsel or {} |
| switch_active = bool(switchsel.get("active")) |
| picked = switchsel.get("picked") or [] |
| |
| switch_vertices = set() |
| switch_segment_edges = [] |
| |
| if switch_active and path: |
| is_closed = (len(path) >= 2 and path[0] == path[-1]) |
| cycle = path[:-1] if is_closed else path[:] |
| n = len(cycle) |
| |
| |
| if n > 0: |
| for idx in picked: |
| if isinstance(idx, int): |
| switch_vertices.add(cycle[idx % n] if is_closed else cycle[idx]) |
| |
| |
| if len(picked) == 3: |
| s0 = consecutive_triple_start([int(i) for i in picked], n, is_closed) |
| if s0 is not None: |
| a = cycle[s0 % n] if is_closed else cycle[s0] |
| b = cycle[(s0 + 1) % n] if is_closed else cycle[s0 + 1] |
| c = cycle[(s0 + 2) % n] if is_closed else cycle[s0 + 2] |
| switch_segment_edges = [(a, b), (b, c)] |
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| neighbor_set = set() |
| if mark_distances and path and len(path)>2: |
| for v in path[1:-1]: |
| for bit in range(d): |
| u = v ^ (1 << bit) |
| if u not in in_path: |
| neighbor_set.add(u) |
| |
| |
| neg_set = set() |
| neg_edges = set() |
| if mark_negations and path: |
| mask = (1 << d) - 1 |
|
|
| |
| for v in path: |
| neg_set.add(mask ^ v) |
|
|
| |
| for i in range(len(path) - 1): |
| a = path[i] |
| b = path[i + 1] |
| na = mask ^ a |
| nb = mask ^ b |
| key = (min(na, nb), max(na, nb)) |
| neg_edges.add(key) |
|
|
| |
| edge_traces = [] |
|
|
| for bit in range(d): |
| xs, ys, cd = [], [], [] |
| for (u, v, b) in edges: |
| if b != bit: |
| continue |
| x1, y1 = pos[u] |
| x2, y2 = pos[v] |
| xs += [x1, x2, None] |
| ys += [y1, y2, None] |
| cd += [[u, v], [u, v], None] |
| edge_traces.append( |
| go.Scatter( |
| x=xs, |
| y=ys, |
| mode="lines", |
| line=dict(width=edge_w, color=dim_color(bit)), |
| opacity=0.35, |
| hoverinfo="skip", |
| name=f"bit {bit}", |
| customdata=cd, |
| ) |
| ) |
|
|
| |
| path_edges = set() |
| for i in range(len(path) - 1): |
| a, b = path[i], path[i + 1] |
| key = (min(a, b), max(a, b)) |
| path_edges.add(key) |
|
|
| if path_edges: |
| for (u, v, b) in edges: |
| key = (min(u, v), max(u, v)) |
| if key in path_edges: |
| x1, y1 = pos[u] |
| x2, y2 = pos[v] |
| edge_traces.append( |
| go.Scatter( |
| x=[x1, x2], |
| y=[y1, y2], |
| mode="lines", |
| line=dict(width=max(1, edge_w * 1.6), color=dim_color(b)), |
| opacity=1.0, |
| hoverinfo="skip", |
| name=f"path bit {b}", |
| ) |
| ) |
|
|
| |
| if mark_negations and neg_edges: |
| for (u, v) in neg_edges: |
| x1, y1 = pos[u] |
| x2, y2 = pos[v] |
| edge_traces.append( |
| go.Scatter( |
| x=[x1, x2], |
| y=[y1, y2], |
| mode="lines", |
| line=dict(width=max(1, edge_w * 1.6), color="red"), |
| opacity=1.0, |
| hoverinfo="skip", |
| name="negated path", |
| ) |
| ) |
|
|
| |
| if sel_active and sel_s is not None and sel_e is not None and sel_e > sel_s: |
| for i in range(sel_s, sel_e): |
| a, b = path[i], path[i + 1] |
| x1, y1 = pos[a] |
| x2, y2 = pos[b] |
| edge_traces.append( |
| go.Scatter( |
| x=[x1, x2], |
| y=[y1, y2], |
| mode="lines", |
| line=dict(width=max(2, edge_w * 2), color="#2563EB"), |
| opacity=1.0, |
| hoverinfo="skip", |
| name="selected subpath", |
| ) |
| ) |
|
|
| |
| if switch_active and switch_segment_edges: |
| for (a, b) in switch_segment_edges: |
| x1, y1 = pos[a] |
| x2, y2 = pos[b] |
| edge_traces.append( |
| go.Scatter( |
| x=[x1, x2], |
| y=[y1, y2], |
| mode="lines", |
| line=dict(width=max(2, edge_w * 2), color="#DC2626"), |
| opacity=1.0, |
| hoverinfo="skip", |
| name="switch dims selection", |
| ) |
| ) |
|
|
|
|
| |
| |
| xs = [pos[v][0] for v in nodes] |
| ys = [pos[v][1] for v in nodes] |
|
|
| bit_labels = [int_to_bin(v, d) for v in nodes] |
| int_labels = [str(v) for v in nodes] |
|
|
| show_any_label = show_bits or show_ints |
| if show_bits and show_ints: |
| texts = [f"{iv}\n{bv}" for iv, bv in zip(int_labels, bit_labels)] |
| elif show_bits: |
| texts = bit_labels |
| elif show_ints: |
| texts = int_labels |
| else: |
| texts = None |
|
|
| sizes = [] |
| colors = [] |
| for v in nodes: |
| base_size = node_r * 2 |
|
|
| |
| if sel_active and v in selected_vertices: |
| sizes.append(base_size * 2.2) |
| elif switch_active and v in switch_vertices: |
| sizes.append(base_size * 2.2) |
| elif mark_distances and v in neighbor_set: |
| sizes.append(base_size * 1.2) |
| else: |
| sizes.append(base_size * (1.6 if v in in_path else 1)) |
|
|
| |
| if sel_active and v in selected_vertices: |
| colors.append("#2563EB") |
| elif switch_active and v in switch_vertices: |
| colors.append("#DC2626") |
| elif path and (v == path[0] or v == path[-1]): |
| colors.append("#111") |
| elif mark_negations and v in neg_set: |
| if mark_distances and v in neighbor_set: |
| colors.append("#F8650C") |
| else: |
| colors.append("red") |
| elif mark_distances and v in neighbor_set: |
| colors.append("yellow") |
| else: |
| colors.append("#222") |
|
|
|
|
|
|
| node_trace = go.Scatter( |
| x=xs, |
| y=ys, |
| mode="markers+text" if show_any_label else "markers", |
| marker=dict(size=sizes, color=colors, line=dict(width=1, color="#333")), |
| text=texts, |
| textposition="middle right", |
| textfont=dict(size=12, color="#333"), |
| hovertemplate=( |
| "id=%{customdata}<br>label=%{text}<extra></extra>" if show_any_label |
| else "id=%{customdata}<extra></extra>" |
| ), |
| customdata=nodes, |
| name="vertices", |
| ) |
|
|
| fig = go.Figure(edge_traces + [node_trace]) |
| pad = max(40, 0.08 * max(width, height)) |
| fig.update_layout( |
| showlegend=False, |
| margin=dict(l=20, r=20, t=40, b=20), |
| xaxis=dict(visible=False, range=[-pad, width + pad]), |
| yaxis=dict( |
| visible=False, |
| scaleanchor="x", |
| scaleratio=1, |
| range=[-pad, height + pad], |
| ), |
| dragmode=False, |
| template="plotly_white", |
| ) |
| return fig |
|
|
|
|
| |
|
|
| app = Dash(__name__) |
| app.title = "Hypercube Visualization and Path Exploration Tool" |
|
|
| app.layout = html.Div( |
| style={"maxWidth": "1200px", "margin": "0 auto", "padding": "0px"}, |
| children=[ |
| html.H2("Hypercube Visualization and Path Exploration Tool"), |
| html.Div(id="stats", style={"opacity": 0.7, "marginBottom": "0px"}), |
|
|
| html.Div( |
| style={"display": "grid", "gridTemplateColumns": "1fr 1fr 1fr 1fr", "gap": "8px", "marginTop": "20px"}, |
| children=[ |
| html.Div([ |
| html.Label("Dimension d"), |
| dcc.Slider( |
| id="dim", |
| min=1, |
| max=12, |
| step=1, |
| value=DEFAULTS["dimension"], |
| marks=None, |
| tooltip={"always_visible": True}, |
| ), |
| ]), |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| html.Div([ |
| dcc.Checklist( |
| id="show_labels", |
| options=[ |
| {"label": " Show bit labels", "value": "bits"}, |
| {"label": " Show int labels", "value": "ints"}, |
| ], |
| value=[], |
| style={"marginTop": "0px"}, |
| ) |
| ]), |
| html.Div([ |
| dcc.Checklist( |
| id="mark_negations", |
| options=[{"label": " Mark negations", "value": "neg"}], |
| value=[], |
| style={"marginTop": "0px", "color": "red"}, |
| ), |
| html.Div(style={"height": "2px"}), |
| html.Div( |
| id="mark_neighbors_wrap", |
| children=[ |
| dcc.Checklist( |
| id="mark_distances", |
| options=[{"label": " Mark neighbors", "value": "dist"}], |
| value=[], |
| style={"marginTop": "0px"}, |
| labelStyle={"display": "inline-block", "background-color": "yellow"}, |
| ) |
| ], |
| ), |
| ]), |
| |
| |
| html.Div( |
| style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"}, |
| children=[ |
| html.Button("Flip subpath", id="btn_flip_subpath", n_clicks=0, |
| style={"background": "#2563EB", "color": "white"}), |
| |
| html.Button("Switch dimensions", id="btn_switch_dims", n_clicks=0, |
| style={"background": "#6B7280", "color": "white"}), |
| |
| dcc.Input( |
| id="subpath_dim", |
| type="number", |
| min=0, |
| step=1, |
| value=0, |
| placeholder="dimension i", |
| style={"width": "130px"}, |
| ), |
| ], |
| ), |
| |
| dcc.Store( |
| id="subpath_select_store", |
| data={"active": False, "start_idx": None, "end_idx": None} |
| ), |
| |
| dcc.Store( |
| id="switch_dims_store", |
| data={"active": False, "picked": []} |
| ), |
|
|
| html.Div(), |
| ], |
| ), |
|
|
|
|
| html.Div( |
| style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"}, |
| children=[ |
| dcc.Input(id="manual_path", |
| placeholder="Enter path (e.g. 0,1,3)", |
| style={"flex": 1}, debounce=True), |
| html.Button("Set path", id="btn_set", n_clicks=0), |
| html.Button("Longest CIB", id="btn_longest_cib", n_clicks=0, |
| style={"background": "#059669", "color": "white"}), |
| html.Button("Longest Symmetric CIB", id="btn_longest_sym_cib", n_clicks=0, |
| style={"background": "#7DD3FC", "color": "#0B1220"}), |
| html.Button("Shorter CIB", id="btn_shorter_cib", n_clicks=0, |
| style={"background": "#00C04B", "color": "white"}), |
| html.Button("Clear", id="btn_clear", n_clicks=0), |
| ] |
| ), |
|
|
| |
| html.Div( |
| style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"}, |
| children=[ |
| html.Span("Swap dimensions:", style={"fontSize": "0.9rem"}), |
| dcc.Input( |
| id="swap_i", |
| type="number", |
| min=0, |
| step=1, |
| value=0, |
| placeholder="i", |
| style={"width": "60px"}, |
| ), |
| dcc.Input( |
| id="swap_j", |
| type="number", |
| min=0, |
| step=1, |
| value=0, |
| placeholder="j", |
| style={"width": "60px"}, |
| ), |
| html.Button("Swap", id="btn_swap", n_clicks=0), |
|
|
| html.Span("Flip dimension:", style={"fontSize": "0.9rem", "marginLeft": "16px"}), |
| dcc.Input( |
| id="flip_k", |
| type="number", |
| min=0, |
| step=1, |
| value=0, |
| placeholder="k", |
| style={"width": "60px"}, |
| ), |
| html.Button("Flip", id="btn_flip", n_clicks=0), |
|
|
| |
| html.Span(" ", style={"fontSize": "0.9rem", "marginLeft": "16px"}), |
| html.Button( |
| "Bipartite layout", |
| id="btn_bipartite_layout", |
| n_clicks=0, |
| style={"background": "#6B7280", "color": "white", "marginHorizontal": "48px"}, |
| ), |
| dcc.Store(id="layout_mode_store", data="default"), |
|
|
| ], |
| ), |
|
|
|
|
| html.Div( |
| id="path_info", |
| style={ |
| "marginBottom": "8px", |
| "fontFamily": "monospace", |
| "whiteSpace": "normal", |
| "overflow": "visible", |
| "textOverflow": "clip", |
| "lineHeight": "1.3", |
| }, |
| ), |
|
|
|
|
| dcc.Graph(id="fig", style={"height": "800px"}, config={"displayModeBar": True}), |
| dcc.Store(id="path_store", data=[]), |
|
|
| html.Div( |
| id="path_bits", |
| style={ |
| "marginTop": "12px", |
| "fontFamily": "monospace", |
| "whiteSpace": "pre-wrap", |
| "fontSize": "12px", |
| }, |
| ), |
| ] |
| ) |
|
|
| @app.callback( |
| Output("stats", "children"), |
| Input("dim", "value"), |
| Input("path_store", "data"), |
| ) |
| def stats(d, path): |
| n = 1 << d |
| m = d * (1 << (d - 1)) |
| plen = len(path) if path else 0 |
| return f"Q_{d} · vertices: {n} · edges: {m} · path length: {max(0, plen - 1)}" |
|
|
| @app.callback( |
| Output("path_store", "data"), |
| Output("subpath_select_store", "data"), |
| Output("switch_dims_store", "data"), |
| Input("fig", "clickData"), |
| Input("btn_clear", "n_clicks"), |
| Input("btn_longest_cib", "n_clicks"), |
| Input("btn_longest_sym_cib", "n_clicks"), |
| Input("btn_shorter_cib", "n_clicks"), |
| Input("btn_set", "n_clicks"), |
| Input("btn_swap", "n_clicks"), |
| Input("btn_flip", "n_clicks"), |
| Input("btn_flip_subpath", "n_clicks"), |
| Input("btn_switch_dims", "n_clicks"), |
| State("path_store", "data"), |
| State("manual_path", "value"), |
| State("dim", "value"), |
| State("swap_i", "value"), |
| State("swap_j", "value"), |
| State("flip_k", "value"), |
| State("subpath_select_store", "data"), |
| State("subpath_dim", "value"), |
| State("switch_dims_store", "data"), |
| prevent_initial_call=True |
| ) |
| def update_path(clickData, |
| n_clear, |
| n_longest, |
| n_longest_sym, |
| n_shorter, |
| n_set, |
| n_swap, |
| n_flip, |
| n_flip_subpath, |
| n_switch_dims, |
| path, |
| manual_text, |
| d, |
| swap_i, |
| swap_j, |
| flip_k, |
| subsel, |
| subpath_dim, |
| switchsel): |
|
|
| trigger = ctx.triggered_id |
| path = path or [] |
| d = int(d) |
|
|
| |
| subsel = subsel or {"active": False, "start_idx": None, "end_idx": None} |
| switchsel = switchsel or {"active": False, "start_idx": None, "end_idx": None} |
|
|
|
|
| |
| if trigger == "btn_clear": |
| return [], {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if trigger == "btn_longest_cib": |
| return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if trigger == "btn_longest_sym_cib": |
| return longest_sym_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| |
| if trigger == "btn_shorter_cib": |
| return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if trigger == "btn_set": |
| newp = parse_path(manual_text or "", d) |
| return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if trigger == "btn_swap": |
| try: |
| i = int(swap_i) if swap_i is not None else None |
| j = int(swap_j) if swap_j is not None else None |
| except (TypeError, ValueError): |
| return path, subsel, switchsel |
| if i is None or j is None: |
| return path, subsel, switchsel |
| if not (0 <= i < d and 0 <= j < d): |
| return path, subsel, switchsel |
| return swap_dims_path(path, d, i, j), subsel, switchsel |
|
|
| |
| if trigger == "btn_flip": |
| try: |
| k = int(flip_k) if flip_k is not None else None |
| except (TypeError, ValueError): |
| return path, subsel, switchsel |
| if k is None or not (0 <= k < d): |
| return path, subsel, switchsel |
| return flip_dim_path(path, d, k), subsel, switchsel |
|
|
| |
| |
| |
| if trigger == "btn_flip_subpath": |
| active = bool(subsel.get("active")) |
| if not active: |
| |
| return path, {"active": True, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| try: |
| i = int(subpath_dim) if subpath_dim is not None else None |
| except (TypeError, ValueError): |
| i = None |
| if i is None or not (0 <= i < d): |
| |
| return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| s = subsel.get("start_idx") |
| e = subsel.get("end_idx") |
| if s is None or e is None or e <= s: |
| |
| return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| |
| |
| head = path[:s + 1] |
| flipped_subpath = [flip_dim_vertex(v, i) for v in path[s:e + 1]] |
| rest = path[e:] |
| new_path = head + flipped_subpath + rest |
|
|
| return new_path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if trigger == "btn_switch_dims": |
| active = bool(switchsel.get("active")) |
| if not active: |
| return path, subsel, {"active": True, "picked": []} |
| else: |
| return path, subsel, {"active": False, "picked": []} |
|
|
| |
| |
| if trigger == "fig" and clickData and clickData.get("points"): |
| p = clickData["points"][0] |
| cd = p.get("customdata") |
|
|
| |
| |
| if switchsel.get("active") and isinstance(cd, (int, float)): |
| vid = int(cd) |
| |
| is_closed = (len(path) >= 2 and path[0] == path[-1]) |
| cycle = path[:-1] if is_closed else path[:] |
| n = len(cycle) |
| |
| if n < 3: |
| return path, subsel, {"active": False, "picked": []} |
| |
| |
| try: |
| idx = cycle.index(vid) |
| except ValueError: |
| return path, subsel, switchsel |
| |
| picked = list(switchsel.get("picked") or []) |
| |
| |
| if idx in picked: |
| picked = [i for i in picked if i != idx] |
| return path, subsel, {"active": True, "picked": picked} |
| |
| |
| if len(picked) >= 3: |
| |
| picked = [idx] |
| return path, subsel, {"active": True, "picked": picked} |
| |
| picked.append(idx) |
| |
| |
| if len(picked) < 3: |
| return path, subsel, {"active": True, "picked": picked} |
| |
| |
| s0 = consecutive_triple_start([int(i) for i in picked], n, is_closed) |
| if s0 is None: |
| |
| return path, subsel, {"active": True, "picked": picked} |
| |
| |
| x = cycle[s0 % n] if is_closed else cycle[s0] |
| y = cycle[(s0 + 1) % n] if is_closed else cycle[s0 + 1] |
| z = cycle[(s0 + 2) % n] if is_closed else cycle[s0 + 2] |
| |
| a = edge_dimension(x, y) |
| b = edge_dimension(y, z) |
| if a is None or b is None: |
| return path, subsel, {"active": False, "picked": []} |
| |
| |
| y2 = x ^ (1 << b) |
| |
| |
| if edge_dimension(y2, z) != a: |
| return path, subsel, {"active": False, "picked": []} |
| |
| |
| if y2 in set(cycle) and y2 not in {x, z}: |
| return path, subsel, {"active": False, "picked": []} |
| |
| cycle2 = cycle[:] |
| cycle2[(s0 + 1) % n if is_closed else (s0 + 1)] = y2 |
| |
| new_path = cycle2 + [cycle2[0]] if is_closed else cycle2 |
| return new_path, subsel, {"active": False, "picked": []} |
|
|
|
|
|
|
| |
| if subsel.get("active") and isinstance(cd, (int, float)): |
| vid = int(cd) |
| idx = index_in_path(path, vid) |
| if idx is None: |
| return path, subsel, switchsel |
|
|
| s = subsel.get("start_idx") |
| e = subsel.get("end_idx") |
|
|
| |
| if s is None: |
| return path, {"active": True, "start_idx": idx, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| |
| if e is None: |
| e = s |
| if idx == e + 1: |
| return path, {"active": True, "start_idx": s, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| if idx == e: |
| |
| new_e = e - 1 if e > s else s |
| return path, {"active": True, "start_idx": s, "end_idx": new_e}, {"active": False, "start_idx": None, "end_idx": None} |
|
|
| |
| return path, subsel, switchsel |
|
|
| |
| if isinstance(cd, (int, float)): |
| vid = int(cd) |
|
|
| if not path: |
| return [vid], subsel, switchsel |
|
|
| if vid == path[-1]: |
| return path[:-1], subsel, switchsel |
|
|
| if len(path) >= 2 and vid == path[-2]: |
| return path[:-1], subsel, switchsel |
|
|
| if hamming_dist(vid, path[-1]) == 1: |
| return path + [vid], subsel, switchsel |
|
|
| return [vid], subsel, switchsel |
|
|
| if isinstance(cd, (list, tuple)) and len(cd) == 2: |
| u, v = int(cd[0]), int(cd[1]) |
| if not path: |
| return [u, v], subsel, switchsel |
| last = path[-1] |
| if last == u: |
| return path + [v], subsel, switchsel |
| if last == v: |
| return path + [u], subsel, switchsel |
| return [u, v], subsel, switchsel |
|
|
| return path, subsel, switchsel |
|
|
|
|
| @app.callback( |
| Output("fig", "figure"), |
| Input("dim", "value"), |
| Input("show_labels", "value"), |
| Input("path_store", "data"), |
| Input("mark_negations", "value"), |
| Input("mark_distances", "value"), |
| Input("subpath_select_store", "data"), |
| Input("switch_dims_store", "data"), |
| Input("layout_mode_store", "data"), |
| ) |
| def render(d, show_labels_vals, path, mark_vals, mark_dist_vals, subsel, switchsel, layout_mode): |
| labels_vals = show_labels_vals or [] |
| show_bits = "bits" in labels_vals |
| show_ints = "ints" in labels_vals |
|
|
| mark_vals = mark_vals or [] |
| mark_negations = "neg" in mark_vals |
|
|
| mark_dist_vals = mark_dist_vals or [] |
| mark_distances = "dist" in mark_dist_vals |
|
|
| fig = make_figure( |
| d=int(d), |
| show_bits=show_bits, |
| show_ints=show_ints, |
| mark_negations=mark_negations, |
| mark_distances=mark_distances, |
| node_r=DEFAULTS["node_radius"], |
| edge_w=DEFAULTS["edge_width"], |
| path=path or [], |
| scale_base=float(DEFAULTS["scale"]), |
| subsel=subsel or {}, |
| switchsel=switchsel or {}, |
| layout_mode=layout_mode or "default", |
| ) |
| return fig |
|
|
|
|
| @app.callback( |
| Output("path_info", "children"), |
| Input("dim", "value"), |
| Input("path_store", "data"), |
| ) |
| def path_info(d, path): |
| path = path or [] |
| d = int(d) |
|
|
| if not path: |
| return html.Span("Path: (empty)") |
|
|
| |
| path_str = ", ".join(str(v) for v in path) |
|
|
| |
| label, valid = classify_path(path, d) |
| color = { |
| "snake": "green", |
| "coil": "green", |
| "symmetric coil": "green", |
| "almost coil": "green", |
| "not snake": "red", |
| }[label] |
|
|
| |
| dims = [] |
| for i in range(len(path) - 1): |
| ed = edge_dimension(path[i], path[i + 1]) |
| dims.append(ed if ed is not None else "?") |
| dims_str = ", ".join(str(x) for x in dims) if dims else "(none)" |
|
|
| |
| label_text = label |
| if label == "not snake": |
| viol = snake_violations(path, d) |
|
|
| pairs = [] |
| pairs.extend(viol.get("dup_vertices", [])) |
| pairs.extend(viol.get("non_adjacent_steps", [])) |
| pairs.extend(viol.get("bad_closing_edge", [])) |
| pairs.extend(viol.get("chords", [])) |
|
|
| |
| seen = set() |
| uniq = [] |
| for a, b in pairs: |
| key = (a, b) |
| if key not in seen: |
| seen.add(key) |
| uniq.append((a, b)) |
|
|
| MAX_SHOW = 30 |
| shown = uniq[:MAX_SHOW] |
| pairs_str = ", ".join(f"({a},{b})" for a, b in shown) |
| if len(uniq) > MAX_SHOW: |
| pairs_str += f", ... (+{len(uniq) - MAX_SHOW} more)" |
|
|
| label_text = f"not snake. violations: {pairs_str if pairs_str else '(none)'}" |
|
|
| return html.Div( |
| [ |
| html.Div( |
| [ |
| html.Span(f"Path: {path_str} "), |
| html.Span(f"[{label_text}]", style={"color": color, "fontWeight": "bold"}), |
| ] |
| ), |
| html.Div( |
| [ |
| html.Span("Dimensions: "), |
| html.Span(dims_str, style={"fontFamily": "monospace"}), |
| ] |
| ), |
| ] |
| ) |
|
|
|
|
| @app.callback( |
| Output("path_bits", "children"), |
| Input("dim", "value"), |
| Input("path_store", "data"), |
| Input("show_labels", "value"), |
| ) |
| def path_bits_view(d, path, show_labels_vals): |
| path = path or [] |
| labels_vals = show_labels_vals or [] |
|
|
| |
| if not path or "bits" not in labels_vals: |
| return "" |
|
|
| d = int(d) |
| lines = [] |
| for v in path: |
| b = int_to_bin(v, d) |
| ones = b.count("1") |
| lines.append(f"{b} ({ones})") |
|
|
| |
| return html.Pre( |
| "\n".join(lines), |
| style={"margin": 0}, |
| ) |
|
|
|
|
| @app.callback( |
| Output("btn_flip_subpath", "style"), |
| Input("subpath_select_store", "data"), |
| ) |
| def style_flip_subpath_button(subsel): |
| subsel = subsel or {} |
| active = bool(subsel.get("active")) |
|
|
| |
| base = { |
| "color": "white", |
| "border": "none", |
| "padding": "6px 12px", |
| "borderRadius": "8px", |
| "cursor": "pointer", |
| } |
|
|
| if active: |
| |
| return {**base, "background": "#2563EB"} |
| else: |
| |
| return {**base, "background": "#6B7280"} |
|
|
|
|
| @app.callback( |
| Output("layout_mode_store", "data"), |
| Input("btn_bipartite_layout", "n_clicks"), |
| State("layout_mode_store", "data"), |
| prevent_initial_call=True |
| ) |
| def toggle_layout(n, mode): |
| mode = mode or "default" |
| return "bipartite" if mode == "default" else "default" |
|
|
|
|
| @app.callback( |
| Output("btn_bipartite_layout", "style"), |
| Input("layout_mode_store", "data"), |
| ) |
| def style_layout_button(mode): |
| base = {"color": "white", "border": "none", "padding": "6px 12px", "borderRadius": "8px", "cursor": "pointer"} |
| if mode == "bipartite": |
| return {**base, "background": "#059669"} |
| return {**base, "background": "#6B7280"} |
|
|
|
|
| @app.callback( |
| Output("mark_neighbors_wrap", "children"), |
| Input("dim", "value"), |
| Input("path_store", "data"), |
| Input("mark_distances", "value"), |
| ) |
| def mark_neighbors_label(d, path, mark_dist_vals): |
| d = int(d) |
| path = path or [] |
| in_path = set(path) |
|
|
| |
| neighbor_set = set() |
| if path and len(path) > 2: |
| for v in path[1:-1]: |
| for bit in range(d): |
| u = v ^ (1 << bit) |
| if u not in in_path: |
| neighbor_set.add(u) |
|
|
| cnt = len(neighbor_set) |
|
|
| |
| mark_dist_vals = mark_dist_vals or [] |
|
|
| return dcc.Checklist( |
| id="mark_distances", |
| options=[{"label": f" Mark neighbors ({cnt})", "value": "dist"}], |
| value=mark_dist_vals, |
| style={"marginTop": "0px"}, |
| labelStyle={"display": "inline-block", "background-color": "yellow"}, |
| ) |
|
|
| @app.callback( |
| Output("btn_switch_dims", "style"), |
| Input("switch_dims_store", "data"), |
| ) |
| def style_switch_dims_button(switchsel): |
| switchsel = switchsel or {} |
| active = bool(switchsel.get("active")) |
|
|
| base = { |
| "color": "white", |
| "border": "none", |
| "padding": "6px 12px", |
| "borderRadius": "8px", |
| "cursor": "pointer", |
| } |
|
|
| return {**base, "background": "#DC2626"} if active else {**base, "background": "#6B7280"} |
| |
| if __name__ == "__main__": |
| import os |
| port = int(os.environ.get("PORT", "7860")) |
| app.run(host="0.0.0.0", port=port, debug=False) |
|
|
|
|