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};
29use oxgraph_topology::{
30 ElementId, ElementIndex, IncidenceElement, IncidenceIndex, IncidenceWeight, RelationId,
31 RelationIndex, RelationWeight,
32};
33
34pub trait PageRankScalar:
48 Copy
49 + fmt::Debug
50 + PartialOrd
51 + Add<Output = Self>
52 + Sub<Output = Self>
53 + Mul<Output = Self>
54 + Div<Output = Self>
55 + AddAssign
56 + 'static
57{
58 const ZERO: Self;
60 const ONE: Self;
62 const INFINITY: Self;
64
65 fn from_usize(value: usize) -> Self;
67
68 fn from_f64(value: f64) -> Self;
70
71 #[must_use]
73 fn abs(self) -> Self;
74
75 fn is_finite(self) -> bool;
77}
78
79impl PageRankScalar for f64 {
80 const ZERO: Self = 0.0;
81 const ONE: Self = 1.0;
82 #[expect(
83 clippy::use_self,
84 reason = "primitive inherent infinity constant is clearer here"
85 )]
86 const INFINITY: Self = f64::INFINITY;
87
88 #[expect(
89 clippy::cast_precision_loss,
90 reason = "PageRank degree conversion is a documented scalar-boundary conversion"
91 )]
92 fn from_usize(value: usize) -> Self {
93 value as Self
94 }
95
96 fn from_f64(value: f64) -> Self {
97 value
98 }
99
100 fn abs(self) -> Self {
101 self.abs()
102 }
103
104 fn is_finite(self) -> bool {
105 self.is_finite()
106 }
107}
108
109impl PageRankScalar for f32 {
110 const ZERO: Self = 0.0;
111 const ONE: Self = 1.0;
112 #[expect(
113 clippy::use_self,
114 reason = "primitive inherent infinity constant is clearer here"
115 )]
116 const INFINITY: Self = f32::INFINITY;
117
118 #[expect(
119 clippy::cast_precision_loss,
120 reason = "PageRank degree conversion is a documented scalar-boundary conversion"
121 )]
122 fn from_usize(value: usize) -> Self {
123 value as Self
124 }
125
126 #[expect(
127 clippy::cast_possible_truncation,
128 reason = "f32 PageRank callers explicitly select f32 rank/config output"
129 )]
130 fn from_f64(value: f64) -> Self {
131 value as Self
132 }
133
134 fn abs(self) -> Self {
135 self.abs()
136 }
137
138 fn is_finite(self) -> bool {
139 self.is_finite()
140 }
141}
142
143pub trait IntoPageRankScalar<S: PageRankScalar> {
149 fn into_pagerank_scalar(self) -> S;
151}
152
153impl<S: PageRankScalar> IntoPageRankScalar<S> for S {
154 fn into_pagerank_scalar(self) -> S {
155 self
156 }
157}
158
159macro_rules! impl_weight_into_pagerank_scalar_from {
161 ($target:ty; $($type:ty),* $(,)?) => {
162 $(
163 impl IntoPageRankScalar<$target> for $type {
164 fn into_pagerank_scalar(self) -> $target { <$target>::from(self) }
165 }
166 )*
167 };
168}
169
170macro_rules! impl_weight_into_pagerank_scalar_cast {
172 ($target:ty; $($type:ty),* $(,)?) => {
173 $(
174 impl IntoPageRankScalar<$target> for $type {
175 #[expect(
176 clippy::cast_precision_loss,
177 reason = "PageRank primitive weight conversions are explicit algorithm-boundary numeric interpretations"
178 )]
179 fn into_pagerank_scalar(self) -> $target { self as $target }
180 }
181 )*
182 };
183}
184
185impl_weight_into_pagerank_scalar_from!(f64; u8, u16, u32, i8, i16, i32, f32);
186impl_weight_into_pagerank_scalar_cast!(f64; u64, usize, i64, isize);
187impl_weight_into_pagerank_scalar_from!(f32; u8, u16, i8, i16);
188impl_weight_into_pagerank_scalar_cast!(f32; u32, u64, usize, i32, i64, isize);
189
190impl IntoPageRankScalar<f32> for f64 {
191 #[expect(
192 clippy::cast_possible_truncation,
193 reason = "f32 PageRank callers explicitly select f32 rank and configuration output"
194 )]
195 fn into_pagerank_scalar(self) -> f32 {
196 self as f32
197 }
198}
199
200pub trait OutgoingDistribution<G, S>
225where
226 G: ForwardGraph + ElementIndex,
227 S: PageRankScalar,
228{
229 #[expect(
241 clippy::too_many_arguments,
242 reason = "distribution kernel needs graph, element, rank, output buffer, and visibility mask explicitly"
243 )]
244 fn distribute_outgoing(
245 &self,
246 graph: &G,
247 element: G::ElementId,
248 rank: S,
249 next: &mut [S],
250 visible: &[u8],
251 ) -> Result<S, PageRankError<S>>;
252}
253
254#[derive(Clone, Copy, Debug, Default)]
264pub struct Uniform;
265
266impl<G, S> OutgoingDistribution<G, S> for Uniform
267where
268 G: ForwardGraph + ElementIndex,
269 S: PageRankScalar,
270{
271 fn distribute_outgoing(
272 &self,
273 graph: &G,
274 element: G::ElementId,
275 rank: S,
276 next: &mut [S],
277 visible: &[u8],
278 ) -> Result<S, PageRankError<S>> {
279 let mut degree = 0_usize;
280 for edge in graph.outgoing_edges(element) {
281 let target = graph.target(edge);
282 let target_index = checked_element_index(graph, target)?;
283 if is_visible(visible, target_index) {
284 degree += 1;
285 }
286 }
287 if degree == 0 {
288 return Ok(rank);
289 }
290 let share = rank / S::from_usize(degree);
291 for edge in graph.outgoing_edges(element) {
292 let target = graph.target(edge);
293 let target_index = checked_element_index(graph, target)?;
294 if is_visible(visible, target_index) {
295 next[target_index] += share;
296 }
297 }
298 Ok(S::ZERO)
299 }
300}
301
302#[derive(Clone, Copy, Debug)]
315pub struct Weighted<'w, W> {
316 weights: &'w W,
318}
319
320impl<'w, W> Weighted<'w, W> {
321 #[must_use]
327 pub const fn new(weights: &'w W) -> Self {
328 Self { weights }
329 }
330}
331
332impl<G, W, S> OutgoingDistribution<G, S> for Weighted<'_, W>
333where
334 G: ForwardGraph + ElementIndex + RelationIndex,
335 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
336 W::Weight: IntoPageRankScalar<S>,
337 S: PageRankScalar,
338{
339 fn distribute_outgoing(
340 &self,
341 graph: &G,
342 element: G::ElementId,
343 rank: S,
344 next: &mut [S],
345 visible: &[u8],
346 ) -> Result<S, PageRankError<S>> {
347 let total = outgoing_weight_total(graph, self.weights, element, visible)?;
348 if total <= S::ZERO {
349 return Ok(rank);
350 }
351 for edge in graph.outgoing_edges(element) {
352 let target = graph.target(edge);
353 let target_index = checked_element_index(graph, target)?;
354 if is_visible(visible, target_index) {
355 let weight = checked_relation_weight(graph, self.weights, edge)?;
356 next[target_index] += rank * (weight / total);
357 }
358 }
359 Ok(S::ZERO)
360 }
361}
362
363pub trait HypergraphOutgoingDistribution<H, S>
389where
390 H: DirectedVertexHyperedges
391 + DirectedHyperedgeIncidences
392 + IncidenceElement
393 + ElementIndex
394 + RelationIndex,
395 S: PageRankScalar,
396{
397 #[expect(
409 clippy::too_many_arguments,
410 reason = "distribution kernel needs hypergraph, element, rank, output buffer, and visibility mask explicitly"
411 )]
412 fn distribute_from_element(
413 &self,
414 hypergraph: &H,
415 element: H::ElementId,
416 rank: S,
417 next_relations: &mut [S],
418 visible_relations: &[u8],
419 ) -> Result<S, PageRankError<S>>;
420
421 #[expect(
433 clippy::too_many_arguments,
434 reason = "distribution kernel needs hypergraph, relation, rank, output buffer, and visibility mask explicitly"
435 )]
436 fn distribute_from_relation(
437 &self,
438 hypergraph: &H,
439 relation: H::RelationId,
440 rank: S,
441 next_elements: &mut [S],
442 visible_elements: &[u8],
443 ) -> Result<S, PageRankError<S>>;
444}
445
446impl<H, S> HypergraphOutgoingDistribution<H, S> for Uniform
447where
448 H: DirectedVertexHyperedges
449 + DirectedHyperedgeIncidences
450 + IncidenceElement
451 + ElementIndex
452 + RelationIndex,
453 S: PageRankScalar,
454{
455 fn distribute_from_element(
456 &self,
457 hypergraph: &H,
458 element: H::ElementId,
459 rank: S,
460 next_relations: &mut [S],
461 visible_relations: &[u8],
462 ) -> Result<S, PageRankError<S>> {
463 let mut degree = 0_usize;
464 for relation in hypergraph.outgoing_hyperedges(element) {
465 let relation_index = checked_relation_index_for(hypergraph, relation)?;
466 if is_visible(visible_relations, relation_index) {
467 degree += 1;
468 }
469 }
470 if degree == 0 {
471 return Ok(rank);
472 }
473 let share = rank / S::from_usize(degree);
474 for relation in hypergraph.outgoing_hyperedges(element) {
475 let relation_index = checked_relation_index_for(hypergraph, relation)?;
476 if is_visible(visible_relations, relation_index) {
477 next_relations[relation_index] += share;
478 }
479 }
480 Ok(S::ZERO)
481 }
482
483 fn distribute_from_relation(
484 &self,
485 hypergraph: &H,
486 relation: H::RelationId,
487 rank: S,
488 next_elements: &mut [S],
489 visible_elements: &[u8],
490 ) -> Result<S, PageRankError<S>> {
491 let mut degree = 0_usize;
492 for incidence in hypergraph.target_incidences(relation) {
493 let target = hypergraph.incidence_element(incidence);
494 let target_index = checked_element_index(hypergraph, target)?;
495 if is_visible(visible_elements, target_index) {
496 degree += 1;
497 }
498 }
499 if degree == 0 {
500 return Ok(rank);
501 }
502 let share = rank / S::from_usize(degree);
503 for incidence in hypergraph.target_incidences(relation) {
504 let target = hypergraph.incidence_element(incidence);
505 let target_index = checked_element_index(hypergraph, target)?;
506 if is_visible(visible_elements, target_index) {
507 next_elements[target_index] += share;
508 }
509 }
510 Ok(S::ZERO)
511 }
512}
513
514#[derive(Clone, Copy, Debug)]
529pub struct HyperWeighted<'rw, 'iw, RW, IW> {
530 relation_weights: &'rw RW,
532 incidence_weights: &'iw IW,
534}
535
536impl<'rw, 'iw, RW, IW> HyperWeighted<'rw, 'iw, RW, IW> {
537 #[must_use]
544 pub const fn new(relation_weights: &'rw RW, incidence_weights: &'iw IW) -> Self {
545 Self {
546 relation_weights,
547 incidence_weights,
548 }
549 }
550}
551
552impl<H, RW, IW, S> HypergraphOutgoingDistribution<H, S> for HyperWeighted<'_, '_, RW, IW>
553where
554 H: DirectedVertexHyperedges
555 + DirectedHyperedgeIncidences
556 + IncidenceElement
557 + ElementIndex
558 + RelationIndex
559 + IncidenceIndex,
560 RW: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
561 RW::Weight: IntoPageRankScalar<S>,
562 IW: IncidenceWeight<
563 ElementId = H::ElementId,
564 RelationId = H::RelationId,
565 IncidenceId = H::IncidenceId,
566 >,
567 IW::Weight: IntoPageRankScalar<S>,
568 S: PageRankScalar,
569{
570 fn distribute_from_element(
571 &self,
572 hypergraph: &H,
573 element: H::ElementId,
574 rank: S,
575 next_relations: &mut [S],
576 visible_relations: &[u8],
577 ) -> Result<S, PageRankError<S>> {
578 let total = hyper_outgoing_relation_weight(
579 hypergraph,
580 self.relation_weights,
581 element,
582 visible_relations,
583 )?;
584 if total <= S::ZERO {
585 return Ok(rank);
586 }
587 for relation in hypergraph.outgoing_hyperedges(element) {
588 let relation_index = checked_relation_index_for(hypergraph, relation)?;
589 if !is_visible(visible_relations, relation_index) {
590 continue;
591 }
592 let weight = checked_relation_weight(hypergraph, self.relation_weights, relation)?;
593 next_relations[relation_index] += rank * (weight / total);
594 }
595 Ok(S::ZERO)
596 }
597
598 fn distribute_from_relation(
599 &self,
600 hypergraph: &H,
601 relation: H::RelationId,
602 rank: S,
603 next_elements: &mut [S],
604 visible_elements: &[u8],
605 ) -> Result<S, PageRankError<S>> {
606 let total = hyper_target_incidence_weight(
607 hypergraph,
608 self.incidence_weights,
609 relation,
610 visible_elements,
611 )?;
612 if total <= S::ZERO {
613 return Ok(rank);
614 }
615 for incidence in hypergraph.target_incidences(relation) {
616 let target = hypergraph.incidence_element(incidence);
617 let target_index = checked_element_index(hypergraph, target)?;
618 if !is_visible(visible_elements, target_index) {
619 continue;
620 }
621 let weight = checked_incidence_weight(hypergraph, self.incidence_weights, incidence)?;
622 next_elements[target_index] += rank * (weight / total);
623 }
624 Ok(S::ZERO)
625 }
626}
627
628#[derive(Clone, Copy, Debug, PartialEq)]
634#[non_exhaustive]
635pub struct PageRankConfig<S> {
636 pub damping: S,
638 pub tolerance: S,
640 pub max_iterations: usize,
642}
643
644impl<S> PageRankConfig<S> {
645 #[must_use]
654 pub const fn new(damping: S, tolerance: S, max_iterations: usize) -> Self {
655 Self {
656 damping,
657 tolerance,
658 max_iterations,
659 }
660 }
661}
662
663#[derive(Clone, Copy, Debug, PartialEq)]
669#[non_exhaustive]
670pub struct PageRankReport<S> {
671 pub iterations: usize,
673 pub delta: S,
675}
676
677#[derive(Debug, Clone, PartialEq)]
683#[non_exhaustive]
684pub enum PageRankError<S> {
685 EmptyState,
687 InvalidDamping {
689 damping: S,
691 },
692 InvalidTolerance {
694 tolerance: S,
696 },
697 InvalidMaxIterations,
699 OutputTooShort {
701 required: usize,
703 actual: usize,
705 },
706 ScratchTooShort {
708 name: &'static str,
710 required: usize,
712 actual: usize,
714 },
715 PersonalizationTooShort {
717 required: usize,
719 actual: usize,
721 },
722 InvalidPersonalization {
724 index: usize,
726 value: S,
728 },
729 ZeroPersonalization,
731 ElementIndexOutOfBounds {
733 index: usize,
735 bound: usize,
737 },
738 DuplicateElement {
740 index: usize,
742 },
743 DuplicateRelation {
745 index: usize,
747 },
748 RelationIndexOutOfBounds {
750 index: usize,
752 bound: usize,
754 },
755 IncidenceIndexOutOfBounds {
757 index: usize,
759 bound: usize,
761 },
762 InvalidRelationWeight {
764 index: usize,
766 value: S,
768 },
769 InvalidIncidenceWeight {
771 index: usize,
773 value: S,
775 },
776 NonConverged {
778 iterations: usize,
780 delta: S,
782 },
783}
784
785impl<S: fmt::Debug> fmt::Display for PageRankError<S> {
786 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
787 match self {
788 Self::EmptyState => formatter.write_str("pagerank state set is empty"),
789 Self::InvalidDamping { damping } => {
790 write!(formatter, "invalid pagerank damping {damping:?}")
791 }
792 Self::InvalidTolerance { tolerance } => {
793 write!(formatter, "invalid pagerank tolerance {tolerance:?}")
794 }
795 Self::InvalidMaxIterations => {
796 formatter.write_str("pagerank max_iterations must be positive")
797 }
798 Self::OutputTooShort { required, actual } => write!(
799 formatter,
800 "pagerank output too short: required {required}, got {actual}"
801 ),
802 Self::ScratchTooShort {
803 name,
804 required,
805 actual,
806 } => write!(
807 formatter,
808 "pagerank scratch '{name}' too short: required {required}, got {actual}"
809 ),
810 Self::PersonalizationTooShort { required, actual } => write!(
811 formatter,
812 "pagerank personalization too short: required {required}, got {actual}"
813 ),
814 Self::InvalidPersonalization { index, value } => write!(
815 formatter,
816 "invalid pagerank personalization at {index}: {value:?}"
817 ),
818 Self::ZeroPersonalization => {
819 formatter.write_str("pagerank personalization sum is zero")
820 }
821 Self::ElementIndexOutOfBounds { index, bound } => {
822 write!(formatter, "element index {index} is outside bound {bound}")
823 }
824 Self::DuplicateElement { index } => {
825 write!(formatter, "duplicate pagerank element index {index}")
826 }
827 Self::DuplicateRelation { index } => {
828 write!(formatter, "duplicate pagerank relation index {index}")
829 }
830 Self::RelationIndexOutOfBounds { index, bound } => {
831 write!(formatter, "relation index {index} is outside bound {bound}")
832 }
833 Self::IncidenceIndexOutOfBounds { index, bound } => {
834 write!(
835 formatter,
836 "incidence index {index} is outside bound {bound}"
837 )
838 }
839 Self::InvalidRelationWeight { index, value } => {
840 write!(formatter, "invalid relation weight at {index}: {value:?}")
841 }
842 Self::InvalidIncidenceWeight { index, value } => {
843 write!(formatter, "invalid incidence weight at {index}: {value:?}")
844 }
845 Self::NonConverged { iterations, delta } => write!(
846 formatter,
847 "pagerank did not converge after {iterations} iterations; delta {delta:?}"
848 ),
849 }
850 }
851}
852
853impl<S: fmt::Debug> Error for PageRankError<S> {}
854
855#[derive(Debug)]
862#[must_use]
863pub struct PageRankScratch<'scratch, S> {
864 teleport: &'scratch mut [S],
866 next: &'scratch mut [S],
868 visible_elements: &'scratch mut [u8],
870}
871
872impl<'scratch, S> PageRankScratch<'scratch, S> {
873 pub const fn new(
879 teleport: &'scratch mut [S],
880 next: &'scratch mut [S],
881 visible_elements: &'scratch mut [u8],
882 ) -> Self {
883 Self {
884 teleport,
885 next,
886 visible_elements,
887 }
888 }
889
890 #[must_use]
896 pub const fn teleport_capacity(&self) -> usize {
897 self.teleport.len()
898 }
899
900 #[must_use]
906 pub const fn next_capacity(&self) -> usize {
907 self.next.len()
908 }
909
910 #[must_use]
916 pub const fn visible_element_capacity(&self) -> usize {
917 self.visible_elements.len()
918 }
919}
920
921#[derive(Debug)]
928#[must_use]
929pub struct HypergraphPageRankScratch<'scratch, S> {
930 teleport: &'scratch mut [S],
932 next_elements: &'scratch mut [S],
934 next_relations: &'scratch mut [S],
936 visible_elements: &'scratch mut [u8],
938 visible_relations: &'scratch mut [u8],
940}
941
942impl<'scratch, S> HypergraphPageRankScratch<'scratch, S> {
943 pub const fn new(
949 teleport: &'scratch mut [S],
950 next_elements: &'scratch mut [S],
951 next_relations: &'scratch mut [S],
952 visible_elements: &'scratch mut [u8],
953 visible_relations: &'scratch mut [u8],
954 ) -> Self {
955 Self {
956 teleport,
957 next_elements,
958 next_relations,
959 visible_elements,
960 visible_relations,
961 }
962 }
963
964 #[must_use]
970 pub const fn teleport_capacity(&self) -> usize {
971 self.teleport.len()
972 }
973}
974
975#[cfg(feature = "alloc")]
984#[derive(Clone, Debug)]
985pub struct PageRankWorkspace<G, S> {
986 teleport: Vec<S>,
988 next: Vec<S>,
990 visible_elements: Vec<u8>,
992 _graph: PhantomData<fn() -> G>,
994}
995
996#[cfg(feature = "alloc")]
997impl<G, S: PageRankScalar> Default for PageRankWorkspace<G, S> {
998 fn default() -> Self {
999 Self::new()
1000 }
1001}
1002
1003#[cfg(feature = "alloc")]
1004impl<G, S: PageRankScalar> PageRankWorkspace<G, S> {
1005 #[must_use]
1011 pub const fn new() -> Self {
1012 Self {
1013 teleport: Vec::new(),
1014 next: Vec::new(),
1015 visible_elements: Vec::new(),
1016 _graph: PhantomData,
1017 }
1018 }
1019
1020 #[must_use]
1026 pub fn for_graph(graph: &G) -> Self
1027 where
1028 G: ElementIndex,
1029 {
1030 Self::with_element_bound(graph.element_bound())
1031 }
1032
1033 #[must_use]
1039 pub fn with_element_bound(element_bound: usize) -> Self {
1040 Self {
1041 teleport: vec![S::ZERO; element_bound],
1042 next: vec![S::ZERO; element_bound],
1043 visible_elements: vec![0; element_bound],
1044 _graph: PhantomData,
1045 }
1046 }
1047
1048 #[must_use]
1054 pub const fn element_bound_capacity(&self) -> usize {
1055 self.teleport.len()
1056 }
1057
1058 fn ensure_element_bound(&mut self, element_bound: usize) {
1060 if self.teleport.len() < element_bound {
1061 self.teleport.resize(element_bound, S::ZERO);
1062 }
1063 if self.next.len() < element_bound {
1064 self.next.resize(element_bound, S::ZERO);
1065 }
1066 if self.visible_elements.len() < element_bound {
1067 self.visible_elements.resize(element_bound, 0);
1068 }
1069 }
1070
1071 fn as_scratch(&mut self) -> PageRankScratch<'_, S> {
1073 PageRankScratch::new(
1074 &mut self.teleport,
1075 &mut self.next,
1076 &mut self.visible_elements,
1077 )
1078 }
1079}
1080
1081#[cfg(feature = "alloc")]
1087#[derive(Clone, Debug)]
1088pub struct HypergraphPageRankWorkspace<H, S> {
1089 teleport: Vec<S>,
1091 next_elements: Vec<S>,
1093 next_relations: Vec<S>,
1095 visible_elements: Vec<u8>,
1097 visible_relations: Vec<u8>,
1099 _hypergraph: PhantomData<fn() -> H>,
1101}
1102
1103#[cfg(feature = "alloc")]
1104impl<H, S: PageRankScalar> Default for HypergraphPageRankWorkspace<H, S> {
1105 fn default() -> Self {
1106 Self::new()
1107 }
1108}
1109
1110#[cfg(feature = "alloc")]
1111impl<H, S: PageRankScalar> HypergraphPageRankWorkspace<H, S> {
1112 #[must_use]
1118 pub const fn new() -> Self {
1119 Self {
1120 teleport: Vec::new(),
1121 next_elements: Vec::new(),
1122 next_relations: Vec::new(),
1123 visible_elements: Vec::new(),
1124 visible_relations: Vec::new(),
1125 _hypergraph: PhantomData,
1126 }
1127 }
1128
1129 #[must_use]
1135 pub fn for_hypergraph(hypergraph: &H) -> Self
1136 where
1137 H: ElementIndex + RelationIndex,
1138 {
1139 Self::with_bounds(hypergraph.element_bound(), hypergraph.relation_bound())
1140 }
1141
1142 #[must_use]
1148 pub fn with_bounds(element_bound: usize, relation_bound: usize) -> Self {
1149 let state_bound = element_bound.saturating_add(relation_bound);
1150 Self {
1151 teleport: vec![S::ZERO; state_bound],
1152 next_elements: vec![S::ZERO; element_bound],
1153 next_relations: vec![S::ZERO; relation_bound],
1154 visible_elements: vec![0; element_bound],
1155 visible_relations: vec![0; relation_bound],
1156 _hypergraph: PhantomData,
1157 }
1158 }
1159
1160 #[must_use]
1166 pub const fn element_bound_capacity(&self) -> usize {
1167 self.next_elements.len()
1168 }
1169
1170 #[must_use]
1176 pub const fn relation_bound_capacity(&self) -> usize {
1177 self.next_relations.len()
1178 }
1179
1180 fn ensure_bounds(&mut self, element_bound: usize, relation_bound: usize, state_bound: usize) {
1182 if self.teleport.len() < state_bound {
1183 self.teleport.resize(state_bound, S::ZERO);
1184 }
1185 if self.next_elements.len() < element_bound {
1186 self.next_elements.resize(element_bound, S::ZERO);
1187 }
1188 if self.next_relations.len() < relation_bound {
1189 self.next_relations.resize(relation_bound, S::ZERO);
1190 }
1191 if self.visible_elements.len() < element_bound {
1192 self.visible_elements.resize(element_bound, 0);
1193 }
1194 if self.visible_relations.len() < relation_bound {
1195 self.visible_relations.resize(relation_bound, 0);
1196 }
1197 }
1198
1199 fn as_scratch(&mut self) -> HypergraphPageRankScratch<'_, S> {
1201 HypergraphPageRankScratch::new(
1202 &mut self.teleport,
1203 &mut self.next_elements,
1204 &mut self.next_relations,
1205 &mut self.visible_elements,
1206 &mut self.visible_relations,
1207 )
1208 }
1209}
1210
1211#[cfg(feature = "alloc")]
1233#[expect(
1234 clippy::too_many_arguments,
1235 reason = "PageRank entry point keeps graph, distribution, elements, config, personalization, and output explicit"
1236)]
1237pub fn pagerank_graph<G, D, I, S>(
1238 graph: &G,
1239 distribution: &D,
1240 elements: I,
1241 config: PageRankConfig<S>,
1242 personalization: Option<&[S]>,
1243 ranks: &mut [S],
1244) -> Result<PageRankReport<S>, PageRankError<S>>
1245where
1246 G: ForwardGraph + ElementIndex,
1247 D: OutgoingDistribution<G, S>,
1248 I: Clone + IntoIterator<Item = ElementId<G>>,
1249 S: PageRankScalar,
1250{
1251 let bound = graph.element_bound();
1252 let mut teleport = vec![S::ZERO; bound];
1253 let mut next = vec![S::ZERO; bound];
1254 let mut visible_elements = vec![0; bound];
1255 pagerank_graph_with_scratch(
1256 graph,
1257 distribution,
1258 elements,
1259 config,
1260 personalization,
1261 ranks,
1262 PageRankScratch::new(&mut teleport, &mut next, &mut visible_elements),
1263 )
1264}
1265
1266#[expect(
1281 clippy::too_many_arguments,
1282 clippy::needless_pass_by_value,
1283 reason = "PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1284)]
1285pub fn pagerank_graph_with_scratch<G, D, I, S>(
1286 graph: &G,
1287 distribution: &D,
1288 elements: I,
1289 config: PageRankConfig<S>,
1290 personalization: Option<&[S]>,
1291 ranks: &mut [S],
1292 scratch: PageRankScratch<'_, S>,
1293) -> Result<PageRankReport<S>, PageRankError<S>>
1294where
1295 G: ForwardGraph + ElementIndex,
1296 D: OutgoingDistribution<G, S>,
1297 I: Clone + IntoIterator<Item = ElementId<G>>,
1298 S: PageRankScalar,
1299{
1300 validate_config(config)?;
1301 let bound = graph.element_bound();
1302 ensure_output_len(ranks.len(), bound)?;
1303 ensure_scratch_len("teleport", scratch.teleport.len(), bound)?;
1304 ensure_scratch_len("next", scratch.next.len(), bound)?;
1305 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), bound)?;
1306 build_personalization_into(
1307 elements.clone(),
1308 bound,
1309 personalization,
1310 |element| graph.element_index(element),
1311 scratch.teleport,
1312 scratch.visible_elements,
1313 )?;
1314 initialize_ranks(elements.clone(), graph, scratch.teleport, ranks)?;
1315 iterate_graph(
1316 graph,
1317 distribution,
1318 elements,
1319 config,
1320 scratch.teleport,
1321 scratch.visible_elements,
1322 ranks,
1323 scratch.next,
1324 )
1325}
1326
1327#[cfg(feature = "alloc")]
1342#[expect(
1343 clippy::too_many_arguments,
1344 reason = "PageRank workspace API keeps policy and reusable storage inputs explicit"
1345)]
1346pub fn pagerank_graph_with_workspace<G, D, I, S>(
1347 graph: &G,
1348 distribution: &D,
1349 elements: I,
1350 config: PageRankConfig<S>,
1351 personalization: Option<&[S]>,
1352 ranks: &mut [S],
1353 workspace: &mut PageRankWorkspace<G, S>,
1354) -> Result<PageRankReport<S>, PageRankError<S>>
1355where
1356 G: ForwardGraph + ElementIndex,
1357 D: OutgoingDistribution<G, S>,
1358 I: Clone + IntoIterator<Item = ElementId<G>>,
1359 S: PageRankScalar,
1360{
1361 workspace.ensure_element_bound(graph.element_bound());
1362 pagerank_graph_with_scratch(
1363 graph,
1364 distribution,
1365 elements,
1366 config,
1367 personalization,
1368 ranks,
1369 workspace.as_scratch(),
1370 )
1371}
1372
1373#[cfg(feature = "alloc")]
1400#[expect(
1401 clippy::too_many_arguments,
1402 reason = "hypergraph PageRank entry point keeps hypergraph, distribution, state families, and output explicit"
1403)]
1404pub fn pagerank_hypergraph<H, D, IE, IR, S>(
1405 hypergraph: &H,
1406 distribution: &D,
1407 elements: IE,
1408 relations: IR,
1409 config: PageRankConfig<S>,
1410 personalization: Option<&[S]>,
1411 element_ranks: &mut [S],
1412 relation_ranks: &mut [S],
1413) -> Result<PageRankReport<S>, PageRankError<S>>
1414where
1415 H: DirectedVertexHyperedges
1416 + DirectedHyperedgeIncidences
1417 + IncidenceElement
1418 + ElementIndex
1419 + RelationIndex,
1420 D: HypergraphOutgoingDistribution<H, S>,
1421 IE: Clone + IntoIterator<Item = ElementId<H>>,
1422 IR: Clone + IntoIterator<Item = RelationId<H>>,
1423 S: PageRankScalar,
1424{
1425 let e_bound = hypergraph.element_bound();
1426 let r_bound = hypergraph.relation_bound();
1427 let state_bound =
1428 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1429 let mut teleport = vec![S::ZERO; state_bound];
1430 let mut next_elements = vec![S::ZERO; e_bound];
1431 let mut next_relations = vec![S::ZERO; r_bound];
1432 let mut visible_elements = vec![0; e_bound];
1433 let mut visible_relations = vec![0; r_bound];
1434 pagerank_hypergraph_with_scratch(
1435 hypergraph,
1436 distribution,
1437 elements,
1438 relations,
1439 config,
1440 personalization,
1441 element_ranks,
1442 relation_ranks,
1443 HypergraphPageRankScratch::new(
1444 &mut teleport,
1445 &mut next_elements,
1446 &mut next_relations,
1447 &mut visible_elements,
1448 &mut visible_relations,
1449 ),
1450 )
1451}
1452
1453#[expect(
1469 clippy::too_many_arguments,
1470 reason = "hypergraph PageRank scratch entry point keeps policy and storage inputs explicit"
1471)]
1472#[expect(
1473 clippy::needless_pass_by_value,
1474 reason = "hypergraph PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
1475)]
1476pub fn pagerank_hypergraph_with_scratch<H, D, IE, IR, S>(
1477 hypergraph: &H,
1478 distribution: &D,
1479 elements: IE,
1480 relations: IR,
1481 config: PageRankConfig<S>,
1482 personalization: Option<&[S]>,
1483 element_ranks: &mut [S],
1484 relation_ranks: &mut [S],
1485 scratch: HypergraphPageRankScratch<'_, S>,
1486) -> Result<PageRankReport<S>, PageRankError<S>>
1487where
1488 H: DirectedVertexHyperedges
1489 + DirectedHyperedgeIncidences
1490 + IncidenceElement
1491 + ElementIndex
1492 + RelationIndex,
1493 D: HypergraphOutgoingDistribution<H, S>,
1494 IE: Clone + IntoIterator<Item = ElementId<H>>,
1495 IR: Clone + IntoIterator<Item = RelationId<H>>,
1496 S: PageRankScalar,
1497{
1498 validate_config(config)?;
1499 let e_bound = hypergraph.element_bound();
1500 let r_bound = hypergraph.relation_bound();
1501 let state_bound =
1502 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1503 ensure_scratch_len("teleport", scratch.teleport.len(), state_bound)?;
1504 ensure_scratch_len("next_elements", scratch.next_elements.len(), e_bound)?;
1505 ensure_scratch_len("next_relations", scratch.next_relations.len(), r_bound)?;
1506 ensure_scratch_len("visible_elements", scratch.visible_elements.len(), e_bound)?;
1507 ensure_scratch_len(
1508 "visible_relations",
1509 scratch.visible_relations.len(),
1510 r_bound,
1511 )?;
1512 build_hyper_personalization_into(
1513 hypergraph,
1514 elements.clone(),
1515 relations.clone(),
1516 state_bound,
1517 personalization,
1518 scratch.teleport,
1519 scratch.visible_elements,
1520 scratch.visible_relations,
1521 )?;
1522 initialize_hyper_ranks(
1523 hypergraph,
1524 elements.clone(),
1525 relations.clone(),
1526 scratch.teleport,
1527 element_ranks,
1528 relation_ranks,
1529 )?;
1530 iterate_hypergraph(
1531 hypergraph,
1532 distribution,
1533 elements,
1534 relations,
1535 config,
1536 scratch.teleport,
1537 scratch.visible_elements,
1538 scratch.visible_relations,
1539 element_ranks,
1540 relation_ranks,
1541 scratch.next_elements,
1542 scratch.next_relations,
1543 )
1544}
1545
1546#[cfg(feature = "alloc")]
1561#[expect(
1562 clippy::too_many_arguments,
1563 reason = "hypergraph PageRank workspace entry point keeps policy and storage inputs explicit"
1564)]
1565pub fn pagerank_hypergraph_with_workspace<H, D, IE, IR, S>(
1566 hypergraph: &H,
1567 distribution: &D,
1568 elements: IE,
1569 relations: IR,
1570 config: PageRankConfig<S>,
1571 personalization: Option<&[S]>,
1572 element_ranks: &mut [S],
1573 relation_ranks: &mut [S],
1574 workspace: &mut HypergraphPageRankWorkspace<H, S>,
1575) -> Result<PageRankReport<S>, PageRankError<S>>
1576where
1577 H: DirectedVertexHyperedges
1578 + DirectedHyperedgeIncidences
1579 + IncidenceElement
1580 + ElementIndex
1581 + RelationIndex,
1582 D: HypergraphOutgoingDistribution<H, S>,
1583 IE: Clone + IntoIterator<Item = ElementId<H>>,
1584 IR: Clone + IntoIterator<Item = RelationId<H>>,
1585 S: PageRankScalar,
1586{
1587 let e_bound = hypergraph.element_bound();
1588 let r_bound = hypergraph.relation_bound();
1589 let state_bound =
1590 checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
1591 workspace.ensure_bounds(e_bound, r_bound, state_bound);
1592 pagerank_hypergraph_with_scratch(
1593 hypergraph,
1594 distribution,
1595 elements,
1596 relations,
1597 config,
1598 personalization,
1599 element_ranks,
1600 relation_ranks,
1601 workspace.as_scratch(),
1602 )
1603}
1604
1605fn validate_config<S: PageRankScalar>(config: PageRankConfig<S>) -> Result<(), PageRankError<S>> {
1606 if !config.damping.is_finite() || config.damping < S::ZERO || config.damping > S::ONE {
1607 return Err(PageRankError::InvalidDamping {
1608 damping: config.damping,
1609 });
1610 }
1611 if !config.tolerance.is_finite() || config.tolerance < S::ZERO {
1612 return Err(PageRankError::InvalidTolerance {
1613 tolerance: config.tolerance,
1614 });
1615 }
1616 if config.max_iterations == 0 {
1617 return Err(PageRankError::InvalidMaxIterations);
1618 }
1619 Ok(())
1620}
1621
1622const fn ensure_output_len<S>(actual: usize, required: usize) -> Result<(), PageRankError<S>> {
1623 if actual < required {
1624 Err(PageRankError::OutputTooShort { required, actual })
1625 } else {
1626 Ok(())
1627 }
1628}
1629
1630const fn ensure_scratch_len<S>(
1631 name: &'static str,
1632 actual: usize,
1633 required: usize,
1634) -> Result<(), PageRankError<S>> {
1635 if actual < required {
1636 Err(PageRankError::ScratchTooShort {
1637 name,
1638 required,
1639 actual,
1640 })
1641 } else {
1642 Ok(())
1643 }
1644}
1645
1646fn checked_state_bound<S>(
1647 e_bound: usize,
1648 r_bound: usize,
1649 element_output_len: usize,
1650 relation_output_len: usize,
1651) -> Result<usize, PageRankError<S>> {
1652 ensure_output_len(element_output_len, e_bound)?;
1653 ensure_output_len(relation_output_len, r_bound)?;
1654 e_bound
1655 .checked_add(r_bound)
1656 .ok_or_else(|| PageRankError::OutputTooShort {
1657 required: usize::MAX,
1658 actual: element_output_len.saturating_add(relation_output_len),
1659 })
1660}
1661
1662fn clear<S: PageRankScalar>(values: &mut [S], len: usize) {
1663 for value in &mut values[..len] {
1664 *value = S::ZERO;
1665 }
1666}
1667
1668fn clear_u8(values: &mut [u8], len: usize) {
1669 for value in &mut values[..len] {
1670 *value = 0;
1671 }
1672}
1673
1674fn mark_visible_element<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1675 if visible[index] != 0 {
1676 return Err(PageRankError::DuplicateElement { index });
1677 }
1678 visible[index] = 1;
1679 Ok(())
1680}
1681
1682fn mark_visible_relation<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
1683 if visible[index] != 0 {
1684 return Err(PageRankError::DuplicateRelation { index });
1685 }
1686 visible[index] = 1;
1687 Ok(())
1688}
1689
1690fn is_visible(visible: &[u8], index: usize) -> bool {
1695 visible[index] != 0
1696}
1697
1698#[expect(
1699 clippy::too_many_arguments,
1700 reason = "personalization normalization keeps topology family bounds and caller buffers explicit"
1701)]
1702fn build_personalization_into<I, F, S>(
1703 elements: I,
1704 bound: usize,
1705 personalization: Option<&[S]>,
1706 index_of: F,
1707 out: &mut [S],
1708 visible: &mut [u8],
1709) -> Result<(), PageRankError<S>>
1710where
1711 I: IntoIterator,
1712 F: Fn(I::Item) -> usize,
1713 S: PageRankScalar,
1714{
1715 clear(out, bound);
1716 clear_u8(visible, bound);
1717 let mut count = 0_usize;
1718 let mut sum = S::ZERO;
1719 if let Some(input) = personalization {
1720 if input.len() < bound {
1721 return Err(PageRankError::PersonalizationTooShort {
1722 required: bound,
1723 actual: input.len(),
1724 });
1725 }
1726 for element in elements {
1727 let index = index_of(element);
1728 check_index(index, bound)?;
1729 mark_visible_element(visible, index)?;
1730 let value = input[index];
1731 check_personalization_value(index, value)?;
1732 out[index] = value;
1733 sum += value;
1734 count += 1;
1735 }
1736 } else {
1737 for element in elements {
1738 let index = index_of(element);
1739 check_index(index, bound)?;
1740 mark_visible_element(visible, index)?;
1741 out[index] = S::ONE;
1742 sum += S::ONE;
1743 count += 1;
1744 }
1745 }
1746 normalize_personalization(out, count, sum)
1747}
1748
1749enum PersonalizationSource<'a, S> {
1760 Uniform,
1762 FromInput(&'a [S]),
1766}
1767
1768impl<S> PersonalizationSource<'_, S>
1769where
1770 S: PageRankScalar,
1771{
1772 fn value_at(&self, state_index: usize) -> Result<S, PageRankError<S>> {
1782 match *self {
1783 Self::Uniform => Ok(S::ONE),
1784 Self::FromInput(input) => {
1785 let value = input[state_index];
1786 check_personalization_value(state_index, value)?;
1787 Ok(value)
1788 }
1789 }
1790 }
1791
1792 #[expect(
1815 clippy::too_many_arguments,
1816 reason = "method threads separate element/relation iterables, scratch slices, and the state bound; bundling them obscures the borrow shape that callers explicitly construct"
1817 )]
1818 fn fill_hypergraph<H, IE, IR>(
1819 &self,
1820 hypergraph: &H,
1821 elements: IE,
1822 relations: IR,
1823 state_bound: usize,
1824 out: &mut [S],
1825 visible_elements: &mut [u8],
1826 visible_relations: &mut [u8],
1827 ) -> Result<(usize, S), PageRankError<S>>
1828 where
1829 H: ElementIndex + RelationIndex,
1830 IE: IntoIterator<Item = ElementId<H>>,
1831 IR: IntoIterator<Item = RelationId<H>>,
1832 {
1833 if let Self::FromInput(input) = *self
1834 && input.len() < state_bound
1835 {
1836 return Err(PageRankError::PersonalizationTooShort {
1837 required: state_bound,
1838 actual: input.len(),
1839 });
1840 }
1841 let e_bound = hypergraph.element_bound();
1842 let r_bound = hypergraph.relation_bound();
1843 let mut count = 0_usize;
1844 let mut sum = S::ZERO;
1845 for element in elements {
1846 let index = hypergraph.element_index(element);
1847 check_index(index, e_bound)?;
1848 mark_visible_element(visible_elements, index)?;
1849 let value = self.value_at(index)?;
1850 out[index] = value;
1851 sum += value;
1852 count += 1;
1853 }
1854 for relation in relations {
1855 let index = hypergraph.relation_index(relation);
1856 check_relation_index(index, r_bound)?;
1857 mark_visible_relation(visible_relations, index)?;
1858 let state = e_bound + index;
1859 let value = self.value_at(state)?;
1860 out[state] = value;
1861 sum += value;
1862 count += 1;
1863 }
1864 Ok((count, sum))
1865 }
1866}
1867
1868#[expect(
1869 clippy::too_many_arguments,
1870 reason = "helper threads separate element/relation state and caller scratch explicitly"
1871)]
1872fn build_hyper_personalization_into<H, IE, IR, S>(
1873 hypergraph: &H,
1874 elements: IE,
1875 relations: IR,
1876 state_bound: usize,
1877 personalization: Option<&[S]>,
1878 out: &mut [S],
1879 visible_elements: &mut [u8],
1880 visible_relations: &mut [u8],
1881) -> Result<(), PageRankError<S>>
1882where
1883 H: ElementIndex + RelationIndex,
1884 IE: IntoIterator<Item = ElementId<H>>,
1885 IR: IntoIterator<Item = RelationId<H>>,
1886 S: PageRankScalar,
1887{
1888 clear(out, state_bound);
1889 clear_u8(visible_elements, hypergraph.element_bound());
1890 clear_u8(visible_relations, hypergraph.relation_bound());
1891 let source = personalization.map_or(
1892 PersonalizationSource::Uniform,
1893 PersonalizationSource::FromInput,
1894 );
1895 let (count, sum) = source.fill_hypergraph(
1896 hypergraph,
1897 elements,
1898 relations,
1899 state_bound,
1900 out,
1901 visible_elements,
1902 visible_relations,
1903 )?;
1904 normalize_personalization(out, count, sum)
1905}
1906
1907fn normalize_personalization<S: PageRankScalar>(
1908 out: &mut [S],
1909 count: usize,
1910 sum: S,
1911) -> Result<(), PageRankError<S>> {
1912 if count == 0 {
1913 return Err(PageRankError::EmptyState);
1914 }
1915 if sum <= S::ZERO {
1916 return Err(PageRankError::ZeroPersonalization);
1917 }
1918 for value in out {
1919 *value = *value / sum;
1920 }
1921 Ok(())
1922}
1923
1924fn check_personalization_value<S: PageRankScalar>(
1925 index: usize,
1926 value: S,
1927) -> Result<(), PageRankError<S>> {
1928 if !value.is_finite() || value < S::ZERO {
1929 Err(PageRankError::InvalidPersonalization { index, value })
1930 } else {
1931 Ok(())
1932 }
1933}
1934
1935fn initialize_ranks<G, I, S>(
1936 elements: I,
1937 graph: &G,
1938 teleport: &[S],
1939 ranks: &mut [S],
1940) -> Result<(), PageRankError<S>>
1941where
1942 G: ElementIndex,
1943 I: IntoIterator<Item = ElementId<G>>,
1944 S: PageRankScalar,
1945{
1946 clear(ranks, graph.element_bound());
1947 for element in elements {
1948 let index = graph.element_index(element);
1949 check_index(index, graph.element_bound())?;
1950 ranks[index] = teleport[index];
1951 }
1952 Ok(())
1953}
1954
1955#[expect(
1956 clippy::too_many_arguments,
1957 reason = "initialization writes separate element and relation rank slices"
1958)]
1959fn initialize_hyper_ranks<H, IE, IR, S>(
1960 hypergraph: &H,
1961 elements: IE,
1962 relations: IR,
1963 teleport: &[S],
1964 element_ranks: &mut [S],
1965 relation_ranks: &mut [S],
1966) -> Result<(), PageRankError<S>>
1967where
1968 H: ElementIndex + RelationIndex,
1969 IE: IntoIterator<Item = ElementId<H>>,
1970 IR: IntoIterator<Item = RelationId<H>>,
1971 S: PageRankScalar,
1972{
1973 clear(element_ranks, hypergraph.element_bound());
1974 clear(relation_ranks, hypergraph.relation_bound());
1975 for element in elements {
1976 let index = hypergraph.element_index(element);
1977 check_index(index, hypergraph.element_bound())?;
1978 element_ranks[index] = teleport[index];
1979 }
1980 for relation in relations {
1981 let index = hypergraph.relation_index(relation);
1982 check_relation_index(index, hypergraph.relation_bound())?;
1983 relation_ranks[index] = teleport[hypergraph.element_bound() + index];
1984 }
1985 Ok(())
1986}
1987
1988#[expect(
1989 clippy::too_many_arguments,
1990 reason = "graph iteration helper keeps distribution, scratch, and policy inputs explicit"
1991)]
1992#[expect(
1993 clippy::needless_pass_by_value,
1994 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
1995)]
1996fn iterate_graph<G, D, I, S>(
1997 graph: &G,
1998 distribution: &D,
1999 elements: I,
2000 config: PageRankConfig<S>,
2001 teleport: &[S],
2002 visible: &[u8],
2003 ranks: &mut [S],
2004 next: &mut [S],
2005) -> Result<PageRankReport<S>, PageRankError<S>>
2006where
2007 G: ForwardGraph + ElementIndex,
2008 D: OutgoingDistribution<G, S>,
2009 I: Clone + IntoIterator<Item = ElementId<G>>,
2010 S: PageRankScalar,
2011{
2012 let mut last_delta = S::INFINITY;
2013 for iteration in 1..=config.max_iterations {
2014 clear(next, graph.element_bound());
2015 let mut dangling = S::ZERO;
2016 for element in elements.clone() {
2017 let index = checked_element_index(graph, element)?;
2018 let rank = ranks[index];
2019 dangling += distribution.distribute_outgoing(graph, element, rank, next, visible)?;
2020 }
2021 let delta = apply_graph_teleport(
2022 graph,
2023 elements.clone(),
2024 config,
2025 teleport,
2026 dangling,
2027 ranks,
2028 next,
2029 )?;
2030 last_delta = delta;
2031 if delta <= config.tolerance {
2032 return Ok(PageRankReport {
2033 iterations: iteration,
2034 delta,
2035 });
2036 }
2037 }
2038 Err(PageRankError::NonConverged {
2039 iterations: config.max_iterations,
2040 delta: last_delta,
2041 })
2042}
2043
2044#[expect(
2045 clippy::too_many_arguments,
2046 reason = "hypergraph iteration helper keeps distribution, state families, scratch, and policy inputs explicit"
2047)]
2048#[expect(
2049 clippy::needless_pass_by_value,
2050 reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
2051)]
2052fn iterate_hypergraph<H, D, IE, IR, S>(
2053 hypergraph: &H,
2054 distribution: &D,
2055 elements: IE,
2056 relations: IR,
2057 config: PageRankConfig<S>,
2058 teleport: &[S],
2059 visible_elements: &[u8],
2060 visible_relations: &[u8],
2061 element_ranks: &mut [S],
2062 relation_ranks: &mut [S],
2063 next_elements: &mut [S],
2064 next_relations: &mut [S],
2065) -> Result<PageRankReport<S>, PageRankError<S>>
2066where
2067 H: DirectedVertexHyperedges
2068 + DirectedHyperedgeIncidences
2069 + IncidenceElement
2070 + ElementIndex
2071 + RelationIndex,
2072 D: HypergraphOutgoingDistribution<H, S>,
2073 IE: Clone + IntoIterator<Item = ElementId<H>>,
2074 IR: Clone + IntoIterator<Item = RelationId<H>>,
2075 S: PageRankScalar,
2076{
2077 let mut last_delta = S::INFINITY;
2078 for iteration in 1..=config.max_iterations {
2079 clear(next_elements, hypergraph.element_bound());
2080 clear(next_relations, hypergraph.relation_bound());
2081 let mut dangling = S::ZERO;
2082 for element in elements.clone() {
2083 let index = checked_element_index(hypergraph, element)?;
2084 let rank = element_ranks[index];
2085 dangling += distribution.distribute_from_element(
2086 hypergraph,
2087 element,
2088 rank,
2089 next_relations,
2090 visible_relations,
2091 )?;
2092 }
2093 for relation in relations.clone() {
2094 let index = checked_relation_index_for(hypergraph, relation)?;
2095 let rank = relation_ranks[index];
2096 dangling += distribution.distribute_from_relation(
2097 hypergraph,
2098 relation,
2099 rank,
2100 next_elements,
2101 visible_elements,
2102 )?;
2103 }
2104 let delta = apply_hyper_teleport(
2105 hypergraph,
2106 elements.clone(),
2107 relations.clone(),
2108 config,
2109 teleport,
2110 dangling,
2111 element_ranks,
2112 relation_ranks,
2113 next_elements,
2114 next_relations,
2115 )?;
2116 last_delta = delta;
2117 if delta <= config.tolerance {
2118 return Ok(PageRankReport {
2119 iterations: iteration,
2120 delta,
2121 });
2122 }
2123 }
2124 Err(PageRankError::NonConverged {
2125 iterations: config.max_iterations,
2126 delta: last_delta,
2127 })
2128}
2129
2130fn checked_element_index<G: ElementIndex, S>(
2131 graph: &G,
2132 element: ElementId<G>,
2133) -> Result<usize, PageRankError<S>> {
2134 let index = graph.element_index(element);
2135 check_index(index, graph.element_bound())?;
2136 Ok(index)
2137}
2138
2139const fn check_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2140 if index < bound {
2141 Ok(())
2142 } else {
2143 Err(PageRankError::ElementIndexOutOfBounds { index, bound })
2144 }
2145}
2146
2147const fn check_relation_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
2148 if index < bound {
2149 Ok(())
2150 } else {
2151 Err(PageRankError::RelationIndexOutOfBounds { index, bound })
2152 }
2153}
2154
2155fn checked_relation_weight<G, W, S>(
2156 graph: &G,
2157 weights: &W,
2158 relation: RelationId<G>,
2159) -> Result<S, PageRankError<S>>
2160where
2161 G: RelationIndex,
2162 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2163 W::Weight: IntoPageRankScalar<S>,
2164 S: PageRankScalar,
2165{
2166 let index = graph.relation_index(relation);
2167 check_relation_index(index, graph.relation_bound())?;
2168 let value = weights.relation_weight(relation).into_pagerank_scalar();
2169 if !value.is_finite() || value < S::ZERO {
2170 Err(PageRankError::InvalidRelationWeight { index, value })
2171 } else {
2172 Ok(value)
2173 }
2174}
2175
2176fn checked_incidence_weight<H, W, S>(
2177 hypergraph: &H,
2178 weights: &W,
2179 incidence: H::IncidenceId,
2180) -> Result<S, PageRankError<S>>
2181where
2182 H: IncidenceIndex,
2183 W: IncidenceWeight<
2184 ElementId = H::ElementId,
2185 RelationId = H::RelationId,
2186 IncidenceId = H::IncidenceId,
2187 >,
2188 W::Weight: IntoPageRankScalar<S>,
2189 S: PageRankScalar,
2190{
2191 let index = hypergraph.incidence_index(incidence);
2192 let bound = hypergraph.incidence_bound();
2193 if index >= bound {
2194 return Err(PageRankError::IncidenceIndexOutOfBounds { index, bound });
2195 }
2196 let value = weights.incidence_weight(incidence).into_pagerank_scalar();
2197 if !value.is_finite() || value < S::ZERO {
2198 Err(PageRankError::InvalidIncidenceWeight { index, value })
2199 } else {
2200 Ok(value)
2201 }
2202}
2203
2204fn outgoing_weight_total<G, W, S>(
2205 graph: &G,
2206 weights: &W,
2207 element: ElementId<G>,
2208 visible: &[u8],
2209) -> Result<S, PageRankError<S>>
2210where
2211 G: ForwardGraph + ElementIndex + RelationIndex,
2212 W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
2213 W::Weight: IntoPageRankScalar<S>,
2214 S: PageRankScalar,
2215{
2216 let mut total = S::ZERO;
2217 for edge in graph.outgoing_edges(element) {
2218 let target = graph.target(edge);
2219 let target_index = checked_element_index(graph, target)?;
2220 if is_visible(visible, target_index) {
2221 total += checked_relation_weight(graph, weights, edge)?;
2222 }
2223 }
2224 Ok(total)
2225}
2226
2227fn teleport_update<S: PageRankScalar>(
2239 damping: S,
2240 teleport_scale: S,
2241 accumulated: S,
2242 teleport: S,
2243 previous: S,
2244) -> (S, S) {
2245 let value = (damping * accumulated) + (teleport_scale * teleport);
2246 let delta = (value - previous).abs();
2247 (value, delta)
2248}
2249
2250#[expect(
2251 clippy::too_many_arguments,
2252 reason = "teleport helper updates caller-provided rank and scratch slices"
2253)]
2254fn apply_graph_teleport<G, I, S>(
2255 graph: &G,
2256 elements: I,
2257 config: PageRankConfig<S>,
2258 teleport: &[S],
2259 dangling: S,
2260 ranks: &mut [S],
2261 next: &[S],
2262) -> Result<S, PageRankError<S>>
2263where
2264 G: ElementIndex,
2265 I: IntoIterator<Item = ElementId<G>>,
2266 S: PageRankScalar,
2267{
2268 let mut delta = S::ZERO;
2269 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2270 for element in elements {
2271 let index = checked_element_index(graph, element)?;
2272 let (value, state_delta) = teleport_update(
2273 config.damping,
2274 teleport_scale,
2275 next[index],
2276 teleport[index],
2277 ranks[index],
2278 );
2279 delta += state_delta;
2280 ranks[index] = value;
2281 }
2282 Ok(delta)
2283}
2284
2285fn checked_relation_index_for<H: RelationIndex, S>(
2286 hypergraph: &H,
2287 relation: RelationId<H>,
2288) -> Result<usize, PageRankError<S>> {
2289 let index = hypergraph.relation_index(relation);
2290 check_relation_index(index, hypergraph.relation_bound())?;
2291 Ok(index)
2292}
2293
2294fn hyper_outgoing_relation_weight<H, W, S>(
2295 hypergraph: &H,
2296 weights: &W,
2297 element: ElementId<H>,
2298 visible_relations: &[u8],
2299) -> Result<S, PageRankError<S>>
2300where
2301 H: DirectedVertexHyperedges + RelationIndex,
2302 W: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
2303 W::Weight: IntoPageRankScalar<S>,
2304 S: PageRankScalar,
2305{
2306 let mut total = S::ZERO;
2307 for relation in hypergraph.outgoing_hyperedges(element) {
2308 let relation_index = checked_relation_index_for(hypergraph, relation)?;
2309 if is_visible(visible_relations, relation_index) {
2310 total += checked_relation_weight(hypergraph, weights, relation)?;
2311 }
2312 }
2313 Ok(total)
2314}
2315
2316fn hyper_target_incidence_weight<H, W, S>(
2317 hypergraph: &H,
2318 weights: &W,
2319 relation: RelationId<H>,
2320 visible_elements: &[u8],
2321) -> Result<S, PageRankError<S>>
2322where
2323 H: DirectedHyperedgeIncidences + IncidenceElement + ElementIndex + IncidenceIndex,
2324 W: IncidenceWeight<
2325 ElementId = H::ElementId,
2326 RelationId = H::RelationId,
2327 IncidenceId = H::IncidenceId,
2328 >,
2329 W::Weight: IntoPageRankScalar<S>,
2330 S: PageRankScalar,
2331{
2332 let mut total = S::ZERO;
2333 for incidence in hypergraph.target_incidences(relation) {
2334 let target = hypergraph.incidence_element(incidence);
2335 let target_index = checked_element_index(hypergraph, target)?;
2336 if is_visible(visible_elements, target_index) {
2337 total += checked_incidence_weight(hypergraph, weights, incidence)?;
2338 }
2339 }
2340 Ok(total)
2341}
2342
2343#[expect(
2344 clippy::too_many_arguments,
2345 reason = "hypergraph teleport updates separate element and relation slices"
2346)]
2347fn apply_hyper_teleport<H, IE, IR, S>(
2348 hypergraph: &H,
2349 elements: IE,
2350 relations: IR,
2351 config: PageRankConfig<S>,
2352 teleport: &[S],
2353 dangling: S,
2354 element_ranks: &mut [S],
2355 relation_ranks: &mut [S],
2356 next_elements: &[S],
2357 next_relations: &[S],
2358) -> Result<S, PageRankError<S>>
2359where
2360 H: ElementIndex + RelationIndex,
2361 IE: IntoIterator<Item = ElementId<H>>,
2362 IR: IntoIterator<Item = RelationId<H>>,
2363 S: PageRankScalar,
2364{
2365 let mut delta = S::ZERO;
2366 let e_bound = hypergraph.element_bound();
2367 let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
2368 for element in elements {
2369 let index = checked_element_index(hypergraph, element)?;
2370 let (value, state_delta) = teleport_update(
2371 config.damping,
2372 teleport_scale,
2373 next_elements[index],
2374 teleport[index],
2375 element_ranks[index],
2376 );
2377 delta += state_delta;
2378 element_ranks[index] = value;
2379 }
2380 for relation in relations {
2381 let index = checked_relation_index_for(hypergraph, relation)?;
2382 let state = e_bound + index;
2383 let (value, state_delta) = teleport_update(
2384 config.damping,
2385 teleport_scale,
2386 next_relations[index],
2387 teleport[state],
2388 relation_ranks[index],
2389 );
2390 delta += state_delta;
2391 relation_ranks[index] = value;
2392 }
2393 Ok(delta)
2394}