# -*- 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.
# pylint: disable=too-many-lines
from typing import List, Tuple, Union
import re
import os
from pathlib import Path
import math
import geopandas
import numpy as np
import pandas as pd
from numpy.linalg import norm
from collections import defaultdict
from platform import system
from scipy.spatial import distance
import shapely
import pyEPR as epr
from pyEPR.ansys import parse_units, HfssApp, release
from qiskit_metal.draw.utility import to_vec3D
from qiskit_metal.draw.basic import is_rectangle
from qiskit_metal.renderers.renderer_base import QRendererAnalysis
from qiskit_metal.toolbox_metal.parsing import is_true
from qiskit_metal.designs.design_base import QDesign
from qiskit_metal import Dict
from .. import config
if not config.is_building_docs():
from qiskit_metal.toolbox_python.utility_functions import (
toggle_numbers,
bad_fillet_idxs,
)
def good_fillet_idxs(coords: list,
fradius: float,
precision: int = 9,
isclosed: bool = False):
"""
Get list of vertex indices in a linestring (isclosed = False) or polygon (isclosed = True)
that can be filleted based on proximity to neighbors.
Args:
coords (list): Ordered list of tuples of vertex coordinates.
fradius (float): User-specified fillet radius from QGeometry table.
precision (int, optional): Digits of precision used for round(). Defaults to 9.
isclosed (bool, optional): Boolean denoting whether the shape is a linestring or
polygon. Defaults to False.
Returns:
list: List of indices of vertices that can be filleted.
"""
if isclosed:
return toggle_numbers(
bad_fillet_idxs(coords, fradius, precision, isclosed=True),
len(coords))
return toggle_numbers(
bad_fillet_idxs(coords, fradius, precision, isclosed=False),
len(coords))[1:-1]
def get_clean_name(name: str) -> str:
"""Create a valid variable name from the given one by removing having it
begin with a letter or underscore followed by an unlimited string of
letters, numbers, and underscores.
Args:
name (str): Initial, possibly unusable, string to be modified.
Returns:
str: Variable name consistent with Python naming conventions.
"""
# Remove invalid characters
name = re.sub("[^0-9a-zA-Z_]", "", name)
# Remove leading characters until we find a letter or underscore
name = re.sub("^[^a-zA-Z_]+", "", name)
return name
[docs]
class QAnsysRenderer(QRendererAnalysis):
"""Extends QRenderer to export designs to Ansys using pyEPR. The methods
which a user will need for Ansys export should be found within this class.
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
* project_path: None -- Default project path; if None --> get active
* max_mesh_length_port: '7um' -- Maximum mesh length for Ports in Eigenmode Simulations
* 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
* wb_threshold:'400um' -- the minimum distance between two vertices of a path for a
wirebond to be added.
* wb_offset:'0um' -- offset distance for wirebond placement (along the direction
of the cpw)
* wb_size: 3 -- scalar which controls the width of the wirebond (wb_size * path['width'])
"""
#: Default options, over-written by passing ``options` dict to render_options.
#: Type: Dict[str, str]
# yapf: disable
default_options = Dict(
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
# bounding_box_scale_x = 1.2, # Ratio of 'main' chip width to bounding box width
# bounding_box_scale_y = 1.2, # Ratio of 'main' chip length to bounding box length
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
wb_threshold = '400um',
wb_offset = '0um',
wb_size = 5,
plot_ansys_fields_options = Dict(
name="NAME:Mag_E1",
UserSpecifyName='0',
UserSpecifyFolder='0',
QuantityName= "Mag_E",
PlotFolder= "E Field",
StreamlinePlot= "False",
AdjacentSidePlot= "False",
FullModelPlot= "False",
IntrinsicVar= "Phase=\'0deg\'",
PlotGeomInfo_0= "1",
PlotGeomInfo_1= "Surface",
PlotGeomInfo_2= "FacesList",
PlotGeomInfo_3= "1",
),
)
"""Default options"""
# yapf: enable
NAME_DELIM = r"_"
"""Name delimiter"""
name = "ansys"
"""Name"""
default_setup = Dict(
drivenmodal=Dict(
name="Setup",
freq_ghz="5.0",
max_delta_s="0.1",
max_passes="10",
min_passes="1",
min_converged="1",
pct_refinement="30",
basis_order="1",
),
eigenmode=Dict(
name="Setup",
min_freq_ghz="1",
n_modes="1",
max_delta_f="0.5",
max_passes="10",
min_passes="1",
min_converged="1",
pct_refinement="30",
basis_order="-1",
),
q3d=Dict(
name="Setup",
freq_ghz="5.0",
save_fields="False",
enabled="True",
max_passes="15",
min_passes="2",
min_converged_passes="2",
percent_error="0.5",
percent_refinement="30",
auto_increase_solution_order="True",
solution_order="High",
solver_type="Iterative",
),
port_inductor_gap=
"10um", # spacing between port and inductor if junction is drawn both ways
)
"""Default setup."""
# When additional columns are added to QGeometry, this is the example to populate it.
# e.g. element_extensions = dict(
# base=dict(color=str, klayer=int),
# path=dict(thickness=float, material=str, perfectE=bool),
# poly=dict(thickness=float, material=str), )
"""Element extensions dictionary element_extensions = dict() from base class"""
# Add columns to junction table during QAnsysRenderer.load()
# element_extensions is now being populated as part of load().
# Determined from element_table_data.
# Dict structure MUST be same as element_extensions!!!!!!
# This dict will be used to update QDesign during init of renderer.
# Keeping this as a cls dict so could be edited before renderer is instantiated.
# To update component.options junction table.
element_table_data = dict(
path=dict(wire_bonds=False),
junction=dict(
inductance=default_options["Lj"],
capacitance=default_options["Cj"],
resistance=default_options["_Rj"],
mesh_kw_jj=parse_units(default_options["max_mesh_length_jj"]),
),
)
"""Element table data."""
def __init__(self, design: "QDesign", initiate=True, options: Dict = None):
"""Create a QRenderer for Ansys.
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.
"""
# Variables to connect to Ansys
self._rapp = None
self._rdesktop = None
# Initialize renderer
super().__init__(design=design, initiate=initiate, options=options)
# Default behavior is to render all components unless a strict subset was chosen
self.render_everything = True
self._pinfo = None
@property
def initialized(self):
"""Returns True if initialized, False otherwise."""
if self._pinfo:
if self._pinfo.project:
try:
# this is because after previous pyEPR close(),
# the pinfo.project becomes None, but the type is not None (method)
# TODO: fix where appropriate, then remove this patch.
self._pinfo.project.name
except AttributeError:
return False
return True
return False
@property
def rapp(self):
return self._rapp
@rapp.setter
def rapp(self, app_com):
if self._rapp:
self._rapp.release()
self._rapp = app_com
@property
def rdesktop(self):
return self._rdesktop
@rdesktop.setter
def rdesktop(self, desktop_com):
if self._rdesktop:
self._rdesktop.release()
self._rdesktop = desktop_com
def _initiate_renderer(self):
"""
Open a session of the default Ansys EDT.
Establishes the connection to the App and Desktop only.
"""
# test if ansys is open
# import psutil
# booted = False
# for proc in psutil.process_iter():
# if 'ansysedt' in proc.name():
# booted = True
# if not booted:
# self._open_ansys(*args, **kwargs)
# need to make it so that it waits for the Ansys boot to end
# after opening, should establish a connection (able to create a new project)
self.rapp = HfssApp()
self.rdesktop = self.rapp.get_app_desktop()
if self.rdesktop.project_count() == 0:
self.rdesktop.new_project()
self.connect_ansys() # TODO: can this be done differently?
# return True to indicate successful completion
return True
def _close_renderer(self):
"""Not used by the gds renderer at this time. only returns True.
Returns:
bool: True
"""
# wipe local variables
self.epr_distributed_analysis = None
self.epr_quantum_analysis = None
# close COM connections to ansys
if self.rdesktop is not None:
self.rdesktop.release()
if self.rapp is not None:
self.rapp.release()
if self.pinfo:
self.disconnect_ansys()
return True
[docs]
def close(self):
"""Alias of _close_renderer()
Returns:
bool: True
"""
return self._close_renderer()
[docs]
def open_ansys(
self,
path: str = None,
executable: str = "reg_ansysedt.exe",
path_var: str = "ANSYSEM_ROOT202",
):
"""Alternative method to open an Ansys session that allows to specify
which version to use. Default is version 2020 R2, but can be overridden.
Args:
path (str): Path to the Ansys executable. Defaults to None
executable (str): Name of the ansys executable. Defaults to 'reg_ansysedt.exe'
path_var (str): Name of the OS environment variable that contains the path to the
Ansys executable. Only used when path=None.
Defaults to 'ANSYSEM_ROOT202' (Ansys ver. 2020 R2)
"""
if not system() == "Windows":
self.logger.warning(
"You are using %s, but this is a renderer to Ansys, which only runs on Windows. "
"Expect any sort of Errors if you try to work with this renderer beyond this point."
% system())
import subprocess
if path is None:
try:
path = os.environ[path_var]
except KeyError:
self.logger.error(
"environment variable %s not found. Is Ansys 2020 R2 installed on this "
"machine? If yes, then create said environment variable. If you have a "
"different version of Ansys, then pass to open_ansys() the path to its "
"binary, or the env var that stores it." % path_var)
raise
else:
path = os.path.abspath(path)
cmdlist = [os.path.sep.join([path, executable]), "-shortcut"]
subprocess.call(cmdlist, cwd=path)
[docs]
def connect_ansys(
self,
project_path: str = None,
project_name: str = None,
design_name: str = None,
):
"""If none of the optional parameters are provided: connects to the
Ansys COM, then checks for, and grab if present, an active project,
design, and design setup.
If the optional parameters are provided: if present, opens the project file and design in
Ansys.
Args:
project_path (str, optional): Path without file name
project_name (str, optional): File name (with or without extension)
design_name (str, optional): Name of the default design to open from the project file
"""
if not system() == "Windows":
self.logger.warning(
"You are using %s, but this is a renderer to Ansys, which only runs on Windows. "
"Expect any sort of Errors if you try to work with this renderer beyond this point."
% system())
# pyEPR does not like extensions
if project_name:
project_name = project_name.replace(".aedt", "")
# open connection through pyEPR
import pythoncom
try:
self._pinfo = epr.ProjectInfo(
do_connect=True,
project_path=self._options["project_path"]
if not project_path else project_path,
project_name=self._options["project_name"]
if not project_name else project_name,
design_name=self._options['design_name']
if not design_name else design_name)
except pythoncom.com_error as error: # pylint: disable=no-member
print("com_error: ", error)
hr, msg, exc, arg = error.args
if (msg == "Invalid class string"
): # and hr == -2147221005 and exc is None and arg is None
self.logger.error(
"pyEPR cannot find the Ansys COM. Ansys installation might not have registered it. "
"To verify if this is the problem, execute the following: "
"`print(win32com.client.Dispatch('AnsoftHfss.HfssScriptInterface'))` "
"If the print-out is not `<COMObject ...>` then Ansys COM is not registered, "
"and you will need to look into correcting your Ansys installation."
)
raise error
[docs]
def disconnect_ansys(self):
"""Disconnect Ansys."""
if self.pinfo:
self.pinfo.disconnect()
else:
self.logger.warning(
"This renderer appears to be already disconnected from Ansys")
[docs]
def new_ansys_project(self):
"""Creates a new empty project in Ansys."""
here = HfssApp()
here.get_app_desktop().new_project()
[docs]
def connect_ansys_design(self, design_name: str = None):
"""Used to switch between existing designs.
Args:
design_name (str, optional): Name within the active project. Defaults to None.
"""
if self.pinfo:
if self.pinfo.project:
all_designs_names = self.pinfo.project.get_design_names()
if design_name not in all_designs_names:
self.logger.warning(
f"The design_name={design_name} is not in project. Connection did not happen."
)
return
try:
self.pinfo.connect_design(design_name)
self.pinfo.connect_setup()
except AttributeError:
self.logger.error(
"Please install a more recent version of pyEPR (>=0.8.4.3)"
)
else:
self.logger.warning(
"Either you do not have a project loaded in Ansys, or you are not connected "
"to it. Try executing hfss.connect_ansys(), or creating a new Ansys project. "
"Also check the help file and other guide notebooks")
else:
self.logger.warning(
"It does not look like you are connected to Ansys. Please use connect_ansys() "
"and make sure self.pinfo is set. There must be a project open in Ansys first."
)
[docs]
def get_active_design_name(self):
"""Returns the name of the Ansys Design Object
Returns:
(str): Name of the active Ansys Design
"""
if self.pinfo:
if self.pinfo.project:
return self.pinfo.project.get_active_design().name
@property
def pinfo(self) -> epr.ProjectInfo:
"""Project info for Ansys renderer (class: pyEPR.ProjectInfo)."""
return self._pinfo
@property
def modeler(self):
"""The modeler from pyEPR HfssModeler.
Returns:
pyEPR.ansys.HfssModeler: Reference to design.HfssModeler in Ansys.
"""
if self.pinfo:
if self.pinfo.design:
return self.pinfo.design.modeler
[docs]
def plot_ansys_fields(self, *args, **kwargs):
"""
(deprecated) use plot_fields()
"""
self.logger.warning(
"This method is deprecated. Change your scripts to use plot_fields()"
)
return self.plot_fields(*args, **kwargs)
[docs]
def plot_fields(
self,
object_name: str,
name: str = None,
UserSpecifyName: int = None,
UserSpecifyFolder: int = None,
QuantityName: str = None,
PlotFolder: str = None,
StreamlinePlot: bool = None,
AdjacentSidePlot: bool = None,
FullModelPlot: bool = None,
IntrinsicVar: str = None,
PlotGeomInfo_0: int = None,
PlotGeomInfo_1: str = None,
PlotGeomInfo_2: str = None,
PlotGeomInfo_3: int = None,
):
"""Plot fields in Ansys. The options are populated by the component's
options.
Args:
object_name (str): Used to plot on faces of.
name (str, optional): "NAME:<PlotName>" Defaults to None.
UserSpecifyName (int, optional): 0 if default name for plot is used, 1 otherwise.
Defaults to None.
UserSpecifyFolder (int, optional): 0 if default folder for plot is used, 1 otherwise.
Defaults to None.
QuantityName (str, optional): Type of plot to create. Possible values are
Mesh plots - "Mesh";
Field plots - "Mag_E", "Mag_H", "Mag_Jvol", "Mag_Jsurf","ComplexMag_E",
"ComplexMag_H", "ComplexMag_Jvol", "ComplexMag_Jsurf", "Vector_E", "Vector_H",
"Vector_Jvol", "Vector_Jsurf", "Vector_RealPoynting","Local_SAR", "Average_SAR".
Defaults to None.
PlotFolder (str, optional): Name of the folder to which the plot should be added.
Possible values are: "E Field", "H Field", "Jvol", "Jsurf", "SARField", and
"MeshPlots". Defaults to None.
StreamlinePlot (bool, optional): Passed to CreateFieldPlot. Defaults to None.
AdjacentSidePlot (bool, optional): Passed to CreateFieldPlot. Defaults to None.
FullModelPlot (bool, optional): Passed to CreateFieldPlot. Defaults to None.
IntrinsicVar (str, optional): Formatted string that specifies the frequency and phase
at which to make the plot. For example: "Freq='1GHz' Phase='30deg'".
Defaults to None.
PlotGeomInfo_0 (int, optional): 0th entry in list for "PlotGeomInfo:=",
<PlotGeomArray>. Defaults to None.
PlotGeomInfo_1 (str, optional): 1st entry in list for "PlotGeomInfo:=",
<PlotGeomArray>. Defaults to None.
PlotGeomInfo_2 (str, optional): 2nd entry in list for "PlotGeomInfo:=",
<PlotGeomArray>. Defaults to None.
PlotGeomInfo_3 (int, optional): 3rd entry in list for "PlotGeomInfo:=",
<PlotGeomArray>. Defaults to None.
Returns:
NoneType: Return information from oFieldsReport.CreateFieldPlot().
The method CreateFieldPlot() always returns None.
"""
self.modeler._modeler.ShowWindow()
if not self.pinfo:
self.logger.warning("pinfo is None.")
return
if self.pinfo.design:
if not self.pinfo.design._fields_calc:
self.logger.warning("The _fields_calc in design is None.")
return
if not self.pinfo.design._modeler:
self.logger.warning("The _modeler in design is None.")
return
else:
self.logger.warning("The design in pinfo is None.")
return
if not self.pinfo.setup:
self.logger.warning("The setup in pinfo is None.")
return
# TODO: This is just a prototype - should add features and flexibility.
oFieldsReport = (self.pinfo.design._fields_calc
) # design.GetModule("FieldsReporter")
oModeler = self.pinfo.design._modeler # design.SetActiveEditor("3D Modeler")
setup = self.pinfo.setup
# Object ID - use to plot on faces of
object_id = oModeler.GetObjectIDByName(object_name)
# Can also use hfss.pinfo.design._modeler.GetFaceIDs("main")
paf = self.options["plot_ansys_fields_options"]
if not name:
name = self.parse_value(paf["name"])
# Name of the solution setup and solution formatted as:"<SolveSetupName> : <WhichSolution>",
# where <WhichSolution> can be "Adaptive_<n>", "LastAdaptive", or "PortOnly".
# HFSS requires a space on either side of the ‘:’ character.
# If it is missing, the plot will not be created.
SolutionName = f"{setup.name} : LastAdaptive"
if not UserSpecifyName:
UserSpecifyName = int(self.parse_value(paf["UserSpecifyName"]))
if not UserSpecifyFolder:
UserSpecifyFolder = int(self.parse_value(paf["UserSpecifyFolder"]))
if not QuantityName:
QuantityName = self.parse_value(paf["QuantityName"])
if not PlotFolder:
PlotFolder = self.parse_value(paf["PlotFolder"])
if not StreamlinePlot:
StreamlinePlot = is_true(self.parse_value(paf["StreamlinePlot"]))
if not AdjacentSidePlot:
AdjacentSidePlot = is_true(self.parse_value(
paf["AdjacentSidePlot"]))
if not FullModelPlot:
FullModelPlot = is_true(self.parse_value(paf["FullModelPlot"]))
if not IntrinsicVar:
IntrinsicVar = self.parse_value(paf["IntrinsicVar"])
if not PlotGeomInfo_0:
PlotGeomInfo_0 = int(self.parse_value(paf["PlotGeomInfo_0"]))
if not PlotGeomInfo_1:
PlotGeomInfo_1 = self.parse_value(paf["PlotGeomInfo_1"])
if not PlotGeomInfo_2:
PlotGeomInfo_2 = self.parse_value(paf["PlotGeomInfo_2"])
if not PlotGeomInfo_3:
PlotGeomInfo_3 = int(self.parse_value(paf["PlotGeomInfo_3"]))
# used to pass to CreateFieldPlot
# Copied from pdf at http://www.ece.uprm.edu/~rafaelr/inel6068/HFSS/scripting.pdf
# <PlotGeomArray>Array(<NumGeomTypes>, <GeomTypeData>,<GeomTypeData>, ...)
# For example:
# Array(4, "Volume", "ObjList", 1, "Box1","Surface", "FacesList", 1, "12", "Line", 1,"Polyline1",
# "Point", 2, "Point1", "Point2"
PlotGeomInfo = [
PlotGeomInfo_0,
PlotGeomInfo_1,
PlotGeomInfo_2,
PlotGeomInfo_3,
str(object_id),
]
# yapf: disable
args_list = [
name ,
"SolutionName:=" , SolutionName, # name of the setup
"UserSpecifyName:=" , UserSpecifyName ,
"UserSpecifyFolder:=", UserSpecifyFolder,
"QuantityName:=" , QuantityName,
"PlotFolder:=" , PlotFolder,
"StreamlinePlot:=" , StreamlinePlot,
"AdjacentSidePlot:=" , AdjacentSidePlot,
"FullModelPlot:=" , FullModelPlot,
"IntrinsicVar:=" , IntrinsicVar,
"PlotGeomInfo:=" , PlotGeomInfo,
]
# yapf: enable
return oFieldsReport.CreateFieldPlot(args_list, "Field")
[docs]
def plot_ansys_delete(self, names: list):
"""
(deprecated) Use clear_fields()
"""
self.logger.warning(
"This method is deprecated. Change your scripts to use clear_fields()"
)
self.clear_fields(names)
[docs]
def clear_fields(self, names: list):
"""
Delete field plots from modeler window in Ansys.
Does not throw an error if names are missing.
Can give multiple names, for example:
hfss.plot_ansys_delete(['Mag_E1', 'Mag_E1_2'])
Args:
names (list): Names of plots to delete from modeler window.
"""
if not names:
names = list(self.pinfo.design._fields_calc.GetFieldPlotNames())
return self.pinfo.design._fields_calc.DeleteFieldPlot(names)
[docs]
def add_message(self, msg: str, severity: int = 0):
"""Add message to Message Manager box in Ansys.
Args:
msg (str): Message to add.
severity (int): 0 = Informational, 1 = Warning, 2 = Error, 3 = Fatal.
"""
self.pinfo.design.add_message(msg, severity)
[docs]
def save_screenshot(self, path: str = None, show: bool = True):
"""Save the screenshot.
Args:
path (str, optional): Path to save location. Defaults to None.
show (bool, optional): Whether or not to display the screenshot. Defaults to True.
Returns:
pathlib.WindowsPath: path to png formatted screenshot.
"""
self.modeler._modeler.ShowWindow()
try:
return self.pinfo.design.save_screenshot(path, show)
except AttributeError:
self.logger.error(
"Please install a more recent version of pyEPR (>=0.8.4.3)")
[docs]
def execute_design(
self,
design_name: str,
solution_type: str,
vars_to_initialize: Dict,
force_redraw: bool = False,
**design_selection,
) -> str:
"""It wraps the render_design() method to
1. skip rendering if the "selection" of components is left empty (re-uses selected design)
2. force design clearing and redraw if force_Redraw is set
Args:
design_name (str): Name to assign to the renderer design
solution_type (str): eigenmode, capacitive or drivenmodal
vars_to_initialize (Dict): Variables to initialize, i.e. Ljx, Cjx
force_redraw (bool, optional): Force re-render the design. Defaults to False.
Returns:
str: final design name (a suffix might have been added to the provided name,
in case of conflicts)
"""
# If a selection of components is not specified, use the active renderer-design
if "selection" in design_selection:
if design_selection["selection"] is None:
try:
return self.pinfo.design.name
except AttributeError:
# if no design exists, then we will proceed and render the full design instead
pass
# either create a new one, or clear the active one, depending on force_redraw.
if force_redraw and (design_name
in self.pinfo.project.get_design_names()):
self.activate_ansys_design(design_name, solution_type)
self.clean_active_design()
else:
self.new_ansys_design(design_name, solution_type)
self.set_variables(vars_to_initialize)
self.render_design(**design_selection)
return self.pinfo.design.name
[docs]
def new_ansys_design(self,
design_name: str,
solution_type: str,
connect: bool = True):
"""Add an Ansys design with the given name to the Ansys project.
Valid solutions_type values are: 'capacitive' (q3d), 'eignemode' and 'drivenmodal' (hfss)
Args:
design_name (str): name of the Design to be created in Ansys
solution_type (str): defines type of Design and solution to be created in Ansys
connect (bool, optional): Should we connect qiskit-metal to this Ansy design? Defaults to True.
Returns(pyEPR.ansys.HfssDesign): The pointer to the design within Ansys.
"""
if self.pinfo:
try:
if solution_type == "capacitive":
adesign = self.pinfo.project.new_q3d_design(design_name)
elif solution_type == "eigenmode":
adesign = self.pinfo.project.new_em_design(design_name)
elif solution_type == "drivenmodal":
adesign = self.pinfo.project.new_dm_design(design_name)
else:
self.logger.error(
f"The solution_type = {solution_type} is not supported by this renderer"
)
except AttributeError:
if self.pinfo.project is None:
self.logger.error("Project not found")
else:
self.logger.error(
"Please install a more recent version of pyEPR (>=0.8.4.4)"
)
raise
if connect:
self.connect_ansys_design(adesign.name)
return adesign
else:
self.logger.info(
"You have to first connect to Ansys and to a project "
"before creating a new design. You can use renderer.connect_ansys()"
)
[docs]
def activate_ansys_design(self,
design_name: str,
solution_type: str = None):
"""Select a design with the given name from the open project.
If the design exists, that will be added WITHOUT altering the suffix of the design name.
Args:
name (str): Name of the new Ansys design
"""
if self.pinfo:
if self.pinfo.project:
try:
names_in_design = self.pinfo.project.get_design_names()
except AttributeError:
self.logger.error(
"Please install a more recent version of pyEPR (>=0.8.4.5)"
)
if design_name in names_in_design:
self.pinfo.connect_design(design_name)
oDesktop = (self.pinfo.design.parent.parent._desktop
) # self.pinfo.design does not work
oProject = oDesktop.SetActiveProject(
self.pinfo.project_name)
oDesign = oProject.SetActiveDesign(design_name)
current_solution_type = self.pinfo.design.solution_type.lower(
)
if current_solution_type == "q3d":
current_solution_type = "capacitive"
if (current_solution_type != solution_type and
solution_type is not None):
self.logger.warning(
f"The design_name={design_name} already exists, but it has solution_type=="
f"{current_solution_type}, which is different from the requested=={solution_type}. "
f"If you want a design with solution type=={solution_type}, please change the name "
"requested for your design to one that does not exist. Alternatively, manually modify "
f"the solution_type for design {design_name} from the Ansys GUI."
)
else:
self.logger.warning(
f"The design_name={design_name} was not in active project. "
f"Designs in active project are: \n{names_in_design}. "
"A new design will be added to the project. ")
if solution_type is not None:
adesign = self.new_ansys_design(
design_name=design_name,
solution_type=solution_type,
connect=True,
)
else:
self.logger.error(
"Please specify the solution_type, to determine what design to create"
)
else:
self.logger.warning(
"Project not found, have you opened a project?")
else:
self.logger.warning(
"Have you run start()? Cannot find a reference to Ansys in QRenderer."
)
[docs]
def new_ansys_setup(self, name: str, **other_setup):
"""Determines the appropriate setup to be created based on the pinfo.design.solution_type.
make sure to set this variable before executing this method
Args:
name (str): name to give to the new setup
Returns:
pyEPR.ansys.HfssEMSetup: Pointer to the ansys setup object
"""
# TODO: only use activate_ansys_setup?
if self.pinfo:
if self.pinfo.design:
if "reuse_setup" in other_setup:
if other_setup["reuse_setup"]:
# delete_setup will check if setup exists, before deleting.
self.pinfo.design.delete_setup(name)
if self.pinfo.design.solution_type == "Eigenmode":
setup = self.add_eigenmode_setup(name, **other_setup)
elif self.pinfo.design.solution_type == "DrivenModal":
setup = self.add_drivenmodal_setup(name, **other_setup)
elif self.pinfo.design.solution_type == "Q3D":
setup = self.add_q3d_setup(name, **other_setup)
return setup
[docs]
def initialize_eigenmode(self, vars: Dict = {}, **kwargs):
"""Any task that needs to occur before running a simulation, such as creating a setup
Args:
vars (Dict, optional): list of parametric variables to set in the renderer.
Defaults to {}.
Returns:
str: Name of the setup that has been updated
"""
self.set_variables(vars)
setup = self.new_ansys_setup(**kwargs) # TODO: activate_ansys_setup?
return setup.name
[docs]
def initialize_drivenmodal(self,
sweep_setup: Dict,
vars: Dict = {},
**kwargs):
"""Any task that needs to occur before running a simulation, such as creating a setup
Args:
sweep_setup (Dict): list of parametric variables to set the frequency sweep.
vars (Dict, optional): list of parametric variables to set in the renderer.
Defaults to {}.
Returns:
str: Name of the setup that has been updated
"""
self.set_variables(vars)
setup = self.new_ansys_setup(**kwargs) # TODO: activate_ansys_setup?
sweep = self.add_sweep(setup.name, **sweep_setup)
return setup.name, sweep.name
[docs]
def activate_ansys_setup(self, setup_name: str):
"""For active design, either get existing setup, make new setup with name,
or make new setup with default name.
Args:
setup_name (str, optional): If name exists for setup, then have pinfo reference it.
If name for setup does not exist, create a new setup with the name.
If name is None, create a new setup with default name.
"""
if self.pinfo:
if self.pinfo.project:
if self.pinfo.design:
# look for setup name, if not there, then add a new one
if setup_name:
all_setup_names = self.pinfo.design.get_setup_names()
self.pinfo.setup_name = setup_name
if setup_name in all_setup_names:
# When name is given and in design. So have pinfo reference existing setup.
self.pinfo.setup = self.pinfo.get_setup(setup_name)
else:
# When name is given, but not in design. So make a new setup with given name.
self.logger.warning(
f"The setup_name={setup_name} was not in active design. "
f"Setups in active design are: \n{all_setup_names}. "
"A new setup will default values will be added to the design. "
)
self.pinfo.setup = self.new_ansys_setup(
name=setup_name)
else:
self.logger.warning(f"Please specify a setup_name.")
else:
self.logger.warning(
"Design not found in selected project, have you opened a design?"
)
else:
self.logger.warning(
"Project not found, have you opened a project?")
else:
self.logger.warning(
"Have you run connect_ansys()? Cannot find a reference to Ansys in QRenderer."
)
[docs]
def render_design(
self,
selection: Union[list, None] = None,
open_pins: 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.
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.
Chip_subtract_dict consists of component names (keys) and a set of all elements within each component that
will eventually be subtracted from the ground plane. Add objects that are perfect conductors and/or have
meshing to self.assign_perfE and self.assign_mesh, respectively; both are initialized as empty lists. Similarly,
if the object is a port in an eigenmode simulation, add it to self.assign_port_mesh, which is initialized
as an empty list. Note that these objects are "refreshed" each time render_design is called (as opposed to
in the init function) to clear QAnsysRenderer of any leftover items from the last call to render_design.
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.
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 QDesign, 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.
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.render_tables()
self.add_endcaps(open_pins)
self.render_chips(box_plus_buffer=box_plus_buffer)
self.subtract_from_ground()
self.add_mesh()
def render_chip(self):
pass
[docs]
def render_component(self):
pass
[docs]
def render_tables(self, skip_junction: bool = False):
"""
Render components in design grouped by table type (path, poly, or junction).
"""
for table_type in self.design.qgeometry.get_element_types():
if table_type != "junction" or not skip_junction:
self.render_components(table_type)
[docs]
def render_components(self, table_type: str):
"""
Render components by breaking them down into individual elements.
Args:
table_type (str): Table type (poly, path, or junction).
"""
table = self.design.qgeometry.tables[table_type]
if self.case == 0: # Render a subset of components using mask
mask = table["component"].isin(self.qcomp_ids)
table = table[mask]
for _, qgeom in table.iterrows():
self.render_element(qgeom, bool(table_type == "junction"))
if table_type == "path":
self.auto_wirebonds(table)
[docs]
def render_element(self, qgeom: pd.Series, is_junction: bool):
"""Render an individual shape whose properties are listed in a row of
QGeometry table. Junction elements are handled separately from non-
junction elements, as the former consist of two rendered shapes, not
just one.
Args:
qgeom (pd.Series): GeoSeries of element properties.
is_junction (bool): Whether or not qgeom belongs to junction table.
"""
qc_shapely = qgeom.geometry
if is_junction:
self.render_element_junction(qgeom)
else:
if isinstance(qc_shapely, shapely.geometry.Polygon):
self.render_element_poly(qgeom)
elif isinstance(qc_shapely, shapely.geometry.LineString):
self.render_element_path(qgeom)
[docs]
def render_element_junction(self, qgeom: pd.Series):
"""
Render a Josephson junction 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.
Args:
qgeom (pd.Series): GeoSeries of element properties.
"""
ansys_options = dict(transparency=0.0)
qc_name = "Lj_" + str(qgeom["component"])
qc_elt = get_clean_name(qgeom["name"])
qc_shapely = qgeom.geometry
qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
qc_width = parse_units(qgeom.width)
name = f"{qc_name}{QAnsysRenderer.NAME_DELIM}{qc_elt}"
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
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
x_min, x_max = min(x0, x1), max(x0, x1)
y_min, y_max = y0 - qc_width / 2, y0 + qc_width / 2
# Draw rectangle
self.logger.debug(f"Drawing a rectangle: {name}")
poly_ansys = self.modeler.draw_rect_corner(
[x_min, y_min, qc_chip_z],
x_max - x_min,
y_max - y_min,
qc_chip_z,
**ansys_options,
)
axis = "x" if abs(x1 - x0) > abs(y1 - y0) else "y"
self.modeler.rename_obj(poly_ansys, "JJ_rect_" + name)
self.assign_mesh.append("JJ_rect_" + name)
# Draw line
poly_jj = self.modeler.draw_polyline(
[endpoints_3d[0], endpoints_3d[1]],
closed=False,
**dict(color=(128, 0, 128)),
)
poly_jj = poly_jj.rename("JJ_" + name + "_")
poly_jj.show_direction = True
[docs]
def render_element_poly(self, qgeom: pd.Series):
"""Render a closed polygon.
Args:
qgeom (pd.Series): GeoSeries of element properties.
"""
ansys_options = dict(transparency=0.0)
qc_name = self.design._components[qgeom["component"]].name
qc_elt = get_clean_name(qgeom["name"])
qc_shapely = qgeom.geometry # shapely geom
qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
qc_fillet = round(qgeom.fillet, 7)
name = f"{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}"
points = parse_units(list(
qc_shapely.exterior.coords)) # list of 2d point tuples
points_3d = to_vec3D(points, qc_chip_z)
if is_rectangle(qc_shapely): # Draw as rectangle
self.logger.debug(f"Drawing a rectangle: {name}")
x_min, y_min, x_max, y_max = qc_shapely.bounds
poly_ansys = self.modeler.draw_rect_corner(
*parse_units([
[x_min, y_min,
self.design.get_chip_z(qgeom.chip)],
x_max - x_min,
y_max - y_min,
0,
]),
**ansys_options,
)
self.modeler.rename_obj(poly_ansys, name)
else:
# Draw general closed poly
poly_ansys = self.modeler.draw_polyline(points_3d[:-1],
closed=True,
**ansys_options)
# rename: handle bug if the name of the cut already exits and is used to make a cut
poly_ansys = poly_ansys.rename(name)
qc_fillet = round(qgeom.fillet, 7)
if qc_fillet > 0:
qc_fillet = parse_units(qc_fillet)
idxs_to_fillet = good_fillet_idxs(
points,
qc_fillet,
precision=self.design._template_options.PRECISION,
isclosed=True,
)
if idxs_to_fillet:
self.modeler._fillet(qc_fillet, idxs_to_fillet, poly_ansys)
# Subtract interior shapes, if any
if len(qc_shapely.interiors) > 0:
for i, x in enumerate(qc_shapely.interiors):
interior_points_3d = to_vec3D(parse_units(list(x.coords)),
qc_chip_z)
inner_shape = self.modeler.draw_polyline(
interior_points_3d[:-1], closed=True)
self.modeler.subtract(name, [inner_shape])
# Input chip info into self.chip_subtract_dict
if qgeom.chip not in self.chip_subtract_dict:
self.chip_subtract_dict[qgeom.chip] = set()
if qgeom["subtract"]:
self.chip_subtract_dict[qgeom.chip].add(name)
# Potentially add to list of elements to metallize
elif not qgeom["helper"]:
self.assign_perfE.append(name)
[docs]
def render_element_path(self, qgeom: pd.Series):
"""Render a path-type element.
Args:
qgeom (pd.Series): GeoSeries of element properties.
"""
ansys_options = dict(transparency=0.0)
qc_name = self.design._components[qgeom["component"]].name
qc_elt = get_clean_name(qgeom["name"])
qc_shapely = qgeom.geometry # shapely geom
qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
name = f"{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}"
qc_width = parse_units(qgeom.width)
points = parse_units(list(qc_shapely.coords))
points_3d = to_vec3D(points, qc_chip_z)
try:
poly_ansys = self.modeler.draw_polyline(points_3d,
closed=False,
**ansys_options)
except AttributeError:
if self.modeler is None:
self.logger.error(
"No modeler was found. Are you connected to an active Ansys Design?"
)
raise
poly_ansys = poly_ansys.rename(name)
qc_fillet = round(qgeom.fillet, 7)
if qc_fillet > 0:
qc_fillet = parse_units(qc_fillet)
idxs_to_fillet = good_fillet_idxs(
points,
qc_fillet,
precision=self.design._template_options.PRECISION,
isclosed=False,
)
if idxs_to_fillet:
self.modeler._fillet(qc_fillet, idxs_to_fillet, poly_ansys)
if qc_width:
x0, y0 = points[0]
x1, y1 = points[1]
vlen = math.sqrt((x1 - x0)**2 + (y1 - y0)**2)
p0 = np.array([
x0, y0, qc_chip_z
]) + qc_width / (2 * vlen) * np.array([y0 - y1, x1 - x0, 0])
p1 = np.array([
x0, y0, qc_chip_z
]) + qc_width / (2 * vlen) * np.array([y1 - y0, x0 - x1, 0])
shortline = self.modeler.draw_polyline([p0, p1],
closed=False) # sweepline
import pythoncom
try:
self.modeler._sweep_along_path(shortline, poly_ansys)
except pythoncom.com_error as error:
print("com_error: ", error)
hr, msg, exc, arg = error.args
if msg == "Exception occurred." and hr == -2147352567:
self.logger.error(
"We cannot find a writable design. \n Either you are trying to use a Ansys "
"design that is not empty, in which case please clear it manually or with the "
"renderer method clean_active_design(). \n Or you accidentally deleted "
"the design in Ansys, in which case please create a new one."
)
raise error
if qgeom.chip not in self.chip_subtract_dict:
self.chip_subtract_dict[qgeom.chip] = set()
if qgeom["subtract"]:
self.chip_subtract_dict[qgeom.chip].add(name)
elif qgeom["width"] and (not qgeom["helper"]):
self.assign_perfE.append(name)
[docs]
def add_endcaps(self, open_pins: Union[list, None] = None):
"""Create endcaps (rectangular cutouts) for all pins in the list
open_pins and add them to chip_subtract_dict. Each element in open_pins
takes on the form (component_name, pin_name) and corresponds to a
single pin.
Args:
open_pins (Union[list, None], optional): List of tuples of pins that are open. Defaults to None.
"""
open_pins = open_pins if open_pins else []
for comp, pin in open_pins:
pin_dict = self.design.components[comp].pins[pin]
width, gap = parse_units([pin_dict["width"], pin_dict["gap"]])
mid, normal = parse_units(pin_dict["middle"]), pin_dict["normal"]
chip_name = self.design.components[comp].options.chip
qc_chip_z = parse_units(self.design.get_chip_z(chip_name))
rect_mid = np.append(mid + normal * gap / 2, [qc_chip_z])
# Assumption: pins only point in x or y directions
# If this assumption is not satisfied, draw_rect_center no longer works -> must use draw_polyline
endcap_name = f"endcap_{comp}_{pin}"
if abs(normal[0]) > abs(normal[1]):
self.modeler.draw_rect_center(rect_mid,
x_size=gap,
y_size=width + 2 * gap,
name=endcap_name)
else:
self.modeler.draw_rect_center(rect_mid,
x_size=width + 2 * gap,
y_size=gap,
name=endcap_name)
self.chip_subtract_dict[pin_dict["chip"]].add(endcap_name)
[docs]
def get_chip_names(self) -> List[str]:
"""
Obtain a list of chips on which the selection of components, if valid, resides.
Returns:
List[str]: Chips to render.
"""
if self.case == 2: # One or more components not in QDesign.
self.logger.warning("One or more components not found.")
return []
chip_names = set()
if self.case == 1: # All components rendered.
comps = self.design.components
for qcomp in comps:
if "chip" not in comps[qcomp].options:
self.chip_designation_error()
return []
# elif comps[qcomp].options.chip != 'main':
# self.chip_not_main()
# return []
chip_names.add(comps[qcomp].options.chip)
else: # Strict subset rendered.
icomps = self.design._components
for qcomp_id in self.qcomp_ids:
if "chip" not in icomps[qcomp_id].options:
self.chip_designation_error()
return []
# elif icomps[qcomp_id].options.chip != 'main':
# self.chip_not_main()
# return []
chip_names.add(icomps[qcomp_id].options.chip)
for unique_name in chip_names:
if unique_name not in self.design.chips:
self.chip_not_in_design_error(unique_name)
return list(chip_names)
[docs]
def chip_designation_error(self):
"""
Warning message that appears when the Ansys renderer fails to locate a component's chip designation.
Provides instructions for a temporary workaround until the layer stack is finalized.
"""
self.logger.warning(
"This component currently lacks a chip designation. Please add chip='main' to the component's default_options dictionary, restart the kernel, and try again."
)
[docs]
def chip_not_in_design_error(self, missing_chip: str):
"""
Warning message that appears when the Ansys renderer fails to locate a component's chip designation in DesignPlanar (or any child of QDesign).
Provides instructions for a temporary workaround until the layer stack is finalized.
"""
self.logger.warning(
f'This component currently lacks a chip designation in DesignPlanar, or any child of QDesign. '
f'Please add dict for chip=\'{missing_chip}\' in DesignPlanar, or child of QDesign. Then restart the kernel, and try again.'
)
[docs]
def chip_not_main(self):
"""
Warning message that appears when a component's chip designation is not 'main'.
As of 05/10/21, all chip designations should be 'main' until the layer stack is finalized.
Provides instructions for a temporary workaround until the layer stack is finalized.
"""
self.logger.warning(
"The chip designation for this component is not 'main'. Please set chip='main' in its default_options dictionary, restart the kernel, and try again."
)
[docs]
def get_min_bounding_box(self) -> Tuple[float]:
"""
Determine the max/min x/y coordinates of the smallest rectangular, axis-aligned
bounding box that will enclose a selection of components to render, given by
self.qcomp_ids. This method is only used when box_plus_buffer is True.
Returns:
Tuple[float]: min x, min y, max x, and max y coordinates of bounding box.
"""
min_x_main = min_y_main = float("inf")
max_x_main = max_y_main = float("-inf")
if self.case == 2: # One or more components not in QDesign.
self.logger.warning("One or more components not found.")
elif self.case == 1: # All components rendered.
for qcomp in self.design.components:
min_x, min_y, max_x, max_y = self.design.components[
qcomp].qgeometry_bounds()
min_x_main = min(min_x, min_x_main)
min_y_main = min(min_y, min_y_main)
max_x_main = max(max_x, max_x_main)
max_y_main = max(max_y, max_y_main)
else: # Strict subset rendered.
for qcomp_id in self.qcomp_ids:
min_x, min_y, max_x, max_y = self.design._components[
qcomp_id].qgeometry_bounds()
min_x_main = min(min_x, min_x_main)
min_y_main = min(min_y, min_y_main)
max_x_main = max(max_x, max_x_main)
max_y_main = max(max_y, max_y_main)
return min_x_main, min_y_main, max_x_main, max_y_main
[docs]
def render_chips(self,
draw_sample_holder: bool = True,
box_plus_buffer: bool = True):
"""
Render all chips containing components in self.qcomp_ids.
Args:
draw_sample_holder (bool, optional): Option to draw vacuum box around chip. Defaults to True.
box_plus_buffer (bool, optional): Whether or not to use a box plus buffer. Defaults to True.
"""
chip_list = self.get_chip_names()
# added this quick hack for the case of flipchip device.
# current self.get_chip_names only renders chips whose components are to be rendered.
# so if you happen to only draw a qubit only, then the other chip (C_chip) does not get rendered because get_chip_names only returns Q_chip
# I hope this gets a prettier fix in the future
if self.design._metadata.design_name == 'FlipChip_Device':
chip_list = self.design.chips.keys()
self.cw_x, self.cw_y = Dict(), Dict()
self.cc_x, self.cc_y = Dict(), Dict()
for chip_name in chip_list:
if box_plus_buffer: # Get bounding box of components first
min_x_main, min_y_main, max_x_main, max_y_main = parse_units(
self.get_min_bounding_box())
self.cw_x.update({chip_name: max_x_main - min_x_main
}) # chip width along x
self.cw_y.update({chip_name: max_y_main - min_y_main
}) # chip width along y
self.cw_x[chip_name] += 2 * parse_units(
self._options["x_buffer_width_mm"])
self.cw_y[chip_name] += 2 * parse_units(
self._options["y_buffer_width_mm"])
self.cc_x.update({chip_name:
(max_x_main + min_x_main) / 2}) # x coord of chip center
self.cc_y.update({chip_name:
(max_y_main + min_y_main) / 2}) # y coord of chip center
else: # Adhere to chip placement and dimensions in QDesign
p = self.design.get_chip_size(
chip_name) # x/y center/width same for all chips
self.cw_x.update({chip_name: parse_units(p["size_x"])})
self.cw_y.update({chip_name: parse_units(p["size_y"])})
self.cc_x.update({chip_name: parse_units(p["center_x"])})
self.cc_y.update({chip_name: parse_units(p["center_y"])})
# self.cw_x, self.cw_y, _ = parse_units(
# [p['size_x'], p['size_y'], p['size_z']])
# self.cc_x, self.cc_y, _ = parse_units(
# [p['center_x'], p['center_y'], p['center_z']])
self.render_chip(chip_name, draw_sample_holder)
if draw_sample_holder: # HFSS
if "sample_holder_top" in self.design.variables.keys():
p = self.design.variables
else:
p = self.design.get_chip_size(chip_list[0])
vac_height = parse_units(
[p["sample_holder_top"], p["sample_holder_bottom"]])
# very simple algorithm to build the vacuum box. It could be made better in the future
# assuming that both
cc_x = np.array([item for item in self.cc_x.values()])
cc_y = np.array([item for item in self.cc_y.values()])
cw_x = np.array([item for item in self.cw_x.values()])
cw_y = np.array([item for item in self.cw_y.values()])
cc_x_left, cc_x_right = np.min(cc_x - cw_x / 2), np.max(cc_x +
cw_x / 2)
cc_y_left, cc_y_right = np.min(cc_y - cw_y / 2), np.max(cc_y +
cw_y / 2)
cc_x = (cc_x_left + cc_x_right) / 2
cc_y = (cc_y_left + cc_y_right) / 2
cw_x = cc_x_right - cc_x_left
cw_y = cc_y_right - cc_y_left
vacuum_box = self.modeler.draw_box_center(
[cc_x, cc_y, (vac_height[0] - vac_height[1]) / 2],
[cw_x, cw_y, sum(vac_height)],
name="sample_holder",
)
[docs]
def render_chip(self, chip_name: str, draw_sample_holder: bool):
"""
Render individual chips.
Args:
chip_name (str): Name of chip.
draw_sample_holder (bool): Option to draw vacuum box around chip.
"""
ansys_options = dict(transparency=0.0)
ops = self.design._chips[chip_name]
p = self.design.get_chip_size(chip_name)
z_coord, height = parse_units([p["center_z"], p["size_z"]])
plane = self.modeler.draw_rect_center(
[self.cc_x[chip_name], self.cc_y[chip_name], z_coord],
x_size=self.cw_x[chip_name],
y_size=self.cw_y[chip_name],
z_size=0,
name=f"ground_{chip_name}_plane",
**ansys_options,
)
whole_chip = self.modeler.draw_box_center(
[self.cc_x[chip_name], self.cc_y[chip_name], z_coord + height / 2],
[self.cw_x[chip_name], self.cw_y[chip_name], -height],
name=chip_name,
material=ops["material"],
color=(186, 186, 205),
transparency=0.2,
wireframe=False,
)
if self.chip_subtract_dict[chip_name]:
# Any layer which has subtract=True qgeometries will have a ground plane
# TODO: Material property assignment may become layer-dependent.
self.assign_perfE.append(f"ground_{chip_name}_plane")
[docs]
def subtract_from_ground(self):
"""For each chip, subtract all "negative" shapes residing on its
surface if any such shapes exist."""
for chip, shapes in self.chip_subtract_dict.items():
if shapes:
import pythoncom
try:
self.modeler.subtract(f"ground_{chip}_plane", list(shapes))
except pythoncom.com_error as error:
print("com_error: ", error)
hr, msg, exc, arg = error.args
if msg == "Exception occurred." and hr == -2147352567:
self.logger.error(
"This error might indicate that a component was not correctly rendered in Ansys. \n"
"This might have been caused by floating point numerical corrections. \n For example "
"Ansys will inconsistently render (or not) routing that has 180deg jogs with the two "
"adjacent segments spaced 'exactly' twice the fillet radius (U shaped routing). \n"
"In this example, changing your fillet radius to a smaller number would solve the issue."
)
raise error
[docs]
def add_mesh(self):
"""Add mesh to all elements in self.assign_mesh."""
if self.assign_mesh:
self.modeler.mesh_length(
"small_mesh",
self.assign_mesh,
MaxLength=self._options["max_mesh_length_jj"],
)
try:
exists_port_mesh = (len(self.assign_port_mesh) > 0)
except:
exists_port_mesh = False
if exists_port_mesh:
self.modeler.mesh_length(
'port_mesh',
self.assign_port_mesh,
MaxLength=self._options['max_mesh_length_port'])
# Still implementing
[docs]
def auto_wirebonds(self, table):
"""
Adds wirebonds to the Ansys model for path elements where;
subtract = True and wire_bonds = True.
Uses render options for determining of the:
* wb_threshold -- the minimum distance between two vertices of a path for a
wirebond to be added.
* wb_offset -- offset distance for wirebond placement (along the direction
of the cpw)
* wb_size -- controls the width of the wirebond (wb_size * path['width'])
"""
norm_z = np.array([0, 0, 1])
wb_threshold = parse_units(self._options["wb_threshold"])
wb_offset = parse_units(self._options["wb_offset"])
# selecting only the qgeometry which meet criteria
wb_table = table.loc[table["hfss_wire_bonds"] == True]
wb_table2 = wb_table.loc[wb_table["subtract"] == True]
# looping through each qgeometry
for _, row in wb_table2.iterrows():
geom = row["geometry"]
width = row["width"]
# looping through the linestring of the path to determine where WBs should be
for index, i_p in enumerate(geom.coords[:-1], start=0):
j_p = np.asarray(geom.coords[:][index + 1])
vert_distance = parse_units(distance.euclidean(i_p, j_p))
if vert_distance > wb_threshold:
# Gets number of wirebonds to fit in section of path
wb_count = int(vert_distance // wb_threshold)
# finds the position vector
wb_pos = (j_p - i_p) / (wb_count + 1)
# gets the norm vector for finding the orthonormal of path
wb_vec = wb_pos / np.linalg.norm(wb_pos)
# finds the orthonormal (for orientation)
wb_perp = np.cross(norm_z, wb_vec)[:2]
# finds the first wirebond to place (rest are in the loop)
wb_pos_step = parse_units(wb_pos + i_p) + (wb_vec *
wb_offset)
# Other input values could be modified, kept to minimal selection for automation
# for the time being. Loops to place N wirebonds based on length of path section.
for wb_i in range(wb_count):
self.modeler.draw_wirebond(
pos=wb_pos_step + parse_units(wb_pos * wb_i),
ori=wb_perp,
width=parse_units(width * self._options["wb_size"]),
height=parse_units(width *
self._options["wb_size"]),
z=0,
wire_diameter="0.015mm",
NumSides=6,
name="g_wb",
material="pec",
solve_inside=False,
)
[docs]
def clean_active_design(self):
"""Remove all elements from Ansys Modeler."""
if self.pinfo:
if self.pinfo.get_all_object_names():
project_name = self.pinfo.project_name
design_name = self.pinfo.design_name
select_all = ",".join(self.pinfo.get_all_object_names())
# self.pinfo.design does not work, thus the following line
oDesktop = self.pinfo.design.parent.parent._desktop
oProject = oDesktop.SetActiveProject(project_name)
oDesign = oProject.SetActiveDesign(design_name)
# The available editors: "Layout", "3D Modeler", "SchematicEditor"
oEditor = oDesign.SetActiveEditor("3D Modeler")
oEditor.Delete(["NAME:Selections", "Selections:=", select_all])
[docs]
def set_variables(self, variables: Dict):
"""Fixes the junction properties before setup. This is necessary becasue the eigenmode
analysis only considers the junction as a lumped CL element.
Args:
variables (Dict): dictionary of variables to set in Ansys. For example it could
contain 'Lj': '10 nH'
"""
if self.pinfo:
if self.pinfo.design:
for k, v in variables.items():
self.pinfo.design.set_variable(k, v)
else:
self.logger.warning(
"Please create a design before setting variables, otherwise all variables will be set to 0 during rendering by default."
)
# TODO: epr methods below should not be in the renderer, but in the analysis files.
# Thus needs to remove the dependency from pinfo, which is Ansys-specific.
[docs]
def epr_start(self, junctions: dict = None, dissipatives: dict = None):
"""Use to initialize the epr analysis package by first identifying which are the junctions,
their electrical properties and their reference plane; then initialize the
DistributedAnalysis package, which can execute microwave analysis on eigenmode results.
Args:
junctions (dict, optional): Each element of this dictionary describes one junction.
Defaults to dict().
dissipatives (dict, optional): Each element of this dictionary describes one dissipative.
Defaults to dict().
"""
if self.pinfo:
if junctions:
for k, v in junctions.items():
self.pinfo.junctions[k] = v
# Check that valid names of variables and objects have been supplied
self.pinfo.validate_junction_info()
if dissipatives:
for k, v in dissipatives.items():
self.pinfo.dissipative[k] = v
# Class handling microwave analysis on eigenmode solutions
self.epr_distributed_analysis = epr.DistributedAnalysis(self.pinfo)
[docs]
def epr_get_stored_energy(self,
junctions: dict = None,
dissipatives: dict = None):
"""Computes the energy stored in the system
pinfo must have a valid list of junctions and dissipatives to compute the energy stored
in the system. So please provide them here, or using epr_start()
Args:
junctions (dict, optional): Each element of this dictionary describes one junction.
Defaults to dict().
dissipatives (dict, optional): Each element of this dictionary describes one dissipative.
Defaults to dict().
Returns:
(float, float, float): energy_elec, energy_elec_substrate, energy_mag
"""
if junctions is not None or dissipatives is not None:
self.epr_start(junctions, dissipatives)
elif self.epr_distributed_analysis is None:
self.epr_start()
if self.pinfo.dissipative["dielectrics_bulk"] is not None:
eprd = self.epr_distributed_analysis
energy_elec = eprd.calc_energy_electric()
energy_elec_substrate = eprd.calc_energy_electric(
None, self.pinfo.dissipative["dielectrics_bulk"][0])
energy_mag = eprd.calc_energy_magnetic()
return energy_elec, energy_elec_substrate, energy_mag
self.logger.error("dielectrics_bulk needs to be defined")
[docs]
def epr_run_analysis(self,
junctions: dict = None,
dissipatives: dict = None):
"""Executes the EPR analysis
pinfo must have a valid list of junctions and dissipatives to compute the energy stored
in the system. So please provide them here, or using epr_start()
Args:
junctions (dict, optional): Each element of this dictionary describes one junction.
Defaults to dict().
dissipatives (dict, optional): Each element of this dictionary describes one dissipative.
Defaults to dict().
"""
if junctions is not None or dissipatives is not None:
self.epr_start(junctions, dissipatives)
self.epr_distributed_analysis.do_EPR_analysis()
[docs]
def epr_spectrum_analysis(self, cos_trunc: int = 8, fock_trunc: int = 7):
"""Core epr analysis method.
Args:
cos_trunc (int, optional): truncation of the cosine. Defaults to 8.
fock_trunc (int, optional): truncation of the fock. Defaults to 7.
"""
self.epr_quantum_analysis = epr.QuantumAnalysis(
self.epr_distributed_analysis.data_filename)
self.epr_quantum_analysis.analyze_all_variations(cos_trunc=cos_trunc,
fock_trunc=fock_trunc)
[docs]
def epr_report_hamiltonian(self,
swp_variable: str = "variation",
numeric=True):
"""Reports in a markdown friendly table the hamiltonian results.
Args:
swp_variable (str, optional): Variable against which we swept. Defaults to 'variation'.
"""
self.epr_quantum_analysis.plot_hamiltonian_results(
swp_variable=swp_variable)
self.epr_quantum_analysis.report_results(swp_variable=swp_variable,
numeric=numeric)
[docs]
def epr_get_frequencies(self,
junctions: dict = None,
dissipatives: dict = None) -> pd.DataFrame:
"""Returns all frequencies and quality factors vs a variation.
It also initializes the systems for the epr analysis in terms of junctions and dissipatives
Args:
junctions (dict, optional): Each element of this dictionary describes one junction.
Defaults to dict().
dissipatives (dict, optional): Each element of this dictionary describes one dissipative.
Defaults to dict().
Returns:
pd.DataFrame: multi-index, frequency and quality factors for each variation point.
"""
# TODO: do I need to reset self.pinfo.junctions (does it keep the older analysis one)
self.epr_start(junctions, dissipatives)
return self.epr_distributed_analysis.get_ansys_frequencies_all()