def eval_cqs(self, vPort, saveOP=False): """ Calculates gate and drain current. Input is a vector as follows: vPort = [vgs(t), vgd(t), vgs(t-tau), vgd(t-tau)] saveOP has no effect for now """ # Calculate junction currents igs = self.diogs.get_id(vPort[0]) qgs = self.diogs.get_qd(vPort[0]) igd = self.diogd.get_id(vPort[1]) qgd = self.diogd.get_qd(vPort[1]) # Add breakdown current igs -= self.ib0 * np.exp(-(vPort[0] + self.vbd) / self._k6) igd -= self.ib0 * np.exp(-(vPort[1] + self.vbd) / self._k6) DtoSswap = ad.condassign(vPort[0] - vPort[1], 1.0, -1.0) vds = DtoSswap * (vPort[0] - vPort[1]) vgsi = ad.condassign(DtoSswap, vPort[0], vPort[1]) vgsiT = ad.condassign(DtoSswap, vPort[2], vPort[3]) # Calculate ids. vx = vgsiT * (1.0 + self._Beta * (self.vds0 - vds)) ids = (self.a0 + vx * (self.a1 + vx * (self.a2 + vx * self.a3))) * np.tanh(self.gama * vds) * self._idsFac # vgsiT makes more sense than vgsi below? (vgsi in original doc) ids = ad.condassign((vgsi - self._Vt0), ids, 0.0) # Must ensure ids > 0 for power conservation ids = ad.condassign(ids, ids, 0.0) # Return numpy array with one element per current source. iVec = np.array([igs, igd, ids * DtoSswap]) * self.area qVec = np.array([qgs, qgd]) * self.area return (iVec, qVec)
def eval_cqs(self, vPort, getOP=False): """ Calculates gate and drain current. Input is a vector as follows: vPort = [vgs(t), vgd(t), vgs(t-tau), vgd(t-tau)] getOP has no effect for now """ # Calculate junction currents igs = self.diogs.get_id(vPort[0]) qgs = self.diogs.get_qd(vPort[0]) igd = self.diogd.get_id(vPort[1]) qgd = self.diogd.get_qd(vPort[1]) # Add breakdown current igs -= self.ib0 * np.exp(-(vPort[0] + self.vbd) / self._k6) igd -= self.ib0 * np.exp(-(vPort[1] + self.vbd) / self._k6) DtoSswap = ad.condassign(vPort[0] - vPort[1], 1., -1.) vds = DtoSswap * (vPort[0] - vPort[1]) vgsi = ad.condassign(DtoSswap, vPort[0], vPort[1]) vgsiT = ad.condassign(DtoSswap, vPort[2], vPort[3]) # Calculate ids. vx = vgsiT * (1. + self._Beta * (self.vds0 - vds)) ids = (self.a0 + vx * (self.a1 + vx * (self.a2 + vx * self.a3))) \ * np.tanh(self.gama * vds) * self._idsFac # vgsiT makes more sense than vgsi below? (vgsi in original doc) ids = ad.condassign((vgsi - self._Vt0), ids, 0.) # Must ensure ids > 0 for power conservation ids = ad.condassign(ids, ids, 0.) # Return numpy array with one element per current source. iVec = np.array([igs, igd, ids * DtoSswap]) * self.area qVec = np.array([qgs, qgd]) * self.area return (iVec, qVec)
def log1pexp(x): """ Safe calculation of log(1 + exp(x)) """ y1 = ad.condassign(x - EXP_THRESHOLD, x, np.log(1.+np.exp(x))) return ad.condassign(-x - EXP_THRESHOLD, np.exp(x), y1)
def get_idvd(self, x): """ Returns junction a tuple (current, voltage) x: state variable """ # Static current b = self._svth - x c = self._t_is * (np.exp(self._alpha * x) - 1.0) d = self._t_is * self._kexp * (1.0 + self._alpha * (x - self._svth)) - self._t_is iD = ad.condassign(b, c, d) # Diode voltage d = self._svth + np.log(1.0 + self._alpha * (x - self._svth)) / self._alpha vD = ad.condassign(b, x, d) return (iD, vD)
def inv_f(f): """ Solve f^(-1)(i_f(r)) to get i_f and i_r using Newton's method Uses a fixed number of iterations for easy taping and a different formulation for f>0 and f<0 to ensure convergence in few iterations. Solution has good accuracy for all values. """ # Obtain a good guess first using EKV's simple interpolation function i1 = 4. * f_simple(f) i2 = i1 for counter in xrange(4): # (f > 0) => (i1 >= 3.) The 1e-15 term needed to prevent AD # library from choking as without it we have log(0) later sqi1p = np.sqrt(1. + i1 + 1e-15) sqi11 = sqi1p - 1. fodfp = (sqi1p - 2. + np.log(sqi11) - f) * 2. * sqi11 i1 = abs(i1 - fodfp) # f < 0 sqi1 = np.sqrt(1. + i2) fodfn = (sqi1 - 1. - np.exp(f - sqi1 + 2.)) * 2. * sqi1 \ / (np.exp(f - sqi1 + 2.) + 1.) i2 = i2 - fodfn return ad.condassign(f, i1, i2)
def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Delete AD tape (if any) ad.delete_tape(self) # Absolute temperature (note self.temp is in deg. C) T = const.T0 + temp # Thermal voltage self._Vt = const.k * T / const.q # threshold voltage vt0T = self._vt0 - self._tcv * (T - self._Tn) self._vt0a = vt0T + self.avto / self._sga kpT = self.kp * pow(T / self._Tn, self.bex) self.kpa = kpT * (1 + self.akp / self._sga) # Clip to zero if negative self.kpa = ad.condassign(self.kpa, self.kpa, 0.) self._t_ucrit = self.ucrit * pow(T / self._Tn, self.ucex) # Energy gap egT = 1.16 - 0.000702 * T * T / (T + 1108.) self.phiT = self.phi * T / self.Tref \ - 3. * self._Vt * np.log(T / self.Tref) \ - self.egTref * T / self.Tref + egT self._t_ibb = self.ibb * (1. + self.ibbt * (T - self.Tref)) self._vc = self._t_ucrit * self._leff * self.ns self._qb0 = self._gammaa * np.sqrt(self.phiT) # Noise variables self._kSt = 4. * const.k * T
def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Delete AD tape (if any) ad.delete_tape(self) # Absolute temperature (note self.temp is in deg. C) T = const.T0 + temp # Thermal voltage self._Vt = const.k * T / const.q # threshold voltage vt0T = self._vt0 - self._tcv * (T - self._Tn) self._vt0a = vt0T + self.avto / self._sga kpT = self.kp * pow(T / self._Tn, self.bex) self.kpa = kpT * (1 + self.akp / self._sga) # Clip to zero if negative self.kpa = ad.condassign(self.kpa, self.kpa, 0.0) self._t_ucrit = self.ucrit * pow(T / self._Tn, self.ucex) # Energy gap egT = 1.16 - 0.000702 * T * T / (T + 1108.0) self.phiT = ( self.phi * T / self.Tref - 3.0 * self._Vt * np.log(T / self.Tref) - self.egTref * T / self.Tref + egT ) self._t_ibb = self.ibb * (1.0 + self.ibbt * (T - self.Tref)) self._vc = self._t_ucrit * self._leff * self.ns self._qb0 = self._gammaa * np.sqrt(self.phiT) # Noise variables self._kSt = 4.0 * const.k * T
def get_idvd(self, x): """ Returns junction a tuple (current, voltage) x: state variable """ # Static current b = self._svth - x c = self._t_is * (np.exp(self._alpha * x) - 1.) d = self._t_is * self._kexp * \ (1. + self._alpha * (x - self._svth)) - self._t_is iD = ad.condassign(b, c, d) # Diode voltage d = self._svth + \ np.log(1. + self._alpha * (x - self._svth)) / self._alpha vD = ad.condassign(b, x, d) return (iD, vD)
def get_qd(self, vd): """ Returns junction depletion charge vd: diode voltage """ b = self.fc * self._t_vj - vd c = self._k5 * (1.0 - pow(1.0 - vd / self._t_vj, self._k4)) d = self._k6 * ( (1.0 - self.fc * (1.0 + self.m)) * vd + 0.5 * self.m * vd * vd / self._t_vj - self._k7 ) + self._k5 * (1.0 - pow(1.0 - self.fc, self._k4)) return ad.condassign(b, c, d)
def get_qd(self, vd): """ Returns junction depletion charge vd: diode voltage """ b = self.fc * self._t_vj - vd c = self._k5 * (1. - pow(1. - vd / self._t_vj, self._k4)) d = self._k6 * ((1. - self.fc * (1. + self.m)) * vd + .5 * self.m * vd * vd / self._t_vj - self._k7) + self._k5 \ * (1. - pow(1. - self.fc, self._k4)) return ad.condassign(b, c, d)
def f_simple(v): """ Simple interpolation function for F(v) Not accurate for moderate inversion """ # Have to treat the function for large negative v specially # otherwise exp(.5*v) << 1 and we get log(1) = 0 b = v + 20. d = np.exp(.5 * v) c = np.log(1. + d) # f = (b > -20) ? c : d f = ad.condassign(b, c, d) return f**2
def inv_f1(f): """ Approximately solve f^(-1)(i_f(r)) to get i_f and i_r using relaxation """ # Obtain a good guess first using EKV's simple interpolation function i_f = 4. * f_simple(f) # Use fixed number of iterations for easy taping for i in xrange(30): sqi1 = np.sqrt(1. + i_f + 1e-15) # Uses different formulation for f>0 and f<0 for convergence i_fnew = ad.condassign(f, (f + 2. - np.log(sqi1 - 1.))**2 - 1., (np.exp(f - sqi1 + 2.) + 1.)**2 - 1.) i_f = .5 * i_f + .5 * i_fnew return i_f
def f_simple(v): """ Simple interpolation function for F(v) Not accurate for moderate inversion """ # Have to treat the function for large negative v specially # otherwise exp(.5*v) << 1 and we get log(1) = 0 b = v + 20.0 d = np.exp(0.5 * v) c = np.log(1.0 + d) # f = (b > -20) ? c : d f = ad.condassign(b, c, d) return f ** 2
def get_qd(self, vd): """ Returns junction depletion charge vd: diode voltage """ if self.cj0: b = self.fc * self._t_vj - vd c = self._k5 * (1. - pow(1. - vd / self._t_vj, self._k4)) d = self._k6 * ((1. - self.fc * (1. + self.m)) * vd + .5 * self.m * vd * vd / self._t_vj - self._k7) + self._k5 \ * (1. - pow(1. - self.fc, self._k4)) return ad.condassign(b, c, d) else: return 0. * vd
def f_accurate(v): """ Calculate a more accurate value of F(v) Eq. (41) and (42) in [1] Refine f_simple with a few Newton iterations. This function performs the Newton iterations even if not needed on purpose to store the operations in the AD tape. """ # get initial estimate i = f_simple(v) # Apply 3 Newton iterations to refine approximation for j in range(3): k1 = np.sqrt(0.25 + i) f = v - (2.0 * k1 - 1.0 + np.log(k1 - 0.5)) vprime = -1.0 / (2.0 * k1 * (0.5 - k1)) + 1.0 / k1 i += f / vprime b = v + 20.0 # If b is very negative we need this to avoid the singularity that # happens when i is very small: k1 = 0.5 and the log goes to infinity i = ad.condassign(b, i, np.exp(v)) # print v, i, np.exp(v) return i
def f_accurate(v): """ Calculate a more accurate value of F(v) Eq. (41) and (42) in [1] Refine f_simple with a few Newton iterations. This function performs the Newton iterations even if not needed on purpose to store the operations in the AD tape. """ # get initial estimate i = f_simple(v) # Apply 3 Newton iterations to refine approximation for j in range(3): k1 = np.sqrt(0.25 + i) f = v - (2. * k1 - 1. + np.log(k1 - .5)) vprime = -1. / (2. * k1 * (0.5 - k1)) + 1. / k1 i += f / vprime b = v + 20. # If b is very negative we need this to avoid the singularity that # happens when i is very small: k1 = 0.5 and the log goes to infinity i = ad.condassign(b, i, np.exp(v)) #print v, i, np.exp(v) return i
def eval_cqs(self, vPort, getOP=False): """ Calculates Ids, Idb, Isb currents and D, G, S, charges. Input: vPort = [vdb , vgb , vsb] Output: vector with Ids, Idb, Isb currents and vector with D, G, S charges. If getOP = True, return normal output vector plus operating point variables in tuple: (iVec, qVec, opV) """ # Invert all voltages in case of a P-channel device vPort1 = self._tf * vPort # If vds is negative, swap Vd and Vs and (also swap charges # later) DtoSswap = ad.condassign(vPort1[0] - vPort1[2], 1.0, -1.0) # perform the swap (need tmp variable) tmp = ad.condassign(DtoSswap, vPort1[0], vPort1[2]) vPort1[2] = ad.condassign(DtoSswap, vPort1[2], vPort1[0]) vPort1[0] = tmp # Effective gate voltage including reverse short channel effect vgprime = vPort1[1] - self._vt0a - self._deltavRSCE + self.phiT + self._gammaa * np.sqrt(self.phiT) # Effective substrate factor including charge-sharing for # short and narrow channels. Pinch-off voltage for # narrow-channel effect vp0 = ( vgprime - self.phiT - self._gammaa * (np.sqrt(vgprime + self._gammaa * self._gammaa / 4.0) - self._gammaa / 2.0) ) vp0 = ad.condassign(vgprime, vp0, -self.phiT) # Effective substrate factor accounting for charge-sharing tmp = 16.0 * self._Vt * self._Vt vsprime = 0.5 * (vPort1[2] + self.phiT + np.sqrt(pow(vPort1[2] + self.phiT, 2) + tmp)) vdprime = 0.5 * (vPort1[0] + self.phiT + np.sqrt(pow(vPort1[0] + self.phiT, 2) + tmp)) tmp = self.leta / self._leff * (np.sqrt(vsprime) + np.sqrt(vdprime)) tmp -= 3.0 * self.weta * np.sqrt(vp0 + self.phiT) / self._weff gammao = self._gammaa - const.epSi / self.cox * tmp gammaprime = 0.5 * (gammao + np.sqrt(gammao * gammao + 0.1 * self._Vt)) # Pinch-off voltage including short- and narrow-channel effect vp = vgprime - self.phiT vp -= gammaprime * (np.sqrt(vgprime + pow(0.5 * gammaprime, 2)) - gammaprime / 2.0) vp = ad.condassign(vgprime, vp, -self.phiT) # Slope factor n = 1 + self._gammaa / (2.0 * np.sqrt(vp + self.phiT + 4.0 * self._Vt)) # Forward normalized current i_f = (vp - vPort1[2]) / self._Vt i_f = self.interp(i_f) # Velocity saturation voltage vdss = np.sqrt(0.25 + self._Vt * np.sqrt(i_f) / self._vc) - 0.5 vdss *= self._vc # Drain-to-source saturation voltage for reverse normalized current tmp = np.sqrt(i_f) - 0.75 * np.log(i_f) vdssprime = np.sqrt(0.25 + self._Vt * tmp / self._vc) - 0.5 vdssprime *= self._vc vdssprime += self._Vt * (np.log(self._vc / (2.0 * self._Vt)) - 0.6) # Channel-length modulation tmp = np.sqrt(i_f) - vdss / self._Vt deltav = np.sqrt(self.Lambda * tmp + 1.0 / 64) deltav *= 4.0 * self._Vt vds = 0.5 * (vPort1[0] - vPort1[2]) vip = np.sqrt(vdss * vdss + deltav * deltav) vip -= np.sqrt(pow(vds - vdss, 2) + deltav * deltav) deltal = np.log(1.0 + (vds - vip) / self._lc / self._t_ucrit) deltal *= self.Lambda * self._lc # Equivalent channel length including channel-length # modulation and velocity saturation lprime = self.ns * self._leff - deltal + (vds + vip) / self._t_ucrit leq = 0.5 * (lprime + np.sqrt(lprime * lprime + self._lmin * self._lmin)) # Reverse normalized current tmp = vp - vds - vPort1[2] tmp -= np.sqrt(vdssprime * vdssprime + deltav * deltav) tmp += np.sqrt(pow(vds - vdssprime, 2) + deltav * deltav) irprime = self.interp(tmp / self._Vt) # Reverse normalized currect for mobility model, intrinsic # charges/capacitances, thermal noise model and NQS time-constant ??? ir = self.interp((vp - vPort1[0]) / self._Vt) # Quasi-static model equations # Dynamic model for the intrinsic node charges sqvpphi = np.sqrt(vp + self.phiT + 1.0e-6) nq = 1.0 + self._gammaa / 2.0 / sqvpphi # Normalized intrinsic node charges xf = np.sqrt(0.25 + i_f) xr = np.sqrt(0.25 + ir) tmp = pow(xf + xr, 2) qd = 3.0 * xr ** 3 + 6.0 * xr * xr * xf + 4.0 * xr * xf * xf + 2.0 * xf ** 3 qd *= 4.0 / 15.0 / tmp qd = -nq * (qd - 0.5) qs = 3.0 * xf ** 3 + 6.0 * xf * xf * xr + 4.0 * xf * xr * xr + 2.0 * xr ** 3 qs *= 4.0 / 15.0 / tmp qs = -nq * (qs - 0.5) qi = qs + qd qb1 = -self._gammaa * sqvpphi / self._Vt qb1 -= (nq - 1.0) * qi / nq qb = ad.condassign(vgprime, qb1, -vgprime / self._Vt) # qg = -qi - qox - qb, but qox == 0, so: qg = -qi - qb - self.qox # Transconductance factor and mobility reduction due to vertical field betao = self.kpa * self.np * self._weff / leq if self.theta != 0.0: # Simple mobility reduction model vpprime = 0.5 * (vp + np.sqrt(vp * vp + 2.0 * self._Vt * self._Vt)) beta = betao / (1.0 + theta * vpprime) else: # Rigorous mobility reduction model betaoprime = 1.0 + self.cox * self._qb0 / self.e0 / const.epSi betaoprime *= betao tmp = np.abs(qb + self.eta * qi) tmp = 1.0 + self.cox * self._Vt * tmp / self.e0 / const.epSi beta = betaoprime / tmp # Specific current IS = 2.0 * n * beta * self._Vt * self._Vt # Drain-to-source current ids = IS * (i_f - irprime) # import pdb; pdb.set_trace() # Impact ionization current vib = vPort1[0] - vPort1[2] - 2.0 * self.ibn * vdss idb1 = ids * self.iba * vib / self._t_ibb idb1 *= ad.safe_exp(-self._t_ibb * self._lc / vib) idb = ad.condassign(vib, idb1, 0.0) # ------------------------------------------------------------- # Create output vectors qVec = np.array([0.0, qg, 0.0], dtype=type(idb)) # have to switch charges if Drain and Source voltages switched qVec[0] = ad.condassign(DtoSswap, qd, qs) qVec[2] = ad.condassign(DtoSswap, qs, qd) # De-normalize charge and invert if needed qVec *= self._Cox * self._Vt * self._tf iVec = np.array([0.0, 0.0, 0.0], dtype=type(idb)) iVec[0] = DtoSswap * ids iVec[1] = ad.condassign(DtoSswap, idb, 0.0) iVec[2] = ad.condassign(DtoSswap, 0.0, idb) # Revert currents if needed iVec *= self._tf # -------------------------------------------------------------- # Operating point information if getOP: # Vth Vth = self._vt0a + self._deltavRSCE + gammaprime * np.sqrt(vsprime) - self._gammaa * np.sqrt(self.phiT) # Non quasi-static equations tau0 = 0.5 * self._Cox / self._Vt / beta tmp = (xf ** 2 + 3.0 * xf * xr + xr ** 2) / pow(xf + xr, 3) tau = tau0 * 4.0 / 15.0 * tmp # Create operating point variables dictionary return { "Vp": vp, "n": n, "Beta": beta, "IS": IS, "IF": i_f, "IR": ir, "IRprime": irprime, "tef": 1.0 / (np.sqrt(0.25 + i_f) + 0.5), "Vth": Vth, "Vov": self._tf * n * (vp - vPort[2]), "Vdsat": self._tf * self._Vt * (2.0 * np.sqrt(i_f) + 4.0), "tau0": tau0, "tau": tau, "Sthermal": self._kSt * beta * np.abs(qi), "Reversed": DtoSswap < 0.0, } else: return (iVec, qVec)
def eval_cqs(self, vPort, getOP = False): """ Calculates currents and charges Input: vPort = [vdb , vgb , vsb] Output: iVec = [ids, idb, isb], qVec = [qd, qg, qs] If getOP = True, return normal output vector plus operating point variables in tuple: (iVec, qVec, opV) """ # import pdb; pdb.set_trace() # Invert all voltages in case of a P-channel device vPort1 = self._tf * vPort # If vds is negative, swap Vd and Vs and (also swap charges # later) DtoSswap = ad.condassign(vPort1[0] - vPort1[2], 1., -1.) # perform the swap (need tmp variable) tmp = ad.condassign(DtoSswap, vPort1[0], vPort1[2]) vPort1[2] = ad.condassign(DtoSswap, vPort1[2], vPort1[0]) vPort1[0] = tmp # Calculate VDS, VGS and VBS for bsim model VDS = vPort1[0] - vPort1[2] VGS = vPort1[1] - vPort1[2] VBS = -vPort1[2] # ---------------------------------------------------------------- T0 = VBS - self.vbsc - 0.001 T1 = np.sqrt(T0 * T0 - 0.004 * self.vbsc) Vbseff = self.vbsc + .5 * (T0 + T1) Vbseff = ad.condassign(-Vbseff + VBS, VBS, Vbseff) #Calculation of Phis, sqrtPhis and Xdep Phis = ad.condassign(Vbseff, self.phi**2 / (self.phi + Vbseff), self.phi - Vbseff) sqrtPhis = ad.condassign(Vbseff, self.phis3 / (self.phi + 0.5 * Vbseff), np.sqrt(Phis)) Xdep = self.Xdep0 * sqrtPhis / self.sqrtPhi #Calculation of Threshold voltage-vth T3 = np.sqrt(Xdep) T1 = ad.condassign(self.dvt2 * Vbseff + .5, 1. + self.dvt2 * Vbseff, (1. + 3. * self.dvt2 * Vbseff) / (3. + 8. * self.dvt2 * Vbseff)) ltl = self.factor1 * T3 * T1 T1 = ad.condassign(self.dvt2w * Vbseff + .5, 1. + self.dvt2w * Vbseff, (1. + 3. * self.dvt2w * Vbseff) / (3. + 8. * self.dvt2w * Vbseff)) ltw = self.factor1 * T3 * T1 # Alternative to prevent overflow (apparently not needed) #T2 = ad.safe_exp(-.5 * self.dvt1 * self.leff / ltl) k_temp = -.5 * self.dvt1 * self.leff / ltl T2 = ad.condassign(k_temp + EXP_THRESHOLD, ad.safe_exp(k_temp), MIN_EXP) Theta0 = T2 * (1. + 2. * T2) thetavth = self.dvt0 * Theta0 Delt_vth = thetavth * self.V0 # Alternative to prevent overflow (apparently not needed) #T2 = ad.safe_exp(-.5 * self.dvt1w * self.weff * self.leff / ltw) # Modified from freeda's source to prevent using uninitialized # variable k_temp = -.5 * self.dvt1w * self.weff * self.leff / ltw T2 = ad.condassign(k_temp + EXP_THRESHOLD, ad.safe_exp(k_temp), MIN_EXP) T2 *= (1. + 2. * T2) T0 = self.dvt0w * T2 T2 = T0 * self.V0 T0 = np.sqrt(1. + self.nlx / self.leff) T1 = self.k1ox * (T0 - 1.) * self.sqrtPhi \ + (self.kt1 + self.kt1l / self.leff + self.kt2 * Vbseff) \ * self._ToTnm1 TMP2 = self.tox * self.phi / (self.weff + self.w0) T3 = self.eta0 + self.etab * Vbseff T4 = ad.condassign(-T3 + 1.0e-4, 1. / (3. - 2e4 * self.eta0 + self.etab * Vbseff), 1.) dDIBL_Sft_dVd = T3 * self.theta0vb0 DIBL_Sft = dDIBL_Sft_dVd * VDS Vth = self._vth0 - self._k1 * self.sqrtPhi + self.k1ox * sqrtPhis \ - self.k2ox * Vbseff - Delt_vth - T2 \ + (self.k3 + self.k3b * Vbseff) * TMP2 + T1 - DIBL_Sft #Calculate n temp_tmp2 = self.nfactor * const.epSi / Xdep temp_tmp3 = self.cdsc + self.cdscb * Vbseff + self.cdscd * VDS temp_tmp4 = (temp_tmp2 + temp_tmp3 * Theta0 + self.cit) / self.cox n = ad.condassign(temp_tmp4 + .5, 1. + temp_tmp4, (1. + 3. * temp_tmp4) \ * (1. / (3. + 8. * temp_tmp4))) #Poly Gate Si Depletion Effect Vgs_eff = VGS Vgst = Vgs_eff - Vth # not in Nikhil's code #Effective Vgst (Vgsteff) Calculation T10 = 2. * n * self._Vt VgstNVt = Vgst / T10 ExpArg = -(2. * 0.08 + Vgst) / T10 T1 = T10 * log1pexp(VgstNVt) T2 = 1. + T10 * self.cox * ad.safe_exp(ExpArg) / self._Vt / self.cdep0 Vgsteff = ad.condassign(VgstNVt - EXP_THRESHOLD, Vgst, T1 / T2) Vgsteff = ad.condassign( ExpArg - EXP_THRESHOLD, self._Vt * self.cdep0 \ / self.cox / ad.safe_exp((Vgst + 0.08) / n / self._Vt), T1 / T2) T3 = T2 * T2 # Calculate Effective Channel Geometry T9 = sqrtPhis - self.sqrtPhi k_temp = self.weff - 2. * (self.dwg * Vgsteff + self.dwb * T9) Weff = ad.condassign(-k_temp + 2.0e-8, 2e-8 * (4.0e-8 - k_temp) * T0, k_temp) T0 = self.prwg * Vgsteff + self.prwb * (sqrtPhis - self.sqrtPhi) Rds = ad.condassign(T0 + 0.9, self.rds0 * (1. + T0), self.rds0 * (.8 +T0) / (17. + 20. * T0)) #Calculate Abulk T1 = 0.5 * self.k1ox / sqrtPhis T9 = np.sqrt(self.xj * Xdep) T5 = self.leff / (self.leff + 2. * T9) T2 = (self.a0 * T5) + self.b0 / (self.weff + self.b1) T6 = T5 * T5 T7 = T5 * T6 Abulk0 = 1. + T1 * T2 T8 = self.ags * self.a0 * T7 Abulk = Abulk0 - T1 * T8 * Vgsteff Abulk0 = ad.condassign(-Abulk0 + .1, (.2 - Abulk0) / (3. - 20. * Abulk0), Abulk0) Abulk = ad.condassign(-Abulk + .1, (.2 - Abulk) / (3. - 20. * Abulk), Abulk) T2 = self.keta * Vbseff T0 = ad.condassign(T2 + 0.9, 1. / (1. + T2), (17. + 20. * T2) / (0.8 + T2)) Abulk *= T0 Abulk0 *= T0 T0 = Vgsteff + 2. * Vth T2 = self._ua + self._uc * Vbseff T3 = T0 / self.tox T5 = T3 * (T2 + self._ub * T3) Denomi = ad.condassign(T5 + .8, 1. + T5, (.6 + T5) / (7. + 10. * T5)) ueff = self.u0temp / Denomi Esat = 2. * self.vsattemp / ueff # Saturation Drain Voltage Vdsat WVCox = Weff * self.vsattemp * self.cox WVCoxRds = WVCox * Rds EsatL = Esat * self.leff Lambda = self.a2 Vgst2Vtm = Vgsteff + 2. * self._Vt T0 = 1. / (Abulk * EsatL + Vgst2Vtm) T3 = EsatL * Vgst2Vtm Vdsat = T3 * T0 # Effective Vds(Vdseff) Calculation T1 = Vdsat - VDS - self.delta T2 = np.sqrt(T1**2 + 4. * self.delta * Vdsat) #T0 = T1 / T2 #T3 = 2. * self.delta / T2 k_temp = Vdsat - .5 * (T1 + T2) Vdseff = ad.condassign(k_temp - VDS, VDS, k_temp) # The following seems unnnecessary: # Added to eliminate non-zero Vdseff at Vds=0.0 #Vdseff = ad.condassign(abs(VDS), # Vdseff, # 0.) # Calculate Vasat T6 = 1. - .5 * Abulk * Vdsat / Vgst2Vtm #T6=tmp4 T9 = WVCoxRds * Vgsteff #expanded T0 = EsatL + Vdsat + 2. * T9 * T6 T9 = WVCoxRds * Abulk T1 = 2. / Lambda - 1. + T9 Vasat = T0 / T1 diffVds = VDS - Vdseff #Calculate VACLM VACLM = ad.condassign( (diffVds - 1.0e-10) * self.pclm, self.leff * (Abulk + Vgsteff / EsatL) * diffVds \ / (self.pclm * Abulk * self.litl), MAX_EXP) #Calculate VADIBL T1 = np.sqrt(const.epSi / const.epOx * self.tox * self.Xdep0) T0 = ad.safe_exp(-.5 * self.drout * self.leff / T1) T2 = T0 + 2. * T0**2 thetaRout = self.pdibl1 * T2 + self.pdibl2 #drout, pdibl1, #pdibl2 are given VADIBL = ad.condassign( thetaRout, (Vgst2Vtm - Vgst2Vtm * Abulk * Vdsat / (Vgst2Vtm + Abulk * Vdsat)) \ / thetaRout, MAX_EXP) VADIBL = ad.condassign(self.pdiblb * Vbseff + 0.9, VADIBL / (1. + self.pdiblb * Vbseff), VADIBL * (17. + 20. * self.pdiblb * Vbseff) \ / (.8 + self.pdiblb * Vbseff)) #Calculate Va T8 = self.pvag / EsatL T9 = T8 * Vgsteff T0 = ad.condassign(T9 + 0.9, 1. + T9, (.8 + T9) / (17. + 20. * T9)) T3 = VACLM + VADIBL #tmp3 = T3 T1 = VACLM * VADIBL / T3 Va = Vasat + T0 * T1 #Calculate VASCBE if self.pscbe1 != 0.: rcpVASCBE = ad.condassign( abs(diffVds), self.pscbe2 * ad.safe_exp(-self.pscbe1 * self.litl/diffVds) \ / self.leff, 0.) else: rcpVASCBE = self.pscbe2 / self.leff # Original: #VASCBE = ad.condassign( # diffVds - self.pscbe1 * self.litl / EXP_THRESHOLD, # self.leff * np.exp(self.pscbe1 * self.litl/diffVds) / self.pscbe2, # MAX_EXP * self.leff / self.pscbe2) #Calculate Ids CoxWovL = self.cox * Weff / self.leff beta = ueff * CoxWovL T0 = 1. - .5 * Abulk * Vdseff / Vgst2Vtm fgche1 = Vgsteff * T0 T9 = Vdseff / EsatL fgche2 = 1. + T9 gche = beta * fgche1 / fgche2 T0 = 1. + gche * Rds T9 = Vdseff / T0 Idl = gche * T9 T9 = diffVds / Va T0 = 1. + T9 Idsa = Idl * T0 T9 = diffVds * rcpVASCBE T0 = 1. + T9 Ids = Idsa * T0 #Substrate current begins T1 = self.alpha0 + self.alpha1 * self.leff k_temp = T1 / self.leff * diffVds #T2 = k_temp * ad.safe_exp(-self.beta0 / diffVds) # Original T2: ad.condassign( diffVds - self.beta0 / EXP_THRESHOLD, k_temp * ad.safe_exp(-self.beta0 / diffVds), k_temp * MIN_EXP) k_temp = ad.condassign(T1, T2 * Idsa, 0.) Isub = ad.condassign(self.beta0, k_temp, 0.) # ********* Output current vector ************** iVec = np.array([0., 0., 0.], dtype=type(Ids)) iVec[0] = DtoSswap * Ids iVec[1] = ad.condassign(DtoSswap, Isub, 0.) iVec[2] = ad.condassign(DtoSswap, 0., Isub) # Revert currents if needed iVec *= self._tf # ********************************************** #-------------------------------------------------------------- # Charge calculation follows (does not work): #calculation of vfbzb T0 = -.5 * self.dvt1w * self.weff * self.leff \ / self.factor1 / np.sqrt(self.Xdep0) #T2 = ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)) T2 = ad.condassign(T0 + EXP_THRESHOLD, ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)), MIN_1) T0 = self.dvt0w * T2 T2 = T0 * (self.vbi - self.phi) T0 = -.5 * self.dvt1 * self.leff / (self.factor1 * np.sqrt(self.Xdep0)) #T3 = ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)) T3 = ad.condassign(T0 + EXP_THRESHOLD, ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)), MIN_1) T3 = self.dvt0 * T3 * (self.vbi - self.phi) T4 = self.tox * self.phi / (self.weff + self.w0) T5 = self.k1ox * (T0 - 1.) * self.sqrtPhi \ + (self.kt1 + self.kt1l / self.leff) * self._ToTnm1 T0 = np.sqrt(1. + self.nlx / self.leff) T6 = self._vth0 - T2 -T3 + self.k3 * T4 + T5 vfbzb = T6 - self.phi - self._k1 * self.sqrtPhi #Calculation for VbseffCV VbseffCV = ad.condassign(Vbseff, self.phi - Phis, Vbseff) #Calculation for VgsteffCV T0 = n * self.noff * self._Vt T1 = (Vgs_eff - Vth) / T0 Vgsteff = ad.condassign(T1 - EXP_THRESHOLD, Vgs_eff - Vth - self.voffcv, T0 * log1pexp(T1)) # This (after the previous) can not be right: #Vgsteff = ad.condassign(-T1 - EXP_THRESHOLD, # T0 * log(1. + MIN_EXP), # T0 * log(1. + np.exp(T1))) #Calculation for Vfbeff V3 = vfbzb - Vgs_eff + VbseffCV - .02 T0 = np.sqrt(V3**2 + .08 * abs(vfbzb)) Vfbeff = vfbzb - 0.5 * (V3 + T0) T0 = (Vgs_eff - VbseffCV - vfbzb) / self._Tox #Calculation for Tcen T1 = T0 * self.acde Tcen = ad.condassign(EXP_THRESHOLD + T1, self.ldeb * np.exp(T1), self.ldeb * MIN_EXP) Tcen = ad.condassign(-EXP_THRESHOLD + T1, self.ldeb * MAX_EXP, Tcen) V3 = self.ldeb - Tcen - 1e-3 * self.tox V4 = np.sqrt(V3**2 + 4e-3 * self.tox * self.ldeb) Tcen = self.ldeb - .5 * (V3 + V4) Ccen = const.epSi / Tcen Coxeff = Ccen * self.cox / (Ccen + self.cox) #Calculation for QoverlapCox CoxWLcen = Weff * self.leff * Coxeff Qac0 = CoxWLcen * (Vfbeff - vfbzb) # QovCox = Qac0 / Coxeff T0 = .5 * self.k1ox T3 = Vgs_eff - Vfbeff - VbseffCV - Vgsteff T1 = np.sqrt(T0 * T0 + T3) T2 = CoxWLcen * T0 / T1 T1 = ad.condassign(-T3, T0 + T3 / self.k1ox, np.sqrt(T0**2 + T3)) T2 = ad.condassign(-T3, CoxWLcen, CoxWLcen * T0 / T1) Qsub0 = CoxWLcen * self.k1ox * (T1 - T0) # QovCox = Qsub0 / Coxeff #Calculation for Delta_phis T2 = ad.condassign(self.k1ox, self.moin * self._Vt * self.k1ox**2, .25 * self.moin * self._Vt) T0 = ad.condassign(self.k1ox, self.k1ox * np.sqrt(self.phi), .5 * np.sqrt(self.phi)) T1 = 2. * T0 + Vgsteff DeltaPhi = self._Vt * np.log(1. + T1 * Vgsteff / T2) #The calculation for Tcen must be done once more # fREEDA: #T0 = (Vgsteff + 4.*(self._vth0 - self.vfb - self.phi))/ (2. * self._Tox) # ngspice: T3 = 4. * (Vth - vfbzb - self.phi) T0 = ad.condassign(T3, .5 * (Vgsteff + T3) / self._Tox, .5 * (Vgsteff + 1.0e-20) / self._Tox) k_temp = 2.01375270747048 * T0 # was np.exp(.7 * log(T0)) T1 = 1. + k_temp T2 = 0.35 * k_temp / (T0 * self._Tox) Tcen = 1.9e-9 / T1 Ccen = const.epSi / Tcen Coxeff = Ccen * self.cox / (Ccen + self.cox) CoxWLcen = Weff * self.leff * Coxeff AbulkCV = Abulk0 * self.AbulkCVfactor VdsatCV = (Vgsteff - DeltaPhi) / AbulkCV T0 = VdsatCV - VDS - .02 T1 = np.sqrt(T0**2 + .08 * VdsatCV) # From ngspice: internal version BSIM3v32V32 VdseffCV = VdsatCV - .5 * (T0 + T1) # From freeda: # VdseffCV = ad.condassign(T0, # VdsatCV - .5 * (T0 + T1), # VdsatCV * (1. - .04/(T1-T0))) # Need this to prevent NaN in charge Jacobian VdseffCV += 1e-200 # Seems not needed #VdseffCV = ad.condassign(abs(VDS), # Vdseff, # 0.) T0 = AbulkCV * VdseffCV T1 = Vgsteff - DeltaPhi T2 = 12. * (T1 - .5 * T0 + 1e-20) T3 = T0 / T2 T4 = 1. - 12. * T3**2 T5 = AbulkCV * (6. * T0 * (4. * T1 - T0) / (T2**2) - .5) T6 = T5 * VdseffCV / AbulkCV qgate = CoxWLcen * (T1 - T0 * (.5 - T3)) qbulk = CoxWLcen * (1. - AbulkCV) * (.5*VdseffCV - T0*VdseffCV/T2) #QovCox = qbulk / Coxeff T2 = T2 / 12. T3 = .5 * CoxWLcen / (T2**2) T4 = T1 * (2. * T0**2 / 3. + T1*(T1 - 4. * T0 / 3.)) - 2. * T0**3 / 15. qsrc = -T3 * T4 qgate += Qac0 + Qsub0 - qbulk qbulk -= (Qac0 + Qsub0) qdrn = -(qbulk + qgate + qsrc) # ************ Output charge vector **************** qVec = np.array([0., qgate, 0.], dtype=type(qsrc)) # have to switch charges if Drain and Source voltages switched qVec[0] = ad.condassign(DtoSswap, qdrn, qsrc) qVec[2] = ad.condassign(DtoSswap, qsrc, qdrn) # invert sign if needed qVec *= self._tf # Return numpy array with one element per current source. return (iVec, qVec)
def eval_cqs(self, vPort, getOP=False): """ Calculates Ids, Idb, Isb currents and D, G, S, charges. Input: vPort = [vdb , vgb , vsb] Output: vector with Ids, Idb, Isb currents and vector with D, G, S charges. If getOP = True, return normal output vector plus operating point variables in tuple: (iVec, qVec, opV) """ # Invert all voltages in case of a P-channel device vPort1 = self._tf * vPort # If vds is negative, swap Vd and Vs and (also swap charges # later) DtoSswap = ad.condassign(vPort1[0] - vPort1[2], 1., -1.) # perform the swap (need tmp variable) tmp = ad.condassign(DtoSswap, vPort1[0], vPort1[2]) vPort1[2] = ad.condassign(DtoSswap, vPort1[2], vPort1[0]) vPort1[0] = tmp # Effective gate voltage including reverse short channel effect vgprime = vPort1[1] - self._vt0a - self._deltavRSCE \ + self.phiT + self._gammaa * np.sqrt(self.phiT) # Effective substrate factor including charge-sharing for # short and narrow channels. Pinch-off voltage for # narrow-channel effect vp0 = vgprime - self.phiT - \ self._gammaa * (np.sqrt(vgprime + self._gammaa*self._gammaa / 4.) - self._gammaa / 2.) vp0 = ad.condassign(vgprime, vp0, -self.phiT) # Effective substrate factor accounting for charge-sharing tmp = 16. * self._Vt * self._Vt vsprime = 0.5 * (vPort1[2] + self.phiT + np.sqrt(pow(vPort1[2] + self.phiT, 2) + tmp)) vdprime = 0.5 * (vPort1[0] + self.phiT + np.sqrt(pow(vPort1[0] + self.phiT, 2) + tmp)) tmp = self.leta / self._leff * (np.sqrt(vsprime) + np.sqrt(vdprime)) tmp -= 3. * self.weta * np.sqrt(vp0 + self.phiT) / self._weff gammao = self._gammaa - const.epSi / self.cox * tmp gammaprime = 0.5 * (gammao + np.sqrt(gammao * gammao + 0.1 * self._Vt)) # Pinch-off voltage including short- and narrow-channel effect vp = vgprime - self.phiT vp -= gammaprime * (np.sqrt(vgprime + pow(.5 * gammaprime, 2)) - gammaprime / 2.) vp = ad.condassign(vgprime, vp, -self.phiT) # Slope factor n = 1 + self._gammaa / (2. * np.sqrt(vp + self.phiT + 4. * self._Vt)) # Forward normalized current i_f = (vp - vPort1[2]) / self._Vt i_f = self.interp(i_f) # Velocity saturation voltage vdss = np.sqrt(0.25 + self._Vt * np.sqrt(i_f) / self._vc) - 0.5 vdss *= self._vc # Drain-to-source saturation voltage for reverse normalized current tmp = np.sqrt(i_f) - 0.75 * np.log(i_f) vdssprime = np.sqrt(0.25 + self._Vt * tmp / self._vc) - 0.5 vdssprime *= self._vc vdssprime += self._Vt * (np.log(self._vc / (2. * self._Vt)) - 0.6) # Channel-length modulation tmp = np.sqrt(i_f) - vdss / self._Vt deltav = np.sqrt(self.Lambda * tmp + 1. / 64) deltav *= 4. * self._Vt vds = .5 * (vPort1[0] - vPort1[2]) vip = np.sqrt(vdss * vdss + deltav * deltav) vip -= np.sqrt(pow(vds - vdss, 2) + deltav * deltav) deltal = np.log(1. + (vds - vip) / self._lc / self._t_ucrit) deltal *= self.Lambda * self._lc # Equivalent channel length including channel-length # modulation and velocity saturation lprime = self.ns * self._leff - deltal + (vds + vip) / self._t_ucrit leq = 0.5 * (lprime + np.sqrt(lprime * lprime + self._lmin * self._lmin)) # Reverse normalized current tmp = vp - vds - vPort1[2] tmp -= np.sqrt(vdssprime * vdssprime + deltav * deltav) tmp += np.sqrt(pow(vds - vdssprime, 2) + deltav * deltav) irprime = self.interp(tmp / self._Vt) # Reverse normalized currect for mobility model, intrinsic # charges/capacitances, thermal noise model and NQS time-constant ??? ir = self.interp((vp - vPort1[0]) / self._Vt) # Quasi-static model equations # Dynamic model for the intrinsic node charges sqvpphi = np.sqrt(vp + self.phiT + 1.e-6) nq = 1. + self._gammaa / 2. / sqvpphi # Normalized intrinsic node charges xf = np.sqrt(0.25 + i_f) xr = np.sqrt(0.25 + ir) tmp = pow(xf + xr, 2) qd = 3. * xr**3 + 6. * xr * xr * xf + 4. * xr * xf * xf + 2. * xf**3 qd *= 4. / 15. / tmp qd = -nq * (qd - .5) qs = 3. * xf**3 + 6. * xf * xf * xr + 4. * xf * xr * xr + 2. * xr**3 qs *= 4. / 15. / tmp qs = -nq * (qs - .5) qi = qs + qd qb1 = -self._gammaa * sqvpphi / self._Vt qb1 -= (nq - 1.) * qi / nq qb = ad.condassign(vgprime, qb1, -vgprime / self._Vt) # qg = -qi - qox - qb, but qox == 0, so: qg = -qi - qb - self.qox # Transconductance factor and mobility reduction due to vertical field betao = self.kpa * self.np * self._weff / leq if self.theta != 0.: # Simple mobility reduction model vpprime = 0.5 * (vp + np.sqrt(vp * vp + 2. * self._Vt * self._Vt)) beta = betao / (1. + theta * vpprime) else: # Rigorous mobility reduction model betaoprime = 1. + self.cox * self._qb0 / self.e0 / const.epSi betaoprime *= betao tmp = np.abs(qb + self.eta * qi) tmp = 1. + self.cox * self._Vt * tmp / self.e0 / const.epSi beta = betaoprime / tmp # Specific current IS = 2. * n * beta * self._Vt * self._Vt # Drain-to-source current ids = IS * (i_f - irprime) # import pdb; pdb.set_trace() # Impact ionization current vib = vPort1[0] - vPort1[2] - 2. * self.ibn * vdss idb1 = ids * self.iba * vib / self._t_ibb idb1 *= ad.safe_exp(-self._t_ibb * self._lc / vib) idb = ad.condassign(vib, idb1, 0.) # ------------------------------------------------------------- # Create output vectors qVec = np.array([0., qg, 0.], dtype=type(idb)) # have to switch charges if Drain and Source voltages switched qVec[0] = ad.condassign(DtoSswap, qd, qs) qVec[2] = ad.condassign(DtoSswap, qs, qd) # De-normalize charge and invert if needed qVec *= self._Cox * self._Vt * self._tf iVec = np.array([0., 0., 0.], dtype=type(idb)) iVec[0] = DtoSswap * ids iVec[1] = ad.condassign(DtoSswap, idb, 0.) iVec[2] = ad.condassign(DtoSswap, 0., idb) # Revert currents if needed iVec *= self._tf #-------------------------------------------------------------- # Operating point information if getOP: # Vth Vth = self._vt0a + self._deltavRSCE \ + gammaprime * np.sqrt(vsprime) \ - self._gammaa * np.sqrt(self.phiT) # Non quasi-static equations tau0 = .5 * self._Cox / self._Vt / beta tmp = (xf**2 + 3. * xf * xr + xr**2) / pow(xf + xr, 3) tau = tau0 * 4. / 15. * tmp # Create operating point variables dictionary return { 'Vp': vp, 'n': n, 'Beta': beta, 'IS': IS, 'IF': i_f, 'IR': ir, 'IRprime': irprime, 'tef': 1. / (np.sqrt(.25 + i_f) + .5), 'Vth': Vth, 'Vov': self._tf * n * (vp - vPort[2]), 'Vdsat': self._tf * self._Vt * (2. * np.sqrt(i_f) + 4.), 'tau0': tau0, 'tau': tau, 'Sthermal': self._kSt * beta * np.abs(qi), 'Reversed': DtoSswap < 0. } else: return (iVec, qVec)
def eval_cqs(self, vPort): """ Calculates currents/charges Input is a vector may be one of the following, depending on parameter values:: vPort = [vbe, vbc] vPort = [vbie, vbic, v1_i] (gyrator voltage, rb != 0) Output also depends on parameter values. Charges only present if parameters make them different than 0 (i.e., cje, tf, cjc, etc. are set to nonzero values):: iVec = [ibe, ibc, ice] iVec = [ibe, ibc, ice, gyr*ib*Rb] (rb != 0) qVec = [qbe, qbc] qVec = [qbe, qbc, qbx] (rb != 0 and cjc != 1) """ # Invert control voltages if needed vPort1 = self._typef * vPort # Calculate regular PN junctions currents and charges ibf = self.jif.get_id(vPort1[0]) ibr = self.jif.get_id(vPort1[1]) if self.ise: ile = self.jile.get_id(vPort1[0]) else: ile = 0. if self.isc: ilc = self.jilc.get_id(vPort1[1]) else: ilc = 0. # Kqb q1m1 = 1. if self.var: q1m1 -= vPort1[0] / self.var if self.vaf: q1m1 -= vPort1[1] / self.vaf kqb = 1. / q1m1 q2 = 0. if self.ikf: q2 += ibf / self.ikf if self.ikr: q2 += ibr / self.ikr if q2: kqb *= .5 * (1. + np.sqrt(1. + 4. * q2)) # Create output vector [ibe, ibc, ice, ...] iVec = np.zeros(self.ncurrents, dtype = type(ibf)) qVec = np.zeros(self.ncharges, dtype = type(ibf)) # ibe iVec[0] = ibf / self._bf_t + ile # ibc iVec[1] = ibr / self._br_t + ilc # ice iVec[2] = (ibf - ibr) / kqb # RB if self.rb: # Using gyrator # vPort1[2] not defined if rb == 0 # ib has area effect included (removed by _ck1 and _ck2) ib = vPort1[2] * glVar.gyr if self.irb: ib1 = np.abs(ib) x = np.sqrt(1. + self._ck1 * ib1) - 1. x *= self._ck2 / np.sqrt(ib1) tx = np.tan(x) c = self.rbm + 3. * (self.rb - self.rbm) \ * (tx - x) / (x * tx * tx) rb = ad.condassign(ib1, c, self.rb) else: rb = self.rbm + (self.rb - self.rbm) / kqb # Output is gyr * ib * rb. It is divided by area^2 to # compensate that the whole vector is multiplied by area # at the end iVec[3] = glVar.gyr * ib * rb / pow(self.area, 2) vbcx = ib * rb / self.area + vPort1[1] # Charges ----------------------------------------------- # Note that if tf == 0 and cje == 0, nothing is calculated and # nothing is assigned to the output vector. # qbe is the first charge (0) if self.tf: # Effective tf tfeff = self.tf if self.vtf: x = ibf / (ibf + self.itf) tfeff *= (1. + self.xtf * x*x * ad.safe_exp(vPort1[1] /1.44 /self.vtf)) qVec[0] = tfeff * ibf if self.cje: qVec[0] += self.jif.get_qd(vPort1[0]) # qbc if self._qbx: if self.tr: qVec[-2] = self.tr * ibr if self.cjc: qVec[-2] += self.jir.get_qd(vPort1[1]) * self.xcjc # qbx qVec[-1] = self.jir.get_qd(vbcx) * (1. - self.xcjc) else: if self.tr: qVec[-1] = self.tr * ibr if self.cjc: qVec[-1] += self.jir.get_qd(vPort1[1]) # Consider area effect and invert currents if needed iVec *= self.area * self._typef qVec *= self.area * self._typef return (iVec, qVec)
def eval_cqs(self, vPort, saveOP = False): """ Calculates currents and charges Input: vPort = [vdb , vgb , vsb] Output: iVec = [ids, idb, isb], qVec = [qd, qg, qs] If saveOP = True, return normal output vector plus operating point variables in tuple: (iVec, qVec, opV) """ # import pdb; pdb.set_trace() # Invert all voltages in case of a P-channel device vPort1 = self._tf * vPort # If vds is negative, swap Vd and Vs and (also swap charges # later) DtoSswap = ad.condassign(vPort1[0] - vPort1[2], 1., -1.) # perform the swap (need tmp variable) tmp = ad.condassign(DtoSswap, vPort1[0], vPort1[2]) vPort1[2] = ad.condassign(DtoSswap, vPort1[2], vPort1[0]) vPort1[0] = tmp # Calculate VDS, VGS and VBS for bsim model VDS = vPort1[0] - vPort1[2] VGS = vPort1[1] - vPort1[2] VBS = -vPort1[2] # ---------------------------------------------------------------- T0 = VBS - self.vbsc - 0.001 T1 = np.sqrt(T0 * T0 - 0.004 * self.vbsc) Vbseff = self.vbsc + .5 * (T0 + T1) Vbseff = ad.condassign(-Vbseff + VBS, VBS, Vbseff) #Calculation of Phis, sqrtPhis and Xdep Phis = ad.condassign(Vbseff, self.phi**2 / (self.phi + Vbseff), self.phi - Vbseff) sqrtPhis = ad.condassign(Vbseff, self.phis3 / (self.phi + 0.5 * Vbseff), np.sqrt(Phis)) Xdep = self.Xdep0 * sqrtPhis / self.sqrtPhi #Calculation of Threshold voltage-vth T3 = np.sqrt(Xdep) T1 = ad.condassign(self.dvt2 * Vbseff + .5, 1. + self.dvt2 * Vbseff, (1. + 3. * self.dvt2 * Vbseff) / (3. + 8. * self.dvt2 * Vbseff)) ltl = self.factor1 * T3 * T1 T1 = ad.condassign(self.dvt2w * Vbseff + .5, 1. + self.dvt2w * Vbseff, (1. + 3. * self.dvt2w * Vbseff) / (3. + 8. * self.dvt2w * Vbseff)) ltw = self.factor1 * T3 * T1 # Alternative to prevent overflow (apparently not needed) #T2 = ad.safe_exp(-.5 * self.dvt1 * self.leff / ltl) k_temp = -.5 * self.dvt1 * self.leff / ltl T2 = ad.condassign(k_temp + EXP_THRESHOLD, ad.safe_exp(k_temp), MIN_EXP) Theta0 = T2 * (1. + 2. * T2) thetavth = self.dvt0 * Theta0 Delt_vth = thetavth * self.V0 # Alternative to prevent overflow (apparently not needed) #T2 = ad.safe_exp(-.5 * self.dvt1w * self.weff * self.leff / ltw) # Modified from freeda's source to prevent using uninitialized # variable k_temp = -.5 * self.dvt1w * self.weff * self.leff / ltw T2 = ad.condassign(k_temp + EXP_THRESHOLD, ad.safe_exp(k_temp), MIN_EXP) T2 *= (1. + 2. * T2) T0 = self.dvt0w * T2 T2 = T0 * self.V0 T0 = np.sqrt(1. + self.nlx / self.leff) T1 = self.k1ox * (T0 - 1.) * self.sqrtPhi \ + (self.kt1 + self.kt1l / self.leff + self.kt2 * Vbseff) \ * self._ToTnm1 TMP2 = self.tox * self.phi / (self.weff + self.w0) T3 = self.eta0 + self.etab * Vbseff T4 = ad.condassign(-T3 + 1.0e-4, 1. / (3. - 2e4 * self.eta0 + self.etab * Vbseff), 1.) dDIBL_Sft_dVd = T3 * self.theta0vb0 DIBL_Sft = dDIBL_Sft_dVd * VDS Vth = self._vth0 - self._k1 * self.sqrtPhi + self.k1ox * sqrtPhis \ - self.k2ox * Vbseff - Delt_vth - T2 \ + (self.k3 + self.k3b * Vbseff) * TMP2 + T1 - DIBL_Sft #Calculate n temp_tmp2 = self.nfactor * const.epSi / Xdep temp_tmp3 = self.cdsc + self.cdscb * Vbseff + self.cdscd * VDS temp_tmp4 = (temp_tmp2 + temp_tmp3 * Theta0 + self.cit) / self.cox n = ad.condassign(temp_tmp4 + .5, 1. + temp_tmp4, (1. + 3. * temp_tmp4) \ * (1. / (3. + 8. * temp_tmp4))) #Poly Gate Si Depletion Effect Vgs_eff = VGS Vgst = Vgs_eff - Vth # not in Nikhil's code #Effective Vgst (Vgsteff) Calculation T10 = 2. * n * self._Vt VgstNVt = Vgst / T10 ExpArg = -(2. * 0.08 + Vgst) / T10 T1 = T10 * log1pexp(VgstNVt) T2 = 1. + T10 * self.cox * ad.safe_exp(ExpArg) / self._Vt / self.cdep0 Vgsteff = ad.condassign(VgstNVt - EXP_THRESHOLD, Vgst, T1 / T2) Vgsteff = ad.condassign( ExpArg - EXP_THRESHOLD, self._Vt * self.cdep0 \ / self.cox / ad.safe_exp((Vgst + 0.08) / n / self._Vt), T1 / T2) T3 = T2 * T2 # Calculate Effective Channel Geometry T9 = sqrtPhis - self.sqrtPhi k_temp = self.weff - 2. * (self.dwg * Vgsteff + self.dwb * T9) Weff = ad.condassign(-k_temp + 2.0e-8, 2e-8 * (4.0e-8 - k_temp) * T0, k_temp) T0 = self.prwg * Vgsteff + self.prwb * (sqrtPhis - self.sqrtPhi) Rds = ad.condassign(T0 + 0.9, self.rds0 * (1. + T0), self.rds0 * (.8 +T0) / (17. + 20. * T0)) #Calculate Abulk T1 = 0.5 * self.k1ox / sqrtPhis T9 = np.sqrt(self.xj * Xdep) T5 = self.leff / (self.leff + 2. * T9) T2 = (self.a0 * T5) + self.b0 / (self.weff + self.b1) T6 = T5 * T5 T7 = T5 * T6 Abulk0 = 1. + T1 * T2 T8 = self.ags * self.a0 * T7 Abulk = Abulk0 - T1 * T8 * Vgsteff Abulk0 = ad.condassign(-Abulk0 + .1, (.2 - Abulk0) / (3. - 20. * Abulk0), Abulk0) Abulk = ad.condassign(-Abulk + .1, (.2 - Abulk) / (3. - 20. * Abulk), Abulk) T2 = self.keta * Vbseff T0 = ad.condassign(T2 + 0.9, 1. / (1. + T2), (17. + 20. * T2) / (0.8 + T2)) Abulk *= T0 Abulk0 *= T0 T0 = Vgsteff + 2. * Vth T2 = self._ua + self._uc * Vbseff T3 = T0 / self.tox T5 = T3 * (T2 + self._ub * T3) Denomi = ad.condassign(T5 + .8, 1. + T5, (.6 + T5) / (7. + 10. * T5)) ueff = self.u0temp / Denomi Esat = 2. * self.vsattemp / ueff # Saturation Drain Voltage Vdsat WVCox = Weff * self.vsattemp * self.cox WVCoxRds = WVCox * Rds EsatL = Esat * self.leff Lambda = self.a2 Vgst2Vtm = Vgsteff + 2. * self._Vt T0 = 1. / (Abulk * EsatL + Vgst2Vtm) T3 = EsatL * Vgst2Vtm Vdsat = T3 * T0 # Effective Vds(Vdseff) Calculation T1 = Vdsat - VDS - self.delta T2 = np.sqrt(T1**2 + 4. * self.delta * Vdsat) #T0 = T1 / T2 #T3 = 2. * self.delta / T2 k_temp = Vdsat - .5 * (T1 + T2) Vdseff = ad.condassign(k_temp - VDS, VDS, k_temp) # The following seems unnnecessary: # Added to eliminate non-zero Vdseff at Vds=0.0 #Vdseff = ad.condassign(abs(VDS), # Vdseff, # 0.) # Calculate Vasat T6 = 1. - .5 * Abulk * Vdsat / Vgst2Vtm #T6=tmp4 T9 = WVCoxRds * Vgsteff #expanded T0 = EsatL + Vdsat + 2. * T9 * T6 T9 = WVCoxRds * Abulk T1 = 2. / Lambda - 1. + T9 Vasat = T0 / T1 diffVds = VDS - Vdseff #Calculate VACLM VACLM = ad.condassign( (diffVds - 1.0e-10) * self.pclm, self.leff * (Abulk + Vgsteff / EsatL) * diffVds \ / (self.pclm * Abulk * self.litl), MAX_EXP) #Calculate VADIBL T1 = np.sqrt(const.epSi / const.epOx * self.tox * self.Xdep0) T0 = ad.safe_exp(-.5 * self.drout * self.leff / T1) T2 = T0 + 2. * T0**2 thetaRout = self.pdibl1 * T2 + self.pdibl2 #drout, pdibl1, #pdibl2 are given VADIBL = ad.condassign( thetaRout, (Vgst2Vtm - Vgst2Vtm * Abulk * Vdsat / (Vgst2Vtm + Abulk * Vdsat)) \ / thetaRout, MAX_EXP) VADIBL = ad.condassign(self.pdiblb * Vbseff + 0.9, VADIBL / (1. + self.pdiblb * Vbseff), VADIBL * (17. + 20. * self.pdiblb * Vbseff) \ / (.8 + self.pdiblb * Vbseff)) #Calculate Va T8 = self.pvag / EsatL T9 = T8 * Vgsteff T0 = ad.condassign(T9 + 0.9, 1. + T9, (.8 + T9) / (17. + 20. * T9)) T3 = VACLM + VADIBL #tmp3 = T3 T1 = VACLM * VADIBL / T3 Va = Vasat + T0 * T1 #Calculate VASCBE if self.pscbe1: rcpVASCBE = ad.condassign( abs(diffVds), self.pscbe2 * ad.safe_exp(-self.pscbe1 * self.litl/diffVds) \ / self.leff, 0.) else: rcpVASCBE = self.pscbe2 / self.leff # Original: #VASCBE = ad.condassign( # diffVds - self.pscbe1 * self.litl / EXP_THRESHOLD, # self.leff * np.exp(self.pscbe1 * self.litl/diffVds) / self.pscbe2, # MAX_EXP * self.leff / self.pscbe2) #Calculate Ids CoxWovL = self.cox * Weff / self.leff beta = ueff * CoxWovL T0 = 1. - .5 * Abulk * Vdseff / Vgst2Vtm fgche1 = Vgsteff * T0 T9 = Vdseff / EsatL fgche2 = 1. + T9 gche = beta * fgche1 / fgche2 T0 = 1. + gche * Rds T9 = Vdseff / T0 Idl = gche * T9 T9 = diffVds / Va T0 = 1. + T9 Idsa = Idl * T0 T9 = diffVds * rcpVASCBE T0 = 1. + T9 Ids = Idsa * T0 #Substrate current begins T1 = self.alpha0 + self.alpha1 * self.leff k_temp = T1 / self.leff * diffVds #T2 = k_temp * ad.safe_exp(-self.beta0 / diffVds) # Original T2: ad.condassign( diffVds - self.beta0 / EXP_THRESHOLD, k_temp * ad.safe_exp(-self.beta0 / diffVds), k_temp * MIN_EXP) k_temp = ad.condassign(T1, T2 * Idsa, 0.) Isub = ad.condassign(self.beta0, k_temp, 0.) # ********* Output current vector ************** iVec = np.array([0., 0., 0.], dtype=type(Ids)) iVec[0] = DtoSswap * Ids iVec[1] = ad.condassign(DtoSswap, Isub, 0.) iVec[2] = ad.condassign(DtoSswap, 0., Isub) # Revert currents if needed iVec *= self._tf # ********************************************** #-------------------------------------------------------------- # Charge calculation follows (does not work): #calculation of vfbzb T0 = -.5 * self.dvt1w * self.weff * self.leff \ / self.factor1 / np.sqrt(self.Xdep0) #T2 = ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)) T2 = ad.condassign(T0 + EXP_THRESHOLD, ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)), MIN_1) T0 = self.dvt0w * T2 T2 = T0 * (self.vbi - self.phi) T0 = -.5 * self.dvt1 * self.leff / (self.factor1 * np.sqrt(self.Xdep0)) #T3 = ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)) T3 = ad.condassign(T0 + EXP_THRESHOLD, ad.safe_exp(T0) * (1. + 2. * ad.safe_exp(T0)), MIN_1) T3 = self.dvt0 * T3 * (self.vbi - self.phi) T4 = self.tox * self.phi / (self.weff + self.w0) T5 = self.k1ox * (T0 - 1.) * self.sqrtPhi \ + (self.kt1 + self.kt1l / self.leff) * self._ToTnm1 T0 = np.sqrt(1. + self.nlx / self.leff) T6 = self._vth0 - T2 -T3 + self.k3 * T4 + T5 vfbzb = T6 - self.phi - self._k1 * self.sqrtPhi #Calculation for VbseffCV VbseffCV = ad.condassign(Vbseff, self.phi - Phis, Vbseff) #Calculation for VgsteffCV T0 = n * self.noff * self._Vt T1 = (Vgs_eff - Vth) / T0 Vgsteff = ad.condassign(T1 - EXP_THRESHOLD, Vgs_eff - Vth - self.voffcv, T0 * log1pexp(T1)) # This (after the previous) can not be right: #Vgsteff = ad.condassign(-T1 - EXP_THRESHOLD, # T0 * log(1. + MIN_EXP), # T0 * log(1. + np.exp(T1))) #Calculation for Vfbeff V3 = vfbzb - Vgs_eff + VbseffCV - .02 T0 = np.sqrt(V3**2 + .08 * abs(vfbzb)) Vfbeff = vfbzb - 0.5 * (V3 + T0) T0 = (Vgs_eff - VbseffCV - vfbzb) / self._Tox #Calculation for Tcen T1 = T0 * self.acde Tcen = ad.condassign(EXP_THRESHOLD + T1, self.ldeb * np.exp(T1), self.ldeb * MIN_EXP) Tcen = ad.condassign(-EXP_THRESHOLD + T1, self.ldeb * MAX_EXP, Tcen) V3 = self.ldeb - Tcen - 1e-3 * self.tox V4 = np.sqrt(V3**2 + 4e-3 * self.tox * self.ldeb) Tcen = self.ldeb - .5 * (V3 + V4) Ccen = const.epSi / Tcen Coxeff = Ccen * self.cox / (Ccen + self.cox) #Calculation for QoverlapCox CoxWLcen = Weff * self.leff * Coxeff Qac0 = CoxWLcen * (Vfbeff - vfbzb) # QovCox = Qac0 / Coxeff T0 = .5 * self.k1ox T3 = Vgs_eff - Vfbeff - VbseffCV - Vgsteff T1 = np.sqrt(T0 * T0 + T3) T2 = CoxWLcen * T0 / T1 T1 = ad.condassign(-T3, T0 + T3 / self.k1ox, np.sqrt(T0**2 + T3)) T2 = ad.condassign(-T3, CoxWLcen, CoxWLcen * T0 / T1) Qsub0 = CoxWLcen * self.k1ox * (T1 - T0) # QovCox = Qsub0 / Coxeff #Calculation for Delta_phis T2 = ad.condassign(self.k1ox, self.moin * self._Vt * self.k1ox**2, .25 * self.moin * self._Vt) T0 = ad.condassign(self.k1ox, self.k1ox * np.sqrt(self.phi), .5 * np.sqrt(self.phi)) T1 = 2. * T0 + Vgsteff DeltaPhi = self._Vt * np.log(1. + T1 * Vgsteff / T2) #The calculation for Tcen must be done once more # fREEDA: #T0 = (Vgsteff + 4.*(self._vth0 - self.vfb - self.phi))/ (2. * self._Tox) # ngspice: T3 = 4. * (Vth - vfbzb - self.phi) T0 = ad.condassign(T3, .5 * (Vgsteff + T3) / self._Tox, .5 * (Vgsteff + 1.0e-20) / self._Tox) k_temp = 2.01375270747048 * T0 # was np.exp(.7 * log(T0)) T1 = 1. + k_temp T2 = 0.35 * k_temp / (T0 * self._Tox) Tcen = 1.9e-9 / T1 Ccen = const.epSi / Tcen Coxeff = Ccen * self.cox / (Ccen + self.cox) CoxWLcen = Weff * self.leff * Coxeff AbulkCV = Abulk0 * self.AbulkCVfactor VdsatCV = (Vgsteff - DeltaPhi) / AbulkCV T0 = VdsatCV - VDS - .02 T1 = np.sqrt(T0**2 + .08 * VdsatCV) # From ngspice: internal version BSIM3v32V32 VdseffCV = VdsatCV - .5 * (T0 + T1) # From freeda: # VdseffCV = ad.condassign(T0, # VdsatCV - .5 * (T0 + T1), # VdsatCV * (1. - .04/(T1-T0))) # Need this to prevent NaN in charge Jacobian VdseffCV += 1e-200 # Seems not needed #VdseffCV = ad.condassign(abs(VDS), # Vdseff, # 0.) T0 = AbulkCV * VdseffCV T1 = Vgsteff - DeltaPhi T2 = 12. * (T1 - .5 * T0 + 1e-20) T3 = T0 / T2 T4 = 1. - 12. * T3**2 T5 = AbulkCV * (6. * T0 * (4. * T1 - T0) / (T2**2) - .5) T6 = T5 * VdseffCV / AbulkCV qgate = CoxWLcen * (T1 - T0 * (.5 - T3)) qbulk = CoxWLcen * (1. - AbulkCV) * (.5*VdseffCV - T0*VdseffCV/T2) #QovCox = qbulk / Coxeff T2 = T2 / 12. T3 = .5 * CoxWLcen / (T2**2) T4 = T1 * (2. * T0**2 / 3. + T1*(T1 - 4. * T0 / 3.)) - 2. * T0**3 / 15. qsrc = -T3 * T4 qgate += Qac0 + Qsub0 - qbulk qbulk -= (Qac0 + Qsub0) qdrn = -(qbulk + qgate + qsrc) # ************ Output charge vector **************** qVec = np.array([0., qgate, 0.], dtype=type(qsrc)) # have to switch charges if Drain and Source voltages switched qVec[0] = ad.condassign(DtoSswap, qdrn, qsrc) qVec[2] = ad.condassign(DtoSswap, qsrc, qdrn) # invert sign if needed qVec *= self._tf # Return numpy array with one element per current source. return (iVec, qVec)
def eval_cqs(self, vPort): """ Calculates currents/charges Input is a vector may be one of the following, depending on parameter values:: vPort = [xbe, xbc] vPort = [xbe, xbc, v4_i] (gyrator voltage, irb != 0) Output also depends on parameter values. Charges only present if parameters make them different than 0 (i.e., cje, tf, cjc, etc. are set to nonzero values):: iVec = [ibe, vbe, ibc, vbc, ice] iVec = [ibe, vbe, ibc, vbc, ice, gyr*ib*Rb] (rb != 0) qVec = [qbe, qbc] qVec = [qbe, qbc, qbx] (rb != 0 and cjc != 1) """ # Invert state variables if needed vPort1 = self._typef * vPort # Calculate junctions currents and voltages (ibf, vbe) = self.jif.get_idvd(vPort1[0]) (ibr, vbc) = self.jir.get_idvd(vPort1[1]) if self.ise != 0.: ile = self.jile.get_id(vbe) else: ile = 0. if self.isc != 0.: ilc = self.jilc.get_id(vbc) else: ilc = 0. # Kqb q1m1 = 1. if self.var != 0.: q1m1 -= vbe / self.var if self.vaf != 0.: q1m1 -= vbc / self.vaf kqb = 1. / q1m1 # We need extra checking to consider the following # possibilities to create the AD tape: # # 1. both ikf and ikr are zero -> no tape generated # 2. One of them is nonzero but both ibf and ibr are zero -> want tape # but only for the nonzero parameter if self.ikf + self.ikr != 0.: q2 = 0. if self.ikf != 0.: q2 += ibf / self.ikf if self.ikr != 0.: q2 += ibr / self.ikr kqb *= .5 * (1. + np.sqrt(1. + 4. * q2)) # Create output vector [ibe, ibc, ice, ...] iVec = np.zeros(self.ncurrents, dtype = type(ibf)) qVec = np.zeros(self.ncharges, dtype = type(ibf)) # ibe, vbe iVec[0] = ibf / self._bf_t + ile iVec[1] = glVar.gyr * vbe # ibc, vbc iVec[2] = ibr / self._br_t + ilc iVec[3] = glVar.gyr * vbc # ice iVec[4] = (ibf - ibr) / kqb # RB if self.rb != 0.: # Using gyrator # vPort1[2] not defined if rb == 0 # ib has area effect included (removed by _ck1 and _ck2) ib = vPort1[2] * glVar.gyr if self.irb != 0.: ib1 = np.abs(ib) x = np.sqrt(1. + self._ck1 * ib1) - 1. x *= self._ck2 / np.sqrt(ib1) tx = np.tan(x) c = self.rbm + 3. * (self.rb - self.rbm) \ * (tx - x) / (x * tx * tx) rb = ad.condassign(ib1, c, self.rb) else: rb = self.rbm + (self.rb - self.rbm) / kqb # Output is gyr * ib * rb. It is divided by area^2 to # compensate that the whole vector is multiplied by area # at the end iVec[5] = glVar.gyr * ib * rb / pow(self.area, 2) vbcx = ib * rb / self.area + vbc # Charges ----------------------------------------------- # Note that if tf == 0 and cje == 0, nothing is calculated and # nothing is assigned to the output vector. # qbe is the first charge (0) if self.tf != 0.: # Effective tf tfeff = self.tf if self.vtf != 0.: x = ibf / (ibf + self.itf) # safe_exp() not needed since positive vbc grows # logarithmically tfeff *= (1. + self.xtf * x*x * np.exp(vbc /1.44 /self.vtf)) qVec[0] = tfeff * ibf if self.cje != 0.: qVec[0] += self.jif.get_qd(vbe) # qbc if self._qbx: if self.tr != 0.: qVec[-2] = self.tr * ibr if self.cjc != 0.: qVec[-2] += self.jir.get_qd(vbc) * self.xcjc # qbx qVec[-1] = self.jir.get_qd(vbcx) * (1. - self.xcjc) else: if self.tr != 0.: qVec[-1] = self.tr * ibr if self.cjc != 0.: qVec[-1] += self.jir.get_qd(vbc) # Consider area effect and invert currents if needed iVec *= self.area * self._typef qVec *= self.area * self._typef return (iVec, qVec)
def eval_cqs(self, vPort): """ Calculates currents/charges Input is a vector may be one of the following, depending on parameter values:: vPort = [xbe, xbc] vPort = [xbe, xbc, v4_i] (gyrator voltage, irb != 0) Output also depends on parameter values. Charges only present if parameters make them different than 0 (i.e., cje, tf, cjc, etc. are set to nonzero values):: iVec = [ibe, vbe, ibc, vbc, ice] iVec = [ibe, vbe, ibc, vbc, ice, gyr*ib*Rb] (rb != 0) qVec = [qbe, qbc] qVec = [qbe, qbc, qbx] (rb != 0 and cjc != 1) """ # Invert state variables if needed vPort1 = self._typef * vPort # Calculate junctions currents and voltages (ibf, vbe) = self.jif.get_idvd(vPort1[0]) (ibr, vbc) = self.jir.get_idvd(vPort1[1]) if self.ise != 0.: ile = self.jile.get_id(vbe) else: ile = 0. if self.isc != 0.: ilc = self.jilc.get_id(vbc) else: ilc = 0. # Kqb q1m1 = 1. if self.var != 0.: q1m1 -= vbe / self.var if self.vaf != 0.: q1m1 -= vbc / self.vaf kqb = 1. / q1m1 # We need extra checking to consider the following # possibilities to create the AD tape: # # 1. both ikf and ikr are zero -> no tape generated # 2. One of them is nonzero but both ibf and ibr are zero -> want tape # but only for the nonzero parameter if self.ikf + self.ikr != 0.: q2 = 0. if self.ikf != 0.: q2 += ibf / self.ikf if self.ikr != 0.: q2 += ibr / self.ikr kqb *= .5 * (1. + np.sqrt(1. + 4. * q2)) # Create output vector [ibe, ibc, ice, ...] iVec = np.zeros(self.ncurrents, dtype=type(ibf)) qVec = np.zeros(self.ncharges, dtype=type(ibf)) # ibe, vbe iVec[0] = ibf / self._bf_t + ile iVec[1] = glVar.gyr * vbe # ibc, vbc iVec[2] = ibr / self._br_t + ilc iVec[3] = glVar.gyr * vbc # ice iVec[4] = (ibf - ibr) / kqb # RB if self.rb != 0.: # Using gyrator # vPort1[2] not defined if rb == 0 # ib has area effect included (removed by _ck1 and _ck2) ib = vPort1[2] * glVar.gyr if self.irb != 0.: ib1 = np.abs(ib) x = np.sqrt(1. + self._ck1 * ib1) - 1. x *= self._ck2 / np.sqrt(ib1) tx = np.tan(x) c = self.rbm + 3. * (self.rb - self.rbm) \ * (tx - x) / (x * tx * tx) rb = ad.condassign(ib1, c, self.rb) else: rb = self.rbm + (self.rb - self.rbm) / kqb # Output is gyr * ib * rb. It is divided by area^2 to # compensate that the whole vector is multiplied by area # at the end iVec[5] = glVar.gyr * ib * rb / pow(self.area, 2) vbcx = ib * rb / self.area + vbc # Charges ----------------------------------------------- # Note that if tf == 0 and cje == 0, nothing is calculated and # nothing is assigned to the output vector. # qbe is the first charge (0) if self.tf != 0.: # Effective tf tfeff = self.tf if self.vtf != 0.: x = ibf / (ibf + self.itf) # safe_exp() not needed since positive vbc grows # logarithmically tfeff *= (1. + self.xtf * x * x * np.exp(vbc / 1.44 / self.vtf)) qVec[0] = tfeff * ibf if self.cje != 0.: qVec[0] += self.jif.get_qd(vbe) # qbc if self._qbx: if self.tr != 0.: qVec[-2] = self.tr * ibr if self.cjc != 0.: qVec[-2] += self.jir.get_qd(vbc) * self.xcjc # qbx qVec[-1] = self.jir.get_qd(vbcx) * (1. - self.xcjc) else: if self.tr != 0.: qVec[-1] = self.tr * ibr if self.cjc != 0.: qVec[-1] += self.jir.get_qd(vbc) # Consider area effect and invert currents if needed iVec *= self.area * self._typef qVec *= self.area * self._typef return (iVec, qVec)