首页/文章/ 详情

用Python实现喇叭天线设计小工具(三)

4年前浏览3969
摘要:本节主要介绍波导查值模块,以及HFSS调用模块的编写。

波导查值模块

该部分要实现的功能主要是根据输入的工作频率,自动选择合适的标准波导尺寸,免去翻资料的麻烦,实现起来逻辑很简单,也就是单纯地根据条件来查值,查值方法有两种:

  1. 根据工作频点来判断;

  2. 根据波导名直接查询;


两种方式均可用简单的函数来实现,代码如下所示(wg.py):

"""
波导尺寸查询
"""

# 标准波导尺寸表
wg_list = (
   ('BJ3', 0.32, 0.49, 584.2, 292.1),
   ('BJ4', 0.35, 0.53, 533.4, 266.7),
   ('BJ5', 0.41, 0.62, 457.2, 228.6),
   ('BJ6', 0.49, 0.75, 381.0, 190.5),
   ('BJ8', 0.64, 0.98, 292.1, 146.05),
   ('BJ9', 0.76, 1.15, 247.65, 123.82),
   ('BJ12', 0.96, 1.46, 195.58, 97.79),
   ('BJ14', 1.13, 1.73, 165.10, 82.55),
   ('BJ18', 1.45, 2.20, 129.54, 64.77),
   ('BJ22', 1.72, 2.61, 109.22, 54.61),
   ('BJ26', 2.17, 3.30, 86.36, 43.18),
   ('BJ32', 2.60, 3.35, 72.14, 34.04),
   ('BJ40', 3.22, 4.90, 58.17, 29.08),
   ('BJ48', 3.94, 5.99, 47.549, 22.149),
   ('BJ58', 4.64, 7.05, 40.386, 20.193),
   ('BJ70', 5.38, 8.17, 34.849, 15.799),
   ('BJ84', 6.57, 9.99, 28.499, 12.624),
   ('BJ100', 8.20, 12.5, 22.86, 10.160),
   ('BJ120', 9.84, 15.0, 19.050, 9.525),
   ('BJ140', 11.9, 18.0, 15.799, 7.899),
   ('BJ180', 14.5, 22.0, 12.954, 6.477),
   ('BJ220', 17.6, 26.7, 10.668, 4.318),
   ('BJ260', 21.7, 33.0, 8.636, 4.318),
   ('BJ320', 26.3, 40.0, 7.112, 3.556),
   ('BJ400', 32.9, 50.1, 5.690, 2.845),
   ('BJ500', 39.2, 59.6, 4.775, 2.388),
   ('BJ620', 49.8, 75.8, 3.759, 1.880),
)


def check_by_freq(freq):
   """
   通过输入频点查找标准波导尺寸
   :param freq: 频点,GHz
   :return: 波导的宽边a和短边b,mm
   """
   a, b = None, None
   for i in wg_list:
       if i[1] <= freq <= i[2]:
           a, b = i[3], i[4]
           break
   return a, b


def check_by_name(name):
   """
   通过名称来查找标准波导尺寸
   :param name: 波导名,如‘BJ100’
   :return: 波导的宽边a和短边b,mm
   """
   a, b = None, None
   for i in wg_list:
       if i[0] == name:
           a, b = i[3], i[4]
           break
   return a, b


if __name__ == '__main__':
   a1, b1 = check_by_freq(25)
   a2, b2 = check_by_name('BJ320')
   print(a1, b1)
   print(a2, b2)

实际使用中第一种“自动判断”的场景要多一些,但有时候也会特别指定波导型号,故两者皆写好供调用。

HFSS调用模块

这一部分是稍微比较麻烦的,需要用到HFSS自己的“代码录制”功能和Python的win32com模块。

首先,众所周知,HFSS支持脚本控制,可将操作录制为一段脚本,便于自动化运行以及实现较为复杂的建模功能,该功能位于Tools——Record Script to File中,支持vbs和python两种格式(早期版本仅支持vbs)。


因此,要调用HFSS建模,自然会想到先用“代码录制”功能将建模操作录制下来,再进行简单修改,最后再想办法用python直接运行调用。


顺着这个思路,首先梳理下角锥喇叭的建模步骤,以及对应步骤抽象出来的封装函数(注:建模方法当然不止一种,以下仅为个人习惯方式):


  • 设置变量。个人习惯先把变量建好,这一步操作应抽象并封装成函数:

设置变量(set_variable)——1
  • 建喇叭天线内腔。画两个同尺寸矩形,connect而成立方体;再画两个不同尺寸矩形,connect而成锥台;两者unite而成联合体。这一步操作有:

画一个矩形(create_centered_rectangle)——2
联合两个面(connect)——3
布尔和(unite)——4
  • 建喇叭天线体并设为金属。采用同上方法画一个更大一些的联合体,然后与第一步得到的联合体相减,最后设置材料为PEC,新操作有:

复制(copy_and_paste)——5
布尔差(subtract)——6
设置材料属性(set_material)——7
  • 建空气盒子和辐射边界。操作有:

画一个区域(create_region)——8
设置辐射边界(assign_radiation_region)——9
插入方向图设置(insert_radiation_setup)——10
  • 画端口并设置解算参数。操作有:

画一个端口(assign_port)——11
设置解算参数(insert_analysis_setup)——12
  • 保存工程后仿真并生成报告。操作有:

保存(save_prj)——13
仿真(run)——14
生成仿真报告(create_reports)——15


梳理以后发现,完成一个喇叭建模,竟然大概要写15个函数。。。感觉不知比直接建模要麻烦到哪儿去了,但考虑到这些函数写好了,以后可以被大量复用,相当在于写一个小型API,故是一种先难后易的做法。


对于每一个封装函数的实现,操作步骤为先用HFSS录制脚本,在进行修改。以实现并封装set_variable这个函数为例,首先在HFSS中设一个名为w1,值为20mm的变量,把这段操作录制为py程序,打开后如下所示:

# ----------------------------------------------
# Script Recorded by ANSYS Electronics Desktop Version 2018.0.0
# 10:13:34  十一月 17, 2018
# ----------------------------------------------
import ScriptEnv
ScriptEnv.Initialize("Ansoft.ElectronicsDesktop")
oDesktop.RestoreWindow()
oProject = oDesktop.SetActiveProject("Project2")
oDesign = oProject.SetActiveDesign("HFSSDesign1")
oDesign.ChangeProperty(
  [
     "NAME:AllTabs",
     [
        "NAME:LocalVariableTab",
        [
           "NAME:PropServers",
           "LocalVariables"
        ],
        [
           "NAME:NewProps",
           [
              "NAME:w1",
              "PropType:="      , "VariableProp",
              "UserDef:="       , True,
              "Value:="     , "20mm"
           ]
        ]
     ]
  ])


这段原始代码有以下几点要注意:


一个HFSS专属坑。一定得要先删除注释中的中文和中文符号!!!这点特别坑爹,HFSS不能正确识别任何带中文字符的路径和脚本,连它自己录制的也不行(希望HFSS下个版本能改掉这个万年bug);


引用模块调用无效。import ScriptEnv 在HFSS外部调用时没有意义,因为这不是一个通用的Python包,也就是说想在Python中写这么一段就调用HFSS是不可能的;


格式。格式有点杂乱,相当地不pythonic,这当然是不能忍的,应手动调整一下。


改写后的代码如下所示(将变量名与变量值设成了输入参数):

def set_variable(_var_name, _var_value):
   _NAME = 'NAME:' _var_name
   _VALUE = str(_var_value) 'mm'
   oDesign.ChangeProperty(["NAME:AllTabs",
                           ["NAME:LocalVariableTab",
                            ["NAME:PropServers", "LocalVariables"],
                            ["NAME:NewProps",
                             [_NAME, "PropType:=", "VariableProp", "UserDef:=", True, "Value:=", _VALUE]]]])


改写以后,是否感觉清爽了很多?采用这种思路,将15个函数统统编好,为了调用方便,再整体封装为一个对象HFSS,程序就比较像样了。


最后要强调的就是,通过查阅各种资料,我个人认为在Python中直接打开HFSS最好的方式还是要借助win32com这个模块,其介绍如下:

python模块:win32com用法详解 - 杜雪峰的个人页面 - 开源中国my.oschina.net


综合以上,形成HFSS调用模块,其代码如下(sim.py):

from win32com import client
import os


