factorforge-cds 3.3.0__tar.gz → 3.3.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.
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/PKG-INFO +2 -2
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/README.md +1 -1
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/pyproject.toml +2 -2
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/feasibility.py +30 -3
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/main.py +203 -16
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/optimizer.py +1 -1
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_golden_set.json +4 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +526 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +70 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +22 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +526 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +70 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +22 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_derived_codons.json +526 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +22 -0
- factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +70 -0
- factorforge_cds-3.3.2/src/factorforge/data/reference/reference_policy_manifest.json +262 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/optimizer.py +14 -6
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/pipeline.py +24 -13
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/reverse_translator.py +81 -10
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring.py +95 -47
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.schema.json +11 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/SOURCES.txt +11 -0
- factorforge_cds-3.3.2/tests/test_benchmark_cli_output_guard.py +87 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_schema.py +15 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/LICENSE +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/setup.cfg +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/metrics.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/utils.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_codon_table_metadata.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_cai.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_database.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_registry_production_sync.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_report.py +0 -0
- {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/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.
|
|
3
|
+
Version: 3.3.2
|
|
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.3.
|
|
96
|
+
FactorForge v3.3.2 (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.3.
|
|
63
|
+
FactorForge v3.3.2 (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.3.
|
|
7
|
+
version = "3.3.2"
|
|
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"
|
|
@@ -53,7 +53,7 @@ factorforge-validate = "factorforge.validation.cli:main"
|
|
|
53
53
|
where = ["src"]
|
|
54
54
|
|
|
55
55
|
[tool.setuptools.package-data]
|
|
56
|
-
"factorforge" = ["data/*.json", "data/templates/*.json", "schemas/*.json"]
|
|
56
|
+
"factorforge" = ["data/*.json", "data/templates/*.json", "data/profiles/*.json", "data/reference/*.json", "schemas/*.json"]
|
|
57
57
|
|
|
58
58
|
[tool.pytest.ini_options]
|
|
59
59
|
testpaths = ["tests"]
|
|
@@ -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
|
),
|
|
@@ -8,15 +8,119 @@ Usage:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
11
13
|
import sys
|
|
12
14
|
|
|
13
15
|
import click
|
|
14
16
|
|
|
15
17
|
from factorforge import __version__
|
|
18
|
+
from factorforge.analysis.feasibility import DEFAULT_CAI_TARGET
|
|
16
19
|
from factorforge.engines.registry import EngineRegistry
|
|
17
20
|
from factorforge.engines.profile.utils import parse_fasta_records
|
|
18
21
|
|
|
19
22
|
HOST_MAP = {"nbenthamiana": "nbenthamiana", "by2": "ntabacum"}
|
|
23
|
+
HOST_TAXIDS = {"nbenthamiana": 4100, "ntabacum": 4097}
|
|
24
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
FACTORFORGE_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
26
|
+
REFERENCE_POLICY_MANIFEST_PATH = (
|
|
27
|
+
FACTORFORGE_REPO_ROOT / "data" / "reference" / "reference_policy_manifest.json"
|
|
28
|
+
)
|
|
29
|
+
BUNDLED_REFERENCE_POLICY_MANIFEST_PATH = (
|
|
30
|
+
PACKAGE_ROOT / "data" / "reference" / "reference_policy_manifest.json"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _reference_policy_manifest_path() -> Path:
|
|
35
|
+
"""Return the repo manifest during development, or the packaged manifest in wheels."""
|
|
36
|
+
if REFERENCE_POLICY_MANIFEST_PATH.exists():
|
|
37
|
+
return REFERENCE_POLICY_MANIFEST_PATH
|
|
38
|
+
return BUNDLED_REFERENCE_POLICY_MANIFEST_PATH
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_packaged_or_repo_path(relative_path: str) -> Path:
|
|
42
|
+
"""Resolve manifest paths in a source checkout or an installed wheel."""
|
|
43
|
+
repo_path = FACTORFORGE_REPO_ROOT / relative_path
|
|
44
|
+
if repo_path.exists():
|
|
45
|
+
return repo_path
|
|
46
|
+
|
|
47
|
+
package_prefix = "src/factorforge/"
|
|
48
|
+
if relative_path.startswith(package_prefix):
|
|
49
|
+
return PACKAGE_ROOT / relative_path.removeprefix(package_prefix)
|
|
50
|
+
return PACKAGE_ROOT / relative_path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_reference_policy_manifest() -> dict:
|
|
54
|
+
"""Load the checksum/tier policy manifest for expert CLI reference selection."""
|
|
55
|
+
return json.loads(_reference_policy_manifest_path().read_text(encoding="utf-8"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _reference_entries_by_id() -> dict[str, dict]:
|
|
59
|
+
manifest = _load_reference_policy_manifest()
|
|
60
|
+
return {entry["reference_id"]: entry for entry in manifest["references"]}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _reference_id_choices() -> tuple[str, ...]:
|
|
64
|
+
return tuple(_reference_entries_by_id())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
REFERENCE_ID_CHOICES = _reference_id_choices()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _reference_entry_by_id(reference_id: str) -> dict:
|
|
71
|
+
entries = _reference_entries_by_id()
|
|
72
|
+
try:
|
|
73
|
+
return entries[reference_id]
|
|
74
|
+
except KeyError as exc:
|
|
75
|
+
choices = ", ".join(sorted(entries))
|
|
76
|
+
raise click.UsageError(
|
|
77
|
+
f"Unknown reference_id {reference_id!r}. Supported values: {choices}"
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _sha256_file(path: Path) -> str:
|
|
82
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_reference_by_id(reference_id: str) -> Path:
|
|
86
|
+
"""Resolve a manifest reference_id to a checksum-verified codon table path."""
|
|
87
|
+
entry = _reference_entry_by_id(reference_id)
|
|
88
|
+
codon_table_path = _resolve_packaged_or_repo_path(str(entry["codon_table_path"]))
|
|
89
|
+
expected = str(entry["checksum_sha256"])
|
|
90
|
+
if not codon_table_path.exists():
|
|
91
|
+
raise click.UsageError(
|
|
92
|
+
f"Codon table file for {reference_id} does not exist: {codon_table_path}"
|
|
93
|
+
)
|
|
94
|
+
actual = _sha256_file(codon_table_path)
|
|
95
|
+
if actual != expected:
|
|
96
|
+
raise click.UsageError(
|
|
97
|
+
"Checksum mismatch for "
|
|
98
|
+
f"{reference_id} at {codon_table_path}: expected {expected}, actual {actual}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if entry["tier"] != "production_enabled":
|
|
102
|
+
limitations = "; ".join(entry.get("known_limitations", []))
|
|
103
|
+
warning = (
|
|
104
|
+
f"Warning: reference_id={reference_id} has tier={entry['tier']}; "
|
|
105
|
+
f"{entry['claim_boundary']}"
|
|
106
|
+
)
|
|
107
|
+
if limitations:
|
|
108
|
+
warning = f"{warning} Known limitations: {limitations}"
|
|
109
|
+
click.echo(warning, err=True)
|
|
110
|
+
|
|
111
|
+
return codon_table_path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _validate_reference_host(reference_id: str, internal_host: str) -> None:
|
|
115
|
+
entry = _reference_entry_by_id(reference_id)
|
|
116
|
+
expected_taxid = HOST_TAXIDS[internal_host]
|
|
117
|
+
actual_taxid = int(entry["ncbi_taxid"])
|
|
118
|
+
if actual_taxid != expected_taxid:
|
|
119
|
+
raise click.UsageError(
|
|
120
|
+
f"reference_id={reference_id} targets {entry['organism']} "
|
|
121
|
+
f"(NCBI taxid {actual_taxid}) and is incompatible with "
|
|
122
|
+
f"--host {internal_host} (expected NCBI taxid {expected_taxid})."
|
|
123
|
+
)
|
|
20
124
|
|
|
21
125
|
|
|
22
126
|
def _configure_stdio() -> None:
|
|
@@ -43,7 +147,15 @@ def _wrap_sequence(sequence, width=80):
|
|
|
43
147
|
return "\n".join(sequence[i : i + width] for i in range(0, len(sequence), width))
|
|
44
148
|
|
|
45
149
|
|
|
46
|
-
def _build_dp_result(
|
|
150
|
+
def _build_dp_result(
|
|
151
|
+
sequence: str,
|
|
152
|
+
objective: str,
|
|
153
|
+
gc_min: float,
|
|
154
|
+
gc_max: float,
|
|
155
|
+
cai_target: float = DEFAULT_CAI_TARGET,
|
|
156
|
+
codon_table_path: Path | None = None,
|
|
157
|
+
codon_reference_id: str | None = None,
|
|
158
|
+
):
|
|
47
159
|
"""Run the constraint-based DP feasibility engine for a single protein sequence."""
|
|
48
160
|
if objective != "feasibility_best":
|
|
49
161
|
raise ValueError("DP engine currently supports --objective feasibility_best.")
|
|
@@ -53,12 +165,14 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
|
|
|
53
165
|
from factorforge.analysis.metrics import load_codon_usage_table
|
|
54
166
|
from factorforge.analysis.feasibility import analyze_feasibility
|
|
55
167
|
|
|
56
|
-
table = load_codon_usage_table()
|
|
168
|
+
table = load_codon_usage_table(path=codon_table_path)
|
|
57
169
|
result = analyze_feasibility(
|
|
58
170
|
sequence,
|
|
59
171
|
table.codon_weights,
|
|
172
|
+
target_cai=cai_target,
|
|
60
173
|
target_gc_low=gc_min,
|
|
61
174
|
target_gc_high=gc_max,
|
|
175
|
+
codon_reference_id=codon_reference_id,
|
|
62
176
|
)
|
|
63
177
|
best = result["target"]["best_candidate"]
|
|
64
178
|
feasible = best is not None
|
|
@@ -75,9 +189,17 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
|
|
|
75
189
|
return best, result, reason
|
|
76
190
|
|
|
77
191
|
|
|
78
|
-
def _format_dp_fasta(
|
|
192
|
+
def _format_dp_fasta(
|
|
193
|
+
sequence_id: str,
|
|
194
|
+
dna_sequence: str,
|
|
195
|
+
cai: float,
|
|
196
|
+
gc: float,
|
|
197
|
+
requested_cai_target: float | None = None,
|
|
198
|
+
) -> str:
|
|
79
199
|
"""Format a DP result as FASTA."""
|
|
80
200
|
header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
|
|
201
|
+
if requested_cai_target is not None:
|
|
202
|
+
header = f"{header}|target_cai={requested_cai_target:.3f}"
|
|
81
203
|
return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
|
|
82
204
|
|
|
83
205
|
|
|
@@ -102,10 +224,19 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
|
|
|
102
224
|
cai = float(result.metrics.get("cai", 0.0))
|
|
103
225
|
gc = float(result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0)))
|
|
104
226
|
score = float(result.metrics.get("score", 0.0))
|
|
105
|
-
header =
|
|
227
|
+
header = (
|
|
228
|
+
f">{sequence_id}|engine=profile|profile={profile}|"
|
|
229
|
+
f"cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
|
|
230
|
+
)
|
|
106
231
|
return f"{header}\n{_wrap_sequence(result.sequence)}\n"
|
|
107
232
|
|
|
108
233
|
|
|
234
|
+
def _single_profile_sequence_id(input_file: str, fasta_records) -> str:
|
|
235
|
+
if fasta_records is not None and len(fasta_records) == 1:
|
|
236
|
+
return str(fasta_records[0][0])
|
|
237
|
+
return Path(input_file).stem or "factorforge_profile"
|
|
238
|
+
|
|
239
|
+
|
|
109
240
|
def _format_profile_comparison_table(profile_results) -> str:
|
|
110
241
|
"""Format profile optimization metrics as a comparison table."""
|
|
111
242
|
divider = "─" * 45
|
|
@@ -166,9 +297,21 @@ def list_engines():
|
|
|
166
297
|
)
|
|
167
298
|
@click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
|
|
168
299
|
@click.option("--gc-max", type=float, default=47.0, help="Maximum target GC percentage")
|
|
300
|
+
@click.option(
|
|
301
|
+
"--cai-target",
|
|
302
|
+
type=float,
|
|
303
|
+
default=DEFAULT_CAI_TARGET,
|
|
304
|
+
help="Requested DP target CAI threshold",
|
|
305
|
+
)
|
|
169
306
|
@click.option("--template", "construct_template", help="Construct template name")
|
|
170
307
|
@click.option("--output", "-o", help="Output file")
|
|
171
308
|
@click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
|
|
309
|
+
@click.option(
|
|
310
|
+
"--reference-id",
|
|
311
|
+
type=click.Choice(REFERENCE_ID_CHOICES, case_sensitive=False),
|
|
312
|
+
default=None,
|
|
313
|
+
help="Expert/research codon-reference ID; checksum-validated.",
|
|
314
|
+
)
|
|
172
315
|
@click.option(
|
|
173
316
|
"--compare-profiles",
|
|
174
317
|
help=(
|
|
@@ -192,9 +335,11 @@ def optimize(
|
|
|
192
335
|
objective,
|
|
193
336
|
gc_min,
|
|
194
337
|
gc_max,
|
|
338
|
+
cai_target,
|
|
195
339
|
construct_template,
|
|
196
340
|
output,
|
|
197
341
|
output_format,
|
|
342
|
+
reference_id,
|
|
198
343
|
compare_profiles,
|
|
199
344
|
scan_mode,
|
|
200
345
|
scan_include,
|
|
@@ -206,6 +351,12 @@ def optimize(
|
|
|
206
351
|
host_value = host.lower()
|
|
207
352
|
internal_host = HOST_MAP[host_value]
|
|
208
353
|
host_was_explicit = _option_was_explicitly_set("host")
|
|
354
|
+
reference_id = reference_id.lower() if reference_id else None
|
|
355
|
+
reference_table_path = None
|
|
356
|
+
|
|
357
|
+
if reference_id is not None:
|
|
358
|
+
_validate_reference_host(reference_id, internal_host)
|
|
359
|
+
reference_table_path = resolve_reference_by_id(reference_id)
|
|
209
360
|
|
|
210
361
|
if host_was_explicit and engine == "dp" and _engine_option_was_explicitly_set():
|
|
211
362
|
raise click.UsageError("--host is only supported with --engine profile")
|
|
@@ -224,6 +375,9 @@ def optimize(
|
|
|
224
375
|
raise click.UsageError("--compare-profiles cannot be used with --engine dp.")
|
|
225
376
|
engine = "profile"
|
|
226
377
|
|
|
378
|
+
if reference_table_path is not None and construct_template:
|
|
379
|
+
raise click.UsageError("--reference-id is not supported with --template mode.")
|
|
380
|
+
|
|
227
381
|
try:
|
|
228
382
|
# Read file
|
|
229
383
|
with open(input_file, encoding="utf-8") as f:
|
|
@@ -247,7 +401,15 @@ def optimize(
|
|
|
247
401
|
if output_format.lower() != "fasta":
|
|
248
402
|
raise ValueError("Profile comparison only supports FASTA output.")
|
|
249
403
|
|
|
250
|
-
|
|
404
|
+
if reference_table_path is not None:
|
|
405
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
406
|
+
|
|
407
|
+
optimizer = RuleBasedOptimizer(
|
|
408
|
+
codon_table_path=str(reference_table_path),
|
|
409
|
+
generation_reference_id=reference_id,
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
optimizer = EngineRegistry.get("profile")
|
|
251
413
|
profile_results = []
|
|
252
414
|
for profile_name in compare_profile_list:
|
|
253
415
|
result = optimizer.optimize(
|
|
@@ -279,7 +441,15 @@ def optimize(
|
|
|
279
441
|
if output_format.lower() != "fasta":
|
|
280
442
|
raise ValueError("Multi-FASTA input only supports FASTA output.")
|
|
281
443
|
|
|
282
|
-
|
|
444
|
+
if reference_table_path is not None and engine == "profile":
|
|
445
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
446
|
+
|
|
447
|
+
optimizer = RuleBasedOptimizer(
|
|
448
|
+
codon_table_path=str(reference_table_path),
|
|
449
|
+
generation_reference_id=reference_id,
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
optimizer = EngineRegistry.get(engine)
|
|
283
453
|
payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
|
|
284
454
|
if hasattr(optimizer, "optimize_batch"):
|
|
285
455
|
results = optimizer.optimize_batch(
|
|
@@ -306,14 +476,9 @@ def optimize(
|
|
|
306
476
|
combined_fasta = []
|
|
307
477
|
for idx, result in enumerate(results):
|
|
308
478
|
seq_id = payload[idx]["id"]
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
score = result.metrics.get("score", 0.0)
|
|
312
|
-
header = (
|
|
313
|
-
f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
|
|
314
|
-
f"gc={float(gc):.2f}|score={float(score):.3f}"
|
|
479
|
+
combined_fasta.append(
|
|
480
|
+
_format_profile_fasta(seq_id, profile, result).rstrip()
|
|
315
481
|
)
|
|
316
|
-
combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
|
|
317
482
|
out_content = "\n".join(combined_fasta) + "\n"
|
|
318
483
|
|
|
319
484
|
if output:
|
|
@@ -336,12 +501,22 @@ def optimize(
|
|
|
336
501
|
objective=objective,
|
|
337
502
|
gc_min=gc_min,
|
|
338
503
|
gc_max=gc_max,
|
|
504
|
+
cai_target=cai_target,
|
|
505
|
+
codon_table_path=reference_table_path,
|
|
506
|
+
codon_reference_id=reference_id,
|
|
339
507
|
)
|
|
340
508
|
dna_sequence = best["dna_sequence"]
|
|
341
509
|
cai = float(best["cai"])
|
|
342
510
|
gc = float(best["gc"])
|
|
343
511
|
sequence_id = Path(input_file).stem or "factorforge_dp"
|
|
344
|
-
|
|
512
|
+
requested_cai_target = cai_target if _option_was_explicitly_set("cai_target") else None
|
|
513
|
+
fasta = _format_dp_fasta(
|
|
514
|
+
sequence_id,
|
|
515
|
+
dna_sequence,
|
|
516
|
+
cai,
|
|
517
|
+
gc,
|
|
518
|
+
requested_cai_target=requested_cai_target,
|
|
519
|
+
)
|
|
345
520
|
|
|
346
521
|
click.echo("Optimizing with DP feasibility engine...")
|
|
347
522
|
if output:
|
|
@@ -356,6 +531,8 @@ def optimize(
|
|
|
356
531
|
click.echo(f" - gc_percent: {gc:.2f}")
|
|
357
532
|
click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
|
|
358
533
|
click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
|
|
534
|
+
if requested_cai_target is not None:
|
|
535
|
+
click.echo(f" - target_cai: {float(feasibility['target']['cai']):.3f}")
|
|
359
536
|
click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
|
|
360
537
|
click.echo(f" - recommendation_reason: {recommendation_reason}")
|
|
361
538
|
return
|
|
@@ -393,7 +570,15 @@ def optimize(
|
|
|
393
570
|
raise ValueError("Non-FASTA output requires --template with profile pipeline.")
|
|
394
571
|
|
|
395
572
|
# Get engine
|
|
396
|
-
|
|
573
|
+
if reference_table_path is not None and engine == "profile":
|
|
574
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
575
|
+
|
|
576
|
+
optimizer = RuleBasedOptimizer(
|
|
577
|
+
codon_table_path=str(reference_table_path),
|
|
578
|
+
generation_reference_id=reference_id,
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
optimizer = EngineRegistry.get(engine)
|
|
397
582
|
|
|
398
583
|
# Optimize
|
|
399
584
|
click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
|
|
@@ -408,8 +593,10 @@ def optimize(
|
|
|
408
593
|
|
|
409
594
|
# Output results
|
|
410
595
|
if output:
|
|
596
|
+
sequence_id = _single_profile_sequence_id(input_file, fasta_records)
|
|
597
|
+
fasta = _format_profile_fasta(sequence_id, profile, result)
|
|
411
598
|
with open(output, "w", encoding="utf-8") as f:
|
|
412
|
-
f.write(
|
|
599
|
+
f.write(fasta)
|
|
413
600
|
click.echo(f"Saved to: {output}")
|
|
414
601
|
else:
|
|
415
602
|
click.echo(f"\n{result.sequence}\n")
|
{factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/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,
|