ratio-graph 0.23.3

Ratio's graph manipulation library.
Documentation
use phf::phf_map;

use crate::Graph;
use crate::serialize::CompactGraph;

/// Datasets as raw JSON encoded CompactGraphs.
const DATASETS_JSON: phf::Map<&'static str, &'static str> = phf_map! {
    "aircraft_engine" => include_str!("./aircraft_engine.json"),
    "architecture_integral" => include_str!("./architecture_integral.json"),
    "architecture_mix" => include_str!("./architecture_mix.json"),
    "architecture_modular" => include_str!("./architecture_modular.json"),
    "climate_control_mg" => include_str!("./climate_control_mg.json"),
    "climate_control" => include_str!("./climate_control.json"),
    "compatibility" => include_str!("./compatibility.json"),
    "design" => include_str!("./design.json"),
    "eefde_lock" => include_str!("./eefde_lock.json"),
    "elevator45" => include_str!("./elevator45.json"),
    "elevator175" => include_str!("./elevator175.json"),
    "ford_hood" => include_str!("./ford_hood.json"),
    "kodak3d" => include_str!("./kodak3d.json"),
    "ledsip" => include_str!("./ledsip.json"),
    "localbus" => include_str!("./localbus.json"),
    "overlap" => include_str!("./overlap.json"),
    "pathfinder" => include_str!("./pathfinder.json"),
    "shaja8" => include_str!("./shaja8.json"),
    "similarity" => include_str!("./similarity.json"),
    "tarjans8" => include_str!("./tarjans8.json"),
    "ucav" => include_str!("./ucav.json"),
};

/// Enumerate all available datasets.
pub fn enumerate_datasets() -> Vec<&'static str> {
    let mut keys: Vec<&'static str> = DATASETS_JSON.keys().copied().collect();
    keys.sort();
    keys
}

/// Get a dataset as the CompactGraph it is encoded as.
fn get_compact(key: &str) -> Option<CompactGraph> {
    DATASETS_JSON
        .get(key)
        .and_then(|&contents| serde_json::from_str(contents).ok())
}

/// Get a dataset as a Graph instance.
pub fn get_dataset(key: &str) -> Option<Graph> {
    get_compact(key).and_then(|c| c.try_into().ok())
}

/// Get the info regarding a dataset.
pub fn get_dataset_info(key: &str) -> Option<String> {
    get_compact(key).and_then(|c| {
        c.metadata
            .annotations
            .get("info")
            .and_then(|info| info.as_str())
            .map(|s| s.into())
    })
}

#[cfg(test)]
mod tests {
    use std::collections::{BTreeMap, BTreeSet};

    #[allow(unused_imports)]
    use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
    use uuid::Uuid;

    #[allow(unused_imports)]
    use super::*;
    use crate::mdm::{NodeSelectionMode, mdm_axis_nodes};
    use crate::{EdgeStore, HasNodeStore, NodeStore};

    #[test]
    fn test_dataset_keys() {
        assert!(
            DATASETS_JSON.contains_key("aircraft_engine"),
            "definitely should have this"
        );
        assert_eq!(DATASETS_JSON.len(), 21);
    }

    #[test]
    fn test_dataset_info() {
        let info = get_dataset_info("localbus").unwrap();
        assert_eq!("Example graph containing a local bus.", info.as_str());
    }

    #[test]
    fn test_dataset_graphs() {
        for key in enumerate_datasets() {
            assert!(get_compact(key).is_some());
            assert!(get_dataset_info(key).is_some());
            let graph = get_dataset(key).unwrap_or_else(|| panic!("a valid graph for {key}"));
            for node in graph.all_nodes() {
                graph.check_node(node).expect("valid node");
            }
            for edge in graph.all_edges() {
                graph
                    .check_edge(edge, graph.node_store())
                    .expect("valid edge");
            }
        }
    }

    #[test]
    fn test_ucav_contents() {
        let mut g = get_dataset("ucav").expect("valid graph");
        let node_agg = g.node_aggregate();
        assert_eq!(node_agg.kinds, BTreeMap::from([("node".to_string(), 14)]));
        assert_eq!(
            node_agg.labels,
            BTreeMap::from([("default".to_string(), 14)])
        );
        let weight_keys = node_agg
            .weights
            .keys()
            .map(|x| x.to_owned())
            .collect::<BTreeSet<String>>();
        let reference_keys = BTreeSet::from([
            "min_cost".to_string(),
            "max_cost".to_string(),
            "mean_cost".to_string(),
            "min_duration".to_string(),
            "mean_duration".to_string(),
            "max_duration".to_string(),
            "improvement_curve".to_string(),
        ]);
        assert_eq!(weight_keys, reference_keys);
        assert_eq!(g.max_node_depth(), 0);
    }

