factorforge-cds 3.2.8__tar.gz → 3.3.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 (110) hide show
  1. {factorforge_cds-3.2.8/src/factorforge_cds.egg-info → factorforge_cds-3.3.1}/PKG-INFO +4 -4
  2. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/README.md +2 -2
  3. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/pyproject.toml +3 -3
  4. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/__init__.py +1 -1
  5. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/feasibility.py +8 -11
  6. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/metrics.py +1 -1
  7. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/main.py +187 -10
  8. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +526 -0
  9. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +70 -0
  10. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +22 -0
  11. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +526 -0
  12. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +70 -0
  13. factorforge_cds-3.3.1/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +22 -0
  14. factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_derived_codons.json +526 -0
  15. factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +22 -0
  16. factorforge_cds-3.3.1/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +70 -0
  17. factorforge_cds-3.3.1/src/factorforge/data/reference/reference_policy_manifest.json +262 -0
  18. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/__init__.py +1 -1
  19. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/__init__.py +1 -1
  20. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/optimizer.py +18 -2
  21. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/pipeline.py +1 -0
  22. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/reverse_translator.py +7 -5
  23. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/rule_engine.py +5 -5
  24. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/scoring.py +111 -72
  25. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/utils.py +7 -7
  26. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/design_package.schema.json +11 -0
  27. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1/src/factorforge_cds.egg-info}/PKG-INFO +4 -4
  28. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/SOURCES.txt +10 -0
  29. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_codon_table_metadata.py +3 -6
  30. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_schema.py +15 -0
  31. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_registry_production_sync.py +13 -16
  32. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_worked_example.py +2 -3
  33. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/LICENSE +0 -0
  34. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/setup.cfg +0 -0
  35. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/__main__.py +0 -0
  36. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/analysis/__init__.py +0 -0
  37. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/__init__.py +0 -0
  38. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/cli/legacy_cli.py +0 -0
  39. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/__init__.py +0 -0
  40. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/exporter.py +0 -0
  41. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/optimizer.py +0 -0
  42. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/core/interfaces/validator.py +0 -0
  43. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/nbenthamiana_codons.json +0 -0
  44. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
  45. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/ntabacum_codons.json +0 -0
  46. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/templates/high_expression.json +0 -0
  47. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/templates/standard_expression.json +0 -0
  48. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
  49. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/database.py +0 -0
  50. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
  51. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/construct_builder.py +0 -0
  52. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/exporter.py +0 -0
  53. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/__init__.py +0 -0
  54. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
  55. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/scoring_ml.py +0 -0
  56. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/profile/validator.py +0 -0
  57. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/engines/registry.py +0 -0
  58. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/__init__.py +0 -0
  59. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/fasta.py +0 -0
  60. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/io/validation.py +0 -0
  61. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/__init__.py +0 -0
  62. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/annotate.py +0 -0
  63. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/kd_scale.py +0 -0
  64. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/risk_classifier.py +0 -0
  65. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/sp_predict.py +0 -0
  66. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/protein_risk/tm_predict.py +0 -0
  67. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/registry/__init__.py +0 -0
  68. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/registry/registry_loader.py +0 -0
  69. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/__init__.py +0 -0
  70. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/schemas/design_package.py +0 -0
  71. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/__init__.py +0 -0
  72. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/construct_id.py +0 -0
  73. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/exceptions.py +0 -0
  74. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/restriction_sites.py +0 -0
  75. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/sequence_validator.py +0 -0
  76. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/utils/validation.py +0 -0
  77. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/__init__.py +0 -0
  78. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/cli.py +0 -0
  79. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation/package_generator.py +0 -0
  80. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation_registry.py +0 -0
  81. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge/validation_report.py +0 -0
  82. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
  83. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
  84. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/requires.txt +0 -0
  85. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/src/factorforge_cds.egg-info/top_level.txt +0 -0
  86. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_baselines.py +0 -0
  87. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_regression.py +0 -0
  88. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_scoring.py +0 -0
  89. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_benchmark_smoke.py +0 -0
  90. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_cai.py +0 -0
  91. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_codon_table_manifest.py +0 -0
  92. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_database.py +0 -0
  93. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_semantics.py +0 -0
  94. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_design_package_serialization.py +0 -0
  95. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_docs_consistency.py +0 -0
  96. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_fasta_io.py +0 -0
  97. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_gc_content.py +0 -0
  98. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_host_profile_metadata.py +0 -0
  99. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_iupac_validation.py +0 -0
  100. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_legacy_cli.py +0 -0
  101. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_no_raw_sequence_logging.py +0 -0
  102. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_openbio_missing_metric_contract.py +0 -0
  103. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_parameter_registry.py +0 -0
  104. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_protein_risk.py +0 -0
  105. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_restriction_sites.py +0 -0
  106. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_sequence_validator.py +0 -0
  107. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_translation_integrity.py +0 -0
  108. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_contract_compat.py +0 -0
  109. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_registry.py +0 -0
  110. {factorforge_cds-3.2.8 → factorforge_cds-3.3.1}/tests/test_validation_report.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.2.8
