Skip to content
Open
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
12 changes: 12 additions & 0 deletions .vulture_whitelist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Vulture whitelist: names that look unused but are intentionally kept.

Referenced from ``[tool.vulture] paths`` in pyproject.toml so these
references count as "used" and don't show up in dead-code reports.
"""

# pytest fixtures: looked up by name via dependency injection, never imported.
from autoflatten.tests import conftest

conftest.mock_freesurfer_env
conftest.temp_subject_dir
conftest.no_freesurfer_env
5 changes: 0 additions & 5 deletions autoflatten/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,11 +589,6 @@ def process_hemisphere(
# =============================================================================


def cmd_default(args):
"""Default command: full pipeline (project + flatten)."""
return cmd_run_full_pipeline(args)


def cmd_run_full_pipeline(args):
"""Run the full pipeline: projection + flattening."""
print("Starting Autoflatten Pipeline...")
Expand Down
2 changes: 0 additions & 2 deletions autoflatten/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ def ensure_continuous_cuts(vertex_dict, subject, hemi):
while remaining and end_comp in remaining:
# Find closest remaining component to any connected component
min_dist = float("inf")
best_conn = None
best_remain = None
closest_v1 = None
closest_v2 = None
Expand All @@ -280,7 +279,6 @@ def ensure_continuous_cuts(vertex_dict, subject, hemi):

if dist < min_dist:
min_dist = dist
best_conn = conn_idx
best_remain = remain_idx
closest_v1 = conn_verts[min_idx[0]]
closest_v2 = remain_verts[min_idx[1]]
Expand Down
30 changes: 1 addition & 29 deletions autoflatten/flatten/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ def grad_J_a_fn(uv):
recovery_energy_fn, n_coarse_steps=n_coarse_steps
)

for r_iter in range(recovery_iterations):
for _ in range(recovery_iterations):
r_grad = recovery_grad_fn(uv)
if n_avg > 0:
r_grad = smooth_gradient(
Expand Down Expand Up @@ -2237,31 +2237,3 @@ def save_result(self, uv: np.ndarray, output_path: str) -> None:

if self.config.verbose:
print(f"Saved flattened patch to {output_path}")

def compute_distance_error(self, uv: np.ndarray) -> float:
"""Compute average % distance error for UV coordinates.

Args:
uv: (V, 2) UV coordinates

Returns:
Percentage distance error
"""
uv_jax = jnp.asarray(uv)
return float(
_compute_distance_error_jit(
uv_jax, self.neighbors_jax, self.targets_jax, self.mask_jax
)
)

def count_flipped(self, uv: np.ndarray) -> int:
"""Count flipped triangles.

Args:
uv: (V, 2) UV coordinates

Returns:
Number of flipped triangles
"""
uv_jax = jnp.asarray(uv)
return int(count_flipped_triangles(uv_jax, self.faces_jax))
78 changes: 2 additions & 76 deletions autoflatten/flatten/distance.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Distance computation functions for surface meshes.

Provides two methods for computing geodesic distances:
1. Heat method (igl): More accurate but slower, good for global distances
2. Graph-based Dijkstra: Fast for local k-ring distances
Provides graph-based Dijkstra for geodesic distances. Fast for local
k-ring distances and accurate for small k (the surface is locally flat).

Includes Numba-accelerated implementations for significant speedups:
- K-ring computation: ~20x faster with parallel Numba
Expand All @@ -28,55 +27,6 @@
GRAPH_DISTANCE_CORRECTION = (1 + np.sqrt(2)) / 2


# =============================================================================
# Heat method (accurate, slower)
# =============================================================================


def setup_heat_geodesic(vertices, faces):
"""Precompute heat geodesic solver.

Parameters
----------
vertices : ndarray of shape (N, 3)
Vertex positions
faces : ndarray of shape (F, 3)
Face indices

Returns
-------
HeatGeodesicsData
Object for use with compute_heat_distance
"""
data = igl.HeatGeodesicsData()
igl.heat_geodesics_precompute(vertices, faces.astype(np.int64), data)
return data


def compute_heat_distance(heat_data, source_idx):
"""Compute geodesic distances from a source vertex using heat method.

Parameters
----------
heat_data : HeatGeodesicsData
Precomputed data from setup_heat_geodesic
source_idx : int
Index of source vertex

Returns
-------
ndarray of shape (N,)
Distances from source to all vertices
"""
gamma = np.array([source_idx], dtype=np.int32)
return igl.heat_geodesics_solve(heat_data, gamma)


# =============================================================================
# Graph-based Dijkstra (fast for local distances)
# =============================================================================


def build_mesh_graph(vertices, faces):
"""Build sparse adjacency matrix with edge lengths as weights.

Expand Down Expand Up @@ -482,30 +432,6 @@ def _limited_dijkstra(v, k_ring, graph, correction):
return np.array([found.get(idx, np.inf) / correction for idx in k_ring])


def compute_graph_distance(graph, source_idx, k_ring, correction=None):
"""Compute graph-based distances from source to k-ring neighbors.

Parameters
----------
graph : sparse.csr_matrix
Sparse CSR adjacency matrix from build_mesh_graph
source_idx : int
Index of source vertex
k_ring : ndarray
Array of target vertex indices
correction : float, optional
Correction factor (default: GRAPH_DISTANCE_CORRECTION)

Returns
-------
ndarray
Distances to k_ring vertices
"""
if correction is None:
correction = GRAPH_DISTANCE_CORRECTION
return _limited_dijkstra(source_idx, k_ring, graph, correction)


