Source code for kqcircuits.simulations.export.xsection.xsection_export

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


import ast
import json
import os
import subprocess
from pathlib import Path
from typing import Callable, List, Tuple, Union
from kqcircuits.defaults import STARTUPINFO, XSECTION_PROCESS_PATH
from kqcircuits.pya_resolver import pya, klayout_executable_command
from kqcircuits.simulations.export.util import export_layers
from kqcircuits.simulations.cross_section_simulation import CrossSectionSimulation
from kqcircuits.simulations.simulation import Simulation


[docs]def xsection_call(input_oas: Path, output_oas: Path, cut1: pya.DPoint, cut2: pya.DPoint, process_path: Path = XSECTION_PROCESS_PATH, parameters_path: Path = None) -> None: """Calls on KLayout to run the XSection plugin Args: input_oas: Input OAS file (top-down geometry) output_oas: Output OAS file (Cross-section of input geometry) cut1: DPoint of first endpoint of the cross-section cut cut2: DPoint of second endpoint of the cross-section cut process_path: XSection process file that defines cross-section etching depths etc parameters_path: If process_path points to kqc_process.xs, parameters_path should point to the XSection parameters json file containing sweeped parameters and layer information. """ if os.name == "nt": klayout_dir_name = "KLayout" elif os.name == "posix": klayout_dir_name = ".klayout" else: raise SystemError("Error: unsupported operating system") xsection_plugin_path = os.path.join(os.path.expanduser("~"), klayout_dir_name, "salt/xsection/macros/xsection.lym") cut_string = f"{cut1.x},{cut1.y};{cut2.x},{cut2.y}" if not klayout_executable_command(): raise Exception("Can't find klayout executable command!") if not Path(xsection_plugin_path).is_file(): raise Exception("The 'xsection' plugin is missing in KLayout! Go to 'Tools->Manage Packages' to install it.") # Hack: Weird prefix keeps getting added when path is converted to string which breaks the ruby plugin xs_run = str(process_path).replace("\\\\?\\", "") xs_params = str(parameters_path).replace("\\\\?\\", "") # When debugging, remove '-z' argument to see ruby error messages subprocess.run([klayout_executable_command(), input_oas.absolute(), '-z', '-nc', '-rx', '-r', xsection_plugin_path, '-rd', f'xs_run={xs_run}', '-rd', f'xs_params={xs_params}', '-rd', f'xs_cut={cut_string}', '-rd', f'xs_out={output_oas.absolute()}'], check=True, startupinfo=STARTUPINFO)
# pylint: disable=dangerous-default-value
[docs]def create_xsections_from_simulations(simulations: List[Simulation], output_path: Path, cuts: Union[Tuple[pya.DPoint, pya.DPoint], List[Tuple[pya.DPoint, pya.DPoint]]], process_path: Path = XSECTION_PROCESS_PATH, ma_permittivity: float = 0, ms_permittivity: float = 0, sa_permittivity: float = 0, ma_thickness: float = 0, ms_thickness: float = 0, sa_thickness: float = 0, london_penetration_depth: float = 0, magnification_order: int = 0 ) -> List[Simulation]: """Create cross-sections of all simulation geometries in the list. Will set 'box' and 'cell' parameters according to the produced cross-section geometry data. Args: simulations: List of Simulation objects, usually produced by a sweep output_path: Path for the exported simulation files cuts: 1. A tuple (p1, p2), where p1 and p2 are endpoints of a cross-section cut or 2. a list of such tuples such that each Simulation object gets an individual cut process_path: XSection process file that defines cross-section etching depths etc ma_permittivity: Permittivity of metal–vacuum (air) interface ms_permittivity: Permittivity of metal–substrate interface sa_permittivity: Permittivity of substrate–vacuum (air) interface ma_thickness: Thickness of metal–vacuum (air) interface ms_thickness: Thickness of metal–substrate interface sa_thickness: Thickness of substrate–vacuum (air) interface london_penetration_depth: London penetration depth of the superconducting material magnification_order: Increase magnification of simulation geometry to accomodate more precise spacial units. 0 = no magnification with 1e-3 dbu 1 = 10x magnification with 1e-4 dbu 2 = 100x magnification with 1e-5 dbu etc Consider setting non-zero value when using oxide layers with < 1e-3 layer thickness or taking cross-sections of thin objects Returns: List of CrossSectionSimulation objects for each Simulation object in simulations """ if isinstance(cuts, Tuple): cuts = [cuts] * len(simulations) if len(simulations) != len(cuts): raise Exception("Number of cuts did not match the number of simulations") if any(len(simulation.get_parameters()["face_stack"]) not in (1, 2) for simulation in simulations): raise Exception("Only single face and flip chip cross section simulations currently supported") xsection_dir = output_path.joinpath("xsection_tmp") xsection_dir.mkdir(parents=True, exist_ok=True) layout = pya.Layout() load_opts = _load_layout_options_for_xsection_output() for simulation, cut in zip(simulations, cuts): xsection_parameters = _dump_xsection_parameters(xsection_dir, simulation) simulation_file = xsection_dir / f"original_{simulation.cell.name}.oas" xsection_file = xsection_dir / f"xsection_{simulation.cell.name}.oas" export_layers(str(simulation_file), simulation.layout, [simulation.cell], output_format='OASIS', layers=None) xsection_call(simulation_file, xsection_file, cut[0], cut[1], process_path, xsection_parameters) layout.read(str(xsection_file), load_opts) xsection_cell = layout.top_cells()[-1] xsection_cell.name = simulation.cell.name _clean_tmp_xsection_directory(xsection_dir, simulations) # Collect cross section simulation sweeps return [_construct_cross_section_simulation( layout, xsection_cell, simulations[idx], ma_permittivity, ms_permittivity, sa_permittivity, ma_thickness, ms_thickness, sa_thickness, london_penetration_depth, magnification_order) for idx, xsection_cell in enumerate(layout.top_cells())]
# pylint: enable=dangerous-default-value
[docs]def separate_signal_layer_shapes(simulation: Simulation, sort_key: Callable[[pya.Shape], float] = None): """Separate shapes in signal layer to their own dedicated signal layers for each face Args: simulation: A Simulation object where the layer will be separated sort_key: A function that, given a Shape object, returns a number. Shapes are sorted according to the number in increasing order. If None, picks a point in shape polygon, sorts points top to bottom then tie-breaks left to right """ if sort_key is None: def sort_key(shape): point_in_shape = list(shape.polygon.each_point_hull())[0] return (-point_in_shape.y, point_in_shape.x) signal_index = 1 gen_free_layer_slots = free_layer_slots(simulation.layout) for face in simulation.face_ids: signal_layer = find_layer_by_name(f"{face}_signal", simulation.layout) if signal_layer is None: continue signal_layer_idx = simulation.layout.layer(signal_layer) for shape in sorted(simulation.cell.each_shape(signal_layer_idx), key=sort_key): # Reuse layer if it already used in layout signal_layer = find_layer_by_name(f"{face}_signal_{signal_index}", simulation.layout) # If no such layer, find next available layer index if signal_layer is None: layer_index = next(gen_free_layer_slots) signal_layer = pya.LayerInfo(layer_index, 0, f"{face}_signal_{signal_index}") simulation.cell.shapes(simulation.layout.layer(signal_layer)).insert(shape) signal_index += 1 simulation.cell.clear(signal_layer_idx)
[docs]def find_layer_by_name(layer_name, layout): """Returns layerinfo if there already is a layer by layer_name in layout. None if no such layer exists""" for l in layout.layer_infos(): if l.datatype == 0 and layer_name == l.name: return l return None
[docs]def free_layer_slots(layout): """A generator of available layer slots""" layer_index = 0 reserved_layer_ids = [l.layer for l in layout.layer_infos() if l.datatype == 0] while True: layer_index += 1 if layer_index in reserved_layer_ids: continue yield layer_index
def _load_layout_options_for_xsection_output(): load_opts = pya.LoadLayoutOptions() load_opts.cell_conflict_resolution = pya.LoadLayoutOptions.CellConflictResolution.RenameCell return load_opts def _remap_face(layer_name, faces_of_flipchip): """Rename face to b_*, t_* convention based on faces_of_flipchip list""" if len(faces_of_flipchip) > 0 and layer_name.startswith(f"{faces_of_flipchip[0]}_"): return f"b_{layer_name[len(faces_of_flipchip[0]) + 1:]}" if len(faces_of_flipchip) > 1 and layer_name.startswith(f"{faces_of_flipchip[1]}_"): return f"t_{layer_name[len(faces_of_flipchip[1]) + 1:]}" return layer_name def _dump_xsection_parameters(xsection_dir, simulation): """If we're sweeping xsection specific parameters, dump them in external file for xsection process file to pick up """ simulation_params = {param_name: param_value for param_name, param_value in simulation.get_parameters().items() if not isinstance(param_value, pya.DBox)} # Hack: ignore non-serializable params # Also dump all used layers in the simulation cell sim_layers = {_remap_face(l.name, simulation_params['face_stack']): f"{l.layer}/{l.datatype}" for l in simulation.layout.layer_infos()} # Find avaiable layer numbers for substrate layers gen_free_layer_slots = free_layer_slots(simulation.layout) sim_layers["b_substrate"] = f"{next(gen_free_layer_slots)}/0" sim_layers["t_substrate"] = f"{next(gen_free_layer_slots)}/0" simulation_params['sim_layers'] = sim_layers xsection_parameters_file = xsection_dir / f"parameters_{simulation.cell.name}.json" with open(xsection_parameters_file, "w") as sweep_file: json.dump(simulation_params, sweep_file) return xsection_parameters_file def _clean_tmp_xsection_directory(xsection_dir, simulations): for simulation in simulations: if os.path.exists(xsection_dir / f"original_{simulation.cell.name}.oas"): os.remove(xsection_dir / f"original_{simulation.cell.name}.oas") if os.path.exists(xsection_dir / f"xsection_{simulation.cell.name}.oas"): os.remove(xsection_dir / f"xsection_{simulation.cell.name}.oas") if os.path.exists(xsection_dir / f"parameters_{simulation.cell.name}.json"): os.remove(xsection_dir / f"parameters_{simulation.cell.name}.json") if os.path.exists(xsection_dir): os.rmdir(xsection_dir) def _combine_region_from_layers(simulation, layers): """Produce a region combined from regions in layers list""" region = pya.Region() for layer in layers: region += pya.Region(simulation.cell.shapes(simulation.layout.layer(layer))) return region def _edge_on_the_box_border(edge, box): """True if edge is exactly at the rim of the box""" return (edge.x1 == box.p1.x and edge.x2 == box.p1.x) or \ (edge.x1 == box.p2.x and edge.x2 == box.p2.x) or \ (edge.y1 == box.p1.y and edge.y2 == box.p1.y) or \ (edge.y1 == box.p2.y and edge.y2 == box.p2.y) def _cut_edge(target_edge, source_edge, extra_edges): """Cut an end of the target_edge with source_edge. If source_edge leaves behind two ends of the target_edge, the second edge bit is stored in extra_edges. """ # Copy target_edge to not modify the original edge instance result_edge = pya.DEdge(target_edge.p1.x, target_edge.p1.y, target_edge.p2.x, target_edge.p2.y) if result_edge.contains_excl(source_edge.p1): if result_edge.contains_excl(source_edge.p2) and source_edge.p2 != result_edge.p2: extra_edges.append(pya.DEdge(source_edge.p2, result_edge.p2)) result_edge.p2 = source_edge.p1 elif result_edge.contains_excl(source_edge.p2): result_edge.p1 = source_edge.p2 return result_edge def _remove_shared_points(target_edge, acting_edges, is_adjacent): """Remove all points shared by target_edge and edges in acting_edges Returns a set of continuous edges that are not contained by acting_edges. Set is_adjacent to True if the shape of acting_edges is adjacent to the shape from which target_edge was taken. Set to False if the shapes are on top of eah other. """ edge_bits = [target_edge] for acting_edge in acting_edges: # Set acting_edge to point to same direction as target_edge if is_adjacent: acting_edge = acting_edge.swapped_points() # Consider edges if they share points, which means they are parallel and have same displacement if acting_edge.is_parallel(target_edge): # Remove edge bits if they are completely covered by acting_edge edge_bits = [e for e in edge_bits if not (acting_edge.contains(e.p1) and acting_edge.contains(e.p2))] extra_edge_bits = [] # Collect extra edge bits here edge_bits = [_cut_edge(e, acting_edge, extra_edge_bits) for e in edge_bits] edge_bits.extend(extra_edge_bits) # Add extra bits edge_bits = [e for e in edge_bits if e.p1 != e.p2] # Remove zero length edge bits return edge_bits def _thicken_edges(edges, thickness, dbu, grow): """Take edges and add thickness to produce a region. Requires dbu. Set grow to True to grow the region outward, False to grow inward """ if thickness <= 0.0: # Don't do anything if no thickness return pya.Region() # Construct a graph from the edges to find paths # Start by finding start points for paths start_points = [e.p1 for e in edges if e.p1 not in [e2.p2 for e2 in edges]] path_graph = {} for edge in edges: path_graph[edge.p1] = edge result_layer = pya.Region() # Take each start_point and follow the path until the end for current_point in start_points: polygon_points = [current_point] normals = [] while True: # First collect path points for the region polygon polygon_points.append(path_graph[current_point].p2) edge_dir = path_graph[current_point].p2 - path_graph[current_point].p1 # Store edge normal, assuming edges go clock-wise around the shape hull normal = pya.DPoint(-edge_dir.y, edge_dir.x) if not grow: # Flip normal if growing inward normal = -normal # Set normal length to thickness normals.append(normal * (thickness / normal.abs())) # At the end point, terminate if path_graph[current_point].p2 not in path_graph: break # Otherwise proceed to next point in path current_point = path_graph[current_point].p2 # Connect to the second layer of the path to add thickness polygon_points.append(polygon_points[-1] + normals[-1]) # Backtrack the path for the second layer of the polygon for idx in range(len(normals) - 1, 0, -1): normal_sum = normals[idx] + normals[idx - 1] # Sum normals of surrounding edges of the point polygon_points.append(polygon_points[idx] + normal_sum) polygon_points.append(polygon_points[0] + normals[0]) # Last second layer point, copied from the start_point result_layer += pya.Region(pya.DPolygon(polygon_points).to_itype(dbu)) return result_layer def _oxidise_layers(simulation, ma_thickness, ms_thickness, sa_thickness): """Take the cross section geometry and add oxide layers between substrate, metal and vaccuum. Will add geometry around metals and etch away substrate to insert oxide geometry. """ substrate_layers = [layer for layer in simulation.layout.layer_infos() if layer.name.endswith("_substrate")] substrate = _combine_region_from_layers(simulation, substrate_layers) metal_layers = [layer for layer in simulation.layout.layer_infos() if layer.name in ["b_ground", "t_ground", "b_signal", "t_signal"]] metal_layers += [layer for layer in simulation.layout.layer_infos() if layer.name.startswith("b_signal_")] metal_layers += [layer for layer in simulation.layout.layer_infos() if layer.name.startswith("t_signal_")] metals = _combine_region_from_layers(simulation, metal_layers) metal_edges = [e.to_dtype(simulation.layout.dbu) for e in metals.edges()] substrate_edges = [e.to_dtype(simulation.layout.dbu) for e in substrate.edges()] ma_edges = [] for metal_edge in metal_edges: if not _edge_on_the_box_border(metal_edge, simulation.box): ma_edges.extend(_remove_shared_points(metal_edge, substrate_edges, True)) sa_edges, ms_edges = [], [] for substrate_edge in substrate_edges: if not _edge_on_the_box_border(substrate_edge, simulation.box): sa_edges.extend(_remove_shared_points(substrate_edge, metal_edges, True)) ms_edges.extend(_remove_shared_points(substrate_edge, sa_edges, False)) ma_layer = _thicken_edges(ma_edges, ma_thickness, simulation.layout.dbu, True) ms_layer = _thicken_edges(ms_edges, ms_thickness, simulation.layout.dbu, False) sa_layer = _thicken_edges(sa_edges, sa_thickness, simulation.layout.dbu, True) sa_layer -= ma_layer # MA layer takes precedence over SA layer # Etch and replace substrate layer regions if ms_thickness > 0.0 or sa_thickness > 0.0: for substrate_layer in substrate_layers: substrate_region = pya.Region(simulation.cell.shapes(simulation.layout.layer(substrate_layer))) simulation.cell.shapes(simulation.layout.layer(substrate_layer)).clear() simulation.cell.shapes(simulation.layout.layer(substrate_layer)).insert( substrate_region - ms_layer) if ma_thickness > 0.0: simulation.cell.shapes(simulation.get_sim_layer("ma_layer")).insert(ma_layer) if ms_thickness > 0.0: simulation.cell.shapes(simulation.get_sim_layer("ms_layer")).insert(ms_layer) if sa_thickness > 0.0: simulation.cell.shapes(simulation.get_sim_layer("sa_layer")).insert(sa_layer) def _construct_cross_section_simulation(layout, xsection_cell, simulation, ma_permittivity, ms_permittivity, sa_permittivity, ma_thickness, ms_thickness, sa_thickness, london_penetration_depth, magnification_order): """Produce CrossSectionSimulation object""" if magnification_order > 0: layout.dbu = 10 ** (-3 - magnification_order) xsection_cell.transform(pya.DCplxTrans(10 ** magnification_order)) xsection_parameters = simulation.get_parameters() xsection_parameters['london_penetration_depth'] = london_penetration_depth cell_bbox = xsection_cell.dbbox() # Disabled for single face and flip-chip cases #cell_bbox.p1 -= pya.DPoint(0, xsection_parameters['lower_box_height']) if len(xsection_parameters['face_stack']) == 1: cell_bbox.p2 += pya.DPoint(0, xsection_parameters['upper_box_height']) xsection_parameters['box'] = cell_bbox xsection_parameters['cell'] = xsection_cell xsection_simulation = CrossSectionSimulation(layout, **xsection_parameters) # Keep all parameters given in simulations for JSON for k, v in xsection_parameters.items(): setattr(xsection_simulation, k, v) xsection_simulation.xsection_source_class = type(simulation) xsection_simulation.register_cell_layers_as_sim_layers() material_dict = xsection_parameters['material_dict'] material_dict = ast.literal_eval(material_dict) if isinstance(material_dict, str) else material_dict substrate_material = xsection_parameters['substrate_material'] b_substrate_permittivity = material_dict[substrate_material[0]]['permittivity'] xsection_simulation.set_permittivity('b_substrate', b_substrate_permittivity) if len(xsection_parameters['face_stack']) == 2: t_substrate_permittivity = b_substrate_permittivity if len(substrate_material) > 1: t_substrate_permittivity = material_dict[substrate_material[1]]['permittivity'] xsection_simulation.set_permittivity('t_substrate', t_substrate_permittivity) _oxidise_layers(xsection_simulation, ma_thickness, ms_thickness, sa_thickness) if ma_thickness > 0.0: xsection_simulation.set_permittivity('ma_layer', ma_permittivity) if ms_thickness > 0.0: xsection_simulation.set_permittivity('ms_layer', ms_permittivity) if sa_thickness > 0.0: xsection_simulation.set_permittivity('sa_layer', sa_permittivity) return xsection_simulation