4
- Summary: FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex.
3
+ Version: 3.3.1
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
7
7
  Project-URL: Homepage, https://factorforge.eijex.com
@@ -33,7 +33,7 @@ Dynamic: license-file
33
33
 
34
34
  # FactorForge
35
35
 
36
- **Open-source constraint-based CDS design and pre-synthesis sequence review engine for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
36
+ **Open-source constraint-based CDS design and pre-synthesis sequence review for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
37
37
 
38
38
  [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE)
39
39
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.2.8 (2026). Open-source constraint-based CDS design engine.
96
+ FactorForge v3.3.1 (2026). Open-source constraint-based CDS design and sequence review.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -1,6 +1,6 @@
1
1
  # FactorForge
2
2
 
3
- **Open-source constraint-based CDS design and pre-synthesis sequence review engine for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
3
+ **Open-source constraint-based CDS design and pre-synthesis sequence review for plant CDS workflows, with primary support for *Nicotiana benthamiana* (Tobacco BY-2: experimental).**
4
4
 
5
5
  [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE)
6
6
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
60
60
  ## Citing
61
61
 
62
62
  ```
63
- FactorForge v3.2.8 (2026). Open-source constraint-based CDS design engine.
63
+ FactorForge v3.3.1 (2026). Open-source constraint-based CDS design and sequence review.
64
64
  Eijex. https://github.com/eijex/factorforge-cds
65
65
  ```
66
66
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "factorforge-cds"
7
- version = "3.2.8"
8
- description = "FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex."
7
+ version = "3.3.1"
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"
11
11
  license-files = ["LICENSE"]
@@ -53,7 +53,7 @@ factorforge-validate = "factorforge.validation.cli:main"
53
53
  where = ["src"]
54
54
 
55
55
  [tool.setuptools.package-data]
56
- "factorforge" = ["data/*.json", "data/templates/*.json", "schemas/*.json"]
56
+ "factorforge" = ["data/*.json", "data/templates/*.json", "data/profiles/*.json", "data/reference/*.json", "schemas/*.json"]
57
57
 
58
58
  [tool.pytest.ini_options]
59
59
  testpaths = ["tests"]
@@ -4,7 +4,7 @@ FactorForge - Codon Optimization Platform
4
4
  profile: constraint-aware rule/profile engine
