Update app.py
Browse files
app.py
CHANGED
|
@@ -23,6 +23,31 @@ DEFAULTS = {
|
|
| 23 |
|
| 24 |
# ---------- Hypercube utilities ----------
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def cycle_edge_dims(cycle: List[int], d: int) -> List[int] | None:
|
| 27 |
"""
|
| 28 |
cycle is vertex list WITHOUT repeating start at end.
|
|
@@ -540,17 +565,31 @@ def make_figure(d: int,
|
|
| 540 |
# ---------- selected vertices for switch-dims (3 consecutive vertices) ----------
|
| 541 |
switchsel = switchsel or {}
|
| 542 |
switch_active = bool(switchsel.get("active"))
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
switch_vertices = set()
|
| 547 |
-
|
|
|
|
|
|
|
| 548 |
is_closed = (len(path) >= 2 and path[0] == path[-1])
|
| 549 |
cycle = path[:-1] if is_closed else path[:]
|
| 550 |
n = len(cycle)
|
|
|
|
|
|
|
| 551 |
if n > 0:
|
| 552 |
-
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
|
| 555 |
|
| 556 |
|
|
@@ -677,29 +716,22 @@ def make_figure(d: int,
|
|
| 677 |
)
|
| 678 |
)
|
| 679 |
|
| 680 |
-
# ---------- highlight switch-dims selection edges ----------
|
| 681 |
-
if switch_active and
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
x=[x1, x2],
|
| 695 |
-
y=[y1, y2],
|
| 696 |
-
mode="lines",
|
| 697 |
-
line=dict(width=max(2, edge_w * 2), color="#DC2626"),
|
| 698 |
-
opacity=1.0,
|
| 699 |
-
hoverinfo="skip",
|
| 700 |
-
name="switch dims selection",
|
| 701 |
-
)
|
| 702 |
)
|
|
|
|
| 703 |
|
| 704 |
|
| 705 |
|
|
@@ -888,10 +920,9 @@ app.layout = html.Div(
|
|
| 888 |
|
| 889 |
dcc.Store(
|
| 890 |
id="switch_dims_store",
|
| 891 |
-
data={"active": False, "
|
| 892 |
),
|
| 893 |
|
| 894 |
-
|
| 895 |
html.Div(), # empty cell just to keep the grid tidy
|
| 896 |
],
|
| 897 |
),
|
|
@@ -1141,11 +1172,9 @@ def update_path(clickData,
|
|
| 1141 |
if trigger == "btn_switch_dims":
|
| 1142 |
active = bool(switchsel.get("active"))
|
| 1143 |
if not active:
|
| 1144 |
-
|
| 1145 |
-
return path, subsel, {"active": True, "start_idx": None, "end_idx": None}
|
| 1146 |
else:
|
| 1147 |
-
|
| 1148 |
-
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1149 |
|
| 1150 |
|
| 1151 |
# 8) Figure clicks
|
|
@@ -1154,82 +1183,75 @@ def update_path(clickData,
|
|
| 1154 |
cd = p.get("customdata")
|
| 1155 |
|
| 1156 |
# If we're in switch-dims mode, vertex clicks define exactly 3 consecutive vertices
|
|
|
|
| 1157 |
if switchsel.get("active") and isinstance(cd, (int, float)):
|
| 1158 |
vid = int(cd)
|
| 1159 |
-
|
| 1160 |
-
# Work on a cycle if path is explicitly closed
|
| 1161 |
is_closed = (len(path) >= 2 and path[0] == path[-1])
|
| 1162 |
cycle = path[:-1] if is_closed else path[:]
|
| 1163 |
n = len(cycle)
|
| 1164 |
-
|
| 1165 |
if n < 3:
|
| 1166 |
-
return path, subsel, {"active": False, "
|
| 1167 |
-
|
| 1168 |
-
#
|
| 1169 |
try:
|
| 1170 |
-
idx = cycle.index(vid)
|
| 1171 |
except ValueError:
|
| 1172 |
return path, subsel, switchsel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
|
| 1174 |
-
s = switchsel.get("start_idx")
|
| 1175 |
-
e = switchsel.get("end_idx")
|
| 1176 |
-
|
| 1177 |
-
# first vertex
|
| 1178 |
-
if s is None:
|
| 1179 |
-
return path, subsel, {"active": True, "start_idx": idx, "end_idx": idx}
|
| 1180 |
-
|
| 1181 |
-
if e is None:
|
| 1182 |
-
e = s
|
| 1183 |
-
|
| 1184 |
-
# distance forward on the cycle (0,1,2,...)
|
| 1185 |
-
dist = (e - s) % n
|
| 1186 |
-
|
| 1187 |
-
# must extend consecutively forward (with wrap)
|
| 1188 |
-
expected = (e + 1) % n
|
| 1189 |
-
if idx == expected and dist < 2:
|
| 1190 |
-
new_e = idx
|
| 1191 |
-
new_dist = (new_e - s) % n
|
| 1192 |
-
|
| 1193 |
-
# if we have 3 vertices selected: apply switch
|
| 1194 |
-
if new_dist == 2:
|
| 1195 |
-
x = cycle[s]
|
| 1196 |
-
y = cycle[(s + 1) % n]
|
| 1197 |
-
z = cycle[(s + 2) % n]
|
| 1198 |
-
|
| 1199 |
-
a = edge_dimension(x, y)
|
| 1200 |
-
b = edge_dimension(y, z)
|
| 1201 |
-
if a is None or b is None:
|
| 1202 |
-
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1203 |
-
|
| 1204 |
-
# new middle vertex to traverse b then a
|
| 1205 |
-
y2 = x ^ (1 << b)
|
| 1206 |
-
|
| 1207 |
-
# verify it actually reaches z by traversing a next
|
| 1208 |
-
if edge_dimension(y2, z) != a:
|
| 1209 |
-
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1210 |
-
|
| 1211 |
-
# avoid creating duplicates in the cycle (allow x,z)
|
| 1212 |
-
if y2 in set(cycle) and y2 not in {x, z}:
|
| 1213 |
-
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1214 |
-
|
| 1215 |
-
cycle2 = cycle[:]
|
| 1216 |
-
cycle2[(s + 1) % n] = y2
|
| 1217 |
-
|
| 1218 |
-
new_path = cycle2 + [cycle2[0]] if is_closed else cycle2
|
| 1219 |
-
return new_path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1220 |
-
|
| 1221 |
-
return path, subsel, {"active": True, "start_idx": s, "end_idx": new_e}
|
| 1222 |
-
|
| 1223 |
-
# allow shrinking by clicking current end
|
| 1224 |
-
if idx == e:
|
| 1225 |
-
if e != s:
|
| 1226 |
-
new_e = (e - 1) % n
|
| 1227 |
-
else:
|
| 1228 |
-
new_e = s
|
| 1229 |
-
return path, subsel, {"active": True, "start_idx": s, "end_idx": new_e}
|
| 1230 |
-
|
| 1231 |
-
# otherwise ignore
|
| 1232 |
-
return path, subsel, switchsel
|
| 1233 |
|
| 1234 |
|
| 1235 |
# If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
|
|
|
|
| 23 |
|
| 24 |
# ---------- Hypercube utilities ----------
|
| 25 |
|
| 26 |
+
|
| 27 |
+
def consecutive_triple_start(indices: list[int], n: int, is_closed: bool) -> int | None:
|
| 28 |
+
"""
|
| 29 |
+
Given 3 indices, return the start index s such that {s,s+1,s+2} (mod n if closed)
|
| 30 |
+
equals the given set. If not possible, return None.
|
| 31 |
+
"""
|
| 32 |
+
if len(indices) != 3 or n <= 0:
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
S = set(indices)
|
| 36 |
+
|
| 37 |
+
if is_closed:
|
| 38 |
+
for s in range(n):
|
| 39 |
+
if {(s) % n, (s + 1) % n, (s + 2) % n} == S:
|
| 40 |
+
return s
|
| 41 |
+
return None
|
| 42 |
+
else:
|
| 43 |
+
# open path: must be literal consecutive indices
|
| 44 |
+
mn = min(S)
|
| 45 |
+
if S == {mn, mn + 1, mn + 2} and (mn + 2) < n:
|
| 46 |
+
return mn
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
def cycle_edge_dims(cycle: List[int], d: int) -> List[int] | None:
|
| 52 |
"""
|
| 53 |
cycle is vertex list WITHOUT repeating start at end.
|
|
|
|
| 565 |
# ---------- selected vertices for switch-dims (3 consecutive vertices) ----------
|
| 566 |
switchsel = switchsel or {}
|
| 567 |
switch_active = bool(switchsel.get("active"))
|
| 568 |
+
picked = switchsel.get("picked") or []
|
| 569 |
+
|
|
|
|
| 570 |
switch_vertices = set()
|
| 571 |
+
switch_segment_edges = [] # list of (a,b) edges to highlight if we can infer the triple segment
|
| 572 |
+
|
| 573 |
+
if switch_active and path:
|
| 574 |
is_closed = (len(path) >= 2 and path[0] == path[-1])
|
| 575 |
cycle = path[:-1] if is_closed else path[:]
|
| 576 |
n = len(cycle)
|
| 577 |
+
|
| 578 |
+
# picked are indices into cycle/path
|
| 579 |
if n > 0:
|
| 580 |
+
for idx in picked:
|
| 581 |
+
if isinstance(idx, int):
|
| 582 |
+
switch_vertices.add(cycle[idx % n] if is_closed else cycle[idx])
|
| 583 |
+
|
| 584 |
+
# If exactly 3 picked and they form a consecutive triple, highlight the 2 edges of that triple
|
| 585 |
+
if len(picked) == 3:
|
| 586 |
+
s0 = consecutive_triple_start([int(i) for i in picked], n, is_closed)
|
| 587 |
+
if s0 is not None:
|
| 588 |
+
a = cycle[s0 % n] if is_closed else cycle[s0]
|
| 589 |
+
b = cycle[(s0 + 1) % n] if is_closed else cycle[s0 + 1]
|
| 590 |
+
c = cycle[(s0 + 2) % n] if is_closed else cycle[s0 + 2]
|
| 591 |
+
switch_segment_edges = [(a, b), (b, c)]
|
| 592 |
+
|
| 593 |
|
| 594 |
|
| 595 |
|
|
|
|
| 716 |
)
|
| 717 |
)
|
| 718 |
|
| 719 |
+
# ---------- highlight switch-dims selection edges (only when triple is valid) ----------
|
| 720 |
+
if switch_active and switch_segment_edges:
|
| 721 |
+
for (a, b) in switch_segment_edges:
|
| 722 |
+
x1, y1 = pos[a]
|
| 723 |
+
x2, y2 = pos[b]
|
| 724 |
+
edge_traces.append(
|
| 725 |
+
go.Scatter(
|
| 726 |
+
x=[x1, x2],
|
| 727 |
+
y=[y1, y2],
|
| 728 |
+
mode="lines",
|
| 729 |
+
line=dict(width=max(2, edge_w * 2), color="#DC2626"),
|
| 730 |
+
opacity=1.0,
|
| 731 |
+
hoverinfo="skip",
|
| 732 |
+
name="switch dims selection",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
)
|
| 734 |
+
)
|
| 735 |
|
| 736 |
|
| 737 |
|
|
|
|
| 920 |
|
| 921 |
dcc.Store(
|
| 922 |
id="switch_dims_store",
|
| 923 |
+
data={"active": False, "picked": []} # picked holds indices into cycle/path
|
| 924 |
),
|
| 925 |
|
|
|
|
| 926 |
html.Div(), # empty cell just to keep the grid tidy
|
| 927 |
],
|
| 928 |
),
|
|
|
|
| 1172 |
if trigger == "btn_switch_dims":
|
| 1173 |
active = bool(switchsel.get("active"))
|
| 1174 |
if not active:
|
| 1175 |
+
return path, subsel, {"active": True, "picked": []}
|
|
|
|
| 1176 |
else:
|
| 1177 |
+
return path, subsel, {"active": False, "picked": []}
|
|
|
|
| 1178 |
|
| 1179 |
|
| 1180 |
# 8) Figure clicks
|
|
|
|
| 1183 |
cd = p.get("customdata")
|
| 1184 |
|
| 1185 |
# If we're in switch-dims mode, vertex clicks define exactly 3 consecutive vertices
|
| 1186 |
+
# If we're in switch-dims mode, user can pick 3 consecutive vertices in ANY order
|
| 1187 |
if switchsel.get("active") and isinstance(cd, (int, float)):
|
| 1188 |
vid = int(cd)
|
| 1189 |
+
|
|
|
|
| 1190 |
is_closed = (len(path) >= 2 and path[0] == path[-1])
|
| 1191 |
cycle = path[:-1] if is_closed else path[:]
|
| 1192 |
n = len(cycle)
|
| 1193 |
+
|
| 1194 |
if n < 3:
|
| 1195 |
+
return path, subsel, {"active": False, "picked": []}
|
| 1196 |
+
|
| 1197 |
+
# find index of clicked vertex inside cycle/path
|
| 1198 |
try:
|
| 1199 |
+
idx = cycle.index(vid) # vertices assumed unique in a valid snake/coil
|
| 1200 |
except ValueError:
|
| 1201 |
return path, subsel, switchsel
|
| 1202 |
+
|
| 1203 |
+
picked = list(switchsel.get("picked") or [])
|
| 1204 |
+
|
| 1205 |
+
# toggle behavior: click again removes it
|
| 1206 |
+
if idx in picked:
|
| 1207 |
+
picked = [i for i in picked if i != idx]
|
| 1208 |
+
return path, subsel, {"active": True, "picked": picked}
|
| 1209 |
+
|
| 1210 |
+
# add it (up to 3)
|
| 1211 |
+
if len(picked) >= 3:
|
| 1212 |
+
# start over from this click
|
| 1213 |
+
picked = [idx]
|
| 1214 |
+
return path, subsel, {"active": True, "picked": picked}
|
| 1215 |
+
|
| 1216 |
+
picked.append(idx)
|
| 1217 |
+
|
| 1218 |
+
# if not yet 3, just store
|
| 1219 |
+
if len(picked) < 3:
|
| 1220 |
+
return path, subsel, {"active": True, "picked": picked}
|
| 1221 |
+
|
| 1222 |
+
# now we have 3: check if they form a consecutive triple
|
| 1223 |
+
s0 = consecutive_triple_start([int(i) for i in picked], n, is_closed)
|
| 1224 |
+
if s0 is None:
|
| 1225 |
+
# not a valid triple, keep them so user can adjust (toggle)
|
| 1226 |
+
return path, subsel, {"active": True, "picked": picked}
|
| 1227 |
+
|
| 1228 |
+
# canonical triple order along the cycle/path
|
| 1229 |
+
x = cycle[s0 % n] if is_closed else cycle[s0]
|
| 1230 |
+
y = cycle[(s0 + 1) % n] if is_closed else cycle[s0 + 1]
|
| 1231 |
+
z = cycle[(s0 + 2) % n] if is_closed else cycle[s0 + 2]
|
| 1232 |
+
|
| 1233 |
+
a = edge_dimension(x, y)
|
| 1234 |
+
b = edge_dimension(y, z)
|
| 1235 |
+
if a is None or b is None:
|
| 1236 |
+
return path, subsel, {"active": False, "picked": []}
|
| 1237 |
+
|
| 1238 |
+
# new middle vertex to traverse b then a
|
| 1239 |
+
y2 = x ^ (1 << b)
|
| 1240 |
+
|
| 1241 |
+
# verify the new 2-edge path really reaches z via dimensions b then a
|
| 1242 |
+
if edge_dimension(y2, z) != a:
|
| 1243 |
+
return path, subsel, {"active": False, "picked": []}
|
| 1244 |
+
|
| 1245 |
+
# avoid duplicates in the cycle/path (allow x and z)
|
| 1246 |
+
if y2 in set(cycle) and y2 not in {x, z}:
|
| 1247 |
+
return path, subsel, {"active": False, "picked": []}
|
| 1248 |
+
|
| 1249 |
+
cycle2 = cycle[:]
|
| 1250 |
+
cycle2[(s0 + 1) % n if is_closed else (s0 + 1)] = y2
|
| 1251 |
+
|
| 1252 |
+
new_path = cycle2 + [cycle2[0]] if is_closed else cycle2
|
| 1253 |
+
return new_path, subsel, {"active": False, "picked": []}
|
| 1254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
|
| 1256 |
|
| 1257 |
# If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
|