qualia_core_db/sparql_library/
vision_shacl.rs1use crate::q_hash;
7use crate::NQuin;
8
9pub const P_VISUAL_OBSERVATION: &str = "https://ns.webizen.org/q42/VisualObservation";
11pub const P_PROPOSES_CLASS: &str = "https://ns.webizen.org/q42/proposesClass";
12pub const P_HAS_BBOX: &str = "https://ns.webizen.org/q42/hasBoundingBox";
13pub const P_HAS_TRACK: &str = "https://ns.webizen.org/q42/hasTrackId";
14pub const P_MODEL_DIGEST: &str = "https://ns.webizen.org/q42/modelDigest";
15pub const P_HUMAN_REJECTS: &str = "https://ns.webizen.org/q42/humanRejects";
16pub const P_HUMAN_CORRECTS: &str = "https://ns.webizen.org/q42/humanCorrectsClass";
17
18#[repr(u8)]
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum VisionShaclViolation {
21 MissingModelDigest = 1,
22 ObservationWithoutClass = 2,
23 InvalidBbox = 3,
24 ScoreOutOfRange = 4,
25 EmptyGraph = 5,
26 OrphanClassProposal = 6,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct VisionShaclReport {
31 pub ok: bool,
32 pub observation_count: u32,
33 pub class_count: u32,
34 pub bbox_count: u32,
35 pub model_digest_count: u32,
36 pub human_attestation_count: u32,
37 pub violation: Option<VisionShaclViolation>,
38}
39
40impl VisionShaclReport {
41 pub const EMPTY_OK: Self = Self {
42 ok: false,
43 observation_count: 0,
44 class_count: 0,
45 bbox_count: 0,
46 model_digest_count: 0,
47 human_attestation_count: 0,
48 violation: Some(VisionShaclViolation::EmptyGraph),
49 };
50}
51
52#[inline]
53fn unpack_bbox(v: u64) -> (u16, u16, u16, u16) {
54 (
55 (v & 0xFFFF) as u16,
56 ((v >> 16) & 0xFFFF) as u16,
57 ((v >> 32) & 0xFFFF) as u16,
58 ((v >> 48) & 0xFFFF) as u16,
59 )
60}
61
62pub fn validate_vision_observation_graph(quins: &[NQuin]) -> VisionShaclReport {
64 if quins.is_empty() {
65 return VisionShaclReport::EMPTY_OK;
66 }
67
68 let p_obs = q_hash(P_VISUAL_OBSERVATION);
69 let p_class = q_hash(P_PROPOSES_CLASS);
70 let p_bbox = q_hash(P_HAS_BBOX);
71 let p_track = q_hash(P_HAS_TRACK);
72 let p_model = q_hash(P_MODEL_DIGEST);
73 let p_rej = q_hash(P_HUMAN_REJECTS);
74 let p_corr = q_hash(P_HUMAN_CORRECTS);
75
76 let mut report = VisionShaclReport {
77 ok: true,
78 observation_count: 0,
79 class_count: 0,
80 bbox_count: 0,
81 model_digest_count: 0,
82 human_attestation_count: 0,
83 violation: None,
84 };
85
86 let mut instances = [0u64; 64];
88 let mut n_inst = 0usize;
89
90 for q in quins {
91 if q.predicate == p_model {
92 report.model_digest_count = report.model_digest_count.saturating_add(1);
93 if q.object == 0 {
94 report.ok = false;
95 report.violation = Some(VisionShaclViolation::MissingModelDigest);
96 }
97 } else if q.predicate == p_obs {
98 report.observation_count = report.observation_count.saturating_add(1);
99 if n_inst < instances.len() {
100 instances[n_inst] = q.object;
101 n_inst += 1;
102 }
103 let score = (q.metadata & 0xFFFF) as u32;
105 if score > 65535 {
106 report.ok = false;
107 report.violation = Some(VisionShaclViolation::ScoreOutOfRange);
108 }
109 } else if q.predicate == p_class {
110 report.class_count = report.class_count.saturating_add(1);
111 if q.object == 0 {
112 report.ok = false;
113 report.violation = Some(VisionShaclViolation::ObservationWithoutClass);
114 }
115 } else if q.predicate == p_bbox {
116 report.bbox_count = report.bbox_count.saturating_add(1);
117 let (x0, y0, x1, y1) = unpack_bbox(q.object);
118 if x1 < x0 || y1 < y0 {
119 report.ok = false;
120 report.violation = Some(VisionShaclViolation::InvalidBbox);
121 }
122 } else if q.predicate == p_track {
123 } else if q.predicate == p_rej || q.predicate == p_corr {
125 report.human_attestation_count = report.human_attestation_count.saturating_add(1);
126 }
127 }
128
129 if report.model_digest_count == 0 && report.observation_count > 0 {
130 report.ok = false;
131 report.violation = Some(VisionShaclViolation::MissingModelDigest);
132 }
133
134 if report.ok && report.observation_count > 0 {
136 for i in 0..n_inst {
137 let inst = instances[i];
138 let has_class = quins
139 .iter()
140 .any(|q| q.predicate == p_class && q.subject == inst);
141 if !has_class {
142 report.ok = false;
143 report.violation = Some(VisionShaclViolation::ObservationWithoutClass);
144 break;
145 }
146 }
147 }
148
149 if report.ok && report.class_count > 0 && report.observation_count == 0 {
151 report.ok = false;
152 report.violation = Some(VisionShaclViolation::OrphanClassProposal);
153 }
154
155 report
156}
157
158pub fn vision_shape_predicate_hashes(out: &mut [u64]) -> usize {
160 let preds = [
161 q_hash(P_MODEL_DIGEST),
162 q_hash(P_VISUAL_OBSERVATION),
163 q_hash(P_PROPOSES_CLASS),
164 q_hash(P_HAS_BBOX),
165 q_hash(P_HAS_TRACK),
166 q_hash(P_HUMAN_REJECTS),
167 q_hash(P_HUMAN_CORRECTS),
168 ];
169 let n = preds.len().min(out.len());
170 out[..n].copy_from_slice(&preds[..n]);
171 n
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn q(s: u64, p: u64, o: u64, c: u64, m: u64) -> NQuin {
179 NQuin {
180 subject: s,
181 predicate: p,
182 object: o,
183 context: c,
184 metadata: m,
185 parity: s ^ p ^ o ^ c ^ m,
186 }
187 }
188
189 #[test]
190 fn valid_bundle_passes() {
191 let media = 9u64;
192 let inst = 2u64;
193 let model = 7u64;
194 let bbox = (0u64) | (0u64 << 16) | (1000u64 << 32) | (2000u64 << 48);
195 let quins = [
196 q(media, q_hash(P_MODEL_DIGEST), model, 1, 64),
197 q(media, q_hash(P_VISUAL_OBSERVATION), inst, 1, 1000),
198 q(inst, q_hash(P_PROPOSES_CLASS), 99, 1, 1000),
199 q(inst, q_hash(P_HAS_BBOX), bbox, 1, 1000),
200 ];
201 let r = validate_vision_observation_graph(&quins);
202 assert!(r.ok, "{r:?}");
203 assert_eq!(r.observation_count, 1);
204 assert_eq!(r.model_digest_count, 1);
205 }
206
207 #[test]
208 fn missing_digest_fails() {
209 let quins = [q(9, q_hash(P_VISUAL_OBSERVATION), 2, 1, 1000)];
210 let r = validate_vision_observation_graph(&quins);
211 assert!(!r.ok);
212 assert_eq!(r.violation, Some(VisionShaclViolation::MissingModelDigest));
213 }
214
215 #[test]
216 fn invalid_bbox_fails() {
217 let bbox = (5000u64) | (0u64 << 16) | (100u64 << 32) | (2000u64 << 48);
219 let quins = [
220 q(9, q_hash(P_MODEL_DIGEST), 7, 1, 1),
221 q(9, q_hash(P_VISUAL_OBSERVATION), 2, 1, 1),
222 q(2, q_hash(P_PROPOSES_CLASS), 99, 1, 1),
223 q(2, q_hash(P_HAS_BBOX), bbox, 1, 1),
224 ];
225 let r = validate_vision_observation_graph(&quins);
226 assert!(!r.ok);
227 assert_eq!(r.violation, Some(VisionShaclViolation::InvalidBbox));
228 }
229
230 #[test]
231 fn human_reject_does_not_require_erasing_machine() {
232 let quins = [
233 q(9, q_hash(P_MODEL_DIGEST), 7, 1, 1),
234 q(9, q_hash(P_VISUAL_OBSERVATION), 2, 1, 1),
235 q(2, q_hash(P_PROPOSES_CLASS), 99, 1, 1),
236 q(0xD1D, q_hash(P_HUMAN_REJECTS), 2, 1, 0),
237 ];
238 let r = validate_vision_observation_graph(&quins);
239 assert!(r.ok);
240 assert_eq!(r.human_attestation_count, 1);
241 assert_eq!(r.class_count, 1); }
243}