Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,10 @@ run = [
"git push"
]

[tasks.lint]
run = [
"uv run ty check",
]

[tasks.kernel]
run = "uv run ipython kernel install --name 'scLoop' --user"
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,8 @@ executionEnvironments = [

venvPath = "/home/stanfish/Git/scloop"
venv = ".venv"

[dependency-groups]
dev = [
"ty>=0.0.1a34",
]
58 changes: 55 additions & 3 deletions src/scloop/computing/homology.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# Copyright 2025 Zhiyuan Yu (Heemskerk's lab, University of Michigan)
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from anndata import AnnData
from scipy.sparse import csr_matrix
from sklearn.neighbors import radius_neighbors_graph

from ..data.metadata import ScloopMeta
from ..data.ripser_lib import get_boundary_matrix, ripser
from ..data.ripser_lib import get_boundary_matrix, ripser # type: ignore[import-not-found]
from ..data.types import Diameter_t, IndexListDistMatrix
from ..data.utils import encode_triangles_and_edges
from ..utils.linear_algebra_gf2.m4ri_lib import solve_multiple_gf2 # type: ignore[import-not-found]

if TYPE_CHECKING:
from ..data.containers import BoundaryMatrixD1


def compute_sparse_pairwise_distance(
Expand All @@ -20,6 +26,8 @@ def compute_sparse_pairwise_distance(
thresh: Diameter_t | None = None,
**nei_kwargs,
) -> tuple[csr_matrix, IndexListDistMatrix | None]:
# important, default is binary graph
nei_kwargs.setdefault("mode", "distance")
assert meta.preprocess is not None
assert meta.preprocess.embedding_method is not None
emb = adata.obsm[f"X_{meta.preprocess.embedding_method}"]
Expand Down Expand Up @@ -53,10 +61,16 @@ def compute_persistence_diagram_and_cocycles(
meta: ScloopMeta,
thresh: Diameter_t | None = None,
bootstrap: bool = False,
noise_scale: float = 1e3,
**nei_kwargs,
) -> tuple[list[np.ndarray], list, IndexListDistMatrix | None, csr_matrix]:
) -> tuple[list, list, IndexListDistMatrix | None, csr_matrix]:
sparse_pairwise_distance_matrix, boot_idx = compute_sparse_pairwise_distance(
adata=adata, meta=meta, bootstrap=bootstrap, thresh=thresh, **nei_kwargs
adata=adata,
meta=meta,
bootstrap=bootstrap,
noise_scale=noise_scale,
thresh=thresh,
**nei_kwargs,
)
result = ripser(
distance_matrix=sparse_pairwise_distance_matrix.tocoo(copy=False),
Expand Down Expand Up @@ -90,3 +104,41 @@ def compute_boundary_matrix_data(
triangles, meta.preprocess.num_vertices
)
return result, edge_ids, trig_ids, sparse_pairwise_distance_matrix, vertex_indices


def compute_loop_homological_equivalence(
boundary_matrix_d1: "BoundaryMatrixD1",
loop_mask_a: np.ndarray,
loop_mask_b: np.ndarray,
n_pairs_check: int = 3,
) -> tuple[list, list]:
"""
Parameters
---------
loop_mask_a: np.ndarray
Boolean mask of shape (n_a, n_edges); True where edge (row) is in the loop
loop_mask_b: np.ndarray
Boolean mask of shape (n_b, n_edges)
"""
assert loop_mask_a.shape[1] == boundary_matrix_d1.shape[0]
assert loop_mask_b.shape[1] == boundary_matrix_d1.shape[0]

# in F2, sum is just xor
loop_sums = loop_mask_a[:, None, :] ^ loop_mask_b[None, :, :]
loop_sums = loop_sums.reshape(-1, loop_sums.shape[-1])
if loop_sums.shape[0] == 0:
return [], []
n_pairs_check = min(n_pairs_check, loop_sums.shape[0])

one_idx_b_list = [
np.flatnonzero(loop_sums[i]).astype(int).tolist() for i in range(n_pairs_check)
]
results, solutions = solve_multiple_gf2(
one_ridx_A=boundary_matrix_d1.data[0],
one_cidx_A=boundary_matrix_d1.data[1],
nrow_A=boundary_matrix_d1.shape[0],
ncol_A=boundary_matrix_d1.shape[1],
one_idx_b_list=one_idx_b_list,
)

return results, solutions
2 changes: 1 addition & 1 deletion src/scloop/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .containers import HomologyData
from .ripser_lib import RipserResults, get_boundary_matrix, ripser
from .ripser_lib import RipserResults, get_boundary_matrix, ripser # type: ignore[import-not-found]
13 changes: 5 additions & 8 deletions src/scloop/data/analysis_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
from __future__ import annotations

import numpy as np
import pandas as pd
from pydantic import ConfigDict, Field
from pydantic.dataclasses import dataclass


@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
@dataclass
class BootstrapAnalysis:
loops_eidx_boot: list[list[list[np.ndarray]]] = Field(default_factory=list)
persistence_diagram_boot: list[np.ndarray] = Field(default_factory=list)
matching_df: list[pd.DataFrame] = Field(default_factory=list)
n_booted: int = 0
loop_rank: pd.DataFrame = Field(default_factory=pd.DataFrame)
parameters: dict = Field(default_factory=dict)
num_bootstraps: int = 0
persistence_diagrams: list[list] = Field(default_factory=list)
cocycles: list[list] = Field(default_factory=list)
loop_representatives: list[list[list[list[int]]]] = Field(default_factory=list)


@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
Expand Down
Loading