nettracer3d 0.8.9__py3-none-any.whl → 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -16,14 +16,15 @@ class CellposeGUILauncher:
16
16
  """
17
17
  self.parent_widget = parent_widget
18
18
  self.cellpose_process = None
19
-
20
- def launch_cellpose_gui(self, image_path=None, working_directory=None):
19
+
20
+ def launch_cellpose_gui(self, image_path=None, working_directory=None, use_3d=False):
21
21
  """
22
22
  Launch cellpose GUI in a separate thread.
23
23
 
24
24
  Args:
25
25
  image_path (str, optional): Path to image file to load automatically
26
26
  working_directory (str, optional): Directory to start cellpose in
27
+ use_3d (bool, optional): Whether to launch cellpose 3D version (default: False)
27
28
 
28
29
  Returns:
29
30
  bool: True if launch was initiated successfully
@@ -34,6 +35,10 @@ class CellposeGUILauncher:
34
35
  # Build command
35
36
  cmd = [sys.executable, "-m", "cellpose"]
36
37
 
38
+ # Add 3D flag if requested
39
+ if use_3d:
40
+ cmd.append("--Zstack")
41
+
37
42
  # Add image path if provided
38
43
  if image_path and Path(image_path).exists():
39
44
  cmd.extend(["--image_path", str(image_path)])
@@ -55,29 +60,35 @@ class CellposeGUILauncher:
55
60
  except Exception as e:
56
61
  if self.parent_widget:
57
62
  # Show error in main thread
58
- self.show_error(f"Failed to launch cellpose GUI: {str(e)}")
63
+ version_str = "3D " if use_3d else ""
64
+ self.show_error(f"Failed to launch cellpose {version_str}GUI: {str(e)}")
59
65
  else:
60
- print(f"Failed to launch cellpose GUI: {str(e)}")
66
+ version_str = "3D " if use_3d else ""
67
+ print(f"Failed to launch cellpose {version_str}GUI: {str(e)}")
61
68
 
62
69
  try:
63
70
  # Start cellpose in separate thread
64
71
  thread = threading.Thread(target=run_cellpose, daemon=True)
65
72
  thread.start()
66
73
 
67
- if self.parent_widget:
68
- self.show_info("Cellpose GUI launched!")
69
- else:
70
- print("Cellpose GUI launched!")
74
+ #if self.parent_widget:
75
+ #version_str = "3D " if use_3d else ""
76
+ #self.show_info(f"Cellpose {version_str}GUI launched!")
77
+ #else:
78
+ #version_str = "3D " if use_3d else ""
79
+ #print(f"Cellpose {version_str}GUI launched!")
71
80
 
72
81
  return True
73
82
 
74
83
  except Exception as e:
75
84
  if self.parent_widget:
76
- self.show_error(f"Failed to start cellpose thread: {str(e)}")
85
+ version_str = "3D " if use_3d else ""
86
+ self.show_error(f"Failed to start cellpose {version_str}thread: {str(e)}")
77
87
  else:
78
- print(f"Failed to start cellpose thread: {str(e)}")
88
+ version_str = "3D " if use_3d else ""
89
+ print(f"Failed to start cellpose {version_str}thread: {str(e)}")
79
90
  return False
80
-
91
+
81
92
  def launch_with_directory(self, directory_path):
