Skip to content

Commit 742f770

Browse files
Merge pull request #137 from MicrobialDarkMatter/update-epymetheus
Update epymetheus
2 parents cb989ee + f5d8535 commit 742f770

8 files changed

Lines changed: 189 additions & 34 deletions

File tree

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
pkgs.glib
3939

4040
# For python package
41-
pkgs.python311Full
41+
pkgs.python3
4242
];
4343
shellHook = ''
4444
export LD_LIBRARY_PATH="${pkgs.libGL}/lib/:${pkgs.stdenv.cc.cc.lib}/lib/:${pkgs.glib.out}/lib/:$LD_LIBRARY_PATH"

nanomotif/dataload.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Data loading functionalities
22
import polars as pl
3+
34
from nanomotif.seq import Assembly
4-
from epymetheus import query_pileup_records
5+
from epymetheus import query_pileup_records, PileupColumn
56
import sys
67
import pysam
78
import pyfastx
@@ -105,7 +106,38 @@ def load_contigs_pileup_bgzip(path: str, contigs: list[str]):
105106
"""
106107
# Step 2: query pileup from Rust
107108
log.debug(f"Querying pileup for {len(contigs)} contigs")
108-
pileup = query_pileup_records(path, contigs)
109+
pileup = query_pileup_records(
110+
path,
111+
contigs,
112+
columns = [
113+
PileupColumn.Contig,
114+
PileupColumn.Start,
115+
PileupColumn.ModType,
116+
PileupColumn.Strand,
117+
PileupColumn.FractionModified,
118+
PileupColumn.NValidCov
119+
]
120+
)
121+
122+
if pileup.height == 0:
123+
log.warning(f"No pileup data found for contigs: {contigs}")
124+
return pl.DataFrame({
125+
"contig": [],
126+
"position": [],
127+
"mod_type": [],
128+
"strand": [],
129+
"fraction_mod": [],
130+
"Nvalid_cov": [],
131+
}, schema = {
132+
"contig": pl.String,
133+
"position": pl.Int64,
134+
"mod_type": pl.Utf8,
135+
"strand": pl.Utf8,
136+
"fraction_mod": pl.Float64,
137+
"Nvalid_cov": pl.Int64,
138+
})
139+
140+
109141
log.debug(f"Renaming and removing unnecessary columns")
110142
pileup = pileup.rename({
111143
"contig": "contig",
@@ -115,8 +147,7 @@ def load_contigs_pileup_bgzip(path: str, contigs: list[str]):
115147
"fraction_modified": "fraction_mod",
116148
"n_valid_cov": "Nvalid_cov",
117149
}) \
118-
.with_columns(pl.col("fraction_mod") / 100) \
119-
.select(["contig", "position", "mod_type", "strand", "fraction_mod", "Nvalid_cov"])
150+
.with_columns(pl.col("fraction_mod") / 100)
120151

121152
return pileup
122153

@@ -213,4 +244,4 @@ def filter_pileup_adjacency_filter(
213244
.drop("roll_max")
214245
)
215246

216-
return pileup
247+
return pileup
5.02 MB
Binary file not shown.
332 Bytes
Binary file not shown.

nanomotif/main.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -141,46 +141,53 @@ def binnary(args, pl):
141141

142142
if args.methylation_output_type == "median":
143143
output_type = MethylationOutput.Median
144-
else:
144+
elif args.methylation_output_type == "weighted-mean":
145145
output_type = MethylationOutput.WeightedMean
146+
else:
147+
log.error(f"Output type must be either median or weighted mean, got: {args.methylation_output_type}")
148+
exit()
146149

147150
contig_methylation_file = f"motifs-scored-read-methylation_{args.methylation_output_type}.tsv"
148151
if not args.force:
149152
log.info(f"Check if {contig_methylation_file} exists")
150153

151154
if os.path.isfile(os.path.join(args.out,contig_methylation_file)) and not args.force:
152155
log.info("motifs-scored-read-methylation.tsv exists. Using existing file! Use --force to override this.")
156+
# Load motifs-scored-read-methylation.tsv
157+
contig_methylation = pl.read_csv(
158+
os.path.join(args.out, contig_methylation_file), separator="\t", has_header = True, schema = {
159+
'contig': pl.String(), 'motif': pl.String(), 'mod_type': pl.String(), 'mod_position': pl.Int8(), 'methylation_value': pl.Float64(), 'mean_read_cov': pl.Float64(), 'n_motif_obs': pl.Int32(),
160+
}
161+
)
153162

154163
elif not os.path.isfile(os.path.join(args.out, contig_methylation_file)) or args.force:
155164
log.info(f"Running epymetheus to create {contig_methylation_file}")
156-
# Create motifs-scored-read-methylation
157-
return_code = methylation_pattern(
158-
pileup = args.pileup,
159-
assembly = args.assembly,
160-
motifs = motifs_in_bin_consensus,
161-
threads = args.threads,
162-
min_valid_read_coverage = args.min_valid_read_coverage,
163-
batch_size=1000,
164-
min_valid_cov_to_diff_fraction=0.8,
165-
output = os.path.join(args.out,contig_methylation_file),
166-
allow_assembly_pileup_mismatch=False,
167-
output_type = output_type
168-
)
169-
170-
if return_code != 0:
171-
log.error("Error running epymetheus")
172-
165+
try:
166+
# Create motifs-scored-read-methylation
167+
contig_methylation = methylation_pattern(
168+
pileup = args.pileup,
169+
assembly = args.assembly,
170+
motifs = motifs_in_bin_consensus,
171+
threads = args.threads,
172+
min_valid_read_coverage = args.min_valid_read_coverage,
173+
batch_size=1000,
174+
min_valid_cov_to_diff_fraction=0.8,
175+
output = os.path.join(args.out,contig_methylation_file),
176+
allow_assembly_pileup_mismatch=True,
177+
output_type = output_type
178+
)
179+
if contig_methylation is None or len(contig_methylation) == 0:
180+
log.warning("epymetheus returned empty result")
181+
sys.exit(1)
182+
except Exception as e:
183+
log.error(f"Failed to run epymetheus: {e}")
184+
sys.exit(1)
185+
173186

174187
log.info("Loading assembly file...")
175188
assembly = data_processing.read_fasta(args.assembly)
176189
contig_lengths = data_processing.find_contig_lengths(assembly)
177190

178-
# Load motifs-scored-read-methylation.tsv
179-
contig_methylation = pl.read_csv(
180-
os.path.join(args.out, contig_methylation_file), separator="\t", has_header = True, schema = {
181-
'contig': pl.String(), 'motif': pl.String(), 'mod_type': pl.String(), 'mod_position': pl.Int8(), 'methylation_value': pl.Float64(), 'mean_read_cov': pl.Float64(), 'n_motif_obs': pl.Int32(),
182-
}
183-
)
184191

185192
contig_methylation = contig_methylation\
186193
.filter((pl.col("n_motif_obs").cast(pl.Float64) * pl.col("mean_read_cov")) >= args.methylation_threshold)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"scikit-learn>=1.5.2",
3636
"networkx>=3.1",
3737
"pyarrow>=15.0.2",
38-
"epimetheus-py==0.7.4",
38+
"epimetheus-py==0.7.5",
3939
"hdbscan",
4040
"Bio>=1.6.2",
4141
"snakemake>=7.32.4",

tests/test_cli_commands.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ def test_motif_discovery():
2727
# Check that the CLI tool executed successfully
2828
assert result.returncode == 0, "CLI tool did not exit successfully"
2929

30+
def test_motif_discovery_gzip():
31+
"""
32+
"""
33+
outdir = "tests/cli_test_motif_discovery"
34+
35+
cmd = [
36+
"nanomotif", "motif_discovery",
37+
"-t", "1",
38+
"nanomotif/datasets/geobacillus-plasmids.assembly.fasta",
39+
"nanomotif/datasets/geobacillus-plasmids.pileup.bed.gz",
40+
"-c", "nanomotif/datasets/geobacillus-contig-bin.tsv",
41+
"--out", outdir
42+
]
43+
44+
result = subprocess.run(cmd)
45+
shutil.rmtree(outdir)
46+
47+
# Check that the CLI tool executed successfully
48+
assert result.returncode == 0, "CLI tool did not exit successfully"
3049

3150
def test_check_installation():
3251
"""
@@ -152,6 +171,104 @@ def test_detect_contamination():
152171
else:
153172
print(f"File not found: {outfile}")
154173

