plot3d 0.1.13

Utilities for reading, writing, and manipulating NASA PLOT3D structured grids.
Documentation
//! Single-blade H-O-H mesh connectivity test.
//!
//! Reads the blade0.xyz binary grid (5 blocks: ss_ogrid, ps_ogrid,
//! le_wedge, te_wedge, passage) and verifies that `connectivity_fast`
//! does NOT produce false-positive face matches between SS and PS ogrid
//! blocks that only share edge lines (LE/TE).
//!
//! Test data: tests/data/blade0.xyz (raw binary, f64, little-endian)
//! Generated by: python tests/single-blade/test_singleblade.py (tgs-py)

use plot3d::{
    connectivity_fast, read_plot3d_binary, BinaryFormat, Endian, FloatPrecision, FaceRecord,
};

const BLADE0_PATH: &str = "tests/data/blade0.xyz";

const BLOCK_NAMES: [&str; 5] = ["ss_ogrid", "ps_ogrid", "le_wedge", "te_wedge", "passage"];

fn face_key(f: &FaceRecord) -> (usize, [usize; 3], [usize; 3]) {
    (
        f.block_index,
        [f.il.min(f.ih), f.jl.min(f.jh), f.kl.min(f.kh)],
        [f.il.max(f.ih), f.jl.max(f.jh), f.kl.max(f.kh)],
    )
}

#[test]
fn test_read_blade0() {
    if !std::path::Path::new(BLADE0_PATH).exists() {
        eprintln!("Skipping: {} not found", BLADE0_PATH);
        return;
    }
    let blocks =
        read_plot3d_binary(BLADE0_PATH, BinaryFormat::Raw, FloatPrecision::F64, Endian::Little)
            .expect("Failed to read blade0.xyz");

    assert_eq!(blocks.len(), 5, "Expected 5 blocks");
    for (i, b) in blocks.iter().enumerate() {
        println!(
            "  Block {} ({}): ({}, {}, {})",
            i, BLOCK_NAMES[i], b.imax, b.jmax, b.kmax
        );
    }
    // Expected shapes from tgs-py
    assert_eq!((blocks[0].imax, blocks[0].jmax, blocks[0].kmax), (161, 25, 65));
    assert_eq!((blocks[1].imax, blocks[1].jmax, blocks[1].kmax), (161, 25, 65));
    assert_eq!((blocks[2].imax, blocks[2].jmax, blocks[2].kmax), (21, 33, 65));
    assert_eq!((blocks[3].imax, blocks[3].jmax, blocks[3].kmax), (33, 25, 65));
    assert_eq!((blocks[4].imax, blocks[4].jmax, blocks[4].kmax), (185, 41, 65));
}

#[test]
fn test_connectivity_no_false_positives() {
    if !std::path::Path::new(BLADE0_PATH).exists() {
        eprintln!("Skipping: {} not found", BLADE0_PATH);
        return;
    }
    let blocks =
        read_plot3d_binary(BLADE0_PATH, BinaryFormat::Raw, FloatPrecision::F64, Endian::Little)
            .expect("Failed to read blade0.xyz");

    let (matches, outer_faces) = connectivity_fast(&blocks);

    println!("\n=== Single-blade connectivity ===");
    println!("  Face matches: {}", matches.len());
    println!("  Outer faces:  {}", outer_faces.len());

    for (i, m) in matches.iter().enumerate() {
        let b1 = &m.block1;
        let b2 = &m.block2;
        let n1 = BLOCK_NAMES.get(b1.block_index).unwrap_or(&"?");
        let n2 = BLOCK_NAMES.get(b2.block_index).unwrap_or(&"?");
        println!(
            "  Match {}: {} [{},{},{}]-[{},{},{}] <-> {} [{},{},{}]-[{},{},{}]  ({} pts)",
            i,
            n1, b1.il, b1.jl, b1.kl, b1.ih, b1.jh, b1.kh,
            n2, b2.il, b2.jl, b2.kl, b2.ih, b2.jh, b2.kh,
            m.points.len(),
        );
    }

    // --- No false positive matches between SS and PS ogrid ---
    // SS (block 0) and PS (block 1) should only match at:
    //   i=0 face (LE edge line)   — both blocks share this edge
    //   i=max face (TE edge line) — both blocks share this edge
    // They should NOT match on j=0 (wall), j=max (outer), k=0 (hub), k=max (shroud)
    // because those are geometrically different surfaces that only share edge lines.

    let false_positive_faces = [
        // j=0 wall faces
        ((0, [0, 0, 0], [160, 0, 64]), (1, [0, 0, 0], [160, 0, 64])),
        // j=max outer faces
        ((0, [0, 24, 0], [160, 24, 64]), (1, [0, 24, 0], [160, 24, 64])),
        // k=0 hub faces
        ((0, [0, 0, 0], [160, 24, 0]), (1, [0, 0, 0], [160, 24, 0])),
        // k=max shroud faces
        ((0, [0, 0, 64], [160, 24, 64]), (1, [0, 0, 64], [160, 24, 64])),
    ];

    for (fp1, fp2) in &false_positive_faces {
        let found = matches.iter().any(|m| {
            let k1 = face_key(&m.block1);
            let k2 = face_key(&m.block2);
            (k1 == *fp1 && k2 == *fp2) || (k1 == *fp2 && k2 == *fp1)
        });
        assert!(
            !found,
            "FALSE POSITIVE: block {} [{:?}]-[{:?}] <-> block {} [{:?}]-[{:?}] should NOT match",
            fp1.0, fp1.1, fp1.2, fp2.0, fp2.1, fp2.2,
        );
    }
    println!("\n  No false positives between SS/PS ogrid.");

    // --- Verify expected real matches ---
    // At minimum, these interface matches must exist:
    let expected_interfaces = [
        // SS i=0 <-> PS i=0 (LE edge)
        ((0, [0, 0, 0], [0, 24, 64]), (1, [0, 0, 0], [0, 24, 64])),
        // SS i=max <-> PS i=max (TE edge)
        ((0, [160, 0, 0], [160, 24, 64]), (1, [160, 0, 0], [160, 24, 64])),
    ];

    for (exp1, exp2) in &expected_interfaces {
        let found = matches.iter().any(|m| {
            let k1 = face_key(&m.block1);
            let k2 = face_key(&m.block2);
            (k1 == *exp1 && k2 == *exp2) || (k1 == *exp2 && k2 == *exp1)
        });
        assert!(
            found,
            "MISSING: block {} [{:?}]-[{:?}] <-> block {} [{:?}]-[{:?}] should match",
            exp1.0, exp1.1, exp1.2, exp2.0, exp2.1, exp2.2,
        );
    }
    println!("  Expected SS/PS LE/TE matches found.");

    // Print outer faces
    println!("\n  Outer faces:");
    for f in &outer_faces {
        let bname = BLOCK_NAMES.get(f.block_index).unwrap_or(&"?");
        println!(
            "    id={:3} {} [{},{},{}]-[{},{},{}]",
            f.id.unwrap_or(0),
            bname,
            f.il, f.jl, f.kl, f.ih, f.jh, f.kh,
        );
    }
}