tzurshubi commited on
Commit
98a9fa8
·
verified ·
1 Parent(s): 0d2d502

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -38
app.py CHANGED
@@ -728,12 +728,16 @@ app.layout = html.Div(
728
  ),
729
  ]),
730
 
731
- # --- Subpath flip controls ---
732
  html.Div(
733
  style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"},
734
  children=[
735
  html.Button("Flip subpath", id="btn_flip_subpath", n_clicks=0,
736
  style={"background": "#2563EB", "color": "white"}),
 
 
 
 
737
  dcc.Input(
738
  id="subpath_dim",
739
  type="number",
@@ -750,6 +754,12 @@ app.layout = html.Div(
750
  id="subpath_select_store",
751
  data={"active": False, "start_idx": None, "end_idx": None}
752
  ),
 
 
 
 
 
 
753
 
754
  html.Div(), # empty cell just to keep the grid tidy
755
  ],
@@ -865,7 +875,8 @@ def stats(d, path):
865
 
866
  @app.callback(
867
  Output("path_store", "data"),
868
- Output("subpath_select_store", "data"), # NEW
 
869
  Input("fig", "clickData"),
870
  Input("btn_clear", "n_clicks"),
871
  Input("btn_longest_cib", "n_clicks"),
@@ -874,15 +885,17 @@ def stats(d, path):
874
  Input("btn_set", "n_clicks"),
875
  Input("btn_swap", "n_clicks"),
876
  Input("btn_flip", "n_clicks"),
877
- Input("btn_flip_subpath", "n_clicks"), # NEW
 
878
  State("path_store", "data"),
879
  State("manual_path", "value"),
880
  State("dim", "value"),
881
  State("swap_i", "value"),
882
  State("swap_j", "value"),
883
  State("flip_k", "value"),
884
- State("subpath_select_store", "data"), # NEW
885
- State("subpath_dim", "value"), # NEW
 
886
  prevent_initial_call=True
887
  )
888
  def update_path(clickData,
@@ -894,6 +907,7 @@ def update_path(clickData,
894
  n_swap,
895
  n_flip,
896
  n_flip_subpath,
 
897
  path,
898
  manual_text,
899
  d,
@@ -901,7 +915,8 @@ def update_path(clickData,
901
  swap_j,
902
  flip_k,
903
  subsel,
904
- subpath_dim):
 
905
 
906
  trigger = ctx.triggered_id
907
  path = path or []
@@ -909,28 +924,30 @@ def update_path(clickData,
909
 
910
  # default selection store
911
  subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
 
 
912
 
913
  # 1) Clear
914
  if trigger == "btn_clear":
915
- return [], {"active": False, "start_idx": None, "end_idx": None}
916
 
917
  # 2a) Longest CIB
918
  if trigger == "btn_longest_cib":
919
- return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}
920
 
921
  # 2b) Longest Symmetric CIB
922
  if trigger == "btn_longest_sym_cib":
923
- return longest_sym_cib(d), {"active": False, "start_idx": None, "end_idx": None}
924
 
925
 
926
  # 2c) Shorter CIB
927
  if trigger == "btn_shorter_cib":
928
- return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}
929
 
930
  # 3) Manual set path
931
  if trigger == "btn_set":
932
  newp = parse_path(manual_text or "", d)
933
- return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}
934
 
935
  # 4) Swap two dimensions
936
  if trigger == "btn_swap":
@@ -938,22 +955,22 @@ def update_path(clickData,
938
  i = int(swap_i) if swap_i is not None else None
939
  j = int(swap_j) if swap_j is not None else None
940
  except (TypeError, ValueError):
941
- return path, subsel
942
  if i is None or j is None:
943
  return path, subsel
944
  if not (0 <= i < d and 0 <= j < d):
945
- return path, subsel
946
- return swap_dims_path(path, d, i, j), subsel
947
 
948
  # 5) Flip one dimension (whole path)
949
  if trigger == "btn_flip":
950
  try:
951
  k = int(flip_k) if flip_k is not None else None
952
  except (TypeError, ValueError):
953
- return path, subsel
954
  if k is None or not (0 <= k < d):
955
- return path, subsel
956
- return flip_dim_path(path, d, k), subsel
957
 
958
  # 6) The new two-click button:
959
  # First click: enter selection mode.
@@ -962,7 +979,7 @@ def update_path(clickData,
962
  active = bool(subsel.get("active"))
963
  if not active:
964
  # enter selection mode
965
- return path, {"active": True, "start_idx": None, "end_idx": None}
966
 
967
  # active == True, so this is the "second click": apply
968
  try:
@@ -971,13 +988,13 @@ def update_path(clickData,
971
  i = None
972
  if i is None or not (0 <= i < d):
973
  # invalid dimension, just exit selection mode
974
- return path, {"active": False, "start_idx": None, "end_idx": None}
975
 
976
  s = subsel.get("start_idx")
977
  e = subsel.get("end_idx")
978
  if s is None or e is None or e <= s:
979
  # nothing meaningful selected, exit
980
- return path, {"active": False, "start_idx": None, "end_idx": None}
981
 
982
  # subpath is path[s:e+1] = (v1,...,vk)
983
  # desired: keep prefix (..., v1), keep suffix (vk, ...),
@@ -987,74 +1004,138 @@ def update_path(clickData,
987
  rest = path[e:] # starts at vk
988
  new_path = head + flipped_subpath + rest
989
 
990
- return new_path, {"active": False, "start_idx": None, "end_idx": None}
991
 
 
 
 
 
 
 
 
 
 
992
 
993
- # 7) Figure clicks
 
994
  if trigger == "fig" and clickData and clickData.get("points"):
995
  p = clickData["points"][0]
996
  cd = p.get("customdata")
997
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
998
  # If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
999
  if subsel.get("active") and isinstance(cd, (int, float)):
1000
  vid = int(cd)
1001
  idx = index_in_path(path, vid)
1002
  if idx is None:
1003
- return path, subsel
1004
 
1005
  s = subsel.get("start_idx")
1006
  e = subsel.get("end_idx")
1007
 
1008
  # first selected vertex
1009
  if s is None:
1010
- return path, {"active": True, "start_idx": idx, "end_idx": idx}
1011
 
1012
  # enforce consecutive forward selection along the path
1013
  # user must click idx == e+1 to extend
1014
  if e is None:
1015
  e = s
1016
  if idx == e + 1:
1017
- return path, {"active": True, "start_idx": s, "end_idx": idx}
1018
 
1019
  # allow shrinking by clicking the current end again
1020
  if idx == e:
1021
  # pop last selected (shrink by 1) if possible
1022
  new_e = e - 1 if e > s else s
1023
- return path, {"active": True, "start_idx": s, "end_idx": new_e}
1024
 
1025
  # otherwise ignore
1026
- return path, subsel
1027
 
1028
  # Normal mode: your existing click-to-build-path behavior
1029
  if isinstance(cd, (int, float)):
1030
  vid = int(cd)
1031
 
1032
  if not path:
1033
- return [vid], subsel
1034
 
1035
  if vid == path[-1]:
1036
- return path[:-1], subsel
1037
 
1038
  if len(path) >= 2 and vid == path[-2]:
1039
- return path[:-1], subsel
1040
 
1041
  if hamming_dist(vid, path[-1]) == 1:
1042
- return path + [vid], subsel
1043
 
1044
- return [vid], subsel
1045
 
1046
  if isinstance(cd, (list, tuple)) and len(cd) == 2:
1047
  u, v = int(cd[0]), int(cd[1])
1048
  if not path:
1049
- return [u, v], subsel
1050
  last = path[-1]
1051
  if last == u:
1052
- return path + [v], subsel
1053
  if last == v:
1054
- return path + [u], subsel
1055
- return [u, v], subsel
1056
 
1057
- return path, subsel
1058
 
1059
 
1060
  @app.callback(
@@ -1251,8 +1332,24 @@ def mark_neighbors_label(d, path, mark_dist_vals):
1251
  labelStyle={"display": "inline-block", "background-color": "yellow"},
1252
  )
1253
 
1254
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
 
 
 
1256
  if __name__ == "__main__":
1257
  import os
1258
  port = int(os.environ.get("PORT", "7860")) # HF uses 7860
 
728
  ),
729
  ]),
730
 
731
+ # --- Subpath flip controls + Switch dimensions ---
732
  html.Div(
733
  style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"},
734
  children=[
735
  html.Button("Flip subpath", id="btn_flip_subpath", n_clicks=0,
736
  style={"background": "#2563EB", "color": "white"}),
737
+
738
+ html.Button("Switch dimensions", id="btn_switch_dims", n_clicks=0,
739
+ style={"background": "#6B7280", "color": "white"}),
740
+
741
  dcc.Input(
742
  id="subpath_dim",
743
  type="number",
 
754
  id="subpath_select_store",
755
  data={"active": False, "start_idx": None, "end_idx": None}
756
  ),
757
+
758
+ dcc.Store(
759
+ id="switch_dims_store",
760
+ data={"active": False, "start_idx": None, "end_idx": None}
761
+ ),
762
+
763
 
764
  html.Div(), # empty cell just to keep the grid tidy
765
  ],
 
875
 
876
  @app.callback(
877
  Output("path_store", "data"),
878
+ Output("subpath_select_store", "data"),
879
+ Output("switch_dims_store", "data"),
880
  Input("fig", "clickData"),
881
  Input("btn_clear", "n_clicks"),
882
  Input("btn_longest_cib", "n_clicks"),
 
885
  Input("btn_set", "n_clicks"),
886
  Input("btn_swap", "n_clicks"),
887
  Input("btn_flip", "n_clicks"),
888
+ Input("btn_flip_subpath", "n_clicks"),
889
+ Input("btn_switch_dims", "n_clicks"),
890
  State("path_store", "data"),
891
  State("manual_path", "value"),
892
  State("dim", "value"),
893
  State("swap_i", "value"),
894
  State("swap_j", "value"),
895
  State("flip_k", "value"),
896
+ State("subpath_select_store", "data"),
897
+ State("subpath_dim", "value"),
898
+ State("switch_dims_store", "data"),
899
  prevent_initial_call=True
900
  )
901
  def update_path(clickData,
 
907
  n_swap,
908
  n_flip,
909
  n_flip_subpath,
910
+ n_switch_dims,
911
  path,
912
  manual_text,
913
  d,
 
915
  swap_j,
916
  flip_k,
917
  subsel,
918
+ subpath_dim,
919
+ switchsel):
920
 
921
  trigger = ctx.triggered_id
922
  path = path or []
 
924
 
925
  # default selection store
926
  subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
927
+ switchsel = switchsel or {"active": False, "start_idx": None, "end_idx": None}
928
+
929
 
930
  # 1) Clear
931
  if trigger == "btn_clear":
932
+ return [], {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
933
 
934
  # 2a) Longest CIB
935
  if trigger == "btn_longest_cib":
936
+ return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
937
 
938
  # 2b) Longest Symmetric CIB
939
  if trigger == "btn_longest_sym_cib":
940
+ return longest_sym_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
941
 
942
 
943
  # 2c) Shorter CIB
944
  if trigger == "btn_shorter_cib":
945
+ return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
946
 
947
  # 3) Manual set path
948
  if trigger == "btn_set":
949
  newp = parse_path(manual_text or "", d)
950
+ return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
951
 
952
  # 4) Swap two dimensions
953
  if trigger == "btn_swap":
 
955
  i = int(swap_i) if swap_i is not None else None
956
  j = int(swap_j) if swap_j is not None else None
957
  except (TypeError, ValueError):
958
+ return path, subsel, switchsel
959
  if i is None or j is None:
960
  return path, subsel
961
  if not (0 <= i < d and 0 <= j < d):
962
+ return path, subsel, switchsel
963
+ return swap_dims_path(path, d, i, j), subsel, switchsel
964
 
965
  # 5) Flip one dimension (whole path)
966
  if trigger == "btn_flip":
967
  try:
968
  k = int(flip_k) if flip_k is not None else None
969
  except (TypeError, ValueError):
970
+ return path, subsel, switchsel
971
  if k is None or not (0 <= k < d):
972
+ return path, subsel, switchsel
973
+ return flip_dim_path(path, d, k), subsel, switchsel
974
 
975
  # 6) The new two-click button:
976
  # First click: enter selection mode.
 
979
  active = bool(subsel.get("active"))
980
  if not active:
981
  # enter selection mode
982
+ return path, {"active": True, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
983
 
984
  # active == True, so this is the "second click": apply
985
  try:
 
988
  i = None
989
  if i is None or not (0 <= i < d):
990
  # invalid dimension, just exit selection mode
991
+ return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
992
 
993
  s = subsel.get("start_idx")
994
  e = subsel.get("end_idx")
995
  if s is None or e is None or e <= s:
996
  # nothing meaningful selected, exit
997
+ return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
998
 
999
  # subpath is path[s:e+1] = (v1,...,vk)
1000
  # desired: keep prefix (..., v1), keep suffix (vk, ...),
 
1004
  rest = path[e:] # starts at vk
1005
  new_path = head + flipped_subpath + rest
1006
 
1007
+ return new_path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
1008
 
1009
+ # 7) Switch dimensions mode toggle
1010
+ if trigger == "btn_switch_dims":
1011
+ active = bool(switchsel.get("active"))
1012
+ if not active:
1013
+ # enter switch-dims selection mode
1014
+ return path, subsel, {"active": True, "start_idx": None, "end_idx": None}
1015
+ else:
1016
+ # cancel switch-dims selection mode
1017
+ return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
1018
 
1019
+
1020
+ # 8) Figure clicks
1021
  if trigger == "fig" and clickData and clickData.get("points"):
1022
  p = clickData["points"][0]
1023
  cd = p.get("customdata")
1024
 
1025
+ # If we're in switch-dims mode, vertex clicks define exactly 3 consecutive vertices
1026
+ if switchsel.get("active") and isinstance(cd, (int, float)):
1027
+ vid = int(cd)
1028
+ idx = index_in_path(path, vid)
1029
+ if idx is None:
1030
+ return path, subsel, switchsel
1031
+
1032
+ s = switchsel.get("start_idx")
1033
+ e = switchsel.get("end_idx")
1034
+
1035
+ # first vertex
1036
+ if s is None:
1037
+ return path, subsel, {"active": True, "start_idx": idx, "end_idx": idx}
1038
+
1039
+ if e is None:
1040
+ e = s
1041
+
1042
+ # must extend consecutively forward: idx == e+1
1043
+ if idx == e + 1 and (idx - s) <= 2:
1044
+ new_sel = {"active": True, "start_idx": s, "end_idx": idx}
1045
+
1046
+ # if we have 3 vertices selected: apply switch
1047
+ if idx == s + 2:
1048
+ x = path[s]
1049
+ y = path[s + 1]
1050
+ z = path[s + 2]
1051
+
1052
+ a = edge_dimension(x, y)
1053
+ b = edge_dimension(y, z)
1054
+ if a is None or b is None:
1055
+ # not a valid 2-edge segment, exit mode
1056
+ return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
1057
+
1058
+ # new middle vertex to traverse b then a
1059
+ y2 = x ^ (1 << b)
1060
+
1061
+ # optional safety: don't create duplicates elsewhere in the path
1062
+ if y2 in set(path) and y2 not in {x, y, z}:
1063
+ return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
1064
+
1065
+ new_path = path[:]
1066
+ new_path[s + 1] = y2
1067
+ return new_path, subsel, {"active": False, "start_idx": None, "end_idx": None}
1068
+
1069
+ return path, subsel, new_sel
1070
+
1071
+ # allow shrinking by clicking current end
1072
+ if idx == e:
1073
+ new_e = e - 1 if e > s else s
1074
+ return path, subsel, {"active": True, "start_idx": s, "end_idx": new_e}
1075
+
1076
+ # otherwise ignore
1077
+ return path, subsel, switchsel
1078
+
1079
  # If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
1080
  if subsel.get("active") and isinstance(cd, (int, float)):
1081
  vid = int(cd)
1082
  idx = index_in_path(path, vid)
1083
  if idx is None:
1084
+ return path, subsel, switchsel
1085
 
1086
  s = subsel.get("start_idx")
1087
  e = subsel.get("end_idx")
1088
 
1089
  # first selected vertex
1090
  if s is None:
1091
+ return path, {"active": True, "start_idx": idx, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None}
1092
 
1093
  # enforce consecutive forward selection along the path
1094
  # user must click idx == e+1 to extend
1095
  if e is None:
1096
  e = s
1097
  if idx == e + 1:
1098
+ return path, {"active": True, "start_idx": s, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None}
1099
 
1100
  # allow shrinking by clicking the current end again
1101
  if idx == e:
1102
  # pop last selected (shrink by 1) if possible
1103
  new_e = e - 1 if e > s else s
1104
+ return path, {"active": True, "start_idx": s, "end_idx": new_e}, {"active": False, "start_idx": None, "end_idx": None}
1105
 
1106
  # otherwise ignore
1107
+ return path, subsel, switchsel
1108
 
1109
  # Normal mode: your existing click-to-build-path behavior
1110
  if isinstance(cd, (int, float)):
1111
  vid = int(cd)
1112
 
1113
  if not path:
1114
+ return [vid], subsel, switchsel
1115
 
1116
  if vid == path[-1]:
1117
+ return path[:-1], subsel, switchsel
1118
 
1119
  if len(path) >= 2 and vid == path[-2]:
1120
+ return path[:-1], subsel, switchsel
1121
 
1122
  if hamming_dist(vid, path[-1]) == 1:
1123
+ return path + [vid], subsel, switchsel
1124
 
1125
+ return [vid], subsel, switchsel
1126
 
1127
  if isinstance(cd, (list, tuple)) and len(cd) == 2:
1128
  u, v = int(cd[0]), int(cd[1])
1129
  if not path:
1130
+ return [u, v], subsel, switchsel
1131
  last = path[-1]
1132
  if last == u:
1133
+ return path + [v], subsel, switchsel
1134
  if last == v:
1135
+ return path + [u], subsel, switchsel
1136
+ return [u, v], subsel, switchsel
1137
 
1138
+ return path, subsel, switchsel
1139
 
1140
 
1141
  @app.callback(
 
1332
  labelStyle={"display": "inline-block", "background-color": "yellow"},
1333
  )
1334
 
1335
+ @app.callback(
1336
+ Output("btn_switch_dims", "style"),
1337
+ Input("switch_dims_store", "data"),
1338
+ )
1339
+ def style_switch_dims_button(switchsel):
1340
+ switchsel = switchsel or {}
1341
+ active = bool(switchsel.get("active"))
1342
+
1343
+ base = {
1344
+ "color": "white",
1345
+ "border": "none",
1346
+ "padding": "6px 12px",
1347
+ "borderRadius": "8px",
1348
+ "cursor": "pointer",
1349
+ }
1350
 
1351
+ return {**base, "background": "#DC2626"} if active else {**base, "background": "#6B7280"}
1352
+
1353
  if __name__ == "__main__":
1354
  import os
1355
  port = int(os.environ.get("PORT", "7860")) # HF uses 7860