Source code for nanohubpadre.solver

"""
Solver configuration for PADRE simulations.

Includes SOLVE, METHOD, SYSTEM, and LINALG commands.
"""

from typing import Optional, Union, List
from .base import PadreCommand


[docs] class System(PadreCommand): """ Define which PDEs to solve and their coupling. Parameters ---------- carriers : int Number of carriers (0, 1, or 2) electrons : bool Solve electron continuity equation holes : bool Solve hole continuity equation n_temperature : bool Solve electron energy balance p_temperature : bool Solve hole energy balance Coupling: newton : bool Fully coupled Newton iteration gummel : bool Decoupled Gummel iteration coupling : list of str Custom coupling specification print_info : bool Print memory allocation info Example ------- >>> # Two-carrier drift-diffusion >>> sys = System(carriers=2, newton=True) >>> >>> # Energy balance with custom coupling >>> sys = System(electrons=True, n_temperature=True, ... coupling=["12", "4"]) """ command_name = "SYSTEM"
[docs] def __init__( self, carriers: int = 0, electrons: bool = False, holes: bool = False, n_temperature: bool = False, p_temperature: bool = False, newton: bool = False, gummel: bool = False, coupling: Optional[List[str]] = None, print_info: bool = False, symmetric: bool = True, ): super().__init__() self.carriers = carriers self.electrons = electrons self.holes = holes self.n_temperature = n_temperature self.p_temperature = p_temperature self.newton = newton self.gummel = gummel self.coupling = coupling self.print_info = print_info self.symmetric = symmetric
[docs] def to_padre(self) -> str: params = {} flags = [] if self.carriers > 0: params["CARR"] = self.carriers if self.electrons: flags.append("ELECTRONS") if self.holes: flags.append("HOLES") if self.n_temperature: flags.append("N.TEMPERATURE") if self.p_temperature: flags.append("P.TEMPERATURE") if self.newton: flags.append("NEWTON") elif self.gummel: flags.append("GUMMEL") elif self.coupling: params["COUPLING"] = ",".join(self.coupling) if self.print_info: flags.append("PRINT") if not self.symmetric: params["SYMMETRIC"] = False return self._build_command(params, flags)
[docs] class LinAlg(PadreCommand): """ Configure linear algebra solver. Parameters ---------- dir_def : bool Use direct solver defaults iter_def : bool Use iterative solver defaults method : str Method specification precondition : str Preconditioner specification acceleration : str Acceleration technique itmax : int Maximum iterations lin_tol : float Linear convergence tolerance lin_atol : float Absolute tolerance Example ------- >>> # Direct solver >>> la = LinAlg(dir_def=True) >>> >>> # Iterative solver >>> la = LinAlg(iter_def=True, itmax=500) """ command_name = "LINALG"
[docs] def __init__( self, dir_def: bool = False, iter_def: bool = False, method: Optional[str] = None, precondition: Optional[str] = None, acceleration: Optional[str] = None, s_method: Optional[str] = None, s_precondition: Optional[str] = None, s_acceleration: Optional[str] = None, itmax: Optional[int] = None, nrmax: Optional[int] = None, lin_tol: Optional[float] = None, lin_atol: Optional[float] = None, nlin_tol: Optional[float] = None, maxfill: Optional[int] = None, k: Optional[int] = None, restart: Optional[float] = None, linscal: bool = True, ptscl: bool = True, scale: bool = False, ): super().__init__() self.dir_def = dir_def self.iter_def = iter_def self.method = method self.precondition = precondition self.acceleration = acceleration self.s_method = s_method self.s_precondition = s_precondition self.s_acceleration = s_acceleration self.itmax = itmax self.nrmax = nrmax self.lin_tol = lin_tol self.lin_atol = lin_atol self.nlin_tol = nlin_tol self.maxfill = maxfill self.k = k self.restart = restart self.linscal = linscal self.ptscl = ptscl self.scale = scale
[docs] def to_padre(self) -> str: params = {} flags = [] if self.dir_def: flags.append("DIR.DEF") if self.iter_def: flags.append("ITER.DEF") if self.method: params["METHOD"] = self.method if self.precondition: params["PRECONDITION"] = self.precondition if self.acceleration: params["ACCELERATION"] = self.acceleration if self.s_method: params["S.METHOD"] = self.s_method if self.s_precondition: params["S.PRECONDITION"] = self.s_precondition if self.s_acceleration: params["S.ACCELERATION"] = self.s_acceleration if self.itmax is not None: params["ITMAX"] = self.itmax if self.nrmax is not None: params["NRMAX"] = self.nrmax if self.lin_tol is not None: params["LIN.TOL"] = self.lin_tol if self.lin_atol is not None: params["LIN.ATOL"] = self.lin_atol if self.nlin_tol is not None: params["NLIN.TOL"] = self.nlin_tol if self.maxfill is not None: params["MAXFILL"] = self.maxfill if self.k is not None: params["K"] = self.k if self.restart is not None: params["RESTART"] = self.restart if not self.linscal: params["LINSCAL"] = False if not self.ptscl: params["PTSCL"] = False if self.scale: flags.append("SCALE") return self._build_command(params, flags)
[docs] class Method(PadreCommand): """ Configure nonlinear iteration and numerical methods. Parameters ---------- Convergence control: itlimit : int Maximum inner iterations (default 20) outloops : int Maximum outer iterations (default 20) gloops : int Number of Gummel smoothing loops x_toler : float or list Update norm tolerance rhs_toler : float or list Residual norm tolerance xnorm : bool Use update norm (default True) rhsnorm : bool Use residual norm l2norm : bool Use L2 norm for residual Pseudo-continuation (trap): trap : bool Enable bias stepping on convergence failure a_trap : float Bias reduction factor (default 0.5) n_trap : int Newton iterations before trap check dv_trap : float Minimum voltage step di_trap : float Minimum current step stop : bool Stop on convergence failure Damping: damped : str Damping mode ("single", "all", "none") dvlimit : float Maximum potential update Time stepping: second_order : bool Use 2nd order TR-BDF2 (default True) tauto : bool Automatic time step (default True) tolr_time : float Relative truncation error tolerance tola_time : float Absolute truncation error tolerance dt_min : float Minimum time step Newton-Richardson: autonr : bool Automatic Newton-Richardson print_iter : bool Print terminal values after each iteration Example ------- >>> # Standard method with trap >>> m = Method(trap=True, a_trap=0.5, itlimit=30) >>> >>> # Transient with auto timestep >>> m = Method(second_order=True, tauto=True, tolr_time=1e-3) """ command_name = "METHOD"
[docs] def __init__( self, # Convergence itlimit: Optional[int] = None, outloops: Optional[int] = None, min_inner: Optional[int] = None, min_outer: Optional[int] = None, gloops: Optional[int] = None, x_toler: Optional[Union[float, List[float]]] = None, rhs_toler: Optional[Union[float, List[float]]] = None, xnorm: bool = True, rhsnorm: bool = False, l2norm: bool = True, # Trap trap: bool = True, dgmin: Optional[float] = None, a_trap: Optional[float] = None, n_trap: Optional[int] = None, m_trap: Optional[int] = None, i_trap: Optional[int] = None, maxneg: Optional[int] = None, dv_trap: Optional[float] = None, di_trap: Optional[float] = None, out_trap: bool = True, ign_inner: bool = False, stop: bool = True, # Damping damped: str = "single", itdamp: Optional[int] = None, delta: Optional[float] = None, damploop: Optional[int] = None, dfactor: Optional[float] = None, dpower: Optional[float] = None, dvlimit: Optional[float] = None, truncate: bool = False, vmargin: Optional[float] = None, # Time stepping second_order: bool = True, tr_print: bool = False, tauto: bool = True, tolr_time: Optional[float] = None, tola_time: Optional[float] = None, l2tnorm: bool = True, dt_min: Optional[float] = None, t_lima: Optional[float] = None, t_limb: Optional[float] = None, extrapolate: bool = False, # Newton-Richardson autonr: bool = True, nrcriterion: Optional[float] = None, nrloop: Optional[int] = None, # AC ac_method: Optional[str] = None, # Misc print_iter: bool = False, err_estimate: bool = False, lin_proj: bool = False, lin_fail: bool = False, ): super().__init__() # Convergence self.itlimit = itlimit self.outloops = outloops self.min_inner = min_inner self.min_outer = min_outer self.gloops = gloops self.x_toler = x_toler self.rhs_toler = rhs_toler self.xnorm = xnorm self.rhsnorm = rhsnorm self.l2norm = l2norm # Trap self.trap = trap self.dgmin = dgmin self.a_trap = a_trap self.n_trap = n_trap self.m_trap = m_trap self.i_trap = i_trap self.maxneg = maxneg self.dv_trap = dv_trap self.di_trap = di_trap self.out_trap = out_trap self.ign_inner = ign_inner self.stop = stop # Damping self.damped = damped self.itdamp = itdamp self.delta = delta self.damploop = damploop self.dfactor = dfactor self.dpower = dpower self.dvlimit = dvlimit self.truncate = truncate self.vmargin = vmargin # Time stepping self.second_order = second_order self.tr_print = tr_print self.tauto = tauto self.tolr_time = tolr_time self.tola_time = tola_time self.l2tnorm = l2tnorm self.dt_min = dt_min self.t_lima = t_lima self.t_limb = t_limb self.extrapolate = extrapolate # Newton-Richardson self.autonr = autonr self.nrcriterion = nrcriterion self.nrloop = nrloop # AC self.ac_method = ac_method # Misc self.print_iter = print_iter self.err_estimate = err_estimate self.lin_proj = lin_proj self.lin_fail = lin_fail
def _format_vector(self, value: Union[float, List[float]]) -> str: if isinstance(value, (list, tuple)): return ",".join(str(v) for v in value) return str(value)
[docs] def to_padre(self) -> str: params = {} flags = [] # Convergence if self.itlimit is not None: params["ITLIMIT"] = self.itlimit if self.outloops is not None: params["OUTLOOPS"] = self.outloops if self.min_inner is not None: params["MIN.INNER"] = self.min_inner if self.min_outer is not None: params["MIN.OUTER"] = self.min_outer if self.gloops is not None: params["GLOOPS"] = self.gloops if self.x_toler is not None: params["X.TOL"] = self._format_vector(self.x_toler) if self.rhs_toler is not None: params["RHS.TOL"] = self._format_vector(self.rhs_toler) if not self.xnorm: params["XNORM"] = False if self.rhsnorm: flags.append("RHSNORM") if not self.l2norm: params["L2NORM"] = False # Trap if self.trap: flags.append("TRAP") if self.dgmin is not None: params["DGMIN"] = self.dgmin if self.a_trap is not None: params["A.TRAP"] = self.a_trap if self.n_trap is not None: params["N.TRAP"] = self.n_trap if self.m_trap is not None: params["M.TRAP"] = self.m_trap if self.i_trap is not None: params["I.TRAP"] = self.i_trap if self.maxneg is not None: params["MAXNEG"] = self.maxneg if self.dv_trap is not None: params["DV.TRAP"] = self.dv_trap if self.di_trap is not None: params["DI.TRAP"] = self.di_trap if not self.out_trap: params["OUT.TRAP"] = False if self.ign_inner: flags.append("IGN.INNER") if not self.stop: params["STOP"] = False # Damping if self.damped != "single": params["DAMPED"] = self.damped if self.itdamp is not None: params["ITDAMP"] = self.itdamp if self.delta is not None: params["DELTA"] = self.delta if self.damploop is not None: params["DAMPLOOP"] = self.damploop if self.dfactor is not None: params["DFACTOR"] = self.dfactor if self.dpower is not None: params["DPOWER"] = self.dpower if self.dvlimit is not None: params["DVLIMIT"] = self.dvlimit if self.truncate: flags.append("TRUNCATE") if self.vmargin is not None: params["VMARGIN"] = self.vmargin # Time stepping if not self.second_order: params["2NDORDER"] = False if self.tr_print: flags.append("TR.PRINT") if not self.tauto: params["TAUTO"] = False if self.tolr_time is not None: params["TOLR.TIME"] = self.tolr_time if self.tola_time is not None: params["TOLA.TIME"] = self.tola_time if not self.l2tnorm: params["L2TNORM"] = False if self.dt_min is not None: params["DT.MIN"] = self.dt_min if self.t_lima is not None: params["T.LIMA"] = self.t_lima if self.t_limb is not None: params["T.LIMB"] = self.t_limb if self.extrapolate: flags.append("EXTRAPOLATE") # Newton-Richardson if not self.autonr: params["AUTONR"] = False if self.nrcriterion is not None: params["NRCRITERION"] = self.nrcriterion if self.nrloop is not None: params["NRLOOP"] = self.nrloop # AC if self.ac_method: params["AC.METHOD"] = self.ac_method # Misc if self.print_iter: flags.append("PRINT") if self.err_estimate: flags.append("ERR.ESTIMATE") if self.lin_proj: flags.append("LIN.PROJ") if self.lin_fail: flags.append("LIN.FAIL") return self._build_command(params, flags)
[docs] class Solve(PadreCommand): """ Solve for one or more bias points. Parameters ---------- Initial guess: initial : bool Compute equilibrium solution (first solve) previous : bool Use previous solution as guess project : bool Project from two previous solutions euler : bool Euler projection guess local : bool Local quasi-Fermi guess (good for reverse bias) Bias specification: v1-v0 : float Voltage on electrode 1-10 i1-i0 : float Current on electrode 1-10 (for current BC) vstep : float Voltage step for stepping istep : float Current step for stepping nsteps : int Number of steps electrode : int or list Electrode(s) to step multiply : bool Multiply by step instead of add Generation: generation : float Blanket generation rate (/s-cm^3) dose_rad : float Radiation dose (rad) absorption : float Absorption coefficient (/um) Transient: tstep : float Time step (seconds) tstop : float Stop time (seconds) tdelta : float Time interval to simulate ramptime : float Ramp duration for bias changes endramp : float End time of ramp seu : bool Single event upset mode dt_seu : float SEU pulse width AC analysis: ac_analysis : bool Perform AC analysis frequency : float AC frequency (Hz) fstep : float Frequency step nfsteps : int Number of frequency steps mult_freq : bool Multiply frequency by step vss : float Small-signal voltage (default 0.1*kT/q) terminal : int Terminal for AC excitation Output: outfile : str Solution output file currents : bool Save current data ascii : bool ASCII output format save : int Save frequency (every n points) Example ------- >>> # Initial equilibrium >>> s = Solve(initial=True, outfile="sol0") >>> >>> # Voltage sweep >>> s = Solve(v1=0.5, vstep=0.1, nsteps=10, electrode=1, ... outfile="sol_a") >>> >>> # Transient simulation >>> s = Solve(v1=2, tstep=1e-12, tstop=1e-9, ramptime=10e-9, ... outfile="trans") """ command_name = "SOLVE"
[docs] def __init__( self, # Initial guess initial: bool = False, previous: bool = False, project: bool = False, euler: bool = False, local: bool = False, # Bias v1: Optional[float] = None, v2: Optional[float] = None, v3: Optional[float] = None, v4: Optional[float] = None, v5: Optional[float] = None, v6: Optional[float] = None, v7: Optional[float] = None, v8: Optional[float] = None, v9: Optional[float] = None, v0: Optional[float] = None, i1: Optional[float] = None, i2: Optional[float] = None, i3: Optional[float] = None, i4: Optional[float] = None, i5: Optional[float] = None, i6: Optional[float] = None, i7: Optional[float] = None, i8: Optional[float] = None, i9: Optional[float] = None, i0: Optional[float] = None, vstep: Optional[float] = None, istep: Optional[float] = None, nsteps: Optional[int] = None, electrode: Optional[Union[int, List[int]]] = None, multiply: bool = False, n_bias: Optional[float] = None, p_bias: Optional[float] = None, # Generation generation: Optional[float] = None, dose_rad: Optional[float] = None, absorption: Optional[float] = None, dir_gen: str = "y", pk_gen: Optional[float] = None, reg_gen: Optional[List[int]] = None, # Transient tstep: Optional[float] = None, tstop: Optional[float] = None, tdelta: Optional[float] = None, tend_refine: bool = False, ramptime: Optional[float] = None, endramp: Optional[float] = None, seu: bool = False, dt_seu: Optional[float] = None, g_tau: Optional[float] = None, # AC ac_analysis: bool = False, frequency: Optional[float] = None, fstep: Optional[float] = None, mult_freq: bool = False, nfsteps: Optional[int] = None, vss: Optional[float] = None, terminal: Optional[int] = None, s_omega: Optional[float] = None, max_inner: Optional[int] = None, tolerance: Optional[float] = None, # Noise noise_anal: bool = False, i_noise: bool = False, e_noise: Optional[int] = None, # Output outfile: Optional[str] = None, currents: bool = True, no_append: bool = False, ascii: bool = True, save: Optional[int] = None, t_save: Optional[List[float]] = None, nfile: Optional[str] = None, mostrans: Optional[int] = None, ): super().__init__() # Initial guess self.initial = initial self.previous = previous self.project = project self.euler = euler self.local = local # Bias self.v1 = v1 self.v2 = v2 self.v3 = v3 self.v4 = v4 self.v5 = v5 self.v6 = v6 self.v7 = v7 self.v8 = v8 self.v9 = v9 self.v0 = v0 self.i1 = i1 self.i2 = i2 self.i3 = i3 self.i4 = i4 self.i5 = i5 self.i6 = i6 self.i7 = i7 self.i8 = i8 self.i9 = i9 self.i0 = i0 self.vstep = vstep self.istep = istep self.nsteps = nsteps self.electrode = electrode self.multiply = multiply self.n_bias = n_bias self.p_bias = p_bias # Generation self.generation = generation self.dose_rad = dose_rad self.absorption = absorption self.dir_gen = dir_gen self.pk_gen = pk_gen self.reg_gen = reg_gen # Transient self.tstep = tstep self.tstop = tstop self.tdelta = tdelta self.tend_refine = tend_refine self.ramptime = ramptime self.endramp = endramp self.seu = seu self.dt_seu = dt_seu self.g_tau = g_tau # AC self.ac_analysis = ac_analysis self.frequency = frequency self.fstep = fstep self.mult_freq = mult_freq self.nfsteps = nfsteps self.vss = vss self.terminal = terminal self.s_omega = s_omega self.max_inner = max_inner self.tolerance = tolerance # Noise self.noise_anal = noise_anal self.i_noise = i_noise self.e_noise = e_noise # Output self.outfile = outfile self.currents = currents self.no_append = no_append self.ascii = ascii self.save = save self.t_save = t_save self.nfile = nfile self.mostrans = mostrans
def _format_electrode(self, electrode: Union[int, List[int]]) -> str: if isinstance(electrode, list): return "".join(str(e) for e in electrode) return str(electrode)
[docs] def to_padre(self) -> str: params = {} flags = [] # Initial guess if self.initial: flags.append("INIT") if self.previous: flags.append("PREV") if self.project: flags.append("PROJ") if self.euler: flags.append("EULER") if self.local: flags.append("LOCAL") # Bias for i in range(1, 10): v = getattr(self, f"v{i}") if v is not None: params[f"V{i}"] = v if self.v0 is not None: params["V0"] = self.v0 for i in range(1, 10): curr = getattr(self, f"i{i}") if curr is not None: params[f"I{i}"] = curr if self.i0 is not None: params["I0"] = self.i0 if self.vstep is not None: params["VSTEP"] = self.vstep if self.istep is not None: params["ISTEP"] = self.istep if self.nsteps is not None: params["NSTEPS"] = self.nsteps if self.electrode is not None: params["ELECT"] = self._format_electrode(self.electrode) if self.multiply: flags.append("MULTIPLY") if self.n_bias is not None: params["N.BIAS"] = self.n_bias if self.p_bias is not None: params["P.BIAS"] = self.p_bias # Generation if self.generation is not None: params["GEN"] = self.generation if self.dose_rad is not None: params["DOSE.RAD"] = self.dose_rad if self.absorption is not None: params["ABSORP"] = self.absorption if self.dir_gen != "y": params["DIR.GEN"] = self.dir_gen if self.pk_gen is not None: params["PK.GEN"] = self.pk_gen if self.reg_gen: params["REG.GEN"] = ",".join(str(r) for r in self.reg_gen) # Transient if self.tstep is not None: params["TSTEP"] = self.tstep if self.tstop is not None: params["TSTOP"] = self.tstop if self.tdelta is not None: params["TDELTA"] = self.tdelta if self.tend_refine: flags.append("TEND.REFINE") if self.ramptime is not None: params["RAMPTIME"] = self.ramptime if self.endramp is not None: params["ENDRAMP"] = self.endramp if self.seu: flags.append("SEU") if self.dt_seu is not None: params["DT.SEU"] = self.dt_seu if self.g_tau is not None: params["G.TAU"] = self.g_tau # AC if self.ac_analysis: flags.append("AC.ANALYSIS") if self.frequency is not None: params["FREQ"] = self.frequency if self.fstep is not None: params["FSTEP"] = self.fstep if self.mult_freq: flags.append("MULT.FREQ") if self.nfsteps is not None: params["NFSTEPS"] = self.nfsteps if self.vss is not None: params["VSS"] = self.vss if self.terminal is not None: params["TERMINAL"] = self.terminal if self.s_omega is not None: params["S.OMEGA"] = self.s_omega if self.max_inner is not None: params["MAX.INNER"] = self.max_inner if self.tolerance is not None: params["TOLERANCE"] = self.tolerance # Noise if self.noise_anal: flags.append("NOISE.ANAL") if self.i_noise: flags.append("I.NOISE") if self.e_noise is not None: params["E.NOISE"] = self.e_noise # Output if self.outfile: params["OUTF"] = self.outfile if not self.currents: params["CURRENTS"] = False if self.no_append: flags.append("NO.APPEND") if not self.ascii: params["ASCII"] = False if self.save is not None: params["SAVE"] = self.save if self.t_save: params["T.SAVE"] = ",".join(str(t) for t in self.t_save) if self.nfile: params["NFILE"] = self.nfile if self.mostrans is not None: params["MOSTRANS"] = self.mostrans return self._build_command(params, flags)
[docs] @classmethod def equilibrium(cls, outfile: Optional[str] = None) -> "Solve": """Create initial equilibrium solve.""" return cls(initial=True, outfile=outfile)
[docs] @classmethod def bias_sweep(cls, electrode: int, start: float, stop: float, step: float, outfile: Optional[str] = None, project: bool = True, **kwargs) -> "Solve": """Create a bias sweep solve.""" nsteps = int((stop - start) / step) biases = {f"v{electrode}": start} return cls(vstep=step, nsteps=nsteps, electrode=electrode, project=project, outfile=outfile, **biases, **kwargs)