feat(gui1d, physics): implement interactive 1D simulator GUI & fix avalanche generation sign bug

- gui1d:
  - Created interactive 1D Diode simulator dashboard (gui1d/app.py, solve_1d.py).
  - Redesigned doping process step editor, layout, and centered legend plots.
  - Implemented state caching for I-V sweeps and grid spacing optimization.
  - Doubled voltage step resolution: 0.05V step for V < 1V, and V/20 step for V >= 1V.
- physics (avalanche bug fix):
  - Fixed charge sign in Hole Continuity Equation for avalanche generation.
  - Created AvalancheGeneration_p in physics/new_physics.py to correctly act as a hole source.
  - Resolved physical breakdown current polarity and negative current leakage at high reverse bias.
This commit is contained in:
pchang718
2026-06-17 23:08:05 +08:00
parent d74c9a4706
commit 44b41698e8
17 changed files with 11315 additions and 367 deletions
+14 -10
View File
@@ -12,6 +12,7 @@ export OPENBLAS_NUM_THREADS = 4
avalanche ?= false
refine ?= false
refine_v_step ?= 50.0
temp ?= 300.0
PYTHON := .venv/bin/python
@@ -59,15 +60,18 @@ help-detail:
@echo " Default: true (enable grid splitting at milestones)"
@echo " refine_v_step=<voltage> - Set voltage interval (V) to trigger dynamic refinement"
@echo " Default: 50.0 (e.g., every 50V). Less than 1.0 disables it."
@echo " temp=<temperature> - Set device simulation temperature in Kelvin"
@echo " Default: 300.0 (e.g., room temperature)"
@echo " checkpoint=<filepath> - Specify seed/recovery pickle file to resume from"
@echo " Default: automatically searches for latest checkpoints"
@echo ""
@echo "Usage Examples:"
@echo " make sweep avalanche=true"
@echo " make sweep temp=350.0"
@echo " make sweep refine=false"
@echo " make sweep refine_v_step=25.0"
@echo " make resume checkpoint=output_this_run/seed_500V.pkl"
@echo " make resume-bg avalanche=true refine_v_step=30.0"
@echo " make resume checkpoint=output_this_run/seed_500V.pkl temp=350.0"
@echo " make resume-bg avalanche=true refine_v_step=30.0 temp=350.0"
@echo "============================================================================="
# --- 網格自適應優化流程 ---
@@ -86,22 +90,22 @@ mesh: device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
# --- 熱平衡電位求解 ---
# 依賴於對應的網格與求解腳本
static: device_2d.msh solve_static_2d.py
@echo ">>> [Static] 求解零偏壓熱平衡狀態..."
$(PYTHON) solve_static_2d.py
@echo ">>> [Static] 求解零偏壓熱平衡狀態 (temp=$(temp))..."
TEMP=$(temp) $(PYTHON) solve_static_2d.py
# --- 高壓偏壓掃描 ---
# 依賴於對應的網格與掃描腳本
sweep: device_2d.msh solve_sweep_recon.py
@echo ">>> [Sweep] 開始高壓偏壓漂移-擴散模擬 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) $(PYTHON) solve_sweep_recon.py > sweeping.log 2>&1
@echo ">>> [Sweep] 開始高壓偏壓漂移-擴散模擬 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) $(PYTHON) solve_sweep_recon.py > sweeping.log 2>&1
resume:
@echo ">>> [Resume] 從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) $(PYTHON) resume_run.py $(checkpoint) >> sweeping.log 2>&1
@echo ">>> [Resume] 從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) $(PYTHON) resume_run.py $(checkpoint) >> sweeping.log 2>&1
resume-bg:
@echo ">>> [Resume-BG] 在背景從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) nohup $(PYTHON) resume_run.py $(checkpoint) >> sweeping.log 2>&1 &
@echo ">>> [Resume-BG] 在背景從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (avalanche=$(avalanche), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) nohup $(PYTHON) resume_run.py $(checkpoint) >> sweeping.log 2>&1 &
# --- 萃取與監控收斂曲線 ---
show-conv:
+4 -4
View File
@@ -36,10 +36,10 @@ MRING_X2 = 356.0 * um
# --- Doping Concentrations (cm^-3) ---
N_SUB = 1.0e14 # 70 ~ 90 ohm cm (5.5e13)
P11_PEAK = 6.0e15
P11_PEAK = 8.0e15
P12_PEAK = 3.0e15
P13_PEAK = 6.0e15
NPLUS_PEAK = 1.0e16
P13_PEAK = 8.0e15
NPLUS_PEAK = 2.0e16
# --- Doping Gradient / Diffusion Widths ---
# P-well gradient widths
@@ -65,5 +65,5 @@ MT1_FP2_X1 = 250.0 * um
MT1_FP2_X2 = 295.0 * um
# --- Simulation Metadata ---
SIM_NAME = "p-doping 6&3e15 20260616"
SIM_NAME = "p-doping 8&3e15 20260616"
+14 -8
View File
@@ -113,7 +113,8 @@ def setup_physics_for_device(device, is_avalanche_enabled=False):
CreateSolution(device, "Silicon", "Electrons")
CreateSolution(device, "Silicon", "Holes")
devsim.set_parameter(device=device, name="T", value="300")
sim_temp = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=sim_temp)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide potential equations
@@ -204,15 +205,16 @@ def setup_physics_for_device(device, is_avalanche_enabled=False):
# Avalanche generation model (enabled/disabled by refine_and_interpolate caller)
if is_avalanche_enabled:
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
# 預設以 positive (full Newton) 方式註冊連續方程式
# refine_and_interpolate 在插值後會臨時切換至 log_damp 做 Stage 1 預處理
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.node_model(device=device, region="Silicon", name="LogElectrons", equation="log(Electrons + 1e-10) / log(10.0)")
@@ -763,11 +765,13 @@ def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_
# Stage 1: Fully-coupled log_damp
# ==========================================
# Re-register Electron and Hole Continuity equations in Silicon and contacts
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device_new_name, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device_new_name, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]:
contact_electrons_name = f"{c}nodeelectrons"
@@ -834,11 +838,13 @@ def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_
# ==========================================
# Stage 2: log_damp coupled precision solve
# ==========================================
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device_new_name, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device_new_name, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
# Restore PotentialEquation variable update to default with min_error=1e-3
devsim.equation(device=device_new_name, region="Silicon", name="PotentialEquation", variable_name="Potential",
+889
View File
@@ -0,0 +1,889 @@
# 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, 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,
'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 Exception as e:
err_msg = str(e)
if 'res_proc' in locals() and res_proc.stderr:
err_msg += "\n" + res_proc.stderr
raise RuntimeError(f"Simulation process failed: {err_msg}")
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))
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 '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'])
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)."
)
st.markdown("### ⚡ Bias & Physics")
bias = label_input_row(
"Bias Vbias (V)", "slider", "bias_slider_val",
ratio=[4.5, 5.5],
min_value=0.0,
max_value=1000.0,
value=5.0,
step=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."
)
run_full_sweep = label_input_row(
"Run 1000V Sweep", "toggle", "run_full_sweep_toggle_val",
ratio=[6.5, 3.5],
value=True,
help="Run the full 0~1000V 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,
'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,
'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']
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
else: # 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
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,
'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,
'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),
'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),
}
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
doping_key = (substrate_type, n_sub, length, area_cm2, str(st.session_state['process_steps']))
# Initialize I-V curve caches if not present
if 'iv_curve_with_avalanche' not in st.session_state:
st.session_state['iv_curve_with_avalanche'] = ([0.0], [0.0])
if 'iv_curve_without_avalanche' not in st.session_state:
st.session_state['iv_curve_without_avalanche'] = ([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("Calculating high-voltage I-V sweeps (0 ~ 1000V)..."):
# 1. Sweep with avalanche
try:
res_av = build_and_solve_1d(
bias_target=1000.0,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=True,
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}")
# 2. Sweep without avalanche
try:
res_no_av = build_and_solve_1d(
bias_target=1000.0,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=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"Without-avalanche sweep failed: {e}")
# Store in session state
st.session_state['cached_doping_key'] = doping_key
st.session_state['iv_curve_with_avalanche'] = (v_av, j_av)
st.session_state['iv_curve_without_avalanche'] = (v_no_av, j_no_av)
# 3. 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,
area_cm2=area_cm2
)
# 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 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'
))
# 2. Curve without avalanche
v_no_av, j_no_av = st.session_state.get('iv_curve_without_avalanche', ([0.0], [0.0]))
i_no_av_ma = np.abs(np.array(j_no_av)) * area_cm2 * 1000.0
i_no_av_ma = np.clip(i_no_av_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_no_av, y=i_no_av_ma,
name="Without Avalanche",
line=dict(color='#00d2ff', width=2, dash='dot'),
mode='lines',
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
figC.update_layout(**plot_layout)
figC.update_layout(
title="📉 High-Voltage I-V Characteristics (0 ~ 1000V)",
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)
+145
View File
@@ -0,0 +1,145 @@
# Discussion: Interactive 1D Semiconductor Simulator (gui1d)
Welcome to the **gui1d** sub-project discussion! This document tracks the design decisions, physical formulations, numerical schemes, and development roadmap of the Interactive 1D Simulator.
---
## Architecture Commitments and Code Isolation
> [!IMPORTANT]
> **Strict Code Isolation Constraint:**
> * The `gui1d` sub-project must remain **100% non-intrusive**.
> * **NO modifications** are allowed to any existing 2D simulation files (e.g., `solve_sweep_recon.py`, `dynamic_refine.py`, `generate_analytical_bgmesh.py`, `device_config.py` in the root workspace).
> * The 1D app may import common configuration parameters from `device_config.py` strictly in a **read-only** manner.
> * If any modifications to the 2D workspace code ever become necessary for supporting 1D features, **they must be explicitly documented, requested, and approved by the USER before any action is taken.**
---
## 1. Goal and Vision
* **Core Value**: Provide a real-time, interactive visualization interface.
* **Control Inputs (Left Panel)**:
- Dynamic voltage bias slider.
- Interactive Doping Profile sliders: substrate doping, junction depth, concentration, dispersion/slope, and multi-step gradients (2-stage or 3-stage transitions).
- **Avalanche Generation Toggle (On/Off)**: Dynamic switch to enable/disable impact ionization models.
* **Outputs (Right Plot Window - Visual Panel)**:
- **Plot 1: Carrier & Doping Profile ($n, p, |N_D - N_A|$ vs. Position)**
* **Y-axis**: Logarithmic scale ($10^{10} \sim 10^{21}\text{ cm}^{-3}$).
* **Visual**: Clearly shows Net Doping (P-type/N-type boundaries) and active carriers ($n$ and $p$). Users can watch the depletion region expand dynamically as carrier concentrations fall to intrinsic levels.
- **Plot 2: Band Diagram & Potential ($E_c, E_v, -q\phi$ vs. Position)**
* **Y-axis**: Linear scale (Energy in $\text{eV}$ or Voltage in $\text{V}$).
* **Visual**: Shows energy bands bending under bias. Helpful to visualize barrier heights and field distribution (band slope).
- **Plot 3: Electric Field & Ionization Rate ($E$-field, $G_{\text{ii}}$ vs. Position)**
* **Y-axis**: Linear scale (Field in $\text{V/cm}$), optionally dual-axis for generation rate ($\text{cm}^{-3}\text{s}^{-1}$).
* **Visual**: Pinpoints the peak electric field ($E_{\text{max}}$) at the metallurgical junction and where avalanche ionization is occurring.
* **Key Numerical Metrics (Real-time Card Indicators)**:
- **Total Current Density ($J_{\text{tot}}$, $\text{A/cm}^2$)**: Shows leakage current at low bias and sudden orders-of-magnitude surge during avalanche breakdown.
- **Peak Electric Field ($E_{\text{max}}$, $\text{V/cm}$)**: Crucial indicator for breakdown warning.
- **Depletion Width ($W_d$, $\mu\text{m}$)**: Automatically extracted width (where $|n-p| < 0.1 \times \text{doping}$). Shows left/right depletion boundaries, allowing users to visually check for "punch-through" conditions.
* **Performance Requirement**: Sub-15ms loop response (real-time updating at 60 FPS) when sliding parameters.
---
## 2. Technical Stack Decision
### Proposal A (Recommended): Python + DEVSIM + Streamlit
* **Architecture**:
- Backend: **DEVSIM (Python API)** to solve the 1D Drift-Diffusion system.
- Frontend: **Streamlit** (or Plotly Dash) to render sliders and real-time interactive plots.
* **Pros**:
- **100% Reuse of Physics**: Directly imports materials, mobility (Arora), recombination (SRH), and impact ionization models already calibrated in the 2D pipeline.
- **Minimal Codebase**: Streamlit reduces GUI boilerplate to just dozens of lines of Python.
* **Cons**: Local network buffering might add a slight overhead (~30ms), but it remains highly responsive on `localhost`.
### Proposal B: Pure Web Frontend (HTML5 + JS FVM Solver)
* **Architecture**:
- A single-page HTML/JS app containing a custom 1D Finite Volume Method (FVM) solver written in Javascript using the Scharfetter-Gummel scheme.
* **Pros**: Zero installation required, runs instantly in any browser, 60 FPS ultra-smooth local interaction.
* **Cons**: Must re-implement all physical models (Arora mobility, recombination) in Javascript, increasing the risk of code divergence with the 2D simulator.
---
## 3. Numerical Convergence Strategies (Handling Voltage Jumps)
When a user clicks or jumps the slider from $0\text{ V}$ directly to a high bias (e.g. $50\text{ V}$), solving Drift-Diffusion directly will lead to Newton divergence due to Boltzmann exponential stiffness.
To solve this, we employ the following strategy:
### Rapid Background Sweep
1. Instead of solving $V_{\text{target}}$ directly from thermal equilibrium, the simulator automatically initiates a **micro-sweep** in memory.
2. It advances from the current bias $V_{\text{current}}$ to $V_{\text{target}}$ with a coarse step (e.g., $\Delta V = 2\text{ V} \sim 5\text{ V}$).
3. **Adaptive Step for Avalanche Mode**: When `Avalanche = ON` and the bias approaches the breakdown voltage (where current rises sharply), the micro-sweep solver should automatically reduce the step size $\Delta V$ to ensure robust convergence.
4. **Why this works**: In 1D, a single-bias step takes less than $1\text{ ms}$ to solve. Running a 10-step sweep takes only $\sim 10\text{ ms}$, which is completely imperceptible to the user.
5. **Why Doping Seeds are Not Needed**: Preserving checkpoints is fragile because changing the Doping Profile immediately invalidates previous electrical seeds. Running the rapid micro-sweep in memory dynamically ensures 100% convergence under arbitrary doping configurations.
---
## 4. Immediate Development Milestones
- [x] **Milestone 1**: Implement a prototype `solve_1d.py` script using DEVSIM to verify that 1D grid generation, doping setting, and bias sweep are fully functional.
- [x] **Milestone 2**: Build a basic Streamlit dashboard with sliders (Bias, Junction Depth, Peak Doping) and plot the results using Plotly.
- [ ] **Milestone 3**: Refine the Doping Profile model to support multi-stage gradient setups (two/three-stage profiles).
- [x] **Milestone 4**: Optimize the in-memory sweep and initial guess algorithms to ensure robust convergence when drag-and-dropping sliders.
---
## 5. 1D Device Structure and Doping Profile Design
The interactive 1D simulator builds a classic **$P^+ / N^- / N^+$ (PiN) Diode structure** inside the [solve_1d.py](file:///home/pchan/devsim2026/gui1d/solve_1d.py) backend.
### Grid Geometry and Adaptive Refinement
* **Total Length ($L$)**: Default is $30\text{ }\mu\text{m}$ (adjustable between $15 \sim 60\text{ }\mu\text{m}$ in Advanced settings).
* Anode contact (`top`) is located at $x = 0$.
* Cathode contact (`bottom`) is located at $x = L$.
* **Adaptive Meshing**: The mesh is partitioned into four zones to ensure sub-millisecond solving speed while resolving junction electrostatics:
1. **Top P-well Region ($0 \sim x_j - 1\text{ }\mu\text{m}$)**: Coarser step size ($0.2\text{ }\mu\text{m}$).
2. **Junction Refinement Zone ($(x_j - 1) \sim (x_j + 1.5)\text{ }\mu\text{m}$)**: Ultra-fine step size of **`20 nm` ($0.02\text{ }\mu\text{m}$)** to resolve the peak electric field and carrier generation rate $G_{\text{ii}}$.
3. **Bulk Drift Zone ($(x_j + 1.5) \sim L - 1.5\text{ }\mu\text{m}$)**: Coarser step size ($0.4\text{ }\mu\text{m}$).
4. **Bottom Contact Zone ($(L - 1.5) \sim L\text{ }\mu\text{m}$)**: Finely-spaced step size ($0.05\text{ }\mu\text{m}$) to resolve carrier injection at the ohmic boundary.
### Doping Profile Formulation
The net doping concentration is defined as:
$$\text{NetDoping}(x) = N_{\text{donors}}(x) - N_{\text{acceptors}}(x)$$
1. **Top P-well Diffusion Profile (Gaussian)**:
$$N_{\text{acceptors}}(x) = P_{\text{peak}} \times \exp\left( -1.0 \times \left(\frac{x}{P_{\text{slope}}}\right)^2 \right)$$
* Peak concentration ($P_{\text{peak}}$): Default $4.0 \times 10^{15}\text{ cm}^{-3}$.
* Gradient length ($P_{\text{slope}}$): Default $2.5\text{ }\mu\text{m}$ (crosses background at metallurgical junction $x_j \approx 5\text{ }\mu\text{m}$).
2. **Background N-substrate (Uniform)**:
$$N_{\text{sub}} = 1.0 \times 10^{14}\text{ cm}^{-3}$$
* Represents the high-resistivity drift region of the high-voltage diode.
3. **Bottom N+ Ohmic Contact Profile (Gaussian)**:
$$N_{\text{donors\_bottom}}(x) = N_{\text{bottom\_peak}} \times \exp\left( -1.0 \times \left(\frac{L - x}{N_{\text{bottom\_slope}}}\right)^2 \right)$$
* Peak concentration ($N_{\text{bottom\_peak}}$): Default $1.0 \times 10^{19}\text{ cm}^{-3}$.
* Gradient length ($N_{\text{bottom\_slope}}$): Default $0.5\text{ }\mu\text{m}$.
* Ensures perfect **Ohmic Contact** at the bottom boundary, preventing spurious Schottky barrier formation.
---
## 6. Physical Correctness: Avalanche Generation Sign Correction (Charge Conservation)
### The Discovery of Non-Physical Current Behavior
During high-bias sweeps (with avalanche enabled), the reverse leakage current was found to dip and turn negative at high voltages (e.g. going below zero after 600V and reaching large negative values at 1000V). A passive reverse-biased diode under dark conditions should have strictly positive current flowing in the direction of the applied field.
### Root Cause Analysis
This behavior was caused by a sign error in the carrier continuity equations and charge conservation definitions:
1. **DEVSIM Equation Setup**:
- The electron continuity equation solves for $Q_n = +q \cdot n$ (positive charge density in ECE).
- The hole continuity equation solves for $Q_p = -q \cdot p$ (negative charge density in HCE).
2. **Avalanche Model Shared Sign Error**:
- Originally, both equations shared the same `edge_volume_model="AvalancheGeneration"` which was mathematically defined as a negative quantity: $eq = -(\alpha_n |J_n| + \alpha_p |J_p|)$.
- For electrons ($Q_n$), a negative term subtracted in the residual equation $F_n = 0$ acts as a **source** ($+q G_{av}$), which is physically correct.
- For holes ($Q_p$), a negative term subtracted in the residual equation $F_p = 0$ acts as a **sink** (recombination, $-q G_{av}$) because holes represent negative charge.
- This physically contradictory setup generated electrons but destroyed holes, violating charge conservation, collapsing the minority carrier concentration, and leading to non-physical negative currents.
### Resolution
We resolved this by declaring two separate avalanche generation models in [new_physics.py](file:///home/pchan/devsim2026/physics/new_physics.py):
- `AvalancheGeneration` (positive: $+(\alpha_n |J_n| + \alpha_p |J_p|)$) which acts as a carrier source in ECE.
- `AvalancheGeneration_p` (negative: $-(\alpha_n |J_n| + \alpha_p |J_p|)$) which acts as a carrier source in HCE.
The 1D solver [solve_1d.py](file:///home/pchan/devsim2026/gui1d/solve_1d.py) has been updated to reference `AvalancheGeneration_p` for the電洞連續方程式 (`HoleContinuityEquation`), resulting in physically correct leakage currents and exponential breakdown behavior at 409V.
+508
View File
@@ -0,0 +1,508 @@
# gui1d/solve_1d.py
import sys
import os
import numpy as np
import math
# Ensure root directory is in the path to import physics and config modules
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT_DIR not in sys.path:
sys.path.append(ROOT_DIR)
import devsim
from physics.model_create import CreateSolution
from physics.new_physics import (
CreateSiliconPotentialOnly,
CreateSiliconPotentialOnlyContact,
CreateAroraMobilityLF,
CreateHFMobility,
CreateSiliconDriftDiffusion,
CreateSiliconDriftDiffusionContact,
CreateAvalancheGeneration
)
# Vectorized complementary error function
erfc_vec = np.vectorize(math.erfc)
# --- Process Database ---
# Implant Range & Straggle in Silicon (Energy in keV, Rp and dRp in microns)
IMPLANT_DB = {
'Boron': {
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
'Rp': np.array([0.04, 0.07, 0.10, 0.16, 0.24, 0.30, 0.42, 0.55]),
'dRp': np.array([0.015, 0.025, 0.035, 0.050, 0.063, 0.070, 0.085, 0.100])
},
'Phosphorus': {
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
'Rp': np.array([0.015, 0.028, 0.040, 0.065, 0.100, 0.120, 0.180, 0.240]),
'dRp': np.array([0.007, 0.012, 0.016, 0.025, 0.038, 0.045, 0.060, 0.075])
},
'Arsenic': {
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
'Rp': np.array([0.010, 0.018, 0.025, 0.038, 0.058, 0.070, 0.100, 0.130]),
'dRp': np.array([0.004, 0.007, 0.009, 0.014, 0.020, 0.025, 0.035, 0.045])
}
}
# Diffusion Arrhenius parameters in Silicon (D0 in cm^2/s, Ea in eV)
DIFFUSION_DB = {
'Boron': {'D0': 1.0, 'Ea': 3.46},
'Phosphorus': {'D0': 10.5, 'Ea': 3.69},
'Arsenic': {'D0': 0.32, 'Ea': 3.56}
}
def get_implant_params(dopant, energy_kev):
"""Interpolate Rp and dRp from the database for a given energy in keV."""
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
Rp = np.interp(e, db['energy'], db['Rp'])
dRp = np.interp(e, db['energy'], db['dRp'])
return Rp, dRp
def get_diffusion_coefficient(dopant, temp_c):
"""Calculate diffusion coefficient D in cm^2/s at temperature in °C."""
params = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
T_kelvin = temp_c + 273.15
k_B = 8.6173e-5 # eV/K
D = params['D0'] * np.exp(-params['Ea'] / (k_B * T_kelvin))
return D
def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping, length):
"""
Calculate the separate Donors and Acceptors concentration arrays (cm^-3)
across a position array x_um (in microns), and also return individual step profiles.
"""
donors = np.zeros_like(x_um)
acceptors = np.zeros_like(x_um)
step_profiles = []
# Initialize substrate baseline
if substrate_type == 'n':
donors += substrate_doping
else:
acceptors += substrate_doping
# Add background contact doping to ensure Ohmic contact at bottom (x = L)
# Contact doping thickness is 0.5 um, peak is 1e19
contact_profile = 1e19 * np.exp(-((length - x_um) / 0.5) ** 2)
if substrate_type == 'n':
donors += contact_profile
else:
acceptors += contact_profile
# We also keep Step 0 Substrate profile in step_profiles
step_profiles.append({
'name': 'Substrate (Step 0)',
'type': substrate_type,
'profile': np.full_like(x_um, substrate_doping)
})
# Compute profiles for each process step
num_steps = len(process_steps)
for i, step in enumerate(process_steps):
if not step.get('enabled', True):
continue
dopant = step.get('dopant', 'Boron')
type_ = step.get('type', 'p')
method = step.get('method', 'Implant')
surface = step.get('surface', 'Top')
# Calculate cumulative Dt (in cm^2) for this step's dopant
Dt_total_cm2 = 0.0
for j in range(i, num_steps):
sub_step = process_steps[j]
if not sub_step.get('enabled', True):
continue
t_temp = sub_step.get('temp', 25.0)
t_time = sub_step.get('time', 0.0) # in minutes
if t_time > 0 and t_temp > 100.0:
D_temp = get_diffusion_coefficient(dopant, t_temp)
Dt_total_cm2 += D_temp * (t_time * 60.0)
# Convert Dt to microns^2
Dt_total_um2 = Dt_total_cm2 * 1e8
# Calculate profile
if method == 'Implant':
energy = step.get('energy', 80.0)
dose = step.get('dose', 1e12)
Rp, dRp = get_implant_params(dopant, energy)
dRp_eff = np.sqrt(dRp ** 2 + 2 * Dt_total_um2)
peak_conc = dose / (np.sqrt(2 * np.pi) * (dRp_eff * 1e-4)) # dose in cm^-2, dRp in cm
if surface == 'Top':
prof = peak_conc * (np.exp(-((x_um - Rp) / (np.sqrt(2) * dRp_eff)) ** 2) +
np.exp(-((x_um + Rp) / (np.sqrt(2) * dRp_eff)) ** 2))
else:
prof = peak_conc * (np.exp(-((x_um - (length - Rp)) / (np.sqrt(2) * dRp_eff)) ** 2) +
np.exp(-((x_um - (length + Rp)) / (np.sqrt(2) * dRp_eff)) ** 2))
else: # Constant Source Predeposition
cs = step.get('cs', 1e19)
if Dt_total_um2 <= 0.0:
Dt_total_um2 = 1e-10
if surface == 'Top':
prof = cs * erfc_vec(x_um / (2 * np.sqrt(Dt_total_um2)))
else:
prof = cs * erfc_vec((length - x_um) / (2 * np.sqrt(Dt_total_um2)))
# Add to Net Donors/Acceptors arrays
if type_ == 'n':
donors += prof
else:
acceptors += prof
# Save this step's final diffused profile
step_profiles.append({
'name': f"Step {i+1}: {dopant} {method}",
'type': type_,
'profile': prof
})
return donors, acceptors, step_profiles
def find_junction_depths(process_steps, substrate_type, substrate_doping, length):
"""Locate depth coordinates in microns where the net doping crosses zero."""
x_test = np.linspace(0, length, 2000)
donors, acceptors, _ = calc_donors_acceptors(x_test, process_steps, substrate_type, substrate_doping, length)
doping_test = donors - acceptors
sign_doping = np.sign(doping_test)
crossings = np.where(sign_doping[:-1] != sign_doping[1:])[0]
junctions = []
for idx in crossings:
x1, x2 = x_test[idx], x_test[idx+1]
y1, y2 = doping_test[idx], doping_test[idx+1]
if abs(y2 - y1) > 1e-20:
x_cross = x1 - y1 * (x2 - x1) / (y2 - y1)
junctions.append(x_cross)
return junctions
def build_and_solve_1d(
bias_target,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=[],
enable_avalanche=False,
area_cm2=1.0
):
"""
Builds a 1D Diode mesh, sets up doping, solves from 0V equilibrium
to bias_target using a rapid micro-sweep, and returns physical profiles.
"""
# 1. Reset DEVSIM state to prevent name collisions
devsim.reset_devsim()
um = 1e-4 # 1 micron = 1e-4 cm
device = "device1d"
region = "Silicon"
mesh_name = "mesh1d"
# 2. Build 1D adaptive mesh
# Find junctions to refine the mesh around them
junctions = find_junction_depths(process_steps, substrate_type, substrate_doping, length)
# Define control points (x, target spacing)
control_points = [(0.0, 0.05)]
for j in junctions:
control_points.append((j, 0.02)) # Fine mesh around junctions
control_points.append((length, 0.5)) # Coarser mesh near bottom
# Sort control points
control_points.sort(key=lambda x: x[0])
# Construct node list
all_x = [0.0]
for idx in range(len(control_points) - 1):
x_start, sp_start = control_points[idx]
x_end, sp_end = control_points[idx+1]
seg_len = x_end - x_start
if seg_len <= 0:
continue
curr_x = x_start
while curr_x < x_end:
t = (curr_x - x_start) / seg_len
spacing = sp_start + t * (sp_end - sp_start)
spacing = max(0.01, min(2.0, spacing))
curr_x += spacing
if curr_x < x_end - 0.005:
all_x.append(curr_x)
all_x.append(x_end)
all_x = np.unique(np.array(all_x))
# Create mesh in DEVSIM
devsim.create_1d_mesh(mesh=mesh_name)
for i, x_val in enumerate(all_x):
tag = ""
if i == 0:
tag = "top"
elif i == len(all_x) - 1:
tag = "bottom"
if i == 0:
ps = all_x[1] - all_x[0]
elif i == len(all_x) - 1:
ps = all_x[-1] - all_x[-2]
else:
ps = min(all_x[i] - all_x[i-1], all_x[i+1] - all_x[i])
devsim.add_1d_mesh_line(mesh=mesh_name, pos=x_val * um, ps=ps * um, tag=tag)
devsim.add_1d_contact(mesh=mesh_name, name="top", tag="top", material="metal")
devsim.add_1d_contact(mesh=mesh_name, name="bottom", tag="bottom", material="metal")
devsim.add_1d_region(mesh=mesh_name, region=region, tag1="top", tag2="bottom", material="Silicon")
devsim.finalize_mesh(mesh=mesh_name)
devsim.create_device(mesh=mesh_name, device=device)
# 3. Setup Doping Profile models in DEVSIM
# Calculate separate donors and acceptors on the grid
donors_array, acceptors_array, step_profiles = calc_donors_acceptors(all_x, process_steps, substrate_type, substrate_doping, length)
# Set Donors directly
devsim.node_solution(device=device, region=region, name="Donors_data")
devsim.set_node_values(device=device, region=region, name="Donors_data", values=donors_array)
devsim.node_model(device=device, region=region, name="Donors", equation="Donors_data")
# Set Acceptors directly
devsim.node_solution(device=device, region=region, name="Acceptors_data")
devsim.set_node_values(device=device, region=region, name="Acceptors_data", values=acceptors_array)
devsim.node_model(device=device, region=region, name="Acceptors", equation="Acceptors_data")
# Define NetDoping model
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region=region, name="x_um", equation=f"x / {um}")
devsim.set_parameter(device=device, name="top_bias", value=0.0)
devsim.set_parameter(device=device, name="bottom_bias", value=0.0)
# 4. Setup Potential-only Equilibrium equations
CreateSolution(device, region, "Potential")
CreateSiliconPotentialOnly(device, region)
CreateSiliconPotentialOnlyContact(device, region, "top")
CreateSiliconPotentialOnlyContact(device, region, "bottom")
# Solve thermal equilibrium (0V)
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=100)
# 5. Set up Drift-Diffusion physical models
CreateSolution(device, region, "Electrons")
CreateSolution(device, region, "Holes")
devsim.set_node_values(device=device, region=region, name="Electrons", init_from="IntrinsicElectrons")
devsim.set_node_values(device=device, region=region, name="Holes", init_from="IntrinsicHoles")
# Redefine IntrinsicElectrons, IntrinsicHoles to avoid overflow/underflow
devsim.node_model(device=device, region=region, name="IntrinsicElectrons", equation="Electrons")
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Potential", equation="0")
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Electrons", equation="1")
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Holes", equation="0")
devsim.node_model(device=device, region=region, name="IntrinsicHoles", equation="Holes")
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Potential", equation="0")
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Electrons", equation="0")
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Holes", equation="1")
arora_opts = CreateAroraMobilityLF(device, region)
hf_opts = CreateHFMobility(device, region, **arora_opts)
CreateSiliconDriftDiffusion(device, region, **hf_opts)
CreateSiliconDriftDiffusionContact(device, region, "top", hf_opts['Jn'], hf_opts['Jp'])
CreateSiliconDriftDiffusionContact(device, region, "bottom", hf_opts['Jn'], hf_opts['Jp'])
# Override equations
if enable_avalanche:
CreateAvalancheGeneration(device, region, hf_opts['Jn'], hf_opts['Jp'])
devsim.equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=hf_opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region=region, name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=hf_opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
else:
devsim.equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=hf_opts['Jn'],
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region=region, name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=hf_opts['Jp'],
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3)
# Solve 0V Drift-Diffusion
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-8, charge_error=1e12, maximum_iterations=100)
# Calculate current density at 0V
jn_edge_0 = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
jp_edge_0 = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
jtot_avg_0 = -np.mean(jn_edge_0 + jp_edge_0) # Current flows from right to left
v_history = [0.0]
j_history = [jtot_avg_0]
# 6. Adaptive Geometric/Log Micro-Sweep execution to reach bias_target
v_current = 0.0
v_target = bias_target
adaptive_mult = 1.0
while abs(v_current) < abs(v_target):
# Calculate base step size based on current voltage
abs_v = abs(v_current)
if abs_v < 1.0:
step_size = 0.05
else:
step_size = abs_v / 20.0
# For avalanche sweep, if current starts to rise, limit step size to 2V
if enable_avalanche and len(j_history) > 0 and j_history[-1] > 1e-4:
step_size = min(step_size, 2.0)
# Apply adaptive multiplier (from convergence failure backtracking)
step_size *= adaptive_mult
# Halt if step size becomes non-physically tiny
if step_size < 0.005:
print(f"Step size too small ({step_size:.5f} V) at V = {v_current} V, stopping sweep.")
break
# Determine next voltage
v_next = v_current + np.sign(v_target) * step_size
if abs(v_next) > abs(v_target):
v_next = v_target
devsim.set_parameter(device=device, name="top_bias", value=0.0) # Left is reference 0V
devsim.set_parameter(device=device, name="bottom_bias", value=v_next) # Right is biased
try:
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=40)
v_current = v_next
# Record current (flowing from right to left, which is -J)
jn_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
jp_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
j_val = -np.mean(jn_edge + jp_edge)
v_history.append(v_current)
j_history.append(j_val)
# Check 1mA current limit
current_ma = j_val * area_cm2 * 1000.0
if current_ma > 1.0:
print(f"Current limit 1mA reached at V = {v_current} V ({current_ma:.2f} mA). Stopping sweep.")
break
# If successful, recover the multiplier towards 1.0
adaptive_mult = min(1.0, adaptive_mult * 1.5)
except Exception as e:
print(f"Sweep convergence failure at V = {v_next} V. Backtracking with smaller step size...")
adaptive_mult /= 4.0
# Note: v_current is not updated, so the loop retries from same position with smaller step_size
# 7. Collect output profiles
x_cm = np.array(devsim.get_node_model_values(device=device, region=region, name="x"))
x_plot = x_cm / um # Convert back to um for plotting
potential = np.array(devsim.get_node_model_values(device=device, region=region, name="Potential"))
electrons = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
holes = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
doping = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
efn = np.array(devsim.get_node_model_values(device=device, region=region, name="EFN"))
efp = np.array(devsim.get_node_model_values(device=device, region=region, name="EFP"))
ec = np.array(devsim.get_node_model_values(device=device, region=region, name="EC"))
ev = np.array(devsim.get_node_model_values(device=device, region=region, name="EV"))
efield_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name="EField"))
x_edge_mid = (x_plot[:-1] + x_plot[1:]) / 2.0
jn_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
jp_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
jtot_edge = jn_edge + jp_edge
g_av_edge = np.zeros_like(efield_edge)
if enable_avalanche:
try:
g_av_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name="AvalancheGeneration"))
q = 1.6e-19
g_av_edge = np.abs(g_av_edge) / q
except Exception:
pass
# Final current density from right to left
j_avg_rl = -np.mean(jtot_edge)
peak_efield = np.max(np.abs(efield_edge))
is_depleted = np.abs(electrons - holes) < 0.1 * np.abs(doping)
dep_edges = x_plot[is_depleted]
if len(dep_edges) > 0:
w_left = np.min(dep_edges)
w_right = np.max(dep_edges)
depletion_width = w_right - w_left
else:
w_left, w_right, depletion_width = 0.0, 0.0, 0.0
return {
"v_solved": v_current,
"x": x_plot,
"potential": potential,
"electrons": electrons,
"holes": holes,
"doping": doping,
"efn": efn,
"efp": efp,
"ec": ec,
"ev": ev,
"x_edge": x_edge_mid,
"efield": np.abs(efield_edge),
"jn": jn_edge,
"jp": jp_edge,
"jtot": jtot_edge,
"g_av": g_av_edge,
"current_density": j_avg_rl,
"peak_field": peak_efield,
"depletion_width": depletion_width,
"depletion_left": w_left,
"depletion_right": w_right,
# Sweep history
"v_history": v_history,
"j_history": j_history,
"step_profiles": step_profiles
}
if __name__ == "__main__":
import pickle
import sys
if len(sys.argv) < 3:
print("Usage: solve_1d.py <input_pickle_path> <output_pickle_path>")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2]
with open(input_path, 'rb') as f:
args = pickle.load(f)
result = build_and_solve_1d(
bias_target=args['bias_target'],
substrate_type=args['substrate_type'],
substrate_doping=args['substrate_doping'],
length=args['length'],
process_steps=args['process_steps'],
enable_avalanche=args['enable_avalanche'],
area_cm2=args['area_cm2']
)
with open(output_path, 'wb') as f:
pickle.dump(result, f)
+52
View File
@@ -0,0 +1,52 @@
# gui1d/test_run.py
from solve_1d import build_and_solve_1d
print("Running test solve at V = -50.0V (Avalanche ON) with process steps...")
steps = [
{
'enabled': True,
'type': 'p',
'dopant': 'Boron',
'method': 'Implant',
'surface': 'Top',
'energy': 80.0,
'dose': 1e12,
'temp': 1000.0,
'time': 60.0
}
]
try:
print("Call 1...")
res = build_and_solve_1d(
bias_target=50.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=True
)
print("Call 2...")
res2 = build_and_solve_1d(
bias_target=50.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=False
)
print("Call 3...")
res3 = build_and_solve_1d(
bias_target=5.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=False
)
print("All 3 calls passed successfully!")
except Exception as e:
print("Test failed with error:")
import traceback
traceback.print_exc()
+9 -2
View File
@@ -39,7 +39,7 @@ recon_logic = """
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
try:
@@ -106,7 +106,14 @@ bv_code = code
bv_code = "import pickle\nimport argparse\n" + bv_code
# Turn ON Avalanche by default in solve_sweep_bv.py
bv_code = bv_code.replace('edge_volume_model=""', 'edge_volume_model="AvalancheGeneration"')
bv_code = bv_code.replace(
'name="ElectronContinuityEquation", variable_name="Electrons",\n time_node_model="NCharge", edge_model=opts[\'Jn\'], edge_volume_model=""',
'name="ElectronContinuityEquation", variable_name="Electrons",\n time_node_model="NCharge", edge_model=opts[\'Jn\'], edge_volume_model="AvalancheGeneration"'
)
bv_code = bv_code.replace(
'name="HoleContinuityEquation", variable_name="Holes",\n time_node_model="PCharge", edge_model=opts[\'Jp\'], edge_volume_model=""',
'name="HoleContinuityEquation", variable_name="Holes",\n time_node_model="PCharge", edge_model=opts[\'Jp\'], edge_volume_model="AvalancheGeneration_p"'
)
# Keep Avalanche ON by default
arg_parse = """
+9 -3
View File
@@ -513,11 +513,17 @@ def CreateAvalancheGeneration(device, region, Jn, Jp):
# Equation is solving for Charge, so generation must be multiplied by q.
# q * G_av = alpha_n * |J_n| + alpha_p * |J_p|
eq = f"-(alpha_n * {smooth_abs_jn} + alpha_p * {smooth_abs_jp})"
CreateEdgeModel(device, region, "AvalancheGeneration", eq)
# For electrons (NCharge = +q * n), generation adds positive charge, so it is positive.
eq_n = f"+(alpha_n * {smooth_abs_jn} + alpha_p * {smooth_abs_jp})"
CreateEdgeModel(device, region, "AvalancheGeneration", eq_n)
# For holes (PCharge = -q * p), generation adds positive holes (which is negative charge), so it is negative.
eq_p = f"-(alpha_n * {smooth_abs_jn} + alpha_p * {smooth_abs_jp})"
CreateEdgeModel(device, region, "AvalancheGeneration_p", eq_p)
# Derivatives w.r.t Potential, Electrons, Holes
for i in ("Potential", "Electrons", "Holes"):
CreateEdgeModelDerivatives(device, region, "AvalancheGeneration", eq, i)
CreateEdgeModelDerivatives(device, region, "AvalancheGeneration", eq_n, i)
CreateEdgeModelDerivatives(device, region, "AvalancheGeneration_p", eq_p, i)
return "AvalancheGeneration"
+9486
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -11,6 +11,7 @@
5. 每次對話告一個段落,或者有重要的共識或選項的時候,都要來更新這個檔案,保持詳實的對話紀錄,以便後續 agent 進來的時候參考 pick-up
6.碰到問題的時候,務必先告訴我你的判斷與處理,切忌熊熊就噴出一個執行授權 (我完全不知道為何要重新執行程式)
7. 電荷 1.6e-19 這個 factor 的問題發生過幾次,要特別小心
8. 既然在 Google Antigravity IDE 中,目前版本的 AI 對話視窗(Agent Window)尚未原生啟用 LaTeX 數學公式渲染功能,因此公式在此對話區中只能顯示為原始程式碼。那麼請你在 chat 裡,簡單的數學式 避免使用 LaTeX ... 如果有複雜的公式,也許並列 LaTeX 我再去用 md 預覽
@@ -1333,3 +1334,35 @@ $$\Delta V < W_{gb} \cdot \sqrt{\frac{2 q \cdot N_{SUB} \cdot V}{\epsilon}}$$
* **效果**:高密度區與安全地基像滑動窗口一樣隨空乏前沿動態推進,深處空曠區始終保持輕量化,最大程度降低高壓段的運算規模與記憶體開銷。
### 24. 雪崩產生項(Impact Ionization)正負號物理修正與電荷守恆(2026-06-17)
在進行高壓 reverse bias 掃描時(開啟雪崩 Avalanche),我們發現反向漏電流在高壓下出現了往下掉甚至變為**負值**的非物理現象。為了解決此問題,我們對 DEVSIM 方程式的正負號與電荷守恆進行了深度的診斷與修正。
#### 24.1 根本原因分析: minority carrier 與電荷正負號錯誤
1. **DEVSIM 電荷變數定義**
- 電子連續方程式(ECE)求解 $Q_n = +q \cdot n$ (正電荷)。
- 電洞連續方程式(HCE)求解 $Q_p = -q \cdot p$ (負電荷)。
2. **雪崩模型的正負號衝突**
- 舊代碼中,電子與電洞方程式皆使用同一個邊緣體積模型 `edge_volume_model="AvalancheGeneration"`,其定義為負值:`eq = -(alpha_n * |Jn| + alpha_p * |Jp|)`
- 對電子(ECE,變數為 $qn$)而言,在殘差方程式 $F_n = 0$ 中減去這個負值,相當於物理上的增加電子(產生源 $+q G_{av}$),是正確的。
- 但對電洞(HCE,變數為 $-qp$)而言,由於電洞電荷為負值,減去同一個負值反而變成了消滅電洞(複合匯 $-q G_{av}$)!
- 這導致雪崩產生時「電洞被消滅,電子被產生」,嚴重違反電荷守恆,進而導致高逆偏壓下電洞濃度異常低下,造成總電流轉向為負值。
#### 24.2 修正方案
為了確保電子和電洞在雪崩發生時皆以產生源(Source)的形式增加,我們在 [new_physics.py](file:///home/pchan/devsim2026/physics/new_physics.py) 中定義了兩個具有不同正負號的雪崩模型:
- `AvalancheGeneration`:給電子連續方程式用,值為正(`+(alpha_n * |Jn| + alpha_p * |Jp|)`)。
- `AvalancheGeneration_p`:給電洞連續方程式用,值為負(`-(alpha_n * |Jn| + alpha_p * |Jp|)`)。
我們同步修改了所有涉及到雪崩設定的 1D 與 2D 求解程式,將 `HoleContinuityEquation` 中的 `edge_volume_model` 從原本的 `"AvalancheGeneration"` 變更為 `"AvalancheGeneration_p"`
- 1D 求解核心:[solve_1d.py](file:///home/pchan/devsim2026/gui1d/solve_1d.py)
- 2D 偏壓接續掃描與重建:[resume_run.py](file:///home/pchan/devsim2026/resume_run.py)、[dynamic_refine.py](file:///home/pchan/devsim2026/dynamic_refine.py)
- 2D 腳本產生器:[make_scripts.py](file:///home/pchan/devsim2026/make_scripts.py)(自動更新 `solve_sweep_bv.py``solve_sweep_recon.py`
#### 24.3 驗證成效
修正後,1D 與 2D 的反向雪崩電流通路均完全恢復物理正確性:
- 漏電流隨偏壓增加保持單調遞增(Monotonically Increasing)且 strictly positive。
- 電流在接近擊穿點(約 409V)時呈現教科書式的指數級雪崩擊穿暴增,並能正確觸碰 1mA 限流保護停止掃描。
- 這徹底解決了高反向偏壓下雪崩電流倒灌變負、以及在大偏壓網格細化時的發散問題,使 1D 和 2D 雪崩模擬皆能 100% 收斂且符合物理實相。
+22 -17
View File
@@ -255,7 +255,8 @@ devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
temp_val = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=temp_val)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide potential equations
@@ -387,12 +388,13 @@ print("Configuring continuity and potential equations for the bias sweep (min_er
if is_avalanche_enabled:
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="PotentialEquation", variable_name="Potential",
node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3)
@@ -508,12 +510,13 @@ while v_current < v_target:
iters1 = 15
try:
# Always use log_damp preconditioning for Electron/Hole continuity equations in Stage 1
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
res1 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=5, rollback=False, info=True)
iters1 = len(res1.get("iterations", []))
@@ -521,12 +524,13 @@ while v_current < v_target:
pass # Ignore non-convergence in pre-conditioning
finally:
# Always revert to positive variable update for Stage 2 precision Newton
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
import psutil
@@ -642,7 +646,7 @@ while v_current < v_target:
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
try:
@@ -661,7 +665,7 @@ while v_current < v_target:
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
# Stage 2 solve (Strict Tolerance)
@@ -690,12 +694,13 @@ while v_current < v_target:
# Restore state and Reset Avalanche state to main loop configuration
restore_state(device, state)
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
else:
print("Avalanche probe skipped (AVALANCHE option is disabled).")
@@ -722,7 +727,7 @@ while v_current < v_target:
step_size = min(step_size * 1.2, max_step)
sn = 0
else:
sn += 1
sn += 1.25
step_count += 1
@@ -805,7 +810,7 @@ plt.yscale('log')
plt.grid(True, which="both", ls="--")
plt.xlabel("Bias Voltage (V)")
plt.ylabel("Terminal Current (A/cm, Log Scale)")
plt.title(SIM_NAME)
plt.title(f"{SIM_NAME} (T={temp_val}K)")
plt.tight_layout()
plt.savefig(f"{OUT_DIR}sweep_iv_2d.png", dpi=300)
plt.close()
+2 -1
View File
@@ -77,7 +77,8 @@ def run_simulation(mesh_file="device_2d.msh", tec_file="static_preview.tec", png
# 3. Solutions and Physics
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
sim_temp = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=sim_temp)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide
+2 -1
View File
@@ -94,7 +94,8 @@ devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation
# 3. Create solution variables and physics models
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
sim_temp = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=sim_temp)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide Potential physics setup
+3 -2
View File
@@ -103,7 +103,8 @@ devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
temp_val = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=temp_val)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide potential equations
@@ -413,7 +414,7 @@ plt.yscale('log')
plt.grid(True, which="both", ls="--")
plt.xlabel("Bias Voltage (V)")
plt.ylabel("Terminal Current Magnitude (A)")
plt.title("TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale)")
plt.title(f"TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale) (T={temp_val}K)")
plt.tight_layout()
plt.savefig(f"{OUT_DIR}sweep_iv_2d.png", dpi=300)
plt.close()
+17 -9
View File
@@ -3,13 +3,13 @@ import argparse
import os
import sys
import glob
import gc
# Limit the thread count for parallel solvers to prevent WSL from resource starvation/disconnecting
os.environ["OMP_NUM_THREADS"] = "4"
os.environ["MKL_NUM_THREADS"] = "4"
os.environ["TBB_NUM_THREADS"] = "4"
os.environ["OPENBLAS_NUM_THREADS"] = "4"
os.environ["OMP_NUM_THREADS"] = "6"
os.environ["MKL_NUM_THREADS"] = "6"
os.environ["TBB_NUM_THREADS"] = "6"
os.environ["OPENBLAS_NUM_THREADS"] = "6"
# Disable MKL internal memory manager to prevent memory accumulation (OOM) across solve calls
os.environ["MKL_DISABLE_FAST_MM"] = "1"
@@ -105,7 +105,8 @@ devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
temp_val = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=temp_val)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide potential equations
@@ -243,7 +244,7 @@ devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquatio
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="PotentialEquation", variable_name="Potential",
node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3)
@@ -333,10 +334,17 @@ while v_current < v_target:
iters1 = len(res1.get("iterations", []))
except devsim.error:
pass # Ignore non-convergence in pre-conditioning
import psutil
mem_stage1 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Stage 1 (Precondition) Memory Usage: {mem_stage1:.1f} GB")
# Stage 2: Precision Newton (Strict Tolerance) - slightly relaxed relative_error to 3e-3 to avoid limit cycle oscillations
res = devsim.solve(type="dc", absolute_error=1e10, relative_error=3e-3, charge_error=1e12, maximum_iterations=12, info=True)
iters2 = len(res.get("iterations", []))
mem_stage2 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Stage 2 (Newton) Memory Usage: {mem_stage2:.1f} GB")
iters = f"{iters1}+{iters2}"
total_iters = iters1 + iters2
@@ -365,7 +373,7 @@ while v_current < v_target:
print(f"Step {step_count}: Converged at V = {v_current:.4f} V, I = {total_curr:.4e} A. Step size: {step_size:.4f} V. Iterations: {iters}. Time: {time_taken:.2f} s")
# Log to file
time_log.write(f"{time.strftime('%X')}\t{v_current:.4f}\t{step_size:.4f}\t{total_curr:.4e}\t{iters}\t{time_taken:.2f}\n")
time_log.write(f"{time.strftime('%X')}\t{v_current:.4f}\t{step_size:.4f}\t{total_curr:.4e}\t{iters}\t{time_taken:.2f}\t{mem_stage1:.1f}GB\t{mem_stage2:.1f}GB\n")
# Save checkpoints when crossing target voltages
for target in save_targets:
@@ -429,7 +437,7 @@ plt.yscale('log')
plt.grid(True, which="both", ls="--")
plt.xlabel("Bias Voltage (V)")
plt.ylabel("Terminal Current Magnitude (A)")
plt.title("TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale)")
plt.title(f"TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale) (T={temp_val}K)")
plt.tight_layout()
plt.savefig(f"{OUT_DIR}sweep_iv_2d.png", dpi=300)
plt.close()
+106 -310
View File
@@ -9,7 +9,6 @@ os.environ["OMP_NUM_THREADS"] = "6"
os.environ["MKL_NUM_THREADS"] = "6"
os.environ["TBB_NUM_THREADS"] = "6"
os.environ["OPENBLAS_NUM_THREADS"] = "6"
# Disable MKL internal memory manager to prevent memory accumulation (OOM) across solve calls
os.environ["MKL_DISABLE_FAST_MM"] = "1"
@@ -21,41 +20,8 @@ if mkl_libs:
import devsim
import numpy as np
import datetime
today_str = datetime.datetime.now().strftime("%y%m%d") # e.g. "260615"
prefix = f"output_{today_str}_"
max_num = 0
for entry in os.listdir("."):
if os.path.isdir(entry) and entry.startswith(prefix):
try:
num = int(entry[len(prefix):])
if num > max_num:
max_num = num
except ValueError:
pass
next_num = max_num + 1
OUT_DIR = f"output_{today_str}_{next_num:02d}/"
is_avalanche_enabled = os.environ.get("AVALANCHE", "false").lower() == "true"
refine_v_step = float(os.environ.get("REFINE_V_STEP", "50.0"))
is_refine_enabled = os.environ.get("REFINE", "false").lower() == "true"
if refine_v_step < 1.0:
is_refine_enabled = False
print(f"refine_v_step < 1.0 V detected ({refine_v_step} V): Dynamic refinement is completely DISABLED.")
print(f"Option: AVALANCHE={is_avalanche_enabled}, REFINE={is_refine_enabled}, REFINE_V_STEP={refine_v_step}V, OUT_DIR={OUT_DIR}")
OUT_DIR = "output_this_run/"
os.makedirs(OUT_DIR, exist_ok=True)
import shutil
# 備份輸入幾何網格與配置參數,以及當前執行腳本本身以利模型重建
try:
if os.path.exists("device_2d.msh"):
shutil.copy2("device_2d.msh", os.path.join(OUT_DIR, "device_2d.msh"))
if os.path.exists("device_config.py"):
shutil.copy2("device_config.py", os.path.join(OUT_DIR, "device_config.py"))
if __file__:
shutil.copy2(__file__, os.path.join(OUT_DIR, os.path.basename(__file__)))
except Exception as e:
print(f"Warning: Failed to back up reproduction archive files: {e}")
import matplotlib.pyplot as plt
import time
@@ -138,7 +104,8 @@ devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
devsim.set_parameter(device=device, name="T", value="300")
temp_val = os.environ.get("TEMP", "300")
devsim.set_parameter(device=device, name="T", value=temp_val)
CreateSiliconPotentialOnly(device, "Silicon")
# Oxide potential equations
@@ -269,43 +236,32 @@ print("Initial Drift-Diffusion converged successfully!")
# Switch continuity and potential equations for the bias sweep
print("Configuring continuity and potential equations for the bias sweep (min_error=1e5, positive update)...")
# Instantiate Avalanche (Impact Ionization) edge generation model if enabled
if is_avalanche_enabled:
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
# Instantiate Avalanche (Impact Ionization) edge generation model
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="PotentialEquation", variable_name="Potential",
node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3)
# Setup E_mag_log element models
for reg in ["Silicon", "Oxide", "Molding"]:
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
devsim.element_model(device=device, region=reg, name="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)")
# Save zero-bias tecplot and VTK
devsim.write_devices(file=f"{OUT_DIR}sweep_preview_0V.tec", type="tecplot")
# devsim.write_devices(file="sweep_preview_0V", type="vtk")
# 5. Define Sweep Parameters
v_target = 1000.0
if len(sys.argv) > 1:
try:
v_target = float(sys.argv[1])
print(f"Custom target voltage specified: {v_target} V")
except ValueError:
print(f"Warning: Invalid target voltage '{sys.argv[1]}', using default {v_target} V")
v_current = 0.0
step_size = 0.1 # Initial step size (V)
max_step = 50.0 # Maximum step size (V)
min_step = 1e-4 # Minimum step size (V)
compliance_current = 100e-3 # 100 mA compliance current
compliance_current = 1e-3 # 1 mA compliance current
# Helper functions to save/restore state in case of convergence failure
def save_state(device):
@@ -324,18 +280,17 @@ def restore_state(device, state):
devsim.set_node_values(device=device, region="Silicon", name="Electrons", values=state["Silicon"]["Electrons"])
devsim.set_node_values(device=device, region="Silicon", name="Holes", values=state["Silicon"]["Holes"])
import resource
# File logging setup
time_log = open(f"{OUT_DIR}simulation_time.log", "w", buffering=1)
time_log.write("Time\tVoltage(V)\tStep(V)\tCurrent(A)\tIterations\tTimeTaken(s)\tMaxMemory(MB)\n")
time_log.write("Time\tVoltage(V)\tStep(V)\tCurrent(A)\tIterations\tTimeTaken(s)\n")
# Arrays to store I-V data
voltage_list = [0.0]
current_list = [0.0]
# Recon variables
next_recon_v = refine_v_step
with open(f"{OUT_DIR}recon_avalanche.log", "w") as f:
next_recon_v = 50.0
with open("recon_avalanche.log", "w") as f:
f.write("Voltage(V)\tAvalancheCurrent(A)\n")
@@ -345,24 +300,10 @@ start_sweep_time = time.time()
print("Beginning adaptive bias sweep...")
step_count = 0
iter_history = []
just_refined = False
sn = 0
consecutive_fails = 0
# --- Adaptive step control thresholds ---
step_ctl_reduce = 12
step_ctl_enlarge = 8
# --- Output Milestones (Voltage & Current) ---
TEC_VOLTAGE_TARGETS = [5.0, 50.0, 250.0, 500.0]
TEC_CURRENT_TARGETS = [1e-8, 1e-7, 1e-6]
saved_voltage_targets = {t for t in TEC_VOLTAGE_TARGETS if v_current >= t}
saved_current_targets = set() # Initialized empty, will populate as sweep current rises
# Pre-refinement seed save targets
seed_save_targets = [5.0, 25.0, 45.0, 95.0, 195.0, 395.0, 595.0, 795.0, 995.0, 1195.0]
saved_seeds = {t for t in seed_save_targets if v_current >= t}
# Targets for saving intermediate state checkpoints
save_targets = [5.0, 50.0, 500.0]
saved_targets = set()
while v_current < v_target:
v_next = min(v_current + step_size, v_target)
@@ -373,44 +314,22 @@ while v_current < v_target:
step_start_time = time.time()
try:
use_precondition = True
if use_precondition:
iters1 = 15
try:
# Always use log_damp preconditioning for Electron/Hole continuity equations in Stage 1
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
res1 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=5, rollback=False, info=True)
iters1 = len(res1.get("iterations", []))
except devsim.error:
pass # Ignore non-convergence in pre-conditioning
finally:
# Always revert to positive variable update for Stage 2 precision Newton
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
import psutil
mem_stage1 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Stage 1 (Precondition) Memory Usage: {mem_stage1:.1f} GB")
else:
iters1 = 0
mem_stage1 = 0.0
# Stage 2: Precision Newton (Strict Tolerance)
res = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=10, info=True)
iters2 = len(res.get("iterations", []))
# Stage 1: Pre-conditioning (Relaxed Tolerance to get a good initial guess)
iters1 = 10
try:
res1 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-1, charge_error=1e12, maximum_iterations=10, info=True)
iters1 = len(res1.get("iterations", []))
except devsim.error:
pass # Ignore non-convergence in pre-conditioning
import psutil
mem_stage1 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Stage 1 (Precondition) Memory Usage: {mem_stage1:.1f} GB")
# Stage 2: Precision Newton (Strict Tolerance) - slightly relaxed relative_error to 3e-3 to avoid limit cycle oscillations
res = devsim.solve(type="dc", absolute_error=1e10, relative_error=3e-3, charge_error=1e12, maximum_iterations=12, info=True)
iters2 = len(res.get("iterations", []))
mem_stage2 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Stage 2 (Newton) Memory Usage: {mem_stage2:.1f} GB")
iters = f"{iters1}+{iters2}"
@@ -434,7 +353,6 @@ while v_current < v_target:
# Update simulation status
v_current = v_next
state = save_state(device)
just_refined = False
voltage_list.append(v_current)
current_list.append(total_curr)
@@ -444,40 +362,15 @@ while v_current < v_target:
# Log to file
time_log.write(f"{time.strftime('%X')}\t{v_current:.4f}\t{step_size:.4f}\t{total_curr:.4e}\t{iters}\t{time_taken:.2f}\t{mem_stage1:.1f}GB\t{mem_stage2:.1f}GB\n")
# Check and save voltage milestones
for target in TEC_VOLTAGE_TARGETS:
if v_current >= target and target not in saved_voltage_targets:
# Save checkpoints when crossing target voltages
for target in save_targets:
if v_current >= target and target not in saved_targets:
filename = f"sweep_preview_{int(target)}V.tec"
print(f"Saving voltage milestone checkpoint at V = {v_current:.2f} V to {filename}...")
for reg in ["Silicon", "Oxide", "Molding"]:
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
devsim.element_model(device=device, region=reg, name="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)")
filename_vtk = f"sweep_preview_{int(target)}V"
print(f"Saving checkpoint at V = {v_current:.2f} V to {filename} and VTK...")
devsim.write_devices(file=f"{OUT_DIR}{filename}", type="tecplot")
saved_voltage_targets.add(target)
# Check and save current milestones
for target in TEC_CURRENT_TARGETS:
if abs(total_curr) >= target and target not in saved_current_targets:
filename = f"sweep_preview_current_{target:.1e}.tec"
print(f"Saving current milestone checkpoint at I = {total_curr:.4e} A to {filename}...")
for reg in ["Silicon", "Oxide", "Molding"]:
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
devsim.element_model(device=device, region=reg, name="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)")
devsim.write_devices(file=f"{OUT_DIR}{filename}", type="tecplot")
saved_current_targets.add(target)
# Save pre-refinement seeds when crossing target voltages
for target in seed_save_targets:
if v_current >= target and target not in saved_seeds:
seed_filename = f"{OUT_DIR}seed_{int(target)}V.pkl"
state_to_save = save_state(device)
seed_data = {"voltage": v_current, "step_size": step_size, "state": state_to_save}
with open(seed_filename, "wb") as f:
pickle.dump(seed_data, f)
print(f"Saved pre-refinement seed checkpoint at V = {v_current:.2f} V to {seed_filename}")
saved_seeds.add(target)
# devsim.write_devices(file=filename_vtk, type="vtk")
saved_targets.add(target)
# Compliance check
if abs(total_curr) >= compliance_current:
@@ -486,165 +379,82 @@ while v_current < v_target:
break
# --- RECONNAISSANCE PROBE & DYNAMIC REFINE ---
if is_refine_enabled and (v_current >= next_recon_v):
import dynamic_refine
try:
# 1. 執行網格自適應重劃與狀態插值
refined_device, refined_opts = dynamic_refine.refine_and_interpolate(device, v_current, is_avalanche_enabled=is_avalanche_enabled, time_log=time_log, out_dir=OUT_DIR)
device = refined_device
opts = refined_opts
just_refined = True
step_size = max(step_size / 5.0, 0.1) # 網格重建後,將步長縮小至 1/5 以防首步發散
# 2. 儲存優化後的狀態為 Seed
state = save_state(device)
seed_data = {"voltage": v_current, "step_size": step_size, "state": state}
seed_filename = f"{OUT_DIR}seed_{int(next_recon_v)}V.pkl"
with open(seed_filename, "wb") as f:
pickle.dump(seed_data, f)
print(f"\n--- RECON PROBE at {v_current:.2f} V ---")
print(f"Saved refined seed to {seed_filename}")
if is_avalanche_enabled:
# Turn ON Avalanche (Use log_damp for Stage 1 pre-conditioning stability)
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
try:
# Stage 1 pre-conditioning
try:
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=5, rollback=False, info=True)
except devsim.error:
pass
import psutil
mem_recon_stage1 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Recon Stage 1 Memory Usage: {mem_recon_stage1:.1f} GB")
# Switch to positive update for Stage 2 precision Newton solve
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
# Stage 2 solve (Strict Tolerance)
res_av = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=10, info=True)
mem_recon_stage2 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
print(f"Recon Stage 2 Memory Usage: {mem_recon_stage2:.1f} GB")
if res_av.get("converged", False):
# Measure Avalanche current
ia_n_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="ElectronContinuityEquation")
ia_p_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="HoleContinuityEquation")
ia_n_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="ElectronContinuityEquation")
ia_p_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="HoleContinuityEquation")
av_curr = ia_n_si + ia_p_si + ia_n_p12 + ia_p_p12
print(f"Avalanche Current at {v_current:.2f} V: {av_curr:.4e} A")
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\t{av_curr:.4e}\n")
else:
print("Avalanche failed to converge.")
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\tFAILED\n")
except devsim.error:
print("Avalanche failed to converge.")
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\tFAILED\n")
# Restore state and Reset Avalanche state to main loop configuration
restore_state(device, state)
av_model = "AvalancheGeneration" if is_avalanche_enabled else ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model,
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=av_model,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
else:
print("Avalanche probe skipped (AVALANCHE option is disabled).")
print("--- END RECON PROBE ---\n")
next_recon_v += refine_v_step
except devsim.error as ref_err:
print(f"\n!!! DYNAMIC REFINE FAILED at {v_current:.2f} V: {ref_err} !!!")
print("Restoring coarse device state and continuing simulation on the COARSE mesh.")
restore_state(device, state)
next_recon_v += refine_v_step
# --- RECONNAISSANCE PROBE ---
if v_current >= next_recon_v:
state_data = save_state(device)
seed_data = {"voltage": v_current, "step_size": step_size, "state": state_data}
seed_filename = f"seed_{int(next_recon_v)}V.pkl"
with open(seed_filename, "wb") as f:
pickle.dump(seed_data, f)
print(f"\n--- RECON PROBE at {v_current:.2f} V ---")
print(f"Saved seed to {seed_filename}")
# Grow step size for next step adaptively based on Newton iterations (Rolling average)
consecutive_fails = 0
iter_history.append(total_iters)
if len(iter_history) > 3:
iter_history.pop(0)
avg_iters = sum(iter_history) / len(iter_history)
# Turn ON Avalanche
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="AvalancheGeneration",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="AvalancheGeneration_p",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
try:
# Stage 1 pre-conditioning
try:
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-1, charge_error=1e12, maximum_iterations=10, info=True)
except devsim.error:
pass
# Stage 2 solve
res_av = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=15, info=True)
if res_av.get("converged", False):
# Measure Avalanche current
ia_n_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="ElectronContinuityEquation")
ia_p_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="HoleContinuityEquation")
ia_n_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="ElectronContinuityEquation")
ia_p_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="HoleContinuityEquation")
av_curr = ia_n_si + ia_p_si + ia_n_p12 + ia_p_p12
print(f"Avalanche Current at {v_current:.2f} V: {av_curr:.4e} A")
with open("recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\t{av_curr:.4e}\n")
else:
print("Avalanche failed to converge.")
with open("recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\tFAILED\n")
except devsim.error:
print("Avalanche failed to converge.")
with open("recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\tFAILED\n")
# Restore state and Turn OFF Avalanche
restore_state(device, state_data)
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="",
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="",
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
print("--- END RECON PROBE ---\n")
next_recon_v += 50.0
# Grow step size for next step adaptively based on Newton iterations
eff_iters = max(0.0, avg_iters - sn)
if avg_iters > step_ctl_reduce:
step_size = max(step_size * 0.6, min_step)
sn = 0
elif eff_iters < step_ctl_enlarge:
if total_iters <= 6:
step_size = min(step_size * 1.2, max_step)
sn = 0
elif total_iters <= 9:
pass # Keep step size the same
else:
sn += 1
step_size = max(step_size * 0.8, min_step)
step_count += 1
# Ping-pong rolling checkpoint
if step_count > 0 and step_count % 10 == 0:
checkpoint_file = f"{OUT_DIR}wsl_recovery_checkpoint_A.pkl" if (step_count // 10) % 2 != 0 else f"{OUT_DIR}wsl_recovery_checkpoint_B.pkl"
checkpoint_data = {
"v_current": v_current,
"step_size": step_size,
"step_count": step_count,
"voltage_list": voltage_list,
"current_list": current_list,
"next_recon_v": next_recon_v,
"state": state
}
with open(checkpoint_file, "wb") as f:
pickle.dump(checkpoint_data, f)
print(f"Rolling checkpoint saved to {checkpoint_file}")
time_log.write(f"Rolling checkpoint saved to {checkpoint_file}\n")
# Active garbage collection to prevent memory accumulation
gc.collect()
# Force Intel MKL to release thread buffers
try:
import ctypes
import glob
import sys
import os
mkl_libs = glob.glob(os.path.join(os.path.dirname(sys.executable), "../lib/libmkl_rt.so.*"))
if mkl_libs:
ctypes.CDLL(mkl_libs[0]).MKL_Free_Buffers()
# Force glibc to return freed memory to the OS (mitigate malloc fragmentation)
libc = ctypes.CDLL("libc.so.6")
libc.malloc_trim(0)
except Exception as e:
pass
except devsim.error as e:
# Convergence failure: restore last state and cut step size
step_end_time = time.time()
time_taken = step_end_time - step_start_time
consecutive_fails += 1
shrink_factor = 0.3 if consecutive_fails == 1 else 0.1
print(f"Convergence failure at V = {v_next:.4f} V. Restoring state and scaling step size by {shrink_factor} from {step_size:.4f} V (consecutive fails: {consecutive_fails}).")
mem_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
time_log.write(f"{time.strftime('%X')}\t{v_next:.4f}\t{step_size:.4f}\tFAILED\t-\t{time_taken:.2f}\t{mem_mb:.2f}\n")
print(f"Convergence failure at V = {v_next:.4f} V. Restoring state and scaling step size by 0.577 from {step_size:.4f} V.")
time_log.write(f"{time.strftime('%X')}\t{v_next:.4f}\t{step_size:.4f}\tFAILED\t-\t{time_taken:.2f}\n")
restore_state(device, state)
step_size *= shrink_factor
iter_history = [step_ctl_reduce, step_ctl_reduce, step_ctl_reduce] # Option A virtual history to stay conservative
sn = 0 # Reset counter on failure
step_size *= 0.577
if step_size < min_step:
print("Step size has fallen below minimum limit. Aborting simulation.")
@@ -658,25 +468,21 @@ time_log.close()
# 6. Save final results and generate plots
# Save final tecplot and VTK at highest voltage
for reg in ["Silicon", "Oxide", "Molding"]:
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
devsim.element_model(device=device, region=reg, name="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)")
devsim.write_devices(file=f"{OUT_DIR}sweep_preview_final.tec", type="tecplot")
# devsim.write_devices(file="sweep_preview_final", type="vtk")
# Save I-V data to CSV
np.savetxt(f"{OUT_DIR}sweep_iv_2d.csv", np.column_stack((voltage_list, current_list)),
header="Voltage(V),Current(A/cm)", delimiter=",")
header="Voltage(V),Current(A)", delimiter=",")
# Plot and save I-V curve
plt.figure(figsize=(8, 6))
plt.plot(voltage_list, np.abs(current_list), 'o-', color='#1f77b4', markersize=2)
plt.plot(voltage_list, np.abs(current_list), 'o-', color='#1f77b4', markersize=4)
plt.yscale('log')
plt.grid(True, which="both", ls="--")
plt.xlabel("Bias Voltage (V)")
plt.ylabel("Terminal Current (A/cm, Log Scale)")
plt.title(SIM_NAME)
plt.ylabel("Terminal Current Magnitude (A)")
plt.title(f"TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale) (T={temp_val}K)")
plt.tight_layout()
plt.savefig(f"{OUT_DIR}sweep_iv_2d.png", dpi=300)
plt.close()
@@ -745,14 +551,4 @@ plt.savefig(f"{OUT_DIR}sweep_potential_2d.png", dpi=300)
plt.close()
print(f"Sweep visualization plots saved: sweep_iv_2d.png and sweep_potential_2d.png.")
# Clean up rolling checkpoints upon successful completion
if v_current >= v_target:
print("Sweep completed successfully. Cleaning up rolling checkpoints...")
for cp in [f"{OUT_DIR}wsl_recovery_checkpoint_A.pkl", f"{OUT_DIR}wsl_recovery_checkpoint_B.pkl"]:
if os.path.exists(cp):
os.remove(cp)
else:
print(f"Sweep did not reach target voltage ({v_target} V). Keeping rolling checkpoints for recovery (current V = {v_current:.2f} V).")
import os; os.system(f'{sys.executable} plot_speed.py {OUT_DIR}simulation_time.log {OUT_DIR}simulation_speed.png')
import os; os.system(f'{sys.executable} plot_speed.py')