Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/plassembler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def run(
longreads,
Path(f"{outdir}/chopper_long_reads.fastq"),
)
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"))
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"), threads)
remove_file(Path(f"{outdir}/chopper_long_reads.fastq"))

# Raven for long only or '--use_raven'
Expand Down Expand Up @@ -1103,7 +1103,7 @@ def assembled(
longreads,
Path(f"{outdir}/chopper_long_reads.fastq"),
)
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"))
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"), threads)
remove_file(Path(f"{outdir}/chopper_long_reads.fastq"))

if short_flag is True:
Expand Down Expand Up @@ -1454,7 +1454,7 @@ def long(
longreads,
Path(f"{outdir}/chopper_long_reads.fastq"),
)
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"))
gzip_file(Path(f"{outdir}/chopper_long_reads.fastq"), threads)
remove_file(Path(f"{outdir}/chopper_long_reads.fastq"))

# flye - skip directory an option here
Expand Down
102 changes: 85 additions & 17 deletions src/plassembler/utils/qc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import gzip
import shutil
import subprocess as sp
from contextlib import ExitStack
from pathlib import Path

from loguru import logger

from plassembler.utils.external_tools import ExternalTool


def gzip_compressor_cmd(threads):
"""Command that reads plain data on stdin and writes a gzip stream to stdout.

Prefers bgzip, which ships with htslib/samtools (already a hard plassembler
dependency) and compresses in parallel. BGZF is a valid gzip stream, so every
downstream reader - flye, minimap2, chopper, gunzip, python's gzip module -
handles the result unchanged.

Serial gzip dominated the chopper stage: on a 300 MB ONT fastq the chain took
47.7s, of which only 5.4s was gunzip+chopper and 42s was gzip.

:param threads: thread count (str or int) to hand to bgzip
:return: argv list for the compressor
"""
if shutil.which("bgzip"):
return ["bgzip", "-@", str(threads), "-c"]
# bgzip should always be present, but never fail QC over a missing binary
return ["gzip"]


def chopper(
input_long_reads, outdir, min_length, min_quality, gzip_flag, threads, logdir
):
Expand Down Expand Up @@ -39,29 +60,59 @@ def chopper(
"--tailcrop",
"75",
]
# `with` guarantees the log and output handles are closed even on error;
# locals are named *_proc to avoid shadowing the `gzip` module / `chopper`
# function name
with open(f"{logfile_prefix}.err", "w") as err_log, open(
filtered_long_reads, "wb"
) as f:
compressor_cmd = gzip_compressor_cmd(threads)

# ExitStack guarantees the log, output and pipe handles are closed even if a
# Popen raises partway through building the chain
with ExitStack() as stack:
err_log = stack.enter_context(open(f"{logfile_prefix}.err", "w"))
out_fh = stack.enter_context(open(filtered_long_reads, "wb"))

stages = []
try:
if gzip_flag is True:
source_proc = sp.Popen(
["gunzip", "-c", input_long_reads], stdout=sp.PIPE
["gunzip", "-c", input_long_reads], stdout=sp.PIPE, stderr=err_log
)
stages.append(("gunzip", source_proc))
chopper_stdin = source_proc.stdout
else:
source_proc = sp.Popen(["cat", input_long_reads], stdout=sp.PIPE)
# plain fastq needs no decompressor: hand the file straight to
# chopper rather than spawning a `cat` to copy it through a pipe
chopper_stdin = stack.enter_context(open(input_long_reads, "rb"))

chopper_proc = sp.Popen(
chopper_cmd,
stdin=source_proc.stdout,
stdout=sp.PIPE,
stderr=err_log,
chopper_cmd, stdin=chopper_stdin, stdout=sp.PIPE, stderr=err_log
)
stages.append(("chopper", chopper_proc))
# the parent must drop its copy of each upstream read end, otherwise
# the downstream stage never sees EOF
if gzip_flag is True:
source_proc.stdout.close()

compress_proc = sp.Popen(
compressor_cmd, stdin=chopper_proc.stdout, stdout=out_fh, stderr=err_log
)
gzip_proc = sp.Popen(["gzip"], stdin=chopper_proc.stdout, stdout=f)
gzip_proc.communicate()
except Exception:
logger.error("Error with chopper")
stages.append((compressor_cmd[0], compress_proc))
chopper_proc.stdout.close()
except OSError as e:
for _, proc in stages:
proc.kill()
logger.error(f"Error with chopper: {e}")
return

# every stage must be waited on. Previously only the last one was, so a
# failing chopper was silently ignored and left a zombie behind
failures = []
for name, proc in reversed(stages):
if proc.wait() != 0:
failures.append(f"{name} (return code {proc.returncode})")

if failures:
logger.error(
f"Error with chopper: {', '.join(reversed(failures))}. "
f"Please check {logfile_prefix}.err"
)
logger.info("Finished running chopper")


Expand Down Expand Up @@ -104,10 +155,27 @@ def copy_sr_fastq_file(infile: Path, outfile: Path):
logger.error("Error with copy_sr_fastq_file")


def gzip_file(input_path):
def gzip_file(input_path, threads=1):
"""gzips a file, in parallel where bgzip is available

Used by --skip_qc to compress the copied long reads. python's gzip module is
both single threaded and slower than the gzip binary, which is a poor fit for
a multi-GB ONT fastq; fall back to it only if spawning the compressor fails.

:param input_path: file to compress
:param threads: threads to give the compressor
:return: path of the compressed file
"""
input_path = Path(input_path)
output_path = input_path.with_suffix(input_path.suffix + ".gz")

try:
with open(input_path, "rb") as f_in, open(output_path, "wb") as f_out:
sp.run(gzip_compressor_cmd(threads), stdin=f_in, stdout=f_out, check=True)
return output_path
except (OSError, sp.CalledProcessError) as e:
logger.warning(f"Falling back to python gzip for {input_path}: {e}")

with open(input_path, "rb") as f_in:
with gzip.open(output_path, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
Expand Down
Binary file modified tests/test_data/end_to_end/input_half.fastq.gz
Binary file not shown.
Loading