Source code for kqcircuits.util.library_helper

# 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).

"""
    Helper module for building KLayout libraries.

    Typical usage example::

        from kqcircuits.elements.airbridges import Airbridge
        from kqcircuits.util.library_helper import load_libraries
        load_libraries(path=Airbridge.LIBRARY_PATH)
        cell = Airbridge.create(layout, **kwargs)
"""

import os
import re
import types
import inspect
import importlib
from pathlib import Path
import logging

from kqcircuits.defaults import SRC_PATHS, kqc_library_names, excluded_module_names
from kqcircuits.pya_resolver import pya


_kqc_libraries = {}  # dictionary {library name: (library, library path relative to kqcircuits)}

# Source directories not to be included in the library
_excluded_paths = (
    "__pycache__",
    "util",
    "simulations",
    "masks",
    "layer_config",
)

# modules NOT to be included in the library, (python file names without extension)
_excluded_module_names = (
    "__init__",
    "element",
    "qubit",
    "airbridge",
    "test_structure",
    "flip_chip_connector",
    "junction",
    "squid",
    "fluxline",
    "marker",
    "junction_test_pads",
    "tsv",
)

# Add user directories to SRC_PATHS when using Salt package.
if SRC_PATHS[0].parts[-4] == "salt":
    user_dirs = os.path.join(SRC_PATHS[0].parents[3], "python")
    for ud in os.listdir(user_dirs):
        SRC_PATHS.append(Path(os.path.join(user_dirs, ud)))


[docs] def load_libraries(flush=False, path=""): """Load all KQCircuits libraries from the given path. Args: flush: If True, old libraries will be deleted and new ones created. Otherwise old libraries will be used. (if old libraries exist) path: path (relative to SRC_PATH) from which the pcell classes and cells are loaded to libraries Returns: A dictionary of libraries that have been loaded, keys are library names and values are libraries. """ if flush: # Try reloading all pcells before deleting libraries, otherwise classes are lost in case of errors _get_all_pcell_classes(flush, path) delete_all_libraries() _kqc_libraries.clear() logging.debug("Deleted all libraries.") else: # if a library with the given path already exist, use it for _, lib_path in _kqc_libraries.values(): if lib_path == path: return {key: value[0] for key, value in _kqc_libraries.items()} for cls in _get_all_pcell_classes(flush, path): library_name = cls.LIBRARY_NAME library_path = cls.LIBRARY_PATH library = pya.Library.library_by_name(library_name) # returns only registered libraries if (library is None) or flush: if library_name in _kqc_libraries: logging.debug('Using created library "%s".', library_name) library, _ = _kqc_libraries[library_name] else: # create a library, but do not register it yet logging.debug(f'Creating new library "{library_name}".') library = pya.Library() library.description = cls.LIBRARY_DESCRIPTION _kqc_libraries[library_name] = (library, library_path) _register_pcell(cls, library, library_name) # Libraries should be registered in dependency-order, otherwise reload will crash. for library_name in kqc_library_names: if library_name not in _kqc_libraries: continue library, _ = _kqc_libraries[library_name] _load_manual_designs(library_name) if library_name not in library.library_names(): library.register(library_name) # library must be registered only after all cells have been added to it return {key: value[0] for key, value in _kqc_libraries.items()}
[docs] def get_library_paths(): """Returns a list of library paths under kqcircuits.""" return (path for _, path in _kqc_libraries.values())
[docs] def delete_all_libraries(): """Delete all KQCircuits libraries from KLayout memory.""" for name in reversed(kqc_library_names): delete_library(name)
[docs] def delete_library(name=None): """Delete a KQCircuits library. Calls library.delete() and removes the library from _kqc_libraries dict. Also deletes any libraries which have this library as a dependency. Args: name: name of the library """ if name is None: return library = pya.Library.library_by_name(name) if library is None: # already deleted return # delete this library library.delete() if name in _kqc_libraries: _kqc_libraries.pop(name) if library._destroyed(): logging.info(f"Successfully deleted library '{name}'.") else: raise SystemError(f"Failed to delete library '{name}'.")
[docs] def element_by_class_name(class_name: str, library_path: str = "elements", library_name: str = "Element Library"): """ Find Element class by class name from a library Args: class_name: Class name to look up library_path: Path to pass to load_libraries library_name: Name of the library Returns: Class of the element, or None if the element is not in the library """ layout = load_libraries(path=library_path)[library_name].layout() for pcell_id in layout.pcell_ids(): pcell_class = layout.pcell_declaration(pcell_id).__class__ if pcell_class.__name__ == class_name: return pcell_class return None
[docs] def to_module_name(class_name=None): """Converts class name to module name. Converts PascalCase class name to module name with each word lowercase and separated by space. Args: class_name: Class name. Returns: A lowercase and spaced by word string. For example:: > module_name = _to_module_name("QualityFactor") > print(module_name) "quality_factor" """ try: _is_valid_class_name(class_name) except ValueError as e: logging.exception("Failed to convert class name to module name.") raise e words = re.sub(r"(?<!^)(?=[A-Z])", "_", class_name).split("_") return _join_module_words(words)
[docs] def to_library_name(class_name=None): """Converts class name to library name. Converts PascalCase class name to library name with each word titled and separated by space. Single letter words are attached to the following word. Args: class_name: Class name. Returns: A titled and spaced by word string which may be used as library name. For example:: > library_name = to_library_name("QualityFactor") > print(library_name) "Quality Factor" """ try: _is_valid_class_name(class_name) except ValueError as e: logging.exception("Failed to convert class name to library name.") raise e words = re.sub(r"(?<!^)(?=[A-Z])", "_", class_name).split("_") return _join_library_words(words)
# ******************************************************************************** # PRIVATE METHODS # ******************************************************************************** def _register_pcell(pcell_class, library, library_name): """Registers the PCell to the library. Args: pcell_class: class of the PCell library: Library where the PCell is registered to library_name: name of the library """ try: pcell_name = to_library_name(pcell_class.__name__) library.layout().register_pcell(pcell_name, pcell_class()) logging.debug(f"Registered pcell [{pcell_name}] to library {library_name}.") except Exception: # pylint: disable=broad-except logging.warning(f"Failed to register pcell in class {pcell_class} to library {library_name}.", exc_info=True) def _load_manual_designs(library_name): """Loads .oas files to the library Args: library_name: name of the library """ library, rel_path = _kqc_libraries[library_name] for src in SRC_PATHS: for path in src.rglob(f"{rel_path}/**/*.oas"): library.layout().read(str(path.absolute())) def _get_all_pcell_classes(reload=False, path="", skip_modules=False): """Returns all PCell classes in the given path. Args: reload: Boolean determining if the modules in kqcircuits should be reloaded. path: path (relative to SRC_PATH) from which the classes are searched skip_modules: Do not consider some pcells classes defined in ``excluded_module_names``. Returns: List of the PCell classes """ pcell_classes = [] for src in SRC_PATHS: pkg = src.parts[-1] if path == "": library_src_paths = [f for f in src.iterdir() if f.is_dir() and f.name not in _excluded_paths] else: library_src_paths = [src.joinpath(path)] skip_list = _excluded_module_names if not skip_modules else _excluded_module_names + excluded_module_names for library_src in library_src_paths: module_paths = library_src.rglob("*.py") for mp in module_paths: module_name = mp.stem if module_name in skip_list: continue # Get the module path starting from the "pkg" directory below project root directory. import_path_parts = mp.parts[::-1][mp.parts[::-1].index(pkg) :: -1] import_path = ".".join(import_path_parts)[:-3] # the -3 is for removing ".py" from the path module = importlib.import_module(import_path) if reload: importlib.reload(module) logging.debug(f"Reloaded module '{module_name}'.") classes = _get_pcell_classes(module) if classes: pcell_classes.append(classes[-1]) return pcell_classes def _get_pcell_classes(module=None): """Returns all PCell classes found in the module. Args: module: Module. Returns: Array of classes. """ if module is None: return [] class_list = [] for member in inspect.getmembers(module, inspect.isclass): if member[1].__module__ == module.__name__: cls = _get_pcell_class(member[0], module) if cls is not None: class_list.append(cls) return class_list def _get_pcell_class(name=None, module=None): """Returns class found for specified path and circuit type. Args: name: Path object for module. module: Module. Returns: The class if it is subclass of PCellDeclarationHelper, otherwise None. """ if name is None or not isinstance(name, str): return None if module is None or not isinstance(module, types.ModuleType): return None value = getattr(module, name) if isinstance(value, type) and issubclass(value, pya.PCellDeclarationHelper): return value else: return None def _is_valid_class_name(value=None): """Check if string value is valid PEP-8 compliant Python class name.""" if value is None or not isinstance(value, str) or len(value) == 0: raise ValueError(f"Cannot convert nil or non-string class name '{value}' to library name.") if re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", value) is None: raise ValueError(f"Cannot convert invalid Python class name '{value}' to library name.") if re.fullmatch(r"([A-Z][a-z0-9]*)+", value) is None: raise ValueError(f"PEP8 compliant class name '{value}' must be PascalCase without underscores.") def _join_module_words(words=None): """Join words to build module name. Joins words such that each word is lowercase and separated by underscore. Single letter words are attached to the following word. Args: words: List of word strings Returns: A string which may be used as module name. """ words = _clean_words(words) words = [w.lower() for w in words] n = len(words) if n == 0: return "" name = words[0] for i in range(1, n): previous = words[i - 1] current = words[i] if len(previous) == 1: name += current else: name += "_" + current return name def _join_library_words(words=None): """Join words to build library name. Joins words such that each word is titled and separated by space. Single letter words are attached to the following word. Args: words: List of word strings Returns: A string which may be used as library name. """ words = _clean_words(words) n = len(words) if n == 0: return "" name = words[0].title() for i in range(1, n): previous = words[i - 1].title() current = words[i].title() if len(previous) == 1: name += current else: name += " " + current return name def _clean_words(words=None): """Clean word list by removing None values, empty strings, and non-string values. Returns: Clean word list. """ if words and isinstance(words, list): words = list(filter(None, words)) words = list(filter(lambda item: isinstance(item, str), words)) return list(filter(len, words)) else: return []