#![forbid(unsafe_code)]
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use fsci_conformance::{ArmCounts, CompareLedger};
use fsci_spatial::KDTree;
use serde::{Deserialize, Serialize};
const PACKET_ID: &str = "FSCI-P2C-012";
const DIST_TOL: f64 = 1.0e-10;
const REQUIRE_SCIPY_ENV: &str = "FSCI_REQUIRE_SCIPY_ORACLE";
#[derive(Debug, Clone, Serialize)]
struct QueryCase {
case_id: String,
points: Vec<Vec<f64>>,
query: Vec<f64>,
}
#[derive(Debug, Clone, Serialize)]
struct QueryKCase {
case_id: String,
points: Vec<Vec<f64>>,
query: Vec<f64>,
k: usize,
}
#[derive(Debug, Clone, Serialize)]
struct QueryBallPointCase {
case_id: String,
points: Vec<Vec<f64>>,
query: Vec<f64>,
r: f64,
}
#[derive(Debug, Clone, Deserialize)]
struct QueryOracleResult {
case_id: String,
index: usize,
distance: f64,
}
#[derive(Debug, Clone, Deserialize)]
struct QueryKOracleResult {
case_id: String,
indices: Vec<usize>,
distances: Vec<f64>,
}
#[derive(Debug, Clone, Deserialize)]
struct QueryBallPointOracleResult {
case_id: String,
indices: Vec<usize>,
}
#[derive(Debug, Clone, Serialize)]
struct QueryDiff {
case_id: String,
dim: usize,
n_points: usize,
rust_index: usize,
rust_distance: f64,
scipy_index: usize,
scipy_distance: f64,
index_match: bool,
dist_diff: f64,
tolerance: f64,
pass: bool,
}
#[derive(Debug, Clone, Serialize)]
struct QueryKDiff {
case_id: String,
dim: usize,
n_points: usize,
k: usize,
rust_indices: Vec<usize>,
rust_distances: Vec<f64>,
scipy_indices: Vec<usize>,
scipy_distances: Vec<f64>,
indices_match: bool,
max_dist_diff: f64,
tolerance: f64,
pass: bool,
}
#[derive(Debug, Clone, Serialize)]
struct QueryBallPointDiff {
case_id: String,
dim: usize,
n_points: usize,
radius: f64,
rust_indices: Vec<usize>,
scipy_indices: Vec<usize>,
indices_match: bool,
pass: bool,
}
#[derive(Debug, Clone, Serialize)]
struct DiffLog {
test_id: String,
category: String,
case_count: usize,
compared: BTreeMap<String, ArmCounts>,
pass_count: usize,
tolerance: f64,
pass: bool,
timestamp_ms: u128,
duration_ns: u128,
}
fn output_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("fixtures/artifacts/{PACKET_ID}/diff"))
}
fn ensure_output_dir() {
fs::create_dir_all(output_dir()).expect("create kdtree diff output dir");
}
fn timestamp_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_millis())
}
fn emit_log(log: &DiffLog, extra: &str) {
ensure_output_dir();
let path = output_dir().join(format!("{}_{}.json", log.test_id, extra));
let json = serde_json::to_string_pretty(log).expect("serialize kdtree diff log");
fs::write(path, json).expect("write kdtree diff log");
}
fn deterministic_point(dim: usize, seed: usize) -> Vec<f64> {
(0..dim)
.map(|idx| {
let base = ((idx + seed) % 7) as f64 * 0.5 - 1.5;
let wave = (((idx * 3 + seed * 2) % 11) as f64 * 0.2) - 1.0;
base + wave + (seed % 5) as f64 * 0.3
})
.collect()
}
fn deterministic_point_cloud(dim: usize, n_points: usize, base_seed: usize) -> Vec<Vec<f64>> {
(0..n_points)
.map(|i| deterministic_point(dim, base_seed + i * 7))
.collect()
}
fn query_cases() -> Vec<QueryCase> {
let configs = [
(2, 10, 100),
(2, 50, 200),
(3, 20, 300),
(3, 100, 400),
(5, 30, 500),
(8, 25, 600),
];
let mut cases = Vec::new();
for (dim, n_points, base_seed) in configs {
let points = deterministic_point_cloud(dim, n_points, base_seed);
for query_seed_offset in 0..5 {
let query = deterministic_point(dim, base_seed + 1000 + query_seed_offset * 13);
cases.push(QueryCase {
case_id: format!("query_dim{dim}_n{n_points}_q{query_seed_offset}"),
points: points.clone(),
query,
});
}
if n_points > 0 {
let exact_idx = n_points / 2;
cases.push(QueryCase {
case_id: format!("query_exact_dim{dim}_n{n_points}"),
points: points.clone(),
query: points[exact_idx].clone(),
});
}
}
cases
}
fn query_k_cases() -> Vec<QueryKCase> {
let configs = [(2, 20, 100), (3, 30, 200), (3, 50, 300), (5, 40, 400)];
let k_values = [1, 3, 5, 10];
let mut cases = Vec::new();
for (dim, n_points, base_seed) in configs {
let points = deterministic_point_cloud(dim, n_points, base_seed);
for &k in &k_values {
if k > n_points {
continue;
}
for query_seed_offset in 0..3 {
let query = deterministic_point(dim, base_seed + 2000 + query_seed_offset * 17);
cases.push(QueryKCase {
case_id: format!("queryk_dim{dim}_n{n_points}_k{k}_q{query_seed_offset}"),
points: points.clone(),
query,
k,
});
}
}
}
cases
}
fn query_ball_point_cases() -> Vec<QueryBallPointCase> {
let configs = [(2, 30, 100), (3, 25, 200), (3, 50, 300), (5, 20, 400)];
let radii = [0.5, 1.0, 2.0, 5.0];
let mut cases = Vec::new();
for (dim, n_points, base_seed) in configs {
let points = deterministic_point_cloud(dim, n_points, base_seed);
for &r in &radii {
for query_seed_offset in 0..3 {
let query = deterministic_point(dim, base_seed + 3000 + query_seed_offset * 19);
cases.push(QueryBallPointCase {
case_id: format!("ballpoint_dim{dim}_n{n_points}_r{r:.1}_q{query_seed_offset}"),
points: points.clone(),
query,
r,
});
}
}
}
cases
}
fn rust_query(case: &QueryCase) -> Option<(usize, f64)> {
let tree = KDTree::new(&case.points).ok()?;
tree.query(&case.query).ok()
}
fn rust_query_k(case: &QueryKCase) -> Option<Vec<(usize, f64)>> {
let tree = KDTree::new(&case.points).ok()?;
tree.query_k(&case.query, case.k).ok()
}
fn rust_query_ball_point(case: &QueryBallPointCase) -> Option<Vec<usize>> {
let tree = KDTree::new(&case.points).ok()?;
tree.query_ball_point(&case.query, case.r).ok()
}
fn run_scipy_query_oracle(cases: &[QueryCase]) -> Option<Vec<QueryOracleResult>> {
let script = r#"
import json
import sys
import numpy as np
from scipy.spatial import KDTree
cases = json.load(sys.stdin)
results = []
for c in cases:
cid = c["case_id"]
points = np.array(c["points"], dtype=np.float64)
query = np.array(c["query"], dtype=np.float64)
try:
tree = KDTree(points)
distance, index = tree.query(query)
results.append({
"case_id": cid,
"index": int(index),
"distance": float(distance)
})
except Exception as e:
pass
json.dump(results, sys.stdout)
"#;
run_python_oracle(script, cases)
}
fn run_scipy_query_k_oracle(cases: &[QueryKCase]) -> Option<Vec<QueryKOracleResult>> {
let script = r#"
import json
import sys
import numpy as np
from scipy.spatial import KDTree
cases = json.load(sys.stdin)
results = []
for c in cases:
cid = c["case_id"]
points = np.array(c["points"], dtype=np.float64)
query = np.array(c["query"], dtype=np.float64)
k = c["k"]
try:
tree = KDTree(points)
distances, indices = tree.query(query, k=k)
if k == 1:
distances = [distances]
indices = [indices]
results.append({
"case_id": cid,
"indices": [int(i) for i in indices],
"distances": [float(d) for d in distances]
})
except Exception as e:
pass
json.dump(results, sys.stdout)
"#;
run_python_oracle(script, cases)
}
fn run_scipy_query_ball_point_oracle(
cases: &[QueryBallPointCase],
) -> Option<Vec<QueryBallPointOracleResult>> {
let script = r#"
import json
import sys
import numpy as np
from scipy.spatial import KDTree
cases = json.load(sys.stdin)
results = []
for c in cases:
cid = c["case_id"]
points = np.array(c["points"], dtype=np.float64)
query = np.array(c["query"], dtype=np.float64)
r = c["r"]
try:
tree = KDTree(points)
indices = tree.query_ball_point(query, r)
results.append({
"case_id": cid,
"indices": sorted([int(i) for i in indices])
})
except Exception as e:
pass
json.dump(results, sys.stdout)
"#;
run_python_oracle(script, cases)
}
fn run_python_oracle<T: Serialize, R: for<'de> Deserialize<'de>>(
script: &str,
cases: &[T],
) -> Option<Vec<R>> {
let mut child = fsci_conformance::scipy_oracle_command()
.args(["-c", script])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
{
let stdin = child.stdin.as_mut()?;
let json_input = serde_json::to_string(cases).ok()?;
stdin.write_all(json_input.as_bytes()).ok()?;
}
let output = child.wait_with_output().ok()?;
if !output.status.success() {
return None;
}
serde_json::from_slice(&output.stdout).ok()
}
fn scipy_oracle_or_skip<T, R>(run_fn: impl FnOnce(&[T]) -> Option<Vec<R>>, cases: &[T]) -> Vec<R> {
match run_fn(cases) {
Some(results) => results,
None => {
assert!(
std::env::var(REQUIRE_SCIPY_ENV).is_err(),
"SciPy oracle required but not available"
);
eprintln!("SciPy oracle not available, skipping diff test");
Vec::new()
}
}
}
fn assert_complete_oracle_results<T>(
test_id: &str,
expected_case_ids: impl IntoIterator<Item = String>,
oracle_count: usize,
oracle_results: &HashMap<String, T>,
) {
let expected_case_ids: Vec<String> = expected_case_ids.into_iter().collect();
assert_eq!(
oracle_count,
expected_case_ids.len(),
"{test_id} SciPy KDTree oracle returned partial or duplicate coverage"
);
assert_eq!(
oracle_results.len(),
expected_case_ids.len(),
"{test_id} SciPy KDTree oracle case ids were incomplete or duplicated"
);
let missing_oracle_cases: Vec<&str> = expected_case_ids
.iter()
.filter(|case_id| !oracle_results.contains_key(case_id.as_str()))
.map(String::as_str)
.collect();
assert!(
missing_oracle_cases.is_empty(),
"{test_id} missing SciPy KDTree oracle results: {:?}",
missing_oracle_cases
);
let unexpected_oracle_cases: Vec<&str> = oracle_results
.keys()
.map(String::as_str)
.filter(|case_id| !expected_case_ids.iter().any(|expected| expected == case_id))
.collect();
assert!(
unexpected_oracle_cases.is_empty(),
"{test_id} unexpected SciPy KDTree oracle results: {:?}",
unexpected_oracle_cases
);
}
fn assert_all_cases_compared(test_id: &str, compared: usize, expected: usize) {
assert_eq!(
compared, expected,
"{test_id} compared {compared}/{expected} expected KDTree cases"
);
}
fn max_distance_diff(a: &[f64], b: &[f64]) -> f64 {
if a.len() != b.len() {
return f64::INFINITY;
}
a.iter()
.zip(b.iter())
.map(|(left, right)| (left - right).abs())
.fold(0.0_f64, |acc, diff| {
if diff.is_nan() {
f64::NAN
} else {
acc.max(diff)
}
})
}
#[test]
fn diff_001_kdtree_query() {
let start = Instant::now();
let cases = query_cases();
let oracle_results = scipy_oracle_or_skip(run_scipy_query_oracle, &cases);
if oracle_results.is_empty() {
return;
}
let oracle_map: HashMap<String, &QueryOracleResult> = oracle_results
.iter()
.map(|r| (r.case_id.clone(), r))
.collect();
assert_complete_oracle_results(
"diff_001_kdtree_query",
cases.iter().map(|case| case.case_id.clone()),
oracle_results.len(),
&oracle_map,
);
let mut diffs = Vec::new();
let mut pass_count = 0;
let mut ledger = CompareLedger::new("diff_001_kdtree_query", &["query"]);
for case in &cases {
let Some((scipy_result, rust_result)) = ledger.both(
"query",
&case.case_id,
oracle_map.get(&case.case_id).copied(),
rust_query(case),
) else {
continue;
};
let index_match = rust_result.0 == scipy_result.index;
let dist_diff = (rust_result.1 - scipy_result.distance).abs();
let pass = dist_diff <= DIST_TOL;
ledger.compared("query", &case.case_id, pass);
if pass {
pass_count += 1;
}
diffs.push(QueryDiff {
case_id: case.case_id.clone(),
dim: case.query.len(),
n_points: case.points.len(),
rust_index: rust_result.0,
rust_distance: rust_result.1,
scipy_index: scipy_result.index,
scipy_distance: scipy_result.distance,
index_match,
dist_diff,
tolerance: DIST_TOL,
pass,
});
}
let all_pass = diffs.len() == cases.len() && diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_001_kdtree_query".into(),
category: "scipy.spatial.KDTree.query".into(),
case_count: diffs.len(),
compared: ledger.counts().clone(),
pass_count,
tolerance: DIST_TOL,
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
};
emit_log(&log, "summary");
assert_all_cases_compared("diff_001_kdtree_query", log.case_count, cases.len());
for diff in &diffs {
assert!(
diff.pass,
"{} mismatch: rust=({}, {}) scipy=({}, {}) index_match={} dist_diff={}",
diff.case_id,
diff.rust_index,
diff.rust_distance,
diff.scipy_index,
diff.scipy_distance,
diff.index_match,
diff.dist_diff
);
}
ledger.finish(cases.len());
}
#[test]
fn diff_002_kdtree_query_k() {
let start = Instant::now();
let cases = query_k_cases();
let oracle_results = scipy_oracle_or_skip(run_scipy_query_k_oracle, &cases);
if oracle_results.is_empty() {
return;
}
let oracle_map: HashMap<String, &QueryKOracleResult> = oracle_results
.iter()
.map(|r| (r.case_id.clone(), r))
.collect();
assert_complete_oracle_results(
"diff_002_kdtree_query_k",
cases.iter().map(|case| case.case_id.clone()),
oracle_results.len(),
&oracle_map,
);
let mut diffs = Vec::new();
let mut pass_count = 0;
let mut ledger = CompareLedger::new("diff_002_kdtree_query_k", &["query_k"]);
for case in &cases {
let Some((scipy_result, rust_result)) = ledger.both(
"query_k",
&case.case_id,
oracle_map.get(&case.case_id).copied(),
rust_query_k(case),
) else {
continue;
};
let rust_indices: Vec<usize> = rust_result.iter().map(|(idx, _)| *idx).collect();
let rust_distances: Vec<f64> = rust_result.iter().map(|(_, d)| *d).collect();
let indices_match = rust_indices == scipy_result.indices;
let max_dist_diff = max_distance_diff(&rust_distances, &scipy_result.distances);
let pass = max_dist_diff <= DIST_TOL;
ledger.compared("query_k", &case.case_id, pass);
if pass {
pass_count += 1;
}
diffs.push(QueryKDiff {
case_id: case.case_id.clone(),
dim: case.query.len(),
n_points: case.points.len(),
k: case.k,
rust_indices,
rust_distances,
scipy_indices: scipy_result.indices.clone(),
scipy_distances: scipy_result.distances.clone(),
indices_match,
max_dist_diff,
tolerance: DIST_TOL,
pass,
});
}
let all_pass = diffs.len() == cases.len() && diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_002_kdtree_query_k".into(),
category: "scipy.spatial.KDTree.query_k".into(),
case_count: diffs.len(),
compared: ledger.counts().clone(),
pass_count,
tolerance: DIST_TOL,
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
};
emit_log(&log, "summary");
assert_all_cases_compared("diff_002_kdtree_query_k", log.case_count, cases.len());
for diff in &diffs {
assert!(
diff.pass,
"{} mismatch: rust_indices={:?} scipy_indices={:?} indices_match={} max_dist_diff={}",
diff.case_id,
diff.rust_indices,
diff.scipy_indices,
diff.indices_match,
diff.max_dist_diff
);
}
ledger.finish(cases.len());
}
#[test]
fn diff_003_kdtree_query_ball_point() {
let start = Instant::now();
let cases = query_ball_point_cases();
let oracle_results = scipy_oracle_or_skip(run_scipy_query_ball_point_oracle, &cases);
if oracle_results.is_empty() {
return;
}
let oracle_map: HashMap<String, &QueryBallPointOracleResult> = oracle_results
.iter()
.map(|r| (r.case_id.clone(), r))
.collect();
assert_complete_oracle_results(
"diff_003_kdtree_query_ball_point",
cases.iter().map(|case| case.case_id.clone()),
oracle_results.len(),
&oracle_map,
);
let mut diffs = Vec::new();
let mut pass_count = 0;
let mut ledger = CompareLedger::new("diff_003_kdtree_query_ball_point", &["query_ball_point"]);
for case in &cases {
let Some((scipy_result, mut rust_result)) = ledger.both(
"query_ball_point",
&case.case_id,
oracle_map.get(&case.case_id).copied(),
rust_query_ball_point(case),
) else {
continue;
};
rust_result.sort();
let indices_match = rust_result == scipy_result.indices;
let pass = indices_match;
ledger.compared("query_ball_point", &case.case_id, pass);
if pass {
pass_count += 1;
}
diffs.push(QueryBallPointDiff {
case_id: case.case_id.clone(),
dim: case.query.len(),
n_points: case.points.len(),
radius: case.r,
rust_indices: rust_result,
scipy_indices: scipy_result.indices.clone(),
indices_match,
pass,
});
}
let all_pass = diffs.len() == cases.len() && diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_003_kdtree_query_ball_point".into(),
category: "scipy.spatial.KDTree.query_ball_point".into(),
case_count: diffs.len(),
compared: ledger.counts().clone(),
pass_count,
tolerance: 0.0,
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
};
emit_log(&log, "summary");
assert_all_cases_compared(
"diff_003_kdtree_query_ball_point",
log.case_count,
cases.len(),
);
for diff in &diffs {
assert!(
diff.pass,
"{} mismatch: rust_indices={:?} scipy_indices={:?}",
diff.case_id, diff.rust_indices, diff.scipy_indices
);
}
ledger.finish(cases.len());
}
#[test]
fn diff_004_kdtree_edge_cases() {
let start = Instant::now();
let cases = vec![
QueryCase {
case_id: "single_point_2d".into(),
points: vec![vec![1.0, 2.0]],
query: vec![0.0, 0.0],
},
QueryCase {
case_id: "single_point_3d".into(),
points: vec![vec![1.0, 2.0, 3.0]],
query: vec![0.0, 0.0, 0.0],
},
QueryCase {
case_id: "two_points_equidistant".into(),
points: vec![vec![1.0, 0.0], vec![-1.0, 0.0]],
query: vec![0.0, 0.0],
},
QueryCase {
case_id: "collinear_points".into(),
points: vec![
vec![0.0, 0.0],
vec![1.0, 0.0],
vec![2.0, 0.0],
vec![3.0, 0.0],
],
query: vec![1.5, 0.1],
},
QueryCase {
case_id: "duplicate_points".into(),
points: vec![vec![1.0, 1.0], vec![1.0, 1.0], vec![2.0, 2.0]],
query: vec![1.0, 1.0],
},
QueryCase {
case_id: "high_dim_5d".into(),
points: vec![
vec![1.0, 0.0, 0.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0, 1.0, 0.0],
vec![0.0, 0.0, 0.0, 0.0, 1.0],
],
query: vec![0.2, 0.2, 0.2, 0.2, 0.2],
},
];
let oracle_results = scipy_oracle_or_skip(run_scipy_query_oracle, &cases);
if oracle_results.is_empty() {
return;
}
let oracle_map: HashMap<String, &QueryOracleResult> = oracle_results
.iter()
.map(|r| (r.case_id.clone(), r))
.collect();
assert_complete_oracle_results(
"diff_004_kdtree_edge_cases",
cases.iter().map(|case| case.case_id.clone()),
oracle_results.len(),
&oracle_map,
);
let mut diffs = Vec::new();
let mut pass_count = 0;
let mut ledger = CompareLedger::new("diff_004_kdtree_edge_cases", &["query"]);
for case in &cases {
let Some((scipy_result, rust_result)) = ledger.both(
"query",
&case.case_id,
oracle_map.get(&case.case_id).copied(),
rust_query(case),
) else {
continue;
};
let dist_diff = (rust_result.1 - scipy_result.distance).abs();
let dist_match = dist_diff <= DIST_TOL;
let pass = dist_match;
ledger.compared("query", &case.case_id, pass);
if pass {
pass_count += 1;
}
diffs.push(QueryDiff {
case_id: case.case_id.clone(),
dim: case.query.len(),
n_points: case.points.len(),
rust_index: rust_result.0,
rust_distance: rust_result.1,
scipy_index: scipy_result.index,
scipy_distance: scipy_result.distance,
index_match: rust_result.0 == scipy_result.index,
dist_diff,
tolerance: DIST_TOL,
pass,
});
}
let all_pass = diffs.len() == cases.len() && diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_004_kdtree_edge_cases".into(),
category: "scipy.spatial.KDTree.edge".into(),
case_count: diffs.len(),
compared: ledger.counts().clone(),
pass_count,
tolerance: DIST_TOL,
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
};
emit_log(&log, "summary");
assert_all_cases_compared("diff_004_kdtree_edge_cases", log.case_count, cases.len());
for diff in &diffs {
assert!(
diff.pass,
"{} distance mismatch: rust={} scipy={} diff={}",
diff.case_id, diff.rust_distance, diff.scipy_distance, diff.dist_diff
);
}
ledger.finish(cases.len());
}