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()
    def _get_psrr_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource) -> float:
        '''
        Returns:
            psrr: PSRR (dB)
            fbw: Power supply -> output 3dB bandwidth (Hz)
        '''
        n_ser = ser_type == 'n'
        n_amp = amp_in == 'n'

        # Supply -> output gain
        ckt_sup = LTICircuit()
        ser_d = 'vdd' if n_ser else 'reg'
        ser_s = 'reg' if n_ser else 'vdd'
        inp_conn = 'gnd' if n_ser else 'reg'
        inn_conn = 'reg' if n_ser else 'gnd'
        tail_rail = 'gnd' if n_amp else 'vdd'
        load_rail = 'vdd' if n_amp else 'gnd'

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

        # Series device
        ckt_sup.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False)

        # Amplifier
        ckt_sup.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False)
        ckt_sup.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False)
        ckt_sup.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False)
        ckt_sup.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False)
        ckt_sup.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False)

        if rsource == 0:
            num_sup, den_sup = ckt_sup.get_num_den(in_name='vdd', out_name='reg', in_type='v')
        else:
            num_sup, den_sup = ckt_sup.get_num_den(in_name='vbat', out_name='reg', in_type='v')
        gain_sup = num_sup[-1]/den_sup[-1]
        wbw_sup = get_w_3db(den_sup, num_sup)

        if gain_sup == 0:
            return float('inf')
        if wbw_sup == None:
            wbw_sup = 0
        fbw_sup = wbw_sup / (2*np.pi)

        psrr = 10*np.log10((1/gain_sup)**2)

        return psrr, fbw_sup
Exemple #3
0
def tf_vs_cfb(op_in, op_load, op_tail, cload, fg=2):
    fvec = np.logspace(6, 11, 1000)
    cvec = np.logspace(np.log10(5e-16), np.log10(5e-14), 5).tolist()

    scale_load = op_in['ibias'] / op_load['ibias'] * fg

    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)
    cir.add_transistor(op_tail, 'out', 'gnd', 'gnd', 'gnd', fg=fg)
    cir.add_cap(cload, 'out', 'gnd')

    gfb = op_load['gm'] * scale_load
    cir.add_conductance(gfb, 'mid', 'x')

    print('fg_in = %d, fg_load=%.3g, rfb = %.4g' % (fg, scale_load, 1 / gfb))

    tf_list, lbl_list = [], []
    for cval in cvec:
        cir.add_cap(cval, 'x', 'out')
        tf_list.append(cir.get_num_den('in', 'out'))
        cir.add_cap(-cval, 'x', 'out')
        lbl_list.append('$C_{f} = %.3g$f' % (cval * 1e15))

    plot_tf(fvec, tf_list, lbl_list)
Exemple #4
0
    def _get_ss_lti(self, op_dict: Mapping[str, Any],
                    nf_dict: Mapping[str, int], cload: float):
        '''
        Inputs:
            op_dict: Dictionary of queried database operating points.
            nf_dict: Dictionary of number of fingers for devices.
            cload: Load capacitance in farads.
        Return:
            gain: Gain from the input to output in V/V
            fbw: 3dB bandwidth in Hz
        '''
        in_d = 'd' if nf_dict['opp'] > 0 else 'gnd'

        ckt = LTICircuit()
        ckt.add_cap(cload, 'out', 'gnd')
        ckt.add_transistor(op_dict['in'],
                           in_d,
                           'in',
                           'out',
                           fg=nf_dict['in'],
                           neg_cap=False)
        ckt.add_transistor(op_dict['same'],
                           'out',
                           'gnd',
                           'gnd',
                           fg=nf_dict['same'],
                           neg_cap=False)
        if nf_dict['opp'] > 0:
            ckt.add_transistor(op_dict['opp'],
                               'd',
                               'gnd',
                               'gnd',
                               fg=nf_dict['opp'],
                               neg_cap=False)

        num, den = ckt.get_num_den(in_name='in', out_name='out', in_type='v')
        gain = num[-1] / den[-1]
        wbw = get_w_3db(num, den)
        if wbw == None:
            wbw = 0
        fbw = wbw / (2 * np.pi)

        return gain, fbw
    def _get_loadreg_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource, vout, iout) -> float:
        '''
        Returns:
            loadreg: Load regulation for peak-to-peak load current variation of 20% (V/V)
        '''
        n_ser = ser_type == 'n'
        n_amp = amp_in == 'n'
        vdd = 'vdd' if rsource != 0 else 'gnd'

        # Supply -> output gain
        ckt = LTICircuit()
        ser_d = vdd if n_ser else 'reg'
        ser_s = 'reg' if n_ser else vdd
        inp_conn = 'gnd' if n_ser else 'reg'
        inn_conn = 'reg' if n_ser else 'gnd'
        tail_rail = 'gnd' if n_amp else vdd
        load_rail = vdd if n_amp else 'gnd'

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

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

        # Amplifier
        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)


        num, den = ckt.get_num_den(in_name='reg', out_name='reg', in_type='i')
        transimpedance = num[-1]/den[-1]

        loadreg = transimpedance*0.2*iout/vout

        return loadreg
    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
    def _get_loopgain_lti(self, op_dict, nf_dict, ser_type, amp_in, rsource) -> float:
        '''
        Returns:
            A: DC loop gain
        '''
        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=True)

        # Passives
        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')
        A = num[-1]/den[-1]

        return A
