use std::collections::HashMap;
use panproto_gat::Name;
use panproto_schema::{Edge, Schema};
#[doc(hidden)]
#[must_use]
pub fn reference_quality(
vertex_map: &HashMap<Name, Name>,
edge_map: &HashMap<Edge, Edge>,
src: &Schema,
tgt: &Schema,
weights: [f64; 4],
) -> f64 {
if vertex_map.is_empty() {
return 1.0;
}
let mut vm_pairs: Vec<(&Name, &Name)> = vertex_map.iter().collect();
vm_pairs.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
let name_score: f64 = {
let mut total = 0.0;
for (src_id, tgt_id) in &vm_pairs {
let dist = edit_distance(src_id.as_str(), tgt_id.as_str());
let max_len = src_id.len().max(tgt_id.len()).max(1);
#[allow(clippy::cast_precision_loss)]
{
total += 1.0 - (dist as f64 / max_len as f64);
}
}
#[allow(clippy::cast_precision_loss)]
{
total / vertex_map.len() as f64
}
};
let edge_score: f64 = if edge_map.is_empty() {
1.0
} else {
let matching = edge_map
.iter()
.filter(|(src_e, tgt_e)| src_e.name == tgt_e.name)
.count();
#[allow(clippy::cast_precision_loss)]
{
matching as f64 / edge_map.len() as f64
}
};
let prop_score: f64 = {
let mut total = 0.0;
let mut count = 0;
for (src_id, tgt_id) in &vm_pairs {
let src_names: std::collections::HashSet<&str> = src
.outgoing_edges(src_id)
.iter()
.filter_map(|e| e.name.as_deref())
.collect();
let tgt_names: std::collections::HashSet<&str> = tgt
.outgoing_edges(tgt_id)
.iter()
.filter_map(|e| e.name.as_deref())
.collect();
if !src_names.is_empty() || !tgt_names.is_empty() {
let intersection = src_names.intersection(&tgt_names).count();
let union = src_names.union(&tgt_names).count();
if union > 0 {
#[allow(clippy::cast_precision_loss)]
{
total += intersection as f64 / union as f64;
}
count += 1;
}
}
}
if count > 0 {
total / f64::from(count)
} else {
1.0
}
};
let degree_score: f64 = {
let mut total = 0.0;
for (src_id, tgt_id) in &vm_pairs {
let src_deg = src.outgoing_edges(src_id).len();
let tgt_deg = tgt.outgoing_edges(tgt_id).len();
let max_deg = src_deg.max(tgt_deg);
if max_deg > 0 {
let diff = src_deg.abs_diff(tgt_deg);
#[allow(clippy::cast_precision_loss)]
{
total += 1.0 - (diff as f64 / max_deg as f64);
}
} else {
total += 1.0;
}
}
#[allow(clippy::cast_precision_loss)]
{
total / vertex_map.len() as f64
}
};
#[allow(clippy::suboptimal_flops)]
let score = weights[0] * name_score
+ weights[1] * edge_score
+ weights[2] * prop_score
+ weights[3] * degree_score;
score
}
pub(crate) fn edit_distance(a: &str, b: &str) -> usize {
let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
let m = a_bytes.len();
let n = b_bytes.len();
let mut prev = (0..=n).collect::<Vec<_>>();
let mut curr = vec![0; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use panproto_schema::{Protocol, SchemaBuilder};
const WEIGHTS: [f64; 4] = [0.25, 0.25, 0.30, 0.20];
fn test_protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["object".into(), "string".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
fn two_vertex_schema() -> Schema {
let protocol = test_protocol();
SchemaBuilder::new(&protocol)
.vertex("root", "object", None::<&str>)
.unwrap()
.vertex("root.label", "string", None::<&str>)
.unwrap()
.edge("root", "root.label", "prop", Some("label"))
.unwrap()
.build()
.unwrap()
}
#[test]
fn edit_distance_is_a_metric_on_short_strings() {
assert_eq!(edit_distance("", ""), 0);
assert_eq!(edit_distance("abc", "abc"), 0);
assert_eq!(edit_distance("abc", ""), 3);
assert_eq!(edit_distance("", "abc"), 3);
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("flaw", "lawn"), 2);
}
#[test]
fn an_empty_vertex_map_scores_one() {
let schema = two_vertex_schema();
let quality =
reference_quality(&HashMap::new(), &HashMap::new(), &schema, &schema, WEIGHTS);
assert_eq!(quality, 1.0);
}
#[test]
fn the_identity_morphism_scores_one() {
let schema = two_vertex_schema();
let vertex_map: HashMap<Name, Name> = schema
.vertices
.keys()
.map(|id| (id.clone(), id.clone()))
.collect();
let edge_map: HashMap<Edge, Edge> = schema
.edges
.keys()
.map(|edge| (edge.clone(), edge.clone()))
.collect();
let quality = reference_quality(&vertex_map, &edge_map, &schema, &schema, WEIGHTS);
assert!((quality - 1.0).abs() < 1e-12, "identity scored {quality}");
}
}