Source code for pyEPR.ansys

"""
pyEPR.ansys
    2014-present

Purpose:
    Handles Ansys interaction and control from version 2014 onward.
    Tested most extensively with V2016 and V2019R3.

@authors:
    Originally contributed by Phil Reinhold.
    Developed further by Zlatko Minev, Zaki Leghtas, and the pyEPR team.
    For the base version of hfss.py, see https://github.com/PhilReinhold/pyHFSS
"""

# Python 2.7 and 3 compatibility
from __future__ import division, print_function

import atexit
import io
import os
import re
import signal
import tempfile
import time
import types
from collections.abc import Iterable
from copy import copy
from numbers import Number
from pathlib import Path
from typing import List

import numpy as np
import pandas as pd
from sympy.parsing import sympy_parser

from . import logger
from .solution_types import DRIVEN_MODAL_NAMES, DRIVEN_TERMINAL_NAMES
from .solution_types import normalize as _normalize_solution_type

# Handle a  few usually troublesome to import packages, which the use may not have
# installed yet
try:
    import pythoncom
except (ImportError, ModuleNotFoundError):
    pass  # raise NameError ("pythoncom module not installed. Please install.")

try:
    # TODO: Replace `win32com` with Linux compatible package.
    # See Ansys python files in IronPython internal.
    from win32com.client import CDispatch, Dispatch
except (ImportError, ModuleNotFoundError):
    pass  # raise NameError ("win32com module not installed. Please install.")

try:
    from pint import UnitRegistry

    ureg = UnitRegistry()
    Q = ureg.Quantity
except (ImportError, ModuleNotFoundError):
    pass  # raise NameError ("Pint module not installed. Please install.")


##############################################################################
###

BASIS_ORDER = {"Zero Order": 0, "First Order": 1, "Second Order": 2, "Mixed Order": -1}

# UNITS
# LENGTH_UNIT         --- HFSS UNITS
# #Assumed default input units for ansys hfss
LENGTH_UNIT = "meter"
# LENGTH_UNIT_ASSUMED --- USER UNITS
# if a user inputs a blank number with no units in `parse_fix`,
# we can assume the following using
LENGTH_UNIT_ASSUMED = "mm"


