From 4d9ac2e462d17cefc420966a10bd704335bc95bf Mon Sep 17 00:00:00 2001 From: pchang718 Date: Mon, 8 Jun 2026 21:12:01 +0800 Subject: [PATCH] chore: save local checkpoint for 1000V simulation scripts --- .gitignore | 16 + Makefile | 77 ++++ device_config.py | 65 +++ devsim_min_error_implementation_notes.md | 47 +++ generate_analytical_bgmesh.py | 125 ++++++ generate_mesh_2d.py | 348 ++++++++++++++++ legend | 1 + physics/__init__.py | 0 physics/model_create.py | 183 +++++++++ physics/new_physics.py | 484 +++++++++++++++++++++++ physics/ramp2.py | 164 ++++++++ preview_doping_2d.py | 207 ++++++++++ preview_doping_3d.py | 194 +++++++++ project_status.md | 179 +++++++++ run_refinement_2d.py | 277 +++++++++++++ solve_static_2d.py | 256 ++++++++++++ solve_sweep_2d.py | 433 ++++++++++++++++++++ test_devsim.py | 17 + test_devsim_correct.py | 40 ++ 19 files changed, 3113 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 device_config.py create mode 100644 devsim_min_error_implementation_notes.md create mode 100644 generate_analytical_bgmesh.py create mode 100644 generate_mesh_2d.py create mode 160000 legend create mode 100644 physics/__init__.py create mode 100644 physics/model_create.py create mode 100644 physics/new_physics.py create mode 100644 physics/ramp2.py create mode 100644 preview_doping_2d.py create mode 100755 preview_doping_3d.py create mode 100644 project_status.md create mode 100644 run_refinement_2d.py create mode 100644 solve_static_2d.py create mode 100644 solve_sweep_2d.py create mode 100644 test_devsim.py create mode 100644 test_devsim_correct.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41776da --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +__pycache__/ +*.pyc +.venv/ +scratch/ +last_run_outputs/ +*.vtu +*.vtm +*.visit +*.tec +*.msh +*.pos +*.png +*.log +*.csv +*.last_log +devsim-dev/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a2d5a6 --- /dev/null +++ b/Makefile @@ -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__ diff --git a/device_config.py b/device_config.py new file mode 100644 index 0000000..d88d749 --- /dev/null +++ b/device_config.py @@ -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 diff --git a/devsim_min_error_implementation_notes.md b/devsim_min_error_implementation_notes.md new file mode 100644 index 0000000..f51c912 --- /dev/null +++ b/devsim_min_error_implementation_notes.md @@ -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` 版本! diff --git a/generate_analytical_bgmesh.py b/generate_analytical_bgmesh.py new file mode 100644 index 0000000..25bfa4a --- /dev/null +++ b/generate_analytical_bgmesh.py @@ -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() diff --git a/generate_mesh_2d.py b/generate_mesh_2d.py new file mode 100644 index 0000000..d46fc41 --- /dev/null +++ b/generate_mesh_2d.py @@ -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() diff --git a/legend b/legend new file mode 160000 index 0000000..933dde2 --- /dev/null +++ b/legend @@ -0,0 +1 @@ +Subproject commit 933dde250e56848436979cb60451d1f31b22042d diff --git a/physics/__init__.py b/physics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/physics/model_create.py b/physics/model_create.py new file mode 100644 index 0000000..12a5156 --- /dev/null +++ b/physics/model_create.py @@ -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") + diff --git a/physics/new_physics.py b/physics/new_physics.py new file mode 100644 index 0000000..29cdf1b --- /dev/null +++ b/physics/new_physics.py @@ -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', + } + + + + diff --git a/physics/ramp2.py b/physics/ramp2.py new file mode 100644 index 0000000..0b5eb6d --- /dev/null +++ b/physics/ramp2.py @@ -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)) + diff --git a/preview_doping_2d.py b/preview_doping_2d.py new file mode 100644 index 0000000..ae75716 --- /dev/null +++ b/preview_doping_2d.py @@ -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") diff --git a/preview_doping_3d.py b/preview_doping_3d.py new file mode 100755 index 0000000..456ced2 --- /dev/null +++ b/preview_doping_3d.py @@ -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.") diff --git a/project_status.md b/project_status.md new file mode 100644 index 0000000..82aafcd --- /dev/null +++ b/project_status.md @@ -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}$),即可觸發雪崩效應。 diff --git a/run_refinement_2d.py b/run_refinement_2d.py new file mode 100644 index 0000000..2dad508 --- /dev/null +++ b/run_refinement_2d.py @@ -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() diff --git a/solve_static_2d.py b/solve_static_2d.py new file mode 100644 index 0000000..71a6886 --- /dev/null +++ b/solve_static_2d.py @@ -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") diff --git a/solve_sweep_2d.py b/solve_sweep_2d.py new file mode 100644 index 0000000..aa19884 --- /dev/null +++ b/solve_sweep_2d.py @@ -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.") diff --git a/test_devsim.py b/test_devsim.py new file mode 100644 index 0000000..4f62e3f --- /dev/null +++ b/test_devsim.py @@ -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}") diff --git a/test_devsim_correct.py b/test_devsim_correct.py new file mode 100644 index 0000000..18fd4a5 --- /dev/null +++ b/test_devsim_correct.py @@ -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}")