5
5
  """
6
6
 
7
- __version__ = "3.2.8"
7
+ __version__ = "3.3.1"
8
8
  __author__ = "Eijex"
9
9
 
10
10
  # Auto-register engines (safe when running from source tree)
@@ -14,19 +14,16 @@ from factorforge.analysis.metrics import (
14
14
  )
15
15
 
16
16
 
17
- # Defaults calibrated to nbenthamiana profile engine output distribution
18
- # (internal benchmark, n=49): avg CAI=0.76, avg GC=60.1% (range 55-71%).
19
- # DEFAULT_CAI_TARGET=0.82 aligns with industry practice (>0.8) and is achievable.
17
+ # Defaults for the current N. benthamiana software-default codon reference.
18
+ # DEFAULT_CAI_TARGET=0.82 remains a soft in-silico target aligned with
19
+ # industry practice (>0.8). DEFAULT_GC_LOW/HIGH track the NbeV1.1
20
+ # high-confidence CDS-derived native composition band. These are not wet-lab
21
+ # validation, expression/yield prediction, or biological-superiority claims.
20
22
  # Exported as named constants so tests/test_registry_production_sync.py can
21
23
  # strictly compare them against the registry (single source of truth).
22
- #
23
- # DEFAULT_GC_LOW/HIGH provisionally reverted from the Job 168/v3.3.0
24
- # native-genome-composition anchor (40-47%, released as part of v3.2.7) back
25
- # to the legacy engine-output-calibrated band, pending an MFE re-sensitivity +
26
- # 2x2 factorial recheck. See scoring.py's GC_OPT_MIN/MAX comment.
27
24
  DEFAULT_CAI_TARGET: float = 0.82
28
- DEFAULT_GC_LOW: float = 55.0
29
- DEFAULT_GC_HIGH: float = 65.0
25
+ DEFAULT_GC_LOW: float = 40.0
26
+ DEFAULT_GC_HIGH: float = 47.0
30
27
 
31
28
 
32
29
  AA_TO_CODONS: dict[str, list[str]] = {}
@@ -121,7 +118,7 @@ def analyze_feasibility(
121
118
  if not protein:
122
119
  raise ValueError("protein_sequence must not be empty")
123
120
 
124
- ranges = gc_ranges or [(55.0, 65.0), (50.0, 65.0), (40.0, 65.0)]
121
+ ranges = gc_ranges or [(40.0, 47.0), (40.0, 55.0), (40.0, 65.0)]
125
122
  normalized_ranges = [
126
123
  (_normalize_gc_bound(low), _normalize_gc_bound(high)) for low, high in ranges
127
124
  ]
@@ -98,7 +98,7 @@ class CodonUsageTable:
98
98
 
99
99
 
100
100
  def _default_codon_table_path() -> Path:
101
- # Job 168 / v3.3.0 (_analysis/025): production default switched from the
101
+ # v3.3.0 reference-policy update: production default switched from the
102
102
  # legacy Kazusa/SGN-derived table to the NbeV1.1 LAB-strain high-confidence
103
103
  # derived table. See data/reference/active_codon_reference.json.
104
104
  return get_data_path() / "profiles" / "nbev11_cds_hc_derived_codons.json"
@@ -8,15 +8,119 @@ Usage:
8
8
  """
9
9
 
10
10
  from pathlib import Path
11
+ import hashlib
12
+ import json
11
13
  import sys
12
14
 
13
15
  import click
14
16
 
15
17
  from factorforge import __version__
18
+ from factorforge.analysis.feasibility import DEFAULT_CAI_TARGET
16
19
  from factorforge.engines.registry import EngineRegistry
17
20
  from factorforge.engines.profile.utils import parse_fasta_records
18
21
 
19
22
  HOST_MAP = {"nbenthamiana": "nbenthamiana", "by2": "ntabacum"}
