emerge 0.4.7__py3-none-any.whl → 0.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of emerge might be problematic. Click here for more details.
- emerge/__init__.py +14 -14
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +1 -1
- emerge/plot.py +1 -1
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
- emerge-0.4.9.dist-info/RECORD +78 -0
- emerge-0.4.9.dist-info/entry_points.txt +2 -0
- emerge-0.4.7.dist-info/RECORD +0 -9
- emerge-0.4.7.dist-info/entry_points.txt +0 -2
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
# EMerge is an open source Python based FEM EM simulation module.
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis.
|
|
3
|
+
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see
|
|
16
|
+
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
import numpy as np
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from typing import Callable, Literal
|
|
22
|
+
from ...selection import Selection, FaceSelection
|
|
23
|
+
from ...cs import CoordinateSystem, Axis, GCS
|
|
24
|
+
from ...coord import Line
|
|
25
|
+
from ...geometry import GeoSurface, GeoObject, GeoPolygon
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
from ...bc import BoundaryCondition, BoundaryConditionSet, Periodic
|
|
29
|
+
from ...geo import XYPolygon, XYPlate
|
|
30
|
+
from ...periodic import PeriodicCell, HexCell, RectCell, Alignment
|
|
31
|
+
|
|
32
|
+
class MWBoundaryConditionSet(BoundaryConditionSet):
|
|
33
|
+
|
|
34
|
+
def __init__(self, periodic_cell: PeriodicCell):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
self.PEC: type[PEC] = self._construct_bc(PEC)
|
|
38
|
+
self.PMC: type[PMC] = self._construct_bc(PMC)
|
|
39
|
+
self.AbsorbingBoundary: type[AbsorbingBoundary] = self._construct_bc(AbsorbingBoundary)
|
|
40
|
+
self.ModalPort: type[ModalPort] = self._construct_bc(ModalPort)
|
|
41
|
+
self.LumpedPort: type[LumpedPort] = self._construct_bc(LumpedPort)
|
|
42
|
+
self.LumpedElement: type[LumpedElement] = self._construct_bc(LumpedElement)
|
|
43
|
+
self.RectangularWaveguide: type[RectangularWaveguide] = self._construct_bc(RectangularWaveguide)
|
|
44
|
+
self.Periodic: type[Periodic] = self._construct_bc(Periodic)
|
|
45
|
+
self.FloquetPort: type[FloquetPort] = self._construct_bc(FloquetPort)
|
|
46
|
+
|
|
47
|
+
self._cell: PeriodicCell = None
|
|
48
|
+
|
|
49
|
+
def floquet_port(self, poly: GeoSurface, port_number: int) -> FloquetPort:
|
|
50
|
+
if self._cell is None:
|
|
51
|
+
raise ValueError('Periodic cel must be defined for this simulation.')
|
|
52
|
+
if isinstance(self._cell, RectCell):
|
|
53
|
+
port = self.FloquetPort(poly, port_number)
|
|
54
|
+
port.width = self._cell.width
|
|
55
|
+
port.height = self._cell.height
|
|
56
|
+
elif isinstance(self._cell, HexCell):
|
|
57
|
+
port = self.FloquetPort(poly, port_number)
|
|
58
|
+
port.area = 1.0
|
|
59
|
+
self._cell._ports.append(port)
|
|
60
|
+
return port
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PEC(BoundaryCondition):
|
|
65
|
+
|
|
66
|
+
def __init__(self,
|
|
67
|
+
face: FaceSelection | GeoSurface):
|
|
68
|
+
"""The general perfect electric conductor boundary condition.
|
|
69
|
+
|
|
70
|
+
The physics compiler will by default always turn all exterior faces into a PEC.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
face (FaceSelection | GeoSurface): The boundary surface
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(face)
|
|
76
|
+
|
|
77
|
+
class PMC(BoundaryCondition):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
class RobinBC(BoundaryCondition):
|
|
81
|
+
|
|
82
|
+
_include_stiff: bool = False
|
|
83
|
+
_include_mass: bool = False
|
|
84
|
+
_include_force: bool = False
|
|
85
|
+
|
|
86
|
+
def __init__(self, selection: GeoSurface | Selection):
|
|
87
|
+
"""A Generalization of any boundary condition of the third kind (Robin).
|
|
88
|
+
|
|
89
|
+
This should not be created directly. A robin boundary condition is the generalized type behind
|
|
90
|
+
port boundaries, radiation boundaries etc. Since all boundary conditions of the thrid kind (Robin)
|
|
91
|
+
are assembled the same, this class is used during assembly.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
selection (GeoSurface | Selection): The boundary surface.
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(selection)
|
|
97
|
+
self.v_integration: bool = False
|
|
98
|
+
self.vintline: Line = None
|
|
99
|
+
|
|
100
|
+
def get_basis(self) -> np.ndarray:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def get_inv_basis(self) -> np.ndarray:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def get_beta(self, k0) -> float:
|
|
107
|
+
raise NotImplementedError('get_beta not implemented for Port class')
|
|
108
|
+
|
|
109
|
+
def get_gamma(self, k0) -> float:
|
|
110
|
+
raise NotImplementedError('get_gamma not implemented for Port class')
|
|
111
|
+
|
|
112
|
+
def get_Uinc(self, k0) -> np.ndarray:
|
|
113
|
+
raise NotImplementedError('get_Uinc not implemented for Port class')
|
|
114
|
+
|
|
115
|
+
class PortBC(RobinBC):
|
|
116
|
+
Zvac: float = 376.730313412
|
|
117
|
+
def __init__(self, face: FaceSelection | GeoSurface):
|
|
118
|
+
"""(DO NOT USE) A generalization of the Port boundary condition.
|
|
119
|
+
|
|
120
|
+
DO NOT USE THIS TO DEFINE PORTS. This class is only indeded for
|
|
121
|
+
class inheritance and type checking.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
face (FaceSelection | GeoSurface): The port face
|
|
125
|
+
"""
|
|
126
|
+
super().__init__(face)
|
|
127
|
+
self.port_number: int = None
|
|
128
|
+
self.cs: CoordinateSystem = None
|
|
129
|
+
self.selected_mode: int = 0
|
|
130
|
+
self.Z0 = None
|
|
131
|
+
self.active: bool = False
|
|
132
|
+
|
|
133
|
+
def get_basis(self) -> np.ndarray:
|
|
134
|
+
return self.cs._basis
|
|
135
|
+
|
|
136
|
+
def get_inv_basis(self) -> np.ndarray:
|
|
137
|
+
return self.cs._basis_inv
|
|
138
|
+
|
|
139
|
+
def portZ0(self, k0: float = None) -> complex:
|
|
140
|
+
"""Returns the port characteristic impedance given a phase constant
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
k0 (float): The phase constant
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
complex: The port impedance
|
|
147
|
+
"""
|
|
148
|
+
return self.Z0
|
|
149
|
+
|
|
150
|
+
def modetype(self, k0: float) -> Literal['TEM','TE','TM']:
|
|
151
|
+
return 'TEM'
|
|
152
|
+
|
|
153
|
+
def Zmode(self, k0: float) -> float:
|
|
154
|
+
if self.modetype(k0)=='TEM':
|
|
155
|
+
return self.Zvac
|
|
156
|
+
elif self.modetype(k0)=='TE':
|
|
157
|
+
return k0*299792458/self.get_beta(k0) * 4*np.pi*1e-7
|
|
158
|
+
elif self.modetype(k0)=='TM':
|
|
159
|
+
return self.get_beta(k0)/(k0*299792458*8.854187818814*1e-12)
|
|
160
|
+
else:
|
|
161
|
+
return ValueError(f'Port mode type should be TEM, TE or TM but instead is {self.modetype(k0)}')
|
|
162
|
+
|
|
163
|
+
def _qmode(self, k0: float) -> float:
|
|
164
|
+
"""Computes a mode amplitude correction factor.
|
|
165
|
+
The total output power of a port as a function of the field amplitude is not constant.
|
|
166
|
+
For TE and TM modes the output power depends on the mode impedance. This factor corrects
|
|
167
|
+
the mode output power to 1W by scaling the E-field appropriately.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
k0 (float): The phase constant of the simulation
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
float: The mode amplitude correction factor.
|
|
174
|
+
"""
|
|
175
|
+
return np.sqrt(self.Zmode(k0)/376.73031341259)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def mode_number(self) -> int:
|
|
179
|
+
return self.selected_mode + 1
|
|
180
|
+
|
|
181
|
+
def get_beta(self, k0) -> float:
|
|
182
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
183
|
+
return k0
|
|
184
|
+
|
|
185
|
+
def get_gamma(self, k0):
|
|
186
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
k0 (float): The free space propagation constant.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
complex: The γ-constant
|
|
193
|
+
"""
|
|
194
|
+
return 1j*self.get_beta(k0)
|
|
195
|
+
|
|
196
|
+
def port_mode_3d(self,
|
|
197
|
+
xs: np.ndarray,
|
|
198
|
+
ys: np.ndarray,
|
|
199
|
+
k0: float,
|
|
200
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
201
|
+
raise NotImplementedError('port_mode_3d not implemented for Port class')
|
|
202
|
+
|
|
203
|
+
def port_mode_3d_global(self,
|
|
204
|
+
x_global: np.ndarray,
|
|
205
|
+
y_global: np.ndarray,
|
|
206
|
+
z_global: np.ndarray,
|
|
207
|
+
k0: float,
|
|
208
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
209
|
+
xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
|
|
210
|
+
Ex, Ey, Ez = self.port_mode_3d(xl, yl, k0)
|
|
211
|
+
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
212
|
+
return np.array([Exg, Eyg, Ezg])
|
|
213
|
+
|
|
214
|
+
class AbsorbingBoundary(RobinBC):
|
|
215
|
+
|
|
216
|
+
_include_stiff: bool = True
|
|
217
|
+
_include_mass: bool = True
|
|
218
|
+
_include_force: bool = False
|
|
219
|
+
|
|
220
|
+
def __init__(self,
|
|
221
|
+
face: FaceSelection | GeoSurface,
|
|
222
|
+
order: int = 1,
|
|
223
|
+
origin: tuple = None):
|
|
224
|
+
"""Creates an AbsorbingBoundary condition.
|
|
225
|
+
|
|
226
|
+
Currently only a first order boundary condition is possible. Second order will be supported later.
|
|
227
|
+
The absorbing boundary is effectively a port boundary condition (Robin) with an assumption on
|
|
228
|
+
the out-of-plane phase constant. For now it always assumes the free-space propagation (normal).
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
face (FaceSelection | GeoSurface): The absorbing boundary face(s)
|
|
232
|
+
order (int, optional): The order (only 1 is supported). Defaults to 1.
|
|
233
|
+
origin (tuple, optional): The radiation origin. Defaults to None.
|
|
234
|
+
"""
|
|
235
|
+
super().__init__(face)
|
|
236
|
+
|
|
237
|
+
self.order: int = order
|
|
238
|
+
self.origin: tuple = origin
|
|
239
|
+
self.cs: CoordinateSystem = GCS
|
|
240
|
+
|
|
241
|
+
def get_basis(self) -> np.ndarray:
|
|
242
|
+
return np.eye(3)
|
|
243
|
+
|
|
244
|
+
def get_beta(self, k0) -> float:
|
|
245
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
246
|
+
return k0
|
|
247
|
+
|
|
248
|
+
def get_gamma(self, k0):
|
|
249
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
k0 (float): The free space propagation constant.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
complex: The γ-constant
|
|
256
|
+
"""
|
|
257
|
+
return 1j*self.get_beta(k0)
|
|
258
|
+
|
|
259
|
+
def get_Uinc(self, x_local, y_local, k0) -> np.ndarray:
|
|
260
|
+
return np.zeros((3, len(x_local)), dtype=np.complex128)
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class PortMode:
|
|
264
|
+
modefield: np.ndarray
|
|
265
|
+
E_function: Callable
|
|
266
|
+
H_function: Callable
|
|
267
|
+
k0: float
|
|
268
|
+
beta: float
|
|
269
|
+
residual: float
|
|
270
|
+
energy: float = None
|
|
271
|
+
norm_factor: float = 1
|
|
272
|
+
freq: float = None
|
|
273
|
+
neff: float = None
|
|
274
|
+
TEM: bool = None
|
|
275
|
+
Z0: float = None
|
|
276
|
+
polarity: float = 1
|
|
277
|
+
modetype: Literal['TEM','TE','TM'] = 'TEM'
|
|
278
|
+
|
|
279
|
+
def __post_init__(self):
|
|
280
|
+
self.neff = self.beta/self.k0
|
|
281
|
+
self.energy = np.mean(np.abs(self.modefield)**2)
|
|
282
|
+
|
|
283
|
+
def __str__(self):
|
|
284
|
+
return f'PortMode(k0={self.k0}, beta={self.beta}, neff={self.neff}, energy={self.energy})'
|
|
285
|
+
|
|
286
|
+
def set_power(self, power: complex) -> None:
|
|
287
|
+
self.norm_factor = np.sqrt(1/np.abs(power))
|
|
288
|
+
logger.info(f'Setting port mode amplitude to: {self.norm_factor} ')
|
|
289
|
+
|
|
290
|
+
class FloquetPort(PortBC):
|
|
291
|
+
_include_stiff: bool = True
|
|
292
|
+
_include_mass: bool = False
|
|
293
|
+
_include_force: bool = True
|
|
294
|
+
|
|
295
|
+
def __init__(self,
|
|
296
|
+
face: FaceSelection | GeoSurface,
|
|
297
|
+
port_number: int,
|
|
298
|
+
cs: CoordinateSystem = None,
|
|
299
|
+
power: float = 1.0,
|
|
300
|
+
er: float = 1.0):
|
|
301
|
+
super().__init__(face)
|
|
302
|
+
self.port_number: int= port_number
|
|
303
|
+
self.active: bool = True
|
|
304
|
+
self.power: float = power
|
|
305
|
+
self.type: str = 'TE'
|
|
306
|
+
self._field_amplitude: np.ndarray = None
|
|
307
|
+
self.mode: tuple[int,int] = (1,0)
|
|
308
|
+
self.cs: CoordinateSystem = cs
|
|
309
|
+
self.scan_theta: float = 0
|
|
310
|
+
self.scan_phi: float = 0
|
|
311
|
+
self.pol_s: complex = 1.0
|
|
312
|
+
self.pol_p: complex = 0.0
|
|
313
|
+
self.Zdir: Axis = -1
|
|
314
|
+
self.area: float = 1
|
|
315
|
+
if self.cs is None:
|
|
316
|
+
self.cs = GCS
|
|
317
|
+
|
|
318
|
+
def portZ0(self, k0: float = None) -> complex:
|
|
319
|
+
return 376.73031341259
|
|
320
|
+
|
|
321
|
+
def get_amplitude(self, k0: float) -> float:
|
|
322
|
+
return 1.0
|
|
323
|
+
|
|
324
|
+
def get_beta(self, k0: float) -> float:
|
|
325
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
326
|
+
return k0*np.cos(self.scan_theta)
|
|
327
|
+
|
|
328
|
+
def get_gamma(self, k0: float):
|
|
329
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
k0 (float): The free space propagation constant.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
complex: The γ-constant
|
|
336
|
+
"""
|
|
337
|
+
return 1j*self.get_beta(k0)
|
|
338
|
+
|
|
339
|
+
def get_Uinc(self, x_local: np.ndarray, y_local: np.ndarray, k0: float) -> np.ndarray:
|
|
340
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
|
|
341
|
+
|
|
342
|
+
def port_mode_3d(self,
|
|
343
|
+
x_local: np.ndarray,
|
|
344
|
+
y_local: np.ndarray,
|
|
345
|
+
k0: float,
|
|
346
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
347
|
+
''' Compute the port mode E-field in local coordinates (XY) + Z out of plane.'''
|
|
348
|
+
|
|
349
|
+
kx = k0*np.sin(self.scan_theta)*np.cos(self.scan_phi)
|
|
350
|
+
ky = k0*np.sin(self.scan_theta)*np.sin(self.scan_phi)
|
|
351
|
+
kz = k0*np.cos(self.scan_theta)
|
|
352
|
+
phi = np.exp(-1j*(x_local*kx + y_local*ky))
|
|
353
|
+
|
|
354
|
+
P = self.pol_p
|
|
355
|
+
S = self.pol_s
|
|
356
|
+
|
|
357
|
+
E0 = self.get_amplitude(k0)*np.sqrt(2*376.73031341259/(self.area))
|
|
358
|
+
Ex = E0*(-S*np.sin(self.scan_phi) - P*np.cos(self.scan_theta)*np.cos(self.scan_phi))*phi
|
|
359
|
+
Ey = E0*(S*np.cos(self.scan_phi) - P*np.cos(self.scan_theta)*np.sin(self.scan_phi))*phi
|
|
360
|
+
Ez = E0*(-P*E0*np.sin(self.scan_theta))*phi
|
|
361
|
+
Exyz = np.array([Ex, Ey, Ez])
|
|
362
|
+
return Exyz
|
|
363
|
+
|
|
364
|
+
def port_mode_3d_global(self,
|
|
365
|
+
x_global: np.ndarray,
|
|
366
|
+
y_global: np.ndarray,
|
|
367
|
+
z_global: np.ndarray,
|
|
368
|
+
k0: float,
|
|
369
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
370
|
+
'''Compute the port mode field for global xyz coordinates.'''
|
|
371
|
+
xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
|
|
372
|
+
Ex, Ey, Ez = self.port_mode_3d(xl, yl, k0)
|
|
373
|
+
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
374
|
+
return np.array([Exg, Eyg, Ezg])
|
|
375
|
+
|
|
376
|
+
class ModalPort(PortBC):
|
|
377
|
+
|
|
378
|
+
_include_stiff: bool = True
|
|
379
|
+
_include_mass: bool = False
|
|
380
|
+
_include_force: bool = True
|
|
381
|
+
|
|
382
|
+
def __init__(self,
|
|
383
|
+
face: FaceSelection | GeoSurface,
|
|
384
|
+
port_number: int,
|
|
385
|
+
active: bool = False,
|
|
386
|
+
cs: CoordinateSystem = None,
|
|
387
|
+
power: float = 1,
|
|
388
|
+
TEM: bool = False,
|
|
389
|
+
mixed_materials: bool = False):
|
|
390
|
+
"""Generes a ModalPort boundary condition for a port that requires eigenmode solutions for the mode.
|
|
391
|
+
|
|
392
|
+
The boundary condition requires a FaceSelection (or GeoSurface related) object for the face and a port
|
|
393
|
+
number.
|
|
394
|
+
If the face coordinate system is not provided a local coordinate system will be derived automatically
|
|
395
|
+
by finding the plane that spans the face nodes with minimial out-of-plane error.
|
|
396
|
+
|
|
397
|
+
All modal ports require the execution of a .modal_analysis() by the physics class to define
|
|
398
|
+
the port mode.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
face (FaceSelection, GeoSurface): The port mode face
|
|
402
|
+
port_number (int): The port number as an integer
|
|
403
|
+
active (bool, optional): Whether the port is set active. Defaults to False.
|
|
404
|
+
cs (CoordinateSystem, optional): The local coordinate system of the port face. Defaults to None.
|
|
405
|
+
power (float, optional): The radiated power. Defaults to 1.
|
|
406
|
+
TEM (bool, optional): Wether the mode should be considered as a TEM mode. Defaults to False
|
|
407
|
+
mixed_materials (bool, optional): Wether the port consists of multiple different dielectrics. This requires
|
|
408
|
+
A recalculation of the port mode at every frequency
|
|
409
|
+
"""
|
|
410
|
+
super().__init__(face)
|
|
411
|
+
|
|
412
|
+
self.port_number: int= port_number
|
|
413
|
+
self.active: bool = active
|
|
414
|
+
self.power: float = power
|
|
415
|
+
self.cs: CoordinateSystem = cs
|
|
416
|
+
|
|
417
|
+
self.selected_mode: int = 0
|
|
418
|
+
self.modes: dict[float, list[PortMode]] = defaultdict(list)
|
|
419
|
+
|
|
420
|
+
self.TEM: bool = TEM
|
|
421
|
+
self.mixed_materials: bool = mixed_materials
|
|
422
|
+
self.initialized: bool = False
|
|
423
|
+
self._first_k0: float = None
|
|
424
|
+
self._last_k0: float = None
|
|
425
|
+
|
|
426
|
+
if self.cs is None:
|
|
427
|
+
logger.info('Constructing coordinate system from normal port')
|
|
428
|
+
self.cs = Axis(self.selection.normal).construct_cs()
|
|
429
|
+
|
|
430
|
+
self._er: np.ndarray = None
|
|
431
|
+
self._ur: np.ndarray = None
|
|
432
|
+
|
|
433
|
+
def portZ0(self, k0: float) -> complex:
|
|
434
|
+
return self.get_mode(k0).Z0
|
|
435
|
+
|
|
436
|
+
def modetype(self, k0: float) -> Literal['TEM','TE','TM']:
|
|
437
|
+
return self.get_mode(k0).modetype
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def nmodes(self) -> int:
|
|
441
|
+
return len(self.modes[self._last_k0])
|
|
442
|
+
|
|
443
|
+
def sort_modes(self) -> None:
|
|
444
|
+
"""Sorts the port modes based on total energy
|
|
445
|
+
"""
|
|
446
|
+
for k0, modes in self.modes.items():
|
|
447
|
+
self.modes[k0] = sorted(modes, key=lambda m: m.energy, reverse=True)
|
|
448
|
+
|
|
449
|
+
def get_mode(self, k0: float, i=None) -> PortMode:
|
|
450
|
+
"""Returns a given mode solution in the form of a PortMode object.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
i (_type_, optional): The mode solution number. Defaults to None.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
PortMode: The requested PortMode object
|
|
457
|
+
"""
|
|
458
|
+
if i is None:
|
|
459
|
+
i = self.selected_mode
|
|
460
|
+
return self.modes[min(self.modes.keys(), key=lambda k: abs(k - k0))][i]
|
|
461
|
+
|
|
462
|
+
def global_field_function(self, k0: float = 0, which: Literal['E','H'] = 'E') -> Callable:
|
|
463
|
+
''' The field function used to compute the E-field.
|
|
464
|
+
This field-function is defined in global coordinates (not local coordinates).'''
|
|
465
|
+
mode = self.get_mode(k0)
|
|
466
|
+
if which == 'E':
|
|
467
|
+
return lambda x,y,z: mode.norm_factor * self._qmode(k0) * mode.E_function(x,y,z)*mode.polarity
|
|
468
|
+
else:
|
|
469
|
+
return lambda x,y,z: mode.norm_factor * self._qmode(k0) * mode.H_function(x,y,z)*mode.polarity
|
|
470
|
+
|
|
471
|
+
def clear_modes(self) -> None:
|
|
472
|
+
"""Clear all port mode data"""
|
|
473
|
+
self.modes: dict[float, list[PortMode]] = defaultdict(list)
|
|
474
|
+
self.initialized = False
|
|
475
|
+
|
|
476
|
+
def add_mode(self,
|
|
477
|
+
field: np.ndarray,
|
|
478
|
+
E_function: Callable,
|
|
479
|
+
H_function: Callable,
|
|
480
|
+
beta: float,
|
|
481
|
+
k0: float,
|
|
482
|
+
residual: float,
|
|
483
|
+
TEM: bool,
|
|
484
|
+
freq: float) -> PortMode:
|
|
485
|
+
"""Add a mode function to the ModalPort
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
field (np.ndarray): The field value array
|
|
489
|
+
E_function (Callable): The E-field callable
|
|
490
|
+
H_function (Callable): The H-field callable
|
|
491
|
+
beta (float): The out-of-plane propagation constant
|
|
492
|
+
k0 (float): The free space phase constant
|
|
493
|
+
residual (float): The solution residual
|
|
494
|
+
TEM (bool): Whether its a TEM mode
|
|
495
|
+
freq (float): The frequency of the port mode
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
PortMode: The port mode object.
|
|
499
|
+
"""
|
|
500
|
+
mode = PortMode(field, E_function, H_function, k0, beta, residual, TEM=TEM, freq=freq)
|
|
501
|
+
if mode.energy < 1e-4:
|
|
502
|
+
logger.debug(f'Ignoring mode due to a low mode energy: {mode.energy}')
|
|
503
|
+
return None
|
|
504
|
+
self.modes[k0].append(mode)
|
|
505
|
+
self.initialized = True
|
|
506
|
+
|
|
507
|
+
self._last_k0 = k0
|
|
508
|
+
if self._first_k0 is None:
|
|
509
|
+
self._first_k0 = k0
|
|
510
|
+
else:
|
|
511
|
+
ref_field = self.get_mode(self._first_k0, -1).modefield
|
|
512
|
+
polarity = np.sign(np.sum(field*ref_field).real)
|
|
513
|
+
logger.debug(f'Mode polarity = {polarity}')
|
|
514
|
+
mode.polarity = polarity
|
|
515
|
+
|
|
516
|
+
return mode
|
|
517
|
+
|
|
518
|
+
def get_basis(self) -> np.ndarray:
|
|
519
|
+
return self.cs._basis
|
|
520
|
+
|
|
521
|
+
def get_beta(self, k0: float) -> float:
|
|
522
|
+
mode = self.get_mode(k0)
|
|
523
|
+
if mode.TEM:
|
|
524
|
+
beta = mode.beta/mode.k0 * k0
|
|
525
|
+
else:
|
|
526
|
+
freq = k0*299792458/(2*np.pi)
|
|
527
|
+
beta = np.sqrt(mode.beta**2 + k0**2 * (1-((mode.freq/freq)**2)))
|
|
528
|
+
return beta
|
|
529
|
+
|
|
530
|
+
def get_gamma(self, k0: float):
|
|
531
|
+
return 1j*self.get_beta(k0)
|
|
532
|
+
|
|
533
|
+
def get_Uinc(self, x_local, y_local, k0) -> np.ndarray:
|
|
534
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
|
|
535
|
+
|
|
536
|
+
def port_mode_3d(self,
|
|
537
|
+
x_local: np.ndarray,
|
|
538
|
+
y_local: np.ndarray,
|
|
539
|
+
k0: float,
|
|
540
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
541
|
+
x_global, y_global, z_global = self.cs.in_global_cs(x_local, y_local, 0*x_local)
|
|
542
|
+
|
|
543
|
+
Egxyz = self.port_mode_3d_global(x_global,y_global,z_global,k0,which=which)
|
|
544
|
+
|
|
545
|
+
Ex, Ey, Ez = self.cs.in_local_basis(Egxyz[0,:], Egxyz[1,:], Egxyz[2,:])
|
|
546
|
+
|
|
547
|
+
Exyz = np.array([Ex, Ey, Ez])
|
|
548
|
+
return Exyz
|
|
549
|
+
|
|
550
|
+
def port_mode_3d_global(self,
|
|
551
|
+
x_global: np.ndarray,
|
|
552
|
+
y_global: np.ndarray,
|
|
553
|
+
z_global: np.ndarray,
|
|
554
|
+
k0: float,
|
|
555
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
556
|
+
Ex, Ey, Ez = self.global_field_function(k0, which)(x_global,y_global,z_global)
|
|
557
|
+
Exyz = np.array([Ex, Ey, Ez])
|
|
558
|
+
return Exyz
|
|
559
|
+
|
|
560
|
+
class RectangularWaveguide(PortBC):
|
|
561
|
+
|
|
562
|
+
_include_stiff: bool = True
|
|
563
|
+
_include_mass: bool = False
|
|
564
|
+
_include_force: bool = True
|
|
565
|
+
|
|
566
|
+
def __init__(self,
|
|
567
|
+
face: FaceSelection | GeoSurface,
|
|
568
|
+
port_number: int,
|
|
569
|
+
active: bool = False,
|
|
570
|
+
cs: CoordinateSystem = None,
|
|
571
|
+
dims: tuple[float, float] = None,
|
|
572
|
+
power: float = 1):
|
|
573
|
+
"""Creates a rectangular waveguide as a port boundary condition.
|
|
574
|
+
|
|
575
|
+
Currently the Rectangular waveguide only supports TE0n modes. The mode field
|
|
576
|
+
is derived analytically. The local face coordinate system and dimensions can be provided
|
|
577
|
+
manually. If not provided the class will attempt to derive the local coordinate system and
|
|
578
|
+
face dimensions itself. It always orients the longest edge along the local X-direction.
|
|
579
|
+
The information on the derived coordiante system will be shown in the DEBUG level logs.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
face (FaceSelection, GeoSurface): The port boundary face selection
|
|
583
|
+
port_number (int): The port number
|
|
584
|
+
active (bool, optional): Ther the port is active. Defaults to False.
|
|
585
|
+
cs (CoordinateSystem, optional): The local coordinate system. Defaults to None.
|
|
586
|
+
dims (tuple[float, float], optional): The port face. Defaults to None.
|
|
587
|
+
power (float): The port power. Default to 1.
|
|
588
|
+
"""
|
|
589
|
+
super().__init__(face)
|
|
590
|
+
|
|
591
|
+
self.port_number: int= port_number
|
|
592
|
+
self.active: bool = active
|
|
593
|
+
self.power: float = power
|
|
594
|
+
self.type: str = 'TE'
|
|
595
|
+
self._field_amplitude: np.ndarray = None
|
|
596
|
+
self.mode: tuple[int,int] = (1,0)
|
|
597
|
+
self.cs: CoordinateSystem = cs
|
|
598
|
+
|
|
599
|
+
if dims is None:
|
|
600
|
+
logger.info("Determining port face based on selection")
|
|
601
|
+
cs, (width, height) = face.rect_basis()
|
|
602
|
+
self.cs = cs
|
|
603
|
+
self.dims = (width, height)
|
|
604
|
+
logger.debug(f'Port CS: {self.cs}')
|
|
605
|
+
logger.debug(f'Detected port {self.port_number} size = {width*1000:.1f} mm x {height*1000:.1f} mm')
|
|
606
|
+
|
|
607
|
+
if self.cs is None:
|
|
608
|
+
logger.info('Constructing coordinate system from normal port')
|
|
609
|
+
self.cs = Axis(self.selection.normal).construct_cs()
|
|
610
|
+
else:
|
|
611
|
+
self.cs: CoordinateSystem = cs
|
|
612
|
+
|
|
613
|
+
def portZ0(self, k0: float = None) -> complex:
|
|
614
|
+
return k0*299792458 * 4*np.pi*1e-7/self.get_beta(k0)
|
|
615
|
+
|
|
616
|
+
def modetype(self, k0):
|
|
617
|
+
return self.type
|
|
618
|
+
|
|
619
|
+
def get_amplitude(self, k0: float) -> float:
|
|
620
|
+
Zte = 376.73031341259
|
|
621
|
+
amplitude= np.sqrt(self.power*4*Zte/(self.dims[0]*self.dims[1]))
|
|
622
|
+
return amplitude
|
|
623
|
+
|
|
624
|
+
def port_mode_2d(self, xs: np.ndarray, ys: np.ndarray, k0: float) -> tuple[np.ndarray, float]:
|
|
625
|
+
x0 = xs[0]
|
|
626
|
+
y0 = ys[0]
|
|
627
|
+
x1 = xs[-1]
|
|
628
|
+
y1 = ys[-1]
|
|
629
|
+
xc = 0.5*(x0+x1)
|
|
630
|
+
yc = 0.5*(y0+y1)
|
|
631
|
+
a = np.sqrt((x1-x0)**2 + (y1-y0)**2)
|
|
632
|
+
|
|
633
|
+
logger.debug(f'Detected port {self.port_number} width = {a*1000:.1f} mm')
|
|
634
|
+
ds = np.sqrt((xs-xc)**2 + (ys-yc)**2)
|
|
635
|
+
return self.amplitude*np.cos(ds*np.pi/a), np.sqrt(k0**2 - (np.pi/a)**2)
|
|
636
|
+
|
|
637
|
+
def get_beta(self, k0: float) -> float:
|
|
638
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
639
|
+
width=self.dims[0]
|
|
640
|
+
height=self.dims[1]
|
|
641
|
+
beta = np.sqrt(k0**2 - (np.pi*self.mode[0]/width)**2 - (np.pi*self.mode[1]/height)**2)
|
|
642
|
+
return beta
|
|
643
|
+
|
|
644
|
+
def get_gamma(self, k0: float):
|
|
645
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
k0 (float): The free space propagation constant.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
complex: The γ-constant
|
|
652
|
+
"""
|
|
653
|
+
return 1j*self.get_beta(k0)
|
|
654
|
+
|
|
655
|
+
def get_Uinc(self, x_local: np.ndarray, y_local: np.ndarray, k0: float) -> np.ndarray:
|
|
656
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d(x_local, y_local, k0)
|
|
657
|
+
|
|
658
|
+
def port_mode_3d(self,
|
|
659
|
+
x_local: np.ndarray,
|
|
660
|
+
y_local: np.ndarray,
|
|
661
|
+
k0: float,
|
|
662
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
663
|
+
''' Compute the port mode E-field in local coordinates (XY) + Z out of plane.'''
|
|
664
|
+
|
|
665
|
+
width = self.dims[0]
|
|
666
|
+
height = self.dims[1]
|
|
667
|
+
|
|
668
|
+
E = self.get_amplitude(k0)*np.cos(np.pi*self.mode[0]*(x_local)/width)*np.cos(np.pi*self.mode[1]*(y_local)/height)
|
|
669
|
+
Ex = 0*E
|
|
670
|
+
Ey = E
|
|
671
|
+
Ez = 0*E
|
|
672
|
+
Exyz = self._qmode(k0) * np.array([Ex, Ey, Ez])
|
|
673
|
+
return Exyz
|
|
674
|
+
|
|
675
|
+
def port_mode_3d_global(self,
|
|
676
|
+
x_global: np.ndarray,
|
|
677
|
+
y_global: np.ndarray,
|
|
678
|
+
z_global: np.ndarray,
|
|
679
|
+
k0: float,
|
|
680
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
681
|
+
'''Compute the port mode field for global xyz coordinates.'''
|
|
682
|
+
xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
|
|
683
|
+
Ex, Ey, Ez = self.port_mode_3d(xl, yl, k0)
|
|
684
|
+
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
685
|
+
return np.array([Exg, Eyg, Ezg])
|
|
686
|
+
|
|
687
|
+
class LumpedPort(PortBC):
|
|
688
|
+
|
|
689
|
+
_include_stiff: bool = True
|
|
690
|
+
_include_mass: bool = False
|
|
691
|
+
_include_force: bool = True
|
|
692
|
+
|
|
693
|
+
def __init__(self,
|
|
694
|
+
face: FaceSelection | GeoSurface,
|
|
695
|
+
port_number: int,
|
|
696
|
+
width: float = None,
|
|
697
|
+
height: float = None,
|
|
698
|
+
direction: Axis = None,
|
|
699
|
+
Idirection: Axis = None,
|
|
700
|
+
active: bool = False,
|
|
701
|
+
power: float = 1,
|
|
702
|
+
Z0: float = 50):
|
|
703
|
+
"""Generates a lumped power boundary condition.
|
|
704
|
+
|
|
705
|
+
The lumped port boundary condition assumes a uniform E-field along the "direction" axis.
|
|
706
|
+
The port with and height must be provided manually in meters. The height is the size
|
|
707
|
+
in the "direction" axis along which the potential is imposed. The width dimension
|
|
708
|
+
is orthogonal to that. For a rectangular face its the width and for a cyllindrical face
|
|
709
|
+
its the circumpherance.
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
face (FaceSelection, GeoSurface): The port surface
|
|
713
|
+
port_number (int): The port number
|
|
714
|
+
width (float): The port width (meters).
|
|
715
|
+
height (float): The port height (meters).
|
|
716
|
+
direction (Axis): The port direction as an Axis object (em.Axis(..) or em.ZAX)
|
|
717
|
+
active (bool, optional): Whether the port is active. Defaults to False.
|
|
718
|
+
power (float, optional): The port output power. Defaults to 1.
|
|
719
|
+
Z0 (float, optional): The port impedance. Defaults to 50.
|
|
720
|
+
"""
|
|
721
|
+
super().__init__(face)
|
|
722
|
+
|
|
723
|
+
if width is None:
|
|
724
|
+
if not isinstance(face, GeoObject):
|
|
725
|
+
raise ValueError(f'The width, height and direction must be defined. Information cannot be extracted from {face}')
|
|
726
|
+
width, height, direction, Idirection = face._data('width','height','vdir', 'idir')
|
|
727
|
+
if width is None or height is None or direction is None:
|
|
728
|
+
raise ValueError(f'The width, height and direction could not be extracted from {face}')
|
|
729
|
+
|
|
730
|
+
logger.debug(f'Lumped port: width={1000*width:.1f}mm, height={1000*height:.1f}mm, direction={direction}')
|
|
731
|
+
self.port_number: int= port_number
|
|
732
|
+
self.active: bool = active
|
|
733
|
+
|
|
734
|
+
self.power: float = power
|
|
735
|
+
self.Z0: float = Z0
|
|
736
|
+
|
|
737
|
+
self._field_amplitude: np.ndarray = None
|
|
738
|
+
self.width: float = width
|
|
739
|
+
self.height: float = height
|
|
740
|
+
self.Vdirection: Axis = direction
|
|
741
|
+
self.Idirection: Axis = Idirection
|
|
742
|
+
self.type = 'TEM'
|
|
743
|
+
|
|
744
|
+
logger.info('Constructing coordinate system from normal port')
|
|
745
|
+
self.cs = Axis(self.selection.normal).construct_cs()
|
|
746
|
+
|
|
747
|
+
self.vintline: Line = None
|
|
748
|
+
self.v_integration = True
|
|
749
|
+
self.iintline: Line = None
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
def surfZ(self) -> float:
|
|
753
|
+
"""The surface sheet impedance for the lumped port
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
float: The surface sheet impedance
|
|
757
|
+
"""
|
|
758
|
+
return self.Z0*self.width/self.height
|
|
759
|
+
|
|
760
|
+
@property
|
|
761
|
+
def voltage(self) -> float:
|
|
762
|
+
"""The Port voltage required for the provided output power (time average)
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
float: The port voltage
|
|
766
|
+
"""
|
|
767
|
+
return np.sqrt(2*self.power*self.Z0)
|
|
768
|
+
|
|
769
|
+
def get_basis(self) -> np.ndarray:
|
|
770
|
+
return self.cs._basis
|
|
771
|
+
|
|
772
|
+
def get_beta(self, k0: float) -> float:
|
|
773
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
774
|
+
|
|
775
|
+
return k0
|
|
776
|
+
|
|
777
|
+
def get_gamma(self, k0: float) -> complex:
|
|
778
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
k0 (float): The free space propagation constant.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
complex: The γ-constant
|
|
785
|
+
"""
|
|
786
|
+
return 1j*k0*376.730313412/self.surfZ
|
|
787
|
+
|
|
788
|
+
def get_Uinc(self, x_local, y_local, k0) -> np.ndarray:
|
|
789
|
+
Emag = -1j*2*k0 * self.voltage/self.height * (376.730313412/self.surfZ)
|
|
790
|
+
return Emag*self.port_mode_3d(x_local, y_local, k0)
|
|
791
|
+
|
|
792
|
+
def port_mode_3d(self,
|
|
793
|
+
x_local: np.ndarray,
|
|
794
|
+
y_local: np.ndarray,
|
|
795
|
+
k0: float,
|
|
796
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
797
|
+
''' Compute the port mode E-field in local coordinates (XY) + Z out of plane.'''
|
|
798
|
+
|
|
799
|
+
px, py, pz = self.cs.in_local_basis(*self.Vdirection.np)
|
|
800
|
+
|
|
801
|
+
Ex = px*np.ones_like(x_local)
|
|
802
|
+
Ey = py*np.ones_like(x_local)
|
|
803
|
+
Ez = pz*np.ones_like(x_local)
|
|
804
|
+
Exyz = np.array([Ex, Ey, Ez])
|
|
805
|
+
return Exyz
|
|
806
|
+
|
|
807
|
+
def port_mode_3d_global(self,
|
|
808
|
+
x_global: np.ndarray,
|
|
809
|
+
y_global: np.ndarray,
|
|
810
|
+
z_global: np.ndarray,
|
|
811
|
+
k0: float,
|
|
812
|
+
which: Literal['E','H'] = 'E') -> np.ndarray:
|
|
813
|
+
"""Computes the port-mode field in global coordinates.
|
|
814
|
+
|
|
815
|
+
The mode field will be evaluated at x,y,z coordinates but projected onto the local 2D coordinate system.
|
|
816
|
+
Additionally, the "which" parameter may be used to request the H-field. This parameter is not always supported.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
x_global (np.ndarray): The X-coordinate
|
|
820
|
+
y_global (np.ndarray): The Y-coordinate
|
|
821
|
+
z_global (np.ndarray): The Z-coordinate
|
|
822
|
+
k0 (float): The free space propagation constant
|
|
823
|
+
which (Literal["E","H"], optional): Which field to return. Defaults to 'E'.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
np.ndarray: The E-field in (3,N) indexing.
|
|
827
|
+
"""
|
|
828
|
+
xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
|
|
829
|
+
Ex, Ey, Ez = self.port_mode_3d(xl, yl, k0)
|
|
830
|
+
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
831
|
+
return np.array([Exg, Eyg, Ezg])
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class LumpedElement(RobinBC):
|
|
835
|
+
|
|
836
|
+
_include_stiff: bool = True
|
|
837
|
+
_include_mass: bool = False
|
|
838
|
+
_include_force: bool = False
|
|
839
|
+
|
|
840
|
+
def __init__(self,
|
|
841
|
+
face: FaceSelection | GeoSurface,
|
|
842
|
+
impedance_function: Callable = None,
|
|
843
|
+
width: float = None,
|
|
844
|
+
height: float = None,
|
|
845
|
+
):
|
|
846
|
+
"""Generates a lumped power boundary condition.
|
|
847
|
+
|
|
848
|
+
The lumped port boundary condition assumes a uniform E-field along the "direction" axis.
|
|
849
|
+
The port with and height must be provided manually in meters. The height is the size
|
|
850
|
+
in the "direction" axis along which the potential is imposed. The width dimension
|
|
851
|
+
is orthogonal to that. For a rectangular face its the width and for a cyllindrical face
|
|
852
|
+
its the circumpherance.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
face (FaceSelection, GeoSurface): The port surface
|
|
856
|
+
port_number (int): The port number
|
|
857
|
+
width (float): The port width (meters).
|
|
858
|
+
height (float): The port height (meters).
|
|
859
|
+
direction (Axis): The port direction as an Axis object (em.Axis(..) or em.ZAX)
|
|
860
|
+
active (bool, optional): Whether the port is active. Defaults to False.
|
|
861
|
+
power (float, optional): The port output power. Defaults to 1.
|
|
862
|
+
Z0 (float, optional): The port impedance. Defaults to 50.
|
|
863
|
+
"""
|
|
864
|
+
super().__init__(face)
|
|
865
|
+
|
|
866
|
+
if width is None:
|
|
867
|
+
if not isinstance(face, GeoObject):
|
|
868
|
+
raise ValueError(f'The width, height and direction must be defined. Information cannot be extracted from {face}')
|
|
869
|
+
width, height, impedance_function = face._data('width','height','func')
|
|
870
|
+
if width is None or height is None or impedance_function is None:
|
|
871
|
+
raise ValueError(f'The width, height and impedance function could not be extracted from {face}')
|
|
872
|
+
|
|
873
|
+
logger.debug(f'Lumped port: width={1000*width:.1f}mm, height={1000*height:.1f}mm')
|
|
874
|
+
|
|
875
|
+
self.Z0: Callable = impedance_function
|
|
876
|
+
|
|
877
|
+
self._field_amplitude: np.ndarray = None
|
|
878
|
+
self.width: float = width
|
|
879
|
+
self.height: float = height
|
|
880
|
+
|
|
881
|
+
logger.info('Constructing coordinate system from normal port')
|
|
882
|
+
self.cs = Axis(self.selection.normal).construct_cs()
|
|
883
|
+
|
|
884
|
+
self.vintline: Line = None
|
|
885
|
+
self.v_integration = True
|
|
886
|
+
self.iintline: Line = None
|
|
887
|
+
|
|
888
|
+
def surfZ(self, k0: float) -> float:
|
|
889
|
+
"""The surface sheet impedance for the lumped port
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
float: The surface sheet impedance
|
|
893
|
+
"""
|
|
894
|
+
Z0 = self.Z0(k0*299792458/(2*np.pi))*self.width/self.height
|
|
895
|
+
return Z0
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def get_basis(self) -> np.ndarray:
|
|
899
|
+
return self.cs._basis
|
|
900
|
+
|
|
901
|
+
def get_beta(self, k0: float) -> float:
|
|
902
|
+
''' Return the out of plane propagation constant. βz.'''
|
|
903
|
+
|
|
904
|
+
return k0
|
|
905
|
+
|
|
906
|
+
def get_gamma(self, k0: float) -> complex:
|
|
907
|
+
"""Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
k0 (float): The free space propagation constant.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
complex: The γ-constant
|
|
914
|
+
"""
|
|
915
|
+
return 1j*k0*376.730313412/self.surfZ(k0)
|