nettracer3d 0.9.3__py3-none-any.whl → 0.9.4__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.

nettracer3d/excelotron.py CHANGED
@@ -4,7 +4,7 @@ import numpy as np
4
4
  from PyQt6.QtWidgets import (QApplication, QMainWindow, QHBoxLayout, QVBoxLayout,
5
5
  QWidget, QTableWidget, QTableWidgetItem, QPushButton,
6
6
  QLabel, QLineEdit, QScrollArea, QFrame, QMessageBox,
7
- QHeaderView, QAbstractItemView, QSplitter, QTabWidget)
7
+ QHeaderView, QAbstractItemView, QSplitter, QTabWidget, QCheckBox)
8
8
  from PyQt6.QtCore import Qt, QMimeData, pyqtSignal
9
9
  from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QDrag, QPainter, QPixmap
10
10
  import os
@@ -502,6 +502,35 @@ class ClassifierGroupWidget(QFrame):
502
502
 
503
503
  header_layout.addStretch()
504
504
 
505
+ # Hierarchical toggle
506
+ self.hierarchical_checkbox = QCheckBox("Hierarchical")
507
+ self.hierarchical_checkbox.setChecked(True) # Default to hierarchical
508
+ self.hierarchical_checkbox.setStyleSheet("""
509
+ QCheckBox {
510
+ font-weight: bold;
511
+ color: #007acc;
512
+ padding: 5px;
513
+ }
514
+ QCheckBox::indicator {
515
+ width: 18px;
516
+ height: 18px;
517
+ }
518
+ QCheckBox::indicator:unchecked {
519
+ border: 2px solid #007acc;
520
+ background-color: white;
521
+ border-radius: 3px;
522
+ }
523
+ QCheckBox::indicator:checked {
524
+ border: 2px solid #007acc;
525
+ background-color: #007acc;
526
+ border-radius: 3px;
527
+ }
528
+ QCheckBox::indicator:checked:hover {
529
+ background-color: #005a9e;
530
+ }
531
+ """)
532
+ header_layout.addWidget(self.hierarchical_checkbox)
533
+
505
534
  # Add classifier button
506
535
  add_btn = QPushButton("+ Add Classifier")
