tzurshubi commited on
Commit
8754dc6
·
verified ·
1 Parent(s): 22bc336

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -37
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 (apply to entire path)
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 (apply to entire path)
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
- # 6) Figure clicks: vertex (int) or edge ([u,v])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  if trigger == "fig" and clickData and clickData.get("points"):
734
  p = clickData["points"][0]
735
  cd = p.get("customdata")
736
 
737
- # Vertex click add if adjacent, else restart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Otherwise not adjacent → start a new path from this vertex
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__":