174+
175+
176+
def test_detect_contamination_bgzip():
177+
"""
178+
"""
179+
import polars as pl
180+
from Bio import SeqIO
181+
from Bio.Seq import Seq
182+
from Bio.SeqRecord import SeqRecord
183+
from epymetheus.epymetheus import bgzf_pileup
184+
185+
infile = "nanomotif/datasets/geobacillus-plasmids.assembly.fasta"
186+
outfile_a = "nanomotif/datasets/geobacillus-plasmids.assembly.duplicated.fasta"
187+
188+
# Read all records from the original file
189+
records = list(SeqIO.parse(infile, "fasta"))
190+
191+
# We'll store our new records here
192+
new_records = []
193+
194+
# For each record, if it's contig_2 or contig_3, create 5 duplicates
195+
# with IDs appended by _1.._5. Otherwise, keep it as is.
196+
for record in records:
197+
if record.id in ["contig_2", "contig_3"]:
198+
for i in range(1, 6):
199+
new_id = f"{record.id}_{i}"
200+
# Create a new SeqRecord with the same sequence
201+
new_record = SeqRecord(
202+
record.seq,
203+
id=new_id,
204+
description=""
205+
)
206+
new_records.append(new_record)
207+
else:
208+
# For non-contig_2/3, simply keep the original record
209+
new_records.append(record)
210+
211+
# Write out the new FASTA file
212+
SeqIO.write(new_records, outfile_a, "fasta")
213+
214+
print(f"Duplicated contigs written to: {outfile_a}")
215+
216+
217+
infile = "nanomotif/datasets/geobacillus-plasmids.pileup.bed"
218+
outfile_p = "nanomotif/datasets/geobacillus-plasmids.pileup.duplicated.bed"
219+
220+
p = pl.read_csv(infile, has_header = False, separator = "\t")
221+
222+
p_dup = pl.DataFrame()
223+
for contig in ["contig_3", "contig_2"]:
224+
p_tmp = p.filter(pl.col("column_1") == contig)
225+
226+
for i in range(1, 6):
227+
p_i = p_tmp.with_columns(
228+
(pl.col("column_1") + f"_{i}").alias("column_1")
229+
)
230+
231+
p_dup = pl.concat([p_dup, p_i])
232+
233+
p_dup.write_csv(outfile_p, separator = "\t", include_header = False)
234+
bgzf_pileup(outfile_p)
235+
236+
237+
238+
outfile_b = "nanomotif/datasets/geobacillus-plasmids.contig_bin.tmp.tsv"
239+
contig_bin = pl.DataFrame({
240+
"contig": [f"contig_{i}_{j}" for i in [2, 3] for j in range(1, 6)],
241+
"bin": ["bin_1"] * 10,
242+
})
243+
244+
contig_bin.write_csv(outfile_b, separator ="\t", include_header = False)
245+
246+
outdir = "tests/cli_test_detect_contamination"
247+
248+
cmd = [
249+
"nanomotif", "detect_contamination",
250+
"-t", "1",
251+
"--force",
252+
"--assembly", outfile_a,
253+
"--pileup", outfile_p + ".gz",
254+
"--contig_bins", outfile_b,
255+
"--bin_motifs", "nanomotif/datasets/geobacillus-plasmids.bin-motifs.tsv",
256+
"--out", outdir
257+
]
258+
result = subprocess.run(cmd)
259+
260+
# Check that the CLI tool executed successfully
261+
shutil.rmtree(outdir)
262+
assert result.returncode == 0, "CLI tool did not exit successfully"
263+
for outfile in [outfile_a, outfile_p, outfile_b, outfile_p + ".gz", outfile_p + "..gz.tbi"]:
264+
if os.path.exists(outfile):
265+
os.remove(outfile)
266+
print(f"Deleted: {outfile}")
267+
else:
268+
print(f"File not found: {outfile}")
269+
270+
271+
155272
def test_detect_contamination_weighted_mean():
156273
"""
157274
"""

0 commit comments

Comments
 (0)