1use super::value::{
26 ExactnessClass as E, ExecutionClass as X, ImmersiveFunctionDescriptor as Desc,
27 ImmersiveValueKind as K, QispError,
28};
29use crate::tensor::Tensor10D;
30
31#[repr(C)]
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct TensorNeighbor {
36 pub index: u32,
37 pub distance: f32,
38}
39
40pub fn tensor_distance(left: &Tensor10D, right: &Tensor10D) -> Result<f32, QispError> {
43 let distance = left.full_distance(right);
44 if distance.is_finite() {
45 Ok(distance)
46 } else {
47 Err(QispError::ProfileMismatch)
48 }
49}
50
51pub fn tensor_within(left: &Tensor10D, right: &Tensor10D, radius: f32) -> Result<bool, QispError> {
54 if !radius.is_finite() || radius < 0.0 {
55 return Err(QispError::ProfileMismatch);
56 }
57 Ok(tensor_distance(left, right)? <= radius)
58}
59
60pub fn tensor_knn_into(
66 query: &Tensor10D,
67 candidates: &[Tensor10D],
68 k: usize,
69 out: &mut [TensorNeighbor],
70) -> Result<usize, QispError> {
71 if k > out.len() || k > u32::MAX as usize || candidates.len() > u32::MAX as usize {
72 return Err(QispError::BudgetExceeded);
73 }
74 if k == 0 || candidates.is_empty() {
75 return Ok(0);
76 }
77 let wanted = k.min(candidates.len());
78 for (index, candidate) in candidates.iter().enumerate() {
79 let distance = tensor_distance(query, candidate)?;
80 let item = TensorNeighbor {
81 index: index as u32,
82 distance,
83 };
84 let populated = index.min(wanted);
85 let mut pos = populated;
86 while pos > 0 {
87 let prev = out[pos - 1];
88 if prev.distance < distance || (prev.distance == distance && prev.index < item.index) {
89 break;
90 }
91 pos -= 1;
92 }
93 if pos < wanted {
94 let upper = populated.min(wanted - 1);
95 for slot in (pos..upper).rev() {
96 out[slot + 1] = out[slot];
97 }
98 out[pos] = item;
99 }
100 }
101 Ok(wanted)
102}
103
104#[derive(Debug, Clone, Copy)]
107pub struct FunctionEntry {
108 pub iri: &'static str,
110 pub descriptor: Desc,
112 pub defers_to: Option<&'static str>,
115}
116
117const fn entry(
119 iri: &'static str,
120 args: [K; 4],
121 arg_count: u8,
122 result: K,
123 execution: X,
124 deterministic: bool,
125 exactness: E,
126 max_input_bytes: u32,
127 max_output_bytes: u32,
128 defers_to: Option<&'static str>,
129) -> FunctionEntry {
130 FunctionEntry {
131 iri,
132 descriptor: Desc::new(
133 crate::q_hash(iri),
134 args,
135 arg_count,
136 result,
137 execution,
138 deterministic,
139 exactness,
140 max_input_bytes,
141 max_output_bytes,
142 ),
143 defers_to,
144 }
145}
146
147const MB: u32 = 1 << 20;
150
151pub const FUNCTIONS: &[FunctionEntry] = &[
156 entry(
158 "https://webizen.org/immersive/function/0.1#intersects",
159 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
160 2,
161 K::Boolean,
162 X::HotZeroHeap,
163 true,
164 E::Exact,
165 MB,
166 0,
167 Some("http://www.opengis.net/def/function/geosparql/sfIntersects"),
168 ),
169 entry(
170 "https://webizen.org/immersive/function/0.1#contains",
171 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
172 2,
173 K::Boolean,
174 X::HotZeroHeap,
175 true,
176 E::Exact,
177 MB,
178 0,
179 Some("http://www.opengis.net/def/function/geosparql/sfContains"),
180 ),
181 entry(
182 "https://webizen.org/immersive/function/0.1#touches",
183 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
184 2,
185 K::Boolean,
186 X::HotZeroHeap,
187 true,
188 E::Exact,
189 MB,
190 0,
191 Some("http://www.opengis.net/def/function/geosparql/sfTouches"),
192 ),
193 entry(
195 "https://webizen.org/immersive/function/0.1#distance",
196 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
197 2,
198 K::Quantity,
199 X::HotZeroHeap,
200 true,
201 E::Exact,
202 MB,
203 0,
204 Some("http://www.opengis.net/def/function/geosparql/distance"),
205 ),
206 entry(
207 "https://webizen.org/immersive/function/0.1#withinDistance",
208 [K::GeometryRef, K::GeometryRef, K::Scalar, K::Boolean],
209 3,
210 K::Boolean,
211 X::HotZeroHeap,
212 true,
213 E::Exact,
214 MB,
215 0,
216 None,
217 ),
218 entry(
219 "https://webizen.org/immersive/function/0.1#nearest",
220 [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
221 2,
222 K::AssetRef,
223 X::ColdBoundedSync,
224 true,
225 E::DeterministicApproximate,
226 4 * MB,
227 MB,
228 None,
229 ),
230 entry(
232 "https://webizen.org/immersive/function/0.1#lineOfSight",
233 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
234 2,
235 K::Boolean,
236 X::ColdBoundedSync,
237 true,
238 E::Exact,
239 4 * MB,
240 0,
241 None,
242 ),
243 entry(
244 "https://webizen.org/immersive/function/0.1#occludes",
245 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
246 2,
247 K::Boolean,
248 X::ColdBoundedSync,
249 true,
250 E::DeterministicApproximate,
251 4 * MB,
252 0,
253 None,
254 ),
255 entry(
257 "https://webizen.org/immersive/function/0.1#volume",
258 [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
259 1,
260 K::Quantity,
261 X::ColdBoundedSync,
262 true,
263 E::Exact,
264 4 * MB,
265 0,
266 None,
267 ),
268 entry(
269 "https://webizen.org/immersive/function/0.1#surfaceArea",
270 [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
271 1,
272 K::Quantity,
273 X::ColdBoundedSync,
274 true,
275 E::Exact,
276 4 * MB,
277 0,
278 None,
279 ),
280 entry(
281 "https://webizen.org/immersive/function/0.1#centroid",
282 [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
283 1,
284 K::GeometryRef,
285 X::HotZeroHeap,
286 true,
287 E::Exact,
288 MB,
289 256,
290 None,
291 ),
292 entry(
294 "https://webizen.org/immersive/function/0.1#intersectsAt",
295 [K::GeometryRef, K::GeometryRef, K::Instant, K::Boolean],
296 3,
297 K::Boolean,
298 X::ColdBoundedSync,
299 true,
300 E::Exact,
301 4 * MB,
302 0,
303 None,
304 ),
305 entry(
306 "https://webizen.org/immersive/function/0.1#trajectoryIntersects",
307 [K::GeometryRef, K::GeometryRef, K::Interval, K::Boolean],
308 3,
309 K::Boolean,
310 X::ColdBoundedSync,
311 true,
312 E::DeterministicApproximate,
313 4 * MB,
314 0,
315 None,
316 ),
317 entry(
318 "https://webizen.org/immersive/function/0.1#sliceAtTime",
319 [K::TensorRef, K::Instant, K::Boolean, K::Boolean],
320 2,
321 K::TensorRef,
322 X::ColdBoundedSync,
323 true,
324 E::Exact,
325 4 * MB,
326 MB,
327 None,
328 ),
329 entry(
331 "https://webizen.org/immersive/function/0.1#tensorDistance",
332 [K::TensorRef, K::TensorRef, K::Boolean, K::Boolean],
333 2,
334 K::Quantity,
335 X::HotZeroHeap,
336 true,
337 E::Exact,
338 256,
339 0,
340 None,
341 ),
342 entry(
343 "https://webizen.org/immersive/function/0.1#tensorWithin",
344 [K::TensorRef, K::TensorRef, K::Scalar, K::Boolean],
345 3,
346 K::Boolean,
347 X::HotZeroHeap,
348 true,
349 E::Exact,
350 256,
351 0,
352 None,
353 ),
354 entry(
355 "https://webizen.org/immersive/function/0.1#tensorSlice",
356 [K::TensorRef, K::Scalar, K::Boolean, K::Boolean],
357 2,
358 K::TensorRef,
359 X::ColdBoundedSync,
360 true,
361 E::Exact,
362 4 * MB,
363 MB,
364 None,
365 ),
366 entry(
371 "https://webizen.org/immersive/function/0.1#knn",
372 [K::TensorRef, K::Scalar, K::Boolean, K::Boolean],
373 2,
374 K::AssetRef,
375 X::ColdBoundedSync,
376 false,
377 E::DeterministicApproximate,
378 4 * MB,
379 4 * MB,
380 None,
381 ),
382 entry(
384 "https://webizen.org/immersive/function/0.1#intersectionGeometry",
385 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
386 2,
387 K::GeometryRef,
388 X::ColdBoundedSync,
389 true,
390 E::Exact,
391 8 * MB,
392 8 * MB,
393 None,
394 ),
395 entry(
396 "https://webizen.org/immersive/function/0.1#unionGeometry",
397 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
398 2,
399 K::GeometryRef,
400 X::ColdBoundedSync,
401 true,
402 E::Exact,
403 8 * MB,
404 8 * MB,
405 None,
406 ),
407 entry(
408 "https://webizen.org/immersive/function/0.1#differenceGeometry",
409 [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
410 2,
411 K::GeometryRef,
412 X::ColdBoundedSync,
413 true,
414 E::Exact,
415 8 * MB,
416 8 * MB,
417 None,
418 ),
419 entry(
421 "https://webizen.org/immersive/function/0.1#transform",
422 [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
423 2,
424 K::GeometryRef,
425 X::ColdBoundedSync,
426 true,
427 E::Exact,
428 8 * MB,
429 8 * MB,
430 None,
431 ),
432 entry(
433 "https://webizen.org/immersive/function/0.1#reproject",
434 [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
435 2,
436 K::GeometryRef,
437 X::ColdBoundedSync,
438 true,
439 E::Exact,
440 8 * MB,
441 8 * MB,
442 None,
443 ),
444 entry(
445 "https://webizen.org/immersive/function/0.1#buffer",
446 [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
447 2,
448 K::GeometryRef,
449 X::ColdBoundedSync,
450 true,
451 E::DeterministicApproximate,
452 8 * MB,
453 8 * MB,
454 None,
455 ),
456];
457
458pub fn entry_for_iri_hash(iri_hash: u64) -> Option<&'static FunctionEntry> {
463 FUNCTIONS.iter().find(|e| e.descriptor.iri_hash == iri_hash)
464}
465
466pub fn entry_for_iri(iri: &str) -> Option<&'static FunctionEntry> {
468 FUNCTIONS.iter().find(|e| e.iri == iri)
469}
470
471pub fn admit_inline(iri_hash: u64) -> Option<Result<&'static FunctionEntry, QispError>> {
483 let e = entry_for_iri_hash(iri_hash)?;
484 Some(if e.descriptor.legal_in_filter() {
485 Ok(e)
486 } else {
487 Err(QispError::NonDeterministicDisallowed)
488 })
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::tensor::Tensor10D;
495
496 #[test]
497 fn every_entry_hash_matches_its_iri() {
498 for e in FUNCTIONS {
501 assert_eq!(
502 e.descriptor.iri_hash,
503 crate::q_hash(e.iri),
504 "hash mismatch for {}",
505 e.iri
506 );
507 assert!(
508 e.iri.starts_with(super::super::QISPF_NS),
509 "{} not in qispf: ns",
510 e.iri
511 );
512 }
513 }
514
515 #[test]
516 fn iri_hashes_are_unique() {
517 for (i, a) in FUNCTIONS.iter().enumerate() {
518 for b in &FUNCTIONS[i + 1..] {
519 assert_ne!(
520 a.descriptor.iri_hash, b.descriptor.iri_hash,
521 "collision: {} vs {}",
522 a.iri, b.iri
523 );
524 }
525 }
526 }
527
528 #[test]
529 fn lookup_by_hash_and_iri_agree() {
530 let iri = "https://webizen.org/immersive/function/0.1#intersects";
531 let by_iri = entry_for_iri(iri).unwrap();
532 let by_hash = entry_for_iri_hash(crate::q_hash(iri)).unwrap();
533 assert_eq!(by_iri.descriptor.iri_hash, by_hash.descriptor.iri_hash);
534 assert_eq!(by_iri.descriptor.result, K::Boolean);
535 assert_eq!(by_iri.descriptor.arg_count, 2);
536 }
537
538 #[test]
539 fn topological_and_proximity_defer_to_geosparql() {
540 for name in ["intersects", "contains", "touches", "distance"] {
541 let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
542 let e = entry_for_iri(&iri).unwrap();
543 assert!(e.defers_to.is_some(), "{name} should defer to geof: for 2D");
544 }
545 let wd =
547 entry_for_iri("https://webizen.org/immersive/function/0.1#withinDistance").unwrap();
548 assert!(wd.defers_to.is_none());
549 }
550
551 #[test]
552 fn admission_rejects_knn_inline_but_admits_predicates() {
553 let intersects = crate::q_hash("https://webizen.org/immersive/function/0.1#intersects");
554 assert!(
555 matches!(admit_inline(intersects), Some(Ok(_))),
556 "intersects is legal inline"
557 );
558
559 let knn = crate::q_hash("https://webizen.org/immersive/function/0.1#knn");
560 assert!(
561 matches!(
562 admit_inline(knn),
563 Some(Err(QispError::NonDeterministicDisallowed))
564 ),
565 "knn is a graph operator, not an inline expression function"
566 );
567
568 assert!(admit_inline(0xDEAD_BEEF_0000_0001).is_none());
570 }
571
572 #[test]
573 fn hot_predicates_are_filter_legal_and_zero_heap() {
574 for name in [
575 "intersects",
576 "contains",
577 "touches",
578 "distance",
579 "withinDistance",
580 "tensorDistance",
581 "tensorWithin",
582 ] {
583 let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
584 let e = entry_for_iri(&iri).unwrap();
585 assert_eq!(e.descriptor.execution, X::HotZeroHeap, "{name} must be hot");
586 assert!(
587 e.descriptor.legal_in_filter(),
588 "{name} must be FILTER-legal"
589 );
590 }
591 }
592
593 #[test]
594 fn constructive_ops_are_cold_bounded_with_output_budget() {
595 for name in [
596 "intersectionGeometry",
597 "unionGeometry",
598 "differenceGeometry",
599 ] {
600 let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
601 let e = entry_for_iri(&iri).unwrap();
602 assert_eq!(e.descriptor.execution, X::ColdBoundedSync);
603 assert!(
604 e.descriptor.max_output_bytes > 0,
605 "{name} must declare an output budget"
606 );
607 assert_eq!(e.descriptor.result, K::GeometryRef);
608 }
609 }
610
611 #[test]
612 fn tensor_predicates_use_canonical_metric_and_reject_bad_radius() {
613 let a = Tensor10D::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
614 let b = Tensor10D::new(0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0);
615 assert_eq!(tensor_distance(&a, &b).unwrap(), a.full_distance(&b));
616 assert_eq!(tensor_within(&a, &b, 5.0), Ok(true));
617 assert_eq!(tensor_within(&a, &b, 4.99), Ok(false));
618 assert_eq!(
619 tensor_within(&a, &b, f32::NAN),
620 Err(QispError::ProfileMismatch)
621 );
622 assert_eq!(tensor_within(&a, &b, -1.0), Err(QispError::ProfileMismatch));
623 }
624
625 #[test]
626 fn tensor_knn_is_bounded_sorted_and_tie_deterministic() {
627 let query = Tensor10D::default();
628 let candidates = [
629 Tensor10D::new(0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
630 Tensor10D::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
631 Tensor10D::new(0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
632 ];
633 let mut out = [TensorNeighbor {
634 index: 99,
635 distance: f32::INFINITY,
636 }; 2];
637 assert_eq!(tensor_knn_into(&query, &candidates, 2, &mut out), Ok(2));
638 assert_eq!(out[0].index, 1);
639 assert_eq!(out[1].index, 2);
640 assert_eq!(out[0].distance, out[1].distance);
641 assert_eq!(
642 tensor_knn_into(&query, &candidates, 3, &mut out),
643 Err(QispError::BudgetExceeded)
644 );
645 }
646}