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)

from typing import List

import atexit
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

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

from . import logger

# 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 Dispatch, CDispatch
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: print("Couldn't parse", expr) raise
[docs]def increment_name(base, existing): if not base 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): """Use: Collects the messages from a specified project and design. Syntax: GetMessages <ProjectName>, <DesignName>, <SeverityName> Return Value: A simple array of strings. Parameters: <ProjectName> Type:<string> Name of the project for which to collect messages. An incorrect project name results in no messages (design is ignored) An empty project name results in all messages (design is ignored) <DesignName> Type: <string> Name of the design in the named project for which to collect messages An incorrect design name results in no messages for the named project An empty design name results in all messages for the named project <SeverityName> Type: <integer> Severity is 0-3, and is tied in to info/warning/error/fatal types as follows: 0 is info and above 1 is warning and above 2 is error and fatal 3 is fatal only (rarely used) """ 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): self._desktop.SetActiveProject(name)
@property def project_directory(self): return self._desktop.GetProjectDirectory() @project_directory.setter def project_directory(self, path): self._desktop.SetProjectDirectory(path) @property def library_directory(self): return self._desktop.GetLibraryDirectory() @library_directory.setter def library_directory(self, path): self._desktop.SetLibraryDirectory(path) @property def temp_directory(self): 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 model design Args: name (str): Name of driven modal design """ return self.new_design(name, "DrivenModal")
[docs] def new_em_design(self, name: str): """Create a new eigenmode design Args: name (str): Name of eigenmode design """ return self.new_design(name, "Eigenmode")
[docs] def new_q3d_design(self, name: str): """Create a new Q3D design. Args: name (str): Name of Q3D design """ return self.new_design(name, "Q3D", "Q3D Extractor")
@property # v2016 def name(self): return self._project.GetName()
[docs]class HfssDesign(COMWrapper): 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: # This function does not exist if the design is not HFSS self.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): 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 display, Image 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): """ :rtype: HfssSetup """ 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 == "DrivenModal": return HfssDMSetup(self, name) elif self.solution_type == "DrivenTerminal": return HfssDTSetup(self, name) elif self.solution_type == "Q3D": return AnsysQ3DSetup(self, name)
[docs] def create_q3d_setup(self, freq_ghz=5., 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): """ DeleteFullVariation Use: Use to selectively make deletions or delete all solution data. Command: HFSS>Results>Clean Up Solutions... Syntax: DeleteFullVariation Array(<parameters>), boolean Parameters: All | <DataSpecifierArray> If, All, all data of existing variations is deleted. Array(<DesignVariationKey>, ) <DesignVariationKey> Type: <string> Design variation string. <Boolean> Type: boolean 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 == 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): 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): ''' Use: Solves a single solution setup and all of its frequency sweeps. Command: Right-click a solution setup in the project tree, and then click Analyze on the shortcut menu. Syntax: Analyze(<SetupName>) Parameters: <setupName> Return Value: None ----------------------------------------------------- Will block the until the analysis is completely done. Will raise a com_error if analysis is 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): ''' Use: Performs a blocking simulation. The next script command will not be executed until the simulation is complete. Command: HFSS>Analyze Syntax: Solve <SetupNameArray> Return Value: Type: <int> -1: simulation error 0: normal completion Parameters: <SetupNameArray>: Array(<SetupName>, <SetupName>, ...) <SetupName> Type: <string> Name of the solution setup to solve. Example: return_status = oDesign.Solve Array("Setup1", "Setup2") ----------------------------------------------------- HFSS abort: still returns 0 , since termination by user. ''' 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=""): ''' Returns df # Triangle Delta % Pass 1 164 NaN ''' 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"): ''' Arguments: ----------- variation: an empty string returns nominal variation. Otherwise need the list frequency: in Hz soln_type = "C", "AC RL" and "DC RL" solution_kind = 'LastAdaptive' # AdaptivePass Internals: ----------- Uses self.solution_name = Setup1 : LastAdaptive Returns: --------------------- 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 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): ''' Gets all available solution names that exist in a design. Return example: ('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. variation_string : str | None This must the string that describes the variation in hFSS, not 0 or 1, but the string of variables, such as "Cj='2fF' Lj='12.75nH'" If None, gets 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, setup_type='parametric_file', pass in a file name and path to swp_params like "C:\\test.csv" or "C:\\test.txt" for example. Example csv formatting: *,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): """ Draws a closed or open polyline. If closed = True, then will make into a sheet. points : need to be in the correct units For optional arguments, see _attributes_array; these include: ``` nonmodel=False, wireframe=False, color=None, transparency=0.9, material=None, # str solve_inside=None, # bool coordinate_system="Global" ``` """ 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. 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. 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): """ Use: Returns the objects for the specified group. Return Value: The objects in the group. Parameters: <groupName> Type: <string> One of <materialName>, <assignmentName>, "Non Model", "Solids", "Unclassi­fied", "Sheets", "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[1] ], ["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 /= project_path / Path(proj_name + 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