# 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).
import ast
import json
import os
import subprocess
from pathlib import Path
from typing import Callable
from kqcircuits.defaults import STARTUPINFO, XSECTION_PROCESS_PATH
from kqcircuits.pya_resolver import pya, klayout_executable_command
from kqcircuits.simulations.export.cross_section.cross_section_export import (
_check_metal_heights,
_iterate_layers_and_modify_region,
_oxidise_layers,
)
from kqcircuits.util.load_save_layout import load_layout, save_layout
from kqcircuits.simulations.cross_section_simulation import CrossSectionSimulation
from kqcircuits.simulations.simulation import Simulation, to_1d_list
from kqcircuits.util.geometry_json_encoder import GeometryJsonEncoder
### DEPRECATED!
### This file contains utility functions that use XSection as an external tool to produce cross sections
### for sweeped simulations.
### Active development should instead be done on kqcircuits.simulations.export.cross_section_export,
### which uses KQCircuits native implementation to produce cross sections.
[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,
)
[docs]
def create_xsections_from_simulations(
simulations: list[Simulation],
output_path: Path,
cuts: tuple[pya.DPoint, pya.DPoint] | list[tuple[pya.DPoint, pya.DPoint]],
process_path: Path = XSECTION_PROCESS_PATH,
post_processing_function: Callable[[CrossSectionSimulation], None] = None,
oxidise_layers_function: Callable[[CrossSectionSimulation, float, float, float], None] = _oxidise_layers,
ma_permittivity: float = 0,
ms_permittivity: float = 0,
sa_permittivity: float = 0,
ma_thickness: float = 0,
ms_thickness: float = 0,
sa_thickness: float = 0,
vertical_cull: tuple[float, float] | None = None,
mer_box: pya.DBox | list[pya.DBox] | None = None,
london_penetration_depth: float | list = 0,
magnification_order: int = 0,
layout: pya.Layout | None = None,
) -> 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
post_processing_function: Additional function to post-process the cross-section geometry.
Defaults to None, in which case no post-processing is performed.
The function takes a CrossSectionSimulation object as argument
oxidise_layers_function: Set this argument if you have a custom way of introducing
oxidization layers to the cross-section metal deposits and substrate.
See expected function signature from pyhints
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
vertical_cull: Tuple of two y-coordinates, will cull all geometry not in-between the y-coordinates.
None by default, which means all geometry is retained.
mer_box: If set as pya.DBox, will create a specified box as metal edge region,
meaning that the geometry inside the region are separated into different layers with '_mer' suffix
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
layout: predefined layout for the cross-section simulation (optional)
Returns:
List of CrossSectionSimulation objects for each Simulation object in simulations
"""
if isinstance(cuts, tuple):
cuts = [cuts] * len(simulations)
cuts = [tuple(c if isinstance(c, pya.DPoint) else c.to_p() for c in cut) for cut in cuts]
if len(simulations) != len(cuts):
raise ValueError("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 ValueError("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)
if layout is None:
layout = pya.Layout()
xsection_cells = []
for simulation, cut in zip(simulations, cuts):
_check_metal_heights(simulation)
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"
save_layout(simulation_file, simulation.layout, [simulation.cell], no_empty_cells=True)
xsection_call(simulation_file, xsection_file, cut[0], cut[1], process_path, xsection_parameters)
load_layout(xsection_file, layout)
for i in layout.layer_indexes():
if all(layout.begin_shapes(cell, i).at_end() for cell in layout.top_cells()):
layout.delete_layer(i) # delete empty layers caused by bug in klayout 0.29.0
xsection_cells.append(layout.top_cells()[-1])
xsection_cells[-1].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],
post_processing_function,
oxidise_layers_function,
ma_permittivity,
ms_permittivity,
sa_permittivity,
ma_thickness,
ms_thickness,
sa_thickness,
vertical_cull,
mer_box,
london_penetration_depth,
magnification_order,
)
for idx, xsection_cell in enumerate(xsection_cells)
]
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
simulation_params["chip_distance"] = to_1d_list(simulation_params["chip_distance"])
# Also dump all used layers in the simulation cell
simulation_params["sim_layers"] = {l.name: f"{l.layer}/{l.datatype}" for l in simulation.layout.layer_infos()}
xsection_parameters_file = xsection_dir / f"parameters_{simulation.cell.name}.json"
with open(xsection_parameters_file, "w", encoding="utf-8") as sweep_file:
json.dump(simulation_params, sweep_file, cls=GeometryJsonEncoder)
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 _construct_cross_section_simulation(
layout,
xsection_cell,
simulation,
post_processing_function,
oxidise_layers_function,
ma_permittivity,
ms_permittivity,
sa_permittivity,
ma_thickness,
ms_thickness,
sa_thickness,
vertical_cull,
mer_box,
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"])
if vertical_cull is not None:
cell_bbox.p1 = pya.DPoint(cell_bbox.p1.x, min(vertical_cull))
cell_bbox.p2 = pya.DPoint(cell_bbox.p2.x, max(vertical_cull))
xsection_parameters["box"] = cell_bbox
xsection_parameters["cell"] = xsection_cell
xsection_simulation = CrossSectionSimulation(layout, **xsection_parameters, ignore_process_layers=True)
# 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"]
substrate_1_permittivity = material_dict[substrate_material[0]]["permittivity"]
xsection_simulation.set_permittivity("substrate_1", substrate_1_permittivity)
if len(xsection_parameters["face_stack"]) == 2:
substrate_2_permittivity = substrate_1_permittivity
if len(substrate_material) > 1:
substrate_2_permittivity = material_dict[substrate_material[1]]["permittivity"]
xsection_simulation.set_permittivity("substrate_2", substrate_2_permittivity)
if post_processing_function:
post_processing_function(xsection_simulation)
if oxidise_layers_function:
oxidise_layers_function(xsection_simulation, ma_thickness, ms_thickness, sa_thickness)
if vertical_cull is not None:
def _cull_region_vertically(region, layer): # pylint: disable=unused-argument
return region & cell_bbox.to_itype(xsection_cell.layout().dbu)
_iterate_layers_and_modify_region(xsection_cell, _cull_region_vertically)
if mer_box is not None:
regions_to_update = {}
if isinstance(mer_box, list):
box_region = pya.Region()
for mb in mer_box:
box_region += pya.Region(mb.to_itype(xsection_cell.layout().dbu))
else:
box_region = pya.Region(mer_box.to_itype(xsection_cell.layout().dbu))
def _separate_region_in_mer_box(region, layer):
region_in_box = region & box_region
regions_to_update[f"{layer.name}_mer"] = region_in_box
return region - box_region
_iterate_layers_and_modify_region(xsection_cell, _separate_region_in_mer_box)
vacuum_in_box = box_region
for layer, region in regions_to_update.items():
vacuum_in_box -= region
xsection_cell.shapes(xsection_simulation.get_sim_layer(layer)).insert(region)
xsection_cell.shapes(xsection_simulation.get_sim_layer("vacuum_mer")).insert(vacuum_in_box)
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)
xsection_simulation.process_layers()
return xsection_simulation