nettracer3d 0.8.3__tar.gz → 0.8.5__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.3 → nettracer3d-0.8.5}/LICENSE +2 -4
- {nettracer3d-0.8.3/src/nettracer3d.egg-info → nettracer3d-0.8.5}/PKG-INFO +6 -3
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/README.md +5 -2
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/pyproject.toml +1 -1
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/community_extractor.py +3 -3
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/excelotron.py +21 -2
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/neighborhoods.py +140 -31
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/nettracer.py +516 -82
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/nettracer_gui.py +1072 -842
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/network_analysis.py +90 -29
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/node_draw.py +6 -2
- nettracer3d-0.8.5/src/nettracer3d/painting.py +373 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/proximity.py +52 -103
- nettracer3d-0.8.5/src/nettracer3d/segmenter.py +1455 -0
- nettracer3d-0.8.5/src/nettracer3d/segmenter_GPU.py +1434 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/smart_dilate.py +44 -10
- {nettracer3d-0.8.3 → nettracer3d-0.8.5/src/nettracer3d.egg-info}/PKG-INFO +6 -3
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d.egg-info/SOURCES.txt +1 -0
- nettracer3d-0.8.3/src/nettracer3d/segmenter.py +0 -1457
- nettracer3d-0.8.3/src/nettracer3d/segmenter_GPU.py +0 -1286
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/setup.cfg +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-0.8.3 → nettracer3d-0.8.5}/src/nettracer3d.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
NetTracer3D is freely available for academic and nonprofit use and can be obtained from Liam McLaughlin (boom2449@gmail.com) OR at pip (pip install nettracer3d), provided that the following citation is included in any abstract, paper, or presentation utilizing NetTracer3D
|
|
1
|
+
NetTracer3D is freely available for academic and nonprofit use and can be obtained from Liam McLaughlin (boom2449@gmail.com) OR at pip (pip install nettracer3d), provided that the following citation is included in any abstract, paper, or presentation utilizing NetTracer3D:
|
|
2
2
|
|
|
3
|
-
Three
|
|
4
|
-
Liam McLaughlin, Bo Zhang, Siddharth Sharma, Amanda L. Knoten, Madhurima Kaushal, Jeffrey M. Purkerson, Heidy Huyck, Gloria S. Pryhuber, Joseph P. Gaut, Sanjay Jain
|
|
5
|
-
bioRxiv 2024.07.29.605633; doi: https://doi.org/10.1101/2024.07.29.605633
|
|
3
|
+
McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neurovascular nephron connectivity map of the human kidney across the lifespan. Nat Commun 16, 5161 (2025). https://doi.org/10.1038/s41467-025-60435-8
|
|
6
4
|
|
|
7
5
|
Commercial use is available for a fee. Copyright © is held by Washington University. Please direct all commercial requests for licensing, information, and limited evaluation copies to Washington University's Office of Technology Management at OTM@wustl.edu.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.5
|
|
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/
|
|
@@ -104,9 +104,12 @@ This gui is built from the PyQt6 package and therefore may not function on docke
|
|
|
104
104
|
For a (slightly outdated) video tutorial on using the GUI: https://www.youtube.com/watch?v=cRatn5VTWDY
|
|
105
105
|
|
|
106
106
|
NetTracer3D is free to use/fork for academic/nonprofit use so long as citation is provided, and is available for commercial use at a fee (see license file for information).
|
|
107
|
+
The current citation is here:
|
|
108
|
+
|
|
109
|
+
McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neurovascular nephron connectivity map of the human kidney across the lifespan. Nat Commun 16, 5161 (2025). https://doi.org/10.1038/s41467-025-60435-8
|
|
107
110
|
|
|
108
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
109
112
|
|
|
110
|
-
-- Version 0.8.
|
|
113
|
+
-- Version 0.8.5 Updates --
|
|
111
114
|
|
|
112
|
-
*
|
|
115
|
+
* See Documentation Once Updated
|
|
@@ -59,9 +59,12 @@ This gui is built from the PyQt6 package and therefore may not function on docke
|
|
|
59
59
|
For a (slightly outdated) video tutorial on using the GUI: https://www.youtube.com/watch?v=cRatn5VTWDY
|
|
60
60
|
|
|
61
61
|
NetTracer3D is free to use/fork for academic/nonprofit use so long as citation is provided, and is available for commercial use at a fee (see license file for information).
|
|
62
|
+
The current citation is here:
|
|
63
|
+
|
|
64
|
+
McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neurovascular nephron connectivity map of the human kidney across the lifespan. Nat Commun 16, 5161 (2025). https://doi.org/10.1038/s41467-025-60435-8
|
|
62
65
|
|
|
63
66
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
64
67
|
|
|
65
|
-
-- Version 0.8.
|
|
68
|
+
-- Version 0.8.5 Updates --
|
|
66
69
|
|
|
67
|
-
*
|
|
70
|
+
* See Documentation Once Updated
|
|
@@ -12,6 +12,7 @@ import random
|
|
|
12
12
|
from . import node_draw
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
def binarize(image):
|
|
16
17
|
"""Convert an array from numerical values to boolean mask"""
|
|
17
18
|
image = image != 0
|
|
@@ -462,7 +463,6 @@ def get_color_name_mapping():
|
|
|
462
463
|
'bright_cyan': (0, 255, 255),
|
|
463
464
|
'dark_teal': (0, 128, 128),
|
|
464
465
|
'turquoise': (64, 224, 208),
|
|
465
|
-
'aqua': (0, 255, 255),
|
|
466
466
|
'seafoam': (159, 226, 191),
|
|
467
467
|
'teal_blue': (54, 117, 136),
|
|
468
468
|
|
|
@@ -655,7 +655,7 @@ def assign_node_colors(node_list: List[int], labeled_array: np.ndarray) -> Tuple
|
|
|
655
655
|
|
|
656
656
|
# Create lookup table
|
|
657
657
|
max_label = max(max(labeled_array.flat), max(node_list) if node_list else 0)
|
|
658
|
-
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8) # Transparent by default
|
|
658
|
+
color_lut = np.zeros((int(max_label) + 1, 4), dtype=np.uint8) # Transparent by default
|
|
659
659
|
|
|
660
660
|
for node_id, color in node_to_color.items():
|
|
661
661
|
color_lut[node_id] = color
|
|
@@ -685,7 +685,7 @@ def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.nd
|
|
|
685
685
|
|
|
686
686
|
# Create lookup table - this is the key optimization
|
|
687
687
|
max_label = max(max(labeled_array.flat), max(node_to_color.keys()) if node_to_color else 0)
|
|
688
|
-
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8) # Transparent by default
|
|
688
|
+
color_lut = np.zeros((int(max_label) + 1, 4), dtype=np.uint8) # Transparent by default
|
|
689
689
|
|
|
690
690
|
for node_id, color in node_to_color.items():
|
|
691
691
|
color_lut[node_id] = color
|
|
@@ -1238,7 +1238,7 @@ class TabbedIdentityWidget(QFrame):
|
|
|
1238
1238
|
|
|
1239
1239
|
class ExcelToDictGUI(QMainWindow):
|
|
1240
1240
|
# Add this signal
|
|
1241
|
-
data_exported = pyqtSignal(dict, str) # dictionary, property_name
|
|
1241
|
+
data_exported = pyqtSignal(dict, str, bool) # dictionary, property_name, add_status
|
|
1242
1242
|
|
|
1243
1243
|
def __init__(self):
|
|
1244
1244
|
super().__init__()
|
|
@@ -1255,6 +1255,7 @@ class ExcelToDictGUI(QMainWindow):
|
|
|
1255
1255
|
|
|
1256
1256
|
self.setWindowTitle("Excel to Python Dictionary Converter")
|
|
1257
1257
|
self.setGeometry(100, 100, 1200, 800)
|
|
1258
|
+
self.add = False
|
|
1258
1259
|
|
|
1259
1260
|
self.setup_ui()
|
|
1260
1261
|
|
|
@@ -1414,6 +1415,14 @@ class ExcelToDictGUI(QMainWindow):
|
|
|
1414
1415
|
self.export_btn.clicked.connect(self.export_dictionary)
|
|
1415
1416
|
export_layout.addWidget(self.export_btn)
|
|
1416
1417
|
|
|
1418
|
+
self.add_button = QPushButton("+")
|
|
1419
|
+
self.add_button.setFixedSize(20, 20)
|
|
1420
|
+
self.add_button.setCheckable(True)
|
|
1421
|
+
self.add_button.setChecked(False)
|
|
1422
|
+
self.add_button.clicked.connect(self.toggle_add)
|
|
1423
|
+
export_layout.addWidget(self.add_button)
|
|
1424
|
+
|
|
1425
|
+
|
|
1417
1426
|
right_layout.addLayout(export_layout)
|
|
1418
1427
|
|
|
1419
1428
|
right_widget.setLayout(right_layout)
|
|
@@ -1431,6 +1440,15 @@ class ExcelToDictGUI(QMainWindow):
|
|
|
1431
1440
|
# Add splitter to main layout
|
|
1432
1441
|
main_layout.addWidget(splitter)
|
|
1433
1442
|
|
|
1443
|
+
def toggle_add(self):
|
|
1444
|
+
|
|
1445
|
+
if self.add_button.isChecked():
|
|
1446
|
+
print("Exported Properties will be added onto existing ones")
|
|
1447
|
+
self.add = True
|
|
1448
|
+
else:
|
|
1449
|
+
print("Exported Properties will be override existing ones")
|
|
1450
|
+
self.add = False
|
|
1451
|
+
|
|
1434
1452
|
def load_template(self, template_name):
|
|
1435
1453
|
if template_name in self.templates:
|
|
1436
1454
|
# Clear existing columns
|
|
@@ -1681,12 +1699,13 @@ class ExcelToDictGUI(QMainWindow):
|
|
|
1681
1699
|
return
|
|
1682
1700
|
|
|
1683
1701
|
# Emit signal to parent application
|
|
1684
|
-
self.data_exported.emit(result_dict, property_name)
|
|
1702
|
+
self.data_exported.emit(result_dict, property_name, self.add)
|
|
1685
1703
|
|
|
1686
1704
|
# Still store in global variables for backward compatibility
|
|
1687
1705
|
import builtins
|
|
1688
1706
|
builtins.excel_dict = result_dict
|
|
1689
1707
|
builtins.target_property = property_name
|
|
1708
|
+
builtins.add = self.add
|
|
1690
1709
|
|
|
1691
1710
|
# Show success message with preview
|
|
1692
1711
|
preview = str(result_dict)
|
|
@@ -2,11 +2,13 @@ import numpy as np
|
|
|
2
2
|
from sklearn.cluster import KMeans
|
|
3
3
|
from sklearn.metrics import calinski_harabasz_score
|
|
4
4
|
import matplotlib.pyplot as plt
|
|
5
|
-
from typing import Dict, Set
|
|
5
|
+
from typing import Dict, Set, List, Tuple, Optional
|
|
6
6
|
import umap
|
|
7
7
|
from matplotlib.colors import LinearSegmentedColormap
|
|
8
8
|
from sklearn.cluster import DBSCAN
|
|
9
9
|
from sklearn.neighbors import NearestNeighbors
|
|
10
|
+
import matplotlib.colors as mcolors
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
import os
|
|
@@ -308,11 +310,43 @@ def plot_dict_heatmap(unsorted_data_dict, id_set, figsize=(12, 8), title="Neighb
|
|
|
308
310
|
|
|
309
311
|
|
|
310
312
|
|
|
313
|
+
def generate_distinct_colors(n_colors: int) -> List[str]:
|
|
314
|
+
"""
|
|
315
|
+
Generate visually distinct colors using matplotlib's tab and Set colormaps.
|
|
316
|
+
Falls back to HSV generation for large numbers of colors.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
n_colors: Number of distinct colors needed
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of color strings compatible with matplotlib
|
|
323
|
+
"""
|
|
324
|
+
if n_colors <= 10:
|
|
325
|
+
# Use tab10 colormap for up to 10 colors
|
|
326
|
+
colors = plt.cm.tab10(np.linspace(0, 1, min(n_colors, 10)))
|
|
327
|
+
return [mcolors.rgb2hex(color) for color in colors[:n_colors]]
|
|
328
|
+
elif n_colors <= 20:
|
|
329
|
+
# Use tab20 for up to 20 colors
|
|
330
|
+
colors = plt.cm.tab20(np.linspace(0, 1, min(n_colors, 20)))
|
|
331
|
+
return [mcolors.rgb2hex(color) for color in colors[:n_colors]]
|
|
332
|
+
else:
|
|
333
|
+
# For larger numbers, use HSV space
|
|
334
|
+
colors = []
|
|
335
|
+
for i in range(n_colors):
|
|
336
|
+
hue = i / n_colors
|
|
337
|
+
color = mcolors.hsv_to_rgb([hue, 0.8, 0.9]) # High saturation and value
|
|
338
|
+
colors.append(mcolors.rgb2hex(color))
|
|
339
|
+
return colors
|
|
340
|
+
|
|
341
|
+
|
|
311
342
|
def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
312
343
|
class_names: Set[str],
|
|
313
|
-
label = False,
|
|
344
|
+
label: bool = False,
|
|
314
345
|
n_components: int = 2,
|
|
315
|
-
random_state: int = 42
|
|
346
|
+
random_state: int = 42,
|
|
347
|
+
id_dictionary: Optional[Dict[int, str]] = None,
|
|
348
|
+
graph_label = "Community ID",
|
|
349
|
+
title = 'UMAP Visualization of Community Compositions'):
|
|
316
350
|
"""
|
|
317
351
|
Convert cluster composition data to UMAP visualization.
|
|
318
352
|
|
|
@@ -323,10 +357,15 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
323
357
|
representing the composition of each cluster
|
|
324
358
|
class_names : set
|
|
325
359
|
Set of strings representing the class names (order corresponds to array indices)
|
|
360
|
+
label : bool
|
|
361
|
+
Whether to show cluster ID labels on the plot
|
|
326
362
|
n_components : int
|
|
327
363
|
Number of UMAP components (default: 2 for 2D visualization)
|
|
328
364
|
random_state : int
|
|
329
365
|
Random state for reproducibility
|
|
366
|
+
id_dictionary : dict, optional
|
|
367
|
+
Dictionary mapping cluster IDs to identity names. If provided, colors will be
|
|
368
|
+
assigned by identity rather than cluster ID, and a legend will be shown.
|
|
330
369
|
|
|
331
370
|
Returns:
|
|
332
371
|
--------
|
|
@@ -335,7 +374,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
335
374
|
"""
|
|
336
375
|
|
|
337
376
|
# Convert set to sorted list for consistent ordering
|
|
338
|
-
|
|
377
|
+
try:
|
|
378
|
+
class_labels = sorted(list(class_names))
|
|
379
|
+
except:
|
|
380
|
+
class_labels = None
|
|
339
381
|
|
|
340
382
|
# Extract cluster IDs and compositions
|
|
341
383
|
cluster_ids = list(cluster_data.keys())
|
|
@@ -347,57 +389,124 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
347
389
|
# Fit and transform the composition data
|
|
348
390
|
embedding = reducer.fit_transform(compositions)
|
|
349
391
|
|
|
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)
|
|
395
|
+
identities = [id_dictionary.get(cluster_id, "Unknown") for cluster_id in cluster_ids]
|
|
396
|
+
unique_identities = sorted(list(set(identities)))
|
|
397
|
+
colors = generate_distinct_colors(len(unique_identities))
|
|
398
|
+
identity_to_color = {identity: colors[i] for i, identity in enumerate(unique_identities)}
|
|
399
|
+
point_colors = [identity_to_color[identity] for identity in identities]
|
|
400
|
+
use_identity_coloring = True
|
|
401
|
+
else:
|
|
402
|
+
# Use default cluster ID coloring
|
|
403
|
+
point_colors = cluster_ids
|
|
404
|
+
use_identity_coloring = False
|
|
405
|
+
|
|
350
406
|
# Create visualization
|
|
351
|
-
plt.figure(figsize=(
|
|
407
|
+
plt.figure(figsize=(12, 8))
|
|
352
408
|
|
|
353
409
|
if n_components == 2:
|
|
354
|
-
|
|
355
|
-
|
|
410
|
+
if use_identity_coloring:
|
|
411
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
412
|
+
c=point_colors, s=100, alpha=0.7)
|
|
413
|
+
else:
|
|
414
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
415
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
356
416
|
|
|
357
417
|
if label:
|
|
358
418
|
# Add cluster ID labels
|
|
359
419
|
for i, cluster_id in enumerate(cluster_ids):
|
|
360
|
-
|
|
420
|
+
display_label = f'{cluster_id}'
|
|
421
|
+
if id_dictionary is not None:
|
|
422
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
423
|
+
display_label = f'{cluster_id}\n({identity})'
|
|
424
|
+
|
|
425
|
+
plt.annotate(display_label,
|
|
361
426
|
(embedding[i, 0], embedding[i, 1]),
|
|
362
427
|
xytext=(5, 5), textcoords='offset points',
|
|
363
|
-
fontsize=9, alpha=0.8)
|
|
428
|
+
fontsize=9, alpha=0.8, ha='left')
|
|
429
|
+
|
|
430
|
+
# Add appropriate legend/colorbar
|
|
431
|
+
if use_identity_coloring:
|
|
432
|
+
# Create custom legend for identities
|
|
433
|
+
legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
|
|
434
|
+
markerfacecolor=identity_to_color[identity],
|
|
435
|
+
markersize=10, label=identity)
|
|
436
|
+
for identity in unique_identities]
|
|
437
|
+
plt.legend(handles=legend_elements, title='Identity',
|
|
438
|
+
bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
439
|
+
else:
|
|
440
|
+
plt.colorbar(scatter, label=graph_label)
|
|
364
441
|
|
|
365
|
-
plt.colorbar(scatter, label='Community ID')
|
|
366
442
|
plt.xlabel('UMAP Component 1')
|
|
367
443
|
plt.ylabel('UMAP Component 2')
|
|
368
|
-
|
|
444
|
+
if use_identity_coloring:
|
|
445
|
+
title += ' (Colored by Identity)'
|
|
446
|
+
plt.title(title)
|
|
369
447
|
|
|
370
448
|
elif n_components == 3:
|
|
371
|
-
fig = plt.figure(figsize=(
|
|
449
|
+
fig = plt.figure(figsize=(14, 10))
|
|
372
450
|
ax = fig.add_subplot(111, projection='3d')
|
|
373
|
-
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
374
|
-
c=cluster_ids, cmap='viridis', s=100, alpha=0.7)
|
|
375
451
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
452
|
+
if use_identity_coloring:
|
|
453
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
454
|
+
c=point_colors, s=100, alpha=0.7)
|
|
455
|
+
else:
|
|
456
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
457
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
458
|
+
|
|
459
|
+
if label:
|
|
460
|
+
# Add cluster ID labels
|
|
461
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
462
|
+
display_label = f'C{cluster_id}'
|
|
463
|
+
if id_dictionary is not None:
|
|
464
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
465
|
+
display_label = f'C{cluster_id}\n({identity})'
|
|
466
|
+
|
|
467
|
+
ax.text(embedding[i, 0], embedding[i, 1], embedding[i, 2],
|
|
468
|
+
display_label, fontsize=8)
|
|
380
469
|
|
|
381
470
|
ax.set_xlabel('UMAP Component 1')
|
|
382
471
|
ax.set_ylabel('UMAP Component 2')
|
|
383
472
|
ax.set_zlabel('UMAP Component 3')
|
|
384
|
-
|
|
385
|
-
|
|
473
|
+
title = '3D UMAP Visualization of Cluster Compositions'
|
|
474
|
+
if use_identity_coloring:
|
|
475
|
+
title += ' (Colored by Identity)'
|
|
476
|
+
ax.set_title(title)
|
|
477
|
+
|
|
478
|
+
# Add appropriate legend/colorbar
|
|
479
|
+
if use_identity_coloring:
|
|
480
|
+
# Create custom legend for identities
|
|
481
|
+
legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
|
|
482
|
+
markerfacecolor=identity_to_color[identity],
|
|
483
|
+
markersize=10, label=identity)
|
|
484
|
+
for identity in unique_identities]
|
|
485
|
+
ax.legend(handles=legend_elements, title='Identity',
|
|
486
|
+
bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
487
|
+
else:
|
|
488
|
+
plt.colorbar(scatter, label='Cluster ID')
|
|
386
489
|
|
|
387
490
|
plt.tight_layout()
|
|
388
491
|
plt.show()
|
|
389
492
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
493
|
+
if class_labels is not None:
|
|
494
|
+
# Print composition details
|
|
495
|
+
print("Cluster Compositions:")
|
|
496
|
+
print(f"Classes: {class_labels}")
|
|
497
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
498
|
+
composition = compositions[i]
|
|
499
|
+
identity_info = ""
|
|
500
|
+
if id_dictionary is not None:
|
|
501
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
502
|
+
identity_info = f" (Identity: {identity})"
|
|
503
|
+
|
|
504
|
+
print(f"Cluster {cluster_id}{identity_info}: {composition}")
|
|
505
|
+
# Show which classes dominate this cluster
|
|
506
|
+
dominant_indices = np.argsort(composition)[::-1][:2] # Top 2
|
|
507
|
+
dominant_classes = [class_labels[idx] for idx in dominant_indices]
|
|
508
|
+
dominant_values = [composition[idx] for idx in dominant_indices]
|
|
509
|
+
print(f" Dominant: {dominant_classes[0]} ({dominant_values[0]:.3f}), {dominant_classes[1]} ({dominant_values[1]:.3f})")
|
|
401
510
|
|
|
402
511
|
return embedding
|
|
403
512
|
|