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,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
|
+
|