From 1e3a1fc549258e49a964c3405c147bb28793b8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20R=C3=A4bin=C3=A4?= Date: Tue, 26 Sep 2023 15:30:05 +0300 Subject: [PATCH] Metal edge region, epr integration, and manual refinement Enable metal edge region to divide substrate, vacuum and TLS layers into smaller pieces next to metal edge. Dimensions of metal edge region are set by parameter `metal_edge_region_dimensions`. Improve TLS layers in simulation class. Now, these can be modelled as sheet layers, non-model layers, or material layers. TLS layers work with sheet or thick metal layers and with vertical over etching, but not with multilayer stack-up feature. Add in-house energy integration to Ansys export. This is now more straight-forward compared to pyEPR method. The energy integration is enabled by setting the export parameter `'integrate_energies': True` and the post-processing to calculate participation ratios is enable by setting `'post_process_script': 'export_epr.py'`. Add `mesh_size` feature to Ansys simulations to enable manual refinement on any layer. Remove parameter `gap_max_element_length`, because `mesh_size` has the same functionality. Fix simulation scripts that are affected by the changes. This includes removal of `gap_max_element_length` and refactoring of `participation_sheet_distance` and `participation_sheet_thickness` parameters. Create simulation script `TLS_waveguide_sim.py` to demonstrate the new features. Fix `t1_estimate.py` to work without need to unite the TLS layers. --- .../simulations/export/ansys/ansys_export.py | 29 ++-- .../kqcircuits/simulations/simulation.py | 140 ++++++++++++++---- .../scripts/simulations/ansys/export_epr.py | 69 +++++++++ .../simulations/ansys/export_solution_data.py | 119 ++++++++------- .../ansys/import_simulation_geometry.py | 88 +++++++---- .../scripts/simulations/ansys/t1_estimate.py | 4 +- .../scripts/simulations/double_pads_sim.py | 6 +- .../simulations/hanger_resonator_sim.py | 2 +- .../simulations/skip_errors_sweep_example.py | 2 +- .../scripts/simulations/tls_waveguide_sim.py | 86 +++++++++++ .../simulations/waveguide_eig_mesh_test.py | 5 +- 11 files changed, 413 insertions(+), 137 deletions(-) create mode 100644 klayout_package/python/scripts/simulations/ansys/export_epr.py create mode 100644 klayout_package/python/scripts/simulations/tls_waveguide_sim.py diff --git a/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py b/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py index d5832de45..d55c1f3a8 100644 --- a/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py +++ b/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py @@ -48,8 +48,9 @@ def export_ansys_json(simulation: Simulation, path: Path, ansys_tool='hfss', frequency_units="GHz", frequency=5, max_delta_s=0.1, percent_error=1, percent_refinement=30, maximum_passes=12, minimum_passes=1, minimum_converged_passes=1, sweep_enabled=True, sweep_start=0, sweep_end=10, sweep_count=101, sweep_type='interpolating', - max_delta_f=0.1, n_modes=2, gap_max_element_length=None, substrate_loss_tangent=0, - dielectric_surfaces=None, simulation_flags=None, ansys_project_template=None): + max_delta_f=0.1, n_modes=2, mesh_size=None, substrate_loss_tangent=0, + dielectric_surfaces=None, simulation_flags=None, ansys_project_template=None, + integrate_energies=False): r""" Export Ansys simulation into json and gds files. @@ -72,8 +73,8 @@ def export_ansys_json(simulation: Simulation, path: Path, ansys_tool='hfss', sweep_type: choices are "interpolating", "discrete" or "fast" max_delta_f: Maximum allowed relative difference in eigenfrequency (%). Used when ``ansys_tool`` is *eigenmode*. n_modes: Number of eigenmodes to solve. Used when ``ansys_tool`` is 'pyepr'. - gap_max_element_length: Largest mesh element length allowed in the gaps given in simulation units - (if None is given, then the mesh element size is not restricted in the gap). + mesh_size(dict): Dictionary to determine manual mesh refinement on layers. Set key as the layer name and + value as the maximal mesh element length inside the layer. substrate_loss_tangent: Bulk loss tangent (:math:`\tan{\delta}`) material parameter. 0 is off. dielectric_surfaces: Material parameters for TLS interfaces, used in post-processing field calculations from the participation sheets. Default is None. Input is of the form:: @@ -95,6 +96,7 @@ def export_ansys_json(simulation: Simulation, path: Path, ansys_tool='hfss', }, simulation_flags: Optional export processing, given as list of strings ansys_project_template: path to the simulation template + integrate_energies: Calculate energy integrals over each layer and save them into a file Returns: Path to exported json file. @@ -125,10 +127,11 @@ def export_ansys_json(simulation: Simulation, path: Path, ansys_tool='hfss', 'max_delta_f': max_delta_f, 'n_modes': n_modes, }, - 'gap_max_element_length': gap_max_element_length, + 'mesh_size': {} if mesh_size is None else mesh_size, 'substrate_loss_tangent': substrate_loss_tangent, 'dielectric_surfaces': dielectric_surfaces, - 'simulation_flags': simulation_flags + 'simulation_flags': simulation_flags, + 'integrate_energies': integrate_energies } if ansys_project_template is not None: @@ -229,12 +232,12 @@ def export_ansys(simulations, path: Path, ansys_tool='hfss', import_script_folde frequency_units="GHz", frequency=5, max_delta_s=0.1, percent_error=1, percent_refinement=30, maximum_passes=12, minimum_passes=1, minimum_converged_passes=1, sweep_enabled=True, sweep_start=0, sweep_end=10, sweep_count=101, sweep_type='interpolating', - max_delta_f=0.1, n_modes=2, gap_max_element_length=None, substrate_loss_tangent=0, + max_delta_f=0.1, n_modes=2, mesh_size=None, substrate_loss_tangent=0, dielectric_surfaces=None, exit_after_run=False, ansys_executable=r"%PROGRAMFILES%\AnsysEM\v232\Win64\ansysedt.exe", import_script='import_and_simulate.py', post_process_script='export_batch_results.py', intermediate_processing_command=None, use_rel_path=True, simulation_flags=None, - ansys_project_template=None, skip_errors=False): + ansys_project_template=None, integrate_energies=False, skip_errors=False): r""" Export Ansys simulations by writing necessary scripts and json, gds, and bat files. @@ -259,8 +262,8 @@ def export_ansys(simulations, path: Path, ansys_tool='hfss', import_script_folde sweep_type: choices are "interpolating", "discrete" or "fast" max_delta_f: Maximum allowed relative difference in eigenfrequency (%). Used when ``ansys_tool`` is *eigenmode*. n_modes: Number of eigenmodes to solve. Used when ``ansys_tool`` is 'eigenmode'. - gap_max_element_length: Largest mesh element length allowed in the gaps given in simulation units - (if None is given, then the mesh element size is not restricted in the gap). + mesh_size(dict): Dictionary to determine manual mesh refinement on layers. Set key as the layer name and + value as the maximal mesh element length inside the layer. substrate_loss_tangent: Bulk loss tangent (:math:`\tan{\delta}`) material parameter. 0 is off. dielectric_surfaces: Material parameters for TLS interfaces, used in post-processing field calculations from the participation sheets. Default is None. Input is of the form:: @@ -294,6 +297,7 @@ def export_ansys(simulations, path: Path, ansys_tool='hfss', import_script_folde use_rel_path: Determines if to use relative paths. simulation_flags: Optional export processing, given as list of strings. See Simulation Export in docs. ansys_project_template: path to the simulation template + integrate_energies: Calculate energy integrals over each layer and save them into a file skip_errors: Skip simulations that cause errors. Default is False. .. warning:: @@ -318,11 +322,12 @@ def export_ansys(simulations, path: Path, ansys_tool='hfss', import_script_folde sweep_enabled=sweep_enabled, sweep_start=sweep_start, sweep_end=sweep_end, sweep_count=sweep_count, sweep_type=sweep_type, max_delta_f=max_delta_f, n_modes=n_modes, - gap_max_element_length=gap_max_element_length, + mesh_size=mesh_size, substrate_loss_tangent=substrate_loss_tangent, dielectric_surfaces=dielectric_surfaces, simulation_flags=simulation_flags, - ansys_project_template=ansys_project_template)) + ansys_project_template=ansys_project_template, + integrate_energies=integrate_energies)) except (IndexError, ValueError, Exception) as e: # pylint: disable=broad-except if skip_errors: logging.warning( diff --git a/klayout_package/python/kqcircuits/simulations/simulation.py b/klayout_package/python/kqcircuits/simulations/simulation.py index 692136064..cb4795859 100644 --- a/klayout_package/python/kqcircuits/simulations/simulation.py +++ b/klayout_package/python/kqcircuits/simulations/simulation.py @@ -149,7 +149,8 @@ class Simulation: airbridge_height = Param(pdt.TypeDouble, "Height of airbridges.", 3.4, unit="µm") metal_height = Param(pdt.TypeList, "Height of metal sheet on each face.", [0.0], unit="µm") dielectric_height = Param(pdt.TypeList, "Height of insulator dielectric on each face.", [0.0], unit="µm") - dielectric_material = Param(pdt.TypeList, "Material of insulator dielectric on each face.", ['silicon'], unit="µm") + dielectric_material = Param(pdt.TypeList, "Material of insulator dielectric on each face.", ['silicon'], unit="µm", + docstring="Use only keywords introduced in material_dict.") waveguide_length = Param(pdt.TypeDouble, "Length of waveguide stubs or distance between couplers and waveguide " "turning point", 100, unit="µm") @@ -157,8 +158,19 @@ class Simulation: vertical_over_etching = Param(pdt.TypeDouble, "Vertical over-etching into substrates at gaps.", 0, unit="μm") hollow_tsv = Param(pdt.TypeBoolean, "Make TSVs hollow with vacuum inside and thin metal boundary.", False) - participation_sheet_distance = Param(pdt.TypeDouble, "Distance to non-model TLS interface sheet.", 0.0, unit="µm") - participation_sheet_thickness = Param(pdt.TypeDouble, "Thickness of non-model TLS interface sheet.", 0.0, unit="µm") + metal_edge_region_dimensions = Param(pdt.TypeList, "Dimensions of metal edge region", [], unit="µm", + docstring="Metal edge region is disabled if the list is empty. The terms in " + "the list correspond to expansion dimensions into direction of gap, " + "vacuum, metal, and substrate, respectively. The implementation " + "uses the modulo operator in indexing, so one can set the list as " + "[r] meaning that r is the expansion to all directions, or set as " + "[r_lat, r_vert] to separate lateral and vertical expansions.") + tls_layer_thickness = Param(pdt.TypeList, "Thickness of TLS interface layers (MA, MS, and SA, respectively)", [0.0], + unit="µm") + tls_layer_material = Param(pdt.TypeList, "Materials of TLS interface layers (MA, MS, and SA, respectively)", None, + docstring="Use None to create non-model layers. Otherwise, use only keywords " + "introduced in material_dict.") + tls_sheet_approximation = Param(pdt.TypeBoolean, "Approximate TLS interface layers as sheets", False) minimum_point_spacing = Param(pdt.TypeDouble, "Tolerance for merging adjacent points in polygon", 0.01, unit="µm") polygon_tolerance = Param(pdt.TypeDouble, "Tolerance for merging adjacent polygons in a layer", 0.004, unit="µm") @@ -460,7 +472,7 @@ def create_simulation_layers(self): # insert TSVs and indium bumps tsv_params = {'edge_material': 'pec'} if self.hollow_tsv else {'material': 'pec'} tsv_region = self.insert_layers_between_faces(i, i - sign, "through_silicon_via", **tsv_params) - self.insert_layers_between_faces(i, i + sign, "indium_bump", material='pec') + bump_region = self.insert_layers_between_faces(i, i + sign, "indium_bump", material='pec') for j, face_id in enumerate(face_ids): ground_box_region = pya.Region(self._face_box(i).to_itype(self.layout.dbu)) @@ -501,18 +513,19 @@ def create_simulation_layers(self): dielectric_region = ground_box_region - self.simplified_region( self.region_from_layer(face_id, "dielectric_etch")) - # Create gap region. Subtract TSVs and ground-grid from regions. - gap_region = ground_box_region - signal_region - ground_region + # Create gap and etch regions and update metals + gap_region = ground_box_region - signal_region - ground_region # excluding ground grid + if self.with_grid: + ground_region -= self.ground_grid_region(face_id) + etch_region = ground_box_region - signal_region - ground_region # including ground grid signal_region -= tsv_region ground_region -= tsv_region dielectric_region -= tsv_region - if self.with_grid: - ground_region -= self.ground_grid_region(face_id) # Insert signal, ground, and dielectric layers to model via splitter if j == 0 and self.vertical_over_etching > 0.0: - self.add_layer_to_splitter(splitter, ground_box_region - signal_region - ground_region, - face_id + "_etch", thickness=-sign * self.vertical_over_etching) + self.add_layer_to_splitter(splitter, etch_region, face_id + "_etch", + thickness=-sign * self.vertical_over_etching) metal_thickness = z[face_id][1] - z[face_id][0] self.add_layer_to_splitter(splitter, signal_region, face_id + "_signal", thickness=metal_thickness, @@ -532,27 +545,91 @@ def create_simulation_layers(self): # Insert airbridges bridge_z = sign * self.airbridge_height - self.insert_layer( - self.simplified_region(self.region_from_layer(face_id, "airbridge_flyover")) & ground_box_region, - face_id + "_airbridge_flyover", z=z[face_id][1] + bridge_z, thickness=0.0, material='pec') - self.insert_layer( - self.simplified_region(self.region_from_layer(face_id, "airbridge_pads")) & ground_box_region, - face_id + "_airbridge_pads", z=z[face_id][1], thickness=bridge_z, material='pec') - - # Insert participation layers (no material) - if j == 0 and self.participation_sheet_distance + self.participation_sheet_thickness > 0.0: - self.insert_layer(signal_region + ground_region, face_id + "_layerMA", - z=z[face_id][1] + sign * self.participation_sheet_distance, - thickness=sign * self.participation_sheet_thickness) - self.insert_layer(signal_region + ground_region, face_id + "_layerMS", - z=z[face_id][0] - sign * self.participation_sheet_distance, - thickness=-sign * self.participation_sheet_thickness) - self.insert_layer(ground_box_region - signal_region - ground_region, face_id + "_layerSA", - z=z[face_id][0] + sign * self.participation_sheet_distance, - thickness=sign * self.participation_sheet_thickness) + ab_flyover_region = self.simplified_region(self.region_from_layer(face_id, "airbridge_flyover") + ) & ground_box_region + self.insert_layer(ab_flyover_region, face_id + "_airbridge_flyover", z=z[face_id][1] + bridge_z, + thickness=0.0, material='pec') + ab_pads_region = self.simplified_region(self.region_from_layer(face_id, "airbridge_pads") + ) & ground_box_region + self.insert_layer(ab_pads_region, face_id + "_airbridge_pads", z=z[face_id][1], thickness=bridge_z, + material='pec') self.insert_splitter_layers(splitter, z[i + 1]) + # Rest of the features are not available with multilayer stack-up + if len(face_ids) != 1: + continue + face_id = face_ids[0] + + # Create metal edge region + metal_region = signal_region + ground_region + me_region = pya.Region() + n_terms = len(self.metal_edge_region_dimensions) + if n_terms > 0: + r_gap = float(self.metal_edge_region_dimensions[0]) + r_metal = float(self.metal_edge_region_dimensions[2 % n_terms]) + me_region = (metal_region.sized(r_gap / self.layout.dbu) & etch_region.sized(r_metal / self.layout.dbu) + & ground_box_region) + + # Insert TLS interface layers + for layer_num, layer_id in enumerate(['MA', 'MS', 'SA']): + layer_name = face_id + "_layer" + layer_id + layer_z = [z[face_id][1], z[face_id][0], z[face_id][0] - sign * self.vertical_over_etching][layer_num] + thickness = float(self.ith_value(self.tls_layer_thickness, layer_num)) + signed_thickness = [sign, -sign, -sign][layer_num] * thickness + if self.tls_sheet_approximation: + params = {'z': layer_z + signed_thickness, + 'thickness': 0.0} + elif thickness != 0.0: + material = self.ith_value(self.tls_layer_material, layer_num) + params = {'z': layer_z, + 'thickness': signed_thickness, + **(dict() if material is None else {'material': material})} + + # Insert wall layer + wall_height = [z[face_id][0] - z[face_id][1], 0.0, sign * self.vertical_over_etching][layer_num] + if wall_height != 0.0: + wall_region = metal_region.sized(thickness / self.layout.dbu) & etch_region + self.insert_layer(wall_region, layer_name + "wall", z=layer_z, thickness=wall_height, + **(dict() if material is None else {'material': material})) + else: + continue + + # Insert layer + layer_region = [metal_region.sized(thickness / self.layout.dbu) & (metal_region + etch_region - + bump_region - ab_pads_region), + metal_region, etch_region][layer_num] + me_layer_region = me_region & layer_region + if me_layer_region.is_empty(): + self.insert_layer(layer_region, layer_name, **params) + else: + self.insert_layer(me_layer_region, layer_name + "mer", **params) + self.insert_layer(layer_region - me_region, layer_name, **params) + + # Insert substrate and vacuum inside metal edge region + if not me_region.is_empty(): + r_vacuum = float(self.metal_edge_region_dimensions[1 % n_terms]) + r_substrate = float(self.metal_edge_region_dimensions[3 % n_terms]) + + if r_substrate != 0.0: + layers = ['_etch', '_through_silicon_via'] + cond_layers = ['_layerMSmer', '_layerSAmer'] + subtract = [k for k, v in self.layers.items() if face_id + '_' in k and + (any(k.endswith(t) for t in layers) or + (any(k.endswith(t) for t in cond_layers) and v.get('material', None) is not None))] + self.insert_layer(me_region, face_id + "_substratemer", z=z[face_id][0] - sign * r_substrate, + thickness=sign * r_substrate, + material=self.ith_value(self.substrate_material, + (i + int(self.lower_box_height <= 0)) // 2), + **({'subtract': subtract} if subtract else dict())) + + if r_vacuum + r_substrate != 0.0: + subtract = [n for n, v in self.layers.items() if face_id + '_' in n and + v.get('material', None) is not None and v.get('thickness', 0.0) != 0.0] + self.insert_layer(me_region, face_id + "_vacuummer", z=z[face_id][0] - sign * r_substrate, + thickness=sign * (r_vacuum + r_substrate), material='vacuum', + **({'subtract': subtract} if subtract else dict())) + # Insert substrates for i in range(int(self.lower_box_height > 0), len(face_stack) + 1, 2): # faces around the substrate @@ -563,8 +640,11 @@ def create_simulation_layers(self): faces += face_stack[i-1] # find layers to be subtracted from substrate - layers = ['etch', 'through_silicon_via'] - subtract = [k for k in self.layers if any(t in k for t in layers) and any(t in k for t in faces)] + layers = ['_etch', '_through_silicon_via'] + cond_layers = ['_layerMS', '_layerSA', '_layerMSmer', '_layerSAmer', '_substratemer'] + subtract = [k for k, v in self.layers.items() if any(t + '_' in k for t in faces) and + (any(k.endswith(t) for t in layers) or + (any(k.endswith(t) for t in cond_layers) and v.get('material', None) is not None))] # insert substrate layer name = 'substrate' if len(face_stack) - int(self.lower_box_height > 0) < 2 else f'substrate_{i // 2}' diff --git a/klayout_package/python/scripts/simulations/ansys/export_epr.py b/klayout_package/python/scripts/simulations/ansys/export_epr.py new file mode 100644 index 000000000..7c2ddba59 --- /dev/null +++ b/klayout_package/python/scripts/simulations/ansys/export_epr.py @@ -0,0 +1,69 @@ +# This code is part of KQCircuits +# Copyright (C) 2023 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). + + +# This is a Python 2.7 script that should be run in HFSS in order to export energy participation ratios +import os +import json +import csv + +# Find data files +path = os.path.curdir + +prefix = os.path.basename(os.path.abspath(path)) + +result_files = [f for f in os.listdir(path) if f.endswith('_project_energy.csv')] +definition_files = [f.replace('_project_energy.csv', '.json') for f in result_files] +keys = [f.replace('_project_energy.csv', '') for f in result_files] +nominal = min(keys, key=len) + +# Load result data +data = {} +parameter_dict = {} +epr_dict = {} + +for key, definition_file, result_file in zip(keys, definition_files, result_files): + with open(definition_file, 'r') as f: + definition = json.load(f) + with open(result_file, 'r') as f: + reader = csv.reader(f, delimiter=',') + result_keys = next(reader) + result_values = next(reader) + result = {k: float(v) for k, v in zip(result_keys, result_values)} + + parameter_dict[key] = definition['parameters'] + total_energy = result.get("total_energy []", 1.0) + epr_dict[key] = {k[2:-3]: v / total_energy for k, v in result.items() if k.startswith("E_") and k.endswith(" []")} + +# Find parameters that are swept +parameters = [] +for parameter in parameter_dict[nominal]: + if not all(parameter_dict[key][parameter] == parameter_dict[nominal][parameter] for key in keys): + parameters.append(parameter) + +# Tabulate C matrix as CSV +with open('%s_epr.csv' % prefix, 'wb') as csvfile: # wb for python 2? + writer = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + + layer_names = sorted({n for k, v in epr_dict.items() for n in v}) + writer.writerow(['key'] + parameters + layer_names) + + for key in keys: + parameter_values = [parameter_dict[key][parameter] for parameter in parameters] + parameter_values_str = [str(parameter_value) for parameter_value in parameter_values] + layer_values = [str(epr_dict[key].get(n, 0.0)) for n in layer_names] + writer.writerow([key] + parameter_values_str + layer_values) diff --git a/klayout_package/python/scripts/simulations/ansys/export_solution_data.py b/klayout_package/python/scripts/simulations/ansys/export_solution_data.py index f9e8280da..161833ea1 100644 --- a/klayout_package/python/scripts/simulations/ansys/export_solution_data.py +++ b/klayout_package/python/scripts/simulations/ansys/export_solution_data.py @@ -54,66 +54,71 @@ def save_capacitance_matrix(file_name, c_matrix, detail=''): matrix_filename = os.path.join(path, basename + '_CMatrix.txt') json_filename = os.path.join(path, basename + '_results.json') eig_filename = os.path.join(path, basename + '_modes.eig') +energy_filename = os.path.join(path, basename + '_energy.csv') # Export solution data separately for HFSS and Q3D design_type = oDesign.GetDesignType() -if design_type == "HFSS" and oDesign.GetSolutionType() == "HFSS Terminal Network": - - freq = '1GHz' # export frequency - (setup, sweep) = get_enabled_setup_and_sweep(oDesign) - solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) - context = [] if sweep is None else ["Domain:=", "Sweep"] - families = ["Freq:=", [freq]] - - # Get list of ports - ports = oBoundarySetup.GetExcitations()[::2] - - # Get solution data - yyMatrix = [[get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, - "yy_{}_{}".format(port_i, port_j))[0] for port_j in ports] for port_i in ports] - CMatrix = [[get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, - "C_{}_{}".format(port_i, port_j))[0] for port_j in ports] for port_i in ports] - - # Save capacitance matrix into readable format - save_capacitance_matrix(matrix_filename, CMatrix, detail=' at ' + freq) - - # Save results in json format - with open(json_filename, 'w') as outfile: - json.dump({'CMatrix': CMatrix, - 'yyMatrix': yyMatrix, - 'freq': freq, - 'yydata': get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, - ["yy_{}_{}".format(port_i, port_j) for port_j in ports - for port_i in ports]), - 'Cdata': get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, - ["C_{}_{}".format(port_i, port_j) for port_j in ports - for port_i in ports]) - }, outfile, cls=ComplexEncoder, indent=4) - - # S-parameter export (only for HFSS) - file_format = 3 # 2 = Tab delimited (.tab), 3 = Touchstone (.sNp), 4 = CitiFile (.cit), 7 = Matlab (.m), ... - file_name = os.path.join(path, basename + '_SMatrix.s{}p'.format(len(ports))) - frequencies = ["All"] - do_renormalize = False - renorm_impedance = 50 - data_type = "S" - pass_number = -1 # -1 = all passes - complex_format = 0 # 0 = magnitude/phase, 1 = real/imag, 2 = dB/phase - precision = 8 - show_gamma_and_impedance = False - - oSolutions.ExportNetworkData("", solution, file_format, file_name, frequencies, do_renormalize, renorm_impedance, - data_type, pass_number, complex_format, precision, True, show_gamma_and_impedance, - True) - -elif design_type == "HFSS" and oDesign.GetSolutionType() == "Eigenmode": - - solution = get_enabled_setup(oDesign, tab="HfssTab") + " : LastAdaptive" - oSolutions.ExportEigenmodes( - solution, - oSolutions.ListVariations(solution)[0], - eig_filename - ) +if design_type == "HFSS": + if oDesign.GetSolutionType() == "HFSS Terminal Network": + freq = '1GHz' # export frequency + (setup, sweep) = get_enabled_setup_and_sweep(oDesign) + solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) + context = [] if sweep is None else ["Domain:=", "Sweep"] + families = ["Freq:=", [freq]] + + # Get list of ports + ports = oBoundarySetup.GetExcitations()[::2] + + # Get solution data + yyMatrix = [[get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, + "yy_{}_{}".format(port_i, port_j))[0] for port_j in ports] for port_i in ports] + CMatrix = [[get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, + "C_{}_{}".format(port_i, port_j))[0] for port_j in ports] for port_i in ports] + + # Save capacitance matrix into readable format + save_capacitance_matrix(matrix_filename, CMatrix, detail=' at ' + freq) + + # Save results in json format + with open(json_filename, 'w') as outfile: + json.dump({'CMatrix': CMatrix, + 'yyMatrix': yyMatrix, + 'freq': freq, + 'yydata': get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, + ["yy_{}_{}".format(port_i, port_j) for port_j in ports + for port_i in ports]), + 'Cdata': get_solution_data(oReportSetup, "Terminal Solution Data", solution, context, families, + ["C_{}_{}".format(port_i, port_j) for port_j in ports + for port_i in ports]) + }, outfile, cls=ComplexEncoder, indent=4) + + # S-parameter export (only for HFSS) + file_format = 3 # 2 = Tab delimited (.tab), 3 = Touchstone (.sNp), 4 = CitiFile (.cit), 7 = Matlab (.m), ... + file_name = os.path.join(path, basename + '_SMatrix.s{}p'.format(len(ports))) + frequencies = ["All"] + do_renormalize = False + renorm_impedance = 50 + data_type = "S" + pass_number = -1 # -1 = all passes + complex_format = 0 # 0 = magnitude/phase, 1 = real/imag, 2 = dB/phase + precision = 8 + show_gamma_and_impedance = False + + oSolutions.ExportNetworkData("", solution, file_format, file_name, frequencies, do_renormalize, + renorm_impedance, data_type, pass_number, complex_format, precision, True, + show_gamma_and_impedance, True) + + elif oDesign.GetSolutionType() == "Eigenmode": + solution = get_enabled_setup(oDesign, tab="HfssTab") + " : LastAdaptive" + context = [] + oSolutions.ExportEigenmodes(solution, oSolutions.ListVariations(solution)[0], eig_filename) + + # Save energy integrals + energies = oReportSetup.GetAllQuantities("Fields", "Data Table", solution, context, "Calculator Expressions") + if energies: + oReportSetup.CreateReport("Energy Integrals", "Fields", "Data Table", solution, context, + ["Freq:=", ["All"], "Phase:=", ["0deg"]], + ["X Component:=", "Freq", "Y Component:=", energies]) + oReportSetup.ExportToFile("Energy Integrals", energy_filename, False) elif design_type == "Q3D Extractor": solution = get_enabled_setup(oDesign) + " : LastAdaptive" diff --git a/klayout_package/python/scripts/simulations/ansys/import_simulation_geometry.py b/klayout_package/python/scripts/simulations/ansys/import_simulation_geometry.py index 15036186f..50aeffb0f 100644 --- a/klayout_package/python/scripts/simulations/ansys/import_simulation_geometry.py +++ b/klayout_package/python/scripts/simulations/ansys/import_simulation_geometry.py @@ -26,7 +26,7 @@ # TODO: Figure out how to set the python path for the Ansys internal IronPython sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'util')) from geometry import create_box, create_rectangle, create_polygon, thicken_sheet, set_material, add_layer, delete, \ - move_vertically, subtract, unite, objects_from_sheet_edges, add_material # pylint: disable=wrong-import-position + move_vertically, subtract, objects_from_sheet_edges, add_material # pylint: disable=wrong-import-position # Set up environment ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") @@ -49,7 +49,7 @@ ansys_project_template = data.get('ansys_project_template', '') vertical_over_etching = data.get('vertical_over_etching', 0) -gap_max_element_length = data.get('gap_max_element_length', None) +mesh_size = data.get('mesh_size', dict()) # Create project oDesktop.RestoreWindow() @@ -82,8 +82,8 @@ # Import GDSII geometry layers = data.get('layers', dict()) -if gap_max_element_length is None: - layers = {n: d for n, d in layers.items() if '_gap' not in n} # ignore gap objects if they are not used +# ignore gap objects if they are not used +layers = {n: d for n, d in layers.items() if '_gap' not in n or n in mesh_size} order_map = [] layer_map = ["NAME:LayerMap"] @@ -353,41 +353,71 @@ oBoundarySetup.AutoIdentifyNets() # Combine Nets by conductor connections. Order: GroundNet, SignalNet, FloatingNet -# Unite sheets modelling participation surfaces -for layer in ['layerMA', 'layerMS', 'layerSA']: - layer_objects = [o for n, v in objects.items() if '_' + layer in n for o in v] - if layer_objects: - unite(oEditor, layer_objects, False) - oEditor.ChangeProperty( - ["NAME:AllTabs", - ["NAME:Geometry3DAttributeTab", - ["NAME:PropServers", layer_objects[0]], - ["NAME:ChangedProps", - ["NAME:Model", "Value:=", False], # non-modelled sheet - ["NAME:Color", "R:=", 197, "G:=", 197, "B:=", 197], # grey - ["NAME:Name", "Value:=", layer] - ] - ] - ]) +# Add field calculations +if data.get('integrate_energies', False) and ansys_tool in {'hfss', 'eigenmode'}: + # Create term for squared E field and call it 'Esq' + oModule = oDesign.GetModule("FieldsReporter") + oModule.EnterQty("E") + oModule.CalcOp("CmplxMag") + oModule.CalcOp("Mag") + oModule.EnterScalar(2) + oModule.CalcOp("Pow") + oModule.AddNamedExpression("Esq", "Fields") + + # Create energy integral terms for each object + total_solids = [] + epsilon_0 = 8.8541878128e-12 + for lname, ldata in layers.items(): + material = ldata.get('material', None) + if material == 'pec': + continue + + thickness = ldata.get('thickness', 0.0) + for n, oname in enumerate(objects[lname]): + oModule.CopyNamedExprToStack("Esq") + if thickness == 0.0: + oModule.EnterSurf(oname) + else: + oModule.EnterVol(oname) + oModule.CalcOp("Integrate") + if n > 0: + oModule.CalcOp("+") + + epsilon = epsilon_0 * material_dict.get(material, {}).get('permittivity', 1.0) + if objects[lname]: + oModule.EnterScalar(epsilon / 2) + oModule.CalcOp("*") + else: + oModule.EnterScalar(0.0) + oModule.AddNamedExpression("E_{}".format(lname), "Fields") + + if thickness != 0.0 and material is not None: + total_solids.append("E_{}".format(lname)) + # Create term for total energy + for n, tname in enumerate(total_solids): + oModule.CopyNamedExprToStack(tname) + if n > 0: + oModule.CalcOp("+") + oModule.AddNamedExpression("total_energy", "Fields") -# Manual mesh refinement on gap objects -if gap_max_element_length is not None: - gap_objects = [o for n, v in objects.items() if '_gap' in n for o in v] - if gap_objects: + +# Manual mesh refinement +for mesh_layer, mesh_length in mesh_size.items(): + mesh_objects = objects.get(mesh_layer, list()) + if mesh_objects: oMeshSetup = oDesign.GetModule("MeshSetup") oMeshSetup.AssignLengthOp( [ - "NAME:GapLength", - "RefineInside:=", False, + "NAME:mesh_size_{}".format(mesh_layer), + "RefineInside:=", layers.get(mesh_layer, dict()).get('thickness', 0.0) != 0.0, "Enabled:=", True, - "Objects:=", gap_objects, + "Objects:=", mesh_objects, "RestrictElem:=", False, "RestrictLength:=", True, - "MaxLength:=", str(gap_max_element_length) + units + "MaxLength:=", str(mesh_length) + units ]) - if not ansys_project_template: # Insert analysis setup setup = data['analysis_setup'] diff --git a/klayout_package/python/scripts/simulations/ansys/t1_estimate.py b/klayout_package/python/scripts/simulations/ansys/t1_estimate.py index 302938f1b..9ca98fb45 100644 --- a/klayout_package/python/scripts/simulations/ansys/t1_estimate.py +++ b/klayout_package/python/scripts/simulations/ansys/t1_estimate.py @@ -66,7 +66,9 @@ any(e in [f'Port{i}', f'Junction{i}'] for i in junction_numbers)) ] else: - pinfo.dissipative['dielectric_surfaces'] = data['dielectric_surfaces'] + pinfo.dissipative['dielectric_surfaces'] = { + e: v for e in pinfo.get_all_object_names() for k, v in data['dielectric_surfaces'].items() if k in e + } oEditor = oDesign.modeler._modeler for j in junction_numbers: diff --git a/klayout_package/python/scripts/simulations/double_pads_sim.py b/klayout_package/python/scripts/simulations/double_pads_sim.py index e1e134853..f995be85d 100644 --- a/klayout_package/python/scripts/simulations/double_pads_sim.py +++ b/klayout_package/python/scripts/simulations/double_pads_sim.py @@ -44,8 +44,8 @@ 'box': pya.DBox(pya.DPoint(0, 0), pya.DPoint(2000, 2000)), 'separate_island_internal_ports': sim_tool != 'eigenmode', # DoublePads specific - 'participation_sheet_distance': 5e-3 if sim_tool == 'eigenmode' else 0.0, # in µm - 'participation_sheet_thickness': 0.0, + 'tls_layer_thickness': 5e-3 if sim_tool == 'eigenmode' else 0.0, # in µm + 'tls_sheet_approximation': sim_tool == 'eigenmode', 'waveguide_length': 200, } @@ -61,7 +61,7 @@ 'max_delta_f': 0.008, # do two passes with tight mesh - 'gap_max_element_length': 25, + 'mesh_size': {'1t1_gap': 25}, 'maximum_passes': 17, 'minimum_passes': 1, 'minimum_converged_passes': 2, diff --git a/klayout_package/python/scripts/simulations/hanger_resonator_sim.py b/klayout_package/python/scripts/simulations/hanger_resonator_sim.py index 264fb9bfe..d9d019c88 100644 --- a/klayout_package/python/scripts/simulations/hanger_resonator_sim.py +++ b/klayout_package/python/scripts/simulations/hanger_resonator_sim.py @@ -149,7 +149,7 @@ 'sweep_count': 1001, 'maximum_passes': 20, 'exit_after_run': True, - 'gap_max_element_length': 1 + 'mesh_size': {'1t1_gap': 1} } else: export_parameters_ansys = { diff --git a/klayout_package/python/scripts/simulations/skip_errors_sweep_example.py b/klayout_package/python/scripts/simulations/skip_errors_sweep_example.py index ef65a8434..5a9bfa977 100644 --- a/klayout_package/python/scripts/simulations/skip_errors_sweep_example.py +++ b/klayout_package/python/scripts/simulations/skip_errors_sweep_example.py @@ -47,7 +47,7 @@ export_parameters = { 'path': dir_path, 'exit_after_run': True, - 'gap_max_element_length': 20, # converges fast + 'mesh_size': {face_id + '_gap': 20 for face_id in ['1t1', '2b1']}, # converges fast 'ansys_tool': 'eigenmode', 'max_delta_f': 0.5, 'maximum_passes': 2, diff --git a/klayout_package/python/scripts/simulations/tls_waveguide_sim.py b/klayout_package/python/scripts/simulations/tls_waveguide_sim.py new file mode 100644 index 000000000..86aee0945 --- /dev/null +++ b/klayout_package/python/scripts/simulations/tls_waveguide_sim.py @@ -0,0 +1,86 @@ +# This code is part of KQCircuits +# Copyright (C) 2023 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 logging +import sys +from pathlib import Path + +from kqcircuits.elements.waveguide_coplanar import WaveguideCoplanar +from kqcircuits.pya_resolver import pya +from kqcircuits.simulations.export.ansys.ansys_export import export_ansys +from kqcircuits.simulations.export.simulation_export import export_simulation_oas, sweep_simulation +from kqcircuits.simulations.port import EdgePort +from kqcircuits.simulations.simulation import Simulation +from kqcircuits.util.export_helper import create_or_empty_tmp_directory, get_active_or_new_layout, \ + open_with_klayout_or_default_application + + +class TlsWaveguideSim(Simulation): + """ A very short segment of waveguide. """ + + def build(self): + self.insert_cell(WaveguideCoplanar, path=pya.DPath([pya.DPoint(self.box.left, self.box.center().y), + pya.DPoint(self.box.right, self.box.center().y)], 0)) + self.ports.append(EdgePort(1, pya.DPoint(self.box.left, self.box.center().y), face=0)) + self.ports.append(EdgePort(2, pya.DPoint(self.box.right, self.box.center().y), face=0)) + + +# Prepare output directory +dir_path = create_or_empty_tmp_directory(Path(__file__).stem + "_output") + +# Simulation parameters +sim_class = TlsWaveguideSim # pylint: disable=invalid-name +sim_parameters = { + 'name': 'tls_waveguide_sim', + 'face_stack': ['1t1'], # single chip + 'box': pya.DBox(pya.DPoint(0, 0), pya.DPoint(10, 100)), + 'substrate_height': 50, # limited simulation domain + 'upper_box_height': 50, # limited simulation domain + 'metal_height': 0.2, + 'metal_edge_region_dimensions': [1.0], + 'tls_layer_thickness': 0.01, + 'tls_layer_material': ['oxideMA', 'oxideMS', 'oxideSA'], + 'material_dict': {**ast.literal_eval(Simulation.material_dict), + 'oxideMA': {'permittivity': 8}, + 'oxideMS': {'permittivity': 11.4}, + 'oxideSA': {'permittivity': 4}}, +} +export_parameters = { + 'path': dir_path, + 'ansys_tool': 'hfss', + 'sweep_enabled': False, + 'exit_after_run': True, + 'mesh_size': {'1t1_layerMAwall': 0.15, + '1t1_layerMAmer': 0.5, + '1t1_layerMSmer': 0.5, + '1t1_layerSAmer': 0.5}, + 'integrate_energies': True, + 'post_process_script': 'export_epr.py', +} + +# Get layout +logging.basicConfig(level=logging.WARN, stream=sys.stdout) +layout = get_active_or_new_layout() + +#Fixed geometry simulation +simulations = sweep_simulation(layout, sim_class, sim_parameters, {'a': [2, 10]}) + +# Export Ansys files +export_ansys(simulations, **export_parameters) + +# Write and open oas file +open_with_klayout_or_default_application(export_simulation_oas(simulations, dir_path)) diff --git a/klayout_package/python/scripts/simulations/waveguide_eig_mesh_test.py b/klayout_package/python/scripts/simulations/waveguide_eig_mesh_test.py index 021b48ccf..2355d3713 100644 --- a/klayout_package/python/scripts/simulations/waveguide_eig_mesh_test.py +++ b/klayout_package/python/scripts/simulations/waveguide_eig_mesh_test.py @@ -25,8 +25,7 @@ from kqcircuits.util.export_helper import create_or_empty_tmp_directory, get_active_or_new_layout, \ open_with_klayout_or_default_application -# This is a test case for initial mesh refinement (Ansys) via -# `gap_max_element_length` that restricts the element length in the gap. +# This is a test case for initial mesh refinement in Ansys sim_class = WaveGuidesSim # pylint: disable=invalid-name path = create_or_empty_tmp_directory("waveguide_eig_mesh_test") @@ -55,7 +54,7 @@ 'ansys_tool': 'eigenmode', 'maximum_passes': 2, 'percent_refinement': 30, - 'gap_max_element_length': 4, + 'mesh_size': {'1t1_gap': 4}, 'exit_after_run': True, 'max_delta_f': 0.1, # maximum relative difference for convergence in % 'n_modes': 1, # eigenmodes to solve