Exemple #8
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
Exemple #9
0
def design_preamp_chain(db_n,
                        db_p,
                        lch,
                        w_finger,
                        vdd,
                        cload,
                        vin_signal,
                        vin_shift,
                        vtail_res,
                        gain_min,
                        fbw_min,
                        vb_p,
                        vb_n,
                        itail_max=10e-6,
                        error_tol=0.01):
    """
    Designs a preamplifier to go between the comparator and TIA.
    N-input differential pair with an active PMOS load.
    Inputs:
        db_n/p:     Database with NMOS/PMOS info
        lch:        Float. Channel length (m)
        w_finger:   Float. Width of a single finger (m)
        vdd:        Float. Supply voltage (V)
        cload:      Float. Load capacitance (F)
        vin_signal: Float. Input bias voltage (V).
        vin_shift:  Float. Gate bias voltage for the non-signal-facing input
                    device (V).
        vtail_res:  Float. Resolution for tail voltage of amplifying
                    and offset devices for sweep (V).
        gain_min:   Float. Minimum gain (V/V)
        fbw_min:    Float. Minimum bandwidth (Hz)
        vb_n/p:     Float. Body/back-gate voltage (V) of NMOS and PMOS devices.
        itail_max:  Float. Max allowable tail current of the main amplifier (A).
        error_tol:  Float. Fractional tolerance for ibias error when computing
                    device ratios.
    Raises:
        ValueError if there is no solution given the requirements.
    Outputs:
        nf_in:          Integer. Number of fingers for input devices.
        nf_tail:        Integer. Number of fingers for tail device.
        nf_load:        Integer. Number of fingers for active load devices.
        itail:          Float. Tail current of main amp (A).
        gain:           Float. Amp gain (V/V).
        fbw:            Float. Bandwidth (Hz).
        cin:            Float. Approximation of input cap (F).
        vtail:          Float. Tail voltage (V).
    """
    vincm = (vin_signal + vin_shift) / 2
    voutcm = vincm
    best_op = None
    best_itail = np.inf

    vthn = estimate_vth(db_n, vdd, 0)

    vtail_min = 0.15  # TODO: Avoid hardcoding?
    vtail_max = vincm
    vtail_vec = np.arange(vtail_min, vtail_max, vtail_res)
    for vtail in vtail_vec:
        print('VTAIL:\t{0}'.format(vtail))
        op_tail = db_n.query(vgs=voutcm, vds=vtail, vbs=vb_n)
        op_in = db_n.query(vgs=vincm - vtail,
                           vds=voutcm - vtail,
                           vbs=vb_n - vtail)
        op_load = db_p.query(vgs=voutcm - vdd,
                             vds=voutcm - vdd,
                             vbs=vb_p - vdd)

        nf_in_min = 2
        nf_in_max = min(100, int(round(.5 * itail_max / op_in['ibias'])))
        nf_in_vec = np.arange(nf_in_min, nf_in_max, 2)
        for nf_in in nf_in_vec:

            # Check if those bias points are feasible given the
            # device sizing quantization
            load_ratio_good, nf_load = verify_ratio(op_in['ibias'],
                                                    op_load['ibias'], nf_in,
                                                    error_tol)
            if not load_ratio_good:
                continue

            tail_ratio_good, nf_tail_half = verify_ratio(
                op_in['ibias'], op_tail['ibias'], nf_in, error_tol)
            nf_tail = nf_tail_half * 2

            if not tail_ratio_good:
                continue

            # Check if it's burning more power than previous solutions
            itail = op_tail['ibias'] * nf_tail
            if itail > best_itail:
                break

            # Check the half circuit for small signal parameters
            half_circuit = LTICircuit()
            half_circuit.add_transistor(op_in, 'out', 'in', 'tail', fg=nf_in)
            half_circuit.add_transistor(op_load,
                                        'out',
                                        'gnd',
                                        'gnd',
                                        fg=nf_load)
            half_circuit.add_transistor(op_tail,
                                        'tail',
                                        'gnd',
                                        'gnd',
                                        fg=nf_tail_half)
            half_circuit.add_cap(cload, 'out', 'gnd')
            num, den = half_circuit.get_num_den(in_name='in',
                                                out_name='out',
                                                in_type='v')
            gain = abs(num[-1] / den[-1])
            if gain < gain_min:
                print("(FAIL) GAIN:\t{0}".format(gain))
                break

            wbw = get_w_3db(num, den)
            if wbw == None:
                wbw = 0
            fbw = wbw / (2 * np.pi)
            if fbw < fbw_min:
                print("(FAIL) BW:\t{0}".format(fbw))
                continue

            cin = nf_in * (op_in['cgs'] + op_in['cgd'] * (1 + gain))

            best_itail = itail
            best_op = dict(nf_in=nf_in,
                           nf_tail=nf_tail,
                           nf_load=nf_load,
                           itail=itail,
                           gain=gain,
                           fbw=fbw,
                           vtail=vtail,
                           cin=cin)

    if best_op == None:
        raise ValueError("No viable solutions.")
    return best_op
