"""
Analysis Settings panel — configurable parameters for sequence analysis.
Provides a comprehensive settings modal for controlling all analysis modules:
GC content, CAI, homopolymers, restriction enzymes, CDS validation,
Kozak, secondary structure, uridine, and dinucleotide analysis.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict
import panel as pn
import param
from core.analysis.restriction_sites import COMMON_ENZYMES
if TYPE_CHECKING:
from ui.state import AppState
# Extended enzyme library organized by category
ENZYME_CATEGORIES = {
"Type IIS / Golden Gate": ["BsaI", "BbsI", "Esp3I", "SapI", "BsmBI"],
"Classic 6-cutters": [
"EcoRI", "HindIII", "BamHI", "XhoI", "XbaI", "NcoI", "NheI",
"SpeI", "NotI", "SalI", "PstI", "KpnI",
],
"Blunt cutters": ["SmaI", "EcoRV", "HpaI", "StuI", "ScaI"],
"Methylation-sensitive": ["DpnI", "MboI", "Sau3AI"],
"Rare cutters (8+ bp)": ["NotI", "SfiI", "PacI", "AscI", "FseI", "SwaI", "PmeI"],
"IVT cleanup": ["DpnI"],
}
# Extended recognition sequences (adding missing ones to COMMON_ENZYMES)
EXTENDED_ENZYMES: Dict[str, str] = {
**COMMON_ENZYMES,
"BsmBI": "CGTCTC",
"EcoRV": "GATATC",
"HpaI": "GTTAAC",
"StuI": "AGGCCT",
"ScaI": "AGTACT",
"MboI": "GATC",
"Sau3AI": "GATC",
"SfiI": "GGCCNNNNNGGCC",
"PacI": "TTAATTAA",
"AscI": "GGCGCGCC",
"FseI": "GGCCGGCC",
"SwaI": "ATTTAAAT",
"PmeI": "GTTTAAAC",
}
DEFAULT_SETTINGS: Dict[str, Any] = {
# GC Content
"gc_enabled": True,
"gc_window": 100,
"gc_step": 1,
"gc_target_min": 45.0,
"gc_target_max": 65.0,
"gc_flag_outside_target": True,
"gc_by_codon_position": True,
# CAI
"cai_enabled": True,
"cai_organism": "Human",
"cai_min_acceptable": 0.7,
"cai_rare_codon_threshold": 0.1,
# Homopolymers
"homopolymer_enabled": True,
"homopolymer_min_run": 5,
"homopolymer_max_allowed": 8,
"homopolymer_bases": ["A", "T", "G", "C"],
# Restriction enzymes
"restriction_enabled": True,
"restriction_enzymes": list(COMMON_ENZYMES.keys()),
"restriction_mode": "Report all sites found",
# CDS Validation
"cds_enabled": True,
"cds_check_start": True,
"cds_check_stop": True,
"cds_double_stop": False,
"cds_accepted_stops": ["TAA", "TAG", "TGA"],
"cds_check_frame": True,
# Kozak
"kozak_enabled": True,
"kozak_min_strength": "Adequate",
"kozak_flag_r3": True,
# Structure
"structure_enabled": True,
"structure_engine": "ViennaRNA",
"structure_temperature": 37.0,
# Uridine
"uridine_enabled": False,
"uridine_flag_threshold": 0.40,
# Dinucleotide
"dinucleotide_enabled": False,
"dinucleotide_flag_cpg": True,
"dinucleotide_flag_upa": True,
}
class AnalysisSettingsPanel(param.Parameterized):
"""Analysis settings configuration panel."""
def __init__(self, state: "AppState", **params: object) -> None:
super().__init__(**params)
self._state = state
# Initialize settings from state or defaults
if not self._state.analysis_settings:
self._state.analysis_settings = dict(DEFAULT_SETTINGS)
def _get(self, key: str) -> Any:
return self._state.analysis_settings.get(key, DEFAULT_SETTINGS.get(key))
def _section_header(self, title: str, toggle_key: str) -> pn.Row:
"""Build a collapsible section header with enable toggle."""
toggle = pn.widgets.Toggle(
name="",
value=self._get(toggle_key),
width=40,
margin=(4, 8),
)
def on_toggle(event):
settings = dict(self._state.analysis_settings)
settings[toggle_key] = event.new
self._state.analysis_settings = settings
toggle.param.watch(on_toggle, "value")
return pn.Row(
pn.pane.HTML(
f'
'
f'{title}
'
),
pn.layout.HSpacer(),
toggle,
sizing_mode="stretch_width",
)
def _build_gc_section(self) -> pn.Column:
s = self._state.analysis_settings
window = pn.widgets.IntSlider(name="Sliding window size", start=20, end=500, value=self._get("gc_window"), step=10, width=300)
step = pn.widgets.IntSlider(name="Sliding window step", start=1, end=50, value=self._get("gc_step"), width=300)
target_min = pn.widgets.FloatSlider(name="Target GC min %", start=20, end=80, value=self._get("gc_target_min"), step=1, width=300)
target_max = pn.widgets.FloatSlider(name="Target GC max %", start=20, end=80, value=self._get("gc_target_max"), step=1, width=300)
flag_outside = pn.widgets.Toggle(name="Flag outside target", value=self._get("gc_flag_outside_target"), width=150)
by_codon = pn.widgets.Toggle(name="GC by codon position", value=self._get("gc_by_codon_position"), width=150)
for w, k in [(window, "gc_window"), (step, "gc_step"), (target_min, "gc_target_min"),
(target_max, "gc_target_max"), (flag_outside, "gc_flag_outside_target"),
(by_codon, "gc_by_codon_position")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("GC Content", "gc_enabled"),
pn.Row(window, step),
pn.Row(target_min, target_max),
pn.Row(flag_outside, by_codon),
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_cai_section(self) -> pn.Column:
organism = pn.widgets.Select(
name="Organism", options=["Human", "Mouse", "E. coli", "CHO", "Yeast", "Custom"],
value=self._get("cai_organism"), width=200,
)
min_cai = pn.widgets.FloatSlider(name="Min acceptable CAI", start=0.0, end=1.0, value=self._get("cai_min_acceptable"), step=0.05, width=300)
rare_threshold = pn.widgets.FloatSlider(name="Rare codon threshold", start=0.0, end=1.0, value=self._get("cai_rare_codon_threshold"), step=0.05, width=300)
for w, k in [(organism, "cai_organism"), (min_cai, "cai_min_acceptable"), (rare_threshold, "cai_rare_codon_threshold")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("Codon Adaptation Index", "cai_enabled"),
pn.Row(organism, min_cai),
rare_threshold,
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_homopolymer_section(self) -> pn.Column:
min_run = pn.widgets.IntSlider(name="Min run length to report", start=3, end=10, value=self._get("homopolymer_min_run"), width=300)
max_allowed = pn.widgets.IntSlider(name="Max allowed run length", start=4, end=15, value=self._get("homopolymer_max_allowed"), width=300)
bases = pn.widgets.MultiChoice(
name="Flag bases", options=["A", "T", "G", "C"],
value=self._get("homopolymer_bases"), width=300,
)
for w, k in [(min_run, "homopolymer_min_run"), (max_allowed, "homopolymer_max_allowed"), (bases, "homopolymer_bases")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("Homopolymer Detection", "homopolymer_enabled"),
pn.Row(min_run, max_allowed),
bases,
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_restriction_section(self) -> pn.Column:
# Enzyme categories as expandable sections
all_enzymes = sorted(set(
e for enzymes in ENZYME_CATEGORIES.values() for e in enzymes
))
enzyme_select = pn.widgets.MultiChoice(
name="Active enzymes",
options=all_enzymes,
value=self._get("restriction_enzymes"),
width=600,
)
mode = pn.widgets.Select(
name="Mode",
options=["Report all sites found", "Flag only unwanted sites"],
value=self._get("restriction_mode"),
width=300,
)
# Category quick-select buttons
category_buttons = []
for cat_name, cat_enzymes in ENZYME_CATEGORIES.items():
btn = pn.widgets.Button(name=cat_name, button_type="light", width=180, margin=(2, 2),
stylesheets=[":host .bk-btn { font-size: 10px; padding: 3px 8px; }"])
def _add_category(event, enzymes=cat_enzymes):
current = list(enzyme_select.value)
for e in enzymes:
if e not in current:
current.append(e)
enzyme_select.value = current
btn.on_click(_add_category)
category_buttons.append(btn)
def _update_enzymes(event):
s = dict(self._state.analysis_settings)
s["restriction_enzymes"] = event.new
self._state.analysis_settings = s
def _update_mode(event):
s = dict(self._state.analysis_settings)
s["restriction_mode"] = event.new
self._state.analysis_settings = s
enzyme_select.param.watch(_update_enzymes, "value")
mode.param.watch(_update_mode, "value")
return pn.Column(
self._section_header("Restriction Enzyme Library", "restriction_enabled"),
pn.pane.HTML('Quick add by category:
'),
pn.Row(*category_buttons, sizing_mode="stretch_width"),
enzyme_select,
mode,
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_cds_section(self) -> pn.Column:
check_start = pn.widgets.Toggle(name="Check start codon (ATG)", value=self._get("cds_check_start"), width=200)
check_stop = pn.widgets.Toggle(name="Check stop codon", value=self._get("cds_check_stop"), width=200)
double_stop = pn.widgets.Toggle(name="Require double stop codon", value=self._get("cds_double_stop"), width=200)
accepted_stops = pn.widgets.MultiChoice(
name="Accepted stop codons", options=["TAA", "TAG", "TGA"],
value=self._get("cds_accepted_stops"), width=300,
)
check_frame = pn.widgets.Toggle(name="Check reading frame (div by 3)", value=self._get("cds_check_frame"), width=250)
for w, k in [(check_start, "cds_check_start"), (check_stop, "cds_check_stop"),
(double_stop, "cds_double_stop"), (accepted_stops, "cds_accepted_stops"),
(check_frame, "cds_check_frame")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("CDS Validation", "cds_enabled"),
pn.Row(check_start, check_stop, double_stop),
accepted_stops,
check_frame,
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_kozak_section(self) -> pn.Column:
min_strength = pn.widgets.Select(
name="Min strength threshold", options=["Strong", "Adequate", "Weak"],
value=self._get("kozak_min_strength"), width=200,
)
flag_r3 = pn.widgets.Toggle(name="Flag non-consensus -3", value=self._get("kozak_flag_r3"), width=200)
for w, k in [(min_strength, "kozak_min_strength"), (flag_r3, "kozak_flag_r3")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("Kozak Context", "kozak_enabled"),
pn.Row(min_strength, flag_r3),
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_structure_section(self) -> pn.Column:
engine = pn.widgets.Select(
name="Engine", options=["ViennaRNA", "LinearFold (if available)"],
value=self._get("structure_engine"), width=250,
)
temp = pn.widgets.FloatInput(name="Temperature (°C)", value=self._get("structure_temperature"), step=0.5, width=150)
for w, k in [(engine, "structure_engine"), (temp, "structure_temperature")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("Secondary Structure", "structure_enabled"),
pn.Row(engine, temp),
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_uridine_section(self) -> pn.Column:
threshold = pn.widgets.FloatSlider(
name="High-U stretch threshold", start=0.2, end=0.6, value=self._get("uridine_flag_threshold"), step=0.05, width=300,
)
def _update(event):
s = dict(self._state.analysis_settings)
s["uridine_flag_threshold"] = event.new
self._state.analysis_settings = s
threshold.param.watch(_update, "value")
return pn.Column(
self._section_header("Uridine Content", "uridine_enabled"),
pn.pane.HTML('Report U/A ratio and flag high-uridine stretches.
'),
threshold,
pn.layout.Divider(),
sizing_mode="stretch_width",
)
def _build_dinucleotide_section(self) -> pn.Column:
flag_cpg = pn.widgets.Toggle(name="Flag CpG dinucleotides", value=self._get("dinucleotide_flag_cpg"), width=200)
flag_upa = pn.widgets.Toggle(name="Flag UpA dinucleotides", value=self._get("dinucleotide_flag_upa"), width=200)
for w, k in [(flag_cpg, "dinucleotide_flag_cpg"), (flag_upa, "dinucleotide_flag_upa")]:
def _update(event, key=k):
s = dict(self._state.analysis_settings)
s[key] = event.new
self._state.analysis_settings = s
w.param.watch(_update, "value")
return pn.Column(
self._section_header("Dinucleotide Frequency", "dinucleotide_enabled"),
pn.pane.HTML('CpG = immunostimulatory; UpA = mRNA instability.
'),
pn.Row(flag_cpg, flag_upa),
sizing_mode="stretch_width",
)
def panel(self) -> pn.Column:
"""Build the full settings panel."""
reset_btn = pn.widgets.Button(name="Reset to Defaults", button_type="warning", width=150, margin=(8, 0))
def on_reset(event):
self._state.analysis_settings = dict(DEFAULT_SETTINGS)
self._state.set_status("Analysis settings reset to defaults")
reset_btn.on_click(on_reset)
apply_btn = pn.widgets.Button(name="Apply & Close", button_type="success", width=150, margin=(8, 8))
def on_apply(event):
self._state.set_status("Analysis settings updated")
apply_btn.on_click(on_apply)
return pn.Column(
pn.pane.HTML(
''
'Analysis Settings
'
''
'Configure parameters for each analysis module. Changes apply '
'to the next analysis run.
'
),
self._build_gc_section(),
self._build_cai_section(),
self._build_homopolymer_section(),
self._build_restriction_section(),
self._build_cds_section(),
self._build_kozak_section(),
self._build_structure_section(),
self._build_uridine_section(),
self._build_dinucleotide_section(),
pn.Row(reset_btn, apply_btn),
sizing_mode="stretch_width",
styles={"padding": "8px 16px", "max-height": "70vh", "overflow-y": "auto"},
)