diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index f5579c4..5cae36e 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -5,9 +5,8 @@ from numpy import pi from masque import layer_t, Pattern, SubPattern, Label from masque.shapes import Polygon -from masque.builder import Device, Port +from masque.builder import Device, Port, port_utils from masque.file.gdsii import writefile -from masque.utils import rotation_matrix_2d import pcgen import basic_shapes @@ -18,6 +17,22 @@ LATTICE_CONSTANT = 512 RADIUS = LATTICE_CONSTANT / 2 * 0.75 +def dev2pat(dev: Device) -> Pattern: + """ + Bake port information into the device. + This places a label at each port location on layer (3, 0) with text content + 'name:ptype angle_deg' + """ + return port_utils.dev2pat(dev, layer=(3, 0)) + + +def pat2dev(pat: Pattern) -> Device: + """ + Scans the Pattern to determine port locations. Same format as `dev2pat` + """ + return port_utils.pat2dev(pat, layers=[(3, 0)]) + + def perturbed_l3( lattice_constant: float, hole: Pattern, @@ -201,78 +216,6 @@ def y_splitter( return Device(pat, ports) -def dev2pat(device: Device, layer: layer_t = (3, 0)) -> Pattern: - """ - Place a text label at each port location, specifying the port data. - - This can be used to debug port locations or to automatically generate ports - when reading in a GDS file. - - NOTE that `device` is modified by this function, and `device.pattern` is returned. - - Args: - device: The device which is to have its ports labeled. MODIFIED in-place. - layer: The layer on which the labels will be placed. - - Returns: - `device.pattern` - """ - for name, port in device.ports.items(): - if port.rotation is None: - angle_deg = numpy.inf - else: - angle_deg = numpy.rad2deg(port.rotation) - device.pattern.labels += [ - Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset) - ] - return device.pattern - - -def pat2dev( - pattern: Pattern, - layers: Sequence[layer_t] = ((3, 0),), - max_depth: int = 999_999, - skip_subcells: bool = True, - ) -> Device: - ports = {} # Note: could do a list here, if they're not unique - annotated_cells = set() - def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: - if len(hierarchy) > max_depth - 1: - return pat - - if skip_subcells and any(parent in annotated_cells for parent in hierarchy): - return pat - - labels = [ll for ll in pat.labels if ll.layer in layers] - - if len(labels) == 0: - return pat - - if skip_subcells: - annotated_cells.add(pat) - - mirr_factor = numpy.array((1, -1)) ** transform[3] - rot_matrix = rotation_matrix_2d(transform[2]) - for label in labels: - name, property_string = label.string.split(':') - properties = property_string.split(' ') - ptype = properties[0] - angle_deg = float(properties[1]) if len(ptype) else 0 - - xy_global = transform[:2] + rot_matrix @ (label.offset * mirr_factor) - angle = numpy.deg2rad(angle_deg) * mirr_factor[0] * mirr_factor[1] + transform[2] - - if name in ports: - raise Exception('Duplicate port name in pattern!') - - ports[name] = Port(offset=xy_global, rotation=angle, ptype=ptype) - - return pat - - pattern.dfs(visit_before=find_ports_each, transform=True) - return Device(pattern, ports) - - def main(interactive: bool = True): # Generate some basic hole patterns diff --git a/examples/tutorial/library.py b/examples/tutorial/library.py index 2154232..2dc2af6 100644 --- a/examples/tutorial/library.py +++ b/examples/tutorial/library.py @@ -11,6 +11,7 @@ from masque.file.gdsii import writefile, load_libraryfile import pcgen import basic_shapes import devices +from devices import pat2dev, dev2pat from basic_shapes import GDS_OPTS @@ -29,7 +30,7 @@ def main() -> None: # Add it into the device library by providing a way to read port info # This maintains the lazy evaluation from above, so no patterns # are actually read yet. - device_lib.add_library(pattern_lib, pat2dev=devices.pat2dev) + device_lib.add_library(pattern_lib, pat2dev=pat2dev) print('Devices loaded from GDS into library:\n' + pformat(list(device_lib.keys()))) @@ -43,10 +44,10 @@ def main() -> None: # Convenience function for adding devices # This is roughly equivalent to - # `device_lib[name] = lambda: devices.dev2pat(fn())` + # `device_lib[name] = lambda: dev2pat(fn())` # but it also guarantees that the resulting pattern is named `name`. def add(name: str, fn: Callable[[], Device]) -> None: - device_lib.add_device(name=name, fn=fn, dev2pat=devices.dev2pat) + device_lib.add_device(name=name, fn=fn, dev2pat=dev2pat) # Triangle-based variants. These are defined here, but they won't run until they're # retrieved from the library. diff --git a/masque/builder/port_utils.py b/masque/builder/port_utils.py new file mode 100644 index 0000000..5746dde --- /dev/null +++ b/masque/builder/port_utils.py @@ -0,0 +1,112 @@ +""" +Functions for writing port data into a Pattern (`dev2pat`) and retrieving it (`pat2dev`). + + These use the format 'name:ptype angle_deg' written into labels, which are placed at +the port locations. This particular approach is just a sensible default; feel free to +to write equivalent functions for your own format or alternate storage methods. +""" +from typing import Sequence +import logging + +import numpy + +from ..pattern import Pattern +from ..label import Label +from ..utils import rotation_matrix_2d, layer_t +from .devices import Device, Port + + +logger = logging.getLogger(__name__) + + +def dev2pat(device: Device, layer: layer_t) -> Pattern: + """ + Place a text label at each port location, specifying the port data in the format + 'name:ptype angle_deg' + + This can be used to debug port locations or to automatically generate ports + when reading in a GDS file. + + NOTE that `device` is modified by this function, and `device.pattern` is returned. + + Args: + device: The device which is to have its ports labeled. MODIFIED in-place. + layer: The layer on which the labels will be placed. + + Returns: + `device.pattern` + """ + for name, port in device.ports.items(): + if port.rotation is None: + angle_deg = numpy.inf + else: + angle_deg = numpy.rad2deg(port.rotation) + device.pattern.labels += [ + Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset) + ] + return device.pattern + + +def pat2dev( + pattern: Pattern, + layers: Sequence[layer_t], + max_depth: int = 999_999, + skip_subcells: bool = True, + ) -> Device: + """ + Examine `pattern` for labels specifying port info, and use that info + to build a `Device` object. + + Labels are assumed to be placed at the port locations, and have the format + 'name:ptype angle_deg' + + Args: + pattern: Pattern object to scan for labels. + layers: Search for labels on all the given layers. + max_depth: Maximum hierarcy depth to search. Default 999_999. + Reduce this to 0 to avoid ever searching subcells. + skip_subcells: If port labels are found at a given hierarcy level, + do not continue searching at deeper levels. This allows subcells + to contain their own port info (and thus become their own Devices). + Default True. + + Returns: + The constructed Device object. Port labels are not removed from the pattern. + """ + ports = {} # Note: could do a list here, if they're not unique + annotated_cells = set() + def find_ports_each(pat, hierarchy, transform, memo) -> Pattern: + if len(hierarchy) > max_depth - 1: + return pat + + if skip_subcells and any(parent in annotated_cells for parent in hierarchy): + return pat + + labels = [ll for ll in pat.labels if ll.layer in layers] + + if len(labels) == 0: + return pat + + if skip_subcells: + annotated_cells.add(pat) + + mirr_factor = numpy.array((1, -1)) ** transform[3] + rot_matrix = rotation_matrix_2d(transform[2]) + for label in labels: + name, property_string = label.string.split(':') + properties = property_string.split(' ') + ptype = properties[0] + angle_deg = float(properties[1]) if len(ptype) else 0 + + xy_global = transform[:2] + rot_matrix @ (label.offset * mirr_factor) + angle = numpy.deg2rad(angle_deg) * mirr_factor[0] * mirr_factor[1] + transform[2] + + if name in ports: + logger.info(f'Duplicate port {name} in pattern {pattern.name}') + + ports[name] = Port(offset=xy_global, rotation=angle, ptype=ptype) + + return pat + + pattern.dfs(visit_before=find_ports_each, transform=True) + return Device(pattern, ports)