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
678#[derive(Clone, Copy, Debug, PartialEq)]
684#[non_exhaustive]
685pub struct PageRankReport<S> {
686 pub iterations: usize,
688 pub delta: S,
690}
691
692#[derive(Debug, Clone, PartialEq)]
698#[non_exhaustive]
699pub enum PageRankError<S> {
700 EmptyState,
702 InvalidDamping {
704 damping: S,
706 },
707 InvalidTolerance {
709 tolerance: S,
711 },
712 InvalidMaxIterations,
714 OutputTooShort {
716 required: usize,
718 actual: usize,
720 },
721 ScratchTooShort {
723 name: &'static str,
725 required: usize,
727 actual: usize,
729 },
730 PersonalizationTooShort {
732 required: usize,
734 actual: usize,
736 },
737 InvalidPersonalization {
739 index: usize,
741 value: S,
743 },
744 ZeroPersonalization,
746 ElementIndexOutOfBounds {
748 index: usize,
750 bound: usize,
752 },
753 DuplicateElement {
755 index: usize,
757 },
758 DuplicateRelation {
760 index: usize,
762 },
763 RelationIndexOutOfBounds {
765 index: usize,
767 bound: usize,
769 },
770 IncidenceIndexOutOfBounds {
772 index: usize,
774 bound: usize,
776 },
777 InvalidRelationWeight {
779 index: usize,
781 value: S,
783 },
784 InvalidIncidenceWeight {
786 index: usize,
788 value: S,
790 },
791 NonConverged {
793 iterations: usize,
795 delta: S,
797 },
798}
799
800impl<S: fmt::Debug> fmt::Display for PageRankError<S> {
801 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
802 match self {
803 Self::EmptyState => formatter.write_str("pagerank state set is empty"),
804 Self::InvalidDamping { damping } => {
805 write!(formatter, "invalid pagerank damping {damping:?}")
806 }
807 Self::InvalidTolerance { tolerance } => {
808 write!(formatter, "invalid pagerank tolerance {tolerance:?}")
809 }
810 Self::InvalidMaxIterations => {
811 formatter.write_str("pagerank max_iterations must be positive")
812 }
813 Self::OutputTooShort { required, actual } => write!(
814 formatter,
815 "pagerank output too short: required {required}, got {actual}"
816 ),
817 Self::ScratchTooShort {
818 name,
819 required,
820 actual,
821 } => write!(
822 formatter,
823 "pagerank scratch '{name}' too short: required {required}, got {actual}"
824 ),
825 Self::PersonalizationTooShort { required, actual } => write!(
826 formatter,
827 "pagerank personalization too short: required {required}, got {actual}"
828 ),
829 Self::InvalidPersonalization { index, value } => write!(
830 formatter,
831 "invalid pagerank personalization at {index}: {value:?}"
832 ),
833 Self::ZeroPersonalization => {
834 formatter.write_str("pagerank personalization sum is zero")
835 }
836 Self::ElementIndexOutOfBounds { index, bound } => {
837 write!(formatter, "element index {index} is outside bound {bound}")
838 }
839 Self::DuplicateElement { index } => {
840 write!(formatter, "duplicate pagerank element index {index}")
841 }
842 Self::DuplicateRelation { index } => {
843 write!(formatter, "duplicate pagerank relation index {index}")
844 }
845 Self::RelationIndexOutOfBounds { index, bound } => {
846 write!(formatter, "relation index {index} is outside bound {bound}")
847 }
848 Self::IncidenceIndexOutOfBounds { index, bound } => {
849 write!(
850 formatter,
851 "incidence index {index} is outside bound {bound}"
852 )
853 }
854 Self::InvalidRelationWeight { index, value } => {
855 write!(formatter, "invalid relation weight at {index}: {value:?}")
856 }
857 Self::InvalidIncidenceWeight { index, value } => {
858 write!(formatter, "invalid incidence weight at {index}: {value:?}")
859 }
860 Self::NonConverged { iterations, delta } => write!(
861 formatter,
862 "pagerank did not converge after {iterations} iterations; delta {delta:?}"
863 ),
864 }
865 }
866}
867
868impl<S: fmt::Debug> Error for PageRankError<S> {}
869
870#[derive(Debug)]
877#[must_use]
878pub struct PageRankScratch<'scratch, S> {
879 teleport: &'scratch mut [S],
881 next: &'scratch mut [S],
883 visible_elements: &'scratch mut [u8],
885}
886
887impl<'scratch, S> PageRankScratch<'scratch, S> {
888 pub const fn new(
894 teleport: &'scratch mut [S],
895 next: &'scratch mut [S],
896 visible_elements: &'scratch mut [u8],
897 ) -> Self {
898 Self {
899 teleport,
900 next,
901 visible_elements,
902 }
903 }
904
905 #[must_use]
911 pub const fn teleport_capacity(&self) -> usize {
912 self.teleport.len()
913 }
914
915 #[must_use]
921 pub const fn next_capacity(&self) -> usize {
922 self.next.len()
923 }
924
925 #[must_use]
931 pub const fn visible_element_capacity(&self) -> usize {
932 self.visible_elements.len()
933 }
934}
935
936#[derive(Debug)]
943#[must_use]
944pub struct HypergraphPageRankScratch<'scratch, S> {
945 teleport: &'scratch mut [S],
947 next_elements: &'scratch mut [S],
949 next_relations: &'scratch mut [S],
951 visible_elements: &'scratch mut [u8],
953 visible_relations: &'scratch mut [u8],
955}
956
957impl<'scratch, S> HypergraphPageRankScratch<'scratch, S> {
958 pub const fn new(
964 teleport: &'scratch mut [S],
965 next_elements: &'scratch mut [S],
966 next_relations: &'scratch mut [S],
967 visible_elements: &'scratch mut [u8],
968 visible_relations: &'scratch mut [u8],
969 ) -> Self {
970 Self {
971 teleport,
972 next_elements,
973 next_relations,
974 visible_elements,
975 visible_relations,
976 }
977 }
978
979 #[must_use]
985 pub const fn teleport_capacity(&self) -> usize {
986 self.teleport.len()
987 }
988}
989
990#[cfg(feature = "alloc")]
999#[derive(Clone, Debug)]
1000pub struct PageRankWorkspace<G, S> {
1001 teleport: Vec<S>,
1003 next: Vec<S>,
1005 visible_elements: Vec<u8>,
1007 _graph: PhantomData<fn() -> G>,
1009}
1010
1011#[cfg(feature = "alloc")]
1012impl<G, S: PageRankScalar> Default for PageRankWorkspace<G, S> {
1013 fn default() -> Self {
1014 Self::new()
1015 }
1016}
1017
1018#[cfg(feature = "alloc")]
1019impl<G, S: PageRankScalar> PageRankWorkspace<G, S> {
1020 #[must_use]
1026 pub const fn new() -> Self {
1027 Self {
1028 teleport: Vec::new(),
1029 next: Vec::new(),
1030 visible_elements: Vec::new(),
1031 _graph: PhantomData,
1032 }
1033 }
1034
1035 #[must_use]
1041 pub fn for_graph(graph: &G) -> Self
1042 where
1043 G: DenseElementIndex,
1044 {
1045 Self::with_element_bound(graph.element_bound())
1046 }
1047
1048 #[must_use]
1054 pub fn with_element_bound(element_bound: usize) -> Self {
1055 Self {
1056 teleport: vec![S::ZERO; element_bound],
1057 next: vec![S::ZERO; element_bound],
1058 visible_elements: vec![0; element_bound],
1059 _graph: PhantomData,
1060 }
1061 }
1062
1063 #[must_use]
1069 pub const fn element_bound_capacity(&self) -> usize {
1070 self.teleport.len()
1071 }
1072
1073 fn ensure_element_bound(&mut self, element_bound: usize) {
1075 if self.teleport.len() < element_bound {
1076 self.teleport.resize(element_bound, S::ZERO);
1077 }
1078 if self.next.len() < element_bound {
1079 self.next.resize(element_bound, S::ZERO);
1080 }
1081 if self.visible_elements.len() < element_bound {
1082 self.visible_elements.resize(element_bound, 0);
1083 }
1084 }
1085
1086 fn as_scratch(&mut self) -> PageRankScratch<'_, S> {
1088 PageRankScratch::new(
1089 &mut self.teleport,
1090 &mut self.next,
1091 &mut self.visible_elements,
1092 )
1093 }
1094}
1095
1096#[cfg(feature = "alloc")]
1102#[derive(Clone, Debug)]
1103pub struct HypergraphPageRankWorkspace<H, S> {
1104 teleport: Vec<S>,
1106 next_elements: Vec<S>,
1108 next_relations: Vec<S>,
1110 visible_elements: Vec<u8>,
1112 visible_relations: Vec<u8>,
1114 _hypergraph: PhantomData<fn() -> H>,
1116}
1117
1118#[cfg(feature = "alloc")]
1119impl<H, S: PageRankScalar> Default for HypergraphPageRankWorkspace<H, S> {
1120 fn default() -> Self {
1121 Self::new()
1122 }
1123}
1124
1125#[cfg(feature = "alloc")]
1126impl<H, S: PageRankScalar> HypergraphPageRankWorkspace<H, S> {
1127 #[must_use]
1133 pub const fn new() -> Self {
1134 Self {
1135 teleport: Vec::new(),
1136 next_elements: Vec::new(),
1137 next_relations: Vec::new(),
1138 visible_elements: Vec::new(),
1139 visible_relations: Vec::new(),
1140 _hypergraph: PhantomData,
1141 }
1142 }
1143
1144 #[must_use]
1150 pub fn for_hypergraph(hypergraph: &H) -> Self
1151 where
1152 H: DenseElementIndex + DenseRelationIndex,
1153 {
1154 Self::with_bounds(hypergraph.element_bound(), hypergraph.relation_bound())
1155 }
1156
1157 #[must_use]
1163 pub fn with_bounds(element_bound: usize, relation_bound: usize) -> Self {
1164 let state_bound = element_bound.saturating_add(relation_bound);
1165 Self {
1166 teleport: vec![S::ZERO; state_bound],
1167 next_elements: vec![S::ZERO; element_bound],
1168 next_relations: vec![S::ZERO; relation_bound],
1169 visible_elements: vec![0; element_bound],
1170 visible_relations: vec![0; relation_bound],
1171 _hypergraph: PhantomData,
1172 }
1173 }
1174
1175 #[must_use]
1181 pub const fn element_bound_capacity(&self) -> usize {
1182 self.next_elements.len()
1183 }
1184
1185 #[must_use]
1191 pub const fn relation_bound_capacity(&self) -> usize {
1192 self.next_relations.len()
1193 }
1194
1195 fn ensure_bounds(&mut self, element_bound: usize, relation_bound: usize, state_bound: usize) {
1197 if self.teleport.len() < state_bound {
1198 self.teleport.resize(state_bound, S::ZERO);
1199 }
1200 if self.next_elements.len() < element_bound {
1201 self.next_elements.resize(element_bound, S::ZERO);
1202 }
1203 if self.next_relations.len() < relation_bound {
1204 self.next_relations.resize(relation_bound, S::ZERO);
1205 }
1206 if self.visible_elements.len() < element_bound {
1207 self.visible_elements.resize(element_bound, 0);
1208 }
1209 if self.visible_relations.len() < relation_bound {
1210 self.visible_relations.resize(relation_bound, 0);
1211 }
1212 }
1213
1214 fn as_scratch(&mut self) -> HypergraphPageRankScratch<'_, S> {
1216 HypergraphPageRankScratch::new(
1217 &mut self.teleport,
1218 &mut self.next_elements,
1219 &mut self.next_relations,
1220 &mut self.visible_elements,
1221 &mut self.visible_relations,
1222 )
1223 }
1224}
1225
1226#[cfg(feature = "alloc")]
1248#[expect(
1249 clippy::too_many_arguments,
1250 reason = "PageRank entry point keeps graph, distribution, elements, config, personalization, and output explicit"
1251)]
1252pub fn pagerank_graph<G, D, I, S>(
1253 graph: &G,
1254 distribution: &D,
1255 elements: I,
1256 config: PageRankConfig<S>,
1257 personalization: Option<&[S]>,
1258 ranks: &mut [S],
1259) -> Result<PageRankReport<S>, PageRankError<S>>
1260where
1261 G: ForwardGraph + DenseElementIndex,
1262 D: OutgoingDistribution<G, S>,
1263 I: Clone + IntoIterator<Item = ElementId<G>>,
1264 S: PageRankScalar,
1265{
1266 let bound = graph.element_bound();
1267 let mut teleport = vec![S::ZERO; bound];
1268 let mut next = vec![S::ZERO; bound];
1269 let mut visible_elements = vec![0; bound];
1270 pagerank_graph_with_scratch(
1271 graph,
1272 distribution,
1273 elements,
1274 config,
1275 personalization,
1276 ranks,
1277 PageRankScratch::new(&mut teleport, &mut next, &mut visible_elements),
1278 )
1279}
1280
1281#[expect(
1296 clippy::too_many_arguments,
1297 clippy::needless_pass_by_value,
1298 reason = "PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1299)]
1300pub fn pagerank_graph_with_scratch<G, D, I, S>(
1301 graph: &G,
1302 distribution: &D,
1303 elements: I,
1304 config: PageRankConfig<S>,
1305 personalization: Option<&[S]>,
1306 ranks: &mut [S],
1307 scratch: PageRankScratch<'_, S>,
1308) -> Result<PageRankReport<S>, PageRankError<S>>
1309where
1310 G: ForwardGraph + DenseElementIndex,
1311 D: OutgoingDistribution<G, S>,
1312 I: Clone + IntoIterator<Item = ElementId<G>>,
1313 S: PageRankScalar,
1314{
1315 validate_config(config)?;
1316 let bound = graph.element_bound();
1317 ensure_output_len(ranks.len(), bound)?;
1318 ensure_scratch_len("teleport", scratch.teleport.len(), bound)?;
1319 ensure_scratch_len("next", scratch.next.len(), bound)?;
1320 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), bound)?;
1321 build_personalization_into(
1322 elements.clone(),
1323 bound,
1324 personalization,
1325 |element| graph.element_index(element),
1326 scratch.teleport,
1327 scratch.visible_elements,
1328 )?;
1329 initialize_ranks(elements.clone(), graph, scratch.teleport, ranks)?;
1330 iterate_graph(
1331 graph,
1332 distribution,
1333 elements,
1334 config,
1335 scratch.teleport,
1336 scratch.visible_elements,
1337 ranks,
1338 scratch.next,
1339 )
1340}
1341
1342#[cfg(feature = "alloc")]
1357#[expect(
1358 clippy::too_many_arguments,
1359 reason = "PageRank workspace API keeps policy and reusable storage inputs explicit"
1360)]
1361pub fn pagerank_graph_with_workspace<G, D, I, S>(
1362 graph: &G,
1363 distribution: &D,
1364 elements: I,
1365 config: PageRankConfig<S>,
1366 personalization: Option<&[S]>,
1367 ranks: &mut [S],
1368 workspace: &mut PageRankWorkspace<G, S>,
1369) -> Result<PageRankReport<S>, PageRankError<S>>
1370where
1371 G: ForwardGraph + DenseElementIndex,
1372 D: OutgoingDistribution<G, S>,
1373 I: Clone + IntoIterator<Item = ElementId<G>>,
1374 S: PageRankScalar,
1375{
1376 workspace.ensure_element_bound(graph.element_bound());
1377 pagerank_graph_with_scratch(
1378 graph,
1379 distribution,
1380 elements,
1381 config,
1382 personalization,
1383 ranks,
1384 workspace.as_scratch(),
1385 )
1386}
1387
1388#[cfg(feature = "alloc")]
1415#[expect(
1416 clippy::too_many_arguments,
1417 reason = "hypergraph PageRank entry point keeps hypergraph, distribution, state families, and output explicit"
1418)]
1419pub fn pagerank_hypergraph<H, D, IE, IR, S>(
1420 hypergraph: &H,
1421 distribution: &D,
1422 elements: IE,
1423 relations: IR,
1424 config: PageRankConfig<S>,
1425 personalization: Option<&[S]>,
1426 element_ranks: &mut [S],
1427 relation_ranks: &mut [S],
1428) -> Result<PageRankReport<S>, PageRankError<S>>
1429where
1430 H: PageRankHypergraph,
1431 D: HypergraphOutgoingDistribution<H, S>,
1432 IE: Clone + IntoIterator<Item = ElementId<H>>,
1433 IR: Clone + IntoIterator<Item = RelationId<H>>,
1434 S: PageRankScalar,
1435{
1436 let e_bound = hypergraph.element_bound();
1437 let r_bound = hypergraph.relation_bound();
1438 let state_bound =
1439 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1440 let mut teleport = vec![S::ZERO; state_bound];
1441 let mut next_elements = vec![S::ZERO; e_bound];
1442 let mut next_relations = vec![S::ZERO; r_bound];
1443 let mut visible_elements = vec![0; e_bound];
1444 let mut visible_relations = vec![0; r_bound];
1445 pagerank_hypergraph_with_scratch(
1446 hypergraph,
1447 distribution,
1448 elements,
1449 relations,
1450 config,
1451 personalization,
1452 element_ranks,
1453 relation_ranks,
1454 HypergraphPageRankScratch::new(
1455 &mut teleport,
1456 &mut next_elements,
1457 &mut next_relations,
1458 &mut visible_elements,
1459 &mut visible_relations,
1460 ),
1461 )
1462}
1463
1464#[expect(
1480 clippy::too_many_arguments,
1481 reason = "hypergraph PageRank scratch entry point keeps policy and storage inputs explicit"
1482)]
1483#[expect(
1484 clippy::needless_pass_by_value,
1485 reason = "hypergraph PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1486)]
1487pub fn pagerank_hypergraph_with_scratch<H, D, IE, IR, S>(
1488 hypergraph: &H,
1489 distribution: &D,
1490 elements: IE,
1491 relations: IR,
1492 config: PageRankConfig<S>,
1493 personalization: Option<&[S]>,
1494 element_ranks: &mut [S],
1495 relation_ranks: &mut [S],
1496 scratch: HypergraphPageRankScratch<'_, S>,
1497) -> Result<PageRankReport<S>, PageRankError<S>>
1498where
1499 H: PageRankHypergraph,
1500 D: HypergraphOutgoingDistribution<H, S>,
1501 IE: Clone + IntoIterator<Item = ElementId<H>>,
1502 IR: Clone + IntoIterator<Item = RelationId<H>>,
1503 S: PageRankScalar,
1504{
1505 validate_config(config)?;
1506 let e_bound = hypergraph.element_bound();
1507 let r_bound = hypergraph.relation_bound();
1508 let state_bound =
1509 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1510 ensure_scratch_len("teleport", scratch.teleport.len(), state_bound)?;
1511 ensure_scratch_len("next_elements", scratch.next_elements.len(), e_bound)?;
1512 ensure_scratch_len("next_relations", scratch.next_relations.len(), r_bound)?;
1513 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), e_bound)?;
1514 ensure_scratch_len(
1515 "visible_relations",
1516 scratch.visible_relations.len(),
1517 r_bound,
1518 )?;
1519 build_hyper_personalization_into(
1520 hypergraph,
1521 elements.clone(),
1522 relations.clone(),
1523 state_bound,
1524 personalization,
1525 scratch.teleport,
1526 scratch.visible_elements,
1527 scratch.visible_relations,
1528 )?;
1529 initialize_hyper_ranks(
1530 hypergraph,
1531 elements.clone(),
1532 relations.clone(),
1533 scratch.teleport,
1534 element_ranks,
1535 relation_ranks,
1536 )?;
1537 iterate_hypergraph(
1538 hypergraph,
1539 distribution,
1540 elements,
1541 relations,
1542 config,
1543 scratch.teleport,
1544 scratch.visible_elements,
1545 scratch.visible_relations,
1546 element_ranks,
1547 relation_ranks,
1548 scratch.next_elements,
1549 scratch.next_relations,
1550 )
1551}
1552
1553#[cfg(feature = "alloc")]
1568#[expect(
1569 clippy::too_many_arguments,
1570 reason = "hypergraph PageRank workspace entry point keeps policy and storage inputs explicit"
1571)]
1572pub fn pagerank_hypergraph_with_workspace<H, D, IE, IR, S>(
1573 hypergraph: &H,
1574 distribution: &D,
1575 elements: IE,
1576 relations: IR,
1577 config: PageRankConfig<S>,
1578 personalization: Option<&[S]>,
1579 element_ranks: &mut [S],
1580 relation_ranks: &mut [S],
1581 workspace: &mut HypergraphPageRankWorkspace<H, S>,
1582) -> Result<PageRankReport<S>, PageRankError<S>>
1583where
1584 H: PageRankHypergraph,
1585 D: HypergraphOutgoingDistribution<H, S>,
1586 IE: Clone + IntoIterator<Item = ElementId<H>>,
1587 IR: Clone + IntoIterator<Item = RelationId<H>>,
1588 S: PageRankScalar,
1589{
1590 let e_bound = hypergraph.element_bound();
1591 let r_bound = hypergraph.relation_bound();
1592 let state_bound =
1593 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1594 workspace.ensure_bounds(e_bound, r_bound, state_bound);
1595 pagerank_hypergraph_with_scratch(
1596 hypergraph,
1597 distribution,
1598 elements,
1599 relations,
1600 config,
1601 personalization,
1602 element_ranks,
1603 relation_ranks,
1604 workspace.as_scratch(),
1605 )
1606}
1607
1608fn validate_config<S: PageRankScalar>(config: PageRankConfig<S>) -> Result<(), PageRankError<S>> {
1609 if !config.damping.is_finite() || config.damping < S::ZERO || config.damping > S::ONE {
1610 return Err(PageRankError::InvalidDamping {
1611 damping: config.damping,
1612 });
1613 }
1614 if !config.tolerance.is_finite() || config.tolerance < S::ZERO {
1615 return Err(PageRankError::InvalidTolerance {
1616 tolerance: config.tolerance,
1617 });
1618 }
1619 if config.max_iterations == 0 {
1620 return Err(PageRankError::InvalidMaxIterations);
1621 }
1622 Ok(())
1623}
1624
1625const fn ensure_output_len<S>(actual: usize, required: usize) -> Result<(), PageRankError<S>> {
1626 if actual < required {
1627 Err(PageRankError::OutputTooShort { required, actual })
1628 } else {
1629 Ok(())
1630 }
1631}
1632
1633const fn ensure_scratch_len<S>(
1634 name: &'static str,
1635 actual: usize,
1636 required: usize,
1637) -> Result<(), PageRankError<S>> {
1638 if actual < required {
1639 Err(PageRankError::ScratchTooShort {
1640 name,
1641 required,
1642 actual,
1643 })
1644 } else {
1645 Ok(())
1646 }
1647}
1648
1649fn checked_state_bound<S>(
1650 e_bound: usize,
1651 r_bound: usize,
1652 element_output_len: usize,
1653 relation_output_len: usize,
1654) -> Result<usize, PageRankError<S>> {
1655 ensure_output_len(element_output_len, e_bound)?;
1656 ensure_output_len(relation_output_len, r_bound)?;
1657 e_bound
1658 .checked_add(r_bound)
1659 .ok_or_else(|| PageRankError::OutputTooShort {
1660 required: usize::MAX,
1661 actual: element_output_len.saturating_add(relation_output_len),
1662 })
1663}
1664
1665fn clear<S: PageRankScalar>(values: &mut [S], len: usize) {
1666 for value in &mut values[..len] {
1667 *value = S::ZERO;
1668 }
1669}
1670
1671fn clear_u8(values: &mut [u8], len: usize) {
1672 for value in &mut values[..len] {
1673 *value = 0;
1674 }
1675}
1676
1677fn mark_visible_element<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1678 if visible[index] != 0 {
1679 return Err(PageRankError::DuplicateElement { index });
1680 }
1681 visible[index] = 1;
1682 Ok(())
1683}
1684
1685fn mark_visible_relation<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1686 if visible[index] != 0 {
1687 return Err(PageRankError::DuplicateRelation { index });
1688 }
1689 visible[index] = 1;
1690 Ok(())
1691}
1692
1693fn is_visible(visible: &[u8], index: usize) -> bool {
1698 visible[index] != 0
1699}
1700
1701#[expect(
1702 clippy::too_many_arguments,
1703 reason = "personalization normalization keeps topology family bounds and caller buffers explicit"
1704)]
1705fn build_personalization_into<I, F, S>(
1706 elements: I,
1707 bound: usize,
1708 personalization: Option<&[S]>,
1709 index_of: F,
1710 out: &mut [S],
1711 visible: &mut [u8],
1712) -> Result<(), PageRankError<S>>
1713where
1714 I: IntoIterator,
1715 F: Fn(I::Item) -> usize,
1716 S: PageRankScalar,
1717{
1718 clear(out, bound);
1719 clear_u8(visible, bound);
1720 let mut count = 0_usize;
1721 let mut sum = S::ZERO;
1722 if let Some(input) = personalization {
1723 if input.len() < bound {
1724 return Err(PageRankError::PersonalizationTooShort {
1725 required: bound,
1726 actual: input.len(),
1727 });
1728 }
1729 for element in elements {
1730 let index = index_of(element);
1731 check_index(index, bound)?;
1732 mark_visible_element(visible, index)?;
1733 let value = input[index];
1734 check_personalization_value(index, value)?;
1735 out[index] = value;
1736 sum += value;
1737 count += 1;
1738 }
1739 } else {
1740 for element in elements {
1741 let index = index_of(element);
1742 check_index(index, bound)?;
1743 mark_visible_element(visible, index)?;
1744 out[index] = S::ONE;
1745 sum += S::ONE;
1746 count += 1;
1747 }
1748 }
1749 normalize_personalization(out, count, sum)
1750}
1751
1752enum PersonalizationSource<'a, S> {
1763 Uniform,
1765 FromInput(&'a [S]),
1769}
1770
1771impl<S> PersonalizationSource<'_, S>
1772where
1773 S: PageRankScalar,
1774{
1775 fn value_at(&self, state_index: usize) -> Result<S, PageRankError<S>> {
1785 match *self {
1786 Self::Uniform => Ok(S::ONE),
1787 Self::FromInput(input) => {
1788 let value = input[state_index];
1789 check_personalization_value(state_index, value)?;
1790 Ok(value)
1791 }
1792 }
1793 }
1794
1795 #[expect(
1818 clippy::too_many_arguments,
1819 reason = "method threads separate element/relation iterables, scratch slices, and the state bound; bundling them obscures the borrow shape that callers explicitly construct"
1820 )]
1821 fn fill_hypergraph<H, IE, IR>(
1822 &self,
1823 hypergraph: &H,
1824 elements: IE,
1825 relations: IR,
1826 state_bound: usize,
1827 out: &mut [S],
1828 visible_elements: &mut [u8],
1829 visible_relations: &mut [u8],
1830 ) -> Result<(usize, S), PageRankError<S>>
1831 where
1832 H: DenseElementIndex + DenseRelationIndex,
1833 IE: IntoIterator<Item = ElementId<H>>,
1834 IR: IntoIterator<Item = RelationId<H>>,
1835 {
1836 if let Self::FromInput(input) = *self
1837 && input.len() < state_bound
1838 {
1839 return Err(PageRankError::PersonalizationTooShort {
1840 required: state_bound,
1841 actual: input.len(),
1842 });
1843 }
1844 let e_bound = hypergraph.element_bound();
1845 let r_bound = hypergraph.relation_bound();
1846 let mut count = 0_usize;
1847 let mut sum = S::ZERO;
1848 for element in elements {
1849 let index = hypergraph.element_index(element);
1850 check_index(index, e_bound)?;
1851 mark_visible_element(visible_elements, index)?;
1852 let value = self.value_at(index)?;
1853 out[index] = value;
1854 sum += value;
1855 count += 1;
1856 }
1857 for relation in relations {
1858 let index = hypergraph.relation_index(relation);
1859 check_relation_index(index, r_bound)?;
1860 mark_visible_relation(visible_relations, index)?;
1861 let state = e_bound + index;
1862 let value = self.value_at(state)?;
1863 out[state] = value;
1864 sum += value;
1865 count += 1;
1866 }
1867 Ok((count, sum))
1868 }
1869}
1870
1871#[expect(
1872 clippy::too_many_arguments,
1873 reason = "helper threads separate element/relation state and caller scratch explicitly"
1874)]
1875fn build_hyper_personalization_into<H, IE, IR, S>(
1876 hypergraph: &H,
1877 elements: IE,
1878 relations: IR,
1879 state_bound: usize,
1880 personalization: Option<&[S]>,
1881 out: &mut [S],
1882 visible_elements: &mut [u8],
1883 visible_relations: &mut [u8],
1884) -> Result<(), PageRankError<S>>
1885where
1886 H: DenseElementIndex + DenseRelationIndex,
1887 IE: IntoIterator<Item = ElementId<H>>,
1888 IR: IntoIterator<Item = RelationId<H>>,
1889 S: PageRankScalar,
1890{
1891 clear(out, state_bound);
1892 clear_u8(visible_elements, hypergraph.element_bound());
1893 clear_u8(visible_relations, hypergraph.relation_bound());
1894 let source = personalization.map_or(
1895 PersonalizationSource::Uniform,
1896 PersonalizationSource::FromInput,
1897 );
1898 let (count, sum) = source.fill_hypergraph(
1899 hypergraph,
1900 elements,
1901 relations,
1902 state_bound,
1903 out,
1904 visible_elements,
1905 visible_relations,
1906 )?;
1907 normalize_personalization(out, count, sum)
1908}
1909
1910fn normalize_personalization<S: PageRankScalar>(
1911 out: &mut [S],
1912 count: usize,
1913 sum: S,
1914) -> Result<(), PageRankError<S>> {
1915 if count == 0 {
1916 return Err(PageRankError::EmptyState);
1917 }
1918 if sum <= S::ZERO {
1919 return Err(PageRankError::ZeroPersonalization);
1920 }
1921 for value in out {
1922 *value = *value / sum;
1923 }
1924 Ok(())
1925}
1926
1927fn check_personalization_value<S: PageRankScalar>(
1928 index: usize,
1929 value: S,
1930) -> Result<(), PageRankError<S>> {
1931 if !value.is_finite() || value < S::ZERO {
1932 Err(PageRankError::InvalidPersonalization { index, value })
1933 } else {
1934 Ok(())
1935 }
1936}
1937
1938fn initialize_ranks<G, I, S>(
1939 elements: I,
1940 graph: &G,
1941 teleport: &[S],
1942 ranks: &mut [S],
1943) -> Result<(), PageRankError<S>>
1944where
1945 G: DenseElementIndex,
1946 I: IntoIterator<Item = ElementId<G>>,
1947 S: PageRankScalar,
1948{
1949 clear(ranks, graph.element_bound());
1950 for element in elements {
1951 let index = graph.element_index(element);
1952 check_index(index, graph.element_bound())?;
1953 ranks[index] = teleport[index];
1954 }
1955 Ok(())
1956}
1957
1958#[expect(
1959 clippy::too_many_arguments,
1960 reason = "initialization writes separate element and relation rank slices"
1961)]
1962fn initialize_hyper_ranks<H, IE, IR, S>(
1963 hypergraph: &H,
1964 elements: IE,
1965 relations: IR,
1966 teleport: &[S],
1967 element_ranks: &mut [S],
1968 relation_ranks: &mut [S],
1969) -> Result<(), PageRankError<S>>
1970where
1971 H: DenseElementIndex + DenseRelationIndex,
1972 IE: IntoIterator<Item = ElementId<H>>,
1973 IR: IntoIterator<Item = RelationId<H>>,
1974 S: PageRankScalar,
1975{
1976 clear(element_ranks, hypergraph.element_bound());
1977 clear(relation_ranks, hypergraph.relation_bound());
1978 for element in elements {
1979 let index = hypergraph.element_index(element);
1980 check_index(index, hypergraph.element_bound())?;
1981 element_ranks[index] = teleport[index];
1982 }
1983 for relation in relations {
1984 let index = hypergraph.relation_index(relation);
1985 check_relation_index(index, hypergraph.relation_bound())?;
1986 relation_ranks[index] = teleport[hypergraph.element_bound() + index];
1987 }
1988 Ok(())
1989}
1990
1991#[expect(
1992 clippy::too_many_arguments,
1993 reason = "graph iteration helper keeps distribution, scratch, and policy inputs explicit"
1994)]
1995#[expect(
1996 clippy::needless_pass_by_value,
1997 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
1998)]
1999fn iterate_graph<G, D, I, S>(
2000 graph: &G,
2001 distribution: &D,
2002 elements: I,
2003 config: PageRankConfig<S>,
2004 teleport: &[S],
2005 visible: &[u8],
2006 ranks: &mut [S],
2007 next: &mut [S],
2008) -> Result<PageRankReport<S>, PageRankError<S>>
2009where
2010 G: ForwardGraph + DenseElementIndex,
2011 D: OutgoingDistribution<G, S>,
2012 I: Clone + IntoIterator<Item = ElementId<G>>,
2013 S: PageRankScalar,
2014{
2015 let mut last_delta = S::INFINITY;
2016 for iteration in 1..=config.max_iterations {
2017 clear(next, graph.element_bound());
2018 let mut dangling = S::ZERO;
2019 for element in elements.clone() {
2020 let index = checked_element_index(graph, element)?;
2021 let rank = ranks[index];
2022 dangling += distribution.distribute_outgoing(graph, element, rank, next, visible)?;
2023 }
2024 let delta = apply_graph_teleport(
2025 graph,
2026 elements.clone(),
2027 config,
2028 teleport,
2029 dangling,
2030 ranks,
2031 next,
2032 )?;
2033 last_delta = delta;
2034 if delta <= config.tolerance {
2035 return Ok(PageRankReport {
2036 iterations: iteration,
2037 delta,
2038 });
2039 }
2040 }
2041 Err(PageRankError::NonConverged {
2042 iterations: config.max_iterations,
2043 delta: last_delta,
2044 })
2045}
2046
2047#[expect(
2048 clippy::too_many_arguments,
2049 reason = "hypergraph iteration helper keeps distribution, state families, scratch, and policy inputs explicit"
2050)]
2051#[expect(
2052 clippy::needless_pass_by_value,
2053 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
2054)]
2055fn iterate_hypergraph<H, D, IE, IR, S>(
2056 hypergraph: &H,
2057 distribution: &D,
2058 elements: IE,
2059 relations: IR,
2060 config: PageRankConfig<S>,
2061 teleport: &[S],
2062 visible_elements: &[u8],
2063 visible_relations: &[u8],
2064 element_ranks: &mut [S],
2065 relation_ranks: &mut [S],
2066 next_elements: &mut [S],
2067 next_relations: &mut [S],
2068) -> Result<PageRankReport<S>, PageRankError<S>>
2069where
2070 H: PageRankHypergraph,
2071 D: HypergraphOutgoingDistribution<H, S>,
2072 IE: Clone + IntoIterator<Item = ElementId<H>>,
2073 IR: Clone + IntoIterator<Item = RelationId<H>>,
2074 S: PageRankScalar,
2075{
2076 let mut last_delta = S::INFINITY;
2077 for iteration in 1..=config.max_iterations {
2078 clear(next_elements, hypergraph.element_bound());
2079 clear(next_relations, hypergraph.relation_bound());
2080 let mut dangling = S::ZERO;
2081 for element in elements.clone() {
2082 let index = checked_element_index(hypergraph, element)?;
2083 let rank = element_ranks[index];
2084 dangling += distribution.distribute_from_element(
2085 hypergraph,
2086 element,
2087 rank,
2088 next_relations,
2089 visible_relations,
2090 )?;
2091 }
2092 for relation in relations.clone() {
2093 let index = checked_relation_index_for(hypergraph, relation)?;
2094 let rank = relation_ranks[index];
2095 dangling += distribution.distribute_from_relation(
2096 hypergraph,
2097 relation,
2098 rank,
2099 next_elements,
2100 visible_elements,
2101 )?;
2102 }
2103 let delta = apply_hyper_teleport(
2104 hypergraph,
2105 elements.clone(),
2106 relations.clone(),
2107 config,
2108 teleport,
2109 dangling,
2110 element_ranks,
2111 relation_ranks,
2112 next_elements,
2113 next_relations,
2114 )?;
2115 last_delta = delta;
2116 if delta <= config.tolerance {
2117 return Ok(PageRankReport {
2118 iterations: iteration,
2119 delta,
2120 });
2121 }
2122 }
2123 Err(PageRankError::NonConverged {
2124 iterations: config.max_iterations,
2125 delta: last_delta,
2126 })
2127}
2128
2129fn checked_element_index<G: DenseElementIndex, S>(
2130 graph: &G,
2131 element: ElementId<G>,
2132) -> Result<usize, PageRankError<S>> {
2133 let index = graph.element_index(element);
2134 check_index(index, graph.element_bound())?;
2135 Ok(index)
2136}
2137
2138const fn check_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2139 if index < bound {
2140 Ok(())
2141 } else {
2142 Err(PageRankError::ElementIndexOutOfBounds { index, bound })
2143 }
2144}
2145
2146const fn check_relation_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2147 if index < bound {
2148 Ok(())
2149 } else {
2150 Err(PageRankError::RelationIndexOutOfBounds { index, bound })
2151 }
2152}
2153
2154fn checked_relation_weight<G, W, S>(
2155 graph: &G,
2156 weights: &W,
2157 relation: RelationId<G>,
2158) -> Result<S, PageRankError<S>>
2159where
2160 G: DenseRelationIndex,
2161 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2162 W::Weight: IntoPageRankScalar<S>,
2163 S: PageRankScalar,
2164{
2165 let index = graph.relation_index(relation);
2166 check_relation_index(index, graph.relation_bound())?;
2167 let value = weights.relation_weight(relation).into_pagerank_scalar();
2168 if !value.is_finite() || value < S::ZERO {
2169 Err(PageRankError::InvalidRelationWeight { index, value })
2170 } else {
2171 Ok(value)
2172 }
2173}
2174
2175fn checked_incidence_weight<H, W, S>(
2176 hypergraph: &H,
2177 weights: &W,
2178 incidence: H::IncidenceId,
2179) -> Result<S, PageRankError<S>>
2180where
2181 H: DenseIncidenceIndex,
2182 W: IncidenceWeight<
2183 ElementId = H::ElementId,
2184 RelationId = H::RelationId,
2185 IncidenceId = H::IncidenceId,
2186 >,
2187 W::Weight: IntoPageRankScalar<S>,
2188 S: PageRankScalar,
2189{
2190 let index = hypergraph.incidence_index(incidence);
2191 let bound = hypergraph.incidence_bound();
2192 if index >= bound {
2193 return Err(PageRankError::IncidenceIndexOutOfBounds { index, bound });
2194 }
2195 let value = weights.incidence_weight(incidence).into_pagerank_scalar();
2196 if !value.is_finite() || value < S::ZERO {
2197 Err(PageRankError::InvalidIncidenceWeight { index, value })
2198 } else {
2199 Ok(value)
2200 }
2201}
2202
2203fn outgoing_weight_total<G, W, S>(
2204 graph: &G,
2205 weights: &W,
2206 element: ElementId<G>,
2207 visible: &[u8],
2208) -> Result<S, PageRankError<S>>
2209where
2210 G: ForwardGraph + DenseElementIndex + DenseRelationIndex,
2211 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2212 W::Weight: IntoPageRankScalar<S>,
2213 S: PageRankScalar,
2214{
2215 let mut total = S::ZERO;
2216 for edge in graph.outgoing_edges(element) {
2217 let target = graph.target(edge);
2218 let target_index = checked_element_index(graph, target)?;
2219 if is_visible(visible, target_index) {
2220 total += checked_relation_weight(graph, weights, edge)?;
2221 }
2222 }
2223 Ok(total)
2224}
2225
2226fn teleport_update<S: PageRankScalar>(
2238 damping: S,
2239 teleport_scale: S,
2240 accumulated: S,
2241 teleport: S,
2242 previous: S,
2243) -> (S, S) {
2244 let value = (damping * accumulated) + (teleport_scale * teleport);
2245 let delta = (value - previous).abs();
2246 (value, delta)
2247}
2248
2249#[expect(
2250 clippy::too_many_arguments,
2251 reason = "teleport helper updates caller-provided rank and scratch slices"
2252)]
2253fn apply_graph_teleport<G, I, S>(
2254 graph: &G,
2255 elements: I,
2256 config: PageRankConfig<S>,
2257 teleport: &[S],
2258 dangling: S,
2259 ranks: &mut [S],
2260 next: &[S],
2261) -> Result<S, PageRankError<S>>
2262where
2263 G: DenseElementIndex,
2264 I: IntoIterator<Item = ElementId<G>>,
2265 S: PageRankScalar,
2266{
2267 let mut delta = S::ZERO;
2268 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2269 for element in elements {
2270 let index = checked_element_index(graph, element)?;
2271 let (value, state_delta) = teleport_update(
2272 config.damping,
2273 teleport_scale,
2274 next[index],
2275 teleport[index],
2276 ranks[index],
2277 );
2278 delta += state_delta;
2279 ranks[index] = value;
2280 }
2281 Ok(delta)
2282}
2283
2284fn checked_relation_index_for<H: DenseRelationIndex, S>(
2285 hypergraph: &H,
2286 relation: RelationId<H>,
2287) -> Result<usize, PageRankError<S>> {
2288 let index = hypergraph.relation_index(relation);
2289 check_relation_index(index, hypergraph.relation_bound())?;
2290 Ok(index)
2291}
2292
2293fn hyper_outgoing_relation_weight<H, W, S>(
2294 hypergraph: &H,
2295 weights: &W,
2296 element: ElementId<H>,
2297 visible_relations: &[u8],
2298) -> Result<S, PageRankError<S>>
2299where
2300 H: DirectedVertexHyperedges + DenseRelationIndex,
2301 W: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
2302 W::Weight: IntoPageRankScalar<S>,
2303 S: PageRankScalar,
2304{
2305 let mut total = S::ZERO;
2306 for relation in hypergraph.outgoing_hyperedges(element) {
2307 let relation_index = checked_relation_index_for(hypergraph, relation)?;
2308 if is_visible(visible_relations, relation_index) {
2309 total += checked_relation_weight(hypergraph, weights, relation)?;
2310 }
2311 }
2312 Ok(total)
2313}
2314
2315fn hyper_target_incidence_weight<H, W, S>(
2316 hypergraph: &H,
2317 weights: &W,
2318 relation: RelationId<H>,
2319 visible_elements: &[u8],
2320) -> Result<S, PageRankError<S>>
2321where
2322 H: DirectedHyperedgeIncidences + IncidenceElement + DenseElementIndex + DenseIncidenceIndex,
2323 W: IncidenceWeight<
2324 ElementId = H::ElementId,
2325 RelationId = H::RelationId,
2326 IncidenceId = H::IncidenceId,
2327 >,
2328 W::Weight: IntoPageRankScalar<S>,
2329 S: PageRankScalar,
2330{
2331 let mut total = S::ZERO;
2332 for incidence in hypergraph.target_incidences(relation) {
2333 let target = hypergraph.incidence_element(incidence);
2334 let target_index = checked_element_index(hypergraph, target)?;
2335 if is_visible(visible_elements, target_index) {
2336 total += checked_incidence_weight(hypergraph, weights, incidence)?;
2337 }
2338 }
2339 Ok(total)
2340}
2341
2342#[expect(
2343 clippy::too_many_arguments,
2344 reason = "hypergraph teleport updates separate element and relation slices"
2345)]
2346fn apply_hyper_teleport<H, IE, IR, S>(
2347 hypergraph: &H,
2348 elements: IE,
2349 relations: IR,
2350 config: PageRankConfig<S>,
2351 teleport: &[S],
2352 dangling: S,
2353 element_ranks: &mut [S],
2354 relation_ranks: &mut [S],
2355 next_elements: &[S],
2356 next_relations: &[S],
2357) -> Result<S, PageRankError<S>>
2358where
2359 H: DenseElementIndex + DenseRelationIndex,
2360 IE: IntoIterator<Item = ElementId<H>>,
2361 IR: IntoIterator<Item = RelationId<H>>,
2362 S: PageRankScalar,
2363{
2364 let mut delta = S::ZERO;
2365 let e_bound = hypergraph.element_bound();
2366 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2367 for element in elements {
2368 let index = checked_element_index(hypergraph, element)?;
2369 let (value, state_delta) = teleport_update(
2370 config.damping,
2371 teleport_scale,
2372 next_elements[index],
2373 teleport[index],
2374 element_ranks[index],
2375 );
2376 delta += state_delta;
2377 element_ranks[index] = value;
2378 }
2379 for relation in relations {
2380 let index = checked_relation_index_for(hypergraph, relation)?;
2381 let state = e_bound + index;
2382 let (value, state_delta) = teleport_update(
2383 config.damping,
2384 teleport_scale,
2385 next_relations[index],
2386 teleport[state],
2387 relation_ranks[index],
2388 );
2389 delta += state_delta;
2390 relation_ranks[index] = value;
2391 }
2392 Ok(delta)
2393}