Module serafin.util

Single-Ended Operational Amplifier Characterization Utility functions

Expand source code
""" Single-Ended Operational Amplifier Characterization Utility functions """

import os
from   tempfile        import mkdtemp
from   shutil          import copyfile
from   typing          import Union, Iterable
from   scipy           import interpolate
import numpy           as np
import pandas          as pd

def setup_dir(pdk_cfg: dict, net_scs: str, ckt_cfg: dict) -> str:
    """
    Setup a temporary simulation directory with testbench and subckt ready to go.
    """

    cwd    = os.path.dirname(os.path.abspath(__file__))
    op_id  = os.path.basename(net_scs).split('.')[0]
    usr_id = os.getlogin()

    with open(net_scs, mode = 'r', encoding = 'utf-8') as net_h:
        net = net_h.read()

    includes = '\n'.join( [ f'include "{p["path"]}" section={p["section"]}'
                            for p in pdk_cfg.get('include', {}) ] )

    defaults  =  ckt_cfg.get( 'parameters', {}).get('testbench', {}
                           ) | pdk_cfg.get('testbench', {})

    tb_params = 'parameters ' \
              + ' '.join([ f'{p}={v}' for p,v in defaults.items() ])

    op_params = 'parameters ' \
              + ' '.join([ f'{p}={v}'
                           for p,v in ckt_cfg.get( 'parameters', {}
                                                ).get( 'geometrical'
                                                     , {} ).items() ] )

    area      = ckt_cfg.get('parameters', {}).get('area', '0.0')
    ae_params = f'parameters area={area}' if area != '0.0' else ''

    op_pre    = pdk_cfg['devices']['dcop']['prefix']
    op_suf    = pdk_cfg['devices']['dcop']['suffix']
    op_par    = pdk_cfg['devices']['dcop']['parameters']
    saves     = 'save ' \
              + '\\\n\t'.join([ f'{op_pre}*{op_suf}:{param}'
                                for param in op_par ])

    subckt    = '\n\n'.join([ includes, tb_params, op_params, ae_params
                            , net, saves ])

    tmp_dir   = mkdtemp(prefix = f'{usr_id}_{op_id}_')

    with open(f'{tmp_dir}/op.scs', 'w',  encoding = 'utf-8') as sub_h:
        sub_h.write(subckt)

    copyfile(f'{cwd}/resource/testbench.scs', f'{tmp_dir}/tb.scs')

    return tmp_dir

def repeat_values(d: dict[str, float], n: int) -> dict[str, list[float]]:
    return { k: n * [v] for k,v in d }

def transpose_dict(ds: Iterable[dict[str, float]]) -> dict[str, Iterable[float]]:
    keys = {d.keys() for d in ds}
    vals = np.array([list(d.values()) for d in ds]).T.tolist()
    return dict(zip(keys, vals))

def find_closest_idx(array: np.array, value: float) -> int:
    return np.argmin(np.abs(array - value), axis = len(array.shape) - 1)

def find_first_idx(array: np.array, value: float, edge: str) -> Union[int, None]:
    if edge == 'r':
        indices = np.argwhere(array > value)
    elif edge == 'f':
        indices = np.argwhere(array < value)
    else:
        indices = np.empty(0)
    return np.min(indices).item() if np.size(indices) else None

def db20(x: Union[float, np.array]) -> Union[float, np.array]:
    return np.log10(np.abs(x)) * 20.0

def nan_frame(cols: Iterable[str]) -> pd.DataFrame:
    length = len(cols)
    return pd.DataFrame(np.full((1,length), np.NaN), columns = cols)

def from_dict(d: dict[str, float]) -> pd.DataFrame:
    return pd.DataFrame.from_dict({k: [v] for k,v in d.items()})

def to_dict(df: pd.DataFrame) -> dict[str, float]:
    return {k: v[0] for k,v in df.to_dict(orient = 'list').items()}

