#!/usr/bin/env python3 """Validate PBCmesh.dat and repair NormOfFace for domains 1-9 (PBC model).""" from __future__ import annotations import argparse import math import sys from pathlib import Path import numpy as np # PBC single-period case: DomainOfTri 1..9 ALL_FACE_DOMAINS = list(range(1, 10)) SBC_OUT = {3, 9} SBC_INC = {8} def read_tag(lines: list[str], i: int, expected: str) -> int: while i < len(lines) and not lines[i].strip(): i += 1 if i >= len(lines) or lines[i].strip() != expected: got = lines[i].strip() if i < len(lines) else "EOF" raise ValueError(f"Expected tag '{expected}' at line {i + 1}, got '{got}'") return i + 1 def face_vertices(V: np.ndarray, T: np.ndarray, num_tet: int, num_face: int) -> np.ndarray: """Return 3x3 array of face vertex coordinates (reference tet local face).""" x = V[T[num_tet] - 1, 0] y = V[T[num_tet] - 1, 1] z = V[T[num_tet] - 1, 2] if num_face == 1: idx = [0, 1, 2] elif num_face == 2: idx = [0, 1, 3] elif num_face == 3: idx = [0, 2, 3] elif num_face == 4: idx = [1, 2, 3] else: raise ValueError(f"bad face {num_face}") pts = np.column_stack([x[idx], y[idx], z[idx]]) return pts def geometric_face_normal(pts: np.ndarray) -> np.ndarray: n = np.cross(pts[1] - pts[0], pts[2] - pts[0]) norm = np.linalg.norm(n) if norm < 1e-30: raise ValueError("degenerate face normal") return n / norm def matlab_out_normal(domain: int, tet_vertex_xyz: np.ndarray) -> np.ndarray: """assembly_out.m normal for one SBC triangle.""" if domain == 3: return np.array([0.0, 0.0, -1.0]) x = tet_vertex_xyz[:, 0] y = tet_vertex_xyz[:, 1] n = np.array([x.sum() / 3.0, y.sum() / 3.0, 0.0]) norm = np.linalg.norm(n) if norm < 1e-30: raise ValueError(f"Degenerate assembly_out normal domain {domain}") return n / norm def matlab_inc_normal() -> np.ndarray: return np.array([0.0, 0.0, 1.0]) def load_mesh_dat(path: Path) -> dict: lines = path.read_text(encoding="utf-8", errors="replace").splitlines() i = 0 i = read_tag(lines, i, "NbrVertex") n_vertex = int(lines[i].strip()) i += 1 i = read_tag(lines, i, "Vertex") vertex = np.zeros((n_vertex, 3)) for r in range(n_vertex): vertex[r] = np.array(list(map(float, lines[i].split()))) i += 1 i = read_tag(lines, i, "NbrTet") n_tet = int(lines[i].strip()) i += 1 i = read_tag(lines, i, "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())), dtype=np.int64) i += 1 i = read_tag(lines, i, "DomainOfTet") i += n_tet i = read_tag(lines, i, "NbrEdge") n_edge = int(lines[i].strip()) i += 1 i = read_tag(lines, i, "Edge") i += n_edge i = read_tag(lines, i, "EdgeOfTet") i += n_tet i = read_tag(lines, i, "NbrTri") n_tri = int(lines[i].strip()) i += 1 i = read_tag(lines, i, "Tri") tri = np.zeros((n_tri, 3), dtype=np.int64) for r in range(n_tri): tri[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64) i += 1 i = read_tag(lines, i, "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()) i += 1 i = read_tag(lines, i, "ConnOfTri") conn_of_tri = np.zeros((n_tri, 2), dtype=np.int64) for r in range(n_tri): conn_of_tri[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64) i += 1 norm_of_face: dict[int, np.ndarray] = {} norm_line = i norm_truncated = False if i < len(lines) and lines[i].strip() == "NormOfFace": i += 1 if i >= len(lines): norm_truncated = True else: nbr_norm = int(lines[i].strip()) i += 1 for _ in range(nbr_norm): if i >= len(lines): norm_truncated = True break parts = list(map(float, lines[i].split())) if len(parts) < 4: raise ValueError(f"Bad NormOfFace at line {i + 1}: {lines[i]!r}") norm_of_face[int(parts[0])] = np.array(parts[1:4]) i += 1 return { "lines": lines, "norm_start": norm_line, "content_end": i, "norm_truncated": norm_truncated, "vertex": vertex, "tet": tet, "tri": tri, "domain_of_tri": domain_of_tri, "conn_of_tri": conn_of_tri, "norm_of_face": norm_of_face, "n_vertex": n_vertex, "n_tet": n_tet, "n_edge": n_edge, "n_tri": n_tri, } def compute_normals(mesh: dict) -> dict[int, np.ndarray]: """Compute NormOfFace for domains 1-9.""" V, T = mesh["vertex"], mesh["tet"] dom_tri, conn = mesh["domain_of_tri"], mesh["conn_of_tri"] result: dict[int, np.ndarray] = {} for domain in ALL_FACE_DOMAINS: tri_idx = np.where(dom_tri == domain)[0] if tri_idx.size == 0: raise ValueError(f"No triangles on domain {domain}") if domain == 8: result[domain] = matlab_inc_normal() continue if domain in SBC_OUT: normals = [] for tri in tri_idx: num_tet = conn[tri, 0] - 1 verts = V[T[num_tet] - 1] normals.append(matlab_out_normal(domain, verts)) arr = np.vstack(normals) n = arr.mean(axis=0) n /= np.linalg.norm(n) result[domain] = n continue # PBC (1,2,4,5) and PMC (6,7): geometric outward normal geo = [] for tri in tri_idx: num_tet = conn[tri, 0] - 1 num_face = conn[tri, 1] pts = face_vertices(V, T, num_tet, num_face) geo.append(geometric_face_normal(pts)) arr = np.vstack(geo) n = arr.mean(axis=0) n /= np.linalg.norm(n) result[domain] = n return result def validate_mesh(mesh: dict, norms: dict[int, np.ndarray]) -> list[str]: issues: list[str] = [] dom_set = set(int(d) for d in np.unique(mesh["domain_of_tri"])) if mesh.get("norm_truncated"): issues.append("CRITICAL: NormOfFace truncated") if dom_set != set(ALL_FACE_DOMAINS): issues.append(f"WARNING: DomainOfTri ids {sorted(dom_set)} (expected 1..9)") extra = set(mesh["norm_of_face"]) - set(ALL_FACE_DOMAINS) if extra: issues.append(f"WARNING: obsolete NormOfFace domain ids {sorted(extra)} (e.g. 14 from SBC template)") missing = set(ALL_FACE_DOMAINS) - set(mesh["norm_of_face"]) if missing: issues.append(f"WARNING: missing NormOfFace entries for domains {sorted(missing)}") if 9 in mesh["norm_of_face"] and np.allclose(mesh["norm_of_face"][9], [1, 0, 0], atol=1e-12): issues.append("CRITICAL: domain 9 was [1,0,0] (SBC template), not assembly_out normal") if mesh["tet"].min() < 1 or mesh["tet"].max() > mesh["n_vertex"]: issues.append("CRITICAL: Tet indices out of range") return issues def build_norm_section(norms: dict[int, np.ndarray]) -> list[str]: lines = ["NormOfFace", str(len(ALL_FACE_DOMAINS))] for d in ALL_FACE_DOMAINS: n = norms[d] lines.append(f"{d} {n[0]:.16g} {n[1]:.16g} {n[2]:.16g}") return lines def fix_mesh_file(src: Path, dst: Path | None = None) -> int: dst = dst or src mesh = load_mesh_dat(src) norms = compute_normals(mesh) issues = validate_mesh(mesh, norms) print(f"=== Validate: {src} ===") print(f" NbrVertex={mesh['n_vertex']}, NbrTet={mesh['n_tet']}, " f"NbrEdge={mesh['n_edge']}, NbrTri={mesh['n_tri']}") print(f" DomainOfTri: {sorted(set(mesh['domain_of_tri'].tolist()))}") for msg in issues: print(f" {msg}") print("\n=== NormOfFace (domains 1-9) ===") for d in ALL_FACE_DOMAINS: tag = "" if d in SBC_OUT: tag = " [SBC out, assembly_out.m]" elif d in SBC_INC: tag = " [SBC inc, assembly_inc.m]" elif d in {1, 2, 4, 5}: tag = " [PBC, geometric]" else: tag = " [PMC, geometric]" n = norms[d] print(f" {d}: ({n[0]:.6g}, {n[1]:.6g}, {n[2]:.6g}){tag}") out_lines = mesh["lines"][: mesh["norm_start"]] out_lines.extend(build_norm_section(norms)) dst.write_text("\n".join(out_lines) + "\n", encoding="ascii") print(f"\nWrote fixed mesh: {dst}") return 0 if not any("CRITICAL" in x for x in issues) else 1 def main() -> int: parser = argparse.ArgumentParser(description="Validate and fix PBCmesh.dat NormOfFace (domains 1-9)") parser.add_argument( "mesh", nargs="?", type=Path, default=Path(__file__).resolve().parents[2] / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+单周期边界" / "PBCmesh.dat", ) parser.add_argument("-o", "--output", type=Path, default=None) parser.add_argument("--copy-to", type=Path, action="append", default=[]) args = parser.parse_args() if not args.mesh.is_file(): print(f"Mesh not found: {args.mesh}", file=sys.stderr) return 2 rc = fix_mesh_file(args.mesh, args.output or args.mesh) out = args.output or args.mesh for cp in args.copy_to: cp.parent.mkdir(parents=True, exist_ok=True) cp.write_text(out.read_text(encoding="ascii"), encoding="ascii") print(f"Copied to {cp}") return rc if __name__ == "__main__": raise SystemExit(main())