CodonAdaptPy 1.0.0__py3-none-any.whl
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.
- codonadaptpy/__init__.py +67 -0
- codonadaptpy/__main__.py +17 -0
- codonadaptpy/__version__.py +21 -0
- codonadaptpy/aggregation.py +94 -0
- codonadaptpy/analyzer.py +270 -0
- codonadaptpy/cli.py +409 -0
- codonadaptpy/comparative.py +538 -0
- codonadaptpy/exceptions.py +39 -0
- codonadaptpy/genetic_code.py +115 -0
- codonadaptpy/metrics/__init__.py +28 -0
- codonadaptpy/metrics/adaptation.py +133 -0
- codonadaptpy/metrics/composition.py +173 -0
- codonadaptpy/metrics/evolutionary.py +60 -0
- codonadaptpy/metrics/host.py +145 -0
- codonadaptpy/metrics/pairs.py +107 -0
- codonadaptpy/metrics/usage.py +155 -0
- codonadaptpy/models.py +128 -0
- codonadaptpy/optimizer.py +194 -0
- codonadaptpy/parsers.py +220 -0
- codonadaptpy/phylo_visualization.py +334 -0
- codonadaptpy/phylogenetics.py +602 -0
- codonadaptpy/py.typed +1 -0
- codonadaptpy/references.py +169 -0
- codonadaptpy/reporting.py +174 -0
- codonadaptpy/validation.py +169 -0
- codonadaptpy/visualization.py +1073 -0
- codonadaptpy-1.0.0.dist-info/METADATA +346 -0
- codonadaptpy-1.0.0.dist-info/RECORD +32 -0
- codonadaptpy-1.0.0.dist-info/WHEEL +5 -0
- codonadaptpy-1.0.0.dist-info/entry_points.txt +2 -0
- codonadaptpy-1.0.0.dist-info/licenses/LICENSE +14 -0
- codonadaptpy-1.0.0.dist-info/top_level.txt +1 -0
codonadaptpy/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodonAdaptPy Package
|
|
3
|
+
|
|
4
|
+
CodonAdaptPy is an integrated, class-based Python toolkit for validating
|
|
5
|
+
coding sequences, calculating codon-usage and host-adaptation statistics,
|
|
6
|
+
performing comparative and evolutionary analyses, generating reproducible
|
|
7
|
+
reports, and designing constrained optimized or deoptimized coding sequences.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- FASTA, GenBank, tabular, pasted-sequence, and batch input workflows
|
|
11
|
+
- CAI, RSCU, ENC, FOP, CBI, RCDI, composition, pair, and motif metrics
|
|
12
|
+
- Multi-host similarity and composite adaptation analysis
|
|
13
|
+
- Comparative statistics, clustering, PCA, and evolutionary diagnostics
|
|
14
|
+
- Reproducible constrained optimization with multiple candidate sequences
|
|
15
|
+
- Shared Python API and command-line interface with file-based outputs
|
|
16
|
+
|
|
17
|
+
Scientific interpretation:
|
|
18
|
+
Codon-usage similarity is descriptive evidence. It is not direct proof of
|
|
19
|
+
expression, replication, virulence, host switching, transmission, or
|
|
20
|
+
biological fitness and should be interpreted with experimental context.
|
|
21
|
+
|
|
22
|
+
:Created: July 20, 2026
|
|
23
|
+
:Updated: July 20, 2026
|
|
24
|
+
:Author: Naveen Duhan
|
|
25
|
+
:Version: 1.0.0
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from .__version__ import __author__, __email__, __version__
|
|
29
|
+
from .aggregation import CDSAggregator, IsolateAnalysis
|
|
30
|
+
from .analyzer import CodonAnalyzer
|
|
31
|
+
from .models import AnalysisResult, SequenceRecord, ValidationConfig, ValidationReport
|
|
32
|
+
from .optimizer import CodonOptimizer, OptimizationConfig, OptimizationResult
|
|
33
|
+
from .phylo_visualization import ETE3TreePlotter
|
|
34
|
+
from .phylogenetics import (
|
|
35
|
+
PhylogeneticAnalyzer,
|
|
36
|
+
PhylogeneticComparator,
|
|
37
|
+
PhylogenyResult,
|
|
38
|
+
TemporalAnalyzer,
|
|
39
|
+
TemporalSignalResult,
|
|
40
|
+
)
|
|
41
|
+
from .references import ReferenceManager
|
|
42
|
+
from .visualization import PlotManager, PublicationPlotManager
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AnalysisResult",
|
|
46
|
+
"CDSAggregator",
|
|
47
|
+
"CodonAnalyzer",
|
|
48
|
+
"CodonOptimizer",
|
|
49
|
+
"ETE3TreePlotter",
|
|
50
|
+
"OptimizationConfig",
|
|
51
|
+
"OptimizationResult",
|
|
52
|
+
"IsolateAnalysis",
|
|
53
|
+
"PlotManager",
|
|
54
|
+
"PhylogeneticAnalyzer",
|
|
55
|
+
"PhylogeneticComparator",
|
|
56
|
+
"PhylogenyResult",
|
|
57
|
+
"PublicationPlotManager",
|
|
58
|
+
"ReferenceManager",
|
|
59
|
+
"SequenceRecord",
|
|
60
|
+
"TemporalAnalyzer",
|
|
61
|
+
"TemporalSignalResult",
|
|
62
|
+
"ValidationConfig",
|
|
63
|
+
"ValidationReport",
|
|
64
|
+
"__author__",
|
|
65
|
+
"__email__",
|
|
66
|
+
"__version__",
|
|
67
|
+
]
|
codonadaptpy/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CodonAdaptPy Module Entry Point
|
|
4
|
+
|
|
5
|
+
This module makes ``python -m codonadaptpy`` equivalent to the installed
|
|
6
|
+
``codonadaptpy`` command while keeping command implementation in
|
|
7
|
+
:mod:`codonadaptpy.cli` for direct unit testing.
|
|
8
|
+
|
|
9
|
+
:Created: July 20, 2026
|
|
10
|
+
:Updated: July 20, 2026
|
|
11
|
+
:Author: Naveen Duhan
|
|
12
|
+
:Version: 1.0.0
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .cli import main
|
|
16
|
+
|
|
17
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodonAdaptPy Version and Authorship Metadata
|
|
3
|
+
|
|
4
|
+
This module provides a single source of truth for package identity. Keeping
|
|
5
|
+
version and author values here ensures that command-line banners, generated
|
|
6
|
+
reports, API responses, and Python callers all expose consistent metadata.
|
|
7
|
+
|
|
8
|
+
Constants:
|
|
9
|
+
- __version__: Semantic package version.
|
|
10
|
+
- __author__: Primary package author.
|
|
11
|
+
- __email__: Public author contact address when configured.
|
|
12
|
+
|
|
13
|
+
:Created: July 20, 2026
|
|
14
|
+
:Updated: July 20, 2026
|
|
15
|
+
:Author: Naveen Duhan
|
|
16
|
+
:Version: 1.0.0
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
__author__ = "Naveen Duhan"
|
|
21
|
+
__email__ = ""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodonAdaptPy Per-Isolate CDS Aggregation
|
|
3
|
+
|
|
4
|
+
This module groups independently framed coding sequences by isolate, produces
|
|
5
|
+
per-gene results, creates boundary-aware genome-wide coding-region summaries,
|
|
6
|
+
and selects any user-nominated focus genes such as F, G, or L. It never treats
|
|
7
|
+
a raw multi-ORF genome as one continuous reading frame.
|
|
8
|
+
|
|
9
|
+
Classes:
|
|
10
|
+
- IsolateAnalysis: Per-gene, genome-wide, and focus-gene results.
|
|
11
|
+
- CDSAggregator: Metadata-aware orchestration for isolate collections.
|
|
12
|
+
|
|
13
|
+
:Created: July 20, 2026
|
|
14
|
+
:Updated: July 20, 2026
|
|
15
|
+
:Author: Naveen Duhan
|
|
16
|
+
:Version: 1.0.0
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from dataclasses import asdict, dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .analyzer import CodonAnalyzer
|
|
26
|
+
from .exceptions import AnalysisError
|
|
27
|
+
from .models import AnalysisResult, SequenceRecord
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class IsolateAnalysis:
|
|
32
|
+
"""Store all analysis scopes generated for one biological isolate."""
|
|
33
|
+
|
|
34
|
+
isolate_id: str
|
|
35
|
+
genome_result: AnalysisResult
|
|
36
|
+
gene_results: list[AnalysisResult]
|
|
37
|
+
focus_gene_results: dict[str, list[AnalysisResult]] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Return nested isolate output in JSON-compatible form."""
|
|
41
|
+
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CDSAggregator:
|
|
46
|
+
"""Group CDS records and calculate gene-wise plus genome-wide results."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, analyzer: CodonAnalyzer) -> None:
|
|
49
|
+
"""Initialize aggregation around an existing configured analyzer."""
|
|
50
|
+
|
|
51
|
+
self.analyzer = analyzer
|
|
52
|
+
|
|
53
|
+
def analyze(
|
|
54
|
+
self,
|
|
55
|
+
records: list[SequenceRecord],
|
|
56
|
+
*,
|
|
57
|
+
isolate_key: str = "isolate",
|
|
58
|
+
gene_key: str = "gene",
|
|
59
|
+
focus_genes: tuple[str, ...] = (),
|
|
60
|
+
) -> list[IsolateAnalysis]:
|
|
61
|
+
"""Analyze CDSs grouped by metadata-defined biological isolate."""
|
|
62
|
+
|
|
63
|
+
grouped: dict[str, list[SequenceRecord]] = defaultdict(list)
|
|
64
|
+
for record in records:
|
|
65
|
+
isolate = str(record.metadata.get(isolate_key, "")).strip()
|
|
66
|
+
gene = str(record.metadata.get(gene_key, "")).strip()
|
|
67
|
+
if not isolate:
|
|
68
|
+
raise AnalysisError(f"Record {record.identifier!r} lacks metadata field {isolate_key!r}.")
|
|
69
|
+
if not gene:
|
|
70
|
+
raise AnalysisError(f"Record {record.identifier!r} lacks metadata field {gene_key!r}.")
|
|
71
|
+
grouped[isolate].append(record)
|
|
72
|
+
|
|
73
|
+
requested = {gene.casefold(): gene for gene in focus_genes}
|
|
74
|
+
outputs: list[IsolateAnalysis] = []
|
|
75
|
+
for isolate, members in sorted(grouped.items()):
|
|
76
|
+
gene_results: list[AnalysisResult] = []
|
|
77
|
+
focus_results: dict[str, list[AnalysisResult]] = {gene: [] for gene in focus_genes}
|
|
78
|
+
for record in members:
|
|
79
|
+
result = self.analyzer.analyze(record)
|
|
80
|
+
gene = str(record.metadata[gene_key])
|
|
81
|
+
result.metadata.update({"analysis_scope": "gene", "isolate": isolate, "gene": gene})
|
|
82
|
+
gene_results.append(result)
|
|
83
|
+
if gene.casefold() in requested:
|
|
84
|
+
focus_results[requested[gene.casefold()]].append(result)
|
|
85
|
+
missing_focus = [gene for gene, results in focus_results.items() if not results]
|
|
86
|
+
if missing_focus:
|
|
87
|
+
raise AnalysisError(f"Isolate {isolate!r} is missing focus gene(s): {', '.join(missing_focus)}")
|
|
88
|
+
genome_result = self.analyzer.analyze_cds_collection(
|
|
89
|
+
isolate,
|
|
90
|
+
members,
|
|
91
|
+
metadata={"isolate": isolate, "genes": [record.metadata[gene_key] for record in members]},
|
|
92
|
+
)
|
|
93
|
+
outputs.append(IsolateAnalysis(isolate, genome_result, gene_results, focus_results))
|
|
94
|
+
return outputs
|
codonadaptpy/analyzer.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodonAdaptPy Analysis Orchestration
|
|
3
|
+
|
|
4
|
+
This module exposes the primary class-based Python API. :class:`CodonAnalyzer`
|
|
5
|
+
coordinates normalization, validation, intrinsic metrics, optional reference-
|
|
6
|
+
dependent adaptation statistics, multi-host comparisons, simulation, and
|
|
7
|
+
sliding-window profiles while keeping each calculation module independently
|
|
8
|
+
testable.
|
|
9
|
+
|
|
10
|
+
Classes:
|
|
11
|
+
- AnalysisConfig: Controls optional analyses and profile resolution.
|
|
12
|
+
- CodonAnalyzer: Analyze one record, many records, or a supported file.
|
|
13
|
+
|
|
14
|
+
:Created: July 20, 2026
|
|
15
|
+
:Updated: July 20, 2026
|
|
16
|
+
:Author: Naveen Duhan
|
|
17
|
+
:Version: 1.0.0
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import platform
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from .__version__ import __version__
|
|
30
|
+
from .exceptions import AnalysisError
|
|
31
|
+
from .metrics import (
|
|
32
|
+
AdaptationMetrics,
|
|
33
|
+
CodonUsageMetrics,
|
|
34
|
+
CompositionMetrics,
|
|
35
|
+
EvolutionaryMetrics,
|
|
36
|
+
HostComparator,
|
|
37
|
+
PairMetrics,
|
|
38
|
+
)
|
|
39
|
+
from .models import AnalysisResult, SequenceRecord, ValidationConfig
|
|
40
|
+
from .parsers import SequenceParser
|
|
41
|
+
from .references import ReferenceManager, ReferenceProfile
|
|
42
|
+
from .validation import SequenceValidator
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class AnalysisConfig:
|
|
47
|
+
"""Configure validation, windows, simulation, and reproducibility settings."""
|
|
48
|
+
|
|
49
|
+
validation: ValidationConfig = field(default_factory=ValidationConfig)
|
|
50
|
+
sliding_window: int = 30
|
|
51
|
+
sliding_step: int = 1
|
|
52
|
+
simulate_cai: int = 0
|
|
53
|
+
random_seed: int = 1
|
|
54
|
+
strict: bool = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CodonAnalyzer:
|
|
58
|
+
"""Run complete codon-usage and adaptation analyses through one API."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
config: AnalysisConfig | None = None,
|
|
63
|
+
*,
|
|
64
|
+
reference: ReferenceProfile | None = None,
|
|
65
|
+
hosts: dict[str, ReferenceProfile] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Initialize an analyzer with optional target and host references."""
|
|
68
|
+
|
|
69
|
+
self.config = config or AnalysisConfig()
|
|
70
|
+
self.validator = SequenceValidator(self.config.validation)
|
|
71
|
+
self.code = self.validator.genetic_code
|
|
72
|
+
self.usage = CodonUsageMetrics(self.code)
|
|
73
|
+
self.composition = CompositionMetrics()
|
|
74
|
+
self.adaptation = AdaptationMetrics(self.code)
|
|
75
|
+
self.pairs = PairMetrics(self.code)
|
|
76
|
+
self.evolutionary = EvolutionaryMetrics()
|
|
77
|
+
self.host_comparator = HostComparator(self.code)
|
|
78
|
+
self.reference = reference
|
|
79
|
+
self.hosts = hosts or {}
|
|
80
|
+
for profile in [reference, *self.hosts.values()]:
|
|
81
|
+
if profile and profile.genetic_code != self.code.table_id:
|
|
82
|
+
raise AnalysisError(
|
|
83
|
+
f"Reference {profile.name!r} uses genetic code {profile.genetic_code}; "
|
|
84
|
+
f"analyzer uses {self.code.table_id}."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def analyze(self, record: SequenceRecord) -> AnalysisResult:
|
|
88
|
+
"""Analyze one sequence and return a complete structured result."""
|
|
89
|
+
|
|
90
|
+
validation = self.validator.validate(record, raise_on_error=self.config.strict)
|
|
91
|
+
sequence = validation.normalized_sequence
|
|
92
|
+
frame = self.config.validation.reading_frame
|
|
93
|
+
framed = sequence[frame:]
|
|
94
|
+
all_codons = [framed[index : index + 3] for index in range(0, len(framed) - 2, 3)]
|
|
95
|
+
codons = [codon for codon in all_codons if self.code.translate(codon) not in {None, "*"}]
|
|
96
|
+
if not codons:
|
|
97
|
+
raise AnalysisError(f"Sequence {record.identifier!r} has no analyzable sense codons.")
|
|
98
|
+
coding_sequence = "".join(codons)
|
|
99
|
+
composition = self.composition.gc_metrics(coding_sequence, genetic_code=self.code)
|
|
100
|
+
codon_counts = self.usage.counts(codons)
|
|
101
|
+
rscu = self.usage.rscu(codons)
|
|
102
|
+
pair_scores = self.pairs.scores(codons)
|
|
103
|
+
metrics: dict[str, Any] = {
|
|
104
|
+
"length_nt": len(sequence),
|
|
105
|
+
"sense_codons": len(codons),
|
|
106
|
+
"enc": self.usage.enc(codons),
|
|
107
|
+
"icdi": self.usage.icdi(codons),
|
|
108
|
+
"codon_pair_bias": self.pairs.bias(codons),
|
|
109
|
+
"coding_length_nt": len(coding_sequence),
|
|
110
|
+
**composition,
|
|
111
|
+
**self.evolutionary.parity_rule_2(
|
|
112
|
+
composition["a3"], composition["t3"], composition["g3"], composition["c3"]
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
sliding_windows: dict[str, list[dict[str, float]]] = {}
|
|
116
|
+
if self.reference:
|
|
117
|
+
weights = self.adaptation.weights_from_counts(self.reference.counts)
|
|
118
|
+
optimal = self.usage.optimal_codons(self.reference.counts)
|
|
119
|
+
reference_total = sum(self.reference.counts.values())
|
|
120
|
+
reference_frequencies = {
|
|
121
|
+
codon: count / reference_total if reference_total else 0.0
|
|
122
|
+
for codon, count in self.reference.counts.items()
|
|
123
|
+
}
|
|
124
|
+
metrics.update(
|
|
125
|
+
{
|
|
126
|
+
"cai": self.adaptation.cai(codons, weights),
|
|
127
|
+
"rcdi": self.adaptation.rcdi(codons, reference_frequencies),
|
|
128
|
+
"fop": self.usage.fop(codons, optimal),
|
|
129
|
+
"cbi": self.usage.cbi(codons, optimal),
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
sliding_windows["cai"] = self.adaptation.sliding_cai(
|
|
133
|
+
codons, weights, window=self.config.sliding_window, step=self.config.sliding_step
|
|
134
|
+
)
|
|
135
|
+
if self.config.simulate_cai:
|
|
136
|
+
metrics["simulated_cai"] = self.adaptation.simulated_cai(
|
|
137
|
+
codons, weights, simulations=self.config.simulate_cai, seed=self.config.random_seed
|
|
138
|
+
)
|
|
139
|
+
host_results = self.host_comparator.compare_many(
|
|
140
|
+
codons, {name: profile.counts for name, profile in self.hosts.items()}
|
|
141
|
+
)
|
|
142
|
+
dinucleotide_odds = self.composition.dinucleotide_odds(coding_sequence)
|
|
143
|
+
metrics["cpg_representation"] = dinucleotide_odds["CG"]
|
|
144
|
+
metrics["upa_representation"] = dinucleotide_odds["TA"]
|
|
145
|
+
return AnalysisResult(
|
|
146
|
+
identifier=record.identifier,
|
|
147
|
+
validation=validation,
|
|
148
|
+
metrics=metrics,
|
|
149
|
+
codon_counts=codon_counts,
|
|
150
|
+
rscu=rscu,
|
|
151
|
+
amino_acid_composition=self.usage.amino_acid_composition(codons),
|
|
152
|
+
dinucleotide_frequencies=self.composition.oligonucleotide_frequencies(coding_sequence, 2),
|
|
153
|
+
dinucleotide_odds=dinucleotide_odds,
|
|
154
|
+
dinucleotide_bias=self.composition.classify_dinucleotide_odds(dinucleotide_odds),
|
|
155
|
+
trinucleotide_frequencies=self.composition.oligonucleotide_frequencies(coding_sequence, 3),
|
|
156
|
+
codon_pair_scores=pair_scores,
|
|
157
|
+
sliding_windows=sliding_windows,
|
|
158
|
+
host_comparisons=host_results,
|
|
159
|
+
metadata=self._metadata(record, sequence),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def analyze_many(self, records: list[SequenceRecord]) -> list[AnalysisResult]:
|
|
163
|
+
"""Analyze records in deterministic input order."""
|
|
164
|
+
|
|
165
|
+
identifiers = [record.identifier for record in records]
|
|
166
|
+
duplicates = sorted({identifier for identifier in identifiers if identifiers.count(identifier) > 1})
|
|
167
|
+
if duplicates:
|
|
168
|
+
raise AnalysisError(f"Duplicate identifiers: {', '.join(duplicates)}")
|
|
169
|
+
return [self.analyze(record) for record in records]
|
|
170
|
+
|
|
171
|
+
def analyze_cds_collection(
|
|
172
|
+
self,
|
|
173
|
+
identifier: str,
|
|
174
|
+
records: list[SequenceRecord],
|
|
175
|
+
*,
|
|
176
|
+
metadata: dict[str, Any] | None = None,
|
|
177
|
+
) -> AnalysisResult:
|
|
178
|
+
"""Analyze pooled coding regions while preserving every CDS boundary.
|
|
179
|
+
|
|
180
|
+
Each member is validated and framed independently. Terminal stops are
|
|
181
|
+
excluded from pooled codon and composition metrics, and sequence-word
|
|
182
|
+
plus codon-pair calculations never span adjacent CDSs.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if not records:
|
|
186
|
+
raise AnalysisError("CDS aggregation requires at least one sequence record.")
|
|
187
|
+
segment_codons: list[list[str]] = []
|
|
188
|
+
segment_sequences: list[str] = []
|
|
189
|
+
member_checksums: dict[str, str] = {}
|
|
190
|
+
for record in records:
|
|
191
|
+
report = self.validator.validate(record, raise_on_error=self.config.strict)
|
|
192
|
+
framed = report.normalized_sequence[self.config.validation.reading_frame :]
|
|
193
|
+
raw_codons = [framed[index : index + 3] for index in range(0, len(framed) - 2, 3)]
|
|
194
|
+
codons = [codon for codon in raw_codons if self.code.translate(codon) not in {None, "*"}]
|
|
195
|
+
if not codons:
|
|
196
|
+
raise AnalysisError(f"CDS {record.identifier!r} has no analyzable sense codons.")
|
|
197
|
+
segment_codons.append(codons)
|
|
198
|
+
segment_sequences.append("".join(codons))
|
|
199
|
+
member_checksums[record.identifier] = hashlib.sha256(report.normalized_sequence.encode()).hexdigest()
|
|
200
|
+
|
|
201
|
+
terminal_stop = sorted(self.code.stop_codons)[0]
|
|
202
|
+
aggregate_record = SequenceRecord(
|
|
203
|
+
identifier=identifier,
|
|
204
|
+
sequence="".join(segment_sequences) + terminal_stop,
|
|
205
|
+
description=f"Boundary-aware aggregate of {len(records)} CDSs",
|
|
206
|
+
metadata=metadata or {},
|
|
207
|
+
)
|
|
208
|
+
result = self.analyze(aggregate_record)
|
|
209
|
+
pair_scores = self.pairs.scores_many(segment_codons)
|
|
210
|
+
dinucleotide_odds = self.composition.dinucleotide_odds_many(segment_sequences)
|
|
211
|
+
result.codon_pair_scores = pair_scores
|
|
212
|
+
result.metrics["codon_pair_bias"] = self.pairs.bias_many(segment_codons, pair_scores)
|
|
213
|
+
result.dinucleotide_frequencies = self.composition.oligonucleotide_frequencies_many(segment_sequences, 2)
|
|
214
|
+
result.dinucleotide_odds = dinucleotide_odds
|
|
215
|
+
result.dinucleotide_bias = self.composition.classify_dinucleotide_odds(dinucleotide_odds)
|
|
216
|
+
result.trinucleotide_frequencies = self.composition.oligonucleotide_frequencies_many(segment_sequences, 3)
|
|
217
|
+
result.metrics["cpg_representation"] = dinucleotide_odds["CG"]
|
|
218
|
+
result.metrics["upa_representation"] = dinucleotide_odds["TA"]
|
|
219
|
+
result.metrics["length_nt"] = sum(len("".join(record.sequence.split())) for record in records)
|
|
220
|
+
result.metrics["coding_length_nt"] = sum(len(sequence) for sequence in segment_sequences)
|
|
221
|
+
result.sliding_windows = {}
|
|
222
|
+
result.metadata.update(
|
|
223
|
+
{
|
|
224
|
+
"analysis_scope": "genome_wide_cds",
|
|
225
|
+
"cds_count": len(records),
|
|
226
|
+
"member_identifiers": [record.identifier for record in records],
|
|
227
|
+
"member_sha256": member_checksums,
|
|
228
|
+
"boundary_policy": "reading frames reset and terminal stops removed per CDS",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
def analyze_file(self, source: str | Path, *, fmt: str | None = None) -> list[AnalysisResult]:
|
|
234
|
+
"""Parse and analyze every record in a supported input file."""
|
|
235
|
+
|
|
236
|
+
return self.analyze_many(SequenceParser().parse(source, fmt=fmt))
|
|
237
|
+
|
|
238
|
+
def _metadata(self, record: SequenceRecord, sequence: str) -> dict[str, Any]:
|
|
239
|
+
"""Record analysis provenance required for reproducible reporting."""
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
**record.metadata,
|
|
243
|
+
"description": record.description,
|
|
244
|
+
"software_version": __version__,
|
|
245
|
+
"python_version": platform.python_version(),
|
|
246
|
+
"analysis_date_utc": datetime.now(timezone.utc).isoformat(),
|
|
247
|
+
"input_sha256": hashlib.sha256(sequence.encode()).hexdigest(),
|
|
248
|
+
"genetic_code": self.code.table_id,
|
|
249
|
+
"reading_frame": self.config.validation.reading_frame,
|
|
250
|
+
"random_seed": self.config.random_seed,
|
|
251
|
+
"reference": self.reference.name if self.reference else None,
|
|
252
|
+
"reference_version": self.reference.version if self.reference else None,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def analyzer_from_reference_files(
|
|
257
|
+
reference_path: str | Path | None = None,
|
|
258
|
+
host_paths: list[str | Path] | None = None,
|
|
259
|
+
*,
|
|
260
|
+
config: AnalysisConfig | None = None,
|
|
261
|
+
) -> CodonAnalyzer:
|
|
262
|
+
"""Build an analyzer by loading reference profiles from disk."""
|
|
263
|
+
|
|
264
|
+
manager = ReferenceManager()
|
|
265
|
+
reference = manager.load(reference_path) if reference_path else None
|
|
266
|
+
hosts = {}
|
|
267
|
+
for path in host_paths or []:
|
|
268
|
+
profile = manager.load(path)
|
|
269
|
+
hosts[profile.name] = profile
|
|
270
|
+
return CodonAnalyzer(config, reference=reference, hosts=hosts)
|