use crate::witness::{EvaluationStatus, Evidence, EvidenceNodeRef, EvidenceRun};
use oxrdf::Term;
use rustc_hash::FxHashMap;
use shifty_algebra::ShapeId;
use std::hash::{Hash, Hasher};
const PAYLOAD_CAP: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Statement {
Normalized(usize),
Authored(usize),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResultSharing {
pub authored_records: usize,
pub normalized_requests: usize,
pub duplicate_records: usize,
pub divergent_duplicates: usize,
pub occurrences: usize,
pub canonical_occurrences: usize,
pub distinct_payloads: usize,
pub shared_payloads: usize,
pub shared_canonical_occurrences: usize,
pub request_reaches: usize,
pub max_payload_requests: usize,
pub distinct_keys: usize,
pub multi_occurrence_keys: usize,
pub divergent_keys: usize,
pub divergent_occurrences: usize,
pub distinct_payloads_per_key: usize,
pub keys_over_payload_cap: usize,
pub both_polarity_addresses: usize,
}
impl ResultSharing {
pub fn payload_redundancy(&self) -> f64 {
ratio(self.occurrences, self.distinct_payloads)
}
pub fn key_redundancy(&self) -> f64 {
ratio(self.occurrences, self.distinct_keys)
}
pub fn shared_payload_fraction(&self) -> f64 {
fraction(self.shared_payloads, self.distinct_payloads)
}
pub fn shared_occurrence_fraction(&self) -> f64 {
fraction(
self.shared_canonical_occurrences,
self.canonical_occurrences,
)
}
pub fn requests_per_payload(&self) -> f64 {
ratio(self.request_reaches, self.distinct_payloads)
}
pub fn divergence_fraction(&self) -> f64 {
fraction(self.divergent_keys, self.multi_occurrence_keys)
}
}
fn ratio(occurrences: usize, distinct: usize) -> f64 {
if distinct == 0 {
1.0
} else {
occurrences as f64 / distinct as f64
}
}
fn fraction(part: usize, whole: usize) -> f64 {
if whole == 0 {
0.0
} else {
part as f64 / whole as f64
}
}
#[derive(Clone, Copy)]
struct Payload<'a>(EvidenceNodeRef<'a>);
impl PartialEq for Payload<'_> {
fn eq(&self, other: &Self) -> bool {
match (self.0, other.0) {
(EvidenceNodeRef::Satisfaction(left), EvidenceNodeRef::Satisfaction(right)) => {
left == right
}
(EvidenceNodeRef::Failure(left), EvidenceNodeRef::Failure(right)) => left == right,
_ => false,
}
}
}
impl Eq for Payload<'_> {}
impl Hash for Payload<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self.0 {
EvidenceNodeRef::Satisfaction(value) => {
state.write_u8(0);
value.hash(state);
}
EvidenceNodeRef::Failure(value) => {
state.write_u8(1);
value.hash(state);
}
}
}
}
type Key<'a> = (ShapeId, Option<&'a Term>, EvaluationStatus);
#[derive(Default)]
struct KeyStats {
occurrences: usize,
payloads: Vec<u32>,
over_cap: bool,
}
#[derive(Default)]
struct Payloads {
occurrences: Vec<u32>,
canonical_occurrences: Vec<u32>,
last_request: Vec<u32>,
requests: Vec<u32>,
}
impl Payloads {
fn push(&mut self) {
self.occurrences.push(0);
self.canonical_occurrences.push(0);
self.last_request.push(u32::MAX);
self.requests.push(0);
}
}
pub fn result_sharing(run: &EvidenceRun) -> ResultSharing {
let mut request_ids: FxHashMap<(Statement, &Term), u32> = FxHashMap::default();
let mut records: Vec<(u32, &Evidence)> = Vec::new();
for statement in &run.statements {
let which = statement
.normalized_statement_id
.map_or(Statement::Authored(statement.source_statement_id), |id| {
Statement::Normalized(id)
});
for focus in &statement.selected_foci {
let next = request_ids.len() as u32;
let id = *request_ids.entry((which, &focus.focus)).or_insert(next);
records.push((id, &focus.evidence));
}
}
records.sort_by_key(|(request, _)| *request);
let mut payload_ids: FxHashMap<Payload<'_>, u32> = FxHashMap::default();
let mut payloads = Payloads::default();
let mut keys: FxHashMap<Key<'_>, KeyStats> = FxHashMap::default();
let mut measured = ResultSharing {
authored_records: records.len(),
normalized_requests: request_ids.len(),
..ResultSharing::default()
};
let mut canonical: Vec<&Evidence> = Vec::with_capacity(request_ids.len());
let mut current = None;
for (request, evidence) in records {
let first = current != Some(request);
current = Some(request);
if first {
canonical.push(evidence);
} else {
measured.duplicate_records += 1;
if canonical[request as usize] != evidence {
measured.divergent_duplicates += 1;
}
}
for node in evidence.walk() {
let next = payload_ids.len() as u32;
let payload = *payload_ids.entry(Payload(node)).or_insert_with(|| {
payloads.push();
next
});
let slot = payload as usize;
payloads.occurrences[slot] += 1;
if first {
payloads.canonical_occurrences[slot] += 1;
}
if payloads.last_request[slot] != request {
payloads.last_request[slot] = request;
payloads.requests[slot] += 1;
}
let stats = keys
.entry((node.constraint_id(), node.node(), node.status()))
.or_default();
stats.occurrences += 1;
if !stats.payloads.contains(&payload) {
if stats.payloads.len() < PAYLOAD_CAP {
stats.payloads.push(payload);
} else {
stats.over_cap = true;
}
}
}
}
measured.distinct_payloads = payloads.occurrences.len();
measured.occurrences = payloads
.occurrences
.iter()
.map(|&count| count as usize)
.sum();
measured.canonical_occurrences = payloads
.canonical_occurrences
.iter()
.map(|&count| count as usize)
.sum();
for (slot, &requests) in payloads.requests.iter().enumerate() {
measured.request_reaches += requests as usize;
measured.max_payload_requests = measured.max_payload_requests.max(requests as usize);
if requests >= 2 {
measured.shared_payloads += 1;
measured.shared_canonical_occurrences += payloads.canonical_occurrences[slot] as usize;
}
}
let mut polarities: FxHashMap<(ShapeId, Option<&Term>), u8> = FxHashMap::default();
measured.distinct_keys = keys.len();
for ((constraint, node, status), stats) in &keys {
if stats.occurrences >= 2 {
measured.multi_occurrence_keys += 1;
}
if stats.payloads.len() >= 2 {
measured.divergent_keys += 1;
measured.divergent_occurrences += stats.occurrences;
}
measured.distinct_payloads_per_key += stats.payloads.len();
measured.keys_over_payload_cap += usize::from(stats.over_cap);
let seen = polarities.entry((*constraint, *node)).or_default();
*seen |= match status {
EvaluationStatus::Pass => 1,
EvaluationStatus::Fail => 2,
};
}
measured.both_polarity_addresses = polarities.values().filter(|&&seen| seen == 3).count();
measured
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate_with_evidence;
use shifty_parse::{load_turtle, parse_turtle};
const TTL: &str = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://ex/> .
ex:S a sh:NodeShape ; sh:targetClass ex:T ;
sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] ;
sh:property [ sh:path ex:n ; sh:datatype xsd:integer ] .
ex:T2 a sh:NodeShape ; sh:targetClass ex:T ;
sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
ex:good a ex:T ; ex:p ex:c1 ; ex:n 3 .
ex:bad a ex:T ; ex:n "not a number" .
ex:c1 a ex:C .
"#;
fn run() -> EvidenceRun {
let parsed = parse_turtle(TTL.as_bytes(), None).unwrap();
let loaded = load_turtle(TTL.as_bytes(), None).unwrap();
validate_with_evidence(&loaded.graph, &parsed.schema).unwrap()
}
#[test]
fn the_typed_pass_agrees_with_the_encoder() {
let original = run();
let measured = result_sharing(&original);
let encoded = crate::compact::sharing(&original).unwrap();
assert_eq!(measured.occurrences, original.walk().len());
assert_eq!(measured.occurrences, encoded.result_occurrences);
assert_eq!(measured.distinct_payloads, encoded.distinct_results);
}
#[test]
fn requests_collapse_authored_records_without_losing_them() {
let original = run();
let measured = result_sharing(&original);
assert_eq!(
measured.authored_records,
original
.statements
.iter()
.map(|statement| statement.selected_foci.len())
.sum::<usize>()
);
assert!(
measured.normalized_requests <= measured.authored_records,
"normalizing cannot invent requests"
);
assert_eq!(
measured.authored_records - measured.normalized_requests,
measured.duplicate_records
);
assert_eq!(
measured.divergent_duplicates, 0,
"records answering one request must agree"
);
}
#[test]
fn the_bracket_is_ordered() {
let measured = result_sharing(&run());
assert!(measured.distinct_keys <= measured.distinct_payloads);
assert!(measured.key_redundancy() >= measured.payload_redundancy());
assert!(measured.payload_redundancy() >= 1.0);
assert!(measured.canonical_occurrences <= measured.occurrences);
assert!(measured.shared_payloads <= measured.distinct_payloads);
assert!(measured.divergent_keys <= measured.multi_occurrence_keys);
assert_eq!(
measured.both_polarity_addresses, 0,
"one truth value per (constraint, node) per run"
);
}
#[test]
fn the_fixture_actually_shares() {
let measured = result_sharing(&run());
assert!(
measured.shared_payloads > 0,
"ex:S and ex:T2 reach the same judgments about the same nodes"
);
assert!(measured.requests_per_payload() > 1.0);
}
#[test]
fn sharing_of_nothing_is_one() {
let empty = ResultSharing::default();
assert_eq!(empty.payload_redundancy(), 1.0);
assert_eq!(empty.key_redundancy(), 1.0);
assert_eq!(empty.requests_per_payload(), 1.0);
assert_eq!(empty.divergence_fraction(), 0.0);
assert_eq!(empty.shared_payload_fraction(), 0.0);
}
}