Source code for pyEPR.core_distributed_analysis

"""
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