Exemple #10
0
def design_preamp(db_n,
                  db_p,
                  lch,
                  w_finger,
                  vdd,
                  cload,
                  vin_signal,
                  vin_shift,
                  voutcm_res,
                  vtail_res,
                  gain_min,
                  fbw_min,
                  vb_p,
                  vb_n,
                  itail_max=10e-6,
                  error_tol=.01):
    """
    Designs a preamplifier to go between the comparator and TIA.
    N-input differential pair with an active PMOS load.
    Inputs:
        db_n/p:     Database with NMOS/PMOS info
        lch:        Float. Channel length (m)
        w_finger:   Float. Width of a single finger (m)
        vdd:        Float. Supply voltage (V)
        cload:      Float. Load capacitance (F)
        vin_signal: Float. Input bias voltage (V).
        vin_shift:  Float. Gate bias voltage for the non-signal-facing input
                    device (V).
        voutcm_res: Float. Resolution for output common mode voltage sweep
                    (V).
        vtail_res:  Float. Resolution for tail voltage of amplifying
                    and offset devices for sweep (V).
        gain_min:   Float. Minimum gain (V/V)
        fbw_min:    Float. Minimum bandwidth (Hz)
        vb_n/p:     Float. Body/back-gate voltage (V) of NMOS and PMOS devices.
        itail_max:  Float. Max allowable tail current of the main amplifier (A).
        error_tol:  Float. Fractional tolerance for ibias error when computing
                    device ratios.
    Raises:
        ValueError if there is no solution given the requirements.
    Outputs:
        nf_in:          Integer. Number of fingers for input devices.
        nf_tail:        Integer. Number of fingers for tail device.
        nf_load:        Integer. Number of fingers for active load devices.
        itail:          Float. Tail current of main amp (A).
        gain:           Float. Amp gain (V/V).
        fbw:            Float. Bandwidth (Hz).
        voutcm:         Float. Output common mode voltage (V).
        cin:            Float. Approximation of input cap (F).
    """
    vthn = estimate_vth(db_n, vdd, 0)
    vthp = estimate_vth(db_p, vdd, 0, mos_type="pmos")
    vincm = (vin_signal + vin_shift) / 2

    vstar_min = .15  # TODO: Avoid hardcoding?

    vtail_min = vstar_min
    vtail_max = vin_signal - vstar_min
    vtail_vec = np.arange(vtail_min, vtail_max, vtail_res)

    best_op = None
    best_itail = np.inf

    for vtail in vtail_vec:
        voutcm_min = max(vin_signal - vthn, vin_shift - vthn, vtail)
        voutcm_max = vdd - vstar_min
        voutcm_vec = np.arange(voutcm_min, voutcm_max, voutcm_res)

        for voutcm in voutcm_vec:
            op_tail = db_n.query(vgs=voutcm, vds=vtail, vbs=vb_n)
            # The "mean" of the input and load devices
            op_in = db_n.query(vgs=vincm - vtail,
                               vds=voutcm - vtail,
                               vbs=vb_n - vtail)
            op_load = db_p.query(vgs=vtail - vdd,
                                 vds=voutcm - vdd,
                                 vbs=vb_p - vdd)

            # Calculate the max tail size based on current limit
            # and size input devices accordingly
            nf_in_min = 2
            nf_in_max = int(round(itail_max / 2 / op_in['ibias']))
            nf_in_vec = np.arange(nf_in_min, nf_in_max, 1)
            for nf_in in nf_in_vec:
                # Matching device ratios to sink the same amount of current
                # given the bias voltages
                tail_ratio_good, nf_tail = verify_ratio(
                    op_in['ibias'] / 2, op_tail['ibias'], nf_in, error_tol)
                if not tail_ratio_good:
                    continue

                itail = op_tail['ibias'] * nf_tail

                load_ratio_good, nf_load = verify_ratio(
                    op_in['ibias'], op_load['ibias'], nf_in, error_tol)

                if not load_ratio_good:
                    continue

                # Check small signal parameters with symmetric circuit
                ckt_sym = LTICircuit()
                ckt_sym.add_transistor(op_in, 'outp', 'gnd', 'tail', fg=nf_in)
                ckt_sym.add_transistor(op_in, 'outn', 'inp', 'tail', fg=nf_in)
                ckt_sym.add_transistor(op_tail,
                                       'tail',
                                       'gnd',
                                       'gnd',
                                       fg=nf_tail)
                ckt_sym.add_transistor(op_load,
                                       'outp',
                                       'gnd',
                                       'gnd',
                                       fg=nf_load)
                ckt_sym.add_transistor(op_load,
                                       'outn',
                                       'gnd',
                                       'gnd',
                                       fg=nf_load)
                ckt_sym.add_cap(cload, 'outp', 'gnd')
                ckt_sym.add_cap(cload, 'outn', 'gnd')
                num, den = ckt_sym.get_num_den(in_name='inp',
                                               out_name='outn',
                                               in_type='v')
                num_unintent, den_unintent = ckt_sym.get_num_den(
                    in_name='inp', out_name='outp', in_type='v')
                gain_intentional = num[-1] / den[-1]
                gain_unintentional = num_unintent[-1] / den_unintent[-1]
                gain = abs(gain_intentional - gain_unintentional)
                wbw = get_w_3db(num, den)
                if wbw == None:
                    wbw = 0
                fbw = wbw / (2 * np.pi)
                if fbw < fbw_min:
                    # print("(FAIL) BW:\t{0}".format(fbw))
                    continue
                if gain < gain_min:
                    # print("(FAIL) GAIN:\t{0}".format(gain))
                    break
                print("(SUCCESS1)")

                if itail > best_itail:
                    break

                ##############################
                if False:
                    # Check once again with asymmetric circuit
                    vin_diff = vin_signal - vin_shift
                    voutn = voutcm - gain * vin_diff / 2
                    voutp = voutcm + gain * vin_diff / 2
                    op_signal = db_n.query(vgs=vin_signal - vtail,
                                           vds=voutn - vtail,
                                           vbs=vb_n - vtail)
                    op_shift = db_n.query(vgs=vin_shift - vtail,
                                          vds=voutp - vtail,
                                          vbs=vb_n - vtail)
                    op_loadsignal = db_p.query(vgs=vtail - vdd,
                                               vds=voutn - vdd,
                                               vbs=vb_p - vdd)
                    op_loadshift = db_p.query(vgs=vtail - vdd,
                                              vds=voutp - vdd,
                                              vbs=vb_p - vdd)
                    ckt = LTICircuit()
                    ckt.add_transistor(op_shift,
                                       'outp',
                                       'gnd',
                                       'tail',
                                       fg=nf_in)
                    ckt.add_transistor(op_signal,
                                       'outn',
                                       'inp',
                                       'tail',
                                       fg=nf_in)
                    ckt.add_transistor(op_tail,
                                       'tail',
                                       'gnd',
                                       'gnd',
                                       fg=nf_tail)
                    ckt.add_transistor(op_loadsignal, 'outn', 'gnd', 'gnd',
                                       nf_load)
                    ckt.add_transistor(op_loadshift, 'outp', 'gnd', 'gnd',
                                       nf_load)
                    ckt.add_cap(cload, 'outn', 'gnd')
                    ckt.add_cap(cload, 'outp', 'gnd')
                    num, den = ckt.get_num_den(in_name='inp',
                                               out_name='outn',
                                               in_type='v')
                    num_unintent, den_unintent = ckt.get_num_den(
                        in_name='inp', out_name='outp', in_type='v')
                    gain_intentional = num[-1] / den[-1]
                    gain_unintentional = num_unintent[-1] / den_unintent[-1]
                    gain = abs(gain_intentional - gain_unintentional)

                    wbw = get_w_3db(num, den)
                    if wbw == None:
                        wbw = 0
                    fbw = wbw / (2 * np.pi)
                    if fbw < fbw_min:
                        print("(FAIL) BW:\t{0}".format(fbw))
                        continue
                    if gain < gain_min:
                        print("(FAIL) GAIN:\t{0}".format(gain))
                        break
                    print("(SUCCESS2)")
                ################################
                op_signal = op_in

                if itail < best_itail:
                    best_itail = itail

                    best_op = dict(nf_in=nf_in,
                                   nf_tail=nf_tail,
                                   nf_load=nf_load,
                                   itail=itail,
                                   gain=gain,
                                   fbw=fbw,
                                   voutcm=voutcm,
                                   vtail=vtail,
                                   cin=op_signal['cgg'] * nf_in)
    if best_op == None:
        raise ValueError("No viable solutions.")
    return best_op
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_TIA_inverter(db_n,
                        db_p,
                        sim_env,
                        vg_res,
                        rf_res,
                        vdd_nom,
                        vdd_vec,
                        cpd,
                        cload,
                        rdc_min,
                        fbw_min,
                        pm_min,
                        BER_max,
                        vos,
                        isw_pkpk,
                        vb_n,
                        vb_p,
                        error_tol=0.05,
                        ibias_max=20e-6):
    """
    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 in volts when sweeping gate voltage.
        rf_res:     Float. Step resolution in ohms when sweeping feedback 
                    resistance.
        vdd_nom:    Float. Nominal supply voltage in volts.
        vdd_vec:    Collection of floats. Elements should include the min and
                    max supply voltage in volts.
        cpd:        Float. Input parasitic capacitance in farads.
        cload:      Float. Output load capacitance in farads.
        rdc_min:    Float. Minimum DC transimpedance in ohms.
        fbw_min:    Float. Minimum bandwidth (Hz).
        pm_min:     Float. Minimum phase margin in degrees.
        BER_max:    Float. Maximum allowable bit error rate (as a fraction).
        vos:        Float. Input-referred DC offset for any subsequent
                    comparator stage as seen at the output
                    of the TIA.
        isw_pkpk:   Float. Input current peak-to-peak swing in amperes.
        vb_n/p:     Float. Back-gate/body voltage (V) of NMOS and PMOS, 
                    respectively.
        error_tol:  Float. Fractional tolerance for ibias error when computing 
                    the p-to-n ratio.
        ibias_max:  Float. Maximum bias current (A) allowed.
    Raises:
        ValueError: If unable to meet the specification requirements.
    Outputs:
        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.
    """
    # Finds all possible designs for one value of VDD, then
    # confirm which work with all other VDD values.
    possibilities = []

    vg_vec = np.arange(0, vdd_nom, vg_res)

    for vg in vg_vec:
        print("VIN:\t{0}".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_nom,
                               vds=vg - vdd_nom,
                               vbs=vb_p - vdd_nom)

        if np.isinf(ibias_max):
            nf_n_max = 200
        else:
            nf_n_max = int(round(ibias_max / n_op_info['ibias']))

        nf_n_vec = np.arange(1, nf_n_max, 1)
        for nf_n in nf_n_vec:
            # Number of fingers can only be integer,
            # so increase as necessary until you get
            # sufficiently accurate/precise bias + current match
            ratio_good, nf_p = verify_ratio(n_op_info['ibias'],
                                            p_op_info['ibias'], nf_n,
                                            error_tol)
            if not ratio_good:
                continue

            # Getting small signal parameters to constrain Rf
            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
            gds_p = p_op_info['gds'] * nf_p
            gds = abs(gds_n) + abs(gds_p)
            ro = 1 / gds

            # 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 * 2, rf_res)
            for rf in rf_vec:
                # With all parameters, check if it meets small signal spec
                meets_SS, SS_vals = verify_TIA_inverter_SS(
                    n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload, rdc_min,
                    fbw_min, pm_min)
                # With all parameters, estimate if it will meet noise spec
                meets_noise, BER = verify_TIA_inverter_BER(
                    n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload, BER_max,
                    vos, isw_pkpk)

                meets_spec = meets_SS  # and meets_noise
                # If it meets small signal spec, append it to the list
                # of possibilities
                if meets_spec:
                    possibilities.append(
                        dict(vg=vg,
                             vdd=vdd_nom,
                             nf_n=nf_n,
                             nf_p=nf_p,
                             rf=rf,
                             rdc=SS_vals['rdc'],
                             fbw=SS_vals['fbw'],
                             pm=SS_vals['pm'],
                             ibias=ibias_n,
                             BER=BER))
                elif SS_vals['fbw'] != None and SS_vals['fbw'] < fbw_min:
                    # Increasing resistor size won't help bandwidth
                    break

    # Go through all possibilities which work at the nominal voltage
    # and ensure functionality at other bias voltages
    # Remove any nonviable options
    print("{0} working at nominal VDD".format(len(possibilities)))
    for candidate in possibilities:
        nf_n = candidate['nf_n']
        nf_p = candidate['nf_p']
        rf = candidate['rf']
        for vdd in vdd_vec:
            new_op_dict = vary_supply(vdd, db_n, db_p, nf_n, nf_p, vb_n, vb_p)
            vg = new_op_dict['vb']
            n_op = new_op_dict['n_op']
            p_op = new_op_dict['p_op']

            # Confirm small signal spec is met
            meets_SS, scratch = verify_TIA_inverter_SS(n_op, p_op, nf_n, nf_p,
                                                       rf, cpd, cload, rdc_min,
                                                       fbw_min, pm_min)

            # Confirm noise spec is met
            meets_noise, BER = verify_TIA_inverter_BER(n_op, p_op, nf_n, nf_p,
                                                       rf, cpd, cload, BER_max,
                                                       vos, isw_pkpk)

            meets_spec = meets_SS  # and meets_noise

            if not meets_spec:
                possibilities.remove(candidate)
                break

    # Of the remaining possibilities, check for lowest power.
    # If there are none, raise a ValueError.
    if len(possibilities) == 0:
        raise ValueError("No final viable solutions")

    print("{0} working at all VDD".format(len(possibilities)))
    best_op = possibilities[0]
    for candidate in possibilities:
        best_op = choose_op_comparison(best_op, candidate)

    return best_op
    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
    def _get_psrr_lti(self, op_dict, nf_dict, series_type, amp_in, rload,
                      cload) -> float:
        '''
        Outputs:
            psrr: PSRR (dB)
            fbw: Power supply -> output 3dB bandwidth (Hz)
        '''
        n_ser = series_type == 'n'
        n_amp = amp_in == 'n'

        # Supply -> output gain
        ckt_sup = LTICircuit()
        ser_d = 'vdd' if n_ser else 'reg'
        ser_s = 'reg' if n_ser else 'vdd'
        inp_conn = 'gnd' if n_ser else 'reg'
        inn_conn = 'reg' if n_ser else 'gnd'
        tail_rail = 'gnd' if n_amp else 'vdd'
        load_rail = 'vdd' if n_amp else 'gnd'
        ckt_sup.add_transistor(op_dict['ser'],
                               ser_d,
                               'out',
                               ser_s,
                               fg=nf_dict['ser'],
                               neg_cap=False)
        ckt_sup.add_res(rload, 'reg', 'gnd')
        ckt_sup.add_cap(rload, 'reg', 'gnd')
        ckt_sup.add_transistor(op_dict['in'],
                               'outx',
                               inp_conn,
                               'tail',
                               fg=nf_dict['in'],
                               neg_cap=False)
        ckt_sup.add_transistor(op_dict['in'],
                               'out',
                               inn_conn,
                               'tail',
                               fg=nf_dict['in'],
                               neg_cap=False)
        ckt_sup.add_transistor(op_dict['tail'],
                               'tail',
                               'gnd',
                               tail_rail,
                               fg=nf_dict['tail'],
                               neg_cap=False)
        ckt_sup.add_transistor(op_dict['load'],
                               'outx',
                               'outx',
                               load_rail,
                               fg=nf_dict['load'],
                               neg_cap=False)
        ckt_sup.add_transistor(op_dict['load'],
                               'out',
                               'outx',
                               load_rail,
                               fg=nf_dict['load'],
                               neg_cap=False)

        num_sup, den_sup = ckt_sup.get_num_den(in_name='vdd',
                                               out_name='reg',
                                               in_type='v')
        gain_sup = num_sup[-1] / den_sup[-1]
        wbw_sup = get_w_3db(num_sup, den_sup)

        # Reference -> output gain
        # ckt_norm = LTICircuit()
        # ser_d = 'gnd' if n_ser else 'reg'
        # ser_s = 'reg' if n_ser else 'gnd'
        # inp_conn = 'in' if n_ser else 'reg'
        # inn_conn = 'reg' if n_ser else 'in'
        # ckt_norm.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False)
        # ckt_norm.add_res(rload, 'reg', 'gnd')
        # ckt_norm.add_cap(rload, 'reg', 'gnd')
        # ckt_norm.add_transistor(op_dict['in'], 'outx', inp_conn, 'tail', fg=nf_dict['in'], neg_cap=False)
        # ckt_norm.add_transistor(op_dict['in'], 'out', inn_conn, 'tail', fg=nf_dict['in'], neg_cap=False)
        # ckt_norm.add_transistor(op_dict['tail'], 'tail', 'gnd', 'gnd', fg=nf_dict['tail'], neg_cap=False)
        # ckt_norm.add_transistor(op_dict['load'], 'outx', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False)
        # ckt_norm.add_transistor(op_dict['load'], 'out', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False)

        # num_norm, den_norm = ckt_norm.get_num_den(in_name='in', out_name='reg', in_type='v')
        # gain_norm = num_norm[-1]/den_norm[-1]

        if gain_sup == 0:
            return float('inf')
        if wbw_sup == None:
            wbw_sup = 0
        fbw_sup = wbw_sup / (2 * np.pi)

        psrr = 10 * np.log10((1 / gain_sup)**2)

        return psrr, fbw_sup
