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.
Files changed (96) hide show
  1. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/PKG-INFO +4 -3
  2. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/README.md +2 -2
  3. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/pyproject.toml +2 -1
  4. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/__init__.py +1 -1
  5. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/reverse_translator.py +1 -1
  6. factorforge_cds-3.2.1/src/factorforge/protein_risk/__init__.py +5 -0
  7. factorforge_cds-3.2.1/src/factorforge/protein_risk/annotate.py +32 -0
  8. factorforge_cds-3.2.1/src/factorforge/protein_risk/kd_scale.py +30 -0
  9. factorforge_cds-3.2.1/src/factorforge/protein_risk/risk_classifier.py +14 -0
  10. factorforge_cds-3.2.1/src/factorforge/protein_risk/sp_predict.py +18 -0
  11. factorforge_cds-3.2.1/src/factorforge/protein_risk/tm_predict.py +30 -0
  12. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/design_package.py +12 -1
  13. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/design_package.schema.json +36 -0
  14. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/PKG-INFO +4 -3
  15. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/SOURCES.txt +10 -1
  16. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/requires.txt +1 -0
  17. factorforge_cds-3.2.1/tests/test_benchmark_scoring.py +98 -0
  18. factorforge_cds-3.2.1/tests/test_docs_consistency.py +143 -0
  19. factorforge_cds-3.2.1/tests/test_protein_risk.py +99 -0
  20. factorforge_cds-3.2.1/tests/test_worked_example.py +240 -0
  21. factorforge_cds-3.2.0/tests/test_benchmark_scoring.py +0 -15
  22. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/LICENSE +0 -0
  23. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/setup.cfg +0 -0
  24. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/__main__.py +0 -0
  25. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/__init__.py +0 -0
  26. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/feasibility.py +0 -0
  27. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/analysis/metrics.py +0 -0
  28. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/__init__.py +0 -0
  29. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/legacy_cli.py +0 -0
  30. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/cli/main.py +0 -0
  31. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/__init__.py +0 -0
  32. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/exporter.py +0 -0
  33. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/optimizer.py +0 -0
  34. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/core/interfaces/validator.py +0 -0
  35. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/nbenthamiana_codons.json +0 -0
  36. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
  37. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/ntabacum_codons.json +0 -0
  38. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/templates/high_expression.json +0 -0
  39. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/templates/standard_expression.json +0 -0
  40. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
  41. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/database.py +0 -0
  42. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/__init__.py +0 -0
  43. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/__init__.py +0 -0
  44. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
  45. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/construct_builder.py +0 -0
  46. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/exporter.py +0 -0
  47. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/optimizer.py +0 -0
  48. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/pipeline.py +0 -0
  49. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/__init__.py +0 -0
  50. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
  51. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
  52. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/scoring.py +0 -0
  53. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/scoring_ml.py +0 -0
  54. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/utils.py +0 -0
  55. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/profile/validator.py +0 -0
  56. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/engines/registry.py +0 -0
  57. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/__init__.py +0 -0
  58. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/fasta.py +0 -0
  59. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/io/validation.py +0 -0
  60. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/registry/__init__.py +0 -0
  61. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/registry/registry_loader.py +0 -0
  62. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/schemas/__init__.py +0 -0
  63. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/__init__.py +0 -0
  64. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/construct_id.py +0 -0
  65. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/exceptions.py +0 -0
  66. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/restriction_sites.py +0 -0
  67. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/sequence_validator.py +0 -0
  68. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/utils/validation.py +0 -0
  69. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/__init__.py +0 -0
  70. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/cli.py +0 -0
  71. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge/validation/package_generator.py +0 -0
  72. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
  73. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
  74. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/src/factorforge_cds.egg-info/top_level.txt +0 -0
  75. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_baselines.py +0 -0
  76. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_codon_table_metadata.py +0 -0
  77. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_regression.py +0 -0
  78. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_benchmark_smoke.py +0 -0
  79. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_cai.py +0 -0
  80. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_codon_table_manifest.py +0 -0
  81. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_database.py +0 -0
  82. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_schema.py +0 -0
  83. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_semantics.py +0 -0
  84. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_design_package_serialization.py +0 -0
  85. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_fasta_io.py +0 -0
  86. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_gc_content.py +0 -0
  87. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_host_profile_metadata.py +0 -0
  88. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_iupac_validation.py +0 -0
  89. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_legacy_cli.py +0 -0
  90. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_no_raw_sequence_logging.py +0 -0
  91. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_openbio_missing_metric_contract.py +0 -0
  92. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_parameter_registry.py +0 -0
  93. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_registry_production_sync.py +0 -0
  94. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_restriction_sites.py +0 -0
  95. {factorforge_cds-3.2.0 → factorforge_cds-3.2.1}/tests/test_sequence_validator.py +0 -0
  96. {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.0
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
  [![PyPI](https://img.shields.io/pypi/v/factorforge-cds.svg)](https://pypi.org/project/factorforge-cds/)
40
41
  [![CI](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml/badge.svg)](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
41
42
  [![codecov](https://codecov.io/gh/eijex/factorforge-cds/branch/main/graph/badge.svg)](https://codecov.io/gh/eijex/factorforge-cds)
42
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20407331.svg)](https://doi.org/10.5281/zenodo.20407331)
43
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20640931.svg)](https://doi.org/10.5281/zenodo.20640931)
43
44
  [![Web App](https://img.shields.io/badge/web-factorforge.eijex.com-brightgreen.svg)](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
  [![PyPI](https://img.shields.io/pypi/v/factorforge-cds.svg)](https://pypi.org/project/factorforge-cds/)
8
8
  [![CI](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml/badge.svg)](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
9
9
  [![codecov](https://codecov.io/gh/eijex/factorforge-cds/branch/main/graph/badge.svg)](https://codecov.io/gh/eijex/factorforge-cds)
10
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20407331.svg)](https://doi.org/10.5281/zenodo.20407331)
10
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20640931.svg)](https://doi.org/10.5281/zenodo.20640931)
11
11
  [![Web App](https://img.shields.io/badge/web-factorforge.eijex.com-brightgreen.svg)](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.0"
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",
@@ -4,7 +4,7 @@ FactorForge - Codon Optimization Platform
4
4
  profile: constraint-aware rule/profile engine
5
5
  """
6
6
 
7
- __version__ = "3.2.0"
7
+ __version__ = "3.2.1"
8
8
  __author__ = "Eijex"
9
9
 
10
10
  # Auto-register engines (safe when running from source tree)
@@ -423,7 +423,7 @@ class ReverseTranslator:
423
423
  """
424
424
  High-CAI profile: use only preferred codons
425
425
 
426
- - CAI > 0.85 guaranteed
426
+ - Selects codons that maximize the profile golden-set CAI reference
427
427
  - No GC constraints
428
428
  """
429
429
  dna_seq: list[str] = []
@@ -0,0 +1,5 @@
1
+ """Deterministic protein structural-risk annotations."""
2
+
3
+ from factorforge.protein_risk.annotate import annotate_protein_risk
4
+
5
+ __all__ = ["annotate_protein_risk"]
@@ -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
@@ -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.0
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
  [![PyPI](https://img.shields.io/pypi/v/factorforge-cds.svg)](https://pypi.org/project/factorforge-cds/)
40
41
  [![CI](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml/badge.svg)](https://github.com/eijex/factorforge-cds/actions/workflows/ci.yml)
41
42
  [![codecov](https://codecov.io/gh/eijex/factorforge-cds/branch/main/graph/badge.svg)](https://codecov.io/gh/eijex/factorforge-cds)
42
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20407331.svg)](https://doi.org/10.5281/zenodo.20407331)
43
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20640931.svg)](https://doi.org/10.5281/zenodo.20640931)
43
44
  [![Web App](https://img.shields.io/badge/web-factorforge.eijex.com-brightgreen.svg)](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
@@ -5,6 +5,7 @@ pydantic>=2.0
5
5
 
6
6
  [dev]
7
7
  jsonschema>=4.0
8
+ pandas>=2.0
8
9
  pytest>=7.0
9
10
  pytest-cov>=4.0
10
11
  ruff>=0.1
@@ -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
+ )