Exemple #1
0
def funity_vs_scale2(op_in, op_load, op_tail, cload, phase_margin, fg=2):
    s2min = 1
    s2max = 40
    num_s = 100
    cmin = 1e-16
    cmax = 1e-9
    ctol = 1e-17
    cstep = 1e-15

    scale_load = op_in['ibias'] / op_load['ibias'] * fg
    gfb = op_load['gm'] * scale_load
    s2vec = np.linspace(s2min, s2max, num_s).tolist()
    f0_list, pm0_list, f1_list, pm1_list, copt_list = [], [], [], [], []
    for s2 in s2vec:
        cir = LTICircuit()
        cir.add_transistor(op_in, 'mid', 'in', 'gnd', 'gnd', fg=fg)
        cir.add_transistor(op_load, 'mid', 'gnd', 'gnd', 'gnd', fg=scale_load)
        cir.add_transistor(op_load,
                           'out',
                           'mid',
                           'gnd',
                           'gnd',
                           fg=scale_load * s2)
        cir.add_transistor(op_tail, 'out', 'gnd', 'gnd', 'gnd', fg=fg * s2)
        cir.add_cap(cload, 'out', 'gnd')

        num, den = cir.get_num_den('in', 'out')
        f0_list.append(get_w_crossings(num, den)[0] / (2 * np.pi))
        pm0_list.append(get_stability_margins(num, den)[0])

        cir.add_conductance(gfb * s2, 'mid', 'x')
        copt = opt_cfb(phase_margin, cir, cmin, cmax, cstep, ctol)
        if copt is None:
            raise ValueError('oops, Cfb is None')
        cir.add_cap(copt, 'x', 'out')
        num, den = cir.get_num_den('in', 'out')

        f1_list.append(get_w_crossings(num, den)[0] / (2 * np.pi))
        pm1_list.append(get_stability_margins(num, den)[0])
        copt_list.append(copt)

    f, (ax0, ax1, ax2) = plt.subplots(3, sharex='all')
    ax0.plot(s2vec, np.array(copt_list) * 1e15)
    ax0.set_ylabel('Cf (fF)')
    ax1.plot(s2vec, pm1_list, label='Cf')
    ax1.plot(s2vec, pm0_list, label='no Cf')
    ax1.legend()
    ax1.set_ylabel('$\phi_{PM}$ (deg)')
    ax2.plot(s2vec, np.array(f1_list) * 1e-9, label='Cf')
    ax2.plot(s2vec, np.array(f0_list) * 1e-9, label='no Cf')
    ax2.legend()
    ax2.set_ylabel('$f_{UG}$ (GHz)')
    ax2.set_xlabel('$I_2/I_1$')
    plt.show()
Exemple #2
0
    def _get_ss_lti(self, op_dict:Mapping[str,Any], 
                    nf_dict:Mapping[str,int], 
                    cload:float) -> Tuple[float,float]:
        ckt_p = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, cload=cload, meas_side='p')
        p_num, p_den = ckt_p.get_num_den(in_name='inp', out_name='out', in_type='v')

        ckt_n = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, cload=cload, meas_side='n')
        n_num, n_den = ckt_n.get_num_den(in_name='inn', out_name='out', in_type='v')

        num, den = num_den_add(p_num, np.convolve(n_num, [-1]),
                               p_den, n_den)

        num = np.convolve(num, [0.5])

        gain = num[-1]/den[-1]
        wbw = get_w_3db(num, den)
        pm, _ = get_stability_margins(num, den)
        ugw, _ = get_w_crossings(num, den)

        if wbw == None:
            wbw = 0
        fbw = wbw/(2*np.pi)

        if ugw == None:
            ugw = 0
        ugf = ugw/(2*np.pi)

        return gain, fbw, ugf, pm
def verify_closedLoop(n_op, p_op, tail_op,
    nf_n, nf_p, nf_tail, pm_min,
    cload, cn=165e-15, cp=83e-15, r=70.71e3):
    '''
    Inputs:
    Outputs:
    '''
    ckt = construct_feedback(n_op, p_op, tail_op,
                nf_n, nf_p, nf_tail,
                cload)
            
    loopBreak = ckt.get_transfer_function(in_name='inn', out_name='out', 
                                        in_type='v')
                                        
    pm, gainm = get_stability_margins(loopBreak.num, loopBreak.den)
    if pm < pm_min or isnan(pm):
        return False, dict(pm=pm)
    return True, dict(pm=pm)
Exemple #4
0
def opt_cfb(phase_margin, cir, cmin, cmax, cstep, ctol):
    bin_iter = FloatBinaryIterator(cmin, None, ctol, search_step=cstep)
    while bin_iter.has_next():
        cur_cf = bin_iter.get_next()
        cir.add_cap(cur_cf, 'x', 'out')
        num, den = cir.get_num_den('in', 'out')
        cur_pm, _ = get_stability_margins(num, den)
        if cur_pm < phase_margin:
            if cur_cf > cmax:
                # no way to make amplifier stable, just return
                return None
            bin_iter.up()
        else:
            bin_iter.save()
            bin_iter.down()
        cir.add_cap(-cur_cf, 'x', 'out')

    return bin_iter.get_last_save()
