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

230 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Diagnose double-PBC A matrix diff: MATLAB vs C++ projection."""
from __future__ import annotations
import math
import re
from collections import Counter, defaultdict
from pathlib import Path
import numpy as np
import scipy.io as sio
from scipy import sparse
ROOT = Path(__file__).resolve().parents[1]
MAT_ROOT = ROOT.parent / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+周期边界"
CPP_OUT = ROOT / "build" / "Release" / "OutFile_double"
MAT_OUT = MAT_ROOT / "OutFile_double"
MAT_ASM = MAT_ROOT / "OutFile_double_asm"
MESH_MAT = MAT_ROOT / "doublePBC_mesh.mat"
def load_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 = []
with open(prefix / "Av.txt", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line:
continue
m = re.match(r"\(([-+0-9.eE]+),([-+0-9.eE]+)\)", line)
av.append(complex(float(m.group(1)), float(m.group(2))))
n = int(max(ai.max(), aj.max()) + 1)
return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))
def find_tri(domains, mesh) -> np.ndarray:
domains = np.atleast_1d(domains)
dom_tri = np.asarray(mesh.DomainOfTri).flatten()
conn = np.asarray(mesh.ConnOfTri, dtype=int)
out = []
for j, d in enumerate(dom_tri):
if d in domains:
out.append(j)
return np.array(out, dtype=int)
def face_edges(mesh, tri_idx: int) -> list[int]:
num_tet, num_face = np.asarray(mesh.ConnOfTri[tri_idx], dtype=int) - 1
e = np.asarray(mesh.EdgeOfTet[num_tet], dtype=int).flatten() - 1
face = num_face + 1
if face == 1:
return [e[0], e[1], e[3]]
if face == 2:
return [e[0], e[2], e[4]]
if face == 3:
return [e[1], e[2], e[5]]
if face == 4:
return [e[3], e[4], e[5]]
raise ValueError(face)
def find_pbc_index(src, dst, dis, mesh) -> np.ndarray:
"""MATLAB findPBCIndex (1-based edge ids in output)."""
src_tris = find_tri(src, mesh)
dst_tris = find_tri(dst, mesh)
src_edges, dst_edges = [], []
for tri in src_tris:
src_edges.extend(face_edges(mesh, tri))
for tri in dst_tris:
dst_edges.extend(face_edges(mesh, tri))
src_edges = np.unique(src_edges)
dst_edges = np.unique(dst_edges)
dis = np.asarray(dis, dtype=float).reshape(3)
dl = np.linalg.norm(dis)
err = dl * 0.00005
vertex = np.asarray(mesh.Vertex, dtype=float)
edge = np.asarray(mesh.Edge, dtype=int) - 1
pairs = []
for si in src_edges:
v1 = vertex[edge[si, 0]]
v2 = vertex[edge[si, 1]]
matched = False
for dj in dst_edges:
v3 = vertex[edge[dj, 0]]
v4 = vertex[edge[dj, 1]]
l1 = abs(np.linalg.norm(v1 - v3) - dl)
l2 = abs(np.linalg.norm(v2 - v4) - dl)
l3 = abs(np.linalg.norm(v1 - v4) - dl)
l4 = abs(np.linalg.norm(v2 - v3) - dl)
if l1 + l2 < err:
pairs.append((si + 1, dj + 1, 1))
matched = True
break
if l3 + l4 < err:
pairs.append((si + 1, dj + 1, -1))
matched = True
break
if not matched:
pairs.append((si + 1, si + 1, 1))
return np.array(pairs, dtype=int)
def tabulate_duplicate_rows(values: np.ndarray) -> np.ndarray:
"""Return row indices where tabulate count > 1 (MATLAB assembly_pbc_double bug)."""
vals = np.sort(values)
uniq, counts = np.unique(vals, return_counts=True)
rows = np.where(counts > 1)[0] + 1 # 1-based row index in tabulate table
return rows
def duplicate_values(values: np.ndarray) -> np.ndarray:
c = Counter(values.tolist())
return np.array(sorted([v for v, n in c.items() if n > 1]), dtype=int)
def merge_pbc_double_matlab_fixed(pbc1: np.ndarray, pbc2: np.ndarray, phi1: float, phi2: float):
"""Intended MATLAB merge: ovDstIndex = tbl2(find(tbl2(:,2)>1), 1)."""
return merge_pbc_double_correct(pbc1, pbc2, phi1, phi2)
def merge_pbc_double_correct(pbc1: np.ndarray, pbc2: np.ndarray, phi1: float, phi2: float):
"""C++ mergeDoublePbcPairs logic (duplicate dst values)."""
ov = duplicate_values(np.concatenate([pbc1[:, 1], pbc2[:, 1]]))
remove1 = np.zeros(len(pbc1), dtype=bool)
remove2 = np.zeros(len(pbc2), dtype=bool)
merged = []
dst1 = pbc1[:, 1]
src1 = pbc1[:, 0]
dst2 = pbc2[:, 1]
src2 = pbc2[:, 0]
for ov_dst in ov:
i1 = np.where(dst2 == ov_dst)[0]
if len(i1) == 0:
continue
i1 = i1[0]
s2 = src2[i1]
i3 = np.where(dst1 == s2)[0]
if len(i3) == 0:
continue
i3 = i3[0]
s1 = src1[i3]
sign = pbc2[i1, 2] * pbc1[i3, 2]
merged.append((s1, ov_dst, sign, phi1 * phi2 * sign))
remove1[i3] = True
remove2[i1] = True
remove1[dst1 == ov_dst] = True
remove2[dst2 == ov_dst] = True
for i in range(len(pbc1)):
if not remove1[i]:
merged.append((pbc1[i, 0], pbc1[i, 1], pbc1[i, 2], phi1 * pbc1[i, 2]))
for i in range(len(pbc2)):
if not remove2[i]:
merged.append((pbc2[i, 0], pbc2[i, 1], pbc2[i, 2], phi2 * pbc2[i, 2]))
arr = np.array([(m[0], m[1], m[2]) for m in merged], dtype=int)
phi = np.array([m[3] for m in merged], dtype=float)
return arr, phi
def build_p(dof: int, pbc: np.ndarray, phi: np.ndarray) -> sparse.csr_matrix:
rows, cols, data = [], [], []
for i in range(dof):
rows.append(i)
cols.append(i)
data.append(1.0)
for k in range(len(pbc)):
src = int(pbc[k, 0]) - 1
dst = int(pbc[k, 1]) - 1
rows.append(dst)
cols.append(src)
data.append(phi[k])
p_full = sparse.csr_matrix((data, (rows, cols)), shape=(dof, dof))
dst_cols = sorted({int(pbc[k, 1]) - 1 for k in range(len(pbc))})
keep = np.ones(dof, dtype=bool)
keep[dst_cols] = False
return p_full[:, keep]
def diff_stats(a: sparse.csr_matrix, b: sparse.csr_matrix, label: str) -> None:
if a.shape != b.shape:
print(f"{label}: shape mismatch {a.shape} vs {b.shape}")
return
d = a - b
mx = float(np.max(np.abs(d.data))) if d.nnz else 0.0
rel = mx / max(float(np.max(np.abs(b.data))), 1e-30)
print(f"{label}: max|diff|={mx:.6g}, rel_max={rel:.6g}, diff_nnz={d.nnz}")
def main() -> None:
mesh = sio.loadmat(MESH_MAT, squeeze_me=True, struct_as_record=False)["mesh"]
dof = int(mesh.NbrEdge)
pbc1 = find_pbc_index([1, 4, 7, 10, 13], [54, 55, 56, 57, 58], [2e-6, 0, 0], mesh)
pbc2 = find_pbc_index([2, 5, 8, 11, 14], [17, 18, 19, 20, 21], [0, 2e-6, 0], mesh)
print(f"PBC1 pairs: {len(pbc1)}, PBC2 pairs: {len(pbc2)}")
ov_vals = duplicate_values(np.concatenate([pbc1[:, 1], pbc2[:, 1]]))
print(f"Corner overlap dst count (correct): {len(ov_vals)}")
pbc_ok, phi_ok = merge_pbc_double_correct(pbc1, pbc2, -1.0, 1.0)
pbc_fix, phi_fix = merge_pbc_double_matlab_fixed(pbc1, pbc2, -1.0, 1.0)
print(f"Merged constraints: correct={len(pbc_ok)}")
a_asm = load_coo(MAT_ASM)
a_mat = load_coo(MAT_OUT)
a_cpp = load_coo(CPP_OUT)
p_ok = build_p(dof, pbc_ok, phi_ok)
a_proj_ok = p_ok.conj().T @ a_asm @ p_ok
diff_stats(a_mat, a_cpp, "MAT out vs C++ out")
diff_stats(a_mat, a_proj_ok, "MAT out vs asm+correct P")
diff_stats(a_cpp, a_proj_ok, "C++ out vs asm+correct P")
# normE quick
ne_cpp = np.loadtxt(CPP_OUT / "normE", comments="//")
ne_mat = np.loadtxt(MAT_OUT / "normE", comments="//")
rel = np.linalg.norm(ne_cpp - ne_mat) / np.linalg.norm(ne_mat)
print(f"normE L2 rel diff: {rel:.6g}, corr={np.corrcoef(ne_cpp, ne_mat)[0,1]:.6f}")
if __name__ == "__main__":
main()