factorforge-cds 3.2.6__tar.gz → 3.2.7__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.6 → factorforge_cds-3.2.7}/PKG-INFO +2 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/README.md +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/pyproject.toml +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/analysis/feasibility.py +14 -6
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/analysis/metrics.py +4 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/cli/main.py +2 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/optimizer.py +7 -3
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/pipeline.py +3 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/reverse_translator.py +27 -12
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/rule_engine.py +6 -4
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/scoring.py +108 -19
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/utils.py +21 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_benchmark_codon_table_metadata.py +32 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_registry_production_sync.py +56 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_worked_example.py +28 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/LICENSE +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/setup.cfg +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/optimizer.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/schemas/design_package.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/schemas/design_package.schema.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/SOURCES.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_cai.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_database.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_design_package_schema.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_validation_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.7
|
|
4
4
|
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review engine 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.2.
|
|
96
|
+
FactorForge v3.2.7 (2026). Open-source constraint-based CDS design engine.
|
|
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.2.
|
|
63
|
+
FactorForge v3.2.7 (2026). Open-source constraint-based CDS design engine.
|
|
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.2.
|
|
7
|
+
version = "3.2.7"
|
|
8
8
|
description = "FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-only"
|
|
@@ -14,14 +14,18 @@ from factorforge.analysis.metrics import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
# (internal benchmark, n=49
|
|
19
|
-
#
|
|
17
|
+
# DEFAULT_CAI_TARGET=0.82 aligns with industry practice (>0.8) and is achievable
|
|
18
|
+
# (internal benchmark, n=49, avg CAI=0.76).
|
|
19
|
+
# DEFAULT_GC_LOW/HIGH = native genome-composition anchor for N. benthamiana CDS
|
|
20
|
+
# (_analysis/025 STEP 2: 004 endogenous n=10 measured range 40-47%, cross-checked
|
|
21
|
+
# against nbev11_cds_hc/all and qld183_v103 derived-asset GC ~42.8-43.1% and
|
|
22
|
+
# external ground truth ~44%). NOT an empirically validated expression optimum —
|
|
23
|
+
# this is a composition anchor, not a target to maximize toward.
|
|
20
24
|
# Exported as named constants so tests/test_registry_production_sync.py can
|
|
21
25
|
# strictly compare them against the registry (single source of truth).
|
|
22
26
|
DEFAULT_CAI_TARGET: float = 0.82
|
|
23
|
-
DEFAULT_GC_LOW: float =
|
|
24
|
-
DEFAULT_GC_HIGH: float =
|
|
27
|
+
DEFAULT_GC_LOW: float = 40.0
|
|
28
|
+
DEFAULT_GC_HIGH: float = 47.0
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
AA_TO_CODONS: dict[str, list[str]] = {}
|
|
@@ -116,7 +120,11 @@ def analyze_feasibility(
|
|
|
116
120
|
if not protein:
|
|
117
121
|
raise ValueError("protein_sequence must not be empty")
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
# Default exploration ranges: genome-grounded native anchor first, then
|
|
124
|
+
# progressively wider windows. (55.0, 65.0) is retained ONLY as an explicit
|
|
125
|
+
# non-native/high-GC option (matches legacy engine-output-calibrated band) —
|
|
126
|
+
# callers must opt in explicitly to it, it is not the production default.
|
|
127
|
+
ranges = gc_ranges or [(40.0, 47.0), (35.0, 50.0), (55.0, 65.0)]
|
|
120
128
|
normalized_ranges = [
|
|
121
129
|
(_normalize_gc_bound(low), _normalize_gc_bound(high)) for low, high in ranges
|
|
122
130
|
]
|
|
@@ -98,7 +98,10 @@ class CodonUsageTable:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def _default_codon_table_path() -> Path:
|
|
101
|
-
|
|
101
|
+
# Job 168 / v3.3.0 (_analysis/025): production default switched from the
|
|
102
|
+
# legacy Kazusa/SGN-derived table to the NbeV1.1 LAB-strain high-confidence
|
|
103
|
+
# derived table. See data/reference/active_codon_reference.json.
|
|
104
|
+
return get_data_path() / "profiles" / "nbev11_cds_hc_derived_codons.json"
|
|
102
105
|
|
|
103
106
|
|
|
104
107
|
def load_codon_usage_table(path: Path | None = None) -> CodonUsageTable:
|
|
@@ -164,8 +164,8 @@ def list_engines():
|
|
|
164
164
|
type=click.Choice(["feasibility_best"], case_sensitive=False),
|
|
165
165
|
help="DP objective",
|
|
166
166
|
)
|
|
167
|
-
@click.option("--gc-min", type=float, default=
|
|
168
|
-
@click.option("--gc-max", type=float, default=
|
|
167
|
+
@click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
|
|
168
|
+
@click.option("--gc-max", type=float, default=47.0, help="Maximum target GC percentage")
|
|
169
169
|
@click.option("--template", "construct_template", help="Construct template name")
|
|
170
170
|
@click.option("--output", "-o", help="Output file")
|
|
171
171
|
@click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/optimizer.py
RENAMED
|
@@ -20,7 +20,7 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
20
20
|
"""Profile-based rule optimization engine."""
|
|
21
21
|
|
|
22
22
|
name = "Profile-based"
|
|
23
|
-
version = "3.2.
|
|
23
|
+
version = "3.2.7"
|
|
24
24
|
|
|
25
25
|
def __init__(self, codon_table_path: str | None = None) -> None:
|
|
26
26
|
"""
|
|
@@ -122,12 +122,16 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
122
122
|
cai = translator.calculate_cai(optimized_dna)
|
|
123
123
|
gc = translator.calculate_gc_content(optimized_dna)
|
|
124
124
|
score = calculate_composite_score(
|
|
125
|
-
cai=cai, gc=gc, sequence=optimized_dna, profile=profile_value
|
|
125
|
+
cai=cai, gc=gc, sequence=optimized_dna, profile=profile_value, host=host
|
|
126
126
|
)
|
|
127
127
|
candidates = [{"sequence": optimized_dna, "cai": cai, "gc": gc, "score": score}]
|
|
128
128
|
else:
|
|
129
|
+
translate_kwargs = {
|
|
130
|
+
k: v for k, v in kwargs.items()
|
|
131
|
+
if k not in ("scan_mode", "scan_include", "scan_exclude")
|
|
132
|
+
}
|
|
129
133
|
candidates = translator.generate_candidates(
|
|
130
|
-
processed_seq, profile=opt_profile, n=1, seed=seed
|
|
134
|
+
processed_seq, profile=opt_profile, n=1, seed=seed, **translate_kwargs
|
|
131
135
|
)
|
|
132
136
|
if not candidates:
|
|
133
137
|
raise ValueError("No candidates generated for input sequence.")
|
|
@@ -192,7 +192,8 @@ class OptimizationPipeline:
|
|
|
192
192
|
cai = translator.calculate_cai(optimized_dna)
|
|
193
193
|
gc = translator.calculate_gc_content(optimized_dna)
|
|
194
194
|
score = calculate_composite_score(
|
|
195
|
-
cai=cai, gc=gc, sequence=optimized_dna, profile=effective_profile
|
|
195
|
+
cai=cai, gc=gc, sequence=optimized_dna, profile=effective_profile,
|
|
196
|
+
host=effective_host,
|
|
196
197
|
)
|
|
197
198
|
candidate_metrics = {"cai": cai, "gc": gc, "score": score}
|
|
198
199
|
else:
|
|
@@ -244,6 +245,7 @@ class OptimizationPipeline:
|
|
|
244
245
|
gc=candidate_metrics["gc"],
|
|
245
246
|
sequence=optimized_dna,
|
|
246
247
|
profile=effective_profile,
|
|
248
|
+
host=effective_host,
|
|
247
249
|
)
|
|
248
250
|
logger.info(
|
|
249
251
|
f"Dinucleotide reduction [{dinu_fix['mode']}]: "
|
|
@@ -15,12 +15,16 @@ from enum import Enum
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, cast
|
|
17
17
|
|
|
18
|
-
from factorforge.engines.profile.scoring import
|
|
18
|
+
from factorforge.engines.profile.scoring import (
|
|
19
|
+
calculate_composite_score,
|
|
20
|
+
resolve_host_gc_range,
|
|
21
|
+
)
|
|
19
22
|
from factorforge.engines.profile.utils import (
|
|
20
23
|
build_aa_to_codons_map,
|
|
21
24
|
calculate_gc,
|
|
22
25
|
get_data_path,
|
|
23
26
|
load_golden_set,
|
|
27
|
+
resolve_host_codon_table_path,
|
|
24
28
|
)
|
|
25
29
|
from factorforge.utils.exceptions import EmptyCandidateError
|
|
26
30
|
|
|
@@ -66,7 +70,7 @@ class ReverseTranslator:
|
|
|
66
70
|
if codon_table_path is None:
|
|
67
71
|
# Use centralized data path management
|
|
68
72
|
data_dir = get_data_path()
|
|
69
|
-
codon_table_path = data_dir
|
|
73
|
+
codon_table_path = resolve_host_codon_table_path(host, data_dir)
|
|
70
74
|
|
|
71
75
|
self.codon_table: dict[str, Any] = self._load_codon_table(codon_table_path)
|
|
72
76
|
self.aa_to_codons: dict[str, list[tuple[str, float]]] = self._build_aa_to_codons_map()
|
|
@@ -370,10 +374,15 @@ class ReverseTranslator:
|
|
|
370
374
|
Balanced profile: CAI first, GC balanced
|
|
371
375
|
|
|
372
376
|
- Preferred codon ratio: 70%
|
|
373
|
-
- Target GC:
|
|
377
|
+
- Target GC: host composition band (nbenthamiana: 40-47%, native
|
|
378
|
+
genome-composition anchor; see _analysis/025 STEP 2 and
|
|
379
|
+
engines/profile/scoring.py GC_OPT_MIN/MAX. Not an empirically
|
|
380
|
+
validated expression optimum. Other hosts: resolve_host_gc_range()
|
|
381
|
+
default, unchanged from pre-v3.3.0 behavior.)
|
|
374
382
|
"""
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
_host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
|
|
384
|
+
target_gc_min = kwargs.get("target_gc_min", _host_gc_min)
|
|
385
|
+
target_gc_max = kwargs.get("target_gc_max", _host_gc_max)
|
|
377
386
|
preferred_ratio = kwargs.get("preferred_ratio", 0.7)
|
|
378
387
|
max_attempts = kwargs.get("max_gc_attempts", 10)
|
|
379
388
|
if max_attempts < 1:
|
|
@@ -443,19 +452,19 @@ class ReverseTranslator:
|
|
|
443
452
|
"""GC-Target profile: drive global GC toward a configurable target.
|
|
444
453
|
|
|
445
454
|
Targets the caller-supplied ``target_gc`` if provided, otherwise the
|
|
446
|
-
host
|
|
447
|
-
|
|
455
|
+
active host's composition midpoint (resolve_host_gc_range(self.host);
|
|
456
|
+
43.5% for nbenthamiana since v3.3.0 / _analysis/025, 60.0% for other
|
|
457
|
+
hosts pending their own host-specific analysis). To target a different
|
|
458
|
+
GC (e.g. for specific vector requirements), pass target_gc explicitly.
|
|
448
459
|
|
|
449
460
|
- GC constraint first
|
|
450
461
|
- CAI may be sacrificed
|
|
451
462
|
- Balance local window GC (50 bp)
|
|
452
|
-
|
|
453
|
-
TODO: GC_OPT_MID is currently a single N. benthamiana-calibrated constant.
|
|
454
|
-
When per-host GC profiles are added, source the default from the active host.
|
|
455
463
|
"""
|
|
456
464
|
target_gc = kwargs.get("target_gc")
|
|
457
465
|
if target_gc is None:
|
|
458
|
-
|
|
466
|
+
_host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
|
|
467
|
+
target_gc = (_host_gc_min + _host_gc_max) / 2
|
|
459
468
|
|
|
460
469
|
dna_seq: list[str] = []
|
|
461
470
|
|
|
@@ -507,9 +516,14 @@ class ReverseTranslator:
|
|
|
507
516
|
raise ValueError("max_attempts must be >= 1")
|
|
508
517
|
last_seq = ""
|
|
509
518
|
|
|
519
|
+
balanced_kwargs = {
|
|
520
|
+
k: v for k, v in kwargs.items() if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
|
|
521
|
+
}
|
|
522
|
+
balanced_kwargs["preferred_ratio"] = 0.6
|
|
523
|
+
|
|
510
524
|
for attempt in range(max_attempts):
|
|
511
525
|
# Start with Balanced strategy
|
|
512
|
-
dna_seq = self._balanced_translate(protein_seq,
|
|
526
|
+
dna_seq = self._balanced_translate(protein_seq, **balanced_kwargs)
|
|
513
527
|
last_seq = dna_seq
|
|
514
528
|
|
|
515
529
|
# Check restriction sites (forward + reverse complement)
|
|
@@ -710,6 +724,7 @@ class ReverseTranslator:
|
|
|
710
724
|
gc=gc,
|
|
711
725
|
sequence=dna_seq,
|
|
712
726
|
profile=profile.value,
|
|
727
|
+
host=self.host,
|
|
713
728
|
**kwargs,
|
|
714
729
|
)
|
|
715
730
|
return {"sequence": dna_seq, "cai": cai, "gc": gc, "score": score}
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/rule_engine.py
RENAMED
|
@@ -358,10 +358,12 @@ class RuleEngine:
|
|
|
358
358
|
|
|
359
359
|
This is a LOCAL synthesis/extreme-window guard (default 25-75% over a
|
|
360
360
|
50 bp window), NOT the global GC target. Global GC is governed separately
|
|
361
|
-
by the scoring band (GC_OPT_MIN/MAX,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
by the scoring band (GC_OPT_MIN/MAX, native genome-composition anchor
|
|
362
|
+
~40-47%, see _analysis/025 STEP 2) and the API/DP gc_min/gc_max
|
|
363
|
+
constraints. The wide 25-75% local band is an independent synthesis/
|
|
364
|
+
hairpin-risk guard (registry `gc_extreme_local_window`), not derived from
|
|
365
|
+
or coupled to the global GC target — narrowing it is a separate decision
|
|
366
|
+
from the global band retarget and is out of scope here.
|
|
365
367
|
|
|
366
368
|
Args:
|
|
367
369
|
seq: DNA sequence
|
|
@@ -12,16 +12,43 @@ from typing import Any
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
# GC band for N. benthamiana codon-optimized sequences.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
# Native genome-composition anchor (_analysis/025 STEP 2: 004 endogenous CDS
|
|
16
|
+
# n=10 measured range 40-47%; cross-checked against nbev11_cds_hc/all and
|
|
17
|
+
# qld183_v103 derived-asset GC ~42.8-43.1% and external ground truth ~44%).
|
|
18
|
+
# NOT an empirically validated expression optimum — this is a composition
|
|
19
|
+
# anchor, not a target to maximize toward. Sequences within [GC_OPT_MIN,
|
|
20
|
+
# GC_OPT_MAX] receive full GC score; outside the band the score decays linearly.
|
|
21
|
+
GC_OPT_MIN = 40.0
|
|
22
|
+
GC_OPT_MAX = 47.0
|
|
23
|
+
GC_OPT_MID = 43.5 # kept for gc_target point-scoring and viral_delivery centering
|
|
23
24
|
GC_DECAY_WIDTH = 20.0 # percentage points outside band before score reaches 0.0
|
|
24
25
|
|
|
26
|
+
# Job 168 / v3.3.0 (_analysis/025) scoped the genome-composition re-derivation
|
|
27
|
+
# to N. benthamiana only. Other hosts (e.g. ntabacum/BY-2) keep the pre-v3.3.0
|
|
28
|
+
# global default (55-65%, internal benchmark n=49 avg GC=60.1%) until they get
|
|
29
|
+
# their own host-specific genome-composition analysis — they must NOT silently
|
|
30
|
+
# inherit GC_OPT_MIN/MAX, which is an N.-benthamiana-specific anchor.
|
|
31
|
+
GC_RANGE_DEFAULT: tuple[float, float] = (55.0, 65.0)
|
|
32
|
+
GC_RANGES_BY_HOST: dict[str, tuple[float, float]] = {
|
|
33
|
+
"nbenthamiana": (GC_OPT_MIN, GC_OPT_MAX),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_host_gc_range(host: str | None) -> tuple[float, float]:
|
|
38
|
+
"""Resolve the (gc_min, gc_max) composition band for a host.
|
|
39
|
+
|
|
40
|
+
nbenthamiana uses the _analysis/025 native genome-composition anchor.
|
|
41
|
+
Any other host (including unknown ones) keeps the pre-v3.3.0 global
|
|
42
|
+
default band until it gets its own host-specific analysis.
|
|
43
|
+
"""
|
|
44
|
+
return GC_RANGES_BY_HOST.get(host or "nbenthamiana", GC_RANGE_DEFAULT)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_host_gc_mid(host: str | None) -> float:
|
|
48
|
+
"""Resolve the GC band midpoint for a host (see resolve_host_gc_range)."""
|
|
49
|
+
gc_min, gc_max = resolve_host_gc_range(host)
|
|
50
|
+
return (gc_min + gc_max) / 2
|
|
51
|
+
|
|
25
52
|
# ViennaRNA availability cache
|
|
26
53
|
_vienna_available: bool | None = None
|
|
27
54
|
|
|
@@ -45,7 +72,12 @@ class ScoringConfig:
|
|
|
45
72
|
use_mfe: bool = True
|
|
46
73
|
|
|
47
74
|
def __post_init__(self) -> None:
|
|
48
|
-
"""
|
|
75
|
+
"""Validate inputs, then normalize weights to sum to 1.0."""
|
|
76
|
+
for field_name in ("w_cai", "w_gc", "w_mfe", "w_dinuc", "w_syncodonlm"):
|
|
77
|
+
if getattr(self, field_name) < 0:
|
|
78
|
+
raise ValueError(f"{field_name} must be >= 0, got {getattr(self, field_name)}")
|
|
79
|
+
if self.gc_min > self.gc_max:
|
|
80
|
+
raise ValueError(f"gc_min ({self.gc_min}) must be <= gc_max ({self.gc_max})")
|
|
49
81
|
self._normalize()
|
|
50
82
|
|
|
51
83
|
def _normalize(self) -> None:
|
|
@@ -110,6 +142,20 @@ def _check_vienna_available() -> bool:
|
|
|
110
142
|
return _vienna_available
|
|
111
143
|
|
|
112
144
|
|
|
145
|
+
# 170-fix: ViennaRNA's RNA.fold() uses Zuker's MFE algorithm, O(n^3) time /
|
|
146
|
+
# O(n^2) memory — there was previously no length guard anywhere in the
|
|
147
|
+
# calculate_mfe() call chain, so a single request at/under the existing
|
|
148
|
+
# public API length limits (5000aa/15000bp) could pin a CPU core for minutes
|
|
149
|
+
# (algorithmic-complexity DoS, CWE-407; confirmed via faulthandler stack
|
|
150
|
+
# traces + an isolated RNA.fold() timing curve: 1000nt ~2.2s, 2000nt ~9.8s,
|
|
151
|
+
# 3000nt ~24.6s). calculate_mfe() is called twice per optimize() call
|
|
152
|
+
# (once for scoring, once independently for compute_mfe_evidence()
|
|
153
|
+
# provenance), so the real per-request cost is ~2x this curve. 1000nt
|
|
154
|
+
# (~333aa) keeps worst-case cost to roughly 4-5s even with that doubling,
|
|
155
|
+
# while still covering most realistic single-protein CDS design requests.
|
|
156
|
+
MFE_MAX_SEQUENCE_LENGTH = 1000
|
|
157
|
+
|
|
158
|
+
|
|
113
159
|
def calculate_mfe(sequence: str) -> float | None:
|
|
114
160
|
"""
|
|
115
161
|
Calculate minimum free energy (MFE) using ViennaRNA.
|
|
@@ -118,11 +164,22 @@ def calculate_mfe(sequence: str) -> float | None:
|
|
|
118
164
|
sequence: DNA or RNA sequence.
|
|
119
165
|
|
|
120
166
|
Returns:
|
|
121
|
-
MFE in kcal/mol, or None if ViennaRNA is not available
|
|
167
|
+
MFE in kcal/mol, or None if ViennaRNA is not available or the
|
|
168
|
+
sequence exceeds MFE_MAX_SEQUENCE_LENGTH.
|
|
122
169
|
"""
|
|
123
170
|
if not _check_vienna_available():
|
|
124
171
|
return None
|
|
125
172
|
|
|
173
|
+
if len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Sequence length (%d nt) exceeds MFE_MAX_SEQUENCE_LENGTH (%d nt); "
|
|
176
|
+
"skipping global MFE calculation to avoid an unbounded ViennaRNA "
|
|
177
|
+
"RNA.fold() runtime (O(n^3)). MFE scoring falls back to a neutral, "
|
|
178
|
+
"zero-weighted contribution for this candidate (170-fix).",
|
|
179
|
+
len(sequence), MFE_MAX_SEQUENCE_LENGTH,
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
182
|
+
|
|
126
183
|
try:
|
|
127
184
|
import RNA
|
|
128
185
|
|
|
@@ -139,24 +196,34 @@ def normalize_mfe(mfe: float, seq_length: int) -> float:
|
|
|
139
196
|
"""
|
|
140
197
|
Normalize MFE to 0-1 range where 1 = no structure (favorable).
|
|
141
198
|
|
|
142
|
-
|
|
143
|
-
-
|
|
199
|
+
Clamp range calibrated empirically in analysis 011
|
|
200
|
+
(eijex-workspace/_analysis/2026-06-26/011-mfe-clamp-calibration):
|
|
201
|
+
measured MFE/nt across 135 FactorForge outputs (N. benthamiana +
|
|
202
|
+
BY-2 hosts, 5 profiles) ranged -0.4064 to -0.1338 (combined 5th/95th
|
|
203
|
+
percentile -0.3839/-0.1760). The range below widens that empirical
|
|
204
|
+
percentile by a margin on both sides to avoid saturating ~10% of
|
|
205
|
+
future sequences at the score floor/ceiling.
|
|
144
206
|
|
|
145
207
|
Args:
|
|
146
208
|
mfe: Minimum free energy in kcal/mol.
|
|
147
209
|
seq_length: Sequence length in nucleotides.
|
|
148
210
|
|
|
149
211
|
Returns:
|
|
150
|
-
Normalized MFE score (0-1
|
|
212
|
+
Normalized MFE score (0-1) under this computational normalization,
|
|
213
|
+
where a higher value represents a less negative whole-CDS MFE/nt.
|
|
214
|
+
This is a Tier-0 computational heuristic only (see
|
|
215
|
+
eijex-validationHub/docs/CLAIM_EVIDENCE_BENCHMARK_MODEL.md §4) — no
|
|
216
|
+
biological interpretation (translation efficiency, mRNA stability,
|
|
217
|
+
or expression outcome) is implied or validated.
|
|
151
218
|
"""
|
|
152
219
|
if seq_length == 0:
|
|
153
220
|
return 0.5
|
|
154
221
|
|
|
155
222
|
mfe_per_nt = mfe / seq_length
|
|
156
|
-
# Clamp to
|
|
157
|
-
clamped = max(-0.
|
|
158
|
-
# Map to [0, 1] where 0.
|
|
159
|
-
return
|
|
223
|
+
# Clamp to empirically calibrated range [-0.40, -0.15] (analysis 011)
|
|
224
|
+
clamped = max(-0.40, min(-0.15, mfe_per_nt))
|
|
225
|
+
# Map to [0, 1] where -0.15 kcal/mol/nt → 1.0 and -0.40 → 0.0
|
|
226
|
+
return (clamped + 0.40) / 0.25
|
|
160
227
|
|
|
161
228
|
|
|
162
229
|
def gc_band_score(
|
|
@@ -210,6 +277,7 @@ def calculate_composite_score(
|
|
|
210
277
|
sequence: str | None = None,
|
|
211
278
|
config: ScoringConfig | None = None,
|
|
212
279
|
profile: str | None = None,
|
|
280
|
+
host: str | None = None,
|
|
213
281
|
**kwargs: Any,
|
|
214
282
|
) -> float:
|
|
215
283
|
"""Calculate multidimensional composite score.
|
|
@@ -238,7 +306,12 @@ def calculate_composite_score(
|
|
|
238
306
|
profile_name = (profile or "balanced").lower()
|
|
239
307
|
config = PROFILE_SCORING_CONFIGS.get(profile_name)
|
|
240
308
|
if config is None:
|
|
241
|
-
|
|
309
|
+
supported = ", ".join(sorted(PROFILE_SCORING_CONFIGS))
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Unknown profile: {profile_name}. Supported profiles: {supported}"
|
|
312
|
+
)
|
|
313
|
+
if "target_gc" in kwargs and profile_name != "gc_target":
|
|
314
|
+
raise ValueError("target_gc is only valid for the gc_target profile")
|
|
242
315
|
|
|
243
316
|
# Component 1: CAI (already 0-1)
|
|
244
317
|
cai_score = max(0.0, min(1.0, cai))
|
|
@@ -250,7 +323,15 @@ def calculate_composite_score(
|
|
|
250
323
|
tgt = float(kwargs["target_gc"])
|
|
251
324
|
gc_score = gc_band_score(gc, tgt - 5.0, tgt + 5.0, config.gc_decay_width)
|
|
252
325
|
else:
|
|
253
|
-
|
|
326
|
+
gc_min, gc_max = config.gc_min, config.gc_max
|
|
327
|
+
# Profiles that haven't customized the band (still on the
|
|
328
|
+
# GC_OPT_MIN/MAX default) follow the active host's composition band.
|
|
329
|
+
# Profiles with an explicit band override (e.g. viral_delivery) keep
|
|
330
|
+
# it regardless of host — that override is profile-specific, not a
|
|
331
|
+
# host default.
|
|
332
|
+
if (gc_min, gc_max) == (GC_OPT_MIN, GC_OPT_MAX):
|
|
333
|
+
gc_min, gc_max = resolve_host_gc_range(host)
|
|
334
|
+
gc_score = gc_band_score(gc, gc_min, gc_max, config.gc_decay_width)
|
|
254
335
|
|
|
255
336
|
# Component 3: MFE (optional)
|
|
256
337
|
mfe_score = 0.5 # neutral default
|
|
@@ -336,6 +417,14 @@ def compute_mfe_evidence(
|
|
|
336
417
|
reason = "MFE was not computed because no sequence was provided."
|
|
337
418
|
elif not _check_vienna_available():
|
|
338
419
|
reason = "MFE was not computed because ViennaRNA is unavailable in this environment."
|
|
420
|
+
elif len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
|
|
421
|
+
# 170-fix: distinguish a deliberate length-based skip from an actual
|
|
422
|
+
# fold failure — the generic "computation failed" message below would
|
|
423
|
+
# otherwise mislead a caller into thinking something is broken.
|
|
424
|
+
reason = (
|
|
425
|
+
f"MFE was skipped because the sequence ({len(sequence)} nt) exceeds "
|
|
426
|
+
f"the {MFE_MAX_SEQUENCE_LENGTH} nt limit for global MFE folding."
|
|
427
|
+
)
|
|
339
428
|
else:
|
|
340
429
|
mfe_value = calculate_mfe(sequence)
|
|
341
430
|
if mfe_value is None:
|
|
@@ -115,6 +115,23 @@ def calculate_dinucleotide_ratio(sequence: str, dinucleotide: str = "CG") -> flo
|
|
|
115
115
|
return observed / expected
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
# Job 168 / v3.3.0 (_analysis/025): host -> production-default codon table file
|
|
119
|
+
# overrides. nbenthamiana moved from the legacy {host}_codons.json convention to
|
|
120
|
+
# the NbeV1.1 LAB-strain derived table. See data/reference/active_codon_reference.json.
|
|
121
|
+
# The legacy file itself (nbenthamiana_codons.json) is left untouched on disk for
|
|
122
|
+
# historical benchmark replay (benchmarks/run_benchmark.py --codon-table-path).
|
|
123
|
+
_HOST_CODON_TABLE_OVERRIDES: dict[str, str] = {
|
|
124
|
+
"nbenthamiana": "profiles/nbev11_cds_hc_derived_codons.json",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve_host_codon_table_path(host: str, codon_tables_dir: Path) -> Path:
|
|
129
|
+
"""Resolve the production-default codon table file path for a host."""
|
|
130
|
+
override = _HOST_CODON_TABLE_OVERRIDES.get(host)
|
|
131
|
+
filename = override or f"{host}_codons.json"
|
|
132
|
+
return codon_tables_dir / filename
|
|
133
|
+
|
|
134
|
+
|
|
118
135
|
def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
|
|
119
136
|
"""Load codon usage table for organism.
|
|
120
137
|
|
|
@@ -128,8 +145,10 @@ def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
|
|
|
128
145
|
Raises:
|
|
129
146
|
FileNotFoundError: If codon table file not found.
|
|
130
147
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
if organism.endswith(".json"):
|
|
149
|
+
codon_table_path = codon_tables_dir / organism
|
|
150
|
+
else:
|
|
151
|
+
codon_table_path = resolve_host_codon_table_path(organism, codon_tables_dir)
|
|
133
152
|
|
|
134
153
|
with open(codon_table_path, "r", encoding="utf-8") as handle:
|
|
135
154
|
return cast(dict[str, Any], json.load(handle))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.7
|
|
4
4
|
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review engine 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.2.
|
|
96
|
+
FactorForge v3.2.7 (2026). Open-source constraint-based CDS design engine.
|
|
97
97
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
98
98
|
```
|
|
99
99
|
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_benchmark_codon_table_metadata.py
RENAMED
|
@@ -110,7 +110,38 @@ def test_smoke_summary_contains_codon_table_fields(tmp_path):
|
|
|
110
110
|
]:
|
|
111
111
|
assert field in data, f"Missing codon table field in smoke summary JSON: {field}"
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
# No --codon-table-path override ⇒ codon_table_id must reflect whichever
|
|
114
|
+
# asset is actually the current production default (Job 168 / v3.3.0,
|
|
115
|
+
# _analysis/025 — see data/reference/active_codon_reference.json), not a
|
|
116
|
+
# hardcoded legacy literal.
|
|
117
|
+
active_ref = json.loads(
|
|
118
|
+
(ROOT / "data" / "reference" / "active_codon_reference.json").read_text(encoding="utf-8")
|
|
119
|
+
)
|
|
120
|
+
assert data["codon_table_id"] == active_ref["active_codon_table_id"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_smoke_summary_contains_vienna_rna_active(tmp_path):
|
|
124
|
+
"""Every future run must record whether ViennaRNA (and thus MFE-weighted
|
|
125
|
+
candidate selection) was active, per analysis 011's provenance-gap finding."""
|
|
126
|
+
from benchmarks.run_benchmark import run
|
|
127
|
+
|
|
128
|
+
out_csv = tmp_path / "results.csv"
|
|
129
|
+
out_md = tmp_path / "summary.md"
|
|
130
|
+
run(
|
|
131
|
+
dataset="synthetic",
|
|
132
|
+
mode="smoke",
|
|
133
|
+
out_csv=out_csv,
|
|
134
|
+
out_md=out_md,
|
|
135
|
+
proteins_fasta=ROOT / "tests" / "fixtures" / "small_proteins.fasta",
|
|
136
|
+
native_fasta=ROOT / "tests" / "fixtures" / "small_native_cds.fasta",
|
|
137
|
+
)
|
|
138
|
+
summary_json = tmp_path / "benchmark_summary.json"
|
|
139
|
+
data = json.loads(summary_json.read_text(encoding="utf-8"))
|
|
140
|
+
assert isinstance(data.get("vienna_rna_active"), bool), (
|
|
141
|
+
"vienna_rna_active must be a bool recording whether ViennaRNA was "
|
|
142
|
+
"available (and therefore could affect MFE-weighted candidate "
|
|
143
|
+
"selection) during this benchmark run"
|
|
144
|
+
)
|
|
114
145
|
|
|
115
146
|
|
|
116
147
|
# --- raw FASTA / genome files must not be committed ---
|
|
@@ -74,3 +74,59 @@ def test_aa_identity_policy_sync():
|
|
|
74
74
|
# registry says identity must be 1.0 — passing case confirms
|
|
75
75
|
ok = validate_cds_output("MKT", "ATGAAAACC")
|
|
76
76
|
assert ok["passed"] and ok["aa_identity"] == registry_val
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── codon_reference source-of-truth sync (Job 168 / v3.3.0, _analysis/025) ────
|
|
80
|
+
|
|
81
|
+
def test_codon_reference_active_sync_with_active_reference_file():
|
|
82
|
+
"""registry's codon_reference.active block must match
|
|
83
|
+
data/reference/active_codon_reference.json (the file run_benchmark.py
|
|
84
|
+
reads at runtime) — both describe "what is the current production
|
|
85
|
+
default", and must not drift apart."""
|
|
86
|
+
import json
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
root = Path(__file__).resolve().parents[1]
|
|
90
|
+
registry_active = _resolve("codon_reference.active")
|
|
91
|
+
active_ref = json.loads(
|
|
92
|
+
(root / "data" / "reference" / "active_codon_reference.json").read_text(encoding="utf-8")
|
|
93
|
+
)
|
|
94
|
+
assert registry_active["id"] == active_ref["active_codon_table_id"]
|
|
95
|
+
assert registry_active["asset_type"] == active_ref["active_asset_type"]
|
|
96
|
+
assert registry_active["codon_reference_contract_version"] == active_ref[
|
|
97
|
+
"codon_reference_contract_version"
|
|
98
|
+
]
|
|
99
|
+
# active_codon_reference.json doesn't carry sha256 directly — cross-check
|
|
100
|
+
# via the file it points at instead.
|
|
101
|
+
import hashlib
|
|
102
|
+
table_path = root / active_ref["file"]
|
|
103
|
+
assert registry_active["sha256"] == hashlib.sha256(table_path.read_bytes()).hexdigest()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_codon_reference_active_sync_with_v2_manifest():
|
|
107
|
+
"""registry's codon_reference.active block must match the schema-conformant
|
|
108
|
+
v2 manifest file's facts (asset_type, sha256, source_status)."""
|
|
109
|
+
import json
|
|
110
|
+
from pathlib import Path
|
|
111
|
+
|
|
112
|
+
registry_active = _resolve("codon_reference.active")
|
|
113
|
+
manifest = json.loads(
|
|
114
|
+
(Path(__file__).resolve().parents[1] / "data" / "reference"
|
|
115
|
+
/ "codon_table_manifest_nbev11_hc_v2.json").read_text(encoding="utf-8")
|
|
116
|
+
)
|
|
117
|
+
assert registry_active["id"] == manifest["codon_table_id"]
|
|
118
|
+
assert registry_active["sha256"] == manifest["sha256"]
|
|
119
|
+
assert registry_active["asset_type"] == manifest["asset_type"]
|
|
120
|
+
assert registry_active["source_status"] == manifest["source_status"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_codon_reference_active_table_sha256_matches_production_default():
|
|
124
|
+
"""The sha256 recorded for the active codon_reference must match the
|
|
125
|
+
actual file the production engine resolves to by default."""
|
|
126
|
+
from factorforge.engines.profile.utils import get_data_path, resolve_host_codon_table_path
|
|
127
|
+
import hashlib
|
|
128
|
+
|
|
129
|
+
registry_active = _resolve("codon_reference.active")
|
|
130
|
+
resolved_path = resolve_host_codon_table_path("nbenthamiana", get_data_path())
|
|
131
|
+
actual_sha256 = hashlib.sha256(resolved_path.read_bytes()).hexdigest()
|
|
132
|
+
assert registry_active["sha256"] == actual_sha256
|
|
@@ -61,7 +61,14 @@ def test_sfgfp_sequence_no_x_residues():
|
|
|
61
61
|
# ---------------------------------------------------------------------------
|
|
62
62
|
|
|
63
63
|
def test_run_example_deterministic():
|
|
64
|
-
"""run_example.py (no --freeze) must exit 0 when frozen outputs exist.
|
|
64
|
+
"""run_example.py (no --freeze) must exit 0 when frozen outputs exist.
|
|
65
|
+
|
|
66
|
+
Job 168 / v3.3.0 (_analysis/025): this script is explicitly pinned to the
|
|
67
|
+
legacy v1 codon reference (GC 55-65%) regardless of the engine's current
|
|
68
|
+
production default, so this reproducibility check stays valid forever.
|
|
69
|
+
See run_example_v2_smoke.py / test_run_example_v2_smoke_succeeds for the
|
|
70
|
+
current-default (v2) path.
|
|
71
|
+
"""
|
|
65
72
|
result = subprocess.run(
|
|
66
73
|
[sys.executable, str(EXAMPLE_DIR / "run_example.py")],
|
|
67
74
|
capture_output=True,
|
|
@@ -74,6 +81,26 @@ def test_run_example_deterministic():
|
|
|
74
81
|
assert "OK" in result.stdout, "expected 'OK' in output"
|
|
75
82
|
|
|
76
83
|
|
|
84
|
+
def test_run_example_v2_smoke_succeeds():
|
|
85
|
+
"""run_example_v2_smoke.py (current production default) must exit 0.
|
|
86
|
+
|
|
87
|
+
No frozen-output comparison — this only checks that the v2 path runs
|
|
88
|
+
end-to-end and reports correct provenance (Job 168 / v3.3.0, _analysis/025).
|
|
89
|
+
"""
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
[sys.executable, str(EXAMPLE_DIR / "run_example_v2_smoke.py")],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
cwd=str(ROOT),
|
|
95
|
+
)
|
|
96
|
+
assert result.returncode == 0, (
|
|
97
|
+
f"run_example_v2_smoke.py exited {result.returncode}\n"
|
|
98
|
+
f"stdout:{result.stdout}\nstderr:{result.stderr}"
|
|
99
|
+
)
|
|
100
|
+
assert "OK" in result.stdout
|
|
101
|
+
assert "nbenthamiana_nbev11_hc_v2" in result.stdout
|
|
102
|
+
|
|
103
|
+
|
|
77
104
|
# ---------------------------------------------------------------------------
|
|
78
105
|
# TC3 — design_package.json claim boundary
|
|
79
106
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/optimizer.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/core/interfaces/validator.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/nbenthamiana_codons.json
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/nbenthamiana_golden_set.json
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/templates/high_expression.json
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/data/wolffia_globosa_codons.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/construct_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/rules/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/scoring_ml.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/engines/profile/validator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/protein_risk/risk_classifier.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/schemas/design_package.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge/validation/package_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/src/factorforge_cds.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.7}/tests/test_openbio_missing_metric_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|