1use oxgraph_layout_util::integrity::{
9 OffsetIntegrityIssue, check_offset_section, check_value_range,
10};
11
12use crate::{
13 error::{BcsrError, BcsrRoleSide, BcsrSection},
14 internal::view::BcsrSections,
15 word::{LayoutIndex, LayoutWord},
16};
17
18fn map_offsets_issue<W: LayoutWord>(
21 section: BcsrSection,
22 offsets: &[W],
23 issue: OffsetIntegrityIssue,
24) -> BcsrError {
25 match issue {
26 OffsetIntegrityIssue::Length { expected, actual } => BcsrError::OffsetLength {
27 section,
28 expected,
29 actual,
30 },
31 OffsetIntegrityIssue::FirstNonZero { actual } => BcsrError::FirstOffset { section, actual },
32 OffsetIntegrityIssue::NonMonotonic {
33 index,
34 previous,
35 actual,
36 } => BcsrError::NonMonotonicOffset {
37 section,
38 index,
39 previous,
40 actual,
41 },
42 OffsetIntegrityIssue::FinalMismatch {
43 final_offset,
44 value_len,
45 } => BcsrError::FinalOffset {
46 section,
47 final_offset,
48 value_len,
49 },
50 OffsetIntegrityIssue::UsizeOverflow { index } => {
51 let value = offsets
52 .get(index)
53 .copied()
54 .and_then(|w| w.get().to_usize())
55 .unwrap_or(usize::MAX);
56 BcsrError::UsizeOverflow { value }
57 }
58 _ => BcsrError::UsizeOverflow { value: usize::MAX },
59 }
60}
61
62fn map_vertex_value_issue<W: LayoutWord>(
65 section: BcsrSection,
66 values: &[W],
67 issue: OffsetIntegrityIssue,
68) -> BcsrError {
69 match issue {
70 OffsetIntegrityIssue::ValueOutOfRange {
71 index,
72 value,
73 bound,
74 } => BcsrError::VertexOutOfRange {
75 section,
76 index,
77 vertex: value,
78 vertex_count: bound,
79 },
80 OffsetIntegrityIssue::UsizeOverflow { index } => {
81 let value = values
82 .get(index)
83 .copied()
84 .and_then(|w| w.get().to_usize())
85 .unwrap_or(usize::MAX);
86 BcsrError::UsizeOverflow { value }
87 }
88 _ => BcsrError::UsizeOverflow { value: usize::MAX },
89 }
90}
91
92fn map_hyperedge_value_issue<W: LayoutWord>(
95 section: BcsrSection,
96 values: &[W],
97 issue: OffsetIntegrityIssue,
98) -> BcsrError {
99 match issue {
100 OffsetIntegrityIssue::ValueOutOfRange {
101 index,
102 value,
103 bound,
104 } => BcsrError::HyperedgeOutOfRange {
105 section,
106 index,
107 hyperedge: value,
108 hyperedge_count: bound,
109 },
110 OffsetIntegrityIssue::UsizeOverflow { index } => {
111 let value = values
112 .get(index)
113 .copied()
114 .and_then(|w| w.get().to_usize())
115 .unwrap_or(usize::MAX);
116 BcsrError::UsizeOverflow { value }
117 }
118 _ => BcsrError::UsizeOverflow { value: usize::MAX },
119 }
120}
121
122#[non_exhaustive]
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub enum BcsrValidation {
136 Layout,
139 Strict,
143}
144
145#[derive(Clone, Copy, Debug)]
154pub(in crate::internal) struct DerivedCounts {
155 pub(in crate::internal) vertex_count: usize,
157 pub(in crate::internal) hyperedge_count: usize,
159 pub(in crate::internal) p_outgoing: usize,
161 pub(in crate::internal) p_incoming: usize,
163 pub(in crate::internal) total_incidences: usize,
166}
167
168pub(in crate::internal) fn validate_sections<OffsetWord, VertexWord, RelationWord>(
183 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
184 level: BcsrValidation,
185) -> Result<DerivedCounts, BcsrError>
186where
187 OffsetWord: LayoutWord,
188 VertexWord: LayoutWord,
189 RelationWord: LayoutWord,
190{
191 let counts = derive_counts(sections)?;
192 validate_all_offsets(sections, counts)?;
193 validate_total_lengths(sections)?;
194 validate_value_ranges(sections, counts)?;
195 validate_within_range_sorted(sections)?;
196 if matches!(level, BcsrValidation::Strict) {
197 validate_cross_direction(sections)?;
198 }
199 Ok(counts)
200}
201
202fn derive_counts<OffsetWord, VertexWord, RelationWord>(
205 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
206) -> Result<DerivedCounts, BcsrError>
207where
208 OffsetWord: LayoutWord,
209 VertexWord: LayoutWord,
210 RelationWord: LayoutWord,
211{
212 let head_len = sections.head_offsets.len();
213 let tail_len = sections.tail_offsets.len();
214 if head_len != tail_len {
215 return Err(BcsrError::HyperedgeOffsetLengthMismatch {
216 head_offsets_len: head_len,
217 tail_offsets_len: tail_len,
218 });
219 }
220 let outgoing_len = sections.vertex_outgoing_offsets.len();
221 let incoming_len = sections.vertex_incoming_offsets.len();
222 if outgoing_len != incoming_len {
223 return Err(BcsrError::VertexOffsetLengthMismatch {
224 outgoing_offsets_len: outgoing_len,
225 incoming_offsets_len: incoming_len,
226 });
227 }
228
229 let hyperedge_count = derive_count_from_offsets(head_len, BcsrSection::HeadOffsets)?;
230 debug_assert_eq!(
237 head_len,
238 hyperedge_count + 1,
239 "head offsets must be hyperedge_count + 1"
240 );
241 debug_assert_eq!(
242 tail_len,
243 hyperedge_count + 1,
244 "tail offsets must be hyperedge_count + 1 (paired with head)"
245 );
246 let vertex_count = derive_count_from_offsets(outgoing_len, BcsrSection::VertexOutgoingOffsets)?;
247 debug_assert_eq!(
248 outgoing_len,
249 vertex_count + 1,
250 "vertex outgoing offsets must be vertex_count + 1"
251 );
252 debug_assert_eq!(
253 incoming_len,
254 vertex_count + 1,
255 "vertex incoming offsets must be vertex_count + 1 (paired with outgoing)"
256 );
257 let p_outgoing = sections.vertex_outgoing_hyperedges.len();
258 let p_incoming = sections.vertex_incoming_hyperedges.len();
259 let total_incidences =
260 p_outgoing
261 .checked_add(p_incoming)
262 .ok_or(BcsrError::TotalIncidenceCountOverflow {
263 p_head: p_outgoing,
264 p_tail: p_incoming,
265 })?;
266
267 Ok(DerivedCounts {
268 vertex_count,
269 hyperedge_count,
270 p_outgoing,
271 p_incoming,
272 total_incidences,
273 })
274}
275
276const fn derive_count_from_offsets(
278 offsets_len: usize,
279 section: BcsrSection,
280) -> Result<usize, BcsrError> {
281 if offsets_len == 0 {
282 return Err(BcsrError::OffsetLength {
283 section,
284 expected: 1,
285 actual: 0,
286 });
287 }
288 Ok(offsets_len - 1)
289}
290
291fn validate_all_offsets<OffsetWord, VertexWord, RelationWord>(
293 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
294 counts: DerivedCounts,
295) -> Result<(), BcsrError>
296where
297 OffsetWord: LayoutWord,
298 VertexWord: LayoutWord,
299 RelationWord: LayoutWord,
300{
301 validate_one_offsets(
302 sections.head_offsets,
303 BcsrSection::HeadOffsets,
304 counts.hyperedge_count,
305 sections.head_participants.len(),
306 )?;
307 validate_one_offsets(
308 sections.tail_offsets,
309 BcsrSection::TailOffsets,
310 counts.hyperedge_count,
311 sections.tail_participants.len(),
312 )?;
313 validate_one_offsets(
314 sections.vertex_outgoing_offsets,
315 BcsrSection::VertexOutgoingOffsets,
316 counts.vertex_count,
317 sections.vertex_outgoing_hyperedges.len(),
318 )?;
319 validate_one_offsets(
320 sections.vertex_incoming_offsets,
321 BcsrSection::VertexIncomingOffsets,
322 counts.vertex_count,
323 sections.vertex_incoming_hyperedges.len(),
324 )
325}
326
327fn validate_one_offsets<Word: LayoutWord>(
332 offsets: &[Word],
333 section: BcsrSection,
334 count: usize,
335 value_len: usize,
336) -> Result<(), BcsrError> {
337 if count.checked_add(1).is_none() {
338 return Err(BcsrError::OffsetLengthOverflow { count });
339 }
340 check_offset_section(offsets, count, value_len)
341 .map_err(|issue| map_offsets_issue(section, offsets, issue))
342}
343
344const fn validate_total_lengths<OffsetWord, VertexWord, RelationWord>(
347 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
348) -> Result<(), BcsrError>
349where
350 OffsetWord: LayoutWord,
351 VertexWord: LayoutWord,
352 RelationWord: LayoutWord,
353{
354 if sections.head_participants.len() != sections.vertex_outgoing_hyperedges.len() {
355 return Err(BcsrError::OutgoingTotalMismatch {
356 head_participants_len: sections.head_participants.len(),
357 outgoing_hyperedges_len: sections.vertex_outgoing_hyperedges.len(),
358 });
359 }
360 if sections.tail_participants.len() != sections.vertex_incoming_hyperedges.len() {
361 return Err(BcsrError::IncomingTotalMismatch {
362 tail_participants_len: sections.tail_participants.len(),
363 incoming_hyperedges_len: sections.vertex_incoming_hyperedges.len(),
364 });
365 }
366 Ok(())
367}
368
369fn validate_value_ranges<OffsetWord, VertexWord, RelationWord>(
371 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
372 counts: DerivedCounts,
373) -> Result<(), BcsrError>
374where
375 OffsetWord: LayoutWord,
376 VertexWord: LayoutWord,
377 RelationWord: LayoutWord,
378{
379 check_vertex_values(
380 sections.head_participants,
381 BcsrSection::HeadParticipants,
382 counts.vertex_count,
383 )?;
384 check_vertex_values(
385 sections.tail_participants,
386 BcsrSection::TailParticipants,
387 counts.vertex_count,
388 )?;
389 check_hyperedge_values(
390 sections.vertex_outgoing_hyperedges,
391 BcsrSection::VertexOutgoingHyperedges,
392 counts.hyperedge_count,
393 )?;
394 check_hyperedge_values(
395 sections.vertex_incoming_hyperedges,
396 BcsrSection::VertexIncomingHyperedges,
397 counts.hyperedge_count,
398 )
399}
400
401fn check_vertex_values<Word: LayoutWord>(
405 values: &[Word],
406 section: BcsrSection,
407 vertex_count: usize,
408) -> Result<(), BcsrError> {
409 check_value_range(values, vertex_count)
410 .map_err(|issue| map_vertex_value_issue(section, values, issue))
411}
412
413fn check_hyperedge_values<Word: LayoutWord>(
417 values: &[Word],
418 section: BcsrSection,
419 hyperedge_count: usize,
420) -> Result<(), BcsrError> {
421 check_value_range(values, hyperedge_count)
422 .map_err(|issue| map_hyperedge_value_issue(section, values, issue))
423}
424
425fn validate_within_range_sorted<OffsetWord, VertexWord, RelationWord>(
428 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
429) -> Result<(), BcsrError>
430where
431 OffsetWord: LayoutWord,
432 VertexWord: LayoutWord,
433 RelationWord: LayoutWord,
434{
435 check_strictly_ascending_buckets(
436 sections.head_offsets,
437 sections.head_participants,
438 BcsrSection::HeadParticipants,
439 )?;
440 check_strictly_ascending_buckets(
441 sections.tail_offsets,
442 sections.tail_participants,
443 BcsrSection::TailParticipants,
444 )?;
445 check_strictly_ascending_buckets(
446 sections.vertex_outgoing_offsets,
447 sections.vertex_outgoing_hyperedges,
448 BcsrSection::VertexOutgoingHyperedges,
449 )?;
450 check_strictly_ascending_buckets(
451 sections.vertex_incoming_offsets,
452 sections.vertex_incoming_hyperedges,
453 BcsrSection::VertexIncomingHyperedges,
454 )
455}
456
457fn check_strictly_ascending_buckets<OffsetWord, Word>(
460 offsets: &[OffsetWord],
461 values: &[Word],
462 section: BcsrSection,
463) -> Result<(), BcsrError>
464where
465 OffsetWord: LayoutWord,
466 Word: LayoutWord,
467{
468 if offsets.len() < 2 {
469 return Ok(());
470 }
471 for window in offsets.windows(2) {
472 let start = index_to_usize(window[0].get())?;
473 let end = index_to_usize(window[1].get())?;
474 check_strictly_ascending_range(values, start, end, section)?;
475 }
476 Ok(())
477}
478
479fn check_strictly_ascending_range<Word: LayoutWord>(
481 values: &[Word],
482 start: usize,
483 end: usize,
484 section: BcsrSection,
485) -> Result<(), BcsrError> {
486 if end <= start + 1 {
487 return Ok(());
488 }
489 let mut previous = index_to_usize(values[start].get())?;
490 for relative in 1..(end - start) {
491 let index = start + relative;
492 let actual = index_to_usize(values[index].get())?;
493 if actual <= previous {
494 return Err(BcsrError::NotStrictlyAscending {
495 section,
496 index,
497 previous,
498 actual,
499 });
500 }
501 previous = actual;
502 }
503 Ok(())
504}
505
506fn validate_cross_direction<OffsetWord, VertexWord, RelationWord>(
510 sections: &BcsrSections<'_, OffsetWord, VertexWord, RelationWord>,
511) -> Result<(), BcsrError>
512where
513 OffsetWord: LayoutWord,
514 VertexWord: LayoutWord,
515 RelationWord: LayoutWord,
516{
517 cross_direction_walk(
520 sections.head_offsets,
521 sections.head_participants,
522 sections.vertex_outgoing_offsets,
523 sections.vertex_outgoing_hyperedges,
524 BcsrRoleSide::Outgoing,
525 )?;
526 cross_direction_walk(
527 sections.tail_offsets,
528 sections.tail_participants,
529 sections.vertex_incoming_offsets,
530 sections.vertex_incoming_hyperedges,
531 BcsrRoleSide::Incoming,
532 )?;
533 converse_direction_walk(
539 sections.vertex_outgoing_offsets,
540 sections.vertex_outgoing_hyperedges,
541 sections.head_offsets,
542 sections.head_participants,
543 BcsrRoleSide::Outgoing,
544 )?;
545 converse_direction_walk(
546 sections.vertex_incoming_offsets,
547 sections.vertex_incoming_hyperedges,
548 sections.tail_offsets,
549 sections.tail_participants,
550 BcsrRoleSide::Incoming,
551 )
552}
553
554fn converse_direction_walk<OffsetWord, VertexWord, RelationWord>(
558 vertex_offsets: &[OffsetWord],
559 vertex_values: &[RelationWord],
560 edge_offsets: &[OffsetWord],
561 edge_values: &[VertexWord],
562 side: BcsrRoleSide,
563) -> Result<(), BcsrError>
564where
565 OffsetWord: LayoutWord,
566 VertexWord: LayoutWord,
567 RelationWord: LayoutWord,
568{
569 if vertex_offsets.len() < 2 {
570 return Ok(());
571 }
572 for vertex_index in 0..(vertex_offsets.len() - 1) {
573 let start = index_to_usize(vertex_offsets[vertex_index].get())?;
574 let end = index_to_usize(vertex_offsets[vertex_index + 1].get())?;
575 for word in vertex_values.iter().take(end).skip(start) {
576 let hyperedge = index_to_usize(word.get())?;
577 let bucket_start = index_to_usize(edge_offsets[hyperedge].get())?;
578 let bucket_end = index_to_usize(edge_offsets[hyperedge + 1].get())?;
579 if !bucket_contains(edge_values, bucket_start, bucket_end, vertex_index) {
580 return Err(BcsrError::CrossDirectionMismatch {
581 side,
582 hyperedge,
583 vertex: vertex_index,
584 });
585 }
586 }
587 }
588 Ok(())
589}
590
591fn cross_direction_walk<OffsetWord, VertexWord, RelationWord>(
594 edge_offsets: &[OffsetWord],
595 edge_values: &[VertexWord],
596 vertex_offsets: &[OffsetWord],
597 vertex_values: &[RelationWord],
598 side: BcsrRoleSide,
599) -> Result<(), BcsrError>
600where
601 OffsetWord: LayoutWord,
602 VertexWord: LayoutWord,
603 RelationWord: LayoutWord,
604{
605 if edge_offsets.len() < 2 {
606 return Ok(());
607 }
608 for hyperedge_index in 0..(edge_offsets.len() - 1) {
609 let start = index_to_usize(edge_offsets[hyperedge_index].get())?;
610 let end = index_to_usize(edge_offsets[hyperedge_index + 1].get())?;
611 let hyperedge = hyperedge_index;
612 cross_direction_check_bucket(CrossDirectionBucket {
613 edge_values,
614 start,
615 end,
616 vertex_offsets,
617 vertex_values,
618 hyperedge,
619 side,
620 })?;
621 }
622 Ok(())
623}
624
625#[derive(Clone, Copy)]
627struct CrossDirectionBucket<'a, OffsetWord, VertexWord, RelationWord> {
628 edge_values: &'a [VertexWord],
630 start: usize,
632 end: usize,
634 vertex_offsets: &'a [OffsetWord],
636 vertex_values: &'a [RelationWord],
638 hyperedge: usize,
640 side: BcsrRoleSide,
642}
643
644fn cross_direction_check_bucket<OffsetWord, VertexWord, RelationWord>(
647 args: CrossDirectionBucket<'_, OffsetWord, VertexWord, RelationWord>,
648) -> Result<(), BcsrError>
649where
650 OffsetWord: LayoutWord,
651 VertexWord: LayoutWord,
652 RelationWord: LayoutWord,
653{
654 for word in args.edge_values.iter().take(args.end).skip(args.start) {
655 let vertex = index_to_usize(word.get())?;
656 let v_index = vertex;
657 let bucket_start = index_to_usize(args.vertex_offsets[v_index].get())?;
658 let bucket_end = index_to_usize(args.vertex_offsets[v_index + 1].get())?;
659 if !bucket_contains(args.vertex_values, bucket_start, bucket_end, args.hyperedge) {
660 return Err(BcsrError::CrossDirectionMismatch {
661 side: args.side,
662 hyperedge: args.hyperedge,
663 vertex,
664 });
665 }
666 }
667 Ok(())
668}
669
670fn bucket_contains<Word: LayoutWord>(
673 values: &[Word],
674 start: usize,
675 end: usize,
676 needle: usize,
677) -> bool {
678 let bucket = &values[start..end];
679 bucket
680 .binary_search_by(|word| index_to_usize_validated(word.get()).cmp(&needle))
681 .is_ok()
682}
683
684pub(in crate::internal) fn index_to_usize<Index: LayoutIndex>(
686 value: Index,
687) -> Result<usize, BcsrError> {
688 value
689 .to_usize()
690 .ok_or(BcsrError::UsizeOverflow { value: usize::MAX })
691}
692
693pub(in crate::internal) fn index_to_usize_validated<Index: LayoutIndex>(value: Index) -> usize {
705 oxgraph_layout_util::integrity::index_to_usize_validated(value)
706 .unwrap_or_else(|| unreachable!("validated bipartite-CSR index must fit usize"))
707}
708
709pub(in crate::internal) fn usize_to_index_validated<Index: LayoutIndex>(value: usize) -> Index {
720 oxgraph_layout_util::integrity::usize_to_index_validated(value)
721 .unwrap_or_else(|| unreachable!("validated BCSR slot must fit index"))
722}