graphlang 0.1.3

Terminal and graphical tool to create and explore graph grammars.
Documentation
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
use std::time::Duration;

use crate::primitives::{Index, Label};
use crate::{Graph, Production};
use rand::{seq::IteratorRandom, thread_rng};
use serde::{Deserialize, Serialize};

/// A `GraphGrammar` yields different graphs, that are constructible using a starting graph and a
/// list of transformations (productions). It is also used to further transform generated graphs
/// using `evolve`. Refer to `generate_random`.
#[derive(Serialize, Deserialize, Clone, Debug, std::cmp::PartialEq)]
pub struct GraphGrammar<I: Index, NL: Label, EL: Label> {
    /// Starting graph
    pub start_graph: Graph<I, NL, EL>,
    /// Number of productions, that transform a matching subgraph to another
    pub productions: HashMap<String, Production<I, NL, EL>>,
    /// List of terminal nodes. A graph has to consist only of terminal nodes
    /// to be considered valid.
    pub terminal_nodes: HashSet<NL>,
}

impl<NL: Label, EL: Label> GraphGrammar<u32, NL, EL> {
    /// Applies a random production, that can applied. This function is very expensive.
    /// Return `None` if it can't be transformed.
    #[must_use]
    pub fn evolve_single_step(&self, g: &Graph<u32, NL, EL>) -> Option<Graph<u32, NL, EL>> {
        let mut tried_productions = Vec::new();
        while tried_productions.len() != self.productions.len() {
            let p = self.productions.iter().choose(&mut thread_rng())?;
            if tried_productions.contains(&p) {
                continue;
            }

            let res = p.1.apply(g);
            if res.is_some() {
                return res;
            }
            tried_productions.push(p);
        }
        None
    }
}

impl<I: Index> From<&GraphGrammar<I, &str, ()>> for GraphGrammar<I, String, ()> {
    fn from(gg: &GraphGrammar<I, &str, ()>) -> Self {
        let start_graph = gg.start_graph.clone().into();
        let productions = gg
            .productions
            .iter()
            .map(|(n, p)| (n.clone(), p.clone().into()))
            .collect();
        let terminal_nodes = gg
            .terminal_nodes
            .iter() //
            .map(|s| (*s).to_owned()) //
            .collect();
        Self {
            start_graph,
            productions,
            terminal_nodes,
        }
    }
}

impl<I: Index> From<GraphGrammar<I, &str, ()>> for GraphGrammar<I, String, ()> {
    fn from(gg: GraphGrammar<I, &str, ()>) -> Self {
        let start_graph = gg.start_graph.clone().into();
        let productions = gg
            .productions
            .iter()
            .map(|(n, p)| (n.clone(), p.clone().into()))
            .collect();
        let terminal_nodes = gg
            .terminal_nodes
            .iter() //
            .map(|s| (*s).to_owned()) //
            .collect();
        Self {
            start_graph,
            productions,
            terminal_nodes,
        }
    }
}

impl<I: Index, NL: Label, EL: Label> GraphGrammar<I, NL, EL> {
    /// A graph has to consist only of terminal nodes to be considered valid.
    /// This validates if a graph `g` is valid under the used `GraphGrammar`.
    #[must_use]
    pub fn is_valid(&self, g: &Graph<I, NL, EL>) -> bool {
        for node in &g.nodes {
            if self.terminal_nodes.get(&node.label).is_none() {
                return false;
            }
        }
        true
    }

    /// Evolves `g` by applying productions of the `GraphGrammar` until the first condition
    /// specified in `qc` is statisfied and returns the resulting graph.
    #[must_use]
    pub fn evolve(&self, g: &Graph<I, NL, EL>, qc: &QuitCondition) -> Graph<I, NL, EL> {
        let _ = self;
        let _ = g;
        let _ = qc;
        unimplemented!()
    }

