use std::fs;
use std::path::Path;
fn frame_of(plane: &serde_json::Value, key: &str) -> Result<Vec<f64>, String> {
plane
.get(key)
.and_then(|v| v.as_array())
.ok_or_else(|| format!("plane is missing the `{key}` frame axis"))?
.iter()
.map(|v| {
v.as_f64()
.ok_or_else(|| format!("`{key}` holds a non-numeric entry"))
})
.collect()
}
fn principal_angles(a1: &[f64], a2: &[f64], b1: &[f64], b2: &[f64]) -> (f64, f64) {
let dot = |x: &[f64], y: &[f64]| x.iter().zip(y.iter()).map(|(p, q)| p * q).sum::<f64>();
let m = [dot(a1, b1), dot(a1, b2), dot(a2, b1), dot(a2, b2)];
let t = m.iter().map(|v| v * v).sum::<f64>();
let d = m[0] * m[3] - m[1] * m[2];
let disc = (t * t - 4.0 * d * d).max(0.0).sqrt();
let s_max = ((t + disc) * 0.5).max(0.0).sqrt().min(1.0);
let s_min = ((t - disc) * 0.5).max(0.0).sqrt().min(1.0);
(s_min.acos(), s_max.acos())
}
struct Normals(u64);
impl Normals {
fn bits(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn unit(&mut self) -> f64 {
(self.bits() >> 11) as f64 / (1u64 << 53) as f64
}
fn normal(&mut self) -> f64 {
let u1 = self.unit().max(1e-300);
let u2 = self.unit();
(-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
}
fn frame(&mut self, p: usize) -> (Vec<f64>, Vec<f64>) {
let mut u: Vec<f64> = (0..p).map(|_| self.normal()).collect();
let nu = u.iter().map(|v| v * v).sum::<f64>().sqrt();
u.iter_mut().for_each(|v| *v /= nu);
let mut w: Vec<f64> = (0..p).map(|_| self.normal()).collect();
let proj: f64 = w.iter().zip(u.iter()).map(|(a, b)| a * b).sum();
for (wi, ui) in w.iter_mut().zip(u.iter()) {
*wi -= proj * ui;
}
let nw = w.iter().map(|v| v * v).sum::<f64>().sqrt();
w.iter_mut().for_each(|v| *v /= nw);
(u, w)
}
}
fn planes(path: &Path) -> Result<Vec<serde_json::Value>, String> {
let text = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
let doc: serde_json::Value =
serde_json::from_str(&text).map_err(|e| format!("parse {}: {e}", path.display()))?;
Ok(doc
.get("planes")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
fn run() -> Result<(), String> {
let args: Vec<String> = std::env::args().collect();
if args.len() != 5 {
return Err("usage: plane_replication <a.planes.json> <b.planes.json> <null_draws> <out.json>".to_string());
}
let a = planes(Path::new(&args[1]))?;
let b = planes(Path::new(&args[2]))?;
let draws: usize = args[3]
.parse()
.map_err(|e| format!("null_draws must be a positive integer: {e}"))?;
if a.is_empty() || b.is_empty() {
return Err(format!(
"nothing to match: {} planes on the left, {} on the right",
a.len(),
b.len()
));
}
let mut rows: Vec<serde_json::Value> = Vec::new();
let mut best_angles: Vec<f64> = Vec::new();
let mut ambient = 0usize;
for pa in &a {
let a1 = frame_of(pa, "e1")?;
let a2 = frame_of(pa, "e2")?;
ambient = a1.len();
let mut best = (std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2, 0usize);
for (j, pb) in b.iter().enumerate() {
let b1 = frame_of(pb, "e1")?;
let b2 = frame_of(pb, "e2")?;
if b1.len() != a1.len() {
return Err("the two censuses have different ambient dimensions".to_string());
}
let (theta_max, theta_min) = principal_angles(&a1, &a2, &b1, &b2);
if theta_max < best.0 {
best = (theta_max, theta_min, j);
}
}
best_angles.push(best.0);
rows.push(serde_json::json!({
"left_members": [pa.get("members_a"), pa.get("members_b")],
"right_members": [b[best.2].get("members_a"), b[best.2].get("members_b")],
"principal_angle_deg_max": best.0.to_degrees(),
"principal_angle_deg_min": best.1.to_degrees(),
"left_kappa": pa.get("kappa"),
"right_kappa": b[best.2].get("kappa"),
}));
}
let mut rng = Normals(0x2502_C13C_1E05_u64 | 1);
let mut null_best: Vec<f64> = Vec::with_capacity(draws);
let right: Vec<(Vec<f64>, Vec<f64>)> = b
.iter()
.map(|pb| Ok((frame_of(pb, "e1")?, frame_of(pb, "e2")?)))
.collect::<Result<_, String>>()?;
for _draw in 0..draws {
let (r1, r2) = rng.frame(ambient);
let mut best = std::f64::consts::FRAC_PI_2;
for (b1, b2) in &right {
let (theta_max, _theta_min) = principal_angles(&r1, &r2, b1, b2);
if theta_max < best {
best = theta_max;
}
}
null_best.push(best);
}
null_best.sort_by(f64::total_cmp);
let null_p05 = null_best[((0.05 * draws as f64) as usize).min(draws.saturating_sub(1))];
let matched = best_angles.iter().filter(|&&t| t < null_p05).count();
let doc = serde_json::json!({
"ambient_p": ambient,
"left_planes": a.len(),
"right_planes": b.len(),
"null_draws": draws,
"null_best_angle_deg_p05": null_p05.to_degrees(),
"null_best_angle_deg_median": null_best[draws / 2].to_degrees(),
"matched_below_null_p05": matched,
"matches": rows,
});
fs::write(
Path::new(&args[4]),
serde_json::to_string(&doc).map_err(|e| format!("serialise: {e}"))?,
)
.map_err(|e| format!("write {}: {e}", args[4]))?;
eprintln!(
"[replication] {} of {} left planes match a right plane below the random-plane \
5% point ({:.1}°); random best-of-{} median {:.1}°",
matched,
a.len(),
null_p05.to_degrees(),
b.len(),
null_best[draws / 2].to_degrees()
);
Ok(())
}
fn main() -> Result<(), String> {
run().map_err(|error| format!("plane_replication: {error}"))
}