Exemple #5
0
    def _get_ss_lti(self, op_dict:Mapping[str,Any], 
                    nf_dict:Mapping[str,int], 
                    opp_drain_conn:Mapping[str,str],
                    cload:float) -> Tuple[float,float,float]:
        '''
        Inputs:
            op_dict: Dictionary of queried database operating points.
            nf_dict: Dictionary of number of fingers for devices.
            opp_drain_conn: Drain connection dictionary, a la n_drain_conn or p_drain_conn
            cload: Load capacitance in farads.
        Outputs:
            gain: Calculated DC gain in V/V
            fbw: Calculated 3dB frequency in Hz
            ugf: Calculated unity gain frequency in Hz
            pm: Simulated unity gain phase margin in degrees. Can also be NaN.
        '''
        ckt_p = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, meas_side='p', 
                                 opp_drain_conn=opp_drain_conn, cload=cload)
        p_num, p_den = ckt_p.get_num_den(in_name='inp', out_name='out', in_type='v')

        ckt_n = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, meas_side='n', 
                                 opp_drain_conn=opp_drain_conn, cload=cload)
        n_num, n_den = ckt_n.get_num_den(in_name='inn', out_name='out', in_type='v')

        # Superposition for inverting and noninverting inputs
        num, den = num_den_add(p_num, np.convolve(n_num, [-1]),
                               p_den, n_den)
        num = np.convolve(num, [0.5])

        # Calculate figures of merit using the LTICircuit transfer function
        gain = num[-1]/den[-1]
        wbw = get_w_3db(num, den)
        pm, _ = get_stability_margins(num, den)

        if wbw == None:
            wbw = 0
        fbw = wbw/(2*np.pi)

        ugw, _ = get_w_crossings(num, den)
        if ugw == None:
            ugw = 0
        ugf = ugw/(2*np.pi)

        return gain, fbw, ugf, pm
    def _get_stb_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource) -> float:
        '''
        Returns:
            pm: Phase margin (degrees)
        '''
        ckt = LTICircuit()

        n_ser = ser_type == 'n'
        n_amp = amp_in == 'n'
        vdd = 'vdd' if rsource != 0 else 'gnd'
       
        # Series device
        ser_d = vdd if n_ser else 'reg'
        ser_s = 'reg' if n_ser else vdd
        ckt.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False)

        # Passives
        ckt.add_cap(cload, 'reg', 'gnd')
        ckt.add_cap(cdecap_amp, 'out', 'reg')
        if rsource != 0:
            ckt.add_res(rsource, 'gnd', 'vdd')

        # Amplifier
        tail_rail = 'gnd' if n_amp else vdd
        load_rail = vdd if n_amp else 'gnd'
        inp_conn = 'gnd' if n_ser else 'amp_in'
        inn_conn = 'gnd' if not n_ser else 'amp_in' 
        ckt.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False)
        ckt.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False)
        ckt.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False)
        ckt.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False)
        ckt.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False)

        # Calculating stability margins
        num, den = ckt.get_num_den(in_name='amp_in', out_name='reg', in_type='v')
        pm, _ = get_stability_margins(np.convolve(num, [-1]), den)

        return pm
