Source code for bag3_testbenches.measurement.digital.timing

# SPDX-License-Identifier: Apache-2.0
# Copyright 2019 Blue Cheetah Analog Design Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Any, Union, Sequence, Tuple, Optional, List, Mapping, Callable, Type

from pathlib import Path

import numpy as np

from pybag.core import FileLogger

from bag.io import open_file
from bag.design.module import Module
from bag.simulation.data import AnalysisType, SimData, SimNetlistInfo, netlist_info_from_dict
from bag.simulation.core import TestbenchManager

from ...schematic.generic_tb import bag3_testbenches__generic_tb
from ..data.tran import bits_to_pwl_iter, get_first_crossings, EdgeType


[docs]class CombLogicTimingTB(TestbenchManager): """This class performs timing measurements on combinational logics. Assumptions: 1. tbit is not swept. 2. power supplies are not swept. Notes ----- specification dictionary has the following entries in addition to the default ones: sim_params : Mapping[str, Any] Required entries are listed below. In addition, all input output power domain should have their voltage as parameters here. tbit : float the input data waveform bit period. trf : float input data rise/fall time, as measured with thres_lo/thres_hi. Finally, CombLogicTimingTB will define a tsim parameter that is a function of tbit. thres_lo : float low threshold for rise/fall time calculation, as fraction of VDD. thres_hi : float high threshold for rise/fall time calculation, as fraction of VDD. stimuli_pwr : str stimuli voltage power domain parameter name. save_outputs : Sequence[str] list of nets to save in simulation data file. rtol : float relative tolerance for equality checking in timing measurement. atol : float absolute tolerance for equality checking in timing measurement. tran_options : Mapping[str, Any] transient simulation options dictionary. nbit_delay : int Optional. Delay in number of bits. Defaults to 0. gen_invert : bool Optional. True to generate complementary input waveform on net "inbar". Defaults to False. ctrl_params : Mapping[str, Sequence[str]] Optional. If given, will simulation multiple input pulses, changing control signal values between each pulse. The keys are control signal net names, and values are a list of control signal values for each input pulse. The length of the values list must be the same for all control signals. clk_params : Dict[str, Any] Optional. If specified, generate a clock waveform. It has the following entries: thres_lo : float low threshold for rise/fall time definition. thres_hi : float high threshold for rise/fall time definition. trf : Union[float, str] clock rise/fall time, either number in seconds or variable name. tper : Union[float, str] clock period, either number in seconds or variable name. nper : int number of clock periods to generate. clk_delay : Union[float, str] the clock delay, either number in seconds or variable name. clk_pwr : str the clock power domain parameter name. Defaults to 'vdd'. clk_invert: bool Optional. True to generate inverted clock waveform on net "clkb". Defaults to False. tstep : Optional[float] Optional. The strobe period. Defaults to no strobing. write_numbers : bool Optional. True to write numbers in generated PWL files. Defaults to False. print_delay_list : List[Union[Tuple[str, str], Tuple[str, str, str, str]]] list of delays to print out in summary report. print_trf_list : List[Union[str, Tuple[str, str]]] list of rise/fall times to print out in summary report. """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @classmethod
[docs] def get_schematic_class(cls) -> Type[Module]: return bag3_testbenches__generic_tb
[docs] def pre_setup(self, sch_params: Optional[Mapping[str, Any]]) -> Optional[Mapping[str, Any]]: """Set up PWL waveform files.""" if sch_params is None: return sch_params specs = self.specs thres_lo: float = specs['thres_lo'] thres_hi: float = specs['thres_hi'] write_numbers: bool = specs.get('write_numbers', False) in_pwr: str = specs.get('stimuli_pwr', 'vdd') gen_invert: bool = specs.get('gen_invert', False) clk_invert: bool = specs.get('clk_invert', False) clk_params: Mapping[str, Any] = specs.get('clk_params', {}) ctrl_params: Mapping[str, Sequence[str]] = specs.get('ctrl_params', {}) sim_params = self.sim_params in_pwr_val = sim_params[in_pwr] sch_in_files: Optional[List[Tuple[str, str]]] = sch_params.get('in_file_list', None) sch_clk_files: Optional[List[Tuple[str, str]]] = sch_params.get('clk_file_list', None) swp_info = self.swp_info local_write_numbers = True if swp_info: if isinstance(swp_info, Mapping): if 'tbit' in swp_info or 'trf' in swp_info or in_pwr in swp_info: local_write_numbers = False else: for l in swp_info: if 'tbit' in l or 'trf' in l or in_pwr in l: local_write_numbers = False write_numbers = write_numbers or local_write_numbers in_file_list = [] clk_file_list = [] if sch_in_files: in_file_list.extend(sch_in_files) if sch_clk_files: clk_file_list.extend(sch_clk_files) nbit_delay, num_runs = self._calc_sim_time_info() trf_scale = thres_hi - thres_lo if ctrl_params: for ctrl_sig_name, ctrl_sig_vals in ctrl_params.items(): cur_len = len(ctrl_sig_vals) if cur_len != num_runs: self.error(f'control signal {ctrl_sig_name} values ' f'length = {cur_len} != {num_runs}') sig_path = self.work_dir / f'{ctrl_sig_name}_pwl.txt' _write_pwl_file(sig_path, ctrl_sig_vals, sim_params, 'tbit', 'trf', trf_scale, nbit_delay, write_numbers, tb_scale=3) in_file_list.append((ctrl_sig_name, str(sig_path.resolve()))) # generate PWL waveform files in_data = [0, in_pwr_val, 0] * num_runs in_path = self.work_dir / 'in_pwl.txt' _write_pwl_file(in_path, in_data, sim_params, 'tbit', 'trf', trf_scale, nbit_delay, write_numbers) in_file_list.append(('in', str(in_path.resolve()))) if gen_invert: in_data_bar = [in_pwr_val, 0, in_pwr_val] * num_runs inbar_path = self.work_dir / 'inbar_pwl.txt' _write_pwl_file(inbar_path, in_data_bar, sim_params, 'tbit', 'trf', trf_scale, nbit_delay, write_numbers) in_file_list.append(('inbar', str(inbar_path.resolve()))) if clk_params: clk_thres_hi: float = clk_params['thres_hi'] clk_thres_lo: float = clk_params['thres_lo'] clk_trf: Union[float, str] = clk_params['trf'] clk_tper: Union[float, str] = clk_params['tper'] clk_nper: int = clk_params.get('nper', 3) clk_delay: Union[float, str] = clk_params.get('clk_delay', '') clk_pwr: str = clk_params.get('clk_pwr', 'vdd') clk_pwr_val = sim_params[clk_pwr] clk_data = [0, clk_pwr_val] * clk_nper clk_data.append(0) clk_trf_scale = clk_thres_hi - clk_thres_lo clk_path = self.work_dir / 'in_clk.txt' _write_pwl_file(clk_path, clk_data, sim_params, clk_tper, clk_trf, clk_trf_scale, 0, write_numbers, tb_scale=0.5, delay=clk_delay) clk_file_list.append(('clk', str(clk_path.resolve()))) if clk_invert: clkb_data = ['vdd', '0'] * clk_nper clkb_data.append('vdd') clkb_path = self.work_dir / 'inb_clk.txt' _write_pwl_file(clkb_path, clkb_data, sim_params, clk_tper, clk_trf, clk_trf_scale, 0, write_numbers, tb_scale=0.5, delay=clk_delay) clk_file_list.append(('clkb', str(clkb_path.resolve()))) ans = {k: v for k, v in sch_params.items()} ans['in_file_list'] = in_file_list ans['clk_file_list'] = clk_file_list return ans
[docs] def get_netlist_info(self) -> SimNetlistInfo: # define tsim parameter nbit_delay, num_runs = self._calc_sim_time_info() num_tbit = nbit_delay + 3 * num_runs self.sim_params['tsim'] = f'{num_tbit}*tbit' specs = self.specs tstep: Optional[float] = specs.get('tstep', None) tran_options: Mapping[str, Any] = specs.get('tran_options', {}) save_outputs: Sequence[str] = specs.get('save_outputs', []) tran_dict = dict(type='TRAN', start=0.0, stop='tsim', options=tran_options, save_outputs=save_outputs, ) if tstep is not None: tran_dict['strobe'] = tstep sim_setup = self.get_netlist_info_dict() sim_setup['analyses'] = [tran_dict] return netlist_info_from_dict(sim_setup)
[docs] def print_results(self, data: SimData) -> None: """Override to print results.""" specs = self.specs delay_list: List[Tuple[Any, ...]] = specs.get('print_delay_list', []) trf_list: List[Union[str, Tuple[str, str]]] = specs.get('print_trf_list', []) for ele in delay_list: if len(ele) == 3: out_invert, in_name, out_name = ele in_pwr = out_pwr = 'vdd' elif len(ele) == 5: out_invert, in_name, out_name, in_pwr, out_pwr = ele else: out_invert = False in_name = out_name = in_pwr = out_pwr = '' self.error(f'Unknown print_delay element: {ele}') tdr, tdf = self.calc_output_delay(data, in_name, out_name, out_invert, in_pwr=in_pwr, out_pwr=out_pwr) self.log(f'in_rise {in_name}/{out_name} td(s):\n{tdr}') self.log(f'in_fall {in_name}/{out_name} td(s):\n{tdf}') for ele in trf_list: if isinstance(ele, str): out_name = ele out_pwr = 'vdd' else: out_name, out_pwr = ele tr, tf = self.calc_output_trf(data, out_name, out_pwr=out_pwr) self.log(f'{out_name} tr (s):\n{tr}') self.log(f'{out_name} tf (s):\n{tf}')
[docs] def calc_output_delay(self, data: SimData, in_name: str, out_name: str, out_invert: bool, shape: Optional[Tuple[int, ...]] = None, in_pwr: str = 'vdd', out_pwr: str = 'vdd') -> Tuple[np.ndarray, np.ndarray]: return self.get_output_delay(data, self.specs, in_name, out_name, out_invert, shape=shape, in_pwr=in_pwr, out_pwr=out_pwr)
[docs] def calc_output_trf(self, data: SimData, out_name: str, shape: Optional[Tuple[int, ...]] = None, out_pwr: str = 'vdd', allow_inf: bool = False) -> Tuple[np.ndarray, np.ndarray]: return self.get_output_trf(data, self.specs, out_name, shape=shape, out_pwr=out_pwr, allow_inf=allow_inf, logger=self.logger)
[docs] def _calc_sim_time_info(self) -> Tuple[int, int]: return self._get_sim_time_info(self.specs)
@classmethod
[docs] def get_output_delay(cls, data: SimData, specs: Mapping[str, Any], in_name: str, out_name: str, out_invert: bool, shape: Optional[Tuple[int, ...]] = None, in_pwr: str = 'vdd', out_pwr: str = 'vdd' ) -> Tuple[np.ndarray, np.ndarray]: """Compute delay from simulation data. if the output never resolved correctly, infinity is returned. Parameters ---------- data : SimData Simulation data. specs : Dict[str, Any] testbench specs. in_name : str input signal name. out_name : str output signal name. out_invert : bool True if output is inverted from input. shape : Optional[Tuple[int, ...]] the delay result output shape. in_pwr : str input supply voltage variable name. out_pwr : str output supply voltage variable name. Returns ------- tdr : np.ndarray array of output delay for rising input edge. tdf : np.ndarray array of output delay for falling input edge. """ # TODO: remove sim_params: Mapping[str, float] = specs['sim_params'] rtol: float = specs.get('rtol', 1e-8) atol: float = specs.get('atol', 1e-22) nbit_delay, num_runs = cls._get_sim_time_info(specs) tbit = sim_params['tbit'] vdd_in = sim_params[in_pwr] vdd_out = sim_params[out_pwr] data.open_analysis(AnalysisType.TRAN) tvec = data['time'] in_vec = data[in_name] out_vec = data[out_name] vth_in = vdd_in / 2 vth_out = vdd_out / 2 t0 = nbit_delay * tbit trun = 3 * tbit tdr_list = [] tdf_list = [] for run_idx in range(num_runs): start = t0 + run_idx * trun tdr, tdf = cls.compute_output_delay(tvec, in_vec, out_vec, vth_in, vth_out, out_invert, start=start, stop=start + trun, rtol=rtol, atol=atol, shape=shape) tdr_list.append(tdr) tdf_list.append(tdf) if num_runs == 1: return tdr_list[0], tdf_list[0] return np.array(tdr_list), np.array(tdf_list)
@classmethod
[docs] def get_output_trf(cls, data: SimData, specs: Mapping[str, Any], out_name: str, shape: Optional[Tuple[int, ...]] = None, out_pwr: str = 'vdd', allow_inf: bool = False, logger: Optional[FileLogger] = None ) -> Tuple[np.ndarray, np.ndarray]: """Compute output rise/fall time from simulation data. if output never crosses the high threshold, infinity is returned. If output never crosses the low threshold, nan is returned. Parameters ---------- data : SimData Simulation data. specs : Dict[str, Any] testbench specs. out_name : str output signal name. shape : Optional[Tuple[int, ...]] the delay result output shape. out_pwr : str output supply voltage variable name. allow_inf: bool Turns off error checking for infinity values Useful for really slow rise/fall times and/or large Cload where the transition is not complete logger : Optional[FileLogger] the optional logger object. Returns ------- tdr : np.ndarray array of output delay for rising input edge. tdf : np.ndarray array of output delay for falling input edge. """ # TODO: remove sim_params: Mapping[str, float] = specs['sim_params'] thres_lo: float = specs['thres_lo'] thres_hi: float = specs['thres_hi'] rtol: float = specs.get('rtol', 1e-8) atol: float = specs.get('atol', 1e-22) nbit_delay, num_runs = cls._get_sim_time_info(specs) vdd_out = sim_params[out_pwr] tbit = sim_params['tbit'] data.open_analysis(AnalysisType.TRAN) tvec = data['time'] yvec = data[out_name] vlo = vdd_out * thres_lo vhi = vdd_out * thres_hi t0 = nbit_delay * tbit trun = 3 * tbit tr_list = [] tf_list = [] for run_idx in range(num_runs): start = t0 + run_idx * trun tr, tf = cls.compute_output_trf(tvec, yvec, vlo, vhi, start=start, stop=start + trun, rtol=rtol, atol=atol, shape=shape, allow_inf=allow_inf, logger=logger) tr_list.append(tr) tf_list.append(tf) if num_runs == 1: return tr_list[0], tf_list[0] return np.array(tr_list), np.array(tf_list)
@classmethod
[docs] def compute_output_delay(cls, tvec: np.ndarray, in_vec: np.ndarray, out_vec: np.ndarray, vth_in: float, vth_out: float, out_invert: bool, **kwargs ) -> Tuple[np.ndarray, np.ndarray]: in_r = get_first_crossings(tvec, in_vec, vth_in, etype=EdgeType.RISE, **kwargs) in_f = get_first_crossings(tvec, in_vec, vth_in, etype=EdgeType.FALL, **kwargs) out_r = get_first_crossings(tvec, out_vec, vth_out, etype=EdgeType.RISE, **kwargs) out_f = get_first_crossings(tvec, out_vec, vth_out, etype=EdgeType.FALL, **kwargs) if out_invert: out_f -= in_r out_r -= in_f return out_f, out_r else: out_r -= in_r out_f -= in_f return out_r, out_f
@classmethod
[docs] def compute_output_trf(cls, tvec: np.ndarray, yvec: np.ndarray, vlo: float, vhi: float, allow_inf: bool = False, logger: Optional[FileLogger] = None, **kwargs: Any) -> Tuple[np.ndarray, np.ndarray]: print_fun = print if logger is None else logger.warn tr0 = get_first_crossings(tvec, yvec, vlo, etype=EdgeType.RISE, **kwargs) tr1 = get_first_crossings(tvec, yvec, vhi, etype=EdgeType.RISE, **kwargs) tf0 = get_first_crossings(tvec, yvec, vhi, etype=EdgeType.FALL, **kwargs) tf1 = get_first_crossings(tvec, yvec, vlo, etype=EdgeType.FALL, **kwargs) tr = cls._trf_helper(tr0, tr1, allow_inf, 'trise', print_fun) tf = cls._trf_helper(tf0, tf1, allow_inf, 'tfall', print_fun) return tr, tf
@classmethod
[docs] def _trf_helper(cls, t0: np.ndarray, t1: np.ndarray, allow_inf: bool, tag: str, print_fun: Callable[[str], None]) -> np.ndarray: has_nan = np.isnan(t0).any() or np.isnan(t1).any() has_inf = np.isinf(t0).any() or np.isinf(t1).any() if has_nan or (has_inf and not allow_inf): print_fun(f'Got an invalid value in computing {tag}, values:') print_fun(str(t0)) print_fun(str(t1)) t1 = np.full(t1.shape, np.inf) else: t1 -= t0 return t1
@classmethod
[docs] def _get_sim_time_info(cls, specs: Mapping[str, Any]) -> Tuple[int, int]: # TODO: remove nbit_delay: int = specs.get('nbit_delay', 0) ctrl_params: Optional[Mapping[str, Sequence[str]]] = specs.get('ctrl_params', None) num_runs = len(next(iter(ctrl_params.values()))) if ctrl_params is not None else 1 return nbit_delay, num_runs
[docs]def _write_pwl_file(path: Path, data: Union[Sequence[str], Sequence[float]], sim_params: Mapping[str, Union[float, str]], tbit: Union[float, str], trf: Union[float, str], trf_scale: float, nbit_delay: int, write_numbers: bool, tb_scale: float = 1, delay: Union[float, str] = '') -> None: with open_file(path, 'w') as f: if write_numbers: tbit_val = sim_params[tbit] if isinstance(tbit, str) else tbit trf_val = sim_params[trf] if isinstance(trf, str) else trf if isinstance(delay, str): delay_val = sim_params[delay] if delay else 0 else: delay_val = delay if isinstance(tbit_val, str) or isinstance(trf_val, str) or isinstance(delay_val, str): raise ValueError('Cannot write numeric pwl file.') for _, s_tb, s_tr, val in bits_to_pwl_iter(data): time_val = (delay_val + tbit_val * (tb_scale * s_tb + nbit_delay) + trf_val * s_tr / trf_scale) f.write(f'{time_val} {val}\n') else: prefix = delay + '+' if delay else '' for _, s_tb, s_tr, val in bits_to_pwl_iter(data): f.write(f'{prefix}{tbit}*{tb_scale * s_tb + nbit_delay:.4g}+'
f'{trf}*({s_tr / trf_scale:.4g}) {val}\n')