class HFSS:
   def __init__(self):
       self.oAnsoftApp = client.Dispatch('AnsoftHfss.HfssScriptInterface')
       self.oDesktop = self.oAnsoftApp.GetAppDesktop()
       self.oProject = self.oDesktop.NewProject()
       self.oProject.InsertDesign('HFSS', 'HFSSDesign1', 'DrivenModal1', '')
       self.oDesign = self.oProject.SetActiveDesign("HFSSDesign1")
       self.oEditor = self.oDesign.SetActiveEditor("3D Modeler")
       self.oModule = self.oDesign.GetModule('BoundarySetup')
       self.transparency = 0.5

   def set_variable(self, _var_name, _var_value):
       _NAME = 'NAME:' _var_name
       _VALUE = str(_var_value) 'mm'
       self.oDesign.ChangeProperty(["NAME:AllTabs",
                                    ["NAME:LocalVariableTab",
                                     ["NAME:PropServers", "LocalVariables"],
                                     ["NAME:NewProps",
                                      [_NAME, "PropType:=", "VariableProp", "UserDef:=", True, "Value:=", _VALUE]]]])

   def create_centered_rectangle(self, _var_x, _var_y, _var_z, _name, _dir='Z'):
       self.oEditor.CreateRectangle(
           [
               "NAME:RectangleParameters",
               "IsCovered:=", True,
               "XStart:=", '-' _var_x '/2',
               "YStart:=", '-' _var_y '/2',
               "ZStart:=", _var_z,
               "Width:=", _var_x,
               "Height:=", _var_y,
               "WhichAxis:=", _dir
           ],
           [
               "NAME:Attributes",
               "Name:=", _name,
               "Flags:=", "",
               "Color:=", "(143 175 143)",
               "Transparency:=", 0,
               "PartCoordinateSystem:=", "Global",
               "UDMId:=", "",
               "MaterialValue:=", "\"vacuum\"",
               "SurfaceMaterialValue:=", "\"\"",
               "SolveInside:=", True,
               "IsMaterialEditable:=", True,
               "UseMaterialAppearance:=", False
           ])

   def connect(self, _obj1, _obj2):
       self.oEditor.Connect(["NAME:Selections", "Selections:=", _obj1 ',' _obj2])

   def unite(self, _obj1, _obj2):
       self.oEditor.Unite(["NAME:Selections", "Selections:=", _obj1 ',' _obj2],
                          ["NAME:UniteParameters", "KeepOriginals:=", False])

   def subtract(self, _obj1, _obj2):
       self.oEditor.Subtract(["NAME:Selections", "Blank Parts:=", _obj1, "Tool Parts:=", _obj2],
                             ["NAME:SubtractParameters", "KeepOriginals:=", False])

   def copy_and_paste(self, _obj):
       self.oEditor.Copy(["NAME:Selections", "Selections:=", _obj])
       self.oEditor.Paste()

   def set_material(self, _obj, _mat='pec'):
       self.oEditor.AssignMaterial(
           [
               "NAME:Selections",
               "AllowRegionDependentPartSelectionForPMLCreation:=", True,
               "AllowRegionSelectionForPMLCreation:=", True,
               "Selections:=", _obj
           ],
           [
               "NAME:Attributes",
               "MaterialValue:=", "\"" _mat "\"",
               "SolveInside:=", False,
               "IsMaterialEditable:=", True,
               "UseMaterialAppearance:=", False
           ])

   def assign_port(self, _obj):
       self.oModule.AssignWavePort(["NAME:1", "Objects:=", [_obj],
                                    "NumModes:=", 1, "RenormalizeAllTerminals:=", True,
                                    "UseLineModeAlignment:=", False, "DoDeembed:=", False,
                                    ["NAME:Modes",
                                     ["NAME:Mode1", "ModeNum:=", 1, "UseIntLine:=", False, "CharImp:=", "Zpi"]],
                                    "ShowReporterFilter:=", False,
                                    "ReporterFilter:=", [True],
                                    "UseAnalyticAlignment:=", False])

   def create_region(self, _var_ab):
       self.oEditor.CreateRegion(
           [
               "NAME:RegionParameters",
               " XPaddingType:=", "Absolute Offset",
               " XPadding:=", _var_ab,
               "-XPaddingType:=", "Absolute Offset",
               "-XPadding:=", _var_ab,
               " YPaddingType:=", "Absolute Offset",
               " YPadding:=", _var_ab,
               "-YPaddingType:=", "Absolute Offset",
               "-YPadding:=", _var_ab,
               " ZPaddingType:=", "Absolute Offset",
               " ZPadding:=", _var_ab,
               "-ZPaddingType:=", "Absolute Offset",
               "-ZPadding:=", _var_ab
           ],
           [
               "NAME:Attributes",
               "Name:=", "Region",
               "Flags:=", "Wireframe#",
               "Color:=", "(143 175 143)",
               "Transparency:=", 0,
               "PartCoordinateSystem:=", "Global",
               "UDMId:=", "",
               "MaterialValue:=", "\"vacuum\"",
               "SurfaceMaterialValue:=", "\"\"",
               "SolveInside:=", True,
               "IsMaterialEditable:=", True,
               "UseMaterialAppearance:=", False
           ])

   def assign_radiation_region(self):
       self.oModule.AssignRadiation(
           [
               "NAME:Rad1",
               "Objects:=", ["Region"],
               "IsFssReference:=", False,
               "IsForPML:=", False
           ])

   def insert_radiation_setup(self):
       mod = self.oDesign.GetModule('RadField')
       mod.InsertFarFieldSphereSetup(
           [
               "NAME:Infinite Sphere1",
               "UseCustomRadiationSurface:=", False,
               "ThetaStart:=", "-180deg",
               "ThetaStop:=", "180deg",
               "ThetaStep:=", "1deg",
               "PhiStart:=", "0deg",
               "PhiStop:=", "90deg",
               "PhiStep:=", "90deg",
               "UseLocalCS:=", False
           ])

   def insert_analysis_setup(self, _freq):
       mod = self.oDesign.GetModule('AnalysisSetup')
       mod.InsertSetup("HfssDriven",
       [
           "NAME:Setup1",
           "AdaptMultipleFreqs:=", False,
           "Frequency:=", str(_freq) 'GHz',
           "MaxDeltaS:=", 0.02,
           "PortsOnly:=", False,
           "UseMatrixConv:=", False,
           "MaximumPasses:=", 20,
           "MinimumPasses:=", 1,
           "MinimumConvergedPasses:=", 1,
           "PercentRefinement:=", 30,
           "IsEnabled:=", True,
           "BasisOrder:=", 1,
           "DoLambdaRefine:=", True,
           "DoMaterialLambda:=", True,
           "SetLambdaTarget:=", False,
           "Target:=", 0.3333,
           "UseMaxTetIncrease:=", False,
           "PortAccuracy:=", 2,
           "UseABCOnPort:=", False,
           "SetPortMinMaxTri:=", False,
           "UseDomains:=", False,
           "UseIterativeSolver:=", False,
           "SaveRadFieldsOnly:=", False,
           "SaveAnyFields:=", True,
           "IESolverType:=", "Auto",
           "LambdaTargetForIESolver:=", 0.15,
           "UseDefaultLambdaTgtForIESolver:=", True
       ])

   def create_reports(self):
       mod = self.oDesign.GetModule('ReportSetup')
       mod.CreateReport("VSWR Plot 1", "Modal Solution Data", "Rectangular Plot", "Setup1 : LastAdaptive", [],
                        ["Freq:=", ["All"], ],
                        ["X Component:=", "Freq",
                         "Y Component:=", ["VSWR(1)"]], [])

       mod.CreateReport("Realized Gain Plot 1", "Far Fields", "Rectangular Plot", "Setup1 : LastAdaptive",
                        ["Context:=", "Infinite Sphere1"],
                        [
                           "Theta:=", ["All"],
                           "Phi:=", ["All"],
                           "Freq:=", ["All"],
                        ],
                        [
                           "X Component:=", "Theta",
                           "Y Component:=", ["dB(RealizedGainTotal)"]
                        ], [])
       mod.AddTraceCharacteristics("Realized Gain Plot 1", "max", [], ["Full"])
       mod.AddTraceCharacteristics("Realized Gain Plot 1", "xdb10Beamwidth", ["3"], ["Full"])

   def save_prj(self):
       _base_path = os.getcwd()
       _prj_num = 1
       while True:
           _path = os.path.join(_base_path, 'Prj{}.aedt'.format(_prj_num))
           if os.path.exists(_path):
               _prj_num = 1
           else:
               break
       self.oProject.SaveAs(_path, True)

   def run(self):
       self.oDesktop.RestoreWindow()
       self.oDesign.Analyze('Setup1')


