factorforge-cds 3.2.8__tar.gz → 3.3.1__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.2.8/src/factorforge_cds.egg-info → factorforge_cds-3.3.1}/PKG-INFO +4 -4
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/README.md +2 -2
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/pyproject.toml +3 -3
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/feasibility.py +8 -11
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/metrics.py +1 -1
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/main.py +187 -10
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +526 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +70 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +22 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +526 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +70 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +22 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_derived_codons.json +526 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +22 -0
- factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +70 -0
- factorforge_cds-3.3.1/src/factorforge/data/reference/reference_policy_manifest.json +262 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/optimizer.py +18 -2
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/pipeline.py +1 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/reverse_translator.py +7 -5
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/rule_engine.py +5 -5
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/scoring.py +111 -72
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/utils.py +7 -7
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/design_package.schema.json +11 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1/src/factorforge_cds.egg-info}/PKG-INFO +4 -4
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/SOURCES.txt +10 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_codon_table_metadata.py +3 -6
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_schema.py +15 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_registry_production_sync.py +13 -16
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_worked_example.py +2 -3
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/LICENSE +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/setup.cfg +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/optimizer.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/design_package.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_cai.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_database.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_report.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.
|
|
4
|
-
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review
|
|
3
|
+
Version: 3.3.1
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://factorforge.eijex.com
|
|
@@ -33,7 +33,7 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
# FactorForge
|
|
35
35
|
|
|
36
|
-
**Open-source constraint-based CDS design and pre-synthesis sequence review
|
|
36
|
+
**Open-source constraint-based CDS design and pre-synthesis sequence review for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
|
|
37
37
|
|
|
38
38
|
[](LICENSE)
|
|
39
39
|
[](https://www.python.org/)
|
|
@@ -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.3.1 (2026). Open-source constraint-based CDS design and sequence review.
|
|
97
97
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# FactorForge
|
|
2
2
|
|
|
3
|
-
**Open-source constraint-based CDS design and pre-synthesis sequence review
|
|
3
|
+
**Open-source constraint-based CDS design and pre-synthesis sequence review for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://www.python.org/)
|
|
@@ -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.3.1 (2026). Open-source constraint-based CDS design and sequence review.
|
|
64
64
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "factorforge-cds"
|
|
7
|
-
version = "3.
|
|
8
|
-
description = "FactorForge - open-source CDS design and pre-synthesis sequence review
|
|
7
|
+
version = "3.3.1"
|
|
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"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
@@ -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"]
|
|
@@ -14,19 +14,16 @@ from factorforge.analysis.metrics import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
# Defaults
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# Defaults for the current N. benthamiana software-default codon reference.
|
|
18
|
+
# DEFAULT_CAI_TARGET=0.82 remains a soft in-silico target aligned with
|
|
19
|
+
# industry practice (>0.8). DEFAULT_GC_LOW/HIGH track the NbeV1.1
|
|
20
|
+
# high-confidence CDS-derived native composition band. These are not wet-lab
|
|
21
|
+
# validation, expression/yield prediction, or biological-superiority claims.
|
|
20
22
|
# Exported as named constants so tests/test_registry_production_sync.py can
|
|
21
23
|
# strictly compare them against the registry (single source of truth).
|
|
22
|
-
#
|
|
23
|
-
# DEFAULT_GC_LOW/HIGH provisionally reverted from the Job 168/v3.3.0
|
|
24
|
-
# native-genome-composition anchor (40-47%, released as part of v3.2.7) back
|
|
25
|
-
# to the legacy engine-output-calibrated band, pending an MFE re-sensitivity +
|
|
26
|
-
# 2x2 factorial recheck. See scoring.py's GC_OPT_MIN/MAX comment.
|
|
27
24
|
DEFAULT_CAI_TARGET: float = 0.82
|
|
28
|
-
DEFAULT_GC_LOW: float =
|
|
29
|
-
DEFAULT_GC_HIGH: float =
|
|
25
|
+
DEFAULT_GC_LOW: float = 40.0
|
|
26
|
+
DEFAULT_GC_HIGH: float = 47.0
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
AA_TO_CODONS: dict[str, list[str]] = {}
|
|
@@ -121,7 +118,7 @@ def analyze_feasibility(
|
|
|
121
118
|
if not protein:
|
|
122
119
|
raise ValueError("protein_sequence must not be empty")
|
|
123
120
|
|
|
124
|
-
ranges = gc_ranges or [(
|
|
121
|
+
ranges = gc_ranges or [(40.0, 47.0), (40.0, 55.0), (40.0, 65.0)]
|
|
125
122
|
normalized_ranges = [
|
|
126
123
|
(_normalize_gc_bound(low), _normalize_gc_bound(high)) for low, high in ranges
|
|
127
124
|
]
|
|
@@ -98,7 +98,7 @@ class CodonUsageTable:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def _default_codon_table_path() -> Path:
|
|
101
|
-
#
|
|
101
|
+
# v3.3.0 reference-policy update: production default switched from the
|
|
102
102
|
# legacy Kazusa/SGN-derived table to the NbeV1.1 LAB-strain high-confidence
|
|
103
103
|
# derived table. See data/reference/active_codon_reference.json.
|
|
104
104
|
return get_data_path() / "profiles" / "nbev11_cds_hc_derived_codons.json"
|
|
@@ -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,14 @@ 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
|
+
):
|
|
47
158
|
"""Run the constraint-based DP feasibility engine for a single protein sequence."""
|
|
48
159
|
if objective != "feasibility_best":
|
|
49
160
|
raise ValueError("DP engine currently supports --objective feasibility_best.")
|
|
@@ -53,10 +164,11 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
|
|
|
53
164
|
from factorforge.analysis.metrics import load_codon_usage_table
|
|
54
165
|
from factorforge.analysis.feasibility import analyze_feasibility
|
|
55
166
|
|
|
56
|
-
table = load_codon_usage_table()
|
|
167
|
+
table = load_codon_usage_table(path=codon_table_path)
|
|
57
168
|
result = analyze_feasibility(
|
|
58
169
|
sequence,
|
|
59
170
|
table.codon_weights,
|
|
171
|
+
target_cai=cai_target,
|
|
60
172
|
target_gc_low=gc_min,
|
|
61
173
|
target_gc_high=gc_max,
|
|
62
174
|
)
|
|
@@ -75,9 +187,17 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
|
|
|
75
187
|
return best, result, reason
|
|
76
188
|
|
|
77
189
|
|
|
78
|
-
def _format_dp_fasta(
|
|
190
|
+
def _format_dp_fasta(
|
|
191
|
+
sequence_id: str,
|
|
192
|
+
dna_sequence: str,
|
|
193
|
+
cai: float,
|
|
194
|
+
gc: float,
|
|
195
|
+
requested_cai_target: float | None = None,
|
|
196
|
+
) -> str:
|
|
79
197
|
"""Format a DP result as FASTA."""
|
|
80
198
|
header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
|
|
199
|
+
if requested_cai_target is not None:
|
|
200
|
+
header = f"{header}|target_cai={requested_cai_target:.3f}"
|
|
81
201
|
return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
|
|
82
202
|
|
|
83
203
|
|
|
@@ -106,6 +226,12 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
|
|
|
106
226
|
return f"{header}\n{_wrap_sequence(result.sequence)}\n"
|
|
107
227
|
|
|
108
228
|
|
|
229
|
+
def _single_profile_sequence_id(input_file: str, fasta_records) -> str:
|
|
230
|
+
if fasta_records is not None and len(fasta_records) == 1:
|
|
231
|
+
return str(fasta_records[0][0])
|
|
232
|
+
return Path(input_file).stem or "factorforge_profile"
|
|
233
|
+
|
|
234
|
+
|
|
109
235
|
def _format_profile_comparison_table(profile_results) -> str:
|
|
110
236
|
"""Format profile optimization metrics as a comparison table."""
|
|
111
237
|
divider = "─" * 45
|
|
@@ -164,11 +290,23 @@ def list_engines():
|
|
|
164
290
|
type=click.Choice(["feasibility_best"], case_sensitive=False),
|
|
165
291
|
help="DP objective",
|
|
166
292
|
)
|
|
167
|
-
@click.option("--gc-min", type=float, default=
|
|
168
|
-
@click.option("--gc-max", type=float, default=
|
|
293
|
+
@click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
|
|
294
|
+
@click.option("--gc-max", type=float, default=47.0, help="Maximum target GC percentage")
|
|
295
|
+
@click.option(
|
|
296
|
+
"--cai-target",
|
|
297
|
+
type=float,
|
|
298
|
+
default=DEFAULT_CAI_TARGET,
|
|
299
|
+
help="Requested DP target CAI threshold",
|
|
300
|
+
)
|
|
169
301
|
@click.option("--template", "construct_template", help="Construct template name")
|
|
170
302
|
@click.option("--output", "-o", help="Output file")
|
|
171
303
|
@click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
|
|
304
|
+
@click.option(
|
|
305
|
+
"--reference-id",
|
|
306
|
+
type=click.Choice(REFERENCE_ID_CHOICES, case_sensitive=False),
|
|
307
|
+
default=None,
|
|
308
|
+
help="Expert/research codon-reference ID; checksum-validated.",
|
|
309
|
+
)
|
|
172
310
|
@click.option(
|
|
173
311
|
"--compare-profiles",
|
|
174
312
|
help=(
|
|
@@ -192,9 +330,11 @@ def optimize(
|
|
|
192
330
|
objective,
|
|
193
331
|
gc_min,
|
|
194
332
|
gc_max,
|
|
333
|
+
cai_target,
|
|
195
334
|
construct_template,
|
|
196
335
|
output,
|
|
197
336
|
output_format,
|
|
337
|
+
reference_id,
|
|
198
338
|
compare_profiles,
|
|
199
339
|
scan_mode,
|
|
200
340
|
scan_include,
|
|
@@ -206,6 +346,12 @@ def optimize(
|
|
|
206
346
|
host_value = host.lower()
|
|
207
347
|
internal_host = HOST_MAP[host_value]
|
|
208
348
|
host_was_explicit = _option_was_explicitly_set("host")
|
|
349
|
+
reference_id = reference_id.lower() if reference_id else None
|
|
350
|
+
reference_table_path = None
|
|
351
|
+
|
|
352
|
+
if reference_id is not None:
|
|
353
|
+
_validate_reference_host(reference_id, internal_host)
|
|
354
|
+
reference_table_path = resolve_reference_by_id(reference_id)
|
|
209
355
|
|
|
210
356
|
if host_was_explicit and engine == "dp" and _engine_option_was_explicitly_set():
|
|
211
357
|
raise click.UsageError("--host is only supported with --engine profile")
|
|
@@ -224,6 +370,9 @@ def optimize(
|
|
|
224
370
|
raise click.UsageError("--compare-profiles cannot be used with --engine dp.")
|
|
225
371
|
engine = "profile"
|
|
226
372
|
|
|
373
|
+
if reference_table_path is not None and construct_template:
|
|
374
|
+
raise click.UsageError("--reference-id is not supported with --template mode.")
|
|
375
|
+
|
|
227
376
|
try:
|
|
228
377
|
# Read file
|
|
229
378
|
with open(input_file, encoding="utf-8") as f:
|
|
@@ -247,7 +396,12 @@ def optimize(
|
|
|
247
396
|
if output_format.lower() != "fasta":
|
|
248
397
|
raise ValueError("Profile comparison only supports FASTA output.")
|
|
249
398
|
|
|
250
|
-
|
|
399
|
+
if reference_table_path is not None:
|
|
400
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
401
|
+
|
|
402
|
+
optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
|
|
403
|
+
else:
|
|
404
|
+
optimizer = EngineRegistry.get("profile")
|
|
251
405
|
profile_results = []
|
|
252
406
|
for profile_name in compare_profile_list:
|
|
253
407
|
result = optimizer.optimize(
|
|
@@ -279,7 +433,12 @@ def optimize(
|
|
|
279
433
|
if output_format.lower() != "fasta":
|
|
280
434
|
raise ValueError("Multi-FASTA input only supports FASTA output.")
|
|
281
435
|
|
|
282
|
-
|
|
436
|
+
if reference_table_path is not None and engine == "profile":
|
|
437
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
438
|
+
|
|
439
|
+
optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
|
|
440
|
+
else:
|
|
441
|
+
optimizer = EngineRegistry.get(engine)
|
|
283
442
|
payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
|
|
284
443
|
if hasattr(optimizer, "optimize_batch"):
|
|
285
444
|
results = optimizer.optimize_batch(
|
|
@@ -336,12 +495,21 @@ def optimize(
|
|
|
336
495
|
objective=objective,
|
|
337
496
|
gc_min=gc_min,
|
|
338
497
|
gc_max=gc_max,
|
|
498
|
+
cai_target=cai_target,
|
|
499
|
+
codon_table_path=reference_table_path,
|
|
339
500
|
)
|
|
340
501
|
dna_sequence = best["dna_sequence"]
|
|
341
502
|
cai = float(best["cai"])
|
|
342
503
|
gc = float(best["gc"])
|
|
343
504
|
sequence_id = Path(input_file).stem or "factorforge_dp"
|
|
344
|
-
|
|
505
|
+
requested_cai_target = cai_target if _option_was_explicitly_set("cai_target") else None
|
|
506
|
+
fasta = _format_dp_fasta(
|
|
507
|
+
sequence_id,
|
|
508
|
+
dna_sequence,
|
|
509
|
+
cai,
|
|
510
|
+
gc,
|
|
511
|
+
requested_cai_target=requested_cai_target,
|
|
512
|
+
)
|
|
345
513
|
|
|
346
514
|
click.echo("Optimizing with DP feasibility engine...")
|
|
347
515
|
if output:
|
|
@@ -356,6 +524,8 @@ def optimize(
|
|
|
356
524
|
click.echo(f" - gc_percent: {gc:.2f}")
|
|
357
525
|
click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
|
|
358
526
|
click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
|
|
527
|
+
if requested_cai_target is not None:
|
|
528
|
+
click.echo(f" - target_cai: {float(feasibility['target']['cai']):.3f}")
|
|
359
529
|
click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
|
|
360
530
|
click.echo(f" - recommendation_reason: {recommendation_reason}")
|
|
361
531
|
return
|
|
@@ -393,7 +563,12 @@ def optimize(
|
|
|
393
563
|
raise ValueError("Non-FASTA output requires --template with profile pipeline.")
|
|
394
564
|
|
|
395
565
|
# Get engine
|
|
396
|
-
|
|
566
|
+
if reference_table_path is not None and engine == "profile":
|
|
567
|
+
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
568
|
+
|
|
569
|
+
optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
|
|
570
|
+
else:
|
|
571
|
+
optimizer = EngineRegistry.get(engine)
|
|
397
572
|
|
|
398
573
|
# Optimize
|
|
399
574
|
click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
|
|
@@ -408,8 +583,10 @@ def optimize(
|
|
|
408
583
|
|
|
409
584
|
# Output results
|
|
410
585
|
if output:
|
|
586
|
+
sequence_id = _single_profile_sequence_id(input_file, fasta_records)
|
|
587
|
+
fasta = _format_profile_fasta(sequence_id, profile, result)
|
|
411
588
|
with open(output, "w", encoding="utf-8") as f:
|
|
412
|
-
f.write(
|
|
589
|
+
f.write(fasta)
|
|
413
590
|
click.echo(f"Saved to: {output}")
|
|
414
591
|
else:
|
|
415
592
|
click.echo(f"\n{result.sequence}\n")
|