Source code for pyEPR.project_info

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

from __future__ import print_function  # Python 2.7 and 3 compatibility

import sys
from pathlib import Path

import pandas as pd

from . import Dict, ansys, config, logger
from .toolbox.pythonic import get_instance_vars

diss_opt = [
    'dielectrics_bulk', 'dielectric_surfaces', 'resistive_surfaces', 'seams'
]


[docs]class ProjectInfo(object): """ Primary class to store interface information between ``pyEPR`` and ``Ansys``. * **Ansys:** stores and provides easy access to the ansys interface classes :py:class:`pyEPR.ansys.HfssApp`, :py:class:`pyEPR.ansys.HfssDesktop`, :py:class:`pyEPR.ansys.HfssProject`, :py:class:`pyEPR.ansys.HfssDesign`, :py:class:`pyEPR.ansys.HfssSetup` (which, if present could nbe a subclass, such as a driven modal setup :py:class:`pyEPR.ansys.HfssDMSetup`, eigenmode :py:class:`pyEPR.ansys.HfssEMSetup`, or Q3D :py:class:`pyEPR.ansys.AnsysQ3DSetup`), the 3D modeler to design geometry :py:class:`pyEPR.ansys.HfssModeler`. * **Junctions:** The class stores params about the design that the user puts will use, such as the names and properties of the junctions, such as which rectangle and line is associated with which junction. Note: **Junction parameters.** The junction parameters are stored in the ``self.junctions`` ordered dictionary A Josephson tunnel junction has to have its parameters specified here for the analysis. Each junction is given a name and is specified by a dictionary. It has the following properties: * ``Lj_variable`` (str): Name of HFSS variable that specifies junction inductance Lj defined on the boundary condition in HFSS. WARNING: DO NOT USE Global names that start with $. * ``rect`` (str): String of Ansys name of the rectangle on which the lumped boundary condition is defined. * ``line`` (str): Name of HFSS polyline which spans the length of the rectangle. Used to define the voltage across the junction. Used to define the current orientation for each junction. Used to define sign of ZPF. * ``length`` (str): Length in HFSS of the junction rectangle and line (specified in meters). To create, you can use :code:`epr.parse_units('100um')`. * ``Cj_variable`` (str, optional) [experimental]: Name of HFSS variable that specifies junction inductance Cj defined on the boundary condition in HFSS. DO NOT USE Global names that start with ``$``. Warning: To define junctions, do **NOT** use global names! I.e., do not use names in ansys that start with ``$``. Note: **Junction parameters example .** To define junction parameters, see the following example .. code-block:: python :linenos: # Create project infor class pinfo = ProjectInfo() # Now, let us add a junction called `j1`, with the following properties pinfo.junctions['j1'] = { 'Lj_variable' : 'Lj_1', # name of Lj variable in Ansys 'rect' : 'jj_rect_1', 'line' : 'jj_line_1', #'Cj' : 'Cj_1' # name of Cj variable in Ansys - optional } To extend to define 5 junctions in bulk, we could use the following script .. code-block:: python :linenos: n_junctions = 5 for i in range(1, n_junctions + 1): pinfo.junctions[f'j{i}'] = {'Lj_variable' : f'Lj_{i}', 'rect' : f'jj_rect_{i}', 'line' : f'jj_line_{i}'} .. _Google Python Style Guide: http://google.github.io/styleguide/pyguide.html """ class _Dissipative: """ Deprecating the _Dissipative class and turning it into a dictionary. This is used to message people on the deprecation so they could change their scripts. """ def __init__(self): self['pinfo'] = None for opt in diss_opt: self[opt] = None def __setitem__(self, key, value): # --- check valid inputs --- if not (key in diss_opt or key == 'pinfo'): raise ValueError(f"No such parameter {key}") if key != 'pinfo' and (not isinstance(value, (list, dict)) or \ not all(isinstance(x, str) for x in value)) and (value != None): raise ValueError(f'dissipative[\'{key}\'] must be a list of strings ' \ 'containing names of models in the project or dictionary of strings of models containing ' \ 'material loss properties!' ) if key != 'pinfo' and hasattr(self['pinfo'], 'design'): for x in value: if x not in self['pinfo'].get_all_object_names(): raise ValueError( f'\'{x}\' is not an object in the HFSS project') super().__setattr__(key, value) def __getitem__(self, attr): if not (attr in diss_opt or attr == 'pinfo'): raise AttributeError(f'dissipative has no attribute "{attr}". '\ f'The possible attributes are:\n {str(diss_opt)}') return super().__getattribute__(attr) def __setattr__(self, attr, value): logger.warning( f"DEPRECATED!! use pinfo.dissipative['{attr}'] = {value} instead!" ) self[attr] = value def __getattr__(self, attr): raise AttributeError(f'dissipative has no attribute "{attr}". '\ f'The possible attributes are:\n {str(diss_opt)}') def __getattribute__(self, attr): if attr in diss_opt: logger.warning( f"DEPRECATED!! use pinfo.dissipative['{attr}'] instead!") return super().__getattribute__(attr) def __repr__(self): return str(self.data()) def data(self): """Return dissipative as dictionary""" return {str(opt): self[opt] for opt in diss_opt} def __init__(self, project_path: str = None, project_name: str = None, design_name: str = None, setup_name: str = None, dielectrics_bulk: list =None, dielectric_surfaces: list = None, resistive_surfaces: list= None, seams: list= None, do_connect: bool = True): """ Keyword Arguments: project_path (str) : Directory path to the hfss project file. Should be the directory, not the file. Defaults to ``None``; i.e., assumes the project is open, and thus gets the project based on `project_name`. project_name (str) : Name of the project within the project_path. Defaults to ``None``, which will get the current active one. design_name (str) : Name of the design within the project. Defaults to ``None``, which will get the current active one. setup_name (str) : Name of the setup within the design. Defaults to ``None``, which will get the current active one. dielectrics_bulk (list(str)) : List of names of dielectric bulk objects. Defaults to ``None``. dielectric_surfaces (list(str)) : List of names of dielectric surfaces. Defaults to ``None``. resistive_surfaces (list(str)) : List of names of resistive surfaces. Defaults to ``None``. seams (list(str)) : List of names of seams. Defaults to ``None``. do_connect (bool) [additional]: Do create connection to Ansys or not? Defaults to ``True``. """ # Path: format path correctly to system convention self.project_path = str(Path(project_path)) \ if not (project_path is None) else None self.project_name = project_name self.design_name = design_name self.setup_name = setup_name # HFSS design: describe junction parameters # TODO: introduce modal labels self.junctions = Dict() # See above for help self.ports = Dict() # Dissipative HFSS volumes and surfaces self.dissipative = self._Dissipative() for opt in diss_opt: self.dissipative[opt] = locals()[opt] self.options = config.ansys # Connected to HFSS variable self.app = None self.desktop = None self.project = None self.design = None self.setup = None if do_connect: self.connect() self.dissipative['pinfo'] = self _Forbidden = [ 'app', 'design', 'desktop', 'project', 'dissipative', 'setup', '_Forbidden', 'junctions' ]
[docs] def save(self): ''' Return all the data in a dictionary form that can be used to be saved ''' return dict( pinfo=pd.Series(get_instance_vars(self, self._Forbidden)), dissip=pd.Series(self.dissipative.data()), options=pd.Series(get_instance_vars(self.options), dtype='object'), junctions=pd.DataFrame(self.junctions), ports=pd.DataFrame(self.ports), )
[docs] def connect_project(self): """Sets self.app self.desktop self.project self.project_name self.project_path """ logger.info('Connecting to Ansys Desktop API...') self.app, self.desktop, self.project = ansys.load_ansys_project( self.project_name, self.project_path) if self.project: # TODO: should be property? self.project_name = self.project.name self.project_path = self.project.get_path()
[docs] def connect_design(self, design_name: str = None): """Sets self.design self.design_name """ if design_name is not None: self.design_name = design_name designs_in_project = self.project.get_designs() if not designs_in_project: self.design = None logger.info( f'No active design found (or error getting active design).') return if self.design_name is None: # Look for the active design try: self.design = self.project.get_active_design() self.design_name = self.design.name logger.info( '\tOpened active design\n' f'\tDesign: {self.design_name} [Solution type: {self.design.solution_type}]' ) except Exception as e: # No active design self.design = None self.design_name = None logger.info( f'No active design found (or error getting active design). Note: {e}' ) else: try: self.design = self.project.get_design(self.design_name) logger.info( '\tOpened active design\n' f'\tDesign: {self.design_name} [Solution type: {self.design.solution_type}]' ) except Exception as e: _traceback = sys.exc_info()[2] logger.error(f"Original error \N{loudly crying face}: {e}\n") raise (Exception(' Did you provide the correct design name?\ Failed to pull up design. \N{loudly crying face}'). with_traceback(_traceback))
[docs] def connect_setup(self): """Connect to the first available setup or create a new in eigenmode and driven modal Raises: Exception: [description] """ # Setup if self.design is not None: try: setup_names = self.design.get_setup_names() if len(setup_names) == 0: logger.warning('\tNo design setup detected.') setup = None if self.design.solution_type == 'Eigenmode': logger.warning('\tCreating eigenmode default setup.') setup = self.design.create_em_setup() elif self.design.solution_type == 'DrivenModal': logger.warning('\tCreating driven modal default setup.') setup = self.design.create_dm_setup() elif self.design.solution_type == 'DrivenTerminal': logger.warning('\tCreating driven terminal default setup.') setup = self.design.create_dt_setup() elif self.design.solution_type == 'Q3D': logger.warning('\tCreating Q3D default setup.') setup = self.design.create_q3d_setup() self.setup_name = setup.name else: self.setup_name = setup_names[0] # get the actual setup if there is one self.get_setup(self.setup_name) except Exception as e: _traceback = sys.exc_info()[2] logger.error(f"Original error \N{loudly crying face}: {e}\n") raise Exception(' Did you provide the correct setup name?\ Failed to pull up setup. \N{loudly crying face}' ).with_traceback(_traceback) else: self.setup = None self.setup_name = None
[docs] def connect(self): """ Do establish connection to Ansys desktop. Connects to project and then get design and setup """ self.connect_project() if not self.project: logger.info('\tConnection to Ansys NOT established. \n') if self.project: self.connect_design() self.connect_setup() # Finalize if self.project: self.project_name = self.project.name if self.design: self.design_name = self.design.name if self.project and self.design: logger.info( f'\tConnected to project \"{self.project_name}\" and design \"{self.design_name}\" \N{grinning face} \n' ) if not self.project: logger.info( '\t Project not detected in Ansys. Is there a project in your desktop app? \N{thinking face} \n' ) if not self.design: logger.info( f'\t Connected to project \"{self.project_name}\". No design detected' ) return self
[docs] def get_setup(self, name: str): """ Connects to a specific setup for the design. Sets self.setup and self.setup_name. Args: name (str): Name of the setup. If the setup does not exist, then throws a logger error. Defaults to ``None``, in which case returns None """ if name is None: return None self.setup = self.design.get_setup(name=name) if self.setup is None: logger.error(f"Could not retrieve setup: {name}\n \ Did you give the right name? Does it exist?") self.setup_name = self.setup.name logger.info( f'\tOpened setup `{self.setup_name}` ({type(self.setup)})') return self.setup
[docs] def check_connected(self): """ Checks if fully connected including setup. """ return\ (self.setup is not None) and\ (self.design is not None) and\ (self.project is not None) and\ (self.desktop is not None) and\ (self.app is not None)
[docs] def disconnect(self): ''' Disconnect from existing Ansys Desktop API. ''' assert self.check_connected() is True,\ "It does not appear that you have connected to HFSS yet.\ Use the connect() method. \N{nauseated face}" self.project.release() self.desktop.release() self.app.release() ansys.release()
# UTILITY FUNCTIONS
[docs] def get_dm(self): ''' Utility shortcut function to get the design and modeler. .. code-block:: python oDesign, oModeler = pinfo.get_dm() ''' return self.design, self.design.modeler
[docs] def get_all_variables_names(self): """Returns array of all project and local design names.""" return self.project.get_variable_names( ) + self.design.get_variable_names()
[docs] def get_all_object_names(self): """Returns array of strings""" o_objects = [] for s in ["Non Model", "Solids", "Unclassified", "Sheets", "Lines"]: o_objects += self.design.modeler.get_objects_in_group(s) return o_objects
[docs] def validate_junction_info(self): """Validate that the user has put in the junction info correctly. Do not also forget to check the length of the rectangles/line of the junction if you change it. """ all_variables_names = self.get_all_variables_names() all_object_names = self.get_all_object_names() for jjnm, jj in self.junctions.items(): assert jj['Lj_variable'] in all_variables_names,\ """pyEPR ProjectInfo user error found \N{face with medical mask}: Seems like for junction `%s` you specified a design or project variable for `Lj_variable` that does not exist in HFSS by the name: `%s` """ % (jjnm, jj['Lj_variable']) for name in ['rect', 'line']: assert jj[name] in all_object_names, \ """pyEPR ProjectInfo user error found \N{face with medical mask}: Seems like for junction `%s` you specified a %s that does not exist in HFSS by the name: `%s` """ % (jjnm, name, jj[name])
def __del__(self): logger.info('Disconnected from Ansys HFSS')
# self.disconnect()