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