diff --git a/.vulture_whitelist.py b/.vulture_whitelist.py new file mode 100644 index 0000000..9a42732 --- /dev/null +++ b/.vulture_whitelist.py @@ -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 diff --git a/autoflatten/cli.py b/autoflatten/cli.py index 8669fd6..c6f9af6 100644 --- a/autoflatten/cli.py +++ b/autoflatten/cli.py @@ -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...") diff --git a/autoflatten/core.py b/autoflatten/core.py index ddba877..de42908 100644 --- a/autoflatten/core.py +++ b/autoflatten/core.py @@ -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 @@ -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]] diff --git a/autoflatten/flatten/algorithm.py b/autoflatten/flatten/algorithm.py index 98cb81f..2fb6d1a 100644 --- a/autoflatten/flatten/algorithm.py +++ b/autoflatten/flatten/algorithm.py @@ -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( @@ -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)) diff --git a/autoflatten/flatten/distance.py b/autoflatten/flatten/distance.py index 91f4296..5fdc1af 100644 --- a/autoflatten/flatten/distance.py +++ b/autoflatten/flatten/distance.py @@ -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 @@ -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. @@ -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 ): diff --git a/autoflatten/flatten/energy.py b/autoflatten/flatten/energy.py index 3798789..ce8b864 100644 --- a/autoflatten/flatten/energy.py +++ b/autoflatten/flatten/energy.py @@ -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). diff --git a/autoflatten/tests/test_core.py b/autoflatten/tests/test_core.py index 832d494..5f203bd 100644 --- a/autoflatten/tests/test_core.py +++ b/autoflatten/tests/test_core.py @@ -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 diff --git a/autoflatten/tests/test_freesurfer.py b/autoflatten/tests/test_freesurfer.py index 227c037..37af41c 100644 --- a/autoflatten/tests/test_freesurfer.py +++ b/autoflatten/tests/test_freesurfer.py @@ -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( @@ -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", @@ -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] @@ -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: @@ -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] @@ -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): @@ -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): diff --git a/pyproject.toml b/pyproject.toml index d8082d4..05243fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,15 @@ autoflatten = ["default_templates/*.json"] skip = ".git*" check-hidden = true +[tool.vulture] +paths = ["autoflatten", "scripts", ".vulture_whitelist.py"] +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" @@ -85,4 +94,5 @@ exclude_lines = [ dev = [ "build>=1.3.0", "twine>=6.2.0", + "vulture>=2.14", ]