factorforge-cds 3.2.6__tar.gz → 3.2.8__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.8}/PKG-INFO +2 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/README.md +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/pyproject.toml +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/feasibility.py +5 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/metrics.py +4 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/optimizer.py +7 -3
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/pipeline.py +3 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/reverse_translator.py +28 -12
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/rule_engine.py +8 -4
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/scoring.py +104 -11
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/utils.py +20 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_codon_table_metadata.py +32 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_registry_production_sync.py +76 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_worked_example.py +33 -1
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/LICENSE +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/setup.cfg +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/main.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/optimizer.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/design_package.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/design_package.schema.json +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/SOURCES.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_cai.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_database.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_schema.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/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.8
|
|
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.8 (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.8 (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.8"
|
|
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"
|
|
@@ -19,6 +19,11 @@ from factorforge.analysis.metrics import (
|
|
|
19
19
|
# DEFAULT_CAI_TARGET=0.82 aligns with industry practice (>0.8) and is achievable.
|
|
20
20
|
# Exported as named constants so tests/test_registry_production_sync.py can
|
|
21
21
|
# 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.
|
|
22
27
|
DEFAULT_CAI_TARGET: float = 0.82
|
|
23
28
|
DEFAULT_GC_LOW: float = 55.0
|
|
24
29
|
DEFAULT_GC_HIGH: float = 65.0
|
|
@@ -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:
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/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.8"
|
|
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,16 @@ 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, resolved via
|
|
378
|
+
resolve_host_gc_range() (engines/profile/scoring.py
|
|
379
|
+
GC_OPT_MIN/MAX). nbenthamiana currently uses the legacy 55-65%
|
|
380
|
+
band; the native genome-composition anchor (40-47%, _analysis/025)
|
|
381
|
+
was provisionally reverted on 2026-06-29 pending an MFE
|
|
382
|
+
re-sensitivity + 2x2 factorial recheck.
|
|
374
383
|
"""
|
|
375
|
-
|
|
376
|
-
|
|
384
|
+
_host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
|
|
385
|
+
target_gc_min = kwargs.get("target_gc_min", _host_gc_min)
|
|
386
|
+
target_gc_max = kwargs.get("target_gc_max", _host_gc_max)
|
|
377
387
|
preferred_ratio = kwargs.get("preferred_ratio", 0.7)
|
|
378
388
|
max_attempts = kwargs.get("max_gc_attempts", 10)
|
|
379
389
|
if max_attempts < 1:
|
|
@@ -443,19 +453,19 @@ class ReverseTranslator:
|
|
|
443
453
|
"""GC-Target profile: drive global GC toward a configurable target.
|
|
444
454
|
|
|
445
455
|
Targets the caller-supplied ``target_gc`` if provided, otherwise the
|
|
446
|
-
host
|
|
447
|
-
|
|
456
|
+
active host's composition midpoint (resolve_host_gc_range(self.host);
|
|
457
|
+
43.5% for nbenthamiana since v3.3.0 / _analysis/025, 60.0% for other
|
|
458
|
+
hosts pending their own host-specific analysis). To target a different
|
|
459
|
+
GC (e.g. for specific vector requirements), pass target_gc explicitly.
|
|
448
460
|
|
|
449
461
|
- GC constraint first
|
|
450
462
|
- CAI may be sacrificed
|
|
451
463
|
- 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
464
|
"""
|
|
456
465
|
target_gc = kwargs.get("target_gc")
|
|
457
466
|
if target_gc is None:
|
|
458
|
-
|
|
467
|
+
_host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
|
|
468
|
+
target_gc = (_host_gc_min + _host_gc_max) / 2
|
|
459
469
|
|
|
460
470
|
dna_seq: list[str] = []
|
|
461
471
|
|
|
@@ -507,9 +517,14 @@ class ReverseTranslator:
|
|
|
507
517
|
raise ValueError("max_attempts must be >= 1")
|
|
508
518
|
last_seq = ""
|
|
509
519
|
|
|
520
|
+
balanced_kwargs = {
|
|
521
|
+
k: v for k, v in kwargs.items() if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
|
|
522
|
+
}
|
|
523
|
+
balanced_kwargs["preferred_ratio"] = 0.6
|
|
524
|
+
|
|
510
525
|
for attempt in range(max_attempts):
|
|
511
526
|
# Start with Balanced strategy
|
|
512
|
-
dna_seq = self._balanced_translate(protein_seq,
|
|
527
|
+
dna_seq = self._balanced_translate(protein_seq, **balanced_kwargs)
|
|
513
528
|
last_seq = dna_seq
|
|
514
529
|
|
|
515
530
|
# Check restriction sites (forward + reverse complement)
|
|
@@ -710,6 +725,7 @@ class ReverseTranslator:
|
|
|
710
725
|
gc=gc,
|
|
711
726
|
sequence=dna_seq,
|
|
712
727
|
profile=profile.value,
|
|
728
|
+
host=self.host,
|
|
713
729
|
**kwargs,
|
|
714
730
|
)
|
|
715
731
|
return {"sequence": dna_seq, "cai": cai, "gc": gc, "score": score}
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/rule_engine.py
RENAMED
|
@@ -358,10 +358,14 @@ 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, currently 55-65% for nbenthamiana
|
|
362
|
+
— the native genome-composition anchor, ~40-47%, _analysis/025, was
|
|
363
|
+
provisionally reverted on 2026-06-29 pending an MFE re-sensitivity +
|
|
364
|
+
2x2 factorial recheck) and the API/DP gc_min/gc_max constraints. The
|
|
365
|
+
wide 25-75% local band is an independent synthesis/hairpin-risk guard
|
|
366
|
+
(registry `gc_extreme_local_window`), not derived from or coupled to
|
|
367
|
+
the global GC target — narrowing it is a separate decision from the
|
|
368
|
+
global band retarget and is out of scope here.
|
|
365
369
|
|
|
366
370
|
Args:
|
|
367
371
|
seq: DNA sequence
|
|
@@ -17,11 +17,42 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
# high-expression codon table which exhibits 3rd-position GC bias.
|
|
18
18
|
# These constants define the acceptable band — sequences within [GC_OPT_MIN, GC_OPT_MAX]
|
|
19
19
|
# receive full GC score; outside the band the score decays linearly.
|
|
20
|
+
#
|
|
21
|
+
# Job 168/v3.3.0 (released as part of v3.2.7, see CHANGELOG) moved this band to
|
|
22
|
+
# 40-47% (native genome-composition anchor, _analysis/025). Provisionally
|
|
23
|
+
# reverted here pending an MFE re-sensitivity + 2x2 factorial recheck
|
|
24
|
+
# (_analysis, scope TBD as of 2026-06-29) — see eijex-workspace
|
|
25
|
+
# _version/factorforge-version-sequencing-plan.md. Not a rejection of the
|
|
26
|
+
# 40-47% anchor, just not yet re-confirmed as the production default.
|
|
20
27
|
GC_OPT_MIN = 55.0
|
|
21
28
|
GC_OPT_MAX = 65.0
|
|
22
29
|
GC_OPT_MID = 60.0 # kept for gc_target point-scoring and viral_delivery centering
|
|
23
30
|
GC_DECAY_WIDTH = 20.0 # percentage points outside band before score reaches 0.0
|
|
24
31
|
|
|
32
|
+
# Host-isolation fix (Job 168/v3.3.0, kept): ntabacum/BY-2 must not silently
|
|
33
|
+
# inherit whatever band nbenthamiana uses. Both currently resolve to the same
|
|
34
|
+
# GC_RANGE_DEFAULT band pending the nbenthamiana re-check above.
|
|
35
|
+
GC_RANGE_DEFAULT: tuple[float, float] = (55.0, 65.0)
|
|
36
|
+
GC_RANGES_BY_HOST: dict[str, tuple[float, float]] = {
|
|
37
|
+
"nbenthamiana": (GC_OPT_MIN, GC_OPT_MAX),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_host_gc_range(host: str | None) -> tuple[float, float]:
|
|
42
|
+
"""Resolve the (gc_min, gc_max) composition band for a host.
|
|
43
|
+
|
|
44
|
+
nbenthamiana uses the _analysis/025 native genome-composition anchor.
|
|
45
|
+
Any other host (including unknown ones) keeps the pre-v3.3.0 global
|
|
46
|
+
default band until it gets its own host-specific analysis.
|
|
47
|
+
"""
|
|
48
|
+
return GC_RANGES_BY_HOST.get(host or "nbenthamiana", GC_RANGE_DEFAULT)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_host_gc_mid(host: str | None) -> float:
|
|
52
|
+
"""Resolve the GC band midpoint for a host (see resolve_host_gc_range)."""
|
|
53
|
+
gc_min, gc_max = resolve_host_gc_range(host)
|
|
54
|
+
return (gc_min + gc_max) / 2
|
|
55
|
+
|
|
25
56
|
# ViennaRNA availability cache
|
|
26
57
|
_vienna_available: bool | None = None
|
|
27
58
|
|
|
@@ -45,7 +76,12 @@ class ScoringConfig:
|
|
|
45
76
|
use_mfe: bool = True
|
|
46
77
|
|
|
47
78
|
def __post_init__(self) -> None:
|
|
48
|
-
"""
|
|
79
|
+
"""Validate inputs, then normalize weights to sum to 1.0."""
|
|
80
|
+
for field_name in ("w_cai", "w_gc", "w_mfe", "w_dinuc", "w_syncodonlm"):
|
|
81
|
+
if getattr(self, field_name) < 0:
|
|
82
|
+
raise ValueError(f"{field_name} must be >= 0, got {getattr(self, field_name)}")
|
|
83
|
+
if self.gc_min > self.gc_max:
|
|
84
|
+
raise ValueError(f"gc_min ({self.gc_min}) must be <= gc_max ({self.gc_max})")
|
|
49
85
|
self._normalize()
|
|
50
86
|
|
|
51
87
|
def _normalize(self) -> None:
|
|
@@ -110,6 +146,20 @@ def _check_vienna_available() -> bool:
|
|
|
110
146
|
return _vienna_available
|
|
111
147
|
|
|
112
148
|
|
|
149
|
+
# 170-fix: ViennaRNA's RNA.fold() uses Zuker's MFE algorithm, O(n^3) time /
|
|
150
|
+
# O(n^2) memory — there was previously no length guard anywhere in the
|
|
151
|
+
# calculate_mfe() call chain, so a single request at/under the existing
|
|
152
|
+
# public API length limits (5000aa/15000bp) could pin a CPU core for minutes
|
|
153
|
+
# (algorithmic-complexity DoS, CWE-407; confirmed via faulthandler stack
|
|
154
|
+
# traces + an isolated RNA.fold() timing curve: 1000nt ~2.2s, 2000nt ~9.8s,
|
|
155
|
+
# 3000nt ~24.6s). calculate_mfe() is called twice per optimize() call
|
|
156
|
+
# (once for scoring, once independently for compute_mfe_evidence()
|
|
157
|
+
# provenance), so the real per-request cost is ~2x this curve. 1000nt
|
|
158
|
+
# (~333aa) keeps worst-case cost to roughly 4-5s even with that doubling,
|
|
159
|
+
# while still covering most realistic single-protein CDS design requests.
|
|
160
|
+
MFE_MAX_SEQUENCE_LENGTH = 1000
|
|
161
|
+
|
|
162
|
+
|
|
113
163
|
def calculate_mfe(sequence: str) -> float | None:
|
|
114
164
|
"""
|
|
115
165
|
Calculate minimum free energy (MFE) using ViennaRNA.
|
|
@@ -118,11 +168,22 @@ def calculate_mfe(sequence: str) -> float | None:
|
|
|
118
168
|
sequence: DNA or RNA sequence.
|
|
119
169
|
|
|
120
170
|
Returns:
|
|
121
|
-
MFE in kcal/mol, or None if ViennaRNA is not available
|
|
171
|
+
MFE in kcal/mol, or None if ViennaRNA is not available or the
|
|
172
|
+
sequence exceeds MFE_MAX_SEQUENCE_LENGTH.
|
|
122
173
|
"""
|
|
123
174
|
if not _check_vienna_available():
|
|
124
175
|
return None
|
|
125
176
|
|
|
177
|
+
if len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"Sequence length (%d nt) exceeds MFE_MAX_SEQUENCE_LENGTH (%d nt); "
|
|
180
|
+
"skipping global MFE calculation to avoid an unbounded ViennaRNA "
|
|
181
|
+
"RNA.fold() runtime (O(n^3)). MFE scoring falls back to a neutral, "
|
|
182
|
+
"zero-weighted contribution for this candidate (170-fix).",
|
|
183
|
+
len(sequence), MFE_MAX_SEQUENCE_LENGTH,
|
|
184
|
+
)
|
|
185
|
+
return None
|
|
186
|
+
|
|
126
187
|
try:
|
|
127
188
|
import RNA
|
|
128
189
|
|
|
@@ -139,24 +200,34 @@ def normalize_mfe(mfe: float, seq_length: int) -> float:
|
|
|
139
200
|
"""
|
|
140
201
|
Normalize MFE to 0-1 range where 1 = no structure (favorable).
|
|
141
202
|
|
|
142
|
-
|
|
143
|
-
-
|
|
203
|
+
Clamp range calibrated empirically in analysis 011
|
|
204
|
+
(eijex-workspace/_analysis/2026-06-26/011-mfe-clamp-calibration):
|
|
205
|
+
measured MFE/nt across 135 FactorForge outputs (N. benthamiana +
|
|
206
|
+
BY-2 hosts, 5 profiles) ranged -0.4064 to -0.1338 (combined 5th/95th
|
|
207
|
+
percentile -0.3839/-0.1760). The range below widens that empirical
|
|
208
|
+
percentile by a margin on both sides to avoid saturating ~10% of
|
|
209
|
+
future sequences at the score floor/ceiling.
|
|
144
210
|
|
|
145
211
|
Args:
|
|
146
212
|
mfe: Minimum free energy in kcal/mol.
|
|
147
213
|
seq_length: Sequence length in nucleotides.
|
|
148
214
|
|
|
149
215
|
Returns:
|
|
150
|
-
Normalized MFE score (0-1
|
|
216
|
+
Normalized MFE score (0-1) under this computational normalization,
|
|
217
|
+
where a higher value represents a less negative whole-CDS MFE/nt.
|
|
218
|
+
This is a Tier-0 computational heuristic only (see
|
|
219
|
+
eijex-validationHub/docs/CLAIM_EVIDENCE_BENCHMARK_MODEL.md §4) — no
|
|
220
|
+
biological interpretation (translation efficiency, mRNA stability,
|
|
221
|
+
or expression outcome) is implied or validated.
|
|
151
222
|
"""
|
|
152
223
|
if seq_length == 0:
|
|
153
224
|
return 0.5
|
|
154
225
|
|
|
155
226
|
mfe_per_nt = mfe / seq_length
|
|
156
|
-
# Clamp to
|
|
157
|
-
clamped = max(-0.
|
|
158
|
-
# Map to [0, 1] where 0.
|
|
159
|
-
return
|
|
227
|
+
# Clamp to empirically calibrated range [-0.40, -0.15] (analysis 011)
|
|
228
|
+
clamped = max(-0.40, min(-0.15, mfe_per_nt))
|
|
229
|
+
# Map to [0, 1] where -0.15 kcal/mol/nt → 1.0 and -0.40 → 0.0
|
|
230
|
+
return (clamped + 0.40) / 0.25
|
|
160
231
|
|
|
161
232
|
|
|
162
233
|
def gc_band_score(
|
|
@@ -210,6 +281,7 @@ def calculate_composite_score(
|
|
|
210
281
|
sequence: str | None = None,
|
|
211
282
|
config: ScoringConfig | None = None,
|
|
212
283
|
profile: str | None = None,
|
|
284
|
+
host: str | None = None,
|
|
213
285
|
**kwargs: Any,
|
|
214
286
|
) -> float:
|
|
215
287
|
"""Calculate multidimensional composite score.
|
|
@@ -238,7 +310,12 @@ def calculate_composite_score(
|
|
|
238
310
|
profile_name = (profile or "balanced").lower()
|
|
239
311
|
config = PROFILE_SCORING_CONFIGS.get(profile_name)
|
|
240
312
|
if config is None:
|
|
241
|
-
|
|
313
|
+
supported = ", ".join(sorted(PROFILE_SCORING_CONFIGS))
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Unknown profile: {profile_name}. Supported profiles: {supported}"
|
|
316
|
+
)
|
|
317
|
+
if "target_gc" in kwargs and profile_name != "gc_target":
|
|
318
|
+
raise ValueError("target_gc is only valid for the gc_target profile")
|
|
242
319
|
|
|
243
320
|
# Component 1: CAI (already 0-1)
|
|
244
321
|
cai_score = max(0.0, min(1.0, cai))
|
|
@@ -250,7 +327,15 @@ def calculate_composite_score(
|
|
|
250
327
|
tgt = float(kwargs["target_gc"])
|
|
251
328
|
gc_score = gc_band_score(gc, tgt - 5.0, tgt + 5.0, config.gc_decay_width)
|
|
252
329
|
else:
|
|
253
|
-
|
|
330
|
+
gc_min, gc_max = config.gc_min, config.gc_max
|
|
331
|
+
# Profiles that haven't customized the band (still on the
|
|
332
|
+
# GC_OPT_MIN/MAX default) follow the active host's composition band.
|
|
333
|
+
# Profiles with an explicit band override (e.g. viral_delivery) keep
|
|
334
|
+
# it regardless of host — that override is profile-specific, not a
|
|
335
|
+
# host default.
|
|
336
|
+
if (gc_min, gc_max) == (GC_OPT_MIN, GC_OPT_MAX):
|
|
337
|
+
gc_min, gc_max = resolve_host_gc_range(host)
|
|
338
|
+
gc_score = gc_band_score(gc, gc_min, gc_max, config.gc_decay_width)
|
|
254
339
|
|
|
255
340
|
# Component 3: MFE (optional)
|
|
256
341
|
mfe_score = 0.5 # neutral default
|
|
@@ -336,6 +421,14 @@ def compute_mfe_evidence(
|
|
|
336
421
|
reason = "MFE was not computed because no sequence was provided."
|
|
337
422
|
elif not _check_vienna_available():
|
|
338
423
|
reason = "MFE was not computed because ViennaRNA is unavailable in this environment."
|
|
424
|
+
elif len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
|
|
425
|
+
# 170-fix: distinguish a deliberate length-based skip from an actual
|
|
426
|
+
# fold failure — the generic "computation failed" message below would
|
|
427
|
+
# otherwise mislead a caller into thinking something is broken.
|
|
428
|
+
reason = (
|
|
429
|
+
f"MFE was skipped because the sequence ({len(sequence)} nt) exceeds "
|
|
430
|
+
f"the {MFE_MAX_SEQUENCE_LENGTH} nt limit for global MFE folding."
|
|
431
|
+
)
|
|
339
432
|
else:
|
|
340
433
|
mfe_value = calculate_mfe(sequence)
|
|
341
434
|
if mfe_value is None:
|
|
@@ -115,6 +115,22 @@ 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) introduced a host -> production-default
|
|
119
|
+
# codon table file override mechanism and pointed nbenthamiana at the NbeV1.1
|
|
120
|
+
# LAB-strain derived table (released as part of v3.2.7). Provisionally
|
|
121
|
+
# reverted to empty (falls back to the legacy {host}_codons.json convention)
|
|
122
|
+
# pending an MFE re-sensitivity + 2x2 factorial recheck. The NbeV1.1 table
|
|
123
|
+
# remains on disk and selectable; see data/reference/active_codon_reference.json.
|
|
124
|
+
_HOST_CODON_TABLE_OVERRIDES: dict[str, str] = {}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_host_codon_table_path(host: str, codon_tables_dir: Path) -> Path:
|
|
128
|
+
"""Resolve the production-default codon table file path for a host."""
|
|
129
|
+
override = _HOST_CODON_TABLE_OVERRIDES.get(host)
|
|
130
|
+
filename = override or f"{host}_codons.json"
|
|
131
|
+
return codon_tables_dir / filename
|
|
132
|
+
|
|
133
|
+
|
|
118
134
|
def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
|
|
119
135
|
"""Load codon usage table for organism.
|
|
120
136
|
|
|
@@ -128,8 +144,10 @@ def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
|
|
|
128
144
|
Raises:
|
|
129
145
|
FileNotFoundError: If codon table file not found.
|
|
130
146
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
if organism.endswith(".json"):
|
|
148
|
+
codon_table_path = codon_tables_dir / organism
|
|
149
|
+
else:
|
|
150
|
+
codon_table_path = resolve_host_codon_table_path(organism, codon_tables_dir)
|
|
133
151
|
|
|
134
152
|
with open(codon_table_path, "r", encoding="utf-8") as handle:
|
|
135
153
|
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.8
|
|
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.8 (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.8}/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,79 @@ 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_legacy_manifest():
|
|
107
|
+
"""registry's codon_reference.active block must match the schema-conformant
|
|
108
|
+
legacy (v1) manifest file's facts (asset_type, sha256, source_status) — v1
|
|
109
|
+
is the production default again as of the v3.2.7 GC-band/codon-reference
|
|
110
|
+
revert, pending an MFE re-sensitivity + 2x2 factorial recheck of v2."""
|
|
111
|
+
import json
|
|
112
|
+
from pathlib import Path
|
|
113
|
+
|
|
114
|
+
registry_active = _resolve("codon_reference.active")
|
|
115
|
+
manifest = json.loads(
|
|
116
|
+
(Path(__file__).resolve().parents[1] / "data" / "reference"
|
|
117
|
+
/ "codon_table_manifest.json").read_text(encoding="utf-8")
|
|
118
|
+
)
|
|
119
|
+
assert registry_active["id"] == manifest["codon_table_id"]
|
|
120
|
+
assert registry_active["sha256"] == manifest["sha256"]
|
|
121
|
+
assert registry_active["asset_type"] == manifest["asset_type"]
|
|
122
|
+
assert registry_active["source_status"] == manifest["source_status"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_codon_reference_candidate_sync_with_v2_manifest():
|
|
126
|
+
"""registry's codon_reference.candidate block (the provisionally
|
|
127
|
+
un-promoted v2 asset) must stay in sync with its own manifest file even
|
|
128
|
+
while not active."""
|
|
129
|
+
import json
|
|
130
|
+
from pathlib import Path
|
|
131
|
+
|
|
132
|
+
registry_candidate = _resolve("codon_reference.candidate")
|
|
133
|
+
manifest = json.loads(
|
|
134
|
+
(Path(__file__).resolve().parents[1] / "data" / "reference"
|
|
135
|
+
/ "codon_table_manifest_nbev11_hc_v2.json").read_text(encoding="utf-8")
|
|
136
|
+
)
|
|
137
|
+
assert registry_candidate["id"] == manifest["codon_table_id"]
|
|
138
|
+
assert registry_candidate["sha256"] == manifest["sha256"]
|
|
139
|
+
assert registry_candidate["asset_type"] == manifest["asset_type"]
|
|
140
|
+
assert registry_candidate["source_status"] == manifest["source_status"]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_codon_reference_active_table_sha256_matches_production_default():
|
|
144
|
+
"""The sha256 recorded for the active codon_reference must match the
|
|
145
|
+
actual file the production engine resolves to by default."""
|
|
146
|
+
from factorforge.engines.profile.utils import get_data_path, resolve_host_codon_table_path
|
|
147
|
+
import hashlib
|
|
148
|
+
|
|
149
|
+
registry_active = _resolve("codon_reference.active")
|
|
150
|
+
resolved_path = resolve_host_codon_table_path("nbenthamiana", get_data_path())
|
|
151
|
+
actual_sha256 = hashlib.sha256(resolved_path.read_bytes()).hexdigest()
|
|
152
|
+
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,31 @@ 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 (tracks whatever the current production
|
|
86
|
+
default actually is) must exit 0 and report a provenance ID that matches
|
|
87
|
+
data/reference/active_codon_reference.json — not hardcoded to v2, since
|
|
88
|
+
the default was provisionally reverted to v1 on 2026-06-29 pending an MFE
|
|
89
|
+
re-sensitivity + 2x2 factorial recheck (Job 168 / v3.3.0, _analysis/025).
|
|
90
|
+
|
|
91
|
+
No frozen-output comparison — this only checks that the current-default
|
|
92
|
+
path runs end-to-end and reports correct provenance.
|
|
93
|
+
"""
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
[sys.executable, str(EXAMPLE_DIR / "run_example_v2_smoke.py")],
|
|
96
|
+
capture_output=True,
|
|
97
|
+
text=True,
|
|
98
|
+
cwd=str(ROOT),
|
|
99
|
+
)
|
|
100
|
+
assert result.returncode == 0, (
|
|
101
|
+
f"run_example_v2_smoke.py exited {result.returncode}\n"
|
|
102
|
+
f"stdout:{result.stdout}\nstderr:{result.stderr}"
|
|
103
|
+
)
|
|
104
|
+
assert "OK" in result.stdout
|
|
105
|
+
active_ref = _load(ROOT / "data" / "reference" / "active_codon_reference.json")
|
|
106
|
+
assert active_ref["active_codon_table_id"] in result.stdout
|
|
107
|
+
|
|
108
|
+
|
|
77
109
|
# ---------------------------------------------------------------------------
|
|
78
110
|
# TC3 — design_package.json claim boundary
|
|
79
111
|
# ---------------------------------------------------------------------------
|
|
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.8}/src/factorforge/core/interfaces/optimizer.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/validator.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_codons.json
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_golden_set.json
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/templates/high_expression.json
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/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.8}/src/factorforge/engines/profile/construct_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/scoring_ml.py
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/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.8}/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.8}/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.8}/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.8}/src/factorforge_cds.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/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.8}/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
|