1#![cfg_attr(
10 not(test),
11 expect(
12 clippy::missing_docs_in_private_items,
13 reason = "PageRank helper functions are private implementation details behind documented public API tiers"
14 )
15)]
16
17#[cfg(feature = "alloc")]
18use alloc::{vec, vec::Vec};
19#[cfg(feature = "alloc")]
20use core::marker::PhantomData;
21use core::{
22 error::Error,
23 fmt,
24 ops::{Add, AddAssign, Div, Mul, Sub},
25};
26
27use oxgraph_graph::ForwardGraph;
28use oxgraph_hyper::{DirectedHyperedgeIncidences, DirectedVertexHyperedges};
29
30pub trait PageRankHypergraph:
41 DirectedVertexHyperedges
42 + DirectedHyperedgeIncidences
43 + IncidenceElement
44 + DenseElementIndex
45 + DenseRelationIndex
46{
47}
48
49impl<T> PageRankHypergraph for T where
50 T: DirectedVertexHyperedges
51 + DirectedHyperedgeIncidences
52 + IncidenceElement
53 + DenseElementIndex
54 + DenseRelationIndex
55{
56}
57use oxgraph_topology::{
58 DenseElementIndex, DenseIncidenceIndex, DenseRelationIndex, ElementId, IncidenceElement,
59 IncidenceWeight, RelationId, RelationWeight,
60};
61
62pub trait PageRankScalar:
76 Copy
77 + fmt::Debug
78 + PartialOrd
79 + Add<Output = Self>
80 + Sub<Output = Self>
81 + Mul<Output = Self>
82 + Div<Output = Self>
83 + AddAssign
84 + 'static
85{
86 const ZERO: Self;
88 const ONE: Self;
90 const INFINITY: Self;
92
93 fn from_usize(value: usize) -> Self;
95
96 fn from_f64(value: f64) -> Self;
98
99 #[must_use]
101 fn abs(self) -> Self;
102
103 fn is_finite(self) -> bool;
105}
106
107impl PageRankScalar for f64 {
108 const ZERO: Self = 0.0;
109 const ONE: Self = 1.0;
110 #[expect(
111 clippy::use_self,
112 reason = "primitive inherent infinity constant is clearer here"
113 )]
114 const INFINITY: Self = f64::INFINITY;
115
116 #[expect(
117 clippy::cast_precision_loss,
118 reason = "PageRank degree conversion is a documented scalar-boundary conversion"
119 )]
120 fn from_usize(value: usize) -> Self {
121 value as Self
122 }
123
124 fn from_f64(value: f64) -> Self {
125 value
126 }
127
128 fn abs(self) -> Self {
129 self.abs()
130 }
131
132 fn is_finite(self) -> bool {
133 self.is_finite()
134 }
135}
136
137impl PageRankScalar for f32 {
138 const ZERO: Self = 0.0;
139 const ONE: Self = 1.0;
140 #[expect(
141 clippy::use_self,
142 reason = "primitive inherent infinity constant is clearer here"
143 )]
144 const INFINITY: Self = f32::INFINITY;
145
146 #[expect(
147 clippy::cast_precision_loss,
148 reason = "PageRank degree conversion is a documented scalar-boundary conversion"
149 )]
150 fn from_usize(value: usize) -> Self {
151 value as Self
152 }
153
154 #[expect(
155 clippy::cast_possible_truncation,
156 reason = "f32 PageRank callers explicitly select f32 rank/config output"
157 )]
158 fn from_f64(value: f64) -> Self {
159 value as Self
160 }
161
162 fn abs(self) -> Self {
163 self.abs()
164 }
165
166 fn is_finite(self) -> bool {
167 self.is_finite()
168 }
169}
170
171pub trait IntoPageRankScalar<S: PageRankScalar> {
177 fn into_pagerank_scalar(self) -> S;
179}
180
181impl<S: PageRankScalar> IntoPageRankScalar<S> for S {
182 fn into_pagerank_scalar(self) -> S {
183 self
184 }
185}
186
187macro_rules! impl_weight_into_pagerank_scalar_from {
189 ($target:ty; $($type:ty),* $(,)?) => {
190 $(
191 impl IntoPageRankScalar<$target> for $type {
192 fn into_pagerank_scalar(self) -> $target { <$target>::from(self) }
193 }
194 )*
195 };
196}
197
198macro_rules! impl_weight_into_pagerank_scalar_cast {
200 ($target:ty; $($type:ty),* $(,)?) => {
201 $(
202 impl IntoPageRankScalar<$target> for $type {
203 #[expect(
204 clippy::cast_precision_loss,
205 reason = "PageRank primitive weight conversions are explicit algorithm-boundary numeric interpretations"
206 )]
207 fn into_pagerank_scalar(self) -> $target { self as $target }
208 }
209 )*
210 };
211}
212
213impl_weight_into_pagerank_scalar_from!(f64; u8, u16, u32, i8, i16, i32, f32);
214impl_weight_into_pagerank_scalar_cast!(f64; u64, usize, i64, isize);
215impl_weight_into_pagerank_scalar_from!(f32; u8, u16, i8, i16);
216impl_weight_into_pagerank_scalar_cast!(f32; u32, u64, usize, i32, i64, isize);
217
218impl IntoPageRankScalar<f32> for f64 {
219 #[expect(
220 clippy::cast_possible_truncation,
221 reason = "f32 PageRank callers explicitly select f32 rank and configuration output"
222 )]
223 fn into_pagerank_scalar(self) -> f32 {
224 self as f32
225 }
226}
227
228pub trait OutgoingDistribution<G, S>
253where
254 G: ForwardGraph + DenseElementIndex,
255 S: PageRankScalar,
256{
257 #[expect(
269 clippy::too_many_arguments,
270 reason = "distribution kernel needs graph, element, rank, output buffer, and visibility mask explicitly"
271 )]
272 fn distribute_outgoing(
273 &self,
274 graph: &G,
275 element: G::ElementId,
276 rank: S,
277 next: &mut [S],
278 visible: &[u8],
279 ) -> Result<S, PageRankError<S>>;
280}
281
282#[derive(Clone, Copy, Debug, Default)]
292pub struct Uniform;
293
294impl<G, S> OutgoingDistribution<G, S> for Uniform
295where
296 G: ForwardGraph + DenseElementIndex,
297 S: PageRankScalar,
298{
299 fn distribute_outgoing(
300 &self,
301 graph: &G,
302 element: G::ElementId,
303 rank: S,
304 next: &mut [S],
305 visible: &[u8],
306 ) -> Result<S, PageRankError<S>> {
307 let mut degree = 0_usize;
308 for edge in graph.outgoing_edges(element) {
309 let target = graph.target(edge);
310 let target_index = checked_element_index(graph, target)?;
311 if is_visible(visible, target_index) {
312 degree += 1;
313 }
314 }
315 if degree == 0 {
316 return Ok(rank);
317 }
318 let share = rank / S::from_usize(degree);
319 for edge in graph.outgoing_edges(element) {
320 let target = graph.target(edge);
321 let target_index = checked_element_index(graph, target)?;
322 if is_visible(visible, target_index) {
323 next[target_index] += share;
324 }
325 }
326 Ok(S::ZERO)
327 }
328}
329
330#[derive(Clone, Copy, Debug)]
343pub struct Weighted<'w, W> {
344 weights: &'w W,
346}
347
348impl<'w, W> Weighted<'w, W> {
349 #[must_use]
355 pub const fn new(weights: &'w W) -> Self {
356 Self { weights }
357 }
358}
359
360impl<G, W, S> OutgoingDistribution<G, S> for Weighted<'_, W>
361where
362 G: ForwardGraph + DenseElementIndex + DenseRelationIndex,
363 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
364 W::Weight: IntoPageRankScalar<S>,
365 S: PageRankScalar,
366{
367 fn distribute_outgoing(
368 &self,
369 graph: &G,
370 element: G::ElementId,
371 rank: S,
372 next: &mut [S],
373 visible: &[u8],
374 ) -> Result<S, PageRankError<S>> {
375 let total = outgoing_weight_total(graph, self.weights, element, visible)?;
376 if total <= S::ZERO {
377 return Ok(rank);
378 }
379 for edge in graph.outgoing_edges(element) {
380 let target = graph.target(edge);
381 let target_index = checked_element_index(graph, target)?;
382 if is_visible(visible, target_index) {
383 let weight = checked_relation_weight(graph, self.weights, edge)?;
384 next[target_index] += rank * (weight / total);
385 }
386 }
387 Ok(S::ZERO)
388 }
389}
390
391pub trait HypergraphOutgoingDistribution<H, S>
417where
418 H: PageRankHypergraph,
419 S: PageRankScalar,
420{
421 #[expect(
433 clippy::too_many_arguments,
434 reason = "distribution kernel needs hypergraph, element, rank, output buffer, and visibility mask explicitly"
435 )]
436 fn distribute_from_element(
437 &self,
438 hypergraph: &H,
439 element: H::ElementId,
440 rank: S,
441 next_relations: &mut [S],
442 visible_relations: &[u8],
443 ) -> Result<S, PageRankError<S>>;
444
445 #[expect(
457 clippy::too_many_arguments,
458 reason = "distribution kernel needs hypergraph, relation, rank, output buffer, and visibility mask explicitly"
459 )]
460 fn distribute_from_relation(
461 &self,
462 hypergraph: &H,
463 relation: H::RelationId,
464 rank: S,
465 next_elements: &mut [S],
466 visible_elements: &[u8],
467 ) -> Result<S, PageRankError<S>>;
468}
469
470impl<H, S> HypergraphOutgoingDistribution<H, S> for Uniform
471where
472 H: PageRankHypergraph,
473 S: PageRankScalar,
474{
475 fn distribute_from_element(
476 &self,
477 hypergraph: &H,
478 element: H::ElementId,
479 rank: S,
480 next_relations: &mut [S],
481 visible_relations: &[u8],
482 ) -> Result<S, PageRankError<S>> {
483 let mut degree = 0_usize;
484 for relation in hypergraph.outgoing_hyperedges(element) {
485 let relation_index = checked_relation_index_for(hypergraph, relation)?;
486 if is_visible(visible_relations, relation_index) {
487 degree += 1;
488 }
489 }
490 if degree == 0 {
491 return Ok(rank);
492 }
493 let share = rank / S::from_usize(degree);
494 for relation in hypergraph.outgoing_hyperedges(element) {
495 let relation_index = checked_relation_index_for(hypergraph, relation)?;
496 if is_visible(visible_relations, relation_index) {
497 next_relations[relation_index] += share;
498 }
499 }
500 Ok(S::ZERO)
501 }
502
503 fn distribute_from_relation(
504 &self,
505 hypergraph: &H,
506 relation: H::RelationId,
507 rank: S,
508 next_elements: &mut [S],
509 visible_elements: &[u8],
510 ) -> Result<S, PageRankError<S>> {
511 let mut degree = 0_usize;
512 for incidence in hypergraph.target_incidences(relation) {
513 let target = hypergraph.incidence_element(incidence);
514 let target_index = checked_element_index(hypergraph, target)?;
515 if is_visible(visible_elements, target_index) {
516 degree += 1;
517 }
518 }
519 if degree == 0 {
520 return Ok(rank);
521 }
522 let share = rank / S::from_usize(degree);
523 for incidence in hypergraph.target_incidences(relation) {
524 let target = hypergraph.incidence_element(incidence);
525 let target_index = checked_element_index(hypergraph, target)?;
526 if is_visible(visible_elements, target_index) {
527 next_elements[target_index] += share;
528 }
529 }
530 Ok(S::ZERO)
531 }
532}
533
534#[derive(Clone, Copy, Debug)]
549pub struct HyperWeighted<'rw, 'iw, RW, IW> {
550 relation_weights: &'rw RW,
552 incidence_weights: &'iw IW,
554}
555
556impl<'rw, 'iw, RW, IW> HyperWeighted<'rw, 'iw, RW, IW> {
557 #[must_use]
564 pub const fn new(relation_weights: &'rw RW, incidence_weights: &'iw IW) -> Self {
565 Self {
566 relation_weights,
567 incidence_weights,
568 }
569 }
570}
571
572impl<H, RW, IW, S> HypergraphOutgoingDistribution<H, S> for HyperWeighted<'_, '_, RW, IW>
573where
574 H: PageRankHypergraph + DenseIncidenceIndex,
575 RW: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
576 RW::Weight: IntoPageRankScalar<S>,
577 IW: IncidenceWeight<
578 ElementId = H::ElementId,
579 RelationId = H::RelationId,
580 IncidenceId = H::IncidenceId,
581 >,
582 IW::Weight: IntoPageRankScalar<S>,
583 S: PageRankScalar,
584{
585 fn distribute_from_element(
586 &self,
587 hypergraph: &H,
588 element: H::ElementId,
589 rank: S,
590 next_relations: &mut [S],
591 visible_relations: &[u8],
592 ) -> Result<S, PageRankError<S>> {
593 let total = hyper_outgoing_relation_weight(
594 hypergraph,
595 self.relation_weights,
596 element,
597 visible_relations,
598 )?;
599 if total <= S::ZERO {
600 return Ok(rank);
601 }
602 for relation in hypergraph.outgoing_hyperedges(element) {
603 let relation_index = checked_relation_index_for(hypergraph, relation)?;
604 if !is_visible(visible_relations, relation_index) {
605 continue;
606 }
607 let weight = checked_relation_weight(hypergraph, self.relation_weights, relation)?;
608 next_relations[relation_index] += rank * (weight / total);
609 }
610 Ok(S::ZERO)
611 }
612
613 fn distribute_from_relation(
614 &self,
615 hypergraph: &H,
616 relation: H::RelationId,
617 rank: S,
618 next_elements: &mut [S],
619 visible_elements: &[u8],
620 ) -> Result<S, PageRankError<S>> {
621 let total = hyper_target_incidence_weight(
622 hypergraph,
623 self.incidence_weights,
624 relation,
625 visible_elements,
626 )?;
627 if total <= S::ZERO {
628 return Ok(rank);
629 }
630 for incidence in hypergraph.target_incidences(relation) {
631 let target = hypergraph.incidence_element(incidence);
632 let target_index = checked_element_index(hypergraph, target)?;
633 if !is_visible(visible_elements, target_index) {
634 continue;
635 }
636 let weight = checked_incidence_weight(hypergraph, self.incidence_weights, incidence)?;
637 next_elements[target_index] += rank * (weight / total);
638 }
639 Ok(S::ZERO)
640 }
641}
642
643#[derive(Clone, Copy, Debug, PartialEq)]
649#[non_exhaustive]
650pub struct PageRankConfig<S> {
651 pub damping: S,
653 pub tolerance: S,
655 pub max_iterations: usize,
657}
658
659impl<S> PageRankConfig<S> {
660 #[must_use]
669 pub const fn new(damping: S, tolerance: S, max_iterations: usize) -> Self {
670 Self {
671 damping,
672 tolerance,
673 max_iterations,
674 }
675 }
676}
677
678impl<S: PageRankScalar> Default for PageRankConfig<S> {
679 fn default() -> Self {
682 Self {
683 damping: S::from_f64(0.85),
684 tolerance: S::from_f64(1e-6),
685 max_iterations: 100,
686 }
687 }
688}
689
690#[derive(Clone, Copy, Debug, PartialEq)]
696#[non_exhaustive]
697pub struct PageRankReport<S> {
698 pub iterations: usize,
700 pub delta: S,
702}
703
704#[derive(Debug, Clone, PartialEq)]
710#[non_exhaustive]
711pub enum PageRankError<S> {
712 EmptyState,
714 InvalidDamping {
716 damping: S,
718 },
719 InvalidTolerance {
721 tolerance: S,
723 },
724 InvalidMaxIterations,
726 OutputTooShort {
728 required: usize,
730 actual: usize,
732 },
733 ScratchTooShort {
735 name: &'static str,
737 required: usize,
739 actual: usize,
741 },
742 PersonalizationTooShort {
744 required: usize,
746 actual: usize,
748 },
749 InvalidPersonalization {
751 index: usize,
753 value: S,
755 },
756 ZeroPersonalization,
758 ElementIndexOutOfBounds {
760 index: usize,
762 bound: usize,
764 },
765 DuplicateElement {
767 index: usize,
769 },
770 DuplicateRelation {
772 index: usize,
774 },
775 RelationIndexOutOfBounds {
777 index: usize,
779 bound: usize,
781 },
782 IncidenceIndexOutOfBounds {
784 index: usize,
786 bound: usize,
788 },
789 InvalidRelationWeight {
791 index: usize,
793 value: S,
795 },
796 InvalidIncidenceWeight {
798 index: usize,
800 value: S,
802 },
803 NonConverged {
805 iterations: usize,
807 delta: S,
809 },
810}
811
812impl<S: fmt::Debug> fmt::Display for PageRankError<S> {
813 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
814 match self {
815 Self::EmptyState => formatter.write_str("pagerank state set is empty"),
816 Self::InvalidDamping { damping } => {
817 write!(formatter, "invalid pagerank damping {damping:?}")
818 }
819 Self::InvalidTolerance { tolerance } => {
820 write!(formatter, "invalid pagerank tolerance {tolerance:?}")
821 }
822 Self::InvalidMaxIterations => {
823 formatter.write_str("pagerank max_iterations must be positive")
824 }
825 Self::OutputTooShort { required, actual } => write!(
826 formatter,
827 "pagerank output too short: required {required}, got {actual}"
828 ),
829 Self::ScratchTooShort {
830 name,
831 required,
832 actual,
833 } => write!(
834 formatter,
835 "pagerank scratch '{name}' too short: required {required}, got {actual}"
836 ),
837 Self::PersonalizationTooShort { required, actual } => write!(
838 formatter,
839 "pagerank personalization too short: required {required}, got {actual}"
840 ),
841 Self::InvalidPersonalization { index, value } => write!(
842 formatter,
843 "invalid pagerank personalization at {index}: {value:?}"
844 ),
845 Self::ZeroPersonalization => {
846 formatter.write_str("pagerank personalization sum is zero")
847 }
848 Self::ElementIndexOutOfBounds { index, bound } => {
849 write!(formatter, "element index {index} is outside bound {bound}")
850 }
851 Self::DuplicateElement { index } => {
852 write!(formatter, "duplicate pagerank element index {index}")
853 }
854 Self::DuplicateRelation { index } => {
855 write!(formatter, "duplicate pagerank relation index {index}")
856 }
857 Self::RelationIndexOutOfBounds { index, bound } => {
858 write!(formatter, "relation index {index} is outside bound {bound}")
859 }
860 Self::IncidenceIndexOutOfBounds { index, bound } => {
861 write!(
862 formatter,
863 "incidence index {index} is outside bound {bound}"
864 )
865 }
866 Self::InvalidRelationWeight { index, value } => {
867 write!(formatter, "invalid relation weight at {index}: {value:?}")
868 }
869 Self::InvalidIncidenceWeight { index, value } => {
870 write!(formatter, "invalid incidence weight at {index}: {value:?}")
871 }
872 Self::NonConverged { iterations, delta } => write!(
873 formatter,
874 "pagerank did not converge after {iterations} iterations; delta {delta:?}"
875 ),
876 }
877 }
878}
879
880impl<S: fmt::Debug> Error for PageRankError<S> {}
881
882#[derive(Debug)]
889#[must_use]
890pub struct PageRankScratch<'scratch, S> {
891 teleport: &'scratch mut [S],
893 next: &'scratch mut [S],
895 visible_elements: &'scratch mut [u8],
897}
898
899impl<'scratch, S> PageRankScratch<'scratch, S> {
900 pub const fn new(
906 teleport: &'scratch mut [S],
907 next: &'scratch mut [S],
908 visible_elements: &'scratch mut [u8],
909 ) -> Self {
910 Self {
911 teleport,
912 next,
913 visible_elements,
914 }
915 }
916
917 #[must_use]
923 pub const fn teleport_capacity(&self) -> usize {
924 self.teleport.len()
925 }
926
927 #[must_use]
933 pub const fn next_capacity(&self) -> usize {
934 self.next.len()
935 }
936
937 #[must_use]
943 pub const fn visible_element_capacity(&self) -> usize {
944 self.visible_elements.len()
945 }
946}
947
948#[derive(Debug)]
955#[must_use]
956pub struct HypergraphPageRankScratch<'scratch, S> {
957 teleport: &'scratch mut [S],
959 next_elements: &'scratch mut [S],
961 next_relations: &'scratch mut [S],
963 visible_elements: &'scratch mut [u8],
965 visible_relations: &'scratch mut [u8],
967}
968
969impl<'scratch, S> HypergraphPageRankScratch<'scratch, S> {
970 pub const fn new(
976 teleport: &'scratch mut [S],
977 next_elements: &'scratch mut [S],
978 next_relations: &'scratch mut [S],
979 visible_elements: &'scratch mut [u8],
980 visible_relations: &'scratch mut [u8],
981 ) -> Self {
982 Self {
983 teleport,
984 next_elements,
985 next_relations,
986 visible_elements,
987 visible_relations,
988 }
989 }
990
991 #[must_use]
997 pub const fn teleport_capacity(&self) -> usize {
998 self.teleport.len()
999 }
1000}
1001
1002#[cfg(feature = "alloc")]
1011#[derive(Clone, Debug)]
1012pub struct PageRankWorkspace<G, S> {
1013 teleport: Vec<S>,
1015 next: Vec<S>,
1017 visible_elements: Vec<u8>,
1019 _graph: PhantomData<fn() -> G>,
1021}
1022
1023#[cfg(feature = "alloc")]
1024impl<G, S: PageRankScalar> Default for PageRankWorkspace<G, S> {
1025 fn default() -> Self {
1026 Self::new()
1027 }
1028}
1029
1030#[cfg(feature = "alloc")]
1031impl<G, S: PageRankScalar> PageRankWorkspace<G, S> {
1032 #[must_use]
1038 pub const fn new() -> Self {
1039 Self {
1040 teleport: Vec::new(),
1041 next: Vec::new(),
1042 visible_elements: Vec::new(),
1043 _graph: PhantomData,
1044 }
1045 }
1046
1047 #[must_use]
1053 pub fn for_graph(graph: &G) -> Self
1054 where
1055 G: DenseElementIndex,
1056 {
1057 Self::with_element_bound(graph.element_bound())
1058 }
1059
1060 #[must_use]
1066 pub fn with_element_bound(element_bound: usize) -> Self {
1067 Self {
1068 teleport: vec![S::ZERO; element_bound],
1069 next: vec![S::ZERO; element_bound],
1070 visible_elements: vec![0; element_bound],
1071 _graph: PhantomData,
1072 }
1073 }
1074
1075 #[must_use]
1081 pub const fn element_bound_capacity(&self) -> usize {
1082 self.teleport.len()
1083 }
1084
1085 fn ensure_element_bound(&mut self, element_bound: usize) {
1087 if self.teleport.len() < element_bound {
1088 self.teleport.resize(element_bound, S::ZERO);
1089 }
1090 if self.next.len() < element_bound {
1091 self.next.resize(element_bound, S::ZERO);
1092 }
1093 if self.visible_elements.len() < element_bound {
1094 self.visible_elements.resize(element_bound, 0);
1095 }
1096 }
1097
1098 fn as_scratch(&mut self) -> PageRankScratch<'_, S> {
1100 PageRankScratch::new(
1101 &mut self.teleport,
1102 &mut self.next,
1103 &mut self.visible_elements,
1104 )
1105 }
1106}
1107
1108#[cfg(feature = "alloc")]
1114#[derive(Clone, Debug)]
1115pub struct HypergraphPageRankWorkspace<H, S> {
1116 teleport: Vec<S>,
1118 next_elements: Vec<S>,
1120 next_relations: Vec<S>,
1122 visible_elements: Vec<u8>,
1124 visible_relations: Vec<u8>,
1126 _hypergraph: PhantomData<fn() -> H>,
1128}
1129
1130#[cfg(feature = "alloc")]
1131impl<H, S: PageRankScalar> Default for HypergraphPageRankWorkspace<H, S> {
1132 fn default() -> Self {
1133 Self::new()
1134 }
1135}
1136
1137#[cfg(feature = "alloc")]
1138impl<H, S: PageRankScalar> HypergraphPageRankWorkspace<H, S> {
1139 #[must_use]
1145 pub const fn new() -> Self {
1146 Self {
1147 teleport: Vec::new(),
1148 next_elements: Vec::new(),
1149 next_relations: Vec::new(),
1150 visible_elements: Vec::new(),
1151 visible_relations: Vec::new(),
1152 _hypergraph: PhantomData,
1153 }
1154 }
1155
1156 #[must_use]
1162 pub fn for_hypergraph(hypergraph: &H) -> Self
1163 where
1164 H: DenseElementIndex + DenseRelationIndex,
1165 {
1166 Self::with_bounds(hypergraph.element_bound(), hypergraph.relation_bound())
1167 }
1168
1169 #[must_use]
1175 pub fn with_bounds(element_bound: usize, relation_bound: usize) -> Self {
1176 let state_bound = element_bound.saturating_add(relation_bound);
1177 Self {
1178 teleport: vec![S::ZERO; state_bound],
1179 next_elements: vec![S::ZERO; element_bound],
1180 next_relations: vec![S::ZERO; relation_bound],
1181 visible_elements: vec![0; element_bound],
1182 visible_relations: vec![0; relation_bound],
1183 _hypergraph: PhantomData,
1184 }
1185 }
1186
1187 #[must_use]
1193 pub const fn element_bound_capacity(&self) -> usize {
1194 self.next_elements.len()
1195 }
1196
1197 #[must_use]
1203 pub const fn relation_bound_capacity(&self) -> usize {
1204 self.next_relations.len()
1205 }
1206
1207 fn ensure_bounds(&mut self, element_bound: usize, relation_bound: usize, state_bound: usize) {
1209 if self.teleport.len() < state_bound {
1210 self.teleport.resize(state_bound, S::ZERO);
1211 }
1212 if self.next_elements.len() < element_bound {
1213 self.next_elements.resize(element_bound, S::ZERO);
1214 }
1215 if self.next_relations.len() < relation_bound {
1216 self.next_relations.resize(relation_bound, S::ZERO);
1217 }
1218 if self.visible_elements.len() < element_bound {
1219 self.visible_elements.resize(element_bound, 0);
1220 }
1221 if self.visible_relations.len() < relation_bound {
1222 self.visible_relations.resize(relation_bound, 0);
1223 }
1224 }
1225
1226 fn as_scratch(&mut self) -> HypergraphPageRankScratch<'_, S> {
1228 HypergraphPageRankScratch::new(
1229 &mut self.teleport,
1230 &mut self.next_elements,
1231 &mut self.next_relations,
1232 &mut self.visible_elements,
1233 &mut self.visible_relations,
1234 )
1235 }
1236}
1237
1238#[cfg(feature = "alloc")]
1260#[expect(
1261 clippy::too_many_arguments,
1262 reason = "PageRank entry point keeps graph, distribution, elements, config, personalization, and output explicit"
1263)]
1264pub fn pagerank_graph<G, D, I, S>(
1265 graph: &G,
1266 distribution: &D,
1267 elements: I,
1268 config: PageRankConfig<S>,
1269 personalization: Option<&[S]>,
1270 ranks: &mut [S],
1271) -> Result<PageRankReport<S>, PageRankError<S>>
1272where
1273 G: ForwardGraph + DenseElementIndex,
1274 D: OutgoingDistribution<G, S>,
1275 I: Clone + IntoIterator<Item = ElementId<G>>,
1276 S: PageRankScalar,
1277{
1278 let bound = graph.element_bound();
1279 let mut teleport = vec![S::ZERO; bound];
1280 let mut next = vec![S::ZERO; bound];
1281 let mut visible_elements = vec![0; bound];
1282 pagerank_graph_with_scratch(
1283 graph,
1284 distribution,
1285 elements,
1286 config,
1287 personalization,
1288 ranks,
1289 PageRankScratch::new(&mut teleport, &mut next, &mut visible_elements),
1290 )
1291}
1292
1293#[expect(
1308 clippy::too_many_arguments,
1309 clippy::needless_pass_by_value,
1310 reason = "PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1311)]
1312pub fn pagerank_graph_with_scratch<G, D, I, S>(
1313 graph: &G,
1314 distribution: &D,
1315 elements: I,
1316 config: PageRankConfig<S>,
1317 personalization: Option<&[S]>,
1318 ranks: &mut [S],
1319 scratch: PageRankScratch<'_, S>,
1320) -> Result<PageRankReport<S>, PageRankError<S>>
1321where
1322 G: ForwardGraph + DenseElementIndex,
1323 D: OutgoingDistribution<G, S>,
1324 I: Clone + IntoIterator<Item = ElementId<G>>,
1325 S: PageRankScalar,
1326{
1327 validate_config(config)?;
1328 let bound = graph.element_bound();
1329 ensure_output_len(ranks.len(), bound)?;
1330 ensure_scratch_len("teleport", scratch.teleport.len(), bound)?;
1331 ensure_scratch_len("next", scratch.next.len(), bound)?;
1332 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), bound)?;
1333 build_personalization_into(
1334 elements.clone(),
1335 bound,
1336 personalization,
1337 |element| graph.element_index(element),
1338 scratch.teleport,
1339 scratch.visible_elements,
1340 )?;
1341 initialize_ranks(elements.clone(), graph, scratch.teleport, ranks)?;
1342 iterate_graph(
1343 graph,
1344 distribution,
1345 elements,
1346 config,
1347 scratch.teleport,
1348 scratch.visible_elements,
1349 ranks,
1350 scratch.next,
1351 )
1352}
1353
1354#[cfg(feature = "alloc")]
1369#[expect(
1370 clippy::too_many_arguments,
1371 reason = "PageRank workspace API keeps policy and reusable storage inputs explicit"
1372)]
1373pub fn pagerank_graph_with_workspace<G, D, I, S>(
1374 graph: &G,
1375 distribution: &D,
1376 elements: I,
1377 config: PageRankConfig<S>,
1378 personalization: Option<&[S]>,
1379 ranks: &mut [S],
1380 workspace: &mut PageRankWorkspace<G, S>,
1381) -> Result<PageRankReport<S>, PageRankError<S>>
1382where
1383 G: ForwardGraph + DenseElementIndex,
1384 D: OutgoingDistribution<G, S>,
1385 I: Clone + IntoIterator<Item = ElementId<G>>,
1386 S: PageRankScalar,
1387{
1388 workspace.ensure_element_bound(graph.element_bound());
1389 pagerank_graph_with_scratch(
1390 graph,
1391 distribution,
1392 elements,
1393 config,
1394 personalization,
1395 ranks,
1396 workspace.as_scratch(),
1397 )
1398}
1399
1400#[cfg(feature = "alloc")]
1427#[expect(
1428 clippy::too_many_arguments,
1429 reason = "hypergraph PageRank entry point keeps hypergraph, distribution, state families, and output explicit"
1430)]
1431pub fn pagerank_hypergraph<H, D, IE, IR, S>(
1432 hypergraph: &H,
1433 distribution: &D,
1434 elements: IE,
1435 relations: IR,
1436 config: PageRankConfig<S>,
1437 personalization: Option<&[S]>,
1438 element_ranks: &mut [S],
1439 relation_ranks: &mut [S],
1440) -> Result<PageRankReport<S>, PageRankError<S>>
1441where
1442 H: PageRankHypergraph,
1443 D: HypergraphOutgoingDistribution<H, S>,
1444 IE: Clone + IntoIterator<Item = ElementId<H>>,
1445 IR: Clone + IntoIterator<Item = RelationId<H>>,
1446 S: PageRankScalar,
1447{
1448 let e_bound = hypergraph.element_bound();
1449 let r_bound = hypergraph.relation_bound();
1450 let state_bound =
1451 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1452 let mut teleport = vec![S::ZERO; state_bound];
1453 let mut next_elements = vec![S::ZERO; e_bound];
1454 let mut next_relations = vec![S::ZERO; r_bound];
1455 let mut visible_elements = vec![0; e_bound];
1456 let mut visible_relations = vec![0; r_bound];
1457 pagerank_hypergraph_with_scratch(
1458 hypergraph,
1459 distribution,
1460 elements,
1461 relations,
1462 config,
1463 personalization,
1464 element_ranks,
1465 relation_ranks,
1466 HypergraphPageRankScratch::new(
1467 &mut teleport,
1468 &mut next_elements,
1469 &mut next_relations,
1470 &mut visible_elements,
1471 &mut visible_relations,
1472 ),
1473 )
1474}
1475
1476#[expect(
1492 clippy::too_many_arguments,
1493 reason = "hypergraph PageRank scratch entry point keeps policy and storage inputs explicit"
1494)]
1495#[expect(
1496 clippy::needless_pass_by_value,
1497 reason = "hypergraph PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1498)]
1499pub fn pagerank_hypergraph_with_scratch<H, D, IE, IR, S>(
1500 hypergraph: &H,
1501 distribution: &D,
1502 elements: IE,
1503 relations: IR,
1504 config: PageRankConfig<S>,
1505 personalization: Option<&[S]>,
1506 element_ranks: &mut [S],
1507 relation_ranks: &mut [S],
1508 scratch: HypergraphPageRankScratch<'_, S>,
1509) -> Result<PageRankReport<S>, PageRankError<S>>
1510where
1511 H: PageRankHypergraph,
1512 D: HypergraphOutgoingDistribution<H, S>,
1513 IE: Clone + IntoIterator<Item = ElementId<H>>,
1514 IR: Clone + IntoIterator<Item = RelationId<H>>,
1515 S: PageRankScalar,
1516{
1517 validate_config(config)?;
1518 let e_bound = hypergraph.element_bound();
1519 let r_bound = hypergraph.relation_bound();
1520 let state_bound =
1521 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1522 ensure_scratch_len("teleport", scratch.teleport.len(), state_bound)?;
1523 ensure_scratch_len("next_elements", scratch.next_elements.len(), e_bound)?;
1524 ensure_scratch_len("next_relations", scratch.next_relations.len(), r_bound)?;
1525 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), e_bound)?;
1526 ensure_scratch_len(
1527 "visible_relations",
1528 scratch.visible_relations.len(),
1529 r_bound,
1530 )?;
1531 build_hyper_personalization_into(
1532 hypergraph,
1533 elements.clone(),
1534 relations.clone(),
1535 state_bound,
1536 personalization,
1537 scratch.teleport,
1538 scratch.visible_elements,
1539 scratch.visible_relations,
1540 )?;
1541 initialize_hyper_ranks(
1542 hypergraph,
1543 elements.clone(),
1544 relations.clone(),
1545 scratch.teleport,
1546 element_ranks,
1547 relation_ranks,
1548 )?;
1549 iterate_hypergraph(
1550 hypergraph,
1551 distribution,
1552 elements,
1553 relations,
1554 config,
1555 scratch.teleport,
1556 scratch.visible_elements,
1557 scratch.visible_relations,
1558 element_ranks,
1559 relation_ranks,
1560 scratch.next_elements,
1561 scratch.next_relations,
1562 )
1563}
1564
1565#[cfg(feature = "alloc")]
1580#[expect(
1581 clippy::too_many_arguments,
1582 reason = "hypergraph PageRank workspace entry point keeps policy and storage inputs explicit"
1583)]
1584pub fn pagerank_hypergraph_with_workspace<H, D, IE, IR, S>(
1585 hypergraph: &H,
1586 distribution: &D,
1587 elements: IE,
1588 relations: IR,
1589 config: PageRankConfig<S>,
1590 personalization: Option<&[S]>,
1591 element_ranks: &mut [S],
1592 relation_ranks: &mut [S],
1593 workspace: &mut HypergraphPageRankWorkspace<H, S>,
1594) -> Result<PageRankReport<S>, PageRankError<S>>
1595where
1596 H: PageRankHypergraph,
1597 D: HypergraphOutgoingDistribution<H, S>,
1598 IE: Clone + IntoIterator<Item = ElementId<H>>,
1599 IR: Clone + IntoIterator<Item = RelationId<H>>,
1600 S: PageRankScalar,
1601{
1602 let e_bound = hypergraph.element_bound();
1603 let r_bound = hypergraph.relation_bound();
1604 let state_bound =
1605 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1606 workspace.ensure_bounds(e_bound, r_bound, state_bound);
1607 pagerank_hypergraph_with_scratch(
1608 hypergraph,
1609 distribution,
1610 elements,
1611 relations,
1612 config,
1613 personalization,
1614 element_ranks,
1615 relation_ranks,
1616 workspace.as_scratch(),
1617 )
1618}
1619
1620fn validate_config<S: PageRankScalar>(config: PageRankConfig<S>) -> Result<(), PageRankError<S>> {
1621 if !config.damping.is_finite() || config.damping < S::ZERO || config.damping > S::ONE {
1622 return Err(PageRankError::InvalidDamping {
1623 damping: config.damping,
1624 });
1625 }
1626 if !config.tolerance.is_finite() || config.tolerance < S::ZERO {
1627 return Err(PageRankError::InvalidTolerance {
1628 tolerance: config.tolerance,
1629 });
1630 }
1631 if config.max_iterations == 0 {
1632 return Err(PageRankError::InvalidMaxIterations);
1633 }
1634 Ok(())
1635}
1636
1637const fn ensure_output_len<S>(actual: usize, required: usize) -> Result<(), PageRankError<S>> {
1638 if actual < required {
1639 Err(PageRankError::OutputTooShort { required, actual })
1640 } else {
1641 Ok(())
1642 }
1643}
1644
1645const fn ensure_scratch_len<S>(
1646 name: &'static str,
1647 actual: usize,
1648 required: usize,
1649) -> Result<(), PageRankError<S>> {
1650 if actual < required {
1651 Err(PageRankError::ScratchTooShort {
1652 name,
1653 required,
1654 actual,
1655 })
1656 } else {
1657 Ok(())
1658 }
1659}
1660
1661fn checked_state_bound<S>(
1662 e_bound: usize,
1663 r_bound: usize,
1664 element_output_len: usize,
1665 relation_output_len: usize,
1666) -> Result<usize, PageRankError<S>> {
1667 ensure_output_len(element_output_len, e_bound)?;
1668 ensure_output_len(relation_output_len, r_bound)?;
1669 e_bound
1670 .checked_add(r_bound)
1671 .ok_or_else(|| PageRankError::OutputTooShort {
1672 required: usize::MAX,
1673 actual: element_output_len.saturating_add(relation_output_len),
1674 })
1675}
1676
1677fn clear<S: PageRankScalar>(values: &mut [S], len: usize) {
1678 for value in &mut values[..len] {
1679 *value = S::ZERO;
1680 }
1681}
1682
1683fn clear_u8(values: &mut [u8], len: usize) {
1684 for value in &mut values[..len] {
1685 *value = 0;
1686 }
1687}
1688
1689fn mark_visible_element<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1690 if visible[index] != 0 {
1691 return Err(PageRankError::DuplicateElement { index });
1692 }
1693 visible[index] = 1;
1694 Ok(())
1695}
1696
1697fn mark_visible_relation<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1698 if visible[index] != 0 {
1699 return Err(PageRankError::DuplicateRelation { index });
1700 }
1701 visible[index] = 1;
1702 Ok(())
1703}
1704
1705fn is_visible(visible: &[u8], index: usize) -> bool {
1710 visible[index] != 0
1711}
1712
1713#[expect(
1714 clippy::too_many_arguments,
1715 reason = "personalization normalization keeps topology family bounds and caller buffers explicit"
1716)]
1717fn build_personalization_into<I, F, S>(
1718 elements: I,
1719 bound: usize,
1720 personalization: Option<&[S]>,
1721 index_of: F,
1722 out: &mut [S],
1723 visible: &mut [u8],
1724) -> Result<(), PageRankError<S>>
1725where
1726 I: IntoIterator,
1727 F: Fn(I::Item) -> usize,
1728 S: PageRankScalar,
1729{
1730 clear(out, bound);
1731 clear_u8(visible, bound);
1732 let mut count = 0_usize;
1733 let mut sum = S::ZERO;
1734 if let Some(input) = personalization {
1735 if input.len() < bound {
1736 return Err(PageRankError::PersonalizationTooShort {
1737 required: bound,
1738 actual: input.len(),
1739 });
1740 }
1741 for element in elements {
1742 let index = index_of(element);
1743 check_index(index, bound)?;
1744 mark_visible_element(visible, index)?;
1745 let value = input[index];
1746 check_personalization_value(index, value)?;
1747 out[index] = value;
1748 sum += value;
1749 count += 1;
1750 }
1751 } else {
1752 for element in elements {
1753 let index = index_of(element);
1754 check_index(index, bound)?;
1755 mark_visible_element(visible, index)?;
1756 out[index] = S::ONE;
1757 sum += S::ONE;
1758 count += 1;
1759 }
1760 }
1761 normalize_personalization(out, count, sum)
1762}
1763
1764enum PersonalizationSource<'a, S> {
1775 Uniform,
1777 FromInput(&'a [S]),
1781}
1782
1783impl<S> PersonalizationSource<'_, S>
1784where
1785 S: PageRankScalar,
1786{
1787 fn value_at(&self, state_index: usize) -> Result<S, PageRankError<S>> {
1797 match *self {
1798 Self::Uniform => Ok(S::ONE),
1799 Self::FromInput(input) => {
1800 let value = input[state_index];
1801 check_personalization_value(state_index, value)?;
1802 Ok(value)
1803 }
1804 }
1805 }
1806
1807 #[expect(
1830 clippy::too_many_arguments,
1831 reason = "method threads separate element/relation iterables, scratch slices, and the state bound; bundling them obscures the borrow shape that callers explicitly construct"
1832 )]
1833 fn fill_hypergraph<H, IE, IR>(
1834 &self,
1835 hypergraph: &H,
1836 elements: IE,
1837 relations: IR,
1838 state_bound: usize,
1839 out: &mut [S],
1840 visible_elements: &mut [u8],
1841 visible_relations: &mut [u8],
1842 ) -> Result<(usize, S), PageRankError<S>>
1843 where
1844 H: DenseElementIndex + DenseRelationIndex,
1845 IE: IntoIterator<Item = ElementId<H>>,
1846 IR: IntoIterator<Item = RelationId<H>>,
1847 {
1848 if let Self::FromInput(input) = *self
1849 && input.len() < state_bound
1850 {
1851 return Err(PageRankError::PersonalizationTooShort {
1852 required: state_bound,
1853 actual: input.len(),
1854 });
1855 }
1856 let e_bound = hypergraph.element_bound();
1857 let r_bound = hypergraph.relation_bound();
1858 let mut count = 0_usize;
1859 let mut sum = S::ZERO;
1860 for element in elements {
1861 let index = hypergraph.element_index(element);
1862 check_index(index, e_bound)?;
1863 mark_visible_element(visible_elements, index)?;
1864 let value = self.value_at(index)?;
1865 out[index] = value;
1866 sum += value;
1867 count += 1;
1868 }
1869 for relation in relations {
1870 let index = hypergraph.relation_index(relation);
1871 check_relation_index(index, r_bound)?;
1872 mark_visible_relation(visible_relations, index)?;
1873 let state = e_bound + index;
1874 let value = self.value_at(state)?;
1875 out[state] = value;
1876 sum += value;
1877 count += 1;
1878 }
1879 Ok((count, sum))
1880 }
1881}
1882
1883#[expect(
1884 clippy::too_many_arguments,
1885 reason = "helper threads separate element/relation state and caller scratch explicitly"
1886)]
1887fn build_hyper_personalization_into<H, IE, IR, S>(
1888 hypergraph: &H,
1889 elements: IE,
1890 relations: IR,
1891 state_bound: usize,
1892 personalization: Option<&[S]>,
1893 out: &mut [S],
1894 visible_elements: &mut [u8],
1895 visible_relations: &mut [u8],
1896) -> Result<(), PageRankError<S>>
1897where
1898 H: DenseElementIndex + DenseRelationIndex,
1899 IE: IntoIterator<Item = ElementId<H>>,
1900 IR: IntoIterator<Item = RelationId<H>>,
1901 S: PageRankScalar,
1902{
1903 clear(out, state_bound);
1904 clear_u8(visible_elements, hypergraph.element_bound());
1905 clear_u8(visible_relations, hypergraph.relation_bound());
1906 let source = personalization.map_or(
1907 PersonalizationSource::Uniform,
1908 PersonalizationSource::FromInput,
1909 );
1910 let (count, sum) = source.fill_hypergraph(
1911 hypergraph,
1912 elements,
1913 relations,
1914 state_bound,
1915 out,
1916 visible_elements,
1917 visible_relations,
1918 )?;
1919 normalize_personalization(out, count, sum)
1920}
1921
1922fn normalize_personalization<S: PageRankScalar>(
1923 out: &mut [S],
1924 count: usize,
1925 sum: S,
1926) -> Result<(), PageRankError<S>> {
1927 if count == 0 {
1928 return Err(PageRankError::EmptyState);
1929 }
1930 if sum <= S::ZERO {
1931 return Err(PageRankError::ZeroPersonalization);
1932 }
1933 for value in out {
1934 *value = *value / sum;
1935 }
1936 Ok(())
1937}
1938
1939fn check_personalization_value<S: PageRankScalar>(
1940 index: usize,
1941 value: S,
1942) -> Result<(), PageRankError<S>> {
1943 if !value.is_finite() || value < S::ZERO {
1944 Err(PageRankError::InvalidPersonalization { index, value })
1945 } else {
1946 Ok(())
1947 }
1948}
1949
1950fn initialize_ranks<G, I, S>(
1951 elements: I,
1952 graph: &G,
1953 teleport: &[S],
1954 ranks: &mut [S],
1955) -> Result<(), PageRankError<S>>
1956where
1957 G: DenseElementIndex,
1958 I: IntoIterator<Item = ElementId<G>>,
1959 S: PageRankScalar,
1960{
1961 clear(ranks, graph.element_bound());
1962 for element in elements {
1963 let index = graph.element_index(element);
1964 check_index(index, graph.element_bound())?;
1965 ranks[index] = teleport[index];
1966 }
1967 Ok(())
1968}
1969
1970#[expect(
1971 clippy::too_many_arguments,
1972 reason = "initialization writes separate element and relation rank slices"
1973)]
1974fn initialize_hyper_ranks<H, IE, IR, S>(
1975 hypergraph: &H,
1976 elements: IE,
1977 relations: IR,
1978 teleport: &[S],
1979 element_ranks: &mut [S],
1980 relation_ranks: &mut [S],
1981) -> Result<(), PageRankError<S>>
1982where
1983 H: DenseElementIndex + DenseRelationIndex,
1984 IE: IntoIterator<Item = ElementId<H>>,
1985 IR: IntoIterator<Item = RelationId<H>>,
1986 S: PageRankScalar,
1987{
1988 clear(element_ranks, hypergraph.element_bound());
1989 clear(relation_ranks, hypergraph.relation_bound());
1990 for element in elements {
1991 let index = hypergraph.element_index(element);
1992 check_index(index, hypergraph.element_bound())?;
1993 element_ranks[index] = teleport[index];
1994 }
1995 for relation in relations {
1996 let index = hypergraph.relation_index(relation);
1997 check_relation_index(index, hypergraph.relation_bound())?;
1998 relation_ranks[index] = teleport[hypergraph.element_bound() + index];
1999 }
2000 Ok(())
2001}
2002
2003#[expect(
2004 clippy::too_many_arguments,
2005 reason = "graph iteration helper keeps distribution, scratch, and policy inputs explicit"
2006)]
2007#[expect(
2008 clippy::needless_pass_by_value,
2009 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
2010)]
2011fn iterate_graph<G, D, I, S>(
2012 graph: &G,
2013 distribution: &D,
2014 elements: I,
2015 config: PageRankConfig<S>,
2016 teleport: &[S],
2017 visible: &[u8],
2018 ranks: &mut [S],
2019 next: &mut [S],
2020) -> Result<PageRankReport<S>, PageRankError<S>>
2021where
2022 G: ForwardGraph + DenseElementIndex,
2023 D: OutgoingDistribution<G, S>,
2024 I: Clone + IntoIterator<Item = ElementId<G>>,
2025 S: PageRankScalar,
2026{
2027 let mut last_delta = S::INFINITY;
2028 for iteration in 1..=config.max_iterations {
2029 clear(next, graph.element_bound());
2030 let mut dangling = S::ZERO;
2031 for element in elements.clone() {
2032 let index = checked_element_index(graph, element)?;
2033 let rank = ranks[index];
2034 dangling += distribution.distribute_outgoing(graph, element, rank, next, visible)?;
2035 }
2036 let delta = apply_graph_teleport(
2037 graph,
2038 elements.clone(),
2039 config,
2040 teleport,
2041 dangling,
2042 ranks,
2043 next,
2044 )?;
2045 last_delta = delta;
2046 if delta <= config.tolerance {
2047 return Ok(PageRankReport {
2048 iterations: iteration,
2049 delta,
2050 });
2051 }
2052 }
2053 Err(PageRankError::NonConverged {
2054 iterations: config.max_iterations,
2055 delta: last_delta,
2056 })
2057}
2058
2059#[expect(
2060 clippy::too_many_arguments,
2061 reason = "hypergraph iteration helper keeps distribution, state families, scratch, and policy inputs explicit"
2062)]
2063#[expect(
2064 clippy::needless_pass_by_value,
2065 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
2066)]
2067fn iterate_hypergraph<H, D, IE, IR, S>(
2068 hypergraph: &H,
2069 distribution: &D,
2070 elements: IE,
2071 relations: IR,
2072 config: PageRankConfig<S>,
2073 teleport: &[S],
2074 visible_elements: &[u8],
2075 visible_relations: &[u8],
2076 element_ranks: &mut [S],
2077 relation_ranks: &mut [S],
2078 next_elements: &mut [S],
2079 next_relations: &mut [S],
2080) -> Result<PageRankReport<S>, PageRankError<S>>
2081where
2082 H: PageRankHypergraph,
2083 D: HypergraphOutgoingDistribution<H, S>,
2084 IE: Clone + IntoIterator<Item = ElementId<H>>,
2085 IR: Clone + IntoIterator<Item = RelationId<H>>,
2086 S: PageRankScalar,
2087{
2088 let mut last_delta = S::INFINITY;
2089 for iteration in 1..=config.max_iterations {
2090 clear(next_elements, hypergraph.element_bound());
2091 clear(next_relations, hypergraph.relation_bound());
2092 let mut dangling = S::ZERO;
2093 for element in elements.clone() {
2094 let index = checked_element_index(hypergraph, element)?;
2095 let rank = element_ranks[index];
2096 dangling += distribution.distribute_from_element(
2097 hypergraph,
2098 element,
2099 rank,
2100 next_relations,
2101 visible_relations,
2102 )?;
2103 }
2104 for relation in relations.clone() {
2105 let index = checked_relation_index_for(hypergraph, relation)?;
2106 let rank = relation_ranks[index];
2107 dangling += distribution.distribute_from_relation(
2108 hypergraph,
2109 relation,
2110 rank,
2111 next_elements,
2112 visible_elements,
2113 )?;
2114 }
2115 let delta = apply_hyper_teleport(
2116 hypergraph,
2117 elements.clone(),
2118 relations.clone(),
2119 config,
2120 teleport,
2121 dangling,
2122 element_ranks,
2123 relation_ranks,
2124 next_elements,
2125 next_relations,
2126 )?;
2127 last_delta = delta;
2128 if delta <= config.tolerance {
2129 return Ok(PageRankReport {
2130 iterations: iteration,
2131 delta,
2132 });
2133 }
2134 }
2135 Err(PageRankError::NonConverged {
2136 iterations: config.max_iterations,
2137 delta: last_delta,
2138 })
2139}
2140
2141fn checked_element_index<G: DenseElementIndex, S>(
2142 graph: &G,
2143 element: ElementId<G>,
2144) -> Result<usize, PageRankError<S>> {
2145 let index = graph.element_index(element);
2146 check_index(index, graph.element_bound())?;
2147 Ok(index)
2148}
2149
2150const fn check_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2151 if index < bound {
2152 Ok(())
2153 } else {
2154 Err(PageRankError::ElementIndexOutOfBounds { index, bound })
2155 }
2156}
2157
2158const fn check_relation_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2159 if index < bound {
2160 Ok(())
2161 } else {
2162 Err(PageRankError::RelationIndexOutOfBounds { index, bound })
2163 }
2164}
2165
2166fn checked_relation_weight<G, W, S>(
2167 graph: &G,
2168 weights: &W,
2169 relation: RelationId<G>,
2170) -> Result<S, PageRankError<S>>
2171where
2172 G: DenseRelationIndex,
2173 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2174 W::Weight: IntoPageRankScalar<S>,
2175 S: PageRankScalar,
2176{
2177 let index = graph.relation_index(relation);
2178 check_relation_index(index, graph.relation_bound())?;
2179 let value = weights.relation_weight(relation).into_pagerank_scalar();
2180 if !value.is_finite() || value < S::ZERO {
2181 Err(PageRankError::InvalidRelationWeight { index, value })
2182 } else {
2183 Ok(value)
2184 }
2185}
2186
2187fn checked_incidence_weight<H, W, S>(
2188 hypergraph: &H,
2189 weights: &W,
2190 incidence: H::IncidenceId,
2191) -> Result<S, PageRankError<S>>
2192where
2193 H: DenseIncidenceIndex,
2194 W: IncidenceWeight<
2195 ElementId = H::ElementId,
2196 RelationId = H::RelationId,
2197 IncidenceId = H::IncidenceId,
2198 >,
2199 W::Weight: IntoPageRankScalar<S>,
2200 S: PageRankScalar,
2201{
2202 let index = hypergraph.incidence_index(incidence);
2203 let bound = hypergraph.incidence_bound();
2204 if index >= bound {
2205 return Err(PageRankError::IncidenceIndexOutOfBounds { index, bound });
2206 }
2207 let value = weights.incidence_weight(incidence).into_pagerank_scalar();
2208 if !value.is_finite() || value < S::ZERO {
2209 Err(PageRankError::InvalidIncidenceWeight { index, value })
2210 } else {
2211 Ok(value)
2212 }
2213}
2214
2215fn outgoing_weight_total<G, W, S>(
2216 graph: &G,
2217 weights: &W,
2218 element: ElementId<G>,
2219 visible: &[u8],
2220) -> Result<S, PageRankError<S>>
2221where
2222 G: ForwardGraph + DenseElementIndex + DenseRelationIndex,
2223 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2224 W::Weight: IntoPageRankScalar<S>,
2225 S: PageRankScalar,
2226{
2227 let mut total = S::ZERO;
2228 for edge in graph.outgoing_edges(element) {
2229 let target = graph.target(edge);
2230 let target_index = checked_element_index(graph, target)?;
2231 if is_visible(visible, target_index) {
2232 total += checked_relation_weight(graph, weights, edge)?;
2233 }
2234 }
2235 Ok(total)
2236}
2237
2238fn teleport_update<S: PageRankScalar>(
2250 damping: S,
2251 teleport_scale: S,
2252 accumulated: S,
2253 teleport: S,
2254 previous: S,
2255) -> (S, S) {
2256 let value = (damping * accumulated) + (teleport_scale * teleport);
2257 let delta = (value - previous).abs();
2258 (value, delta)
2259}
2260
2261#[expect(
2262 clippy::too_many_arguments,
2263 reason = "teleport helper updates caller-provided rank and scratch slices"
2264)]
2265fn apply_graph_teleport<G, I, S>(
2266 graph: &G,
2267 elements: I,
2268 config: PageRankConfig<S>,
2269 teleport: &[S],
2270 dangling: S,
2271 ranks: &mut [S],
2272 next: &[S],
2273) -> Result<S, PageRankError<S>>
2274where
2275 G: DenseElementIndex,
2276 I: IntoIterator<Item = ElementId<G>>,
2277 S: PageRankScalar,
2278{
2279 let mut delta = S::ZERO;
2280 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2281 for element in elements {
2282 let index = checked_element_index(graph, element)?;
2283 let (value, state_delta) = teleport_update(
2284 config.damping,
2285 teleport_scale,
2286 next[index],
2287 teleport[index],
2288 ranks[index],
2289 );
2290 delta += state_delta;
2291 ranks[index] = value;
2292 }
2293 Ok(delta)
2294}
2295
2296fn checked_relation_index_for<H: DenseRelationIndex, S>(
2297 hypergraph: &H,
2298 relation: RelationId<H>,
2299) -> Result<usize, PageRankError<S>> {
2300 let index = hypergraph.relation_index(relation);
2301 check_relation_index(index, hypergraph.relation_bound())?;
2302 Ok(index)
2303}
2304
2305fn hyper_outgoing_relation_weight<H, W, S>(
2306 hypergraph: &H,
2307 weights: &W,
2308 element: ElementId<H>,
2309 visible_relations: &[u8],
2310) -> Result<S, PageRankError<S>>
2311where
2312 H: DirectedVertexHyperedges + DenseRelationIndex,
2313 W: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
2314 W::Weight: IntoPageRankScalar<S>,
2315 S: PageRankScalar,
2316{
2317 let mut total = S::ZERO;
2318 for relation in hypergraph.outgoing_hyperedges(element) {
2319 let relation_index = checked_relation_index_for(hypergraph, relation)?;
2320 if is_visible(visible_relations, relation_index) {
2321 total += checked_relation_weight(hypergraph, weights, relation)?;
2322 }
2323 }
2324 Ok(total)
2325}
2326
2327fn hyper_target_incidence_weight<H, W, S>(
2328 hypergraph: &H,
2329 weights: &W,
2330 relation: RelationId<H>,
2331 visible_elements: &[u8],
2332) -> Result<S, PageRankError<S>>
2333where
2334 H: DirectedHyperedgeIncidences + IncidenceElement + DenseElementIndex + DenseIncidenceIndex,
2335 W: IncidenceWeight<
2336 ElementId = H::ElementId,
2337 RelationId = H::RelationId,
2338 IncidenceId = H::IncidenceId,
2339 >,
2340 W::Weight: IntoPageRankScalar<S>,
2341 S: PageRankScalar,
2342{
2343 let mut total = S::ZERO;
2344 for incidence in hypergraph.target_incidences(relation) {
2345 let target = hypergraph.incidence_element(incidence);
2346 let target_index = checked_element_index(hypergraph, target)?;
2347 if is_visible(visible_elements, target_index) {
2348 total += checked_incidence_weight(hypergraph, weights, incidence)?;
2349 }
2350 }
2351 Ok(total)
2352}
2353
2354#[expect(
2355 clippy::too_many_arguments,
2356 reason = "hypergraph teleport updates separate element and relation slices"
2357)]
2358fn apply_hyper_teleport<H, IE, IR, S>(
2359 hypergraph: &H,
2360 elements: IE,
2361 relations: IR,
2362 config: PageRankConfig<S>,
2363 teleport: &[S],
2364 dangling: S,
2365 element_ranks: &mut [S],
2366 relation_ranks: &mut [S],
2367 next_elements: &[S],
2368 next_relations: &[S],
2369) -> Result<S, PageRankError<S>>
2370where
2371 H: DenseElementIndex + DenseRelationIndex,
2372 IE: IntoIterator<Item = ElementId<H>>,
2373 IR: IntoIterator<Item = RelationId<H>>,
2374 S: PageRankScalar,
2375{
2376 let mut delta = S::ZERO;
2377 let e_bound = hypergraph.element_bound();
2378 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2379 for element in elements {
2380 let index = checked_element_index(hypergraph, element)?;
2381 let (value, state_delta) = teleport_update(
2382 config.damping,
2383 teleport_scale,
2384 next_elements[index],
2385 teleport[index],
2386 element_ranks[index],
2387 );
2388 delta += state_delta;
2389 element_ranks[index] = value;
2390 }
2391 for relation in relations {
2392 let index = checked_relation_index_for(hypergraph, relation)?;
2393 let state = e_bound + index;
2394 let (value, state_delta) = teleport_update(
2395 config.damping,
2396 teleport_scale,
2397 next_relations[index],
2398 teleport[state],
2399 relation_ranks[index],
2400 );
2401 delta += state_delta;
2402 relation_ranks[index] = value;
2403 }
2404 Ok(delta)
2405}