507
536
  add_btn.setStyleSheet("""
@@ -559,7 +588,7 @@ class ClassifierGroupWidget(QFrame):
559
588
  layout.addWidget(preview_btn)
560
589
 
561
590
  self.setLayout(layout)
562
-
591
+
563
592
  def add_classifier(self):
564
593
  self.classifier_counter += 1
565
594
  classifier_id = self.classifier_counter
@@ -597,25 +626,50 @@ class ClassifierGroupWidget(QFrame):
597
626
  matched_identities = set()
598
627
  classifier_usage = {classifier_id: 0 for classifier_id in classifier_ids} # Now classifier_ids is defined
599
628
 
629
+
600
630
  for identity in original_identities:
601
- identity_str = str(identity)
602
-
603
- # Check classifiers in order
604
- for classifier_id in classifier_ids:
605
- classifier = self.classifiers[classifier_id]
606
-
607
- if classifier.matches_identity(identity_str):
608
- # This classifier matches
609
- matched_identities.add(identity)
610
- classifier_usage[classifier_id] += 1 # Track usage
631
+ identity_str = str(identity)
611
632
 
612
- # Set the new ID if provided
613
- new_id = classifier.get_new_id()
614
- if new_id and identity in self.identity_remap_widget.identity_mappings:
615
- self.identity_remap_widget.identity_mappings[identity]['new_edit'].setText(new_id)
616
-
617
- # Move to next identity (hierarchical - first match wins)
618
- break
633
+ # Check classifiers in order
634
+ for classifier_id in classifier_ids:
635
+ classifier = self.classifiers[classifier_id]
636
+
637
+ if classifier.matches_identity(identity_str):
638
+ # This classifier matches
639
+ matched_identities.add(identity)
640
+ classifier_usage[classifier_id] += 1 # Track usage
641
+
642
+ # Set the new ID if provided
643
+ new_id = classifier.get_new_id()
644
+ if new_id and identity in self.identity_remap_widget.identity_mappings:
645
+ if self.hierarchical_checkbox.isChecked():
646
+ # Hierarchical mode - just set the new ID
647
+ self.identity_remap_widget.identity_mappings[identity]['new_edit'].setText(new_id)
648
+ else:
649
+ # Non-hierarchical mode - append to existing or create new
650
+ current_text = self.identity_remap_widget.identity_mappings[identity]['new_edit'].text().strip()
651
+ if current_text:
652
+ # Parse existing text to see if it's already a list
653
+ try:
654
+ existing_list = literal_eval(current_text)
655
+ if isinstance(existing_list, list):
656
+ existing_list.append(new_id)
657
+ new_text = str(existing_list)
658
+ else:
659
+ # Current text is a single value, make it a list
660
+ new_text = str([current_text, new_id])
661
+ except:
662
+ # If parsing fails, treat as single value
663
+ new_text = str([current_text, new_id])
664
+ else:
665
+ # No existing text, just set the new ID
666
+ new_text = new_id
667
+
668
+ self.identity_remap_widget.identity_mappings[identity]['new_edit'].setText(new_text)
669
+
670
+ # Only break if hierarchical mode (first match wins)
671
+ if self.hierarchical_checkbox.isChecked():
672
+ break
619
673
 
620
674
  # Remove identities that didn't match any classifier
621
675
  unmatched_identities = set(original_identities) - matched_identities
@@ -347,7 +347,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
347
347
  id_dictionary: Optional[Dict[int, str]] = None,
348
348
  graph_label = "Community ID",
349
349
  title = 'UMAP Visualization of Community Compositions',
350
- neighborhoods: Optional[Dict[int, int]] = None):
350
+ neighborhoods: Optional[Dict[int, int]] = None,
351
+ draw_lines: bool = False):
351
352
  """
352
353
  Convert cluster composition data to UMAP visualization.
353
354
 
@@ -370,6 +371,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
370
371
  neighborhoods : dict, optional
371
372
  Dictionary mapping node IDs to neighborhood IDs {node_id: neighborhood_id}.
372
373
  If provided, points will be colored by neighborhood using community coloration methods.
374
+ draw_lines : bool
375
+ Whether to draw lines between nodes that share identities (default: False)
373
376
 
374
377
  Returns:
375
378
  --------
@@ -453,15 +456,111 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
453
456
  plt.figure(figsize=(12, 8))
454
457
 
455
458
  if n_components == 2:
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:
460
- scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
461
- c=point_colors, s=100, alpha=0.7)
459
+ # Draw scatter with different markers for multi-identity nodes if draw_lines is enabled
460
+ if draw_lines:
461
+ # Separate multi-identity and singleton nodes for different markers
462
+ singleton_indices = []
463
+ multi_indices = []
464
+ singleton_colors = []
465
+ multi_colors = []
466
+
467
+ for i, cluster_id in enumerate(cluster_ids):
468
+ vec = cluster_data[cluster_id]
469
+ if np.sum(vec) > 1: # Multi-identity
470
+ multi_indices.append(i)
471
+ multi_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
472
+ else: # Singleton
473
+ singleton_indices.append(i)
474
+ singleton_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
475
+
476
+ # Draw singleton nodes as circles
477
+ if singleton_indices:
478
+ if use_neighborhood_coloring or use_identity_coloring:
479
+ scatter1 = plt.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1],
480
+ c=singleton_colors, s=100, alpha=0.7, marker='o')
481
+ else:
482
+ scatter1 = plt.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1],
483
+ c=[point_colors[i] for i in singleton_indices], cmap='viridis', s=100, alpha=0.7, marker='o')
484
+
485
+ # Draw multi-identity nodes as squares
486
+ if multi_indices:
487
+ if use_neighborhood_coloring or use_identity_coloring:
488
+ scatter2 = plt.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1],
489
+ c=multi_colors, s=100, alpha=0.7, marker='s')
490
+ else:
491
+ scatter2 = plt.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1],
492
+ c=[point_colors[i] for i in multi_indices], cmap='viridis', s=100, alpha=0.7, marker='s')
493
+ scatter = scatter2 # For colorbar reference
494
+ else:
495
+ scatter = scatter1 if singleton_indices else None
462
496
  else:
463
- scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
464
- c=point_colors, cmap='viridis', s=100, alpha=0.7)
497
+ # Original behavior when draw_lines is False
498
+ if use_neighborhood_coloring:
499
+ scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
500
+ c=point_colors, s=100, alpha=0.7)
501
+ elif use_identity_coloring:
502
+ scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
503
+ c=point_colors, s=100, alpha=0.7)
504
+ else:
505
+ scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
506
+ c=point_colors, cmap='viridis', s=100, alpha=0.7)
507
+
508
+ # Draw lines between nodes with shared identities (only if draw_lines=True)
509
+ if draw_lines:
510
+ # First pass: identify unique multi-identity configurations and their representatives
511
+ multi_config_map = {} # Maps tuple(config) -> {'count': int, 'representative_idx': int}
512
+
513
+ for i, cluster_id in enumerate(cluster_ids):
514
+ vec = cluster_data[cluster_id]
515
+ if np.sum(vec) > 1: # Multi-identity node
516
+ config = tuple(vec) # Convert to hashable tuple
517
+ if config not in multi_config_map:
518
+ multi_config_map[config] = {'count': 1, 'representative_idx': i}
519
+ else:
520
+ multi_config_map[config]['count'] += 1
521
+
522
+ # Second pass: draw lines for each unique configuration
523
+ for config, info in multi_config_map.items():
524
+ i = info['representative_idx']
525
+ count = info['count']
526
+ vec1 = np.array(config)
527
+
528
+ # For each identity this configuration has, find the closest representative
529
+ identity_indices = np.where(vec1 == 1)[0]
530
+
531
+ for identity_idx in identity_indices:
532
+ best_target = None
533
+ best_distance = float('inf')
534
+ backup_target = None
535
+ backup_distance = float('inf')
536
+
537
+ # Find closest node with this specific identity
538
+ for j, cluster_id2 in enumerate(cluster_ids):
539
+ if i != j: # Don't connect to self
540
+ vec2 = cluster_data[cluster_id2]
541
+ if vec2[identity_idx] == 1: # Shares this specific identity
542
+ distance = np.linalg.norm(embedding[i] - embedding[j])
543
+
544
+ # Prefer singleton nodes
545
+ if np.sum(vec2) == 1: # Singleton
546
+ if distance < best_distance:
547
+ best_distance = distance
548
+ best_target = j
549
+ else: # Multi-identity node (backup)
550
+ if distance < backup_distance:
551
+ backup_distance = distance
552
+ backup_target = j
553
+
554
+ # Draw line to best target (prefer singleton, fallback to multi)
555
+ target = best_target if best_target is not None else backup_target
556
+ if target is not None:
557
+ # Calculate relative line weight with reasonable cap
558
+ max_count = max(info['count'] for info in multi_config_map.values())
559
+ relative_weight = count / max_count # Normalize to 0-1
560
+ line_weight = 0.3 + relative_weight * 1.2 # Scale to 0.3-1.5 range
561
+ plt.plot([embedding[i, 0], embedding[target, 0]],
562
+ [embedding[i, 1], embedding[target, 1]],
563
+ alpha=0.3, color='gray', linewidth=line_weight)
465
564
 
466
565
  if label:
467
566
  # Add cluster ID labels
@@ -516,15 +615,112 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
516
615
  fig = plt.figure(figsize=(14, 10))
517
616
  ax = fig.add_subplot(111, projection='3d')
518
617
 
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:
523
- scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
524
- c=point_colors, s=100, alpha=0.7)
618
+ # Draw scatter with different markers for multi-identity nodes if draw_lines is enabled
619
+ if draw_lines:
620
+ # Separate multi-identity and singleton nodes for different markers
621
+ singleton_indices = []
622
+ multi_indices = []
623
+ singleton_colors = []
624
+ multi_colors = []
625
+
626
+ for i, cluster_id in enumerate(cluster_ids):
627
+ vec = cluster_data[cluster_id]
628
+ if np.sum(vec) > 1: # Multi-identity
629
+ multi_indices.append(i)
630
+ multi_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
631
+ else: # Singleton
632
+ singleton_indices.append(i)
633
+ singleton_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
634
+
635
+ # Draw singleton nodes as circles
636
+ if singleton_indices:
637
+ if use_neighborhood_coloring or use_identity_coloring:
638
+ scatter1 = ax.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1], embedding[singleton_indices, 2],
639
+ c=singleton_colors, s=100, alpha=0.7, marker='o')
640
+ else:
641
+ scatter1 = ax.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1], embedding[singleton_indices, 2],
642
+ c=[point_colors[i] for i in singleton_indices], cmap='viridis', s=100, alpha=0.7, marker='o')
643
+
644
+ # Draw multi-identity nodes as squares
645
+ if multi_indices:
646
+ if use_neighborhood_coloring or use_identity_coloring:
647
+ scatter2 = ax.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1], embedding[multi_indices, 2],
648
+ c=multi_colors, s=100, alpha=0.7, marker='s')
649
+ else:
650
+ scatter2 = ax.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1], embedding[multi_indices, 2],
651
+ c=[point_colors[i] for i in multi_indices], cmap='viridis', s=100, alpha=0.7, marker='s')
652
+ scatter = scatter2 # For colorbar reference
653
+ else:
654
+ scatter = scatter1 if singleton_indices else None
525
655
  else:
