""" 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)")