CAT-pack 7.0.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. cat_pack-7.0.0a0/CAT_pack/__init__.py +1 -0
  2. cat_pack-7.0.0a0/CAT_pack/__main__.py +11 -0
  3. cat_pack-7.0.0a0/CAT_pack/about.py +7 -0
  4. cat_pack-7.0.0a0/CAT_pack/classification.py +138 -0
  5. cat_pack-7.0.0a0/CAT_pack/cli.py +684 -0
  6. cat_pack-7.0.0a0/CAT_pack/cli_options.py +143 -0
  7. cat_pack-7.0.0a0/CAT_pack/config/__init__.py +15 -0
  8. cat_pack-7.0.0a0/CAT_pack/config/options.py +133 -0
  9. cat_pack-7.0.0a0/CAT_pack/config/settings.py +330 -0
  10. cat_pack-7.0.0a0/CAT_pack/config/validation.py +616 -0
  11. cat_pack-7.0.0a0/CAT_pack/io/__init__.py +22 -0
  12. cat_pack-7.0.0a0/CAT_pack/io/download.py +434 -0
  13. cat_pack-7.0.0a0/CAT_pack/io/parsers.py +546 -0
  14. cat_pack-7.0.0a0/CAT_pack/io/reference_mapping.py +107 -0
  15. cat_pack-7.0.0a0/CAT_pack/io/writers.py +116 -0
  16. cat_pack-7.0.0a0/CAT_pack/pipeline.py +365 -0
  17. cat_pack-7.0.0a0/CAT_pack/prepare.py +188 -0
  18. cat_pack-7.0.0a0/CAT_pack/progress.py +157 -0
  19. cat_pack-7.0.0a0/CAT_pack/reads.py +327 -0
  20. cat_pack-7.0.0a0/CAT_pack/results.py +78 -0
  21. cat_pack-7.0.0a0/CAT_pack/tax.py +479 -0
  22. cat_pack-7.0.0a0/CAT_pack/tools/__init__.py +19 -0
  23. cat_pack-7.0.0a0/CAT_pack/tools/aligner.py +56 -0
  24. cat_pack-7.0.0a0/CAT_pack/tools/bwa_mem.py +106 -0
  25. cat_pack-7.0.0a0/CAT_pack/tools/pyrodigal.py +98 -0
  26. cat_pack-7.0.0a0/CAT_pack/tools/tool_runner.py +47 -0
  27. cat_pack-7.0.0a0/CAT_pack/utils/__init__.py +7 -0
  28. cat_pack-7.0.0a0/CAT_pack/utils/check.py +180 -0
  29. cat_pack-7.0.0a0/CAT_pack/utils/errors.py +144 -0
  30. cat_pack-7.0.0a0/CAT_pack/utils/locking.py +40 -0
  31. cat_pack-7.0.0a0/CAT_pack/utils/logging.py +78 -0
  32. cat_pack-7.0.0a0/CAT_pack/utils/memory.py +32 -0
  33. cat_pack-7.0.0a0/CAT_pack/utils/typing.py +8 -0
  34. cat_pack-7.0.0a0/CAT_pack.egg-info/PKG-INFO +498 -0
  35. cat_pack-7.0.0a0/CAT_pack.egg-info/SOURCES.txt +47 -0
  36. cat_pack-7.0.0a0/CAT_pack.egg-info/dependency_links.txt +1 -0
  37. cat_pack-7.0.0a0/CAT_pack.egg-info/entry_points.txt +2 -0
  38. cat_pack-7.0.0a0/CAT_pack.egg-info/requires.txt +6 -0
  39. cat_pack-7.0.0a0/CAT_pack.egg-info/top_level.txt +1 -0
  40. cat_pack-7.0.0a0/LICENSE.md +7 -0
  41. cat_pack-7.0.0a0/PKG-INFO +498 -0
  42. cat_pack-7.0.0a0/README.md +478 -0
  43. cat_pack-7.0.0a0/pyproject.toml +37 -0
  44. cat_pack-7.0.0a0/setup.cfg +4 -0
  45. cat_pack-7.0.0a0/tests/test_basic_classification.py +86 -0
  46. cat_pack-7.0.0a0/tests/test_cli.py +171 -0
  47. cat_pack-7.0.0a0/tests/test_file_locking.py +65 -0
  48. cat_pack-7.0.0a0/tests/test_options.py +29 -0
  49. cat_pack-7.0.0a0/tests/test_sequence_hit_counts.py +135 -0