Exemple #7
0
def design_inverter_tia_lti(db_n, db_p, sim_env,
        vg_res, rf_res,
        vdd, cpd, cload, 
        rdc_min, fbw_min, pm_min,
        vb_n, vb_p):
    """
    Designs a transimpedance amplifier with an inverter amplifier
    in resistive feedback. Uses the LTICircuit functionality.
    Inputs:
        db_n/p:     Databases for NMOS and PMOS device characterization data, 
                    respectively.
        sim_env:    Simulation corner.
        vg_res:     Float. Step resolution when sweeping gate voltage.
        rf_res:     Float. Step resolution when sweeping feedback resistance.
        vdd:        Float. Supply voltage.
        cpd:        Float. Input parasitic capacitance.
        cload:      Float. Output load capacitance.
        rdc_min:    Float. Minimum DC transimpedance.
        fbw_min:    Float. Minimum bandwidth (Hz).
        pm_min:     Float. Minimum phase margin.
        vb_n/p:     Float. Back-gate/body voltage of NMOS and PMOS, respectively.
    Raises:
        ValueError: If unable to meet the specification requirements.
    Returns:
        A dictionary with the following key:value pairings:
        vg:     Float. Input bias voltage.
        nf_n:   Integer. NMOS number of channel fingers.
        nf_p:   Integer. PMOS number of channel fingers.
        rf:     Float. Value of feedback resistor.
        rdc:    Float. Expected DC transimpedance.
        fbw:    Float. Expected bandwidth (Hz).
        pm:     Float. Expected phase margin.
        ibias:  Float. Expected DC bias current.
    """
    ibn_fun = db_n.get_function('ibias', env=sim_env)
    ibp_fun = db_p.get_function('ibias', env=sim_env)    

    # Get sweep values (Vg, Vd)
    vg_min = 0
    vg_max = vdd
    vg_vec = np.arange(vg_min, vg_max, vg_res)
    nf_n_vec = np.arange(1, 20, 1)  # DEBUGGING: Is there a non-brute force way of setting this?

    # Find the best operating point
    best_ibias = float('inf')
    best_op = None

    for vg in vg_vec:
        vdd_vd_ratio = vdd/vg
        #print("\nVD/VG: {}".format(vg))
        n_op_info = db_n.query(vgs=vg, vds=vg, vbs=vb_n-0)
        p_op_info = db_p.query(vgs=vg-vdd, vds=vg-vdd, vbs=vb_p-vdd)
        # Find ratio of fingers to get desired output common mode
        ibias_n = n_op_info['ibias']
        ibias_p = p_op_info['ibias']
        pn_match = abs(ibias_n/ibias_p)
        pn_ratio = pn_match/(vdd_vd_ratio - 1)  # DEBUGGING: Won't be exact
        if pn_ratio == 0:
            continue
        # Sweep the number of fingers to minimize power
        for nf_n in nf_n_vec:
            nf_p = int(round(nf_n * pn_ratio))
            if nf_p <= 0:
                continue
            ibias_error = abs(abs(ibias_p)*nf_p-abs(ibias_n)*nf_n)/(abs(ibias_n)*nf_n)
            if ibias_error > 0.05:
                continue
            print("N/P: {}/{} fingers".format(nf_n, nf_p))
            # Finding amplifier ss parameters
            inv = LTICircuit()
            inv.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n)
            inv.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p)
            inv_num, inv_den = inv.get_num_den(in_name='in', out_name='out', in_type='v')
            A0 = abs(inv_num[-1]/inv_den[-1]) 

            gds_n = n_op_info['gds'] * nf_n
            gm_n  = n_op_info['gm']  * nf_n            
            cgs_n = n_op_info['cgs'] * nf_n            
            cgd_n = n_op_info['cgd'] * nf_n            
            cds_n = n_op_info['cds'] * nf_n
            cgb_n = n_op_info.get('cgb', 0) * nf_n
            cdb_n = n_op_info.get('cdb', 0) * nf_n
            cdd_n = n_op_info['cdd'] * nf_n
            cgg_n = n_op_info['cgg'] * nf_n

            gds_p = p_op_info['gds'] * nf_p
            gm_p  = p_op_info['gm']  * nf_p                        
            cgs_p = p_op_info['cgs'] * nf_p       
            cgd_p = p_op_info['cgd'] * nf_p            
            cds_p = p_op_info['cds'] * nf_p
            cgb_p = p_op_info.get('cgb', 0) * nf_p
            cdb_p = p_op_info.get('cdb', 0) * nf_p
            cdd_p = p_op_info['cdd'] * nf_p
            cgg_p = p_op_info['cgg'] * nf_p

            gm = abs(gm_n) + abs(gm_p)
            gds = abs(gds_n) + abs(gds_p)
            ro = 1/gds
            cgs = cgs_n + cgs_p
            cds = cds_n + cds_p
            cgb = cgb_n + cgb_p
            cdb = cdb_n + cdb_p
            cgd = cgd_n + cgd_p
            cdd = cdd_n + cdd_p
            cgg = cgg_n + cgg_p

            # Assume Rdc is negative, bound Rf
            rf_min = max(rdc_min*(1+A0)/A0 + ro/A0, 0)
            rf_vec = np.arange(rf_min, rdc_min*5, rf_res)
            # Sweep values of Rf to check f3dB and PM spec
            for rf in rf_vec:
                # Circuit for GBW
                circuit = LTICircuit()
                circuit.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n)
                circuit.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p)
                circuit.add_res(rf, 'in', 'out')
                circuit.add_cap(cpd, 'in', 'gnd')
                circuit.add_cap(cload, 'out', 'gnd')
                # Determining if it meets spec
                num, den = circuit.get_num_den(in_name='in', out_name='out', in_type='i')
                rdc = num[-1]/den[-1]
                if abs(rdc) < rdc_min-1e-8:
                    print("RDC: {0:.2f} (FAIL)\n".format(rdc))
                    continue
                else:
                    print("RDC: {0:.2f}".format(rdc))
                fbw = get_w_3db(num, den)/(2*np.pi)
                if fbw < fbw_min or isnan(fbw):
                    print("BW: {} (FAIL)\n".format(fbw))
                    break   # Increasing Rf isn't going to help
                else:
                    print("BW: {}".format(fbw))

                # Circuit for phase margin
                # miller = (1-gm*rf)/(ro+rf)*ro
                circuit2 = LTICircuit()
                circuit2.add_conductance(gds, 'out', 'gnd')
                circuit2.add_cap(cgg+cpd, 'in', 'gnd')
                circuit2.add_cap(cdd+cload, 'out', 'gnd')
                circuit2.add_cap(cgd, 'in', 'out')
                circuit2.add_res(rf, 'in', 'out')
                loopBreak = circuit2.get_transfer_function(in_name='out', out_name='in', in_type='i')
                pm, gainm = get_stability_margins(loopBreak.num*gm, loopBreak.den)
                if pm < pm_min or isnan(pm):
                    print("PM: {} (FAIL)\n".format(pm))
                    continue
                else:
                    print("PM: {}\n".format(pm))
                if ibias_n*nf_n < best_ibias:
                    best_ibias = ibias_n*nf_n
                    best_op = dict(
                    vg=vg,
                    nf_n=nf_n,
                    nf_p=nf_p,
                    rf=rf,
                    rdc=rdc,
                    fbw=fbw,
                    pm=pm,
                    ibias=best_ibias)
                
    if best_op == None:
        raise ValueError("No solutions.")
    return best_op
    def _find_rz_cf(self,
                    gm_db,
                    load_db,
                    vtail_list,
                    vg_list,
                    vmid_list,
                    vout_list,
                    vbias_list,
                    vb_gm,
                    vb_load,
                    cload,
                    cpar1,
                    w_dict,
                    th_dict,
                    stack_dict,
                    seg_dict,
                    gm2_list,
                    res_var,
                    phase_margin,
                    cap_tol=1e-15,
                    cap_step=10e-15,
                    cap_min=1e-15,
                    cap_max=1e-9):
        """Find minimum miller cap that stabilizes the system.

        NOTE: This function assume phase of system for any miller cap value will not loop
        around 360, otherwise it may get the phase margin wrong.  This assumption should be valid
        for this op amp.
        """
        gz_worst = float(min(gm2_list))
        gz_nom = gz_worst * (1 - res_var)
        # find maximum Cf needed to stabilize all corners
        cf_min = cap_min
        for env_idx, (vtail, vg, vmid, vout, vbias) in \
                enumerate(zip(vtail_list, vg_list, vmid_list, vout_list, vbias_list)):
            cir = self._make_circuit(env_idx, gm_db, load_db, vtail, vg, vmid,
                                     vout, vbias, vb_gm, vb_load, cload, cpar1,
                                     w_dict, th_dict, stack_dict, seg_dict,
                                     gz_worst)

            bin_iter = FloatBinaryIterator(cf_min,
                                           None,
                                           cap_tol,
                                           search_step=cap_step)
            while bin_iter.has_next():
                cur_cf = bin_iter.get_next()
                cir.add_cap(cur_cf, 'outp', 'xp')
                cir.add_cap(cur_cf, 'outn', 'xn')
                num, den = cir.get_num_den('in', 'out')
                cur_pm, _ = get_stability_margins(num, den)
                if cur_pm < phase_margin:
                    if cur_cf > cap_max:
                        # no way to make amplifier stable, just return
                        return None, None, None, None, None, None
                    bin_iter.up()
                else:
                    bin_iter.save()
                    bin_iter.down()
                cir.add_cap(-cur_cf, 'outp', 'xp')
                cir.add_cap(-cur_cf, 'outn', 'xn')

            # bin_iter is guaranteed to save at least one value, so don't need to worry about
            # cf_min being None
            cf_min = bin_iter.get_last_save()

        # find gain, unity gain bandwidth, and phase margin across corners
        gain_list, f3db_list, funity_list, pm_list = [], [], [], []
        for env_idx, (vtail, vg, vmid, vout, vbias) in \
                enumerate(zip(vtail_list, vg_list, vmid_list, vout_list, vbias_list)):
            cir = self._make_circuit(env_idx, gm_db, load_db, vtail, vg, vmid,
                                     vout, vbias, vb_gm, vb_load, cload, cpar1,
                                     w_dict, th_dict, stack_dict, seg_dict,
                                     gz_nom)
            cir.add_cap(cf_min, 'outp', 'xp')
            cir.add_cap(cf_min, 'outn', 'xn')
            num, den = cir.get_num_den('in', 'out')
            pn = np.poly1d(num)
            pd = np.poly1d(den)
            gain_list.append(abs(pn(0) / pd(0)))
            f3db_list.append(get_w_3db(num, den) / 2 / np.pi)
            funity_list.append(get_w_crossings(num, den)[0] / 2 / np.pi)
            pm_list.append(get_stability_margins(num, den)[0])

        return funity_list, 1 / gz_nom, cf_min, gain_list, f3db_list, pm_list
