1use crate::witness::{EvaluationStatus, Evidence, EvidenceNodeRef, EvidenceRun};
47use oxrdf::Term;
48use rustc_hash::FxHashMap;
49use shifty_algebra::ShapeId;
50use std::hash::{Hash, Hasher};
51
52const PAYLOAD_CAP: usize = 32;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65enum Statement {
66 Normalized(usize),
69 Authored(usize),
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75pub struct ResultSharing {
76 pub authored_records: usize,
78 pub normalized_requests: usize,
80 pub duplicate_records: usize,
82 pub divergent_duplicates: usize,
85
86 pub occurrences: usize,
88 pub canonical_occurrences: usize,
91 pub distinct_payloads: usize,
93 pub shared_payloads: usize,
95 pub shared_canonical_occurrences: usize,
97 pub request_reaches: usize,
99 pub max_payload_requests: usize,
101
102 pub distinct_keys: usize,
104 pub multi_occurrence_keys: usize,
106 pub divergent_keys: usize,
108 pub divergent_occurrences: usize,
110 pub distinct_payloads_per_key: usize,
112 pub keys_over_payload_cap: usize,
114 pub both_polarity_addresses: usize,
118}
119
120impl ResultSharing {
121 pub fn payload_redundancy(&self) -> f64 {
124 ratio(self.occurrences, self.distinct_payloads)
125 }
126
127 pub fn key_redundancy(&self) -> f64 {
131 ratio(self.occurrences, self.distinct_keys)
132 }
133
134 pub fn shared_payload_fraction(&self) -> f64 {
136 fraction(self.shared_payloads, self.distinct_payloads)
137 }
138
139 pub fn shared_occurrence_fraction(&self) -> f64 {
142 fraction(
143 self.shared_canonical_occurrences,
144 self.canonical_occurrences,
145 )
146 }
147
148 pub fn requests_per_payload(&self) -> f64 {
151 ratio(self.request_reaches, self.distinct_payloads)
152 }
153
154 pub fn divergence_fraction(&self) -> f64 {
158 fraction(self.divergent_keys, self.multi_occurrence_keys)
159 }
160}
161
162fn ratio(occurrences: usize, distinct: usize) -> f64 {
163 if distinct == 0 {
164 1.0
165 } else {
166 occurrences as f64 / distinct as f64
167 }
168}
169
170fn fraction(part: usize, whole: usize) -> f64 {
171 if whole == 0 {
172 0.0
173 } else {
174 part as f64 / whole as f64
175 }
176}
177
178#[derive(Clone, Copy)]
186struct Payload<'a>(EvidenceNodeRef<'a>);
187
188impl PartialEq for Payload<'_> {
189 fn eq(&self, other: &Self) -> bool {
190 match (self.0, other.0) {
191 (EvidenceNodeRef::Satisfaction(left), EvidenceNodeRef::Satisfaction(right)) => {
192 left == right
193 }
194 (EvidenceNodeRef::Failure(left), EvidenceNodeRef::Failure(right)) => left == right,
195 _ => false,
196 }
197 }
198}
199
200impl Eq for Payload<'_> {}
201
202impl Hash for Payload<'_> {
203 fn hash<H: Hasher>(&self, state: &mut H) {
204 match self.0 {
205 EvidenceNodeRef::Satisfaction(value) => {
206 state.write_u8(0);
207 value.hash(state);
208 }
209 EvidenceNodeRef::Failure(value) => {
210 state.write_u8(1);
211 value.hash(state);
212 }
213 }
214 }
215}
216
217type Key<'a> = (ShapeId, Option<&'a Term>, EvaluationStatus);
219
220#[derive(Default)]
221struct KeyStats {
222 occurrences: usize,
223 payloads: Vec<u32>,
224 over_cap: bool,
225}
226
227#[derive(Default)]
229struct Payloads {
230 occurrences: Vec<u32>,
231 canonical_occurrences: Vec<u32>,
232 last_request: Vec<u32>,
236 requests: Vec<u32>,
237}
238
239impl Payloads {
240 fn push(&mut self) {
241 self.occurrences.push(0);
242 self.canonical_occurrences.push(0);
243 self.last_request.push(u32::MAX);
244 self.requests.push(0);
245 }
246}
247
248pub fn result_sharing(run: &EvidenceRun) -> ResultSharing {
253 let mut request_ids: FxHashMap<(Statement, &Term), u32> = FxHashMap::default();
256 let mut records: Vec<(u32, &Evidence)> = Vec::new();
257 for statement in &run.statements {
258 let which = statement
259 .normalized_statement_id
260 .map_or(Statement::Authored(statement.source_statement_id), |id| {
261 Statement::Normalized(id)
262 });
263 for focus in &statement.selected_foci {
264 let next = request_ids.len() as u32;
265 let id = *request_ids.entry((which, &focus.focus)).or_insert(next);
266 records.push((id, &focus.evidence));
267 }
268 }
269 records.sort_by_key(|(request, _)| *request);
274
275 let mut payload_ids: FxHashMap<Payload<'_>, u32> = FxHashMap::default();
276 let mut payloads = Payloads::default();
277 let mut keys: FxHashMap<Key<'_>, KeyStats> = FxHashMap::default();
278
279 let mut measured = ResultSharing {
280 authored_records: records.len(),
281 normalized_requests: request_ids.len(),
282 ..ResultSharing::default()
283 };
284 let mut canonical: Vec<&Evidence> = Vec::with_capacity(request_ids.len());
285 let mut current = None;
286
287 for (request, evidence) in records {
288 let first = current != Some(request);
289 current = Some(request);
290 if first {
291 canonical.push(evidence);
292 } else {
293 measured.duplicate_records += 1;
294 if canonical[request as usize] != evidence {
295 measured.divergent_duplicates += 1;
296 }
297 }
298
299 for node in evidence.walk() {
300 let next = payload_ids.len() as u32;
301 let payload = *payload_ids.entry(Payload(node)).or_insert_with(|| {
302 payloads.push();
303 next
304 });
305 let slot = payload as usize;
306
307 payloads.occurrences[slot] += 1;
308 if first {
309 payloads.canonical_occurrences[slot] += 1;
310 }
311 if payloads.last_request[slot] != request {
312 payloads.last_request[slot] = request;
313 payloads.requests[slot] += 1;
314 }
315
316 let stats = keys
317 .entry((node.constraint_id(), node.node(), node.status()))
318 .or_default();
319 stats.occurrences += 1;
320 if !stats.payloads.contains(&payload) {
321 if stats.payloads.len() < PAYLOAD_CAP {
322 stats.payloads.push(payload);
323 } else {
324 stats.over_cap = true;
325 }
326 }
327 }
328 }
329
330 measured.distinct_payloads = payloads.occurrences.len();
331 measured.occurrences = payloads
332 .occurrences
333 .iter()
334 .map(|&count| count as usize)
335 .sum();
336 measured.canonical_occurrences = payloads
337 .canonical_occurrences
338 .iter()
339 .map(|&count| count as usize)
340 .sum();
341 for (slot, &requests) in payloads.requests.iter().enumerate() {
342 measured.request_reaches += requests as usize;
343 measured.max_payload_requests = measured.max_payload_requests.max(requests as usize);
344 if requests >= 2 {
345 measured.shared_payloads += 1;
346 measured.shared_canonical_occurrences += payloads.canonical_occurrences[slot] as usize;
347 }
348 }
349
350 let mut polarities: FxHashMap<(ShapeId, Option<&Term>), u8> = FxHashMap::default();
353 measured.distinct_keys = keys.len();
354 for ((constraint, node, status), stats) in &keys {
355 if stats.occurrences >= 2 {
356 measured.multi_occurrence_keys += 1;
357 }
358 if stats.payloads.len() >= 2 {
359 measured.divergent_keys += 1;
360 measured.divergent_occurrences += stats.occurrences;
361 }
362 measured.distinct_payloads_per_key += stats.payloads.len();
363 measured.keys_over_payload_cap += usize::from(stats.over_cap);
364
365 let seen = polarities.entry((*constraint, *node)).or_default();
366 *seen |= match status {
367 EvaluationStatus::Pass => 1,
368 EvaluationStatus::Fail => 2,
369 };
370 }
371 measured.both_polarity_addresses = polarities.values().filter(|&&seen| seen == 3).count();
372
373 measured
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::validate_with_evidence;
380 use shifty_parse::{load_turtle, parse_turtle};
381
382 const TTL: &str = r#"
386 @prefix sh: <http://www.w3.org/ns/shacl#> .
387 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
388 @prefix ex: <http://ex/> .
389 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
390 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] ;
391 sh:property [ sh:path ex:n ; sh:datatype xsd:integer ] .
392 ex:T2 a sh:NodeShape ; sh:targetClass ex:T ;
393 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
394 ex:good a ex:T ; ex:p ex:c1 ; ex:n 3 .
395 ex:bad a ex:T ; ex:n "not a number" .
396 ex:c1 a ex:C .
397 "#;
398
399 fn run() -> EvidenceRun {
400 let parsed = parse_turtle(TTL.as_bytes(), None).unwrap();
401 let loaded = load_turtle(TTL.as_bytes(), None).unwrap();
402 validate_with_evidence(&loaded.graph, &parsed.schema).unwrap()
403 }
404
405 #[test]
409 fn the_typed_pass_agrees_with_the_encoder() {
410 let original = run();
411 let measured = result_sharing(&original);
412 let encoded = crate::compact::sharing(&original).unwrap();
413
414 assert_eq!(measured.occurrences, original.walk().len());
415 assert_eq!(measured.occurrences, encoded.result_occurrences);
416 assert_eq!(measured.distinct_payloads, encoded.distinct_results);
417 }
418
419 #[test]
420 fn requests_collapse_authored_records_without_losing_them() {
421 let original = run();
422 let measured = result_sharing(&original);
423
424 assert_eq!(
425 measured.authored_records,
426 original
427 .statements
428 .iter()
429 .map(|statement| statement.selected_foci.len())
430 .sum::<usize>()
431 );
432 assert!(
433 measured.normalized_requests <= measured.authored_records,
434 "normalizing cannot invent requests"
435 );
436 assert_eq!(
437 measured.authored_records - measured.normalized_requests,
438 measured.duplicate_records
439 );
440 assert_eq!(
441 measured.divergent_duplicates, 0,
442 "records answering one request must agree"
443 );
444 }
445
446 #[test]
447 fn the_bracket_is_ordered() {
448 let measured = result_sharing(&run());
449
450 assert!(measured.distinct_keys <= measured.distinct_payloads);
454 assert!(measured.key_redundancy() >= measured.payload_redundancy());
455 assert!(measured.payload_redundancy() >= 1.0);
456
457 assert!(measured.canonical_occurrences <= measured.occurrences);
458 assert!(measured.shared_payloads <= measured.distinct_payloads);
459 assert!(measured.divergent_keys <= measured.multi_occurrence_keys);
460 assert_eq!(
461 measured.both_polarity_addresses, 0,
462 "one truth value per (constraint, node) per run"
463 );
464 }
465
466 #[test]
467 fn the_fixture_actually_shares() {
468 let measured = result_sharing(&run());
469 assert!(
470 measured.shared_payloads > 0,
471 "ex:S and ex:T2 reach the same judgments about the same nodes"
472 );
473 assert!(measured.requests_per_payload() > 1.0);
474 }
475
476 #[test]
477 fn sharing_of_nothing_is_one() {
478 let empty = ResultSharing::default();
479 assert_eq!(empty.payload_redundancy(), 1.0);
480 assert_eq!(empty.key_redundancy(), 1.0);
481 assert_eq!(empty.requests_per_payload(), 1.0);
482 assert_eq!(empty.divergence_fraction(), 0.0);
483 assert_eq!(empty.shared_payload_fraction(), 0.0);
484 }
485}