factorforge-cds 3.3.0__tar.gz → 3.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/PKG-INFO +2 -2
  2. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/README.md +1 -1
  3. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/pyproject.toml +2 -2
  4. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/__init__.py +1 -1
  5. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/feasibility.py +30 -3
  6. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/main.py +203 -16
  7. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/optimizer.py +1 -1
  8. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_golden_set.json +4 -0
  9. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +526 -0
  10. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +70 -0
  11. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +22 -0
  12. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +526 -0
  13. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +70 -0
  14. factorforge_cds-3.3.2/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +22 -0
  15. factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_derived_codons.json +526 -0
  16. factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +22 -0
  17. factorforge_cds-3.3.2/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +70 -0
  18. factorforge_cds-3.3.2/src/factorforge/data/reference/reference_policy_manifest.json +262 -0
  19. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/__init__.py +1 -1
  20. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/__init__.py +1 -1
  21. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/optimizer.py +14 -6
  22. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/pipeline.py +24 -13
  23. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/reverse_translator.py +81 -10
  24. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring.py +95 -47
  25. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.schema.json +11 -0
  26. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
  27. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/SOURCES.txt +11 -0
  28. factorforge_cds-3.3.2/tests/test_benchmark_cli_output_guard.py +87 -0
  29. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_schema.py +15 -0
  30. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/LICENSE +0 -0
  31. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/setup.cfg +0 -0
  32. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/__main__.py +0 -0
  33. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/__init__.py +0 -0
  34. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/analysis/metrics.py +0 -0
  35. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/__init__.py +0 -0
  36. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/cli/legacy_cli.py +0 -0
  37. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/__init__.py +0 -0
  38. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/exporter.py +0 -0
  39. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/validator.py +0 -0
  40. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_codons.json +0 -0
  41. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/ntabacum_codons.json +0 -0
  42. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/templates/high_expression.json +0 -0
  43. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/templates/standard_expression.json +0 -0
  44. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
  45. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/database.py +0 -0
  46. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
  47. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/construct_builder.py +0 -0
  48. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/exporter.py +0 -0
  49. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/__init__.py +0 -0
  50. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
  51. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
  52. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring_ml.py +0 -0
  53. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/utils.py +0 -0
  54. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/validator.py +0 -0
  55. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/engines/registry.py +0 -0
  56. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/__init__.py +0 -0
  57. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/fasta.py +0 -0
  58. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/io/validation.py +0 -0
  59. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/__init__.py +0 -0
  60. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/annotate.py +0 -0
  61. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/kd_scale.py +0 -0
  62. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/risk_classifier.py +0 -0
  63. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/sp_predict.py +0 -0
  64. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/tm_predict.py +0 -0
  65. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/registry/__init__.py +0 -0
  66. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/registry/registry_loader.py +0 -0
  67. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/__init__.py +0 -0
  68. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.py +0 -0
  69. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/__init__.py +0 -0
  70. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/construct_id.py +0 -0
  71. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/exceptions.py +0 -0
  72. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/restriction_sites.py +0 -0
  73. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/sequence_validator.py +0 -0
  74. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/utils/validation.py +0 -0
  75. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/__init__.py +0 -0
  76. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/cli.py +0 -0
  77. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation/package_generator.py +0 -0
  78. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation_registry.py +0 -0
  79. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge/validation_report.py +0 -0
  80. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
  81. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
  82. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/requires.txt +0 -0
  83. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/top_level.txt +0 -0
  84. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_baselines.py +0 -0
  85. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_codon_table_metadata.py +0 -0
  86. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_regression.py +0 -0
  87. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_scoring.py +0 -0
  88. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_benchmark_smoke.py +0 -0
  89. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_cai.py +0 -0
  90. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_codon_table_manifest.py +0 -0
  91. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_database.py +0 -0
  92. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_semantics.py +0 -0
  93. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_design_package_serialization.py +0 -0
  94. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_docs_consistency.py +0 -0
  95. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_fasta_io.py +0 -0
  96. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_gc_content.py +0 -0
  97. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_host_profile_metadata.py +0 -0
  98. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_iupac_validation.py +0 -0
  99. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_legacy_cli.py +0 -0
  100. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_no_raw_sequence_logging.py +0 -0
  101. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_openbio_missing_metric_contract.py +0 -0
  102. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_parameter_registry.py +0 -0
  103. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_protein_risk.py +0 -0
  104. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_registry_production_sync.py +0 -0
  105. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_restriction_sites.py +0 -0
  106. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_sequence_validator.py +0 -0
  107. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_translation_integrity.py +0 -0
  108. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_contract_compat.py +0 -0
  109. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_registry.py +0 -0
  110. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_validation_report.py +0 -0
  111. {factorforge_cds-3.3.0 → factorforge_cds-3.3.2}/tests/test_worked_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.3.0
