Skip to content
Merged
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
12 changes: 12 additions & 0 deletions metapang/app/profile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ def gather_candidates():
candidates.append(species)
mp_log.trace(f" candidate species '{species}' (from genome '{name}')")

detection_file = output_directory / f"{sample_name}.detection.tsv"
with open(detection_file, "w") as det:
det.write("genome\tspecies\tf_query\tf_match\tcum_f_query\n")
cum = 0.0
for name, f_query, f_match in gather_results:
cum += float(f_query)
det.write(
f"{name}\t{name.split('@')[0]}\t"
f"{float(f_query):.6f}\t{float(f_match):.6f}\t{cum:.6f}\n"
)
mp_log.info(f"Wrote species detection scores to '{detection_file.name}'")

if candidates:
mp_log.info(f"{len(candidates)} candidate species: {', '.join(candidates)}")
else:
Expand Down
197 changes: 129 additions & 68 deletions metapang/core/index/index.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import signal
import threading
from collections.abc import Iterable
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import asdict, dataclass, field
Expand Down Expand Up @@ -246,27 +247,38 @@ def pangenome_construct(

@staticmethod
def file_batch_signature(
file: "Path | list[Path]",
k: int,
scaled: int,
n: int,
force: bool,
nb_cores: int,
queue_size: int,
batch_size: int,
with_log: bool = True,
) -> SourmashSignature:
"""Compute one Sourmash signature for one or several files, in parallel batches.

Several files (e.g. paired-end or split reads) are folded into a single MinHash,
so the result is one signature for the whole sample.
"""
file, k, scaled, n, force, nb_cores, queue_size, batch_size, with_log=True
):
files = [file] if isinstance(file, (str, Path)) else list(file)

man = MinHashManager(ctx=_MP_CONTEXT)
man.register("MinHash", MinHash)
man.start()
m = man.MinHash(n=n, ksize=k, scaled=scaled) # type: ignore[attr-defined]
result = MinHash(n=n, ksize=k, scaled=scaled)
merge_lock = threading.Lock() # callbacks run in a thread of the main process
errors: list[BaseException] = []

def _merge(fut):
try:
mh = fut.result()
except BaseException as e:
errors.append(e)
return
with merge_lock:
result.merge(mh)

def _iter_sequences():
for f in files:
if is_fastx(f):
for record in Fastx(f):
yield record[1]
else:
with zopen(f, "rb") as file_io:
for record in gb_io.iter(file_io):
yield record.sequence.decode("utf-8")

def _submit(executor, batch):
fut = executor.submit(
IndexBuilder.sequence_batch_minhash, batch, k, n, scaled, force
)
fut.add_done_callback(_merge)

with BoundedProcessPoolExecutor(
queue_size,
Expand All @@ -275,60 +287,109 @@ def file_batch_signature(
initializer=_ignore_sigint,
) as executor:
try:
sequences = []
i = 0
for f in files:
if is_fastx(f):
for record in Fastx(f):
sequences.append(record[1])
i += 1
if i % batch_size == 0:
executor.submit(
IndexBuilder.add_sequences_minhash,
m,
sequences,
k,
n,
scaled,
force,
)
sequences = []
else:
with zopen(f, "rb") as file_io:
for record in gb_io.iter(file_io):
sequences.append(record.sequence.decode("utf-8"))
i += 1
if i % batch_size == 0:
executor.submit(
IndexBuilder.add_sequences_minhash,
m,
sequences,
k,
n,
scaled,
force,
)
sequences = []

if sequences:
executor.submit(
IndexBuilder.add_sequences_minhash,
m,
sequences,
k,
n,
scaled,
force,
)

batch = []
for seq in _iter_sequences():
batch.append(seq)
if len(batch) == batch_size:
_submit(executor, batch)
batch = []
if batch:
_submit(executor, batch)
except KeyboardInterrupt:
mp_log_error(with_log, "Interrupted by user (Ctrl+C)")
executor.shutdown(wait=False)
os._exit(1)

signature = SourmashSignature(m.copy(), name=files[0].stem)
man.shutdown()
return signature
if errors:
raise errors[0]

return SourmashSignature(result, name=Path(files[0]).stem)

# @staticmethod
# def file_batch_signature(
# file: "Path | list[Path]",
# k: int,
# scaled: int,
# n: int,
# force: bool,
# nb_cores: int,
# queue_size: int,
# batch_size: int,
# with_log: bool = True,
# ) -> SourmashSignature:
# """Compute one Sourmash signature for one or several files, in parallel batches.

# Several files (e.g. paired-end or split reads) are folded into a single MinHash,
# so the result is one signature for the whole sample.
# """
# files = [file] if isinstance(file, (str, Path)) else list(file)

# man = MinHashManager(ctx=_MP_CONTEXT)
# man.register("MinHash", MinHash)
# man.start()
# m = man.MinHash(n=n, ksize=k, scaled=scaled) # type: ignore[attr-defined]

# with BoundedProcessPoolExecutor(
# queue_size,
# max_workers=nb_cores,
# mp_context=_MP_CONTEXT,
# initializer=_ignore_sigint,
# ) as executor:
# try:
# sequences = []
# i = 0
# for f in files:
# if is_fastx(f):
# for record in Fastx(f):
# sequences.append(record[1])
# i += 1
# if i % batch_size == 0:
# executor.submit(
# IndexBuilder.add_sequences_minhash,
# m,
# sequences,
# k,
# n,
# scaled,
# force,
# )
# sequences = []
# else:
# with zopen(f, "rb") as file_io:
# for record in gb_io.iter(file_io):
# sequences.append(record.sequence.decode("utf-8"))
# i += 1
# if i % batch_size == 0:
# executor.submit(
# IndexBuilder.add_sequences_minhash,
# m,
# sequences,
# k,
# n,
# scaled,
# force,
# )
# sequences = []

# if sequences:
# executor.submit(
# IndexBuilder.add_sequences_minhash,
# m,
# sequences,
# k,
# n,
# scaled,
# force,
# )

# except KeyboardInterrupt:
# mp_log_error(with_log, "Interrupted by user (Ctrl+C)")
# executor.shutdown(wait=False)
# os._exit(1)

# signature = SourmashSignature(m.copy(), name=files[0].stem)
# man.shutdown()
# return signature

def construct(self, nb_cores: int, with_log: bool = True) -> IndexInfo:
"""Construct and save the index, returning its IndexInfo."""
Expand Down
Loading
Loading