def __init__(self, instanceName): """ Here the Element constructor must be called. Do not connect internal nodes here. """ cir.Element.__init__(self, instanceName) self.diogs = Junction() self.diogd = Junction()
def __init__(self, instanceName): cir.Element.__init__(self, instanceName) # Use junctions to model diodes and capacitors self.jif = SVJunction() self.jir = SVJunction() self.jile = Junction() self.jilc = Junction() # collector/emitter terminal numbers: may be re-mapped by # extrinsic device self._ct = 0 self._et = 2
class SVBJTi(cir.Element): """ State-variable-based Gummel-Poon intrinsic BJT model based This implementation based mainly on previous implementation in carrot and some equations from Pspice manual, with the addition of the state-variable definitions. Terminal order: 0 Collector, 1 Base, 2 Emitter, (3 Bulk, not included):: C (0) o----, 4----o E (2) \ / \ / --------- | o B (1) Can be used for NPN or PNP transistors. Intrinsic Internal Topology +++++++++++++++++++++++++++ The state variable formulation is achieved by replacing the BE and BC diodes (Ibf, Ibr) with state-variable based diodes. This requires two additional variables (nodes) but eliminates large positive exponentials from the model:: Term : x2 +--------------------------+ | | /|\ /^\ ( | ) gyr v2 ( | ) gyr vbc(x) \V/ \|/ tref | | ,----+--------------------------+ | | | | /^\ /|\ | ( | ) gyr v1 ( | ) gyr vbe(x) --- \|/ \V/ V | | +--------------------------+ Term : x1 All currents/charges in the model are functions of voltages v3 (x2) and v4 (x1). Note that vbc and vbe are now also functions of x1, x2. In addition we may need 2 additional nodes (plus reference) if rb is not zero: Bi for the internal base node and tib to measure the internal base current and calculate Rb(ib). 1. If RB == 0:: +----------------+--o 0 (C) - | | /^\ | v2 ( | ) ibc(x2) | \|/ | + | /|\ (B) 1 o---------+ ( | ) ice(x1,x2) + | \V/ /|\ | v1 ( | ) ibe(x1) | \V/ | - | | +----------------+--o 2 (E) 2. If RB != 0:: +----------------+--o 0 (C) - | | /^\ | gyr tib v2 ( | ) ibc(x2) | \|/ | ,---, + | /|\ (B) 1 o----( --> )----------+ Term : Bi ( | ) ice(x1,x2) `---` + | \V/ /|\ | v1 ( | ) ibe(x1) | \V/ | - | | gyr v(1,Bi) +----------------+--o 2 (E) ,---, +---( <-- ) -----+ | `---` | tref | | ib/gyr ,--+ | | | ,---, | Term : ib | +---( --> )------+ | `---` --- V gyr ib Rb(ib) Charge sources are connected between internal nodes defined above. If xcjc is not 1 but RB is zero, xcjc is ignored. """ devType = "svbjt" paramDict = dict( cir.Element.tempItem, type = ('Type (npn or pnp)', '', str, 'npn'), isat = ('Transport saturation current', 'A', float, 1e-16), bf = ('Ideal maximum forward beta', '', float, 100.), nf = ('Forward current emission coefficient', '', float, 1.), vaf = ('Forward early voltage', 'V', float, 0.), ikf = ('Forward-beta high current roll-off knee current', 'A', float, 0.), ise = ('Base-emitter leakage saturation current', 'A', float, 0.), ne = ('Base-emitter leakage emission coefficient', '', float, 1.5), br = ('Ideal maximum reverse beta', '', float, 1.), nr = ('Reverse current emission coefficient', '', float, 1.), var = ('Reverse early voltage', 'V', float, 0.), ikr = ('Corner for reverse-beta high current roll off', 'A', float, 0.), isc = ('Base collector leakage saturation current', 'A', float, 0.), nc = ('Base-collector leakage emission coefficient', '', float, 2.), rb = ('Zero bias base resistance', 'Ohm', float, 0.), rbm = ('Minimum base resistance', 'Ohm', float, 0.), irb = ('Current at which rb falls to half of rbm', 'A', float, 0.), eg = ('Badgap voltage', 'eV', float, 1.11), cje = ('Base emitter zero bias p-n capacitance', 'F', float, 0.), vje = ('Base emitter built in potential', 'V', float, 0.75), mje = ('Base emitter p-n grading factor', '', float, 0.33), cjc = ('Base collector zero bias p-n capacitance', 'F', float, 0.), vjc = ('Base collector built in potential', 'V', float, 0.75), mjc = ('Base collector p-n grading factor', '', float, 0.33), xcjc = ('Fraction of cbc connected internal to rb', '', float, 1.), fc = ('Forward bias depletion capacitor coefficient', '', float, 0.5), tf = ('Ideal forward transit time', 's', float, 0.), xtf = ('Transit time bias dependence coefficient', '', float, 0.), vtf = ('Transit time dependency on vbc', 'V', float, 0.), itf = ('Transit time dependency on ic', 'A', float, 0.), tr = ('Ideal reverse transit time', 's', float, 0.), xtb = ('Forward and reverse beta temperature coefficient', '', float, 0.), xti = ('IS temperature effect exponent', '', float, 3.), tnom = ('Nominal temperature', 'C', float, 27.), area = ('Current multiplier', '', float, 1.) ) def __init__(self, instanceName): cir.Element.__init__(self, instanceName) # Use junctions to model diodes and capacitors self.jif = SVJunction() self.jir = SVJunction() self.jile = Junction() self.jilc = Junction() # collector/emitter terminal numbers: may be re-mapped by # extrinsic device self._ct = 0 self._et = 2 def process_params(self): """ Adjusts internal topology and makes preliminary calculations according to parameters. """ # Define topology first. Add state variable nodes x2 = self.add_internal_term('x2','s.v.') x1 = self.add_internal_term('x1','s.v.') tref = self.add_reference_term() # Default configuration assumes rb == 0 # ibe, vbe, ibc, vbc, ice self.csOutPorts = [(1, self._et), (tref, x1), (1, self._ct), (tref, x2), (self._ct, self._et)] # Controling voltages are x1, x2 self.controlPorts = [(x1, tref), (x2, tref)] # qbe, qbc self.qsOutPorts = [(1, self._et), (1, self._ct)] # Flag to signal if the extra charge Qbx is needed or not self._qbx = False # Default state-variable VCCSs self.linearVCCS = [((1, self._et), (x1, tref), glVar.gyr), ((1, self._ct), (x2, tref), glVar.gyr)] if self.rb != 0.: # rb is not zero: add internal terminals tBi = self.add_internal_term('Bi', 'V') tib = self.add_internal_term('ib', '{0} A'.format(glVar.gyr)) # Add Linear VCCS for gyrator(s) self.linearVCCS = [((tBi, self._et), (x1, tref), glVar.gyr), ((tBi, 0), (x2, tref), glVar.gyr), ((1, tBi), (tib, tref), glVar.gyr), ((tib, tref), (1, tBi), glVar.gyr)] # ibe, vbe, ibc, vbc, ice, Rb(ib) * ib self.csOutPorts = [(tBi, self._et), (tref, x1), (tBi, self._ct), (tref, x2), (self._ct, self._et), (tref, tib)] # Controling voltages are x1, x2 and gyrator port self.controlPorts = [(x1, tref), (x2, tref), (tib, tref)] # qbie, qbic self.qsOutPorts = [(tBi, self._et), (tBi, self._ct)] # Now check if Cjbc must be splitted (since rb != 0) if (self.cjc != 0.) and (self.xcjc < 1.): # add extra charge source self.qsOutPorts.append((1, self._ct)) self._qbx = True # Make sure the guess is consistent self.vPortGuess = np.zeros(len(self.controlPorts)) # # Try guess in active region # self.vPortGuess[0] = 100. # x1 # self.vPortGuess[1] = -1. # x2 # if self.rb != 0.: # self.vPortGuess[2] = 1e-6 / glVar.gyr # ib # In principle we may not need any charge keepPorts = [ ] if self.cje + self.tf != 0.: # keep qbe keepPorts.append(self.qsOutPorts[0]) if self.cjc + self.tr != 0.: # keep qbc, qbx (if any) if self._qbx: keepPorts += self.qsOutPorts[-2:] else: keepPorts.append(self.qsOutPorts[-1]) self.qsOutPorts = keepPorts # keep track of how many output variables are needed self.ncurrents = len(self.csOutPorts) self.ncharges = len(self.qsOutPorts) # NPN or PNP if self.type == 'pnp': self._typef = -1. else: self._typef = 1. # Calculate common variables # Absolute nominal temperature self.Tnomabs = self.tnom + const.T0 self.egapn = self.eg - .000702 * (self.Tnomabs**2) \ / (self.Tnomabs + 1108.) # jif produces if, cje self.jif.process_params(self.isat, self.nf, self.fc, self.cje, self.vje, self.mje, self.xti, self.eg, self.Tnomabs) # jir produces ir, cjc self.jir.process_params(self.isat, self.nr, self.fc, self.cjc, self.vjc, self.mjc, self.xti, self.eg, self.Tnomabs) if self.ise != 0.: # jile produces ile self.jile.process_params(self.ise, self.ne, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) if self.isc != 0.: # jilc produces ilc self.jilc.process_params(self.isc, self.nc, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) # Constants needed for rb(ib) calculation if self.irb != 0.: self._ck1 = 144. / self.irb / self.area /np.pi/np.pi self._ck2 = np.pi*np.pi * np.sqrt(self.irb * self.area) / 24. def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Absolute temperature (note self.temp is in deg. C) self.Tabs = const.T0 + temp # Normalized temp self.tnratio = self.Tabs / self.Tnomabs tnXTB = pow(self.tnratio, self.xtb) # Thermal voltage self.vt = const.k * self.Tabs / const.q # Temperature-adjusted egap self.egap_t = self.eg - .000702 * (self.Tabs**2) / (self.Tabs + 1108.) # set temperature in juctions self.jif.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jir.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) # Adjust ise and isc (which have different temperature variation) if self.ise != 0.: self.jile.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jile._t_is /= tnXTB if self.isc != 0.: self.jilc.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jilc._t_is /= tnXTB # Now some BJT-only variables self._bf_t = self.bf * tnXTB self._br_t = self.br * tnXTB 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 power(self, vPort, currV): """ Calculate total instantaneous power Input: control voltages as in eval_cqs() and currents from returned by eval_cqs() """ # vce = vbe - vbc gyrvce = currV[1] - currV[3] if self.rb != 0.: # currV[5] = ib * Rb * gyr # vPort[2] = ib / gyr pRb = currV[5] * vPort[2] else: pRb = 0. # pout = ibe * vbie + ibc * vbic + vce * ice + pRb pout = (currV[0] * currV[1] + currV[2] * currV[3] + currV[4] * gyrvce) / glVar.gyr + pRb return pout def get_OP(self, vPort): """ Calculates operating point information Input: same as eval_cqs Output: dictionary with OP variables For now it is quite incomplete """ # First we need the Jacobian (outV, jac) = self.eval_and_deriv(vPort) power = self.power(vPort, outV) # calculate gm, etc. in terms od jac for state-variable # formulation opDict = dict( VBE = outV[1] / glVar.gyr, VCE = (outV[1] - outV[3]) / glVar.gyr, IB = outV[0] + outV[2], IC = outV[4] - outV[2], IE = - outV[4] - outV[0], Temp = self.temp, Power = power, ) return opDict def get_noise(self, f): """ Return noise spectral density at frequency f Requires a previous call to get_OP() Not implemented yet """ return None
def __init__(self, instanceName): IBJT.__init__(self, instanceName) # Collector-bulk junction self.cbjtn = Junction() self.__doc__ += IBJT.__doc__
class BJTi(cir.Element): """ Gummel-Poon intrinsic BJT model This implementation based mainly on previous implementation in carrot and some equations from Pspice manual. Terminal order: 0 Collector, 1 Base, 2 Emitter:: C (0) o----, 4----o E (2) \ / \ / --------- | o B (1) Can be used for NPN or PNP transistors. Intrinsic Internal Topology +++++++++++++++++++++++++++ Internally may add 2 additional nodes (plus reference) if rb is not zero: Bi for the internal base node and tib to measure the internal base current and calculate Rb(ib). The possible configurations are described here. 1. If RB == 0:: +----------------+--o 0 (C) | | /^\ | ( | ) ibc(vbc) | \|/ | | /|\ (B) 1 o---------+ ( | ) ice | \V/ /|\ | ( | ) ibe(vbe) | \V/ | | | +----------------+--o 2 (E) 2. If RB != 0:: +----------------+--o 0 (C) | | /^\ | ( | ) ibc(vbc) | gyr * tib \|/ | ,---, | /|\ (B) 1 o----( --> )----------+ Term : Bi ( | ) ice `---` | \V/ /|\ | ( | ) ibe(vbe) | \V/ | | | +----------------+--o 2 (E) gyr v(1,Bi) ,---, +---( <-- )------+ | `---` | tref | | voltage: ib/gyr ,---+ | | | ,---, | | +---( --> )------+ Term : ib | `---` --- gyr ib Rb(ib) V Charge sources are connected between internal nodes defined above. If xcjc is not 1 but RB is zero, xcjc is ignored. """ devType = "bjt" paramDict = dict( cir.Element.tempItem, type = ('Type (npn or pnp)', '', str, 'npn'), isat = ('Transport saturation current', 'A', float, 1e-16), bf = ('Ideal maximum forward beta', '', float, 100.), nf = ('Forward current emission coefficient', '', float, 1.), vaf = ('Forward early voltage', 'V', float, 0.), ikf = ('Forward-beta high current roll-off knee current', 'A', float, 0.), ise = ('Base-emitter leakage saturation current', 'A', float, 0.), ne = ('Base-emitter leakage emission coefficient', '', float, 1.5), br = ('Ideal maximum reverse beta', '', float, 1.), nr = ('Reverse current emission coefficient', '', float, 1.), var = ('Reverse early voltage', 'V', float, 0.), ikr = ('Corner for reverse-beta high current roll off', 'A', float, 0.), isc = ('Base collector leakage saturation current', 'A', float, 0.), nc = ('Base-collector leakage emission coefficient', '', float, 2.), rb = ('Zero bias base resistance', 'W', float, 0.), rbm = ('Minimum base resistance', 'W', float, 0.), irb = ('Current at which rb falls to half of rbm', 'A', float, 0.), eg = ('Badgap voltage', 'eV', float, 1.11), cje = ('Base emitter zero bias p-n capacitance', 'F', float, 0.), vje = ('Base emitter built in potential', 'V', float, 0.75), mje = ('Base emitter p-n grading factor', '', float, 0.33), cjc = ('Base collector zero bias p-n capacitance', 'F', float, 0.), vjc = ('Base collector built in potential', 'V', float, 0.75), mjc = ('Base collector p-n grading factor', '', float, 0.33), xcjc = ('Fraction of cbc connected internal to rb', '', float, 1.), fc = ('Forward bias depletion capacitor coefficient', '', float, 0.5), tf = ('Ideal forward transit time', 'S', float, 0.), xtf = ('Transit time bias dependence coefficient', '', float, 0.), vtf = ('Transit time dependency on vbc', 'V', float, 0.), itf = ('Transit time dependency on ic', 'A', float, 0.), tr = ('Ideal reverse transit time', 'S', float, 0.), xtb = ('Forward and reverse beta temperature coefficient', '', float, 0.), xti = ('IS temperature effect exponent', '', float, 3.), tnom = ('Nominal temperature', 'C', float, 27.), area = ('Current multiplier', '', float, 1.) ) def __init__(self, instanceName): cir.Element.__init__(self, instanceName) # Use junctions to model diodes and capacitors self.jif = Junction() self.jir = Junction() self.jile = Junction() self.jilc = Junction() # collector/emitter terminal numbers: may be re-mapped by # extrinsic device self._ct = 0 self._et = 2 def process_params(self): """ Adjusts internal topology and makes preliminary calculations according to parameters """ # Default configuration assumes rb == 0 # ibe, ibc, ice self.csOutPorts = [(1, self._et), (1, self._ct), (self._ct, self._et)] # Controling voltages are vbe, vbc self.controlPorts = [(1, self._et), (1, self._ct)] self.vPortGuess = np.array([0., 0.]) # qbe, qbc self.qsOutPorts = [(1, self._et), (1, self._ct)] # Define topology first # Flag to signal if the extra charge Qbx is needed or not self._qbx = False if self.rb: # rb is not zero: add internal terminals tBi = self.add_internal_term('Bi', 'V') tib = self.add_internal_term('ib', '{0} A'.format(glVar.gyr)) tref = self.add_reference_term() # Linear VCCS for gyrator(s) self.linearVCCS = [((1, tBi), (tib, tref), glVar.gyr), ((tib, tref), (1, tBi), glVar.gyr)] # ibe, ibc, ice, Rb(ib) * ib self.csOutPorts = [(tBi, self._et), (tBi, self._ct), (self._ct, self._et), (tref, tib)] # Controling voltages are vbie, vbic and gyrator port self.controlPorts = [(tBi, self._et), (tBi, self._ct), (tib, tref)] self.vPortGuess = np.array([0., 0., 0.]) # qbie, qbic self.qsOutPorts = [(tBi, self._et), (tBi, self._ct)] # Now check if Cjbc must be splitted (since rb != 0) if self.cjc and (self.xcjc < 1.): self.qsOutPorts.append((1, self._ct)) self._qbx = True # In principle we may not need any charge keepPorts = [ ] if self.cje + self.tf: # keep qbe keepPorts.append(self.qsOutPorts[0]) if self.cjc + self.tr: # keep qbc, qbx (if any) if self._qbx: keepPorts += self.qsOutPorts[-2:] else: keepPorts.append(self.qsOutPorts[-1]) self.qsOutPorts = keepPorts # keep track of how many output variables are needed self.ncurrents = len(self.csOutPorts) self.ncharges = len(self.qsOutPorts) # NPN or PNP if self.type == 'pnp': self._typef = -1. else: self._typef = 1. # Calculate common variables # Absolute nominal temperature self.Tnomabs = self.tnom + const.T0 self.egapn = self.eg - .000702 * (self.Tnomabs**2) \ / (self.Tnomabs + 1108.) # jif produces if, cje self.jif.process_params(self.isat, self.nf, self.fc, self.cje, self.vje, self.mje, self.xti, self.eg, self.Tnomabs) # jir produces ir, cjc self.jir.process_params(self.isat, self.nr, self.fc, self.cjc, self.vjc, self.mjc, self.xti, self.eg, self.Tnomabs) if self.ise: # jile produces ile self.jile.process_params(self.ise, self.ne, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) if self.isc: # jilc produces ilc self.jilc.process_params(self.isc, self.nc, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) # Constants needed for rb(ib) calculation if self.irb: self._ck1 = 144. / self.irb / self.area /np.pi/np.pi self._ck2 = np.pi*np.pi * np.sqrt(self.irb * self.area) / 24. def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Absolute temperature (note self.temp is in deg. C) self.Tabs = const.T0 + temp # Normalized temp self.tnratio = self.Tabs / self.Tnomabs tnXTB = pow(self.tnratio, self.xtb) # Thermal voltage self.vt = const.k * self.Tabs / const.q # Temperature-adjusted egap self.egap_t = self.eg - .000702 * (self.Tabs**2) / (self.Tabs + 1108.) # set temperature in juctions self.jif.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jir.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) # Adjust ise and isc (which have different temperature variation) if self.ise: self.jile.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jile._t_is /= tnXTB if self.isc: self.jilc.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jilc._t_is /= tnXTB # Now some BJT-only variables self._bf_t = self.bf * tnXTB self._br_t = self.br * tnXTB 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 power(self, vPort, currV): """ Calculate total instantaneous power Input: control voltages as in eval_cqs() and currents returned by eval_cqs() """ vce = vPort[0] - vPort[1] if self.rb: # currV[3] = ib * Rb * gyr # vPort[2] = ib / gyr pRb = currV[3] * vPort[2] else: pRb = 0. # pout = ibe * vbie + ibc * vbic + vce * ice + pRb pout = currV[0] * vPort[0] + currV[1] * vPort[1] \ + currV[2] * vce + pRb return pout def get_OP(self, vPort): """ Calculates operating point information Input: same as eval_cqs Output: dictionary with OP variables For now it is quite incomplete """ # First we need the Jacobian (outV, jac) = self.eval_and_deriv(vPort) self.OP = dict( VBE = vPort[0], VCE = vPort[0] - vPort[1], IB = outV[0] + outV[1], IC = outV[2] - outV[1], IE = - outV[2] - outV[0], gm = jac[2,0] - jac[1,0], rpi = 1./(jac[0,0] + jac[1,0]), ) return self.OP def get_noise(self, f): """ Return noise spectral density at frequency f Requires a previous call to get_OP() Not implemented yet """ return None
class BJT(IBJT): """ Bipolar Junction Transistor --------------------------- This device accepts 3 or 4 terminal connections. Netlist examples:: bjt:q1 2 3 4 1 model = mypnp isat=4e-17 bf=147 iss=10fA bjt:q2 2 3 4 model = mypnp isat=4e-17 bf=147 vaf=80 ikf=4m svbjt:q3 2 3 4 1 model = mypnp vaf=80 ikf=4m iss=15fA # Electro-thermal versions bjt_t:q2 2 3 5 1 pout gnd model = mypnp svbjt_t:q3 2 3 5 1 pout gnd model = mypnp # Model statement .model mypnp bjt_t (type=pnp isat=5e-17 cje=60fF vje=0.83 mje=0.35) Extrinsic Internal Topology +++++++++++++++++++++++++++ RC, RE and a Collector-Bulk connection are added to intrinsic BJT models:: RC Term: ct Term: et RE C (0) o---/\/\/\/--+-----, 4----/\/\/\/----o E (2) | \ / | \ / ----- --------- / \ | / \ o ----- | B (1) o Bulk (3) If RE or RC are zero the internal nodes (ct, et) are not created. If only 3 connections are specified then the Bulk-Collector junction is not connected. Important Note ++++++++++++++ This implementation does not account for the power dissipation in RE, RC. Use external thermal resistors if that is needed. Intrinsic Model Information +++++++++++++++++++++++++++ """ # Additional documentation extraDoc = IBJT.__doc__ # Create electrothermal device makeAutoThermal = True isNonlinear = True # Device category category = "Semiconductor devices" # devtype is the 'model' name: remove the 'i' from intrinsic name devType = IBJT.devType # Do not set numTerms to allow 3 or 4 terminals to be connected paramDict = dict( IBJT.paramDict.items(), re = ('Emitter ohmic resistance', 'W', float, 0.), rc = ('Collector ohmic resistance', 'W', float, 0.), cjs = ('Collector substrate capacitance', 'F', float, 0.), mjs = ('substrate junction exponential factor', '', float, 0.), vjs = ('substrate junction built in potential', 'V', float, 0.75), ns = ('substrate p-n coefficient', '', float, 1.), iss = ('Substrate saturation current', 'A', float, 1e-14) ) def __init__(self, instanceName): IBJT.__init__(self, instanceName) # Collector-bulk junction self.cbjtn = Junction() self.__doc__ += IBJT.__doc__ def process_params(self, thermal = False): # Remove tape if present ad.delete_tape(self) if thermal: extraTerms = 2 else: extraTerms = 0 # First check external connections if self.numTerms == 3 + extraTerms: self.__addCBjtn = False elif self.numTerms == 4 + extraTerms: self.__addCBjtn = True else: raise cir.CircuitError( '{0}: Wrong number of connections. \ Can only be {1} or {2}, {3} found.'.format(self.nodeName, 3 + extraTerms, 4 + extraTerms, self.numTerms)) # Remove internal terminals self.clean_internal_terms() # Tell autothermal to re-generate thermal ports self.__addThermalPorts = True extraVCCS = list() if self.re: # Add et node and translate port descriptions self._et = self.add_internal_term('et', 'V') extraVCCS += [((2, self._et), (2, self._et), self.area / self.re)] if self.rc: # Add ct node and translate port descriptions self._ct = self.add_internal_term('ct', 'V') extraVCCS += [((0, self._ct), (0, self._ct), self.area / self.rc)] # Process parameters from intrinsic device: emitter and # collector terminals are already substituted. IBJT.process_params(self) # Calculate variables in junction self.cbjtn.process_params(self.iss, self.ns, self.fc, self.cjs, self.vjs, self.mjs, self.xti, self.eg, self.Tnomabs) # Add RE, RC resistors (if any) self.linearVCCS += extraVCCS if self.__addCBjtn: # Add bulk-collector junction self.__bccn = len(self.controlPorts) self.__bcon = len(self.csOutPorts) self.controlPorts.append((3, self._ct)) self.csOutPorts.append((3, self._ct)) if self.cjs: self.qsOutPorts.append((3, self._ct)) # Initial guess for input ports: try: if len(self.vPortGuess) < len(self.controlPorts): self.vPortGuess = np.concatenate( (self.vPortGuess, [1]), axis=0) except AttributeError: # Ignore if vPortGuess not provided pass # Adjust temperature self.set_temp_vars(self.temp) def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Remove tape if present ad.delete_tape(self) # First calculate variables from base class IBJT.set_temp_vars(self, temp) # Adjust collector-bulk junction temperature self.cbjtn.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) def eval_cqs(self, vPort, saveOP = False): """ vPort is a vector with control voltages """ if self.__addCBjtn: # calculate currents and charges in base class (iVec1, qVec1) = IBJT.eval_cqs(self, vPort) # Add contribution of substrate. Ignore extra control # ports only used for power calculation v1 = vPort[self.__bccn] * self._typef isub = self.cbjtn.get_id(v1) * self.area * self._typef iVec = np.concatenate((iVec1, [isub]), axis = 0) if self.cjs: qsub = self.cbjtn.get_qd(v1) * self.area * self._typef qVec = np.concatenate((qVec1, [qsub]), axis = 0) else: qVec = qVec1 return (iVec, qVec) else: return IBJT.eval_cqs(self, vPort) # Create these using the AD facility eval_and_deriv = ad.eval_and_deriv eval = ad.eval def power(self, vPort, currV): """ Calculate total instantaneous power Power in RE, RC not considered. Input: control voltages as in eval_cqs() and currents returned by eval_cqs() """ pout = IBJT.power(self, vPort, currV) if self.__addCBjtn: # add power from substrate junction (RE, RC not counted) pout += vPort[self.__bccn] * currV[self.__bcon] return pout
class Device(cir.Element): """ Cubic Curtice-Ettemberg Intrinsic MESFET Model ---------------------------------------------- Model derived from fREEDA 1.4 MesfetCT model adapted to re-use junction code from ``diode.py``. Some parameter names have been changed: ``isat``, ``tau``. Uses symmetric diodes and capacitances. Works in reversed mode. Terminal order: 0 Drain, 1 Gate, 2 Source:: Drain 0 o | | |---+ | Gate 1 o---->| | |---+ | | o Source 2 Netlist example:: mesfetc:m1 2 3 4 a0=0.09910 a1=0.08541 a2=-0.02030 a3=-0.01543 Internal Topology:: ,----------------,------------,--o 0 (D) | | | /^\ | | ( | ) igd(Vgd) ----- Cgd | \|/ ----- | | | /|\ (G) 1 o----+----------------, ( | ) ids(Vgs, Vgd) | | \V/ /|\ | | ( | ) igs(Vgs) ----- Cgs | \V/ ----- | | | | `----------------'------------'--o 2 (S) """ # Device category category = "Semiconductor devices" devType = "mesfetc" paramDict = dict( cir.Element.tempItem, a0=("Drain saturation current for Vgs=0", "A", float, 0.1), a1=("Coefficient for V1", "A/V", float, 0.05), a2=("Coefficient for V1^2", "A/V^2", float, 0.0), a3=("Coefficient for V1^3", "A/V^3", float, 0.0), beta=("V1 dependance on Vds", "1/V", float, 0.0), vds0=("Vds at which BETA was measured", "V", float, 4.0), gama=("Slope of drain characteristic in the linear region", "1/V", float, 1.5), vt0=( "Voltage at which the channel current is forced to be zero\ for Vgs<=Vto", "V", float, -np.inf, ), cgs0=("Gate-source Schottky barrier capacitance for Vgs=0", "F", float, 0.0), cgd0=("Gate-drain Schottky barrier capacitance for Vgd=0", "F", float, 0.0), isat=("Diode saturation current", "A", float, 0.0), n=("Diode ideality factor", "", float, 1.0), ib0=("Breakdown current parameter", "A", float, 0.0), nr=("Breakdown ideality factor", "", float, 10.0), vbd=("Breakdown voltage", "V", float, np.inf), tau=("Channel transit time", "s", float, 0.0), vbi=("Built-in potential of the Schottky junctions", "V", float, 0.8), fcc=("Forward-bias depletion capacitance coefficient", "V", float, 0.5), tnom=("Nominal temperature", "C", float, 27.0), avt0=("Pinch-off voltage (VP0 or VT0) linear temp. coefficient", "1/K", float, 0.0), bvt0=("Pinch-off voltage (VP0 or VT0) quadratic temp. coefficient", "1/K^2", float, 0.0), tbet=("BETA power law temperature coefficient", "1/K", float, 0), tm=("Ids linear temp. coeff.", "1/K", float, 0.0), tme=("Ids power law temp. coeff.", "1/K^2", float, 0.0), eg0=("Barrier height at 0 K", "eV", float, 0.8), mgs=("Gate-source grading coefficient", "", float, 0.5), mgd=("Gate-drain grading coefficient", "", float, 0.5), xti=("Diode saturation current temperature exponent", "", float, 2.0), area=("Area multiplier", "", float, 1.0), ) numTerms = 3 # Create electrothermal device makeAutoThermal = True isNonlinear = True nDelays = 2 # igs, igd, ids csOutPorts = [(1, 2), (1, 0), (0, 2)] # Controling voltages are Vgs, Vgd controlPorts = [(1, 2), (1, 0)] # Time-delayed control port added later. Guess includes time-delayed port vPortGuess = np.array([0.0, 0.0, 0.0, 0.0]) # charge sources qsOutPorts = [(1, 2), (1, 0)] def __init__(self, instanceName): """ Here the Element constructor must be called. Do not connect internal nodes here. """ cir.Element.__init__(self, instanceName) self.diogs = Junction() self.diogd = Junction() def process_params(self, thermal=False): # Called once the external terminals have been connected and # the non-default parameters have been set. Make sanity checks # here. Internal terminals/devices should also be defined # here. Raise cir.CircuitError if a fatal error is found. ad.delete_tape(self) # Time-delayed control port self.delayedContPorts = [(1, 2, self.tau), (1, 0, self.tau)] # Absolute nominal temperature self.Tnomabs = self.tnom + const.T0 self.egapn = self.eg0 - 0.000702 * (self.Tnomabs ** 2) / (self.Tnomabs + 1108.0) # Calculate variables in junctions self.diogs.process_params( self.isat, self.n, self.fcc, self.cgs0, self.vbi, self.mgs, self.xti, self.eg0, self.Tnomabs ) self.diogd.process_params( self.isat, self.n, self.fcc, self.cgd0, self.vbi, self.mgd, self.xti, self.eg0, self.Tnomabs ) if not thermal: # Calculate temperature-dependent variables self.set_temp_vars(self.temp) def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ ad.delete_tape(self) # Absolute temperature (note self.temp is in deg. C) self.Tabs = const.T0 + temp # Thermal voltage self.vt = const.k * self.Tabs / const.q # Temperature-adjusted egap self.egap_t = self.eg0 - 0.000702 * (self.Tabs ** 2) / (self.Tabs + 1108.0) # Everything else is handled by junctions self.diogs.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.diogd.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) delta_T = temp - self.tnom self._k6 = self.nr * self.vt self._Vt0 = self.vt0 * (1.0 + (delta_T * self.avt0) + (delta_T ** 2 * self.bvt0)) if self.tbet: self._Beta = self.beta * pow(1.01, (delta_T * self.tbet)) else: self._Beta = self.beta if self.tme and self.tm: self._idsFac = pow((1.0 + delta_T * self.tm), self.tme) else: self._idsFac = 1.0 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) # Use AD for eval and deriv function eval_and_deriv = ad.eval_and_deriv eval = ad.eval # get_op_vars = ad.get_op_vars def power(self, vPort, currV): """ Calculate total instantaneous power Input: control voltages and currents from eval_cqs() """ vds = vPort[0] - vPort[1] # pout = vds*ids + vgs*igs + vgd*igd pout = vds * currV[2] + vPort[0] * currV[0] + vPort[1] * currV[1] return pout def get_OP(self, vPort): """ Calculates operating point information Input: vPort = [vgs , vgd , vgs] Output: dictionary with OP variables """ # First we need the Jacobian (outV, jac) = self.eval_and_deriv(vPort) # opV = self.get_op_vars(vPort) self.OP = dict(VGS=vPort[0], VDS=vPort[0] - vPort[1], IDS=outV[2], IGS=outV[0], IGD=outV[1]) return self.OP def get_noise(self, f): """ Return noise spectral density at frequency f Requires a previous call to get_OP() Not implemented yet """ return None
class ExtMOS(IMOS): """ Extrinsic Silicon MOSFET ------------------------ Extrinsic Internal Topology +++++++++++++++++++++++++++ The model adds the following to the intrinsic model (for NMOS):: o D (0) | \ Cgdo / Rd Drain/source area plus \ sidewall model || |-----------,-----, ,------||------------| | | | || | ----- ----- | ||--- ----- / \ | || | ----- G (1) o---+----------------||<-------------+-----+------o B (3) | || | ----- | ||--- ----- \ / | || | ----- ----- `------||------------| | | || |-----------'-----' \ Cgso / Rs \ | o S (2) Note: electrothermal implementation (if any) does not account for the power dissipation in Rd and Rs. Use external thermal resistors if that is needed. """ # devtype is the 'model' name: remove the '_i' from intrinsic name devType = 'mos' + IMOS.devType.split('_i')[0] # Additional documentation extraDoc = """ Netlist examples ++++++++++++++++ The model accepts extrinsic plus intrinsic parameters (only extrinsic parameters shown in example):: {0}:m1 2 3 4 gnd w=10u l=1u asrc=4e-12 ps=8e=12 model=nch {0}:m2 4 5 6 6 w=30e-6 l=1e-6 pd=8u ps=16u type=p .model nch {0} (type=n js=1e-3 cj=2e-4 cjsw=1n) Intrinsic model +++++++++++++++ See **{1}** intrinsic model documentation. """.format(devType, IMOS.devType) paramDict = dict( IMOS.paramDict.items(), m = ('Parallel multiplier', '', float, 1.), cgdo = ('Gate-drain overlap capacitance per meter channel width', 'F/m', float, 0.), cgso = ('Gate-source overlap capacitance per meter channel width', 'F/m', float, 0.), cgbo = ('Gate-bulk overlap capacitance per meter channel length', 'F/m', float, 0.), rsh = ('Drain and source diffusion sheet resistance', 'Ohm/square', float, 0.), js = ('Source drain junction current density', 'A/m^2', float, 0.), jssw = ('Source drain sidewall junction current density', 'A/m', float, 0.), pb = ('Built in potential of source drain junction', 'V', float, .8), mj = ('Grading coefficient of source drain junction', '', float, .5), pbsw = ('Built in potential of source, drain junction sidewall', 'V', float, .8), mjsw = ('Grading coefficient of source drain junction sidewall', '', float, .33), cj = ('Source drain junction capacitance per unit area', 'F/m^2', float, 0.), cjsw = ( 'Source drain junction sidewall capacitance per unit length', 'F/m', float, 0.), ad = ('Drain area', 'm^2', float, 0.), asrc = ('Source area', 'm^2', float, 0.), pd = ('Drain perimeter', 'm', float, 0.), ps = ('Source perimeter', 'm', float, 0.), nrd = ('Number of squares in drain', 'squares', float, 1.), nrs = ('Number of squares in source', 'squares', float, 1.), fc = ('Coefficient for forward-bias depletion capacitances', ' ', float, .5), xti = ('Junction saturation current temperature exponent', '', float, 3.), eg0 = ('Energy bandgap', 'eV', float, 1.11) ) def __init__(self, instanceName): IMOS.__init__(self, instanceName) self.__doc__ += IMOS.__doc__ def process_params(self, thermal = False): # Remove tape if present ad.delete_tape(self) # Remove internal terminals (there should be none created # by intrinsic model) self.clean_internal_terms() # Tell autothermal (if used) to re-generate thermal ports self.__addThermalPorts = True # By default drain and source are terminals 0 and 2 self.__di = 0 self.__si = 2 # Resistances extraVCCS = list() if self.rsh: if self.nrd: # Drain resistor self.__di = self.add_internal_term('di', 'V') extraVCCS += [((0, self.__di), (0, self.__di), 1. / self.rsh / self.nrd)] if self.nrs: # Source resistor self.__si = self.add_internal_term('si', 'V') extraVCCS += [((2, self.__si), (2, self.__si), 1. / self.rsh / self.nrs)] # Linear capacitances extraVCQS = list() if self.cgdo: # Gate-drain ovelrlap cap extraVCQS += [((1, self.__di), (1, self.__di), self.cgdo * self.w)] if self.cgso: # Gate-source ovelrlap cap extraVCQS += [((1, self.__si), (1, self.__si), self.cgso * self.w)] if self.cgbo: # Gate-bulk ovelrlap cap extraVCQS += [((1, 3), (1, 3), self.cgbo * self.l)] # Add extra linear resistors/caps (if any) self.linearVCCS = extraVCCS self.linearVCQS = extraVCQS # Override nonlinear port specs if needed if extraVCCS: # Ids, Idb, Isb self.csOutPorts = [(self.__di, self.__si), (self.__di, 3), (self.__si, 3)] # Controling voltages are DB, GB and SB self.controlPorts = [(self.__di, 3), (1, 3), (self.__si, 3)] # One charge source connected to each D, G, S self.qsOutPorts = [(self.__di, 3), (1, 3), (self.__si, 3)] # Calculate some variables (that may also be calculated in # intrinsic model) self.__Tnabs = const.T0 + self.tnom self.__egapn = self.eg0 - .000702 * (self.__Tnabs**2) \ / (self.__Tnabs + 1108.) # Initialize variables in junctions if self.ad: self.dj = Junction() self.dj.process_params(isat = self.js * self.ad, cj0 = self.cj * self.ad, vj = self.pb, m = self.mj, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.asrc: self.sj = Junction() self.sj.process_params(isat = self.js * self.asrc, cj0 = self.cj * self.asrc, vj = self.pb, m = self.mj, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.pd: self.djsw = Junction() self.djsw.process_params(isat = self.jssw * self.pd, cj0 = self.cjsw * self.pd, vj = self.pbsw, m = self.mjsw, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.ps: self.sjsw = Junction() self.sjsw.process_params(isat = self.jssw * self.ps, cj0 = self.cjsw * self.ps, vj = self.pbsw, m = self.mjsw, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) # Process parameters from intrinsic device: # set_temp_vars() called there IMOS.process_params(self) def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Remove tape if present ad.delete_tape(self) # First calculate variables from base class IMOS.set_temp_vars(self, temp) # Absolute temperature Tabs = temp + const.T0 # Temperature-adjusted egap egap_t = self.eg0 - .000702 * (Tabs**2) / (Tabs + 1108.) # Thermal voltage Vt = const.k * Tabs / const.q # Adjust junction temperatures if self.ad: self.dj.set_temp_vars(Tabs, self.__Tnabs, Vt, self.__egapn, egap_t) if self.pd: self.djsw.set_temp_vars(Tabs, self.__Tnabs, Vt, self.__egapn, egap_t) if self.asrc: self.sj.set_temp_vars(Tabs, self.__Tnabs, Vt, self.__egapn, egap_t) if self.ps: self.sjsw.set_temp_vars(Tabs, self.__Tnabs, Vt, self.__egapn, egap_t) def eval_cqs(self, vPort, saveOP = False): """ vPort is a vector with control voltages """ # calculate currents and charges in base class if saveOP: (iVec, qVec, opVec) = IMOS.eval_cqs(self, vPort, saveOP) else: (iVec, qVec) = IMOS.eval_cqs(self, vPort) # Add contribution drain diode v1 = -vPort[0] * self._tf if self.ad: # substract to idb iVec[1] -= self.dj.get_id(v1) * self._tf if self.cj: # substract to qd qVec[0] -= self.dj.get_qd(v1) * self._tf if self.pd: # substract to idb iVec[1] -= self.djsw.get_id(v1) * self._tf if self.cjsw: qVec[0] -= self.djsw.get_qd(v1) * self._tf # Add contribution source diode v1 = -vPort[2] * self._tf if self.asrc: # substract to isb iVec[2] -= self.sj.get_id(v1) * self._tf if self.cj: # substract to qs qVec[2] -= self.sj.get_qd(v1) * self._tf if self.ps: # substract to isb iVec[2] -= self.sjsw.get_id(v1) * self._tf if self.cjsw: qVec[2] -= self.sjsw.get_qd(v1) * self._tf # Apply parallel multiplier iVec *= self.m qVec *= self.m if saveOP: return (iVec, qVec, opVec) else: return (iVec, qVec) # Create these using the AD facility eval_and_deriv = ad.eval_and_deriv eval = ad.eval def power(self, vPort, currV): """ Calculate total instantaneous power Power in RE, RC not considered. Input: control voltages as in eval_cqs() and currents returned by eval_cqs() """ pout = IMOS.power(self, vPort, currV) if self.__addCBjtn: # add power from substrate junction and RE, RC pout += vPort[self.__bccn] * currV[self.__bcon] return pout
def process_params(self, thermal = False): # Remove tape if present ad.delete_tape(self) # Remove internal terminals (there should be none created # by intrinsic model) self.clean_internal_terms() # Tell autothermal (if used) to re-generate thermal ports self.__addThermalPorts = True # By default drain and source are terminals 0 and 2 self.__di = 0 self.__si = 2 # Resistances extraVCCS = list() if self.rsh: if self.nrd: # Drain resistor self.__di = self.add_internal_term('di', 'V') extraVCCS += [((0, self.__di), (0, self.__di), 1. / self.rsh / self.nrd)] if self.nrs: # Source resistor self.__si = self.add_internal_term('si', 'V') extraVCCS += [((2, self.__si), (2, self.__si), 1. / self.rsh / self.nrs)] # Linear capacitances extraVCQS = list() if self.cgdo: # Gate-drain ovelrlap cap extraVCQS += [((1, self.__di), (1, self.__di), self.cgdo * self.w)] if self.cgso: # Gate-source ovelrlap cap extraVCQS += [((1, self.__si), (1, self.__si), self.cgso * self.w)] if self.cgbo: # Gate-bulk ovelrlap cap extraVCQS += [((1, 3), (1, 3), self.cgbo * self.l)] # Add extra linear resistors/caps (if any) self.linearVCCS = extraVCCS self.linearVCQS = extraVCQS # Override nonlinear port specs if needed if extraVCCS: # Ids, Idb, Isb self.csOutPorts = [(self.__di, self.__si), (self.__di, 3), (self.__si, 3)] # Controling voltages are DB, GB and SB self.controlPorts = [(self.__di, 3), (1, 3), (self.__si, 3)] # One charge source connected to each D, G, S self.qsOutPorts = [(self.__di, 3), (1, 3), (self.__si, 3)] # Calculate some variables (that may also be calculated in # intrinsic model) self.__Tnabs = const.T0 + self.tnom self.__egapn = self.eg0 - .000702 * (self.__Tnabs**2) \ / (self.__Tnabs + 1108.) # Initialize variables in junctions if self.ad: self.dj = Junction() self.dj.process_params(isat = self.js * self.ad, cj0 = self.cj * self.ad, vj = self.pb, m = self.mj, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.asrc: self.sj = Junction() self.sj.process_params(isat = self.js * self.asrc, cj0 = self.cj * self.asrc, vj = self.pb, m = self.mj, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.pd: self.djsw = Junction() self.djsw.process_params(isat = self.jssw * self.pd, cj0 = self.cjsw * self.pd, vj = self.pbsw, m = self.mjsw, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) if self.ps: self.sjsw = Junction() self.sjsw.process_params(isat = self.jssw * self.ps, cj0 = self.cjsw * self.ps, vj = self.pbsw, m = self.mjsw, n = 1., fc = self.fc, xti = self.xti, eg0 = self.eg0, Tnomabs = self.__Tnabs) # Process parameters from intrinsic device: # set_temp_vars() called there IMOS.process_params(self)
class Device(cir.Element): """ Cubic Curtice-Ettemberg Intrinsic MESFET Model ---------------------------------------------- Model derived from fREEDA 1.4 MesfetCT model adapted to re-use junction code from ``diode.py``. Some parameter names have been changed: ``isat``, ``tau``. Uses symmetric diodes and capacitances. Works in reversed mode. Terminal order: 0 Drain, 1 Gate, 2 Source:: Drain 0 o | | |---+ | Gate 1 o---->| | |---+ | | o Source 2 Netlist example:: mesfetc:m1 2 3 4 a0=0.09910 a1=0.08541 a2=-0.02030 a3=-0.01543 Internal Topology:: ,----------------,------------,--o 0 (D) | | | /^\ | | ( | ) igd(Vgd) ----- Cgd | \|/ ----- | | | /|\ (G) 1 o----+----------------, ( | ) ids(Vgs, Vgd) | | \V/ /|\ | | ( | ) igs(Vgs) ----- Cgs | \V/ ----- | | | | `----------------'------------'--o 2 (S) """ # Device category category = "Semiconductor devices" devType = "mesfetc" paramDict = dict( cir.Element.tempItem, a0=('Drain saturation current for Vgs=0', 'A', float, 0.1), a1=('Coefficient for V1', 'A/V', float, 0.05), a2=('Coefficient for V1^2', 'A/V^2', float, 0.), a3=('Coefficient for V1^3', 'A/V^3', float, 0.), beta=('V1 dependance on Vds', '1/V', float, 0.), vds0=('Vds at which BETA was measured', 'V', float, 4.), gama=('Slope of drain characteristic in the linear region', '1/V', float, 1.5), vt0=('Voltage at which the channel current is forced to be zero\ for Vgs<=Vto', 'V', float, -np.inf), cgs0=('Gate-source Schottky barrier capacitance for Vgs=0', 'F', float, 0.), cgd0=('Gate-drain Schottky barrier capacitance for Vgd=0', 'F', float, 0.), isat=('Diode saturation current', 'A', float, 0.), n=('Diode ideality factor', '', float, 1.), ib0=('Breakdown current parameter', 'A', float, 0.), nr=('Breakdown ideality factor', '', float, 10.), vbd=('Breakdown voltage', 'V', float, np.inf), tau=('Channel transit time', 's', float, 0.), vbi=('Built-in potential of the Schottky junctions', 'V', float, 0.8), fcc=('Forward-bias depletion capacitance coefficient', 'V', float, 0.5), tnom=('Nominal temperature', 'C', float, 27.), avt0=('Pinch-off voltage (VP0 or VT0) linear temp. coefficient', '1/K', float, 0.), bvt0=('Pinch-off voltage (VP0 or VT0) quadratic temp. coefficient', '1/K^2', float, 0.), tbet=('BETA power law temperature coefficient', '1/K', float, 0), tm=('Ids linear temp. coeff.', '1/K', float, 0.), tme=('Ids power law temp. coeff.', '1/K^2', float, 0.), eg0=('Barrier height at 0 K', 'eV', float, 0.8), mgs=('Gate-source grading coefficient', '', float, 0.5), mgd=('Gate-drain grading coefficient', '', float, 0.5), xti=('Diode saturation current temperature exponent', '', float, 2.), area=('Area multiplier', '', float, 1.), ) numTerms = 3 # Create electrothermal device makeAutoThermal = True isNonlinear = True nDelays = 2 # igs, igd, ids csOutPorts = [(1, 2), (1, 0), (0, 2)] # Controling voltages are Vgs, Vgd controlPorts = [(1, 2), (1, 0)] # Time-delayed control port added later. Guess includes time-delayed port vPortGuess = np.array([0., 0., 0., 0.]) # charge sources qsOutPorts = [(1, 2), (1, 0)] def __init__(self, instanceName): """ Here the Element constructor must be called. Do not connect internal nodes here. """ cir.Element.__init__(self, instanceName) self.diogs = Junction() self.diogd = Junction() def process_params(self, thermal=False): # Called once the external terminals have been connected and # the non-default parameters have been set. Make sanity checks # here. Internal terminals/devices should also be defined # here. Raise cir.CircuitError if a fatal error is found. ad.delete_tape(self) # Time-delayed control port self.delayedContPorts = [(1, 2, self.tau), (1, 0, self.tau)] # Absolute nominal temperature self.Tnomabs = self.tnom + const.T0 self.egapn = self.eg0 - .000702 * (self.Tnomabs**2) \ / (self.Tnomabs + 1108.) # Calculate variables in junctions self.diogs.process_params(self.isat, self.n, self.fcc, self.cgs0, self.vbi, self.mgs, self.xti, self.eg0, self.Tnomabs) self.diogd.process_params(self.isat, self.n, self.fcc, self.cgd0, self.vbi, self.mgd, self.xti, self.eg0, self.Tnomabs) if not thermal: # Calculate temperature-dependent variables self.set_temp_vars(self.temp) def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ ad.delete_tape(self) # Absolute temperature (note self.temp is in deg. C) self.Tabs = const.T0 + temp # Thermal voltage self.vt = const.k * self.Tabs / const.q # Temperature-adjusted egap self.egap_t = self.eg0 - .000702 * (self.Tabs**2) / (self.Tabs + 1108.) # Everything else is handled by junctions self.diogs.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.diogd.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) delta_T = temp - self.tnom self._k6 = self.nr * self.vt self._Vt0 = self.vt0 * (1. + (delta_T * self.avt0) + (delta_T**2 * self.bvt0)) if (self.tbet): self._Beta = self.beta * pow(1.01, (delta_T * self.tbet)) else: self._Beta = self.beta if self.tme * self.tm != 0.: self._idsFac = pow((1. + delta_T * self.tm), self.tme) else: self._idsFac = 1. 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) # Use AD for eval and deriv function eval_and_deriv = ad.eval_and_deriv eval = ad.eval def power(self, vPort, currV): """ Calculate total instantaneous power Input: control voltages and currents from eval_cqs() """ vds = vPort[0] - vPort[1] # pout = vds*ids + vgs*igs + vgd*igd pout = vds * currV[2] + vPort[0] * currV[0] + vPort[1] * currV[1] return pout def get_OP(self, vPort): """ Calculates operating point information Input: vPort = [vgs , vgd , vgs] Output: dictionary with OP variables """ # First we need the Jacobian (outV, jac) = self.eval_and_deriv(vPort) opDict = dict(VGS=vPort[0], VDS=vPort[0] - vPort[1], IDS=outV[2], IGS=outV[0], IGD=outV[1]) return opDict def get_noise(self, f): """ Return noise spectral density at frequency f Requires a previous call to get_OP() Not implemented yet """ return None
class BJTi(cir.Element): """ Gummel-Poon intrinsic BJT model This implementation based mainly on previous implementation in carrot and some equations from Pspice manual. Terminal order: 0 Collector, 1 Base, 2 Emitter:: C (0) o----, 4----o E (2) \ / \ / --------- | o B (1) Can be used for NPN or PNP transistors. Intrinsic Internal Topology +++++++++++++++++++++++++++ Internally may add 2 additional nodes (plus reference) if rb is not zero: Bi for the internal base node and tib to measure the internal base current and calculate Rb(ib). The possible configurations are described here. 1. If RB == 0:: +----------------+--o 0 (C) | | /^\ | ( | ) ibc(vbc) | \|/ | | /|\ (B) 1 o---------+ ( | ) ice | \V/ /|\ | ( | ) ibe(vbe) | \V/ | | | +----------------+--o 2 (E) 2. If RB != 0:: +----------------+--o 0 (C) | | /^\ | ( | ) ibc(vbc) | gyr * tib \|/ | ,---, | /|\ (B) 1 o----( --> )----------+ Term : Bi ( | ) ice `---` | \V/ /|\ | ( | ) ibe(vbe) | \V/ | | | +----------------+--o 2 (E) gyr v(1,Bi) ,---, +---( <-- )------+ | `---` | tref | | voltage: ib/gyr ,---+ | | | ,---, | | +---( --> )------+ Term : ib | `---` --- gyr ib Rb(ib) V Charge sources are connected between internal nodes defined above. If xcjc is not 1 but RB is zero, xcjc is ignored. """ devType = "bjt" paramDict = dict( cir.Element.tempItem, type=('Type (npn or pnp)', '', str, 'npn'), isat=('Transport saturation current', 'A', float, 1e-16), bf=('Ideal maximum forward beta', '', float, 100.), nf=('Forward current emission coefficient', '', float, 1.), vaf=('Forward early voltage', 'V', float, 0.), ikf=('Forward-beta high current roll-off knee current', 'A', float, 0.), ise=('Base-emitter leakage saturation current', 'A', float, 0.), ne=('Base-emitter leakage emission coefficient', '', float, 1.5), br=('Ideal maximum reverse beta', '', float, 1.), nr=('Reverse current emission coefficient', '', float, 1.), var=('Reverse early voltage', 'V', float, 0.), ikr=('Corner for reverse-beta high current roll off', 'A', float, 0.), isc=('Base collector leakage saturation current', 'A', float, 0.), nc=('Base-collector leakage emission coefficient', '', float, 2.), rb=('Zero bias base resistance', 'Ohm', float, 0.), rbm=('Minimum base resistance', 'Ohm', float, 0.), irb=('Current at which rb falls to half of rbm', 'A', float, 0.), eg=('Badgap voltage', 'eV', float, 1.11), cje=('Base emitter zero bias p-n capacitance', 'F', float, 0.), vje=('Base emitter built in potential', 'V', float, 0.75), mje=('Base emitter p-n grading factor', '', float, 0.33), cjc=('Base collector zero bias p-n capacitance', 'F', float, 0.), vjc=('Base collector built in potential', 'V', float, 0.75), mjc=('Base collector p-n grading factor', '', float, 0.33), xcjc=('Fraction of cbc connected internal to rb', '', float, 1.), fc=('Forward bias depletion capacitor coefficient', '', float, 0.5), tf=('Ideal forward transit time', 's', float, 0.), xtf=('Transit time bias dependence coefficient', '', float, 0.), vtf=('Transit time dependency on vbc', 'V', float, 0.), itf=('Transit time dependency on ic', 'A', float, 0.), tr=('Ideal reverse transit time', 's', float, 0.), xtb=('Forward and reverse beta temperature coefficient', '', float, 0.), xti=('IS temperature effect exponent', '', float, 3.), tnom=('Nominal temperature', 'C', float, 27.), area=('Current multiplier', '', float, 1.)) def __init__(self, instanceName): cir.Element.__init__(self, instanceName) # Use junctions to model diodes and capacitors self.jif = Junction() self.jir = Junction() self.jile = Junction() self.jilc = Junction() # collector/emitter terminal numbers: may be re-mapped by # extrinsic device self._ct = 0 self._et = 2 def process_params(self): """ Adjusts internal topology and makes preliminary calculations according to parameters """ # Default configuration assumes rb == 0 # ibe, ibc, ice self.csOutPorts = [(1, self._et), (1, self._ct), (self._ct, self._et)] # Controling voltages are vbe, vbc self.controlPorts = [(1, self._et), (1, self._ct)] self.vPortGuess = np.array([0., 0.]) # qbe, qbc self.qsOutPorts = [(1, self._et), (1, self._ct)] # Define topology first # Flag to signal if the extra charge Qbx is needed or not self._qbx = False self.linearVCCS = [] if self.rb != 0.: # rb is not zero: add internal terminals tBi = self.add_internal_term('Bi', 'V') tib = self.add_internal_term('ib', '{0} A'.format(glVar.gyr)) tref = self.add_reference_term() # Linear VCCS for gyrator(s) self.linearVCCS = [((1, tBi), (tib, tref), glVar.gyr), ((tib, tref), (1, tBi), glVar.gyr)] # ibe, ibc, ice, Rb(ib) * ib self.csOutPorts = [(tBi, self._et), (tBi, self._ct), (self._ct, self._et), (tref, tib)] # Controling voltages are vbie, vbic and gyrator port self.controlPorts = [(tBi, self._et), (tBi, self._ct), (tib, tref)] self.vPortGuess = np.array([0., 0., 0.]) # qbie, qbic self.qsOutPorts = [(tBi, self._et), (tBi, self._ct)] # Now check if Cjbc must be splitted (since rb != 0) if (self.cjc != 0.) and (self.xcjc < 1.): self.qsOutPorts.append((1, self._ct)) self._qbx = True # In principle we may not need any charge keepPorts = [] if self.cje + self.tf != 0.: # keep qbe keepPorts.append(self.qsOutPorts[0]) if self.cjc + self.tr != 0.: # keep qbc, qbx (if any) if self._qbx: keepPorts += self.qsOutPorts[-2:] else: keepPorts.append(self.qsOutPorts[-1]) self.qsOutPorts = keepPorts # keep track of how many output variables are needed self.ncurrents = len(self.csOutPorts) self.ncharges = len(self.qsOutPorts) # NPN or PNP if self.type == 'pnp': self._typef = -1. else: self._typef = 1. # Calculate common variables # Absolute nominal temperature self.Tnomabs = self.tnom + const.T0 self.egapn = self.eg - .000702 * (self.Tnomabs**2) \ / (self.Tnomabs + 1108.) # jif produces if, cje self.jif.process_params(self.isat, self.nf, self.fc, self.cje, self.vje, self.mje, self.xti, self.eg, self.Tnomabs) # jir produces ir, cjc self.jir.process_params(self.isat, self.nr, self.fc, self.cjc, self.vjc, self.mjc, self.xti, self.eg, self.Tnomabs) if self.ise != 0.: # jile produces ile self.jile.process_params(self.ise, self.ne, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) if self.isc != 0.: # jilc produces ilc self.jilc.process_params(self.isc, self.nc, 0, 0, 0, 0, self.xti, self.eg, self.Tnomabs) # Constants needed for rb(ib) calculation if self.irb != 0.: self._ck1 = 144. / self.irb / self.area / np.pi / np.pi self._ck2 = np.pi * np.pi * np.sqrt(self.irb * self.area) / 24. def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Absolute temperature (note self.temp is in deg. C) self.Tabs = const.T0 + temp # Normalized temp self.tnratio = self.Tabs / self.Tnomabs tnXTB = pow(self.tnratio, self.xtb) # Thermal voltage self.vt = const.k * self.Tabs / const.q # Temperature-adjusted egap self.egap_t = self.eg - .000702 * (self.Tabs**2) / (self.Tabs + 1108.) # set temperature in juctions self.jif.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jir.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) # Adjust ise and isc (which have different temperature variation) if self.ise != 0.: self.jile.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jile._t_is /= tnXTB if self.isc != 0.: self.jilc.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) self.jilc._t_is /= tnXTB # Now some BJT-only variables self._bf_t = self.bf * tnXTB self._br_t = self.br * tnXTB 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 != 0.: ile = self.jile.get_id(vPort1[0]) else: ile = 0. if self.isc != 0.: ilc = self.jilc.get_id(vPort1[1]) else: ilc = 0. # Kqb q1m1 = 1. if self.var != 0.: q1m1 -= vPort1[0] / self.var if self.vaf != 0.: q1m1 -= vPort1[1] / 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 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 != 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[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 != 0.: # Effective tf tfeff = self.tf if self.vtf != 0.: 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 != 0.: qVec[0] += self.jif.get_qd(vPort1[0]) # qbc if self._qbx: if self.tr != 0.: qVec[-2] = self.tr * ibr if self.cjc != 0.: 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 != 0.: qVec[-1] = self.tr * ibr if self.cjc != 0.: 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 power(self, vPort, currV): """ Calculate total instantaneous power Input: control voltages as in eval_cqs() and currents returned by eval_cqs() """ vce = vPort[0] - vPort[1] if self.rb != 0.: # currV[3] = ib * Rb * gyr # vPort[2] = ib / gyr pRb = currV[3] * vPort[2] else: pRb = 0. # pout = ibe * vbie + ibc * vbic + vce * ice + pRb pout = currV[0] * vPort[0] + currV[1] * vPort[1] \ + currV[2] * vce + pRb return pout def get_OP(self, vPort): """ Calculates operating point information Input: same as eval_cqs Output: dictionary with OP variables For now it is quite incomplete """ # First we need the Jacobian (outV, jac) = self.eval_and_deriv(vPort) power = self.power(vPort, outV) opDict = dict( VBE=vPort[0], VCE=vPort[0] - vPort[1], IB=outV[0] + outV[1], IC=outV[2] - outV[1], IE=-outV[2] - outV[0], Temp=self.temp, Power=power, gm=jac[2, 0] - jac[1, 0], rpi=1. / (jac[0, 0] + jac[1, 0]), ) return opDict def get_noise(self, f): """ Return noise spectral density at frequency f Requires a previous call to get_OP() Not implemented yet """ return None
class BJT(IBJT): """ Bipolar Junction Transistor --------------------------- This device accepts 3 or 4 terminal connections. Netlist examples:: bjt:q1 2 3 4 1 model = mypnp isat=4e-17 bf=147 iss=10fA bjt:q2 2 3 4 model = mypnp isat=4e-17 bf=147 vaf=80 ikf=4m svbjt:q3 2 3 4 1 model = mypnp vaf=80 ikf=4m iss=15fA # Electro-thermal versions bjt_t:q2 2 3 5 1 pout gnd model = mypnp svbjt_t:q3 2 3 5 1 pout gnd model = mypnp # Model statement .model mypnp bjt_t (type=pnp isat=5e-17 cje=60fF vje=0.83 mje=0.35) Extrinsic Internal Topology +++++++++++++++++++++++++++ RC, RE and a Collector-Bulk connection are added to intrinsic BJT models:: RC Term: ct Term: et RE C (0) o---/\/\/\/--+-----, 4----/\/\/\/----o E (2) | \ / | \ / ----- --------- / \ | / \ o ----- | B (1) o Bulk (3) If RE or RC are zero the internal nodes (ct, et) are not created. If only 3 connections are specified then the Bulk-Collector junction is not connected. Important Note ++++++++++++++ This implementation does not account for the power dissipation in RE, RC. Use external thermal resistors if that is needed. Intrinsic Model Information +++++++++++++++++++++++++++ """ # Additional documentation extraDoc = IBJT.__doc__ # Create electrothermal device makeAutoThermal = True isNonlinear = True # Device category category = "Semiconductor devices" # devtype is the 'model' name: remove the 'i' from intrinsic name devType = IBJT.devType # Do not set numTerms to allow 3 or 4 terminals to be connected paramDict = dict( IBJT.paramDict.items(), re=('Emitter ohmic resistance', 'Ohm', float, None), rc=('Collector ohmic resistance', 'Ohm', float, None), cjs=('Collector substrate capacitance', 'F', float, 0.), mjs=('substrate junction exponential factor', '', float, 0.), vjs=('substrate junction built in potential', 'V', float, 0.75), ns=('substrate p-n coefficient', '', float, 1.), iss=('Substrate saturation current', 'A', float, 1e-14)) def __init__(self, instanceName): IBJT.__init__(self, instanceName) # Collector-bulk junction self.cbjtn = Junction() self.__doc__ += IBJT.__doc__ def process_params(self, thermal=False): # Remove tape if present ad.delete_tape(self) if thermal: extraTerms = 2 else: extraTerms = 0 # First check external connections if self.numTerms == 3 + extraTerms: self.__addCBjtn = False elif self.numTerms == 4 + extraTerms: self.__addCBjtn = True else: raise cir.CircuitError('{0}: Wrong number of connections. \ Can only be {1} or {2}, {3} found.'.format(self.instanceName, 3 + extraTerms, 4 + extraTerms, self.numTerms)) # Remove internal terminals self.clean_internal_terms() # Tell autothermal to re-generate thermal ports self.__addThermalPorts = True extraVCCS = list() if self.re != None: # Add et node and translate port descriptions self._et = self.add_internal_term('et', 'V') extraVCCS += [((2, self._et), (2, self._et), self.area / self.re)] if self.rc != None: # Add ct node and translate port descriptions self._ct = self.add_internal_term('ct', 'V') extraVCCS += [((0, self._ct), (0, self._ct), self.area / self.rc)] # Process parameters from intrinsic device: emitter and # collector terminals are already substituted. IBJT.process_params(self) # Calculate variables in junction self.cbjtn.process_params(self.iss, self.ns, self.fc, self.cjs, self.vjs, self.mjs, self.xti, self.eg, self.Tnomabs) # Add RE, RC resistors (if any) self.linearVCCS += extraVCCS if self.__addCBjtn: # Add bulk-collector junction self.__bccn = len(self.controlPorts) self.__bcon = len(self.csOutPorts) self.controlPorts.append((3, self._ct)) self.csOutPorts.append((3, self._ct)) if self.cjs != 0.: self.qsOutPorts.append((3, self._ct)) # Initial guess for input ports: try: if len(self.vPortGuess) < len(self.controlPorts): self.vPortGuess = np.concatenate( (self.vPortGuess, [1]), axis=0) except AttributeError: # Ignore if vPortGuess not provided pass # Adjust temperature self.set_temp_vars(self.temp) def set_temp_vars(self, temp): """ Calculate temperature-dependent variables, given temp in deg. C """ # Remove tape if present ad.delete_tape(self) # First calculate variables from base class IBJT.set_temp_vars(self, temp) # Adjust collector-bulk junction temperature self.cbjtn.set_temp_vars(self.Tabs, self.Tnomabs, self.vt, self.egapn, self.egap_t) def eval_cqs(self, vPort, getOP=False): """ vPort is a vector with control voltages """ if self.__addCBjtn: # calculate currents and charges in base class (iVec1, qVec1) = IBJT.eval_cqs(self, vPort) # Add contribution of substrate. Ignore extra control # ports only used for power calculation v1 = vPort[self.__bccn] * self._typef isub = self.cbjtn.get_id(v1) * self.area * self._typef iVec = np.concatenate((iVec1, [isub]), axis=0) if self.cjs != 0.: qsub = self.cbjtn.get_qd(v1) * self.area * self._typef qVec = np.concatenate((qVec1, [qsub]), axis=0) else: qVec = qVec1 return (iVec, qVec) else: return IBJT.eval_cqs(self, vPort) # Create these using the AD facility eval_and_deriv = ad.eval_and_deriv eval = ad.eval def power(self, vPort, currV): """ Calculate total instantaneous power Power in RE, RC not considered. Input: control voltages as in eval_cqs() and currents returned by eval_cqs() """ pout = IBJT.power(self, vPort, currV) if self.__addCBjtn: # add power from substrate junction (RE, RC not counted) pout += vPort[self.__bccn] * currV[self.__bcon] return pout