126 lines
5.9 KiB
Python
126 lines
5.9 KiB
Python
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()
|