Skip to content

Commit cf285f1

Browse files
committed
removed unused code and in line comments
1 parent 378a32d commit cf285f1

3 files changed

Lines changed: 157 additions & 175 deletions

File tree

synapse_net/cristae_analysis.py

Lines changed: 55 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ def _voxel_radius_xy(thickness_nm: float, voxel_size: Union[float, Dict[str, flo
3737
return max(1, int(round(thickness_nm / xy_nm)))
3838

3939

40+
def _gap_radius(
41+
voxel_size: Union[float, Dict[str, float]],
42+
membrane_thickness_nm: float,
43+
border_gap_nm: Optional[float],
44+
ndim: int,
45+
) -> int:
46+
"""Border-zone / mesh-trim radius in voxels; ``border_gap_nm`` defaults to ``membrane_thickness_nm``."""
47+
gap_nm = border_gap_nm if border_gap_nm is not None else membrane_thickness_nm
48+
return _voxel_radius(gap_nm, voxel_size, ndim)
49+
50+
4051
def _border_zone(shape: tuple, radius: int) -> np.ndarray:
4152
"""Boolean mask that is True within `radius` voxels of any face of the volume."""
4253
mask = np.zeros(shape, dtype=bool)
@@ -89,7 +100,7 @@ def _surface_mesh(
89100
padded = np.pad(binary.astype(np.float32), pad_width)
90101
verts, faces, _, _ = marching_cubes(padded, level=0.5, spacing=tuple(float(s) for s in sampling))
91102
pad_before = np.array([pw[0] for pw in pad_width], dtype=float)
92-
verts = verts - pad_before * np.asarray(sampling, dtype=float) # padded frame -> unpadded mask frame
103+
verts = verts - pad_before * np.asarray(sampling, dtype=float)
93104
return verts, faces
94105

95106

@@ -152,7 +163,7 @@ def _open_trimmed_mesh(
152163
if mesh is None:
153164
return None
154165
verts, faces = mesh
155-
verts = verts + np.array(lo, dtype=float) * np.asarray(sampling, dtype=float) # -> original frame
166+
verts = verts + np.array(lo, dtype=float) * np.asarray(sampling, dtype=float)
156167
return verts, faces
157168

158169

@@ -189,7 +200,7 @@ def _available_memory_bytes() -> int:
189200
try:
190201
return int(os.sysconf("SC_AVPHYS_PAGES") * os.sysconf("SC_PAGE_SIZE"))
191202
except Exception:
192-
return 4 * 1024 ** 3 # conservative fallback
203+
return 4 * 1024 ** 3
193204

194205

195206
def _bounded_workers(n_jobs: int, per_worker_bytes: int, fraction: float = 0.5) -> int:
@@ -242,6 +253,14 @@ def approximate_membrane(
242253
(and left open there) at mesh time by :func:`_open_trimmed_mesh`, which requires the untrimmed
243254
interior to produce an open cut rather than a fabricated cap.
244255
256+
Implementation notes: ``"slice_2d"`` erodes each Z-slice on the mito XY bbox with a
257+
``membrane_radius`` margin, so the cropped ``border_value=1`` erosion matches eroding the full
258+
slice (empty slices are skipped), then adds Z-caps via a separable Z-only line erosion (which
259+
inspects only the same column, so no XY-shape bleed); ``border_value=1`` leaves ends clipped by a
260+
volume Z-face uncapped, and the ``border_gap`` removal clears anything near a face, so only true
261+
ends are capped. ``"shell_3d"`` erodes the *merged* binary in each instance's padded bbox so
262+
instances that share a boundary are handled together.
263+
245264
Args:
246265
mito_segmentation: Instance label array (background = 0).
247266
voxel_size: Voxel size in nm — scalar or dict with "z"/"y"/"x" keys.
@@ -268,7 +287,6 @@ def approximate_membrane(
268287
mito_binary = mito_segmentation > 0
269288

270289
if membrane_mode == "shell_3d":
271-
# Full 3D erosion → single connected shell (incl. Z-caps), per instance on its padded bbox.
272290
sampling = _to_sampling(voxel_size, ndim)
273291
k = max(1, int(round(float(membrane_thickness_nm) / float(np.mean(sampling)))))
274292
struct = np.ones((3,) * ndim, dtype=bool)
@@ -280,15 +298,12 @@ def approximate_membrane(
280298
slice(max(0, bbox[i] - k), min(mito_segmentation.shape[i], bbox[i + ndim] + k))
281299
for i in range(ndim)
282300
)
283-
sub = mito_binary[sl] # merged mito (all instances in the crop) → shared boundaries
301+
sub = mito_binary[sl]
284302
eroded = binary_erosion(sub, structure=struct, iterations=k, border_value=1)
285303
cur = mito_segmentation[sl] == prop.label
286304
membrane_mask[sl] |= cur & ~eroded
287305
lumen_mask[sl] |= cur & eroded
288306
elif ndim == 3:
289-
# Per-Z-slice 2D erosion (no z-bleed), parallelised over Z. Only the mito XY bounding box
290-
# needs eroding; a `membrane_radius` margin makes the cropped erosion (border_value=1)
291-
# identical to eroding the full slice, and empty slices are skipped.
292307
membrane_radius = _voxel_radius_xy(membrane_thickness_nm, voxel_size)
293308
struct = disk(membrane_radius)
294309
membrane_mask = np.zeros_like(mito_binary)
@@ -306,7 +321,7 @@ def _erode_slice(z):
306321
if not sl.any():
307322
return z, None
308323
eroded = binary_erosion(sl, structure=struct, border_value=1)
309-
return z, (sl & ~eroded, eroded) # (membrane shell, lumen interior)
324+
return z, (sl & ~eroded, eroded)
310325

311326
z_range = range(int(zmin), int(zmax))
312327
if n_jobs == 1:
@@ -322,12 +337,6 @@ def _erode_slice(z):
322337
membrane_mask[z, y0:y1, x0:x1] = mem_sl
323338
lumen_mask[z, y0:y1, x0:x1] = lum_sl
324339

325-
# Z-caps: the per-slice XY erosion never caps a column-end, so a mito that truly ends in Z
326-
# (its top/bottom face, not a clipped volume face) would have no membrane there. Erode
327-
# along Z only (a line structuring element that inspects just the same (y, x) column, so no
328-
# XY-shape bleeds across slices) and treat the removed voxels as membrane. border_value=1
329-
# leaves a clipped end (mito at a volume Z-face) uneroded — and the border_gap suppression
330-
# below clears anything near a face — so only true ends are capped.
331340
k_z = max(1, int(round(float(membrane_thickness_nm) / float(_to_sampling(voxel_size, ndim)[0]))))
332341
z0m, z1m = max(0, int(zmin) - k_z), min(mito_binary.shape[0], int(zmax) + k_z)
333342
sub = mito_binary[z0m:z1m, y0:y1, x0:x1]
@@ -340,8 +349,7 @@ def _erode_slice(z):
340349
membrane_mask = mito_binary & ~eroded
341350
lumen_mask = mito_binary & eroded
342351

343-
gap_nm = border_gap_nm if border_gap_nm is not None else membrane_thickness_nm
344-
gap_radius = _voxel_radius(gap_nm, voxel_size, ndim)
352+
gap_radius = _gap_radius(voxel_size, membrane_thickness_nm, border_gap_nm, ndim)
345353
membrane_mask &= ~_border_zone(mito_segmentation.shape, gap_radius)
346354
if return_lumen:
347355
return membrane_mask.astype(bool), lumen_mask.astype(bool)
@@ -362,6 +370,13 @@ def compute_crista_orientation(
362370
Uses ``bioimage_cpp.filters.structure_tensor_eigenvalues`` (a fast C++ routine). Only the
363371
anisotropy is produced (the principal directions / eigenvectors are not computed).
364372
373+
The structure tensor's outer/integration sigma is ``neighborhood_size_nm`` per axis (in voxels);
374+
the inner (derivative) sigma must be > 0, so a minimal 1-voxel scale is used. Eigenvalues are
375+
non-negative in theory, but the solver emits tiny negatives for near-rank-deficient tensors
376+
(degenerate sheets/tubes), so they are clamped to 0 and the ratio is taken as
377+
``max/min`` over the trailing axis — order-agnostic and sign-safe, so a tiny negative minor
378+
eigenvalue cannot flip the denominator and blow the ratio up.
379+
365380
Args:
366381
crista_mask: Binary crista segmentation.
367382
voxel_size: Voxel size in nm — scalar or dict with "z"/"y"/"x" keys.
@@ -376,15 +391,9 @@ def compute_crista_orientation(
376391
ndim = crista_mask.ndim
377392
sampling = _to_sampling(voxel_size, ndim)
378393

379-
# outer_sigma is the integration scale (per axis, in voxels); inner_sigma is the derivative
380-
# smoothing and must be > 0 (0 is rejected), so use a minimal 1-voxel scale.
381394
outer_sigma = [float(s) for s in (neighborhood_size_nm / sampling)]
382395
inner_sigma = 1.0
383396
evals = structure_tensor_eigenvalues(crista_mask.astype(np.float32), inner_sigma, outer_sigma)
384-
# Structure-tensor eigenvalues are non-negative in theory, but the solver emits tiny negatives for
385-
# near-rank-deficient tensors (degenerate sheets/tubes). Clamp them and take λ_max/λ_min via
386-
# max/min over the trailing axis — order-agnostic and sign-safe, so a tiny negative minor
387-
# eigenvalue can't flip the denominator and blow the ratio up.
388397
evals = np.clip(evals, 0.0, None)
389398
anisotropy = evals.max(axis=-1) / (evals.min(axis=-1) + 1e-10)
390399
return anisotropy.astype(np.float32)
@@ -426,8 +435,6 @@ def _downsampled_orientation_anisotropy(
426435
Mean anisotropy over the downsampled crista region, or NaN if it vanishes when downsampled.
427436
"""
428437
ndim = crista_mask.ndim
429-
# Nearest-neighbour downsample by strided subsampling — keeps the mask binary and needs no float
430-
# conversion (compute_crista_orientation casts internally).
431438
sub = crista_mask[(slice(None, None, factor),) * ndim]
432439
region = sub.astype(bool)
433440
if not region.any():
@@ -482,34 +489,6 @@ def compute_crista_proximity(
482489
return distance_map, summary
483490

484491

485-
def compute_crista_density(
486-
crista_mask: np.ndarray,
487-
mito_mask: np.ndarray,
488-
voxel_size: Union[float, Dict[str, float]],
489-
) -> Dict[str, float]:
490-
"""Volume fraction of crista within a mitochondrion.
491-
492-
Args:
493-
crista_mask: Binary crista segmentation.
494-
mito_mask: Binary or instance mito mask (> 0 = inside mito).
495-
voxel_size: Voxel size in nm — scalar or dict with "z"/"y"/"x" keys.
496-
497-
Returns:
498-
Dict with crista_volume_nm3, mito_volume_nm3, crista_fraction.
499-
"""
500-
sampling = _to_sampling(voxel_size, crista_mask.ndim)
501-
voxel_vol = float(np.prod(sampling))
502-
mito_binary = mito_mask > 0
503-
504-
mito_vol = float(mito_binary.sum()) * voxel_vol
505-
crista_vol = float((crista_mask.astype(bool) & mito_binary).sum()) * voxel_vol
506-
return {
507-
"crista_volume_nm3": crista_vol,
508-
"mito_volume_nm3": mito_vol,
509-
"crista_fraction": crista_vol / mito_vol if mito_vol > 0 else np.nan,
510-
}
511-
512-
513492
# ---------------------------------------------------------------------------
514493
# Contact sites
515494
# ---------------------------------------------------------------------------
@@ -582,26 +561,27 @@ def _junction_matrix_mesh(
582561
sampling: Voxel size per axis (nm), array (z, y, x) order.
583562
vertices: Mesh vertices (n_vertices, 3) in nm (unpadded mask frame).
584563
faces: Mesh triangle indices (n_faces, 3).
585-
n_jobs: 1 = serial, -1 = all cores (forwarded to the C++ solver's thread count).
564+
n_jobs: Forwarded to the C++ solver's ``number_of_threads``: -1/0 map to 0 (the solver's
565+
"use hardware_concurrency"), otherwise that many threads.
586566
587567
Returns:
588568
(n, n) geodesic distance matrix in nm (0 diagonal, NaN for disconnected pairs), or None.
569+
Disconnected pairs come back from the solver as ``+inf`` and are converted to NaN.
589570
"""
590571
verts = np.ascontiguousarray(vertices, dtype=np.float64)
591572
tris = np.ascontiguousarray(faces, dtype=np.int64)
592573
if verts.shape[0] == 0 or tris.shape[0] == 0:
593574
return None
594575
from scipy.spatial import cKDTree
595576

596-
# Mask voxel (z, y, x) sits at index * sampling in the unpadded mesh frame (_surface_mesh).
597577
points = np.asarray(centroids, dtype=float) * sampling
598578
_, vertex_ids = cKDTree(verts).query(points)
599579
vertex_ids = np.atleast_1d(np.asarray(vertex_ids, dtype=np.int64))
600-
n_threads = 0 if n_jobs in (-1, 0) else max(1, int(n_jobs)) # bic: 0 = hardware_concurrency
580+
n_threads = 0 if n_jobs in (-1, 0) else max(1, int(n_jobs))
601581
dm = np.asarray(
602582
geodesic_distances_mesh(verts, tris, vertex_ids, number_of_threads=n_threads), dtype=float
603583
)
604-
dm[~np.isfinite(dm)] = np.nan # disconnected components come back as +inf
584+
dm[~np.isfinite(dm)] = np.nan
605585
np.fill_diagonal(dm, 0.0)
606586
return dm
607587

@@ -651,6 +631,10 @@ def compute_junction_distances(
651631
Notes:
652632
The Clark-Evans expected nearest-neighbour distance uses the standard 2-D planar
653633
approximation ``0.5 * sqrt(A / n)`` with ``A = surface_area_nm2``.
634+
635+
Each junction's nearest-neighbour distance is the smallest distance to a *reachable* other
636+
junction: self (diagonal) and unreachable (NaN) pairs are set to +inf before the per-row
637+
minimum, and rows with no reachable neighbour (min stays +inf) are dropped.
654638
"""
655639
ndim = contact_labels.ndim
656640
sampling = _to_sampling(voxel_size, ndim)
@@ -663,12 +647,10 @@ def compute_junction_distances(
663647
summary["junction_count"] = n
664648
return np.zeros((n, n), dtype=float), summary
665649

666-
# Junction centroids in one labelled reduction (uniform weights → geometric centroid).
667650
centroids = np.atleast_2d(
668651
np.asarray(center_of_mass(contact_labels > 0, labels=contact_labels, index=labels), dtype=float)
669652
)
670653

671-
# Surface geodesic along the supplied eroded-mito mesh (or a mesh built from the membrane).
672654
if mesh_vertices is not None and mesh_faces is not None and len(mesh_faces) > 0:
673655
mesh = (mesh_vertices, mesh_faces)
674656
else:
@@ -678,14 +660,10 @@ def compute_junction_distances(
678660
)
679661

680662
if distance_matrix is None:
681-
# No usable surface mesh (empty membrane / degenerate mesh) → distances are NaN.
682663
summary = dict(_JUNCTION_DISTANCE_NAN)
683664
summary["junction_count"] = n
684665
return np.full((n, n), np.nan, dtype=float), summary
685666

686-
# Nearest-neighbour distance per junction (nearest reachable other junction). Vectorised:
687-
# exclude self (diagonal) and unreachable pairs (NaN) by setting them to +inf, take the row
688-
# minimum, and drop rows with no reachable neighbour (min stays +inf).
689667
dm = distance_matrix.copy()
690668
np.fill_diagonal(dm, np.inf)
691669
dm[~np.isfinite(dm)] = np.inf
@@ -739,7 +717,6 @@ def compute_crista_morphology(
739717
result["cristae_surface_area_nm2"] = _surface_area(crista_mask, sampling)
740718

741719
if method in ("medial_axis", "both"):
742-
# Local maxima of the EDT form the medial axis; 2 × distance there = local thickness.
743720
result["avg_thickness_nm"] = _medial_axis_thickness_nm(crista_mask, sampling)
744721

745722
return result
@@ -785,7 +762,8 @@ def _single_mito_row(
785762
are trimmed/opened at faces where the mito is clipped by the volume boundary: the lumen via
786763
:func:`_open_trimmed_mesh` (trimmed to the certain region and left open, so geodesics do not
787764
shortcut across a cap), the mito surface via ``closed_faces`` (open, so a fabricated cap is not
788-
counted as membrane area).
765+
counted as membrane area). The membrane distance transform is freed before the (memory-heavy)
766+
orientation stage to cap peak memory.
789767
"""
790768
ndim = mito_crop.ndim
791769
touches_border = any(
@@ -800,8 +778,6 @@ def _single_mito_row(
800778
mito_vol = float(mito_local.sum()) * voxel_vol
801779
crista_vol = float(crista_local.sum()) * voxel_vol
802780

803-
# True where the bbox face is at the volume boundary (mito clipped there). The mito outer surface
804-
# leaves these open (no fabricated cap in the area); the lumen mesh is trimmed + opened there.
805781
boundary = np.array(
806782
[[bbox[a] == 0, bbox[a + ndim] == vol_shape[a]] for a in range(ndim)], dtype=bool
807783
)
@@ -824,7 +800,6 @@ def _single_mito_row(
824800
surface_area_nm2=mito_surface, n_jobs=inner_n_jobs,
825801
mesh_vertices=mesh_verts, mesh_faces=mesh_faces,
826802
)
827-
# Free the (potentially large) distance-transform array before the orientation computation.
828803
del membrane_distance, contact_labels_local
829804
else:
830805
contact_summary = {"contact_voxel_count": 0, "crista_junction_count": 0, "contact_volume_nm3": 0.0}
@@ -841,7 +816,7 @@ def _single_mito_row(
841816
crista_orientation_anisotropy = _downsampled_orientation_anisotropy(
842817
crista_local, voxel_size, factor=2
843818
)
844-
else: # exact
819+
else:
845820
anisotropy = compute_crista_orientation(crista_local, voxel_size)
846821
crista_orientation_anisotropy = float(np.mean(anisotropy[crista_local]))
847822
else:
@@ -932,6 +907,16 @@ def compute_mito_crista_statistics(
932907
(``bioimage_cpp.distance.geodesic_distances_mesh``); for a mito with no usable mesh (empty
933908
membrane / degenerate mesh) those columns are NaN.
934909
910+
Implementation notes: each mito is pre-cropped to its bounding box by basic slicing (views, so
911+
cropping is memory-free; only the bbox region is pickled to a process worker). Parallelism is
912+
adaptive and single-level (never oversubscribed): with many mitochondria the work is parallelised
913+
*across* them as loky processes (so the GIL-bound stages scale) with each worker's inner stages
914+
serial and BLAS capped (``inner_max_num_threads=1``); with few mitochondria they run serially and
915+
each mito's junction-distance stage gets all cores while BLAS multithreads the orientation. The
916+
concurrent worker count is additionally capped so the combined per-mito working set (tensor
917+
components + label crops, ~40 bytes/voxel of the largest mito) fits in RAM. Rows are finally
918+
sorted by label for an n_jobs-independent ordering.
919+
935920
Returns:
936921
DataFrame with one row per mito instance:
937922
label | mito_volume_nm3 | crista_volume_nm3 | crista_fraction |
@@ -950,8 +935,6 @@ def compute_mito_crista_statistics(
950935
if method not in ("fast", "exact", "skip"):
951936
raise ValueError(f"method must be 'fast', 'exact', or 'skip', got {method!r}")
952937
if membrane_mask is None:
953-
# Derive the matching lumen alongside the membrane; a caller-supplied membrane keeps its
954-
# (possibly None) lumen_mask and falls back per-instance in _single_mito_row.
955938
membrane_mask, lumen_mask = approximate_membrane(
956939
mito_segmentation, voxel_size, membrane_thickness_nm, border_gap_nm,
957940
n_jobs=n_jobs, membrane_mode=membrane_mode, return_lumen=True,
@@ -962,11 +945,8 @@ def compute_mito_crista_statistics(
962945
voxel_vol = float(np.prod(sampling))
963946
crista_binary = crista_mask.astype(bool)
964947
vol_shape = mito_segmentation.shape
965-
effective_gap_nm = border_gap_nm if border_gap_nm is not None else membrane_thickness_nm
966-
border_radius = _voxel_radius(effective_gap_nm, voxel_size, ndim)
948+
border_radius = _gap_radius(voxel_size, membrane_thickness_nm, border_gap_nm, ndim)
967949

968-
# Pre-crop each mito to its bounding box (basic slicing → views, so this is memory-free;
969-
# for the process path only the bbox region gets pickled, not the whole volume).
970950
tasks = []
971951
for prop in regionprops(mito_segmentation):
972952
bbox = prop.bbox
@@ -988,11 +968,6 @@ def _run(task, inner_n_jobs):
988968
)
989969

990970
n_workers = os.cpu_count() if n_jobs == -1 else max(1, n_jobs)
991-
# Adaptive: many mitochondria → parallelize ACROSS them (processes, so the GIL-bound stages
992-
# actually scale), BLAS capped per worker, inner stages serial. Few mitochondria (e.g. one
993-
# dominant mito) → run them serially but give each mito's junction-distance stage all the
994-
# cores and let BLAS multithread the orientation. Exactly one level of parallelism is ever
995-
# active, so there is no process/thread oversubscription.
996971
across = n_workers > 1 and total >= n_workers
997972

998973
rows = []
@@ -1007,12 +982,8 @@ def _consume(results):
1007982

1008983
if across:
1009984
from joblib import Parallel, delayed, parallel_config
1010-
# Cap concurrent workers so their combined per-mito working set (graph + tensor
1011-
# components + label crops, ~40 bytes/voxel of the largest mito) fits in RAM.
1012985
max_voxels = max(int(task[2].size) for task in tasks)
1013986
across_workers = _bounded_workers(n_jobs, per_worker_bytes=max_voxels * 40)
1014-
# loky processes bypass the GIL; inner_max_num_threads=1 stops each worker's BLAS from
1015-
# oversubscribing against the pool.
1016987
with parallel_config(backend="loky", inner_max_num_threads=1):
1017988
_consume(
1018989
Parallel(n_jobs=across_workers, return_as="generator_unordered")(
@@ -1022,6 +993,5 @@ def _consume(results):
1022993
else:
1023994
_consume(_run(task, n_workers) for task in tasks)
1024995

1025-
# Stable, n_jobs-independent ordering.
1026996
rows.sort(key=lambda row: row["mito_label_id"])
1027997
return pd.DataFrame(rows)

0 commit comments

Comments
 (0)