def find_sr_r(times: np.array, values: np.array, upper: float, lower: float) -> float:

    rising_hi     = find_first_idx(values, upper, 'r')
    rising_lo     = find_first_idx(values, lower, 'r')

    if rising_hi and rising_lo:
        d_rising  = times[rising_hi]-times[rising_lo]
        sr_r      = (upper - lower) / d_rising if d_rising > 0 else np.nan
    else:
        sr_r      = np.nan
    return sr_r

def find_sr_f(times: np.array, values: np.array, upper: float, lower: float) -> float:
    flipped       = np.flip(values)
    candidate     = find_first_idx(flipped, upper, 'r')
    falling_hi    = (-1* candidate) -1 if candidate else None

    window        = values[falling_hi:]
    candidate     = find_first_idx(window, lower, 'f')
    falling_lo    = (falling_hi + candidate) if (candidate and falling_hi) else None

    if falling_hi and falling_lo:
        d_falling = times[falling_lo]-times[falling_hi]
        sr_f      = (lower - upper) / d_falling if d_falling > 0 else np.nan
    else:
        sr_f      = np.nan
    return sr_f

PERFORMANCE_PARAMETERS: dict[str,str] = { 'area':       'Estimated Area'
                                        , 'a_0':        'DC Loop Gain'
                                        , 'cmrr':       'Common Mode Rejection Ratio'
                                        , 'cof':        'Cross-Over Frequency'
                                        , 'gm':         'Gain Margin'
                                        , 'i_out_max':  'Maximum output Current'
                                        , 'i_out_min':  'Minimum output Current'
                                        , 'idd':        'Current Consumption'
                                        , 'iss':        'Current Consumption'
                                        , 'os_f':       'Overshoot Falling'
                                        , 'os_r':       'Overshoot Rising'
                                        , 'pm':         'Phase Margin'
                                        , 'psrr_n':     'Power Supply Rejection Ratio'
                                        , 'psrr_p':     'Power Supply Rejection Ratio'
                                        , 'sr_f':       'Slew Rate Falling'
                                        , 'sr_r':       'Slew Rate Rising'
                                        , 'ugbw':       'Unity Gain Bandwidth'
                                        , 'v_ih':       'Input Voltage Hight'
                                        , 'v_il':       'Input Voltage Low'
                                        , 'v_oh':       'Output Voltage High'
                                        , 'v_ol':       'Output Voltage Low'
                                        , 'vn_1Hz':     'Output Referred Noise @ 1Hz'
                                        , 'vn_10Hz':    'Output Referred Noise @ 10Hz'
                                        , 'vn_100Hz':   'Output Referred Noise @ 100Hz'
                                        , 'vn_1kHz':    'Output Referred Noise @ 1kHz'
                                        , 'vn_10kHz':   'Output Referred Noise @ 10kHz'
                                        , 'vn_100kHz':  'Output Referred Noise @ 100kHz'
                                        , 'voff_stat':  'Statistical Offset'
                                        , 'voff_syst':  'Systematic Offset'
                                        , }

def offset( dcmatch: pd.DataFrame, offset_params: dict[str, str]
          ) -> pd.DataFrame:
    col_ids  = { 'totalOutput.sigmaOut': 'voff_stat'
               , 'totalOutput.dcOp':     'voff_syst'
               } | offset_params

    left     = [ cn for co,cn in col_ids.items()
                    if co not in list(dcmatch.columns) ]
    right    = [ c for c in col_ids.keys() if c in list(dcmatch.columns) ]

    offset_  = dcmatch[right].rename(columns = col_ids)
    offset   = pd.concat( [ offset_
                          , pd.DataFrame( np.zeros((1,len(left)))
                                        , columns = left)]
                        , axis = 1
                        , ) if left else offset_
    return offset

def stability(stb: pd.DataFrame) -> pd.DataFrame:
    cols      = ['a_0', 'ugbw', 'pm', 'gm', 'cof']
    loop_gain = stb['loopGain'].values
    freq      = stb['freq'].values
    gain      = db20(loop_gain)
    phase     = np.angle(loop_gain, deg = True)
    a0_idx    = find_first_idx(gain, 0.0, 'f')
    ph0_idx   = find_first_idx(phase, 0.0, 'f')
    if a0_idx and ph0_idx:
        a0db      = gain[0].item()
        a3db      = a0db - 3.0
        f0db      = freq[a0_idx].real.item()
        f0_idx    = find_closest_idx(freq, f0db)
        pm        = phase[f0_idx].item()
        cof       = freq[ph0_idx].real.item()
        gm        = gain[ph0_idx].item()
    else:
        a0db      = np.nan
        f0db      = np.nan
        pm        = np.nan
        gm        = np.nan
        cof       = np.nan
    stability = pd.DataFrame( np.array([[a0db, f0db, pm, gm, cof]])
                            , columns = cols )
    return stability

