factorforge-cds 3.2.0__tar.gz → 3.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/PKG-INFO +4 -3
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/README.md +2 -2
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/pyproject.toml +2 -1
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/__init__.py +1 -1
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/reverse_translator.py +1 -1
- factorforge_cds-3.2.1/src/factorforge/protein_risk/__init__.py +5 -0
- factorforge_cds-3.2.1/src/factorforge/protein_risk/annotate.py +32 -0
- factorforge_cds-3.2.1/src/factorforge/protein_risk/kd_scale.py +30 -0
- factorforge_cds-3.2.1/src/factorforge/protein_risk/risk_classifier.py +14 -0
- factorforge_cds-3.2.1/src/factorforge/protein_risk/sp_predict.py +18 -0
- factorforge_cds-3.2.1/src/factorforge/protein_risk/tm_predict.py +30 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/design_package.py +12 -1
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/design_package.schema.json +36 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/PKG-INFO +4 -3
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/SOURCES.txt +10 -1
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/requires.txt +1 -0
- factorforge_cds-3.2.1/tests/test_benchmark_scoring.py +98 -0
- factorforge_cds-3.2.1/tests/test_docs_consistency.py +143 -0
- factorforge_cds-3.2.1/tests/test_protein_risk.py +99 -0
- factorforge_cds-3.2.1/tests/test_worked_example.py +240 -0
- factorforge_cds-3.2.0/tests/test_benchmark_scoring.py +0 -15
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/LICENSE +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/setup.cfg +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/__main__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/feasibility.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/metrics.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/legacy_cli.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/main.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/exporter.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/optimizer.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/validator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/nbenthamiana_codons.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/ntabacum_codons.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/templates/high_expression.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/templates/standard_expression.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/database.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/construct_builder.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/exporter.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/optimizer.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/pipeline.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/scoring.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/scoring_ml.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/utils.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/validator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/registry.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/fasta.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/validation.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/registry/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/registry/registry_loader.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/construct_id.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/exceptions.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/restriction_sites.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/sequence_validator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/validation.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/__init__.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/cli.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/package_generator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/top_level.txt +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_baselines.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_codon_table_metadata.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_regression.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_smoke.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_cai.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_codon_table_manifest.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_database.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_schema.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_semantics.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_serialization.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_fasta_io.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_gc_content.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_host_profile_metadata.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_iupac_validation.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_legacy_cli.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_no_raw_sequence_logging.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_openbio_missing_metric_contract.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_parameter_registry.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_registry_production_sync.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_restriction_sites.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_sequence_validator.py +0 -0
- {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_translation_integrity.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.1
|
|
4
4
|
Summary: FactorForge - open-source constraint-based CDS design engine by Eijex.
|
|
5
5
|
Author-email: Eijex <eijex.lab@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -21,6 +21,7 @@ Requires-Dist: click>=8.0
|
|
|
21
21
|
Requires-Dist: pydantic>=2.0
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pandas>=2.0; extra == "dev"
|
|
24
25
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
26
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
26
27
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
@@ -39,7 +40,7 @@ Dynamic: license-file
|
|
|
39
40
|
[](https://pypi.org/project/factorforge-cds/)
|
|
40
41
|
[](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
|
|
41
42
|
[](https://codecov.io/gh/eijex/factorforge-cds)
|
|
42
|
-
[](https://doi.org/10.5281/zenodo.20640931)
|
|
43
44
|
[](https://factorforge.eijex.com)
|
|
44
45
|
|
|
45
46
|
FactorForge performs profile-guided CDS design with CAI/GC metrics, PolyA-signal screening, and Golden Gate/MoClo-aware checks. Primary support: *N. benthamiana* (agroinfiltration). Experimental host context: Tobacco BY-2 (`--host by2`).
|
|
@@ -113,7 +114,7 @@ GNU Affero General Public License v3.0 — see [LICENSE](LICENSE).
|
|
|
113
114
|
## Get in Touch
|
|
114
115
|
|
|
115
116
|
- **Docs** — [eijex.github.io/factorforge-cds](https://eijex.github.io/factorforge-cds/)
|
|
116
|
-
- **Wet-lab Results** — Public-safe validation summaries are welcome. Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
117
|
+
- **Wet-lab Results** — Public-safe validation summaries are welcome. [Share Wet-lab Results (Form)](https://docs.google.com/forms/d/e/1FAIpQLSeSx-wYvF6YwHhSPdLMl-L44frCugdm25X_eDz50OaqTD66qA/viewform) or [Share Wet-lab Results (GitHub)](https://github.com/eijex/factorforge-cds/issues/new?template=wet_lab_result.yml) (public-safe summaries only). Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
117
118
|
- **GitHub Issues** — bugs, features: [github.com/eijex/factorforge-cds/issues](https://github.com/eijex/factorforge-cds/issues)
|
|
118
119
|
- **Email** — eijex.lab@gmail.com
|
|
119
120
|
- **FactorForge** — [factorforge.eijex.com](https://factorforge.eijex.com)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://pypi.org/project/factorforge-cds/)
|
|
8
8
|
[](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
|
|
9
9
|
[](https://codecov.io/gh/eijex/factorforge-cds)
|
|
10
|
-
[](https://doi.org/10.5281/zenodo.20640931)
|
|
11
11
|
[](https://factorforge.eijex.com)
|
|
12
12
|
|
|
13
13
|
FactorForge performs profile-guided CDS design with CAI/GC metrics, PolyA-signal screening, and Golden Gate/MoClo-aware checks. Primary support: *N. benthamiana* (agroinfiltration). Experimental host context: Tobacco BY-2 (`--host by2`).
|
|
@@ -81,7 +81,7 @@ GNU Affero General Public License v3.0 — see [LICENSE](LICENSE).
|
|
|
81
81
|
## Get in Touch
|
|
82
82
|
|
|
83
83
|
- **Docs** — [eijex.github.io/factorforge-cds](https://eijex.github.io/factorforge-cds/)
|
|
84
|
-
- **Wet-lab Results** — Public-safe validation summaries are welcome. Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
84
|
+
- **Wet-lab Results** — Public-safe validation summaries are welcome. [Share Wet-lab Results (Form)](https://docs.google.com/forms/d/e/1FAIpQLSeSx-wYvF6YwHhSPdLMl-L44frCugdm25X_eDz50OaqTD66qA/viewform) or [Share Wet-lab Results (GitHub)](https://github.com/eijex/factorforge-cds/issues/new?template=wet_lab_result.yml) (public-safe summaries only). Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
85
85
|
- **GitHub Issues** — bugs, features: [github.com/eijex/factorforge-cds/issues](https://github.com/eijex/factorforge-cds/issues)
|
|
86
86
|
- **Email** — eijex.lab@gmail.com
|
|
87
87
|
- **FactorForge** — [factorforge.eijex.com](https://factorforge.eijex.com)
|
|
@@ -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.1"
|
|
8
8
|
description = "FactorForge - open-source constraint-based CDS design engine by Eijex."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-only"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = [
|
|
31
31
|
"jsonschema>=4.0",
|
|
32
|
+
"pandas>=2.0",
|
|
32
33
|
"pytest>=7.0",
|
|
33
34
|
"pytest-cov>=4.0",
|
|
34
35
|
"ruff>=0.1",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Compose protein structural-risk annotations."""
|
|
2
|
+
|
|
3
|
+
from factorforge.protein_risk.risk_classifier import classify_risk
|
|
4
|
+
from factorforge.protein_risk.sp_predict import predict_signal_peptide
|
|
5
|
+
from factorforge.protein_risk.tm_predict import predict_tm_segments
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def annotate_protein_risk(protein_seq: str) -> dict:
|
|
9
|
+
"""Return deterministic structural-risk indicators without sequence material."""
|
|
10
|
+
tm_prediction = predict_tm_segments(protein_seq)
|
|
11
|
+
sp_predicted = predict_signal_peptide(protein_seq)
|
|
12
|
+
risk_level = classify_risk(
|
|
13
|
+
tm_prediction["tm_count"],
|
|
14
|
+
tm_prediction["mean_kd_score"],
|
|
15
|
+
sp_predicted,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
warnings = []
|
|
19
|
+
if risk_level != "LOW":
|
|
20
|
+
warnings.append(
|
|
21
|
+
"Protein structural risk indicator: "
|
|
22
|
+
f"{risk_level} (tm_count={tm_prediction['tm_count']}). "
|
|
23
|
+
"CDS-level checks are independent of protein structural risk."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"tm_count": tm_prediction["tm_count"],
|
|
28
|
+
"mean_kd_score": tm_prediction["mean_kd_score"],
|
|
29
|
+
"signal_peptide_predicted": sp_predicted,
|
|
30
|
+
"risk_level": risk_level,
|
|
31
|
+
"warnings": warnings,
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Kyte-Doolittle scale and thresholds used by protein-risk heuristics."""
|
|
2
|
+
|
|
3
|
+
KD_SCALE = {
|
|
4
|
+
"A": 1.8,
|
|
5
|
+
"R": -4.5,
|
|
6
|
+
"N": -3.5,
|
|
7
|
+
"D": -3.5,
|
|
8
|
+
"C": 2.5,
|
|
9
|
+
"Q": -3.5,
|
|
10
|
+
"E": -3.5,
|
|
11
|
+
"G": -0.4,
|
|
12
|
+
"H": -3.2,
|
|
13
|
+
"I": 4.5,
|
|
14
|
+
"L": 3.8,
|
|
15
|
+
"K": -3.9,
|
|
16
|
+
"M": 1.9,
|
|
17
|
+
"F": 2.8,
|
|
18
|
+
"P": -1.6,
|
|
19
|
+
"S": -0.8,
|
|
20
|
+
"T": -0.7,
|
|
21
|
+
"W": -0.9,
|
|
22
|
+
"Y": -1.3,
|
|
23
|
+
"V": 4.2,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
TM_WINDOW = 19
|
|
27
|
+
TM_THRESHOLD = 1.6
|
|
28
|
+
MEAN_KD_RISK_THRESHOLD = 0.3
|
|
29
|
+
SP_HRUN_MIN = 7
|
|
30
|
+
SP_SCAN_WINDOW = 30
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Protein structural-risk classification rules."""
|
|
2
|
+
|
|
3
|
+
from factorforge.protein_risk.kd_scale import MEAN_KD_RISK_THRESHOLD
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def classify_risk(tm_count: int, mean_kd: float, sp_predicted: bool) -> str:
|
|
7
|
+
"""Classify deterministic structural-risk indicators."""
|
|
8
|
+
if tm_count >= 3 or (
|
|
9
|
+
tm_count >= 1 and mean_kd >= MEAN_KD_RISK_THRESHOLD and not sp_predicted
|
|
10
|
+
):
|
|
11
|
+
return "HIGH"
|
|
12
|
+
if tm_count >= 1:
|
|
13
|
+
return "MEDIUM"
|
|
14
|
+
return "LOW"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Deterministic N-terminal signal-peptide heuristic."""
|
|
2
|
+
|
|
3
|
+
from factorforge.protein_risk.kd_scale import SP_HRUN_MIN, SP_SCAN_WINDOW
|
|
4
|
+
|
|
5
|
+
HYDROPHOBIC_RESIDUES = frozenset("ILVAMFW")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def predict_signal_peptide(protein_seq: str) -> bool:
|
|
9
|
+
"""Return whether an N-terminal hydrophobic run meets the heuristic."""
|
|
10
|
+
run_length = 0
|
|
11
|
+
for residue in protein_seq.upper()[:SP_SCAN_WINDOW]:
|
|
12
|
+
if residue in HYDROPHOBIC_RESIDUES:
|
|
13
|
+
run_length += 1
|
|
14
|
+
if run_length >= SP_HRUN_MIN:
|
|
15
|
+
return True
|
|
16
|
+
else:
|
|
17
|
+
run_length = 0
|
|
18
|
+
return False
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Deterministic transmembrane-segment heuristic."""
|
|
2
|
+
|
|
3
|
+
from factorforge.protein_risk.kd_scale import KD_SCALE, TM_THRESHOLD, TM_WINDOW
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def predict_tm_segments(protein_seq: str) -> dict:
|
|
7
|
+
"""Return merged hydrophobic windows and whole-protein mean KD score."""
|
|
8
|
+
if not protein_seq:
|
|
9
|
+
raise ValueError("protein_seq must not be empty")
|
|
10
|
+
|
|
11
|
+
scores = [KD_SCALE.get(residue, 0.0) for residue in protein_seq.upper()]
|
|
12
|
+
candidate_segments: list[tuple[int, int]] = []
|
|
13
|
+
for start in range(max(0, len(scores) - TM_WINDOW + 1)):
|
|
14
|
+
window_mean = sum(scores[start : start + TM_WINDOW]) / TM_WINDOW
|
|
15
|
+
if window_mean >= TM_THRESHOLD:
|
|
16
|
+
candidate_segments.append((start, start + TM_WINDOW))
|
|
17
|
+
|
|
18
|
+
merged_segments: list[tuple[int, int]] = []
|
|
19
|
+
for start, end in candidate_segments:
|
|
20
|
+
if merged_segments and start <= merged_segments[-1][1]:
|
|
21
|
+
previous_start, previous_end = merged_segments[-1]
|
|
22
|
+
merged_segments[-1] = (previous_start, max(previous_end, end))
|
|
23
|
+
else:
|
|
24
|
+
merged_segments.append((start, end))
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"tm_count": len(merged_segments),
|
|
28
|
+
"mean_kd_score": sum(scores) / len(scores),
|
|
29
|
+
"segments": merged_segments,
|
|
30
|
+
}
|
|
@@ -17,7 +17,7 @@ Separation rationale:
|
|
|
17
17
|
before any output is published or shared externally.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
from typing import Any, Optional
|
|
20
|
+
from typing import Any, Literal, Optional
|
|
21
21
|
|
|
22
22
|
from pydantic import BaseModel, ConfigDict, Field
|
|
23
23
|
|
|
@@ -90,6 +90,16 @@ class WetLabFeedback(BaseModel):
|
|
|
90
90
|
submissions: list[Any] = Field(default_factory=list)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
class ProteinRisk(BaseModel):
|
|
94
|
+
model_config = ConfigDict(extra="forbid")
|
|
95
|
+
|
|
96
|
+
tm_count: int = Field(ge=0)
|
|
97
|
+
mean_kd_score: float
|
|
98
|
+
signal_peptide_predicted: bool
|
|
99
|
+
risk_level: Literal["LOW", "MEDIUM", "HIGH"]
|
|
100
|
+
warnings: list[str] = Field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
93
103
|
class DesignPackage(BaseModel):
|
|
94
104
|
model_config = ConfigDict(extra="allow")
|
|
95
105
|
|
|
@@ -103,3 +113,4 @@ class DesignPackage(BaseModel):
|
|
|
103
113
|
validation_status: Optional[ValidationStatus] = None
|
|
104
114
|
provenance: Provenance
|
|
105
115
|
wet_lab_feedback: WetLabFeedback = Field(default_factory=WetLabFeedback)
|
|
116
|
+
protein_risk: Optional[ProteinRisk] = None
|
{factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/design_package.schema.json
RENAMED
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
},
|
|
52
52
|
"claim_boundary": {
|
|
53
53
|
"$ref": "#/$defs/claimBoundary"
|
|
54
|
+
},
|
|
55
|
+
"protein_risk": {
|
|
56
|
+
"$ref": "#/$defs/proteinRisk"
|
|
54
57
|
}
|
|
55
58
|
},
|
|
56
59
|
"allOf": [
|
|
@@ -134,6 +137,39 @@
|
|
|
134
137
|
}
|
|
135
138
|
],
|
|
136
139
|
"$defs": {
|
|
140
|
+
"proteinRisk": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"additionalProperties": false,
|
|
143
|
+
"required": [
|
|
144
|
+
"tm_count",
|
|
145
|
+
"mean_kd_score",
|
|
146
|
+
"signal_peptide_predicted",
|
|
147
|
+
"risk_level",
|
|
148
|
+
"warnings"
|
|
149
|
+
],
|
|
150
|
+
"properties": {
|
|
151
|
+
"tm_count": {
|
|
152
|
+
"type": "integer",
|
|
153
|
+
"minimum": 0
|
|
154
|
+
},
|
|
155
|
+
"mean_kd_score": {
|
|
156
|
+
"type": "number"
|
|
157
|
+
},
|
|
158
|
+
"signal_peptide_predicted": {
|
|
159
|
+
"type": "boolean"
|
|
160
|
+
},
|
|
161
|
+
"risk_level": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"enum": ["LOW", "MEDIUM", "HIGH"]
|
|
164
|
+
},
|
|
165
|
+
"warnings": {
|
|
166
|
+
"type": "array",
|
|
167
|
+
"items": {
|
|
168
|
+
"type": "string"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
137
173
|
"hostProfile": {
|
|
138
174
|
"type": "object",
|
|
139
175
|
"additionalProperties": false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: factorforge-cds
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: FactorForge - open-source constraint-based CDS design engine by Eijex.
|
|
5
5
|
Author-email: Eijex <eijex.lab@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -21,6 +21,7 @@ Requires-Dist: click>=8.0
|
|
|
21
21
|
Requires-Dist: pydantic>=2.0
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pandas>=2.0; extra == "dev"
|
|
24
25
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
26
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
26
27
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
@@ -39,7 +40,7 @@ Dynamic: license-file
|
|
|
39
40
|
[](https://pypi.org/project/factorforge-cds/)
|
|
40
41
|
[](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
|
|
41
42
|
[](https://codecov.io/gh/eijex/factorforge-cds)
|
|
42
|
-
[](https://doi.org/10.5281/zenodo.20640931)
|
|
43
44
|
[](https://factorforge.eijex.com)
|
|
44
45
|
|
|
45
46
|
FactorForge performs profile-guided CDS design with CAI/GC metrics, PolyA-signal screening, and Golden Gate/MoClo-aware checks. Primary support: *N. benthamiana* (agroinfiltration). Experimental host context: Tobacco BY-2 (`--host by2`).
|
|
@@ -113,7 +114,7 @@ GNU Affero General Public License v3.0 — see [LICENSE](LICENSE).
|
|
|
113
114
|
## Get in Touch
|
|
114
115
|
|
|
115
116
|
- **Docs** — [eijex.github.io/factorforge-cds](https://eijex.github.io/factorforge-cds/)
|
|
116
|
-
- **Wet-lab Results** — Public-safe validation summaries are welcome. Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
117
|
+
- **Wet-lab Results** — Public-safe validation summaries are welcome. [Share Wet-lab Results (Form)](https://docs.google.com/forms/d/e/1FAIpQLSeSx-wYvF6YwHhSPdLMl-L44frCugdm25X_eDz50OaqTD66qA/viewform) or [Share Wet-lab Results (GitHub)](https://github.com/eijex/factorforge-cds/issues/new?template=wet_lab_result.yml) (public-safe summaries only). Do not submit raw sequences, confidential construct details, internal batch IDs, patient data, private contact information, exact process parameters, or confidential partner/customer data. See [VALIDATION.md](VALIDATION.md) before submitting.
|
|
117
118
|
- **GitHub Issues** — bugs, features: [github.com/eijex/factorforge-cds/issues](https://github.com/eijex/factorforge-cds/issues)
|
|
118
119
|
- **Email** — eijex.lab@gmail.com
|
|
119
120
|
- **FactorForge** — [factorforge.eijex.com](https://factorforge.eijex.com)
|
|
@@ -39,6 +39,12 @@ src/factorforge/engines/profile/rules/rule_engine.py
|
|
|
39
39
|
src/factorforge/io/__init__.py
|
|
40
40
|
src/factorforge/io/fasta.py
|
|
41
41
|
src/factorforge/io/validation.py
|
|
42
|
+
src/factorforge/protein_risk/__init__.py
|
|
43
|
+
src/factorforge/protein_risk/annotate.py
|
|
44
|
+
src/factorforge/protein_risk/kd_scale.py
|
|
45
|
+
src/factorforge/protein_risk/risk_classifier.py
|
|
46
|
+
src/factorforge/protein_risk/sp_predict.py
|
|
47
|
+
src/factorforge/protein_risk/tm_predict.py
|
|
42
48
|
src/factorforge/registry/__init__.py
|
|
43
49
|
src/factorforge/registry/registry_loader.py
|
|
44
50
|
src/factorforge/schemas/__init__.py
|
|
@@ -70,6 +76,7 @@ tests/test_database.py
|
|
|
70
76
|
tests/test_design_package_schema.py
|
|
71
77
|
tests/test_design_package_semantics.py
|
|
72
78
|
tests/test_design_package_serialization.py
|
|
79
|
+
tests/test_docs_consistency.py
|
|
73
80
|
tests/test_fasta_io.py
|
|
74
81
|
tests/test_gc_content.py
|
|
75
82
|
tests/test_host_profile_metadata.py
|
|
@@ -78,7 +85,9 @@ tests/test_legacy_cli.py
|
|
|
78
85
|
tests/test_no_raw_sequence_logging.py
|
|
79
86
|
tests/test_openbio_missing_metric_contract.py
|
|
80
87
|
tests/test_parameter_registry.py
|
|
88
|
+
tests/test_protein_risk.py
|
|
81
89
|
tests/test_registry_production_sync.py
|
|
82
90
|
tests/test_restriction_sites.py
|
|
83
91
|
tests/test_sequence_validator.py
|
|
84
|
-
tests/test_translation_integrity.py
|
|
92
|
+
tests/test_translation_integrity.py
|
|
93
|
+
tests/test_worked_example.py
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pytest
|
|
4
|
+
from benchmarks.config import load_benchmark_config
|
|
5
|
+
from benchmarks.scoring import score_cds, canonical_multi_constraint_pass
|
|
6
|
+
|
|
7
|
+
CFG = load_benchmark_config()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_score_cds_full_schema():
|
|
11
|
+
row = score_cds("random_synonymous", "baseline", "p1", "MKT", "ATGAAAACC", CFG, runtime_seconds=0.01)
|
|
12
|
+
expected = {"method","method_type","sequence_id","aa_identity","internal_stop_count",
|
|
13
|
+
"invalid_codon_count","length_multiple_of_three","cai","gc_percent",
|
|
14
|
+
"gc_in_target_range","forbidden_type_iis_site_count","biological_pass",
|
|
15
|
+
"assembly_pass","multi_constraint_pass","runtime_seconds"}
|
|
16
|
+
assert expected.issubset(row.keys())
|
|
17
|
+
assert row["biological_pass"] is True
|
|
18
|
+
assert row["aa_identity"] == 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --- scoring_contract v1.1 semantic invariant tests ---
|
|
22
|
+
|
|
23
|
+
def test_multi_constraint_pass_requires_gc_in_range():
|
|
24
|
+
"""multi_constraint_pass=True implies gc_in_target_range=True (scoring_contract v1.1)."""
|
|
25
|
+
# Use a very narrow GC window that forces gc_in_target_range=False for a typical CDS
|
|
26
|
+
cfg_narrow = dataclasses.replace(CFG, gc_min=99.0, gc_max=100.0)
|
|
27
|
+
row = score_cds("greedy_cai", "baseline", "t1", "MKT", "ATGAAAACC", cfg_narrow, runtime_seconds=0.01)
|
|
28
|
+
assert row["gc_in_target_range"] is False
|
|
29
|
+
assert row["multi_constraint_pass"] is False, (
|
|
30
|
+
"multi_constraint_pass must be False when gc_in_target_range is False (scoring_contract v1.1)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_multi_constraint_pass_true_only_when_all_primitives_pass():
|
|
35
|
+
"""multi_constraint_pass=True only when biological_pass AND assembly_pass AND gc_in_target_range."""
|
|
36
|
+
# MKT codon ATGAAAACC: GC = (0+0+0+0+0+1+0+1+1)/9 = 3/9 ~ 33%
|
|
37
|
+
# Default gc_min=55, gc_max=65 → gc_in_target_range=False → multi_constraint_pass=False
|
|
38
|
+
row = score_cds("greedy_cai", "baseline", "t2", "MKT", "ATGAAAACC", CFG, runtime_seconds=0.01)
|
|
39
|
+
if not row["gc_in_target_range"]:
|
|
40
|
+
assert row["multi_constraint_pass"] is False
|
|
41
|
+
if not row["assembly_pass"]:
|
|
42
|
+
assert row["multi_constraint_pass"] is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_canonical_helper_gc_fail_implies_false():
|
|
46
|
+
"""canonical_multi_constraint_pass: gc_in_target_range=False → result=False."""
|
|
47
|
+
df = pd.DataFrame([{
|
|
48
|
+
"biological_pass": True,
|
|
49
|
+
"assembly_pass": True,
|
|
50
|
+
"gc_in_target_range": False,
|
|
51
|
+
}])
|
|
52
|
+
result = canonical_multi_constraint_pass(df, gc_min=55.0, gc_max=65.0)
|
|
53
|
+
assert bool(result.iloc[0]) is False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_canonical_helper_all_pass_implies_true():
|
|
57
|
+
"""canonical_multi_constraint_pass: all primitives True → result=True."""
|
|
58
|
+
df = pd.DataFrame([{
|
|
59
|
+
"biological_pass": True,
|
|
60
|
+
"assembly_pass": True,
|
|
61
|
+
"gc_in_target_range": True,
|
|
62
|
+
}])
|
|
63
|
+
result = canonical_multi_constraint_pass(df, gc_min=55.0, gc_max=65.0)
|
|
64
|
+
assert bool(result.iloc[0]) is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_canonical_helper_assembly_fail_implies_false():
|
|
68
|
+
"""canonical_multi_constraint_pass: assembly_pass=False → result=False."""
|
|
69
|
+
df = pd.DataFrame([{
|
|
70
|
+
"biological_pass": True,
|
|
71
|
+
"assembly_pass": False,
|
|
72
|
+
"gc_in_target_range": True,
|
|
73
|
+
}])
|
|
74
|
+
result = canonical_multi_constraint_pass(df, gc_min=55.0, gc_max=65.0)
|
|
75
|
+
assert bool(result.iloc[0]) is False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_canonical_helper_biological_fail_implies_false():
|
|
79
|
+
"""canonical_multi_constraint_pass: biological_pass=False → result=False."""
|
|
80
|
+
df = pd.DataFrame([{
|
|
81
|
+
"biological_pass": False,
|
|
82
|
+
"assembly_pass": True,
|
|
83
|
+
"gc_in_target_range": True,
|
|
84
|
+
}])
|
|
85
|
+
result = canonical_multi_constraint_pass(df, gc_min=55.0, gc_max=65.0)
|
|
86
|
+
assert bool(result.iloc[0]) is False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_canonical_helper_nan_treated_as_false():
|
|
90
|
+
"""canonical_multi_constraint_pass: NaN in any primitive → result=False."""
|
|
91
|
+
import numpy as np
|
|
92
|
+
df = pd.DataFrame([{
|
|
93
|
+
"biological_pass": True,
|
|
94
|
+
"assembly_pass": True,
|
|
95
|
+
"gc_in_target_range": float("nan"),
|
|
96
|
+
}])
|
|
97
|
+
result = canonical_multi_constraint_pass(df, gc_min=55.0, gc_max=65.0)
|
|
98
|
+
assert bool(result.iloc[0]) is False
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Docs-as-Code consistency guardrails (job 108).
|
|
2
|
+
|
|
3
|
+
Checks that key documentation files stay in sync with their source-of-truth
|
|
4
|
+
artifacts. Fails CI if claim wording, ablation layer definitions, or
|
|
5
|
+
reproducibility anchors drift.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
BENCH_DIR = ROOT / "reproducibility" / "benchmark_v0.5.1"
|
|
17
|
+
ABLATION_SPEC = ROOT / "benchmarks" / "ablation" / "ablation_spec.yaml"
|
|
18
|
+
BENCHMARK_README = BENCH_DIR / "README.md"
|
|
19
|
+
MANIFEST_PATH = BENCH_DIR / "MANIFEST.json"
|
|
20
|
+
FROZEN_SUMMARY = BENCH_DIR / "data" / "benchmark_summary.frozen.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Reproducibility anchor files
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def test_manifest_json_exists_and_valid():
|
|
28
|
+
assert MANIFEST_PATH.exists(), f"Missing: {MANIFEST_PATH}"
|
|
29
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
30
|
+
for key in ("factorforge_version", "git_commit", "scoring_contract", "inputs", "outputs"):
|
|
31
|
+
assert key in data, f"MANIFEST.json missing key: {key}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_benchmark_summary_frozen_exists():
|
|
35
|
+
assert FROZEN_SUMMARY.exists(), f"Missing: {FROZEN_SUMMARY}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_manifest_scoring_contract_v1_1():
|
|
39
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
40
|
+
assert data["scoring_contract"] == "v1.1", (
|
|
41
|
+
f"Expected scoring_contract='v1.1', got {data['scoring_contract']!r}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_manifest_inputs_sha256_non_empty():
|
|
46
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
47
|
+
for name, entry in data["inputs"].items():
|
|
48
|
+
sha = entry.get("sha256", "")
|
|
49
|
+
assert sha and len(sha) == 64, (
|
|
50
|
+
f"MANIFEST.json inputs[{name!r}].sha256 is missing or invalid"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_manifest_input_sha256_matches_files():
|
|
55
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
56
|
+
for name, entry in data["inputs"].items():
|
|
57
|
+
path = ROOT / entry["path"]
|
|
58
|
+
actual = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
59
|
+
assert actual == entry["sha256"], f"MANIFEST.json hash drift for {name}: {path}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_manifest_commands_use_reproducible_entrypoints():
|
|
63
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
64
|
+
commands = "\n".join(data["commands"])
|
|
65
|
+
assert "python scripts/benchmark.py" not in commands
|
|
66
|
+
assert "python -m benchmarks.run_benchmark" in commands
|
|
67
|
+
assert "--dataset nbenthamiana_full --mode formal --seed 320" in commands
|
|
68
|
+
assert "python -m benchmarks.ablation.run_ablation" in commands
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_manifest_separates_software_and_benchmark_dois():
|
|
72
|
+
data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
73
|
+
assert data["archives"]["software_release"]["doi"] == "10.5281/zenodo.20640931"
|
|
74
|
+
assert data["archives"]["corrected_benchmark_dataset"]["doi"] == "10.5281/zenodo.20676276"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Ablation spec ↔ benchmark README consistency
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
@pytest.fixture(scope="module")
|
|
82
|
+
def ablation_spec():
|
|
83
|
+
return yaml.safe_load(ABLATION_SPEC.read_text(encoding="utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture(scope="module")
|
|
87
|
+
def benchmark_readme():
|
|
88
|
+
return BENCHMARK_README.read_text(encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_ablation_spec_layers_in_benchmark_readme(ablation_spec, benchmark_readme):
|
|
92
|
+
"""Every layer key (L0–L5) from ablation_spec.yaml must appear in the benchmark README."""
|
|
93
|
+
layers = ablation_spec.get("layers", {})
|
|
94
|
+
assert layers, "ablation_spec.yaml has no 'layers' section"
|
|
95
|
+
for level in layers:
|
|
96
|
+
assert level in benchmark_readme, (
|
|
97
|
+
f"ablation_spec layer '{level}' not documented in benchmark README"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_ablation_spec_layer_names_in_benchmark_readme(ablation_spec, benchmark_readme):
|
|
102
|
+
"""Every layer 'name' value from ablation_spec.yaml must appear in the benchmark README."""
|
|
103
|
+
layers = ablation_spec.get("layers", {})
|
|
104
|
+
for level, cfg in layers.items():
|
|
105
|
+
name = cfg.get("name", "")
|
|
106
|
+
assert name and name in benchmark_readme, (
|
|
107
|
+
f"Layer {level} name '{name}' not found in benchmark README"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Evidence boundary / claim wording
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def test_main_readme_evidence_boundary():
|
|
116
|
+
"""Main README must contain in-silico or computational disclaimer."""
|
|
117
|
+
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
118
|
+
assert "in-silico" in readme or "computational" in readme, (
|
|
119
|
+
"Main README missing evidence boundary disclaimer (in-silico / computational)"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_benchmark_readme_evidence_boundary(benchmark_readme):
|
|
124
|
+
"""Benchmark README must contain in-silico or computational disclaimer."""
|
|
125
|
+
assert "in-silico" in benchmark_readme or "computational" in benchmark_readme, (
|
|
126
|
+
"Benchmark README missing evidence boundary disclaimer"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_benchmark_readme_no_wet_lab_claim(benchmark_readme):
|
|
131
|
+
"""Benchmark README must not contain unsupported wet-lab outcome claims."""
|
|
132
|
+
forbidden = [
|
|
133
|
+
"predicts expression",
|
|
134
|
+
"predicts yield",
|
|
135
|
+
"wet-lab success",
|
|
136
|
+
"guarantees cloning",
|
|
137
|
+
"guarantees synthesis",
|
|
138
|
+
"predicts synthesis success",
|
|
139
|
+
]
|
|
140
|
+
for phrase in forbidden:
|
|
141
|
+
assert phrase.lower() not in benchmark_readme.lower(), (
|
|
142
|
+
f"Benchmark README contains forbidden claim phrase: {phrase!r}"
|
|
143
|
+
)
|