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

177 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Compare C++ wave-only A with Python replication of MATLAB PhysicMatrixAssembly."""
from __future__ import annotations
import re
from pathlib import Path
import h5py
import numpy as np
from scipy import sparse
PI = np.pi
def bf_edge(num: int, u: float, v: float, w: float) -> np.ndarray:
out = np.zeros(3)
if num == 1:
out[:] = [-v, u, 0.0]
elif num == 2:
out[:] = [-w, 0.0, u]
elif num == 3:
out[:] = [-1 + v + w, -u, -u]
elif num == 4:
out[:] = [0.0, -w, v]
elif num == 5:
out[:] = [-v, -1 + u + w, -v]
elif num == 6:
out[:] = [-w, -w, -1 + u + v]
else:
raise ValueError(num)
return out
def bf_curl_edge(num: int, u: float, v: float, w: float) -> np.ndarray:
out = np.zeros(3)
if num == 1:
out[2] = 2.0
elif num == 2:
out[1] = -2.0
elif num == 3:
out[1] = 2.0
out[2] = -2.0
elif num == 4:
out[0] = 2.0
elif num == 5:
out[0] = -2.0
out[2] = 2.0
elif num == 6:
out[0] = 2.0
out[1] = -2.0
else:
raise ValueError(num)
return out
def gauss_tet_order2() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
xt = np.array([0.25, 0.166666666667, 0.166666666667, 0.166666666667, 0.5])
yt = np.array([0.25, 0.166666666667, 0.166666666667, 0.5, 0.166666666667])
zt = np.array([0.25, 0.166666666667, 0.5, 0.166666666667, 0.166666666667])
pt = np.array([-0.133333333333, 0.075, 0.075, 0.075, 0.075])
return xt, yt, zt, pt
def build_edges(elements: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
el2no = elements.T
n1 = el2no[[0, 0, 0, 1, 1, 2], :]
n2 = el2no[[1, 2, 3, 2, 3, 3], :]
el_ed = np.column_stack([n1.reshape(-1, order="F"), n2.reshape(-1, order="F")])
edge, ic = np.unique(el_ed, axis=0, return_inverse=True)
eoe = ic.reshape(6, -1, order="F").T
return edge, eoe
def assemble_wave_matlab(
nodes: np.ndarray, elements: np.ndarray, domains: np.ndarray
) -> sparse.csr_matrix:
lam0 = 1.55e-6
k0 = 2 * PI / lam0
epsilonr = np.array([4.0, 11.9, 11.9, 11.9], dtype=complex)
sigma = np.array([0.0, 0.0, 5000.0, 0.0])
mur = np.ones(4)
temp = 1 / k0 * 120 * PI
epsilon = epsilonr - 1j * sigma * temp
_, eoe = build_edges(elements)
xt, yt, zt, pt = gauss_tet_order2()
trips: list[tuple[int, int, complex]] = []
for n in range(elements.shape[0]):
idx = elements[n] - 1
x = nodes[idx, 0]
y = nodes[idx, 1]
z = nodes[idx, 2]
l = np.ones(6)
pairs = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
for i, (a, b) in enumerate(pairs):
l[i] = np.linalg.norm(nodes[idx[a]] - nodes[idx[b]])
jac = np.column_stack([x[:3] - x[3], y[:3] - y[3], z[:3] - z[3]])
inv_jac = np.linalg.inv(jac)
t_jac = jac.T / np.linalg.det(jac)
det_jac = abs(np.linalg.det(jac))
domain = int(domains[n]) - 1
mu = 1.0 / mur[domain]
eps = epsilon[domain]
ne = np.zeros((3, 6, len(xt)), dtype=complex)
curl_ne = np.zeros((3, 6, len(xt)), dtype=complex)
for i in range(6):
for k in range(len(xt)):
temp_bf = bf_edge(i + 1, xt[k], yt[k], zt[k])
ne[:, i, k] = inv_jac @ temp_bf * l[i]
temp_c = bf_curl_edge(i + 1, xt[k], yt[k], zt[k])
curl_ne[:, i, k] = t_jac @ temp_c * l[i]
ae = np.zeros((6, 6), dtype=complex)
for i in range(6):
for j in range(6):
for k in range(len(pt)):
ae[i, j] += pt[k] * det_jac * (
mu * np.vdot(curl_ne[:, i, k], curl_ne[:, j, k])
- k0 * k0 * eps * np.vdot(ne[:, i, k], ne[:, j, k])
)
for i in range(6):
for j in range(6):
ii = int(eoe[n, i])
jj = int(eoe[n, j])
trips.append((ii, jj, ae[i, j]))
ai = np.array([t[0] for t in trips], dtype=np.int64)
aj = np.array([t[1] for t in trips], dtype=np.int64)
av = np.array([t[2] for t in trips])
n = int(max(ai.max(), aj.max()) + 1)
return sparse.coo_matrix((av, (ai, aj)), shape=(n, n)).tocsr()
def load_cpp_wave(cpp_asm: Path) -> sparse.csr_matrix:
ai = np.loadtxt(cpp_asm / "Ai.txt", dtype=np.int64)
aj = np.loadtxt(cpp_asm / "Aj.txt", dtype=np.int64)
av = []
for line in (cpp_asm / "Av.txt").read_text().splitlines():
line = line.strip()
if not line or line.startswith("//"):
continue
m = re.match(r"\(([^,]+),([^)]+)\)", line)
av.append(complex(float(m.group(1)), float(m.group(2))))
av = np.array(av)
n = int(max(ai.max(), aj.max()) + 1)
return sparse.coo_matrix((av, (ai, aj)), shape=(n, n)).tocsr()
def main() -> int:
root = Path(__file__).resolve().parents[2]
mat_path = next(root.rglob("MeshData2x.mat"))
cpp_asm = root / "3D opticsfem-master/port/Release/OutFile_asm"
with h5py.File(mat_path, "r") as f:
nodes = np.asarray(f["Mesh/Nodes"]).T
elements = np.asarray(f["Mesh/Elements"]).T.astype(int)
domains = np.asarray(f["Mesh/Domains"]).reshape(-1).astype(int)
print("Assembling MATLAB-style wave matrix ...")
a_py = assemble_wave_matlab(nodes, elements, domains)
a_cpp = load_cpp_wave(cpp_asm)
print(f"Python nnz={a_py.nnz} C++ nnz={a_cpp.nnz}")
diff = a_py - a_cpp
print(f"max |A_py - A_cpp| = {max(abs(diff.data).max() if diff.nnz else 0.0, 0.0):.6e}")
rel = diff.data / (a_cpp.data + 1e-30)
if diff.nnz:
print(f"median rel diff on overlapping nnz = {np.median(np.abs(rel)):.6e}")
return 0
if __name__ == "__main__":
raise SystemExit(main())