    #[test]
    fn test_climate_control_mg() {
        let compact: CompactGraph = DATASETS_JSON
            .get("climate_control_mg")
            .map(|&contents| serde_json::from_str(contents).expect("a correctly encoded dataset"))
            .expect("valid compact graph");

        let mut graph: Result<Graph, _> = compact.try_into();
        match graph.as_mut() {
            Ok(graph) => assert_eq!(graph.max_node_depth(), 3),
            Err(err) => panic!("{err:?}"),
        }

        // Do some descendant/ascendant checks.
        let graph = graph.unwrap();

        // node_node6's ID from the JSON. Le root.
        let node_id = graph.root_ids().next().unwrap();
        assert_eq!(graph.node_height(&node_id, true, None, None).unwrap(), 3);
        assert_eq!(graph.node_depth(&node_id, true, None, None).unwrap(), 0);
        assert_eq!(graph.node_depth(&node_id, true, None, Some(0)).unwrap(), 0);

        assert_eq!(graph.leaf_ids().count(), 16);

        // Now cap this search at 2 levels.
        assert_eq!(graph.node_height(&node_id, true, None, Some(2)).unwrap(), 2);

        // Now cap it at it's children.
        let leaf_ids: BTreeSet<Uuid> = graph
            .get_node(&node_id)
            .unwrap()
            .children
            .clone()
            .into_iter()
            .collect();
        assert_eq!(
            graph
                .node_height(&node_id, true, Some(&leaf_ids), Some(10))
                .unwrap(),
            1
        );

        // Check the height of one of the leafs.
        let node_id = graph.leaf_ids().next().unwrap();
        assert_eq!(graph.node_depth(&node_id, true, None, None).unwrap(), 3);

        // Now check the axis node functions.
        let ax_only_root = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            None,
            Some(0),
            &NodeSelectionMode::Independent,
        )
        .unwrap();
        assert_eq!(ax_only_root.len(), 1);

        let ax_full_depth = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            None,
            Some(3),
            &NodeSelectionMode::Independent,
        )
        .unwrap();
        assert_eq!(ax_full_depth.len(), 16);

        let ax_in_between = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            None,
            Some(2),
            &NodeSelectionMode::Independent,
        )
        .unwrap();
        assert_eq!(ax_in_between.len(), 10);

        // There's only one kind here, but should still work.
        let kinds = graph
            .node_aggregate()
            .kinds
            .to_owned()
            .into_keys()
            .collect::<BTreeSet<_>>();
        assert_eq!(kinds.len(), 1);
        let ax_fake_dependent = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            None,
            Some(2),
            &NodeSelectionMode::Dependent(kinds),
        )
        .unwrap();
        assert_eq!(ax_fake_dependent.len(), 10);
    }

    #[test]
    fn test_ledsip_mdm() {
        let compact: CompactGraph = DATASETS_JSON
            .get("ledsip")
            .map(|&contents| serde_json::from_str(contents).expect("a correctly encoded dataset"))
            .expect("valid compact graph");

        let graph: Graph = compact.try_into().unwrap();
        let axis = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            Default::default(),
            Default::default(),
            &Default::default(),
        )
        .unwrap();

        assert_eq!(axis.len(), 1200);
    }

    #[test]
    fn test_compatibility_mdm() {
        let compact: CompactGraph = DATASETS_JSON
            .get("compatibility")
            .map(|&contents| serde_json::from_str(contents).expect("a correctly encoded dataset"))
            .expect("valid compact graph");

        let graph: Graph = compact.try_into().unwrap();

        let axis = mdm_axis_nodes(
            &graph,
            &Default::default(),
            &Default::default(),
            Default::default(),
            Default::default(),
            &NodeSelectionMode::Dependent(BTreeSet::from(["A".to_string()])),
        )
        .unwrap();
        assert_eq!(graph.leaf_ids().count(), 6);
        assert_eq!(axis.len(), 5); // C1 is not connected to A.
    }
}