Exemple #9
0
def design_inv_tia(specs, pch_op_info, nch_op_info, pch_scale):
    sim_env = specs['sim_env']
    pch_db = specs['pch_db']
    nch_db = specs['nch_db']
    vgs_res = specs['vgs_res']
    vdd = specs['vdd']
    rdc_targ = specs['rdc']
    f3db_targ = specs['f3db']
    pm_targ = specs['pm']
    cin = specs['cin']
    cl = specs['cl']
    scale_min = specs['scale_min']
    scale_max = specs['scale_max']
    n_scale = specs['n_scale']
    rf_min = specs['rf_min']
    rf_max = specs['rf_max']
    n_rf = specs['n_rf']

    pch_ibias = pch_op_info['ibias']
    nch_ibias = nch_op_info['ibias']

    gmp = pch_op_info['gm'] * pch_scale
    gmn = nch_op_info['gm']
    gm_tot = gmp + gmn
    gdsp = pch_op_info['gds'] * pch_scale
    gdsn = nch_op_info['gds']
    rop = 1 / gdsp
    ron = 1 / gdsn
    ro_tot = rop * ron / (rop + ron)
    gds_tot = 1 / ro_tot

    cgsp = pch_op_info['cgs'] * pch_scale
    cgsn = nch_op_info['cgs']
    cgs_tot = cgsp + cgsn
    cgbp = pch_op_info['cgb'] * pch_scale
    cgbn = nch_op_info['cgb']
    cgb_tot = cgbp + cgbn
    cgdp = pch_op_info['cgd'] * pch_scale
    cgdn = nch_op_info['cgd']
    cgd_tot = cgdp + cgdn
    cggp = pch_op_info['cgg'] * pch_scale
    cggn = nch_op_info['cgg']
    cgg_tot = cggp + cggn
    cdsp = pch_op_info['cds'] * pch_scale
    cdsn = nch_op_info['cds']
    cds_tot = cdsp + cdsn
    cdbp = pch_op_info['cdb'] * pch_scale
    cdbn = nch_op_info['cdb']
    cdb_tot = cdbp + cdbn
    cddp = pch_op_info['cdd'] * pch_scale
    cddn = nch_op_info['cdd']
    cdd_tot = cddp + cddn

    scale_vec = np.linspace(scale_min, scale_max, n_scale)
    for scale in scale_vec:
        for rf in np.linspace(rf_min, rf_max, n_rf):
            # Build circuit
            cir = LTICircuit()
            cir.add_transistor(pch_op_info,
                               'out',
                               'in',
                               'gnd',
                               'gnd',
                               fg=scale * pch_scale)
            cir.add_transistor(nch_op_info,
                               'out',
                               'in',
                               'gnd',
                               'gnd',
                               fg=scale)
            cir.add_res(rf, 'out', 'in')
            cir.add_cap(cin, 'in', 'gnd')
            cir.add_cap(cl, 'out', 'gnd')

            # Get gain/poles/zeros/Bode plot
            # Note: any in_type other than 'v' results in current source input
            tf = cir.get_transfer_function('in', 'out', in_type='i')
            rdc = np.absolute(tf.num[-1] / tf.den[-1])
            w3db = get_w_3db(tf.num, tf.den)
            f3db = w3db / (2 * np.pi)

            cin_tot = cin + cgg_tot * scale
            cl_tot = cl + cdd_tot * scale
            cgd_scaled = cgd_tot * scale
            gm_scaled = gm_tot * scale
            gds_scaled = gds_tot * scale

            cir_open_loop = LTICircuit()
            cir_open_loop.add_vccs(gm_scaled, 'out', 'gnd', 'vt', 'gnd')
            cir_open_loop.add_conductance(gds_scaled, 'out', 'gnd')
            cir_open_loop.add_cap(cl_tot, 'out', 'gnd')
            cir_open_loop.add_res(rf, 'out', 'vr')
            cir_open_loop.add_cap(cgd_scaled, 'out', 'vr')
            cir_open_loop.add_cap(cin_tot, 'vr', 'gnd')

            tf_open_loop = cir_open_loop.get_transfer_function('vt',
                                                               'vr',
                                                               in_type='v')
            pm, gain_margin = get_stability_margins(tf_open_loop.num,
                                                    tf_open_loop.den)
            pm = pm - 180

            if rdc >= rdc_targ and f3db >= f3db_targ and pm >= pm_targ:
                ibias = scale * nch_ibias
                design = dict(IB=ibias,
                              Scale=scale,
                              Rf=rf,
                              RDC=rdc,
                              f3dB=f3db,
                              PM=pm)
                return design
    def _get_stb_lti(self, op_dict, nf_dict, series_type, rload,
                     cload) -> float:
        '''
        Returns:
            pm: Phase margins (in degrees)
        '''
        ckt = LTICircuit()

        n_ser = series_type == 'n'

        # Series device
        ser_d = 'gnd' if n_ser else 'reg'
        ser_s = 'reg' if n_ser else 'gnd'
        ckt.add_transistor(op_dict['ser'],
                           ser_d,
                           'out',
                           ser_s,
                           fg=nf_dict['ser'],
                           neg_cap=False)

        # Load passives
        ckt.add_res(rload, 'reg', 'gnd')
        ckt.add_cap(rload, 'reg', 'gnd')
        # TODO include any compensation passives

        # Amplifier
        inp_conn = 'gnd' if n_ser else 'in'
        inn_conn = 'gnd' if not n_ser else 'in'
        ckt.add_transistor(op_dict['in'],
                           'outx',
                           inp_conn,
                           'tail',
                           fg=nf_dict['in'],
                           neg_cap=False)
        ckt.add_transistor(op_dict['in'],
                           'out',
                           inn_conn,
                           'tail',
                           fg=nf_dict['in'],
                           neg_cap=False)
        ckt.add_transistor(op_dict['tail'],
                           'tail',
                           'gnd',
                           'gnd',
                           fg=nf_dict['tail'],
                           neg_cap=False)
        ckt.add_transistor(op_dict['load'],
                           'outx',
                           'outx',
                           'gnd',
                           fg=nf_dict['load'],
                           neg_cap=False)
        ckt.add_transistor(op_dict['load'],
                           'out',
                           'outx',
                           'gnd',
                           fg=nf_dict['load'],
                           neg_cap=False)

        # Calculating stability margins
        num, den = ckt.get_num_den(in_name='in', out_name='reg', in_type='v')
        pm, _ = get_stability_margins(np.convolve(num, [-1]), den)

        return pm
