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

250 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Compare PBC-projected A matrix: MATLAB vs C++, and verify PBC projection pipeline."""
import re
import math
from pathlib import Path
import numpy as np
from scipy import sparse
ROOT = Path(__file__).resolve().parents[1]
MATLAB_DIR = ROOT.parent / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+单周期边界"
CPP_OUT = ROOT / "build" / "Release" / "OutFile"
MAT_OUT = MATLAB_DIR / "OutFile"
MAT_ASM = MATLAB_DIR / "OutFile_asm"
MESH_FILE = MATLAB_DIR / "PBCmesh.dat"
PBC_ANGLE = math.pi / 3
PBC_PHI = complex(0.5, 0.8660254037844386)
def load_coo(prefix: Path) -> sparse.csr_matrix:
def read_col(p):
with open(p, encoding="utf-8", errors="ignore") as f:
return np.array([int(x.strip()) for x in f if x.strip()], dtype=np.int64)
def read_val(p):
vals = []
with open(p, 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)
if m:
vals.append(complex(float(m.group(1)), float(m.group(2))))
else:
vals.append(complex(float(line), 0.0))
return np.array(vals, dtype=np.complex128)
ai = read_col(prefix / "Ai.txt")
aj = read_col(prefix / "Aj.txt")
av = read_val(prefix / "Av.txt")
n = int(max(ai.max(), aj.max()) + 1)
return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))
def read_mesh_dat(path: Path):
"""Minimal PBCmesh.dat reader (0-based indices)."""
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
i = 0
def read_block(name):
nonlocal i
while i < len(lines) and lines[i].strip() != name:
i += 1
if i >= len(lines):
raise KeyError(name)
i += 1
return i
def read_int():
nonlocal i
v = int(lines[i].strip())
i += 1
return v
def read_floats(n):
nonlocal i
vals = list(map(float, lines[i].split()))
i += 1
return np.array(vals[:n])
read_block("NbrVertex")
n_vertex = read_int()
read_block("Vertex")
vertex = np.zeros((n_vertex, 3))
for r in range(n_vertex):
vertex[r] = read_floats(3)
read_block("NbrEdge")
n_edge = read_int()
read_block("Edge")
edge = np.zeros((n_edge, 2), dtype=np.int64)
for r in range(n_edge):
a, b = map(int, lines[i].split())
edge[r] = (a - 1, b - 1)
i += 1
read_block("NbrTet")
n_tet = read_int()
read_block("Tet")
tet = np.zeros((n_tet, 4), dtype=np.int64)
for r in range(n_tet):
tet[r] = np.array(list(map(int, lines[i].split()))) - 1
i += 1
read_block("EdgeOfTet")
edge_of_tet = np.zeros((n_tet, 6), dtype=np.int64)
for r in range(n_tet):
edge_of_tet[r] = np.array(list(map(int, lines[i].split()))) - 1
i += 1
read_block("NbrTri")
n_tri = read_int()
read_block("ConnOfTri")
conn_of_tri = np.zeros((n_tri, 2), dtype=np.int64)
for r in range(n_tri):
a, b = map(int, lines[i].split())
conn_of_tri[r] = (a - 1, b - 1)
i += 1
read_block("DomainOfTri")
domain_of_tri = np.zeros(n_tri, dtype=np.int64)
for r in range(n_tri):
domain_of_tri[r] = int(lines[i].strip()) # keep 1-based like MATLAB mesh
i += 1
return {
"vertex": vertex,
"edge": edge,
"tet": tet,
"edge_of_tet": edge_of_tet,
"conn_of_tri": conn_of_tri,
"domain_of_tri": domain_of_tri,
"n_edge": n_edge,
}
def find_tri(domains, mesh):
domains = np.atleast_1d(domains)
out = []
for j in range(mesh["conn_of_tri"].shape[0]):
num_tet, _ = mesh["conn_of_tri"][j]
dom = mesh["domain_of_tri"][j]
# DomainOfTri in dat is 1-based after +1 in C++ loader; MATLAB c6 may differ.
# PBCmesh.dat stores 0-based domain ids before C++ +1.
if dom in domains or (dom + 1) in domains:
out.append(j)
return np.array(out, dtype=np.int64)
def face_edges(mesh, tri_idx):
num_tet, num_face = mesh["conn_of_tri"][tri_idx]
e = mesh["edge_of_tet"][num_tet]
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, theta, mesh):
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)
tra = np.array(
[[math.cos(theta), -math.sin(theta), 0.0],
[math.sin(theta), math.cos(theta), 0.0],
[0.0, 0.0, 1.0]]
)
tol = 0.01 * 0.00005
pairs = []
for si in src_edges:
v1 = mesh["vertex"][mesh["edge"][si, 0]]
v2 = mesh["vertex"][mesh["edge"][si, 1]]
matched = False
for dj, di in enumerate(dst_edges):
v3 = tra @ mesh["vertex"][mesh["edge"][di, 0]]
v4 = tra @ mesh["vertex"][mesh["edge"][di, 1]]
l1 = np.linalg.norm(v1 - v3)
l2 = np.linalg.norm(v2 - v4)
l3 = np.linalg.norm(v1 - v4)
l4 = np.linalg.norm(v2 - v3)
if l1 + l2 < tol:
pairs.append((si, di, 1))
matched = True
break
if l3 + l4 < tol:
pairs.append((si, di, -1))
matched = True
break
if not matched:
pairs.append((si, -1, 0))
pairs = [(s, d, sg) for s, d, sg in pairs if s != d and d >= 0]
return pairs
def build_P(dof, pairs):
P = sparse.lil_matrix((dof, dof), dtype=np.complex128)
for i in range(dof):
P[i, i] = 1.0
dst_cols = []
for src, dst, sign in pairs:
P[dst, src] = sign * PBC_PHI
dst_cols.append(dst)
keep = [c for c in range(dof) if c not in dst_cols]
return P[:, keep].tocsr()
def diff_stats(A, B, label):
D = A - B
print(f"\n=== {label} ===")
print(f" max |diff| = {np.max(np.abs(D.data)) if D.nnz else 0:.6g}")
print(f" max |real diff| = {np.max(np.abs(D.real.data)) if D.nnz else 0:.6g}")
print(f" max |imag diff| = {np.max(np.abs(D.imag.data)) if D.nnz else 0:.6g}")
if D.nnz and A.shape[0] == B.shape[0]:
di = np.abs(A.diagonal().imag - B.diagonal().imag)
print(f" diag imag max diff = {di.max():.6g}, >10 count = {(di > 10).sum()}")
def main():
print("Loading matrices...")
A_cpp = load_coo(CPP_OUT)
A_mat = load_coo(MAT_OUT)
A_asm = load_coo(MAT_ASM)
diff_stats(A_cpp, A_mat, "C++ OutFile vs MATLAB OutFile (after PBC)")
print("\nLoading mesh and building PBC pairs...")
mesh = read_mesh_dat(MESH_FILE)
# domains in case_config: src=[1,4], dst=[2,5] (1-based in MATLAB)
pairs = find_pbc_index(np.array([1, 4]), np.array([2, 5]), PBC_ANGLE, mesh)
print(f" PBC pairs: {len(pairs)}")
signs = [p[2] for p in pairs]
print(f" sign +1: {signs.count(1)}, sign -1: {signs.count(-1)}")
P = build_P(mesh["n_edge"], pairs)
A_proj = P.conj().T @ A_asm @ P
diff_stats(A_proj, A_mat, "MATLAB asm projected with Python P vs MATLAB OutFile")
diff_stats(A_proj, A_cpp, "MATLAB asm projected with Python P vs C++ OutFile")
# Test sign-flip hypothesis on all -1 pairs
pairs_flip = [(s, d, -sg if sg < 0 else sg) for s, d, sg in pairs]
Pf = build_P(mesh["n_edge"], pairs_flip)
Af = Pf.conj().T @ A_asm @ Pf
diff_stats(Af, A_cpp, "MATLAB asm with flipped -1 signs vs C++ OutFile")
if __name__ == "__main__":
main()