3
+ Version: 3.3.2
4
4
  Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
5
5
  Author-email: Eijex <eijex.lab@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.3.0 (2026). Open-source constraint-based CDS design and sequence review.
96
+ FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
60
60
  ## Citing
61
61
 
62
62
  ```
63
- FactorForge v3.3.0 (2026). Open-source constraint-based CDS design and sequence review.
63
+ FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
64
64
  Eijex. https://github.com/eijex/factorforge-cds
65
65
  ```
66
66
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "factorforge-cds"
7
- version = "3.3.0"
7
+ version = "3.3.2"
8
8
  description = "FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-only"
@@ -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.3.0"
7
+ __version__ = "3.3.2"
8
8
  __author__ = "Eijex"
9
9
 
10
10
  # Auto-register engines (safe when running from source tree)
@@ -51,6 +51,7 @@ def _cai_from_log(log_sum: float, codon_count: int) -> float:
51
51
  def _candidate_summary(
52
52
  dna_sequence: str | None,
53
53
  codon_weights: dict[str, float],
54
+ cai_authority: dict[str, Any],
54
55
  ) -> dict[str, Any] | None:
55
56
  if dna_sequence is None:
56
57
  return None
@@ -59,6 +60,7 @@ def _candidate_summary(
59
60
  return {
60
61
  "dna_sequence": dna_sequence,
61
62
  "cai": calculate_cai(dna_sequence, codon_weights),
63
+ "cai_authority": dict(cai_authority),
62
64
  "gc": calculate_gc(dna_sequence),
63
65
  "first_region_gc": calculate_first_region_gc(dna_sequence),
64
66
  "gc_window_min": min(window_values) if window_values else 0.0,
@@ -66,6 +68,20 @@ def _candidate_summary(
66
68
  }
67
69
 
68
70
 
71
+ def _build_cai_authority(codon_reference_id: str | None) -> dict[str, Any]:
72
+ if codon_reference_id is None:
73
+ return {
74
+ "reference_id": None,
75
+ "reference_role": "cai_evaluation",
76
+ "reference_relationship": "unresolved",
77
+ }
78
+ return {
79
+ "reference_id": codon_reference_id,
80
+ "reference_role": "cai_evaluation",
81
+ "reference_relationship": "same_as_generation_reference",
82
+ }
83
+
84
+
69
85
  def _best_gc_under_gc_range(
70
86
  states: dict[int, float],
71
87
  protein_length: int,
@@ -104,6 +120,7 @@ def analyze_feasibility(
104
120
  target_gc_low: float = DEFAULT_GC_LOW,
105
121
  target_gc_high: float = DEFAULT_GC_HIGH,
106
122
  gc_ranges: list[tuple[float, float]] | None = None,
123
+ codon_reference_id: str | None = None,
107
124
  ) -> dict[str, Any]:
108
125
  """Compute exact CAI/GC feasibility over synonymous codon choices.
109
126
 
@@ -160,6 +177,7 @@ def analyze_feasibility(
160
177
 
161
178
  protein_length = len(protein)
162
179
  total_bases = protein_length * 3
180
+ cai_authority = _build_cai_authority(codon_reference_id)
163
181
  best_any_gc = max(states, key=lambda gc_count: states[gc_count])
164
182
  best_any_log_sum = states[best_any_gc]
165
183
  best_any_sequence = _reconstruct_sequence(backrefs, best_any_gc)
@@ -180,7 +198,7 @@ def analyze_feasibility(
180
198
  range_results[key] = {
181
199
  "feasible": True,
182
200
  "max_cai": _cai_from_log(states[gc_count], protein_length),
183
- "best_candidate": _candidate_summary(sequence, codon_weights),
201
+ "best_candidate": _candidate_summary(sequence, codon_weights, cai_authority),
184
202
  }
185
203
 
186
204
  target_cai_possible = (
@@ -192,8 +210,13 @@ def analyze_feasibility(
192
210
  "protein_length": protein_length,
193
211
  "minimum_possible_gc": (min_gc_count / total_bases) * 100.0,
194
212
  "maximum_possible_gc": (max_gc_count / total_bases) * 100.0,
213
+ "cai_authority": dict(cai_authority),
195
214
  "maximum_achievable_cai_without_gc": _cai_from_log(best_any_log_sum, protein_length),
196
- "best_candidate_without_gc": _candidate_summary(best_any_sequence, codon_weights),
215
+ "best_candidate_without_gc": _candidate_summary(
216
+ best_any_sequence,
217
+ codon_weights,
218
+ cai_authority,
219
+ ),
197
220
  "ranges": range_results,
198
221
  "target": {
199
222
  "cai": target_cai,
@@ -206,7 +229,11 @@ def analyze_feasibility(
206
229
  else None
207
230
  ),
208
231
  "best_candidate": (
209
- _candidate_summary(_reconstruct_sequence(backrefs, target_gc_count), codon_weights)
232
+ _candidate_summary(
233
+ _reconstruct_sequence(backrefs, target_gc_count),
234
+ codon_weights,
235
+ cai_authority,
236
+ )
210
237
  if target_gc_count is not None
211
238
  else None
212
239
  ),
@@ -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,15 @@ 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
+ codon_reference_id: str | None = None,
158
+ ):
47
159
  """Run the constraint-based DP feasibility engine for a single protein sequence."""
48
160
  if objective != "feasibility_best":
49
161
  raise ValueError("DP engine currently supports --objective feasibility_best.")
@@ -53,12 +165,14 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
53
165
  from factorforge.analysis.metrics import load_codon_usage_table
54
166
  from factorforge.analysis.feasibility import analyze_feasibility
55
167
 
56
- table = load_codon_usage_table()
168
+ table = load_codon_usage_table(path=codon_table_path)
57
169
  result = analyze_feasibility(
58
170
  sequence,
59
171
  table.codon_weights,
172
+ target_cai=cai_target,
60
173
  target_gc_low=gc_min,
61
174
  target_gc_high=gc_max,
175
+ codon_reference_id=codon_reference_id,
62
176
  )
63
177
  best = result["target"]["best_candidate"]
64
178
  feasible = best is not None
@@ -75,9 +189,17 @@ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float
75
189
  return best, result, reason
76
190
 
77
191
 
78
- def _format_dp_fasta(sequence_id: str, dna_sequence: str, cai: float, gc: float) -> str:
192
+ def _format_dp_fasta(
193
+ sequence_id: str,
194
+ dna_sequence: str,
195
+ cai: float,
196
+ gc: float,
197
+ requested_cai_target: float | None = None,
198
+ ) -> str:
79
199
  """Format a DP result as FASTA."""
80
200
  header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
201
+ if requested_cai_target is not None:
202
+ header = f"{header}|target_cai={requested_cai_target:.3f}"
81
203
  return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
82
204
 
83
205
 
@@ -102,10 +224,19 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
102
224
  cai = float(result.metrics.get("cai", 0.0))
103
225
  gc = float(result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0)))
104
226
  score = float(result.metrics.get("score", 0.0))
105
- header = f">{sequence_id}|profile={profile}|cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
227
+ header = (
228
+ f">{sequence_id}|engine=profile|profile={profile}|"
229
+ f"cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
230
+ )
106
231
  return f"{header}\n{_wrap_sequence(result.sequence)}\n"
107
232
 
108
233
 
234
+ def _single_profile_sequence_id(input_file: str, fasta_records) -> str:
235
+ if fasta_records is not None and len(fasta_records) == 1:
236
+ return str(fasta_records[0][0])
237
+ return Path(input_file).stem or "factorforge_profile"
238
+
239
+
109
240
  def _format_profile_comparison_table(profile_results) -> str:
110
241
  """Format profile optimization metrics as a comparison table."""
111
242
  divider = "─" * 45
@@ -166,9 +297,21 @@ def list_engines():
166
297
  )
167
298
  @click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
168
299
  @click.option("--gc-max", type=float, default=47.0, help="Maximum target GC percentage")
300
+ @click.option(
301
+ "--cai-target",
302
+ type=float,
303
+ default=DEFAULT_CAI_TARGET,
304
+ help="Requested DP target CAI threshold",
305
+ )
169
306
  @click.option("--template", "construct_template", help="Construct template name")
170
307
  @click.option("--output", "-o", help="Output file")
171
308
  @click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
309
+ @click.option(
310
+ "--reference-id",
311
+ type=click.Choice(REFERENCE_ID_CHOICES, case_sensitive=False),
312
+ default=None,
313
+ help="Expert/research codon-reference ID; checksum-validated.",
314
+ )
172
315
  @click.option(
173
316
  "--compare-profiles",
174
317
  help=(
@@ -192,9 +335,11 @@ def optimize(
192
335
  objective,
193
336
  gc_min,
194
337
  gc_max,
338
+ cai_target,
195
339
  construct_template,
196
340
  output,
197
341
  output_format,
342
+ reference_id,
198
343
  compare_profiles,
199
344
  scan_mode,
200
345
  scan_include,
@@ -206,6 +351,12 @@ def optimize(
206
351
  host_value = host.lower()
207
352
  internal_host = HOST_MAP[host_value]
208
353
  host_was_explicit = _option_was_explicitly_set("host")
354
+ reference_id = reference_id.lower() if reference_id else None
355
+ reference_table_path = None
356
+
357
+ if reference_id is not None:
358
+ _validate_reference_host(reference_id, internal_host)
359
+ reference_table_path = resolve_reference_by_id(reference_id)
209
360
 
210
361
  if host_was_explicit and engine == "dp" and _engine_option_was_explicitly_set():
211
362
  raise click.UsageError("--host is only supported with --engine profile")
@@ -224,6 +375,9 @@ def optimize(
224
375
  raise click.UsageError("--compare-profiles cannot be used with --engine dp.")
225
376
  engine = "profile"
226
377
 
378
+ if reference_table_path is not None and construct_template:
379
+ raise click.UsageError("--reference-id is not supported with --template mode.")
380
+
227
381
  try:
228
382
  # Read file
229
383
  with open(input_file, encoding="utf-8") as f:
@@ -247,7 +401,15 @@ def optimize(
247
401
  if output_format.lower() != "fasta":
248
402
  raise ValueError("Profile comparison only supports FASTA output.")
249
403
 
250
- optimizer = EngineRegistry.get("profile")
404
+ if reference_table_path is not None:
405
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
406
+
407
+ optimizer = RuleBasedOptimizer(
408
+ codon_table_path=str(reference_table_path),
409
+ generation_reference_id=reference_id,
410
+ )
411
+ else:
412
+ optimizer = EngineRegistry.get("profile")
251
413
  profile_results = []
252
414
  for profile_name in compare_profile_list:
253
415
  result = optimizer.optimize(
@@ -279,7 +441,15 @@ def optimize(
279
441
  if output_format.lower() != "fasta":
280
442
  raise ValueError("Multi-FASTA input only supports FASTA output.")
281
443
 
282
- optimizer = EngineRegistry.get(engine)
444
+ if reference_table_path is not None and engine == "profile":
445
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
446
+
447
+ optimizer = RuleBasedOptimizer(
448
+ codon_table_path=str(reference_table_path),
449
+ generation_reference_id=reference_id,
450
+ )
451
+ else:
452
+ optimizer = EngineRegistry.get(engine)
283
453
  payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
284
454
  if hasattr(optimizer, "optimize_batch"):
285
455
  results = optimizer.optimize_batch(
@@ -306,14 +476,9 @@ def optimize(
306
476
  combined_fasta = []
307
477
  for idx, result in enumerate(results):
308
478
  seq_id = payload[idx]["id"]
309
- cai = result.metrics.get("cai", 0.0)
310
- gc = result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0))
311
- score = result.metrics.get("score", 0.0)
312
- header = (
313
- f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
314
- f"gc={float(gc):.2f}|score={float(score):.3f}"
479
+ combined_fasta.append(
480
+ _format_profile_fasta(seq_id, profile, result).rstrip()
315
481
  )
316
- combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
317
482
  out_content = "\n".join(combined_fasta) + "\n"
318
483
 
319
484
  if output:
@@ -336,12 +501,22 @@ def optimize(
336
501
  objective=objective,
337
502
  gc_min=gc_min,
338
503
  gc_max=gc_max,
504
+ cai_target=cai_target,
505
+ codon_table_path=reference_table_path,
506
+ codon_reference_id=reference_id,
339
507
  )
340
508
  dna_sequence = best["dna_sequence"]
341
509
  cai = float(best["cai"])
342
510
  gc = float(best["gc"])
343
511
  sequence_id = Path(input_file).stem or "factorforge_dp"
344
- fasta = _format_dp_fasta(sequence_id, dna_sequence, cai, gc)
512
+ requested_cai_target = cai_target if _option_was_explicitly_set("cai_target") else None
513
+ fasta = _format_dp_fasta(
514
+ sequence_id,
515
+ dna_sequence,
516
+ cai,
517
+ gc,
518
+ requested_cai_target=requested_cai_target,
519
+ )
345
520
 
346
521
  click.echo("Optimizing with DP feasibility engine...")
347
522
  if output:
@@ -356,6 +531,8 @@ def optimize(
356
531
  click.echo(f" - gc_percent: {gc:.2f}")
357
532
  click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
358
533
  click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
534
+ if requested_cai_target is not None:
535
+ click.echo(f" - target_cai: {float(feasibility['target']['cai']):.3f}")
359
536
  click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
360
537
  click.echo(f" - recommendation_reason: {recommendation_reason}")
361
538
  return
@@ -393,7 +570,15 @@ def optimize(
393
570
  raise ValueError("Non-FASTA output requires --template with profile pipeline.")
394
571
 
395
572
  # Get engine
396
- optimizer = EngineRegistry.get(engine)
573
+ if reference_table_path is not None and engine == "profile":
574
+ from factorforge.engines.profile.optimizer import RuleBasedOptimizer
575
+
576
+ optimizer = RuleBasedOptimizer(
577
+ codon_table_path=str(reference_table_path),
578
+ generation_reference_id=reference_id,
579
+ )
580
+ else:
581
+ optimizer = EngineRegistry.get(engine)
397
582
 
398
583
  # Optimize
399
584
  click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
@@ -408,8 +593,10 @@ def optimize(
408
593
 
409
594
  # Output results
410
595
  if output:
596
+ sequence_id = _single_profile_sequence_id(input_file, fasta_records)
597
+ fasta = _format_profile_fasta(sequence_id, profile, result)
411
598
  with open(output, "w", encoding="utf-8") as f:
412
- f.write(result.sequence)
599
+ f.write(fasta)
413
600
  click.echo(f"Saved to: {output}")
414
601
  else:
415
602
  click.echo(f"\n{result.sequence}\n")
@@ -16,7 +16,7 @@ class OptimizationResult:
16
16
  def __init__(
17
17
  self,
18
18
  sequence: str,
19
- metrics: dict[str, float],
19
+ metrics: dict[str, Any],
20
20
  metadata: dict[str, Any] | None = None,
21
21
  ) -> None:
22
22
  self.sequence = sequence
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "organism": "Nicotiana benthamiana",
3
+ "reference_id": "nbenthamiana_golden_set_v1",
4
+ "reference_role": "cai_evaluation",
5
+ "reference_version": "1",
3
6
  "source": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
7
+ "source_basis": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
4
8
  "description": "Codon usage frequencies weighted toward highly expressed N. benthamiana genes (RuBisCO, ribosomal proteins, histones). Used as CAI reference weights per Sharp & Li (1987). All source data is publicly available.",
5
9
  "blend_ratio": 0.7,
6
10
  "optimal_gc_content": 42.5,