def transient( tran: pd.DataFrame, vs: float = 0.5 ) -> dict[str, float]:
    cols       = ['sr_r', 'sr_f', 'os_r', 'os_f']
    time       = tran['time'].values
    out        = tran['OUT'].values

    idx_100    = find_closest_idx(time, 100e-9)
    idx_090    = find_closest_idx(time, 90e-6)
    idx_050    = find_closest_idx(time, 50e-6)
    idx_099    = find_closest_idx(time, 99.9e-6)

    rising     = out[idx_100:idx_050]
    falling    = out[(idx_050+1):idx_099]

    time_r     = time[idx_100:idx_050]
    time_f     = time[(idx_050+1):idx_099]

    lower      = (0.1 * vs) - (vs / 2.0)
    upper      = (0.9 * vs) - (vs / 2.0)

    sr_rising  = find_sr_r(time_r, rising, upper, lower)
    sr_falling = find_sr_f(time_f, falling, upper, lower)

    os_rising  = 100 * (np.max(rising) - out[idx_050]) / (out[idx_050] - out[idx_100])
    os_falling = 100 * (np.min(falling) - out[idx_090]) / (out[idx_090] - out[idx_050])

    transient  =  pd.DataFrame( np.array([[ sr_rising, sr_falling
                                          , os_rising, os_falling]])
                              , columns = cols )
    return transient

def output_referred_noise(noise: pd.DataFrame) -> pd.DataFrame:
    cols  = ['vn_1Hz', 'vn_10Hz', 'vn_100Hz', 'vn_1kHz', 'vn_10kHz', 'vn_100kHz']
    fs    = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5]
    freq  = noise['freq'].values
    out   = noise['out'].values
    vn    = interpolate.pchip_interpolate(freq, out, fs).reshape(1,-1)
    noise = pd.DataFrame(vn, columns = cols)
    return noise

def out_swing_dc( dc1: pd.DataFrame, vdd: float = 1.8, dev: float = 1.0e-4
                ) -> pd.DataFrame:
    cols      = ['v_oh', 'v_ol']
    out       = dc1['OUT'].values
    out_ideal = dc1['OUT_IDEAL'].values
    vid       = dc1['vid'].values
    out_dc    = (out - out[find_closest_idx(vid, 0.0)])
    dev_rel   = np.abs(out_dc - out_ideal) / vdd
    vil_idx   = np.argmin(np.abs(dev_rel[vid <= 0.0] - dev))
    vih_idx   = np.argmin(np.abs(dev_rel[vid >= 0.0] - dev))
    vol_dc    = (out_dc[vil_idx] + (vdd / 2.0)).item()
    voh_dc    = (out_dc[vih_idx] + (vdd / 2.0)).item()
    swing     =  pd.DataFrame(np.array([[voh_dc, vol_dc]]), columns = cols)
    return swing

def rejection(xf: pd.DataFrame) -> pd.DataFrame:
    cols     = ['psrr_p', 'psrr_n', 'cmrr']
    vid_db   = db20(xf['VID'].values)
    vicm_db  = db20(xf['VICM'].values)
    vsupp_db = db20(xf['VSUPP'].values)
    vsupn_db = db20(xf['VSUPN'].values)
    psrr_p   = (vid_db - vsupp_db)[0].item()
    psrr_n   = (vid_db - vsupn_db)[0].item()
    cmrr     = (vid_db - vicm_db)[0].item()
    ratios   = pd.DataFrame( np.array([[psrr_p, psrr_n, cmrr]])
                           , columns = cols )
    return ratios

