use crate::powl_arena::{PowlArena, PowlNode};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConnectivityResult {
pub connected: bool,
pub unreachable_nodes: Vec<usize>,
pub nodes_cant_reach_end: Vec<usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AcyclicityResult {
pub acyclic: bool,
pub cycle_nodes: Vec<usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DgSoundnessReport {
pub sound: bool,
pub connectivity: ConnectivityResult,
pub acyclicity: AcyclicityResult,
pub has_start_nodes: bool,
pub has_end_nodes: bool,
}
fn can_reach(order: &crate::powl_arena::BinaryRelation, src: usize, tgt: usize) -> bool {
if src == tgt {
return true;
}
let mut visited = vec![false; order.n];
let mut stack = Vec::new();
stack.push(src);
visited[src] = true;
while let Some(current) = stack.pop() {
for neighbor in order.get_postset(current) {
if neighbor == tgt {
return true;
}
if !visited[neighbor] {
visited[neighbor] = true;
stack.push(neighbor);
}
}
}
false
}
pub fn validate_connectivity(arena: &PowlArena, root: u32) -> ConnectivityResult {
let dg = match arena.get(root) {
Some(PowlNode::DecisionGraph(dg)) => dg,
_ => {
return ConnectivityResult {
connected: false,
unreachable_nodes: vec![],
nodes_cant_reach_end: vec![],
}
}
};
let n = dg.children.len();
let mut unreachable_nodes = Vec::new();
let mut nodes_cant_reach_end = Vec::new();
for child_local in 0..n {
let reachable_from_start = dg
.start_nodes
.iter()
.any(|&start| can_reach(&dg.order, start, child_local));
if !reachable_from_start {
unreachable_nodes.push(child_local);
}
let can_reach_end = dg
.end_nodes
.iter()
.any(|&end| can_reach(&dg.order, child_local, end));
if !can_reach_end {
nodes_cant_reach_end.push(child_local);
}
}
let connected = unreachable_nodes.is_empty() && nodes_cant_reach_end.is_empty();
ConnectivityResult {
connected,
unreachable_nodes,
nodes_cant_reach_end,
}
}
pub fn validate_acyclicity(arena: &PowlArena, root: u32) -> AcyclicityResult {
let dg = match arena.get(root) {
Some(PowlNode::DecisionGraph(dg)) => dg,
_ => {
return AcyclicityResult {
acyclic: false,
cycle_nodes: vec![],
}
}
};
let n = dg.children.len();
let mut color: Vec<u8> = vec![0; n];
let mut cycle_nodes = Vec::new();
for start in 0..n {
if color[start] != 0 {
continue;
}
let mut dfs_stack: Vec<(usize, usize, bool)> = vec![(start, 0, true)];
while let Some((node, neighbor_idx, first_visit)) = dfs_stack.pop() {
if first_visit {
if color[node] == 2 {
continue;
}
if color[node] == 1 {
cycle_nodes.push(node);
continue;
}
color[node] = 1; }
let neighbors = dg.order.get_postset(node);
let mut found_next = false;
for &neighbor in &neighbors[neighbor_idx..] {
let abs_idx = neighbor_idx
+ neighbors[neighbor_idx..]
.iter()
.position(|&x| x == neighbor)
.unwrap();
if color[neighbor] == 1 {
cycle_nodes.push(neighbor);
cycle_nodes.push(node);
continue;
}
if color[neighbor] == 0 {
dfs_stack.push((node, abs_idx + 1, false));
dfs_stack.push((neighbor, 0, true));
found_next = true;
break;
}
}
if !found_next {
color[node] = 2;
}
}
}
cycle_nodes.sort_unstable();
cycle_nodes.dedup();
AcyclicityResult {
acyclic: cycle_nodes.is_empty(),
cycle_nodes,
}
}
pub fn get_soundness_report(arena: &PowlArena, root: u32) -> DgSoundnessReport {
let dg = match arena.get(root) {
Some(PowlNode::DecisionGraph(dg)) => dg,
_ => {
return DgSoundnessReport {
sound: false,
connectivity: ConnectivityResult {
connected: false,
unreachable_nodes: vec![],
nodes_cant_reach_end: vec![],
},
acyclicity: AcyclicityResult {
acyclic: false,
cycle_nodes: vec![],
},
has_start_nodes: false,
has_end_nodes: false,
}
}
};
let has_start_nodes = !dg.start_nodes.is_empty();
let has_end_nodes = !dg.end_nodes.is_empty();
let connectivity = validate_connectivity(arena, root);
let acyclicity = validate_acyclicity(arena, root);
let sound = has_start_nodes && has_end_nodes && connectivity.connected && acyclicity.acyclic;
DgSoundnessReport {
sound,
connectivity,
acyclicity,
has_start_nodes,
has_end_nodes,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::powl_arena::{BinaryRelation, PowlArena};
fn build_dg(
children_labels: &[&str],
edges: &[(usize, usize)],
start_nodes: &[usize],
end_nodes: &[usize],
empty_path: bool,
) -> (PowlArena, u32) {
let mut arena = PowlArena::new();
let child_indices: Vec<u32> = children_labels
.iter()
.map(|&label| arena.add_transition(Some(label.to_string())))
.collect();
let n = child_indices.len();
let mut order = BinaryRelation::new(n);
for &(src, tgt) in edges {
order.add_edge(src, tgt);
}
let dg = arena.add_decision_graph(
child_indices,
order,
start_nodes.to_vec(),
end_nodes.to_vec(),
empty_path,
);
(arena, dg)
}
#[test]
fn test_connectivity_validation() {
let (arena, dg) = build_dg(
&["A", "B", "C", "D"],
&[(0, 1), (0, 2), (1, 3), (2, 3)],
&[0],
&[3],
false,
);
let result = validate_connectivity(&arena, dg);
assert!(result.connected);
let (arena, dg) = build_dg(&["A", "B", "C"], &[(0, 1)], &[0], &[1], false);
let result = validate_connectivity(&arena, dg);
assert!(!result.connected);
assert!(result.unreachable_nodes.contains(&2));
}
#[test]
fn test_acyclicity_validation() {
let (arena, dg) = build_dg(&["A", "B", "C"], &[(0, 1), (1, 2)], &[0], &[2], false);
let result = validate_acyclicity(&arena, dg);
assert!(result.acyclic);
let (arena, dg) = build_dg(&["A", "B"], &[(0, 0)], &[0], &[1], false);
let result = validate_acyclicity(&arena, dg);
assert!(!result.acyclic);
let (arena, dg) = build_dg(
&["A", "B", "C"],
&[(0, 1), (1, 2), (2, 0)],
&[0],
&[2],
false,
);
let result = validate_acyclicity(&arena, dg);
assert!(!result.acyclic);
}
#[test]
fn test_soundness_report() {
let (arena, dg) = build_dg(
&["A", "B", "C", "D"],
&[(0, 1), (0, 2), (1, 3), (2, 3)],
&[0],
&[3],
false,
);
let report = get_soundness_report(&arena, dg);
assert!(report.sound);
assert!(report.has_start_nodes && report.has_end_nodes);
let (arena, dg) = build_dg(
&["A", "B", "C"],
&[(0, 1), (1, 2), (2, 0)],
&[0],
&[2],
false,
);
let report = get_soundness_report(&arena, dg);
assert!(!report.sound);
let (arena, dg) = build_dg(&["A", "B", "C"], &[(0, 1)], &[], &[1], false);
let report = get_soundness_report(&arena, dg);
assert!(!report.sound);
}
}