"""
Main distributed analysis module to use pyEPR.
Contains code to connect to Ansys and to analyze HFSS files using the EPR method.
This module handles the microwave part of the analysis and connection to
Further contains code to be able to do autogenerated reports,
Copyright Zlatko Minev, Zaki Leghtas, and the pyEPR team
2015, 2016, 2017, 2018, 2019, 2020
"""
# pylint: disable=invalid-name
# todo remove this pylint hack later
from __future__ import print_function # Python 2.7 and 3 compatibility
from typing import List
import pickle
import sys
import time
from collections import OrderedDict
from pathlib import Path
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from . import Dict, config, logger
from .ansys import CalcObject, ConstantVecCalcObject, set_property, ureg
from .calcs.constants import epsilon_0
from .project_info import ProjectInfo
from .reports import (plot_convergence_f_vspass, plot_convergence_max_df,
plot_convergence_maxdf_vs_sol,
plot_convergence_solved_elem)
from .toolbox.pythonic import print_NoNewLine
# class AnsysAnalysisBase():
#
# def __init__():
# """
# Instantiate in super after call.
# """
[docs]class DistributedAnalysis(object):
"""
DISTRIBUTED ANALYSIS of layout and microwave results.
Main computation class & interface with HFSS.
This class defines a DistributedAnalysis object which calculates and saves
Hamiltonian parameters from an HFSS simulation.
Further, it allows one to calculate dissipation, etc.
"""
def __init__(self, *args, **kwargs):
'''
Pass in the arguments for ProjectInfo. See help for `?ProjectInfo`.
Parameters:
-------------------
project_info : ProjectInfo
Supply the project info or the parameters to create pinfo
Use notes:
-------------------
* If you change the setup or number of eigenmodes in HFSS, etc.
call `update_ansys_info()`
Example use:
-------------------
See the tutorials in the repository.
.. code-block:: python
:linenos:
import pyEPR as epr
pinfo = epr.ProjectInfo(project_path = path_to_project,
project_name = 'pyEPR_tutorial1',
design_name = '1. single_transmon')
eprd = epr.DistributedAnalysis(pinfo)
To now quickly see the result of a sweep of a variable in ansys, you can use:
.. code-block:: python
:linenos:
swp_var = 'Lj'
display(eprd.get_ansys_variables())
fs = eprd.quick_plot_frequencies(swp_var)
display(fs)
To perform distributed analysis
.. code-block:: python
:linenos:
eprd.do_EPR_analysis(append_analysis=True);
Key internal parameters:
-------------------
n_modes (int) : Number of eignemodes; e.g., 2
variations (List[str]) : A list of string identifier of **solved** variation
for the selected setup. Example: '['0', '1']
_list_variations : An array of strings corresponding to **solved** variations.
List of identifier strings for the SOLVED ansys variation for the selected setup.
These do not include unsolved variables added after the solution!
.. code-block:: python
("Height='0.06mm' Lj='13.5nH'", "Height='0.06mm' Lj='15.3nH'")
'''
# Get the project info
project_info = None
if (len(args) == 1) and (args[0].__class__.__name__ == 'ProjectInfo'):
# isinstance(args[0], ProjectInfo): # fails on module repload with changes
project_info = args[0]
else:
assert len(args) == 0, '''Since you did not pass a ProjectInfo object
as a argument, we now assume you are trying to create a project
info object here by passing its arguments. See ProjectInfo.
It does not take any arguments, only kwargs. \N{face with medical mask}'''
project_info = ProjectInfo(*args, **kwargs)
# Input
self.pinfo = project_info # : project_info: a reference to a Project_Info class
if self.pinfo.check_connected() is False:
self.pinfo.connect()
# hfss connect module
self.fields = None
self.solutions = None
if self.setup:
self.fields = self.setup.get_fields()
self.solutions = self.setup.get_solutions()
# Stores results from sims
self.results = Dict() # of variations. Saved results
# TODO: turn into base class shared with analysis!
# Modes and variations - the following get updated in update_variation_information
self.n_modes = int(1) # : Number of eigenmodes
self.modes = None
#: List of variation indices, which are strings of ints, such as ['0', '1']
self.variations = []
self.variations_analyzed = [] # : List of analyzed variations. List of indices
# String identifier of variables, such as "Cj='2fF' Lj='12.5nH'"
self._nominal_variation = ''
self._list_variations = ("",) # tuple set of variables
# container for eBBQ list of variables; basically the same as _list_variations
self._hfss_variables = Dict()
self._previously_analyzed = set() # previously analyzed variations
self.update_ansys_info()
print('Design \"%s\" info:' % self.design.name)
print('\t%-15s %d\n\t%-15s %d' % ('# eigenmodes', self.n_modes,
'# variations', self.n_variations))
# Setup data saving
self.data_dir = None
self.file_name = None
self.setup_data()
@property
def setup(self):
"""Ansys setup class handle. Could be None."""
return self.pinfo.setup
@property
def design(self):
"""Ansys design class handle"""
return self.pinfo.design
@property
def project(self):
"""Ansys project class handle"""
return self.pinfo.project
# @property
# def desktop(self):
# """Ansys desktop class handle"""
# return self.pinfo.desktop
# @property
# def app(self):
# """Ansys App class handle"""
# return self.pinfo.app
# @property
# def junctions(self):
# """Project info junctions"""
# return self.pinfo.junctions
# @property
# def ports(self):
# return self.pinfo.ports
@property
def options(self):
""" Project info options"""
return self.pinfo.options
[docs] def setup_data(self):
'''
Set up folder paths for saving data to.
Sets the save filename with the current time.
Saves to Path(config.root_dir) / self.project.name / self.design.name
'''
if len(self.design.name) > 50:
logger.error('WARNING! DESIGN FILENAME MAY BE TOO LONG! ')
self.data_dir = Path(config.root_dir) / \
self.project.name / self.design.name
self.data_filename = self.data_dir / (time.strftime(config.save_format,
time.localtime()) + '.npz')
if not self.data_dir.is_dir():
self.data_dir.mkdir(parents=True, exist_ok=True)
[docs] def calc_p_junction_single(self, mode, variation, U_E=None, U_H=None):
'''
This function is used in the case of a single junction only.
For multiple junctions, see :func:`~pyEPR.DistributedAnalysis.calc_p_junction`.
Assumes no lumped capacitive elements.
'''
if U_E is None:
U_E = self.calc_energy_electric(variation)
if U_H is None:
U_H = self.calc_energy_magnetic(variation)
pj = OrderedDict()
pj_val = (U_E-U_H)/U_E
pj['pj_'+str(mode)] = np.abs(pj_val)
print(' p_j_' + str(mode) + ' = ' + str(pj_val))
return pj
# TODO: replace this method with the one below, here because some funcs use it still
[docs] def get_freqs_bare(self, variation: str):
"""
Warning:
Outdated. Do not use. To be deprecated
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
[type] -- [description]
"""
# str(self._get_lv(variation))
freqs_bare_vals = []
freqs_bare_dict = OrderedDict()
freqs, kappa_over_2pis = self.solutions.eigenmodes(
self.get_variation_string(variation))
for m in range(self.n_modes):
freqs_bare_dict['freq_bare_'+str(m)] = 1e9*freqs[m]
freqs_bare_vals.append(1e9*freqs[m])
if kappa_over_2pis is not None:
freqs_bare_dict['Q_'+str(m)] = freqs[m]/kappa_over_2pis[m]
else:
freqs_bare_dict['Q_'+str(m)] = 0
#self.freqs_bare = freqs_bare_dict
#self.freqs_bare_vals = freqs_bare_vals
return freqs_bare_dict, freqs_bare_vals
[docs] def get_freqs_bare_pd(self, variation: str, frame=True):
"""Return the freq and Qs of the solved modes for a variation.
I.e., the Ansys solved frequencies.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
frame {bool} -- if True returns dataframe, else tuple of series.
Returns:
If frame = True, then a multi-index Dataframe that looks something like this
.. code-block:: python
Freq. (GHz) Quality Factor
variation mode
0 0 5.436892 1020
1 7.030932 50200
1 0 5.490328 2010
1 7.032116 104500
If frame = False, then a tuple of two Series, such as
(Fs, Qs) -- Tuple of pandas.Series objects; the row index is the mode number
"""
variation_str = self.get_variation_string(variation)
freqs, kappa_over_2pis = self.solutions.eigenmodes(variation_str)
if kappa_over_2pis is None:
kappa_over_2pis = np.zeros(len(freqs))
freqs = pd.Series(freqs, index=range(len(freqs))) # GHz
Qs = freqs / pd.Series(kappa_over_2pis, index=range(len(freqs)))
if frame:
df = pd.DataFrame({'Freq. (GHz)': freqs, 'Quality Factor': Qs})
df.index.name = 'mode'
return df
else:
return freqs, Qs
[docs] def get_ansys_frequencies_all(self, vs='variation'):
"""
Return all ansys frequencies and quality factors vs a variation
Returns a multi-index pandas DataFrame
"""
df = dict()
variable = None if vs == 'variation' else self.get_variable_vs_variations(
vs)
for variation in self.variations: # just for the first 2
if vs == 'variation':
label = variation
else:
label = variable[variation]
df[label] = self.get_freqs_bare_pd(variation=variation)
# TODO: maybe sort column and index? # todo: maybe generalize
return pd.concat(df, names=[vs])
def _get_lv(self, variation=None):
'''
List of variation variables in a format that is used when feeding back to ansys.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
list of var names and var values.
Such as
.. code-block:: python
['Lj1:=','13nH', 'QubitGap:=','100um']
'''
if variation is None:
lv = self._nominal_variation # "Cj='2fF' Lj='12.5nH'"
lv = self._parse_listvariations(lv)
else:
lv = self._list_variations[ureg(variation)]
lv = self._parse_listvariations(lv)
return lv
# Functions that deal with variations exclusively
@property
def n_variations(self):
""" Number of **solved** variations, corresponding to the
selected Setup. """
return len(self._list_variations)
[docs] def set_variation(self, variation: str):
"""
Set the ansys design to a solved variation.
This will change all local variables!
Warning: not tested with global variables.
"""
variation_string = self.get_variation_string(variation)
self.design.set_variables(variation_string)
[docs] def get_variations(self):
"""
An array of strings corresponding to **solved** variations corresponding to the
selected Setup.
Returns:
Returns a list of strings that give the variation labels for HFSS.
.. code-block:: python
OrderedDict([
('0', "Cj='2fF' Lj='12nH'"),
('1', "Cj='2fF' Lj='12.5nH'"),
('2', "Cj='2fF' Lj='13nH'"),
('3', "Cj='2fF' Lj='13.5nH'"),
('4', "Cj='2fF' Lj='14nH'")])
"""
return OrderedDict(zip(self.variations, self._list_variations))
[docs] def get_variation_string(self, variation=None):
"""
**Solved** variation string identifier.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
Return the list variation string of parameters in ansys used to identify the variation.
.. code-block:: python
"$test='0.25mm' Cj='2fF' Lj='12.5nH'"
"""
if variation is None:
return self._nominal_variation
return self._list_variations[ureg(variation)]
def _parse_listvariations(self, lv):
"""
Turns
"Cj='2fF' Lj='13.5nH'"
into
['Cj:=', '2fF', 'Lj:=', '13.5nH']
"""
lv = str(lv)
lv = lv.replace("=", ":=,")
lv = lv.replace(' ', ',')
lv = lv.replace("'", "")
lv = lv.split(",")
return lv
[docs] def get_nominal_variation_index(self):
"""
Returns:
A string identifies, such as '0' or '1', that labels the
nominal variation index number.
This may not be in the solved list!s
"""
try:
return str(self._list_variations.index(self._nominal_variation))
except Exception:
print('WARNING: Unsure of the index, returning 0')
return '0'
[docs] def get_ansys_variations(self):
"""
Will update ansys information and result the list of variations.
Returns:
For example:
.. code-block:: python
("Cj='2fF' Lj='12nH'",
"Cj='2fF' Lj='12.5nH'",
"Cj='2fF' Lj='13nH'",
"Cj='2fF' Lj='13.5nH'",
"Cj='2fF' Lj='14nH'")
"""
self.update_ansys_info()
return self._list_variations
[docs] def update_ansys_info(self):
''''
Updates all information about the Ansys solved variations and variables.
.. code-block:: python
:linenos:
n_modes, _list_variations, nominal_variation, n_variations
'''
# from oDesign
self._nominal_variation = self.design.get_nominal_variation()
if self.setup:
# from oSetup -- only for the solved variations!
self._list_variations = self.solutions.list_variations()
self.variations = [str(i) for i in range(
self.n_variations)] # TODO: change to integer?
# eigenmodes
if self.design.solution_type == 'Eigenmode':
self.n_modes = int(self.setup.n_modes)
else:
self.n_modes = 0
self._update_ansys_variables()
def _update_ansys_variables(self, variations=None):
"""
Updates the list of ansys hfss variables for the set of sweeps.
"""
variations = variations or self.variations
for variation in variations:
self._hfss_variables[variation] = pd.Series(
self.get_variables(variation=variation))
return self._hfss_variables
[docs] def get_ansys_variables(self):
"""
Get ansys variables for all variations
Returns:
Return a dataframe of variables as index and columns as the variations
"""
vs = 'variation'
df = pd.DataFrame(self._hfss_variables, columns=self.variations)
df.columns.name = vs
df.index = [x[1:] if x.startswith('_') else x for x in df.index]
#df.index.name = 'variable'
return df
[docs] def get_variables(self, variation=None):
"""
Get ansys variables.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
"""
lv = self._get_lv(variation)
variables = OrderedDict()
for ii in range(int(len(lv)/2)):
variables['_'+lv[2*ii][:-2]] = lv[2*ii+1]
#self.variables = variables
return variables
[docs] def get_variable_vs_variations(self, variable: str, convert: bool = True):
"""
Get ansys variables
Return HFSS variable from :py:func:`self.get_ansys_variables()` as a
pandas series vs variations.
Args:
convert (bool) : Convert to a numeric quantity if possible using the
ureg
"""
# TODO: These should be common function to the analysis and here!
# BOth should be subclasses of a base class
s = self.get_ansys_variables().loc[variable, :] # : pd.Series
if convert:
s = s.apply(lambda x: ureg.Quantity(x).magnitude)
return s
[docs] def calc_energy_electric(self,
variation: str = None,
obj: str = 'AllObjects',
volume: str = 'Deprecated',
smooth: bool = False,
obj_dims: int = 3):
r'''
Calculates two times the peak electric energy, or 4 times the RMS,
:math:`4*\mathcal{E}_{\mathrm{elec}}`
(since we do not divide by 2 and use the peak phasors).
.. math::
\mathcal{E}_{\mathrm{elec}}=\frac{1}{4}\mathrm{Re}\int_{V}\mathrm{d}v\vec{E}_{\text{max}}^{*}\overleftrightarrow{\epsilon}\vec{E}_{\text{max}}
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
obj (string | 'AllObjects'): Name of the object to integrate over
smooth (bool | False) : Smooth the electric field or not when performing calculation
obj_dims (int | 3) : 1 - line, 2 - surface, 3 - volume. Default volume
Example:
Example use to calculate the energy participation ratio (EPR) of a substrate
.. code-block:: python
:linenos:
ℰ_total = epr_hfss.calc_energy_electric(obj='AllObjects')
ℰ_substr = epr_hfss.calc_energy_electric(obj='Box1')
print(f'Energy in substrate = {100*ℰ_substr/ℰ_total:.1f}%')
'''
if volume != 'Deprecated':
logger.warning('The use of the "volume" argument is deprecated... use "obj" instead')
obj = volume
calcobject = CalcObject([], self.setup)
vecE = calcobject.getQty("E")
if smooth:
vecE = vecE.smooth()
A = vecE.times_eps()
B = vecE.conj()
A = A.dot(B)
A = A.real()
if obj_dims == 1:
A = A.integrate_line(name=obj)
elif obj_dims == 2:
A = A.integrate_surf(name=obj)
elif obj_dims == 3:
A = A.integrate_vol(name=obj)
else:
logger.warning('Invalid object dimensions %s, using default of 3 (volume)' % obj_dims)
A = A.integrate_vol(name=obj)
lv = self._get_lv(variation)
return A.evaluate(lv=lv)
[docs] def calc_energy_magnetic(self,
variation: str = None,
obj: str = 'AllObjects',
volume: str = 'Deprecated',
smooth: bool = False,
obj_dims: int = 3):
'''
See calc_energy_electric.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
volume (string | 'AllObjects'): Name of the volume to integrate over
smooth (bool | False) : Smooth the electric field or not when performing calculation
obj_dims (int | 3) : 1 - line, 2 - surface, 3 - volume. Default volume
'''
if volume != 'Deprecated':
logger.warning('The use of the "volume" argument is deprecated... use "obj" instead')
obj = volume
calcobject = CalcObject([], self.setup)
vecH = calcobject.getQty("H")
if smooth:
vecH = vecH.smooth()
A = vecH.times_mu()
B = vecH.conj()
A = A.dot(B)
A = A.real()
if obj_dims == 1:
A = A.integrate_line(name=obj)
elif obj_dims == 2:
A = A.integrate_surf(name=obj)
elif obj_dims == 3:
A = A.integrate_vol(name=obj)
else:
logger.warn(f'Invalid object dimensions {obj_dims}, using default of 3 (volume)')
A = A.integrate_vol(name=obj)
lv = self._get_lv(variation)
return A.evaluate(lv=lv)
[docs] def calc_p_electric_volume(self,
name_dielectric3D,
relative_to='AllObjects',
variation=None,
E_total=None
):
r'''
Calculate the dielectric energy-participation ratio
of a 3D object (one that has volume) relative to the dielectric energy of
a list of objects.
This is as a function relative to another object or all objects.
When all objects are specified, this does not include any energy
that might be stored in any lumped elements or lumped capacitors.
Returns:
ℰ_object/ℰ_total, (ℰ_object, _total)
'''
if E_total is None:
logger.debug('Calculating ℰ_total')
ℰ_total = self.calc_energy_electric(obj=relative_to, variation=variation)
else:
ℰ_total = E_total
logger.debug('Calculating ℰ_object')
ℰ_object = self.calc_energy_electric(obj=name_dielectric3D, variation=variation)
return ℰ_object/ℰ_total, (ℰ_object, ℰ_total)
[docs] def calc_current(self, fields, line: str):
'''
Function to calculate Current based on line. Not in use.
Args:
line (str) : integration line between plates - name
'''
self.design.Clear_Field_Clac_Stack()
comp = fields.Vector_H
exp = comp.integrate_line_tangent(line)
I = exp.evaluate(phase=90)
self.design.Clear_Field_Clac_Stack()
return I
[docs] def calc_avg_current_J_surf_mag(self, variation: str, junc_rect: str, junc_line):
''' Peak current I_max for mode J in junction J
The avg. is over the surface of the junction. I.e., spatial.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
junc_rect (str) : name of rectangle to integrate over
junc_line (str) : name of junction line to integrate over
Returns:
Value of peak current
'''
lv = self._get_lv(variation)
jl, uj = self.get_junc_len_dir(variation, junc_line)
uj = ConstantVecCalcObject(uj, self.setup)
calc = CalcObject([], self.setup)
#calc = calc.getQty("Jsurf").mag().integrate_surf(name = junc_rect)
calc = (((calc.getQty("Jsurf")).dot(uj)).imag()
).integrate_surf(name=junc_rect)
I = calc.evaluate(lv=lv) / jl # phase = 90
# self.design.Clear_Field_Clac_Stack()
return I
[docs] def calc_current_using_line_voltage(self, variation: str, junc_line_name: str,
junc_L_Henries: float, Cj_Farads: float = None):
'''
Peak current I_max for prespecified mode calculating line voltage across junction.
Make sure that you have set the correct variation in HFSS before running this
Args:
variation: variation number
junc_line_name: name of the HFSS line spanning the junction
junc_L_Henries: junction inductance in henries
Cj_Farads: junction cap in Farads
TODO: Smooth?
'''
lv = self._get_lv(variation)
v_calc_real = CalcObject([], self.setup).getQty(
"E").real().integrate_line_tangent(name=junc_line_name)
v_calc_imag = CalcObject([], self.setup).getQty(
"E").imag().integrate_line_tangent(name=junc_line_name)
V = np.sign(v_calc_real.evaluate(lv=lv)) * np.sqrt(v_calc_real.evaluate(lv=lv)**2 +
v_calc_imag.evaluate(lv=lv)**2)
# Get frequency
freq = CalcObject(
[('EnterOutputVar', ('Freq', "Complex"))], self.setup).real().evaluate()
omega = 2*np.pi*freq # in SI radian Hz units
Z = omega*junc_L_Henries
if abs(float(Cj_Farads)) > 1E-29: # zero
#print('Non-zero Cj used in calc_current_using_line_voltage')
#Z += 1./(omega*Cj_Farads)
print(
'\t\t'f'Energy fraction (Lj over Lj&Cj)= {100./(1.+omega**2 *Cj_Farads*junc_L_Henries):.2f}%')
# f'Z_L= {omega*junc_L_Henries:.1f} Ohms Z_C= {1./(omega*Cj_Farads):.1f} Ohms')
I_peak = V/Z # I=V/(wL)s
return I_peak, V, freq
[docs] def calc_line_current(self, variation, junc_line_name):
lv = self._get_lv(variation)
calc = CalcObject([], self.setup)
calc = calc.getQty("H").imag().integrate_line_tangent(
name=junc_line_name)
# self.design.Clear_Field_Clac_Stack()
return calc.evaluate(lv=lv)
[docs] def get_junc_len_dir(self, variation: str, junc_line):
'''
Return the length and direction of a junction defined by a line
Args:
variation (str): simulation variation
junc_line (str): polyline object
Returns:
jl (float) : junction length
uj (list of 3 floats): x,y,z coordinates of the unit vector
tangent to the junction line
'''
#
lv = self._get_lv(variation)
u = []
for coor in ['X', 'Y', 'Z']:
calc = CalcObject([], self.setup)
calc = calc.line_tangent_coor(junc_line, coor)
u.append(calc.evaluate(lv=lv))
jl = float(np.sqrt(u[0]**2+u[1]**2+u[2]**2))
uj = [float(u[0]/jl), float(u[1]/jl), float(u[2]/jl)]
return jl, uj
[docs] def get_Qseam(self, seam, mode, variation, U_H=None):
r'''
Calculate the contribution to Q of a seam, by integrating the current in
the seam with finite conductance: set in the config file
ref: http://arxiv.org/pdf/1509.01119.pdf
'''
if U_H is None:
U_H = self.calc_energy_magnetic(variation)
_, freqs_bare_vals = self.get_freqs_bare(variation)
self.omega = 2*np.pi*freqs_bare_vals[mode]
lv = self._get_lv(variation)
Qseam = OrderedDict()
print(f'Calculating Qseam_{seam} for mode {mode} ({mode}/{self.n_modes-1})')
# overestimating the loss by taking norm2 of j, rather than jperp**2
j_2_norm = self.fields.Vector_Jsurf.norm_2()
int_j_2 = j_2_norm.integrate_line(seam)
int_j_2_val = int_j_2.evaluate(lv=lv, phase=90)
yseam = int_j_2_val/U_H/self.omega
Qseam['Qseam_'+seam+'_' +
str(mode)] = config.dissipation.gseam/yseam
print('Qseam_' + seam + '_' + str(mode), '=', str(config.dissipation.gseam/yseam))
return pd.Series(Qseam)
[docs] def get_Qseam_sweep(self, seam, mode, variation, variable, values, unit, U_H=None, pltresult=True):
"""
Q due to seam loss.
values = ['5mm','6mm','7mm']
ref: http://arxiv.org/pdf/1509.01119.pdf
"""
if U_H is None:
U_H = self.calc_energy_magnetic(variation)
self.solutions.set_mode(mode+1, 0)
self.fields = self.setup.get_fields()
freqs_bare_dict, freqs_bare_vals = self.get_freqs_bare(variation)
self.omega = 2*np.pi*freqs_bare_vals[mode]
print(variation)
print(type(variation))
print(ureg(variation))
lv = self._get_lv(variation)
Qseamsweep = []
print('Calculating Qseam_' + seam + ' for mode ' + str(mode) +
' (' + str(mode) + '/' + str(self.n_modes-1) + ')')
for value in values:
self.design.set_variable(variable, str(value)+unit)
# overestimating the loss by taking norm2 of j, rather than jperp**2
j_2_norm = self.fields.Vector_Jsurf.norm_2()
int_j_2 = j_2_norm.integrate_line(seam)
int_j_2_val = int_j_2.evaluate(lv=lv, phase=90)
yseam = int_j_2_val/U_H/self.omega
Qseamsweep.append(config.dissipation.gseam/yseam)
# Qseamsweep['Qseam_sweep_'+seam+'_'+str(mode)] = gseam/yseam
# Cprint 'Qseam_' + seam + '_' + str(mode) + str(' = ') + str(gseam/yseam)
if pltresult:
_, ax = plt.subplots()
ax.plot(values, Qseamsweep)
ax.set_yscale('log')
ax.set_xlabel(variable+' ('+unit+')')
ax.set_ylabel('Q'+'_'+seam)
return Qseamsweep
[docs] def get_Qdielectric(self, dielectric, mode, variation, U_E=None):
if U_E is None:
U_E = self.calc_energy_electric(variation)
Qdielectric = OrderedDict()
print('Calculating Qdielectric_' + dielectric + ' for mode ' +
str(mode) + ' (' + str(mode) + '/' + str(self.n_modes-1) + ')')
U_dielectric = self.calc_energy_electric(variation, obj=dielectric)
p_dielectric = U_dielectric/U_E
# TODO: Update make p saved sep. and get Q for diff materials, indep. specify in pinfo
Qdielectric['Qdielectric_' + dielectric] = 1/(p_dielectric*config.dissipation.tan_delta_sapp)
print('p_dielectric'+'_'+dielectric+'_' + str(mode) + ' = ' + str(p_dielectric))
return pd.Series(Qdielectric)
[docs] def get_Qsurface(self, mode, variation, name, U_E=None, material_properties=None):
'''
Calculate the contribution to Q of a dielectric layer of dirt on a given surface.
Set the dirt thickness and loss tangent in the config file
ref: http://arxiv.org/pdf/1509.01854.pdf
'''
if U_E is None:
U_E = self.calc_energy_electric(variation)
if material_properties is None:
material_properties = {}
th = material_properties.get('th', config.dissipation.th)
eps_r = material_properties.get('eps_r', config.dissipation.eps_r)
tan_delta_surf = material_properties.get('tan_delta_surf', config.dissipation.tan_delta_surf)
lv = self._get_lv(variation)
Qsurf = OrderedDict()
print(f'Calculating Qsurface {name} for mode ({mode}/{self.n_modes-1})')
calcobject = CalcObject([], self.setup)
vecE = calcobject.getQty("E")
A = vecE
B = vecE.conj()
A = A.dot(B)
A = A.real()
A = A.integrate_surf(name=name)
U_surf = A.evaluate(lv=lv)
U_surf *= th * epsilon_0 * eps_r
p_surf = U_surf/U_E
Qsurf[f'Qsurf_{name}'] = 1 / (p_surf * tan_delta_surf)
print(f'p_surf_{name}_{mode} = {p_surf}')
return pd.Series(Qsurf)
[docs] def get_Qsurface_all(self, mode, variation, U_E=None):
'''
Calculate the contribution to Q of a dielectric layer of dirt on all surfaces.
Set the dirt thickness and loss tangent in the config file
ref: http://arxiv.org/pdf/1509.01854.pdf
'''
return self.get_Qsurface(mode, variation, name='AllObjects', U_E=U_E)
[docs] def calc_Q_external(self, variation, freq_GHz, U_E = None):
'''
Calculate the coupling Q of mode m with each port p
Expected that you have specified the mode before calling this
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
'''
if U_E is None:
U_E = self.calc_energy_electric(variation)
Qp = pd.Series({}, dtype='float64')
freq = freq_GHz * 1e9 # freq in Hz
for port_nm, port in self.pinfo.ports.items():
I_peak = self.calc_avg_current_J_surf_mag(variation, port['rect'],
port['line'])
U_dissip = 0.5 * port['R'] * I_peak**2 * 1 / freq
p = U_dissip / (U_E/2) # U_E is 2x the peak electrical energy
kappa = p * freq
Q = 2 * np.pi * freq / kappa
Qp['Q_' + port_nm] = Q
return Qp
[docs] def calc_p_junction(self, variation, U_H, U_E, Ljs, Cjs):
'''
For a single specific mode.
Expected that you have specified the mode before calling this, :func:`~pyEPR.DistributedAnalysis.set_mode`.
Expected to precalc U_H and U_E for mode, will return pandas pd.Series object:
* junc_rect = ['junc_rect1', 'junc_rect2'] name of junc rectangles to integrate H over
* junc_len = [0.0001] specify in SI units; i.e., meters
* LJs = [8e-09, 8e-09] SI units
* calc_sign = ['junc_line1', 'junc_line2']
WARNING: Cjs is experimental.
This function assumes there are no lumped capacitors in model.
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
.. note::
U_E and U_H are the total peak energy. (NOT twice as in U_ and U_H other places)
.. warning::
Potential errors: If you dont have a line or rect by the right name you will prob
get an error of the type: com_error: (-2147352567, 'Exception occurred.',
(0, None, None, None, 0, -2147024365), None)
'''
# ------------------------------------------------------------
# Calculate all peak voltage and currents for all junctions in a given mode
method = self.pinfo.options.method_calc_P_mj
I_peak_ = {}
V_peak_ = {}
Sj = pd.Series({}, dtype='float64')
for j_name, j_props in self.pinfo.junctions.items():
logger.debug(f'Calculating participations for {(j_name, j_props)}')
Lj = Ljs[j_name]
Cj = Cjs[j_name]
line_name = j_props['line']
if method == 'J_surf_mag': # old method
_I_peak_1 = self.calc_avg_current_J_surf_mag(
variation, j_props['rect'], line_name)
# could also use this to back out the V_peak using the impedances as in the line
# below for now, keep both methods
_I_peak_2, _V_peak_2, _ = self.calc_current_using_line_voltage(
variation, line_name, Lj, Cj)
logger.debug(
f'Difference in I_Peak calculation ala the two methods: {(_I_peak_1,_I_peak_2)}')
V_peak = _V_peak_2 # make sure this is signed
I_peak = _I_peak_1
elif method == 'line_voltage': # new preferred method
I_peak, V_peak, _ = self.calc_current_using_line_voltage(
variation, line_name, Lj, Cj)
else:
raise NotImplementedError('Other calculation methods\
(self.pinfo.options.method_calc_P_mj) are possible but not implemented here. ')
# save results
I_peak_[j_name] = I_peak
V_peak_[j_name] = V_peak
Sj['s_' + j_name] = _Smj = 1 if V_peak > 0 else - 1
# REPORT preliminary
pmj_ind = 0.5*Ljs[j_name] * I_peak**2 / U_E
pmj_cap = 0.5*Cjs[j_name] * V_peak**2 / U_E
#print('\tpmj_ind=',pmj_ind, Ljs[j_name], U_E)
self.I_peak = I_peak
self.V_peak = V_peak
self.Ljs = Ljs
self.Cjs = Cjs
print(
f'\t{j_name:<15} {pmj_ind:>8.6g}{("(+)"if _Smj else "(-)"):>5s} {pmj_cap:>8.6g}')
#print('\tV_peak=', V_peak)
# ------------------------------------------------------------
# Calculate participation from the peak voltage and currents
#
# All junction capacitive and inductive lumped energies - all peak
U_J_inds = {j_name: 0.5*Ljs[j_name] * I_peak_[j_name]
** 2 for j_name in self.pinfo.junctions}
U_J_caps = {j_name: 0.5*Cjs[j_name] * V_peak_[j_name]
** 2 for j_name in self.pinfo.junctions}
U_tot_ind = U_H + sum(list(U_J_inds.values())) # total
U_tot_cap = U_E + sum(list(U_J_caps.values()))
# what to use for the norm? U_tot_cap or the mean of U_tot_ind and U_tot_cap?
# i.e., (U_tot_ind + U_tot_cap)/2
U_norm = U_tot_cap
U_diff = (U_tot_cap-U_tot_ind)/(U_tot_cap+U_tot_ind)
print("\t\t"f"(U_tot_cap-U_tot_ind)/mean={U_diff*100:.2f}%")
if abs(U_diff) > 0.15:
print('WARNING: This simulation must not have converged well!!!\
The difference in the total cap and ind energies is larger than 10%.\
Proceed with caution.')
Pj = pd.Series(OrderedDict([(j_name, Uj_ind/U_norm)
for j_name, Uj_ind in U_J_inds.items()]))
PCj = pd.Series(OrderedDict([(j_name, Uj_cap/U_norm)
for j_name, Uj_cap in U_J_caps.items()]))
# print('\t{:<15} {:>8.6g} {:>5s}'.format(
# j_name,
# Pj['p_' + j_name],
# '+' if Sj['s_' + j_name] > 0 else '-'))
return Pj, Sj, PCj, pd.Series(I_peak), pd.Series(V_peak), \
{'U_J_inds': U_J_inds,
'U_J_caps': U_J_caps,
'U_H': U_H,
'U_E': U_E,
'U_tot_ind': U_tot_ind,
'U_tot_cap': U_tot_cap,
'U_norm': U_norm,
'U_diff': U_diff}
[docs] def get_previously_analyzed(self):
"""
Return previously analyzed data.
Does not yet handle data that was previously saved in a filename.
"""
# TODO: maybe load from data_file
# Rerun previously analyze variations from load filename
return self._previously_analyzed
[docs] def get_junctions_L_and_C(self, variation: str):
"""
Returns a pandas Series with the index being the junction name as specified in the
project_info.
The values in the series are numeric and in SI base units, i.e., not nH but Henries,
and not fF but Farads.
Args:
variation (str) : label such as '0' or 'all', in which case return
pandas table for all variations
"""
if variation == 'all':
# for all variations and concat
raise NotImplementedError() # TODO
else:
Ljs = pd.Series({}, dtype='float64')
Cjs = pd.Series({}, dtype='float64')
for junc_name, val in self.pinfo.junctions.items(): # junction nickname
_variables = self._hfss_variables[variation]
def _parse(name): return ureg.Quantity(
_variables['_'+val[name]]).to_base_units().magnitude
Ljs[junc_name] = _parse('Lj_variable')
Cjs[junc_name] = 2E-15 # _parse(
# 'Cj_variable') if 'Cj_variable' in val else 0
return Ljs, Cjs
[docs] def do_EPR_analysis(self,
variations: list = None,
modes=None,
append_analysis=True):
"""
Main analysis routine
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Optional Parameters:
------------------------
variations : list | None
Example list of variations is ['0', '1']
A variation is a combination of project/design variables in an optimetric sweep
modes : list | None
Modes to analyze
for example modes = [0, 2, 3]
append_analysis (bool) :
When we run the Ansys analysis, should we redo any variations that we have already done?
Ansys Notes:
------------------------
Assumptions:
Low dissipation (high-Q).
It is easier to assume no lumped capacitors to simply calculations, but we have
recently added Cj_variable as a new feature that is begin tested to handle capacitors.
See the paper.
Using the results:
------------------------
Load results with epr.QuantumAnalysis class
Example use:
----------------
.. code-block:: python
:linenos:
eprd = epr.DistributedAnalysis(pinfo)
eprd.do_EPR_analysis(append_analysis=False)
"""
if not modes is None:
assert max(modes) < self.n_modes, 'Non-existing mode selected. \n'\
f'The possible modes are between 0 and {self.n_modes-1}.'
if len(modes) != len(set(modes)):
logger.warn(f'Select each mode only once! Fixing...\n'\
'modes: {modes} --> {list(set(modes))}')
modes = list(set(modes))
# Track the total timing
self._run_time = time.strftime('%Y%m%d_%H%M%S', time.localtime())
# Update the latest hfss variation information
self.update_ansys_info()
variations = variations or self.variations
modes = modes or range(self.n_modes)
self.modes = modes
self.pinfo.save()
# Main loop - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# TODO: Move inside of loop to function calle self.analyze_variation
for ii, variation in enumerate(variations):
print(f'\nVariation {variation} [{ii+1}/{len(variations)}]')
# Previously analyzed and we should re analyze
if append_analysis and variation in self.get_previously_analyzed():
print_NoNewLine(' previously analyzed ...\n')
continue
# QUESTION! should we set the current variation, can this save time, set the variables
# If not, clear the results
self.results[variation] = Dict()
self.lv = self._get_lv(variation)
time.sleep(0.4)
if self.has_fields() == False:
logger.error(f" Error: HFSS does not have field solution for variation={ii}.\
Skipping this mode in the analysis")
continue
try:
# This should allow us to load the fields only once, and then do the calculations
# faster. The loading of the fields does not happen here, but a the first ClcEval call.
# This could fail if more variables are added after the simulation is completed.
self.set_variation(variation)
except Exception as e:
print('\tERROR: Could not set the variation string.'
'\nPossible causes: Did you add a variable after the simulation was already solved? '
'\nAttempting to proceed nonetheless, should be just slower ...')
# use nonframe because old style
freqs_bare_GHz, Qs_bare = self.get_freqs_bare_pd(
variation, frame=False)
# update to the latest
self._hfss_variables[variation] = pd.Series(
self.get_variables(variation=variation))
# Create Ljs and Cjs series for a variation
Ljs, Cjs = self.get_junctions_L_and_C(variation)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# This is crummy now. use dict
#result = dict()
Om = OrderedDict() # Matrix of angular frequency (of analyzed modes)
Pm = OrderedDict() # Participation P matrix
Sm = OrderedDict() # Sign S matrix
Qm_coupling = OrderedDict() # Quality factor matrix
SOL = OrderedDict() # Other results
Pm_cap = OrderedDict()
I_peak = OrderedDict()
V_peak = OrderedDict()
ansys_energies = OrderedDict()
for mode in modes: # integer of mode number [0,1,2,3,..]
# Load fields for mode
self.set_mode(mode)
# Get HFSS solved frequencies
_Om = pd.Series({}, dtype='float64')
temp_freq = freqs_bare_GHz[mode]
_Om['freq_GHz'] = temp_freq # freq
Om[mode] = _Om
print(
'\n'f' \033[1mMode {mode} at {"%.2f" % temp_freq} GHz [{mode+1}/{self.n_modes}]\033[0m')
# EPR Hamiltonian calculations
# Calculation global energies and report
# Magnetic
print(' Calculating ℰ_magnetic', end=',')
try:
self.U_H = self.calc_energy_magnetic(variation)
except Exception as e:
tb = sys.exc_info()[2]
print("\n\nError:\n", e)
raise(Exception(' Did you save the field solutions?\n\
Failed during calculation of the total magnetic energy.\
This is the first calculation step, and is indicative that there are \
no field solutions saved. ').with_traceback(tb))
# Electric
print('ℰ_electric')
self.U_E = self.calc_energy_electric(variation)
# the unnormed
sol = pd.Series({'U_H': self.U_H, 'U_E': self.U_E})
# Fraction - report the peak energy, properly normalized
# the 2 is from the calculation methods
print(f""" {'(ℰ_E-ℰ_H)/ℰ_E':>15s} {'ℰ_E':>9s} {'ℰ_H':>9s}
{100*(self.U_E - self.U_H)/self.U_E:>15.1f}% {self.U_E/2:>9.4g} {self.U_H/2:>9.4g}\n""")
# Calculate EPR for each of the junctions
print(
f' Calculating junction energy participation ration (EPR)\n\tmethod=`{self.pinfo.options.method_calc_P_mj}`. First estimates:')
print(
f"\t{'junction':<15s} EPR p_{mode}j sign s_{mode}j (p_capacitive)")
Pm[mode], Sm[mode], Pm_cap[mode], I_peak[mode], V_peak[mode], ansys_energies[mode] = self.calc_p_junction(
variation, self.U_H/2., self.U_E/2., Ljs, Cjs)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# EPR Dissipative calculations -- should be a function block below
# TODO: this should really be passed as argument to the functions rather than a
# property of the calss I would say
self.omega = 2*np.pi*freqs_bare_GHz[mode]
Qm_coupling[mode] = self.calc_Q_external(variation,
freqs_bare_GHz[mode],
self.U_E)
# get seam Q
if self.pinfo.dissipative['seams']:
for seam in self.pinfo.dissipative['seams']:
sol = pd.concat([sol, self.get_Qseam(seam, mode, variation, self.U_H)])
# get Q dielectric
if self.pinfo.dissipative['dielectrics_bulk']:
for dielectric in self.pinfo.dissipative['dielectrics_bulk']:
sol = pd.concat([sol, self.get_Qdielectric(dielectric, mode, variation, self.U_E)])
# get Q surface
if self.pinfo.dissipative['dielectric_surfaces']:
if self.pinfo.dissipative['dielectric_surfaces'] == 'all':
sol = pd.concat([sol, self.get_Qsurface_all(mode, variation, self.U_E)])
else:
for surface, properties in self.pinfo.dissipative['dielectric_surfaces'].items():
sol = pd.concat([sol, self.get_Qsurface(mode, variation, surface, self.U_E, properties)])
SOL[mode] = sol
# Save
self._update_results(variation, Om, Pm, Sm, Qm_coupling, SOL,
freqs_bare_GHz, Qs_bare, Ljs, Cjs,
Pm_cap, I_peak, V_peak,
ansys_energies,
self._hfss_variables[variation])
self.save()
self._previously_analyzed.add(variation)
print('\nANALYSIS DONE. Data saved to:\n\n' +
str(self.data_filename)+'\n\n')
return self.data_filename, variations
def _update_results(self, variation: str, Om, Pm, Sm, Qm_coupling, sols,
freqs_bare_GHz, Qs_bare, Ljs, Cjs, Pm_cap, I_peak, V_peak,
ansys_energies, _hfss_variables):
'''
Save variation
'''
# raw, not normalized - DataFrames
self.results[variation]['Pm'] = pd.DataFrame(Pm).transpose()
self.results[variation]['Pm_cap'] = pd.DataFrame(Pm_cap).transpose()
self.results[variation]['Sm'] = pd.DataFrame(Sm).transpose()
self.results[variation]['Om'] = pd.DataFrame(Om)
self.results[variation]['sols'] = pd.DataFrame(sols).transpose()
self.results[variation]['Qm_coupling'] = pd.DataFrame(
Qm_coupling).transpose()
self.results[variation]['Ljs'] = Ljs # pd.Series
self.results[variation]['Cjs'] = Cjs # pd.Series
self.results[variation]['Qs'] = Qs_bare
self.results[variation]['freqs_hfss_GHz'] = freqs_bare_GHz
self.results[variation]['hfss_variables'] = _hfss_variables
self.results[variation]['modes'] = self.modes
# mostly for debug info
self.results[variation]['I_peak'] = pd.Series(I_peak)
self.results[variation]['V_peak'] = pd.Series(V_peak)
self.results[variation]['ansys_energies'] = ansys_energies # dict
self.results[variation]['mesh'] = None
self.results[variation]['convergence'] = None
self.results[variation]['convergence_f_pass'] = None
if self.options.save_mesh_stats:
self.results[variation]['mesh'] = self.get_mesh_statistics(
variation) # dataframe
self.results[variation]['convergence'] = self.get_convergence(
variation)
self.results[variation]['convergence_f_pass'] = self.hfss_report_f_convergence(
variation, save_csv=False) # dataframe
[docs] @staticmethod
def results_variations_on_inside(results: dict):
"""
Switches the order on result of variations. Reverse dict.
"""
# TODO: THis need to be changed, wont work in the future with updating result etc.
# if i want to make a base class
keys = set()
variations = list(results.keys())
# Get all keys
for variation in variations:
result = results[variation]
keys.update(result.keys())
new_res = dict()
for key in keys:
new_res[key] = {variation: results[variation].get(key, None)
for variation in variations}
# Conver to pandas Dataframe if all are pd.Series
if all(isinstance(new_res[key][variation], pd.Series) for variation in variations):
# print(key) # Conver these to dataframe
# Variations will become columns
new_res[key] = pd.DataFrame(new_res[key])
new_res[key].columns.name = 'variation'
# sort_df_col : maybe sort
return new_res # dict of keys now
[docs] def save(self, project_info: dict = None):
"""Save results to self.data_filename
Keyword Arguments:
project_info {dict} -- [description] (default: {None})
"""
if project_info is None:
project_info = self.pinfo.save()
to_save = dict(
project_info=project_info,
results=self.results,
)
with open(str(self.data_filename), 'wb') as handle:
pickle.dump(to_save, handle) # , protocol=pickle.HIGHEST_PROTOCOL)
[docs] def load(self, filepath=None):
"""Utility function to load results file
Keyword Arguments:
filepath {[type]} -- [description] (default: {None})
"""
filepath = filepath or self.data_filename
with open(str(filepath), 'rb') as handle:
loaded = pickle.load(handle)
return loaded
[docs] def get_mesh_statistics(self, variation='0'):
'''
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
A pandas dataframe, such as
.. code-block:: text
:linenos:
Name Num Tets Min edge length Max edge length RMS edge length Min tet vol Max tet vol Mean tet vol Std Devn (vol)
0 Region 909451 0.000243 0.860488 0.037048 6.006260e-13 0.037352 0.000029 6.268190e-04
1 substrate 1490356 0.000270 0.893770 0.023639 1.160090e-12 0.031253 0.000007 2.309920e-04
'''
variation = self._list_variations[ureg(variation)]
return self.setup.get_mesh_stats(variation)
[docs] def get_convergence(self, variation='0'):
'''
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
A pandas DataFrame object
.. code-block:: text
:linenos:
Solved Elements Max Delta Freq. % Pass Number
1 128955 NaN
2 167607 11.745000
3 192746 3.208600
4 199244 1.524000
'''
variation = self._list_variations[ureg(variation)]
df, _ = self.setup.get_convergence(variation)
return df
[docs] def get_convergence_vs_pass(self, variation='0'):
'''
Makes a plot in HFSS that return a pandas dataframe
Args:
variation (str): A string identifier of the variation,
such as '0', '1', ...
Returns:
Returns a convergence vs pass number of the eignemode freqs.
.. code-block:: text
:linenos:
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
'''
return self.hfss_report_f_convergence(variation)
[docs] def set_mode(self, mode_num, phase=0):
'''
Set source excitations should be used for fields post processing.
Counting modes from 0 onward
'''
assert self.setup, "ERROR: There is no 'setup' connected. \N{face with medical mask}"
if mode_num < 0:
logger.error('Too small a mode number')
self.solutions.set_mode(mode_num + 1, phase)
if self.has_fields() == False:
logger.warning(f" Error: HFSS does not have field solution for variation={mode_num}.\
Skipping this mode in the analysis \N{face with medical mask}")
self.fields = self.setup.get_fields()
[docs] def has_fields(self, variation: str = None):
'''
Determine if fields exist for a particular solution.
Just calls `self.solutions.has_fields(variation_string)`
Args:
variation (str): String of variation label, such as '0' or '1'. If None, gets the nominal variation
'''
if self.solutions:
#print('variation=', variation)
variation_string = self.get_variation_string(variation)
return self.solutions.has_fields(variation_string)
else:
return False
[docs] def hfss_report_f_convergence(self, variation='0', save_csv=True):
'''
Create a report inside HFSS to plot the converge of freq and style it.
Saves report to csv file.
Returns a convergence vs pass number of the eignemode freqs.
Returns a pandas dataframe:
.. 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
'''
# TODO: Move to class for reporter ?
if not self.setup:
logger.error('NO SETUP PRESENT - hfss_report_f_convergence.')
return None
if not self.design.solution_type == 'Eigenmode':
return None
oDesign = self.design
variation = self._get_lv(variation)
report = oDesign._reporter
# Create report
ycomp = [f"re(Mode({i}))" for i in range(1, 1+self.n_modes)]
params = ["Pass:=", ["All"]]+variation
report_name = "Freq. vs. pass"
if report_name in report.GetAllReportNames():
report.DeleteReports([report_name])
self.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+self.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(self.data_dir)/'hfss_eig_f_convergence.csv'
report.ExportToFile(report_name, path)
logger.info(f'Saved convergences to {path}')
return pd.read_csv(path, index_col=0)
except Exception as e:
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
[docs] def hfss_report_full_convergence(self, fig=None, _display=True):
"""Plot a full report of teh convergences of an eigenmode analysis for a
a given variation. Makes a plot inside hfss too.
Keyword Arguments:
fig {matplotlib figure} -- Optional figure (default: {None})
_display {bool} -- Force display or not. (default: {True})
Returns:
[type] -- [description]
"""
if fig is None:
fig = plt.figure(figsize=(11, 3.))
for variation, variation_labels in self.get_variations().items():
fig.clf()
# Grid spec and axes; height_ratios=[4, 1], wspace=0.5
gs = mpl.gridspec.GridSpec(1, 3, width_ratios=[1.2, 1.5, 1])
axs = [fig.add_subplot(gs[i]) for i in range(3)]
logger.info(f'Creating report for variation {variation}')
convergence_t = self.get_convergence(variation=variation)
convergence_f = self.hfss_report_f_convergence(variation=variation)
axs[0].set_ylabel(variation_labels.replace(' ', '\n')) # add variation labels to y-axis of first plot
ax0t = axs[1].twinx()
plot_convergence_f_vspass(axs[0], convergence_f)
plot_convergence_max_df(axs[1], convergence_t.iloc[:, 1])
plot_convergence_solved_elem(ax0t, convergence_t.iloc[:, 0])
plot_convergence_maxdf_vs_sol(axs[2], convergence_t.iloc[:, 1],
convergence_t.iloc[:, 0])
fig.tight_layout(w_pad=0.1) # pad=0.0, w_pad=0.1, h_pad=1.0)
if _display:
from IPython.display import display
display(fig)
return fig
[docs] def quick_plot_frequencies(self, swp_variable='variations', ax=None):
"""
Quick plot of frequencies from HFSS
"""
fs = self.get_ansys_frequencies_all(swp_variable)
ax = ax or plt.gca()
fs['Freq. (GHz)'].unstack(0).transpose().plot(marker='o', ax=ax)
ax.set_ylabel('Ansys frequencies (MHz)')
ax.grid(alpha=0.2)
return fs