# -*- 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 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