Source code for simudo.gui.panels.bcs

"""
Simudo GUI — Boundary Conditions panel.

Shows left and right contact cards.  Each card has:
  • role   (Select: swept / reference)
  • per-band BC  (RadioButtonGroup: ohmic / zero_current / SRV)
  • when SRV selected: value + unit inputs for surface recombination velocity
"""

from __future__ import annotations
from typing import TYPE_CHECKING

import panel as pn

from simudo.gui.panels.shared import (
    INPUT_SS, SELECT_SS, RADIO_SS,
    sec_header, label,
)

if TYPE_CHECKING:
    from simudo.gui.app import SimudoApp

_ROLES = ["swept", "reference"]
_BC_OPTS = ["ohmic", "zero_current", "SRV"]

# Map display label ↔ YAML type string
_DISPLAY_TO_TYPE = {
    "ohmic":         "ohmic",
    "zero_current":  "zero_current",
    "SRV":           "surface_recombination",
}
_TYPE_TO_DISPLAY = {v: k for k, v in _DISPLAY_TO_TYPE.items()}

_SRV_UNITS = ["cm/s", "m/s"]


def _bc_display(bc_spec) -> str:
    """Return the RadioButtonGroup label for a bc_spec (string or dict)."""
    if isinstance(bc_spec, dict):
        t = bc_spec.get("type", "ohmic")
    else:
        t = bc_spec or "ohmic"
    return _TYPE_TO_DISPLAY.get(t, "ohmic")


def _srv_params(bc_spec):
    """Return (value_str, unit) from a bc_spec, with defaults."""
    if isinstance(bc_spec, dict):
        srv = bc_spec.get("srv", {})
        if isinstance(srv, dict):
            return str(srv.get("value", 1000.0)), str(srv.get("unit", "cm/s"))
    return "1000.0", "cm/s"


[docs] class BCsPanel: def __init__(self, app: "SimudoApp"): self.app = app self._left_col = pn.Column(sizing_mode="stretch_width", styles={"gap": "4px"}) self._right_col = pn.Column(sizing_mode="stretch_width", styles={"gap": "4px"}) self._view = pn.Column( sec_header("BOUNDARY CONDITIONS"), pn.Row( pn.Column( pn.pane.HTML( '<div style="font-size:11px;font-weight:700;color:#8a9bb5;' 'padding:4px 0 6px;">LEFT CONTACT</div>', height=24, margin=0, ), self._left_col, sizing_mode="stretch_width", styles={ "background": "#1a2435", "border": "1px solid #2d3748", "border-radius": "4px", "padding": "8px", "gap": "4px", }, ), pn.Column( pn.pane.HTML( '<div style="font-size:11px;font-weight:700;color:#8a9bb5;' 'padding:4px 0 6px;">RIGHT CONTACT</div>', height=24, margin=0, ), self._right_col, sizing_mode="stretch_width", styles={ "background": "#1a2435", "border": "1px solid #2d3748", "border-radius": "4px", "padding": "8px", "gap": "4px", }, ), sizing_mode="stretch_width", styles={"gap": "8px"}, ), sizing_mode="stretch_width", styles={"padding": "12px", "gap": "8px"}, )
[docs] def view(self) -> pn.Column: self._refresh() return self._view
def _refresh(self): self._left_col.objects = self._build_contact_widgets("left") self._right_col.objects = self._build_contact_widgets("right") def _build_contact_widgets(self, side: str) -> list: contacts = self.app.project.contacts contact = contacts.get(side) if contact is None: return [pn.pane.HTML('<span style="color:#556070;font-size:12px;">No contact defined.</span>')] bands = self.app.project.bands # ── Role ───────────────────────────────────────────────────────────── role_sel = pn.widgets.Select( value=contact.role if contact.role in _ROLES else _ROLES[0], options=_ROLES, height=26, width=110, margin=0, stylesheets=SELECT_SS, ) def _on_role(e, s=side): contacts[s].role = e.new self.app.autosave() role_sel.param.watch(_on_role, "value") role_row = pn.Row( label("Role:", 36), role_sel, margin=0, styles={"gap": "4px", "align-items": "center"}, ) # ── Per-band BCs ────────────────────────────────────────────────────── band_rows = [] for band in bands: current_bc = contact.band_bcs.get(band.name, "ohmic") display_val = _bc_display(current_bc) srv_val_str, srv_unit = _srv_params(current_bc) bc_rbg = pn.widgets.RadioButtonGroup( options=_BC_OPTS, value=display_val if display_val in _BC_OPTS else "ohmic", height=26, margin=0, stylesheets=RADIO_SS, ) # SRV sub-row: value TextInput + unit Select srv_val_in = pn.widgets.TextInput( value=srv_val_str, placeholder="1000.0", width=80, height=26, margin=0, stylesheets=INPUT_SS, ) srv_unit_sel = pn.widgets.Select( value=srv_unit if srv_unit in _SRV_UNITS else "cm/s", options=_SRV_UNITS, width=70, height=26, margin=0, stylesheets=SELECT_SS, ) srv_row = pn.Row( label("SRV:", 36), srv_val_in, srv_unit_sel, margin=0, styles={"gap": "4px", "align-items": "center"}, visible=(display_val == "SRV"), ) def _on_bc(e, s=side, bn=band.name, srow=srv_row, sval=srv_val_in, sunit=srv_unit_sel): srow.visible = (e.new == "SRV") if e.new == "SRV": try: val = float(sval.value) except (ValueError, TypeError): val = 1000.0 contacts[s].band_bcs[bn] = { "type": "surface_recombination", "srv": {"value": val, "unit": sunit.value}, } else: contacts[s].band_bcs[bn] = _DISPLAY_TO_TYPE[e.new] self.app.autosave() bc_rbg.param.watch(_on_bc, "value") def _on_srv_val(e, s=side, bn=band.name, sunit=srv_unit_sel): try: val = float(e.new) except (ValueError, TypeError): return contacts[s].band_bcs[bn] = { "type": "surface_recombination", "srv": {"value": val, "unit": sunit.value}, } self.app.autosave() srv_val_in.param.watch(_on_srv_val, "value") def _on_srv_unit(e, s=side, bn=band.name, sval=srv_val_in): try: val = float(sval.value) except (ValueError, TypeError): val = 1000.0 contacts[s].band_bcs[bn] = { "type": "surface_recombination", "srv": {"value": val, "unit": e.new}, } self.app.autosave() srv_unit_sel.param.watch(_on_srv_unit, "value") band_col = pn.Column( pn.Row( label(f"{band.name}:", 36), bc_rbg, margin=0, styles={"gap": "4px", "align-items": "center"}, sizing_mode="stretch_width", ), srv_row, margin=0, styles={"gap": "2px"}, ) band_rows.append(band_col) return [role_row] + band_rows