diff --git a/klayout_package/python/kqcircuits/defaults.py b/klayout_package/python/kqcircuits/defaults.py index b8073403a..e34b4cb14 100644 --- a/klayout_package/python/kqcircuits/defaults.py +++ b/klayout_package/python/kqcircuits/defaults.py @@ -54,7 +54,7 @@ TMP_PATH.mkdir(parents=True, exist_ok=True) # TODO move elsewhere? -ANSYS_EXECUTABLE = find_ansys_executable(r"%PROGRAMFILES%\AnsysEM\v241\Win64\ansysedt.exe") +ANSYS_EXECUTABLE = find_ansys_executable(r"%PROGRAMFILES%\AnsysEM\AnsysEM21.2\Win64\ansysedt.exe") ANSYS_SCRIPT_PATHS = [ SCRIPTS_PATH.joinpath("simulations").joinpath("ansys"), SCRIPTS_PATH.joinpath("simulations").joinpath("post_process"), diff --git a/klayout_package/python/kqcircuits/junctions/__init__.py b/klayout_package/python/kqcircuits/junctions/__init__.py index 946420e96..4877b09d0 100644 --- a/klayout_package/python/kqcircuits/junctions/__init__.py +++ b/klayout_package/python/kqcircuits/junctions/__init__.py @@ -31,6 +31,7 @@ "No Squid", "Manhattan", "Manhattan Single Junction", + 'Manhattan Single Junction Centered', #'QCD1', "Sim", ] diff --git a/klayout_package/python/scripts/calculate_q_from_s.py b/klayout_package/python/scripts/calculate_q_from_s.py new file mode 100644 index 000000000..1b4fd03b7 --- /dev/null +++ b/klayout_package/python/scripts/calculate_q_from_s.py @@ -0,0 +1,53 @@ +# This code is part of KQCircuits +# Copyright (C) 2024 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). +""" +Calculates Q-factors from S-parameter results. +For each port creates network where all other ports are terminated by resistor. +The Q-factor of the port is the calculated from y-parameters by imag(y) / real(y), +""" +import json +import os +import sys +import skrf + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +from post_process_helpers import read_snp_network # pylint: disable=wrong-import-position, no-name-in-module + +# Find data files +path = os.path.curdir +result_files = [f for f in os.listdir(path) if f[:-2].endswith("_project_SMatrix.s")] +for result_file in result_files: + snp_network, z0s = read_snp_network(result_file) + freq = skrf.Frequency.from_f(snp_network.f) + + output_data = {"frequencies": list(freq.f)} + for i, z0 in enumerate(z0s): + port = skrf.Circuit.Port(freq, "port", z0=z0) + connections = [[(snp_network, i), (port, 0)]] + for j, z0j in enumerate(z0s): + if j != i: + resistor = skrf.Circuit.SeriesImpedance(freq, z0j, f"res{j}]") + gnd = skrf.Circuit.Ground(freq, f"gnd{j}") + connections += [[(snp_network, j), (resistor, 0)], [(resistor, 1), (gnd, 0)]] + circ = skrf.Circuit(connections) + q = [y[0][0].imag / y[0][0].real for y in circ.network.y] + if any(v > 0 for v in q): # ignore ports that gets invalid q values + output_data[f"Q_port{i + 1}"] = q + + output_file = result_file[:-2].replace("_project_SMatrix.s", "_q.json") + with open(output_file, "w") as f: + json.dump(output_data, f, indent=4) diff --git a/klayout_package/python/scripts/create_capacitive_pi_model.py b/klayout_package/python/scripts/create_capacitive_pi_model.py new file mode 100644 index 000000000..1f35ec3ec --- /dev/null +++ b/klayout_package/python/scripts/create_capacitive_pi_model.py @@ -0,0 +1,97 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 Ansys Electronics Desktop in order to create capacitance matrix +# output variables. +import os +import sys +import time + +import ScriptEnv + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +from util import get_enabled_setup, get_enabled_sweep # pylint: disable=wrong-import-position,no-name-in-module + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") + +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() +oBoundarySetup = oDesign.GetModule("BoundarySetup") +oOutputVariable = oDesign.GetModule("OutputVariable") + + +oDesktop.AddMessage("", "", 0, "Creating capacitive PI model for all ports (%s)" % time.asctime(time.localtime())) + +design_type = oDesign.GetDesignType() +if design_type == "HFSS": + setup = get_enabled_setup(oDesign) + if oDesign.GetSolutionType() == "HFSS Terminal Network": + sweep = get_enabled_sweep(oDesign, setup) + solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) + context = [] if sweep is None else ["Domain:=", "Sweep"] + + ports = oBoundarySetup.GetExcitations()[::2] + + for i, port_i in enumerate(ports): + for j, port_j in enumerate(ports): + # PI model admittance element + if i == j: + # admittance yii between port i and ground + expression = " + ".join(["Yt(%s,%s)" % (port_i, port_k) for port_k in ports]) + else: + # admittance yij between port i and j + expression = "-Yt(%s,%s)" % (port_i, port_j) + + oOutputVariable.CreateOutputVariable( + "yy_%s_%s" % (port_i, port_j), expression, solution, "Terminal Solution Data", [] + ) + + # Estimate capacitance vs frequency assuming capacitive elements + oOutputVariable.CreateOutputVariable( + "C_%s_%s" % (port_i, port_j), + "im(yy_%s_%s)/(2*pi*Freq)" % (port_i, port_j), + solution, + "Terminal Solution Data", + [], + ) + +elif design_type == "Q3D Extractor": + setup = get_enabled_setup(oDesign, tab="General") + nets = oBoundarySetup.GetExcitations()[::2] + net_types = oBoundarySetup.GetExcitations()[1::2] + signal_nets = [net for net, net_type in zip(nets, net_types) if net_type == "SignalNet"] + + for i, net_i in enumerate(signal_nets): + for j, net_j in enumerate(signal_nets): + if i == j: + expression = " + ".join(["C({},{})".format(net_i, net_k) for net_k in signal_nets]) + else: + expression = "-C({},{})".format(net_i, net_j) + + oOutputVariable.CreateOutputVariable( + "C_{}_{}".format(net_i, net_j), + expression, + setup + " : LastAdaptive", + "Matrix", + ["Context:=", "Original"], + ) + +# Notify the end of script +oDesktop.AddMessage("", "", 0, "The capacitive PI model created (%s)" % time.asctime(time.localtime())) diff --git a/klayout_package/python/scripts/create_reports.py b/klayout_package/python/scripts/create_reports.py new file mode 100644 index 000000000..062874f68 --- /dev/null +++ b/klayout_package/python/scripts/create_reports.py @@ -0,0 +1,179 @@ +# 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 Ansys Electronics Desktop in order to create reports. +import os +import sys +import time + +import ScriptEnv + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +# fmt: off +from util import get_enabled_setup, get_enabled_sweep, create_x_vs_y_plot, get_quantities \ + # pylint: disable=wrong-import-position,no-name-in-module +# fmt: on + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") + +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() +oBoundarySetup = oDesign.GetModule("BoundarySetup") +oOutputVariable = oDesign.GetModule("OutputVariable") +oReportSetup = oDesign.GetModule("ReportSetup") + +# Create model separately for HFSS and Q3D +oDesktop.AddMessage("", "", 0, "Creating reports (%s)" % time.asctime(time.localtime())) + +design_type = oDesign.GetDesignType() +if design_type == "HFSS": + setup = get_enabled_setup(oDesign) + if oDesign.GetSolutionType() == "HFSS Terminal Network": + sweep = get_enabled_sweep(oDesign, setup) + solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) + context = [] if sweep is None else ["Domain:=", "Sweep"] + ports = oBoundarySetup.GetExcitations()[::2] + + # Create capacitance vs frequency report + unique_elements_c = [ + "C_%s_%s" % (port_i, ports[j]) for i, port_i in enumerate(ports) for j in range(i, len(ports)) + ] # The unique elements (half matrix), used for plotting C-matrix + unique_output_c = [e for e in unique_elements_c if e in oOutputVariable.GetOutputVariables()] + if unique_output_c: + create_x_vs_y_plot( + oReportSetup, + "Capacitance vs Frequency", + "Terminal Solution Data", + solution, + context, + ["Freq:=", ["All"]], + "Freq", + "C [fF]", + unique_output_c, + ) + + # Create S vs frequency and S convergence reports + unique_elements_s = [ + "St(%s,%s)" % (port_i, ports[j]) for i, port_i in enumerate(ports) for j in range(i, len(ports)) + ] # The unique elements (half matrix), used for plotting S-matrix + unique_output_s = [ + "dB(%s)" % e + for e in unique_elements_s + if e in get_quantities(oReportSetup, "Terminal Solution Data", solution, context, "Terminal S Parameter") + ] + if unique_output_s: + create_x_vs_y_plot( + oReportSetup, + "S vs Frequency", + "Terminal Solution Data", + solution, + context, + ["Freq:=", ["All"]], + "Freq", + "S [dB]", + unique_output_s, + ) + create_x_vs_y_plot( + oReportSetup, + "Solution Convergence", + "Terminal Solution Data", + setup + " : Adaptivepass", + [], + ["Pass:=", ["All"], "Freq:=", ["All"]], + "Pass", + "S [dB]", + unique_output_s, + ) + + elif oDesign.GetSolutionType() == "Eigenmode": + # Create eigenmode convergence report + solution = setup + " : AdaptivePass" + modes = get_quantities(oReportSetup, "Eigenmode Parameters", solution, [], "Eigen Modes") + create_x_vs_y_plot( + oReportSetup, + "Solution Convergence", + "Eigenmode Parameters", + solution, + [], + ["Pass:=", ["All"]], + "Pass", + "Frequency [Hz]", + ["re({})".format(m) for m in modes], + ) + + # Create integral reports + solution = setup + " : LastAdaptive" + integrals = get_quantities(oReportSetup, "Fields", solution, [], "Calculator Expressions") + energies = [e for e in integrals if e.startswith("E_") or e.startswith("Ez_") or e.startswith("Exy_")] + if energies: + create_x_vs_y_plot( + oReportSetup, "Energy Integrals", "Fields", solution, [], ["Phase:=", ["0deg"]], "Phase", "E [J]", energies + ) + fluxes = [e for e in integrals if e.startswith("flux_")] + if fluxes: + create_x_vs_y_plot( + oReportSetup, + "Magnetic Fluxes", + "Fields", + solution, + [], + ["Phase:=", ["0deg"]], + "Phase", + "Magnetic flux quanta", + fluxes, + ) + +elif design_type == "Q3D Extractor": + setup = get_enabled_setup(oDesign, tab="General") + nets = oBoundarySetup.GetExcitations()[::2] + net_types = oBoundarySetup.GetExcitations()[1::2] + signal_nets = [net for net, net_type in zip(nets, net_types) if net_type == "SignalNet"] + + # Create capacitance vs frequency and capacitance convergence reports + unique_elements_c = [ + "C_%s_%s" % (net_i, signal_nets[j]) for i, net_i in enumerate(signal_nets) for j in range(i, len(signal_nets)) + ] # The unique elements (half matrix), used for plotting + unique_output_c = [e for e in unique_elements_c if e in oOutputVariable.GetOutputVariables()] + if unique_output_c: + create_x_vs_y_plot( + oReportSetup, + "Capacitance vs Frequency", + "Matrix", + setup + " : LastAdaptive", + ["Context:=", "Original"], + ["Freq:=", ["All"]], + "Freq", + "C", + unique_output_c, + ) + create_x_vs_y_plot( + oReportSetup, + "Solution Convergence", + "Matrix", + setup + " : AdaptivePass", + ["Context:=", "Original"], + ["Pass:=", ["All"], "Freq:=", ["All"]], + "Pass", + "C", + unique_output_c, + ) + +# Notify the end of script +oDesktop.AddMessage("", "", 0, "Reports created (%s)" % time.asctime(time.localtime())) diff --git a/klayout_package/python/scripts/delete_all_output_variables.py b/klayout_package/python/scripts/delete_all_output_variables.py new file mode 100644 index 000000000..1c2e2719b --- /dev/null +++ b/klayout_package/python/scripts/delete_all_output_variables.py @@ -0,0 +1,35 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 import and run the simulation +import time +import ScriptEnv + +## SET UP ENVIRONMENT +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") + +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() +oOutputVariable = oDesign.GetModule("OutputVariable") + +outputvars = oOutputVariable.GetOutputVariables() +for x in outputvars: + oOutputVariable.DeleteOutputVariable(x) + +oDesktop.AddMessage("", "", 0, "Deleted %d output variables (%s)" % (len(outputvars), time.asctime(time.localtime()))) diff --git a/klayout_package/python/scripts/export_snp_no_deembed.py b/klayout_package/python/scripts/export_snp_no_deembed.py new file mode 100644 index 000000000..155a388e7 --- /dev/null +++ b/klayout_package/python/scripts/export_snp_no_deembed.py @@ -0,0 +1,97 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 Ansys Electronics Desktop +# in order to export non de-embedded S-matrix network data. +import os +import ScriptEnv + +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() + +path = oProject.GetPath() +basename = oProject.GetName() + +oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:HfssTab", + ["NAME:PropServers", "BoundarySetup:1"], + [ + "NAME:ChangedProps", + [ + "NAME:Deembed Dist", + "Value:=", + "0um", + "NAME:Renorm All Terminals", + "Value:=", + False, + "NAME:Deembed", + "Value:=", + False, + ], + ], + ], + ] +) +oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:HfssTab", + ["NAME:PropServers", "BoundarySetup:2"], + [ + "NAME:ChangedProps", + [ + "NAME:Deembed Dist", + "Value:=", + "0um", + "NAME:Renorm All Terminals", + "Value:=", + False, + "NAME:Deembed", + "Value:=", + False, + ], + ], + ], + ] +) + +oModule = oDesign.GetModule("Solutions") +oModule.ExportNetworkData( + "", + ["Setup1:Sweep"], + 3, + os.path.join(path, basename + "_SMatrix_nodeembed.s2p"), + ["All"], + False, + 50, + "S", + -1, + 0, + 15, + True, + True, + True, +) + +oProject.Save() diff --git a/klayout_package/python/scripts/export_solution_data.py b/klayout_package/python/scripts/export_solution_data.py new file mode 100644 index 000000000..cbeed86c4 --- /dev/null +++ b/klayout_package/python/scripts/export_solution_data.py @@ -0,0 +1,217 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 solution data +import time +import os +import sys +import json +import ScriptEnv + +# TODO: Figure out how to set the python path for the HFSS internal IronPython +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +# fmt: off +from util import get_enabled_setup, get_enabled_sweep, get_solution_data, get_quantities, \ + ComplexEncoder # pylint: disable=wrong-import-position,no-name-in-module +# fmt on + + +def save_capacitance_matrix(file_name, c_matrix, detail=""): + """Save capacitance matrix in readable format.""" + with open(file_name, "w") as out_file: + out_file.write( + "Capacitance matrix" + + detail + + ":\n" + + "\n".join(["\t".join(["%8.3f" % (item * 1e15) for item in row]) for row in c_matrix]) + + "\n" + ) + + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") +oDesktop.AddMessage("", "", 0, "Exporting solution data (%s)" % time.asctime(time.localtime())) + +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() +oSolutions = oDesign.GetModule("Solutions") +oBoundarySetup = oDesign.GetModule("BoundarySetup") +oOutputVariable = oDesign.GetModule("OutputVariable") +oReportSetup = oDesign.GetModule("ReportSetup") + +# Set file names +path = oProject.GetPath() +basename = oProject.GetName() +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") +flux_filename = os.path.join(path, basename + "_flux.csv") + +# Export solution data separately for HFSS and Q3D +design_type = oDesign.GetDesignType() +if design_type == "HFSS": + setup = get_enabled_setup(oDesign) + if oDesign.GetSolutionType() == "HFSS Terminal Network": + sweep = get_enabled_sweep(oDesign, setup) + ports = oBoundarySetup.GetExcitations()[::2] + + # Capacitance matrix export + output_variables = oOutputVariable.GetOutputVariables() + if all("C_{}_{}".format(i, j) in output_variables for j in ports for i in ports): + # find lowest-frequency solution (of LastAdaptive) for capacitance extraction + solution = setup + " : LastAdaptive" + context = [] + res = oReportSetup.GetSolutionDataPerVariation( + "Terminal Solution Data", solution, context, ["Freq:=", ["All"]], [""] + )[0] + freq = "{}{}".format(res.GetSweepValues("Freq", False)[0], res.GetSweepUnits("Freq")) + families = ["Freq:=", [freq]] + + # Get solution data + yyMatrix = [ + [ + get_solution_data( + oReportSetup, "Terminal Solution Data", solution, context, families, "yy_{}_{}".format(i, j) + )[0] + for j in ports + ] + for i in ports + ] + CMatrix = [ + [ + get_solution_data( + oReportSetup, "Terminal Solution Data", solution, context, families, "C_{}_{}".format(i, j) + )[0] + for j in ports + ] + for 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(i, j) for i in ports for j in ports], + ), + "Cdata": get_solution_data( + oReportSetup, + "Terminal Solution Data", + solution, + context, + families, + ["C_{}_{}".format(i, j) for i in ports for j in ports], + ), + }, + outfile, + cls=ComplexEncoder, + indent=4, + ) + + # S-parameter export + solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) + context = [] if sweep is None else ["Domain:=", "Sweep"] + if get_quantities(oReportSetup, "Terminal Solution Data", solution, context, "Terminal S Parameter"): + file_name = os.path.join(path, basename + "_SMatrix.s{}p".format(len(ports))) + oSolutions.ExportNetworkData( + "", # Design variation key. Pass empty string for the current nominal variation. + solution, # Selected solutions + 3, # File format: 2 = Tab delimited (.tab), 3 = Touchstone (.sNp), 4 = CitiFile (.cit), 7 = Matlab (.m) + file_name, # Full path to the file to write out + ["All"], # The frequencies to export. Use ["All"] to export all available frequencies + False, # Specifies whether to renormalize the data before export + 50, # Real impedance value in ohms, for renormalization + "S", # The matrix to export. "S", "Y", or "Z" + -1, # The pass to export. Specifying -1 gets all available passes + 0, # Format of complex numbers: 0 = magnitude/phase, 1 = real/imag, 2 = dB/phase + 15, # Touchstone number of digits precision + True, # Specifies whether to use export frequencies + False, # Specifies whether to include Gamma and Impedance comments + True, # Specifies whether to support non-standard Touchstone extensions for mixed reference impedance + ) + + elif oDesign.GetSolutionType() == "Eigenmode": + solution = setup + " : LastAdaptive" + oSolutions.ExportEigenmodes(solution, oSolutions.ListVariations(solution)[0], eig_filename) + + # Save energy integrals + report_names = oReportSetup.GetAllReportNames() + if "Energy Integrals" in report_names: + oReportSetup.ExportToFile("Energy Integrals", energy_filename, False) + + # Save magnetic flux integrals + report_names = oReportSetup.GetAllReportNames() + if "Magnetic Fluxes" in report_names: + oReportSetup.ExportToFile("Magnetic Fluxes", flux_filename, False) + +elif design_type == "Q3D Extractor": + setup = get_enabled_setup(oDesign, tab="General") + solution = setup + " : LastAdaptive" + context = ["Context:=", "Original"] + + # Get list of signal nets + nets = oBoundarySetup.GetExcitations()[::2] + net_types = oBoundarySetup.GetExcitations()[1::2] + signal_nets = [net for net, net_type in zip(nets, net_types) if net_type == "SignalNet"] + + # Get solution data + CMatrix = [ + [ + get_solution_data(oReportSetup, "Matrix", solution, context, [], "C_{}_{}".format(net_i, net_j))[0] + for net_j in signal_nets + ] + for net_i in signal_nets + ] + + # Save capacitance matrix into readable format + save_capacitance_matrix(matrix_filename, CMatrix) + + # Save results in json format + with open(json_filename, "w") as outfile: + json.dump( + { + "CMatrix": CMatrix, + "Cdata": get_solution_data( + oReportSetup, + "Matrix", + solution, + context, + [], + ["C_{}_{}".format(net_i, net_j) for net_j in signal_nets for net_i in signal_nets], + ), + }, + outfile, + cls=ComplexEncoder, + indent=4, + ) + +# Notify the end of script +oDesktop.AddMessage("", "", 0, "Done exporting solution data (%s)" % time.asctime(time.localtime())) diff --git a/klayout_package/python/scripts/export_tdr.py b/klayout_package/python/scripts/export_tdr.py new file mode 100644 index 000000000..7436deb4d --- /dev/null +++ b/klayout_package/python/scripts/export_tdr.py @@ -0,0 +1,146 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 Ansys Electronics Desktop +# in order to create Time Domain Reflectometry and export it. +import os +import sys +import time + +import ScriptEnv + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +from util import get_enabled_setup, get_enabled_sweep # pylint: disable=wrong-import-position,no-name-in-module + + +def create_z_vs_time_plot(report_setup, report_type, solution_name, context_array, y_label, y_components): + report_setup.CreateReport( + "Time Domain Reflectometry", + report_type, + "Rectangular Plot", + solution_name, + context_array, + ["Time:=", ["All"]], + ["X Component:=", "Time", "Y Component:=", y_components], + [], + ) + report_setup.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Legend", + ["NAME:PropServers", "Time Domain Reflectometry:Legend"], + [ + "NAME:ChangedProps", + ["NAME:Show Variation Key", "Value:=", False], + ["NAME:Show Solution Name", "Value:=", False], + ["NAME:DockMode", "Value:=", "Dock Right"], + ], + ], + [ + "NAME:Axis", + ["NAME:PropServers", "Time Domain Reflectometry:AxisY1"], + ["NAME:ChangedProps", ["NAME:Specify Name", "Value:=", True], ["NAME:Name", "Value:=", y_label]], + ], + ] + ) + report_setup.ExportToFile("Time Domain Reflectometry", csv_filename) + + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") +oDesktop.AddMessage("", "", 0, "Plotting TDR for all ports (%s)" % time.asctime(time.localtime())) + +oDesktop.RestoreWindow() +oProject = oDesktop.GetActiveProject() +oDesign = oProject.GetActiveDesign() +oBoundarySetup = oDesign.GetModule("BoundarySetup") +oReportSetup = oDesign.GetModule("ReportSetup") + +# Set file name +path = oProject.GetPath() +basename = oProject.GetName() +csv_filename = os.path.join(path, basename + "_TDR.csv") + +# No de-embedding +oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:HfssTab", + ["NAME:PropServers", "BoundarySetup:1"], + ["NAME:ChangedProps", ["NAME:Renorm All Terminals", "Value:=", False, "NAME:Deembed", "Value:=", False]], + ], + ] +) +oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:HfssTab", + ["NAME:PropServers", "BoundarySetup:2"], + ["NAME:ChangedProps", ["NAME:Renorm All Terminals", "Value:=", False, "NAME:Deembed", "Value:=", False]], + ], + ] +) + +# Create model only for HFSS +design_type = oDesign.GetDesignType() +if design_type == "HFSS": + setup = get_enabled_setup(oDesign) + if oDesign.GetSolutionType() == "HFSS Terminal Network": + sweep = get_enabled_sweep(oDesign, setup) + solution = setup + (" : LastAdaptive" if sweep is None else " : " + sweep) + context = ( + [] + if sweep is None + else [ + "Domain:=", + "Time", + "HoldTime:=", + 1, + "RiseTime:=", + 10e-12, # picoseconds + "StepTime:=", + 2e-12, + "Step:=", + True, + "WindowWidth:=", + 1, + "WindowType:=", + 4, # Hanning + "KaiserParameter:=", + 4.44659081257122e-323, + "MaximumTime:=", + 200e-12, + ] + ) + + ports = oBoundarySetup.GetExcitations()[::2] + + create_z_vs_time_plot( + oReportSetup, + "Terminal Solution Data", + solution, + context, + "Z [Ohm]", + ["TDRZt(%s)" % (port) for port in ports], + ) + +# Notify the end of script +oDesktop.AddMessage("", "", 0, "TDR created (%s)" % time.asctime(time.localtime())) diff --git a/klayout_package/python/scripts/import_and_simulate.py b/klayout_package/python/scripts/import_and_simulate.py new file mode 100644 index 000000000..8b50f20e4 --- /dev/null +++ b/klayout_package/python/scripts/import_and_simulate.py @@ -0,0 +1,95 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 import and run the simulation +import os +import json +import sys +import platform +import ScriptEnv + + +def write_simulation_machine_versions_file(oDesktop): + """ + Writes file SIMULATION_MACHINE_VERSIONS into given file path. + """ + versions = {} + versions["platform"] = platform.platform() + versions["python"] = sys.version_info + versions["Ansys ElectronicsDesktop"] = oDesktop.GetVersion() + + with open("SIMULATION_MACHINE_VERSIONS.json", "w") as file: + json.dump(versions, file) + + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") +scriptpath = os.path.dirname(__file__) + +jsonfile = ScriptArgument +path = os.path.abspath(os.path.dirname(jsonfile)) +basename = os.path.splitext(os.path.basename(jsonfile))[0] + +# Read simulation_flags settings from .json +with open(jsonfile, "r") as fp: + data = json.load(fp) +simulation_flags = data["simulation_flags"] + +# Create project and geometry +oDesktop.RunScriptWithArguments(os.path.join(scriptpath, "import_simulation_geometry.py"), jsonfile) + +# Set up capacitive PI model +if data.get("ansys_tool", "hfss") == "q3d" or data.get("hfss_capacitance_export", False): + oDesktop.RunScript(os.path.join(scriptpath, "create_capacitive_pi_model.py")) + +# Create reports +oDesktop.RunScript(os.path.join(scriptpath, "create_reports.py")) +oDesktop.TileWindows(0) + +# Save project +oProject = oDesktop.GetActiveProject() +oProject.SaveAs(os.path.join(path, basename + "_project.aedt"), True) + +# only import geometry for pyEPR simulations +if "pyepr" in simulation_flags: + sys.exit(0) + +# Run +oDesign = oProject.GetActiveDesign() +oDesign.AnalyzeAll() + +# Save solution +oProject.Save() + +# Export results +oDesktop.RunScript(os.path.join(scriptpath, "export_solution_data.py")) + + +####################### +# Optional processing # +####################### + +# Time Domain Reflectometry +if "tdr" in simulation_flags: + oDesktop.RunScript(os.path.join(scriptpath, "export_tdr.py")) + +# Export Touchstone S-matrix (.sNp) w/o de-embedding +if "snp_no_deembed" in simulation_flags: + oDesktop.RunScript(os.path.join(scriptpath, "export_snp_no_deembed.py")) + +write_simulation_machine_versions_file(oDesktop) diff --git a/klayout_package/python/scripts/import_simulation_geometry.py b/klayout_package/python/scripts/import_simulation_geometry.py new file mode 100644 index 000000000..4c915628d --- /dev/null +++ b/klayout_package/python/scripts/import_simulation_geometry.py @@ -0,0 +1,835 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 Ansys Electronic Desktop in order to import and run the simulation +from math import cos, pi +import time +import os +import sys +import json +import ScriptEnv + +# 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")) +# fmt: off +from geometry import create_box, create_rectangle, create_polygon, thicken_sheet, set_material, add_layer, subtract, \ + move_vertically, delete, objects_from_sheet_edges, add_material, set_color # pylint: disable=wrong-import-position +from field_calculation import add_squared_electric_field_expression, add_energy_integral_expression, \ + add_magnetic_flux_integral_expression # pylint: disable=wrong-import-position +# fmt: on + +# Set up environment +ScriptEnv.Initialize("Ansoft.ElectronicsDesktop") +oDesktop.AddMessage("", "", 0, "Starting import script (%s)" % time.asctime(time.localtime())) + +# Import metadata (bounding box and port information) +jsonfile = ScriptArgument +path = os.path.dirname(jsonfile) + +with open(jsonfile, "r") as fjsonfile: + data = json.load(fjsonfile) + +ansys_tool = data.get("ansys_tool", "hfss") + +simulation_flags = data["simulation_flags"] +gds_file = data["gds_file"] +units = data.get("units", "um") +material_dict = data.get("material_dict", dict()) +box = data["box"] + +ansys_project_template = data.get("ansys_project_template", "") +vertical_over_etching = data.get("vertical_over_etching", 0) +mesh_size = data.get("mesh_size", dict()) + +# Create project +oDesktop.RestoreWindow() +oProject = oDesktop.NewProject() +oDefinitionManager = oProject.GetDefinitionManager() + +hfss_tools = {"hfss", "current", "voltage", "eigenmode"} + +design_name = ansys_tool.capitalize() + "Design" +if ansys_tool == "eigenmode": + oProject.InsertDesign("HFSS", design_name, "Eigenmode", "") + oDesign = oProject.SetActiveDesign(design_name) +elif ansys_tool in hfss_tools: + oProject.InsertDesign("HFSS", design_name, "HFSS Terminal Network", "") + oDesign = oProject.SetActiveDesign(design_name) +elif ansys_tool == "q3d": + oProject.InsertDesign("Q3D Extractor", design_name, "", "") + oDesign = oProject.SetActiveDesign(design_name) + +oEditor = oDesign.SetActiveEditor("3D Modeler") +oBoundarySetup = oDesign.GetModule("BoundarySetup") +oAnalysisSetup = oDesign.GetModule("AnalysisSetup") +oOutputVariable = oDesign.GetModule("OutputVariable") +oSolutions = oDesign.GetModule("Solutions") +oReportSetup = oDesign.GetModule("ReportSetup") + + +# Define colors +def color_by_material(material, is_sheet=False): + if material == "pec": + return 240, 120, 240, 0.5 + n = 0.3 * (material_dict.get(material, dict()).get("permittivity", 1.0) - 1.0) + alpha = 0.93 ** (2 * n if is_sheet else n) + return tuple(int(100 + 80 * c) for c in [cos(n - pi / 3), cos(n + pi), cos(n + pi / 3)]) + (alpha,) + + +# Set units +oEditor.SetModelUnits(["NAME:Units Parameter", "Units:=", units, "Rescale:=", False]) + +# Add materials +for name, params in material_dict.items(): + add_material(oDefinitionManager, name, **params) + +# Import GDSII geometry +layers = data.get("layers", dict()) +# 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"] +order = 0 +for lname, ldata in layers.items(): + if "layer" in ldata: + add_layer(layer_map, order_map, ldata["layer"], lname, order) + order += 1 + +oEditor.ImportGDSII( + [ + "NAME:options", + "FileName:=", + os.path.join(path, gds_file), + "FlattenHierarchy:=", + True, + "ImportMethod:=", + 1, + layer_map, + "OrderMap:=", + order_map, + ["NAME:Structs", ["NAME:GDSIIStruct", "ImportStruct:=", True, "CreateNewCell:=", True, "StructName:=", "SIM1"]], + ] +) + +# Create 3D geometry +objects = {} +pec_sheets = [] +for lname, ldata in layers.items(): + z = ldata.get("z", 0.0) + thickness = ldata.get("thickness", 0.0) + if "layer" in ldata: + # Get imported objects + objects[lname] = oEditor.GetMatchedObjectName(lname + "_*") + move_vertically(oEditor, objects[lname], z, units) + + # Create pec-sheets from edges + edge_material = ldata.get("edge_material", None) + if edge_material == "pec" and thickness != 0.0: + pec_sheets += objects_from_sheet_edges(oEditor, objects[lname], thickness, units) + + thicken_sheet(oEditor, objects[lname], thickness, units) + else: + # Create object covering full box + objects[lname] = [lname] + if thickness != 0.0: + create_box( + oEditor, + lname, + box["p1"]["x"], + box["p1"]["y"], + z, + box["p2"]["x"] - box["p1"]["x"], + box["p2"]["y"] - box["p1"]["y"], + thickness, + units, + ) + else: + create_rectangle( + oEditor, + lname, + box["p1"]["x"], + box["p1"]["y"], + z, + box["p2"]["x"] - box["p1"]["x"], + box["p2"]["y"] - box["p1"]["y"], + "Z", + units, + ) + + # Set material + material = ldata.get("material", None) + if thickness != 0.0: + # Solve Inside parameter must be set in hfss_tools simulations to avoid warnings. + # Solve Inside doesn't exist in 'q3d', so we use None to ignore the parameter. + solve_inside = material != "pec" if ansys_tool in hfss_tools else None + set_material(oEditor, objects[lname], material, solve_inside) + set_color(oEditor, objects[lname], *color_by_material(material)) + elif material == "pec": + pec_sheets += objects[lname] + else: + set_color(oEditor, objects[lname], *color_by_material(material, True)) + if lname not in mesh_size: + set_material(oEditor, objects[lname], None, None) # set sheet as non-model + +# Assign perfect electric conductor to metal sheets +if pec_sheets: + set_color(oEditor, pec_sheets, *color_by_material("pec", True)) + if ansys_tool in hfss_tools: + oBoundarySetup.AssignPerfectE(["NAME:PerfE1", "Objects:=", pec_sheets, "InfGroundPlane:=", False]) + elif ansys_tool == "q3d": + oBoundarySetup.AssignThinConductor( + [ + "NAME:ThinCond1", + "Objects:=", + pec_sheets, + "Material:=", + "pec", + "Thickness:=", + "1nm", # thickness does not matter when material is pec + ] + ) + + +# Subtract objects from others +for lname, ldata in layers.items(): + if "subtract" in ldata: + subtract(oEditor, objects[lname], [o for n in ldata["subtract"] for o in objects[n]], True) + + +# Create ports or nets +signal_objects = [o for n, v in objects.items() if "_signal" in n for o in v] +ground_objects = [o for n, v in objects.items() if "_ground" in n for o in v] +if ansys_tool in hfss_tools: + ports = sorted(data["ports"], key=lambda k: k["number"]) + for port in ports: + is_wave_port = port["type"] == "EdgePort" + if not is_wave_port or not ansys_project_template: + if "polygon" not in port: + continue + + polyname = "Port%d" % port["number"] + + # Create polygon spanning the two edges + create_polygon(oEditor, polyname, [list(p) for p in port["polygon"]], units) + set_color(oEditor, [polyname], 240, 180, 180, 0.8) + + if ansys_tool == "hfss": + oBoundarySetup.AutoIdentifyPorts( + ["NAME:Faces", int(oEditor.GetFaceIDs(polyname)[0])], + is_wave_port, + ["NAME:ReferenceConductors"] + ground_objects, + str(port["number"]), + False, + ) + + renorm = port.get("renormalization", None) + oBoundarySetup.SetTerminalReferenceImpedances( + "" if renorm is None else "{}ohm".format(renorm), str(port["number"]), renorm is not None + ) + + deembed_len = port.get("deembed_len", None) + if deembed_len is not None: + oBoundarySetup.EditWavePort( + str(port["number"]), + [ + "Name:%d" % port["number"], + "DoDeembed:=", + True, + "DeembedDist:=", + "%f%s" % (deembed_len, units), + ], + ) + + elif ansys_tool == "current": + oBoundarySetup.AssignCurrent( + [ + "NAME:{}".format(polyname), + "Objects:=", + [polyname], + [ + "NAME:Direction", + "Coordinate System:=", + "Global", + "Start:=", + ["%.32e%s" % (p, units) for p in port["signal_location"]], + "End:=", + ["%.32e%s" % (p, units) for p in port["ground_location"]], + ], + ] + ) + elif ansys_tool == "voltage": + oBoundarySetup.AssignVoltage( + [ + "NAME:{}".format(polyname), + "Objects:=", + [polyname], + [ + "NAME:Direction", + "Coordinate System:=", + "Global", + "Start:=", + ["%.32e%s" % (p, units) for p in port["signal_location"]], + "End:=", + ["%.32e%s" % (p, units) for p in port["ground_location"]], + ], + ] + ) + + elif port["junction"] and ansys_tool == "eigenmode": + # add junction inductance variable + oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:LocalVariableTab", + ["NAME:PropServers", "LocalVariables"], + [ + "NAME:NewProps", + [ + "NAME:Lj_%d" % port["number"], + "PropType:=", + "VariableProp", + "UserDef:=", + True, + "Value:=", + "%.32eH" % port["inductance"], + ], # use best float precision + ], + ], + ] + ) + # add junction capacitance variable + oDesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:LocalVariableTab", + ["NAME:PropServers", "LocalVariables"], + [ + "NAME:NewProps", + [ + "NAME:Cj_%d" % port["number"], + "PropType:=", + "VariableProp", + "UserDef:=", + True, + "Value:=", + "%.32efarad" % port["capacitance"], + ], + ], + ], + ] + ) + + # Turn junctions to lumped RLC + current_start = ["%.32e%s" % (p, units) for p in port["signal_location"]] + current_end = ["%.32e%s" % (p, units) for p in port["ground_location"]] + oBoundarySetup.AssignLumpedRLC( + [ + "NAME:LumpRLC_jj_%d" % port["number"], + "Objects:=", + [polyname], + [ + "NAME:CurrentLine", # set direction of current across junction + "Coordinate System:=", + "Global", + "Start:=", + current_start, + "End:=", + current_end, + ], + "RLC Type:=", + "Parallel", + "UseResist:=", + False, + "UseInduct:=", + True, + "Inductance:=", + "Lj_%d" % port["number"], + "UseCap:=", + True, + "Capacitance:=", + "Cj_%d" % port["number"], + "Faces:=", + [int(oEditor.GetFaceIDs(polyname)[0])], + ] + ) + + if "pyepr" in simulation_flags: + # add polyline across junction for voltage across the junction + oEditor.CreatePolyline( + [ + "NAME:PolylineParameters", + "IsPolylineCovered:=", + True, + "IsPolylineClosed:=", + False, + [ + "NAME:PolylinePoints", + [ + "NAME:PLPoint", + "X:=", + current_start[0], + "Y:=", + current_start[1], + "Z:=", + current_start[2], + ], + ["NAME:PLPoint", "X:=", current_end[0], "Y:=", current_end[1], "Z:=", current_end[2]], + ], + [ + "NAME:PolylineSegments", + ["NAME:PLSegment", "SegmentType:=", "Line", "StartIndex:=", 0, "NoOfPoints:=", 2], + ], + [ + "NAME:PolylineXSection", + "XSectionType:=", + "None", + "XSectionOrient:=", + "Auto", + "XSectionWidth:=", + "0" + units, + "XSectionTopWidth:=", + "0" + units, + "XSectionHeight:=", + "0" + units, + "XSectionNumSegments:=", + "0", + "XSectionBendType:=", + "Corner", + ], + ], + [ + "NAME:Attributes", + "Name:=", + "Junction%d" % port["number"], + "Flags:=", + "", + "Color:=", + "(143 175 143)", + "Transparency:=", + 0.4, + "PartCoordinateSystem:=", + "Global", + "UDMId:=", + "", + "MaterialValue:=", + '"vacuum"', + "SurfaceMaterialValue:=", + '""', + "SolveInside:=", + True, + "ShellElement:=", + False, + "ShellElementThickness:=", + "0" + units, + "IsMaterialEditable:=", + True, + "UseMaterialAppearance:=", + False, + "IsLightweight:=", + False, + ], + ) + + oEditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Geometry3DAttributeTab", + ["NAME:PropServers", "Junction%d" % port["number"]], + ["NAME:ChangedProps", ["NAME:Show Direction", "Value:=", True]], + ], + ] + ) + + +elif ansys_tool == "q3d": + port_objects = [] # signal objects to be assigned as SignalNets + ports = sorted(data["ports"], key=lambda k: k["number"]) + for port in ports: + signal_location = port["signal_location"] + if "ground_location" in port: + # Use 1e-2 safe margin to ensure that signal_location is inside the signal polygon: + signal_location = [x + 1e-2 * (x - y) for x, y in zip(signal_location, port["ground_location"])] + port_object = oEditor.GetBodyNamesByPosition( + [ + "NAME:Parameters", + "XPosition:=", + str(signal_location[0]) + units, + "YPosition:=", + str(signal_location[1]) + units, + "ZPosition:=", + str(signal_location[2]) + units, + ] + ) + + port_object = [o for o in port_object if "_signal" in o] + + if len(port_object) == 1 and port_object[0] not in port_objects and port_object[0] in signal_objects: + port_objects.append(port_object[0]) + + if not port_objects: + port_objects = signal_objects # port_objects is empty -> assign all signals as SignalNets without sorting + + for i, signal_object in enumerate(port_objects): + oBoundarySetup.AssignSignalNet(["NAME:Net{}".format(i + 1), "Objects:=", [signal_object]]) + for i, floating_object in enumerate([obj for obj in signal_objects if obj not in port_objects]): + oBoundarySetup.AssignFloatingNet(["NAME:Floating{}".format(i + 1), "Objects:=", [floating_object]]) + for i, ground_object in enumerate(ground_objects): + oBoundarySetup.AssignGroundNet(["NAME:Ground{}".format(i + 1), "Objects:=", [ground_object]]) + oBoundarySetup.AutoIdentifyNets() # Combine Nets by conductor connections. Order: GroundNet, SignalNet, FloatingNet + + +# Add field calculations +if data.get("integrate_energies", False) and ansys_tool in hfss_tools: + # Create term for squared E fields + oModule = oDesign.GetModule("FieldsReporter") + add_squared_electric_field_expression(oModule, "Esq", "Mag") + add_squared_electric_field_expression(oModule, "Ezsq", "ScalarZ") + + # Create energy integral terms for each object + 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) + if thickness == 0.0: + add_energy_integral_expression(oModule, "Ez_{}".format(lname), objects[lname], "Ezsq", 2, epsilon_0, "") + add_energy_integral_expression( + oModule, "Exy_{}".format(lname), objects[lname], "Esq", 2, epsilon_0, "Ez_{}".format(lname) + ) + elif material is not None: + epsilon = epsilon_0 * material_dict.get(material, {}).get("permittivity", 1.0) + add_energy_integral_expression(oModule, "E_{}".format(lname), objects[lname], "Esq", 3, epsilon, "") + +if data.get("integrate_magnetic_flux", False) and ansys_tool in hfss_tools: + oModule = oDesign.GetModule("FieldsReporter") + for lname, ldata in layers.items(): + if ldata.get("thickness", 0.0) != 0.0 or ldata.get("material", None) == "pec": + continue + + add_magnetic_flux_integral_expression(oModule, "flux_{}".format(lname), objects[lname]) + +# 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:mesh_size_{}".format(mesh_layer), + "RefineInside:=", + layers.get(mesh_layer, dict()).get("thickness", 0.0) != 0.0, + "Enabled:=", + True, + "Objects:=", + mesh_objects, + "RestrictElem:=", + False, + "RestrictLength:=", + True, + "MaxLength:=", + str(mesh_length) + units, + ] + ) + +if not ansys_project_template: + # Insert analysis setup + setup = data["analysis_setup"] + + if ansys_tool == "hfss": + # create setup_list for analysis setup with TWO OPTIONS: multiple frequency or single frequency + multiple_frequency = isinstance(setup["frequency"], list) + setup_list = ["NAME:Setup1", "AdaptMultipleFreqs:=", multiple_frequency] + + if multiple_frequency: + max_delta_s = setup["max_delta_s"] + if not isinstance(type(max_delta_s), list): + max_delta_s = [max_delta_s] * len(setup["frequency"]) # make max_delta_s a list + maf_setup_list = ["NAME:MultipleAdaptiveFreqsSetup"] + for f, s in zip(setup["frequency"], max_delta_s): + maf_setup_list += [str(f) + setup["frequency_units"] + ":=", [s]] + setup_list += [maf_setup_list] + else: + setup_list += [ + "Frequency:=", + str(setup["frequency"]) + setup["frequency_units"], + "MaxDeltaS:=", + setup["max_delta_s"], + ] + + setup_list += [ + "MaximumPasses:=", + setup["maximum_passes"], + "MinimumPasses:=", + setup["minimum_passes"], + "MinimumConvergedPasses:=", + setup["minimum_converged_passes"], + "PercentRefinement:=", + setup["percent_refinement"], + "IsEnabled:=", + True, + ["NAME:MeshLink", "ImportMesh:=", False], + "BasisOrder:=", + 1, + "DoLambdaRefine:=", + True, + "DoMaterialLambda:=", + True, + "SetLambdaTarget:=", + False, + "Target:=", + 0.3333, + "UseMaxTetIncrease:=", + False, + "PortAccuracy:=", + 0.2, + "UseABCOnPort:=", + False, + "SetPortMinMaxTri:=", + False, + "UseDomains:=", + False, + "UseIterativeSolver:=", + False, + "SaveRadFieldsOnly:=", + False, + "SaveAnyFields:=", + True, + "IESolverType:=", + "Auto", + "LambdaTargetForIESolver:=", + 0.15, + "UseDefaultLambdaTgtForIESolver:=", + True, + ] + oAnalysisSetup.InsertSetup("HfssDriven", setup_list) + + oAnalysisSetup.InsertFrequencySweep( + "Setup1", + [ + "NAME:Sweep", + "IsEnabled:=", + setup["sweep_enabled"], + "RangeType:=", + "LinearCount", + "RangeStart:=", + str(setup["sweep_start"]) + setup["frequency_units"], + "RangeEnd:=", + str(setup["sweep_end"]) + setup["frequency_units"], + "RangeCount:=", + setup["sweep_count"], + "Type:=", + setup["sweep_type"], + "SaveFields:=", + False, + "SaveRadFields:=", + False, + "InterpTolerance:=", + 0.5, + "InterpMaxSolns:=", + 250, + "InterpMinSolns:=", + 0, + "InterpMinSubranges:=", + 1, + "ExtrapToDC:=", + setup["sweep_start"] == 0, + "MinSolvedFreq:=", + "0.01GHz", + "InterpUseS:=", + True, + "InterpUsePortImped:=", + True, + "InterpUsePropConst:=", + True, + "UseDerivativeConvergence:=", + False, + "InterpDerivTolerance:=", + 0.2, + "UseFullBasis:=", + True, + "EnforcePassivity:=", + True, + "PassivityErrorTolerance:=", + 0.0001, + "EnforceCausality:=", + False, + ], + ) + elif ansys_tool in ["current", "voltage"]: + oAnalysisSetup.InsertSetup( + "HfssDriven", + [ + "NAME:Setup1", + "SolveType:=", + "Single", + "Frequency:=", + str(setup["frequency"]) + setup["frequency_units"], + "MaxDeltaE:=", + setup["max_delta_e"], + "MaximumPasses:=", + setup["maximum_passes"], + "MinimumPasses:=", + setup["minimum_passes"], + "MinimumConvergedPasses:=", + setup["minimum_converged_passes"], + "PercentRefinement:=", + setup["percent_refinement"], + "IsEnabled:=", + True, + ["NAME:MeshLink", "ImportMesh:=", False], + "BasisOrder:=", + 1, + "DoLambdaRefine:=", + True, + "DoMaterialLambda:=", + True, + "SetLambdaTarget:=", + False, + "Target:=", + 0.3333, + "UseMaxTetIncrease:=", + False, + "DrivenSolverType:=", + "Direct Solver", + "EnhancedLowFreqAccuracy:=", + False, + "SaveRadFieldsOnly:=", + False, + "SaveAnyFields:=", + True, + "IESolverType:=", + "Auto", + "LambdaTargetForIESolver:=", + 0.15, + "UseDefaultLambdaTgtForIESolver:=", + True, + "IE Solver Accuracy:=", + "Balanced", + "InfiniteSphereSetup:=", + "", + ], + ) + elif ansys_tool == "q3d": + if isinstance(type(setup["frequency"]), list): + setup["frequency"] = setup["frequency"][0] + oDesktop.AddMessage( + "", + "", + 0, + "Multi-frequency is not supported in Q3D. Create setup with frequency " + "{}.".format(str(setup["frequency"]) + setup["frequency_units"]), + ) + + oAnalysisSetup.InsertSetup( + "Matrix", + [ + "NAME:Setup1", + "AdaptiveFreq:=", + str(setup["frequency"]) + setup["frequency_units"], + "SaveFields:=", + False, + "Enabled:=", + True, + [ + "NAME:Cap", + "MaxPass:=", + setup["maximum_passes"], + "MinPass:=", + setup["minimum_passes"], + "MinConvPass:=", + setup["minimum_converged_passes"], + "PerError:=", + setup["percent_error"], + "PerRefine:=", + setup["percent_refinement"], + "AutoIncreaseSolutionOrder:=", + True, + "SolutionOrder:=", + "High", + "Solver Type:=", + "Iterative", + ], + ], + ) + elif ansys_tool == "eigenmode": + # Create EM setup + min_freq_ghz = str(setup.get("frequency", 0.1)) + setup["frequency_units"] + + setup_list = [ + "NAME:Setup1", + "MinimumFrequency:=", + min_freq_ghz, + "NumModes:=", + setup["n_modes"], + "MaxDeltaFreq:=", + setup["max_delta_f"], + "ConvergeOnRealFreq:=", + True, + "MaximumPasses:=", + setup["maximum_passes"], + "MinimumPasses:=", + setup["minimum_passes"], + "MinimumConvergedPasses:=", + setup["minimum_converged_passes"], + "PercentRefinement:=", + setup["percent_refinement"], + "IsEnabled:=", + True, + "BasisOrder:=", + 1, + ] + oAnalysisSetup.InsertSetup("HfssEigen", setup_list) + +else: # use ansys_project_template + # delete substrate and vacuum objects + delete(oEditor, [o for n, v in objects.items() if "substrate" in n or "vacuum" in n for o in v]) + + scriptpath = os.path.dirname(__file__) + aedt_path = os.path.join(scriptpath, "../") + basename = os.path.splitext(os.path.basename(jsonfile))[0] + build_geom_name = basename + "_build_geometry" + template_path = data["ansys_project_template"] + template_basename = os.path.splitext(os.path.basename(template_path))[0] + + oProject = oDesktop.GetActiveProject() + oProject.SaveAs(os.path.join(aedt_path, build_geom_name + ".aedt"), True) + + oDesign = oProject.GetActiveDesign() + oEditor = oDesign.SetActiveEditor("3D Modeler") + sheet_name_list = oEditor.GetObjectsInGroup("Sheets") + oEditor.GetObjectsInGroup("Solids") + oEditor.Copy(["NAME:Selections", "Selections:=", ",".join(sheet_name_list)]) + + oDesktop.OpenProject(os.path.join(aedt_path, template_path)) + oProject = oDesktop.SetActiveProject(template_basename) + oDesign = oProject.GetActiveDesign() + oEditor = oDesign.SetActiveEditor("3D Modeler") + oEditor.Paste() + oDesktop.CloseProject(build_geom_name) + + +# Fit window to objects +oEditor.FitAll() + +# Notify the end of script +oDesktop.AddMessage("", "", 0, "Import completed (%s)" % time.asctime(time.localtime())) diff --git a/klayout_package/python/scripts/post_process_helpers.py b/klayout_package/python/scripts/post_process_helpers.py new file mode 100644 index 000000000..a29727a63 --- /dev/null +++ b/klayout_package/python/scripts/post_process_helpers.py @@ -0,0 +1,87 @@ +# This code is part of KQCircuits +# Copyright (C) 2024 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 csv +import json +import skrf + + +def find_varied_parameters(json_files): + """Finds the parameters that vary between the definitions in the json files. + + Args: + json_files: List of json file names + + Returns: + tuple (list, dict) + - list of parameter names + - dictionary with json file prefix as key and list of parameter values as value + """ + keys = [f.replace(".json", "") for f in json_files] + nominal = min(keys, key=len) + + # Load data from json files + parameter_dict = {} + for key, json_file in zip(keys, json_files): + with open(json_file, "r") as f: + definition = json.load(f) + parameter_dict[key] = definition["parameters"] + + # Find parameters that are varied + 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) + + # Return compressed parameter_dict including only varied parameters + parameter_values = {k: [v[p] for p in parameters] for k, v in parameter_dict.items()} + return parameters, parameter_values + + +def tabulate_into_csv(file_name, data_dict, parameters, parameter_values): + """Tabulate dictionary data into CSV + + Args: + file_name: output csv file name + data_dict: dictionary with relevant data to be saved + parameters: list of parameter names changed between simulations + parameter_values: dictionary with list of parameter values as value + """ + with open(file_name, "w") as csvfile: + writer = csv.writer(csvfile, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) + + layer_names = sorted({n for v in data_dict.values() for n in v}) + writer.writerow(["key"] + parameters + layer_names) + + for key, values in parameter_values.items(): + parameter_values_str = [str(parameter_value) for parameter_value in values] + layer_values = [str(data_dict[key].get(n, 0.0)) for n in layer_names] + writer.writerow([key] + parameter_values_str + layer_values) + + +def read_snp_network(snp_file): + """Read sNp file and returns network and list of z0 for each port""" + snp_network = skrf.Network(snp_file) + + # skrf.Network fails to read multiple Z0 terms from s2p file, so we do it separately. + with open(snp_file) as file: + lines = file.readlines() + for line in lines: + if line.startswith("# GHz S MA R "): + z0s = [float(z) for z in line[13:].split()] + if len(z0s) > 1: + return snp_network, z0s + return snp_network, snp_network.z0[0] diff --git a/klayout_package/python/scripts/produce_cmatrix_table.py b/klayout_package/python/scripts/produce_cmatrix_table.py new file mode 100644 index 000000000..2c4004bec --- /dev/null +++ b/klayout_package/python/scripts/produce_cmatrix_table.py @@ -0,0 +1,46 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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). +""" +Produces cmatrix table from Ansys or Elmer results +""" + +import os +import json +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +from post_process_helpers import ( # pylint: disable=wrong-import-position, no-name-in-module + find_varied_parameters, + tabulate_into_csv, +) + +# Find data files +path = os.path.curdir +result_files = [f for f in os.listdir(path) if f.endswith("_project_results.json")] +if result_files: + # Find parameters that are swept + definition_files = [f.replace("_project_results.json", ".json") for f in result_files] + parameters, parameter_values = find_varied_parameters(definition_files) + + # Load result data + cmatrix = {} + for key, result_file in zip(parameter_values.keys(), result_files): + with open(result_file, "r") as f: + result = json.load(f) + cmatrix[key] = {f"C{i+1}{j+1}": c for i, l in enumerate(result["CMatrix"]) for j, c in enumerate(l)} + + tabulate_into_csv(f"{os.path.basename(os.path.abspath(path))}_results.csv", cmatrix, parameters, parameter_values) diff --git a/klayout_package/python/scripts/produce_epr_table.py b/klayout_package/python/scripts/produce_epr_table.py new file mode 100644 index 000000000..098ec089c --- /dev/null +++ b/klayout_package/python/scripts/produce_epr_table.py @@ -0,0 +1,221 @@ +# 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). +""" +Produces EPR table from Ansys or Elmer results + +Args: + sys.argv[1]: parameter file for EPR calculations. Can include following: + - sheet_approximations: dictionary to transform sheet integral to thin layer integral. Includes parameters for + thickness, eps_r, and background_eps_r + - groups: List of layer keys. If given, the layers including a key are grouped together and other layers are ignored + - mer_correction_paths: If given, the script tries to look for Elmer cross-section results for EPR correction +""" +import json +import os +import sys +import csv +from pathlib import Path + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) + + +def get_mer_coefficients(key, reg, mer_correction_path, groups): + """ + returns the MER coefficients for certain path and saves the loaded coefficients in a separate json file. + + Args. + key(String): filename key for result file where the coefficients are stored + reg(String): region name + mer_correction_path(String): path to the folder where the coefficients are located + groups(List[String]): group names (usually ma, sa, ms, vacuum, substrate) + + Returns. + mer_coefficients(dict()): dict containing + + """ + mer_coefficients = dict() + full_mer_path = Path(mer_correction_path).joinpath(key + "_project_results.json") + if not os.path.isfile(full_mer_path): + full_mer_path = "//wsl.localhost/Ubuntu" + str(full_mer_path) + if not os.path.isfile(full_mer_path): + full_mer_path = os.path.join( + os.path.abspath(__file__ + "/../../../"), "/".join(full_mer_path.split(os.sep)[-2:]) + ) + + with open(full_mer_path, "r") as f: + correction_results = json.load(f) + + mer_keys_E = [k for k, v in correction_results.items() if "mer" in k and k.startswith("E_")] + mer_total_2d = sum([v for k, v in correction_results.items() if k in mer_keys_E]) + for group in groups: + mer_coefficients[group] = ( + sum([v for k, v in correction_results.items() if k in mer_keys_E and group.lower() in k.lower()]) + / mer_total_2d + ) + + with open(f"mer_coefficients_{reg}_{key}.json", "w") as f: + json.dump(mer_coefficients, f) + + return mer_coefficients + + +from post_process_helpers import ( # pylint: disable=wrong-import-position, no-name-in-module + find_varied_parameters, + tabulate_into_csv, +) + +pp_data = dict() +if len(sys.argv) > 1: + with open(sys.argv[1], "r") as fp: + pp_data = json.load(fp) + +# Find data files +path = os.path.curdir +result_files = [f for f in os.listdir(path) if f.endswith("_project_energy.csv") or f.endswith("_project_results.json")] +if result_files: + # Find parameters that are swept + definition_files = [ + ( + f.replace("_project_results.json", ".json") + if f.endswith("_project_results.json") + else f.replace("_project_energy.csv", ".json") + ) + for f in result_files + ] + parameters, parameter_values = find_varied_parameters(definition_files) + + # Load result data + epr_dict = {} + for key, result_file in zip(parameter_values.keys(), result_files): + if result_file.endswith("_project_results.json"): + # read results from Elmer output + with open(result_file, "r") as f: + result = json.load(f) + else: + # read results from Ansys output + with open(result_file, "r") as f: + reader = csv.reader(f, delimiter=",") + result_keys = next(reader) + result_values = next(reader) + result = {k[:-3]: float(v) for k, v in zip(result_keys, result_values)} + + energy = {k[2:]: v for k, v in result.items() if k.startswith("E_")} + + # add sheet energies if 'sheet_approximations' are available + if "sheet_approximations" in pp_data: + xy_energy = {k[4:]: v for k, v in result.items() if k.startswith("Exy_")} + z_energy = {k[3:]: v for k, v in result.items() if k.startswith("Ez_")} + for k, d in pp_data["sheet_approximations"].items(): + if "thickness" not in d: + continue + eps_r = d.get("eps_r", 1.0) + xy_scale = d["thickness"] * eps_r + for xy_k, xy_v in xy_energy.items(): + if k in xy_k: + energy[xy_k] = energy.get(xy_k, 0.0) + xy_scale * xy_v + background_eps_r = d.get("background_eps_r", 1.0) + z_scale = d["thickness"] * background_eps_r * background_eps_r / eps_r + for z_k, z_v in z_energy.items(): + if k in z_k: + energy[z_k] = energy.get(z_k, 0.0) + z_scale * z_v + + total_energy = sum(energy.values()) + if total_energy == 0.0: + continue + + energy_items = energy.items() + if "groups" not in pp_data: + # calculate EPR corresponding to each energy integral + epr_dict[key] = {"p_" + k: v / total_energy for k, v in energy_items} + elif "mer_correction_paths" not in pp_data: + # use EPR groups to combine layers + epr_dict[key] = { + "p_" + group: sum([v / total_energy for k, v in energy_items if group in k]) + for group in pp_data["groups"] + } + else: + # distinguish EPRs to mer and nonmer groups and calculate corrected EPRs + + mer_correction_dict = dict() + energy_items_dict = dict() + energy_items_dict["mer"] = dict() + energy_items_dict["nonmer"] = dict() + region_keys = [] # Collect all those that are defined as regions except 'default' region + for reg, mer_correction_path in pp_data["mer_correction_paths"]: + mer_correction_dict[reg] = get_mer_coefficients(key, reg, mer_correction_path, pp_data["groups"]) + if not reg == "default": + energy_items_dict["mer"][reg] = [(k, v) for k, v in energy_items if "mer" in k and reg in k] + energy_items_dict["nonmer"][reg] = [(k, v) for k, v in energy_items if not "mer" in k and reg in k] + region_keys += [k for k, v in energy_items if reg in k] + + # Now take all those region keys and remove them from all the keys. + # These keys are those that are not in any region and are defined as 'default' + # and where default MER correction is used. We need to do this currently because + # we cannot easily add 'default' label to all the leftover layers. + region_keys = set(region_keys) + all_keys = set(energy.keys()) + default_keys = all_keys - region_keys + + energy_items_dict["mer"]["default"] = [(k, energy[k]) for k in default_keys if "mer" in k] + energy_items_dict["nonmer"]["default"] = [(k, energy[k]) for k in default_keys if not "mer" in k] + + epr_dict[key] = {} + for reg, mer_coefficients in mer_correction_dict.items(): + mer_total = sum([v for k, v in energy_items_dict["mer"][reg]]) + for group in pp_data["groups"]: + epr_mer = {k: v / total_energy for k, v in energy_items_dict["mer"][reg] if group in k} + epr_nonmer = {k: v / total_energy for k, v in energy_items_dict["nonmer"][reg] if group in k} + epr_dict[key][f"p_{group}_{reg}_nonmer"] = sum([v for k, v in epr_nonmer.items()]) + epr_dict[key][f"p_{group}_{reg}_mer"] = sum([v for k, v in epr_mer.items()]) + epr_dict[key][f"p_{group}_{reg}_mer_fixed"] = mer_total * mer_coefficients[group] / total_energy + epr_dict[key][f"p_{group}_{reg}"] = sum(epr_mer.values()) + sum(epr_nonmer.values()) + epr_dict[key][f"p_{group}_{reg}_fixed"] = ( + epr_dict[key][f"p_{group}_{reg}_mer_fixed"] + epr_dict[key][f"p_{group}_{reg}_nonmer"] + ) + + epr_dict[key]["total_participation"] = 0.0 + epr_dict[key]["total_participation_fixed"] = 0.0 + for i, group in enumerate(pp_data["groups"]): + epr_dict[key][f"p_{group}_nonmer"] = sum( + [v for k, v in epr_dict[key].items() if group in k and "nonmer" in k] + ) + epr_dict[key][f"p_{group}_mer"] = sum( + [ + v + for k, v in epr_dict[key].items() + if group in k and "mer" in k and "fixed" not in k and "nonmer" not in k + ] + ) + epr_dict[key][f"p_{group}_mer_fixed"] = sum( + [v for k, v in epr_dict[key].items() if group in k and "mer_fixed" in k] + ) + + epr_dict[key][f"p_{group}"] = sum( + [v for k, v in epr_dict[key].items() if group in k and "mer" not in k and "fixed" not in k] + ) + + epr_dict[key][f"p_{group}_fixed"] = sum( + [v for k, v in epr_dict[key].items() if group in k and "fixed" in k and "mer" not in k] + ) + + epr_dict[key]["total_participation"] += epr_dict[key][f"p_{group}"] + epr_dict[key]["total_participation_fixed"] += epr_dict[key][f"p_{group}_fixed"] + + print("total_participation", epr_dict[key]["total_participation"]) + print("total_participation_fixed", epr_dict[key]["total_participation_fixed"]) + + tabulate_into_csv(f"{os.path.basename(os.path.abspath(path))}_epr.csv", epr_dict, parameters, parameter_values) diff --git a/klayout_package/python/scripts/produce_q_factor_table.py b/klayout_package/python/scripts/produce_q_factor_table.py new file mode 100644 index 000000000..733e5ae6d --- /dev/null +++ b/klayout_package/python/scripts/produce_q_factor_table.py @@ -0,0 +1,80 @@ +# 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). +""" +Produces quality factor table from Ansys or Elmer results + +Args: + sys.argv[1]: parameter file name, where the file includes loss tangents for different layers + +""" +import json +import os +import sys +import csv + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "util")) +from post_process_helpers import ( # pylint: disable=wrong-import-position, no-name-in-module + find_varied_parameters, + tabulate_into_csv, +) + +with open(sys.argv[1], "r") as fp: + loss_tangents = json.load(fp) + +# Find data files +path = os.path.curdir +result_files = [f for f in os.listdir(path) if f.endswith("_project_energy.csv") or f.endswith("_project_results.json")] +if result_files: + # Find parameters that are swept + definition_files = [ + ( + f.replace("_project_results.json", ".json") + if f.endswith("_project_results.json") + else f.replace("_project_energy.csv", ".json") + ) + for f in result_files + ] + parameters, parameter_values = find_varied_parameters(definition_files) + + # Load result data + q = {} + for key, result_file in zip(parameter_values.keys(), result_files): + if result_file.endswith("_project_results.json"): + # read results from Elmer output + with open(result_file, "r") as f: + result = json.load(f) + else: + # read results from Ansys output + with open(result_file, "r") as f: + reader = csv.reader(f, delimiter=",") + result_keys = next(reader) + result_values = next(reader) + result = {k[:-3]: float(v) for k, v in zip(result_keys, result_values)} + + energy = {k[2:]: v for k, v in result.items() if k.startswith("E_")} + total_energy = result.get("total_energy", sum(energy.values())) + if total_energy == 0.0: + continue + + loss = { + loss_layer: sum([loss * v / total_energy for k, v in energy.items() if loss_layer in k]) + for loss_layer, loss in loss_tangents.items() + } + q[key] = {"Q_" + k: (1.0 / v if v else float("inf")) for k, v in loss.items()} + q[key]["Q_total"] = 1.0 / sum(loss.values()) + + tabulate_into_csv(f"{os.path.basename(os.path.abspath(path))}_q_factors.csv", q, parameters, parameter_values) diff --git a/klayout_package/python/scripts/run_pyepr_t1_estimate.py b/klayout_package/python/scripts/run_pyepr_t1_estimate.py new file mode 100644 index 000000000..7dc33e979 --- /dev/null +++ b/klayout_package/python/scripts/run_pyepr_t1_estimate.py @@ -0,0 +1,181 @@ +# This code is part of KQCircuits +# Copyright (C) 2022 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). +""" +Executes pyEPR calculations on Ansys simulation tool. +The geometry should be imported and Ansys project should be saved before calling this. + +Args: + sys.argv[1]: json file of the simulation + sys.argv[2]: parameter file for pyEPR calculations + +""" +import sys +import json + +from pathlib import Path + +import pandas as pd +import pyEPR as epr +import qutip.fileio + +project_path = Path.cwd() +project_name = str(Path(sys.argv[1]).stem if Path(sys.argv[1]).suffixes else Path(sys.argv[1])) + "_project" + +with open(sys.argv[2], "r") as fp: + pp_data = json.load(fp) + +try: + # Nominal values, good for relative comparison + epr.config["dissipation"].update( + { + ## 3D, given in `pinfo.dissipative['dielectrics_bulk']` + # integration to get participation ratio which is multiplied with the loss tangent, adds to Q as 1/(p tan_d) + # loss tangent (currently does not support multiple different materials) + "tan_delta_sapp": pp_data.get("substrate_loss_tangent", 1e-6), + ## 2D, given in `pinfo.dissipative['dielectric_surfaces']`. + # These values are not used if layer specific values for `dielectric_surfaces` are given in the JSON + # Approximates having the surface energy on a thin dielectric layer with + "tan_delta_surf": 0.001, # loss tangent + "th": 3e-9, # thickness + "eps_r": 10, # relative permittivity + } + ) + + # Rough correction factors from comparing to cross-section EPR. For more info, see Niko Savola M.Sc. thesis + correction_factor = { + "layerMA": 1 / 2.5, + "layerMS": 1 / 0.35, + "layerSA": 1 / 0.7, + } + + pinfo = epr.ProjectInfo( + project_path=project_path, + project_name=project_name, + design_name="EigenmodeDesign", + ) + oDesign = pinfo.design + + junction_numbers = [int(e.split("Junction")[-1]) for e in pinfo.get_all_object_names() if "Junction" in e] + + ## Set dissipative elements + # Ansys solids + pinfo.dissipative["dielectrics_bulk"] = [e for e in pinfo.get_all_object_names() if e.startswith("Substrate")] + if pp_data.get("dielectric_surfaces", None) is None: + pinfo.dissipative["dielectric_surfaces"] = [ # Ansys sheets (exclude 3D volumes, ports, and junctions) + e + for e in pinfo.get_all_object_names() + if not ( + e.startswith("Vacuum") + or e.startswith("Substrate") + or any(e in [f"Port{i}", f"Junction{i}"] for i in junction_numbers) + ) + ] + else: + pinfo.dissipative["dielectric_surfaces"] = { + e: v for e in pinfo.get_all_object_names() for k, v in pp_data["dielectric_surfaces"].items() if k in e + } + + oEditor = oDesign.modeler._modeler + for j in junction_numbers: + pinfo.junctions[f"j{j}"] = { + "Lj_variable": f"Lj_{j}", # junction inductance Lj + "rect": f"Port{j}", # rectangle on which lumped boundary condition is specified + "line": (name := f"Junction{j}"), # polyline spanning the length of the rectangle + "Cj_variable": f"Cj_{j}", # junction capacitance Cj (optional but needed for dissipation) + "length": f"{oEditor.GetEdgeLength(oEditor.GetEdgeIDsFromObject(name)[0])}{oEditor.GetModelUnits()}", + } + pinfo.validate_junction_info() + + # Simulate + if pinfo.setup.solution_name: + pinfo.setup.analyze() + + eprh = epr.DistributedAnalysis(pinfo) + eprh.do_EPR_analysis() # do field calculations + + epra = epr.QuantumAnalysis(eprh.data_filename) + epr_results = epra.analyze_all_variations() # post-process field calculations to get results + + df = pd.DataFrame() + for variation, data in epr_results.items(): + + f_ND, chi_ND, hamiltonian = epr.calcs.back_box_numeric.epr_numerical_diagonalization( + data["f_0"] / 1e3, # in GHz + data["Ljs"], # in H + data["ZPF"], # in units of reduced flux quantum + return_H=True, + ) + # older versions of qutip require str path + qutip.fileio.qsave(hamiltonian, str(project_path / f"Hamiltonian_{project_name}_{variation}.qu")) + + # fmt: off + df = pd.concat([df, pd.DataFrame({ + 'variation': eprh.get_variation_string(variation), + + # Substrate quality factor and participation + **{(Q_bulk := data['sol'].filter(regex=bulk+'$')).columns[0]: Q_bulk.values.flatten() + for bulk in pinfo.dissipative['dielectrics_bulk']}, + **{f'p_dielectric_{bulk}': (1 / ( + data['sol'].filter(regex=bulk+'$') * epr.config['dissipation']['tan_delta_sapp'] + )).values.flatten() + for bulk in pinfo.dissipative['dielectrics_bulk']}, + + # Surface quality factors and participation + **{(Q_surf := data['sol'].filter(regex=surface)).columns[0]: Q_surf.values.flatten() \ + / correction_factor.get(surface, 1) + for surface in pinfo.dissipative['dielectric_surfaces'].keys()}, + **{f'p_surf_{surface}': (1 / ( # th & eps_r still affect + data['sol'].filter(regex=surface) \ + * (pinfo.dissipative['dielectric_surfaces'][surface]['tan_delta_surf'] + if isinstance(pinfo.dissipative['dielectric_surfaces'], dict) + else epr.config['dissipation']['tan_delta_surf']) + * correction_factor.get(surface, 1) + )).values.flatten() + for surface in pinfo.dissipative['dielectric_surfaces'].keys()}, + + 'Q_ansys': data.get('Qs', None), + 'f_0': data['f_0'] * 1e6, + 'f_1': data['f_1'] * 1e6, # f_0 frequency - Lamb shift from cavity + # for ZPF details consult https://github.com/zlatko-minev/pyEPR/blob/master/pyEPR/calcs/basic.py#L12 + 'ZPF': data['ZPF'].flatten(), + 'Pm_normed': data['Pm_normed'].flatten(), + 'Ljs': data['Ljs'][0], + 'Cjs': data['Cjs'][0], + 'Chi_O1': str(data['chi_O1'].values), # first order perturbation theory + # Results from numerical diagonalisation + 'Chi_ND': str(chi_ND), + 'f_ND': f_ND, + # 'n_zpf': ZPF from capacitive participation, small compared to inductive + }).rename_axis('mode')]) + # fmt: on + + # Total quality factor (harmonic sum) after corrected values + df["Q_total"] = (1 / (1 / df.filter(regex="^Q(dielectric|surf).*")).sum(axis=1)).values.flatten() + + # Convert to multi-index + df.set_index(["variation", df.index], inplace=True) + df.to_csv(project_path / f"QData_{project_name}.csv", index_label=["variation", "mode"]) + + # Save HFSS project file + pinfo.project.save() + # This allows Ansys to be closed, but doesn't close it + pinfo.disconnect() + +finally: + + # Always exit Ansys + epr.ansys.HfssApp().get_app_desktop()._desktop.QuitApplication() diff --git a/klayout_package/python/scripts/simulations/double_pads_elmer_capacitive_sim.py b/klayout_package/python/scripts/simulations/double_pads_elmer_capacitive_sim.py new file mode 100644 index 000000000..5bad3ab35 --- /dev/null +++ b/klayout_package/python/scripts/simulations/double_pads_elmer_capacitive_sim.py @@ -0,0 +1,102 @@ +# This code is part of KQCircuits +# Copyright (C) 2022 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). + +import logging +import sys +from pathlib import Path + +import numpy as np + +from kqcircuits.qubits.double_pads import DoublePads +from kqcircuits.simulations.post_process import PostProcess +from kqcircuits.simulations.single_element_simulation import get_single_element_sim_class +from kqcircuits.pya_resolver import pya +from kqcircuits.simulations.export.ansys.ansys_export import export_ansys +from kqcircuits.simulations.export.elmer.elmer_export import export_elmer +from kqcircuits.simulations.export.simulation_export import export_simulation_oas +from kqcircuits.util.export_helper import ( + create_or_empty_tmp_directory, + get_active_or_new_layout, + open_with_klayout_or_default_application, +) + + +sim_tool = "elmer" + +# Simulation parameters +sim_class = get_single_element_sim_class(DoublePads) # pylint: disable=invalid-name +sim_parameters = { + "name": "double_pads", + "use_internal_ports": True, + "use_ports": True, + "face_stack": ["1t1"], + "box": pya.DBox(pya.DPoint(0, 0), pya.DPoint(2000, 2000)), + "separate_island_internal_ports": sim_tool != "eigenmode", # DoublePads specific + "tls_layer_thickness": 5e-3 if sim_tool == "eigenmode" else 0.0, # in µm + "tls_sheet_approximation": sim_tool == "eigenmode", + "waveguide_length": 200, +} + +dir_path = create_or_empty_tmp_directory(Path(__file__).stem + f"_output_{sim_tool}") +print(dir_path) +export_parameters_elmer = { + "tool": "capacitance", + "workflow": { + "python_executable": "python", + "n_workers": 4, + "elmer_n_processes": -1, + "gmsh_n_threads": -1, + "elmer_n_threads": -1, + }, + "mesh_size": { + "global_max": 50.0, + "1t1_gap&1t1_signal": [2.0, 4.0], + "1t1_gap&1t1_ground": [2.0, 4.0], + }, +} + +# Get layout +logging.basicConfig(level=logging.INFO, stream=sys.stdout) +layout = get_active_or_new_layout() + +name = sim_parameters["name"] +simulations = [ + sim_class( + layout, + **{ + **sim_parameters, + "ground_gap": [900, 900], + "a": 5, + "b": 20, + "coupler_a": 5, + "coupler_extent": [50, 20], + "coupler_offset": 100, + "junction_type": "Manhattan", + "island2_taper_junction_width": 31.7, + "junction_total_length": 39.5, + "name": name, + }, + ) +] + +# Create simulation +oas = export_simulation_oas(simulations, dir_path) + +export_elmer(simulations, dir_path, **export_parameters_elmer) + +logging.info(f"Total simulations: {len(simulations)}") +open_with_klayout_or_default_application(oas) diff --git a/klayout_package/python/scripts/util/field_calculation.py b/klayout_package/python/scripts/util/field_calculation.py new file mode 100644 index 000000000..46b927541 --- /dev/null +++ b/klayout_package/python/scripts/util/field_calculation.py @@ -0,0 +1,66 @@ +# This code is part of KQCircuits +# Copyright (C) 2024 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). + + +def add_squared_electric_field_expression(oModule, name, vec_operator): + """Adds squared complex-magnitude electric field expression to field calculation""" + oModule.EnterQty("E") + oModule.CalcOp("CmplxMag") + oModule.CalcOp(vec_operator) + oModule.EnterScalar(2) + oModule.CalcOp("Pow") + oModule.AddNamedExpression(name, "Fields") + + +def add_energy_integral_expression(oModule, name, objects, field_expr, dim, epsilon, subtraction_field): + """Adds energy integral expression to field calculation""" + for i, obj in enumerate(objects): + oModule.CopyNamedExprToStack(field_expr) + if dim == 2: + oModule.EnterSurf(obj) + else: + oModule.EnterVol(obj) + oModule.CalcOp("Integrate") + if i > 0: + oModule.CalcOp("+") + if objects: + oModule.EnterScalar(epsilon / 2) + oModule.CalcOp("*") + else: + oModule.EnterScalar(0.0) + if subtraction_field: + oModule.CopyNamedExprToStack(subtraction_field) + oModule.CalcOp("-") + oModule.AddNamedExpression(name, "Fields") + + +def add_magnetic_flux_integral_expression(oModule, name, objects): + """Adds magnetic flux integral expression to field calculation. Will be in units of magnetic flux quanta.""" + for i, obj in enumerate(objects): + oModule.EnterQty("H") + oModule.CalcOp("ScalarZ") + oModule.CalcOp("Real") + oModule.EnterSurf(obj) + oModule.CalcOp("Integrate") + if i > 0: + oModule.CalcOp("+") + if objects: + oModule.EnterScalar(607706979.4822713) # 2 * mu_0 * elementary_charge / Planck + oModule.CalcOp("*") # Transform flux into unit of magnetic flux quanta. + else: + oModule.EnterScalar(0.0) + oModule.AddNamedExpression(name, "Fields") diff --git a/klayout_package/python/scripts/util/geometry.py b/klayout_package/python/scripts/util/geometry.py new file mode 100644 index 000000000..20fe1cac0 --- /dev/null +++ b/klayout_package/python/scripts/util/geometry.py @@ -0,0 +1,297 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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). + + +def format_position(x, units): + if isinstance(x, list): + return [format_position(p, units) for p in x] + elif isinstance(x, str): + return x + else: + return str(x) + units + + +def create_rectangle(oEditor, name, x, y, z, w, h, axis, units): + if w != 0.0 and h != 0.0: + oEditor.CreateRectangle( + [ + "NAME:RectangleParameters", + "IsCovered:=", + True, + "XStart:=", + format_position(x, units), + "YStart:=", + format_position(y, units), + "ZStart:=", + format_position(z, units), + "Width:=", + format_position(w, units), + "Height:=", + format_position(h, units), + "WhichAxis:=", + axis, + ], + ["NAME:Attributes", "Name:=", name, "PartCoordinateSystem:=", "Global"], + ) + + +def create_polygon(oEditor, name, points, units): + oEditor.CreatePolyline( + [ + "NAME:PolylineParameters", + "IsPolylineCovered:=", + True, + "IsPolylineClosed:=", + True, + ["NAME:PolylinePoints"] + + [ + [ + "NAME:PLPoint", + "X:=", + format_position(p[0], units), + "Y:=", + format_position(p[1], units), + "Z:=", + format_position(p[2], units), + ] + for p in points + [points[0]] + ], + ["NAME:PolylineSegments"] + + [ + ["NAME:PLSegment", "SegmentType:=", "Line", "StartIndex:=", i, "NoOfPoints:=", 2] + for i in range(len(points)) + ], + [ + "NAME:PolylineXSection", + "XSectionType:=", + "None", + "XSectionOrient:=", + "Auto", + "XSectionWidth:=", + "0" + units, + "XSectionTopWidth:=", + "0" + units, + "XSectionHeight:=", + "0" + units, + "XSectionNumSegments:=", + "0", + "XSectionBendType:=", + "Corner", + ], + ], + ["NAME:Attributes", "Name:=", name, "Flags:=", "", "PartCoordinateSystem:=", "Global"], + ) + + +def create_box(oEditor, name, x, y, z, sx, sy, sz, units): + if sx != 0.0 and sy != 0.0 and sz != 0.0: + oEditor.CreateBox( + [ + "NAME:BoxParameters", + "XPosition:=", + format_position(x, units), + "YPosition:=", + format_position(y, units), + "ZPosition:=", + format_position(z, units), + "XSize:=", + format_position(sx, units), + "YSize:=", + format_position(sy, units), + "ZSize:=", + format_position(sz, units), + ], + [ + "NAME:Attributes", + "Name:=", + name, + "MaterialValue:=", + '""', + "Flags:=", + "", + "PartCoordinateSystem:=", + "Global", + ], + ) + + +def thicken_sheet(oEditor, objects, thickness, units): + """Thickens sheet to solid with given thickness and material""" + if objects and thickness != 0.0: + oEditor.SweepAlongVector( + ["NAME:Selections", "Selections:=", ",".join(objects), "NewPartsModelFlag:=", "Model"], + [ + "NAME:VectorSweepParameters", + "DraftAngle:=", + "0deg", + "DraftType:=", + "Round", + "CheckFaceFaceIntersection:=", + False, + "SweepVectorX:=", + "0um", + "SweepVectorY:=", + "0um", + "SweepVectorZ:=", + "{} {}".format(thickness, units), + ], + ) + + +def set_material(oEditor, objects, material=None, solve_inside=None): + if objects: + if solve_inside is not None: + oEditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Geometry3DAttributeTab", + ["NAME:PropServers"] + objects, + ["NAME:ChangedProps", ["NAME:Solve Inside", "Value:=", solve_inside]], + ], + ] + ) + if material is not None: + oEditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Geometry3DAttributeTab", + ["NAME:PropServers"] + objects, + ["NAME:ChangedProps", ["NAME:Material", "Value:=", '"{}"'.format(material)]], + ], + ] + ) + else: + oEditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Geometry3DAttributeTab", + ["NAME:PropServers"] + objects, + ["NAME:ChangedProps", ["NAME:Model", "Value:=", False]], + ], + ] + ) + + +def add_layer(layer_map, order_map, layer_num, dest_layer, order, layer_type="signal"): + """Appends layer data to layer_map and order_map.""" + layer_map.append( + ["NAME:LayerMapInfo", "LayerNum:=", layer_num, "DestLayer:=", dest_layer, "layer_type:=", layer_type] + ) + order_map += ["entry:=", ["order:=", order, "layer:=", dest_layer]] + + +def move_vertically(oEditor, objects, z_shift, units): + """Moves objects in z-direction by z_shift.""" + if objects and z_shift != 0.0: + oEditor.Move( + ["NAME:Selections", "Selections:=", ",".join(objects), "NewPartsModelFlag:=", "Model"], + [ + "NAME:TranslateParameters", + "TranslateVectorX:=", + "0 {}".format(units), + "TranslateVectorY:=", + "0 {}".format(units), + "TranslateVectorZ:=", + "{} {}".format(z_shift, units), + ], + ) + + +def copy_paste(oEditor, objects): + """Duplicates objects and returns new object names.""" + if objects: + oEditor.Copy(["NAME:Selections", "Selections:=", ",".join(objects)]) + return oEditor.Paste() + else: + return [] + + +def delete(oEditor, objects): + """Delete given objects""" + if objects: + oEditor.Delete(["NAME:Selections", "Selections:=", ",".join(objects)]) + + +def subtract(oEditor, objects, tool_objects, keep_originals=False): + """Subtract tool_objects from objects.""" + if objects and tool_objects: + oEditor.Subtract( + ["NAME:Selections", "Blank Parts:=", ",".join(objects), "Tool Parts:=", ",".join(tool_objects)], + ["NAME:SubtractParameters", "KeepOriginals:=", keep_originals, "TurnOnNBodyBoolean:=", True], + ) + + +def unite(oEditor, objects, keep_originals=False): + """Unite objects into the first object.""" + if len(objects) > 1: + oEditor.Unite( + ["NAME:Selections", "Selections:=", ",".join(objects)], + ["NAME:UniteParameters", "KeepOriginals:=", keep_originals, "TurnOnNBodyBoolean:=", True], + ) + + +def objects_from_sheet_edges(oEditor, objects, thickness, units): + """Creates boundary objects for each sheet and returns object names in list.""" + edges = [] + for o in objects: + boundary_ids = [int(i) for i in oEditor.GetEdgeIDsFromObject(o)] + edge_list = oEditor.CreateObjectFromEdges( + ["NAME:Selections", "Selections:=", o, "NewPartsModelFlag:=", "Model"], + ["NAME:Parameters", ["NAME:BodyFromEdgeToParameters", "Edges:=", boundary_ids]], + ["CreateGroupsForNewObjects:=", False], + ) + thicken_sheet(oEditor, edge_list, thickness, units) + unite(oEditor, edge_list, False) + edges.append(edge_list[0]) + return edges + + +def add_material(oDefinitionManager, name, **parameters): + """Adds material with given name and parameters.""" + param_list = [ + "NAME:" + name, + "CoordinateSystemType:=", + "Cartesian", + "BulkOrSurfaceType:=", + 1, + ["NAME:PhysicsTypes", "set:=", ["Electromagnetic"]], + ] + for key, value in parameters.items(): + param_list += ["{}:=".format(key), str(value)] + oDefinitionManager.AddMaterial(param_list) + + +def set_color(oEditor, objects, red, green, blue, transparency): + """Sets color and transparency for given objects.""" + if objects: + oEditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Geometry3DAttributeTab", + ["NAME:PropServers"] + objects, + [ + "NAME:ChangedProps", + ["NAME:Color", "R:=", red, "G:=", green, "B:=", blue], + ["NAME:Transparent", "Value:=", transparency], + ], + ], + ] + ) diff --git a/klayout_package/python/scripts/util/util.py b/klayout_package/python/scripts/util/util.py new file mode 100644 index 000000000..184b5a9e9 --- /dev/null +++ b/klayout_package/python/scripts/util/util.py @@ -0,0 +1,123 @@ +# This code is part of KQCircuits +# Copyright (C) 2021 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 json + + +def get_enabled_setup(oDesign, tab="HfssTab"): + """Returns enabled analysis setup. Returns None if not enabled.""" + setup_names = oDesign.GetModule("AnalysisSetup").GetSetups() + for name in setup_names: + if oDesign.GetPropertyValue(tab, "AnalysisSetup:" + name, "Enabled") == "true": + return name + return None + + +def get_enabled_sweep(oDesign, setup, tab="HfssTab"): + """Returns enabled analysis sweep. Returns None if not enabled.""" + sweep_names = oDesign.GetModule("AnalysisSetup").GetSweeps(str(setup)) + for name in sweep_names: + if oDesign.GetPropertyValue(tab, "AnalysisSetup:" + setup + ":" + name, "Enabled") == "true": + return name + return None + + +def get_solution_data(report_setup, report_type, solution_name, context_array, families_array, expression): + """Returns value of given expression. If expression is a list the result is a dictionary.""" + if isinstance(expression, list): + expressions = expression + else: + expressions = [expression] + + # get solution data by frequency and expression + s = report_setup.GetSolutionDataPerVariation( + report_type, solution_name, context_array, families_array, expressions + )[0] + + def getresult(s, expr): + if s.IsDataComplex(expr): + return [ + complex(re, im) for (re, im) in zip(s.GetRealDataValues(expr, True), s.GetImagDataValues(expr, True)) + ] + else: + return [float(re) for re in s.GetRealDataValues(expr, True)] + + if isinstance(expression, list): + result = {e: getresult(s, e) for e in expressions} + result["Freq"] = [float(re) for re in s.GetSweepValues("Freq")] + else: + result = getresult(s, expression) + + return result + + +def get_quantities(report_setup, report_type, solution_name, context_array, category_name): + """Returns the available quantities in given category""" + return report_setup.GetAllQuantities(report_type, "Rectangular Plot", solution_name, context_array, category_name) + + +def create_x_vs_y_plot( + report_setup, + plot_name, + report_type, + solution_name, + context_array, + input_family, + x_components, + y_label, + y_components, +): + """Create report (plot) with x vs. y in the given context. Report is docked to view.""" + report_setup.CreateReport( + plot_name, + report_type, + "Rectangular Plot", + solution_name, + context_array, + input_family, + ["X Component:=", x_components, "Y Component:=", y_components], + ) + report_setup.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Legend", + ["NAME:PropServers", "%s:Legend" % plot_name], + [ + "NAME:ChangedProps", + ["NAME:Show Variation Key", "Value:=", False], + ["NAME:Show Solution Name", "Value:=", False], + ["NAME:DockMode", "Value:=", "Dock Right"], + ], + ], + [ + "NAME:Axis", + ["NAME:PropServers", "%s:AxisY1" % plot_name], + ["NAME:ChangedProps", ["NAME:Specify Name", "Value:=", True], ["NAME:Name", "Value:=", y_label]], + ], + ] + ) + + +# Helper class to encode complex data in json output +class ComplexEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, complex): + return [o.real, o.imag] + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, o)