use plot3d::{
connectivity_fast, create_rotation_matrix, face_match_to_json, face_matches_to_dict,
read_plot3d_binary, rotated_periodicity, verify_connectivity, verify_periodicity,
align_face_orientations, permutation_matrices_json,
BinaryFormat, Endian, Float, FloatPrecision,
};
use serde_json::{json, Value};
const MESH_PATH: &str = "tests/data/vspt_mesh_scaled.xyz";
const NBLADES: usize = 55;
const ROTATION_AXIS: char = 'x';
const TOL: Float = 1e-4;
fn build_connectivity_json() -> (Vec<plot3d::Block>, Value) {
let blocks =
read_plot3d_binary(MESH_PATH, BinaryFormat::Raw, FloatPrecision::F32, Endian::Little)
.expect("read mesh failed");
let rotation_angle_deg = 360.0 / NBLADES as Float;
let rotation_angle_rad = rotation_angle_deg.to_radians();
let (face_matches, outer_faces) = connectivity_fast(&blocks);
let face_matches = face_matches_to_dict(&blocks, &face_matches);
let (face_matches, _unverified) = verify_connectivity(&blocks, &face_matches, TOL);
let (face_matches, _unaligned) = align_face_orientations(&blocks, &face_matches, TOL);
let (periodic_pairs, outer_faces_remaining) = rotated_periodicity(
&blocks,
&face_matches,
&outer_faces,
rotation_angle_deg,
ROTATION_AXIS,
true,
);
let (periodic_pairs, _unverified) =
verify_periodicity(&blocks, &periodic_pairs, rotation_angle_rad, ROTATION_AXIS, TOL);
let rot_mat = create_rotation_matrix(rotation_angle_rad, ROTATION_AXIS);
let rot_mat_json: Vec<Vec<Float>> = rot_mat.iter().map(|row| row.to_vec()).collect();
let fm_json: Vec<Value> = face_matches.iter().map(|m| face_match_to_json(m)).collect();
let pf_json: Vec<Value> = periodic_pairs.iter().map(|m| face_match_to_json(m)).collect();
let of_json: Vec<Value> = outer_faces_remaining
.iter()
.map(|f| {
json!({
"lb": [f.il, f.jl, f.kl],
"ub": [f.ih, f.jh, f.kh],
"block_index": f.block_index,
})
})
.collect();
let payload = json!({
"mesh_file": MESH_PATH,
"nblocks": blocks.len(),
"face_matches": fm_json,
"outer_faces": of_json,
"periodic_faces": pf_json,
"periodicity": {
"nblades": NBLADES,
"rotation_axis": ROTATION_AXIS.to_string(),
"rotation_angle_rad": rotation_angle_rad,
"rotation_angle_deg": rotation_angle_deg,
"transformation_matrix": rot_mat_json,
"convention": "Face_B_points = (transformation_matrix @ Face_A_points.T).T",
"source": "plot3d-rs::rotated_periodicity",
},
"permutation_matrices": permutation_matrices_json(),
});
(blocks, payload)
}
fn extract_face_points(block: &plot3d::Block, lb: &[usize; 3], ub: &[usize; 3]) -> Vec<[Float; 3]> {
let imin = lb[0].min(ub[0]);
let imax = lb[0].max(ub[0]);
let jmin = lb[1].min(ub[1]);
let jmax = lb[1].max(ub[1]);
let kmin = lb[2].min(ub[2]);
let kmax = lb[2].max(ub[2]);
let mut pts = Vec::new();
for i in imin..=imax {
for j in jmin..=jmax {
for k in kmin..=kmax {
let (x, y, z) = block.xyz(i, j, k);
pts.push([x, y, z]);
}
}
}
pts
}
fn parse_ijk(v: &Value) -> [usize; 3] {
let arr = v.as_array().unwrap();
[
arr[0].as_u64().unwrap() as usize,
arr[1].as_u64().unwrap() as usize,
arr[2].as_u64().unwrap() as usize,
]
}
fn max_nearest_dist(query: &[[Float; 3]], target: &[[Float; 3]]) -> Float {
query
.iter()
.map(|q| {
target
.iter()
.map(|t| {
let dx = q[0] - t[0];
let dy = q[1] - t[1];
let dz = q[2] - t[2];
dx * dx + dy * dy + dz * dz
})
.fold(Float::INFINITY, Float::min)
.sqrt()
})
.fold(0.0 as Float, Float::max)
}
#[test]
fn test_creates_connectivity_json() {
let (blocks, payload) = build_connectivity_json();
assert_eq!(payload["nblocks"].as_u64().unwrap() as usize, blocks.len());
assert!(payload["face_matches"].as_array().unwrap().len() > 0);
assert!(payload["periodic_faces"].as_array().unwrap().len() > 0);
let json_str = serde_json::to_string_pretty(&payload).unwrap();
std::fs::write("vspt_connectivity.json", &json_str).unwrap();
println!("Wrote vspt_connectivity.json ({} bytes)", json_str.len());
}
#[test]
fn test_periodic_faces_geometry() {
let (blocks, payload) = build_connectivity_json();
let rot_mat_json = payload["periodicity"]["transformation_matrix"]
.as_array()
.unwrap();
let rot_mat: [[Float; 3]; 3] = {
let mut m = [[0.0; 3]; 3];
for (i, row) in rot_mat_json.iter().enumerate() {
for (j, val) in row.as_array().unwrap().iter().enumerate() {
m[i][j] = val.as_f64().unwrap() as Float;
}
}
m
};
for (idx, pf) in payload["periodic_faces"]
.as_array()
.unwrap()
.iter()
.enumerate()
{
let b1_idx = pf["block1"]["block_index"].as_u64().unwrap() as usize;
let b2_idx = pf["block2"]["block_index"].as_u64().unwrap() as usize;
let lb1 = parse_ijk(&pf["block1"]["lb"]);
let ub1 = parse_ijk(&pf["block1"]["ub"]);
let lb2 = parse_ijk(&pf["block2"]["lb"]);
let ub2 = parse_ijk(&pf["block2"]["ub"]);
let pts1 = extract_face_points(&blocks[b1_idx], &lb1, &ub1);
let pts2 = extract_face_points(&blocks[b2_idx], &lb2, &ub2);
let apply_rot = |p: &[Float; 3], m: &[[Float; 3]; 3]| -> [Float; 3] {
[
m[0][0] * p[0] + m[0][1] * p[1] + m[0][2] * p[2],
m[1][0] * p[0] + m[1][1] * p[1] + m[1][2] * p[2],
m[2][0] * p[0] + m[2][1] * p[1] + m[2][2] * p[2],
]
};
let rot_inv: [[Float; 3]; 3] = [
[rot_mat[0][0], rot_mat[1][0], rot_mat[2][0]],
[rot_mat[0][1], rot_mat[1][1], rot_mat[2][1]],
[rot_mat[0][2], rot_mat[1][2], rot_mat[2][2]],
];
let pts1_fwd: Vec<_> = pts1.iter().map(|p| apply_rot(p, &rot_mat)).collect();
let pts1_rev: Vec<_> = pts1.iter().map(|p| apply_rot(p, &rot_inv)).collect();
let dist_fwd = max_nearest_dist(&pts1_fwd, &pts2);
let dist_rev = max_nearest_dist(&pts1_rev, &pts2);
let max_dist = dist_fwd.min(dist_rev);
assert!(
max_dist < TOL,
"periodic[{idx}]: blk{b1_idx} -> blk{b2_idx} max_dist={max_dist:.2e} >= TOL (fwd={dist_fwd:.2e}, rev={dist_rev:.2e})"
);
}
}
#[test]
fn test_connectivity_faces_geometry() {
let (blocks, payload) = build_connectivity_json();
for (idx, fm) in payload["face_matches"]
.as_array()
.unwrap()
.iter()
.enumerate()
{
let b1_idx = fm["block1"]["block_index"].as_u64().unwrap() as usize;
let b2_idx = fm["block2"]["block_index"].as_u64().unwrap() as usize;
let lb1 = parse_ijk(&fm["block1"]["lb"]);
let ub1 = parse_ijk(&fm["block1"]["ub"]);
let lb2 = parse_ijk(&fm["block2"]["lb"]);
let ub2 = parse_ijk(&fm["block2"]["ub"]);
let pts1 = extract_face_points(&blocks[b1_idx], &lb1, &ub1);
let pts2 = extract_face_points(&blocks[b2_idx], &lb2, &ub2);
let max_dist = max_nearest_dist(&pts1, &pts2);
assert!(
max_dist < TOL,
"connectivity[{idx}]: blk{b1_idx} <-> blk{b2_idx} max_dist={max_dist:.2e} >= TOL"
);
}
}
#[test]
fn test_no_overlapping_face_matches() {
let (_blocks, payload) = build_connectivity_json();
let fms = payload["face_matches"].as_array().unwrap();
fn norm(a: usize, b: usize) -> (usize, usize) {
(a.min(b), a.max(b))
}
fn ranges_overlap(a: (usize, usize), b: (usize, usize)) -> bool {
!(a.1 < b.0 || b.1 < a.0)
}
fn side_overlaps(a_lb: [usize; 3], a_ub: [usize; 3], b_lb: [usize; 3], b_ub: [usize; 3]) -> bool {
(0..3).all(|d| ranges_overlap(norm(a_lb[d], a_ub[d]), norm(b_lb[d], b_ub[d])))
}
for i in 0..fms.len() {
for j in (i + 1)..fms.len() {
let a = &fms[i];
let b = &fms[j];
let a_b1 = a["block1"]["block_index"].as_u64().unwrap() as usize;
let a_b2 = a["block2"]["block_index"].as_u64().unwrap() as usize;
let b_b1 = b["block1"]["block_index"].as_u64().unwrap() as usize;
let b_b2 = b["block2"]["block_index"].as_u64().unwrap() as usize;
let a_lb1 = parse_ijk(&a["block1"]["lb"]);
let a_ub1 = parse_ijk(&a["block1"]["ub"]);
let a_lb2 = parse_ijk(&a["block2"]["lb"]);
let a_ub2 = parse_ijk(&a["block2"]["ub"]);
let (b_lb1, b_ub1, b_lb2, b_ub2) = if a_b1 == b_b1 && a_b2 == b_b2 {
(
parse_ijk(&b["block1"]["lb"]),
parse_ijk(&b["block1"]["ub"]),
parse_ijk(&b["block2"]["lb"]),
parse_ijk(&b["block2"]["ub"]),
)
} else if a_b1 == b_b2 && a_b2 == b_b1 {
(
parse_ijk(&b["block2"]["lb"]),
parse_ijk(&b["block2"]["ub"]),
parse_ijk(&b["block1"]["lb"]),
parse_ijk(&b["block1"]["ub"]),
)
} else {
continue;
};
let side1 = side_overlaps(a_lb1, a_ub1, b_lb1, b_ub1);
let side2 = side_overlaps(a_lb2, a_ub2, b_lb2, b_ub2);
assert!(
!(side1 && side2),
"face_matches[{i}] and face_matches[{j}] on pair \
({a_b1},{a_b2}) overlap on both sides: \
a=(blk{a_b1} {a_lb1:?}..{a_ub1:?}, blk{a_b2} {a_lb2:?}..{a_ub2:?}) \
b=(blk{a_b1} {b_lb1:?}..{b_ub1:?}, blk{a_b2} {b_lb2:?}..{b_ub2:?})"
);
}
}
}