nettracer3d 0.8.9__tar.gz → 0.9.1__tar.gz
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.
- {nettracer3d-0.8.9/src/nettracer3d.egg-info → nettracer3d-0.9.1}/PKG-INFO +7 -4
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/README.md +6 -3
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/pyproject.toml +1 -1
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/cellpose_manager.py +22 -11
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/modularity.py +24 -9
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/neighborhoods.py +153 -30
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/nettracer.py +321 -54
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/nettracer_gui.py +1095 -478
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/proximity.py +101 -48
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/segmenter.py +514 -372
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/segmenter_GPU.py +434 -281
- {nettracer3d-0.8.9 → nettracer3d-0.9.1/src/nettracer3d.egg-info}/PKG-INFO +7 -4
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/LICENSE +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/setup.cfg +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/painting.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-0.8.9 → nettracer3d-0.9.1}/src/nettracer3d.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -110,6 +110,9 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 0.
|
|
114
|
-
|
|
115
|
-
*
|
|
113
|
+
-- Version 0.9.1 Updates --
|
|
114
|
+
* Adjusted the segment by 3D function to now show the 3D chunks in the preview mode. Previously it showed 2D segmentations in the preview which finished the current plane faster but didn't show accurate training data.
|
|
115
|
+
* Adjusted the neighborhood heatmap predicted range value to now just simulate a uniform distribution rather than trying to use a mathematical algorithm.
|
|
116
|
+
* The image display window now uses image pyramids and cropping for zoom ins so it should run a lot faster on bigger images.
|
|
117
|
+
* The community UMAP can now color them by neighborhood.
|
|
118
|
+
* No longer zooms all the way out by default with right click in zoom mode. Now user needs to Shift + Right Click.
|
|
@@ -65,6 +65,9 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
65
65
|
|
|
66
66
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
67
67
|
|
|
68
|
-
-- Version 0.
|
|
69
|
-
|
|
70
|
-
*
|
|
68
|
+
-- Version 0.9.1 Updates --
|
|
69
|
+
* Adjusted the segment by 3D function to now show the 3D chunks in the preview mode. Previously it showed 2D segmentations in the preview which finished the current plane faster but didn't show accurate training data.
|
|
70
|
+
* Adjusted the neighborhood heatmap predicted range value to now just simulate a uniform distribution rather than trying to use a mathematical algorithm.
|
|
71
|
+
* The image display window now uses image pyramids and cropping for zoom ins so it should run a lot faster on bigger images.
|
|
72
|
+
* The community UMAP can now color them by neighborhood.
|
|
73
|
+
* No longer zooms all the way out by default with right click in zoom mode. Now user needs to Shift + Right Click.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
|
|
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=[
|
|
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=[
|
|
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 = [
|
|
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
|
-
#
|
|
393
|
-
if
|
|
394
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
500
|
-
if
|
|
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
|
-
|
|
593
|
+
additional_info = f" (Identity: {identity})"
|
|
503
594
|
|
|
504
|
-
print(f"Cluster {cluster_id}{
|
|
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
|
-
|
|
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
|
|
653
|
-
normalized =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
877
|
-
normalized =
|
|
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
|
-
|
|
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
|
|