23
+ HOST_TAXIDS = {"nbenthamiana": 4100, "ntabacum": 4097}
24
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
25
+ FACTORFORGE_REPO_ROOT = Path(__file__).resolve().parents[3]
26
+ REFERENCE_POLICY_MANIFEST_PATH = (
27
+ FACTORFORGE_REPO_ROOT / "data" / "reference" / "reference_policy_manifest.json"
28
+ )
29
+ BUNDLED_REFERENCE_POLICY_MANIFEST_PATH = (
30
+ PACKAGE_ROOT / "data" / "reference" / "reference_policy_manifest.json"
31
+ )
32
+
33
+
34
+ def _reference_policy_manifest_path() -> Path:
35
+ """Return the repo manifest during development, or the packaged manifest in wheels."""
36
+ if REFERENCE_POLICY_MANIFEST_PATH.exists():
37
+ return REFERENCE_POLICY_MANIFEST_PATH
38
+ return BUNDLED_REFERENCE_POLICY_MANIFEST_PATH
39
+
40
+
41
+ def _resolve_packaged_or_repo_path(relative_path: str) -> Path:
42
+ """Resolve manifest paths in a source checkout or an installed wheel."""
43
+ repo_path = FACTORFORGE_REPO_ROOT / relative_path
44
+ if repo_path.exists():
45
+ return repo_path
46
+
47
+ package_prefix = "src/factorforge/"
48
+ if relative_path.startswith(package_prefix):
49
+ return PACKAGE_ROOT / relative_path.removeprefix(package_prefix)
50
+ return PACKAGE_ROOT / relative_path
51
+
52
+
53
+ def _load_reference_policy_manifest() -> dict:
54
+ """Load the checksum/tier policy manifest for expert CLI reference selection."""
55
+ return json.loads(_reference_policy_manifest_path().read_text(encoding="utf-8"))
56
+
57
+
58
+ def _reference_entries_by_id() -> dict[str, dict]:
59
+ manifest = _load_reference_policy_manifest()
60
+ return {entry["reference_id"]: entry for entry in manifest["references"]}
61
+
62
+
63
+ def _reference_id_choices() -> tuple[str, ...]:
64
+ return tuple(_reference_entries_by_id())
65
+
66
+
67
+ REFERENCE_ID_CHOICES = _reference_id_choices()
68
+
69
+
70
+ def _reference_entry_by_id(reference_id: str) -> dict:
71
+ entries = _reference_entries_by_id()
72
+ try:
73
+ return entries[reference_id]
74
+ except KeyError as exc:
75
+ choices = ", ".join(sorted(entries))
76
+ raise click.UsageError(
77
+ f"Unknown reference_id {reference_id!r}. Supported values: {choices}"
78
+ ) from exc
79
+
80
+
81
+ def _sha256_file(path: Path) -> str:
82
+ return hashlib.sha256(path.read_bytes()).hexdigest()
83
+
84
+
85
+ def resolve_reference_by_id(reference_id: str) -> Path:
86
+ """Resolve a manifest reference_id to a checksum-verified codon table path."""
87
+ entry = _reference_entry_by_id(reference_id)
88
+ codon_table_path = _resolve_packaged_or_repo_path(str(entry["codon_table_path"]))
89
+ expected = str(entry["checksum_sha256"])
90
+ if not codon_table_path.exists():
91
+ raise click.UsageError(
92
+ f"Codon table file for {reference_id} does not exist: {codon_table_path}"
93
+ )
94
+ actual = _sha256_file(codon_table_path)
95
+ if actual != expected:
96
+ raise click.UsageError(
97
+ "Checksum mismatch for "
98
+ f"{reference_id} at {codon_table_path}: expected {expected}, actual {actual}"
99
+ )
100
+
101
+ if entry["tier"] != "production_enabled":
102
+ limitations = "; ".join(entry.get("known_limitations", []))
103
+ warning = (
104
+ f"Warning: reference_id={reference_id} has tier={entry['tier']}; "
105
+ f"{entry['claim_boundary']}"
106
+ )
107
+ if limitations:
108
+ warning = f"{warning} Known limitations: {limitations}"
109
+ click.echo(warning, err=True)
110
+
111
+ return codon_table_path
112
+
113
+
114
+ def _validate_reference_host(reference_id: str, internal_host: str) -> None:
115
+ entry = _reference_entry_by_id(reference_id)
116
+ expected_taxid = HOST_TAXIDS[internal_host]
117
+ actual_taxid = int(entry["ncbi_taxid"])
118
+ if actual_taxid != expected_taxid:
119
+ raise click.UsageError(
120
+ f"reference_id={reference_id} targets {entry['organism']} "
121
+ f"(NCBI taxid {actual_taxid}) and is incompatible with "
122
+ f"--host {internal_host} (expected NCBI taxid {expected_taxid})."
123
+ )
20
124
 
21
125
 
22
126
  def _configure_stdio() -> None:
@@ -43,7 +147,14 @@ def _wrap_sequence(sequence, width=80):
43
147
  return "\n".join(sequence[i : i + width] for i in range(0, len(sequence), width))
44
148
 
45
149
 
46
- def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float):
150
+ def _build_dp_result(
151
+ sequence: str,
152
+ objective: str,
153
+ gc_min: float,
154
+ gc_max: float,
155
+ cai_target: float = DEFAULT_CAI_TARGET,
156
+ codon_table_path: Path | None = None,
157
+ ):
47
158
  """Run the constraint-based DP feasibility engine for a single protein sequence."""
