emerge 0.4.6__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 (80) hide show
  1. emerge/__init__.py +54 -0
  2. emerge/__main__.py +5 -0
  3. emerge/_emerge/__init__.py +42 -0
  4. emerge/_emerge/bc.py +197 -0
  5. emerge/_emerge/coord.py +119 -0
  6. emerge/_emerge/cs.py +523 -0
  7. emerge/_emerge/dataset.py +36 -0
  8. emerge/_emerge/elements/__init__.py +19 -0
  9. emerge/_emerge/elements/femdata.py +212 -0
  10. emerge/_emerge/elements/index_interp.py +64 -0
  11. emerge/_emerge/elements/legrange2.py +172 -0
  12. emerge/_emerge/elements/ned2_interp.py +645 -0
  13. emerge/_emerge/elements/nedelec2.py +140 -0
  14. emerge/_emerge/elements/nedleg2.py +217 -0
  15. emerge/_emerge/geo/__init__.py +24 -0
  16. emerge/_emerge/geo/horn.py +107 -0
  17. emerge/_emerge/geo/modeler.py +449 -0
  18. emerge/_emerge/geo/operations.py +254 -0
  19. emerge/_emerge/geo/pcb.py +1244 -0
  20. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  21. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  22. emerge/_emerge/geo/pmlbox.py +204 -0
  23. emerge/_emerge/geo/polybased.py +529 -0
  24. emerge/_emerge/geo/shapes.py +427 -0
  25. emerge/_emerge/geo/step.py +77 -0
  26. emerge/_emerge/geo2d.py +86 -0
  27. emerge/_emerge/geometry.py +510 -0
  28. emerge/_emerge/howto.py +214 -0
  29. emerge/_emerge/logsettings.py +5 -0
  30. emerge/_emerge/material.py +118 -0
  31. emerge/_emerge/mesh3d.py +730 -0
  32. emerge/_emerge/mesher.py +339 -0
  33. emerge/_emerge/mth/common_functions.py +33 -0
  34. emerge/_emerge/mth/integrals.py +71 -0
  35. emerge/_emerge/mth/optimized.py +357 -0
  36. emerge/_emerge/periodic.py +263 -0
  37. emerge/_emerge/physics/__init__.py +0 -0
  38. emerge/_emerge/physics/microwave/__init__.py +1 -0
  39. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  40. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  41. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  42. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  43. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  44. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  45. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  46. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  47. emerge/_emerge/physics/microwave/periodic.py +82 -0
  48. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  49. emerge/_emerge/physics/microwave/sc.py +175 -0
  50. emerge/_emerge/physics/microwave/simjob.py +147 -0
  51. emerge/_emerge/physics/microwave/sparam.py +138 -0
  52. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  53. emerge/_emerge/plot/__init__.py +0 -0
  54. emerge/_emerge/plot/display.py +394 -0
  55. emerge/_emerge/plot/grapher.py +93 -0
  56. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  57. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  58. emerge/_emerge/plot/pyvista/display.py +931 -0
  59. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  60. emerge/_emerge/plot/simple_plots.py +551 -0
  61. emerge/_emerge/plot.py +225 -0
  62. emerge/_emerge/projects/__init__.py +0 -0
  63. emerge/_emerge/projects/_gen_base.txt +32 -0
  64. emerge/_emerge/projects/_load_base.txt +24 -0
  65. emerge/_emerge/projects/generate_project.py +40 -0
  66. emerge/_emerge/selection.py +596 -0
  67. emerge/_emerge/simmodel.py +444 -0
  68. emerge/_emerge/simulation_data.py +411 -0
  69. emerge/_emerge/solver.py +993 -0
  70. emerge/_emerge/system.py +54 -0
  71. emerge/cli.py +19 -0
  72. emerge/lib.py +57 -0
  73. emerge/plot.py +1 -0
  74. emerge/pyvista.py +1 -0
  75. {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  76. emerge-0.4.8.dist-info/RECORD +78 -0
  77. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  78. emerge-0.4.6.dist-info/RECORD +0 -4
  79. emerge-0.4.6.dist-info/entry_points.txt +0 -2
  80. {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # SPDX-License-Identifier: GPL-2.0-or-later
4
+ """
5
+ adaptive_frequency.py
6
+ ---------------------
7
+ Copyright (c) 2014 Phil Reinhold
8
+ Copyright (c) 2025 Robert Fennis
9
+
10
+ This file is part of EMerge.
11
+
12
+ This program is free software: you can redistribute it and/or modify
13
+ it under the terms of the GNU General Public License **version 2**,
14
+ or (at your option) **any later version**, as published by the Free
15
+ Software Foundation.
16
+
17
+ This program is distributed in the hope that it will be useful,
18
+ but **WITHOUT ANY WARRANTY**; without even the implied warranty of
19
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ GNU General Public License for more details.
21
+
22
+ You should have received a copy of the GNU General Public License
23
+ along with this program (see the file “COPYING” in the project root).
24
+ If not, see <https://www.gnu.org/licenses/>.
25
+
26
+ -------------------------------------------------------------------------------
27
+ Origins
28
+ -------------------------------------------------------------------------------
29
+ This module is based on “Duplication of the vector fitting algorithm in
30
+ Python” (GPL-2.0) by Phil Reinhold, which re-implements the original MATLAB
31
+ code by **Bjørn Gustavsen**. Key academic references:
32
+
33
+ [1] B. Gustavsen & A. Semlyen, “Rational approximation of frequency
34
+ domain responses by Vector Fitting”, *IEEE Trans. Power Delivery*,
35
+ 14 (3): 1052-1061, Jul 1999.
36
+
37
+ [2] B. Gustavsen, “Improving the pole relocating properties of vector
38
+ fitting”, *IEEE Trans. Power Delivery*, 21 (3): 1587-1592, Jul 2006.
39
+
40
+ [3] D. Deschrijver *et al.*, “Macromodeling of Multiport Systems Using a
41
+ Fast Implementation of the Vector Fitting Method”, *IEEE MWC Lett.*,
42
+ 18 (6): 383-385, Jun 2008.
43
+
44
+ -------------------------------------------------------------------------------
45
+ Modification history
46
+ -------------------------------------------------------------------------------
47
+ * 2025-06-23 Robert Fennis fennisrobert@gmail.com
48
+ - Refactored API with logic realized in the SparamModel class.
49
+
50
+ """
51
+
52
+ import numpy as np
53
+ from typing import Literal
54
+ from loguru import logger
55
+ def cc(z):
56
+ return z.conjugate()
57
+
58
+ def model(s, poles, residues, d, h):
59
+ return np.sum(r/(s-p) for p, r in zip(poles, residues)) + d + s*h
60
+
61
+ def vectfit_step(f: np.ndarray, s: np.ndarray, poles: np.ndarray) -> np.ndarray:
62
+ """
63
+ f = complex data to fit
64
+ s = j*frequency
65
+ poles = initial poles guess
66
+ note: All complex poles must come in sequential complex conjugate pairs
67
+ returns adjusted poles
68
+ """
69
+ N = len(poles)
70
+ Ns = len(s)
71
+
72
+ cindex = np.zeros(N)
73
+ # cindex is:
74
+ # - 0 for real poles
75
+ # - 1 for the first of a complex-conjugate pair
76
+ # - 2 for the second of a cc pair
77
+ for i, p in enumerate(poles):
78
+ if p.imag != 0:
79
+ if i == 0 or cindex[i-1] != 1:
80
+ assert cc(poles[i]) == poles[i+1], ("Complex poles must come in conjugate pairs: %s, %s" % (poles[i], poles[i+1]))
81
+ cindex[i] = 1
82
+ else:
83
+ cindex[i] = 2
84
+
85
+ # First linear equation to solve. See Appendix A
86
+ A = np.zeros((Ns, 2*N+2), dtype=np.complex128)
87
+ for i, p in enumerate(poles):
88
+ if cindex[i] == 0:
89
+ A[:, i] = 1/(s - p)
90
+ elif cindex[i] == 1:
91
+ A[:, i] = 1/(s - p) + 1/(s - cc(p))
92
+ elif cindex[i] == 2:
93
+ A[:, i] = 1j/(s - p) - 1j/(s - cc(p))
94
+ else:
95
+ raise ValueError("cindex[%s] = %s" % (i, cindex[i]))
96
+
97
+ A [:, N+2+i] = -A[:, i] * f
98
+
99
+ A[:, N] = 1
100
+ A[:, N+1] = s
101
+
102
+ # Solve Ax == b using pseudo-inverse
103
+ b = f
104
+ A = np.vstack((A.real, A.imag))
105
+ b = np.concatenate((b.real, b.imag))
106
+ x, residuals, rnk, s = np.linalg.lstsq(A, b, rcond=-1)
107
+
108
+ residues = x[:N]
109
+ d = x[N]
110
+ h = x[N+1]
111
+
112
+ # We only want the "tilde" part in (A.4)
113
+ x = x[-N:]
114
+
115
+ # Calculation of zeros: Appendix B
116
+ A = np.diag(poles)
117
+ b = np.ones(N)
118
+ c = x
119
+ for i, (ci, p) in enumerate(zip(cindex, poles)):
120
+ if ci == 1:
121
+ x, y = p.real, p.imag
122
+ A[i, i] = A[i+1, i+1] = x
123
+ A[i, i+1] = -y
124
+ A[i+1, i] = y
125
+ b[i] = 2
126
+ b[i+1] = 0
127
+
128
+ H = A - np.outer(b, c)
129
+ H = H.real
130
+ new_poles = np.sort(np.linalg.eigvals(H))
131
+ unstable = np.real(new_poles) > 0
132
+ new_poles[unstable] -= 2*np.real(new_poles)[unstable]
133
+ return new_poles
134
+
135
+ # Dear gods of coding style, I sincerely apologize for the following copy/paste
136
+ def calculate_residues(f: np.ndarray, s: np.ndarray, poles: np.ndarray, rcond=-1) -> tuple[np.ndarray, float, float]:
137
+ Ns = len(s)
138
+ N = len(poles)
139
+
140
+ cindex = np.zeros(N)
141
+ for i, p in enumerate(poles):
142
+ if p.imag != 0:
143
+ if i == 0 or cindex[i-1] != 1:
144
+ assert cc(poles[i]) == poles[i+1], ("Complex poles must come in conjugate pairs: %s, %s" % poles[i:i+1])
145
+ cindex[i] = 1
146
+ else:
147
+ cindex[i] = 2
148
+
149
+ # use the new poles to extract the residues
150
+ A = np.zeros((Ns, N+2), dtype=np.complex128)
151
+ for i, p in enumerate(poles):
152
+ if cindex[i] == 0:
153
+ A[:, i] = 1/(s - p)
154
+ elif cindex[i] == 1:
155
+ A[:, i] = 1/(s - p) + 1/(s - cc(p))
156
+ elif cindex[i] == 2:
157
+ A[:, i] = 1j/(s - p) - 1j/(s - cc(p))
158
+ else:
159
+ raise RuntimeError("cindex[%s] = %s" % (i, cindex[i]))
160
+
161
+ A[:, N] = 1
162
+ A[:, N+1] = s
163
+ # Solve Ax == b using pseudo-inverse
164
+ b = f
165
+ A = np.vstack((A.real, A.imag))
166
+ b = np.concatenate((b.real, b.imag))
167
+ cA = np.linalg.cond(A)
168
+ if cA > 1e13:
169
+ print('Warning!: Ill Conditioned Matrix. Consider scaling the problem down')
170
+ print('Cond(A)', cA)
171
+ x, residuals, rnk, s = np.linalg.lstsq(A, b, rcond=rcond)
172
+
173
+ # Recover complex values
174
+ x = np.complex128(x)
175
+ for i, ci in enumerate(cindex):
176
+ if ci == 1:
177
+ r1, r2 = x[i:i+2]
178
+ x[i] = r1 - 1j*r2
179
+ x[i+1] = r1 + 1j*r2
180
+
181
+ residues = x[:N]
182
+ d = x[N].real
183
+ h = x[N+1].real
184
+ return residues, d, h
185
+
186
+ def vectfit_auto(f: np.ndarray,
187
+ s: np.ndarray,
188
+ n_poles: int = 10,
189
+ n_iter: int = 10,
190
+ inc_real: bool = False,
191
+ loss_ratio: float = 1e-2,
192
+ rcond: int = -1,
193
+ track_poles: bool = False) -> tuple[np.ndarray, np.ndarray, float, float]:
194
+ w = s.imag
195
+ pole_locs = np.linspace(w[0], w[-1], n_poles+2)[1:-1]
196
+ lr = loss_ratio
197
+ init_poles = poles = np.concatenate([[p*(-lr + 1j), p*(-lr - 1j)] for p in pole_locs])
198
+
199
+ if inc_real:
200
+ poles = np.concatenate((poles, [1]))
201
+
202
+ poles_list = []
203
+ for _ in range(n_iter):
204
+ poles = vectfit_step(f, s, poles)
205
+ poles_list.append(poles)
206
+
207
+ residues, d, h = calculate_residues(f, s, poles, rcond=rcond)
208
+
209
+ if track_poles:
210
+ return poles, residues, d, h, np.array(poles_list)
211
+ return poles, residues, d, h
212
+
213
+ def vectfit_auto_rescale(f: np.ndarray, s: np.ndarray,
214
+ n_poles: int = 10,
215
+ n_iter: int = 10,
216
+ inc_real: bool = False,
217
+ loss_ratio: float = 1e-2,
218
+ rcond: int = -1,
219
+ track_poles: bool = False) -> tuple[np.ndarray, np.ndarray, float, float]:
220
+ s_scale = abs(s[-1])
221
+ f_scale = abs(f[-1])
222
+ poles_s, residues_s, d_s, h_s = vectfit_auto(f / f_scale,
223
+ s / s_scale,
224
+ n_poles=n_poles,
225
+ n_iter = n_iter,
226
+ inc_real=inc_real,
227
+ loss_ratio=loss_ratio,
228
+ rcond=rcond,
229
+ track_poles=track_poles)
230
+ poles = poles_s * s_scale
231
+ residues = residues_s * f_scale * s_scale
232
+ d = d_s * f_scale
233
+ h = h_s * f_scale / s_scale
234
+ return poles, residues, d, h
235
+
236
+
237
+ class SparamModel:
238
+
239
+ def __init__(self,
240
+ frequencies: np.ndarray,
241
+ Sparam: np.ndarray,
242
+ n_poles: int | Literal['auto'] = 10,
243
+ inc_real: bool = False,
244
+ maxpoles: int = 40):
245
+ self.f: np.ndarray = frequencies
246
+ self.S: np.ndarray = Sparam
247
+
248
+ s = 1j*frequencies
249
+
250
+ if n_poles == 'auto':
251
+ fdense = np.linspace(min(self.f), max(self.f), max(201, 10*self.f.shape[0]))
252
+ success = False
253
+ for nps in range(1,maxpoles):
254
+ poles, residues, d, h = vectfit_auto_rescale(Sparam, s, n_poles=nps, inc_real=inc_real)
255
+ self.poles: np.ndarray = poles
256
+ self.residues: np.ndarray = residues
257
+ self.d = d
258
+ self.h = h
259
+
260
+ S = self(fdense)
261
+
262
+ error = np.mean(np.abs(Sparam-self(self.f)))
263
+ if all(np.abs(S) <= 1.0) and error < 1e-2:
264
+ logger.debug(f'Using {nps} poles.')
265
+ success = True
266
+ break
267
+ if not success:
268
+ logger.warning('Could not model S-parameters. Try a denser grid')
269
+
270
+ else:
271
+ poles, residues, d, h = vectfit_auto_rescale(Sparam, s, n_poles=n_poles, inc_real=inc_real)
272
+ self.poles: np.ndarray = poles
273
+ self.residues: np.ndarray = residues
274
+ self.d = d
275
+ self.h = h
276
+
277
+ def __call__(self, f: np.ndarray) -> np.ndarray:
278
+ return model(1j*f, self.poles, self.residues, self.d, self.h)
279
+