nettracer3d 0.8.0__py3-none-any.whl → 0.8.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.
- nettracer3d/neighborhoods.py +395 -58
- nettracer3d/nettracer.py +120 -10
- nettracer3d/nettracer_gui.py +276 -15
- nettracer3d/node_draw.py +22 -12
- nettracer3d/proximity.py +63 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/METADATA +5 -8
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/RECORD +11 -11
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.8.0.dist-info → nettracer3d-0.8.1.dist-info}/top_level.txt +0 -0
nettracer3d/neighborhoods.py
CHANGED
|
@@ -202,10 +202,12 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
202
202
|
|
|
203
203
|
return embedding
|
|
204
204
|
|
|
205
|
-
def create_community_heatmap(community_intensity, node_community, node_centroids, is_3d=True,
|
|
206
|
-
figsize=(12, 8), point_size=50, alpha=0.7,
|
|
205
|
+
def create_community_heatmap(community_intensity, node_community, node_centroids, shape=None, is_3d=True,
|
|
206
|
+
labeled_array=None, figsize=(12, 8), point_size=50, alpha=0.7,
|
|
207
|
+
colorbar_label="Community Intensity", title="Community Intensity Heatmap"):
|
|
207
208
|
"""
|
|
208
209
|
Create a 2D or 3D heatmap showing nodes colored by their community intensities.
|
|
210
|
+
Can return either matplotlib plot or numpy RGB array for overlay purposes.
|
|
209
211
|
|
|
210
212
|
Parameters:
|
|
211
213
|
-----------
|
|
@@ -220,25 +222,39 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
|
|
|
220
222
|
Dictionary mapping node IDs to centroids
|
|
221
223
|
Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
|
|
222
224
|
|
|
225
|
+
shape : tuple, optional
|
|
226
|
+
Shape of the output array in [Z, Y, X] format
|
|
227
|
+
If None, will be inferred from node_centroids
|
|
228
|
+
|
|
223
229
|
is_3d : bool, default=True
|
|
224
|
-
If True, create 3D plot. If False, create 2D plot.
|
|
230
|
+
If True, create 3D plot/array. If False, create 2D plot/array.
|
|
231
|
+
|
|
232
|
+
labeled_array : np.ndarray, optional
|
|
233
|
+
If provided, returns numpy RGB array overlay using this labeled array template
|
|
234
|
+
instead of matplotlib plot. Uses lookup table approach for efficiency.
|
|
225
235
|
|
|
226
236
|
figsize : tuple, default=(12, 8)
|
|
227
|
-
Figure size (width, height)
|
|
237
|
+
Figure size (width, height) - only used for matplotlib
|
|
228
238
|
|
|
229
239
|
point_size : int, default=50
|
|
230
|
-
Size of scatter plot points
|
|
240
|
+
Size of scatter plot points - only used for matplotlib
|
|
231
241
|
|
|
232
242
|
alpha : float, default=0.7
|
|
233
|
-
Transparency of points (0-1)
|
|
243
|
+
Transparency of points (0-1) - only used for matplotlib
|
|
234
244
|
|
|
235
245
|
colorbar_label : str, default="Community Intensity"
|
|
236
|
-
Label for the colorbar
|
|
246
|
+
Label for the colorbar - only used for matplotlib
|
|
247
|
+
|
|
248
|
+
title : str, default="Community Intensity Heatmap"
|
|
249
|
+
Title for the plot
|
|
237
250
|
|
|
238
251
|
Returns:
|
|
239
252
|
--------
|
|
240
|
-
fig, ax
|
|
253
|
+
If labeled_array is None: fig, ax (matplotlib figure and axis objects)
|
|
254
|
+
If labeled_array is provided: np.ndarray (RGB heatmap array with community intensity colors)
|
|
241
255
|
"""
|
|
256
|
+
import numpy as np
|
|
257
|
+
import matplotlib.pyplot as plt
|
|
242
258
|
|
|
243
259
|
# Convert numpy int64 keys to regular ints for consistency
|
|
244
260
|
community_intensity_clean = {}
|
|
@@ -254,6 +270,10 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
|
|
|
254
270
|
|
|
255
271
|
for node_id, centroid in node_centroids.items():
|
|
256
272
|
try:
|
|
273
|
+
# Convert node_id to regular int if it's numpy
|
|
274
|
+
if hasattr(node_id, 'item'):
|
|
275
|
+
node_id = node_id.item()
|
|
276
|
+
|
|
257
277
|
# Get community for this node
|
|
258
278
|
community_id = node_community[node_id]
|
|
259
279
|
|
|
@@ -266,74 +286,392 @@ def create_community_heatmap(community_intensity, node_community, node_centroids
|
|
|
266
286
|
|
|
267
287
|
node_positions.append(centroid)
|
|
268
288
|
node_intensities.append(intensity)
|
|
269
|
-
except:
|
|
289
|
+
except KeyError:
|
|
290
|
+
# Skip nodes that don't have community assignments or community intensities
|
|
270
291
|
pass
|
|
271
292
|
|
|
272
293
|
# Convert to numpy arrays
|
|
273
294
|
positions = np.array(node_positions)
|
|
274
295
|
intensities = np.array(node_intensities)
|
|
275
296
|
|
|
276
|
-
# Determine
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
# Determine shape if not provided
|
|
298
|
+
if shape is None:
|
|
299
|
+
if len(positions) > 0:
|
|
300
|
+
max_coords = np.max(positions, axis=0).astype(int)
|
|
301
|
+
shape = tuple(max_coords + 1)
|
|
302
|
+
else:
|
|
303
|
+
shape = (100, 100, 100) if is_3d else (1, 100, 100)
|
|
279
304
|
|
|
280
|
-
#
|
|
281
|
-
|
|
305
|
+
# Determine min and max intensities for scaling
|
|
306
|
+
if len(intensities) > 0:
|
|
307
|
+
min_intensity = np.min(intensities)
|
|
308
|
+
max_intensity = np.max(intensities)
|
|
309
|
+
else:
|
|
310
|
+
min_intensity, max_intensity = 0, 1
|
|
282
311
|
|
|
283
|
-
if
|
|
284
|
-
#
|
|
285
|
-
|
|
312
|
+
if labeled_array is not None:
|
|
313
|
+
# Create numpy RGB array output using labeled array and lookup table approach
|
|
314
|
+
|
|
315
|
+
# Create mapping from node ID to community intensity value
|
|
316
|
+
node_to_community_intensity = {}
|
|
317
|
+
for node_id, centroid in node_centroids.items():
|
|
318
|
+
# Convert node_id to regular int if it's numpy
|
|
319
|
+
if hasattr(node_id, 'item'):
|
|
320
|
+
node_id = node_id.item()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Get community for this node
|
|
324
|
+
community_id = node_community[node_id]
|
|
325
|
+
|
|
326
|
+
# Convert community_id to regular int if it's numpy
|
|
327
|
+
if hasattr(community_id, 'item'):
|
|
328
|
+
community_id = community_id.item()
|
|
329
|
+
|
|
330
|
+
# Get intensity for this community
|
|
331
|
+
if community_id in community_intensity_clean:
|
|
332
|
+
node_to_community_intensity[node_id] = community_intensity_clean[community_id]
|
|
333
|
+
except KeyError:
|
|
334
|
+
# Skip nodes that don't have community assignments
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
# Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
|
|
338
|
+
def intensity_to_rgb(intensity, min_val, max_val):
|
|
339
|
+
"""Convert intensity value to RGB using RdBu_r colormap logic"""
|
|
340
|
+
if max_val == min_val:
|
|
341
|
+
# All same value, use neutral color
|
|
342
|
+
return np.array([255, 255, 255], dtype=np.uint8) # White
|
|
343
|
+
|
|
344
|
+
# Normalize to -1 to 1 range (like RdBu_r colormap)
|
|
345
|
+
normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
|
|
346
|
+
normalized = np.clip(normalized, -1, 1)
|
|
347
|
+
|
|
348
|
+
if normalized > 0:
|
|
349
|
+
# Positive values: white to red
|
|
350
|
+
r = 255
|
|
351
|
+
g = int(255 * (1 - normalized))
|
|
352
|
+
b = int(255 * (1 - normalized))
|
|
353
|
+
else:
|
|
354
|
+
# Negative values: white to blue
|
|
355
|
+
r = int(255 * (1 + normalized))
|
|
356
|
+
g = int(255 * (1 + normalized))
|
|
357
|
+
b = 255
|
|
358
|
+
|
|
359
|
+
return np.array([r, g, b], dtype=np.uint8)
|
|
286
360
|
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
x_coords = positions[:, 2]
|
|
361
|
+
# Create lookup table for RGB colors
|
|
362
|
+
max_label = max(max(labeled_array.flat), max(node_to_community_intensity.keys()) if node_to_community_intensity else 0)
|
|
363
|
+
color_lut = np.zeros((max_label + 1, 3), dtype=np.uint8) # Default to black (0,0,0)
|
|
291
364
|
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
365
|
+
# Fill lookup table with RGB colors based on community intensity
|
|
366
|
+
for node_id, intensity in node_to_community_intensity.items():
|
|
367
|
+
rgb_color = intensity_to_rgb(intensity, min_intensity, max_intensity)
|
|
368
|
+
color_lut[int(node_id)] = rgb_color
|
|
296
369
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
370
|
+
# Apply lookup table to labeled array - single vectorized operation
|
|
371
|
+
if is_3d:
|
|
372
|
+
# Return full 3D RGB array [Z, Y, X, 3]
|
|
373
|
+
heatmap_array = color_lut[labeled_array]
|
|
374
|
+
else:
|
|
375
|
+
# Return 2D RGB array
|
|
376
|
+
if labeled_array.ndim == 3:
|
|
377
|
+
# Take middle slice for 2D representation
|
|
378
|
+
middle_slice = labeled_array.shape[0] // 2
|
|
379
|
+
heatmap_array = color_lut[labeled_array[middle_slice]]
|
|
380
|
+
else:
|
|
381
|
+
# Already 2D
|
|
382
|
+
heatmap_array = color_lut[labeled_array]
|
|
301
383
|
|
|
384
|
+
return heatmap_array
|
|
385
|
+
|
|
302
386
|
else:
|
|
303
|
-
#
|
|
304
|
-
|
|
387
|
+
# Create matplotlib plot
|
|
388
|
+
fig = plt.figure(figsize=figsize)
|
|
305
389
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
390
|
+
if is_3d:
|
|
391
|
+
# 3D plot
|
|
392
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
393
|
+
|
|
394
|
+
# Extract coordinates (assuming [Z, Y, X] format)
|
|
395
|
+
z_coords = positions[:, 0]
|
|
396
|
+
y_coords = positions[:, 1]
|
|
397
|
+
x_coords = positions[:, 2]
|
|
398
|
+
|
|
399
|
+
# Create scatter plot
|
|
400
|
+
scatter = ax.scatter(x_coords, y_coords, z_coords,
|
|
401
|
+
c=intensities, s=point_size, alpha=alpha,
|
|
402
|
+
cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
|
|
403
|
+
|
|
404
|
+
ax.set_xlabel('X')
|
|
405
|
+
ax.set_ylabel('Y')
|
|
406
|
+
ax.set_zlabel('Z')
|
|
407
|
+
ax.set_title(f'{title}')
|
|
408
|
+
|
|
409
|
+
# Set axis limits based on shape
|
|
410
|
+
ax.set_xlim(0, shape[2])
|
|
411
|
+
ax.set_ylim(0, shape[1])
|
|
412
|
+
ax.set_zlim(0, shape[0])
|
|
413
|
+
|
|
414
|
+
else:
|
|
415
|
+
# 2D plot (using Y, X coordinates, ignoring Z/first dimension)
|
|
416
|
+
ax = fig.add_subplot(111)
|
|
417
|
+
|
|
418
|
+
# Extract Y, X coordinates
|
|
419
|
+
y_coords = positions[:, 1]
|
|
420
|
+
x_coords = positions[:, 2]
|
|
421
|
+
|
|
422
|
+
# Create scatter plot
|
|
423
|
+
scatter = ax.scatter(x_coords, y_coords,
|
|
424
|
+
c=intensities, s=point_size, alpha=alpha,
|
|
425
|
+
cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
|
|
426
|
+
|
|
427
|
+
ax.set_xlabel('X')
|
|
428
|
+
ax.set_ylabel('Y')
|
|
429
|
+
ax.set_title(f'{title}')
|
|
430
|
+
ax.grid(True, alpha=0.3)
|
|
431
|
+
|
|
432
|
+
# Set axis limits based on shape
|
|
433
|
+
ax.set_xlim(0, shape[2])
|
|
434
|
+
ax.set_ylim(0, shape[1])
|
|
435
|
+
|
|
436
|
+
# Set origin to top-left (invert Y-axis)
|
|
437
|
+
ax.invert_yaxis()
|
|
309
438
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
|
|
439
|
+
# Add colorbar
|
|
440
|
+
cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
|
|
441
|
+
cbar.set_label(colorbar_label)
|
|
314
442
|
|
|
315
|
-
|
|
316
|
-
ax.
|
|
317
|
-
|
|
318
|
-
ax.
|
|
443
|
+
# Add text annotations for min/max values
|
|
444
|
+
cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
|
|
445
|
+
transform=cbar.ax.transAxes, va='bottom')
|
|
446
|
+
cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
|
|
447
|
+
transform=cbar.ax.transAxes, va='top')
|
|
319
448
|
|
|
320
|
-
|
|
321
|
-
|
|
449
|
+
plt.tight_layout()
|
|
450
|
+
plt.show()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def create_node_heatmap(node_intensity, node_centroids, shape=None, is_3d=True,
|
|
454
|
+
labeled_array=None, figsize=(12, 8), point_size=50, alpha=0.7,
|
|
455
|
+
colorbar_label="Node Intensity", title="Node Clustering Intensity Heatmap"):
|
|
456
|
+
"""
|
|
457
|
+
Create a 2D or 3D heatmap showing nodes colored by their individual intensities.
|
|
458
|
+
Can return either matplotlib plot or numpy array for overlay purposes.
|
|
322
459
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
460
|
+
Parameters:
|
|
461
|
+
-----------
|
|
462
|
+
node_intensity : dict
|
|
463
|
+
Dictionary mapping node IDs to intensity values
|
|
464
|
+
Keys can be np.int64 or regular ints
|
|
465
|
+
|
|
466
|
+
node_centroids : dict
|
|
467
|
+
Dictionary mapping node IDs to centroids
|
|
468
|
+
Centroids should be [Z, Y, X] for 3D or [1, Y, X] for pseudo-3D
|
|
469
|
+
|
|
470
|
+
shape : tuple, optional
|
|
471
|
+
Shape of the output array in [Z, Y, X] format
|
|
472
|
+
If None, will be inferred from node_centroids
|
|
473
|
+
|
|
474
|
+
is_3d : bool, default=True
|
|
475
|
+
If True, create 3D plot/array. If False, create 2D plot/array.
|
|
476
|
+
|
|
477
|
+
labeled_array : np.ndarray, optional
|
|
478
|
+
If provided, returns numpy array overlay using this labeled array template
|
|
479
|
+
instead of matplotlib plot. Uses lookup table approach for efficiency.
|
|
480
|
+
|
|
481
|
+
figsize : tuple, default=(12, 8)
|
|
482
|
+
Figure size (width, height) - only used for matplotlib
|
|
483
|
+
|
|
484
|
+
point_size : int, default=50
|
|
485
|
+
Size of scatter plot points - only used for matplotlib
|
|
486
|
+
|
|
487
|
+
alpha : float, default=0.7
|
|
488
|
+
Transparency of points (0-1) - only used for matplotlib
|
|
489
|
+
|
|
490
|
+
colorbar_label : str, default="Node Intensity"
|
|
491
|
+
Label for the colorbar - only used for matplotlib
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
--------
|
|
495
|
+
If labeled_array is None: fig, ax (matplotlib figure and axis objects)
|
|
496
|
+
If labeled_array is provided: np.ndarray (heatmap array with intensity values)
|
|
497
|
+
"""
|
|
498
|
+
import numpy as np
|
|
499
|
+
import matplotlib.pyplot as plt
|
|
500
|
+
|
|
501
|
+
# Convert numpy int64 keys to regular ints for consistency
|
|
502
|
+
node_intensity_clean = {}
|
|
503
|
+
for k, v in node_intensity.items():
|
|
504
|
+
if hasattr(k, 'item'): # numpy scalar
|
|
505
|
+
node_intensity_clean[k.item()] = v
|
|
506
|
+
else:
|
|
507
|
+
node_intensity_clean[k] = v
|
|
326
508
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
|
|
331
|
-
transform=cbar.ax.transAxes, va='top')
|
|
509
|
+
# Prepare data for plotting/array creation
|
|
510
|
+
node_positions = []
|
|
511
|
+
node_intensities = []
|
|
332
512
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
513
|
+
for node_id, centroid in node_centroids.items():
|
|
514
|
+
try:
|
|
515
|
+
# Convert node_id to regular int if it's numpy
|
|
516
|
+
if hasattr(node_id, 'item'):
|
|
517
|
+
node_id = node_id.item()
|
|
518
|
+
|
|
519
|
+
# Get intensity for this node
|
|
520
|
+
intensity = node_intensity_clean[node_id]
|
|
521
|
+
|
|
522
|
+
node_positions.append(centroid)
|
|
523
|
+
node_intensities.append(intensity)
|
|
524
|
+
except KeyError:
|
|
525
|
+
# Skip nodes that don't have intensity values
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
# Convert to numpy arrays
|
|
529
|
+
positions = np.array(node_positions)
|
|
530
|
+
intensities = np.array(node_intensities)
|
|
531
|
+
|
|
532
|
+
# Determine shape if not provided
|
|
533
|
+
if shape is None:
|
|
534
|
+
if len(positions) > 0:
|
|
535
|
+
max_coords = np.max(positions, axis=0).astype(int)
|
|
536
|
+
shape = tuple(max_coords + 1)
|
|
537
|
+
else:
|
|
538
|
+
shape = (100, 100, 100) if is_3d else (1, 100, 100)
|
|
539
|
+
|
|
540
|
+
# Determine min and max intensities for scaling
|
|
541
|
+
if len(intensities) > 0:
|
|
542
|
+
min_intensity = np.min(intensities)
|
|
543
|
+
max_intensity = np.max(intensities)
|
|
544
|
+
else:
|
|
545
|
+
min_intensity, max_intensity = 0, 1
|
|
546
|
+
|
|
547
|
+
if labeled_array is not None:
|
|
548
|
+
# Create numpy RGB array output using labeled array and lookup table approach
|
|
549
|
+
|
|
550
|
+
# Create mapping from node ID to intensity value (keep original float values)
|
|
551
|
+
node_to_intensity = {}
|
|
552
|
+
for node_id, centroid in node_centroids.items():
|
|
553
|
+
# Convert node_id to regular int if it's numpy
|
|
554
|
+
if hasattr(node_id, 'item'):
|
|
555
|
+
node_id = node_id.item()
|
|
556
|
+
|
|
557
|
+
# Only include nodes that have intensity values
|
|
558
|
+
if node_id in node_intensity_clean:
|
|
559
|
+
node_to_intensity[node_id] = node_intensity_clean[node_id]
|
|
560
|
+
|
|
561
|
+
# Create colormap function (RdBu_r - red for high, blue for low, yellow/white for middle)
|
|
562
|
+
def intensity_to_rgb(intensity, min_val, max_val):
|
|
563
|
+
"""Convert intensity value to RGB using RdBu_r colormap logic"""
|
|
564
|
+
if max_val == min_val:
|
|
565
|
+
# All same value, use neutral color
|
|
566
|
+
return np.array([255, 255, 255], dtype=np.uint8) # White
|
|
567
|
+
|
|
568
|
+
# Normalize to -1 to 1 range (like RdBu_r colormap)
|
|
569
|
+
normalized = 2 * (intensity - min_val) / (max_val - min_val) - 1
|
|
570
|
+
normalized = np.clip(normalized, -1, 1)
|
|
571
|
+
|
|
572
|
+
if normalized > 0:
|
|
573
|
+
# Positive values: white to red
|
|
574
|
+
r = 255
|
|
575
|
+
g = int(255 * (1 - normalized))
|
|
576
|
+
b = int(255 * (1 - normalized))
|
|
577
|
+
else:
|
|
578
|
+
# Negative values: white to blue
|
|
579
|
+
r = int(255 * (1 + normalized))
|
|
580
|
+
g = int(255 * (1 + normalized))
|
|
581
|
+
b = 255
|
|
582
|
+
|
|
583
|
+
return np.array([r, g, b], dtype=np.uint8)
|
|
584
|
+
|
|
585
|
+
# Create lookup table for RGB colors
|
|
586
|
+
max_label = max(max(labeled_array.flat), max(node_to_intensity.keys()) if node_to_intensity else 0)
|
|
587
|
+
color_lut = np.zeros((max_label + 1, 3), dtype=np.uint8) # Default to black (0,0,0)
|
|
588
|
+
|
|
589
|
+
# Fill lookup table with RGB colors based on intensity
|
|
590
|
+
for node_id, intensity in node_to_intensity.items():
|
|
591
|
+
rgb_color = intensity_to_rgb(intensity, min_intensity, max_intensity)
|
|
592
|
+
color_lut[int(node_id)] = rgb_color
|
|
593
|
+
|
|
594
|
+
# Apply lookup table to labeled array - single vectorized operation
|
|
595
|
+
if is_3d:
|
|
596
|
+
# Return full 3D RGB array [Z, Y, X, 3]
|
|
597
|
+
heatmap_array = color_lut[labeled_array]
|
|
598
|
+
else:
|
|
599
|
+
# Return 2D RGB array
|
|
600
|
+
if labeled_array.ndim == 3:
|
|
601
|
+
# Take middle slice for 2D representation
|
|
602
|
+
middle_slice = labeled_array.shape[0] // 2
|
|
603
|
+
heatmap_array = color_lut[labeled_array[middle_slice]]
|
|
604
|
+
else:
|
|
605
|
+
# Already 2D
|
|
606
|
+
heatmap_array = color_lut[labeled_array]
|
|
607
|
+
|
|
608
|
+
return heatmap_array
|
|
609
|
+
|
|
610
|
+
else:
|
|
611
|
+
# Create matplotlib plot
|
|
612
|
+
fig = plt.figure(figsize=figsize)
|
|
613
|
+
|
|
614
|
+
if is_3d:
|
|
615
|
+
# 3D plot
|
|
616
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
617
|
+
|
|
618
|
+
# Extract coordinates (assuming [Z, Y, X] format)
|
|
619
|
+
z_coords = positions[:, 0]
|
|
620
|
+
y_coords = positions[:, 1]
|
|
621
|
+
x_coords = positions[:, 2]
|
|
622
|
+
|
|
623
|
+
# Create scatter plot
|
|
624
|
+
scatter = ax.scatter(x_coords, y_coords, z_coords,
|
|
625
|
+
c=intensities, s=point_size, alpha=alpha,
|
|
626
|
+
cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
|
|
627
|
+
|
|
628
|
+
ax.set_xlabel('X')
|
|
629
|
+
ax.set_ylabel('Y')
|
|
630
|
+
ax.set_zlabel('Z')
|
|
631
|
+
ax.set_title(f'{title}')
|
|
632
|
+
|
|
633
|
+
# Set axis limits based on shape
|
|
634
|
+
ax.set_xlim(0, shape[2])
|
|
635
|
+
ax.set_ylim(0, shape[1])
|
|
636
|
+
ax.set_zlim(0, shape[0])
|
|
637
|
+
|
|
638
|
+
else:
|
|
639
|
+
# 2D plot (using Y, X coordinates, ignoring Z/first dimension)
|
|
640
|
+
ax = fig.add_subplot(111)
|
|
641
|
+
|
|
642
|
+
# Extract Y, X coordinates
|
|
643
|
+
y_coords = positions[:, 1]
|
|
644
|
+
x_coords = positions[:, 2]
|
|
645
|
+
|
|
646
|
+
# Create scatter plot
|
|
647
|
+
scatter = ax.scatter(x_coords, y_coords,
|
|
648
|
+
c=intensities, s=point_size, alpha=alpha,
|
|
649
|
+
cmap='RdBu_r', vmin=min_intensity, vmax=max_intensity)
|
|
650
|
+
|
|
651
|
+
ax.set_xlabel('X')
|
|
652
|
+
ax.set_ylabel('Y')
|
|
653
|
+
ax.set_title(f'{title}')
|
|
654
|
+
ax.grid(True, alpha=0.3)
|
|
655
|
+
|
|
656
|
+
# Set axis limits based on shape
|
|
657
|
+
ax.set_xlim(0, shape[2])
|
|
658
|
+
ax.set_ylim(0, shape[1])
|
|
659
|
+
|
|
660
|
+
# Set origin to top-left (invert Y-axis)
|
|
661
|
+
ax.invert_yaxis()
|
|
662
|
+
|
|
663
|
+
# Add colorbar
|
|
664
|
+
cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
|
|
665
|
+
cbar.set_label(colorbar_label)
|
|
666
|
+
|
|
667
|
+
# Add text annotations for min/max values
|
|
668
|
+
cbar.ax.text(1.05, 0, f'Min: {min_intensity:.3f}\n(Blue)',
|
|
669
|
+
transform=cbar.ax.transAxes, va='bottom')
|
|
670
|
+
cbar.ax.text(1.05, 1, f'Max: {max_intensity:.3f}\n(Red)',
|
|
671
|
+
transform=cbar.ax.transAxes, va='top')
|
|
672
|
+
|
|
673
|
+
plt.tight_layout()
|
|
674
|
+
plt.show()
|
|
337
675
|
|
|
338
676
|
# Example usage:
|
|
339
677
|
if __name__ == "__main__":
|
|
@@ -350,5 +688,4 @@ if __name__ == "__main__":
|
|
|
350
688
|
fig, ax = plot_dict_heatmap(sample_dict, sample_id_set,
|
|
351
689
|
title="Sample Heatmap Visualization")
|
|
352
690
|
|
|
353
|
-
plt.show()
|
|
354
|
-
|
|
691
|
+
plt.show()
|
nettracer3d/nettracer.py
CHANGED
|
@@ -4291,7 +4291,7 @@ class Network_3D:
|
|
|
4291
4291
|
|
|
4292
4292
|
|
|
4293
4293
|
|
|
4294
|
-
def get_degrees(self, down_factor = 1, directory = None, called = False, no_img = 0):
|
|
4294
|
+
def get_degrees(self, down_factor = 1, directory = None, called = False, no_img = 0, heatmap = False):
|
|
4295
4295
|
"""
|
|
4296
4296
|
Method to obtain information on the degrees of nodes in the network, also generating overlays that relate this information to the 3D structure.
|
|
4297
4297
|
Overlays include a grayscale image where nodes are assigned a grayscale value corresponding to their degree, and a numerical index where numbers are drawn at nodes corresponding to their degree.
|
|
@@ -4301,6 +4301,27 @@ class Network_3D:
|
|
|
4301
4301
|
:returns: A dictionary of degree values for each node.
|
|
4302
4302
|
"""
|
|
4303
4303
|
|
|
4304
|
+
if heatmap:
|
|
4305
|
+
import statistics
|
|
4306
|
+
degrees_dict = {node: val for (node, val) in self.network.degree()}
|
|
4307
|
+
pred = statistics.mean(list(degrees_dict.values()))
|
|
4308
|
+
|
|
4309
|
+
node_intensity = {}
|
|
4310
|
+
import math
|
|
4311
|
+
node_centroids = {}
|
|
4312
|
+
|
|
4313
|
+
for node in list(self.network.nodes()):
|
|
4314
|
+
node_intensity[node] = math.log(self.network.degree(node)/pred)
|
|
4315
|
+
node_centroids[node] = self.node_centroids[node]
|
|
4316
|
+
|
|
4317
|
+
from . import neighborhoods
|
|
4318
|
+
|
|
4319
|
+
overlay = neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = self.nodes.shape, is_3d=True, labeled_array = self.nodes)
|
|
4320
|
+
|
|
4321
|
+
return degrees_dict, overlay
|
|
4322
|
+
|
|
4323
|
+
|
|
4324
|
+
|
|
4304
4325
|
if down_factor > 1:
|
|
4305
4326
|
centroids = self._node_centroids.copy()
|
|
4306
4327
|
for item in self._node_centroids:
|
|
@@ -4657,18 +4678,16 @@ class Network_3D:
|
|
|
4657
4678
|
dim = 3
|
|
4658
4679
|
break
|
|
4659
4680
|
|
|
4660
|
-
|
|
4661
4681
|
if ignore_dims:
|
|
4662
4682
|
|
|
4663
4683
|
factor = 0.25
|
|
4664
4684
|
|
|
4685
|
+
big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
|
|
4665
4686
|
|
|
4666
4687
|
if bounds is None:
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
min_coords = np.array([0,0,0])
|
|
4671
|
-
max_coords = np.max(points_array, axis=0)
|
|
4688
|
+
min_coords = np.array([0,0,0])
|
|
4689
|
+
max_coords = [np.max(big_array[:, 0]), np.max(big_array[:, 1]), np.max(big_array[:, 2])]
|
|
4690
|
+
del big_array
|
|
4672
4691
|
max_coords = np.flip(max_coords)
|
|
4673
4692
|
bounds = (min_coords, max_coords)
|
|
4674
4693
|
else:
|
|
@@ -4683,11 +4702,16 @@ class Network_3D:
|
|
|
4683
4702
|
|
|
4684
4703
|
new_list = []
|
|
4685
4704
|
|
|
4705
|
+
|
|
4686
4706
|
if dim == 3:
|
|
4687
4707
|
for centroid in roots:
|
|
4688
4708
|
|
|
4689
4709
|
if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
|
|
4690
4710
|
new_list.append(centroid)
|
|
4711
|
+
|
|
4712
|
+
|
|
4713
|
+
#if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
|
|
4714
|
+
#new_list.append(centroid)
|
|
4691
4715
|
#print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
|
|
4692
4716
|
else:
|
|
4693
4717
|
for centroid in roots:
|
|
@@ -5068,7 +5092,7 @@ class Network_3D:
|
|
|
5068
5092
|
|
|
5069
5093
|
self.communities = invert_dict(com_dict)
|
|
5070
5094
|
|
|
5071
|
-
def community_heatmap(self, num_nodes = None, is3d = True):
|
|
5095
|
+
def community_heatmap(self, num_nodes = None, is3d = True, numpy = False):
|
|
5072
5096
|
|
|
5073
5097
|
import math
|
|
5074
5098
|
|
|
@@ -5106,10 +5130,21 @@ class Network_3D:
|
|
|
5106
5130
|
for com, nodes in coms.items():
|
|
5107
5131
|
heat_dict[com] = math.log(len(nodes)/rand_dens)
|
|
5108
5132
|
|
|
5133
|
+
try:
|
|
5134
|
+
shape = self.nodes.shape
|
|
5135
|
+
except:
|
|
5136
|
+
big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
|
|
5137
|
+
shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
|
|
5138
|
+
|
|
5109
5139
|
from . import neighborhoods
|
|
5110
|
-
|
|
5140
|
+
if not numpy:
|
|
5141
|
+
neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, shape = shape, is_3d=is3d)
|
|
5142
|
+
|
|
5143
|
+
return heat_dict
|
|
5144
|
+
else:
|
|
5145
|
+
overlay = neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, shape = shape, is_3d=is3d, labeled_array = self.nodes)
|
|
5146
|
+
return heat_dict, overlay
|
|
5111
5147
|
|
|
5112
|
-
return heat_dict
|
|
5113
5148
|
|
|
5114
5149
|
def merge_node_ids(self, path, data):
|
|
5115
5150
|
|
|
@@ -5161,6 +5196,81 @@ class Network_3D:
|
|
|
5161
5196
|
pass
|
|
5162
5197
|
|
|
5163
5198
|
|
|
5199
|
+
def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False):
|
|
5200
|
+
|
|
5201
|
+
root_set = []
|
|
5202
|
+
|
|
5203
|
+
compare_set = []
|
|
5204
|
+
|
|
5205
|
+
if root is None:
|
|
5206
|
+
|
|
5207
|
+
root_set = list(self.node_centroids.keys())
|
|
5208
|
+
compare_set = root_set
|
|
5209
|
+
title = "Nearest Neighbors Between Nodes Heatmap"
|
|
5210
|
+
|
|
5211
|
+
else:
|
|
5212
|
+
|
|
5213
|
+
title = f"Nearest Neighbors of ID {targ} from ID {root} Heatmap"
|
|
5214
|
+
|
|
5215
|
+
for node, iden in self.node_identities.items():
|
|
5216
|
+
|
|
5217
|
+
if iden == root:
|
|
5218
|
+
|
|
5219
|
+
root_set.append(node)
|
|
5220
|
+
|
|
5221
|
+
elif (iden == targ) or (targ == 'All Others (Excluding Self)'):
|
|
5222
|
+
|
|
5223
|
+
compare_set.append(node)
|
|
5224
|
+
|
|
5225
|
+
if root == targ:
|
|
5226
|
+
|
|
5227
|
+
compare_set = root_set
|
|
5228
|
+
if len(compare_set) - 1 < num:
|
|
5229
|
+
|
|
5230
|
+
print("Error: Not enough neighbor nodes for requested number of neighbors")
|
|
5231
|
+
return
|
|
5232
|
+
|
|
5233
|
+
if len(compare_set) < num:
|
|
5234
|
+
|
|
5235
|
+
print("Error: Not enough neighbor nodes for requested number of neighbors")
|
|
5236
|
+
return
|
|
5237
|
+
|
|
5238
|
+
avg, output = proximity.average_nearest_neighbor_distances(self.node_centroids, root_set, compare_set, xy_scale=self.xy_scale, z_scale=self.z_scale, num = num)
|
|
5239
|
+
|
|
5240
|
+
if heatmap:
|
|
5241
|
+
|
|
5242
|
+
|
|
5243
|
+
from . import neighborhoods
|
|
5244
|
+
try:
|
|
5245
|
+
shape = self.nodes.shape
|
|
5246
|
+
except:
|
|
5247
|
+
big_array = proximity.convert_centroids_to_array(list(self.node_centroids.values()))
|
|
5248
|
+
shape = [np.max(big_array[0, :]) + 1, np.max(big_array[1, :]) + 1, np.max(big_array[2, :]) + 1]
|
|
5249
|
+
|
|
5250
|
+
pred = avg
|
|
5251
|
+
|
|
5252
|
+
node_intensity = {}
|
|
5253
|
+
import math
|
|
5254
|
+
node_centroids = {}
|
|
5255
|
+
|
|
5256
|
+
for node in root_set:
|
|
5257
|
+
node_intensity[node] = math.log(pred/output[node])
|
|
5258
|
+
node_centroids[node] = self.node_centroids[node]
|
|
5259
|
+
|
|
5260
|
+
if numpy:
|
|
5261
|
+
|
|
5262
|
+
overlay = neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = self.nodes, colorbar_label="Clustering Intensity", title = title)
|
|
5263
|
+
|
|
5264
|
+
return avg, output, overlay
|
|
5265
|
+
|
|
5266
|
+
else:
|
|
5267
|
+
neighborhoods.create_node_heatmap(node_intensity, node_centroids, shape = shape, is_3d=threed, labeled_array = None, colorbar_label="Clustering Intensity", title = title)
|
|
5268
|
+
|
|
5269
|
+
return avg, output
|
|
5270
|
+
|
|
5271
|
+
|
|
5272
|
+
|
|
5273
|
+
|
|
5164
5274
|
|
|
5165
5275
|
|
|
5166
5276
|
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -3432,6 +3432,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3432
3432
|
ripley_action.triggered.connect(self.show_ripley_dialog)
|
|
3433
3433
|
heatmap_action = stats_menu.addAction("Community Cluster Heatmap")
|
|
3434
3434
|
heatmap_action.triggered.connect(self.show_heatmap_dialog)
|
|
3435
|
+
nearneigh_action = stats_menu.addAction("Average Nearest Neighbors")
|
|
3436
|
+
nearneigh_action.triggered.connect(self.show_nearneigh_dialog)
|
|
3435
3437
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
3436
3438
|
vol_action.triggered.connect(self.volumes)
|
|
3437
3439
|
rad_action = stats_menu.addAction("Calculate Radii")
|
|
@@ -5212,6 +5214,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5212
5214
|
dialog = HeatmapDialog(self)
|
|
5213
5215
|
dialog.exec()
|
|
5214
5216
|
|
|
5217
|
+
def show_nearneigh_dialog(self):
|
|
5218
|
+
dialog = NearNeighDialog(self)
|
|
5219
|
+
dialog.exec()
|
|
5220
|
+
|
|
5215
5221
|
def show_random_dialog(self):
|
|
5216
5222
|
dialog = RandomDialog(self)
|
|
5217
5223
|
dialog.exec()
|
|
@@ -7269,6 +7275,174 @@ class DegreeDistDialog(QDialog):
|
|
|
7269
7275
|
except Exception as e:
|
|
7270
7276
|
print(f"An error occurred: {e}")
|
|
7271
7277
|
|
|
7278
|
+
class NearNeighDialog(QDialog):
|
|
7279
|
+
def __init__(self, parent=None):
|
|
7280
|
+
super().__init__(parent)
|
|
7281
|
+
self.setWindowTitle(f"Nearest Neighborhood Averages (Using Centroids)")
|
|
7282
|
+
self.setModal(True)
|
|
7283
|
+
|
|
7284
|
+
# Main layout
|
|
7285
|
+
main_layout = QVBoxLayout(self)
|
|
7286
|
+
|
|
7287
|
+
# Identities group box (only if node_identities exists)
|
|
7288
|
+
identities_group = QGroupBox("Identities")
|
|
7289
|
+
identities_layout = QFormLayout(identities_group)
|
|
7290
|
+
|
|
7291
|
+
if my_network.node_identities is not None:
|
|
7292
|
+
|
|
7293
|
+
self.root = QComboBox()
|
|
7294
|
+
self.root.addItems(list(set(my_network.node_identities.values())))
|
|
7295
|
+
self.root.setCurrentIndex(0)
|
|
7296
|
+
identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
|
|
7297
|
+
|
|
7298
|
+
self.targ = QComboBox()
|
|
7299
|
+
neighs = list(set(my_network.node_identities.values()))
|
|
7300
|
+
neighs.append("All Others (Excluding Self)")
|
|
7301
|
+
self.targ.addItems(neighs)
|
|
7302
|
+
self.targ.setCurrentIndex(0)
|
|
7303
|
+
identities_layout.addRow("Neighbor Identities to Search For?", self.targ)
|
|
7304
|
+
else:
|
|
7305
|
+
self.root = None
|
|
7306
|
+
self.targ = None
|
|
7307
|
+
|
|
7308
|
+
self.num = QLineEdit("1")
|
|
7309
|
+
identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
|
|
7310
|
+
|
|
7311
|
+
|
|
7312
|
+
main_layout.addWidget(identities_group)
|
|
7313
|
+
|
|
7314
|
+
|
|
7315
|
+
# Optional Heatmap group box
|
|
7316
|
+
heatmap_group = QGroupBox("Optional Heatmap")
|
|
7317
|
+
heatmap_layout = QFormLayout(heatmap_group)
|
|
7318
|
+
|
|
7319
|
+
self.map = QPushButton("(If getting distribution): Generate Heatmap?")
|
|
7320
|
+
self.map.setCheckable(True)
|
|
7321
|
+
self.map.setChecked(False)
|
|
7322
|
+
heatmap_layout.addRow("Heatmap:", self.map)
|
|
7323
|
+
|
|
7324
|
+
self.threed = QPushButton("(For above): Return 3D map? (uncheck for 2D): ")
|
|
7325
|
+
self.threed.setCheckable(True)
|
|
7326
|
+
self.threed.setChecked(True)
|
|
7327
|
+
heatmap_layout.addRow("3D:", self.threed)
|
|
7328
|
+
|
|
7329
|
+
self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
|
|
7330
|
+
self.numpy.setCheckable(True)
|
|
7331
|
+
self.numpy.setChecked(False)
|
|
7332
|
+
self.numpy.clicked.connect(self.toggle_map)
|
|
7333
|
+
heatmap_layout.addRow("Overlay:", self.numpy)
|
|
7334
|
+
|
|
7335
|
+
main_layout.addWidget(heatmap_group)
|
|
7336
|
+
|
|
7337
|
+
# Get Distribution group box
|
|
7338
|
+
distribution_group = QGroupBox("Get Distribution")
|
|
7339
|
+
distribution_layout = QVBoxLayout(distribution_group)
|
|
7340
|
+
|
|
7341
|
+
run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
|
|
7342
|
+
run_button.clicked.connect(self.run)
|
|
7343
|
+
distribution_layout.addWidget(run_button)
|
|
7344
|
+
|
|
7345
|
+
main_layout.addWidget(distribution_group)
|
|
7346
|
+
|
|
7347
|
+
# Get All Averages group box (only if node_identities exists)
|
|
7348
|
+
if my_network.node_identities is not None:
|
|
7349
|
+
averages_group = QGroupBox("Get All Averages")
|
|
7350
|
+
averages_layout = QVBoxLayout(averages_group)
|
|
7351
|
+
|
|
7352
|
+
run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
|
|
7353
|
+
run_button2.clicked.connect(self.run2)
|
|
7354
|
+
averages_layout.addWidget(run_button2)
|
|
7355
|
+
|
|
7356
|
+
main_layout.addWidget(averages_group)
|
|
7357
|
+
|
|
7358
|
+
def toggle_map(self):
|
|
7359
|
+
|
|
7360
|
+
if self.numpy.isChecked():
|
|
7361
|
+
|
|
7362
|
+
if not self.map.isChecked():
|
|
7363
|
+
|
|
7364
|
+
self.map.click()
|
|
7365
|
+
|
|
7366
|
+
def run(self):
|
|
7367
|
+
|
|
7368
|
+
try:
|
|
7369
|
+
|
|
7370
|
+
try:
|
|
7371
|
+
root = self.root.currentText()
|
|
7372
|
+
except:
|
|
7373
|
+
root = None
|
|
7374
|
+
try:
|
|
7375
|
+
targ = self.targ.currentText()
|
|
7376
|
+
except:
|
|
7377
|
+
targ = None
|
|
7378
|
+
|
|
7379
|
+
heatmap = self.map.isChecked()
|
|
7380
|
+
threed = self.threed.isChecked()
|
|
7381
|
+
numpy = self.numpy.isChecked()
|
|
7382
|
+
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
7383
|
+
|
|
7384
|
+
if root is not None and targ is not None:
|
|
7385
|
+
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
7386
|
+
header = f"Shortest Distance to Closest {num} {targ}(s)"
|
|
7387
|
+
header2 = f"{root} Node ID"
|
|
7388
|
+
else:
|
|
7389
|
+
title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
|
|
7390
|
+
header = f"Shortest Distance to Closest {num} Nodes"
|
|
7391
|
+
header2 = "Root Node ID"
|
|
7392
|
+
|
|
7393
|
+
if my_network.node_centroids is None:
|
|
7394
|
+
self.parent().show_centroid_dialog()
|
|
7395
|
+
if my_network.node_centroids is None:
|
|
7396
|
+
return
|
|
7397
|
+
|
|
7398
|
+
if not numpy:
|
|
7399
|
+
avg, output = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed)
|
|
7400
|
+
else:
|
|
7401
|
+
avg, output, overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True)
|
|
7402
|
+
self.parent().load_channel(3, overlay, data = True)
|
|
7403
|
+
|
|
7404
|
+
self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
|
|
7405
|
+
self.parent().format_for_upperright_table(output, header2, header, title = title)
|
|
7406
|
+
|
|
7407
|
+
self.accept()
|
|
7408
|
+
|
|
7409
|
+
except Exception as e:
|
|
7410
|
+
import traceback
|
|
7411
|
+
print(traceback.format_exc())
|
|
7412
|
+
|
|
7413
|
+
print(f"Error: {e}")
|
|
7414
|
+
|
|
7415
|
+
def run2(self):
|
|
7416
|
+
|
|
7417
|
+
try:
|
|
7418
|
+
|
|
7419
|
+
available = list(set(my_network.node_identities.values()))
|
|
7420
|
+
|
|
7421
|
+
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
7422
|
+
|
|
7423
|
+
output_dict = {}
|
|
7424
|
+
|
|
7425
|
+
while len(available) > 1:
|
|
7426
|
+
|
|
7427
|
+
root = available[0]
|
|
7428
|
+
|
|
7429
|
+
for targ in available:
|
|
7430
|
+
|
|
7431
|
+
avg, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
|
|
7432
|
+
|
|
7433
|
+
output_dict[f"{root} vs {targ}"] = avg
|
|
7434
|
+
|
|
7435
|
+
del available[0]
|
|
7436
|
+
|
|
7437
|
+
self.parent().format_for_upperright_table(output_dict, "ID Combo", "Avg Distance to Nearest", title = "Average Distance to Nearest Neighbors for All ID Combos")
|
|
7438
|
+
|
|
7439
|
+
self.accept()
|
|
7440
|
+
|
|
7441
|
+
except Exception as e:
|
|
7442
|
+
|
|
7443
|
+
print(f"Error: {e}")
|
|
7444
|
+
|
|
7445
|
+
|
|
7272
7446
|
class NeighborIdentityDialog(QDialog):
|
|
7273
7447
|
|
|
7274
7448
|
def __init__(self, parent=None):
|
|
@@ -7455,8 +7629,7 @@ class RipleyDialog(QDialog):
|
|
|
7455
7629
|
"Error:",
|
|
7456
7630
|
f"Failed to preform cluster analysis: {str(e)}"
|
|
7457
7631
|
)
|
|
7458
|
-
|
|
7459
|
-
print(traceback.format_exc())
|
|
7632
|
+
|
|
7460
7633
|
print(f"Error: {e}")
|
|
7461
7634
|
|
|
7462
7635
|
class HeatmapDialog(QDialog):
|
|
@@ -7479,6 +7652,11 @@ class HeatmapDialog(QDialog):
|
|
|
7479
7652
|
self.is3d.setChecked(True)
|
|
7480
7653
|
layout.addRow("Use 3D Plot (uncheck for 2D)?:", self.is3d)
|
|
7481
7654
|
|
|
7655
|
+
self.numpy = QPushButton("(For heatmap): Return image overlay instead of graph? (Goes in Overlay 2): ")
|
|
7656
|
+
self.numpy.setCheckable(True)
|
|
7657
|
+
self.numpy.setChecked(False)
|
|
7658
|
+
layout.addRow("Overlay:", self.numpy)
|
|
7659
|
+
|
|
7482
7660
|
|
|
7483
7661
|
# Add Run button
|
|
7484
7662
|
run_button = QPushButton("Run")
|
|
@@ -7487,25 +7665,40 @@ class HeatmapDialog(QDialog):
|
|
|
7487
7665
|
|
|
7488
7666
|
def run(self):
|
|
7489
7667
|
|
|
7490
|
-
|
|
7668
|
+
try:
|
|
7491
7669
|
|
|
7492
|
-
|
|
7670
|
+
nodecount = int(self.nodecount.text()) if self.nodecount.text().strip() else None
|
|
7671
|
+
|
|
7672
|
+
is3d = self.is3d.isChecked()
|
|
7493
7673
|
|
|
7494
7674
|
|
|
7495
|
-
if my_network.communities is None:
|
|
7496
|
-
if my_network.network is not None:
|
|
7497
|
-
self.parent().show_partition_dialog()
|
|
7498
|
-
else:
|
|
7499
|
-
self.parent().handle_com_cell()
|
|
7500
7675
|
if my_network.communities is None:
|
|
7501
|
-
|
|
7676
|
+
if my_network.network is not None:
|
|
7677
|
+
self.parent().show_partition_dialog()
|
|
7678
|
+
else:
|
|
7679
|
+
self.parent().handle_com_cell()
|
|
7680
|
+
if my_network.communities is None:
|
|
7681
|
+
return
|
|
7502
7682
|
|
|
7503
|
-
|
|
7683
|
+
numpy = self.numpy.isChecked()
|
|
7504
7684
|
|
|
7505
|
-
|
|
7685
|
+
if not numpy:
|
|
7506
7686
|
|
|
7507
|
-
|
|
7687
|
+
heat_dict = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d)
|
|
7508
7688
|
|
|
7689
|
+
else:
|
|
7690
|
+
|
|
7691
|
+
heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
|
|
7692
|
+
self.parent().load_channel(3, overlay, data = True)
|
|
7693
|
+
|
|
7694
|
+
|
|
7695
|
+
self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
|
|
7696
|
+
|
|
7697
|
+
self.accept()
|
|
7698
|
+
|
|
7699
|
+
except Exception as e:
|
|
7700
|
+
|
|
7701
|
+
print(f"Error: {e}")
|
|
7509
7702
|
|
|
7510
7703
|
|
|
7511
7704
|
|
|
@@ -7749,7 +7942,7 @@ class DegreeDialog(QDialog):
|
|
|
7749
7942
|
|
|
7750
7943
|
# Add mode selection dropdown
|
|
7751
7944
|
self.mode_selector = QComboBox()
|
|
7752
|
-
self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis
|
|
7945
|
+
self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis)", "Create Heatmap of Degrees"])
|
|
7753
7946
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
7754
7947
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
7755
7948
|
|
|
@@ -7770,6 +7963,14 @@ class DegreeDialog(QDialog):
|
|
|
7770
7963
|
|
|
7771
7964
|
accepted_mode = self.mode_selector.currentIndex()
|
|
7772
7965
|
|
|
7966
|
+
if accepted_mode == 3:
|
|
7967
|
+
degree_dict, overlay = my_network.get_degrees(heatmap = True)
|
|
7968
|
+
self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
|
|
7969
|
+
self.parent().load_channel(3, channel_data = overlay, data = True)
|
|
7970
|
+
self.accept()
|
|
7971
|
+
return
|
|
7972
|
+
|
|
7973
|
+
|
|
7773
7974
|
try:
|
|
7774
7975
|
down_factor = float(self.down_factor.text()) if self.down_factor.text() else 1
|
|
7775
7976
|
except ValueError:
|
|
@@ -10274,7 +10475,7 @@ class CropDialog(QDialog):
|
|
|
10274
10475
|
try:
|
|
10275
10476
|
|
|
10276
10477
|
super().__init__(parent)
|
|
10277
|
-
self.setWindowTitle("Crop Image?")
|
|
10478
|
+
self.setWindowTitle("Crop Image (Will transpose any centroids)?")
|
|
10278
10479
|
self.setModal(True)
|
|
10279
10480
|
|
|
10280
10481
|
layout = QFormLayout(self)
|
|
@@ -10330,10 +10531,70 @@ class CropDialog(QDialog):
|
|
|
10330
10531
|
|
|
10331
10532
|
self.parent().load_channel(i, array, data = True)
|
|
10332
10533
|
|
|
10534
|
+
print("Transposing centroids...")
|
|
10535
|
+
|
|
10536
|
+
try:
|
|
10537
|
+
|
|
10538
|
+
if my_network.node_centroids is not None:
|
|
10539
|
+
nodes = list(my_network.node_centroids.keys())
|
|
10540
|
+
centroids = np.array(list(my_network.node_centroids.values()))
|
|
10541
|
+
|
|
10542
|
+
# Transform all at once
|
|
10543
|
+
transformed = centroids - np.array([zmin, ymin, xmin])
|
|
10544
|
+
transformed = transformed.astype(int)
|
|
10545
|
+
|
|
10546
|
+
# Boolean mask for valid coordinates
|
|
10547
|
+
valid_mask = ((transformed >= 0) &
|
|
10548
|
+
(transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
|
|
10549
|
+
|
|
10550
|
+
# Rebuild dictionary with only valid entries
|
|
10551
|
+
my_network.node_centroids = {
|
|
10552
|
+
nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
|
|
10553
|
+
for i in range(len(nodes)) if valid_mask[i]
|
|
10554
|
+
}
|
|
10555
|
+
|
|
10556
|
+
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
10557
|
+
|
|
10558
|
+
except Exception as e:
|
|
10559
|
+
|
|
10560
|
+
print(f"Error transposing node centroids: {e}")
|
|
10561
|
+
|
|
10562
|
+
try:
|
|
10563
|
+
|
|
10564
|
+
if my_network.edge_centroids is not None:
|
|
10565
|
+
|
|
10566
|
+
if my_network.edge_centroids is not None:
|
|
10567
|
+
nodes = list(my_network.edge_centroids.keys())
|
|
10568
|
+
centroids = np.array(list(my_network.edge_centroids.values()))
|
|
10569
|
+
|
|
10570
|
+
# Transform all at once
|
|
10571
|
+
transformed = centroids - np.array([zmin, ymin, xmin])
|
|
10572
|
+
transformed = transformed.astype(int)
|
|
10573
|
+
|
|
10574
|
+
# Boolean mask for valid coordinates
|
|
10575
|
+
valid_mask = ((transformed >= 0) &
|
|
10576
|
+
(transformed <= np.array([zmax, ymax, xmax]))).all(axis=1)
|
|
10577
|
+
|
|
10578
|
+
# Rebuild dictionary with only valid entries
|
|
10579
|
+
my_network.edge_centroids = {
|
|
10580
|
+
nodes[int(i)]: [int(transformed[i, 0]), int(transformed[i, 1]), int(transformed[i, 2])]
|
|
10581
|
+
for i in range(len(nodes)) if valid_mask[i]
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10584
|
+
self.parent().format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
10585
|
+
|
|
10586
|
+
except Exception as e:
|
|
10587
|
+
|
|
10588
|
+
print(f"Error transposing edge centroids: {e}")
|
|
10589
|
+
|
|
10590
|
+
|
|
10333
10591
|
self.accept()
|
|
10334
10592
|
|
|
10335
10593
|
except Exception as e:
|
|
10336
10594
|
|
|
10595
|
+
import traceback
|
|
10596
|
+
print(traceback.format_exc())
|
|
10597
|
+
|
|
10337
10598
|
print(f"Error cropping: {e}")
|
|
10338
10599
|
|
|
10339
10600
|
|
nettracer3d/node_draw.py
CHANGED
|
@@ -201,18 +201,28 @@ def degree_draw(degree_dict, centroid_dict, nodes):
|
|
|
201
201
|
return draw_array
|
|
202
202
|
|
|
203
203
|
def degree_infect(degree_dict, nodes):
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
return_nodes = np.zeros_like(nodes) # Start with all zeros
|
|
205
|
+
|
|
206
|
+
if not degree_dict: # Handle empty dict
|
|
207
|
+
return return_nodes
|
|
208
|
+
|
|
209
|
+
# Create arrays for old and new values
|
|
210
|
+
old_vals = np.array(list(degree_dict.keys()))
|
|
211
|
+
new_vals = np.array(list(degree_dict.values()))
|
|
212
|
+
|
|
213
|
+
# Sort for searchsorted to work correctly
|
|
214
|
+
sort_idx = np.argsort(old_vals)
|
|
215
|
+
old_vals_sorted = old_vals[sort_idx]
|
|
216
|
+
new_vals_sorted = new_vals[sort_idx]
|
|
217
|
+
|
|
218
|
+
# Find which nodes exist in the dictionary
|
|
219
|
+
mask = np.isin(nodes, old_vals_sorted)
|
|
220
|
+
|
|
221
|
+
# Only process nodes that exist in the dictionary
|
|
222
|
+
if np.any(mask):
|
|
223
|
+
indices = np.searchsorted(old_vals_sorted, nodes[mask])
|
|
224
|
+
return_nodes[mask] = new_vals_sorted[indices]
|
|
225
|
+
|
|
216
226
|
return return_nodes
|
|
217
227
|
|
|
218
228
|
|
nettracer3d/proximity.py
CHANGED
|
@@ -296,6 +296,69 @@ def extract_pairwise_connections(connections):
|
|
|
296
296
|
return output
|
|
297
297
|
|
|
298
298
|
|
|
299
|
+
def average_nearest_neighbor_distances(point_centroids, root_set, compare_set, xy_scale=1.0, z_scale=1.0, num = 1):
|
|
300
|
+
"""
|
|
301
|
+
Calculate the average distance between each point in root_set and its nearest neighbor in compare_set.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
point_centroids (dict): Dictionary mapping point IDs to [Z, Y, X] coordinates
|
|
305
|
+
root_set (set): Set of point IDs to find nearest neighbors for
|
|
306
|
+
compare_set (set): Set of point IDs to search for nearest neighbors in
|
|
307
|
+
xy_scale (float): Scaling factor for X and Y coordinates
|
|
308
|
+
z_scale (float): Scaling factor for Z coordinate
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
float: Average distance to nearest neighbors
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
# Extract and scale coordinates for compare_set
|
|
315
|
+
compare_coords = []
|
|
316
|
+
compare_ids = list(compare_set)
|
|
317
|
+
|
|
318
|
+
for point_id in compare_ids:
|
|
319
|
+
z, y, x = point_centroids[point_id]
|
|
320
|
+
compare_coords.append([z * z_scale, y * xy_scale, x * xy_scale])
|
|
321
|
+
|
|
322
|
+
compare_coords = np.array(compare_coords)
|
|
323
|
+
|
|
324
|
+
# Build KDTree for efficient nearest neighbor search
|
|
325
|
+
tree = KDTree(compare_coords)
|
|
326
|
+
|
|
327
|
+
distances = {}
|
|
328
|
+
same_sets = root_set == compare_set
|
|
329
|
+
|
|
330
|
+
for root_id in root_set:
|
|
331
|
+
# Get scaled coordinates for root point
|
|
332
|
+
z, y, x = point_centroids[root_id]
|
|
333
|
+
root_coord = np.array([z * z_scale, y * xy_scale, x * xy_scale])
|
|
334
|
+
|
|
335
|
+
if same_sets:
|
|
336
|
+
# When sets are the same, find 2 nearest neighbors and take the second one
|
|
337
|
+
# (first one would be the point itself)
|
|
338
|
+
distances_to_all, indices = tree.query(root_coord, k= (num + 1))
|
|
339
|
+
|
|
340
|
+
temp_dist = 0
|
|
341
|
+
for i in range(1, len(distances_to_all)):
|
|
342
|
+
temp_dist += distances_to_all[i]
|
|
343
|
+
|
|
344
|
+
distances[root_id] = temp_dist/(len(distances_to_all) - 1)
|
|
345
|
+
|
|
346
|
+
else:
|
|
347
|
+
# Different sets, find nearest neighbors
|
|
348
|
+
distances_to_all, _ = tree.query(root_coord, k=num)
|
|
349
|
+
temp_dist = 0
|
|
350
|
+
for val in distances_to_all:
|
|
351
|
+
temp_dist += val
|
|
352
|
+
|
|
353
|
+
distances[root_id] = temp_dist/(len(distances_to_all))
|
|
354
|
+
|
|
355
|
+
avg = np.mean(list(distances.values())) if list(distances.values()) else 0.0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# Return average distance
|
|
359
|
+
return avg, distances
|
|
360
|
+
|
|
361
|
+
|
|
299
362
|
|
|
300
363
|
#voronois:
|
|
301
364
|
def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, int, int], List[int]]],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.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/
|
|
@@ -73,11 +73,8 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
|
|
|
73
73
|
|
|
74
74
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
75
75
|
|
|
76
|
-
-- Version 0.8.
|
|
76
|
+
-- Version 0.8.1 Updates --
|
|
77
77
|
|
|
78
|
-
* Added
|
|
79
|
-
*
|
|
80
|
-
* Bug fixes and
|
|
81
|
-
* Added ability to 'merge node identities' which just uses the nodes image as a reference for collecting 'identity' information from a group of other images - ie can use with cell nuclei (DAPI) to see what markers from the same imaging session overlap.
|
|
82
|
-
* Added ability to search for specific nodes directly in the nodes image with 'shift + f' or right click.
|
|
83
|
-
|
|
78
|
+
* Added nearest neighbor evaluation function (Analysis -> Stats -> Avg Nearest Neighbor)
|
|
79
|
+
* Added heatmap outputs for node degrees (Analysis -> Data/Overlays -> Get Degree Information).
|
|
80
|
+
* Bug fixes and misc improvements.
|
|
@@ -3,21 +3,21 @@ nettracer3d/community_extractor.py,sha256=ZOz97Au4k7dHK8azarWQtxCPyvwzGmDHNL8kTI
|
|
|
3
3
|
nettracer3d/excelotron.py,sha256=lS5vnpoOGZWp7fdqVpTPqeC-mUKrfwDrWHfx4PQ7Uzg,71384
|
|
4
4
|
nettracer3d/modularity.py,sha256=O9OeKbjD3v6gSFz9K2GzP6LsxlpQaPfeJbM1pyIEigw,21788
|
|
5
5
|
nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
|
|
6
|
-
nettracer3d/neighborhoods.py,sha256=
|
|
7
|
-
nettracer3d/nettracer.py,sha256=
|
|
8
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
6
|
+
nettracer3d/neighborhoods.py,sha256=LnflCJdlNOuILal_fsT-idp1IROQH2PvLG-Ro3m55vo,26757
|
|
7
|
+
nettracer3d/nettracer.py,sha256=F91vCLJ-OB5qGLjOXRjNh2qE3vFkXYh_VPpFud2Cu4o,231437
|
|
8
|
+
nettracer3d/nettracer_gui.py,sha256=rRm3hquNbJJ8wbqPyojK45jFKc--Y72L4Ia9_dMSONE,512171
|
|
9
9
|
nettracer3d/network_analysis.py,sha256=h-5yzUWdE0hcWYy8wcBA5LV1bRhdqiMnKbQLrRzb1Sw,41443
|
|
10
10
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
11
|
-
nettracer3d/node_draw.py,sha256=
|
|
12
|
-
nettracer3d/proximity.py,sha256=
|
|
11
|
+
nettracer3d/node_draw.py,sha256=LoeTFeOcrX6kPquZvCqYnMW-jDd9oqKM27r-rTlKEtY,10274
|
|
12
|
+
nettracer3d/proximity.py,sha256=3cgF9hWYlzodO_vFc9TA2QO1tZG6jqYbz4_AG89EKDE,30867
|
|
13
13
|
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
14
14
|
nettracer3d/segmenter.py,sha256=VatOSpc41lxhPuYLTTejCxG1CcwP5hwiQ3ZFK9OBavA,60115
|
|
15
15
|
nettracer3d/segmenter_GPU.py,sha256=sFVmz_cYIVOQqnfFV3peK9hzb6IoIV5WDQHH9Lws96I,53915
|
|
16
16
|
nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
|
|
17
17
|
nettracer3d/smart_dilate.py,sha256=DOEOQq9ig6-AO4MpqAG0CqrGDFqw5_UBeqfSedqHk28,25933
|
|
18
|
-
nettracer3d-0.8.
|
|
19
|
-
nettracer3d-0.8.
|
|
20
|
-
nettracer3d-0.8.
|
|
21
|
-
nettracer3d-0.8.
|
|
22
|
-
nettracer3d-0.8.
|
|
23
|
-
nettracer3d-0.8.
|
|
18
|
+
nettracer3d-0.8.1.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
19
|
+
nettracer3d-0.8.1.dist-info/METADATA,sha256=kw33GSA0zjO1PbRkCfMDjh-opacoSn1OSx7X5M8UA88,4332
|
|
20
|
+
nettracer3d-0.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
nettracer3d-0.8.1.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
22
|
+
nettracer3d-0.8.1.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
23
|
+
nettracer3d-0.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|