620 lines
24 KiB
Python
620 lines
24 KiB
Python
import copy
|
||
from typing import Any, Dict, List, Optional, Union
|
||
|
||
import numpy as np
|
||
from skfem import Basis, ElementTriP1, Mesh, asm, solve
|
||
from skfem.assembly import BilinearForm, FacetBasis, LinearForm
|
||
from skfem.helpers import dot, grad
|
||
|
||
from .domain import create_domain
|
||
from .fem_util import get_aggregation_per_element, get_element_midpoints
|
||
|
||
|
||
class HelmholtzProblem:
|
||
"""2D Helmholtz scattering FEM solver with Sommerfeld BC."""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
fem_config: Dict[Union[str, int], Any],
|
||
random_state: np.random.RandomState = np.random.RandomState(),
|
||
):
|
||
helmholtz_config = fem_config.get("helmholtz", {})
|
||
|
||
# ── 1. 波数 k ──
|
||
wave_number_mode = helmholtz_config.get("wave_number_mode", "fixed")
|
||
if wave_number_mode == "random_uniform":
|
||
k_min = helmholtz_config.get("wave_number_min", 2.0)
|
||
k_max = helmholtz_config.get("wave_number_max", 8.0)
|
||
self._k = float(random_state.uniform(k_min, k_max))
|
||
else:
|
||
self._k = float(helmholtz_config.get("wave_number", 10.0))
|
||
|
||
# ── 2. 介质散射体参数 ──
|
||
sc = helmholtz_config.get("scatterer", {})
|
||
scatterer_mode = sc.get("mode", "fixed")
|
||
|
||
if scatterer_mode == "random_uniform":
|
||
self._cx = float(
|
||
random_state.uniform(sc.get("cx_min", 0.3), sc.get("cx_max", 0.7))
|
||
)
|
||
self._cy = float(
|
||
random_state.uniform(sc.get("cy_min", 0.3), sc.get("cy_max", 0.7))
|
||
)
|
||
self._radius = float(
|
||
random_state.uniform(
|
||
sc.get("radius_min", 0.1), sc.get("radius_max", 0.25)
|
||
)
|
||
)
|
||
self._eps_r = float(
|
||
random_state.uniform(
|
||
sc.get("eps_r_min", 2.0), sc.get("eps_r_max", 7.0)
|
||
)
|
||
)
|
||
else:
|
||
self._cx = float(sc.get("cx", 0.5))
|
||
self._cy = float(sc.get("cy", 0.5))
|
||
self._radius = float(sc.get("radius", 0.2))
|
||
self._eps_r = float(sc.get("eps_r", 2.0))
|
||
|
||
# ── 3. 组装 FEM 双线性和线性形式 ──
|
||
self._bilin_form = self._make_bilinear_form()
|
||
self._lin_form_real = self._make_linear_form_real()
|
||
self._lin_form_imag = self._make_linear_form_imag()
|
||
|
||
# ── 4. 初始化域(k^exponent 自适应网格密度 × domain area 线性缩放)──
|
||
# exponent 和 k_ref 均可通过 helmholtz config 配置
|
||
# exponent=2: P1 Helmholtz 理论最优 (污染误差 ∝ (kh)^2, N ∝ k^2)
|
||
# exponent=1.5: 工程折中,避免高 k 初始过密
|
||
# domain area 缩放: 保证不同域尺寸下每单位面积单元数一致 → h 不变
|
||
domain_cfg = copy.deepcopy(fem_config.get("domain"))
|
||
boundary = domain_cfg.get("boundary", [0, 0, 1, 1])
|
||
domain_area = (boundary[2] - boundary[0]) * (boundary[3] - boundary[1])
|
||
k_ref = helmholtz_config.get("k_ref", 6.0)
|
||
k_exponent = helmholtz_config.get("k_exponent", 1.5)
|
||
base_elements = domain_cfg.get("initial_num_elements", 400)
|
||
scaled_elements = int(base_elements * (self._k / k_ref) ** k_exponent * domain_area)
|
||
domain_cfg["initial_num_elements"] = max(scaled_elements, int(base_elements * domain_area))
|
||
self._domain = create_domain(
|
||
domain_config=domain_cfg,
|
||
random_state=copy.deepcopy(random_state),
|
||
)
|
||
|
||
# ── 4.5. 介质区前渐近区边缘约束 ──
|
||
# 放宽 Nyquist (N=4) → 前渐近区边缘 (N=1~1.5),赋予介质内初始网格基本相位解析能力
|
||
# 约束: h_init ≤ λ_local / N,λ_local = 2π/(k√ε_r)
|
||
# N=1.5 对应约 1.5 点/波长,刚好跨过渐近区门槛,不撑爆物理预算
|
||
pre_asymptotic_N = helmholtz_config.get("pre_asymptotic_N", 1.5)
|
||
pre_asymptotic_mesh = self._enforce_nyquist_in_dielectric(
|
||
self._domain.initial_mesh, N=pre_asymptotic_N
|
||
)
|
||
self._domain.replace_initial_mesh(pre_asymptotic_mesh)
|
||
|
||
# ── 5. PDE 特征名称 ──
|
||
pde_config = fem_config.get(fem_config.get("pde_type", "helmholtz"), {})
|
||
self._element_feature_names = [
|
||
name
|
||
for name, include in pde_config.get("element_features", {}).items()
|
||
if include
|
||
]
|
||
|
||
# ── Public interface ─────────────────────────────────────
|
||
|
||
def mesh_to_basis(self, mesh: Mesh) -> Basis:
|
||
return Basis(mesh, ElementTriP1())
|
||
|
||
def calculate_solution(self, basis: Basis, cache: bool = False) -> np.ndarray:
|
||
K = asm(self._bilin_form, basis)
|
||
f = asm(self._lin_form_real, basis) + 1j * asm(self._lin_form_imag, basis)
|
||
|
||
boundary_facets = basis.mesh.boundary_facets()
|
||
facet_basis = FacetBasis(basis.mesh, basis.elem, facets=boundary_facets)
|
||
|
||
@BilinearForm
|
||
def boundary_mass(u, v, w):
|
||
return u * v
|
||
|
||
M_boundary = asm(boundary_mass, facet_basis)
|
||
K_total = K.astype(np.complex128) - 1j * self._k * M_boundary
|
||
u_scat = solve(K_total, f)
|
||
|
||
return u_scat
|
||
|
||
def get_error_estimate_per_element(
|
||
self, basis: Basis, solution: np.ndarray
|
||
) -> Dict[str, np.ndarray]:
|
||
eps_r_arr = _compute_eps_r_at_midpoints(basis.mesh, self._cx, self._cy, self._radius, self._eps_r)
|
||
return {"indicator": _compute_residual_indicator(basis.mesh, solution, k=self._k, eps_r=eps_r_arr)}
|
||
|
||
def element_features(self, mesh: Mesh, element_feature_names: List[str]) -> Optional[np.ndarray]:
|
||
features_list = []
|
||
if "epsilon_r" in element_feature_names:
|
||
features_list.append(
|
||
_compute_eps_r_at_midpoints(mesh, self._cx, self._cy, self._radius, self._eps_r)[:, None]
|
||
)
|
||
return np.concatenate(features_list, axis=1) if features_list else None
|
||
|
||
def _enforce_nyquist_in_dielectric(self, mesh: Mesh, N: float = 1.5, max_iter: int = 10) -> Mesh:
|
||
"""Iteratively refine elements inside the dielectric until h_K ≤ λ_d/N.
|
||
|
||
λ_d = 2π/(k√ε_r) is the wavelength inside the dielectric.
|
||
N=1.5 corresponds to the edge of the pre-asymptotic regime (~1.5 points
|
||
per wavelength) — just enough for the wave field to exhibit basic phase
|
||
resolution without exhausting the physical element budget. This relaxes
|
||
the old Nyquist N=4 constraint, leaving headroom for the RL agent to
|
||
selectively refine where residual indicators demand it.
|
||
"""
|
||
lambda_d = 2.0 * np.pi / (self._k * np.sqrt(self._eps_r))
|
||
h_max = lambda_d / N
|
||
|
||
for _ in range(max_iter):
|
||
i0, i1, i2 = mesh.t[0], mesh.t[1], mesh.t[2]
|
||
x0, y0 = mesh.p[0, i0], mesh.p[1, i0]
|
||
x1, y1 = mesh.p[0, i1], mesh.p[1, i1]
|
||
x2, y2 = mesh.p[0, i2], mesh.p[1, i2]
|
||
|
||
e01 = np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
|
||
e12 = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||
e20 = np.sqrt((x0 - x2) ** 2 + (y0 - y2) ** 2)
|
||
h_K = np.maximum(np.maximum(e01, e12), e20)
|
||
|
||
midpoints = np.mean(mesh.p[:, mesh.t], axis=1).T
|
||
in_dielectric = (
|
||
(midpoints[:, 0] - self._cx) ** 2
|
||
+ (midpoints[:, 1] - self._cy) ** 2
|
||
<= self._radius**2
|
||
)
|
||
|
||
to_refine = np.where(in_dielectric & (h_K > h_max))[0]
|
||
if len(to_refine) == 0:
|
||
break
|
||
mesh = mesh.refined(to_refine)
|
||
|
||
return mesh
|
||
|
||
# ── Properties ───────────────────────────────────────────
|
||
|
||
@property
|
||
def initial_mesh(self) -> Mesh:
|
||
return self._domain.initial_mesh
|
||
|
||
@property
|
||
def boundary_line_segments(self) -> np.ndarray:
|
||
return self._domain.boundary_line_segments
|
||
|
||
|
||
@staticmethod
|
||
def project_to_scalar(values: np.ndarray) -> np.ndarray:
|
||
return values
|
||
|
||
def additional_plots_from_mesh(self, mesh: Mesh) -> Dict:
|
||
return {}
|
||
|
||
# ── FEM form assembly ────────────────────────────────────
|
||
|
||
def _eps_r_at_quad_points(self, x, y):
|
||
in_cyl = (x - self._cx) ** 2 + (y - self._cy) ** 2 <= self._radius**2
|
||
return np.where(in_cyl, self._eps_r, 1.0)
|
||
|
||
def _make_bilinear_form(self):
|
||
k = self._k
|
||
|
||
@BilinearForm
|
||
def bilin(u, v, w):
|
||
eps_r = self._eps_r_at_quad_points(w.x[0], w.x[1])
|
||
return dot(grad(u), grad(v)) - k**2 * eps_r * u * v
|
||
|
||
return bilin
|
||
|
||
def _make_linear_form_real(self):
|
||
k = self._k
|
||
|
||
@LinearForm
|
||
def lin(v, w):
|
||
eps_r = self._eps_r_at_quad_points(w.x[0], w.x[1])
|
||
return k**2 * (eps_r - 1.0) * np.cos(k * w.x[0]) * v
|
||
|
||
return lin
|
||
|
||
def _make_linear_form_imag(self):
|
||
k = self._k
|
||
|
||
@LinearForm
|
||
def lin(v, w):
|
||
eps_r = self._eps_r_at_quad_points(w.x[0], w.x[1])
|
||
return k**2 * (eps_r - 1.0) * np.sin(k * w.x[0]) * v
|
||
|
||
return lin
|
||
|
||
|
||
# ── 辅助函数 ──────────────────────────────────────────────────
|
||
|
||
|
||
def _compute_eps_r_at_midpoints(
|
||
mesh: Mesh,
|
||
cx: float = 0.5,
|
||
cy: float = 0.5,
|
||
radius: float = 0.2,
|
||
eps_r_in: float = 2.0,
|
||
) -> np.ndarray:
|
||
"""
|
||
计算每个单元中点处的相对介电常数 ε_r。
|
||
|
||
判断单元中点是否落在介质圆柱内:
|
||
- 在圆柱内 → ε_r = eps_r_in (如 2.0)
|
||
- 在圆柱外 → ε_r = 1.0 (真空)
|
||
|
||
Returns:
|
||
eps_r: shape (num_elements,)
|
||
"""
|
||
midpoints = get_element_midpoints(mesh)
|
||
x_mid, y_mid = midpoints[:, 0], midpoints[:, 1]
|
||
in_cylinder = (x_mid - cx) ** 2 + (y_mid - cy) ** 2 <= radius**2
|
||
return np.where(in_cylinder, eps_r_in, 1.0)
|
||
|
||
|
||
def _compute_residual_indicator(
|
||
mesh: Mesh,
|
||
u_h: np.ndarray,
|
||
k: float = 10.0,
|
||
eps_r: Union[float, np.ndarray] = 1.0,
|
||
) -> np.ndarray:
|
||
"""
|
||
基于残差的逐单元后验误差估计 — 无量纲化版本。
|
||
|
||
引入局部波数 k_local = k√ε_r 消除纯几何尺度 h 带来的特征偏差,
|
||
使误差指示子反映"相位分辨率残差"而非"网格粗疏程度"。
|
||
|
||
P1 单元三项:
|
||
1. r_int = (h_K/k_local)·√V_K · |k²ε_r·u_h + k²(ε_r-1)·u_inc|
|
||
2. r_jump = √(½ Σ_{e∈∂K} (h_e/k_local)·|[[∇u_h·n]]|²)
|
||
3. r_sbc = (h_bnd/k_local)·|∂u/∂n - i·k_local·u|
|
||
|
||
Returns:
|
||
eta_elements: shape (num_elements,) 的逐单元误差指标
|
||
"""
|
||
n_elements = mesh.t.shape[1]
|
||
eps_r = np.asarray(eps_r)
|
||
k_local = k * np.sqrt(np.maximum(eps_r, 1.0))
|
||
|
||
# ── 1. 单元几何量 ──
|
||
i0, i1, i2 = mesh.t[0], mesh.t[1], mesh.t[2]
|
||
x0, y0 = mesh.p[0, i0], mesh.p[1, i0]
|
||
x1, y1 = mesh.p[0, i1], mesh.p[1, i1]
|
||
x2, y2 = mesh.p[0, i2], mesh.p[1, i2]
|
||
|
||
det_J = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
|
||
element_areas = np.abs(det_J) / 2.0
|
||
|
||
edge_len_01 = np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
|
||
edge_len_12 = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||
edge_len_20 = np.sqrt((x0 - x2) ** 2 + (y0 - y2) ** 2)
|
||
h_K = np.maximum(np.maximum(edge_len_01, edge_len_12), edge_len_20)
|
||
|
||
# ── 2. 梯度(常数,因为是 P1 单元)──
|
||
u0, u1, u2 = u_h[i0], u_h[i1], u_h[i2]
|
||
inv_det = np.divide(1.0, det_J, where=det_J != 0, out=np.zeros_like(det_J))
|
||
du10, du20 = u1 - u0, u2 - u0
|
||
|
||
grad_x = ((y2 - y0) * du10 - (y1 - y0) * du20) * inv_det
|
||
grad_y = (-(x2 - x0) * du10 + (x1 - x0) * du20) * inv_det
|
||
grad_per_element = np.column_stack([grad_x, grad_y])
|
||
|
||
# ── 3. 单元内部残差 ──
|
||
u_mid = (u0 + u1 + u2) / 3.0
|
||
x_mid = (x0 + x1 + x2) / 3.0
|
||
u_inc_mid = np.exp(1j * k * x_mid)
|
||
f_mid = (k**2) * (eps_r - 1.0) * u_inc_mid
|
||
r_mid = f_mid + (k**2) * eps_r * u_mid
|
||
|
||
cell_residual_sq = (h_K**2) * element_areas * np.abs(r_mid) ** 2 / (k_local ** 2)
|
||
cell_residual_sq[element_areas < 1e-15] = 0.0
|
||
|
||
# ── 4. 内部边梯度跳变 ──
|
||
interior_facets_idx = np.where(mesh.f2t[1] != -1)[0]
|
||
elem_left = mesh.f2t[0, interior_facets_idx]
|
||
elem_right = mesh.f2t[1, interior_facets_idx]
|
||
|
||
edges_p1 = mesh.p[:, mesh.facets[0, interior_facets_idx]].T
|
||
edges_p2 = mesh.p[:, mesh.facets[1, interior_facets_idx]].T
|
||
edge_vectors = edges_p2 - edges_p1
|
||
h_e = np.linalg.norm(edge_vectors, axis=1)
|
||
n_e = np.c_[edge_vectors[:, 1], -edge_vectors[:, 0]] / (h_e[:, None] + 1e-15)
|
||
|
||
grad_left = grad_per_element[elem_left]
|
||
grad_right = grad_per_element[elem_right]
|
||
jump_val = np.abs(np.sum((grad_left - grad_right) * n_e, axis=1))
|
||
jump_val_sq = jump_val ** 2
|
||
|
||
jump_residual_sq = np.zeros(n_elements)
|
||
np.add.at(jump_residual_sq, elem_left, 0.5 * h_e * jump_val_sq / k_local[elem_left])
|
||
np.add.at(jump_residual_sq, elem_right, 0.5 * h_e * jump_val_sq / k_local[elem_right])
|
||
|
||
# ── 5. 合并 ──
|
||
eta_sq = cell_residual_sq + jump_residual_sq
|
||
|
||
# ── 6. SBC 边界残差 ──
|
||
boundary_facets_idx = np.where(mesh.f2t[1] == -1)[0]
|
||
if len(boundary_facets_idx) > 0:
|
||
bnd_elem = mesh.f2t[0, boundary_facets_idx]
|
||
bnd_p1 = mesh.p[:, mesh.facets[0, boundary_facets_idx]].T
|
||
bnd_p2 = mesh.p[:, mesh.facets[1, boundary_facets_idx]].T
|
||
bnd_vectors = bnd_p2 - bnd_p1
|
||
h_bnd = np.linalg.norm(bnd_vectors, axis=1)
|
||
n_bnd = np.c_[bnd_vectors[:, 1], -bnd_vectors[:, 0]] / (h_bnd[:, None] + 1e-15)
|
||
|
||
grad_bnd = grad_per_element[bnd_elem]
|
||
du_dn = np.sum(grad_bnd * n_bnd, axis=1)
|
||
|
||
if eps_r.ndim == 1:
|
||
k_local = k * np.sqrt(np.maximum(eps_r[bnd_elem], 1.0))
|
||
else:
|
||
k_local = k
|
||
|
||
u_edge_mean = (
|
||
u_h[mesh.facets[0, boundary_facets_idx]]
|
||
+ u_h[mesh.facets[1, boundary_facets_idx]]
|
||
) / 2.0
|
||
sbc_residual = du_dn - 1j * k_local * u_edge_mean
|
||
sbc_residual_sq = (h_bnd ** 2) * np.abs(sbc_residual) ** 2 / (k_local ** 2)
|
||
np.add.at(eta_sq, bnd_elem, sbc_residual_sq)
|
||
|
||
eta_sq = np.maximum(eta_sq, 0.0)
|
||
return np.sqrt(eta_sq)
|
||
|
||
|
||
def _compute_residual_components(
|
||
mesh: Mesh,
|
||
u_h: np.ndarray,
|
||
k: float = 10.0,
|
||
eps_r: Union[float, np.ndarray] = 1.0,
|
||
apply_log: bool = True,
|
||
) -> Dict[str, np.ndarray]:
|
||
"""
|
||
计算逐单元的三项 PDE 物理残差(分离版,无量纲化)。
|
||
|
||
引入 k_local = k√ε_r 消除几何尺度偏差,使 GNN 跨介质公平感知"相位分辨率残差"。
|
||
保留源项信息(k²(ε_r-1)·u_inc),确保极粗网格下介质内部巨大物理激励仍可被网络捕捉。
|
||
|
||
P1 单元返回:
|
||
internal_residual: (h_K/k_local)·√V_i · |k²ε_r·u + k²(ε_r-1)·u_inc|
|
||
gradient_jump: √(½ Σ_{e∈∂K_i} (h_e/k_local)·|[[∇u·n]]|²)
|
||
sbc_residual: (h_bnd/k_local)·|∂u/∂n - i·k_local·u|
|
||
element_areas: 单元面积
|
||
is_sbc_boundary: 该单元是否与 SBC 边界相邻 (0/1)
|
||
|
||
Args:
|
||
apply_log: True → log10 压缩(喂 GNN);False → 原始值(喂 reward)
|
||
"""
|
||
n_elements = mesh.t.shape[1]
|
||
eps_r = np.asarray(eps_r)
|
||
k_local = k * np.sqrt(np.maximum(eps_r, 1.0))
|
||
|
||
# ── 1. 单元几何量 ──
|
||
i0, i1, i2 = mesh.t[0], mesh.t[1], mesh.t[2]
|
||
x0, y0 = mesh.p[0, i0], mesh.p[1, i0]
|
||
x1, y1 = mesh.p[0, i1], mesh.p[1, i1]
|
||
x2, y2 = mesh.p[0, i2], mesh.p[1, i2]
|
||
|
||
det_J = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
|
||
element_areas = np.abs(det_J) / 2.0
|
||
|
||
edge_len_01 = np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
|
||
edge_len_12 = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||
edge_len_20 = np.sqrt((x0 - x2) ** 2 + (y0 - y2) ** 2)
|
||
h_K = np.maximum(np.maximum(edge_len_01, edge_len_12), edge_len_20)
|
||
|
||
# ── 2. 梯度(常数,因为是 P1 单元)──
|
||
u0, u1, u2 = u_h[i0], u_h[i1], u_h[i2]
|
||
inv_det = np.divide(1.0, det_J, where=det_J != 0, out=np.zeros_like(det_J))
|
||
du10, du20 = u1 - u0, u2 - u0
|
||
|
||
grad_x = ((y2 - y0) * du10 - (y1 - y0) * du20) * inv_det
|
||
grad_y = (-(x2 - x0) * du10 + (x1 - x0) * du20) * inv_det
|
||
grad_per_element = np.column_stack([grad_x, grad_y])
|
||
|
||
# P1 单元内部残差: ∇²u_h = 0(线性元二阶导为零),故仅含反应项
|
||
# 完整强形式: |∇²u + k²·ε_r·u + k²·(ε_r-1)·u_inc|
|
||
# 对 P1: ∇²u_h ≡ 0 → 残差 = |k²·ε_r·u + k²·(ε_r-1)·u_inc|
|
||
u_mid = (u0 + u1 + u2) / 3.0
|
||
x_mid = (x0 + x1 + x2) / 3.0
|
||
u_inc_mid = np.exp(1j * k * x_mid)
|
||
f_mid = (k**2) * (eps_r - 1.0) * u_inc_mid
|
||
r_mid = f_mid + (k**2) * eps_r * u_mid
|
||
internal_residual = (h_K / k_local) * np.sqrt(element_areas) * np.abs(r_mid)
|
||
internal_residual[element_areas < 1e-15] = 0.0
|
||
|
||
# ── 4. 内部边梯度跳变 (逐单元) ──
|
||
interior_facets_idx = np.where(mesh.f2t[1] != -1)[0]
|
||
elem_left = mesh.f2t[0, interior_facets_idx]
|
||
elem_right = mesh.f2t[1, interior_facets_idx]
|
||
|
||
edges_p1 = mesh.p[:, mesh.facets[0, interior_facets_idx]].T
|
||
edges_p2 = mesh.p[:, mesh.facets[1, interior_facets_idx]].T
|
||
edge_vectors = edges_p2 - edges_p1
|
||
h_e = np.linalg.norm(edge_vectors, axis=1)
|
||
n_e = np.c_[edge_vectors[:, 1], -edge_vectors[:, 0]] / (h_e[:, None] + 1e-15)
|
||
|
||
grad_left = grad_per_element[elem_left]
|
||
grad_right = grad_per_element[elem_right]
|
||
jump_val = np.abs(np.sum((grad_left - grad_right) * n_e, axis=1))
|
||
|
||
gradient_jump = np.zeros(n_elements, dtype=np.float64)
|
||
jump_sq_per_edge = jump_val ** 2
|
||
np.add.at(gradient_jump, elem_left, 0.5 * h_e * jump_sq_per_edge / k_local[elem_left])
|
||
np.add.at(gradient_jump, elem_right, 0.5 * h_e * jump_sq_per_edge / k_local[elem_right])
|
||
gradient_jump = np.sqrt(gradient_jump)
|
||
|
||
# ── 5. SBC 边界残差 + 边界标记 ──
|
||
sbc_residual = np.zeros(n_elements, dtype=np.float64)
|
||
is_sbc_boundary = np.zeros(n_elements, dtype=np.float32)
|
||
boundary_facets_idx = np.where(mesh.f2t[1] == -1)[0]
|
||
if len(boundary_facets_idx) > 0:
|
||
bnd_elem = mesh.f2t[0, boundary_facets_idx]
|
||
bnd_p1 = mesh.p[:, mesh.facets[0, boundary_facets_idx]].T
|
||
bnd_p2 = mesh.p[:, mesh.facets[1, boundary_facets_idx]].T
|
||
bnd_vectors = bnd_p2 - bnd_p1
|
||
h_bnd = np.linalg.norm(bnd_vectors, axis=1)
|
||
n_bnd = np.c_[bnd_vectors[:, 1], -bnd_vectors[:, 0]] / (h_bnd[:, None] + 1e-15)
|
||
|
||
grad_bnd = grad_per_element[bnd_elem]
|
||
du_dn = np.sum(grad_bnd * n_bnd, axis=1)
|
||
|
||
if eps_r.ndim == 1:
|
||
k_local = k * np.sqrt(np.maximum(eps_r[bnd_elem], 1.0))
|
||
else:
|
||
k_local = k
|
||
|
||
u_edge_mean = (
|
||
u_h[mesh.facets[0, boundary_facets_idx]]
|
||
+ u_h[mesh.facets[1, boundary_facets_idx]]
|
||
) / 2.0
|
||
sbc_val = np.abs(du_dn - 1j * k_local * u_edge_mean)
|
||
np.add.at(sbc_residual, bnd_elem, (h_bnd / k_local) * sbc_val)
|
||
is_sbc_boundary[bnd_elem] = 1.0
|
||
|
||
# ── 对数预处理:压缩跨数量级动态范围(仅 GNN 特征需要)──
|
||
if apply_log:
|
||
_log_eps = 1e-8
|
||
internal_residual = np.log10(np.maximum(internal_residual, _log_eps))
|
||
gradient_jump = np.log10(np.maximum(gradient_jump, _log_eps))
|
||
sbc_residual = np.log10(np.maximum(sbc_residual, _log_eps))
|
||
|
||
return {
|
||
"internal_residual": internal_residual.astype(np.float32),
|
||
"gradient_jump": gradient_jump.astype(np.float32),
|
||
"sbc_residual": sbc_residual.astype(np.float32),
|
||
"element_areas": element_areas.astype(np.float32),
|
||
"is_sbc_boundary": is_sbc_boundary,
|
||
}
|
||
|
||
|
||
def _compute_residual_density(
|
||
mesh: Mesh,
|
||
u_h: np.ndarray,
|
||
k: float = 10.0,
|
||
eps_r: Union[float, np.ndarray] = 1.0,
|
||
) -> Dict[str, np.ndarray]:
|
||
"""
|
||
Compute intensive (h-free) residual density components for reward.
|
||
|
||
Unlike _compute_residual_components which includes h-scaling
|
||
(h_K·√V, h_e·|jump|, h_bnd·|sbc|), this returns the raw PDE residuals
|
||
that are independent of element size — true "error densities".
|
||
|
||
Returns:
|
||
rho_int: |k²·ε_r·u + k²·(ε_r-1)·u_inc| per element
|
||
rho_jump: √(mean_{e∈∂K_int} |[[∇u·n]]|²) per element
|
||
rho_sbc: √(mean_{e∈∂K∩Γ_sbc} |∂u/∂n - i·k·u|²) per element
|
||
"""
|
||
n_elements = mesh.t.shape[1]
|
||
eps_r = np.asarray(eps_r)
|
||
|
||
# ── 1. element geometry ──
|
||
i0, i1, i2 = mesh.t[0], mesh.t[1], mesh.t[2]
|
||
x0, y0 = mesh.p[0, i0], mesh.p[1, i0]
|
||
x1, y1 = mesh.p[0, i1], mesh.p[1, i1]
|
||
x2, y2 = mesh.p[0, i2], mesh.p[1, i2]
|
||
|
||
det_J = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
|
||
|
||
# ── 2. gradient (constant per P1 element) ──
|
||
u0, u1, u2 = u_h[i0], u_h[i1], u_h[i2]
|
||
inv_det = np.divide(1.0, det_J, where=det_J != 0, out=np.zeros_like(det_J))
|
||
du10, du20 = u1 - u0, u2 - u0
|
||
|
||
grad_x = ((y2 - y0) * du10 - (y1 - y0) * du20) * inv_det
|
||
grad_y = (-(x2 - x0) * du10 + (x1 - x0) * du20) * inv_det
|
||
grad_per_element = np.column_stack([grad_x, grad_y])
|
||
|
||
# ── 3. interior residual density: |k²·ε_r·u_mid + k²·(ε_r-1)·u_inc_mid| ──
|
||
u_mid = (u0 + u1 + u2) / 3.0
|
||
x_mid = (x0 + x1 + x2) / 3.0
|
||
u_inc_mid = np.exp(1j * k * x_mid)
|
||
r_mid = (k**2) * eps_r * u_mid + (k**2) * (eps_r - 1.0) * u_inc_mid
|
||
rho_int = np.abs(r_mid)
|
||
|
||
# ── 4. gradient jump density: √(mean |[[∇u·n]]|²) per element ──
|
||
interior_facets_idx = np.where(mesh.f2t[1] != -1)[0]
|
||
elem_left = mesh.f2t[0, interior_facets_idx]
|
||
elem_right = mesh.f2t[1, interior_facets_idx]
|
||
|
||
edges_p1 = mesh.p[:, mesh.facets[0, interior_facets_idx]].T
|
||
edges_p2 = mesh.p[:, mesh.facets[1, interior_facets_idx]].T
|
||
edge_vectors = edges_p2 - edges_p1
|
||
h_e = np.linalg.norm(edge_vectors, axis=1)
|
||
n_e = np.c_[edge_vectors[:, 1], -edge_vectors[:, 0]] / (h_e[:, None] + 1e-15)
|
||
|
||
grad_left = grad_per_element[elem_left]
|
||
grad_right = grad_per_element[elem_right]
|
||
jump_val_sq = np.abs(np.sum((grad_left - grad_right) * n_e, axis=1)) ** 2
|
||
|
||
jump_sq_sum = np.zeros(n_elements, dtype=np.float64)
|
||
jump_count = np.zeros(n_elements, dtype=np.float64)
|
||
np.add.at(jump_sq_sum, elem_left, jump_val_sq)
|
||
np.add.at(jump_sq_sum, elem_right, jump_val_sq)
|
||
np.add.at(jump_count, elem_left, 1)
|
||
np.add.at(jump_count, elem_right, 1)
|
||
|
||
rho_jump = np.zeros(n_elements, dtype=np.float64)
|
||
mask_jump = jump_count > 0
|
||
rho_jump[mask_jump] = np.sqrt(jump_sq_sum[mask_jump] / jump_count[mask_jump])
|
||
|
||
# ── 5. SBC boundary density: √(mean |∂u/∂n - i·k·u|²) per element ──
|
||
rho_sbc = np.zeros(n_elements, dtype=np.float64)
|
||
boundary_facets_idx = np.where(mesh.f2t[1] == -1)[0]
|
||
if len(boundary_facets_idx) > 0:
|
||
bnd_elem = mesh.f2t[0, boundary_facets_idx]
|
||
bnd_p1 = mesh.p[:, mesh.facets[0, boundary_facets_idx]].T
|
||
bnd_p2 = mesh.p[:, mesh.facets[1, boundary_facets_idx]].T
|
||
bnd_vectors = bnd_p2 - bnd_p1
|
||
h_bnd = np.linalg.norm(bnd_vectors, axis=1)
|
||
n_bnd = np.c_[bnd_vectors[:, 1], -bnd_vectors[:, 0]] / (h_bnd[:, None] + 1e-15)
|
||
|
||
grad_bnd = grad_per_element[bnd_elem]
|
||
du_dn = np.sum(grad_bnd * n_bnd, axis=1)
|
||
|
||
if eps_r.ndim == 1:
|
||
k_local = k * np.sqrt(np.maximum(eps_r[bnd_elem], 1.0))
|
||
else:
|
||
k_local = k
|
||
|
||
u_edge_mean = (
|
||
u_h[mesh.facets[0, boundary_facets_idx]]
|
||
+ u_h[mesh.facets[1, boundary_facets_idx]]
|
||
) / 2.0
|
||
sbc_val_sq = np.abs(du_dn - 1j * k_local * u_edge_mean) ** 2
|
||
|
||
sbc_sq_sum = np.zeros(n_elements, dtype=np.float64)
|
||
sbc_count = np.zeros(n_elements, dtype=np.float64)
|
||
np.add.at(sbc_sq_sum, bnd_elem, sbc_val_sq)
|
||
np.add.at(sbc_count, bnd_elem, 1)
|
||
|
||
mask_sbc = sbc_count > 0
|
||
rho_sbc[mask_sbc] = np.sqrt(sbc_sq_sum[mask_sbc] / sbc_count[mask_sbc])
|
||
|
||
return {
|
||
"rho_int": rho_int.astype(np.float64),
|
||
"rho_jump": rho_jump.astype(np.float64),
|
||
"rho_sbc": rho_sbc.astype(np.float64),
|
||
}
|
||
|
||
|
||
# ── 工厂函数 ──────────────────────────────────────────────────
|
||
|
||
|
||
def create_helmholtz_problem(
|
||
*, fem_config: Dict[Union[str, int], Any], random_state: np.random.RandomState
|
||
) -> HelmholtzProblem:
|
||
"""
|
||
创建 Helmholtz 问题实例。
|
||
|
||
Args:
|
||
fem_config: FEM 配置字典
|
||
random_state: 随机状态
|
||
|
||
Returns:
|
||
HelmholtzProblem 实例
|
||
"""
|
||
return HelmholtzProblem(fem_config=fem_config, random_state=random_state)
|