[docs] def simplify_arith_expr(expr): try: out = repr(sympy_parser.parse_expr(str(expr))) return out except Exception: print("Couldn't parse", expr) raise
[docs] def increment_name(base, existing): if base not in existing: return base n = 1 def make_name(): return base + str(n) while make_name() in existing: n += 1 return make_name()
[docs] def extract_value_unit(expr, units): """ :type expr: str :type units: str :return: float """ try: return Q(expr).to(units).magnitude except Exception: try: return float(expr) except Exception: return expr
[docs] def extract_value_dim(expr): """ type expr: str """ return str(Q(expr).dimensionality)
[docs] def parse_entry(entry, convert_to_unit=LENGTH_UNIT): """ Should take a list of tuple of list... of int, float or str... For iterables, returns lists """ if not isinstance(entry, list) and not isinstance(entry, tuple): return extract_value_unit(entry, convert_to_unit) else: entries = entry _entry = [] for entry in entries: _entry.append(parse_entry(entry, convert_to_unit=convert_to_unit)) return _entry
[docs] def fix_units(x, unit_assumed=None): """ Convert all numbers to string and append the assumed units if needed. For an iterable, returns a list """ unit_assumed = LENGTH_UNIT_ASSUMED if unit_assumed is None else unit_assumed if isinstance(x, str): # Check if there are already units defined, assume of form 2.46mm or 2.0 or 4. if x[-1].isdigit() or x[-1] == ".": # number return x + unit_assumed else: # units are already applied return x elif isinstance(x, Number): return fix_units(str(x) + unit_assumed, unit_assumed=unit_assumed) elif isinstance(x, Iterable): # hasattr(x, '__iter__'): return [fix_units(y, unit_assumed=unit_assumed) for y in x] else: return x
[docs] def parse_units(x): """ Convert number, string, and lists/arrays/tuples to numbers scaled in HFSS units. Converts to LENGTH_UNIT = meters [HFSS UNITS] Assumes input units LENGTH_UNIT_ASSUMED = mm [USER UNITS] [USER UNITS] ----> [HFSS UNITS] """ return parse_entry(fix_units(x))
[docs] def unparse_units(x): """ Undo effect of parse_unit. Converts to LENGTH_UNIT_ASSUMED = mm [USER UNITS] Assumes input units LENGTH_UNIT = meters [HFSS UNITS] [HFSS UNITS] ----> [USER UNITS] """ return parse_entry(fix_units(x, unit_assumed=LENGTH_UNIT), LENGTH_UNIT_ASSUMED)
[docs] def parse_units_user(x): """ Convert from user assumed units to user assumed units [USER UNITS] ----> [USER UNITS] """ return parse_entry(fix_units(x, LENGTH_UNIT_ASSUMED), LENGTH_UNIT_ASSUMED)
[docs] class VariableString(str): def __add__(self, other): return var("(%s) + (%s)" % (self, other)) def __radd__(self, other): return var("(%s) + (%s)" % (other, self)) def __sub__(self, other): return var("(%s) - (%s)" % (self, other)) def __rsub__(self, other): return var("(%s) - (%s)" % (other, self)) def __mul__(self, other): return var("(%s) * (%s)" % (self, other)) def __rmul__(self, other): return var("(%s) * (%s)" % (other, self)) def __div__(self, other): return var("(%s) / (%s)" % (self, other)) def __rdiv__(self, other): return var("(%s) / (%s)" % (other, self)) def __truediv__(self, other): return var("(%s) / (%s)" % (self, other)) def __rtruediv__(self, other): return var("(%s) / (%s)" % (other, self)) def __pow__(self, other): return var("(%s) ^ (%s)" % (self, other)) def __rpow__(self, other): return var("(%s) ^ (%s)" % (other, self)) def __neg__(self): return var("-(%s)" % self) def __abs__(self): return var("abs(%s)" % self)
[docs] def var(x): if isinstance(x, str): return VariableString(simplify_arith_expr(x)) return x
_release_fns = [] def _add_release_fn(fn): global _release_fns _release_fns.append(fn) atexit.register(fn) signal.signal(signal.SIGTERM, fn) signal.signal(signal.SIGABRT, fn)
[docs] def release(): """ Release COM connection to Ansys. """ global _release_fns for fn in _release_fns: fn() time.sleep(0.1) # Note that _GetInterfaceCount is a member refcount = pythoncom._GetInterfaceCount() # pylint: disable=no-member if refcount > 0: print("Warning! %d COM references still alive" % (refcount)) print("Ansys will likely refuse to shut down")
[docs] class COMWrapper(object): def __init__(self): _add_release_fn(self.release)
[docs] def release(self): for k, v in self.__dict__.items(): if isinstance(v, CDispatch): setattr(self, k, None)
[docs] class HfssPropertyObject(COMWrapper): prop_holder = None prop_tab = None prop_server = None
[docs] def make_str_prop(name, prop_tab=None, prop_server=None): return make_prop(name, prop_tab=prop_tab, prop_server=prop_server)
[docs] def make_int_prop(name, prop_tab=None, prop_server=None): return make_prop( name, prop_tab=prop_tab, prop_server=prop_server, prop_args=["MustBeInt:=", True], )
[docs] def make_float_prop(name, prop_tab=None, prop_server=None): return make_prop( name, prop_tab=prop_tab, prop_server=prop_server, prop_args=["MustBeInt:=", False], )
[docs] def make_prop(name, prop_tab=None, prop_server=None, prop_args=None): def set_prop( self, value, prop_tab=prop_tab, prop_server=prop_server, prop_args=prop_args ): prop_tab = self.prop_tab if prop_tab is None else prop_tab prop_server = self.prop_server if prop_server is None else prop_server if isinstance(prop_tab, types.FunctionType): prop_tab = prop_tab(self) if isinstance(prop_server, types.FunctionType): prop_server = prop_server(self) if prop_args is None: prop_args = [] self.prop_holder.ChangeProperty( [ "NAME:AllTabs", [ "NAME:" + prop_tab, ["NAME:PropServers", prop_server], [ "NAME:ChangedProps", ["NAME:" + name, "Value:=", value] + prop_args, ], ], ] ) def get_prop(self, prop_tab=prop_tab, prop_server=prop_server): prop_tab = self.prop_tab if prop_tab is None else prop_tab prop_server = self.prop_server if prop_server is None else prop_server if isinstance(prop_tab, types.FunctionType): prop_tab = prop_tab(self) if isinstance(prop_server, types.FunctionType): prop_server = prop_server(self) return self.prop_holder.GetPropertyValue(prop_tab, prop_server, name) return property(get_prop, set_prop)
[docs] def set_property(prop_holder, prop_tab, prop_server, name, value, prop_args=None): """ More general non obj oriented, functional version prop_args = [] by default """ if not isinstance(prop_server, list): prop_server = [prop_server] return prop_holder.ChangeProperty( [ "NAME:AllTabs", [ "NAME:" + prop_tab, ["NAME:PropServers", *prop_server], [ "NAME:ChangedProps", ["NAME:" + name, "Value:=", value] + (prop_args or []), ], ], ] )
[docs] class HfssApp(COMWrapper): def __init__(self, ProgID="AnsoftHfss.HfssScriptInterface"): """ Connect to IDispatch-based COM object. Parameter is the ProgID or CLSID of the COM object. This is found in the regkey. Version changes for Ansys HFSS for the main object v2016 - 'Ansoft.ElectronicsDesktop' v2017 and subsequent - 'AnsoftHfss.HfssScriptInterface' """ super(HfssApp, self).__init__() self._app = Dispatch(ProgID)
[docs] def get_app_desktop(self): return HfssDesktop(self, self._app.GetAppDesktop())
# in v2016, there is also getApp - which can be called with HFSS
[docs] class HfssDesktop(COMWrapper): def __init__(self, app, desktop): """ :type app: HfssApp :type desktop: Dispatch """ super(HfssDesktop, self).__init__() self.parent = app self._desktop = desktop # ansys version, needed to check for command changes, # since some commands have changed over the years self.version = self.get_version()
[docs] def close_all_windows(self): self._desktop.CloseAllWindows()
[docs] def project_count(self): count = len(self._desktop.GetProjects()) return count
[docs] def get_active_project(self): return HfssProject(self, self._desktop.GetActiveProject())
[docs] def get_projects(self): return [HfssProject(self, p) for p in self._desktop.GetProjects()]
[docs] def get_project_names(self): return self._desktop.GetProjectList()
[docs] def get_messages(self, project_name="", design_name="", level=0): """Collect messages from a specified project and design. Parameters ---------- project_name : str Empty string returns all messages; wrong name returns no messages. design_name : str Empty string returns all messages for the project. level : int Severity filter: 0 = info+, 1 = warning+, 2 = error+fatal, 3 = fatal only. Returns ------- list of str """ return self._desktop.GetMessages(project_name, design_name, level)
[docs] def get_version(self): return self._desktop.GetVersion()
[docs] def new_project(self): return HfssProject(self, self._desktop.NewProject())
[docs] def open_project(self, path): """returns error if already open""" return HfssProject(self, self._desktop.OpenProject(path))
[docs] def set_active_project(self, name): """Make the named project the active project in the desktop.""" self._desktop.SetActiveProject(name)
@property def project_directory(self): """str: Default directory where new projects are saved.""" return self._desktop.GetProjectDirectory() @project_directory.setter def project_directory(self, path): self._desktop.SetProjectDirectory(path) @property def library_directory(self): """str: Path to the user component library directory.""" return self._desktop.GetLibraryDirectory() @library_directory.setter def library_directory(self, path): self._desktop.SetLibraryDirectory(path) @property def temp_directory(self): """str: Path to the HFSS temporary files directory.""" return self._desktop.GetTempDirectory() @temp_directory.setter def temp_directory(self, path): self._desktop.SetTempDirectory(path)
[docs] class HfssProject(COMWrapper): def __init__(self, desktop, project): """ :type desktop: HfssDesktop :type project: Dispatch """ super(HfssProject, self).__init__() self.parent = desktop self._project = project # self.name = project.GetName() self._ansys_version = self.parent.version
[docs] def close(self): self._project.Close()
[docs] def make_active(self): self.parent.set_active_project(self.name)
[docs] def get_designs(self): return [HfssDesign(self, d) for d in self._project.GetDesigns()]
[docs] def get_design_names(self): return [d.GetName() for d in self._project.GetDesigns()]
[docs] def save(self, path=None): if path is None: self._project.Save() else: self._project.SaveAs(path, True)
[docs] def simulate_all(self): self._project.SimulateAll()
[docs] def import_dataset(self, path): self._project.ImportDataset(path)
[docs] def rename_design(self, design, rename): if design in self.get_designs(): design.rename_design(design.name, rename) else: raise ValueError("%s design does not exist" % design.name)
[docs] def duplicate_design(self, target, source): src_design = self.get_design(source) return src_design.duplicate(name=target)
[docs] def get_variable_names(self): return [VariableString(s) for s in self._project.GetVariables()]
[docs] def get_variables(self): """Returns the project variables only, which start with $. These are global variables.""" return { VariableString(s): self.get_variable_value(s) for s in self._project.GetVariables() }
[docs] def get_variable_value(self, name): return self._project.GetVariableValue(name)
[docs] def create_variable(self, name, value): self._project.ChangeProperty( [ "NAME:AllTabs", [ "NAME:ProjectVariableTab", ["NAME:PropServers", "ProjectVariables"], [ "Name:NewProps", [ "NAME:" + name, "PropType:=", "VariableProp", "UserDef:=", True, "Value:=", value, ], ], ], ] )
[docs] def set_variable(self, name, value): if name not in self._project.GetVariables(): self.create_variable(name, value) else: self._project.SetVariableValue(name, value) return VariableString(name)
[docs] def get_path(self): if self._project: return self._project.GetPath() else: raise Exception( """Error: HFSS Project does not have a path. Either there is no HFSS project open, or it is not saved.""" )
[docs] def new_design(self, design_name, solution_type, design_type="HFSS"): design_name_int = increment_name( design_name, [d.GetName() for d in self._project.GetDesigns()] ) return HfssDesign( self, self._project.InsertDesign(design_type, design_name_int, solution_type, ""), )
[docs] def get_design(self, name): return HfssDesign(self, self._project.GetDesign(name))
[docs] def get_active_design(self): d = self._project.GetActiveDesign() if d is None: raise EnvironmentError("No Design Active") return HfssDesign(self, d)
[docs] def new_dm_design(self, name: str): """Create a new driven-modal (non-hybrid) HFSS design. On AEDT 2024.1+, ``InsertDesign`` with solution type ``"DrivenModal"`` silently creates an ``"HFSS Hybrid Modal Network"`` design instead of the classic ``"HFSS Modal Network"``. pyEPR explicitly calls ``SetSolutionType`` after creation to enforce the non-hybrid type, matching the behaviour of all previous HFSS releases. See Ansys scripting docs: InsertDesign — cannot specify hybrid vs. non-hybrid at creation time. SetSolutionType — used post-creation to correct the type. Args: name (str): Name for the new driven-modal design. Returns: HfssDesign: Wrapper around the newly created HFSS design. """ design = self.new_design(name, "DrivenModal") # AEDT 2024.1+ defaults InsertDesign to Hybrid; override to non-hybrid. if self._ansys_version >= "2024.1": design._design.SetSolutionType("HFSS Modal Network") return design
[docs] def new_dt_design(self, name: str): """Create a new driven-terminal (non-hybrid) HFSS design. On AEDT 2024.1+, ``InsertDesign`` with solution type ``"DrivenTerminal"`` silently creates an ``"HFSS Hybrid Terminal Network"`` design instead of the classic ``"HFSS Terminal Network"``. pyEPR explicitly calls ``SetSolutionType`` after creation to enforce the non-hybrid type, matching the behaviour of all previous HFSS releases. This is the terminal-port counterpart to :meth:`new_dm_design`. Use it when you need S-parameter or impedance extraction via terminal (lumped-port) excitations rather than wave-port modes. See Ansys scripting docs: InsertDesign — cannot specify hybrid vs. non-hybrid at creation time. SetSolutionType — used post-creation to correct the type. Args: name (str): Name for the new driven-terminal design. Returns: HfssDesign: Wrapper around the newly created HFSS design. """ design = self.new_design(name, "DrivenTerminal") # AEDT 2024.1+ defaults InsertDesign to Hybrid; override to non-hybrid. if self._ansys_version >= "2024.1": design._design.SetSolutionType("HFSS Terminal Network") return design
[docs] def new_em_design(self, name: str): """Create a new eigenmode HFSS design. Eigenmode designs compute the resonant frequencies and field distributions of a closed (or nearly closed) cavity without external ports. No ``SetSolutionType`` workaround is needed because HFSS has never introduced a hybrid variant for Eigenmode. Args: name (str): Name for the new eigenmode design. Returns: HfssDesign: Wrapper around the newly created HFSS design. """ return self.new_design(name, "Eigenmode")
[docs] def new_q3d_design(self, name: str): """Create a new Q3D Extractor design. Args: name (str): Name for the new Q3D design. Returns: HfssDesign: Wrapper around the newly created Q3D design. """ return self.new_design(name, "Q3D", "Q3D Extractor")
@property # v2016 def name(self): return self._project.GetName()
[docs] class HfssDesign(COMWrapper): """Wrapper around an Ansys HFSS (or Q3D) design COM object. Exposes design-level operations: variable management, geometry (via ``HfssModeler``), solution setup, field calculation, and Optimetrics. Created by ``HfssProject.get_design(name)`` or the ``new_*_design`` factory methods; not instantiated directly by user code. Attributes ---------- solution_type : str Canonical solution type string (``"Eigenmode"``, ``"DrivenModal"``, ``"DrivenTerminal"``, or ``"Q3D"``). Always the pre-2021.2 form regardless of AEDT version. modeler : HfssModeler Geometry editor interface. optimetrics : Optimetrics Parametric / optimisation sweep interface. """ def __init__(self, project, design): super(HfssDesign, self).__init__() self.parent = project self._design = design self.name = design.GetName() self._ansys_version = self.parent._ansys_version try: # GetSolutionType() does not exist for non-HFSS designs (e.g. Q3D). # AEDT 2021.2+ renamed the returned strings (e.g. "DrivenModal" → # "HFSS Modal Network"); normalise via solution_types.normalize() so # all downstream code sees stable canonical names. See PR #176. self.solution_type = _normalize_solution_type(design.GetSolutionType()) except Exception as e: logger.debug( f"Exception occurred at design.GetSolutionType() {e}. Assuming Q3D design" ) self.solution_type = "Q3D" if design is None: return self._setup_module = design.GetModule("AnalysisSetup") self._solutions = design.GetModule("Solutions") self._fields_calc = design.GetModule("FieldsReporter") self._output = design.GetModule("OutputVariable") self._boundaries = design.GetModule("BoundarySetup") self._reporter = design.GetModule("ReportSetup") self._modeler = design.SetActiveEditor("3D Modeler") self._optimetrics = design.GetModule("Optimetrics") self._mesh = design.GetModule("MeshSetup") self.modeler = HfssModeler(self, self._modeler, self._boundaries, self._mesh) self.optimetrics = Optimetrics(self)
[docs] def add_message(self, message: str, severity: int = 0): """ Add a message to HFSS log with severity and context to message window. Keyword Args: severity (int) : 0 = Informational, 1 = Warning, 2 = Error, 3 = Fatal.. """ project = self.parent desktop = project.parent oDesktop = desktop._desktop oDesktop.AddMessage(project.name, self.name, severity, message)
[docs] def save_screenshot(self, path: str = None, show: bool = True): """Export a PNG screenshot of the 3-D model view. Parameters ---------- path : str, optional Destination file path. Defaults to ``ansys.png`` in the current working directory. show : bool If ``True`` (default), display the image in a Jupyter notebook via ``IPython.display``. Returns ------- pathlib.Path Path to the saved PNG file. """ if not path: path = Path().absolute() / "ansys.png" # TODO find better self._modeler.ExportModelImageToFile( str(path), 0, 0, # can be 0 For the default, use 0, 0. For higher resolution, set desired <width> and <height>, for example for 8k export as: 7680, 4320. [ "NAME:SaveImageParams", "ShowAxis:=", "True", "ShowGrid:=", "True", "ShowRuler:=", "True", "ShowRegion:=", "Default", "Selections:=", "", "Orientation:=", "", ], ) if show: from IPython.display import Image, display display(Image(str(path))) return path
[docs] def rename_design(self, name): old_name = self._design.GetName() self._design.RenameDesignInstance(old_name, name)
[docs] def copy_to_project(self, project): project.make_active() project._project.CopyDesign(self.name) project._project.Paste() return project.get_active_design()
[docs] def duplicate(self, name=None): dup = self.copy_to_project(self.parent) if name is not None: dup.rename_design(name) return dup
[docs] def get_setup_names(self): return self._setup_module.GetSetups()
[docs] def get_setup(self, name=None): """Return the named analysis setup, or the first setup if *name* is omitted. Parameters ---------- name : str, optional Name of the setup to retrieve. If ``None``, the first setup in the design is returned. Returns ------- HfssSetup An ``HfssEMSetup``, ``HfssDMSetup``, ``HfssDTSetup``, or ``AnsysQ3DSetup`` instance depending on ``self.solution_type``. Raises ------ EnvironmentError If no setups exist, or if *name* is not found. """ setups = self.get_setup_names() if not setups: raise EnvironmentError(" *** No Setups Present ***") if name is None: name = setups[0] elif name not in setups: raise EnvironmentError("Setup {} not found: {}".format(name, setups)) if self.solution_type == "Eigenmode": return HfssEMSetup(self, name) elif self.solution_type in DRIVEN_MODAL_NAMES: return HfssDMSetup(self, name) elif self.solution_type in DRIVEN_TERMINAL_NAMES: return HfssDTSetup(self, name) elif self.solution_type == "Q3D": return AnsysQ3DSetup(self, name)
[docs] def create_q3d_setup( self, freq_ghz=5.0, name="Setup", save_fields=False, enabled=True, max_passes=15, min_passes=2, min_converged_passes=2, percent_error=0.5, percent_refinement=30, auto_increase_solution_order=True, solution_order="High", solver_type="Iterative", ): name = increment_name(name, self.get_setup_names()) self._setup_module.InsertSetup( "Matrix", [ f"NAME:{name}", "AdaptiveFreq:=", f"{freq_ghz}GHz", "SaveFields:=", save_fields, "Enabled:=", enabled, [ "NAME:Cap", "MaxPass:=", max_passes, "MinPass:=", min_passes, "MinConvPass:=", min_converged_passes, "PerError:=", percent_error, "PerRefine:=", percent_refinement, "AutoIncreaseSolutionOrder:=", auto_increase_solution_order, "SolutionOrder:=", solution_order, "Solver Type:=", solver_type, ], ], ) return AnsysQ3DSetup(self, name)
[docs] def create_dm_setup( self, freq_ghz=1, name="Setup", max_delta_s=0.1, max_passes=10, min_passes=1, min_converged=1, pct_refinement=30, basis_order=-1, ): name = increment_name(name, self.get_setup_names()) self._setup_module.InsertSetup( "HfssDriven", [ "NAME:" + name, "Frequency:=", str(freq_ghz) + "GHz", "MaxDeltaS:=", max_delta_s, "MaximumPasses:=", max_passes, "MinimumPasses:=", min_passes, "MinimumConvergedPasses:=", min_converged, "PercentRefinement:=", pct_refinement, "IsEnabled:=", True, "BasisOrder:=", basis_order, ], ) return HfssDMSetup(self, name)
[docs] def create_dt_setup( self, freq_ghz=1, name="Setup", max_delta_s=0.1, max_passes=10, min_passes=1, min_converged=1, pct_refinement=30, basis_order=-1, ): name = increment_name(name, self.get_setup_names()) self._setup_module.InsertSetup( "HfssDriven", [ "NAME:" + name, "Frequency:=", str(freq_ghz) + "GHz", "MaxDeltaS:=", max_delta_s, "MaximumPasses:=", max_passes, "MinimumPasses:=", min_passes, "MinimumConvergedPasses:=", min_converged, "PercentRefinement:=", pct_refinement, "IsEnabled:=", True, "BasisOrder:=", basis_order, ], ) return HfssDTSetup(self, name)
[docs] def create_em_setup( self, name="Setup", min_freq_ghz=1, n_modes=1, max_delta_f=0.1, max_passes=10, min_passes=1, min_converged=1, pct_refinement=30, basis_order=-1, ): name = increment_name(name, self.get_setup_names()) self._setup_module.InsertSetup( "HfssEigen", [ "NAME:" + name, "MinimumFrequency:=", str(min_freq_ghz) + "GHz", "NumModes:=", n_modes, "MaxDeltaFreq:=", max_delta_f, "ConvergeOnRealFreq:=", True, "MaximumPasses:=", max_passes, "MinimumPasses:=", min_passes, "MinimumConvergedPasses:=", min_converged, "PercentRefinement:=", pct_refinement, "IsEnabled:=", True, "BasisOrder:=", basis_order, ], ) return HfssEMSetup(self, name)
[docs] def delete_setup(self, name): if name in self.get_setup_names(): self._setup_module.DeleteSetups(name)
[docs] def delete_full_variation(self, DesignVariationKey="All", del_linked_data=False): """Delete solution data for one or all design variations. Wraps ``DeleteFullVariation`` in the HFSS scripting API (HFSS → Results → Clean Up Solutions). Parameters ---------- DesignVariationKey : str ``"All"`` deletes all variations; otherwise a design-variation string. del_linked_data : bool Whether to also delete linked data. """ self._design.DeleteFullVariation("All", False)
[docs] def get_nominal_variation(self): """ Use: Gets the nominal variation string Return Value: Returns a string representing the nominal variation Returns string such as "Height='0.06mm' Lj='13.5nH'" """ return self._design.GetNominalVariation()
[docs] def create_variable(self, name, value, postprocessing=False): if postprocessing is True: variableprop = "PostProcessingVariableProp" else: variableprop = "VariableProp" self._design.ChangeProperty( [ "NAME:AllTabs", [ "NAME:LocalVariableTab", ["NAME:PropServers", "LocalVariables"], [ "Name:NewProps", [ "NAME:" + name, "PropType:=", variableprop, "UserDef:=", True, "Value:=", value, ], ], ], ] )
def _variation_string_to_variable_list( self, variation_string: str, for_prop_server=True ): """Example: Takes "Cj='2fF' Lj='13.5nH'" for for_prop_server=True into [['NAME:Cj', 'Value:=', '2fF'], ['NAME:Lj', 'Value:=', '13.5nH']] or for for_prop_server=False into [['Cj', '2fF'], ['Lj', '13.5nH']] """ s = variation_string s = s.split(" ") s = [s1.strip().strip("''").split("='") for s1 in s] if for_prop_server: local, project = [], [] for arr in s: to_add = [f"NAME:{arr[0]}", "Value:=", arr[1]] if arr[0][0] == "$": project += [to_add] # global variable else: local += [to_add] # local variable return local, project else: return s
[docs] def set_variables(self, variation_string: str): """ Set all variables to match a solved variation string. Args: variation_string (str) : Variation string such as "Cj='2fF' Lj='13.5nH'" """ assert isinstance(variation_string, str) content = ["NAME:ChangedProps"] local, project = self._variation_string_to_variable_list(variation_string) # print('\nlocal=', local, '\nproject=', project) if len(project) > 0: self._design.ChangeProperty( [ "NAME:AllTabs", [ "NAME:ProjectVariableTab", ["NAME:PropServers", "ProjectVariables"], content + project, ], ] ) if len(local) > 0: self._design.ChangeProperty( [ "NAME:AllTabs", [ "NAME:LocalVariableTab", ["NAME:PropServers", "LocalVariables"], content + local, ], ] )
[docs] def set_variable(self, name: str, value: str, postprocessing=False): """Warning: THis is case sensitive, Arguments: name {str} -- Name of variable to set, such as 'Lj_1'. This is not the same as as 'LJ_1'. You must use the same casing. value {str} -- Value, such as '10nH' Keyword Arguments: postprocessing {bool} -- Postprocessing variable only or not. (default: {False}) Returns: VariableString """ # TODO: check if variable does not exist and quit if it doesn't? if name not in self.get_variable_names(): self.create_variable(name, value, postprocessing=postprocessing) else: self._design.SetVariableValue(name, value) return VariableString(name)
[docs] def get_variable_value(self, name): """Can only access the design variables, i.e., the local ones Cannot access the project (global) variables, which start with $.""" return self._design.GetVariableValue(name)
[docs] def get_variable_names(self): """Returns the local design variables. Does not return the project (global) variables, which start with $.""" return [ VariableString(s) for s in self._design.GetVariables() + self._design.GetPostProcessingVariables() ]
[docs] def get_variables(self): """Returns dictionary of local design variables and their values. Does not return the project (global) variables and their values, whose names start with $.""" local_variables = ( self._design.GetVariables() + self._design.GetPostProcessingVariables() ) return {lv: self.get_variable_value(lv) for lv in local_variables}
[docs] def copy_design_variables(self, source_design): """does not check that variables are all present""" # don't care about values source_variables = source_design.get_variables() for name, value in source_variables.items(): self.set_variable(name, value)
[docs] def get_excitations(self): return self._boundaries.GetExcitations()
def _evaluate_variable_expression(self, expr, units): """ :type expr: str :type units: str :return: float """ try: sexp = sympy_parser.parse_expr(expr) except SyntaxError: return Q(expr).to(units).magnitude sub_exprs = {fs: self.get_variable_value(fs.name) for fs in sexp.free_symbols} return float( sexp.subs( { fs: self._evaluate_variable_expression(e, units) for fs, e in sub_exprs.items() } ) )
[docs] def eval_expr(self, expr, units="mm"): return str(self._evaluate_variable_expression(expr, units)) + units
[docs] def Clear_Field_Clac_Stack(self): self._fields_calc.CalcStack("Clear")
[docs] def clean_up_solutions(self): self._design.DeleteFullVariation("All", True) # Delete existing solutions
[docs] class HfssSetup(HfssPropertyObject): prop_tab = "HfssTab" passes = make_int_prop("Passes") # see EditSetup n_modes = make_int_prop("Modes") pct_refinement = make_float_prop("Percent Refinement") delta_f = make_float_prop("Delta F") min_freq = make_float_prop("Min Freq") basis_order = make_str_prop("Basis Order") def __init__(self, design, setup: str): """ :type design: HfssDesign :type setup: Dispatch :COM Scripting Help: "Analysis Setup Module Script Commands" Get properties: setup.parent._design.GetProperties("HfssTab",'AnalysisSetup:Setup1') """ super(HfssSetup, self).__init__() self.parent = design self.prop_holder = design._design self._setup_module = design._setup_module self._reporter = design._reporter self._solutions = design._solutions self.name = setup self.solution_name = setup + " : LastAdaptive" # self.solution_name_pass = setup + " : AdaptivePass" self.prop_server = "AnalysisSetup:" + setup self.expression_cache_items = [] self._ansys_version = self.parent._ansys_version
[docs] def analyze(self, name=None): """Solve a single solution setup and all its frequency sweeps. Wraps ``Analyze(<SetupName>)`` in the HFSS scripting API. Blocks until analysis is complete; raises a COM error if aborted in HFSS. """ if name is None: name = self.name logger.info(f"Analyzing setup {name}") return self.parent._design.Analyze(name)
[docs] def solve(self, name=None): """Perform a blocking simulation via ``Solve(<SetupNameArray>)``. Returns 0 on normal completion, -1 on simulation error. User abort also returns 0. """ if name is None: name = self.name return self.parent._design.Solve(name)
[docs] def insert_sweep( self, start_ghz, stop_ghz, count=None, step_ghz=None, name="Sweep", type="Fast", save_fields=False, ): if not type in ["Fast", "Interpolating", "Discrete"]: logger.error( "insert_sweep: Error type was not in ['Fast', 'Interpolating', 'Discrete']" ) name = increment_name(name, self.get_sweep_names()) params = [ "NAME:" + name, "IsEnabled:=", True, "Type:=", type, "SaveFields:=", save_fields, "SaveRadFields:=", False, # "GenerateFieldsForAllFreqs:=" "ExtrapToDC:=", False, ] # not sure when exactly this changed between 2016 and 2019 if self._ansys_version >= "2019": if count: params.extend( [ "RangeType:=", "LinearCount", "RangeStart:=", f"{start_ghz:f}GHz", "RangeEnd:=", f"{stop_ghz:f}GHz", "RangeCount:=", count, ] ) if step_ghz: params.extend( [ "RangeType:=", "LinearStep", "RangeStart:=", f"{start_ghz:f}GHz", "RangeEnd:=", f"{stop_ghz:f}GHz", "RangeStep:=", step_ghz, ] ) if (count and step_ghz) or ((not count) and (not step_ghz)): logger.error( "ERROR: you should provide either step_ghz or count \ when inserting an HFSS driven model freq sweep. \ YOu either provided both or neither! See insert_sweep." ) else: params.extend( ["StartValue:=", "%fGHz" % start_ghz, "StopValue:=", "%fGHz" % stop_ghz] ) if step_ghz is not None: params.extend( ["SetupType:=", "LinearSetup", "StepSize:=", "%fGHz" % step_ghz] ) else: params.extend(["SetupType:=", "LinearCount", "Count:=", count]) self._setup_module.InsertFrequencySweep(self.name, params) return HfssFrequencySweep(self, name)
[docs] def delete_sweep(self, name): self._setup_module.DeleteSweep(self.name, name)
# def add_fields_convergence_expr(self, expr, pct_delta, phase=0): # """note: because of hfss idiocy, you must call "commit_convergence_exprs" # after adding all exprs""" # assert isinstance(expr, NamedCalcObject) # self.expression_cache_items.append( # ["NAME:CacheItem", # "Title:=", expr.name+"_conv", # "Expression:=", expr.name, # "Intrinsics:=", "Phase='{}deg'".format(phase), # "IsConvergence:=", True, # "UseRelativeConvergence:=", 1, # "MaxConvergenceDelta:=", pct_delta, # "MaxConvergeValue:=", "0.05", # "ReportType:=", "Fields", # ["NAME:ExpressionContext"]]) # def commit_convergence_exprs(self): # """note: this will eliminate any convergence expressions not added # through this interface""" # args = [ # "NAME:"+self.name, # ["NAME:ExpressionCache", self.expression_cache_items] # ] # self._setup_module.EditSetup(self.name, args)
[docs] def get_sweep_names(self): return self._setup_module.GetSweeps(self.name)
[docs] def get_sweep(self, name=None): sweeps = self.get_sweep_names() if not sweeps: raise EnvironmentError("No Sweeps Present") if name is None: name = sweeps[0] elif name not in sweeps: raise EnvironmentError("Sweep {} not found in {}".format(name, sweeps)) return HfssFrequencySweep(self, name)
[docs] def add_fields_convergence_expr(self, expr, pct_delta, phase=0): """note: because of hfss idiocy, you must call "commit_convergence_exprs" after adding all exprs""" assert isinstance(expr, NamedCalcObject) self.expression_cache_items.append( [ "NAME:CacheItem", "Title:=", expr.name + "_conv", "Expression:=", expr.name, "Intrinsics:=", "Phase='{}deg'".format(phase), "IsConvergence:=", True, "UseRelativeConvergence:=", 1, "MaxConvergenceDelta:=", pct_delta, "MaxConvergeValue:=", "0.05", "ReportType:=", "Fields", ["NAME:ExpressionContext"], ] )
[docs] def commit_convergence_exprs(self): """note: this will eliminate any convergence expressions not added through this interface""" args = [ "NAME:" + self.name, ["NAME:ExpressionCache", self.expression_cache_items], ] self._setup_module.EditSetup(self.name, args)
[docs] def get_convergence(self, variation="", pre_fn_args=[], overwrite=True): """ Returns converge as a dataframe Variation should be in the form variation = "scale_factor='1.2001'" ... """ # TODO: (Daniel) I think this data should be store in a more comfortable datatype (dictionary maybe?) # Write file temp = tempfile.NamedTemporaryFile() temp.close() temp = temp.name + ".conv" self.parent._design.ExportConvergence( self.name, variation, *pre_fn_args, temp, overwrite ) # Read File temp = Path(temp) if not temp.is_file(): logger.error( f"""ERROR! Error in trying to read temporary convergence file. `get_convergence` did not seem to have the file written {str(temp)}. Perhaps there was no convergence? Check to see if there is a CONV available for this current variation. If the nominal design is not solved, it will not have a CONV., but will show up as a variation Check for error messages in HFSS. Retuning None""" ) return None, "" text = temp.read_text() # Parse file text2 = text.split(r"==================") if len(text) >= 3: df = pd.read_csv( io.StringIO(text2[3].strip()), sep="|", skipinitialspace=True, index_col=0, ).drop("Unnamed: 3", axis=1) else: logger.error(f"ERROR IN reading in {temp}:\n{text}") df = None return df, text
[docs] def get_mesh_stats(self, variation=""): """variation should be in the form variation = "scale_factor='1.2001'" ... """ temp = tempfile.NamedTemporaryFile() temp.close() # print(temp.name0 # seems broken in 2016 because of extra text added to the top of the file self.parent._design.ExportMeshStats( self.name, variation, temp.name + ".mesh", True ) try: df = pd.read_csv( temp.name + ".mesh", delimiter="|", skipinitialspace=True, skiprows=7, skipfooter=1, skip_blank_lines=True, engine="python", ) df = df.drop("Unnamed: 9", axis=1) except Exception as e: print("ERROR in MESH reading operation.") print(e) print( "ERROR! Error in trying to read temporary MESH file " + temp.name + "\n. Check to see if there is a mesh available for this current variation.\ If the nominal design is not solved, it will not have a mesh., \ but will show up as a variation." ) df = None return df
[docs] def get_profile(self, variation=""): fn = tempfile.mktemp() self.parent._design.ExportProfile(self.name, variation, fn, False) df = pd.read_csv( fn, delimiter="\t", skipinitialspace=True, skiprows=6, skipfooter=1, skip_blank_lines=True, engine="python", ) # just broken down by new lines return df
[docs] def get_fields(self): return HfssFieldsCalc(self)
[docs] class HfssDMSetup(HfssSetup): """ Driven modal setup """ solution_freq = make_float_prop("Solution Freq") delta_s = make_float_prop("Delta S") solver_type = make_str_prop("Solver Type") def _map_variables_by_name(self): """does not check that variables are all present""" # don't care about values project_variables = self.parent.parent.get_variable_names() design_variables = self.parent.get_variable_names() # build array args = [ "NAME:Params", ] for name in project_variables: args.extend([str(name) + ":=", str(name)]) for name in design_variables: args.extend([str(name) + ":=", str(name)]) return args
[docs] def get_solutions(self): return HfssDMDesignSolutions(self, self.parent._solutions)
[docs] class HfssDTSetup(HfssDMSetup):
[docs] def get_solutions(self): return HfssDTDesignSolutions(self, self.parent._solutions)
[docs] class HfssEMSetup(HfssSetup): """ Eigenmode setup """ min_freq = make_float_prop("Min Freq") n_modes = make_int_prop("Modes") delta_f = make_float_prop("Delta F")
[docs] def get_solutions(self): return HfssEMDesignSolutions(self, self.parent._solutions)
[docs] class AnsysQ3DSetup(HfssSetup): """ Q3D setup """ prop_tab = "CG" max_pass = make_int_prop("Max. Number of Passes") min_pass = make_int_prop("Min. Number of Passes") pct_error = make_int_prop("Percent Error") frequency = make_str_prop("Adaptive Freq", "General") # e.g., '5GHz' n_modes = 0 # for compatibility with eigenmode
[docs] def get_frequency_Hz(self): return int(ureg(self.frequency).to("Hz").magnitude)
[docs] def get_solutions(self): return HfssQ3DDesignSolutions(self, self.parent._solutions)
[docs] def get_convergence(self, variation=""): """Return Q3D convergence data as a DataFrame (columns: Triangle, Delta %).""" return super().get_convergence(variation, pre_fn_args=["CG"])
[docs] def get_matrix( self, variation="", pass_number=0, frequency=None, MatrixType="Maxwell", solution_kind="LastAdaptive", # AdaptivePass ACPlusDCResistance=False, soln_type="C", ): """Export and return the Q3D capacitance/conductance matrix. Parameters ---------- variation : str Empty string returns the nominal variation. frequency : float, optional Frequency in Hz. soln_type : str One of ``"C"``, ``"AC RL"``, or ``"DC RL"``. solution_kind : str ``"LastAdaptive"`` or ``"AdaptivePass"``. Returns ------- tuple ``(df_cmat, user_units, (df_cond, units_cond), design_variation)`` """ if frequency is None: frequency = self.get_frequency_Hz() temp = tempfile.NamedTemporaryFile() temp.close() path = temp.name + ".txt" # <FileName>, <SolnType>, <DesignVariationKey>, <Solution>, <Matrix>, <ResUnit>, # <IndUnit>, <CapUnit>, <CondUnit>, <Frequency>, <MatrixType>, <PassNumber>, # <ACPlusDCResistance> logger.info( f"Exporting matrix data to ({path}, {soln_type}, {variation}, " f"{self.name}:{solution_kind}, " '"Original", "ohm", "nH", "fF", ' f'"mSie", {frequency}, {MatrixType}, ' f"{pass_number}, {ACPlusDCResistance}" ) self.parent._design.ExportMatrixData( path, soln_type, variation, f"{self.name}:{solution_kind}", "Original", "ohm", "nH", "fF", "mSie", frequency, MatrixType, pass_number, ACPlusDCResistance, ) ( df_cmat, user_units, (df_cond, units_cond), design_variation, ) = self.load_q3d_matrix(path) return df_cmat, user_units, (df_cond, units_cond), design_variation
@staticmethod def _readin_Q3D_matrix(path: str): """ Read in the txt file created from q3d export and output the capacitance matrix When exporting pick "save as type: data table" See Zlatko RETURNS: Dataframe Example file: ``` DesignVariation:$BBoxL='650um' $boxH='750um' $boxL='2mm' $QubitGap='30um' \ $QubitH='90um' $QubitL='450um' Lj_1='13nH' Setup1:LastAdaptive Problem Type:C C Units:farad, G Units:mSie Reduce Matrix:Original Frequency: 5.5E+09 Hz Capacitance Matrix ground_plane Q1_bus_Q0_connector_pad Q1_bus_Q2_connector_pad Q1_pad_bot Q1_pad_top1 Q1_readout_connector_pad ground_plane 2.8829E-13 -3.254E-14 -3.1978E-14 -4.0063E-14 -4.3842E-14 -3.0053E-14 Q1_bus_Q0_connector_pad -3.254E-14 4.7257E-14 -2.2765E-16 -1.269E-14 -1.3351E-15 -1.451E-16 Q1_bus_Q2_connector_pad -3.1978E-14 -2.2765E-16 4.5327E-14 -1.218E-15 -1.1552E-14 -5.0414E-17 Q1_pad_bot -4.0063E-14 -1.269E-14 -1.218E-15 9.5831E-14 -3.2415E-14 -8.3665E-15 Q1_pad_top1 -4.3842E-14 -1.3351E-15 -1.1552E-14 -3.2415E-14 9.132E-14 -1.0199E-15 Q1_readout_connector_pad -3.0053E-14 -1.451E-16 -5.0414E-17 -8.3665E-15 -1.0199E-15 3.9884E-14 Conductance Matrix ground_plane Q1_bus_Q0_connector_pad Q1_bus_Q2_connector_pad Q1_pad_bot Q1_pad_top1 Q1_readout_connector_pad ground_plane 0 0 0 0 0 0 Q1_bus_Q0_connector_pad 0 0 0 0 0 0 Q1_bus_Q2_connector_pad 0 0 0 0 0 0 Q1_pad_bot 0 0 0 0 0 0 Q1_pad_top1 0 0 0 0 0 0 Q1_readout_connector_pad 0 0 0 0 0 0 ``` """ text = Path(path).read_text() s1 = text.split("Capacitance Matrix") assert len(s1) == 2, "Could not split text to `Capacitance Matrix`" s2 = s1[1].split("Conductance Matrix") df_cmat = pd.read_csv( io.StringIO(s2[0].strip()), delim_whitespace=True, skipinitialspace=True, index_col=0, ) units = re.findall(r"C Units:(.*?),", text)[0] if len(s2) > 1: df_cond = pd.read_csv( io.StringIO(s2[1].strip()), delim_whitespace=True, skipinitialspace=True, index_col=0, ) units_cond = re.findall(r"G Units:(.*?)\n", text)[0] else: df_cond = None units_cond = None var = re.findall(r"DesignVariation:(.*?)\n", text) # this changed circa v2020 if len(var) < 1: # didnt find var = re.findall(r"Design Variation:(.*?)\n", text) if len(var) < 1: # didnt find # May not be present if there are no design variations to begin # with and no variables in the design. pass # logger.error(f'Failed to parse Q3D matrix Design Variation:\nFile:{path}\nText:{text}') var = [""] design_variation = var[0] return df_cmat, units, design_variation, df_cond, units_cond
[docs] @staticmethod def load_q3d_matrix(path, user_units="fF"): """Load Q3D capacitance file exported as Maxwell matrix. Exports also conductance conductance. Units are read in automatically and converted to user units. Arguments: path {[str or Path]} -- [path to file text with matrix] Returns: df_cmat, user_units, (df_cond, units_cond), design_variation dataframes: df_cmat, df_cond """ ( df_cmat, Cunits, design_variation, df_cond, units_cond, ) = AnsysQ3DSetup._readin_Q3D_matrix(path) # Unit convert q = ureg.parse_expression(Cunits).to(user_units) df_cmat = df_cmat * q.magnitude # scale to user units # print("Imported capacitance matrix with UNITS: [%s] now converted to USER UNITS:[%s] from file:\n\t%s"%(Cunits, user_units, path)) return df_cmat, user_units, (df_cond, units_cond), design_variation
[docs] class HfssDesignSolutions(COMWrapper): def __init__(self, setup, solutions): """ :type setup: HfssSetup """ super(HfssDesignSolutions, self).__init__() self.parent = setup self._solutions = solutions self._ansys_version = self.parent._ansys_version
[docs] def get_valid_solution_list(self): """Get all available solution names that exist in a design. Returns a tuple of strings such as ``('Setup1 : AdaptivePass', 'Setup1 : LastAdaptive')``. """ return self._solutions.GetValidISolutionList()
[docs] def list_variations(self, setup_name: str = None): """ Get a list of solved variations. Args: setup_name(str) : Example name ("Setup1 : LastAdaptive") Defaults to None. Returns: An array of strings corresponding to solved variations. .. 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'") """ if setup_name is None: setup_name = str(self.parent.solution_name) return self._solutions.ListVariations(setup_name)
[docs] class HfssEMDesignSolutions(HfssDesignSolutions):
[docs] def eigenmodes(self, lv=""): """ Returns the eigenmode data of freq and kappa/2p """ fn = tempfile.mktemp() # print(self.parent.solution_name, lv, fn) self._solutions.ExportEigenmodes(self.parent.solution_name, lv, fn) data = np.genfromtxt(fn, dtype="str") # Update to Py 3: # np.loadtxt and np.genfromtxt operate in byte mode, which is the default string type in Python 2. # But Python 3 uses unicode, and marks bytestrings with this b. # getting around the very annoying fact that if np.size(np.shape(data)) == 1: # in Python a 1D array does not have shape (N,1) data = np.array([data]) else: # but rather (N,) .... pass if np.size(data[0, :]) == 6: # checking if values for Q were saved # eigvalue=(omega-i*kappa/2)/2pi kappa_over_2pis = [2 * float(ii) for ii in data[:, 3]] # so kappa/2pi = 2*Im(eigvalue) else: kappa_over_2pis = None # print(data[:,1]) freqs = [float(ii) for ii in data[:, 1]] return freqs, kappa_over_2pis
""" Export eigenmodes vs pass number Did not figure out how to set pass number in a hurry. import tempfile self = epr_hfss.solutions ''' HFSS: Exports a tab delimited table of Eigenmodes in HFSS. Not in HFSS-IE. <setupName> <solutionName> <DesignVariationKey> <filename> Return Value: None Parameters: <SolutionName> Type: <string> Name of the solutions within the solution setup. <DesignVariationKey> Type: <string> Design variation string. ''' setup = self.parent fn = tempfile.mktemp() variation_list='' soln_name = f'{setup.name} : AdaptivePas' available_solns = self._solutions.GetValidISolutionList() if not(soln_name in available_solns): logger.error(f'ERROR Tried to export freq vs pass number, but solution `{soln_name}` was not in available `{available_solns}`. Returning []') #return [] self._solutions.ExportEigenmodes(soln_name, ['Pass:=5'], fn) # ['Pass:=5'] fails can do with '' """
[docs] def set_mode(self, n, phase=0, FieldType="EigenStoredEnergy"): """ Indicates which source excitations should be used for fields post processing. HFSS>Fields>Edit Sources Mode count starts at 1 Amplitude is set to 1 No error is thrown if a number exceeding number of modes is set FieldType -- EigenStoredEnergy or EigenPeakElecticField """ n_modes = int(self.parent.n_modes) if n < 1: err = f"ERROR: You tried to set a mode < 1. {n}/{n_modes}" logger.error(err) raise Exception(err) if n > n_modes: err = f"ERROR: You tried to set a mode > number of modes {n}/{n_modes}" logger.error(err) raise Exception(err) if self._ansys_version >= "2019": # THIS WORKS FOR v2019R2 self._solutions.EditSources( [ ["FieldType:=", "EigenPeakElectricField"], [ "Name:=", "Modes", "Magnitudes:=", ["1" if i + 1 == n else "0" for i in range(n_modes)], "Phases:=", [str(phase) if i + 1 == n else "0" for i in range(n_modes)], ], ] ) else: # The syntax has changed for AEDT 18.2. # see https://ansyshelp.ansys.com/account/secured?returnurl=/Views/Secured/Electronics/v195//Subsystems/HFSS/Subsystems/HFSS%20Scripting/HFSS%20Scripting.htm self._solutions.EditSources( "EigenStoredEnergy", ["NAME:SourceNames", "EigenMode"], ["NAME:Modes", n_modes], ["NAME:Magnitudes"] + [1 if i + 1 == n else 0 for i in range(n_modes)], ["NAME:Phases"] + [phase if i + 1 == n else 0 for i in range(n_modes)], ["NAME:Terminated"], ["NAME:Impedances"], )
[docs] def has_fields(self, variation_string=None): """Determine if fields exist for a particular solution. Parameters ---------- variation_string : str, optional HFSS variation string such as ``"Cj='2fF' Lj='12.75nH'"``. If ``None``, uses the nominal variation. """ if variation_string is None: variation_string = self.parent.parent.get_nominal_variation() return bool( self._solutions.HasFields(self.parent.solution_name, variation_string) )
[docs] def create_report(self, plot_name, xcomp, ycomp, params, pass_name="LastAdaptive"): """ pass_name: AdaptivePass, LastAdaptive Example ------- Example plot for a single variation all pass converge of mode freq .. code-block:: python ycomp = [f"re(Mode({i}))" for i in range(1,1+epr_hfss.n_modes)] params = ["Pass:=", ["All"]]+variation setup.create_report("Freq. vs. pass", "Pass", ycomp, params, pass_name='AdaptivePass') """ assert isinstance(ycomp, list) assert isinstance(params, list) setup = self.parent reporter = setup._reporter return reporter.CreateReport( plot_name, "Eigenmode Parameters", "Rectangular Plot", f"{setup.name} : {pass_name}", [], params, ["X Component:=", xcomp, "Y Component:=", ycomp], [], )
[docs] class HfssDMDesignSolutions(HfssDesignSolutions): pass
[docs] class HfssDTDesignSolutions(HfssDesignSolutions): pass
[docs] class HfssQ3DDesignSolutions(HfssDesignSolutions): pass
[docs] class HfssFrequencySweep(COMWrapper): prop_tab = "HfssTab" start_freq = make_float_prop("Start") stop_freq = make_float_prop("Stop") step_size = make_float_prop("Step Size") count = make_float_prop("Count") sweep_type = make_str_prop("Type") def __init__(self, setup, name): """ :type setup: HfssSetup :type name: str """ super(HfssFrequencySweep, self).__init__() self.parent = setup self.name = name self.solution_name = self.parent.name + " : " + name self.prop_holder = self.parent.prop_holder self.prop_server = self.parent.prop_server + ":" + name self._ansys_version = self.parent._ansys_version
[docs] def analyze_sweep(self): self.parent.analyze(self.solution_name)
[docs] def get_network_data(self, formats): if isinstance(formats, str): formats = formats.split(",") formats = [f.upper() for f in formats] fmts_lists = {"S": [], "Y": [], "Z": []} for f in formats: fmts_lists[f[0]].append((int(f[1]), int(f[2]))) ret = [None] * len(formats) freq = None for data_type, list in fmts_lists.items(): if list: fn = tempfile.mktemp() self.parent._solutions.ExportNetworkData( [], self.parent.name + " : " + self.name, 2, fn, ["all"], False, 0, data_type, -1, 1, 15, ) with open(fn) as f: f.readline() colnames = f.readline().split() array = np.loadtxt(fn, skiprows=2) # WARNING for python 3 probably need to use genfromtxt if freq is None: freq = array[:, 0] # TODO: If Ansys version is 2019, use 'Real' and 'Imag' # in place of 'Re' and 'Im for i, j in list: real_idx = colnames.index("%s[%d,%d]_Re" % (data_type, i, j)) imag_idx = colnames.index("%s[%d,%d]_Im" % (data_type, i, j)) c_arr = array[:, real_idx] + 1j * array[:, imag_idx] ret[formats.index("%s%d%d" % (data_type, i, j))] = c_arr return freq, ret
[docs] def create_report(self, name, expr): existing = self.parent._reporter.GetAllReportNames() name = increment_name(name, existing) var_names = self.parent.parent.get_variable_names() var_args = sum([["%s:=" % v_name, ["Nominal"]] for v_name in var_names], []) self.parent._reporter.CreateReport( name, "Modal Solution Data", "Rectangular Plot", self.solution_name, ["Domain:=", "Sweep"], ["Freq:=", ["All"]] + var_args, ["X Component:=", "Freq", "Y Component:=", [expr]], [], ) return HfssReport(self.parent.parent, name)
[docs] def get_report_arrays(self, expr): r = self.create_report("Temp", expr) return r.get_arrays()
[docs] class HfssReport(COMWrapper): def __init__(self, design, name): """ :type design: HfssDesign :type name: str """ super(HfssReport, self).__init__() self.parent_design = design self.name = name
[docs] def export_to_file(self, filename): filepath = os.path.abspath(filename) self.parent_design._reporter.ExportToFile(self.name, filepath)
[docs] def get_arrays(self): fn = tempfile.mktemp(suffix=".csv") self.export_to_file(fn) return np.loadtxt(fn, skiprows=1, delimiter=",").transpose()
# warning for python 3 probably need to use genfromtxt
[docs] class Optimetrics(COMWrapper): """ Optimetrics script commands executed by the "Optimetrics" module. Example use: .. code-block:: python opti = Optimetrics(pinfo.design) names = opti.get_setup_names() print('Names of optimetrics: ', names) opti.solve_setup(names[0]) Note that running optimetrics requires the license for Optimetrics by Ansys. """ def __init__(self, design): super(Optimetrics, self).__init__() self.design = design # parent self._optimetrics = self.design._optimetrics # <COMObject GetModule> self.setup_names = None
[docs] def get_setup_names(self): """ Return list of Optimetrics setup names """ self.setup_names = list(self._optimetrics.GetSetupNames()) return self.setup_names.copy()
[docs] def solve_setup(self, setup_name: str): """ Solves the specified Optimetrics setup. Corresponds to: Right-click the setup in the project tree, and then click Analyze on the shortcut menu. setup_name (str) : name of setup, should be in get_setup_names Blocks execution until ready to use. Note that this requires the license for Optimetrics by Ansys. """ return self._optimetrics.SolveSetup(setup_name)
[docs] def create_setup( self, variable, swp_params, name="ParametricSetup1", swp_type="linear_step", setup_name=None, save_fields=True, copy_mesh=True, solve_with_copied_mesh_only=True, setup_type="parametric", ): """ Inserts a new parametric setup of one variable. Either with sweep definition or from file. *Synchronized* sweeps (more than one variable changing at once) can be implemented by giving a list of variables to ``variable`` and corresponding lists to ``swp_params`` and ``swp_type``. The lengths of the sweep types should match (excluding single value). Corresponds to ui access: Right-click the Optimetrics folder in the project tree, and then click Add> Parametric on the shortcut menu. Ansys provides six sweep definitions types specified using the swp_type variable. Sweep type definitions: - 'single_value' Specify a single value for the sweep definition. - 'linear_step' Specify a linear range of values with a constant step size. - 'linear_count' Specify a linear range of values and the number, or count of points within this range. - 'decade_count' Specify a logarithmic (base 10) series of values, and the number of values to calculate in each decade. - 'octave_count' Specify a logarithmic (base 2) series of values, and the number of values to calculate in each octave. - 'exponential_count' Specify an exponential (base e) series of values, and the number of values to calculate. For swp_type='single_value' swp_params is the single value. For swp_type='linear_step' swp_params is start, stop, step: swp_params = ("12.8nH", "13.6nH", "0.2nH") All other types swp_params is start, stop, count: swp_params = ("12.8nH", "13.6nH", 4) The definition of count varies amongst the available types. For Decade count and Octave count, the Count value specifies the number of points to calculate in every decade or octave. For Exponential count, the Count value is the total number of points. The total number of points includes the start and stop values. For parametric from file, set ``setup_type='parametric_file'`` and pass a file path (e.g. ``"C:\\\\test.csv"``) to ``swp_params``. Example CSV format:: *,Lj_qubit 1,12.2nH 2,9.7nH 3,10.2nH See Ansys documentation for additional formatting instructions. """ setup_name = setup_name or self.design.get_setup_names()[0] print( f"Inserting optimetrics setup `{name}` for simulation setup: `{setup_name}`" ) if setup_type == "parametric": type_map = { "linear_count": "LINC", "decade_count": "DEC", "octave_count": "OCT", "exponential_count": "ESTP", } valid_swp_types = {"single_value", "linear_step"} | set(type_map.keys()) if isinstance(variable, Iterable) and not isinstance(variable, str): # synchronized sweep, check that data is in correct format assert len(swp_params) == len(swp_type) == len(variable), ( "Incorrect swp_params or swp_type format for synchronised sweep." ) synchronize = True else: # convert all to lists as we can reuse same code for synchronized swp_type = [swp_type] swp_params = [swp_params] variable = [variable] synchronize = False if any(e not in valid_swp_types for e in swp_type): raise NotImplementedError() else: swp_str = list() for i, e in enumerate(swp_type): if e == "single_value": # Single takes string of single variable no swp_type_name swp_str.append(f"{swp_params[i]}") else: # correct number of inputs assert len(swp_params[i]) == 3, ( "Incorrect number of sweep parameters." ) # Not checking for compatible unit types if e == "linear_step": swp_type_name = "LIN" else: # counts needs to be an integer number assert isinstance(swp_params[i][2], int), ( "Count must be integer." ) swp_type_name = type_map[e] # prepare the string to pass to Ansys swp_str.append( f"{swp_type_name} {swp_params[i][0]} {swp_params[i][1]} {swp_params[i][2]}" ) self._optimetrics.InsertSetup( "OptiParametric", [ f"NAME:{name}", "IsEnabled:=", True, [ "NAME:ProdOptiSetupDataV2", "SaveFields:=", save_fields, "CopyMesh:=", copy_mesh, "SolveWithCopiedMeshOnly:=", solve_with_copied_mesh_only, ], ["NAME:StartingPoint"], "Sim. Setups:=", [setup_name], [ "NAME:Sweeps", *[ [ "NAME:SweepDefinition", "Variable:=", var_name, "Data:=", swp, "OffsetF1:=", False, "Synchronize:=", int(synchronize), ] for var_name, swp in zip(variable, swp_str) ], ], ["NAME:Sweep Operations"], ["NAME:Goals"], ], ) elif setup_type == "parametric_file": # Uses the file name as the swp_params filename = swp_params self._optimetrics.ImportSetup( "OptiParametric", [ f"NAME:{name}", filename, ], ) self._optimetrics.EditSetup( f"{name}", [ f"NAME:{name}", [ "NAME:ProdOptiSetupDataV2", "SaveFields:=", save_fields, "CopyMesh:=", copy_mesh, "SolveWithCopiedMeshOnly:=", solve_with_copied_mesh_only, ], ], ) else: raise NotImplementedError()
[docs] class HfssModeler(COMWrapper): def __init__(self, design, modeler, boundaries, mesh): """ :type design: HfssDesign """ super(HfssModeler, self).__init__() self.parent = design self._modeler = modeler self._boundaries = boundaries self._mesh = mesh # Mesh module
[docs] def set_units(self, units, rescale=True): self._modeler.SetModelUnits( ["NAME:Units Parameter", "Units:=", units, "Rescale:=", rescale] )
[docs] def get_units(self): """Get the model units. Return Value: A string contains current model units.""" return str(self._modeler.GetModelUnits())
[docs] def get_all_properties(self, obj_name, PropTab="Geometry3DAttributeTab"): """ Get all properties for modeler PropTab, PropServer """ PropServer = obj_name properties = {} for key in self._modeler.GetProperties(PropTab, PropServer): properties[key] = self._modeler.GetPropertyValue(PropTab, PropServer, key) return properties
def _attributes_array( self, name=None, nonmodel=False, wireframe=False, color=None, transparency=0.9, material=None, # str solve_inside=None, # bool coordinate_system="Global", ): arr = ["NAME:Attributes", "PartCoordinateSystem:=", coordinate_system] if name is not None: arr.extend(["Name:=", name]) if nonmodel or wireframe: flags = "NonModel" if nonmodel else "" # can be done smarter if wireframe: flags += "#" if len(flags) > 0 else "" flags += "Wireframe" arr.extend(["Flags:=", flags]) if color is not None: arr.extend(["Color:=", "(%d %d %d)" % color]) if transparency is not None: arr.extend(["Transparency:=", transparency]) if material is not None: arr.extend(["MaterialName:=", material]) if solve_inside is not None: arr.extend(["SolveInside:=", solve_inside]) return arr def _selections_array(self, *names): return ["NAME:Selections", "Selections:=", ",".join(names)]
[docs] def mesh_length(self, name_mesh, objects: list, MaxLength="0.1mm", **kwargs): """ "RefineInside:=" , False, "Enabled:=" , True, "RestrictElem:=" , False, "NumMaxElem:=" , "1000", "RestrictLength:=" , True, "MaxLength:=" , "0.1mm" Example use: modeler.assign_mesh_length('mesh2', ["Q1_mesh"], MaxLength=0.1) """ assert isinstance(objects, list) arr = [f"NAME:{name_mesh}", "Objects:=", objects, "MaxLength:=", MaxLength] ops = [ "RefineInside", "Enabled", "RestrictElem", "NumMaxElem", "RestrictLength", ] for key, val in kwargs.items(): if key in ops: arr += [key + ":=", str(val)] else: logger.error("KEY `{key}` NOT IN ops!") self._mesh.AssignLengthOp(arr)
[docs] def mesh_reassign(self, name_mesh, objects: list): assert isinstance(objects, list) self._mesh.ReassignOp(name_mesh, ["Objects:=", objects])
[docs] def mesh_get_names(self, kind="Length Based"): """ "Length Based", "Skin Depth Based", ...""" return list(self._mesh.GetOperationNames(kind))
[docs] def mesh_get_all_props(self, mesh_name): # TODO: make mesh tis own class with properties prop_tab = "MeshSetupTab" prop_server = f"MeshSetup:{mesh_name}" prop_names = self.parent._design.GetProperties("MeshSetupTab", prop_server) dic = {} for name in prop_names: dic[name] = self._modeler.GetPropertyValue(prop_tab, prop_server, name) return dic
[docs] def draw_box_corner(self, pos, size, **kwargs): name = self._modeler.CreateBox( [ "NAME:BoxParameters", "XPosition:=", str(pos[0]), "YPosition:=", str(pos[1]), "ZPosition:=", str(pos[2]), "XSize:=", str(size[0]), "YSize:=", str(size[1]), "ZSize:=", str(size[2]), ], self._attributes_array(**kwargs), ) return Box(name, self, pos, size)
[docs] def draw_box_center(self, pos, size, **kwargs): """ Creates a 3-D box centered at pos [x0, y0, z0], with width size [xwidth, ywidth, zwidth] along each respective direction. Args: pos (list): Coordinates of center of box, [x0, y0, z0] size (list): Width of box along each direction, [xwidth, ywidth, zwidth] """ corner_pos = [var(p) - var(s) / 2 for p, s in zip(pos, size)] return self.draw_box_corner(corner_pos, size, **kwargs)
[docs] def draw_polyline(self, points, closed=True, **kwargs): """Draw a closed or open polyline. If ``closed=True``, converts it to a sheet. Points must be in the correct units. Optional keyword arguments are forwarded to ``_attributes_array`` and include ``nonmodel``, ``wireframe``, ``color``, ``transparency``, ``material``, ``solve_inside``, and ``coordinate_system``. """ pointsStr = ["NAME:PolylinePoints"] indexsStr = ["NAME:PolylineSegments"] for ii, point in enumerate(points): pointsStr.append( [ "NAME:PLPoint", "X:=", str(point[0]), "Y:=", str(point[1]), "Z:=", str(point[2]), ] ) indexsStr.append( [ "NAME:PLSegment", "SegmentType:=", "Line", "StartIndex:=", ii, "NoOfPoints:=", 2, ] ) if closed: pointsStr.append( [ "NAME:PLPoint", "X:=", str(points[0][0]), "Y:=", str(points[0][1]), "Z:=", str(points[0][2]), ] ) params_closed = ["IsPolylineCovered:=", True, "IsPolylineClosed:=", True] else: indexsStr = indexsStr[:-1] params_closed = ["IsPolylineCovered:=", True, "IsPolylineClosed:=", False] name = self._modeler.CreatePolyline( ["NAME:PolylineParameters", *params_closed, pointsStr, indexsStr], self._attributes_array(**kwargs), ) if closed: return Polyline(name, self, points) else: return OpenPolyline(name, self, points)
[docs] def draw_rect_corner(self, pos, x_size=0, y_size=0, z_size=0, **kwargs): size = [x_size, y_size, z_size] assert 0 in size axis = "XYZ"[size.index(0)] w_idx, h_idx = {"X": (1, 2), "Y": (2, 0), "Z": (0, 1)}[axis] name = self._modeler.CreateRectangle( [ "NAME:RectangleParameters", "XStart:=", str(pos[0]), "YStart:=", str(pos[1]), "ZStart:=", str(pos[2]), "Width:=", str(size[w_idx]), "Height:=", str(size[h_idx]), "WhichAxis:=", axis, ], self._attributes_array(**kwargs), ) return Rect(name, self, pos, size)
[docs] def draw_rect_center(self, pos, x_size=0, y_size=0, z_size=0, **kwargs): """ Creates a rectangle centered at pos [x0, y0, z0]. It is assumed that the rectangle lies parallel to the xy, yz, or xz plane. User inputs 2 of 3 of the following: x_size, y_size, and z_size depending on how the rectangle is oriented. Args: pos (list): Coordinates of rectangle center, [x0, y0, z0] x_size (int, optional): Width along the x direction. Defaults to 0. y_size (int, optional): Width along the y direction. Defaults to 0. z_size (int, optional): Width along the z direction]. Defaults to 0. """ corner_pos = [ var(p) - var(s) / 2.0 for p, s in zip(pos, [x_size, y_size, z_size]) ] return self.draw_rect_corner(corner_pos, x_size, y_size, z_size, **kwargs)
[docs] def draw_cylinder(self, pos, radius, height, axis, **kwargs): assert axis in "XYZ" return self._modeler.CreateCylinder( [ "NAME:CylinderParameters", "XCenter:=", pos[0], "YCenter:=", pos[1], "ZCenter:=", pos[2], "Radius:=", radius, "Height:=", height, "WhichAxis:=", axis, "NumSides:=", 0, ], self._attributes_array(**kwargs), )
[docs] def draw_cylinder_center(self, pos, radius, height, axis, **kwargs): axis_idx = ["X", "Y", "Z"].index(axis) edge_pos = copy(pos) edge_pos[axis_idx] = var(pos[axis_idx]) - var(height) / 2 return self.draw_cylinder(edge_pos, radius, height, axis, **kwargs)
[docs] def draw_wirebond( self, pos, ori, width, height="0.1mm", z=0, wire_diameter="0.02mm", NumSides=6, **kwargs, ): """ Args: pos: 2D position vector (specify center point) ori: should be normed z: z position # TODO create Wirebond class position is the origin of one point ori is the orientation vector, which gets normalized """ p = np.array(pos) o = np.array(ori) pad1 = p - o * width / 2.0 name = self._modeler.CreateBondwire( [ "NAME:BondwireParameters", "WireType:=", "Low", "WireDiameter:=", wire_diameter, "NumSides:=", NumSides, "XPadPos:=", pad1[0], "YPadPos:=", pad1[1], "ZPadPos:=", z, "XDir:=", ori[0], "YDir:=", ori[1], "ZDir:=", 0, "Distance:=", width, "h1:=", height, "h2:=", "0mm", "alpha:=", "80deg", "beta:=", "80deg", "WhichAxis:=", "Z", ], self._attributes_array(**kwargs), ) return name
[docs] def draw_region( self, Padding, PaddingType="Percentage Offset", name="Region", material='"vacuum"', ): """ PaddingType : 'Absolute Offset', "Percentage Offset" """ # TODO: Add option to modify these RegionAttributes = [ "NAME:Attributes", "Name:=", name, "Flags:=", "Wireframe#", "Color:=", "(255 0 0)", "Transparency:=", 1, "PartCoordinateSystem:=", "Global", "UDMId:=", "", "IsAlwaysHiden:=", False, "MaterialValue:=", material, "SolveInside:=", True, ] self._modeler.CreateRegion( [ "NAME:RegionParameters", "+XPaddingType:=", PaddingType, "+XPadding:=", Padding[0][0], "-XPaddingType:=", PaddingType, "-XPadding:=", Padding[0][1], "+YPaddingType:=", PaddingType, "+YPadding:=", Padding[1][0], "-YPaddingType:=", PaddingType, "-YPadding:=", Padding[1][1], "+ZPaddingType:=", PaddingType, "+ZPadding:=", Padding[2][0], "-ZPaddingType:=", PaddingType, "-ZPadding:=", Padding[2][1], ], RegionAttributes, )
[docs] def unite(self, names, keep_originals=False): self._modeler.Unite( self._selections_array(*names), ["NAME:UniteParameters", "KeepOriginals:=", keep_originals], ) return names[0]
[docs] def intersect(self, names, keep_originals=False): self._modeler.Intersect( self._selections_array(*names), ["NAME:IntersectParameters", "KeepOriginals:=", keep_originals], ) return names[0]
[docs] def translate(self, name, vector): self._modeler.Move( self._selections_array(name), [ "NAME:TranslateParameters", "TranslateVectorX:=", vector[0], "TranslateVectorY:=", vector[1], "TranslateVectorZ:=", vector[2], ], )
[docs] def get_boundary_assignment(self, boundary_name: str): # Gets a list of face IDs associated with the given boundary or excitation assignment. objects = self._boundaries.GetBoundaryAssignment(boundary_name) # Gets an object name corresponding to the input face id. Returns the name of the corresponding object name. objects = [self._modeler.GetObjectNameByFaceID(k) for k in objects] return objects
[docs] def append_PerfE_assignment(self, boundary_name: str, object_names: list): """ This will create a new boundary if need, and will otherwise append given names to an existing boundary """ # enforce boundary_name = str(boundary_name) if isinstance(object_names, str): object_names = [object_names] object_names = list(object_names) # enforce list # do actual work if ( boundary_name not in self._boundaries.GetBoundaries() ): # GetBoundariesOfType("Perfect E") # need to make a new boundary self.assign_perfect_E(object_names, name=boundary_name) else: # need to append objects = list(self.get_boundary_assignment(boundary_name)) self._boundaries.ReassignBoundary( [ "NAME:" + boundary_name, "Objects:=", list(set(objects + object_names)), ] )
[docs] def append_mesh(self, mesh_name: str, object_names: list, old_objs: list, **kwargs): """ This will create a new boundary if need, and will otherwise append given names to an existing boundary old_obj = circ._mesh_assign """ mesh_name = str(mesh_name) if isinstance(object_names, str): object_names = [object_names] object_names = list(object_names) # enforce list if mesh_name not in self.mesh_get_names(): # need to make a new boundary objs = object_names self.mesh_length(mesh_name, object_names, **kwargs) else: # need to append objs = list(set(old_objs + object_names)) self.mesh_reassign(mesh_name, objs) return objs
[docs] def assign_perfect_E(self, obj: List[str], name: str = "PerfE"): """ Assign a boundary condition to a list of objects. Arg: objs (List[str]): Takes a name of an object or a list of object names. name(str): If `name` is not specified `PerfE` is appended to object name for the name. """ if not isinstance(obj, list): obj = [obj] if name == "PerfE": name = str(obj) + "_" + name name = increment_name(name, self._boundaries.GetBoundaries()) self._boundaries.AssignPerfectE( ["NAME:" + name, "Objects:=", obj, "InfGroundPlane:=", False] )
def _make_lumped_rlc(self, r, l, c, start, end, obj_arr, name="LumpRLC"): name = increment_name(name, self._boundaries.GetBoundaries()) params = ["NAME:" + name] params += obj_arr params.append( [ "NAME:CurrentLine", # for some reason here it seems to switch to use the model units, rather than meters "Start:=", fix_units(start, unit_assumed=LENGTH_UNIT), "End:=", fix_units(end, unit_assumed=LENGTH_UNIT), ] ) params += [ "UseResist:=", r != 0, "Resistance:=", r, "UseInduct:=", l != 0, "Inductance:=", l, "UseCap:=", c != 0, "Capacitance:=", c, ] self._boundaries.AssignLumpedRLC(params) def _make_lumped_port(self, start, end, obj_arr, z0="50ohm", name="LumpPort"): start = fix_units(start, unit_assumed=LENGTH_UNIT) end = fix_units(end, unit_assumed=LENGTH_UNIT) name = increment_name(name, self._boundaries.GetBoundaries()) params = ["NAME:" + name] params += obj_arr params += [ "RenormalizeAllTerminals:=", True, "DoDeembed:=", False, [ "NAME:Modes", [ "NAME:Mode1", "ModeNum:=", 1, "UseIntLine:=", True, ["NAME:IntLine", "Start:=", start, "End:=", end], "CharImp:=", "Zpi", "AlignmentGroup:=", 0, "RenormImp:=", "50ohm", ], ], "ShowReporterFilter:=", False, "ReporterFilter:=", [True], "FullResistance:=", z0, "FullReactance:=", "0ohm", ] self._boundaries.AssignLumpedPort(params)
[docs] def get_face_ids(self, obj): return self._modeler.GetFaceIDs(obj)
[docs] def get_object_name_by_face_id(self, ID: str): """Gets an object name corresponding to the input face id.""" return self._modeler.GetObjectNameByFaceID(ID)
[docs] def get_vertex_ids(self, obj): """ Get the vertex IDs of given an object name oVertexIDs = oEditor.GetVertexIDsFromObject(“Box1”) """ return self._modeler.GetVertexIDsFromObject(obj)
[docs] def eval_expr(self, expr, units="mm"): if not isinstance(expr, str): return expr return self.parent.eval_expr(expr, units)
[docs] def get_objects_in_group(self, group): """Return the objects in the specified HFSS group. Parameters ---------- group : str One of a material name, assignment name, ``"Non Model"``, ``"Solids"``, ``"Unclassified"``, ``"Sheets"``, or ``"Lines"``. """ if self._modeler: return list(self._modeler.GetObjectsInGroup(group)) else: return list()
[docs] def set_working_coordinate_system(self, cs_name="Global"): """ Use: Sets the working coordinate system. Command: Modeler>Coordinate System>Set Working CS """ self._modeler.SetWCS( [ "NAME:SetWCS Parameter", "Working Coordinate System:=", cs_name, "RegionDepCSOk:=", False, # this one is prob not needed, but comes with the record tool ] )
[docs] def create_relative_coorinate_system_both( self, cs_name, origin=["0um", "0um", "0um"], XAxisVec=["1um", "0um", "0um"], YAxisVec=["0um", "1um", "0um"], ): """ Use: Creates a relative coordinate system. Only the Name attribute of the <AttributesArray> parameter is supported. Command: Modeler>Coordinate System>Create>Relative CS->Offset Modeler>Coordinate System>Create>Relative CS->Rotated Modeler>Coordinate System>Create>Relative CS->Both Current coordinate system is set right after this. cs_name : name of coord. sys If the name already exists, then a new coordinate system with _1 is created. origin, XAxisVec, YAxisVec: 3-vectors You can also pass in params such as origin = [0,1,0] rather than ["0um","1um","0um"], but these will be interpreted in default units, so it is safer to be explicit. Explicit over implicit. """ self._modeler.CreateRelativeCS( [ "NAME:RelativeCSParameters", "Mode:=", "Axis/Position", "OriginX:=", origin[0], "OriginY:=", origin[1], "OriginZ:=", origin[2], "XAxisXvec:=", XAxisVec[0], "XAxisYvec:=", XAxisVec[1], "XAxisZvec:=", XAxisVec[2], "YAxisXvec:=", YAxisVec[0], "YAxisYvec:=", YAxisVec[1], "YAxisZvec:=", YAxisVec[2], ], ["NAME:Attributes", "Name:=", cs_name], )
[docs] def subtract(self, blank_name, tool_names, keep_originals=False): selection_array = [ "NAME:Selections", "Blank Parts:=", blank_name, "Tool Parts:=", ",".join(tool_names), ] self._modeler.Subtract( selection_array, ["NAME:UniteParameters", "KeepOriginals:=", keep_originals] ) return blank_name
def _fillet(self, radius, vertex_index, obj): vertices = self._modeler.GetVertexIDsFromObject(obj) if isinstance(vertex_index, list): to_fillet = [int(vertices[v]) for v in vertex_index] else: to_fillet = [int(vertices[vertex_index])] # print(vertices) # print(radius) self._modeler.Fillet( ["NAME:Selections", "Selections:=", obj], [ "NAME:Parameters", [ "NAME:FilletParameters", "Edges:=", [], "Vertices:=", to_fillet, "Radius:=", radius, "Setback:=", "0mm", ], ], ) def _fillet_edges(self, radius, edge_index, obj): edges = self._modeler.GetEdgeIDsFromObject(obj) if isinstance(edge_index, list): to_fillet = [int(edges[e]) for e in edge_index] else: to_fillet = [int(edges[edge_index])] self._modeler.Fillet( ["NAME:Selections", "Selections:=", obj], [ "NAME:Parameters", [ "NAME:FilletParameters", "Edges:=", to_fillet, "Vertices:=", [], "Radius:=", radius, "Setback:=", "0mm", ], ], ) def _fillets(self, radius, vertices, obj): self._modeler.Fillet( ["NAME:Selections", "Selections:=", obj], [ "NAME:Parameters", [ "NAME:FilletParameters", "Edges:=", [], "Vertices:=", vertices, "Radius:=", radius, "Setback:=", "0mm", ], ], ) def _sweep_along_path(self, to_sweep, path_obj): """ Adds thickness to path_obj by extending to a new dimension. to_sweep acts as a putty knife that determines the thickness. Args: to_sweep (polyline): Small polyline running perpendicular to path_obj whose length is the desired resulting thickness path_obj (polyline): Original polyline; want to broaden this """ self.rename_obj(path_obj, str(path_obj) + "_path") new_name = self.rename_obj(to_sweep, path_obj) names = [path_obj, str(path_obj) + "_path"] self._modeler.SweepAlongPath( self._selections_array(*names), [ "NAME:PathSweepParameters", "DraftAngle:=", "0deg", "DraftType:=", "Round", "CheckFaceFaceIntersection:=", False, "TwistAngle:=", "0deg", ], ) return Polyline(new_name, self)
[docs] def sweep_along_vector(self, names, vector): self._modeler.SweepAlongVector( self._selections_array(*names), [ "NAME:VectorSweepParameters", "DraftAngle:=", "0deg", "DraftType:=", "Round", "CheckFaceFaceIntersection:=", False, "SweepVectorX:=", vector[0], "SweepVectorY:=", vector[1], "SweepVectorZ:=", vector[2], ], )
[docs] def rename_obj(self, obj, name): self._modeler.ChangeProperty( [ "NAME:AllTabs", [ "NAME:Geometry3DAttributeTab", ["NAME:PropServers", str(obj)], ["NAME:ChangedProps", ["NAME:Name", "Value:=", str(name)]], ], ] ) return name
[docs] class ModelEntity(str, HfssPropertyObject): prop_tab = "Geometry3DCmdTab" model_command = None transparency = make_float_prop( "Transparent", prop_tab="Geometry3DAttributeTab", prop_server=lambda self: self ) material = make_str_prop( "Material", prop_tab="Geometry3DAttributeTab", prop_server=lambda self: self ) wireframe = make_float_prop( "Display Wireframe", prop_tab="Geometry3DAttributeTab", prop_server=lambda self: self, ) coordinate_system = make_str_prop("Coordinate System") def __new__(self, val, *args, **kwargs): return str.__new__(self, val) def __init__(self, val, modeler): """ :type val: str :type modeler: HfssModeler """ super( ModelEntity, self ).__init__() # val) #Comment out keyword to match arguments self.modeler = modeler self.prop_server = self + ":" + self.model_command + ":1"
[docs] class Box(ModelEntity): model_command = "CreateBox" position = make_float_prop("Position") x_size = make_float_prop("XSize") y_size = make_float_prop("YSize") z_size = make_float_prop("ZSize") def __init__(self, name, modeler, corner, size): """ :type name: str :type modeler: HfssModeler :type corner: [(VariableString, VariableString, VariableString)] :param size: [(VariableString, VariableString, VariableString)] """ super(Box, self).__init__(name, modeler) self.modeler = modeler self.prop_holder = modeler._modeler self.corner = corner self.size = size self.center = [c + s / 2 for c, s in zip(corner, size)] faces = modeler.get_face_ids(self) self.z_back_face, self.z_front_face = faces[0], faces[1] self.y_back_face, self.y_front_face = faces[2], faces[4] self.x_back_face, self.x_front_face = faces[3], faces[5]
[docs] class Rect(ModelEntity): model_command = "CreateRectangle" # TODO: Add a rotated rectangle object. # Will need to first create rect, then apply rotate operation. def __init__(self, name, modeler, corner, size): super(Rect, self).__init__(name, modeler) self.prop_holder = modeler._modeler self.corner = corner self.size = size self.center = [c + s / 2 if s else c for c, s in zip(corner, size)]
[docs] def make_center_line(self, axis): """ Returns `start` and `end` list of 3 coordinates """ axis_idx = ["x", "y", "z"].index(axis.lower()) start = [c for c in self.center] start[axis_idx] -= self.size[axis_idx] / 2 start = [self.modeler.eval_expr(s) for s in start] end = [c for c in self.center] end[axis_idx] += self.size[axis_idx] / 2 end = [self.modeler.eval_expr(s) for s in end] return start, end
[docs] def make_rlc_boundary(self, axis, r=0, l=0, c=0, name="LumpRLC"): start, end = self.make_center_line(axis) self.modeler._make_lumped_rlc( r, l, c, start, end, ["Objects:=", [self]], name=name )
[docs] def make_lumped_port(self, axis, z0="50ohm", name="LumpPort"): start, end = self.make_center_line(axis) self.modeler._make_lumped_port( start, end, ["Objects:=", [self]], z0=z0, name=name )
[docs] class Polyline(ModelEntity): """ Assume closed polyline, which creates a polygon. """ model_command = "CreatePolyline" def __init__(self, name, modeler, points=None): super(Polyline, self).__init__(name, modeler) self.prop_holder = modeler._modeler if points is not None: self.points = points self.n_points = len(points) else: pass # TODO: points = collection of points # axis = find_orth_axis() # TODO: find the plane of the polyline for now, assume Z # def find_orth_axis(): # X, Y, Z = (True, True, True) # for point in points: # X =
[docs] def unite(self, list_other): union = self.modeler.unite(self + list_other) return Polyline(union, self.modeler)
[docs] def make_center_line(self, axis): # Expects to act on a rectangle... # first : find center and size center = [0, 0, 0] for point in self.points: center = [ center[0] + point[0] / self.n_points, center[1] + point[1] / self.n_points, center[2] + point[2] / self.n_points, ] size = [ 2 * (center[0] - self.points[0][0]), 2 * (center[1] - self.points[0][1]), 2 * (center[1] - self.points[0][2]), ] axis_idx = ["x", "y", "z"].index(axis.lower()) start = [c for c in center] start[axis_idx] -= size[axis_idx] / 2 start = [self.modeler.eval_var_str(s, unit=LENGTH_UNIT) for s in start] # TODO end = [c for c in center] end[axis_idx] += size[axis_idx] / 2 end = [self.modeler.eval_var_str(s, unit=LENGTH_UNIT) for s in end] return start, end
[docs] def make_rlc_boundary(self, axis, r=0, l=0, c=0, name="LumpRLC"): name = str(self) + "_" + name start, end = self.make_center_line(axis) self.modeler._make_lumped_rlc( r, l, c, start, end, ["Objects:=", [self]], name=name )
[docs] def fillet(self, radius, vertex_index): self.modeler._fillet(radius, vertex_index, self)
[docs] def vertices(self): return self.modeler.get_vertex_ids(self)
[docs] def rename(self, new_name): """ Warning: The increment_name only works if the sheet has not been stracted or used as a tool elsewhere. These names are not checked; they require modifying get_objects_in_group. """ new_name = increment_name( new_name, self.modeler.get_objects_in_group("Sheets") ) # this is for a closed polyline # check to get the actual new name in case there was a substracted object with that name face_ids = self.modeler.get_face_ids(str(self)) self.modeler.rename_obj(self, new_name) # now rename if len(face_ids) > 0: new_name = self.modeler.get_object_name_by_face_id(face_ids[0]) return Polyline(str(new_name), self.modeler)
[docs] class OpenPolyline(ModelEntity): # Assume closed polyline model_command = "CreatePolyline" show_direction = make_prop( "Show Direction", prop_tab="Geometry3DAttributeTab", prop_server=lambda self: self, ) def __init__(self, name, modeler, points=None): super(OpenPolyline, self).__init__(name, modeler) self.prop_holder = modeler._modeler if points is not None: self.points = points self.n_points = len(points) else: pass # axis = find_orth_axis() # TODO: find the plane of the polyline for now, assume Z # def find_orth_axis(): # X, Y, Z = (True, True, True) # for point in points: # X =
[docs] def vertices(self): return self.modeler.get_vertex_ids(self)
[docs] def fillet(self, radius, vertex_index): self.modeler._fillet(radius, vertex_index, self)
[docs] def fillets(self, radius, do_not_fillet=[]): """ do_not_fillet : Index list of vertices to not fillete """ raw_list_vertices = self.modeler.get_vertex_ids(self) list_vertices = [] for vertex in raw_list_vertices[1:-1]: # ignore the start and finish list_vertices.append(int(vertex)) list_vertices = list( map(int, np.delete(list_vertices, np.array(do_not_fillet, dtype=int) - 1)) ) # print(list_vertices, type(list_vertices[0])) if len(list_vertices) != 0: self.modeler._fillets(radius, list_vertices, self) else: pass
[docs] def sweep_along_path(self, to_sweep): return self.modeler._sweep_along_path(to_sweep, self)
[docs] def rename(self, new_name): """ Warning: The increment_name only works if the sheet has not been stracted or used as a tool elsewher. These names are not checked - They require modifying get_objects_in_group """ new_name = increment_name(new_name, self.modeler.get_objects_in_group("Lines")) # , self.points) return OpenPolyline(self.modeler.rename_obj(self, new_name), self.modeler)
[docs] def copy(self, new_name): new_obj = OpenPolyline(self.modeler.copy(self), self.modeler) return new_obj.rename(new_name)
[docs] class HfssFieldsCalc(COMWrapper): def __init__(self, setup): """ :type setup: HfssSetup """ self.setup = setup super(HfssFieldsCalc, self).__init__() self.parent = setup self.Mag_E = NamedCalcObject("Mag_E", setup) self.Mag_H = NamedCalcObject("Mag_H", setup) self.Mag_Jsurf = NamedCalcObject("Mag_Jsurf", setup) self.Mag_Jvol = NamedCalcObject("Mag_Jvol", setup) self.Vector_E = NamedCalcObject("Vector_E", setup) self.Vector_H = NamedCalcObject("Vector_H", setup) self.Vector_Jsurf = NamedCalcObject("Vector_Jsurf", setup) self.Vector_Jvol = NamedCalcObject("Vector_Jvol", setup) self.ComplexMag_E = NamedCalcObject("ComplexMag_E", setup) self.ComplexMag_H = NamedCalcObject("ComplexMag_H", setup) self.ComplexMag_Jsurf = NamedCalcObject("ComplexMag_Jsurf", setup) self.ComplexMag_Jvol = NamedCalcObject("ComplexMag_Jvol", setup) self.P_J = NamedCalcObject("P_J", setup) self.named_expression = {} # dictionary to hold additional named expressions
[docs] def clear_named_expressions(self): self.parent.parent._fields_calc.ClearAllNamedExpr()
[docs] def declare_named_expression(self, name): """ " If a named expression has been created in the fields calculator, this function can be called to initialize the name to work with the fields object """ self.named_expression[name] = NamedCalcObject(name, self.setup)
[docs] def use_named_expression(self, name): """ Expression can be used to access dictionary of named expressions, Alternately user can access dictionary directly via named_expression() """ return self.named_expression[name]
[docs] class CalcObject(COMWrapper): def __init__(self, stack, setup): """ :type stack: [(str, str)] :type setup: HfssSetup """ super(CalcObject, self).__init__() self.stack = stack self.setup = setup self.calc_module = setup.parent._fields_calc def _bin_op(self, other, op): if isinstance(other, (int, float)): other = ConstantCalcObject(other, self.setup) stack = self.stack + other.stack stack.append(("CalcOp", op)) return CalcObject(stack, self.setup) def _unary_op(self, op): stack = self.stack[:] stack.append(("CalcOp", op)) return CalcObject(stack, self.setup) def __add__(self, other): return self._bin_op(other, "+") def __radd__(self, other): return self + other def __sub__(self, other): return self._bin_op(other, "-") def __rsub__(self, other): return (-self) + other def __mul__(self, other): return self._bin_op(other, "*") def __rmul__(self, other): return self * other def __div__(self, other): return self._bin_op(other, "/") def __rdiv__(self, other): other = ConstantCalcObject(other, self.setup) return other / self def __pow__(self, other): return self._bin_op(other, "Pow")
[docs] def dot(self, other): return self._bin_op(other, "Dot")
def __neg__(self): return self._unary_op("Neg") def __abs__(self): return self._unary_op("Abs") def __mag__(self): return self._unary_op("Mag")
[docs] def mag(self): return self._unary_op("Mag")
[docs] def smooth(self): return self._unary_op("Smooth")
[docs] def conj(self): return self._unary_op("Conj") # make this right
[docs] def scalar_x(self): return self._unary_op("ScalarX")
[docs] def scalar_y(self): return self._unary_op("ScalarY")
[docs] def scalar_z(self): return self._unary_op("ScalarZ")
[docs] def norm_2(self): return (self.__mag__()).__pow__(2)
# return self._unary_op("ScalarX")**2+self._unary_op("ScalarY")**2+self._unary_op("ScalarZ")**2
[docs] def real(self): return self._unary_op("Real")
[docs] def imag(self): return self._unary_op("Imag")
[docs] def complexmag(self): return self._unary_op("CmplxMag")
def _integrate(self, name, type): stack = self.stack + [(type, name), ("CalcOp", "Integrate")] return CalcObject(stack, self.setup) def _maximum(self, name, type): stack = self.stack + [(type, name), ("CalcOp", "Maximum")] return CalcObject(stack, self.setup)
[docs] def getQty(self, name): stack = self.stack + [("EnterQty", name)] return CalcObject(stack, self.setup)
[docs] def integrate_line(self, name): return self._integrate(name, "EnterLine")
[docs] def normal2surface(self, name): """return the part normal to surface. Complex Vector.""" stack = self.stack + [("EnterSurf", name), ("CalcOp", "Normal")] stack.append(("CalcOp", "Dot")) stack.append(("EnterSurf", name)) stack.append(("CalcOp", "Normal")) stack.append(("CalcOp", "*")) return CalcObject(stack, self.setup)
[docs] def tangent2surface(self, name): """return the part tangent to surface. Complex Vector.""" stack = self.stack + [("EnterSurf", name), ("CalcOp", "Normal")] stack.append(("CalcOp", "Dot")) stack.append(("EnterSurf", name)) stack.append(("CalcOp", "Normal")) stack.append(("CalcOp", "*")) stack = self.stack + stack stack.append(("CalcOp", "-")) return CalcObject(stack, self.setup)
[docs] def integrate_line_tangent(self, name): """integrate line tangent to vector expression \n name = of line to integrate over""" self.stack = self.stack + [ ("EnterLine", name), ("CalcOp", "Tangent"), ("CalcOp", "Dot"), ] return self.integrate_line(name)
[docs] def line_tangent_coor(self, name, coordinate): """integrate line tangent to vector expression \n name = of line to integrate over""" if coordinate not in ["X", "Y", "Z"]: raise ValueError self.stack = self.stack + [ ("EnterLine", name), ("CalcOp", "Tangent"), ("CalcOp", "Scalar" + coordinate), ] return self.integrate_line(name)
[docs] def integrate_surf(self, name="AllObjects"): return self._integrate(name, "EnterSurf")
[docs] def integrate_vol(self, name="AllObjects"): return self._integrate(name, "EnterVol")
[docs] def maximum_vol(self, name="AllObjects"): return self._maximum(name, "EnterVol")
[docs] def times_eps(self): stack = self.stack + [("ClcMaterial", ("Permittivity (epsi)", "mult"))] return CalcObject(stack, self.setup)
[docs] def times_mu(self): stack = self.stack + [("ClcMaterial", ("Permeability (mu)", "mult"))] return CalcObject(stack, self.setup)
[docs] def write_stack(self): for fn, arg in self.stack: if np.size(arg) > 1 and fn not in ["EnterVector"]: getattr(self.calc_module, fn)(*arg) else: getattr(self.calc_module, fn)(arg)
[docs] def save_as(self, name): """if the object already exists, try clearing your named expressions first with fields.clear_named_expressions""" self.write_stack() self.calc_module.AddNamedExpr(name) return NamedCalcObject(name, self.setup)
[docs] def evaluate(self, phase=0, lv=None, print_debug=False): # , n_mode=1): self.write_stack() if print_debug: print("---------------------") print("writing to stack: OK") print("-----------------") # self.calc_module.set_mode(n_mode, 0) setup_name = self.setup.solution_name if lv is not None: args = lv else: args = [] args.append("Phase:=") args.append(str(int(phase)) + "deg") if isinstance(self.setup, HfssDMSetup): args.extend(["Freq:=", self.setup.solution_freq]) self.calc_module.ClcEval(setup_name, args) return float(self.calc_module.GetTopEntryValue(setup_name, args)[0])
[docs] class NamedCalcObject(CalcObject): def __init__(self, name, setup): self.name = name stack = [("CopyNamedExprToStack", name)] super(NamedCalcObject, self).__init__(stack, setup)
[docs] class ConstantCalcObject(CalcObject): def __init__(self, num, setup): stack = [("EnterScalar", num)] super(ConstantCalcObject, self).__init__(stack, setup)
[docs] class ConstantVecCalcObject(CalcObject): def __init__(self, vec, setup): stack = [("EnterVector", vec)] super(ConstantVecCalcObject, self).__init__(stack, setup)
[docs] def get_active_project(): """If you see the error: "The requested operation requires elevation." then you need to run your python as an admin. """ import ctypes import os try: is_admin = os.getuid() == 0 except AttributeError: is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 if not is_admin: print( "\033[93m WARNING: you are not running as an admin! \ You need to run as an admin. You will probably get an error next.\ \033[0m" ) app = HfssApp() desktop = app.get_app_desktop() return desktop.get_active_project()
[docs] def get_active_design(): project = get_active_project() return project.get_active_design()
[docs] def get_report_arrays(name: str): d = get_active_design() r = HfssReport(d, name) return r.get_arrays()
[docs] def load_ansys_project( proj_name: str, project_path: str = None, extension: str = ".aedt" ): """ Utility function to load an Ansys project. Args: proj_name : None --> get active. (make sure 2 run as admin) extension : `aedt` is for 2016 version and newer """ if project_path: # convert slashes correctly for system project_path = Path(project_path) # Checks assert project_path.is_dir(), ( "ERROR! project_path is not a valid directory \N{LOUDLY CRYING FACE}.\ Check the path, and especially \\ characters." ) project_path = Path(project_path, proj_name).with_suffix(extension) if (project_path).is_file(): logger.info("\tFile path to HFSS project found.") else: raise Exception( "ERROR! Valid directory, but invalid project filename. \N{LOUDLY CRYING FACE} Not found!\ Please check your filename.\n%s\n" % project_path ) if (project_path / ".lock").is_file(): logger.warning( "\t\tFile is locked. \N{FEARFUL FACE} If connection fails, delete the .lock file." ) app = HfssApp() logger.info("\tOpened Ansys App") desktop = app.get_app_desktop() logger.info(f"\tOpened Ansys Desktop v{desktop.get_version()}") # logger.debug(f"\tOpen projects: {desktop.get_project_names()}") if proj_name is not None: if proj_name in desktop.get_project_names(): desktop.set_active_project(proj_name) project = desktop.get_active_project() else: project = desktop.open_project(str(project_path)) else: projects_in_app = desktop.get_projects() if projects_in_app: project = desktop.get_active_project() else: project = None if project: logger.info( f"\tOpened Ansys Project\n\tFolder: {project.get_path()}\n\tProject: {project.name}" ) else: logger.info(f"\tAnsys Project was not found.\n\t Project is None.") return app, desktop, project