526
- scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
527
- c=point_colors, cmap='viridis', s=100, alpha=0.7)
656
+ # Original behavior when draw_lines is False
657
+ if use_neighborhood_coloring:
658
+ scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
659
+ c=point_colors, s=100, alpha=0.7)
660
+ elif use_identity_coloring:
661
+ scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
662
+ c=point_colors, s=100, alpha=0.7)
663
+ else:
664
+ scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
665
+ c=point_colors, cmap='viridis', s=100, alpha=0.7)
666
+
667
+ # Draw lines between nodes with shared identities (only if draw_lines=True)
668
+ if draw_lines:
669
+ # First pass: identify unique multi-identity configurations and their representatives
670
+ multi_config_map = {} # Maps tuple(config) -> {'count': int, 'representative_idx': int}
671
+
672
+ for i, cluster_id in enumerate(cluster_ids):
673
+ vec = cluster_data[cluster_id]
674
+ if np.sum(vec) > 1: # Multi-identity node
675
+ config = tuple(vec) # Convert to hashable tuple
676
+ if config not in multi_config_map:
677
+ multi_config_map[config] = {'count': 1, 'representative_idx': i}
678
+ else:
679
+ multi_config_map[config]['count'] += 1
680
+
681
+ # Second pass: draw lines for each unique configuration
682
+ for config, info in multi_config_map.items():
683
+ i = info['representative_idx']
684
+ count = info['count']
685
+ vec1 = np.array(config)
686
+
687
+ # For each identity this configuration has, find the closest representative
688
+ identity_indices = np.where(vec1 == 1)[0]
689
+
690
+ for identity_idx in identity_indices:
691
+ best_target = None
692
+ best_distance = float('inf')
693
+ backup_target = None
694
+ backup_distance = float('inf')
695
+
696
+ # Find closest node with this specific identity
697
+ for j, cluster_id2 in enumerate(cluster_ids):
698
+ if i != j: # Don't connect to self
699
+ vec2 = cluster_data[cluster_id2]
700
+ if vec2[identity_idx] == 1: # Shares this specific identity
701
+ distance = np.linalg.norm(embedding[i] - embedding[j])
702
+
703
+ # Prefer singleton nodes
704
+ if np.sum(vec2) == 1: # Singleton
705
+ if distance < best_distance:
706
+ best_distance = distance
707
+ best_target = j
708
+ else: # Multi-identity node (backup)
709
+ if distance < backup_distance:
710
+ backup_distance = distance
711
+ backup_target = j
712
+
713
+ # Draw line to best target (prefer singleton, fallback to multi)
714
+ target = best_target if best_target is not None else backup_target
715
+ if target is not None:
716
+ # Calculate relative line weight with reasonable cap
717
+ max_count = max(info['count'] for info in multi_config_map.values())
718
+ relative_weight = count / max_count # Normalize to 0-1
719
+ line_weight = 0.3 + relative_weight * 1.2 # Scale to 0.3-1.5 range
720
+ ax.plot([embedding[i, 0], embedding[target, 0]],
721
+ [embedding[i, 1], embedding[target, 1]],
722
+ [embedding[i, 2], embedding[target, 2]],
723
+ alpha=0.3, color='gray', linewidth=line_weight)
528
724
 
529
725
  if label:
530
726
  # Add cluster ID labels
nettracer3d/nettracer.py CHANGED
@@ -4,6 +4,7 @@ import tifffile
4
4
  from scipy import ndimage
5
5
  from skimage import measure
6
6
  import cv2
7
+ import ast
7
8
  import concurrent.futures
