# BSD 3-Clause License
#
# Copyright (c) 2018, Regents of the University of California
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""This package contains measurement class for transistors."""
from typing import Optional, Tuple, Dict, Any, List, Mapping, Sequence, Union, Type, cast
import math
from pathlib import Path
from copy import deepcopy
import numpy as np
import scipy.interpolate as interp
import scipy.optimize as sciopt
from bag.concurrent.util import GatherHelper
from bag.design.module import Module
from bag.io.file import write_yaml
from bag.io.sim_data import save_sim_results, load_sim_file
from bag.math.interpolate import LinearInterpolator
from bag.simulation.cache import SimulationDB, DesignInstance
from bag.simulation.core import TestbenchManager
from bag.simulation.data import AnalysisType, SimNetlistInfo, SimData, AnalysisData, netlist_info_from_dict
from bag.simulation.measure import MeasurementManager
from ...schematic.mos_tb_ibias import bag3_testbenches__mos_tb_ibias
from ...schematic.mos_tb_sp import bag3_testbenches__mos_tb_sp
from ...schematic.mos_tb_noise import bag3_testbenches__mos_tb_noise
[docs]class MOSIdTB(TestbenchManager):
"""This class sets up the transistor drain current measurement testbench.
"""
@classmethod
[docs] def get_schematic_class(cls) -> Type[Module]:
return bag3_testbenches__mos_tb_ibias
[docs] def get_netlist_info(self) -> SimNetlistInfo:
dc_dict = dict(type='DC')
sim_setup = self.get_netlist_info_dict()
sim_setup['analyses'] = [dc_dict]
return netlist_info_from_dict(sim_setup)
[docs] def pre_setup(self, sch_params: Optional[Mapping[str, Any]]):
self.sim_params['vs'] = 0
vgs_max = self.specs['vgs_max']
vgs_min = self.specs.get('vgs_min', 0)
vgs_num = self.specs['vgs_num']
if self.specs['is_nmos']:
vgs_start, vgs_stop = vgs_min, vgs_max
else:
vgs_start, vgs_stop = -vgs_max, -vgs_min
self.set_swp_info([
('vgs', dict(type='LINEAR', start=vgs_start, stop=vgs_stop, num=vgs_num))
])
return super().pre_setup(sch_params)
@classmethod
[docs] def get_vgs_range(cls, data: SimData, ibias_min_seg: float, ibias_max_seg: float, vgs_resolution: float,
seg: int, is_nmos: bool, **kwargs: Dict[str, Any]) -> Tuple[float, float]:
# invert NMOS ibias sign
ibias_sgn = -1.0 if is_nmos else 1.0
vgs = data['vgs']
ibias_key = 'VD:p'
ibias = data[ibias_key] * ibias_sgn
# assume first sweep parameter is corner, second sweep parameter is vgs
try:
corner_idx = data.sweep_params.index('corner')
ivec_max = np.amax(ibias, corner_idx)
ivec_min = np.amin(ibias, corner_idx)
except ValueError:
ivec_max = ivec_min = ibias
vgs1 = cls._get_best_crossing(vgs, ivec_max, ibias_min_seg * seg)
vgs2 = cls._get_best_crossing(vgs, ivec_min, ibias_max_seg * seg)
vgs_min = min(vgs1, vgs2)
vgs_max = max(vgs1, vgs2)
vgs_min = math.floor(vgs_min / vgs_resolution) * vgs_resolution
vgs_max = math.ceil(vgs_max / vgs_resolution) * vgs_resolution
return vgs_min, vgs_max
@classmethod
[docs] def _get_best_crossing(cls, xvec, yvec, val):
interp_fun = interp.InterpolatedUnivariateSpline(xvec, yvec)
def fzero(x):
return interp_fun(x) - val
xstart, xstop = xvec[0], xvec[-1]
try:
return sciopt.brentq(fzero, xstart, xstop)
except ValueError:
# avoid no solution
if abs(fzero(xstart)) < abs(fzero(xstop)):
return xstart
return xstop
[docs]class MOSSPTB(TestbenchManager):
"""This class sets up the transistor S parameter measurement testbench.
"""
@classmethod
[docs] def get_schematic_class(cls) -> Type[Module]:
return bag3_testbenches__mos_tb_sp
[docs] def get_netlist_info(self) -> SimNetlistInfo:
dc_dict = dict(type='DC')
sp_dict = dict(type='SP',
freq=self.specs['sp_freq'],
ports=['PORTG', 'PORTD', 'PORTS'],
param_type='Y')
sim_setup = self.get_netlist_info_dict()
sim_setup['analyses'] = [dc_dict, sp_dict]
return netlist_info_from_dict(sim_setup)
[docs] def pre_setup(self, sch_params: Optional[Mapping[str, Any]]) -> Optional[Mapping[str, Any]]:
is_nmos = self.specs['is_nmos']
vbs_val = self.specs['vbs']
vds_min = self.specs['vds_min']
vds_max = self.specs['vds_max']
vds_num = self.specs['vds_num']
vgs_num = self.specs['vgs_num']
vgs_start, vgs_stop = self.specs['vgs_range']
# Add VGS sweep
swp_info = [('vgs', dict(type='LINEAR', start=vgs_start, stop=vgs_stop, num=vgs_num))]
# handle VBS sign and set parameters.
if isinstance(vbs_val, list):
if is_nmos:
vbs_val = sorted((-abs(v) for v in vbs_val))
else:
vbs_val = sorted((abs(v) for v in vbs_val))
swp_info.append(('vbs', dict(type='LIST', values=vbs_val)))
else:
if is_nmos:
vbs_val = -abs(vbs_val)
else:
vbs_val = abs(vbs_val)
self.sim_params['vbs'] = vbs_val
# handle VDS/VGS sign for nmos/pmos
if is_nmos:
self.sim_params['vb_dc'] = 0
vds_start, vds_stop = vds_min, vds_max
else:
if vds_max > vds_min:
print('vds_max = {:.4g} > {:.4g} = vds_min, flipping sign'.format(vds_max, vds_min))
vds_start, vds_stop = -vds_max, -vds_min
else:
vds_start, vds_stop = vds_min, vds_max
self.sim_params['vb_dc'] = abs(vgs_start)
swp_info.append(('vds', dict(type='LINEAR', start=vds_start, stop=vds_stop, num=vds_num)))
self.set_swp_info(swp_info)
return super().pre_setup(sch_params)
@classmethod
[docs] def get_ss_params(cls, data: SimData, sim_envs: List[str], cfit_method: str, sp_freq: float, seg: int,
is_nmos: bool, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
swp_vars = data.sweep_params
data.open_analysis(AnalysisType.DC)
# invert NMOS ibias sign
ibias_sgn = -1.0 if is_nmos else 1.0
ibias_key = 'VD:p'
ibias = data[ibias_key] * ibias_sgn
data.open_analysis(AnalysisType.SP)
ss_dict = cls.mos_y_to_ss(data, sp_freq, seg, ibias, cfit_method=cfit_method)
new_result = {}
new_shape = list(data.data_shape)
del new_shape[data.sweep_params.index('freq')]
sweep_params = {}
for key, val in ss_dict.items():
new_result[key] = val.reshape(new_shape)
sweep_params[key] = swp_vars
new_result['corner'] = np.array(sim_envs)
for var in swp_vars:
if var == 'corner':
continue
new_result[var] = data[var]
new_result['sweep_params'] = sweep_params
return new_result
@classmethod
[docs] def mos_y_to_ss(cls, sim_data: SimData, char_freq: float, seg: int, ibias: np.ndarray,
cfit_method: str = 'average') -> Dict[str, np.ndarray]:
"""Convert transistor Y parameters to small-signal parameters.
This function computes MOSFET small signal parameters from 3-port
Y parameter measurements done on gate, drain and source, with body
bias fixed. This functions fits the Y parameter to a capcitor-only
small signal model using least-mean-square error.
Parameters
----------
sim_data : Dict[str, np.ndarray]
A dictionary of Y parameters values stored as complex numpy arrays.
char_freq : float
the frequency Y parameters are measured at.
seg : int
number of transistor fingers used for the Y parameter measurement.
ibias : np.ndarray
the DC bias current of the transistor. Always positive.
cfit_method : str
method used to extract capacitance from Y parameters. Currently
supports 'average' or 'worst'
Returns
-------
ss_dict : Dict[str, np.ndarray]
A dictionary of small signal parameter values stored as numpy
arrays. These values are normalized to 1-finger transistor.
"""
w = 2 * np.pi * char_freq
gm = (sim_data['y21'].real - sim_data['y31'].real) / 2.0
gds = (sim_data['y22'].real - sim_data['y32'].real) / 2.0
gb = (sim_data['y33'].real - sim_data['y23'].real) / 2.0 - gm - gds
cgd12 = -sim_data['y12'].imag / w
cgd21 = -sim_data['y21'].imag / w
cgs13 = -sim_data['y13'].imag / w
cgs31 = -sim_data['y31'].imag / w
cds23 = -sim_data['y23'].imag / w
cds32 = -sim_data['y32'].imag / w
cgg = sim_data['y11'].imag / w
cdd = sim_data['y22'].imag / w
css = sim_data['y33'].imag / w
if cfit_method == 'average':
cgd = (cgd12 + cgd21) / 2
cgs = (cgs13 + cgs31) / 2
cds = (cds23 + cds32) / 2
elif cfit_method == 'worst':
cgd = np.maximum(cgd12, cgd21)
cgs = np.maximum(cgs13, cgs31)
cds = np.maximum(cds23, cds32)
else:
raise ValueError('Unknown cfit_method = %s' % cfit_method)
cgb = cgg - cgd - cgs
cdb = cdd - cds - cgd
csb = css - cgs - cds
ibias = ibias / seg
gm = gm / seg
gds = gds / seg
gb = gb / seg
cgd = cgd / seg
cgs = cgs / seg
cds = cds / seg
cgb = cgb / seg
cdb = cdb / seg
csb = csb / seg
return dict(
ibias=ibias,
gm=gm,
gds=gds,
gb=gb,
cgd=cgd,
cgs=cgs,
cds=cds,
cgb=cgb,
cdb=cdb,
csb=csb,
)
[docs]class MOSNoiseTB(TestbenchManager):
"""This class sets up the transistor small-signal noise measurement testbench.
"""
@classmethod
[docs] def get_schematic_class(cls) -> Type[Module]:
return bag3_testbenches__mos_tb_noise
[docs] def get_netlist_info(self) -> SimNetlistInfo:
freq_start: float = self.specs['freq_start']
freq_stop: float = self.specs['freq_stop']
num = np.rint(np.log10(freq_stop / freq_start) * self.specs['num_per_dec'])
noise_dict = dict(type='NOISE',
param='freq',
sweep=dict(
type='LOG',
start=freq_start,
stop=freq_stop,
num=num,
endpoint=True
),
# save_outputs=save_outputs,
out_probe='VD'
)
sim_setup = self.get_netlist_info_dict()
sim_setup['analyses'] = [noise_dict]
return netlist_info_from_dict(sim_setup)
[docs] def pre_setup(self, sch_params: Optional[Mapping[str, Any]]) -> Optional[Mapping[str, Any]]:
is_nmos = self.specs['is_nmos']
vbs_val = self.specs['vbs']
vds_min = self.specs['vds_min']
vds_max = self.specs['vds_max']
vds_num = self.specs['vds_num']
vgs_num = self.specs['vgs_num']
vgs_start, vgs_stop = self.specs['vgs_range']
# Add VGS sweep
swp_info = [('vgs', dict(type='LINEAR', start=vgs_start, stop=vgs_stop, num=vgs_num))]
# handle VBS sign and set parameters.
if isinstance(vbs_val, list):
if is_nmos:
vbs_val = sorted((-abs(v) for v in vbs_val))
else:
vbs_val = sorted((abs(v) for v in vbs_val))
swp_info.append(('vbs', dict(type='LIST', values=vbs_val)))
else:
if is_nmos:
vbs_val = -abs(vbs_val)
else:
vbs_val = abs(vbs_val)
self.sim_params['vbs'] = vbs_val
# handle VDS/VGS sign for nmos/pmos
if is_nmos:
self.sim_params['vb_dc'] = 0
vds_start, vds_stop = vds_min, vds_max
else:
if vds_max > vds_min:
print('vds_max = {:.4g} > {:.4g} = vds_min, flipping sign'.format(vds_max, vds_min))
vds_start, vds_stop = -vds_max, -vds_min
else:
vds_start, vds_stop = vds_min, vds_max
self.sim_params['vb_dc'] = abs(vgs_start)
swp_info.append(('vds', dict(type='LINEAR', start=vds_start, stop=vds_stop, num=vds_num)))
self.set_swp_info(swp_info)
return super().pre_setup(sch_params)
@classmethod
[docs] def get_integrated_noise(cls, data: SimData, ss_data: Dict[str, Any], freq_start: float, freq_stop: float, seg: int,
scale: float = 1.0, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
data.open_analysis(AnalysisType.NOISE)
ss_data_swp_order = ss_data['sweep_params']['gm']
idn = data['out']
# rearrange array axis
old_swp_order = data.sweep_params
new_swp_order = list(ss_data_swp_order) + ['freq']
transposed_order = [new_swp_order.index(name) for name in old_swp_order]
idn = np.transpose(idn, axes=transposed_order)
noise_swp_vars = new_swp_order
corner_list = data.sim_envs
if not np.all(ss_data['corner'] == corner_list):
raise ValueError("Inconsistent corners between noise simulation and previous simulations")
cur_points = [data[name] for name in noise_swp_vars[1:]]
cur_points[-1] = np.log(data['freq'])
# construct new SS parameter result dictionary
fstart_log = np.log(freq_start)
fstop_log = np.log(freq_stop)
# rearrange array axis
idn = np.log(scale / seg * (idn ** 2))
delta_list = [1e-6] * (len(noise_swp_vars) - 1) # TODO: don't hardcode delta_list
delta_list[-1] = 1e-3
integ_noise_list = []
for idx in range(len(corner_list)):
noise_fun = LinearInterpolator(cur_points, idn[idx, ...], delta_list, extrapolate=True)
integ_noise_list.append(noise_fun.integrate(fstart_log, fstop_log, axis=-1, logx=True, logy=True, raw=True))
# get temperatures from sim_envs
gm = ss_data['gm']
temp = np.ones(gm.shape)
for idx, sim_env in enumerate(data.sim_envs):
_temp = sim_env.split('_')[1]
if _temp[0] == 'm':
temp[idx] *= 273 - float(_temp[1:])
else:
temp[idx] *= 273 + float(_temp)
gamma = np.array(integ_noise_list) / (4.0 * 1.38e-23 * temp * gm * (freq_stop - freq_start))
new_result = deepcopy(ss_data)
new_result['gamma'] = gamma
new_result['sweep_params']['gamma'] = noise_swp_vars[:-1]
return new_result