def out_swing_ac( ac: pd.DataFrame, A3dB: float, vdd: float = 1.8
                 ) -> pd.DataFrame:
    cols   = ['v_ih', 'v_il']
    vicm   = ac['vicm'].values
    out_ac = db20(ac['OUT'].values)
    leq_0  = out_ac[vicm <= 0.0]
    geq_0  = out_ac[vicm >= 0.0]
    vil_ac = (vicm[np.argmin(np.abs(leq_0 - A3dB))] + (vdd / 2.0)
             ).real.item() if leq_0.size > 0 else 0.0
    vih_ac = (vicm[np.argmin(np.abs(geq_0 - A3dB))] + (vdd / 2.0)
             ).real.item() if geq_0.size > 0 else 0.0
    swing  = pd.DataFrame(np.array([[vih_ac, vil_ac ]]), columns = cols)
    return swing

def output_current(dc3: pd.DataFrame, dc4: pd.DataFrame) -> pd.DataFrame:
    cols      = ['i_out_min', 'i_out_max']
    i_out_min = dc3['DUT:O'].values[0].item()
    i_out_max = dc4['DUT:O'].values[0].item()
    current   = pd.DataFrame( np.array([[i_out_min, i_out_max]])
                            , columns = cols )
    return current

def operating_point( dcop: pd.DataFrame, dcop_params: dict[str, str]
                   ) -> pd.DataFrame:
    col_ids = { 'DUT:VDD': 'idd', 'DUT:VSS': 'iss'
              } | dcop_params
    dcop    = dcop[list(col_ids.keys())].rename(columns = col_ids)
    return dcop

def estimated_area(expr: str, sizing: pd.DataFrame) -> pd.DataFrame:
    params = to_dict(sizing)
    area   = np.array([[eval(expr, {}, params)]])
    return pd.DataFrame(area, columns = ['area'])

Functions

def db20(x: Union[float, ]) ‑> Union[float, ]
Expand source code
def db20(x: Union[float, np.array]) -> Union[float, np.array]:
    return np.log10(np.abs(x)) * 20.0
def estimated_area(expr: str, sizing: pandas.core.frame.DataFrame) ‑> pandas.core.frame.DataFrame
Expand source code
def estimated_area(expr: str, sizing: pd.DataFrame) -> pd.DataFrame:
    params = to_dict(sizing)
    area   = np.array([[eval(expr, {}, params)]])
    return pd.DataFrame(area, columns = ['area'])
def find_closest_idx(array: , value: float) ‑> int
Expand source code
def find_closest_idx(array: np.array, value: float) -> int:
    return np.argmin(np.abs(array - value), axis = len(array.shape) - 1)
def find_first_idx(array: , value: float, edge: str) ‑> Optional[int]
Expand source code
def find_first_idx(array: np.array, value: float, edge: str) -> Union[int, None]:
    if edge == 'r':
        indices = np.argwhere(array > value)
    elif edge == 'f':
        indices = np.argwhere(array < value)
    else:
        indices = np.empty(0)
    return np.min(indices).item() if np.size(indices) else None
def find_sr_f(times: , values: , upper: float, lower: float) ‑> float
Expand source code
def find_sr_f(times: np.array, values: np.array, upper: float, lower: float) -> float:
    flipped       = np.flip(values)
    candidate     = find_first_idx(flipped, upper, 'r')
    falling_hi    = (-1* candidate) -1 if candidate else None

    window        = values[falling_hi:]
    candidate     = find_first_idx(window, lower, 'f')
    falling_lo    = (falling_hi + candidate) if (candidate and falling_hi) else None

    if falling_hi and falling_lo:
        d_falling = times[falling_lo]-times[falling_hi]
        sr_f      = (lower - upper) / d_falling if d_falling > 0 else np.nan
    else:
        sr_f      = np.nan
    return sr_f
def find_sr_r(times: , values: , upper: float, lower: float) ‑> float
Expand source code
def find_sr_r(times: np.array, values: np.array, upper: float, lower: float) -> float:

    rising_hi     = find_first_idx(values, upper, 'r')
    rising_lo     = find_first_idx(values, lower, 'r')

    if rising_hi and rising_lo:
        d_rising  = times[rising_hi]-times[rising_lo]
        sr_r      = (upper - lower) / d_rising if d_rising > 0 else np.nan
    else:
        sr_r      = np.nan
    return sr_r
