Skip to content

Commit aa58c1d

Browse files
authored
Merge pull request #173 from computational-cell-analytics/cristae-analysis-updates
Cristae analysis updates
2 parents 306ee8e + e446629 commit aa58c1d

10 files changed

Lines changed: 2602 additions & 228 deletions

docs/cristae_analysis.md

Lines changed: 265 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Benchmark compute_mito_crista_statistics: orientation fast vs exact vs skip, serial vs parallel.
2+
3+
Builds a synthetic volume with a configurable number of mitochondria (each filled with parallel
4+
cristae) and times the analysis. The orientation ``method`` is the only stage that differs between
5+
runs: ``"fast"`` computes the anisotropy on a 2x-downsampled crop (the structure tensor is the
6+
dominant cost), ``"exact"`` uses the full resolution, ``"skip"`` omits it. Everything else
7+
(marching-cubes surface areas, mesh geodesic junction distances, EDT thickness/proximity) is
8+
identical, so the script expects ~0% relative error on those columns and reports the orientation
9+
separately (fast is a downsampled, relative-only approximation).
10+
11+
python scripts/cooper/benchmark_cristae_analysis.py
12+
python scripts/cooper/benchmark_cristae_analysis.py --mito 4 --size 160 --voxel_size 1.5
13+
14+
To benchmark on a real tomogram, replace ``build_volume`` with loaders for your mito instance
15+
segmentation and binary cristae mask (e.g. read the .h5/.mrc as in the other cooper scripts) and
16+
pass the matching ``voxel_size``.
17+
"""
18+
19+
import argparse
20+
import time
21+
import tracemalloc
22+
23+
import numpy as np
24+
25+
from synapse_net.cristae_analysis import approximate_membrane, compute_mito_crista_statistics
26+
27+
28+
def build_volume(n_mito, size, gap=4):
29+
"""A row of `n_mito` cube mitochondria, each packed with parallel crista sheets."""
30+
shape = (size, size, size * n_mito)
31+
mito = np.zeros(shape, dtype="uint32")
32+
crista = np.zeros(shape, dtype=bool)
33+
for m in range(n_mito):
34+
x0 = m * size
35+
mito[gap:size - gap, gap:size - gap, x0 + gap:x0 + size - gap] = m + 1
36+
for x in range(x0 + gap + 4, x0 + size - gap - 2, 6):
37+
crista[gap + 2:size - gap - 2, gap + 2:size - gap - 2, x:x + 2] = True
38+
return mito, crista
39+
40+
41+
def main():
42+
parser = argparse.ArgumentParser(description="Benchmark cristae analysis (orientation, serial vs parallel).")
43+
parser.add_argument("--mito", type=int, default=4, help="Number of mitochondria.")
44+
parser.add_argument("--size", type=int, default=140, help="Cube side length (voxels) per mito.")
45+
parser.add_argument("--voxel_size", type=float, default=1.5, help="Voxel size (nm, isotropic).")
46+
args = parser.parse_args()
47+
48+
mito, crista = build_volume(args.mito, args.size)
49+
print(f"volume {mito.shape} ({mito.size / 1e6:.1f} M voxels), {args.mito} mitochondria")
50+
51+
membrane = approximate_membrane(mito, args.voxel_size)
52+
53+
def run(method, n_jobs, track_ram=False, verbose=False):
54+
if track_ram:
55+
tracemalloc.start()
56+
t = time.time()
57+
df = compute_mito_crista_statistics(
58+
crista, mito, args.voxel_size, membrane_mask=membrane,
59+
method=method, n_jobs=n_jobs, verbose=verbose,
60+
)
61+
dt = time.time() - t
62+
peak = None
63+
if track_ram:
64+
peak = tracemalloc.get_traced_memory()[1]
65+
tracemalloc.stop()
66+
return dt, df, peak
67+
68+
def rel_err(col, df_fast, df_exact):
69+
a = df_fast[col].to_numpy(dtype=float)
70+
b = df_exact[col].to_numpy(dtype=float)
71+
mask = np.isfinite(a) & np.isfinite(b) & (np.abs(b) > 0)
72+
if not mask.any():
73+
return float("nan")
74+
return float(np.mean(np.abs(a[mask] - b[mask]) / np.abs(b[mask])))
75+
76+
# --- Timing: exact vs fast (both serial), fast also reports peak RAM and the progress bar. ---
77+
t_exact, df_exact, _ = run("exact", 1)
78+
print(f"exact n_jobs=1 : {t_exact:6.2f} s ({len(df_exact)} rows, "
79+
f"{int(df_exact['crista_junction_count'].sum())} junctions total)")
80+
81+
t_fast, df_fast, peak = run("fast", 1, track_ram=True, verbose=True)
82+
print(f"fast n_jobs=1 : {t_fast:6.2f} s ({len(df_fast)} rows, "
83+
f"{int(df_fast['crista_junction_count'].sum())} junctions total), "
84+
f"peak RAM {peak / 1e6:.0f} MB")
85+
if t_fast > 0:
86+
print(f"fast vs exact speedup : {t_exact / t_fast:.2f}x")
87+
88+
# --- Skip orientation entirely: the fastest mode (structure tensor not run at all). ---
89+
t_skip, df_skip, _ = run("skip", 1)
90+
print(f"skip n_jobs=1 : {t_skip:6.2f} s (orientation column empty)")
91+
if t_skip > 0:
92+
print(f"skip vs exact speedup : {t_exact / t_skip:.2f}x")
93+
94+
# --- Per-mito parallelism on the fast path. ---
95+
t_par, _, _ = run("fast", -1)
96+
print(f"fast n_jobs=-1: {t_par:6.2f} s")
97+
if t_par > 0:
98+
print(f"fast serial vs parallel speedup : {t_fast / t_par:.2f}x")
99+
100+
# --- Accuracy vs exact. Surface / junction / thickness are shared code -> expect ~0%.
101+
# Orientation is the one approximated stage (downsampled) -> reported separately. ---
102+
print("\nrelative error (fast vs exact), mean over mitochondria:")
103+
for col in (
104+
"cristae_surface_area_nm2", "mito_surface_area_nm2",
105+
"avg_thickness_nm", "mean_nn_junction_distance_nm",
106+
):
107+
print(f" {col:<32}: {rel_err(col, df_fast, df_exact):.1%} (expect ~0%)")
108+
print(f" {'crista_orientation_anisotropy':<32}: "
109+
f"{rel_err('crista_orientation_anisotropy', df_fast, df_exact):.1%} "
110+
f"(downsampled approximation, differs by design)")
111+
112+
113+
if __name__ == "__main__":
114+
main()

0 commit comments

Comments
 (0)