factorforge-cds 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- factorforge/__init__.py +19 -0
- factorforge/__main__.py +8 -0
- factorforge/cli/__init__.py +5 -0
- factorforge/cli/legacy_cli.py +157 -0
- factorforge/cli/main.py +305 -0
- factorforge/core/interfaces/__init__.py +7 -0
- factorforge/core/interfaces/exporter.py +13 -0
- factorforge/core/interfaces/optimizer.py +85 -0
- factorforge/core/interfaces/validator.py +9 -0
- factorforge/database.py +150 -0
- factorforge/engines/__init__.py +60 -0
- factorforge/engines/ml/__init__.py +0 -0
- factorforge/engines/ml/plant_optimizer.py +325 -0
- factorforge/engines/registry.py +141 -0
- factorforge/engines/v1_archived/__init__.py +15 -0
- factorforge/engines/v2/__init__.py +13 -0
- factorforge/engines/v2/codon_table_builder.py +107 -0
- factorforge/engines/v2/construct_builder.py +403 -0
- factorforge/engines/v2/exporter.py +455 -0
- factorforge/engines/v2/optimizer.py +190 -0
- factorforge/engines/v2/pipeline.py +275 -0
- factorforge/engines/v2/rules/__init__.py +3 -0
- factorforge/engines/v2/rules/domesticator.py +403 -0
- factorforge/engines/v2/rules/reverse_translator.py +765 -0
- factorforge/engines/v2/rules/rule_engine.py +867 -0
- factorforge/engines/v2/scoring.py +232 -0
- factorforge/engines/v2/utils.py +231 -0
- factorforge/engines/v2/validator.py +383 -0
- factorforge/engines/v3/__init__.py +12 -0
- factorforge/engines/v3/explain.py +119 -0
- factorforge/engines/v3/inference/__init__.py +6 -0
- factorforge/engines/v3/inference/constrained_decoder.py +80 -0
- factorforge/engines/v3/inference/v2_adapter.py +72 -0
- factorforge/engines/v3/metrics.py +145 -0
- factorforge/engines/v3/modeling_bart_decoder.py +127 -0
- factorforge/engines/v3/pipeline.py +192 -0
- factorforge/engines/v3/synonym_mask.py +61 -0
- factorforge/engines/v3/tokenizer.py +192 -0
- factorforge/ml/__init__.py +33 -0
- factorforge/ml/feasibility.py +199 -0
- factorforge/ml/metrics.py +295 -0
- factorforge/utils/__init__.py +31 -0
- factorforge/utils/construct_id.py +8 -0
- factorforge/utils/exceptions.py +32 -0
- factorforge/utils/sequence_validator.py +189 -0
- factorforge/utils/validation.py +104 -0
- factorforge_cds-3.0.0.dist-info/METADATA +475 -0
- factorforge_cds-3.0.0.dist-info/RECORD +52 -0
- factorforge_cds-3.0.0.dist-info/WHEEL +5 -0
- factorforge_cds-3.0.0.dist-info/entry_points.txt +2 -0
- factorforge_cds-3.0.0.dist-info/licenses/LICENSE +201 -0
- factorforge_cds-3.0.0.dist-info/top_level.txt +1 -0
factorforge/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FactorForge - Codon Optimization Platform
|
|
3
|
+
|
|
4
|
+
v1_archived: Rule-based v1 (Archived)
|
|
5
|
+
v2: Rule-based (Production) — engine version 3.0.0
|
|
6
|
+
v3: ML engine / v3-alpha (ESM2 + BART, in development)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "3.0.0"
|
|
10
|
+
__author__ = "Eijex"
|
|
11
|
+
|
|
12
|
+
# Auto-register engines (safe when running from source tree)
|
|
13
|
+
try:
|
|
14
|
+
from .engines import EngineRegistry, register_builtin_engines
|
|
15
|
+
|
|
16
|
+
register_builtin_engines()
|
|
17
|
+
__all__ = ["EngineRegistry"]
|
|
18
|
+
except Exception:
|
|
19
|
+
__all__ = ["__version__"]
|
factorforge/__main__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FactorForge Command Line Interface
|
|
3
|
+
For Linux servers and automation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Add src root to path
|
|
12
|
+
src_root = next(p for p in Path(__file__).resolve().parents if p.name == "src")
|
|
13
|
+
sys.path.insert(0, str(src_root))
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(description="FactorForge - AI-Powered Codon Optimization")
|
|
17
|
+
|
|
18
|
+
parser.add_argument("input", help="Input DNA sequence or FASTA file")
|
|
19
|
+
|
|
20
|
+
parser.add_argument("-o", "--output", help="Output file (JSON format)", default=None)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-m", "--model", help="Model checkpoint path", default="outputs/checkpoints/phase3"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.add_argument("--batch", action="store_true", help="Process as FASTA file (batch mode)")
|
|
27
|
+
|
|
28
|
+
parser.add_argument("--verbose", action="store_true", help="Verbose output")
|
|
29
|
+
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from factorforge.engines.v1_archived.evaluation.metrics import BiologicalMetrics
|
|
34
|
+
from factorforge.engines.v1_archived.tokenization.codon_tokenizer import CodonTokenizer
|
|
35
|
+
except ImportError as exc:
|
|
36
|
+
raise SystemExit(
|
|
37
|
+
"v1 dependencies not installed. Install with: pip install -e \".[v1]\""
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
# Load tokenizer
|
|
41
|
+
if args.verbose:
|
|
42
|
+
print(f"Loading tokenizer from {args.model}...")
|
|
43
|
+
|
|
44
|
+
tokenizer_path = Path(args.model) / "tokenizer"
|
|
45
|
+
if not tokenizer_path.exists():
|
|
46
|
+
print(f"Error: Tokenizer not found at {tokenizer_path}")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
tokenizer = CodonTokenizer.load(str(tokenizer_path))
|
|
50
|
+
|
|
51
|
+
# Get sequence
|
|
52
|
+
sequences = []
|
|
53
|
+
if Path(args.input).exists():
|
|
54
|
+
# File input
|
|
55
|
+
with open(args.input) as f:
|
|
56
|
+
content = f.read()
|
|
57
|
+
|
|
58
|
+
# Handle FASTA
|
|
59
|
+
if content.startswith(">"):
|
|
60
|
+
if args.batch:
|
|
61
|
+
sequences = parse_fasta(content)
|
|
62
|
+
else:
|
|
63
|
+
sequences = [extract_first_sequence(content)]
|
|
64
|
+
else:
|
|
65
|
+
sequences = [content.strip()]
|
|
66
|
+
else:
|
|
67
|
+
# Direct sequence input
|
|
68
|
+
sequences = [args.input]
|
|
69
|
+
|
|
70
|
+
# Process sequences
|
|
71
|
+
results = []
|
|
72
|
+
bio = BiologicalMetrics()
|
|
73
|
+
|
|
74
|
+
for i, seq in enumerate(sequences):
|
|
75
|
+
# Clean sequence
|
|
76
|
+
seq = "".join(c for c in seq.upper() if c in "ATGC")
|
|
77
|
+
|
|
78
|
+
if len(seq) == 0:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if len(seq) % 3 != 0:
|
|
82
|
+
if args.verbose:
|
|
83
|
+
print(f"Warning: Sequence {i+1} length not multiple of 3, skipping...")
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Tokenize
|
|
87
|
+
tokens = tokenizer.encode(seq)
|
|
88
|
+
|
|
89
|
+
# Metrics
|
|
90
|
+
metrics = bio.evaluate_sequence_quality(seq)
|
|
91
|
+
is_quality, checks = bio.is_high_quality(seq)
|
|
92
|
+
|
|
93
|
+
result = {
|
|
94
|
+
"sequence_id": i + 1,
|
|
95
|
+
"length": len(seq),
|
|
96
|
+
"num_tokens": len(tokens),
|
|
97
|
+
"compression_ratio": round(len(seq) / len(tokens), 2) if len(tokens) > 0 else 0,
|
|
98
|
+
"gc_content": metrics["gc_content"],
|
|
99
|
+
"cai": metrics["cai"],
|
|
100
|
+
"rare_codon_freq": metrics["rare_codon_freq"],
|
|
101
|
+
"high_quality": is_quality,
|
|
102
|
+
"quality_checks": checks,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
results.append(result)
|
|
106
|
+
|
|
107
|
+
if args.verbose:
|
|
108
|
+
print(f"\nSequence {i+1}:")
|
|
109
|
+
print(f" Length: {result['length']} bp")
|
|
110
|
+
print(f" Tokens: {result['num_tokens']}")
|
|
111
|
+
print(f" GC%: {result['gc_content']:.1f}%")
|
|
112
|
+
print(f" CAI: {result['cai']:.3f}")
|
|
113
|
+
print(f" Quality: {'✅ PASS' if is_quality else '❌ FAIL'}")
|
|
114
|
+
|
|
115
|
+
# Output
|
|
116
|
+
if args.output:
|
|
117
|
+
with open(args.output, "w") as f:
|
|
118
|
+
json.dump(results, f, indent=2)
|
|
119
|
+
print(f"\n✅ Results saved to {args.output}")
|
|
120
|
+
else:
|
|
121
|
+
print("\n" + json.dumps(results, indent=2))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_fasta(content):
|
|
125
|
+
"""Parse FASTA file with multiple sequences"""
|
|
126
|
+
sequences = []
|
|
127
|
+
current_seq = []
|
|
128
|
+
|
|
129
|
+
for line in content.split("\n"):
|
|
130
|
+
if line.startswith(">"):
|
|
131
|
+
if current_seq:
|
|
132
|
+
sequences.append("".join(current_seq))
|
|
133
|
+
current_seq = []
|
|
134
|
+
else:
|
|
135
|
+
current_seq.append(line.strip())
|
|
136
|
+
|
|
137
|
+
if current_seq:
|
|
138
|
+
sequences.append("".join(current_seq))
|
|
139
|
+
|
|
140
|
+
return sequences
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def extract_first_sequence(content):
|
|
144
|
+
"""Extract first sequence from FASTA"""
|
|
145
|
+
lines = content.split("\n")
|
|
146
|
+
sequence = []
|
|
147
|
+
|
|
148
|
+
for line in lines:
|
|
149
|
+
if line.startswith(">"):
|
|
150
|
+
continue
|
|
151
|
+
sequence.append(line.strip())
|
|
152
|
+
|
|
153
|
+
return "".join(sequence)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
factorforge/cli/main.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FactorForge CLI
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
factorforge optimize input.fasta -e v2 -p balanced -o output.fasta
|
|
6
|
+
factorforge optimize input.fasta -e v2 -p balanced --template standard_expression -o output.gb --format genbank
|
|
7
|
+
factorforge list-engines
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from factorforge import __version__
|
|
16
|
+
from factorforge.engines.registry import EngineRegistry
|
|
17
|
+
from factorforge.engines.v2.utils import parse_fasta_records
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _configure_stdio() -> None:
|
|
21
|
+
"""Best-effort UTF-8 for Windows consoles."""
|
|
22
|
+
for stream in (sys.stdout, sys.stderr):
|
|
23
|
+
try:
|
|
24
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
25
|
+
if callable(reconfigure):
|
|
26
|
+
reconfigure(encoding="utf-8")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_csv_option(value):
|
|
32
|
+
"""Parse comma-separated option values."""
|
|
33
|
+
if not value:
|
|
34
|
+
return None
|
|
35
|
+
parsed = [item.strip() for item in value.split(",") if item.strip()]
|
|
36
|
+
return parsed or None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _wrap_sequence(sequence, width=80):
|
|
40
|
+
"""Wrap sequence to fixed-width lines."""
|
|
41
|
+
return "\n".join(sequence[i : i + width] for i in range(0, len(sequence), width))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float):
|
|
45
|
+
"""Run the constraint-based DP feasibility engine for a single protein sequence."""
|
|
46
|
+
if objective != "feasibility_best":
|
|
47
|
+
raise ValueError("DP engine currently supports --objective feasibility_best.")
|
|
48
|
+
if gc_min > gc_max:
|
|
49
|
+
raise ValueError("--gc-min must be <= --gc-max.")
|
|
50
|
+
|
|
51
|
+
from factorforge.engines.v3.metrics import load_codon_usage_table
|
|
52
|
+
from factorforge.ml.feasibility import analyze_feasibility
|
|
53
|
+
|
|
54
|
+
table = load_codon_usage_table()
|
|
55
|
+
result = analyze_feasibility(
|
|
56
|
+
sequence,
|
|
57
|
+
table.codon_weights,
|
|
58
|
+
target_gc_low=gc_min,
|
|
59
|
+
target_gc_high=gc_max,
|
|
60
|
+
)
|
|
61
|
+
best = result["target"]["best_candidate"]
|
|
62
|
+
feasible = best is not None
|
|
63
|
+
if best is None:
|
|
64
|
+
best = result["best_candidate_without_gc"]
|
|
65
|
+
if best is None:
|
|
66
|
+
raise ValueError("No DP candidate generated.")
|
|
67
|
+
|
|
68
|
+
reason = (
|
|
69
|
+
f"Maximum CAI under GC {gc_min:g}-{gc_max:g}%"
|
|
70
|
+
if feasible
|
|
71
|
+
else "Maximum CAI without GC constraint; requested GC range was infeasible"
|
|
72
|
+
)
|
|
73
|
+
return best, result, reason
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _format_dp_fasta(sequence_id: str, dna_sequence: str, cai: float, gc: float) -> str:
|
|
77
|
+
"""Format a DP result as FASTA."""
|
|
78
|
+
header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
|
|
79
|
+
return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.group()
|
|
83
|
+
@click.version_option(version=__version__)
|
|
84
|
+
def cli():
|
|
85
|
+
"""FactorForge - Codon Optimization Platform"""
|
|
86
|
+
_configure_stdio()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cli.command()
|
|
90
|
+
def list_engines():
|
|
91
|
+
"""List available optimization engines"""
|
|
92
|
+
engines = EngineRegistry.list_engines()
|
|
93
|
+
|
|
94
|
+
click.echo("\nAvailable Engines:\n")
|
|
95
|
+
for name, info in engines.items():
|
|
96
|
+
click.echo(f" - {name}: {info['name']} v{info['version']}")
|
|
97
|
+
click.echo()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@cli.command()
|
|
101
|
+
@click.argument("input_file", type=click.Path(exists=True))
|
|
102
|
+
@click.option(
|
|
103
|
+
"--engine",
|
|
104
|
+
"-e",
|
|
105
|
+
default="dp",
|
|
106
|
+
type=click.Choice(["dp", "v2"], case_sensitive=False),
|
|
107
|
+
help="Engine (dp, v2)",
|
|
108
|
+
)
|
|
109
|
+
@click.option("--profile", "-p", default="balanced", help="Optimization profile")
|
|
110
|
+
@click.option(
|
|
111
|
+
"--objective",
|
|
112
|
+
default="feasibility_best",
|
|
113
|
+
type=click.Choice(["feasibility_best", "gc_target", "high_cai"], case_sensitive=False),
|
|
114
|
+
help="DP objective",
|
|
115
|
+
)
|
|
116
|
+
@click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
|
|
117
|
+
@click.option("--gc-max", type=float, default=55.0, help="Maximum target GC percentage")
|
|
118
|
+
@click.option("--template", "construct_template", help="Construct template name")
|
|
119
|
+
@click.option("--output", "-o", help="Output file")
|
|
120
|
+
@click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
|
|
121
|
+
@click.option(
|
|
122
|
+
"--scan-mode",
|
|
123
|
+
default="full",
|
|
124
|
+
type=click.Choice(["full", "fast"], case_sensitive=False),
|
|
125
|
+
help="Rule scan mode",
|
|
126
|
+
)
|
|
127
|
+
@click.option("--scan-include", help="Comma-separated scanner names to include")
|
|
128
|
+
@click.option("--scan-exclude", help="Comma-separated scanner names to exclude")
|
|
129
|
+
def optimize(
|
|
130
|
+
input_file,
|
|
131
|
+
engine,
|
|
132
|
+
profile,
|
|
133
|
+
objective,
|
|
134
|
+
gc_min,
|
|
135
|
+
gc_max,
|
|
136
|
+
construct_template,
|
|
137
|
+
output,
|
|
138
|
+
output_format,
|
|
139
|
+
scan_mode,
|
|
140
|
+
scan_include,
|
|
141
|
+
scan_exclude,
|
|
142
|
+
):
|
|
143
|
+
"""Optimize protein sequence"""
|
|
144
|
+
try:
|
|
145
|
+
# Read file
|
|
146
|
+
with open(input_file, encoding="utf-8") as f:
|
|
147
|
+
raw_input = f.read()
|
|
148
|
+
|
|
149
|
+
scan_include_list = _parse_csv_option(scan_include)
|
|
150
|
+
scan_exclude_list = _parse_csv_option(scan_exclude)
|
|
151
|
+
|
|
152
|
+
fasta_records = None
|
|
153
|
+
sequence = raw_input.strip()
|
|
154
|
+
if raw_input.lstrip().startswith(">"):
|
|
155
|
+
fasta_records = parse_fasta_records(raw_input)
|
|
156
|
+
if len(fasta_records) == 1:
|
|
157
|
+
sequence = fasta_records[0][1]
|
|
158
|
+
|
|
159
|
+
if fasta_records is not None and len(fasta_records) > 1:
|
|
160
|
+
if engine == "dp":
|
|
161
|
+
raise ValueError("Multi-FASTA input requires --engine v2.")
|
|
162
|
+
if construct_template:
|
|
163
|
+
raise ValueError("Multi-FASTA input does not support --template mode.")
|
|
164
|
+
if output_format.lower() != "fasta":
|
|
165
|
+
raise ValueError("Multi-FASTA input only supports FASTA output.")
|
|
166
|
+
|
|
167
|
+
optimizer = EngineRegistry.get(engine)
|
|
168
|
+
payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
|
|
169
|
+
if hasattr(optimizer, "optimize_batch"):
|
|
170
|
+
results = optimizer.optimize_batch(
|
|
171
|
+
payload,
|
|
172
|
+
profile=profile,
|
|
173
|
+
scan_mode=scan_mode,
|
|
174
|
+
scan_include=scan_include_list,
|
|
175
|
+
scan_exclude=scan_exclude_list,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
results = [
|
|
179
|
+
optimizer.optimize(
|
|
180
|
+
seq,
|
|
181
|
+
profile=profile,
|
|
182
|
+
scan_mode=scan_mode,
|
|
183
|
+
scan_include=scan_include_list,
|
|
184
|
+
scan_exclude=scan_exclude_list,
|
|
185
|
+
)
|
|
186
|
+
for _id, seq in fasta_records
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
combined_fasta = []
|
|
190
|
+
for idx, result in enumerate(results):
|
|
191
|
+
seq_id = payload[idx]["id"]
|
|
192
|
+
cai = result.metrics.get("cai", 0.0)
|
|
193
|
+
gc = result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0))
|
|
194
|
+
score = result.metrics.get("score", 0.0)
|
|
195
|
+
header = (
|
|
196
|
+
f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
|
|
197
|
+
f"gc={float(gc):.2f}|score={float(score):.3f}"
|
|
198
|
+
)
|
|
199
|
+
combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
|
|
200
|
+
out_content = "\n".join(combined_fasta) + "\n"
|
|
201
|
+
|
|
202
|
+
if output:
|
|
203
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
204
|
+
f.write(out_content)
|
|
205
|
+
click.echo(f"Saved batch FASTA to: {output}")
|
|
206
|
+
else:
|
|
207
|
+
click.echo(f"\n{out_content}")
|
|
208
|
+
click.echo(f"Batch optimized: {len(results)} sequences")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if engine == "dp":
|
|
212
|
+
if construct_template:
|
|
213
|
+
raise ValueError("DP engine does not support --template mode.")
|
|
214
|
+
if output_format.lower() != "fasta":
|
|
215
|
+
raise ValueError("DP engine only supports FASTA output.")
|
|
216
|
+
|
|
217
|
+
best, feasibility, recommendation_reason = _build_dp_result(
|
|
218
|
+
sequence,
|
|
219
|
+
objective=objective,
|
|
220
|
+
gc_min=gc_min,
|
|
221
|
+
gc_max=gc_max,
|
|
222
|
+
)
|
|
223
|
+
dna_sequence = best["dna_sequence"]
|
|
224
|
+
cai = float(best["cai"])
|
|
225
|
+
gc = float(best["gc"])
|
|
226
|
+
sequence_id = Path(input_file).stem or "factorforge_dp"
|
|
227
|
+
fasta = _format_dp_fasta(sequence_id, dna_sequence, cai, gc)
|
|
228
|
+
|
|
229
|
+
click.echo("Optimizing with DP feasibility engine...")
|
|
230
|
+
if output:
|
|
231
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
232
|
+
f.write(fasta)
|
|
233
|
+
click.echo(f"Saved to: {output}")
|
|
234
|
+
else:
|
|
235
|
+
click.echo(f"\n{fasta}")
|
|
236
|
+
|
|
237
|
+
click.echo("Metrics:")
|
|
238
|
+
click.echo(f" - cai: {cai:.3f}")
|
|
239
|
+
click.echo(f" - gc_percent: {gc:.2f}")
|
|
240
|
+
click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
|
|
241
|
+
click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
|
|
242
|
+
click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
|
|
243
|
+
click.echo(f" - recommendation_reason: {recommendation_reason}")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if engine == "v2" and construct_template:
|
|
247
|
+
from factorforge.engines.v2.pipeline import OptimizationPipeline
|
|
248
|
+
|
|
249
|
+
pipeline = OptimizationPipeline(profile=profile, construct_template=construct_template)
|
|
250
|
+
result = pipeline.run(
|
|
251
|
+
sequence,
|
|
252
|
+
scan_mode=scan_mode,
|
|
253
|
+
scan_include=scan_include_list,
|
|
254
|
+
scan_exclude=scan_exclude_list,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if output_format.lower() == "genbank" and not output:
|
|
258
|
+
raise ValueError("GenBank output requires --output file path.")
|
|
259
|
+
|
|
260
|
+
if output:
|
|
261
|
+
result.save(Path(output), format=output_format)
|
|
262
|
+
click.echo(f"Saved to: {output}")
|
|
263
|
+
else:
|
|
264
|
+
click.echo(f"\n{result.sequence}\n")
|
|
265
|
+
|
|
266
|
+
click.echo("Metrics:")
|
|
267
|
+
for key, value in result.metadata.get("metrics", {}).items():
|
|
268
|
+
click.echo(f" - {key}: {value}")
|
|
269
|
+
else:
|
|
270
|
+
if output_format.lower() != "fasta":
|
|
271
|
+
raise ValueError("Non-FASTA output requires --template with v2 pipeline.")
|
|
272
|
+
|
|
273
|
+
# Get engine
|
|
274
|
+
optimizer = EngineRegistry.get(engine)
|
|
275
|
+
|
|
276
|
+
# Optimize
|
|
277
|
+
click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
|
|
278
|
+
result = optimizer.optimize(
|
|
279
|
+
sequence,
|
|
280
|
+
profile=profile,
|
|
281
|
+
scan_mode=scan_mode,
|
|
282
|
+
scan_include=scan_include_list,
|
|
283
|
+
scan_exclude=scan_exclude_list,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Output results
|
|
287
|
+
if output:
|
|
288
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
289
|
+
f.write(result.sequence)
|
|
290
|
+
click.echo(f"Saved to: {output}")
|
|
291
|
+
else:
|
|
292
|
+
click.echo(f"\n{result.sequence}\n")
|
|
293
|
+
|
|
294
|
+
# Output metrics
|
|
295
|
+
click.echo("Metrics:")
|
|
296
|
+
for key, value in result.metrics.items():
|
|
297
|
+
click.echo(f" - {key}: {value}")
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
click.echo(f"Error: {e}", err=True)
|
|
301
|
+
raise click.Abort()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
cli()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exporter interface"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Exporter(ABC):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def export(self, data: Any, format: str) -> str:
|
|
12
|
+
"""Export data to the requested format."""
|
|
13
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimizer Engine Interface
|
|
3
|
+
|
|
4
|
+
Interface that all optimization engines (v1, v2, v3...) must implement
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OptimizationResult:
|
|
14
|
+
"""Optimization result"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
sequence: str,
|
|
19
|
+
metrics: dict[str, float],
|
|
20
|
+
metadata: dict[str, Any] | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.sequence = sequence
|
|
23
|
+
self.metrics = metrics
|
|
24
|
+
self.metadata = metadata or {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OptimizerEngine(ABC):
|
|
28
|
+
"""Abstract optimization engine interface"""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
"""Engine name"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def version(self) -> str:
|
|
39
|
+
"""Engine version"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def optimize(
|
|
44
|
+
self,
|
|
45
|
+
sequence: str,
|
|
46
|
+
profile: str | None = None,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> OptimizationResult:
|
|
49
|
+
"""
|
|
50
|
+
Optimize a sequence
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
sequence: Input protein sequence
|
|
54
|
+
profile: Optimization profile (e.g., balanced, high_gc)
|
|
55
|
+
**kwargs: Additional parameters
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
OptimizationResult
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def validate(self, sequence: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Validate input
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
sequence: Sequence to validate
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
bool: True if valid
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def get_metadata(self) -> dict[str, Any]:
|
|
76
|
+
"""Engine metadata"""
|
|
77
|
+
return {
|
|
78
|
+
"name": self.name,
|
|
79
|
+
"version": self.version,
|
|
80
|
+
"supported_profiles": self.get_supported_profiles(),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def get_supported_profiles(self) -> list[str]:
|
|
84
|
+
"""List of supported profiles"""
|
|
85
|
+
return []
|