"""
Generate Sequences tab — tools for sequence manipulation and generation.
Provides:
1. Clean for Cloning — prepare sequences for cloning vectors
2. Codon Optimization — optimize CDS for target organism
3. Imported generative models — apply models from the repository
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Optional
import panel as pn
import param
from core.models.sequence import mRNASequence
if TYPE_CHECKING:
from ui.state import AppState
logger = logging.getLogger(__name__)
class GenerateSequencesPanel(param.Parameterized):
"""Generate Sequences tab panel."""
def __init__(self, state: "AppState", **params: object) -> None:
super().__init__(**params)
self._state = state
self._output_pane = pn.Column(sizing_mode="stretch_width")
@param.depends("_state.worklist", "_state.model_registry")
def panel(self) -> pn.Column:
# ── Header ────────────────────────────────────────────────────────────
header = pn.pane.HTML(
'
'
'Generate Sequences
'
''
'Clean, optimize, and generate mRNA sequences using built-in tools '
'and imported generative models.
'
)
# ── Input selector ────────────────────────────────────────────────────
input_mode = pn.widgets.Select(
name="Input", options=["Worklist", "Single Sequence"],
value="Worklist", width=200,
)
single_seq_input = pn.widgets.TextAreaInput(
name="Sequence (DNA)",
placeholder="Paste CDS sequence here...",
height=80,
width=500,
visible=False,
)
def toggle_input(event):
single_seq_input.visible = (event.new == "Single Sequence")
input_mode.param.watch(toggle_input, "value")
input_section = pn.Row(input_mode, single_seq_input, sizing_mode="stretch_width")
# ── Built-in Tools ────────────────────────────────────────────────────
clean_btn = pn.widgets.Button(
name="Clean for Cloning",
button_type="primary",
width=180,
margin=(4, 4),
)
clean_btn.on_click(lambda e: self._run_clean_for_cloning(input_mode.value, single_seq_input.value))
optimize_btn = pn.widgets.Button(
name="Codon Optimization",
button_type="primary",
width=180,
margin=(4, 4),
)
optimize_btn.on_click(lambda e: self._run_codon_optimization(input_mode.value, single_seq_input.value))
# Clean for Cloning settings
clean_settings = pn.Column(
pn.pane.HTML('Clean for Cloning Settings
'),
sizing_mode="stretch_width",
visible=False,
)
self._enzyme_avoid = pn.widgets.MultiChoice(
name="Enzymes to avoid",
options=["BsaI", "BbsI", "Esp3I", "BsmBI", "EcoRI", "BamHI", "HindIII", "NotI", "XhoI"],
value=["BsaI"],
width=400,
)
self._preferred_stop = pn.widgets.Select(
name="Preferred stop", options=["TAA", "TAG", "TGA"], value="TAA", width=100,
)
self._double_stop = pn.widgets.Toggle(name="Double stop codon", value=True, width=150)
self._max_homopolymer = pn.widgets.IntSlider(name="Max homopolymer", start=4, end=10, value=6, width=200)
clean_settings.extend([
pn.Row(self._enzyme_avoid),
pn.Row(self._preferred_stop, self._double_stop, self._max_homopolymer),
])
def toggle_clean_settings(event):
clean_settings.visible = not clean_settings.visible
clean_settings_toggle = pn.widgets.Button(name="Settings", button_type="light", width=70, margin=(4, 0))
clean_settings_toggle.on_click(toggle_clean_settings)
# Codon optimization settings
opt_settings = pn.Column(
pn.pane.HTML('Codon Optimization Settings
'),
sizing_mode="stretch_width",
visible=False,
)
self._opt_organism = pn.widgets.Select(
name="Target organism",
options=["Human", "Mouse", "E. coli", "CHO", "Yeast", "Zebrafish"],
value="Human", width=200,
)
self._opt_strategy = pn.widgets.Select(
name="Strategy",
options=["Match host CAI", "Harmonize", "Balance"],
value="Match host CAI", width=200,
)
self._opt_min_cai = pn.widgets.FloatSlider(name="Min CAI target", start=0.5, end=1.0, value=0.8, step=0.05, width=250)
opt_settings.extend([
pn.Row(self._opt_organism, self._opt_strategy),
self._opt_min_cai,
])
opt_settings_toggle = pn.widgets.Button(name="Settings", button_type="light", width=70, margin=(4, 0))
opt_settings_toggle.on_click(lambda e: setattr(opt_settings, "visible", not opt_settings.visible))
builtin_section = pn.Column(
pn.pane.HTML(
''
'Built-in Tools
'
),
pn.Row(clean_btn, clean_settings_toggle, optimize_btn, opt_settings_toggle),
clean_settings,
opt_settings,
sizing_mode="stretch_width",
styles={"background": "#FFFFFF", "padding": "12px", "border-radius": "6px",
"border": "1px solid #E2E8F0", "margin-bottom": "12px"},
)
# ── Imported Generative Models ────────────────────────────────────────
gen_models = []
if self._state.model_registry:
gen_models = self._state.model_registry.generative_models
if gen_models:
model_buttons = []
for model_reg in gen_models:
btn = pn.widgets.Button(
name=f"{model_reg.model.name}",
button_type="light",
width=180,
margin=(4, 4),
stylesheets=["""
:host .bk-btn {
border: 1px solid #6D28D9;
color: #6D28D9;
font-size: 11px;
}
"""],
)
btn.on_click(lambda e, m=model_reg: self._run_generative_model(m, input_mode.value, single_seq_input.value))
model_buttons.append(btn)
gen_section = pn.Column(
pn.pane.HTML(
''
'Imported Generative Models
'
),
pn.Row(*model_buttons, sizing_mode="stretch_width"),
sizing_mode="stretch_width",
styles={"background": "#FFFFFF", "padding": "12px", "border-radius": "6px",
"border": "1px solid #E2E8F0", "margin-bottom": "12px"},
)
else:
gen_section = pn.Column(
pn.pane.HTML(
''
'Imported Generative Models
'
''
'No generative models imported. Use the '
'Model Repository '
'tab to import models.
'
),
sizing_mode="stretch_width",
styles={"background": "#FFFFFF", "padding": "12px", "border-radius": "6px",
"border": "1px solid #E2E8F0", "margin-bottom": "12px"},
)
# ── Output Section ────────────────────────────────────────────────────
self._generated_sequences = []
self._worklist_name_input = pn.widgets.TextInput(
name="Worklist Name",
placeholder="e.g. Optimized Batch 1",
width=250,
)
add_to_worklist_btn = pn.widgets.Button(
name="+ Add to Worklist",
button_type="success",
width=160,
margin=(20, 4, 4, 4),
)
self._add_btn = add_to_worklist_btn
add_to_worklist_btn.on_click(self._on_add_to_worklist)
self._add_to_wl_section = pn.Column(
pn.pane.HTML(
''
'Save to Worklist
'
''
'Create a new worklist from the generated sequences.
'
),
pn.Row(self._worklist_name_input, add_to_worklist_btn),
sizing_mode="stretch_width",
styles={"background": "#F8FAFC", "padding": "12px", "border-radius": "6px",
"border": "1px solid #E2E8F0", "margin-top": "8px"},
visible=False,
)
output_section = pn.Column(
pn.pane.HTML(
''
'Output
'
),
self._output_pane,
self._add_to_wl_section,
sizing_mode="stretch_width",
styles={"background": "#FFFFFF", "padding": "12px", "border-radius": "6px",
"border": "1px solid #E2E8F0"},
)
return pn.Column(
header,
input_section,
builtin_section,
gen_section,
output_section,
sizing_mode="stretch_width",
styles={"padding": "8px 16px"},
)
def _get_input_sequences(self, mode: str, single_text: str):
"""Get sequences based on input mode."""
if mode == "Single Sequence" and single_text.strip():
seq = mRNASequence(
name="input_sequence",
source="local",
cds=single_text.strip().upper().replace("U", "T"),
)
return [seq]
elif self._state.worklist and self._state.worklist.count > 0:
return [item.sequence for item in self._state.worklist.items]
return []
def _run_clean_for_cloning(self, mode: str, single_text: str) -> None:
"""Run Clean for Cloning on input sequences."""
from core.sequence_tools.clean_for_cloning import clean_for_cloning
sequences = self._get_input_sequences(mode, single_text)
if not sequences:
self._output_pane.clear()
self._output_pane.append(pn.pane.HTML('No input sequences available.
'))
return
self._output_pane.clear()
self._generated_sequences = []
results_html = []
for seq in sequences:
cds = seq.cds or seq.assembled_sequence
if not cds:
continue
result = clean_for_cloning(
cds=cds,
enzymes_to_avoid=self._enzyme_avoid.value,
preferred_stop=self._preferred_stop.value,
use_double_stop=self._double_stop.value,
max_homopolymer=self._max_homopolymer.value,
)
# Create cleaned sequence
cleaned_seq = mRNASequence(
name=f"{seq.name}_cleaned",
source="local",
cds=result.cleaned,
five_prime_utr=seq.five_prime_utr,
kozak=seq.kozak,
three_prime_utr=seq.three_prime_utr,
poly_a=seq.poly_a,
)
self._generated_sequences.append(cleaned_seq)
# Build diff summary
changes_html = "".join(
f'• {c}
'
for c in result.changes[:10]
)
if len(result.changes) > 10:
changes_html += f'... +{len(result.changes) - 10} more
'
len_diff = len(result.cleaned) - len(result.original)
len_str = f"+{len_diff}" if len_diff > 0 else str(len_diff)
results_html.append(
f''
f'
{seq.name}
'
f'
'
f'Length: {len(result.original)} → {len(result.cleaned)} ({len_str}) | '
f'Sites removed: {result.restriction_sites_removed} | '
f'Homopolymers flagged: {result.homopolymers_shortened}'
f'{"| Double stop: Yes" if result.double_stop_added else ""}'
f'
'
f'{changes_html}'
f'
'
)
summary = (
f''
f'Cleaned {len(self._generated_sequences)} sequence(s) for cloning
'
)
self._output_pane.append(pn.pane.HTML(summary + "".join(results_html), sizing_mode="stretch_width"))
if self._generated_sequences:
self._worklist_name_input.value = "Cleaned Sequences"
self._add_to_wl_section.visible = True
def _run_codon_optimization(self, mode: str, single_text: str) -> None:
"""Run codon optimization on input sequences."""
from core.sequence_tools.codon_optimizer import optimize_codons
sequences = self._get_input_sequences(mode, single_text)
if not sequences:
self._output_pane.clear()
self._output_pane.append(pn.pane.HTML('No input sequences available.
'))
return
strategy_map = {"Match host CAI": "match_host", "Harmonize": "harmonize", "Balance": "balance"}
strategy = strategy_map.get(self._opt_strategy.value, "match_host")
self._output_pane.clear()
self._generated_sequences = []
results_html = []
for seq in sequences:
cds = seq.cds or seq.assembled_sequence
if not cds:
continue
result = optimize_codons(
cds=cds,
organism=self._opt_organism.value,
min_cai_target=self._opt_min_cai.value,
strategy=strategy,
)
# Create optimized sequence
opt_seq = mRNASequence(
name=f"{seq.name}_optimized",
source="local",
cds=result.optimized_cds,
five_prime_utr=seq.five_prime_utr,
kozak=seq.kozak,
three_prime_utr=seq.three_prime_utr,
poly_a=seq.poly_a,
)
self._generated_sequences.append(opt_seq)
results_html.append(
f''
f'
{seq.name}
'
f'
'
f'CAI: {result.original_cai:.3f} → {result.optimized_cai:.3f} | '
f'Codons changed: {result.codons_changed}/{result.total_codons} | '
f'Organism: {result.organism}'
f'
'
f'
'
)
summary = (
f''
f'Optimized {len(self._generated_sequences)} sequence(s) for {self._opt_organism.value}
'
)
self._output_pane.append(pn.pane.HTML(summary + "".join(results_html), sizing_mode="stretch_width"))
if self._generated_sequences:
self._worklist_name_input.value = f"Optimized ({self._opt_organism.value})"
self._add_to_wl_section.visible = True
def _run_generative_model(self, model_reg, mode: str, single_text: str) -> None:
"""Run an imported generative model."""
self._output_pane.clear()
model = model_reg.model
self._output_pane.append(pn.pane.HTML(
f''
f'Running {model.name}...
'
))
try:
results = model.generate(constraints={}, n=5)
if results:
self._generated_sequences = results
results_html = "".join(
f''
f'
{seq.name}
'
f'
Length: {seq.length} nt
'
f'
'
for seq in results
)
self._output_pane.clear()
self._output_pane.append(pn.pane.HTML(
f''
f'Generated {len(results)} sequence(s) with {model.name}
'
f'{results_html}'
))
self._worklist_name_input.value = f"{model.name} Output"
self._add_to_wl_section.visible = True
else:
self._output_pane.clear()
self._output_pane.append(pn.pane.HTML(
f'{model.name} (demo mode): '
f'Model registered but not running actual inference. '
f'In production, this would generate sequences using the model.
'
))
self._add_to_wl_section.visible = False
except Exception as e:
self._output_pane.clear()
self._output_pane.append(pn.pane.HTML(
f'Error: {e}
'
))
def _on_add_to_worklist(self, event) -> None:
"""Create a new worklist from generated sequences."""
if not self._generated_sequences:
return
from core.models.worklist import Worklist
name = self._worklist_name_input.value.strip()
if not name:
name = "Generated Sequences"
new_wl = Worklist(name=name)
new_wl.add_many(self._generated_sequences, origin="generated")
updated = list(self._state.worklists) + [new_wl]
self._state.worklists = updated
self._state.active_worklist_index = len(updated) - 1
self._state.worklist = new_wl
self._state.active_tab = "worklist"
count = len(self._generated_sequences)
self._add_to_wl_section.visible = False
self._generated_sequences = []
self._state.set_status(f"Created worklist '{name}' with {count} sequence(s)")