def from_dict(d: dict[str, float]) ‑> pandas.core.frame.DataFrame
Expand source code
def from_dict(d: dict[str, float]) -> pd.DataFrame:
    return pd.DataFrame.from_dict({k: [v] for k,v in d.items()})
def nan_frame(cols: Iterable[str]) ‑> pandas.core.frame.DataFrame
Expand source code
def nan_frame(cols: Iterable[str]) -> pd.DataFrame:
    length = len(cols)
    return pd.DataFrame(np.full((1,length), np.NaN), columns = cols)
def offset(dcmatch: pandas.core.frame.DataFrame, offset_params: dict[str, str]) ‑> pandas.core.frame.DataFrame
Expand source code
def offset( dcmatch: pd.DataFrame, offset_params: dict[str, str]
          ) -> pd.DataFrame:
    col_ids  = { 'totalOutput.sigmaOut': 'voff_stat'
               , 'totalOutput.dcOp':     'voff_syst'
               } | offset_params

    left     = [ cn for co,cn in col_ids.items()
                    if co not in list(dcmatch.columns) ]
    right    = [ c for c in col_ids.keys() if c in list(dcmatch.columns) ]

    offset_  = dcmatch[right].rename(columns = col_ids)
    offset   = pd.concat( [ offset_
                          , pd.DataFrame( np.zeros((1,len(left)))
                                        , columns = left)]
                        , axis = 1
                        , ) if left else offset_
    return offset
def operating_point(dcop: pandas.core.frame.DataFrame, dcop_params: dict[str, str]) ‑> pandas.core.frame.DataFrame
Expand source code
def operating_point( dcop: pd.DataFrame, dcop_params: dict[str, str]
                   ) -> pd.DataFrame:
    col_ids = { 'DUT:VDD': 'idd', 'DUT:VSS': 'iss'
              } | dcop_params
    dcop    = dcop[list(col_ids.keys())].rename(columns = col_ids)
    return dcop
def out_swing_ac(ac: pandas.core.frame.DataFrame, A3dB: float, vdd: float = 1.8) ‑> pandas.core.frame.DataFrame
Expand source code
def out_swing_ac( ac: pd.DataFrame, A3dB: float, vdd: float = 1.8
                 ) -> pd.DataFrame:
    cols   = ['v_ih', 'v_il']
    vicm   = ac['vicm'].values
    out_ac = db20(ac['OUT'].values)
    leq_0  = out_ac[vicm <= 0.0]
    geq_0  = out_ac[vicm >= 0.0]
    vil_ac = (vicm[np.argmin(np.abs(leq_0 - A3dB))] + (vdd / 2.0)
             ).real.item() if leq_0.size > 0 else 0.0
    vih_ac = (vicm[np.argmin(np.abs(geq_0 - A3dB))] + (vdd / 2.0)
             ).real.item() if geq_0.size > 0 else 0.0
    swing  = pd.DataFrame(np.array([[vih_ac, vil_ac ]]), columns = cols)
    return swing
def out_swing_dc(dc1: pandas.core.frame.DataFrame, vdd: float = 1.8, dev: float = 0.0001) ‑> pandas.core.frame.DataFrame
Expand source code
def out_swing_dc( dc1: pd.DataFrame, vdd: float = 1.8, dev: float = 1.0e-4
                ) -> pd.DataFrame:
    cols      = ['v_oh', 'v_ol']
    out       = dc1['OUT'].values
    out_ideal = dc1['OUT_IDEAL'].values
    vid       = dc1['vid'].values
    out_dc    = (out - out[find_closest_idx(vid, 0.0)])
    dev_rel   = np.abs(out_dc - out_ideal) / vdd
    vil_idx   = np.argmin(np.abs(dev_rel[vid <= 0.0] - dev))
    vih_idx   = np.argmin(np.abs(dev_rel[vid >= 0.0] - dev))
    vol_dc    = (out_dc[vil_idx] + (vdd / 2.0)).item()
    voh_dc    = (out_dc[vih_idx] + (vdd / 2.0)).item()
    swing     =  pd.DataFrame(np.array([[voh_dc, vol_dc]]), columns = cols)
    return swing
