diff --git a/Dockerfile b/Dockerfile index 5cd9cd9..f08955e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ FROM python:3.13-bookworm AS deps WORKDIR /app -COPY --from=ghcr.io/virtool/tools:1.1.0 /tools/bowtie2/2.5.4/bowtie* /usr/local/bin/ -COPY --from=ghcr.io/virtool/tools:1.1.0 /tools/pigz/2.8/pigz /usr/local/bin/ -COPY --from=ghcr.io/virtool/tools:1.1.0 /tools/samtools/1.22.1/bin/samtools /usr/local/bin/ +COPY --from=ghcr.io/virtool/tools:1.2.0 /tools/bowtie2/2.5.4/bowtie* /usr/local/bin/ +COPY --from=ghcr.io/virtool/tools:1.2.0 /tools/cd-hit/4.8.1/cd-hit-est /usr/local/bin/ +COPY --from=ghcr.io/virtool/tools:1.2.0 /tools/pigz/2.8/pigz /usr/local/bin/ +COPY --from=ghcr.io/virtool/tools:1.2.0 /tools/samtools/1.22.1/bin/samtools /usr/local/bin/ FROM python:3.13-bookworm AS uv WORKDIR /app diff --git a/fixtures.py b/fixtures.py index e77948d..84b365a 100644 --- a/fixtures.py +++ b/fixtures.py @@ -2,19 +2,52 @@ from types import SimpleNamespace from pyfixtures import fixture +from virtool.workflow.data.index_sqlite import INDEX_SQLITE_FILE_NAME + + +def get_reference_index_path(work_path: Path) -> Path: + return work_path / "reference_index" / "reference" + + +def get_collapsed_reference_path(work_path: Path) -> Path: + return work_path / "collapsed_reference" / INDEX_SQLITE_FILE_NAME + + +def get_subtraction_indexes_path(work_path: Path) -> Path: + return work_path / "subtraction_indexes" + + +def get_isolate_path(work_path: Path) -> Path: + return work_path / "isolates" + + +def get_isolate_fastq_path(isolate_path: Path) -> Path: + return isolate_path / "isolate_mapped.fq" + + +def get_isolate_index_path(isolate_path: Path) -> Path: + return isolate_path / "isolates" + + +def get_isolate_bam_path(isolate_path: Path) -> Path: + return isolate_path / "to_isolates.bam" + + +def get_subtracted_bam_path(work_path: Path) -> Path: + return work_path / "subtracted.bam" @fixture def intermediate(): """A namespace for storing intermediate values.""" return SimpleNamespace( - to_otus=set(), + candidate_sequence_ids=set(), ) @fixture def isolate_path(work_path: Path): - path = work_path / "isolates" + path = get_isolate_path(work_path) path.mkdir() return path @@ -22,12 +55,17 @@ def isolate_path(work_path: Path): @fixture def reference_index_path(work_path: Path): - return work_path / "reference_index" / "reference" + return get_reference_index_path(work_path) + + +@fixture +def collapsed_reference_path(work_path: Path): + return get_collapsed_reference_path(work_path) @fixture def subtraction_indexes_path(work_path: Path) -> Path: - return work_path / "subtraction_indexes" + return get_subtraction_indexes_path(work_path) @fixture @@ -37,17 +75,17 @@ def isolate_fasta_path(isolate_path: Path): @fixture def isolate_fastq_path(isolate_path: Path): - return isolate_path / "isolate_mapped.fq" + return get_isolate_fastq_path(isolate_path) @fixture def isolate_index_path(isolate_path: Path): - return isolate_path / "isolates" + return get_isolate_index_path(isolate_path) @fixture def isolate_bam_path(isolate_path: Path): - return isolate_path / "to_isolates.bam" + return get_isolate_bam_path(isolate_path) @fixture @@ -58,4 +96,4 @@ def p_score_cutoff(): @fixture def subtracted_bam_path(work_path: Path) -> Path: """The path to the BAM file after subtraction reads have been eliminated.""" - return work_path / "subtracted.bam" + return get_subtracted_bam_path(work_path) diff --git a/pyproject.toml b/pyproject.toml index fd4fd82..630ef85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ features = ["pyo3/extension-module"] asyncio_mode = "auto" [tool.uv.sources] -virtool = { git = "https://github.com/virtool/virtool", tag = "38.48.0" } +virtool = { git = "https://github.com/virtool/virtool", tag = "39.63.0" } [build-system] requires = ["maturin>=1.14.1,<2.0"] diff --git a/python/workflow_pathoscope/reference.py b/python/workflow_pathoscope/reference.py new file mode 100644 index 0000000..83a0e24 --- /dev/null +++ b/python/workflow_pathoscope/reference.py @@ -0,0 +1,359 @@ +import asyncio +import re +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TypedDict + +from virtool.workflow.data.indexes import WFIndex +from virtool.workflow.errors import SubprocessFailedError +from virtool.workflow.runtime.run_subprocess import RunSubprocess +from virtool.workflow.utils import get_workflow_version + + +CD_HIT_EST_TOOL = "cd-hit-est" +CD_HIT_EST_IDENTITY = "0.99" +WORKFLOW_NAME = "pathoscope" +WORKFLOW_VERSION = get_workflow_version() + + +class OtuCollapseOutcome(StrEnum): + COLLAPSED = "collapsed" + UNCHANGED = "unchanged" + SKIPPED = "skipped" + + +@dataclass(frozen=True) +class OtuCollapsePreparation: + eligible: bool + sequences_by_segment: dict[str, list[dict]] + + +@dataclass(frozen=True) +class OtuCollapseResult: + otu: dict + outcome: OtuCollapseOutcome + + +class ReferenceCollapseSummary(TypedDict): + isolate_count_before: int + isolate_count_after: int + isolate_count_removed: int + otu_count_collapsed: int + otu_count_unchanged: int + otu_count_skipped: int + + +async def get_cd_hit_est_version(run_subprocess: RunSubprocess) -> str: + output: list[str] = [] + + async def collect_output(line: bytes) -> None: + output.append(line.decode()) + + try: + await run_subprocess( + [CD_HIT_EST_TOOL, "-h"], + stderr_handler=collect_output, + stdout_handler=collect_output, + ) + except SubprocessFailedError: + # cd-hit-est -h prints help/version text and exits with code 1. + pass + + match = re.search(r"\bCD-HIT\s+version\s+([^\s]+)", "".join(output)) + + if match is None: + raise ValueError("Could not parse cd-hit-est version") + + return match.group(1) + + +async def get_reference_collapse_cache_params( + parent_id: str, + run_subprocess: RunSubprocess, +) -> dict[str, str]: + tool_version = await get_cd_hit_est_version(run_subprocess) + + return { + "index_kind": "collapsed_reference", + "workflow": WORKFLOW_NAME, + "workflow_version": WORKFLOW_VERSION, + "parent_id": parent_id, + "source": "index_sqlite", + "tool_name": CD_HIT_EST_TOOL, + "tool_version": tool_version, + "identity": CD_HIT_EST_IDENTITY, + } + + +def _prepare_otu_collapse(otu: dict) -> OtuCollapsePreparation: + """Group an OTU's sequences when its structure is safe to collapse.""" + sequences_by_segment: dict[str, list[dict]] = {} + + if not otu["schema"]: + for isolate in otu["isolates"]: + if len(isolate["sequences"]) != 1: + return OtuCollapsePreparation(eligible=False, sequences_by_segment={}) + + sequences_by_segment.setdefault("", []).extend(isolate["sequences"]) + + return OtuCollapsePreparation( + eligible=True, + sequences_by_segment=sequences_by_segment, + ) + + schema_segments = {segment["name"] for segment in otu["schema"]} + + for isolate in otu["isolates"]: + isolate_segments: set[str] = set() + + for sequence in isolate["sequences"]: + segment = sequence.get("segment") + + if not isinstance(segment, str) or not segment: + return OtuCollapsePreparation(eligible=False, sequences_by_segment={}) + + if segment not in schema_segments: + return OtuCollapsePreparation(eligible=False, sequences_by_segment={}) + + if segment in isolate_segments: + return OtuCollapsePreparation(eligible=False, sequences_by_segment={}) + + isolate_segments.add(segment) + sequences_by_segment.setdefault(segment, []).append(sequence) + + return OtuCollapsePreparation( + eligible=True, + sequences_by_segment=sequences_by_segment, + ) + + +def _write_fasta(sequences: list[dict], path: Path) -> None: + with open(path, "w") as handle: + for sequence in sequences: + handle.write(f">{sequence['id']}\n{sequence['sequence']}\n") + + +def _parse_cd_hit_clusters(cluster_path: Path) -> dict[str, str]: + representatives_by_sequence_id: dict[str, str] = {} + cluster_sequence_ids: list[str] = [] + representative_id: str | None = None + + def flush_cluster() -> None: + if representative_id is None: + return + + for sequence_id in cluster_sequence_ids: + representatives_by_sequence_id[sequence_id] = representative_id + + with open(cluster_path) as handle: + for line in handle: + line = line.strip() + + if line.startswith(">Cluster"): + flush_cluster() + cluster_sequence_ids = [] + representative_id = None + continue + + match = re.search(r">(.+?)\.\.\.", line) + + if match is None: + continue + + sequence_id = match.group(1) + cluster_sequence_ids.append(sequence_id) + + if line.endswith("*"): + representative_id = sequence_id + + flush_cluster() + + return representatives_by_sequence_id + + +def _build_representative_set( + isolate: dict, + representatives_by_sequence_id: dict[str, str], +) -> frozenset[str]: + return frozenset( + representatives_by_sequence_id[sequence["id"]] + for sequence in isolate["sequences"] + ) + + +async def _collapse_reference_segment( + segment_input_path: Path, + segment_output_path: Path, + sequences: list[dict], + run_subprocess: RunSubprocess, +) -> dict[str, str]: + await asyncio.to_thread(_write_fasta, sequences, segment_input_path) + + await run_subprocess( + [ + CD_HIT_EST_TOOL, + "-i", + str(segment_input_path), + "-o", + str(segment_output_path), + "-c", + CD_HIT_EST_IDENTITY, + "-T", + "1", + "-M", + "0", + "-d", + "0", + ], + ) + + return _parse_cd_hit_clusters(segment_output_path.with_suffix(".cdhit.clstr")) + + +async def _collapse_otu_segments( + otu: dict, + sequences_by_segment: dict[str, list[dict]], + temp_path: Path, + collapse_segment: Callable[[Path, Path, list[dict]], Awaitable[dict[str, str]]], +) -> dict[str, str]: + representatives_by_sequence_id: dict[str, str] = {} + + segment_tasks: list[Awaitable[dict[str, str]]] = [] + sorted_segment_sequences = sorted( + sequences_by_segment.items(), + key=lambda item: item[0], + ) + + for segment_name, sequences in sorted_segment_sequences: + otu_id = otu["id"] + segment_input_path = temp_path / f"otu-{otu_id}-segment-{segment_name}.fa" + segment_output_path = temp_path / f"otu-{otu_id}-segment-{segment_name}.cdhit" + + segment_tasks.append( + collapse_segment( + segment_input_path, + segment_output_path, + sequences, + ) + ) + + for representatives in await asyncio.gather(*segment_tasks): + representatives_by_sequence_id.update(representatives) + + return representatives_by_sequence_id + + +async def _collapse_otu_reference( + otu: dict, + temp_path: Path, + collapse_segment: Callable[[Path, Path, list[dict]], Awaitable[dict[str, str]]], +) -> OtuCollapseResult: + if len(otu["isolates"]) == 1: + return OtuCollapseResult(otu, OtuCollapseOutcome.UNCHANGED) + + preparation = _prepare_otu_collapse(otu) + + if not preparation.eligible: + return OtuCollapseResult( + otu, + OtuCollapseOutcome.SKIPPED, + ) + + representatives_by_sequence_id = await _collapse_otu_segments( + otu, + preparation.sequences_by_segment, + temp_path, + collapse_segment, + ) + + default_sequence_ids = { + sequence["id"] + for isolate in otu["isolates"] + if isolate["default"] + for sequence in isolate["sequences"] + } + seen_representative_sets: set[frozenset[str]] = set() + collapsed_isolates: list[dict] = [] + + for isolate in otu["isolates"]: + representative_set = _build_representative_set( + isolate, + representatives_by_sequence_id, + ) + + first_for_set = representative_set not in seen_representative_sets + seen_representative_sets.add(representative_set) + + contains_default_sequence = any( + sequence["id"] in default_sequence_ids for sequence in isolate["sequences"] + ) + + if isolate["default"] or contains_default_sequence or first_for_set: + collapsed_isolates.append(isolate) + + return OtuCollapseResult( + {**otu, "isolates": collapsed_isolates}, + OtuCollapseOutcome.COLLAPSED, + ) + + +async def collapse_reference_index( + index: WFIndex, + target_path: Path, + proc: int, + run_subprocess: RunSubprocess, +) -> ReferenceCollapseSummary: + """Collapse redundant isolates into a new workflow SQLite index.""" + before_count = 0 + after_count = 0 + otu_counts = {outcome: 0 for outcome in OtuCollapseOutcome} + + with TemporaryDirectory(prefix="pathoscope-collapse-") as temp_dir: + temp_path = Path(temp_dir) + semaphore = asyncio.Semaphore(proc) + + async def collapse_segment( + segment_input_path: Path, + segment_output_path: Path, + sequences: list[dict], + ) -> dict[str, str]: + async with semaphore: + return await _collapse_reference_segment( + segment_input_path, + segment_output_path, + sequences, + run_subprocess, + ) + + collapsed_otus: list[dict] = [] + + async for otu in index.iter_otus(): + result = await _collapse_otu_reference( + otu, + temp_path, + collapse_segment, + ) + collapsed_otus.append(result.otu) + otu_counts[result.outcome] += 1 + + before_count += len(otu["isolates"]) + after_count += len(result.otu["isolates"]) + + try: + reference = await index.get_reference_metadata() + except ValueError: + reference = None + + await WFIndex.create(index.id, target_path, reference, collapsed_otus) + + return { + "isolate_count_before": before_count, + "isolate_count_after": after_count, + "isolate_count_removed": before_count - after_count, + "otu_count_collapsed": otu_counts[OtuCollapseOutcome.COLLAPSED], + "otu_count_unchanged": otu_counts[OtuCollapseOutcome.UNCHANGED], + "otu_count_skipped": otu_counts[OtuCollapseOutcome.SKIPPED], + } diff --git a/python/workflow_pathoscope/utils.py b/python/workflow_pathoscope/utils.py index fc0eea4..a459da9 100755 --- a/python/workflow_pathoscope/utils.py +++ b/python/workflow_pathoscope/utils.py @@ -1,7 +1,5 @@ import asyncio import csv -import gzip -import json import re from pathlib import Path @@ -132,66 +130,6 @@ async def create_mapping_index( log.info("mapping index cache already exists", outcome="put_skipped") -def _open_json(path: Path): - if path.suffix == ".gz": - return gzip.open(path, "rt") - - return open(path) - - -def _get_reference_otus(reference_data): - if isinstance(reference_data, dict): - return reference_data["otus"] - - return reference_data - - -def write_default_isolate_fasta( - json_path: Path, - target_path: Path, -) -> dict[str, int]: - """Generate a FASTA file containing only default isolates from a reference JSON.""" - lengths = {} - - with _open_json(json_path) as f_json, open(target_path, "w") as f_target: - for otu in _get_reference_otus(json.load(f_json)): - for isolate in otu["isolates"]: - if not isolate["default"]: - continue - - for sequence in isolate["sequences"]: - f_target.write(f">{sequence['_id']}\n{sequence['sequence']}\n") - lengths[sequence["_id"]] = len(sequence["sequence"]) - - return lengths - - -def write_isolate_fasta( - otu_ids: set[str], - json_path: Path, - target_path: Path, -) -> dict[str, int]: - """Generate a FASTA file for all the isolates of the OTUs specified by ``otu_ids``. - - :param otu_ids: the list of OTU IDs for which to generate and index - :param json_path: the path to the reference index json file - :param target_path: the path to write the fasta file to - :return: a dictionary of the lengths of all sequences keyed by their IDS - - """ - lengths = {} - - with _open_json(json_path) as f_json, open(target_path, "w") as f_target: - for otu in _get_reference_otus(json.load(f_json)): - if otu["_id"] in otu_ids: - for isolate in otu["isolates"]: - for sequence in isolate["sequences"]: - f_target.write(f">{sequence['_id']}\n{sequence['sequence']}\n") - lengths[sequence["_id"]] = len(sequence["sequence"]) - - return lengths - - def write_report( path, pathoscope_results: PathoscopeResults, diff --git a/tests/fixtures.py b/tests/fixtures.py index 6f0b3a2..a8ed427 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -16,2045 +16,3 @@ def get_sam_lines(): @pytest.fixture(params=get_sam_lines(), ids=lambda x: x.split("\t")[0]) def sam_line(request): return request.param.split("\t") - - -@pytest.fixture() -def ref_lengths(): - return { - "0": 857, - "NC_003862": 2711, - "NC_010987": 7229, - "NC_005778": 9711, - "NC_008302": 8912, - "NC_018402": 3169, - "NC_022003": 2566, - "NC_011348": 2630, - "NC_008059": 2605, - "NC_006064": 3382, - "NC_013803": 1366, - "NC_005853": 2543, - "13C245A": 9393, - "NC_003735": 3617, - "NC_004043": 2580, - "NC_008728": 1931, - "NC_001554": 4776, - "NC_013075": 7492, - "Fr823299": 7062, - "NC_011066": 1028, - "NC_002600": 9709, - "NC_011706": 1581, - "AF185958": 1645, - "NC_014128": 2573, - "NC_021786": 9499, - "NC_008824": 9816, - "NC_013015": 1852, - "NC_014745": 2764, - "NC_009559": 1342, - "NC_021099": 2349, - "NC_008585": 17481, - "NC_002991": 3582, - "NC_011535": 4731, - "NC_003608": 3911, - "NC_003750": 3808, - "KC771898": 1305, - "NC_009991": 9058, - "NC_020236": 2748, - "NC_007640": 1384, - "NC_011552": 7988, - "13TF149_Reovirus_TF1_Seg08": 1466, - "NC_003826": 822, - "NC_002040": 2188, - "NC_009564": 1378, - "NC_005065": 15045, - "NC_014845": 2739, - "NC_025725": 2735, - "NC_003491": 5722, - "JN692499": 1909, - "NC_018717": 2599, - "NC_016143": 3196, - "NC_003499": 8512, - "NC_003731": 1900, - "NC_019409": 9445, - "NC_017000": 2656, - "NC_013455": 7317, - "NC_007638": 2757, - "NC_010833": 2708, - "NC_003724": 3134, - "13TF149_Reovirus_TF1_Seg04": 2417, - "NC_002357": 3232, - "NC_023880": 995, - "NC_003492": 9532, - "NC_012787": 2644, - "NC_011268": 2743, - "NC_006358": 2724, - "NC_003523": 1104, - "NC_012557": 1389, - "NC_023876": 1379, - "NC_003725": 2964, - "NC_022978": 15602, - "NC_021065": 9718, - "NC_001556": 6355, - "NC_003355": 6298, - "NC_011568": 6081, - "NC_014602": 2563, - "JX971983": 2354, - "NC_003478": 3164, - "NC_001439": 2647, - "NC_017830": 7140, - "NC_009491": 2559, - "NC_004120": 3252, - "U16734": 998, - "NC_014067": 2582, - "NC_003830": 2634, - "NC_003737": 2645, - "HM588723": 16883, - "NC_003749": 3849, - "NC_009893": 1356, - "NC_016080": 8315, - "NC_018384": 4607, - "NC_010791": 2738, - "NC_001507": 2588, - "NC_003650": 2435, - "NC_021708": 1292, - "NC_004121": 2898, - "NC_014897": 2744, - "NC_017987": 2751, - "NC_023017": 4065, - "NC_012211": 7118, - "NC_011309": 2757, - "NC_003659": 2970, - "NC_005304": 9591, - "13C244_Geminivirus_TF1": 2817, - "NC_015492": 7452, - "NC_005848": 3518, - "NC_023158": 988, - "NC_019415": 9842, - "NC_004725": 4576, - "NC_003482": 3524, - "NC_004583": 2748, - "FJ969831": 2803, - "NC_015051": 2860, - "AJ781402": 1841, - "NC_006051": 7651, - "NC_010239": 1358, - "NC_022614": 2013, - "NC_024012": 2610, - "NC_011591": 3437, - "NC_011542": 12381, - "NC_008251": 6858, - "NC_002738": 893, - "NC_026514": 2738, - "NC_002051": 2916, - "NC_014381": 2607, - "NC_008020": 8739, - "NC_026513": 2666, - "DQ178611": 2494, - "NC_018383": 7288, - "NC_015301": 1541, - "NC_003832": 8917, - "NC_004607": 2751, - "NC_009536": 2309, - "NC_029564": 1472, - "NC_014606": 1205, - "NC_003766": 1163, - "NC_009451": 2760, - "NC_006314": 8186, - "NC_020469": 3966, - "NC_015503": 7458, - "AB079657": 1514, - "16GVP035_TyV_GV1": 6737, - "NC_004300": 2757, - "NC_011097": 9205, - "NC_003603": 4019, - "NC_023305": 1010, - "NC_012539": 2521, - "NC_014907": 1416, - "NC_001513": 6211, - "NC_009759": 8404, - "AF399672": 1989, - "NC_012805": 342, - "NC_003971": 457, - "U16731": 1022, - "13TF149_Reovirus_TF1_Seg03": 3167, - "NC_003680": 5273, - "KX269866": 6163, - "NC_021926": 3919, - "NC_001929": 2585, - "NC_004755": 2735, - "NC_003733": 1801, - "NC_013017": 2386, - "NC_009557": 1367, - "NC_014546": 6643, - "NC_007448": 16494, - "NC_025789": 8336, - "NC_009644": 2677, - "NC_022801": 6379, - "NC_016082": 2621, - "NC_016444": 4735, - "NC_002042": 3593, - "NC_015491": 2585, - "NC_003668": 4504, - "NC_003870": 8146, - "NC_003622": 7212, - "NC_010318": 1015, - "KT273410": 4991, - "NC_003398": 9596, - "NC_014847": 2723, - "NC_010732": 5920, - "AF004657": 1984, - "NC_021874": 1352, - "NC_003605": 10080, - "NC_001359": 2631, - "NC_001616": 9704, - "U82446": 1860, - "13TF156_Totivirus_TF1": 4931, - "NC_003618": 7193, - "NC_003512": 2529, - "AJ781400": 2499, - "NC_002801": 1181, - "NC_006632": 1350, - "NC_003549": 5889, - "NC_012137": 2759, - "NC_004650": 2828, - "NC_002509": 9835, - "NC_009354": 2753, - "NC_008190": 1791, - "NC_003747": 4212, - "NC_009087": 8855, - "NC_017005": 2741, - "NC_003554": 7743, - "NC_011643": 4009, - "NC_003807": 1449, - "NC_006063": 5339, - "NC_008169": 8745, - "NC_010297": 1338, - "NC_003740": 2231, - "NC_003797": 10818, - "NC_005028": 10155, - "NC_008317": 2784, - "NC_006962": 7800, - "KJ572575": 13387, - "NC_013107": 1348, - "NC_013133": 6228, - "NC_013019": 2774, - "Z46351": 3065, - "NC_028146": 3644, - "NC_025254": 9491, - "NC_014791": 9070, - "NC_001946": 8372, - "NC_005330": 2675, - "NC_007211": 2745, - "NC_003418": 2741, - "NC_001801": 6424, - "AY485276": 1724, - "NC_023014": 9557, - "NC_009558": 1340, - "NC_015553": 2689, - "NC_012786": 2672, - "NC_012910": 8556, - "NC_016141": 8372, - "NC_003496": 5995, - "NC_007162": 1819, - "NC_014710": 2176, - "AY542957": 1063, - "NC_003739": 5449, - "NC_003624": 2992, - "NC_004673": 2739, - "NC_018173": 8607, - "NC_010714": 2757, - "JN117279": 1525, - "AJ581528": 4607, - "NC_011187": 3983, - "NC_003561": 996, - "KJ572574": 1663, - "AY500882": 1231, - "NC_010713": 2767, - "NC_005267": 3914, - "NC_010838": 2634, - "NC_016509": 18659, - "NC_015044": 2668, - "KT454832": 2742, - "NC_014846": 1385, - "NC_003498": 7932, - "NC_004553": 4086, - "NC_002024": 2593, - "NC_014742": 10820, - "NC_004019": 1586, - "AB099711": 1675, - "NC_001928": 2632, - "NC_004661": 2612, - "AB561003": 303, - "NC_023896": 1344, - "NC_015504": 7401, - "NC_003758": 1914, - "NC_004809": 9123, - "NC_011068": 2814, - "NC_004656": 2763, - "NC_012554": 2578, - "NC_025674": 9524, - "NC_020889": 1335, - "NC_003382": 7559, - "NC_023296": 1002, - "NC_001468": 2724, - "NC_010799": 2740, - "NC_008614": 6485, - "NC_004363": 2591, - "NC_014903": 2679, - "AJ781397": 3399, - "NC_004123": 9407, - "NC_020160": 342, - "NC_003623": 3774, - "NC_003612": 363, - "NC_013099": 986, - "AY137775": 1415, - "NC_005807": 2784, - "NC_003535": 4029, - "NC_003544": 3732, - "NC_021098": 2431, - "NC_010736": 9842, - "NC_007180": 9723, - "NC_004779": 7794, - "NC_015220": 7241, - "NC_005048": 1387, - "NC_003493": 2561, - "NC_012210": 8604, - "NC_001557": 1239, - "NC_002325": 3123, - "NC_020072": 9621, - "NC_023311": 983, - "NC_024305": 2594, - "NC_009562": 1344, - "NC_009553": 2752, - "NC_009742": 9689, - "NC_004676": 2777, - "JN117278": 1609, - "NC_018716": 2616, - "NC_003645": 985, - "U16730": 1001, - "NC_004618": 2787, - "13TF149_Reovirus_TF1_Seg09": 1495, - "NC_023299": 1008, - "NC_002598": 4326, - "NC_011918": 9792, - "NC_008316": 2767, - "NC_014037": 9782, - "NC_003533": 322, - "NC_018174": 8041, - "NC_004645": 2612, - "NC_016038": 5666, - "NC_013108": 1559, - "NC_005046": 1351, - "NC_010305": 8412, - "NC_002510": 2764, - "NC_003760": 1578, - "NC_003631": 1168, - "AF290026": 1787, - "NC_026239": 3892, - "NC_007341": 8247, - "NC_003397": 9992, - "NC_002602": 336, - "NC_001615": 13720, - "NC_001467": 2779, - "NC_026020": 6949, - "NC_004715": 1424, - "NC_001497": 8024, - "NC_016578": 2725, - "DQ399259": 11872, - "NC_016573": 2678, - "NC_003746": 14042, - "NC_027718": 8955, - "AJ969411": 1413, - "NC_015414": 7918, - "NC_010417": 2845, - "NC_022005": 2597, - "NC_003653": 1802, - "NC_013415": 2657, - "NC_007067": 1323, - "NC_017001": 2620, - "NC_015050": 6244, - "NC_012545": 1234, - "NC_006942": 12020, - "NC_003379": 2672, - "NC_020415": 3971, - "NC_006995": 2746, - "NC_014138": 2653, - "NC_009744": 9659, - "NC_010295": 5816, - "NC_007725": 895, - "NC_005030": 1336, - "NC_014712": 1900, - "NC_019034": 2781, - "NC_003617": 8118, - "NC_005852": 2575, - "NC_010348": 1446, - "DQ641511": 1699, - "NC_003475": 1060, - "NC_003654": 4391, - "NC_012931": 5673, - "NC_014324": 2981, - "AB033689": 7226, - "NC_023157": 985, - "NC_005895": 8066, - "NC_010812": 2756, - "FJ196838": 3030, - "NC_011539": 6154, - "NC_023023": 2753, - "NC_025967": 18569, - "NC_007242": 1779, - "NC_024894": 2737, - "NC_003628": 9871, - "AF335429": 4018, - "JN579656": 2357, - "NC_010315": 1057, - "NC_006061": 3892, - "NC_002198": 5662, - "NC_009243": 3224, - "NC_004017": 2284, - "NC_014896": 1354, - "NC_020896": 10081, - "NC_011181": 2660, - "NC_003627": 4437, - "NC_014600": 3818, - "NC_008304": 2613, - "NC_010946": 2724, - "NC_001822": 3662, - "NC_003851": 388, - "NC_004008": 3158, - "NC_004660": 2589, - "NC_003532": 4733, - "NC_021345": 1224, - "NC_003565": 1015, - "NC_003785": 6794, - "NC_005266": 7935, - "NC_006446": 1186, - "NC_001984": 2675, - "NC_002634": 9588, - "NC_008493": 2589, - "NC_010620": 1388, - "NC_011620": 6435, - "KJ541746": 8893, - "FJ915122": 6463, - "NC_005845": 2739, - "NC_014599": 3914, - "EF445546": 1610, - "EF203681": 1604, - "NC_014064": 9489, - "NC_012135": 2935, - "NC_013267": 2862, - "NC_007649": 13936, - "NC_019035": 2645, - "NC_022895": 4464, - "NC_002795": 8657, - "L13446": 1317, - "NC_001368": 3374, - "NC_016416": 13830, - "NC_018381": 13467, - "NC_002187": 4014, - "NC_010951": 2578, - "NC_002766": 5776, - "NC_013113": 958, - "NC_021579": 2669, - "NC_014325": 9741, - "NC_020258": 2650, - "NC_003487": 3762, - "NC_003755": 8970, - "NC_011347": 2739, - "NC_013268": 2424, - "NC_003822": 2580, - "NC_003673": 3128, - "NC_007726": 2674, - "AY913795": 3897, - "NC_016142": 3134, - "NC_007157": 2831, - "NC_024810": 2749, - "NC_014645": 2792, - "NC_010735": 9742, - "NC_014894": 2724, - "NC_000885": 360, - "NC_020235": 1828, - "NC_009533": 4293, - "NC_002815": 6614, - "NC_005954": 1377, - "NC_001768": 9475, - "NC_021736": 4315, - "NC_023626": 991, - "NC_030229": 6350, - "NC_024448": 15362, - "JN196537": 2290, - "AF035633": 1379, - "NC_019036": 2640, - "NC_013259": 7913, - "NC_013920": 7148, - "NC_004638": 2582, - "ARWaV_1S": 1362, - "NC_006946": 7494, - "NC_023678": 13908, - "NC_019946": 2574, - "NC_013640": 2823, - "JN196536": 2420, - "NC_016442": 6506, - "NC_007163": 2194, - "NC_015781": 14728, - "KP970122": 1089, - "NC_013006": 8517, - "NC_018858": 7954, - "NC_004063": 6318, - "NC_018505": 7757, - "NC_019030": 7001, - "NC_001934": 2593, - "NC_008283": 2750, - "NC_011803": 6449, - "NC_003664": 2622, - "NC_011805": 2737, - "NC_002984": 2632, - "NC_004045": 2843, - "NC_012134": 3350, - "NC_015524": 6519, - "NC_007017": 3883, - "NC_003560": 1003, - "NC_010834": 2663, - "NC_004515": 1356, - "NC_009764": 8727, - "NC_011584": 2776, - "FJ028650": 3478, - "NC_003896": 2766, - "NC_010949": 2561, - "NC_003906": 366, - "EF710625": 1719, - "NC_023308": 976, - "NC_024304": 2602, - "NC_005343": 8741, - "NC_003753": 2157, - "NC_001948": 8744, - "NC_004422": 6303, - "NC_000947": 9760, - "JF446924": 369, - "NC_014717": 3571, - "NC_007147": 9611, - "NC_012041": 2757, - "NC_023685": 1810, - "NC_004098": 2571, - "NC_003483": 7262, - "16TFP078_Partiti_TF2_seg2": 1342, - "NC_024458": 6677, - "NC_009537": 3433, - "NC_003405": 1353, - "NC_003669": 6828, - "NC_003501": 9672, - "NC_002025": 2037, - "NC_012909": 8591, - "AB011404": 1306, - "NC_010126": 1371, - "NC_003539": 284, - "NC_007212": 1347, - "NC_004018": 1782, - "NC_002048": 2584, - "NC_023485": 7435, - "NC_007000": 2721, - "NC_009017": 4172, - "NC_011920": 8222, - "U82447": 2271, - "NC_016086": 2588, - "NC_003772": 3195, - "NC_012541": 1702, - "NC_003809": 2939, - "L07940": 2299, - "NC_004625": 2802, - "NC_016084": 5906, - "NC_003604": 7351, - "NC_003849": 6128, - "NC_006566": 3431, - "NC_018715": 2564, - "NC_026618": 4731, - "NC_007161": 1959, - "NC_012664": 2754, - "NC_003810": 2310, - "NC_003625": 8776, - "NC_001466": 2750, - "NC_008708": 3429, - "NC_003868": 2586, - "HM560702": 1598, - "NC_022962": 1118, - "NC_026240": 3865, - "NC_025245": 733, - "NC_023298": 971, - "NC_008552": 8281, - "AB017630": 2326, - "NC_013101": 987, - "NC_003562": 992, - "NC_005099": 824, - "NC_001346": 2689, - "NC_029800": 3105, - "NC_005052": 1344, - "NC_023292": 1364, - "NC_005896": 7978, - "NC_015299": 2245, - "NC_003730": 2193, - "NC_006458": 781, - "NC_003480": 2056, - "NC_011807": 2307, - "AY697300": 2147, - "NC_011182": 2653, - "NC_016649": 2750, - "NC_011069": 1064, - "NC_016648": 13459, - "NC_022073": 2825, - "NC_005210": 7903, - "NC_006444": 1781, - "NC_014739": 2605, - "NC_003614": 8911, - "AF465545": 2646, - "AB033690": 3574, - "AB795264": 372, - "NC_017824": 10057, - "NC_018456": 2585, - "NC_007647": 13952, - "NC_024808": 4236, - "NC_001409": 7555, - "NC_018519": 17798, - "NC_008780": 2586, - "NC_012869": 8433, - "NC_007913": 9544, - "NC_025837": 5820, - "NC_008729": 3566, - "NC_004101": 2613, - "NC_009088": 2728, - "NC_010944": 6381, - "NC_003570": 2315, - "NC_014509": 4247, - "NC_010954": 9656, - "NC_004007": 2799, - "NC_011592": 8759, - "NC_016579": 2614, - "NC_011557": 1783, - "NC_018404": 2530, - "NC_007159": 4532, - "NC_002802": 308, - "NC_006057": 7334, - "NC_003004": 3607, - "NC_004452": 3644, - "HM560704": 1522, - "S63913": 1607, - "NC_012136": 2214, - "NC_025483": 2553, - "NC_011590": 348, - "ARWaV_2L": 7351, - "NC_015522": 6423, - "NC_021096": 2430, - "NC_018571": 5822, - "NC_013424": 1363, - "NC_025479": 6909, - "NC_001800": 8106, - "NC_006631": 2747, - "NC_024695": 1349, - "NC_009041": 6395, - "NC_016999": 2660, - "NC_013110": 2344, - "NC_015784": 7753, - "EU881937": 3294, - "NC_009556": 1358, - "NC_005854": 2268, - "NC_023015": 1348, - "NC_016574": 2649, - "NC_009245": 1171, - "NC_003763": 1579, - "NC_013218": 6082, - "NC_004635": 2608, - "KP970124": 1235, - "NC_004362": 3332, - "NC_003770": 1938, - "NC_018105": 7659, - "NC_005029": 10538, - "NC_022586": 2786, - "FJ946835": 1133, - "NC_016536": 662, - "NC_017859": 8601, - "NC_008182": 7581, - "NC_026759": 8287, - "NC_003847": 826, - "NC_015928": 1377, - "NC_005136": 9346, - "NC_008034": 7531, - "ARWaV_2M1": 1469, - "AJ440010": 4605, - "NC_003765": 1224, - "JN591720": 5218, - "NC_014597": 1379, - "NC_004011": 10142, - "NC_001495": 3644, - "NC_013012": 1321, - "NC_007160": 3150, - "NC_004582": 2744, - "NC_001873": 6311, - "NC_005976": 1754, - "NC_001836": 16934, - "NC_011809": 2916, - "NC_017989": 1577, - "jq899443": 8400, - "AJ781398": 3125, - "NC_003542": 2173, - "NC_003514": 6746, - "NC_004667": 17919, - "NC_007001": 2091, - "NC_010489": 3279, - "NC_009992": 14214, - "NC_006271": 7034, - "JQ821386": 2602, - "NC_009731": 1346, - "NC_028145": 3579, - "NC_003519": 3005, - "NC_020257": 2685, - "NC_016577": 2633, - "NC_020502": 1364, - "NC_003655": 3732, - "NC_020473": 2743, - "KP970121": 1095, - "NC_002568": 4148, - "NC_003791": 7471, - "NC_012665": 2743, - "NC_016519": 6514, - "NC_003376": 8405, - "NC_004642": 2593, - "NC_014605": 1936, - "NC_003644": 981, - "NC_004014": 6878, - "NC_009612": 2571, - "NC_005347": 2786, - "NC_003883": 2185, - "NC_005057": 1367, - "NC_001464": 371, - "NC_001574": 7161, - "NC_002543": 3080, - "NC_014066": 2612, - "NC_023310": 977, - "NC_013076": 5109, - "NC_003375": 8660, - "NC_005636": 2677, - "NC_019947": 2547, - "NC_003609": 2757, - "DQ152192": 1591, - "NC_009242": 1198, - "NC_010837": 2666, - "NC_002323": 9760, - "FJ550605": 1776, - "NC_017828": 2783, - "NC_003722": 2743, - "NC_008017": 7568, - "NC_003515": 4609, - "NC_003648": 1001, - "NC_003518": 3454, - "NC_004654": 2761, - "NC_021095": 2348, - "NC_013098": 981, - "AF022446": 1685, - "13TF149_Reovirus_TF1_Seg07": 2240, - "NC_002050": 4821, - "NC_007003": 7801, - "NC_009249": 2622, - "NC_003547": 2990, - "NC_003856": 2607, - "NC_022131": 2572, - "NC_004729": 5841, - "NC_023156": 988, - "NC_003516": 1774, - "NC_020897": 3486, - "NC_015227": 3929, - "NC_014904": 9760, - "NC_005288": 9644, - "NC_006960": 2223, - "NC_003403": 1347, - "NC_006272": 3315, - "NC_003754": 3514, - "NC_009520": 1465, - "NC_003844": 3491, - "NC_017004": 2769, - "NC_003400": 6985, - "NC_025484": 2564, - "NC_004750": 5677, - "NC_003649": 3383, - "NC_011024": 2771, - "NC_001553": 360, - "NC_009521": 1479, - "NC_023989": 2830, - "NC_010806": 5808, - "NC_002251": 13222, - "NC_004192": 2753, - "NC_013134": 3007, - "NC_022252": 2034, - "NC_019569": 2647, - "NC_001939": 2544, - "NC_004097": 2615, - "NC_002035": 3050, - "NC_003638": 1007, - "NC_016576": 2661, - "NC_012542": 1434, - "NC_001340": 329, - "NC_014252": 9750, - "NC_008561": 1373, - "NC_009248": 4505, - "NC_001749": 6495, - "NC_004450": 7458, - "NC_001828": 2597, - "NC_015488": 2611, - "NC_015505": 7532, - "NC_025480": 4701, - "NC_008236": 2768, - "NC_002038": 3357, - "NC_003639": 1009, - "NC_014707": 2736, - "NC_011555": 2290, - "NC_008365": 6279, - "AY861351": 1514, - "NC_013801": 1347, - "AF283103": 14861, - "NC_026617": 8905, - "NC_022128": 2872, - "NC_025475": 2547, - "NC_009568": 7522, - "NC_018090": 2741, - "NC_006060": 6653, - "NC_004995": 4048, - "NC_005047": 1350, - "NC_006944": 15450, - "NC_003818": 1017, - "NC_003848": 1118, - "NC_016443": 6139, - "NC_014141": 2708, - "NC_003640": 1000, - "NC_001412": 2994, - "NC_024490": 1406, - "NC_010317": 1074, - "NC_011583": 2797, - "NC_007408": 7009, - "HM560703": 1575, - "NC_009549": 2751, - "NC_025478": 2762, - "AB465308": 1915, - "NC_018935": 1345, - "NC_016441": 9649, - "NC_008307": 4815, - "NC_014709": 2651, - "NC_009645": 2731, - "NC_010560": 8965, - "NC_005286": 3923, - "NC_008292": 8386, - "NC_011108": 4198, - "NC_003710": 1781, - "NC_003857": 2597, - "NC_003656": 3753, - "NC_014967": 3956, - "NC_006262": 10035, - "NC_004035": 9624, - "NC_003773": 4422, - "NC_003867": 2620, - "NC_008794": 2745, - "NC_022007": 2624, - "NC_003778": 1165, - "NC_014127": 6517, - "NC_001780": 3879, - "NC_001777": 3684, - "NC_012799": 10282, - "NC_005904": 9463, - "NC_010988": 4906, - "NC_003708": 2745, - "AJ781401": 2021, - "NC_003481": 3289, - "NC_001361": 8533, - "NC_003764": 1397, - "ARWaV_1M": 1289, - "KT454833": 2660, - "NC_018453": 2771, - "13TF149_Reovirus_TF1_Seg02": 3701, - "NC_011067": 960, - "NC_014545": 5866, - "NC_003798": 324, - "NC_023154": 1002, - "NC_012519": 7781, - "NC_015394": 9682, - "NC_004425": 2417, - "NC_022619": 17486, - "NC_010435": 2740, - "NC_015048": 2682, - "NC_016085": 3935, - "NC_003788": 3384, - "NC_003646": 997, - "HM357461": 2615, - "NC_003546": 2289, - "NC_015468": 4848, - "NC_006950": 6820, - "NC_011556": 2008, - "NC_003889": 359, - "NC_004675": 2766, - "NC_011553": 3478, - "NC_023159": 993, - "NC_013527": 8662, - "NC_001438": 2585, - "NC_006943": 6185, - "NC_009246": 1202, - "ADEPP": 3671, - "NC_004730": 4507, - "NC_011538": 6285, - "NC_008058": 2614, - "NC_003505": 2572, - "NC_008735": 3162, - "NC_008310": 6292, - "NC_003672": 5897, - "Z46353": 1468, - "NC_007920": 10375, - "NC_002034": 3357, - "NC_014746": 1259, - "NC_012538": 2559, - "NC_004092": 741, - "NC_023849": 2723, - "M81487": 1519, - "NC_003865": 2663, - "KC740618": 487, - "AY500881": 1238, - "NC_005974": 13782, - "13TF149_Reovirus_TF1_Seg01": 4091, - "NC_004573": 9899, - "NC_013414": 2733, - "NC_018869": 1360, - "NC_024807": 521, - "NC_003563": 1006, - "NC_021196": 9731, - "NC_014604": 1957, - "NC_003839": 7271, - "NC_002027": 2865, - "NC_011052": 2780, - "NC_010434": 7674, - "NC_005132": 6966, - "NC_004640": 2773, - "NC_026592": 7531, - "HQ840786": 9280, - "GQ131573": 359, - "NC_007069": 13883, - "NC_003845": 2205, - "AB817729": 358, - "NC_011540": 8500, - "NC_023301": 1021, - "NC_024779": 2733, - "NC_025821": 9741, - "NC_029565": 2252, - "NC_002786": 6305, - "NC_014593": 3431, - "NC_009741": 9804, - "NC_024013": 2588, - "NC_019493": 12154, - "NC_006941": 9751, - "EF442668": 1772, - "NC_004147": 2746, - "13TF149_Reovirus_TF1_Seg10": 924, - "S40180": 2227, - "NC_015631": 1395, - "NC_003452": 2404, - "AF228516": 1707, - "NC_003833": 3459, - "NC_003756": 3890, - "NC_007155": 3568, - "NC_009609": 6001, - "NC_003674": 2735, - "NC_014730": 8127, - "KX109927": 6863, - "NC_004751": 5723, - "NC_016436": 16404, - "NC_003537": 10038, - "NC_009563": 1372, - "NC_011086": 6290, - "NC_022799": 3876, - "NC_002792": 6311, - "NC_008492": 2606, - "NC_022232": 2834, - "NC_001661": 19296, - "NC_029798": 4821, - "NC_012482": 2542, - "NC_014142": 2647, - "NC_023175": 10346, - "NC_025220": 733, - "NC_004728": 335, - "NC_015328": 7723, - "NC_009252": 853, - "NC_004781": 1788, - "NC_013423": 1364, - "NC_008377": 2792, - "NC_008579": 1346, - "AJ781399": 2833, - "EU980442": 9905, - "KC855267": 5300, - "NC_020898": 7253, - "NC_004440": 4633, - "ARWaV_2S2": 1618, - "NC_003453": 2053, - "NC_001445": 9741, - "AY542958": 1265, - "AF491352": 1648, - "AM999771": 1920, - "NC_026816": 6688, - "NC_025834": 2739, - "NC_010491": 8919, - "NC_010316": 1087, - "NC_008039": 3374, - "NC_003616": 4972, - "NC_012543": 1187, - "NC_019031": 9508, - "NC_004006": 2293, - "NC_019025": 8328, - "NC_024501": 5519, - "NC_010178": 13071, - "NC_010235": 1361, - "NC_023892": 8659, - "NC_003853": 4253, - "NC_004659": 2603, - "NC_007724": 2745, - "U16732": 991, - "NC_003509": 7711, - "NC_014968": 2799, - "NC_015523": 6175, - "NC_014630": 1331, - "NC_003556": 2766, - "NC_010619": 1196, - "NC_028244": 12706, - "NC_020255": 2690, - "EU099845": 4657, - "NC_005138": 8394, - "MF095096": 8701, - "NC_017988": 1491, - "NC_019944": 1350, - "NC_002026": 3234, - "NC_004005": 2781, - "NC_011070": 974, - "NC_009642": 6375, - "NC_004651": 2714, - "NC_002555": 2622, - "NC_003831": 2579, - "NC_025389": 13100, - "NC_010347": 1485, - "NC_015469": 8876, - "NC_003641": 990, - "ARWaV_2S1": 1588, - "U53224": 3337, - "NC_002164": 6099, - "NC_004609": 2616, - "NC_003564": 1020, - "NC_003741": 6033, - "NC_011705": 1609, - "NC_021484": 5612, - "NC_022617": 1710, - "NC_004042": 2617, - "NC_000939": 4415, - "NC_025266": 2560, - "NC_003465": 2979, - "NC_022230": 6398, - "NC_011541": 9650, - "NC_012666": 1337, - "NC_002041": 7099, - "NC_008494": 2569, - "NC_003866": 2583, - "NC_014607": 1141, - "NC_020234": 1985, - "NC_013096": 994, - "NC_022798": 7598, - "NC_003553": 369, - "NC_011544": 6528, - "NC_003761": 2218, - "NC_010709": 6064, - "NC_003885": 1645, - "NC_026619": 3294, - "NC_014822": 2797, - "NC_001886": 9384, - "NC_013007": 16354, - "NC_001508": 2508, - "NC_023300": 1017, - "NC_003629": 5706, - "NC_005635": 2728, - "NC_002028": 2111, - "NC_009547": 2753, - "NC_010293": 2551, - "X73883": 4646, - "NC_020256": 2649, - "NC_003786": 5344, - "NC_023443": 1365, - "NC_009383": 8590, - "NC_010569": 1362, - "NC_007965": 2557, - "GQ478668": 1240, - "NC_003836": 2386, - "NC_021147": 2430, - "NC_001726": 4201, - "NC_014598": 3948, - "NC_004674": 2799, - "NC_015493": 5821, - "AY706994": 6617, - "U82448": 1730, - "L54073": 3620, - "NC_016660": 2740, - "NC_003850": 377, - "NC_008170": 4986, - "NC_004646": 2578, - "NC_009538": 2914, - "NC_001471": 254, - "NC_017002": 2577, - "EU371896": 3434, - "NC_001917": 2562, - "JF320812": 8651, - "NC_011062": 6539, - "NC_020470": 6337, - "NC_016998": 2675, - "NC_003633": 4114, - "NC_003834": 2944, - "AJ549960": 2608, - "NC_015655": 8082, - "NC_023484": 1359, - "DQ315388": 360, - "NC_009244": 3514, - "NC_008031": 1352, - "NC_001648": 8159, - "NC_003837": 3410, - "NC_014713": 1798, - "NC_007729": 4094, - "NC_014646": 1351, - "NC_010737": 7713, - "NC_018703": 6471, - "JX971982": 2444, - "NC_008037": 2593, - "NC_009646": 2649, - "NC_003378": 7767, - "NC_023875": 1030, - "NC_007219": 1355, - "NC_015494": 1696, - "NC_003800": 3354, - "NC_016575": 2727, - "NC_009995": 9730, - "EU881936": 5870, - "NC_001930": 2576, - "KP970123": 1273, - "NC_001932": 2815, - "KX269872": 3468, - "AY618902": 2743, - "NC_001575": 4449, - "NC_015706": 7718, - "NC_003381": 7389, - "NC_015467": 3067, - "NC_002046": 2642, - "NC_023844": 2220, - "NC_007642": 12807, - "NC_005287": 4421, - "NC_007241": 2012, - "NC_005843": 2606, - "NC_021480": 1060, - "NC_002326": 2915, - "13C203_CV_TF1_A": 17143, - "NC_002692": 6383, - "NC_002990": 7637, - "NC_004628": 2754, - "NC_011362": 356, - "NC_015324": 2824, - "EF016486": 2605, - "NC_008191": 1866, - "NC_003890": 1374, - "NC_018151": 3640, - "NC_007221": 1677, - "NC_016087": 1724, - "NC_024906": 17656, - "NC_015507": 7769, - "NC_014536": 9870, - "NC_004060": 4132, - "NC_007730": 2745, - "NC_004666": 5685, - "NC_020471": 6226, - "NC_017827": 2754, - "NC_011532": 12926, - "NC_013109": 2390, - "NC_025481": 2117, - "NC_010738": 8463, - "NC_024687": 2741, - "NC_016581": 2494, - "NC_003559": 999, - "NC_013261": 9502, - "NC_006056": 3820, - "NC_024298": 5534, - "NC_001469": 4701, - "NC_013266": 3249, - "NC_014647": 2754, - "NC_023303": 970, - "NC_008793": 2762, - "NC_022127": 3481, - "NC_005319": 2746, - "NC_012118": 2764, - "NC_022615": 1837, - "NC_005985": 3904, - "NC_005096": 3929, - "AB518485": 7727, - "NC_003507": 1720, - "FJ647429": 399, - "NC_003503": 4616, - "NC_003369": 5964, - "DQ143874": 2644, - "13TF107_BFV_TF1": 8403, - "FM160943": 2634, - "AB366022": 396, - "NC_003843": 3534, - "NC_010836": 2498, - "NC_008366": 17039, - "AF185955": 1642, - "NC_008779": 2611, - "NC_008733": 4501, - "NC_003634": 6673, - "NC_008038": 2129, - "NC_024117": 2785, - "AY710267": 2917, - "NC_004810": 7976, - "NC_015506": 7408, - "NC_004658": 2587, - "GU234166": 10557, - "NC_016984": 2747, - "KJ541745": 4710, - "NC_008249": 5900, - "NC_004733": 1348, - "M84483": 2265, - "NC_018573": 1358, - "NC_003550": 3481, - "NC_003657": 3560, - "NC_010521": 9836, - "NC_010294": 2604, - "NC_005060": 1370, - "NC_015552": 6791, - "NC_021481": 5736, - "NC_029566": 1752, - "NC_018671": 692, - "NC_011659": 6633, - "NC_003757": 1132, - "NC_003651": 2659, - "NC_013100": 984, - "NC_004016": 7550, - "NC_017829": 1436, - "PSZBETACD": 3591, - "NC_009032": 5403, - "JQ080272": 305, - "NC_004426": 9878, - "NC_005338": 2761, - "NC_003811": 3855, - "NC_016572": 2661, - "NC_014379": 1334, - "NC_004036": 7857, - "NC_003711": 1611, - "NC_004644": 2535, - "EF121755": 2621, - "NC_015962": 2625, - "NC_001753": 7015, - "NC_008736": 2186, - "NC_003835": 2229, - "NC_027720": 4830, - "NC_010344": 1521, - "NC_022961": 8424, - "NC_003003": 5951, - "NC_001642": 6366, - "NC_000882": 2744, - "NC_003814": 1022, - "NC_003031": 7687, - "NC_015487": 2641, - "NC_004824": 2742, - "NC_003872": 1432, - "NC_013464": 2744, - "NC_007639": 1365, - "NC_006066": 1938, - "NC_014902": 2727, - "NC_028649": 6888, - "NC_004608": 2745, - "NC_001343": 7489, - "AY388995": 1625, - "NC_025477": 3404, - "NC_004099": 2642, - "NC_014603": 1996, - "AF185957": 1685, - "NC_029568": 1212, - "NC_003759": 2682, - "AB465309": 1730, - "NC_013262": 7586, - "NC_024886": 1002, - "NC_001546": 300, - "NC_018574": 1316, - "NC_003536": 9584, - "NC_017938": 3753, - "NC_009892": 9005, - "NC_013219": 3985, - "NC_003838": 3074, - "NC_003451": 3373, - "NC_002324": 4056, - "NC_010561": 8530, - "NC_004752": 9608, - "NC_003495": 3662, - "NC_013022": 2791, - "NC_023881": 1001, - "NC_026238": 7836, - "NC_002049": 2542, - "NC_011808": 3431, - "NC_013103": 1369, - "NC_003803": 2800, - "NC_006948": 5914, - "NC_004634": 2731, - "NC_018091": 1379, - "NC_002160": 5695, - "NC_007213": 1355, - "NC_003821": 4054, - "NC_014711": 1928, - "NC_001555": 9494, - "NC_025258": 2737, - "NC_013593": 1387, - "NC_003652": 1430, - "NC_006265": 5723, - "NC_011190": 5862, - "NC_029563": 1490, - "NC_016404": 7488, - "GU479877": 555, - "AF185956": 1622, - "NC_003689": 7383, - "NC_008266": 8539, - "NC_003709": 2753, - "NC_009532": 6223, - "NC_007216": 9695, - "NC_001658": 6376, - "FR823302": 1675, - "NC_009013": 7808, - "NC_001936": 2634, - "NC_007223": 677, - "NC_018718": 2598, - "AY312434": 3469, - "NC_004047": 9612, - "NC_003474": 1043, - "NC_001983": 2723, - "NC_002981": 2748, - "eu099844": 6634, - "NC_024777": 687, - "NC_015045": 2717, - "NC_004657": 2605, - "NC_003473": 1075, - "NC_014790": 9682, - "NC_002633": 6562, - "NC_014648": 7543, - "NC_003643": 977, - "NC_008056": 2577, - "NC_023161": 979, - "NC_003632": 7059, - "NC_021197": 9868, - "NC_018576": 2806, - "DQ178610": 2575, - "NC_003620": 4801, - "NC_009608": 6413, - "NC_020475": 1390, - "NC_003877": 8747, - "NC_006059": 6624, - "NC_001747": 5987, - "NC_018455": 8224, - "NC_015489": 2551, - "NC_022745": 9359, - "NC_010346": 1749, - "NC_023160": 978, - "NC_001265": 4003, - "NC_007648": 17635, - "NC_006567": 2570, - "NC_015122": 2625, - "NC_012038": 8542, - "NC_010647": 2630, - "NC_026616": 8041, - "NC_007158": 3623, - "NC_004090": 2768, - "NC_005289": 5817, - "NC_007339": 2736, - "NC_015395": 6942, - "NC_021929": 673, - "NC_023339": 4767, - "NC_003887": 2513, - "NC_003732": 1936, - "NC_024301": 7424, - "NC_006065": 2810, - "NC_018458": 7551, - "NC_001830": 315, - "NC_022616": 1936, - "NC_012546": 1040, - "NC_004322": 6056, - "NC_014547": 2762, - "NC_025388": 9409, - "AY250986": 2291, - "FR823301": 1365, - "NC_003774": 3512, - "NC_011554": 2879, - "NC_014795": 2572, - "NC_012535": 4475, - "NC_015327": 1396, - "NC_007289": 8478, - "NC_001369": 2589, - "NC_003878": 6513, - "NC_021851": 6277, - "KC855266": 7586, - "NC_003799": 5865, - "NC_004324": 8253, - "NC_007816": 4431, - "NC_001504": 4266, - "NC_003557": 8363, - "FJ550604": 1971, - "NC_024009": 2772, - "NC_001483": 6151, - "NC_009545": 2761, - "13SL157": 8674, - "NC_010416": 6937, - "NC_001935": 2547, - "NC_004009": 618, - "KC706605": 2644, - "NC_003658": 3427, - "NC_018577": 2789, - "NC_004639": 2661, - "NC_018628": 1478, - "NC_010238": 2756, - "AM999772": 1757, - "NC_008737": 1870, - "U20621": 3082, - "NC_026761": 2756, - "NC_001739": 8178, - "NC_006316": 1447, - "NC_003469": 3768, - "NC_004124": 8223, - "GQ225585": 8010, - "NC_002556": 2572, - "NC_019547": 1378, - "NC_008559": 2728, - "AF203538": 1754, - "NC_006550": 9104, - "FR694185": 1716, - "NC_003723": 6043, - "NC_003671": 2438, - "13SL134": 5849, - "NC_025250": 9553, - "NC_007433": 10429, - "13C210_Leutovirus_TF1": 5791, - "NC_007609": 6181, - "NC_018082": 1376, - "NC_005285": 4744, - "NC_008706": 2245, - "NC_020996": 8397, - "NC_003611": 256, - "16TFP078_Partiti_TF2_seg1": 1569, - "NC_008560": 1357, - "NC_008495": 2514, - "NC_003380": 220, - "NC_016580": 2540, - "NC_003736": 3164, - "NC_006999": 3176, - "NC_015628": 2553, - "NC_012728": 7961, - "NC_003477": 1018, - "Z46352": 1598, - "NC_021735": 7038, - "NC_001812": 6227, - "NC_014380": 2628, - "HQ588147": 1120, - "AB084453": 3372, - "NC_002047": 2585, - "NC_001785": 10326, - "L75930": 2336, - "NC_003199": 2754, - "NC_015298": 7026, - "NC_013111": 2740, - "NC_002356": 3569, - "U16736": 988, - "NC_003497": 836, - "NC_008732": 3826, - "NC_003860": 2662, - "NC_016440": 8638, - "NC_025468": 8047, - "NC_015929": 1343, - "NC_022072": 18643, - "NC_012484": 6506, - "NC_001367": 6395, - "NC_001440": 2216, - "NC_010797": 2736, - "NC_018833": 9544, - "NC_023864": 2858, - "NC_003548": 3404, - "NC_002039": 2947, - "NC_001696": 4279, - "NC_023302": 1006, - "13SL170_SHMV": 6532, - "NC_022251": 2603, - "NC_011106": 7564, - "NC_010950": 2569, - "NC_006939": 3683, - "NC_005903": 9540, - "NC_019024": 2598, - "NC_009241": 1578, - "NC_003568": 2874, - "Kt273413": 6701, - "NC_021202": 2739, - "AF228693": 1294, - "NC_027719": 2947, - "NC_009740": 1349, - "NC_017970": 10731, - "NC_001746": 6362, - "NC_002618": 4082, - "NC_002036": 7073, - "KF826466": 3414, - "NC_008018": 7722, - "NC_017917": 2882, - "NC_003825": 2861, - "NC_024693": 2831, - "NC_008305": 2602, - "AB274027": 4090, - "NC_006460": 615, - "NC_003613": 370, - "NC_001933": 2645, - "NC_010352": 2746, - "NC_004655": 2785, - "NC_021094": 2435, - "NC_004039": 9585, - "NC_011702": 13696, - "NC_008707": 2876, - "NC_009805": 9636, - "NC_029578": 4020, - "NC_019029": 6956, - "NC_015228": 8837, - "JF803258": 2561, - "NC_004106": 6524, - "NC_008730": 3617, - "NC_024457": 991, - "NC_025435": 5986, - "NC_004540": 7591, - "NC_004364": 1957, - "NC_001478": 2701, - "NC_022008": 2584, - "NC_022129": 2224, - "L14952": 1991, - "NC_004071": 2755, - "NC_005850": 2560, - "NC_020253": 2676, - "NC_003502": 6405, - "NC_009571": 1346, - "NC_025469": 8155, - "NC_016003": 6495, - "FR823300": 2135, - "NC_007222": 1500, - "NC_006053": 1527, - "NC_012536": 3522, - "NC_017914": 4138, - "NC_001517": 9640, - "16TFP175_VV_TF2": 3542, - "NC_008558": 10851, - "AF028004": 2316, - "NC_003630": 6357, - "NC_009010": 7261, - "NC_015502": 7519, - "NC_005975": 12133, - "NC_012537": 3180, - "NC_022644": 2754, - "NC_017913": 2589, - "NC_003357": 2737, - "NC_005849": 2922, - "NC_012553": 2609, - "NC_019412": 9538, - "NC_019497": 2650, - "NC_011560": 9973, - "NC_005846": 2705, - "NC_002359": 7147, - "NC_003768": 1066, - "NC_003742": 9663, - "NC_001441": 6955, - "NC_009535": 1346, - "NC_001721": 3699, - "NC_023851": 2383, - "NC_016963": 689, - "NC_009994": 9570, - "NC_024372": 1374, - "X52627": 1313, - "NC_011536": 4547, - "NC_004637": 2668, - "NC_001728": 6618, - "NC_015229": 6704, - "KC588948": 6497, - "NC_011559": 6151, - "NC_024809": 2691, - "16SP029_GVD": 7489, - "NC_011525": 7890, - "NC_007679": 6582, - "NC_005874": 1354, - "NC_005875": 667, - "AJ223827": 1346, - "NC_007538": 2279, - "NC_009555": 1322, - "NC_006062": 8035, - "NC_004010": 9848, - "NC_007459": 1358, - "NC_005321": 2751, - "NC_015415": 6360, - "NC_014139": 2618, - "NC_009250": 1648, - "NC_015782": 7275, - "NC_003743": 5641, - "NC_017939": 7386, - "NC_003792": 4667, - "NC_003510": 6003, - "NC_008605": 1367, - "NC_003767": 1036, - "NC_023297": 975, - "NC_024502": 3869, - "NC_001598": 15480, - "NC_007210": 2758, - "NC_018457": 2623, - "NC_004100": 2605, - "NC_003264": 292, - "NC_004546": 1354, - "NC_014601": 3650, - "NC_026615": 9720, - "JN427014": 7566, - "NC_003508": 1203, - "NC_001814": 9535, - "NC_022006": 3532, - "NC_006965": 3842, - "NC_003787": 6812, - "NC_004627": 2734, - "NC_018530": 2816, - "NC_003661": 1640, - "NC_008731": 2639, - "NC_014714": 4500, - "NC_004580": 2630, - "AJ488768": 2622, - "NC_007340": 8595, - "NC_015490": 2640, - "NC_020105": 9796, - "NC_003642": 989, - "NC_006964": 7496, - "JF803265": 2531, - "NC_023628": 9687, - "NC_001725": 7876, - "NC_018072": 4886, - "NC_000874": 5899, - "NC_018070": 9040, - "NC_001841": 10820, - "NC_013106": 2335, - "NC_003543": 3171, - "NC_008303": 4823, - "U16733": 1002, - "NC_010490": 4945, - "NC_003446": 5619, - "NC_004439": 7358, - "NC_004156": 231621, - "NC_003647": 1022, - "NC_007735": 1331, - "NC_015046": 2680, - "NC_029562": 7039, - "NC_003619": 3057, - "NC_004013": 9465, - "NC_013447": 16614, - "NC_010835": 2560, - "NC_007727": 2626, - "NC_014446": 2608, - "NC_012540": 1665, - "NC_016044": 9595, - "NC_003820": 5845, - "NC_005049": 1356, - "NC_018449": 4145, - "NC_003861": 2755, - "NC_023016": 6080, - "13TF149_Reovirus_TF1_Seg11": 965, - "NC_027527": 8041, - "NC_004122": 2213, - "NC_001868": 2706, - "NC_003517": 1465, - "NC_024491": 2778, - "NC_003751": 3699, - "NC_004091": 2749, - "NC_004630": 2741, - "NC_013097": 980, - "NC_004067": 6450, - "NC_017977": 9908, - "NC_011919": 2751, - "NC_013467": 2814, - "NC_025265": 2564, - "NC_026819": 7641, - "16SP042_EM_CAM1_A_RNA1": 7127, - "NC_003806": 3876, - "NC_004346": 4258, - "NC_013802": 1354, - "TPM": 360, - "NC_007731": 6607, - "NC_024373": 1353, - "NC_016136": 12875, - "NC_003804": 2760, - "NC_003520": 5834, - "NC_002052": 8897, - "NC_003347": 7564, - "NC_023850": 2806, - "NC_004782": 1515, - "NC_003841": 4880, - "NC_010328": 1339, - "NC_001793": 6509, - "NC_001339": 4789, - "NC_026763": 2737, - "NC_013105": 7040, - "NC_002743": 682, - "NC_001748": 6656, - "EF427894": 9455, - "NC_010818": 2737, - "NC_020999": 9314, - "NC_004825": 2713, - "NC_004626": 2741, - "NC_006955": 7650, - "NC_004366": 4152, - "NC_003445": 7036, - "AB510477": 3137, - "NC_023309": 971, - "DQ860839": 17039, - "U16735": 1017, - "NC_018579": 2816, - "NC_001625": 4193, - "NC_020073": 4772, - "NC_003541": 2774, - "NC_005331": 2656, - "NC_006052": 1830, - "NC_002817": 2767, - "NC_015627": 2601, - "AY312436": 1831, - "NC_003752": 2157, - "NC_007655": 1354, - "NC_022002": 3206, - "HQ588148": 945, - "16SP042_BFV_CAM1_B": 7780, - "NC_011537": 6206, - "NC_019498": 1479, - "NC_020252": 22786, - "NC_022004": 6068, - "NC_015047": 2637, - "NC_024710": 1437, - "NC_006275": 1955, - "NC_003558": 1004, - "KJ541744": 2485, - "NC_008183": 6364, - "NC_001600": 4041, - "NC_008734": 1798, - "NC_003621": 4441, - "NC_024476": 1934, - "NC_015495": 1415, - "NC_014129": 361, - "NC_003531": 1403, - "NC_014794": 2627, - "JN117276": 1563, - "KJ886934": 1607, - "NC_023307": 1001, - "AF467107": 1115, - "NC_018572": 9847, - "NC_012120": 2725, - "NC_014744": 1383, - "NC_010792": 2708, - "NC_003566": 986, - "NC_003434": 2746, - "ARWaV_1L": 7212, - "NC_004423": 5600, - "HQ449546": 5929, - "NC_002588": 6364, - "EU559678": 5093, - "NC_023641": 15088, - "NC_024449": 8431, - "NC_003842": 2926, - "NC_005290": 3446, - "DQ357617": 402, - "NC_025252": 6158, - "NC_009490": 2609, - "NC_003513": 1320, - "NC_003884": 2203, - "NC_009903": 1354, - "AB518486": 3854, - "NC_014895": 1373, - "NC_011189": 3418, - "NC_006315": 1645, - "NC_003511": 2913, - "NC_003771": 3823, - "NC_003569": 3431, - "NC_028111": 8334, - "NC_009251": 1652, - "NC_002351": 7025, - "NC_004356": 2750, - "NC_025381": 6431, - "NC_023983": 7475, - "NC_001671": 9924, - "NC_002797": 1787, - "FR772082": 2748, - "NC_010538": 8550, - "NC_025681": 2782, - "NC_021148": 2354, - "NC_010952": 2605, - "NC_009546": 2757, - "NC_004780": 1402, - "AF016626": 7977, - "NC_004662": 2593, - "NC_006963": 7916, - "NC_010809": 5674, - "NC_003626": 11832, - "NC_014898": 10113, - "NC_002468": 8432, - "NC_001465": 1291, - "NC_010307": 2725, - "NC_003688": 5669, - "FJ561293": 2078, - "NC_022365": 7562, - "NC_002328": 2584, - "NC_003762": 2449, - "NC_008057": 2558, - "NC_009519": 1734, - "GQ497733": 1145, - "NC_015317": 2782, - "NC_010648": 2593, - "NC_003775": 1448, - "NC_008237": 1358, - "NC_011065": 2769, - "NC_003545": 5957, - "NC_011058": 2584, - "NC_014715": 3815, - "NC_021873": 1712, - "NC_007192": 5823, - "NC_005097": 7514, - "NC_002330": 3683, - "GQ376201": 8220, - "NC_024374": 1392, - "NC_003769": 1162, - "NC_003138": 8303, - "NC_002729": 7352, - "NC_026253": 2661, - "13TF170_Tymovirus_TF1": 7206, - "X79270": 3006, - "NC_016033": 4152, - "NC_022645": 2804, - "NC_006359": 2691, - "NC_003734": 3812, - "NC_029076": 10423, - "NC_002552": 8612, - "NC_010832": 7212, - "NC_018071": 2584, - "NC_008393": 9890, - "NC_014140": 2768, - "NC_023304": 1017, - "NC_007728": 10046, - "EU919669": 2821, - "NC_017967": 9660, - "NC_003414": 1360, - "NC_006568": 2484, - "NC_014327": 9745, - "DQ011234": 4764, - "JQ821387": 2591, - "NC_004015": 3418, - "NC_021245": 2742, - "NC_009647": 2672, - "NC_010953": 2553, - "NC_014906": 1356, - "DQ143875": 1794, - "AM774001": 1603, - "NC_021564": 5983, - "NC_016159": 9626, - "NC_004581": 2550, - "NC_011515": 4193, - "NC_023306": 1011, - "NC_023155": 983, - "NC_009745": 9711, - "NC_003886": 1445, - "NC_003744": 2758, - "NC_014447": 2578, - "NC_002350": 7636, - "NC_003852": 6507, - "NC_023684": 2020, - "NC_018448": 8857, - "NC_003567": 1007, - "NC_010710": 4020, - "NC_002985": 2600, - "NC_004706": 1372, - "AF228515": 1720, - "NC_014130": 2611, - "NC_011558": 6797, - "NC_004424": 2831, - "U20622": 1908, - "NC_028139": 5909, - "NC_018872": 9512, - "NC_024471": 9292, - "NC_004093": 1305, - "NC_001977": 6035, - "KC622054": 529, - "NC_014481": 3427, - "NC_005851": 2525, - "NC_006276": 1708, - "NC_001647": 2705, - "NC_022919": 1127, - "AY312435": 2337, - "NC_014065": 1370, - "NC_011346": 2776, - "NC_004096": 2595, - "NC_007156": 1843, - "NC_001938": 2601, - "NC_021097": 2353, - "NC_003056": 5853, - "NC_014473": 2744, - "NC_018176": 9630, - "NC_012698": 8995, - "NC_001839": 7206, - "NC_009450": 1350, - "NC_020254": 2664, - "NC_003729": 4501, - "NC_001914": 8002, - "NC_013258": 8271, - "NC_010314": 1090, - "NC_003670": 1799, - "NC_003854": 717, - "NC_003776": 2504, - "NC_004904": 1342, - "NC_016083": 2601, - "NC_003500": 4760, - "X87254": 1828, - "NC_002358": 7111, - "NC_009607": 2593, - "NC_004641": 2760, - "AB158522": 1867, - "NC_024895": 2716, - "NC_003615": 7342, - "NC_003794": 5966, - "NC_006935": 1348, - "NC_001634": 8174, - "NC_003855": 393, - "NC_021705": 4195, - "NC_001931": 2615, - "NC_003738": 3543, - "NC_001632": 12226, - "NC_007537": 2299, - "NC_013800": 1344, - "NC_010947": 2755, - "NC_013014": 1874, - "NC_022894": 7838, - "NC_015961": 2605, - "NC_013013": 1378, - "JN700748": 4772, - "NC_006054": 1417, - "NC_003660": 1994, - "NC_003840": 8214, - "KX354202": 6719, - "NC_015393": 9999, - "NC_010343": 1717, - "NC_019499": 1350, - "NC_021706": 10770, - "NC_002349": 3659, - "NC_008284": 2726, - "NC_006957": 1209, - "NC_001410": 247, - "JF694486": 2744, - "NC_003871": 2835, - "NC_003808": 3439, - "AB084452": 5836, - "NC_008300": 3232, - "HQ402596": 3006, - "NC_010948": 2622, - "NC_005844": 2742, - "NC_025482": 3130, - "JQ429791": 2652, - "NC_028144": 7178, - "NC_019032": 2591, - "NC_006952": 1379, - "13TF149_Reovirus_TF1_Seg06": 1825, - "NC_006445": 1586, - "NC_001818": 3803, - "EF151292": 337, - "NC_024455": 13954, - "AB079655": 1607, - "NC_014799": 2542, - "NC_008306": 8918, - "KT893296": 9654, - "NC_006956": 1032, - "NC_003665": 2573, - "NC_013095": 992, - "NC_014740": 2582, - "KF584011": 1368, - "NC_003530": 3840, - "NC_026472": 7661, - "NC_014716": 3618, - "NC_003224": 9591, - "NC_004756": 5666, - "NC_009247": 2542, - "NC_003093": 7560, - "NC_015397": 2566, - "NC_006384": 2735, - "NC_014038": 9965, - "NC_026760": 2932, - "NC_025964": 2752, - "NC_005051": 1307, - "NC_007002": 7263, - "NC_014905": 9917, - "NC_007966": 2492, - "NC_003200": 1351, - "NC_018451": 301, - "NC_003728": 3572, - "NC_013465": 2807, - "NC_012481": 2597, - "NC_010345": 1485, - "13TF149_Reovirus_TF1_Seg05": 2059, - "FR823303": 1718, - "NC_011804": 2740, - "NC_006267": 1346, - "NC_018093": 10798, - "NC_003602": 7599, - "NC_003464": 3476, - "NC_026771": 2952, - "NC_010319": 1099, - "NC_014708": 3167, - "NC_007711": 1354, - "NC_005209": 8006, - "NC_015300": 1544, - "NC_012544": 1205, - "NC_008301": 3477, - "NC_003506": 6683, - "NC_003476": 1089, - "NC_022250": 3352, - "NC_013094": 1003, - "NC_018175": 8410, - "NC_014631": 3065, - "NC_001651": 315, - "JN117277": 1512, - "NC_002800": 1585, - "NC_003326": 2750, - "NC_007154": 3820, - "NC_007338": 2719, - "NC_013011": 1315, - "NC_003399": 9324, - "NC_003479": 1111, - "NC_003610": 6514, - "NC_003805": 6791, - "NC_004020": 1186, - "ARWaV_2M2": 1326, - "NC_004012": 8451, - "NC_011543": 4652, - "NC_002327": 2704, - "NC_003504": 2603, - "NC_003606": 9779, - "NC_008028": 9548, - "NC_018403": 2507, - "NC_023295": 7315, - "NC_010618": 2747, - "NC_009550": 2740, - "NC_029799": 8880, - "NC_014596": 2707, - "NC_009561": 1346, - "NC_003377": 9515, - "NC_007733": 3964, - "NC_007290": 2751, - "NC_019568": 2632, - "NC_018578": 2716, - "NC_003795": 8832, - "NC_008716": 6794, - "NC_005977": 1582, - "NC_013112": 1137, - "NC_001480": 6331, - "16SP042_BFV_CAM2_B": 7435, - "NC_007721": 1350, - "NC_006961": 2296, - "NC_003462": 9332, - "NC_001937": 2607, - "NC_009449": 1336, - "NC_018616": 7916, - "KT005821": 354, - "AY340584": 1040, - "NC_002500": 8018, - "NC_015043": 2679, - "DQ450199": 1815, - "NC_024686": 6835, - } diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 3d58be3..657ab28 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -1,5 +1,5 @@ -import gzip -import json +import asyncio +import re import shutil import pysam @@ -12,13 +12,25 @@ from virtool.caches.utils import derive_key from virtool.workflow import RunSubprocess from virtool.workflow.data.analyses import WFAnalysis -from virtool.workflow.data.cache import CacheHit, CacheMiss +from virtool.workflow.data.cache import WorkflowCache from virtool.workflow.data.indexes import WFIndex from virtool.workflow.data.samples import WFSample from virtool.workflow.data.subtractions import WFSubtraction +from virtool.workflow.errors import JobsAPINotFoundError from virtool.workflow.pytest_plugin import WorkflowData +from fixtures import ( + get_collapsed_reference_path, + get_isolate_bam_path, + get_isolate_fastq_path, + get_isolate_index_path, + get_isolate_path, + get_reference_index_path, + get_subtracted_bam_path, + get_subtraction_indexes_path, +) from workflow import ( + collapse_reference, build_isolate_index, create_reference_index, create_subtraction_index, @@ -28,10 +40,13 @@ map_isolates, reassignment, ) -from workflow_pathoscope.utils import ( - get_mapping_index_cache_params, - write_default_isolate_fasta, +from workflow_pathoscope.reference import ( + CD_HIT_EST_IDENTITY, + _prepare_otu_collapse, + collapse_reference_index, + get_reference_collapse_cache_params, ) +from workflow_pathoscope.utils import get_mapping_index_cache_params BOWTIE2_INDEX_SUFFIXES = ( @@ -42,6 +57,7 @@ "rev.1.bt2", "rev.2.bt2", ) +TOOL_VERSION_PATTERN = re.compile(r"\d+(?:\.\d+)+(?:[-+._A-Za-z0-9]*)?") @pytest.fixture() @@ -54,12 +70,17 @@ def work_path(tmpdir): @pytest.fixture() def reference_index_path(work_path: Path) -> Path: - return work_path / "reference_index" / "reference" + return get_reference_index_path(work_path) + + +@pytest.fixture() +def collapsed_reference_path(work_path: Path) -> Path: + return get_collapsed_reference_path(work_path) @pytest.fixture() def subtraction_indexes_path(work_path: Path) -> Path: - return work_path / "subtraction_indexes" + return get_subtraction_indexes_path(work_path) @pytest.fixture() @@ -70,71 +91,85 @@ def subtraction_index_path( return get_subtraction_index_path(subtraction_indexes_path, subtractions[0].id) -class FakeWorkflowCache: +@pytest.fixture() +def isolate_path(work_path: Path) -> Path: + path = get_isolate_path(work_path) + path.mkdir() + + return path + + +@pytest.fixture() +def isolate_fastq_path(isolate_path: Path) -> Path: + return get_isolate_fastq_path(isolate_path) + + +@pytest.fixture() +def isolate_index_path(isolate_path: Path) -> Path: + return get_isolate_index_path(isolate_path) + + +@pytest.fixture() +def isolate_bam_path(isolate_path: Path) -> Path: + return get_isolate_bam_path(isolate_path) + + +@pytest.fixture() +def subtracted_bam_path(work_path: Path) -> Path: + return get_subtracted_bam_path(work_path) + + +class _FakeWorkflowCacheAPI: + """Fake only the API calls used by the real workflow cache.""" + def __init__( self, - hit_source: Path | None = None, + work_dir: Path, + *, put_exception: Exception | None = None, - put_created: bool = True, - ): - self.hit_source = hit_source + put_created: bool | None = None, + ) -> None: + self.work_dir = work_dir self.put_exception = put_exception self.put_created = put_created - self.gets = [] - self.puts = [] - - async def get(self, key: str, target: Path): - self.gets.append((key, target)) - - if self.hit_source is None: - return CacheMiss(key) - - target.mkdir(parents=True, exist_ok=True) + self.stored: dict[str, tuple[Path, dict | None]] = {} - restored_path = target / self.hit_source.name - shutil.copytree(self.hit_source, restored_path) + async def get_cache(self, key: str, dest: Path) -> None: + try: + source, _ = self.stored[key] + except KeyError: + raise JobsAPINotFoundError from None - return CacheHit(key, restored_path) + dest.parent.mkdir(parents=True, exist_ok=True) + await asyncio.to_thread(shutil.copyfile, source, dest) - async def put(self, key: str, source: Path, params: dict | None = None): - self.puts.append((key, source, params)) - - if self.put_exception is not None: + async def put_cache( + self, + key: str, + path: Path, + params: dict | None = None, + ) -> bool: + if self.put_exception: raise self.put_exception - return self.put_created + if key in self.stored: + return False + stored_path = self.work_dir / "stored" / key / "cache.tar" + stored_path.parent.mkdir(parents=True, exist_ok=True) + await asyncio.to_thread(shutil.copyfile, path, stored_path) + self.stored[key] = (stored_path, params) -class FakeRunSubprocess: - def __init__(self): - self.commands = [] + if self.put_created is not None: + return self.put_created - async def __call__( - self, - command: list[str], - cwd: str | Path | None = None, - env: dict | None = None, - stderr_handler=None, - stdout_handler=None, - ): - self.commands.append(command) - - if command == ["bowtie2-build", "--version"]: - await stdout_handler(b"/usr/bin/bowtie2-build-s version 2.5.4\n") - return SimpleNamespace(returncode=0) - - if command[0] == "bowtie2-build": - prefix = Path(command[-1]) - prefix.parent.mkdir(parents=True, exist_ok=True) - - for suffix in BOWTIE2_INDEX_SUFFIXES: - (prefix.parent / f"{prefix.name}.{suffix}").write_bytes( - f"{prefix.name}.{suffix}".encode(), - ) + return True - return SimpleNamespace(returncode=0) - raise AssertionError(f"Unexpected subprocess command: {command}") +@pytest.fixture() +def workflow_cache(tmp_path: Path) -> WorkflowCache: + api = _FakeWorkflowCacheAPI(tmp_path / "fake_workflow_cache_api") + return WorkflowCache(api, tmp_path / "workflow_cache") def read_directory_bytes(path: Path) -> dict[str, bytes]: @@ -163,48 +198,91 @@ def write_bowtie2_bundle( (path / name).write_bytes(file_content) -def write_reference_json(path: Path): - path.parent.mkdir(parents=True, exist_ok=True) - - opener = gzip.open if path.suffix == ".gz" else open +def assert_bowtie2_index_exists(prefix: Path): + assert read_directory_bytes(prefix.parent).keys() == { + f"{prefix.name}.{suffix}" for suffix in BOWTIE2_INDEX_SUFFIXES + } - with opener(path, "wt") as f: - json.dump( + for suffix in BOWTIE2_INDEX_SUFFIXES: + assert (prefix.parent / f"{prefix.name}.{suffix}").stat().st_size > 0 + + +def assert_cache_params( + params: dict[str, str], + expected: dict[str, str], +) -> None: + assert params.keys() == {*expected.keys(), "tool_version"} + assert { + key: value for key, value in params.items() if key != "tool_version" + } == expected + assert TOOL_VERSION_PATTERN.fullmatch(params["tool_version"]) + + +async def create_test_index(path: Path) -> WFIndex: + def sequence(id_: str, value: str) -> dict: + return { + "id": id_, + "accession": id_, + "definition": id_, + "host": "", + "segment": None, + "sequence": value, + } + + def isolate(id_: str, default: bool, sequences: list[dict]) -> dict: + return { + "id": id_, + "default": default, + "sequences": sequences, + "source_name": id_, + "source_type": "isolate", + } + + return await WFIndex.create( + "test-index", + path, + None, + [ { - "otus": [ - { - "_id": "default-otu", - "isolates": [ - { - "default": True, - "sequences": [ - {"_id": "default-a", "sequence": "ACGT"}, - {"_id": "default-b", "sequence": "TTA"}, - ], - }, - { - "default": False, - "sequences": [ - {"_id": "non-default", "sequence": "GGGG"}, - ], - }, + "id": "default-otu", + "abbreviation": "DEFAULT", + "isolates": [ + isolate( + "default", + True, + [ + sequence("default-a", "ACGT"), + sequence("default-b", "TTA"), ], - }, - { - "_id": "non-default-otu", - "isolates": [ - { - "default": False, - "sequences": [ - {"_id": "non-default-only", "sequence": "CCCC"}, - ], - }, - ], - }, + ), + isolate( + "non-default", + False, + [sequence("non-default", "GGGG")], + ), ], + "name": "Default OTU", + "schema": [], + "taxid": None, + "version": 1, }, - f, - ) + { + "id": "non-default-otu", + "abbreviation": "NONDEFAULT", + "isolates": [ + isolate( + "non-default-only", + False, + [sequence("non-default-only", "CCCC")], + ), + ], + "name": "Non-default OTU", + "schema": [], + "taxid": None, + "version": 1, + }, + ], + ) @pytest.fixture() @@ -220,54 +298,887 @@ def analysis(workflow_data: WorkflowData, mocker): @pytest.fixture() -def index(workflow_data: WorkflowData, example_path: Path, work_path: Path): - workflow_data.index.manifest = {"foobar": 10, "reo": 5, "baz": 6} - - index_path = work_path / "indexes" / workflow_data.index.id - - shutil.copytree(example_path / "index", index_path) - - return WFIndex( - id=workflow_data.index.id, - path=index_path, - manifest=workflow_data.index.manifest, - reference=workflow_data.index.reference, - sequence_lengths={}, - sequence_otu_map={ - "NC_016509": "foobar", - "NC_001948": "foobar", - "13TF149_Reovirus_TF1_Seg06": "reo", - "13TF149_Reovirus_TF1_Seg03": "reo", - "13TF149_Reovirus_TF1_Seg07": "reo", - "13TF149_Reovirus_TF1_Seg02": "reo", - "13TF149_Reovirus_TF1_Seg08": "reo", - "13TF149_Reovirus_TF1_Seg11": "reo", - "13TF149_Reovirus_TF1_Seg04": "reo", - "NC_004667": "foobar", - "NC_003347": "foobar", - "NC_003615": "foobar", - "NC_003689": "foobar", - "NC_011552": "foobar", - "KX109927": "baz", - "NC_008039": "foobar", - "NC_015782": "foobar", - "NC_016416": "foobar", - "NC_003623": "foobar", - "NC_008038": "foobar", - "NC_001836": "foobar", - "JQ080272": "baz", - "NC_017938": "foobar", - "NC_008037": "foobar", - "NC_007448": "foobar", +async def index(workflow_data: WorkflowData, work_path: Path): + sequence_otu_map = { + "NC_016509": "foobar", + "NC_001948": "foobar", + "13TF149_Reovirus_TF1_Seg06": "reo", + "13TF149_Reovirus_TF1_Seg03": "reo", + "13TF149_Reovirus_TF1_Seg07": "reo", + "13TF149_Reovirus_TF1_Seg02": "reo", + "13TF149_Reovirus_TF1_Seg08": "reo", + "13TF149_Reovirus_TF1_Seg11": "reo", + "13TF149_Reovirus_TF1_Seg04": "reo", + "NC_004667": "foobar", + "NC_003347": "foobar", + "NC_003615": "foobar", + "NC_003689": "foobar", + "NC_011552": "foobar", + "KX109927": "baz", + "NC_008039": "foobar", + "NC_015782": "foobar", + "NC_016416": "foobar", + "NC_003623": "foobar", + "NC_008038": "foobar", + "NC_001836": "foobar", + "JQ080272": "baz", + "NC_017938": "foobar", + "NC_008037": "foobar", + "NC_007448": "foobar", + } + manifest = {"foobar": 10, "reo": 5, "baz": 6} + otus = [] + + for otu_id, version in manifest.items(): + otus.append( + { + "id": otu_id, + "abbreviation": otu_id.upper(), + "isolates": [ + { + "default": True, + "id": f"{otu_id}-isolate", + "sequences": [ + { + "id": sequence_id, + "accession": sequence_id, + "definition": sequence_id, + "host": "", + "segment": None, + "sequence": "ACGT", + } + for sequence_id, sequence_otu_id in sequence_otu_map.items() + if sequence_otu_id == otu_id + ], + "source_name": otu_id, + "source_type": "isolate", + }, + ], + "name": otu_id, + "schema": [], + "taxid": None, + "version": version, + }, + ) + + return await WFIndex.create( + workflow_data.index.id, + work_path / "indexes" / workflow_data.index.id / "index.sqlite", + None, + otus, + ) + + +def make_index_sequence( + sequence_id: str, + *, + segment: object, + sequence: str = "ACGT", +) -> dict: + return { + "id": sequence_id, + "accession": sequence_id, + "definition": sequence_id, + "host": "", + "segment": segment, + "sequence": sequence, + } + + +def make_index_isolate( + isolate_id: str, + *, + sequences: list[dict], + default: bool = False, +) -> dict: + return { + "id": isolate_id, + "default": default, + "sequences": sequences, + "source_name": isolate_id, + "source_type": "isolate", + } + + +def make_index_otu( + otu_id: str, + *, + isolates: list[dict], + schema: list[str], +) -> dict: + return { + "id": otu_id, + "abbreviation": "COLLAPSE", + "name": "Collapse OTU", + "taxid": None, + "version": 1, + "schema": [{"name": segment} for segment in schema], + "isolates": isolates, + } + + +def make_redundant_index_otu() -> dict: + sequence_data = { + "default": ( + "ACGTACGTACGTACGTACGT", + "TGCATGCATGCATGCATGCA", + ), + "representative-1": ( + "ACGTACGTACGTACGTACGT", + "TGCAGGATCGTTTAACGTAG", + ), + "representative-2": ( + "CCCCAAAAGGGGTTTTCCCC", + "AAAACCCCGGGGTTTTAAAA", + ), + "unique-combo": ( + "CCCCAAAAGGGGTTTTCCCC", + "TGCAGGATCGTTTAACGTAG", + ), + "duplicate": ( + "CCCCAAAAGGGGTTTTCCCC", + "AAAACCCCGGGGTTTTAAAA", + ), + } + + return make_index_otu( + "collapse-otu", + schema=["a", "b"], + isolates=[ + make_index_isolate( + isolate_id, + default=isolate_id == "default", + sequences=[ + make_index_sequence( + f"{isolate_id}-a", + segment="a", + sequence=sequences[0], + ), + make_index_sequence( + f"{isolate_id}-b", + segment="b", + sequence=sequences[1], + ), + ], + ) + for isolate_id, sequences in sequence_data.items() + ], + ) + + +@pytest.fixture() +async def redundant_index(workflow_data: WorkflowData, tmp_path: Path) -> WFIndex: + return await WFIndex.create( + workflow_data.index.id, + tmp_path / "redundant-index.sqlite", + None, + [make_redundant_index_otu()], + ) + + +async def test_collapse_reference_hit( + collapsed_reference_path: Path, + index: WFIndex, + run_subprocess: RunSubprocess, + tmp_path: Path, + workflow_cache: WorkflowCache, +): + source = tmp_path / collapsed_reference_path.parent.name + source.mkdir() + await create_test_index(source / collapsed_reference_path.name) + params = await get_reference_collapse_cache_params(index.id, run_subprocess) + key = derive_key(params) + assert await workflow_cache.put(key, source, params) + + logger = get_logger("test") + + result_path = await collapse_reference( + workflow_cache, + collapsed_reference_path, + index, + logger, + 4, + run_subprocess, + ) + + assert result_path == collapsed_reference_path + assert read_directory_bytes( + collapsed_reference_path.parent + ) == read_directory_bytes(source) + + +async def test_collapse_reference_miss_retains_required_isolates( + collapsed_reference_path: Path, + redundant_index: WFIndex, + run_subprocess: RunSubprocess, + workflow_cache: WorkflowCache, +): + logger = get_logger("test") + + result_path = await collapse_reference( + workflow_cache, + collapsed_reference_path, + redundant_index, + logger, + 4, + run_subprocess, + ) + + params = await get_reference_collapse_cache_params( + redundant_index.id, + run_subprocess, + ) + + assert result_path == collapsed_reference_path + assert_cache_params( + params, + { + "index_kind": "collapsed_reference", + "workflow": "pathoscope", + "workflow_version": "UNKNOWN", + "parent_id": redundant_index.id, + "source": "index_sqlite", + "tool_name": "cd-hit-est", + "identity": "0.99", }, ) + collapsed_index = WFIndex.load(redundant_index.id, collapsed_reference_path) + collapsed_otus = [otu async for otu in collapsed_index.iter_otus()] + + assert [isolate["id"] for isolate in collapsed_otus[0]["isolates"]] == [ + "default", + "representative-1", + "representative-2", + "unique-combo", + ] + assert [ + (isolate["sequences"][0]["id"], isolate["sequences"][1]["id"]) + for isolate in collapsed_otus[0]["isolates"] + ] == [ + ("default-a", "default-b"), + ("representative-1-a", "representative-1-b"), + ("representative-2-a", "representative-2-b"), + ("unique-combo-a", "unique-combo-b"), + ] + assert not (collapsed_reference_path.parent / "collapse-manifest.json").exists() + + +async def test_collapse_reference_index_outputs_collapsed_reference_fasta( + redundant_index: WFIndex, + run_subprocess: RunSubprocess, + tmp_path: Path, +): + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + default_fasta_path = tmp_path / "default.fa" + isolate_fasta_path = tmp_path / "isolates.fa" + + assert await collapse_reference_index( + redundant_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 5, + "isolate_count_after": 4, + "isolate_count_removed": 1, + "otu_count_collapsed": 1, + "otu_count_unchanged": 0, + "otu_count_skipped": 0, + } + + collapsed_index = WFIndex.load(redundant_index.id, collapsed_path) + + await collapsed_index.write_fasta( + default_fasta_path, + collapsed_index.iter_default_sequences(), + ) + await collapsed_index.write_fasta( + isolate_fasta_path, + collapsed_index.iter_otu_sequences("collapse-otu"), + ) + + assert default_fasta_path.read_text() == ( + ">default-a\nACGTACGTACGTACGTACGT\n>default-b\nTGCATGCATGCATGCATGCA\n" + ) + assert "duplicate-a" not in isolate_fasta_path.read_text() + assert "duplicate-b" not in isolate_fasta_path.read_text() + + +async def test_collapse_reference_index_allows_isolates_missing_schema_segments( + run_subprocess: RunSubprocess, + tmp_path: Path, +): + otu = make_redundant_index_otu() + default_isolate = next( + isolate for isolate in otu["isolates"] if isolate["id"] == "default" + ) + default_isolate["sequences"] = [ + sequence + for sequence in default_isolate["sequences"] + if sequence["segment"] == "b" + ] + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 5, + "isolate_count_after": 4, + "isolate_count_removed": 1, + "otu_count_collapsed": 1, + "otu_count_unchanged": 0, + "otu_count_skipped": 0, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + collapsed_otus = [otu async for otu in collapsed_index.iter_otus()] + + assert [ + sequence["id"] for sequence in collapsed_otus[0]["isolates"][0]["sequences"] + ] == ["default-b"] + + +async def test_collapse_reference_index_allows_unsegmented_isolates( + run_subprocess: RunSubprocess, + tmp_path: Path, +): + otu = make_index_otu( + "collapse-otu", + schema=[], + isolates=[ + make_index_isolate( + "default", + default=True, + sequences=[ + make_index_sequence( + "default", + segment=None, + sequence="ACGTACGTACGTACGTACGT", + ), + ], + ), + make_index_isolate( + "representative-1", + sequences=[ + make_index_sequence( + "representative-1", + segment="ignored", + sequence="ACGTACGTACGTACGTACGT", + ), + ], + ), + make_index_isolate( + "representative-2", + sequences=[ + make_index_sequence( + "representative-2", + segment="ignored", + sequence="CCCCAAAAGGGGTTTTCCCC", + ), + ], + ), + make_index_isolate( + "unique-combo", + sequences=[ + make_index_sequence( + "unique-combo", + segment="ignored", + sequence="CCCCAAAAGGGGTTTTCCCC", + ), + ], + ), + make_index_isolate( + "duplicate", + sequences=[ + make_index_sequence( + "duplicate", + segment="ignored", + sequence="CCCCAAAAGGGGTTTTCCCC", + ), + ], + ), + ], + ) + + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 5, + "isolate_count_after": 2, + "isolate_count_removed": 3, + "otu_count_collapsed": 1, + "otu_count_unchanged": 0, + "otu_count_skipped": 0, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + collapsed_otus = [otu async for otu in collapsed_index.iter_otus()] + + assert collapsed_otus[0]["schema"] == [] + assert [isolate["id"] for isolate in collapsed_otus[0]["isolates"]] == [ + "default", + "representative-2", + ] + + +async def test_collapse_reference_index_rejects_multiple_sequences_for_unsegmented_otu( + mocker, + tmp_path: Path, +): + otu = make_index_otu( + "unsegmented", + schema=[], + isolates=[ + make_index_isolate( + "multiple-sequences", + default=True, + sequences=[ + make_index_sequence("sequence-1", segment=None), + make_index_sequence("sequence-2", segment=None), + ], + ), + make_index_isolate( + "single-sequence", + sequences=[make_index_sequence("sequence-3", segment=None)], + ), + ], + ) + + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + run_subprocess = mocker.AsyncMock() + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 2, + "isolate_count_after": 2, + "isolate_count_removed": 0, + "otu_count_collapsed": 0, + "otu_count_unchanged": 0, + "otu_count_skipped": 1, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + assert [otu async for otu in collapsed_index.iter_otus()] == [ + otu async for otu in source_index.iter_otus() + ] + run_subprocess.assert_not_awaited() + + +async def test_collapse_reference_index_preserves_single_unsegmented_isolate( + mocker, + tmp_path: Path, +): + otu = make_index_otu( + "unsegmented", + schema=[], + isolates=[ + make_index_isolate( + "only-isolate", + default=True, + sequences=[ + make_index_sequence("sequence-1", segment=None), + make_index_sequence("sequence-2", segment=None), + ], + ), + ], + ) + run_subprocess = mocker.AsyncMock() + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 1, + "isolate_count_after": 1, + "isolate_count_removed": 0, + "otu_count_collapsed": 0, + "otu_count_unchanged": 1, + "otu_count_skipped": 0, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + collapsed_otus = [otu async for otu in collapsed_index.iter_otus()] + + assert [ + sequence["id"] for sequence in collapsed_otus[0]["isolates"][0]["sequences"] + ] == ["sequence-1", "sequence-2"] + run_subprocess.assert_not_awaited() + + +async def test_collapse_reference_index_preserves_single_schema_isolate_without_validation( + mocker, + tmp_path: Path, +): + otu = make_index_otu( + "segmented", + schema=["a", "b"], + isolates=[ + make_index_isolate( + "only-isolate", + default=True, + sequences=[ + make_index_sequence("sequence-a", segment=None), + make_index_sequence("sequence-b", segment="b"), + ], + ), + ], + ) + run_subprocess = mocker.AsyncMock() + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) == { + "isolate_count_before": 1, + "isolate_count_after": 1, + "isolate_count_removed": 0, + "otu_count_collapsed": 0, + "otu_count_unchanged": 1, + "otu_count_skipped": 0, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + assert [otu async for otu in collapsed_index.iter_otus()] == [ + otu async for otu in source_index.iter_otus() + ] + run_subprocess.assert_not_awaited() + + +@pytest.mark.parametrize("segment", [None, ""]) +async def test_collapse_reference_index_skips_invalid_schema_segment_names( + mocker, + segment, + tmp_path: Path, +): + otu = make_index_otu( + "segmented", + schema=["a", "b"], + isolates=[ + make_index_isolate( + "invalid", + default=True, + sequences=[ + make_index_sequence("invalid-a", segment=segment), + make_index_sequence("invalid-b", segment="b"), + ], + ), + make_index_isolate( + "valid", + sequences=[ + make_index_sequence("valid-a", segment="a"), + make_index_sequence("valid-b", segment="b"), + ], + ), + ], + ) + run_subprocess = mocker.AsyncMock() + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + summary = await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) + + assert summary["otu_count_skipped"] == 1 + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + assert [otu async for otu in collapsed_index.iter_otus()] == [ + otu async for otu in source_index.iter_otus() + ] + run_subprocess.assert_not_awaited() + + +def test_otu_collapse_preparation_rejects_non_string_schema_segment(): + otu = make_index_otu( + "segmented", + schema=["a"], + isolates=[ + make_index_isolate( + "invalid", + sequences=[make_index_sequence("invalid-a", segment=42)], + ), + ], + ) + + preparation = _prepare_otu_collapse(otu) + + assert preparation.eligible is False + assert preparation.sequences_by_segment == {} + + +def test_otu_collapse_preparation_rejects_empty_schemaless_isolate(): + otu = make_index_otu( + "unsegmented", + schema=[], + isolates=[ + make_index_isolate("empty", sequences=[]), + make_index_isolate( + "valid", + sequences=[make_index_sequence("valid", segment=None)], + ), + ], + ) + + preparation = _prepare_otu_collapse(otu) + + assert preparation.eligible is False + assert preparation.sequences_by_segment == {} + + +async def test_collapse_reference_index_rejects_duplicate_isolate_segments( + mocker, + tmp_path: Path, +): + otu = make_index_otu( + "segmented", + schema=["a", "b"], + isolates=[ + make_index_isolate( + "duplicate", + default=True, + sequences=[ + make_index_sequence("duplicate-a-1", segment="a"), + make_index_sequence("duplicate-a-2", segment="a"), + ], + ), + make_index_isolate( + "valid", + sequences=[ + make_index_sequence("valid-a", segment="a"), + make_index_sequence("valid-b", segment="b"), + ], + ), + ], + ) + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + run_subprocess = mocker.AsyncMock() + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + summary = await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) + + assert summary["otu_count_skipped"] == 1 + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + assert [otu async for otu in collapsed_index.iter_otus()] == [ + otu async for otu in source_index.iter_otus() + ] + run_subprocess.assert_not_awaited() + + +async def test_collapse_reference_index_rejects_unknown_isolate_segments( + mocker, + tmp_path: Path, +): + otu = make_index_otu( + "segmented", + schema=["a", "b"], + isolates=[ + make_index_isolate( + "unknown", + default=True, + sequences=[ + make_index_sequence("unknown-c", segment="c"), + make_index_sequence("unknown-b", segment="b"), + ], + ), + make_index_isolate( + "valid", + sequences=[ + make_index_sequence("valid-a", segment="a"), + make_index_sequence("valid-b", segment="b"), + ], + ), + ], + ) + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [otu], + ) + run_subprocess = mocker.AsyncMock() + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + + summary = await collapse_reference_index( + source_index, + collapsed_path, + 2, + run_subprocess, + ) + + assert summary["otu_count_skipped"] == 1 + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + assert [otu async for otu in collapsed_index.iter_otus()] == [ + otu async for otu in source_index.iter_otus() + ] + run_subprocess.assert_not_awaited() + + +async def test_collapse_reference_index_handles_mixed_otu_outcomes( + run_subprocess: RunSubprocess, + tmp_path: Path, +): + eligible_otu = make_redundant_index_otu() + skipped_otu = make_index_otu( + "skipped", + schema=["a"], + isolates=[ + make_index_isolate( + "skipped-invalid", + default=True, + sequences=[ + make_index_sequence("skipped-invalid", segment=None), + ], + ), + make_index_isolate( + "skipped-valid", + sequences=[ + make_index_sequence("skipped-valid", segment="a"), + ], + ), + ], + ) + unchanged_otu = make_index_otu( + "unchanged", + schema=["a"], + isolates=[ + make_index_isolate( + "unchanged-invalid", + default=True, + sequences=[ + make_index_sequence("unchanged-invalid", segment=None), + ], + ), + ], + ) + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [eligible_otu, skipped_otu, unchanged_otu], + ) + collapsed_path = tmp_path / "collapsed" / "index.sqlite" + subprocess_commands: list[list[str]] = [] + + async def tracked_run_subprocess(command, **kwargs): + subprocess_commands.append(command) + return await run_subprocess(command, **kwargs) + + assert await collapse_reference_index( + source_index, + collapsed_path, + 2, + tracked_run_subprocess, + ) == { + "isolate_count_before": 8, + "isolate_count_after": 7, + "isolate_count_removed": 1, + "otu_count_collapsed": 1, + "otu_count_unchanged": 1, + "otu_count_skipped": 1, + } + + collapsed_index = WFIndex.load(source_index.id, collapsed_path) + collapsed_otus = {otu["id"]: otu async for otu in collapsed_index.iter_otus()} + source_otus = {otu["id"]: otu async for otu in source_index.iter_otus()} + + assert len(collapsed_otus["collapse-otu"]["isolates"]) == 4 + assert collapsed_otus["skipped"] == source_otus["skipped"] + assert collapsed_otus["unchanged"] == source_otus["unchanged"] + assert len(subprocess_commands) == 2 + + +async def test_collapse_reference_index_propagates_subprocess_failures( + mocker, + tmp_path: Path, +): + source_index = await WFIndex.create( + "test-index", + tmp_path / "source.sqlite", + None, + [make_redundant_index_otu()], + ) + run_subprocess = mocker.AsyncMock(side_effect=RuntimeError("cd-hit-est failed")) + + with pytest.raises(RuntimeError, match="cd-hit-est failed"): + await collapse_reference_index( + source_index, + tmp_path / "collapsed" / "index.sqlite", + 2, + run_subprocess, + ) + @pytest.fixture() def sample(workflow_data: WorkflowData, example_path: Path, work_path: Path): workflow_data.sample.library_type = "normal" - path = work_path / "samples" / workflow_data.sample.id + path = work_path / "samples" / str(workflow_data.sample.id) path.mkdir(parents=True) shutil.copyfile(example_path / "sample" / "reads_1.fq.gz", path / "reads_1.fq.gz") @@ -304,11 +1215,14 @@ def subtractions(workflow_data: WorkflowData, example_path: Path, work_path: Pat async def test_create_reference_index_hit( + collapsed_reference_path: Path, index: WFIndex, reference_index_path: Path, + run_subprocess: RunSubprocess, tmp_path: Path, + workflow_cache: WorkflowCache, ): - write_reference_json(index.json_path) + await create_test_index(collapsed_reference_path) source = tmp_path / reference_index_path.parent.name write_bowtie2_bundle( source, @@ -316,12 +1230,24 @@ async def test_create_reference_index_hit( b"cached-reference", {"cache-manifest.json": b"cached-manifest"}, ) - cache = FakeWorkflowCache(source) - run_subprocess = FakeRunSubprocess() + params = await get_mapping_index_cache_params( + "reference_mapping_index", + index.id, + run_subprocess, + { + "collapse_identity": CD_HIT_EST_IDENTITY, + "source": "collapsed_reference", + "selection": "default_isolates", + }, + ) + key = derive_key(params) + assert await workflow_cache.put(key, source, params) + logger = get_logger("test") result_index_path = await create_reference_index( - cache, + workflow_cache, + collapsed_reference_path, index, logger, 4, @@ -329,41 +1255,25 @@ async def test_create_reference_index_hit( reference_index_path, ) - params = await get_mapping_index_cache_params( - "reference_mapping_index", - index.id, - FakeRunSubprocess(), - { - "source": "index_json", - "selection": "default_isolates", - }, - ) - - assert cache.gets == [ - ( - derive_key(params), - reference_index_path.parent.parent, - ), - ] - assert cache.puts == [] assert result_index_path == reference_index_path - assert run_subprocess.commands == [["bowtie2-build", "--version"]] assert read_directory_bytes(reference_index_path.parent) == read_directory_bytes( source ) async def test_create_reference_index_miss( + collapsed_reference_path: Path, index: WFIndex, reference_index_path: Path, + run_subprocess: RunSubprocess, + workflow_cache: WorkflowCache, ): - write_reference_json(index.json_path) - cache = FakeWorkflowCache() - run_subprocess = FakeRunSubprocess() + await create_test_index(collapsed_reference_path) logger = get_logger("test") result_index_path = await create_reference_index( - cache, + workflow_cache, + collapsed_reference_path, index, logger, 4, @@ -374,51 +1284,50 @@ async def test_create_reference_index_miss( params = await get_mapping_index_cache_params( "reference_mapping_index", index.id, - FakeRunSubprocess(), + run_subprocess, { - "source": "index_json", + "collapse_identity": CD_HIT_EST_IDENTITY, + "source": "collapsed_reference", "selection": "default_isolates", }, ) - key = derive_key(params) - assert cache.gets == [(key, reference_index_path.parent.parent)] - assert cache.puts == [(key, reference_index_path.parent, params)] assert result_index_path == reference_index_path - assert params == { - "index_kind": "reference_mapping_index", - "workflow": "pathoscope", - "workflow_version": "UNKNOWN", - "parent_id": index.id, - "source": "index_json", - "selection": "default_isolates", - "tool_name": "bowtie2-build", - "tool_version": "2.5.4", - } - assert len(run_subprocess.commands) == 2 - assert run_subprocess.commands[0] == ["bowtie2-build", "--version"] - assert run_subprocess.commands[1][:3] == [ - "bowtie2-build", - "--threads", - "4", - ] - assert Path(run_subprocess.commands[1][3]).name == "reference.fa" - assert run_subprocess.commands[1][4] == str(reference_index_path) - assert read_directory_bytes(reference_index_path.parent) == { - **bowtie2_bundle_bytes("reference"), - } + assert_cache_params( + params, + { + "index_kind": "reference_mapping_index", + "workflow": "pathoscope", + "workflow_version": "UNKNOWN", + "parent_id": index.id, + "collapse_identity": "0.99", + "source": "collapsed_reference", + "selection": "default_isolates", + "tool_name": "bowtie2-build", + }, + ) + assert_bowtie2_index_exists(reference_index_path) async def test_create_reference_index_continues_when_cache_put_is_skipped( + collapsed_reference_path: Path, index: WFIndex, reference_index_path: Path, + run_subprocess: RunSubprocess, + tmp_path: Path, ): - write_reference_json(index.json_path) - cache = FakeWorkflowCache(put_created=False) - run_subprocess = FakeRunSubprocess() + await create_test_index(collapsed_reference_path) + cache = WorkflowCache( + _FakeWorkflowCacheAPI( + tmp_path / "fake_workflow_cache_api", + put_created=False, + ), + tmp_path / "workflow_cache", + ) logger = get_logger("test") result_index_path = await create_reference_index( cache, + collapsed_reference_path, index, logger, 4, @@ -426,36 +1335,31 @@ async def test_create_reference_index_continues_when_cache_put_is_skipped( reference_index_path, ) - params = await get_mapping_index_cache_params( - "reference_mapping_index", - index.id, - FakeRunSubprocess(), - { - "source": "index_json", - "selection": "default_isolates", - }, - ) - key = derive_key(params) - assert cache.gets == [(key, reference_index_path.parent.parent)] - assert cache.puts == [(key, reference_index_path.parent, params)] assert result_index_path == reference_index_path - assert read_directory_bytes(reference_index_path.parent) == { - **bowtie2_bundle_bytes("reference"), - } + assert_bowtie2_index_exists(reference_index_path) async def test_create_reference_index_raises_unexpected_cache_put_failure( + collapsed_reference_path: Path, index: WFIndex, reference_index_path: Path, + run_subprocess: RunSubprocess, + tmp_path: Path, ): - write_reference_json(index.json_path) - cache = FakeWorkflowCache(put_exception=RuntimeError("cache upload failed")) - run_subprocess = FakeRunSubprocess() + await create_test_index(collapsed_reference_path) + cache = WorkflowCache( + _FakeWorkflowCacheAPI( + tmp_path / "fake_workflow_cache_api", + put_exception=RuntimeError("cache upload failed"), + ), + tmp_path / "workflow_cache", + ) logger = get_logger("test") with pytest.raises(RuntimeError, match="cache upload failed"): await create_reference_index( cache, + collapsed_reference_path, index, logger, 4, @@ -464,24 +1368,13 @@ async def test_create_reference_index_raises_unexpected_cache_put_failure( ) -def test_write_default_isolate_fasta(tmp_path: Path): - json_path = tmp_path / "reference.json.gz" - target_path = tmp_path / "reference.fa" - - write_reference_json(json_path) - - assert write_default_isolate_fasta(json_path, target_path) == { - "default-a": 4, - "default-b": 3, - } - assert target_path.read_text() == ">default-a\nACGT\n>default-b\nTTA\n" - - async def test_create_subtraction_index_hit( + run_subprocess: RunSubprocess, subtractions: list[WFSubtraction], subtraction_index_path: Path, subtraction_indexes_path: Path, tmp_path: Path, + workflow_cache: WorkflowCache, ): source = tmp_path / subtraction_index_path.parent.name write_bowtie2_bundle( @@ -490,13 +1383,19 @@ async def test_create_subtraction_index_hit( b"cached-subtraction", {"cache-manifest.json": b"cached-manifest"}, ) - cache = FakeWorkflowCache(source) - run_subprocess = FakeRunSubprocess() - logger = get_logger("test") subtraction = subtractions[0] + params = await get_mapping_index_cache_params( + "subtraction_mapping_index", + subtraction.id, + run_subprocess, + ) + key = derive_key(params) + assert await workflow_cache.put(key, source, params) + + logger = get_logger("test") result_indexes_path = await create_subtraction_index( - cache, + workflow_cache, logger, 4, run_subprocess, @@ -504,38 +1403,24 @@ async def test_create_subtraction_index_hit( subtraction_indexes_path, ) - params = await get_mapping_index_cache_params( - "subtraction_mapping_index", - subtraction.id, - FakeRunSubprocess(), - ) - - assert cache.gets == [ - ( - derive_key(params), - subtraction_index_path.parent.parent, - ), - ] - assert cache.puts == [] assert result_indexes_path == subtraction_indexes_path - assert run_subprocess.commands == [["bowtie2-build", "--version"]] assert read_directory_bytes(subtraction_index_path.parent) == read_directory_bytes( source ) async def test_create_subtraction_index_miss( + run_subprocess: RunSubprocess, subtractions: list[WFSubtraction], subtraction_index_path: Path, subtraction_indexes_path: Path, + workflow_cache: WorkflowCache, ): - cache = FakeWorkflowCache() - run_subprocess = FakeRunSubprocess() logger = get_logger("test") subtraction = subtractions[0] result_indexes_path = await create_subtraction_index( - cache, + workflow_cache, logger, 4, run_subprocess, @@ -546,33 +1431,20 @@ async def test_create_subtraction_index_miss( params = await get_mapping_index_cache_params( "subtraction_mapping_index", subtraction.id, - FakeRunSubprocess(), + run_subprocess, ) - key = derive_key(params) - assert cache.gets == [(key, subtraction_index_path.parent.parent)] - assert cache.puts == [(key, subtraction_index_path.parent, params)] assert result_indexes_path == subtraction_indexes_path - assert params == { - "index_kind": "subtraction_mapping_index", - "workflow": "pathoscope", - "workflow_version": "UNKNOWN", - "parent_id": subtraction.id, - "tool_name": "bowtie2-build", - "tool_version": "2.5.4", - } - assert run_subprocess.commands == [ - ["bowtie2-build", "--version"], - [ - "bowtie2-build", - "--threads", - "4", - str(subtraction.fasta_path), - str(subtraction_index_path), - ], - ] - assert read_directory_bytes(subtraction_index_path.parent) == bowtie2_bundle_bytes( - "subtraction" + assert_cache_params( + params, + { + "index_kind": "subtraction_mapping_index", + "workflow": "pathoscope", + "workflow_version": "UNKNOWN", + "parent_id": subtraction.id, + "tool_name": "bowtie2-build", + }, ) + assert_bowtie2_index_exists(subtraction_index_path) async def test_map_default_isolates( @@ -589,7 +1461,7 @@ async def test_map_default_isolates( if "reference" in path.name: shutil.copyfile(path, reference_index_path.parent / path.name) - intermediate = SimpleNamespace(to_otus=set()) + intermediate = SimpleNamespace(candidate_sequence_ids=set()) logger = get_logger("test") @@ -603,31 +1475,29 @@ async def test_map_default_isolates( sample, ) - assert sorted(intermediate.to_otus) == snapshot + assert sorted(intermediate.candidate_sequence_ids) == snapshot async def test_map_isolates( example_path: Path, index: WFIndex, + isolate_bam_path: Path, + isolate_fastq_path: Path, + isolate_index_path: Path, sample: WFSample, run_subprocess: RunSubprocess, snapshot: SnapshotAssertion, - work_path: Path, ): for path in (example_path / "index").iterdir(): if "reference" in path.name: shutil.copyfile( path, - work_path / path.name.replace("reference", "isolates"), + isolate_index_path.parent / path.name.replace("reference", "isolates"), ) - isolate_fastq_path = work_path / "mapped.fq" - isolate_index_path = work_path / "isolates" - isolate_bam_path = work_path / "to_isolates.bam" - proc = 1 - intermediate = SimpleNamespace(lengths={"foo": 100}) + intermediate = SimpleNamespace(candidate_sequence_ids={"candidate"}) await map_isolates( intermediate, @@ -658,18 +1528,18 @@ async def test_map_isolates( ) async def test_eliminate_subtraction( example_path: Path, + isolate_bam_path: Path, + isolate_fastq_path: Path, no_subtractions: bool, + subtracted_bam_path: Path, subtractions: list[WFSubtraction], subtraction_indexes_path: Path, run_subprocess: RunSubprocess, snapshot: SnapshotAssertion, tmp_path: Path, + workflow_cache: WorkflowCache, work_path: Path, ): - isolate_fastq_path = work_path / "to_isolates.fq" - isolate_bam_path = work_path / "to_isolates.bam" - subtracted_path = work_path / "subtracted.bam" - shutil.copyfile(example_path / "to_isolates.bam", isolate_bam_path) shutil.copyfile(example_path / "to_isolates.fq", isolate_fastq_path) @@ -680,17 +1550,24 @@ async def test_eliminate_subtraction( if no_subtractions: subtractions = [] - intermediate = SimpleNamespace(lengths={"foo": 100}) + intermediate = SimpleNamespace(candidate_sequence_ids={"candidate"}) if subtractions: cached_subtraction_path = tmp_path / str(subtractions[0].id) shutil.copytree(subtractions[0].path, cached_subtraction_path) + params = await get_mapping_index_cache_params( + "subtraction_mapping_index", + subtractions[0].id, + run_subprocess, + ) + key = derive_key(params) + assert await workflow_cache.put(key, cached_subtraction_path, params) await create_subtraction_index( - FakeWorkflowCache(cached_subtraction_path), + workflow_cache, logger, proc, - FakeRunSubprocess(), + run_subprocess, subtractions, subtraction_indexes_path, ) @@ -706,7 +1583,7 @@ async def test_eliminate_subtraction( run_subprocess, subtraction_indexes_path, subtractions, - subtracted_path, + subtracted_bam_path, work_path, ) @@ -756,8 +1633,8 @@ def parse_alignments(path: Path) -> set[tuple]: for read in alignment_file } - assert parse_alignments(work_path / "subtracted.bam") == snapshot(name="alignments") - assert parse_headers(work_path / "subtracted.bam") == snapshot(name="headers") + assert parse_alignments(subtracted_bam_path) == snapshot(name="alignments") + assert parse_headers(subtracted_bam_path) == snapshot(name="headers") async def test_pathoscope( @@ -765,15 +1642,13 @@ async def test_pathoscope( example_path: Path, index: WFIndex, mocker, - ref_lengths, snapshot: SnapshotAssertion, + subtracted_bam_path: Path, work_path: Path, ): - subtracted_bam_path = work_path / "to_isolates.bam" - shutil.copyfile(example_path / "to_isolates.bam", subtracted_bam_path) - intermediate = SimpleNamespace(lengths=ref_lengths) + intermediate = SimpleNamespace(candidate_sequence_ids={"candidate"}) p_score_cutoff = 0.01 results = {} @@ -836,7 +1711,9 @@ async def test_pathoscope( async def test_build_isolate_index_no_candidates( + collapsed_reference_path: Path, index: WFIndex, + mocker, work_path: Path, ): """When no candidate OTUs are found the isolate FASTA is empty. @@ -844,15 +1721,14 @@ async def test_build_isolate_index_no_candidates( ``bowtie2-build`` exits 1 on an empty FASTA, so the index build must be skipped entirely (VIR-2569) rather than invoking the subprocess. """ - write_reference_json(index.json_path) - isolate_path = work_path / "isolates" isolate_path.mkdir() - run_subprocess = FakeRunSubprocess() - intermediate = SimpleNamespace(to_otus=set()) + run_subprocess = mocker.AsyncMock() + intermediate = SimpleNamespace(candidate_sequence_ids=set()) await build_isolate_index( + collapsed_reference_path, index, intermediate, isolate_path / "isolate_index.fa", @@ -861,13 +1737,13 @@ async def test_build_isolate_index_no_candidates( 2, ) - assert intermediate.lengths == {} - assert run_subprocess.commands == [] + run_subprocess.assert_not_awaited() async def test_no_candidates_uploads_empty_result( analysis: WFAnalysis, index: WFIndex, + mocker, sample: WFSample, work_path: Path, ): @@ -876,8 +1752,8 @@ async def test_no_candidates_uploads_empty_result( ``map_isolates``, ``eliminate_subtraction`` and ``reassignment`` must not run any subprocess and the analysis is finished with an empty result (VIR-2569). """ - run_subprocess = FakeRunSubprocess() - intermediate = SimpleNamespace(lengths={}) + run_subprocess = mocker.AsyncMock() + intermediate = SimpleNamespace(candidate_sequence_ids=set()) logger = get_logger("test") results = {} @@ -919,7 +1795,7 @@ async def test_no_candidates_uploads_empty_result( work_path, ) - assert run_subprocess.commands == [] + run_subprocess.assert_not_awaited() analysis.upload_result.assert_called_once_with( {"subtracted_count": 0, "read_count": 0, "hits": []}, ) diff --git a/uv.lock b/uv.lock index 4d73eff..9e3d301 100644 --- a/uv.lock +++ b/uv.lock @@ -107,16 +107,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.18.4" +version = "1.18.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, ] [package.optional-dependencies] @@ -348,14 +348,14 @@ wheels = [ [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -422,14 +422,14 @@ wheels = [ [[package]] name = "faker" -version = "40.21.0" +version = "40.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/6f/d7b251fb31de7dce0e482680bf7ca876aa0043f475c04aeefa1459ea80d4/faker-40.21.0.tar.gz", hash = "sha256:2fdee1b650a723a54432db9c6dfe17cfa29d1adc8bd60520444a07698524ba4d", size = 1970295, upload-time = "2026-06-02T17:53:46.27Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/2d/3ead106cef42eab305e15f9c089dcc68a40387d5ce04af18585af4ebbb44/faker-40.28.1.tar.gz", hash = "sha256:2a63fb51abab8790636d4030a094cf942404cbccc9c288d1cad70e2e3e9ecd58", size = 2022717, upload-time = "2026-07-01T22:23:43.763Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/77/6adb5a9dcd028f687f81fc9f789591f9572cb5a46454337122add004e134/faker-40.21.0-py3-none-any.whl", hash = "sha256:cb6601b2ae8e128895dc96814d271eab6b930a2d2d7932c6f9ff26785c24ee18", size = 2008808, upload-time = "2026-06-02T17:53:44.346Z" }, + { url = "https://files.pythonhosted.org/packages/92/a6/6111b9f13c1564b0e2f5dbeeb611fd00dc731e6add2336f62b798598c73d/faker-40.28.1-py3-none-any.whl", hash = "sha256:e8d3f5c469100a553d246dce7937c291308068a5ec6c9c3a228d7878b50720be", size = 2061052, upload-time = "2026-07-01T22:23:41.946Z" }, ] [[package]] @@ -678,36 +678,23 @@ wheels = [ [[package]] name = "obstore" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/e3/34852e48d5acedc1dbfa8bfb1ddcee448c7bb1dee7cefca659dce5ee10b4/obstore-0.10.0.tar.gz", hash = "sha256:b581d2f78b521c7c72862721384c109b6764c57222b7bf06dc83626a7c4a6945", size = 126379, upload-time = "2026-06-01T20:56:59.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/47/46357284f39465aeaec6b49f7741a26aa6c0258be6d0f968a3c5c1a93401/obstore-0.10.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:03b4e07d97e5271da136942dae71febd5a42c455aeee42a40892a6977171e9fa", size = 4090761, upload-time = "2026-06-01T20:55:33.434Z" }, - { url = "https://files.pythonhosted.org/packages/48/40/dc3d2acc664af08c4b1062cd677c36a1f3a7face6fa4e966562ba58d92cc/obstore-0.10.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:e45deb76f3f1a54cf730c43e917af72d3f1a46e4c502190a9111d2dabec3d71e", size = 3871052, upload-time = "2026-06-01T20:55:35.001Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/2a4eec925df64b46839b37222fb46fa32faa6790173688b6bbc1a5fd303c/obstore-0.10.0-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d90894aafb8baf02d4d7d4e6debbbd43b26890bfbe0bcad7c6d75b41b019ed6b", size = 4024833, upload-time = "2026-06-01T20:55:36.527Z" }, - { url = "https://files.pythonhosted.org/packages/27/a4/638775f75c7df43596c44684f722db4f130d9a47bc95047c81dcffd8461d/obstore-0.10.0-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a443c729dfb6324591664aa4cca5394d89924014d585ea4ac7e2d448ad3a8e", size = 4122218, upload-time = "2026-06-01T20:55:38.02Z" }, - { url = "https://files.pythonhosted.org/packages/0a/31/e7ad24144996cb1c84414f5dfca83ce149ccfca621399a4fafcabdf18635/obstore-0.10.0-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52ed02949c1ea3dd445c963b246e95801ccd54b5d0d24050ad7ae9570e2c1195", size = 4410778, upload-time = "2026-06-01T20:55:39.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/37/d5710ed7aa32082933c89d3864197e9b413dbabd954ebd5504b976b998a6/obstore-0.10.0-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e360ce37f4b5f7b6e3eaf6517b8bbb481379eb0773a16bb2971c02b4363ca787", size = 4291676, upload-time = "2026-06-01T20:55:41.184Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5a/11b2947870663c4d804a5467462dcfc66a13505cd360e97b2d6ccbc93686/obstore-0.10.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70accdf1fc624559c810bed68041f599a218f3dc4fb3afdeed50f7151e240ff8", size = 4210395, upload-time = "2026-06-01T20:55:42.923Z" }, - { url = "https://files.pythonhosted.org/packages/63/2d/363d7d7f89378753e5491e5d1189b0c728d68ea39d2a02baa6644fb8c4ca/obstore-0.10.0-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:4fc646ad2c2ceaa3dbe07536eb207e1e868e4c701c89f71dd35678e1ee957283", size = 4101733, upload-time = "2026-06-01T20:55:44.561Z" }, - { url = "https://files.pythonhosted.org/packages/51/fd/7ceffe6b89feb6169f81d2b1b5d06eb53998422346c79365424dd06978b1/obstore-0.10.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:72610b6e7b3da608762b6a8a37989e0559c813ac115b9376a176810eb0173bd8", size = 4285774, upload-time = "2026-06-01T20:55:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d9/4c534074516645236157135127e3379184f6f8e2622ab93485946ce2df42/obstore-0.10.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:38a10746b540aab3f898422d87fa7112dab35a824dfa912b64480eb37b55c755", size = 4258354, upload-time = "2026-06-01T20:55:48.435Z" }, - { url = "https://files.pythonhosted.org/packages/ff/dc/3f40b59c19054d8dc0c2699cdc72348bfd56edec78008c5e6bebe8aed55e/obstore-0.10.0-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:e62afd200b2e5bc93cff75e6f930877f4203a6a2f0e13252c6ce7dba78d77c9c", size = 4247831, upload-time = "2026-06-01T20:55:50.005Z" }, - { url = "https://files.pythonhosted.org/packages/a0/08/28d6917d454ae587ef10b10b5d591c22ceb42bcb42c777463fe04ff3efb7/obstore-0.10.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25e1cad14a7723e48e358c5635dc199ec1d0ddb0f6724ff58d92057ed93ad81b", size = 4429790, upload-time = "2026-06-01T20:55:51.765Z" }, - { url = "https://files.pythonhosted.org/packages/f1/de/5c683e75a7504ee73d51e34ac4b98c1eb2ab7679d53d292009028e05338b/obstore-0.10.0-cp311-abi3-win_amd64.whl", hash = "sha256:7e14e1ef6bb63730d6aec78499b0aa48dde160d1ad8fdd1e5553c930e7664107", size = 4166690, upload-time = "2026-06-01T20:55:53.314Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/782617a10203916d193ab2bc6efe363f8f42b5bc6fbde0e0cc3bb51a58dc/obstore-0.10.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f343227c37c6f217aaf56e82f58335fa5f34b70a727f4e5e718becc9c613060a", size = 4073158, upload-time = "2026-06-01T20:55:54.886Z" }, - { url = "https://files.pythonhosted.org/packages/15/6b/31d0a6801a05fb032060bb38c94acb5348ac4ceb0a3a55d0b100439005b4/obstore-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b071781d2ef95653f78aef588bd054480381d679ae57c4f5d2553b366de1a05", size = 3862303, upload-time = "2026-06-01T20:55:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0c/70c1ba253ab9a7dbdb73996381b73ff9a121b032e9d04125709378f44b67/obstore-0.10.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71657a256f938715f5a4d3fe84c5141b79572dbe2436b90d0020ecb9cfe6548b", size = 4021507, upload-time = "2026-06-01T20:55:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/81/d5/5495cb6056fac0f9394518c8eb85340ba625245a14de4384385966efeaba/obstore-0.10.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a96beb5628f3303d8fdfd003b1957f4c2a38f0f224a47b6e488ab0b4eb23edb", size = 4113513, upload-time = "2026-06-01T20:55:59.991Z" }, - { url = "https://files.pythonhosted.org/packages/eb/80/90b292e9989ea387b60f66077cafcb6cc637f3a3ec109ba2c3fd7849b19d/obstore-0.10.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2509b29b89e5a00480f165d5351132da2efeeca6a57d29f018c4e0534a5a5dc0", size = 4400976, upload-time = "2026-06-01T20:56:01.512Z" }, - { url = "https://files.pythonhosted.org/packages/3d/33/7ae84485bccb672f4abab5a4362e2e1554d3df83f07f76e2d68bee2a0657/obstore-0.10.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b53a9c3e3afe24ea53e554c1d5626fdd456b2e7960091fce4a77af5b7a0ffb7", size = 4298268, upload-time = "2026-06-01T20:56:03.197Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f1/9934e3ae0869085449d87c54e41cef137c8ea8527a87d60f9934f822bc08/obstore-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d07b04a16c6a586444de1997f2121a332db05aac7953fbcb3e8fb28140306a8", size = 4208768, upload-time = "2026-06-01T20:56:05.314Z" }, - { url = "https://files.pythonhosted.org/packages/69/76/50cd42e12d8fecef6637a00a857b39c4aade6fa8a100634fe8a000fe56ca/obstore-0.10.0-cp313-cp313t-manylinux_2_24_aarch64.whl", hash = "sha256:012ebb24ba0c47e54840fa02bef780ce4f4d73a3c9685d9addd53222f1b375b2", size = 4100278, upload-time = "2026-06-01T20:56:07.187Z" }, - { url = "https://files.pythonhosted.org/packages/e5/be/378d4ea417771bfffd49222711f70b426f475475881aa6adddf67e62be18/obstore-0.10.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:defe889c3baf0781eb83e0335e89fb1331723bebd6a015dc4eecb99794c70210", size = 4285696, upload-time = "2026-06-01T20:56:08.898Z" }, - { url = "https://files.pythonhosted.org/packages/1f/26/40a9a419de6ccc9e828336fab4a89a5d90c1e01523cce781600dfa970b6b/obstore-0.10.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:b4b1fcf083474384565c5fd06523894419280fca4aeb23a7f3f88add8db2d824", size = 4255725, upload-time = "2026-06-01T20:56:10.609Z" }, - { url = "https://files.pythonhosted.org/packages/6c/db/038ae746b5f8ca2677fa95418234edfa22ebdeff77a55fa349abcb6a6823/obstore-0.10.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c2c4974845b1192ec50959ad8579e01699b6ae8c64adb6433addcfbf35263901", size = 4242564, upload-time = "2026-06-01T20:56:12.127Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/dad7562624b89ed8b8c06241eea9340812decb4bbf1e4f744e9fbf652461/obstore-0.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:315fe386cc631247175c2cc64dfa77f7ec4746248d0a03a7126d9b6a063a1a1d", size = 4429578, upload-time = "2026-06-01T20:56:13.887Z" }, - { url = "https://files.pythonhosted.org/packages/96/d2/b2a232c428b7848862da2b31428694237bd4fe71166ace65d48bbfea6eb4/obstore-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1fc3a232026f9b33affa4c6aa9d4a1765c08aa4e2d0aa8a6034801cf3f8a3cf6", size = 4160793, upload-time = "2026-06-01T20:56:15.629Z" }, +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/2f/f83afaab7945509d72245b2b00af0b4834ce78fdd2d9ae9f0ad1a3036a91/obstore-0.11.0.tar.gz", hash = "sha256:a2f55163bcd348b4a60d12e6893eac50eddc742bad8032a1705d49140b992204", size = 130565, upload-time = "2026-06-25T18:29:49.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b2/00c213e7e5ca8065f97e37e55294adab836e3f6a88b23e4029069aaecf95/obstore-0.11.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:42f36546c7ac44dbab1173d2330a8a1b1a3f0e37950e553b8c904e3dd0744b25", size = 5491935, upload-time = "2026-06-25T18:28:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/6a6b9a5e15a8a37c24d14317a87648097c4888593b588510c03c030d2e90/obstore-0.11.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:687bb9d3962d568b7c439c5d0c6fea19b2749862a8e5c8eebd0c058c4eccde9e", size = 4672619, upload-time = "2026-06-25T18:28:33.852Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/6745ce8c4f7bfac19dc14a4438b48a2e93a689b92b0cecfc695e41a4e8b1/obstore-0.11.0-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:010b51578c7514a41719d795cdb7a1e6529be509dac3772e477187a59422bb97", size = 5072806, upload-time = "2026-06-25T18:28:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/991d3b3cdd851c0225e55f3dc45b47fd9e249827d188995011469f805132/obstore-0.11.0-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfaa8129a3f5d8518a3a75184d4b02348db0f6263177cd1f0951f6568243cc9e", size = 5303777, upload-time = "2026-06-25T18:28:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e9/90e56015a45b5e56a84fc3188c4e5fb088b288d41992c73a629e10df6760/obstore-0.11.0-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c790a5cb9ff2970d1f464a6a708d734dce9939e9f668cb6708c5dba5d61589b2", size = 5493871, upload-time = "2026-06-25T18:28:39.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/02/f1744091d59ce71c5523174eb860fbb298275c901e89b9ea6fbf3e654a33/obstore-0.11.0-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:827113e12fe8088e0281a9d57b90b2b8dbc8a6ffe3b15dadb9baa5feb3d266c1", size = 5361913, upload-time = "2026-06-25T18:28:42.089Z" }, + { url = "https://files.pythonhosted.org/packages/5d/59/3f47822683ee2b6db8685faa25829946d6343a561251ec2704548455d946/obstore-0.11.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2ff6d3ed553298828fb760b4aef6347fbcc7b5c5e3ce3f8381ce805c370021a", size = 5638724, upload-time = "2026-06-25T18:28:43.897Z" }, + { url = "https://files.pythonhosted.org/packages/23/50/1df335fdf9b527b3933f1e94ab6fc720ad314260fab8591cb0b6668ff192/obstore-0.11.0-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:39d04b324fcf984e7050734ebda77b81764025b0c011750201a0d8954087f7aa", size = 5413508, upload-time = "2026-06-25T18:28:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/de/dc/a259aba149b841ca7c91fea177df9972a60a636b54077beed1a35b254994/obstore-0.11.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:37c0d15d775b1370ef5204ee3919a5ddf7e2592d11815213105f8db031f2ab8d", size = 5619995, upload-time = "2026-06-25T18:28:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b4/ec25fdb4d6b060bc6eea647fc0e88f75fcc20fe8d16d67fb0dbe999d323b/obstore-0.11.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7f468caf9b6e0f12ff151e5fe618de5fc9192befa9bd02734b06de4efd2e49f6", size = 5299512, upload-time = "2026-06-25T18:28:49.629Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e5/29be060d06ec13e2af3d1b6cfb77b7c37f8be6c56b77295c945fefad73e4/obstore-0.11.0-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:42d8e8fad85be8ee488c1a9a9b7c6a42128abb84e67175da40d3d1165c1846df", size = 5427026, upload-time = "2026-06-25T18:28:51.317Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/577a965f440e9ea64243518663f9d16be7df8eafc7123818e8e841fa21ce/obstore-0.11.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9c8fd2a544e2e0b926669c47fcfb8d2314e234abc240ea165dae04ee42e1d7ac", size = 5869187, upload-time = "2026-06-25T18:28:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/e2/18/8fdbaee22bfd5b9c44e1fdff8ca0508e2fe60c42bf9fc85f0c9c27b4ecf2/obstore-0.11.0-cp311-abi3-win_amd64.whl", hash = "sha256:6fb3d4678c0f4242d3109362e9b1df5d7b27765f43d5aacb2e81af53a75cb9ef", size = 5329384, upload-time = "2026-06-25T18:28:55.305Z" }, ] [[package]] @@ -1058,15 +1045,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.61.1" +version = "2.65.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/1f/ed17a390348156ca99fe622b97cd7d2f1969b5f49df89084b0f28e7953e9/sentry_sdk-2.65.0.tar.gz", hash = "sha256:c94dc945d54bad49d4f20448b1e6b217ca2f92f46d05c3e83d41764af685c3d1", size = 932133, upload-time = "2026-07-13T11:33:19.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, + { url = "https://files.pythonhosted.org/packages/21/3b/326ad4c03b5da89b5124c8890af66e8119c4d2e10abc0619e0d67d9f7c7f/sentry_sdk-2.65.0-py3-none-any.whl", hash = "sha256:3595169677a808e4d0e1ea6ffb89443459549c7a98392ed71c77c847182ab6bf", size = 503869, upload-time = "2026-07-13T11:33:17.71Z" }, ] [[package]] @@ -1080,22 +1067,22 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.50" +version = "2.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" }, - { url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" }, - { url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" }, - { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] [[package]] @@ -1183,7 +1170,7 @@ wheels = [ [[package]] name = "virtool" version = "0.0.0" -source = { git = "https://github.com/virtool/virtool?tag=38.48.0#fb5cb07b8209d89990d7cdfc59c31b160689cc45" } +source = { git = "https://github.com/virtool/virtool?tag=39.63.0#9d38725682cdc5528753284d8b6e4e234cba086d" } dependencies = [ { name = "aiofiles" }, { name = "aiohttp", extra = ["speedups"] }, @@ -1248,7 +1235,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "virtool", git = "https://github.com/virtool/virtool?tag=38.48.0" }] +requires-dist = [{ name = "virtool", git = "https://github.com/virtool/virtool?tag=39.63.0" }] [package.metadata.requires-dev] dev = [ diff --git a/workflow.py b/workflow.py index 16c4ccf..7940daa 100644 --- a/workflow.py +++ b/workflow.py @@ -7,19 +7,23 @@ from types import SimpleNamespace from typing import Any +from virtool.caches.utils import derive_key from virtool.workflow import hooks, step from virtool.workflow.data.analyses import WFAnalysis -from virtool.workflow.data.cache import WorkflowCache +from virtool.workflow.data.cache import CacheHit, WorkflowCache from virtool.workflow.data.indexes import WFIndex from virtool.workflow.data.samples import WFSample from virtool.workflow.data.subtractions import WFSubtraction from virtool.workflow.runtime.run_subprocess import RunSubprocess +from workflow_pathoscope.reference import ( + CD_HIT_EST_IDENTITY, + collapse_reference_index, + get_reference_collapse_cache_params, +) from workflow_pathoscope.utils import ( build_bowtie2_index, create_mapping_index, run_pathoscope, - write_default_isolate_fasta, - write_isolate_fasta, write_report, ) @@ -46,9 +50,58 @@ async def delete_analysis_document(analysis: WFAnalysis): await analysis.delete() +@step +async def collapse_reference( + cache: WorkflowCache, + collapsed_reference_path: Path, + index: WFIndex, + logger, + proc: int, + run_subprocess: RunSubprocess, +) -> Path: + """Ensure a cd-hit-est collapsed reference index exists locally.""" + params = await get_reference_collapse_cache_params(index.id, run_subprocess) + key = derive_key(params) + collapsed_reference_dir = collapsed_reference_path.parent + log = logger.bind( + identity=CD_HIT_EST_IDENTITY, + index_kind="collapsed_reference", + key=key, + parent_id=index.id, + ) + + log.info("checking workflow cache") + + result = await cache.get(key, collapsed_reference_dir.parent) + + if isinstance(result, CacheHit): + log.info("restored cached collapsed reference", outcome="hit") + + return collapsed_reference_path + + log.info("collapsing reference", outcome="miss", source=str(index.path)) + + collapse_counts = await collapse_reference_index( + index, + collapsed_reference_path, + proc, + run_subprocess, + ) + + log.info("reference collapse complete", **collapse_counts) + + if await cache.put(key, collapsed_reference_dir, params=params): + log.info("cached collapsed reference", outcome="put") + else: + log.info("collapsed reference cache already exists", outcome="put_skipped") + + return collapsed_reference_path + + @step async def create_reference_index( cache: WorkflowCache, + collapsed_reference_path: Path, index: WFIndex, logger, proc: int, @@ -58,16 +111,16 @@ async def create_reference_index( """Ensure the reference Bowtie2 index exists locally.""" with TemporaryDirectory(prefix="pathoscope-reference-") as temp_dir: reference_fasta_path = Path(temp_dir) / "reference.fa" + collapsed_index = WFIndex.load(index.id, collapsed_reference_path) - await asyncio.to_thread( - write_default_isolate_fasta, - index.json_path, + await collapsed_index.write_fasta( reference_fasta_path, + collapsed_index.iter_default_sequences(), ) logger.info( "assembled default reference fasta", - source=str(index.json_path), + source=str(collapsed_reference_path), ) await create_mapping_index( @@ -80,7 +133,8 @@ async def create_reference_index( index_prefix=reference_index_path, parent_id=index.id, extra_params={ - "source": "index_json", + "collapse_identity": CD_HIT_EST_IDENTITY, + "source": "collapsed_reference", "selection": "default_isolates", }, ) @@ -134,7 +188,7 @@ async def map_default_isolates( """ logger.info("running bowtie2 directly from rust with streaming") - candidate_otus = await asyncio.to_thread( + candidate_sequence_ids = await asyncio.to_thread( find_candidate_otus_with_bowtie2, str(reference_index_path), [str(path) for path in sample.read_paths], @@ -142,13 +196,17 @@ async def map_default_isolates( p_score_cutoff, ) - intermediate.to_otus = set(candidate_otus) + intermediate.candidate_sequence_ids = set(candidate_sequence_ids) - logger.info("found candidate otus", count=len(intermediate.to_otus)) + logger.info( + "found candidate sequences", + count=len(intermediate.candidate_sequence_ids), + ) @step async def build_isolate_index( + collapsed_reference_path: Path, index: WFIndex, intermediate: SimpleNamespace, isolate_fasta_path: Path, @@ -158,20 +216,21 @@ async def build_isolate_index( ): """Build a mapping index containing all isolates of candidate OTUs.""" - intermediate.lengths = await asyncio.to_thread( - write_isolate_fasta, - {index.get_otu_id_by_sequence_id(id_) for id_ in intermediate.to_otus}, - index.json_path, - isolate_fasta_path, + if not intermediate.candidate_sequence_ids: + # bowtie2-build exits 1 on an empty FASTA. The remaining steps also + # short-circuit on the empty candidate set. + return + + collapsed_index = WFIndex.load(index.id, collapsed_reference_path) + otu_refs = await collapsed_index.get_otu_refs_by_sequence_ids( + intermediate.candidate_sequence_ids, ) + otu_ids = {otu_ref["id"] for otu_ref in otu_refs.values()} - if not intermediate.lengths: - # A sample can have zero reads mapping to any candidate OTU, producing an - # empty isolate FASTA. bowtie2-build exits 1 on empty input (VIR-2569), so - # skip building the index. The remaining mapping and reassignment steps - # short-circuit on the empty ``intermediate.lengths`` and the analysis is - # finished with an empty result. - return + await collapsed_index.write_fasta( + isolate_fasta_path, + collapsed_index.iter_otu_sequences(otu_ids), + ) await build_bowtie2_index( isolate_fasta_path, @@ -192,7 +251,7 @@ async def map_isolates( sample: WFSample, ): """Map sample reads to the all isolate index.""" - if not intermediate.lengths: + if not intermediate.candidate_sequence_ids: # No candidate OTUs were found, so no isolate index was built. There is # nothing to map against. return @@ -252,7 +311,7 @@ async def eliminate_subtraction( :param work_path: path to the workflow working directory """ - if not intermediate.lengths: + if not intermediate.candidate_sequence_ids: # No candidate OTUs were found, so no reads were mapped to isolates and # there is nothing to subtract. results["subtracted_count"] = 0 @@ -353,7 +412,7 @@ async def reassignment( Tab-separated output is written to ``pathoscope.tsv``. The results are also parsed and saved to `intermediate.coverage`. """ - if not intermediate.lengths: + if not intermediate.candidate_sequence_ids: # No candidate OTUs were found, so no isolate index was built and no reads # were mapped. Finish the analysis with an empty result instead of running # Pathoscope on a non-existent alignment. @@ -387,14 +446,15 @@ async def reassignment( logger.info("preparing hits") hits = [] + otu_refs = await index.get_otu_refs_by_sequence_ids(report) for sequence_id, hit in report.items(): - otu_id = index.get_otu_id_by_sequence_id(sequence_id) + otu_ref = otu_refs[sequence_id] hit["id"] = sequence_id # Attach "otu" (id, version) to the hit. - hit["otu"] = {"id": otu_id, "version": index.manifest[otu_id]} + hit["otu"] = {"id": otu_ref["id"], "version": otu_ref["version"]} # Get the coverage for the sequence. hit_coverage = pathoscope_results.coverage[sequence_id]