609 lines
26 KiB
Python
609 lines
26 KiB
Python
# gui1d/solve_1d.py
|
|
import sys
|
|
import os
|
|
import numpy as np
|
|
import math
|
|
|
|
# Ensure root directory is in the path to import physics and config modules
|
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
if ROOT_DIR not in sys.path:
|
|
sys.path.append(ROOT_DIR)
|
|
|
|
import devsim
|
|
from physics.model_create import CreateSolution
|
|
from physics.new_physics import (
|
|
CreateSiliconPotentialOnly,
|
|
CreateSiliconPotentialOnlyContact,
|
|
CreateAroraMobilityLF,
|
|
CreateHFMobility,
|
|
CreateSiliconDriftDiffusion,
|
|
CreateSiliconDriftDiffusionContact,
|
|
CreateAvalancheGeneration,
|
|
CreateBTBTGeneration
|
|
)
|
|
|
|
# Vectorized complementary error function
|
|
erfc_vec = np.vectorize(math.erfc)
|
|
|
|
# --- Process Database ---
|
|
# Implant Range & Straggle in Silicon (Energy in keV, Rp and dRp in microns)
|
|
IMPLANT_DB = {
|
|
'Boron': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.04, 0.07, 0.10, 0.16, 0.24, 0.30, 0.42, 0.55]),
|
|
'dRp': np.array([0.015, 0.025, 0.035, 0.050, 0.063, 0.070, 0.085, 0.100])
|
|
},
|
|
'Phosphorus': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.015, 0.028, 0.040, 0.065, 0.100, 0.120, 0.180, 0.240]),
|
|
'dRp': np.array([0.007, 0.012, 0.016, 0.025, 0.038, 0.045, 0.060, 0.075])
|
|
},
|
|
'Arsenic': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.010, 0.018, 0.025, 0.038, 0.058, 0.070, 0.100, 0.130]),
|
|
'dRp': np.array([0.004, 0.007, 0.009, 0.014, 0.020, 0.025, 0.035, 0.045])
|
|
}
|
|
}
|
|
|
|
# Diffusion Arrhenius parameters in Silicon (D0 in cm^2/s, Ea in eV)
|
|
DIFFUSION_DB = {
|
|
'Boron': {'D0': 1.0, 'Ea': 3.46},
|
|
'Phosphorus': {'D0': 10.5, 'Ea': 3.69},
|
|
'Arsenic': {'D0': 0.32, 'Ea': 3.56}
|
|
}
|
|
|
|
|
|
def get_implant_params(dopant, energy_kev):
|
|
"""Interpolate Rp and dRp from the database for a given energy in keV."""
|
|
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
|
|
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
|
|
Rp = np.interp(e, db['energy'], db['Rp'])
|
|
dRp = np.interp(e, db['energy'], db['dRp'])
|
|
return Rp, dRp
|
|
|
|
|
|
def get_diffusion_coefficient(dopant, temp_c):
|
|
"""Calculate diffusion coefficient D in cm^2/s at temperature in °C."""
|
|
params = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
|
|
T_kelvin = temp_c + 273.15
|
|
k_B = 8.6173e-5 # eV/K
|
|
D = params['D0'] * np.exp(-params['Ea'] / (k_B * T_kelvin))
|
|
return D
|
|
|
|
|
|
def calc_donors_acceptors(x_um, process_steps, substrate_type, substrate_doping, length):
|
|
"""
|
|
Calculate the separate Donors and Acceptors concentration arrays (cm^-3)
|
|
across a position array x_um (in microns), and also return individual step profiles.
|
|
length here is total_length.
|
|
"""
|
|
donors = np.zeros_like(x_um)
|
|
acceptors = np.zeros_like(x_um)
|
|
step_profiles = []
|
|
|
|
# Calculate total epi thickness to know where substrate is
|
|
epi_thickness = sum(step.get('thickness', 0.0) for step in process_steps if step.get('enabled', True) and step.get('method') == 'Epi Growth')
|
|
substrate_thickness = length - epi_thickness
|
|
|
|
# Initialize substrate baseline
|
|
# Substrate occupies [epi_thickness, length]
|
|
if substrate_type == 'n':
|
|
donors += np.where(x_um >= epi_thickness, substrate_doping, 0.0)
|
|
else:
|
|
acceptors += np.where(x_um >= epi_thickness, substrate_doping, 0.0)
|
|
|
|
# Add background contact doping to ensure Ohmic contact at bottom (x = L)
|
|
# Contact doping thickness is 0.5 um, peak is 1e19
|
|
contact_profile = 1e19 * np.exp(-((length - x_um) / 0.5) ** 2)
|
|
if substrate_type == 'n':
|
|
donors += contact_profile
|
|
else:
|
|
acceptors += contact_profile
|
|
|
|
# We also keep Step 0 Substrate profile in step_profiles
|
|
step_profiles.append({
|
|
'name': 'Substrate (Step 0)',
|
|
'type': substrate_type,
|
|
'profile': np.where(x_um >= epi_thickness, substrate_doping, 0.0)
|
|
})
|
|
|
|
# Compute profiles for each process step
|
|
num_steps = len(process_steps)
|
|
for i, step in enumerate(process_steps):
|
|
if not step.get('enabled', True):
|
|
continue
|
|
|
|
dopant = step.get('dopant', 'Boron')
|
|
type_ = step.get('type', 'p')
|
|
method = step.get('method', 'Implant')
|
|
surface = step.get('surface', 'Top')
|
|
|
|
# Calculate cumulative Dt (in cm^2) for this step's dopant
|
|
Dt_total_cm2 = 0.0
|
|
for j in range(i, num_steps):
|
|
sub_step = process_steps[j]
|
|
if not sub_step.get('enabled', True):
|
|
continue
|
|
t_temp = sub_step.get('temp', 25.0)
|
|
t_time = sub_step.get('time', 0.0) # in minutes
|
|
if t_time > 0 and t_temp > 100.0:
|
|
D_temp = get_diffusion_coefficient(dopant, t_temp)
|
|
Dt_total_cm2 += D_temp * (t_time * 60.0)
|
|
|
|
# Convert Dt to microns^2
|
|
Dt_total_um2 = Dt_total_cm2 * 1e8
|
|
|
|
# Calculate reference position for the surface at step i.
|
|
# It is shifted by all Epi Growth steps that occur AFTER step i.
|
|
x_left = sum(sub_step.get('thickness', 0.0) for sub_step in process_steps[i+1:] if sub_step.get('enabled', True) and sub_step.get('method') == 'Epi Growth')
|
|
|
|
# Calculate profile
|
|
if method == 'Implant':
|
|
energy = step.get('energy', 80.0)
|
|
dose = step.get('dose', 1e12)
|
|
Rp, dRp = get_implant_params(dopant, energy)
|
|
dRp_eff = np.sqrt(dRp ** 2 + 2 * Dt_total_um2)
|
|
peak_conc = dose / (np.sqrt(2 * np.pi) * (dRp_eff * 1e-4)) # dose in cm^-2, dRp in cm
|
|
|
|
if surface == 'Top':
|
|
prof = peak_conc * (np.exp(-((x_um - x_left - Rp) / (np.sqrt(2) * dRp_eff)) ** 2) +
|
|
np.exp(-((x_um - x_left + Rp) / (np.sqrt(2) * dRp_eff)) ** 2))
|
|
else:
|
|
prof = peak_conc * (np.exp(-((x_um - (length - Rp)) / (np.sqrt(2) * dRp_eff)) ** 2) +
|
|
np.exp(-((x_um - (length + Rp)) / (np.sqrt(2) * dRp_eff)) ** 2))
|
|
|
|
elif method == 'Diffusion': # Constant Source Predeposition
|
|
cs = step.get('cs', 1e19)
|
|
if Dt_total_um2 <= 0.0:
|
|
Dt_total_um2 = 1e-10
|
|
|
|
if surface == 'Top':
|
|
prof = cs * erfc_vec((x_um - x_left) / (2 * np.sqrt(Dt_total_um2)))
|
|
else:
|
|
prof = cs * erfc_vec((length - x_um) / (2 * np.sqrt(Dt_total_um2)))
|
|
|
|
else: # Epi Growth
|
|
thick_val = step.get('thickness', 10.0)
|
|
cs_val = step.get('cs', 1e15)
|
|
x_right = x_left + thick_val
|
|
|
|
if Dt_total_um2 <= 0.0:
|
|
Dt_total_um2 = 1e-10
|
|
|
|
# Slab diffusion formula
|
|
prof = 0.5 * cs_val * (erfc_vec((x_um - x_right) / (2 * np.sqrt(Dt_total_um2))) -
|
|
erfc_vec((x_um - x_left) / (2 * np.sqrt(Dt_total_um2))))
|
|
|
|
# Add to Net Donors/Acceptors arrays
|
|
if type_ == 'n':
|
|
donors += prof
|
|
else:
|
|
acceptors += prof
|
|
|
|
# Save this step's final diffused profile
|
|
step_profiles.append({
|
|
'name': f"Step {i+1}: {dopant} {method}",
|
|
'type': type_,
|
|
'profile': prof
|
|
})
|
|
|
|
return donors, acceptors, step_profiles
|
|
|
|
|
|
def find_junction_depths(process_steps, substrate_type, substrate_doping, length):
|
|
"""Locate depth coordinates in microns where the net doping crosses zero."""
|
|
x_test = np.linspace(0, length, 2000)
|
|
donors, acceptors, _ = calc_donors_acceptors(x_test, process_steps, substrate_type, substrate_doping, length)
|
|
doping_test = donors - acceptors
|
|
sign_doping = np.sign(doping_test)
|
|
crossings = np.where(sign_doping[:-1] != sign_doping[1:])[0]
|
|
junctions = []
|
|
for idx in crossings:
|
|
x1, x2 = x_test[idx], x_test[idx+1]
|
|
y1, y2 = doping_test[idx], doping_test[idx+1]
|
|
if abs(y2 - y1) > 1e-20:
|
|
x_cross = x1 - y1 * (x2 - x1) / (y2 - y1)
|
|
junctions.append(x_cross)
|
|
return junctions
|
|
|
|
def build_and_solve_1d(
|
|
bias_target,
|
|
substrate_type='n',
|
|
substrate_doping=1e14,
|
|
length=30.0,
|
|
process_steps=[],
|
|
enable_avalanche=False,
|
|
enable_btbt=False,
|
|
area_cm2=1.0
|
|
):
|
|
"""
|
|
Builds a 1D Diode mesh, sets up doping, solves from 0V equilibrium
|
|
to bias_target using a rapid micro-sweep, and returns physical profiles.
|
|
"""
|
|
# Calculate total thickness including Epi Growth steps
|
|
epi_thickness = sum(step.get('thickness', 0.0) for step in process_steps if step.get('enabled', True) and step.get('method') == 'Epi Growth')
|
|
total_length = length + epi_thickness
|
|
|
|
# 1. Reset DEVSIM state to prevent name collisions
|
|
devsim.reset_devsim()
|
|
|
|
um = 1e-4 # 1 micron = 1e-4 cm
|
|
|
|
device = "device1d"
|
|
region = "Silicon"
|
|
mesh_name = "mesh1d"
|
|
|
|
# 2. Build 1D adaptive mesh
|
|
# Find junctions to refine the mesh around them
|
|
junctions = find_junction_depths(process_steps, substrate_type, substrate_doping, total_length)
|
|
|
|
# Define control points (x, target spacing)
|
|
# Higher doping -> narrower depletion region -> needs finer mesh (down to 0.5nm)
|
|
junction_spacing = max(0.0005, min(0.02, 4e15 / float(substrate_doping)))
|
|
|
|
control_points = [(0.0, 0.05)]
|
|
for j in junctions:
|
|
# Refine a symmetric 100nm region around the junction to ensure
|
|
# smooth electric field profiles even when depletion region expands under bias
|
|
control_points.append((max(0.0, j - 0.05), junction_spacing))
|
|
control_points.append((j, junction_spacing))
|
|
control_points.append((min(total_length, j + 0.05), junction_spacing))
|
|
control_points.append((total_length, 0.5)) # Coarser mesh near bottom
|
|
|
|
# Sort control points
|
|
control_points.sort(key=lambda x: x[0])
|
|
|
|
# Construct node list
|
|
all_x = [0.0]
|
|
for idx in range(len(control_points) - 1):
|
|
x_start, sp_start = control_points[idx]
|
|
x_end, sp_end = control_points[idx+1]
|
|
seg_len = x_end - x_start
|
|
if seg_len <= 0:
|
|
continue
|
|
|
|
curr_x = x_start
|
|
while curr_x < x_end:
|
|
t = (curr_x - x_start) / seg_len
|
|
spacing = sp_start + t * (sp_end - sp_start)
|
|
spacing = max(0.0005, min(2.0, spacing))
|
|
curr_x += spacing
|
|
if curr_x < x_end - 0.005:
|
|
all_x.append(curr_x)
|
|
all_x.append(x_end)
|
|
|
|
all_x = np.unique(np.array(all_x))
|
|
|
|
# Create mesh in DEVSIM
|
|
devsim.create_1d_mesh(mesh=mesh_name)
|
|
for i, x_val in enumerate(all_x):
|
|
tag = ""
|
|
if i == 0:
|
|
tag = "top"
|
|
elif i == len(all_x) - 1:
|
|
tag = "bottom"
|
|
|
|
if i == 0:
|
|
ps = all_x[1] - all_x[0]
|
|
elif i == len(all_x) - 1:
|
|
ps = all_x[-1] - all_x[-2]
|
|
else:
|
|
ps = min(all_x[i] - all_x[i-1], all_x[i+1] - all_x[i])
|
|
|
|
devsim.add_1d_mesh_line(mesh=mesh_name, pos=x_val * um, ps=ps * um, tag=tag)
|
|
|
|
devsim.add_1d_contact(mesh=mesh_name, name="top", tag="top", material="metal")
|
|
devsim.add_1d_contact(mesh=mesh_name, name="bottom", tag="bottom", material="metal")
|
|
devsim.add_1d_region(mesh=mesh_name, region=region, tag1="top", tag2="bottom", material="Silicon")
|
|
devsim.finalize_mesh(mesh=mesh_name)
|
|
|
|
devsim.create_device(mesh=mesh_name, device=device)
|
|
|
|
# 3. Setup Doping Profile models in DEVSIM
|
|
# Get actual finalized node coordinates from DEVSIM (guarantees length matching)
|
|
x_devsim = np.array(devsim.get_node_model_values(device=device, region=region, name="x")) / um
|
|
|
|
# Calculate separate donors and acceptors on the actual grid coordinates
|
|
donors_array, acceptors_array, step_profiles = calc_donors_acceptors(x_devsim, process_steps, substrate_type, substrate_doping, total_length)
|
|
|
|
# Set Donors directly
|
|
devsim.node_solution(device=device, region=region, name="Donors_data")
|
|
devsim.set_node_values(device=device, region=region, name="Donors_data", values=donors_array)
|
|
devsim.node_model(device=device, region=region, name="Donors", equation="Donors_data")
|
|
|
|
# Set Acceptors directly
|
|
devsim.node_solution(device=device, region=region, name="Acceptors_data")
|
|
devsim.set_node_values(device=device, region=region, name="Acceptors_data", values=acceptors_array)
|
|
devsim.node_model(device=device, region=region, name="Acceptors", equation="Acceptors_data")
|
|
|
|
# Define NetDoping model
|
|
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
|
|
devsim.node_model(device=device, region=region, name="x_um", equation=f"x / {um}")
|
|
|
|
devsim.set_parameter(device=device, name="top_bias", value=0.0)
|
|
devsim.set_parameter(device=device, name="bottom_bias", value=0.0)
|
|
|
|
# 4. Setup Potential-only Equilibrium equations
|
|
CreateSolution(device, region, "Potential")
|
|
CreateSiliconPotentialOnly(device, region)
|
|
CreateSiliconPotentialOnlyContact(device, region, "top")
|
|
CreateSiliconPotentialOnlyContact(device, region, "bottom")
|
|
|
|
# Solve thermal equilibrium (0V)
|
|
devsim.solve(type="dc", absolute_error=1.0, relative_error=1e-10, maximum_iterations=100)
|
|
|
|
# 5. Set up Drift-Diffusion physical models
|
|
CreateSolution(device, region, "Electrons")
|
|
CreateSolution(device, region, "Holes")
|
|
devsim.set_node_values(device=device, region=region, name="Electrons", init_from="IntrinsicElectrons")
|
|
devsim.set_node_values(device=device, region=region, name="Holes", init_from="IntrinsicHoles")
|
|
|
|
# Redefine IntrinsicElectrons, IntrinsicHoles to avoid overflow/underflow
|
|
devsim.node_model(device=device, region=region, name="IntrinsicElectrons", equation="Electrons")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Potential", equation="0")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Electrons", equation="1")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicElectrons:Holes", equation="0")
|
|
|
|
devsim.node_model(device=device, region=region, name="IntrinsicHoles", equation="Holes")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Potential", equation="0")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Electrons", equation="0")
|
|
devsim.node_model(device=device, region=region, name="IntrinsicHoles:Holes", equation="1")
|
|
|
|
arora_opts = CreateAroraMobilityLF(device, region)
|
|
hf_opts = CreateHFMobility(device, region, **arora_opts)
|
|
CreateSiliconDriftDiffusion(device, region, **hf_opts)
|
|
CreateSiliconDriftDiffusionContact(device, region, "top", hf_opts['Jn'], hf_opts['Jp'])
|
|
CreateSiliconDriftDiffusionContact(device, region, "bottom", hf_opts['Jn'], hf_opts['Jp'])
|
|
|
|
# Override equations
|
|
if enable_avalanche:
|
|
CreateAvalancheGeneration(device, region, hf_opts['Jn'], hf_opts['Jp'])
|
|
if enable_btbt:
|
|
CreateBTBTGeneration(device, region)
|
|
|
|
av_model_n = "AvalancheGeneration" if enable_avalanche else ""
|
|
av_model_p = "AvalancheGeneration_p" if enable_avalanche else ""
|
|
btbt_model_n = "BTBTGeneration" if enable_btbt else ""
|
|
btbt_model_p = "BTBTGeneration_p" if enable_btbt else ""
|
|
|
|
if av_model_n and btbt_model_n:
|
|
from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives
|
|
CreateEdgeModel(device, region, "CombinedGeneration", "AvalancheGeneration + BTBTGeneration")
|
|
CreateEdgeModel(device, region, "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p")
|
|
for i in ("Potential", "Electrons", "Holes"):
|
|
CreateEdgeModelDerivatives(device, region, "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i)
|
|
CreateEdgeModelDerivatives(device, region, "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p", i)
|
|
gen_model_n = "CombinedGeneration"
|
|
gen_model_p = "CombinedGeneration_p"
|
|
elif av_model_n:
|
|
gen_model_n = av_model_n
|
|
gen_model_p = av_model_p
|
|
elif btbt_model_n:
|
|
gen_model_n = btbt_model_n
|
|
gen_model_p = btbt_model_p
|
|
else:
|
|
gen_model_n = ""
|
|
gen_model_p = ""
|
|
|
|
# First set up equations WITHOUT generation terms (basic drift-diffusion)
|
|
devsim.equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
|
|
time_node_model="NCharge", edge_model=hf_opts['Jn'],
|
|
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
|
|
devsim.equation(device=device, region=region, name="HoleContinuityEquation", variable_name="Holes",
|
|
time_node_model="PCharge", edge_model=hf_opts['Jp'],
|
|
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
|
|
devsim.equation(device=device, region=region, name="PotentialEquation", variable_name="Potential",
|
|
node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3)
|
|
|
|
# Solve 0V basic Drift-Diffusion (highly stable)
|
|
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=100)
|
|
|
|
# Now if generation models are enabled, add them and re-solve at 0V starting from the solved state
|
|
if gen_model_n:
|
|
devsim.equation(device=device, region=region, name="ElectronContinuityEquation", variable_name="Electrons",
|
|
time_node_model="NCharge", edge_model=hf_opts['Jn'], edge_volume_model=gen_model_n,
|
|
variable_update="positive", node_model="ElectronGeneration", min_error=1e5)
|
|
devsim.equation(device=device, region=region, name="HoleContinuityEquation", variable_name="Holes",
|
|
time_node_model="PCharge", edge_model=hf_opts['Jp'], edge_volume_model=gen_model_p,
|
|
variable_update="positive", node_model="HoleGeneration", min_error=1e5)
|
|
# Re-solve at 0V to incorporate generation models smoothly
|
|
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=100)
|
|
|
|
# Check 0V depletion for punch-through
|
|
electrons_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
|
|
holes_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
|
|
doping_0 = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
|
|
is_depleted_0 = np.abs(electrons_0 - holes_0) < 0.1 * np.abs(doping_0)
|
|
dep_edges_0 = x_devsim[is_depleted_0]
|
|
if len(dep_edges_0) > 0 and np.min(dep_edges_0) <= 0.05:
|
|
v_punchthrough = 0.0
|
|
else:
|
|
v_punchthrough = None
|
|
|
|
# Calculate current density at 0V
|
|
jn_edge_0 = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
|
|
jp_edge_0 = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
|
|
jtot_avg_0 = -np.mean(jn_edge_0 + jp_edge_0) # Current flows from right to left
|
|
|
|
v_history = [0.0]
|
|
j_history = [jtot_avg_0]
|
|
|
|
# 6. Adaptive Geometric/Log Micro-Sweep execution to reach bias_target
|
|
v_current = 0.0
|
|
v_target = bias_target
|
|
adaptive_mult = 1.0
|
|
|
|
while abs(v_current) < abs(v_target):
|
|
# Calculate base step size based on current voltage
|
|
abs_v = abs(v_current)
|
|
if abs_v < 1.0:
|
|
step_size = 0.05
|
|
else:
|
|
step_size = abs_v / 20.0
|
|
|
|
# For avalanche or BTBT sweep, if current starts to rise, limit step size to 2V
|
|
if (enable_avalanche or enable_btbt) and len(j_history) > 0 and j_history[-1] > 1e-4:
|
|
step_size = min(step_size, 2.0)
|
|
|
|
# Apply adaptive multiplier (from convergence failure backtracking)
|
|
step_size *= adaptive_mult
|
|
|
|
# Halt if step size becomes non-physically tiny
|
|
if step_size < 0.005:
|
|
print(f"Step size too small ({step_size:.5f} V) at V = {v_current} V, stopping sweep.")
|
|
break
|
|
|
|
# Determine next voltage
|
|
v_next = v_current + np.sign(v_target) * step_size
|
|
if abs(v_next) > abs(v_target):
|
|
v_next = v_target
|
|
|
|
devsim.set_parameter(device=device, name="top_bias", value=0.0) # Left is reference 0V
|
|
devsim.set_parameter(device=device, name="bottom_bias", value=v_next) # Right is biased
|
|
|
|
try:
|
|
devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=40)
|
|
v_current = v_next
|
|
|
|
# Record current (flowing from right to left, which is -J)
|
|
jn_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
|
|
jp_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
|
|
j_val = -np.mean(jn_edge + jp_edge)
|
|
|
|
v_history.append(v_current)
|
|
j_history.append(j_val)
|
|
|
|
# Check for punch-through in sweep
|
|
if v_punchthrough is None:
|
|
electrons_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
|
|
holes_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
|
|
doping_sweep = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
|
|
is_depleted_sweep = np.abs(electrons_sweep - holes_sweep) < 0.1 * np.abs(doping_sweep)
|
|
dep_edges_sweep = x_devsim[is_depleted_sweep]
|
|
if len(dep_edges_sweep) > 0 and np.min(dep_edges_sweep) <= 0.05:
|
|
v_punchthrough = v_current
|
|
|
|
# Check 1mA current limit
|
|
current_ma = j_val * area_cm2 * 1000.0
|
|
if current_ma > 1.0:
|
|
print(f"Current limit 1mA reached at V = {v_current} V ({current_ma:.2f} mA). Stopping sweep.")
|
|
break
|
|
|
|
# If successful, recover the multiplier towards 1.0
|
|
adaptive_mult = min(1.0, adaptive_mult * 1.5)
|
|
|
|
except Exception as e:
|
|
print(f"Sweep convergence failure at V = {v_next} V. Backtracking with smaller step size...")
|
|
adaptive_mult /= 4.0
|
|
# Note: v_current is not updated, so the loop retries from same position with smaller step_size
|
|
|
|
# 7. Collect output profiles
|
|
x_cm = np.array(devsim.get_node_model_values(device=device, region=region, name="x"))
|
|
x_plot = x_cm / um # Convert back to um for plotting
|
|
|
|
potential = np.array(devsim.get_node_model_values(device=device, region=region, name="Potential"))
|
|
electrons = np.array(devsim.get_node_model_values(device=device, region=region, name="Electrons"))
|
|
holes = np.array(devsim.get_node_model_values(device=device, region=region, name="Holes"))
|
|
doping = np.array(devsim.get_node_model_values(device=device, region=region, name="NetDoping"))
|
|
|
|
efn = np.array(devsim.get_node_model_values(device=device, region=region, name="EFN"))
|
|
efp = np.array(devsim.get_node_model_values(device=device, region=region, name="EFP"))
|
|
ec = np.array(devsim.get_node_model_values(device=device, region=region, name="EC"))
|
|
ev = np.array(devsim.get_node_model_values(device=device, region=region, name="EV"))
|
|
|
|
efield_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name="EField"))
|
|
x_edge_mid = (x_plot[:-1] + x_plot[1:]) / 2.0
|
|
|
|
jn_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jn']))
|
|
jp_edge = np.array(devsim.get_edge_model_values(device=device, region=region, name=hf_opts['Jp']))
|
|
jtot_edge = jn_edge + jp_edge
|
|
|
|
g_av_edge = np.zeros_like(efield_edge)
|
|
if enable_avalanche or enable_btbt:
|
|
q = 1.6e-19
|
|
g_total = np.zeros_like(efield_edge)
|
|
if enable_avalanche:
|
|
try:
|
|
g_av = np.array(devsim.get_edge_model_values(device=device, region=region, name="AvalancheGeneration"))
|
|
g_total += np.abs(g_av) / q
|
|
except Exception:
|
|
pass
|
|
if enable_btbt:
|
|
try:
|
|
g_btbt = np.array(devsim.get_edge_model_values(device=device, region=region, name="BTBTGeneration"))
|
|
g_total += np.abs(g_btbt) / q
|
|
except Exception:
|
|
pass
|
|
g_av_edge = g_total
|
|
|
|
# Final current density from right to left
|
|
j_avg_rl = -np.mean(jtot_edge)
|
|
peak_efield = np.max(np.abs(efield_edge))
|
|
|
|
is_depleted = np.abs(electrons - holes) < 0.1 * np.abs(doping)
|
|
dep_edges = x_plot[is_depleted]
|
|
if len(dep_edges) > 0:
|
|
w_left = np.min(dep_edges)
|
|
w_right = np.max(dep_edges)
|
|
depletion_width = w_right - w_left
|
|
else:
|
|
w_left, w_right, depletion_width = 0.0, 0.0, 0.0
|
|
|
|
return {
|
|
"v_solved": v_current,
|
|
"x": x_plot,
|
|
"potential": potential,
|
|
"electrons": electrons,
|
|
"holes": holes,
|
|
"doping": doping,
|
|
"efn": efn,
|
|
"efp": efp,
|
|
"ec": ec,
|
|
"ev": ev,
|
|
"x_edge": x_edge_mid,
|
|
"efield": np.abs(efield_edge),
|
|
"jn": jn_edge,
|
|
"jp": jp_edge,
|
|
"jtot": jtot_edge,
|
|
"g_av": g_av_edge,
|
|
"current_density": j_avg_rl,
|
|
"peak_field": peak_efield,
|
|
"depletion_width": depletion_width,
|
|
"depletion_left": w_left,
|
|
"depletion_right": w_right,
|
|
# Sweep history
|
|
"v_history": v_history,
|
|
"j_history": j_history,
|
|
"step_profiles": step_profiles,
|
|
"v_punchthrough": v_punchthrough
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pickle
|
|
import sys
|
|
|
|
if len(sys.argv) < 3:
|
|
print("Usage: solve_1d.py <input_pickle_path> <output_pickle_path>")
|
|
sys.exit(1)
|
|
|
|
input_path = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
|
|
with open(input_path, 'rb') as f:
|
|
args = pickle.load(f)
|
|
|
|
result = build_and_solve_1d(
|
|
bias_target=args['bias_target'],
|
|
substrate_type=args['substrate_type'],
|
|
substrate_doping=args['substrate_doping'],
|
|
length=args['length'],
|
|
process_steps=args['process_steps'],
|
|
enable_avalanche=args['enable_avalanche'],
|
|
enable_btbt=args.get('enable_btbt', False),
|
|
area_cm2=args['area_cm2']
|
|
)
|
|
|
|
with open(output_path, 'wb') as f:
|
|
pickle.dump(result, f)
|