use std::collections::{HashMap, HashSet};
use super::Engine;
use super::coeffs::Coefficients;
use super::fields::Fields;
use super::kernel::flush;
use super::operator::{Hooks, Operator, StepContext};
use crate::axis::Axis;
use crate::{Error, Result};
mod sheets;
pub use sheets::{ConformalSheets, SheetCutEdge, SheetCutFace, SheetRegion};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConformalEdge {
pub axis: Axis,
pub node: [usize; 3],
pub fraction: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConformalFace {
pub axis: Axis,
pub node: [usize; 3],
pub fraction: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConformalOptions {
pub merge_below: f64,
pub max_stability: f64,
pub iterations: usize,
pub max_rounds: usize,
}
impl Default for ConformalOptions {
fn default() -> Self {
Self {
merge_below: 0.5,
max_stability: 0.999,
iterations: 300,
max_rounds: 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConformalReport {
pub edges: usize,
pub faces: usize,
pub merged: usize,
pub groups: usize,
pub floored: usize,
pub merge_below: f64,
pub stability: f64,
pub verified: bool,
pub sheet_edges: usize,
pub sheet_faces: usize,
pub sheet_currents: usize,
pub sheet_merged: usize,
pub sheet_floored: usize,
}
impl ConformalReport {
pub fn warning(&self) -> Option<String> {
(!self.verified).then(|| {
format!(
"conformal PEC: estimated stability factor {:.4} (> 1 is unstable) with \
merge threshold {:.3}; reduce the time step factor",
self.stability, self.merge_below
)
})
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConformalPec {
edges: Vec<ConformalEdge>,
faces: Vec<ConformalFace>,
sheet_edges: Vec<SheetCutEdge>,
sheet_faces: Vec<SheetCutFace>,
options: ConformalOptions,
}
type Key = (usize, usize);
impl ConformalPec {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_options(mut self, options: ConformalOptions) -> Self {
self.options = options;
self
}
pub fn options(&self) -> &ConformalOptions {
&self.options
}
pub fn push_edge(&mut self, e: ConformalEdge) {
self.edges.push(e);
}
pub fn push_face(&mut self, f: ConformalFace) {
self.faces.push(f);
}
pub fn edges(&self) -> &[ConformalEdge] {
&self.edges
}
pub fn faces(&self) -> &[ConformalFace] {
&self.faces
}
pub fn push_sheet_edge(&mut self, e: SheetCutEdge) {
self.sheet_edges.push(e);
}
pub fn push_sheet_face(&mut self, f: SheetCutFace) {
self.sheet_faces.push(f);
}
pub fn sheet_edges(&self) -> &[SheetCutEdge] {
&self.sheet_edges
}
pub fn sheet_faces(&self) -> &[SheetCutFace] {
&self.sheet_faces
}
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
&& self.faces.is_empty()
&& self.sheet_edges.is_empty()
&& self.sheet_faces.is_empty()
}
pub fn apply(&self, engine: &mut Engine) -> Result<ConformalReport> {
let n = engine.grid().nodes();
self.validate(n)?;
let dt = engine.dt();
let mut co = engine.coefficients().clone();
for e in &self.edges {
let (_, cb) = co.e_mut(e.axis);
let k = index(n, e.node);
cb[k] = (cb[k] as f64 * e.fraction) as f32;
}
let mut seeds: Vec<(usize, usize)> = Vec::new();
for f in &self.faces {
for (a, p) in face_edges(f.axis, f.node) {
seeds.push((a.index(), index(n, p)));
}
}
let sheet = (!self.sheet_faces.is_empty())
.then(|| sheets::Prepared::new(&self.sheet_edges, &self.sheet_faces, &co, n, dt));
if let Some(p) = &sheet {
seeds.extend(p.seeds());
}
let volume_faces: HashSet<Key> = self
.faces
.iter()
.map(|f| (f.axis.index(), index(n, f.node)))
.collect();
let mut tau = self.options.merge_below.clamp(0.0, 1.0);
let rounds = self.options.max_rounds.max(1);
for round in 0..rounds {
let built = sheet
.as_ref()
.map(|p| p.construct(&co, n, dt, tau, &volume_faces));
let empty = HashSet::new();
let owned = built.as_ref().map_or(&empty, |b| &b.owned);
let (mut coeffs, groups, mut report) = self.construct(&co, n, dt, tau, owned);
let mut gterms = group_terms(&groups, n);
let mut extra = Vec::new();
if let Some(b) = &built {
b.apply_coefficients(&mut coeffs, n);
let (g, sq) = b.stability_terms(n);
gterms.extend(g);
extra = sq;
report.sheet_edges = b.report.edges;
report.sheet_faces = b.report.faces;
report.sheet_currents = b.report.groups;
report.sheet_merged = b.report.merged;
report.sheet_floored = b.report.floored;
}
let last = round + 1 == rounds || tau >= 1.0;
if self.options.iterations > 0 {
let s = stability(&coeffs, n, >erms, &extra, &seeds, self.options.iterations);
report.stability = s;
report.verified = s <= self.options.max_stability;
}
if report.verified || last {
*engine.coefficients_mut() = coeffs;
if let Some(b) = built {
engine.add_operator(b.op);
}
if !groups.is_empty() {
engine.add_operator(MergedFaces::new(groups));
}
return Ok(report);
}
tau = (tau * 1.25).min(1.0);
}
unreachable!("the last round returns")
}
fn validate(&self, n: [usize; 3]) -> Result<()> {
for e in &self.edges {
let a = e.axis.index();
if !(0.0..=1.0).contains(&e.fraction)
|| (0..3).any(|d| e.node[d] >= n[d])
|| e.node[a] + 1 >= n[a]
{
return Err(Error::Invalid(format!("invalid conformal edge {e:?}")));
}
}
for f in &self.faces {
if !(0.0..=1.0).contains(&f.fraction) || !face_exists(f.axis, f.node, n) {
return Err(Error::Invalid(format!("invalid conformal face {f:?}")));
}
}
sheets::validate(&self.sheet_edges, &self.sheet_faces, n)
}
fn construct(
&self,
co: &Coefficients,
n: [usize; 3],
dt: f64,
tau: f64,
owned: &HashSet<Key>,
) -> (Coefficients, Vec<Group>, ConformalReport) {
let idx = |p: [usize; 3]| index(n, p);
let mut out = co.clone();
let mut report = ConformalReport {
edges: self.edges.len(),
faces: 0,
merged: 0,
groups: 0,
floored: 0,
merge_below: tau,
stability: f64::NAN,
verified: true,
sheet_edges: 0,
sheet_faces: 0,
sheet_currents: 0,
sheet_merged: 0,
sheet_floored: 0,
};
let face_frac: HashMap<Key, f64> = self
.faces
.iter()
.map(|f| ((f.axis.index(), idx(f.node)), f.fraction))
.collect();
let edge_frac: HashMap<Key, f64> = self
.edges
.iter()
.map(|e| ((e.axis.index(), idx(e.node)), e.fraction))
.collect();
let fa_of = |a: Axis, p: [usize; 3]| *face_frac.get(&(a.index(), idx(p))).unwrap_or(&1.0);
let open = |a: Axis, p: [usize; 3]| co.cb(a)[idx(p)] > 0.0;
let fe_of = |a: Axis, p: [usize; 3]| {
if open(a, p) {
*edge_frac.get(&(a.index(), idx(p))).unwrap_or(&1.0)
} else {
0.0
}
};
let live = |a: Axis, p: [usize; 3]| {
!owned.contains(&(a.index(), idx(p)))
&& face_edges(a, p).iter().any(|&(e, q)| open(e, q))
};
let mut uf = Groups::default();
let small: Vec<&ConformalFace> = self
.faces
.iter()
.filter(|f| f.fraction < tau && live(f.axis, f.node))
.collect();
for f in &small {
uf.add((f.axis.index(), idx(f.node)), f.node, f.fraction);
}
for _pass in 0..small.len().max(1) {
let mut changed = false;
for f in &small {
let k0 = (f.axis.index(), idx(f.node));
let root = uf.find(k0);
if uf.area[&root] >= tau || uf.members[&root][0] != k0 {
continue;
}
let mut best: Option<((f64, f64), Key, [usize; 3])> = None;
for m in uf.members[&root].clone() {
let p = uf.node[&m];
for (e, q, nb) in in_plane_neighbours(f.axis, p, n) {
let fe = fe_of(e, q);
if fe <= 0.0 || !live(f.axis, nb) {
continue;
}
let kn = (f.axis.index(), idx(nb));
if uf.contains(kn) && uf.find(kn) == root {
continue;
}
let score = (fe, fa_of(f.axis, nb));
if best.is_none_or(|b| score > b.0) {
best = Some((score, kn, nb));
}
}
}
let Some(((_, fa_nb), kn, nb)) = best else {
continue;
};
if !uf.contains(kn) {
uf.add(kn, nb, fa_nb);
}
uf.union(root, kn);
changed = true;
}
if !changed {
break;
}
}
let mut grouped: HashMap<Key, ()> = HashMap::new();
let mut groups = Vec::new();
for (_, members) in uf.groups() {
let (mut l, mut r) = (0.0, 0.0);
let mut terms: HashMap<Key, (Axis, [usize; 3], i32)> = HashMap::new();
let mut faces = Vec::with_capacity(members.len());
for &k in &members {
let a = Axis::from_index(k.0).expect("axis in 0..3");
let p = uf.node[&k];
let (da, db) = (co.da(a)[k.1] as f64, co.db(a)[k.1] as f64);
if db > 0.0 {
let fa = fa_of(a, p);
l += fa * dt * (1.0 + da) / (2.0 * db);
r += fa * (1.0 - da) / db;
}
for ((e, q), sign) in face_edges(a, p).into_iter().zip(SIGNS) {
terms
.entry((e.index(), idx(q)))
.and_modify(|t| t.2 += sign)
.or_insert((e, q, sign));
}
faces.push((a, p));
grouped.insert(k, ());
let (da_m, db_m) = out.h_mut(a);
da_m[k.1] = 1.0;
db_m[k.1] = 0.0;
}
report.merged += members.len();
let mut terms: Vec<(Axis, [usize; 3], f64)> = terms
.into_values()
.filter(|t| t.2 != 0)
.map(|t| (t.0, t.1, t.2 as f64))
.collect();
terms.sort_by_key(|t| (t.0.index(), idx(t.1)));
if l <= 0.0 {
continue;
}
let den = l + 0.5 * r * dt;
groups.push(Group {
faces,
terms,
da: (l - 0.5 * r * dt) / den,
db: dt / den,
l,
dt_over_l: dt / l,
i: 0.0,
});
}
report.groups = groups.len();
for f in &self.faces {
let k = idx(f.node);
if grouped.contains_key(&(f.axis.index(), k)) || !live(f.axis, f.node) {
continue;
}
let mut fa = f.fraction;
if fa < tau {
fa = tau.max(1e-6);
report.floored += 1;
}
if fa < 1.0 {
let (_, db) = out.h_mut(f.axis);
db[k] = (db[k] as f64 / fa) as f32;
}
report.faces += 1;
}
(out, groups, report)
}
}
fn index(n: [usize; 3], p: [usize; 3]) -> usize {
(p[0] * n[1] + p[1]) * n[2] + p[2]
}
const SIGNS: [i32; 4] = [1, 1, -1, -1];
fn face_exists(axis: Axis, p: [usize; 3], n: [usize; 3]) -> bool {
let (b, c) = axis.others();
(0..3).all(|d| p[d] < n[d])
&& p[b.index()] + 1 < n[b.index()]
&& p[c.index()] + 1 < n[c.index()]
}
fn face_edges(axis: Axis, node: [usize; 3]) -> [(Axis, [usize; 3]); 4] {
let (b, c) = axis.others();
let mut pb = node;
pb[b.index()] += 1;
let mut pc = node;
pc[c.index()] += 1;
[(b, node), (c, pb), (b, pc), (c, node)]
}
fn in_plane_neighbours(
axis: Axis,
p: [usize; 3],
n: [usize; 3],
) -> Vec<(Axis, [usize; 3], [usize; 3])> {
let (b, c) = axis.others();
let mut v = Vec::with_capacity(4);
for (edge, off) in [(b, c), (c, b)] {
let o = off.index();
if p[o] > 0 {
let mut q = p;
q[o] -= 1;
v.push((edge, p, q));
}
let mut q = p;
q[o] += 1;
if face_exists(axis, q, n) {
v.push((edge, q, q));
}
}
v
}
#[derive(Default)]
struct Groups {
parent: HashMap<Key, Key>,
node: HashMap<Key, [usize; 3]>,
members: HashMap<Key, Vec<Key>>,
area: HashMap<Key, f64>,
}
impl Groups {
fn add(&mut self, k: Key, node: [usize; 3], area: f64) {
self.parent.insert(k, k);
self.node.insert(k, node);
self.members.insert(k, vec![k]);
self.area.insert(k, area);
}
fn contains(&self, k: Key) -> bool {
self.parent.contains_key(&k)
}
fn find(&mut self, k: Key) -> Key {
let mut r = k;
while self.parent[&r] != r {
r = self.parent[&r];
}
let mut c = k;
while self.parent[&c] != r {
let next = self.parent[&c];
self.parent.insert(c, r);
c = next;
}
r
}
fn union(&mut self, a: Key, b: Key) {
let (ra, rb) = (self.find(a), self.find(b));
if ra == rb {
return;
}
let (keep, gone) = if ra < rb { (ra, rb) } else { (rb, ra) };
self.parent.insert(gone, keep);
let a = self.area.remove(&gone).unwrap_or(0.0);
*self.area.get_mut(&keep).expect("root area") += a;
let mv = self.members.remove(&gone).unwrap_or_default();
let list = self.members.get_mut(&keep).expect("root members");
list.extend(mv);
list.sort_unstable();
}
fn groups(&self) -> Vec<(Key, Vec<Key>)> {
let mut g: Vec<(Key, Vec<Key>)> = self
.members
.iter()
.filter(|(_, m)| m.len() > 1)
.map(|(&r, m)| (r, m.clone()))
.collect();
g.sort_unstable();
g
}
}
#[derive(Debug, Clone, PartialEq)]
struct Group {
faces: Vec<(Axis, [usize; 3])>,
terms: Vec<(Axis, [usize; 3], f64)>,
da: f64,
db: f64,
l: f64,
dt_over_l: f64,
i: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MergedFaces {
groups: Vec<Group>,
energy: f64,
}
impl MergedFaces {
fn new(groups: Vec<Group>) -> Self {
Self {
groups,
energy: 0.0,
}
}
pub fn len(&self) -> usize {
self.groups.len()
}
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
}
fn circulation(g: &Group, f: &Fields<'_>) -> f64 {
g.terms
.iter()
.map(|&(a, p, s)| s * f.u.get(a, p) as f64)
.sum()
}
impl Operator for MergedFaces {
fn hooks(&self) -> Hooks {
Hooks::POST_H | Hooks::POST_E
}
fn post_h(&mut self, f: &mut Fields<'_>, _ctx: &StepContext<'_>) {
for g in &mut self.groups {
let c = circulation(g, f);
g.i = flush((g.da * g.i - g.db * c) as f32) as f64;
for &(a, p) in &g.faces {
f.i.set(a, p, g.i as f32);
}
}
}
fn post_e(&mut self, f: &mut Fields<'_>, _ctx: &StepContext<'_>) {
let mut w = 0.0;
for g in &self.groups {
let next = flush((g.da * g.i - g.db * circulation(g, f)) as f32) as f64;
w += 0.5 * g.l * g.i * next;
}
self.energy = w;
}
fn energy(&self) -> f64 {
self.energy
}
}
pub fn stability_factor(
co: &Coefficients,
nodes: [usize; 3],
seeds: &[(usize, usize)],
iterations: usize,
) -> f64 {
stability(co, nodes, &[], &[], seeds, iterations)
}
type GroupTerms = (f64, Vec<(usize, usize, f64)>);
fn group_terms(groups: &[Group], n: [usize; 3]) -> Vec<GroupTerms> {
groups
.iter()
.map(|g| {
(
g.dt_over_l,
g.terms
.iter()
.map(|&(a, p, s)| (a.index(), index(n, p), s))
.collect(),
)
})
.collect()
}
fn stability(
co: &Coefficients,
n: [usize; 3],
gterms: &[GroupTerms],
extra: &[f64],
seeds: &[(usize, usize)],
iterations: usize,
) -> f64 {
let len = n[0] * n[1] * n[2];
let sq: [Vec<f64>; 4] = core::array::from_fn(|a| {
if a < 3 {
co.data[3 + a].iter().map(|&c| (c as f64).sqrt()).collect()
} else {
extra.to_vec()
}
});
let db: [&[f32]; 3] = core::array::from_fn(|a| &co.data[9 + a][..]);
let mut v: [Vec<f64>; 4] = core::array::from_fn(|a| {
let mut state = 0x9e37_79b9_7f4a_7c15u64 ^ (a as u64 + 1);
(0..sq[a].len())
.map(|k| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let r = (state >> 11) as f64 / (1u64 << 53) as f64;
if sq[a][k] > 0.0 {
1e-3 * (r - 0.5)
} else {
0.0
}
})
.collect()
});
for &(a, k) in seeds {
if sq[a][k] > 0.0 {
v[a][k] += 1.0;
}
}
let mut h: [Vec<f64>; 3] = core::array::from_fn(|_| vec![0.0; len]);
let mut w: [Vec<f64>; 4] = core::array::from_fn(|a| vec![0.0; sq[a].len()]);
let mut lambda = 0.0;
for _ in 0..iterations.max(1) {
let norm = v.iter().flatten().map(|x| x * x).sum::<f64>().sqrt();
if norm == 0.0 {
return 0.0;
}
for x in v.iter_mut().flatten() {
*x /= norm;
}
apply_s(n, &sq, &db, gterms, &v, &mut h, &mut w);
lambda = v
.iter()
.flatten()
.zip(w.iter().flatten())
.map(|(a, b)| a * b)
.sum::<f64>();
core::mem::swap(&mut v, &mut w);
}
lambda.max(0.0).sqrt() / 2.0
}
fn apply_s(
n: [usize; 3],
sq: &[Vec<f64>; 4],
db: &[&[f32]; 3],
groups: &[GroupTerms],
v: &[Vec<f64>; 4],
h: &mut [Vec<f64>; 3],
w: &mut [Vec<f64>; 4],
) {
let stride = [n[1] * n[2], n[2], 1];
let x: [Vec<f64>; 4] =
core::array::from_fn(|a| v[a].iter().zip(&sq[a]).map(|(v, s)| v * s).collect());
for wa in w.iter_mut() {
wa.fill(0.0);
}
for axis in Axis::ALL {
let a = axis.index();
let (bb, cc) = axis.others();
let (b, c) = (bb.index(), cc.index());
let ha = &mut h[a];
for i in 0..n[0] {
for j in 0..n[1] {
for k in 0..n[2] {
let p = [i, j, k];
let q = (i * n[1] + j) * n[2] + k;
let d = db[a][q] as f64;
if d == 0.0 || p[b] + 1 >= n[b] || p[c] + 1 >= n[c] {
ha[q] = 0.0;
continue;
}
let curl = x[c][q + stride[b]] - x[c][q] - x[b][q + stride[c]] + x[b][q];
ha[q] = d * curl;
}
}
}
for q in 0..ha.len() {
let f = ha[q];
if f == 0.0 {
continue;
}
w[c][q + stride[b]] += f;
w[c][q] -= f;
w[b][q + stride[c]] -= f;
w[b][q] += f;
}
}
for (d, terms) in groups {
let curl: f64 = terms.iter().map(|&(a, k, s)| s * x[a][k]).sum();
let f = d * curl;
for &(a, k, s) in terms {
w[a][k] += s * f;
}
}
for a in 0..4 {
for (wv, s) in w[a].iter_mut().zip(&sq[a]) {
*wv *= s;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{CellMaterials, Grid, TimeStep};
fn engine(s: f64) -> Engine {
let g = Grid::uniform([12, 12, 12], [1e-3; 3]).unwrap();
Engine::builder(g.clone())
.materials(CellMaterials::vacuum(&g))
.timestep(TimeStep::Courant(s))
.threads(1)
.build()
.unwrap()
}
#[test]
fn uncut_grid_stability_is_courant_factor() {
let e = engine(0.9);
let n = e.grid().nodes();
let s = stability_factor(&e.coefficients(), n, &[], 400);
assert!(s < 0.9 && s > 0.85, "{s}");
}
fn sliver() -> ConformalPec {
let mut c = ConformalPec::new();
c.push_face(ConformalFace {
axis: Axis::Z,
node: [5, 5, 5],
fraction: 0.02,
});
for q in [[5, 5, 5], [6, 5, 5]] {
c.push_edge(ConformalEdge {
axis: Axis::Y,
node: q,
fraction: 0.02,
});
}
c.push_edge(ConformalEdge {
axis: Axis::X,
node: [5, 6, 5],
fraction: 0.0,
});
c
}
#[test]
fn sliver_face_is_merged_and_stable() {
let raw = sliver().with_options(ConformalOptions {
merge_below: 0.0,
max_rounds: 1,
..ConformalOptions::default()
});
let mut e = engine(0.99);
let r = raw.apply(&mut e).unwrap();
assert!(!r.verified && r.stability > 1.5, "{r:?}");
assert!(r.warning().is_some());
assert_eq!(r.merged, 0);
let mut e = engine(0.99);
let r = sliver().apply(&mut e).unwrap();
assert!(r.verified, "{r:?}");
assert_eq!((r.merged, r.groups, r.floored), (2, 1, 0), "{r:?}");
assert!(r.stability < 0.999 && r.stability > 0.9, "{r:?}");
let n = e.grid().nodes();
let base = engine(0.99);
let k = index(n, [5, 5, 5]);
let want = base.coefficients().cb(Axis::Y)[k] as f64 * 0.02;
assert!((e.coefficients().cb(Axis::Y)[k] as f64 / want - 1.0).abs() < 1e-6);
assert_eq!(e.coefficients().cb(Axis::X)[index(n, [5, 6, 5])], 0.0);
assert_eq!(e.coefficients().db(Axis::Z)[k], 0.0);
assert_eq!(e.coefficients().db(Axis::Z)[index(n, [5, 4, 5])], 0.0);
}
#[test]
fn merged_scheme_conserves_energy() {
let mut e = engine(0.99);
let r = sliver().apply(&mut e).unwrap();
assert!(r.verified);
{
let mut f = e.fields_mut();
f.u.set(Axis::X, [5, 5, 5], 1.0);
f.u.set(Axis::Y, [3, 7, 4], -0.5);
}
e.run(1);
let w0 = e.energy();
let mut dev: f64 = 0.0;
for _ in 0..50 {
e.run(100);
dev = dev.max((e.energy() / w0 - 1.0).abs());
}
assert!(dev < 1e-5, "{dev}");
let a = e.energy();
let b = e.step_with_energy();
assert_eq!(a, b);
}
#[test]
fn rejects_bad_input() {
let mut e = engine(0.9);
let mut c = ConformalPec::new();
c.push_edge(ConformalEdge {
axis: Axis::X,
node: [12, 0, 0],
fraction: 0.5,
});
assert!(c.apply(&mut e).is_err());
let mut c = ConformalPec::new();
c.push_face(ConformalFace {
axis: Axis::X,
node: [0, 0, 0],
fraction: 1.5,
});
assert!(c.apply(&mut e).is_err());
}
}