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.
@@ -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
+ ]
@@ -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
@@ -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)