调用方法是首先实例化HFSS对象,再使用其方法,就可以顺利调用HFSS啦,例如:

if __name__ == '__main__':
   h = HFSS()

   # 设置变量
   h.set_variable('wg_a', 22.86)
   h.set_variable('wg_b', 10.16)

   # 画个矩形
   h.create_centered_rectangle('wg_a', 'wg_b', 0, 'wg_in')

   # 保存一下
   h.save_prj()


要注意的是,save_prj函数会将新建立的工程保存在与程序同样目录中,这个目录的路径照例不可以有任何中文符号,否则HFSS保存出错,至于文件名,save_prj函数中已作了简单的判重处理,不用担心覆盖,当然,有特别需求的请自行修改。


小结

本部分是相对比较麻烦的部分,相当于写了一个HFSS与Python之间的通讯接口,然而后续将会看到,有了这部分基础,主调程序才可以用相对清晰的逻辑来操作HFSS建模。


本篇介绍即到此结束,下一部分将会讲到主调用模块及GUI模块,谢谢各位观看(*^_^*)!


Peace!


云计算理论科普HFSS
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2020-10-26
最近编辑:4年前
仿真圈
技术圈粉 知识付费 学习强国
获赞 10050粉丝 21522文章 3526课程 218
点赞
收藏
未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习 福利任务 兑换礼品
下载APP
联系我们
帮助与反馈