82
93
  """
83
94
  Launch cellpose GUI with a specific directory.
nettracer3d/modularity.py CHANGED
@@ -103,7 +103,7 @@ def read_excel_to_lists(file_path, sheet_name=0):
103
103
 
104
104
 
105
105
 
106
- def show_communities_flex(G, master_list, normalized_weights, geo_info = None, geometric=False, directory=None, weighted=True, partition=None, style=0):
106
+ def show_communities_flex(G, master_list, normalized_weights, geo_info=None, geometric=False, directory=None, weighted=True, partition=None, style=0):
107
107
 
108
108
  if normalized_weights is None:
109
109
  G, edge_weights = network_analysis.weighted_network(master_list)
@@ -137,10 +137,25 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info = None, g
137
137
 
138
138
  # Create a mapping of community IDs to sequential indices
139
139
  unique_communities = sorted(set(partition.values()))
140
- community_to_index = {comm: idx for idx, comm in enumerate(unique_communities)}
141
-
142
- # Prepare colors using the number of unique communities
143
- colors = [plt.cm.jet(i / len(unique_communities)) for i in range(len(unique_communities))]
140
+
141
+ # Use the same color generation method as the overlay system
142
+ # Get community sizes for sorting (largest first)
143
+ from collections import Counter
144
+ community_sizes = Counter(partition.values())
145
+ sorted_communities = sorted(unique_communities, key=lambda x: community_sizes[x], reverse=True)
146
+
147
+ from . import community_extractor
148
+
149
+ # Generate distinct colors using the same method as assign_community_colors
150
+ colors_rgb = community_extractor.generate_distinct_colors(len(unique_communities))
151
+
152
+ # Create community to color mapping (same order as the overlay system)
153
+ community_to_color = {comm: colors_rgb[i] for i, comm in enumerate(sorted_communities)}
154
+
155
+ # Convert RGB tuples to matplotlib format (0-1 range)
156
+ colors_matplotlib = {}
157
+ for comm, rgb in community_to_color.items():
158
+ colors_matplotlib[comm] = tuple(c/255.0 for c in rgb)
144
159
 
145
160
  if weighted:
146
161
  G = nx.Graph()
@@ -156,7 +171,7 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info = None, g
156
171
  for community_id, nodes in communities.items():
157
172
  node_sizes_list = [z_pos[node] for node in nodes]
158
173
  nx.draw_networkx_nodes(G, pos, nodelist=nodes,
159
- node_color=[colors[community_to_index[community_id]]],
174
+ node_color=[colors_matplotlib[community_id]],
160
175
  node_size=node_sizes_list, alpha=0.8)
161
176
 
162
177
  # Draw edges with normalized weights
@@ -172,7 +187,7 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info = None, g
172
187
  # Draw the nodes, coloring them according to their community
173
188
  for community_id, nodes in communities.items():
174
189
  nx.draw_networkx_nodes(G, pos, nodelist=nodes,
175
- node_color=[colors[community_to_index[community_id]]],
190
+ node_color=[colors_matplotlib[community_id]],
176
191
  node_size=100, alpha=0.8)
177
192
 
178
193
  # Draw edges with normalized weights
@@ -183,8 +198,8 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info = None, g
183
198
  nx.draw_networkx_labels(G, pos)
184
199
 
185
200
  else:
186
- # Create node color list based on partition and mapping
187
- node_colors = [colors[community_to_index[partition[node]]] for node in G.nodes()]
201
+ # Create node color list based on partition and the same color mapping
202
+ node_colors = [colors_matplotlib[partition[node]] for node in G.nodes()]
188
203
 
189
204
  if geometric:
190
205
  pos, z_pos = simple_network.geometric_positions(geo_info[0], geo_info[1])
@@ -346,7 +346,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
346
346
  random_state: int = 42,
347
347
  id_dictionary: Optional[Dict[int, str]] = None,
348
348
  graph_label = "Community ID",
349
- title = 'UMAP Visualization of Community Compositions'):
349
+ title = 'UMAP Visualization of Community Compositions',
350
+ neighborhoods: Optional[Dict[int, int]] = None):
350
351
  """
351
352
  Convert cluster composition data to UMAP visualization.
352
353
 
@@ -366,6 +367,9 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
366
367
  id_dictionary : dict, optional
367
368
  Dictionary mapping cluster IDs to identity names. If provided, colors will be
368
369
  assigned by identity rather than cluster ID, and a legend will be shown.
370
+ neighborhoods : dict, optional
371
+ Dictionary mapping node IDs to neighborhood IDs {node_id: neighborhood_id}.
372
+ If provided, points will be colored by neighborhood using community coloration methods.
369
373
 
370
374
  Returns:
371
375
  --------
@@ -389,25 +393,70 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
389
393
  # Fit and transform the composition data
390
394
  embedding = reducer.fit_transform(compositions)
391
395
 