48
159
  if objective != "feasibility_best":
49
160
  raise ValueError("DP engine currently supports --objective feasibility_best.")
@@ -53,10 +164,11 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
53
164
  from factorforge.analysis.metrics import load_codon_usage_table
54
165
  from factorforge.analysis.feasibility import analyze_feasibility
55
166
 
56
- table = load_codon_usage_table()
167
+ table = load_codon_usage_table(path=codon_table_path)
57
168
  result = analyze_feasibility(
58
169
  sequence,
59
170
  table.codon_weights,
171
+ target_cai=cai_target,
60
172
  target_gc_low=gc_min,
61
173
  target_gc_high=gc_max,
62
174
  )
@@ -75,9 +187,17 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
75
187
  return best, result, reason
76
188
 
77
189
 
78
- def _format_dp_fasta(sequence_id: str, dna_sequence: str, cai: float, gc: float) -> str:
190
+ def _format_dp_fasta(
191
+ sequence_id: str,
192
+ dna_sequence: str,
193
+ cai: float,
194
+ gc: float,
195
+ requested_cai_target: float | None = None,
196
+ ) -> str:
79
197
  """Format a DP result as FASTA."""
80
198
  header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
199
+ if requested_cai_target is not None:
200
+ header = f"{header}|target_cai={requested_cai_target:.3f}"
81
201
  return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
82
202
 
83
203
 
@@ -106,6 +226,12 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
106
226
  return f"{header}\n{_wrap_sequence(result.sequence)}\n"
107
227
 
108
228
 
229
+ def _single_profile_sequence_id(input_file: str, fasta_records) -> str:
230
+ if fasta_records is not None and len(fasta_records) == 1:
231
+ return str(fasta_records[0][0])
232
+ return Path(input_file).stem or "factorforge_profile"
233
+
234
+
109
235
  def _format_profile_comparison_table(profile_results) -> str:
110
236
  """Format profile optimization metrics as a comparison table."""
111
237
  divider = "─" * 45
@@ -164,11 +290,23 @@ def list_engines():
164
290
  type=click.Choice(["feasibility_best"], case_sensitive=False),
165
291
  help="DP objective",
166
292
  )
167
- @click.option("--gc-min", type=float, default=55.0, help="Minimum target GC percentage")
168
- @click.option("--gc-max", type=float, default=65.0, help="Maximum target GC percentage")
293
+ @click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
294
+ @click.option("--gc-max", type=float, default=47.0, help="Maximum target GC percentage")
295
+ @click.option(
296
+ "--cai-target",
297
+ type=float,
298
+ default=DEFAULT_CAI_TARGET,
299
+ help="Requested DP target CAI threshold",
300
+ )
169
301
  @click.option("--template", "construct_template", help="Construct template name")
170
302
  @click.option("--output", "-o", help="Output file")
171
303
  @click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
