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
20 changes: 18 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ wheels/
*.egg-info
*.so

*.a
*.la
src/scloop/utils/linear_algebra_gf2/include/
src/scloop/utils/linear_algebra_gf2/pkgconfig/

# Virtual environments
.venv

Expand All @@ -16,10 +21,21 @@ wheels/

.claude/
.specify/
.ccb/
.codex
.cursor/
graphify-out/
todo/
*.ipynb
*.ipynb.bak*
.vscode/
*.h5ad
*.csv
*.pdf
*.pkl
*.tar

# Refactor planning
REFACTOR_PLAN.md
/*.md
!/README.md
/*.png
/tmp/
5 changes: 5 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
*.png
*.md
/*_SCVI_*/
/checkpoints/
/examples/
/*data*
24 changes: 24 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pre-commit:
parallel: true
jobs:
- name: ruff
glob: "*.{py,pyi}"
stage_fixed: true
run: uv run --no-sync ruff check --select I --fix {staged_files} && uv run --no-sync ruff format {staged_files}
- name: pyproject-fmt
glob: "pyproject.toml"
stage_fixed: true
run: pyproject-fmt {staged_files} || true

pre-bump:
fail_on_changes: always
jobs:
- name: ruff check
glob: "*.{py,pyi}"
run: uv run --no-sync ruff check --fix {all_files}
- name: ruff format
glob: "*.{py,pyi}"
run: uv run --no-sync ruff format {all_files}
- name: pyproject-fmt
glob: "pyproject.toml"
run: pyproject-fmt {all_files} || true
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[tools]
uv = "latest"
lefthook = "latest"

[env]
CPLUS_INCLUDE_PATH = "{{config_root}}/src/scloop/data"
Expand Down Expand Up @@ -54,6 +55,7 @@ run = [

[tasks.bump]
run = [
"lefthook run pre-bump",
"uv version --bump patch",
"git add uv.lock pyproject.toml",
'git commit -m "new tag"',
Expand Down
86 changes: 58 additions & 28 deletions src/scloop/analyzing/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,14 @@ def run_single_bootstrap(
extra_diameter_homology_equivalence: float = DEFAULT_EXTRA_DIAM_EQUIVALENCE,
filter_column_homology_equivalence: bool = True,
column_trim_method: ColumnTrimMethod = DEFAULT_COLUMN_TRIM_METHOD,
death_scale: float | None = None,
n_neighbors_column_trim: int = DEFAULT_N_NEIGHBORS_COLUMN_TRIM,
full_pairwise_distance_matrix: csr_matrix | None = None,
full_vertex_ids: list[int] | None = None,
reconstruct_on_full_data: bool = False,
candidate_method: Literal["geometric", "image"] = "geometric",
require_homological_equivalence: bool = True,
compute_homotopy_coherence: bool = True,
source_cocycle_bases: list[tuple[float, np.ndarray] | None] | None = None,
**kwargs,
) -> BootstrapResult:
if candidate_method not in {"geometric", "image"}:
Expand Down Expand Up @@ -296,6 +297,7 @@ def run_single_bootstrap(
do_clean_cocycle_region=True,
foreign_chord_mult=foreign_chord_mult,
max_perimeter_mult=max_perimeter_mult,
validate_representatives=False,
)
else:
bootstrap_loop_classes = compute_loop_representatives(
Expand Down Expand Up @@ -323,6 +325,7 @@ def run_single_bootstrap(
bootstrap=True,
foreign_chord_mult=foreign_chord_mult,
max_perimeter_mult=max_perimeter_mult,
validate_representatives=True,
)

if image_homology_result is not None:
Expand Down Expand Up @@ -432,6 +435,8 @@ def run_single_bootstrap(
or target_loop.representatives is None
):
continue
source_loops = source_loop.filter_valid(source_loop.representatives)
target_loops = target_loop.filter_valid(target_loop.representatives)
max_column_diameter = None
if filter_column_homology_equivalence:
if extra_diameter_homology_equivalence < 0:
Expand All @@ -443,28 +448,46 @@ def run_single_bootstrap(
max(source_loop.death, target_loop.death)
+ float(extra_diameter_homology_equivalence) * max_lifetime
)
cocycle_basis_masks = None
if not compute_homotopy_coherence and source_cocycle_bases is not None:
cocycle_basis = source_cocycle_bases[source_idx]
if cocycle_basis is not None:
max_column_diameter, cocycle_basis_masks = cocycle_basis
column_scores = (
source_loop.column_proximity_scores(
original_boundary_matrix_d1,
embedding,
n_neighbors_column_trim,
)
if column_trim_method == "loop_proximity"
and cocycle_basis_masks is None
else None
)
source_fillings = (
source_loop.filter_valid(
source_loop.fillings(
original_boundary_matrix_d1, column_trim_method, column_scores
)
)
if compute_homotopy_coherence
else None
)
match.topological_equivalence = check_homological_equivalence(
source_loops=source_loop.representatives,
target_loops=target_loop.representatives,
source_loops=source_loops,
target_loops=target_loops,
boundary_matrix_d1=original_boundary_matrix_d1,
n_pairs_check=n_pairs_check_equivalence,
with_relaxation=with_relaxation_equivalence,
n_hubs_relaxation=n_hubs_relaxation_equivalence,
max_n_edges_relaxation=max_n_edges_relaxation_equivalence,
max_column_diameter=max_column_diameter,
cocycle_edge_mask=cocycle_edge_masks[source_idx],
compute_homotopy_coherence=compute_homotopy_coherence,
cocycle_basis_masks=cocycle_basis_masks,
source_fillings=source_fillings,
column_trim_method=column_trim_method,
embedding=embedding,
death_scale=death_scale,
column_scores=(
source_loop.column_proximity_scores(
original_boundary_matrix_d1,
embedding,
n_neighbors_column_trim,
)
if column_trim_method == "loop_proximity"
else None
),
column_scores=column_scores,
)
match.boundary_checked = True

Expand Down Expand Up @@ -499,10 +522,6 @@ def run_bootstrap_pipeline(
**kwargs,
) -> list[BootstrapResult]:
results: list[BootstrapResult] = []
_original_deaths = [lc.death for lc in original_loop_classes if lc is not None]
global_death_scale = (
max(_original_deaths) if _original_deaths else meta.bootstrap.threshold_homology
)

full_pairwise_distance_matrix: csr_matrix | None = None
full_vertex_ids: list[int] | None = None
Expand All @@ -516,19 +535,31 @@ def run_bootstrap_pipeline(
)
)

if kwargs.get("column_trim_method", DEFAULT_COLUMN_TRIM_METHOD) == "loop_proximity":
column_trim_method = kwargs.get("column_trim_method", DEFAULT_COLUMN_TRIM_METHOD)
warm_fillings = kwargs.get("require_homological_equivalence", True) and kwargs.get(
"compute_homotopy_coherence", True
)
warm_embedding = None
if column_trim_method == "loop_proximity" and warm_fillings:
assert meta.preprocess is not None
assert meta.preprocess.embedding_method is not None
warm_embedding = np.array(adata.obsm[f"X_{meta.preprocess.embedding_method}"])
for loop_class in original_loop_classes:
if loop_class is not None and loop_class.representatives is not None:
loop_class.column_proximity_scores(
original_boundary_matrix_d1,
warm_embedding,
kwargs.get(
"n_neighbors_column_trim", DEFAULT_N_NEIGHBORS_COLUMN_TRIM
),
)
for loop_class in original_loop_classes:
if loop_class is None or loop_class.representatives is None:
continue
column_scores = (
loop_class.column_proximity_scores(
original_boundary_matrix_d1,
warm_embedding,
kwargs.get("n_neighbors_column_trim", DEFAULT_N_NEIGHBORS_COLUMN_TRIM),
)
if warm_embedding is not None
else None
)
if warm_fillings:
loop_class.fillings(
original_boundary_matrix_d1, column_trim_method, column_scores
)

ExecutorClass = ThreadPoolExecutor

Expand All @@ -542,7 +573,6 @@ def run_bootstrap_pipeline(
meta=meta,
original_loop_classes=original_loop_classes,
original_boundary_matrix_d1=original_boundary_matrix_d1,
death_scale=global_death_scale,
full_pairwise_distance_matrix=full_pairwise_distance_matrix,
full_vertex_ids=full_vertex_ids,
reconstruct_on_full_data=reconstruct_on_full_data,
Expand Down
Loading
Loading