392
- # Prepare colors and labels based on whether id_dictionary is provided
393
- if id_dictionary is not None:
394
- # Get unique identities and assign colors (group all unknown into single "Unknown" category)
396
+ # Determine coloring scheme based on parameters
397
+ if neighborhoods is not None:
398
+ # Use neighborhood coloring - import the community extractor methods
399
+ from . import community_extractor
400
+
401
+ # Filter neighborhoods to only include cluster_ids that exist in our data
402
+ filtered_neighborhoods = {node_id: neighborhood_id
403
+ for node_id, neighborhood_id in neighborhoods.items()
404
+ if node_id in cluster_ids}
405
+
406
+ # Create a dummy labeled array just for the coloring function
407
+ # We only need the coloring logic, not actual clustering
408
+ dummy_array = np.array(cluster_ids)
409
+
410
+ # Get colors using the community coloration method
411
+ _, neighborhood_color_names = community_extractor.assign_community_colors(
412
+ filtered_neighborhoods, dummy_array
413
+ )
414
+
415
+ # Create color mapping for our points
416
+ unique_neighborhoods = sorted(list(set(filtered_neighborhoods.values())))
417
+ colors = community_extractor.generate_distinct_colors(len(unique_neighborhoods))
418
+ neighborhood_to_color = {neighborhood: colors[i] for i, neighborhood in enumerate(unique_neighborhoods)}
419
+
420
+ # Map each cluster to its neighborhood color
421
+ point_colors = []
422
+ neighborhood_labels = []
423
+ for cluster_id in cluster_ids:
424
+ if cluster_id in filtered_neighborhoods:
425
+ neighborhood_id = filtered_neighborhoods[cluster_id]
426
+ point_colors.append(neighborhood_to_color[neighborhood_id])
427
+ neighborhood_labels.append(neighborhood_id)
428
+ else:
429
+ # Default color for nodes not in any neighborhood
430
+ point_colors.append((128, 128, 128)) # Gray
431
+ neighborhood_labels.append("Unknown")
432
+
433
+ # Normalize RGB values for matplotlib (0-1 range)
434
+ point_colors = [(r/255.0, g/255.0, b/255.0) for r, g, b in point_colors]
435
+ use_neighborhood_coloring = True
436
+
437
+ elif id_dictionary is not None:
438
+ # Use identity coloring (existing logic)
395
439
  identities = [id_dictionary.get(cluster_id, "Unknown") for cluster_id in cluster_ids]
396
440
  unique_identities = sorted(list(set(identities)))
397
441
  colors = generate_distinct_colors(len(unique_identities))
398
442
  identity_to_color = {identity: colors[i] for i, identity in enumerate(unique_identities)}
399
443
  point_colors = [identity_to_color[identity] for identity in identities]
400
444
  use_identity_coloring = True
445
+ use_neighborhood_coloring = False
401
446
  else:
402
447
  # Use default cluster ID coloring
403
448
  point_colors = cluster_ids
404
449
  use_identity_coloring = False
450
+ use_neighborhood_coloring = False
405
451
 
406
452
  # Create visualization
407
453
  plt.figure(figsize=(12, 8))
408
454
 
409
455
  if n_components == 2:
410
- if use_identity_coloring:
456
+ if use_neighborhood_coloring:
457
+ scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
458
+ c=point_colors, s=100, alpha=0.7)
459
+ elif use_identity_coloring:
411
460
  scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
412
461
  c=point_colors, s=100, alpha=0.7)
413
462
  else:
@@ -418,7 +467,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
418
467
  # Add cluster ID labels
419
468
  for i, cluster_id in enumerate(cluster_ids):
420
469
  display_label = f'{cluster_id}'
421
- if id_dictionary is not None:
470
+ if use_neighborhood_coloring and cluster_id in filtered_neighborhoods:
471
+ neighborhood_id = filtered_neighborhoods[cluster_id]
472
+ display_label = f'{cluster_id}\n(N{neighborhood_id})'
473
+ elif id_dictionary is not None:
422
474
  identity = id_dictionary.get(cluster_id, "Unknown")
423
475
  display_label = f'{cluster_id}\n({identity})'
424
476
 
@@ -428,7 +480,20 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
428
480
  fontsize=9, alpha=0.8, ha='left')
429
481
 
430
482
  # Add appropriate legend/colorbar
431
- if use_identity_coloring:
483
+ if use_neighborhood_coloring:
484
+ # Create custom legend for neighborhoods
485
+ legend_elements = []
486
+ for neighborhood_id in unique_neighborhoods:
487
+ color = neighborhood_to_color[neighborhood_id]
488
+ norm_color = (color[0]/255.0, color[1]/255.0, color[2]/255.0)
489
+ legend_elements.append(
490
+ plt.Line2D([0], [0], marker='o', color='w',
491
+ markerfacecolor=norm_color,
492
+ markersize=10, label=f'Neighborhood {neighborhood_id}')
493
+ )
494
+ plt.legend(handles=legend_elements, title='Neighborhoods',
495
+ bbox_to_anchor=(1.05, 1), loc='upper left')
496
+ elif use_identity_coloring:
432
497
  # Create custom legend for identities
433
498
  legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
434
499
  markerfacecolor=identity_to_color[identity],
@@ -441,7 +506,9 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
441
506
 
442
507
  plt.xlabel('UMAP Component 1')
443
508
  plt.ylabel('UMAP Component 2')
444
- if use_identity_coloring:
509
+ if use_neighborhood_coloring:
510
+ title += ' (Colored by Neighborhood)'
511
+ elif use_identity_coloring:
445
512
  title += ' (Colored by Identity)'
446
513
  plt.title(title)
447
514
 
@@ -449,7 +516,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
449
516
  fig = plt.figure(figsize=(14, 10))
450
517
  ax = fig.add_subplot(111, projection='3d')
451
518
 
452
- if use_identity_coloring:
519
+ if use_neighborhood_coloring:
520
+ scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
521
+ c=point_colors, s=100, alpha=0.7)
522
+ elif use_identity_coloring:
453
523
  scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
454
524
  c=point_colors, s=100, alpha=0.7)
455
525
  else:
@@ -460,7 +530,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
460
530
  # Add cluster ID labels
461
531
  for i, cluster_id in enumerate(cluster_ids):
462
532
  display_label = f'C{cluster_id}'
463
- if id_dictionary is not None:
533
+ if use_neighborhood_coloring and cluster_id in filtered_neighborhoods:
534
+ neighborhood_id = filtered_neighborhoods[cluster_id]
535
+ display_label = f'C{cluster_id}\n(N{neighborhood_id})'
536
+ elif id_dictionary is not None:
464
537
  identity = id_dictionary.get(cluster_id, "Unknown")
465
538
  display_label = f'C{cluster_id}\n({identity})'
466
539
 
@@ -471,12 +544,27 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
471
544
  ax.set_ylabel('UMAP Component 2')
472
545
  ax.set_zlabel('UMAP Component 3')
473
546
  title = '3D UMAP Visualization of Cluster Compositions'
474
- if use_identity_coloring:
547
+ if use_neighborhood_coloring:
548
+ title += ' (Colored by Neighborhood)'
549
+ elif use_identity_coloring:
475
550
  title += ' (Colored by Identity)'
476
551
  ax.set_title(title)
477
552
 
478
553
  # Add appropriate legend/colorbar
479
- if use_identity_coloring:
554
+ if use_neighborhood_coloring:
555
+ # Create custom legend for neighborhoods
556
+ legend_elements = []
557
+ for neighborhood_id in unique_neighborhoods:
558
+ color = neighborhood_to_color[neighborhood_id]
559
+ norm_color = (color[0]/255.0, color[1]/255.0, color[2]/255.0)
560
+ legend_elements.append(
561
+ plt.Line2D([0], [0], marker='o', color='w',
562
+ markerfacecolor=norm_color,
563
+ markersize=10, label=f'Neighborhood {neighborhood_id}')
564
+ )
565
+ ax.legend(handles=legend_elements, title='Neighborhoods',
566
+ bbox_to_anchor=(1.05, 1), loc='upper left')
567
+ elif use_identity_coloring:
480
568
  # Create custom legend for identities
481
569
  legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
482
570
  markerfacecolor=identity_to_color[identity],
@@ -496,12 +584,15 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
496
584
  print(f"Classes: {class_labels}")
497
585
  for i, cluster_id in enumerate(cluster_ids):
498
586
  composition = compositions[i]
499
- identity_info = ""
500
- if id_dictionary is not None:
587
+ additional_info = ""
588
+ if use_neighborhood_coloring and cluster_id in filtered_neighborhoods:
589
+ neighborhood_id = filtered_neighborhoods[cluster_id]
590
+ additional_info = f" (Neighborhood: {neighborhood_id})"
591
+ elif id_dictionary is not None:
501
592
  identity = id_dictionary.get(cluster_id, "Unknown")
502
- identity_info = f" (Identity: {identity})"
593
+ additional_info = f" (Identity: {identity})"
503
594
 
504
- print(f"Cluster {cluster_id}{identity_info}: {composition}")
595
+ print(f"Cluster {cluster_id}{additional_info}: {composition}")
505
596
  # Show which classes dominate this cluster
506
597
  dominant_indices = np.argsort(composition)[::-1][:2] # Top 2
507
598
  dominant_classes = [class_labels[idx] for idx in dominant_indices]
@@ -644,25 +735,41 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
644
735
 
645
736
  # Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
