afem/environment/helmholtz.py

635 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import copy
import time
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", 2.0)
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:
_t = {}
_t0 = time.perf_counter()
K = asm(self._bilin_form, basis)
_t1 = time.perf_counter()
f = asm(self._lin_form_real, basis) + 1j * asm(self._lin_form_imag, basis)
_t2 = time.perf_counter()
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)
_t3 = time.perf_counter()
K_total = K.astype(np.complex128) - 1j * self._k * M_boundary
u_scat = solve(K_total, f)
_t4 = time.perf_counter()
_t["assemble_K"] = _t1 - _t0
_t["assemble_f"] = _t2 - _t1
_t["assemble_boundary"] = _t3 - _t2
_t["solve"] = _t4 - _t3
_t["total"] = _t4 - _t0
_t["n_dof"] = int(basis.mesh.p.shape[1])
self._last_solve_timing = _t
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₀ 归一化(非 k_local使误差指示子反映"绝对物理误差"
而非"相对局部波长的分辨率"。介质内短波ε_r>1的残差在 k_local 下被
压低 √ε_r 倍,改用 k₀ 后介质内 η 自然放大Agent 获得正确优先级。
P1 单元三项:
1. r_int = (h_K/k)·√V_K · |k²ε_r·u_h + k²(ε_r-1)·u_inc|
2. r_jump = √(½ Σ_{e∈∂K} (h_e/k)·|[[∇u_h·n]]|²)
3. r_sbc = (h_bnd/k)·|∂u/∂n - i·k_local·u| (SBC 条件仍用 k_local)
Returns:
eta_elements: shape (num_elements,) 的逐单元误差指标
"""
n_elements = mesh.t.shape[1]
eps_r = np.asarray(eps_r)
# ── 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 ** 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)
np.add.at(jump_residual_sq, elem_right, 0.5 * h_e * jump_val_sq / k)
# ── 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 ** 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₀ 归一化 — 介质内短波残差不再被 k_local 压低GNN 获得
正确的介质内/外优先级信号。
P1 单元返回:
internal_residual: (h_K/k)·√V_i · |k²ε_r·u + k²(ε_r-1)·u_inc|
gradient_jump: √(½ Σ_{e∈∂K_i} (h_e/k)·|[[∇u·n]]|²)
sbc_residual: (h_bnd/k)·|∂u/∂n - i·k_local·u| (SBC 条件仍用 k_local)
element_areas: 单元面积
is_sbc_boundary: 该单元是否与 SBC 边界相邻 (0/1)
Args:
apply_log: True → log10 压缩(喂 GNNFalse → 原始值(喂 reward
"""
n_elements = mesh.t.shape[1]
eps_r = np.asarray(eps_r)
# ── 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) * 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)
np.add.at(gradient_jump, elem_right, 0.5 * h_e * jump_sq_per_edge / k)
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) * 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)