factorforge-cds 3.3.1__tar.gz → 3.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/PKG-INFO +2 -2
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/README.md +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/pyproject.toml +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/feasibility.py +30 -3
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/main.py +21 -11
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/optimizer.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_golden_set.json +4 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/__init__.py +1 -1
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/optimizer.py +14 -6
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/pipeline.py +23 -13
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/reverse_translator.py +81 -10
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/SOURCES.txt +1 -0
- factorforge_cds-3.3.2/tests/test_benchmark_cli_output_guard.py +87 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/LICENSE +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/setup.cfg +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/metrics.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_derived_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/reference/reference_policy_manifest.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/utils.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/annotate.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/kd_scale.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/risk_classifier.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/sp_predict.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/tm_predict.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.schema.json +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation_report.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/requires.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_codon_table_metadata.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_scoring.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_cai.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_database.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_schema.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_docs_consistency.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_protein_risk.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_registry_production_sync.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_translation_integrity.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_contract_compat.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_registry.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_report.py +0 -0
- {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_worked_example.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
4
|
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
|
|
5
5
|
Author-email: Eijex <eijex.lab@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
|
|
|
93
93
|
## Citing
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
FactorForge v3.3.
|
|
96
|
+
FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
|
|
97
97
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
|
|
|
60
60
|
## Citing
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
FactorForge v3.3.
|
|
63
|
+
FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
|
|
64
64
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "factorforge-cds"
|
|
7
|
-
version = "3.3.
|
|
7
|
+
version = "3.3.2"
|
|
8
8
|
description = "FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-only"
|
|
@@ -51,6 +51,7 @@ def _cai_from_log(log_sum: float, codon_count: int) -> float:
|
|
|
51
51
|
def _candidate_summary(
|
|
52
52
|
dna_sequence: str | None,
|
|
53
53
|
codon_weights: dict[str, float],
|
|
54
|
+
cai_authority: dict[str, Any],
|
|
54
55
|
) -> dict[str, Any] | None:
|
|
55
56
|
if dna_sequence is None:
|
|
56
57
|
return None
|
|
@@ -59,6 +60,7 @@ def _candidate_summary(
|
|
|
59
60
|
return {
|
|
60
61
|
"dna_sequence": dna_sequence,
|
|
61
62
|
"cai": calculate_cai(dna_sequence, codon_weights),
|
|
63
|
+
"cai_authority": dict(cai_authority),
|
|
62
64
|
"gc": calculate_gc(dna_sequence),
|
|
63
65
|
"first_region_gc": calculate_first_region_gc(dna_sequence),
|
|
64
66
|
"gc_window_min": min(window_values) if window_values else 0.0,
|
|
@@ -66,6 +68,20 @@ def _candidate_summary(
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def _build_cai_authority(codon_reference_id: str | None) -> dict[str, Any]:
|
|
72
|
+
if codon_reference_id is None:
|
|
73
|
+
return {
|
|
74
|
+
"reference_id": None,
|
|
75
|
+
"reference_role": "cai_evaluation",
|
|
76
|
+
"reference_relationship": "unresolved",
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
"reference_id": codon_reference_id,
|
|
80
|
+
"reference_role": "cai_evaluation",
|
|
81
|
+
"reference_relationship": "same_as_generation_reference",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
69
85
|
def _best_gc_under_gc_range(
|
|
70
86
|
states: dict[int, float],
|
|
71
87
|
protein_length: int,
|
|
@@ -104,6 +120,7 @@ def analyze_feasibility(
|
|
|
104
120
|
target_gc_low: float = DEFAULT_GC_LOW,
|
|
105
121
|
target_gc_high: float = DEFAULT_GC_HIGH,
|
|
106
122
|
gc_ranges: list[tuple[float, float]] | None = None,
|
|
123
|
+
codon_reference_id: str | None = None,
|
|
107
124
|
) -> dict[str, Any]:
|
|
108
125
|
"""Compute exact CAI/GC feasibility over synonymous codon choices.
|
|
109
126
|
|
|
@@ -160,6 +177,7 @@ def analyze_feasibility(
|
|
|
160
177
|
|
|
161
178
|
protein_length = len(protein)
|
|
162
179
|
total_bases = protein_length * 3
|
|
180
|
+
cai_authority = _build_cai_authority(codon_reference_id)
|
|
163
181
|
best_any_gc = max(states, key=lambda gc_count: states[gc_count])
|
|
164
182
|
best_any_log_sum = states[best_any_gc]
|
|
165
183
|
best_any_sequence = _reconstruct_sequence(backrefs, best_any_gc)
|
|
@@ -180,7 +198,7 @@ def analyze_feasibility(
|
|
|
180
198
|
range_results[key] = {
|
|
181
199
|
"feasible": True,
|
|
182
200
|
"max_cai": _cai_from_log(states[gc_count], protein_length),
|
|
183
|
-
"best_candidate": _candidate_summary(sequence, codon_weights),
|
|
201
|
+
"best_candidate": _candidate_summary(sequence, codon_weights, cai_authority),
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
target_cai_possible = (
|
|
@@ -192,8 +210,13 @@ def analyze_feasibility(
|
|
|
192
210
|
"protein_length": protein_length,
|
|
193
211
|
"minimum_possible_gc": (min_gc_count / total_bases) * 100.0,
|
|
194
212
|
"maximum_possible_gc": (max_gc_count / total_bases) * 100.0,
|
|
213
|
+
"cai_authority": dict(cai_authority),
|
|
195
214
|
"maximum_achievable_cai_without_gc": _cai_from_log(best_any_log_sum, protein_length),
|
|
196
|
-
"best_candidate_without_gc": _candidate_summary(
|
|
215
|
+
"best_candidate_without_gc": _candidate_summary(
|
|
216
|
+
best_any_sequence,
|
|
217
|
+
codon_weights,
|
|
218
|
+
cai_authority,
|
|
219
|
+
),
|
|
197
220
|
"ranges": range_results,
|
|
198
221
|
"target": {
|
|
199
222
|
"cai": target_cai,
|
|
@@ -206,7 +229,11 @@ def analyze_feasibility(
|
|
|
206
229
|
else None
|
|
207
230
|
),
|
|
208
231
|
"best_candidate": (
|
|
209
|
-
_candidate_summary(
|
|
232
|
+
_candidate_summary(
|
|
233
|
+
_reconstruct_sequence(backrefs, target_gc_count),
|
|
234
|
+
codon_weights,
|
|
235
|
+
cai_authority,
|
|
236
|
+
)
|
|
210
237
|
if target_gc_count is not None
|
|
211
238
|
else None
|
|
212
239
|
),
|
|
@@ -154,6 +154,7 @@ def _build_dp_result(
|
|
|
154
154
|
gc_max: float,
|
|
155
155
|
cai_target: float = DEFAULT_CAI_TARGET,
|
|
156
156
|
codon_table_path: Path | None = None,
|
|
157
|
+
codon_reference_id: str | None = None,
|
|
157
158
|
):
|
|
158
159
|
"""Run the constraint-based DP feasibility engine for a single protein sequence."""
|
|
159
160
|
if objective != "feasibility_best":
|
|
@@ -171,6 +172,7 @@ def _build_dp_result(
|
|
|
171
172
|
target_cai=cai_target,
|
|
172
173
|
target_gc_low=gc_min,
|
|
173
174
|
target_gc_high=gc_max,
|
|
175
|
+
codon_reference_id=codon_reference_id,
|
|
174
176
|
)
|
|
175
177
|
best = result["target"]["best_candidate"]
|
|
176
178
|
feasible = best is not None
|
|
@@ -222,7 +224,10 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
|
|
|
222
224
|
cai = float(result.metrics.get("cai", 0.0))
|
|
223
225
|
gc = float(result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0)))
|
|
224
226
|
score = float(result.metrics.get("score", 0.0))
|
|
225
|
-
header =
|
|
227
|
+
header = (
|
|
228
|
+
f">{sequence_id}|engine=profile|profile={profile}|"
|
|
229
|
+
f"cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
|
|
230
|
+
)
|
|
226
231
|
return f"{header}\n{_wrap_sequence(result.sequence)}\n"
|
|
227
232
|
|
|
228
233
|
|
|
@@ -399,7 +404,10 @@ def optimize(
|
|
|
399
404
|
if reference_table_path is not None:
|
|
400
405
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
401
406
|
|
|
402
|
-
optimizer = RuleBasedOptimizer(
|
|
407
|
+
optimizer = RuleBasedOptimizer(
|
|
408
|
+
codon_table_path=str(reference_table_path),
|
|
409
|
+
generation_reference_id=reference_id,
|
|
410
|
+
)
|
|
403
411
|
else:
|
|
404
412
|
optimizer = EngineRegistry.get("profile")
|
|
405
413
|
profile_results = []
|
|
@@ -436,7 +444,10 @@ def optimize(
|
|
|
436
444
|
if reference_table_path is not None and engine == "profile":
|
|
437
445
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
438
446
|
|
|
439
|
-
optimizer = RuleBasedOptimizer(
|
|
447
|
+
optimizer = RuleBasedOptimizer(
|
|
448
|
+
codon_table_path=str(reference_table_path),
|
|
449
|
+
generation_reference_id=reference_id,
|
|
450
|
+
)
|
|
440
451
|
else:
|
|
441
452
|
optimizer = EngineRegistry.get(engine)
|
|
442
453
|
payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
|
|
@@ -465,14 +476,9 @@ def optimize(
|
|
|
465
476
|
combined_fasta = []
|
|
466
477
|
for idx, result in enumerate(results):
|
|
467
478
|
seq_id = payload[idx]["id"]
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
score = result.metrics.get("score", 0.0)
|
|
471
|
-
header = (
|
|
472
|
-
f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
|
|
473
|
-
f"gc={float(gc):.2f}|score={float(score):.3f}"
|
|
479
|
+
combined_fasta.append(
|
|
480
|
+
_format_profile_fasta(seq_id, profile, result).rstrip()
|
|
474
481
|
)
|
|
475
|
-
combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
|
|
476
482
|
out_content = "\n".join(combined_fasta) + "\n"
|
|
477
483
|
|
|
478
484
|
if output:
|
|
@@ -497,6 +503,7 @@ def optimize(
|
|
|
497
503
|
gc_max=gc_max,
|
|
498
504
|
cai_target=cai_target,
|
|
499
505
|
codon_table_path=reference_table_path,
|
|
506
|
+
codon_reference_id=reference_id,
|
|
500
507
|
)
|
|
501
508
|
dna_sequence = best["dna_sequence"]
|
|
502
509
|
cai = float(best["cai"])
|
|
@@ -566,7 +573,10 @@ def optimize(
|
|
|
566
573
|
if reference_table_path is not None and engine == "profile":
|
|
567
574
|
from factorforge.engines.profile.optimizer import RuleBasedOptimizer
|
|
568
575
|
|
|
569
|
-
optimizer = RuleBasedOptimizer(
|
|
576
|
+
optimizer = RuleBasedOptimizer(
|
|
577
|
+
codon_table_path=str(reference_table_path),
|
|
578
|
+
generation_reference_id=reference_id,
|
|
579
|
+
)
|
|
570
580
|
else:
|
|
571
581
|
optimizer = EngineRegistry.get(engine)
|
|
572
582
|
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_golden_set.json
RENAMED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"organism": "Nicotiana benthamiana",
|
|
3
|
+
"reference_id": "nbenthamiana_golden_set_v1",
|
|
4
|
+
"reference_role": "cai_evaluation",
|
|
5
|
+
"reference_version": "1",
|
|
3
6
|
"source": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
|
|
7
|
+
"source_basis": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
|
|
4
8
|
"description": "Codon usage frequencies weighted toward highly expressed N. benthamiana genes (RuBisCO, ribosomal proteins, histones). Used as CAI reference weights per Sharp & Li (1987). All source data is publicly available.",
|
|
5
9
|
"blend_ratio": 0.7,
|
|
6
10
|
"optimal_gc_content": 42.5,
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/optimizer.py
RENAMED
|
@@ -20,9 +20,13 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
20
20
|
"""Profile-based rule optimization engine."""
|
|
21
21
|
|
|
22
22
|
name = "Profile-based"
|
|
23
|
-
version = "3.3.
|
|
23
|
+
version = "3.3.2"
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
codon_table_path: str | None = None,
|
|
28
|
+
generation_reference_id: str | None = None,
|
|
29
|
+
) -> None:
|
|
26
30
|
"""
|
|
27
31
|
Args:
|
|
28
32
|
codon_table_path: Optional path to a codon usage table JSON used for
|
|
@@ -31,6 +35,8 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
31
35
|
product `optimize` CLI and the registered engine ever take, so
|
|
32
36
|
their behavior is unchanged. Injection is used by the benchmark
|
|
33
37
|
harness to drive design with alternative source-profile tables.
|
|
38
|
+
generation_reference_id: Explicit manifest reference id when the
|
|
39
|
+
injected design table is also the CAI evaluation reference.
|
|
34
40
|
"""
|
|
35
41
|
self.validator = InputValidator()
|
|
36
42
|
self._codon_table_path = codon_table_path
|
|
@@ -43,6 +49,8 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
43
49
|
self.translator = ReverseTranslator(
|
|
44
50
|
codon_table_path=codon_table_path,
|
|
45
51
|
golden_set_path=codon_table_path,
|
|
52
|
+
generation_reference_id=generation_reference_id,
|
|
53
|
+
_allow_golden_set_metadata_fallback=codon_table_path is not None,
|
|
46
54
|
)
|
|
47
55
|
self.rule_engine = RuleEngine()
|
|
48
56
|
self.exporter = SequenceExporter()
|
|
@@ -127,7 +135,8 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
127
135
|
candidates = [{"sequence": optimized_dna, "cai": cai, "gc": gc, "score": score}]
|
|
128
136
|
else:
|
|
129
137
|
translate_kwargs = {
|
|
130
|
-
k: v
|
|
138
|
+
k: v
|
|
139
|
+
for k, v in kwargs.items()
|
|
131
140
|
if k not in ("scan_mode", "scan_include", "scan_exclude")
|
|
132
141
|
}
|
|
133
142
|
candidates = translator.generate_candidates(
|
|
@@ -151,6 +160,7 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
151
160
|
# 5. Build result
|
|
152
161
|
metrics = {
|
|
153
162
|
"cai": candidates[0]["cai"],
|
|
163
|
+
"cai_authority": dict(translator.cai_authority),
|
|
154
164
|
# Keep both names for compatibility across existing tests/callers.
|
|
155
165
|
"gc_content": candidates[0]["gc"],
|
|
156
166
|
"gc_percent": candidates[0]["gc"],
|
|
@@ -165,9 +175,7 @@ class RuleBasedOptimizer(OptimizerEngine):
|
|
|
165
175
|
metrics.update(
|
|
166
176
|
{
|
|
167
177
|
"gc_target_reached": (
|
|
168
|
-
requested_gc_min_percent
|
|
169
|
-
<= achieved_gc_percent
|
|
170
|
-
<= requested_gc_max_percent
|
|
178
|
+
requested_gc_min_percent <= achieved_gc_percent <= requested_gc_max_percent
|
|
171
179
|
),
|
|
172
180
|
"requested_gc_min_percent": requested_gc_min_percent,
|
|
173
181
|
"requested_gc_max_percent": requested_gc_max_percent,
|
|
@@ -187,16 +187,25 @@ class OptimizationPipeline:
|
|
|
187
187
|
f"Unknown profile: {effective_profile}. Supported profiles: {supported}"
|
|
188
188
|
) from exc
|
|
189
189
|
|
|
190
|
+
candidate_metrics: dict[str, Any]
|
|
190
191
|
if seq_type == "dna":
|
|
191
192
|
optimized_dna = processed
|
|
192
193
|
expected_protein = translate_dna(processed).rstrip("*")
|
|
193
194
|
cai = translator.calculate_cai(optimized_dna)
|
|
194
195
|
gc = translator.calculate_gc_content(optimized_dna)
|
|
195
196
|
score = calculate_composite_score(
|
|
196
|
-
cai=cai,
|
|
197
|
+
cai=cai,
|
|
198
|
+
gc=gc,
|
|
199
|
+
sequence=optimized_dna,
|
|
200
|
+
profile=effective_profile,
|
|
197
201
|
host=effective_host,
|
|
198
202
|
)
|
|
199
|
-
candidate_metrics = {
|
|
203
|
+
candidate_metrics = {
|
|
204
|
+
"cai": cai,
|
|
205
|
+
"cai_authority": dict(translator.cai_authority),
|
|
206
|
+
"gc": gc,
|
|
207
|
+
"score": score,
|
|
208
|
+
}
|
|
200
209
|
else:
|
|
201
210
|
expected_protein = processed.rstrip("*")
|
|
202
211
|
logger.debug(f"Generating candidates with profile: {opt_profile.value}")
|
|
@@ -207,6 +216,7 @@ class OptimizationPipeline:
|
|
|
207
216
|
optimized_dna = candidates[0]["sequence"]
|
|
208
217
|
candidate_metrics = {
|
|
209
218
|
"cai": candidates[0]["cai"],
|
|
219
|
+
"cai_authority": dict(translator.cai_authority),
|
|
210
220
|
"gc": candidates[0]["gc"],
|
|
211
221
|
"score": candidates[0]["score"],
|
|
212
222
|
}
|
|
@@ -216,9 +226,7 @@ class OptimizationPipeline:
|
|
|
216
226
|
)
|
|
217
227
|
|
|
218
228
|
# Fast pre-check avoids an expensive full rule scan before PolyA fixing.
|
|
219
|
-
has_polya_signal = any(
|
|
220
|
-
pattern in optimized_dna for pattern in rule_engine.POLYA_PATTERNS
|
|
221
|
-
)
|
|
229
|
+
has_polya_signal = any(pattern in optimized_dna for pattern in rule_engine.POLYA_PATTERNS)
|
|
222
230
|
if has_polya_signal:
|
|
223
231
|
logger.debug("Potential PolyA signal detected; attempting iterative fix")
|
|
224
232
|
polya_fix = rule_engine.fix_polya_iterative(optimized_dna)
|
|
@@ -239,15 +247,19 @@ class OptimizationPipeline:
|
|
|
239
247
|
dinu_fix = rule_engine.fix_dinucleotides(optimized_dna, mode="balanced")
|
|
240
248
|
if dinu_fix["success"]:
|
|
241
249
|
optimized_dna = dinu_fix["modified_seq"]
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
cai=
|
|
246
|
-
gc=
|
|
250
|
+
cai_after_dinucleotide_fix = round(translator.calculate_cai(optimized_dna), 4)
|
|
251
|
+
gc_after_dinucleotide_fix = translator.calculate_gc_content(optimized_dna)
|
|
252
|
+
score_after_dinucleotide_fix = calculate_composite_score(
|
|
253
|
+
cai=cai_after_dinucleotide_fix,
|
|
254
|
+
gc=gc_after_dinucleotide_fix,
|
|
247
255
|
sequence=optimized_dna,
|
|
248
256
|
profile=effective_profile,
|
|
249
257
|
host=effective_host,
|
|
250
258
|
)
|
|
259
|
+
candidate_metrics["cai"] = cai_after_dinucleotide_fix
|
|
260
|
+
candidate_metrics["cai_authority"] = dict(translator.cai_authority)
|
|
261
|
+
candidate_metrics["gc"] = gc_after_dinucleotide_fix
|
|
262
|
+
candidate_metrics["score"] = score_after_dinucleotide_fix
|
|
251
263
|
logger.info(
|
|
252
264
|
f"Dinucleotide reduction [{dinu_fix['mode']}]: "
|
|
253
265
|
f"{dinu_fix['initial_count']} -> "
|
|
@@ -287,9 +299,7 @@ class OptimizationPipeline:
|
|
|
287
299
|
template_name = construct_template or self.construct_template
|
|
288
300
|
if template_name:
|
|
289
301
|
if self.construct_builder is None:
|
|
290
|
-
template_dir = (
|
|
291
|
-
self.template_dir or get_data_path() / "templates"
|
|
292
|
-
)
|
|
302
|
+
template_dir = self.template_dir or get_data_path() / "templates"
|
|
293
303
|
self.construct_builder = ConstructBuilder(template_dir)
|
|
294
304
|
construct_record = self.construct_builder.generate_construct(
|
|
295
305
|
gene_sequence=domesticated_sequence,
|
|
@@ -13,7 +13,7 @@ import random
|
|
|
13
13
|
import secrets
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import Any, cast
|
|
16
|
+
from typing import Any, TypedDict, cast
|
|
17
17
|
|
|
18
18
|
from factorforge.engines.profile.scoring import (
|
|
19
19
|
calculate_composite_score,
|
|
@@ -23,7 +23,6 @@ from factorforge.engines.profile.utils import (
|
|
|
23
23
|
build_aa_to_codons_map,
|
|
24
24
|
calculate_gc,
|
|
25
25
|
get_data_path,
|
|
26
|
-
load_golden_set,
|
|
27
26
|
resolve_host_codon_table_path,
|
|
28
27
|
)
|
|
29
28
|
from factorforge.utils.exceptions import EmptyCandidateError
|
|
@@ -31,6 +30,16 @@ from factorforge.utils.exceptions import EmptyCandidateError
|
|
|
31
30
|
logger = logging.getLogger(__name__)
|
|
32
31
|
|
|
33
32
|
|
|
33
|
+
class CaiAuthority(TypedDict, total=False):
|
|
34
|
+
"""JSON-serializable CAI reference authority metadata."""
|
|
35
|
+
|
|
36
|
+
reference_id: str | None
|
|
37
|
+
reference_role: str
|
|
38
|
+
reference_version: str | None
|
|
39
|
+
reference_relationship: str
|
|
40
|
+
fallback_used: bool
|
|
41
|
+
|
|
42
|
+
|
|
34
43
|
class OptimizationProfile(Enum):
|
|
35
44
|
"""Optimization profile"""
|
|
36
45
|
|
|
@@ -58,6 +67,8 @@ class ReverseTranslator:
|
|
|
58
67
|
codon_table_path: str | Path | None = None,
|
|
59
68
|
golden_set_path: str | Path | None = None,
|
|
60
69
|
host: str = "nbenthamiana",
|
|
70
|
+
generation_reference_id: str | None = None,
|
|
71
|
+
_allow_golden_set_metadata_fallback: bool = False,
|
|
61
72
|
) -> None:
|
|
62
73
|
"""
|
|
63
74
|
Args:
|
|
@@ -65,6 +76,8 @@ class ReverseTranslator:
|
|
|
65
76
|
golden_set_path: Path to golden set JSON for CAI reference weights.
|
|
66
77
|
If None, attempts to load default golden set.
|
|
67
78
|
host: Host codon table name used when codon_table_path is not provided.
|
|
79
|
+
generation_reference_id: Explicit manifest reference id when the CAI
|
|
80
|
+
reference is intentionally the same table used for generation.
|
|
68
81
|
"""
|
|
69
82
|
self.host = host
|
|
70
83
|
if codon_table_path is None:
|
|
@@ -76,13 +89,38 @@ class ReverseTranslator:
|
|
|
76
89
|
self.aa_to_codons: dict[str, list[tuple[str, float]]] = self._build_aa_to_codons_map()
|
|
77
90
|
|
|
78
91
|
# Load golden set for CAI reference weights
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.golden_set_table =
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
try:
|
|
93
|
+
if golden_set_path is not None:
|
|
94
|
+
self.golden_set_table = self._load_codon_table(golden_set_path)
|
|
95
|
+
else:
|
|
96
|
+
self.golden_set_table = self._load_codon_table(
|
|
97
|
+
get_data_path() / "nbenthamiana_golden_set.json"
|
|
98
|
+
)
|
|
99
|
+
if generation_reference_id is not None:
|
|
100
|
+
self.cai_authority = self._build_generation_reference_cai_authority(
|
|
101
|
+
generation_reference_id
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
try:
|
|
105
|
+
self.cai_authority = self._build_cai_authority(self.golden_set_table)
|
|
106
|
+
except ValueError:
|
|
107
|
+
if not _allow_golden_set_metadata_fallback:
|
|
108
|
+
raise
|
|
109
|
+
self.golden_set_table = self.codon_table
|
|
110
|
+
self.cai_authority = {
|
|
111
|
+
"reference_id": self.codon_table.get("reference_id"),
|
|
112
|
+
"reference_role": "cai_evaluation",
|
|
113
|
+
"reference_relationship": "fallback_to_generation_reference",
|
|
114
|
+
"fallback_used": True,
|
|
115
|
+
}
|
|
116
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
117
|
+
self.golden_set_table = self.codon_table
|
|
118
|
+
self.cai_authority = {
|
|
119
|
+
"reference_id": self.codon_table.get("reference_id"),
|
|
120
|
+
"reference_role": "cai_evaluation",
|
|
121
|
+
"reference_relationship": "fallback_to_generation_reference",
|
|
122
|
+
"fallback_used": True,
|
|
123
|
+
}
|
|
86
124
|
|
|
87
125
|
# Pre-compute relative adaptiveness weights from golden set (Sharp & Li 1987)
|
|
88
126
|
self.golden_ref_weights: dict[str, float] = self._build_ref_weights(self.golden_set_table)
|
|
@@ -132,6 +170,37 @@ class ReverseTranslator:
|
|
|
132
170
|
with open(path, "r", encoding="utf-8") as f:
|
|
133
171
|
return cast(dict[str, Any], json.load(f))
|
|
134
172
|
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _build_cai_authority(golden_set_table: dict[str, Any]) -> CaiAuthority:
|
|
175
|
+
"""Validate and expose golden-set CAI authority metadata."""
|
|
176
|
+
required_fields = ("reference_id", "reference_role", "reference_version")
|
|
177
|
+
missing = [field for field in required_fields if not golden_set_table.get(field)]
|
|
178
|
+
if missing:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"Golden-set CAI authority metadata is missing required field(s): "
|
|
181
|
+
+ ", ".join(missing)
|
|
182
|
+
)
|
|
183
|
+
if golden_set_table["reference_role"] != "cai_evaluation":
|
|
184
|
+
raise ValueError("Golden-set CAI authority reference_role must be 'cai_evaluation'.")
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"reference_id": str(golden_set_table["reference_id"]),
|
|
188
|
+
"reference_role": "cai_evaluation",
|
|
189
|
+
"reference_version": str(golden_set_table["reference_version"]),
|
|
190
|
+
"reference_relationship": "distinct_from_generation_reference",
|
|
191
|
+
"fallback_used": False,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _build_generation_reference_cai_authority(reference_id: str) -> CaiAuthority:
|
|
196
|
+
"""Expose explicit generation-reference reuse as intentional, not fallback."""
|
|
197
|
+
return {
|
|
198
|
+
"reference_id": reference_id,
|
|
199
|
+
"reference_role": "cai_evaluation",
|
|
200
|
+
"reference_relationship": "same_as_generation_reference",
|
|
201
|
+
"fallback_used": False,
|
|
202
|
+
}
|
|
203
|
+
|
|
135
204
|
def _build_aa_to_codons_map(self) -> dict[str, list[tuple[str, float]]]:
|
|
136
205
|
"""
|
|
137
206
|
Build amino-acid-to-codons map
|
|
@@ -520,7 +589,9 @@ class ReverseTranslator:
|
|
|
520
589
|
last_seq = ""
|
|
521
590
|
|
|
522
591
|
balanced_kwargs = {
|
|
523
|
-
k: v
|
|
592
|
+
k: v
|
|
593
|
+
for k, v in kwargs.items()
|
|
594
|
+
if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
|
|
524
595
|
}
|
|
525
596
|
balanced_kwargs["preferred_ratio"] = 0.6
|
|
526
597
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
4
|
Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
|
|
5
5
|
Author-email: Eijex <eijex.lab@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
|
|
|
93
93
|
## Citing
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
FactorForge v3.3.
|
|
96
|
+
FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
|
|
97
97
|
Eijex. https://github.com/eijex/factorforge-cds
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -78,6 +78,7 @@ src/factorforge_cds.egg-info/entry_points.txt
|
|
|
78
78
|
src/factorforge_cds.egg-info/requires.txt
|
|
79
79
|
src/factorforge_cds.egg-info/top_level.txt
|
|
80
80
|
tests/test_baselines.py
|
|
81
|
+
tests/test_benchmark_cli_output_guard.py
|
|
81
82
|
tests/test_benchmark_codon_table_metadata.py
|
|
82
83
|
tests/test_benchmark_regression.py
|
|
83
84
|
tests/test_benchmark_scoring.py
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""CLI output-directory safety tests for the benchmark runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import benchmarks.run_benchmark as run_benchmark
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _invoke_main(monkeypatch: pytest.MonkeyPatch, args: list[str], fake_run) -> None:
|
|
15
|
+
monkeypatch.setattr(sys, "argv", ["run_benchmark.py", *args])
|
|
16
|
+
monkeypatch.setattr(run_benchmark, "run", fake_run)
|
|
17
|
+
run_benchmark.main()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_cli_default_output_dir_stays_v320(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
21
|
+
captured: dict[str, Path] = {}
|
|
22
|
+
|
|
23
|
+
def fake_run(**kwargs) -> None:
|
|
24
|
+
captured["out_csv"] = kwargs["out_csv"]
|
|
25
|
+
captured["out_md"] = kwargs["out_md"]
|
|
26
|
+
|
|
27
|
+
_invoke_main(monkeypatch, ["--force"], fake_run)
|
|
28
|
+
|
|
29
|
+
assert captured["out_csv"] == run_benchmark.DEFAULT_RESULTS_DIR.resolve() / "benchmark_results.csv"
|
|
30
|
+
assert captured["out_md"] == run_benchmark.DEFAULT_RESULTS_DIR.resolve() / "benchmark_summary.md"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_cli_refuses_existing_results_without_force(
|
|
34
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
35
|
+
tmp_path: Path,
|
|
36
|
+
) -> None:
|
|
37
|
+
output_dir = tmp_path / "existing"
|
|
38
|
+
output_dir.mkdir()
|
|
39
|
+
existing_csv = output_dir / "benchmark_results.csv"
|
|
40
|
+
existing_json = output_dir / "benchmark_summary.json"
|
|
41
|
+
existing_csv.write_text("do-not-overwrite\n", encoding="utf-8")
|
|
42
|
+
existing_json.write_text('{"status": "do-not-overwrite"}\n', encoding="utf-8")
|
|
43
|
+
before_csv_hash = hashlib.sha256(existing_csv.read_bytes()).hexdigest()
|
|
44
|
+
before_json_hash = hashlib.sha256(existing_json.read_bytes()).hexdigest()
|
|
45
|
+
before_csv_mtime = existing_csv.stat().st_mtime_ns
|
|
46
|
+
before_json_mtime = existing_json.stat().st_mtime_ns
|
|
47
|
+
|
|
48
|
+
def fail_run(**_kwargs) -> None:
|
|
49
|
+
raise AssertionError("run() must not be called when overwrite guard fails")
|
|
50
|
+
|
|
51
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
52
|
+
_invoke_main(monkeypatch, ["--out-dir", str(output_dir)], fail_run)
|
|
53
|
+
|
|
54
|
+
message = str(exc_info.value)
|
|
55
|
+
assert "Pass --force" in message
|
|
56
|
+
assert "benchmark_results.csv" in message
|
|
57
|
+
assert "benchmark_summary.json" in message
|
|
58
|
+
assert hashlib.sha256(existing_csv.read_bytes()).hexdigest() == before_csv_hash
|
|
59
|
+
assert hashlib.sha256(existing_json.read_bytes()).hexdigest() == before_json_hash
|
|
60
|
+
assert existing_csv.stat().st_mtime_ns == before_csv_mtime
|
|
61
|
+
assert existing_json.stat().st_mtime_ns == before_json_mtime
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_cli_force_allows_existing_results_overwrite(
|
|
65
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
66
|
+
tmp_path: Path,
|
|
67
|
+
) -> None:
|
|
68
|
+
output_dir = tmp_path / "existing"
|
|
69
|
+
output_dir.mkdir()
|
|
70
|
+
(output_dir / "benchmark_results.csv").write_text("old\n", encoding="utf-8")
|
|
71
|
+
(output_dir / "benchmark_summary.json").write_text('{"status": "old"}\n', encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
def fake_run(**kwargs) -> None:
|
|
74
|
+
kwargs["out_csv"].write_text("new\n", encoding="utf-8")
|
|
75
|
+
kwargs["out_md"].write_text("# new\n", encoding="utf-8")
|
|
76
|
+
(kwargs["out_md"].parent / "benchmark_summary.json").write_text(
|
|
77
|
+
'{"status": "new"}\n',
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
_invoke_main(monkeypatch, ["--out-dir", str(output_dir), "--force"], fake_run)
|
|
82
|
+
|
|
83
|
+
assert (output_dir / "benchmark_results.csv").read_text(encoding="utf-8") == "new\n"
|
|
84
|
+
assert (output_dir / "benchmark_summary.md").read_text(encoding="utf-8") == "# new\n"
|
|
85
|
+
assert (output_dir / "benchmark_summary.json").read_text(encoding="utf-8") == (
|
|
86
|
+
'{"status": "new"}\n'
|
|
87
|
+
)
|
|
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.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/validator.py
RENAMED
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_codons.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/templates/high_expression.json
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/wolffia_globosa_codons.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/construct_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/rule_engine.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring_ml.py
RENAMED
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/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.3.1 → factorforge_cds-3.3.2}/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.3.1 → factorforge_cds-3.3.2}/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.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/package_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_codon_table_metadata.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
|
|
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.3.1 → factorforge_cds-3.3.2}/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
|
|
File without changes
|
|
File without changes
|