emerge 1.1.0__py3-none-any.whl → 1.2.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 +2 -1
- emerge/_emerge/const.py +2 -1
- emerge/_emerge/elements/ned2_interp.py +122 -42
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/operations.py +20 -0
- emerge/_emerge/geo/pcb.py +34 -16
- emerge/_emerge/geo/shapes.py +1 -0
- emerge/_emerge/geo/step.py +177 -41
- emerge/_emerge/geometry.py +166 -19
- emerge/_emerge/material.py +2 -0
- emerge/_emerge/mesh3d.py +1 -3
- emerge/_emerge/mesher.py +33 -9
- emerge/_emerge/mth/common_functions.py +1 -1
- emerge/_emerge/mth/optimized.py +2 -2
- emerge/_emerge/physics/microwave/adaptive_mesh.py +451 -100
- emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
- emerge/_emerge/physics/microwave/microwave_3d.py +2 -2
- emerge/_emerge/physics/microwave/microwave_bc.py +1 -0
- emerge/_emerge/physics/microwave/microwave_data.py +95 -5
- emerge/_emerge/plot/pyvista/display.py +42 -15
- emerge/_emerge/plot/simple_plots.py +116 -4
- emerge/_emerge/selection.py +17 -2
- emerge/_emerge/simmodel.py +111 -49
- emerge/_emerge/solve_interfaces/cudss_interface.py +1 -1
- emerge/_emerge/solver.py +3 -3
- emerge/plot.py +1 -1
- {emerge-1.1.0.dist-info → emerge-1.2.0.dist-info}/METADATA +1 -1
- {emerge-1.1.0.dist-info → emerge-1.2.0.dist-info}/RECORD +31 -31
- {emerge-1.1.0.dist-info → emerge-1.2.0.dist-info}/WHEEL +0 -0
- {emerge-1.1.0.dist-info → emerge-1.2.0.dist-info}/entry_points.txt +0 -0
- {emerge-1.1.0.dist-info → emerge-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -273,13 +273,17 @@ class Assembler:
|
|
|
273
273
|
|
|
274
274
|
logger.debug('Implementing PEC Boundary Conditions.')
|
|
275
275
|
pec_ids: list[int] = []
|
|
276
|
+
pec_tris: list[int] = []
|
|
276
277
|
|
|
277
278
|
# Conductivity above al imit, consider it all PEC
|
|
278
279
|
ipec = 0
|
|
280
|
+
|
|
279
281
|
for itet in range(field.n_tets):
|
|
280
282
|
if cond[0,0,itet] > self.settings.mw_3d_peclim:
|
|
281
283
|
ipec+=1
|
|
282
284
|
pec_ids.extend(field.tet_to_field[:,itet])
|
|
285
|
+
for tri in field.mesh.tet_to_tri[:,itet]:
|
|
286
|
+
pec_tris.append(tri)
|
|
283
287
|
if ipec>0:
|
|
284
288
|
logger.trace(f'Extended PEC with {ipec} tets with a conductivity > {self.settings.mw_3d_peclim}.')
|
|
285
289
|
|
|
@@ -295,9 +299,12 @@ class Assembler:
|
|
|
295
299
|
eids = field.edge_to_field[:, ii]
|
|
296
300
|
pec_ids.extend(list(eids))
|
|
297
301
|
|
|
302
|
+
|
|
298
303
|
for ii in tri_ids:
|
|
299
304
|
tids = field.tri_to_field[:, ii]
|
|
300
305
|
pec_ids.extend(list(tids))
|
|
306
|
+
|
|
307
|
+
pec_tris.extend(tri_ids)
|
|
301
308
|
|
|
302
309
|
|
|
303
310
|
############################################################
|
|
@@ -412,7 +419,8 @@ class Assembler:
|
|
|
412
419
|
|
|
413
420
|
simjob.port_vectors = port_vectors
|
|
414
421
|
simjob.solve_ids = solve_ids
|
|
415
|
-
|
|
422
|
+
simjob._pec_tris = pec_tris
|
|
423
|
+
|
|
416
424
|
if has_periodic:
|
|
417
425
|
simjob.P = Pmat
|
|
418
426
|
simjob.Pd = Pmat.getH()
|
|
@@ -876,7 +876,7 @@ class Microwave3D:
|
|
|
876
876
|
def _run_adaptive_mesh(self,
|
|
877
877
|
iteration: int,
|
|
878
878
|
frequency: float,
|
|
879
|
-
automatic_modal_analysis: bool = True) -> MWData:
|
|
879
|
+
automatic_modal_analysis: bool = True) -> tuple[MWData, list[int]]:
|
|
880
880
|
"""Executes a frequency domain study
|
|
881
881
|
|
|
882
882
|
The study is distributed over "n_workers" workers.
|
|
@@ -965,7 +965,7 @@ class Microwave3D:
|
|
|
965
965
|
self.solveroutine.reset()
|
|
966
966
|
### Compute S-parameters and return
|
|
967
967
|
self._post_process([job,], [mats,])
|
|
968
|
-
return self.data
|
|
968
|
+
return self.data, job._pec_tris
|
|
969
969
|
|
|
970
970
|
def eigenmode(self, search_frequency: float,
|
|
971
971
|
nmodes: int = 6,
|
|
@@ -773,6 +773,7 @@ class RectangularWaveguide(PortBC):
|
|
|
773
773
|
logger.info(' - Constructing coordinate system from normal port')
|
|
774
774
|
self.cs = Axis(self.selection.normal).construct_cs()
|
|
775
775
|
logger.debug(f' - Port CS: {self.cs}')
|
|
776
|
+
|
|
776
777
|
def get_basis(self) -> np.ndarray:
|
|
777
778
|
return self.cs._basis
|
|
778
779
|
|
|
@@ -667,13 +667,51 @@ class MWField:
|
|
|
667
667
|
return sum([self.excitation[mode.port_number]*self._fields[mode.port_number] for mode in self.port_modes]) # type: ignore
|
|
668
668
|
|
|
669
669
|
def set_field_vector(self) -> None:
|
|
670
|
-
"""Defines the default excitation coefficients for the current dataset"""
|
|
670
|
+
"""Defines the default excitation coefficients for the current dataset as an excitation of only port 1."""
|
|
671
671
|
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
672
672
|
self.excitation[self.port_modes[0].port_number] = 1.0 + 0j
|
|
673
673
|
|
|
674
|
-
def excite_port(self, number: int) -> None:
|
|
674
|
+
def excite_port(self, number: int, excitation: complex = 1.0 + 0.0j) -> None:
|
|
675
|
+
"""Excite a single port provided by a given port number
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
number (int): The port number to excite
|
|
679
|
+
coefficient (complex): The port excitation. Defaults to 1.0 + 0.0j
|
|
680
|
+
"""
|
|
681
|
+
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
682
|
+
self.excitation[self.port_modes[number-1].port_number] = excitation
|
|
683
|
+
|
|
684
|
+
def set_excitations(self, *excitations: complex) -> None:
|
|
685
|
+
"""Set bulk port excitations by an ordered array of excitation coefficients.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
*complex: A sequence of complex numbers
|
|
689
|
+
"""
|
|
675
690
|
self.excitation = {key: 0.0 for key in self._fields.keys()}
|
|
676
|
-
|
|
691
|
+
for iport, coeff in enumerate(excitations):
|
|
692
|
+
self.excitation[self.port_modes[iport].port_number] = coeff
|
|
693
|
+
|
|
694
|
+
def combine_ports(self, p1: int, p2: int) -> MWField:
|
|
695
|
+
"""Combines ports p1 and p2 into a cifferential and common mode port respectively.
|
|
696
|
+
|
|
697
|
+
The p1 index becomes the differential mode port
|
|
698
|
+
The p2 index becomes the common mode port
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
p1 (int): The first port number
|
|
702
|
+
p2 (int): The second port number
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
MWField: _description_
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
fp1 = self._fields[p1]
|
|
709
|
+
fp2 = self._fields[p2]
|
|
710
|
+
|
|
711
|
+
self._fields[p1] = (fp1-fp2)/np.sqrt(2)
|
|
712
|
+
self._fields[p2] = (fp1+fp2)/np.sqrt(2)
|
|
713
|
+
|
|
714
|
+
return self
|
|
677
715
|
|
|
678
716
|
@property
|
|
679
717
|
def EH(self) -> tuple[np.ndarray, np.ndarray]:
|
|
@@ -720,7 +758,9 @@ class MWField:
|
|
|
720
758
|
xf = xs.flatten()
|
|
721
759
|
yf = ys.flatten()
|
|
722
760
|
zf = zs.flatten()
|
|
761
|
+
logger.debug(f'Interpolating {xf.shape[0]} field points')
|
|
723
762
|
Ex, Ey, Ez = self.basis.interpolate(self._field, xf, yf, zf, usenan=usenan)
|
|
763
|
+
logger.debug('E Interpolation complete')
|
|
724
764
|
self.Ex = Ex.reshape(shp)
|
|
725
765
|
self.Ey = Ey.reshape(shp)
|
|
726
766
|
self.Ez = Ez.reshape(shp)
|
|
@@ -728,6 +768,7 @@ class MWField:
|
|
|
728
768
|
|
|
729
769
|
constants = 1/ (-1j*2*np.pi*self.freq*(self._dur*MU0) )
|
|
730
770
|
Hx, Hy, Hz = self.basis.interpolate_curl(self._field, xf, yf, zf, constants, usenan=usenan)
|
|
771
|
+
logger.debug('H Interpolation complete')
|
|
731
772
|
ids = self.basis.interpolate_index(xf, yf, zf)
|
|
732
773
|
|
|
733
774
|
self.er = self._der[ids].reshape(shp)
|
|
@@ -743,14 +784,15 @@ class MWField:
|
|
|
743
784
|
|
|
744
785
|
return field
|
|
745
786
|
|
|
746
|
-
def _solution_quality(self) -> tuple[np.ndarray, np.ndarray]:
|
|
787
|
+
def _solution_quality(self, solve_ids: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
747
788
|
from .adaptive_mesh import compute_error_estimate
|
|
748
789
|
|
|
749
|
-
error_tet, max_elem_size = compute_error_estimate(self)
|
|
790
|
+
error_tet, max_elem_size = compute_error_estimate(self, solve_ids)
|
|
750
791
|
return error_tet, max_elem_size
|
|
751
792
|
|
|
752
793
|
def boundary(self,
|
|
753
794
|
selection: FaceSelection) -> EHField:
|
|
795
|
+
""" Interpolate the field on the node coordinates of the surface."""
|
|
754
796
|
nodes = self.mesh.nodes
|
|
755
797
|
x = nodes[0,:]
|
|
756
798
|
y = nodes[1,:]
|
|
@@ -1146,6 +1188,54 @@ class MWScalarNdim:
|
|
|
1146
1188
|
def S(self, i1: int, i2: int) -> np.ndarray:
|
|
1147
1189
|
return self.Sp[...,self._portmap[i1], self._portmap[i2]]
|
|
1148
1190
|
|
|
1191
|
+
def combine_ports(self, p1: int, p2: int) -> MWScalarNdim:
|
|
1192
|
+
"""Combine ports p1 and p2 into a differential and common mode port respectively.
|
|
1193
|
+
|
|
1194
|
+
The p1 index becomes the differential mode port
|
|
1195
|
+
The p2 index becomes the common mode port
|
|
1196
|
+
|
|
1197
|
+
Args:
|
|
1198
|
+
p1 (int): The first port number
|
|
1199
|
+
p2 (int): The second port number
|
|
1200
|
+
|
|
1201
|
+
Returns:
|
|
1202
|
+
MWScalarNdim: _description_
|
|
1203
|
+
"""
|
|
1204
|
+
if p1==p2:
|
|
1205
|
+
raise ValueError('p1 and p2 must be different port numbers')
|
|
1206
|
+
|
|
1207
|
+
F, N, _ = self.Sp.shape
|
|
1208
|
+
p1 = p1-1
|
|
1209
|
+
p2 = p2-1
|
|
1210
|
+
|
|
1211
|
+
if not (0 <= p1 < N and 0 <= p2 < N):
|
|
1212
|
+
raise IndexError(f'Ports {p1+1} or {p2+1} are out of range {N}')
|
|
1213
|
+
|
|
1214
|
+
Sout = self.Sp.copy()
|
|
1215
|
+
ii, jj = p1, p2
|
|
1216
|
+
idx = np.ones(N, dtype=np.bool)
|
|
1217
|
+
idx[[ii,jj]] = False
|
|
1218
|
+
others = np.nonzero(idx)[0]
|
|
1219
|
+
isqrt2 = 1.0 / np.sqrt(2.0)
|
|
1220
|
+
|
|
1221
|
+
Sout[:, others, ii] = (self.Sp[:, others, ii] - self.Sp[:, others, jj]) * isqrt2
|
|
1222
|
+
Sout[:, others, jj] = (self.Sp[:, others, ii] + self.Sp[:, others, jj]) * isqrt2
|
|
1223
|
+
Sout[:, ii, others] = (self.Sp[:, ii, others] - self.Sp[:, jj, others]) * isqrt2
|
|
1224
|
+
Sout[:, jj, others] = (self.Sp[:, ii, others] + self.Sp[:, jj, others]) * isqrt2
|
|
1225
|
+
|
|
1226
|
+
Sii = self.Sp[:, ii, ii]
|
|
1227
|
+
Sij = self.Sp[:, ii, jj]
|
|
1228
|
+
Sji = self.Sp[:, jj, ii]
|
|
1229
|
+
Sjj = self.Sp[:, jj, jj]
|
|
1230
|
+
|
|
1231
|
+
Sout[:, ii, ii] = 0.5 *(Sii - Sij - Sji + Sjj)
|
|
1232
|
+
Sout[:, ii, jj] = 0.5 *(Sii + Sij - Sji - Sjj)
|
|
1233
|
+
Sout[:, jj, ii] = 0.5 *(Sii - Sij + Sji - Sjj)
|
|
1234
|
+
Sout[:, jj, jj] = 0.5 *(Sii + Sij + Sji + Sjj)
|
|
1235
|
+
|
|
1236
|
+
self.Sp = Sout
|
|
1237
|
+
|
|
1238
|
+
return self
|
|
1149
1239
|
@property
|
|
1150
1240
|
def Smat(self) -> np.ndarray:
|
|
1151
1241
|
"""Returns the full S-matrix
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program; if not, see
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
17
18
|
from __future__ import annotations
|
|
18
19
|
from ...mesh3d import Mesh3D
|
|
19
20
|
from ...simstate import SimState
|
|
@@ -43,21 +44,43 @@ cmap_names = Literal['bgy','bgyw','kbc','blues','bmw','bmy','kgy','gray','dimgra
|
|
|
43
44
|
'bkr','bky','coolwarm','gwv','bjy','bwy','cwr','colorwheel','isolum','rainbow','fire',
|
|
44
45
|
'cet_fire','gouldian','kbgyw','cwr','CET_CBL1','CET_CBL3','CET_D1A']
|
|
45
46
|
|
|
46
|
-
EMERGE_AMP = make_colormap(["#1F0061","#
|
|
47
|
+
EMERGE_AMP = make_colormap(["#1F0061","#4218c0","#2849db", "#ff007b", "#ff7c51"], (0.0, 0.15, 0.3, 0.7, 0.9))
|
|
47
48
|
EMERGE_WAVE = make_colormap(["#4ab9ff","#0510B2B8","#3A37466E","#CC0954B9","#ff9036"], (0.0, 0.3, 0.5, 0.7, 1.0))
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
## Cycler class
|
|
52
|
+
|
|
53
|
+
class _Cycler:
|
|
54
|
+
"""Like itertools.cycle(iterable) but with reset(). Materializes the iterable."""
|
|
55
|
+
def __init__(self, iterable):
|
|
56
|
+
self._data = list(iterable)
|
|
57
|
+
self._n = len(self._data)
|
|
58
|
+
self._i = 0
|
|
59
|
+
|
|
60
|
+
def __iter__(self):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __next__(self):
|
|
64
|
+
if self._n == 0:
|
|
65
|
+
raise StopIteration
|
|
66
|
+
item = self._data[self._i]
|
|
67
|
+
self._i += 1
|
|
68
|
+
if self._i == self._n:
|
|
69
|
+
self._i = 0
|
|
70
|
+
return item
|
|
71
|
+
|
|
72
|
+
def reset(self):
|
|
73
|
+
self._i = 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
C_CYCLE = _Cycler([
|
|
51
77
|
"#0000aa",
|
|
52
78
|
"#aa0000",
|
|
53
79
|
"#009900",
|
|
54
80
|
"#990099",
|
|
55
81
|
"#994400",
|
|
56
82
|
"#005588"
|
|
57
|
-
]
|
|
58
|
-
return cycle(colors)
|
|
59
|
-
|
|
60
|
-
C_CYCLE = _gen_c_cycle()
|
|
83
|
+
])
|
|
61
84
|
|
|
62
85
|
class _RunState:
|
|
63
86
|
|
|
@@ -242,6 +265,7 @@ class PVDisplay(BaseDisplay):
|
|
|
242
265
|
self._stop: bool = False
|
|
243
266
|
self._objs: list[_AnimObject] = []
|
|
244
267
|
self._do_animate: bool = False
|
|
268
|
+
self._animate_next: bool = False
|
|
245
269
|
self._closed_via_x: bool = False
|
|
246
270
|
self._Nsteps: int = 0
|
|
247
271
|
self._fps: int = 25
|
|
@@ -334,7 +358,9 @@ class PVDisplay(BaseDisplay):
|
|
|
334
358
|
self._plot = pv.Plotter()
|
|
335
359
|
self._stop = False
|
|
336
360
|
self._objs = []
|
|
337
|
-
|
|
361
|
+
self._animate_next = False
|
|
362
|
+
self._reset_cbar()
|
|
363
|
+
C_CYCLE.reset()
|
|
338
364
|
|
|
339
365
|
def _close_callback(self, arg):
|
|
340
366
|
"""The private callback function that stops the animation.
|
|
@@ -403,6 +429,7 @@ class PVDisplay(BaseDisplay):
|
|
|
403
429
|
print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
|
|
404
430
|
self._Nsteps = Nsteps
|
|
405
431
|
self._fps = fps
|
|
432
|
+
self._animate_next = True
|
|
406
433
|
self._do_animate = True
|
|
407
434
|
return self
|
|
408
435
|
|
|
@@ -518,7 +545,7 @@ class PVDisplay(BaseDisplay):
|
|
|
518
545
|
|
|
519
546
|
self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=2, show_edges=True)
|
|
520
547
|
|
|
521
|
-
if
|
|
548
|
+
if label:
|
|
522
549
|
points = []
|
|
523
550
|
labels = []
|
|
524
551
|
for dt in obj.dimtags:
|
|
@@ -691,14 +718,14 @@ class PVDisplay(BaseDisplay):
|
|
|
691
718
|
actor = self._plot.add_mesh(grid_no_nan, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
|
|
692
719
|
|
|
693
720
|
|
|
694
|
-
if self.
|
|
721
|
+
if self._animate_next:
|
|
695
722
|
def on_update(obj: _AnimObject, phi: complex):
|
|
696
723
|
field_anim = obj.T(np.real(obj.field * phi))
|
|
697
724
|
obj.grid[name] = field_anim
|
|
698
725
|
obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
|
|
699
726
|
#obj.fgrid replace with thresholded scalar data.
|
|
700
727
|
self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
|
|
701
|
-
|
|
728
|
+
self._animate_next = False
|
|
702
729
|
self._reset_cbar()
|
|
703
730
|
|
|
704
731
|
def add_boundary_field(self,
|
|
@@ -773,13 +800,13 @@ class PVDisplay(BaseDisplay):
|
|
|
773
800
|
kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
|
|
774
801
|
actor = self._plot.add_mesh(grid, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
|
|
775
802
|
|
|
776
|
-
if self.
|
|
803
|
+
if self._animate_next:
|
|
777
804
|
def on_update(obj: _AnimObject, phi: complex):
|
|
778
805
|
field_anim = obj.T(np.real(obj.field * phi))
|
|
779
806
|
obj.grid[name] = field_anim
|
|
780
807
|
#obj.fgrid replace with thresholded scalar data.
|
|
781
808
|
self._objs.append(_AnimObject(field_flat, T, grid, grid, actor, on_update))
|
|
782
|
-
|
|
809
|
+
self._animate_next = False
|
|
783
810
|
self._reset_cbar()
|
|
784
811
|
|
|
785
812
|
def add_title(self, title: str) -> None:
|
|
@@ -921,7 +948,7 @@ class PVDisplay(BaseDisplay):
|
|
|
921
948
|
|
|
922
949
|
actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
|
|
923
950
|
|
|
924
|
-
if self.
|
|
951
|
+
if self._animate_next:
|
|
925
952
|
def on_update(obj: _AnimObject, phi: complex):
|
|
926
953
|
new_vals = obj.T(np.real(obj.field * phi))
|
|
927
954
|
obj.grid['anim'] = new_vals
|
|
@@ -929,7 +956,7 @@ class PVDisplay(BaseDisplay):
|
|
|
929
956
|
obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
|
|
930
957
|
|
|
931
958
|
self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
|
|
932
|
-
|
|
959
|
+
self._animate_next = False
|
|
933
960
|
self._reset_cbar()
|
|
934
961
|
|
|
935
962
|
def _add_aux_items(self) -> None:
|
|
@@ -495,11 +495,11 @@ def plot_sp(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
|
|
|
495
495
|
if isinstance(levelindicator, (int, float)) and levelindicator is not None:
|
|
496
496
|
lvl = levelindicator
|
|
497
497
|
fcross = hintersections(f, SdB, lvl)
|
|
498
|
-
for
|
|
498
|
+
for freqs in fcross:
|
|
499
499
|
ax_mag.annotate(
|
|
500
|
-
f"{str(
|
|
501
|
-
xy=(
|
|
502
|
-
xytext=(
|
|
500
|
+
f"{str(freqs)[:4]}{xunit}",
|
|
501
|
+
xy=(freqs, lvl),
|
|
502
|
+
xytext=(freqs + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
|
|
503
503
|
arrowprops=dict(facecolor="black", width=1, headwidth=5),
|
|
504
504
|
)
|
|
505
505
|
if fill_areas is not None:
|
|
@@ -541,6 +541,118 @@ def plot_sp(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
|
|
|
541
541
|
|
|
542
542
|
return fig, ax_mag, ax_phase
|
|
543
543
|
|
|
544
|
+
def plot_vswr(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
|
|
545
|
+
swrlim=[1, 5],
|
|
546
|
+
xunit="GHz",
|
|
547
|
+
levelindicator: int | float | None = None,
|
|
548
|
+
fill_areas: list[tuple] | None = None,
|
|
549
|
+
spec_area: list[tuple[float,...]] | None = None,
|
|
550
|
+
labels: list[str] | None = None,
|
|
551
|
+
linestyles: list[str] | None = None,
|
|
552
|
+
colorcycle: list[int] | None = None,
|
|
553
|
+
filename: str | None = None,
|
|
554
|
+
show_plot: bool = True,
|
|
555
|
+
figdata: tuple | None = None) -> tuple[plt.Figure, plt.Axes, plt.Axes]:
|
|
556
|
+
"""Plot S-parameters in VSWR
|
|
557
|
+
|
|
558
|
+
One may provide:
|
|
559
|
+
- A single frequency with a single S-parameter
|
|
560
|
+
- A single frequency with a list of S-parameters
|
|
561
|
+
- A list of frequencies with a list of S-parameters
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
f (np.ndarray | list[np.ndarray]): Frequency vector or list of frequencies
|
|
565
|
+
S (list[np.ndarray] | np.ndarray): S-parameters to plot (list or single array)
|
|
566
|
+
swrlim (list, optional): VSWR y-axis limit. Defaults to [1, 5].
|
|
567
|
+
xunit (str, optional): Frequency unit. Defaults to "GHz".
|
|
568
|
+
levelindicator (int | float, optional): Level at which annotation arrows will be added. Defaults to None.
|
|
569
|
+
fill_areas (list[tuple], optional): Regions to fill (fmin, fmax). Defaults to None.
|
|
570
|
+
spec_area (list[tuple[float]], optional): _description_. Defaults to None.
|
|
571
|
+
labels (list[str], optional): A lists of labels to use. Defaults to None.
|
|
572
|
+
linestyles (list[str], optional): The linestyle to use (list or single string). Defaults to None.
|
|
573
|
+
colorcycle (list[int], optional): A list of colors to use. Defaults to None.
|
|
574
|
+
filename (str, optional): The filename (will automatically save). Defaults to None.
|
|
575
|
+
show_plot (bool, optional): If or not to show the resulting plot. Defaults to True.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
if not isinstance(S, list):
|
|
579
|
+
Ss = [S]
|
|
580
|
+
else:
|
|
581
|
+
Ss = S
|
|
582
|
+
|
|
583
|
+
if not isinstance(f, list):
|
|
584
|
+
fs = [f for _ in Ss]
|
|
585
|
+
else:
|
|
586
|
+
fs = f
|
|
587
|
+
|
|
588
|
+
if linestyles is None:
|
|
589
|
+
linestyles = ['-' for _ in S]
|
|
590
|
+
|
|
591
|
+
if colorcycle is None:
|
|
592
|
+
colorcycle = [i for i, S in enumerate(S)]
|
|
593
|
+
|
|
594
|
+
unitdivider: dict[str, float] = {"MHz": 1e6, "GHz": 1e9, "kHz": 1e3}
|
|
595
|
+
|
|
596
|
+
fs = [f / unitdivider[xunit] for f in fs]
|
|
597
|
+
|
|
598
|
+
if figdata is None:
|
|
599
|
+
# Create two subplots: one for magnitude and one for phase
|
|
600
|
+
fig, ax_swr = plt.subplots()
|
|
601
|
+
fig.subplots_adjust(hspace=0.3)
|
|
602
|
+
else:
|
|
603
|
+
fig, ax_swr = figdata
|
|
604
|
+
maxy = 5
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
for f, s, ls, cid in zip(fs, Ss, linestyles, colorcycle):
|
|
608
|
+
# Calculate and plot magnitude in dB
|
|
609
|
+
SWR = np.divide((1 + abs(s)), (1 - abs(s)))
|
|
610
|
+
ax_swr.plot(f, SWR, label="VSWR", linestyle=ls, color=EMERGE_COLORS[cid % len(EMERGE_COLORS)])
|
|
611
|
+
if np.max(SWR) > maxy:
|
|
612
|
+
maxy = np.max(SWR)
|
|
613
|
+
|
|
614
|
+
# Annotate level indicators if specified
|
|
615
|
+
if isinstance(levelindicator, (int, float)) and levelindicator is not None:
|
|
616
|
+
lvl = levelindicator
|
|
617
|
+
fcross = hintersections(f, SWR, lvl)
|
|
618
|
+
for fa in fcross:
|
|
619
|
+
ax_swr.annotate(
|
|
620
|
+
f"{str(fa)[:4]}{xunit}",
|
|
621
|
+
xy=(fa, lvl),
|
|
622
|
+
xytext=(fa + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
|
|
623
|
+
arrowprops=dict(facecolor="black", width=1, headwidth=5),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
if fill_areas is not None:
|
|
628
|
+
for fmin, fmax in fill_areas:
|
|
629
|
+
f1 = fmin / unitdivider[xunit]
|
|
630
|
+
f2 = fmax / unitdivider[xunit]
|
|
631
|
+
ax_swr.fill_between([f1, f2], swrlim[0], swrlim[1], color='grey', alpha= 0.2)
|
|
632
|
+
|
|
633
|
+
if spec_area is not None:
|
|
634
|
+
for fmin, fmax, vmin, vmax in spec_area:
|
|
635
|
+
f1 = fmin / unitdivider[xunit]
|
|
636
|
+
f2 = fmax / unitdivider[xunit]
|
|
637
|
+
ax_swr.fill_between([f1, f2], vmin,vmax, color='red', alpha=0.2)
|
|
638
|
+
|
|
639
|
+
# Configure magnitude plot (ax_swr)
|
|
640
|
+
fmin = min([min(f) for f in fs])
|
|
641
|
+
fmax = max([max(f) for f in fs])
|
|
642
|
+
ax_swr.set_ylabel("VSWR")
|
|
643
|
+
ax_swr.set_xlabel(f"Frequency ({xunit})")
|
|
644
|
+
ax_swr.axis([fmin, fmax, swrlim[0], max(maxy*1.1,swrlim[1])]) # type: ignore
|
|
645
|
+
ax_swr.xaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
646
|
+
ax_swr.yaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
647
|
+
|
|
648
|
+
if labels is not None:
|
|
649
|
+
ax_swr.legend(labels)
|
|
650
|
+
if show_plot:
|
|
651
|
+
plt.show()
|
|
652
|
+
if filename is not None:
|
|
653
|
+
fig.savefig(filename)
|
|
654
|
+
|
|
655
|
+
return fig, ax_swr
|
|
544
656
|
|
|
545
657
|
def plot_ff(
|
|
546
658
|
theta: np.ndarray | list[np.ndarray],
|
emerge/_emerge/selection.py
CHANGED
|
@@ -21,6 +21,9 @@ import numpy as np
|
|
|
21
21
|
from .cs import Axis, CoordinateSystem, _parse_vector, Plane
|
|
22
22
|
from typing import Callable, TypeVar, Iterable, Any
|
|
23
23
|
|
|
24
|
+
class SelectionError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
24
27
|
def align_rectangle_frame(pts3d: np.ndarray, normal: np.ndarray) -> dict[str, Any]:
|
|
25
28
|
"""Tries to find a rectangle as convex-hull of a set of points with a given normal vector.
|
|
26
29
|
|
|
@@ -174,7 +177,7 @@ class Selection:
|
|
|
174
177
|
"""
|
|
175
178
|
dim: int = -1
|
|
176
179
|
def __init__(self, tags: list[int] | set[int] | tuple[int] | None = None):
|
|
177
|
-
|
|
180
|
+
self.name: str = 'Selection'
|
|
178
181
|
self._tags: set[int] = set()
|
|
179
182
|
|
|
180
183
|
if tags is not None:
|
|
@@ -252,7 +255,19 @@ class Selection:
|
|
|
252
255
|
maxy = max(maxy, y1)
|
|
253
256
|
maxz = max(maxz, z1)
|
|
254
257
|
return (minx, miny, minz), (maxx, maxy, maxz)
|
|
255
|
-
|
|
258
|
+
|
|
259
|
+
def _named(self, name: str) -> Selection:
|
|
260
|
+
"""Sets the name of the selection and returns it
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
name (str): The name of the selection
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Selection: The same selection object
|
|
267
|
+
"""
|
|
268
|
+
self.name = name
|
|
269
|
+
return self
|
|
270
|
+
|
|
256
271
|
def exclude(self, xyz_excl_function: Callable = lambda x,y,z: True, plane: Plane | None = None, axis: Axis | None = None) -> Selection:
|
|
257
272
|
"""Exclude points by evaluating a function(x,y,z)-> bool
|
|
258
273
|
|