Exemple #15
0
def design_preamp_string(db_n,
                         lch,
                         w_finger,
                         vdd,
                         cload,
                         vin_signal,
                         vin_shift,
                         voutcm,
                         vtail_res,
                         gain_min,
                         fbw_min,
                         rload_res,
                         vb_n,
                         itail_max=10e-6,
                         error_tol=.01):
    """
    Designs a preamplifier to go between the comparator and TIA, stringing them
    together.
    Inputs:
    Outputs:
    """
    # Estimate vth for use in calculating biasing ranges
    vthn = estimate_vth(db_n, vdd, 0)
    vincm = (vin_signal + vin_shift) / 2

    vtail_min = .15  # TODO: Avoid hardcoding?
    vtail_max = vin_signal - vthn
    vtail_vec = np.arange(vtail_min, vtail_max, vtail_res)

    best_op = None
    best_itail = np.inf

    for vtail in vtail_vec:
        op_tail = db_n.query(vgs=voutcm, vds=vtail, vbs=vb_n)
        # The "mean" of the input devices
        op_in = db_n.query(vgs=vincm - vtail,
                           vds=voutcm - vtail,
                           vbs=vb_n - vtail)

        # Calculate the max tail size based on current limit
        # and size input devices accordingly
        nf_in_min = 2
        nf_in_max = int(round(itail_max / 2 / op_in['ibias']))
        nf_in_vec = np.arange(nf_in_min, nf_in_max, 1)
        for nf_in in nf_in_vec:
            amp_ratio_good, nf_tail = verify_ratio(op_in['ibias'] / 2,
                                                   op_tail['ibias'], nf_in,
                                                   error_tol)
            if not amp_ratio_good:
                continue

            itail = op_tail['ibias'] * nf_tail
            rload = (vdd - voutcm) / (itail / 2)

            # Input devices too small
            if op_in['gm'] * nf_in * rload < gain_min:
                continue

            # Check small signal parameters with symmetric circuit
            ckt_sym = LTICircuit()
            ckt_sym.add_transistor(op_in, 'outp', 'gnd', 'tail', fg=nf_in)
            ckt_sym.add_transistor(op_in, 'outn', 'inp', 'tail', fg=nf_in)
            ckt_sym.add_transistor(op_tail, 'tail', 'gnd', 'gnd', fg=nf_tail)
            ckt_sym.add_res(rload, 'outp', 'gnd')
            ckt_sym.add_res(rload, 'outn', 'gnd')
            ckt_sym.add_cap(cload, 'outp', 'gnd')
            ckt_sym.add_cap(cload, 'outn', 'gnd')
            num, den = ckt_sym.get_num_den(in_name='inp',
                                           out_name='outn',
                                           in_type='v')
            num_unintent, den_unintent = ckt_sym.get_num_den(in_name='inp',
                                                             out_name='outp',
                                                             in_type='v')
            gain_intentional = num[-1] / den[-1]
            gain_unintentional = num_unintent[-1] / den_unintent[-1]
            gain = abs(gain_intentional - gain_unintentional)
            wbw = get_w_3db(num, den)
            if wbw == None:
                wbw = 0
            fbw = wbw / (2 * np.pi)
            if fbw < fbw_min:
                print("(FAIL) BW:\t{0}".format(fbw))
                continue
            if gain < gain_min:
                print("(FAIL) GAIN:\t{0}".format(gain))
                continue

            print("(SUCCESS)")
            if itail > best_itail:
                continue

            # Check once again with asymmetric circuit
            vin_diff = vin_signal - vin_shift
            voutn = voutcm - gain * vin_diff / 2
            voutp = voutcm + gain * vin_diff / 2
            op_signal = db_n.query(vgs=vin_signal - vtail,
                                   vds=voutn - vtail,
                                   vbs=vb_n - vtail)
            op_shift = db_n.query(vgs=vin_shift - vtail,
                                  vds=voutp - vtail,
                                  vbs=vb_n - vtail)
            ckt = LTICircuit()
            ckt.add_transistor(op_shift, 'outp', 'gnd', 'tail', fg=nf_in)
            ckt.add_transistor(op_signal, 'outn', 'inp', 'tail', fg=nf_in)
            ckt.add_transistor(op_tail, 'tail', 'gnd', 'gnd', fg=nf_tail)
            ckt.add_res(rload, 'outp', 'gnd')
            ckt.add_res(rload, 'outn', 'gnd')
            ckt.add_cap(cload, 'outp', 'gnd')
            ckt.add_cap(cload, 'outn', 'gnd')
            num, den = ckt.get_num_den(in_name='inp',
                                       out_name='outn',
                                       in_type='v')
            num_unintent, den_unintent = ckt.get_num_den(in_name='inp',
                                                         out_name='outp',
                                                         in_type='v')
            gain_intentional = num[-1] / den[-1]
            gain_unintentional = num_unintent[-1] / den_unintent[-1]
            gain = abs(gain_intentional - gain_unintentional)
            wbw = get_w_3db(num, den)
            if wbw == None:
                wbw = 0
            fbw = wbw / (2 * np.pi)
            if fbw < fbw_min:
                print("(FAIL) BW:\t{0}".format(fbw))
                continue
            if gain < gain_min:
                print("(FAIL) GAIN:\t{0}".format(gain))
                continue

            print("(SUCCESS)")
            if itail < best_itail:
                best_itail = itail
                best_op = dict(nf_in=nf_in,
                               nf_tail=nf_tail,
                               itail=itail,
                               rload=rload,
                               gain=gain,
                               fbw=fbw,
                               voutcm=voutcm,
                               vtail=vtail,
                               cin=op_signal['cgg'] * nf_in)
    if best_op == None:
        raise ValueError("No viable solutions.")
    return best_op
