In [1]:
# Set up sys.path so that 'src/spindle_dev' is importable as 'spindle_dev'
import sys
import importlib
from pathlib import Path
project_root = '/data/sarkar_lab/Projects/spindle_dev'
src_path = Path(project_root) / 'src'
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))
import spindle_dev
# Reload to pick up code changes without restarting the kernel
importlib.reload(spindle_dev)
Out[1]:
<module 'spindle_dev' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/__init__.py'>
In [1]:
save_figure = False
In [3]:
import scanpy as sc
In [4]:
adata = sc.read_h5ad("/data/sarkar_lab/zenodo_data/2023_xenium_human_breast_cancer/adata.h5ad")
In [5]:
adata = adata[adata.obs.loc[adata.obs.Cluster != "Unlabeled"].index, :].copy()
In [6]:
sc.pl.spatial(adata, color=["Cluster"], spot_size=13)
/tmp/ipykernel_740051/1942974121.py:1: FutureWarning: Use `squidpy.pl.spatial_scatter` instead. sc.pl.spatial(adata, color=["Cluster"], spot_size=13)
In [7]:
# Reload spindle_dev metrics, index, and package after code changes
import importlib
import spindle_dev
import spindle_dev.metrics as metrics
import spindle_dev.index as index
import spindle_dev.preprocessing as preprocessing
import spindle_dev.plotting as plotting
# Reload in dependency order so new symbols are available
importlib.reload(metrics)
importlib.reload(index)
importlib.reload(preprocessing)
importlib.reload(plotting)
importlib.reload(spindle_dev)
import spindle_dev.typing as typing
importlib.reload(typing)
import spindle_dev.test as test
import spindle_dev.search as search
importlib.reload(test)
importlib.reload(search)
Out[7]:
<module 'spindle_dev.search' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/search.py'>
In [8]:
def prepare_to_index(adata, resolution=0.2, min_final_size=20):
coords = adata.obsm["spatial"]
tiles = preprocessing.build_quadtree_tiles(coords, max_pts=200, min_side=0.0, max_depth=40)
num_genes = adata.n_vars
genes_work, gene_idx = spindle_dev.preprocessing.topvar_genes(adata, G=num_genes)
tile_covs = spindle_dev.preprocessing.build_tile_covs_full(adata, tiles, gene_idx, n_jobs=8, eps=1e-6)
data = index.ProcessedData(tiles, tile_covs, genes_work, adata.n_obs)
data.reduce_dim(num_pca_components=30, n_components=2, do_umap=True)
data.cluster_spds(cluster_distance="tree", cluster_method="leiden", resolution=resolution)
data.assign_label_to_spots()
data.get_corr_mean_by_cluster()
out_dict = data.get_adaptive_runs(find_blocks=True, with_size_guard=True,min_final_size=min_final_size,max_final_size=100)
return tiles, tile_covs, genes_work, data, out_dict
In [9]:
tiles, tile_covs, genes_work, data, out_dict = prepare_to_index(adata, min_final_size=15)
[2026-01-20 10:48:05,424] INFO spindle_dev.index: Clustering SPD-s using 'tree' distance. [2026-01-20 10:48:05,502] INFO spindle_dev.index: Building ultrametric features from SPD matrices. [2026-01-20 10:48:12,135] INFO spindle_dev.index: Computing latent features from the tree representations. [2026-01-20 10:48:12,703] INFO spindle_dev.index: Reducing latent features to 30 dimensions using PCA. [2026-01-20 10:48:19,694] INFO spindle_dev.index: Explained variance ratios by PCA components: [0.06878107 0.05380314 0.02646384 0.01915205 0.01300586 0.01025453 0.00781812 0.00735731 0.00665263 0.00645236 0.00500298 0.00465103 0.00423426 0.00416654 0.00382042 0.00364907 0.00355717 0.00348549 0.00334294 0.00319313 0.00303316 0.00301945 0.00300855 0.00294676 0.00288963 0.00281065 0.00275823 0.00274145 0.00271152 0.00265672] [2026-01-20 10:48:19,696] INFO spindle_dev.index: Reducing latent features to 2 dimensions using UMAP. /panfs/accrepfs.vampire/home/sarkah1/miniforge3/envs/spatial/lib/python3.10/site-packages/umap/umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism. warn( [2026-01-20 10:48:33,818] INFO spindle_dev.index: Clustering SPD-s using 'tree' distance. [2026-01-20 10:48:33,819] INFO spindle_dev.index: Clustering SPD matrices using Leiden clustering with resolution 0.20. [2026-01-20 10:48:34,044] INFO spindle_dev.index: Since clustering method is tree, I am going to find global order per cluster [2026-01-20 10:48:34,045] INFO spindle_dev.index: Finding consensus tree for cluster 0 [2026-01-20 10:48:34,149] INFO spindle_dev.index: Finding consensus tree for cluster 1 [2026-01-20 10:48:34,241] INFO spindle_dev.index: Finding consensus tree for cluster 2 [2026-01-20 10:48:34,319] INFO spindle_dev.index: Finding consensus tree for cluster 3 [2026-01-20 10:48:34,764] INFO spindle_dev.index: Computing mean correlation matrix for cluster 0 [2026-01-20 10:48:35,540] INFO spindle_dev.index: Computing mean correlation matrix for cluster 1 [2026-01-20 10:48:36,126] INFO spindle_dev.index: Computing mean correlation matrix for cluster 2 [2026-01-20 10:48:36,608] INFO spindle_dev.index: Computing mean correlation matrix for cluster 3 [2026-01-20 10:48:36,841] INFO spindle_dev.index: Finding adaptive block runs for cluster 0 [2026-01-20 10:48:37,110] INFO spindle_dev.index: Chose t=0.7638580524895158 resulting in 200 blocks instead of 61 blocks would have gotten by default [2026-01-20 10:48:37,140] INFO spindle_dev.index: Final block runs for cluster 0: 16 blocks. [2026-01-20 10:48:37,141] INFO spindle_dev.index: Finding adaptive block runs for cluster 1 [2026-01-20 10:48:37,417] INFO spindle_dev.index: Chose t=0.8451053062066296 resulting in 159 blocks instead of 37 blocks would have gotten by default [2026-01-20 10:48:37,437] INFO spindle_dev.index: Final block runs for cluster 1: 14 blocks. [2026-01-20 10:48:37,438] INFO spindle_dev.index: Finding adaptive block runs for cluster 2 [2026-01-20 10:48:37,710] INFO spindle_dev.index: Chose t=0.7649764855224312 resulting in 188 blocks instead of 40 blocks would have gotten by default [2026-01-20 10:48:37,737] INFO spindle_dev.index: Final block runs for cluster 2: 16 blocks. [2026-01-20 10:48:37,738] INFO spindle_dev.index: Finding adaptive block runs for cluster 3 [2026-01-20 10:48:38,007] INFO spindle_dev.index: Chose t=0.8187994098657192 resulting in 220 blocks instead of 98 blocks would have gotten by default [2026-01-20 10:48:38,044] INFO spindle_dev.index: Final block runs for cluster 3: 15 blocks.
In [10]:
import matplotlib.pylab as plt
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
In [12]:
import numpy as np
palette = sc.pl.palettes.default_20
point_colors = [palette[lab] for lab in data.spot_label.values()]
labels = data.labels
indices = np.array([int(i) for i in data.spot_label.keys()])
coords = adata.obsm['spatial']
plt.scatter(coords[indices,0], coords[indices,1], s=1, c=point_colors, lw=0, alpha=0.8)
patches = []
for t in tiles:
bbox = t.bbox if hasattr(t, "bbox") else t["bbox"] # (xmin,ymin,xmax,ymax)
x0, y0, x1, y1 = bbox
patches.append(Rectangle((x0, y0), x1 - x0, y1 - y0, fill=False))
pc = PatchCollection(patches, match_original=True, linewidths=0.2, edgecolors='b', alpha=0.3)
# add collection
ax = plt.gca()
ax.add_collection(pc)
ax.invert_yaxis()
ax.axis('off')
if save_figure:
plt.savefig("/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/spatial_plot_with_cluster.png", dpi=300)
plt.show()
In [13]:
# Create colors for clusters
import numpy as np
import matplotlib.pyplot as plt
n_clusters = len(set(data.labels))
# Use Scanpy's default_20 palette for consistency
palette = sc.pl.palettes.default_20
cluster_colors = [palette[lab] for lab in data.labels]
plt.scatter(data.latent['umap'][:,0], data.latent['umap'][:,1], s=6, lw=0, alpha=0.8, c=cluster_colors)
# Write label on the plot
# with backgrond circle for better visibility
# and bold font
for i in range(n_clusters):
cluster_points = data.latent['umap'][data.labels == i]
if len(cluster_points) == 0:
continue
x_mean = np.mean(cluster_points[:,0])
y_mean = np.mean(cluster_points[:,1])
plt.text(x_mean, y_mean, str(i), color='black', fontsize=10, ha='center', va='center', fontweight='bold',
bbox=dict(facecolor='white', edgecolor='black', boxstyle='circle,pad=0.5', alpha=0.7))
# take away top and right border
ax = plt.gca()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# remove axis ticks
ax.set_xticks([])
ax.set_yticks([])
if save_figure:
plt.savefig("/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/umap_plot_with_cluster_labels.png", dpi=300)
plt.show()
In [14]:
epsilon_block_wise_dict = {}
epsilon_dict = {}
for cluster_id in set(data.labels):
eps_per_block, eps_elbow_per_block, eps = index.choose_adaptive_epsilons(data, cluster_id, k_target_per_block=64)
epsilon_block_wise_dict[int(cluster_id)] = eps_elbow_per_block
epsilon_dict[int(cluster_id)] = eps
In [ ]:
In [15]:
config = typing.IndexConfig()
config.epsilon_dict = epsilon_dict
config.epsilon_block_wise_dict = epsilon_block_wise_dict
config.threshold_type = 'constant'
config.kmean_method = 'epsilon_net'
In [16]:
epsilon_block_wise_dict
Out[16]:
{0: {0: 7.551487154783578,
1: 7.688574605978839,
2: 8.348619550689628,
3: 8.12886229785936,
4: 6.941772257682238,
5: 7.11207711085268,
6: 7.4634645865588745,
7: 8.12941780999703,
8: 7.394765319810765,
9: 6.714566808424555,
10: 7.174724333051149,
11: 6.700368214926148,
12: 6.223964337518237,
13: 5.151625529484852,
14: 4.577674131656347,
15: 9.7037491104398},
1: {0: 7.34932238515324,
1: 8.493449641575141,
2: 8.901136453589094,
3: 7.8762835362284145,
4: 7.151354147881101,
5: 7.800650669737451,
6: 8.425077404859794,
7: 8.975881255696738,
8: 8.588472145359653,
9: 5.793292280974387,
10: 9.691527557373046,
11: 9.951465035398689,
12: 6.811198569674318,
13: 4.351012706756592},
2: {0: 6.799333509719286,
1: 8.557316793247027,
2: 8.452427434089639,
3: 7.588833091125586,
4: 6.785369982212529,
5: 5.5096939776630975,
6: 5.248055444220603,
7: 5.746564254670582,
8: 5.975592355011169,
9: 3.1322101267631437,
10: 4.466462934560956,
11: 8.84131131853376,
12: 6.445534355120787,
13: 5.415287845182453,
14: 6.687208104027927,
15: 7.126931829888523},
3: {0: 7.388558401037387,
1: 7.783292293548584,
2: 8.289245743649037,
3: 7.992608086706223,
4: 7.854193064200638,
5: 8.034181459182257,
6: 7.878430512484119,
7: 8.066423324555597,
8: 9.167032732265142,
9: 7.850585689461382,
10: 8.363619363357321,
11: 6.642502405394938,
12: 10.199246521661854,
13: 8.335221526620284,
14: 8.539956410725912}}
In [17]:
# Create index
dag_dict, stat, dist_list = index.index_spds(data, config=config)
[2026-01-20 10:50:25,175] INFO spindle_dev.index: Processing cluster 0 [2026-01-20 10:50:25,177] INFO spindle_dev.index: Building SPD index with epsilon=7.570320670118333 [2026-01-20 10:50:25,179] INFO spindle_dev.index: Step 1: Cluster blocks within each class of SPD matrices. [2026-01-20 10:50:25,220] INFO spindle_dev.index: Cluster 0: 733 SPDs, 16 blocks [2026-01-20 10:50:25,222] INFO spindle_dev.index: Using epsilon-net clustering for block 0 [2026-01-20 10:50:25,291] INFO spindle_dev.index: Finished block 0 in 0.07 seconds, found 2 clusters. [2026-01-20 10:50:25,293] INFO spindle_dev.index: Using epsilon-net clustering for block 1 [2026-01-20 10:50:25,356] INFO spindle_dev.index: Finished block 1 in 0.06 seconds, found 5 clusters. [2026-01-20 10:50:25,358] INFO spindle_dev.index: Using epsilon-net clustering for block 2 [2026-01-20 10:50:25,428] INFO spindle_dev.index: Finished block 2 in 0.07 seconds, found 6 clusters. [2026-01-20 10:50:25,430] INFO spindle_dev.index: Using epsilon-net clustering for block 3 [2026-01-20 10:50:25,505] INFO spindle_dev.index: Finished block 3 in 0.08 seconds, found 7 clusters. [2026-01-20 10:50:25,507] INFO spindle_dev.index: Using epsilon-net clustering for block 4 [2026-01-20 10:50:25,564] INFO spindle_dev.index: Finished block 4 in 0.06 seconds, found 3 clusters. [2026-01-20 10:50:25,566] INFO spindle_dev.index: Using epsilon-net clustering for block 5 [2026-01-20 10:50:25,612] INFO spindle_dev.index: Finished block 5 in 0.05 seconds, found 2 clusters. [2026-01-20 10:50:25,614] INFO spindle_dev.index: Using epsilon-net clustering for block 6 [2026-01-20 10:50:25,676] INFO spindle_dev.index: Finished block 6 in 0.06 seconds, found 2 clusters. [2026-01-20 10:50:25,677] INFO spindle_dev.index: Using epsilon-net clustering for block 7 [2026-01-20 10:50:25,729] INFO spindle_dev.index: Finished block 7 in 0.05 seconds, found 3 clusters. [2026-01-20 10:50:25,730] INFO spindle_dev.index: Using epsilon-net clustering for block 8 [2026-01-20 10:50:25,782] INFO spindle_dev.index: Finished block 8 in 0.05 seconds, found 3 clusters. [2026-01-20 10:50:25,782] INFO spindle_dev.index: Using epsilon-net clustering for block 9 [2026-01-20 10:50:25,828] INFO spindle_dev.index: Finished block 9 in 0.05 seconds, found 2 clusters. [2026-01-20 10:50:25,829] INFO spindle_dev.index: Using epsilon-net clustering for block 10 [2026-01-20 10:50:25,888] INFO spindle_dev.index: Finished block 10 in 0.06 seconds, found 3 clusters. [2026-01-20 10:50:25,888] INFO spindle_dev.index: Using epsilon-net clustering for block 11 [2026-01-20 10:50:25,961] INFO spindle_dev.index: Finished block 11 in 0.07 seconds, found 2 clusters. [2026-01-20 10:50:25,961] INFO spindle_dev.index: Using epsilon-net clustering for block 12 [2026-01-20 10:50:26,039] INFO spindle_dev.index: Finished block 12 in 0.08 seconds, found 2 clusters. [2026-01-20 10:50:26,039] INFO spindle_dev.index: Using epsilon-net clustering for block 13 [2026-01-20 10:50:26,081] INFO spindle_dev.index: Finished block 13 in 0.04 seconds, found 1 clusters. [2026-01-20 10:50:26,082] INFO spindle_dev.index: Using epsilon-net clustering for block 14 [2026-01-20 10:50:26,123] INFO spindle_dev.index: Finished block 14 in 0.04 seconds, found 1 clusters. [2026-01-20 10:50:26,124] INFO spindle_dev.index: Using epsilon-net clustering for block 15 [2026-01-20 10:50:26,406] INFO spindle_dev.index: Finished block 15 in 0.28 seconds, found 11 clusters. [2026-01-20 10:50:26,407] INFO spindle_dev.index: Step 2: Build DAG connections between block clusters. [2026-01-20 10:50:26,407] INFO spindle_dev.index: Step 2.1: For each layer order the block-clusters by [2026-01-20 10:50:26,408] INFO spindle_dev.index: Not implemented: ordering block-clusters ? How to order them? [2026-01-20 10:50:26,408] INFO spindle_dev.index: We will use triangle inequality to order clusters. [2026-01-20 10:50:26,408] INFO spindle_dev.index: Step 2.2: Connect block-clusters between layers based on co-occurrence in SPDs. [2026-01-20 10:50:26,415] INFO spindle_dev.index: Check if node global_node_id matches index in nodes list [2026-01-20 10:50:26,416] INFO spindle_dev.index: Step 2.3: Ordering block-clusters within each layer using log-Euclidean distances. [2026-01-20 10:50:26,416] INFO spindle_dev.index: Processing cluster 1 [2026-01-20 10:50:26,417] INFO spindle_dev.index: Building SPD index with epsilon=8.204116141691717 [2026-01-20 10:50:26,417] INFO spindle_dev.index: Step 1: Cluster blocks within each class of SPD matrices. [2026-01-20 10:50:26,447] INFO spindle_dev.index: Cluster 1: 567 SPDs, 14 blocks [2026-01-20 10:50:26,448] INFO spindle_dev.index: Using epsilon-net clustering for block 0 [2026-01-20 10:50:26,479] INFO spindle_dev.index: Finished block 0 in 0.03 seconds, found 2 clusters. [2026-01-20 10:50:26,479] INFO spindle_dev.index: Using epsilon-net clustering for block 1 [2026-01-20 10:50:26,525] INFO spindle_dev.index: Finished block 1 in 0.05 seconds, found 5 clusters. [2026-01-20 10:50:26,525] INFO spindle_dev.index: Using epsilon-net clustering for block 2 [2026-01-20 10:50:26,567] INFO spindle_dev.index: Finished block 2 in 0.04 seconds, found 4 clusters. [2026-01-20 10:50:26,568] INFO spindle_dev.index: Using epsilon-net clustering for block 3 [2026-01-20 10:50:26,606] INFO spindle_dev.index: Finished block 3 in 0.04 seconds, found 3 clusters. [2026-01-20 10:50:26,606] INFO spindle_dev.index: Using epsilon-net clustering for block 4 [2026-01-20 10:50:26,658] INFO spindle_dev.index: Finished block 4 in 0.05 seconds, found 3 clusters. [2026-01-20 10:50:26,658] INFO spindle_dev.index: Using epsilon-net clustering for block 5 [2026-01-20 10:50:26,716] INFO spindle_dev.index: Finished block 5 in 0.06 seconds, found 3 clusters. [2026-01-20 10:50:26,717] INFO spindle_dev.index: Using epsilon-net clustering for block 6 [2026-01-20 10:50:26,831] INFO spindle_dev.index: Finished block 6 in 0.11 seconds, found 3 clusters. [2026-01-20 10:50:26,832] INFO spindle_dev.index: Using epsilon-net clustering for block 7 [2026-01-20 10:50:26,911] INFO spindle_dev.index: Finished block 7 in 0.08 seconds, found 4 clusters. [2026-01-20 10:50:26,911] INFO spindle_dev.index: Using epsilon-net clustering for block 8 [2026-01-20 10:50:26,987] INFO spindle_dev.index: Finished block 8 in 0.08 seconds, found 4 clusters. [2026-01-20 10:50:26,987] INFO spindle_dev.index: Using epsilon-net clustering for block 9 [2026-01-20 10:50:27,023] INFO spindle_dev.index: Finished block 9 in 0.04 seconds, found 2 clusters. [2026-01-20 10:50:27,023] INFO spindle_dev.index: Using epsilon-net clustering for block 10 [2026-01-20 10:50:27,097] INFO spindle_dev.index: Finished block 10 in 0.07 seconds, found 5 clusters. [2026-01-20 10:50:27,097] INFO spindle_dev.index: Using epsilon-net clustering for block 11 [2026-01-20 10:50:27,216] INFO spindle_dev.index: Finished block 11 in 0.12 seconds, found 10 clusters. [2026-01-20 10:50:27,217] INFO spindle_dev.index: Using epsilon-net clustering for block 12 [2026-01-20 10:50:27,284] INFO spindle_dev.index: Finished block 12 in 0.07 seconds, found 3 clusters. [2026-01-20 10:50:27,285] INFO spindle_dev.index: Using epsilon-net clustering for block 13 [2026-01-20 10:50:27,326] INFO spindle_dev.index: Finished block 13 in 0.04 seconds, found 3 clusters. [2026-01-20 10:50:27,327] INFO spindle_dev.index: Step 2: Build DAG connections between block clusters. [2026-01-20 10:50:27,327] INFO spindle_dev.index: Step 2.1: For each layer order the block-clusters by [2026-01-20 10:50:27,328] INFO spindle_dev.index: Not implemented: ordering block-clusters ? How to order them? [2026-01-20 10:50:27,328] INFO spindle_dev.index: We will use triangle inequality to order clusters. [2026-01-20 10:50:27,328] INFO spindle_dev.index: Step 2.2: Connect block-clusters between layers based on co-occurrence in SPDs. [2026-01-20 10:50:27,333] INFO spindle_dev.index: Check if node global_node_id matches index in nodes list [2026-01-20 10:50:27,333] INFO spindle_dev.index: Step 2.3: Ordering block-clusters within each layer using log-Euclidean distances. [2026-01-20 10:50:27,334] INFO spindle_dev.index: Processing cluster 2 [2026-01-20 10:50:27,334] INFO spindle_dev.index: Building SPD index with epsilon=6.784064466997531 [2026-01-20 10:50:27,334] INFO spindle_dev.index: Step 1: Cluster blocks within each class of SPD matrices. [2026-01-20 10:50:27,346] INFO spindle_dev.index: Cluster 2: 504 SPDs, 16 blocks [2026-01-20 10:50:27,346] INFO spindle_dev.index: Using epsilon-net clustering for block 0 [2026-01-20 10:50:27,400] INFO spindle_dev.index: Finished block 0 in 0.05 seconds, found 6 clusters. [2026-01-20 10:50:27,400] INFO spindle_dev.index: Using epsilon-net clustering for block 1 [2026-01-20 10:50:27,476] INFO spindle_dev.index: Finished block 1 in 0.08 seconds, found 14 clusters. [2026-01-20 10:50:27,477] INFO spindle_dev.index: Using epsilon-net clustering for block 2 [2026-01-20 10:50:27,542] INFO spindle_dev.index: Finished block 2 in 0.07 seconds, found 11 clusters. [2026-01-20 10:50:27,543] INFO spindle_dev.index: Using epsilon-net clustering for block 3 [2026-01-20 10:50:27,585] INFO spindle_dev.index: Finished block 3 in 0.04 seconds, found 5 clusters. [2026-01-20 10:50:27,586] INFO spindle_dev.index: Using epsilon-net clustering for block 4 [2026-01-20 10:50:27,621] INFO spindle_dev.index: Finished block 4 in 0.03 seconds, found 3 clusters. [2026-01-20 10:50:27,621] INFO spindle_dev.index: Using epsilon-net clustering for block 5 [2026-01-20 10:50:27,656] INFO spindle_dev.index: Finished block 5 in 0.03 seconds, found 3 clusters. [2026-01-20 10:50:27,657] INFO spindle_dev.index: Using epsilon-net clustering for block 6 [2026-01-20 10:50:27,693] INFO spindle_dev.index: Finished block 6 in 0.04 seconds, found 2 clusters. [2026-01-20 10:50:27,694] INFO spindle_dev.index: Using epsilon-net clustering for block 7 [2026-01-20 10:50:27,725] INFO spindle_dev.index: Finished block 7 in 0.03 seconds, found 2 clusters. [2026-01-20 10:50:27,726] INFO spindle_dev.index: Using epsilon-net clustering for block 8 [2026-01-20 10:50:27,757] INFO spindle_dev.index: Finished block 8 in 0.03 seconds, found 2 clusters. [2026-01-20 10:50:27,758] INFO spindle_dev.index: Using epsilon-net clustering for block 9 [2026-01-20 10:50:27,785] INFO spindle_dev.index: Finished block 9 in 0.03 seconds, found 1 clusters. [2026-01-20 10:50:27,786] INFO spindle_dev.index: Using epsilon-net clustering for block 10 [2026-01-20 10:50:27,850] INFO spindle_dev.index: Finished block 10 in 0.06 seconds, found 2 clusters. [2026-01-20 10:50:27,851] INFO spindle_dev.index: Using epsilon-net clustering for block 11 [2026-01-20 10:50:28,057] INFO spindle_dev.index: Finished block 11 in 0.21 seconds, found 16 clusters. [2026-01-20 10:50:28,057] INFO spindle_dev.index: Using epsilon-net clustering for block 12 [2026-01-20 10:50:28,098] INFO spindle_dev.index: Finished block 12 in 0.04 seconds, found 2 clusters. [2026-01-20 10:50:28,098] INFO spindle_dev.index: Using epsilon-net clustering for block 13 [2026-01-20 10:50:28,131] INFO spindle_dev.index: Finished block 13 in 0.03 seconds, found 1 clusters. [2026-01-20 10:50:28,132] INFO spindle_dev.index: Using epsilon-net clustering for block 14 [2026-01-20 10:50:28,174] INFO spindle_dev.index: Finished block 14 in 0.04 seconds, found 4 clusters. [2026-01-20 10:50:28,175] INFO spindle_dev.index: Using epsilon-net clustering for block 15 [2026-01-20 10:50:28,219] INFO spindle_dev.index: Finished block 15 in 0.04 seconds, found 4 clusters. [2026-01-20 10:50:28,220] INFO spindle_dev.index: Step 2: Build DAG connections between block clusters. [2026-01-20 10:50:28,220] INFO spindle_dev.index: Step 2.1: For each layer order the block-clusters by [2026-01-20 10:50:28,220] INFO spindle_dev.index: Not implemented: ordering block-clusters ? How to order them? [2026-01-20 10:50:28,220] INFO spindle_dev.index: We will use triangle inequality to order clusters. [2026-01-20 10:50:28,221] INFO spindle_dev.index: Step 2.2: Connect block-clusters between layers based on co-occurrence in SPDs. [2026-01-20 10:50:28,225] INFO spindle_dev.index: Check if node global_node_id matches index in nodes list [2026-01-20 10:50:28,226] INFO spindle_dev.index: Step 2.3: Ordering block-clusters within each layer using log-Euclidean distances. [2026-01-20 10:50:28,226] INFO spindle_dev.index: Processing cluster 3 [2026-01-20 10:50:28,227] INFO spindle_dev.index: Building SPD index with epsilon=8.425444379709184 [2026-01-20 10:50:28,227] INFO spindle_dev.index: Step 1: Cluster blocks within each class of SPD matrices. [2026-01-20 10:50:28,233] INFO spindle_dev.index: Cluster 3: 277 SPDs, 15 blocks [2026-01-20 10:50:28,234] INFO spindle_dev.index: Using epsilon-net clustering for block 0 [2026-01-20 10:50:28,249] INFO spindle_dev.index: Finished block 0 in 0.01 seconds, found 1 clusters. [2026-01-20 10:50:28,249] INFO spindle_dev.index: Using epsilon-net clustering for block 1 [2026-01-20 10:50:28,266] INFO spindle_dev.index: Finished block 1 in 0.02 seconds, found 3 clusters. [2026-01-20 10:50:28,267] INFO spindle_dev.index: Using epsilon-net clustering for block 2 [2026-01-20 10:50:28,282] INFO spindle_dev.index: Finished block 2 in 0.01 seconds, found 2 clusters. [2026-01-20 10:50:28,282] INFO spindle_dev.index: Using epsilon-net clustering for block 3 [2026-01-20 10:50:28,301] INFO spindle_dev.index: Finished block 3 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,301] INFO spindle_dev.index: Using epsilon-net clustering for block 4 [2026-01-20 10:50:28,317] INFO spindle_dev.index: Finished block 4 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,318] INFO spindle_dev.index: Using epsilon-net clustering for block 5 [2026-01-20 10:50:28,334] INFO spindle_dev.index: Finished block 5 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,334] INFO spindle_dev.index: Using epsilon-net clustering for block 6 [2026-01-20 10:50:28,356] INFO spindle_dev.index: Finished block 6 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,357] INFO spindle_dev.index: Using epsilon-net clustering for block 7 [2026-01-20 10:50:28,373] INFO spindle_dev.index: Finished block 7 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,374] INFO spindle_dev.index: Using epsilon-net clustering for block 8 [2026-01-20 10:50:28,393] INFO spindle_dev.index: Finished block 8 in 0.02 seconds, found 3 clusters. [2026-01-20 10:50:28,393] INFO spindle_dev.index: Using epsilon-net clustering for block 9 [2026-01-20 10:50:28,415] INFO spindle_dev.index: Finished block 9 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,416] INFO spindle_dev.index: Using epsilon-net clustering for block 10 [2026-01-20 10:50:28,433] INFO spindle_dev.index: Finished block 10 in 0.02 seconds, found 2 clusters. [2026-01-20 10:50:28,434] INFO spindle_dev.index: Using epsilon-net clustering for block 11 [2026-01-20 10:50:28,453] INFO spindle_dev.index: Finished block 11 in 0.02 seconds, found 3 clusters. [2026-01-20 10:50:28,454] INFO spindle_dev.index: Using epsilon-net clustering for block 12 [2026-01-20 10:50:28,544] INFO spindle_dev.index: Finished block 12 in 0.09 seconds, found 8 clusters. [2026-01-20 10:50:28,545] INFO spindle_dev.index: Using epsilon-net clustering for block 13 [2026-01-20 10:50:28,574] INFO spindle_dev.index: Finished block 13 in 0.03 seconds, found 2 clusters. [2026-01-20 10:50:28,574] INFO spindle_dev.index: Using epsilon-net clustering for block 14 [2026-01-20 10:50:28,624] INFO spindle_dev.index: Finished block 14 in 0.05 seconds, found 3 clusters. [2026-01-20 10:50:28,624] INFO spindle_dev.index: Step 2: Build DAG connections between block clusters. [2026-01-20 10:50:28,625] INFO spindle_dev.index: Step 2.1: For each layer order the block-clusters by [2026-01-20 10:50:28,625] INFO spindle_dev.index: Not implemented: ordering block-clusters ? How to order them? [2026-01-20 10:50:28,625] INFO spindle_dev.index: We will use triangle inequality to order clusters. [2026-01-20 10:50:28,625] INFO spindle_dev.index: Step 2.2: Connect block-clusters between layers based on co-occurrence in SPDs. [2026-01-20 10:50:28,628] INFO spindle_dev.index: Check if node global_node_id matches index in nodes list [2026-01-20 10:50:28,628] INFO spindle_dev.index: Step 2.3: Ordering block-clusters within each layer using log-Euclidean distances.
In [18]:
search_cfg = search.SearchConfig(max_results=2, debug=False, max_failed_starts=5, max_failed_paths=10)
In [ ]:
## Take ground truth paths
In [19]:
gt_paths = test.create_ground_truth_paths(dag_dict)
[2026-01-20 10:51:48,735] INFO spindle_dev.test: Built ground-truth paths for 733 SPDs in cluster 0 across 16 blocks. [2026-01-20 10:51:48,904] INFO spindle_dev.test: Built ground-truth paths for 567 SPDs in cluster 1 across 14 blocks. [2026-01-20 10:51:49,058] INFO spindle_dev.test: Built ground-truth paths for 504 SPDs in cluster 2 across 16 blocks. [2026-01-20 10:51:49,103] INFO spindle_dev.test: Built ground-truth paths for 277 SPDs in cluster 3 across 15 blocks.
In [44]:
gt_paths[1]
Out[44]:
{6: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
14: [0, 4, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
16: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
25: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
26: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
27: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
29: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 46, 48, 51],
30: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
31: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
32: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
33: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
34: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
37: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
38: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
39: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
40: [0, 5, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
42: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
43: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
44: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
45: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
46: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
47: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
48: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
53: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
55: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
61: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
62: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
64: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
67: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
69: [1, 6, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
85: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
86: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
87: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
89: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
90: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
91: [0, 3, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
92: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
93: [0, 4, 8, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
94: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
95: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
105: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
106: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
107: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
108: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
109: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
110: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
111: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
112: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
113: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
115: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
118: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
121: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
122: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
123: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
124: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
125: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
126: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
127: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
129: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
132: [0, 5, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
134: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
136: [0, 4, 7, 11, 15, 17, 21, 26, 29, 31, 35, 40, 50, 51],
137: [1, 5, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
138: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
139: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
140: [0, 5, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
141: [0, 2, 9, 11, 14, 17, 21, 23, 27, 31, 33, 43, 48, 51],
142: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
143: [0, 4, 9, 11, 14, 17, 20, 23, 27, 31, 33, 45, 48, 51],
144: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
145: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
146: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
147: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
148: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
149: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
150: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
151: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
152: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
154: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
155: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
156: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
157: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
158: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
159: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
161: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
162: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
165: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
167: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
185: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
197: [0, 3, 7, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
211: [0, 2, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
212: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
213: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
214: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
216: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
217: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
221: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
222: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
223: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
224: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
225: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
226: [0, 6, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
244: [0, 6, 8, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
257: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
262: [0, 4, 9, 12, 15, 19, 21, 25, 28, 32, 36, 42, 49, 52],
296: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
298: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
310: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
312: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
313: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
316: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
351: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
352: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
353: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
354: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
355: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
356: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
357: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
358: [1, 3, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
359: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
360: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
361: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
362: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
363: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
364: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
367: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
368: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
370: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
375: [1, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
391: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
393: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
408: [0, 5, 8, 11, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
409: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
416: [0, 4, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
426: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
427: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
442: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
443: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
444: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
459: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
461: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
462: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
465: [0, 6, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
466: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
467: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
468: [0, 2, 9, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
469: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
470: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
471: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
472: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
473: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
474: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
476: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
478: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
480: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
482: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
483: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
487: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
488: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
489: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
490: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
491: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
493: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
495: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
496: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
497: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
498: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
499: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
500: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
501: [1, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
502: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
503: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
504: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
505: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
506: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
507: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
508: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
509: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
510: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
511: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
512: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
513: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
514: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
515: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
516: [1, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
517: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
518: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
519: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
520: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
521: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
522: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
523: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
524: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
525: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
526: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
528: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
534: [1, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
553: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
560: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
563: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
564: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
565: [0, 2, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
566: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
572: [0, 5, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
574: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
575: [0, 4, 10, 11, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
576: [0, 4, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
577: [0, 4, 10, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
578: [1, 5, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
581: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
583: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
584: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
586: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
587: [0, 2, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
590: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
596: [0, 2, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
601: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
602: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
605: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
634: [0, 3, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
642: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
644: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
646: [0, 5, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
653: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
656: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
672: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
674: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
675: [0, 2, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
676: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
681: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
684: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
685: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
686: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
687: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
688: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
689: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
690: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
691: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
692: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
693: [0, 3, 8, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
694: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
695: [0, 3, 8, 12, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
696: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
697: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
698: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
699: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
700: [1, 6, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
701: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
702: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
703: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
704: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
705: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
706: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
707: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
708: [0, 3, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
709: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
710: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
711: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
712: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
713: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
714: [0, 3, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
715: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
716: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
717: [0, 6, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
718: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
719: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
721: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
722: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
723: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
728: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
729: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
730: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
731: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
732: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
733: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
734: [0, 2, 8, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
740: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
742: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
743: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
744: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
746: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
748: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
749: [1, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
751: [0, 3, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
753: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
754: [0, 4, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
755: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
756: [0, 3, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
757: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
758: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
759: [0, 6, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
761: [0, 3, 7, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
762: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
763: [0, 5, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
764: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
765: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
766: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
767: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
768: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
769: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
780: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
786: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
790: [0, 4, 8, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
791: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
792: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
793: [0, 2, 9, 13, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
794: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
800: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
802: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
808: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
809: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
816: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
818: [1, 2, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
819: [0, 6, 7, 13, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
820: [1, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
879: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
881: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
882: [0, 3, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
884: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
885: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
886: [0, 6, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
887: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
909: [1, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
911: [0, 3, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
912: [0, 3, 9, 11, 16, 17, 20, 23, 27, 31, 33, 44, 48, 51],
914: [0, 3, 8, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
917: [0, 3, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
918: [0, 3, 9, 11, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
919: [0, 4, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
920: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
951: [1, 6, 9, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
983: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
985: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1022: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1023: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1024: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1026: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1027: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1029: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1030: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1032: [0, 5, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1033: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1034: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1035: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1044: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1046: [0, 4, 9, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1047: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1049: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1050: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1051: [0, 4, 7, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1054: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1056: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1058: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1059: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1060: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1061: [0, 5, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1062: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1063: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1064: [1, 2, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1065: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1067: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1068: [1, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1070: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1071: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1072: [0, 3, 9, 12, 16, 18, 21, 24, 30, 32, 34, 39, 49, 53],
1073: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1074: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1075: [1, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1076: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1078: [0, 6, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1079: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1081: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1082: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1083: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1084: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1085: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1086: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1087: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1088: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1089: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1090: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1091: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1092: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1093: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1094: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1096: [1, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1097: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1098: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1099: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1100: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1101: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1102: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1103: [0, 4, 10, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1104: [0, 3, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1105: [0, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1106: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1107: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1108: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1109: [0, 6, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1110: [1, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1111: [0, 4, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1112: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1113: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1114: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1115: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1116: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1125: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1126: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1127: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1130: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1140: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1145: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1147: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1149: [0, 6, 7, 12, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1151: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 37, 41, 48, 51],
1152: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1153: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1154: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1155: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1156: [0, 3, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1157: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1158: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1161: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1163: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1164: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1167: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1168: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1169: [0, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1170: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1171: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1173: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1174: [0, 2, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1176: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1177: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1178: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1179: [0, 3, 9, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1180: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1181: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1182: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1183: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1184: [0, 5, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1185: [0, 3, 9, 11, 14, 17, 20, 23, 27, 31, 33, 47, 48, 51],
1186: [0, 4, 10, 11, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1187: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1188: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1189: [0, 4, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1191: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1192: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1193: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1195: [0, 5, 8, 11, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1197: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1213: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1215: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1223: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1227: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1229: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1230: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1232: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1233: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1234: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1235: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1238: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1241: [0, 4, 9, 11, 14, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1250: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1251: [0, 4, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1252: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1253: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1254: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1255: [0, 2, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1268: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1285: [0, 5, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1286: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1287: [0, 6, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1289: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1290: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1301: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1302: [0, 6, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1304: [0, 4, 10, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1306: [0, 4, 10, 12, 16, 19, 21, 23, 27, 31, 33, 38, 48, 51],
1307: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1317: [0, 3, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1318: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1319: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1324: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1325: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1326: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1327: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1328: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1329: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1330: [1, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1332: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1333: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1334: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1335: [0, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1336: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1338: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1339: [0, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1340: [0, 3, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1341: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1342: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1343: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1344: [0, 4, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1345: [0, 4, 9, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1346: [1, 4, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1347: [0, 4, 8, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1348: [0, 4, 9, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1350: [0, 4, 7, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1352: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1353: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1354: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1355: [0, 4, 8, 12, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1356: [1, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1358: [0, 6, 8, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1359: [0, 3, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1360: [1, 6, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1362: [1, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1368: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1371: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1372: [1, 3, 10, 12, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1376: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1429: [0, 4, 7, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1464: [0, 3, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1466: [0, 4, 8, 12, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1469: [0, 3, 8, 12, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1471: [0, 5, 9, 12, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1472: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1474: [0, 6, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1492: [0, 6, 10, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1544: [0, 4, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1545: [0, 4, 7, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1575: [0, 4, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1590: [0, 4, 10, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1667: [1, 3, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1679: [0, 2, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1687: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1689: [0, 4, 10, 12, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1720: [0, 3, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1725: [0, 4, 10, 13, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1744: [0, 5, 9, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1745: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1820: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1821: [0, 6, 7, 12, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1823: [0, 4, 9, 11, 15, 17, 22, 23, 27, 31, 33, 38, 48, 51],
1846: [0, 6, 7, 12, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1847: [0, 2, 9, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1901: [0, 6, 8, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1905: [0, 4, 10, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1914: [0, 4, 10, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1925: [0, 4, 10, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1927: [0, 4, 10, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1928: [0, 4, 10, 13, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1931: [0, 3, 8, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1949: [0, 3, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1951: [0, 3, 7, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1954: [0, 4, 8, 11, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1985: [0, 3, 10, 12, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
1988: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
1993: [0, 4, 8, 13, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2005: [0, 2, 8, 13, 16, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2007: [0, 3, 8, 13, 16, 19, 20, 23, 27, 31, 33, 38, 48, 51],
2016: [0, 4, 10, 13, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2019: [0, 4, 7, 13, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2028: [1, 5, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2030: [1, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2059: [0, 2, 8, 11, 15, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2065: [0, 2, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51],
2067: [0, 4, 7, 11, 14, 17, 20, 23, 27, 31, 33, 38, 48, 51]}
In [53]:
seed = 40
rng = np.random.default_rng(seed)
all_indices = np.arange(len(data.spd_matrices))
valid_clusters = list(dag_dict.keys())
mask = np.isin(data.labels, valid_clusters)
candidate_indices = all_indices[mask]
n_queries = 2000
query_indices = rng.choice(candidate_indices, size=n_queries, replace=False)
In [54]:
1518 in gt_paths[0]
Out[54]:
True
In [63]:
# assign cluster
\
query_matrices = [data.spd_matrices[i] for i in query_indices]
true_clusters = [int(data.labels[i]) for i in query_indices]
predicted_clusters = search.assign_clusters_to_new_spds(query_matrices, data)
predicted_clusters_cca = search.assign_clusters_supervised_cca(query_matrices, data)
/panfs/accrepfs.vampire/home/sarkah1/miniforge3/envs/spatial/lib/python3.10/site-packages/sklearn/cross_decomposition/_pls.py:308: UserWarning: y residual is constant at iteration 9
warnings.warn(f"y residual is constant at iteration {k}")
The Kernel crashed while executing code in the current cell or a previous cell. Please review the code in the cell(s) to identify a possible cause of the failure. Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. View Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details.
Canceled future for execute_request message before replies were done
Canceled future for execute_request message before replies were done. View Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details.
In [58]:
import pandas as pd
In [60]:
predicted_df = pd.DataFrame({'True Cluster': true_clusters, 'Predicted Cluster': predicted_clusters})
In [ ]:
# Test robustness with noisy query matrices
# Add varying levels of noise and see if we can still retrieve the correct cluster
noise_levels = [0.0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5]
n_queries_noise = 500
results_by_noise = []
for noise_level in noise_levels:
print(f"\nTesting with noise level: {noise_level}")
# Select query indices
seed = 40
rng = np.random.default_rng(seed)
all_indices = np.arange(len(data.spd_matrices))
valid_clusters = list(dag_dict.keys())
mask = np.isin(data.labels, valid_clusters)
candidate_indices = all_indices[mask]
query_indices = rng.choice(candidate_indices, size=n_queries_noise, replace=False)
# Create noisy query matrices
query_matrices_clean = [data.spd_matrices[i] for i in query_indices]
if noise_level > 0:
query_matrices_noisy = [metrics.add_spd_noise(spd, noise_level=noise_level, seed=i)
for i, spd in enumerate(query_matrices_clean)]
else:
query_matrices_noisy = query_matrices_clean
# Get true clusters
true_clusters = [int(data.labels[i]) for i in query_indices]
# Predict clusters using noisy queries
predicted_clusters = search.assign_clusters_to_new_spds(query_matrices_noisy, data)
# Calculate accuracy
correct = sum(1 for t, p in zip(true_clusters, predicted_clusters) if t == p)
accuracy = correct / len(true_clusters)
print(f" Accuracy: {accuracy:.3f} ({correct}/{len(true_clusters)})")
results_by_noise.append({
'noise_level': noise_level,
'accuracy': accuracy,
'correct': correct,
'total': len(true_clusters)
})
In [ ]:
# Create DataFrame and visualize results
noise_df = pd.DataFrame(results_by_noise)
noise_df
In [ ]:
# Plot accuracy vs noise level
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 5))
plt.plot(noise_df['noise_level'], noise_df['accuracy'], marker='o', linewidth=2, markersize=8)
plt.xlabel('Noise Level (std dev in log-eigenvalue space)', fontsize=12)
plt.ylabel('Cluster Assignment Accuracy', fontsize=12)
plt.title('Robustness of Cross-Cluster Search to SPD Noise', fontsize=14)
plt.grid(True, alpha=0.3)
plt.ylim([0, 1.05])
plt.tight_layout()
if save_figure:
plt.savefig("/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/noise_robustness.png", dpi=300)
plt.show()
In [ ]:
# Verify that noisy matrices are still SPD
print("Verifying SPD property of noisy matrices...")
test_noise = 0.2
test_spd = data.spd_matrices[0]
test_noisy = metrics.add_spd_noise(test_spd, noise_level=test_noise, seed=42)
print(f"Original matrix is SPD: {metrics.is_spd(test_spd)}")
print(f"Noisy matrix is SPD: {metrics.is_spd(test_noisy)}")
print(f"\nLog-Euclidean distance between original and noisy: {metrics.log_euclidean_distance(test_spd, test_noisy):.4f}")
In [62]:
# how many correct, vs incorrect
correct = (predicted_df['True Cluster'] == predicted_df['Predicted Cluster']).sum()
incorrect = len(predicted_df) - correct
correct, incorrect
Out[62]:
(np.int64(1977), np.int64(23))
In [51]:
predicted_clusters
Out[51]:
array([0, 1])
In [52]:
true_clusters
Out[52]:
[np.int64(0), np.int64(1)]
In [ ]:
records = []
for q_idx in query_indices:
q_idx = int(q_idx)
true_cluster = int(data.labels[q_idx])
gt_path = gt_paths.get(true_cluster, {}).get(q_idx)
# search all clusters
if gt_path is None:
print(f"Ground-truth path not found for query idx {q_idx} in cluster {cluster_id} with true cluster {true_cluster}.")
for cluster_id in dag_dict.keys():
index_handle = dag_dict[cluster_id]
num_blocks = len(index_handle.sorted_blocks)
epsilon = config.epsilon_dict[cluster_id]
budget = float(epsilon) * float(num_blocks) * 2
q_spd = data.spd_matrices[q_idx]
perm = data.perm_list[cluster_id]
q_spd_perm = q_spd[np.ix_(perm, perm)]
query_block_runs = data.block_dict[cluster_id]
num_blocks = len(query_block_runs) if query_block_runs is not None else 1
results = search.search_index(
index_handle,
q_spd_perm,
[],
query_block_runs,
budget,
config=search_cfg,
)
if cluster_id == true_cluster:
matched = False
matched_leaf = False
matched_budget = None
for path in results.paths:
if path.node_path == gt_path:
matched = True
matched_leaf = True
matched_budget = path.total_distance
break
# If no exact path match, see if any result shares the same leaf.
if not matched:
gt_leaf_node = gt_path[-1]
for path in results.paths:
if path.node_path and path.node_path[-1] == gt_leaf_node:
matched_leaf = True
matched_budget = path.total_distance
break
else:
budget_used = results.paths[0].total_distance if results.paths else None
matched = False
matched_leaf = False
matched_budget = budget_used
record = {
"query": q_idx,
"cluster": int(cluster_id),
"true_cluster": true_cluster,
"matched": matched,
"matched_leaf": matched_leaf,
"budget": matched_budget,
"query_budget": budget
}
records.append(record)
In [49]:
records
Out[49]:
[{'query': 1518,
'cluster': np.int64(0),
'true_cluster': 0,
'matched': True,
'matched_leaf': True,
'budget': np.float64(67.1699586061134),
'query_budget': 242.25026144378666},
{'query': 1518,
'cluster': np.int64(1),
'true_cluster': 0,
'matched': False,
'matched_leaf': False,
'budget': np.float64(65.83725354211727),
'query_budget': 229.7152519673681},
{'query': 1518,
'cluster': np.int64(2),
'true_cluster': 0,
'matched': False,
'matched_leaf': False,
'budget': np.float64(63.27872627339358),
'query_budget': 217.090062943921},
{'query': 1518,
'cluster': np.int64(3),
'true_cluster': 0,
'matched': False,
'matched_leaf': False,
'budget': np.float64(71.34037247799046),
'query_budget': 252.76333139127553},
{'query': 1168,
'cluster': np.int64(0),
'true_cluster': 1,
'matched': False,
'matched_leaf': False,
'budget': np.float64(52.0738057554577),
'query_budget': 242.25026144378666},
{'query': 1168,
'cluster': np.int64(1),
'true_cluster': 1,
'matched': True,
'matched_leaf': True,
'budget': np.float64(54.98122768535769),
'query_budget': 229.7152519673681},
{'query': 1168,
'cluster': np.int64(2),
'true_cluster': 1,
'matched': False,
'matched_leaf': False,
'budget': np.float64(49.97379098495247),
'query_budget': 217.090062943921},
{'query': 1168,
'cluster': np.int64(3),
'true_cluster': 1,
'matched': False,
'matched_leaf': False,
'budget': np.float64(69.60184170781065),
'query_budget': 252.76333139127553}]
In [23]:
test_results = test.run_sanity_search(data, dag_dict, config, search_cfg, max_queries=20, skip_baseline=True)
[2026-01-20 10:54:11,407] INFO spindle_dev.test: Built ground-truth paths for 733 SPDs in cluster 0 across 16 blocks. [2026-01-20 10:54:11,601] INFO spindle_dev.test: Built ground-truth paths for 567 SPDs in cluster 1 across 14 blocks. [2026-01-20 10:54:11,776] INFO spindle_dev.test: Built ground-truth paths for 504 SPDs in cluster 2 across 16 blocks. [2026-01-20 10:54:11,829] INFO spindle_dev.test: Built ground-truth paths for 277 SPDs in cluster 3 across 15 blocks. [2026-01-20 10:54:11,830] INFO spindle_dev.test: Created ground-truth paths for all clusters. [2026-01-20 10:54:11,832] INFO spindle_dev.test: Search config: SearchConfig(max_results=2, max_failed_starts=5, max_failed_paths=10, total_paths_limit=1000, deterministic=DeterministicConfig(seed=0), debug=False) [2026-01-20 10:54:11,835] INFO spindle_dev.test: Running sanity search with 20 queries. 100%|██████████| 20/20 [00:00<00:00, 141.87it/s] [2026-01-20 10:54:11,983] INFO spindle_dev.test: Sanity search: 20 queries, 20 exact path matches, 20 leaf matches, mean search_time=0.0061s
In [26]:
test_results['records']
Out[26]:
[{'query_idx': 1596,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(76.21915377856332),
'search_time': 0.006303895264863968,
'baseline_time': 0},
{'query_idx': 1489,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(68.84017167642352),
'search_time': 0.005631420761346817,
'baseline_time': 0},
{'query_idx': 417,
'cluster_id': 2,
'budget': 162.81754720794075,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(35.77087216009459),
'search_time': 0.007895773276686668,
'baseline_time': 0},
{'query_idx': 2022,
'cluster_id': 3,
'budget': 189.57249854345665,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(58.89973718838964),
'search_time': 0.004101822152733803,
'baseline_time': 0},
{'query_idx': 1746,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(69.85056062235579),
'search_time': 0.005463402718305588,
'baseline_time': 0},
{'query_idx': 1579,
'cluster_id': 2,
'budget': 162.81754720794075,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(44.78088828636458),
'search_time': 0.007192153483629227,
'baseline_time': 0},
{'query_idx': 266,
'cluster_id': 3,
'budget': 189.57249854345665,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(60.54968833338795),
'search_time': 0.003784187138080597,
'baseline_time': 0},
{'query_idx': 184,
'cluster_id': 3,
'budget': 189.57249854345665,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(64.73309025669386),
'search_time': 0.0037744510918855667,
'baseline_time': 0},
{'query_idx': 1351,
'cluster_id': 2,
'budget': 162.81754720794075,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(63.28077097417226),
'search_time': 0.006838075816631317,
'baseline_time': 0},
{'query_idx': 1066,
'cluster_id': 2,
'budget': 162.81754720794075,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(37.81919106212044),
'search_time': 0.007918491959571838,
'baseline_time': 0},
{'query_idx': 894,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(78.66577236263392),
'search_time': 0.0054337382316589355,
'baseline_time': 0},
{'query_idx': 177,
'cluster_id': 3,
'budget': 189.57249854345665,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(70.05341476942334),
'search_time': 0.00356966070830822,
'baseline_time': 0},
{'query_idx': 937,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(53.718565508720516),
'search_time': 0.005697328597307205,
'baseline_time': 0},
{'query_idx': 1774,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(81.58227409923433),
'search_time': 0.005186652764678001,
'baseline_time': 0},
{'query_idx': 195,
'cluster_id': 2,
'budget': 162.81754720794075,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(62.39276053351604),
'search_time': 0.0072003863751888275,
'baseline_time': 0},
{'query_idx': 1525,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(67.57255818140675),
'search_time': 0.005431702360510826,
'baseline_time': 0},
{'query_idx': 1632,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(62.51784634954607),
'search_time': 0.005800047889351845,
'baseline_time': 0},
{'query_idx': 906,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(59.1232739923123),
'search_time': 0.005654977634549141,
'baseline_time': 0},
{'query_idx': 1442,
'cluster_id': 0,
'budget': 181.68769608283998,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(65.54994210044126),
'search_time': 0.005470195785164833,
'baseline_time': 0},
{'query_idx': 1090,
'cluster_id': 1,
'budget': 172.28643897552607,
'matched_gt': True,
'matched_leaf': True,
'matched_budget': np.float64(56.012312035749474),
'search_time': 0.014269845560193062,
'baseline_time': 0}]
In [181]:
fig = plotting.visualize_block_dag_sankey_scaled(dag_dict[2].nodes, height=800, thickness=20.0)
fig
In [24]:
#fig.write_image("/data/sarkar_lab/Projects/spindle_dev/ISMB_notebook/figures/breast_cluster_0_block_dag_sankey.png", engine="kaleido", width=2000, height=1200, scale=2)
In [25]:
import pandas as pd
def get_dag_stats(dag_dict):
dag_stats = []
for cluster_id, cluster_dag in dag_dict.items():
block_to_node = cluster_dag.block_to_node_indices
for node in cluster_dag.nodes:
num_spds = len(node.metadata.members)
block_size = node.metadata.mean.shape[0]
block_id = node.block_index
radius = node.metadata.radius
#print(f"Cluster_id {cluster_id} Node id {node.global_node_id} Block {block_id}: {num_spds} spds")
dag_stats.append({
"cluster_id": cluster_id,
"node_id": node.global_node_id,
"block_id": block_id,
"block_cluster_id": node.block_cluster_id,
"num_spds": num_spds,
"radius": radius,
"block_size": block_size
})
dag_stats_df = pd.DataFrame(dag_stats)
return dag_stats_df
In [26]:
dag_stats_df = get_dag_stats(dag_dict)
In [117]:
importlib.reload(index)
Out[117]:
<module 'spindle_dev.index' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/index.py'>
In [118]:
score_list = []
for cluster_id in set(data.labels):
cluster_dag = dag_dict[cluster_id]
cluster_score = index.score_nodes_from_a_cluster(data, cluster_id, dag_dict, take_representative_mean=True)
score_list.extend(cluster_score)
100%|██████████| 45/45 [00:00<00:00, 326.93it/s] 100%|██████████| 57/57 [00:00<00:00, 228.34it/s] 100%|██████████| 62/62 [00:00<00:00, 243.52it/s] 100%|██████████| 38/38 [00:00<00:00, 282.96it/s]
In [119]:
node_scores = []
for node_score in score_list:
node_scores.append({
"cluster_id": node_score["cluster_id"],
"block_id": node_score["block_id"],
"node_id": node_score["node_id"],
"node_score": node_score["node_score"],
"num_spds": node_score["num_spds"],
"block_size": node_score["block_size"],
"E": node_score["E"],
"Q": node_score["Q"],
"S": node_score["S"],
"num_modules": len(node_score["modules"])
})
node_score_df = pd.DataFrame(node_scores)
In [121]:
node_score_df.sort_values(by='node_score', ascending=False).head(20)
Out[121]:
| cluster_id | block_id | node_id | node_score | num_spds | block_size | E | Q | S | num_modules | |
|---|---|---|---|---|---|---|---|---|---|---|
| 75 | 1 | 6 | 30 | 0.239966 | 1 | 38 | 1.932651 | 0.254274 | 0.511693 | 3 |
| 146 | 2 | 11 | 44 | 0.165974 | 1 | 49 | 1.627859 | 0.164071 | 0.378574 | 4 |
| 5 | 0 | 1 | 5 | 0.159690 | 91 | 15 | 0.774484 | 0.390674 | 0.472224 | 4 |
| 94 | 1 | 12 | 49 | 0.158631 | 13 | 26 | 1.273377 | 0.231421 | 0.461699 | 4 |
| 74 | 1 | 6 | 29 | 0.156417 | 5 | 38 | 1.526276 | 0.186053 | 0.449176 | 6 |
| 156 | 2 | 13 | 54 | 0.150369 | 2 | 18 | 0.935547 | 0.288826 | 0.443511 | 5 |
| 60 | 1 | 4 | 15 | 0.148257 | 397 | 21 | 0.976389 | 0.243900 | 0.377444 | 7 |
| 145 | 2 | 11 | 43 | 0.148110 | 1 | 49 | 2.225433 | 0.125890 | 0.471341 | 4 |
| 89 | 1 | 12 | 44 | 0.140268 | 543 | 26 | 1.321063 | 0.188080 | 0.435467 | 3 |
| 173 | 3 | 3 | 9 | 0.136184 | 49 | 20 | 0.794279 | 0.282274 | 0.392594 | 4 |
| 80 | 1 | 8 | 35 | 0.132119 | 1 | 27 | 1.412164 | 0.179984 | 0.480190 | 4 |
| 79 | 1 | 8 | 34 | 0.131210 | 2 | 27 | 1.464330 | 0.189470 | 0.527084 | 4 |
| 198 | 3 | 13 | 34 | 0.125699 | 266 | 26 | 0.862653 | 0.251575 | 0.420804 | 3 |
| 201 | 3 | 14 | 37 | 0.113385 | 11 | 36 | 1.961125 | 0.121871 | 0.525596 | 3 |
| 52 | 1 | 1 | 7 | 0.105444 | 91 | 15 | 0.571309 | 0.322258 | 0.427276 | 5 |
| 86 | 1 | 10 | 41 | 0.104808 | 3 | 25 | 1.835981 | 0.124215 | 0.540434 | 3 |
| 131 | 2 | 8 | 29 | 0.104168 | 42 | 15 | 0.792322 | 0.248719 | 0.471405 | 3 |
| 77 | 1 | 7 | 32 | 0.101813 | 3 | 28 | 2.014848 | 0.139933 | 0.638893 | 4 |
| 91 | 1 | 12 | 46 | 0.101234 | 1 | 26 | 1.344811 | 0.142968 | 0.473469 | 5 |
| 97 | 1 | 13 | 52 | 0.100772 | 1 | 16 | 1.536831 | 0.159211 | 0.588153 | 2 |
In [156]:
importlib.reload(plotting)
Out[156]:
<module 'spindle_dev.plotting' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/plotting.py'>
In [217]:
def get_module_info(id):
"""
Return metadata + L_mean/index_handle WITHOUT making any plots.
"""
# Take the top scored node
cluster_id = int(node_score_df.iloc[id]["cluster_id"])
node_id = int(node_score_df.iloc[id]["node_id"])
block_id = int(
dag_stats_df.loc[
(dag_stats_df["cluster_id"] == cluster_id) & (dag_stats_df["node_id"] == node_id),
"block_id",
].values[0]
)
print(f"Top scored node is in cluster {cluster_id}, node {node_id}, block {block_id}")
# Most frequent center (kept for your debug / parity, not strictly needed for L_mean retrieval below)
_ = dag_stats_df.loc[
(dag_stats_df["cluster_id"] == cluster_id) & (dag_stats_df["block_id"] == block_id)
].sort_values(by="num_spds", ascending=False).node_id.values[0]
index_handle = dag_dict[cluster_id]
# Representative log-mean reference for this block
L_mean = index.get_node_log_ref(index_handle, block_id, use_representative_mean=True)
return cluster_id, node_id, block_id, L_mean, index_handle
def _fig_to_rgb_array(fig):
from io import BytesIO
from PIL import Image
# Ensure figure is rendered
try:
fig.canvas.draw()
except:
pass
buf = BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight', pad_inches=0.05)
buf.seek(0)
img = np.array(Image.open(buf))
if img.shape[2] == 4:
# If RGBA, convert to RGB
img = img[..., :3]
buf.close()
return img
def _as_figure(fig_or_tuple):
# Accept fig, or (fig, ax), or (fig, axs), etc.
if isinstance(fig_or_tuple, tuple):
return fig_or_tuple[0]
return fig_or_tuple
def plot_module_heatmap_plus_spatial(id, module_ids=(0, 1), zero_corr_threshold=0.05):
"""
One panel:
- Left: corr heatmap with modules (auto-sized based on L_mean.shape[0])
- Right: spatial module score plots for module_ids (stacked, constant size)
"""
# Get metadata
cluster_id = int(node_score_df.iloc[id]["cluster_id"])
node_id = int(node_score_df.iloc[id]["node_id"])
block_id = int(
dag_stats_df.loc[
(dag_stats_df["cluster_id"] == cluster_id) & (dag_stats_df["node_id"] == node_id),
"block_id",
].values[0]
)
index_handle = dag_dict[cluster_id]
L_mean = index.get_node_log_ref(index_handle, block_id, use_representative_mean=True)
p = int(L_mean.shape[0])
print(f"Creating combined figure for cluster {cluster_id}, node {node_id}, block {block_id}")
# Make the heatmap and embed as image
fig_hm, ax_hm, ordered_genes, gene_to_mod = plotting.plot_corr_heatmap_with_modules(
score_list[id],
show_values=False,
value_fontsize=10,
bold_labels=True,
figsize=(8, 7),
filter_zero_correlation=True,
zero_corr_threshold=zero_corr_threshold,
)
hm_img = _fig_to_rgb_array(fig_hm)
plt.close(fig_hm)
# Build the combined figure
heat_w = max(6.5, 0.25 * p)
heat_h = max(5.0, 0.21 * p)
spatial_w = 5.5
total_w = heat_w + spatial_w
total_h = max(heat_h, 6.5)
fig = plt.figure(figsize=(total_w, total_h), constrained_layout=False)
gs = fig.add_gridspec(
nrows=2, ncols=2,
width_ratios=[heat_w, spatial_w],
height_ratios=[1, 1],
wspace=0.05, hspace=0.10
)
ax_heat = fig.add_subplot(gs[:, 0])
ax_sp0 = fig.add_subplot(gs[0, 1])
ax_sp1 = fig.add_subplot(gs[1, 1])
# Place heatmap image
ax_heat.imshow(hm_img)
ax_heat.set_axis_off()
ax_heat.set_title(f"Cluster {cluster_id} | Node {node_id} | Block {block_id}", fontsize=10, pad=6)
# Create spatial plots directly into axes (NO intermediate figures!)
scores_final = None
for ax_sp, mid in zip([ax_sp0, ax_sp1], module_ids):
# print(f"Getting module {mid} score for cluster {cluster_id}, node {node_id}, block {block_id}")
scores_tiles_wrt_ref, info = index.tile_module_scores_from_reference(
data,
score_list[id]["modules"][mid],
index_handle,
block_id,
L_mean,
cluster_id,
)
spot_score = plotting.assign_module_score_to_spots(data, scores_tiles_wrt_ref)
scores = np.array(list(spot_score.values()))
scores_final = scores
# Plot directly into the axis - no intermediate figure!
plotting.plot_spot_module_scores(
spot_score, tiles, adata,
ax=ax_sp, # Pass the axis directly!
grid=False,
label=f"Module {mid}",
)
ax_sp.set_title(f"Module {mid}", fontsize=10, pad=4)
return scores_final, fig
In [142]:
# This function is deprecated - use plot_module_heatmap_plus_spatial instead
# def get_module_info(id):
# # Take the top scored node
# cluster_id = int(node_score_df.iloc[id]['cluster_id'])
# node_id = int(node_score_df.iloc[id]['node_id'])
# block_id = int(dag_stats_df[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['node_id'] == node_id)]['block_id'].values[0])
# print(f"Top scored node is in cluster {cluster_id}, node {node_id}, block {block_id}")
# fig, ax, ordered_genes, gene_to_mod = plotting.plot_corr_heatmap_with_modules(score_list[id],
# show_values=False, value_fontsize=10, bold_labels=True, figsize=(8,7), filter_zero_correlation=True, zero_corr_threshold=0.05)
# # save
# # fig.savefig(f"/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X/corr_heatmap_cluster{cluster_id}_node_{node_id}.png", dpi=300)
#
# # Find out the most frequent center in the same block
# freq_node_center = dag_stats_df.loc[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['block_id'] == block_id)].sort_values(by='num_spds', ascending=False).node_id.values[0]
# index_handle = dag_dict[cluster_id]
# #L_mean = dag_dict[cluster_id].nodes[freq_node_center].metadata.representative_mean
# L_mean = index.get_node_log_ref(index_handle, block_id, use_representative_mean=True)
# return cluster_id, node_id, block_id, L_mean, index_handle
In [219]:
scores_146, fig_146 = plot_module_heatmap_plus_spatial(146)
fig_146.savefig("/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/module_146_heatmap_spatial.png", dpi=300)
Creating combined figure for cluster 2, node 44, block 11
In [220]:
out_146 = go_score.enrich_modules_with_gseapy(score_list[146]['modules'])
Module 0: enrichment completed with 401 terms Module 1: enrichment completed with 340 terms Module 2: enrichment completed with 270 terms Module 3: enrichment completed with 206 terms
In [ ]:
In [227]:
for module_id, module_genes, res in out_146:
if module_id == 1:
print(module_genes)
fig, axes = go_score.plot_module_enrichment_libraries(
module_id=module_id,
module_genes=module_genes,
res=res,
top_n=7,
ncols=2,
bar_color="#D41515EE", # pick your color
tick_fontsize=10,
label_fontsize=10,
title_fontsize=8,
compact_height_per_term=0.2,
min_fig_h=6,
save_dir="/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/go_enrichment/"
)
if fig is not None:
plt.show()
['ADAM9', 'CCDC6', 'CCND1', 'CD9', 'DSP', 'ELF3', 'EPCAM', 'JUP', 'KRT7', 'KRT8', 'S100A14', 'SERPINA3', 'TACSTD2', 'TRAF4'] ['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
In [158]:
scores_156, fig_156 = plot_module_heatmap_plus_spatial(156)
Creating combined figure for cluster 2, node 54, block 13
Creating combined figure for cluster 2, node 54, block 13
In [175]:
# Create a dictionary of tile_id to block_id
# Clusters do not share tile ids across them
from collections import defaultdict
tile_to_block_dict = {}
cluster_to_tile = defaultdict(set)
for cluster_id, cluster_dag in dag_dict.items():
block_to_node = cluster_dag.block_to_node_indices
for node in cluster_dag.nodes:
member_tile_ids = [tid for tid,_ in node.metadata.members]
for tile_id in member_tile_ids:
if not tile_id in tile_to_block_dict:
tile_to_block_dict[tile_id] = {}
tile_to_block_dict[tile_id][node.block_index] = node.block_cluster_id
cluster_to_tile[cluster_id].add(tile_id)
In [193]:
importlib.reload(plotting)
Out[193]:
<module 'spindle_dev.plotting' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/plotting.py'>
In [196]:
def get_spot_score_dict(block_id, cluster_id, tile_to_block_dict, grid=False):
spot_score = {}
tiles_of_cluster = cluster_to_tile[cluster_id]
for tid in tiles_of_cluster:
if tid not in tile_to_block_dict:
continue
c = tile_to_block_dict[tid][block_id]
t = data.metadata['tiles'][tid]
for id in t.idx:
spot_score[id] = c
fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata, color='discrete', grid=grid)
return fig, ax
In [207]:
fig, ax = get_spot_score_dict(5, 2, tile_to_block_dict, grid=False)
In [208]:
node_score_df.loc[(node_score_df['cluster_id'] == 2) & (node_score_df['block_id'] == 5)]
Out[208]:
| cluster_id | block_id | node_id | node_score | num_spds | block_size | E | Q | S | num_modules | |
|---|---|---|---|---|---|---|---|---|---|---|
| 116 | 2 | 5 | 14 | 0.001398 | 350 | 15 | 1.661521 | 0.010595 | 0.920589 | 2 |
| 117 | 2 | 5 | 15 | 0.003958 | 2 | 15 | 0.563854 | 0.051842 | 0.864585 | 4 |
| 118 | 2 | 5 | 16 | 0.001554 | 22 | 15 | 0.287832 | 0.029581 | 0.817513 | 5 |
| 119 | 2 | 5 | 17 | 0.010833 | 67 | 15 | 0.351048 | 0.100392 | 0.692624 | 3 |
| 120 | 2 | 5 | 18 | 0.007216 | 63 | 15 | 0.419383 | 0.075795 | 0.772996 | 4 |
In [214]:
scores_116, fig_116 = plot_module_heatmap_plus_spatial(116)
Creating combined figure for cluster 2, node 14, block 5
In [215]:
fig_116.savefig("/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/module_heatmap_plus_spatial_node_116.png", dpi=300)
In [ ]:
In [ ]:
scores_102, fig_102 = plot_module_heatmap_plus_spatial()
In [211]:
out = go_score.enrich_modules_with_gseapy(score_list[116]['modules'])
Module 0: enrichment completed with 239 terms Module 1: skipped (only 2 genes)
In [212]:
for module_id, module_genes, res in out:
fig, axes = go_score.plot_module_enrichment_libraries(
module_id=module_id,
module_genes=module_genes,
res=res,
top_n=7,
ncols=2,
bar_color="#D41515EE", # pick your color
tick_fontsize=10,
label_fontsize=8,
title_fontsize=8,
compact_height_per_term=0.2,
min_fig_h=8,
#save_dir="/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X/go_enrichment/"
)
if fig is not None:
plt.show()
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
In [ ]:
In [ ]:
In [168]:
importlib.reload(plotting)
Out[168]:
<module 'spindle_dev.plotting' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/plotting.py'>
In [ ]:
In [165]:
len(tile_to_block_dict)
Out[165]:
2081
In [ ]:
In [166]:
len(tiles)
Out[166]:
2081
In [ ]:
In [162]:
dag_stats_df[['cluster_id', 'block_id', 'block_cluster_id', ]]
Out[162]:
| cluster_id | block_id | block_cluster_id | |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 2 | 0 | 1 | 0 |
| 3 | 0 | 1 | 1 |
| 4 | 0 | 1 | 2 |
| ... | ... | ... | ... |
| 197 | 3 | 12 | 1 |
| 198 | 3 | 13 | 0 |
| 199 | 3 | 13 | 1 |
| 200 | 3 | 14 | 0 |
| 201 | 3 | 14 | 1 |
202 rows × 3 columns
In [ ]:
In [123]:
node_score_df.sort_values(by='node_score', ascending=False).head(20)
Out[123]:
| cluster_id | block_id | node_id | node_score | num_spds | block_size | E | Q | S | num_modules | |
|---|---|---|---|---|---|---|---|---|---|---|
| 75 | 1 | 6 | 30 | 0.239966 | 1 | 38 | 1.932651 | 0.254274 | 0.511693 | 3 |
| 146 | 2 | 11 | 44 | 0.165974 | 1 | 49 | 1.627859 | 0.164071 | 0.378574 | 4 |
| 5 | 0 | 1 | 5 | 0.159690 | 91 | 15 | 0.774484 | 0.390674 | 0.472224 | 4 |
| 94 | 1 | 12 | 49 | 0.158631 | 13 | 26 | 1.273377 | 0.231421 | 0.461699 | 4 |
| 74 | 1 | 6 | 29 | 0.156417 | 5 | 38 | 1.526276 | 0.186053 | 0.449176 | 6 |
| 156 | 2 | 13 | 54 | 0.150369 | 2 | 18 | 0.935547 | 0.288826 | 0.443511 | 5 |
| 60 | 1 | 4 | 15 | 0.148257 | 397 | 21 | 0.976389 | 0.243900 | 0.377444 | 7 |
| 145 | 2 | 11 | 43 | 0.148110 | 1 | 49 | 2.225433 | 0.125890 | 0.471341 | 4 |
| 89 | 1 | 12 | 44 | 0.140268 | 543 | 26 | 1.321063 | 0.188080 | 0.435467 | 3 |
| 173 | 3 | 3 | 9 | 0.136184 | 49 | 20 | 0.794279 | 0.282274 | 0.392594 | 4 |
| 80 | 1 | 8 | 35 | 0.132119 | 1 | 27 | 1.412164 | 0.179984 | 0.480190 | 4 |
| 79 | 1 | 8 | 34 | 0.131210 | 2 | 27 | 1.464330 | 0.189470 | 0.527084 | 4 |
| 198 | 3 | 13 | 34 | 0.125699 | 266 | 26 | 0.862653 | 0.251575 | 0.420804 | 3 |
| 201 | 3 | 14 | 37 | 0.113385 | 11 | 36 | 1.961125 | 0.121871 | 0.525596 | 3 |
| 52 | 1 | 1 | 7 | 0.105444 | 91 | 15 | 0.571309 | 0.322258 | 0.427276 | 5 |
| 86 | 1 | 10 | 41 | 0.104808 | 3 | 25 | 1.835981 | 0.124215 | 0.540434 | 3 |
| 131 | 2 | 8 | 29 | 0.104168 | 42 | 15 | 0.792322 | 0.248719 | 0.471405 | 3 |
| 77 | 1 | 7 | 32 | 0.101813 | 3 | 28 | 2.014848 | 0.139933 | 0.638893 | 4 |
| 91 | 1 | 12 | 46 | 0.101234 | 1 | 26 | 1.344811 | 0.142968 | 0.473469 | 5 |
| 97 | 1 | 13 | 52 | 0.100772 | 1 | 16 | 1.536831 | 0.159211 | 0.588153 | 2 |
In [ ]:
In [97]:
cluster_id, node_id, block_id, L_mean, index_handle = get_module_info(146)
Top scored node is in cluster 2, node 44, block 11 Module of size 20: mean abs corr = 0.2477 Adding module of size 20 with mean abs corr = 0.2477 Module of size 14: mean abs corr = 0.4084 Adding module of size 14 with mean abs corr = 0.4084 Module of size 8: mean abs corr = 0.2126 Adding module of size 8 with mean abs corr = 0.2126 Module of size 7: mean abs corr = 0.2540 Adding module of size 7 with mean abs corr = 0.2540 Using 4 modules for heatmap plotting.
In [101]:
module_id = 1
scores_146, fig_spatial = get_module_score_plot_spatial(146, module_id, cluster_id, node_id, block_id, L_mean, index_handle)
Getting module 1 score for cluster 2, node 44, block 11
In [102]:
cluster_id, node_id, block_id, L_mean, index_handle = get_module_info(145)
Top scored node is in cluster 2, node 43, block 11 Module of size 28: mean abs corr = 0.2114 Adding module of size 28 with mean abs corr = 0.2114 Module of size 18: mean abs corr = 0.5462 Adding module of size 18 with mean abs corr = 0.5462 Module of size 2: mean abs corr = 0.5234 Adding module of size 2 with mean abs corr = 0.5234 Using 3 modules for heatmap plotting.
In [ ]:
In [103]:
module_id = 1
scores_145, fig_spatial = get_module_score_plot_spatial(145, module_id, cluster_id, node_id, block_id, L_mean, index_handle)
Getting module 1 score for cluster 2, node 43, block 11
In [113]:
plt.scatter(scores_145, scores_146, s=1, alpha=0.1)
plt.ylabel(f"Cluster{cluster_id} Module 1 Score in Node 43")
plt.xlabel(f"Cluster{cluster_id} Module 1 Score in Node 44")
Out[113]:
Text(0.5, 0, 'Cluster2 Module 1 Score in Node 44')
In [108]:
plt.hist(scores_145, bins=50, alpha=0.5);
plt.hist(scores_146, bins=50, alpha=0.5);
In [ ]:
In [89]:
#id = node_score_df.sort_values(by='node_score', ascending=False).index[1]
id = 146
# Take the top scored node
cluster_id = int(node_score_df.iloc[id]['cluster_id'])
node_id = int(node_score_df.iloc[id]['node_id'])
block_id = int(dag_stats_df[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['node_id'] == node_id)]['block_id'].values[0])
print(f"Top scored node is in cluster {cluster_id}, node {node_id}, block {block_id}")
fig, ax, ordered_genes, gene_to_mod = plotting.plot_corr_heatmap_with_modules(score_list[id],
show_values=False, value_fontsize=10, bold_labels=True, figsize=(8,7), filter_zero_correlation=True, zero_corr_threshold=0.05)
# save
fig.savefig(f"/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X/corr_heatmap_cluster{cluster_id}_node_{node_id}.png", dpi=300)
# Find out the most frequent center in the same block
freq_node_center = dag_stats_df.loc[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['block_id'] == block_id)].sort_values(by='num_spds', ascending=False).node_id.values[0]
index_handle = dag_dict[cluster_id]
#L_mean = dag_dict[cluster_id].nodes[freq_node_center].metadata.representative_mean
L_mean = index.get_node_log_ref(index_handle, block_id, use_representative_mean=True)
Top scored node is in cluster 2, node 44, block 11 Module of size 20: mean abs corr = 0.2477 Adding module of size 20 with mean abs corr = 0.2477 Module of size 14: mean abs corr = 0.4084 Adding module of size 14 with mean abs corr = 0.4084 Module of size 8: mean abs corr = 0.2126 Adding module of size 8 with mean abs corr = 0.2126 Module of size 7: mean abs corr = 0.2540 Adding module of size 7 with mean abs corr = 0.2540 Using 4 modules for heatmap plotting.
In [ ]:
In [90]:
importlib.reload(plotting)
Out[90]:
<module 'spindle_dev.plotting' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/plotting.py'>
In [91]:
importlib.reload(index)
Out[91]:
<module 'spindle_dev.index' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/index.py'>
In [92]:
# Differential LE score for all tiles for module 2
module_id = 1
scores_tiles_wrt_ref, info = index.tile_module_scores_from_reference(data, score_list[id]['modules'][module_id], index_handle, block_id, L_mean, cluster_id)
# assign module scores to spots
spot_score = plotting.assign_module_score_to_spots(data, scores_tiles_wrt_ref)
scores = np.array(list(spot_score.values()))
# fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
# # save
# fig.savefig(f"/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/spatial_module{module_id}_score_cluster{cluster_id}_node{node_id}.png", dpi=300)
In [93]:
importlib.reload(plotting)
Out[93]:
<module 'spindle_dev.plotting' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/plotting.py'>
In [94]:
fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
In [88]:
fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
In [71]:
from spindle_dev import go_score
In [ ]:
out = go_score.enrich_modules_with_gseapy(score_list[146]['modules'])
Module 0: enrichment completed with 401 terms Module 1: enrichment completed with 340 terms Module 2: enrichment completed with 270 terms Module 3: enrichment completed with 206 terms
In [76]:
for module_id, module_genes, res in out:
fig, axes = go_score.plot_module_enrichment_libraries(
module_id=module_id,
module_genes=module_genes,
res=res,
top_n=7,
ncols=2,
bar_color="#D41515EE", # pick your color
tick_fontsize=10,
label_fontsize=8,
title_fontsize=8,
compact_height_per_term=0.2,
min_fig_h=8,
#save_dir="/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X/go_enrichment/"
)
if fig is not None:
plt.show()
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
In [39]:
from spindle_dev import index
importlib.reload(index)
Out[39]:
<module 'spindle_dev.index' from '/data/sarkar_lab/Projects/spindle_dev/src/spindle_dev/index.py'>
In [40]:
import pandas as pd
In [41]:
cluster_id = 0
index_handle = dag_dict[cluster_id]
node_rows, per_node_tile_scores, tile_z = index.node_scores_from_reference(data, index_handle, cluster_id, representative_mean=True)
In [42]:
import pandas as pd
In [58]:
node_df = pd.DataFrame(node_rows).sort_values('z_scale', ascending=False)
In [60]:
node_score_df.loc[(node_score_df.cluster_id == cluster_id) & (node_score_df.node_id == 21)]
Out[60]:
| cluster_id | node_id | node_score | num_spds | block_size | E | Q | S | num_modules | |
|---|---|---|---|---|---|---|---|---|---|
| 21 | 0 | 21 | 0.052074 | 659 | 15 | 0.758758 | 0.153221 | 0.552082 | 3 |
In [59]:
node_df.head(10)
Out[59]:
| block_id | cluster_id | node_id | node_index | prevalence | median_dist | mad_dist | quality | interest | k_genes | z_median | z_mad | z_scale | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 21 | 7 | 0 | 21 | 21 | 1.0 | 3.609869 | 1.693648 | 0.004974 | 0.004974 | 15 | 3.609869 | 1.693648 | 2.511002 |
| 23 | 8 | 0 | 23 | 23 | 1.0 | 3.275327 | 1.256047 | 0.010766 | 0.010766 | 15 | 3.275327 | 1.256047 | 1.862216 |
| 16 | 5 | 0 | 16 | 16 | 1.0 | 3.077620 | 1.237488 | 0.013365 | 0.013365 | 15 | 3.077620 | 1.237488 | 1.834699 |
| 26 | 9 | 0 | 26 | 26 | 1.0 | 3.195635 | 1.227180 | 0.012000 | 0.012000 | 15 | 3.195635 | 1.227180 | 1.819417 |
| 5 | 1 | 0 | 5 | 5 | 1.0 | 4.973354 | 1.050387 | 0.002421 | 0.002421 | 15 | 4.973354 | 1.050387 | 1.557303 |
| 13 | 4 | 0 | 13 | 13 | 1.0 | 4.723527 | 0.958816 | 0.003406 | 0.003406 | 15 | 4.723527 | 0.958816 | 1.421540 |
| 10 | 3 | 0 | 10 | 10 | 1.0 | 5.474510 | 0.894123 | 0.001715 | 0.001715 | 15 | 5.474510 | 0.894123 | 1.325627 |
| 34 | 11 | 0 | 35 | 35 | 1.0 | 2.700770 | 0.753236 | 0.031619 | 0.031619 | 24 | 2.700770 | 0.753236 | 1.116748 |
| 30 | 10 | 0 | 30 | 30 | 1.0 | 3.227608 | 0.726557 | 0.019175 | 0.019175 | 18 | 3.227608 | 0.726557 | 1.077193 |
| 7 | 2 | 0 | 7 | 7 | 1.0 | 6.275040 | 0.642391 | 0.000990 | 0.000990 | 15 | 6.275040 | 0.642391 | 0.952408 |
In [61]:
id = node_score_df.index[21]
# Take the top scored node
cluster_id = int(node_score_df.iloc[id]['cluster_id'])
node_id = int(node_score_df.iloc[id]['node_id'])
block_id = int(dag_stats_df[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['node_id'] == node_id)]['block_id'].values[0])
print(f"Top scored node is in cluster {cluster_id}, node {node_id}, block {block_id}")
fig, ax, ordered_genes, gene_to_mod = plotting.plot_corr_heatmap_with_modules(score_list[id],
show_values=False, value_fontsize=10, bold_labels=True, figsize=(8,7), filter_zero_correlation=True, zero_corr_threshold=0.05)
# save
fig.savefig(f"/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X/corr_heatmap_cluster{cluster_id}_node_{node_id}.png", dpi=300)
# Find out the most frequent center in the same block
freq_node_center = dag_stats_df.loc[(dag_stats_df['cluster_id'] == cluster_id) & (dag_stats_df['block_id'] == block_id)].sort_values(by='num_spds', ascending=False).node_id.values[0]
index_handle = dag_dict[cluster_id]
#L_mean = dag_dict[cluster_id].nodes[freq_node_center].metadata.representative_mean
L_mean = index.get_node_log_ref(index_handle, block_id, use_representative_mean=True)
Top scored node is in cluster 0, node 21, block 7 Module of size 6: mean abs corr = 0.2030 Adding module of size 6 with mean abs corr = 0.2030 Module of size 5: mean abs corr = 0.2500 Adding module of size 5 with mean abs corr = 0.2500 Module of size 4: mean abs corr = 0.3630 Adding module of size 4 with mean abs corr = 0.3630 Using 3 modules for heatmap plotting.
In [62]:
# Differential LE score for all tiles for module 2
module_id = 2
scores_tiles_wrt_ref, info = index.tile_module_scores_from_reference(data, score_list[id]['modules'][module_id], index_handle, block_id, L_mean, cluster_id)
# assign module scores to spots
spot_score = plotting.assign_module_score_to_spots(data, scores_tiles_wrt_ref)
scores = np.array(list(spot_score.values()))
# fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
# # save
# fig.savefig(f"/data/sarkar_lab/Projects/spindle_dev/results/hbreast_10X_wo_unlabeled/spatial_module{module_id}_score_cluster{cluster_id}_node{node_id}.png", dpi=300)
In [63]:
fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
In [46]:
from collections import defaultdict
def block_dag_edges_consecutive(nodes, block_id_from_members=True):
# group nodes by layer
by_layer = defaultdict(list)
for n in nodes:
by_layer[n.block_index].append(n)
layers = sorted(by_layer.keys())
# stable order within each layer
ordered_by_layer = {
b: sorted(by_layer[b], key=lambda n: (n.order_id, n.global_node_id))
for b in layers
}
# spd_id -> node_id in each layer
spd_to_node_in_layer = {b: {} for b in layers}
for b in layers:
for n in ordered_by_layer[b]:
for spd_id, blk_id in n.metadata.members:
if (not block_id_from_members) or (blk_id == b):
spd_to_node_in_layer[b][spd_id] = n.global_node_id
# edges between consecutive layers only (u -> v) counted by shared SPD ids
edges = [] # list of (u_id, v_id, count_true)
for b0, b1 in zip(layers[:-1], layers[1:]):
m0 = spd_to_node_in_layer[b0]
m1 = spd_to_node_in_layer[b1]
common = set(m0.keys()) & set(m1.keys())
cnt = defaultdict(int)
for spd_id in common:
cnt[(m0[spd_id], m1[spd_id])] += 1
for (u, v), c in cnt.items():
edges.append((u, v, c))
return layers, ordered_by_layer, edges
def block_dag_to_dot(layers, ordered_by_layer, edges, node_label_fn=None, edge_label_fn=None):
"""
node_label_fn(n) -> str
edge_label_fn(count) -> str
"""
if node_label_fn is None:
node_label_fn = lambda n: str(n.global_node_id)
if edge_label_fn is None:
edge_label_fn = lambda c: str(c)
# map node_id -> layer index (for rank)
node_to_layer = {}
for b in layers:
for n in ordered_by_layer[b]:
node_to_layer[n.global_node_id] = b
lines = []
lines.append("digraph G {")
lines.append(' graph [rankdir=TB, splines=true, nodesep=0.25, ranksep=0.5];')
lines.append(' node [shape=circle, fixedsize=true, width=0.28, fontsize=10, penwidth=1];')
lines.append(' edge [fontsize=9, penwidth=1, arrowsize=0.6];')
# --- ranks (each layer on one horizontal row)
for b in layers:
ids = [n.global_node_id for n in ordered_by_layer[b]]
lines.append(f" subgraph cluster_rank_{b} {{")
lines.append(" rank=same;")
# invisible box for rank grouping label if you want later
for nid in ids:
lines.append(f' "{nid}";')
lines.append(" }")
# --- node labels
for b in layers:
for n in ordered_by_layer[b]:
nid = n.global_node_id
label = node_label_fn(n).replace('"', '\\"')
lines.append(f' "{nid}" [label="{label}"];')
# --- edges with labels (counts)
for u, v, c in edges:
elab = edge_label_fn(c).replace('"', '\\"')
lines.append(f' "{u}" -> "{v}" [label="{elab}"];')
lines.append("}")
return "\n".join(lines)
def render_dot(dot_str, out_path, fmt="png"):
"""
Requires:
- system graphviz (dot)
- python package: graphviz (pip install graphviz)
"""
from graphviz import Source
src = Source(dot_str, format=fmt)
# graphviz adds extension automatically; keep clean name
filename = out_path.rsplit(".", 1)[0]
return src.render(filename=filename, cleanup=True)
In [47]:
layers, ordered_by_layer, edges = block_dag_edges_consecutive(dag_dict[0].nodes)
dot = block_dag_to_dot(
layers,
ordered_by_layer,
edges,
node_label_fn=lambda n: f"{n.order_id}", # or cluster id, or short name
edge_label_fn=lambda c: str(c), # true SPD counts
)
# save dot text
with open("block_dag.dot", "w") as f:
f.write(dot)
# render (optional)
render_dot(dot, "block_dag.png", fmt="png")
Out[47]:
'block_dag.png'
In [ ]:
In [ ]:
In [48]:
from graphviz import Source
In [49]:
Source(dot)
Out[49]:
In [ ]: