Files
ovf/main.py
2025-09-18 06:44:34 -04:00

478 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from core.orthonormal_basis import generate_laguerre_basis
from core.sk_iter import generate_starting_poles
import numpy as np
import skrf as rf
import matplotlib.pyplot as plt
import sympy as sp
from scipy.linalg import null_space
from plotly.subplots import make_subplots
import plotly.graph_objs
network = rf.Network("/tmp/paramer/simulation/4000/4000.s2p")
freqs = network.f
s = freqs * 2j * np.pi
vf = rf.VectorFitting(network)
vf.vector_fit(n_poles_real=2, n_poles_cmplx=1,parameter_type="y")
poles = vf.poles
residues = vf.residues
y = vf.network.y
# fig, ax = plt.subplots(2, 2)
# fig.set_size_inches(12, 8)
# vf.plot("mag",0,0,freqs,ax=ax[0][0],parameter="y")
# rms_error11 = vf.get_rms_error(0,0,"y")
# print("rms_error11",rms_error11)
# vf.plot("mag",1,0,freqs,ax=ax[1][0],parameter="y")
# rms_error21 = vf.get_rms_error(1,0,"y")
# print("rms_error21",rms_error21)
# vf.plot("mag",0,1,freqs,ax=ax[0][1],parameter="y")
# rms_error12 = vf.get_rms_error(0,1,"y")
# print("rms_error12",rms_error12)
# vf.plot("mag",1,1,freqs,ax=ax[1][1],parameter="y")
# rms_error22 = vf.get_rms_error(1,1,"y")
# print("rms_error22",rms_error22)
# fig.tight_layout()
# plt.show()
# plt.savefig(f"img.png")
def formula_67(s,y):
diag_values = [y[i][0][0] for i in range(len(y))]
H = np.diag(diag_values)
P = 2
start_poles = generate_starting_poles(P,beta_min=1e8,beta_max=freqs[-1])
basis = generate_laguerre_basis(start_poles,s).T
print("start_poles",start_poles)
print("basis",basis)
# first step iteration
# A*x = b
A11 = np.real(basis)
print("A11 shape:",A11.shape)
A12 = np.real(- H @ basis[:,1:])
print("A12 shape:",A12.shape)
# print("A11",A11)
A21 = np.imag(basis)
print("A21 shape:",A21.shape)
A22 = np.imag(- H @ basis[:,1:])
print("A22 shape:",A22.shape)
A1 = np.hstack([A11,A12])
A2 = np.hstack([A21,A22])
A = np.vstack([A1,A2])
print ("A shape:",A.shape)
b1 = np.real(H @ basis[:,0])
b2 = np.imag(H @ basis[:,0])
b = np.hstack([b1,b2])
Q, R = np.linalg.qr(A, mode='reduced')
print("Q :",Q)
print("R :",R)
print("b shape:",b.shape)
# x = np.linalg.solve(R, Q.T @ b)
x_star, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
# print("x_qr",x_star)
print("residuals",residuals)
# print("rank",rank)
# print("s",s)
x_ne = np.linalg.inv(A.T @ A) @ A.T @ b
print("x_ne",x_ne)
# sk iteration
target_vec = np.array([1+0j] + list(x_ne[len(x_ne)//2+1:]))
D_t = basis @ target_vec
print("D_t",D_t)
K = 25
for i in range(K):
print(f"Iteration {i+1}/{K}")
A11 = np.real(basis / D_t[:, np.newaxis])
A12 = np.real(- H @ basis[:,1:] / D_t[:, np.newaxis])
A21 = np.imag(basis / D_t[:, np.newaxis])
A22 = np.imag(- H @ basis[:,1:] / D_t[:, np.newaxis])
A1 = np.hstack([A11,A12])
A2 = np.hstack([A21,A22])
A = np.vstack([A1,A2])
b1 = np.real(H @ basis[:,0])
b2 = np.imag(H @ basis[:,0])
b = np.hstack([b1,b2])
x_star, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
# print("x_lstsq",x_star)
# print("residuals",residuals)
# print("rank",rank)
# print("s",s)
x_ne = np.linalg.inv(A.T @ A) @ A.T @ b
# print("x_ne",x_ne)
target_vec = np.array([1+0j] + list(x_ne[len(x_ne)//2+1:]))
D_t_pre = D_t
D_t = basis @ target_vec
print("Dt", D_t)
# print("D_t/D_t_pre",D_t/D_t_pre)
# print("D_t",D_t)
n_target_vec = np.array(list(x_ne[:len(x_ne)//2+1]))
N_t = basis @ n_target_vec
# print("H = N_t / D_t",np.abs(N_t / D_t))
class formula_70:
"""
VF-(70) with final pole-residue model.
After fit():
self.poles : (P,) complex
self.res : (P, M) complex # residues per response (columns)
self.h : (M,) complex # optional constants
self.g : (M,) complex # optional proportional terms
"""
# -------- internals --------
def __init__(self):
self.cond = []
self.rel = []
@staticmethod
def _psi(s, z):
s = np.asarray(s, np.complex128).reshape(-1)
z = np.asarray(z, np.complex128).reshape(-1)
return 1.0 / (s[:, None] + z[None, :])
@staticmethod
def _lhp(z):
z = np.asarray(z, np.complex128).reshape(-1).copy()
z[z.real > 0] = -np.conj(z[z.real > 0])
return z
def _build_70(self, s, H_list, z_ref, d0):
Hs = [np.asarray(h, np.complex128).reshape(-1) for h in H_list]
K, P, M = len(s), len(z_ref), len(Hs)
Psi = self._psi(s, z_ref)
Z = np.zeros((K, P))
rows, rhs = [], []
for m, H in enumerate(Hs):
Hp = H[:, None]
L_re = np.real(Hp * Psi); L_im = np.imag(Hp * Psi) # acts on c
R_re = -np.real(Psi); R_im = -np.imag(Psi) # acts on r^(m)
rows.append(np.hstack([L_re] + [R_re if j == m else Z for j in range(M)]))
rhs.append(-np.real(d0 * H))
rows.append(np.hstack([L_im] + [R_im if j == m else Z for j in range(M)]))
rhs.append(-np.imag(d0 * H))
A = np.vstack(rows)
b = np.concatenate(rhs)
return A, b, M, P
def _step_70(self, s, H_list, z_ref, d0=1.0, scale=True):
A, b, M, P = self._build_70(s, H_list, z_ref, d0)
if scale:
coln = np.maximum(np.linalg.norm(A, axis=0), 1e-12)
x, *_ = np.linalg.lstsq(A / coln, b, rcond=None)
x = x / coln
cond = np.linalg.cond(A)
else:
x, *_ = np.linalg.lstsq(A, b, rcond=None)
cond = np.linalg.cond(A)
c = x[:P]
res_ratio = np.empty((P, M), np.complex128)
off = P
for m in range(M):
res_ratio[:, m] = x[off:off+P]; off += P
# relocate poles with test matrix
Sigma = np.diag(-np.asarray(z_ref, np.complex128))
T = Sigma - (np.ones((P, 1), np.complex128) @ (c.reshape(1, -1) / d0))
z_new = -np.linalg.eigvals(T)
z_new = self._lhp(z_new)
# cond = np.linalg.cond(T)
return z_new, c, cond, res_ratio
# -------- public API --------
def fit(self, s, H, z0, n_iter=20, d0=1.0, include_const=True, include_linear=False, verbose=True):
"""
s : (K,) complex samples (j*2π*f_sel)
H : (K,) complex or (K,M) or list of M vectors
z0: initial poles (P,) complex (LHP + conjugate pairs recommended)
"""
# normalize responses -> list
if isinstance(H, (list, tuple)):
H_list = [np.asarray(h, np.complex128).reshape(-1) for h in H]
else:
H_arr = np.asarray(H, np.complex128)
if H_arr.ndim == 1:
H_list = [H_arr]
elif H_arr.ndim == 2 and H_arr.shape[0] == len(s):
H_list = [H_arr[:, i].copy() for i in range(H_arr.shape[1])]
else:
raise ValueError("H must be (K,), list of (K,), or (K,M) with M responses.")
M = len(H_list)
z = self._lhp(np.asarray(z0, np.complex128))
# SK/VF relocations
for it in range(n_iter):
z_next, c_last, cond, _ = self._step_70(s, H_list, z, d0=d0, scale=True)
rel = np.linalg.norm(z_next) / max(1.0, np.linalg.norm(z))
if verbose:
print(f"[VF-70] iter {it+1:02d}/{n_iter:02d} Δz_rel={rel} cond(z)={cond}")
self.cond.append(cond)
self.rel.append(rel)
z = z_next
# ---- Finalize: refit residues with fixed poles z (poleresidue model) ----
Phi = self._psi(s, z) # K x P
# Build design matrix for extras
extras = []
if include_const: extras.append(np.ones(len(s), np.complex128))
if include_linear: extras.append(s.astype(np.complex128))
if extras:
X_base = np.column_stack([Phi] + extras) # K x (P + E)
else:
X_base = Phi
res = np.empty((len(z), M), np.complex128)
h = np.zeros(M, np.complex128)
g = np.zeros(M, np.complex128)
for m, Hm in enumerate(H_list):
theta, *_ = np.linalg.lstsq(X_base, Hm, rcond=None) # complex LS
res[:, m] = theta[:len(z)]
e = len(theta) - len(z)
if e >= 1: h[m] = theta[len(z)]
if e >= 2: g[m] = theta[len(z)+1]
# store the rational function
self.poles = z # (P,)
self.res = res # (P, M)
self.h = h # (M,)
self.g = g # (M,)
return self
# Evaluate the stored **rational** model on any grid
def evaluate(self, s_eval, m=None):
if not hasattr(self, "poles"):
raise RuntimeError("Model not fitted. Call fit(...) first.")
s_eval = np.asarray(s_eval, np.complex128).reshape(-1)
Phi = self._psi(s_eval, self.poles) # K_eval x P
if m is None:
H = Phi @ self.res
if np.any(self.h): H += self.h
if np.any(self.g): H += s_eval[:, None] * self.g
return H # (K_eval, M) or (K_eval,1)
m = int(m)
H = Phi @ self.res[:, m]
H += self.h[m]
H += s_eval * self.g[m]
return H # (K_eval,)
# def plot_rel_and_cond(self):
# fig, (ax1, ax2) = plt.subplots(2,1,figsize=(15,20))
# ax1.plot(np.log(self.rel), 'g-', label='rel')
# ax2.plot(np.log(self.cond), 'b-', label='cond')
# ax1.set_xlabel('Iteration')
# ax1.set_ylabel('Relative Change', color='g')
# ax2.set_ylabel('Condition Number', color='b')
# ax1.tick_params(axis='y', labelcolor='g')
# ax2.tick_params(axis='y', labelcolor='b')
# fig.tight_layout()
# plt.title('Relative Change and Condition Number per Iteration')
# # plt.show()
# plt.savefig(f"img_rel_cond.png")
def plot_rel_and_cond(self):
fig = make_subplots(rows=2, cols=1, subplot_titles=('Relative Change', 'Condition Number'))
fig.add_trace(plotly.graph_objs.Scatter(y=self.rel, mode='lines+markers', name='rel'), row=1, col=1)
fig.add_trace(plotly.graph_objs.Scatter(y=self.cond, mode='lines+markers', name='cond'), row=2, col=1)
fig.update_xaxes(title_text='Iteration', row=1, col=1)
fig.update_yaxes(title_text='Relative Change', row=1, col=1)
fig.update_yaxes(title_text='Condition Number', row=2, col=1)
fig.update_layout(height=800, width=800, title_text='Relative Change and Condition Number per Iteration')
fig.write_image("img_rel_cond.png")
# fig.show()
def evaluate_on_freq(self, freq_eval, m=None):
return self.evaluate(1j * 2*np.pi * np.asarray(freq_eval, float), m=m)
# Optional: save/load the final rational model
def save(self, path):
if not hasattr(self, "poles"):
raise RuntimeError("Nothing to save; call fit(...) first.")
np.savez(path, poles=self.poles, res=self.res, h=self.h, g=self.g)
@classmethod
def load(cls, path):
d = np.load(path, allow_pickle=False)
obj = cls()
obj.poles = d["poles"]; obj.res = d["res"]; obj.h = d["h"]; obj.g = d["g"]
return obj
def auto_select(H, freq,
n_baseline=64, # log-spaced backbone points
peak_prominence=0.05, # fraction of |H| dB dynamic range for peak detection
peak_window=5, # take ±peak_window samples around each peak
topgrad_q=0.98, # keep top 2% largest slope/phase-change points
max_points=25, # final cap on selected samples (None = no cap)
ensure_ends=True):
"""
Select several significant sample points for vector fitting.
Strategy:
1) Always keep endpoints (optional).
2) Add a log-spaced baseline over the band.
3) Detect resonance peaks in |H| (on a log scale) and keep small windows around them.
4) Add points with the largest magnitude slope and phase-change (w.r.t log-f).
5) De-duplicate, sort, and optionally thin to 'max_points' with priority
to endpoints and detected peaks.
Parameters
----------
H : (N,) complex array
Frequency response samples.
freq : (N,) float array
Frequency axis [Hz], strictly increasing.
n_baseline : int
Count of log-spaced baseline samples across the band.
peak_prominence : float
Peak prominence threshold as a fraction of the dynamic range in log|H|.
0.05 ≈ keep peaks ≥ 5% of the range.
peak_window : int
Number of neighbor indices to include on each side of every detected peak.
topgrad_q : float in (0,1)
Quantile for selecting strong slope/phase points.
0.98 ⇒ keep the top 2% largest derivatives.
max_points : int or None
If not None, cap the total number of selected indices to this value.
ensure_ends : bool
Always include the first and last samples.
Returns
-------
H_sel : (K,) complex array
freq_sel : (K,) float array
"""
H = np.asarray(H).reshape(-1)
f = np.asarray(freq).reshape(-1)
if H.size != f.size:
raise ValueError("H and freq must have the same length.")
N = f.size
if N < 4:
return H.copy(), f.copy()
eps = 1e-16
mag = np.abs(H)
logmag = np.log10(mag + eps)
phase = np.unwrap(np.angle(H))
# log-frequency axis (scale-invariant derivatives)
# keep it linear if any non-positive freq sneaks in
if np.all(f > 0):
lf = np.log(f)
else:
lf = f.copy()
dlf = np.gradient(lf)
d_logmag = np.gradient(logmag) / (dlf + 1e-16)
d_phase = np.gradient(phase) / (dlf + 1e-16)
idx = set()
if ensure_ends:
idx.update([0, N-1])
# 1) log-spaced baseline
if n_baseline > 0:
# map a log grid to nearest indices
grid = np.linspace(lf.min(), lf.max(), n_baseline)
base_idx = np.clip(np.searchsorted(lf, grid), 0, N-1)
idx.update(np.unique(base_idx).tolist())
# 2) peaks in |H|
try:
from scipy.signal import find_peaks
dyn = logmag.max() - logmag.min()
prom = peak_prominence * (dyn + 1e-12)
peaks, _ = find_peaks(logmag, prominence=prom)
except Exception:
# simple fallback: strict local maxima
peaks = np.where((mag[1:-1] > mag[:-2]) & (mag[1:-1] > mag[2:]))[0] + 1
for p in peaks:
lo = max(0, p - peak_window)
hi = min(N, p + peak_window + 1)
idx.update(range(lo, hi))
# 3) strongest slope / phase-change points
thr_slope = np.quantile(np.abs(d_logmag), topgrad_q)
thr_phase = np.quantile(np.abs(d_phase), topgrad_q)
idx.update(np.where(np.abs(d_logmag) >= thr_slope)[0].tolist())
idx.update(np.where(np.abs(d_phase) >= thr_phase)[0].tolist())
# 4) finalize set
sel = np.array(sorted(idx), dtype=int)
# 5) optional thinning with priority to endpoints and peaks
if max_points is not None and sel.size > max_points:
priority = np.zeros(sel.size, dtype=int)
if ensure_ends:
priority[(sel == 0) | (sel == N-1)] = 3
if peaks.size:
priority[np.isin(sel, peaks)] = np.maximum(priority[np.isin(sel, peaks)], 2)
keep = []
budget = max_points
# keep highest-priority first
for lev in (3, 2, 1, 0):
cand = sel[priority == lev]
if cand.size == 0:
continue
if cand.size <= budget:
keep.extend(cand.tolist())
budget -= cand.size
else:
step = max(1, int(np.ceil(cand.size / budget)))
keep.extend(cand[::step][:budget].tolist())
budget = 0
if budget == 0:
break
sel = np.array(sorted(set(keep)), dtype=int)
return H[sel], f[sel]
if __name__ == "__main__":
# formula_67(s,y)
H11 = np.array([y[i,0,0] for i in range(len(y))])
H11_slice,freqs_slice = auto_select(H11,freqs,max_points=20)
s_slice = freqs_slice * 2j * np.pi
P_pairs = 1
z0 = generate_starting_poles(P_pairs, beta_min=1e8, beta_max=freqs_slice[-1])
z0 = np.array(z0, dtype=np.complex128)
f70 = formula_70()
K = 10
model = f70.fit(s_slice, H11_slice, z0, n_iter=K, d0=1.0, verbose=True)
model.save("vf70_model.npz")
model.plot_rel_and_cond()
Hfit_dense = model.evaluate_on_freq(freqs)
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=False)
# (1) Magnitude plot like your screenshot
ax0 = axes[0]
ax0.plot(freqs, np.abs(H11), 'o', ms=4, color='red', label='Samples')
ax0.plot(freqs, np.abs(Hfit_dense), '-', lw=2, color='k', label='Fit')
ax0.plot(freqs_slice, np.abs(H11_slice), 'x', ms=4, color='blue', label='Input Samples')
ax0.set_title("Response i=0, j=0")
ax0.set_ylabel("Magnitude")
ax0.legend(loc="best")
# (2) RMS error vs iteration
# ax1 = axes[1]
# its = np.arange(1, K+1)
# ax1.plot(its, hist["rms_rel"], '-o', lw=2)
# ax1.set_xlabel("Iteration")
# ax1.set_ylabel("RMS error (relative)")
# ax1.grid(True, alpha=0.3)
# ax1.set_title(f"RMS(final) = {hist['rms_rel'][-1]:.3e}")
fig.tight_layout()
plt.savefig(f"img_formula_70.png")