Exemple #16
0
    def meet_spec(self, **params) -> List[Mapping[str, Any]]:
        """To be overridden by subclasses to design this module.

        Raises a ValueError if there is no solution.
        """
        ### Get DBs for each device
        specfile_dict = params['specfile_dict']
        l_dict = params['l_dict']
        th_dict = params['th_dict']
        sim_env = params['sim_env']

        # Databases
        db_dict = {
            k: get_mos_db(spec_file=specfile_dict[k],
                          intent=th_dict[k],
                          lch=l_dict[k],
                          sim_env=sim_env)
            for k in specfile_dict.keys()
        }

        ### Design devices
        in_type = params['in_type']
        vdd = params['vdd']
        vincm = params['vincm']
        voutcm = params['voutcm']
        cload = params['cload']
        gain_min = params['gain']
        fbw_min = params['fbw']
        ugf_min = params['ugf']
        ibias_max = params['ibias']

        # Somewhat arbitrary vstar_min in this case
        optional_params = params['optional_params']
        vstar_min = optional_params.get('vstar_min', 0.2)
        res_vstep = optional_params.get('res_vstep', 10e-3)
        error_tol = optional_params.get('error_tol', 0.01)
        vout1_opt = optional_params.get('vout1', None)

        n_in = in_type == 'n'
        vtest_in = vdd / 2 if n_in else -vdd / 2
        vtest_tail = vdd / 2 if n_in else -vdd / 2
        vtest_out = -vdd / 2 if n_in else vdd / 2

        vb_in = 0 if n_in else vdd
        vb_tail = 0 if n_in else vdd
        vb_load = vdd if n_in else 0
        vb_out = 0 if n_in else vdd

        # Estimate threshold of each device TODO can this be more generalized?
        vth_in = estimate_vth(is_nch=n_in,
                              vgs=vtest_in,
                              vbs=0,
                              db=db_dict['in'],
                              lch=l_dict['in'])
        vth_tail = estimate_vth(is_nch=n_in,
                                vgs=vtest_tail,
                                vbs=0,
                                db=db_dict['tail'],
                                lch=l_dict['tail'])
        vth_load = estimate_vth(is_nch=(not n_in),
                                vgs=vtest_out,
                                vbs=0,
                                db=db_dict['load'],
                                lch=l_dict['load'])
        vth_out = estimate_vth(is_nch=n_in,
                               vgs=vtest_in,
                               vbs=0,
                               db=db_dict['out'],
                               lch=l_dict['out'])

        # Keeping track of operating points which work for future comparison
        viable_op_list = []

        # Get tail voltage range
        vtail_min = vstar_min if n_in else vincm - vth_in
        vtail_max = vincm - vth_in if n_in else vdd - vstar_min
        vtail_vec = np.arange(vtail_min, vtail_max, res_vstep)
        print(f'Sweeping tail from {vtail_min} to {vtail_max}')

        # Get out1 common mode range
        vout1_min = vincm - vth_in if n_in else vth_load + vstar_min
        vout1_max = vdd + vth_load - vstar_min if n_in else vincm - vth_in
        if vout1_opt == None:
            vout1_vec = np.arange(vout1_min, vout1_max, res_vstep)
        else:
            vout1_vec = [vout1_opt]
            if vout1_opt < vout1_min or vout1_opt > vout1_max:
                warnings.warn(
                    f'vout11 {vout1_opt} falls outside recommended range ({vout1_min}, {vout1_max})'
                )

        # Sweep tail voltage
        for vtail in vtail_vec:
            # Sweep out1 common mode
            for vout1 in vout1_vec:
                in_op = db_dict['in'].query(vgs=vincm - vtail,
                                            vds=vout1 - vtail,
                                            vbs=vb_in - vtail)
                load_op = db_dict['load'].query(vgs=vout1 - vb_load,
                                                vds=vout1 - vb_load,
                                                vbs=0)
                load_copy_op = db_dict['load'].query(vgs=vout1 - vb_load,
                                                     vds=voutcm - vb_load,
                                                     vbs=0)
                out_op = db_dict['out'].query(vgs=voutcm - vb_out,
                                              vds=voutcm - vb_out,
                                              vbs=0)

                itail_min = 4 * in_op['ibias']

                # Step input device size (integer steps)
                nf_in_max = int(round(ibias_max / itail_min))
                nf_in_vec = np.arange(1, nf_in_max, 1)
                # if len(nf_in_vec) > 0:
                #     print(f'Number of input devices {min(nf_in_vec)} to {max(nf_in_vec)}')

                for nf_in in nf_in_vec:
                    itail = itail_min * nf_in

                    # Match load device size
                    load_match, nf_load = verify_ratio(in_op['ibias'] * 2,
                                                       load_op['ibias'], nf_in,
                                                       error_tol)

                    if not load_match:
                        print(f"load match {nf_load}")
                        # assert False, 'blep'
                        continue

                    iflip_max = (ibias_max - itail) / 2
                    nf_out_max = int(round(iflip_max / out_op['ibias']))
                    nf_out_vec = [1] if nf_out_max == 1 else np.arange(
                        1, nf_out_max, 1)
                    # if len(nf_out_vec) > 0:
                    #     print(f'Number of output devices {min(nf_out_vec)} to {max(nf_out_vec)}')

                    # Step output device size
                    for nf_out in nf_out_vec:
                        iflip_branch = out_op['ibias'] * nf_out
                        # Match load copy device size
                        load_copy_match, nf_load_copy = verify_ratio(
                            out_op['ibias'], load_copy_op['ibias'], nf_out,
                            error_tol)
                        if not load_copy_match:
                            print('load copy match')
                            continue

                        # Check target specs
                        ckt_half = LTICircuit()
                        ckt_half.add_transistor(in_op,
                                                'out',
                                                'in',
                                                'gnd',
                                                fg=nf_in * 2,
                                                neg_cap=False)
                        ckt_half.add_transistor(load_op,
                                                'out1',
                                                'out1',
                                                'gnd',
                                                fg=nf_load,
                                                neg_cap=False)
                        ckt_half.add_transistor(load_copy_op,
                                                'out',
                                                'out1',
                                                'gnd',
                                                fg=nf_load_copy,
                                                neg_cap=False)
                        ckt_half.add_transistor(out_op,
                                                'out',
                                                'out',
                                                'gnd',
                                                fg=nf_out,
                                                neg_cap=False)
                        ckt_half.add_cap(cload, 'out', 'gnd')

                        num, den = ckt_half.get_num_den(in_name='in',
                                                        out_name='out',
                                                        in_type='v')
                        # num = np.convolve(num, [-1]) # To get positive gain

                        wbw = get_w_3db(num, den)
                        if wbw == None:
                            wbw = 0
                        fbw = wbw / (2 * np.pi)

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

                        # Rout = parallel(1/(nf_in*2*in_op['gds']),
                        #                 1/(nf_out*out_op['gm']),
                        #                 1/(nf_out*out_op['gds']))
                        # Gm = in_op['gm']*nf_in*2
                        # gain = Gm * Rout
                        # Cout = cload + (nf_in*2*in_op['cgs']) + (1+gain)*(nf_in*2*in_op['cds'])
                        # fbw = 1/(2*np.pi*Rout*Cout)
                        # ugf = Gm / Cout * (1/2*np.pi)
                        gain = -num[-1] / den[-1]
                        ugf = fbw * gain
                        if fbw < fbw_min:
                            print(f"fbw {fbw}")
                            continue

                        if ugf < ugf_min:
                            print(f"ugf {ugf}")
                            break

                        if gain < gain_min:
                            print(f'gain {gain}')
                            break

                        # Design matching tail
                        vgtail_min = vth_tail + vstar_min if n_in else vtail + vth_tail
                        vgtail_max = vtail + vth_tail if n_in else vdd + vth_tail - vstar_min
                        vgtail_vec = np.arange(vgtail_min, vgtail_max,
                                               res_vstep)
                        print(f"Tail gate from {vgtail_min} to {vgtail_max}")
                        for vgtail in vgtail_vec:
                            tail_op = db_dict['tail'].query(
                                vgs=vgtail - vb_tail,
                                vds=vtail - vb_tail,
                                vbs=0)
                            tail_match, nf_tail = verify_ratio(
                                in_op['ibias'] * 4, tail_op['ibias'], nf_in,
                                error_tol)

                            if not tail_match:
                                print("tail match")
                                continue

                            print('(SUCCESS)')
                            viable_op = dict(
                                nf_in=nf_in,
                                nf_load=nf_load,
                                nf_load_copy=nf_load_copy,
                                nf_out=nf_out,
                                nf_tail=nf_tail,
                                vgtail=vgtail,
                                gain=gain,
                                fbw=fbw,
                                ugf=ugf,
                                vtail=vtail,
                                vout1=vout1,
                                itail=itail,
                                iflip_branch=nf_out * out_op['ibias'],
                                ibias=itail + 2 * nf_out * out_op['ibias'])
                            print(viable_op)
                            viable_op_list.append(viable_op)
        self.other_params = dict(
            in_type=in_type,
            lch_dict=l_dict,
            w_dict={k: db.width_list[0]
                    for k, db in db_dict.items()},
            th_dict=th_dict)
        return viable_op_list
