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.

Files changed (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  74. emerge-0.4.8.dist-info/RECORD +78 -0
  75. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {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()