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

# 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).
from typing import Callable, Sequence

from kqcircuits.simulations.export.cross_section.cross_section_export import (
    visualise_cross_section_cut_on_original_layout,
)
from kqcircuits.simulations.export.cross_section.cut_simulation import CutSimulation

from kqcircuits.simulations.simulation import Simulation

from kqcircuits.simulations.partition_region import get_list_of_two

from kqcircuits.simulations.export.simulation_export import cross_combine

from kqcircuits.simulations.export.elmer.elmer_solution import ElmerCrossSectionSolution

from kqcircuits.pya_resolver import pya
from kqcircuits.simulations.post_process import PostProcess


[docs] def get_epr_correction_elmer_solution(**override_args): """Returns ElmerCrossSectionSolution for EPR correction simulations with default arguments. Optionally, 'override_args' can be used to override any arguments. """ return ElmerCrossSectionSolution( **{ "p_element_order": 3, "is_axisymmetric": False, "mesh_size": { "ms_layer_mer&ma_layer_mer": [0.0005, 0.0005, 0.2], }, "integrate_energies": True, "run_inductance_sim": False, **override_args, } )
[docs] def get_epr_correction_simulations( simulations: list[Simulation], correction_cuts: dict[str, dict] | Callable[[Simulation], dict[str, dict]], ma_eps_r: float = 8, ms_eps_r: float = 11.4, sa_eps_r: float = 4, ma_thickness: float = 4.8e-3, # in µm ms_thickness: float = 0.3e-3, # in µm sa_thickness: float = 2.4e-3, # in µm metal_height: float = 0.2, ) -> tuple[list[tuple[CutSimulation, ElmerCrossSectionSolution]], list[PostProcess]]: """Helper function to produce EPR correction simulations. Args: simulations: list of simulation objects correction_cuts: dictionary or function that returns a dictionary of correction cuts for given simulation. Key is the name of cut and values are dicts containing: - p1: pya.DPoint indicating the first end of the cut segment - p2: pya.DPoint indicating the second end of the cut segment - metal_edges: list of dictionaries indicating metal-edge-region locations and orientations in 2D simulation. Can contain keywords: - x: lateral distance from p1 to mer metal edge, - x_reversed: whether gap is closer to p1 than metal, default=False - z: height of substrate-vacuum interface, default=0 - z_reversed: whether vacuum is below substrate, default=False - partition_regions: (optional) list of partition region names. The correction cut key is used if not assigned. - simulations: (optional) list of simulation names. Is applied on all simulations if not assigned. - solution: (optional) solution object for the sim. `get_epr_correction_elmer_solution` is used if not assigned. If `solution` is not set, all items under `correction_cuts[Key]` are given to `get_epr_correction_elmer_solution` except items with keys `["p1", "p2", "metal_edges", "partition_regions", "simulations"]`. ma_eps_r: relative permittivity of MA layer ms_eps_r: relative permittivity of MS layer sa_eps_r: relative permittivity of SA layer ma_thickness: thickness of MA layer ms_thickness: thickness of MS layer sa_thickness: thickness of SA layer metal_height: Thickness of metal layer if sheet in the source simulation Returns: tuple containing list of correction simulations and list of post_process objects """ correction_simulations = [] correction_layout = pya.Layout() simulations = [sim if isinstance(sim, Sequence) else (sim, None) for sim in simulations] source_sims, source_sols = list(zip(*simulations)) region_corrections = {p.name: None for s in source_sims for p in s.get_partition_regions()} for source_sim, source_sol in zip(source_sims, source_sols): cuts = correction_cuts(source_sim) if callable(correction_cuts) else correction_cuts for key, cut in cuts.items(): if "simulations" in cut and source_sim.name not in cut["simulations"]: continue part_names = cut.get("partition_regions", [key]) if not part_names: raise ValueError("Correction cut has no partition region attached") parts = [p for p in source_sim.get_partition_regions() if p.name in part_names] if not parts: continue region_corrections.update({p: key for p in part_names}) v_dims = get_list_of_two(parts[0].vertical_dimensions) h_dims = get_list_of_two(parts[0].metal_edge_dimensions) if None in v_dims or any(v_dims != get_list_of_two(p.vertical_dimensions) for p in parts): raise ValueError(f"Partition region vertical_dimensions are invalid for correction_cut {key}.") if None in h_dims or any(h_dims != get_list_of_two(p.metal_edge_dimensions) for p in parts): raise ValueError(f"Partition region metal_edge_dimensions are invalid for correction_cut {key}.") mer_box = [] for me in cut.get("metal_edges", []): # TODO: replace x, z, x_reversed, z_reversed with face, intersection_n # TODO: we can deduce x from intersection data, just need to know which intersection from the left # TODO: we can deduce z from face name. z_reversed implicit from face name (t or b) x = me.get("x", 0) z = me.get("z", 0) dx = int(me.get("x_reversed", False)) dz = int(me.get("z_reversed", False)) mer_box.append(pya.DBox(x - h_dims[1 - dx], z - v_dims[dz], x + h_dims[dx], z + v_dims[1 - dz])) cords_list = [(cut["p1"], cut["p2"])] correction_layout.dbu = 1e-4 cross_section = CutSimulation( correction_layout, name=source_sim.name + getattr(source_sol, "name", "") + "_" + key, source_sim=source_sim, cut_start=cut["p1"], cut_end=cut["p2"], tls_layer_thickness=[ma_thickness, ms_thickness, sa_thickness], tls_layer_material=["ma", "ms", "sa"], material_dict={ "ma": {"permittivity": ma_eps_r}, "ms": {"permittivity": ms_eps_r}, "sa": {"permittivity": sa_eps_r}, }, region_map={"_mer": mer_box or part_names}, metal_height=metal_height, ) if "solution" in cut: cut_solution = cut["solution"] else: cut_solution = get_epr_correction_elmer_solution( voltage_excitations=getattr(source_sol, "voltage_excitations", None), **{ k: v for k, v in cut.items() if k not in ["p1", "p2", "metal_edges", "partition_regions", "simulations"] }, ) correction_simulations += cross_combine(cross_section, cut_solution) visualise_cross_section_cut_on_original_layout([source_sim], cords_list, cut_label=key, width_ratio=0.03) post_process = [ PostProcess( "produce_epr_table.py", data_file_prefix="correction", sheet_approximations={ "MA": {"thickness": ma_thickness * 1e-6, "eps_r": ma_eps_r}, "MS": {"thickness": ms_thickness * 1e-6, "eps_r": ms_eps_r}, "SA": {"thickness": sa_thickness * 1e-6, "eps_r": sa_eps_r}, }, groups=["MA", "MS", "SA", "substrate", "vacuum"], region_corrections=region_corrections, ), ] return correction_simulations, post_process