@@ -0,0 +1 @@
1
+ """CAT pack 7 :)"""
@@ -0,0 +1,11 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ root = str(Path(__file__).resolve().parent.parent)
5
+ if root not in sys.path:
6
+ sys.path.insert(0, root)
7
+
8
+ from CAT_pack.cli import app
9
+
10
+ if __name__ == "__main__":
11
+ app()
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+
3
+ __authors__ = [
4
+ "F. A. Bastiaan von Meijenfeldt", "Nikos Pappas", "Ernestina Hauptfeld"
5
+ ]
6
+ __version__ = "7.0.0alpha"
7
+ __date__ = "20 October, 2025"
@@ -0,0 +1,138 @@
1
+ """Shared taxonomic classification engine for CAT and BAT"""
2
+ import logging
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from decimal import Decimal
5
+ from itertools import chain
6
+ from typing import Mapping, Sequence
7
+
8
+ import numpy as np
9
+
10
+ from . import tax
11
+ from .io.parsers import AlignmentInput
12
+ from .results import Classification, ClassificationStatus
13
+
14
+ log = logging.getLogger("CAT_pack")
15
+
16
+
17
+ # DEVNOTE: Add a flowchart in devdocs to make this more clear for non
18
+ # Numpy users
19
+ class ClassificationEngine:
20
+ """Classify ORFs and combine their total support for one contig or bin."""
21
+
22
+ def __init__(
23
+ self, taxonomy: tax.Taxonomy, fastaid2taxid: np.ndarray,
24
+ fraction: Decimal, threads: int = 1,
25
+ ) -> None:
26
+ self.taxonomy = taxonomy
27
+ self.hit2taxon = fastaid2taxid
28
+ self.fraction = fraction
29
+ self.threads = threads
30
+
31
+ def classify(
32
+ self, entity_type: str, entity2ORFs: Mapping[str, Sequence[str]], alignment: AlignmentInput,
33
+ ) -> Classification:
34
+ log.info(f"Starting classification calculation: {len(entity2ORFs)} {entity_type}.")
35
+ orf_entities_names = sorted(entity2ORFs)
36
+ n_entities = len(orf_entities_names)
37
+
38
+ # Flatten ORFs, ordered by contig/bin.
39
+ ORF_names = list(chain.from_iterable(entity2ORFs[name] for name in orf_entities_names))
40
+ n_ORFs = np.array([len(entity2ORFs[name]) for name in orf_entities_names], dtype=np.int64)
41
+ ORF_starts = np.concatenate(([0], np.cumsum(n_ORFs)))
42
+ ORF2contig_or_bin = np.repeat(np.arange(n_entities), n_ORFs)
43
+ ORF2alignment_index, ORF_has_hits, LCA_per_ORF, n_hits_per_ORF, top_bitscore_per_ORF = (self.per_ORF_stats(ORF_names, alignment))
44
+
45
+ # Combine the ORF lowest common ancestors per contig (or bin)
46
+ # and weighted by their tob bit score(s)
47
+ weighted_LCAs = tax.WeightedLCA.find(
48
+ self.taxonomy, ORF2contig_or_bin, LCA_per_ORF,
49
+ top_bitscore_per_ORF, self.fraction, n_entities)
50
+ log.info("Finished Weighted LCA")
51
+ assignment_starts = np.searchsorted(weighted_LCAs.assignment_entity, np.arange(n_entities + 1))
52
+
53
+ def count_per_entity(ORF_mask: np.ndarray) -> np.ndarray:
54
+ return np.bincount(ORF2contig_or_bin[ORF_mask], minlength=n_entities)
55
+
56
+ n_ORFs_with_hits = count_per_entity(ORF_has_hits)
57
+ n_ORFs_with_taxid = count_per_entity(LCA_per_ORF != tax.ORF.NO_TAXID)
58
+ n_assignments = np.diff(assignment_starts)
59
+
60
+ # DEVNOTE: For in the devdocs. np.select picks the first matching condition
61
+ classification_status = np.select(
62
+ [n_ORFs == 0, n_ORFs_with_hits == 0, n_ORFs_with_taxid == 0, n_assignments == 0],
63
+ [status.value for status in (
64
+ ClassificationStatus.NO_ORFS, ClassificationStatus.NO_HITS,
65
+ ClassificationStatus.NO_TAXIDS, ClassificationStatus.NO_LINEAGE_SUPPORT)],
66
+ default=ClassificationStatus.ASSIGNED.value,
67
+ )
68
+
69
+ return Classification(
70
+ entity_type=entity_type,
71
+ entities=orf_entities_names,
72
+ status=classification_status,
73
+ orf_offsets=ORF_starts,
74
+ orf_names=ORF_names,
75
+ orf_row=ORF2alignment_index,
76
+ orf_lca=LCA_per_ORF,
77
+ orf_n_hits=n_hits_per_ORF,
78
+ orf_top_bitscore=top_bitscore_per_ORF,
79
+ n_orfs_with_taxid=n_ORFs_with_taxid,
80
+ assignment_offsets=assignment_starts,
81
+ weighted=weighted_LCAs,
82
+ alignment=alignment,
83
+ taxonomy=self.taxonomy,
84
+ )
85
+
86
+ def per_ORF_stats(self, ORF_names: Sequence[str], alignment: AlignmentInput):
87
+ n_ORFs = len(ORF_names)
88
+
89
+ ORF2alignment_index = np.fromiter(
90
+ (alignment.orf_index.get(ORF, tax.ORF.NO_HIT) for ORF in ORF_names), dtype=np.int64, count=n_ORFs)
91
+ ORF_has_hits = ORF2alignment_index != tax.ORF.NO_HIT
92
+
93
+ alignment_indices = ORF2alignment_index[ORF_has_hits]
94
+ hits_start = alignment.orf_offsets[alignment_indices]
95
+ hits_end = alignment.orf_offsets[alignment_indices + 1]
96
+
97
+ hit_taxids = self.hit2taxon[alignment.hit_ids]
98
+ log.info(f"hit taxonomy lookup: {len(hit_taxids)}accepted hits")
99
+ n_alignment_ORFs = len(alignment.orf_names)
100
+ workers = min(self.threads, max(1, n_alignment_ORFs // 20_000))
101
+
102
+ log.info(f"Starting ORF LCA calculation: with {n_alignment_ORFs} alignment ORFs, "
103
+ f"{len(hit_taxids)} accepted hits, and {workers} workers.")
104
+ if workers > 1:
105
+ ORF_bounds = np.linspace(
106
+ 0, n_alignment_ORFs, workers + 1, dtype=np.int64
107
+ )
108
+ hit_bounds = alignment.orf_offsets[ORF_bounds]
109
+ offset_batches = (
110
+ alignment.orf_offsets[start:end + 1] - hit_start
111
+ for start, end, hit_start in zip(
112
+ ORF_bounds[:-1], ORF_bounds[1:], hit_bounds[:-1]
113
+ )
114
+ )
115
+ hit_batches = (
116
+ hit_taxids[start:end]
117
+ for start, end in zip(hit_bounds[:-1], hit_bounds[1:])
118
+ )
119
+ with ThreadPoolExecutor(max_workers=workers) as pool:
120
+ alignment_ORF_LCAs = np.concatenate(list(pool.map(
121
+ self.taxonomy.find_LCA_for_ORF,
122
+ offset_batches, hit_batches)))
123
+ else:
124
+ alignment_ORF_LCAs = self.taxonomy.find_LCA_for_ORF(
125
+ alignment.orf_offsets, hit_taxids
126
+ )
127
+
128
+ LCA_per_ORF = np.full(n_ORFs, tax.ORF.NO_TAXID, dtype=np.int32)
129
+ LCA_per_ORF[ORF_has_hits] = alignment_ORF_LCAs[alignment_indices]
130
+
131
+ n_hits_per_ORF = np.zeros(n_ORFs, dtype=np.int64)
132
+ n_hits_per_ORF[ORF_has_hits] = hits_end - hits_start
133
+
134
+ top_bitscore_per_ORF = np.zeros(n_ORFs, dtype=np.int64)
135
+ top_bitscore_per_ORF[ORF_has_hits] = alignment.bitscores[hits_start]
136
+
137
+ return ORF2alignment_index, ORF_has_hits, LCA_per_ORF, n_hits_per_ORF, top_bitscore_per_ORF
138
+