8
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
10
  from scipy.ndimage import zoom
@@ -382,6 +383,27 @@ def invert_dict(d):
382
383
  inverted.setdefault(value, []).append(key)
383
384
  return inverted
384
385
 
386
+ def invert_dict_special(d):
387
+
388
+ d = invert_dict(d)
389
+
390
+ new_dict = copy.deepcopy(d)
391
+
392
+ for key, vals in d.items():
393
+
394
+ try:
395
+ idens = ast.literal_eval(key)
396
+ for iden in idens:
397
+ try:
398
+ new_dict[iden].extend(vals)
399
+ except:
400
+ new_dict[iden] = vals
401
+ del new_dict[key]
402
+ except:
403
+ pass
404
+ return new_dict
405
+
406
+
385
407
  def invert_array(array):
386
408
  """Internal method used to flip node array indices. 0 becomes 255 and vice versa."""
387
409
  inverted_array = np.where(array == 0, 255, 0).astype(np.uint8)
@@ -2117,7 +2139,19 @@ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
2117
2139
 
2118
2140
  return arrayimage
2119
2141
 
2142
+ def iden_set(idens):
2143
+
2144
+ idens = set(idens)
2145
+ real_iden_set = []
2146
+ for iden in idens:
2147
+ try:
2148
+ options = ast.literal_eval(iden)
2149
+ for opt in options:
2150
+ real_iden_set.append(opt)
2151
+ except:
2152
+ real_iden_set.append(iden)
2120
2153
 
2154
+ return set(real_iden_set)
2121
2155
 
2122
2156
 
2123
2157
 
@@ -5451,7 +5485,7 @@ class Network_3D:
5451
5485
 
5452
5486
  community_dict = invert_dict(self.communities)
5453
5487
  summation = 0
5454
- id_set = set(self.node_identities.values())
5488
+ id_set = iden_set(self.node_identities.values())
5455
5489
  output = {sort: 0 for sort in id_set}
5456
5490
  template = copy.deepcopy(output)
5457
5491
 
@@ -5463,7 +5497,12 @@ class Network_3D:
5463
5497
 
5464
5498
  # Count identities in this community
5465
5499
  for node in nodes:
5466
- counter[self.node_identities[node]] += 1
5500
+ try:
5501
+ idens = ast.literal_eval(self.node_identities[node])
5502
+ for iden in idens:
5503
+ counter[iden] += 1
5504
+ except:
5505
+ counter[self.node_identities[node]] += 1
5467
5506
 
5468
5507
  # Convert to proportions within this community and weight by size
5469
5508
  for sort in counter:
@@ -5492,11 +5531,60 @@ class Network_3D:
5492
5531
  neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
5493
5532
 
5494
5533
 
5534
+
5535
+ def identity_umap(self):
5536
+
5537
+ try:
5538
+
5539
+ id_set = iden_set(self.node_identities.values())
5540
+
5541
+ template = np.zeros(len(id_set))
5542
+
5543
+ id_dict = {}
5544
+ for i, iden in enumerate(id_set):
5545
+ id_dict[iden] = i
5546
+
5547
+ umap_dict = {}
5548
+
5549
+ for node in self.node_identities.keys():
5550
+ umap_dict[node] = copy.deepcopy(template)
5551
+ try:
5552
+ idens = ast.literal_eval(self.node_identities[node])
5553
+ for iden in idens:
5554
+ index = id_dict[iden]
5555
+ ref = umap_dict[node]
5556
+ ref[index] = 1
5557
+ umap_dict[node] = ref
5558
+ except:
5559
+ index = id_dict[self.node_identities[node]]
5560
+ ref = umap_dict[node]
5561
+ ref[index] = 1
5562
+ umap_dict[node] = ref
5563
+
5564
+ neighbor_classes = {}
5565
+ import random
5566
+
5567
+ for node, iden in self.node_identities.items():
5568
+ try:
5569
+ idens = ast.literal_eval(iden)
5570
+ neighbor_classes[node] = random.choice(idens)
5571
+ except:
5572
+ neighbor_classes[node] = iden
5573
+
5574
+
5575
+ from . import neighborhoods
5576
+
5577
+ neighborhoods.visualize_cluster_composition_umap(umap_dict, None, id_dictionary = neighbor_classes, graph_label = "Node ID", title = 'UMAP Visualization of Node Identities', draw_lines = True)
5578
+
5579
+ except Exception as e:
5580
+ print(f"Error: {e}")
5581
+
5582
+
5495
5583
  def community_id_info_per_com(self, umap = False, label = 0, limit = 0, proportional = False, neighbors = None):
