factorforge-cds 3.3.1__tar.gz → 3.4.0__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.
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/PKG-INFO +2 -2
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/README.md +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/pyproject.toml +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/analysis/feasibility.py +30 -3
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/cli/main.py +21 -11
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/core/interfaces/optimizer.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/nbenthamiana_golden_set.json +4 -0
- factorforge_cds-3.4.0/src/factorforge/design_review.py +437 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/optimizer.py +38 -8
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/pipeline.py +38 -15
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/rules/reverse_translator.py +81 -10
- factorforge_cds-3.4.0/src/factorforge/review/__init__.py +2 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/schemas/design_package.py +30 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/SOURCES.txt +4 -0
- factorforge_cds-3.4.0/tests/test_benchmark_cli_output_guard.py +87 -0
- factorforge_cds-3.4.0/tests/test_design_review.py +150 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/LICENSE +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/setup.cfg +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/analysis/metrics.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/qld183_v103_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/reference/reference_policy_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/scoring.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/utils.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/schemas/design_package.schema.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_benchmark_codon_table_metadata.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_cai.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_database.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_design_package_schema.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_registry_production_sync.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_validation_report.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/tests/test_worked_example.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
|
|
5
5
|
Author-email: Eijex <eijex.lab@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
|
|
|
93
93
|
## Citing
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
FactorForge v3.
|
|
96
|
+
FactorForge v3.4.0 (2026). Open-source constraint-based CDS design and sequence review.
|
|
97
97
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
|
|
|
60
60
|
## Citing
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
FactorForge v3.
|
|
63
|
+
FactorForge v3.4.0 (2026). Open-source constraint-based CDS design and sequence review.
|
|
64
64
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "factorforge-cds"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-only"
|
|
@@ -51,6 +51,7 @@ def _cai_from_log(log_sum: float, codon_count: int) -> float:
|
|
|
51
51
|
def _candidate_summary(
|
|
52
52
|
dna_sequence: str | None,
|
|
53
53
|
codon_weights: dict[str, float],
|
|
54
|
+
cai_authority: dict[str, Any],
|
|
54
55
|
) -> dict[str, Any] | None:
|
|
55
56
|
if dna_sequence is None:
|
|
56
57
|
return None
|
|
@@ -59,6 +60,7 @@ def _candidate_summary(
|
|
|
59
60
|
return {
|
|
60
61
|
"dna_sequence": dna_sequence,
|
|
61
62
|
"cai": calculate_cai(dna_sequence, codon_weights),
|
|
63
|
+
"cai_authority": dict(cai_authority),
|
|
62
64
|
"gc": calculate_gc(dna_sequence),
|
|
63
65
|
"first_region_gc": calculate_first_region_gc(dna_sequence),
|
|
64
66
|
"gc_window_min": min(window_values) if window_values else 0.0,
|
|
@@ -66,6 +68,20 @@ def _candidate_summary(
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def _build_cai_authority(codon_reference_id: str | None) -> dict[str, Any]:
|
|
72
|
+
if codon_reference_id is None:
|
|
73
|
+
return {
|
|
74
|
+
"reference_id": None,
|
|
75
|
+
"reference_role": "cai_evaluation",
|
|
76
|
+
"reference_relationship": "unresolved",
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
"reference_id": codon_reference_id,
|
|
80
|
+
"reference_role": "cai_evaluation",
|
|
81
|
+
"reference_relationship": "same_as_generation_reference",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
69
85
|
def _best_gc_under_gc_range(
|
|
70
86
|
states: dict[int, float],
|
|
71
87
|
protein_length: int,
|
|
@@ -104,6 +120,7 @@ def analyze_feasibility(
|
|
|
104
120
|
target_gc_low: float = DEFAULT_GC_LOW,
|
|
105
121
|
target_gc_high: float = DEFAULT_GC_HIGH,
|
|
106
122
|
gc_ranges: list[tuple[float, float]] | None = None,
|
|
123
|
+
codon_reference_id: str | None = None,
|
|
107
124
|
) -> dict[str, Any]:
|
|
108
125
|
"""Compute exact CAI/GC feasibility over synonymous codon choices.
|
|
109
126
|
|
|
@@ -160,6 +177,7 @@ def analyze_feasibility(
|
|
|
160
177
|
|
|
161
178
|
protein_length = len(protein)
|
|
162
179
|
total_bases = protein_length * 3
|
|
180
|
+
cai_authority = _build_cai_authority(codon_reference_id)
|
|
163
181
|
best_any_gc = max(states, key=lambda gc_count: states[gc_count])
|
|
164
182
|
best_any_log_sum = states[best_any_gc]
|
|
165
183
|
best_any_sequence = _reconstruct_sequence(backrefs, best_any_gc)
|
|
@@ -180,7 +198,7 @@ def analyze_feasibility(
|
|
|
180
198
|
range_results[key] = {
|
|
181
199
|
"feasible": True,
|
|
182
200
|
"max_cai": _cai_from_log(states[gc_count], protein_length),
|
|
183
|
-
"best_candidate": _candidate_summary(sequence, codon_weights),
|
|
201
|
+
"best_candidate": _candidate_summary(sequence, codon_weights, cai_authority),
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
target_cai_possible = (
|
|
@@ -192,8 +210,13 @@ def analyze_feasibility(
|
|
|
192
210
|
"protein_length": protein_length,
|
|
193
211
|
"minimum_possible_gc": (min_gc_count / total_bases) * 100.0,
|
|
194
212
|
"maximum_possible_gc": (max_gc_count / total_bases) * 100.0,
|
|
213
|
+
"cai_authority": dict(cai_authority),
|
|
195
214
|
"maximum_achievable_cai_without_gc": _cai_from_log(best_any_log_sum, protein_length),
|
|
196
|
-
"best_candidate_without_gc": _candidate_summary(
|
|
215
|
+
"best_candidate_without_gc": _candidate_summary(
|
|
216
|
+
best_any_sequence,
|
|
217
|
+
codon_weights,
|
|
218
|
+
cai_authority,
|
|
219
|
+
),
|
|
197
220
|
"ranges": range_results,
|
|
198
221
|
"target": {
|
|
199
222
|
"cai": target_cai,
|
|
@@ -206,7 +229,11 @@ def analyze_feasibility(
|
|
|
206
229
|
else None
|
|
207
230
|
),
|
|
208
231
|
"best_candidate": (
|
|
209
|
-
_candidate_summary(
|
|
232
|
+
_candidate_summary(
|
|
233
|
+
_reconstruct_sequence(backrefs, target_gc_count),
|
|
234
|
+
codon_weights,
|
|
235
|
+
cai_authority,
|
|
236
|
+
)
|
|
210
237
|
if target_gc_count is not None
|
|
211
238
|
else None
|
|
212
239
|
),
|
|
@@ -154,6 +154,7 @@ def _build_dp_result(
|
|
|
154
154
|
gc_max: float,
|
|
155
155
|
cai_target: float = DEFAULT_CAI_TARGET,
|
|
156
156
|
codon_table_path: Path | None = None,
|
|
157
|
+
codon_reference_id: str | None = None,
|
|
157
158
|
):
|
|
158
159
|
"""Run the constraint-based DP feasibility engine for a single protein sequence."""
|
|
159
160
|
if objective != "feasibility_best":
|
|
@@ -171,6 +172,7 @@ def _build_dp_result(
|
|
|
171
172
|
target_cai=cai_target,
|
|
172
173
|
target_gc_low=gc_min,
|
|
173
174
|
target_gc_high=gc_max,
|
|
175
|
+
codon_reference_id=codon_reference_id,
|
|
174
176
|
)
|
|
175
177
|
best = result["target"]["best_candidate"]
|
|
176
178
|
feasible = best is not None
|
|
@@ -222,7 +224,10 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
|
|
|
222
224
|
cai = float(result.metrics.get("cai", 0.0))
|
|
223
225
|
gc = float(result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0)))
|
|
224
226
|
score = float(result.metrics.get("score", 0.0))
|
|
225
|
-
header =
|
|
227
|
+
header = (
|
|
228
|
+
f">{sequence_id}|engine=profile|profile={profile}|"
|
|
229
|
+
f"cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
|
|
230
|
+
)
|
|
226
231
|
return f"{header}\n{_wrap_sequence(result.sequence)}\n"
|
|
227
232
|
|
|
228
233
|
|
|
@@ -399,7 +404,10 @@ def optimize(
|
|
|
399
404
|
if reference_table_path is not None:
|
|
400
405
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
401
406
|
|
|
402
|
-
optimizer = RuleBasedOptimizer(
|
|
407
|
+
optimizer = RuleBasedOptimizer(
|
|
408
|
+
codon_table_path=str(reference_table_path),
|
|
409
|
+
generation_reference_id=reference_id,
|
|
410
|
+
)
|
|
403
411
|
else:
|
|
404
412
|
optimizer = EngineRegistry.get("profile")
|
|
405
413
|
profile_results = []
|
|
@@ -436,7 +444,10 @@ def optimize(
|
|
|
436
444
|
if reference_table_path is not None and engine == "profile":
|
|
437
445
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
438
446
|
|
|
439
|
-
optimizer = RuleBasedOptimizer(
|
|
447
|
+
optimizer = RuleBasedOptimizer(
|
|
448
|
+
codon_table_path=str(reference_table_path),
|
|
449
|
+
generation_reference_id=reference_id,
|
|
450
|
+
)
|
|
440
451
|
else:
|
|
441
452
|
optimizer = EngineRegistry.get(engine)
|
|
442
453
|
payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
|
|
@@ -465,14 +476,9 @@ def optimize(
|
|
|
465
476
|
combined_fasta = []
|
|
466
477
|
for idx, result in enumerate(results):
|
|
467
478
|
seq_id = payload[idx]["id"]
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
score = result.metrics.get("score", 0.0)
|
|
471
|
-
header = (
|
|
472
|
-
f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
|
|
473
|
-
f"gc={float(gc):.2f}|score={float(score):.3f}"
|
|
479
|
+
combined_fasta.append(
|
|
480
|
+
_format_profile_fasta(seq_id, profile, result).rstrip()
|
|
474
481
|
)
|
|
475
|
-
combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
|
|
476
482
|
out_content = "\n".join(combined_fasta) + "\n"
|
|
477
483
|
|
|
478
484
|
if output:
|
|
@@ -497,6 +503,7 @@ def optimize(
|
|
|
497
503
|
gc_max=gc_max,
|
|
498
504
|
cai_target=cai_target,
|
|
499
505
|
codon_table_path=reference_table_path,
|
|
506
|
+
codon_reference_id=reference_id,
|
|
500
507
|
)
|
|
501
508
|
dna_sequence = best["dna_sequence"]
|
|
502
509
|
cai = float(best["cai"])
|
|
@@ -566,7 +573,10 @@ def optimize(
|
|
|
566
573
|
if reference_table_path is not None and engine == "profile":
|
|
567
574
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
568
575
|
|
|
569
|
-
optimizer = RuleBasedOptimizer(
|
|
576
|
+
optimizer = RuleBasedOptimizer(
|
|
577
|
+
codon_table_path=str(reference_table_path),
|
|
578
|
+
generation_reference_id=reference_id,
|
|
579
|
+
)
|
|
570
580
|
else:
|
|
571
581
|
optimizer = EngineRegistry.get(engine)
|
|
572
582
|
|
{factorforge_cds-3.3.1 → factorforge_cds-3.4.0}/src/factorforge/data/nbenthamiana_golden_set.json
RENAMED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"organism": "Nicotiana benthamiana",
|
|
3
|
+
"reference_id": "nbenthamiana_golden_set_v1",
|
|
4
|
+
"reference_role": "cai_evaluation",
|
|
5
|
+
"reference_version": "1",
|
|
3
6
|
"source": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
|
|
7
|
+
"source_basis": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
|
|
4
8
|
"description": "Codon usage frequencies weighted toward highly expressed N. benthamiana genes (RuBisCO, ribosomal proteins, histones). Used as CAI reference weights per Sharp & Li (1987). All source data is publicly available.",
|
|
5
9
|
"blend_ratio": 0.7,
|
|
6
10
|
"optimal_gc_content": 42.5,
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Deterministic CDS design-review parsing, criteria, and decision helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from factorforge.analysis.metrics import (
|
|
12
|
+
STOP_CODONS,
|
|
13
|
+
calculate_cai,
|
|
14
|
+
calculate_gc,
|
|
15
|
+
calculate_gc_windows,
|
|
16
|
+
detect_forbidden_motifs,
|
|
17
|
+
translate_dna,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ConstraintMode = Literal["required", "preferred", "ignored"]
|
|
21
|
+
AutomatedDecision = Literal["PASS", "CONDITIONAL_PASS", "FAIL"]
|
|
22
|
+
|
|
23
|
+
MULTI_FASTA_ERROR = "Multiple FASTA records detected. Upload one sequence at a time."
|
|
24
|
+
VALID_PROTEIN = frozenset("ACDEFGHIKLMNPQRSTVWY*")
|
|
25
|
+
VALID_DNA = frozenset("ACGT")
|
|
26
|
+
TYPE_IIS_SITES = {
|
|
27
|
+
"BsaI": ("GGTCTC", "GAGACC"),
|
|
28
|
+
"BsmBI/Esp3I": ("CGTCTC", "GAGACG"),
|
|
29
|
+
"SapI": ("GCTCTTC", "GAAGAGC"),
|
|
30
|
+
}
|
|
31
|
+
VALID_MODES = frozenset({"required", "preferred", "ignored"})
|
|
32
|
+
VALID_DISPOSITIONS = frozenset(
|
|
33
|
+
{"accept", "accept_with_exception", "return_for_redesign", "reject"}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _compact_sequence(value: str) -> str:
|
|
38
|
+
return re.sub(r"\s+", "", value).upper()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_sequence_input(raw: str) -> dict[str, Any]:
|
|
42
|
+
"""Parse one plain sequence or one FASTA record and classify it explicitly."""
|
|
43
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
44
|
+
raise ValueError("Sequence is required.")
|
|
45
|
+
|
|
46
|
+
lines = raw.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
47
|
+
nonempty = [line.strip() for line in lines if line.strip()]
|
|
48
|
+
headers = [index for index, line in enumerate(nonempty) if line.startswith(">")]
|
|
49
|
+
if len(headers) > 1:
|
|
50
|
+
raise ValueError(MULTI_FASTA_ERROR)
|
|
51
|
+
if headers and headers[0] != 0:
|
|
52
|
+
raise ValueError("FASTA header must be the first non-empty line.")
|
|
53
|
+
|
|
54
|
+
header = nonempty[0][1:].strip() if headers else None
|
|
55
|
+
sequence_lines = nonempty[1:] if headers else nonempty
|
|
56
|
+
sequence = _compact_sequence("".join(sequence_lines))
|
|
57
|
+
if not sequence:
|
|
58
|
+
raise ValueError("Sequence is required.")
|
|
59
|
+
|
|
60
|
+
characters = set(sequence)
|
|
61
|
+
if characters <= VALID_DNA:
|
|
62
|
+
return _build_cds_context(sequence, header)
|
|
63
|
+
|
|
64
|
+
invalid = characters - VALID_PROTEIN
|
|
65
|
+
if invalid:
|
|
66
|
+
raise ValueError(f"Invalid sequence characters: {', '.join(sorted(invalid))}.")
|
|
67
|
+
if "*" in sequence[:-1]:
|
|
68
|
+
raise ValueError("Protein input contains an internal stop marker.")
|
|
69
|
+
|
|
70
|
+
protein = sequence.rstrip("*")
|
|
71
|
+
if not protein:
|
|
72
|
+
raise ValueError("Protein sequence must contain at least one amino acid.")
|
|
73
|
+
return {
|
|
74
|
+
"input_type": "protein",
|
|
75
|
+
"normalized_sequence": sequence,
|
|
76
|
+
"optimization_sequence": protein,
|
|
77
|
+
"fasta_header": header,
|
|
78
|
+
"generation_allowed": True,
|
|
79
|
+
"message": (
|
|
80
|
+
"Protein sequence detected. FactorForge will reverse translate it into "
|
|
81
|
+
"a host-adapted coding sequence."
|
|
82
|
+
),
|
|
83
|
+
"summary": {
|
|
84
|
+
"protein_length_aa": len(protein),
|
|
85
|
+
"terminal_stop_marker": sequence.endswith("*"),
|
|
86
|
+
},
|
|
87
|
+
"errors": [],
|
|
88
|
+
"warnings": [],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_cds_context(sequence: str, header: str | None) -> dict[str, Any]:
|
|
93
|
+
errors: list[str] = []
|
|
94
|
+
warnings: list[str] = []
|
|
95
|
+
frame_valid = len(sequence) % 3 == 0
|
|
96
|
+
if not frame_valid:
|
|
97
|
+
errors.append("CDS length must be divisible by 3.")
|
|
98
|
+
|
|
99
|
+
codons = [sequence[index : index + 3] for index in range(0, len(sequence), 3)]
|
|
100
|
+
complete_codons = [codon for codon in codons if len(codon) == 3]
|
|
101
|
+
start_present = bool(complete_codons) and complete_codons[0] == "ATG"
|
|
102
|
+
terminal_stop_present = bool(complete_codons) and complete_codons[-1] in STOP_CODONS
|
|
103
|
+
internal_stops = [
|
|
104
|
+
index for index, codon in enumerate(complete_codons[:-1]) if codon in STOP_CODONS
|
|
105
|
+
]
|
|
106
|
+
if internal_stops:
|
|
107
|
+
errors.append("CDS contains internal stop codon(s).")
|
|
108
|
+
if not start_present:
|
|
109
|
+
warnings.append("Start codon is absent.")
|
|
110
|
+
if not terminal_stop_present:
|
|
111
|
+
warnings.append("Terminal stop codon is absent.")
|
|
112
|
+
|
|
113
|
+
translated = translate_dna(sequence) if frame_valid else ""
|
|
114
|
+
protein = translated[:-1] if terminal_stop_present else translated
|
|
115
|
+
return {
|
|
116
|
+
"input_type": "cds",
|
|
117
|
+
"normalized_sequence": sequence,
|
|
118
|
+
"optimization_sequence": protein,
|
|
119
|
+
"fasta_header": header,
|
|
120
|
+
"generation_allowed": not errors,
|
|
121
|
+
"message": (
|
|
122
|
+
"DNA coding sequence detected. FactorForge will preserve the translated "
|
|
123
|
+
"protein while redesigning synonymous codons for the selected host and constraints."
|
|
124
|
+
),
|
|
125
|
+
"summary": {
|
|
126
|
+
"cds_length_bp": len(sequence),
|
|
127
|
+
"codon_count": len(complete_codons),
|
|
128
|
+
"protein_length_aa": len(protein),
|
|
129
|
+
"gc_percent": round(calculate_gc(sequence), 1),
|
|
130
|
+
"frame_valid": frame_valid,
|
|
131
|
+
"start_codon_present": start_present,
|
|
132
|
+
"terminal_stop_present": terminal_stop_present,
|
|
133
|
+
"terminal_stop_codon": complete_codons[-1] if terminal_stop_present else None,
|
|
134
|
+
"internal_stop_count": len(internal_stops),
|
|
135
|
+
},
|
|
136
|
+
"errors": errors,
|
|
137
|
+
"warnings": warnings,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def restore_cds_stop_policy(candidate: str, input_context: dict[str, Any]) -> str:
|
|
142
|
+
"""Restore the original CDS terminal-stop policy to a generated candidate."""
|
|
143
|
+
sequence = _compact_sequence(candidate)
|
|
144
|
+
if input_context["input_type"] != "cds":
|
|
145
|
+
return sequence
|
|
146
|
+
stop = input_context["summary"]["terminal_stop_codon"]
|
|
147
|
+
translated = translate_dna(sequence)
|
|
148
|
+
if translated.endswith("*"):
|
|
149
|
+
sequence = sequence[:-3]
|
|
150
|
+
return sequence + (stop or "")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def assert_pathway_invariants(
|
|
154
|
+
original_cds: str,
|
|
155
|
+
optimized_cds: str,
|
|
156
|
+
input_context: dict[str, Any],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Raise visibly when CDS length or translated-protein invariants regress."""
|
|
159
|
+
if input_context["input_type"] != "cds":
|
|
160
|
+
return
|
|
161
|
+
if len(original_cds) != len(optimized_cds):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"CDS invariant failed: optimized nucleotide length differs from the original."
|
|
164
|
+
)
|
|
165
|
+
original_protein = translate_dna(original_cds).rstrip("*")
|
|
166
|
+
optimized_protein = translate_dna(optimized_cds).rstrip("*")
|
|
167
|
+
if original_protein != optimized_protein:
|
|
168
|
+
raise ValueError("CDS invariant failed: translated protein was not preserved.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def default_acceptance_criteria(
|
|
172
|
+
gc_min: float = 40.0,
|
|
173
|
+
gc_max: float = 55.0,
|
|
174
|
+
) -> dict[str, dict[str, Any]]:
|
|
175
|
+
return {
|
|
176
|
+
"cai": {"mode": "preferred", "minimum": 0.8},
|
|
177
|
+
"overall_gc": {"mode": "preferred", "minimum": gc_min, "maximum": gc_max},
|
|
178
|
+
"local_gc": {
|
|
179
|
+
"mode": "preferred",
|
|
180
|
+
"minimum": 30.0,
|
|
181
|
+
"maximum": 70.0,
|
|
182
|
+
"window_size": 60,
|
|
183
|
+
},
|
|
184
|
+
"type_iis": {
|
|
185
|
+
"mode": "required",
|
|
186
|
+
"enzymes": ["BsaI", "BsmBI/Esp3I", "SapI"],
|
|
187
|
+
"custom_sites": [],
|
|
188
|
+
},
|
|
189
|
+
"repeats": {"mode": "preferred", "minimum_length": 18, "maximum_count": 0},
|
|
190
|
+
"homopolymers": {"mode": "preferred", "maximum_length": 8},
|
|
191
|
+
"forbidden_motifs": {
|
|
192
|
+
"mode": "preferred",
|
|
193
|
+
"motifs": ["AATAAA", "GTAAGT", "ATTTA"],
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def normalize_acceptance_criteria(
|
|
199
|
+
value: dict[str, Any] | None,
|
|
200
|
+
*,
|
|
201
|
+
gc_min: float,
|
|
202
|
+
gc_max: float,
|
|
203
|
+
) -> dict[str, dict[str, Any]]:
|
|
204
|
+
criteria = default_acceptance_criteria(gc_min, gc_max)
|
|
205
|
+
if value is None:
|
|
206
|
+
return criteria
|
|
207
|
+
if not isinstance(value, dict):
|
|
208
|
+
raise ValueError("acceptance_criteria must be an object.")
|
|
209
|
+
unknown = set(value) - set(criteria)
|
|
210
|
+
if unknown:
|
|
211
|
+
raise ValueError(f"Unknown acceptance criteria: {', '.join(sorted(unknown))}.")
|
|
212
|
+
for name, override in value.items():
|
|
213
|
+
if not isinstance(override, dict):
|
|
214
|
+
raise ValueError(f"acceptance_criteria.{name} must be an object.")
|
|
215
|
+
criteria[name].update(deepcopy(override))
|
|
216
|
+
mode = criteria[name].get("mode")
|
|
217
|
+
if mode not in VALID_MODES:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"acceptance_criteria.{name}.mode must be required, preferred, or ignored."
|
|
220
|
+
)
|
|
221
|
+
_validate_criteria_ranges(criteria)
|
|
222
|
+
return criteria
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _validate_criteria_ranges(criteria: dict[str, dict[str, Any]]) -> None:
|
|
226
|
+
for name in ("overall_gc", "local_gc"):
|
|
227
|
+
minimum = float(criteria[name]["minimum"])
|
|
228
|
+
maximum = float(criteria[name]["maximum"])
|
|
229
|
+
if not 0 <= minimum <= maximum <= 100:
|
|
230
|
+
raise ValueError(f"acceptance_criteria.{name} range must be within 0-100.")
|
|
231
|
+
cai_minimum = float(criteria["cai"]["minimum"])
|
|
232
|
+
if not 0 <= cai_minimum <= 1:
|
|
233
|
+
raise ValueError("acceptance_criteria.cai.minimum must be within 0-1.")
|
|
234
|
+
if int(criteria["local_gc"]["window_size"]) <= 0:
|
|
235
|
+
raise ValueError("acceptance_criteria.local_gc.window_size must be positive.")
|
|
236
|
+
if int(criteria["homopolymers"]["maximum_length"]) < 2:
|
|
237
|
+
raise ValueError("acceptance_criteria.homopolymers.maximum_length must be at least 2.")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _count_direct_repeats(sequence: str, minimum_length: int) -> int:
|
|
241
|
+
if len(sequence) < minimum_length * 2:
|
|
242
|
+
return 0
|
|
243
|
+
seen: set[str] = set()
|
|
244
|
+
repeated: set[str] = set()
|
|
245
|
+
for index in range(0, len(sequence) - minimum_length + 1):
|
|
246
|
+
motif = sequence[index : index + minimum_length]
|
|
247
|
+
if motif in seen:
|
|
248
|
+
repeated.add(motif)
|
|
249
|
+
seen.add(motif)
|
|
250
|
+
return len(repeated)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _longest_homopolymer(sequence: str) -> int:
|
|
254
|
+
return max((len(match.group(0)) for match in re.finditer(r"([ACGT])\1*", sequence)), default=0)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _type_iis_findings(
|
|
258
|
+
sequence: str,
|
|
259
|
+
criterion: dict[str, Any],
|
|
260
|
+
) -> list[dict[str, Any]]:
|
|
261
|
+
findings: list[dict[str, Any]] = []
|
|
262
|
+
enabled = set(criterion.get("enzymes", []))
|
|
263
|
+
for enzyme, patterns in TYPE_IIS_SITES.items():
|
|
264
|
+
if enzyme not in enabled:
|
|
265
|
+
continue
|
|
266
|
+
for pattern in patterns:
|
|
267
|
+
start = sequence.find(pattern)
|
|
268
|
+
while start >= 0:
|
|
269
|
+
findings.append({"enzyme": enzyme, "site": pattern, "start": start})
|
|
270
|
+
start = sequence.find(pattern, start + 1)
|
|
271
|
+
for site in criterion.get("custom_sites", []):
|
|
272
|
+
if isinstance(site, dict):
|
|
273
|
+
name = str(site.get("name") or "Custom")
|
|
274
|
+
pattern = _compact_sequence(str(site.get("sequence") or ""))
|
|
275
|
+
else:
|
|
276
|
+
name = "Custom"
|
|
277
|
+
pattern = _compact_sequence(str(site))
|
|
278
|
+
if not pattern or set(pattern) - VALID_DNA:
|
|
279
|
+
continue
|
|
280
|
+
start = sequence.find(pattern)
|
|
281
|
+
while start >= 0:
|
|
282
|
+
findings.append({"enzyme": name, "site": pattern, "start": start})
|
|
283
|
+
start = sequence.find(pattern, start + 1)
|
|
284
|
+
return findings
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def evaluate_candidate(
|
|
288
|
+
sequence: str,
|
|
289
|
+
*,
|
|
290
|
+
cai: float,
|
|
291
|
+
criteria: dict[str, dict[str, Any]],
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""Evaluate one CDS against a normalized criteria snapshot."""
|
|
294
|
+
seq = _compact_sequence(sequence)
|
|
295
|
+
overall_gc = calculate_gc(seq)
|
|
296
|
+
local_rule = criteria["local_gc"]
|
|
297
|
+
windows = calculate_gc_windows(
|
|
298
|
+
seq,
|
|
299
|
+
window_size=int(local_rule["window_size"]),
|
|
300
|
+
step=max(1, int(local_rule["window_size"]) // 2),
|
|
301
|
+
)
|
|
302
|
+
local_values = [float(window["gc"]) for window in windows] or [overall_gc]
|
|
303
|
+
type_iis = _type_iis_findings(seq, criteria["type_iis"])
|
|
304
|
+
repeat_count = _count_direct_repeats(seq, int(criteria["repeats"]["minimum_length"]))
|
|
305
|
+
longest_homopolymer = _longest_homopolymer(seq)
|
|
306
|
+
motifs = detect_forbidden_motifs(seq, list(criteria["forbidden_motifs"].get("motifs", [])))
|
|
307
|
+
|
|
308
|
+
observations = {
|
|
309
|
+
"cai": (float(cai), float(criteria["cai"]["minimum"]), float(cai) >= float(criteria["cai"]["minimum"])),
|
|
310
|
+
"overall_gc": (
|
|
311
|
+
round(overall_gc, 1),
|
|
312
|
+
f"{float(criteria['overall_gc']['minimum']):g}-{float(criteria['overall_gc']['maximum']):g}%",
|
|
313
|
+
float(criteria["overall_gc"]["minimum"]) <= overall_gc <= float(criteria["overall_gc"]["maximum"]),
|
|
314
|
+
),
|
|
315
|
+
"local_gc": (
|
|
316
|
+
f"{min(local_values):.1f}-{max(local_values):.1f}%",
|
|
317
|
+
f"{float(local_rule['minimum']):g}-{float(local_rule['maximum']):g}%",
|
|
318
|
+
min(local_values) >= float(local_rule["minimum"]) and max(local_values) <= float(local_rule["maximum"]),
|
|
319
|
+
),
|
|
320
|
+
"type_iis": (len(type_iis), 0, not type_iis),
|
|
321
|
+
"repeats": (
|
|
322
|
+
repeat_count,
|
|
323
|
+
int(criteria["repeats"]["maximum_count"]),
|
|
324
|
+
repeat_count <= int(criteria["repeats"]["maximum_count"]),
|
|
325
|
+
),
|
|
326
|
+
"homopolymers": (
|
|
327
|
+
longest_homopolymer,
|
|
328
|
+
int(criteria["homopolymers"]["maximum_length"]),
|
|
329
|
+
longest_homopolymer <= int(criteria["homopolymers"]["maximum_length"]),
|
|
330
|
+
),
|
|
331
|
+
"forbidden_motifs": (len(motifs), 0, not motifs),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
rows: list[dict[str, Any]] = []
|
|
335
|
+
required_failures = 0
|
|
336
|
+
preferred_warnings = 0
|
|
337
|
+
for name, (observed, threshold, passed) in observations.items():
|
|
338
|
+
mode: ConstraintMode = criteria[name]["mode"]
|
|
339
|
+
if mode == "ignored":
|
|
340
|
+
result = "IGNORED"
|
|
341
|
+
elif passed:
|
|
342
|
+
result = "PASS"
|
|
343
|
+
elif mode == "required":
|
|
344
|
+
result = "FAIL"
|
|
345
|
+
required_failures += 1
|
|
346
|
+
else:
|
|
347
|
+
result = "WARN"
|
|
348
|
+
preferred_warnings += 1
|
|
349
|
+
rows.append(
|
|
350
|
+
{
|
|
351
|
+
"criterion": name,
|
|
352
|
+
"mode": mode,
|
|
353
|
+
"observed": observed,
|
|
354
|
+
"threshold": threshold,
|
|
355
|
+
"result": result,
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if required_failures:
|
|
360
|
+
decision: AutomatedDecision = "FAIL"
|
|
361
|
+
elif preferred_warnings:
|
|
362
|
+
decision = "CONDITIONAL_PASS"
|
|
363
|
+
else:
|
|
364
|
+
decision = "PASS"
|
|
365
|
+
failed_names = [
|
|
366
|
+
row["criterion"] for row in rows if row["result"] in {"FAIL", "WARN"}
|
|
367
|
+
]
|
|
368
|
+
explanation = (
|
|
369
|
+
f"{', '.join(failed_names)} require review."
|
|
370
|
+
if failed_names
|
|
371
|
+
else "All active acceptance criteria passed."
|
|
372
|
+
)
|
|
373
|
+
return {
|
|
374
|
+
"automated_decision": decision,
|
|
375
|
+
"required_failure_count": required_failures,
|
|
376
|
+
"preferred_warning_count": preferred_warnings,
|
|
377
|
+
"explanation": explanation,
|
|
378
|
+
"criteria": rows,
|
|
379
|
+
"details": {
|
|
380
|
+
"type_iis_sites": type_iis,
|
|
381
|
+
"forbidden_motifs": motifs,
|
|
382
|
+
"local_gc_min": round(min(local_values), 1),
|
|
383
|
+
"local_gc_max": round(max(local_values), 1),
|
|
384
|
+
"repeat_count": repeat_count,
|
|
385
|
+
"longest_homopolymer": longest_homopolymer,
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def apply_reviewer_disposition(
|
|
391
|
+
automated_decision: AutomatedDecision,
|
|
392
|
+
value: dict[str, Any] | None,
|
|
393
|
+
) -> dict[str, Any] | None:
|
|
394
|
+
if value is None:
|
|
395
|
+
return None
|
|
396
|
+
if not isinstance(value, dict):
|
|
397
|
+
raise ValueError("reviewer_disposition must be an object.")
|
|
398
|
+
disposition = str(value.get("disposition") or "").strip().lower()
|
|
399
|
+
if disposition not in VALID_DISPOSITIONS:
|
|
400
|
+
raise ValueError(
|
|
401
|
+
"reviewer_disposition.disposition must be accept, accept_with_exception, "
|
|
402
|
+
"return_for_redesign, or reject."
|
|
403
|
+
)
|
|
404
|
+
reason = str(value.get("reason") or "").strip()
|
|
405
|
+
accepting_failure = automated_decision == "FAIL" and disposition in {
|
|
406
|
+
"accept",
|
|
407
|
+
"accept_with_exception",
|
|
408
|
+
}
|
|
409
|
+
if accepting_failure and not reason:
|
|
410
|
+
raise ValueError("A written reason is required to accept an automated FAIL.")
|
|
411
|
+
final_state = {
|
|
412
|
+
"accept": "ACCEPTED",
|
|
413
|
+
"accept_with_exception": "ACCEPTED_WITH_EXCEPTION",
|
|
414
|
+
"return_for_redesign": "RETURNED_FOR_REDESIGN",
|
|
415
|
+
"reject": "REJECTED",
|
|
416
|
+
}[disposition]
|
|
417
|
+
if accepting_failure:
|
|
418
|
+
final_state = "MANUALLY_ACCEPTED"
|
|
419
|
+
return {
|
|
420
|
+
"disposition": disposition,
|
|
421
|
+
"reason": reason or None,
|
|
422
|
+
"timestamp": value.get("timestamp")
|
|
423
|
+
or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
424
|
+
"final_state": final_state,
|
|
425
|
+
"automated_decision": automated_decision,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def build_result_identifier(input_sequence: str, criteria: dict[str, Any]) -> str:
|
|
430
|
+
payload = f"{input_sequence}|{repr(criteria)}".encode("utf-8")
|
|
431
|
+
return "ff-" + hashlib.sha256(payload).hexdigest()[:16]
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def original_cai(sequence: str, codon_weights: dict[str, float]) -> float | None:
|
|
435
|
+
if not sequence or len(sequence) % 3:
|
|
436
|
+
return None
|
|
437
|
+
return calculate_cai(sequence, codon_weights)
|