def __init__(self, yaml_fname: str, database: ModuleDB, params: Param, *, copy_state: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None: self._cv: Optional[PySchCellView] = None if copy_state: self._netlist_dir = copy_state['netlist_dir'] self._cv = copy_state['cv'] self._pins: Dict[str, TermType] = copy_state['pins'] self._orig_cell_name = copy_state['orig_cell_name'] self.instances: Dict[str, SchInstance] = copy_state['instances'] else: self._pins: Dict[str, TermType] = {} if yaml_fname: # normal schematic yaml_fname = os.path.abspath(yaml_fname) self._netlist_dir = os.path.dirname(yaml_fname) self._cv = PySchCellView(yaml_fname, 'symbol') self._orig_cell_name = self._cv.cell_name self.instances: Dict[str, SchInstance] = {name: SchInstance(database, ref) for name, ref in self._cv.inst_refs()} if not self.is_primitive(): self._cv.lib_name = database.lib_name else: # empty yaml file name, this is a BAG primitive self._netlist_dir = '' self._orig_cell_name = self.__class__.__name__.split('__')[1] self.instances: Dict[str, SchInstance] = {} # initialize schematic master DesignMaster.__init__(self, database, params, copy_state=copy_state, **kwargs)
class Module(DesignMaster): """The base class of all schematic generators. This represents a schematic master. This class defines all the methods needed to implement a design in the CAD database. Parameters ---------- yaml_fname : str the netlist information file name. database : ModuleDB the design database object. params : Param the parameters dictionary. copy_state : Optional[Dict[str, Any]] If not None, set content of this master from this dictionary. **kwargs : Any optional arguments """ def __init__(self, yaml_fname: str, database: ModuleDB, params: Param, *, copy_state: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None: self._cv: Optional[PySchCellView] = None if copy_state: self._netlist_dir: Optional[Path] = copy_state['netlist_dir'] self._cv = copy_state['cv'] self._pins: Dict[str, TermType] = copy_state['pins'] self._orig_lib_name = copy_state['orig_lib_name'] self._orig_cell_name = copy_state['orig_cell_name'] self.instances: Dict[str, SchInstance] = copy_state['instances'] else: self._pins: Dict[str, TermType] = {} if yaml_fname: # normal schematic yaml_path = Path(yaml_fname).resolve() self._netlist_dir: Optional[Path] = yaml_path.parent self._cv = PySchCellView(str(yaml_path), 'symbol') self._orig_lib_name = self._cv.lib_name self._orig_cell_name = self._cv.cell_name self.instances: Dict[str, SchInstance] = {name: SchInstance(database, ref) for name, ref in self._cv.inst_refs()} if not self.is_primitive(): self._cv.lib_name = database.lib_name else: # empty yaml file name, this is a BAG primitive self._netlist_dir: Optional[Path] = None self._orig_lib_name, self._orig_cell_name = self.__class__.__name__.split('__') self.instances: Dict[str, SchInstance] = {} # initialize schematic master DesignMaster.__init__(self, database, params, copy_state=copy_state, **kwargs) @classmethod def get_hidden_params(cls) -> Dict[str, Any]: ans = DesignMaster.get_hidden_params() ans['model_params'] = None return ans @classmethod def is_primitive(cls) -> bool: """Returns True if this Module represents a BAG primitive. NOTE: This method is only used by BAG and schematic primitives. This method prevents the module from being copied during design implementation. Custom subclasses should not override this method. Returns ------- is_primitive : bool True if this Module represents a BAG primitive. """ return False @classmethod def is_leaf_model(cls) -> bool: """Returns True if this class is always the leaf model cell.""" return False @property def sch_db(self) -> ModuleDB: # noinspection PyTypeChecker return self.master_db def get_master_basename(self) -> str: return self.orig_cell_name def get_copy_state_with(self, new_params: Param) -> Dict[str, Any]: base = DesignMaster.get_copy_state_with(self, new_params) new_cv = self._cv.get_copy() new_inst = {name: SchInstance(self.sch_db, ref, master=self.instances[name].master) for name, ref in new_cv.inst_refs()} base['netlist_dir'] = self._netlist_dir base['cv'] = new_cv base['pins'] = self._pins.copy() base['orig_lib_name'] = self._orig_lib_name base['orig_cell_name'] = self._orig_cell_name base['instances'] = new_inst return base @property def tech_info(self) -> TechInfo: return self.master_db.tech_info @property def sch_scale(self) -> float: tech_info = self.master_db.tech_info return tech_info.resolution * tech_info.layout_unit @property def pins(self) -> Dict[str, TermType]: return self._pins @abc.abstractmethod def design(self, **kwargs: Any) -> None: """To be overridden by subclasses to design this module. To design instances of this module, you can call their :meth:`.design` method or any other ways you coded. To modify schematic structure, call: :meth:`.rename_pin` :meth:`.delete_instance` :meth:`.replace_instance_master` :meth:`.reconnect_instance_terminal` :meth:`.array_instance` """ pass def design_model(self, key: Any) -> None: self.update_signature(key) self._cv.cell_name = self.cell_name model_params = self.params['model_params'] if 'view_name' not in model_params: # this is a hierarchical model if not self.instances: # found a leaf cell with no behavioral model raise ValueError('Schematic master has no instances and no behavioral model.') self.clear_children_key() master_db = self.master_db for name, inst in self.instances.items(): if master_db.exclude_model(inst.lib_name, inst.cell_name): continue cur_params: Optional[Param] = model_params.get(name, None) if cur_params is None: raise ValueError('Cannot find model parameters for instance {}'.format(name)) inst.design_model(cur_params) if not inst.is_primitive: self.add_child_key(inst.master_key) def set_param(self, key: str, val: Union[int, float, bool, str]) -> None: """Set schematic parameters for this master. This method is only used to set parameters for BAG primitives. Parameters ---------- key : str parameter name. val : Union[int, float, bool, str] parameter value. """ self._cv.set_param(key, val) def finalize(self) -> None: """Finalize this master instance. """ # invoke design function, excluding model_params args = dict((k, v) for k, v in self.params.items() if k != 'model_params') self.design(**args) # get set of children master keys for name, inst in self.instances.items(): if not inst.is_valid: raise ValueError(f'Schematic instance {name} is not valid. ' 'Did you forget to call design()?') if not inst.is_primitive: # NOTE: only non-primitive instance can have ports change try: inst.check_connections() except RuntimeError as err: raise RuntimeError(f'Error checking connection of instance {name}') from err self.add_child_key(inst.master_key) if self._cv is not None: # get pins self._pins = {k: TermType(v) for k, v in self._cv.terminals()} # update cell name self._cv.cell_name = self.cell_name # call super finalize routine DesignMaster.finalize(self) def get_content(self, output_type: DesignOutput, rename_dict: Dict[str, str], name_prefix: str, name_suffix: str, shell: bool, exact_cell_names: Set[str], supply_wrap_mode: SupplyWrapMode) -> Tuple[str, Any]: if not self.finalized: raise ValueError('This module is not finalized yet') cell_name = format_cell_name(self.cell_name, rename_dict, name_prefix, name_suffix, exact_cell_names, supply_wrap_mode) if self.is_primitive(): return cell_name, (None, '') netlist = '' if not shell and output_type.is_model: # NOTE: only get model netlist if we're doing real netlisting (versus shell netlisting) model_params: Optional[Param] = self.params['model_params'] if model_params is None: # model parameters is unset. This happens if a behavioral model view is used # at a top level block, and this cell gets shadows out. # If this is the case, just return None so this cellview won't be netlisted. return cell_name, (None, '') view_name: Optional[str] = model_params.get('view_name', None) if view_name is not None: fpath = self.get_model_path(output_type, view_name) template = self.sch_db.get_model_netlist_template(fpath) netlist = template.render(_header=get_cv_header(self._cv, cell_name, output_type), _sch_params=self.params, _pins=self.pins, _cell_name=cell_name, **model_params) return cell_name, (self._cv, netlist) @property def cell_name(self) -> str: """The master cell name.""" if self.is_primitive(): return self.get_cell_name_from_parameters() return super(Module, self).cell_name @property def orig_lib_name(self) -> str: """The original schematic template library name.""" return self._orig_lib_name @property def orig_cell_name(self) -> str: """The original schematic template cell name.""" return self._orig_cell_name def get_model_path(self, output_type: DesignOutput, view_name: str = '') -> Path: """Returns the model file path.""" if view_name: basename = f'{self.orig_cell_name}.{view_name}' else: basename = self.orig_cell_name file_name = f'{basename}.{output_type.extension}' path: Path = self._netlist_dir.parent / 'models' / file_name if not path.is_file(): fallback_type = output_type.fallback_model_type if fallback_type is not output_type: # if there is a fallback model type defined, try to return that model file # instead. test_path = path.with_name(f'{basename}.{fallback_type.extension}') if test_path.is_file(): return test_path return path def should_delete_instance(self) -> bool: """Returns True if this instance should be deleted based on its parameters. This method is mainly used to delete 0 finger or 0 width transistors. However, You can override this method if there exists parameter settings which corresponds to an empty schematic. Returns ------- delete : bool True if parent should delete this instance. """ return False def get_schematic_parameters(self) -> Dict[str, str]: """Returns the schematic parameter dictionary of this instance. NOTE: This method is only used by BAG primitives, as they are implemented with parameterized cells in the CAD database. Custom subclasses should not override this method. Returns ------- params : Dict[str, str] the schematic parameter dictionary. """ return {} def get_cell_name_from_parameters(self) -> str: """Returns new cell name based on parameters. NOTE: This method is only used by BAG primitives. This method enables a BAG primitive to change the cell master based on design parameters (e.g. change transistor instance based on the intent parameter). Custom subclasses should not override this method. Returns ------- cell : str the cell name based on parameters. """ return self.orig_cell_name def rename_pin(self, old_pin: str, new_pin: str) -> None: """Renames an input/output pin of this schematic. NOTE: Make sure to call :meth:`.reconnect_instance_terminal` so that instances are connected to the new pin. Parameters ---------- old_pin : str the old pin name. new_pin : str the new pin name. """ self._cv.rename_pin(old_pin, new_pin) def add_pin(self, new_pin: str, pin_type: Union[TermType, str], sig_type: SigType = SigType.signal) -> None: """Adds a new pin to this schematic. NOTE: Make sure to call :meth:`.reconnect_instance_terminal` so that instances are connected to the new pin. Parameters ---------- new_pin : str the new pin name. pin_type : Union[TermType, str] the new pin type. sig_type : SigType the signal type of the pin. """ if isinstance(pin_type, str): pin_type = TermType[pin_type] self._cv.add_pin(new_pin, pin_type.value, sig_type.value) def get_signal_type(self, pin_name: str) -> SigType: if not self.finalized: raise ValueError('This method only works on finalized master.') return self._cv.get_signal_type(pin_name) def remove_pin(self, remove_pin: str) -> bool: """Removes a pin from this schematic. Parameters ---------- remove_pin : str the pin to remove. Returns ------- success : bool True if the pin is successfully found and removed. """ return self._cv.remove_pin(remove_pin) def set_pin_attribute(self, pin_name: str, key: str, val: str) -> None: """Set an attribute on the given pin. Parameters ---------- pin_name : str the pin name. key : str the attribute name. val : str the attribute value. """ self._cv.set_pin_attribute(pin_name, key, val) def rename_instance(self, old_name: str, new_name: str, conn_list: Optional[Union[Iterable[Tuple[str, str]], ItemsView[str, str]]] = None) -> None: """Renames an instance in this schematic. Parameters ---------- old_name : str the old instance name. new_name : str the new instance name. conn_list : Optional[Union[Iterable[Tuple[str, str]], ItemsView[str, str]]] an optional connection list. """ self._cv.rename_instance(old_name, new_name) self.instances[new_name] = inst = self.instances.pop(old_name) if conn_list: for term, net in conn_list: inst.update_connection(new_name, term, net) def remove_instance(self, inst_name: str) -> bool: """Removes the instance with the given name. Parameters ---------- inst_name : str the child instance to delete. Returns ------- success : bool True if the instance is successfully found and removed. """ success = self._cv.remove_instance(inst_name) if success: del self.instances[inst_name] return success def delete_instance(self, inst_name: str) -> bool: """Delete the instance with the given name. This method is identical to remove_instance(). It's here only for backwards compatibility. """ return self.remove_instance(inst_name) def replace_instance_master(self, inst_name: str, lib_name: str, cell_name: str, static: bool = False, keep_connections: bool = False) -> None: """Replace the master of the given instance. NOTE: all terminal connections will be reset. Call reconnect_instance_terminal() to modify terminal connections. Parameters ---------- inst_name : str the child instance to replace. lib_name : str the new library name. cell_name : str the new cell name. static : bool True if we're replacing instance with a static schematic instead of a design module. keep_connections : bool True to keep the old connections when the instance master changed. """ if inst_name not in self.instances: raise ValueError('Cannot find instance with name: %s' % inst_name) self.instances[inst_name].change_generator(lib_name, cell_name, static=static, keep_connections=keep_connections) def reconnect_instance_terminal(self, inst_name: str, term_name: str, net_name: str) -> None: """Reconnect the instance terminal to a new net. Parameters ---------- inst_name : str the instance to modify. term_name : str the instance terminal name to reconnect. net_name : str the net to connect the instance terminal to. """ inst = self.instances.get(inst_name, None) if inst is None: raise ValueError('Cannot find instance {}'.format(inst_name)) inst.update_connection(inst_name, term_name, net_name) def reconnect_instance(self, inst_name: str, term_net_iter: Union[Iterable[Tuple[str, str]], ItemsView[str, str]]) -> None: """Reconnect all give instance terminals Parameters ---------- inst_name : str the instance to modify. term_net_iter : Union[Iterable[Tuple[str, str]], ItemsView[str, str]] an iterable of (term, net) tuples. """ inst = self.instances.get(inst_name, None) if inst is None: raise ValueError('Cannot find instance {}'.format(inst_name)) for term, net in term_net_iter: inst.update_connection(inst_name, term, net) def array_instance(self, inst_name: str, inst_name_list: Optional[List[str]] = None, term_list: Optional[List[Dict[str, str]]] = None, inst_term_list: Optional[List[Tuple[str, Iterable[Tuple[str, str]]]]] = None, dx: int = 0, dy: int = 0) -> None: """Replace the given instance by an array of instances. This method will replace self.instances[inst_name] by a list of Modules. The user can then design each of those modules. Parameters ---------- inst_name : str the instance to array. inst_name_list : Optional[List[str]] a list of the names for each array item. term_list : Optional[List[Dict[str, str]]] a list of modified terminal connections for each array item. The keys are instance terminal names, and the values are the net names to connect them to. Only terminal connections different than the parent instance should be listed here. If None, assume terminal connections are not changed. inst_term_list : Optional[List[Tuple[str, List[Tuple[str, str]]]]] zipped version of inst_name_list and term_list. If given, this is used instead. dx : int the X coordinate shift. If dx = dy = 0, default to shift right. dy : int the Y coordinate shift. If dx = dy = 0, default to shift right. """ if inst_term_list is None: if inst_name_list is None: raise ValueError('inst_name_list cannot be None if inst_term_iter is None.') # get instance/terminal list iterator if term_list is None: inst_term_list = zip_longest(inst_name_list, [], fillvalue=[]) elif len(inst_name_list) != len(term_list): raise ValueError('inst_name_list and term_list length mismatch.') else: inst_term_list = zip_longest(inst_name_list, (term.items() for term in term_list)) else: inst_name_list = [arg[0] for arg in inst_term_list] # array instance self._cv.array_instance(inst_name, dx, dy, inst_term_list) # update instance dictionary orig_inst = self.instances.pop(inst_name) db = orig_inst.database for name in inst_name_list: inst_ptr = self._cv.get_inst_ref(name) self.instances[name] = SchInstance(db, inst_ptr, master=orig_inst.master) def design_sources_and_loads(self, params_list: Optional[Sequence[Mapping[str, Any]]] = None, default_name: str = 'VDC') -> None: """Convenience function for generating sources and loads, Given DC voltage/current bias sources information, array the given voltage/current bias sources and configure the voltage/current. Each bias dictionary is a dictionary from bias source name to a 3-element list. The first two elements are the PLUS/MINUS net names, respectively, and the third element is the DC voltage/current value as a string or float. A variable name can be given to define a testbench parameter. Parameters ---------- params_list : Optional[Sequence[Mapping[str, Any]]] List of dictionaries representing the element to be used Each dictionary should have the following format: 'lib': Optional[str] (default: analogLib) -> lib name of the master 'type': str -> type of of the master (i.e 'vdc') 'value': Union[T, Dict[str, T], T = Union[str, float, int] -> value of the master 'conns': Dict[str, str] -> connections of the master default_name : str Default name of the instance in the testbench """ if not params_list: self.delete_instance(default_name) return # TODO: find better places to put these template_names = { 'analogLib': { 'vdc': 'VDC{}', 'idc': 'IDC{}', 'cap': 'C{}', 'res': 'R{}', 'vcvs': 'VCVS{}', } } type_to_value_dict = { 'analogLib': { 'vdc': 'vdc', 'cap': 'c', 'res': 'r', 'idc': 'idc', 'vpulse': None, 'vcvs': 'egain', }, } element_list = [] name_list = [] for i, params_dict in enumerate(params_list): lib = params_dict.get('lib', 'analogLib') cell_type = params_dict['type'] value = params_dict['value'] conn_dict = params_dict['conns'] if not isinstance(conn_dict, Mapping): raise ValueError('Got a non dictionary for the connections in ' 'design_sources_and_loads') if cell_type not in type_to_value_dict[lib]: raise ValueError(f'Got an unsupported type {cell_type} for element type in ' f'design_sources_and_loads') # make sure value is either string or dictionary if isinstance(value, (int, float)): value = float_to_si_string(value) # create value_dict if isinstance(value, str): key = type_to_value_dict[lib][cell_type] if key is None: raise ValueError(f'{cell_type} source must specify value dictionary.') value_dict = {key: value} else: if not isinstance(value, Mapping): raise ValueError(f'type not supported for value {value} of type {type(value)}') value_dict = {} for key, val in value.items(): if isinstance(val, (int, float)): value_dict[key] = float_to_si_string(val) elif isinstance(val, str): value_dict[key] = val else: raise ValueError(f'type not supported for key={key}, val={val} ' f'with type {type(val)}') tmp_name = template_names[lib].get(cell_type, 'X{}').format(i) element_list.append((tmp_name, lib, cell_type, value_dict, conn_dict)) name_list.append(tmp_name) self.array_instance(default_name, inst_name_list=name_list) for name, lib, cell, val_dict, conns in element_list: self.replace_instance_master(name, lib, cell, static=True, keep_connections=True) inst = self.instances[name] for k, v in val_dict.items(): inst.set_param(k, v) self.reconnect_instance(name, conns.items()) def design_dummy_transistors(self, dum_info: List[Tuple[Any]], inst_name: str, vdd_name: str, vss_name: str, net_map: Optional[Dict[str, str]] = None) -> None: """Convenience function for generating dummy transistor schematic. Given dummy information (computed by AnalogBase) and a BAG transistor instance, this method generates dummy schematics by arraying and modifying the BAG transistor instance. Parameters ---------- dum_info : List[Tuple[Any]] the dummy information data structure. inst_name : str the BAG transistor instance name. vdd_name : str VDD net name. Used for PMOS dummies. vss_name : str VSS net name. Used for NMOS dummies. net_map : Optional[Dict[str, str]] optional net name transformation mapping. """ if not dum_info: self.delete_instance(inst_name) else: num_arr = len(dum_info) arr_name_list = ['XDUMMY%d' % idx for idx in range(num_arr)] self.array_instance(inst_name, arr_name_list) for name, ((mos_type, w, lch, th, s_net, d_net), fg) in zip(arr_name_list, dum_info): if mos_type == 'pch': cell_name = 'pmos4_standard' sup_name = vdd_name else: cell_name = 'nmos4_standard' sup_name = vss_name if net_map is not None: s_net = net_map.get(s_net, s_net) d_net = net_map.get(d_net, d_net) s_name = s_net if s_net else sup_name d_name = d_net if d_net else sup_name inst = self.instances[name] inst.change_generator('BAG_prim', cell_name) inst.update_connection(name, 'G', sup_name) inst.update_connection(name, 'B', sup_name) inst.update_connection(name, 'D', d_name) inst.update_connection(name, 'S', s_name) inst.design(w=w, l=lch, nf=fg, intent=th) def design_transistor(self, inst_name: str, w: int, lch: int, seg: int, intent: str, m: str = '', d: str = '', g: Union[str, List[str]] = '', s: str = '', b: str = '', stack: int = 1, mos_type: str = '') -> None: """Design a BAG_prim transistor (with stacking support). This is a convenient method to design a stack transistor. Additional transistors will be created on the right. The intermediate nodes of each parallel segment are not shorted together. Parameters ---------- inst_name : str name of the BAG_prim transistor instance. w : int the width of the transistor, in number of fins or resolution units. lch : int the channel length, in resolution units. seg : int number of parallel segments of stacked transistors. intent : str the threshold flavor. m : str base name of the intermediate nodes. the intermediate nodes will be named 'midX', where X is an non-negative integer. d : str the drain name. Empty string to not rename. g : Union[str, List[str]] the gate name. Empty string to not rename. If a list is given, then a NAND-gate structure will be built where the gate nets may be different. Index 0 corresponds to the gate of the source transistor. s : str the source name. Empty string to not rename. b : str the body name. Empty string to not rename. stack : int number of series stack transistors. mos_type : str if non-empty, will change the transistor master to this type. """ inst = self.instances[inst_name] if not issubclass(inst.master_class, MosModuleBase): raise ValueError('This method only works on BAG_prim transistors.') if stack <= 0 or seg <= 0: raise ValueError('stack and seg must be positive') if mos_type: cell_name = 'nmos4_standard' if mos_type == 'nch' else 'pmos4_standard' inst.change_generator('BAG_prim', cell_name, keep_connections=True) g_is_str = isinstance(g, str) if stack == 1: # design instance inst.design(w=w, l=lch, nf=seg, intent=intent) # connect terminals if not g_is_str: g = g[0] for term, net in (('D', d), ('G', g), ('S', s), ('B', b)): if net: inst.update_connection(inst_name, term, net) else: if not m: raise ValueError('Intermediate node base name cannot be empty.') # design instance inst.design(w=w, l=lch, nf=1, intent=intent) # rename G/B if g_is_str and g: inst.update_connection(inst_name, 'G', g) if b: inst.update_connection(inst_name, 'B', b) if not d: d = inst.get_connection('D') if not s: s = inst.get_connection('S') if seg == 1: # only one segment, array instance via naming # rename instance new_name = inst_name + '<0:{}>'.format(stack - 1) self.rename_instance(inst_name, new_name) # rename D/S if stack > 2: m += '<0:{}>'.format(stack - 2) new_s = s + ',' + m new_d = m + ',' + d inst.update_connection(new_name, 'D', new_d) inst.update_connection(new_name, 'S', new_s) if not g_is_str: inst.update_connection(new_name, 'G', ','.join(g)) else: # multiple segment and stacks, have to array instance # construct instance name/terminal map iterator inst_term_list = [] last_cnt = (stack - 1) * seg g_cnt = 0 for cnt in range(0, last_cnt + 1, seg): d_suf = '<{}:{}>'.format(cnt + seg - 1, cnt) s_suf = '<{}:{}>'.format(cnt - 1, cnt - seg) iname = inst_name + d_suf if cnt == 0: s_name = s d_name = m + d_suf elif cnt == last_cnt: s_name = m + s_suf d_name = d else: s_name = m + s_suf d_name = m + d_suf term_list = [('S', s_name), ('D', d_name)] if not g_is_str: term_list.append(('G', g[g_cnt])) g_cnt += 1 inst_term_list.append((iname, term_list)) self.array_instance(inst_name, inst_term_list=inst_term_list) def replace_with_ideal_switch(self, inst_name: str, rclosed: str = 'rclosed', ropen: str = 'ropen', vclosed: str = 'vclosed', vopen: str = 'vopen'): # figure out real switch connections inst = self.instances[inst_name] term_net_list = [('N+', inst.get_connection('S')), ('N-', inst.get_connection('D'))] if 'pmos' in inst.cell_name: term_net_list += [('NC+', 'VDD'), ('NC-', inst.get_connection('G'))] elif 'nmos' in inst.cell_name: term_net_list += [('NC+', inst.get_connection('G')), ('NC-', 'VSS')] else: raise ValueError(f'Cannot replace {inst.cell_name} with ideal switch.') # replace with ideal switch self.replace_instance_master(inst_name, 'analogLib', 'switch', static=True) # reconnect terminals of ideal switch for term, net in term_net_list: self.reconnect_instance_terminal(inst_name, term, net) for key, val in [('vt1', vopen), ('vt2', vclosed), ('ro', ropen), ('rc', rclosed)]: self.instances[inst_name].set_param(key, val) # noinspection PyUnusedLocal def get_lef_options(self, options: Dict[str, Any], config: Mapping[str, Any]) -> None: """Populate the LEF options dictionary. Parameters ---------- options : Dict[str, Any] the result LEF options dictionary. config : Mapping[str, Any] the LEF configuration dictionary. """ if not self.finalized: raise ValueError('This method only works on finalized master.') pin_groups = {SigType.power: [], SigType.ground: [], SigType.clock: [], SigType.analog: []} out_pins = [] for name, term_type in self.pins.items(): sig_type = self.get_signal_type(name) pin_list = pin_groups.get(sig_type, None) if pin_list is not None: pin_list.append(name) if term_type is TermType.output: out_pins.append(name) options['pwr_pins'] = pin_groups[SigType.power] options['gnd_pins'] = pin_groups[SigType.ground] options['clk_pins'] = pin_groups[SigType.clock] options['analog_pins'] = pin_groups[SigType.analog] options['output_pins'] = out_pins def get_instance_hierarchy(self, output_type: DesignOutput, leaf_cells: Optional[Dict[str, List[str]]] = None, default_view_name: str = '') -> Dict[str, Any]: """Returns a nested dictionary representing the modeling instance hierarchy. By default, we try to netlist as deeply as possible. This behavior can be modified by specifying the leaf cells. Parameters ---------- output_type : DesignOutput the behavioral model output type. leaf_cells : Optional[Dict[str, List[str]]] data structure storing leaf cells. default_view_name : str default model view name. Returns ------- hier : Dict[str, Any] the instance hierarchy dictionary. """ is_leaf_table = {} if leaf_cells: for lib_name, cell_list in leaf_cells.items(): for cell in cell_list: is_leaf_table[(lib_name, cell)] = True return self._get_hierarchy_helper(output_type, is_leaf_table, default_view_name) def _get_hierarchy_helper(self, output_type: DesignOutput, is_leaf_table: Dict[Tuple[str, str], bool], default_view_name: str, ) -> Optional[Dict[str, Any]]: model_path = self.get_model_path(output_type, default_view_name) key = (self._orig_lib_name, self._orig_cell_name) if self.is_leaf_model() or is_leaf_table.get(key, False): if not model_path.is_file(): raise ValueError(f'Cannot find model file for {key}') return dict(view_name=default_view_name) ans = {} master_db = self.master_db for inst_name, sch_inst in self.instances.items(): if master_db.exclude_model(sch_inst.lib_name, sch_inst.cell_name): continue if sch_inst.is_primitive: # primitive/static instance has no model file. # so we must use model file for this cell if not model_path.is_file(): raise ValueError(f'Cannot find model file for {key}') ans.clear() ans['view_name'] = default_view_name return ans else: try: ans[inst_name] = sch_inst.master._get_hierarchy_helper(output_type, is_leaf_table, default_view_name) except ValueError as ex: # cannot generate model for this instance if not model_path.is_file(): # Cannot model this schematic too, re-raise error from instance raise ex # otherwise, this is a leaf model cell ans.clear() ans['view_name'] = default_view_name return ans # get here if all instances are successfully modeled return ans
class Module(DesignMaster): """The base class of all schematic generators. This represents a schematic master. This class defines all the methods needed to implement a design in the CAD database. Parameters ---------- yaml_fname : str the netlist information file name. database : ModuleDB the design database object. params : Param the parameters dictionary. copy_state : Optional[Dict[str, Any]] If not None, set content of this master from this dictionary. **kwargs : Any optional arguments """ def __init__(self, yaml_fname: str, database: ModuleDB, params: Param, *, copy_state: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None: self._cv: Optional[PySchCellView] = None if copy_state: self._netlist_dir = copy_state['netlist_dir'] self._cv = copy_state['cv'] self._pins: Dict[str, TermType] = copy_state['pins'] self._orig_cell_name = copy_state['orig_cell_name'] self.instances: Dict[str, SchInstance] = copy_state['instances'] else: self._pins: Dict[str, TermType] = {} if yaml_fname: # normal schematic yaml_fname = os.path.abspath(yaml_fname) self._netlist_dir = os.path.dirname(yaml_fname) self._cv = PySchCellView(yaml_fname, 'symbol') self._orig_cell_name = self._cv.cell_name self.instances: Dict[str, SchInstance] = {name: SchInstance(database, ref) for name, ref in self._cv.inst_refs()} if not self.is_primitive(): self._cv.lib_name = database.lib_name else: # empty yaml file name, this is a BAG primitive self._netlist_dir = '' self._orig_cell_name = self.__class__.__name__.split('__')[1] self.instances: Dict[str, SchInstance] = {} # initialize schematic master DesignMaster.__init__(self, database, params, copy_state=copy_state, **kwargs) @classmethod def get_hidden_params(cls) -> Dict[str, Any]: ans = DesignMaster.get_hidden_params() ans['model_params'] = None return ans @classmethod def is_primitive(cls) -> bool: """Returns True if this Module represents a BAG primitive. NOTE: This method is only used by BAG and schematic primitives. This method prevents the module from being copied during design implementation. Custom subclasses should not override this method. Returns ------- is_primitive : bool True if this Module represents a BAG primitive. """ return False def get_master_basename(self) -> str: return self.orig_cell_name def get_copy_state(self) -> Dict[str, Any]: base = DesignMaster.get_copy_state(self) new_cv = self._cv.get_copy() new_inst = {name: SchInstance(self.master_db, ref, master=self.instances[name].master) for name, ref in new_cv.inst_refs()} base['netlist_dir'] = self._netlist_dir base['cv'] = new_cv base['pins'] = self._pins.copy() base['orig_cell_name'] = self._orig_cell_name base['instances'] = new_inst return base @property def tech_info(self) -> TechInfo: return self.master_db.tech_info @property def sch_scale(self) -> float: tech_info = self.master_db.tech_info return tech_info.resolution * tech_info.layout_unit @property def pins(self) -> Dict[str, TermType]: return self._pins @abc.abstractmethod def design(self, **kwargs: Any) -> None: """To be overridden by subclasses to design this module. To design instances of this module, you can call their :meth:`.design` method or any other ways you coded. To modify schematic structure, call: :meth:`.rename_pin` :meth:`.delete_instance` :meth:`.replace_instance_master` :meth:`.reconnect_instance_terminal` :meth:`.array_instance` """ pass def design_model(self, model_params: Param, key: Any) -> None: self.params = self.params.copy(append=dict(model_params=model_params)) self.update_signature(key) self._cv.cell_name = self.cell_name if 'view_name' not in model_params: # this is a hierarchical model if not self.instances: # found a leaf cell with no behavioral model raise ValueError('Schematic master has no instances and no behavioral model.') self.clear_children_key() for name, inst in self.instances.items(): cur_params: Optional[Param] = model_params.get(name, None) if cur_params is None: raise ValueError('Cannot find model parameters for instance {}'.format(name)) inst.design_model(cur_params) if not inst.is_primitive: self.add_child_key(inst.master_key) def set_param(self, key: str, val: Union[int, float, bool, str]) -> None: """Set schematic parameters for this master. This method is only used to set parameters for BAG primitives. Parameters ---------- key : str parameter name. val : Union[int, float, bool, str] parameter value. """ self._cv.set_param(key, val) def finalize(self) -> None: """Finalize this master instance. """ # invoke design function, excluding model_params args = dict((k, v) for k, v in self.params.items() if k != 'model_params') self.design(**args) # get set of children master keys for inst in self.instances.values(): if not inst.is_primitive: # NOTE: only non-primitive instance can have ports change inst.check_connections() self.add_child_key(inst.master_key) if self._cv is not None: # get pins self._pins = {k: TermType(v) for k, v in self._cv.terminals()} # update cell name self._cv.cell_name = self.cell_name # call super finalize routine DesignMaster.finalize(self) def get_content(self, output_type: DesignOutput, rename_dict: Dict[str, str], name_prefix: str, name_suffix: str) -> Tuple[str, Any]: cell_name = self.format_cell_name(self.cell_name, rename_dict, name_prefix, name_suffix) if self.is_primitive(): return cell_name, (None, '') netlist = '' if is_model_type(output_type): model_params: Optional[Param] = self.params['model_params'] if model_params is None: # model parameters is unset. This happens if a behavioral model view is used # at a top level block, and this cell gets shadows out. # If this is the case, just return None so this cellview won't be netlisted. return cell_name, (None, '') view_name = model_params.get('view_name', None) if view_name is not None: ext = get_extension(output_type) if view_name: fname = '{}.{}.{}'.format(self.orig_cell_name, view_name, ext) else: # empty view_name, use default name fname = '{}.{}'.format(self.orig_cell_name, ext) fname = os.path.join(os.path.dirname(self._netlist_dir), 'models', fname) netlist = self.master_db.generate_model_netlist(fname, cell_name, model_params) return cell_name, (self._cv, netlist) @property def cell_name(self) -> str: """The master cell name.""" if self.is_primitive(): return self.get_cell_name_from_parameters() return super(Module, self).cell_name @property def orig_cell_name(self) -> str: """The original schematic template cell name.""" return self._orig_cell_name def should_delete_instance(self) -> bool: """Returns True if this instance should be deleted based on its parameters. This method is mainly used to delete 0 finger or 0 width transistors. However, You can override this method if there exists parameter settings which corresponds to an empty schematic. Returns ------- delete : bool True if parent should delete this instance. """ return False def get_schematic_parameters(self) -> Dict[str, str]: """Returns the schematic parameter dictionary of this instance. NOTE: This method is only used by BAG primitives, as they are implemented with parameterized cells in the CAD database. Custom subclasses should not override this method. Returns ------- params : Dict[str, str] the schematic parameter dictionary. """ return {} def get_cell_name_from_parameters(self) -> str: """Returns new cell name based on parameters. NOTE: This method is only used by BAG primitives. This method enables a BAG primitive to change the cell master based on design parameters (e.g. change transistor instance based on the intent parameter). Custom subclasses should not override this method. Returns ------- cell : str the cell name based on parameters. """ return self.orig_cell_name def rename_pin(self, old_pin: str, new_pin: str) -> None: """Renames an input/output pin of this schematic. NOTE: Make sure to call :meth:`.reconnect_instance_terminal` so that instances are connected to the new pin. Parameters ---------- old_pin : str the old pin name. new_pin : str the new pin name. """ self._cv.rename_pin(old_pin, new_pin) def add_pin(self, new_pin: str, pin_type: Union[TermType, str]) -> None: """Adds a new pin to this schematic. NOTE: Make sure to call :meth:`.reconnect_instance_terminal` so that instances are connected to the new pin. Parameters ---------- new_pin : str the new pin name. pin_type : Union[TermType, str] the new pin type. """ if isinstance(pin_type, TermType): self._cv.add_pin(new_pin, pin_type) else: self._cv.add_pin(new_pin, TermType[pin_type].value) def remove_pin(self, remove_pin: str) -> bool: """Removes a pin from this schematic. Parameters ---------- remove_pin : str the pin to remove. Returns ------- success : bool True if the pin is successfully found and removed. """ return self._cv.remove_pin(remove_pin) def set_pin_attribute(self, pin_name: str, key: str, val: str) -> None: """Set an attribute on the given pin. Parameters ---------- pin_name : str the pin name. key : str the attribute name. val : str the attribute value. """ self._cv.set_pin_attribute(pin_name, key, val) def rename_instance(self, old_name: str, new_name: str, conn_list: Optional[Iterable[Tuple[str, str]]] = None) -> None: """Renames an instance in this schematic. Parameters ---------- old_name : str the old instance name. new_name : str the new instance name. conn_list : Optional[Iterable[Tuple[str, str]]] an optional connection list. """ self._cv.rename_instance(old_name, new_name) self.instances[new_name] = inst = self.instances.pop(old_name) if conn_list: for term, net in conn_list: inst.update_connection(new_name, term, net) def remove_instance(self, inst_name: str) -> bool: """Removes the instance with the given name. Parameters ---------- inst_name : str the child instance to delete. Returns ------- success : bool True if the instance is successfully found and removed. """ success = self._cv.remove_instance(inst_name) if success: del self.instances[inst_name] return success def delete_instance(self, inst_name: str) -> bool: """Delete the instance with the given name. This method is identical to remove_instance(). It's here only for backwards compatibility. """ return self.remove_instance(inst_name) def replace_instance_master(self, inst_name: str, lib_name: str, cell_name: str, static: bool = False, keep_connections: bool = False) -> None: """Replace the master of the given instance. NOTE: all terminal connections will be reset. Call reconnect_instance_terminal() to modify terminal connections. Parameters ---------- inst_name : str the child instance to replace. lib_name : str the new library name. cell_name : str the new cell name. static : bool True if we're replacing instance with a static schematic instead of a design module. keep_connections : bool True to keep the old connections when the instance master changed. """ if inst_name not in self.instances: raise ValueError('Cannot find instance with name: %s' % inst_name) self.instances[inst_name].change_generator(lib_name, cell_name, static=static, keep_connections=keep_connections) def reconnect_instance_terminal(self, inst_name: str, term_name: str, net_name: str) -> None: """Reconnect the instance terminal to a new net. Parameters ---------- inst_name : str the instance to modify. term_name : str the instance terminal name to reconnect. net_name : str the net to connect the instance terminal to. """ inst = self.instances.get(inst_name, None) if inst is None: raise ValueError('Cannot find instance {}'.format(inst_name)) inst.update_connection(inst_name, term_name, net_name) def reconnect_instance(self, inst_name: str, term_net_iter: Iterable[Tuple[str, str]]) -> None: """Reconnect all give instance terminals Parameters ---------- inst_name : str the instance to modify. term_net_iter : Iterable[Tuple[str, str]] an iterable of (term, net) tuples. """ inst = self.instances.get(inst_name, None) if inst is None: raise ValueError('Cannot find instance {}'.format(inst_name)) for term, net in term_net_iter: inst.update_connection(inst_name, term, net) def array_instance(self, inst_name: str, inst_name_list: Optional[List[str]] = None, term_list: Optional[List[Dict[str, str]]] = None, inst_term_list: Optional[List[Tuple[str, Iterable[Tuple[str, str]]]]] = None, dx: int = 0, dy: int = 0) -> None: """Replace the given instance by an array of instances. This method will replace self.instances[inst_name] by a list of Modules. The user can then design each of those modules. Parameters ---------- inst_name : str the instance to array. inst_name_list : Optional[List[str]] a list of the names for each array item. term_list : Optional[List[Dict[str, str]]] a list of modified terminal connections for each array item. The keys are instance terminal names, and the values are the net names to connect them to. Only terminal connections different than the parent instance should be listed here. If None, assume terminal connections are not changed. inst_term_list : Optional[List[Tuple[str, List[Tuple[str, str]]]]] zipped version of inst_name_list and term_list. If given, this is used instead. dx : int the X coordinate shift. If dx = dy = 0, default to shift right. dy : int the Y coordinate shift. If dx = dy = 0, default to shift right. """ if inst_term_list is None: if inst_name_list is None: raise ValueError('inst_name_list cannot be None if inst_term_iter is None.') # get instance/terminal list iterator if term_list is None: inst_term_list = zip_longest(inst_name_list, [], fillvalue=[]) elif len(inst_name_list) != len(term_list): raise ValueError('inst_name_list and term_list length mismatch.') else: inst_term_list = zip_longest(inst_name_list, (term.items() for term in term_list)) else: inst_name_list = [arg[0] for arg in inst_term_list] # array instance self._cv.array_instance(inst_name, dx, dy, inst_term_list) # update instance dictionary orig_inst = self.instances.pop(inst_name) db = orig_inst.database for name in inst_name_list: inst_ptr = self._cv.get_inst_ref(name) self.instances[name] = SchInstance(db, inst_ptr, master=orig_inst.master) def design_dc_bias_sources(self, vbias_dict: Optional[Dict[str, List[str]]], ibias_dict: Optional[Dict[str, List[str]]], vinst_name: str, iinst_name: str, define_vdd: bool = True) -> None: """Convenience function for generating DC bias sources. Given DC voltage/current bias sources information, array the given voltage/current bias sources and configure the voltage/current. Each bias dictionary is a dictionary from bias source name to a 3-element list. The first two elements are the PLUS/MINUS net names, respectively, and the third element is the DC voltage/current value as a string or float. A variable name can be given to define a testbench parameter. Parameters ---------- vbias_dict : Optional[Dict[str, List[str]]] the voltage bias dictionary. None or empty to disable. ibias_dict : Optional[Dict[str, List[str]]] the current bias dictionary. None or empty to disable. vinst_name : str the DC voltage source instance name. iinst_name : str the DC current source instance name. define_vdd : bool True to include a supply voltage source connected to VDD/VSS, with voltage value 'vdd'. """ if define_vdd and 'SUP' not in vbias_dict: vbias_dict = vbias_dict.copy() vbias_dict['SUP'] = ['VDD', 'VSS', 'vdd'] for bias_dict, name_template, param_name, inst_name in \ ((vbias_dict, 'V%s', 'vdc', vinst_name), (ibias_dict, 'I%s', 'idc', iinst_name)): if bias_dict: name_list, term_list, val_list, param_dict_list = [], [], [], [] for name in sorted(bias_dict.keys()): value_tuple = bias_dict[name] pname, nname, bias_val = value_tuple[:3] param_dict = value_tuple[3] if len(value_tuple) > 3 \ else None # type: Optional[Dict] term_list.append(dict(PLUS=pname, MINUS=nname)) name_list.append(name_template % name) param_dict_list.append(param_dict) if isinstance(bias_val, str): val_list.append(bias_val) elif isinstance(bias_val, int) or isinstance(bias_val, float): val_list.append(float_to_si_string(bias_val)) else: raise ValueError('value %s of type %s ' 'not supported' % (bias_val, type(bias_val))) self.array_instance(inst_name, name_list, term_list=term_list) for name, val, param_dict in zip(name_list, val_list, param_dict_list): inst = self.instances[name] inst.set_param(param_name, val) if param_dict is not None: for k, v in param_dict.items(): if isinstance(v, str): pass elif isinstance(v, int) or isinstance(v, float): v = float_to_si_string(v) else: raise ValueError('value %s of type %s not supported' % (v, type(v))) inst.set_param(k, v) else: self.delete_instance(inst_name) def design_dummy_transistors(self, dum_info: List[Tuple[Any]], inst_name: str, vdd_name: str, vss_name: str, net_map: Optional[Dict[str, str]] = None) -> None: """Convenience function for generating dummy transistor schematic. Given dummy information (computed by AnalogBase) and a BAG transistor instance, this method generates dummy schematics by arraying and modifying the BAG transistor instance. Parameters ---------- dum_info : List[Tuple[Any]] the dummy information data structure. inst_name : str the BAG transistor instance name. vdd_name : str VDD net name. Used for PMOS dummies. vss_name : str VSS net name. Used for NMOS dummies. net_map : Optional[Dict[str, str]] optional net name transformation mapping. """ if not dum_info: self.delete_instance(inst_name) else: num_arr = len(dum_info) arr_name_list = ['XDUMMY%d' % idx for idx in range(num_arr)] self.array_instance(inst_name, arr_name_list) for name, ((mos_type, w, lch, th, s_net, d_net), fg) in zip(arr_name_list, dum_info): if mos_type == 'pch': cell_name = 'pmos4_standard' sup_name = vdd_name else: cell_name = 'nmos4_standard' sup_name = vss_name if net_map is not None: s_net = net_map.get(s_net, s_net) d_net = net_map.get(d_net, d_net) s_name = s_net if s_net else sup_name d_name = d_net if d_net else sup_name inst = self.instances[name] inst.change_generator('BAG_prim', cell_name) inst.update_connection(name, 'G', sup_name) inst.update_connection(name, 'B', sup_name) inst.update_connection(name, 'D', d_name) inst.update_connection(name, 'S', s_name) inst.design(w=w, l=lch, nf=fg, intent=th) def design_transistor(self, inst_name: str, w: int, lch: int, seg: int, intent: str, m: str = '', d: str = '', g: Union[str, List[str]] = '', s: str = '', b: str = '', stack: int = 1, mos_type: str = '') -> None: """Design a BAG_prim transistor (with stacking support). This is a convenient method to design a stack transistor. Additional transistors will be created on the right. The intermediate nodes of each parallel segment are not shorted together. Parameters ---------- inst_name : str name of the BAG_prim transistor instance. w : int the width of the transistor, in number of fins or resolution units. lch : int the channel length, in resolution units. seg : int number of parallel segments of stacked transistors. intent : str the threshold flavor. m : str base name of the intermediate nodes. the intermediate nodes will be named 'midX', where X is an non-negative integer. d : str the drain name. Empty string to not rename. g : Union[str, List[str]] the gate name. Empty string to not rename. If a list is given, then a NAND-gate structure will be built where the gate nets may be different. Index 0 corresponds to the gate of the source transistor. s : str the source name. Empty string to not rename. b : str the body name. Empty string to not rename. stack : int number of series stack transistors. mos_type : str if non-empty, will change the transistor master to this type. """ inst = self.instances[inst_name] if not issubclass(inst.master_class, MosModuleBase): raise ValueError('This method only works on BAG_prim transistors.') if stack <= 0 or seg <= 0: raise ValueError('stack and seg must be positive') if mos_type: cell_name = 'nmos4_standard' if mos_type == 'nch' else 'pmos4_standard' inst.change_generator('BAG_prim', cell_name, keep_connections=True) g_is_str = isinstance(g, str) if stack == 1: # design instance inst.design(w=w, l=lch, nf=seg, intent=intent) # connect terminals if not g_is_str: g = g[0] for term, net in (('D', d), ('G', g), ('S', s), ('B', b)): if net: inst.update_connection(inst_name, term, net) else: if not m: raise ValueError('Intermediate node base name cannot be empty.') # design instance inst.design(w=w, l=lch, nf=1, intent=intent) # rename G/B if g_is_str and g: inst.update_connection(inst_name, 'G', g) if b: inst.update_connection(inst_name, 'B', b) if not d: d = inst.get_connection('D') if not s: s = inst.get_connection('S') if seg == 1: # only one segment, array instance via naming # rename instance new_name = inst_name + '<0:{}>'.format(stack - 1) self.rename_instance(inst_name, new_name) # rename D/S if stack > 2: m += '<0:{}>'.format(stack - 2) new_s = s + ',' + m new_d = m + ',' + d inst.update_connection(new_name, 'D', new_d) inst.update_connection(new_name, 'S', new_s) if not g_is_str: inst.update_connection(new_name, 'G', ','.join(g)) else: # multiple segment and stacks, have to array instance # construct instance name/terminal map iterator inst_term_list = [] last_cnt = (stack - 1) * seg g_cnt = 0 for cnt in range(0, last_cnt + 1, seg): d_suf = '<{}:{}>'.format(cnt + seg - 1, cnt) s_suf = '<{}:{}>'.format(cnt - 1, cnt - seg) iname = inst_name + d_suf if cnt == 0: s_name = s d_name = m + d_suf elif cnt == last_cnt: s_name = m + s_suf d_name = d else: s_name = m + s_suf d_name = m + d_suf term_list = [('S', s_name), ('D', d_name)] if not g_is_str: term_list.append(('G', g[g_cnt])) g_cnt += 1 inst_term_list.append((iname, term_list)) self.array_instance(inst_name, inst_term_list=inst_term_list)
def build_cv(fname): yaml_file = pkg_resources.resource_filename(__name__, os.path.join('data', fname)) return PySchCellView(yaml_file)