5496
5584
 
5497
5585
  community_dict = invert_dict(self.communities)
5498
5586
  summation = 0
5499
- id_set = set(self.node_identities.values())
5587
+ id_set = iden_set(self.node_identities.values())
5500
5588
  id_dict = {}
5501
5589
  for i, iden in enumerate(id_set):
5502
5590
  id_dict[iden] = i
@@ -5515,7 +5603,12 @@ class Network_3D:
5515
5603
 
5516
5604
  # Count identities in this community
5517
5605
  for node in nodes:
5518
- counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5606
+ try:
5607
+ idens = ast.literal_eval(self.node_identities[node])
5608
+ for iden in idens:
5609
+ counter[id_dict[iden]] += 1
5610
+ except:
5611
+ counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5519
5612
 
5520
5613
  for i in range(len(counter)): # Translate them into proportions out of 1
5521
5614
 
@@ -5527,12 +5620,11 @@ class Network_3D:
5527
5620
  umap_dict[community] = counter
5528
5621
 
5529
5622
  else:
5530
- idens = invert_dict(self.node_identities)
5623
+ idens = invert_dict_special(self.node_identities)
5531
5624
  iden_count = {}
5532
5625
  template = {}
5533
5626
  node_count = len(list(self.communities.keys()))
5534
5627
 
5535
-
5536
5628
  for iden in id_set:
5537
5629
  template[iden] = 0
5538
5630
 
@@ -5548,7 +5640,12 @@ class Network_3D:
5548
5640
  counter = np.zeros(len(id_set))
5549
5641
 
5550
5642
  for node in nodes:
5551
- iden_tracker[self.node_identities[node]] += 1
5643
+ try:
5644
+ idents = ast.literal_eval(self.node_identities[node])
5645
+ for iden in idents:
5646
+ iden_tracker[iden] += 1
5647
+ except:
5648
+ iden_tracker[self.node_identities[node]] += 1
5552
5649
 
5553
5650
  i = 0
5554
5651
 
@@ -5819,7 +5916,7 @@ class Network_3D:
5819
5916
  return heat_dict, overlay
5820
5917
 
5821
5918
 
5822
- def merge_node_ids(self, path, data):
5919
+ def merge_node_ids(self, path, data, include = True):
5823
5920
 
5824
5921
  if self.node_identities is None: # Prepare modular dict
5825
5922
 
@@ -5828,9 +5925,17 @@ class Network_3D:
5828
5925
  nodes = list(np.unique(data))
5829
5926
  if 0 in nodes:
5830
5927
  del nodes[0]
5831
-
5832
5928
  for node in nodes:
5833
- self.node_identities[node] = ''
5929
+
5930
+ self.node_identities[node] = [] # Assign to lists at first
5931
+ else:
5932
+ for node, iden in self.node_identities.items():
5933
+ try:
5934
+ self.node_identities[node] = ast.literal_eval(iden)
5935
+ except:
5936
+ self.node_identities[node] = [iden]
5937
+
5938
+
5834
5939
 
5835
5940
  img_list = directory_info(path)
5836
5941
 
@@ -5840,9 +5945,12 @@ class Network_3D:
5840
5945
  if len(np.unique(mask)) != 2:
5841
5946
 
5842
5947
  mask = otsu_binarize(mask)
5948
+ else:
5949
+ mask = mask != 0
5843
5950
 
5844
5951
  nodes = data * mask
5845
- nodes = list(np.unique(nodes))
5952
+ nodes = np.unique(nodes)
5953
+ nodes = nodes.tolist()
5846
5954
  if 0 in nodes:
5847
5955
  del nodes[0]
5848
5956
 
@@ -5853,21 +5961,43 @@ class Network_3D:
5853
5961
  else:
5854
5962
  base_name = img
5855
5963
 
5964
+ assigned = {}
5965
+
5966
+
5856
5967
  for node in self.node_identities.keys():
5857
5968
 
5858
5969
  try:
5859
5970
 
5860
- if node in nodes:
5971
+ if int(node) in nodes:
5861
5972
 
