From a76710592f2edce1ec11952a91cd890684ecf34d Mon Sep 17 00:00:00 2001 From: Caspar Date: Thu, 12 Oct 2023 12:38:27 +0300 Subject: [PATCH] Support multi-face layouts and rectangular chips in masks This commit adds new features to mask layout generation: - Make it easier to create mask layouts for multiple faces on the same wafer - Make it possible to use rectangular (non-square) chips - New mask demo_multiface.py to demonstrate these features --- .../layer_config/default_layer_config.py | 6 +- .../python/kqcircuits/masks/mask_export.py | 14 ++- .../python/kqcircuits/masks/mask_layout.py | 115 +++++++++++------- .../python/kqcircuits/masks/mask_set.py | 30 ++++- .../masks/multi_face_mask_layout.py | 75 ++++++++++++ .../python/kqcircuits/util/label.py | 41 +++---- .../python/scripts/masks/demo_multiface.py | 108 ++++++++++++++++ 7 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 klayout_package/python/kqcircuits/masks/multi_face_mask_layout.py create mode 100644 klayout_package/python/scripts/masks/demo_multiface.py diff --git a/klayout_package/python/kqcircuits/layer_config/default_layer_config.py b/klayout_package/python/kqcircuits/layer_config/default_layer_config.py index 9aa2882b1..c742c145f 100644 --- a/klayout_package/python/kqcircuits/layer_config/default_layer_config.py +++ b/klayout_package/python/kqcircuits/layer_config/default_layer_config.py @@ -239,7 +239,7 @@ def _shift_layers(layers, shift_ID, shift_data_type): "chips_map_offset": pya.DVector(-1200, 1200), "chip_size": 10000, "chip_box_offset": pya.DVector(0, 0), - "chip_trans": pya.DTrans(pya.DPoint(0, 0)) * pya.DTrans().M90, + "chip_trans": pya.DTrans(), "dice_width": 200, "text_margin": 100, "mask_text_scale": 1.0, @@ -261,7 +261,7 @@ def _shift_layers(layers, shift_ID, shift_data_type): "chips_map_offset": pya.DVector(-2700, 2700), "chip_size": 7000, "chip_box_offset": pya.DVector(1500, 1500), - "chip_trans": pya.DTrans(pya.DPoint(10000, 0)) * pya.DTrans().M90, + "chip_trans": pya.DTrans(pya.DVector(10000, 0)) * pya.DTrans().M90, "dice_width": 140, "text_margin": 100, "mask_text_scale": 0.7, @@ -272,7 +272,7 @@ def _shift_layers(layers, shift_ID, shift_data_type): "chips_map_offset": pya.DVector(-2700, 2700), "chip_size": 7000, "chip_box_offset": pya.DVector(1500, 1500), - "chip_trans": pya.DTrans(), + "chip_trans": pya.DTrans(pya.DVector(10000, 0)) * pya.DTrans().M90, "dice_width": 140, "text_margin": 100, "mask_text_scale": 0.7, diff --git a/klayout_package/python/kqcircuits/masks/mask_export.py b/klayout_package/python/kqcircuits/masks/mask_export.py index b87ceb1ea..53e7a7824 100644 --- a/klayout_package/python/kqcircuits/masks/mask_export.py +++ b/klayout_package/python/kqcircuits/masks/mask_export.py @@ -171,7 +171,9 @@ def export_mask(export_dir, layer_name, mask_layout, mask_set): Args: export_dir: directory for the files - layer_name: name of the layer exported as a mask, if starts with '-' then it will be inverted + layer_name: name of the layer exported as a mask. The following prefixes can be used to modify the export: + * Prefix ``-``: invert the shapes on this layer + * Prefix ``^``: mirror the layer (left-right) mask_layout: MaskLayout object for the cell and face reference mask_set: MaskSet object for the name and version attributes to be included in the filename """ @@ -179,6 +181,10 @@ def export_mask(export_dir, layer_name, mask_layout, mask_set): if layer_name.startswith('-'): layer_name = layer_name[1:] invert = True + mirror = False + if layer_name.startswith('^'): + layer_name = layer_name[1:] + mirror = True top_cell = mask_layout.top_cell layout = top_cell.layout() @@ -193,6 +199,12 @@ def export_mask(export_dir, layer_name, mask_layout, mask_set): layout.clear_layer(layer) top_cell.shapes(layer).insert(wafer ^ disc) + if mirror: + wafer = pya.Region(top_cell.begin_shapes_rec(layer)).merged() + layout.copy_layer(layer, tmp_layer) + layout.clear_layer(layer) + top_cell.shapes(layer).insert(wafer.transformed(pya.Trans(2, True, 0, 0))) + layers_to_export = {layer_info.name: layer} path = export_dir / (_get_mask_layout_full_name(mask_set, mask_layout) + f"-{layer_info.name}.oas") _export_cell(path, top_cell, layers_to_export) diff --git a/klayout_package/python/kqcircuits/masks/mask_layout.py b/klayout_package/python/kqcircuits/masks/mask_layout.py index 3823814ba..3bf1ea50a 100644 --- a/klayout_package/python/kqcircuits/masks/mask_layout.py +++ b/klayout_package/python/kqcircuits/masks/mask_layout.py @@ -15,6 +15,8 @@ # (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements # for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). import math +from collections.abc import Sequence + from autologging import logged from tqdm import tqdm @@ -23,6 +25,7 @@ default_layers_to_mask, default_covered_region_excluded_layers, default_mask_export_layers, default_bar_format from kqcircuits.elements.markers.marker import Marker from kqcircuits.elements.markers.mask_marker_fc import MaskMarkerFc +from kqcircuits.util.geometry_helper import circle_polygon from kqcircuits.util.label import produce_label, LabelOrigin from kqcircuits.util.merge import merge_layout_layers_on_face, convert_child_instances_to_static @@ -51,10 +54,12 @@ class MaskLayout: wafer_bottom_flat_length: length of flat edge at the bottom of the wafer dice_width: Dicing width for this mask layout text_margin: Text margin for this mask layout - chip_size: side width of the chips (assuming square chips) + chip_size: side width of the chips (for square chips), or tuple (width, height) for rectangular chips chip_array_to_export: List of lists where for each chip in the array we have the following record - (row, column, Active/inactive, chip ID, chip type). Gets exported as an external csv file edge_clearance: minimum clearance of outer chips from the edge of the mask + remove_chips: if True (default), chips that violate edge_clearance or conflict with markers are removed from + chip maps. Note that ``extra_chips`` are never removed. chip_box_offset: Offset (pya.DVector) from chip origin of the chip frame boxes for this face chip_trans: DTrans applied to all chips mask_name_offset: (DEPRECATED) mask name label offset from default position (DPoint) @@ -74,6 +79,8 @@ class MaskLayout: top_cell: Top cell of this mask layout added_chips: List of (chip name, chip position, chip bounding box, chip dtrans, position_label) populated by chips added during build() + mirror_labels: Boolean, if True mask and chip copy labels are mirrored. Default False. + bbox_face_ids: List of face_ids to consider when calcualting the bounding box of chips. Defaults to [face_id] """ def __init__(self, layout, name, version, with_grid, chips_map, face_id, **kwargs): @@ -85,6 +92,7 @@ def __init__(self, layout, name, version, with_grid, chips_map, face_id, **kwarg self.face_id = face_id self.chips_map = chips_map self.chips_map_legend = None + self.chip_bounding_boxes = None self.layers_to_mask = kwargs.get("layers_to_mask", default_layers_to_mask) self.covered_region_excluded_layers = kwargs.get("covered_region_excluded_layers", @@ -98,7 +106,12 @@ def __init__(self, layout, name, version, with_grid, chips_map, face_id, **kwarg self.dice_width = kwargs.get("dice_width", default_mask_parameters[self.face_id]["dice_width"]) self.text_margin = kwargs.get("text_margin", default_mask_parameters[self.face_id]["text_margin"]) self.chip_size = kwargs.get("chip_size", default_mask_parameters[self.face_id]["chip_size"]) - self.edge_clearance = kwargs.get("edge_clearance", self.chip_size / 2) + if isinstance(self.chip_size, Sequence): + self.chip_width, self.chip_height = self.chip_size + else: + self.chip_width, self.chip_height = self.chip_size, self.chip_size + self.edge_clearance = kwargs.get("edge_clearance", (self.chip_width + self.chip_height) / 4) + self.remove_chips = kwargs.get("remove_chips", True) self.chip_box_offset = kwargs.get("chip_box_offset", default_mask_parameters[self.face_id]["chip_box_offset"]) self.chip_trans = kwargs.get("chip_trans", default_mask_parameters[self.face_id]["chip_trans"]) self.mask_name_offset = kwargs.get("mask_name_offset", pya.DPoint(0, 0)) # DEPRECATED @@ -114,6 +127,8 @@ def __init__(self, layout, name, version, with_grid, chips_map, face_id, **kwarg self.submasks = kwargs.get("submasks", []) self.extra_id = kwargs.get("extra_id", "") self.extra_chips = kwargs.get("extra_chips", []) + self.mirror_labels = kwargs.get("mirror_labels", False) + self.bbox_face_ids = kwargs.get("bbox_face_ids", [self.face_id]) self.top_cell = self.layout.create_cell(f"{self.name} {self.face_id}") self.added_chips = [] @@ -131,7 +146,7 @@ def __init__(self, layout, name, version, with_grid, chips_map, face_id, **kwarg self._min_x = 0 self._min_y = 0 - def add_chips_map(self, chips_map, align=None, align_to=None, chip_size=None): + def add_chips_map(self, chips_map, align=None, align_to=None, chip_size=None, chip_trans=None): """Add additional chip maps to the main chip map. The specified extra chip map, a.k.a. sub-grid, will be attached to the main grid. It may use @@ -143,9 +158,10 @@ def add_chips_map(self, chips_map, align=None, align_to=None, chip_size=None): align: to what side of the main grid this sub-grid attaches. Allowed values: top, left, right and bottom. align_to: optional exact point of placement. (x, y) coordinate tuple chip_size: a different chip size may be used in each sub-grid + chip_trans: chip transformation to use for chips in this sub-grid, defaults to self.chip_trans. """ chip_size = self.chip_size if not chip_size else chip_size - self.extra_chips_maps.append((chips_map, chip_size, align, align_to)) + self.extra_chips_maps.append((chips_map, chip_size, align, align_to, chip_trans)) def build(self, chips_map_legend): """Builds the cell hierarchy for this mask layout. @@ -160,6 +176,7 @@ def build(self, chips_map_legend): """ self.chips_map_legend = {} + self.chip_bounding_boxes = {} for name, cell in tqdm(chips_map_legend.items(), desc='Building cell hierarchy', bar_format=default_bar_format): self.chip_counts[name] = 0 @@ -168,6 +185,17 @@ def build(self, chips_map_legend): # create copies of the chips, so that modifying these only affects the ones in this MaskLayout new_cell = self.layout.create_cell(name) new_cell.copy_tree(cell) + + # Find the bounding box encompassing base metal gap shapes in all in bbox_face_ids + bboxes = [new_cell.dbbox_per_layer(self.layout.layer(default_faces[face_id]["base_metal_gap_wo_grid"])) + for face_id in self.bbox_face_ids] + if not all(b.empty() for b in bboxes): + p1_xs, p1_ys, p2_xs, p2_ys = zip(*[(b.p1.x, b.p1.y, b.p2.x, b.p2.y) + for b in bboxes if not b.empty()]) + self.chip_bounding_boxes[name] = pya.DBox(min(p1_xs), min(p1_ys), max(p2_xs), max(p2_ys)) + else: + self.chip_bounding_boxes[name] = pya.DBox() + # remove layers belonging to another face for face_id, face_dictionary in default_faces.items(): if face_id != self.face_id: @@ -205,8 +233,8 @@ def build(self, chips_map_legend): self._insert_mask_name_label(self.top_cell, default_layers["mask_graphical_rep"], 'G') # add chips from chips_map self._add_chips_from_map(self.chips_map, self.chip_size, None, self.align_to, marker_region) - for (chips_map, chip_size, align, align_to) in self.extra_chips_maps: - self._add_chips_from_map(chips_map, chip_size, align, align_to, marker_region) + for (chips_map, chip_size, align, align_to, chip_trans) in self.extra_chips_maps: + self._add_chips_from_map(chips_map, chip_size, align, align_to, marker_region, chip_trans) # add chips outside chips_map for name, pos, *optional in self.extra_chips: @@ -280,11 +308,11 @@ def get_position_label(i, j): raise ValueError(f"Duplicate use of chip position label {position_label}. " f"When using extra_chips, please make sure to only use unreserved position labels") used_position_labels.add(position_label) - bbox_x1 = bbox.left if dtrans.is_mirror() else bbox.right + bbox_x1 = bbox.left if (bool(dtrans.is_mirror()) ^ bool(self.mirror_labels)) else bbox.right produce_label(labels_cell_2, position_label, dtrans * (pya.DPoint(bbox_x1, bbox.bottom)), LabelOrigin.BOTTOMRIGHT, mask_layout.dice_width, mask_layout.text_margin, [mask_layout.face()[layer] for layer in layers], - mask_layout.face()["ground_grid_avoidance"]) + mask_layout.face()["ground_grid_avoidance"], mirror=self.mirror_labels) bbox_x2 = bbox.right if dtrans.is_mirror() else bbox.left mask_layout._add_chip_graphical_representation_layer(chip_name, dtrans * (pya.DPoint(bbox_x2, bbox.bottom)), @@ -324,13 +352,19 @@ def _mask_create_geometry(self): region_covered = pya.Region(pya.DPolygon(points).to_itype(self.layout.dbu)) return region_covered - def _add_chips_from_map(self, chips_map, chip_size, align, align_to, marker_region): + def _add_chips_from_map(self, chips_map, chip_size, align, align_to, marker_region, chip_trans=None): + if chip_trans is None: + chip_trans = self.chip_trans + if isinstance(chip_size, Sequence): + chip_width, chip_height = chip_size + else: + chip_width, chip_height = chip_size, chip_size orig = pya.DVector(-self.wafer_rad, self.wafer_rad) - self.chips_map_offset if align_to: orig = pya.DVector(*align_to) elif align: # autoalign to the specified side of the existing layout - w = len(chips_map[0]) * chip_size / 2 - h = len(chips_map) * chip_size + w = len(chips_map[0]) * chip_width / 2 + h = len(chips_map) * chip_height if align == "top": orig = pya.DVector(-w, h + self._max_y * self.layout.dbu) elif align == "bottom": @@ -342,48 +376,42 @@ def _add_chips_from_map(self, chips_map, chip_size, align, align_to, marker_regi if align in ("left", "right"): # rotate clockwise chips_map = zip(*reversed(chips_map)) - orig_chip_size = self.chip_size - self.chip_size = chip_size region_used = pya.Region() + allowed_region = pya.Region([circle_polygon(self.wafer_rad - self.edge_clearance).to_itype(self.layout.dbu)]) \ + - marker_region \ + - pya.Region(pya.DBox(-self.wafer_rad, self._mask_name_box_bottom_y, + self.wafer_rad, self.wafer_rad).to_itype(self.layout.dbu)) for (i, row) in enumerate(tqdm(chips_map, desc='Adding chips to mask', bar_format=default_bar_format)): for (j, name) in enumerate(row): if name == "---": continue - position = pya.DPoint(chip_size * j, -chip_size * (i + 1)) + orig - pos = position - self.wafer_center - test_x = chip_size if pos.x + chip_size / 2 > 0 else 0 - test_y = chip_size if pos.y + chip_size / 2 > 0 else 0 - d_edge = self.wafer_rad - (pos + pya.DVector(test_x, test_y)).abs() - if d_edge < self.edge_clearance: - print(f" Warning, dropping chip {name} at ({i}, {j}), '{self.face_id}' - too close to edge " - f" {d_edge:.2f} < {self.edge_clearance}") - elif pos.y + chip_size > self._mask_name_box_bottom_y: - print(f" Warning, dropping chip {name} at ({i}, {j}), '{self.face_id}' - too close to mask label " - f" {(pos.y + chip_size):.2f} < {self._mask_name_box_bottom_y}") - elif pya.Region(pya.Box(pos.x, pos.y, pos.x + chip_size, pos.y + chip_size) * (1 / self.layout.dbu)) \ - & marker_region: - print(f" Warning, dropping chip {name} at ({i}, {j}), '{self.face_id}' - overlaps with marker ") - else: - added_chip, region_chip = self._add_chip(name, position, self.chip_trans) - region_used += region_chip - if added_chip: - self.chip_counts[name] += 1 + position = pya.DPoint(chip_width * j, -chip_height * (i + 1)) + orig + added_chip, region_chip = self._add_chip(name, position, chip_trans, allowed_region=allowed_region, + chip_width=chip_width) + region_used += region_chip + if added_chip: + self.chip_counts[name] += 1 self.region_covered -= region_used box = region_used.bbox() self._min_x = min(box.p1.x, self._min_x) self._min_y = min(box.p1.y, self._min_y) self._max_x = max(box.p2.x, self._max_x) self._max_y = max(box.p2.y, self._max_y) - self.chip_size = orig_chip_size - def _add_chip(self, name, position, trans, position_label=None): + def _add_chip(self, name, position, trans, position_label=None, allowed_region=None, chip_width=None): """Returns a tuple (Boolean telling if the chip was added, Region which the chip covers).""" + if chip_width is None: + chip_width = self.chip_width chip_region = pya.Region() if name in self.chips_map_legend.keys(): - chip_cell, bounding_box, bbox_offset = self._get_chip_cell_and_bbox(name) + chip_cell = self.chips_map_legend[name] + bounding_box = self.chip_bounding_boxes[name] + bbox_offset = chip_width - bounding_box.width() trans = pya.DTrans(position + pya.DVector(bbox_offset, 0) - self.chip_box_offset) * trans - self.top_cell.insert(pya.DCellInstArray(chip_cell.cell_index(), trans)) chip_region = pya.Region(pya.Box(trans * bounding_box * (1 / self.layout.dbu))) + if self.remove_chips and allowed_region is not None and chip_region.inside(allowed_region).is_empty(): + return False, pya.Region() + self.top_cell.insert(pya.DCellInstArray(chip_cell.cell_index(), trans)) self.added_chips.append((name, position, bounding_box, trans, position_label)) return True, chip_region return False, chip_region @@ -439,13 +467,7 @@ def _get_chip_name(self, search_cell): return chip_name return "" - def _get_chip_cell_and_bbox(self, chip_name): - chip_cell = self.chips_map_legend[chip_name] - bounding_box = chip_cell.dbbox_per_layer(self.layout.layer(self.face()["base_metal_gap_wo_grid"])) - bbox_offset = self.chip_size - bounding_box.width() # for chips that are smaller than self.chip_size - return chip_cell, bounding_box, bbox_offset - - def _add_chip_graphical_representation_layer(self, chip_name, position, pos_index_name, chip_size, cell): + def _add_chip_graphical_representation_layer(self, chip_name, position, pos_index_name, chip_width, cell): chip_name_text = self.layout.create_cell("TEXT", "Basic", { "layer": default_layers["mask_graphical_rep"], "text": chip_name, @@ -456,10 +478,10 @@ def _add_chip_graphical_representation_layer(self, chip_name, position, pos_inde "text": pos_index_name, "mag": 4000 * self.mask_text_scale, }) - chip_name_trans = pya.DTrans(position + pya.DVector((chip_size - chip_name_text.dbbox().width()) / 2, + chip_name_trans = pya.DTrans(position + pya.DVector((chip_width - chip_name_text.dbbox().width()) / 2, self.mask_text_scale * 750)) cell.insert(pya.DCellInstArray(chip_name_text.cell_index(), chip_name_trans)) - pos_index_trans = pya.DTrans(position + pya.DVector((chip_size - pos_index_name_text.dbbox().width()) / 2, + pos_index_trans = pya.DTrans(position + pya.DVector((chip_width - pos_index_name_text.dbbox().width()) / 2, self.mask_text_scale * 6000)) cell.insert(pya.DCellInstArray(pos_index_name_text.cell_index(), pos_index_trans)) @@ -484,5 +506,6 @@ def _create_mask_name_label(self, layer, postfix=""): self._mask_name_box_bottom_y = cell_mask_name_y - cell_mask_name_h - 2 * self.mask_name_box_margin trans = pya.DTrans(- self._mask_name_letter_I_offset - cell_mask_name_w / 2, cell_mask_name_y - cell_mask_name_h - self.mask_name_box_margin) - + if self.mirror_labels: + trans *= pya.DTrans(2, True, -2 * trans.disp.x, 0) return cell_mask_name, trans diff --git a/klayout_package/python/kqcircuits/masks/mask_set.py b/klayout_package/python/kqcircuits/masks/mask_set.py index 0d5fcd101..902054965 100644 --- a/klayout_package/python/kqcircuits/masks/mask_set.py +++ b/klayout_package/python/kqcircuits/masks/mask_set.py @@ -28,6 +28,7 @@ from tqdm import tqdm from kqcircuits.chips.chip import Chip +from kqcircuits.masks.multi_face_mask_layout import MultiFaceMaskLayout from kqcircuits.util.log_router import route_log from kqcircuits.pya_resolver import pya, is_standalone_session from kqcircuits.defaults import default_bar_format, TMP_PATH, default_face_id @@ -102,7 +103,6 @@ def __init__(self, view=None, name="MaskSet", version=1, with_grid=False, export if '-c' in argv and len(argv) > argv.index('-c') + 1: self._cpu_override = int(argv[argv.index('-c') + 1]) - def add_mask_layout(self, chips_map, face_id=default_face_id, mask_layout_type=MaskLayout, **kwargs): """Creates a mask layout from chips_map and adds it to self.mask_layouts. @@ -123,6 +123,34 @@ def add_mask_layout(self, chips_map, face_id=default_face_id, mask_layout_type=M self.mask_layouts.append(mask_layout) return mask_layout + def add_multi_face_mask_layout(self, face_ids, chips_map=None, extra_face_params=None, mask_layout_type=MaskLayout, + **kwargs): + """Create a multi face mask layout, which can be used to make masks with matching chip maps on multiple faces. + + A ``MaskLayout`` is created of each face in ``face_ids``. By default, the individual mask layouts all have + identical parameters, but parameters can be overwritten for a single face id through ``extra_face_params``. + + By default, ``bbox_face_ids`` is set to ``face_ids`` for all mask layouts. + + Args: + face_ids: list of face ids to include + chips_map: Chips map to use, or None to use an empty chips map. + extra_face_params: a dictionary of ``{face_id: extra_kwargs}``, where ``extra_kwargs`` is a dictionary of + keyword arguments to apply only to the mask layout for ``face_id``. + mask_layout_type: optional subclass of MaskLayout to use + kwargs: any keyword arguments are passed to all containing mask layouts. + + Returns: a ``MultiFaceMaskLayout`` instance + """ + if ("mask_export_layers" not in kwargs) and self.mask_export_layers: + kwargs["mask_export_layers"] = self.mask_export_layers + + mfml = MultiFaceMaskLayout(self.layout, self.name, self.version, self.with_grid, face_ids, + chips_map, extra_face_params, mask_layout_type, **kwargs) + for face_id in mfml.face_ids: + self.mask_layouts.append(mfml.mask_layouts[face_id]) + return mfml + def add_chip(self, chips, variant_name=None, cpus=None, **parameters): """Adds a chip (or list of chips) with parameters to self.chips_map_legend and exports the files for each chip. diff --git a/klayout_package/python/kqcircuits/masks/multi_face_mask_layout.py b/klayout_package/python/kqcircuits/masks/multi_face_mask_layout.py new file mode 100644 index 000000000..922fdc720 --- /dev/null +++ b/klayout_package/python/kqcircuits/masks/multi_face_mask_layout.py @@ -0,0 +1,75 @@ +# This code is part of KQCircuits +# Copyright (C) 2023 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). +from kqcircuits.masks.mask_layout import MaskLayout + + +class MultiFaceMaskLayout: + """Class representing multiple mask layouts, corresponding to multiple faces on the same wafer. + + This is a helper class to create multiple ``MaskLayout`` instances, one for each face, and set the same properties + and chips map for each, and a container for the created mask layouts. It also provides `add_chips_map` that + distributes over each containing `MaskLayout.add_chips_map`. + + The usual way to instantiate ``MultiFaceMaskLayout`` is through ``MaskSet.add_multi_face_mask_layout``. + + Attributes: + face_ids: List of face ids to include in this mask layout + mask_layouts: Dictionary of {face_id: mask_layout} of the individual ``MaskLayouts`` contained in this class + """ + def __init__(self, layout, name, version, with_grid, face_ids, chips_map=None, extra_face_params=None, + mask_layout_type=MaskLayout, **kwargs): + """Create a multi face mask layout, which can be used to make masks with matching chip maps on multiple faces. + + A ``MaskLayout`` is created of each face in ``face_ids``. If ``face_ids`` is a list, the individual mask layouts + all have identical parameters. To specify some parameters differently for each mask layout, supply ``face_ids`` + as a dictionary ``{face_ids: extra_params}``, where ``extra_params`` is a dictionary of arguments passed only + to the mask layout for that face id. These override ``kwargs`` if they contain the same keys. + + By default, ``bbox_face_ids`` is set to ``list(face_ids)`` for all mask layouts. + + Args: + layout: Layout to use + name: name of the mask + version: version of the mask + with_grid: if True, ground grids are generated + face_ids: either a list of face ids to include, or a dictionary of ``{face_id: extra_params}``, where + ``extra_params`` is a dictionary of keyword arguments to apply only to this mask layout. + chips_map: Chips map to use, or None to use an empty chips map. + mask_layout_type: optional subclass of MaskLayout to use + kwargs: any keyword arguments are passed to all containing mask layouts. + """ + self.face_ids = face_ids + self.mask_layouts = {} + + for face_id in face_ids: + all_kwargs = {'bbox_face_ids': self.face_ids} + all_kwargs.update(kwargs) + if extra_face_params is not None and face_id in extra_face_params: + all_kwargs.update(**extra_face_params[face_id]) + self.mask_layouts[face_id]: MaskLayout = mask_layout_type( + layout=layout, + name=name, + version=version, + with_grid=with_grid, + face_id=face_id, + chips_map=chips_map if chips_map is not None else [[]], + **all_kwargs + ) + + def add_chips_map(self, chips_map, **kwargs): + for face_id in self.face_ids: + self.mask_layouts[face_id].add_chips_map(chips_map, **kwargs) diff --git a/klayout_package/python/kqcircuits/util/label.py b/klayout_package/python/kqcircuits/util/label.py index 7a0cc7992..1e2bf56f4 100644 --- a/klayout_package/python/kqcircuits/util/label.py +++ b/klayout_package/python/kqcircuits/util/label.py @@ -27,7 +27,8 @@ class LabelOrigin(Enum): TOPLEFT = auto() TOPRIGHT = auto() -def produce_label(cell, label, location, origin, origin_offset, margin, layers, layer_protection, size=350): +def produce_label(cell, label, location, origin, origin_offset, margin, layers, layer_protection, size=350, + mirror=False): """Produces a Text PCell accounting for desired relative position of the text respect to the given location and the spacing. @@ -47,7 +48,6 @@ def produce_label(cell, label, location, origin, origin_offset, margin, layers, """ layout = cell.layout() - dbu = layout.dbu if not label: label = "A13" # longest label on 6 inch wafer @@ -65,35 +65,32 @@ def produce_label(cell, label, location, origin, origin_offset, margin, layers, })) # relative placement with margin - margin = margin / dbu - origin_offset = origin_offset / dbu - - trans = pya.DTrans(location + { + relative_placement = { LabelOrigin.BOTTOMLEFT: pya.Vector( - subcells[0].bbox().p1.x - margin - origin_offset, - subcells[0].bbox().p1.y - margin - origin_offset), + subcells[0].dbbox().p1.x - margin - origin_offset, + subcells[0].dbbox().p1.y - margin - origin_offset), LabelOrigin.TOPLEFT: pya.Vector( - subcells[0].bbox().p1.x - margin - origin_offset, - subcells[0].bbox().p2.y + margin + origin_offset), + subcells[0].dbbox().p1.x - margin - origin_offset, + subcells[0].dbbox().p2.y + margin + origin_offset), LabelOrigin.TOPRIGHT: pya.Vector( - subcells[0].bbox().p2.x + margin + origin_offset, - subcells[0].bbox().p2.y + margin + origin_offset), + subcells[0].dbbox().p2.x + margin + origin_offset, + subcells[0].dbbox().p2.y + margin + origin_offset), LabelOrigin.BOTTOMRIGHT: pya.Vector( - subcells[0].bbox().p2.x + margin + origin_offset, - subcells[0].bbox().p1.y - margin - origin_offset), - }[origin] * dbu * (-1)) + subcells[0].dbbox().p2.x + margin + origin_offset, + subcells[0].dbbox().p1.y - margin - origin_offset), + }[origin] * (-1) + + if mirror: + trans = pya.DTrans(2, True, location.x - relative_placement.x, location.y + relative_placement.y) + else: + trans = pya.DTrans(location + relative_placement) if not protection_only: for subcell in subcells: cell.insert(pya.DCellInstArray(subcell.cell_index(), trans)) # protection layer with margin - protection = pya.DBox(pya.Point( - subcells[0].bbox().p1.x - margin, - subcells[0].bbox().p1.y - margin) * dbu, - pya.Point( - subcells[0].bbox().p2.x + margin, - subcells[0].bbox().p2.y + margin) * dbu - ) + protection = pya.DBox(pya.DPoint(subcells[0].dbbox().p1.x - margin, subcells[0].dbbox().p1.y - margin), + pya.DPoint(subcells[0].dbbox().p2.x + margin, subcells[0].dbbox().p2.y + margin)) cell.shapes(layout.layer(layer_protection)).insert( trans.trans(protection)) diff --git a/klayout_package/python/scripts/masks/demo_multiface.py b/klayout_package/python/scripts/masks/demo_multiface.py new file mode 100644 index 000000000..6c71b1c95 --- /dev/null +++ b/klayout_package/python/scripts/masks/demo_multiface.py @@ -0,0 +1,108 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). + +""" +This mask layout demonstrates using multiple faces on a single wafer. As example, we use `1t1` as the top face, and +`1b1` as the bottom face of the wafer. We draw some example chips with metalization on both faces, and TSVs to connect +them together. + +A separate MaskLayout is generated for each face, which means that things like markers can be customized on each side +of the wafer if needed. However, it is important that the chips on both sides line up exactly. The method +`MaskSet.add_multi_face_mask_layout` helps with this: it creates identical mask layouts for multiple faces, except for +any differences per face that are specified explicitly. + +To generate this example, run `kqc mask demo_multiface.py` in the command line. + +In this example, we use the convention that in the full mask files (`DemoMF_v1_1b1.oas` and `DemoMF_v1_1t1.oas`), all +shapes are seen from the top, so looking at `1t1` and seeing `1b1` "through the wafer". Hence, `1t1` is drawn with text +readable normally, and `1b1` with text mirrored. One can verify the combined mask of both side by opening both of these +`oas` files in KLayout in the same panel. + +To draw the mirrored texts in `1b1`, we set `mirror_labels=True` for the mask layout, and also set +`frames_mirrored[1]=True` for each chip, where 1 is the chip frame index of `1b1` in our case. + +For fabrication, usually the photomasks will have to be mirrored for the `1b1` face, such that they are again normal +when looking at the wafer from the bottom. In this script, the mirroring is done for all `1b1` mask exports, using the +`^` prefix in `mask_export_layers`. You can verify this by opening `DemoMF_v1-1b1-1b1_base_metal_gap.oas` for example, +here the texts are normally readable again. + +This example also shows adding multiple sizes of chips to the same mask layout, and using rectangular vs square chips. +""" + +from kqcircuits.chips.chip import Chip +from kqcircuits.chips.sample_holder_test import SampleHolderTest +from kqcircuits.defaults import default_marker_type +from kqcircuits.masks.mask_set import MaskSet +from kqcircuits.pya_resolver import pya + +mask_set = MaskSet(name="DemoMF", version=1, with_grid=False) + +# Create a multi-face mask layout with regular chip maps (default size 10x10mm) +wafer_1 = mask_set.add_multi_face_mask_layout( + chips_map=[ + ["CH1"] * 15 + ] * 7, + face_ids=['1t1', '1b1'], + extra_face_params={ + '1t1': { + "layers_to_mask": {"base_metal_gap": "1", "through_silicon_via": "2"}, + "mask_export_layers": ["base_metal_gap", "through_silicon_via"] + }, + '1b1': { + "mirror_labels": True, # Mask label and chip copy labels are mirrored on the bottom side of the wafer + "layers_to_mask": {"base_metal_gap": "1", "through_silicon_via": "2"}, + "mask_export_layers": ["^base_metal_gap", "^through_silicon_via"] # Mirror individual output files + }, + } +) + +# Add 20x10mm chips on part of the chip +wafer_1.add_chips_map( + [ + ["ST1"] * 6, + ] * 7, + align_to=(-65000, 5000), # Top-left corner of the chip map + chip_size=(20000, 10000), # (width, height) of the ST1 chip +) + +# Chip parameters for an empty multi-face chip that uses `1t1` and `1b1` +multi_face_parameters = { + "face_ids": ['1t1', '2b1', '1b1', '2t1'], + "frames_enabled": [0, 2], + "frames_marker_dist": [1500, 1500], + "frames_mirrored": [False, True], + "frames_dice_width": [200, 200], + "face_boxes": [None] * 4, # Same size on all faces + "with_gnd_tsvs": True, + "marker_types": [default_marker_type]*8 +} + +# Chip parameters for a rectangular 20x10 mm chip. +# Note: only some chips in the KQC library support dynamic resizing, typically one would create a custom chip with +# the size and design needed. +rectangular_parametes = { + **multi_face_parameters, + "box": pya.DBox(0, 0, 20000, 10000), +} + +mask_set.add_chip([ + (Chip, "CH1", multi_face_parameters), + (SampleHolderTest, "ST1", rectangular_parametes) +]) + +mask_set.build() +mask_set.export()