import devsim import os import shutil import pickle import numpy as np import sys sys.path.append("/home/pchan/devsim2026") from device_config import * from physics.model_create import * from physics.new_physics import * import generate_mesh_2d OUT_DIR = "output_this_run/" def setup_physics_for_device(device, is_avalanche_enabled=False, is_btbt_enabled=False): 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") # 1. Add GMSH region, contacts, and interfaces try: 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) except devsim.error as e: if "must not be finalized" in str(e) or "already exists" in str(e): pass else: raise e try: devsim.create_device(mesh=device, device=device) except devsim.error as e: if "already exists" in str(e): pass else: raise e # 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. Create solutions CreateSolution(device, "Silicon", "Potential") CreateSolution(device, "Silicon", "Electrons") CreateSolution(device, "Silicon", "Holes") sim_temp = os.environ.get("TEMP", "300") devsim.set_parameter(device=device, name="T", value=sim_temp) 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) 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 = ["MT1_Mold", "MT2_Mold"] for c in molding_contacts: devsim.set_parameter(device=device, name=GetContactBiasName(c), value=0.0) CreateMoldingPotentialOnlyContact(device, "Molding", c) # Redefine IntrinsicElectrons, IntrinsicHoles, and related models to avoid potential 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") # 全耦合 Drift-Diffusion 系統(Arora mobility + 連續方程式) opts = CreateAroraMobilityLF(device, "Silicon") CreateSiliconDriftDiffusion(device, "Silicon", **opts) # Drift diffusion contacts for c in silicon_contacts: CreateSiliconDriftDiffusionContact(device, "Silicon", c, opts['Jn'], opts['Jp']) # Avalanche generation model (enabled/disabled by refine_and_interpolate caller) if is_avalanche_enabled: CreateAvalancheGeneration(device, "Silicon", opts['Jn'], opts['Jp']) # BTBT generation model if is_btbt_enabled: CreateBTBTGeneration(device, "Silicon") av_model_n = "AvalancheGeneration" if is_avalanche_enabled else "" av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else "" btbt_model_n = "BTBTGeneration" if is_btbt_enabled else "" btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else "" if av_model_n and btbt_model_n: from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives CreateEdgeModel(device, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration") CreateEdgeModel(device, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p") for i in ("Potential", "Electrons", "Holes"): CreateEdgeModelDerivatives(device, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i) CreateEdgeModelDerivatives(device, "Silicon", "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 = "" # 預設以 positive (full Newton) 方式註冊連續方程式 # refine_and_interpolate 在插值後會臨時切換至 log_damp 做 Stage 1 預處理 devsim.equation(device=device, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons", time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_model_n, 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=gen_model_p, variable_update="positive", node_model="HoleGeneration", min_error=1e5) 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)") devsim.equation(device=device, region="Silicon", name="PotentialEquation", variable_name="Potential", node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3) # Setup E_mag_log element models 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="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)") return opts def solve_decoupled_carriers(device_name, opts): # We temporarily delete PotentialEquation to solve carrier equations individually for reg in ["Silicon", "Oxide", "Molding"]: try: devsim.delete_equation(device=device_name, region=reg, name="PotentialEquation") except Exception: pass for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si", "MT1_Ox", "MT2_Ox", "MT1_Mold", "MT2_Mold"]: try: devsim.delete_contact_equation(device=device_name, contact=c, name="PotentialEquation") except Exception: pass for i in ["Si_Ox", "Ox_Mold", "Si_Mold"]: try: devsim.delete_interface_equation(device=device_name, interface=i, name="PotentialEquation") except Exception: pass # Delete HoleContinuityEquation before solving ElectronContinuityEquation try: devsim.delete_equation(device=device_name, region="Silicon", name="HoleContinuityEquation") except Exception: pass for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: try: devsim.delete_contact_equation(device=device_name, contact=c, name="HoleContinuityEquation") except Exception: pass # Solve ElectronContinuityEquation alone (linear in Electrons) print(" [Gummel] Solving Electron Continuity Equation alone...") devsim.equation(device=device_name, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons", time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model="", variable_update="default", node_model="ElectronGeneration", min_error=1e5) for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: contact_electrons_name = f"{c}nodeelectrons" devsim.contact_equation(device=device_name, contact=c, name="ElectronContinuityEquation", node_model=contact_electrons_name, edge_current_model=opts['Jn']) try: devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=15, info=True) except Exception as e: print(f" [Gummel] Electron solve warning: {e}") devsim.delete_equation(device=device_name, region="Silicon", name="ElectronContinuityEquation") for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: devsim.delete_contact_equation(device=device_name, contact=c, name="ElectronContinuityEquation") # Solve HoleContinuityEquation alone (linear in Holes) print(" [Gummel] Solving Hole Continuity Equation alone...") devsim.equation(device=device_name, region="Silicon", name="HoleContinuityEquation", variable_name="Holes", time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model="", variable_update="default", node_model="HoleGeneration", min_error=1e5) for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: contact_holes_name = f"{c}nodeholes" devsim.contact_equation(device=device_name, contact=c, name="HoleContinuityEquation", node_model=contact_holes_name, edge_current_model=opts['Jp']) try: devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-5, charge_error=1e12, maximum_iterations=15, info=True) except Exception as e: print(f" [Gummel] Hole solve warning: {e}") devsim.delete_equation(device=device_name, region="Silicon", name="HoleContinuityEquation") for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: devsim.delete_contact_equation(device=device_name, contact=c, name="HoleContinuityEquation") def enforce_contact_boundary_conditions(device_name): import math print("[Boundary] Enforcing exact contact boundary conditions to prevent numerical shock...") # Silicon Contacts silicon_contacts = ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"] try: net_doping = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="NetDoping")) vt = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="V_t")) nie = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="NIE")) pot = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="Potential")) elec = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="Electrons")) holes = np.array(devsim.get_node_model_values(device=device_name, region="Silicon", name="Holes")) changed_count = 0 for c in silicon_contacts: bias_name = f"{c}_bias" try: bias_val = devsim.get_parameter(device=device_name, name=bias_name) except Exception: bias_val = 0.0 try: nodes_list = devsim.get_element_node_list(device=device_name, region="Silicon", contact=c) contact_nodes = sorted(list(set(n for elem in nodes_list for n in elem))) except Exception as e_nodes: print(f" Warning: could not get nodes for contact {c}: {e_nodes}") continue for node in contact_nodes: doping = net_doping[node] ni = nie[node] v_t = vt[node] # Calculate celec and chole celec = 1e-10 + 0.5 * abs(doping + math.sqrt(doping**2 + 4.0 * ni**2)) chole = 1e-10 + 0.5 * abs(-doping + math.sqrt(doping**2 + 4.0 * ni**2)) # Calculate equilibrium values if doping > 0: elec_eq = celec holes_eq = (ni**2) / chole pot_eq = bias_val + v_t * math.log(celec / ni) else: elec_eq = (ni**2) / chole holes_eq = chole pot_eq = bias_val - v_t * math.log(chole / ni) pot[node] = pot_eq elec[node] = elec_eq holes[node] = holes_eq changed_count += 1 if changed_count > 0: devsim.set_node_values(device=device_name, region="Silicon", name="Potential", values=list(pot)) devsim.set_node_values(device=device_name, region="Silicon", name="Electrons", values=list(elec)) devsim.set_node_values(device=device_name, region="Silicon", name="Holes", values=list(holes)) print(f" Enforced boundary values on {changed_count} Silicon contact nodes.") except Exception as ex: print(f" Error enforcing Silicon contact boundary conditions: {ex}") # Oxide and Molding Contacts for reg, contacts in [("Oxide", ["MT1_Ox", "MT2_Ox"]), ("Molding", ["MT1_Mold", "MT2_Mold"])]: try: pot = np.array(devsim.get_node_model_values(device=device_name, region=reg, name="Potential")) changed_count = 0 for c in contacts: bias_name = f"{c}_bias" try: bias_val = devsim.get_parameter(device=device_name, name=bias_name) except Exception: bias_val = 0.0 try: nodes_list = devsim.get_element_node_list(device=device_name, region=reg, contact=c) contact_nodes = sorted(list(set(n for elem in nodes_list for n in elem))) except Exception: continue for node in contact_nodes: pot[node] = bias_val changed_count += 1 if changed_count > 0: devsim.set_node_values(device=device_name, region=reg, name="Potential", values=list(pot)) print(f" Enforced boundary values on {changed_count} {reg} contact nodes.") except Exception as ex: print(f" Error enforcing {reg} contact boundary conditions: {ex}") def refine_and_interpolate(device_old, v_bias, is_avalanche_enabled=False, is_btbt_enabled=False, time_log=None, out_dir=None): if out_dir is None: out_dir = OUT_DIR import time refine_start_time = time.time() device_new_name = f"device_refined_{int(v_bias)}V" print(f"\n--- DYNAMIC REFINE & INTERPOLATE at {v_bias:.2f} V ---") # 1. 確保舊元件中已計算出電場 for reg in ["Silicon", "Oxide", "Molding"]: devsim.element_from_edge_model(edge_model="EField", device=device_old, region=reg) devsim.element_model(device=device_old, region=reg, name="Emag", equation="(EField_x^2 + EField_y^2)^(0.5)") devsim.element_model(device=device_old, region=reg, name="E_mag_log", equation="asinh(Emag / 2.0) / log(10.0)") # 取得 Silicon 中各節點之座標與電場 x_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="x")) y_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="y")) # 由於 Emag 是一個 element model,我們手動將其平均到各節點上 emag_elements = np.array(devsim.get_element_model_values(device=device_old, region="Silicon", name="Emag")) triangles = devsim.get_element_node_list(device=device_old, region="Silicon") emag_si = np.zeros(len(x_si)) emag_count = np.zeros(len(x_si)) for i, tri in enumerate(triangles): n0, n1, n2 = tri[0], tri[1], tri[2] emag_si[n0] += emag_elements[3*i] emag_si[n1] += emag_elements[3*i+1] emag_si[n2] += emag_elements[3*i+2] emag_count[n0] += 1 emag_count[n1] += 1 emag_count[n2] += 1 emag_si = np.where(emag_count > 0, emag_si / emag_count, 0.0) # 2. 計算空乏區最深邊界 (E > 2e4 V/cm) dep_nodes = [y_val for y_val, e_val in zip(y_si, emag_si) if e_val > 2.0e4] y_dep = max(dep_nodes) if dep_nodes else 15.0 * um # 預測加密外推 10 µm (Guard Band) y_box_max = min(y_dep + 10.0 * um, H_SI) y_medium_max = min(y_dep + 30.0 * um, H_SI) print(f"Detected depletion max depth: {y_dep / um:.2f} um. Dynamic Box encryption boundary set to: {y_box_max / um:.2f} um, medium boundary set to: {y_medium_max / um:.2f} um.") # 3. 動態寫出 bgmesh.pos LcMin = 0.15 * um LcMax = 20.0 * um alpha = 1.0e-3 bgmesh_pos_path = f"{out_dir}device_bgmesh_refined.pos" with open(bgmesh_pos_path, "w") as f: f.write('View "background mesh" {\n') for reg in ["Silicon", "Oxide"]: x = np.array(devsim.get_node_model_values(device=device_old, region=reg, name="x")) y = np.array(devsim.get_node_model_values(device=device_old, region=reg, name="y")) triangles = np.array(devsim.get_element_node_list(device=device_old, region=reg)) emag = np.array(devsim.get_element_model_values(device=device_old, region=reg, name="Emag"))[::3] for i, tri in enumerate(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] e_val = emag[i] # 計算 lc_val lc_val = LcMax / (1.0 + alpha * e_val) if lc_val < LcMin: lc_val = LcMin 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") # 4. 動態調用 Gmsh 重建網格 mesh_out_path = f"{out_dir}device_2d_refined.msh" generate_mesh_2d.create_mesh(y_box_max=y_box_max, y_medium_max=y_medium_max, mesh_out=mesh_out_path, bgmesh_pos=bgmesh_pos_path) # 5. 載入新網格並設定物理與 solutions devsim.create_gmsh_mesh(mesh=device_new_name, file=mesh_out_path) opts = setup_physics_for_device(device_new_name, is_avalanche_enabled=is_avalanche_enabled, is_btbt_enabled=is_btbt_enabled) # 6. Apply bias to contacts of the new device for c in ["MT1_Si", "MT1_P12_Si", "MT1_Ox", "MT1_Mold"]: devsim.set_parameter(device=device_new_name, name=f"{c}_bias", value=v_bias) for c in ["MT2_Si", "MT2_P12_Si", "MT2_Ox", "MT2_Mold"]: devsim.set_parameter(device=device_new_name, name=f"{c}_bias", value=0.0) # 7. Interpolate solutions from old device using scipy.interpolate.griddata print("Interpolating solutions to the refined mesh using scipy...") from scipy.interpolate import griddata # 7.1 Gather global old coordinates and potential to ensure 100% continuous interface potential interpolation x_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="x")) y_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="y")) pot_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="Potential")) x_old_ox = np.array(devsim.get_node_model_values(device=device_old, region="Oxide", name="x")) y_old_ox = np.array(devsim.get_node_model_values(device=device_old, region="Oxide", name="y")) pot_old_ox = np.array(devsim.get_node_model_values(device=device_old, region="Oxide", name="Potential")) x_old_mold = np.array(devsim.get_node_model_values(device=device_old, region="Molding", name="x")) y_old_mold = np.array(devsim.get_node_model_values(device=device_old, region="Molding", name="y")) pot_old_mold = np.array(devsim.get_node_model_values(device=device_old, region="Molding", name="Potential")) points_old_global = np.column_stack(( np.concatenate((x_old_si, x_old_ox, x_old_mold)), np.concatenate((y_old_si, y_old_ox, y_old_mold)) )) pot_old_global = np.concatenate((pot_old_si, pot_old_ox, pot_old_mold)) # 7.2 Interpolate Potential to all regions of the new refined mesh using the global dataset for reg in ["Silicon", "Oxide", "Molding"]: x_new = np.array(devsim.get_node_model_values(device=device_new_name, region=reg, name="x")) y_new = np.array(devsim.get_node_model_values(device=device_new_name, region=reg, name="y")) points_new = np.column_stack((x_new, y_new)) pot_new = griddata(points_old_global, pot_old_global, points_new, method='linear') if np.isnan(pot_new).any(): pot_new_nearest = griddata(points_old_global, pot_old_global, points_new, method='nearest') pot_new = np.where(np.isnan(pot_new), pot_new_nearest, pot_new) devsim.set_node_values(device=device_new_name, region=reg, name="Potential", values=list(pot_new)) # 7.3 Volume-weighted conservative interpolation of Silicon carriers # Strategy: Instead of log-space re-sampling (Boltzmann-consistent but not charge-conserving), # use volume-weighted barycentric interpolation to conserve total charge from M0 to M1. # Formula: n_j = Σ_i (λ_i * n_i * V_i^M0) / Σ_i (λ_i * V_i^M0) # where λ_i are barycentric coords of M1 node j within M0 triangle, # and V_i^M0 are the Voronoi volumes of M0 nodes. print("Interpolating Electrons and Holes (log-space + global charge scaling)...") points_old_si = np.column_stack((x_old_si, y_old_si)) x_new_si = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="x")) y_new_si = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="y")) points_new_si = np.column_stack((x_new_si, y_new_si)) # Get Voronoi volumes from M0 (BEFORE deleting old device) V_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="NodeVolume")) # Build Delaunay triangulation of M0 Silicon nodes from scipy.spatial import Delaunay tri_si = Delaunay(points_old_si) def log_space_interp(val_old, tri, points_new): """Log-space barycentric interpolation for Boltzmann consistency.""" log_val_old = np.log(np.maximum(val_old, 1e-40)) # Find containing simplex for each M1 node simplex_idx = tri.find_simplex(points_new) # Compute barycentric coordinates T = tri.transform # shape (N_simplices, ndim, ndim) r = points_new - T[simplex_idx, 2] # translate to simplex origin bary_partial = np.einsum('nij,nj->ni', T[simplex_idx, :2], r) # (N_new, 2) bary = np.concatenate([bary_partial, 1.0 - bary_partial.sum(axis=1, keepdims=True)], axis=1) # (N_new, 3) bary = np.clip(bary, 0.0, 1.0) bary /= bary.sum(axis=1, keepdims=True) # Vertex indices of each simplex verts = tri.simplices[simplex_idx] # (N_new, 3) log_val_verts = log_val_old[verts] # (N_new, 3) result_log = (bary * log_val_verts).sum(axis=1) # Handle M1 nodes outside M0 convex hull (simplex_idx == -1) outside = simplex_idx < 0 if outside.any(): from scipy.interpolate import griddata result_nearest = griddata(points_old_si, log_val_old, points_new[outside], method='nearest') result_log[outside] = result_nearest return np.exp(result_log) # Fetch old carrier values and new/old NodeVolumes to perform and verify interpolation elec_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="Electrons")) holes_old_si = np.array(devsim.get_node_model_values(device=device_old, region="Silicon", name="Holes")) V_new_si = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="NodeVolume")) # Perform log-space interpolation electrons_interp = log_space_interp(elec_old_si, tri_si, points_new_si) holes_interp = log_space_interp(holes_old_si, tri_si, points_new_si) # Scale interpolated concentrations globally to enforce exact charge conservation Q_m0_n = float(np.dot(elec_old_si, V_old_si)) Q_m0_p = float(np.dot(holes_old_si, V_old_si)) Q_before_n = float(np.dot(electrons_interp, V_new_si)) Q_before_p = float(np.dot(holes_interp, V_new_si)) if Q_before_n > 0: electrons_interp *= (Q_m0_n / Q_before_n) if Q_before_p > 0: holes_interp *= (Q_m0_p / Q_before_p) print(f" Interpolation (before scaling) — Electrons ratio: {Q_before_n/Q_m0_n:.6f} (M0: {Q_m0_n:.4e}, M1: {Q_before_n:.4e})") print(f" Interpolation (before scaling) — Holes ratio: {Q_before_p/Q_m0_p:.6f} (M0: {Q_m0_p:.4e}, M1: {Q_before_p:.4e})") print(f" After scaling charge conservation — Electrons ratio: {float(np.dot(electrons_interp, V_new_si))/Q_m0_n:.6f}") print(f" After scaling charge conservation — Holes ratio: {float(np.dot(holes_interp, V_new_si))/Q_m0_p:.6f}") def slope_preserving_conservative_smooth(n, V, pts, tri_m0, N_passes=5, alpha=0.3): """ Slope-preserving, charge-conserving smoothing (user's 11.3-11.4 algorithm). For each edge (i,j) in the Delaunay triangulation: 1. Estimate per-node local gradient [gx, gy] via inverse-distance-weighted LSQ 2. dn_expected = gradient_midpoint · (r_j - r_i) (what the slope predicts) 3. residual = (n_j - n_i) - dn_expected (deviation from local trend) 4. flux = alpha * residual * V_i*V_j/(V_i+V_j) (antisymmetric → conserved) Properties: - Exactly charge-conserving: Σ flux = 0 for each edge - Slope-preserving: if n follows local gradient, residual=0, no flux - Reduces 2nd-order deviations (peaks/valleys above/below local trend) - At depletion boundary: n_C=5e16 is ABOVE linear extrapolation → reduced toward A """ n = n.copy().astype(np.float64) N = len(n) # Build unique edge list from M0 Delaunay (used for M1 gradient; M1 tri not yet built) # For M1 smoothing we need M1 edges. Build M1 Delaunay from pts. from scipy.spatial import Delaunay as Delaunay_local tri_m1 = Delaunay_local(pts) edge_set = set() for s in tri_m1.simplices: a, b, c = s[0], s[1], s[2] edge_set.add((min(a,b), max(a,b))) edge_set.add((min(b,c), max(b,c))) edge_set.add((min(a,c), max(a,c))) ei_arr = np.array([e[0] for e in edge_set], dtype=np.int64) ej_arr = np.array([e[1] for e in edge_set], dtype=np.int64) dx = pts[ej_arr, 0] - pts[ei_arr, 0] dy = pts[ej_arr, 1] - pts[ei_arr, 1] d = np.sqrt(dx**2 + dy**2) inv_d = 1.0 / np.maximum(d, 1e-30) print(f" [smooth] M1 edges: {len(ei_arr)}, nodes: {N}, passes: {N_passes}, alpha: {alpha}") for pass_idx in range(N_passes): dn = n[ej_arr] - n[ei_arr] # actual concentration difference along each edge # --- Gradient estimation at each node via weighted LSQ --- # From edge (i,j): at node i, neighbor j is at (dx, dy) with value difference dn # gx_i * dx + gy_i * dy ≈ dn # From edge (i,j): at node j, neighbor i is at (-dx,-dy) with value difference -dn # gx_j * (-dx) + gy_j * (-dy) ≈ -dn → same normal equations (symmetric) w = inv_d A11 = np.bincount(ei_arr, w*dx**2, N) + np.bincount(ej_arr, w*dx**2, N) A12 = np.bincount(ei_arr, w*dx*dy, N) + np.bincount(ej_arr, w*dx*dy, N) A22 = np.bincount(ei_arr, w*dy**2, N) + np.bincount(ej_arr, w*dy**2, N) B1 = np.bincount(ei_arr, w*dx*dn, N) + np.bincount(ej_arr, w*dx*dn, N) B2 = np.bincount(ei_arr, w*dy*dn, N) + np.bincount(ej_arr, w*dy*dn, N) det = A11*A22 - A12**2 safe = np.abs(det) > 1e-60 det_s = np.where(safe, det, 1.0) gx = np.where(safe, ( A22*B1 - A12*B2) / det_s, 0.0) gy = np.where(safe, (-A12*B1 + A11*B2) / det_s, 0.0) # --- Edge-level gradient-corrected flux --- gx_mid = 0.5 * (gx[ei_arr] + gx[ej_arr]) gy_mid = 0.5 * (gy[ei_arr] + gy[ej_arr]) dn_expected = gx_mid * dx + gy_mid * dy # residual > 0 → n_j is HIGHER than gradient predicts → j should give charge to i residual = dn - dn_expected # Harmonic-volume weighted flux (antisymmetric → charge conserved) V_harm = V[ei_arr] * V[ej_arr] / (V[ei_arr] + V[ej_arr]) flux = alpha * residual * V_harm # charge [cm^-3 * cm^3] # i gains flux, j loses flux (antisymmetric) delta_charge = (np.bincount(ei_arr, flux, N) + np.bincount(ej_arr, -flux, N)) n = np.clip(n + delta_charge / V, 1e-40, None) return n # Apply slope-preserving conservative smooth on M1 grid # Bypassed because log-space interpolation is Boltzmann-consistent and inherently smooth. # Smoothing in linear space destroys the exponential carrier profiles across PN junctions. print(" Bypassing slope-preserving conservative smooth (log-space interpolation is Boltzmann-consistent)...") # Set on new mesh devsim.set_node_values(device=device_new_name, region="Silicon", name="Electrons", values=list(electrons_interp)) devsim.set_node_values(device=device_new_name, region="Silicon", name="Holes", values=list(holes_interp)) print(" Log-space interpolated and scaled Electrons/Holes set on new mesh.") enforce_contact_boundary_conditions(device_new_name) # 8. Destroy old device to release memory BEFORE solving print("Deleting old device to avoid simultaneous solving in DEVSIM...") devsim.delete_device(device=device_old) print("Old device deleted.") av_model = "AvalancheGeneration" if is_avalanche_enabled else "" # ========================================== # Stage 0.5: Nonlinear Poisson pre-solve with frozen carriers & Debye screening # ========================================== print("[Stage 0.5] Running nonlinear Poisson pre-solve with Debye screening...") # 1. Store reference solutions for Debye screening pot_ref_si = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="Potential")) devsim.node_solution(device=device_new_name, region="Silicon", name="Electrons_ref") devsim.node_solution(device=device_new_name, region="Silicon", name="Holes_ref") devsim.node_solution(device=device_new_name, region="Silicon", name="Potential_ref") devsim.set_node_values(device=device_new_name, region="Silicon", name="Electrons_ref", values=list(electrons_interp)) devsim.set_node_values(device=device_new_name, region="Silicon", name="Holes_ref", values=list(holes_interp)) devsim.set_node_values(device=device_new_name, region="Silicon", name="Potential_ref", values=list(pot_ref_si)) # 2. Temporarily replace PotentialNodeCharge with nonlinear Gummel-style Poisson model devsim.node_model(device=device_new_name, region="Silicon", name="dV_gummel", equation="(Potential - Potential_ref) / V_t") devsim.node_model(device=device_new_name, region="Silicon", name="dV_gummel_clipped", equation="ifelse(dV_gummel > 50, 50, ifelse(dV_gummel < -50, -50, dV_gummel))") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge", equation="-q * (Holes_ref * exp(-dV_gummel_clipped) - Electrons_ref * exp(dV_gummel_clipped) + NetDoping)") devsim.node_model(device=device_new_name, region="Silicon", name="dV_gummel_clipped:Potential", equation="ifelse(dV_gummel > 50, 0, ifelse(dV_gummel < -50, 0, 1 / V_t))") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Potential", equation="-q * (-Holes_ref * exp(-dV_gummel_clipped) * dV_gummel_clipped:Potential - Electrons_ref * exp(dV_gummel_clipped) * dV_gummel_clipped:Potential)") # Since carrier equations are deleted, the derivatives w.r.t Electrons and Holes are set to 0 devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Electrons", equation="0") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Holes", equation="0") # 3. Delete continuity equations from Silicon region and contacts devsim.delete_equation(device=device_new_name, region="Silicon", name="ElectronContinuityEquation") devsim.delete_equation(device=device_new_name, region="Silicon", name="HoleContinuityEquation") for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: devsim.delete_contact_equation(device=device_new_name, contact=c, name="ElectronContinuityEquation") devsim.delete_contact_equation(device=device_new_name, contact=c, name="HoleContinuityEquation") # Temporarily set PotentialEquation variable update to log_damp devsim.equation(device=device_new_name, region="Silicon", name="PotentialEquation", variable_name="Potential", node_model="PotentialNodeCharge", edge_model="DField", variable_update="log_damp", min_error=1e-3) iters_s0 = 0 iters_s1 = 0 iters_s2 = 0 try: # Print Potential statistics before Poisson solve for reg in ["Silicon", "Oxide", "Molding"]: pot = np.array(devsim.get_node_model_values(device=device_new_name, region=reg, name="Potential")) print(f" [pre-solve] Potential in {reg} before solve: min={pot.min():.2e}, max={pot.max():.2e}, mean={pot.mean():.2e}") # 4. Solve nonlinear Poisson equation. We use a relaxed tolerance (2.0) and 6 iterations to ensure successful convergence and update. print(" Solving nonlinear Poisson with Debye screening...") res_s0 = devsim.solve(type="dc", absolute_error=1e10, relative_error=2.0, charge_error=1e12, maximum_iterations=6, info=True) iters_s0 = len(res_s0.get("iterations", [])) if (isinstance(res_s0, dict) and "iterations" in res_s0) else 6 print(" Nonlinear Poisson solve converged successfully.") # Print Potential statistics after Poisson solve for reg in ["Silicon", "Oxide", "Molding"]: pot = np.array(devsim.get_node_model_values(device=device_new_name, region=reg, name="Potential")) print(f" [pre-solve] Potential in {reg} after solve: min={pot.min():.2e}, max={pot.max():.2e}, mean={pot.mean():.2e}") except devsim.error as e: print(f" Nonlinear Poisson solve failed: {e}. Proceeding anyway...") # Bypass decoupled carrier solve and Stage 0.5.2 to preserve the high-quality interpolated and smoothed carriers. # Decoupled Gummel carriers solve is unstable at high reverse bias (500V+) and destroys the carriers by setting them to zero. elec_new = electrons_interp holes_new = holes_interp print("[Stage 0.5.2] Bypassed decoupled carrier initialization and Stage 0.5.2 to preserve charge-conserved carriers.") # 5. Restore original PotentialNodeCharge and its derivatives for drift-diffusion print(" Restoring original PotentialNodeCharge and derivatives...") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge", equation="-q * kahan3(Holes, -Electrons, NetDoping)") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Potential", equation="0") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Electrons", equation="q") devsim.node_model(device=device_new_name, region="Silicon", name="PotentialNodeCharge:Holes", equation="-q") # Restore PotentialEquation variable update to log_damp devsim.equation(device=device_new_name, region="Silicon", name="PotentialEquation", variable_name="Potential", node_model="PotentialNodeCharge", edge_model="DField", variable_update="log_damp", min_error=1e-3) print("Solving refined mesh: Stage 1 fully-coupled log_damp (Poisson pre-solved), Stage 2 positive...") # ========================================== # Stage 1: Fully-coupled log_damp # ========================================== av_model_n = "AvalancheGeneration" if is_avalanche_enabled else "" av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else "" btbt_model_n = "BTBTGeneration" if is_btbt_enabled else "" btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else "" if av_model_n and btbt_model_n: from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration") CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p") for i in ("Potential", "Electrons", "Holes"): CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i) CreateEdgeModelDerivatives(device_new_name, "Silicon", "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 = "" devsim.equation(device=device_new_name, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons", time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_model_n, variable_update="log_damp", node_model="ElectronGeneration", min_error=1e5) devsim.equation(device=device_new_name, region="Silicon", name="HoleContinuityEquation", variable_name="Holes", time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=gen_model_p, variable_update="log_damp", node_model="HoleGeneration", min_error=1e5) for c in ["MT1_Si", "MT2_Si", "MT1_P12_Si", "MT2_P12_Si"]: contact_electrons_name = f"{c}nodeelectrons" contact_holes_name = f"{c}nodeholes" devsim.contact_equation(device=device_new_name, contact=c, name="ElectronContinuityEquation", node_model=contact_electrons_name, edge_current_model=opts['Jn']) devsim.contact_equation(device=device_new_name, contact=c, name="HoleContinuityEquation", node_model=contact_holes_name, edge_current_model=opts['Jp']) # Set carrier values again to ensure they are not reset/cleared by equation re-registration. # We update Electrons and Holes to their Gummel-adjusted Boltzmann values based on the solved Potential. try: pot = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="Potential")) pot_ref = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="Potential_ref")) vt = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="V_t")) dv_clipped = np.clip((pot - pot_ref) / vt, -50.0, 50.0) electrons_updated = np.clip(elec_new * np.exp(dv_clipped), 1e-40, None) holes_updated = np.clip(holes_new * np.exp(-dv_clipped), 1e-40, None) print(f" Boltzmann carrier update applied. dV_gummel_clipped: min={dv_clipped.min():.4f}, max={dv_clipped.max():.4f}, mean={dv_clipped.mean():.4f}") print(f" Electrons sum: before={elec_new.sum():.4e}, after={electrons_updated.sum():.4e}") print(f" Holes sum: before={holes_new.sum():.4e}, after={holes_updated.sum():.4e}") except Exception as e_boltz: print(f" Failed to calculate Boltzmann updated carriers: {e_boltz}. Falling back to original carrier values.") electrons_updated = elec_new holes_updated = holes_new devsim.set_node_values(device=device_new_name, region="Silicon", name="Electrons", values=list(electrons_updated)) devsim.set_node_values(device=device_new_name, region="Silicon", name="Holes", values=list(holes_updated)) print(" Electrons/Holes values restored and Boltzmann-adjusted after equation registration.") enforce_contact_boundary_conditions(device_new_name) print("[Stage 1] Running fully-coupled log_damp solve...") # Change PotentialEquation variable_update to default for Stage 1 pre-conditioning stability devsim.equation(device=device_new_name, region="Silicon", name="PotentialEquation", variable_name="Potential", node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3) for reg in ["Oxide", "Molding"]: devsim.equation(device=device_new_name, region=reg, name="PotentialEquation", variable_name="Potential", edge_model="PotentialEdgeFlux", variable_update="default", min_error=1e-3) try: res_s1 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=12, rollback=False, info=True) iters_s1 = len(res_s1.get("iterations", [])) if (isinstance(res_s1, dict) and "iterations" in res_s1) else 12 print(" Stage 1 Coupled log_damp solve finished.") except devsim.error as e: print(f" Stage 1 Coupled log_damp solve errored: {e}. Proceeding to Stage 2 anyway.") res_s1 = {"converged": False} iters_s1 = 20 # Clip any negative Electrons/Holes that log_damp Stage 1 may have introduced # (log_damp can sometimes produce negative values in extreme depletion regions due to # large Potential updates interacting with carrier boundary conditions) print("[Stage 1→2] Clipping negative Electrons/Holes to floor (1e-40) before Stage 2...") try: n_vals = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="Electrons")) p_vals = np.array(devsim.get_node_model_values(device=device_new_name, region="Silicon", name="Holes")) n_clipped = np.clip(n_vals, 1e-40, None) p_clipped = np.clip(p_vals, 1e-40, None) if (n_clipped != n_vals).any() or (p_clipped != p_vals).any(): print(f" Clipped {(n_clipped != n_vals).sum()} Electrons nodes, {(p_clipped != p_vals).sum()} Holes nodes") devsim.set_node_values(device=device_new_name, region="Silicon", name="Electrons", values=list(n_clipped)) devsim.set_node_values(device=device_new_name, region="Silicon", name="Holes", values=list(p_clipped)) except Exception as e_clip: print(f" Clip step failed: {e_clip} (proceeding anyway)") # ========================================== # Stage 2: log_damp coupled precision solve # ========================================== av_model_n = "AvalancheGeneration" if is_avalanche_enabled else "" av_model_p = "AvalancheGeneration_p" if is_avalanche_enabled else "" btbt_model_n = "BTBTGeneration" if is_btbt_enabled else "" btbt_model_p = "BTBTGeneration_p" if is_btbt_enabled else "" if av_model_n and btbt_model_n: from physics.model_create import CreateEdgeModel, CreateEdgeModelDerivatives CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration") CreateEdgeModel(device_new_name, "Silicon", "CombinedGeneration_p", "AvalancheGeneration_p + BTBTGeneration_p") for i in ("Potential", "Electrons", "Holes"): CreateEdgeModelDerivatives(device_new_name, "Silicon", "CombinedGeneration", "AvalancheGeneration + BTBTGeneration", i) CreateEdgeModelDerivatives(device_new_name, "Silicon", "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 = "" devsim.equation(device=device_new_name, region="Silicon", name="ElectronContinuityEquation", variable_name="Electrons", time_node_model="NCharge", edge_model=opts['Jn'], edge_volume_model=gen_model_n, variable_update="positive", node_model="ElectronGeneration", min_error=1e5) devsim.equation(device=device_new_name, region="Silicon", name="HoleContinuityEquation", variable_name="Holes", time_node_model="PCharge", edge_model=opts['Jp'], edge_volume_model=gen_model_p, variable_update="positive", node_model="HoleGeneration", min_error=1e5) # Restore PotentialEquation variable update to default with min_error=1e-3 devsim.equation(device=device_new_name, region="Silicon", name="PotentialEquation", variable_name="Potential", node_model="PotentialNodeCharge", edge_model="DField", variable_update="default", min_error=1e-3) for reg in ["Oxide", "Molding"]: devsim.equation(device=device_new_name, region=reg, name="PotentialEquation", variable_name="Potential", edge_model="PotentialEdgeFlux", variable_update="default", min_error=1e-3) print("[Stage 2] Running log_damp coupled precision solve (max 30 iters)...") try: res_stage2 = devsim.solve(type="dc", absolute_error=1e10, relative_error=1e-3, charge_error=1e12, maximum_iterations=30, info=True) iters_s2 = len(res_stage2.get("iterations", [])) if (isinstance(res_stage2, dict) and "iterations" in res_stage2) else 30 # We check both absolute error and charge error convergence through devsim solve. # If solve completed without raising exception, we assume it achieved correct physical state. if res_stage2 is None: res_stage2 = {"converged": True} elif not res_stage2.get("converged", False): # Fallback check if info=True dict indicates failure raise devsim.error("Stage 2 solve did not achieve convergence status.") # Log successful refinement convergence to simulation_time.log try: import time time_taken = time.time() - refine_start_time if time_log is not None: time_log.write(f"{time.strftime('%X')}\t{v_bias:.4f}\t[Refine]\t-\t{iters_s0}+{iters_s1}+{iters_s2}\t{time_taken:.2f}s\tSuccess\n") time_log.flush() else: with open(f"{out_dir}simulation_time.log", "a") as local_log: local_log.write(f"{time.strftime('%X')}\t{v_bias:.4f}\t[Refine]\t-\t{iters_s0}+{iters_s1}+{iters_s2}\t{time_taken:.2f}s\tSuccess\n") except Exception as e_log: print(f"Warning: Failed to log refinement progress to simulation_time.log: {e_log}") except devsim.error as e: print(f"Refinement Precision Newton solve failed/errored: {e}! Recreating and restoring old device '{device_old}'...") # Log failure to simulation_time.log try: import time time_taken = time.time() - refine_start_time if time_log is not None: time_log.write(f"{time.strftime('%X')}\t{v_bias:.4f}\t[Refine]\t-\tStage 2 Failed\t{time_taken:.2f}s\tReverted to Coarse\n") time_log.flush() else: with open(f"{out_dir}simulation_time.log", "a") as local_log: local_log.write(f"{time.strftime('%X')}\t{v_bias:.4f}\t[Refine]\t-\tStage 2 Failed\t{time_taken:.2f}s\tReverted to Coarse\n") except Exception as e_log: pass try: if devsim.get_device_list() and device_new_name in devsim.get_device_list(): devsim.delete_device(device=device_new_name) except Exception: pass # Recreate old device to allow restore_state to work try: devsim.create_gmsh_mesh(mesh=device_old, file="device_2d.msh") except Exception: pass setup_physics_for_device(device_old, is_avalanche_enabled=is_avalanche_enabled, is_btbt_enabled=is_btbt_enabled) raise devsim.error(f"Precision Newton solve on refined mesh did not converge: {e}") print("Convergence on refined mesh achieved successfully!") return device_new_name, opts