5862
- self.node_identities[node] += f" {base_name}+"
5973
+ self.node_identities[node].append(f'{base_name}+')
5863
5974
 
5864
- else:
5975
+ elif include:
5865
5976
 
5866
- self.node_identities[node] += f" {base_name}-"
5977
+ self.node_identities[node].append(f'{base_name}-')
5867
5978
 
5868
5979
  except:
5869
5980
  pass
5870
5981
 
5982
+ modify_dict = copy.deepcopy(self.node_identities)
5983
+
5984
+ for node, iden in self.node_identities.items():
5985
+
5986
+ try:
5987
+
5988
+ if len(iden) == 1:
5989
+
5990
+ modify_dict[node] = str(iden[0]) # Singleton lists become bare strings
5991
+ elif len(iden) == 0:
5992
+ del modify_dict[node]
5993
+ else:
5994
+ modify_dict[node] = str(iden) # We hold multi element lists as strings for compatibility
5995
+
5996
+ except:
5997
+ pass
5998
+
5999
+ self.node_identities = modify_dict
6000
+
5871
6001
 
5872
6002
  def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False, centroids = True):
5873
6003
 
@@ -4011,7 +4011,9 @@ class ImageViewerWindow(QMainWindow):
4011
4011
  id_code_action = overlay_menu.addAction("Code Identities")
4012
4012
  id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
4013
4013
  umap_action = overlay_menu.addAction("Centroid UMAP")
4014
- umap_action.triggered.connect(self.handle_umap)
4014
+ umap_action.triggered.connect(self.handle_centroid_umap)
4015
+ iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
4016
+ iden_umap_action.triggered.connect(self.handle_iden_umap)
4015
4017
 
4016
4018
  rand_menu = analysis_menu.addMenu("Randomize")
4017
4019
  random_action = rand_menu.addAction("Generate Equivalent Random Network")
@@ -5858,8 +5860,8 @@ class ImageViewerWindow(QMainWindow):
5858
5860
  y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
5859
5861
 
5860
5862
  if self.pan_mode:
5861
- box_len = (x_max - x_min)
5862
- box_height = (y_max - y_min)
5863
+ box_len = int((x_max - x_min)/2)
5864
+ box_height = int((y_max - y_min)/2)
5863
5865
  x_min = max(0, x_min - box_len)
5864
5866
  x_max = min(self.shape[2], x_max + box_len)
5865
5867
  y_min = max(0, y_min - box_height)
@@ -6195,13 +6197,20 @@ class ImageViewerWindow(QMainWindow):
6195
6197
  dialog = CodeDialog(self, sort = sort)
6196
6198
  dialog.exec()
6197
6199
 
6198
- def handle_umap(self):
6200
+ def handle_centroid_umap(self):
6199
6201
 
6200
6202
  if my_network.node_centroids is None:
6201
6203
  self.show_centroid_dialog()
6202
6204
 
6203
6205
  my_network.centroid_umap()
6204
6206
 
6207
+ def handle_iden_umap(self):
6208
+
6209
+ if my_network.node_identities is None:
6210
+ return
6211
+
6212
+ my_network.identity_umap()
6213
+
6205
6214
  def closeEvent(self, event):
6206
6215
  """Override closeEvent to close all windows when main window closes"""
6207
6216
 
@@ -7468,6 +7477,12 @@ class MergeNodeIdDialog(QDialog):
7468
7477
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
7469
7478
  layout.addRow("z_scale:", self.z_scale)
7470
7479
 
7480
+ # Add Run button
7481
+ self.include = QPushButton("Include Negative Gates?")
7482
+ self.include.setCheckable(True)
7483
+ self.include.setChecked(True)
7484
+ layout.addWidget(self.include)
7485
+
7471
7486
  # Add Run button
7472
7487
  run_button = QPushButton("Get Directory")
7473
7488
  run_button.clicked.connect(self.run)
@@ -7483,6 +7498,7 @@ class MergeNodeIdDialog(QDialog):
7483
7498
 
7484
7499
 
7485
7500
  data = self.parent().channel_data[0]
7501
+ include = self.include.isChecked()
7486
7502
 
7487
7503
  if data is None:
7488
7504
  return
@@ -7501,7 +7517,7 @@ class MergeNodeIdDialog(QDialog):
7501
7517
  if search > 0:
