Source code for qiskit_metal.renderers.renderer_ansys.hfss_renderer

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# modified by Chalmers/SK/20210621
# modified by Samarth Hawaldar

from collections import defaultdict
from pathlib import Path
from typing import Union, Tuple

import logging
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pyEPR as epr
from pyEPR.ansys import set_property, parse_units

from qiskit_metal import Dict
from qiskit_metal.draw.utility import to_vec3D
from qiskit_metal.renderers.renderer_ansys.ansys_renderer import (
    QAnsysRenderer, get_clean_name)


[docs] class QHFSSRenderer(QAnsysRenderer): """Subclass of QAnsysRenderer for running HFSS simulations. QAnsysRenderer Default Options: * Lj: '10nH' -- Lj has units of nanoHenries (nH) * Cj: 0 -- Cj *must* be 0 for pyEPR analysis! Cj has units of femtofarads (fF) * _Rj: 0 -- _Rj *must* be 0 for pyEPR analysis! _Rj has units of Ohms * max_mesh_length_jj: '7um' -- Maximum mesh length for Josephson junction elements * max_mesh_length_port: '7um' -- Maximum mesh length for Ports in Eigenmode Simulations * project_path: None -- Default project path; if None --> get active * project_name: None -- Default project name * design_name: None -- Default design name * ansys_file_extension: '.aedt' -- Ansys file extension for 2016 version and newer * x_buffer_width_mm: 0.2 -- Buffer between max/min x and edge of ground plane, in mm * y_buffer_width_mm: 0.2 -- Buffer between max/min y and edge of ground plane, in mm """ name = 'hfss' """Name""" hfss_options = Dict( port_inductor_gap= '10um' # spacing between port and inductor if junction is drawn both ways ) """HFSS Options""" def __init__(self, design: 'QDesign', initiate=True, options: Dict = None): """Create a QRenderer for HFSS simulations, subclassed from QAnsysRenderer. Args: design (QDesign): Use QGeometry within QDesign to obtain elements for Ansys. initiate (bool, optional): True to initiate the renderer. Defaults to True. options (Dict, optional): Used to override all options. Defaults to None. """ super().__init__(design=design, initiate=initiate, options=options) self.current_sweep = None QHFSSRenderer.load()
[docs] def render_design(self, selection: Union[list, None] = None, open_pins: Union[list, None] = None, port_list: Union[list, None] = None, jj_to_port: Union[list, None] = None, ignored_jjs: Union[list, None] = None, box_plus_buffer: bool = True): """Initiate rendering of components in design contained in selection, assuming they're valid. Components are rendered before the chips they reside on, and subtraction of negative shapes is performed at the very end. Add the metalize() method here to turn objects in self.assign_perfE (see init in QAnsysRenderer class) into perfect electrical conductors. Create lumped ports as needed. First obtain a list of IDs of components to render and a corresponding case, denoted by self.qcomp_ids and self.case, respectively. If self.case == 1, all components in QDesign are to be rendered. If self.case == 0, a strict subset of components in QDesign are to be rendered. Otherwise, if self.case == 2, one or more component names in selection cannot be found in QDesign. Among the components selected for export, there may or may not be unused (unconnected) pins. The second parameter, open_pins, contains tuples of the form (component_name, pin_name) that specify exactly which pins should be open rather than shorted during the simulation. Both the component and pin name must be specified because the latter could be shared by multiple components. All pins in this list are rendered with an additional endcap in the form of a rectangular cutout, to be subtracted from its respective plane. In driven modal solutions, the Ansys design must include one or more ports. This is done by adding all port designations and their respective impedances in Ohms as (qcomp, pin, impedance) to port_list. Note that an open endcap must separate the two sides of each pin before inserting a lumped port in between, so behind the scenes all pins in port_list are also added to open_pins. Practically, however, port_list and open_pins are inputted as mutually exclusive lists. Also in driven modal solutions, one may want to render junctions as lumped ports and/or inductors, or omit them altogether. To do so, tuples of the form (component_name, element_name, impedance, draw_ind) are added to the list jj_to_port. For example, ('Q1', 'rect_jj', 70, True) indicates that rect_jj of component Q1 is to be rendered as both a lumped port with an impedance of 70 Ohms as well as an inductor whose properties are given in default_options. Replacing the last entry of this 4-element tuple with False would indicate that only the port is to be drawn, not the inductor. Alternatively for driven modal solutions, one may want to disregard select junctions in the Metal design altogether to simulate the capacitive effect while keeping the qubit in an "off" state. Such junctions are specified in the form (component_name, element_name) in the list ignored_jjs. The final parameter, box_plus_buffer, determines how the chip is drawn. When set to True, it takes the minimum rectangular bounding box of all rendered components and adds a buffer of x_buffer_width_mm and y_buffer_width_mm horizontally and vertically, respectively, to the chip size. The center of the chip lies at the midpoint x/y coordinates of the minimum rectangular bounding box and may change depending on which components are rendered and how they're positioned. If box_plus_buffer is False, however, the chip position and dimensions are taken from the chip info dictionary found in self.design, irrespective of what's being rendered. While this latter option is faster because it doesn't require calculating a bounding box, it runs the risk of rendered components being too close to the edge of the chip or even falling outside its boundaries. Args: selection (Union[list, None], optional): List of components to render. Defaults to None. open_pins (Union[list, None], optional): List of tuples of pins that are open. Defaults to None. port_list (Union[list, None], optional): List of tuples of pins to be rendered as ports. Defaults to None. jj_to_port (Union[list, None], optional): List of tuples of jj's to be rendered as ports. Defaults to None. ignored_jjs (Union[list, None], optional): List of tuples of jj's that shouldn't be rendered. Defaults to None. box_plus_buffer (bool): Either calculate a bounding box based on the location of rendered geometries or use chip size from design class. """ self.qcomp_ids, self.case = self.get_unique_component_ids(selection) if self.case == 2: self.logger.warning( 'Unable to proceed with rendering. Please check selection.') return self.chip_subtract_dict = defaultdict(set) self.assign_perfE = [] self.assign_mesh = [] self.assign_port_mesh = [] self.jj_lumped_ports = {} self.jj_to_ignore = set() if jj_to_port: self.jj_lumped_ports = { (qcomp, elt): [impedance, draw_ind] for qcomp, elt, impedance, draw_ind in jj_to_port } if ignored_jjs: self.jj_to_ignore = {(qcomp, qelt) for qcomp, qelt in ignored_jjs} self.render_tables() if port_list: self.add_endcaps(open_pins + [(qcomp, pin) for qcomp, pin, _ in port_list]) else: self.add_endcaps(open_pins) self.render_chips(box_plus_buffer=box_plus_buffer) self.subtract_from_ground() if port_list: self.create_ports(port_list) self.add_mesh() self.metallize()
[docs] def create_ports(self, port_list: list): """Add ports and their respective impedances in Ohms to designated pins in port_list. Port_list is formatted as [(qcomp_0, pin_0, impedance_0), (qcomp_1, pin_1, impedance_1), ...]. Args: port_list (list): List of tuples of pins to be rendered as ports. """ for qcomp, pin, impedance in port_list: port_name = f'Port_{qcomp}_{pin}' pdict = self.design.components[qcomp].pins[pin] midpt, gap_size, norm_vec, width = pdict['middle'], pdict['gap'], \ pdict['normal'], pdict['width'] width = parse_units(width) endpoints = parse_units([midpt, midpt + gap_size * norm_vec]) endpoints_3d = to_vec3D(endpoints, 0) # Set z height to 0 x0, y0 = endpoints_3d[0][:2] x1, y1 = endpoints_3d[1][:2] if abs(y1 - y0) > abs(x1 - x0): # Junction runs vertically up/down x_min, x_max = x0 - width / 2, x0 + width / 2 y_min, y_max = min(y0, y1), max(y0, y1) else: # Junction runs horizontally left/right x_min, x_max = min(x0, x1), max(x0, x1) y_min, y_max = y0 - width / 2, y0 + width / 2 # Draw rectangle self.logger.debug(f'Drawing a rectangle: {port_name}') poly_ansys = self.modeler.draw_rect_corner([x_min, y_min, 0], x_max - x_min, y_max - y_min, 0, **dict(transparency=0.0)) axis = 'x' if abs(x1 - x0) > abs(y1 - y0) else 'y' if self.pinfo.design.solution_type != 'Eigenmode': poly_ansys.make_lumped_port(axis, z0=str(impedance) + 'ohm', name=f'LumpPort_{qcomp}_{pin}') self.modeler.rename_obj(poly_ansys, port_name) else: poly_ansys.make_rlc_boundary(axis, r=str(impedance) + 'ohm', name=f'RLCBoundary_{qcomp}_{pin}') self.modeler.rename_obj(poly_ansys, port_name) self.assign_port_mesh.append(port_name) # Draw line lump_line = self.modeler.draw_polyline( [endpoints_3d[0], endpoints_3d[1]], closed=False, **dict(color=(128, 0, 128))) lump_line = lump_line.rename(f'voltage_line_{port_name}') lump_line.show_direction = True
[docs] def render_element_junction(self, qgeom: pd.Series): """ Render a Josephson junction depending on the solution type. If in HFSS eigenmode, junctions are rendered as inductors consisting of 1. A rectangle of length pad_gap and width inductor_width. Defines lumped element RLC boundary condition. 2. A line that is later used to calculate the voltage in post-processing analysis. If in HFSS driven modal, junctions can be inductors, lumped ports, both inductors and lumped ports, or omitted altogether. Ports are characterized by an impedance value given in the list jj_to_port when render_design() is called. Args: qgeom (pd.Series): GeoSeries of element properties. """ qcomp = self.design._components[qgeom['component']].name qc_elt = get_clean_name(qgeom['name']) if (qcomp, qc_elt) not in self.jj_to_ignore: qc_shapely = qgeom.geometry qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip)) qc_width = parse_units(qgeom.width) endpoints = parse_units(list(qc_shapely.coords)) endpoints_3d = to_vec3D(endpoints, qc_chip_z) x0, y0, z0 = endpoints_3d[0] x1, y1, z0 = endpoints_3d[1] if abs(y1 - y0) > abs(x1 - x0): # Junction runs vertically up/down axis = 'y' x_min, x_max = x0 - qc_width / 2, x0 + qc_width / 2 y_min, y_max = min(y0, y1), max(y0, y1) else: # Junction runs horizontally left/right axis = 'x' x_min, x_max = min(x0, x1), max(x0, x1) y_min, y_max = y0 - qc_width / 2, y0 + qc_width / 2 if (qcomp, qc_elt) in self.jj_lumped_ports: if self.jj_lumped_ports[(qcomp, qc_elt)][1]: # Draw both port and inductor side by side with small gap in between gap = parse_units(self.hfss_options['port_inductor_gap']) x_mid, y_mid = (x_min + x_max) / 2, (y_min + y_max) / 2 if axis == 'x': y_mid_hi = y_mid + gap / 2 y_mid_lo = y_mid - gap / 2 self.render_junction_port(qgeom, x_min, x_max, y_mid_hi, y_max, qc_chip_z, axis) self.render_junction_inductor(qgeom, x_min, x_max, y_min, y_mid_lo, qc_chip_z, axis) elif axis == 'y': x_mid_lo = x_mid - gap / 2 x_mid_hi = x_mid + gap / 2 self.render_junction_port(qgeom, x_mid_hi, x_max, y_min, y_max, qc_chip_z, axis) self.render_junction_inductor(qgeom, x_min, x_mid_lo, y_min, y_max, qc_chip_z, axis) else: # Only draw port self.render_junction_port(qgeom, x_min, x_max, y_min, y_max, qc_chip_z, axis) else: # Only draw inductor self.render_junction_inductor(qgeom, x_min, x_max, y_min, y_max, qc_chip_z, axis)
[docs] def render_junction_port(self, qgeom: pd.Series, xmin: float, xmax: float, ymin: float, ymax: float, z: float, axis: str): """Render a junction as a port with a bounding box given by xmin/xmax and ymin/ymax, a height z, and a horizontal or vertical axis. Args: qgeom (pd.Series): GeoSeries of element properties. xmin (float): Smallest x coordinate xmax (float): Largest x coordinate ymin (float): Smallest y coordinate ymax (float): Largest y coordinate z (float): z coordinate axis (str): Orientation, either 'x' or 'y' """ ansys_options = dict(transparency=0.0) qcomp = self.design._components[qgeom['component']].name qc_elt = get_clean_name(qgeom['name']) port_name = f'Port_{qcomp}_{qc_elt}' impedance = self.jj_lumped_ports[(qcomp, qc_elt)][0] # Draw rectangle for lumped port. self.logger.debug(f'Drawing a rectangle: {port_name}') poly_ansys = self.modeler.draw_rect_corner([xmin, ymin, z], xmax - xmin, ymax - ymin, z, **ansys_options) poly_ansys.make_lumped_port(axis, z0=str(impedance) + 'ohm', name=f'LumpPort_{qcomp}_{qc_elt}') self.modeler.rename_obj(poly_ansys, port_name) # Draw line for lumped port. if axis == 'x': ymid = (ymin + ymax) / 2 start, end = [xmin, ymid, z], [xmax, ymid, z] elif axis == 'y': xmid = (xmin + xmax) / 2 start, end = [xmid, ymin, z], [xmid, ymax, z] lump_line = self.modeler.draw_polyline([start, end], closed=False, **dict(color=(128, 0, 128))) lump_line = lump_line.rename(f'voltage_line_{port_name}') lump_line.show_direction = True
[docs] def render_junction_inductor(self, qgeom: pd.Series, xmin: float, xmax: float, ymin: float, ymax: float, z: float, axis: str): """Render a junction as an inductor with a bounding box given by xmin/xmax and ymin/ymax, a height z, and a horizontal or vertical axis. Args: qgeom (pd.Series): GeoSeries of element properties. xmin (float): Smallest x coordinate xmax (float): Largest x coordinate ymin (float): Smallest y coordinate ymax (float): Largest y coordinate z (float): z coordinate axis (str): Orientation, either 'x' or 'y' """ ansys_options = dict(transparency=0.0) qcomp = self.design._components[qgeom['component']].name qc_elt = get_clean_name(qgeom['name']) qc_name = 'Lj_' + qcomp inductor_name = f'{qc_name}{QAnsysRenderer.NAME_DELIM}{qc_elt}' # Draw rectangle for inductor. self.logger.debug(f'Drawing a rectangle: {inductor_name}') poly_ansys = self.modeler.draw_rect_corner([xmin, ymin, z], xmax - xmin, ymax - ymin, 0, **ansys_options) poly_ansys.make_rlc_boundary(axis, l=qgeom['hfss_inductance'], c=qgeom['hfss_capacitance'], r=qgeom['hfss_resistance'], name='Lj_' + inductor_name) self.modeler.rename_obj(poly_ansys, 'JJ_rect_' + inductor_name) self.assign_mesh.append('JJ_rect_' + inductor_name) # Draw line for inductor. if axis == 'x': ymid = (ymin + ymax) / 2 start, end = [xmin, ymid, z], [xmax, ymid, z] elif axis == 'y': xmid = (xmin + xmax) / 2 start, end = [xmid, ymin, z], [xmid, ymax, z] induc_line = self.modeler.draw_polyline([start, end], closed=False, **dict(color=(128, 0, 128))) induc_line = induc_line.rename('JJ_' + inductor_name + '_') induc_line.show_direction = True
[docs] def metallize(self): """Assign metallic property to all shapes in self.assign_perfE list.""" self.modeler.assign_perfect_E(self.assign_perfE)
[docs] def add_drivenmodal_design(self, name: str, connect: bool = True): """ (deprecated) use new_ansys_design() """ self.logger.warning( 'This method is deprecated. Change your scripts to use new_ansys_design()' ) self.new_ansys_design(name, 'drivenmodal', connect)
[docs] def activate_drivenmodal_design(self, name: str = "MetalHFSSDrivenModal"): """ (deprecated) use activate_ansys_design() """ self.logger.warning( 'This method is deprecated. Change your scripts to use activate_ansys_design()' ) self.activate_ansys_design(name, 'drivenmodal')
[docs] def activate_drivenmodal_setup(self, setup_name_activate: str = None): """ (deprecated) use activate_ansys_setup() """ self.logger.warning( 'This method is deprecated. Change your scripts to use activate_ansys_setup()' ) self.activate_ansys_setup(setup_name_activate)
[docs] def add_drivenmodal_setup(self, name: str = None, freq_ghz: int = None, max_delta_s: float = None, max_passes: int = None, min_passes: int = None, min_converged: int = None, pct_refinement: int = None, basis_order: int = None, *args, **kwargs): """Create a solution setup in Ansys HFSS Driven Modal. If user does not provide arguments, they will be obtained from default_setup dict. Args: name (str, optional): Name of driven modal setup. Defaults to None. freq_ghz (int, optional): Frequency in GHz. Defaults to None. max_delta_s (float, optional): Absolute value of maximum difference in scattering parameter S. Defaults to None. max_passes (int, optional): Maximum number of passes. Defaults to None. min_passes (int, optional): Minimum number of passes. Defaults to None. min_converged (int, optional): Minimum number of converged passes. Defaults to None. pct_refinement (int, optional): Percent refinement. Defaults to None. basis_order (int, optional): Basis order. Defaults to None. """ dsu = self.default_setup.drivenmodal if not name: name = self.parse_value(dsu['name']) if not freq_ghz: freq_ghz = int(self.parse_value(dsu['freq_ghz'])) if not max_delta_s: max_delta_s = float(self.parse_value(dsu['max_delta_s'])) if not max_passes: max_passes = int(self.parse_value(dsu['max_passes'])) if not min_passes: min_passes = int(self.parse_value(dsu['min_passes'])) if not min_converged: min_converged = int(self.parse_value(dsu['min_converged'])) if not pct_refinement: pct_refinement = int(self.parse_value(dsu['pct_refinement'])) if not basis_order: basis_order = int(self.parse_value(dsu['basis_order'])) if self.pinfo: if self.pinfo.design: return self.pinfo.design.create_dm_setup( freq_ghz=freq_ghz, name=name, max_delta_s=max_delta_s, max_passes=max_passes, min_passes=min_passes, min_converged=min_converged, pct_refinement=pct_refinement, basis_order=basis_order)
[docs] def add_eigenmode_design(self, name: str, connect: bool = True): """ (deprecated) use new_ansys_design() """ self.logger.warning( 'This method is deprecated. Change your scripts to use new_ansys_design()' ) self.new_ansys_design(name, 'eigenmode', connect)
[docs] def activate_eigenmode_design(self, name: str = "MetalHFSSEigenmode"): """ (deprecated) use activate_ansys_design() """ self.logger.warning( 'This method is deprecated. Change your scripts to use activate_ansys_design()' ) self.activate_ansys_design(name, 'eigenmode')
[docs] def activate_eigenmode_setup(self, setup_name_activate: str = None): """ (deprecated) use activate_ansys_setup() """ self.logger.warning( 'This method is deprecated. Change your scripts to use activate_ansys_setup()' ) self.activate_ansys_setup(setup_name_activate)
[docs] def add_eigenmode_setup(self, name: str = None, min_freq_ghz: int = None, n_modes: int = None, max_delta_f: float = None, max_passes: int = None, min_passes: int = None, min_converged: int = None, pct_refinement: int = None, basis_order: int = None, *args, **kwargs): """Create a solution setup in Ansys HFSS Eigenmode. If user does not provide arguments, they will be obtained from hfss_options dict. Args: name (str, optional): Name of eigenmode setup. Defaults to "Setup". min_freq_ghz (int, optional): Minimum frequency in GHz. Defaults to 1. n_modes (int, optional): Number of modes. Defaults to 1. max_delta_f (float, optional): Maximum difference in freq between consecutive passes. Defaults to 0.5. max_passes (int, optional): Maximum number of passes. Defaults to 10. min_passes (int, optional): Minimum number of passes. Defaults to 1. min_converged (int, optional): Minimum number of converged passes. Defaults to 1. pct_refinement (int, optional): Percent refinement. Defaults to 30. basis_order (int, optional): Basis order. Defaults to -1. """ esu = self.default_setup.eigenmode if not name: name = self.parse_value(esu['name']) if not min_freq_ghz: min_freq_ghz = int(self.parse_value(esu['min_freq_ghz'])) if not n_modes: n_modes = int(self.parse_value(esu['n_modes'])) if not max_delta_f: max_delta_f = float(self.parse_value(esu['max_delta_f'])) if not max_passes: max_passes = int(self.parse_value(esu['max_passes'])) if not min_passes: min_passes = int(self.parse_value(esu['min_passes'])) if not min_converged: min_converged = int(self.parse_value(esu['min_converged'])) if not pct_refinement: pct_refinement = int(self.parse_value(esu['pct_refinement'])) if not basis_order: basis_order = int(self.parse_value(esu['basis_order'])) if self.pinfo: if self.pinfo.design: return self.pinfo.design.create_em_setup( name=name, min_freq_ghz=min_freq_ghz, n_modes=n_modes, max_delta_f=max_delta_f, max_passes=max_passes, min_passes=min_passes, min_converged=min_converged, pct_refinement=pct_refinement, basis_order=basis_order)
[docs] def edit_eigenmode_setup(self, setup_args: Dict): """User can pass key/values to edit the setup for active eigenmode setup. Args: setup_args (Dict): a Dict with possible keys/values. **setup_args** dict contents: * name (str, optional): Name of eigenmode setup. Defaults to "Setup". * min_freq_ghz (int, optional): Minimum frequency in GHz. Defaults to 1. * n_modes (int, optional): Number of modes. Defaults to 1. * max_delta_f (float, optional): Maximum difference in freq between consecutive passes. Defaults to 0.5. * max_passes (int, optional): Maximum number of passes. Defaults to 10. * pct_refinement (int, optional): Percent refinement. Defaults to 30. * basis_order (int, optional): Basis order. Defaults to -1. Note, that these two are currently NOT implemented: Ansys API named EditSetup not documented for HFSS, and self.pinfo.setup does not have all the property variables used for Setup. * min_passes (int, optional): Minimum number of passes. Defaults to 1. * min_converged (int, optional): Minimum number of converged passes. Defaults to 1. """ if self.pinfo: if self.pinfo.project: if self.pinfo.design: if self.pinfo.design.solution_type == 'Eigenmode': if self.pinfo.setup_name != setup_args.name: self.design.logger.warning( f'The name of active setup={self.pinfo.setup_name} does not match' f'the name of of setup_args.name={setup_args.name}. ' f'To use this method, activate the desired Setup before editing it. The ' f'setup_args was not used to update the active Setup.' ) return for key, value in setup_args.items(): if key == "name": continue #Checked for above. if key == "n_modes": #EditSetup not documented, this is just attempt to use. #args_editsetup = ["NAME:" + setup_args.name,["NumModes:=", setup_args.n_modes]] #self.pinfo.setup._setup_module.EditSetup([setup_args.name, args_editsetup]) if value < 0 or value > 20 or not isinstance( value, int): self.logger.warning( f'Value of n_modes={value} must be integer from 1 to 20.' ) else: self.pinfo.setup.n_modes = value continue if key == "min_freq_ghz": if not isinstance(value, int): self.logger.warning( 'The value for min_freq_ghz should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.min_freq = f'{value}GHz' continue if key == 'max_delta_f': if not isinstance(value, float): self.logger.warning( 'The value for max_delta_f should be float. ' f'The present value is {value}.') else: self.pinfo.setup.delta_f = value continue if key == 'max_passes': if not isinstance(value, int): self.logger.warning( 'The value for max_passes should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.passes = value continue if key == 'pct_refinement': if not isinstance(value, int): self.logger.warning( 'The value for pct_refinement should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.pct_refinement = value continue if key == 'basis_order': if not isinstance(value, int): self.logger.warning( 'The value for basis_order should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.basis_order = value continue self.design.logger.warning( f'In setup_args, key={key}, value={value} is not in pinfo.setup, ' 'the key/value pair from setup_args not added to Setup in Ansys.' ) else: self.logger.warning( 'The design does not have solution type as "Eigenmode". The Setup not updated.' ) else: self.logger.warning( 'A design is not in active project. The Setup not updated.' ) else: self.logger.warning( "Project not available, have you opened a project? Setup not updated." ) else: self.logger.warning( "Have you run connect_ansys()? " "Cannot find a reference to Ansys in QRenderer. Setup not updated. " )
[docs] def edit_drivenmodal_setup(self, setup_args: Dict): """User can pass key/values to edit the setup for active driven modal setup. Args: setup_args (Dict): a Dict with possible keys/values. **setup_args** dict contents: * name (str, optional): Name of eigenmode setup. Defaults to "Setup". * freq_ghz (int, optional): Minimum frequency in GHz. Defaults to 1. * max_passes (int, optional): Maximum number of passes. Defaults to 10. * pct_refinement (int, optional): Percent refinement. Defaults to 30. * basis_order (int, optional): Basis order. Defaults to -1 (1 is "Mixed Order"). Note, that these three are currently NOT implemented: Ansys API named EditSetup not documented for HFSS, and self.pinfo.setup does not have all the property variables used for Setup. * max_delta_s (float, optional): Absolute value of maximum difference in scattering parameter S. Defaults to 0.1. * min_passes (int, optional): Minimum number of passes. Defaults to 1. * min_converged (int, optional): Minimum number of converged passes. Defaults to 1. """ if self.pinfo: if self.pinfo.project: if self.pinfo.design: if self.pinfo.design.solution_type == 'DrivenModal': if self.pinfo.setup_name != setup_args.name: self.design.logger.warning( f'The name of active setup={self.pinfo.setup_name} does not match' f'the name of of setup_args.name={setup_args.name}. ' f'To use this method, activate the desired Setup before editing it. The ' f'setup_args was not used to update the active Setup.' ) return for key, value in setup_args.items(): if key == "name": continue #Checked for above. if key == "freq_ghz": if not isinstance(value, float): self.logger.warning( 'The value for freq_ghz should be an float. ' f'The present value is {value}.') else: self.pinfo.setup.solution_freq = f'{value}GHz' continue if key == 'max_passes': if not isinstance(value, int): self.logger.warning( 'The value for passes should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.passes = value continue if key == 'pct_refinement': if not isinstance(value, int): self.logger.warning( 'The value for pct_refinement should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.pct_refinement = value continue if key == 'basis_order': if not isinstance(value, int): self.logger.warning( 'The value for basis_order should be an int. ' f'The present value is {value}.') else: self.pinfo.setup.basis_order = value continue self.design.logger.warning( f'In setup_args, key={key}, value={value} is not in pinfo.setup, ' 'the key/value pair from setup_args not added to Setup in Ansys.' ) else: self.logger.warning( 'The design does not have solution type as "Driven Modal". The Setup not updated.' ) else: self.logger.warning( 'A design is not in active project. The Setup not updated.' ) else: self.logger.warning( "Project not available, have you opened a project? Setup not updated." ) else: self.logger.warning( "Have you run connect_ansys()? " "Cannot find a reference to Ansys in QRenderer. Setup not updated. " )
[docs] def set_mode(self, mode: int, setup_name: str): """Set the eigenmode in pyEPR for a design with solution_type set to Eigenmode. Args: mode (int): Identify a mode from 1 to n_modes. setup_name (str): Select a setup from the active design. """ if self.pinfo: if self.pinfo.project: if self.pinfo.design: # double parent, becasue self.pinfo.design does not work o_desktop = self.pinfo.design.parent.parent._desktop o_project = o_desktop.SetActiveProject( self.pinfo.project_name) o_design = o_project.GetActiveDesign() if o_design.GetSolutionType() == 'Eigenmode': # The set_mode() method is in HfssEMDesignSolutions # class in pyEPR. # The class HfssEMDesignSolutions is instantiated by # get_setup() and create_em_setup(). setup = self.pinfo.get_setup(setup_name) if 0 < int(mode) <= int(setup.n_modes): setup_solutions = setup.get_solutions() if setup_solutions: setup_solutions.set_mode(mode) else: self.logger.warning( 'Not able to get setup_solutions, ' 'the mode was not set.') else: self.logger.warning( f'The requested mode={mode} is not a valid ' f'(1 to {setup.n_modes}) selection. ' 'The mode was not set.') else: self.logger.warning( 'The design does not have solution type as ' '"Eigenmode". The mode was not set.') else: self.logger.warning('A design is not in active project. ' 'The mode was not set.') else: self.logger.warning( "Project not available, have you opened a project? " "The mode was not set.") else: self.logger.warning( "Have you run connect_ansys()? " "Cannot find a reference to Ansys in QRenderer. " "The mode was not set.")
[docs] def analyze_setup(self, setup_name: str): """Run a specific solution setup in Ansys HFSS. Args: setup_name (str): Name of setup. """ if self.pinfo: setup = self.pinfo.get_setup(setup_name) setup.analyze(setup_name)
[docs] def add_sweep(self, setup_name="Setup", start_ghz=2.0, stop_ghz=8.0, count=101, step_ghz=None, name="Sweep", type="Fast", save_fields=False): """Add a frequency sweep to a driven modal setup. Args: setup_name (str, optional): Name of driven modal simulation setup. Defaults to "Setup". start_ghz (float, optional): Starting frequency of sweep in GHz. Defaults to 2.0. stop_ghz (float, optional): Ending frequency of sweep in GHz. Defaults to 8.0. count (int, optional): Total number of frequencies. Defaults to 101. step_ghz (float, optional): Difference between adjacent frequencies. Defaults to None. name (str, optional): Name of sweep. Defaults to "Sweep". type (str, optional): Type of sweep. Defaults to "Fast". save_fields (bool, optional): Whether or not to save fields. Defaults to False. """ if self.pinfo: setup = self.pinfo.get_setup(setup_name) return setup.insert_sweep(start_ghz=start_ghz, stop_ghz=stop_ghz, count=count, step_ghz=step_ghz, name=name, type=type, save_fields=save_fields)
[docs] def analyze_sweep(self, sweep_name: str, setup_name: str): """Analyze a single sweep within the setup. Args: sweep_name (str): Name of sweep to analyze. setup_name (str): Name of setup to analyze. """ if self.pinfo: setup = self.pinfo.get_setup(setup_name) sweep = setup.get_sweep(sweep_name) sweep.analyze_sweep() self.current_sweep = sweep
[docs] def get_params(self, param_name: Union[list, None] = None): """Get one or more parameters (S, Y, or Z) as a function of frequency. Args: param_name (Union[list, None], optional): Parameters to obtain. Defaults to None. """ if self.current_sweep: freqs, Pcurves = self.current_sweep.get_network_data(param_name) Pparams = pd.DataFrame(Pcurves, columns=freqs / 1e9, index=param_name).transpose() return freqs, Pcurves, Pparams
# yapf: disable
[docs] def get_all_Pparms_matrices(self, matrix_size: int) -> Tuple[ Union[pd.core.frame.DataFrame, None], Union[pd.core.frame.DataFrame, None], Union[pd.core.frame.DataFrame, None]]: #yapf: enable ''' S = scattering matrix, Y = Admittance, Z= impedance. matrix_size should be 1 or larger. This method will get the entire Scattering matrix based on matrix_size. Example:'S21' S matrix: SAB means, excite B, measure A ''' s_param_name = [] y_param_name = [] z_param_name = [] if matrix_size < 1: return None, None, None for excite in range(1, matrix_size + 1): for measure in range(1, matrix_size + 1): s_param_name.append(f'S{measure}{excite}') y_param_name.append(f'Y{measure}{excite}') z_param_name.append(f'Z{measure}{excite}') dummy_freqs, dummy_Pcurves, S_Pparams = self.get_params(s_param_name) dummy_freqs, dummy_Pcurves, Y_Pparams = self.get_params(y_param_name) dummy_freqs, dummy_Pcurves, Z_Pparams = self.get_params(z_param_name) return S_Pparams, Y_Pparams, Z_Pparams
[docs] def plot_params(self, param_name: Union[list, None] = None): """Plot one or more parameters (S, Y, or Z) as a function of frequency. S = scattering matrix, Y = Admittance, Z= impedance. Args: param_name (Union[list, None], optional): Parameters to plot. Defaults to None. """ freqs, Pcurves, Pparams = self.get_params(param_name) if Pparams is not None: fig, axs = plt.subplots(1, 2, figsize=(10, 6)) Pparams.apply(lambda x: 20 * np.log10(np.abs(x))).plot(ax=axs[0]) axs[0].set_xlabel('Frequency (GHz)') axs[0].set_ylabel('Amplitude (dB)') Pparams.apply(lambda x: np.angle(x)).plot(ax=axs[1]) axs[1].set_xlabel('Frequency (GHz)') axs[1].set_ylabel('Phase (rad.)') for ax in axs: ax.autoscale() return Pparams, fig
[docs] def distributed_analysis(self): """Returns class containing info on Hamiltonian parameters from HFSS simulation. Returns: DistributedAnalysis: A class from pyEPR which does DISTRIBUTED ANALYSIS of layout and microwave results. It is the main computation class & interface with HFSS. This class defines a DistributedAnalysis object which calculates and saves Hamiltonian parameters from an HFSS simulation. It allows one to calculate dissipation. """ if self.pinfo: return epr.DistributedAnalysis(self.pinfo)
[docs] def get_convergences(self, variation: str = None): """Get convergence for convergence_t, convergence_f, and text from GUI for solution data. Args: variation (str, optional): Information from pyEPR; variation should be in the form variation = "scale_factor='1.2001'". Defaults to None. Returns: tuple[pandas.DataFrame, pandas.DataFrame, str]: 1st DataFrame: Convergence_t 2nd DataFrame: Convergence_f 3rd str: Text from GUI of solution data. """ if self.pinfo: convergence_t, text = self.pinfo.setup.get_convergence(variation) convergence_f = self.get_f_convergence([]) # TODO; Fix variation [] return convergence_t, convergence_f, text
[docs] def plot_convergences(self, variation: str = None, fig: mpl.figure.Figure = None): """ (deprecated) use EPRanalysis.plot_convergences() """ print( 'This method is deprecated. Change your scripts to use the equivalent method ' 'plot_convergence() that has been moved inside the EPRanalysis class.' )
[docs] def get_f_convergence(self, variation: str = None, save_csv: bool = True): """Create a report inside HFSS to plot the converge of frequency and style it. Saves report to csv file. .. code-block:: text re(Mode(1)) [g] re(Mode(2)) [g] re(Mode(3)) [g] Pass [] 1 4.643101 4.944204 5.586289 2 5.114490 5.505828 6.242423 3 5.278594 5.604426 6.296777 Args: variation ('str', optional): Information from pyEPR; variation should be in the form variation = "scale_factor='1.2001'". Defaults to None. save_csv (bool, optional): Save to file? Defaults to True. Returns: pd.DataFrame: Returns a convergence vs pass number of the eigenemode frequencies. """ if self.pinfo: o_design = self.pinfo.design setup = self.pinfo.setup if not o_design.solution_type == 'Eigenmode': return None report = o_design._reporter # reporter OModule for Ansys # Create report n_modes = int(setup.n_modes) ycomp = [f"re(Mode({i}))" for i in range(1, 1 + n_modes)] params = ["Pass:=", ["All"]] + variation report_name = "Freq. vs. pass" if report_name in report.GetAllReportNames(): report.DeleteReports([report_name]) solutions = setup.get_solutions() solutions.create_report(report_name, "Pass", ycomp, params, pass_name='AdaptivePass') # Properties of lines curves = [ f"{report_name}:re(Mode({i})):Curve1" for i in range(1, 1 + n_modes) ] set_property(report, 'Attributes', curves, 'Line Width', 3) set_property(report, 'Scaling', f"{report_name}:AxisY1", 'Auto Units', False) set_property(report, 'Scaling', f"{report_name}:AxisY1", 'Units', 'g') set_property(report, 'Legend', f"{report_name}:Legend", 'Show Solution Name', False) if save_csv: # Save try: path = Path().absolute( ) / 'hfss_eig_f_convergence.csv' # TODO: Determine better path report.ExportToFile(report_name, path) self.logger.info(f'Saved convergences to {path}') return pd.read_csv(path, index_col=0) except Exception as e: self.logger.error( f"Error could not save and export hfss plot to {path}.\ Is the plot made in HFSS with the correct name.\ Check the HFSS error window. \t Error = {e}") return None