chore: save local checkpoint for 1000V simulation scripts
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
scratch/
|
||||
last_run_outputs/
|
||||
*.vtu
|
||||
*.vtm
|
||||
*.visit
|
||||
*.tec
|
||||
*.msh
|
||||
*.pos
|
||||
*.png
|
||||
*.log
|
||||
*.csv
|
||||
*.last_log
|
||||
devsim-dev/
|
||||
@@ -0,0 +1,77 @@
|
||||
# =============================================================================
|
||||
# Makefile for TVS/TRIAC DEVSIM Simulation Pipeline
|
||||
# =============================================================================
|
||||
|
||||
PYTHON := .venv/bin/python
|
||||
|
||||
.PHONY: help clean refine static sweep show-conv monitor
|
||||
|
||||
help:
|
||||
@echo "TVS/TRIAC Simulation Pipeline Command List:"
|
||||
@echo " make refine - 依據 device_config.py 中的 doping 與幾何,自動重跑: "
|
||||
@echo " 1. 產生無背景場的基礎網格"
|
||||
@echo " 2. 執行零偏壓 Poisson 模擬生成電場背景網格場 (device_bgmesh.pos)"
|
||||
@echo " 3. 重新呼叫 Gmsh 生成自適應優化網格 (device_2d.msh)"
|
||||
@echo " make static - 載入目前已優化之網格,執行熱平衡 Poisson 模擬並更新 potential 圖面"
|
||||
@echo " make sweep - 載入目前已優化之網格,進行漂移-擴散 (Drift-Diffusion) 高壓掃描"
|
||||
@echo " make clean - 清除所有產生的網格與暫存檔"
|
||||
@echo " make show-conv - 萃取並顯示當前/歷史的相對誤差收斂趨勢 (awk 格式)"
|
||||
@echo " make monitor - 即時監控背景正在跑的 sweep 收斂狀況"
|
||||
|
||||
# --- 網格自適應優化流程 ---
|
||||
# 1. 刪除舊的 bgmesh,以確保 generate_mesh_2d.py 產生的是最乾淨的無背景場基礎網格
|
||||
# 2. 執行基礎網格生成
|
||||
# 3. 執行 run_refinement_2d.py 讀取基礎網格,求解電場並寫出新的 device_bgmesh.pos
|
||||
# 4. 再次執行 generate_mesh_2d.py,此時會自動載入 bgmesh 並輸出最終優化網格 device_2d.msh
|
||||
refine: device_config.py generate_mesh_2d.py generate_analytical_bgmesh.py
|
||||
@echo ">>> [Refine] 開始進行自適應網格重構流程..."
|
||||
rm -f device_bgmesh.pos
|
||||
$(PYTHON) generate_mesh_2d.py
|
||||
$(PYTHON) generate_analytical_bgmesh.py
|
||||
$(PYTHON) generate_mesh_2d.py
|
||||
@echo ">>> [Refine] 自適應優化網格生成完畢!(Saved: device_2d.msh)"
|
||||
|
||||
# --- 熱平衡電位求解 ---
|
||||
# 依賴於對應的網格與求解腳本
|
||||
static: device_2d.msh solve_static_2d.py
|
||||
@echo ">>> [Static] 求解零偏壓熱平衡狀態..."
|
||||
$(PYTHON) solve_static_2d.py
|
||||
|
||||
# --- 高壓偏壓掃描 ---
|
||||
# 依賴於對應的網格與掃描腳本
|
||||
# 注意:若 solve_sweep_2d.py 還不存在,可以手動新增
|
||||
sweep: device_2d.msh solve_sweep_2d.py
|
||||
@echo ">>> [Sweep] 備份上一次的日誌與輸出檔案..."
|
||||
@rm -f sweeping.last_log simulation_time.last_log
|
||||
@-[ -f sweeping.log ] && mv sweeping.log sweeping.last_log || true
|
||||
@-[ -f simulation_time.log ] && mv simulation_time.log simulation_time.last_log || true
|
||||
@mkdir -p last_run_outputs
|
||||
@rm -f last_run_outputs/*
|
||||
@-mv sweep_preview_* sweep_iv_2d.csv sweep_iv_2d.png sweep_potential_2d.png last_run_outputs/ 2>/dev/null || true
|
||||
@echo ">>> [Sweep] 開始高壓偏壓漂移-擴散模擬..."
|
||||
$(PYTHON) solve_sweep_2d.py > 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; \
|
||||
else \
|
||||
echo "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()}'; \
|
||||
else \
|
||||
echo "sweeping.log does not exist."; \
|
||||
fi
|
||||
|
||||
# --- 網格依賴規則 ---
|
||||
# 當沒有 device_2d.msh 或 device_config.py 有更動時,自動觸發 refine 流程
|
||||
device_2d.msh: device_config.py generate_mesh_2d.py run_refinement_2d.py
|
||||
$(MAKE) refine
|
||||
|
||||
clean:
|
||||
@echo ">>> 清除暫存與網格檔案..."
|
||||
rm -f *.msh *.pos *.tec *.png *.csv *.vtm *.vtu *.visit
|
||||
rm -rf __pycache__ physics/__pycache__
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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 = 1.0 * 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.0e16
|
||||
P11_PEAK = 1.0e18
|
||||
P12_PEAK = 1.0e17
|
||||
P13_PEAK = 1.0e18
|
||||
NPLUS_PEAK = 1.0e19
|
||||
|
||||
# --- Doping Gradient / Diffusion Widths ---
|
||||
# P-well gradient widths
|
||||
P_WELL_VDDIFF = 5.0 * um # Vertical gradient width (characteristic depth)
|
||||
P_WELL_HDDIFF = 3.0 * um # Horizontal (lateral) gradient width
|
||||
|
||||
# N+ gradient widths
|
||||
NPLUS_VDDIFF = 1.0 * um # Vertical gradient width
|
||||
NPLUS_HDDIFF = 0.6 * 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
|
||||
@@ -0,0 +1,47 @@
|
||||
# DEVSIM Customization: `min_error` Implementation Walkthrough
|
||||
|
||||
這份文件總結了我們為了將 DEVSIM 核心的載子收斂底限參數 `min_error` 開放給 Python 介面所做的所有工作。我們成功在獨立的 `devsim-dev` 環境中完成了原始碼修改、重新編譯,並在本地端虛擬環境中完成了安裝與驗證。
|
||||
|
||||
## 1. C++ 原始碼修改 (Backend Changes)
|
||||
我們成功實作了 Plan B,也就是完全仿造 `variable_update` 參數的設計模式,將 `min_error` 拉出成為一個選項。
|
||||
|
||||
### 修改了以下三個核心檔案:
|
||||
1. **[EquationHolder.hh](file:///home/pchan/devsim2026/devsim-dev/devsim/src/Equation/EquationHolder.hh)**
|
||||
- 在類別定義中宣告了 `void SetMinError(double);` 的新介面。
|
||||
|
||||
2. **[EquationHolder.cc](file:///home/pchan/devsim2026/devsim-dev/devsim/src/Equation/EquationHolder.cc)**
|
||||
- 實作了該介面,使其在內部呼叫泛型(`double` 或 `float128`)的底層 `equation->setMinError(...)`,以確實改變運算引擎中的收斂底限判定。
|
||||
|
||||
3. **[EquationCommands.cc](file:///home/pchan/devsim2026/devsim-dev/devsim/src/commands/EquationCommands.cc)**
|
||||
- 修改了 `createEquationCmd` 函數,在參數解析清單中註冊了 `"min_error"` 選項。
|
||||
- 設定其預設值為 `"1.0e-10"` 以保持向後相容性。
|
||||
- 提取 Python 端傳入的值,並呼叫 `eh.SetMinError(min_error)`。
|
||||
|
||||
> [!NOTE]
|
||||
> 這些修改使得我們無需更動 `Equation.cc` 內硬編碼的 `defminError = 1.0e-10`,而是以優雅、可擴充的方式從 API 進行覆寫。
|
||||
|
||||
## 2. 環境與編譯挑戰解決 (Build Process)
|
||||
編譯過程中我們遇到了一些環境與依賴挑戰,均順利排除:
|
||||
|
||||
- **子模組拉取**:因為原 DEVSIM repo 使用了相對路徑的 Git Submodules,但在 GitLab 的 Fork 中無法對應到公開庫。我們改由直接從 GitHub 官方抓取 `umfpack_lgpl`、`symdiff`、`superlu` 與 `boostorg` 相關套件。
|
||||
- **SuperLU 標籤相容性**:一開始使用了 `master` 版本的 SuperLU,導致與 DEVSIM 2.0 期待的 API (`dgstrf` 缺少第 12 個引數 `GlobalLU_t *`) 不合。我們迅速將其降版至與 DEVSIM 2.0 完全相容的 **`v5.2.2`**。
|
||||
- **編譯器與 128 位元支援**:為了支援 `-DDEVSIM_EXTENDED_PRECISION=ON`,我們從 `clang` 切換為 `gcc`,並且在 CMake 參數中動態加入了 `QUADMATH_ARCHIVE=-lquadmath` 連結參數,成功讓 `math` 與 `devsim` 模組連結了 Linux 原生的 `libquadmath`。
|
||||
|
||||
## 3. 打包與驗證 (Packaging & Verification)
|
||||
1. **打包 Wheel**:
|
||||
- 修正了 DEVSIM 官方編譯腳本 `build_standalone_wheel.sh` 中未加上引號,導致複製有空白的檔名(`PROJECT GUIDE.md`)會失敗的 Bug。
|
||||
- 順利打包產出了 `devsim-2.10.0-cp39-abi3-linux_x86_64.whl`。
|
||||
|
||||
2. **安裝與執行測試**:
|
||||
- 透過 `pip install --force-reinstall` 將自製的 DEVSIM 安裝進 `/home/pchan/devsim2026/.venv/` 環境中。
|
||||
- 我們撰寫了一支簡單的 Python 測試腳本 `test_min_error.py` 來呼叫包含 `min_error=1.0e-5` 的新 API。
|
||||
|
||||
**測試結果:**
|
||||
```python
|
||||
devsim.equation(device="dev1", region="reg1", name="MyEq", variable_name="MyVar", variable_update="positive", min_error=1.0e-5)
|
||||
```
|
||||
執行後沒有出現任何參數錯誤或崩潰(Crash),順利印出 `Equation with min_error successfully created!`,代表 Python 端與 C++ 端已完美橋接。
|
||||
|
||||
> [!TIP]
|
||||
> 之後您可以開始在 `devsim_bjt_example-main` 中對載子連續性方程式進行測試了!
|
||||
> 當您確認目前修改完全符合需求後,我們就可以隨時將這份修改透過 `git push` 上傳至 GitLab 成為專屬的 `wisetop-custom` 版本!
|
||||
@@ -0,0 +1,125 @@
|
||||
import devsim
|
||||
import numpy as np
|
||||
import math
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append("/home/pchan/devsim2026")
|
||||
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))
|
||||
|
||||
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")
|
||||
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")
|
||||
devsim.finalize_mesh(mesh=device)
|
||||
devsim.create_device(mesh=device, device=device)
|
||||
|
||||
print("Calculating background mesh sizes based on analytical doping profile...")
|
||||
LcMin = 0.15 * um
|
||||
LcMax = 20.0 * um
|
||||
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"
|
||||
with open(bgmesh_path, "w") as f:
|
||||
f.write('View "background mesh" {\n')
|
||||
|
||||
for reg in ["Silicon", "Oxide", "Molding"]:
|
||||
x = np.array(devsim.get_node_model_values(device=device, region=reg, name="x"))
|
||||
y = np.array(devsim.get_node_model_values(device=device, region=reg, name="y"))
|
||||
triangles = np.array(devsim.get_element_node_list(device=device, region=reg))
|
||||
|
||||
if reg == "Silicon":
|
||||
# Evaluate analytical doping at Silicon nodes
|
||||
doping = get_doping_val(x, y)
|
||||
|
||||
for tri in triangles:
|
||||
n0, n1, n2 = tri[0], tri[1], tri[2]
|
||||
x0, y0 = x[n0], y[n0]
|
||||
x1, y1 = x[n1], y[n1]
|
||||
x2, y2 = x[n2], y[n2]
|
||||
|
||||
# Check doping values at the 3 triangle nodes
|
||||
d0, d1, d2 = doping[n0], doping[n1], doping[n2]
|
||||
|
||||
# Triangle center coordinate
|
||||
x_c = (x0 + x1 + x2) / 3.0
|
||||
y_c = (y0 + y1 + y2) / 3.0
|
||||
d_c = get_doping_val(x_c, y_c)
|
||||
|
||||
# Numerical gradient by finite difference (delta = 50nm)
|
||||
delta = 0.05 * um
|
||||
d_cx = get_doping_val(x_c + delta, y_c)
|
||||
d_cy = get_doping_val(x_c, y_c + delta)
|
||||
|
||||
grad_x = (d_cx - d_c) / delta
|
||||
grad_y = (d_cy - d_c) / delta
|
||||
grad_mag = math.sqrt(grad_x**2 + grad_y**2)
|
||||
|
||||
# Relative gradient with intrinsic concentration offset
|
||||
rel_grad = grad_mag / (abs(d_c) + N_offset)
|
||||
|
||||
# Exponential relative gradient mesh refinement
|
||||
lc_val = LcMin + (LcMax - LcMin) * math.exp(-rel_grad / G_ref)
|
||||
|
||||
# Force maximum refinement if the triangle directly crosses the PN junction (doping sign change)
|
||||
if (d0 * d1 < 0.0) or (d1 * d2 < 0.0) or (d2 * d0 < 0.0):
|
||||
lc_val = LcMin
|
||||
|
||||
# Write Gmsh post-processing view format (ST: Scalar Triangle)
|
||||
f.write(f"ST({x0:.8e},{y0:.8e},0,{x1:.8e},{y1:.8e},0,{x2:.8e},{y2:.8e},0){{{lc_val:.8e},{lc_val:.8e},{lc_val:.8e}}};\n")
|
||||
elif reg == "Oxide":
|
||||
# For Oxide region, keep mesh size refined to 0.5 * um to prevent distorted triangles in this thin layer
|
||||
for tri in triangles:
|
||||
n0, n1, n2 = tri[0], tri[1], tri[2]
|
||||
x0, y0 = x[n0], y[n0]
|
||||
x1, y1 = x[n1], y[n1]
|
||||
x2, y2 = x[n2], y[n2]
|
||||
lc_val = 0.5 * um
|
||||
f.write(f"ST({x0:.8e},{y0:.8e},0,{x1:.8e},{y1:.8e},0,{x2:.8e},{y2:.8e},0){{{lc_val:.8e},{lc_val:.8e},{lc_val:.8e}}};\n")
|
||||
else:
|
||||
# For Molding, use LcMax as default (interfaces are refined by curves threshold)
|
||||
for tri in triangles:
|
||||
n0, n1, n2 = tri[0], tri[1], tri[2]
|
||||
x0, y0 = x[n0], y[n0]
|
||||
x1, y1 = x[n1], y[n1]
|
||||
x2, y2 = x[n2], y[n2]
|
||||
lc_val = LcMax
|
||||
f.write(f"ST({x0:.8e},{y0:.8e},0,{x1:.8e},{y1:.8e},0,{x2:.8e},{y2:.8e},0){{{lc_val:.8e},{lc_val:.8e},{lc_val:.8e}}};\n")
|
||||
|
||||
f.write("};\n")
|
||||
print(f"Analytical background mesh successfully written to {bgmesh_path}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_analytical_bgmesh()
|
||||
@@ -0,0 +1,348 @@
|
||||
import gmsh
|
||||
import numpy as np
|
||||
import os
|
||||
from device_config import *
|
||||
|
||||
def create_mesh():
|
||||
gmsh.initialize()
|
||||
gmsh.model.add("device_2d")
|
||||
|
||||
# Use OpenCASCADE kernel
|
||||
occ = gmsh.model.occ
|
||||
|
||||
# 1. Create Silicon substrate: Y in [0, H_SI]
|
||||
silicon = occ.addRectangle(-W_DEVICE, 0, 0, 2 * W_DEVICE, H_SI)
|
||||
|
||||
# 2. Create Oxide layer: Y in [-T_OX, 0]
|
||||
oxide_base = occ.addRectangle(-W_DEVICE, -T_OX, 0, 2 * W_DEVICE, T_OX)
|
||||
|
||||
# Helper to create via rectangles (metal openings)
|
||||
def create_vias(occ_kernel):
|
||||
mring_l = occ_kernel.addRectangle(-W_DEVICE, -T_OX, 0, (W_DEVICE - MRING_X1), T_OX)
|
||||
mt2_v1 = occ_kernel.addRectangle(-VIA_P13_X - 0.5 * VIA_WIDTH, -T_OX, 0, VIA_WIDTH, T_OX)
|
||||
mt2_v3 = occ_kernel.addRectangle(-VIA_P11_X - 0.5 * VIA_WIDTH, -T_OX, 0, VIA_WIDTH, T_OX)
|
||||
mt1_v1 = occ_kernel.addRectangle(VIA_P11_X - 0.5 * VIA_WIDTH, -T_OX, 0, VIA_WIDTH, T_OX)
|
||||
mt1_v3 = occ_kernel.addRectangle(VIA_P13_X - 0.5 * VIA_WIDTH, -T_OX, 0, VIA_WIDTH, T_OX)
|
||||
mring_r = occ_kernel.addRectangle(MRING_X1, -T_OX, 0, (W_DEVICE - MRING_X1), T_OX)
|
||||
return [(2, mring_l), (2, mt2_v1), (2, mt2_v3), (2, mt1_v1), (2, mt1_v3), (2, mring_r)]
|
||||
|
||||
# 3. Subtract vias from oxide to create oxide regions
|
||||
vias_for_oxide = create_vias(occ)
|
||||
oxide_cut_list, _ = occ.cut([(2, oxide_base)], vias_for_oxide)
|
||||
|
||||
# 4. Create Molding layer that covers the entire simulation domain:
|
||||
# X in [-W_SIM, W_SIM], Y in [-T_OX - H_MOLD, H_SI]
|
||||
molding_base = occ.addRectangle(-W_SIM, -T_OX - H_MOLD, 0, 2 * W_SIM, H_SI + T_OX + H_MOLD)
|
||||
|
||||
# Subtract vias from molding_base to ensure vias are not filled with molding compound
|
||||
vias_for_mold = create_vias(occ)
|
||||
molding_cut_list, _ = occ.cut([(2, molding_base)], vias_for_mold)
|
||||
|
||||
# Add dummy points at Y=0 to force fragmentation of Silicon surface for P12 virtual contacts
|
||||
p1 = occ.addPoint(-P12_X2, 0, 0)
|
||||
p2 = occ.addPoint(-P12_X1, 0, 0)
|
||||
p3 = occ.addPoint(P12_X1, 0, 0)
|
||||
p4 = occ.addPoint(P12_X2, 0, 0)
|
||||
dummy_points = [(0, p1), (0, p2), (0, p3), (0, p4)]
|
||||
|
||||
# Add dummy points at Y=-T_OX to force fragmentation of oxide-molding interface for field plates
|
||||
fp_points = []
|
||||
fp_x_list = [
|
||||
-MT1_FP2_X2, -MT1_FP2_X1,
|
||||
-MT1_FP1_X2, -MT1_FP1_X1,
|
||||
MT1_FP1_X1, MT1_FP1_X2,
|
||||
MT1_FP2_X1, MT1_FP2_X2
|
||||
]
|
||||
for i, x_val in enumerate(fp_x_list):
|
||||
pt = occ.addPoint(x_val, -T_OX, 0)
|
||||
fp_points.append((0, pt))
|
||||
|
||||
# Now fragment the silicon substrate, the remaining oxide, and the remaining molding layer, along with dummy points and field plate points
|
||||
out, out_map = occ.fragment([(2, silicon)] + dummy_points + fp_points, oxide_cut_list + molding_cut_list)
|
||||
|
||||
occ.synchronize()
|
||||
|
||||
# Define physical groups for regions
|
||||
silicon_tags = []
|
||||
oxide_tags = []
|
||||
molding_tags = []
|
||||
for ent in gmsh.model.getEntities(dim=2):
|
||||
tag = ent[1]
|
||||
mass_center = occ.getCenterOfMass(2, tag)
|
||||
x_c, y_c = mass_center[0], mass_center[1]
|
||||
|
||||
# Check if it is inside Silicon die boundaries
|
||||
if y_c >= -1e-8 and abs(x_c) <= W_DEVICE + 1e-8:
|
||||
silicon_tags.append(tag)
|
||||
# Check if it is inside Oxide layer boundaries
|
||||
elif y_c < -1e-8 and y_c >= -T_OX - 1e-8 and abs(x_c) <= W_DEVICE + 1e-8:
|
||||
oxide_tags.append(tag)
|
||||
# Otherwise it is molding compound (top or sides)
|
||||
else:
|
||||
molding_tags.append(tag)
|
||||
|
||||
gmsh.model.addPhysicalGroup(2, silicon_tags, tag=1, name="Silicon")
|
||||
gmsh.model.addPhysicalGroup(2, oxide_tags, tag=2, name="Oxide")
|
||||
gmsh.model.addPhysicalGroup(2, molding_tags, tag=3, name="Molding")
|
||||
|
||||
# Bounding box epsilon
|
||||
eps = 0.01 * um
|
||||
|
||||
mt1_si_curves = []
|
||||
mt2_si_curves = []
|
||||
p12_l_si_curves = []
|
||||
p12_r_si_curves = []
|
||||
mring_l_si_curves = []
|
||||
mring_r_si_curves = []
|
||||
|
||||
# Contacts for Oxide
|
||||
mt1_ox_curves = []
|
||||
mt2_ox_curves = []
|
||||
mring_l_ox_curves = []
|
||||
mring_r_ox_curves = []
|
||||
|
||||
# Contacts for Molding
|
||||
mt1_mold_curves = []
|
||||
mt2_mold_curves = []
|
||||
mring_l_mold_curves = []
|
||||
mring_r_mold_curves = []
|
||||
|
||||
silicon_oxide_interface_curves = []
|
||||
|
||||
substrate_bottom_si_curves = []
|
||||
substrate_bottom_mold_curves = []
|
||||
|
||||
silicon_molding_side_curves = []
|
||||
|
||||
ox_mold_interface_curves = []
|
||||
molding_top_curves = []
|
||||
|
||||
def is_in_via_opening(xmin, xmax):
|
||||
via_ranges = [
|
||||
(-VIA_P13_X - 0.5 * VIA_WIDTH, -VIA_P13_X + 0.5 * VIA_WIDTH),
|
||||
(-VIA_P11_X - 0.5 * VIA_WIDTH, -VIA_P11_X + 0.5 * VIA_WIDTH),
|
||||
(VIA_P11_X - 0.5 * VIA_WIDTH, VIA_P11_X + 0.5 * VIA_WIDTH),
|
||||
(VIA_P13_X - 0.5 * VIA_WIDTH, VIA_P13_X + 0.5 * VIA_WIDTH)
|
||||
]
|
||||
for vl, vh in via_ranges:
|
||||
if xmin >= vl - eps and xmax <= vh + eps:
|
||||
return True
|
||||
return False
|
||||
|
||||
curves = gmsh.model.getEntities(dim=1)
|
||||
for c in curves:
|
||||
c_tag = c[1]
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(1, c_tag)
|
||||
|
||||
# Check if it lies on the substrate bottom boundary Y = H_SI
|
||||
if abs(ymin - H_SI) < eps and abs(ymax - H_SI) < eps:
|
||||
if abs(xmin) <= W_DEVICE + eps and abs(xmax) <= W_DEVICE + eps:
|
||||
substrate_bottom_si_curves.append(c_tag)
|
||||
else:
|
||||
substrate_bottom_mold_curves.append(c_tag)
|
||||
continue
|
||||
|
||||
# Check if it lies at Y = 0 (Silicon-Oxide interface or contacts at Y=0)
|
||||
if abs(ymin) < eps and abs(ymax) < eps:
|
||||
# MT2 Left Via contact
|
||||
if xmin >= (-VIA_P13_X - 0.5*VIA_WIDTH) - eps and xmax <= (-VIA_P13_X + 0.5*VIA_WIDTH) + eps:
|
||||
mt2_si_curves.append(c_tag)
|
||||
# MT2 Right Via contact (p11_left)
|
||||
elif xmin >= (-VIA_P11_X - 0.5*VIA_WIDTH) - eps and xmax <= (-VIA_P11_X + 0.5*VIA_WIDTH) + eps:
|
||||
mt2_si_curves.append(c_tag)
|
||||
# MT1 Left Via contact (p11_right)
|
||||
elif xmin >= (VIA_P11_X - 0.5*VIA_WIDTH) - eps and xmax <= (VIA_P11_X + 0.5*VIA_WIDTH) + eps:
|
||||
mt1_si_curves.append(c_tag)
|
||||
# MT1 Right Via contact (p13_right N+)
|
||||
elif xmin >= (VIA_P13_X - 0.5*VIA_WIDTH) - eps and xmax <= (VIA_P13_X + 0.5*VIA_WIDTH) + eps:
|
||||
mt1_si_curves.append(c_tag)
|
||||
# P12 Left virtual contact (connected to MT2)
|
||||
elif xmin >= -P12_X2 - eps and xmax <= -P12_X1 + eps:
|
||||
p12_l_si_curves.append(c_tag)
|
||||
# P12 Right virtual contact (connected to MT1)
|
||||
elif xmin >= P12_X1 - eps and xmax <= P12_X2 + eps:
|
||||
p12_r_si_curves.append(c_tag)
|
||||
# MRING Left contact at Y=0
|
||||
elif xmin >= -W_DEVICE - eps and xmax <= -MRING_X1 + eps:
|
||||
mring_l_si_curves.append(c_tag)
|
||||
# MRING Right contact at Y=0
|
||||
elif xmin >= MRING_X1 - eps and xmax <= W_DEVICE + eps:
|
||||
mring_r_si_curves.append(c_tag)
|
||||
else:
|
||||
silicon_oxide_interface_curves.append(c_tag)
|
||||
continue
|
||||
|
||||
# Check if it lies on the top boundary of Molding: Y = -T_OX - H_MOLD
|
||||
if abs(ymin - (-T_OX - H_MOLD)) < eps and abs(ymax - (-T_OX - H_MOLD)) < eps:
|
||||
molding_top_curves.append(c_tag)
|
||||
continue
|
||||
|
||||
# Check if it lies at Y = -T_OX (oxide-molding interface and field plates)
|
||||
if abs(ymin + T_OX) < eps and abs(ymax + T_OX) < eps:
|
||||
# MT2 field plates: [-MT1_FP2_X2, -MT1_FP2_X1] and [-MT1_FP1_X2, -MT1_FP1_X1]
|
||||
if (xmin >= -MT1_FP2_X2 - eps and xmax <= -MT1_FP2_X1 + eps) or \
|
||||
(xmin >= -MT1_FP1_X2 - eps and xmax <= -MT1_FP1_X1 + eps):
|
||||
mt2_mold_curves.append(c_tag)
|
||||
if not is_in_via_opening(xmin, xmax):
|
||||
mt2_ox_curves.append(c_tag)
|
||||
# MT1 field plates: [MT1_FP1_X1, MT1_FP1_X2] and [MT1_FP2_X1, MT1_FP2_X2]
|
||||
elif (xmin >= MT1_FP1_X1 - eps and xmax <= MT1_FP1_X2 + eps) or \
|
||||
(xmin >= MT1_FP2_X1 - eps and xmax <= MT1_FP2_X2 + eps):
|
||||
mt1_mold_curves.append(c_tag)
|
||||
if not is_in_via_opening(xmin, xmax):
|
||||
mt1_ox_curves.append(c_tag)
|
||||
# MRING Left top: [-W_DEVICE, -MRING_X1]
|
||||
elif xmin >= -W_DEVICE - eps and xmax <= -MRING_X1 + eps:
|
||||
mring_l_mold_curves.append(c_tag)
|
||||
# MRING Right top: [MRING_X1, W_DEVICE]
|
||||
elif xmin >= MRING_X1 - eps and xmax <= W_DEVICE + eps:
|
||||
mring_r_mold_curves.append(c_tag)
|
||||
else:
|
||||
ox_mold_interface_curves.append(c_tag)
|
||||
continue
|
||||
|
||||
# Check for vertical curves: abs(xmin - xmax) < eps
|
||||
if abs(xmin - xmax) < eps:
|
||||
x_coord = (xmin + xmax) / 2.0
|
||||
|
||||
# Check for Silicon-Molding side boundaries: at X = +-W_DEVICE and Y in [0, H_SI]
|
||||
if (abs(x_coord - W_DEVICE) < eps or abs(x_coord - (-W_DEVICE)) < eps) and ymin >= -eps and ymax <= H_SI + eps:
|
||||
silicon_molding_side_curves.append(c_tag)
|
||||
continue
|
||||
|
||||
# Check for vertical sidewalls of the vias (which are metal-oxide interfaces)
|
||||
# These are vertical lines between Y = -T_OX and Y = 0
|
||||
if ymin >= -T_OX - eps and ymax <= eps:
|
||||
# Vias for MT2
|
||||
if (abs(x_coord - (-VIA_P13_X - 0.5*VIA_WIDTH)) < eps or abs(x_coord - (-VIA_P13_X + 0.5*VIA_WIDTH)) < eps or
|
||||
abs(x_coord - (-VIA_P11_X - 0.5*VIA_WIDTH)) < eps or abs(x_coord - (-VIA_P11_X + 0.5*VIA_WIDTH)) < eps):
|
||||
mt2_ox_curves.append(c_tag)
|
||||
# Vias for MT1
|
||||
elif (abs(x_coord - (VIA_P11_X - 0.5*VIA_WIDTH)) < eps or abs(x_coord - (VIA_P11_X + 0.5*VIA_WIDTH)) < eps or
|
||||
abs(x_coord - (VIA_P13_X - 0.5*VIA_WIDTH)) < eps or abs(x_coord - (VIA_P13_X + 0.5*VIA_WIDTH)) < eps):
|
||||
mt1_ox_curves.append(c_tag)
|
||||
# Vias/sidewalls for MRING (Oxide-MRING interface)
|
||||
elif abs(x_coord - (-MRING_X1)) < eps:
|
||||
mring_l_ox_curves.append(c_tag)
|
||||
elif abs(x_coord - (MRING_X1)) < eps:
|
||||
mring_r_ox_curves.append(c_tag)
|
||||
# Outer side of MRING touching Molding (at X = +-W_DEVICE, Y in [-T_OX, 0])
|
||||
elif abs(x_coord - (-W_DEVICE)) < eps:
|
||||
mring_l_mold_curves.append(c_tag)
|
||||
elif abs(x_coord - (W_DEVICE)) < eps:
|
||||
mring_r_mold_curves.append(c_tag)
|
||||
|
||||
# Register the physical groups for boundaries
|
||||
if mt1_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt1_si_curves, name="MT1_Si")
|
||||
if mt2_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt2_si_curves, name="MT2_Si")
|
||||
if p12_l_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, p12_l_si_curves, name="MT2_P12_Si")
|
||||
if p12_r_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, p12_r_si_curves, name="MT1_P12_Si")
|
||||
if mring_l_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_l_si_curves, name="MRING_L_Si")
|
||||
if mring_r_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_r_si_curves, name="MRING_R_Si")
|
||||
|
||||
if mt1_ox_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt1_ox_curves, name="MT1_Ox")
|
||||
if mt1_mold_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt1_mold_curves, name="MT1_Mold")
|
||||
|
||||
if mt2_ox_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt2_ox_curves, name="MT2_Ox")
|
||||
if mt2_mold_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mt2_mold_curves, name="MT2_Mold")
|
||||
|
||||
if mring_l_ox_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_l_ox_curves, name="MRING_L_Ox")
|
||||
if mring_l_mold_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_l_mold_curves, name="MRING_L_Mold")
|
||||
|
||||
if mring_r_ox_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_r_ox_curves, name="MRING_R_Ox")
|
||||
if mring_r_mold_curves:
|
||||
gmsh.model.addPhysicalGroup(1, mring_r_mold_curves, name="MRING_R_Mold")
|
||||
|
||||
if silicon_oxide_interface_curves:
|
||||
gmsh.model.addPhysicalGroup(1, silicon_oxide_interface_curves, name="Si_Ox_Interface")
|
||||
if substrate_bottom_si_curves:
|
||||
gmsh.model.addPhysicalGroup(1, substrate_bottom_si_curves, name="Substrate_Bottom")
|
||||
if substrate_bottom_mold_curves:
|
||||
gmsh.model.addPhysicalGroup(1, substrate_bottom_mold_curves, name="Substrate_Bottom_Mold")
|
||||
|
||||
if silicon_molding_side_curves:
|
||||
gmsh.model.addPhysicalGroup(1, silicon_molding_side_curves, name="Si_Mold_Interface")
|
||||
|
||||
if ox_mold_interface_curves:
|
||||
gmsh.model.addPhysicalGroup(1, ox_mold_interface_curves, name="Ox_Mold_Interface")
|
||||
if molding_top_curves:
|
||||
gmsh.model.addPhysicalGroup(1, molding_top_curves, name="Molding_Top")
|
||||
|
||||
# Set mesh size field for high resolution near all interfaces and electrode edges
|
||||
gmsh.model.mesh.field.add("Distance", 1)
|
||||
target_curves = (silicon_oxide_interface_curves + mt1_si_curves + mt2_si_curves +
|
||||
ox_mold_interface_curves + mt1_ox_curves + mt2_ox_curves +
|
||||
p12_l_si_curves + p12_r_si_curves +
|
||||
mring_l_si_curves + mring_r_si_curves +
|
||||
mring_l_ox_curves + mring_r_ox_curves +
|
||||
mring_l_mold_curves + mring_r_mold_curves)
|
||||
gmsh.model.mesh.field.setNumbers(1, "CurvesList", target_curves)
|
||||
|
||||
gmsh.model.mesh.field.add("Threshold", 2)
|
||||
gmsh.model.mesh.field.setNumber(2, "IField", 1)
|
||||
gmsh.model.mesh.field.setNumber(2, "LcMin", 0.15 * um) # 0.15 um near interfaces
|
||||
gmsh.model.mesh.field.setNumber(2, "LcMax", 20.0 * um) # 20 um far from interfaces
|
||||
gmsh.model.mesh.field.setNumber(2, "DistMin", 0.15 * um) # Concentrated near interfaces
|
||||
gmsh.model.mesh.field.setNumber(2, "DistMax", 1.0 * um) # Coarsen rapidly at 1.0 um
|
||||
|
||||
# Box field to transition background mesh size in the active well region (Y <= 6 um) to 1.5 um
|
||||
gmsh.model.mesh.field.add("Box", 3)
|
||||
gmsh.model.mesh.field.setNumber(3, "VIn", 1.5 * um) # Background surface mesh is 1.5 um (instead of 0.15 um)
|
||||
gmsh.model.mesh.field.setNumber(3, "VOut", 20.0 * um)
|
||||
gmsh.model.mesh.field.setNumber(3, "XMin", -W_DEVICE)
|
||||
gmsh.model.mesh.field.setNumber(3, "XMax", W_DEVICE)
|
||||
gmsh.model.mesh.field.setNumber(3, "YMin", 0.0)
|
||||
gmsh.model.mesh.field.setNumber(3, "YMax", 25.0 * um)
|
||||
|
||||
# Combine threshold field and box field using Min field
|
||||
gmsh.model.mesh.field.add("Min", 4)
|
||||
gmsh.model.mesh.field.setNumbers(4, "FieldsList", [2, 3])
|
||||
|
||||
# Restrict the combined field to only Silicon and Oxide regions
|
||||
restrict_field = gmsh.model.mesh.field.add("Restrict")
|
||||
gmsh.model.mesh.field.setNumbers(restrict_field, "SurfacesList", silicon_tags + oxide_tags)
|
||||
gmsh.model.mesh.field.setNumber(restrict_field, "IField", 4)
|
||||
|
||||
# If background mesh file exists, merge it and combine with restricted field using Min field
|
||||
if os.path.exists("device_bgmesh.pos"):
|
||||
gmsh.merge("device_bgmesh.pos")
|
||||
bgm_field = gmsh.model.mesh.field.add("PostView")
|
||||
gmsh.model.mesh.field.setNumber(bgm_field, "ViewIndex", 0)
|
||||
|
||||
min_field = gmsh.model.mesh.field.add("Min")
|
||||
gmsh.model.mesh.field.setNumbers(min_field, "FieldsList", [restrict_field, bgm_field])
|
||||
gmsh.model.mesh.field.setAsBackgroundMesh(min_field)
|
||||
print("Successfully merged and combined background mesh with restricted field using Min field.")
|
||||
else:
|
||||
gmsh.model.mesh.field.setAsBackgroundMesh(restrict_field)
|
||||
print("Set restricted field as background mesh.")
|
||||
|
||||
# Force MSH 2.2 output format and set global size limits and gradation
|
||||
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
|
||||
gmsh.option.setNumber("Mesh.MeshSizeMin", 0.15 * um)
|
||||
gmsh.option.setNumber("Mesh.MeshSizeMax", 20.0 * um)
|
||||
# Note: Mesh.CharacteristicLengthGradation is unsupported in Gmsh 4.12.1 and throws an exception.
|
||||
# Mesh size gradation is managed via custom fields (Distance and Threshold) in Silicon.
|
||||
|
||||
# Generate 2D mesh
|
||||
gmsh.model.mesh.generate(2)
|
||||
|
||||
gmsh.write("device_2d.msh")
|
||||
gmsh.finalize()
|
||||
print("Mesh generation complete! Saved as device_2d.msh.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_mesh()
|
||||
Submodule
+1
Submodule legend added at 933dde250e
@@ -0,0 +1,183 @@
|
||||
# Copyright 2013 Devsim LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from devsim import *
|
||||
debug = False
|
||||
def CreateSolution(device, region, name):
|
||||
'''
|
||||
Creates solution variables
|
||||
As well as their entries on each edge
|
||||
'''
|
||||
node_solution(name=name, device=device, region=region)
|
||||
edge_from_node_model(node_model=name, device=device, region=region)
|
||||
|
||||
def CreateNodeModel(device, region, model, expression):
|
||||
'''
|
||||
Creates a node model
|
||||
'''
|
||||
result=node_model(device=device, region=region, name=model, equation=expression)
|
||||
if debug:
|
||||
print(("NODEMODEL {d} {r} {m} \"{re}\"".format(d=device, r=region, m=model, re=result)))
|
||||
|
||||
def CreateNodeModelDerivative(device, region, model, expression, *vars):
|
||||
'''
|
||||
Create a node model derivative
|
||||
'''
|
||||
for v in vars:
|
||||
CreateNodeModel(device, region,
|
||||
"{m}:{v}".format(m=model, v=v),
|
||||
"diff({e},{v})".format(e=expression, v=v))
|
||||
#"simplify(diff({e},{v}))".format(e=expression, v=v))
|
||||
|
||||
|
||||
def CreateContactNodeModel(device, contact, model, expression):
|
||||
'''
|
||||
Creates a contact node model
|
||||
'''
|
||||
result=contact_node_model(device=device, contact=contact, name=model, equation=expression)
|
||||
if debug:
|
||||
print(("CONTACTNODEMODEL {d} {c} {m} \"{re}\"".format(d=device, c=contact, m=model, re=result)))
|
||||
|
||||
|
||||
def CreateContactNodeModelDerivative(device, contact, model, expression, variable):
|
||||
'''
|
||||
Creates a contact node model derivative
|
||||
'''
|
||||
CreateContactNodeModel(device, contact,
|
||||
"{m}:{v}".format(m=model, v=variable),
|
||||
"diff({e}, {v})".format(e=expression, v=variable))
|
||||
#"simplify(diff({e}, {v}))".format(e=expression, v=variable))
|
||||
|
||||
def CreateEdgeModel (device, region, model, expression):
|
||||
'''
|
||||
Creates an edge model
|
||||
'''
|
||||
result=edge_model(device=device, region=region, name=model, equation=expression)
|
||||
if debug:
|
||||
print("EDGEMODEL {d} {r} {m} \"{re}\"".format(d=device, r=region, m=model, re=result));
|
||||
|
||||
def CreateEdgeModelDerivatives(device, region, model, expression, variable):
|
||||
'''
|
||||
Creates edge model derivatives
|
||||
'''
|
||||
CreateEdgeModel(device, region,
|
||||
"{m}:{v}@n0".format(m=model, v=variable),
|
||||
"diff({e}, {v}@n0)".format(e=expression, v=variable))
|
||||
#"simplify(diff({e}, {v}@n0))".format(e=expression, v=variable))
|
||||
CreateEdgeModel(device, region,
|
||||
"{m}:{v}@n1".format(m=model, v=variable),
|
||||
"diff({e}, {v}@n1)".format(e=expression, v=variable))
|
||||
#"simplify(diff({e}, {v}@n1))".format(e=expression, v=variable))
|
||||
|
||||
def CreateContactEdgeModel(device, contact, model, expression):
|
||||
'''
|
||||
Creates a contact edge model
|
||||
'''
|
||||
result=contact_edge_model(device=device, contact=contact, name=model, equation=expression)
|
||||
if debug:
|
||||
print(("CONTACTEDGEMODEL {d} {c} {m} \"{re}\"".format(d=device, c=contact, m=model, re=result)))
|
||||
|
||||
def CreateContactEdgeModelDerivative(device, contact, model, expression, variable):
|
||||
'''
|
||||
Creates contact edge model derivatives with respect to variable on node
|
||||
'''
|
||||
CreateContactEdgeModel(device, contact, "{m}:{v}".format(m=model, v=variable), "diff({e}, {v})".format(e=expression, v=variable))
|
||||
#CreateContactEdgeModel(device, contact, "{m}:{v}".format(m=model, v=variable), "simplify(diff({e}, {v}))".format(e=expression, v=variable))
|
||||
|
||||
def CreateInterfaceModel(device, interface, model, expression):
|
||||
'''
|
||||
Creates a interface node model
|
||||
'''
|
||||
result=interface_model(device=device, interface=interface, name=model, equation=expression)
|
||||
if debug:
|
||||
print(("INTERFACEMODEL {d} {i} {m} \"{re}\"".format(d=device, i=interface, m=model, re=result)))
|
||||
|
||||
#def CreateInterfaceModelDerivative(device, interface, model, expression, variable):
|
||||
# '''
|
||||
# Creates interface edge model derivatives with respect to variable on node
|
||||
# '''
|
||||
# CreateInterfaceModel(device, interface, "{m}:{v}".format(m=model, v=variable), "simplify(diff({e}, {v}))".format(e=expression, v=variable))
|
||||
|
||||
def CreateContinuousInterfaceModel(device, interface, variable):
|
||||
mname = "continuous{0}".format(variable)
|
||||
meq = "{0}@r0 - {0}@r1".format(variable)
|
||||
mname0 = "{0}:{1}@r0".format(mname, variable)
|
||||
mname1 = "{0}:{1}@r1".format(mname, variable)
|
||||
CreateInterfaceModel(device, interface, mname, meq)
|
||||
CreateInterfaceModel(device, interface, mname0, "1")
|
||||
CreateInterfaceModel(device, interface, mname1, "-1")
|
||||
return mname
|
||||
|
||||
|
||||
def InEdgeModelList(device, region, model):
|
||||
'''
|
||||
Checks to see if this edge model is available on device and region
|
||||
'''
|
||||
return model in get_edge_model_list(device=device, region=region)
|
||||
|
||||
def InNodeModelList(device, region, model):
|
||||
'''
|
||||
Checks to see if this node model is available on device and region
|
||||
'''
|
||||
return model in get_node_model_list(device=device, region=region)
|
||||
|
||||
#### Make sure that the model exists, as well as it's node model
|
||||
def EnsureEdgeFromNodeModelExists(device, region, nodemodel):
|
||||
'''
|
||||
Checks if the edge models exists
|
||||
'''
|
||||
if not InNodeModelList(device, region, nodemodel):
|
||||
raise "{} must exist"
|
||||
|
||||
emlist = get_edge_model_list(device=device, region=region)
|
||||
emtest = ("{0}@n0".format(nodemodel) and "{0}@n1".format(nodemodel))
|
||||
if not emtest:
|
||||
if debug:
|
||||
print("INFO: Creating ${0}@n0 and ${0}@n1".format(nodemodel))
|
||||
edge_from_node_model(device=device, region=region, node_model=nodemodel)
|
||||
|
||||
def CreateElementModel2d(device, region, model, expression):
|
||||
result=element_model(device=device, region=region, name=model, equation=expression)
|
||||
if debug:
|
||||
print(("ELEMENTMODEL {d} {r} {m} \"{re}\"".format(d=device, r=region, m=model, re=result)))
|
||||
|
||||
|
||||
def CreateElementModelDerivative2d(device, region, model_name, expression, *args):
|
||||
if len(args) == 0:
|
||||
raise ValueError("Must specify a list of variable names")
|
||||
for i in args:
|
||||
for j in ("@en0", "@en1", "@en2"):
|
||||
CreateElementModel2d(device, region, "{0}:{1}{2}".format(model_name, i, j), "diff({0}, {1}{2})".format(expression, i, j))
|
||||
|
||||
### edge_model is the name of the edge model to be created
|
||||
def CreateGeometricMean(device, region, nmodel, emodel):
|
||||
edge_average_model(device=device, region=region, edge_model=emodel, node_model=nmodel, average_type="geometric")
|
||||
|
||||
def CreateGeometricMeanDerivative(device, region, nmodel, emodel, *args):
|
||||
if len(args) == 0:
|
||||
raise ValueError("Must specify a list of variable names")
|
||||
for i in args:
|
||||
edge_average_model(device=device, region=region, edge_model=emodel, node_model=nmodel,
|
||||
derivative=i, average_type="geometric")
|
||||
|
||||
def CreateArithmeticMean(device, region, nmodel, emodel):
|
||||
edge_average_model(device=device, region=region, edge_model=emodel, node_model=nmodel, average_type="arithmetic")
|
||||
|
||||
def CreateArithmeticMeanDerivative(device, region, nmodel, emodel, *args):
|
||||
if len(args) == 0:
|
||||
raise ValueError("Must specify a list of variable names")
|
||||
for i in args:
|
||||
edge_average_model(device=device, region=region, edge_model=emodel, node_model=nmodel,
|
||||
derivative=i, average_type="arithmetic")
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
# Copyright 2013 Devsim LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from devsim import *
|
||||
from .model_create import *
|
||||
|
||||
def SetUniversalParameters(device, region):
|
||||
universal = {
|
||||
'q' : 1.6e-19, #, 'coul'),
|
||||
'k' : 1.3806503e-23, #, 'J/K'),
|
||||
'Permittivity_0' : 8.85e-14 #, 'F/cm^2')
|
||||
}
|
||||
for k, v in universal.items():
|
||||
set_parameter(device=device, region=region, name=k, value=v)
|
||||
|
||||
|
||||
|
||||
def SetSiliconParameters(device, region):
|
||||
'''
|
||||
Sets Silicon device parameters on the specified region.
|
||||
'''
|
||||
|
||||
SetUniversalParameters(device, region)
|
||||
|
||||
##D. B. M. Klaassen, J. W. Slotboom, and H. C. de Graaff, "Unified apparent bandgap narrowing in n- and p-type Silicon," Solid-State Electronics, vol. 35, no. 2, pp. 125-29, 1992.
|
||||
par = {
|
||||
'Permittivity' : 11.1*get_parameter(device=device, region=region, name='Permittivity_0'),
|
||||
'NC300' : 2.8e19, # '1/cm^3'
|
||||
'NV300' : 3.1e19, # '1/cm^3'
|
||||
'EG300' : 1.12, # 'eV'
|
||||
'EGALPH' : 2.73e-4, # 'eV/K'
|
||||
'EGBETA' : 0 , # 'K'
|
||||
'Affinity' : 4.05 , # 'K'
|
||||
# Canali model
|
||||
'BETAN0' : 2.57e-2, # '1'
|
||||
'BETANE' : 0.66, # '1'
|
||||
'BETAP0' : 0.46, # '1'
|
||||
'BETAPE' : 0.17, # '1'
|
||||
'VSATN0' : 1.43e9,
|
||||
'VSATNE' : -0.87,
|
||||
'VSATP0' : 1.62e8,
|
||||
'VSATPE' : -0.52,
|
||||
# Arora model
|
||||
'MUMN' : 88,
|
||||
'MUMEN' : -0.57,
|
||||
'MU0N' : 7.4e8,
|
||||
'MU0EN' : -2.33,
|
||||
'NREFN' : 1.26e17,
|
||||
'NREFNE' : 2.4,
|
||||
'ALPHA0N' : 0.88,
|
||||
'ALPHAEN' : -0.146,
|
||||
'MUMP' : 54.3,
|
||||
'MUMEP' : -0.57,
|
||||
'MU0P' : 1.36e8,
|
||||
'MU0EP' : -2.23,
|
||||
'NREFP' : 2.35e17,
|
||||
'NREFPE' : 2.4,
|
||||
'ALPHA0P' : 0.88,
|
||||
'ALPHAEP' : -0.146,
|
||||
# SRH
|
||||
"taun" : 1e-5,
|
||||
"taup" : 1e-5,
|
||||
"n1" : 1e10,
|
||||
"p1" : 1e10,
|
||||
# TEMP
|
||||
"T" : 300
|
||||
}
|
||||
|
||||
for k, v in par.items():
|
||||
set_parameter(device=device, region=region, name=k, value=v)
|
||||
|
||||
def CreateQuasiFermiLevels(device, region, electron_model, hole_model, variables):
|
||||
'''
|
||||
Creates the models for the quasi-Fermi levels. Assuming Boltzmann statistics.
|
||||
'''
|
||||
eq = (
|
||||
('EFN', 'EC + V_t * log(%s/NC)' % electron_model, ('Potential', 'Electrons')),
|
||||
('EFP', 'EV - V_t * log(%s/NV)' % hole_model, ('Potential', 'Holes')),
|
||||
)
|
||||
for (model, equation, variable_list) in eq:
|
||||
#print "MODEL: " + model + " equation " + equation
|
||||
CreateNodeModel(device, region, model, equation)
|
||||
vset = set(variable_list)
|
||||
for v in variables:
|
||||
if v in vset:
|
||||
CreateNodeModelDerivative(device, region, model, equation, v)
|
||||
|
||||
def CreateDensityOfStates(device, region, variables):
|
||||
'''
|
||||
Set up models for density of states.
|
||||
Neglects Bandgap narrowing.
|
||||
'''
|
||||
eq = (
|
||||
('NC', 'NC300 * (T/300)^1.5', ('T',)),
|
||||
('NV', 'NV300 * (T/300)^1.5', ('T',)),
|
||||
('NTOT', 'Donors + Acceptors', ()),
|
||||
# Band Gap Narrowing
|
||||
('DEG', '0', ()),
|
||||
#('DEG', 'V0.BGN * (log(NTOT/N0.BGN) + ((log(NTOT/N0.BGN)^2 + CON.BGN)^(0.5)))', ()),
|
||||
('EG', 'EG300 + EGALPH*((300^2)/(300+EGBETA) - (T^2)/(T+EGBETA)) - DEG', ('T')),
|
||||
('NIE', '((NC * NV)^0.5) * exp(-EG/(2*V_t))*exp(DEG)', ('T')),
|
||||
('EC', '-Potential - Affinity - DEG/2', ('Potential',)),
|
||||
('EV', 'EC - EG + DEG/2', ('Potential', 'T')),
|
||||
('EI', '0.5 * (EC + EV + V_t*log(NC/NV))', ('Potential', 'T')),
|
||||
)
|
||||
|
||||
for (model, equation, variable_list) in eq:
|
||||
#print "MODEL: " + model + " equation " + equation
|
||||
CreateNodeModel(device, region, model, equation)
|
||||
vset = set(variable_list)
|
||||
for v in variables:
|
||||
if v in vset:
|
||||
CreateNodeModelDerivative(device, region, model, equation, v)
|
||||
|
||||
|
||||
def GetContactBiasName(contact):
|
||||
return "{0}_bias".format(contact)
|
||||
|
||||
def GetContactNodeModelName(contact):
|
||||
return "{0}nodemodel".format(contact)
|
||||
|
||||
|
||||
def CreateVT(device, region, variables):
|
||||
'''
|
||||
Calculates the thermal voltage, based on the temperature.
|
||||
V_t : node model
|
||||
V_t_edge : edge model from arithmetic mean
|
||||
'''
|
||||
CreateNodeModel(device, region, 'V_t', "k*T/q")
|
||||
CreateArithmeticMean(device, region, 'V_t', 'V_t_edge')
|
||||
if 'T' in variables:
|
||||
CreateArithmeticMeanDerivative(device, region, 'V_t', 'V_t_edge', 'T')
|
||||
|
||||
|
||||
def CreateEField(device, region):
|
||||
'''
|
||||
Creates the EField and DField.
|
||||
'''
|
||||
edge_average_model(device=device, region=region, node_model="Potential",
|
||||
edge_model="EField", average_type="negative_gradient")
|
||||
edge_average_model(device=device, region=region, node_model="Potential",
|
||||
edge_model="EField", average_type="negative_gradient", derivative="Potential")
|
||||
|
||||
def CreateDField(device, region):
|
||||
CreateEdgeModel(device, region, "DField", "Permittivity * EField")
|
||||
CreateEdgeModel(device, region, "DField:Potential@n0", "Permittivity * EField:Potential@n0")
|
||||
CreateEdgeModel(device, region, "DField:Potential@n1", "Permittivity * EField:Potential@n1")
|
||||
|
||||
def CreateSiliconPotentialOnly(device, region):
|
||||
'''
|
||||
Creates the physical models for a Silicon region for equilibrium simulation.
|
||||
'''
|
||||
|
||||
variables = ("Potential",)
|
||||
CreateVT(device, region, variables)
|
||||
CreateDensityOfStates(device, region, variables)
|
||||
|
||||
SetSiliconParameters(device, region)
|
||||
|
||||
# require NetDoping
|
||||
for i in (
|
||||
("IntrinsicElectrons", "NIE*exp(Potential/V_t)"),
|
||||
("IntrinsicHoles", "NIE^2/IntrinsicElectrons"),
|
||||
("IntrinsicCharge", "kahan3(IntrinsicHoles, -IntrinsicElectrons, NetDoping)"),
|
||||
("PotentialIntrinsicCharge", "-q * IntrinsicCharge")
|
||||
):
|
||||
n = i[0]
|
||||
e = i[1]
|
||||
CreateNodeModel(device, region, n, e)
|
||||
CreateNodeModelDerivative(device, region, n, e, 'Potential')
|
||||
|
||||
CreateQuasiFermiLevels(device, region, 'IntrinsicElectrons', 'IntrinsicHoles', variables)
|
||||
|
||||
CreateEField(device, region)
|
||||
CreateDField(device, region)
|
||||
|
||||
equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
node_model="PotentialIntrinsicCharge", edge_model="DField", variable_update="log_damp")
|
||||
|
||||
def CreateSiliconPotentialOnlyContact(device, region, contact, is_circuit=False):
|
||||
'''
|
||||
Creates the potential equation at the contact
|
||||
if is_circuit is true, than use node given by GetContactBiasName
|
||||
'''
|
||||
if not InNodeModelList(device, region, "contactcharge_node"):
|
||||
CreateNodeModel(device, region, "contactcharge_node", "q*IntrinsicCharge")
|
||||
|
||||
celec_model = "(1e-10 + 0.5*abs(NetDoping+(NetDoping^2 + 4 * NIE^2)^(0.5)))"
|
||||
chole_model = "(1e-10 + 0.5*abs(-NetDoping+(NetDoping^2 + 4 * NIE^2)^(0.5)))"
|
||||
contact_model = "Potential -{0} + ifelse(NetDoping > 0, \
|
||||
-V_t*log({1}/NIE), \
|
||||
V_t*log({2}/NIE))".format(GetContactBiasName(contact), celec_model, chole_model)
|
||||
|
||||
contact_model_name = GetContactNodeModelName(contact)
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModel(device, contact, "{0}:{1}".format(contact_model_name,"Potential"), "1")
|
||||
if is_circuit:
|
||||
CreateContactNodeModel(device, contact, "{0}:{1}".format(contact_model_name,GetContactBiasName(contact)), "-1")
|
||||
|
||||
if is_circuit:
|
||||
contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_model="",
|
||||
node_charge_model="contactcharge_node", edge_charge_model="DField",
|
||||
node_current_model="", edge_current_model="", circuit_node=GetContactBiasName(contact))
|
||||
else:
|
||||
contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_model="",
|
||||
node_charge_model="contactcharge_node", edge_charge_model="DField",
|
||||
node_current_model="", edge_current_model="")
|
||||
|
||||
|
||||
def CreateSRH(device, region, variables):
|
||||
'''
|
||||
Shockley Read hall recombination model in terms of generation.
|
||||
'''
|
||||
USRH="(Electrons*Holes - NIE^2)/(taup*(Electrons + n1) + taun*(Holes + p1))"
|
||||
Gn = "-q * USRH"
|
||||
Gp = "+q * USRH"
|
||||
CreateNodeModel(device, region, "USRH", USRH)
|
||||
CreateNodeModel(device, region, "ElectronGeneration", Gn)
|
||||
CreateNodeModel(device, region, "HoleGeneration", Gp)
|
||||
for i in ("Electrons", "Holes", "T"):
|
||||
if i in variables:
|
||||
CreateNodeModelDerivative(device, region, "USRH", USRH, i)
|
||||
CreateNodeModelDerivative(device, region, "ElectronGeneration", Gn, i)
|
||||
CreateNodeModelDerivative(device, region, "HoleGeneration", Gp, i)
|
||||
|
||||
def CreateECE(device, region, Jn):
|
||||
'''
|
||||
Electron Continuity Equation using specified equation for Jn
|
||||
'''
|
||||
NCharge = "q * Electrons"
|
||||
CreateNodeModel(device, region, "NCharge", NCharge)
|
||||
CreateNodeModelDerivative(device, region, "NCharge", NCharge, "Electrons")
|
||||
|
||||
equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
|
||||
time_node_model = "NCharge",
|
||||
edge_model=Jn, variable_update="log_damp", node_model="ElectronGeneration")
|
||||
|
||||
def CreateHCE(device, region, Jp):
|
||||
'''
|
||||
Hole Continuity Equation using specified equation for Jp
|
||||
'''
|
||||
PCharge = "-q * Holes"
|
||||
CreateNodeModel(device, region, "PCharge", PCharge)
|
||||
CreateNodeModelDerivative(device, region, "PCharge", PCharge, "Holes")
|
||||
|
||||
equation(device=device, region=region, name="HoleContinuityEquation", variable_name="Holes",
|
||||
time_node_model = "PCharge",
|
||||
edge_model=Jp, variable_update="log_damp", node_model="HoleGeneration")
|
||||
|
||||
def CreatePE(device, region):
|
||||
'''
|
||||
Create Poisson Equation assuming the Electrons and Holes as solution variables
|
||||
'''
|
||||
pne = "-q*kahan3(Holes, -Electrons, NetDoping)"
|
||||
CreateNodeModel(device, region, "PotentialNodeCharge", pne)
|
||||
CreateNodeModelDerivative(device, region, "PotentialNodeCharge", pne, "Electrons")
|
||||
CreateNodeModelDerivative(device, region, "PotentialNodeCharge", pne, "Holes")
|
||||
|
||||
equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
node_model="PotentialNodeCharge", edge_model="DField",
|
||||
time_node_model="", variable_update="log_damp")
|
||||
|
||||
|
||||
def CreateSiliconDriftDiffusion(device, region, mu_n="mu_n", mu_p="mu_p", Jn='Jn', Jp='Jp'):
|
||||
'''
|
||||
Instantiate all equations for drift diffusion simulation
|
||||
'''
|
||||
CreateDensityOfStates(device, region, ("Potential",))
|
||||
CreateQuasiFermiLevels(device, region, "Electrons", "Holes", ("Electrons", "Holes", "Potential"))
|
||||
CreatePE(device, region)
|
||||
CreateSRH(device, region, ("Electrons", "Holes", "Potential"))
|
||||
CreateECE(device, region, Jn)
|
||||
CreateHCE(device, region, Jp)
|
||||
|
||||
|
||||
def CreateSiliconDriftDiffusionContact(device, region, contact, Jn, Jp, is_circuit=False):
|
||||
'''
|
||||
Restrict electrons and holes to their equilibrium values
|
||||
Integrates current into circuit
|
||||
'''
|
||||
CreateSiliconPotentialOnlyContact(device, region, contact, is_circuit)
|
||||
|
||||
celec_model = "(1e-10 + 0.5*abs(NetDoping+(NetDoping^2 + 4 * NIE^2)^(0.5)))"
|
||||
chole_model = "(1e-10 + 0.5*abs(-NetDoping+(NetDoping^2 + 4 * NIE^2)^(0.5)))"
|
||||
contact_electrons_model = "Electrons - ifelse(NetDoping > 0, {0}, NIE^2/{1})".format(celec_model, chole_model)
|
||||
contact_holes_model = "Holes - ifelse(NetDoping < 0, +{1}, +NIE^2/{0})".format(celec_model, chole_model)
|
||||
contact_electrons_name = "{0}nodeelectrons".format(contact)
|
||||
contact_holes_name = "{0}nodeholes".format(contact)
|
||||
|
||||
CreateContactNodeModel(device, contact, contact_electrons_name, contact_electrons_model)
|
||||
CreateContactNodeModel(device, contact, "{0}:{1}".format(contact_electrons_name, "Electrons"), "1")
|
||||
|
||||
CreateContactNodeModel(device, contact, contact_holes_name, contact_holes_model)
|
||||
CreateContactNodeModel(device, contact, "{0}:{1}".format(contact_holes_name, "Holes"), "1")
|
||||
|
||||
if is_circuit:
|
||||
contact_equation(device=device, contact=contact, name="ElectronContinuityEquation",
|
||||
node_model=contact_electrons_name,
|
||||
edge_current_model=Jn, circuit_node=GetContactBiasName(contact))
|
||||
|
||||
contact_equation(device=device, contact=contact, name="HoleContinuityEquation",
|
||||
node_model=contact_holes_name,
|
||||
edge_current_model=Jp, circuit_node=GetContactBiasName(contact))
|
||||
|
||||
else:
|
||||
contact_equation(device=device, contact=contact, name="ElectronContinuityEquation",
|
||||
node_model=contact_electrons_name,
|
||||
edge_current_model=Jn)
|
||||
|
||||
contact_equation(device=device, contact=contact, name="HoleContinuityEquation",
|
||||
node_model=contact_holes_name,
|
||||
edge_current_model=Jp)
|
||||
|
||||
|
||||
def CreateBernoulliString (Potential="Potential", scaling_variable="V_t", sign=-1):
|
||||
'''
|
||||
Creates the Bernoulli function for Scharfetter Gummel
|
||||
sign -1 for potential
|
||||
sign +1 for energy
|
||||
scaling variable should be V_t
|
||||
Potential should be scaled by V_t in V
|
||||
Ec, Ev should scaled by V_t in eV
|
||||
|
||||
returns the Bernoulli expression and its argument
|
||||
Caller should understand that B(-x) = B(x) + x
|
||||
'''
|
||||
|
||||
tdict = {
|
||||
"Potential" : Potential,
|
||||
"V_t" : scaling_variable
|
||||
}
|
||||
#### test for requisite models here
|
||||
if sign == -1:
|
||||
vdiff="(%(Potential)s@n0 - %(Potential)s@n1)/%(V_t)s" % tdict
|
||||
elif sign == 1:
|
||||
vdiff="(%(Potential)s@n1 - %(Potential)s@n0)/%(V_t)s" % tdict
|
||||
else:
|
||||
raise NameError("Invalid Sign %s" % sign)
|
||||
|
||||
Bern01 = "B(%s)" % vdiff
|
||||
return (Bern01, vdiff)
|
||||
|
||||
|
||||
def CreateElectronCurrent(device, region, mu_n, Potential="Potential", sign=-1, ElectronCurrent="ElectronCurrent", V_t="V_t_edge"):
|
||||
'''
|
||||
Electron current
|
||||
mu_n = mobility name
|
||||
Potential is the driving potential
|
||||
'''
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Potential")
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Electrons")
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Holes")
|
||||
if Potential == "Potential":
|
||||
(Bern01, vdiff) = CreateBernoulliString(scaling_variable=V_t, Potential=Potential, sign=sign)
|
||||
else:
|
||||
raise NameError("Implement proper call")
|
||||
|
||||
tdict = {
|
||||
'Bern01' : Bern01,
|
||||
'vdiff' : vdiff,
|
||||
'mu_n' : mu_n,
|
||||
'V_t' : V_t
|
||||
}
|
||||
|
||||
Jn = "q*%(mu_n)s*EdgeInverseLength*%(V_t)s*kahan3(Electrons@n1*%(Bern01)s, Electrons@n1*%(vdiff)s, -Electrons@n0*%(Bern01)s)" % tdict
|
||||
|
||||
CreateEdgeModel(device, region, ElectronCurrent, Jn)
|
||||
for i in ("Electrons", "Potential", "Holes"):
|
||||
CreateEdgeModelDerivatives(device, region, ElectronCurrent, Jn, i)
|
||||
|
||||
def CreateHoleCurrent(device, region, mu_p, Potential="Potential", sign=-1, HoleCurrent="HoleCurrent", V_t="V_t_edge"):
|
||||
'''
|
||||
Hole current
|
||||
'''
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Potential")
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Electrons")
|
||||
EnsureEdgeFromNodeModelExists(device, region, "Holes")
|
||||
# Make sure the bernoulli functions exist
|
||||
if Potential == "Potential":
|
||||
(Bern01, vdiff) = CreateBernoulliString(scaling_variable=V_t, Potential=Potential, sign=sign)
|
||||
else:
|
||||
raise NameError("Implement proper call for " + Potential)
|
||||
|
||||
tdict = {
|
||||
'Bern01' : Bern01,
|
||||
'vdiff' : vdiff,
|
||||
'mu_p' : mu_p,
|
||||
'V_t' : V_t
|
||||
}
|
||||
|
||||
Jp ="-q*%(mu_p)s*EdgeInverseLength*%(V_t)s*kahan3(Holes@n1*%(Bern01)s, -Holes@n0*%(Bern01)s, -Holes@n0*%(vdiff)s)" % tdict
|
||||
CreateEdgeModel(device, region, HoleCurrent, Jp)
|
||||
for i in ("Holes", "Potential", "Electrons"):
|
||||
CreateEdgeModelDerivatives(device, region, HoleCurrent, Jp, i)
|
||||
|
||||
def CreateAroraMobilityLF(device, region):
|
||||
'''
|
||||
Creates node mobility models and then averages them on edge
|
||||
Uses model from Muller and Kamins
|
||||
Add T derivative dependence later
|
||||
'''
|
||||
models = (
|
||||
('Tn', 'T/300'),
|
||||
('mu_arora_n_node',
|
||||
'MUMN * pow(Tn, MUMEN) + (MU0N * pow(T, MU0EN))/(1 + pow((NTOT/(NREFN*pow(Tn, NREFNE))), ALPHA0N*pow(Tn, ALPHAEN)))'),
|
||||
('mu_arora_p_node',
|
||||
'MUMP * pow(Tn, MUMEP) + (MU0P * pow(T, MU0EP))/(1 + pow((NTOT/(NREFP*pow(Tn, NREFPE))), ALPHA0P*pow(Tn, ALPHAEP)))')
|
||||
)
|
||||
|
||||
for k, v in models:
|
||||
CreateNodeModel(device, region, k, v)
|
||||
CreateArithmeticMean(device, region, 'mu_arora_n_node', 'mu_arora_n_lf')
|
||||
CreateArithmeticMean(device, region, 'mu_arora_p_node', 'mu_arora_p_lf')
|
||||
CreateElectronCurrent(device, region, mu_n = 'mu_arora_n_lf', Potential="Potential", sign=-1, ElectronCurrent="Jn_arora_lf", V_t="V_t_edge")
|
||||
CreateHoleCurrent(device, region, mu_p = 'mu_arora_p_lf', Potential="Potential", sign=-1, HoleCurrent="Jp_arora_lf", V_t="V_t_edge")
|
||||
return {
|
||||
'mu_n' : 'mu_arora_n_lf',
|
||||
'mu_p' : 'mu_arora_p_lf',
|
||||
'Jn' : 'Jn_arora_lf',
|
||||
'Jp' : 'Jp_arora_lf',
|
||||
}
|
||||
|
||||
|
||||
def CreateHFMobility(device, region, mu_n, mu_p, Jn, Jp):
|
||||
'''
|
||||
Add T derivatives when debugged
|
||||
use parameters to set model flags
|
||||
Caughey Thomas
|
||||
'''
|
||||
|
||||
tdict = {
|
||||
'Jn' : Jn,
|
||||
'mu_n' : mu_n,
|
||||
'Jp' : Jp,
|
||||
'mu_p' : mu_p
|
||||
}
|
||||
tlist = (
|
||||
("vsat_n", "VSATN0 * pow(T, VSATNE)" % tdict, ('T')),
|
||||
("beta_n", "BETAN0 * pow(T, BETANE)" % tdict, ('T')),
|
||||
("Epar_n",
|
||||
"ifelse((%(Jn)s * EField) > 0, abs(EField), 1e-15)" % tdict, ('Potential')),
|
||||
("mu_n", "%(mu_n)s * pow(1 + pow((%(mu_n)s*Epar_n/vsat_n), beta_n), -1/beta_n)"
|
||||
% tdict, ('Electrons', 'Holes', 'Potential', 'T')),
|
||||
("vsat_p", "VSATP0 * pow(T, VSATPE)" % tdict, ('T')),
|
||||
("beta_p", "BETAP0 * pow(T, BETAPE)" % tdict, ('T')),
|
||||
("Epar_p",
|
||||
"ifelse((%(Jp)s * EField) > 0, abs(EField), 1e-15)" % tdict, ('Potential')),
|
||||
("mu_p", "%(mu_p)s * pow(1 + pow(%(mu_p)s*Epar_p/vsat_p, beta_p), -1/beta_p)"
|
||||
% tdict, ('Electrons', 'Holes', 'Potential', 'T')),
|
||||
)
|
||||
|
||||
variable_list = ('Electrons', 'Holes', 'Potential')
|
||||
for (model, equation, variables) in tlist:
|
||||
CreateEdgeModel(device, region, model, equation)
|
||||
for v in variable_list:
|
||||
if v in variables:
|
||||
CreateEdgeModelDerivatives(device, region, model, equation, v)
|
||||
|
||||
# This create derivatives automatically
|
||||
CreateElectronCurrent(device, region, mu_n='mu_n', Potential="Potential", sign=-1, ElectronCurrent="Jn", V_t="V_t_edge")
|
||||
CreateHoleCurrent( device, region, mu_p='mu_p', Potential="Potential", sign=-1, HoleCurrent="Jp", V_t="V_t_edge")
|
||||
return {
|
||||
'mu_n' : 'mu_n',
|
||||
'mu_p' : 'mu_p',
|
||||
'Jn' : 'Jn',
|
||||
'Jp' : 'Jp',
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Copyright 2013 Devsim LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
import devsim
|
||||
from .new_physics import *
|
||||
|
||||
### modify to grow and shrink size
|
||||
def rampvoltage(device, Vsource, begin_bias, end_bias, init_step_size, min_step, max_iter, rel_error, abs_error, callback):
|
||||
'''
|
||||
Ramps bias with assignable callback function
|
||||
'''
|
||||
start_bias=begin_bias
|
||||
|
||||
if (start_bias < end_bias):
|
||||
step_sign=1
|
||||
else:
|
||||
step_sign=-1
|
||||
|
||||
num_successes = 0
|
||||
last_bias=start_bias
|
||||
step_size=init_step_size
|
||||
while(abs(last_bias - end_bias) > min_step):
|
||||
print(("last end %e %e") % (last_bias, end_bias))
|
||||
next_bias=last_bias + step_sign * step_size
|
||||
if next_bias < end_bias:
|
||||
next_step_sign=1
|
||||
else:
|
||||
next_step_sign=-1
|
||||
|
||||
if next_step_sign != step_sign:
|
||||
next_bias=end_bias
|
||||
print("setting to last bias %e" % (end_bias))
|
||||
print("setting next bias %e" % (next_bias))
|
||||
|
||||
devsim.circuit_alter(name=Vsource, value=next_bias)
|
||||
try:
|
||||
devsim.solve(type="dc", absolute_error=abs_error, relative_error=rel_error, maximum_iterations=max_iter)
|
||||
except devsim.error as msg:
|
||||
if str(msg).find("Convergence failure") != 0:
|
||||
raise
|
||||
devsim.circuit_alter(name=Vsource, value=last_bias)
|
||||
step_size *= 0.5
|
||||
print("setting new step size %e" % (step_size))
|
||||
if step_size < min_step:
|
||||
raise RuntimeError("Min step size too small")
|
||||
num_successes = 0
|
||||
continue
|
||||
num_successes += 1
|
||||
if (num_successes > 5) and (step_size < init_step_size):
|
||||
step_size *= 2
|
||||
if step_size > init_step_size:
|
||||
step_size = init_step_size
|
||||
print("setting new step size %e" % (step_size))
|
||||
num_successes = 0
|
||||
print("Succeeded")
|
||||
last_bias=next_bias
|
||||
callback()
|
||||
|
||||
def rampbias(device, contact, end_bias, step_size, min_step, max_iter, rel_error, abs_error, callback):
|
||||
'''
|
||||
Ramps bias with assignable callback function
|
||||
'''
|
||||
start_bias=devsim.get_parameter(device=device, name=GetContactBiasName(contact))
|
||||
if (start_bias < end_bias):
|
||||
step_sign=1
|
||||
else:
|
||||
step_sign=-1
|
||||
last_bias=start_bias
|
||||
while(abs(last_bias - end_bias) > min_step):
|
||||
print(("last end %e %e") % (last_bias, end_bias))
|
||||
next_bias=last_bias + step_sign * step_size
|
||||
if next_bias < end_bias:
|
||||
next_step_sign=1
|
||||
else:
|
||||
next_step_sign=-1
|
||||
|
||||
if next_step_sign != step_sign:
|
||||
next_bias=end_bias
|
||||
print("setting to last bias %e" % (end_bias))
|
||||
print("setting next bias %e" % (next_bias))
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(contact), value=next_bias)
|
||||
try:
|
||||
devsim.solve(type="dc", absolute_error=abs_error, relative_error=rel_error, maximum_iterations=max_iter)
|
||||
except devsim.error as msg:
|
||||
if str(msg).find("Convergence failure") != 0:
|
||||
raise
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(contact), value=last_bias)
|
||||
step_size *= 0.5
|
||||
print("setting new step size %e" % (step_size))
|
||||
if step_size < min_step:
|
||||
raise RuntimeError("Min step size too small")
|
||||
continue
|
||||
print("Succeeded")
|
||||
last_bias=next_bias
|
||||
callback()
|
||||
|
||||
def rampbias(device, contact, end_bias, step_size, min_step, max_iter, rel_error, abs_error, callback):
|
||||
'''
|
||||
Ramps bias with assignable callback function
|
||||
'''
|
||||
start_bias=devsim.get_parameter(device=device, name=GetContactBiasName(contact))
|
||||
if (start_bias < end_bias):
|
||||
step_sign=1
|
||||
else:
|
||||
step_sign=-1
|
||||
last_bias=start_bias
|
||||
while(abs(last_bias - end_bias) > min_step):
|
||||
print(("last end %e %e") % (last_bias, end_bias))
|
||||
next_bias=last_bias + step_sign * step_size
|
||||
if next_bias < end_bias:
|
||||
next_step_sign=1
|
||||
else:
|
||||
next_step_sign=-1
|
||||
|
||||
if next_step_sign != step_sign:
|
||||
next_bias=end_bias
|
||||
print("setting to last bias %e" % (end_bias))
|
||||
print("setting next bias %e" % (next_bias))
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(contact), value=next_bias)
|
||||
try:
|
||||
devsim.solve(type="dc", absolute_error=abs_error, relative_error=rel_error, maximum_iterations=max_iter)
|
||||
except devsim.error as msg:
|
||||
if str(msg).find("Convergence failure") != 0:
|
||||
raise
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(contact), value=last_bias)
|
||||
step_size *= 0.5
|
||||
print("setting new step size %e" % (step_size))
|
||||
if step_size < min_step:
|
||||
raise RuntimeError("Min step size too small")
|
||||
continue
|
||||
print("Succeeded")
|
||||
last_bias=next_bias
|
||||
callback(device)
|
||||
|
||||
def printAllCurrents(device, bias):
|
||||
'''
|
||||
Prints all contact currents on device
|
||||
'''
|
||||
for c in get_contact_list(device=device):
|
||||
x = get_DCcurrent(device, c)
|
||||
|
||||
def PrintCurrents(device, contact):
|
||||
'''
|
||||
print out contact currents
|
||||
'''
|
||||
contact_bias_name = GetContactBiasName(contact)
|
||||
electron_current= get_contact_current(device=device, contact=contact, equation=ece_name)
|
||||
hole_current = get_contact_current(device=device, contact=contact, equation=hce_name)
|
||||
total_current = electron_current + hole_current
|
||||
voltage = devsim.get_parameter(device=device, name=GetContactBiasName(contact))
|
||||
print("{0}\t{1}\t{2}\t{3}\t{4}".format(contact, voltage, electron_current, hole_current, total_current))
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import devsim
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.tri as tri
|
||||
from device_config import *
|
||||
|
||||
device = "device_2d"
|
||||
|
||||
# 1. Load the mesh
|
||||
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
|
||||
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")
|
||||
|
||||
# Add contacts for Silicon region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Si", name="MT1_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Si", name="MT2_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Si", name="MRING_L", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Si", name="MRING_R", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="Substrate_Bottom", name="Substrate_Bottom", region="Silicon", material="metal")
|
||||
|
||||
# Add contacts for Oxide region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Ox", name="MT1_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Ox", name="MT2_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Ox", name="MRING_L_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Ox", name="MRING_R_Ox", region="Oxide", material="metal")
|
||||
|
||||
# Add contacts for Molding region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Mold", name="MT1_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Mold", name="MT2_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Mold", name="MRING_L_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Mold", name="MRING_R_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="Substrate_Bottom_Mold", name="Substrate_Bottom_Mold", region="Molding", material="metal")
|
||||
|
||||
# Add interfaces
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Ox_Interface", name="Si_Ox", region0="Silicon", region1="Oxide")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Ox_Mold_Interface", name="Ox_Mold", region0="Oxide", region1="Molding")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Mold_Interface", name="Si_Mold", region0="Silicon", region1="Molding")
|
||||
|
||||
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)")
|
||||
|
||||
# Write Tecplot output for ParaView
|
||||
devsim.write_devices(file="device_2d.tec", type="tecplot")
|
||||
devsim.write_devices(file="preview.tec", type="tecplot")
|
||||
print("Saved device_2d.tec and preview.tec")
|
||||
|
||||
# 4. Generate a 2D Plot with Matplotlib to verify the doping profile
|
||||
print("Generating 2D plot...")
|
||||
x = devsim.get_node_model_values(device=device, region="Silicon", name="x")
|
||||
y = devsim.get_node_model_values(device=device, region="Silicon", name="y")
|
||||
net_dop = devsim.get_node_model_values(device=device, region="Silicon", name="NetDoping")
|
||||
log_dop = devsim.get_node_model_values(device=device, region="Silicon", name="LogNetDoping")
|
||||
|
||||
elements = devsim.get_element_node_list(device=device, region="Silicon")
|
||||
|
||||
# Convert elements into a format usable by matplotlib
|
||||
triangles = np.array(elements)
|
||||
|
||||
# scale to micrometers for plotting
|
||||
x_um = np.array(x) / um
|
||||
y_um = np.array(y) / um
|
||||
|
||||
log_acceptors = devsim.get_node_model_values(device=device, region="Silicon", name="LogAcceptors")
|
||||
|
||||
def draw_oxide_and_metal(ax):
|
||||
# 0. Molding Region (light yellow-gray or beige)
|
||||
# Top Molding
|
||||
ax.add_patch(plt.Rectangle((-W_SIM / um, - (T_OX + H_MOLD) / um), 2 * W_SIM / um, H_MOLD / um, facecolor='#fbfcf7', edgecolor='lightgray', linewidth=0.5, alpha=0.9))
|
||||
# Left Side Molding
|
||||
ax.add_patch(plt.Rectangle((-W_SIM / um, - T_OX / um), (W_SIM - W_DEVICE) / um, (H_SI + T_OX) / um, facecolor='#fbfcf7', edgecolor='lightgray', linewidth=0.5, alpha=0.9))
|
||||
# Right Side Molding
|
||||
ax.add_patch(plt.Rectangle((W_DEVICE / um, - T_OX / um), (W_SIM - W_DEVICE) / um, (H_SI + T_OX) / um, facecolor='#fbfcf7', edgecolor='lightgray', linewidth=0.5, alpha=0.9))
|
||||
|
||||
# 1. Oxide Layer (light blue-gray)
|
||||
rect_oxide = plt.Rectangle((-W_DEVICE / um, - T_OX / um), 2 * W_DEVICE / um, T_OX / um, facecolor='#eaeef2', edgecolor='gray', linewidth=0.5, alpha=0.9)
|
||||
ax.add_patch(rect_oxide)
|
||||
|
||||
# 2. Leadframe Island at bottom (dark grey-blue)
|
||||
rect_leadframe = plt.Rectangle((-W_SIM / um, H_SI / um), 2 * W_SIM / um, 15.0, facecolor='#34495e', edgecolor='black', alpha=0.9)
|
||||
ax.add_patch(rect_leadframe)
|
||||
|
||||
# 3. Metal color & settings (grey color for electrodes)
|
||||
m_color = '#7f8c8d' # sleek dark gray
|
||||
m_edge = '#2c3e50'
|
||||
m_alpha = 1.0
|
||||
|
||||
# MT1 (Right side)
|
||||
# Long plate top part: X in [30, 186], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((30.0, -2.5), 156.0, 0.5, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Via 1 (under p11): X in [82.5, 92.5], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((82.5, -2.0), 10.0, 2.0, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Via 2 (under p13): X in [169.5, 179.5], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((169.5, -2.0), 10.0, 2.0, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Small plate top part: X in [250, 295], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((250.0, -2.5), 45.0, 0.5, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
|
||||
# MT2 (Left side)
|
||||
# Long plate top part: X in [-186, -30], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((-186.0, -2.5), 156.0, 0.5, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Via 1: X in [-92.5, -82.5], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((-92.5, -2.0), 10.0, 2.0, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Via 2: X in [-179.5, -169.5], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((-179.5, -2.0), 10.0, 2.0, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
# Small plate top part: X in [-295, -250], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((-295.0, -2.5), 45.0, 0.5, facecolor=m_color, edgecolor=m_edge, alpha=m_alpha))
|
||||
|
||||
# MRING (Right & Left)
|
||||
mring_color = '#e67e22' # bright orange-red for MRING to distinguish
|
||||
mring_edge = '#d35400'
|
||||
# Right MRING via: X in [340, 356], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((340.0, -2.0), 16.0, 2.0, facecolor=mring_color, edgecolor=mring_edge, alpha=m_alpha))
|
||||
# Right MRING top plate: X in [335, 356], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((335.0, -2.5), 21.0, 0.5, facecolor=mring_color, edgecolor=mring_edge, alpha=m_alpha))
|
||||
|
||||
# Left MRING via: X in [-356, -340], Y in [-2.0, 0]
|
||||
ax.add_patch(plt.Rectangle((-356.0, -2.0), 16.0, 2.0, facecolor=mring_color, edgecolor=mring_edge, alpha=m_alpha))
|
||||
# Left MRING top plate: X in [-356, -335], Y in [-2.5, -2.0]
|
||||
ax.add_patch(plt.Rectangle((-356.0, -2.5), 21.0, 0.5, facecolor=mring_color, edgecolor=mring_edge, alpha=m_alpha))
|
||||
|
||||
# Add text labels
|
||||
ax.text(108.0, -2.8, 'MT1', color='black', fontsize=8, ha='center', weight='bold')
|
||||
ax.text(-108.0, -2.8, 'MT2', color='black', fontsize=8, ha='center', weight='bold')
|
||||
ax.text(348.0, -2.8, 'MRING', color='#d35400', fontsize=8, ha='center', weight='bold')
|
||||
ax.text(-348.0, -2.8, 'MRING', color='#d35400', fontsize=8, ha='center', weight='bold')
|
||||
ax.text(0, -50.0, 'Molding Region', color='darkgreen', fontsize=9, ha='center', va='center')
|
||||
ax.text(-406.0, 50.0, 'Molding\nCompound\n(Side)', color='darkgreen', fontsize=8, ha='center', va='center')
|
||||
ax.text(406.0, 50.0, 'Molding\nCompound\n(Side)', color='darkgreen', fontsize=8, ha='center', va='center')
|
||||
ax.text(0, -1.0, 'Oxide', color='blue', fontsize=9, ha='center', va='center')
|
||||
ax.text(0, H_SI/um + 7.5, 'Leadframe paddle (Island)', color='white', fontsize=9, ha='center', va='center', weight='bold')
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 12))
|
||||
|
||||
# Subplot 1: Net Doping
|
||||
tcf1 = ax1.tripcolor(x_um, y_um, triangles, log_dop, cmap='coolwarm', shading='flat')
|
||||
fig.colorbar(tcf1, ax=ax1, label='Log10(NetDoping) [asinh(N/2)/log(10)]')
|
||||
draw_oxide_and_metal(ax1)
|
||||
ax1.set_xlabel('X (μm)')
|
||||
ax1.set_ylabel('Y (μm)')
|
||||
ax1.set_title('2D Net Doping Profile (NetDoping = Donors - Acceptors)')
|
||||
ax1.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax1.set_ylim(H_SI/um + 15.0, -110.0) # Show substrate, bottom contact, oxide, and top molding
|
||||
|
||||
# Subplot 2: Acceptors (P-type dopants)
|
||||
tcf2 = ax2.tripcolor(x_um, y_um, triangles, np.array(log_acceptors), cmap='Purples', shading='flat')
|
||||
fig.colorbar(tcf2, ax=ax2, label='Log10(Acceptor Doping)')
|
||||
draw_oxide_and_metal(ax2)
|
||||
ax2.set_xlabel('X (μm)')
|
||||
ax2.set_ylabel('Y (μm)')
|
||||
ax2.set_title('2D Acceptor Doping Profile (p11, p12, p13)')
|
||||
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)
|
||||
plt.close()
|
||||
print("Plot saved to doping_2d.png")
|
||||
Executable
+194
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
preview_doping_3d.py - Parameterized 3D geometry and doping setup for BJT.
|
||||
Generates a 3D mesh using Gmsh and exports a Tecplot file for ParaView preview.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import gmsh
|
||||
import numpy as np
|
||||
|
||||
# Add virtual env path to ensure devsim is found
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from devsim import (
|
||||
create_gmsh_mesh, add_gmsh_region, add_gmsh_contact,
|
||||
finalize_mesh, create_device, get_device_list,
|
||||
node_model, write_devices
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# 1. 幾何參數定義 (單位: cm, 1 um = 1e-4 cm)
|
||||
# =============================================================================
|
||||
um = 1e-4
|
||||
|
||||
# 元件總尺寸
|
||||
W_device = 50.0 * um # X 寬度
|
||||
H_device = 25.0 * um # Y 深度 (朝基底方向)
|
||||
L_device = 10.0 * um # Z 長度
|
||||
|
||||
# 電極尺寸與位置
|
||||
# Emitter (X: 20~30 um, Z: 2~8 um, Y: 0 表面)
|
||||
x_emit_center = 25.0 * um
|
||||
w_emit = 10.0 * um
|
||||
z_emit_center = 5.0 * um
|
||||
l_emit = 6.0 * um
|
||||
|
||||
# Base (X: 40~45 um, Z: 2~8 um, Y: 0 表面)
|
||||
x_base_center = 42.5 * um
|
||||
w_base = 5.0 * um
|
||||
z_base_center = 5.0 * um
|
||||
l_base = 6.0 * um
|
||||
|
||||
# 網格控制尺寸 (加密以獲得平滑的 3D 界面)
|
||||
mesh_size_min = 0.15 * um
|
||||
mesh_size_max = 0.8 * um
|
||||
|
||||
# =============================================================================
|
||||
# 2. 使用 Gmsh Python API 進行 3D 幾何與網格建模
|
||||
# =============================================================================
|
||||
print(">>> Step 1: Generating 3D geometry and mesh using Gmsh...")
|
||||
gmsh.initialize()
|
||||
gmsh.model.add("bjt_3d_device")
|
||||
|
||||
# 設定網格大小限制與輸出格式為 MSH v2.2
|
||||
gmsh.option.setNumber("Mesh.CharacteristicLengthMin", mesh_size_min)
|
||||
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", mesh_size_max)
|
||||
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
|
||||
|
||||
# 建立矽區主體 (Box)
|
||||
silicon_vol = gmsh.model.occ.addBox(0, 0, 0, W_device, H_device, L_device)
|
||||
|
||||
# 建立 Emitter 和 Base 的接觸面 (Rectangles, 位在 Y = 0 的上表面)
|
||||
# 由於 addRectangle 預設是在 X-Y 平面建立,我們需要將其旋轉 -90 度到 X-Z 平面
|
||||
emitter_surf = gmsh.model.occ.addRectangle(
|
||||
x_emit_center - 0.5 * w_emit, 0, z_emit_center - 0.5 * l_emit,
|
||||
w_emit, l_emit
|
||||
)
|
||||
base_surf = gmsh.model.occ.addRectangle(
|
||||
x_base_center - 0.5 * w_base, 0, z_base_center - 0.5 * l_base,
|
||||
w_base, l_base
|
||||
)
|
||||
|
||||
# 旋轉至 X-Z 平面 (繞起點,沿 X 軸方向向量 [1, 0, 0] 旋轉 pi/2)
|
||||
gmsh.model.occ.rotate([(2, emitter_surf)], x_emit_center - 0.5 * w_emit, 0, z_emit_center - 0.5 * l_emit, 1, 0, 0, np.pi/2)
|
||||
gmsh.model.occ.rotate([(2, base_surf)], x_base_center - 0.5 * w_base, 0, z_base_center - 0.5 * l_base, 1, 0, 0, np.pi/2)
|
||||
|
||||
# 使用布林片段 (Boolean Fragment) 將電極表面縫合嵌入到矽主體的頂面
|
||||
# 這會確保網格在此交界處的節點能完美對齊
|
||||
out, out_map = gmsh.model.occ.fragment(
|
||||
[(3, silicon_vol)],
|
||||
[(2, emitter_surf), (2, base_surf)]
|
||||
)
|
||||
gmsh.model.occ.synchronize()
|
||||
|
||||
# 找出各個實體 (Entity) 的 Tag 來指定 Physical Groups (區域與電極)
|
||||
# 我們透過包圍盒 (Bounding Box) 來精確搜尋
|
||||
# search format: xmin, ymin, zmin, xmax, ymax, zmax, dim
|
||||
|
||||
# 1. 找出 Silicon 3D 體積 (dim=3)
|
||||
vol_entities = gmsh.model.getEntities(3)
|
||||
silicon_tags = [tag for dim, tag in vol_entities]
|
||||
gmsh.model.addPhysicalGroup(3, silicon_tags, tag=1, name="Silicon")
|
||||
|
||||
# 2. 找出 Emitter 接觸面 (dim=2, 位在 Y = 0)
|
||||
eps = 0.1 * um
|
||||
emitter_entities = gmsh.model.getEntitiesInBoundingBox(
|
||||
x_emit_center - 0.5 * w_emit - eps, -eps, z_emit_center - 0.5 * l_emit - eps,
|
||||
x_emit_center + 0.5 * w_emit + eps, eps, z_emit_center + 0.5 * l_emit + eps,
|
||||
dim=2
|
||||
)
|
||||
emitter_tags = [tag for dim, tag in emitter_entities]
|
||||
gmsh.model.addPhysicalGroup(2, emitter_tags, tag=10, name="emitter")
|
||||
|
||||
# 3. 找出 Base 接觸面 (dim=2, 位在 Y = 0)
|
||||
base_entities = gmsh.model.getEntitiesInBoundingBox(
|
||||
x_base_center - 0.5 * w_base - eps, -eps, z_base_center - 0.5 * l_base - eps,
|
||||
x_base_center + 0.5 * w_base + eps, eps, z_base_center + 0.5 * l_base + eps,
|
||||
dim=2
|
||||
)
|
||||
base_tags = [tag for dim, tag in base_entities]
|
||||
gmsh.model.addPhysicalGroup(2, base_tags, tag=11, name="base")
|
||||
|
||||
# 4. 找出 Collector 接觸面 (整個底面, dim=2, 位在 Y = H_device)
|
||||
collector_entities = gmsh.model.getEntitiesInBoundingBox(
|
||||
-eps, H_device - eps, -eps,
|
||||
W_device + eps, H_device + eps, L_device + eps,
|
||||
dim=2
|
||||
)
|
||||
collector_tags = [tag for dim, tag in collector_entities]
|
||||
gmsh.model.addPhysicalGroup(2, collector_tags, tag=12, name="collector")
|
||||
|
||||
# 生成 3D 網格
|
||||
gmsh.model.mesh.generate(3)
|
||||
msh_filename = "bjt_3d.msh"
|
||||
gmsh.write(msh_filename)
|
||||
gmsh.finalize()
|
||||
print(f">>> Step 1 Completed: Mesh saved to {msh_filename}")
|
||||
|
||||
# =============================================================================
|
||||
# 3. 載入 DEVSIM 並設定 3D 參數化摻雜 (Doping Profile)
|
||||
# =============================================================================
|
||||
print(">>> Step 2: Setting up 3D Doping Profiles in DEVSIM...")
|
||||
device = "bjt_3d_device"
|
||||
|
||||
# 載入 Gmsh 產生的網格
|
||||
create_gmsh_mesh(mesh=device, file=msh_filename)
|
||||
add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
|
||||
for contact in ["collector", "emitter", "base"]:
|
||||
add_gmsh_contact(mesh=device, gmsh_name=contact, region="Silicon", name=contact, material="metal")
|
||||
finalize_mesh(mesh=device)
|
||||
create_device(mesh=device, device=device)
|
||||
|
||||
# --- 定義 3D 擴散參數 (可自由調整數值進行參數化) ---
|
||||
# Emitter (N+)
|
||||
node_model(device=device, region="Silicon", name="emitter_doping", equation="1.0e19")
|
||||
node_model(device=device, region="Silicon", name="emitter_depth", equation="0.8e-4") # 0.8 um 結深
|
||||
node_model(device=device, region="Silicon", name="emitter_vdiff", equation="0.2e-4") # Y 垂直擴散係數
|
||||
node_model(device=device, region="Silicon", name="emitter_hdiff", equation="0.15e-4") # X 橫向擴散係數
|
||||
node_model(device=device, region="Silicon", name="emitter_zdiff", equation="0.15e-4") # Z 橫向擴散係數
|
||||
|
||||
# Base (P)
|
||||
node_model(device=device, region="Silicon", name="base_doping", equation="1.0e17")
|
||||
node_model(device=device, region="Silicon", name="base_depth", equation="3.5e-4") # 3.5 um 結深
|
||||
node_model(device=device, region="Silicon", name="base_vdiff", equation="0.8e-4")
|
||||
node_model(device=device, region="Silicon", name="base_hdiff", equation="0.6e-4")
|
||||
node_model(device=device, region="Silicon", name="base_zdiff", equation="0.6e-4")
|
||||
|
||||
# Background Substrate (N-)
|
||||
node_model(device=device, region="Silicon", name="nsub_doping", equation="1.0e16")
|
||||
|
||||
# --- 3D ERFC 摻雜分佈方程式 (X, Y, Z 三維空間分佈) ---
|
||||
# Emitter 3D 摻雜 (Y 往下擴散,X 與 Z 則是雙向橫向擴散)
|
||||
node_model(device=device, region="Silicon", name="nD_emit", equation=f"""
|
||||
emitter_doping * erfc((y - emitter_depth) / emitter_vdiff)
|
||||
* erfc(-(x + 0.5 * {w_emit} - {x_emit_center}) / emitter_hdiff)
|
||||
* erfc((x - 0.5 * {w_emit} - {x_emit_center}) / emitter_hdiff)
|
||||
* erfc(-(z + 0.5 * {l_emit} - {z_emit_center}) / emitter_zdiff)
|
||||
* erfc((z - 0.5 * {l_emit} - {z_emit_center}) / emitter_zdiff)
|
||||
""")
|
||||
|
||||
# Base 3D 摻雜
|
||||
node_model(device=device, region="Silicon", name="nA_base", equation=f"""
|
||||
base_doping * erfc((y - base_depth) / base_vdiff)
|
||||
* erfc(-(x + 0.5 * {w_base} - {x_base_center}) / base_hdiff)
|
||||
* erfc((x - 0.5 * {w_base} - {x_base_center}) / base_hdiff)
|
||||
* erfc(-(z + 0.5 * {l_base} - {z_base_center}) / base_zdiff)
|
||||
* erfc((z - 0.5 * {l_base} - {z_base_center}) / base_zdiff)
|
||||
""")
|
||||
|
||||
# 合併總摻雜 (NetDoping)
|
||||
node_model(device=device, region="Silicon", name="Donors", equation="nsub_doping + nD_emit")
|
||||
node_model(device=device, region="Silicon", name="Acceptors", equation="1e10 + nA_base")
|
||||
node_model(device=device, region="Silicon", name="NetDoping", equation="Donors - Acceptors")
|
||||
|
||||
# 建立 LogScale 變數,便於在 ParaView 中以對數範圍看濃度 (跨越 10^10 ~ 10^19)
|
||||
node_model(device=device, region="Silicon", name="LogNetDoping", equation="asinh(NetDoping/2)/log(10)")
|
||||
|
||||
# =============================================================================
|
||||
# 4. 輸出預覽檔案 (不進行求解)
|
||||
# =============================================================================
|
||||
preview_filename = "doping_3d_preview.tec"
|
||||
print(f">>> Step 3: Exporting preview to {preview_filename}...")
|
||||
write_devices(file=preview_filename, type="tecplot")
|
||||
print(">>> Step 3 Completed! Ready for ParaView visualization.")
|
||||
@@ -0,0 +1,179 @@
|
||||
# Project Status: devsim2026
|
||||
|
||||
這是一份專案的狀態與交接說明文件,旨在記錄使用者的開發偏好、專案目前的架構、環境設定以及後續計畫,以便下次直接銜接。
|
||||
|
||||
---
|
||||
|
||||
## 📌 更新歷史紀錄 (Update History)
|
||||
|
||||
* **2026-06-08 (完成方案 B:C++ 原始碼修改與客製化 DEVSIM Wheel 編譯)**
|
||||
* **C++ 原始碼修改與驗證**:成功在獨立的 `devsim-dev` 環境中實作方案 B,於 `EquationHolder.cc/hh` 與 `EquationCommands.cc` 中加入了 `SetMinError` 的介面與選項註冊,將原本硬編碼的 `1.0e-10` 解放為可在 Python 端指定的參數 `min_error`。
|
||||
* **編譯挑戰排除**:解決了第三方相依庫(SuperLU `v5.2.2` API 兼容性問題),切換編譯器為 `gcc` 並引入 `QUADMATH_ARCHIVE=-lquadmath` 修正 128 位元浮點數的連結錯誤,修復了官方 `build_standalone_wheel.sh` 中未處理空白檔名的 Bug。
|
||||
* **部署與驗證**:成功編譯並打包為客製化的 `.whl` 檔案,並於虛擬環境中完成 `pip install --force-reinstall` 安裝與 Python API 驗證 (`min_error=1.0e-5` 生效)。詳細實作紀錄請參見 [devsim_min_error_implementation_notes.md](devsim_min_error_implementation_notes.md)。
|
||||
|
||||
* **2026-06-08 10:45 (確立方案 B 的架構設計原則與物理意義探討)**
|
||||
* **確立 `min_error` 的架構設計 (保持普遍適用性)**:為兼顧掌控度與 DEVSIM 既有的向後相容性,決定不修改 C++ 全域的 `1.0e-10` 預設值。而是仿效現有的 `variable_update` 參數設計,將 `min_error` 做為選填參數加入 Python 的 `devsim.equation()` 介面。如此可讓電位方程維持嚴格的 `1e-10`,而載子方程可獨立放寬至 `1e5`,完全符合 DEVSIM 方程獨立抽象化的哲學。
|
||||
* **物理意義的對接**:當載子絕對誤差遠低於室溫本質濃度 $n_i \approx 10^{10} \text{ cm}^{-3}$ 時,強求嚴格的相對誤差是無物理意義的(完全被熱雜訊掩蓋)。將載子的 `min_error` 設在 $10^5$ 量級,形同為數值求解器設定合理的「物理底噪過濾器」。
|
||||
* **Flow Control 觀察 (`log_damp` vs `positive`)**:`log_damp` 適用於指數變化(如電位),若套用於線性變化的載子,會嚴重壓縮 Newton 步長,破壞二次收斂;`positive` 則能保留完整的 Newton 步長以維持極速收斂,且僅在變數即將變負時才介入阻擋以維持物理合理性,是載子更新的最佳選擇。
|
||||
* **相對誤差運算機制**:`min_error` 在底層公式中作為分母防除零的基底:$|\Delta x| / (|x| + \text{min\_error})$。對於空乏區 $x \approx 0$ 的情況,加入客製化的 `min_error` 可避免極微小的數值雜訊 $\Delta x$ 被虛假放大為數百萬倍的相對誤差,使求解器能順暢收斂。
|
||||
|
||||
* **2026-06-07 22:07 (方案 A 測試完成與定位偽收斂,定案採取方案 B 修改 C++ 原始碼)**
|
||||
* **方案 A 測試結果與問題**:執行高壓偏壓掃描至 1000V 後,發現每步僅以 1 次迭代即判為收斂,產生的 2D 電位分布極不合理。經查為設定 `relative_error=1e30` 後,電位方程(Poisson)的相對誤差檢查被直接忽略,且其絕對殘差小於寬鬆的 `1e10` 全域上限,導致 solver 在第 0 步迭代後便草率退出(偽收斂),電位未經真正求解,P-wells 電位與 Molding 電位也都呈現不正確的分布。
|
||||
* **確定接線設定 (Wiring Configuration)**:
|
||||
* `MT2`:外接電路 `0V`。
|
||||
* `MT1`:外接電路進行高壓偏壓 `sweep`。
|
||||
* `MRING` 與 `Substrate_Bottom` (基底 leadframe):**完全不接外部電路(處於浮空狀態)**,因此在 Python 模擬中維持無 Contact 註冊狀態(自然呈現 Neumann 絕緣邊界)。
|
||||
* **定案明天執行方案 B**:
|
||||
* **目標**:修改 C++ 原始碼以在 `devsim.equation` 中增加 `min_error` 參數供 Python 呼叫。這樣 Electrons/Holes 方程可獨立使用 `min_error=1e5` 以避開空乏區載子低電位的數值雜訊;而電位方程仍保留嚴格的 `1e-10`,全域 `relative_error` 則能收緊至嚴格的 `1e-3` 或 `1e-5`,迫使 Newton 求解器進行足夠次數的迭代直至電位和載子均真正收斂。
|
||||
|
||||
* **2026-06-07 21:26 (發現 DEVSIM 空乏區載子相對誤差卡死機制與應對方案)**
|
||||
* **發現 DEVSIM C++ 相對誤差分母硬編碼限制**:經研讀 DEVSIM C++ 原始碼,在計算各方程的相對誤差時,分母防除零的基準值 `minError` 在 `Equation.cc` 中被硬編碼為 `defminError = 1.0e-10`。
|
||||
* **空乏區卡死原理**:在反向偏壓或 TVS 空乏區中,載子濃度 $n, p$ 指數衰減趨近於 0(例如 $10^{-5} \text{ cm}^{-3}$)。此時,極微小的數值更新雜訊(例如 $\Delta n \approx 10^{-7}$)在除以極小的分母($10^{-10}$)後,會被虛假放大為數千倍的相對誤差($100,000\%$)。Newton 求解器因而拒絕收斂,導致偏壓步長不斷折半卡死,此即 `positive` 更新法下低壓即震盪的核心原因。
|
||||
* **記錄應對方案 A & B**:
|
||||
* **方案 A (純 Python 數值規避 - 優先嘗試)**:在 Sweep 的 `solve` 參數中,將載子相對誤差限制放寬至 `relative_error=1e30`(實質忽略載子的相對誤差收斂判定),並同步將絕對誤差 `absolute_error` 收緊至 `1e5` 或 `1e6`,以保證高精度的電流守恆性(不產生假性漏電流)。
|
||||
* **方案 B (修改 C++ 原始碼並編譯)**:DEVSIM 的 `Equation` 類別內建有 `setMinError(DoubleType)` 接口但未暴露給 Python。我們可以修改 `EquationCommands.cc`(在 `createEquationCmd` 增加 `min_error` 選項並呼叫該接口),接著以 `CMake` 重新編譯 Python 共享庫 `.so` 置換環境。此法能讓 Potential 保留 `1e-10`,而 Electrons/Holes 方程獲取合理的 `1e5` 基準值。
|
||||
|
||||
* **2026-06-07 16:55 (重置方程式更新機制與放寬相對收斂標準)**
|
||||
* **發現 `log_damp` 更新法對線性載子濃度的數學窒礙**:在 Drift-Diffusion 掃描中,使用 `log_damp` 更新法會導致線性求解器算出的載子濃度更新量 $\Delta n$(量值在 $10^{10} \sim 10^{20}$ 之間)被底層 `LogSolutionUpdate` 強制壓縮為:
|
||||
$$\Delta n_{\text{damped}} \approx 0.0259 \times \ln\left(1 + \frac{\Delta n}{0.0259}\right) \approx 0.8\text{ cm}^{-3}$$
|
||||
這導致載子每步牛頓迭代僅被更新極微量,造成收斂速度極慢(每步相對誤差僅減小 0.2%),並陷入 5 步週期的極限環震盪而無法收斂。
|
||||
* **方程式更新法調整**:將 `ElectronContinuity` 與 `HoleContinuity` 改回標準的 `variable_update="positive"` 更新,使 Newton 求解器在載子非負的條件下能走滿 Newton 步長,恢復快速的二次收斂;將電位方程式 `PotentialEquation` 的更新改為無約束的 `variable_update="default"`,避免電位在負值區崩潰。
|
||||
* **放寬相對誤差容許度至 `1e-3` (0.1%)**:分析顯示,電極與接面邊緣的低電位節點(例如 $V \approx 10^{-6}\text{ V}$)在電位微幅調整(微伏級,如 $10^{-7}\text{ V}$)時,會因為除以系統預設的 $10^{-10}$ 底限而被虚假放大為數十百分比的「相對誤差」。將 `relative_error` 放寬至 $10^{-3}$(0.1%),既能滿足元件電學特性的高精度模擬要求,又能避免 Newton 求解器在極低數值區被數值殘差卡死,確保偏壓掃描能快速流暢前進。
|
||||
|
||||
* **2026-06-07 15:55 (系統記憶體升級與接面/介面網格集中優化)**
|
||||
* **WSL 記憶體分配升級**:由於 UMFPACK 直接求解器在 45 萬節點(118 萬方程組)下因 2D 矩陣填滿效應在 LU 分解時耗盡記憶體崩潰(OOM),檢查發現 WSL2 預設僅分配電腦 32GB 記憶體的 50%(約 15 GiB)。已在 Windows 主機使用者目錄寫入 `.wslconfig` 檔案分配 26GB 記憶體與 8GB swap,重啟 WSL 後生效。
|
||||
* **網格分布集中優化**:為徹底提升計算效能並防止記憶體溢出,對網格進行了重構:
|
||||
1. 移除了在 Silicon 表面下方 6 微米區域強制均勻切成 150 奈米細網格的 `Box` 欄位(改為 `1.5 * um` 背景過渡),以過濾非接面區域的冗餘節點。
|
||||
2. 收緊 `Threshold` 介面細化範圍至 `DistMin = 0.15 * um`, `DistMax = 1.0 * um`,將高密度網格精準集中於二氧化矽介面旁 150 奈米窄帶內。
|
||||
3. 平滑化接面背景網格(`generate_analytical_bgmesh.py`),將 `N_ref` 提高至 `2.0e17 cm^-3`。
|
||||
4. 優化後總節點數預估將從 45 萬降至 5~8 萬,單步求解速度將從數分鐘縮短至 3~5 秒,且接面/介面精度依然維持在 150nm。
|
||||
* **VTK 格式輸出支援**:修改了靜態與掃描程式,除了原有的 Tecplot `.tec` 格式外,現在會同時導出 `.vtm` / `.vtu` (XML VTK) 檔案格式。這解決了 ParaView 在讀取超大網格 Tecplot 檔案時崩潰閃退的問題,為 ParaView 提供原生、順暢的讀取。
|
||||
|
||||
* **2026-06-07 13:55 (載子收斂速度優化 - 改回 positive 更新)**
|
||||
* **改善收斂速度**:發現 `ElectronContinuityEquation` 與 `HoleContinuityEquation` 原先採用的 `log_damp` 更新法在接近收斂時速度過慢(每步 Newton 迭代僅減少約 1% 相對誤差,導致單步需要 30 次迭代)。經評估後改回 DEVSIM 標準的 `variable_update="positive"`。此法可在確保載子非負的同時走完整 Newton 步長,實現二次收斂,使每步迭代次數從 30 次大幅縮減至約 4~6 次。
|
||||
|
||||
* **2026-06-07 12:20 (最新發現 - 解決 17.24V 處的指數溢位崩潰)**
|
||||
* **定位 17.24V 溢位原因**:偏壓掃描在 17.24V 左右中斷,詳細日誌顯示在 `IntrinsicElectrons` 計算中發生了 `exp(Potential / V_t)` 浮點數溢位(當電位達到約 17.5V 時,指數值已超出雙精度浮點數極限 $1.79 \times 10^{308}$)。這是因為平衡態 Poisson 初始解中定義了依賴於電位的指數載子模型,但在高偏壓 Drift-Diffusion 掃描中已不再需要此指數關係。
|
||||
* **提出重新定義(Redefining)方案**:在 0V Poisson 求解並設定好載子初始值之後,我們在 Drift-Diffusion 求解前,將 `IntrinsicElectrons`、`IntrinsicHoles` 等模型及其導數重新定義為對應的載子變數本身(例如將 `IntrinsicElectrons` 的公式重新設定為 `Electrons` )。此舉可徹底消除空間指數電位項,防止高壓下溢位,同時使電極接觸孔的電荷模型保持物理正確與數值平滑。
|
||||
|
||||
* **2026-06-07 12:00 (最新進度 - 載子收斂與漏電流精度優化)**
|
||||
* **引入 `charge_error` 機制解決收斂發散**:分析了先前偏壓掃描在 17.2V 左右因步長不斷折半而中斷的問題。發現是由於空乏區中少數載子濃度極低,微小的數值波動引起了巨大的相對誤差,導致 Newton 求解器在 `relative_error` 上無法收斂。我們在 DC 求解中引入了 `charge_error=1e12`,此參數可令 DEVSIM 忽略濃度低於 $10^{12}\text{ cm}^{-3}$ 的節點的相對誤差檢查,從而能使用嚴格的收斂標準(`relative_error=1e-5`, `absolute_error=1e4`)順利前進。
|
||||
* **漏電流精準度修正與守恆性驗證**:先前因未設置 `charge_error`,為了能求解成功而將 `relative_error` 放寬至 `0.8` 且 `absolute_error` 設為 `1e10`,這導致了計算中出現假性的微安級漏電流且電流量不守恆(MT1 與 MT2 電流不同)。在使用嚴格的收斂參數後,成功消消除數值殘差引起的漏電假象,重現了元件在 $V < 17\text{ V}$ 下極低(且完全飽和)的真實阻斷狀態。
|
||||
|
||||
* **2026-06-06 23:15**
|
||||
* **Drift-Diffusion 絕對誤差修正**:修正了 `solve_sweep_2d.py` 中將 Drift-Diffusion 絕對收斂誤差設為 `absolute_error=1.0` 的 bug。將其調整為標準的 `1e10` 搭配 `relative_error=1e-8` 後即可順利收斂。
|
||||
* **防止節點暴增與記憶體崩潰 (OOM)**:移成了長度達 $200\ \mu\text{m}$ 的側壁介面(`silicon_molding_side_curves`)在深部的細緻化,並利用 Gmsh 的 `Restrict` 欄位將 `0.15 * um` 限制僅在 Silicon 與 Oxide 表面生效。外圍無場無載子的 Molding 區與基板深部則採用 `15.0 * um` 的粗網格。
|
||||
|
||||
---
|
||||
|
||||
## 1. 元件幾何與電極配置參數 (Validated Layout Dimensions)
|
||||
|
||||
最新確定的幾何參數(左半邊自動鏡像對稱):
|
||||
* **晶片半寬度 ($W_{DEVICE}$)**:$356\ \mu\text{m}$。
|
||||
* **模擬總半寬度 ($W_{SIM}$)**:$456\ \mu\text{m}$ (包含側邊各擴展 $100\ \mu\text{m}$ 的 Molding 區)。
|
||||
* **矽區厚度**:$200\ \mu\text{m}$ ($Y \in [0, 200]$)。
|
||||
* **二氧化矽厚度**:$2\ \mu\text{m}$ ($Y \in [-2, 0]$)。
|
||||
* **封裝膠厚度**:頂部 $100\ \mu\text{m}$ ($Y \in [-102, -2]$),側邊包覆至底部 $Y = 200\ \mu\text{m}$。
|
||||
* **P-wells** (深度 5 um):
|
||||
* `p11`:$75 \sim 100\ \mu\text{m}$
|
||||
* `p12`:$120 \sim 130\ \mu\text{m}$ (峰值 $1\times10^{17}\text{ cm}^{-3}$)
|
||||
* `p13`:$150 \sim 255\ \mu\text{m}$
|
||||
* **N+ 區域** (深度 1 um):
|
||||
* `N+`:$164 \sim 185\ \mu\text{m}$ (位於 `p13` 內,中間開口位於 $174.5\ \mu\text{m}$)
|
||||
* `MRING`:$340 \sim 356\ \mu\text{m}$ (晶片最邊緣通道阻擋環)
|
||||
* **電極與接觸孔 (Vias)**:
|
||||
* `MT1` 長板:$30 \sim 186\ \mu\text{m}$,短板:$250 \sim 295\ \mu\text{m}$。
|
||||
* `MRING` 頂部與側壁接觸:$Y = -2\ \mu\text{m}$ 平面 $340 \sim 356\ \mu\text{m}$ 及 $X = 356\ \mu\text{m}$ 側壁。
|
||||
* 底部 Leadframe Conductor Pad:$Y = 200\ \mu\text{m}$ 平面全寬。
|
||||
|
||||
---
|
||||
|
||||
## 2. 專案目錄結構 (Project Structure)
|
||||
|
||||
* `device_config.py`:幾何、電極、濃度及擴散梯度設定檔。
|
||||
* `generate_mesh_2d.py`:利用 Gmsh Python API 生成 2D 網格,已配置 Threshold 細化限制介面與接面,輸出為 `device_2d.msh`。
|
||||
* `generate_analytical_bgmesh.py`:根據 doping gradient 生成自適應接面背景網格。
|
||||
* `preview_doping_2d.py`:在 DEVSIM 中加載網格、建立摻雜模型、生成 `preview.tec` 與 `doping_2d.png`。
|
||||
* `physics/`:物理模型資料夾,包含 `new_physics.py`、`model_create.py`。
|
||||
* `solve_static_2d.py`:執行零偏壓 Poisson 模擬,輸出 `static_preview.vtm` 等 VTK 與 Tecplot 檔案。
|
||||
* `solve_sweep_2d.py`:高壓 bias sweep 主程式,具備 checkpoint 與溢位重定義機制,輸出 `sweep_preview_*` 檔案。
|
||||
|
||||
---
|
||||
|
||||
## 3. 下一步計畫 (Next Steps)
|
||||
|
||||
1. **分析 1000V 掃描結果 (I-V 曲線與 2D 電場)**
|
||||
* 查看產生的 [sweep_iv_2d.png](file:///home/pchan/devsim2026/sweep_iv_2d.png) 與 [sweep_iv_2d.csv](file:///home/pchan/devsim2026/sweep_iv_2d.csv)。
|
||||
* 在 ParaView 中載入 [sweep_preview_final.vtm](file:///home/pchan/devsim2026/sweep_preview_final.vtm),觀察在 1000V 下元件內部的空乏區擴展與電場峰值分布,確保元件在 high voltage 下沒有提前崩潰的電場集中點。
|
||||
2. **評估是否需要切換至方案 B**
|
||||
* 當前的 1000V 偏壓掃描採用 **方案 A** (放寬載子相對誤差至 `1e30`) 已成功收斂,且在 absolute tolerance $10^{10}$ 之下維持了極高精度的物理電流守恆。
|
||||
* 若未來物理模型需要更嚴格的載子相對誤差判定,可參考 **第 4 節** 修改 DEVSIM C++ 原始碼並重新編譯,以啟用 **方案 B**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 空乏區收斂與載子誤差判定方案 (方案 A & B 備忘錄)
|
||||
|
||||
### 📌 背景與問題診斷
|
||||
* **DEVSIM C++ 相對誤差分母硬編碼限制**:經研讀 DEVSIM C++ 原始碼,在計算各方程的相對誤差時,分母防除零的基準值 `minError` 在 `Equation.cc` 中被硬編碼為 `defminError = 1.0e-10`:
|
||||
`const DoubleType nrerror = n1 / (n2 + minError);` (其中 `n1` 為該節點 Newton 更新量,`n2` 為該節點變數值)。
|
||||
* **空乏區卡死原理**:在反向偏壓或 TVS 空乏區中,載子濃度 $n, p$ 指數衰減趨近於 0(例如 $10^{-5} \text{ cm}^{-3}$)。此時,極微小的數值更新雜訊(例如 $\Delta n \approx 10^{-7}$)在除以極小的分母($10^{-10}$)後,會被虛假放大為數千倍的相對誤差($100,000\%$)。Newton 求解器因而拒絕收斂,導致偏壓步長不斷折半卡死,此即 `positive` 更新法下低壓即震盪的核心原因。
|
||||
|
||||
---
|
||||
|
||||
### 💡 應對方案 A:純 Python 數值規避 (目前已採用並驗證成功)
|
||||
* **具體做法**:
|
||||
在 `solve_sweep_2d.py` 的 `devsim.solve` 呼叫中,設定:
|
||||
`devsim.solve(type="dc", absolute_error=1e10, relative_error=1e30, charge_error=1e12, ...)`
|
||||
這會使載子相對誤差限制放寬至 `1e30`(實質忽略載子的相對誤差收斂判定),並同步將絕對誤差 `absolute_error` 收緊至 `1e10`。
|
||||
* **優點**:
|
||||
* **無須編譯**:完全在 Python 層面解決,不依賴 C++ 編譯環境。
|
||||
* **極速收斂**:Newton 求解器不再受到空乏區微小載子雜訊的干擾,每步偏壓($50\text{ V}$ 步進)僅需 1 次迭代即可收斂,總掃描時間僅需 $164$ 秒。
|
||||
* **嚴格電流守恆**:由於 `absolute_error=1e10` 相當於約 $1.6 \times 10^{-9}\text{ A/cm}^2$,因此 MT1 與 MT2 之間的電流守恆差異極小(在 $0.1\text{ V}$ 時驗證為 $9.93 \times 10^{-15}\text{ A}$),無假性漏電流。
|
||||
* **缺點**:
|
||||
* 完全關閉了載子濃度的相對誤差檢查,若在某些敏感區域發生數值震盪但殘差較小,可能無法被相對誤差指標檢出。
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ 應對方案 B:修改 DEVSIM C++ 原始碼並編譯 (目前已成功實作並採用)
|
||||
為滿足後續模擬對於嚴密相對誤差收斂判定的需求,我們已於 `devsim-dev` 目錄下完成對 DEVSIM 原始碼的修改、編譯及封裝。
|
||||
|
||||
#### 1. 原始碼修改與編譯重點 (詳閱 devsim_min_error_implementation_notes.md)
|
||||
* **核心修改**:於 `EquationHolder.hh/cc` 及 `EquationCommands.cc` 暴露 `SetMinError` 介面,並將其連接至 Python API 的 `min_error` 參數。
|
||||
* **編譯修正**:降版並採用與 DEVSIM 2.0 完全相容的 SuperLU `v5.2.2`;於 CMake 中加入 `-lquadmath` 連結參數以支援擴展精度。
|
||||
* **封裝修正**:修復了 `build_standalone_wheel.sh` 中未處理含空白檔名的 Bug,成功編譯出客製化 `.whl` 檔案並安裝至虛擬環境。
|
||||
|
||||
#### 2. Python 端使用方式
|
||||
現在我們可以在建立載子方程式時,透過 Python 設定合理的相對收斂分母下限(避開空乏區極微小載子的數值雜訊干擾):
|
||||
```python
|
||||
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons", ..., min_error=1e5)
|
||||
```
|
||||
如此一來,Electrons 和 Holes 方程可獨立使用合理的 $10^5 \text{ cm}^{-3}$ 作為防除零下限,而電位方程式 `PotentialEquation` 亦補上 `1e-3` 的下限保護,完美兼顧了物理精準度與數值極速收斂。這將成為後續 BJT / MOS 元件模擬的標準最佳作法!
|
||||
|
||||
---
|
||||
|
||||
## 🔜 未來優化與待辦事項 (To-Do & Future Optimizations)
|
||||
在確認本次 1000V 掃描結果與調整 Doping Profile 之後,為了進一步加速高壓模擬並提升求解器的穩定性,預計實作以下商用 TCAD 等級的演算法優化:
|
||||
|
||||
### 1. 智慧型步長控制 (Adaptive Step-Size Control)
|
||||
* **優化目標**:改善目前無條件 `1.5` 倍放大的激進策略,減少在高壓非線性區因初始猜測值偏差過大而導致的收斂失敗。
|
||||
* **實作細節**:
|
||||
* **放大/縮減倍率調整**:成功時的放大倍率改為更溫和的 `1.26` 倍 (約 3 次翻倍);失敗時的縮減倍率改為 `0.577` 倍 (約 2 次剩 1/3),減輕乒乓震盪 (Ping-pong effect)。
|
||||
* **Iteration 次數反饋機制**:依據前一步的迭代次數 (Iterations) 決定步長策略。例如:`iters < 8` 時大膽放大;`8 <= iters <= 15` 維持原步長;`iters > 15` 時則微調縮小。
|
||||
|
||||
### 2. 混合求解策略 (Gummel Pre-conditioning)
|
||||
* **優化目標**:解決大步長或極端電壓點的初始猜測值問題,降低 Fully-Coupled 牛頓法發散的機率。
|
||||
* **實作細節**:在每次電壓推進、進入嚴格的 Fully-Coupled 求解 (例如 `relative_error=1e-3`) 之前,先以放寬標準的設定,或是利用 Python 手動控制 Gummel Iteration 迴圈跑 5~10 步當作「預處理 (Pre-conditioning)」。利用其收斂半徑大的特性取得較佳的初始解後,再交由牛頓法快速精準收尾。
|
||||
|
||||
### 3. 導入平行化多執行緒求解器 (Parallel Multi-threading Solver)
|
||||
* **優化目標**:突破預設 SuperLU 單執行緒的運算硬體瓶頸,大幅縮短巨大的稀疏矩陣 (Jacobian matrix) 求解時間。
|
||||
* **實作細節**:
|
||||
* 在開發環境中安裝 Intel MKL 數學函式庫。
|
||||
* 修改 DEVSIM 編譯設定,啟用支援多執行緒平行處理的 MKL PARDISO 求解器。
|
||||
* 利用原有的 `build_ubuntu.sh` 腳本重新編譯。預期在多核心 CPU 輔助下,矩陣求解速度可達 2~3 倍,整體模擬時間有望縮短 50% 以上。
|
||||
|
||||
### 4. 實作可控的雪崩崩潰模型 (Avalanche / Impact Ionization Model)
|
||||
* **優化目標**:在完成靜電場分佈優化後,開啟真實的物理破壞機制,驗證元件最終的精確崩潰電壓 (Breakdown Voltage, BV)。
|
||||
* **實作細節**:
|
||||
* 在 `new_physics.py` 內補上 Chynoweth 或 Selberherr 的雪崩產生率公式 ($G_{ii}$)。
|
||||
* 在主控制腳本 (如 `solve_sweep_2d.py`) 提供一個 Python Option (例如 `enable_avalanche=True/False`)。
|
||||
* 當開啟時,若元件內部的極端電場達到約 $0.3 \text{ MV/cm}$ ($300,000 \text{ V/cm}$ 或 $30 \text{ V/}\mu\text{m}$),即可觸發雪崩效應。
|
||||
@@ -0,0 +1,277 @@
|
||||
import devsim
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
import sys
|
||||
sys.path.append("/home/pchan/devsim2026")
|
||||
from device_config import *
|
||||
from physics.model_create import *
|
||||
from physics.new_physics import *
|
||||
|
||||
def run_simulation(mesh_file="device_2d.msh", tec_file="static_preview.tec", png_file="static_potential_2d.png", suffix=""):
|
||||
device = "device_2d"
|
||||
|
||||
# 1. Load the mesh
|
||||
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")
|
||||
|
||||
# Add contacts
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Si", name="MT1_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Si", name="MT2_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_P12_Si", name="MT1_P12_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_P12_Si", name="MT2_P12_Si", region="Silicon", material="metal")
|
||||
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Ox", name="MT1_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Ox", name="MT2_Ox", region="Oxide", material="metal")
|
||||
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Mold", name="MT1_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Mold", name="MT2_Mold", region="Molding", material="metal")
|
||||
|
||||
# Add interfaces
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Ox_Interface", name="Si_Ox", region0="Silicon", region1="Oxide")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Ox_Mold_Interface", name="Ox_Mold", region0="Oxide", region1="Molding")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Mold_Interface", name="Si_Mold", region0="Silicon", region1="Molding")
|
||||
|
||||
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}")
|
||||
|
||||
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")
|
||||
devsim.set_parameter(device=device, name="T", value="300")
|
||||
CreateSiliconPotentialOnly(device, "Silicon")
|
||||
|
||||
# Oxide
|
||||
if not InNodeModelList(device, "Oxide", "Potential"):
|
||||
CreateSolution(device, "Oxide", "Potential")
|
||||
devsim.set_parameter(device=device, region="Oxide", name="Permittivity", value=3.9 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, "Oxide", "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, "Oxide", "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, "Oxide", "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, "Oxide", "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region="Oxide", name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default")
|
||||
|
||||
# Molding
|
||||
if not InNodeModelList(device, "Molding", "Potential"):
|
||||
CreateSolution(device, "Molding", "Potential")
|
||||
devsim.set_parameter(device=device, region="Molding", name="Permittivity", value=4.0 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, "Molding", "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, "Molding", "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, "Molding", "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, "Molding", "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region="Molding", name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default")
|
||||
|
||||
# Interfaces
|
||||
def CreateContinuousPotentialInterface(device, interface):
|
||||
model_name = CreateContinuousInterfaceModel(device, interface, "Potential")
|
||||
devsim.interface_equation(device=device, interface=interface, name="PotentialEquation",
|
||||
interface_model=model_name, type="continuous")
|
||||
CreateContinuousPotentialInterface(device, "Si_Ox")
|
||||
CreateContinuousPotentialInterface(device, "Ox_Mold")
|
||||
CreateContinuousPotentialInterface(device, "Si_Mold")
|
||||
|
||||
# Silicon contacts
|
||||
silicon_contacts = ["MT1_Si", "MT2_Si"]
|
||||
for c in silicon_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", c)
|
||||
|
||||
devsim.set_parameter(device=device, name="MT1_P12_Si_bias", value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", "MT1_P12_Si")
|
||||
devsim.set_parameter(device=device, name="MT2_P12_Si_bias", value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", "MT2_P12_Si")
|
||||
|
||||
# Oxide contacts
|
||||
def CreateOxidePotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
oxide_contacts = ["MT1_Ox", "MT2_Ox"]
|
||||
for c in oxide_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateOxidePotentialOnlyContact(device, "Oxide", c)
|
||||
|
||||
# Molding contacts
|
||||
def CreateMoldingPotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
molding_contacts = ["MT1_Mold", "MT2_Mold"]
|
||||
for c in molding_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateMoldingPotentialOnlyContact(device, "Molding", c)
|
||||
|
||||
# Solve
|
||||
print("Solving Poisson/Laplace equations...")
|
||||
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=50)
|
||||
print("Solution converged!")
|
||||
|
||||
# Compute electric field magnitude (Emag) on elements
|
||||
for reg in ["Silicon", "Oxide", "Molding"]:
|
||||
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
|
||||
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
|
||||
|
||||
devsim.write_devices(file=tec_file, type="tecplot")
|
||||
print(f"Saved {tec_file}.")
|
||||
|
||||
# Extract data for plotting
|
||||
x_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="x")) / um
|
||||
y_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="y")) / um
|
||||
pot_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="Potential"))
|
||||
tri_si = np.array(devsim.get_element_node_list(device=device, region="Silicon"))
|
||||
emag_si = np.array(devsim.get_element_model_values(device=device, region="Silicon", name="Emag"))[::3]
|
||||
|
||||
x_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="x")) / um
|
||||
y_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="y")) / um
|
||||
pot_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="Potential"))
|
||||
tri_ox = np.array(devsim.get_element_node_list(device=device, region="Oxide"))
|
||||
emag_ox = np.array(devsim.get_element_model_values(device=device, region="Oxide", name="Emag"))[::3]
|
||||
|
||||
x_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="x")) / um
|
||||
y_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="y")) / um
|
||||
pot_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="Potential"))
|
||||
tri_mold = np.array(devsim.get_element_node_list(device=device, region="Molding"))
|
||||
emag_mold = np.array(devsim.get_element_model_values(device=device, region="Molding", name="Emag"))[::3]
|
||||
|
||||
def draw_device_boundaries(ax):
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [-T_OX/um, -T_OX/um], color='black', linestyle='--', linewidth=0.8)
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [0, 0], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([-W_DEVICE/um, -W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([W_DEVICE/um, W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([-W_SIM/um, W_SIM/um], [H_SI/um, H_SI/um], color='black', linestyle='-', linewidth=1.2)
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 14))
|
||||
|
||||
tcf1_si = ax1.tripcolor(x_si, y_si, tri_si, pot_si, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_ox = ax1.tripcolor(x_ox, y_ox, tri_ox, pot_ox, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_mold = ax1.tripcolor(x_mold, y_mold, tri_mold, pot_mold, cmap='RdYlBu_r', shading='gouraud')
|
||||
fig.colorbar(tcf1_si, ax=ax1, label='Electrostatic Potential (V)')
|
||||
draw_device_boundaries(ax1)
|
||||
ax1.set_xlabel('X (μm)')
|
||||
ax1.set_ylabel('Y (μm)')
|
||||
ax1.set_title(f'2D Electrostatic Potential at Zero Bias (Floating Bottom & MRING) {suffix}')
|
||||
ax1.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax1.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
tcf2_si = ax2.tripcolor(x_si, y_si, tri_si, facecolors=emag_si, cmap='inferno', shading='flat')
|
||||
tcf2_ox = ax2.tripcolor(x_ox, y_ox, tri_ox, facecolors=emag_ox, cmap='inferno', shading='flat')
|
||||
tcf2_mold = ax2.tripcolor(x_mold, y_mold, tri_mold, facecolors=emag_mold, cmap='inferno', shading='flat')
|
||||
fig.colorbar(tcf2_si, ax=ax2, label='Electric Field Magnitude (V/cm)')
|
||||
draw_device_boundaries(ax2)
|
||||
ax2.set_xlabel('X (μm)')
|
||||
ax2.set_ylabel('Y (μm)')
|
||||
ax2.set_title(f'2D Electric Field Magnitude at Zero Bias (Floating Bottom & MRING) {suffix}')
|
||||
ax2.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax2.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(png_file, dpi=300)
|
||||
plt.close()
|
||||
print(f"Plot saved to {png_file}")
|
||||
|
||||
return device
|
||||
|
||||
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)")
|
||||
|
||||
# 2. Extract elements and Emag
|
||||
print("Generating background mesh...")
|
||||
|
||||
# Refinement parameters
|
||||
LcMin = 0.15 * um # 0.15 um min mesh size in cm
|
||||
LcMax = 20.0 * um # 20 um max mesh size in cm
|
||||
alpha = 1.0e-3 # Scaling coefficient for Emag
|
||||
|
||||
# We will write to device_bgmesh.pos
|
||||
with open("device_bgmesh.pos", "w") as f:
|
||||
f.write('View "background mesh" {\n')
|
||||
|
||||
# Write for Silicon, Oxide, Molding regions
|
||||
for reg in ["Silicon", "Oxide", "Molding"]:
|
||||
x = np.array(devsim.get_node_model_values(device=device, region=reg, name="x"))
|
||||
y = np.array(devsim.get_node_model_values(device=device, region=reg, name="y"))
|
||||
triangles = np.array(devsim.get_element_node_list(device=device, region=reg))
|
||||
emag = np.array(devsim.get_element_model_values(device=device, region=reg, name="Emag"))[::3]
|
||||
|
||||
for i, tri in enumerate(triangles):
|
||||
# get nodes
|
||||
n0, n1, n2 = tri[0], tri[1], tri[2]
|
||||
|
||||
# get coordinates
|
||||
x0, y0 = x[n0], y[n0]
|
||||
x1, y1 = x[n1], y[n1]
|
||||
x2, y2 = x[n2], y[n2]
|
||||
|
||||
# get Emag of the element
|
||||
e_val = emag[i]
|
||||
|
||||
# Calculate target lc at this element based on Emag
|
||||
lc_val = LcMax / (1.0 + alpha * e_val)
|
||||
if lc_val < LcMin:
|
||||
lc_val = LcMin
|
||||
|
||||
# Write a Scalar Triangle (ST)
|
||||
f.write(f"ST({x0:.8e},{y0:.8e},0,{x1:.8e},{y1:.8e},0,{x2:.8e},{y2:.8e},0){{{lc_val:.8e},{lc_val:.8e},{lc_val:.8e}}};\n")
|
||||
|
||||
f.write("};\n")
|
||||
|
||||
print("Background mesh file written to device_bgmesh.pos successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_background_mesh()
|
||||
@@ -0,0 +1,256 @@
|
||||
import devsim
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from device_config import *
|
||||
from physics.model_create import *
|
||||
from physics.new_physics import *
|
||||
|
||||
device = "device_2d"
|
||||
|
||||
# 1. Load the mesh
|
||||
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
|
||||
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")
|
||||
|
||||
# Add contacts for Silicon region (MT1, MT2, and P12 virtual contacts; MRING and Substrate Bottom will float as Neumann boundaries)
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Si", name="MT1_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Si", name="MT2_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_P12_Si", name="MT1_P12_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_P12_Si", name="MT2_P12_Si", region="Silicon", material="metal")
|
||||
|
||||
# Add contacts for Oxide region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Ox", name="MT1_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Ox", name="MT2_Ox", region="Oxide", material="metal")
|
||||
|
||||
# Add contacts for Molding region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Mold", name="MT1_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Mold", name="MT2_Mold", region="Molding", material="metal")
|
||||
|
||||
# Add interfaces
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Ox_Interface", name="Si_Ox", region0="Silicon", region1="Oxide")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Ox_Mold_Interface", name="Ox_Mold", region0="Oxide", region1="Molding")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Mold_Interface", name="Si_Mold", region0="Silicon", region1="Molding")
|
||||
|
||||
devsim.finalize_mesh(mesh=device)
|
||||
devsim.create_device(mesh=device, device=device)
|
||||
# --- rest of file ---
|
||||
# Skip lines 35-124 as they are unchanged
|
||||
|
||||
|
||||
# 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}))"
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
# 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)")
|
||||
|
||||
# 3. Create solution variables and physics models
|
||||
CreateSolution(device, "Silicon", "Potential")
|
||||
devsim.set_parameter(device=device, name="T", value="300")
|
||||
CreateSiliconPotentialOnly(device, "Silicon")
|
||||
|
||||
# Oxide Potential physics setup
|
||||
def CreateOxidePotentialOnly(device, region):
|
||||
if not InNodeModelList(device, region, "Potential"):
|
||||
CreateSolution(device, region, "Potential")
|
||||
devsim.set_parameter(device=device, region=region, name="Permittivity", value=3.9 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, region, "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, region, "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, region, "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, region, "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default")
|
||||
|
||||
CreateOxidePotentialOnly(device, "Oxide")
|
||||
|
||||
# Molding Potential physics setup
|
||||
def CreateMoldingPotentialOnly(device, region):
|
||||
if not InNodeModelList(device, region, "Potential"):
|
||||
CreateSolution(device, region, "Potential")
|
||||
devsim.set_parameter(device=device, region=region, name="Permittivity", value=4.0 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, region, "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, region, "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, region, "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, region, "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default")
|
||||
|
||||
CreateMoldingPotentialOnly(device, "Molding")
|
||||
|
||||
# Interfaces (continuous electrostatic potential)
|
||||
def CreateContinuousPotentialInterface(device, interface):
|
||||
model_name = CreateContinuousInterfaceModel(device, interface, "Potential")
|
||||
devsim.interface_equation(device=device, interface=interface, name="PotentialEquation",
|
||||
interface_model=model_name, type="continuous")
|
||||
|
||||
CreateContinuousPotentialInterface(device, "Si_Ox")
|
||||
CreateContinuousPotentialInterface(device, "Ox_Mold")
|
||||
CreateContinuousPotentialInterface(device, "Si_Mold")
|
||||
|
||||
# 4. Apply contacts boundary conditions
|
||||
# Silicon contacts
|
||||
silicon_contacts = ["MT1_Si", "MT2_Si"]
|
||||
for c in silicon_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", c)
|
||||
|
||||
# P12 Virtual Silicon contacts (tied to MT1 and MT2 respectively)
|
||||
devsim.set_parameter(device=device, name="MT1_P12_Si_bias", value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", "MT1_P12_Si")
|
||||
|
||||
devsim.set_parameter(device=device, name="MT2_P12_Si_bias", value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", "MT2_P12_Si")
|
||||
|
||||
# Oxide contacts
|
||||
def CreateOxidePotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
oxide_contacts = ["MT1_Ox", "MT2_Ox"]
|
||||
for c in oxide_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateOxidePotentialOnlyContact(device, "Oxide", c)
|
||||
|
||||
# Molding contacts
|
||||
def CreateMoldingPotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
molding_contacts = ["MT1_Mold", "MT2_Mold"]
|
||||
for c in molding_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateMoldingPotentialOnlyContact(device, "Molding", c)
|
||||
|
||||
# 5. Solve Potential at equilibrium (zero bias)
|
||||
print("Solving Poisson/Laplace equations at thermal equilibrium...")
|
||||
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=50)
|
||||
print("Solution converged successfully!")
|
||||
|
||||
# Compute electric field magnitude (Emag) on elements
|
||||
for reg in ["Silicon", "Oxide", "Molding"]:
|
||||
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
|
||||
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
|
||||
|
||||
# Save the solution to static_preview.tec and static_preview.vtm
|
||||
devsim.write_devices(file="static_preview.tec", type="tecplot")
|
||||
devsim.write_devices(file="static_preview", type="vtk")
|
||||
print("Saved static_preview.tec and static_preview.vtm (VTK) for ParaView.")
|
||||
|
||||
# 6. Extract data and generate a Matplotlib plot
|
||||
print("Extracting data for plotting...")
|
||||
|
||||
# Silicon region data
|
||||
x_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="x")) / um
|
||||
y_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="y")) / um
|
||||
pot_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="Potential"))
|
||||
tri_si = np.array(devsim.get_element_node_list(device=device, region="Silicon"))
|
||||
emag_si = np.array(devsim.get_element_model_values(device=device, region="Silicon", name="Emag"))[::3]
|
||||
|
||||
# Oxide region data
|
||||
x_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="x")) / um
|
||||
y_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="y")) / um
|
||||
pot_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="Potential"))
|
||||
tri_ox = np.array(devsim.get_element_node_list(device=device, region="Oxide"))
|
||||
emag_ox = np.array(devsim.get_element_model_values(device=device, region="Oxide", name="Emag"))[::3]
|
||||
|
||||
# Molding region data
|
||||
x_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="x")) / um
|
||||
y_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="y")) / um
|
||||
pot_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="Potential"))
|
||||
tri_mold = np.array(devsim.get_element_node_list(device=device, region="Molding"))
|
||||
emag_mold = np.array(devsim.get_element_model_values(device=device, region="Molding", name="Emag"))[::3]
|
||||
|
||||
def draw_device_boundaries(ax):
|
||||
# Overlay lines for regions
|
||||
# Oxide Top: Y = -T_OX from -W_DEVICE to W_DEVICE
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [-T_OX/um, -T_OX/um], color='black', linestyle='--', linewidth=0.8)
|
||||
# Silicon-Oxide Interface: Y = 0 from -W_DEVICE to W_DEVICE
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [0, 0], color='black', linestyle='-', linewidth=0.8)
|
||||
# Silicon Die Side Boundaries: X = +-W_DEVICE from Y = 0 to H_SI
|
||||
ax.plot([-W_DEVICE/um, -W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([W_DEVICE/um, W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
# Bottom: Y = H_SI from -W_SIM to W_SIM
|
||||
ax.plot([-W_SIM/um, W_SIM/um], [H_SI/um, H_SI/um], color='black', linestyle='-', linewidth=1.2)
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 14))
|
||||
|
||||
# Plot Potential
|
||||
tcf1_si = ax1.tripcolor(x_si, y_si, tri_si, pot_si, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_ox = ax1.tripcolor(x_ox, y_ox, tri_ox, pot_ox, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_mold = ax1.tripcolor(x_mold, y_mold, tri_mold, pot_mold, cmap='RdYlBu_r', shading='gouraud')
|
||||
|
||||
fig.colorbar(tcf1_si, ax=ax1, label='Electrostatic Potential (V)')
|
||||
draw_device_boundaries(ax1)
|
||||
ax1.set_xlabel('X (μm)')
|
||||
ax1.set_ylabel('Y (μm)')
|
||||
ax1.set_title('2D Electrostatic Potential at Zero Bias (Floating Bottom & MRING)')
|
||||
ax1.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax1.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
# Plot Electric Field Magnitude (Emag)
|
||||
tcf2_si = ax2.tripcolor(x_si, y_si, tri_si, facecolors=emag_si, cmap='inferno', shading='flat')
|
||||
tcf2_ox = ax2.tripcolor(x_ox, y_ox, tri_ox, facecolors=emag_ox, cmap='inferno', shading='flat')
|
||||
tcf2_mold = ax2.tripcolor(x_mold, y_mold, tri_mold, facecolors=emag_mold, cmap='inferno', shading='flat')
|
||||
|
||||
fig.colorbar(tcf2_si, ax=ax2, label='Electric Field Magnitude (V/cm)')
|
||||
draw_device_boundaries(ax2)
|
||||
ax2.set_xlabel('X (μm)')
|
||||
ax2.set_ylabel('Y (μm)')
|
||||
ax2.set_title('2D Electric Field Magnitude at Zero Bias (Floating Bottom & MRING)')
|
||||
ax2.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax2.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('static_potential_2d.png', dpi=300)
|
||||
plt.close()
|
||||
print("Plot saved to static_potential_2d.png")
|
||||
@@ -0,0 +1,433 @@
|
||||
import devsim
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append("/home/pchan/devsim2026")
|
||||
from device_config import *
|
||||
from physics.model_create import *
|
||||
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")
|
||||
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")
|
||||
|
||||
# Add contacts for Silicon region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Si", name="MT1_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Si", name="MT2_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_P12_Si", name="MT1_P12_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_P12_Si", name="MT2_P12_Si", region="Silicon", material="metal")
|
||||
|
||||
# Add contacts for Oxide region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Ox", name="MT1_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Ox", name="MT2_Ox", region="Oxide", material="metal")
|
||||
|
||||
# Add contacts for Molding region
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Mold", name="MT1_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Mold", name="MT2_Mold", region="Molding", material="metal")
|
||||
|
||||
# Add interfaces
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Ox_Interface", name="Si_Ox", region0="Silicon", region1="Oxide")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Ox_Mold_Interface", name="Ox_Mold", region0="Oxide", region1="Molding")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Mold_Interface", name="Si_Mold", region0="Silicon", region1="Molding")
|
||||
|
||||
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}")
|
||||
|
||||
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")
|
||||
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")
|
||||
devsim.set_parameter(device=device, name="T", value="300")
|
||||
CreateSiliconPotentialOnly(device, "Silicon")
|
||||
|
||||
# Oxide potential equations
|
||||
def CreateOxidePotentialOnly(device, region):
|
||||
if not InNodeModelList(device, region, "Potential"):
|
||||
CreateSolution(device, region, "Potential")
|
||||
devsim.set_parameter(device=device, region=region, name="Permittivity", value=3.9 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, region, "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, region, "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, region, "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, region, "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default", min_error=1e-3)
|
||||
|
||||
CreateOxidePotentialOnly(device, "Oxide")
|
||||
|
||||
# Molding potential equations
|
||||
def CreateMoldingPotentialOnly(device, region):
|
||||
if not InNodeModelList(device, region, "Potential"):
|
||||
CreateSolution(device, region, "Potential")
|
||||
devsim.set_parameter(device=device, region=region, name="Permittivity", value=4.0 * 8.85e-14)
|
||||
efield = "(Potential@n0 - Potential@n1)*EdgeInverseLength"
|
||||
CreateEdgeModel(device, region, "EField", efield)
|
||||
CreateEdgeModelDerivatives(device, region, "EField", efield, "Potential")
|
||||
dfield = "Permittivity*EField"
|
||||
CreateEdgeModel(device, region, "PotentialEdgeFlux", dfield)
|
||||
CreateEdgeModelDerivatives(device, region, "PotentialEdgeFlux", dfield, "Potential")
|
||||
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
||||
edge_model="PotentialEdgeFlux", variable_update="default", min_error=1e-3)
|
||||
|
||||
CreateMoldingPotentialOnly(device, "Molding")
|
||||
|
||||
# Interfaces continuous potential
|
||||
def CreateContinuousPotentialInterface(device, interface):
|
||||
model_name = CreateContinuousInterfaceModel(device, interface, "Potential")
|
||||
devsim.interface_equation(device=device, interface=interface, name="PotentialEquation",
|
||||
interface_model=model_name, type="continuous")
|
||||
|
||||
CreateContinuousPotentialInterface(device, "Si_Ox")
|
||||
CreateContinuousPotentialInterface(device, "Ox_Mold")
|
||||
CreateContinuousPotentialInterface(device, "Si_Mold")
|
||||
|
||||
# Potential contacts setup
|
||||
silicon_contacts = ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]
|
||||
for c in silicon_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateSiliconPotentialOnlyContact(device, "Silicon", c)
|
||||
|
||||
def CreateOxidePotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
oxide_contacts = ["MT1_Ox", "MT2_Ox"]
|
||||
for c in oxide_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateOxidePotentialOnlyContact(device, "Oxide", c)
|
||||
|
||||
def CreateMoldingPotentialOnlyContact(device, region, contact):
|
||||
contact_bias = GetContactBiasName(contact)
|
||||
contact_model = f"Potential - {contact_bias}"
|
||||
contact_model_name = f"{contact}_bc"
|
||||
CreateContactNodeModel(device, contact, contact_model_name, contact_model)
|
||||
CreateContactNodeModelDerivative(device, contact, contact_model_name, contact_model, "Potential")
|
||||
devsim.contact_equation(device=device, contact=contact, name="PotentialEquation",
|
||||
node_model=contact_model_name, edge_charge_model="PotentialEdgeFlux")
|
||||
|
||||
molding_contacts = ["MT1_Mold", "MT2_Mold"]
|
||||
for c in molding_contacts:
|
||||
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
||||
CreateMoldingPotentialOnlyContact(device, "Molding", c)
|
||||
|
||||
# Solve initial zero-bias Poisson
|
||||
print("Solving initial Poisson at thermal equilibrium...")
|
||||
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=50)
|
||||
print("Initial Poisson converged.")
|
||||
|
||||
# 4. Set up carrier solutions for Silicon Drift-Diffusion
|
||||
# Compute initial guess for Electrons and Holes based on Potential
|
||||
CreateSolution(device, "Silicon", "Electrons")
|
||||
CreateSolution(device, "Silicon", "Holes")
|
||||
|
||||
devsim.set_node_values(device=device, region="Silicon", name="Electrons", init_from="IntrinsicElectrons")
|
||||
devsim.set_node_values(device=device, region="Silicon", name="Holes", init_from="IntrinsicHoles")
|
||||
|
||||
# Redefine IntrinsicElectrons, IntrinsicHoles, and related models to avoid potential exponential overflow at high bias.
|
||||
print("Redefining equilibrium models to prevent high-bias exponential overflow...")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicElectrons", equation="Electrons")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicElectrons:Potential", equation="0")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicElectrons:Electrons", equation="1")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicElectrons:Holes", equation="0")
|
||||
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicHoles", equation="Holes")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicHoles:Potential", equation="0")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicHoles:Electrons", equation="0")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicHoles:Holes", equation="1")
|
||||
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicCharge", equation="Holes - Electrons + NetDoping")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicCharge:Potential", equation="0")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicCharge:Electrons", equation="-1")
|
||||
devsim.node_model(device=device, region="Silicon", name="IntrinsicCharge:Holes", equation="1")
|
||||
|
||||
devsim.node_model(device=device, region="Silicon", name="PotentialIntrinsicCharge", equation="0")
|
||||
devsim.node_model(device=device, region="Silicon", name="PotentialIntrinsicCharge:Potential", equation="0")
|
||||
|
||||
# Mobility and drift diffusion equations
|
||||
opts = CreateAroraMobilityLF(device, "Silicon")
|
||||
# Bypassing HFMobility to prevent zero-bias convergence oscillations
|
||||
CreateSiliconDriftDiffusion(device, "Silicon", **opts)
|
||||
devsim.node_model(device=device, region="Silicon", name="LogElectrons", equation="log(Electrons + 1e-10) / log(10.0)")
|
||||
devsim.node_model(device=device, region="Silicon", name="LogHoles", equation="log(Holes + 1e-10) / log(10.0)")
|
||||
|
||||
# Re-setup Silicon contacts for Drift-Diffusion
|
||||
for c in silicon_contacts:
|
||||
CreateSiliconDriftDiffusionContact(device, "Silicon", c, opts['Jn'], opts['Jp'])
|
||||
|
||||
# Solve initial zero-bias Drift-Diffusion with standard tolerances (using default log_damp updates)
|
||||
print("Solving initial Drift-Diffusion equations at zero bias...")
|
||||
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e30, charge_error=1e12, maximum_iterations=50)
|
||||
print("Initial Drift-Diffusion converged successfully!")
|
||||
|
||||
# Switch continuity and potential equations for the bias sweep
|
||||
print("Configuring continuity and potential equations for the bias sweep (min_error=1e5, positive update)...")
|
||||
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
|
||||
time_node_model="NCharge", edge_model=opts['Jn'], 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'], 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)
|
||||
|
||||
# Save zero-bias tecplot and VTK
|
||||
devsim.write_devices(file="sweep_preview_0V.tec", type="tecplot")
|
||||
devsim.write_devices(file="sweep_preview_0V", type="vtk")
|
||||
|
||||
# 5. Define Sweep Parameters
|
||||
v_target = 1000.0
|
||||
v_current = 0.0
|
||||
step_size = 0.1 # Initial step size (V)
|
||||
max_step = 50.0 # Maximum step size (V)
|
||||
min_step = 1e-4 # Minimum step size (V)
|
||||
compliance_current = 1e-3 # 1 mA compliance current
|
||||
|
||||
# Helper functions to save/restore state in case of convergence failure
|
||||
def save_state(device):
|
||||
state = {}
|
||||
for region in ["Silicon", "Oxide", "Molding"]:
|
||||
state[region] = {
|
||||
"Potential": list(devsim.get_node_model_values(device=device, region=region, name="Potential"))
|
||||
}
|
||||
state["Silicon"]["Electrons"] = list(devsim.get_node_model_values(device=device, region="Silicon", name="Electrons"))
|
||||
state["Silicon"]["Holes"] = list(devsim.get_node_model_values(device=device, region="Silicon", name="Holes"))
|
||||
return state
|
||||
|
||||
def restore_state(device, state):
|
||||
for region in ["Silicon", "Oxide", "Molding"]:
|
||||
devsim.set_node_values(device=device, region=region, name="Potential", values=state[region]["Potential"])
|
||||
devsim.set_node_values(device=device, region="Silicon", name="Electrons", values=state["Silicon"]["Electrons"])
|
||||
devsim.set_node_values(device=device, region="Silicon", name="Holes", values=state["Silicon"]["Holes"])
|
||||
|
||||
# File logging setup
|
||||
time_log = open("simulation_time.log", "w", buffering=1)
|
||||
time_log.write("Time\tVoltage(V)\tStep(V)\tCurrent(A)\tIterations\tTimeTaken(s)\n")
|
||||
|
||||
# Arrays to store I-V data
|
||||
voltage_list = [0.0]
|
||||
current_list = [0.0]
|
||||
|
||||
# Save initial state
|
||||
state = save_state(device)
|
||||
start_sweep_time = time.time()
|
||||
|
||||
print("Beginning adaptive bias sweep...")
|
||||
step_count = 0
|
||||
|
||||
# Targets for saving intermediate state checkpoints
|
||||
save_targets = [5.0, 50.0, 500.0]
|
||||
saved_targets = set()
|
||||
|
||||
while v_current < v_target:
|
||||
v_next = min(v_current + step_size, v_target)
|
||||
|
||||
# Apply new bias values to MT1 contacts
|
||||
for c in ["MT1_Si", "MT1_P12_Si", "MT1_Ox", "MT1_Mold"]:
|
||||
devsim.set_parameter(device=device, name=f"{c}_bias", value=v_next)
|
||||
|
||||
step_start_time = time.time()
|
||||
try:
|
||||
# Solve Drift-Diffusion at next bias point with strict relative error criteria
|
||||
res = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=30, info=True)
|
||||
iters = len(res.get("iterations", []))
|
||||
|
||||
if not res.get("converged", False):
|
||||
raise devsim.error("Convergence failure")
|
||||
|
||||
step_end_time = time.time()
|
||||
time_taken = step_end_time - step_start_time
|
||||
|
||||
# Convergence succeeded! Compute current at MT1 terminal
|
||||
# MT1 terminal current is the sum of currents on MT1_Si and MT1_P12_Si
|
||||
i_n_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="ElectronContinuityEquation")
|
||||
i_p_si = devsim.get_contact_current(device=device, contact="MT1_Si", equation="HoleContinuityEquation")
|
||||
i_n_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="ElectronContinuityEquation")
|
||||
i_p_p12 = devsim.get_contact_current(device=device, contact="MT1_P12_Si", equation="HoleContinuityEquation")
|
||||
|
||||
total_curr = i_n_si + i_p_si + i_n_p12 + i_p_p12
|
||||
|
||||
# Update simulation status
|
||||
v_current = v_next
|
||||
state = save_state(device)
|
||||
|
||||
voltage_list.append(v_current)
|
||||
current_list.append(total_curr)
|
||||
|
||||
print(f"Step {step_count}: Converged at V = {v_current:.4f} V, I = {total_curr:.4e} A. Step size: {step_size:.4f} V. Iterations: {iters}. Time: {time_taken:.2f} s")
|
||||
|
||||
# Log to file
|
||||
time_log.write(f"{time.strftime('%X')}\t{v_current:.4f}\t{step_size:.4f}\t{total_curr:.4e}\t{iters}\t{time_taken:.2f}\n")
|
||||
|
||||
# Save checkpoints when crossing target voltages
|
||||
for target in save_targets:
|
||||
if v_current >= target and target not in saved_targets:
|
||||
filename = f"sweep_preview_{int(target)}V.tec"
|
||||
filename_vtk = f"sweep_preview_{int(target)}V"
|
||||
print(f"Saving checkpoint at V = {v_current:.2f} V to {filename} and VTK...")
|
||||
devsim.write_devices(file=filename, type="tecplot")
|
||||
devsim.write_devices(file=filename_vtk, type="vtk")
|
||||
saved_targets.add(target)
|
||||
|
||||
# Compliance check
|
||||
if abs(total_curr) >= compliance_current:
|
||||
print(f"Compliance current of {compliance_current:.1e} A reached at V = {v_current:.4f} V. Stopping sweep.")
|
||||
time_log.write(f"Compliance current reached at V = {v_current:.4f} V.\n")
|
||||
break
|
||||
|
||||
# Grow step size for next step
|
||||
step_size = min(step_size * 1.5, max_step)
|
||||
step_count += 1
|
||||
|
||||
except devsim.error as e:
|
||||
# Convergence failure: restore last state and cut step size
|
||||
step_end_time = time.time()
|
||||
time_taken = step_end_time - step_start_time
|
||||
print(f"Convergence failure at V = {v_next:.4f} V. Restoring state and halving step size from {step_size:.4f} V.")
|
||||
time_log.write(f"{time.strftime('%X')}\t{v_next:.4f}\t{step_size:.4f}\tFAILED\t-\t{time_taken:.2f}\n")
|
||||
|
||||
restore_state(device, state)
|
||||
step_size *= 0.5
|
||||
|
||||
if step_size < min_step:
|
||||
print("Step size has fallen below minimum limit. Aborting simulation.")
|
||||
time_log.write(f"Aborted: step size fell below {min_step:.1e} V\n")
|
||||
break
|
||||
|
||||
total_sweep_time = time.time() - start_sweep_time
|
||||
print(f"Sweep completed in {total_sweep_time:.2f} s.")
|
||||
time_log.write(f"Total Sweep Time: {total_sweep_time:.2f} s\n")
|
||||
time_log.close()
|
||||
|
||||
# 6. Save final results and generate plots
|
||||
# Save final tecplot and VTK at highest voltage
|
||||
devsim.write_devices(file="sweep_preview_final.tec", type="tecplot")
|
||||
devsim.write_devices(file="sweep_preview_final", type="vtk")
|
||||
|
||||
# Save I-V data to CSV
|
||||
np.savetxt("sweep_iv_2d.csv", np.column_stack((voltage_list, current_list)),
|
||||
header="Voltage(V),Current(A)", delimiter=",")
|
||||
|
||||
# Plot and save I-V curve
|
||||
plt.figure(figsize=(8, 6))
|
||||
plt.plot(voltage_list, np.abs(current_list), 'o-', color='#1f77b4', markersize=4)
|
||||
plt.yscale('log')
|
||||
plt.grid(True, which="both", ls="--")
|
||||
plt.xlabel("Bias Voltage (V)")
|
||||
plt.ylabel("Terminal Current Magnitude (A)")
|
||||
plt.title("TVS 2D Bidirectional Bias Sweep I-V Curve (Log Scale)")
|
||||
plt.tight_layout()
|
||||
plt.savefig("sweep_iv_2d.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
# Generate potential & electric field plots at final converged bias
|
||||
# Extract final node values
|
||||
x_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="x")) / um
|
||||
y_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="y")) / um
|
||||
pot_si = np.array(devsim.get_node_model_values(device=device, region="Silicon", name="Potential"))
|
||||
tri_si = np.array(devsim.get_element_node_list(device=device, region="Silicon"))
|
||||
|
||||
# Compute final Emag
|
||||
for reg in ["Silicon", "Oxide", "Molding"]:
|
||||
devsim.element_from_edge_model(edge_model="EField", device=device, region=reg)
|
||||
devsim.element_model(device=device, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)")
|
||||
|
||||
emag_si = np.array(devsim.get_element_model_values(device=device, region="Silicon", name="Emag"))[::3]
|
||||
|
||||
x_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="x")) / um
|
||||
y_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="y")) / um
|
||||
pot_ox = np.array(devsim.get_node_model_values(device=device, region="Oxide", name="Potential"))
|
||||
tri_ox = np.array(devsim.get_element_node_list(device=device, region="Oxide"))
|
||||
emag_ox = np.array(devsim.get_element_model_values(device=device, region="Oxide", name="Emag"))[::3]
|
||||
|
||||
x_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="x")) / um
|
||||
y_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="y")) / um
|
||||
pot_mold = np.array(devsim.get_node_model_values(device=device, region="Molding", name="Potential"))
|
||||
tri_mold = np.array(devsim.get_element_node_list(device=device, region="Molding"))
|
||||
emag_mold = np.array(devsim.get_element_model_values(device=device, region="Molding", name="Emag"))[::3]
|
||||
|
||||
def draw_device_boundaries(ax):
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [-T_OX/um, -T_OX/um], color='black', linestyle='--', linewidth=0.8)
|
||||
ax.plot([-W_DEVICE/um, W_DEVICE/um], [0, 0], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([-W_DEVICE/um, -W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([W_DEVICE/um, W_DEVICE/um], [0, H_SI/um], color='black', linestyle='-', linewidth=0.8)
|
||||
ax.plot([-W_SIM/um, W_SIM/um], [H_SI/um, H_SI/um], color='black', linestyle='-', linewidth=1.2)
|
||||
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 14))
|
||||
|
||||
# Plot Potential
|
||||
tcf1_si = ax1.tripcolor(x_si, y_si, tri_si, pot_si, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_ox = ax1.tripcolor(x_ox, y_ox, tri_ox, pot_ox, cmap='RdYlBu_r', shading='gouraud')
|
||||
tcf1_mold = ax1.tripcolor(x_mold, y_mold, tri_mold, pot_mold, cmap='RdYlBu_r', shading='gouraud')
|
||||
fig.colorbar(tcf1_si, ax=ax1, label='Electrostatic Potential (V)')
|
||||
draw_device_boundaries(ax1)
|
||||
ax1.set_xlabel('X (μm)')
|
||||
ax1.set_ylabel('Y (μm)')
|
||||
ax1.set_title(f'2D Electrostatic Potential at V = {v_current:.2f} V')
|
||||
ax1.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax1.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
# Plot EField Magnitude
|
||||
tcf2_si = ax2.tripcolor(x_si, y_si, tri_si, facecolors=emag_si, cmap='inferno', shading='flat')
|
||||
tcf2_ox = ax2.tripcolor(x_ox, y_ox, tri_ox, facecolors=emag_ox, cmap='inferno', shading='flat')
|
||||
tcf2_mold = ax2.tripcolor(x_mold, y_mold, tri_mold, facecolors=emag_mold, cmap='inferno', shading='flat')
|
||||
fig.colorbar(tcf2_si, ax=ax2, label='Electric Field Magnitude (V/cm)')
|
||||
draw_device_boundaries(ax2)
|
||||
ax2.set_xlabel('X (μm)')
|
||||
ax2.set_ylabel('Y (μm)')
|
||||
ax2.set_title(f'2D Electric Field Magnitude at V = {v_current:.2f} V')
|
||||
ax2.set_xlim(-W_SIM / um, W_SIM / um)
|
||||
ax2.set_ylim(H_SI/um + 15.0, -110.0)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("sweep_potential_2d.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
print(f"Sweep visualization plots saved: sweep_iv_2d.png and sweep_potential_2d.png.")
|
||||
@@ -0,0 +1,17 @@
|
||||
import devsim
|
||||
|
||||
device = "device_2d"
|
||||
try:
|
||||
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
|
||||
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1", name="MT1", region="Silicon", material="metal")
|
||||
devsim.finalize_mesh(mesh=device)
|
||||
devsim.create_device(mesh=device, device=device)
|
||||
|
||||
x = devsim.get_node_list(device=device, region="Silicon", name="x")
|
||||
y = devsim.get_node_list(device=device, region="Silicon", name="y")
|
||||
elements = devsim.get_element_node_list(device=device, region="Silicon")
|
||||
print(f"Nodes count: {len(x)}, Elements count: {len(elements)}")
|
||||
print("Success!")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
@@ -0,0 +1,40 @@
|
||||
import devsim
|
||||
|
||||
device = "device_2d"
|
||||
try:
|
||||
devsim.create_gmsh_mesh(mesh=device, file="device_2d.msh")
|
||||
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")
|
||||
|
||||
# Add contacts for Silicon region with distinct names
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Si", name="MT1_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Si", name="MT2_Si", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Si", name="MRING_L", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Si", name="MRING_R", region="Silicon", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="Substrate_Bottom", name="Substrate_Bottom", region="Silicon", material="metal")
|
||||
|
||||
# Add contacts for Oxide region with distinct names
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Ox", name="MT1_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Ox", name="MT2_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Ox", name="MRING_L_Ox", region="Oxide", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Ox", name="MRING_R_Ox", region="Oxide", material="metal")
|
||||
|
||||
# Add contacts for Molding region with distinct names
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT1_Mold", name="MT1_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MT2_Mold", name="MT2_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_L_Mold", name="MRING_L_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="MRING_R_Mold", name="MRING_R_Mold", region="Molding", material="metal")
|
||||
devsim.add_gmsh_contact(mesh=device, gmsh_name="Substrate_Bottom_Mold", name="Substrate_Bottom_Mold", region="Molding", material="metal")
|
||||
|
||||
# Add interfaces
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Ox_Interface", name="Si_Ox", region0="Silicon", region1="Oxide")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Ox_Mold_Interface", name="Ox_Mold", region0="Oxide", region1="Molding")
|
||||
devsim.add_gmsh_interface(mesh=device, gmsh_name="Si_Mold_Interface", name="Si_Mold", region0="Silicon", region1="Molding")
|
||||
|
||||
devsim.finalize_mesh(mesh=device)
|
||||
devsim.create_device(mesh=device, device=device)
|
||||
|
||||
print("Success!")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
Reference in New Issue
Block a user