"""
ProjectInfo — primary configuration class for pyEPR's HFSS interface.
Stores connection handles (app, desktop, project, design, setup) and user-defined
parameters (junctions, dissipative objects) needed by
:class:`~pyEPR.core_distributed_analysis.DistributedAnalysis`.
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 .solution_types import DRIVEN_MODAL_NAMES, DRIVEN_TERMINAL_NAMES
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 is not 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,
junctions: dict = 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``.
junctions (dict, optional) : Pre-populate junction parameters at construction time.
Keys are junction names; values are dicts with keys ``Lj_variable``, ``rect``,
``line``, and optionally ``Cj_variable``. Equivalent to setting
``pinfo.junctions[name] = {...}`` after construction.
Defaults to ``None``.
do_connect (bool) : Connect to the Ansys Desktop API on construction.
Set to ``False`` to build a ``ProjectInfo`` object offline (e.g. for testing).
Defaults to ``True``.
Returns:
ProjectInfo: Configured instance, optionally connected to Ansys.
"""
# Path: format path correctly to system convention
self.project_path = (
str(Path(project_path)) if project_path is not 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
if junctions:
self.junctions.update(junctions)
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) -> dict:
"""Serialise project info to a dictionary of pandas objects.
Returns
-------
dict
Keys: ``"pinfo"`` (Series of scalar attributes), ``"dissip"`` (Series),
``"options"`` (Series), ``"junctions"`` (DataFrame), ``"ports"`` (DataFrame).
"""
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) -> None:
"""Open the Ansys Desktop application and attach to the target project.
Sets ``self.app``, ``self.desktop``, ``self.project``,
``self.project_name``, and ``self.project_path``.
If ``project_name`` is ``None``, attaches to the currently active project
in the running Ansys Desktop instance.
"""
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) -> None:
"""Attach to an HFSS design within the open project.
Sets ``self.design`` and ``self.design_name``.
Parameters
----------
design_name : str, optional
Name of the design to open. If ``None``, attaches to the currently
active design. Raises if the named design does not exist.
"""
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) -> None:
"""Attach to a simulation setup within the active design.
If ``setup_name`` was specified in the constructor and the setup exists,
that setup is used. If ``setup_name`` is ``None``, the first available
setup is selected automatically. If no setups exist, a default one is
created for Eigenmode, DrivenModal, DrivenTerminal, and Q3D designs.
Raises
------
ValueError
If ``setup_name`` was specified but does not exist in the design.
ValueError
If the design solution type is not supported.
"""
# 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 in DRIVEN_MODAL_NAMES:
logger.warning("\tCreating driven modal default setup.")
setup = self.design.create_dm_setup()
elif self.design.solution_type in DRIVEN_TERMINAL_NAMES:
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()
else:
raise ValueError(
f"Unsupported solution type: {self.design.solution_type!r}"
)
self.setup_name = setup.name
else:
if self.setup_name:
if self.setup_name not in setup_names:
raise ValueError(
f"Setup '{self.setup_name}' not found in design. "
f"Available setups: {setup_names}"
)
# else: keep the user-specified name as-is
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) -> "ProjectInfo":
"""Establish a full connection to the Ansys Desktop API.
Calls :meth:`connect_project`, :meth:`connect_design`, and
:meth:`connect_setup` in sequence. Logs connection status at each step.
Returns
-------
ProjectInfo
Returns ``self`` to allow chaining (e.g. ``pinfo = ProjectInfo(...).connect()``).
"""
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) -> bool:
"""Return ``True`` if fully connected to Ansys (app, desktop, project, design, and setup).
Returns
-------
bool
``True`` when all five COM handles are non-``None``.
"""
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) -> None:
"""Release all COM handles and disconnect from the Ansys Desktop API.
Raises
------
AssertionError
If not currently connected. Call :meth:`check_connected` first,
or use the context manager form (``with ProjectInfo(...) as pinfo:``)
which guards the call automatically.
"""
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()
def __enter__(self) -> "ProjectInfo":
"""Support use as a context manager. Returns ``self``."""
return self
def __exit__(self, *args) -> None:
"""Disconnect from Ansys on context-manager exit (if connected)."""
if self.check_connected():
self.disconnect()
[docs]
def get_variable_value(self, name: str) -> str:
"""Return the value of a local design variable as a string.
Parameters
----------
name : str
Name of the design-level variable (e.g. ``'Lj'``).
Returns
-------
str
Value string as stored in HFSS (e.g. ``'10nH'``).
Note
----
Only reads **local** design variables. Global project variables
(prefixed with ``$``) are not accessible via this method.
"""
return self.design.get_variable_value(name)
# UTILITY FUNCTIONS
[docs]
def get_dm(self) -> tuple:
"""Return the active design and 3-D modeler as a tuple.
Returns
-------
tuple
``(design, design.modeler)`` — the :class:`~pyEPR.ansys.HfssDesign`
and :class:`~pyEPR.ansys.HfssModeler` handles.
Example
-------
.. code-block:: python
oDesign, oModeler = pinfo.get_dm()
"""
return self.design, self.design.modeler
[docs]
def get_all_variables_names(self) -> list:
"""Return all project-level and design-level variable names.
Returns
-------
list of str
Concatenation of project variable names and local design variable names.
Does **not** include global ``$``-prefixed project variables from other designs.
"""
return self.project.get_variable_names() + self.design.get_variable_names()
[docs]
def get_all_object_names(self) -> list:
"""Return the names of all 3-D modeler objects in the active design.
Covers Non Model, Solids, Unclassified, Sheets, and Lines groups.
Returns
-------
list of str
All object names as reported by the HFSS 3-D Modeler.
"""
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) -> None:
"""Validate that all junction parameters refer to objects that exist in HFSS.
Checks that each junction's ``Lj_variable`` exists as a design or project
variable, and that ``rect`` and ``line`` exist as 3-D modeler objects.
Raises
------
AssertionError
Descriptive message identifying the junction name and the missing
variable or object.
Note
----
Also verify the physical length of junction rectangles and polylines if
you modify geometry after the initial setup.
"""
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()