Files
tcad-devsim_triac/gui1d/app.py
T

993 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# gui1d/app.py
import streamlit as st
import json
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import os
import sys
import threading
# Ensure root directory is in the path to import backend
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT_DIR not in sys.path:
sys.path.append(ROOT_DIR)
# from gui1d.solve_1d import build_and_solve_1d
import subprocess
import pickle
def build_and_solve_1d(bias_target, substrate_type, substrate_doping, length, process_steps, enable_avalanche, enable_btbt, area_cm2):
# Run the simulation in a separate python process to ensure thread-safety and prevent DEVSIM C++ segment faults
temp_dir = os.path.join(ROOT_DIR, "gui1d", "temp_runs")
os.makedirs(temp_dir, exist_ok=True)
thread_id = threading.get_ident()
in_file = os.path.join(temp_dir, f"in_{thread_id}.pkl")
out_file = os.path.join(temp_dir, f"out_{thread_id}.pkl")
args = {
'bias_target': bias_target,
'substrate_type': substrate_type,
'substrate_doping': substrate_doping,
'length': length,
'process_steps': process_steps,
'enable_avalanche': enable_avalanche,
'enable_btbt': enable_btbt,
'area_cm2': area_cm2
}
try:
with open(in_file, 'wb') as f:
pickle.dump(args, f)
python_bin = sys.executable # Runs the same virtualenv python
cmd = [python_bin, os.path.join(ROOT_DIR, "gui1d", "solve_1d.py"), in_file, out_file]
res_proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
with open(out_file, 'rb') as f:
result = pickle.load(f)
return result
except subprocess.CalledProcessError as e:
err_msg = str(e)
if e.stderr:
err_msg += "\n" + e.stderr
raise RuntimeError(f"Simulation process failed: {err_msg}")
except Exception as e:
raise RuntimeError(f"Simulation process failed: {str(e)}")
finally:
for fpath in (in_file, out_file):
if os.path.exists(fpath):
try:
os.remove(fpath)
except Exception:
pass
# Global lock to prevent concurrent thread execution in DEVSIM
DEVSIM_LOCK = threading.Lock()
# Helper to render label and input on the same row, hide default label
def label_input_row(label_text, widget_type, key, ratio=[5, 5], **kwargs):
col_lbl, col_val = st.columns(ratio, gap="small")
col_lbl.markdown(f"<div style='padding-top: 6px; font-weight: 500; font-size: 0.8rem; line-height: 1.2; color: #8a99ad;'>{label_text}</div>", unsafe_allow_html=True)
if widget_type == "selectbox":
if key in st.session_state and "options" in kwargs:
val = st.session_state[key]
if val in kwargs["options"]:
kwargs["index"] = kwargs["options"].index(val)
return col_val.selectbox("", label_visibility="collapsed", key=key, **kwargs)
elif widget_type == "number_input":
if key in st.session_state:
kwargs["value"] = st.session_state[key]
return col_val.number_input("", label_visibility="collapsed", key=key, **kwargs)
elif widget_type == "slider":
if key in st.session_state:
kwargs["value"] = st.session_state[key]
return col_val.slider("", label_visibility="collapsed", key=key, **kwargs)
elif widget_type == "toggle":
if key in st.session_state:
kwargs["value"] = st.session_state[key]
return col_val.toggle("", label_visibility="collapsed", key=key, **kwargs)
# --- 1. Streamlit Page Setup ---
st.set_page_config(
page_title="Interactive 1D Semiconductor Simulator",
layout="wide",
initial_sidebar_state="expanded"
)
# --- 2. Custom Modern CSS (Premium Dark Theme & Typography) ---
st.markdown(
"""
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet">
<style>
/* Global CSS Overrides */
html, body, [class*="css"] {
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #0e1117;
color: #fafafa;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
font-weight: 600;
letter-spacing: -0.5px;
}
/* Metric Card Styling */
.metric-container {
display: flex;
justify-content: space-between;
gap: 1.5rem;
margin-bottom: 2rem;
}
.metric-card {
flex: 1;
background: linear-gradient(135deg, #171d26 0%, #1e2633 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
text-align: center;
transition: all 0.3s ease;
}
.metric-card:hover {
transform: translateY(-4px);
border-color: rgba(0, 200, 255, 0.4);
box-shadow: 0 12px 40px 0 rgba(0, 200, 255, 0.15);
}
.metric-label {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #8a99ad;
margin-bottom: 0.5rem;
font-weight: 400;
}
.metric-value {
font-size: 2rem;
font-weight: 800;
color: #00d2ff;
font-family: 'Outfit', sans-serif;
}
.metric-sub {
font-size: 0.8rem;
color: #627285;
margin-top: 0.25rem;
}
/* Sidebar Styling */
section[data-testid="stSidebar"] {
background-color: #090d13 !important;
border-right: 1px solid rgba(255, 255, 255, 0.05);
}
/* Code font styling */
code, pre {
font-family: 'JetBrains Mono', monospace !important;
}
/* --- Compact Sidebar Style Overrides --- */
[data-testid="stSidebarUserContent"] {
padding-top: 1rem !important;
padding-left: 0.8rem !important;
padding-right: 0.8rem !important;
}
[data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
gap: 6px !important;
}
[data-testid="stSidebar"] .element-container {
margin-bottom: 6px !important;
padding: 0px !important;
}
[data-testid="stSidebar"] hr {
margin-top: 10px !important;
margin-bottom: 10px !important;
}
[data-testid="stSidebar"] [data-testid="stHorizontalBlock"] {
gap: 8px !important;
}
[data-testid="stSidebar"] [data-testid="column"] {
padding-left: 0px !important;
padding-right: 0px !important;
}
/* Expander compacting */
[data-testid="stSidebar"] [data-testid="stExpander"] [data-testid="stVerticalBlock"] {
padding-left: 4px !important;
padding-right: 4px !important;
gap: 4px !important;
}
[data-testid="stSidebar"] [data-testid="stExpander"] {
border: 1px solid rgba(255, 255, 255, 0.08) !important;
background-color: transparent !important;
margin-top: 4px !important;
margin-bottom: 4px !important;
}
[data-testid="stSidebar"] [data-testid="stExpander"] summary {
padding: 6px 10px !important;
}
[data-testid="stSidebar"] [data-testid="stNumberInput"],
[data-testid="stSidebar"] [data-testid="stSelectbox"],
[data-testid="stSidebar"] [data-testid="stSlider"],
[data-testid="stSidebar"] [data-testid="stToggle"] {
margin-top: 0px !important;
margin-bottom: 0px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
}
[data-testid="stSidebar"] div[data-baseweb="select"] {
height: 32px !important;
min-height: 32px !important;
}
[data-testid="stSidebar"] div[data-baseweb="select"] > div {
height: 32px !important;
min-height: 32px !important;
display: flex !important;
align-items: center !important;
}
[data-testid="stSidebar"] div[data-baseweb="input"] {
height: 32px !important;
min-height: 32px !important;
}
[data-testid="stSidebar"] input {
height: 32px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
padding-left: 8px !important;
padding-right: 8px !important;
font-size: 0.85rem !important;
}
[data-testid="stSidebar"] div[data-baseweb="select"] [data-testid="stSelectboxSelectedValue"] {
font-size: 0.85rem !important;
display: flex !important;
align-items: center !important;
height: 100% !important;
}
[data-testid="stSidebar"] button {
padding-top: 0px !important;
padding-bottom: 0px !important;
min-height: 28px !important;
height: 28px !important;
line-height: 28px !important;
font-size: 0.8rem !important;
margin: 0px !important;
}
[data-testid="stSidebar"] [data-testid="stCheckbox"] {
padding-top: 0px !important;
padding-bottom: 0px !important;
margin-top: 2px !important;
margin-bottom: 2px !important;
}
[data-testid="stSidebar"] [data-testid="stCheckbox"] label {
font-size: 0.85rem !important;
padding-top: 2px !important;
}
[data-testid="stSidebar"] [data-testid="stToggle"] {
padding-top: 2px !important;
padding-bottom: 2px !important;
}
[data-testid="stSidebar"] [data-testid="stToggle"] label {
font-size: 0.85rem !important;
}
[data-testid="stSidebar"] [data-testid="stSlider"] {
padding-top: 0px !important;
padding-bottom: 0px !important;
height: 32px !important;
}
[data-testid="stSidebar"] [data-testid="stSlider"] > div {
padding-top: 0px !important;
padding-bottom: 0px !important;
}
[data-testid="stSidebar"] input[type="number"] {
-moz-appearance: textfield !important;
}
[data-testid="stSidebar"] input[type="number"]::-webkit-outer-spin-button,
[data-testid="stSidebar"] input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0 !important;
}
button[data-testid="stNumberInputStepUp"],
button[data-testid="stNumberInputStepDown"] {
display: none !important;
}
</style>
""",
unsafe_allow_html=True
)
# --- 3. Sidebar Container ---
with st.sidebar:
# --- 3. Sidebar Header ---
st.markdown(
"""
<div style="text-align: center; margin-bottom: 1rem;">
<h1 style="color: #00d2ff; font-size: 1.8rem; font-weight: 800; margin-bottom: 0.2rem;">sim1d</h1>
<p style="color: #627285; font-size: 0.85rem; letter-spacing: 1px; text-transform: uppercase;">1D TCAD Process & Device</p>
</div>
""",
unsafe_allow_html=True
)
with st.expander("💾 Config File (Save/Retrieve)", expanded=False):
st.markdown("<div style='font-size: 0.8rem; color: #8a99ad; margin-bottom: 0.3rem;'>Retrieve saved configuration file (.json):</div>", unsafe_allow_html=True)
uploaded_file = st.file_uploader("Retrieve Config JSON", type=["json"], label_visibility="collapsed")
if uploaded_file is not None:
try:
import hashlib
file_bytes = uploaded_file.getvalue()
file_hash = hashlib.md5(file_bytes).hexdigest()
if st.session_state.get('last_loaded_config_hash') != file_hash:
config = json.loads(file_bytes.decode('utf-8'))
# Clear process step widget keys to force Streamlit to refresh widgets with new config values
keys_to_clear = [k for k in st.session_state.keys() if k.startswith("step_") or k.startswith("delete_confirm_")]
for k in keys_to_clear:
del st.session_state[k]
if 'sub_type' in config:
st.session_state['sub_type'] = config['sub_type']
if 'sub_doping' in config:
st.session_state['sub_doping'] = float(config['sub_doping'])
if 'sub_length' in config:
st.session_state['sub_length'] = float(config['sub_length'])
if 'device_area' in config:
st.session_state['device_area'] = float(config['device_area'])
if 'process_steps' in config:
st.session_state['process_steps'] = config['process_steps']
# Explicitly set widget keys in session state to force Streamlit inputs to update and bypass fallback caching bugs
for idx, step in enumerate(config['process_steps']):
st.session_state[f"step_{idx}_enabled"] = bool(step.get('enabled', True))
st.session_state[f"step_{idx}_type"] = step.get('type', 'p')
st.session_state[f"step_{idx}_dopant"] = step.get('dopant', 'Boron')
st.session_state[f"step_{idx}_method"] = step.get('method', 'Implant')
st.session_state[f"step_{idx}_surface"] = step.get('surface', 'Top')
st.session_state[f"step_{idx}_energy"] = float(step.get('energy', 80.0))
st.session_state[f"step_{idx}_dose"] = float(step.get('dose', 1e12))
st.session_state[f"step_{idx}_cs"] = float(step.get('cs', 1e19))
st.session_state[f"step_{idx}_temp"] = float(step.get('temp', 1000.0))
st.session_state[f"step_{idx}_time"] = float(step.get('time', 60.0))
st.session_state[f"step_{idx}_thickness"] = float(step.get('thickness', 10.0))
if 'bias_slider_val' in config:
st.session_state['bias_slider_val'] = float(config['bias_slider_val'])
if 'enable_avalanche_toggle_val' in config:
st.session_state['enable_avalanche_toggle_val'] = bool(config['enable_avalanche_toggle_val'])
if 'enable_btbt_toggle_val' in config:
st.session_state['enable_btbt_toggle_val'] = bool(config['enable_btbt_toggle_val'])
if 'run_full_sweep_toggle_val' in config:
st.session_state['run_full_sweep_toggle_val'] = bool(config['run_full_sweep_toggle_val'])
if 'plot_doping_xmax' in config:
st.session_state['plot_doping_xmax'] = float(config['plot_doping_xmax'])
if 'plot_electro_xmax' in config:
st.session_state['plot_electro_xmax'] = float(config['plot_electro_xmax'])
if 'sweep_v_max' in config:
st.session_state['sweep_v_max'] = float(config['sweep_v_max'])
st.session_state['last_loaded_config_hash'] = file_hash
st.success("Config retrieved successfully!")
st.rerun()
except Exception as e:
st.error(f"Error: {e}")
else:
if 'last_loaded_config_hash' in st.session_state:
st.session_state['last_loaded_config_hash'] = None
# Create an empty container for the download button, to be populated at the bottom of the sidebar.
download_btn_container = st.container()
with st.expander("📈 Plot Viewport 繪圖視角", expanded=False):
plot_doping_xmax = label_input_row(
"Doping X-Max (μm)", "number_input", "plot_doping_xmax",
ratio=[7, 3],
min_value=0.1, max_value=500.0, value=5.0, step=0.5,
help="Doping Profile X-axis maximum range (μm)."
)
plot_electro_xmax = label_input_row(
"Electro X-Max (μm)", "number_input", "plot_electro_xmax",
ratio=[7, 3],
min_value=0.1, max_value=500.0, value=5.0, step=0.5,
help="Electrostatic Profile X-axis maximum range (μm)."
)
sweep_v_max = label_input_row(
"Sweep Max (V)", "number_input", "sweep_v_max",
ratio=[7, 3],
min_value=1.0, max_value=1000.0, value=1000.0, step=10.0,
help="Sweep target voltage (e.g. 1000V for high-voltage, 15V for Zener)."
)
st.markdown("### ⚡ Bias & Physics")
sweep_max_v = float(st.session_state.get('sweep_v_max', 1000.0))
if st.session_state.get('bias_slider_val', 5.0) > sweep_max_v:
st.session_state['bias_slider_val'] = sweep_max_v
bias = label_input_row(
"Bias Vbias (V)", "slider", "bias_slider_val",
ratio=[4.5, 5.5],
min_value=0.0,
max_value=sweep_max_v,
value=min(5.0, sweep_max_v),
step=0.1 if sweep_max_v <= 20.0 else 1.0,
help="Applied at bottom contact (x = L). Top contact (x = 0) is reference 0V."
)
enable_avalanche = label_input_row(
"Avalanche (Impact)", "toggle", "enable_avalanche_toggle_val",
ratio=[6.5, 3.5],
value=False,
help="Enable impact ionization for the selected voltage simulation."
)
enable_btbt = label_input_row(
"BTBT (Tunneling)", "toggle", "enable_btbt_toggle_val",
ratio=[6.5, 3.5],
value=False,
help="Enable band-to-band tunneling for the selected voltage simulation."
)
run_full_sweep = label_input_row(
f"Run {sweep_max_v:.0f}V Sweep", "toggle", "run_full_sweep_toggle_val",
ratio=[6.5, 3.5],
value=True,
help=f"Run the full 0~{sweep_max_v:.0f}V sweep with and without avalanche. Turn off for faster parameter tuning."
)
st.markdown("---")
st.markdown("### 🎛️ Step 0: Substrate 基底")
substrate_type = label_input_row(
"Substrate Type", "selectbox", "sub_type",
options=['n', 'p'], index=0,
help="Select the background doping type of the substrate wafer."
)
n_sub = label_input_row(
"Doping (cm⁻³)", "number_input", "sub_doping",
ratio=[7, 3],
min_value=1e10, max_value=1e20, value=5.5e13, format="%e",
help="Doping concentration of the background wafer substrate."
)
length = label_input_row(
"Thickness (μm)", "number_input", "sub_length",
ratio=[7, 3],
min_value=5.0, max_value=500.0, value=100.0, step=5.0,
help="Total thickness of the substrate wafer (determines L)."
)
area_cm2 = label_input_row(
"Area (cm²)", "number_input", "device_area",
ratio=[7, 3],
min_value=1e-6, max_value=100.0, value=0.01, format="%e",
help="Cross-sectional area of the diode to calculate current from current density."
)
# Initialize process steps list if not present
if 'process_steps' not in st.session_state:
st.session_state['process_steps'] = [
# Default Step 1: Boron P-well Implant + thermal drive-in
{
'enabled': True,
'type': 'p',
'dopant': 'Boron',
'method': 'Implant',
'surface': 'Top',
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
}
]
# If process_steps is empty, show a button to add the first step in the sidebar
if len(st.session_state['process_steps']) == 0:
if st.button(" Add Step 1", key="add_first_step"):
st.session_state['process_steps'].append({
'enabled': True,
'type': 'p',
'dopant': 'Boron',
'method': 'Implant',
'surface': 'Top',
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
})
st.rerun()
st.markdown("### 🧬 Process Steps (Step 1 ~ n)")
# Render process steps inputs dynamically
new_steps = []
steps_to_pop = []
steps_to_move_up = -1
steps_to_insert_below = -1
for idx, step in enumerate(st.session_state['process_steps']):
st.markdown("---")
st.markdown(f"**Step {idx+1}: {step['dopant']} {step['method']}**")
if idx == 0:
# Step 1: no Move Up button, so only 3 columns!
col_en, col_ins, col_del = st.columns([2.2, 4.8, 3.5])
enabled = col_en.checkbox("On", value=step['enabled'], key=f"step_{idx}_enabled")
if col_ins.button(" Insert", key=f"step_{idx}_insert"):
steps_to_insert_below = idx
else:
# Step 2+: 4 columns!
col_en, col_ins, col_mv, col_del = st.columns([2.2, 3.0, 2.8, 2.5])
enabled = col_en.checkbox("On", value=step['enabled'], key=f"step_{idx}_enabled")
if col_ins.button(" Insert", key=f"step_{idx}_insert"):
steps_to_insert_below = idx
if col_mv.button("🔼 Up", key=f"step_{idx}_move"):
steps_to_move_up = idx
# Confirmable delete
confirm_key = f"delete_confirm_{idx}"
if confirm_key not in st.session_state:
st.session_state[confirm_key] = False
if not st.session_state[confirm_key]:
if col_del.button("❌ Del", key=f"step_{idx}_delete"):
st.session_state[confirm_key] = True
st.rerun()
else:
st.warning(f"Delete Step {idx+1}?")
col_yes, col_no = st.columns(2)
if col_yes.button("Yes", key=f"step_{idx}_confirm_yes"):
steps_to_pop.append(idx)
st.session_state[confirm_key] = False
if col_no.button("No", key=f"step_{idx}_confirm_no"):
st.session_state[confirm_key] = False
st.rerun()
# Expander for parameters
with st.expander("Parameters", expanded=True):
type_ = label_input_row("Doping Type", "selectbox", f"step_{idx}_type", options=['p', 'n'], index=0 if step['type'] == 'p' else 1)
dopant_options = ['Boron', 'Phosphorus', 'Arsenic']
dopant_idx = dopant_options.index(step['dopant']) if step['dopant'] in dopant_options else 0
dopant = label_input_row("Dopant", "selectbox", f"step_{idx}_dopant", options=dopant_options, index=dopant_idx)
method_options = ['Implant', 'Diffusion', 'Epi Growth']
method_idx = method_options.index(step['method']) if step['method'] in method_options else 0
method = label_input_row("Method", "selectbox", f"step_{idx}_method", options=method_options, index=method_idx)
if method == 'Implant':
surface = 'Top'
energy = label_input_row("Energy (keV)", "number_input", f"step_{idx}_energy", ratio=[7, 3], min_value=1.0, max_value=1000.0, value=float(step.get('energy', 80.0)), step=5.0)
dose = label_input_row("Dose (cm⁻²)", "number_input", f"step_{idx}_dose", ratio=[7, 3], min_value=1e9, max_value=1e17, value=float(step.get('dose', 1e12)), format="%e")
cs = step.get('cs', 1e19) # preserve
thickness = step.get('thickness', 10.0) # preserve
elif method == 'Diffusion': # Diffusion
surface_options = ['Top', 'Bottom']
surface_idx = surface_options.index(step.get('surface', 'Top')) if step.get('surface', 'Top') in surface_options else 0
surface = label_input_row("Surface Loc.", "selectbox", f"step_{idx}_surface", ratio=[5, 5], options=surface_options, index=surface_idx)
cs = label_input_row("Surface Cs (cm⁻³)", "number_input", f"step_{idx}_cs", ratio=[7, 3], min_value=1e13, max_value=1e22, value=float(step.get('cs', 1e19)), format="%e")
energy = step.get('energy', 80.0) # preserve
dose = step.get('dose', 1e12) # preserve
thickness = step.get('thickness', 10.0) # preserve
else: # Epi Growth
surface = 'Top'
thickness = label_input_row("Thickness (μm)", "number_input", f"step_{idx}_thickness", ratio=[7, 3], min_value=0.1, max_value=200.0, value=float(step.get('thickness', 10.0)), step=0.5)
cs = label_input_row("Doping Conc. (cm⁻³)", "number_input", f"step_{idx}_cs", ratio=[7, 3], min_value=1e10, max_value=1e20, value=float(step.get('cs', 1e15)), format="%e")
energy = step.get('energy', 80.0) # preserve
dose = step.get('dose', 1e12) # preserve
temp = label_input_row("Temp (°C)", "number_input", f"step_{idx}_temp", ratio=[7, 3], min_value=25.0, max_value=1300.0, value=float(step['temp']), step=25.0)
time = label_input_row("Time (min)", "number_input", f"step_{idx}_time", ratio=[7, 3], min_value=0.0, max_value=1000.0, value=float(step['time']), step=5.0)
new_steps.append({
'enabled': enabled,
'type': type_,
'dopant': dopant,
'method': method,
'surface': surface,
'energy': energy,
'dose': dose,
'cs': cs,
'thickness': thickness,
'temp': temp,
'time': time
})
# Apply actions
if len(steps_to_pop) > 0 or steps_to_move_up != -1 or steps_to_insert_below != -1:
# Clear step keys to prevent index-shift misalignment in Streamlit widgets
keys_to_clear = [k for k in st.session_state.keys() if k.startswith("step_") or k.startswith("delete_confirm_")]
for k in keys_to_clear:
del st.session_state[k]
if len(steps_to_pop) > 0:
for pop_idx in sorted(steps_to_pop, reverse=True):
st.session_state['process_steps'].pop(pop_idx)
st.rerun()
if steps_to_move_up != -1:
idx = steps_to_move_up
st.session_state['process_steps'][idx], st.session_state['process_steps'][idx-1] = st.session_state['process_steps'][idx-1], st.session_state['process_steps'][idx]
st.rerun()
if steps_to_insert_below != -1:
idx = steps_to_insert_below
default_step = {
'enabled': True,
'type': 'p',
'dopant': 'Boron',
'method': 'Implant',
'surface': 'Top',
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
}
st.session_state['process_steps'].insert(idx+1, default_step)
st.rerun()
st.session_state['process_steps'] = new_steps
# Generate JSON string of current state and populate the top container at the end of the sidebar execution
config_to_save = {
'sub_type': st.session_state.get('sub_type', 'n'),
'sub_doping': st.session_state.get('sub_doping', 5.5e13),
'sub_length': st.session_state.get('sub_length', 100.0),
'device_area': st.session_state.get('device_area', 0.01),
'process_steps': st.session_state.get('process_steps', []),
'bias_slider_val': st.session_state.get('bias_slider_val', 5.0),
'enable_avalanche_toggle_val': st.session_state.get('enable_avalanche_toggle_val', False),
'enable_btbt_toggle_val': st.session_state.get('enable_btbt_toggle_val', False),
'run_full_sweep_toggle_val': st.session_state.get('run_full_sweep_toggle_val', True),
'plot_doping_xmax': st.session_state.get('plot_doping_xmax', 5.0),
'plot_electro_xmax': st.session_state.get('plot_electro_xmax', 5.0),
'sweep_v_max': st.session_state.get('sweep_v_max', 1000.0),
}
json_str = json.dumps(config_to_save, indent=2)
download_btn_container.download_button(
label="📤 Save Settings",
data=json_str,
file_name="sim1d_config.json",
mime="application/json",
use_container_width=True
)
# --- 5. Main Dashboard Header ---
st.markdown(
"""
<div style="margin-bottom: 2rem;">
<h1 style="font-size: 2.2rem; font-weight: 800; background: linear-gradient(to right, #fafafa, #8a99ad); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Interactive 1D Diode Process-Device Simulator</h1>
<p style="color: #8a99ad; font-size: 1rem; margin-top: 0.2rem;">Real-time FVM Drift-Diffusion & Cumulative Thermal Budget process modeling</p>
</div>
""",
unsafe_allow_html=True
)
# --- 6. Execute Simulation & Cache ---
# Create unique key to track doping state changes
sweep_max_v = float(st.session_state.get('sweep_v_max', 1000.0))
doping_key = (substrate_type, n_sub, length, area_cm2, str(st.session_state['process_steps']), sweep_max_v)
# Initialize I-V curve caches if not present
if 'iv_curve_basic' not in st.session_state:
st.session_state['iv_curve_basic'] = ([0.0], [0.0])
if 'iv_curve_with_avalanche' not in st.session_state:
st.session_state['iv_curve_with_avalanche'] = ([0.0], [0.0])
if 'iv_curve_with_btbt' not in st.session_state:
st.session_state['iv_curve_with_btbt'] = ([0.0], [0.0])
try:
with DEVSIM_LOCK:
if run_full_sweep and ('cached_doping_key' not in st.session_state or st.session_state['cached_doping_key'] != doping_key):
with st.spinner(f"Calculating I-V sweeps (0 ~ {sweep_max_v:.0f}V)..."):
# 1. Sweep Basic (no avalanche, no btbt)
try:
res_no_av = build_and_solve_1d(
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=False,
enable_btbt=False,
area_cm2=area_cm2
)
v_no_av, j_no_av = res_no_av['v_history'], res_no_av['j_history']
except Exception as e:
v_no_av, j_no_av = [0.0], [0.0]
st.error(f"Basic sweep failed: {e}")
# 2. Sweep with avalanche (no btbt)
try:
res_av = build_and_solve_1d(
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=True,
enable_btbt=False,
area_cm2=area_cm2
)
v_av, j_av = res_av['v_history'], res_av['j_history']
except Exception as e:
v_av, j_av = [0.0], [0.0]
st.error(f"Avalanche sweep failed to converge: {e}")
# 3. Sweep with BTBT (no avalanche)
try:
res_btbt = build_and_solve_1d(
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=False,
enable_btbt=True,
area_cm2=area_cm2
)
v_btbt, j_btbt = res_btbt['v_history'], res_btbt['j_history']
except Exception as e:
v_btbt, j_btbt = [0.0], [0.0]
st.error(f"BTBT sweep failed to converge: {e}")
# Store in session state
st.session_state['cached_doping_key'] = doping_key
st.session_state['iv_curve_basic'] = (v_no_av, j_no_av)
st.session_state['iv_curve_with_avalanche'] = (v_av, j_av)
st.session_state['iv_curve_with_btbt'] = (v_btbt, j_btbt)
v_pt_val = None
if 'res_no_av' in locals() and isinstance(res_no_av, dict):
v_pt_val = res_no_av.get('v_punchthrough')
if v_pt_val is None and 'res_av' in locals() and isinstance(res_av, dict):
v_pt_val = res_av.get('v_punchthrough')
if v_pt_val is None and 'res_btbt' in locals() and isinstance(res_btbt, dict):
v_pt_val = res_btbt.get('v_punchthrough')
st.session_state['v_punchthrough'] = v_pt_val
# 4. Single-bias simulation for electrostatic plots
with st.spinner(f"Solving electrostatic profiles at Vbias = {bias} V..."):
res = build_and_solve_1d(
bias_target=bias,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=enable_avalanche,
enable_btbt=enable_btbt,
area_cm2=area_cm2
)
# If full sweep did not run or v_pt was not found, try to grab from single bias solve
if not run_full_sweep or st.session_state.get('v_punchthrough') is None:
st.session_state['v_punchthrough'] = res.get('v_punchthrough')
# Check if target bias was reached
v_actual = res["v_solved"]
if abs(v_actual - bias) > 0.01:
st.warning(f"⚠️ Convergence Limit: Simulation halted at {v_actual:.2f} V due to breakdown divergence.")
# Calculate current in mA
current_ma = res['current_density'] * area_cm2 * 1000.0
# --- 7. Metrics Cards Row ---
st.markdown(
f"""
<div class="metric-container">
<div class="metric-card">
<div class="metric-label">Current (I)</div>
<div class="metric-value">{current_ma:.4e}</div>
<div class="metric-sub">mA (flowing right to left)</div>
</div>
<div class="metric-card">
<div class="metric-label">Peak Electric Field</div>
<div class="metric-value">{res['peak_field']:.3e}</div>
<div class="metric-sub">V/cm</div>
</div>
<div class="metric-card">
<div class="metric-label">Depletion Width</div>
<div class="metric-value">{res['depletion_width']:.3f}</div>
<div class="metric-sub">μm (from {res['depletion_left']:.2f} to {res['depletion_right']:.2f})</div>
</div>
</div>
""",
unsafe_allow_html=True
)
# --- 8. Plots Dashboard (2x2 Grid Layout reorganized to 2 columns) ---
plot_layout = dict(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin=dict(l=50, r=50, t=60, b=80),
legend=dict(orientation="h", yanchor="top", y=-0.2, xanchor="center", x=0.5),
font=dict(family="Outfit, sans-serif", color="#fafafa"),
uirevision="plot_state", # Preserve user zoom/pan zoom state on parameter changes!
xaxis=dict(
title="Position (μm)",
gridcolor='rgba(255,255,255,0.05)',
zerolinecolor='rgba(255,255,255,0.1)',
linecolor='rgba(255,255,255,0.2)',
mirror=True
),
yaxis=dict(
gridcolor='rgba(255,255,255,0.05)',
zerolinecolor='rgba(255,255,255,0.1)',
linecolor='rgba(255,255,255,0.2)',
mirror=True
),
hovermode="x unified"
)
# Create 2 columns for layout: Left has Plot A and B, Right has Plot C
col_left, col_right = st.columns(2)
# --- LEFT COLUMN: Stacked Plot A (Voltage & E-field) & Plot B (Doping profiles) ---
with col_left:
# Plot A: Voltage & Electric Field (dual y-axis)
figA = make_subplots(specs=[[{"secondary_y": True}]])
if res['depletion_width'] > 0.05:
figA.add_vrect(
x0=res['depletion_left'],
x1=res['depletion_right'],
fillcolor="rgba(0, 210, 255, 0.08)",
line_width=0,
annotation_text="Depletion Region",
annotation_position="top left",
annotation_font=dict(color="#00d2ff", size=11)
)
# Voltage Trace (relative to top surface)
figA.add_trace(
go.Scatter(
x=res['x'], y=res['potential'] - res['potential'][0],
name="Voltage Profile (V)",
line=dict(color='#ff4b4b', width=3),
hovertemplate='%{y:.2f} V'
),
secondary_y=False
)
# Electric Field Trace
figA.add_trace(
go.Scatter(
x=res['x_edge'], y=res['efield'],
name="Electric Field (V/cm)",
line=dict(color='#00d2ff', width=3),
hovertemplate='%{y:.3e} V/cm'
),
secondary_y=True
)
figA.update_layout(**plot_layout)
figA.update_xaxes(range=[0.0, float(st.session_state.get('plot_electro_xmax', 5.0))])
figA.update_layout(title=f"⚡ Voltage & Electric Field Profile at {bias}V", height=360, legend=dict(y=-0.25))
figA.update_yaxes(title_text="Voltage (V)", range=[0.0, max(0.1, float(bias))], secondary_y=False)
figA.update_yaxes(title_text="Electric Field (V/cm)", secondary_y=True)
st.plotly_chart(figA, use_container_width=True)
# Plot B: Doping Profiles (arcsinh log scale)
figB = go.Figure()
# Plot Net Doping
y_net = np.arcsinh(res['doping'] / 2.0) / np.log(10.0)
figB.add_trace(go.Scatter(
x=res['x'], y=y_net,
name="Net Doping (arcsinh)",
line=dict(color='#00ff88', width=3),
hovertemplate='Net: %{y:.2f}'
))
# Plot individual step profiles (signed based on type: N is positive, P is negative)
for step_prof in res['step_profiles']:
prof_data = step_prof['profile']
if step_prof['type'] == 'p':
y_val = np.arcsinh(-prof_data / 2.0) / np.log(10.0)
else:
y_val = np.arcsinh(prof_data / 2.0) / np.log(10.0)
figB.add_trace(go.Scatter(
x=res['x'], y=y_val,
name=step_prof['name'],
line=dict(width=1.5, dash='dash'),
hovertemplate='%{y:.2f}'
))
figB.update_layout(**plot_layout)
figB.update_xaxes(range=[0.0, float(st.session_state.get('plot_doping_xmax', 5.0))])
figB.update_layout(
title="📈 Doping Profiles (arcsinh Log Scale)",
height=360,
legend=dict(y=-0.25),
yaxis=dict(
title="arcsinh(Doping / 2) / ln(10)",
zeroline=True,
zerolinecolor='rgba(255, 255, 255, 0.25)',
zerolinewidth=1
)
)
st.plotly_chart(figB, use_container_width=True)
# --- RIGHT COLUMN: Taller I-V Curves ---
with col_right:
figC = go.Figure()
# 1. Curve: Basic (Without Avalanche, Without BTBT)
v_basic, j_basic = st.session_state.get('iv_curve_basic', ([0.0], [0.0]))
if len(v_basic) == 1 and v_basic[0] == 0.0:
# Fallback to legacy cached key
v_basic, j_basic = st.session_state.get('iv_curve_without_avalanche', ([0.0], [0.0]))
i_basic_ma = np.abs(np.array(j_basic)) * area_cm2 * 1000.0
i_basic_ma = np.clip(i_basic_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_basic, y=i_basic_ma,
name="Basic (Drift-Diffusion)",
line=dict(color='#00d2ff', width=2, dash='dot'),
mode='lines+markers',
marker=dict(size=4),
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
# 2. Curve: With Avalanche
v_av, j_av = st.session_state.get('iv_curve_with_avalanche', ([0.0], [0.0]))
i_av_ma = np.abs(np.array(j_av)) * area_cm2 * 1000.0
i_av_ma = np.clip(i_av_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_av, y=i_av_ma,
name="With Avalanche",
line=dict(color='#ffae00', width=3),
mode='lines+markers',
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
# 3. Curve: With BTBT
v_btbt, j_btbt = st.session_state.get('iv_curve_with_btbt', ([0.0], [0.0]))
i_btbt_ma = np.abs(np.array(j_btbt)) * area_cm2 * 1000.0
i_btbt_ma = np.clip(i_btbt_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_btbt, y=i_btbt_ma,
name="With BTBT",
line=dict(color='#a000ff', width=3),
mode='lines+markers',
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
# Add punch-through vertical line if available
v_pt = st.session_state.get('v_punchthrough')
if v_pt is not None:
figC.add_vline(
x=v_pt,
line_width=2,
line_dash="dash",
line_color="#ff4b4b",
annotation_text=f"Punch-through: {v_pt:.1f}V",
annotation_position="top left",
annotation_font=dict(color="#ff4b4b", size=12)
)
figC.update_layout(**plot_layout)
figC.update_layout(
title=f"📉 I-V Characteristics (0 ~ {sweep_max_v:.0f}V)",
height=750,
legend=dict(y=-0.12),
xaxis=dict(title="Applied Bias Vbias (V)"),
yaxis=dict(type="log", title="Current |I| (mA)", range=[-12, 0.0], exponentformat="power")
)
st.plotly_chart(figC, use_container_width=True)
except Exception as ex:
st.error("❌ Simulation Error")
st.exception(ex)