CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 3s
CI / Build (ubuntu-latest) (push) Failing after 4s
CI / Format Check (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
CI / Build CPU-Only (Explicit) (push) Failing after 1m6s
Documentation / Build API Documentation (push) Failing after 1m9s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
567 lines
20 KiB
Rust
567 lines
20 KiB
Rust
//! embedded3 item 11a: the fresh-cell falsifier of the 2D track
|
||
//! (`embedded_fresh_cell_falsifier.rs`, omni-cortex
|
||
//! `docs/fresh_cell_gcl_campaign.md`) on the 3D solver, per unit span —
|
||
//! the rigid Turek–Hron flag (0.35 × 0.02 m) extruded across a periodic
|
||
//! slab, oscillating transversely in still fluid at the flag's tip speed
|
||
//! (1 m/s peak, 80 mm amplitude) on h = 1/152 at dt = 3.24e-4. Per step:
|
||
//! the load per unit span (the ghost wall's traction route over the
|
||
//! plate's samples; the cut wall's operator route), a far-field pressure
|
||
//! probe, the fluid's kinetic energy, the fresh-cell count.
|
||
//!
|
||
//! Registered gates (`docs/embedded3_campaign.md` item 11):
|
||
//! - GhostBinary reproduces the 2D wall's impulse: energy per flipped
|
||
//! column within 30 % of the 2D 0.048 J/m per flipped cell, spike RMS
|
||
//! exponent in dt ≈ −1 (published −0.8 for the raw volume source);
|
||
//! - CutCell: energy per fresh column ≥ 20× lower, max force spike < 5 %
|
||
//! of ½ρU²L, exponent ∈ [−0.3, 0.3].
|
||
//!
|
||
//! Default run: dt only, both schemes (minutes on the host);
|
||
//! `RTX_E3_FALSIFIER_LADDER=1` runs dt, dt/2, dt/4 and fits the exponent
|
||
//! (the gated variant is `#[ignore]`); `RTX_E3_FALSIFIER_NZ` sets the
|
||
//! span in cells (default 4); `RTX_E3_FALSIFIER_CSV=<dir>` dumps records.
|
||
|
||
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||
use rtx_cfd::solvers::incompressible::embedded3::{
|
||
Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme,
|
||
};
|
||
use std::io::Write as _;
|
||
|
||
const RHO: f64 = 1000.0;
|
||
const MU: f64 = 1.0;
|
||
const N: usize = 152;
|
||
const DT_FSI2: f64 = 3.24e-4;
|
||
const HX: f64 = 0.175;
|
||
const HY: f64 = 0.01;
|
||
const AMP: f64 = 0.08;
|
||
const U_PEAK: f64 = 1.0;
|
||
const CX: f64 = 0.5;
|
||
const CY0: f64 = 0.5;
|
||
/// The 2D wall's measured energy per flipped cell (J/m at U = 1, h = 1/152).
|
||
const ENERGY_2D: f64 = 0.048;
|
||
|
||
fn span_cells() -> usize {
|
||
std::env::var("RTX_E3_FALSIFIER_NZ")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(4)
|
||
}
|
||
|
||
fn center_y(t: f64) -> f64 {
|
||
CY0 + AMP * (U_PEAK / AMP * t).sin()
|
||
}
|
||
|
||
fn center_v(t: f64) -> f64 {
|
||
U_PEAK * (U_PEAK / AMP * t).cos()
|
||
}
|
||
|
||
fn plate_sdf(x: f64, y: f64, yc: f64) -> f64 {
|
||
let qx = (x - CX).abs() - HX;
|
||
let qy = (y - yc).abs() - HY;
|
||
let outside = (qx.max(0.0).powi(2) + qy.max(0.0).powi(2)).sqrt();
|
||
outside + qx.max(qy).min(0.0)
|
||
}
|
||
|
||
const R_CIRCLE: f64 = 0.05;
|
||
|
||
/// `RTX_E3_FALSIFIER_BODY=circle`: the 2D falsifier's smooth body (a
|
||
/// cylinder of radius 0.05 across the span) instead of the plate.
|
||
fn circle_body() -> bool {
|
||
std::env::var("RTX_E3_FALSIFIER_BODY").is_ok_and(|v| v == "circle")
|
||
}
|
||
|
||
/// `RTX_E3_FALSIFIER_BODY=stadium`: the plate with semicircular ends
|
||
/// (radius `HY`): the same length and thickness, a smooth interface for
|
||
/// the cut geometry's linear interpolant.
|
||
fn stadium_body() -> bool {
|
||
std::env::var("RTX_E3_FALSIFIER_BODY").is_ok_and(|v| v == "stadium")
|
||
}
|
||
|
||
fn stadium_sdf(x: f64, y: f64, yc: f64) -> f64 {
|
||
let half = HX - HY;
|
||
let qx = (x - CX).abs().max(half) - half;
|
||
(qx * qx + (y - yc).powi(2)).sqrt() - HY
|
||
}
|
||
|
||
fn plate(moving: bool) -> Body {
|
||
let yc = move |t: f64| if moving { center_y(t) } else { CY0 };
|
||
let vc = move |t: f64| if moving { center_v(t) } else { 0.0 };
|
||
if circle_body() {
|
||
return Body::from_sdf(move |x, y, _z, t| {
|
||
((x - CX).powi(2) + (y - yc(t)).powi(2)).sqrt() - R_CIRCLE
|
||
})
|
||
.with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0));
|
||
}
|
||
if stadium_body() {
|
||
return Body::from_sdf(move |x, y, _z, t| stadium_sdf(x, y, yc(t)))
|
||
.with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0));
|
||
}
|
||
Body::from_sdf(move |x, y, _z, t| plate_sdf(x, y, yc(t)))
|
||
.with_surface_velocity(move |_, _, _, t| (0.0, vc(t), 0.0))
|
||
}
|
||
|
||
/// The load scale `½ρU²L` of the body (its length across the motion).
|
||
fn load_scale() -> f64 {
|
||
let l = if circle_body() {
|
||
2.0 * R_CIRCLE
|
||
} else {
|
||
2.0 * HX
|
||
};
|
||
0.5 * RHO * U_PEAK * U_PEAK * l
|
||
}
|
||
|
||
/// Surface samples of the plate at `t`: `(x, y, z, nx, ny, area)` over the
|
||
/// four edges at spacing `ds` and `nz` z levels.
|
||
fn samples(
|
||
t: f64,
|
||
moving: bool,
|
||
ds: f64,
|
||
nz: usize,
|
||
dz: f64,
|
||
) -> Vec<(f64, f64, f64, f64, f64, f64)> {
|
||
let yc = if moving { center_y(t) } else { CY0 };
|
||
let mut out = Vec::new();
|
||
if circle_body() {
|
||
let n = ((2.0 * std::f64::consts::PI * R_CIRCLE / ds).ceil() as usize).max(8);
|
||
let dth = 2.0 * std::f64::consts::PI / n as f64;
|
||
for k in 0..n {
|
||
let th = (k as f64 + 0.5) * dth;
|
||
let (sn, cs) = th.sin_cos();
|
||
for kz in 0..nz {
|
||
out.push((
|
||
CX + R_CIRCLE * cs,
|
||
yc + R_CIRCLE * sn,
|
||
(kz as f64 + 0.5) * dz,
|
||
cs,
|
||
sn,
|
||
R_CIRCLE * dth * dz,
|
||
));
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
if stadium_body() {
|
||
let half = HX - HY;
|
||
let n_flat = ((2.0 * half / ds).ceil() as usize).max(1);
|
||
for k in 0..n_flat {
|
||
let x = CX - half + (k as f64 + 0.5) / n_flat as f64 * 2.0 * half;
|
||
for kz in 0..nz {
|
||
let z = (kz as f64 + 0.5) * dz;
|
||
let a = 2.0 * half / n_flat as f64 * dz;
|
||
out.push((x, yc + HY, z, 0.0, 1.0, a));
|
||
out.push((x, yc - HY, z, 0.0, -1.0, a));
|
||
}
|
||
}
|
||
let n_arc = ((std::f64::consts::PI * HY / ds).ceil() as usize).max(4);
|
||
for (cx, sign) in [(CX + half, 1.0), (CX - half, -1.0)] {
|
||
for k in 0..n_arc {
|
||
let th = -std::f64::consts::FRAC_PI_2
|
||
+ (k as f64 + 0.5) / n_arc as f64 * std::f64::consts::PI;
|
||
let (sn, cs) = th.sin_cos();
|
||
let (nx, ny) = (sign * cs, sn);
|
||
for kz in 0..nz {
|
||
out.push((
|
||
cx + HY * nx,
|
||
yc + HY * ny,
|
||
(kz as f64 + 0.5) * dz,
|
||
nx,
|
||
ny,
|
||
std::f64::consts::PI * HY / n_arc as f64 * dz,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
let (x0, x1, y0, y1) = (CX - HX, CX + HX, yc - HY, yc + HY);
|
||
let mut edge = |ax: f64, ay: f64, bx: f64, by: f64, nx: f64, ny: f64| {
|
||
let len = ((bx - ax).powi(2) + (by - ay).powi(2)).sqrt();
|
||
let n = ((len / ds).ceil() as usize).max(1);
|
||
for k in 0..n {
|
||
let s = (k as f64 + 0.5) / n as f64;
|
||
for kz in 0..nz {
|
||
out.push((
|
||
ax + s * (bx - ax),
|
||
ay + s * (by - ay),
|
||
(kz as f64 + 0.5) * dz,
|
||
nx,
|
||
ny,
|
||
len / n as f64 * dz,
|
||
));
|
||
}
|
||
}
|
||
};
|
||
edge(x0, y0, x1, y0, 0.0, -1.0);
|
||
edge(x1, y0, x1, y1, 1.0, 0.0);
|
||
edge(x1, y1, x0, y1, 0.0, 1.0);
|
||
edge(x0, y1, x0, y0, -1.0, 0.0);
|
||
out
|
||
}
|
||
|
||
struct Record {
|
||
t: f64,
|
||
/// Load per unit span (the scheme's wall route).
|
||
fy: f64,
|
||
/// Load per unit span by the control-volume route (a box of whole
|
||
/// cells around the body, reading no near-wall value).
|
||
fy_cv: f64,
|
||
fresh: usize,
|
||
skipped: usize,
|
||
p_far: f64,
|
||
/// Kinetic energy per unit span over the fluid cells.
|
||
ke: f64,
|
||
}
|
||
|
||
struct Run {
|
||
records: Vec<Record>,
|
||
/// The largest kinetic-energy change per step at a step with fresh
|
||
/// cells (after the impulsive start) over that step's flipped columns
|
||
/// (J/m) — the 2D falsifier's 2.604 J/m over 54 cells = 0.048.
|
||
energy_per_flip: f64,
|
||
seconds: f64,
|
||
}
|
||
|
||
fn run(scheme: WallScheme, moving: bool, dt: f64, t_end: f64) -> Run {
|
||
let nz = span_cells();
|
||
let h = 1.0 / N as f64;
|
||
let lz = nz as f64 * h;
|
||
let mut solver = Solver::new(
|
||
Fluid {
|
||
density: RHO,
|
||
viscosity: MU,
|
||
reference_velocity: 1.0,
|
||
reference_length: 2.0 * HY,
|
||
},
|
||
Parameters {
|
||
corrector_steps: 2,
|
||
tolerance: 1e-8,
|
||
convection_scheme: ConvectionScheme::Upwind,
|
||
wall_scheme: scheme,
|
||
boundaries: Boundaries {
|
||
z0: Side::Periodic,
|
||
z1: Side::Periodic,
|
||
..Boundaries::default()
|
||
},
|
||
..Parameters::default()
|
||
},
|
||
);
|
||
solver.set_boundary_velocity(|_, _, _, _| (0.0, 0.0, 0.0));
|
||
if moving {
|
||
solver.set_moving_body(plate(true));
|
||
} else {
|
||
solver.set_body(plate(false));
|
||
}
|
||
let g = Grid::cubic(N, N, nz, h);
|
||
let mut field = Field::new(g);
|
||
solver.initialize(&mut field);
|
||
let steps = (t_end / dt).round() as usize;
|
||
let mut records = Vec::with_capacity(steps);
|
||
// The 2D definition: the largest |ΔKE| step's energy over that
|
||
// step's flipped columns.
|
||
let mut largest_jump = 0.0_f64;
|
||
let mut energy_per_flip = 0.0_f64;
|
||
let mut ke_prev: Option<f64> = None;
|
||
let start = std::time::Instant::now();
|
||
let (jp, ip, kp) = (
|
||
(0.92 * N as f64) as usize,
|
||
(0.5 * N as f64) as usize,
|
||
nz / 2,
|
||
);
|
||
for step in 0..steps {
|
||
let result = solver.advance(&mut field, dt);
|
||
let t = (step + 1) as f64 * dt;
|
||
let mask = solver.mask().expect("mask");
|
||
let body = solver.body().expect("body");
|
||
let (mut fy, mut skipped) = (0.0, 0usize);
|
||
match scheme {
|
||
WallScheme::GhostBinary => {
|
||
for (x, y, z, nx, ny, area) in samples(t, moving, 0.5 * h, nz, h) {
|
||
match mask.traction_at(body, &field, MU, t, [x, y, z], [nx, ny, 0.0]) {
|
||
Some(tr) => fy += tr[1] * area,
|
||
None => skipped += 1,
|
||
}
|
||
}
|
||
}
|
||
WallScheme::CutCell => {
|
||
fy = mask.cut_wall_force(body, &field, MU, t).expect("cut wall")[1];
|
||
}
|
||
}
|
||
fy /= lz;
|
||
let margin = 8;
|
||
let fy_cv = mask.control_volume_force(
|
||
&field,
|
||
dt,
|
||
RHO,
|
||
MU,
|
||
None,
|
||
(margin, N - margin, margin, N - margin, 0, nz),
|
||
)[1] / lz;
|
||
let p_far = field.p[g.cell(kp, jp, ip)];
|
||
let mut ke = 0.0;
|
||
for k in 0..nz {
|
||
for j in 0..N {
|
||
for i in 0..N {
|
||
let idx = g.cell(k, j, i);
|
||
if mask.is_fluid_cell(idx) {
|
||
let uc = 0.5 * (field.u[g.uface(k, j, i)] + field.u[g.uface(k, j, i + 1)]);
|
||
let vc = 0.5 * (field.v[g.vface(k, j, i)] + field.v[g.vface(k, j + 1, i)]);
|
||
let wc = 0.5 * (field.w[g.wface(k, j, i)] + field.w[g.wface(k + 1, j, i)]);
|
||
ke += 0.5 * RHO * (uc * uc + vc * vc + wc * wc) * h * h * h * mask.vol(idx);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
ke /= lz;
|
||
if let Some(prev) = ke_prev {
|
||
if step > 30 && result.fresh_cells > 0 && (ke - prev).abs() > largest_jump {
|
||
largest_jump = (ke - prev).abs();
|
||
// The plate's event is its row (the 2D divided by the row's
|
||
// 54 cells); the circle's is the step's fresh columns.
|
||
let columns = if circle_body() {
|
||
result.fresh_cells as f64 / nz as f64
|
||
} else {
|
||
(2.0 * HX / h).round()
|
||
};
|
||
energy_per_flip = largest_jump / columns;
|
||
}
|
||
}
|
||
ke_prev = Some(ke);
|
||
records.push(Record {
|
||
t,
|
||
fy,
|
||
fy_cv,
|
||
fresh: result.fresh_cells,
|
||
skipped,
|
||
p_far,
|
||
ke,
|
||
});
|
||
}
|
||
Run {
|
||
records,
|
||
energy_per_flip,
|
||
seconds: start.elapsed().as_secs_f64(),
|
||
}
|
||
}
|
||
|
||
/// Spike series: the load minus its 21-step running median.
|
||
fn spikes(f: &[f64]) -> Vec<f64> {
|
||
let w = 10usize;
|
||
(0..f.len())
|
||
.map(|k| {
|
||
let lo = k.saturating_sub(w);
|
||
let hi = (k + w + 1).min(f.len());
|
||
let mut win: Vec<f64> = f[lo..hi].to_vec();
|
||
win.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
f[k] - win[win.len() / 2]
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
struct Stats {
|
||
rms_force: f64,
|
||
rms_spike: f64,
|
||
max_spike: f64,
|
||
rms_spike_cv: f64,
|
||
max_spike_cv: f64,
|
||
rms_pfar_spike: f64,
|
||
max_pfar_spike: f64,
|
||
max_ke_jump: f64,
|
||
fresh_total: usize,
|
||
skipped_max: usize,
|
||
}
|
||
|
||
fn stats(records: &[Record], t_lo: f64, t_hi: f64) -> Stats {
|
||
let fy: Vec<f64> = records.iter().map(|r| r.fy).collect();
|
||
let sp = spikes(&fy);
|
||
let fcv: Vec<f64> = records.iter().map(|r| r.fy_cv).collect();
|
||
let spc = spikes(&fcv);
|
||
let pf: Vec<f64> = records.iter().map(|r| r.p_far).collect();
|
||
let spf = spikes(&pf);
|
||
let idx: Vec<usize> = (0..records.len())
|
||
.filter(|&k| records[k].t >= t_lo && records[k].t <= t_hi)
|
||
.collect();
|
||
let rms = |v: &dyn Fn(usize) -> f64| {
|
||
(idx.iter().map(|&k| v(k) * v(k)).sum::<f64>() / idx.len().max(1) as f64).sqrt()
|
||
};
|
||
Stats {
|
||
rms_force: rms(&|k| fy[k]),
|
||
rms_spike: rms(&|k| sp[k]),
|
||
max_spike: idx.iter().map(|&k| sp[k].abs()).fold(0.0, f64::max),
|
||
rms_spike_cv: rms(&|k| spc[k]),
|
||
max_spike_cv: idx.iter().map(|&k| spc[k].abs()).fold(0.0, f64::max),
|
||
rms_pfar_spike: rms(&|k| spf[k]),
|
||
max_pfar_spike: idx.iter().map(|&k| spf[k].abs()).fold(0.0, f64::max),
|
||
max_ke_jump: idx
|
||
.iter()
|
||
.filter(|&&k| k > 0)
|
||
.map(|&k| (records[k].ke - records[k - 1].ke).abs())
|
||
.fold(0.0, f64::max),
|
||
fresh_total: idx.iter().map(|&k| records[k].fresh).sum(),
|
||
skipped_max: idx.iter().map(|&k| records[k].skipped).max().unwrap_or(0),
|
||
}
|
||
}
|
||
|
||
fn dump(dir: &str, name: &str, records: &[Record]) {
|
||
let path = std::path::Path::new(dir).join(format!("{name}.csv"));
|
||
let mut f = std::fs::File::create(path).expect("csv");
|
||
writeln!(f, "t,fy,fy_cv,fresh,skipped,p_far,ke").unwrap();
|
||
for r in records {
|
||
writeln!(
|
||
f,
|
||
"{:.6},{:.6e},{:.6e},{},{},{:.6e},{:.6e}",
|
||
r.t, r.fy, r.fy_cv, r.fresh, r.skipped, r.p_far, r.ke
|
||
)
|
||
.unwrap();
|
||
}
|
||
}
|
||
|
||
struct Verdict {
|
||
energy_per_flip: f64,
|
||
max_spike: f64,
|
||
exponent: Option<f64>,
|
||
}
|
||
|
||
fn falsify(scheme: WallScheme, ladder: bool) -> Verdict {
|
||
let csv_dir = std::env::var("RTX_E3_FALSIFIER_CSV").ok();
|
||
let period = 2.0 * std::f64::consts::PI * AMP / U_PEAK;
|
||
let t_end = 0.3 * period;
|
||
let (t_lo, t_hi) = (0.02 * period, 0.28 * period);
|
||
let rest = run(scheme, false, DT_FSI2, t_end);
|
||
let s0 = stats(&rest.records, t_lo, t_hi);
|
||
println!(
|
||
" {scheme:?} plate AT REST, dt {DT_FSI2:.2e} ({:.0} s): rms force {:.3e}, rms spike {:.3e}, max spike {:.3e}, fresh {}, skipped max {}",
|
||
rest.seconds, s0.rms_force, s0.rms_spike, s0.max_spike, s0.fresh_total, s0.skipped_max
|
||
);
|
||
if let Some(d) = &csv_dir {
|
||
dump(d, &format!("{scheme:?}_rest"), &rest.records);
|
||
}
|
||
let dts: Vec<f64> = if ladder {
|
||
vec![DT_FSI2, DT_FSI2 / 2.0, DT_FSI2 / 4.0]
|
||
} else {
|
||
vec![DT_FSI2]
|
||
};
|
||
let mut points = Vec::new();
|
||
let mut energy = 0.0_f64;
|
||
let mut max_spike = 0.0_f64;
|
||
for &dt in &dts {
|
||
let r = run(scheme, true, dt, t_end);
|
||
let s = stats(&r.records, t_lo, t_hi);
|
||
println!(
|
||
" {scheme:?} plate MOVING, dt {dt:.3e} ({} steps, {:.0} s): rms force {:.3e}, rms spike {:.3e} ({:.1}x rest), max spike {:.3e} N/m ({:.2e} of ½ρU²L), fresh cells {} ({:.2}/step), skipped max {}",
|
||
r.records.len(),
|
||
r.seconds,
|
||
s.rms_force,
|
||
s.rms_spike,
|
||
s.rms_spike / s0.rms_spike.max(1e-300),
|
||
s.max_spike,
|
||
s.max_spike / load_scale(),
|
||
s.fresh_total,
|
||
s.fresh_total as f64 / r.records.len() as f64,
|
||
s.skipped_max
|
||
);
|
||
println!(
|
||
" control-volume route: rms spike {:.3e}, max spike {:.3e} N/m ({:.2e} of ½ρU²L)",
|
||
s.rms_spike_cv,
|
||
s.max_spike_cv,
|
||
s.max_spike_cv / load_scale()
|
||
);
|
||
println!(
|
||
" far probe p(0.5, 0.92): rms spike {:.3e}, max spike {:.3e}; max |ΔKE| per step {:.3e} J/m; energy per flipped column {:.3e} J/m ({:.2} of the 2D wall's {ENERGY_2D})",
|
||
s.rms_pfar_spike,
|
||
s.max_pfar_spike,
|
||
s.max_ke_jump,
|
||
r.energy_per_flip,
|
||
r.energy_per_flip / ENERGY_2D
|
||
);
|
||
if let Some(d) = &csv_dir {
|
||
dump(d, &format!("{scheme:?}_moving_dt{dt:.3e}"), &r.records);
|
||
}
|
||
assert!(s.rms_force.is_finite() && s.rms_spike.is_finite());
|
||
if dt == DT_FSI2 {
|
||
energy = r.energy_per_flip;
|
||
max_spike = s.max_spike;
|
||
}
|
||
points.push((dt, s.rms_spike));
|
||
}
|
||
let exponent = (points.len() >= 2).then(|| {
|
||
let xs: Vec<f64> = points.iter().map(|p| p.0.ln()).collect();
|
||
let ys: Vec<f64> = points.iter().map(|p| p.1.ln()).collect();
|
||
let mx = xs.iter().sum::<f64>() / xs.len() as f64;
|
||
let my = ys.iter().sum::<f64>() / ys.len() as f64;
|
||
let num: f64 = xs.iter().zip(&ys).map(|(x, y)| (x - mx) * (y - my)).sum();
|
||
let den: f64 = xs.iter().map(|x| (x - mx).powi(2)).sum();
|
||
let e = num / den;
|
||
println!(
|
||
" {scheme:?}: spike RMS ~ (dt)^{e:.2} across {} time steps",
|
||
points.len()
|
||
);
|
||
e
|
||
});
|
||
Verdict {
|
||
energy_per_flip: energy,
|
||
max_spike,
|
||
exponent,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn oscillating_plate_both_walls() {
|
||
let ladder = std::env::var("RTX_E3_FALSIFIER_LADDER").is_ok();
|
||
println!(
|
||
" body: {}",
|
||
if circle_body() {
|
||
"circle R 0.05"
|
||
} else if stadium_body() {
|
||
"stadium 0.35 x 0.02 (semicircular ends)"
|
||
} else {
|
||
"plate 0.35 x 0.02"
|
||
}
|
||
);
|
||
let ghost = falsify(WallScheme::GhostBinary, ladder);
|
||
let cut = falsify(WallScheme::CutCell, ladder);
|
||
println!(
|
||
" energy per flipped column: ghost {:.3e}, cut {:.3e} (ratio {:.1}x); max spike: ghost {:.3e}, cut {:.3e} N/m",
|
||
ghost.energy_per_flip,
|
||
cut.energy_per_flip,
|
||
ghost.energy_per_flip / cut.energy_per_flip.max(1e-300),
|
||
ghost.max_spike,
|
||
cut.max_spike
|
||
);
|
||
assert!(
|
||
ghost.energy_per_flip > 0.0,
|
||
"the binary wall must flip cells"
|
||
);
|
||
}
|
||
|
||
/// The registered gates on the dt ladder.
|
||
#[test]
|
||
#[ignore = "item 11's gated ladder (dt, dt/2, dt/4 on both walls; tens of minutes on the host)"]
|
||
fn oscillating_plate_gates() {
|
||
let ghost = falsify(WallScheme::GhostBinary, true);
|
||
let cut = falsify(WallScheme::CutCell, true);
|
||
let ratio = ghost.energy_per_flip / cut.energy_per_flip.max(1e-300);
|
||
println!(
|
||
" GATES: ghost energy per flipped column {:.3e} ({:.2} of 2D), exponent {:.2}; cut energy {:.3e} ({:.1}x lower), max spike {:.3e} N/m ({:.2e} of ½ρU²L), exponent {:.2}",
|
||
ghost.energy_per_flip,
|
||
ghost.energy_per_flip / ENERGY_2D,
|
||
ghost.exponent.unwrap(),
|
||
cut.energy_per_flip,
|
||
ratio,
|
||
cut.max_spike,
|
||
cut.max_spike / load_scale(),
|
||
cut.exponent.unwrap()
|
||
);
|
||
let g2d = ghost.energy_per_flip / ENERGY_2D;
|
||
assert!(
|
||
(0.7..=1.3).contains(&g2d),
|
||
"ghost energy per flip {g2d:.2} of 2D"
|
||
);
|
||
assert!(ratio >= 20.0, "cut energy only {ratio:.1}x lower");
|
||
assert!(
|
||
cut.max_spike < 0.05 * load_scale(),
|
||
"cut max spike {:.3e}",
|
||
cut.max_spike
|
||
);
|
||
let e = cut.exponent.unwrap();
|
||
assert!((-0.3..=0.3).contains(&e), "cut exponent {e:.2}");
|
||
}
|