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)
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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
No description has been provided for this image
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']
No description has been provided for this image
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
No description has been provided for this image
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)
No description has been provided for this image
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
No description has been provided for this image
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']
No description has been provided for this image
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.
No description has been provided for this image
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
No description has been provided for this image
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.
No description has been provided for this image
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
No description has been provided for this image
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')
No description has been provided for this image
In [108]:
plt.hist(scores_145, bins=50, alpha=0.5);
plt.hist(scores_146, bins=50, alpha=0.5);
No description has been provided for this image
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.
No description has been provided for this image
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")
No description has been provided for this image
In [88]:
fig, ax = plotting.plot_spot_module_scores(spot_score, tiles, adata,grid=False, figsize=(6,3.5), label="Module 2 Score")
No description has been provided for this image
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']
No description has been provided for this image
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
No description has been provided for this image
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
No description has been provided for this image
['GO_Biological_Process_2023', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020', 'Reactome_2022']
No description has been provided for this image
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.
No description has been provided for this image
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")
No description has been provided for this image
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]:
No description has been provided for this image
In [ ]: