Source code for kqcircuits.simulations.export.cross_section.cross_section_profile

# This code is part of KQCircuits
# Copyright (C) 2025 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).


# This file shouldn't import anything substantial from kqcircuits to prevent cyclical import

import logging
import re
from typing import Any, Callable, Sequence
from kqcircuits.pya_resolver import pya


SimulationFunction = Callable[["Simulation"], Any]


def _wrap_constant_or_function(value: Any) -> SimulationFunction:
    """If value is constant, returns function that returns value for any argument"""
    if not callable(value):
        return lambda simulation: value
    return value


def _collect_layers_from_regexes(layer_keys: Sequence[pya.LayerInfo], regexes: Sequence[str]) -> set[pya.LayerInfo]:
    """Given set of layers ``layer_keys``, select layers whose name matches at least one regex from ``regexes``"""
    return set(
        layer_info
        for layer_regex in regexes
        for layer_info in [l for l in layer_keys if re.fullmatch(layer_regex, l.name)]
    )


[docs] class CrossSectionProfile: """Defines vertical level (bottom, top) values for each layer in order to produce cross section geometry. Also contains following features:: * Level values can be configured using functions ``Simulation -> float`` so that the value is calculated from Simulation's parameters * Set priority by regex - if a shape from one layer overlaps with shape from another layer, keep regions disjoint by cutting out the overlap region on former layer * Layer in original layout may have its cross section placed at another layer in cross section layout. When developing class methods here, please maintain the assumption that a simulation instance passed here is not modified. """ def __init__(self): self._levels = {} self._layer_priority = {} self._layer_change_map = {}
[docs] def level( self, regex: str, bottom_function: float | SimulationFunction, top_function: float | SimulationFunction, change_to_layer: str | SimulationFunction = "", ) -> None: """Define level values for layers whose name matches ``regex``. Args: regex: this level configuration takes effect for layers whose name matches regex. bottom_function: z value for the bottom of the cross section shape, can be defined as function top_function: z value for the top of the cross section shape, can be defined as function change_to_layer: (Optional) cross section shapes on layers that match ``regex`` will be placed to ``change_to_layer`` layer in cross section layout. """ self._levels[regex] = (_wrap_constant_or_function(bottom_function), _wrap_constant_or_function(top_function)) self._layer_change_map[regex] = _wrap_constant_or_function(change_to_layer)
[docs] def priority(self, target_layer_regex: str, dominant_layer_regex: str) -> None: """Configure profile so that in case a region from a layer that matches ``target_layer_regex`` overlaps a region that matches ``dominant_layer_regex``, the regions will be made disjoint by removing the overlapping region from layer matching ``target_layer_regex``. """ self._layer_priority[target_layer_regex] = _wrap_constant_or_function(dominant_layer_regex)
[docs] def get_layers(self, layout: pya.Layout) -> set[pya.LayerInfo]: """Collect all layers from ``layout`` that were configured to this profile using ``level`` function""" return _collect_layers_from_regexes(layout.layer_infos(), self._levels)
[docs] def get_level(self, layer_name: str, simulation: "Simulation") -> tuple[float, float]: """Given concrete layer name, returns bottom and top level for such layer in cross section profile Args: layer_name: concrete layer name, not regex simulation: simulation object from which cross section is taken Returns: tuple - coordinate of bottom and top levels for given layer in the cross section profile """ level, first_match = None, None for layer_regex, lvl in self._levels.items(): if re.fullmatch(layer_regex, layer_name): if level: logging.warning(f"Layer {layer_name} matches both {first_match} and {layer_regex} regexes") else: first_match = layer_regex level = lvl if not level: logging.warning(f"Layer {layer_name} matches no regex configured in the cross section profile") return None return (level[0](simulation), level[1](simulation))
[docs] def get_dominant_layer_regex(self, layer_name: str, simulation: "Simulation") -> str: """Constructs a regex for all layers that have higher priority than ``layer_name``. Args: layer_name: Name of the layer (not regex) for which we wish to know what layers it is dominated by simulation: simulation object from which cross section is taken Returns: Regex, matching any layer that dominates layer with name ``layer_name`` """ dominant_layer_regex = None for layer_regex, dom_layers in self._layer_priority.items(): if re.fullmatch(layer_regex, layer_name): dom_layers_value = dom_layers(simulation) if dominant_layer_regex: dominant_layer_regex += f"{dominant_layer_regex}|({dom_layers_value})" dominant_layer_regex = f"({dom_layers_value})" return dominant_layer_regex
[docs] def get_invisible_layers(self, simulation: "Simulation") -> set[pya.LayerInfo]: """Setting ``change_to_layer`` to None causes the level to not be written to any layer. This function returns all layers that are configured in such way. Args: simulation: simulation object from which cross section is taken Returns: Set of layers that will be made invisible in the cross section """ invis_layers = [l_regex for l_regex, l_output in self._layer_change_map.items() if l_output(simulation) is None] return _collect_layers_from_regexes(simulation.layout.layer_infos(), invis_layers)
[docs] def change_layer(self, input_layer: pya.LayerInfo, simulation: "Simulation") -> pya.LayerInfo: """Given ``input_layer`` on original layout, returns which layer on the cross section layout the shapes should be added to. Args: input_layer: Layer in original layout, for which to look up the layer on the cross section layout simulation: simulation object from which cross section is taken Returns: Layer on the cross section layout, where the cross section shape of ``input_layer`` will be written to. """ output_layer = None previous_layer_regex = None for layer_regex, layer in self._layer_change_map.items(): if re.fullmatch(layer_regex, input_layer.name): if output_layer: logging.warning( f"Layer {input_layer} matching '{layer_regex}' already matched '{previous_layer_regex}'" ) layer_value = layer(simulation) if layer_value != "": output_layer = layer_value previous_layer_regex = layer_regex # input_layer not configured to change, so we just write the cross section to input_layer if not output_layer: return input_layer # Find pya.LayerInfo with output_layer as name layer_info_matches = [l for l in set(simulation.layout.layer_infos()) if l.name == output_layer] if len(layer_info_matches) > 1: logging.warning( f"Multiple layers with name {output_layer} found: {layer_info_matches}. Returning first layer." ) if not layer_info_matches: logging.warning(f"No layers found with name {output_layer}. Initialising such layer") return pya.LayerInfo(output_layer) return layer_info_matches[0]
[docs] def add_face(self, face_id: str) -> None: """Add standard configuration within KQC context for given face""" # Use face_z_levels to determine levels for ground metal self.level( f"{face_id}_ground", lambda s: s.face_z_levels()[face_id][0], lambda s: s.face_z_levels()[face_id][1] ) # Do the same for all signal metals self.level( f"{face_id}_signal_?[0-9]*", lambda s: s.face_z_levels()[face_id][0], lambda s: s.face_z_levels()[face_id][1], ) # Also determine level for possible dielectric layer between metals self.level( f"{face_id}_dielectric", lambda s: s.face_z_levels()[face_id][0], lambda s: s.face_z_levels()[face_id][2] ) # Remove parts of dielectric if metal shapes overlap with it self.priority(f"{face_id}_dielectric", f"{face_id}_ground|({face_id}_signal_?[0-9]*)")
# Prepared cross section profiles
[docs] def get_single_face_cross_section_profile() -> CrossSectionProfile: """Standard KQC single face cross section profile""" profile = CrossSectionProfile() # Add standard 1t1 face profile.add_face("1t1") # Define levels for bottom substrate profile.level( "substrate_1", lambda s: s.face_z_levels()["1t1"][0] - s.substrate_height[0], lambda s: s.face_z_levels()["1t1"][0], ) # Define levels for gaps (including possible over etching) and change it to vacuum layer profile.level( "1t1_gap", lambda s: s.face_z_levels()["1t1"][0] - s.vertical_over_etching, lambda s: s.face_z_levels()["1t1"][0], change_to_layer="vacuum", ) # Make sure gap regions eat away from substrate profile.priority("substrate_1", "1t1_gap") return profile
[docs] def get_flip_chip_cross_section_profile() -> CrossSectionProfile: """Standard KQC flip chip cross section profile""" # Take single face profile and add stuff on top of it profile = get_single_face_cross_section_profile() # Add standard 2b1 face profile.add_face("2b1") # Define levels for top substrate profile.level( "substrate_2", lambda s: s.face_z_levels()["2b1"][0] + s.substrate_height[1], lambda s: s.face_z_levels()["2b1"][0], ) # Define levels for gaps (including possible over etching) and change it to vacuum layer profile.level( "2b1_gap", lambda s: s.face_z_levels()["2b1"][0] + s.vertical_over_etching, lambda s: s.face_z_levels()["2b1"][0], change_to_layer="vacuum", ) # Make sure gap regions eat away from substrate profile.priority("substrate_2", "2b1_gap") return profile
[docs] def get_cross_section_profile(simulation: "Simulation") -> CrossSectionProfile: """Given simulation's face_stack, either returns single face or flip chip cross section profile""" if len(simulation.face_stack) > 1: return get_flip_chip_cross_section_profile() return get_single_face_cross_section_profile()