Exemple #11
0
def design_inv_tia(specs, pch_op_info, nch_op_info, pch_scale):
    sim_env = specs['sim_env']
    pch_db = specs['pch_db']
    nch_db = specs['nch_db']
    vgs_res = specs['vgs_res']
    vdd = specs['vdd']
    isw = specs['isw']
    ber_targ = specs['ber']
    voff = specs['voff']
    snr_like_targ = specs['snr_like']
    pmos_noise_scale = specs['pmos_noise_scale']
    # rdc_targ = specs['rdc']
    noise_const = specs['noise_const']
    tper = specs['tper']
    f_factor = specs['f_factor']
    # f3db_targ = specs['f3db']
    f3db_targ = f_factor * 1 / tper
    pm_targ = specs['pm']
    cin = specs['cin']
    cl = specs['cl']
    scale_min = specs['scale_min']
    scale_max = specs['scale_max']
    n_scale = specs['n_scale']
    rf_min = specs['rf_min']
    rf_max = specs['rf_max']
    n_rf = specs['n_rf']

    pch_ibias = pch_op_info['ibias']
    nch_ibias = nch_op_info['ibias']

    gmp = pch_op_info['gm'] * pch_scale
    gmn = nch_op_info['gm']
    gm_tot = gmp + gmn
    gammap = pch_op_info['gamma']
    gamman = nch_op_info['gamma']
    gdsp = pch_op_info['gds'] * pch_scale
    gdsn = nch_op_info['gds']
    rop = 1 / gdsp
    ron = 1 / gdsn
    ro_tot = rop * ron / (rop + ron)
    gds_tot = 1 / ro_tot

    cgsp = pch_op_info['cgs'] * pch_scale
    cgsn = nch_op_info['cgs']
    cgs_tot = cgsp + cgsn
    cgbp = pch_op_info['cgb'] * pch_scale
    cgbn = nch_op_info['cgb']
    cgb_tot = cgbp + cgbn
    cgdp = pch_op_info['cgd'] * pch_scale
    cgdn = nch_op_info['cgd']
    cgd_tot = cgdp + cgdn
    cggp = pch_op_info['cgg'] * pch_scale
    cggn = nch_op_info['cgg']
    cgg_tot = cggp + cggn
    cdsp = pch_op_info['cds'] * pch_scale
    cdsn = nch_op_info['cds']
    cds_tot = cdsp + cdsn
    cdbp = pch_op_info['cdb'] * pch_scale
    cdbn = nch_op_info['cdb']
    cdb_tot = cdbp + cdbn
    cddp = pch_op_info['cdd'] * pch_scale
    cddn = nch_op_info['cdd']
    cdd_tot = cddp + cddn

    scale_vec = np.linspace(scale_min, scale_max, n_scale)
    for scale in scale_vec:
        for rf in np.linspace(rf_max, rf_min, n_rf):
            # Build circuit
            cir = LTICircuit()
            cir.add_transistor(pch_op_info,
                               'out',
                               'in',
                               'gnd',
                               'gnd',
                               fg=scale * pch_scale)
            cir.add_transistor(nch_op_info,
                               'out',
                               'in',
                               'gnd',
                               'gnd',
                               fg=scale)
            cir.add_res(rf, 'out', 'in')
            cir.add_cap(cin, 'in', 'gnd')
            cir.add_cap(cl, 'out', 'gnd')

            # Get gain/poles/zeros/Bode plot
            # Note: any in_type other than 'v' results in current source input
            tf = cir.get_transfer_function('in', 'out', in_type='i')
            rdc = np.absolute(tf.num[-1] / tf.den[-1])
            w3db = get_w_3db(tf.num, tf.den)
            f3db = w3db / (2 * np.pi)

            if f3db >= f3db_targ:
                # Noise
                in_gmp = noise_const * gammap * gmp
                in_gmn = noise_const * gamman * gmn
                in_rf = noise_const / rf

                tf_gm = cir.get_transfer_function('out', 'out', in_type='i')

                dc_gm = tf_gm.num[-1] / tf_gm.den[-1]
                wo2_gm = tf_gm.den[-1] / tf_gm.den[0]
                woQ_gm = tf_gm.den[-1] / tf_gm.den[1]
                wz_gm = tf_gm.num[-1] / tf_gm.num[0]
                von_gmp = pmos_noise_scale * in_gmp * dc_gm**2 * woQ_gm / 4 * (
                    wo2_gm / wz_gm**2 + 1)
                von_gmn = in_gmn * dc_gm**2 * woQ_gm / 4 * (wo2_gm / wz_gm**2 +
                                                            1)

                cir.add_vccs(1, 'out', 'in', 'vn', 'gnd')
                tf_rf = cir.get_transfer_function('vn', 'out', in_type='v')

                dc_rf = tf_rf.num[-1] / tf_rf.den[-1]
                wo2_rf = tf_rf.den[-1] / tf_rf.den[0]
                woQ_rf = tf_rf.den[-1] / tf_rf.den[1]
                wz_rf = tf_rf.num[-1] / tf_rf.num[0]
                von_rf = in_rf * dc_rf**2 * woQ_rf / 4 * (wo2_rf / wz_rf**2 +
                                                          1)

                von = von_gmp + von_gmn + von_rf

                # Signal
                vo = isw * rdc
                snr_like = (vo - voff) / np.sqrt(von)

                if snr_like >= snr_like_targ:
                    cin_tot = cin + cgg_tot * scale
                    cl_tot = cl + cdd_tot * scale
                    cgd_scaled = cgd_tot * scale
                    gm_scaled = gm_tot * scale
                    gds_scaled = gds_tot * scale

                    # Build open loop circuit for phase margin
                    cir_open_loop = LTICircuit()
                    cir_open_loop.add_vccs(gm_scaled, 'out', 'gnd', 'vt',
                                           'gnd')  # ***
                    cir_open_loop.add_conductance(gds_scaled, 'out', 'gnd')
                    cir_open_loop.add_cap(cl_tot, 'out', 'gnd')
                    cir_open_loop.add_res(rf, 'out', 'vr')
                    cir_open_loop.add_cap(cgd_scaled, 'out', 'vr')
                    cir_open_loop.add_cap(cin_tot, 'vr', 'gnd')

                    tf_open_loop = cir_open_loop.get_transfer_function(
                        'vt', 'vr', in_type='v')
                    pm, gm = get_stability_margins(tf_open_loop.num,
                                                   tf_open_loop.den)
                    pm = pm - 180

                    if pm >= pm_targ:
                        ibias = scale * nch_ibias
                        design = dict(IB=ibias,
                                      Scale=scale,
                                      Rf=rf,
                                      RDC=rdc,
                                      f3dB=f3db,
                                      PM=pm,
                                      Von_Rf=von_rf,
                                      Von_gmp=von_gmp,
                                      Von_gmn=von_gmn,
                                      Von=von,
                                      SNR_Like=snr_like)
                        return design