def output_current(dc3: pandas.core.frame.DataFrame, dc4: pandas.core.frame.DataFrame) ‑> pandas.core.frame.DataFrame
Expand source code
def output_current(dc3: pd.DataFrame, dc4: pd.DataFrame) -> pd.DataFrame:
    cols      = ['i_out_min', 'i_out_max']
    i_out_min = dc3['DUT:O'].values[0].item()
    i_out_max = dc4['DUT:O'].values[0].item()
    current   = pd.DataFrame( np.array([[i_out_min, i_out_max]])
                            , columns = cols )
    return current
def output_referred_noise(noise: pandas.core.frame.DataFrame) ‑> pandas.core.frame.DataFrame
Expand source code
def output_referred_noise(noise: pd.DataFrame) -> pd.DataFrame:
    cols  = ['vn_1Hz', 'vn_10Hz', 'vn_100Hz', 'vn_1kHz', 'vn_10kHz', 'vn_100kHz']
    fs    = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5]
    freq  = noise['freq'].values
    out   = noise['out'].values
    vn    = interpolate.pchip_interpolate(freq, out, fs).reshape(1,-1)
    noise = pd.DataFrame(vn, columns = cols)
    return noise
def rejection(xf: pandas.core.frame.DataFrame) ‑> pandas.core.frame.DataFrame
Expand source code
def rejection(xf: pd.DataFrame) -> pd.DataFrame:
    cols     = ['psrr_p', 'psrr_n', 'cmrr']
    vid_db   = db20(xf['VID'].values)
    vicm_db  = db20(xf['VICM'].values)
    vsupp_db = db20(xf['VSUPP'].values)
    vsupn_db = db20(xf['VSUPN'].values)
    psrr_p   = (vid_db - vsupp_db)[0].item()
    psrr_n   = (vid_db - vsupn_db)[0].item()
    cmrr     = (vid_db - vicm_db)[0].item()
    ratios   = pd.DataFrame( np.array([[psrr_p, psrr_n, cmrr]])
                           , columns = cols )
    return ratios
def repeat_values(d: dict[str, float], n: int) ‑> dict[str, list[float]]
Expand source code
def repeat_values(d: dict[str, float], n: int) -> dict[str, list[float]]:
    return { k: n * [v] for k,v in d }
def setup_dir(pdk_cfg: dict, net_scs: str, ckt_cfg: dict) ‑> str

Setup a temporary simulation directory with testbench and subckt ready to go.

Expand source code
def setup_dir(pdk_cfg: dict, net_scs: str, ckt_cfg: dict) -> str:
    """
    Setup a temporary simulation directory with testbench and subckt ready to go.
    """

    cwd    = os.path.dirname(os.path.abspath(__file__))
    op_id  = os.path.basename(net_scs).split('.')[0]
    usr_id = os.getlogin()

    with open(net_scs, mode = 'r', encoding = 'utf-8') as net_h:
        net = net_h.read()

    includes = '\n'.join( [ f'include "{p["path"]}" section={p["section"]}'
                            for p in pdk_cfg.get('include', {}) ] )

    defaults  =  ckt_cfg.get( 'parameters', {}).get('testbench', {}
                           ) | pdk_cfg.get('testbench', {})

    tb_params = 'parameters ' \
              + ' '.join([ f'{p}={v}' for p,v in defaults.items() ])

    op_params = 'parameters ' \
              + ' '.join([ f'{p}={v}'
                           for p,v in ckt_cfg.get( 'parameters', {}
                                                ).get( 'geometrical'
                                                     , {} ).items() ] )

    area      = ckt_cfg.get('parameters', {}).get('area', '0.0')
    ae_params = f'parameters area={area}' if area != '0.0' else ''

    op_pre    = pdk_cfg['devices']['dcop']['prefix']
    op_suf    = pdk_cfg['devices']['dcop']['suffix']
    op_par    = pdk_cfg['devices']['dcop']['parameters']
    saves     = 'save ' \
              + '\\\n\t'.join([ f'{op_pre}*{op_suf}:{param}'
                                for param in op_par ])

    subckt    = '\n\n'.join([ includes, tb_params, op_params, ae_params
                            , net, saves ])

    tmp_dir   = mkdtemp(prefix = f'{usr_id}_{op_id}_')

    with open(f'{tmp_dir}/op.scs', 'w',  encoding = 'utf-8') as sub_h:
        sub_h.write(subckt)

    copyfile(f'{cwd}/resource/testbench.scs', f'{tmp_dir}/tb.scs')

    return tmp_dir
