emerge 0.5.5__py3-none-any.whl → 0.6.0__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 +4 -1
- emerge/_emerge/cs.py +2 -2
- emerge/_emerge/elements/ned2_interp.py +21 -26
- emerge/_emerge/elements/nedleg2.py +27 -45
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/modeler.py +2 -2
- emerge/_emerge/geo/pcb.py +4 -4
- emerge/_emerge/geo/shapes.py +37 -14
- emerge/_emerge/geometry.py +27 -1
- emerge/_emerge/howto.py +9 -9
- emerge/_emerge/material.py +1 -0
- emerge/_emerge/mesh3d.py +63 -14
- emerge/_emerge/mesher.py +7 -4
- emerge/_emerge/mth/optimized.py +30 -0
- emerge/_emerge/periodic.py +46 -16
- emerge/_emerge/physics/microwave/assembly/assembler.py +4 -21
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +23 -19
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +465 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +59 -18
- emerge/_emerge/physics/microwave/microwave_3d.py +38 -186
- emerge/_emerge/physics/microwave/microwave_bc.py +101 -35
- emerge/_emerge/physics/microwave/microwave_data.py +1 -1
- emerge/_emerge/plot/pyvista/display.py +40 -7
- emerge/_emerge/plot/pyvista/display_settings.py +1 -0
- emerge/_emerge/plot/simple_plots.py +159 -27
- emerge/_emerge/projects/_gen_base.txt +2 -2
- emerge/_emerge/projects/_load_base.txt +1 -1
- emerge/_emerge/simmodel.py +22 -7
- emerge/_emerge/solve_interfaces/cudss_interface.py +44 -2
- emerge/_emerge/solve_interfaces/pardiso_interface.py +1 -0
- emerge/_emerge/solver.py +26 -19
- emerge/ext.py +4 -0
- emerge/lib.py +1 -1
- {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/METADATA +6 -4
- {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/RECORD +38 -37
- emerge/_emerge/elements/legrange2.py +0 -172
- {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/WHEEL +0 -0
- {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/entry_points.txt +0 -0
- {emerge-0.5.5.dist-info → emerge-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -24,6 +24,7 @@ from ...elements.nedelec2 import Nedelec2
|
|
|
24
24
|
from ...solver import DEFAULT_ROUTINE, SolveRoutine
|
|
25
25
|
from ...system import called_from_main_function
|
|
26
26
|
from ...selection import FaceSelection
|
|
27
|
+
from ...mth.optimized import compute_distances
|
|
27
28
|
from .microwave_bc import MWBoundaryConditionSet, PEC, ModalPort, LumpedPort, PortBC
|
|
28
29
|
from .microwave_data import MWData
|
|
29
30
|
from .assembly.assembler import Assembler
|
|
@@ -60,7 +61,7 @@ def run_job_multi(job: SimJob) -> SimJob:
|
|
|
60
61
|
return job
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def _dimstring(data: list[float]) -> str:
|
|
64
|
+
def _dimstring(data: list[float] | np.ndarray) -> str:
|
|
64
65
|
"""A String formatter for dimensions in millimeters
|
|
65
66
|
|
|
66
67
|
Args:
|
|
@@ -99,6 +100,21 @@ def shortest_path(xyz1: np.ndarray, xyz2: np.ndarray, Npts: int) -> np.ndarray:
|
|
|
99
100
|
|
|
100
101
|
return path
|
|
101
102
|
|
|
103
|
+
def _pick_central(vertices: np.ndarray) -> np.ndarray:
|
|
104
|
+
"""Computes the coordinate in the vertex set that has the shortest square distance to all other points.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
vertices (np.ndarray): The set of coordinates [3,:]
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
np.ndarray: The most central point
|
|
112
|
+
"""
|
|
113
|
+
Ds = compute_distances(vertices[0,:], vertices[1,:], vertices[2,:])
|
|
114
|
+
sumDs = np.sum(Ds**2, axis=1)
|
|
115
|
+
id_central = np.argwhere(sumDs==np.min(sumDs)).flatten()[0]
|
|
116
|
+
return vertices[:, id_central].squeeze()
|
|
117
|
+
|
|
102
118
|
class Microwave3D:
|
|
103
119
|
"""The Electrodynamics time harmonic physics class.
|
|
104
120
|
|
|
@@ -302,8 +318,8 @@ class Microwave3D:
|
|
|
302
318
|
dotprod = xs*field_axis[0] + ys*field_axis[1] + zs*field_axis[2]
|
|
303
319
|
|
|
304
320
|
start_id = points[np.argwhere(dotprod == np.min(dotprod))]
|
|
305
|
-
|
|
306
|
-
start =
|
|
321
|
+
|
|
322
|
+
start = _pick_central(self.mesh.nodes[:,start_id.flatten()])
|
|
307
323
|
logger.info(f'Starting node = {_dimstring(start)}')
|
|
308
324
|
end = start + port.Vdirection.np*port.height
|
|
309
325
|
|
|
@@ -493,6 +509,7 @@ class Microwave3D:
|
|
|
493
509
|
if TEM:
|
|
494
510
|
target_kz = ermean*urmean*1.1*k0
|
|
495
511
|
else:
|
|
512
|
+
|
|
496
513
|
target_kz = ermean*urmean*0.7*k0
|
|
497
514
|
|
|
498
515
|
|
|
@@ -514,7 +531,8 @@ class Microwave3D:
|
|
|
514
531
|
Emode[solve_ids] = np.squeeze(eigenmode)
|
|
515
532
|
Emode = Emode * np.exp(-1j*np.angle(np.max(Emode)))
|
|
516
533
|
|
|
517
|
-
|
|
534
|
+
beta_base = np.emath.sqrt(-eigen_values[i])
|
|
535
|
+
beta = min(k0*np.sqrt(ermax*urmax), beta_base)
|
|
518
536
|
|
|
519
537
|
residuals = -1
|
|
520
538
|
|
|
@@ -559,14 +577,14 @@ class Microwave3D:
|
|
|
559
577
|
logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
|
|
560
578
|
return None
|
|
561
579
|
|
|
562
|
-
def
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
580
|
+
def run_sweep(self,
|
|
581
|
+
parallel: bool = False,
|
|
582
|
+
njobs: int = 2,
|
|
583
|
+
harddisc_threshold: int | None = None,
|
|
584
|
+
harddisc_path: str = 'EMergeSparse',
|
|
585
|
+
frequency_groups: int = -1,
|
|
586
|
+
multi_processing: bool = False,
|
|
587
|
+
automatic_modal_analysis: bool = True) -> MWData:
|
|
570
588
|
"""Executes a frequency domain study
|
|
571
589
|
|
|
572
590
|
The study is distributed over "njobs" workers.
|
|
@@ -999,179 +1017,13 @@ class Microwave3D:
|
|
|
999
1017
|
mode_p = sparam_mode_power(self.mesh.nodes, tri_vertices, bc, k0, const, 5)
|
|
1000
1018
|
return field_p, mode_p
|
|
1001
1019
|
|
|
1002
|
-
# def frequency_domain_single(self, automatic_modal_analysis: bool = False) -> MWData:
|
|
1003
|
-
# """Execute a frequency domain study without distributed frequency sweep.
|
|
1004
|
-
|
|
1005
|
-
# Args:
|
|
1006
|
-
# automatic_modal_analysis (bool, optional): Automatically compute port modes. Defaults to False.
|
|
1007
|
-
|
|
1008
|
-
# Raises:
|
|
1009
|
-
# SimulationError: _description_
|
|
1010
|
-
|
|
1011
|
-
# Returns:
|
|
1012
|
-
# MWSimData: The Simulation data.
|
|
1013
|
-
# """
|
|
1014
|
-
# T0 = time.time()
|
|
1015
|
-
# mesh = self.mesh
|
|
1016
|
-
# if self.bc._initialized is False:
|
|
1017
|
-
# raise SimulationError('Cannot run a modal analysis because no boundary conditions have been assigned.')
|
|
1018
|
-
|
|
1019
|
-
# self._initialize_field()
|
|
1020
|
-
# self._initialize_bc_data()
|
|
1021
|
-
|
|
1022
|
-
# er = self.mesh.retreive(lambda mat,x,y,z: mat.fer3d_mat(x,y,z), self.mesher.volumes)
|
|
1023
|
-
# ur = self.mesh.retreive(lambda mat,x,y,z: mat.fur3d_mat(x,y,z), self.mesher.volumes)
|
|
1024
|
-
# cond = self.mesh.retreive(lambda mat,x,y,z: mat.cond, self.mesher.volumes)[0,0,:]
|
|
1025
|
-
|
|
1026
|
-
# ertri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
1027
|
-
# urtri = np.zeros((3,3,self.mesh.n_tris), dtype=np.complex128)
|
|
1028
|
-
# condtri = np.zeros((self.mesh.n_tris,), dtype=np.complex128)
|
|
1029
|
-
|
|
1030
|
-
# for itri in range(self.mesh.n_tris):
|
|
1031
|
-
# itet = self.mesh.tri_to_tet[0,itri]
|
|
1032
|
-
# ertri[:,:,itri] = er[:,:,itet]
|
|
1033
|
-
# urtri[:,:,itri] = ur[:,:,itet]
|
|
1034
|
-
# condtri[itri] = cond[itet]
|
|
1035
|
-
|
|
1036
|
-
# #### Port settings
|
|
1037
|
-
|
|
1038
|
-
# all_ports = self.bc.oftype(PortBC)
|
|
1039
|
-
# port_numbers = [port.port_number for port in all_ports]
|
|
1040
|
-
|
|
1041
|
-
# ##### FOR PORT SWEEP SET ALL ACTIVE TO FALSE. THIS SHOULD BE FIXED LATER
|
|
1042
|
-
# ### COMPUTE WHICH TETS ARE CONNECTED TO PORT INDICES
|
|
1043
|
-
|
|
1044
|
-
# all_port_tets = []
|
|
1045
|
-
# for port in all_ports:
|
|
1046
|
-
# port.active=False
|
|
1047
|
-
|
|
1048
|
-
# all_port_tets = mesh.get_face_tets(*[port.tags for port in all_ports])
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
# logger.debug(f'Starting the simulation of {len(self.frequencies)} frequency points.')
|
|
1052
|
-
|
|
1053
|
-
# # ITERATE OVER FREQUENCIES
|
|
1054
|
-
# for freq in self.frequencies:
|
|
1055
|
-
# logger.info(f'Simulation frequency = {freq/1e9:.3f} GHz')
|
|
1056
|
-
|
|
1057
|
-
# # Assembling matrix problem
|
|
1058
|
-
# if automatic_modal_analysis:
|
|
1059
|
-
# self._compute_modes(freq)
|
|
1060
|
-
|
|
1061
|
-
# job = self.assembler.assemble_freq_matrix(self.basis, er, ur, cond, self.bc.boundary_conditions, freq, cache_matrices=self.cache_matrices)
|
|
1062
|
-
|
|
1063
|
-
# logger.debug(f'Routine: {self.solveroutine}')
|
|
1064
1020
|
|
|
1065
|
-
|
|
1066
|
-
#
|
|
1067
|
-
|
|
1021
|
+
############################################################
|
|
1022
|
+
# DEPRICATED FUNCTIONS #
|
|
1023
|
+
############################################################
|
|
1068
1024
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
# scalardata.init_sp(port_numbers)
|
|
1075
|
-
# scalardata.freq = freq
|
|
1076
|
-
# scalardata.k0 = k0
|
|
1077
|
-
|
|
1078
|
-
# fielddata = self.data.field.new(freq=freq, **self._params)
|
|
1079
|
-
# fielddata.freq = freq
|
|
1080
|
-
# fielddata.er = np.squeeze(er[0,0,:])
|
|
1081
|
-
# fielddata.ur = np.squeeze(ur[0,0,:])
|
|
1082
|
-
|
|
1083
|
-
# # Recording port information
|
|
1084
|
-
# for i, port in enumerate(all_ports):
|
|
1085
|
-
# fielddata.add_port_properties(port.port_number,
|
|
1086
|
-
# mode_number=port.mode_number,
|
|
1087
|
-
# k0 = k0,
|
|
1088
|
-
# beta = port.get_beta(k0),
|
|
1089
|
-
# Z0 = port.portZ0(k0),
|
|
1090
|
-
# Pout= port.power)
|
|
1091
|
-
|
|
1092
|
-
# for active_port in all_ports:
|
|
1093
|
-
|
|
1094
|
-
# active_port.active = True
|
|
1095
|
-
# solution = job._fields[active_port.port_number]
|
|
1096
|
-
|
|
1097
|
-
# fielddata._fields[active_port.port_number] = solution # TODO: THIS IS VERY FRAIL
|
|
1098
|
-
# fielddata.basis = self.basis
|
|
1099
|
-
|
|
1100
|
-
# # Compute the S-parameters
|
|
1101
|
-
# # Define the field interpolation function
|
|
1102
|
-
# fieldf = self.basis.interpolate_Ef(solution, tetids=all_port_tets)
|
|
1103
|
-
|
|
1104
|
-
# # Active port power
|
|
1105
|
-
# logger.debug('Active ports:')
|
|
1106
|
-
# tris = mesh.get_triangles(active_port.tags)
|
|
1107
|
-
# tri_vertices = mesh.tris[:,tris]
|
|
1108
|
-
# pfield, pmode = self._compute_s_data(active_port, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
|
|
1109
|
-
# logger.debug(f'Field Amplitude = {np.abs(pfield):.3f}, Excitation = {np.abs(pmode):.2f}')
|
|
1110
|
-
# Pout = pmode
|
|
1111
|
-
|
|
1112
|
-
# #Passive ports
|
|
1113
|
-
# logger.debug('Passive ports:')
|
|
1114
|
-
# for bc in all_ports:
|
|
1115
|
-
# tris = mesh.get_triangles(bc.tags)
|
|
1116
|
-
# tri_vertices = mesh.tris[:,tris]
|
|
1117
|
-
# pfield, pmode = self._compute_s_data(bc, fieldf, tri_vertices, k0, ertri[:,:,tris], urtri[:,:,tris])
|
|
1118
|
-
# logger.debug(f'Field amplitude = {np.abs(pfield):.3f}, Excitation= {np.abs(pmode):.2f}')
|
|
1119
|
-
# scalardata.write_S(bc.port_number, active_port.port_number, pfield/Pout)
|
|
1120
|
-
|
|
1121
|
-
# active_port.active=False
|
|
1122
|
-
|
|
1123
|
-
# fielddata.set_field_vector()
|
|
1124
|
-
|
|
1125
|
-
# logger.info('Simulation Complete!')
|
|
1126
|
-
# T2 = time.time()
|
|
1127
|
-
# logger.info(f'Elapsed time = {(T2-T0):.2f} seconds.')
|
|
1128
|
-
# return self.data
|
|
1129
|
-
## DEPRICATED
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
#
|
|
1134
|
-
# def eigenmode(self, mesh: Mesh3D, solver = None, num_sols: int = 6):
|
|
1135
|
-
# if solver is None:
|
|
1136
|
-
# logger.info('Defaulting to BiCGStab.')
|
|
1137
|
-
# solver = sparse.linalg.eigs
|
|
1138
|
-
|
|
1139
|
-
# if self.order == 1:
|
|
1140
|
-
# logger.info('Detected 1st order elements.')
|
|
1141
|
-
# from ...elements.nedelec1.assembly import assemble_eig_matrix
|
|
1142
|
-
# ft = FieldType.VEC_LIN
|
|
1143
|
-
|
|
1144
|
-
# elif self.order == 2:
|
|
1145
|
-
# logger.info('Detected 2nd order elements.')
|
|
1146
|
-
# from ...elements.nedelec2.assembly import assemble_eig_matrix_E
|
|
1147
|
-
# ft = FieldType.VEC_QUAD
|
|
1148
|
-
|
|
1149
|
-
# er = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fer3d(x,y,z))
|
|
1150
|
-
# ur = self.mesh.retreive(mesh.centers, lambda mat,x,y,z: mat.fur3d(x,y,z))
|
|
1151
|
-
|
|
1152
|
-
# dataset = Dataset3D(mesh, self.frequencies, 0, ft)
|
|
1153
|
-
# dataset.er = er
|
|
1154
|
-
# dataset.ur = ur
|
|
1155
|
-
# logger.info('Solving eigenmodes.')
|
|
1156
|
-
|
|
1157
|
-
# f_target = self.frequencies[0]
|
|
1158
|
-
# sigma = (2 * np.pi * f_target / 299792458)**2
|
|
1159
|
-
|
|
1160
|
-
# A, B, solvenodes = assemble_eig_matrix(mesh, er, ur, self.boundary_conditions)
|
|
1161
|
-
|
|
1162
|
-
# A = A[np.ix_(solvenodes, solvenodes)]
|
|
1163
|
-
# B = B[np.ix_(solvenodes, solvenodes)]
|
|
1164
|
-
# #A = sparse.csc_matrix(A)
|
|
1165
|
-
# #B = sparse.csc_matrix(B)
|
|
1166
|
-
|
|
1167
|
-
# w, v = sparse.linalg.eigs(A, k=num_sols, M=B, sigma=sigma, which='LM')
|
|
1168
|
-
|
|
1169
|
-
# logger.info(f'Eigenvalues: {np.sqrt(w)*299792458/(2*np.pi) * 1e-9} GHz')
|
|
1170
|
-
|
|
1171
|
-
# Esol = np.zeros((num_sols, mesh.nfield), dtype=np.complex128)
|
|
1172
|
-
|
|
1173
|
-
# Esol[:, solvenodes] = v.T
|
|
1174
|
-
|
|
1175
|
-
# dataset.set_efield(Esol)
|
|
1176
|
-
|
|
1177
|
-
# self.basis = dataset
|
|
1025
|
+
def frequency_domain(self, *args, **kwargs):
|
|
1026
|
+
"""DEPRICATED VERSION: Use run_sweep() instead.
|
|
1027
|
+
"""
|
|
1028
|
+
logger.warning('This function is depricated. Please use run_sweep() instead')
|
|
1029
|
+
self.run_sweep(*args, **kwargs)
|
|
@@ -20,7 +20,7 @@ import numpy as np
|
|
|
20
20
|
from loguru import logger
|
|
21
21
|
from typing import Callable, Literal
|
|
22
22
|
from ...selection import Selection, FaceSelection
|
|
23
|
-
from ...cs import CoordinateSystem, Axis, GCS
|
|
23
|
+
from ...cs import CoordinateSystem, Axis, GCS, _parse_axis
|
|
24
24
|
from ...coord import Line
|
|
25
25
|
from ...geometry import GeoSurface, GeoObject
|
|
26
26
|
from dataclasses import dataclass
|
|
@@ -28,7 +28,23 @@ from collections import defaultdict
|
|
|
28
28
|
from ...bc import BoundaryCondition, BoundaryConditionSet, Periodic
|
|
29
29
|
from ...periodic import PeriodicCell, HexCell, RectCell
|
|
30
30
|
from ...material import Material
|
|
31
|
-
from ...const import Z0, C0,
|
|
31
|
+
from ...const import Z0, C0, EPS0, MU0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
############################################################
|
|
36
|
+
# UTILITY FUNCTIONS #
|
|
37
|
+
############################################################
|
|
38
|
+
|
|
39
|
+
def _inner_product(function: Callable, x: np.ndarray, y: np.ndarray, z: np.ndarray, ax: Axis) -> float:
|
|
40
|
+
Exyz = function(x,y,z)
|
|
41
|
+
return np.sum(Exyz[0,:]*ax.x + Exyz[1,:]*ax.y + Exyz[2,:]*ax.z)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
############################################################
|
|
46
|
+
# MAIN BC MANAGER CLASS #
|
|
47
|
+
############################################################
|
|
32
48
|
|
|
33
49
|
class MWBoundaryConditionSet(BoundaryConditionSet):
|
|
34
50
|
|
|
@@ -83,6 +99,10 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
|
|
|
83
99
|
return port
|
|
84
100
|
|
|
85
101
|
|
|
102
|
+
############################################################
|
|
103
|
+
# BOUNDARY CONDITIONS #
|
|
104
|
+
############################################################
|
|
105
|
+
|
|
86
106
|
|
|
87
107
|
class PEC(BoundaryCondition):
|
|
88
108
|
|
|
@@ -313,7 +333,7 @@ class PortMode:
|
|
|
313
333
|
self.energy = np.mean(np.abs(self.modefield)**2)
|
|
314
334
|
|
|
315
335
|
def __str__(self):
|
|
316
|
-
return f'PortMode(k0={self.k0}, beta={self.beta}
|
|
336
|
+
return f'PortMode(k0={self.k0}, beta={self.beta}({self.neff:.3f}))'
|
|
317
337
|
|
|
318
338
|
def set_power(self, power: complex) -> None:
|
|
319
339
|
self.norm_factor = np.sqrt(1/np.abs(power))
|
|
@@ -371,8 +391,8 @@ class FloquetPort(PortBC):
|
|
|
371
391
|
"""
|
|
372
392
|
return 1j*self.get_beta(k0)
|
|
373
393
|
|
|
374
|
-
def get_Uinc(self,
|
|
375
|
-
return -2*1j*self.get_beta(k0)*self.
|
|
394
|
+
def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
|
|
395
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
|
|
376
396
|
|
|
377
397
|
def port_mode_3d(self,
|
|
378
398
|
x_local: np.ndarray,
|
|
@@ -449,7 +469,7 @@ class ModalPort(PortBC):
|
|
|
449
469
|
self.port_number: int= port_number
|
|
450
470
|
self.active: bool = active
|
|
451
471
|
self.power: float = power
|
|
452
|
-
|
|
472
|
+
self.alignment_vectors: list[Axis] = []
|
|
453
473
|
|
|
454
474
|
self.selected_mode: int = 0
|
|
455
475
|
self.modes: dict[float, list[PortMode]] = defaultdict(list)
|
|
@@ -459,6 +479,7 @@ class ModalPort(PortBC):
|
|
|
459
479
|
self.initialized: bool = False
|
|
460
480
|
self._first_k0: float | None = None
|
|
461
481
|
self._last_k0: float | None = None
|
|
482
|
+
|
|
462
483
|
|
|
463
484
|
if cs is None:
|
|
464
485
|
logger.info('Constructing coordinate system from normal port')
|
|
@@ -474,6 +495,17 @@ class ModalPort(PortBC):
|
|
|
474
495
|
def modetype(self, k0: float) -> Literal['TEM','TE','TM']:
|
|
475
496
|
return self.get_mode(k0).modetype
|
|
476
497
|
|
|
498
|
+
def align_modes(self, *axes: tuple | np.ndarray | Axis) -> None:
|
|
499
|
+
"""Set a reriees of Axis objects that define a sequence of mode field
|
|
500
|
+
alignments.
|
|
501
|
+
|
|
502
|
+
The modes will be sorted to maximize the inner product: |∬ E(x,y) · ax dS|
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
*axes (tuple, np.ndarray, Axis): The alignment vectors.
|
|
506
|
+
"""
|
|
507
|
+
self.alignment_vectors = [_parse_axis(ax) for ax in axes]
|
|
508
|
+
|
|
477
509
|
@property
|
|
478
510
|
def nmodes(self) -> int:
|
|
479
511
|
if self._last_k0 is None:
|
|
@@ -488,6 +520,26 @@ class ModalPort(PortBC):
|
|
|
488
520
|
def sort_modes(self) -> None:
|
|
489
521
|
"""Sorts the port modes based on total energy
|
|
490
522
|
"""
|
|
523
|
+
|
|
524
|
+
if len(self.alignment_vectors) > 0:
|
|
525
|
+
logger.trace(f'Sorting modes based on alignment vectors: {self.alignment_vectors}')
|
|
526
|
+
X, Y, Z = self.selection.sample(5)
|
|
527
|
+
X = X.flatten()
|
|
528
|
+
Y = Y.flatten()
|
|
529
|
+
Z = Z.flatten()
|
|
530
|
+
for k0, modes in self.modes.items():
|
|
531
|
+
logger.trace(f'Aligning modes for k0={k0:.3f} rad/m')
|
|
532
|
+
new_modes = []
|
|
533
|
+
for ax in self.alignment_vectors:
|
|
534
|
+
logger.trace(f'.mode vector {ax}')
|
|
535
|
+
integrals = [_inner_product(m.E_function, X, Y, Z, ax) for m in modes]
|
|
536
|
+
integral, opt_mode = sorted([pair for pair in zip(integrals, modes)], key=lambda x: abs(x[0]), reverse=True)[0]
|
|
537
|
+
opt_mode.polarity = np.sign(integral.real)
|
|
538
|
+
logger.trace(f'Optimal mode = {opt_mode} ({integral}), polarization alignment = {opt_mode.polarity}')
|
|
539
|
+
new_modes.append(opt_mode)
|
|
540
|
+
|
|
541
|
+
self.modes[k0] = new_modes
|
|
542
|
+
return
|
|
491
543
|
for k0, modes in self.modes.items():
|
|
492
544
|
self.modes[k0] = sorted(modes, key=lambda m: m.energy, reverse=True)
|
|
493
545
|
|
|
@@ -578,8 +630,8 @@ class ModalPort(PortBC):
|
|
|
578
630
|
def get_gamma(self, k0: float) -> complex:
|
|
579
631
|
return 1j*self.get_beta(k0)
|
|
580
632
|
|
|
581
|
-
def get_Uinc(self,
|
|
582
|
-
return -2*1j*self.get_beta(k0)*self.
|
|
633
|
+
def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0) -> np.ndarray:
|
|
634
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
|
|
583
635
|
|
|
584
636
|
def port_mode_3d(self,
|
|
585
637
|
x_local: np.ndarray,
|
|
@@ -691,8 +743,8 @@ class RectangularWaveguide(PortBC):
|
|
|
691
743
|
"""
|
|
692
744
|
return 1j*self.get_beta(k0)
|
|
693
745
|
|
|
694
|
-
def get_Uinc(self,
|
|
695
|
-
return -2*1j*self.get_beta(k0)*self.
|
|
746
|
+
def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
|
|
747
|
+
return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
|
|
696
748
|
|
|
697
749
|
def port_mode_3d(self,
|
|
698
750
|
x_local: np.ndarray,
|
|
@@ -826,9 +878,9 @@ class LumpedPort(PortBC):
|
|
|
826
878
|
"""
|
|
827
879
|
return 1j*k0*Z0/self.surfZ
|
|
828
880
|
|
|
829
|
-
def get_Uinc(self,
|
|
881
|
+
def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0) -> np.ndarray:
|
|
830
882
|
Emag = -1j*2*k0 * self.voltage/self.height * (Z0/self.surfZ)
|
|
831
|
-
return Emag*self.
|
|
883
|
+
return Emag*self.port_mode_3d_global(x_global, y_global, z_global, k0)
|
|
832
884
|
|
|
833
885
|
def port_mode_3d(self,
|
|
834
886
|
x_local: np.ndarray,
|
|
@@ -871,7 +923,6 @@ class LumpedPort(PortBC):
|
|
|
871
923
|
Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
|
|
872
924
|
return np.array([Exg, Eyg, Ezg])
|
|
873
925
|
|
|
874
|
-
|
|
875
926
|
class LumpedElement(RobinBC):
|
|
876
927
|
|
|
877
928
|
_include_stiff: bool = True
|
|
@@ -948,8 +999,6 @@ class LumpedElement(RobinBC):
|
|
|
948
999
|
"""
|
|
949
1000
|
return 1j*k0*Z0/self.surfZ(k0)
|
|
950
1001
|
|
|
951
|
-
|
|
952
|
-
|
|
953
1002
|
class SurfaceImpedance(RobinBC):
|
|
954
1003
|
|
|
955
1004
|
_include_stiff: bool = True
|
|
@@ -959,29 +1008,45 @@ class SurfaceImpedance(RobinBC):
|
|
|
959
1008
|
def __init__(self,
|
|
960
1009
|
face: FaceSelection | GeoSurface,
|
|
961
1010
|
material: Material | None = None,
|
|
1011
|
+
surface_conductance: float | None = None,
|
|
1012
|
+
surface_roughness: float = 0,
|
|
1013
|
+
sr_model: Literal['Hammerstad-Jensen'] = 'Hammerstad-Jensen',
|
|
962
1014
|
):
|
|
963
|
-
"""Generates a
|
|
964
|
-
|
|
965
|
-
The
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1015
|
+
"""Generates a SurfaceImpedance bounary condition.
|
|
1016
|
+
|
|
1017
|
+
The surface impedance model treats a 2D surface selection as a finite conductor. It is not
|
|
1018
|
+
intended to be used for dielectric materials.
|
|
1019
|
+
|
|
1020
|
+
The surface resistivity is computed based on the material properties: σ, ε and μ.
|
|
1021
|
+
|
|
1022
|
+
The user may also supply the surface condutivity directly.
|
|
1023
|
+
|
|
1024
|
+
Optionally, a surface roughness in meters RMS may be supplied. In the current implementation
|
|
1025
|
+
The Hammersstad-Jensen model is used increasing the resistivity by a factor (1 + 2/π tan⁻¹(1.4(Δ/δ)²).
|
|
970
1026
|
|
|
971
1027
|
Args:
|
|
972
|
-
face (FaceSelection
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
active (bool, optional): Whether the port is active. Defaults to False.
|
|
978
|
-
power (float, optional): The port output power. Defaults to 1.
|
|
979
|
-
Z0 (float, optional): The port impedance. Defaults to 50.
|
|
1028
|
+
face (FaceSelection | GeoSurface): The face to apply this condition to.
|
|
1029
|
+
material (Material | None, optional): The matrial to assign. Defaults to None.
|
|
1030
|
+
surface_conductance (float | None, optional): The specific bulk conductivity to use. Defaults to None.
|
|
1031
|
+
surface_roughness (float, optional): The surface roughness. Defaults to 0.
|
|
1032
|
+
sr_model (Literal['Hammerstad, optional): The surface roughness model. Defaults to 'Hammerstad-Jensen'.
|
|
980
1033
|
"""
|
|
981
1034
|
super().__init__(face)
|
|
982
1035
|
|
|
983
|
-
self.
|
|
984
|
-
|
|
1036
|
+
self._material: Material | None = material
|
|
1037
|
+
self._mur: float | complex = 1.0
|
|
1038
|
+
self._epsr: float | complex = 1.0
|
|
1039
|
+
|
|
1040
|
+
self.sigma: float = 0.0
|
|
1041
|
+
if material is not None:
|
|
1042
|
+
self.sigma = material.cond
|
|
1043
|
+
self._mur = material.ur
|
|
1044
|
+
self._epsr = material.er
|
|
1045
|
+
if surface_conductance is not None:
|
|
1046
|
+
self.sigma = surface_conductance
|
|
1047
|
+
|
|
1048
|
+
self._sr: float = surface_roughness
|
|
1049
|
+
self._sr_model: str = sr_model
|
|
985
1050
|
|
|
986
1051
|
def get_basis(self) -> np.ndarray | None:
|
|
987
1052
|
return None
|
|
@@ -1004,9 +1069,10 @@ class SurfaceImpedance(RobinBC):
|
|
|
1004
1069
|
complex: The γ-constant
|
|
1005
1070
|
"""
|
|
1006
1071
|
w0 = k0*C0
|
|
1007
|
-
sigma = self.
|
|
1072
|
+
sigma = self.sigma
|
|
1008
1073
|
rho = 1/sigma
|
|
1009
|
-
d_skin = (2*rho/(w0*MU0) * ((1+(w0*EPS0*rho)**2)**0.5 + rho*w0*EPS0))**0.5
|
|
1010
|
-
d_skin = (2*rho/(w0*MU0))**0.5
|
|
1074
|
+
d_skin = (2*rho/(w0*MU0*self._mur) * ((1+(w0*EPS0*self._epsr*rho)**2)**0.5 + rho*w0*EPS0*self._epsr))**0.5
|
|
1011
1075
|
R = rho/d_skin
|
|
1076
|
+
if self._sr_model=='Hammerstad-Jensen' and self._sr > 0.0:
|
|
1077
|
+
R = R * (1 + 2/np.pi * np.arctan(1.4*(self._sr/d_skin)**2))
|
|
1012
1078
|
return 1j*k0*Z0/R
|
|
@@ -777,7 +777,7 @@ class MWField:
|
|
|
777
777
|
syms (list[Literal['Ex','Ey','Ez','Hx','Hy','Hz']], optional): E and H-plane symmetry planes where Ex is E-symmetry in x=0. Defaults to []
|
|
778
778
|
|
|
779
779
|
Returns:
|
|
780
|
-
tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
780
|
+
tuple[np.ndarray, np.ndarray, np.ndarray]: Angles (N,), E(3,N), H(3,N)
|
|
781
781
|
"""
|
|
782
782
|
refdir = _parse_axis(ref_direction).np
|
|
783
783
|
plane_normal_parsed = _parse_axis(plane_normal).np
|
|
@@ -213,8 +213,18 @@ class PVDisplay(BaseDisplay):
|
|
|
213
213
|
self._plot.add_key_event("m", self.activate_ruler) # type: ignore
|
|
214
214
|
self._plot.add_key_event("f", self.activate_object) # type: ignore
|
|
215
215
|
|
|
216
|
-
self._ctr: int = 0
|
|
217
|
-
|
|
216
|
+
self._ctr: int = 0
|
|
217
|
+
|
|
218
|
+
self.camera_position = (1, -1, 1) # +X, +Z, -Y
|
|
219
|
+
|
|
220
|
+
def _update_camera(self):
|
|
221
|
+
x,y,z = self._plot.camera.position
|
|
222
|
+
d = (x**2+y**2+z**2)**(0.5)
|
|
223
|
+
px, py, pz = self.camera_position
|
|
224
|
+
dp = (px**2+py**2+pz**2)**(0.5)
|
|
225
|
+
px, py, pz = px/dp, py/dp, pz/dp
|
|
226
|
+
self._plot.camera.position = (d*px, d*py, d*pz)
|
|
227
|
+
|
|
218
228
|
def activate_ruler(self):
|
|
219
229
|
self._plot.disable_picking()
|
|
220
230
|
self._selector.turn_off()
|
|
@@ -228,6 +238,7 @@ class PVDisplay(BaseDisplay):
|
|
|
228
238
|
def show(self):
|
|
229
239
|
""" Shows the Pyvista display. """
|
|
230
240
|
self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
|
|
241
|
+
self._update_camera()
|
|
231
242
|
self._add_aux_items()
|
|
232
243
|
if self._do_animate:
|
|
233
244
|
self._plot.show(auto_close=False, interactive_update=True, before_close_callback=self._close_callback)
|
|
@@ -300,7 +311,25 @@ class PVDisplay(BaseDisplay):
|
|
|
300
311
|
cells[:,1:] = self._mesh.tets[:,tets].T
|
|
301
312
|
cells[:,0] = 4
|
|
302
313
|
celltypes = np.full(ntets, fill_value=pv.CellType.TETRA, dtype=np.uint8)
|
|
303
|
-
points = self._mesh.nodes.T
|
|
314
|
+
points = self._mesh.nodes.copy().T
|
|
315
|
+
return pv.UnstructuredGrid(cells, celltypes, points)
|
|
316
|
+
|
|
317
|
+
def _volume_edges(self, obj: GeoObject | Selection) -> pv.UnstructuredGrid:
|
|
318
|
+
"""Adds the edges of objects
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
obj (DomainSelection | None, optional): _description_. Defaults to None.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
pv.UnstructuredGrid: The unstrutured grid object
|
|
325
|
+
"""
|
|
326
|
+
edge_ids = self._mesh.domain_edges(obj.dimtags)
|
|
327
|
+
nedges = edge_ids.shape[0]
|
|
328
|
+
cells = np.zeros((nedges,3), dtype=np.int64)
|
|
329
|
+
cells[:,1:] = self._mesh.edges[:,edge_ids].T
|
|
330
|
+
cells[:,0] = 2
|
|
331
|
+
celltypes = np.full(nedges, fill_value=pv.CellType.CUBIC_LINE, dtype=np.uint8)
|
|
332
|
+
points = self._mesh.nodes.copy().T
|
|
304
333
|
return pv.UnstructuredGrid(cells, celltypes, points)
|
|
305
334
|
|
|
306
335
|
def mesh_surface(self, surface: FaceSelection) -> pv.UnstructuredGrid:
|
|
@@ -310,7 +339,8 @@ class PVDisplay(BaseDisplay):
|
|
|
310
339
|
cells[:,1:] = self._mesh.tris[:,tris].T
|
|
311
340
|
cells[:,0] = 3
|
|
312
341
|
celltypes = np.full(ntris, fill_value=pv.CellType.TRIANGLE, dtype=np.uint8)
|
|
313
|
-
points = self._mesh.nodes.T
|
|
342
|
+
points = self._mesh.nodes.copy().T
|
|
343
|
+
points[:,2] += self.set.z_boost
|
|
314
344
|
return pv.UnstructuredGrid(cells, celltypes, points)
|
|
315
345
|
|
|
316
346
|
def mesh(self, obj: GeoObject | Selection | Iterable) -> pv.UnstructuredGrid | None:
|
|
@@ -328,8 +358,10 @@ class PVDisplay(BaseDisplay):
|
|
|
328
358
|
|
|
329
359
|
## OBLIGATORY METHODS
|
|
330
360
|
def add_object(self, obj: GeoObject | Selection, *args, **kwargs):
|
|
331
|
-
kwargs = setdefault(kwargs, color=obj.color_rgb, opacity=obj.opacity, silhouette=
|
|
332
|
-
|
|
361
|
+
kwargs = setdefault(kwargs, color=obj.color_rgb, opacity=obj.opacity, silhouette=False, show_edges=False, pickable=True)
|
|
362
|
+
|
|
363
|
+
actor = self._plot.add_mesh(self.mesh(obj), *args, **kwargs)
|
|
364
|
+
self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=1, show_edges=True)
|
|
333
365
|
|
|
334
366
|
def add_scatter(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray):
|
|
335
367
|
"""Adds a scatter point cloud
|
|
@@ -348,7 +380,7 @@ class PVDisplay(BaseDisplay):
|
|
|
348
380
|
XYZ=None,
|
|
349
381
|
field: Literal['E','H'] = 'E',
|
|
350
382
|
k0: float | None = None,
|
|
351
|
-
mode_number: int
|
|
383
|
+
mode_number: int = 0) -> None:
|
|
352
384
|
|
|
353
385
|
if XYZ:
|
|
354
386
|
X,Y,Z = XYZ
|
|
@@ -385,6 +417,7 @@ class PVDisplay(BaseDisplay):
|
|
|
385
417
|
k0 = port.get_mode(0).k0
|
|
386
418
|
else:
|
|
387
419
|
k0 = 1
|
|
420
|
+
port.selected_mode = mode_number
|
|
388
421
|
F = port.port_mode_3d_global(xf,yf,zf,k0, which=field)
|
|
389
422
|
|
|
390
423
|
Fx = F[0,:].reshape(X.shape).T
|