Exemple #12
0
def design_LPF_AMP(db_n, db_p, db_bias, sim_env,
    vin, vdd_nom, vdd_vec, cload,
    gain_min, fbw_min, pm_min,
    vb_n, vb_p, error_tol=0.1, ibias_max=20e-6):
    '''
    Designs an amplifier with an N-input differential pair
    with a source follower. Uses the LTICircuit functionality.
    Inputs:
        db_n/p:     Databases for non-biasing NMOS and PMOS device 
                    characterization data, respectively.
        db_bias:    Database for tail NMOS device characterization data.
        sim_env:    Simulation corner.
        vin:        Float. Input (and output and tail) bias voltage in volts.
        vtail_res:  Float. Step resolution in volts when sweeping tail voltage.
        vdd_nom:    Float. Nominal supply voltage in volts.
        vdd_vec:    Collection of floats. Elements should include the min and
                    max supply voltage in volts.
        cload:      Float. Output load capacitance in farads.
        gain_min:   Float. Minimum DC voltage gain in V/V.
        fbw_min:    Float. Minimum bandwidth (Hz).
        pm_min:     Float. Minimum phase margin in degrees.
        vb_n/p:     Float. Back-gate/body voltage (V) of NMOS and PMOS, 
                    respectively (nominal).
        error_tol:  Float. Fractional tolerance for ibias error when computing 
                    the p-to-n ratio.
        ibias_max:  Float. Maximum bias current (A) allowed with nominal vdd.
    Raises:
        ValueError: If unable to meet the specification requirements.
    Outputs:
        A dictionary with the following key:value pairings:
        nf_cs_n/pB/nB:  Integer. Number of minimum device widths for common source
                        N-input/active P-load/tail device.
        nf_sfN_n/pB/nB: Integer. Number of minimum device widths for source follower
                        N-input/P-bias/N-bias.
        nf_sfP_p/pB/nB: Integer. Number of minimum device widths for source follower
                        P-input/P-bias/N-bias.
        vtail:          Float. DC tail voltage for the common source stage.
                        This value is reused throughout the circuit.
        gain_cs:        Float. DC voltage gain of the CS amplifier (V/V).
        gain_sfN:       Float. DC voltage gain of the first source follower (V/V).
        gain_sfP:       Float. DC voltage gain of the second source follower (V/V).
        gain:           Float. DC voltage gain of both stages combined (V/V).
        fbw:            Float. Bandwdith (Hz).
        pm:             Float. Phase margin (degrees) for unity gain.
    '''
    possibilities = []
    ibias_budget = ibias_max
    
    # Given this amplifier will be unity gain feedback at DC
    vout = vin
    
    vstar_min = 0.15
    vtail_vec = np.arange(vstar_min, vout, vtail_res)
    for vtail in vtail_vec:
        print("\nVTAIL: {0}".format(vtail))
        n_op = db_n.query(vgs=vin-vtail, vds=vout-vtail, vbs=vb_n-vtail)
        p_B_op = db_p.query(vgs=vout-vdd_nom, vds=vout-vdd_nom, vbs=vb_p-vdd_nom)
        n_B_op = db_bias.query(vgs=vout-0, vds=vtail-0, vbs=vb_n-0)
        p_op = db_p.query(vgs=vtail-vout, vds=vtail-vout, vbs=vb_p-vout)
    
        # Finding the ratio between devices to converge to correct biasing
        idn_base = n_op['ibias']
        idp_B_base = p_B_op['ibias']
        idn_B_base = n_B_op['ibias']
        idp_base = p_op['ibias']
                
        pB_to_n = abs(idn_base/idp_B_base)
        nB_to_n = abs(idn_base/idn_B_base)
        pB_to_p = abs(idp_base/idp_B_base)
        nB_to_p = abs(idp_base/idn_B_base)
        
        ### P-input SF ###
        sfP_mult_max = int(round(abs(ibias_budget/idn_base)))
        sfP_mult_vec = np.arange(1, sfP_mult_max, 1)
        for sfP_mult in sfP_mult_vec in 
            nf_sfP_p = sfP_mult
            
            # Verify that the sizing is feasible and gets sufficiently
            # good current matching
            pB_sfP_good, nf_sfP_pB = verify_ratio(idp_base, idp_B_base, pB_to_p, 
                                        nf_sfP_p, error_tol)
            nB_sfP_good, nf_sfP_nB = verify_ratio(idp_base, idn_B_base, nB_to_p,
                                        nf_sfP_p, error_tol)
            if not (pB_sfP_good and nB_sfP_good):
                continue
            
            # Check SF2 bandwidth; size up until it meets
            ckt = construct_sfP_LTI(p_op, p_B_op, n_B_op, 
                    nf_sfP_p, nf_sfP_pB, nf_sfP_nB,
                    cload)
            sfP_num, sfP_den = ckt.get_num_den(in_name='out2', out_name='out', in_type='v')
            fbw_sfP = get_w_3db(sfP_num, sfP_den)/(2*np.pi)
            
            # No need to keep sizing up
            if fbw_sfP >= fbw_min:
                break
        
        # Final stage can never meet bandwidth given biasing
        if fbw_sfP < fbw_min:
            print("SFP: BW failure\n")
            continue
        
        # Sizing ratio failure
        if 0 in [nf_sfP_pB, nf_sfP_nB]:
            print("SFP: sizing ratio failure\n")
            continue
        
        ibias_sfP = idp_base * nf_sfP_p
        ibias_budget = ibias_budget - ibias_sfP
        print("Remaining budget\t:\t {0}".format(ibias_budget))
        
        ### N-input SF ###
        sfN_mult_max = int(round(abs(ibias_budget/idn_base)))
        sfN_mult_vec = np.arange(1, sfN_mult_max, 1)
        for sfN_mult in sfN_mult_vec:
            nf_sfN_n = sfN_mult
            
            # Repeat the same feasibility + current matching check
            pB_sfN_good, nf_sfN_pB = verify_ratio(idn_base, idp_B_base, 
                                            pB_to_n, nf_sfN_n, error_tol)
            nB_sfN_good, nf_sfN_pN = verify_ratio(idn_base, idn_B_base,
                                            nB_to_n, nf_sfN_n, error_tol)
            if not (pB_sfn_good and nB_sfN_good):
                continue
            
            # Check SF1 bandwidth; size up until it meets
            ckt = construct_sfN_LTI(p_op, p_B_op, n_B_op, 
                    nf_sfP_p, nf_sfP_pB, nf_sfP_nB,
                    cload,
                    n_op,
                    nf_sfN_n, nf_sfN_pB, nf_sfN_nB)
            sfN_num, sfN_den = ckt.get_num_den(in_name='out1', out_name='out', in_type='v')
            fbw_sfN = get_w_3db(sfN_num, sfN_den)/(2*np.pi)
            
            # No need to keep sizing up
            if fbw_sfN >= fbw_min:
                break
            
        # Second stage can never meet bandwidth given restrictions
        if fbw_sfN < fbw_min:
            print("SFN: BW failure\n")
            continue
        
        # Sizing ratio failure
        if 0 in [nf_sfN_pB, nf_sfN_nB]:
            print("SFN: sizing ratio failure\n")
            continue
        
        ibias_sfN = idn_base * nf_sfN_n
        ibias_budget = ibias_budget - ibias_sfN
        print("Remaining budget\t:\t {0}".format(ibias_budget))
        
        ### CS input ###
        cs_mult_max = int(round(abs(ibias_budget/idn_base)))
        cs_mult_vec = np.arange(1, cs_mult_max, 1)
        for cs_mult in cs_mult_vec:
            nf_cs_n = cs_mult_vec
            
            # Verify that the sizing is feasible and gets sufficiently
            # good current matching
            pB_cs_good, nf_cs_pB = verify_ratio(idn_base, idp_B_base, pB_to_n, nf_cs_n, 
                                        error_tol)
            nB_cs_good, nf_cs_nB = verify_ratio(idn_base, idn_B_base, nB_to_n, nf_cs_n*2,
                                        error_tol)
            if not (pB_cs_good and nB_cs_good):
                continue 
            
            # Check combined stages' small signal
            ckt = construct_total_LTI(p_op, p_B_op, n_B_op, 
                    nf_sfP_p, nf_sfP_pB, nf_sfP_nB,
                    cload,
                    n_op,
                    nf_sfN_n, nf_sfN_pB, nf_sfN_nB,
                    nf_cs_n, nf_cs_pB, nf_cs_nB)
            total_num, total_den = ckt.get_num_den(in_name='in', out_name='out', in_type='v')
            
            # Check cumulative gain
            gain_total = abs(total_num[-1]/total_den[-1])
            if gain_total < gain_min:
                print("CS: A0 failure {0}\n".format(gain_total))
                break # Biasing sets the gain
            
            # Check cumulative bandwidth
            fbw_total = get_w_3db(total_num, total_den)/(2*np.pi)
            if fbw_total < fbw_min:
                print("CS: BW failure {0}\n".format(fbw_total))
                continue
                
            # Check phase margin (TODO?)
            loopBreak = ckt.get_transfer_function(in_name='out', out_name='in', in_type='v')
            pm, gainm = get_stability_margins(loopBreak.num, loopBreak.den)
            if pm < pm_min or isnan(pm):
                print("CS: PM failure {0}\n".format(pm))
                continue
                
            # If gain, bw, and PM are met, no need to keep sizing up
            break

        # Sizing ratio failure
        if 0 in [nf_cs_pB, nf_cs_nB]:
            print("CS: sizing ratio failure\n")
            continue
        
        # Iterated, biasing condition + constraints can't meet spec
        if fbw_total < fbw_min or gain_total < gain_min or pm < pm_min:
            continue
        else:
            print("HALLELUJAH SUCCESS\n")
            cs_num, cs_den = ckt.get_num_den(in_name='in', out_name='out1', in_type='v')
            sfN_num, sfN_den = ckt.get_num_den(in_name='out1', out_name='out2', in_type='v')
            sfP_num, sfP_den = ckt.get_num_den(in_name='out2', out_name='out', in_type='v')
            
            gain_cs = abs(cs_num[-1]/cs_den[-1])
            gain_sfN = abs(sfN_num[-1]/sfN_den[-1])
            gain_sfP = abs(sfP_num[-1]/sfP_den[-1])
            viable = dict(
                        nf_cs_n     = nf_cs_n,
                        nf_cs_pB    = nf_cs_pB,
                        nf_cs_nB    = nf_cs_nB,
                        nf_sfN_n    = nf_sfN_n,
                        nf_sfN_pB   = nf_sfN_pB,
                        nf_sfN_nB   = nf_sfN_nB,
                        nf_sfP_p    = nf_sfP_p,
                        nf_sfP_pB   = nf_sfP_pB,
                        nf_sfP_nB   = nf_sfP_nB,
                        vtail       = vtail,
                        gain_cs     = gain_cs,
                        gain_sfN    = gain_sfN,
                        gain_sfP    = gain_sfP,
                        gain        = gain_total,
                        fbw         = fbw_total,
                        pm          = pm)
            pprint.pprint(viable)
            print("\n")
            possibilities.append([viable])
        
    # TODO: Check all other VDDs
    return possibilities
