use crate::fft::{fftn, wavenumber};
use crate::linalg::symmetric_eigen;
use metrology::{G, MU_0};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Grid {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
}
impl Grid {
pub fn points(&self) -> impl Iterator<Item = (f64, f64)> + '_ {
(0..self.nx).flat_map(move |i| (0..self.ny).map(move |j| (i as f64 * self.dx, j as f64 * self.dy)))
}
pub fn filter(&self, values: &[f64], kernel: impl Fn(f64, f64) -> (f64, f64)) -> Vec<f64> {
assert_eq!(values.len(), self.nx * self.ny, "one value per grid point");
let (mut re, mut im) = (values.to_vec(), vec![0.0; values.len()]);
fftn(&mut re, &mut im, &[self.nx, self.ny], false);
for i in 0..self.nx {
let kx = wavenumber(i, self.nx, self.dx);
for j in 0..self.ny {
let (a, b) = kernel(kx, wavenumber(j, self.ny, self.dy));
let k = i * self.ny + j;
(re[k], im[k]) = (re[k] * a - im[k] * b, re[k] * b + im[k] * a);
}
}
fftn(&mut re, &mut im, &[self.nx, self.ny], true);
re
}
pub fn padded(&self, values: &[f64], taper: bool) -> (Grid, Vec<f64>) {
assert_eq!(values.len(), self.nx * self.ny, "one value per grid point");
let big = Grid { nx: 2 * self.nx, ny: 2 * self.ny, ..*self };
let (px, py) = (self.nx / 2, self.ny / 2);
let window = |i: usize, n: usize, pad: usize| -> f64 {
if !taper || (pad..pad + n).contains(&i) {
1.0
} else {
let out = if i < pad { (pad - i) as f64 } else { (i + 1 - pad - n) as f64 };
0.5 + 0.5 * (core::f64::consts::PI * out / pad as f64).cos()
}
};
let out = (0..big.nx)
.flat_map(|i| {
(0..big.ny).map(move |j| {
let (si, sj) = (i.saturating_sub(px).min(self.nx - 1), j.saturating_sub(py).min(self.ny - 1));
values[si * self.ny + sj] * window(i, self.nx, px) * window(j, self.ny, py)
})
})
.collect();
(big, out)
}
pub fn unpadded(&self, values: &[f64]) -> Vec<f64> {
assert_eq!(values.len(), 4 * self.nx * self.ny, "values on the padded grid");
let (px, py) = (self.nx / 2, self.ny / 2);
(0..self.nx).flat_map(|i| (0..self.ny).map(move |j| values[(i + px) * 2 * self.ny + j + py])).collect()
}
pub fn upward_continue(&self, values: &[f64], h: f64) -> Vec<f64> {
self.filter(values, |kx, ky| ((-kx.hypot(ky) * h).exp(), 0.0))
}
pub fn reduce_to_pole(&self, values: &[f64], m: [f64; 3], f: [f64; 3]) -> Vec<f64> {
self.redirect(values, (m, f), POLE, 0.0)
}
pub fn reduce_to_pole_damped(&self, values: &[f64], m: [f64; 3], f: [f64; 3], epsilon: f64) -> Vec<f64> {
self.redirect(values, (m, f), POLE, epsilon)
}
pub fn reduce_to_equator(&self, values: &[f64], m: [f64; 3], f: [f64; 3]) -> Vec<f64> {
let flat = |u: [f64; 3]| {
let h = u[0].hypot(u[1]);
[u[0] / h, u[1] / h, 0.0]
};
self.redirect(values, (m, f), (flat(m), flat(f)), 0.0)
}
pub fn redirect(&self, values: &[f64], from: ([f64; 3], [f64; 3]), to: ([f64; 3], [f64; 3]), epsilon: f64) -> Vec<f64> {
self.filter(values, |kx, ky| direction_kernel(kx, ky, from, to, 1.0, epsilon))
}
pub fn pseudo_gravity(&self, values: &[f64], m: [f64; 3], f: [f64; 3], density_per_magnetisation: f64) -> Vec<f64> {
let scale = G * density_per_magnetisation * 4.0 * core::f64::consts::PI / MU_0;
self.filter(values, |kx, ky| direction_kernel(kx, ky, (m, f), POLE, scale / kx.hypot(ky), 0.0))
}
}
pub fn euler(x: &[f64], z: &[f64], f: &[f64], fx: &[f64], fz: &[f64], n: f64) -> [f64; 3] {
let len = x.len();
assert!([z.len(), f.len(), fx.len(), fz.len()].iter().all(|&l| l == len), "one value per point");
least_squares(len, |i| [fx[i], fz[i], n], |i| x[i] * fx[i] + z[i] * fz[i] + n * f[i]).0
}
pub fn euler3(points: &[[f64; 3]], f: &[f64], fx: &[f64], fy: &[f64], fz: &[f64], n: f64) -> ([f64; 4], [[f64; 4]; 4]) {
let len = points.len();
assert!([f.len(), fx.len(), fy.len(), fz.len()].iter().all(|&l| l == len), "one value per point");
least_squares(len, |i| [fx[i], fy[i], fz[i], n], |i| points[i][0] * fx[i] + points[i][1] * fy[i] + points[i][2] * fz[i] + n * f[i])
}
pub fn euler_windows(grid: &Grid, values: &[f64], derivatives: [&[f64]; 3], n: f64, size: usize, step: usize, keep: f64) -> Vec<[f64; 4]> {
let [fx, fy, fz] = derivatives;
let mut solutions = Vec::new();
for i0 in (0..=grid.nx - size).step_by(step.max(1)) {
for j0 in (0..=grid.ny - size).step_by(step.max(1)) {
let index: Vec<usize> = (i0..i0 + size).flat_map(|i| (j0..j0 + size).map(move |j| i * grid.ny + j)).collect();
let points: Vec<[f64; 3]> = index.iter().map(|&k| [(k / grid.ny) as f64 * grid.dx, (k % grid.ny) as f64 * grid.dy, 0.0]).collect();
let pick = |v: &[f64]| index.iter().map(|&k| v[k]).collect::<Vec<_>>();
let (p, cov) = euler3(&points, &pick(values), &pick(fx), &pick(fy), &pick(fz), n);
solutions.push((cov[2][2], p));
}
}
solutions.sort_by(|a, b| a.0.total_cmp(&b.0));
solutions.truncate(((solutions.len() as f64 * keep).round() as usize).max(1));
solutions.into_iter().map(|(_, p)| p).collect()
}
pub fn euler_inversion(points: &[[f64; 3]], f: &[f64], fx: &[f64], fy: &[f64], fz: &[f64], n: f64) -> ([f64; 4], f64) {
let len = points.len();
assert!([f.len(), fx.len(), fy.len(), fz.len()].iter().all(|&l| l == len), "one value per point");
const WEIGHTS: [f64; 4] = [1.0, 0.1, 0.1, 0.025];
const NU: f64 = 0.1;
let observed: Vec<[f64; 4]> = (0..len).map(|i| [f[i], fx[i], fy[i], fz[i]]).collect();
let mut predicted: Vec<[f64; 4]> = observed.iter().map(|d| d.map(|x| 0.9 * x)).collect();
let mut p = euler3(points, f, fx, fy, fz, n).0;
let euler = |d: &[f64; 4], q: &[f64; 4], r: &[f64; 3]| (r[0] - q[0]) * d[1] + (r[1] - q[1]) * d[2] + (r[2] - q[2]) * d[3] + n * (d[0] - q[3]);
let merit = |pred: &[[f64; 4]], q: &[f64; 4]| {
let misfit: f64 = observed.iter().zip(pred).map(|(o, d)| (0..4).map(|k| (WEIGHTS[k] * (o[k] - d[k])).powi(2)).sum::<f64>()).sum::<f64>().sqrt();
let residual: f64 = pred.iter().zip(points).map(|(d, r)| euler(d, q, r).powi(2)).sum::<f64>().sqrt();
misfit + NU * residual
};
let mut m0 = merit(&predicted, &p);
for _ in 0..20 {
let (mut normal, mut rhs) = ([[0.0; 4]; 4], [0.0; 4]);
let rows: Vec<([f64; 4], [f64; 4], f64, f64)> = predicted
.iter()
.zip(&observed)
.zip(points)
.map(|((d, o), r)| {
let a = [-d[1], -d[2], -d[3], -n];
let b = [n, r[0] - p[0], r[1] - p[1], r[2] - p[2]];
let q_inv = 1.0 / (0..4).map(|k| b[k] * b[k] / WEIGHTS[k]).sum::<f64>();
let br: f64 = (0..4).map(|k| b[k] * (o[k] - d[k])).sum();
(a, b, q_inv, euler(d, &p, r) + br)
})
.collect();
for (a, _, q_inv, e) in &rows {
for i in 0..4 {
rhs[i] -= a[i] * q_inv * e;
for j in 0..4 {
normal[i][j] += a[i] * q_inv * a[j];
}
}
}
let dp = pseudo_solve(&normal, &rhs);
let next: Vec<[f64; 4]> = rows
.iter()
.zip(&predicted)
.zip(&observed)
.map(|(((a, b, q_inv, e), _d), o)| {
let lambda = q_inv * (e + (0..4).map(|i| a[i] * dp[i]).sum::<f64>());
core::array::from_fn(|k| o[k] - b[k] * lambda / WEIGHTS[k])
})
.collect();
let q: [f64; 4] = core::array::from_fn(|i| p[i] + dp[i]);
let m1 = merit(&next, &q);
if m1 > m0 {
break;
}
(p, predicted) = (q, next);
if (m0 - m1).abs() / m0 < 0.1 {
break;
}
m0 = m1;
}
let wrmse = (observed.iter().zip(&predicted).map(|(o, d)| (0..4).map(|k| WEIGHTS[k] * (o[k] - d[k]).powi(2)).sum::<f64>()).sum::<f64>() / (4 * len) as f64).sqrt();
(p, wrmse)
}
fn least_squares<const K: usize>(len: usize, row: impl Fn(usize) -> [f64; K], rhs: impl Fn(usize) -> f64) -> ([f64; K], [[f64; K]; K]) {
let scale: [f64; K] = core::array::from_fn(|c| 1.0 / (0..len).map(|i| row(i)[c].powi(2)).sum::<f64>().sqrt());
let (mut normal, mut b) = ([[0.0; K]; K], [0.0; K]);
for i in 0..len {
let (a, r) = (row(i), rhs(i));
for p in 0..K {
b[p] += a[p] * scale[p] * r;
for q in 0..K {
normal[p][q] += a[p] * scale[p] * a[q] * scale[q];
}
}
}
let (inverse, undetermined, rank) = pseudo_inverse(&normal);
let y: [f64; K] = core::array::from_fn(|p| (0..K).map(|q| inverse[p][q] * b[q]).sum::<f64>() * scale[p]);
let residual: f64 = (0..len).map(|i| (rhs(i) - (0..K).map(|c| row(i)[c] * y[c]).sum::<f64>()).powi(2)).sum();
let sigma2 = if len > rank { residual / (len - rank) as f64 } else { 0.0 };
(y, core::array::from_fn(|p| core::array::from_fn(|q| if undetermined[p][q] { f64::INFINITY } else { sigma2 * inverse[p][q] * scale[p] * scale[q] })))
}
fn pseudo_inverse<const K: usize>(a: &[[f64; K]; K]) -> ([[f64; K]; K], [[bool; K]; K], usize) {
let flat: Vec<f64> = a.iter().flatten().copied().collect();
let (values, vectors) = symmetric_eigen(&flat, K);
let cut = 1e-12 * values.iter().fold(0f64, |m, v| m.max(v.abs()));
let rank = values.iter().filter(|&&v| v > cut).count();
let inv = core::array::from_fn(|p| core::array::from_fn(|q| (0..K).filter(|&k| values[k] > cut).map(|k| vectors[p * K + k] * vectors[q * K + k] / values[k]).sum::<f64>()));
let null = core::array::from_fn(|p| core::array::from_fn(|q| (0..K).any(|k| values[k] <= cut && (vectors[p * K + k] * vectors[q * K + k]).abs() > 1e-12)));
(inv, null, rank)
}
fn pseudo_solve<const K: usize>(a: &[[f64; K]; K], b: &[f64; K]) -> [f64; K] {
let (inverse, _, _) = pseudo_inverse(a);
core::array::from_fn(|p| (0..K).map(|q| inverse[p][q] * b[q]).sum())
}
const POLE: ([f64; 3], [f64; 3]) = ([0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
fn direction_kernel(kx: f64, ky: f64, from: ([f64; 3], [f64; 3]), to: ([f64; 3], [f64; 3]), scale: f64, epsilon: f64) -> (f64, f64) {
let k = kx.hypot(ky);
if k == 0.0 {
return (0.0, 0.0);
}
let theta = |u: [f64; 3]| (u[2], (u[0] * kx + u[1] * ky) / k);
let product = |(a, b): (f64, f64), (c, d): (f64, f64)| (a * c - b * d, a * d + b * c);
let (re, im) = product(theta(from.0), theta(from.1));
let (nr, ni) = product(theta(to.0), theta(to.1));
let norm = scale / (re * re + im * im + epsilon);
product((nr, ni), (re * norm, -im * norm))
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
const GRID: Grid = Grid { nx: 128, ny: 128, dx: 10.0, dy: 10.0 };
fn dipole(depth: f64, m: [f64; 3], f: [f64; 3]) -> Vec<f64> {
GRID.points()
.map(|(x, y)| {
let d = [x - 640.0, y - 640.0, -depth];
let r2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
let (dm, df, mf) = ((0..3).map(|i| d[i] * m[i]).sum::<f64>(), (0..3).map(|i| d[i] * f[i]).sum::<f64>(), (0..3).map(|i| m[i] * f[i]).sum::<f64>());
(3.0 * dm * df - r2 * mf) / (r2 * r2 * r2.sqrt())
})
.collect()
}
fn rms_relative(a: &[f64], b: &[f64]) -> f64 {
(a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum::<f64>() / b.iter().map(|y| y * y).sum::<f64>()).sqrt()
}
fn unit(inclination: f64, declination: f64) -> [f64; 3] {
let (i, d) = (inclination.to_radians(), declination.to_radians());
[i.cos() * d.cos(), i.cos() * d.sin(), i.sin()]
}
#[test]
fn upward_continuation() {
let pole = [0.0, 0.0, 1.0];
let up = GRID.upward_continue(&dipole(50.0, pole, pole), 30.0);
let err = rms_relative(&up, &dipole(80.0, pole, pole));
assert!((err / 6.9e-4 - 1.0).abs() < 0.02, "{err:e}");
let field = dipole(50.0, pole, pole);
for (h, tol) in [(5.0, 1e-14), (30.0, 2e-11)] {
let err = rms_relative(&GRID.upward_continue(&GRID.upward_continue(&field, h), -h), &field);
assert!(err < tol, "{h} m: {err:e}");
}
}
#[test]
fn padding() {
let pole = [0.0, 0.0, 1.0];
let continued = |depth: f64, pad: bool| {
let field = dipole(depth, pole, pole);
let up = if pad {
let (big, values) = GRID.padded(&field, true);
GRID.unpadded(&big.upward_continue(&values, 30.0))
} else {
GRID.upward_continue(&field, 30.0)
};
rms_relative(&up, &dipole(depth + 30.0, pole, pole))
};
assert!((continued(300.0, false) / 1.8e-2 - 1.0).abs() < 0.1 && (continued(300.0, true) / 1.1e-3 - 1.0).abs() < 0.1, "{} {}", continued(300.0, false), continued(300.0, true));
assert!((continued(50.0, true) / 2.5e-4 - 1.0).abs() < 0.1);
let field = dipole(300.0, pole, pole);
let (big, values) = GRID.padded(&field, false);
assert_eq!((big.nx, big.ny, values.len()), (256, 256, 65536));
assert_eq!(values[0], field[0]);
assert_eq!(values[64 * 256 + 300 - 64], field[127]);
assert!(GRID.unpadded(&values) == field && GRID.unpadded(&GRID.padded(&field, true).1) == field);
let tapered = GRID.padded(&field, true).1;
assert!(tapered[0].abs() < 1e-12 * field[64 * 128 + 64] && tapered[64 * 256 + 64] == field[0]);
}
#[test]
fn low_inclination() {
let grid = Grid { nx: 64, ny: 64, dx: 25.0, dy: 25.0 };
let field = |m: [f64; 3], f: [f64; 3]| -> Vec<f64> {
grid.points()
.map(|(x, y)| {
let d = [x - 800.0, y - 800.0, -100.0];
let r2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
let (dm, df, mf) = ((0..3).map(|i| d[i] * m[i]).sum::<f64>(), (0..3).map(|i| d[i] * f[i]).sum::<f64>(), (0..3).map(|i| m[i] * f[i]).sum::<f64>());
(3.0 * dm * df - r2 * mf) / (r2 * r2 * r2.sqrt())
})
.collect()
};
let apply = |values: &[f64], f: &dyn Fn(&Grid, &[f64]) -> Vec<f64>| {
let (big, padded) = grid.padded(values, true);
grid.unpadded(&f(&big, &padded))
};
let (low, equator, pole) = (unit(10.0, 0.0), unit(0.0, 0.0), [0.0, 0.0, 1.0]);
let (data, at_equator, at_pole) = (field(low, low), field(equator, equator), field(pole, pole));
let rte = apply(&data, &|g, v| g.reduce_to_equator(v, low, low));
assert!(rms_relative(&rte, &at_equator) < 0.01, "{}", rms_relative(&rte, &at_equator));
let back = grid.redirect(&grid.reduce_to_pole(&data, low, low), POLE, (low, low), 0.0);
let mean = data.iter().sum::<f64>() / data.len() as f64;
let round_trip = rms_relative(&back.iter().map(|v| v + mean).collect::<Vec<_>>(), &data);
assert!(round_trip < 1e-4, "{round_trip:e}");
let mut state = 7u64;
let peak = data.iter().fold(0f64, |m, v| m.max(v.abs()));
let noisy: Vec<f64> = data
.iter()
.map(|v| {
let g: f64 = (0..12)
.map(|_| {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(state >> 11) as f64 / (1u64 << 53) as f64
})
.sum::<f64>()
- 6.0;
v + 0.01 * peak * g
})
.collect();
let plain = rms_relative(&apply(&noisy, &|g, v| g.reduce_to_pole(v, low, low)), &at_pole);
let damped = rms_relative(&apply(&noisy, &|g, v| g.reduce_to_pole_damped(v, low, low, 0.025)), &at_pole);
assert!(plain > 0.5 && damped < 0.6 && damped < 0.6 * plain, "plain {plain} damped {damped}");
let (tiny, exact) = (grid.reduce_to_pole_damped(&data, low, low, 1e-18), grid.reduce_to_pole(&data, low, low));
assert!(rms_relative(&tiny, &exact) < 1e-12);
}
#[test]
fn reduction_to_the_pole() {
let (tilted, pole) = (unit(45.0, 0.0), [0.0, 0.0, 1.0]);
let reduced = GRID.reduce_to_pole(&dipole(50.0, tilted, tilted), tilted, tilted);
let err = rms_relative(&reduced, &dipole(50.0, pole, pole));
assert!((err / 0.011 - 1.0).abs() < 0.1, "{err}");
let flipped = [-tilted[0], -tilted[1], tilted[2]];
assert!(rms_relative(&GRID.reduce_to_pole(&dipole(50.0, tilted, tilted), flipped, flipped), &dipole(50.0, pole, pole)) > 1.0);
let field = dipole(50.0, pole, pole);
let mean = field.iter().sum::<f64>() / field.len() as f64;
let same = GRID.reduce_to_pole(&field, pole, pole);
assert!(same.iter().zip(&field).all(|(a, b)| (a - (b - mean)).abs() < 1e-14 * field[64 * 128 + 64]));
}
fn sampled(field: impl Fn(f64, f64) -> f64, xs: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let h = 1e-2;
let d = |g: &dyn Fn(f64) -> f64| (g(-2.0 * h) - 8.0 * g(-h) + 8.0 * g(h) - g(2.0 * h)) / (12.0 * h);
let f = xs.iter().map(|&x| field(x, 0.0)).collect();
let fx = xs.iter().map(|&x| d(&|e| field(x + e, 0.0))).collect();
let fz = xs.iter().map(|&x| d(&|e| field(x, e))).collect();
(f, fx, fz)
}
#[test]
fn euler_deconvolution() {
let (x0, z0) = (3.0, 20.0);
let tilted = unit(60.0, 0.0);
let dipole = move |x: f64, z: f64| {
let d = [x - x0, 0.0, z - z0];
let r2 = d[0] * d[0] + d[2] * d[2];
let dm = d[0] * tilted[0] + d[2] * tilted[2];
(3.0 * dm * dm - r2) / (r2 * r2 * r2.sqrt())
};
let line_dipole = move |x: f64, z: f64| {
let (dx, dz) = (x - x0, z - z0);
let r2 = dx * dx + dz * dz;
((dz * dz - dx * dx) * (tilted[2].powi(2) - tilted[0].powi(2)) + 4.0 * dx * dz * tilted[0] * tilted[2]) / (r2 * r2)
};
let point_mass = move |x: f64, z: f64| (z0 - z) / ((x - x0).powi(2) + (z0 - z).powi(2)).powf(1.5);
let line_mass = move |x: f64, z: f64| (z0 - z) / ((x - x0).powi(2) + (z0 - z).powi(2));
let xs: Vec<f64> = (0..41).map(|i| 2.0 * i as f64 - 40.0).collect();
let zs = vec![0.0; xs.len()];
type Profile<'a> = &'a dyn Fn(f64, f64) -> f64;
let cases: [(Profile, f64); 4] = [(&dipole, 3.0), (&line_dipole, 2.0), (&point_mass, 2.0), (&line_mass, 1.0)];
for (k, (field, n)) in cases.into_iter().enumerate() {
let (f, fx, fz) = sampled(field, &xs);
let background = 0.1 * f.iter().fold(0f64, |m, v| m.max(v.abs()));
let f: Vec<f64> = f.iter().map(|v| v + background).collect();
let [x, z, b] = euler(&xs, &zs, &f, &fx, &fz, n);
assert!((x - x0).abs() < 1e-10 && (z - z0).abs() < 1e-10, "case {k}: {x} {z}");
assert!((b / background - 1.0).abs() < 1e-11, "case {k}: background {b:e}");
}
let (f, fx, fz) = sampled(dipole, &xs);
assert!(euler(&xs, &zs, &f, &fx, &fz, 2.0)[1] < 0.9 * z0);
}
#[test]
fn euler_in_three_dimensions() {
let (x0, y0, z0) = (10e3, 8e3, 2e3);
let tilted = unit(60.0, 0.0);
let dipole = move |r: [f64; 3]| {
let d = [r[0] - x0, r[1] - y0, r[2] - z0];
let r2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
let dm: f64 = (0..3).map(|i| d[i] * tilted[i]).sum();
1e11 * (3.0 * dm * dm - r2) / (r2 * r2 * r2.sqrt())
};
let point_mass = move |r: [f64; 3]| 1e13 * (z0 - r[2]) / ((r[0] - x0).powi(2) + (r[1] - y0).powi(2) + (z0 - r[2]).powi(2)).powf(1.5);
let (ca, sa) = (30f64.to_radians().cos(), 30f64.to_radians().sin());
let line_mass = move |r: [f64; 3]| 1e9 * (z0 - r[2]) / (((r[0] - x0) * ca - (r[1] - y0) * sa).powi(2) + (z0 - r[2]).powi(2));
let points: Vec<[f64; 3]> = (0..81).flat_map(|i| (0..65).map(move |j| [i as f64 * 250.0, j as f64 * 250.0, -500.0])).collect();
type Source<'a> = &'a dyn Fn([f64; 3]) -> f64;
let cases: [(Source, f64, bool); 3] = [(&dipole, 3.0, false), (&point_mass, 2.0, false), (&line_mass, 1.0, true)];
for (field, n, two_d) in cases {
let sample = |shift: [f64; 3]| -> Vec<f64> { points.iter().map(|r| field([r[0] + shift[0], r[1] + shift[1], r[2] + shift[2]]) + 50.0).collect() };
let f = sample([0.0; 3]);
let d = |axis: usize| -> Vec<f64> {
let (mut up, mut down) = ([0.0; 3], [0.0; 3]);
(up[axis], down[axis]) = (1.0, -1.0);
sample(up).iter().zip(sample(down)).map(|(a, b)| (a - b) / 2.0).collect()
};
let (fx, fy, fz) = (d(0), d(1), d(2));
let (p, cov) = euler3(&points, &f, &fx, &fy, &fz, n);
let across = if two_d { ((p[0] - x0) * ca - (p[1] - y0) * sa).abs() } else { (p[0] - x0).hypot(p[1] - y0) };
assert!(across < 2e-3 && (p[2] - z0).abs() < 2e-3 && (p[3] - 50.0).abs() < 1e-2, "N = {n}: {p:?}");
assert!(cov[2][2].is_finite() && cov[2][2] >= 0.0);
if two_d {
let along = (p[0] - x0) * sa + (p[1] - y0) * ca;
assert!(along.abs() > 100.0 && (cov[0][0].is_infinite() || cov[1][1].is_infinite()), "{p:?} {cov:?}");
}
let fits = [1.0, 2.0, 3.0].map(|si| euler_inversion(&points, &f, &fx, &fy, &fz, si).1);
let best = (0..3).min_by(|&a, &b| fits[a].total_cmp(&fits[b])).unwrap();
assert!((best + 1) as f64 == n, "N = {n}: misfits {fits:?}");
let (q, _) = euler_inversion(&points, &f, &fx, &fy, &fz, n);
assert!((q[2] - z0).abs() < 1.0, "inversion at N = {n}: {q:?}");
}
let grid = Grid { nx: 128, ny: 64, dx: 250.0, dy: 250.0 };
let field: Vec<f64> = grid.points().map(|(x, y)| dipole([x, y, 0.0])).collect();
let (big, padded) = grid.padded(&field, true);
let spectral = |kernel: &dyn Fn(f64, f64) -> (f64, f64)| grid.unpadded(&big.filter(&padded, kernel));
let (fx, fy, fz) = (spectral(&|kx, _| (0.0, kx)), spectral(&|_, ky| (0.0, ky)), spectral(&|kx, ky| (kx.hypot(ky), 0.0)));
let kept = euler_windows(&grid, &field, [&fx, &fy, &fz], 3.0, 12, 4, 0.1);
let best = kept[0];
assert!((best[2] / z0 - 1.0).abs() < 0.02 && (best[0] - x0).hypot(best[1] - y0) < 100.0, "{best:?}");
assert!(kept.len() >= 10 && kept.iter().all(|p| p[2] > 0.0));
}
#[test]
fn pseudo_gravity_is_poisson() {
let (a, depth, rho) = (10.0f64, 50.0, 2700.0);
let volume = 4.0 / 3.0 * PI * a.powi(3);
let pole = [0.0, 0.0, 1.0];
let anomaly: Vec<f64> = dipole(depth, pole, pole).iter().map(|t| MU_0 / (4.0 * PI) * volume * t).collect();
let gz: Vec<f64> = GRID.points().map(|(x, y)| G * rho * volume * depth / ((x - 640.0).powi(2) + (y - 640.0).powi(2) + depth * depth).powf(1.5)).collect();
let pseudo = GRID.pseudo_gravity(&anomaly, pole, pole, rho);
let centre = 64 * 128 + 64;
let err = (pseudo[centre] - pseudo[0]) / (gz[centre] - gz[0]) - 1.0;
assert!((err / -1.3e-3 - 1.0).abs() < 0.05, "{err:e}");
}
}