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