def verify_TIA_inverter_SS(n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload,
                           rdc_min, fbw_min, pm_min):
    """
    Inputs:
        n/p_op_info:    The MOSDBDiscrete library for the NMOS and PMOS
                        devices in the bias point for verification.
        nf_n/p:         Integer. Number of channel fingers for the NMOS/PMOS.
        rf:             Float. Value of the feedback resistor in ohms.
        cpd:            Float. Input capacitance not from the TIA in farads.
        cload:          Float. Output capacitance not from the TIA in farads.
        rdc_min:        Float. Minimum DC transimpedance in ohms.
        fbw_min:        Float. Minimum bandwidth (Hz).
        pm_min:         Float. Minimum phase margin in degrees.
    Outputs:
        Returns two values
        The first is True if the spec is met, False otherwise.
        The second is a dictionary of values for rdc (DC transimpedance, V/I), 
        bw (bandwidth, Hz), and pm (phase margin, deg) if computed. None otherwise.
    """
    # Getting relevant small-signal parameters
    gds_n = n_op_info['gds'] * nf_n
    gds_p = p_op_info['gds'] * nf_p
    gds = gds_n + gds_p

    gm_n = n_op_info['gm'] * nf_n
    gm_p = p_op_info['gm'] * nf_p
    gm = gm_n + gm_p

    cgs_n = n_op_info['cgs'] * nf_n
    cgs_p = p_op_info['cgs'] * nf_p
    cgs = cgs_n + cgs_p

    cds_n = n_op_info['cds'] * nf_n
    cds_p = p_op_info['cds'] * nf_p
    cds = cds_n + cds_p

    cgd_n = n_op_info['cgd'] * nf_n
    cgd_p = p_op_info['cgd'] * nf_p
    cgd = cgd_n + cgd_p

    # Circuit for GBW
    circuit = LTICircuit()
    circuit.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n)
    circuit.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p)
    circuit.add_res(rf, 'in', 'out')
    circuit.add_cap(cpd, 'in', 'gnd')
    circuit.add_cap(cload, 'out', 'gnd')

    # Check gain
    num, den = circuit.get_num_den(in_name='in', out_name='out', in_type='i')
    rdc = num[-1] / den[-1]

    if abs(round(rdc)) < round(rdc_min):
        print("GAIN:\t{0} (FAIL)".format(rdc))
        return False, dict(rdc=rdc, fbw=None, pm=None)

    # Check bandwidth
    fbw = get_w_3db(num, den) / (2 * np.pi)
    if fbw < fbw_min or np.isnan(fbw):
        print("BW:\t{0} (FAIL)".format(fbw))
        return False, dict(rdc=rdc, fbw=fbw, pm=None)

    # Check phase margin by constructing an LTICircuit first
    circuit2 = LTICircuit()
    """circuit2.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n)
    circuit2.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p)
    circuit2.add_cap(cpd, 'in', 'gnd')
    circuit2.add_cap(cload, 'out', 'gnd')
    circuit2.add_res(rf, 'in', 'break')
    # Cancel Cgd to correctly break loop
    circuit2.add_cap(-cgd, 'in' , 'out')
    circuit.add_cap(cgd, 'in', 'break')"""

    circuit2.add_conductance(gds, 'out', 'gnd')
    circuit2.add_cap(cgs + cpd, 'in', 'gnd')
    circuit2.add_cap(cds + cload, 'out', 'gnd')
    circuit2.add_cap(cgd, 'in', 'out')
    circuit2.add_res(rf, 'in', 'out')

    loopBreak = circuit2.get_transfer_function(in_name='in',
                                               out_name='out',
                                               in_type='i')
    pm, gainm = get_stability_margins(loopBreak.num * gm, loopBreak.den)
    if pm < pm_min or np.isnan(pm):
        print("PM:\t{0} (FAIL)\n".format(pm))
        return False, dict(rdc=rdc, fbw=fbw, pm=pm)
    print("SUCCESS\n")
    return True, dict(rdc=rdc, fbw=fbw, pm=pm)
