XIAN-FEM-2026June/3D opticsfem-master/tools/verify_eigen3d.py

260 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Verify FemType=5 (3D eigen frequency) outputs without requiring a pre-made reference.
Checks (in order):
1. Self-consistency: normE == sqrt(|Ex|^2+|Ey|^2+|Ez|^2) per mode
2. Independent freq: SciPy eigs(A,B,sigma) on exported A/B vs OutFile/freq
3. Optional MATLAB: compare freq/normE if mat_outdir contains those files
Usage:
python tools/verify_eigen3d.py build/Release/OutFile
python tools/verify_eigen3d.py build/Release/OutFile --json eigen3d.json
python tools/verify_eigen3d.py build/Release/OutFile --mat-outdir path/to/matlab/OutFile
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
import numpy as np
from scipy import sparse
from scipy.sparse.linalg import eigs
C_LIGHT = 2.9979e8
def load_real_coo(prefix: Path) -> sparse.csr_matrix:
ai = np.loadtxt(prefix / "Ai.txt", dtype=np.int64)
aj = np.loadtxt(prefix / "Aj.txt", dtype=np.int64)
av = np.loadtxt(prefix / "Av.txt", dtype=np.float64)
n = int(max(ai.max(), aj.max()) + 1)
return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))
def load_complex_vector_modes(path: Path) -> list[np.ndarray]:
"""Ex/Ey/Ez: each mode is nv complex values."""
modes: list[list[complex]] = []
cur: list[complex] = []
for line in path.read_text().splitlines():
line = line.strip()
if not line:
continue
if line == "//" or line.startswith("//"):
if line.startswith("//") and len(line) > 2:
parts = line[2:].strip().split()
if parts:
cur.append(complex(float(parts[0]), float(parts[1]) if len(parts) > 1 else 0.0))
if cur:
modes.append(cur)
cur = []
continue
parts = line.split()
cur.append(complex(float(parts[0]), float(parts[1])))
if cur:
modes.append(cur)
return [np.asarray(m, dtype=np.complex128) for m in modes]
def load_real_modes(path: Path) -> list[np.ndarray]:
modes: list[list[float]] = []
cur: list[float] = []
for line in path.read_text().splitlines():
line = line.strip()
if not line:
continue
if line == "//" or line.startswith("//"):
if line.startswith("//") and len(line) > 2:
rest = line[2:].strip().split()
if rest:
cur.append(float(rest[0]))
if cur:
modes.append(cur)
cur = []
continue
cur.append(float(line.split()[0]))
if cur:
modes.append(cur)
return [np.asarray(m, dtype=np.float64) for m in modes]
def load_freq(path: Path) -> np.ndarray:
rows = []
for line in path.read_text().splitlines():
line = line.strip()
if not line:
continue
parts = line.split()
rows.append(complex(float(parts[0]), float(parts[1]) if len(parts) > 1 else 0.0))
return np.asarray(rows, dtype=np.complex128)
def self_check(outdir: Path) -> dict:
ex = load_complex_vector_modes(outdir / "Ex")
ey = load_complex_vector_modes(outdir / "Ey")
ez = load_complex_vector_modes(outdir / "Ez")
ne = load_real_modes(outdir / "normE")
n_mode = len(ne)
if not (len(ex) == len(ey) == len(ez) == n_mode):
raise SystemExit(
f"mode count mismatch: Ex={len(ex)} Ey={len(ey)} Ez={len(ez)} normE={n_mode}"
)
rels, corrs = [], []
for i in range(n_mode):
n_calc = np.sqrt(np.abs(ex[i]) ** 2 + np.abs(ey[i]) ** 2 + np.abs(ez[i]) ** 2)
n_ref = ne[i]
if n_ref.size != n_calc.size:
raise SystemExit(f"mode {i}: size {n_ref.size} vs {n_calc.size}")
denom = np.linalg.norm(n_ref)
rel = np.linalg.norm(n_calc - n_ref) / (denom if denom > 0 else 1.0)
corr = np.corrcoef(n_calc, n_ref)[0, 1] if denom > 0 else 1.0
rels.append(rel)
corrs.append(corr)
return {
"n_modes": n_mode,
"n_vertex": int(ne[0].size) if ne else 0,
"normE_rel_err": rels,
"normE_corr": corrs,
}
def scipy_freq_check(outdir: Path, json_path: Path | None) -> dict:
A = load_real_coo_with_names(outdir, "A")
B = load_real_coo_with_names(outdir, "B")
freq_cpp = load_freq(outdir / "freq")
k = freq_cpp.size
ff0 = None
if json_path and json_path.exists():
js = json.loads(json_path.read_text(encoding="utf-8"))
ff0 = float(js.get("searchValue", 0))
lam0 = float(js.get("lambda0", 0.8))
if ff0 <= 0:
ff0 = C_LIGHT / lam0
if ff0 is None or ff0 <= 0:
ff0 = C_LIGHT / 0.8
k0 = 2.0 * np.pi / (C_LIGHT / ff0)
sigma = k0 * k0
print(f" SciPy eigs: n={A.shape[0]}, k={k}, sigma={sigma:.6g} (ff0={ff0:.6g} Hz) ...")
vals, _ = eigs(A, k=k, M=B, sigma=sigma, which="LM")
vals = np.sort(vals.real)[::-1]
freq_scipy = C_LIGHT / (2.0 * np.pi / np.sqrt(vals))
freq_cpp_hz = freq_cpp.real
freq_scipy_hz = np.sort(freq_scipy)[::-1]
# Match modes by nearest frequency (ordering may differ)
used = set()
pairs = []
for f in freq_cpp_hz:
j = int(np.argmin([abs(f - s) if idx not in used else np.inf for idx, s in enumerate(freq_scipy_hz)]))
used.add(j)
pairs.append((f, freq_scipy_hz[j], abs(f - freq_scipy_hz[j]) / max(abs(f), 1e-30)))
rels = [p[2] for p in pairs]
return {
"freq_cpp_Hz": freq_cpp_hz.tolist(),
"freq_scipy_Hz": freq_scipy_hz.tolist(),
"freq_rel_err_matched": rels,
"freq_max_rel_err": float(max(rels) if rels else 0.0),
}
def load_real_coo_with_names(outdir: Path, which: str) -> sparse.csr_matrix:
ai = np.loadtxt(outdir / f"{which}i.txt", dtype=np.int64)
aj = np.loadtxt(outdir / f"{which}j.txt", dtype=np.int64)
av = np.loadtxt(outdir / f"{which}v.txt", dtype=np.float64)
n = int(max(ai.max(), aj.max()) + 1)
return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))
def compare_matlab(outdir: Path, mat_outdir: Path) -> dict:
freq_cpp = load_freq(outdir / "freq").real
if not (mat_outdir / "freq").exists():
return {"skipped": "matlab OutFile/freq not found"}
freq_mat = load_freq(mat_outdir / "freq").real
k = min(freq_cpp.size, freq_mat.size)
freq_cpp = np.sort(freq_cpp[:k])[::-1]
freq_mat = np.sort(freq_mat[:k])[::-1]
rel_f = np.linalg.norm(freq_cpp - freq_mat) / np.linalg.norm(freq_mat)
out = {"freq_rel_err": float(rel_f)}
if (mat_outdir / "normE").exists():
ne_cpp_modes = load_real_modes(outdir / "normE")
ne_mat_modes = load_real_modes(mat_outdir / "normE")
m = min(len(ne_cpp_modes), len(ne_mat_modes))
rels, corrs = [], []
for i in range(m):
a, b = ne_cpp_modes[i], ne_mat_modes[i]
if a.size != b.size:
continue
rels.append(np.linalg.norm(a - b) / np.linalg.norm(b))
corrs.append(np.corrcoef(a, b)[0, 1])
out["normE_rel_err_per_mode"] = rels
out["normE_corr_per_mode"] = corrs
return out
def main() -> None:
parser = argparse.ArgumentParser(description="Verify 3D eigenfrequency OutFile")
parser.add_argument("outdir", type=Path, help="C++ OutFile directory")
parser.add_argument("--json", type=Path, default=None, help="eigen3d.json for sigma")
parser.add_argument("--mat-outdir", type=Path, default=None, help="MATLAB OutFile for comparison")
parser.add_argument("--skip-scipy", action="store_true", help="Skip SciPy eigs (slow)")
args = parser.parse_args()
outdir = args.outdir.resolve()
if not (outdir / "normE").exists():
raise SystemExit(f"missing {outdir / 'normE'}")
print("=== 1. Self-check: normE vs Ex/Ey/Ez ===")
sc = self_check(outdir)
print(f" modes={sc['n_modes']}, vertices/mode={sc['n_vertex']}")
for i, (rel, corr) in enumerate(zip(sc["normE_rel_err"], sc["normE_corr"])):
ok = "OK" if rel < 1e-10 else "WARN"
print(f" mode {i}: rel_err={rel:.3e}, corr={corr:.6f} [{ok}]")
if not args.skip_scipy and (outdir / "Bi.txt").exists():
print("\n=== 2. Independent freq: SciPy eigs(A,B) vs freq ===")
js = args.json
if js is None:
cand = outdir.parent / "eigen3d.json"
js = cand if cand.exists() else None
fc = scipy_freq_check(outdir, js)
print(f" C++ freq (Hz): {[f'{x:.6e}' for x in fc['freq_cpp_Hz']]}")
print(f" SciPy freq (Hz): {[f'{x:.6e}' for x in fc['freq_scipy_Hz']]}")
print(f" matched max rel err: {fc['freq_max_rel_err']:.3e}")
if fc["freq_max_rel_err"] < 1e-4:
print(" => freq solver consistent with exported A/B [OK]")
else:
print(" => freq mismatch — check solver or matrix export [WARN]")
if args.mat_outdir:
print(f"\n=== 3. Compare MATLAB: {args.mat_outdir} ===")
mc = compare_matlab(outdir, args.mat_outdir.resolve())
if mc.get("skipped"):
print(f" skipped: {mc['skipped']}")
else:
print(f" freq rel err: {mc['freq_rel_err']:.3e}")
if "normE_rel_err_per_mode" in mc:
for i, (rel, corr) in enumerate(
zip(mc["normE_rel_err_per_mode"], mc["normE_corr_per_mode"])
):
print(f" normE mode {i}: rel={rel:.3e}, corr={corr:.6f}")
print("\n--- How to get a MATLAB reference ---")
print(" cd 三维matlab代码/matlab 3D一阶本征问题/3D一阶本征问题2")
print(" % need SBCmesh.mat (from COMSOL/export); then run main.m")
print(" % add after main.m loop:")
print(" % writematrix([solverff0.real, zeros(num,1)], 'OutFile/freq')")
print(" % for i=1:numberSolve, export one normE block with // separator")
print(" Then: python tools/verify_eigen3d.py ... --mat-outdir path/to/matlab/OutFile")
if __name__ == "__main__":
main()