7502
7518
  data = sdl.smart_dilate(data, 1, 1, GPU = False, fast_dil = False, use_dt_dil_amount = search, xy_scale = xy_scale, z_scale = z_scale)
7503
7519
 
7504
- my_network.merge_node_ids(selected_path, data)
7520
+ my_network.merge_node_ids(selected_path, data, include)
7505
7521
 
7506
7522
  self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
7507
7523
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.9.3
3
+ Version: 0.9.4
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,6 @@ 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.9.3 Updates --
113
+ -- Version 0.9.4 Updates --
114
114
 
115
- * Some minor bug fixes.
115
+ * Added some compatibility for nodes being assigned 'multiple identities'
@@ -1,12 +1,12 @@
1
1
  nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  nettracer3d/cellpose_manager.py,sha256=NfRqW6Zl7yRU4qHCS_KjmR0R6QANSSgCO0_dr-eivxg,6694
3
3
  nettracer3d/community_extractor.py,sha256=QG2v2y9lKb07LRU0zXCAlA_z-96BNqLHh5xP17KLmYA,28147
4
- nettracer3d/excelotron.py,sha256=X9v_mte8gJBPNGdj6NJNUYja0Z6eorVoKAFx4nHiMnU,72058
4
+ nettracer3d/excelotron.py,sha256=aNof6k-DgMxVyFgsl3ltSCxG4vZW49cuvCBzfzhYhUY,75072
5
5
  nettracer3d/modularity.py,sha256=pborVcDBvICB2-g8lNoSVZbIReIBlfeBmjFbPYmtq7Y,22443
6
6
  nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
7
- nettracer3d/neighborhoods.py,sha256=iIaHU1COIdRtzRpAuIQKfLGLNKYFK3dL8Vb_EeJIlEA,46459
8
- nettracer3d/nettracer.py,sha256=KTVodpVpu2mfzdRR-ZucZlTQCZc1pqgH4jAI26vWAgY,264551
9
- nettracer3d/nettracer_gui.py,sha256=eZuO9v9szm-pj_-Bl71Lyg0mw80u_HJragARR2muVLo,601133
7
+ nettracer3d/neighborhoods.py,sha256=2QzGKkQCSUerxigkcHF4iN7ys8kv-KHTbkD_9_2Krgo,58365
8
+ nettracer3d/nettracer.py,sha256=wraH1Ba61bA0awmBfH71u_hkKcytUG_pISkJX6iP-ic,268658
9
+ nettracer3d/nettracer_gui.py,sha256=tYg0wDV2p6b0czvozMrrpja7fbx3nSBgjR2ZV4b5vBE,601760
10
10
  nettracer3d/network_analysis.py,sha256=kBzsVaq4dZkMe0k-VGvQIUvM-tK0ZZ8bvb-wtsugZRQ,46150
11
11
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
12
12
  nettracer3d/node_draw.py,sha256=kZcR1PekLg0riioNeGcALIXQyZ5PtHA_9MT6z7Zovdk,10401
@@ -17,9 +17,9 @@ nettracer3d/segmenter.py,sha256=-Llkhp3TlAIBXZNhcfMFQRdg0vec1xtlOm0c4_bSU9U,7576
17
17
  nettracer3d/segmenter_GPU.py,sha256=optCZ_zLIfe99rgqmyKWUZlWW5TF5jEC_C3keu1m7VQ,77771
18
18
  nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
19
19
  nettracer3d/smart_dilate.py,sha256=TvRUh6B4q4zIdCO1BWH-xgTdND5OUNmo99eyxG9oIAU,27145
20
- nettracer3d-0.9.3.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
- nettracer3d-0.9.3.dist-info/METADATA,sha256=lfHt7f_M_votMYdihCuZWBmL4jUCV6YXrN7PFu7iEkQ,6998
22
- nettracer3d-0.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- nettracer3d-0.9.3.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
- nettracer3d-0.9.3.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
- nettracer3d-0.9.3.dist-info/RECORD,,
20
+ nettracer3d-0.9.4.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
+ nettracer3d-0.9.4.dist-info/METADATA,sha256=-xNx22dCKedpkF7uLVduYpMYBCJcXv_2xssw34nEo4c,7048
22
+ nettracer3d-0.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ nettracer3d-0.9.4.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
+ nettracer3d-0.9.4.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
+ nettracer3d-0.9.4.dist-info/RECORD,,