Skip to content

Commit f60b45a

Browse files
authored
Merge pull request #38 from jolespin/dev
Refactoring for performance improvements in v2026.7.16
2 parents eeb307a + ff6e61b commit f60b45a

11 files changed

Lines changed: 136 additions & 112 deletions

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#### Daily Change Log:
2-
* [2026.3.11] - Added support for `-r/--single_reads` in both `profile-taxonomy.py` and `profile-pathway.py`. However, the `profile-pathway.py` needs `oarfish` to use long reads. [issue/#24](https://github.com/jolespin/leviathan/issues/24)
2+
* [2026.7.16] - Added parallelization support for pathway coverage using `profile_pathway_coverage` from `kegg_pathway_profiler>=2026.7.16` in `leviathan-profile-pathway.py`. The `--n_jobs` flag now applies to both `salmon quant` and pathway coverage computation.
3+
* [2026.7.16] - Added `--deterministic` flag to `leviathan-profile-pathway.py` which passes `--deterministic` to `salmon quant` for byte-identical results across runs and thread counts.
4+
* [2026.7.8] - Added `--update_salmon_index` to `leviathan-index.py` and remove `-u` alias on `--update_with_genomes`
5+
* [2026.7.8] - **BREAKING CHANGE:** `Salmon` is reimplemented with `Rust` from `C++` so any `Leviathan` databases using `Salmon 1.x` will need to be updated. `Leviathan` now requires `Salmon ≥ 2.x`.
6+
* [2026.3.11] - Added support for `-r/--single_reads` in both `profile-taxonomy.py` and `profile-pathway.py`. However, the `profile-pathway.py` needs `oarfish` to use long reads but there are no synthetic long-read metatranscriptomics to benchmark. [issue/#24](https://github.com/jolespin/leviathan/issues/24)
37
* [2026.3.10] - Added `--veba_major_version` to `compile-manifest-from-veba.py` since `cluster` output directory is changing
48
* [2026.3.3] - Added `step_coverage` output files [issue/#22](https://github.com/jolespin/leviathan/issues/22)
59
* [2025.12.17] - Added `--table_format parquet|tsv` to `leviathan-merge.py` and merge functions in `leviathan-profile-pathway.py`/`leviathan-profile-taxonomy.py` [Issue #20](https://github.com/jolespin/leviathan/issues)

FAQ.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Frequently Asked Questions
2+
## 1. Is it required to run `leviathan-profile-taxonomy` before `leviathan-profile-pathway` or vice versa?
3+
No, they are functionally independent. However, you can always filter the pathway profiling results by taxonomic filters post hoc.
4+
5+
```python
6+
# Load taxonomic profiling
7+
ds_taxonomic = xr.open_dataset("leviathan_output/artifacts/taxonomic_abundances.genome_clusters.nc")
8+
9+
# Load functional profiling
10+
ds_pathway = xr.open_dataset("leviathan_output/artifacts/pathway.genome_clusters.nc")
11+
12+
# Get (pan)genomes to retain from taxonomic abundances
13+
minimum_samples_detected = 2
14+
retained_organisms = (ds_taxonomic["taxonomic_abundances"].to_pandas() > 0).sum(axis=0)[lambda x: x >= minimum_samples_detected].index
15+
16+
# Filter functional profiling with taxonomic abundance gate
17+
ds_pathway_filtered = ds_pathway.sel(genome_clusters=retained_organisms)
18+
```
19+
## 2. How can I estimate the reference coverage (i.e., percent of assigned reads)?
20+
```python
21+
# Load taxonomic profiling
22+
ds_taxonomic = xr.open_dataset("leviathan_output/artifacts/taxonomic_abundances.genome_clusters.nc")
23+
24+
# Sum the sequence abundances (not taxonomic abundances)
25+
percent_assigned_reads = ds_taxonomic["sequence_abundances"].sum(axis=1).to_pandas()
26+
percent_unassigned_reads = 100 - percent_assigned_reads
27+
```
28+
## 3. Do I need my own genomes or are there reference catalogs?

README.md

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,69 +24,7 @@ Leviathan: A fast, memory-efficient, and scalable taxonomic and pathway profiler
2424
Detailed explanation on how to run each module including downloading test data and interpreting output files.
2525

2626
## Benchmarking
27-
### Benchmarking against 10, 100, 1000, and 10000 genomes
28-
Benchmarking using trimmed `SRR12042303` sample with 4 threads (ml.m5.4xlarge)
29-
30-
| number_of_genomes | number_of_cds_with_features | preprocess | index | profile-taxonomy | profile-pathway |
31-
|-------------------|-----------------------------|------------|-------|------------------|-----------------|
32-
| 10 | 1928 | 0:03 | 0:09 | 0:41 | 2:09 |
33-
| 100 | 18410 | 0:31 | 0:26 | 0:41 | 4:29 |
34-
| 1000 | 191155 | 5:29 | 3:55 | 0:43 | 12:50 |
35-
| 10000 | 1684876 | 46:00 | 39:10 | 0:48 | 18:14 |
36-
37-
### Benchmarking against CAMI-I and CAMI-II using 16 threads
38-
All benchmarking and analysis was performed using a virtual machine with the following
39-
specifications: Linux Ubuntu 22.04 64-bit (x86_64), 30 Intel Xeon Platinum 8358 CPU, 222 GB
40-
memory, and 1 NVIDIA A10 GPU. Benchmarking and analysis was performed using 16 threads
41-
running 2 jobs simultaneously for *Leviathan* and *HUMAnN*.
42-
43-
#### Computational Performance
44-
| | | Leviathan | | HUMAnN | | Fold Improvement | |
45-
|-----------------|----------|----------------------|--------------------|----------------------|--------------------|--------------------|--------|
46-
| | | Duration (minutes) | Peak Memory (GB) | Duration (minutes) | Peak Memory (GB) | Duration | Memory |
47-
| CAMI_high_toy | H_S001 | 14.61 | 2.34 | 1083.57 | 32.31 | 74.19 | 13.84 |
48-
| | H_S002 | 14.89 | 2.35 | 949.73 | 32.28 | 63.78 | 13.75 |
49-
| | H_S003 | 14.96 | 2.34 | 875.83 | 32.64 | 58.56 | 13.97 |
50-
| | H_S004 | 15.02 | 2.35 | 852.18 | 32.33 | 56.72 | 13.79 |
51-
| | H_S005 | 15.27 | 2.33 | 826.25 | 32.23 | 54.13 | 13.82 |
52-
| CAMI_medium_toy | M2_S001 | 5.78 | 1.62 | 219.25 | 15.95 | 37.96 | 9.83 |
53-
| | M2_S002 | 5.81 | 1.62 | 174.12 | 16.96 | 29.95 | 10.45 |
54-
| CAMI_low_toy | S_S001 | 3.29 | 1.27 | 76.90 | 10.00 | 23.40 | 7.87 |
55-
| Marine | sample_0 | 13.78 | 2.70 | 119.52 | 17.92 | 8.68 | 6.63 |
56-
| | sample_1 | 15.22 | 2.69 | 121.30 | 18.00 | 7.97 | 6.69 |
57-
| | sample_2 | 14.97 | 2.71 | 120.27 | 17.99 | 8.03 | 6.65 |
58-
| | sample_3 | 17.10 | 2.71 | 124.05 | 17.83 | 7.25 | 6.59 |
59-
| | sample_4 | 14.32 | 2.74 | 118.47 | 17.82 | 8.27 | 6.51 |
60-
| | sample_5 | 15.53 | 2.72 | 119.40 | 17.80 | 7.69 | 6.54 |
61-
| | sample_6 | 16.09 | 2.71 | 119.72 | 17.91 | 7.44 | 6.62 |
62-
| | sample_7 | 14.90 | 2.73 | 119.92 | 17.92 | 8.05 | 6.56 |
63-
| | sample_8 | 16.41 | 2.72 | 121.73 | 17.95 | 7.42 | 6.61 |
64-
| | sample_9 | 14.45 | 2.73 | 118.87 | 17.79 | 8.23 | 6.51 |
65-
66-
#### Accuracy Performance
67-
Ranges from 0.0 - 1.0
68-
69-
| | Accuracy | Leviathan | | HUMAnN | | Improvement | |
70-
|-----------------|----------|-----------|-----------|--------|-----------|-------------|-----------|
71-
| Dataset | SampleID | Genome | Pangenome | Genome | Pangenome | Genome | Pangenome |
72-
| CAMI_high_toy | H_S001 | 0.9492 | 0.9970 | 0.9049 | 0.9610 | 0.0442 | 0.0360 |
73-
| | H_S002 | 0.9551 | 0.9899 | 0.8992 | 0.9591 | 0.0558 | 0.0308 |
74-
| | H_S003 | 0.9556 | 0.9888 | 0.9004 | 0.9598 | 0.0553 | 0.0290 |
75-
| | H_S004 | 0.9496 | 0.9872 | 0.8947 | 0.9588 | 0.0548 | 0.0284 |
76-
| | H_S005 | 0.9420 | 0.9877 | 0.8901 | 0.9573 | 0.0519 | 0.0304 |
77-
| CAMI_medium_toy | M2_S001 | 0.9692 | 0.9983 | 0.9101 | 0.9620 | 0.0591 | 0.0363 |
78-
| | M2_S002 | 0.9762 | 0.9988 | 0.9177 | 0.9650 | 0.0585 | 0.0338 |
79-
| CAMI_low_toy | S_S001 | 1.0000 | 1.0000 | 0.9845 | 0.9845 | 0.0155 | 0.0155 |
80-
| Marine | sample_0 | 0.9727 | 0.9933 | 0.8783 | 0.9538 | 0.0944 | 0.0396 |
81-
| | sample_1 | 0.9298 | 0.9922 | 0.8793 | 0.9554 | 0.0505 | 0.0367 |
82-
| | sample_2 | 0.9686 | 0.9817 | 0.8768 | 0.9393 | 0.0918 | 0.0424 |
83-
| | sample_3 | 0.9706 | 0.9842 | 0.8596 | 0.9517 | 0.1110 | 0.0325 |
84-
| | sample_4 | 0.9661 | 0.9880 | 0.8454 | 0.9389 | 0.1207 | 0.0491 |
85-
| | sample_5 | 0.9614 | 0.9856 | 0.8740 | 0.9612 | 0.0874 | 0.0244 |
86-
| | sample_6 | 0.9283 | 0.9869 | 0.8684 | 0.9574 | 0.0599 | 0.0295 |
87-
| | sample_7 | 0.9231 | 0.9942 | 0.8719 | 0.9466 | 0.0512 | 0.0476 |
88-
| | sample_8 | 0.9703 | 0.9889 | 0.8764 | 0.9488 | 0.0940 | 0.0401 |
89-
| | sample_9 | 0.9459 | 0.9859 | 0.8657 | 0.9548 | 0.0802 | 0.0311 |
27+
Please refer to the publication for benchmarking specs but you should be able to run this easily on a machine with 16GB of RAM.
9028

9129
## Modules
9230
### `leviathan-preprocess`

WALKTHROUGH.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,16 @@ Here are the output files:
250250

251251
## 7. Reading output files
252252
### 7a. Reading Parquet files with Pandas
253-
254253
```python
255-
!pip install fastparquet
254+
!pip install pyarrow
256255
df = pd.read_parquet("path/to/file.parquet")
257256
```
258257

259258
### 7b. Reading NetCDF files with Xarray
260-
259+
> [!NOTE]
260+
> To load `.nc` files you must have `h5netcdf` installed before `xarray` is loaded
261261
```python
262-
!pip install xarray
262+
!pip install xarray h5netcdf
263263
import xarray as xr
264264

265265
# Taxonomic abundances for genomes
@@ -291,8 +291,34 @@ Data variables:
291291

292292
```
293293

294-
### 7c. Reformatting Xarray NetCDF files into Pandas DataFrames
294+
### 7c. Estimating the percent of unassigned reads (i.e., reference coverage)
295+
Sequence coverage only sums to 100% if every read was assigned so you can subtract from 100% to determine the percent of unassigned reads
296+
```python
297+
# Load taxonomic profiling
298+
ds_taxonomic = xr.open_dataset("leviathan_output/artifacts/taxonomic_abundances.genome_clusters.nc")
299+
300+
# Sum the sequence abundances (not taxonomic abundances)
301+
percent_assigned_reads = ds_taxonomic["sequence_abundances"].sum(axis=1).to_pandas()
302+
percent_unassigned_reads = 100 - percent_assigned_reads
303+
```
304+
305+
### 7d. Filtering functional profiling results with taxonomy gate
306+
```python
307+
# Load taxonomic profiling
308+
ds_taxonomic = xr.open_dataset("leviathan_output/artifacts/taxonomic_abundances.genome_clusters.nc")
309+
310+
# Load functional profiling
311+
ds_pathway = xr.open_dataset("leviathan_output/artifacts/pathway.genome_clusters.nc")
312+
313+
# Get (pan)genomes to retain from taxonomic abundances
314+
minimum_samples_detected = 2
315+
retained_organisms = (ds_taxonomic["taxonomic_abundances"].to_pandas() > 0).sum(axis=0)[lambda x: x >= minimum_samples_detected].index
316+
317+
# Filter functional profiling with taxonomic abundance gate
318+
ds_pathway_filtered = ds_pathway.sel(genome_clusters=retained_organisms)
319+
```
295320

321+
### 7e. Reformatting Xarray NetCDF files into Pandas DataFrames
296322
```python
297323
# Load pathway abundances
298324
ds_pathway = xr.open_dataset("leviathan_output/artifacts/pathway.genome_clusters.nc")
@@ -320,7 +346,7 @@ X_coverage = X_coverage.loc[:,features_passed_qc]
320346
# Downstream analysis with filtered `X_counts`
321347
```
322348

323-
### 7d. Selecting coverage `prevalence` cutoff
349+
### 7f. Selecting coverage `prevalence` cutoff
324350

325351
```python
326352
import compositional as coda

bin/leviathan-index.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def main(args=None):
6464
parser_io.add_argument("-m","--feature_mapping", type=str, help = "path/to/feature_mapping.tsv [id_gene, feature_set, id_genome, (Optional: id_genome_cluster)] (No header)")
6565
parser_io.add_argument("-g","--genomes", type=str, help = "path/to/genomes.tsv [id_genome, path/to/genome] (No header)")
6666
parser_io.add_argument("-d","--index_directory", type=str, required=True, help = "path/to/index_directory/ (Recommended: leviathan_output/index/ if this will only be used for one project or a centralized location if it will be used for multiple projects)")
67-
parser_io.add_argument("-u", "--update_with_genomes", action="store_true", help = "Update databases with genomes for Sylph sketches")
67+
parser_io.add_argument("--update_with_genomes", action="store_true", help = "Update databases with genomes for Sylph sketches")
68+
parser_io.add_argument("--update_salmon_index", action="store_true", help = "Update the Salmon index on an existing database (e.g., Leviathan databases prior to v2026.7.8 were built with Salmon v1.x and need to be updated with Salmon v2.x)")
6869

6970
# Utilities
7071
parser_utility = parser.add_argument_group('Utility arguments')
@@ -102,7 +103,7 @@ def main(args=None):
102103
logger.info(f"Command: {sys.argv}")
103104

104105
# Checks
105-
if not opts.no_check:
106+
if not opts.no_check and not opts.update_salmon_index:
106107
genomes_with_filepaths = set()
107108
with open_file_reader(opts.genomes) as f:
108109
for line in f:
@@ -144,6 +145,16 @@ def main(args=None):
144145
if not opts.genomes:
145146
logger.warning("--genomes not provided but can incoporated post hoc by rerunning with --update_with_genomes")
146147

148+
if opts.update_salmon_index:
149+
if opts.update_with_genomes:
150+
msg = "--update_salmon_index and --update_with_genomes are mutually exclusive"
151+
logger.critical(msg)
152+
parser.error(msg)
153+
if not opts.fasta:
154+
msg = "--fasta is required when --update_salmon_index is specified"
155+
logger.critical(msg)
156+
parser.error(msg)
157+
147158
# Threads
148159
if opts.n_jobs == -1:
149160
from multiprocessing import cpu_count
@@ -186,6 +197,7 @@ def main(args=None):
186197
if all([
187198
os.path.exists(opts.index_directory),
188199
not opts.update_with_genomes,
200+
not opts.update_salmon_index,
189201
]):
190202
msg = f"--index_directory {opts.index_directory} already exists. If you want to update with genomes, please use --update_with_genomes or remove directory to overwrite"
191203
logger.critical(msg)
@@ -195,7 +207,22 @@ def main(args=None):
195207
os.makedirs(os.path.join(opts.index_directory, "logs"), exist_ok=True)
196208
os.makedirs(os.path.join(opts.index_directory, "tmp"), exist_ok=True)
197209

198-
if not opts.update_with_genomes:
210+
if opts.update_salmon_index:
211+
# ==================
212+
# Rebuild Salmon Index
213+
# ==================
214+
logger.info("Rebuilding salmon index")
215+
cmd_salmon_indexer = run_salmon_indexer(
216+
logger=logger,
217+
log_directory=os.path.join(opts.index_directory, "logs"),
218+
salmon_executable=opts.salmon_executable,
219+
n_jobs=opts.n_jobs,
220+
fasta=opts.fasta,
221+
index_directory=opts.index_directory,
222+
index_options=opts.salmon_index_options,
223+
)
224+
225+
elif not opts.update_with_genomes:
199226
# Setup config
200227
logger.info("Setting up config")
201228

@@ -295,7 +322,9 @@ def main(args=None):
295322
# ==================
296323
# Build Sylph Sketch
297324
# ==================
298-
if config["contains_genome_filepaths"]:
325+
if opts.update_salmon_index:
326+
logger.info("Skipping Sylph sketching (only updating salmon index)")
327+
elif config["contains_genome_filepaths"]:
299328
logger.info("Writing genome filepaths for Sylph")
300329

301330
# Write filepaths

bin/leviathan-profile-pathway.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
check_salmon_index,
3333
)
3434

35+
from kegg_pathway_profiler.pathways import profile_pathway_coverage
36+
3537
from leviathan.profile_pathway import(
3638
check_reads_format,
3739
run_salmon_quant,
@@ -40,7 +42,6 @@
4042
build_wide_feature_prevalence_matrix,
4143
build_feature_prevalence_dictionary,
4244
build_feature_pathway_dictionary,
43-
calculate_pathway_coverage,
4445
aggregate_pathway_abundance_and_append_coverage,
4546
aggregate_feature_abundance_for_clusters,
4647
)
@@ -83,6 +84,7 @@ def main(args=None):
8384
parser_salmon_quant.add_argument("--salmon_include_mappings", action="store_true", help="salmon quant| Include mappings")
8485
parser_salmon_quant.add_argument("--salmon_gzip", action="store_true", help="salmon quant | Gzip quant.sf")
8586

87+
parser_salmon_quant.add_argument("--deterministic", action="store_true", help="salmon quant | Deterministic quantification: byte-identical results across runs and thread counts [Default: False]")
8688
parser_salmon_quant.add_argument("--salmon_quant_options", type=str, default="", help="salmon quant| More options (e.g. --arg=1 ) https://salmon.readthedocs.io/en/latest/ [Default: '']")
8789

8890
# Samtools
@@ -214,7 +216,8 @@ def main(args=None):
214216
include_mappings=opts.salmon_include_mappings,
215217
alignment_format=opts.alignment_format,
216218
salmon_gzip=opts.salmon_gzip,
217-
salmon_quant_options=opts.salmon_quant_options,
219+
salmon_quant_options=opts.salmon_quant_options,
220+
deterministic=opts.deterministic,
218221
)
219222

220223
# ===============================
@@ -277,8 +280,13 @@ def main(args=None):
277280
logger.info(f"[level={level}] Building feature to pathways dictionary")
278281
feature_to_pathways = build_feature_pathway_dictionary(pathway_to_data)
279282
logger.info(f"[level={level}] Calculating pathway coverage")
280-
coverages, step_coverages = calculate_pathway_coverage(genome_to_features, pathway_to_data)
281-
283+
coverages, step_coverages, _ = profile_pathway_coverage(
284+
genome_to_kos=genome_to_features,
285+
database=pathway_to_data,
286+
n_jobs=opts.n_jobs,
287+
serialize_output=False,
288+
)
289+
282290
# Pathway abundances
283291
pathway_abundances_filepath = os.path.join(output_directory, "output", f"pathway_abundances.{level}s.{opts.output_format}")
284292
if opts.output_format != "parquet":
@@ -354,7 +362,12 @@ def main(args=None):
354362
logger.info(f"[level={level}] Building feature to pathways dictionary")
355363
feature_to_pathways = build_feature_pathway_dictionary(pathway_to_data)
356364
logger.info(f"[level={level}] Calculating pathway coverage")
357-
coverages, step_coverages = calculate_pathway_coverage(genome_to_features, pathway_to_data)
365+
coverages, step_coverages, _ = profile_pathway_coverage(
366+
genome_to_kos=genome_to_features,
367+
database=pathway_to_data,
368+
n_jobs=opts.n_jobs,
369+
serialize_output=False,
370+
)
358371

359372
# Pathway abundances
360373
pathway_abundances_filepath = os.path.join(output_directory, "output", f"pathway_abundances.{level}s.{opts.output_format}")

leviathan/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env python
2-
__version__ = "2026.3.11"
2+
__version__ = "2026.7.16"
33
from . import utils
44
from . import index
55
from . import profile_taxonomy
6-
from . import profile_pathway
7-
6+
from . import profile_pathway

0 commit comments

Comments
 (0)