304
+ @click.option(
305
+ "--reference-id",
306
+ type=click.Choice(REFERENCE_ID_CHOICES, case_sensitive=False),
307
+ default=None,
308
+ help="Expert/research codon-reference ID; checksum-validated.",
309
+ )
172
310
  @click.option(
173
311
  "--compare-profiles",
174
312
  help=(
@@ -192,9 +330,11 @@ def optimize(
192
330
  objective,
193
331
  gc_min,
194
332
  gc_max,
333
+ cai_target,
195
334
  construct_template,
196
335
  output,
197
336
  output_format,
337
+ reference_id,
198
338
  compare_profiles,
199
339
  scan_mode,
200
340
  scan_include,
@@ -206,6 +346,12 @@ def optimize(
206
346
  host_value = host.lower()
207
347
  internal_host = HOST_MAP[host_value]
208
348
  host_was_explicit = _option_was_explicitly_set("host")
349
+ reference_id = reference_id.lower() if reference_id else None
350
+ reference_table_path = None
351
+
352
+ if reference_id is not None:
353
+ _validate_reference_host(reference_id, internal_host)
354
+ reference_table_path = resolve_reference_by_id(reference_id)
209
355
 
210
356
  if host_was_explicit and engine == "dp" and _engine_option_was_explicitly_set():
211
357
  raise click.UsageError("--host is only supported with --engine profile")
@@ -224,6 +370,9 @@ def optimize(
224
370
  raise click.UsageError("--compare-profiles cannot be used with --engine dp.")
225
371
  engine = "profile"
226
372
 
373
+ if reference_table_path is not None and construct_template:
374
+ raise click.UsageError("--reference-id is not supported with --template mode.")
375
+
227
376
  try:
228
377
  # Read file
229
378
  with open(input_file, encoding="utf-8") as f:
@@ -247,7 +396,12 @@ def optimize(
247
396
  if output_format.lower() != "fasta":
248
397
  raise ValueError("Profile comparison only supports FASTA output.")
249
398
 
250
- optimizer = EngineRegistry.get("profile")
399
+ if reference_table_path is not None:
400
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
401
+
402
+ optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
403
+ else:
404
+ optimizer = EngineRegistry.get("profile")
251
405
  profile_results = []
252
406
  for profile_name in compare_profile_list:
253
407
  result = optimizer.optimize(
@@ -279,7 +433,12 @@ def optimize(
279
433
  if output_format.lower() != "fasta":
280
434
  raise ValueError("Multi-FASTA input only supports FASTA output.")
281
435
 
282
- optimizer = EngineRegistry.get(engine)
436
+ if reference_table_path is not None and engine == "profile":
437
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
438
+
439
+ optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
440
+ else:
441
+ optimizer = EngineRegistry.get(engine)
283
442
  payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
284
443
  if hasattr(optimizer, "optimize_batch"):
285
444
  results = optimizer.optimize_batch(
@@ -336,12 +495,21 @@ def optimize(
336
495
  objective=objective,
337
496
  gc_min=gc_min,
338
497
  gc_max=gc_max,
498
+ cai_target=cai_target,
499
+ codon_table_path=reference_table_path,
339
500
  )
340
501
  dna_sequence = best["dna_sequence"]
341
502
  cai = float(best["cai"])
342
503
  gc = float(best["gc"])
343
504
  sequence_id = Path(input_file).stem or "factorforge_dp"
344
- fasta = _format_dp_fasta(sequence_id, dna_sequence, cai, gc)
505
+ requested_cai_target = cai_target if _option_was_explicitly_set("cai_target") else None
506
+ fasta = _format_dp_fasta(
507
+ sequence_id,
508
+ dna_sequence,
509
+ cai,
510
+ gc,
511
+ requested_cai_target=requested_cai_target,
512
+ )
345
513
 
346
514
  click.echo("Optimizing with DP feasibility engine...")
347
515
  if output:
@@ -356,6 +524,8 @@ def optimize(
356
524
  click.echo(f" - gc_percent: {gc:.2f}")
357
525
  click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
358
526
  click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
527
+ if requested_cai_target is not None:
528
+ click.echo(f" - target_cai: {float(feasibility['target']['cai']):.3f}")
359
529
  click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
360
530
  click.echo(f" - recommendation_reason: {recommendation_reason}")
361
531
  return
@@ -393,7 +563,12 @@ def optimize(
393
563
  raise ValueError("Non-FASTA output requires --template with profile pipeline.")
394
564
 
395
565
  # Get engine
396
- optimizer = EngineRegistry.get(engine)
566
+ if reference_table_path is not None and engine == "profile":
567
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
568
+
569
+ optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
570
+ else:
571
+ optimizer = EngineRegistry.get(engine)
397
572
 
398
573
  # Optimize
399
574
  click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
@@ -408,8 +583,10 @@ def optimize(
408
583
 
409
584
  # Output results
410
585
  if output:
586
+ sequence_id = _single_profile_sequence_id(input_file, fasta_records)
587
+ fasta = _format_profile_fasta(sequence_id, profile, result)
411
588
  with open(output, "w", encoding="utf-8") as f:
412
- f.write(result.sequence)
589
+ f.write(fasta)
413
590
  click.echo(f"Saved to: {output}")
414
591
  else:
415
592
  click.echo(f"\n{result.sequence}\n")