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.
- {bsvae-0.2.0 → bsvae-0.2.1}/PKG-INFO +10 -5
- {bsvae-0.2.0 → bsvae-0.2.1}/README.md +9 -4
- {bsvae-0.2.0 → bsvae-0.2.1}/pyproject.toml +1 -1
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/cli.py +242 -15
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/extract_networks.py +221 -14
- bsvae-0.2.1/src/bsvae/networks/module_extraction.py +898 -0
- bsvae-0.2.0/src/bsvae/networks/module_extraction.py +0 -426
- {bsvae-0.2.0 → bsvae-0.2.1}/LICENSE +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/__init__.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/hyperparam.ini +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/__init__.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/latent_analysis.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/latent/latent_export.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/main.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/__init__.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/decoder.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/encoder.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/losses.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/structured.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/models/vae.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/__init__.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/networks/utils.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/ppi_cli.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/__init__.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/datasets.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/evaluate.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/helpers.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/initialization.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/mapping.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/modelIO.py +0 -0
- {bsvae-0.2.0 → bsvae-0.2.1}/src/bsvae/utils/ppi.py +0 -0
- {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.
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
|