PGAP2 2.4__tar.gz → 2.4.2__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.
- {pgap2-2.4 → pgap2-2.4.2/PGAP2.egg-info}/PKG-INFO +1 -1
- {pgap2-2.4 → pgap2-2.4.2}/PGAP2.egg-info/SOURCES.txt +1 -0
- {pgap2-2.4/PGAP2.egg-info → pgap2-2.4.2}/PKG-INFO +1 -1
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/gene_retriever.py +14 -12
- {pgap2-2.4 → pgap2-2.4.2}/pyproject.toml +1 -1
- pgap2-2.4.2/tests/test_gene_retriever.py +261 -0
- {pgap2-2.4 → pgap2-2.4.2}/LICENSE +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/MANIFEST.in +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/PGAP2.egg-info/dependency_links.txt +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/PGAP2.egg-info/entry_points.txt +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/PGAP2.egg-info/requires.txt +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/PGAP2.egg-info/top_level.txt +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/README.md +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/neighborhood.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/partition.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/partition_add.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/partition_merge.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/partition_minus.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/postprocess.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/preprocess.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/cli/sqlite_cmd.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/dependencies/draw_post_profile.r +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/dependencies/draw_post_stat.r +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/dependencies/draw_prep.r +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/dependencies/maskrc-svg.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/dependencies/run_fastbaps +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/basic.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/panclust.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/pangenome.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/pangp.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/phylogeny.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/pklcheck.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/species.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/strain.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/synteny.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/lib/tree.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/main.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/baps.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/base.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/gene_annotation.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/gene_cluster.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/gene_neighborhood.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/pav.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/phylogeny.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/preprocess.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/project.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/project_file.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/statistics.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/strain.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/models/tajimas_d.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/baps.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/graph.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/neighborhood.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/profile.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/reference_layout.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/singletree.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/stat.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/postprocess/tajimas_d.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/toy_input/287_91.fa +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/toy_input/287_91.gff +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/toy_input/AKU_12601.gff +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/toy_input/Readme_input.md +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/toy_input/SC_B67.gbk +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/__init__.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/alignment.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/arrangement_detector.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/data_loader.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/draw.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/generate_tree.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/graph_utils.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/neighborhood.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/partition.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/partition_add.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/partition_merge.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/partition_minus.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/postprocess.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/preprocess.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/sqlite_builder.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/supply.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/pgap2/utils/tools.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/setup.cfg +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/tests/test_main.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/tests/test_pangenome.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/tests/test_post.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/tests/test_prep.py +0 -0
- {pgap2-2.4 → pgap2-2.4.2}/tests/test_synteny.py +0 -0
|
@@ -103,7 +103,6 @@ def retrieve_from_paf(fpaf, min_overlap_bp=1):
|
|
|
103
103
|
split_list = line.rstrip('\n').split('\t')
|
|
104
104
|
|
|
105
105
|
query = split_list[0]
|
|
106
|
-
qlen = int(split_list[1]) * 3
|
|
107
106
|
strand = 1 if split_list[4] == '+' else -1
|
|
108
107
|
contig = split_list[5]
|
|
109
108
|
t_start = int(split_list[7])
|
|
@@ -122,8 +121,8 @@ def retrieve_from_paf(fpaf, min_overlap_bp=1):
|
|
|
122
121
|
f"PAF line {line.strip()} does not contain AS:i: score, using matches {matches} as fallback."
|
|
123
122
|
)
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
# Both counts are in nucleotide units in miniprot PAF.
|
|
125
|
+
identity = round(matches / block, 5)
|
|
127
126
|
|
|
128
127
|
rec = dict(q=query, start=t_start, end=t_end,
|
|
129
128
|
score=score, identity=identity,
|
|
@@ -258,7 +257,7 @@ def realign(pg: Pangenome, tree: Tree, outdir, need_realign_root: set):
|
|
|
258
257
|
if node in pg.nucl_fa:
|
|
259
258
|
exitst_file_flag = True
|
|
260
259
|
nucl_seq = Seq(pg.nucl_fa[node])
|
|
261
|
-
fh.write(f'>{node}\n{nucl_seq.translate()}\n')
|
|
260
|
+
fh.write(f'>{node}\n{nucl_seq.translate(table=pg.gcode)}\n')
|
|
262
261
|
if exitst_file_flag:
|
|
263
262
|
logger.info('Re-aligning sequences to update the identity network...')
|
|
264
263
|
edges, notused_edges = realign_parser(outdir=outdir, evalue=pg.evalue, aligner=pg.aligner, mcxdeblast=sfw.mcxdeblast,
|
|
@@ -403,16 +402,18 @@ def retrieve_gene_from_genome(G: nx.Graph, pg: Pangenome, tree: Tree):
|
|
|
403
402
|
logger.trace(
|
|
404
403
|
f'Retrieved gene ({loc}) overlaps existing CDS, skip.')
|
|
405
404
|
continue
|
|
406
|
-
|
|
407
|
-
|
|
405
|
+
# Gene IDs use the contig-wide order, not the strand-specific rank.
|
|
406
|
+
before_gene = f'{strain}:{contig}:{gene_rank_all.index(before_s)}'
|
|
407
|
+
after_gene = f'{strain}:{contig}:{gene_rank_all.index(after_s)}'
|
|
408
|
+
before_e = before_s + pg.annot[before_gene]['len']*3+3
|
|
408
409
|
if before_e >= start:
|
|
409
410
|
logger.trace(
|
|
410
411
|
f'Retrived gene ({loc})) has overlap with exists coding gene, skip it.')
|
|
411
412
|
continue
|
|
412
413
|
|
|
413
414
|
assumed_gene = f'{strain}:{contig}:{gene_num+j}'
|
|
414
|
-
before_node = member2leaf[
|
|
415
|
-
after_node = member2leaf[
|
|
415
|
+
before_node = member2leaf[before_gene]
|
|
416
|
+
after_node = member2leaf[after_gene]
|
|
416
417
|
|
|
417
418
|
if not G.has_node(before_node) or not G.has_node(after_node):
|
|
418
419
|
# this gene has been merged baited by other strain's gene
|
|
@@ -451,13 +452,14 @@ def retrieve_gene_from_genome(G: nx.Graph, pg: Pangenome, tree: Tree):
|
|
|
451
452
|
start=start, end=end, strand=loc['strand']), type="CDS", id=retrieve_gene_name)
|
|
452
453
|
seq = feature.extract(strain_seq_dict[contig].seq)
|
|
453
454
|
with warnings.catch_warnings(record=True) as w:
|
|
454
|
-
# only capture BiopythonWarning
|
|
455
455
|
warnings.simplefilter("always", BiopythonWarning)
|
|
456
456
|
|
|
457
|
-
prot = seq.translate()
|
|
457
|
+
prot = seq.translate(table=pg.gcode)
|
|
458
458
|
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
has_translation_warning = any(
|
|
460
|
+
issubclass(warning.category, BiopythonWarning) for warning in w)
|
|
461
|
+
# Internal stops do not raise warnings; a single terminal stop is allowed.
|
|
462
|
+
if has_translation_warning or '*' in prot[:-1]:
|
|
461
463
|
pseudo_gene_product = f'pseudo|{identity}|{cigar}'
|
|
462
464
|
tree.pseudo_root_leaf[tree.leaf_root[expect_node]].add(
|
|
463
465
|
assumed_gene)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import pytest
|
|
6
|
+
from Bio import SeqIO
|
|
7
|
+
from Bio.Seq import Seq
|
|
8
|
+
|
|
9
|
+
from pgap2.lib.pangenome import Pangenome
|
|
10
|
+
from pgap2.lib.tree import Tree
|
|
11
|
+
from pgap2.utils import gene_retriever
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _paf_line(query, protein_length, matches, block, strand="+", score=50):
|
|
15
|
+
fields = [
|
|
16
|
+
query, protein_length, 0, min(protein_length, block // 3), strand,
|
|
17
|
+
"0", 1200, 200, 200 + block, matches, block, 60, f"AS:i:{score}",
|
|
18
|
+
]
|
|
19
|
+
return "\t".join(map(str, fields)) + "\n"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"protein_length,matches,block,identity",
|
|
24
|
+
[
|
|
25
|
+
pytest.param(100, 120, 300, 0.4, id="full-length-low-identity"),
|
|
26
|
+
pytest.param(100, 270, 300, 0.9, id="full-length-mismatches"),
|
|
27
|
+
pytest.param(100, 270, 360, 0.75, id="longer-alignment"),
|
|
28
|
+
pytest.param(100, 180, 240, 0.75, id="partial-alignment"),
|
|
29
|
+
pytest.param(100, 300, 300, 1.0, id="perfect-match"),
|
|
30
|
+
pytest.param(100, 198, 297, 0.66667, id="rounding"),
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
def test_retrieve_from_paf_identity(tmp_path, protein_length, matches, block,
|
|
34
|
+
identity):
|
|
35
|
+
paf_file = tmp_path / "hits.paf"
|
|
36
|
+
paf_file.write_text(_paf_line("query", protein_length, matches, block))
|
|
37
|
+
|
|
38
|
+
hits = gene_retriever.retrieve_from_paf(paf_file)
|
|
39
|
+
|
|
40
|
+
assert hits["0"]["query"]["identity"] == identity
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize("high_identity_first", [False, True])
|
|
44
|
+
def test_retrieve_from_paf_overlap_prefers_identity(tmp_path, high_identity_first):
|
|
45
|
+
lines = [
|
|
46
|
+
_paf_line("low_identity", 100, 120, 300),
|
|
47
|
+
_paf_line("high_identity", 100, 270, 300),
|
|
48
|
+
]
|
|
49
|
+
if high_identity_first:
|
|
50
|
+
lines.reverse()
|
|
51
|
+
paf_file = tmp_path / "hits.paf"
|
|
52
|
+
paf_file.write_text("".join(lines))
|
|
53
|
+
|
|
54
|
+
hits = gene_retriever.retrieve_from_paf(paf_file)
|
|
55
|
+
|
|
56
|
+
assert set(hits["0"]) == {"high_identity"}
|
|
57
|
+
assert hits["0"]["high_identity"]["identity"] == 0.9
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize("strand", [1, -1])
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
"protein_length,matches,outcome",
|
|
63
|
+
[
|
|
64
|
+
pytest.param(100, 120, "rejected", id="low-identity-filtered"),
|
|
65
|
+
pytest.param(100, 270, "inserted", id="below-orth-not-merged"),
|
|
66
|
+
pytest.param(120, 270, "inserted", id="mixed-strand-neighbors"),
|
|
67
|
+
pytest.param(100, 300, "merged", id="high-identity-merged"),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
def test_retrieval_identity_and_neighbors(tmp_path, monkeypatch, strand,
|
|
71
|
+
protein_length, matches, outcome):
|
|
72
|
+
expected_node = "1:0:0"
|
|
73
|
+
retrieved_gene = "0:0:4"
|
|
74
|
+
left_node, right_node = "0:0:1", "0:0:3"
|
|
75
|
+
graph = nx.Graph()
|
|
76
|
+
graph.add_node(expected_node, length=protein_length, members={expected_node},
|
|
77
|
+
strains={1}, repre_nodes=[expected_node])
|
|
78
|
+
# The same-strand flanks are genes 1 and 3, not strand-local indices 0 and 1.
|
|
79
|
+
positions = [(0, -strand), (60, strand), (90, -strand), (900, strand)]
|
|
80
|
+
rows = [
|
|
81
|
+
f"{expected_node}\treference\tcontig_1\t[0:360](+)\t{protein_length}"
|
|
82
|
+
"\treference_gene\tgene\tproduct\tATG\tM\n"
|
|
83
|
+
]
|
|
84
|
+
for index, (start, gene_strand) in enumerate(positions):
|
|
85
|
+
gene = f"0:0:{index}"
|
|
86
|
+
graph.add_node(gene, length=5, members={gene, f"1:0:{index + 1}"},
|
|
87
|
+
strains={0, 1}, repre_nodes=[gene])
|
|
88
|
+
sign = "+" if gene_strand == 1 else "-"
|
|
89
|
+
rows.append(
|
|
90
|
+
f"{gene}\ttarget\tcontig_1\t[{start}:{start + 18}]({sign})"
|
|
91
|
+
f"\t5\tgene_{index}\tgene\tproduct\tATG\tM\n")
|
|
92
|
+
graph.add_edges_from([(expected_node, left_node), (expected_node, right_node)])
|
|
93
|
+
|
|
94
|
+
pg = Pangenome(outdir=str(tmp_path), threads=1, gcode=11, disable=True)
|
|
95
|
+
annot_file = tmp_path / "total.involved_annot.tsv"
|
|
96
|
+
annot_file.write_text("".join(rows))
|
|
97
|
+
pg.total_gene_num = len(rows)
|
|
98
|
+
pg.load_annot_file(str(annot_file))
|
|
99
|
+
pg.reload_annot_file(retrieve=True)
|
|
100
|
+
pg.para_id, pg.orth_id = 0.7, 0.98
|
|
101
|
+
pg.nucl_fa = {}
|
|
102
|
+
pg.prot_file = str(tmp_path / "query-source.fa")
|
|
103
|
+
pg.strain_dict = {0: SimpleNamespace(strain_name="target")}
|
|
104
|
+
|
|
105
|
+
tree = Tree()
|
|
106
|
+
tree.leaf_root[expected_node] = 0
|
|
107
|
+
tree.root_leaf[0].add(expected_node)
|
|
108
|
+
tree.distance_graph.add_node(expected_node)
|
|
109
|
+
tree.raw_distance_graph = tree.distance_graph.copy()
|
|
110
|
+
|
|
111
|
+
genome_dir = tmp_path / "genome_index" / "0" / "0"
|
|
112
|
+
genome_dir.mkdir(parents=True)
|
|
113
|
+
dna = "ATG" + "AAA" * 99
|
|
114
|
+
genomic_dna = dna if strand == 1 else str(Seq(dna).reverse_complement())
|
|
115
|
+
(genome_dir / "ref.fa").write_text(
|
|
116
|
+
">0\n" + "A" * 200 + genomic_dna + "A" * 700 + "\n")
|
|
117
|
+
(genome_dir / "out.paf").write_text(
|
|
118
|
+
_paf_line(expected_node, protein_length, matches, 300,
|
|
119
|
+
strand="+" if strand == 1 else "-"))
|
|
120
|
+
monkeypatch.setattr(gene_retriever, "sfw",
|
|
121
|
+
SimpleNamespace(seqtk="seqtk", miniprot="miniprot"))
|
|
122
|
+
monkeypatch.setattr(gene_retriever, "run_command", Mock())
|
|
123
|
+
|
|
124
|
+
# Keep the PAF parser, graph updates, extraction, and annotation writing real.
|
|
125
|
+
graph, pg, tree, realign_roots = gene_retriever.retrieve_gene_from_genome(
|
|
126
|
+
graph, pg, tree)
|
|
127
|
+
|
|
128
|
+
annotation = (tmp_path / "total.involved_retrieved_annot.tsv").read_text()
|
|
129
|
+
if outcome == "rejected":
|
|
130
|
+
assert annotation == ""
|
|
131
|
+
assert retrieved_gene not in pg.annot
|
|
132
|
+
assert not graph.has_node(retrieved_gene)
|
|
133
|
+
assert graph.nodes[expected_node]["members"] == {expected_node}
|
|
134
|
+
assert not tree.distance_graph.has_node(retrieved_gene)
|
|
135
|
+
assert realign_roots == set()
|
|
136
|
+
else:
|
|
137
|
+
fields = annotation.strip().split("\t")
|
|
138
|
+
assert fields[0] == "#" + retrieved_gene
|
|
139
|
+
assert fields[7].split("|")[:2] == ["coding", str(matches / 300)]
|
|
140
|
+
assert fields[8] == dna
|
|
141
|
+
assert tree.distance_graph[retrieved_gene][expected_node]["weight"] == matches / 300
|
|
142
|
+
assert realign_roots == {0}
|
|
143
|
+
if outcome == "inserted":
|
|
144
|
+
assert set(graph.neighbors(retrieved_gene)) == {left_node, right_node}
|
|
145
|
+
assert graph.nodes[expected_node]["members"] == {expected_node}
|
|
146
|
+
else:
|
|
147
|
+
assert not graph.has_node(retrieved_gene)
|
|
148
|
+
assert retrieved_gene in graph.nodes[expected_node]["members"]
|
|
149
|
+
assert set(graph.neighbors(expected_node)) == {left_node, right_node}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize("strand", [1, -1])
|
|
153
|
+
@pytest.mark.parametrize(
|
|
154
|
+
"dna,gcode,protein,status",
|
|
155
|
+
[
|
|
156
|
+
pytest.param("ATGAAATAAGGG", 11, "MK*G", "pseudo", id="internal-taa"),
|
|
157
|
+
pytest.param("ATGAAATAGGGG", 11, "MK*G", "pseudo", id="internal-tag"),
|
|
158
|
+
pytest.param("ATGAAATGAGGG", 11, "MK*G", "pseudo", id="internal-tga"),
|
|
159
|
+
pytest.param("ATGAAAGGGTAA", 11, "MKG*", "coding", id="terminal-stop"),
|
|
160
|
+
pytest.param("ATGAAAGGG", 11, "MKG", "coding", id="no-terminal-stop"),
|
|
161
|
+
pytest.param("AAAGGG", 11, "KG", "coding", id="no-start-codon"),
|
|
162
|
+
pytest.param("ATGAAATAAGGGTAA", 11, "MK*G*", "pseudo", id="both-stops"),
|
|
163
|
+
pytest.param("ATGAAATAATAA", 11, "MK**", "pseudo", id="consecutive-stops"),
|
|
164
|
+
pytest.param("ATGAAAGGGA", 11, "MKG", "pseudo", id="partial-codon"),
|
|
165
|
+
pytest.param("ATGAAATGAGGG", 4, "MKWG", "coding", id="alternative-code"),
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
def test_retrieved_gene_classification(tmp_path, monkeypatch, strand, dna, gcode,
|
|
169
|
+
protein, status):
|
|
170
|
+
expected_node = "1:0:0"
|
|
171
|
+
left_node, right_node = "0:0:0", "0:0:1"
|
|
172
|
+
retrieved_gene = "0:0:2"
|
|
173
|
+
graph = nx.Graph()
|
|
174
|
+
graph.add_node(expected_node, length=10, members={expected_node},
|
|
175
|
+
strains={1}, repre_nodes=[expected_node])
|
|
176
|
+
for node, other_member in [(left_node, "1:0:1"), (right_node, "1:0:2")]:
|
|
177
|
+
graph.add_node(node, length=5, members={node, other_member},
|
|
178
|
+
strains={0, 1}, repre_nodes=[node])
|
|
179
|
+
graph.add_edge(expected_node, node)
|
|
180
|
+
|
|
181
|
+
pg = Pangenome(outdir=str(tmp_path), threads=1, gcode=gcode, disable=True)
|
|
182
|
+
pg.para_id, pg.orth_id = 0.7, 0.98
|
|
183
|
+
pg.nucl_fa = {}
|
|
184
|
+
pg.prot_file = str(tmp_path / "query-source.fa")
|
|
185
|
+
pg.annot = {
|
|
186
|
+
expected_node: {"len": 10, "id": "reference_gene"},
|
|
187
|
+
left_node: {"len": 5},
|
|
188
|
+
right_node: {"len": 5},
|
|
189
|
+
}
|
|
190
|
+
pg.annot_contig_map = {"0:0": "contig_1"}
|
|
191
|
+
pg.gene_rank = {"0:0": [[0, 300], []] if strand == 1 else [[], [0, 300]]}
|
|
192
|
+
pg.strain_dict = {0: SimpleNamespace(strain_name="target_strain")}
|
|
193
|
+
|
|
194
|
+
tree = Tree()
|
|
195
|
+
tree.leaf_root[expected_node] = 0
|
|
196
|
+
tree.root_leaf[0].add(expected_node)
|
|
197
|
+
tree.distance_graph.add_node(expected_node)
|
|
198
|
+
tree.raw_distance_graph = tree.distance_graph.copy()
|
|
199
|
+
|
|
200
|
+
genome_dir = tmp_path / "genome_index" / "0" / "0"
|
|
201
|
+
genome_dir.mkdir(parents=True)
|
|
202
|
+
genomic_dna = dna if strand == 1 else str(Seq(dna).reverse_complement())
|
|
203
|
+
(genome_dir / "ref.fa").write_text(
|
|
204
|
+
">0\n" + "A" * 100 + genomic_dna + "A" * 300 + "\n",
|
|
205
|
+
encoding="utf-8",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Supply a controlled hit while exercising the real extraction and output path.
|
|
209
|
+
monkeypatch.setattr(gene_retriever, "sfw",
|
|
210
|
+
SimpleNamespace(seqtk="seqtk", miniprot="miniprot"))
|
|
211
|
+
monkeypatch.setattr(gene_retriever, "run_command", Mock())
|
|
212
|
+
monkeypatch.setattr(gene_retriever, "retrieve_from_paf", Mock(return_value={
|
|
213
|
+
"0": {expected_node: {
|
|
214
|
+
"identity": 0.99,
|
|
215
|
+
"cigar": "cg:Z:4M",
|
|
216
|
+
"loc": {"start": 100, "end": 100 + len(dna), "strand": strand},
|
|
217
|
+
}},
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
graph, pg, tree, realign_roots = gene_retriever.retrieve_gene_from_genome(
|
|
221
|
+
graph, pg, tree)
|
|
222
|
+
|
|
223
|
+
rows = (tmp_path / "total.involved_retrieved_annot.tsv").read_text().splitlines()
|
|
224
|
+
assert len(rows) == 1
|
|
225
|
+
fields = rows[0].split("\t")
|
|
226
|
+
assert fields[0] == "#" + retrieved_gene
|
|
227
|
+
assert fields[7].split("|")[0] == status
|
|
228
|
+
assert fields[8] == dna
|
|
229
|
+
assert fields[9] == protein
|
|
230
|
+
assert int(fields[4]) == len(protein)
|
|
231
|
+
assert pg.annot[retrieved_gene]["product"] == fields[7]
|
|
232
|
+
assert retrieved_gene in graph.nodes[expected_node]["members"]
|
|
233
|
+
if status == "pseudo":
|
|
234
|
+
assert retrieved_gene in tree.pseudo_root_leaf[0]
|
|
235
|
+
assert retrieved_gene not in pg.nucl_fa
|
|
236
|
+
assert retrieved_gene not in tree.root_leaf[0]
|
|
237
|
+
assert realign_roots == set()
|
|
238
|
+
else:
|
|
239
|
+
assert retrieved_gene not in tree.pseudo_root_leaf[0]
|
|
240
|
+
assert str(pg.nucl_fa[retrieved_gene]) == dna
|
|
241
|
+
assert retrieved_gene in tree.root_leaf[0]
|
|
242
|
+
assert realign_roots == {0}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.parametrize("gcode,protein", [(4, "MKW"), (11, "MK*")])
|
|
246
|
+
def test_realign_uses_configured_genetic_code(tmp_path, monkeypatch, gcode, protein):
|
|
247
|
+
pg = Pangenome(outdir=str(tmp_path), threads=1, gcode=gcode, disable=True)
|
|
248
|
+
pg.nucl_fa = {"0:0:0": Seq("ATGAAATGA")}
|
|
249
|
+
tree = Tree()
|
|
250
|
+
tree.root_leaf[0].update(["0:0:0", "1:0:0", "2:0:0"])
|
|
251
|
+
parser = Mock(return_value=(None, None))
|
|
252
|
+
monkeypatch.setattr(gene_retriever, "realign_parser", parser)
|
|
253
|
+
monkeypatch.setattr(gene_retriever, "sfw",
|
|
254
|
+
SimpleNamespace(mcxdeblast="mcxdeblast", mcl="mcl"))
|
|
255
|
+
|
|
256
|
+
gene_retriever.realign(pg, tree, str(tmp_path), {0})
|
|
257
|
+
|
|
258
|
+
record = SeqIO.read(tmp_path / "seq.fa", "fasta")
|
|
259
|
+
assert record.id == "0:0:0"
|
|
260
|
+
assert str(record.seq) == protein
|
|
261
|
+
parser.assert_called_once()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|