Source code for qnngds.sample

"""Sample defines the Sample class which is used to generate a wafer/piece layout from multiple experiments"""

# can be removed in python 3.14, see https://peps.python.org/pep-0749/
from __future__ import annotations

import numpy as np

import qnngds as qg
import phidl.geometry as pg

from qnngds.typing import LayerSpec, DeviceSpec
from qnngds import Device

from collections.abc import Sequence
from collections import deque

from itertools import product

from phidl import quickplot as qp


[docs]class PlaceError(Exception): """Exception raised when placement fails"""
[docs] def __init__(self, message): """Constructor for PlaceError Args: message (str): error message """ self.message = message super().__init__(self.message)
def _wafer(radius: float, flat: float) -> Device: """Generic template for wafers Args: radius (float): radius of wafer flat (float): length of primary flat Returns: (Device): wafer template """ flat_dist = (radius**2 - (flat / 2) ** 2) ** 0.5 circ = pg.circle(radius=radius, angle_resolution=2.5, layer=(1, 0)) FLAT = Device() flat = FLAT << pg.rectangle(size=(flat, radius - flat_dist), layer=(1, 0)) flat.move((flat.x, flat.ymin), (circ.x, circ.ymin)) W = Device(f"wafer_{round(radius / 1e3)}") w_i = W << pg.kl_boolean(A=circ, B=FLAT, operation="A-B", layer=(1, 0)) w_i.move(w_i.center, (0, 0)) return W
[docs]@qg.device def wafer150mm() -> Device: """Template for 150 mm wafer""" return _wafer(radius=75e3, flat=57.5e3)
[docs]@qg.device def wafer100mm() -> Device: """Template for 100 mm wafer""" return _wafer(radius=50e3, flat=32.5e3)
[docs]@qg.device def piece10mm(): """Template for 10 mm piece""" P = Device("piece_10") p_i = P << pg.rectangle(size=(10e3, 10e3), layer=(1, 0)) p_i.move(p_i.center, (0, 0)) return P
[docs]class Sample(object): """Class for managing die/experiment area, with manual placement and basic autoplacement Defines a grid size and divides a sample (wafer/piece) into cells. Experiments (generated with :py:func:`qnngds.utilities.generate_experiment`) can be placed on one or more cells in the grid, manually or automatically. If initialized with ``allow_cell_span``, experiments can span multiple columns/rows. For a wafer, it is recommended to have two hierarchies of ``Sample`` s. At the top-level, define a wafer-sized Sample, with a number of cells that can be cut/cleaved into dies. Each cell may be defined as a ``Sample`` to place multiple experiments on a grid within the die. For example: >>> my_experiment_die_1 = qg.sample.Sample( >>> cell_size=1e3, sample=qg.sample.piece10mm, edge_exclusion=500, allow_cell_span=True, >>> ) >>> # place experiments (e.g. circuits, devices, lithographic structures etc.) on nTron_die >>> # ... >>> # create other dies >>> # ... >>> # ... >>> sample = qg.sample.Sample( >>> cell_size=10e3, sample=qg.sample.wafer100mm, edge_exclusion=10e3, allow_cell_span=False, >>> ) >>> # place nTron_die and other dies on sample """
[docs] def __init__( self, cell_size: float = 10000, sample: DeviceSpec | Device = wafer100mm, edge_exclusion: float = 10000, allow_cell_span: bool = False, ) -> None: """Constructor for Sample class Args: cell_size (float): pitch of cell grid sample (Device | DeviceSpec): desired sample shape (e.g. wafer/piece) edge_exclusion (float): desired edge exclusion for die placement allow_cell_span (bool): if True, allows a device added to the sample to span multiple cells Returns: None """ # Device containing all devices added to the sample self.devices = Device("top") # save the cell_size self.cell_size = cell_size # save if cell spanning is allowed self.allow_cell_span = allow_cell_span ################################## # Determine the legal cell regions # that do not fall within the # edge exclusion region ################################## self.sample = qg.get_device(sample) mask_region = pg.kl_offset( self.sample, distance=-edge_exclusion, tile_size=None, layer=(1, 0) ) # get number of rows/columns self.n_cols = int(np.floor(mask_region.xsize / cell_size)) self.n_rows = int(np.floor(mask_region.ysize / cell_size)) # create a device whose area defines which placements are legal mask = Device() mask.add_ref(mask_region) mask.move(mask.center, (0, 0)) # coordinates from left-to-right, top-to-bottom self.origin = (-self.n_cols * cell_size / 2, self.n_rows * cell_size / 2) self.open_cells = set([]) self.full_cells = set([]) rect = pg.rectangle(size=(cell_size, cell_size), layer=(1, 0)) dummy = Device() d = dummy << rect for row in range(self.n_rows): for col in range(self.n_cols): d.move((d.xmin, d.ymax), self.origin).movex(col * cell_size).movey( -row * cell_size ) intersection = pg.kl_boolean( A=d, B=mask, tile_size=(mask_region.xsize, mask_region.ysize), operation="A-B", layer=(1, 0), ) intersection_polygon = intersection.get_polygons() if len(intersection_polygon) == 0: self.open_cells.add((row, col)) self.bounds = self.open_cells.copy()
[docs] def visualize_open_cells(self) -> Device: """Visualize open cells Returns: (Device): device used for visualization """ dies = Device() rect = pg.rectangle(size=(self.cell_size, self.cell_size), layer=(1, 0)) for cell in self.open_cells: d = dies << rect d.move((d.xmin, d.ymax), self.origin).movex(cell[1] * self.cell_size).movey( -cell[0] * self.cell_size ) qp(dies) return dies
@staticmethod def _get_bbox_extents(cell_coordinate_bbox: tuple) -> tuple: """Gets extents of a cell coordinate bounding box Args: cell_coordinate_bbox (tuple[tuple[int, int], tuple[int, int]]): bounding box of cell coordiantes within which a device should be placed. Returns: (tuple): xmin, ymin, xmax, ymax, row_span, col_span defined by bounding box """ ymin, xmin = map(int, np.min(np.array(cell_coordinate_bbox), axis=0)) ymax, xmax = map(int, np.max(np.array(cell_coordinate_bbox), axis=0)) row_span = range(ymin, ymax + 1) col_span = range(xmin, xmax + 1) return xmin, ymin, xmax, ymax, row_span, col_span
[docs] def place_on_sample( self, device: Device, cell_coordinate_bbox: tuple, ignore_collisions: bool = False, ) -> None: """Place device on sample See also :py:meth:`place_multiple_on_sample`. Args: device (Device): device to place cell_coordinate_bbox (tuple[int, int] | tuple[tuple[int, int], tuple[int, int]]): bounding box of cell coordiantes within which the device should be placed. If the device fits within a single cell, then a tuple[int, int] is acceptable instead of passing a tuple with identical coordinates for the bbox. If device spans multiple cells, then the bbox coordinates must be unique. ignore_collisions (bool): If True, ignores any collision of device with previously-placed devices. Returns: None Side Effects: Updates self.open_cells to remove newly allocated cells Updates self.full_cells to add newly allocated cells """ spans_multiple = self._check_device_size(device) # check that device fits within specified bounding box if not isinstance(cell_coordinate_bbox[0], tuple) and spans_multiple: raise ValueError( f"device {device.name} spans multiple cells, " "but bbox-like coordinates were not provided" ) # convert singleton to bbox with identical endpoints if not isinstance(cell_coordinate_bbox[0], tuple): cell_coordinate_bbox = (cell_coordinate_bbox, cell_coordinate_bbox) assert np.array(cell_coordinate_bbox).shape == (2, 2) # check that all cells within the bbox are available xmin, ymin, xmax, ymax, row_span, col_span = Sample._get_bbox_extents( cell_coordinate_bbox ) cells_to_occupy = set(product(row_span, col_span)) for row in row_span: for col in col_span: if (row, col) not in self.bounds: raise PlaceError( f"cell {(row, col)=} is outside of sample " f"when attempting to place {device.name=}" ) if (row, col) not in self.open_cells: error_msg = ( f"cell {(row, col)=} is occupied when " f"attempting to place {device.name=}" ) # cell isn't open if (row, col) in self.full_cells: # cell is occupied if not ignore_collisions: raise PlaceError(error_msg) else: # illegal cell (e.g. in exclusion region) raise PlaceError(error_msg) # Placement can proceed. # First update our open/full tracking sets self.open_cells.difference_update(cells_to_occupy) self.full_cells.update(cells_to_occupy) # actually place the devices c = self.devices.add_ref(device) # move the device dcenter = np.array(((xmin + xmax + 1), -(ymin + ymax + 1))) * self.cell_size / 2 c.move(c.center, np.array(self.origin) + dcenter)
[docs] def place_multiple_on_sample( self, devices: Sequence[Device], cell_coordinate_bbox: tuple, column_major: bool = True, ignore_collisions: bool = False, ) -> None: """Place devices on sample See also :py:meth:`place_on_sample`. Args: devices (Sequence[Device]): sequence of devices to place cell_coordinate_bbox (tuple[tuple[int, int], tuple[int, int]]): bounding box of cell coordiantes within which the device should be placed. column_major (bool): If True, orders devices in column-major order within bbox. (top-to-bottom, then left-to-right). Otherwise, orders row-major (left-to-right, then top-to-bottom). ignore_collisions (bool): If True, ignores any collision of device with previously-placed devices. Returns: None Side Effects: Updates self.open_cells to remove newly allocated cells Updates self.full_cells to add newly allocated cells """ # infer desired grid from coordinate bbox _, _, _, _, row_span, col_span = Sample._get_bbox_extents(cell_coordinate_bbox) # iterate over ax_inner within ax_outer loop # by default, column-major iterates over rows within column loop ax_outer = list(col_span) ax_inner = list(row_span) if not column_major: ax_outer, ax_inner = ax_inner, ax_outer device_queue = deque(devices) for iout, outer in enumerate(ax_outer): for iin, inner in enumerate(ax_inner): if len(device_queue) == 0: break row = inner col = outer if not column_major: row, col = col, row if (row, col) in self.open_cells: # generate a bounding box device = device_queue.popleft() rows = int(np.ceil(device.ysize / self.cell_size)) cols = int(np.ceil(device.xsize / self.cell_size)) bbox = ((row, col), (row + rows - 1, col + cols - 1)) if bbox[0] not in self.bounds or bbox[1] not in self.bounds: # make sure extents of proposed bbox are in bounds device_queue.appendleft(device) continue try: self.place_on_sample( device, cell_coordinate_bbox=bbox, ignore_collisions=ignore_collisions, ) except PlaceError: device_queue.appendleft(device) if len(device_queue) > 0: raise PlaceError( "insufficient area provided, available space exhausted and " f"still have {len(device_queue)} remaining devices." )
[docs] def write_cell_corners(self, width: float, layer: LayerSpec) -> None: """Adds corner markers to all full cells Args: width (float): width of corner marker layer (LayerSpec): marker's layer specification Returns: None Side Effects: Updates self.devices with the new markers """ corner = pg.L( width=width, size=(5 * width, 5 * width), layer=qg.get_layer(layer) ) die_corners = Device("corners") for i in range(4): c = die_corners << corner c.rotate(90 * i) if i == 0 or i == 3: c.movex(-c.xmin) else: c.movex(self.cell_size - c.xmax) if i // 2 == 0: c.movey(-self.cell_size - c.ymin) else: c.movey(-c.ymax) for cell in self.full_cells: marks = self.devices << die_corners dcenter = np.array((2 * cell[1] + 1, -2 * cell[0] - 1)) * self.cell_size / 2 marks.move(marks.center, np.array(self.origin) + dcenter)
[docs] def write_cell_labels( self, size: float, layer: LayerSpec, inset_dist: float, location: int ) -> None: """Adds text label to all cells Args: size (float): text size layer (LayerSpec): text's layer specification inset_dist (float): distance between label and corner location (int): 0 -> NW, 1 -> NE, 2 -> SE, 3 -> SW Returns: None Side Effects: Updates self.devices with the new labels """ row_digits = int(np.ceil(np.log(self.n_rows) / np.log(26))) col_digits = int(np.ceil(np.log(self.n_cols) / np.log(10))) for cell in self.full_cells: col_str = f"{cell[1]:0{col_digits}d}" row = cell[0] row_str = "" for _ in range(row_digits): row_str = chr(65 + (row % 26)) + row_str row = row // 26 label = self.devices << pg.text( text=row_str + col_str, size=size, layer=qg.get_layer(layer), justify="center", ) if ((location + 1) % 4) // 2 > 0: label.movex(self.cell_size - inset_dist - label.xmax) else: label.movex(inset_dist - label.xmin) if location // 2 == 0: label.movey(self.cell_size - inset_dist - label.ymax) else: label.movey(inset_dist - label.ymin) dcenter = np.array((2 * cell[1] + 1, -2 * cell[0] - 1)) * self.cell_size / 2 label.move( (self.cell_size / 2, self.cell_size / 2), np.array(self.origin) + dcenter, )
[docs] def write_alignment_marks( self, marker_spec: DeviceSpec | Device, location: tuple[float, float] ) -> None: """Adds alignment markers Args: marker_spec (DeviceSpec | Device): marker to use (e.g. cross) location (tuple[float, float]): location of top-right alignment mark. makes symmetric alignment marks about origin (0, 0) Returns: None Side Effects: Updates self.devices with the alignment markers """ marker_refs = self.devices.add_array( qg.get_device(marker_spec), columns=2, rows=2, spacing=(2 * location[0], 2 * location[1]), ) marker_refs.center = (0, 0)
[docs] def num_full_cells(self) -> int: """Get number of cells in sample that are filled""" return len(self.full_cells)
[docs] def num_open_cells(self) -> int: """Get number of cells in sample that are open""" return len(self.open_cells)
def _check_device_size(self, device: Device) -> bool: """Checks device size Args: device (Device): device to check Returns: True if device spans multiple cells Raises: RuntimeError if device requires more than one (1) cell of area, but ``allow_cell_span`` is false """ spans_multiple = device.xsize > self.cell_size or device.ysize > self.cell_size if spans_multiple and not self.allow_cell_span: raise RuntimeError( "allow_cell_span is set to False, " f"but the provided device {device.name} " "is larger than a single cell {self.cell_size=}" ) return spans_multiple