646
737
  def intensity_to_rgb(intensity, min_val, max_val):
647
- """Convert intensity value to RGB using RdBu_r colormap logic"""
738
+ """Convert intensity value to RGB using RdBu_r colormap logic, centered at 0"""
739
+
740
+ # Handle edge case where all values are the same
648
741
  if max_val == min_val:
649
- # All same value, use neutral color
742
+ if intensity == 0:
743
+ return np.array([255, 255, 255], dtype=np.uint8) # White for 0
744
+ elif intensity > 0:
745
+ return np.array([255, 200, 200], dtype=np.uint8) # Light red for positive
746
+ else:
747
+ return np.array([200, 200, 255], dtype=np.uint8) # Light blue for negative
748
+
749
+ # Find the maximum absolute value for symmetric scaling around 0
750
+ max_abs = max(abs(min_val), abs(max_val))
751
+
752
+ # If max_abs is 0, everything is 0, so return white
753
+ if max_abs == 0:
650
754
  return np.array([255, 255, 255], dtype=np.uint8) # White
651
755
 
652
- # Normalize to -1 to 1 range (like RdBu_r colormap)
653
- normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
756
+ # Normalize intensity to -1 to 1 range, centered at 0
757
+ normalized = intensity / max_abs
654
758
  normalized = np.clip(normalized, -1, 1)
655
759
 
656
760
  if normalized > 0:
657
- # Positive values: white to red
761
+ # Positive values: white to red (intensity 0 = white, max positive = red)
658
762
  r = 255
659
763
  g = int(255 * (1 - normalized))
660
764
  b = int(255 * (1 - normalized))
661
- else:
662
- # Negative values: white to blue
765
+ elif normalized < 0:
766
+ # Negative values: white to blue (intensity 0 = white, max negative = blue)
663
767
  r = int(255 * (1 + normalized))
664
768
  g = int(255 * (1 + normalized))
665
769
  b = 255
770
+ else:
771
+ # Exactly 0: white
772
+ r, g, b = 255, 255, 255
666
773
 
667
774
  return np.array([r, g, b], dtype=np.uint8)
668
775
 
@@ -868,25 +975,41 @@ def create_node_heatmap(node_intensity, node_centroids, shape=None, is_3d=True,
868
975
 
869
976
  # Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
870
977
  def intensity_to_rgb(intensity, min_val, max_val):
871
- """Convert intensity value to RGB using RdBu_r colormap logic"""
978
+ """Convert intensity value to RGB using RdBu_r colormap logic, centered at 0"""
979
+
980
+ # Handle edge case where all values are the same
872
981
  if max_val == min_val:
873
- # All same value, use neutral color
982
+ if intensity == 0:
983
+ return np.array([255, 255, 255], dtype=np.uint8) # White for 0
984
+ elif intensity > 0:
985
+ return np.array([255, 200, 200], dtype=np.uint8) # Light red for positive
986
+ else:
987
+ return np.array([200, 200, 255], dtype=np.uint8) # Light blue for negative
988
+
989
+ # Find the maximum absolute value for symmetric scaling around 0
990
+ max_abs = max(abs(min_val), abs(max_val))
991
+
992
+ # If max_abs is 0, everything is 0, so return white
993
+ if max_abs == 0:
874
994
  return np.array([255, 255, 255], dtype=np.uint8) # White
875
995
 
876
- # Normalize to -1 to 1 range (like RdBu_r colormap)
877
- normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
996
+ # Normalize intensity to -1 to 1 range, centered at 0
997
+ normalized = intensity / max_abs
878
998
  normalized = np.clip(normalized, -1, 1)
879
999
 
880
1000
  if normalized > 0:
881
- # Positive values: white to red
1001
+ # Positive values: white to red (intensity 0 = white, max positive = red)
882
1002
  r = 255
883
1003
  g = int(255 * (1 - normalized))
884
1004
  b = int(255 * (1 - normalized))
885
- else:
886
- # Negative values: white to blue
1005
+ elif normalized < 0:
1006
+ # Negative values: white to blue (intensity 0 = white, max negative = blue)
887
1007
  r = int(255 * (1 + normalized))
888
1008
  g = int(255 * (1 + normalized))
889
1009
  b = 255
1010
+ else:
1011
+ # Exactly 0: white
1012
+ r, g, b = 255, 255, 255
890
1013
 
891
1014
  return np.array([r, g, b], dtype=np.uint8)
892
1015