Gilmullin Almaz commited on
Commit
59ff193
·
1 Parent(s): 2fec28f

Refactor route handling to replace ReducedRouteCGR with SB-CGR across multiple modules, enhancing clarity and consistency in clustering and visualization processes.

Browse files
app.py CHANGED
@@ -26,6 +26,7 @@ from synplan.utils.visualisation import (
26
  generate_results_html,
27
  html_top_routes_cluster,
28
  get_route_svg,
 
29
  )
30
  from synplan.utils.config import TreeConfig, PolicyNetworkConfig
31
  from synplan.utils.loading import load_reaction_rules, load_building_blocks
@@ -169,8 +170,10 @@ def initialize_app():
169
  st.session_state.num_clusters_setting = 10
170
  if "route_cgrs_dict" not in st.session_state:
171
  st.session_state.route_cgrs_dict = None
172
- if "r_route_cgrs_dict" not in st.session_state:
173
- st.session_state.r_route_cgrs_dict = None
 
 
174
 
175
  # Subclustering state
176
  if "subclustering_done" not in st.session_state:
@@ -439,7 +442,8 @@ def setup_planning_options():
439
  st.session_state.reactions_dict = None
440
  st.session_state.subclusters = None
441
  st.session_state.route_cgrs_dict = None
442
- st.session_state.r_route_cgrs_dict = None
 
443
  active_smile_code = st.session_state.get(
444
  "ketcher", DEFAULT_MOL
445
  ) # Get current SMILES
@@ -520,8 +524,6 @@ def setup_planning_options():
520
 
521
  if error is not None:
522
  st.error(f"An error occurred during planning: {error}")
523
- # else:
524
- # st.rerun()
525
 
526
 
527
  def display_planning_results():
@@ -564,6 +566,7 @@ def display_planning_results():
564
  num_steps = len(tree.synthesis_route(node_id))
565
  route_score = round(tree.route_score(node_id), 3)
566
  svg = get_route_svg(tree, node_id)
 
567
  if svg:
568
  st.image(
569
  svg,
@@ -622,15 +625,6 @@ def download_planning_results():
622
  ):
623
  res = st.session_state.res
624
  tree = st.session_state.tree
625
- # This section is usually placed within a column in the original script
626
- # We'll assume it's called after display_planning_results and can use a new column or area.
627
- # For proper layout, this should be integrated with display_planning_results' columns.
628
- # For now, creating a placeholder or separate section for downloads:
629
- # st.subheader("Downloads") # This might be redundant if called within a layout context.
630
-
631
- # The original code places downloads in the second column of planning results.
632
- # To replicate, we'd need to pass the column object or call this within that context.
633
- # Simulating this by just creating the download links:
634
  try:
635
  html_body = generate_results_html(tree, html_path=None, extended=True)
