use std::collections::{HashMap, HashSet, VecDeque};
use molrs::SmartsPattern;
use super::meta::OplsTypingMeta;
#[derive(Debug, Clone)]
pub struct OplsDependencyAnalyzer {
dependencies: HashMap<String, HashSet<String>>,
levels: HashMap<String, usize>,
circular_groups: Vec<HashSet<String>>,
}
impl OplsDependencyAnalyzer {
pub fn new(meta: &OplsTypingMeta) -> Self {
let mut pattern_types: HashSet<String> = HashSet::new();
let mut raw_deps: HashMap<String, HashSet<String>> = HashMap::new();
for (name, row) in meta.iter() {
let Some(def) = row.def.as_deref() else {
continue;
};
let Ok(pat) = SmartsPattern::parse(def) else {
pattern_types.insert(name.clone());
raw_deps.insert(name.clone(), HashSet::new());
continue;
};
pattern_types.insert(name.clone());
let deps: HashSet<String> = pat
.context_labels()
.into_iter()
.filter(|l| l.starts_with("opls_"))
.collect();
raw_deps.insert(name.clone(), deps);
}
let dependencies: HashMap<String, HashSet<String>> = raw_deps
.into_iter()
.map(|(name, deps)| {
let valid = deps
.into_iter()
.filter(|d| pattern_types.contains(d))
.collect();
(name, valid)
})
.collect();
let (levels, circular_groups) = compute_levels(&dependencies);
Self {
dependencies,
levels,
circular_groups,
}
}
pub fn dependencies_of(&self, name: &str) -> Option<&HashSet<String>> {
self.dependencies.get(name)
}
pub fn level(&self, name: &str) -> Option<usize> {
self.levels.get(name).copied()
}
pub fn max_level(&self) -> Option<usize> {
self.levels.values().copied().max()
}
pub fn types_at_level(&self, level: usize) -> Vec<String> {
self.levels
.iter()
.filter(|&(_, &l)| l == level)
.map(|(n, _)| n.clone())
.collect()
}
pub fn circular_groups(&self) -> &[HashSet<String>] {
&self.circular_groups
}
pub fn is_circular(&self, name: &str) -> bool {
self.circular_groups.iter().any(|g| g.contains(name))
}
}
fn compute_levels(
dependencies: &HashMap<String, HashSet<String>>,
) -> (HashMap<String, usize>, Vec<HashSet<String>>) {
let mut levels: HashMap<String, usize> = HashMap::new();
let mut in_degree: HashMap<String, usize> = dependencies
.iter()
.map(|(name, deps)| (name.clone(), deps.len()))
.collect();
let mut queue: VecDeque<String> = dependencies
.iter()
.filter(|(_, deps)| deps.is_empty())
.map(|(name, _)| name.clone())
.collect();
let mut processed: HashSet<String> = HashSet::new();
let mut current_level = 0usize;
while !queue.is_empty() {
let level_size = queue.len();
for _ in 0..level_size {
let name = queue.pop_front().unwrap();
levels.insert(name.clone(), current_level);
processed.insert(name.clone());
for (other, deps) in dependencies.iter() {
if deps.contains(&name)
&& let Some(d) = in_degree.get_mut(other)
{
*d -= 1;
if *d == 0 && !processed.contains(other) {
queue.push_back(other.clone());
}
}
}
}
current_level += 1;
}
let unprocessed: HashSet<String> = dependencies
.keys()
.filter(|n| !processed.contains(*n))
.cloned()
.collect();
let mut circular_groups = Vec::new();
if !unprocessed.is_empty() {
circular_groups = detect_circular_groups(dependencies, &unprocessed);
let max_level = levels.values().copied().max().map_or(-1i64, |m| m as i64);
let cycle_level = (max_level + 1) as usize;
for group in &circular_groups {
for name in group {
levels.insert(name.clone(), cycle_level);
}
}
}
(levels, circular_groups)
}
fn detect_circular_groups(
dependencies: &HashMap<String, HashSet<String>>,
unprocessed: &HashSet<String>,
) -> Vec<HashSet<String>> {
struct Tarjan<'a> {
deps: &'a HashMap<String, HashSet<String>>,
unprocessed: &'a HashSet<String>,
index_counter: usize,
index: HashMap<String, usize>,
lowlink: HashMap<String, usize>,
stack: Vec<String>,
on_stack: HashSet<String>,
groups: Vec<HashSet<String>>,
}
impl Tarjan<'_> {
fn strongconnect(&mut self, node: &str) {
self.index.insert(node.to_string(), self.index_counter);
self.lowlink.insert(node.to_string(), self.index_counter);
self.index_counter += 1;
self.stack.push(node.to_string());
self.on_stack.insert(node.to_string());
if let Some(succs) = self.deps.get(node) {
let mut succs: Vec<&String> = succs.iter().collect();
succs.sort();
for other in succs {
if !self.unprocessed.contains(other) {
continue;
}
if !self.index.contains_key(other) {
self.strongconnect(other);
let lo = self.lowlink[other];
let cur = self.lowlink[node];
self.lowlink.insert(node.to_string(), cur.min(lo));
} else if self.on_stack.contains(other) {
let oi = self.index[other];
let cur = self.lowlink[node];
self.lowlink.insert(node.to_string(), cur.min(oi));
}
}
}
if self.lowlink[node] == self.index[node] {
let mut component = HashSet::new();
loop {
let w = self.stack.pop().unwrap();
self.on_stack.remove(&w);
let is_root = w == node;
component.insert(w);
if is_root {
break;
}
}
if component.len() > 1 {
self.groups.push(component);
}
}
}
}
let mut t = Tarjan {
deps: dependencies,
unprocessed,
index_counter: 0,
index: HashMap::new(),
lowlink: HashMap::new(),
stack: Vec::new(),
on_stack: HashSet::new(),
groups: Vec::new(),
};
let mut nodes: Vec<&String> = unprocessed.iter().collect();
nodes.sort();
for node in nodes {
if !t.index.contains_key(node) {
t.strongconnect(node);
}
}
t.groups
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ff::typifier::opls::meta::OplsTypeRow;
fn row(def: Option<&str>) -> OplsTypeRow {
OplsTypeRow {
class: "X".to_string(),
def: def.map(str::to_string),
overrides: Vec::new(),
priority: None,
layer: 0,
}
}
fn meta_with(rows: &[(&str, Option<&str>)]) -> OplsTypingMeta {
let mut m = OplsTypingMeta::new();
for (name, def) in rows {
m.insert(*name, row(*def));
}
m
}
#[test]
fn extracts_percent_dependencies() {
let meta = meta_with(&[
(
"opls_145",
Some("[C;X3;r6]1[C;X3;r6][C;X3;r6][C;X3;r6][C;X3;r6][C;X3;r6]1"),
),
("opls_146", Some("H[C;%opls_145]")),
]);
let a = OplsDependencyAnalyzer::new(&meta);
assert!(a.dependencies_of("opls_145").unwrap().is_empty());
let d146 = a.dependencies_of("opls_146").unwrap();
assert_eq!(d146.len(), 1);
assert!(d146.contains("opls_145"));
}
#[test]
fn dependency_on_legacy_nodef_type_is_dropped() {
let meta = meta_with(&[
("opls_999", None), ("opls_500", Some("H[C;%opls_999]")),
]);
let a = OplsDependencyAnalyzer::new(&meta);
assert!(a.dependencies_of("opls_500").unwrap().is_empty());
assert_eq!(a.level("opls_500"), Some(0));
assert_eq!(a.level("opls_999"), None);
}
#[test]
fn kahn_levels_three_chain() {
let meta = meta_with(&[
("opls_a", Some("[C;X4]")),
("opls_b", Some("H[C;%opls_a]")),
("opls_c", Some("[O;%opls_b]")),
]);
let a = OplsDependencyAnalyzer::new(&meta);
assert_eq!(a.level("opls_a"), Some(0));
assert_eq!(a.level("opls_b"), Some(1));
assert_eq!(a.level("opls_c"), Some(2));
assert_eq!(a.max_level(), Some(2));
assert_eq!(a.types_at_level(0), vec!["opls_a".to_string()]);
assert!(a.circular_groups().is_empty());
}
#[test]
fn tarjan_detects_two_cycle_at_max_level_plus_one() {
let meta = meta_with(&[
("opls_base", Some("[C;X4]")),
("opls_x", Some("[C;%opls_base][O;%opls_y]")),
("opls_y", Some("[N;%opls_x]")),
]);
let a = OplsDependencyAnalyzer::new(&meta);
assert_eq!(a.level("opls_base"), Some(0));
assert_eq!(a.circular_groups().len(), 1);
let grp = &a.circular_groups()[0];
assert!(grp.contains("opls_x") && grp.contains("opls_y"));
assert_eq!(grp.len(), 2);
assert!(a.is_circular("opls_x") && a.is_circular("opls_y"));
assert_eq!(a.level("opls_x"), Some(1));
assert_eq!(a.level("opls_y"), Some(1));
assert!(!a.is_circular("opls_base"));
}
#[test]
fn no_deps_all_level_zero() {
let meta = meta_with(&[
("opls_135", Some("[C;X4](C)(H)(H)H")),
("opls_140", Some("H[C;X4]")),
]);
let a = OplsDependencyAnalyzer::new(&meta);
assert_eq!(a.level("opls_135"), Some(0));
assert_eq!(a.level("opls_140"), Some(0));
assert_eq!(a.max_level(), Some(0));
}
}