|
| 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