# 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/iqm-open-source-trademark-policy). IQM welcomes contributions to the code.
# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement)
# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement).
import importlib
import warnings
from kqcircuits.elements.element import insert_cell_into
from kqcircuits.pya_resolver import pya, lay, is_standalone_session
from kqcircuits.defaults import (
default_layers,
default_png_dimensions,
mask_bitmap_export_layers,
all_layers_bitmap_hide_layers,
default_faces,
default_layer_props,
)
[docs]
class KLayoutView:
"""Helper object to represent the KLayout rendering environment.
``KLayoutView`` is a wrapper around the KLayout ``LayoutView`` and ``CellView`` objects, that represent containers
for viewing a layout in the GUI. It provides methods to initialize the views and layout for KQCircuits, for placing
KQCircuits ``Elements``, and for exporting images.
Create a new view as follows::
view = KLayoutView()
This creates a new set of ``LayoutView``, ``CellView``, ``Layout`` and top ``Cell`` objects with layers initialized
to the KQCircuits layer configuration. In the KLayout GUI, the new view will show as a new tab.
Note: In standalone python mode, the user must keep a reference to the ``KLayoutView`` object in scope, otherwise
the associated layout and cells may also go out of scope.
When running scripts or macros in the KLayout application, the following command creates a wrapper around the
currently active view::
view = KLayoutView(current=True)
This can be used in macros to act on the existing layout. The argument ``current=True`` is not available in
standalone python mode.
Once a view is created, new ``Elements`` can be placed with the ``insert_cell`` method.
Several methods are available to export PNG files of the current view or specific cells and layers.
In Jupyter notebooks, the ``show`` method displays the current view inline in the notebook.
"""
if hasattr(lay, "LayoutView"):
layout_view: lay.LayoutView
def __init__(self, current=False, initialize=None, background_color="#ffffff"):
"""Initialize a ``KLayoutView`` instance.
Args:
current: Boolean. If True, wrap the currently active ``LayoutView`` (not available in standalone python)
initialize: Boolean, specify whether to initialize the layout with the default layer configuration and a
top cell. Defaults to True if ``current==False``, and to False if ``current==True``.
background_color: Background color as HTML color code. Defaults to `"#ffffff"` (white).
"""
if not hasattr(lay, "LayoutView"):
# Standalone session before KLayout 0.28
raise MissingUILibraryException(
"KLayoutView is not supported in standalone mode for this klayout version. "
"Consider upgrading your klayout package to version 0.28 or above."
)
if initialize is None:
initialize = not current
if is_standalone_session():
# Standalone session since KLayout 0.28
if current:
raise MissingUILibraryException(
"In standalone python mode only KLayoutView(current=False) " + "is supported."
)
self.layout_view = lay.LayoutView(True) # Creates a new LayoutView in editable mode
self.layout_view.show_layout(pya.Layout(), True) # Adds a CellView and Layout
self.layout_view.set_config("background-color", background_color)
else:
# Regular session in the KLayout GUI
if not current or (lay.LayoutView.current() is None):
pya.MainWindow.instance().create_layout(1)
self.layout_view = lay.LayoutView.current()
if initialize:
self.add_default_layers()
self.create_top_cell()
[docs]
def insert_cell(
self, cell, trans=None, inst_name=None, label_trans=None, align_to=None, align=None, rec_levels=0, **parameters
):
"""Inserts a subcell into the first top cell (the very first cell in the cell window)
It will use the given ``cell`` object or if ``cell`` is an Element class' name then directly
take the provided keyword arguments to first create the cell object.
If `inst_name` given, a label ``inst_name`` is added to labels layer at the ``base`` refpoint and `label_trans`
transformation.
Args:
cell: cell object or Element class name
trans: used transformation for placement. None by default, which places the subcell into the coordinate
origin of the parent cell. If `align` and `align_to` arguments are used, `trans` is applied to the
`cell` before alignment transform which allows for example rotation of the `cell` before placement.
inst_name: possible instance name inserted into subcell properties under `id`. Default is None
label_trans: relative transformation for the instance name label
align_to: ``DPoint`` or ``DVector`` location in parent cell coordinates for alignment of cell.
Default is None
align: name of the ``cell`` refpoint aligned to argument ``align_to``. Default is None
rec_levels: recursion level when looking for refpoints from subcells. Set to 0 to disable recursion.
**parameters: PCell parameters for the element, as keyword argument
Return:
tuple of placed cell instance and reference points with the same transformation
"""
return insert_cell_into(
self.top_cell, cell, trans, inst_name, label_trans, align_to, align, rec_levels, **parameters
)
[docs]
def focus(self, cell=None):
"""Sets a given cell as the active cell, and fits the zoom level to fit the cell.
Args:
cell: cell to focus on, or None to focus on the currently active cell
"""
if cell is not None and isinstance(cell, pya.Cell):
self.layout_view.active_cellview().active().cell = cell
self.layout_view.max_hier()
self.layout_view.zoom_fit()
[docs]
def show(self, **kwargs):
"""In KLayout, show this LayoutView as the current in the main window.
In standalone python, display an image of the view. Requires IPython / Jupyter. Keyword arguments are passed
to ``KLayoutView.get_pixels``.
"""
if is_standalone_session():
display = importlib.import_module("IPython.display")
display.display_png(self.get_pixels(**kwargs).to_png_data(), raw=True)
else:
main_window = lay.MainWindow.instance()
for i in range(main_window.views()):
if main_window.view(i) is self.layout_view:
main_window.current_view_index = i
break
[docs]
def close(self):
"""Closes the current LayoutView."""
if is_standalone_session():
self.layout_view.destroy()
else:
lay.MainWindow.instance().close_current_view()
@property
def cell_view(self) -> "lay.CellView":
"""The active ``CellView``"""
return self.layout_view.active_cellview()
@property
def layout(self) -> pya.Layout:
"""The active ``Layout``"""
return self.cell_view.layout()
@property
def active_cell(self) -> pya.Cell:
"""The active ``Cell``, which is shown as current top in the cellview and bold in the cell window.
Can be set to any Cell in the layout."""
return self.cell_view.cell
@active_cell.setter
def active_cell(self, cell: pya.Cell):
self.cell_view.cell = cell
@property
def top_cell(self) -> pya.Cell:
"""The first top cell of the active layout."""
cells = self.layout.top_cells()
return cells[0] if len(cells) else None
[docs]
def clear_layers(self):
"""Clear the layer view."""
self.layout_view.clear_layers()
[docs]
def add_default_layers(self):
"""Populate view with KQCircuits default layers. Adds the layers to the layout, and populates the layer view."""
self.clear_layers()
layout = self.layout
for layer in default_layers.values():
layout.layer(layer)
self.layout_view.add_missing_layers()
if default_layer_props:
self.layout_view.load_layer_props(default_layer_props, True)
[docs]
def create_top_cell(self, top_cell_name="Top Cell"):
"""Creates a new static cell and set it as the top cell."""
top_cell = self.layout.create_cell(top_cell_name)
self.cell_view.cell_name = top_cell.name # Shows the new cell
return top_cell
[docs]
def export_layers_bitmaps(self, path, cell, filename=None, layers_set=None, face_id=None):
"""Exports each layer to a separate png image.
Args:
path: Directory to place the exported file in
cell: Cell to export
filename: Filename to export to, or None to use the cell's name.
layers_set: A list of layer names to export, or None for default values specified in the layer configuration
face_id: The face id for which to export the given layers, or None for general layers not associated to
a face.
"""
if layers_set is None:
layers_set = mask_bitmap_export_layers
for layer_name in layers_set:
layer_info = resolve_default_layer_info(layer_name, face_id)
self._export_bitmap(path, cell, filename=filename, layers_set=[layer_info])
[docs]
def export_all_layers_bitmap(self, path, cell, filename=None):
"""Exports a cell to a .png file with all layers visible
Args:
path: Directory to place the exported file in
cell: Cell to export
filename: Filename to export to, or None to use the cell's name.
"""
self._export_bitmap(path, cell, filename=filename, layers_set="all")
[docs]
def export_pcell_png(self, path, cell, filename=None, max_size=default_png_dimensions[0]):
"""Exports a cell to a .png file no bigger than max_size at either dimension.
Args:
path: Directory to place the exported file in
cell: Cell to export
filename: Filename to export to, or None to use the cell's name.
max_size: Maximum size of the image.
"""
zoom = cell.dbbox()
x, y = zoom.width(), zoom.height()
if max_size * x / y < max_size - 200: # 200x100 is enough for the sizebar
size = (max_size * x / y + 200, max_size)
else:
size = (max_size, max_size * y / x + 100)
self._export_bitmap(path, cell, filename=filename, layers_set="all", z_box=zoom, pngsize=size)
[docs]
def get_pixels(self, cell=None, width=None, height=None, layers_set=None, box=None):
"""Returns a PixelBuffer render of the current view.
This method first zooms to fit the whole layout and shows all hierarchy levels. If either ``width`` or
``height`` is specified, the other is chosen correspondingly to keep the same viewport aspect ratio. If neither
is specified, the current viewport size is used.
Args:
cell: Cell to render, or None to render the currently active cell.
width: image width in pixels, or None for automatic.
height: image height in pixels, or None for automatic.
layers_set: list of layer names to export, or None for the default set.
box: DBox area to show, or None for the full layout.
Returns: PixelBuffer
"""
if width is None and height is None:
width = self.layout_view.viewport_width()
height = self.layout_view.viewport_height()
elif width is None:
width = height * self.layout_view.viewport_width() / self.layout_view.viewport_height()
elif height is None:
height = width * self.layout_view.viewport_height() / self.layout_view.viewport_width()
if layers_set is not None:
layers_set = [resolve_default_layer_info(layer_name) for layer_name in layers_set]
def export_callback():
pixel_buffer = self.layout_view.get_pixels(width, height)
return pixel_buffer
return self._export_bitmap_configure(export_callback, cell, layers_set, box)
[docs]
@staticmethod
def get_active_cell_view():
"""Gets the currently active CellView. Not supported in standalone python mode.
Deprecated, use ``KLayoutView(current=True).cell_view`` to get the same behavior."""
warnings.warn(
"KLayoutView.get_active_cell_view will be deprecated. "
+ "Use instance property KLayoutView.cell_view instead.",
DeprecationWarning,
)
return lay.CellView.active()
[docs]
@staticmethod
def get_active_layout():
"""Gets the layout of the currently active CellView. Not supported in standalone python mode.
Deprecated, use ``KLayoutView(current=True).layout`` to get the same behavior. If you already have a
KLayoutView instance, use the ``layout`` property of that instance instead."""
warnings.warn(
"KLayoutView.get_active_layout will be deprecated. " + "Use instance property KLayoutView.layout instead.",
DeprecationWarning,
)
return lay.CellView.active().layout()
[docs]
@staticmethod
def get_active_cell():
"""Gets the active cell of the currently active CellView. Not supported in standalone python mode.
Deprecated, use ``KLayoutView(current=True).active_cell`` to get the same behavior. If you already have a
KLayoutView instance, use the ``active_cell`` property of that instance instead."""
warnings.warn(
"KLayoutView.get_active_cell will be deprecated. "
+ "Use instance property KLayoutView.active_cell instead.",
DeprecationWarning,
)
return lay.CellView.active().cell
# ********************************************************************************
# PRIVATE METHODS
# ********************************************************************************
def _export_bitmap(
self, path, cell=None, filename=None, layers_set=None, z_box=None, pngsize=default_png_dimensions
):
if filename is None:
filename = cell.name
if layers_set is None:
layers_set = [resolve_default_layer_info(layer_name) for layer_name in mask_bitmap_export_layers]
if len(layers_set) == 1:
layer_str = "-" + layers_set[0].name
else:
layer_str = ""
cell_png_name = path / f"{filename}{layer_str}.png"
def export_callback():
self.layout_view.save_image(str(cell_png_name), pngsize[0], pngsize[1])
self._export_bitmap_configure(export_callback, cell, layers_set, z_box)
def _export_bitmap_configure(self, export_callback, cell, layers_set, z_box):
"""Common configuration for export functions."""
def get_visibility_state():
"""Get the current layer visibility and drawing focus state"""
current_layer_visibility = [_layer.visible for _layer in self.layout_view.each_layer()]
current_cell = self.layout_view.active_cellview().cell
current_hier = (self.layout_view.min_hier_levels, self.layout_view.max_hier_levels)
current_zoom = self.layout_view.box()
return current_layer_visibility, current_cell, current_zoom, current_hier
def restore_visibility_state(layer_visibility, cell, zoom, hier):
"""Restore the layer visibility and drawing focus state.
Assumes order of layers has not changed since calling ``get_visibility_state``"""
for _layer, _visible in zip(self.layout_view.each_layer(), layer_visibility):
_layer.visible = _visible
self.layout_view.active_cellview().cell = cell
self.layout_view.min_hier_levels, self.layout_view.max_hier_levels = hier
self.layout_view.zoom_box(zoom)
visibility_state = get_visibility_state()
if cell is not None:
self.layout_view.active_cellview().cell = cell
self.layout_view.max_hier()
self.layout_view.zoom_fit() # Has to be done also before zoom_box
if z_box is not None:
self.layout_view.zoom_box(z_box)
if layers_set is None or layers_set == "all":
for layer in self.layout_view.each_layer():
layer.visible = True
for layer_to_hide in all_layers_bitmap_hide_layers:
if layer.source_layer == layer_to_hide.layer and layer.source_datatype == layer_to_hide.datatype:
layer.visible = False
break
else:
layer_infos = self.layout.layer_infos()
for layer in self.layout_view.each_layer():
# need to avoid hiding layer groups, because that could hide also layers in layers_set
is_layer_group = True
for layer_info in layer_infos:
if pya.LayerInfo(layer.source_layer, layer.source_datatype).is_equivalent(layer_info):
is_layer_group = False
break
if not is_layer_group:
layer.visible = False
for layer_to_show in layers_set:
if layer.source_layer == layer_to_show.layer and layer.source_datatype == layer_to_show.datatype:
layer.visible = True
break
if hasattr(lay, "Application"):
# Make sure the layer property changes are reflected before saving the image
lay.Application.instance().process_events()
else:
# Undocumented method timer does basically the same thing as process_events in GUI.
self.layout_view.timer()
export_return = export_callback()
restore_visibility_state(*visibility_state)
return export_return
[docs]
class MissingUILibraryException(Exception):
def __init__(self, message="Missing KLayout UI library."):
Exception.__init__(self, message)
[docs]
def resolve_default_layer_info(layer_name, face_id=None):
"""Returns LayerInfo based on default_layers.
Assumes that layer_name is valid, and that face_id is valid or None.
Args:
layer_name: layer name (without face prefix for face-specific layers)
face_id: id of the face from which this layer should be
"""
if face_id:
if layer_name in default_faces[face_id]:
return default_faces[face_id][layer_name]
else:
return default_layers[layer_name]
else:
return default_layers[layer_name]