636
  dl_html = download_button(
@@ -675,7 +669,7 @@ def setup_clustering():
675
  st.session_state.reactions_dict = None
676
  st.session_state.subclusters = None
677
  st.session_state.route_cgrs_dict = None
678
- st.session_state.r_route_cgrs_dict = None
679
 
680
  with st.spinner("Performing clustering..."):
681
  error = None
@@ -687,19 +681,22 @@ def setup_clustering():
687
 
688
  st.write("Calculating RoutesCGRs...")
689
  route_cgrs_dict = compose_all_route_cgrs(current_tree)
690
- st.write("Processing ReducedRoutesCGRs...")
691
- r_route_cgrs_dict = compose_all_reduced_route_cgrs(route_cgrs_dict)
 
692
 
693
  results = cluster_routes(
694
- r_route_cgrs_dict, use_strat=False
695
- ) # num_clusters was removed from args
696
  results = dict(sorted(results.items(), key=lambda x: float(x[0])))
697
 
698
  st.session_state.clusters = results
699
  st.session_state.route_cgrs_dict = route_cgrs_dict
700
- st.session_state.r_route_cgrs_dict = r_route_cgrs_dict
701
  st.write("Extracting reactions...")
702
- st.session_state.reactions_dict = extract_reactions(current_tree)
 
 
703
 
704
  if (
705
  st.session_state.clusters is not None
@@ -713,9 +710,8 @@ def setup_clustering():
713
  st.error("Clustering failed or returned empty results.")
714
  st.session_state.clustering_done = False
715
 
716
- del results # route_cgrs_dict, r_route_cgrs_dict are stored
717
  gc.collect()
718
- # st.rerun()
719
  except Exception as e:
720
  error = e
721
  st.error(f"An error occurred during clustering: {e}")
@@ -723,8 +719,6 @@ def setup_clustering():
723
 
724
  if error is not None:
725
  st.error(f"An error occurred during planning: {error}")
726
- # else:
727
- # st.rerun()
728
 
729
 
730
  def display_clustering_results():
@@ -765,16 +759,17 @@ def display_clustering_results():
765
  num_steps = len(tree.synthesis_route(node_id))
766
  route_score = round(tree.route_score(node_id), 3)
767
  svg = get_route_svg(tree, node_id)
768
- r_route_cgr = group_data.get("r_route_cgr") # Safely get r_route_cgr
769
- r_route_cgr_svg = None
770
- if r_route_cgr:
771
- r_route_cgr.clean2d()
772
- r_route_cgr_svg = cgr_display(r_route_cgr)
773
-
774
- if svg and r_route_cgr_svg:
 
775
  col1, col2 = st.columns([0.2, 0.8])
776
  with col1:
777
- st.image(r_route_cgr_svg, caption="ReducedRouteCGR")
778
  with col2:
779
  st.image(
780
  svg,
@@ -786,11 +781,11 @@ def display_clustering_results():
786
  caption=f"Route {node_id}; {num_steps} steps; Route score: {route_score}",
787
  )
788
  st.warning(
789
- f"ReducedRouteCGR could not be displayed for cluster {cluster_num}."
790
  )
791
  else:
792
  st.warning(
793
- f"Could not generate SVG for route {node_id} or its ReducedRouteCGR."
794
  )
795
  except Exception as e:
796
  st.error(
@@ -816,17 +811,18 @@ def display_clustering_results():
816
  try:
817
  num_steps = len(tree.synthesis_route(node_id))
818
  route_score = round(tree.route_score(node_id), 3)
819
- svg = get_route_svg(tree, node_id)
820
- r_route_cgr = group_data.get("r_route_cgr")
821
- r_route_cgr_svg = None
822
- if r_route_cgr:
823
- r_route_cgr.clean2d()
824
- r_route_cgr_svg = cgr_display(r_route_cgr)
825
-
826
- if svg and r_route_cgr_svg:
 
827
  col1, col2 = st.columns([0.2, 0.8])
828
  with col1:
829
- st.image(r_route_cgr_svg, caption="ReducedRouteCGR")
830
  with col2:
831
  st.image(
832
  svg,
@@ -838,11 +834,11 @@ def display_clustering_results():
838
  caption=f"Route {node_id}; {num_steps} steps; Route score: {route_score}",
839
  )
840
  st.warning(
841
- f"ReducedRouteCGR could not be displayed for cluster {cluster_num}."
842
  )
843
  else:
844
  st.warning(
845
- f"Could not generate SVG for route {node_id} or its ReducedRouteCGR."
846
  )
847
  except Exception as e:
848
  st.error(
@@ -855,8 +851,8 @@ def download_clustering_results():
855
  if st.session_state.get("clustering_done", False):
856
  tree_for_html = st.session_state.get("tree")
857
  clusters_for_html = st.session_state.get("clusters")
858
- r_route_cgrs_for_html = st.session_state.get(
859
- "r_route_cgrs_dict"
860
  ) # This was used instead of reactions_dict in the original for report
861
 
862
  if not tree_for_html:
@@ -865,7 +861,7 @@ def download_clustering_results():
865
  if not clusters_for_html:
866
  st.warning("Cluster data not found. Cannot generate cluster reports.")
867
  return
868
- # r_route_cgrs_for_html is optional for routes_clustering_report if not essential
869
 
870
  st.subheader("Cluster Reports") # Changed subheader in original
871
  st.write("Generate downloadable HTML reports for each cluster:")
@@ -884,7 +880,7 @@ def download_clustering_results():
884
  tree_for_html,
885
  clusters_for_html, # Pass the whole dict
886
  str(cluster_idx), # Pass the key of the cluster
887
- r_route_cgrs_for_html, # Pass the r_route_cgrs dict
888
  aam=False,
889
  )
890
  st.download_button(
@@ -911,7 +907,7 @@ def download_clustering_results():
911
  tree_for_html,
912
  clusters_for_html,
913
  str(group_index),
914
- r_route_cgrs_for_html,
915
  aam=False,
916
  )
917
  st.download_button(
@@ -936,7 +932,7 @@ def download_clustering_results():
936
  tree_for_html,
937
  clusters_for_html,
938
  str(idx),
939
- r_route_cgrs_for_html,
940
  aam=False,
941
  )
942
  filename = f"cluster_{idx}_{st.session_state.target_smiles}.html"
@@ -969,19 +965,19 @@ def setup_subclustering():
969
  error = None
970
  try:
971
  clusters_for_sub = st.session_state.get("clusters")
972
- r_route_cgrs_dict_for_sub = st.session_state.get(
973
- "r_route_cgrs_dict"
974
  )
975
  route_cgrs_dict_for_sub = st.session_state.get("route_cgrs_dict")
976
 
977
  if (
978
  clusters_for_sub
979
- and r_route_cgrs_dict_for_sub
980
  and route_cgrs_dict_for_sub
981
  ): # Ensure all are present
982
  all_subgroups = subcluster_all_clusters(
983
  clusters_for_sub,
984
- r_route_cgrs_dict_for_sub,
985
  route_cgrs_dict_for_sub,
986
  )
987
  st.session_state.subclusters = all_subgroups
@@ -993,8 +989,8 @@ def setup_subclustering():
993
  missing = []
994
  if not clusters_for_sub:
995
  missing.append("clusters")
996
- if not r_route_cgrs_dict_for_sub:
997
- missing.append("ReducedRouteCGRs dictionary")
998
  if not route_cgrs_dict_for_sub:
999
  missing.append("RouteCGRs dictionary")
1000
  st.error(
@@ -1061,17 +1057,17 @@ def display_subclustering_results():
1061
  current_subcluster_data = sub[user_input_cluster_num_display][
1062
  selected_subcluster_idx
1063
  ]
1064
- if "r_route_cgr" in current_subcluster_data:
1065
- cluster_r_route_cgr_display = current_subcluster_data[
1066
- "r_route_cgr"
1067
  ]
1068
- cluster_r_route_cgr_display.clean2d()
1069
  st.image(
1070
- cluster_r_route_cgr_display.depict(),
1071
- caption=f"ReducedRouteCGR of parent Cluster {user_input_cluster_num_display}",
1072
  )
1073
  else:
1074
- st.warning("ReducedRouteCGR for this subcluster not found.")
1075
  else:
1076
  st.warning(
1077
  f"Selected cluster {user_input_cluster_num_display} not found in subclustering results."
@@ -1126,48 +1122,50 @@ def display_subclustering_results():
1126
  st.warning(f"Could not depict synthon reaction: {e_depict}")
1127
  else:
1128
  st.info("No synthon reaction data for this subcluster.")
1129
-
1130
- for route_id in routes_to_display_direct:
1131
- try:
1132
- route_score_sub = round(tree.route_score(route_id), 3)
1133
- svg_sub = get_route_svg(tree, route_id)
1134
- if svg_sub:
1135
- st.image(
1136
- svg_sub,
1137
- caption=f"Route {route_id}; Score: {route_score_sub}",
1138
- )
1139
- else:
1140
- st.warning(
1141
- f"Could not generate SVG for route {route_id}."
 
 
 
 
 
1142
  )
1143
- except Exception as e:
1144
- st.error(
1145
- f"Error displaying route {route_id} in subcluster: {e}"
1146
- )
1147
 
1148
- if remaining_routes_sub:
1149
- with st.expander(
1150
- f"... and {len(remaining_routes_sub)} more routes in this subcluster"
1151
- ):
1152
- for route_id in remaining_routes_sub:
1153
- try:
1154
- route_score_sub = round(
1155
- tree.route_score(route_id), 3
1156
- )
1157
- svg_sub = get_route_svg(tree, route_id)
1158
- if svg_sub:
1159
- st.image(
1160
- svg_sub,
1161
- caption=f"Route {route_id}; Score: {route_score_sub}",
1162
  )
1163
- else:
1164
- st.warning(
1165
- f"Could not generate SVG for route {route_id}."
 
 
 
 
 
 
 
 
 
 
 
1166
  )
1167
- except Exception as e:
1168
- st.error(
1169
- f"Error displaying route {route_id} in subcluster (expanded): {e}"
1170
- )
1171
  else:
1172
  st.info("Select a valid cluster and subcluster index to see details.")
1173
 
@@ -1182,16 +1180,16 @@ def download_subclustering_results():
1182
 
1183
  sub = st.session_state.get("subclusters")
1184
  tree = st.session_state.get("tree")
1185
- r_route_cgrs_for_report = st.session_state.get(
1186
- "r_route_cgrs_dict"
1187
  ) # Used by routes_subclustering_report
1188
 
1189
  user_input_cluster_num_display = st.session_state.subcluster_num_select_key
1190
  selected_subcluster_idx = st.session_state.subcluster_index_select_key
1191
 
1192
- if not tree or not sub or not r_route_cgrs_for_report:
1193
  st.warning(
1194
- "Missing data for subclustering report generation (tree, subclusters, or ReducedRouteCGRs)."
1195
  )
1196
  return
1197
 
@@ -1226,7 +1224,7 @@ def download_subclustering_results():
1226
  processed_subcluster_data, # Pass the specific post-processed subcluster data
1227
  user_input_cluster_num_display,
1228
  selected_subcluster_idx,
1229
- r_route_cgrs_for_report, # Pass the whole r_route_cgrs dict
1230
  if_lg_group=True, # This parameter was in the original call
1231
  )
1232
  st.download_button(
@@ -1259,7 +1257,7 @@ def implement_restart():
1259
  "reactions_dict",
1260
  "num_clusters_setting",
1261
  "route_cgrs_dict",
1262
- "r_route_cgrs_dict",
1263
  "subclustering_done",
1264
  "subclusters", # "sub" was renamed
1265
  "clusters_downloaded",
 
26
  generate_results_html,
27
  html_top_routes_cluster,
28
  get_route_svg,
29
+ get_route_svg_from_json
30
  )
31
  from synplan.utils.config import TreeConfig, PolicyNetworkConfig
32
  from synplan.utils.loading import load_reaction_rules, load_building_blocks
 
170
  st.session_state.num_clusters_setting = 10
171
  if "route_cgrs_dict" not in st.session_state:
172
  st.session_state.route_cgrs_dict = None
173
+ if "sb_cgrs_dict" not in st.session_state:
174
+ st.session_state.sb_cgrs_dict = None
175
+ if "route_json" not in st.session_state:
176
+ st.session_state.route_json = None
177
 
178
  # Subclustering state
179
  if "subclustering_done" not in st.session_state:
 
442
  st.session_state.reactions_dict = None
443
  st.session_state.subclusters = None
444
  st.session_state.route_cgrs_dict = None
445
+ st.session_state.sb_cgrs_dict = None
446
+ st.session_state.route_json = None
447
  active_smile_code = st.session_state.get(
448
  "ketcher", DEFAULT_MOL
449
  ) # Get current SMILES
 
524
 
525
  if error is not None:
526
  st.error(f"An error occurred during planning: {error}")
 
 
527
 
528
 
529
  def display_planning_results():
 
566
  num_steps = len(tree.synthesis_route(node_id))
567
  route_score = round(tree.route_score(node_id), 3)
568
  svg = get_route_svg(tree, node_id)
569
+ # svg = get_route_svg_from_json(st.session_state.route_json, node_id)
570
  if svg:
571
  st.image(
572
  svg,
 
625
  ):
626
  res = st.session_state.res
627
  tree = st.session_state.tree
 
 
 
 
 
 
 
 
 
628
  try:
629
  html_body = generate_results_html(tree, html_path=None, extended=True)
630
  dl_html = download_button(
 
669
  st.session_state.reactions_dict = None
670
  st.session_state.subclusters = None
671
  st.session_state.route_cgrs_dict = None
672
+ st.session_state.sb_cgrs_dict = None
673
 
674
  with st.spinner("Performing clustering..."):
675
  error = None
 
681
 
682
  st.write("Calculating RoutesCGRs...")
683
  route_cgrs_dict = compose_all_route_cgrs(current_tree)
684
+ st.write("Processing SB-CGRs...")
685
+ sb_cgrs_dict = compose_all_sb_cgrs(route_cgrs_dict)
686
+ # route_json = make_json(extract_reactions(current_tree))
687
 
688
  results = cluster_routes(
689
+ sb_cgrs_dict, use_strat=False
690
+ )
691
  results = dict(sorted(results.items(), key=lambda x: float(x[0])))
692
 
693
  st.session_state.clusters = results
694
  st.session_state.route_cgrs_dict = route_cgrs_dict
695
+ st.session_state.sb_cgrs_dict = sb_cgrs_dict
696
  st.write("Extracting reactions...")
697
+ reactions_dict = extract_reactions(current_tree)
698
+ st.session_state.reactions_dict = reactions_dict
699
+ # st.session_state.route_json = make_json(reactions_dict)
700
 
701
  if (
702
  st.session_state.clusters is not None
 
710
  st.error("Clustering failed or returned empty results.")
711
  st.session_state.clustering_done = False
712
 
713
+ del results # sb_cgrs_dict are stored
714
  gc.collect()
 
715
  except Exception as e:
716
  error = e
717
  st.error(f"An error occurred during clustering: {e}")
 
719
 
720
  if error is not None:
721
  st.error(f"An error occurred during planning: {error}")
 
 
722
 
723
 
724
  def display_clustering_results():
 
759
  num_steps = len(tree.synthesis_route(node_id))
760
  route_score = round(tree.route_score(node_id), 3)
761
  svg = get_route_svg(tree, node_id)
762
+ # svg = get_route_svg_from_json(st.session_state.route_json, node_id)
763
+ sb_cgr = group_data.get("sb_cgr") # Safely get sb_cgr
764
+ sb_cgr_svg = None
765
+ if sb_cgr:
766
+ sb_cgr.clean2d()
767
+ sb_cgr_svg = cgr_display(sb_cgr)
768
+
769
+ if svg and sb_cgr_svg:
770
  col1, col2 = st.columns([0.2, 0.8])
771
  with col1:
772
+ st.image(sb_cgr_svg, caption="SB-CGR")
773
  with col2:
774
  st.image(
775
  svg,
 
781
  caption=f"Route {node_id}; {num_steps} steps; Route score: {route_score}",
782
  )
783
  st.warning(
784
+ f"SB-CGR could not be displayed for cluster {cluster_num}."
785
  )
786
  else:
787
  st.warning(
788
+ f"Could not generate SVG for route {node_id} or its SB-CGR."
789
  )
790
  except Exception as e:
791
  st.error(
 
811
  try:
812
  num_steps = len(tree.synthesis_route(node_id))
813
  route_score = round(tree.route_score(node_id), 3)
814
+ # svg = get_route_svg(tree, node_id)
815
+ svg = get_route_svg_from_json(st.session_state.route_json, node_id)
816
+ sb_cgr = group_data.get("sb_cgr")
817
+ sb_cgr_svg = None
818
+ if sb_cgr:
819
+ sb_cgr.clean2d()
820
+ sb_cgr_svg = cgr_display(sb_cgr)
821
+
822
+ if svg and sb_cgr_svg:
823
  col1, col2 = st.columns([0.2, 0.8])
824
  with col1:
825
+ st.image(sb_cgr_svg, caption="SB-CGR")
826
  with col2:
827
  st.image(
828
  svg,
 
834
  caption=f"Route {node_id}; {num_steps} steps; Route score: {route_score}",
835
  )
836
  st.warning(
837
+ f"SB-CGR could not be displayed for cluster {cluster_num}."
838
  )
839
  else:
840
  st.warning(
841
+ f"Could not generate SVG for route {node_id} or its SB-CGR."
842
  )
843
  except Exception as e:
844
  st.error(
 
851
  if st.session_state.get("clustering_done", False):
852
  tree_for_html = st.session_state.get("tree")
853
  clusters_for_html = st.session_state.get("clusters")
854
+ sb_cgrs_for_html = st.session_state.get(
855
+ "sb_cgrs_dict"
856
  ) # This was used instead of reactions_dict in the original for report
857
 
858
  if not tree_for_html:
 
861
  if not clusters_for_html:
862
  st.warning("Cluster data not found. Cannot generate cluster reports.")
863
  return
864
+ # sb_cgrs_for_html is optional for routes_clustering_report if not essential
865
 
866
  st.subheader("Cluster Reports") # Changed subheader in original
867
  st.write("Generate downloadable HTML reports for each cluster:")
 
880
  tree_for_html,
881
  clusters_for_html, # Pass the whole dict
882
  str(cluster_idx), # Pass the key of the cluster
883
+ sb_cgrs_for_html, # Pass the sb_cgrs dict
884
  aam=False,
885
  )
886
  st.download_button(
 
907
  tree_for_html,
908
  clusters_for_html,
909
  str(group_index),
910
+ sb_cgrs_for_html,
911
  aam=False,
912
  )
913
  st.download_button(
 
932
  tree_for_html,
933
  clusters_for_html,
934
  str(idx),
935
+ sb_cgrs_for_html,
936
  aam=False,
937
  )
938
  filename = f"cluster_{idx}_{st.session_state.target_smiles}.html"
 
965
  error = None
966
  try:
967
  clusters_for_sub = st.session_state.get("clusters")
968
+ sb_cgrs_dict_for_sub = st.session_state.get(
969
+ "sb_cgrs_dict"
970
  )
971
  route_cgrs_dict_for_sub = st.session_state.get("route_cgrs_dict")
972
 
973
  if (
974
  clusters_for_sub
975
+ and sb_cgrs_dict_for_sub
976
  and route_cgrs_dict_for_sub
977
  ): # Ensure all are present
978
  all_subgroups = subcluster_all_clusters(
979
  clusters_for_sub,
980
+ sb_cgrs_dict_for_sub,
981
  route_cgrs_dict_for_sub,
982
  )
983
  st.session_state.subclusters = all_subgroups
 
989
  missing = []
990
  if not clusters_for_sub:
991
  missing.append("clusters")
992
+ if not sb_cgrs_dict_for_sub:
993
+ missing.append("SB-CGRs dictionary")
994
  if not route_cgrs_dict_for_sub:
995
  missing.append("RouteCGRs dictionary")
996
  st.error(
 
1057
  current_subcluster_data = sub[user_input_cluster_num_display][
1058
  selected_subcluster_idx
1059
  ]
1060
+ if "sb_cgr" in current_subcluster_data:
1061
+ cluster_sb_cgr_display = current_subcluster_data[
1062
+ "sb_cgr"
1063
  ]
1064
+ cluster_sb_cgr_display.clean2d()
1065
  st.image(
1066
+ cluster_sb_cgr_display.depict(),
1067
+ caption=f"SB-CGR of parent Cluster {user_input_cluster_num_display}",
1068
  )
1069
  else:
1070
+ st.warning("SB-CGR for this subcluster not found.")
1071
  else:
1072
  st.warning(
1073
  f"Selected cluster {user_input_cluster_num_display} not found in subclustering results."
 
1122
  st.warning(f"Could not depict synthon reaction: {e_depict}")
1123
  else:
1124
  st.info("No synthon reaction data for this subcluster.")
1125
+ with st.container(height=300):
1126
+ for route_id in routes_to_display_direct:
1127
+ try:
1128
+ route_score_sub = round(tree.route_score(route_id), 3)
1129
+ # svg_sub = get_route_svg(tree, route_id)
1130
+ svg = get_route_svg_from_json(st.session_state.route_json, route_id)
1131
+ if svg_sub:
1132
+ st.image(
1133
+ svg_sub,
1134
+ caption=f"Route {route_id}; Score: {route_score_sub}",
1135
+ )
1136
+ else:
1137
+ st.warning(
1138
+ f"Could not generate SVG for route {route_id}."
1139
+ )
1140
+ except Exception as e:
1141
+ st.error(
1142
+ f"Error displaying route {route_id} in subcluster: {e}"
1143
  )
 
 
 
 
1144
 
1145
+ if remaining_routes_sub:
1146
+ with st.expander(
1147
+ f"... and {len(remaining_routes_sub)} more routes in this subcluster"
1148
+ ):
1149
+ for route_id in remaining_routes_sub:
1150
+ try:
1151
+ route_score_sub = round(
1152
+ tree.route_score(route_id), 3
 
 
 
 
 
 
1153
  )
1154
+ # svg_sub = get_route_svg(tree, route_id)
1155
+ svg_sub = get_route_svg_from_json(st.session_state.route_json, route_id)
1156
+ if svg_sub:
1157
+ st.image(
1158
+ svg_sub,
1159
+ caption=f"Route {route_id}; Score: {route_score_sub}",
1160
+ )
1161
+ else:
1162
+ st.warning(
1163
+ f"Could not generate SVG for route {route_id}."
1164
+ )
1165
+ except Exception as e:
1166
+ st.error(
1167
+ f"Error displaying route {route_id} in subcluster (expanded): {e}"
1168
  )
 
 
 
 
1169
  else:
1170
  st.info("Select a valid cluster and subcluster index to see details.")
1171
 
 
1180
 
1181
  sub = st.session_state.get("subclusters")
1182
  tree = st.session_state.get("tree")
1183
+ sb_cgrs_for_report = st.session_state.get(
1184
+ "sb_cgrs_dict"
1185
  ) # Used by routes_subclustering_report
1186
 
1187
  user_input_cluster_num_display = st.session_state.subcluster_num_select_key
1188
  selected_subcluster_idx = st.session_state.subcluster_index_select_key
1189
 
1190
+ if not tree or not sub or not sb_cgrs_for_report:
1191
  st.warning(
1192
+ "Missing data for subclustering report generation (tree, subclusters, or SB-CGRs)."
1193
  )
1194
  return
1195
 
 
1224
  processed_subcluster_data, # Pass the specific post-processed subcluster data
1225
  user_input_cluster_num_display,
1226
  selected_subcluster_idx,
1227
+ sb_cgrs_for_report, # Pass the whole sb_cgrs dict
1228
  if_lg_group=True, # This parameter was in the original call
1229
  )
1230
  st.download_button(
 
1257
  "reactions_dict",
1258
  "num_clusters_setting",
1259
  "route_cgrs_dict",
1260
+ "sb_cgrs_dict",
1261
  "subclustering_done",
1262
  "subclusters", # "sub" was renamed
1263
  "clusters_downloaded",
synplan/chem/reaction_routes/clustering.py CHANGED
@@ -57,7 +57,7 @@ def run_cluster_cli(
57
  # Compose condensed graph representations
58
  route_cgrs = compose_all_route_cgrs(routes_dict)
59
  click.echo(f"Generating RouteCGR")
60
- reduced_cgrs = compose_all_reduced_route_cgrs(route_cgrs)
61
  click.echo(f"Generating ReducedRouteCGR")
62
 
63
  # Perform clustering
@@ -129,7 +129,7 @@ def cluster_route_from_csv(routes_file: str):
129
  """
130
  routes_dict = read_routes_csv(routes_file)
131
  route_cgrs_dict = compose_all_route_cgrs(routes_dict)
132
- reduced_route_cgrs_dict = compose_all_reduced_route_cgrs(route_cgrs_dict)
133
  clusters = cluster_routes(reduced_route_cgrs_dict, use_strat=False)
134
  return clusters
135
 
@@ -157,7 +157,7 @@ def cluster_route_from_json(routes_file: str):
157
  routes_json = read_routes_json(routes_file)
158
  routes_dict = make_dict(routes_json)
159
  route_cgrs_dict = compose_all_route_cgrs(routes_dict)
160
- reduced_route_cgrs_dict = compose_all_reduced_route_cgrs(route_cgrs_dict)
161
  clusters = cluster_routes(reduced_route_cgrs_dict, use_strat=False)
162
  return clusters
163
 
@@ -196,34 +196,34 @@ def extract_strat_bonds(target_cgr: CGRContainer):
196
  return sorted(result)
197
 
198
 
199
- def cluster_routes(r_route_cgrs: dict, use_strat=False):
200
  """
201
  Cluster routes objects based on their strategic bonds
202
  or CGRContainer object signature (not avoid mapping)
203
 
204
  Args:
205
- r_route_cgrs: Dictionary mapping node_id to r_route_cgr objects.
206
 
207
  Returns:
208
  Dictionary with groups keyed by '{length}.{index}' containing
209
- 'r_route_cgr', 'node_ids', and 'strat_bonds'.
210
  """
211
  temp_groups = defaultdict(
212
- lambda: {"node_ids": [], "r_route_cgr": None, "strat_bonds": None}
213
  )
214
 
215
  # 1. Initial grouping based on the content of strategic bonds
216
- for node_id, r_route_cgr in r_route_cgrs.items():
217
- strat_bonds_list = extract_strat_bonds(r_route_cgr)
218
  if use_strat == True:
219
  group_key = tuple(strat_bonds_list)
220
  else:
221
- group_key = str(r_route_cgr)
222
 
223
  if not temp_groups[group_key]["node_ids"]: # First time seeing this group
224
  temp_groups[group_key][
225
- "r_route_cgr"
226
- ] = r_route_cgr # Store the first CGR as representative
227
  temp_groups[group_key][
228
  "strat_bonds"
229
  ] = strat_bonds_list # Store the actual list
@@ -437,7 +437,7 @@ class SubclusterError(Exception):
437
  """Raised when subcluster_one_cluster cannot complete successfully."""
438
 
439
 
440
- def subcluster_one_cluster(group, r_route_cgrs_dict, route_cgrs_dict):
441
  """
442
  Generate synthon data for each route in a single cluster.
443
 
@@ -449,7 +449,7 @@ def subcluster_one_cluster(group, r_route_cgrs_dict, route_cgrs_dict):
449
  ----------
450
  group : dict
451
  Must include `'node_ids'`, a list of node identifiers.
452
- r_route_cgrs_dict : dict
453
  Maps node IDs to their ReducedRouteCGR.
454
  route_cgrs_dict : dict
455
  Maps node IDs to their RouteCGR.
@@ -458,7 +458,7 @@ def subcluster_one_cluster(group, r_route_cgrs_dict, route_cgrs_dict):
458
  -------
459
  dict or None
460
  If successful, returns a dict mapping each `node_id` to a tuple:
461
- `(r_route_cgr, original_reaction, synthon_cgr, new_reaction, lg_groups)`.
462
  Or raises SubclusterError on any failure: if any step (X replacement or reaction
463
  parsing) fails for a node.
464
 
@@ -472,7 +472,7 @@ def subcluster_one_cluster(group, r_route_cgrs_dict, route_cgrs_dict):
472
 
473
  result = {}
474
  for node_id in node_ids:
475
- r_route_cgr = r_route_cgrs_dict[node_id]
476
  route_cgr = route_cgrs_dict[node_id]
477
 
478
  # 1) Replace leaving groups in RouteCGR
@@ -502,7 +502,7 @@ def subcluster_one_cluster(group, r_route_cgrs_dict, route_cgrs_dict):
502
  ) from e
503
 
504
  result[node_id] = (
505
- r_route_cgr,
506
  ReactionContainer(reactants=old_reactants, products=[target_mol]),
507
  synthon_cgr,
508
  new_rxn,
@@ -521,7 +521,7 @@ def group_nodes_by_synthon_detail(data_dict: dict):
521
  data_dict: Dictionary {node_id: [synthon_cgr, synthon_reaction, node_data, ...]}.
522
 
523
  Returns:
524
- Dictionary {group_index: {'r_route_cgr': ... ,'synthon_cgr': ..., 'synthon_reaction': ...,
525
  'nodes_data': {node_id1: node_data1, ...}}}.
526
  """
527
  temp_groups = defaultdict(list)
@@ -553,7 +553,7 @@ def group_nodes_by_synthon_detail(data_dict: dict):
553
  sorted_temp_groups = sorted(temp_groups.items(), key=lambda item: item[1])
554
  for group_key, node_ids in sorted_temp_groups:
555
 
556
- r_route_cgr, unlabeled_reaction, synthon_cgr, synthon_reaction = group_key
557
  nodes_data_dict = {}
558
 
559
  # Iterate through the node IDs belonging to this group
@@ -568,7 +568,7 @@ def group_nodes_by_synthon_detail(data_dict: dict):
568
  nodes_data_dict[node_id] = node_specific_data # Add to the sub-dictionary
569
 
570
  final_grouped_results[group_index] = {
571
- "r_route_cgr": r_route_cgr,
572
  "unlabeled_reaction": unlabeled_reaction,
573
  "synthon_cgr": synthon_cgr,
574
  "synthon_reaction": synthon_reaction,
@@ -580,7 +580,7 @@ def group_nodes_by_synthon_detail(data_dict: dict):
580
  return final_grouped_results
581
 
582
 
583
- def subcluster_all_clusters(groups, r_route_cgrs_dict, route_cgrs_dict):
584
  """
585
  Subdivide each reaction cluster into detailed synthon-based subgroups.
586
 
@@ -591,7 +591,7 @@ def subcluster_all_clusters(groups, r_route_cgrs_dict, route_cgrs_dict):
591
  ----------
592
  groups : dict
593
  Mapping of cluster indices to cluster data.
594
- r_route_cgrs_dict : dict
595
  Dictionary of ReducedRoteCGRs
596
  route_cgrs_dict : dict
597
  Dictionary of RoteCGRs
@@ -605,7 +605,7 @@ def subcluster_all_clusters(groups, r_route_cgrs_dict, route_cgrs_dict):
605
  all_subgroups = {}
606
  for group_index, group in groups.items():
607
  group_synthons = subcluster_one_cluster(
608
- group, r_route_cgrs_dict, route_cgrs_dict
609
  )
610
  if group_synthons is None:
611
  return None
 
57
  # Compose condensed graph representations
58
  route_cgrs = compose_all_route_cgrs(routes_dict)
59
  click.echo(f"Generating RouteCGR")
60
+ reduced_cgrs = compose_all_sb_cgrs(route_cgrs)
61
  click.echo(f"Generating ReducedRouteCGR")
62
 
63
  # Perform clustering
 
129
  """
130
  routes_dict = read_routes_csv(routes_file)
131
  route_cgrs_dict = compose_all_route_cgrs(routes_dict)
132
+ reduced_route_cgrs_dict = compose_all_sb_cgrs(route_cgrs_dict)
133
  clusters = cluster_routes(reduced_route_cgrs_dict, use_strat=False)
134
  return clusters
135
 
 
157
  routes_json = read_routes_json(routes_file)
158
  routes_dict = make_dict(routes_json)
159
  route_cgrs_dict = compose_all_route_cgrs(routes_dict)
160
+ reduced_route_cgrs_dict = compose_all_sb_cgrs(route_cgrs_dict)
161
  clusters = cluster_routes(reduced_route_cgrs_dict, use_strat=False)
162
  return clusters
163
 
 
196
  return sorted(result)
197
 
198
 
199
+ def cluster_routes(sb_cgrs: dict, use_strat=False):
200
  """
201
  Cluster routes objects based on their strategic bonds
202
  or CGRContainer object signature (not avoid mapping)
203
 
204
  Args:
205
+ sb_cgrs: Dictionary mapping node_id to sb_cgr objects.
206
 
207
  Returns:
208
  Dictionary with groups keyed by '{length}.{index}' containing
209
+ 'sb_cgr', 'node_ids', and 'strat_bonds'.
210
  """
211
  temp_groups = defaultdict(
212
+ lambda: {"node_ids": [], "sb_cgr": None, "strat_bonds": None}
213
  )
214
 
215
  # 1. Initial grouping based on the content of strategic bonds
216
+ for node_id, sb_cgr in sb_cgrs.items():
217
+ strat_bonds_list = extract_strat_bonds(sb_cgr)
218
  if use_strat == True:
219
  group_key = tuple(strat_bonds_list)
220
  else:
221
+ group_key = str(sb_cgr)
222
 
223
  if not temp_groups[group_key]["node_ids"]: # First time seeing this group
224
  temp_groups[group_key][
225
+ "sb_cgr"
226
+ ] = sb_cgr # Store the first CGR as representative
227
  temp_groups[group_key][
228
  "strat_bonds"
229
  ] = strat_bonds_list # Store the actual list
 
437
  """Raised when subcluster_one_cluster cannot complete successfully."""
438
 
439
 
440
+ def subcluster_one_cluster(group, sb_cgrs_dict, route_cgrs_dict):
441
  """
442
  Generate synthon data for each route in a single cluster.
443
 
 
449
  ----------
450
  group : dict
451
  Must include `'node_ids'`, a list of node identifiers.
452
+ sb_cgrs_dict : dict
453
  Maps node IDs to their ReducedRouteCGR.
454
  route_cgrs_dict : dict
455
  Maps node IDs to their RouteCGR.
 
458
  -------
459
  dict or None
460
  If successful, returns a dict mapping each `node_id` to a tuple:
461
+ `(sb_cgr, original_reaction, synthon_cgr, new_reaction, lg_groups)`.
462
  Or raises SubclusterError on any failure: if any step (X replacement or reaction
463
  parsing) fails for a node.
464
 
 
472
 
473
  result = {}
474
  for node_id in node_ids:
475
+ sb_cgr = sb_cgrs_dict[node_id]
476
  route_cgr = route_cgrs_dict[node_id]
477
 
478
  # 1) Replace leaving groups in RouteCGR
 
502
  ) from e
503
 
504
  result[node_id] = (
505
+ sb_cgr,
506
  ReactionContainer(reactants=old_reactants, products=[target_mol]),
507
  synthon_cgr,
508
  new_rxn,
 
521
  data_dict: Dictionary {node_id: [synthon_cgr, synthon_reaction, node_data, ...]}.
522
 
523
  Returns:
524
+ Dictionary {group_index: {'sb_cgr': ... ,'synthon_cgr': ..., 'synthon_reaction': ...,
525
  'nodes_data': {node_id1: node_data1, ...}}}.
526
  """
527
  temp_groups = defaultdict(list)
 
553
  sorted_temp_groups = sorted(temp_groups.items(), key=lambda item: item[1])
554
  for group_key, node_ids in sorted_temp_groups:
555
 
556
+ sb_cgr, unlabeled_reaction, synthon_cgr, synthon_reaction = group_key
557
  nodes_data_dict = {}
558
 
559
  # Iterate through the node IDs belonging to this group
 
568
  nodes_data_dict[node_id] = node_specific_data # Add to the sub-dictionary
569
 
570
  final_grouped_results[group_index] = {
571
+ "sb_cgr": sb_cgr,
572
  "unlabeled_reaction": unlabeled_reaction,
573
  "synthon_cgr": synthon_cgr,
574
  "synthon_reaction": synthon_reaction,
 
580
  return final_grouped_results
581
 
582
 
583
+ def subcluster_all_clusters(groups, sb_cgrs_dict, route_cgrs_dict):
584
  """
585
  Subdivide each reaction cluster into detailed synthon-based subgroups.
586
 
 
591
  ----------
592
  groups : dict
593
  Mapping of cluster indices to cluster data.
594
+ sb_cgrs_dict : dict
595
  Dictionary of ReducedRoteCGRs
596
  route_cgrs_dict : dict
597
  Dictionary of RoteCGRs
 
605
  all_subgroups = {}
606
  for group_index, group in groups.items():
607
  group_synthons = subcluster_one_cluster(
608
+ group, sb_cgrs_dict, route_cgrs_dict
609
  )
610
  if group_synthons is None:
611
  return None
synplan/chem/reaction_routes/route_cgr.py CHANGED
@@ -487,7 +487,7 @@ def extract_reactions(tree: Tree, node_id=None):
487
  return dict(sorted(react_dict.items()))
488
 
489
 
490
- def compose_reduced_route_cgr(route_cgr: CGRContainer):
491
  """
492
  Reduces a Routes Condensed Graph of reaction (RouteCGR) by performing the following steps:
493
 
@@ -550,7 +550,7 @@ def compose_reduced_route_cgr(route_cgr: CGRContainer):
550
  return reduced_route_cgr
551
 
552
 
553
- def compose_all_reduced_route_cgrs(route_cgrs_dict: dict):
554
  """
555
  Processes a collection (dictionary) of RouteCGRs to generate their reduced forms (ReducedRouteCGRs).
556
 
@@ -566,5 +566,5 @@ def compose_all_reduced_route_cgrs(route_cgrs_dict: dict):
566
  """
567
  all_reduced_route_cgrs = dict()
568
  for num, cgr in route_cgrs_dict.items():
569
- all_reduced_route_cgrs[num] = compose_reduced_route_cgr(cgr)
570
  return all_reduced_route_cgrs
 
487
  return dict(sorted(react_dict.items()))
488
 
489
 
490
+ def compose_sb_cgr(route_cgr: CGRContainer):
491
  """
492
  Reduces a Routes Condensed Graph of reaction (RouteCGR) by performing the following steps:
493
 
 
550
  return reduced_route_cgr
551
 
552
 
553
+ def compose_all_sb_cgrs(route_cgrs_dict: dict):
554
  """
555
  Processes a collection (dictionary) of RouteCGRs to generate their reduced forms (ReducedRouteCGRs).
556
 
 
566
  """
567
  all_reduced_route_cgrs = dict()
568
  for num, cgr in route_cgrs_dict.items():
569
+ all_reduced_route_cgrs[num] = compose_sb_cgr(cgr)
570
  return all_reduced_route_cgrs
synplan/utils/visualisation.py CHANGED
@@ -536,7 +536,7 @@ def html_top_routes_cluster(clusters: dict, tree: Tree, target_smiles: str) -> s
536
  node_id = node_ids[0]
537
  # Get SVGs
538
  svg = get_route_svg(tree, node_id)
539
- r_cgr = group_data.get("r_route_cgr")
540
  r_cgr_svg = None
541
  if r_cgr:
542
  r_cgr.clean2d()
@@ -573,7 +573,7 @@ def routes_clustering_report(
573
  source: Union[Tree, dict],
574
  clusters: dict,
575
  group_index: str,
576
- r_route_cgrs_dict: dict,
577
  aam: bool = False,
578
  html_path: str = None,
579
  ) -> str:
@@ -597,7 +597,7 @@ def routes_clustering_report(
597
  group_index (str): The key identifying the specific cluster within the
598
  `clusters` dictionary for which the report should be
599
  generated.
600
- r_route_cgrs_dict (dict): A dictionary mapping route IDs (integers) to
601
  ReducedRouteCGR (Retrosynthetic Graph-based Chemical
602
  Reaction) objects. Used to display a representative
603
  ReducedRouteCGR for the cluster.
@@ -749,18 +749,18 @@ def routes_clustering_report(
749
  # --- Add ReducedRouteCGR Image ---
750
  first_route_id = valid_routes[0] if valid_routes else None
751
 
752
- if first_route_id and r_route_cgrs_dict:
753
  try:
754
- r_route_cgr = r_route_cgrs_dict[first_route_id]
755
- r_route_cgr.clean2d()
756
- r_route_cgr_svg = cgr_display(r_route_cgr)
757
 
758
- if r_route_cgr_svg.strip().startswith("<svg"):
759
- table += f"<tr>{td}{font_normal}Identified Strategic Bonds{font_close}<br>{r_route_cgr_svg}</td></tr>"
760
  else:
761
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Invalid SVG format retrieved.</i></td></tr>"
762
  print(
763
- f"Warning: Expected SVG for ReducedRouteCGR of node {first_route_id}, but got: {r_route_cgr_svg[:100]}..."
764
  )
765
  except Exception as e:
766
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Error retrieving/displaying ReducedRouteCGR: {e}</i></td></tr>"
@@ -993,7 +993,7 @@ def routes_subclustering_report(
993
  subcluster: dict,
994
  group_index: str,
995
  cluster_num: int,
996
- r_route_cgrs_dict: dict,
997
  if_lg_group: bool = False,
998
  aam: bool = False,
999
  html_path: str = None,
@@ -1022,7 +1022,7 @@ def routes_subclustering_report(
1022
  subcluster belongs. Used for report titling.
1023
  cluster_num (int): The number or identifier of the subcluster within
1024
  its main group. Used for report titling.
1025
- r_route_cgrs_dict (dict): A dictionary mapping route IDs (integers) to
1026
  ReducedRouteCGR objects. Used to display a representative
1027
  ReducedRouteCGR for the cluster.
1028
  if_lg_group (bool, optional): If True, the leaving groups table will
@@ -1179,18 +1179,18 @@ def routes_subclustering_report(
1179
  # --- Add ReducedRouteCGR Image ---
1180
  first_route_id = valid_routes[0] if valid_routes else None
1181
 
1182
- if first_route_id and r_route_cgrs_dict:
1183
  try:
1184
- r_route_cgr = r_route_cgrs_dict[first_route_id]
1185
- r_route_cgr.clean2d()
1186
- r_route_cgr_svg = cgr_display(r_route_cgr)
1187
 
1188
- if r_route_cgr_svg.strip().startswith("<svg"):
1189
- table += f"<tr>{td}{font_normal}Identified Strategic Bonds{font_close}<br>{r_route_cgr_svg}</td></tr>"
1190
  else:
1191
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Invalid SVG format retrieved.</i></td></tr>"
1192
  print(
1193
- f"Warning: Expected SVG for ReducedRouteCGR of node {first_route_id}, but got: {r_route_cgr_svg[:100]}..."
1194
  )
1195
  except Exception as e:
1196
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Error retrieving/displaying ReducedRouteCGR: {e}</i></td></tr>"
 
536
  node_id = node_ids[0]
537
  # Get SVGs
538
  svg = get_route_svg(tree, node_id)
539
+ r_cgr = group_data.get("sb_cgr")
540
  r_cgr_svg = None
541
  if r_cgr:
542
  r_cgr.clean2d()
 
573
  source: Union[Tree, dict],
574
  clusters: dict,
575
  group_index: str,
576
+ sb_cgrs_dict: dict,
577
  aam: bool = False,
578
  html_path: str = None,
579
  ) -> str:
 
597
  group_index (str): The key identifying the specific cluster within the
598
  `clusters` dictionary for which the report should be
599
  generated.
600
+ sb_cgrs_dict (dict): A dictionary mapping route IDs (integers) to
601
  ReducedRouteCGR (Retrosynthetic Graph-based Chemical
602
  Reaction) objects. Used to display a representative
603
  ReducedRouteCGR for the cluster.
 
749
  # --- Add ReducedRouteCGR Image ---
750
  first_route_id = valid_routes[0] if valid_routes else None
751
 
752
+ if first_route_id and sb_cgrs_dict:
753
  try:
754
+ sb_cgr = sb_cgrs_dict[first_route_id]
755
+ sb_cgr.clean2d()
756
+ sb_cgr_svg = cgr_display(sb_cgr)
757
 
758
+ if sb_cgr_svg.strip().startswith("<svg"):
759
+ table += f"<tr>{td}{font_normal}Identified Strategic Bonds{font_close}<br>{sb_cgr_svg}</td></tr>"
760
  else:
761
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Invalid SVG format retrieved.</i></td></tr>"
762
  print(
763
+ f"Warning: Expected SVG for ReducedRouteCGR of node {first_route_id}, but got: {sb_cgr_svg[:100]}..."
764
  )
765
  except Exception as e:
766
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Error retrieving/displaying ReducedRouteCGR: {e}</i></td></tr>"
 
993
  subcluster: dict,
994
  group_index: str,
995
  cluster_num: int,
996
+ sb_cgrs_dict: dict,
997
  if_lg_group: bool = False,
998
  aam: bool = False,
999
  html_path: str = None,
 
1022
  subcluster belongs. Used for report titling.
1023
  cluster_num (int): The number or identifier of the subcluster within
1024
  its main group. Used for report titling.
1025
+ sb_cgrs_dict (dict): A dictionary mapping route IDs (integers) to
1026
  ReducedRouteCGR objects. Used to display a representative
1027
  ReducedRouteCGR for the cluster.
1028
  if_lg_group (bool, optional): If True, the leaving groups table will
 
1179
  # --- Add ReducedRouteCGR Image ---
1180
  first_route_id = valid_routes[0] if valid_routes else None
1181
 
1182
+ if first_route_id and sb_cgrs_dict:
1183
  try:
1184
+ sb_cgr = sb_cgrs_dict[first_route_id]
1185
+ sb_cgr.clean2d()
1186
+ sb_cgr_svg = cgr_display(sb_cgr)
1187
 
1188
+ if sb_cgr_svg.strip().startswith("<svg"):
1189
+ table += f"<tr>{td}{font_normal}Identified Strategic Bonds{font_close}<br>{sb_cgr_svg}</td></tr>"
1190
  else:
1191
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Invalid SVG format retrieved.</i></td></tr>"
1192
  print(
1193
+ f"Warning: Expected SVG for ReducedRouteCGR of node {first_route_id}, but got: {sb_cgr_svg[:100]}..."
1194
  )
1195
  except Exception as e:
1196
  table += f"<tr>{td}{font_normal}Cluster Representative ReducedRouteCGR (from Route {first_route_id}):{font_close}<br><i>Error retrieving/displaying ReducedRouteCGR: {e}</i></td></tr>"