Skip to content

Commit 4c09c76

Browse files
authored
Merge pull request #87 from gbouras13/issues
fix: unicycler --spades_options without --unicycler_options; canu pacbio-hifi (#83, #84)
2 parents b6d21ce + 0e74942 commit 4c09c76

4 files changed

Lines changed: 158 additions & 89 deletions

File tree

src/plassembler/__init__.py

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from plassembler.utils.plass_class import Assembly, Plass
3939
from plassembler.utils.qc import chopper, copy_sr_fastq_file, fastp, gzip_file
4040
from plassembler.utils.run_canu import ( # make_blastdb,; process_blast_output,; run_blast,
41+
canu_read_type_and_error_rate,
4142
filter_entropy,
4243
filter_entropy_fastqs,
4344
run_canu,
@@ -1575,19 +1576,11 @@ def long(
15751576
plasmidfastqs: Path = Path(outdir) / "plasmid_long.fastq"
15761577
extract_long_fastqs_fast(samfile, plasmidfastqs, threads)
15771578

1578-
# to set error rate
1579-
canu_nano_or_pacbio = "nanopore"
1580-
1581-
if pacbio_model != "nothing":
1582-
if pacbio_model == "pacbio-hifi":
1583-
canu_nano_or_pacbio = "pacbio-hifi"
1584-
corrected_error_rate = 0.005
1585-
else:
1586-
canu_nano_or_pacbio = "pacbio"
1587-
corrected_error_rate = 0.045
1588-
else:
1589-
canu_nano_or_pacbio = "nanopore"
1590-
# corrected error rate default will be 0.12
1579+
# map the validated --pacbio_model to canu's read-type flag, error rate,
1580+
# and whether read correction should be skipped (HiFi reads)
1581+
canu_nano_or_pacbio, corrected_error_rate, skip_canu_correct = (
1582+
canu_read_type_and_error_rate(pacbio_model, corrected_error_rate)
1583+
)
15911584

15921585
if canu_flag is True:
15931586
assembler = "canu"
@@ -1641,28 +1634,38 @@ def long(
16411634
)
16421635
filter_entropy_fastqs(plasmidfastqs, entropy_filtered_fastq)
16431636

1644-
logger.info("Correcting reads with canu prior to running Unicycler.")
1645-
1646-
try:
1647-
run_canu_correct(
1648-
threads,
1649-
logdir,
1650-
entropy_filtered_fastq,
1651-
canu_output_dir,
1652-
canu_nano_or_pacbio,
1653-
total_flye_plasmid_length,
1654-
corrected_error_rate,
1655-
coverage,
1656-
)
1657-
# convert the corrected .fasta.gz from Canu to fastq
1658-
canu_reads: Path = (
1659-
Path(canu_output_dir) / "canu.correctedReads.fasta.gz"
1637+
if skip_canu_correct:
1638+
# PacBio HiFi reads are already high-accuracy; canu -correct
1639+
# rejects them ("Cannot correct already corrected reads"), so
1640+
# skip correction and assemble the entropy-filtered reads (#84).
1641+
logger.info(
1642+
"PacBio HiFi reads detected - skipping canu read correction."
16601643
)
1661-
corrected_fastqs: Path = Path(outdir) / "corrected_plasmid_long.fastq"
1662-
corrected_fasta_to_fastq(canu_reads, corrected_fastqs)
1663-
except Exception:
1664-
logger.warning("Advancing with uncorrected reads")
16651644
corrected_fastqs = entropy_filtered_fastq
1645+
else:
1646+
logger.info("Correcting reads with canu prior to running Unicycler.")
1647+
try:
1648+
run_canu_correct(
1649+
threads,
1650+
logdir,
1651+
entropy_filtered_fastq,
1652+
canu_output_dir,
1653+
canu_nano_or_pacbio,
1654+
total_flye_plasmid_length,
1655+
corrected_error_rate,
1656+
coverage,
1657+
)
1658+
# convert the corrected .fasta.gz from Canu to fastq
1659+
canu_reads: Path = (
1660+
Path(canu_output_dir) / "canu.correctedReads.fasta.gz"
1661+
)
1662+
corrected_fastqs: Path = (
1663+
Path(outdir) / "corrected_plasmid_long.fastq"
1664+
)
1665+
corrected_fasta_to_fastq(canu_reads, corrected_fastqs)
1666+
except Exception:
1667+
logger.warning("Advancing with uncorrected reads")
1668+
corrected_fastqs = entropy_filtered_fastq
16661669

16671670
# remove canu directory
16681671
remove_directory(canu_output_dir)

src/plassembler/utils/run_canu.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@
99
from plassembler.utils.external_tools import ExternalTool
1010

1111

12+
def canu_read_type_and_error_rate(pacbio_model, default_error_rate):
13+
"""Map a validated ``--pacbio_model`` value to canu's read-type flag, the
14+
corrected error rate, and whether canu read correction should be skipped.
15+
16+
``pacbio_model`` is the output of ``validate_pacbio_model`` (so one of
17+
``"--pacbio-raw"``, ``"--pacbio-corr"``, ``"--pacbio-hifi"``) or
18+
``"nothing"``.
19+
20+
PacBio HiFi reads are already high-accuracy: canu's read type is
21+
``-pacbio-hifi`` and its correction step is skipped (running ``canu -correct``
22+
on HiFi reads errors with "Cannot correct already corrected reads" - issue
23+
#84). Previously the check compared against ``"pacbio-hifi"`` (without the
24+
``--`` prefix), so HiFi reads were wrongly assembled as regular ``-pacbio``.
25+
26+
:return: ``(canu_read_type, corrected_error_rate, skip_correction)``
27+
"""
28+
if pacbio_model == "--pacbio-hifi":
29+
return "pacbio-hifi", 0.005, True
30+
if pacbio_model != "nothing":
31+
return "pacbio", 0.045, False
32+
return "nanopore", default_error_rate, False
33+
34+
1235
def run_canu_correct(
1336
threads: Path,
1437
logdir: Path,

src/plassembler/utils/run_unicycler.py

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66
from plassembler.utils.external_tools import ExternalTool
77

88

9+
def build_extra_unicycler_options(unicycler_options, spades_options):
10+
"""Build the extra-options suffix for a unicycler command.
11+
12+
Each of ``unicycler_options`` / ``spades_options`` may be ``None``
13+
independently. Regression (issue #83): when only ``spades_options`` was set,
14+
a ``None`` ``unicycler_options`` was interpolated into the command as the
15+
literal string ``"None"``, which Unicycler rejected.
16+
"""
17+
parts = []
18+
if unicycler_options is not None:
19+
parts.append(str(unicycler_options))
20+
if spades_options is not None:
21+
parts.append(f'--spades_options "{spades_options}"')
22+
return " ".join(parts)
23+
24+
925
def run_unicycler(
1026
threads: int,
1127
logdir: Path,
@@ -28,34 +44,18 @@ def run_unicycler(
2844
:return:
2945
"""
3046

31-
if unicycler_options is None and spades_options is None:
32-
unicycler = ExternalTool(
33-
tool="unicycler",
34-
input="",
35-
output="",
36-
params=f" -1 {short_one} -2 {short_two} -l {longreads} -t {threads} -o {unicycler_output_dir}",
37-
logdir=logdir,
38-
outfile="",
39-
)
40-
else:
41-
if spades_options is None:
42-
unicycler = ExternalTool(
43-
tool="unicycler",
44-
input="",
45-
output="",
46-
params=f" -1 {short_one} -2 {short_two} -l {longreads} -t {threads} -o {unicycler_output_dir} {unicycler_options}",
47-
logdir=logdir,
48-
outfile="",
49-
)
50-
else:
51-
unicycler = ExternalTool(
52-
tool="unicycler",
53-
input="",
54-
output="",
55-
params=f' -1 {short_one} -2 {short_two} -l {longreads} -t {threads} -o {unicycler_output_dir} {unicycler_options} --spades_options "{spades_options}" ',
56-
logdir=logdir,
57-
outfile="",
58-
)
47+
extra = build_extra_unicycler_options(unicycler_options, spades_options)
48+
unicycler = ExternalTool(
49+
tool="unicycler",
50+
input="",
51+
output="",
52+
params=(
53+
f" -1 {short_one} -2 {short_two} -l {longreads} -t {threads} "
54+
f"-o {unicycler_output_dir} {extra}"
55+
),
56+
logdir=logdir,
57+
outfile="",
58+
)
5959

6060
ExternalTool.run_tool(unicycler, to_stdout=False)
6161

@@ -80,34 +80,18 @@ def run_unicycler_long(
8080
:return:
8181
"""
8282

83-
if unicycler_options is None and spades_options is None:
84-
unicycler_long = ExternalTool(
85-
tool="unicycler",
86-
input="",
87-
output="",
88-
params=f" -s {corrected_longreads} -l {entropy_filtered_longreads} -t {threads} -o {unicycler_output_dir}",
89-
logdir=logdir,
90-
outfile="",
91-
)
92-
else:
93-
if spades_options is None:
94-
unicycler_long = ExternalTool(
95-
tool="unicycler",
96-
input="",
97-
output="",
98-
params=f" -s {corrected_longreads} -l {entropy_filtered_longreads} -t {threads} -o {unicycler_output_dir} {unicycler_options}",
99-
logdir=logdir,
100-
outfile="",
101-
)
102-
else:
103-
unicycler_long = ExternalTool(
104-
tool="unicycler",
105-
input="",
106-
output="",
107-
params=f' -s {corrected_longreads} -l {entropy_filtered_longreads} -t {threads} -o {unicycler_output_dir} {unicycler_options} --spades_options "{spades_options}"',
108-
logdir=logdir,
109-
outfile="",
110-
)
83+
extra = build_extra_unicycler_options(unicycler_options, spades_options)
84+
unicycler_long = ExternalTool(
85+
tool="unicycler",
86+
input="",
87+
output="",
88+
params=(
89+
f" -s {corrected_longreads} -l {entropy_filtered_longreads} "
90+
f"-t {threads} -o {unicycler_output_dir} {extra}"
91+
),
92+
logdir=logdir,
93+
outfile="",
94+
)
11195

11296
ExternalTool.run_tool(unicycler_long, to_stdout=False)
11397

tests/test_issues.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Regression tests for reported GitHub issues."""
2+
3+
import pytest
4+
5+
from src.plassembler.utils.run_canu import canu_read_type_and_error_rate
6+
from src.plassembler.utils.run_unicycler import build_extra_unicycler_options
7+
8+
# ---------------------------------------------------------------------------
9+
# issue #83: Unicycler crashes with --spades_options but no --unicycler_options
10+
# (a None unicycler_options was interpolated into the command as "None")
11+
# ---------------------------------------------------------------------------
12+
13+
14+
def test_unicycler_options_both_none():
15+
assert build_extra_unicycler_options(None, None) == ""
16+
17+
18+
def test_unicycler_options_spades_only_has_no_none():
19+
out = build_extra_unicycler_options(None, "--memory 2000")
20+
assert out == '--spades_options "--memory 2000"'
21+
assert "None" not in out
22+
23+
24+
def test_unicycler_options_unicycler_only():
25+
assert build_extra_unicycler_options("--mode bold", None) == "--mode bold"
26+
27+
28+
def test_unicycler_options_both():
29+
out = build_extra_unicycler_options("--mode bold", "--memory 2000")
30+
assert out == '--mode bold --spades_options "--memory 2000"'
31+
32+
33+
# ---------------------------------------------------------------------------
34+
# issue #84: `plassembler long --pacbio_model pacbio-hifi` passed the wrong
35+
# read type to canu (-pacbio instead of -pacbio-hifi) and tried to correct
36+
# already-corrected HiFi reads
37+
# ---------------------------------------------------------------------------
38+
39+
40+
def test_canu_read_type_hifi_skips_correction():
41+
read_type, err, skip = canu_read_type_and_error_rate("--pacbio-hifi", 0.12)
42+
assert read_type == "pacbio-hifi"
43+
assert err == 0.005
44+
assert skip is True
45+
46+
47+
@pytest.mark.parametrize("model", ["--pacbio-raw", "--pacbio-corr"])
48+
def test_canu_read_type_pacbio(model):
49+
read_type, err, skip = canu_read_type_and_error_rate(model, 0.12)
50+
assert read_type == "pacbio"
51+
assert err == 0.045
52+
assert skip is False
53+
54+
55+
def test_canu_read_type_nanopore_passthrough():
56+
read_type, err, skip = canu_read_type_and_error_rate("nothing", 0.12)
57+
assert read_type == "nanopore"
58+
assert err == 0.12 # keeps the CLI --corrected_error_rate
59+
assert skip is False

0 commit comments

Comments
 (0)