def compute_kring_geodesic_distances(
vertices, faces, k, correction=None, use_numba=True, n_threads=None, tqdm_position=0
):
Expand Down
35 changes: 0 additions & 35 deletions autoflatten/flatten/energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,41 +144,6 @@ def compute_metric_energy_edges(uv, src, dst, targets, n_vertices):
return jnp.sum(errors)


@jax.jit
def compute_both_energies_edges(
uv, src, dst, edge_targets, n_vertices, faces, original_areas
):
"""Compute both energy components using edge list format.

Parameters
----------
uv : ndarray of shape (V, 2)
Current 2D vertex positions
src : ndarray of shape (E,)
Source vertex indices
dst : ndarray of shape (E,)
Destination vertex indices
edge_targets : ndarray of shape (E,)
Target distances
n_vertices : int
Number of vertices
faces : ndarray of shape (T, 3)
Triangle indices
original_areas : ndarray of shape (T,)
Original 3D triangle areas

Returns
-------
J_d : float
Metric distortion energy
J_a : float
Area energy
"""
J_d = compute_metric_energy_edges(uv, src, dst, edge_targets, n_vertices)
J_a = compute_area_energy(uv, faces, original_areas)
return J_d, J_a


@jax.jit
def compute_metric_energy(uv, neighbors, targets, mask):
"""Compute metric distortion energy J_d (vectorized, JIT-compiled).
Expand Down
2 changes: 1 addition & 1 deletion autoflatten/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import subprocess
import tempfile
from unittest.mock import MagicMock, call, patch
from unittest.mock import MagicMock, patch

import networkx as nx
import numpy as np
Expand Down
14 changes: 7 additions & 7 deletions autoflatten/tests/test_freesurfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ def test_input_patch_not_exists(self, tmp_path, monkeypatch):
hemi = "lh"
surf_dir = tmp_path / "subjects" / subject / "surf"
surf_dir.mkdir(parents=True)
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

with pytest.raises(FileNotFoundError):
run_mris_flatten(
Expand All @@ -409,7 +409,7 @@ def test_output_exists_no_overwrite(self, tmp_path, monkeypatch):
existing_output = output_dir / output_name
existing_output.write_text("old")

monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))
monkeypatch.setattr(
fs,
"_run_command",
Expand Down Expand Up @@ -446,7 +446,7 @@ def test_overwrite_true_creates_and_cleans(self, tmp_path, monkeypatch):
existing_output = output_dir / output_name
existing_output.write_text("old")

monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

def fake_run_command(cmd, cwd, log_path, env=None):
flat = cmd[-1]
Expand Down Expand Up @@ -487,7 +487,7 @@ def test_command_failure_raises(self, tmp_path, monkeypatch):
output_dir = tmp_path / "outdir"
output_dir.mkdir()

monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

def fake_fail(cmd, cwd, log_path, env=None):
with open(log_path, "w") as f:
Expand All @@ -514,7 +514,7 @@ def test_default_output_name(self, tmp_path, monkeypatch):
output_dir = tmp_path / "outdir"
output_dir.mkdir()

monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

def fake_run(cmd, cwd, log_path, env=None):
flat = cmd[-1]
Expand Down Expand Up @@ -614,7 +614,7 @@ def test_run_mris_flatten_preserves_original_surf_dir(self, tmp_path, monkeypatc
output_dir.mkdir()

# Mock _resolve_subject_dir to return our test surf_dir
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

# Mock _run_command to simulate successful mris_flatten
def fake_run_command(cmd, cwd, log_path, env=None):
Expand Down Expand Up @@ -699,7 +699,7 @@ def test_debug_flag_preserves_temp_directory(self, tmp_path, monkeypatch):
temp_dirs = []

# Mock _resolve_subject_dir
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda subj: str(surf_dir))
monkeypatch.setattr(fs, "_resolve_subject_dir", lambda _subj: str(surf_dir))

# Mock _run_command and capture temp directory
def fake_run_command(cmd, cwd, log_path, env=None):
Expand Down
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ autoflatten = ["default_templates/*.json"]
skip = ".git*"
check-hidden = true

[tool.vulture]
paths = ["autoflatten", "scripts", ".vulture_whitelist.py"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The scripts directory is included in the vulture configuration but does not appear to exist in the repository. This may cause vulture to fail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scripts/ does exist — it contains 4 Python files (demo_flatten_animation.py, make_fsaverage_cut_template.py, make_subject_cut_template.py, plot_all_flatmaps.py). Including it in the vulture paths is intentional: it lets vulture see that helper functions like setup_freesurfer, identify_surface_components, and save_json are referenced (only from these scripts), so they don't get flagged as dead. vulture runs to completion with exit code 0 against the current config.


Generated by Claude Code

exclude = ["autoflatten/_version.py"]
min_confidence = 80
sort_by_size = true
# kwargs: backend interface accepts/forwards opts even when this impl ignores them
# trim: kept as no-op for backward compatibility in viz.plot_patch
ignore_names = ["kwargs", "trim"]

[tool.setuptools_scm]
version_file = "autoflatten/_version.py"
local_scheme = "no-local-version"
Expand All @@ -85,4 +94,5 @@ exclude_lines = [
dev = [
"build>=1.3.0",
"twine>=6.2.0",
"vulture>=2.14",
]
Loading