feat: 完成了算法的最基础部分

This commit is contained in:
mayge
2025-09-20 12:02:24 -04:00
parent ec5e2aaddc
commit e62e8df013
10 changed files with 381 additions and 1202 deletions

241
core/basis.py Normal file
View File

@@ -0,0 +1,241 @@
import numpy as np
from core.sk_iter import generate_starting_poles
from scipy.linalg import block_diag
import skrf as rf
from skrf import VectorFitting
from core.freqency import auto_select
class BasicBasis:
def __init__(self,H,freqs,poles,weights=None):
self.least_squares_rms_error = None
self.least_squares_condition = None
self.eigenval_condition = None
self.eigenval_rms_error = None
self.H = H
self.freqs = freqs
self.s = self.freqs * 2j * np.pi
self.P = len(poles)
self.poles = poles
self.Phi = self.generate_basis(self.s, self.poles)
self.A = self.matrix_A(self.poles)
self.B = self.vector_B(self.poles)
self.D = 1.0
self.Cr,self.Cw = self.fit_denominator(self.H, d0=1.0, weights=weights)
z = np.linalg.eigvals(self.A - self.B @ self.Cw)
p_next = -z
# enforce LHP and pair ordering
p_next = np.where(np.real(p_next) < 0, p_next, -np.conj(p_next))
p_next = np.sort_complex(p_next)
self.next_poles = p_next
# z = np.where(np.real(z) < 0, z, -np.conj(z)) # enforce LHP
# self.next_poles = np.sort_complex(z)
self.eigenval_condition = np.linalg.cond(self.A - self.B @ self.Cw)
self.eigenval_rms_error = np.sqrt(np.mean(np.abs(np.real(z) - np.real(poles))**2 + np.abs(np.imag(z) - np.imag(poles))**2))
self.Dt = self.eval_Dt_state_space()
self.delta = self.Dt / weights if weights is not None else self.Dt
pass
def eval_Dt_state_space(self):
"""Return D(s_k)=C(s_k I - A)^(-1)B + D for all k (complex 1D array)."""
s = 1j * 2*np.pi * np.asarray(self.freqs, float).ravel()
A = np.asarray(self.A, np.complex128); n = A.shape[0]
B = np.asarray(self.B, np.complex128).reshape(n, 1)
C = np.asarray(self.Cw, float).reshape(1, n)
D = self.D
I = np.eye(n, dtype=np.complex128)
out = np.empty_like(s, dtype=np.complex128)
for k, sk in enumerate(s):
DS = D + (C @ np.linalg.inv(sk*I - A) @ B)
out[k] = DS[0, 0]
return out
def generate_basis(self,s, poles):
"""Real basis of (15)-(16); returns Φ(s) and a layout for packing C."""
cols = []
i = 0
while i < len(poles):
p = poles[i]
if p.real > 0:
raise ValueError("poles must be in the LHP")
if i+1 < len(poles) and np.isclose(poles[i+1], np.conj(p)):
pc = poles[i+1]
phi1 = 1/(s - p) + 1/(s - pc) # eq (15)generate_basis
phi2 = 1j*(1/(s - p) - 1/(s - pc)) # eq (16) (fixed sign)
cols += [phi1, phi2]
i += 2
else:
cols.append(1/(s - p))
i += 1
Phi = np.column_stack(cols).astype(np.complex128)
return Phi
def matrix_A(self, poles):
def A_block(p):
if abs(p.imag) < 1e-14:
return np.array([[p.real]], float) # A_p = [ p ]
return np.array([[p.real, p.imag], # A_p = [[Re p, Im p],
[-p.imag, p.real]], float) # [-Im p, Re p]]
A = None; i = 0
while i < len(poles):
p = poles[i]
Ab = A_block(p)
if i+1 < len(poles) and np.isclose(poles[i+1], np.conj(p)): i += 2
else: i += 1
A = Ab if A is None else block_diag(A, Ab)
return A
def vector_B(self, poles):
def B_block(p):
return np.array([[1.0]], float) if abs(p.imag)<1e-14 else np.array([[2.0],[0.0]], float)
B = None; i = 0
while i < len(poles):
p = poles[i]
Bb = B_block(p)
if i+1 < len(poles) and np.isclose(poles[i+1], np.conj(p)): i += 2
else: i += 1
B = Bb if B is None else np.vstack([B, Bb])
return B
def fit_denominator(self, H, d0=1.0, weights=None):
"""
Solve formula (70) on the real basis Φ to obtain:
- d (real) → packs into C for this state's block structure
- gamma (complex)
Optional 'weights' (K,) apply row scaling: SK weighting if 1/|D_prev|.
"""
if weights is None:
weights = np.diag(np.ones(len(H), np.complex128))
else:
weights = np.diag([1/res for res in weights])
s = self.s
H = np.asarray(H, np.complex128).reshape(-1,1)
Phi = self.Phi
psi = weights @ Phi
psi = Phi
HPhi = H * Phi
A_re = np.hstack([np.real(-psi), np.real(-HPhi)])
A_im = np.hstack([np.imag(-psi), np.imag(-HPhi)])
b_re = np.real(d0 * H)
b_im = np.imag(d0 * H)
A = np.vstack([A_re, A_im]).astype(float)
# rown = np.linalg.norm(A, axis=1)
# rown = np.sqrt(rown)
# A = rown[:,None] * A
b = np.concatenate([b_re, b_im]).astype(float)
x = np.linalg.inv(A.T @ A) @ A.T @ b
self.least_squares_rms_error = np.sqrt(np.mean((A @ x - b)**2))
self.least_squares_condition = np.linalg.cond(A)
Cn,Cd = self.vector_C(x)
return Cn,Cd
def vector_C(self,x):
Cn = np.asarray([x[:len(x)//2]], float).reshape(1,-1)
Cd = np.asarray([x[len(x)//2:]], float).reshape(1,-1)
return Cn, Cd
def evaluate(self,freqs,poles,Cn,Cd,d0=1.0):
s = 1j * 2*np.pi * np.asarray(freqs, float).ravel()
phi = self.generate_basis(s, poles)
num = phi @ Cn.T
den = d0 + phi @ Cd.T
H = num / den
return H.ravel()
if __name__ == "__main__":
network = rf.Network("/tmp/paramer/simulation/3000/3000.s2p")
K = 10
H11,freqs = auto_select([network.y[i][0][0] for i in range(2,len(network.y))],network.f[2:],max_points=20)
poles = generate_starting_poles(2,beta_min=freqs[0]/1.1,beta_max=freqs[-1]*1.1)
Dt_1 = np.ones((len(freqs),1),np.complex128)
# Levi step (no weighting):
basis = BasicBasis(H11,freqs,poles=poles)
Dt = basis.Dt
poles = basis.next_poles
print("Levi step (no weighting):")
print("A:",basis.A)
print("B:",basis.B)
print("C:",basis.Cw)
print("D:",basis.D)
print("next_pozles:",basis.next_poles)
print("Dt:",Dt, "norm:",np.linalg.norm(Dt))
# SK weighting (optional, after first pass):
least_squares_condition = []
least_squares_rms_error = []
eigenval_condition = []
eigenval_rms_error = []
for i in range(K):
basis = BasicBasis(H11,freqs,poles=poles,weights=Dt)
Dt_1 = Dt
Dt = basis.Dt
poles = basis.next_poles
print(f"SK Iteration {i+1}/{K}")
print("A:",basis.A)
print("B:",basis.B)
print("C:",basis.Cw)
print("D:",basis.D)
print("z:",basis.next_poles)
print("Dt:",Dt)
print("Dt/Dt-1",np.linalg.norm(Dt) / np.linalg.norm(Dt_1))
least_squares_condition.append(basis.least_squares_condition)
least_squares_rms_error.append(basis.least_squares_rms_error)
eigenval_condition.append(basis.eigenval_condition)
eigenval_rms_error.append(basis.eigenval_rms_error)
# H11_evaluated = basis.evaluate_pole_residue(network.f[1:],poles,basis.C[0])
H11_evaluated = basis.evaluate(network.f[2:], poles, basis.Cr[0],basis.Cw[0], d0=1.0)
import matplotlib.pyplot as plt
fig, axes = plt.subplots(3, 2, figsize=(15, 16), sharex=False)
ax0 = axes[0][0]
ax0.plot(network.f[2:], np.abs([network.y[i][0][0] for i in range(2,len(network.y))]), 'o', ms=4, color='red', label='Samples')
ax0.plot(network.f[2:], np.abs(H11_evaluated), '-', lw=2, color='k', label='Fit')
ax0.plot(freqs, np.abs(H11), 'x', ms=4, color='blue', label='Input Samples')
ax0.set_title("Response i=0, j=0")
ax0.set_ylabel("Magnitude")
ax0.legend(loc="best")
ax1 = axes[1][0]
ax1.plot(least_squares_condition, label='Least Squares Condition')
ax1.set_title("least_squares_condition")
ax1.set_ylabel("Magnitude")
ax1.legend(loc="best")
ax2 = axes[1][1]
ax2.plot(least_squares_rms_error, label='Least Squares RMS Error')
ax2.set_title("least_squares_rms_error")
ax2.set_ylabel("Magnitude")
ax2.legend(loc="best")
ax3 = axes[2][0]
ax3.plot(eigenval_condition, label='Eigenvalue Condition')
ax3.set_title("eigenval_condition")
ax3.set_ylabel("Magnitude")
ax3.legend(loc="best")
ax4 = axes[2][1]
ax4.plot(eigenval_rms_error, label='Eigenvalue RMS Error')
ax4.set_title("eigenval_rms_error")
ax4.set_ylabel("Magnitude")
ax4.legend(loc="best")
fig.tight_layout()
plt.savefig(f"basic_basis.png")

131
core/freqency.py Normal file
View File

@@ -0,0 +1,131 @@
import numpy as np
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]

View File

@@ -1,36 +1,5 @@
import numpy as np
# ------------------------------------------------------------
# 按论文公式 (9)(10)(11) 生成 MuntzLaguerre 正交有理基 (解析形式)
#
# 给定稳定极点集合 {p_k} (Re(p_k)<0)。论文记法中使用 -a_k其中 Re(a_k)>0。
# 对应关系p_k = -a_k ⇒ a_k = -p_k, Re(a_k)= -Re(p_k) >0
#
# 连续内积意义下(沿 jω 轴积分)这些 φ_k 解析正交。离散频率采样后数值上
# 可能偏离,可再用加权 QR 做数值再正交(可选)。
#
# 公式在“稳定极点 p 表达”下的改写:
# (原) 实极点: φ_p(s) = sqrt(2 Re(a_p)) / (s + a_p) * Π (s - a_i^*)/(s + a_i)
# 变换 a_p = -p ⇒ Re(a_p)= -Re(p) = σ >0 且 (s + a_p) = (s - p)
# 且乘积 (s - a_i^*)/(s + a_i) = (s + p_i^*)/(s - p_i)
# ⇒ φ_p(s) = sqrt(-2 Re(p)) / (s - p) * Π_{i<p} (s + p_i^*)/(s - p_i)
#
# 复对 (p, p*),取 imag(p)>0 的 p 作为首:
# (原) φ_p = sqrt(2 Re(a_p)) (s - |a_p|)/[(s + a_p)(s + a_p^*)] * Π(...)
# (原) φ_{p+1}= sqrt(2 Re(a_p)) (s + |a_p|)/[(s + a_p)(s + a_p^*)] * Π(...)
# 代入 a_p=-p
# Re(a_p)= -Re(p)=σ>0, (s + a_p) = (s - p), (s + a_p^*)=(s - p^*)
# |a_p| = |p|
# 乘积同上 ⇒ Π_{i<p} (s + p_i^*)/(s - p_i)
#
# ⇒ φ_p(s) = sqrt(-2 Re(p)) (s - |p|)/[(s - p)(s - p^*)] * Π_{i<p} (s + p_i^*)/(s - p_i)
# φ_{p^*}(s)= sqrt(-2 Re(p)) (s + |p|)/[(s - p)(s - p^*)] * Π_{i<p} (s + p_i^*)/(s - p_i)
#
# product 递推维护prod_k = Π_{i≤k} (s + p_i^*)/(s - p_i)
# 对复对要顺序乘两次p 与 p*)。
# ------------------------------------------------------------
def generate_laguerre_basis(poles:list|np.ndarray, s: np.ndarray):
basis = np.zeros((len(poles)+1,len(s)),dtype=complex)
@@ -82,93 +51,3 @@ def generate_laguerre_basis(poles:list|np.ndarray, s: np.ndarray):
i += 1
basis[i + 1] = phi
return basis
# class MuntzLaguerreIterator:
# def __init__(self, s: np.ndarray, stable_poles: list | np.ndarray):
# """
# s: 复频率数组 (Nf,), s = j 2π f
# stable_poles: 稳定极点列表 (Re<0). 复共轭对要求正虚部在前 (p, p*).
# """
# self.s = np.asarray(s, dtype=complex)
# self.poles = list(stable_poles)
# self.N = len(self.poles)
# self.k = 0
# # 初始化乘积 Π_{i<p} (s + p_i^*)/(s - p_i)
# self.product = np.ones_like(self.s, dtype=complex)
# def __iter__(self):
# return self
# def __next__(self):
# if self.k >= self.N:
# raise StopIteration
# p = self.poles[self.k]
# if np.real(p) >= 0:
# raise ValueError(f"极点必须在左半平面: {p}")
# # 复对首 (正虚部)
# if np.iscomplex(p) and np.imag(p) > 0:
# if self.k + 1 >= self.N:
# raise ValueError("复极点缺少共轭")
# pc = self.poles[self.k + 1]
# if not np.isclose(pc, np.conj(p)):
# raise ValueError("复极点未按 (p, p*) 顺序排列 (正虚部在前)")
# sigma = -np.real(p) # >0
# scale = np.sqrt(2 * sigma)
# r = np.abs(p)
# denom = (self.s - p) * (self.s - pc)
# # 两个基函数
# phi_p = scale * (self.s - r) / denom * self.product
# phi_pc = scale * (self.s + r) / denom * self.product
# # product 先乘 (s + p^*)/(s - p),再乘 (s + p)/(s - p^*)
# self.product = self.product * (self.s + pc) / (self.s - p)
# self.product = self.product * (self.s + p) / (self.s - pc)
# self.k += 2
# return [phi_p, phi_pc]
# # 复对次 (负虚部) —— 应该被首元素处理,出现表示顺序错误
# if np.iscomplex(p) and np.imag(p) < 0:
# raise ValueError("检测到负虚部复极点但其共轭尚未处理,请将正虚部成员放在前面。")
# # 实极点
# sigma = -np.real(p)
# if sigma <= 0:
# raise ValueError("实极点实部应为负 (稳定)。")
# scale = np.sqrt(2 * sigma)
# phi = scale / (self.s - p) * self.product
# # 更新乘积
# self.product = self.product * (self.s + p) / (self.s - p)
# self.k += 1
# return [phi]
# def generate_muntz_laguerre_basis(s: np.ndarray, init_poles: list | np.ndarray):
# """
# 生成完整基函数列表: [φ_0=1, φ_1, φ_2, ...]
# """
# basis = [np.ones_like(s, dtype=complex)]
# for block in MuntzLaguerreIterator(s, init_poles):
# basis.extend(block)
# return basis
# if __name__ == "__main__":
# # 示例稳定极点 (复对正虚部在前)
# stable_poles = [
# -0.8e9,
# -1.0e9 + 2.5e9j,
# -1.0e9 - 2.5e9j,
# -2.2e9
# ]
# freqs = np.linspace(1e8, 8e9, 400)
# s = 1j * 2 * np.pi * freqs
# basis = generate_muntz_laguerre_basis(s, stable_poles)
# print(f"生成 {len(basis)} 个基函数,{basis}")

View File

@@ -1,191 +0,0 @@
from models.basic import ModelBasic
from typing import List,Literal,Dict
import numpy as np
import random
from pydantic import BaseModel
from skrf import Network
from models.basic import ModelBasicDatasetUnit
class RobustParametricConfig(BaseModel):
n_poles: int
max_iter: int = 10
parameter_type: Literal["s","y","z"]
class RobustParametricModel:
def __init__(self,model:ModelBasic,config:RobustParametricConfig):
self.model = model
self.config = config
# 区分训练集和测试集
def _train_test_split(self,train_ratio:float=0.8,random_state:int=42):
random.seed(random_state)
dataset = self.model.results
random.shuffle(dataset)
train_size = int(len(dataset)*train_ratio)
self.train_set = dataset[:train_size]
self.test_set = dataset[train_size:]
return self.train_set, self.test_set
def first_iteration_step(self, datasets: List[ModelBasicDatasetUnit],
param_degree: int = 1):
'''
SK 迭代的第一步 (t=0) 对应论文公式 (3).
目标:
最小化 sum_{样本 n, 频点 f} | N^{(0)}(s_f, g_n) - H(s_f, g_n) |^2
在 t=0 阶段我们令 D^{(0)}(s,g)=1, 仅拟合分子:
N^{(0)}(s,g) = Σ_{p∈P} Σ_{v∈V} c_{p,v} * φ_freq_p(s) * φ_param_v(g)
因此是一个纯线性最小二乘:
H ≈ Σ_{p,v} c_{p,v} F[:,p] ⊗ G[n,v]
记:
F: (Nf, P) 频率正交(或原始)基列
G: (Ns, V) 参数多项式(含常数)基列
设计矩阵 A 大小 (Ns*Nf, P*V), A[(n*Nf + f), (p*V + v)] = F[f,p] * G[n,v]
输出:
self.first_iter_coeffs[(i,j)] = C_{p,v} (P × V) 每个端口对一套系数
self.freq_basis_F = F
self.param_basis_G = G
self.poles 初始极点 (供后续构造有理正交基 / SK 加权使用)
参数:
datasets: 训练用样本列表
param_degree: 参数多项式最大总次数 (默认 1 → 常数 + 线性)
注意:
这里使用简单频率基 [1, 1/(s-a_k)],未做 QR 正交化;
后续 SK 迭代 / 正交化可在第二步再进行。
'''
assert len(datasets) > 0, "空数据集"
# ---------------- 收集频率与端口信息 ----------------
Ns = len(datasets)
freqs = datasets[0].freqs # 频率需要对齐
Nf = freqs.shape[0]
nports = self.model.info.nports
# ---------------- 构造初始极点 (对数分布 + 阻尼) ----------------
def _init_poles(n_poles: int):
fmin, fmax = freqs[0], freqs[-1]
if n_poles <= 0:
return np.array([], dtype=complex)
# 避免 0 Hz用第二个点或微小偏移
start_f = max(fmin if fmin > 0 else freqs[1] if Nf > 1 else 1.0e-3, 1e-12)
f_samples = np.logspace(np.log10(start_f), np.log10(fmax), n_poles)
sigma = 0.02 * 2 * np.pi * fmax # 固定阻尼,可放入 config
return -sigma + 1j * 2 * np.pi * f_samples
self.poles = _init_poles(self.config.n_poles)
s_vec = 1j * 2 * np.pi * freqs # (Nf,)
# ---------------- 频率基 F ----------------
# 列0: 常数 1, 后续列: 1/(s - a_k)
P = 1 + len(self.poles)
F = np.zeros((Nf, P), dtype=complex)
F[:, 0] = 1.0
for k, a in enumerate(self.poles, start=1):
F[:, k] = 1.0 / (s_vec - a)
self.freq_basis_F = F # (Nf, P)
# ---------------- 参数基 G ----------------
# 取出参数向量 g = [param1, param2, ...]
# 假设 datasets[i].parameters 为 dict
param_keys = list(datasets[0].parameters.keys())
d = len(param_keys)
# 构造参数矩阵 (Ns,d)
Pmat = np.zeros((Ns, d), dtype=float)
for i, unit in enumerate(datasets):
for j, k in enumerate(param_keys):
Pmat[i, j] = float(unit.parameters[k])
# 标准化
mean = Pmat.mean(axis=0)
std = Pmat.std(axis=0) + 1e-15
Xn = (Pmat - mean) / std
# 生成多项式指数 (总次数 <= param_degree)
def _gen_param_exps(dim, D):
exps = []
def rec(cur, idx, rem):
if idx == dim:
exps.append(tuple(cur)); return
for t in range(rem + 1):
cur.append(t); rec(cur, idx + 1, rem - t); cur.pop()
rec([], 0, D)
return exps
exps = _gen_param_exps(d, param_degree) # V_exps
V = len(exps)
G = np.zeros((Ns, V), dtype=float)
for vidx, e in enumerate(exps):
val = np.ones(Ns)
for j, p in enumerate(e):
if p:
val *= Xn[:, j] ** p
G[:, vidx] = val
self.param_basis_G = G # (Ns, V)
self.param_basis_meta = {
"keys": param_keys,
"mean": mean.tolist(),
"std": std.tolist(),
"exponents": exps,
"degree": param_degree
}
# ---------------- 构造设计矩阵 A (Kronecker) ----------------
# A shape: (Ns*Nf, P*V)
# 利用广播A_block[n,f,p,v] = F[f,p] * G[n,v]
A4 = np.einsum('fp,nv->nfpv', F, G) # (Ns, Nf, P, V)
A = A4.reshape(Ns * Nf, P * V)
# ---------------- 采集目标 H 数据 ----------------
# 对每个端口对 (i,j) 分别解一个向量 c_{p,v}
# 选择参数类型
param_type = self.config.parameter_type.lower()
valid_types = {"s", "y", "z"}
if param_type not in valid_types:
raise ValueError(f"parameter_type 必须在 {valid_types}")
# 预先载入全部 Network (避免重复 IO)
# H_data[(i,j)] -> (Ns,Nf) 复数
H_data = {}
for i_port in range(nports):
for j_port in range(nports):
H_mat = np.zeros((Ns, Nf), dtype=complex)
for n, unit in enumerate(datasets):
net: Network = unit.network
if param_type == "s":
sij = net.s[:, i_port, j_port]
elif param_type == "y":
sij = net.y[:, i_port, j_port]
else:
sij = net.z[:, i_port, j_port]
# 插值或对齐假设已完成
H_mat[n, :] = sij
H_data[(i_port, j_port)] = H_mat
# ---------------- 最小二乘求系数 ----------------
self.first_iter_coeffs = {}
# 可选: 预计算 A^+ (伪逆) 如果数据不巨大
# pinv = np.linalg.pinv(A)
for key, Hmat in H_data.items():
b = Hmat.reshape(Ns * Nf) # (Ns*Nf,)
# 解 x (长度 P*V)
x, *_ = np.linalg.lstsq(A, b, rcond=None)
C = x.reshape(P, V)
self.first_iter_coeffs[key] = C
# ---------------- 存储便于后续 SK 迭代使用 ----------------
self.meta_first_iter = {
"A_shape": A.shape,
"num_samples": Ns,
"num_freqs": Nf,
"freq_basis_dim": P,
"param_basis_dim": V
}
if getattr(self.config, "verbose", True):
print(f"[t=0] 线性最小二乘完成: A={A.shape}, 频率基P={P}, 参数基V={V}, 端口对={nports*nports}")
return self.first_iter_coeffs

View File

@@ -2,7 +2,6 @@ import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Tuple
# ---------- 1. 生成初始极点(按论文 Re(-a_p) 小、Im 线性分布) ----------
def generate_starting_poles(n_pairs: int, beta_min: float, beta_max: float, alpha_scale: float = 0.01):
"""
仅生成复共轭对: p = -alpha + j beta, p*。
@@ -19,308 +18,3 @@ def generate_starting_poles(n_pairs: int, beta_min: float, beta_max: float, alph
poles += [p, np.conj(p)]
print(f"生成 {len(poles)} 个初始极点 (复对) {poles}]")
return poles
# ---------- 2. MuntzLaguerre 频率基 (与你现有 orthonormal_basis 中一致的核心) ----------
def muntz_laguerre_basis(freqs_hz: np.ndarray, stable_poles: List[complex]) -> np.ndarray:
"""
返回 Φ_ml (Nf, P) 列顺序: φ_0=1, 后续依次 (单极点 / 复对两列)
"""
s = 1j * 2 * np.pi * freqs_hz
basis = [np.ones_like(s, dtype=complex)]
product = np.ones_like(s, dtype=complex)
k = 0
N = len(stable_poles)
while k < N:
p = stable_poles[k]
if np.real(p) >= 0:
raise ValueError("极点需在左半平面")
if np.imag(p) > 0: # 复对首
if k + 1 >= N or not np.isclose(stable_poles[k+1], np.conj(p)):
raise ValueError("复对未配对")
pc = stable_poles[k+1]
sigma = -np.real(p)
scale = np.sqrt(2 * sigma)
r = np.abs(p)
denom = (s - p) * (s - pc)
phi_p = scale * (s - r) / denom * product
phi_pc = scale * (s + r) / denom * product
basis.extend([phi_p, phi_pc])
# update product
product = product * (s + pc)/(s - p) * (s + p)/(s - pc)
k += 2
elif np.imag(p) < 0:
raise ValueError("负虚部复极点必须跟在正虚部之后")
else: # 实极点
sigma = -np.real(p)
scale = np.sqrt(2 * sigma)
phi = scale / (s - p) * product
basis.append(phi)
product = product * (s + p)/(s - p)
k += 1
return np.column_stack(basis) # (Nf, P)
# ---------- 3. 原始部分分式基 ----------
def raw_partial_fraction_basis(freqs_hz: np.ndarray, stable_poles: List[complex]) -> np.ndarray:
s = 1j * 2 * np.pi * freqs_hz
cols = [np.ones_like(s, dtype=complex)]
for p in stable_poles:
cols.append(1.0 / (s - p))
return np.column_stack(cols) # (Nf, P)
# ---------- 4. 变换矩阵 Φ_ml T = G_raw ----------
def compute_transform_matrix(Phi_ml: np.ndarray, G_raw: np.ndarray, weights: np.ndarray | None = None) -> np.ndarray:
# 加权最小二乘 T = (Φ^H W Φ)^{-1} Φ^H W G
if weights is None:
WPhi = Phi_ml
WG = G_raw
M = Phi_ml.conj().T @ WPhi
RHS = Phi_ml.conj().T @ WG
else:
w = weights[:, None]
M = Phi_ml.conj().T @ (w * Phi_ml)
RHS = Phi_ml.conj().T @ (w * G_raw)
T = np.linalg.solve(M, RHS)
return T # (P,P), Φ T = G
# ---------- 5. 参数多项式正交基 ----------
def generate_param_basis(param_matrix: np.ndarray, total_degree: int):
"""
param_matrix: (Ns, d)
返回:
Qμ: (Ns, V) 正交列
Rμ: (V, V) 上三角
exps: 多指数列表
"""
X = param_matrix
Ns, d = X.shape
# 生成多指数
exps=[]
def rec(cur,i,rem):
if i==d:
exps.append(tuple(cur)); return
for k in range(rem+1):
cur.append(k); rec(cur,i+1,rem-k); cur.pop()
rec([],0,total_degree)
V = len(exps)
M = np.zeros((Ns, V))
for idx,e in enumerate(exps):
v = np.ones(Ns)
for j,p in enumerate(e):
if p: v *= X[:,j]**p
M[:,idx]=v
Q,R = np.linalg.qr(M)
return Q,R,exps
# ---------- 6. t=0 构建设计矩阵并解 C ----------
def fit_t0(H_list: List[np.ndarray], Phi_f: np.ndarray, : np.ndarray) -> np.ndarray:
"""
H_list: 长度 Ns, 每项 (Nf,) 复数
Phi_f: (Nf, P)
Qμ: (Ns, V)
返回: C (P,V)
"""
Ns = len(H_list); Nf,P = Phi_f.shape; V = .shape[1]
cols = P*V
A = np.zeros((Ns*Nf, cols), dtype=complex)
b = np.zeros(Ns*Nf, dtype=complex)
r=0
for n,Hn in enumerate(H_list):
blk = np.einsum('fp,v->fpv', Phi_f, [n]).reshape(Nf, cols)
A[r:r+Nf,:]=blk
b[r:r+Nf]=Hn
r+=Nf
x, *_ = np.linalg.lstsq(A, b, rcond=None)
C = x.reshape(P, V)
return C
# ---------- 7. t=1 (首轮 SK) 解 C, Ct ----------
def fit_t1_SK(H_list: List[np.ndarray], Phi_f: np.ndarray, : np.ndarray,
C_prev: np.ndarray, max_iter: int =1, tol: float =1e-3):
"""
只做一轮 (或少量) SK初始 D=1。
返回: C_new, Ct_new
"""
Ns = len(H_list); Nf,P = Phi_f.shape; V = .shape[1]
# 初始化分母系数 Ct: Ct[0,0]=1
Ct = np.zeros((P,V), dtype=complex)
Ct[0,0]=1.0
C = C_prev.copy()
for it in range(max_iter):
# 构建线性系统: (N - D*H)=0
# 未知顺序: C(:), Ct(:) 去掉固定 Ct[0,0]
mask_fix = np.zeros((P,V), dtype=bool); mask_fix[0,0]=True
col_map={}
col=0
for i in range(P):
for j in range(V):
col_map[('C',i,j)]=col; col+=1
for i in range(P):
for j in range(V):
if mask_fix[i,j]: continue
col_map[('Ct',i,j)]=col; col+=1
A = np.zeros((Ns*Nf, col), dtype=complex)
b = np.zeros(Ns*Nf, dtype=complex)
r=0
# 评价当前 D_prev= Σ Ct φ ψ
for n,Hn in enumerate(H_list):
# 分子块 (Φ_f ⊗ ψ_n)
blk = np.einsum('fp,v->fpv', Phi_f, [n]).reshape(Nf, P*V)
# 填 C 部分
for i in range(P):
for j in range(V):
A[r:r+Nf, col_map[('C',i,j)]] = blk[:, i*V + j]
# 分母块: - H * blk
for i in range(P):
for j in range(V):
if mask_fix[i,j]: continue
A[r:r+Nf, col_map[('Ct',i,j)]] = - Hn * blk[:, i*V + j]
r+=Nf
# 求解
x, *_ = np.linalg.lstsq(A, b, rcond=None)
# 拆回
idx=0
for i in range(P):
for j in range(V):
C[i,j]=x[idx]; idx+=1
Ct_new = Ct.copy()
for i in range(P):
for j in range(V):
if mask_fix[i,j]: continue
Ct_new[i,j]=x[idx]; idx+=1
# 收敛性简单检查:分母变化
diff = np.max(np.abs(Ct_new - Ct))
Ct = Ct_new
if diff < tol:
break
return C, Ct
# ---------- 8. 假极点检测 (在 raw 基上) ----------
def detect_spurious_poles(C_ml: np.ndarray, Ct_ml: np.ndarray, T: np.ndarray,
: np.ndarray,
tau_cancel=1e-2, tau_small=1e-3, eps=1e-14):
"""
C_ml, Ct_ml: (P,V) (Muntz-Laguerre 基)
T: Φ_ml T = G_raw
Qμ: (Ns,V)
"""
# raw 系数: c_raw = T^{-1} c_ml
Tinv = np.linalg.inv(T)
C_raw = Tinv @ C_ml
Ct_raw = Tinv @ Ct_ml
P,V = C_ml.shape
Ns = .shape[0]
r_vals = C_raw @ .T
q_vals = Ct_raw @ .T
metrics={}
scales=[]
for k in range(1,P): # 跳过常数列
rv = r_vals[k]; qv = q_vals[k]
diff = np.max(np.abs(rv - qv))
scale = np.max([np.max(np.abs(rv)), np.max(np.abs(qv)), eps])
eta = diff / scale
metrics[k] = {"diff": diff, "scale": scale, "eta": eta}
scales.append(scale)
S_max = max(scales) if scales else 1.0
cancel_spurious = [k for k in range(1,P)
if metrics[k]["eta"] < tau_cancel and metrics[k]["scale"] > tau_small * S_max]
small_spurious = [k for k in range(1,P)
if metrics[k]["scale"] <= tau_small * S_max]
return {
"cancel_spurious": cancel_spurious,
"small_spurious": small_spurious,
"metrics": metrics,
"S_max": S_max,
"C_raw": C_raw,
"Ct_raw": Ct_raw
}
# ---------- 9. 统一入口 ----------
@dataclass
class OPVFResult:
C_ml: np.ndarray
Ct_ml: np.ndarray
C_raw: np.ndarray
Ct_raw: np.ndarray
T: np.ndarray
: np.ndarray
: np.ndarray
exps: List[Tuple[int]]
poles: List[complex]
spurious: Dict
def opvf_from_H(H_list: List[np.ndarray],
freqs_hz: np.ndarray,
param_matrix: np.ndarray,
total_degree: int,
poles: List[complex],
do_t1: bool = True) -> OPVFResult:
"""
H_list: 长度 Ns; 每项 shape (Nf,)
freqs_hz: (Nf,)
param_matrix: (Ns,d) 归一化前参数值 (内部不自动标准化以保持可控)
poles: 初始稳定极点列表
"""
# 频率基
Phi_ml = muntz_laguerre_basis(freqs_hz, poles) # (Nf,P)
G_raw = raw_partial_fraction_basis(freqs_hz, poles) # (Nf,P)
# 简单权 (ω 权): w = Δf (因 (1/2π)∫ φφ* dω => ∑ Δf)
w = _trap_weights(freqs_hz)
T = compute_transform_matrix(Phi_ml, G_raw, w)
# 参数基
, , exps = generate_param_basis(param_matrix, total_degree)
# t=0
C_ml = fit_t0(H_list, Phi_ml, )
if do_t1:
C_ml, Ct_ml = fit_t1_SK(H_list, Phi_ml, , C_ml, max_iter=1)
else:
# D=1
P,V = C_ml.shape
Ct_ml = np.zeros((P,V), dtype=complex)
Ct_ml[0,0]=1.0
# 假极点检测
spurious = detect_spurious_poles(C_ml, Ct_ml, T, )
return OPVFResult(
C_ml=C_ml,
Ct_ml=Ct_ml,
C_raw=spurious["C_raw"],
Ct_raw=spurious["Ct_raw"],
T=T,
=,
=,
exps=exps,
poles=poles,
spurious=spurious
)
# ---------- 辅助: 梯形权 ----------
def _trap_weights(f: np.ndarray):
if len(f)==1: return np.ones(1)
df = np.diff(f)
w = np.zeros_like(f)
w[0]=0.5*df[0]; w[-1]=0.5*df[-1]
if len(f)>2:
w[1:-1]=0.5*(df[:-1]+df[1:])
return w
# ---------- 简单测试占位 ----------
if __name__ == "__main__":
# 虚构数据: 2 个参数样本 (Ns=2), 频率 200 点
freqs = np.linspace(1e8, 5e9, 200)
# 真正模型 (示例): H = Σ_k R_k/(s - p_k)
true_poles = [-0.5e3 + 1.2e9j, -0.5e3 - 1.2e9j]
s = 1j*2*np.pi*freqs
def synth(Rs):
return Rs[0]/(s - true_poles[0]) + Rs[1]/(s - true_poles[1])
H_list = [synth([0.8+0.1j, 1.2-0.2j]), synth([0.9+0.05j, 1.1+0.1j])]
params = np.array([[0.0],[1.0]]) # 1 维参数
# 给一个冗余极点集合 (含真实 + 额外)
start_poles = generate_starting_poles(n_pairs=10, beta_min=5e8, beta_max=2.0e9, alpha_scale=0.000001)
res = opvf_from_H(H_list, freqs, params, total_degree=1, poles=start_poles)
print("C_ml shape:", res.C_ml.shape)
print("假极点(cancel):", res.spurious["cancel_spurious"])
print("假极点(small):", res.spurious["small_spurious"])