Update app.py
Browse files
app.py
CHANGED
|
@@ -23,6 +23,14 @@ DEFAULTS = {
|
|
| 23 |
|
| 24 |
# ---------- Hypercube utilities ----------
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def swap_dims_vertex(v: int, i: int, j: int) -> int:
|
| 27 |
"""
|
| 28 |
Swap bits i and j in the vertex id v.
|
|
@@ -499,7 +507,7 @@ app.layout = html.Div(
|
|
| 499 |
html.Div(id="stats", style={"opacity": 0.7, "marginBottom": "0px"}),
|
| 500 |
|
| 501 |
html.Div(
|
| 502 |
-
style={"display": "grid", "gridTemplateColumns": "1fr 1fr 1fr", "gap": "8px", "marginTop": "20px"},
|
| 503 |
children=[
|
| 504 |
html.Div([
|
| 505 |
html.Label("Dimension d"),
|
|
@@ -552,6 +560,30 @@ app.layout = html.Div(
|
|
| 552 |
labelStyle={"display": "inline-block","background-color": "yellow"}
|
| 553 |
),
|
| 554 |
]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
|
| 556 |
html.Div(), # empty cell just to keep the grid tidy
|
| 557 |
],
|
|
@@ -654,6 +686,7 @@ def stats(d, path):
|
|
| 654 |
|
| 655 |
@app.callback(
|
| 656 |
Output("path_store", "data"),
|
|
|
|
| 657 |
Input("fig", "clickData"),
|
| 658 |
Input("btn_clear", "n_clicks"),
|
| 659 |
Input("btn_longest_cib", "n_clicks"),
|
|
@@ -661,12 +694,15 @@ def stats(d, path):
|
|
| 661 |
Input("btn_set", "n_clicks"),
|
| 662 |
Input("btn_swap", "n_clicks"),
|
| 663 |
Input("btn_flip", "n_clicks"),
|
|
|
|
| 664 |
State("path_store", "data"),
|
| 665 |
State("manual_path", "value"),
|
| 666 |
State("dim", "value"),
|
| 667 |
State("swap_i", "value"),
|
| 668 |
State("swap_j", "value"),
|
| 669 |
State("flip_k", "value"),
|
|
|
|
|
|
|
| 670 |
prevent_initial_call=True
|
| 671 |
)
|
| 672 |
def update_path(clickData,
|
|
@@ -676,101 +712,164 @@ def update_path(clickData,
|
|
| 676 |
n_set,
|
| 677 |
n_swap,
|
| 678 |
n_flip,
|
|
|
|
| 679 |
path,
|
| 680 |
manual_text,
|
| 681 |
d,
|
| 682 |
swap_i,
|
| 683 |
swap_j,
|
| 684 |
-
flip_k
|
|
|
|
|
|
|
|
|
|
| 685 |
trigger = ctx.triggered_id
|
| 686 |
path = path or []
|
| 687 |
d = int(d)
|
| 688 |
|
|
|
|
|
|
|
| 689 |
|
| 690 |
# 1) Clear
|
| 691 |
if trigger == "btn_clear":
|
| 692 |
-
return []
|
| 693 |
|
| 694 |
# 2a) Longest CIB
|
| 695 |
if trigger == "btn_longest_cib":
|
| 696 |
-
return longest_cib(d)
|
| 697 |
|
| 698 |
# 2b) Shorter CIB
|
| 699 |
if trigger == "btn_shorter_cib":
|
| 700 |
-
return shorter_cib(d)
|
| 701 |
|
| 702 |
# 3) Manual set path
|
| 703 |
if trigger == "btn_set":
|
| 704 |
newp = parse_path(manual_text or "", d)
|
| 705 |
-
return newp if newp else path
|
| 706 |
|
| 707 |
-
# 4) Swap two dimensions
|
| 708 |
if trigger == "btn_swap":
|
| 709 |
try:
|
| 710 |
i = int(swap_i) if swap_i is not None else None
|
| 711 |
j = int(swap_j) if swap_j is not None else None
|
| 712 |
except (TypeError, ValueError):
|
| 713 |
-
return path
|
| 714 |
if i is None or j is None:
|
| 715 |
-
return path
|
| 716 |
-
# clamp to valid range
|
| 717 |
if not (0 <= i < d and 0 <= j < d):
|
| 718 |
-
return path
|
| 719 |
-
return swap_dims_path(path, d, i, j)
|
| 720 |
|
| 721 |
-
# 5) Flip one dimension (
|
| 722 |
if trigger == "btn_flip":
|
| 723 |
try:
|
| 724 |
k = int(flip_k) if flip_k is not None else None
|
| 725 |
except (TypeError, ValueError):
|
| 726 |
-
return path
|
| 727 |
if k is None or not (0 <= k < d):
|
| 728 |
-
return path
|
| 729 |
-
return flip_dim_path(path, d, k)
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
if trigger == "fig" and clickData and clickData.get("points"):
|
| 734 |
p = clickData["points"][0]
|
| 735 |
cd = p.get("customdata")
|
| 736 |
|
| 737 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
if isinstance(cd, (int, float)):
|
| 739 |
vid = int(cd)
|
| 740 |
|
| 741 |
-
# If no path yet → start it
|
| 742 |
if not path:
|
| 743 |
-
return [vid]
|
| 744 |
|
| 745 |
-
# If clicked the *last* vertex again → remove it (un-click)
|
| 746 |
if vid == path[-1]:
|
| 747 |
-
return path[:-1]
|
| 748 |
|
| 749 |
-
# If clicked a vertex that is directly before the last one → also allow backtracking
|
| 750 |
if len(path) >= 2 and vid == path[-2]:
|
| 751 |
-
return path[:-1]
|
| 752 |
|
| 753 |
-
# Otherwise, if adjacent to last vertex → extend path
|
| 754 |
if hamming_dist(vid, path[-1]) == 1:
|
| 755 |
-
return path + [vid]
|
| 756 |
|
| 757 |
-
|
| 758 |
-
return [vid]
|
| 759 |
|
| 760 |
-
|
| 761 |
-
# Edge click → add other endpoint if extending current endpoint
|
| 762 |
if isinstance(cd, (list, tuple)) and len(cd) == 2:
|
| 763 |
u, v = int(cd[0]), int(cd[1])
|
| 764 |
if not path:
|
| 765 |
-
return [u, v]
|
| 766 |
last = path[-1]
|
| 767 |
if last == u:
|
| 768 |
-
return path + [v]
|
| 769 |
if last == v:
|
| 770 |
-
return path + [u]
|
| 771 |
-
return [u, v]
|
|
|
|
|
|
|
| 772 |
|
| 773 |
-
return path
|
| 774 |
|
| 775 |
@app.callback(
|
| 776 |
Output("fig", "figure"),
|
|
@@ -881,6 +980,21 @@ def path_bits_view(d, path, show_labels_vals):
|
|
| 881 |
style={"margin": 0},
|
| 882 |
)
|
| 883 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
|
| 885 |
|
| 886 |
if __name__ == "__main__":
|
|
|
|
| 23 |
|
| 24 |
# ---------- Hypercube utilities ----------
|
| 25 |
|
| 26 |
+
def index_in_path(path: List[int], vid: int):
|
| 27 |
+
"""Return the index of vid in path (first occurrence), or None."""
|
| 28 |
+
try:
|
| 29 |
+
return path.index(vid)
|
| 30 |
+
except ValueError:
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def swap_dims_vertex(v: int, i: int, j: int) -> int:
|
| 35 |
"""
|
| 36 |
Swap bits i and j in the vertex id v.
|
|
|
|
| 507 |
html.Div(id="stats", style={"opacity": 0.7, "marginBottom": "0px"}),
|
| 508 |
|
| 509 |
html.Div(
|
| 510 |
+
style={"display": "grid", "gridTemplateColumns": "1fr 1fr 1fr 1fr", "gap": "8px", "marginTop": "20px"},
|
| 511 |
children=[
|
| 512 |
html.Div([
|
| 513 |
html.Label("Dimension d"),
|
|
|
|
| 560 |
labelStyle={"display": "inline-block","background-color": "yellow"}
|
| 561 |
),
|
| 562 |
]),
|
| 563 |
+
|
| 564 |
+
# --- Subpath flip controls ---
|
| 565 |
+
html.Div(
|
| 566 |
+
style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"},
|
| 567 |
+
children=[
|
| 568 |
+
html.Button("Flip subpath (2 clicks)", id="btn_flip_subpath", n_clicks=0,
|
| 569 |
+
style={"background": "#2563EB", "color": "white"}),
|
| 570 |
+
dcc.Input(
|
| 571 |
+
id="subpath_dim",
|
| 572 |
+
type="number",
|
| 573 |
+
min=0,
|
| 574 |
+
step=1,
|
| 575 |
+
value=0,
|
| 576 |
+
placeholder="dimension i",
|
| 577 |
+
style={"width": "130px"},
|
| 578 |
+
),
|
| 579 |
+
html.Span(id="subpath_status", style={"opacity": 0.8}),
|
| 580 |
+
],
|
| 581 |
+
),
|
| 582 |
+
|
| 583 |
+
dcc.Store(
|
| 584 |
+
id="subpath_select_store",
|
| 585 |
+
data={"active": False, "start_idx": None, "end_idx": None}
|
| 586 |
+
),
|
| 587 |
|
| 588 |
html.Div(), # empty cell just to keep the grid tidy
|
| 589 |
],
|
|
|
|
| 686 |
|
| 687 |
@app.callback(
|
| 688 |
Output("path_store", "data"),
|
| 689 |
+
Output("subpath_select_store", "data"), # NEW
|
| 690 |
Input("fig", "clickData"),
|
| 691 |
Input("btn_clear", "n_clicks"),
|
| 692 |
Input("btn_longest_cib", "n_clicks"),
|
|
|
|
| 694 |
Input("btn_set", "n_clicks"),
|
| 695 |
Input("btn_swap", "n_clicks"),
|
| 696 |
Input("btn_flip", "n_clicks"),
|
| 697 |
+
Input("btn_flip_subpath", "n_clicks"), # NEW
|
| 698 |
State("path_store", "data"),
|
| 699 |
State("manual_path", "value"),
|
| 700 |
State("dim", "value"),
|
| 701 |
State("swap_i", "value"),
|
| 702 |
State("swap_j", "value"),
|
| 703 |
State("flip_k", "value"),
|
| 704 |
+
State("subpath_select_store", "data"), # NEW
|
| 705 |
+
State("subpath_dim", "value"), # NEW
|
| 706 |
prevent_initial_call=True
|
| 707 |
)
|
| 708 |
def update_path(clickData,
|
|
|
|
| 712 |
n_set,
|
| 713 |
n_swap,
|
| 714 |
n_flip,
|
| 715 |
+
n_flip_subpath,
|
| 716 |
path,
|
| 717 |
manual_text,
|
| 718 |
d,
|
| 719 |
swap_i,
|
| 720 |
swap_j,
|
| 721 |
+
flip_k,
|
| 722 |
+
subsel,
|
| 723 |
+
subpath_dim):
|
| 724 |
+
|
| 725 |
trigger = ctx.triggered_id
|
| 726 |
path = path or []
|
| 727 |
d = int(d)
|
| 728 |
|
| 729 |
+
# default selection store
|
| 730 |
+
subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
|
| 731 |
|
| 732 |
# 1) Clear
|
| 733 |
if trigger == "btn_clear":
|
| 734 |
+
return [], {"active": False, "start_idx": None, "end_idx": None}
|
| 735 |
|
| 736 |
# 2a) Longest CIB
|
| 737 |
if trigger == "btn_longest_cib":
|
| 738 |
+
return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}
|
| 739 |
|
| 740 |
# 2b) Shorter CIB
|
| 741 |
if trigger == "btn_shorter_cib":
|
| 742 |
+
return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}
|
| 743 |
|
| 744 |
# 3) Manual set path
|
| 745 |
if trigger == "btn_set":
|
| 746 |
newp = parse_path(manual_text or "", d)
|
| 747 |
+
return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}
|
| 748 |
|
| 749 |
+
# 4) Swap two dimensions
|
| 750 |
if trigger == "btn_swap":
|
| 751 |
try:
|
| 752 |
i = int(swap_i) if swap_i is not None else None
|
| 753 |
j = int(swap_j) if swap_j is not None else None
|
| 754 |
except (TypeError, ValueError):
|
| 755 |
+
return path, subsel
|
| 756 |
if i is None or j is None:
|
| 757 |
+
return path, subsel
|
|
|
|
| 758 |
if not (0 <= i < d and 0 <= j < d):
|
| 759 |
+
return path, subsel
|
| 760 |
+
return swap_dims_path(path, d, i, j), subsel
|
| 761 |
|
| 762 |
+
# 5) Flip one dimension (whole path)
|
| 763 |
if trigger == "btn_flip":
|
| 764 |
try:
|
| 765 |
k = int(flip_k) if flip_k is not None else None
|
| 766 |
except (TypeError, ValueError):
|
| 767 |
+
return path, subsel
|
| 768 |
if k is None or not (0 <= k < d):
|
| 769 |
+
return path, subsel
|
| 770 |
+
return flip_dim_path(path, d, k), subsel
|
| 771 |
+
|
| 772 |
+
# 6) The new two-click button:
|
| 773 |
+
# First click: enter selection mode.
|
| 774 |
+
# Second click: apply flip on the selected subpath and exit mode.
|
| 775 |
+
if trigger == "btn_flip_subpath":
|
| 776 |
+
active = bool(subsel.get("active"))
|
| 777 |
+
if not active:
|
| 778 |
+
# enter selection mode
|
| 779 |
+
return path, {"active": True, "start_idx": None, "end_idx": None}
|
| 780 |
+
|
| 781 |
+
# active == True, so this is the "second click": apply
|
| 782 |
+
try:
|
| 783 |
+
i = int(subpath_dim) if subpath_dim is not None else None
|
| 784 |
+
except (TypeError, ValueError):
|
| 785 |
+
i = None
|
| 786 |
+
if i is None or not (0 <= i < d):
|
| 787 |
+
# invalid dimension, just exit selection mode
|
| 788 |
+
return path, {"active": False, "start_idx": None, "end_idx": None}
|
| 789 |
+
|
| 790 |
+
s = subsel.get("start_idx")
|
| 791 |
+
e = subsel.get("end_idx")
|
| 792 |
+
if s is None or e is None or e <= s:
|
| 793 |
+
# nothing meaningful selected, exit
|
| 794 |
+
return path, {"active": False, "start_idx": None, "end_idx": None}
|
| 795 |
+
|
| 796 |
+
# subpath is path[s:e+1] = (v1,...,vk)
|
| 797 |
+
# requirement: remove v2..vk and replace them with flipped(v2..vk)
|
| 798 |
+
# so v1 stays as-is, and we replace the tail of the selected subpath.
|
| 799 |
+
head = path[:s + 1]
|
| 800 |
+
tail_selected = path[s + 1:e + 1] # v2..vk
|
| 801 |
+
flipped_tail = [flip_dim_vertex(v, i) for v in tail_selected]
|
| 802 |
+
rest = path[e + 1:]
|
| 803 |
+
new_path = head + flipped_tail + rest
|
| 804 |
+
|
| 805 |
+
return new_path, {"active": False, "start_idx": None, "end_idx": None}
|
| 806 |
+
|
| 807 |
+
# 7) Figure clicks
|
| 808 |
if trigger == "fig" and clickData and clickData.get("points"):
|
| 809 |
p = clickData["points"][0]
|
| 810 |
cd = p.get("customdata")
|
| 811 |
|
| 812 |
+
# If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
|
| 813 |
+
if subsel.get("active") and isinstance(cd, (int, float)):
|
| 814 |
+
vid = int(cd)
|
| 815 |
+
idx = index_in_path(path, vid)
|
| 816 |
+
if idx is None:
|
| 817 |
+
return path, subsel
|
| 818 |
+
|
| 819 |
+
s = subsel.get("start_idx")
|
| 820 |
+
e = subsel.get("end_idx")
|
| 821 |
+
|
| 822 |
+
# first selected vertex
|
| 823 |
+
if s is None:
|
| 824 |
+
return path, {"active": True, "start_idx": idx, "end_idx": idx}
|
| 825 |
+
|
| 826 |
+
# enforce consecutive forward selection along the path
|
| 827 |
+
# user must click idx == e+1 to extend
|
| 828 |
+
if e is None:
|
| 829 |
+
e = s
|
| 830 |
+
if idx == e + 1:
|
| 831 |
+
return path, {"active": True, "start_idx": s, "end_idx": idx}
|
| 832 |
+
|
| 833 |
+
# allow shrinking by clicking the current end again
|
| 834 |
+
if idx == e:
|
| 835 |
+
# pop last selected (shrink by 1) if possible
|
| 836 |
+
new_e = e - 1 if e > s else s
|
| 837 |
+
return path, {"active": True, "start_idx": s, "end_idx": new_e}
|
| 838 |
+
|
| 839 |
+
# otherwise ignore
|
| 840 |
+
return path, subsel
|
| 841 |
+
|
| 842 |
+
# Normal mode: your existing click-to-build-path behavior
|
| 843 |
if isinstance(cd, (int, float)):
|
| 844 |
vid = int(cd)
|
| 845 |
|
|
|
|
| 846 |
if not path:
|
| 847 |
+
return [vid], subsel
|
| 848 |
|
|
|
|
| 849 |
if vid == path[-1]:
|
| 850 |
+
return path[:-1], subsel
|
| 851 |
|
|
|
|
| 852 |
if len(path) >= 2 and vid == path[-2]:
|
| 853 |
+
return path[:-1], subsel
|
| 854 |
|
|
|
|
| 855 |
if hamming_dist(vid, path[-1]) == 1:
|
| 856 |
+
return path + [vid], subsel
|
| 857 |
|
| 858 |
+
return [vid], subsel
|
|
|
|
| 859 |
|
|
|
|
|
|
|
| 860 |
if isinstance(cd, (list, tuple)) and len(cd) == 2:
|
| 861 |
u, v = int(cd[0]), int(cd[1])
|
| 862 |
if not path:
|
| 863 |
+
return [u, v], subsel
|
| 864 |
last = path[-1]
|
| 865 |
if last == u:
|
| 866 |
+
return path + [v], subsel
|
| 867 |
if last == v:
|
| 868 |
+
return path + [u], subsel
|
| 869 |
+
return [u, v], subsel
|
| 870 |
+
|
| 871 |
+
return path, subsel
|
| 872 |
|
|
|
|
| 873 |
|
| 874 |
@app.callback(
|
| 875 |
Output("fig", "figure"),
|
|
|
|
| 980 |
style={"margin": 0},
|
| 981 |
)
|
| 982 |
|
| 983 |
+
@app.callback(
|
| 984 |
+
Output("subpath_status", "children"),
|
| 985 |
+
Input("subpath_select_store", "data"),
|
| 986 |
+
)
|
| 987 |
+
def subpath_status(subsel):
|
| 988 |
+
subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
|
| 989 |
+
if not subsel.get("active"):
|
| 990 |
+
return "Idle"
|
| 991 |
+
s = subsel.get("start_idx")
|
| 992 |
+
e = subsel.get("end_idx")
|
| 993 |
+
if s is None:
|
| 994 |
+
return "Selection: click a vertex on the current path to start"
|
| 995 |
+
if e is None or e == s:
|
| 996 |
+
return f"Selection: start at index {s}. Click the next path vertex to extend"
|
| 997 |
+
return f"Selection: indices {s}..{e}. Click next vertex to extend, then click the button again to apply"
|
| 998 |
|
| 999 |
|
| 1000 |
if __name__ == "__main__":
|