def stability(stb: pandas.core.frame.DataFrame) ‑> pandas.core.frame.DataFrame
Expand source code
def stability(stb: pd.DataFrame) -> pd.DataFrame:
    cols      = ['a_0', 'ugbw', 'pm', 'gm', 'cof']
    loop_gain = stb['loopGain'].values
    freq      = stb['freq'].values
    gain      = db20(loop_gain)
    phase     = np.angle(loop_gain, deg = True)
    a0_idx    = find_first_idx(gain, 0.0, 'f')
    ph0_idx   = find_first_idx(phase, 0.0, 'f')
    if a0_idx and ph0_idx:
        a0db      = gain[0].item()
        a3db      = a0db - 3.0
        f0db      = freq[a0_idx].real.item()
        f0_idx    = find_closest_idx(freq, f0db)
        pm        = phase[f0_idx].item()
        cof       = freq[ph0_idx].real.item()
        gm        = gain[ph0_idx].item()
    else:
        a0db      = np.nan
        f0db      = np.nan
        pm        = np.nan
        gm        = np.nan
        cof       = np.nan
    stability = pd.DataFrame( np.array([[a0db, f0db, pm, gm, cof]])
                            , columns = cols )
    return stability
def to_dict(df: pandas.core.frame.DataFrame) ‑> dict[str, float]
Expand source code
def to_dict(df: pd.DataFrame) -> dict[str, float]:
    return {k: v[0] for k,v in df.to_dict(orient = 'list').items()}
def transient(tran: pandas.core.frame.DataFrame, vs: float = 0.5) ‑> dict[str, float]
Expand source code
def transient( tran: pd.DataFrame, vs: float = 0.5 ) -> dict[str, float]:
    cols       = ['sr_r', 'sr_f', 'os_r', 'os_f']
    time       = tran['time'].values
    out        = tran['OUT'].values

    idx_100    = find_closest_idx(time, 100e-9)
    idx_090    = find_closest_idx(time, 90e-6)
    idx_050    = find_closest_idx(time, 50e-6)
    idx_099    = find_closest_idx(time, 99.9e-6)

    rising     = out[idx_100:idx_050]
    falling    = out[(idx_050+1):idx_099]

    time_r     = time[idx_100:idx_050]
    time_f     = time[(idx_050+1):idx_099]

    lower      = (0.1 * vs) - (vs / 2.0)
    upper      = (0.9 * vs) - (vs / 2.0)

    sr_rising  = find_sr_r(time_r, rising, upper, lower)
    sr_falling = find_sr_f(time_f, falling, upper, lower)

    os_rising  = 100 * (np.max(rising) - out[idx_050]) / (out[idx_050] - out[idx_100])
    os_falling = 100 * (np.min(falling) - out[idx_090]) / (out[idx_090] - out[idx_050])

    transient  =  pd.DataFrame( np.array([[ sr_rising, sr_falling
                                          , os_rising, os_falling]])
                              , columns = cols )
    return transient
def transpose_dict(ds: Iterable[dict[str, float]]) ‑> dict[str, typing.Iterable[float]]
Expand source code
def transpose_dict(ds: Iterable[dict[str, float]]) -> dict[str, Iterable[float]]:
    keys = {d.keys() for d in ds}
    vals = np.array([list(d.values()) for d in ds]).T.tolist()
    return dict(zip(keys, vals))