gspx 0.1.2

Sparse graph signal processing and spectral graph wavelets in Rust
Documentation
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;

use gspx::{Dataset, ExampleData, GraphKind, KernelPreset, load_ply_laplacian, load_ply_xyz};
use tempfile::{TempDir, tempdir};

fn create_example_data_dir() -> TempDir {
    let dir = tempdir().expect("tempdir should be created");
    fs::create_dir_all(dir.path().join("MESH")).expect("MESH directory should be created");
    fs::create_dir_all(dir.path().join("KERNELS")).expect("KERNELS directory should be created");

    fs::write(
        dir.path().join("MESH").join("BUNNY.ply"),
        r#"ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
end_header
0 0 0
1 0 0
1 1 0
0 1 0
3 0 1 2
3 0 2 3
"#,
    )
    .expect("mesh fixture should be written");

    fs::write(
        dir.path().join("KERNELS").join("MODIFIED_MORLET.json"),
        r#"{
  "poles": [
    { "q": -1.0, "r": [0.5] },
    { "q": -4.0, "r": [0.25] }
  ],
  "d": [0.1]
}"#,
    )
    .expect("VF kernel fixture should be written");

    fs::write(
        dir.path().join("KERNELS").join("SHANNON.json"),
        r#"{
  "spectrum_bound": 4.0,
  "min_lambda": 0.0,
  "approximations": [
    { "coeffs": [1.0, 0.25, -0.125] }
  ]
}"#,
    )
    .expect("Chebyshev kernel fixture should be written");

    dir
}

#[test]
fn example_data_loads_mesh_and_coords_from_local_dir() {
    let dir = create_example_data_dir();
    let data = ExampleData::from_dir(dir.path()).expect("local example data should open");

    let laplacian = data
        .graph(Dataset::Bunny, GraphKind::Mesh)
        .expect("mesh graph should load from local directory");
    let coords = data
        .coords(Dataset::Bunny)
        .expect("mesh coordinates should load from local directory");

    assert_eq!(laplacian.shape(), (4, 4));
    assert_eq!(coords.shape(), &[4, 3]);
}

#[test]
fn example_data_loads_kernel_presets_from_local_dir() {
    let dir = create_example_data_dir();
    let data = ExampleData::from_dir(dir.path()).expect("local example data should open");

    let vf = data
        .vf_kernel(KernelPreset::ModifiedMorlet)
        .expect("VF kernel should load from local directory");
    let cheby = data
        .cheby_kernel(KernelPreset::Shannon)
        .expect("Chebyshev kernel should load from local directory");

    assert_eq!(vf.residues.nrows(), vf.poles.len());
    assert_eq!(vf.direct.len(), vf.residues.ncols());
    assert_eq!(cheby.coefficients.shape(), &[3, 1]);
}

#[test]
fn example_data_rejects_missing_directory() {
    let missing = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("resource-tests")
        .join("missing-library");
    let err = ExampleData::from_dir(&missing).expect_err("missing directory should fail");
    assert!(err.to_string().contains("example-data directory not found"));
}

#[test]
fn bunny_mesh_files_parse_when_present() {
    let bunny = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("resources")
        .join("library")
        .join("MESH")
        .join("BUNNY.ply");
    if !bunny.exists() {
        return;
    }
    let l = load_ply_laplacian(&bunny).expect("bunny laplacian should parse");
    let xyz = load_ply_xyz(&bunny).expect("bunny coordinates should parse");
    assert_eq!(l.rows(), l.cols());
    assert_eq!(l.rows(), xyz.nrows());
    assert_eq!(xyz.ncols(), 3);
    assert!(l.nnz() > l.rows());
}

#[test]
fn ascii_ply_loaders_parse_fixture() {
    let dir = tempdir().expect("tempdir should be created");
    let path = dir.path().join("ascii_fixture.ply");
    let content = r#"ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
end_header
0 0 0
1 0 0
1 1 0
0 1 0
3 0 1 2
3 0 2 3
"#;
    fs::write(&path, content).expect("ascii ply fixture should be written");

    let laplacian = load_ply_laplacian(&path).expect("ascii ply laplacian should load");
    let xyz = load_ply_xyz(&path).expect("ascii ply coordinates should load");

    assert_eq!(laplacian.shape(), (4, 4));
    assert_eq!(xyz.shape(), &[4, 3]);
}

#[test]
fn binary_ply_loaders_parse_fixture_with_fallback_vertex_props() {
    let dir = tempdir().expect("tempdir should be created");
    let path = dir.path().join("binary_fixture.ply");
    let mut file = File::create(&path).expect("binary ply file should be created");
    let header = b"ply
format binary_little_endian 1.0
element vertex 3
property float px
property float py
property float pz
element face 1
property list uchar int vertex_indices
end_header
";
    file.write_all(header).expect("header should be written");
    for value in [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 1.0, 0.0] {
        file.write_all(&value.to_le_bytes())
            .expect("vertex bytes should be written");
    }
    file.write_all(&[3u8])
        .expect("face arity byte should be written");
    for index in [0i32, 1, 2] {
        file.write_all(&index.to_le_bytes())
            .expect("face index bytes should be written");
    }
    drop(file);

    let laplacian = load_ply_laplacian(&path).expect("binary ply laplacian should load");
    let xyz = load_ply_xyz(&path).expect("binary ply coordinates should load");

    assert_eq!(laplacian.shape(), (3, 3));
    assert_eq!(xyz.shape(), &[3, 3]);
    assert_eq!(xyz[[2, 0]], 0.5);
    assert_eq!(xyz[[2, 1]], 1.0);
    assert_eq!(xyz[[2, 2]], 0.0);
}