    /// Similar to `evolve`. Returns the first valid graph that is part of the language
    /// specified by the grammar using random productions on the starting graph.
    ///
    /// # Panics
    /// Panics if quitcondition is invalid.
    #[must_use]
    pub fn generate_random(&self, g: &Graph<I, NL, EL>, cond: QuitCondition) -> Graph<I, NL, EL> {
        assert!(cond.is_valid());
        let _ = self;
        let _ = g;
        let _ = cond;
        unimplemented!()
    }
}

/// Used for randomly evolving a graph using a `GraphGrammar` to specify
/// when to stop trying random productions. All fields are optional, but
/// at least one must be used. Options can be set using the builder pattern
/// on a `new`ly created struct.
#[derive(Serialize, Deserialize, Default, PartialEq, Clone, Copy, Debug)]
pub struct QuitCondition {
    num_productions: Option<NonZeroU64>,
    timeout: Option<Duration>,
    valid: Option<bool>,
}

impl QuitCondition {
    /// Creates an empty struct. It utilises the builder pattern.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Maximal duration allowed. Note that this technically denotes the minimal
    /// runtime after which it exits after the first possible point.
    #[must_use]
    pub const fn add_timeout(mut self, t: Duration) -> Self {
        self.timeout = Some(t);
        self
    }

    /// Maximal number of productions.
    #[must_use]
    pub const fn add_max_productions(mut self, num: NonZeroU64) -> Self {
        self.num_productions = Some(num);
        self
    }

    /// Quits as soon as the graph becomes valid w.r.t. the graph grammar
    /// i.e. it contains only terminal nodes.
    #[must_use]
    pub const fn on_first_valid_graph(mut self) -> Self {
        self.valid = Some(true);
        self
    }

    /// Checks if at least one condition is set.
    #[must_use]
    pub const fn is_valid(&self) -> bool {
        self.valid.is_some() || self.timeout.is_some() || self.num_productions.is_some()
    }
}

#[cfg(test)]
mod quitcondition {
    #[allow(clippy::wildcard_imports)]
    use super::*;
    use crate::primitives::Node;

    #[test]
    fn new_is_invalid() {
        assert!(!QuitCondition::new().is_valid());
    }

    #[test]
    fn timeout_is_valid() {
        let d = Duration::from_secs(1);
        let cond = QuitCondition::new().add_timeout(d);
        assert!(cond.is_valid());
        assert_eq!(d, cond.timeout.unwrap());
    }

    #[test]
    fn prodnum_is_valid() {
        let num = NonZeroU64::new(1000).unwrap();
        let cond = QuitCondition::new().add_max_productions(num);
        assert!(cond.is_valid());
        assert_eq!(num, cond.num_productions.unwrap());
    }

    #[test]
    fn valid_graph_is_valid() {
        let cond = QuitCondition::new().on_first_valid_graph();
        assert!(cond.is_valid());
    }

    #[test]
    fn all_options() {
        let d = Duration::from_secs(1);
        let num = NonZeroU64::new(1000).unwrap();
        let cond = QuitCondition::new()
            .on_first_valid_graph()
            .add_timeout(d)
            .add_max_productions(num);
        assert!(cond.is_valid());
        assert_eq!(num, cond.num_productions.unwrap());
        assert_eq!(d, cond.timeout.unwrap());
    }

    #[test]
    fn trivial_production() {
        let mut g = Graph::<_, _, &str> {
            nodes: vec![Node::new(0u32, "a"), Node::new(1, "b")],
            edges: vec![],
        };
        let p = Production::new(
            Graph {
                nodes: vec![Node::new(0u32, "a")],
                edges: vec![],
            },
            Graph {
                nodes: vec![],
                edges: vec![],
            },
            Graph {
                nodes: vec![Node::new(0, "b")],
                edges: vec![],
            },
        );

        let h = Graph {
            nodes: vec![Node::new(0u32, "b"), Node::new(1, "b")],
            edges: vec![],
        };

        p.apply_inplace(&mut g);
        assert_eq!(g, h);
    }
}