def design_diffAmpP(db_n,
                    db_p,
                    sim_env,
                    vdd,
                    cload,
                    vincm,
                    T,
                    gain_min,
                    fbw_min,
                    pm_min,
                    inoiseout_std_max,
                    vb_p,
                    vb_n,
                    error_tol=0.05):
    """
    Inputs:
    Returns:
    """
    # Constants
    kBT = 1.38e-23 * T

    # TODO: change hardcoded value
    vstar_min = 0.15

    # find the best operating point
    best_ibias = float('inf')
    best_op = None

    # (loosely) constrain tail voltage
    # TODO: replace with binary search
    vtail_min = vincm + 2 * vstar_min
    vtail_max = vdd
    vtail_vec = np.arange(vtail_min, vtail_max, 10e-3)

    # sweep tail voltage
    for vtail in vtail_vec:
        # (loosely) constrain output DC voltage
        # TODO: replace with binary search
        vout_min = vstar_min
        vout_max = vtail - vstar_min
        vout_vec = np.arange(vout_min, vout_max, 10e-3)
        # sweep output DC voltage
        for vout in vout_vec:
            in_op = db_p.query(vgs=vincm - vtail,
                               vds=vout - vtail,
                               vbs=vb_p - vtail)
            load_op = db_n.query(vgs=vout, vds=vout, vbs=vb_n - 0)
            # TODO: constrain number of input devices
            nf_in_min = 4
            nf_in_max = 30
            nf_in_vec = np.arange(nf_in_min, nf_in_max, 2)
            for nf_in in nf_in_vec:
                ibranch = abs(in_op['ibias'] * nf_in)
                if ibranch * 2 > best_ibias:
                    continue
                # matching NMOS and PMOS bias current
                nf_load = int(abs(round(ibranch / load_op['ibias'])))
                if nf_load < 1:
                    continue
                iload = load_op['ibias'] * nf_load
                ibranch_error = (abs(iload) - abs(ibranch)) / abs(ibranch)
                if ibranch_error > error_tol:
                    continue

                # create LTICircuit
                amp = LTICircuit()
                amp.add_transistor(in_op, 'out', 'in', 'gnd', 'gnd', fg=nf_in)
                amp.add_transistor(load_op,
                                   'out',
                                   'gnd',
                                   'gnd',
                                   'gnd',
                                   fg=nf_load)
                amp.add_cap(cload, 'out', 'gnd')
                num, den = amp.get_num_den(in_name='in',
                                           out_name='out',
                                           in_type='v')

                gm = in_op['gm'] * nf_in
                ro = 1 / (in_op['gds'] * nf_in + load_op['gds'] * nf_load)

                # Check against gain
                gain = abs(num[-1] / den[-1])
                if gain < gain_min:
                    print("GAIN: {0:.2f} (FAIL)\n".format(gain))
                    continue
                print("GAIN: {0:.2f}".format(gain))

                # Check against bandwidth
                wbw = get_w_3db(num, den)
                if wbw == None:
                    print("BW: None (FAIL)\n")
                    continue
                fbw = wbw / (2 * np.pi)
                if fbw < fbw_min:
                    print("BW: {0:.2f} (FAIL)\n".format(fbw))
                    continue
                print("BW: {0:.2f}".format(fbw))
                pm, gainm = get_stability_margins(num, den)

                # Check against phase margin
                if pm < pm_min or isnan(pm):
                    print("PM: {0:.2f} (FAIL)\n".format(pm))
                    continue
                print("PM: {0:.2f}".format(pm))

                # Check against noise
                inoiseout_std = np.sqrt(
                    4 * kBT * (in_op['gamma'] * in_op['gm'] * nf_in * 2 +
                               load_op['gamma'] * load_op['gm'] * nf_load * 2))
                if inoiseout_std > inoiseout_std_max:
                    print("INOISE STD: {} (FAIL)\n".format(inoiseout_std))
                    continue
                print("INOISE STD: {}".format(inoiseout_std))

                # Check against best bias current
                if ibranch * 2 < best_ibias:
                    biasing_spec = dict(
                        db_n=db_n,
                        db_p=db_p,
                        sim_env=sim_env,
                        vdd=vdd,
                        vout=vout,
                        vincm=vincm,
                        nf_in=nf_in,
                        nf_load=nf_load,
                        vtail_target=vtail,
                        itail_target=ibranch * 2,
                        iref_res=10e-9,
                        vb_p=vb_p,
                        vb_n=vb_n,
                        error_tol=error_tol,
                    )
                    biasing_params = design_biasing(**biasing_spec)
                    if biasing_params == None:
                        print("BIASING PARAMS (FAIL)\n")
                        continue
                    print("(SUCCESS)\n")
                    best_ibias = ibranch * 2
                    best_op = dict(
                        itail=best_ibias,
                        nf_in=nf_in,
                        nf_load=nf_load,
                        vout=vout,
                        vtail=vtail,
                        gain=gain,
                        fbw=fbw,
                        pm=pm,
                        nf_tail=biasing_params['nf_tail'],
                        nf_bias_tail=biasing_params['nf_bias_tail'],
                        nf_bias_in=biasing_params['nf_bias_in'],
                        nf_bias_loadM=biasing_params['nf_bias_loadM'],
                        nf_bias_load1=biasing_params['nf_bias_load1'],
                        iref=biasing_params['iref'],
                        inoiseout_std=inoiseout_std)
                    break
    if best_op == None:
        raise ValueError("No solution for P-in diffamp")
    return best_op