From c798b9da93566026450e0d3fac5f50c224a8402e Mon Sep 17 00:00:00 2001 From: Priti Ashvin Shah <74020801+priti-ashvin-shah-ibm@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:42:56 -0500 Subject: [PATCH] 865 gds handle multiplanar design with multiple chip names and layer. (#869) * Fix typos, add method for getting chip size to multi-planar * Fix linting and typos * Add logic to allow for junctions to be in multiple chips. Thus, a file with junctions only needs to be imported the gdspy only once. --- qiskit_metal/designs/design_base.py | 2 +- qiskit_metal/designs/design_multiplanar.py | 56 +++++++++ .../renderers/renderer_gds/gds_renderer.py | 118 +++++++++++------- 3 files changed, 132 insertions(+), 44 deletions(-) diff --git a/qiskit_metal/designs/design_base.py b/qiskit_metal/designs/design_base.py index ed0579c8f..e6b6eed9c 100644 --- a/qiskit_metal/designs/design_base.py +++ b/qiskit_metal/designs/design_base.py @@ -51,7 +51,7 @@ class QDesign(): """ # pylint: disable=too-many-instance-attributes, too-many-public-methods - # Dummy private attribute used to check if an instanciated object is + # Dummy private attribute used to check if an instantiated object is # indeed a QDesign class. The problem is that the `isinstance` # built-in method fails when this module is reloaded. # Used by `is_design` to check. diff --git a/qiskit_metal/designs/design_multiplanar.py b/qiskit_metal/designs/design_multiplanar.py index cef7ce4f9..717b6871c 100644 --- a/qiskit_metal/designs/design_multiplanar.py +++ b/qiskit_metal/designs/design_multiplanar.py @@ -19,6 +19,7 @@ from qiskit_metal.designs.design_base import QDesign from qiskit_metal.toolbox_metal.layer_stack_handler import LayerStackHandler from addict import Dict +from typing import Tuple __all__ = ['MultiPlanar'] @@ -92,3 +93,58 @@ def _add_chip_info(self): size_x='9mm', size_y='7mm', ) + + def get_x_y_for_chip(self, chip_name: str) -> Tuple[tuple, int]: + """If the chip_name is in self.chips, along with entry for size + information then return a tuple=(minx, miny, maxx, maxy). Used for + subtraction while exporting design. + + Args: + chip_name (str): Name of chip that you want the size of. + + Returns: + Tuple[tuple, int]: + tuple: The exact placement on rectangle coordinate (minx, miny, maxx, maxy). + int: 0=all is good + 1=chip_name not in self._chips + 2=size information missing or no good + """ + x_y_location = tuple() + + if chip_name in self._chips: + if 'size' in self._chips[chip_name]: + + size = self.parse_value(self.chips[chip_name]['size']) + if 'center_x' in size \ + and 'center_y' in size \ + and 'size_x' in size \ + and 'size_y' in size: + if type(size.center_x) in [int, float] and \ + type(size.center_y) in [int, float] and \ + type(size.size_x) in [int, float] and \ + type(size.size_y) in [int, float]: + x_y_location = ( + size['center_x'] - (size['size_x'] / 2.0), + size['center_y'] - (size['size_y'] / 2.0), + size['center_x'] + (size['size_x'] / 2.0), + size['center_y'] + (size['size_y'] / 2.0)) + return x_y_location, 0 + + self.logger.warning( + f'Size information within self.chips[{chip_name}]["size"]' + f' is NOT an int or float.') + return x_y_location, 2 + + self.logger.warning('center_x or center_y or size_x or size_y ' + f' NOT in self._chips[{chip_name}]["size"]') + return x_y_location, 2 + + self.logger.warning( + f'Information for size in NOT in self._chips[{chip_name}]' + ' dict. Return "None" in tuple.') + return x_y_location, 2 + + self.logger.warning( + f'Chip name "{chip_name}" is not in self._chips dict. Return "None" in tuple.' + ) + return x_y_location, 1 diff --git a/qiskit_metal/renderers/renderer_gds/gds_renderer.py b/qiskit_metal/renderers/renderer_gds/gds_renderer.py index c4cb24b78..c6e2e7670 100644 --- a/qiskit_metal/renderers/renderer_gds/gds_renderer.py +++ b/qiskit_metal/renderers/renderer_gds/gds_renderer.py @@ -305,7 +305,7 @@ def __init__(self, Defaults to True. render_template (Dict, optional): Typically used by GUI for template options for GDS. Defaults to None. - render_options (Dict, optional): Used to overide all options. + render_options (Dict, optional): Used to override all options. Defaults to None. """ @@ -325,6 +325,9 @@ def __init__(self, # check the scale self._check_bounding_box_scale() + # if imported, hold the path to file name, otherwise None. + self.imported_junction_gds = None + QGDSRenderer.load() def _initiate_renderer(self): @@ -340,7 +343,6 @@ def _close_renderer(self): def render_design(self): """Export the design to GDS.""" self.export_to_gds(file_name=self.design.name, highlight_qcomponents=[]) - pass def _check_bounding_box_scale(self): """Some error checking for bounding_box_scale_x and @@ -420,11 +422,12 @@ def _separate_subtract_shapes(self, chip_name: str, table_name: str, @staticmethod def _get_bounds( - gs_table: geopandas.GeoSeries) -> Tuple[float, float, float, float]: + gs_table: geopandas.GeoDataFrame + ) -> Tuple[float, float, float, float]: """Get the bounds for all of the elements in gs_table. Args: - gs_table (pandas.GeoSeries): A pandas GeoSeries used to describe + gs_table (geopandas.GeoDataFrame): A GeoPandas GeoDataFrame used to describe components in a design. Returns: @@ -530,7 +533,7 @@ def _scale_max_bounds(self, chip_name: str, def _check_qcomps(self, highlight_qcomponents: list = None) -> Tuple[list, int]: - """Confirm the list doesn't have names of components repeated. Comfirm + """Confirm the list doesn't have names of components repeated. Confirm that the name of component exists in QDesign. Args: @@ -577,7 +580,7 @@ def _create_qgeometry_for_gds(self, """Using self.design, this method does the following: 1. Gather the QGeometries to be used to write to file. - Duplicate names in hightlight_qcomponents will be removed without + Duplicate names in highlight_qcomponents will be removed without warning. 2. Populate self.dict_bounds, for each chip, contains the maximum bound @@ -607,7 +610,7 @@ def _create_qgeometry_for_gds(self, return 1 self.dict_bounds.clear() - for chip_name in self.chip_info: + for chip_name, _ in self.chip_info.items(): # put the QGeometry into GDS format. # There can be more than one chip in QGeometry. # They all export to one gds file. @@ -632,7 +635,7 @@ def _create_qgeometry_for_gds(self, else: # For every chip, and layer, separate the "subtract" # and "no_subtract" elements and gather bounds. - # dict_bounds[chip_name] = list_bounds + # self.dict_bounds[chip_name] = list_bounds self._gather_subtract_elements_and_bounds( chip_name, table_name, table, all_table_subtracts, all_table_no_subtracts) @@ -732,7 +735,7 @@ def _fix_short_segments_within_table(self, chip_name: str, chip_layer: int, """Update self.chip_info geopandas.GeoDataFrame. Will iterate through the rows to examine the LineString. - Then determine if there is a segment that is shorter than the critera + Then determine if there is a segment that is shorter than the criteria based on default_options. If so, then remove the row, and append shorter LineString with no fillet, within the dataframe. @@ -784,13 +787,13 @@ def _check_length(self, a_shapely: shapely.geometry.LineString, """Determine if a_shapely has short segments based on scaled fillet value. - Use check_short_segments_by_scaling_fillet to determine the critera + Use check_short_segments_by_scaling_fillet to determine the criteria for flagging a segment. Return Tuple with flagged segments. The "status" returned in int: * -1: Method needs to update the return code. * 0: No issues, no short segments found - * int: The number of shapelys returned. New shapeleys, should + * int: The number of shapelys returned. New shapelys, should replace the ones provided in a_shapely The "shorter_lines" returned in dict: @@ -965,7 +968,7 @@ def _identify_vertex_not_to_fillet(self, coords: list, a_fillet: float, Key 'midpoints' will hold list of tuples. The index of a tuple corresponds to two index within coords. For example, a index in midpoints is x, - that coresponds midpoint of segment x-1 to x. + that corresponds midpoint of segment x-1 to x. """ # Depreciated since there is no longer a scale factor @@ -1352,7 +1355,7 @@ def _populate_no_cheese(self): fab = is_true(self.options.fabricate) - for chip_name in self.chip_info: + for chip_name, _ in self.chip_info.items(): layers_in_chip = self.design.qgeometry.get_all_unique_layers( chip_name) @@ -1422,7 +1425,7 @@ def _cheese_buffer_maker( Returns: Union[None, shapely.geometry.multipolygon.MultiPolygon]: The shapely which combines the polygons and linestrings and creates - buffer as specificed through default_options. + buffer as specified through default_options. """ # pylint: disable=too-many-locals style_cap = int(self.parse_value(self.options.no_cheese.cap_style)) @@ -1519,7 +1522,7 @@ def _populate_poly_path_for_export(self): all_chips_top_name = 'TOP' all_chips_top = lib.new_cell(all_chips_top_name, overwrite_duplicate=True) - for chip_name in self.chip_info: + for chip_name, _ in self.chip_info.items(): chip_only_top_name = f'TOP_{chip_name}' chip_only_top = lib.new_cell(chip_only_top_name, overwrite_duplicate=True) @@ -1736,12 +1739,12 @@ def _add_groundcell_to_chip_only_top(cls, lib: gdspy.GdsLibrary, lib.remove(ground_cell) def _get_linestring_characteristics( - self, row: 'pandas.Pandas') -> Tuple[Tuple, float, float]: + self, row: 'pd.Pandas') -> Tuple[Tuple, float, float]: """Given a row in the Junction table, give the characteristics of LineString in row.geometry. Args: - row (pandas.Pandas): A row from Junction table of QGeometry. + row (pd.Pandas): A row from Junction table of QGeometry. Returns: Tuple: @@ -1766,14 +1769,13 @@ def _get_linestring_characteristics( return center, rotation, magnitude def _give_rotation_center_twopads( - self, row: 'pandas.Pandas', - a_cell_bounding_box: 'numpy.ndarray') -> Tuple: + self, row: 'pd.Pandas', a_cell_bounding_box: 'np.ndarray') -> Tuple: """Calculate the angle for rotation, center of LineString in row.geometry, and if needed create two pads to connect the junction to qubit. Args: - row (pandas.Pandas): A row from Junction table of QGeometry. + row (pd.Pandas): A row from Junction table of QGeometry. a_cell_bounding_box (numpy.ndarray): Give the bounding box of cell used in row.gds_cell_name. @@ -1836,6 +1838,38 @@ def _give_rotation_center_twopads( ############ + def _import_junction_gds_file(self, lib: gdspy.library, + directory_name: str) -> bool: + """Import the file which contains all junctions for design. + If the file has already been imported, just return True. + + When the design has junctions on multiple chips, + we only need to import file once to get ALL of the junctions. + + Args: + lib (gdspy.library): The library used to export the entire QDesign. + directory_name (str): The path of directory to read file with junctions. + + Returns: + bool: True if file imported to GDS lib or previously imported. + False if file not found. + """ + + if self.imported_junction_gds is not None: + return True + + if os.path.isfile(self.options.path_filename): + lib.read_gds(self.options.path_filename, units='convert') + self.imported_junction_gds = self.options.path_filename + return True + else: + message_str = ( + f'Not able to find file:"{self.options.path_filename}". ' + f'Not used to replace junction.' + f' Checked directory:"{directory_name}".') + self.logger.warning(message_str) + return False + def _import_junctions_to_one_cell(self, chip_name: str, lib: gdspy.library, chip_only_top: gdspy.library.Cell, layers_in_chip: list): @@ -1865,8 +1899,9 @@ def _import_junctions_to_one_cell(self, chip_name: str, lib: gdspy.library, layers_in_junction_table = set( self.chip_info[chip_name]['junction']['layer']) - if os.path.isfile(self.options.path_filename): - lib.read_gds(self.options.path_filename, units='convert') + if self._import_junction_gds_file(lib=lib, + directory_name=directory_name): + for iter_layer in layers_in_chip: if self._is_negative_mask(chip_name, iter_layer): # Want to export negative mask @@ -1887,7 +1922,7 @@ def _import_junctions_to_one_cell(self, chip_name: str, lib: gdspy.library, hold_all_jj_cell = lib.new_cell( hold_all_jj_cell_name, overwrite_duplicate=True) - self._add_negative_extention_to_jj( + self._add_negative_extension_to_jj( chip_name, iter_layer, lib, chip_only_top, chip_only_top_layer, hold_all_pads_cell, hold_all_jj_cell) @@ -1904,7 +1939,7 @@ def _import_junctions_to_one_cell(self, chip_name: str, lib: gdspy.library, if row.gds_cell_name in lib.cells.keys(): # When positive mask, just add the pads to chip_only_top - self._add_positive_extention_to_jj( + self._add_positive_extension_to_jj( lib, row, chip_layer_cell) else: self.logger.warning( @@ -1913,20 +1948,14 @@ def _import_junctions_to_one_cell(self, chip_name: str, lib: gdspy.library, f'file: {self.options.path_filename}.' f' The cell was not used.') - else: - self.logger.warning( - f'Not able to find file:"{self.options.path_filename}". ' - f'Not used to replace junction.' - f' Checked directory:"{directory_name}".') - - def _add_negative_extention_to_jj(self, chip_name: str, jj_layer: int, + def _add_negative_extension_to_jj(self, chip_name: str, jj_layer: int, lib: gdspy.library, chip_only_top: gdspy.library.Cell, chip_only_top_layer: gdspy.library.Cell, hold_all_pads_cell: gdspy.library.Cell, hold_all_jj_cell: gdspy.library.Cell): """Manipulate existing geometries for the layer that the junctions need - to be added. Since boolean subtaction is computationally intensive, + to be added. Since boolean subtraction is computationally intensive, the method will gather the pads for a layer, and do the boolean just once. Then add the junctions to difference. @@ -1950,7 +1979,7 @@ def _add_negative_extention_to_jj(self, chip_name: str, jj_layer: int, if row.gds_cell_name in lib.cells.keys(): # For negative mask, collect the pads to subtract per layer, # and subtract from chip_only_top_layer - self._gather_negative_extention_for_jj(lib, row, + self._gather_negative_extension_for_jj(lib, row, hold_all_pads_cell, hold_all_jj_cell) else: @@ -2012,18 +2041,18 @@ def _clean_hierarchy(cls, lib, chip_only_top, chip_only_top_layer, lib.remove(value.ref_cell.name) lib.remove(hold_all_pads_cell) - def _gather_negative_extention_for_jj( - self, lib: gdspy.library, row: 'pandas.core.frame.Pandas', + def _gather_negative_extension_for_jj( + self, lib: gdspy.library, row: 'pd.core.frame.Pandas', hold_all_pads_cell: gdspy.library.Cell, hold_all_jj_cell: gdspy.library.Cell): - """Gather the pads and jjs and put them in seprate cells. The + """Gather the pads and jjs and put them in separate cells. The the pads can be boolean'd 'not' just once. After boolean for pads, then the jjs will be added to result. The boolean is very time intensive, so just want to do it once. Args: lib (gdspy.library): The library used to export the entire QDesign. - row (pandas.core.frame.Pandas): Each row is from the qgeometry junction table. + row (pd.core.frame.Pandas): Each row is from the qgeometry junction table. hold_all_pads_cell (gdspy.library.Cell): Collect all the pads with movement. hold_all_jj_cell (gdspy.library.Cell): Collect all the jj's with movement. """ @@ -2048,15 +2077,15 @@ def _gather_negative_extention_for_jj( hold_all_pads_cell.add( gdspy.CellReference(temp_cell, origin=center, rotation=rotation)) - def _add_positive_extention_to_jj(self, lib: gdspy.library, - row: 'pandas.core.frame.Pandas', + def _add_positive_extension_to_jj(self, lib: gdspy.library, + row: 'pd.core.frame.Pandas', chip_only_top_layer: gdspy.library.Cell): - """Get the extention pads, then add or subtract to extracted cell based on + """Get the extension pads, then add or subtract to extracted cell based on positive or negative mask. Args: lib (gdspy.library): The library used to export the entire QDesign. - row (pandas.core.frame.Pandas): Each row is from the qgeometry + row (pd.core.frame.Pandas): Each row is from the qgeometry junction table. chip_only_top_layer (gdspy.library.Cell): The cell used for chip_name and layer_num. @@ -2126,6 +2155,9 @@ def export_to_gds(self, self.chip_info.clear() self.chip_info.update(self._get_chip_names()) + # if imported, hold the path to file name, otherwise None. + self.imported_junction_gds = None + if self._create_qgeometry_for_gds(highlight_qcomponents) == 0: # Create self.lib and populate path and poly. self._populate_poly_path_for_export() @@ -2193,14 +2225,14 @@ def _multipolygon_to_gds( layer=layer, datatype=data_type, precision=precision) - # Poly facturing leading to a funny shape. Leave this out of gds output for now. + # Poly fracturing leading to a funny shape. Leave this out of gds output for now. # a_poly.fillet(no_cheese_buffer, # points_per_2pi=128, # max_points=max_points, # precision=precision) all_gds.append(a_poly) else: - # Poly facturing leading to a funny shape. Leave this out of gds output for now. + # Poly fracturing leading to a funny shape. Leave this out of gds output for now. # exterior_poly.fillet(no_cheese_buffer, # points_per_2pi=128, # max_points=max_points,