437 lines
13 KiB
Python
437 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""Plot normE on boundary face domains (e.g. PBC dst faces 2 and 5)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
from pathlib import Path
|
||
|
||
import matplotlib.pyplot as plt
|
||
import numpy as np
|
||
from matplotlib.collections import PolyCollection
|
||
|
||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial"]
|
||
plt.rcParams["axes.unicode_minus"] = False
|
||
|
||
# Reuse mesh loader
|
||
import importlib.util
|
||
|
||
_ROOT = Path(__file__).resolve().parents[1]
|
||
_spec = importlib.util.spec_from_file_location(
|
||
"validate_pbcmesh", _ROOT / "tools" / "validate_and_fix_pbcmesh.py"
|
||
)
|
||
_v = importlib.util.module_from_spec(_spec)
|
||
_spec.loader.exec_module(_v)
|
||
load_mesh_dat = _v.load_mesh_dat
|
||
face_vertices = _v.face_vertices
|
||
|
||
|
||
def face_vertex_ids(tet: np.ndarray, num_tet: int, num_face: int) -> np.ndarray:
|
||
"""Return 3 mesh vertex ids (0-based) for a boundary face."""
|
||
tv = tet[num_tet - 1] - 1
|
||
if num_face == 1:
|
||
local = [0, 1, 2]
|
||
elif num_face == 2:
|
||
local = [0, 1, 3]
|
||
elif num_face == 3:
|
||
local = [0, 2, 3]
|
||
elif num_face == 4:
|
||
local = [1, 2, 3]
|
||
else:
|
||
raise ValueError(f"bad face id {num_face}")
|
||
return tv[np.array(local)]
|
||
|
||
|
||
def load_normE(path: Path) -> np.ndarray:
|
||
vals: list[float] = []
|
||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
if line.startswith("//"):
|
||
line = line[2:].strip()
|
||
if not line:
|
||
continue
|
||
vals.append(float(line.split()[0]))
|
||
return np.array(vals)
|
||
|
||
|
||
def collect_face_tris(
|
||
mesh: dict, domain: int, normE: np.ndarray
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Return (points Nx2 in xz, triangles Mx3, face_values M)."""
|
||
V = mesh["vertex"]
|
||
T = mesh["tet"]
|
||
dom = mesh["domain_of_tri"]
|
||
conn = mesh["conn_of_tri"]
|
||
|
||
pts_list: list[np.ndarray] = []
|
||
tri_list: list[tuple[int, int, int]] = []
|
||
vals_list: list[float] = []
|
||
pt_index: dict[tuple[float, float], int] = {}
|
||
|
||
def get_pt(x: float, z: float) -> int:
|
||
key = (round(x, 10), round(z, 10))
|
||
if key not in pt_index:
|
||
pt_index[key] = len(pts_list)
|
||
pts_list.append([x, z])
|
||
return pt_index[key]
|
||
|
||
for tri_idx in np.where(dom == domain)[0]:
|
||
num_tet, num_face = int(conn[tri_idx, 0]), int(conn[tri_idx, 1])
|
||
vids = face_vertex_ids(T, num_tet, num_face)
|
||
xyz = V[vids]
|
||
i0 = get_pt(xyz[0, 0], xyz[0, 2])
|
||
i1 = get_pt(xyz[1, 0], xyz[1, 2])
|
||
i2 = get_pt(xyz[2, 0], xyz[2, 2])
|
||
tri_list.append((i0, i1, i2))
|
||
vals_list.append(float(np.mean(normE[vids])))
|
||
|
||
if not pts_list:
|
||
return np.zeros((0, 2)), np.zeros((0, 3), dtype=int), np.zeros(0)
|
||
|
||
return np.array(pts_list), np.array(tri_list, dtype=int), np.array(vals_list)
|
||
|
||
|
||
def plot_domain_ax(ax, pts: np.ndarray, tris: np.ndarray, fvals: np.ndarray, title: str):
|
||
if len(pts) == 0:
|
||
ax.set_title(title + " (empty)")
|
||
return
|
||
# Per-triangle constant color
|
||
polys = pts[tris]
|
||
coll = PolyCollection(
|
||
polys,
|
||
array=fvals,
|
||
cmap="jet",
|
||
edgecolors="k",
|
||
linewidths=0.15,
|
||
alpha=0.95,
|
||
)
|
||
ax.add_collection(coll)
|
||
ax.set_aspect("equal")
|
||
ax.set_xlabel("x (m)")
|
||
ax.set_ylabel("z (m)")
|
||
ax.set_title(title)
|
||
ax.autoscale()
|
||
mappable = coll
|
||
return mappable
|
||
|
||
|
||
def _tri_centroids(pts: np.ndarray, tris: np.ndarray) -> np.ndarray:
|
||
return np.array([pts[t].mean(axis=0) for t in tris])
|
||
|
||
|
||
def _centroids_inside_patch(
|
||
centroids: np.ndarray, patch_pts: np.ndarray, patch_tris: np.ndarray
|
||
) -> np.ndarray:
|
||
"""True where centroid lies inside the xz footprint of a patch (bbox test)."""
|
||
xmin, xmax = patch_pts[:, 0].min(), patch_pts[:, 0].max()
|
||
zmin, zmax = patch_pts[:, 1].min(), patch_pts[:, 1].max()
|
||
return (
|
||
(centroids[:, 0] >= xmin)
|
||
& (centroids[:, 0] <= xmax)
|
||
& (centroids[:, 1] >= zmin)
|
||
& (centroids[:, 1] <= zmax)
|
||
)
|
||
|
||
|
||
def _append_tris_unified(
|
||
pts_acc: list[np.ndarray],
|
||
tris_acc: list[tuple[int, int, int]],
|
||
vals_acc: list[float],
|
||
pt_index: dict[tuple[float, float], int],
|
||
patch_pts: np.ndarray,
|
||
patch_tris: np.ndarray,
|
||
patch_vals: np.ndarray,
|
||
) -> None:
|
||
def get_pt(x: float, z: float) -> int:
|
||
key = (round(x, 10), round(z, 10))
|
||
if key not in pt_index:
|
||
pt_index[key] = len(pts_acc)
|
||
pts_acc.append([x, z])
|
||
return pt_index[key]
|
||
|
||
for tri, val in zip(patch_tris, patch_vals):
|
||
p0, p1, p2 = patch_pts[tri]
|
||
i0 = get_pt(p0[0], p0[1])
|
||
i1 = get_pt(p1[0], p1[1])
|
||
i2 = get_pt(p2[0], p2[1])
|
||
tris_acc.append((i0, i1, i2))
|
||
vals_acc.append(float(val))
|
||
|
||
|
||
def collect_domains_combined(
|
||
mesh: dict,
|
||
domains: list[int],
|
||
normE: np.ndarray,
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Merge face domains on one x-z plane with shared vertex indexing.
|
||
|
||
Adjacent patches (e.g. domain 2 + 5) share boundary vertices; duplicate
|
||
points at the same (x, z) are welded so triangles tile without z-fighting.
|
||
"""
|
||
if not domains:
|
||
return np.zeros((0, 2)), np.zeros((0, 3), dtype=int), np.zeros(0)
|
||
|
||
raw = [(d, *collect_face_tris(mesh, d, normE)) for d in domains]
|
||
raw = [(d, p, t, f) for d, p, t, f in raw if len(t) > 0]
|
||
if not raw:
|
||
return np.zeros((0, 2)), np.zeros((0, 3), dtype=int), np.zeros(0)
|
||
|
||
pts_acc: list[list[float]] = []
|
||
tris_acc: list[tuple[int, int, int]] = []
|
||
vals_acc: list[float] = []
|
||
pt_index: dict[tuple[float, float], int] = {}
|
||
|
||
for _, pts, tris, fvals in raw:
|
||
_append_tris_unified(
|
||
pts_acc, tris_acc, vals_acc, pt_index, pts, tris, fvals,
|
||
)
|
||
|
||
if not pts_acc:
|
||
return np.zeros((0, 2)), np.zeros((0, 3), dtype=int), np.zeros(0)
|
||
return (
|
||
np.array(pts_acc),
|
||
np.array(tris_acc, dtype=int),
|
||
np.array(vals_acc),
|
||
)
|
||
|
||
|
||
def plot_combined_faces(
|
||
mesh: dict,
|
||
domains: list[int],
|
||
normE_cpp: np.ndarray,
|
||
normE_mat: np.ndarray | None,
|
||
output: Path,
|
||
title: str | None = None,
|
||
cpp_outdir: Path | None = None,
|
||
mat_outdir: Path | None = None,
|
||
layout: str = "merge",
|
||
) -> None:
|
||
"""Plot PBC face domains on one axes.
|
||
|
||
layout:
|
||
merge – weld shared vertices and tile all domains (default)
|
||
side – one panel per domain, horizontal row
|
||
"""
|
||
has_mat = normE_mat is not None
|
||
plot_domains = list(domains)
|
||
use_side = layout == "side" and len(plot_domains) > 1
|
||
ncols = (len(plot_domains) if use_side else 1) * (2 if has_mat else 1)
|
||
fig_w = 5.5 * ncols if use_side else (6.8 * ncols if has_mat else 6.0)
|
||
fig, axes = plt.subplots(
|
||
1, ncols if ncols else 1,
|
||
figsize=(fig_w, 5.5),
|
||
squeeze=False,
|
||
facecolor="white",
|
||
)
|
||
|
||
cpp_is_mat = (
|
||
has_mat
|
||
and cpp_outdir is not None
|
||
and mat_outdir is not None
|
||
and cpp_outdir.resolve() == mat_outdir.resolve()
|
||
)
|
||
mappables = []
|
||
labels = ["C++ OpticsFEM", "MATLAB 参考"] if has_mat else ["OpticsFEM |E|"]
|
||
if cpp_is_mat:
|
||
labels[0] = "MATLAB 参考 (C++ OutFile 待 pbc3d_sbc.json 重跑)"
|
||
datasets = [normE_cpp]
|
||
if has_mat:
|
||
datasets.append(normE_mat)
|
||
|
||
domain_label = " + ".join(f"domain {d}" for d in plot_domains)
|
||
|
||
if use_side:
|
||
mappables = []
|
||
for i, dom in enumerate(plot_domains):
|
||
col = i * (2 if has_mat else 1)
|
||
for j, (normE, suffix) in enumerate(
|
||
[(normE_cpp, "C++"), (normE_mat, "MATLAB")] if has_mat else [(normE_cpp, "")]
|
||
):
|
||
if normE is None:
|
||
continue
|
||
pts, tris, fvals = collect_face_tris(mesh, dom, normE)
|
||
ax = axes[0, col + j]
|
||
label = f"{suffix} domain {dom}" if suffix else f"domain {dom}"
|
||
m = plot_domain_ax(ax, pts, tris, fvals, f"{label}\n(n_tri={len(fvals)})")
|
||
if m is not None:
|
||
mappables.append(m)
|
||
else:
|
||
for col, (normE, label) in enumerate(zip(datasets, labels)):
|
||
pts, tris, fvals = collect_domains_combined(mesh, plot_domains, normE)
|
||
n_tri = len(fvals)
|
||
m = plot_domain_ax(
|
||
axes[0, col],
|
||
pts,
|
||
tris,
|
||
fvals,
|
||
f"{label}\n(domain {' + '.join(map(str, plot_domains))}, n_tri={n_tri})",
|
||
)
|
||
if m is not None:
|
||
mappables.append(m)
|
||
|
||
if mappables:
|
||
vmin = min(m.get_array().min() for m in mappables if m.get_array() is not None)
|
||
vmax = max(m.get_array().max() for m in mappables if m.get_array() is not None)
|
||
for m in mappables:
|
||
m.set_clim(vmin, vmax)
|
||
if has_mat:
|
||
fig.colorbar(
|
||
mappables[0],
|
||
ax=axes.ravel().tolist(),
|
||
shrink=0.82,
|
||
pad=0.02,
|
||
label="|E| (V/m)",
|
||
)
|
||
else:
|
||
fig.subplots_adjust(right=0.88)
|
||
cbar = fig.colorbar(
|
||
mappables[0],
|
||
ax=axes[0, 0],
|
||
fraction=0.046,
|
||
pad=0.04,
|
||
label="|E| (V/m)",
|
||
)
|
||
cbar.ax.tick_params(labelsize=9)
|
||
|
||
fig.subplots_adjust(top=0.92, bottom=0.12, left=0.10, right=0.88 if not has_mat else 0.95)
|
||
output.parent.mkdir(parents=True, exist_ok=True)
|
||
fig.savefig(output, dpi=160, bbox_inches="tight", facecolor="white")
|
||
plt.close(fig)
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="Plot normE on mesh face domains")
|
||
parser.add_argument(
|
||
"--mesh",
|
||
type=Path,
|
||
default=_ROOT / "build" / "Release" / "PBCmesh.dat",
|
||
)
|
||
parser.add_argument(
|
||
"--outdir",
|
||
type=Path,
|
||
default=_ROOT / "build" / "Release" / "OutFile",
|
||
help="OpticsFEM OutFile with normE",
|
||
)
|
||
parser.add_argument(
|
||
"--mat-outdir",
|
||
type=Path,
|
||
default=_ROOT.parent
|
||
/ "三维matlab代码"
|
||
/ "matlab 3D一阶基+散射边界条件+单周期边界"
|
||
/ "OutFile",
|
||
help="MATLAB OutFile for comparison (set empty to skip)",
|
||
)
|
||
parser.add_argument(
|
||
"--domains",
|
||
type=int,
|
||
nargs="+",
|
||
default=[2, 5],
|
||
help="DomainOfTri ids to plot",
|
||
)
|
||
parser.add_argument(
|
||
"--output",
|
||
type=Path,
|
||
default=_ROOT / "build" / "Release" / "OutFile" / "faces_2_5.png",
|
||
)
|
||
parser.add_argument(
|
||
"--combined",
|
||
action="store_true",
|
||
help="Plot all --domains on the same axes (connected geometry)",
|
||
)
|
||
parser.add_argument(
|
||
"--no-compare",
|
||
action="store_true",
|
||
help="Skip MATLAB comparison panel (simulation result only)",
|
||
)
|
||
parser.add_argument(
|
||
"--layout",
|
||
choices=["merge", "side"],
|
||
default="merge",
|
||
help="merge: weld vertices and tile domains; side: horizontal panels",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
mesh = load_mesh_dat(args.mesh)
|
||
normE_cpp = load_normE(args.outdir / "normE")
|
||
if normE_cpp.size != mesh["n_vertex"]:
|
||
raise SystemExit(
|
||
f"normE size {normE_cpp.size} != NbrVertex {mesh['n_vertex']}"
|
||
)
|
||
|
||
has_mat = (
|
||
not args.no_compare
|
||
and args.mat_outdir is not None
|
||
and (args.mat_outdir / "normE").exists()
|
||
)
|
||
normE_mat = load_normE(args.mat_outdir / "normE") if has_mat else None
|
||
|
||
if args.combined:
|
||
plot_combined_faces(
|
||
mesh,
|
||
args.domains,
|
||
normE_cpp,
|
||
normE_mat,
|
||
args.output,
|
||
cpp_outdir=args.outdir,
|
||
mat_outdir=args.mat_outdir if has_mat else None,
|
||
layout=args.layout,
|
||
)
|
||
print(f"Saved combined {args.output}")
|
||
return
|
||
|
||
ncols = 2 if has_mat else 1
|
||
nrows = len(args.domains)
|
||
fig, axes = plt.subplots(
|
||
nrows, ncols, figsize=(6.5 * ncols, 5 * nrows), squeeze=False, facecolor="white"
|
||
)
|
||
|
||
mappables = []
|
||
for row, dom in enumerate(args.domains):
|
||
pts, tris, fvals = collect_face_tris(mesh, dom, normE_cpp)
|
||
m = plot_domain_ax(
|
||
axes[row, 0],
|
||
pts,
|
||
tris,
|
||
fvals,
|
||
f"C++ domain {dom} (n_tri={len(fvals)})",
|
||
)
|
||
if m is not None:
|
||
mappables.append(m)
|
||
|
||
if has_mat:
|
||
pts_m, tris_m, fvals_m = collect_face_tris(mesh, dom, normE_mat)
|
||
m2 = plot_domain_ax(
|
||
axes[row, 1],
|
||
pts_m,
|
||
tris_m,
|
||
fvals_m,
|
||
f"MATLAB domain {dom}",
|
||
)
|
||
if m2 is not None:
|
||
mappables.append(m2)
|
||
|
||
if mappables:
|
||
vmin = min(m.get_array().min() for m in mappables if m.get_array() is not None)
|
||
vmax = max(m.get_array().max() for m in mappables if m.get_array() is not None)
|
||
for m in mappables:
|
||
m.set_clim(vmin, vmax)
|
||
fig.colorbar(mappables[0], ax=axes.ravel().tolist(), shrink=0.6, label="|E| (V/m)")
|
||
|
||
fig.suptitle(
|
||
"normE on PBC faces (x–z projection, y≈0)\n"
|
||
+ ", ".join(f"domain {d}" for d in args.domains),
|
||
fontsize=13,
|
||
fontweight="bold",
|
||
)
|
||
fig.tight_layout()
|
||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||
fig.savefig(args.output, dpi=160, bbox_inches="tight")
|
||
print(f"Saved {args.output}")
|
||
plt.close(fig)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|