├── .gitignore ├── examples ├── cup.zip ├── inter.zip ├── patch.zip ├── dishal.pdf ├── cup.sh ├── patch.sh ├── inter.sh ├── patch.py ├── cup.py ├── scadtool.py ├── showresult.py └── inter.py ├── res ├── cup-model.png ├── cup-pattern.png ├── filter-model.png ├── patch-model.png ├── filter-sparam.png ├── patch-pattern.png └── readme.py ├── openems_apple_silicon.patch ├── README.md └── rfems.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.npz 2 | -------------------------------------------------------------------------------- /examples/cup.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/examples/cup.zip -------------------------------------------------------------------------------- /examples/inter.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/examples/inter.zip -------------------------------------------------------------------------------- /examples/patch.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/examples/patch.zip -------------------------------------------------------------------------------- /res/cup-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/cup-model.png -------------------------------------------------------------------------------- /examples/dishal.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/examples/dishal.pdf -------------------------------------------------------------------------------- /res/cup-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/cup-pattern.png -------------------------------------------------------------------------------- /res/filter-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/filter-model.png -------------------------------------------------------------------------------- /res/patch-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/patch-model.png -------------------------------------------------------------------------------- /res/filter-sparam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/filter-sparam.png -------------------------------------------------------------------------------- /res/patch-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseengineering/rfems/HEAD/res/patch-pattern.png -------------------------------------------------------------------------------- /examples/cup.sh: -------------------------------------------------------------------------------- 1 | python examples/cup.py 2 | python rfems.py examples/cup --pitch .005 --frequency 1.3e9 --threads $(nproc) --farfield 3 | python examples/showresult.py examples/cup 4 | -------------------------------------------------------------------------------- /examples/patch.sh: -------------------------------------------------------------------------------- 1 | python examples/patch.py 2 | python rfems.py examples/patch --pitch .005 --frequency 2e9 --criteria -40 --threads $(nproc) --farfield $@ 3 | python examples/showresult.py examples/patch 4 | -------------------------------------------------------------------------------- /examples/inter.sh: -------------------------------------------------------------------------------- 1 | 2 | python examples/inter.py \ 3 | --rod 0.0006875 -0.001875 0.0006875 \ 4 | --sep 0.0008125 0.0008125 \ 5 | --tap 0.00492334 0.00492334 \ 6 | --qe1 11.7818 11.7818 \ 7 | --kij 0.0600168 0.0600168 \ 8 | --freq 1.296e+09 --a 1 --b 3 9 | python rfems.py examples/inter --freq 1.296e+09 --span 4.4e+08 --line 50 --threads 6 --pitch 0.001 $@ 10 | python examples/showresult.py examples/inter 11 | -------------------------------------------------------------------------------- /examples/patch.py: -------------------------------------------------------------------------------- 1 | 2 | from openEMS.physical_constants import * 3 | import sys 4 | from solid2 import * 5 | from scadtool import openzip 6 | 7 | def main(): 8 | eps = 1e-3 9 | substrate_x = 60 10 | substrate_y = 60 11 | substrate_thickness = 1.524 12 | patch_x = 32 13 | patch_y = 40 14 | patch_z = .035 15 | feed_x = -6 16 | substrate_epsR = 3.38 17 | substrate_kappa = 1e-3 * 2 * np.pi * 2.45e9 * EPS0 * substrate_epsR 18 | 19 | with openzip(sys.argv[0]) as zf: 20 | # air 21 | size = [200, 200, 200] 22 | solid = cube(size, center=True) 23 | zf.save('sim_box-air', solid) 24 | 25 | # patch 26 | size = [ patch_x, patch_y, eps ] 27 | solid = cube(size, center=True).translateZ(substrate_thickness / 2) 28 | zf.save('patch-pec priority=10', solid) 29 | 30 | # ground 31 | size = [ substrate_x, substrate_y, eps ] 32 | solid = cube(size, center=True).translateZ(-substrate_thickness / 2) 33 | zf.save('ground-pec priority=10', solid) 34 | 35 | # substrate 36 | size = [ substrate_x, substrate_y, substrate_thickness ] 37 | solid = cube(size, center=True) 38 | zf.save(f'substrate-epsilon={substrate_epsR:g} kappa={substrate_kappa:g}', solid) 39 | 40 | # port 41 | size = [ eps, eps, substrate_thickness ] 42 | solid = cube(size, center=True).translateX(feed_x) 43 | zf.save(f'port z 1', solid) 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /openems_apple_silicon.patch: -------------------------------------------------------------------------------- 1 | # patch for compiling openems with python support on apple silicon 2 | # also disables the installation of python libraries in --user space 3 | diff -r -u0 openEMS-Project.orig/CSXCAD/src/CSXCAD_Global.h openEMS-Project/CSXCAD/src/CSXCAD_Global.h 4 | --- openEMS-Project.orig/CSXCAD/src/CSXCAD_Global.h 2023-01-15 11:20:13 5 | +++ openEMS-Project/CSXCAD/src/CSXCAD_Global.h 2023-01-15 11:20:13 6 | @@ -35,0 +36 @@ 7 | +#include 8 | diff -r -u0 openEMS-Project.orig/fparser/CMakeLists.txt openEMS-Project/fparser/CMakeLists.txt 9 | --- openEMS-Project.orig/fparser/CMakeLists.txt 2023-01-15 11:20:13 10 | +++ openEMS-Project/fparser/CMakeLists.txt 2023-01-15 11:20:13 11 | @@ -7,0 +8 @@ 12 | +set(CMAKE_INSTALL_NAME_DIR ${CMAKE_INSTALL_PREFIX}/lib) 13 | diff -r -u0 openEMS-Project.orig/openEMS/FDTD/excitation.cpp openEMS-Project/openEMS/FDTD/excitation.cpp 14 | --- openEMS-Project.orig/openEMS/FDTD/excitation.cpp 2023-01-15 11:20:13 15 | +++ openEMS-Project/openEMS/FDTD/excitation.cpp 2023-01-15 11:20:13 16 | @@ -23,0 +24 @@ 17 | +#include 18 | diff -r -u0 openEMS-Project.orig/openEMS/nf2ff/nf2ff.cpp openEMS-Project/openEMS/nf2ff/nf2ff.cpp 19 | --- openEMS-Project.orig/openEMS/nf2ff/nf2ff.cpp 2023-01-15 11:20:13 20 | +++ openEMS-Project/openEMS/nf2ff/nf2ff.cpp 2023-01-15 11:20:13 21 | @@ -32,0 +33 @@ 22 | +#include 23 | diff -r -u0 openEMS-Project.orig/update_openEMS.sh openEMS-Project/update_openEMS.sh 24 | --- openEMS-Project.orig/update_openEMS.sh 2023-01-15 11:20:13 25 | +++ openEMS-Project/update_openEMS.sh 2023-01-15 11:20:13 26 | @@ -154 +154 @@ 27 | - PY_INST_USER='--user' 28 | + PY_INST_USER='' 29 | -------------------------------------------------------------------------------- /examples/cup.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | from solid2 import * 4 | from scadtool import openzip 5 | 6 | def main(): 7 | fo = 1296e6 8 | wavelen = 3e8 / fo * 1000 9 | 10 | diam = 2 11 | dipole_length = 0.9 * wavelen / 2 12 | cup_height = 0.5 * wavelen # 0.5 to 0.6 13 | cup_diameter = 1.1 * wavelen # 1.1 to 1.4 14 | cup_thickness = 8 15 | cup_bottom = -wavelen / 4 16 | sim_side = 4.2e11 / fo + cup_diameter 17 | 18 | with openzip(sys.argv[0]) as zf: 19 | # air 20 | size = [sim_side] * 3 21 | solid = cube(size, center=True) 22 | zf.save('sim_box-air', solid) 23 | 24 | # dipole 25 | size = [ dipole_length, diam, diam ] 26 | solid = cube(size, center=True) 27 | zf.save('dipole-copper', solid) 28 | 29 | # cup rim 30 | inner = cylinder(cup_diameter/2, h=cup_height) \ 31 | .translateZ(cup_bottom) 32 | outer = cylinder(cup_diameter/2 + cup_thickness, h=cup_height) \ 33 | .translateZ(cup_bottom) 34 | zf.save('cup rim-aluminum', outer - inner) 35 | 36 | # cup bottom 37 | solid = cylinder(cup_diameter/2 + cup_thickness, h=cup_thickness) \ 38 | .translateZ(cup_bottom - cup_thickness) 39 | zf.save('cup bottom-aluminum', solid) 40 | 41 | # port 42 | size = [ diam ] * 3 43 | solid = cube(size, center=True) 44 | zf.save(f'port x 1', solid) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/scadtool.py: -------------------------------------------------------------------------------- 1 | 2 | import os, zipfile, tempfile, subprocess 3 | 4 | def render_stl(name, d): 5 | buf = None 6 | dirname = os.getcwd() 7 | with tempfile.TemporaryDirectory() as tmpdirname: 8 | os.chdir(tmpdirname) 9 | scad_filename = 'model.scad' 10 | stl_filename = 'model.stl' 11 | d.save_as_scad(scad_filename) 12 | cmd = [ 'openscad', '--export', 'asciistl', '-o', stl_filename, scad_filename ] 13 | res = subprocess.run(cmd, capture_output=True) 14 | if 'model.stl' in os.listdir(): 15 | with open(stl_filename, 'rb') as f: 16 | buf = f.read() 17 | if not buf: 18 | raise ValueError(f'Bad stl file: {name}') 19 | os.chdir(dirname) 20 | return buf 21 | 22 | 23 | class openzip(object): 24 | def __init__(self, filename): 25 | self.filename = filename 26 | self.manifest = [] 27 | 28 | def save(self, filename, solid): 29 | self.manifest.append((filename, solid)) 30 | 31 | def __enter__(self): 32 | return self 33 | 34 | def __exit__(self, *args): 35 | filename = self.filename 36 | root, ext = os.path.splitext(filename) 37 | if ext != '.zip': 38 | filename = f'{root}.zip' 39 | with zipfile.ZipFile(filename, mode='w') as zf: 40 | for name, solid in self.manifest: 41 | buf = render_stl(name, solid) 42 | name = f'{name}.stl' 43 | if name in zf.namelist(): 44 | print(f'already exists in archive: {name}') 45 | else: 46 | zinfo = zipfile.ZipInfo(name) 47 | zf.writestr(zinfo, buf, compress_type=zipfile.ZIP_DEFLATED) 48 | 49 | def save(self, filename, solid): 50 | self.manifest.append([ filename, solid ]) 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/showresult.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, os 3 | from pylab import * 4 | 5 | #* theta : theta angles 6 | #* phi : phi angles 7 | #* r : radius 8 | #* freq : frequencies 9 | #* Dmax : directivity over frequency 10 | #* Prad : total radiated power over frequency 11 | #* E_theta : theta component of electric field over frequency/theta/phi 12 | #* E_phi : phi component of electric field over frequency/theta/phi 13 | #* E_norm : abs component of electric field over frequency/theta/phi 14 | #* E_cprh : theta component of electric field over frequency/theta/phi 15 | #* E_cplh : theta component of electric field over frequency/theta/phi 16 | #* P_rad : radiated power (S) over frequency/theta/phi 17 | 18 | class loadz(object): 19 | def __init__(self, filename): 20 | root, ext = os.path.splitext(filename) 21 | if ext != f'.npz': 22 | filename = f'{root}.npz' 23 | with np.load(filename) as my_dict: 24 | for key in my_dict: 25 | setattr(self, key, my_dict[key]) 26 | 27 | 28 | def plot_sparameters(f, s): 29 | figure() 30 | nport = s.shape[1] 31 | for n in range(nport): 32 | if np.all(s[:,:,n] == 0): continue 33 | sdb = 20 * np.log10(abs(s[:,:,n])) 34 | plot(f/1e9, sdb[:,0], 'k-', linewidth=2, label=f'$S_{{1{n+1}}}$') 35 | if nport > 1: 36 | plot(f/1e9, sdb[:,1], 'r--', linewidth=2, label=f'$S_{{2{n+1}}}$') 37 | grid() 38 | legend() 39 | ylabel('S-Parameter (dB)') 40 | xlabel('frequency (GHz)') 41 | 42 | 43 | def plot_pattern(phi, theta, E, Dmax, name): 44 | phi = phi * 180 / np.pi 45 | theta = theta * 180 / np.pi 46 | xz = np.where(isclose(phi, 0))[0] 47 | yz = np.where(isclose(phi, 90))[0] 48 | E = 20.0 * np.log10(abs(E) / np.max(abs(E)) * Dmax) 49 | figure() 50 | plot(theta, np.squeeze(E[:,xz]), 'k-', linewidth=2, label='xz-plane') 51 | plot(theta, np.squeeze(E[:,yz]), 'r--', linewidth=2, label='yz-plane') 52 | grid() 53 | ylabel('Directivity (dBi)') 54 | xlabel('Theta (deg)') 55 | title(name) 56 | legend() 57 | 58 | 59 | def main(filename): 60 | res = loadz(filename) 61 | 62 | f = res.f 63 | s = res.s 64 | z = res.z 65 | plot_sparameters(f, s) 66 | 67 | if hasattr(res, 'Dmax'): 68 | phi = res.phi 69 | theta = res.theta 70 | freq = res.freq[0] 71 | Dmax = res.Dmax[0] 72 | E_norm = res.E_norm[0] 73 | E_theta = res.E_theta[0] 74 | E_phi = res.E_phi[0] 75 | 76 | # Calculate co- and cross-pol, Modern Antenna Design, p.22 77 | E_co = E_theta * np.cos(phi) - E_phi * np.sin(phi) 78 | E_cx = E_theta * np.sin(phi) + E_phi * np.cos(phi) 79 | 80 | name = f'{freq/1e6:.3f} MHz' 81 | plot_pattern(phi=phi, theta=theta, E=E_norm, Dmax=Dmax, name=f'E norm at {name}') 82 | plot_pattern(phi=phi, theta=theta, E=E_cx, Dmax=Dmax, name=f'E cross-pol at {name}') 83 | 84 | show() 85 | 86 | 87 | if __name__ == "__main__": 88 | main(sys.argv[1]) 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/inter.py: -------------------------------------------------------------------------------- 1 | 2 | # See Fisher cavity filter in the ARRL handbook 3 | # Also see Dishal's paper from 1965 on interdigital filters 4 | # for calculating coupling distances, and included as a PDF. 5 | 6 | import sys, argparse 7 | import numpy as np 8 | from solid2 import * 9 | from scadtool import openzip 10 | 11 | def parse_args(): 12 | formatter_class = argparse.ArgumentDefaultsHelpFormatter 13 | parser = argparse.ArgumentParser(formatter_class=formatter_class) 14 | parser.add_argument('--a', type=int, default=0, help='rod to resonate') 15 | parser.add_argument('--b', type=int, default=0, help='second rod to resonate') 16 | parser.add_argument('--rod', type=float, nargs='*', help='rod length (mm)') 17 | parser.add_argument('--sep', type=float, nargs='*', help='rod separation (mm)') 18 | parser.add_argument('--tap', type=float, nargs='*', help='tap position (mm)') 19 | parser.add_argument('--short', type=int, help='short resonantors') 20 | parser.add_argument('--kij', type=float, nargs='*', help='denormalized kij') 21 | parser.add_argument('--qe1', type=float, nargs='*', help='denormalized qe1') 22 | parser.add_argument('--frequency', type=float, default=1296e6, help='center frequency (Hz)') 23 | parser.add_argument('--notap', action='store_true', help='do not tap resonantors') 24 | parser.add_argument('--deembed', action='store_true', help='deembed filter') 25 | return parser.parse_args() 26 | 27 | 28 | ###################################### 29 | 30 | 31 | def sign(value): 32 | return 2 * bool(value) - 1 33 | 34 | 35 | def main(): 36 | output = sys.argv[0] 37 | frequency = args.frequency 38 | rod = np.array(args.rod) * 1000 # mm 39 | sep = np.array(args.sep) * 1000 # mm 40 | tap = np.array(args.tap) * 1000 # mm 41 | 42 | eps = 1e-3 # infinitely thin 43 | mm = 25.4 44 | thick = 2 # thickness of aluminum enclosure 45 | screw = .112 * mm # (inches) 4-40 screw 46 | diam = .25 * mm 47 | boxh = .75 * mm # short edge 48 | boxl = 3e8 / frequency / 4 * 1000 # mm 49 | endsep = boxh if args.deembed else .7 * boxh # for <2% effect (see dishal) 50 | zo = 138 * np.log10(1.25 * boxh / diam) # rod impedance 51 | 52 | minspace = 1 if frequency > 800e6 else 2 53 | base_rod = boxl * (.9 if frequency > 800e6 else .95) 54 | 55 | base_tap = 0 56 | if args.qe1: 57 | qe1 = np.array(args.qe1) 58 | base_tap = 2 * boxl / np.pi * np.arcsin(np.sqrt(50 * np.pi / (4 * qe1 * zo))) 59 | 60 | base_sep = 0 61 | if args.kij: 62 | kij = np.array(args.kij) 63 | base_sep = (.91 * diam / boxh - np.log10(kij) - 0.048) * boxh / 1.37 64 | 65 | rod = rod + base_rod 66 | sep = sep + base_sep 67 | tap = tap + base_tap 68 | 69 | ################### 70 | 71 | rodpos = np.cumsum(np.hstack((0, sep))) - np.sum(sep) / 2 72 | boxw = np.sum(sep) + 2 * endsep 73 | 74 | thick = np.array(thick) 75 | cavity = np.array([ boxw, boxh, boxl ]) 76 | 77 | with openzip(sys.argv[0]) as zf: 78 | 79 | ######################### 80 | # enclosure 81 | ######################### 82 | 83 | # tops 84 | solid = (cube(cavity + 2 * thick, center=True) - 85 | cube(cavity + [ 2, 2, 0 ] * thick, center=True)) 86 | zf.save('box tops-aluminum priority=10', solid) 87 | 88 | # edges 89 | solid = (cube(cavity + 2 * thick, center=True) - 90 | cube(cavity + [ 0, 2, 2 ] * thick, center=True)) 91 | zf.save('box edges-aluminum priority=10', solid) 92 | 93 | # lids 94 | solid = (cube(cavity + 2 * thick, center=True) - 95 | cube(cavity + [ 2, 0, 2 ] * thick, center=True)) 96 | zf.save('box lids-aluminum priority=10', solid) 97 | 98 | ######################### 99 | # rods 100 | ######################### 101 | 102 | portnum = 1 103 | w = cavity[0] / 2 104 | z = cavity[2] / 2 105 | rw = 1 # wire diameter 106 | 107 | for i in range(len(rod)): 108 | screw_length = 2 * z - rod[i] - minspace 109 | 110 | solid = (cylinder(diam / 2, rod[i], center=True) 111 | .translateX(rodpos[i]) 112 | .translateZ(sign(i % 2) * (z - rod[i] / 2))) 113 | zf.save(f'rods{i+1}-aluminum priority=10', solid) 114 | 115 | if i == args.a - 1 or i == args.b - 1: 116 | if not args.notap and (i == 0 or i == len(rod) - 1): 117 | h = sign(i % 2) * (z - tap[1 if i else 0]) 118 | 119 | # tap wire 120 | wire_length = w - sign(i) * rodpos[i] 121 | size = [ wire_length, rw, rw ] 122 | solid = (cube(size, center=True) 123 | .translateX(sign(i) * (w - wire_length / 2)) 124 | .translateZ(h)) 125 | zf.save(f'tap{portnum}-copper priority=5', solid) 126 | 127 | # tap port 128 | size = [ rw, rw, rw ] 129 | solid = (cube(size, center=True) 130 | .translateX(sign(i) * (w - rw / 2)) 131 | .translateZ(h)) 132 | zf.save(f'port x {portnum}', solid) 133 | else: 134 | # probe port 135 | size = [ screw, eps, minspace ] 136 | solid = (cube(size, center=True) 137 | .translateX(rodpos[i]) 138 | .translateZ(sign(i % 2) * (z - rod[i] - minspace / 2))) 139 | zf.save(f'port z {portnum}', solid) 140 | 141 | portnum += 1 142 | 143 | elif args.short is not None and i >= args.short: 144 | screw_length = 2 * z - rod[i] 145 | 146 | solid = (cylinder(screw/2, screw_length, center=True) 147 | .translateX(rodpos[i]) 148 | .translateZ(sign(i % 2) * (screw_length / 2 - z))) 149 | zf.save(f'screw{i+1}-aluminum priority=10', solid) 150 | 151 | 152 | if __name__ == "__main__": 153 | args = parse_args() 154 | main() 155 | 156 | 157 | -------------------------------------------------------------------------------- /res/readme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, subprocess 4 | 5 | def run(command): 6 | result = subprocess.run(command, shell=True, capture_output=True, text=True) 7 | return f'{command}\n{result.stdout.rstrip()}' 8 | 9 | 10 | print(f""" 11 | 12 | ![](res/cup-model.png) 13 | ![](res/cup-pattern.png) 14 | 15 | # Rfems 16 | 17 | This script lets you run an openEMS FDTD simulation 18 | from a collection of STL 3D models. To use the script, first you need to 19 | have openEMS installed as well as its Python API extension. You 20 | will also need a method to generate the STL files that will compose your FDTD model. 21 | The example models in this repo use Python and Openscad to do this. But you can use whatever you want. 22 | 23 | ## How To Simulation Using Rfems 24 | 25 | To create a simulation, generate a STL for every material or solid that you want 26 | to be modeled. The name of the STL indicates the type of material that composes 27 | that solid. So a file named 'aluminum.stl' will be considered to model a aluminum 28 | part. Rfems has preset material names of silver, copper, gold, aluminum, brass, 29 | steel, pec, port, and air. Unknown material names are considered PEC. 30 | Material names must not have spaces in them. 31 | 32 | Multiple STL models using the same material 33 | can be differentiated using labels: prefix the material with a label name and 34 | then a dash, for example 'tee-aluminum.stl'. Label names can have spaces in their names. 35 | 36 | In fact it is usually much easier to 37 | create separate STL models for each component of the same material, like using 38 | one STL model for each resonator in a cavity filter. Sometimes it is 39 | even neccessary because of the limitation in openEMS. For example a enclosure 40 | in openEMS must be have its lid, edges, and tops each in a separate file. For a cup antenna the sides 41 | of cup must be in a separate STL file than the bottom of the cup. Use the option --dump to check. 42 | 43 | Anything after this label-material combination is considered a 'material variable'. 44 | Material variables are key-value pairs separated by an equal sign. 45 | 46 | For example to create material with specific values for conductance and dielectic permittivity, 47 | use the material variables kappa and epsilon. For example, if your material has a conductance of 44.3e6 48 | name your file 'custom kappa=44.3e6.stl'. 49 | 50 | The port material is a special case. Its STL model creates a lumped port in 51 | openEMS. The format of the port material is 'port {{polarity}} {{port number}}' where 52 | polarity is either x, y, or z and port number starts from 1. 53 | The lumped port's impedance defaults to the value provided 54 | through the --line option. Or you can set it directly using the 'zo' material 55 | variable. For example for a lumped port 1 with an impedance of 75 ohms use 'port x 1 zo=75'. 56 | The bounding box of the STL model will be used as the lumped port's dimensions. 57 | 58 | To set the priority of the material use the material variable 'priority'. For example 59 | to set the priority of an aluminum STL model to 10 use, 'aluminum priority=10'. The 60 | default priority for materials is 0, the lowest priority. A conductive material 61 | lying on top of a substrate material should have a higher priority set, see patch.py. 62 | 63 | All these models must be then zipped up into a single zip file. This zip file is 64 | presented to rfems as the complete model to simulate. To view the complete model use the --show option. 65 | 66 | ## S-parameter Support 67 | 68 | After each simulation the s-parameter result is written out to a .npz numpy formatted 69 | data file. The s parameters are in the 's' variable, the frequency points are in 70 | the 'f' variable. The 'z' variable is an array of each port's characteristic impedance. 71 | 72 | ## Antenna Far Field Support 73 | 74 | Rfems supports the generation of far field radiation patterns using the option 75 | --farfield. If the option --nominimum is NOT used, the far field pattern generated 76 | is only for the frequency point of minimum VSWR. Otherwise rfems computes the 77 | far field patterns for all the simulation points. (The rfems default of 1000 frequency points 78 | might take a while, so you probably want to change it.) 79 | The radiation pattern is included in the .npz file output. 80 | See the showresults.py file in the examples directory for the list of variables written 81 | out (these are also the same variables generated by openEMS). 82 | 83 | When enabling farfield, the boundary surrounding your model will switch from PEC 84 | to MUR. To put space between your antenna model and this MUR boundary create a STL model 85 | and name it using the material name air. See the examples, cup.py and patch.py for 86 | examples of how this works. 87 | 88 | ## Dependencies 89 | 90 | To run rfems: 91 | 92 | 1. openEMS binary libraries ($ apt-get install openems) 93 | 2. openEMS python libraries ($ apt-get install python3-openems) 94 | 3. numpy ($ pip install numpy) 95 | 96 | To run the examples in the repo (optional): 97 | 98 | 1. solidpython2 ($ pip install solidpython2) 99 | 2. matplotlib ($ pip install matplotlib) 100 | 3. openscad ($ apt-get install openscad) 101 | 102 | ## Example 103 | 104 | ``` 105 | $ python examples/patch.py 106 | $ { run('unzip -l examples/patch.zip') } 107 | $ python rfems.py examples/patch.zip --pitch .005 --frequency 2e9 --criteria -40 --threads $(nproc) --farfield 108 | $ python examples/showresult.py examples/patch.npz 109 | ``` 110 | ![](res/patch-model.png) 111 | ![](res/patch-pattern.png) 112 | 113 | ``` 114 | $ python examples/inter.py \\ 115 | --rod 0.0006875 -0.001875 0.0006875 \\ 116 | --sep 0.0008125 0.0008125 \\ 117 | --tap 0.00492334 0.00492334 \\ 118 | --qe1 11.7818 11.7818 \\ 119 | --kij 0.0600168 0.0600168 \\ 120 | --freq 1.296e+09 --a 1 --b 3 121 | $ { run('unzip -l examples/inter.zip') } 122 | $ python rfems.py examples/inter.zip --freq 1.296e+09 --span 4.4e+08 --line 50 --threads 6 --pitch 0.001 123 | $ python examples/showresult.py examples/inter.npz 124 | ``` 125 | ![](res/filter-model.png) 126 | ![](res/filter-sparam.png) 127 | 128 | ## Usage 129 | 130 | ``` 131 | $ { run("python rfems.py --help") } 132 | ``` 133 | 134 | ## Notes 135 | 136 | Openscad cannot create STL models of planar surfaces. As a work around, use a very small value for the flat dimension instead of zero. Rfems flattens all STL files with bounding box dimensions less than or equal to 1e-6 m (or 1e-3 in STL units) to their planar 2D and 1D box equivalent. See patch.py. STL files are considered to use millimeter units. 137 | """) 138 | 139 | 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![](res/cup-model.png) 4 | ![](res/cup-pattern.png) 5 | 6 | # Rfems 7 | 8 | This script lets you run an openEMS FDTD simulation 9 | from a collection of STL 3D models. To use the script, first you need to 10 | have openEMS installed as well as its Python API extension. You 11 | will also need a method to generate the STL files that will compose your FDTD model. 12 | The example models in this repo use Python and Openscad to do this. But you can use whatever you want. 13 | 14 | ## How To Simulation Using Rfems 15 | 16 | To create a simulation, generate a STL for every material or solid that you want 17 | to be modeled. The name of the STL indicates the type of material that composes 18 | that solid. So a file named 'aluminum.stl' will be considered to model a aluminum 19 | part. Rfems has preset material names of silver, copper, gold, aluminum, brass, 20 | steel, pec, port, and air. Unknown material names are considered PEC. 21 | Material names must not have spaces in them. 22 | 23 | Multiple STL models using the same material 24 | can be differentiated using labels: prefix the material with a label name and 25 | then a dash, for example 'tee-aluminum.stl'. Label names can have spaces in their names. 26 | 27 | In fact it is usually much easier to 28 | create separate STL models for each component of the same material, like using 29 | one STL model for each resonator in a cavity filter. Sometimes it is 30 | even neccessary because of the limitation in openEMS. For example a enclosure 31 | in openEMS must be have its lid, edges, and tops each in a separate file. For a cup antenna the sides 32 | of cup must be in a separate STL file than the bottom of the cup. Use the option --dump to check. 33 | 34 | Anything after this label-material combination is considered a 'material variable'. 35 | Material variables are key-value pairs separated by an equal sign. 36 | 37 | For example to create material with specific values for conductance and dielectic permittivity, 38 | use the material variables kappa and epsilon. For example, if your material has a conductance of 44.3e6 39 | name your file 'custom kappa=44.3e6.stl'. 40 | 41 | The port material is a special case. Its STL model creates a lumped port in 42 | openEMS. The format of the port material is 'port {polarity} {port number}' where 43 | polarity is either x, y, or z and port number starts from 1. 44 | The lumped port's impedance defaults to the value provided 45 | through the --line option. Or you can set it directly using the 'zo' material 46 | variable. For example for a lumped port 1 with an impedance of 75 ohms use 'port x 1 zo=75'. 47 | The bounding box of the STL model will be used as the lumped port's dimensions. 48 | 49 | To set the priority of the material use the material variable 'priority'. For example 50 | to set the priority of an aluminum STL model to 10 use, 'aluminum priority=10'. The 51 | default priority for materials is 0, the lowest priority. A conductive material 52 | lying on top of a substrate material should have a higher priority set, see patch.py. 53 | 54 | All these models must be then zipped up into a single zip file. This zip file is 55 | presented to rfems as the complete model to simulate. To view the complete model use the --show option. 56 | 57 | ## S-parameter Support 58 | 59 | After each simulation the s-parameter result is written out to a .npz numpy formatted 60 | data file. The s parameters are in the 's' variable, the frequency points are in 61 | the 'f' variable. The 'z' variable is an array of each port's characteristic impedance. 62 | 63 | ## Antenna Far Field Support 64 | 65 | Rfems supports the generation of far field radiation patterns using the option 66 | --farfield. If the option --nominimum is NOT used, the far field pattern generated 67 | is only for the frequency point of minimum VSWR. Otherwise rfems computes the 68 | far field patterns for all the simulation points. (The rfems default of 1000 frequency points 69 | might take a while, so you probably want to change it.) 70 | The radiation pattern is included in the .npz file output. 71 | See the showresults.py file in the examples directory for the list of variables written 72 | out (these are also the same variables generated by openEMS). 73 | 74 | When enabling farfield, the boundary surrounding your model will switch from PEC 75 | to MUR. To put space between your antenna model and this MUR boundary create a STL model 76 | and name it using the material name air. See the examples, cup.py and patch.py for 77 | examples of how this works. 78 | 79 | ## Dependencies 80 | 81 | To run rfems: 82 | 83 | 1. openEMS binary libraries ($ apt-get install openems) 84 | 2. openEMS python libraries ($ apt-get install python3-openems) 85 | 3. numpy ($ pip install numpy) 86 | 87 | To run the examples in the repo (optional): 88 | 89 | 1. solidpython2 ($ pip install solidpython2) 90 | 2. matplotlib ($ pip install matplotlib) 91 | 3. openscad ($ apt-get install openscad) 92 | 93 | ## Example 94 | 95 | ``` 96 | $ python examples/patch.py 97 | $ unzip -l examples/patch.zip 98 | Archive: examples/patch.zip 99 | Length Date Time Name 100 | --------- ---------- ----- ---- 101 | 1719 01-01-1980 00:00 sim_box-air.stl 102 | 1737 01-01-1980 00:00 patch-pec priority=10.stl 103 | 1773 01-01-1980 00:00 ground-pec priority=10.stl 104 | 1719 01-01-1980 00:00 substrate-epsilon=3.38 kappa=0.000460693.stl 105 | 2025 01-01-1980 00:00 port z 1.stl 106 | --------- ------- 107 | 8973 5 files 108 | $ python rfems.py examples/patch.zip --pitch .005 --frequency 2e9 --criteria -40 --threads $(nproc) --farfield 109 | $ python examples/showresult.py examples/patch.npz 110 | ``` 111 | ![](res/patch-model.png) 112 | ![](res/patch-pattern.png) 113 | 114 | ``` 115 | $ python examples/inter.py \ 116 | --rod 0.0006875 -0.001875 0.0006875 \ 117 | --sep 0.0008125 0.0008125 \ 118 | --tap 0.00492334 0.00492334 \ 119 | --qe1 11.7818 11.7818 \ 120 | --kij 0.0600168 0.0600168 \ 121 | --freq 1.296e+09 --a 1 --b 3 122 | $ unzip -l examples/inter.zip 123 | Archive: examples/inter.zip 124 | Length Date Time Name 125 | --------- ---------- ----- ---- 126 | 4185 01-01-1980 00:00 box tops-aluminum priority=10.stl 127 | 4185 01-01-1980 00:00 box edges-aluminum priority=10.stl 128 | 4149 01-01-1980 00:00 box lids-aluminum priority=10.stl 129 | 6563 01-01-1980 00:00 rods1-aluminum priority=10.stl 130 | 2043 01-01-1980 00:00 tap1-copper priority=5.stl 131 | 2043 01-01-1980 00:00 port x 1.stl 132 | 2973 01-01-1980 00:00 screw1-aluminum priority=10.stl 133 | 6533 01-01-1980 00:00 rods2-aluminum priority=10.stl 134 | 3005 01-01-1980 00:00 screw2-aluminum priority=10.stl 135 | 6455 01-01-1980 00:00 rods3-aluminum priority=10.stl 136 | 2007 01-01-1980 00:00 tap2-copper priority=5.stl 137 | 2007 01-01-1980 00:00 port x 2.stl 138 | 2925 01-01-1980 00:00 screw3-aluminum priority=10.stl 139 | --------- ------- 140 | 49073 13 files 141 | $ python rfems.py examples/inter.zip --freq 1.296e+09 --span 4.4e+08 --line 50 --threads 6 --pitch 0.001 142 | $ python examples/showresult.py examples/inter.npz 143 | ``` 144 | ![](res/filter-model.png) 145 | ![](res/filter-sparam.png) 146 | 147 | ## Usage 148 | 149 | ``` 150 | $ python rfems.py --help 151 | usage: rfems.py [-h] [--pitch PITCH] [--frequency FREQ] [--span SPAN] 152 | [--points POINTS] [--start PORT] [--stop PORT] [--line LINE] 153 | [--farfield] [--dphi DPHI] [--dtheta DTHETA] [--nominimum] 154 | [--criteria CRITERIA] [--average] [--verbose VERBOSE] 155 | [--threads THREADS] [--show-model] [--dump-pec] 156 | input_filename [output_filename] 157 | 158 | positional arguments: 159 | input_filename input zip file of STL models 160 | output_filename s-parameter and farfield .npz output file (default: 161 | None) 162 | 163 | options: 164 | -h, --help show this help message and exit 165 | --pitch PITCH length of a uniform yee cell side (m) (default: 0.001) 166 | --frequency FREQ center simulation frequency (Hz) (default: None) 167 | --span SPAN simulation span, -20dB passband ends (Hz) (default: 168 | None) 169 | --points POINTS measurement frequency points, set to 1 for center 170 | frequency (default: 1000) 171 | --start PORT first port to excite, starting from 1 (default: None) 172 | --stop PORT last port to excite, starting from 1 (default: None) 173 | --line LINE default characteristic impedance of ports (default: 50) 174 | 175 | farfield options: 176 | --farfield generate free-space farfield radiation patterns 177 | (default: False) 178 | --dphi DPHI azimuth increment (degree) (default: 2) 179 | --dtheta DTHETA elevation increment (degree) (default: 2) 180 | --nominimum do not find frequency of least VWSR (default: False) 181 | 182 | openems options: 183 | --criteria CRITERIA end criteria, eg -60 (dB) (default: None) 184 | --average use cell material averaging (default: False) 185 | --verbose VERBOSE openems verbose setting (default: 0) 186 | --threads THREADS number of threads to use, 0 for all (default: 0) 187 | 188 | debugging options: 189 | --show-model run AppCSXCAD on input model, no simulation (default: 190 | False) 191 | --dump-pec generate PEC dump file and run ParaView on it (default: 192 | False) 193 | ``` 194 | 195 | ## Notes 196 | 197 | Openscad cannot create STL models of planar surfaces. As a work around, use a very small value for the flat dimension instead of zero. Rfems flattens all STL files with bounding box dimensions less than or equal to 1e-6 m (or 1e-3 in STL units) to their planar 2D and 1D box equivalent. See patch.py. STL files are considered to use millimeter units. 198 | 199 | -------------------------------------------------------------------------------- /rfems.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import zipfile, tempfile, os, sys, argparse, platform, struct 4 | from openEMS.physical_constants import C0 5 | from openEMS import openEMS 6 | from CSXCAD import ContinuousStructure 7 | 8 | STL_TOL = .001 # mm 9 | STL_UNIT = 1e-3 10 | 11 | DEFAULT_PITCH = 1e-3 12 | DEFAULT_POINTS = 1000 # even to ensure group delay calculation 13 | DEFAULT_REFERENCE = 50 14 | DEFAULT_PRIORITY = 0 15 | DEFAULT_DPHI = 2 16 | DEFAULT_DTHETA = 2 17 | 18 | MATERIALS = { # s/m 19 | 'silver': { "kappa": 62.1e6 }, 20 | 'copper': { "kappa": 58.7e6 }, 21 | 'gold': { "kappa": 44.2e6 }, 22 | 'aluminum': { "kappa": 36.9e6 }, 23 | 'brass': { "kappa": 15.9e6 }, 24 | 'steel': { "kappa": 10.1e6 }, 25 | } 26 | 27 | COLORS = { 28 | "pec": "#dbc7b8", 29 | "silver": "#c0c0c0", 30 | "copper": "#e6be8a", 31 | "gold": "#ffd700", 32 | "aluminum": "#d0d5d9", 33 | "brass": "#ac9f3c", 34 | "steel": "#888b8d", 35 | } 36 | 37 | 38 | def parse_args(): 39 | formatter_class = argparse.ArgumentDefaultsHelpFormatter 40 | parser = argparse.ArgumentParser(formatter_class=formatter_class) 41 | parser.add_argument('input_filename', nargs=1, 42 | help='input zip file of STL models') 43 | parser.add_argument('output_filename', nargs='?', 44 | help='s-parameter and farfield .npz output file') 45 | 46 | parser.add_argument('--pitch', type=float, default=DEFAULT_PITCH, 47 | help='length of a uniform yee cell side (m)') 48 | parser.add_argument('--frequency', type=float, 49 | metavar='FREQ', 50 | help='center simulation frequency (Hz)') 51 | parser.add_argument('--span', type=float, 52 | help='simulation span, -20dB passband ends (Hz)') 53 | parser.add_argument('--points', type=int, default=DEFAULT_POINTS, 54 | help='measurement frequency points, set to 1 for center frequency') 55 | parser.add_argument('--start', type=int, 56 | metavar='PORT', 57 | help='first port to excite, starting from 1') 58 | parser.add_argument('--stop', type=int, 59 | metavar='PORT', 60 | help='last port to excite, starting from 1') 61 | parser.add_argument('--line', type=float, default=DEFAULT_REFERENCE, 62 | help='default characteristic impedance of ports') 63 | 64 | pat_group = parser.add_argument_group("farfield options") 65 | pat_group.add_argument('--farfield', action='store_true', 66 | help='generate free-space farfield radiation patterns') 67 | pat_group.add_argument('--dphi', type=float, default=DEFAULT_DPHI, 68 | help='azimuth increment (degree)') 69 | pat_group.add_argument('--dtheta', type=float, default=DEFAULT_DTHETA, 70 | help='elevation increment (degree)') 71 | pat_group.add_argument('--nominimum', action='store_true', 72 | help='do not find frequency of least VWSR') 73 | 74 | sim_group = parser.add_argument_group("openems options") 75 | sim_group.add_argument('--criteria', type=float, 76 | help='end criteria, eg -60 (dB)') 77 | sim_group.add_argument('--average', action='store_true', 78 | help='use cell material averaging') 79 | sim_group.add_argument('--verbose', type=int, default=0, 80 | help='openems verbose setting') 81 | sim_group.add_argument('--threads', type=int, default=0, 82 | help='number of threads to use, 0 for all') 83 | 84 | debug_group = parser.add_argument_group("debugging options") 85 | debug_group.add_argument('--show-model', action='store_true', 86 | help='run AppCSXCAD on input model, no simulation') 87 | debug_group.add_argument('--dump-pec', action='store_true', 88 | help='generate PEC dump file and run ParaView on it') 89 | return parser.parse_args() 90 | 91 | 92 | def value_error(message): 93 | print(f'ERROR: {message}.') 94 | sys.exit(1) 95 | 96 | 97 | def parse_stl(filename): 98 | data = [] 99 | with open(filename, 'rb') as fp: 100 | header = fp.read(5) 101 | if header == b'solid': 102 | facet = [] 103 | for ln in fp: 104 | d = ln.split() 105 | if d[0] == b'endfacet' and facet: 106 | data.append(np.array(facet)) 107 | facet = [] 108 | if d[0] == b'vertex' and len(d) == 4: 109 | facet.append([ float(x) for x in d[1:] ]) 110 | else: 111 | raise ValueError('binary stl files unsupported: not enough precision') 112 | return data 113 | 114 | 115 | def model_bbox(data): 116 | start = None 117 | stop = None 118 | for facet in data: 119 | for v in facet: 120 | start = v if start is None else np.minimum(v, start) 121 | stop = v if stop is None else np.maximum(v, stop) 122 | return start, stop 123 | 124 | 125 | def unzip_models(filename, dirname): 126 | root, ext = os.path.splitext(filename) 127 | if ext != '.zip': 128 | filename = f'{root}.zip' 129 | zf = zipfile.ZipFile(filename) 130 | data = {} 131 | for info in zf.infolist(): 132 | if not info.is_dir(): 133 | root, ext = os.path.splitext(info.filename) 134 | if ext == '.stl': 135 | name = os.path.basename(root) 136 | path = zf.extract(info, dirname) 137 | data[name] = path 138 | else: 139 | print(f'WARNING: ignoring {info.filename}, only .stl files allowed') 140 | return data 141 | 142 | 143 | def get_material(name): 144 | return name.split('-')[-1].strip().lower() 145 | 146 | 147 | def get_portdir(name): 148 | name = get_material(name) 149 | data = name.split() 150 | for d in data: 151 | if d == 'x': return 0 152 | if d == 'y': return 1 153 | if d == 'z': return 2 154 | value_error('No port direction provided in port name') 155 | 156 | 157 | def get_zo(name): 158 | data = get_material(name).split() 159 | for d in data: 160 | key, _, value = d.partition('=') 161 | if key == 'zo': return int(value) 162 | return args.line 163 | 164 | 165 | def get_priority(name): 166 | data = get_material(name).split() 167 | for d in data: 168 | key, _, value = d.partition('=') 169 | if key == 'priority': return int(value) 170 | return DEFAULT_PRIORITY 171 | 172 | 173 | def get_custom_material(name): 174 | data = get_material(name).split() 175 | options = {} 176 | for d in data: 177 | key, _, value = d.partition('=') 178 | if key == 'epsilon': options['epsilon'] = float(value) 179 | if key == 'kappa': options['kappa'] = float(value) 180 | return options 181 | 182 | 183 | def toint(s): 184 | try: 185 | return int(s) 186 | except ValueError: 187 | pass 188 | 189 | 190 | def is_port(name): 191 | data = get_material(name).split() 192 | return data and data[0] == 'port' 193 | 194 | 195 | def get_portnum(name): 196 | data = get_material(name).split() 197 | for d in data: 198 | n = toint(d) 199 | if n is None: 200 | continue 201 | if n <= 0: 202 | value_error('Port number must be 1 or greater') 203 | return n 204 | value_error('No port number provided') 205 | 206 | 207 | def get_simports(models): 208 | port_models = [ k for k in models.keys() if is_port(k) ] 209 | portnum = sorted([ get_portnum(k) for k in port_models ]) 210 | nport = len(port_models) 211 | if nport == 0: 212 | print('WARNING: No ports provided') 213 | if portnum != list(range(1, nport + 1)): 214 | value_error('Ports must be numbered consecutive') 215 | port_start = max(0, args.start-1) if args.start else 0 216 | port_stop = port_start + 1 if args.stop is None else args.stop 217 | port_stop = nport if port_stop == 0 else min(port_stop, nport) 218 | port_stop = max(port_stop, port_start + 1) 219 | return port_start, port_stop, nport 220 | 221 | 222 | def frequency_sweep(): 223 | frequency = args.frequency 224 | span = args.span 225 | if frequency: 226 | span = span or frequency 227 | elif span: 228 | frequency = frequency or span 229 | elif args.show_model or args.dump_pec: 230 | frequency = span = 1 231 | else: 232 | value_error('Either frequency span or center frequency must be set') 233 | return frequency, span 234 | 235 | 236 | def setup_simulation(CSX): 237 | average = args.average 238 | fo, span = frequency_sweep() 239 | kw = {} 240 | if args.criteria: 241 | kw['EndCriteria'] = 10 ** (args.criteria / 10) 242 | if args.dump_pec: 243 | kw['NrTS'] = 0 244 | FDTD = openEMS(CellConstantMaterial=not average, **kw) 245 | FDTD.SetGaussExcite(fo, span / 2) 246 | boundary = [ 'MUR' if args.farfield else 'PEC' ] * 6 247 | FDTD.SetBoundaryCond(boundary) 248 | FDTD.SetCSX(CSX) 249 | return FDTD 250 | 251 | 252 | def get_frequencies(): 253 | points = args.points 254 | fo, span = frequency_sweep() 255 | if points == 1: 256 | f = np.array([ fo ]) 257 | else: 258 | f = np.linspace(fo - span / 2, fo + span / 2, points) 259 | return f 260 | 261 | 262 | def run_appcsxcad(CSX, sim_path): 263 | os.mkdir(sim_path) 264 | CSX_file = os.path.join(sim_path, 'model.xml') 265 | CSX.Write2XML(CSX_file) 266 | os.system('AppCSXCAD "{}"'.format(CSX_file)) 267 | sys.exit(0) 268 | 269 | 270 | def run_paraview(): 271 | os.system('paraview "PEC_dump.vtp"') 272 | sys.exit(0) 273 | 274 | 275 | def run_simulation(FDTD, sim_path): 276 | threads = max(0, args.threads) 277 | verbose = args.verbose 278 | dump_pec = args.dump_pec 279 | FDTD.Run(sim_path, verbose=verbose, numThreads=threads, debug_pec=dump_pec) 280 | 281 | 282 | def calc_sparameters(ports, s, n): 283 | for m in range(len(ports)): 284 | s[:,m,n] = ports[m].uf_ref / ports[n].uf_inc 285 | 286 | 287 | def calc_radiation(sim_path, s, n, nf2ff): 288 | dphi = args.dphi 289 | dtheta = args.dtheta 290 | theta = np.arange(-180.0, 180.0, dtheta) 291 | phi = np.arange(0, 180, dphi) 292 | frequency = get_frequencies() 293 | if not args.nominimum: 294 | ix = np.argmin(np.abs(s[:,n,n])) 295 | frequency = frequency[ix] or frequency_sweep()[0] 296 | res = nf2ff.CalcNF2FF(sim_path, frequency, theta, phi) 297 | res = dict(res.__dict__) 298 | return res 299 | 300 | 301 | def save_results(filename, f, s, z, ff): 302 | root, ext = os.path.splitext(filename) 303 | if ext != '.npz': 304 | filename = f'{root}.npz' 305 | np.savez(filename, f=f, s=s, z=z, **ff) 306 | 307 | 308 | def is_applesilicon(): 309 | return platform.system() == 'Darwin' and platform.processor() == 'arm' 310 | 311 | 312 | ##################### 313 | 314 | def add_parts(CSX, models): 315 | pitch = args.pitch 316 | mesh = CSX.GetGrid() 317 | mesh.SetDeltaUnit(STL_UNIT) 318 | bbox = [ None, None ] 319 | for name in sorted([ k for k in models.keys() if not is_port(k) ]): 320 | material = get_material(name) 321 | priority = get_priority(name) 322 | start, stop = model_bbox(parse_stl(models[name])) 323 | 324 | # handle <3d surfaces 325 | ix = np.logical_or(stop - start < STL_TOL, np.isclose(stop - start, STL_TOL)) 326 | start[ix] = stop[ix] = ((start + stop) / 2)[ix] 327 | 328 | bbox[0] = start if bbox[0] is None else np.minimum(bbox[0], start) 329 | bbox[1] = stop if bbox[1] is None else np.maximum(bbox[1], stop) 330 | 331 | # get material 332 | tag = material.split()[0] 333 | if tag == 'air': 334 | continue 335 | if args.dump_pec or tag == 'pec': 336 | options = None 337 | elif tag in MATERIALS: 338 | options = MATERIALS[tag] 339 | else: 340 | options = get_custom_material(material) 341 | 342 | # set material 343 | if options: 344 | mat = CSX.AddMaterial(name, **options) 345 | else: 346 | mat = CSX.AddMetal(name) 347 | 348 | # set color 349 | if tag in COLORS: 350 | mat.SetColor(COLORS[tag]) 351 | 352 | # set model 353 | for n in range(3): 354 | mesh.AddLine('xyz'[n], [ start[n], stop[n] ]) 355 | if np.any(np.isclose(stop - start, 0)): 356 | prim = mat.AddBox(start, stop, priority=priority) 357 | else: 358 | prim = mat.AddPolyhedronReader(models[name], priority=priority) 359 | prim.ReadFile() 360 | 361 | bbox = np.array(bbox).T 362 | mesh.AddLine('x', bbox[0]) 363 | mesh.AddLine('y', bbox[1]) 364 | mesh.AddLine('z', bbox[2]) 365 | return mesh 366 | 367 | 368 | def add_ports(FDTD, mesh, models, n): 369 | port = [] 370 | ports = [ name for name in models.keys() if is_port(name) ] 371 | for name in sorted(ports, key=get_portnum): 372 | priority = get_priority(name) 373 | zo = get_zo(name) 374 | port_nr = get_portnum(name) 375 | p_dir = get_portdir(name) 376 | excite = (port_nr == n + 1) 377 | edges2grid = [ 'yz', 'xz', 'xy' ][p_dir] 378 | start, stop = model_bbox(parse_stl(models[name])) 379 | 380 | # handle <3d surfaces 381 | ix = np.logical_or(stop - start < STL_TOL, np.isclose(stop - start, STL_TOL)) 382 | start[ix] = stop[ix] = ((start + stop) / 2)[ix] 383 | 384 | p = FDTD.AddLumpedPort(port_nr=port_nr, R=zo, start=start, stop=stop, 385 | p_dir=p_dir, excite=excite, priority=priority, edges2grid=edges2grid) 386 | port.append(p) 387 | for n in range(3): 388 | mesh.AddLine('xyz'[n], [ start[n], stop[n] ]) 389 | 390 | return port 391 | 392 | 393 | def smooth_mesh(mesh): 394 | pitch = args.pitch 395 | mesh.SmoothMeshLines('all', pitch / STL_UNIT) 396 | 397 | 398 | def main(): 399 | input_filename = os.path.abspath(args.input_filename[0]) 400 | output_filename = os.path.abspath(args.output_filename or input_filename) 401 | 402 | with tempfile.TemporaryDirectory() as tempdir: 403 | tempdir = os.path.realpath(tempdir) 404 | mod_path = os.path.join(tempdir, 'mod') 405 | sim_path = os.path.join(tempdir, 'sim') 406 | models = unzip_models(input_filename, mod_path) 407 | port_start, port_stop, nport = get_simports(models) 408 | frequency = get_frequencies() 409 | z = [ get_zo(name) for name in models.keys() if is_port(name) ] 410 | s = np.zeros((len(frequency), nport, nport), dtype=np.complex128) 411 | ff = {} 412 | 413 | if args.farfield or is_applesilicon(): 414 | if port_stop - port_start > 1: 415 | value_error('Only one port can be simulated with farfield or apple silicon') 416 | 417 | for n in range(port_start, port_stop): 418 | CSX = ContinuousStructure() 419 | FDTD = setup_simulation(CSX) 420 | mesh = add_parts(CSX, models) 421 | ports = add_ports(FDTD, mesh, models, n) 422 | smooth_mesh(mesh) 423 | 424 | if args.farfield: 425 | nf2ff = FDTD.CreateNF2FFBox() 426 | if args.show_model: 427 | run_appcsxcad(CSX, sim_path) 428 | run_simulation(FDTD, sim_path) 429 | if args.dump_pec: 430 | run_paraview() 431 | for p in ports: 432 | p.CalcPort(sim_path, frequency) 433 | calc_sparameters(ports, s, n) 434 | if args.farfield: 435 | ff = calc_radiation(sim_path, s, n, nf2ff) 436 | 437 | save_results(output_filename, f=frequency, s=s, z=z, ff=ff) 438 | if is_applesilicon(): 439 | os.kill(os.getpid(), 9) 440 | 441 | 442 | if __name__ == '__main__': 443 | args = parse_args() 444 | main() 445 | 446 | 447 | --------------------------------------------------------------------------------