Exemple #17
0
            # LTICircuit simulation
            circuit = LTICircuit()
            circuit.add_transistor(op_in, 'outp', 'gnd', 'tail', fg=nf_in)
            circuit.add_transistor(op_in, 'outn', 'inp', 'tail', fg=nf_in)
            circuit.add_transistor(op_trim_on, 'outn', 'gnd', 'gnd', fg=nf_trim)
            circuit.add_transistor(op_trim_off, 'outp', 'gnd', 'gnd', fg=nf_trim)
            circuit.add_transistor(op_tail, 'tail', 'vmirr', 'gnd', fg=nf_tail)
            circuit.add_transistor(op_mirr, 'vmirr', 'vmirr', 'gnd', fg=nf_mirr)
            circuit.add_transistor(op_dummy, 'gnd', 'gnd', 'vmirr', fg=nf_dummy)
            circuit.add_res(rload, 'outp', 'gnd')
            circuit.add_res(rload, 'outn', 'gnd')
            circuit.add_cap(cload, 'outp', 'gnd')
            circuit.add_cap(cload, 'outn', 'gnd')
            circuit.add_cap(dac_cap, 'tail', 'gnd')
            num, den = circuit.get_num_den(in_name='inp', out_name='outn', in_type='v')
            num_unintentional, den_unintentional = circuit.get_num_den(in_name='inp', out_name='outp', in_type='v')
            gain_intentional = num[-1]/den[-1]
            gain_unintentional = num_unintentional[-1]/den_unintentional[-1]
            gain = abs(gain_intentional-gain_unintentional)
            wbw = get_w_3db(num, den)
            if wbw == None:
                wbw = 0
            fbw = wbw/(2*np.pi)
            if fbw < bw_min:
                cond_print("Bandwidth fail: {}".format(fbw), debugMode)
                cond_print("NF,IN: {}".format(nf_in), debugMode)
                cond_print("NF,TAIL: {}".format(nf_tail), debugMode)
                cond_print("NF,MIRR: {}".format(nf_mirr), debugMode)
                cond_print("NF,DUMMY: {}".format(nf_dummy), debugMode)
                continue
    def meet_spec(self, **params) -> List[Mapping[str, Any]]:
        """To be overridden by subclasses to design this module.

        Raises a ValueError if there is no solution.
        """
        ### Get DBs for each device
        specfile_dict = params['specfile_dict']
        in_type = params['in_type']
        l_dict = params['l_dict']
        th_dict = params['th_dict']
        sim_env = params['sim_env']

        # Databases
        db_dict = {
            k: get_mos_db(spec_file=specfile_dict[k],
                          intent=th_dict[k],
                          lch=l_dict[k],
                          sim_env=sim_env)
            for k in specfile_dict.keys()
        }

        ### Design devices
        vdd = params['vdd']
        vincm = params['vincm']
        cload = params['cload']
        gain_min, gain_max = params['gain_lim']
        fbw_min = params['fbw']
        ibias_max = params['ibias']
        optional_params = params['optional_params']

        # Pulling out optional parameters
        vstar_min = optional_params.get('vstar_min', 0.2)
        res_vstep = optional_params.get('res_vstep', 10e-3)
        error_tol = optional_params.get('error_tol', 0.01)

        n_in = in_type == 'n'

        # Estimate threshold of each device
        vtest = vdd / 2 if n_in else -vdd / 2
        vb = 0 if n_in else vdd

        vth_in = estimate_vth(is_nch=n_in,
                              vgs=vtest,
                              vbs=0,
                              db=db_dict['in'],
                              lch=l_dict['in'])
        vth_tail = estimate_vth(is_nch=n_in,
                                vgs=vtest,
                                vbs=0,
                                db=db_dict['tail'],
                                lch=l_dict['tail'])

        # Keeping track of operating points which work for future comparison
        viable_op_list = []

        # Sweep tail voltage
        vtail_min = vstar_min if n_in else vincm - vth_in
        vtail_max = vincm - vth_in if n_in else vdd - vstar_min
        vtail_vec = np.arange(vtail_min, vtail_max, res_vstep)
        print(f'Sweeping tail from {vtail_min} to {vtail_max}')
        for vtail in vtail_vec:
            voutcm_min = vincm - vth_in if n_in else 0
            voutcm_max = vdd if n_in else vincm - vth_in

            voutcm = optional_params.get('voutcm', None)

            if voutcm == None:
                voutcm_vec = np.arange(voutcm_min, voutcm_max, res_vstep)
            else:
                voutcm_vec = [voutcm]
            # Sweep output common mode
            for voutcm in voutcm_vec:
                in_op = db_dict['in'].query(vgs=vincm - vtail,
                                            vds=voutcm - vtail,
                                            vbs=vb - vtail)
                ibias_min = 2 * in_op['ibias']
                # Step input device size (integer steps)
                nf_in_max = int(round(ibias_max / ibias_min))
                nf_in_vec = np.arange(2, nf_in_max, 2)
                for nf_in in nf_in_vec:
                    ibias = ibias_min * nf_in
                    ibranch = ibias / 2
                    res_val = (vdd -
                               voutcm) / ibranch if n_in else voutcm / ibranch
                    # Check gain, bandwidth
                    ckt_half = LTICircuit()
                    ckt_half.add_transistor(in_op,
                                            'out',
                                            'in',
                                            'gnd',
                                            fg=nf_in,
                                            neg_cap=False)
                    ckt_half.add_res(res_val, 'out', 'gnd')
                    ckt_half.add_cap(cload, 'out', 'gnd')

                    num, den = ckt_half.get_num_den(in_name='in',
                                                    out_name='out',
                                                    in_type='v')
                    gain = -(num[-1] / den[-1])
                    wbw = get_w_3db(num, den)
                    if wbw == None:
                        wbw = 0
                    fbw = wbw / (2 * np.pi)

                    if gain < gain_min or gain > gain_max:
                        print(f'gain: {gain}')
                        break

                    if fbw < fbw_min:
                        print(f'fbw: {fbw}')
                        continue

                    # Design tail to current match
                    vgtail_min = vth_tail + vstar_min if n_in else vtail + vth_tail
                    vgtail_max = vtail + vth_tail if n_in else vdd + vth_tail - vstar_min
                    vgtail_vec = np.arange(vgtail_min, vgtail_max, res_vstep)
                    print(f'vgtail {vgtail_min} to {vgtail_max}')
                    for vgtail in vgtail_vec:
                        tail_op = db_dict['tail'].query(vgs=vgtail - vb,
                                                        vds=vtail - vb,
                                                        vbs=0)
                        tail_success, nf_tail = verify_ratio(
                            in_op['ibias'] * 2, tail_op['ibias'], nf_in,
                            error_tol)
                        if not tail_success:
                            print('tail match')
                            continue

                        viable_op = dict(nf_in=nf_in,
                                         nf_tail=nf_tail,
                                         res_val=res_val,
                                         voutcm=voutcm,
                                         vgtail=vgtail,
                                         gain=gain,
                                         fbw=fbw,
                                         vtail=vtail,
                                         ibias=tail_op['ibias'] * nf_tail,
                                         cmfb_cload=tail_op['cgg'] * nf_tail)
                        viable_op_list.append(viable_op)
                        print("\n(SUCCESS)")
                        print(viable_op)

        self.other_params = dict(
            in_type=in_type,
            l_dict=l_dict,
            w_dict={k: db.width_list[0]
                    for k, db in db_dict.items()},
            th_dict=th_dict)

        # assert False, 'blep'

        return viable_op_list
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