Skip to content

Commit

Permalink
Support multi-face layouts and rectangular chips in masks
Browse files Browse the repository at this point in the history
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
  • Loading branch information
caspar-iqm committed Oct 18, 2023
1 parent e35b22f commit a767105
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion klayout_package/python/kqcircuits/masks/mask_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,20 @@ 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
"""
invert = False
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()
Expand All @@ -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)
Expand Down
115 changes: 69 additions & 46 deletions klayout_package/python/kqcircuits/masks/mask_layout.py

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion klayout_package/python/kqcircuits/masks/mask_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
75 changes: 75 additions & 0 deletions klayout_package/python/kqcircuits/masks/multi_face_mask_layout.py
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 19 additions & 22 deletions klayout_package/python/kqcircuits/util/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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))
108 changes: 108 additions & 0 deletions klayout_package/python/scripts/masks/demo_multiface.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit a767105

Please sign in to comment.