689 lines
32 KiB
Python
689 lines
32 KiB
Python
# mos_sweep.py
|
|
# 2D MOSFET Ids-Vds Curve Sweep for LDMOS
|
|
# Sweeps Vds under multiple constant Vgs values
|
|
|
|
import os
|
|
import sys
|
|
import glob
|
|
import gc
|
|
import time
|
|
import pickle
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
|
|
# Limit thread count to avoid system resource starvation
|
|
os.environ["OMP_NUM_THREADS"] = "4"
|
|
os.environ["MKL_NUM_THREADS"] = "4"
|
|
os.environ["TBB_NUM_THREADS"] = "4"
|
|
os.environ["OPENBLAS_NUM_THREADS"] = "4"
|
|
os.environ["MKL_DISABLE_FAST_MM"] = "1"
|
|
|
|
# Load Intel MKL runtime library if available
|
|
mkl_libs = glob.glob(os.path.join(os.path.dirname(sys.executable), "../lib/libmkl_rt.so.*"))
|
|
if mkl_libs:
|
|
os.environ["DEVSIM_MATH_LIBS"] = mkl_libs[0]
|
|
|
|
import devsim
|
|
|
|
# Use MKL PARDISO solver
|
|
devsim.set_parameter(name="solver_type", value="pardiso")
|
|
|
|
# Default device directory to LDMOS
|
|
DEV_DIR = os.environ.get("DEV_DIR", "devices/LDMOS")
|
|
sys.path.insert(0, os.path.abspath(DEV_DIR))
|
|
|
|
# Respect OUT_DIR env variable, defaulting to devices/LDMOS/output_yymmdd_03 based on current date
|
|
default_out_dirname = f"output_{time.strftime('%y%m%d')}_03"
|
|
OUT_DIR = os.path.join(os.environ.get("OUT_DIR", os.path.join(DEV_DIR, default_out_dirname)), "")
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
|
|
# Import geometry parameters and physics creators
|
|
from device_config import *
|
|
from physics.model_create import *
|
|
from physics.new_physics import *
|
|
|
|
device = "device_2d"
|
|
|
|
# --- 1. Load Mesh ---
|
|
mesh_file = os.path.join(DEV_DIR, "device_2d.msh")
|
|
print(f"Loading mesh: {mesh_file}...")
|
|
devsim.create_gmsh_mesh(mesh=device, file=mesh_file)
|
|
devsim.add_gmsh_region(mesh=device, gmsh_name="Silicon", region="Silicon", material="Silicon")
|
|
devsim.add_gmsh_region(mesh=device, gmsh_name="Oxide", region="Oxide", material="Oxide")
|
|
devsim.add_gmsh_region(mesh=device, gmsh_name="Molding", region="Molding", material="Molding")
|
|
|
|
# Add contacts
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="contR_Si", name="contR_Si", region="Silicon", material="metal")
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="contL_Si", name="contL_Si", region="Silicon", material="metal")
|
|
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="gate_Ox", name="gate_Ox", region="Oxide", material="metal")
|
|
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="gate_Mold", name="gate_Mold", region="Molding", material="metal")
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="contR_Mold", name="contR_Mold", region="Molding", material="metal")
|
|
devsim.add_gmsh_contact(mesh=device, gmsh_name="contL_Mold", name="contL_Mold", region="Molding", material="metal")
|
|
|
|
# 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.finalize_mesh(mesh=device)
|
|
devsim.create_device(mesh=device, device=device)
|
|
|
|
# --- 2. Setup Doping (PCAD) ---
|
|
try:
|
|
from device_pcad_config import apply_pcad_doping_2d
|
|
apply_pcad_doping_2d(device, region="Silicon")
|
|
print("Applied PCAD doping successfully.")
|
|
except ImportError:
|
|
print("Error: device_pcad_config not found. Make sure DEV_DIR points to the correct LDMOS folder.")
|
|
sys.exit(1)
|
|
|
|
# --- 3. Initialize Electrostatic Solution (Poisson) ---
|
|
CreateSolution(device, "Silicon", "Potential")
|
|
CreateSiliconPotentialOnly(device, "Silicon")
|
|
|
|
# Set temperature T on both device level and region level to prevent SetSiliconParameters overriding it back to 300K
|
|
temp_val = os.environ.get("TEMP", "300")
|
|
devsim.set_parameter(device=device, name="T", value=temp_val)
|
|
devsim.set_parameter(device=device, region="Silicon", name="T", value=temp_val)
|
|
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
# Contact Potential helper functions
|
|
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")
|
|
|
|
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")
|
|
|
|
# Potential boundary conditions on contacts
|
|
silicon_contacts = ["contL_Si", "contR_Si"]
|
|
oxide_contacts = ["gate_Ox"]
|
|
molding_contacts = ["gate_Mold", "contL_Mold", "contR_Mold"]
|
|
|
|
for c in silicon_contacts:
|
|
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
|
CreateSiliconPotentialOnlyContact(device, "Silicon", c)
|
|
|
|
for c in oxide_contacts:
|
|
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
|
CreateOxidePotentialOnlyContact(device, "Oxide", c)
|
|
|
|
for c in molding_contacts:
|
|
devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0)
|
|
CreateMoldingPotentialOnlyContact(device, "Molding", c)
|
|
|
|
print("Solving zero-bias Poisson...")
|
|
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=50)
|
|
print("Poisson converged.")
|
|
|
|
# --- 4. Setup Drift-Diffusion (DD) ---
|
|
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")
|
|
|
|
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")
|
|
|
|
opts = CreateAroraMobilityLF(device, "Silicon")
|
|
CreateSiliconDriftDiffusion(device, "Silicon", **opts)
|
|
CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp'])
|
|
|
|
for c in silicon_contacts:
|
|
CreateSiliconDriftDiffusionContact(device, "Silicon", c, opts['Jn'], opts['Jp'])
|
|
|
|
print("Solving zero-bias Drift-Diffusion...")
|
|
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e30, charge_error=1e12, maximum_iterations=50)
|
|
print("Initial Drift-Diffusion converged.")
|
|
|
|
# Set up positive update continuity equations for stable sweeping
|
|
devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons",
|
|
time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="",
|
|
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
|
|
devsim.equation(device=device, region="Silicon", name="HoleContinuityEquation", variable_name="Holes",
|
|
time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="",
|
|
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)
|
|
|
|
|
|
# --- 5. State Saving/Restoration and Sweeping Logic ---
|
|
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"])
|
|
|
|
# Keep the original zero-bias state as reference
|
|
zero_bias_state = save_state(device)
|
|
|
|
|
|
def ramp_gate_bias(device, target_vgs):
|
|
"""
|
|
Ramps gate voltage from 0.0V to target_vgs under Vds = 0.0V.
|
|
"""
|
|
# Sync bias parameters with zero_bias_state before ramping
|
|
for c in ["gate_Ox", "gate_Mold", "contR_Si", "contR_Mold", "contL_Si", "contL_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=0.0)
|
|
|
|
current_vgs = 0.0
|
|
if abs(current_vgs - target_vgs) < 1e-5:
|
|
return True
|
|
|
|
print(f"Ramping gate bias from {current_vgs:.2f}V to {target_vgs:.2f}V...")
|
|
|
|
g_step = 0.5 if target_vgs > current_vgs else -0.5
|
|
v_curr = current_vgs
|
|
|
|
while abs(v_curr - target_vgs) > 1e-5:
|
|
v_next = v_curr + g_step
|
|
if g_step > 0 and v_next > target_vgs:
|
|
v_next = target_vgs
|
|
elif g_step < 0 and v_next < target_vgs:
|
|
v_next = target_vgs
|
|
|
|
for c in ["gate_Ox", "gate_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=v_next)
|
|
|
|
try:
|
|
devsim.solve(type="dc", absolute_error=1e10, relative_error=3e-3, charge_error=1e12, maximum_iterations=20)
|
|
v_curr = v_next
|
|
except devsim.error:
|
|
# Revert to last converged bias and reduce step size
|
|
for c in ["gate_Ox", "gate_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=v_curr)
|
|
g_step *= 0.5
|
|
if abs(g_step) < 1e-3:
|
|
print("Error: Gate bias ramping failed to converge.")
|
|
return False
|
|
print(f"Gate bias successfully ramped to {target_vgs:.2f}V.")
|
|
return True
|
|
|
|
|
|
# Whitelist of variables to output for visualization.
|
|
# Unwanted variables can be commented out below to reduce file size without losing the ability to enable them later.
|
|
visual_variables = {
|
|
"x", "y",
|
|
"Potential", "Electrons", "Holes",
|
|
"NetDoping", "Donors", "Acceptors", "LogNetDoping",
|
|
"Emag", "logEmag",
|
|
"Jmag", "logJmag",
|
|
"AvalancheGeneration_mag", "logAvalancheGen",
|
|
|
|
# --- Vector components (commented out by default, uncomment if needed) ---
|
|
# "EField_x", "EField_y",
|
|
# "Jn_x", "Jn_y",
|
|
# "Jp_x", "Jp_y",
|
|
|
|
# --- Geometry/Volume metrics (commented out by default, uncomment if needed) ---
|
|
# "NodeVolume", "ElementNodeVolume", "ElementEdgeCouple",
|
|
|
|
# --- Derivative/Jacobian variables (commented out by default to save space/speed up loading) ---
|
|
# "Potential:Potential@n0", "Potential:Potential@n1",
|
|
# "Electrons:Electrons@n0", "Electrons:Electrons@n1",
|
|
# "Holes:Holes@n0", "Holes:Holes@n1",
|
|
# "AvalancheGeneration:Electrons@n0", "AvalancheGeneration:Electrons@n1",
|
|
# "AvalancheGeneration:Holes@n0", "AvalancheGeneration:Holes@n1",
|
|
# "AvalancheGeneration:Potential@n0", "AvalancheGeneration:Potential@n1",
|
|
}
|
|
|
|
|
|
def save_visualization(device, vgs, vds, prefix):
|
|
# Calculate current density components on elements for visualization
|
|
for reg in ["Silicon"]:
|
|
jn_name = opts['Jn']
|
|
jp_name = opts['Jp']
|
|
devsim.element_from_edge_model(edge_model=jn_name, device=device, region=reg)
|
|
devsim.element_from_edge_model(edge_model=jp_name, device=device, region=reg)
|
|
devsim.element_model(device=device, region=reg, name="Jn_x", equation=f"{jn_name}_x")
|
|
devsim.element_model(device=device, region=reg, name="Jn_y", equation=f"{jn_name}_y")
|
|
devsim.element_model(device=device, region=reg, name="Jp_x", equation=f"{jp_name}_x")
|
|
devsim.element_model(device=device, region=reg, name="Jp_y", equation=f"{jp_name}_y")
|
|
devsim.element_model(device=device, region=reg, name="Jmag",
|
|
equation="((Jn_x + Jp_x)^2 + (Jn_y + Jp_y)^2)^(0.5)")
|
|
devsim.element_model(device=device, region=reg, name="logJmag",
|
|
equation="log(Jmag + 1e-20) / log(10.0)")
|
|
|
|
devsim.element_from_edge_model(edge_model="AvalancheGeneration", device=device, region=reg)
|
|
devsim.element_model(device=device, region=reg, name="AvalancheGeneration_raw",
|
|
equation="(AvalancheGeneration_x^2 + AvalancheGeneration_y^2)^(0.5)")
|
|
devsim.element_model(device=device, region=reg, name="AvalancheGeneration_mag",
|
|
equation="(AvalancheGeneration_raw > 1e-20) * AvalancheGeneration_raw")
|
|
devsim.element_model(device=device, region=reg, name="logAvalancheGen",
|
|
equation="log(AvalancheGeneration_mag + 1e-40) / log(10.0)")
|
|
|
|
# Define placeholder variables in non-conducting regions to prevent Tecplot column mismatch in ParaView
|
|
for reg in ["Oxide", "Molding"]:
|
|
devsim.element_model(device=device, region=reg, name="Jn_x", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="Jn_y", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="Jp_x", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="Jp_y", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="Jmag", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="logJmag", equation="-20.0")
|
|
devsim.element_model(device=device, region=reg, name="AvalancheGeneration_raw", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="AvalancheGeneration_mag", equation="0.0")
|
|
devsim.element_model(device=device, region=reg, name="logAvalancheGen", equation="-40.0")
|
|
|
|
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.element_model(device=device, region=reg, name="logEmag",
|
|
equation="log(Emag + 1e-20) / log(10.0)")
|
|
|
|
# Save tecplot file
|
|
tec_file = os.path.join(OUT_DIR, f"mos_sweep_vgs_{vgs:.1f}V_vds_{prefix}.tec")
|
|
devsim.write_devices(file=tec_file, type="tecplot", include_test=lambda x: x in visual_variables)
|
|
print(f"Saved 2D field visualization to {tec_file}")
|
|
|
|
# Save VTK XML files (.vtm/.vtu) (commented out to keep output folder clean)
|
|
# orig_cwd = os.getcwd()
|
|
# try:
|
|
# os.chdir(OUT_DIR)
|
|
# vtk_file = f"mos_sweep_vgs_{vgs:.1f}V_vds_{prefix}"
|
|
# devsim.write_devices(file=vtk_file, type="vtk", include_test=lambda x: x in visual_variables)
|
|
# print(f"Saved 2D VTK field visualization to {os.path.join(OUT_DIR, vtk_file)}.vtm")
|
|
# finally:
|
|
# os.chdir(orig_cwd)
|
|
|
|
|
|
def sweep_vds(device, vgs, vds_max):
|
|
"""
|
|
Sweeps Vds from 0.0V to vds_max while keeping Vgs constant.
|
|
"""
|
|
print(f"Starting Vds sweep up to Vds_max = {vds_max:.2f}V for Vgs = {vgs:.2f}V...")
|
|
|
|
log_path = os.path.join(OUT_DIR, "simulation_time.log")
|
|
file_exists = os.path.exists(log_path)
|
|
time_log = open(log_path, "a", buffering=1)
|
|
if not file_exists:
|
|
time_log.write("Time\tVoltage(V)\tStep(V)\tCurrent(A)\tIav_eval(A)\tIterations\tTimeTaken(s)\tMemStage1\tMemStage2\n")
|
|
|
|
# Check if there is an existing seed checkpoint to resume from (checked in descending Vds order)
|
|
resume_data = None
|
|
for checkpoint in [60.0, 30.0, 10.0]:
|
|
seed_path = os.path.join(OUT_DIR, f"seed_vgs_{vgs:.1f}V_vds_{int(checkpoint)}V.pkl")
|
|
if os.path.exists(seed_path):
|
|
try:
|
|
with open(seed_path, "rb") as f:
|
|
resume_data = pickle.load(f)
|
|
print(f"Found seed checkpoint file: {seed_path}")
|
|
print(f"Successfully loaded state and resuming sweep from Vds = {checkpoint:.1f}V...")
|
|
break
|
|
except Exception as e:
|
|
print(f"Warning: Failed to load seed file {seed_path}: {e}")
|
|
|
|
if resume_data:
|
|
v_curr = resume_data["voltage"]
|
|
step_size = resume_data["step_size"]
|
|
vds_list = list(resume_data["vds_list"])
|
|
ids_list = list(resume_data["ids_list"])
|
|
iav_list = list(resume_data["iav_list"])
|
|
# Restore node values
|
|
restore_state(device, resume_data["state"])
|
|
# Restore contact bias parameters in DEVSIM
|
|
for c in ["contR_Si", "contR_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=v_curr)
|
|
for c in ["gate_Ox", "gate_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=vgs)
|
|
step_state = save_state(device)
|
|
else:
|
|
vds_list = [0.0]
|
|
# Compute initial current at Vds = 0
|
|
i_n_si = devsim.get_contact_current(device=device, contact="contR_Si", equation="ElectronContinuityEquation")
|
|
i_p_si = devsim.get_contact_current(device=device, contact="contR_Si", equation="HoleContinuityEquation")
|
|
ids_list = [i_n_si + i_p_si]
|
|
|
|
g_av_init = devsim.get_edge_model_values(device=device, region="Silicon", name="AvalancheGeneration")
|
|
edge_vol_init = devsim.get_edge_model_values(device=device, region="Silicon", name="EdgeNodeVolume")
|
|
init_av = 2.0 * sum(g * v for g, v in zip(g_av_init, edge_vol_init))
|
|
iav_list = [init_av]
|
|
|
|
v_curr = 0.0
|
|
step_size = 0.05 # Start with small step size to capture the linear region smoothly
|
|
step_state = save_state(device)
|
|
|
|
max_step = max(1.0, min(50.0, vds_max / 10.0)) # Dynamic limit for high voltage sweeps
|
|
min_step = 1e-4
|
|
|
|
consecutive_fails = 0
|
|
step_ctl_reduce = 12
|
|
step_ctl_enlarge = 8
|
|
iter_history = []
|
|
sn = 0
|
|
|
|
while v_curr < vds_max:
|
|
v_next = v_curr + step_size
|
|
# Force exact hits on checkpoint targets (e.g. 10V, 30V, 40V, 60V)
|
|
for target in [10.0, 30.0, 40.0, 60.0]:
|
|
if v_curr < target <= v_next:
|
|
v_next = target
|
|
break
|
|
v_next = min(v_next, vds_max)
|
|
|
|
# Apply new Vds to drain contacts
|
|
for c in ["contR_Si", "contR_Mold"]:
|
|
devsim.set_parameter(device=device, name=f"{c}_bias", value=v_next)
|
|
|
|
t_start = time.time()
|
|
try:
|
|
# Stage 1: Pre-conditioning
|
|
iters1 = 10
|
|
try:
|
|
res1 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-1, charge_error=1e12, maximum_iterations=10, info=True)
|
|
iters1 = len(res1.get("iterations", []))
|
|
except devsim.error:
|
|
pass
|
|
|
|
import psutil
|
|
mem_stage1 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
|
|
# Stage 2: Precision Newton
|
|
res = devsim.solve(type="dc", absolute_error=1e10, relative_error=3e-3, charge_error=1e12, maximum_iterations=15, info=True)
|
|
iters2 = len(res.get("iterations", []))
|
|
total_iters = iters2
|
|
|
|
mem_stage2 = psutil.Process(os.getpid()).memory_info().rss / (1024**3)
|
|
|
|
if not res.get("converged", False):
|
|
raise devsim.error("Convergence failure")
|
|
|
|
# Convergence succeeded! Extract currents
|
|
i_n = devsim.get_contact_current(device=device, contact="contR_Si", equation="ElectronContinuityEquation")
|
|
i_p = devsim.get_contact_current(device=device, contact="contR_Si", equation="HoleContinuityEquation")
|
|
total_curr = i_n + i_p
|
|
|
|
# Integrated ionization current (avalanche onset evaluation)
|
|
g_av = devsim.get_edge_model_values(device=device, region="Silicon", name="AvalancheGeneration")
|
|
edge_vol = devsim.get_edge_model_values(device=device, region="Silicon", name="EdgeNodeVolume")
|
|
total_av_curr = 2.0 * sum(g * v for g, v in zip(g_av, edge_vol))
|
|
|
|
v_curr = v_next
|
|
step_state = save_state(device)
|
|
|
|
vds_list.append(v_curr)
|
|
ids_list.append(total_curr)
|
|
iav_list.append(total_av_curr)
|
|
|
|
t_taken = time.time() - t_start
|
|
print(f" Vds = {v_curr:.3f}V, Ids = {total_curr:.4e}A, I_av_eval = {total_av_curr:.4e}A (step: {step_size:.3f}V, iters: {iters1}+{iters2})")
|
|
time_log.write(f"{time.strftime('%X')}\t{v_curr:.4f}\t{step_size:.4f}\t{total_curr:.4e}\t{total_av_curr:.4e}\t{iters1}+{iters2}\t{t_taken:.2f}\t{mem_stage1:.1f}GB\t{mem_stage2:.1f}GB\n")
|
|
|
|
# Save visual checkpoints at exactly 40.0V and 60.0V
|
|
if abs(v_curr - 40.0) < 1e-5:
|
|
save_visualization(device, vgs, v_curr, "40.0V")
|
|
elif abs(v_curr - 60.0) < 1e-5:
|
|
save_visualization(device, vgs, v_curr, "60.0V")
|
|
|
|
# Save state seed files (.pkl) at exactly 10.0V, 30.0V, and 60.0V
|
|
for cp in [10.0, 30.0, 60.0]:
|
|
if abs(v_curr - cp) < 1e-5:
|
|
seed_path = os.path.join(OUT_DIR, f"seed_vgs_{vgs:.1f}V_vds_{int(cp)}V.pkl")
|
|
seed_data = {
|
|
"voltage": v_curr,
|
|
"step_size": step_size,
|
|
"vds_list": vds_list,
|
|
"ids_list": ids_list,
|
|
"iav_list": iav_list,
|
|
"state": save_state(device)
|
|
}
|
|
with open(seed_path, "wb") as f:
|
|
pickle.dump(seed_data, f)
|
|
print(f"Saved state seed checkpoint at Vds = {v_curr:.1f}V to {seed_path}")
|
|
|
|
# Adaptive step control
|
|
consecutive_fails = 0
|
|
iter_history.append(total_iters)
|
|
if len(iter_history) > 3:
|
|
iter_history.pop(0)
|
|
avg_iters = sum(iter_history) / len(iter_history)
|
|
|
|
eff_iters = max(0.0, avg_iters - sn)
|
|
if avg_iters > step_ctl_reduce:
|
|
step_size = max(step_size * 0.5, min_step)
|
|
sn = 0
|
|
elif eff_iters < step_ctl_enlarge:
|
|
step_size = min(step_size * 1.5, max_step)
|
|
sn = 0
|
|
else:
|
|
sn += 1.25
|
|
|
|
gc.collect()
|
|
|
|
except devsim.error:
|
|
consecutive_fails += 1
|
|
attempted_step = step_size
|
|
shrink_factor = 0.3 if consecutive_fails == 1 else 0.1
|
|
step_size *= shrink_factor
|
|
t_taken = time.time() - t_start
|
|
print(f" Convergence failure at Vds = {v_next:.3f}V. Reverting state and cutting step size to {step_size:.4f}V")
|
|
time_log.write(f"{time.strftime('%X')}\t{v_next:.4f}\t{attempted_step:.4f}\tFAILED\t-\t{t_taken:.2f}\n")
|
|
|
|
restore_state(device, step_state)
|
|
iter_history = [step_ctl_reduce, step_ctl_reduce, step_ctl_reduce]
|
|
sn = 0
|
|
|
|
if step_size < min_step:
|
|
print(" Step size fell below minimum limit. Aborting sweep for this Vgs.")
|
|
break
|
|
|
|
time_log.close()
|
|
return np.array(vds_list), np.array(ids_list), np.array(iav_list)
|
|
|
|
|
|
# --- 6. Run Sweeps ---
|
|
# Configurable Vgs list and max Vds
|
|
#vgs_list = [0.0, 1.0, 2.0, 5.0, 10.0, 15.0]
|
|
vgs_list = [0.0, 2.0, 5.0, 10.0]
|
|
vds_max = 60.0
|
|
|
|
print(f"MOS Sweep Configuration:")
|
|
print(f" Vgs List: {vgs_list}")
|
|
print(f" Vds Sweep Range: 0.0V to {vds_max}V")
|
|
print(f" Output Directory: {OUT_DIR}")
|
|
|
|
sweep_results_active = {}
|
|
|
|
for vgs in vgs_list:
|
|
print(f"\n========================================")
|
|
print(f"Processing Vgs = {vgs:.2f}V")
|
|
print(f"========================================")
|
|
|
|
# Restore to clean zero-bias state
|
|
restore_state(device, zero_bias_state)
|
|
|
|
# Ramp gate to target Vgs
|
|
success = ramp_gate_bias(device, vgs)
|
|
if not success:
|
|
print(f"Failed to ramp gate bias to Vgs = {vgs}V. Skipping this sweep.")
|
|
continue
|
|
|
|
# Sweep Vds
|
|
vds_vals, ids_vals, iav_vals = sweep_vds(device, vgs, vds_max)
|
|
sweep_results_active[vgs] = (vds_vals, ids_vals, iav_vals)
|
|
|
|
# Save CSV for this Vgs
|
|
csv_file = os.path.join(OUT_DIR, f"mos_sweep_vgs_{vgs:.1f}V.csv")
|
|
np.savetxt(csv_file, np.column_stack((vds_vals, ids_vals, iav_vals)),
|
|
header="Vds(V),Ids(A),Iav_eval(A)", delimiter=",", comments="")
|
|
print(f"Saved Vds-Ids-Iav data for Vgs = {vgs:.1f}V to {csv_file}")
|
|
|
|
# Calculate current density components on elements for visualization
|
|
# Save final visual outputs using our global function
|
|
save_visualization(device, vgs, vds_vals[-1], "final_vds")
|
|
|
|
|
|
# --- 7. Plotting and Global Data Save ---
|
|
# Populate final sweep_results by combining active run and disk files
|
|
all_vgs_targets = sorted(list(set(vgs_list + [0.0, 1.0, 2.0, 5.0, 10.0, 15.0])))
|
|
sweep_results = {}
|
|
for v in all_vgs_targets:
|
|
if v in sweep_results_active:
|
|
sweep_results[v] = sweep_results_active[v]
|
|
else:
|
|
csv_file = os.path.join(OUT_DIR, f"mos_sweep_vgs_{v:.1f}V.csv")
|
|
if os.path.exists(csv_file):
|
|
try:
|
|
data = np.loadtxt(csv_file, delimiter=",", skiprows=1)
|
|
if data.ndim == 2:
|
|
if data.shape[1] == 3:
|
|
sweep_results[v] = (data[:, 0], data[:, 1], data[:, 2])
|
|
elif data.shape[1] == 2:
|
|
sweep_results[v] = (data[:, 0], data[:, 1], np.zeros(len(data[:, 0])))
|
|
except Exception as e:
|
|
print(f"Warning: Could not load {csv_file}: {e}")
|
|
|
|
if not sweep_results:
|
|
print("Warning: No sweep results found to plot or combine.")
|
|
else:
|
|
# Combine all curves to a single combined CSV file
|
|
combined_csv_file = os.path.join(OUT_DIR, "mos_sweep_all_curves.csv")
|
|
with open(combined_csv_file, "w") as f_combined:
|
|
# Write header
|
|
headers = []
|
|
for vgs in sorted(sweep_results.keys()):
|
|
headers.append(f"Vds_Vgs_{vgs:.1f}V(V)")
|
|
headers.append(f"Ids_Vgs_{vgs:.1f}V(A)")
|
|
headers.append(f"Iav_eval_Vgs_{vgs:.1f}V(A)")
|
|
f_combined.write(",".join(headers) + "\n")
|
|
|
|
# Find max length to align columns
|
|
max_len = max(len(v[0]) for v in sweep_results.values())
|
|
for idx in range(max_len):
|
|
row_cells = []
|
|
for vgs in sorted(sweep_results.keys()):
|
|
vds_vals, ids_vals, iav_vals = sweep_results[vgs]
|
|
if idx < len(vds_vals):
|
|
row_cells.append(f"{vds_vals[idx]:.6e}")
|
|
row_cells.append(f"{ids_vals[idx]:.6e}")
|
|
row_cells.append(f"{iav_vals[idx]:.6e}")
|
|
else:
|
|
row_cells.append("")
|
|
row_cells.append("")
|
|
row_cells.append("")
|
|
f_combined.write(",".join(row_cells) + "\n")
|
|
|
|
print(f"\nSaved combined curve data to {combined_csv_file}")
|
|
|
|
# Plot 1: Standard Linear Scale Plot (Ids in mA)
|
|
plt.figure(figsize=(10, 7))
|
|
for vgs in sorted(sweep_results.keys()):
|
|
vds_vals, ids_vals, _ = sweep_results[vgs]
|
|
plt.plot(vds_vals, ids_vals * 1e3, 'o-', markersize=3, label=f"Vgs = {vgs:.1f} V")
|
|
plt.grid(True, which="both", linestyle="--", alpha=0.5)
|
|
plt.xlabel("Drain-Source Voltage Vds (V)", fontsize=12)
|
|
plt.ylabel("Drain-Source Current Ids (mA)", fontsize=12)
|
|
plt.title("LDMOS 2D Ids vs Vds Output Characteristics", fontsize=14, fontweight="bold")
|
|
plt.legend(loc="upper left", fontsize=10)
|
|
plt.tight_layout()
|
|
plot_path = os.path.join(OUT_DIR, "mos_sweep_iv_curves.png")
|
|
plt.savefig(plot_path, dpi=300)
|
|
plt.close()
|
|
print(f"Saved linear visualization plot to {plot_path}")
|
|
|
|
# Plot 2: Logarithmic Scale Plot (Ids and Iav_eval in Amperes)
|
|
plt.figure(figsize=(10, 7))
|
|
for vgs in sorted(sweep_results.keys()):
|
|
vds_vals, ids_vals, iav_vals = sweep_results[vgs]
|
|
line, = plt.plot(vds_vals, np.abs(ids_vals), '-', linewidth=1.5, label=f"Vgs = {vgs:.1f} V")
|
|
plt.plot(vds_vals, np.abs(iav_vals), '--', color=line.get_color(), linewidth=1.2)
|
|
|
|
# Add dummy entries for line style legend
|
|
plt.plot([], [], 'k-', label='Total Ids (Solid)')
|
|
plt.plot([], [], 'k--', label='Iav_eval (Dashed)')
|
|
|
|
plt.yscale('log')
|
|
plt.ylim(1e-14, 1.0) # Limit range to see leakage and active region clearly
|
|
plt.grid(True, which="both", linestyle="--", alpha=0.5)
|
|
plt.xlabel("Drain-Source Voltage Vds (V)", fontsize=12)
|
|
plt.ylabel("Current Magnitude (A)", fontsize=12)
|
|
plt.title("LDMOS 2D Ids & Integrated Ionization Current (Log Scale)", fontsize=14, fontweight="bold")
|
|
plt.legend(loc="lower right", fontsize=10)
|
|
plt.tight_layout()
|
|
plot_path_log = os.path.join(OUT_DIR, "mos_sweep_iv_curves_log.png")
|
|
plt.savefig(plot_path_log, dpi=300)
|
|
plt.close()
|
|
print(f"Saved log visualization plot to {plot_path_log}")
|
|
|
|
# Run speed plotting script
|
|
log_path = os.path.join(OUT_DIR, "simulation_time.log")
|
|
speed_png = os.path.join(OUT_DIR, "simulation_speed.png")
|
|
if os.path.exists(log_path):
|
|
print(f"Plotting simulation speed to {speed_png}...")
|
|
import os; os.system(f'{sys.executable} plot_speed.py {log_path} {speed_png}')
|
|
|
|
print("\nMOS Sweep finished successfully!")
|