Skip to content

Commit bdec60d

Browse files
authored
Add support for gzipped fasta files (#89)
* Add support for gzipped fasta files * Add pytest with gzipped fastas * Adjust samtools faidx rule in snakemake * Fixes for pylint * Account for differing assembly names in gzip pytest
1 parent 6156664 commit bdec60d

4 files changed

Lines changed: 43 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ More information can be found on our [wiki page](https://github.com/BirolLab/ntS
175175

176176
### Tips / Visualization <a name=tips></a>
177177
- To lower the peak memory usage, increase the number of hash functions (--hashes) and/or increase the false positive rate (--fpr) for the constructed Bloom filter
178+
- Your input fasta files can be gzipped
178179
- Customize parameters such as --merge, --indel, --block_size and --w_rounds for your particular input data and research questions
179180
- For visualizing the multi-genome output synteny blocks, check out our streamlined visualization tool [ntSynt-viz](https://github.com/BirolLab/ntSynt-viz). Examples of the scripts used to generate the figures in the [ntSynt manuscript](https://link.springer.com/article/10.1186/s12915-025-02455-w) can be found in the sub-directory [visualization_scripts](https://github.com/BirolLab/ntSynt/tree/main/visualization_scripts)
180181
- If you do not know the approximate sequence divergence between the input assemblies, we recommend using [Mash](https://github.com/marbl/Mash) to estimate the divergences

bin/ntsynt_run_pipeline.smk

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ rule faidx:
5151
output: "{file}.fai"
5252
params: benchmarking=expand("{benchmark_path} -o {{file}}.faidx.time", benchmark_path=benchmark_path) if benchmark else []
5353
threads: 1
54-
shell: "{params.benchmarking} samtools faidx -o {output} {input.fa}"
54+
run:
55+
if input.fa.endswith(".gz"):
56+
shell("{params.benchmarking} gunzip -c {input.fa} | samtools faidx -o {output} -")
57+
else:
58+
shell("{params.benchmarking} samtools faidx -o {output} {input.fa}")
5559

5660
rule make_common_bf:
5761
input: refs=references

bin/ntsynt_synteny.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import shlex
1313
import subprocess
1414
import sys
15+
import gzip
16+
import shutil
17+
import tempfile
1518
import intervaltree
1619
import pybedtools
1720
import btllib
@@ -132,6 +135,17 @@ def get_synteny_bed_lists(paths):
132135

133136
return synteny_beds
134137

138+
def prepare_fasta_for_bedtools(self, fa_filename_full):
139+
"""If fasta is gzipped, decompress to a temp file and return its path.
140+
Returns (path, is_temp) — caller should delete path if is_temp is True."""
141+
if fa_filename_full.endswith(".gz"):
142+
tmp = tempfile.NamedTemporaryFile(suffix=".fa", delete=False, dir=os.getcwd()) # pylint: disable=consider-using-with
143+
with gzip.open(fa_filename_full, "rb") as f_in:
144+
shutil.copyfileobj(f_in, tmp)
145+
tmp.close()
146+
return tmp.name, True
147+
return fa_filename_full, False
148+
135149
def mask_assemblies_with_synteny_extents(self, synteny_beds, w):
136150
"Mask each reference assembly with determined synteny blocks"
137151
mx_to_fa_dict = {}
@@ -142,10 +156,15 @@ def mask_assemblies_with_synteny_extents(self, synteny_beds, w):
142156
bed_str = "\n".join(bed_str)
143157
fa_filename = self.find_fa_name(assembly)
144158
fa_filename_full = assembly_to_fastas[fa_filename]
145-
synteny_bed = pybedtools.BedTool(bed_str, from_string=True).slop(g=f"{fa_filename}.fai",
146-
l=-1*(w+self.args.k),
147-
r=-1*(w+self.args.k)).sort()
148-
synteny_bed.mask_fasta(fi=fa_filename_full, fo=f"{fa_filename}_masked.fa.tmp")
159+
fa_tmp, is_temp = self.prepare_fasta_for_bedtools(fa_filename_full)
160+
try:
161+
synteny_bed = pybedtools.BedTool(bed_str, from_string=True).slop(g=f"{fa_filename}.fai",
162+
l=-1*(w+self.args.k),
163+
r=-1*(w+self.args.k)).sort()
164+
synteny_bed.mask_fasta(fi=fa_tmp, fo=f"{fa_filename}_masked.fa.tmp")
165+
finally:
166+
if is_temp:
167+
os.remove(fa_tmp)
149168

150169
# pybedtools may output multi-line fasta which breaks btllib reading, need seqtk to make single line
151170
with open(f"{fa_filename}_masked.fa", "w", encoding="utf-8") as fout:

tests/ntsynt_tests.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ def launch_ntSynt_fof(genome_file_list, prefix="ntSynt", k=24, w=1000, **kwargs)
2222
ret_code = subprocess.call(cmd)
2323
assert ret_code == 0
2424

25+
def strip_gz_from_line(line):
26+
"Strip .gz from the assembly name (second column) of the synteny TSV line"
27+
fields = line.split("\t")
28+
fields[1] = fields[1].replace(".gz", "")
29+
return "\t".join(fields)
30+
2531
def are_expected_blocks(block_file1, block_file2):
2632
"Check that the given synteny blocks are as expected"
2733
with open(block_file1, 'r', encoding="utf-8") as fin1:
2834
with open(block_file2, 'r', encoding="utf-8") as fin2:
2935
while (line1 := fin1.readline()) and (line2 := fin2.readline()):
30-
assert line1 == line2
36+
assert strip_gz_from_line(line1) == strip_gz_from_line(line2)
3137

3238
def test_prep_files():
3339
"Prep the test files"
@@ -68,3 +74,10 @@ def test_cleanup():
6874
cmd = shlex.split(f"pigz {infile}")
6975
ret_code = subprocess.call(cmd)
7076
assert ret_code == 0
77+
78+
def test_3_genomes_gz():
79+
"Testing ntSynt with three input gzipped genomes"
80+
genome1, genome2, genome3 = "celegans-chrII-III.fa.gz", "celegans-chrII-III.A.fa.gz", "celegans-chrII-III.B.fa.gz"
81+
launch_ntSynt(genome1, genome2, genome3, k=20, prefix="celegans-A-B-ntSynt_gz", indel=500, merge=3000)
82+
are_expected_blocks("celegans-A-B-ntSynt_gz.synteny_blocks.tsv",
83+
"expected_result/celegans-A-B-ntSynt.synteny_blocks.tsv")

0 commit comments

Comments
 (0)