Restructure 2D simulation pipeline to support isolated subdirectory-based device management and output directory relocation
This commit is contained in:
@@ -15,6 +15,7 @@ last_run_outputs/
|
||||
*.last_log
|
||||
devsim-dev/
|
||||
output_*/
|
||||
devices/*/output_*/
|
||||
*.pkl
|
||||
*.txt
|
||||
nohup.out
|
||||
|
||||
@@ -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__
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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!")
|
||||
|
||||
@@ -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
@@ -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
@@ -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)"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user