bsvae 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {bsvae-0.2.0 → bsvae-0.2.1}/PKG-INFO +10 -5
  2. {bsvae-0.2.0 → bsvae-0.2.1}/README.md +9 -4
  3. {bsvae-0.2.0 → bsvae-0.2.1}/pyproject.toml +1 -1
  4. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/cli.py +242 -15
  5. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/extract_networks.py +221 -14
  6. bsvae-0.2.1/src/bsvae/networks/module_extraction.py +898 -0
  7. bsvae-0.2.0/src/bsvae/networks/module_extraction.py +0 -426
  8. {bsvae-0.2.0 → bsvae-0.2.1}/LICENSE +0 -0
  9. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/__init__.py +0 -0
  10. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/hyperparam.ini +0 -0
  11. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/__init__.py +0 -0
  12. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/latent_analysis.py +0 -0
  13. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/latent_export.py +0 -0
  14. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/main.py +0 -0
  15. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/__init__.py +0 -0
  16. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/decoder.py +0 -0
  17. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/encoder.py +0 -0
  18. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/losses.py +0 -0
  19. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/structured.py +0 -0
  20. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/vae.py +0 -0
  21. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/__init__.py +0 -0
  22. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/utils.py +0 -0
  23. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/ppi_cli.py +0 -0
  24. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/__init__.py +0 -0
  25. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/datasets.py +0 -0
  26. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/evaluate.py +0 -0
  27. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/helpers.py +0 -0
  28. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/initialization.py +0 -0
  29. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/mapping.py +0 -0
  30. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/modelIO.py +0 -0
  31. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/ppi.py +0 -0
  32. {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/training.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsvae
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Biologically structured variational autoencoder for gene expression data with PPI priors.
5
5
  License: GPL-3.0-only
6
6
  License-File: LICENSE
@@ -136,10 +136,15 @@ bsvae-networks export-latents \
136
136
  --output results/exp1/latents.h5ad
137
137
  ```
138
138
 
139
- The extractor writes adjacency matrices, edge lists, and optional heatmaps for
140
- each requested method. By default the decoder-loading cosine similarity
141
- (``w_similarity``) is computed; add other methods with ``--methods``. Latent exports include per-sample ``mu`` and
142
- ``logvar`` as tidy CSV or AnnData files.
139
+ The extractor writes **sparse NPZ** adjacency matrices and **Parquet** edge
140
+ lists by default (``--sparse`` and ``--compress`` are on unless disabled with
141
+ ``--no-sparse``/``--no-compress``). When ``--threshold 0`` with sparse output,
142
+ an adaptive threshold is computed from ``--target-sparsity`` (default 0.01 =
143
+ top 1% of edges). Use ``--quantize`` (default ``int8``) to reduce adjacency
144
+ size. Legacy dense CSV/TSV/NPY outputs are available only with ``--no-sparse``
145
+ or explicit legacy formats. By default the decoder-loading cosine similarity
146
+ (``w_similarity``) is computed; add other methods with ``--methods``. Latent
147
+ exports include per-sample ``mu`` and ``logvar`` as tidy CSV or AnnData files.
143
148
 
144
149
  ---
145
150
 
@@ -104,10 +104,15 @@ bsvae-networks export-latents \
104
104
  --output results/exp1/latents.h5ad
105
105
  ```
106
106
 
107
- The extractor writes adjacency matrices, edge lists, and optional heatmaps for
108
- each requested method. By default the decoder-loading cosine similarity
109
- (``w_similarity``) is computed; add other methods with ``--methods``. Latent exports include per-sample ``mu`` and
110
- ``logvar`` as tidy CSV or AnnData files.
107
+ The extractor writes **sparse NPZ** adjacency matrices and **Parquet** edge
108
+ lists by default (``--sparse`` and ``--compress`` are on unless disabled with
109
+ ``--no-sparse``/``--no-compress``). When ``--threshold 0`` with sparse output,
110
+ an adaptive threshold is computed from ``--target-sparsity`` (default 0.01 =
111
+ top 1% of edges). Use ``--quantize`` (default ``int8``) to reduce adjacency
112
+ size. Legacy dense CSV/TSV/NPY outputs are available only with ``--no-sparse``
113
+ or explicit legacy formats. By default the decoder-loading cosine similarity
114
+ (``w_similarity``) is computed; add other methods with ``--methods``. Latent
115
+ exports include per-sample ``mu`` and ``logvar`` as tidy CSV or AnnData files.
111
116
 
112
117
  ---
113
118
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bsvae"
3
- version = "0.2.0" # dynamically resolved by poetry-dynamic-versioning
3
+ version = "0.2.1" # dynamically resolved by poetry-dynamic-versioning
4
4
  description = "Biologically structured variational autoencoder for gene expression data with PPI priors."
5
5
  readme = "README.md"
6
6
  authors = ["Kynon J Benjamin <kj.benjamin90@gmail.com>"]
@@ -20,8 +20,11 @@ from bsvae.networks.extract_networks import (
20
20
  from bsvae.latent.latent_export import extract_latents, save_latents
21
21
  from bsvae.networks.module_extraction import (
22
22
  compute_module_eigengenes,
23
+ leiden_modules_from_graph,
23
24
  leiden_modules,
24
25
  load_adjacency,
26
+ optimize_resolution_modularity,
27
+ prepare_leiden_graph,
25
28
  save_eigengenes,
26
29
  save_modules,
27
30
  spectral_modules,
@@ -104,7 +107,64 @@ def build_parser() -> argparse.ArgumentParser:
104
107
  module_parser.add_argument("--expr", required=True, help="Expression matrix (genes × samples) for eigengenes")
105
108
  module_parser.add_argument("--output-dir", required=True, help="Directory to write module outputs")
106
109
  module_parser.add_argument("--cluster-method", choices=["leiden", "spectral"], default="leiden")
107
- module_parser.add_argument("--resolution", type=float, default=1.0, help="Leiden resolution parameter")
110
+ module_parser.add_argument("--resolution", type=float, default=1.0, help="Leiden resolution parameter (ignored if --resolutions or --resolution-auto is used)")
111
+ module_parser.add_argument(
112
+ "--resolutions",
113
+ type=float,
114
+ nargs="+",
115
+ help="Multiple resolution values for sweep (e.g., --resolutions 1.0 2.0 5.0 10.0). "
116
+ "Creates separate output subdirectories for each resolution.",
117
+ )
118
+ module_parser.add_argument(
119
+ "--resolution-auto",
120
+ action="store_true",
121
+ help="Automatically select resolution by maximizing modularity (no ground truth needed). "
122
+ "Can be combined with --resolutions to also run fixed resolutions.",
123
+ )
124
+ module_parser.add_argument(
125
+ "--resolution-min",
126
+ type=float,
127
+ default=0.5,
128
+ help="Minimum resolution for auto-optimization search (default: 0.5)",
129
+ )
130
+ module_parser.add_argument(
131
+ "--resolution-max",
132
+ type=float,
133
+ default=15.0,
134
+ help="Maximum resolution for auto-optimization search (default: 15.0)",
135
+ )
136
+ module_parser.add_argument(
137
+ "--resolution-steps",
138
+ type=int,
139
+ default=30,
140
+ help="Number of resolution values to test during auto-optimization (default: 30). "
141
+ "Ignored when --resolution-two-phase is used.",
142
+ )
143
+ module_parser.add_argument(
144
+ "--resolution-two-phase",
145
+ action="store_true",
146
+ help="Use memory-efficient two-phase search: coarse sweep to find ballpark, "
147
+ "then fine sweep in narrower range. Reduces peak memory usage.",
148
+ )
149
+ module_parser.add_argument(
150
+ "--resolution-coarse-steps",
151
+ type=int,
152
+ default=10,
153
+ help="Number of steps in coarse phase of two-phase search (default: 10)",
154
+ )
155
+ module_parser.add_argument(
156
+ "--resolution-fine-steps",
157
+ type=int,
158
+ default=10,
159
+ help="Number of steps in fine phase of two-phase search (default: 10)",
160
+ )
161
+ module_parser.add_argument(
162
+ "--resolution-fine-range",
163
+ type=float,
164
+ default=0.3,
165
+ help="Fraction of original range for fine search, centered on best coarse "
166
+ "resolution (default: 0.3 = 30%% of original range)",
167
+ )
108
168
  module_parser.add_argument("--n-clusters", type=int, help="Number of clusters for spectral clustering")
109
169
  module_parser.add_argument("--n-components", type=int, help="Number of eigenvectors for spectral clustering")
110
170
  module_parser.add_argument("--batch-size", type=int, default=128)
@@ -123,6 +183,11 @@ def build_parser() -> argparse.ArgumentParser:
123
183
  default="w_similarity",
124
184
  help="Network extraction method to run when computing adjacency (default: w_similarity)",
125
185
  )
186
+ module_parser.add_argument(
187
+ "--progress",
188
+ action="store_true",
189
+ help="Show progress bar during resolution auto-optimization",
190
+ )
126
191
 
127
192
  latent_analysis_parser = subparsers.add_parser("latent-analysis", help="Sample-level latent space analysis.")
128
193
  latent_analysis_parser.add_argument("--model-path", required=True, help="Directory with specs.json/model.pt or checkpoint path")
@@ -201,6 +266,94 @@ def _load_covariates(path: str, sample_ids: Sequence[str]) -> pd.DataFrame:
201
266
  return df
202
267
 
203
268
 
269
+ def _run_single_clustering(
270
+ adjacency_df: pd.DataFrame,
271
+ expr_df: pd.DataFrame,
272
+ output_dir: Path,
273
+ resolution: float,
274
+ adjacency_mode: str,
275
+ cluster_method: str,
276
+ n_clusters: Optional[int],
277
+ n_components: Optional[int],
278
+ logger: logging.Logger,
279
+ resolution_label: Optional[str] = None,
280
+ graph=None,
281
+ genes: Optional[Sequence[str]] = None,
282
+ compute_eigengenes: bool = True,
283
+ precomputed_modules: Optional[pd.Series] = None,
284
+ ) -> int:
285
+ """Run clustering at a single resolution and save results.
286
+
287
+ Parameters
288
+ ----------
289
+ precomputed_modules:
290
+ Optional pre-computed module assignments (e.g., from resolution
291
+ optimization) to avoid redundant Leiden computation.
292
+
293
+ Returns the number of modules detected.
294
+ """
295
+ output_dir.mkdir(parents=True, exist_ok=True)
296
+
297
+ if precomputed_modules is not None:
298
+ modules = precomputed_modules
299
+ logger.info(
300
+ "Using precomputed modules (resolution=%.3f, %d modules)",
301
+ resolution,
302
+ modules.nunique(),
303
+ )
304
+ elif cluster_method == "leiden":
305
+ logger.info(
306
+ "Running Leiden clustering (resolution=%.3f, adjacency_mode=%s)",
307
+ resolution,
308
+ adjacency_mode,
309
+ )
310
+ if graph is not None and genes is not None:
311
+ modules = leiden_modules_from_graph(graph, genes, resolution)
312
+ else:
313
+ modules = leiden_modules(
314
+ adjacency_df,
315
+ resolution=resolution,
316
+ adjacency_mode=adjacency_mode,
317
+ )
318
+ else:
319
+ modules = spectral_modules(
320
+ adjacency_df,
321
+ n_clusters=n_clusters,
322
+ n_components=n_components,
323
+ adjacency_mode=adjacency_mode,
324
+ )
325
+
326
+ n_modules = modules.nunique()
327
+
328
+ save_modules(modules, output_dir / "modules.csv")
329
+ if compute_eigengenes:
330
+ eigengenes = compute_module_eigengenes(expr_df, modules)
331
+ save_eigengenes(eigengenes, output_dir / "eigengenes.csv")
332
+ else:
333
+ logger.info("Skipping eigengene computation for resolution %.3f", resolution)
334
+
335
+ # Save resolution metadata for reproducibility
336
+ if cluster_method == "leiden":
337
+ metadata = {
338
+ "resolution": resolution,
339
+ "resolution_label": resolution_label or str(resolution),
340
+ "n_modules": n_modules,
341
+ "adjacency_mode": adjacency_mode,
342
+ }
343
+ metadata_path = output_dir / "clustering_metadata.json"
344
+ import json
345
+ with open(metadata_path, "w") as f:
346
+ json.dump(metadata, f, indent=2)
347
+
348
+ logger.info(
349
+ "Resolution %.3f: %d modules detected, saved to %s",
350
+ resolution,
351
+ n_modules,
352
+ output_dir,
353
+ )
354
+ return n_modules
355
+
356
+
204
357
  def handle_extract_modules(args, logger: logging.Logger) -> None:
205
358
  if not args.adjacency and (not args.model_path or not args.dataset):
206
359
  raise ValueError("Either --adjacency or both --model-path and --dataset must be provided")
@@ -235,26 +388,100 @@ def handle_extract_modules(args, logger: logging.Logger) -> None:
235
388
  # Convert adjacency to DataFrame with gene labels so clustering functions
236
389
  # return modules indexed by gene name instead of numeric indices
237
390
  adjacency_df = pd.DataFrame(adjacency, index=genes, columns=genes)
238
-
391
+ expr_df = load_expression(args.expr)
392
+ leiden_graph = None
393
+ leiden_genes = None
239
394
  if args.cluster_method == "leiden":
240
- logger.info(
241
- "Running Leiden clustering (resolution=%.2f, adjacency_mode=%s)",
242
- args.resolution,
243
- args.adjacency_mode,
244
- )
245
- modules = leiden_modules(
395
+ leiden_graph, leiden_genes = prepare_leiden_graph(
246
396
  adjacency_df,
247
- resolution=args.resolution,
248
397
  adjacency_mode=args.adjacency_mode,
249
398
  )
250
- else:
251
- modules = spectral_modules(adjacency_df, n_clusters=args.n_clusters, n_components=args.n_components)
252
399
 
253
- expr_df = load_expression(args.expr)
254
- eigengenes = compute_module_eigengenes(expr_df, modules)
400
+ # Determine which resolutions to run
401
+ # (resolution, label, output_dir, precomputed_modules)
402
+ resolutions_to_run: List[tuple[float, str, Path, Optional[pd.Series]]] = []
403
+
404
+ # Handle resolution auto-optimization
405
+ if args.resolution_auto and args.cluster_method == "leiden":
406
+ two_phase = getattr(args, "resolution_two_phase", False)
407
+ if two_phase:
408
+ logger.info("Running two-phase resolution auto-optimization (modularity-based)")
409
+ else:
410
+ logger.info("Running resolution auto-optimization (modularity-based)")
411
+ best_res, best_qual, auto_modules = optimize_resolution_modularity(
412
+ adjacency_df,
413
+ adjacency_mode=args.adjacency_mode,
414
+ resolution_min=args.resolution_min,
415
+ resolution_max=args.resolution_max,
416
+ n_steps=args.resolution_steps,
417
+ graph=leiden_graph,
418
+ return_modules=True,
419
+ progress=getattr(args, "progress", False),
420
+ two_phase=two_phase,
421
+ coarse_steps=getattr(args, "resolution_coarse_steps", 10),
422
+ fine_steps=getattr(args, "resolution_fine_steps", 10),
423
+ fine_range_fraction=getattr(args, "resolution_fine_range", 0.3),
424
+ )
425
+ auto_output = Path(args.output_dir) / "res_auto"
426
+ resolutions_to_run.append((best_res, "auto", auto_output, auto_modules))
427
+ logger.info("Auto-selected resolution: %.3f (modularity=%.4f)", best_res, best_qual)
428
+
429
+ # Handle resolution sweep
430
+ if args.resolutions and args.cluster_method == "leiden":
431
+ for res in args.resolutions:
432
+ # Format resolution for directory name (e.g., 1.0 -> "1.0", 10.0 -> "10.0")
433
+ res_label = f"{res:.1f}".replace(".", "_")
434
+ res_output = Path(args.output_dir) / f"res_{res_label}"
435
+ resolutions_to_run.append((res, f"fixed_{res}", res_output, None))
436
+
437
+ # If no sweep or auto, use single resolution (original behavior)
438
+ if not resolutions_to_run:
439
+ resolutions_to_run.append((args.resolution, "single", Path(args.output_dir), None))
440
+
441
+ # Run clustering for each resolution
442
+ results_summary = []
443
+ multiple_resolutions = len(resolutions_to_run) > 1
444
+ for resolution, label, output_dir, precomputed_modules in resolutions_to_run:
445
+ compute_eigengenes = not (label == "auto" and multiple_resolutions)
446
+ n_modules = _run_single_clustering(
447
+ adjacency_df=adjacency_df,
448
+ expr_df=expr_df,
449
+ output_dir=output_dir,
450
+ resolution=resolution,
451
+ adjacency_mode=args.adjacency_mode,
452
+ cluster_method=args.cluster_method,
453
+ n_clusters=args.n_clusters,
454
+ n_components=args.n_components,
455
+ logger=logger,
456
+ resolution_label=label,
457
+ graph=leiden_graph,
458
+ genes=leiden_genes,
459
+ compute_eigengenes=compute_eigengenes,
460
+ precomputed_modules=precomputed_modules,
461
+ )
462
+ results_summary.append({
463
+ "resolution": resolution,
464
+ "label": label,
465
+ "n_modules": n_modules,
466
+ "output_dir": str(output_dir),
467
+ })
468
+
469
+ # Save summary if multiple resolutions were run
470
+ if len(resolutions_to_run) > 1:
471
+ import json
472
+ summary_path = Path(args.output_dir) / "resolution_sweep_summary.json"
473
+ with open(summary_path, "w") as f:
474
+ json.dump(results_summary, f, indent=2)
475
+ logger.info("Resolution sweep summary saved to %s", summary_path)
476
+
477
+ # Also save as TSV for easy viewing
478
+ summary_df = pd.DataFrame(results_summary)
479
+ summary_df.to_csv(
480
+ Path(args.output_dir) / "resolution_sweep_summary.tsv",
481
+ sep="\t",
482
+ index=False,
483
+ )
255
484
 
256
- save_modules(modules, Path(args.output_dir) / "modules.csv")
257
- save_eigengenes(eigengenes, Path(args.output_dir) / "eigengenes.csv")
258
485
  logger.info("Module extraction complete; outputs saved to %s", args.output_dir)
259
486
 
260
487