Restructure 2D simulation pipeline to support isolated subdirectory-based device management and output directory relocation

This commit is contained in:
pchang718
2026-06-25 22:09:55 +08:00
parent 44b41698e8
commit 978b8d94d4
33 changed files with 2515 additions and 482 deletions
+1
View File
@@ -15,6 +15,7 @@ last_run_outputs/
*.last_log
devsim-dev/
output_*/
devices/*/output_*/
*.pkl
*.txt
nohup.out
+45 -31
View File
@@ -10,9 +10,17 @@ export OPENBLAS_NUM_THREADS = 4
# Default simulation control options
avalanche ?= false
btbt ?= false
refine ?= false
refine_v_step ?= 50.0
temp ?= 300.0
pcad ?= false
# Subdirectory device and output management
dev ?= Triac_rp
out ?= output_260625_01
DEV_DIR = devices/$(dev)
OUT_DIR = $(DEV_DIR)/$(out)
PYTHON := .venv/bin/python
@@ -40,8 +48,7 @@ help:
@echo " make show-conv - Print last few convergence step error details"
@echo ""
@echo "Output & Backup Rules:"
@echo " output_this_run/ - Current logs, checkpoints & visualization plots"
@echo " output_last_run/ - Previous run archive (triggered by new make mesh or sweep)"
@echo " devices/\$$(dev)/\$$(out)/ - Current logs, checkpoints & visualization plots"
@echo " * Note: Rebuilding mesh via make mesh auto-archives output_this_run to prevent"
@echo " loading old checkpoints on the updated grid structure."
@echo ""
@@ -54,8 +61,12 @@ help-detail:
@echo "Detailed Command Parameters and Variables:"
@echo "============================================================================="
@echo "Command Variables:"
@echo " dev=<device_name> - Target device directory name (default: Triac_rp)"
@echo " out=<output_name> - Target output folder name (default: output_260625_01)"
@echo " avalanche=true|false - Toggle impact ionization (avalanche) model"
@echo " Default: false (normal sweep without avalanche)"
@echo " btbt=true|false - Toggle band-to-band tunneling (BTBT) model"
@echo " Default: false (normal sweep without BTBT)"
@echo " refine=true|false - Toggle dynamic adaptive refinement during sweep"
@echo " Default: true (enable grid splitting at milestones)"
@echo " refine_v_step=<voltage> - Set voltage interval (V) to trigger dynamic refinement"
@@ -66,11 +77,10 @@ help-detail:
@echo " Default: automatically searches for latest checkpoints"
@echo ""
@echo "Usage Examples:"
@echo " make sweep avalanche=true"
@echo " make sweep dev=Triac_rp out=output_260625_01 pcad=true"
@echo " make sweep dev=Triac_rp 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 temp=350.0"
@echo " make resume checkpoint=devices/Triac_rp/output_260625_01/seed_500V.pkl temp=350.0"
@echo " make resume-bg avalanche=true refine_v_step=30.0 temp=350.0"
@echo "============================================================================="
@@ -79,55 +89,59 @@ help-detail:
# 2. 執行基礎網格生成
# 3. 執行 run_refinement_2d.py 讀取基礎網格,求解電場並寫出新的 device_bgmesh.pos
# 4. 再次執行 generate_mesh_2d.py,此時會自動載入 bgmesh 並輸出最終優化網格 device_2d.msh
mesh: device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
@echo ">>> [Mesh] 開始進行自適應網格重構流程..."
rm -f device_bgmesh.pos
$(PYTHON) generate_mesh_2d.py
$(PYTHON) generate_analytical_bgmesh.py
$(PYTHON) generate_mesh_2d.py
@echo ">>> [Mesh] 自適應優化網格生成完畢!(Saved: device_2d.msh)"
mesh: $(DEV_DIR)/device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
@echo ">>> [Mesh] 開始進行自適應網格重構流程 (dev=$(dev))..."
rm -f $(DEV_DIR)/device_bgmesh.pos
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) $(PYTHON) generate_mesh_2d.py
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) $(PYTHON) generate_analytical_bgmesh.py
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) $(PYTHON) generate_mesh_2d.py
@echo ">>> [Mesh] 自適應優化網格生成完畢!(Saved: $(DEV_DIR)/device_2d.msh)"
# --- 熱平衡電位求解 ---
# 依賴於對應的網格與求解腳本
static: device_2d.msh solve_static_2d.py
@echo ">>> [Static] 求解零偏壓熱平衡狀態 (temp=$(temp))..."
TEMP=$(temp) $(PYTHON) solve_static_2d.py
static: $(DEV_DIR)/device_2d.msh solve_static_2d.py
@echo ">>> [Static] 求解零偏壓熱平衡狀態 (dev=$(dev), temp=$(temp))..."
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) 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), temp=$(temp))..."
AVALANCHE=$(avalanche) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) $(PYTHON) solve_sweep_recon.py > sweeping.log 2>&1
sweep: $(DEV_DIR)/device_2d.msh solve_sweep_recon.py
@echo ">>> [Sweep] 開始高壓偏壓漂移-擴散模擬 (dev=$(dev), out=$(out), avalanche=$(avalanche), btbt=$(btbt), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
mkdir -p $(OUT_DIR)
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) AVALANCHE=$(avalanche) BTBT=$(btbt) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) $(PYTHON) -u solve_sweep_recon.py > $(OUT_DIR)/sweeping.log 2>&1
resume:
@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
@echo ">>> [Resume] 從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (dev=$(dev), out=$(out), avalanche=$(avalanche), btbt=$(btbt), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
mkdir -p $(OUT_DIR)
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) AVALANCHE=$(avalanche) BTBT=$(btbt) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) $(PYTHON) -u resume_run.py $(checkpoint) >> $(OUT_DIR)/sweeping.log 2>&1
resume-bg:
@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 &
@echo ">>> [Resume-BG] 在背景從指定的 Checkpoint ($(checkpoint)) 或最新的自動備份接續掃描 (dev=$(dev), out=$(out), avalanche=$(avalanche), btbt=$(btbt), refine=$(refine), refine_v_step=$(refine_v_step), temp=$(temp))..."
mkdir -p $(OUT_DIR)
DEV_DIR=$(DEV_DIR) OUT_DIR=$(OUT_DIR)/ USE_PCAD=$(pcad) AVALANCHE=$(avalanche) BTBT=$(btbt) REFINE=$(refine) REFINE_V_STEP=$(refine_v_step) TEMP=$(temp) nohup $(PYTHON) -u resume_run.py $(checkpoint) >> $(OUT_DIR)/sweeping.log 2>&1 &
# --- 萃取與監控收斂曲線 ---
show-conv:
@if [ -f sweeping.log ]; then \
awk '/Iteration:/ {printf "Iteration %s", $$2} /Device:/ {print $$4}' sweeping.log | tail -n 10; \
@if [ -f $(OUT_DIR)/sweeping.log ]; then \
awk '/Iteration:/ {printf "Iteration %s", $$2} /Device:/ {print $$4}' $(OUT_DIR)/sweeping.log | tail -n 10; \
else \
echo "sweeping.log does not exist."; \
echo "$(OUT_DIR)/sweeping.log does not exist."; \
fi
monitor:
@if [ -f sweeping.log ]; then \
tail -f sweeping.log | awk '/Iteration:/ {printf "Iteration %s", $$2; fflush()} /Device:/ {print $$4; fflush()}'; \
@if [ -f $(OUT_DIR)/sweeping.log ]; then \
tail -f $(OUT_DIR)/sweeping.log | awk '/Iteration:/ {printf "Iteration %s", $$2; fflush()} /Device:/ {print $$4; fflush()}'; \
else \
echo "sweeping.log does not exist."; \
echo "$(OUT_DIR)/sweeping.log does not exist."; \
fi
# --- 網格依賴規則 ---
# 當沒有 device_2d.msh 或 device_config.py 有更動時,自動觸發 mesh 流程
device_2d.msh: device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
$(MAKE) mesh
$(DEV_DIR)/device_2d.msh: $(DEV_DIR)/device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
$(MAKE) mesh dev=$(dev)
clean:
@echo ">>> 清除暫存與網格檔案..."
rm -f *.msh *.pos *.tec *.png *.csv *.vtm *.vtu *.visit
rm -rf __pycache__ physics/__pycache__
+340
View File
@@ -0,0 +1,340 @@
# device_pcad_config.py
# Process-Step Oriented 2D Doping Configuration File
# This file coexists with device_config.py and keeps the original doping path intact.
import os
import sys
import numpy as np
import math
# All units in cm (1 um = 1e-4 cm)
um = 1e-4
# Import geometric and simulation parameters from the original device_config
from device_config import (
W_DEVICE, H_SI, T_OX, H_MOLD, W_SIDE_MOLD, W_SIM,
VIA_WIDTH, VIA_P11_X, VIA_P13_X,
MT1_FP1_X1, MT1_FP1_X2, MT1_FP2_X1, MT1_FP2_X2,
SIM_NAME
)
# Define mask openings (Horizontal coordinate ranges, positive X-axis. Mirroring is handled dynamically)
masks = {
'p_well_mask': [(75.0 * um, 100.0 * um)],
'p12_mask': [(120.0 * um, 130.0 * um)],
'p13_mask': [(150.0 * um, 255.0 * um)],
'nplus_mask': [(164.0 * um, 185.0 * um)],
'mring_mask': [(340.0 * um, W_DEVICE)],
}
# --- 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 database for a given energy in keV. Returns values in cm."""
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
Rp_um = np.interp(e, db['energy'], db['Rp'])
dRp_um = np.interp(e, db['energy'], db['dRp'])
return Rp_um * 1e-4, dRp_um * 1e-4
def get_diffusion_coefficient(dopant, temp_c):
"""Calculate the diffusion coefficient D in cm^2/s at temperature temp_c in Celsius."""
db = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
T_k = temp_c + 273.15
k_B = 8.617333262145e-5 # eV/K
D0 = db['D0']
Ea = db['Ea']
return D0 * np.exp(-Ea / (k_B * T_k))
# Define process steps for the 2D doping profile
process_steps = [
# Step 0: Substrate Wafer
{
'enabled': True,
'name': 'Substrate',
'method': 'Substrate',
'type': 'n',
'doping': 1.0e14, # N_SUB
},
# Step 1: P11 Well Implant & Diffusion
{
'enabled': True,
'name': 'P11_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0, # keV
'dose': 1.77245385091e12, # cm^-2
'mask': 'p_well_mask',
'temp': 1150.0, # Celsius
'time': 465.54227, # minutes
'lateral_ratio': 0.8,
},
# Step 2: P12 Well Implant & Diffusion
{
'enabled': True,
'name': 'P12_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 6.6467019409e11,
'mask': 'p12_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 3: P13 Well Implant & Diffusion
{
'enabled': True,
'name': 'P13_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 1.77245385091e12,
'mask': 'p13_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 4: N+ Source Active Region
{
'enabled': True,
'name': 'NPlus_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'nplus_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
},
# Step 5: MRING Guard Ring Active Region
{
'enabled': True,
'name': 'MRing_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'mring_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
}
]
def resolve_steps():
"""
Resolves the physical process steps into analytical model parameters
(peak, vdiff, hdiff, x_ranges) for profile evaluation.
"""
resolved = []
# 1. Resolve substrate first
sub_step = [s for s in process_steps if s['method'] == 'Substrate'][0]
if sub_step.get('enabled', True):
resolved.append({
'name': sub_step.get('name', 'Substrate'),
'method': 'Substrate',
'type': sub_step['type'],
'doping': sub_step['doping']
})
# 2. Resolve implant/diffusion steps
for step in process_steps:
if step['method'] == 'Substrate' or not step.get('enabled', True):
continue
name = step.get('name')
type_ = step['type']
dopant = step['dopant']
method = step['method']
# Look up mask coordinate ranges
mask_name = step['mask']
x_ranges = masks.get(mask_name, [])
# Get implant properties
energy = step.get('energy', 80.0)
dose = step.get('dose', 1.0e12)
Rp, dRp = get_implant_params(dopant, energy)
# Get thermal diffusion properties
temp = step.get('temp', 1000.0)
time_min = step.get('time', 30.0)
D = get_diffusion_coefficient(dopant, temp)
time_sec = time_min * 60.0
Dt = D * time_sec
# Standard deviation incorporating both implant straggle and thermal budget diffusion
vdiff = math.sqrt(2.0 * (dRp ** 2) + 4.0 * Dt)
# Horizontal standard deviation based on lateral ratio
lateral_ratio = step.get('lateral_ratio', 0.8)
hdiff = vdiff * lateral_ratio
# Calculate peak concentration matching the integrated dose for erfc profile:
# peak = dose / (vdiff * (sqrt(pi) / 2))
peak = dose / (vdiff * (math.sqrt(math.pi) / 2.0))
resolved.append({
'name': name,
'method': 'Implant_and_Diffuse',
'type': type_,
'peak': peak,
'vdiff': vdiff,
'hdiff': hdiff,
'x_ranges': x_ranges
})
return resolved
def apply_pcad_doping_2d(device, region="Silicon"):
"""
Parses the process steps list and registers the corresponding
2D analytical doping models in DEVSIM.
"""
import devsim
donor_terms = []
acceptor_terms = []
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
devsim.node_model(device=device, region=region, name="nD_sub", equation=f"{sub_doping}")
if sub_type == 'n':
donor_terms.append("nD_sub")
else:
acceptor_terms.append("nD_sub")
# 2. Iterate and build expressions for Implant/Diffusion steps
idx = 1
for step in steps:
if step['method'] == 'Substrate':
continue
name = step['name']
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
# Construct analytical 2D erfc expression for each window range, including mirroring
expr_terms = []
for x1, x2 in x_ranges:
# Right side (positive X)
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))")
# Left side (negative X, mirrored: x1 -> -x1 and x2 -> -x2, so window is [-x2, -x1])
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({-x2})) / {hdiff}) - erf((x - ({-x1})) / {hdiff}))")
# Combine terms and multiply by peak concentration
combined_expr = f"{peak} * (" + " + ".join(expr_terms) + ")"
model_name = f"nA_{name}" if type_ == 'p' else f"nD_{name}"
devsim.node_model(device=device, region=region, name=model_name, equation=combined_expr)
if type_ == 'n':
donor_terms.append(model_name)
else:
acceptor_terms.append(model_name)
idx += 1
# 3. Combine separate models into global Donors and Acceptors fields in DEVSIM
donor_equation = " + ".join(donor_terms)
acceptor_equation = "1e10 + " + " + ".join(acceptor_terms)
devsim.node_model(device=device, region=region, name="Donors", equation=donor_equation)
devsim.node_model(device=device, region=region, name="Acceptors", equation=acceptor_equation)
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region=region, name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
def get_pcad_doping_val_2d(x, y):
"""
Evaluates the 2D process-step doping profile analytically on numpy arrays x and y.
"""
import numpy as np
import math
erf_vec = np.vectorize(math.erf)
erfc_vec = np.vectorize(math.erfc)
donors = np.zeros_like(x, dtype=float)
acceptors = np.zeros_like(x, dtype=float)
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
if sub_type == 'n':
donors += sub_doping
else:
acceptors += sub_doping
# 2. Iterate and build expressions for Implant/Diffusion steps
for step in steps:
if step['method'] == 'Substrate':
continue
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
prof = np.zeros_like(x, dtype=float)
for x1, x2 in x_ranges:
# Right side
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
# Left side (mirrored)
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - (-x2)) / hdiff) - erf_vec((x - (-x1)) / hdiff))
if type_ == 'n':
donors += peak * prof
else:
acceptors += peak * prof
# Acceptor base offset 1e10
acceptors += 1e10
return donors - acceptors
+69
View File
@@ -0,0 +1,69 @@
# device_config.py
# All units in cm (1 um = 1e-4 cm)
um = 1e-4
# --- Geometric Dimensions ---
W_DEVICE = 400.0 * um # Half-width of the device (400 x 2 total width)
H_SI = 200.0 * um # Silicon substrate thickness
T_OX = 2.0 * um # Oxide thickness
H_MOLD = 20.0 * um # Molding compound thickness (above oxide)
W_SIDE_MOLD = 20.0 * um # Molding compound width on the sides
W_SIM = W_DEVICE + W_SIDE_MOLD # Half-width of the total simulation domain
# --- P-well parameters (p11, p12, p13) ---
P_WELL_DEPTH = 5.0 * um # 5 um depth for all P-wells
# P-well X boundaries (Right half, will be mirrored for left half)
P11_X1 = 75.0 * um
P11_X2 = 100.0 * um
P12_X1 = 120.0 * um
P12_X2 = 130.0 * um
P13_X1 = 150.0 * um
P13_X2 = 255.0 * um
# --- N+ region parameters ---
NPLUS_DEPTH = 0.1 * um # 1 um depth for all N+ regions
# N+ X boundaries (Right half, mirrored for left half)
NPLUS_X1 = 164.0 * um
NPLUS_X2 = 185.0 * um
# MRING X boundaries (Right half, mirrored for left half)
MRING_X1 = 340.0 * um
MRING_X2 = 356.0 * um
# --- Doping Concentrations (cm^-3) ---
N_SUB = 1.0e14 # 70 ~ 90 ohm cm (5.5e13)
P11_PEAK = 8.0e15
P12_PEAK = 3.0e15
P13_PEAK = 8.0e15
NPLUS_PEAK = 2.0e16
# --- Doping Gradient / Diffusion Widths ---
# P-well gradient widths
P_WELL_VDDIFF = 2.5 * um # Vertical gradient width (characteristic depth)
P_WELL_HDDIFF = 2.0 * um # Horizontal (lateral) gradient width
# N+ gradient widths
NPLUS_VDDIFF = 0.15 * um # Vertical gradient width
NPLUS_HDDIFF = 0.1 * um # Horizontal (lateral) gradient width
# --- Contact Vias Width and Positions (Right half, mirrored for left) ---
VIA_WIDTH = 3.0 * um
# Contact via center positions
VIA_P11_X = 87.5 * um
VIA_P13_X = 174.5 * um
# --- Metal Field Plate X boundaries (Right half, mirrored for left) ---
MT1_FP1_X1 = 30.0 * um
MT1_FP1_X2 = 186.0 * um
MT1_FP2_X1 = 250.0 * um
MT1_FP2_X2 = 295.0 * um
# --- Simulation Metadata ---
SIM_NAME = "symmetric LDMOS 20260625"
+340
View File
@@ -0,0 +1,340 @@
# device_pcad_config.py
# Process-Step Oriented 2D Doping Configuration File
# This file coexists with device_config.py and keeps the original doping path intact.
import os
import sys
import numpy as np
import math
# All units in cm (1 um = 1e-4 cm)
um = 1e-4
# Import geometric and simulation parameters from the original device_config
from device_config import (
W_DEVICE, H_SI, T_OX, H_MOLD, W_SIDE_MOLD, W_SIM,
VIA_WIDTH, VIA_P11_X, VIA_P13_X,
MT1_FP1_X1, MT1_FP1_X2, MT1_FP2_X1, MT1_FP2_X2,
SIM_NAME
)
# Define mask openings (Horizontal coordinate ranges, positive X-axis. Mirroring is handled dynamically)
masks = {
'p_well_mask': [(75.0 * um, 100.0 * um)],
'p12_mask': [(120.0 * um, 130.0 * um)],
'p13_mask': [(150.0 * um, 255.0 * um)],
'nplus_mask': [(164.0 * um, 185.0 * um)],
'mring_mask': [(340.0 * um, W_DEVICE)],
}
# --- 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 database for a given energy in keV. Returns values in cm."""
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
Rp_um = np.interp(e, db['energy'], db['Rp'])
dRp_um = np.interp(e, db['energy'], db['dRp'])
return Rp_um * 1e-4, dRp_um * 1e-4
def get_diffusion_coefficient(dopant, temp_c):
"""Calculate the diffusion coefficient D in cm^2/s at temperature temp_c in Celsius."""
db = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
T_k = temp_c + 273.15
k_B = 8.617333262145e-5 # eV/K
D0 = db['D0']
Ea = db['Ea']
return D0 * np.exp(-Ea / (k_B * T_k))
# Define process steps for the 2D doping profile
process_steps = [
# Step 0: Substrate Wafer
{
'enabled': True,
'name': 'Substrate',
'method': 'Substrate',
'type': 'n',
'doping': 1.0e14, # N_SUB
},
# Step 1: P11 Well Implant & Diffusion
{
'enabled': True,
'name': 'P11_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0, # keV
'dose': 1.77245385091e12, # cm^-2
'mask': 'p_well_mask',
'temp': 1150.0, # Celsius
'time': 465.54227, # minutes
'lateral_ratio': 0.8,
},
# Step 2: P12 Well Implant & Diffusion
{
'enabled': True,
'name': 'P12_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 6.6467019409e11,
'mask': 'p12_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 3: P13 Well Implant & Diffusion
{
'enabled': True,
'name': 'P13_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 1.77245385091e12,
'mask': 'p13_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 4: N+ Source Active Region
{
'enabled': True,
'name': 'NPlus_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'nplus_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
},
# Step 5: MRING Guard Ring Active Region
{
'enabled': True,
'name': 'MRing_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'mring_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
}
]
def resolve_steps():
"""
Resolves the physical process steps into analytical model parameters
(peak, vdiff, hdiff, x_ranges) for profile evaluation.
"""
resolved = []
# 1. Resolve substrate first
sub_step = [s for s in process_steps if s['method'] == 'Substrate'][0]
if sub_step.get('enabled', True):
resolved.append({
'name': sub_step.get('name', 'Substrate'),
'method': 'Substrate',
'type': sub_step['type'],
'doping': sub_step['doping']
})
# 2. Resolve implant/diffusion steps
for step in process_steps:
if step['method'] == 'Substrate' or not step.get('enabled', True):
continue
name = step.get('name')
type_ = step['type']
dopant = step['dopant']
method = step['method']
# Look up mask coordinate ranges
mask_name = step['mask']
x_ranges = masks.get(mask_name, [])
# Get implant properties
energy = step.get('energy', 80.0)
dose = step.get('dose', 1.0e12)
Rp, dRp = get_implant_params(dopant, energy)
# Get thermal diffusion properties
temp = step.get('temp', 1000.0)
time_min = step.get('time', 30.0)
D = get_diffusion_coefficient(dopant, temp)
time_sec = time_min * 60.0
Dt = D * time_sec
# Standard deviation incorporating both implant straggle and thermal budget diffusion
vdiff = math.sqrt(2.0 * (dRp ** 2) + 4.0 * Dt)
# Horizontal standard deviation based on lateral ratio
lateral_ratio = step.get('lateral_ratio', 0.8)
hdiff = vdiff * lateral_ratio
# Calculate peak concentration matching the integrated dose for erfc profile:
# peak = dose / (vdiff * (sqrt(pi) / 2))
peak = dose / (vdiff * (math.sqrt(math.pi) / 2.0))
resolved.append({
'name': name,
'method': 'Implant_and_Diffuse',
'type': type_,
'peak': peak,
'vdiff': vdiff,
'hdiff': hdiff,
'x_ranges': x_ranges
})
return resolved
def apply_pcad_doping_2d(device, region="Silicon"):
"""
Parses the process steps list and registers the corresponding
2D analytical doping models in DEVSIM.
"""
import devsim
donor_terms = []
acceptor_terms = []
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
devsim.node_model(device=device, region=region, name="nD_sub", equation=f"{sub_doping}")
if sub_type == 'n':
donor_terms.append("nD_sub")
else:
acceptor_terms.append("nD_sub")
# 2. Iterate and build expressions for Implant/Diffusion steps
idx = 1
for step in steps:
if step['method'] == 'Substrate':
continue
name = step['name']
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
# Construct analytical 2D erfc expression for each window range, including mirroring
expr_terms = []
for x1, x2 in x_ranges:
# Right side (positive X)
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))")
# Left side (negative X, mirrored: x1 -> -x1 and x2 -> -x2, so window is [-x2, -x1])
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({-x2})) / {hdiff}) - erf((x - ({-x1})) / {hdiff}))")
# Combine terms and multiply by peak concentration
combined_expr = f"{peak} * (" + " + ".join(expr_terms) + ")"
model_name = f"nA_{name}" if type_ == 'p' else f"nD_{name}"
devsim.node_model(device=device, region=region, name=model_name, equation=combined_expr)
if type_ == 'n':
donor_terms.append(model_name)
else:
acceptor_terms.append(model_name)
idx += 1
# 3. Combine separate models into global Donors and Acceptors fields in DEVSIM
donor_equation = " + ".join(donor_terms)
acceptor_equation = "1e10 + " + " + ".join(acceptor_terms)
devsim.node_model(device=device, region=region, name="Donors", equation=donor_equation)
devsim.node_model(device=device, region=region, name="Acceptors", equation=acceptor_equation)
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region=region, name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
def get_pcad_doping_val_2d(x, y):
"""
Evaluates the 2D process-step doping profile analytically on numpy arrays x and y.
"""
import numpy as np
import math
erf_vec = np.vectorize(math.erf)
erfc_vec = np.vectorize(math.erfc)
donors = np.zeros_like(x, dtype=float)
acceptors = np.zeros_like(x, dtype=float)
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
if sub_type == 'n':
donors += sub_doping
else:
acceptors += sub_doping
# 2. Iterate and build expressions for Implant/Diffusion steps
for step in steps:
if step['method'] == 'Substrate':
continue
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
prof = np.zeros_like(x, dtype=float)
for x1, x2 in x_ranges:
# Right side
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
# Left side (mirrored)
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - (-x2)) / hdiff) - erf_vec((x - (-x1)) / hdiff))
if type_ == 'n':
donors += peak * prof
else:
acceptors += peak * prof
# Acceptor base offset 1e10
acceptors += 1e10
return donors - acceptors
+69
View File
@@ -0,0 +1,69 @@
# device_config.py
# All units in cm (1 um = 1e-4 cm)
um = 1e-4
# --- Geometric Dimensions ---
W_DEVICE = 356.0 * um # Half-width of the device (356 x 2 total width)
H_SI = 200.0 * um # Silicon substrate thickness
T_OX = 2.0 * um # Oxide thickness
H_MOLD = 100.0 * um # Molding compound thickness (above oxide)
W_SIDE_MOLD = 100.0 * um # Molding compound width on the sides
W_SIM = W_DEVICE + W_SIDE_MOLD # Half-width of the total simulation domain
# --- P-well parameters (p11, p12, p13) ---
P_WELL_DEPTH = 5.0 * um # 5 um depth for all P-wells
# P-well X boundaries (Right half, will be mirrored for left half)
P11_X1 = 75.0 * um
P11_X2 = 100.0 * um
P12_X1 = 120.0 * um
P12_X2 = 130.0 * um
P13_X1 = 150.0 * um
P13_X2 = 255.0 * um
# --- N+ region parameters ---
NPLUS_DEPTH = 0.1 * um # 1 um depth for all N+ regions
# N+ X boundaries (Right half, mirrored for left half)
NPLUS_X1 = 164.0 * um
NPLUS_X2 = 185.0 * um
# MRING X boundaries (Right half, mirrored for left half)
MRING_X1 = 340.0 * um
MRING_X2 = 356.0 * um
# --- Doping Concentrations (cm^-3) ---
N_SUB = 1.0e14 # 70 ~ 90 ohm cm (5.5e13)
P11_PEAK = 8.0e15
P12_PEAK = 3.0e15
P13_PEAK = 8.0e15
NPLUS_PEAK = 2.0e16
# --- Doping Gradient / Diffusion Widths ---
# P-well gradient widths
P_WELL_VDDIFF = 2.5 * um # Vertical gradient width (characteristic depth)
P_WELL_HDDIFF = 2.0 * um # Horizontal (lateral) gradient width
# N+ gradient widths
NPLUS_VDDIFF = 0.15 * um # Vertical gradient width
NPLUS_HDDIFF = 0.1 * um # Horizontal (lateral) gradient width
# --- Contact Vias Width and Positions (Right half, mirrored for left) ---
VIA_WIDTH = 10.0 * um
# Contact via center positions
VIA_P11_X = 87.5 * um
VIA_P13_X = 174.5 * um
# --- Metal Field Plate X boundaries (Right half, mirrored for left) ---
MT1_FP1_X1 = 30.0 * um
MT1_FP1_X2 = 186.0 * um
MT1_FP2_X1 = 250.0 * um
MT1_FP2_X2 = 295.0 * um
# --- Simulation Metadata ---
SIM_NAME = "p-doping 8&3e15 20260616"
+340
View File
@@ -0,0 +1,340 @@
# device_pcad_config.py
# Process-Step Oriented 2D Doping Configuration File
# This file coexists with device_config.py and keeps the original doping path intact.
import os
import sys
import numpy as np
import math
# All units in cm (1 um = 1e-4 cm)
um = 1e-4
# Import geometric and simulation parameters from the original device_config
from device_config import (
W_DEVICE, H_SI, T_OX, H_MOLD, W_SIDE_MOLD, W_SIM,
VIA_WIDTH, VIA_P11_X, VIA_P13_X,
MT1_FP1_X1, MT1_FP1_X2, MT1_FP2_X1, MT1_FP2_X2,
SIM_NAME
)
# Define mask openings (Horizontal coordinate ranges, positive X-axis. Mirroring is handled dynamically)
masks = {
'p_well_mask': [(75.0 * um, 100.0 * um)],
'p12_mask': [(120.0 * um, 130.0 * um)],
'p13_mask': [(150.0 * um, 255.0 * um)],
'nplus_mask': [(164.0 * um, 185.0 * um)],
'mring_mask': [(340.0 * um, W_DEVICE)],
}
# --- 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 database for a given energy in keV. Returns values in cm."""
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
Rp_um = np.interp(e, db['energy'], db['Rp'])
dRp_um = np.interp(e, db['energy'], db['dRp'])
return Rp_um * 1e-4, dRp_um * 1e-4
def get_diffusion_coefficient(dopant, temp_c):
"""Calculate the diffusion coefficient D in cm^2/s at temperature temp_c in Celsius."""
db = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
T_k = temp_c + 273.15
k_B = 8.617333262145e-5 # eV/K
D0 = db['D0']
Ea = db['Ea']
return D0 * np.exp(-Ea / (k_B * T_k))
# Define process steps for the 2D doping profile
process_steps = [
# Step 0: Substrate Wafer
{
'enabled': True,
'name': 'Substrate',
'method': 'Substrate',
'type': 'n',
'doping': 1.0e14, # N_SUB
},
# Step 1: P11 Well Implant & Diffusion
{
'enabled': True,
'name': 'P11_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0, # keV
'dose': 1.77245385091e12, # cm^-2
'mask': 'p_well_mask',
'temp': 1150.0, # Celsius
'time': 465.54227, # minutes
'lateral_ratio': 0.8,
},
# Step 2: P12 Well Implant & Diffusion
{
'enabled': True,
'name': 'P12_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 6.6467019409e11,
'mask': 'p12_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 3: P13 Well Implant & Diffusion
{
'enabled': True,
'name': 'P13_Well',
'method': 'Implant_and_Diffuse',
'type': 'p',
'dopant': 'Boron',
'energy': 80.0,
'dose': 1.77245385091e12,
'mask': 'p13_mask',
'temp': 1150.0,
'time': 465.54227,
'lateral_ratio': 0.8,
},
# Step 4: N+ Source Active Region
{
'enabled': True,
'name': 'NPlus_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'nplus_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
},
# Step 5: MRING Guard Ring Active Region
{
'enabled': True,
'name': 'MRing_Active',
'method': 'Implant_and_Diffuse',
'type': 'n',
'dopant': 'Phosphorus',
'energy': 50.0,
'dose': 2.65868077636e11,
'mask': 'mring_mask',
'temp': 1000.0,
'time': 34.108586,
'lateral_ratio': 0.666666666667,
}
]
def resolve_steps():
"""
Resolves the physical process steps into analytical model parameters
(peak, vdiff, hdiff, x_ranges) for profile evaluation.
"""
resolved = []
# 1. Resolve substrate first
sub_step = [s for s in process_steps if s['method'] == 'Substrate'][0]
if sub_step.get('enabled', True):
resolved.append({
'name': sub_step.get('name', 'Substrate'),
'method': 'Substrate',
'type': sub_step['type'],
'doping': sub_step['doping']
})
# 2. Resolve implant/diffusion steps
for step in process_steps:
if step['method'] == 'Substrate' or not step.get('enabled', True):
continue
name = step.get('name')
type_ = step['type']
dopant = step['dopant']
method = step['method']
# Look up mask coordinate ranges
mask_name = step['mask']
x_ranges = masks.get(mask_name, [])
# Get implant properties
energy = step.get('energy', 80.0)
dose = step.get('dose', 1.0e12)
Rp, dRp = get_implant_params(dopant, energy)
# Get thermal diffusion properties
temp = step.get('temp', 1000.0)
time_min = step.get('time', 30.0)
D = get_diffusion_coefficient(dopant, temp)
time_sec = time_min * 60.0
Dt = D * time_sec
# Standard deviation incorporating both implant straggle and thermal budget diffusion
vdiff = math.sqrt(2.0 * (dRp ** 2) + 4.0 * Dt)
# Horizontal standard deviation based on lateral ratio
lateral_ratio = step.get('lateral_ratio', 0.8)
hdiff = vdiff * lateral_ratio
# Calculate peak concentration matching the integrated dose for erfc profile:
# peak = dose / (vdiff * (sqrt(pi) / 2))
peak = dose / (vdiff * (math.sqrt(math.pi) / 2.0))
resolved.append({
'name': name,
'method': 'Implant_and_Diffuse',
'type': type_,
'peak': peak,
'vdiff': vdiff,
'hdiff': hdiff,
'x_ranges': x_ranges
})
return resolved
def apply_pcad_doping_2d(device, region="Silicon"):
"""
Parses the process steps list and registers the corresponding
2D analytical doping models in DEVSIM.
"""
import devsim
donor_terms = []
acceptor_terms = []
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
devsim.node_model(device=device, region=region, name="nD_sub", equation=f"{sub_doping}")
if sub_type == 'n':
donor_terms.append("nD_sub")
else:
acceptor_terms.append("nD_sub")
# 2. Iterate and build expressions for Implant/Diffusion steps
idx = 1
for step in steps:
if step['method'] == 'Substrate':
continue
name = step['name']
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
# Construct analytical 2D erfc expression for each window range, including mirroring
expr_terms = []
for x1, x2 in x_ranges:
# Right side (positive X)
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))")
# Left side (negative X, mirrored: x1 -> -x1 and x2 -> -x2, so window is [-x2, -x1])
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({-x2})) / {hdiff}) - erf((x - ({-x1})) / {hdiff}))")
# Combine terms and multiply by peak concentration
combined_expr = f"{peak} * (" + " + ".join(expr_terms) + ")"
model_name = f"nA_{name}" if type_ == 'p' else f"nD_{name}"
devsim.node_model(device=device, region=region, name=model_name, equation=combined_expr)
if type_ == 'n':
donor_terms.append(model_name)
else:
acceptor_terms.append(model_name)
idx += 1
# 3. Combine separate models into global Donors and Acceptors fields in DEVSIM
donor_equation = " + ".join(donor_terms)
acceptor_equation = "1e10 + " + " + ".join(acceptor_terms)
devsim.node_model(device=device, region=region, name="Donors", equation=donor_equation)
devsim.node_model(device=device, region=region, name="Acceptors", equation=acceptor_equation)
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region=region, name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
def get_pcad_doping_val_2d(x, y):
"""
Evaluates the 2D process-step doping profile analytically on numpy arrays x and y.
"""
import numpy as np
import math
erf_vec = np.vectorize(math.erf)
erfc_vec = np.vectorize(math.erfc)
donors = np.zeros_like(x, dtype=float)
acceptors = np.zeros_like(x, dtype=float)
steps = resolve_steps()
# 1. Setup Substrate baseline first
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
sub_doping = sub_step['doping']
sub_type = sub_step['type']
if sub_type == 'n':
donors += sub_doping
else:
acceptors += sub_doping
# 2. Iterate and build expressions for Implant/Diffusion steps
for step in steps:
if step['method'] == 'Substrate':
continue
type_ = step['type']
peak = step['peak']
x_ranges = step['x_ranges']
vdiff = step['vdiff']
hdiff = step['hdiff']
prof = np.zeros_like(x, dtype=float)
for x1, x2 in x_ranges:
# Right side
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
# Left side (mirrored)
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - (-x2)) / hdiff) - erf_vec((x - (-x1)) / hdiff))
if type_ == 'n':
donors += peak * prof
else:
acceptors += peak * prof
# Acceptor base offset 1e10
acceptors += 1e10
return donors - acceptors
+78 -11
View File
@@ -11,7 +11,7 @@ from physics.new_physics import *
import generate_mesh_2d
OUT_DIR = "output_this_run/"
def setup_physics_for_device(device, is_avalanche_enabled=False):
def setup_physics_for_device(device, is_avalanche_enabled=False, is_btbt_enabled=False):
def CreateOxidePotentialOnlyContact(device, region, contact):
contact_bias = GetContactBiasName(contact)
contact_model = f"Potential - {contact_bias}"
@@ -205,16 +205,40 @@ 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'])
# BTBT generation model
if is_btbt_enabled:
CreateBTBTGeneration(device, "Silicon")
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
btbt_model_n = "BTBTGeneration" if is_btbt_enabled else ""
btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else ""
if av_model_n and btbt_model_n:
from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives
CreateEdgeModel(device, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration")
CreateEdgeModel(device, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p")
for i in ("Potential", "Electrons", "Holes"):
CreateEdgeModelDerivatives(device, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i)
CreateEdgeModelDerivatives(device, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p", i)
gen_model_n = "CombinedGeneration"
gen_model_p = "CombinedGeneration_p"
elif av_model_n:
gen_model_n = av_model_n
gen_model_p = av_model_p
elif btbt_model_n:
gen_model_n = btbt_model_n
gen_model_p = btbt_model_p
else:
gen_model_n = ""
gen_model_p = ""
# 預設以 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_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=gen_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)")
@@ -388,7 +412,7 @@ def enforce_contact_boundary_conditions(device_name):
except Exception as ex:
print(f" Error enforcing {reg} contact boundary conditions: {ex}")
def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_log=None, out_dir=None):
def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, is_btbt_enabled=False, time_log=None, out_dir=None):
if out_dir is None:
out_dir = OUT_DIR
import time
@@ -465,7 +489,7 @@ def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_
# 5. 載入新網格並設定物理與 solutions
devsim.create_gmsh_mesh(mesh=device_new_name, file=mesh_out_path)
opts = setup_physics_for_device(device_new_name, is_avalanche_enabled=is_avalanche_enabled)
opts = setup_physics_for_device(device_new_name, is_avalanche_enabled=is_avalanche_enabled, is_btbt_enabled=is_btbt_enabled)
# 6. Apply bias to contacts of the new device
for c in ["MT1_Si", "MT1_P12_Si", "MT1_Ox", "MT1_Mold"]:
@@ -764,14 +788,35 @@ 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 ""
btbt_model_n = "BTBTGeneration" if is_btbt_enabled else ""
btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else ""
if av_model_n and btbt_model_n:
from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives
CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration")
CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p")
for i in ("Potential", "Electrons", "Holes"):
CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i)
CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p", i)
gen_model_n = "CombinedGeneration"
gen_model_p = "CombinedGeneration_p"
elif av_model_n:
gen_model_n = av_model_n
gen_model_p = av_model_p
elif btbt_model_n:
gen_model_n = btbt_model_n
gen_model_p = btbt_model_p
else:
gen_model_n = ""
gen_model_p = ""
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_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=gen_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"
@@ -840,11 +885,33 @@ def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_
# ==========================================
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
btbt_model_n = "BTBTGeneration" if is_btbt_enabled else ""
btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else ""
if av_model_n and btbt_model_n:
from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives
CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration")
CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p")
for i in ("Potential", "Electrons", "Holes"):
CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i)
CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p", i)
gen_model_n = "CombinedGeneration"
gen_model_p = "CombinedGeneration_p"
elif av_model_n:
gen_model_n = av_model_n
gen_model_p = av_model_p
elif btbt_model_n:
gen_model_n = btbt_model_n
gen_model_p = btbt_model_p
else:
gen_model_n = ""
gen_model_p = ""
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_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=gen_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",
@@ -905,7 +972,7 @@ def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, time_
devsim.create_gmsh_mesh(mesh=device_old, file="device_2d.msh")
except Exception:
pass
setup_physics_for_device(device_old, is_avalanche_enabled=is_avalanche_enabled)
setup_physics_for_device(device_old, is_avalanche_enabled=is_avalanche_enabled, is_btbt_enabled=is_btbt_enabled)
raise devsim.error(f"Precision Newton solve on refined mesh did not converge: {e}")
print("Convergence on refined mesh achieved successfully!")
+31 -24
View File
@@ -4,41 +4,48 @@ import math
import sys
import os
sys.path.append("/home/pchan/devsim2026")
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
from device_config import *
# Vectorize math functions for fast numpy operations
erf_vec = np.vectorize(math.erf)
erfc_vec = np.vectorize(math.erfc)
def erfc_doping(x, y, peak, x1, x2, hdiff, vdiff):
return peak * erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import get_pcad_doping_val_2d
def get_doping_val(x, y):
return get_pcad_doping_val_2d(x, y)
else:
def erfc_doping(x, y, peak, x1, x2, hdiff, vdiff):
return peak * erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
def get_doping_val(x, y):
# Donors
nD = N_SUB
nD += erfc_doping(x, y, NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
# Acceptors
nA = 1e10
nA += erfc_doping(x, y, P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
return nD - nA
def get_doping_val(x, y):
# Donors
nD = N_SUB
nD += erfc_doping(x, y, NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nD += erfc_doping(x, y, NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
# Acceptors
nA = 1e10
nA += erfc_doping(x, y, P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
nA += erfc_doping(x, y, P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
return nD - nA
def generate_analytical_bgmesh():
device = "device_2d"
# Load the mesh generated by first pass of generate_mesh_2d.py (coarse base mesh)
print("Loading base mesh: device_2d.msh...")
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_path = os.path.join(DEV_DIR, "device_2d.msh")
print(f"Loading base mesh: {mesh_path}...")
devsim.create_gmsh_mesh(mesh=device, file=mesh_path)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -51,7 +58,7 @@ def generate_analytical_bgmesh():
N_offset = 1.0e10 # Intrinsic concentration offset to avoid division by zero
G_ref = 0.2 / um # Reference relative gradient (0.2 um^-1) for transition smoothness
bgmesh_path = "device_bgmesh.pos"
bgmesh_path = os.path.join(DEV_DIR, "device_bgmesh.pos")
with open(bgmesh_path, "w") as f:
f.write('View "background mesh" {\n')
+7 -1
View File
@@ -1,6 +1,9 @@
import gmsh
import numpy as np
import os
import sys
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
from device_config import *
def create_mesh(y_box_max=12.0*um, y_medium_max=20.0*um, mesh_out="device_2d.msh", bgmesh_pos="device_bgmesh.pos"):
@@ -348,4 +351,7 @@ def create_mesh(y_box_max=12.0*um, y_medium_max=20.0*um, mesh_out="device_2d.msh
print("Mesh generation complete! Saved as device_2d.msh.")
if __name__ == "__main__":
create_mesh()
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
mesh_out = os.path.join(DEV_DIR, "device_2d.msh")
bgmesh_pos = os.path.join(DEV_DIR, "device_bgmesh.pos")
create_mesh(mesh_out=mesh_out, bgmesh_pos=bgmesh_pos)
+138 -35
View File
@@ -17,7 +17,7 @@ if ROOT_DIR not in sys.path:
import subprocess
import pickle
def build_and_solve_1d(bias_target, substrate_type, substrate_doping, length, process_steps, enable_avalanche, area_cm2):
def build_and_solve_1d(bias_target, substrate_type, substrate_doping, length, process_steps, enable_avalanche, enable_btbt, area_cm2):
# Run the simulation in a separate python process to ensure thread-safety and prevent DEVSIM C++ segment faults
temp_dir = os.path.join(ROOT_DIR, "gui1d", "temp_runs")
os.makedirs(temp_dir, exist_ok=True)
@@ -33,6 +33,7 @@ def build_and_solve_1d(bias_target, substrate_type, substrate_doping, length, pr
'length': length,
'process_steps': process_steps,
'enable_avalanche': enable_avalanche,
'enable_btbt': enable_btbt,
'area_cm2': area_cm2
}
@@ -49,11 +50,13 @@ def build_and_solve_1d(bias_target, substrate_type, substrate_doping, length, pr
result = pickle.load(f)
return result
except Exception as e:
except subprocess.CalledProcessError as e:
err_msg = str(e)
if 'res_proc' in locals() and res_proc.stderr:
err_msg += "\n" + res_proc.stderr
if e.stderr:
err_msg += "\n" + e.stderr
raise RuntimeError(f"Simulation process failed: {err_msg}")
except Exception as e:
raise RuntimeError(f"Simulation process failed: {str(e)}")
finally:
for fpath in (in_file, out_file):
if os.path.exists(fpath):
@@ -359,16 +362,21 @@ with st.sidebar:
st.session_state[f"step_{idx}_cs"] = float(step.get('cs', 1e19))
st.session_state[f"step_{idx}_temp"] = float(step.get('temp', 1000.0))
st.session_state[f"step_{idx}_time"] = float(step.get('time', 60.0))
st.session_state[f"step_{idx}_thickness"] = float(step.get('thickness', 10.0))
if 'bias_slider_val' in config:
st.session_state['bias_slider_val'] = float(config['bias_slider_val'])
if 'enable_avalanche_toggle_val' in config:
st.session_state['enable_avalanche_toggle_val'] = bool(config['enable_avalanche_toggle_val'])
if 'enable_btbt_toggle_val' in config:
st.session_state['enable_btbt_toggle_val'] = bool(config['enable_btbt_toggle_val'])
if 'run_full_sweep_toggle_val' in config:
st.session_state['run_full_sweep_toggle_val'] = bool(config['run_full_sweep_toggle_val'])
if 'plot_doping_xmax' in config:
st.session_state['plot_doping_xmax'] = float(config['plot_doping_xmax'])
if 'plot_electro_xmax' in config:
st.session_state['plot_electro_xmax'] = float(config['plot_electro_xmax'])
if 'sweep_v_max' in config:
st.session_state['sweep_v_max'] = float(config['sweep_v_max'])
st.session_state['last_loaded_config_hash'] = file_hash
st.success("Config retrieved successfully!")
@@ -395,15 +403,25 @@ with st.sidebar:
min_value=0.1, max_value=500.0, value=5.0, step=0.5,
help="Electrostatic Profile X-axis maximum range (μm)."
)
sweep_v_max = label_input_row(
"Sweep Max (V)", "number_input", "sweep_v_max",
ratio=[7, 3],
min_value=1.0, max_value=1000.0, value=1000.0, step=10.0,
help="Sweep target voltage (e.g. 1000V for high-voltage, 15V for Zener)."
)
st.markdown("### ⚡ Bias & Physics")
sweep_max_v = float(st.session_state.get('sweep_v_max', 1000.0))
if st.session_state.get('bias_slider_val', 5.0) > sweep_max_v:
st.session_state['bias_slider_val'] = sweep_max_v
bias = label_input_row(
"Bias Vbias (V)", "slider", "bias_slider_val",
ratio=[4.5, 5.5],
min_value=0.0,
max_value=1000.0,
value=5.0,
step=1.0,
max_value=sweep_max_v,
value=min(5.0, sweep_max_v),
step=0.1 if sweep_max_v <= 20.0 else 1.0,
help="Applied at bottom contact (x = L). Top contact (x = 0) is reference 0V."
)
@@ -414,11 +432,18 @@ with st.sidebar:
help="Enable impact ionization for the selected voltage simulation."
)
enable_btbt = label_input_row(
"BTBT (Tunneling)", "toggle", "enable_btbt_toggle_val",
ratio=[6.5, 3.5],
value=False,
help="Enable band-to-band tunneling for the selected voltage simulation."
)
run_full_sweep = label_input_row(
"Run 1000V Sweep", "toggle", "run_full_sweep_toggle_val",
f"Run {sweep_max_v:.0f}V 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."
help=f"Run the full 0~{sweep_max_v:.0f}V sweep with and without avalanche. Turn off for faster parameter tuning."
)
st.markdown("---")
@@ -464,6 +489,7 @@ with st.sidebar:
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
}
@@ -481,6 +507,7 @@ with st.sidebar:
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
})
@@ -539,7 +566,7 @@ with st.sidebar:
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_options = ['Implant', 'Diffusion', 'Epi Growth']
method_idx = method_options.index(step['method']) if step['method'] in method_options else 0
method = label_input_row("Method", "selectbox", f"step_{idx}_method", options=method_options, index=method_idx)
@@ -548,13 +575,21 @@ with st.sidebar:
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
thickness = step.get('thickness', 10.0) # preserve
elif method == 'Diffusion': # Diffusion
surface_options = ['Top', 'Bottom']
surface_idx = surface_options.index(step.get('surface', 'Top')) if step.get('surface', 'Top') in surface_options else 0
surface = label_input_row("Surface Loc.", "selectbox", f"step_{idx}_surface", ratio=[5, 5], options=surface_options, index=surface_idx)
cs = label_input_row("Surface Cs (cm⁻³)", "number_input", f"step_{idx}_cs", ratio=[7, 3], min_value=1e13, max_value=1e22, value=float(step.get('cs', 1e19)), format="%e")
energy = step.get('energy', 80.0) # preserve
dose = step.get('dose', 1e12) # preserve
thickness = step.get('thickness', 10.0) # preserve
else: # Epi Growth
surface = 'Top'
thickness = label_input_row("Thickness (μm)", "number_input", f"step_{idx}_thickness", ratio=[7, 3], min_value=0.1, max_value=200.0, value=float(step.get('thickness', 10.0)), step=0.5)
cs = label_input_row("Doping Conc. (cm⁻³)", "number_input", f"step_{idx}_cs", ratio=[7, 3], min_value=1e10, max_value=1e20, value=float(step.get('cs', 1e15)), format="%e")
energy = step.get('energy', 80.0) # preserve
dose = step.get('dose', 1e12) # preserve
temp = label_input_row("Temp (°C)", "number_input", f"step_{idx}_temp", ratio=[7, 3], min_value=25.0, max_value=1300.0, value=float(step['temp']), step=25.0)
time = label_input_row("Time (min)", "number_input", f"step_{idx}_time", ratio=[7, 3], min_value=0.0, max_value=1000.0, value=float(step['time']), step=5.0)
@@ -568,6 +603,7 @@ with st.sidebar:
'energy': energy,
'dose': dose,
'cs': cs,
'thickness': thickness,
'temp': temp,
'time': time
})
@@ -600,6 +636,7 @@ with st.sidebar:
'energy': 80.0,
'dose': 1e12,
'cs': 1e19,
'thickness': 10.0,
'temp': 1000.0,
'time': 60.0
}
@@ -617,9 +654,11 @@ with st.sidebar:
'process_steps': st.session_state.get('process_steps', []),
'bias_slider_val': st.session_state.get('bias_slider_val', 5.0),
'enable_avalanche_toggle_val': st.session_state.get('enable_avalanche_toggle_val', False),
'enable_btbt_toggle_val': st.session_state.get('enable_btbt_toggle_val', False),
'run_full_sweep_toggle_val': st.session_state.get('run_full_sweep_toggle_val', True),
'plot_doping_xmax': st.session_state.get('plot_doping_xmax', 5.0),
'plot_electro_xmax': st.session_state.get('plot_electro_xmax', 5.0),
'sweep_v_max': st.session_state.get('sweep_v_max', 1000.0),
}
json_str = json.dumps(config_to_save, indent=2)
download_btn_container.download_button(
@@ -643,27 +682,48 @@ st.markdown(
# --- 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']))
sweep_max_v = float(st.session_state.get('sweep_v_max', 1000.0))
doping_key = (substrate_type, n_sub, length, area_cm2, str(st.session_state['process_steps']), sweep_max_v)
# Initialize I-V curve caches if not present
if 'iv_curve_basic' not in st.session_state:
st.session_state['iv_curve_basic'] = ([0.0], [0.0])
if 'iv_curve_with_avalanche' not in st.session_state:
st.session_state['iv_curve_with_avalanche'] = ([0.0], [0.0])
if 'iv_curve_without_avalanche' not in st.session_state:
st.session_state['iv_curve_without_avalanche'] = ([0.0], [0.0])
if 'iv_curve_with_btbt' not in st.session_state:
st.session_state['iv_curve_with_btbt'] = ([0.0], [0.0])
try:
with DEVSIM_LOCK:
if run_full_sweep and ('cached_doping_key' not in st.session_state or st.session_state['cached_doping_key'] != doping_key):
with st.spinner("Calculating high-voltage I-V sweeps (0 ~ 1000V)..."):
# 1. Sweep with avalanche
with st.spinner(f"Calculating I-V sweeps (0 ~ {sweep_max_v:.0f}V)..."):
# 1. Sweep Basic (no avalanche, no btbt)
try:
res_no_av = build_and_solve_1d(
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=False,
enable_btbt=False,
area_cm2=area_cm2
)
v_no_av, j_no_av = res_no_av['v_history'], res_no_av['j_history']
except Exception as e:
v_no_av, j_no_av = [0.0], [0.0]
st.error(f"Basic sweep failed: {e}")
# 2. Sweep with avalanche (no btbt)
try:
res_av = build_and_solve_1d(
bias_target=1000.0,
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=True,
enable_btbt=False,
area_cm2=area_cm2
)
v_av, j_av = res_av['v_history'], res_av['j_history']
@@ -671,28 +731,38 @@ try:
v_av, j_av = [0.0], [0.0]
st.error(f"Avalanche sweep failed to converge: {e}")
# 2. Sweep without avalanche
# 3. Sweep with BTBT (no avalanche)
try:
res_no_av = build_and_solve_1d(
bias_target=1000.0,
res_btbt = build_and_solve_1d(
bias_target=sweep_max_v,
substrate_type=substrate_type,
substrate_doping=n_sub,
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=False,
enable_btbt=True,
area_cm2=area_cm2
)
v_no_av, j_no_av = res_no_av['v_history'], res_no_av['j_history']
v_btbt, j_btbt = res_btbt['v_history'], res_btbt['j_history']
except Exception as e:
v_no_av, j_no_av = [0.0], [0.0]
st.error(f"Without-avalanche sweep failed: {e}")
v_btbt, j_btbt = [0.0], [0.0]
st.error(f"BTBT sweep failed to converge: {e}")
# Store in session state
st.session_state['cached_doping_key'] = doping_key
st.session_state['iv_curve_basic'] = (v_no_av, j_no_av)
st.session_state['iv_curve_with_avalanche'] = (v_av, j_av)
st.session_state['iv_curve_without_avalanche'] = (v_no_av, j_no_av)
st.session_state['iv_curve_with_btbt'] = (v_btbt, j_btbt)
v_pt_val = None
if 'res_no_av' in locals() and isinstance(res_no_av, dict):
v_pt_val = res_no_av.get('v_punchthrough')
if v_pt_val is None and 'res_av' in locals() and isinstance(res_av, dict):
v_pt_val = res_av.get('v_punchthrough')
if v_pt_val is None and 'res_btbt' in locals() and isinstance(res_btbt, dict):
v_pt_val = res_btbt.get('v_punchthrough')
st.session_state['v_punchthrough'] = v_pt_val
# 3. Single-bias simulation for electrostatic plots
# 4. Single-bias simulation for electrostatic plots
with st.spinner(f"Solving electrostatic profiles at Vbias = {bias} V..."):
res = build_and_solve_1d(
bias_target=bias,
@@ -701,8 +771,12 @@ try:
length=length,
process_steps=st.session_state['process_steps'],
enable_avalanche=enable_avalanche,
enable_btbt=enable_btbt,
area_cm2=area_cm2
)
# If full sweep did not run or v_pt was not found, try to grab from single bias solve
if not run_full_sweep or st.session_state.get('v_punchthrough') is None:
st.session_state['v_punchthrough'] = res.get('v_punchthrough')
# Check if target bias was reached
v_actual = res["v_solved"]
@@ -850,7 +924,23 @@ try:
with col_right:
figC = go.Figure()
# 1. Curve with avalanche
# 1. Curve: Basic (Without Avalanche, Without BTBT)
v_basic, j_basic = st.session_state.get('iv_curve_basic', ([0.0], [0.0]))
if len(v_basic) == 1 and v_basic[0] == 0.0:
# Fallback to legacy cached key
v_basic, j_basic = st.session_state.get('iv_curve_without_avalanche', ([0.0], [0.0]))
i_basic_ma = np.abs(np.array(j_basic)) * area_cm2 * 1000.0
i_basic_ma = np.clip(i_basic_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_basic, y=i_basic_ma,
name="Basic (Drift-Diffusion)",
line=dict(color='#00d2ff', width=2, dash='dot'),
mode='lines+markers',
marker=dict(size=4),
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
# 2. Curve: With Avalanche
v_av, j_av = st.session_state.get('iv_curve_with_avalanche', ([0.0], [0.0]))
i_av_ma = np.abs(np.array(j_av)) * area_cm2 * 1000.0
i_av_ma = np.clip(i_av_ma, 1e-12, None)
@@ -862,21 +952,34 @@ try:
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)
# 3. Curve: With BTBT
v_btbt, j_btbt = st.session_state.get('iv_curve_with_btbt', ([0.0], [0.0]))
i_btbt_ma = np.abs(np.array(j_btbt)) * area_cm2 * 1000.0
i_btbt_ma = np.clip(i_btbt_ma, 1e-12, None)
figC.add_trace(go.Scatter(
x=v_no_av, y=i_no_av_ma,
name="Without Avalanche",
line=dict(color='#00d2ff', width=2, dash='dot'),
mode='lines',
x=v_btbt, y=i_btbt_ma,
name="With BTBT",
line=dict(color='#a000ff', width=3),
mode='lines+markers',
hovertemplate='Bias: %{x:.1f} V<br>Current: %{y:.3e} mA'
))
# Add punch-through vertical line if available
v_pt = st.session_state.get('v_punchthrough')
if v_pt is not None:
figC.add_vline(
x=v_pt,
line_width=2,
line_dash="dash",
line_color="#ff4b4b",
annotation_text=f"Punch-through: {v_pt:.1f}V",
annotation_position="top left",
annotation_font=dict(color="#ff4b4b", size=12)
)
figC.update_layout(**plot_layout)
figC.update_layout(
title="📉 High-Voltage I-V Characteristics (0 ~ 1000V)",
title=f"📉 I-V Characteristics (0 ~ {sweep_max_v:.0f}V)",
height=750,
legend=dict(y=-0.12),
xaxis=dict(title="Applied Bias Vbias (V)"),
+37
View File
@@ -0,0 +1,37 @@
{
"sub_type": "n",
"sub_doping": 55000000000000.0,
"sub_length": 100.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 35.0,
"dose": 6000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 60.0
},
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 90.0,
"dose": 300000000000000.0,
"cs": 1e+19,
"temp": 1150.0,
"time": 360.0
}
],
"bias_slider_val": 188.0,
"enable_avalanche_toggle_val": false,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 16.0,
"plot_electro_xmax": 100.0
}
+37
View File
@@ -0,0 +1,37 @@
{
"sub_type": "n",
"sub_doping": 55000000000000.0,
"sub_length": 200.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 35.0,
"dose": 6000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 60.0
},
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 90.0,
"dose": 300000000000000.0,
"cs": 1e+19,
"temp": 1150.0,
"time": 360.0
}
],
"bias_slider_val": 953.0,
"enable_avalanche_toggle_val": false,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 16.0,
"plot_electro_xmax": 200.0
}
+26
View File
@@ -0,0 +1,26 @@
{
"sub_type": "n",
"sub_doping": 2e+18,
"sub_length": 100.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Diffusion",
"surface": "Top",
"energy": 80.0,
"dose": 1000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 30.0
}
],
"bias_slider_val": 5.0,
"enable_avalanche_toggle_val": true,
"enable_btbt_toggle_val": true,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 1.5,
"plot_electro_xmax": 1.5
}
+27
View File
@@ -0,0 +1,27 @@
{
"sub_type": "n",
"sub_doping": 2e+18,
"sub_length": 10.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Diffusion",
"surface": "Top",
"energy": 80.0,
"dose": 1000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 15.0
}
],
"bias_slider_val": 5.0,
"enable_avalanche_toggle_val": true,
"enable_btbt_toggle_val": true,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 1.5,
"plot_electro_xmax": 1.5,
"sweep_v_max": 10.0
}
+37
View File
@@ -0,0 +1,37 @@
{
"sub_type": "n",
"sub_doping": 200000000000000.0,
"sub_length": 200.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 35.0,
"dose": 6000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 60.0
},
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 90.0,
"dose": 300000000000000.0,
"cs": 1e+19,
"temp": 1150.0,
"time": 360.0
}
],
"bias_slider_val": 953.0,
"enable_avalanche_toggle_val": false,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 16.0,
"plot_electro_xmax": 200.0
}
+54
View File
@@ -0,0 +1,54 @@
{
"sub_type": "n",
"sub_doping": 1000000000000000.0,
"sub_length": 50.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "n",
"dopant": "Phosphorus",
"method": "Implant",
"surface": "Top",
"energy": 80.0,
"dose": 5000000000000000.0,
"cs": 1000000000000000.0,
"thickness": 10.0,
"temp": 1150.0,
"time": 180.0
},
{
"enabled": true,
"type": "n",
"dopant": "Arsenic",
"method": "Epi Growth",
"surface": "Top",
"energy": 80.0,
"dose": 1000000000000.0,
"cs": 1000000000000000.0,
"thickness": 10.0,
"temp": 1050.0,
"time": 30.0
},
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 30.0,
"dose": 1000000000000.0,
"cs": 1e+19,
"thickness": 10.0,
"temp": 950.0,
"time": 30.0
}
],
"bias_slider_val": 56.0,
"enable_avalanche_toggle_val": false,
"enable_btbt_toggle_val": false,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 30.0,
"plot_electro_xmax": 30.0,
"sweep_v_max": 1000.0
}
+54
View File
@@ -0,0 +1,54 @@
{
"sub_type": "n",
"sub_doping": 1000000000000000.0,
"sub_length": 50.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "n",
"dopant": "Phosphorus",
"method": "Implant",
"surface": "Top",
"energy": 80.0,
"dose": 5000000000000000.0,
"cs": 1000000000000000.0,
"thickness": 10.0,
"temp": 1150.0,
"time": 60.0
},
{
"enabled": true,
"type": "n",
"dopant": "Arsenic",
"method": "Epi Growth",
"surface": "Top",
"energy": 80.0,
"dose": 1000000000000.0,
"cs": 1000000000000000.0,
"thickness": 10.0,
"temp": 1050.0,
"time": 30.0
},
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 30.0,
"dose": 1000000000000.0,
"cs": 1e+19,
"thickness": 10.0,
"temp": 950.0,
"time": 30.0
}
],
"bias_slider_val": 30.0,
"enable_avalanche_toggle_val": false,
"enable_btbt_toggle_val": false,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 30.0,
"plot_electro_xmax": 30.0,
"sweep_v_max": 1000.0
}
+26
View File
@@ -0,0 +1,26 @@
{
"sub_type": "n",
"sub_doping": 3e+16,
"sub_length": 200.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 35.0,
"dose": 6000000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 60.0
}
],
"bias_slider_val": 953.0,
"enable_avalanche_toggle_val": true,
"enable_btbt_toggle_val": true,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 5.0,
"plot_electro_xmax": 5.0
}
+26
View File
@@ -0,0 +1,26 @@
{
"sub_type": "n",
"sub_doping": 1e+17,
"sub_length": 200.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Diffusion",
"surface": "Top",
"energy": 35.0,
"dose": 6000000000000.0,
"cs": 1e+19,
"temp": 1100.0,
"time": 180.0
}
],
"bias_slider_val": 19.0,
"enable_avalanche_toggle_val": true,
"enable_btbt_toggle_val": true,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 5.0,
"plot_electro_xmax": 5.0
}
+25
View File
@@ -0,0 +1,25 @@
{
"sub_type": "n",
"sub_doping": 55000000000000.0,
"sub_length": 200.0,
"device_area": 0.01,
"process_steps": [
{
"enabled": true,
"type": "p",
"dopant": "Boron",
"method": "Implant",
"surface": "Top",
"energy": 35.0,
"dose": 700000000000.0,
"cs": 1e+19,
"temp": 1000.0,
"time": 60.0
}
],
"bias_slider_val": 953.0,
"enable_avalanche_toggle_val": true,
"run_full_sweep_toggle_val": true,
"plot_doping_xmax": 16.0,
"plot_electro_xmax": 200.0
}
+141 -41
View File
@@ -18,7 +18,8 @@ from physics.new_physics import (
CreateHFMobility,
CreateSiliconDriftDiffusion,
CreateSiliconDriftDiffusionContact,
CreateAvalancheGeneration
CreateAvalancheGeneration,
CreateBTBTGeneration
)
# Vectorized complementary error function
@@ -74,16 +75,22 @@ def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping,
"""
Calculate the separate Donors and Acceptors concentration arrays (cm^-3)
across a position array x_um (in microns), and also return individual step profiles.
length here is total_length.
"""
donors = np.zeros_like(x_um)
acceptors = np.zeros_like(x_um)
step_profiles = []
# Calculate total epi thickness to know where substrate is
epi_thickness = sum(step.get('thickness', 0.0) for step in process_steps if step.get('enabled', True) and step.get('method') == 'Epi Growth')
substrate_thickness = length - epi_thickness
# Initialize substrate baseline
# Substrate occupies [epi_thickness, length]
if substrate_type == 'n':
donors += substrate_doping
donors += np.where(x_um >= epi_thickness, substrate_doping, 0.0)
else:
acceptors += substrate_doping
acceptors += np.where(x_um >= epi_thickness, substrate_doping, 0.0)
# Add background contact doping to ensure Ohmic contact at bottom (x = L)
# Contact doping thickness is 0.5 um, peak is 1e19
@@ -97,7 +104,7 @@ def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping,
step_profiles.append({
'name': 'Substrate (Step 0)',
'type': substrate_type,
'profile': np.full_like(x_um, substrate_doping)
'profile': np.where(x_um >= epi_thickness, substrate_doping, 0.0)
})
# Compute profiles for each process step
@@ -126,6 +133,10 @@ def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping,
# Convert Dt to microns^2
Dt_total_um2 = Dt_total_cm2 * 1e8
# Calculate reference position for the surface at step i.
# It is shifted by all Epi Growth steps that occur AFTER step i.
x_left = sum(sub_step.get('thickness', 0.0) for sub_step in process_steps[i+1:] if sub_step.get('enabled', True) and sub_step.get('method') == 'Epi Growth')
# Calculate profile
if method == 'Implant':
energy = step.get('energy', 80.0)
@@ -135,22 +146,34 @@ def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping,
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))
prof = peak_conc * (np.exp(-((x_um - x_left - Rp) / (np.sqrt(2) * dRp_eff)) ** 2) +
np.exp(-((x_um - x_left + 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
elif method == 'Diffusion': # 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)))
prof = cs * erfc_vec((x_um - x_left) / (2 * np.sqrt(Dt_total_um2)))
else:
prof = cs * erfc_vec((length - x_um) / (2 * np.sqrt(Dt_total_um2)))
else: # Epi Growth
thick_val = step.get('thickness', 10.0)
cs_val = step.get('cs', 1e15)
x_right = x_left + thick_val
if Dt_total_um2 <= 0.0:
Dt_total_um2 = 1e-10
# Slab diffusion formula
prof = 0.5 * cs_val * (erfc_vec((x_um - x_right) / (2 * np.sqrt(Dt_total_um2))) -
erfc_vec((x_um - x_left) / (2 * np.sqrt(Dt_total_um2))))
# Add to Net Donors/Acceptors arrays
if type_ == 'n':
donors += prof
@@ -183,7 +206,6 @@ def find_junction_depths(process_steps, substrate_type, substrate_doping, length
junctions.append(x_cross)
return junctions
def build_and_solve_1d(
bias_target,
substrate_type='n',
@@ -191,12 +213,17 @@ def build_and_solve_1d(
length=30.0,
process_steps=[],
enable_avalanche=False,
enable_btbt=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.
"""
# Calculate total thickness including Epi Growth steps
epi_thickness = sum(step.get('thickness', 0.0) for step in process_steps if step.get('enabled', True) and step.get('method') == 'Epi Growth')
total_length = length + epi_thickness
# 1. Reset DEVSIM state to prevent name collisions
devsim.reset_devsim()
@@ -208,13 +235,20 @@ def build_and_solve_1d(
# 2. Build 1D adaptive mesh
# Find junctions to refine the mesh around them
junctions = find_junction_depths(process_steps, substrate_type, substrate_doping, length)
junctions = find_junction_depths(process_steps, substrate_type, substrate_doping, total_length)
# Define control points (x, target spacing)
# Higher doping -> narrower depletion region -> needs finer mesh (down to 0.5nm)
junction_spacing = max(0.0005, min(0.02, 4e15 / float(substrate_doping)))
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
# Refine a symmetric 100nm region around the junction to ensure
# smooth electric field profiles even when depletion region expands under bias
control_points.append((max(0.0, j - 0.05), junction_spacing))
control_points.append((j, junction_spacing))
control_points.append((min(total_length, j + 0.05), junction_spacing))
control_points.append((total_length, 0.5)) # Coarser mesh near bottom
# Sort control points
control_points.sort(key=lambda x: x[0])
@@ -232,7 +266,7 @@ def build_and_solve_1d(
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))
spacing = max(0.0005, min(2.0, spacing))
curr_x += spacing
if curr_x < x_end - 0.005:
all_x.append(curr_x)
@@ -262,12 +296,15 @@ def build_and_solve_1d(
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)
# Get actual finalized node coordinates from DEVSIM (guarantees length matching)
x_devsim = np.array(devsim.get_node_model_values(device=device, region=region, name="x")) / um
# Calculate separate donors and acceptors on the actual grid coordinates
donors_array, acceptors_array, step_profiles = calc_donors_acceptors(x_devsim, process_steps, substrate_type, substrate_doping, total_length)
# Set Donors directly
devsim.node_solution(device=device, region=region, name="Donors_data")
@@ -321,25 +358,67 @@ def build_and_solve_1d(
# 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)
if enable_btbt:
CreateBTBTGeneration(device, region)
av_model_n = "AvalancheGeneration" if enable_avalanche else ""
av_model_p = "AvalancheGeneration_p" if enable_avalanche else ""
btbt_model_n = "BTBTGeneration" if enable_btbt else ""
btbt_model_p = "BTBTGeneration_p" if enable_btbt else ""
if av_model_n and btbt_model_n:
from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives
CreateEdgeModel(device, region, "CombinedGeneration", "AvalancheGeneration + BTBTGeneration")
CreateEdgeModel(device, region, "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p")
for i in ("Potential", "Electrons", "Holes"):
CreateEdgeModelDerivatives(device, region, "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i)
CreateEdgeModelDerivatives(device, region, "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p", i)
gen_model_n = "CombinedGeneration"
gen_model_p = "CombinedGeneration_p"
elif av_model_n:
gen_model_n = av_model_n
gen_model_p = av_model_p
elif btbt_model_n:
gen_model_n = btbt_model_n
gen_model_p = btbt_model_p
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)
gen_model_n = ""
gen_model_p = ""
# First set up equations WITHOUT generation terms (basic drift-diffusion)
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)
# Solve 0V basic Drift-Diffusion (highly stable)
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=100)
# Now if generation models are enabled, add them and re-solve at 0V starting from the solved state
if gen_model_n:
devsim.equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=hf_opts['Jn'], edge_volume_model=gen_model_n,
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=gen_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
# Re-solve at 0V to incorporate generation models smoothly
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=100)
# Check 0V depletion for punch-through
electrons_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
holes_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
doping_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
is_depleted_0 = np.abs(electrons_0 - holes_0) < 0.1 * np.abs(doping_0)
dep_edges_0 = x_devsim[is_depleted_0]
if len(dep_edges_0) > 0 and np.min(dep_edges_0) <= 0.05:
v_punchthrough = 0.0
else:
v_punchthrough = None
# Calculate current density at 0V
jn_edge_0 = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
@@ -362,8 +441,8 @@ def build_and_solve_1d(
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:
# For avalanche or BTBT sweep, if current starts to rise, limit step size to 2V
if (enable_avalanche or enable_btbt) 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)
@@ -394,6 +473,16 @@ def build_and_solve_1d(
v_history.append(v_current)
j_history.append(j_val)
# Check for punch-through in sweep
if v_punchthrough is None:
electrons_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
holes_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
doping_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
is_depleted_sweep = np.abs(electrons_sweep - holes_sweep) < 0.1 * np.abs(doping_sweep)
dep_edges_sweep = x_devsim[is_depleted_sweep]
if len(dep_edges_sweep) > 0 and np.min(dep_edges_sweep) <= 0.05:
v_punchthrough = v_current
# Check 1mA current limit
current_ma = j_val * area_cm2 * 1000.0
if current_ma > 1.0:
@@ -430,13 +519,22 @@ def build_and_solve_1d(
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
if enable_avalanche or enable_btbt:
q = 1.6e-19
g_total = np.zeros_like(efield_edge)
if enable_avalanche:
try:
g_av = np.array(devsim.get_edge_model_values(device=device, region=region, name="AvalancheGeneration"))
g_total += np.abs(g_av) / q
except Exception:
pass
if enable_btbt:
try:
g_btbt = np.array(devsim.get_edge_model_values(device=device, region=region, name="BTBTGeneration"))
g_total += np.abs(g_btbt) / q
except Exception:
pass
g_av_edge = g_total
# Final current density from right to left
j_avg_rl = -np.mean(jtot_edge)
@@ -476,7 +574,8 @@ def build_and_solve_1d(
# Sweep history
"v_history": v_history,
"j_history": j_history,
"step_profiles": step_profiles
"step_profiles": step_profiles,
"v_punchthrough": v_punchthrough
}
@@ -501,6 +600,7 @@ if __name__ == "__main__":
length=args['length'],
process_steps=args['process_steps'],
enable_avalanche=args['enable_avalanche'],
enable_btbt=args.get('enable_btbt', False),
area_cm2=args['area_cm2']
)
+26 -14
View File
@@ -1,6 +1,8 @@
# 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...")
import numpy as np
print("Running test sweep comparison up to 300V...")
steps = [
{
@@ -17,36 +19,46 @@ steps = [
]
try:
print("Call 1...")
res = build_and_solve_1d(
bias_target=50.0,
print("\n--- Solving case 1: Basic (Drift-Diffusion only) ---")
res_basic = build_and_solve_1d(
bias_target=300.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=True
enable_avalanche=False,
enable_btbt=False
)
print("Call 2...")
res2 = build_and_solve_1d(
bias_target=50.0,
print(f"Basic Solved Bias: {res_basic['v_solved']} V, Current Density: {res_basic['current_density']:.4e} A/cm^2")
print("\n--- Solving case 2: Avalanche-only ---")
res_av = build_and_solve_1d(
bias_target=300.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=False
enable_avalanche=True,
enable_btbt=False
)
print("Call 3...")
res3 = build_and_solve_1d(
bias_target=5.0,
print(f"Avalanche Solved Bias: {res_av['v_solved']} V, Current Density: {res_av['current_density']:.4e} A/cm^2")
print("\n--- Solving case 3: BTBT-only ---")
res_btbt = build_and_solve_1d(
bias_target=300.0,
substrate_type='n',
substrate_doping=1e14,
length=30.0,
process_steps=steps,
enable_avalanche=False
enable_avalanche=False,
enable_btbt=True
)
print("All 3 calls passed successfully!")
print(f"BTBT Solved Bias: {res_btbt['v_solved']} V, Current Density: {res_btbt['current_density']:.4e} A/cm^2")
print("\nAll comparison runs finished successfully!")
except Exception as e:
print("Test failed with error:")
import traceback
traceback.print_exc()
+5 -5
View File
@@ -28,7 +28,7 @@ recon_logic = """
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"
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 ---")
@@ -58,15 +58,15 @@ recon_logic = """
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:
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("recon_avalanche.log", "a") as f:
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("recon_avalanche.log", "a") as f:
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\\tFAILED\\n")
# Restore state and Turn OFF Avalanche
@@ -90,7 +90,7 @@ current_list = [0.0]
# Recon variables
next_recon_v = 50.0
with open("recon_avalanche.log", "w") as f:
with open(f"{OUT_DIR}recon_avalanche.log", "w") as f:
f.write("Voltage(V)\\tAvalancheCurrent(A)\\n")
"""
+31
View File
@@ -527,3 +527,34 @@ def CreateAvalancheGeneration(device, region, Jn, Jp):
CreateEdgeModelDerivatives(device, region, "AvalancheGeneration_p", eq_p, i)
return "AvalancheGeneration"
def CreateBTBTGeneration(device, region):
'''
Band-to-Band Tunneling (BTBT) model using Hurkx formulation.
Generates edge models `BTBTGeneration` and `BTBTGeneration_p` to be used
in continuity equations.
'''
# Set Hurkx parameters
set_parameter(device=device, region=region, name="A_btbt", value=4.0e14) # cm^-1 s^-1 V^-2
set_parameter(device=device, region=region, name="B_btbt", value=1.9e7) # V/cm
# Calculate Electric Field Magnitude if not already created
if not InEdgeModelList(device, region, "E_mag"):
CreateEdgeModel(device, region, "E_mag", "(EField^2 + 1e-4)^0.5")
for i in ("Potential",):
CreateEdgeModelDerivatives(device, region, "E_mag", "(EField^2 + 1e-4)^0.5", i)
# Calculate BTBT Generation rate (multiplied by q, so unit is A/cm^3)
eq_n = "q * A_btbt * (E_mag^2) * exp(-B_btbt / E_mag)"
CreateEdgeModel(device, region, "BTBTGeneration", eq_n)
eq_p = "-q * A_btbt * (E_mag^2) * exp(-B_btbt / E_mag)"
CreateEdgeModel(device, region, "BTBTGeneration_p", eq_p)
for i in ("Potential",):
CreateEdgeModelDerivatives(device, region, "BTBTGeneration", eq_n, i)
CreateEdgeModelDerivatives(device, region, "BTBTGeneration_p", eq_p, i)
return "BTBTGeneration"
+64 -53
View File
@@ -2,12 +2,17 @@ import devsim
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as tri
import os
import sys
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
from device_config import *
device = "device_2d"
# 1. Load the mesh
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -41,58 +46,63 @@ devsim.finalize_mesh(mesh=device)
devsim.create_device(mesh=device, device=device)
# 2. Define Doping Profiles using sub-models to avoid long strings
# Substrate (N-type)
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
# Helper to generate 2D erfc profile string
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
# P-well profiles (p11, p12, p13 on both sides)
# p11
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
# p12
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
# p13
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
# N+ profiles
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
# MRING N+ profiles
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
# Combine into Donors and Acceptors
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
# NetDoping
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="LogAcceptors", equation="log(Acceptors) / log(10.0)")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
devsim.node_model(device=device, region="Silicon", name="LogAcceptors", equation="log(Acceptors) / log(10.0)")
else:
# Substrate (N-type)
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
# Helper to generate 2D erfc profile string
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
# P-well profiles (p11, p12, p13 on both sides)
# p11
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
# p12
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
# p13
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
# N+ profiles
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
# MRING N+ profiles
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
# Combine into Donors and Acceptors
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
# NetDoping
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="LogAcceptors", equation="log(Acceptors) / log(10.0)")
# Write Tecplot output for ParaView
devsim.write_devices(file="device_2d.tec", type="tecplot")
devsim.write_devices(file="preview.tec", type="tecplot")
devsim.write_devices(file=os.path.join(DEV_DIR, "device_2d.tec"), type="tecplot")
devsim.write_devices(file=os.path.join(DEV_DIR, "preview.tec"), type="tecplot")
print("Saved device_2d.tec and preview.tec")
# 4. Generate a 2D Plot with Matplotlib to verify the doping profile
@@ -202,6 +212,7 @@ ax2.set_xlim(-W_SIM / um, W_SIM / um)
ax2.set_ylim(H_SI/um + 15.0, -110.0)
plt.tight_layout()
plt.savefig('doping_2d.png', dpi=300)
png_out = os.path.join(DEV_DIR, "doping_2d.png")
plt.savefig(png_out, dpi=300)
plt.close()
print("Plot saved to doping_2d.png")
print(f"Plot saved to {png_out}")
+67
View File
@@ -1365,4 +1365,71 @@ $$\Delta V < W_{gb} \cdot \sqrt{\frac{2 q \cdot N_{SUB} \cdot V}{\epsilon}}$$
- 這徹底解決了高反向偏壓下雪崩電流倒灌變負、以及在大偏壓網格細化時的發散問題,使 1D 和 2D 雪崩模擬皆能 100% 收斂且符合物理實相。
### 25. 關於二極體與 BJT 穿通機制之物理本質差異,以及未來 Zener 穿隧效應(BTBT)之擴充建議(2026-06-20
在進行 1D Diode 模擬與高壓穿通分析時,我們深入討論了二極體與三極體(BJT)在穿通(Punch-through)狀態下的電學行為與數值模擬特徵,並為未來引入低壓齊納穿隧效應提出了具體的架構性建議。
#### 25.1 穿通(Punch-through)與擊穿(Breakdown)的物理本質差異
1. **二極體 ($P^+ - N - N^+$) 穿通的特徵**
* **物理機制**:當反向偏壓增加時,空乏區延伸至同類載流子接觸區(例如基底 $N^-$ 空乏區觸及底部的 $N^+$ 歐姆區,或 P-well 被吹透至 $x=0$ 的陽極金屬)。此時,元件內部的電場分布會由「三角形」轉變為「梯形」。
* **電流行為**:在無雪崩效應的理想 Drift-Diffusion 框架下,穿通本身**不會**導致漏電流激增。因為理想歐姆接觸的邊界條件強制鎖定了少數載流子濃度(例如 $n(0) \approx 10^3 \text{ cm}^{-3}$),金屬接觸面無法源源不絕地向空乏區注入電子。因此,電流依然只由微弱的熱產生控制,維持在極低且飽和的狀態。這與真實物理中,二極體穿通後需要繼續升高電壓觸發**雪崩擊穿(Avalanche Breakdown**電流才會暴增的現象完全吻合。
2. **BJT ($N^+ - P - N$) 穿通的特徵**
* **物理機制**:在三層結構中,穿通空間意味着中間阻擋層(Base, P 區)被完全空乏,使得發射極(Emitter, $N^+$)與集極(Collector, $N$)之間的勢壘(Barrier)徹底坍塌。
* **電流行為**:由於發射極接觸面為 $N^+$,邊界上存在極高濃度的電子源($n(0) \approx 10^{19} \text{ cm}^{-3}$)。一旦勢壘消除,大量電子會瞬間注入並被電場掃向 Collector,導致 I-V 曲線在穿通電壓處發生**指數級的電流暴增**(即使沒有開啟雪崩效應)。
#### 25.2 目前已實作之物理機制彙整
當前專案中已實作且驗證的物理機制包括:
* **Scharfetter-Gummel 漂移-擴散傳導**(高電場數值穩定)
* **Arora 低場與 Canali 高場飽和遷移率模型**(速度飽和)
* **Shockley-Read-Hall (SRH) 熱複合-產生模型**
* **Chynoweth 碰撞游離雪崩模型**(電荷守恆對稱宣告)
#### 25.3 未來可擴充之機制:Zener 穿隧效應 (BTBT) 建議
當 TVS 元件設計於低壓工作區(如 $V_Z < 5\text{ V}$)時,接面兩側摻雜極高,空乏區極窄,會觸發量子力學的**能帶間穿隧(Band-to-Band Tunneling, BTBT**。目前專案尚未包含此機制,未來可依循以下方案於 `physics/new_physics.py` 中掛載:
1. **宣告 Hurkx 穿隧模型 (Hurkx BTBT Model)**
利用局部電場強度 $E$,定義 BTBT 產生率模型:
$$G_{BTBT} = A \cdot E^2 \cdot \exp\left( - \frac{B}{E} \right)$$
在 Python 中宣告對應的邊緣模型(Edge Model)與對 `Potential` 的 Jacobian 偏微分導數。
2. **將穿隧源項納入連續方程式**
`ElectronContinuityEquation``HoleContinuityEquation` 中,將 `BTBT` 產生率累加至 `edge_volume_model` 之中(與原有的 `AvalancheGeneration` 疊加):
* 電子方程:`edge_volume_model = "AvalancheGeneration + BTBTGeneration"`
* 電洞方程:`edge_volume_model = "AvalancheGeneration_p - BTBTGeneration"` (維持電荷守恆)
這將使元件在低壓重摻雜下能正確模擬出穿隧漏電與穩壓特性,是專案後續向低壓 TVS 元件擴展的關鍵物理拼圖。
### 26. 關於未來 2D 製程配置與元件建模之設計討論(2026-06-25)
為了在將來導入 2D 模擬時,能夠更靈活、更貼近實際製程思維地建立元件摻雜配置(Doping Configuration),我們討論並整理了以下 2D 元件設定的需求作為後續的開發計畫 (To-Do List)。
#### 26.1 2D 元件配置之設計思維與需求
未來在設計 2D `device_config.py` 與對應的圖形介面時,製程建模的前置作業將以 **「2D 幾何與局部開窗」** 的思維來獨立設計,暫不強求與 1D 模擬邏輯進行直接的連動。
具體製程步驟之配置應支援以下三種核心類型:
1. **局部摻雜步驟 (Selective Doping Step: Implant / Diffusion)**
每一步驟皆可包含以下參數:
* **摻雜類型 (Type)**`n``p`
* **方法 (Method)**`Implant` (離子佈植) 或 `Diffusion` (熱擴散)。
* **劑量/濃度 (Dosage / Surface Cs)**:定量摻雜參數。
* **能量 (Energy)**:離子佈植能量(若為 Implant)。
* **熱預算 (Thermal Budget)**:溫度(Temperature)與時間(Time)。
* **局部開窗區域 (Opening Areas)**:指定水平方向的多個開窗區間,例如 `x1 ~ x2``x3 ~ x4` 等,只有在開窗區內才會引入雜質。
2. **局部埋層步驟 (Buried Layer Step)**
用以直接在 Epi 下方或元件特定深處定義局部高濃度埋層。參數應包含:
* **埋層物理參數**:峰值濃度、縱向深度、縱向與橫向擴散長度等。
* **影響區域 (Affected Areas)**:水平方向的作用區間,例如 `x11 ~ x12``x13 ~ x14` 等。
3. **磊晶成長步驟 (Epi Growth Step)**
* **磊晶規格**:厚度、摻雜類型與濃度等。
* 此步驟通常是全域性的,用於覆蓋先前已進行局部摻雜/埋層的表面,並在後續熱處理中做為雜質雙向擴散的介質。
#### 26.2 實現路徑之評估
在 2D 實現此設計時,我們將繼續秉持**不直接在 DEVSIM 中執行網格形變/動態擴散偏微分求解**的原則,以降低實作複雜度。取而代之的是,在靜態的 2D 網格中,利用解析函數(Analytical Functions)結合水平方向的 `erf` 邊緣過渡函數,動態拼接出符合各步驟開窗範圍的 2D 摻雜分佈。
+158 -90
View File
@@ -4,6 +4,10 @@ import sys
import glob
import gc
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, "output_this_run")), "")
# Limit the thread count for parallel solvers to prevent WSL from resource starvation/disconnecting
os.environ["OMP_NUM_THREADS"] = "6"
os.environ["MKL_NUM_THREADS"] = "6"
@@ -22,12 +26,13 @@ import devsim
import numpy as np
is_avalanche_enabled = os.environ.get("AVALANCHE", "false").lower() == "true"
is_btbt_enabled = os.environ.get("BTBT", "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")
print(f"Option: AVALANCHE={is_avalanche_enabled}, BTBT={is_btbt_enabled}, REFINE={is_refine_enabled}, REFINE_V_STEP={refine_v_step}V")
import matplotlib.pyplot as plt
import time
@@ -59,7 +64,7 @@ if specified_checkpoint:
print(f"Error: Specified checkpoint path '{specified_checkpoint}' does not exist.")
else:
# 2. 自動搜尋邏輯(原本的機制)
search_dirs = ["output_this_run/"]
search_dirs = [OUT_DIR]
for d in glob.glob("output_*"):
if os.path.isdir(d):
search_dirs.append(d + "/")
@@ -102,18 +107,18 @@ latest_mtime, latest_voltage, latest_cp, cp_data = valid_checkpoints[0]
cp_dir = os.path.dirname(latest_cp)
if cp_dir:
OUT_DIR = cp_dir + "/"
else:
OUT_DIR = "output_this_run/"
OUT_DIR = os.path.join(cp_dir, "")
print(f"Resuming outputs to directory: {OUT_DIR}")
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"))
mesh_orig = os.path.join(DEV_DIR, "device_2d.msh")
if os.path.exists(mesh_orig):
shutil.copy2(mesh_orig, os.path.join(OUT_DIR, "device_2d.msh"))
config_orig = os.path.join(DEV_DIR, "device_config.py")
if os.path.exists(config_orig):
shutil.copy2(config_orig, 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:
@@ -135,7 +140,7 @@ silicon_state_len = len(cp_data["state"]["Silicon"]["Potential"])
try:
# Load coarse mesh temporarily to count Silicon nodes
devsim.create_gmsh_mesh(mesh="temp_check", file="device_2d.msh")
devsim.create_gmsh_mesh(mesh="temp_check", file=os.path.join(DEV_DIR, "device_2d.msh"))
devsim.add_gmsh_region(mesh="temp_check", gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh="temp_check", gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh="temp_check", gmsh_name="Molding", region="Molding", material="Molding")
@@ -160,7 +165,7 @@ if is_refined:
cp_dir = os.path.dirname(latest_cp)
mesh_candidates = [
os.path.join(cp_dir, "device_2d_refined.msh") if cp_dir else None,
os.path.join("output_this_run", "device_2d_refined.msh")
os.path.join(OUT_DIR, "device_2d_refined.msh")
]
mesh_file = None
for cand in mesh_candidates:
@@ -176,10 +181,10 @@ if is_refined:
raise RuntimeError("Refined checkpoint requires device_2d_refined.msh, but it was not found.")
print(f"Refined checkpoint detected. Loading refined mesh: {mesh_file}")
else:
mesh_file = "device_2d.msh"
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
print(f"Standard checkpoint detected. Loading base mesh: {mesh_file}")
sys.path.append("/home/pchan/devsim2026")
# sys.path already configured at top
from device_config import *
from physics.model_create import *
from physics.new_physics import *
@@ -216,42 +221,46 @@ devsim.finalize_mesh(mesh=device)
devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
@@ -384,17 +393,33 @@ print("Skipping initial Drift-Diffusion solve, preparing to restore from checkpo
# 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 and BTBT generation models
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
CreateBTBTGeneration(device, "Silicon")
av_model_n = "AvalancheGeneration" if is_avalanche_enabled else ""
av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else ""
btbt_model_n = "BTBTGeneration" if is_btbt_enabled else ""
btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else ""
if av_model_n and btbt_model_n:
main_edge_volume_model_n = f"{av_model_n} + {btbt_model_n}"
main_edge_volume_model_p = f"{av_model_p} + {btbt_model_p}"
elif av_model_n:
main_edge_volume_model_n = av_model_n
main_edge_volume_model_p = av_model_p
elif btbt_model_n:
main_edge_volume_model_n = btbt_model_n
main_edge_volume_model_p = btbt_model_p
else:
main_edge_volume_model_n = ""
main_edge_volume_model_p = ""
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=av_model_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=main_edge_volume_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=main_edge_volume_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)
@@ -445,7 +470,7 @@ print(f"Resuming from V = {v_current:.4f} V, step = {step_size:.4f} V")
# 為了讓 log 連續,若 checkpoint 來自其他目錄,將舊日誌複製至當前的 output_this_run/ 中
cp_dir = os.path.dirname(latest_cp)
if cp_dir and cp_dir != OUT_DIR.rstrip("/"):
for log_name in ["simulation_time.log", "recon_avalanche.log"]:
for log_name in ["simulation_time.log", "recon_avalanche.log", "recon_btbt.log", "recon_av_btbt.log"]:
old_log = os.path.join(cp_dir, log_name)
new_log = os.path.join(OUT_DIR, log_name)
if os.path.exists(old_log) and not os.path.exists(new_log):
@@ -464,9 +489,9 @@ for c in ["MT1_Si", "MT1_P12_Si", "MT1_Ox", "MT1_Mold"]:
restore_state(device, state)
# File logging setup (append mode for resume)
time_log = open(f"{OUT_DIR}simulation_time.log", "a", buffering=1)
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
pass # Append mode for recon
for log_name in ["recon_avalanche.log", "recon_btbt.log", "recon_av_btbt.log"]:
with open(f"{OUT_DIR}{log_name}", "a") as f:
pass
start_sweep_time = time.time()
@@ -496,6 +521,9 @@ saved_current_targets = {t for t in TEC_CURRENT_TARGETS if any(abs(i) >= t for i
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}
# Open time log
time_log = open(f"{OUT_DIR}simulation_time.log", "a", buffering=1)
while v_current < v_target:
v_next = min(v_current + step_size, v_target)
@@ -510,13 +538,11 @@ while v_current < v_target:
iters1 = 15
try:
# Always use log_damp preconditioning for Electron/Hole continuity equations in Stage 1
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_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=main_edge_volume_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=main_edge_volume_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", []))
@@ -524,13 +550,11 @@ 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_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_n,
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=main_edge_volume_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_p,
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=main_edge_volume_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
import psutil
@@ -625,7 +649,7 @@ while v_current < v_target:
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)
refined_device, refined_opts = dynamic_refine.refine_and_interpolate(device, v_current, is_avalanche_enabled=is_avalanche_enabled, is_btbt_enabled=is_btbt_enabled, time_log=time_log, out_dir=OUT_DIR)
device = refined_device
opts = refined_opts
just_refined = True
@@ -640,15 +664,19 @@ while v_current < v_target:
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)
# Helper to run a recon probe
def run_recon_probe(probe_name, model_n, model_p, log_file):
print(f" Running Recon Probe ({probe_name})...")
# Temporarily set equations
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)
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=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="AvalancheGeneration_p",
variable_update="log_damp", node_model="HoleGeneration", min_error=1e5)
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
converged = False
total_curr = 0.0
try:
# Stage 1 pre-conditioning
try:
@@ -662,10 +690,10 @@ while v_current < v_target:
# 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",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=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="AvalancheGeneration_p",
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
# Stage 2 solve (Strict Tolerance)
@@ -674,34 +702,40 @@ while v_current < v_target:
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")
total_curr = ia_n_si + ia_p_si + ia_n_p12 + ia_p_p12
converged = True
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")
pass
# Restore state and Reset Avalanche state to main loop configuration
# Restore state immediately after solve attempts
restore_state(device, state)
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_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_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
with open(log_file, "a") as f:
if converged:
print(f" {probe_name} Current at {v_current:.2f} V: {total_curr:.4e} A")
f.write(f"{v_current:.2f}\t{total_curr:.4e}\n")
else:
print(f" {probe_name} failed to converge.")
f.write(f"{v_current:.2f}\tFAILED\n")
# 1. Avalanche Only
run_recon_probe("Avalanche", "AvalancheGeneration", "AvalancheGeneration_p", f"{OUT_DIR}recon_avalanche.log")
# 2. BTBT Only
run_recon_probe("BTBT", "BTBTGeneration", "BTBTGeneration_p", f"{OUT_DIR}recon_btbt.log")
# 3. Avalanche + BTBT
run_recon_probe("Av+BTBT", "AvalancheGeneration + BTBTGeneration", "AvalancheGeneration_p + BTBTGeneration_p", f"{OUT_DIR}recon_av_btbt.log")
# Revert equations to main sweep configuration
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=main_edge_volume_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=main_edge_volume_model_p,
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
else:
print("Avalanche probe skipped (AVALANCHE option is disabled).")
print("--- END RECON PROBE ---\n")
@@ -803,14 +837,48 @@ devsim.write_devices(file=f"{OUT_DIR}sweep_preview_final.tec", type="tecplot")
np.savetxt(f"{OUT_DIR}sweep_iv_2d.csv", np.column_stack((voltage_list, current_list)),
header="Voltage(V),Current(A/cm)", delimiter=",")
# Helper to read recon logs
def load_recon_log(filepath):
voltages = []
currents = []
if os.path.exists(filepath):
with open(filepath, "r") as f:
for line in f:
if "Voltage" in line or not line.strip():
continue
parts = line.strip().split()
if len(parts) == 2 and parts[1] != "FAILED":
try:
v = float(parts[0])
i = float(parts[1])
voltages.append(v)
currents.append(abs(i))
except ValueError:
pass
return voltages, currents
# 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=2, label='Main Sweep (Drift-Diffusion)')
v_av, i_av = load_recon_log(f"{OUT_DIR}recon_avalanche.log")
if v_av:
plt.plot(v_av, i_av, 's--', color='#ff7f0e', markersize=2, label='Avalanche Only (Recon)')
v_bt, i_bt = load_recon_log(f"{OUT_DIR}recon_btbt.log")
if v_bt:
plt.plot(v_bt, i_bt, '^--', color='#2ca02c', markersize=2, label='BTBT Only (Recon)')
v_av_bt, i_av_bt = load_recon_log(f"{OUT_DIR}recon_av_btbt.log")
if v_av_bt:
plt.plot(v_av_bt, i_av_bt, 'd--', color='#d62728', markersize=2, label='Avalanche + BTBT (Recon)')
plt.yscale('log')
plt.grid(True, which="both", ls="--")
plt.xlabel("Bias Voltage (V)")
plt.ylabel("Terminal Current (A/cm, Log Scale)")
plt.title(f"{SIM_NAME} (T={temp_val}K)")
plt.legend()
plt.tight_layout()
plt.savefig(f"{OUT_DIR}sweep_iv_2d.png", dpi=300)
plt.close()
+48 -38
View File
@@ -2,8 +2,10 @@ import devsim
import numpy as np
import matplotlib.pyplot as plt
import os
import os
import sys
sys.path.append("/home/pchan/devsim2026")
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
from device_config import *
from physics.model_create import *
from physics.new_physics import *
@@ -39,41 +41,45 @@ def run_simulation(mesh_file="device_2d.msh", tec_file="static_preview.tec", png
devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
# 3. Solutions and Physics
CreateSolution(device, "Silicon", "Potential")
@@ -229,7 +235,10 @@ def run_simulation(mesh_file="device_2d.msh", tec_file="static_preview.tec", png
def generate_background_mesh():
# 1. Run simulation on current mesh to get Emag
device = run_simulation("device_2d.msh", "static_preview.tec", "static_potential_2d.png", suffix="(Coarse Mesh)")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
tec_file = os.path.join(DEV_DIR, "static_preview.tec")
png_file = os.path.join(DEV_DIR, "static_potential_2d.png")
device = run_simulation(mesh_file, tec_file, png_file, suffix="(Coarse Mesh)")
# 2. Extract elements and Emag
print("Generating background mesh...")
@@ -240,7 +249,8 @@ def generate_background_mesh():
alpha = 1.0e-3 # Scaling coefficient for Emag
# We will write to device_bgmesh.pos
with open("device_bgmesh.pos", "w") as f:
bgmesh_file = os.path.join(DEV_DIR, "device_bgmesh.pos")
with open(bgmesh_file, "w") as f:
f.write('View "background mesh" {\n')
# Write for Silicon, Oxide, Molding regions
+43 -35
View File
@@ -10,7 +10,10 @@ os.environ["OPENBLAS_NUM_THREADS"] = "4"
import devsim
import numpy as np
OUT_DIR = "output_this_run/"
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, "output_this_run")), "")
os.makedirs(OUT_DIR, exist_ok=True)
import matplotlib.pyplot as plt
from device_config import *
@@ -20,7 +23,8 @@ from physics.new_physics import *
device = "device_2d"
# 1. Load the mesh
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -51,46 +55,50 @@ devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
# P-wells
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
# P-wells
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
# N+
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
# N+
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
# MRING
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
# MRING
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
# Combine into Donors and Acceptors
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# Combine into Donors and Acceptors
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# 3. Create solution variables and physics models
CreateSolution(device, "Silicon", "Potential")
+40 -33
View File
@@ -19,7 +19,10 @@ if mkl_libs:
import devsim
import numpy as np
OUT_DIR = "output_this_run/"
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, "output_this_run")), "")
os.makedirs(OUT_DIR, exist_ok=True)
import matplotlib.pyplot as plt
import time
@@ -27,7 +30,6 @@ import time
# Enable Intel MKL PARDISO multi-threaded sparse solver
devsim.set_parameter(name="solver_type", value="pardiso")
sys.path.append("/home/pchan/devsim2026")
from device_config import *
from physics.model_create import *
from physics.new_physics import *
@@ -35,8 +37,9 @@ from physics.new_physics import *
device = "device_2d"
# 1. Load the mesh
print("Loading mesh: device_2d.msh...")
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
print(f"Loading mesh: {mesh_file}...")
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -64,42 +67,46 @@ devsim.finalize_mesh(mesh=device)
devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
+40 -33
View File
@@ -21,7 +21,10 @@ if mkl_libs:
import devsim
import numpy as np
OUT_DIR = "output_this_run/"
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, "output_this_run")), "")
os.makedirs(OUT_DIR, exist_ok=True)
import matplotlib.pyplot as plt
import time
@@ -29,7 +32,6 @@ import time
# Enable Intel MKL PARDISO multi-threaded sparse solver
devsim.set_parameter(name="solver_type", value="pardiso")
sys.path.append("/home/pchan/devsim2026")
from device_config import *
from physics.model_create import *
from physics.new_physics import *
@@ -37,8 +39,9 @@ from physics.new_physics import *
device = "device_2d"
# 1. Load the mesh
print("Loading mesh: device_2d.msh...")
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
print(f"Loading mesh: {mesh_file}...")
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -66,42 +69,46 @@ devsim.finalize_mesh(mesh=device)
devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
+45 -38
View File
@@ -20,7 +20,10 @@ if mkl_libs:
import devsim
import numpy as np
OUT_DIR = "output_this_run/"
DEV_DIR = os.environ.get("DEV_DIR", "devices/Triac_rp")
sys.path.insert(0, os.path.abspath(DEV_DIR))
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, "output_this_run")), "")
os.makedirs(OUT_DIR, exist_ok=True)
import matplotlib.pyplot as plt
import time
@@ -28,7 +31,6 @@ import time
# Enable Intel MKL PARDISO multi-threaded sparse solver
devsim.set_parameter(name="solver_type", value="pardiso")
sys.path.append("/home/pchan/devsim2026")
from device_config import *
from physics.model_create import *
from physics.new_physics import *
@@ -36,8 +38,9 @@ from physics.new_physics import *
device = "device_2d"
# 1. Load the mesh
print("Loading mesh: device_2d.msh...")
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
print(f"Loading mesh: {mesh_file}...")
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
@@ -65,42 +68,46 @@ devsim.finalize_mesh(mesh=device)
devsim.create_device(mesh=device, device=device)
# 2. Set up doping in Silicon region
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
if os.environ.get("USE_PCAD", "false").lower() == "true":
from device_pcad_config import apply_pcad_doping_2d
apply_pcad_doping_2d(device, region="Silicon")
else:
devsim.node_model(device=device, region="Silicon", name="nD_sub", equation=f"{N_SUB}")
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
def get_erfc_expr(peak, x1, x2, hdiff, vdiff):
return f"{peak} * erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))"
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p11_left_expr = get_erfc_expr(P11_PEAK, -P11_X2, -P11_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p11_right_expr = get_erfc_expr(P11_PEAK, P11_X1, P11_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p11_l", equation=p11_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p11_r", equation=p11_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p12_left_expr = get_erfc_expr(P12_PEAK, -P12_X2, -P12_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p12_right_expr = get_erfc_expr(P12_PEAK, P12_X1, P12_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p12_l", equation=p12_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p12_r", equation=p12_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
p13_left_expr = get_erfc_expr(P13_PEAK, -P13_X2, -P13_X1, P_WELL_HDDIFF, P_WELL_VDDIFF)
p13_right_expr = get_erfc_expr(P13_PEAK, P13_X1, P13_X2, P_WELL_HDDIFF, P_WELL_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nA_p13_l", equation=p13_left_expr)
devsim.node_model(device=device, region="Silicon", name="nA_p13_r", equation=p13_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
nplus_left_expr = get_erfc_expr(NPLUS_PEAK, -NPLUS_X2, -NPLUS_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
nplus_right_expr = get_erfc_expr(NPLUS_PEAK, NPLUS_X1, NPLUS_X2, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_l", equation=nplus_left_expr)
devsim.node_model(device=device, region="Silicon", name="nD_nplus_r", equation=nplus_right_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
mring_l_expr = get_erfc_expr(NPLUS_PEAK, -W_DEVICE, -MRING_X1, NPLUS_HDDIFF, NPLUS_VDDIFF)
mring_r_expr = get_erfc_expr(NPLUS_PEAK, MRING_X1, W_DEVICE, NPLUS_HDDIFF, NPLUS_VDDIFF)
devsim.node_model(device=device, region="Silicon", name="nD_mring_l", equation=mring_l_expr)
devsim.node_model(device=device, region="Silicon", name="nD_mring_r", equation=mring_r_expr)
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
devsim.node_model(device=device, region="Silicon", name="Donors",
equation="nD_sub + nD_nplus_l + nD_nplus_r + nD_mring_l + nD_mring_r")
devsim.node_model(device=device, region="Silicon", name="Acceptors",
equation="1e10 + nA_p11_l + nA_p11_r + nA_p12_l + nA_p12_r + nA_p13_l + nA_p13_r")
devsim.node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
devsim.node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
# 3. Initialize electrostatic potential simulation (Poisson only)
CreateSolution(device, "Silicon", "Potential")
@@ -290,7 +297,7 @@ current_list = [0.0]
# Recon variables
next_recon_v = 50.0
with open("recon_avalanche.log", "w") as f:
with open(f"{OUT_DIR}recon_avalanche.log", "w") as f:
f.write("Voltage(V)\tAvalancheCurrent(A)\n")
@@ -383,7 +390,7 @@ while v_current < v_target:
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"
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 ---")
@@ -413,15 +420,15 @@ while v_current < v_target:
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:
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("recon_avalanche.log", "a") as f:
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("recon_avalanche.log", "a") as f:
with open(f"{OUT_DIR}recon_avalanche.log", "a") as f:
f.write(f"{v_current:.2f}\tFAILED\n")
# Restore state and Turn OFF Avalanche