emerge 0.4.7__py3-none-any.whl → 0.4.8__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.8.dist-info}/METADATA +1 -1
- emerge-0.4.8.dist-info/RECORD +78 -0
- emerge-0.4.8.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.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
class PVDisplaySettings:
|
|
3
|
+
|
|
4
|
+
def __init__(self):
|
|
5
|
+
self.draw_xplane: bool = True
|
|
6
|
+
self.draw_yplane: bool = True
|
|
7
|
+
self.draw_zplane: bool = True
|
|
8
|
+
self.draw_xax: bool = True
|
|
9
|
+
self.draw_yax: bool = True
|
|
10
|
+
self.draw_zax: bool = True
|
|
11
|
+
self.plane_ratio: float = 0.5
|
|
12
|
+
self.plane_opacity: float = 0.00
|
|
13
|
+
self.plane_edge_width: float = 1.0
|
|
14
|
+
self.axis_line_width: float = 1.5
|
|
15
|
+
self.show_xgrid: bool = False
|
|
16
|
+
self.show_ygrid: bool = False
|
|
17
|
+
self.show_zgrid: bool = True
|
|
18
|
+
self.grid_line_width: float = 1.0
|
|
19
|
+
self.add_light: bool = False
|
|
20
|
+
self.light_angle: float = (20, -20)
|
|
21
|
+
self.cast_shadows: bool = True
|
|
22
|
+
self.background_bottom: str = "#c0d2e8"
|
|
23
|
+
self.background_top: str = "#ffffff"
|
|
24
|
+
self.grid_line_color: str = "#8e8e8e"
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.ticker as tck
|
|
6
|
+
from typing import (
|
|
7
|
+
Union, Sequence, Callable, List, Optional, Tuple
|
|
8
|
+
)
|
|
9
|
+
from cycler import cycler
|
|
10
|
+
|
|
11
|
+
#_colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
|
|
12
|
+
|
|
13
|
+
EMERGE_COLORS = ["#1A14CE", "#D54A09", "#1F82A6", "#D3107B", "#119D40"]
|
|
14
|
+
EMERGE_CYCLER = cycler(color=EMERGE_COLORS)
|
|
15
|
+
plt.rc('axes', prop_cycle=EMERGE_CYCLER)
|
|
16
|
+
|
|
17
|
+
ggplot_styles = {
|
|
18
|
+
"axes.edgecolor": "000000",
|
|
19
|
+
"axes.facecolor": "F2F2F2",
|
|
20
|
+
"axes.grid": True,
|
|
21
|
+
"axes.grid.which": "both",
|
|
22
|
+
"axes.spines.left": True,
|
|
23
|
+
"axes.spines.right": True,
|
|
24
|
+
"axes.spines.top": True,
|
|
25
|
+
"axes.spines.bottom": True,
|
|
26
|
+
"grid.color": "A0A0A0",
|
|
27
|
+
"grid.linewidth": "0.8",
|
|
28
|
+
"xtick.color": "555555",
|
|
29
|
+
"xtick.major.bottom": True,
|
|
30
|
+
"xtick.minor.bottom": False,
|
|
31
|
+
"ytick.color": "555555",
|
|
32
|
+
"ytick.major.left": True,
|
|
33
|
+
"ytick.minor.left": False,
|
|
34
|
+
"lines.linewidth": 2,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
plt.rcParams.update(ggplot_styles)
|
|
38
|
+
|
|
39
|
+
def _gen_grid(xs: tuple, ys: tuple, N = 201) -> list[np.ndarray]:
|
|
40
|
+
"""Generate a grid of lines for the Smith Chart
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
xs (tuple): Tuple containing the x-axis values
|
|
44
|
+
ys (tuple): Tuple containing the y-axis values
|
|
45
|
+
N (int, optional): Number Used. Defaults to 201.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
list[np.ndarray]: List of lines
|
|
49
|
+
"""
|
|
50
|
+
xgrid = np.arange(xs[0], xs[1]+xs[2], xs[2])
|
|
51
|
+
ygrid = np.arange(ys[0], ys[1]+ys[2], ys[2])
|
|
52
|
+
xsmooth = np.logspace(np.log10(xs[0]+1e-8), np.log10(xs[1]), N)
|
|
53
|
+
ysmooth = np.logspace(np.log10(ys[0]+1e-8), np.log10(ys[1]), N)
|
|
54
|
+
ones = np.ones((N,))
|
|
55
|
+
lines = []
|
|
56
|
+
for x in xgrid:
|
|
57
|
+
lines.append((x*ones, ysmooth))
|
|
58
|
+
lines.append((x*ones, -ysmooth))
|
|
59
|
+
for y in ygrid:
|
|
60
|
+
lines.append((xsmooth, y*ones))
|
|
61
|
+
lines.append((xsmooth, -y*ones))
|
|
62
|
+
|
|
63
|
+
return lines
|
|
64
|
+
|
|
65
|
+
def _generate_grids(orders = (0, 0.5, 1, 2, 5, 10, 50,1e5), N=201) -> list[tuple[np.ndarray, np.ndarray]]:
|
|
66
|
+
"""Generate the grid for the Smith Chart
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
orders (tuple, optional): Locations for Smithchart Lines. Defaults to (0, 0.5, 1, 2, 5, 10, 50,1e5).
|
|
70
|
+
N (int, optional): N distrectization points. Defaults to 201.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
list[tuple[np.ndarray, np.ndarray]]: List of axes lines
|
|
74
|
+
"""
|
|
75
|
+
lines = []
|
|
76
|
+
xgrids = orders
|
|
77
|
+
for o1, o2 in zip(xgrids[:-1], xgrids[1:]):
|
|
78
|
+
step = o2/10
|
|
79
|
+
lines += _gen_grid((0, o2, step), (0, o2, step), N)
|
|
80
|
+
return lines
|
|
81
|
+
|
|
82
|
+
def _smith_transform(lines: list[tuple[np.ndarray, np.ndarray]]) -> list[tuple[np.ndarray, np.ndarray]]:
|
|
83
|
+
"""Executes the Smith Transform on a list of lines
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
lines (list[tuple[np.ndarray, np.ndarray]]): List of lines
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
list[tuple[np.ndarray, np.ndarray]]: List of transformed lines
|
|
90
|
+
"""
|
|
91
|
+
new_lines = []
|
|
92
|
+
for line in lines:
|
|
93
|
+
x, y = line
|
|
94
|
+
z = x + 1j*y
|
|
95
|
+
new_z = (z-1)/(z+1)
|
|
96
|
+
new_x = new_z.real
|
|
97
|
+
new_y = new_z.imag
|
|
98
|
+
new_lines.append((new_x, new_y))
|
|
99
|
+
return new_lines
|
|
100
|
+
|
|
101
|
+
def hintersections(x: np.ndarray, y: np.ndarray, level: float) -> list[float]:
|
|
102
|
+
"""Find the intersections of a line with a level
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
x (np.ndarray): X-axis values
|
|
106
|
+
y (np.ndarray): Y-axis values
|
|
107
|
+
level (float): Level to intersect
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
list[float]: List of x-values where the intersection occurs
|
|
111
|
+
"""
|
|
112
|
+
y1 = y[:-1] - level
|
|
113
|
+
y2 = y[1:] - level
|
|
114
|
+
ycross = y1 * y2
|
|
115
|
+
id1 = np.where(ycross < 0)[0]
|
|
116
|
+
id2 = id1 + 1
|
|
117
|
+
x1 = x[id1]
|
|
118
|
+
x2 = x[id2]
|
|
119
|
+
y1 = y[id1] - level
|
|
120
|
+
y2 = y[id2] - level
|
|
121
|
+
a = (y2 - y1) / (x2 - x1)
|
|
122
|
+
b = y1 - a * x1
|
|
123
|
+
xcross = list(-b / a)
|
|
124
|
+
xlevel = list(x[np.where(y == level)])
|
|
125
|
+
return xcross + xlevel
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def plot(
|
|
130
|
+
x: np.ndarray,
|
|
131
|
+
y: Union[np.ndarray, Sequence[np.ndarray]],
|
|
132
|
+
grid: bool = True,
|
|
133
|
+
labels: Optional[List[str]] = None,
|
|
134
|
+
xlabel: str = "x",
|
|
135
|
+
ylabel: str = "y",
|
|
136
|
+
linestyles: Union[str, List[str]] = "-",
|
|
137
|
+
linewidth: float = 2.0,
|
|
138
|
+
markers: Optional[Union[str, List[Optional[str]]]] = None,
|
|
139
|
+
logx: bool = False,
|
|
140
|
+
logy: bool = False,
|
|
141
|
+
transformation: Optional[Callable[[np.ndarray], np.ndarray]] = None,
|
|
142
|
+
xlim: Optional[Tuple[float, float]] = None,
|
|
143
|
+
ylim: Optional[Tuple[float, float]] = None,
|
|
144
|
+
title: Optional[str] = None
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Plot one or more y‐series against a common x‐axis, with extensive formatting options.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
x : np.ndarray
|
|
152
|
+
1D array of x‐values.
|
|
153
|
+
y : np.ndarray or sequence of np.ndarray
|
|
154
|
+
Either a single 1D array of y‐values, or a sequence of such arrays.
|
|
155
|
+
grid : bool, default True
|
|
156
|
+
Whether to show the grid.
|
|
157
|
+
labels : list of str, optional
|
|
158
|
+
One label per series. If None, no legend is drawn.
|
|
159
|
+
xlabel : str, default "x"
|
|
160
|
+
Label for the x‐axis.
|
|
161
|
+
ylabel : str, default "y"
|
|
162
|
+
Label for the y‐axis.
|
|
163
|
+
linestyles : str or list of str, default "-"
|
|
164
|
+
Matplotlib linestyle(s) for each series.
|
|
165
|
+
linewidth : float, default 2.0
|
|
166
|
+
Line width for all series.
|
|
167
|
+
markers : str or list of str or None, default None
|
|
168
|
+
Marker style(s) for each series. If None, no markers.
|
|
169
|
+
logx : bool, default False
|
|
170
|
+
If True, set x‐axis to logarithmic scale.
|
|
171
|
+
logy : bool, default False
|
|
172
|
+
If True, set y‐axis to logarithmic scale.
|
|
173
|
+
transformation : callable, optional
|
|
174
|
+
Function `f(y)` to transform each y‐array before plotting.
|
|
175
|
+
xlim : tuple (xmin, xmax), optional
|
|
176
|
+
Limits for the x‐axis.
|
|
177
|
+
ylim : tuple (ymin, ymax), optional
|
|
178
|
+
Limits for the y‐axis.
|
|
179
|
+
title : str, optional
|
|
180
|
+
Figure title.
|
|
181
|
+
"""
|
|
182
|
+
# Ensure y_list is a list of arrays
|
|
183
|
+
if isinstance(y, np.ndarray):
|
|
184
|
+
y_list = [y]
|
|
185
|
+
else:
|
|
186
|
+
y_list = list(y)
|
|
187
|
+
|
|
188
|
+
n_series = len(y_list)
|
|
189
|
+
|
|
190
|
+
# Prepare labels, linestyles, markers
|
|
191
|
+
if labels is not None and len(labels) != n_series:
|
|
192
|
+
raise ValueError("`labels` length must match number of y‐series")
|
|
193
|
+
# Turn single styles into lists of length n_series
|
|
194
|
+
def _broadcast(param, default):
|
|
195
|
+
if isinstance(param, list):
|
|
196
|
+
if len(param) != n_series:
|
|
197
|
+
raise ValueError(f"List length of `{param}` must match number of series")
|
|
198
|
+
return param
|
|
199
|
+
else:
|
|
200
|
+
return [param] * n_series
|
|
201
|
+
|
|
202
|
+
linestyles = _broadcast(linestyles, "-")
|
|
203
|
+
markers = _broadcast(markers, None) if markers is not None else [None] * n_series
|
|
204
|
+
|
|
205
|
+
# Apply transformation if given
|
|
206
|
+
if transformation is not None:
|
|
207
|
+
y_list = [trans(y_i) for trans, y_i in zip([transformation]*n_series, y_list)]
|
|
208
|
+
|
|
209
|
+
# Create plot
|
|
210
|
+
fig, ax = plt.subplots()
|
|
211
|
+
for i, y_i in enumerate(y_list):
|
|
212
|
+
ax.plot(
|
|
213
|
+
x, y_i,
|
|
214
|
+
linestyle=linestyles[i],
|
|
215
|
+
linewidth=linewidth,
|
|
216
|
+
marker=markers[i],
|
|
217
|
+
label=(labels[i] if labels is not None else None)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Axes scales
|
|
221
|
+
if logx:
|
|
222
|
+
ax.set_xscale("log")
|
|
223
|
+
if logy:
|
|
224
|
+
ax.set_yscale("log")
|
|
225
|
+
|
|
226
|
+
# Grid, labels, title
|
|
227
|
+
ax.grid(grid)
|
|
228
|
+
ax.set_xlabel(xlabel)
|
|
229
|
+
ax.set_ylabel(ylabel)
|
|
230
|
+
if title is not None:
|
|
231
|
+
ax.set_title(title)
|
|
232
|
+
|
|
233
|
+
# Limits
|
|
234
|
+
if xlim is not None:
|
|
235
|
+
ax.set_xlim(*xlim)
|
|
236
|
+
if ylim is not None:
|
|
237
|
+
ax.set_ylim(*ylim)
|
|
238
|
+
|
|
239
|
+
# Legend
|
|
240
|
+
if labels is not None:
|
|
241
|
+
ax.legend()
|
|
242
|
+
|
|
243
|
+
plt.show()
|
|
244
|
+
|
|
245
|
+
def smith(f: np.ndarray, S: np.ndarray) -> None:
|
|
246
|
+
""" Plot the Smith Chart
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
f (np.ndarray): Frequency vector (Not used yet)
|
|
250
|
+
S (np.ndarray): S-parameters to plot
|
|
251
|
+
"""
|
|
252
|
+
if not isinstance(S, list):
|
|
253
|
+
Ss = [S]
|
|
254
|
+
else:
|
|
255
|
+
Ss = S
|
|
256
|
+
|
|
257
|
+
fig, ax = plt.subplots()
|
|
258
|
+
for line in _smith_transform(_generate_grids()):
|
|
259
|
+
ax.plot(line[0], line[1], color='grey', alpha=0.3, linewidth=0.7)
|
|
260
|
+
p = np.linspace(0,2*np.pi,101)
|
|
261
|
+
ax.plot(np.cos(p), np.sin(p), color='black', alpha=0.5)
|
|
262
|
+
# Add important numbers for the Impedance Axes
|
|
263
|
+
# X and Y values (0, 0.5, 1, 2, 10, 50)
|
|
264
|
+
for i in [0, 0.2, 0.5, 1, 2, 10]:
|
|
265
|
+
z = i + 1j*0
|
|
266
|
+
G = (z-1)/(z+1)
|
|
267
|
+
ax.annotate(f"{i}", (G.real, G.imag), color='black', fontsize=8)
|
|
268
|
+
for i in [0, 0.2, 0.5, 1, 2, 10]:
|
|
269
|
+
z = 0 + 1j*i
|
|
270
|
+
G = (z-1)/(z+1)
|
|
271
|
+
ax.annotate(f"{i}", (G.real, G.imag), color='black', fontsize=8)
|
|
272
|
+
ax.annotate(f"{-i}", (G.real, -G.imag), color='black', fontsize=8)
|
|
273
|
+
for s in Ss:
|
|
274
|
+
ax.plot(s.real, s.imag, color='blue')
|
|
275
|
+
ax.grid(False)
|
|
276
|
+
ax.axis('equal')
|
|
277
|
+
plt.show()
|
|
278
|
+
|
|
279
|
+
def plot_sp(f: np.ndarray, S: list[np.ndarray] | np.ndarray,
|
|
280
|
+
dblim=[-40, 5],
|
|
281
|
+
xunit="GHz",
|
|
282
|
+
levelindicator: int | float =None,
|
|
283
|
+
noise_floor=-150,
|
|
284
|
+
fill_areas: list[tuple]= None,
|
|
285
|
+
spec_area: list[tuple[float]] = None,
|
|
286
|
+
unwrap_phase=False,
|
|
287
|
+
logx: bool = False,
|
|
288
|
+
labels: list[str] = None,
|
|
289
|
+
linestyles: list[str] = None,
|
|
290
|
+
colorcycle: list[int] = None,
|
|
291
|
+
filename: str = None,
|
|
292
|
+
show_plot: bool = True,
|
|
293
|
+
figdata: tuple = None) -> None:
|
|
294
|
+
"""Plot S-parameters in dB and phase
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
f (np.ndarray): Frequency vector
|
|
298
|
+
S (list[np.ndarray] | np.ndarray): S-parameters to plot (list or single array)
|
|
299
|
+
dblim (list, optional): Decibel y-axis limit. Defaults to [-80, 5].
|
|
300
|
+
xunit (str, optional): Frequency unit. Defaults to "GHz".
|
|
301
|
+
levelindicator (int | float, optional): Level at which annotation arrows will be added. Defaults to None.
|
|
302
|
+
noise_floor (int, optional): Artificial random noise floor level. Defaults to -150.
|
|
303
|
+
fill_areas (list[tuple], optional): Regions to fill (fmin, fmax). Defaults to None.
|
|
304
|
+
spec_area (list[tuple[float]], optional): _description_. Defaults to None.
|
|
305
|
+
unwrap_phase (bool, optional): If or not to unwrap the phase data. Defaults to False.
|
|
306
|
+
logx (bool, optional): Whether to use logarithmic frequency axes. Defaults to False.
|
|
307
|
+
labels (list[str], optional): A lists of labels to use. Defaults to None.
|
|
308
|
+
linestyles (list[str], optional): The linestyle to use (list or single string). Defaults to None.
|
|
309
|
+
colorcycle (list[int], optional): A list of colors to use. Defaults to None.
|
|
310
|
+
filename (str, optional): The filename (will automatically save). Defaults to None.
|
|
311
|
+
show_plot (bool, optional): If or not to show the resulting plot. Defaults to True.
|
|
312
|
+
"""
|
|
313
|
+
if not isinstance(S, list):
|
|
314
|
+
Ss = [S]
|
|
315
|
+
else:
|
|
316
|
+
Ss = S
|
|
317
|
+
|
|
318
|
+
if linestyles is None:
|
|
319
|
+
linestyles = ['-' for _ in S]
|
|
320
|
+
|
|
321
|
+
if colorcycle is None:
|
|
322
|
+
colorcycle = [i for i, S in enumerate(S)]
|
|
323
|
+
|
|
324
|
+
unitdivider = {"MHz": 1e6, "GHz": 1e9, "kHz": 1e3}
|
|
325
|
+
fnew = f / unitdivider[xunit]
|
|
326
|
+
|
|
327
|
+
if figdata is None:
|
|
328
|
+
# Create two subplots: one for magnitude and one for phase
|
|
329
|
+
fig, (ax_mag, ax_phase) = plt.subplots(2, 1, sharex=False, gridspec_kw={'height_ratios': [3, 1]})
|
|
330
|
+
fig.subplots_adjust(hspace=0.3)
|
|
331
|
+
else:
|
|
332
|
+
fig, ax_mag, ax_phase = figdata
|
|
333
|
+
minphase, maxphase = -180, 180
|
|
334
|
+
|
|
335
|
+
maxy = 0
|
|
336
|
+
for s, ls, cid in zip(Ss, linestyles, colorcycle):
|
|
337
|
+
# Calculate and plot magnitude in dB
|
|
338
|
+
SdB = 20 * np.log10(np.abs(s) + 10**(noise_floor/20) * np.random.rand(*s.shape) + 10**((noise_floor-30)/20))
|
|
339
|
+
ax_mag.plot(fnew, SdB, label="Magnitude (dB)", linestyle=ls, color=EMERGE_COLORS[cid % len(EMERGE_COLORS)])
|
|
340
|
+
if np.max(SdB) > maxy:
|
|
341
|
+
maxy = np.max(SdB)
|
|
342
|
+
# Calculate and plot phase in degrees
|
|
343
|
+
phase = np.angle(s, deg=True)
|
|
344
|
+
if unwrap_phase:
|
|
345
|
+
phase = np.unwrap(phase, period=360)
|
|
346
|
+
minphase = min(np.min(phase), minphase)
|
|
347
|
+
maxphase = max(np.max(phase), maxphase)
|
|
348
|
+
ax_phase.plot(fnew, phase, label="Phase (degrees)", linestyle=ls, color=EMERGE_COLORS[cid % len(EMERGE_COLORS)])
|
|
349
|
+
|
|
350
|
+
# Annotate level indicators if specified
|
|
351
|
+
if isinstance(levelindicator, (int, float)) and levelindicator is not None:
|
|
352
|
+
lvl = levelindicator
|
|
353
|
+
fcross = hintersections(fnew, SdB, lvl)
|
|
354
|
+
for fs in fcross:
|
|
355
|
+
ax_mag.annotate(
|
|
356
|
+
f"{str(fs)[:4]}{xunit}",
|
|
357
|
+
xy=(fs, lvl),
|
|
358
|
+
xytext=(fs + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
|
|
359
|
+
arrowprops=dict(facecolor="black", width=1, headwidth=5),
|
|
360
|
+
)
|
|
361
|
+
if fill_areas is not None:
|
|
362
|
+
for fmin, fmax in fill_areas:
|
|
363
|
+
f1 = fmin / unitdivider[xunit]
|
|
364
|
+
f2 = fmax / unitdivider[xunit]
|
|
365
|
+
ax_mag.fill_between([f1, f2], dblim[0], dblim[1], color='grey', alpha= 0.2)
|
|
366
|
+
ax_phase.fill_between([f1, f2], minphase, maxphase, color='grey', alpha= 0.2)
|
|
367
|
+
if spec_area is not None:
|
|
368
|
+
for fmin, fmax, vmin, vmax in spec_area:
|
|
369
|
+
f1 = fmin / unitdivider[xunit]
|
|
370
|
+
f2 = fmax / unitdivider[xunit]
|
|
371
|
+
ax_mag.fill_between([f1, f2], vmin,vmax, color='red', alpha=0.2)
|
|
372
|
+
# Configure magnitude plot (ax_mag)
|
|
373
|
+
ax_mag.set_ylabel("Magnitude (dB)")
|
|
374
|
+
ax_mag.set_xlabel(f"Frequency ({xunit})")
|
|
375
|
+
ax_mag.axis([min(fnew), max(fnew), dblim[0], max(maxy*1.1,dblim[1])])
|
|
376
|
+
ax_mag.axhline(y=0, color="k", linewidth=1)
|
|
377
|
+
ax_mag.xaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
378
|
+
ax_mag.yaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
379
|
+
# Configure phase plot (ax_phase)
|
|
380
|
+
ax_phase.set_ylabel("Phase (degrees)")
|
|
381
|
+
ax_phase.set_xlabel(f"Frequency ({xunit})")
|
|
382
|
+
ax_phase.axis([min(fnew), max(fnew), minphase, maxphase])
|
|
383
|
+
ax_phase.xaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
384
|
+
ax_phase.yaxis.set_minor_locator(tck.AutoMinorLocator(2))
|
|
385
|
+
if logx:
|
|
386
|
+
ax_mag.set_xscale('log')
|
|
387
|
+
ax_phase.set_xscale('log')
|
|
388
|
+
if labels is not None:
|
|
389
|
+
ax_mag.legend(labels)
|
|
390
|
+
ax_phase.legend(labels)
|
|
391
|
+
if show_plot:
|
|
392
|
+
plt.show()
|
|
393
|
+
if filename is not None:
|
|
394
|
+
fig.savefig(filename)
|
|
395
|
+
|
|
396
|
+
return fig, ax_mag, ax_phase
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def plot_ff(
|
|
400
|
+
theta: np.ndarray,
|
|
401
|
+
E: Union[np.ndarray, Sequence[np.ndarray]],
|
|
402
|
+
grid: bool = True,
|
|
403
|
+
labels: Optional[List[str]] = None,
|
|
404
|
+
xlabel: str = "Theta (rad)",
|
|
405
|
+
ylabel: str = "|E|",
|
|
406
|
+
linestyles: Union[str, List[str]] = "-",
|
|
407
|
+
linewidth: float = 2.0,
|
|
408
|
+
markers: Optional[Union[str, List[Optional[str]]]] = None,
|
|
409
|
+
xlim: Optional[Tuple[float, float]] = None,
|
|
410
|
+
ylim: Optional[Tuple[float, float]] = None,
|
|
411
|
+
title: Optional[str] = None
|
|
412
|
+
) -> None:
|
|
413
|
+
"""
|
|
414
|
+
Far-field rectangular plot of E-field magnitude vs angle.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
theta : np.ndarray
|
|
419
|
+
Angle array (radians).
|
|
420
|
+
E : np.ndarray or sequence of np.ndarray
|
|
421
|
+
Complex E-field samples; magnitude will be plotted.
|
|
422
|
+
grid : bool
|
|
423
|
+
Show grid.
|
|
424
|
+
labels : list of str, optional
|
|
425
|
+
Series labels.
|
|
426
|
+
xlabel, ylabel : str
|
|
427
|
+
Axis labels.
|
|
428
|
+
linestyles, linewidth, markers : styling parameters.
|
|
429
|
+
xlim, ylim : tuple, optional
|
|
430
|
+
Axis limits.
|
|
431
|
+
title : str, optional
|
|
432
|
+
Plot title.
|
|
433
|
+
"""
|
|
434
|
+
# Prepare data series
|
|
435
|
+
if isinstance(E, np.ndarray):
|
|
436
|
+
E_list = [E]
|
|
437
|
+
else:
|
|
438
|
+
E_list = list(E)
|
|
439
|
+
n_series = len(E_list)
|
|
440
|
+
|
|
441
|
+
# Style broadcasting
|
|
442
|
+
def _broadcast(param, default):
|
|
443
|
+
if isinstance(param, list):
|
|
444
|
+
if len(param) != n_series:
|
|
445
|
+
raise ValueError(f"List length of `{param}` must match number of series")
|
|
446
|
+
return param
|
|
447
|
+
else:
|
|
448
|
+
return [param] * n_series
|
|
449
|
+
|
|
450
|
+
linestyles = _broadcast(linestyles, "-")
|
|
451
|
+
markers = _broadcast(markers, None) if markers is not None else [None] * n_series
|
|
452
|
+
|
|
453
|
+
fig, ax = plt.subplots()
|
|
454
|
+
for i, Ei in enumerate(E_list):
|
|
455
|
+
mag = np.abs(Ei)
|
|
456
|
+
ax.plot(
|
|
457
|
+
theta, mag,
|
|
458
|
+
linestyle=linestyles[i],
|
|
459
|
+
linewidth=linewidth,
|
|
460
|
+
marker=markers[i],
|
|
461
|
+
label=(labels[i] if labels else None)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
ax.grid(grid)
|
|
465
|
+
ax.set_xlabel(xlabel)
|
|
466
|
+
ax.set_ylabel(ylabel)
|
|
467
|
+
if title:
|
|
468
|
+
ax.set_title(title)
|
|
469
|
+
if xlim:
|
|
470
|
+
ax.set_xlim(*xlim)
|
|
471
|
+
if ylim:
|
|
472
|
+
ax.set_ylim(*ylim)
|
|
473
|
+
if labels:
|
|
474
|
+
ax.legend()
|
|
475
|
+
|
|
476
|
+
plt.show()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def plot_ff_polar(
|
|
480
|
+
theta: np.ndarray,
|
|
481
|
+
E: Union[np.ndarray, Sequence[np.ndarray]],
|
|
482
|
+
labels: Optional[List[str]] = None,
|
|
483
|
+
linestyles: Union[str, List[str]] = "-",
|
|
484
|
+
linewidth: float = 2.0,
|
|
485
|
+
markers: Optional[Union[str, List[Optional[str]]]] = None,
|
|
486
|
+
zero_location: str = 'N',
|
|
487
|
+
clockwise: bool = False,
|
|
488
|
+
rlabel_angle: float = 45,
|
|
489
|
+
title: Optional[str] = None
|
|
490
|
+
) -> None:
|
|
491
|
+
"""
|
|
492
|
+
Far-field polar plot of E-field magnitude vs angle.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
theta : np.ndarray
|
|
497
|
+
Angle array (radians).
|
|
498
|
+
E : np.ndarray or sequence of np.ndarray
|
|
499
|
+
Complex E-field samples; magnitude will be plotted.
|
|
500
|
+
labels : list of str, optional
|
|
501
|
+
Series labels.
|
|
502
|
+
linestyles, linewidth, markers : styling parameters.
|
|
503
|
+
zero_location : str
|
|
504
|
+
Theta zero location (e.g. 'N', 'E').
|
|
505
|
+
clockwise : bool
|
|
506
|
+
If True, theta increases clockwise.
|
|
507
|
+
rlabel_angle : float
|
|
508
|
+
Position (deg) of radial labels.
|
|
509
|
+
title : str, optional
|
|
510
|
+
Plot title.
|
|
511
|
+
"""
|
|
512
|
+
# Prepare data series
|
|
513
|
+
if isinstance(E, np.ndarray):
|
|
514
|
+
E_list = [E]
|
|
515
|
+
else:
|
|
516
|
+
E_list = list(E)
|
|
517
|
+
n_series = len(E_list)
|
|
518
|
+
|
|
519
|
+
# Style broadcasting
|
|
520
|
+
def _broadcast(param, default):
|
|
521
|
+
if isinstance(param, list):
|
|
522
|
+
if len(param) != n_series:
|
|
523
|
+
raise ValueError(f"List length of `{param}` must match number of series")
|
|
524
|
+
return param
|
|
525
|
+
else:
|
|
526
|
+
return [param] * n_series
|
|
527
|
+
|
|
528
|
+
linestyles = _broadcast(linestyles, "-")
|
|
529
|
+
markers = _broadcast(markers, None) if markers is not None else [None] * n_series
|
|
530
|
+
|
|
531
|
+
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
|
|
532
|
+
ax.set_theta_zero_location(zero_location)
|
|
533
|
+
ax.set_theta_direction(-1 if clockwise else 1)
|
|
534
|
+
ax.set_rlabel_position(rlabel_angle)
|
|
535
|
+
|
|
536
|
+
for i, Ei in enumerate(E_list):
|
|
537
|
+
mag = np.abs(Ei)
|
|
538
|
+
ax.plot(
|
|
539
|
+
theta, mag,
|
|
540
|
+
linestyle=linestyles[i],
|
|
541
|
+
linewidth=linewidth,
|
|
542
|
+
marker=markers[i],
|
|
543
|
+
label=(labels[i] if labels else None)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if title:
|
|
547
|
+
ax.set_title(title, va='bottom')
|
|
548
|
+
if labels:
|
|
549
|
+
ax.legend(loc='upper right', bbox_to_anchor=(1.1, 1.1))
|
|
550
|
+
|
|
551
|
+
plt.show()
|