#![cfg_attr(
not(test),
expect(
clippy::missing_docs_in_private_items,
reason = "PageRank helper functions are private implementation details behind documented public API tiers"
)
)]
#[cfg(feature = "alloc")]
use alloc::{vec, vec::Vec};
#[cfg(feature = "alloc")]
use core::marker::PhantomData;
use core::{
error::Error,
fmt,
ops::{Add, AddAssign, Div, Mul, Sub},
};
use oxgraph_graph::ForwardGraph;
use oxgraph_hyper::{DirectedHyperedgeIncidences, DirectedVertexHyperedges};
use oxgraph_topology::{
ElementId, ElementIndex, IncidenceElement, IncidenceIndex, IncidenceWeight, RelationId,
RelationIndex, RelationWeight,
};
pub trait PageRankScalar:
Copy
+ fmt::Debug
+ PartialOrd
+ Add<Output = Self>
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ AddAssign
+ 'static
{
const ZERO: Self;
const ONE: Self;
const INFINITY: Self;
fn from_usize(value: usize) -> Self;
fn from_f64(value: f64) -> Self;
#[must_use]
fn abs(self) -> Self;
fn is_finite(self) -> bool;
}
impl PageRankScalar for f64 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
#[expect(
clippy::use_self,
reason = "primitive inherent infinity constant is clearer here"
)]
const INFINITY: Self = f64::INFINITY;
#[expect(
clippy::cast_precision_loss,
reason = "PageRank degree conversion is a documented scalar-boundary conversion"
)]
fn from_usize(value: usize) -> Self {
value as Self
}
fn from_f64(value: f64) -> Self {
value
}
fn abs(self) -> Self {
self.abs()
}
fn is_finite(self) -> bool {
self.is_finite()
}
}
impl PageRankScalar for f32 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
#[expect(
clippy::use_self,
reason = "primitive inherent infinity constant is clearer here"
)]
const INFINITY: Self = f32::INFINITY;
#[expect(
clippy::cast_precision_loss,
reason = "PageRank degree conversion is a documented scalar-boundary conversion"
)]
fn from_usize(value: usize) -> Self {
value as Self
}
#[expect(
clippy::cast_possible_truncation,
reason = "f32 PageRank callers explicitly select f32 rank/config output"
)]
fn from_f64(value: f64) -> Self {
value as Self
}
fn abs(self) -> Self {
self.abs()
}
fn is_finite(self) -> bool {
self.is_finite()
}
}
pub trait IntoPageRankScalar<S: PageRankScalar> {
fn into_pagerank_scalar(self) -> S;
}
impl<S: PageRankScalar> IntoPageRankScalar<S> for S {
fn into_pagerank_scalar(self) -> S {
self
}
}
macro_rules! impl_weight_into_pagerank_scalar_from {
($target:ty; $($type:ty),* $(,)?) => {
$(
impl IntoPageRankScalar<$target> for $type {
fn into_pagerank_scalar(self) -> $target { <$target>::from(self) }
}
)*
};
}
macro_rules! impl_weight_into_pagerank_scalar_cast {
($target:ty; $($type:ty),* $(,)?) => {
$(
impl IntoPageRankScalar<$target> for $type {
#[expect(
clippy::cast_precision_loss,
reason = "PageRank primitive weight conversions are explicit algorithm-boundary numeric interpretations"
)]
fn into_pagerank_scalar(self) -> $target { self as $target }
}
)*
};
}
impl_weight_into_pagerank_scalar_from!(f64; u8, u16, u32, i8, i16, i32, f32);
impl_weight_into_pagerank_scalar_cast!(f64; u64, usize, i64, isize);
impl_weight_into_pagerank_scalar_from!(f32; u8, u16, i8, i16);
impl_weight_into_pagerank_scalar_cast!(f32; u32, u64, usize, i32, i64, isize);
impl IntoPageRankScalar<f32> for f64 {
#[expect(
clippy::cast_possible_truncation,
reason = "f32 PageRank callers explicitly select f32 rank and configuration output"
)]
fn into_pagerank_scalar(self) -> f32 {
self as f32
}
}
pub trait OutgoingDistribution<G, S>
where
G: ForwardGraph + ElementIndex,
S: PageRankScalar,
{
#[expect(
clippy::too_many_arguments,
reason = "distribution kernel needs graph, element, rank, output buffer, and visibility mask explicitly"
)]
fn distribute_outgoing(
&self,
graph: &G,
element: G::ElementId,
rank: S,
next: &mut [S],
visible: &[u8],
) -> Result<S, PageRankError<S>>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Uniform;
impl<G, S> OutgoingDistribution<G, S> for Uniform
where
G: ForwardGraph + ElementIndex,
S: PageRankScalar,
{
fn distribute_outgoing(
&self,
graph: &G,
element: G::ElementId,
rank: S,
next: &mut [S],
visible: &[u8],
) -> Result<S, PageRankError<S>> {
let mut degree = 0_usize;
for edge in graph.outgoing_edges(element) {
let target = graph.target(edge);
let target_index = checked_element_index(graph, target)?;
if is_visible(visible, target_index) {
degree += 1;
}
}
if degree == 0 {
return Ok(rank);
}
let share = rank / S::from_usize(degree);
for edge in graph.outgoing_edges(element) {
let target = graph.target(edge);
let target_index = checked_element_index(graph, target)?;
if is_visible(visible, target_index) {
next[target_index] += share;
}
}
Ok(S::ZERO)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Weighted<'w, W> {
weights: &'w W,
}
impl<'w, W> Weighted<'w, W> {
#[must_use]
pub const fn new(weights: &'w W) -> Self {
Self { weights }
}
}
impl<G, W, S> OutgoingDistribution<G, S> for Weighted<'_, W>
where
G: ForwardGraph + ElementIndex + RelationIndex,
W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
fn distribute_outgoing(
&self,
graph: &G,
element: G::ElementId,
rank: S,
next: &mut [S],
visible: &[u8],
) -> Result<S, PageRankError<S>> {
let total = outgoing_weight_total(graph, self.weights, element, visible)?;
if total <= S::ZERO {
return Ok(rank);
}
for edge in graph.outgoing_edges(element) {
let target = graph.target(edge);
let target_index = checked_element_index(graph, target)?;
if is_visible(visible, target_index) {
let weight = checked_relation_weight(graph, self.weights, edge)?;
next[target_index] += rank * (weight / total);
}
}
Ok(S::ZERO)
}
}
pub trait HypergraphOutgoingDistribution<H, S>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
S: PageRankScalar,
{
#[expect(
clippy::too_many_arguments,
reason = "distribution kernel needs hypergraph, element, rank, output buffer, and visibility mask explicitly"
)]
fn distribute_from_element(
&self,
hypergraph: &H,
element: H::ElementId,
rank: S,
next_relations: &mut [S],
visible_relations: &[u8],
) -> Result<S, PageRankError<S>>;
#[expect(
clippy::too_many_arguments,
reason = "distribution kernel needs hypergraph, relation, rank, output buffer, and visibility mask explicitly"
)]
fn distribute_from_relation(
&self,
hypergraph: &H,
relation: H::RelationId,
rank: S,
next_elements: &mut [S],
visible_elements: &[u8],
) -> Result<S, PageRankError<S>>;
}
impl<H, S> HypergraphOutgoingDistribution<H, S> for Uniform
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
S: PageRankScalar,
{
fn distribute_from_element(
&self,
hypergraph: &H,
element: H::ElementId,
rank: S,
next_relations: &mut [S],
visible_relations: &[u8],
) -> Result<S, PageRankError<S>> {
let mut degree = 0_usize;
for relation in hypergraph.outgoing_hyperedges(element) {
let relation_index = checked_relation_index_for(hypergraph, relation)?;
if is_visible(visible_relations, relation_index) {
degree += 1;
}
}
if degree == 0 {
return Ok(rank);
}
let share = rank / S::from_usize(degree);
for relation in hypergraph.outgoing_hyperedges(element) {
let relation_index = checked_relation_index_for(hypergraph, relation)?;
if is_visible(visible_relations, relation_index) {
next_relations[relation_index] += share;
}
}
Ok(S::ZERO)
}
fn distribute_from_relation(
&self,
hypergraph: &H,
relation: H::RelationId,
rank: S,
next_elements: &mut [S],
visible_elements: &[u8],
) -> Result<S, PageRankError<S>> {
let mut degree = 0_usize;
for incidence in hypergraph.target_incidences(relation) {
let target = hypergraph.incidence_element(incidence);
let target_index = checked_element_index(hypergraph, target)?;
if is_visible(visible_elements, target_index) {
degree += 1;
}
}
if degree == 0 {
return Ok(rank);
}
let share = rank / S::from_usize(degree);
for incidence in hypergraph.target_incidences(relation) {
let target = hypergraph.incidence_element(incidence);
let target_index = checked_element_index(hypergraph, target)?;
if is_visible(visible_elements, target_index) {
next_elements[target_index] += share;
}
}
Ok(S::ZERO)
}
}
#[derive(Clone, Copy, Debug)]
pub struct HyperWeighted<'rw, 'iw, RW, IW> {
relation_weights: &'rw RW,
incidence_weights: &'iw IW,
}
impl<'rw, 'iw, RW, IW> HyperWeighted<'rw, 'iw, RW, IW> {
#[must_use]
pub const fn new(relation_weights: &'rw RW, incidence_weights: &'iw IW) -> Self {
Self {
relation_weights,
incidence_weights,
}
}
}
impl<H, RW, IW, S> HypergraphOutgoingDistribution<H, S> for HyperWeighted<'_, '_, RW, IW>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex
+ IncidenceIndex,
RW: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
RW::Weight: IntoPageRankScalar<S>,
IW: IncidenceWeight<
ElementId = H::ElementId,
RelationId = H::RelationId,
IncidenceId = H::IncidenceId,
>,
IW::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
fn distribute_from_element(
&self,
hypergraph: &H,
element: H::ElementId,
rank: S,
next_relations: &mut [S],
visible_relations: &[u8],
) -> Result<S, PageRankError<S>> {
let total = hyper_outgoing_relation_weight(
hypergraph,
self.relation_weights,
element,
visible_relations,
)?;
if total <= S::ZERO {
return Ok(rank);
}
for relation in hypergraph.outgoing_hyperedges(element) {
let relation_index = checked_relation_index_for(hypergraph, relation)?;
if !is_visible(visible_relations, relation_index) {
continue;
}
let weight = checked_relation_weight(hypergraph, self.relation_weights, relation)?;
next_relations[relation_index] += rank * (weight / total);
}
Ok(S::ZERO)
}
fn distribute_from_relation(
&self,
hypergraph: &H,
relation: H::RelationId,
rank: S,
next_elements: &mut [S],
visible_elements: &[u8],
) -> Result<S, PageRankError<S>> {
let total = hyper_target_incidence_weight(
hypergraph,
self.incidence_weights,
relation,
visible_elements,
)?;
if total <= S::ZERO {
return Ok(rank);
}
for incidence in hypergraph.target_incidences(relation) {
let target = hypergraph.incidence_element(incidence);
let target_index = checked_element_index(hypergraph, target)?;
if !is_visible(visible_elements, target_index) {
continue;
}
let weight = checked_incidence_weight(hypergraph, self.incidence_weights, incidence)?;
next_elements[target_index] += rank * (weight / total);
}
Ok(S::ZERO)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct PageRankConfig<S> {
pub damping: S,
pub tolerance: S,
pub max_iterations: usize,
}
impl<S> PageRankConfig<S> {
#[must_use]
pub const fn new(damping: S, tolerance: S, max_iterations: usize) -> Self {
Self {
damping,
tolerance,
max_iterations,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct PageRankReport<S> {
pub iterations: usize,
pub delta: S,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PageRankError<S> {
EmptyState,
InvalidDamping {
damping: S,
},
InvalidTolerance {
tolerance: S,
},
InvalidMaxIterations,
OutputTooShort {
required: usize,
actual: usize,
},
ScratchTooShort {
name: &'static str,
required: usize,
actual: usize,
},
PersonalizationTooShort {
required: usize,
actual: usize,
},
InvalidPersonalization {
index: usize,
value: S,
},
ZeroPersonalization,
ElementIndexOutOfBounds {
index: usize,
bound: usize,
},
DuplicateElement {
index: usize,
},
DuplicateRelation {
index: usize,
},
RelationIndexOutOfBounds {
index: usize,
bound: usize,
},
IncidenceIndexOutOfBounds {
index: usize,
bound: usize,
},
InvalidRelationWeight {
index: usize,
value: S,
},
InvalidIncidenceWeight {
index: usize,
value: S,
},
NonConverged {
iterations: usize,
delta: S,
},
}
impl<S: fmt::Debug> fmt::Display for PageRankError<S> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyState => formatter.write_str("pagerank state set is empty"),
Self::InvalidDamping { damping } => {
write!(formatter, "invalid pagerank damping {damping:?}")
}
Self::InvalidTolerance { tolerance } => {
write!(formatter, "invalid pagerank tolerance {tolerance:?}")
}
Self::InvalidMaxIterations => {
formatter.write_str("pagerank max_iterations must be positive")
}
Self::OutputTooShort { required, actual } => write!(
formatter,
"pagerank output too short: required {required}, got {actual}"
),
Self::ScratchTooShort {
name,
required,
actual,
} => write!(
formatter,
"pagerank scratch '{name}' too short: required {required}, got {actual}"
),
Self::PersonalizationTooShort { required, actual } => write!(
formatter,
"pagerank personalization too short: required {required}, got {actual}"
),
Self::InvalidPersonalization { index, value } => write!(
formatter,
"invalid pagerank personalization at {index}: {value:?}"
),
Self::ZeroPersonalization => {
formatter.write_str("pagerank personalization sum is zero")
}
Self::ElementIndexOutOfBounds { index, bound } => {
write!(formatter, "element index {index} is outside bound {bound}")
}
Self::DuplicateElement { index } => {
write!(formatter, "duplicate pagerank element index {index}")
}
Self::DuplicateRelation { index } => {
write!(formatter, "duplicate pagerank relation index {index}")
}
Self::RelationIndexOutOfBounds { index, bound } => {
write!(formatter, "relation index {index} is outside bound {bound}")
}
Self::IncidenceIndexOutOfBounds { index, bound } => {
write!(
formatter,
"incidence index {index} is outside bound {bound}"
)
}
Self::InvalidRelationWeight { index, value } => {
write!(formatter, "invalid relation weight at {index}: {value:?}")
}
Self::InvalidIncidenceWeight { index, value } => {
write!(formatter, "invalid incidence weight at {index}: {value:?}")
}
Self::NonConverged { iterations, delta } => write!(
formatter,
"pagerank did not converge after {iterations} iterations; delta {delta:?}"
),
}
}
}
impl<S: fmt::Debug> Error for PageRankError<S> {}
#[derive(Debug)]
#[must_use]
pub struct PageRankScratch<'scratch, S> {
teleport: &'scratch mut [S],
next: &'scratch mut [S],
visible_elements: &'scratch mut [u8],
}
impl<'scratch, S> PageRankScratch<'scratch, S> {
pub const fn new(
teleport: &'scratch mut [S],
next: &'scratch mut [S],
visible_elements: &'scratch mut [u8],
) -> Self {
Self {
teleport,
next,
visible_elements,
}
}
#[must_use]
pub const fn teleport_capacity(&self) -> usize {
self.teleport.len()
}
#[must_use]
pub const fn next_capacity(&self) -> usize {
self.next.len()
}
#[must_use]
pub const fn visible_element_capacity(&self) -> usize {
self.visible_elements.len()
}
}
#[derive(Debug)]
#[must_use]
pub struct HypergraphPageRankScratch<'scratch, S> {
teleport: &'scratch mut [S],
next_elements: &'scratch mut [S],
next_relations: &'scratch mut [S],
visible_elements: &'scratch mut [u8],
visible_relations: &'scratch mut [u8],
}
impl<'scratch, S> HypergraphPageRankScratch<'scratch, S> {
pub const fn new(
teleport: &'scratch mut [S],
next_elements: &'scratch mut [S],
next_relations: &'scratch mut [S],
visible_elements: &'scratch mut [u8],
visible_relations: &'scratch mut [u8],
) -> Self {
Self {
teleport,
next_elements,
next_relations,
visible_elements,
visible_relations,
}
}
#[must_use]
pub const fn teleport_capacity(&self) -> usize {
self.teleport.len()
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
pub struct PageRankWorkspace<G, S> {
teleport: Vec<S>,
next: Vec<S>,
visible_elements: Vec<u8>,
_graph: PhantomData<fn() -> G>,
}
#[cfg(feature = "alloc")]
impl<G, S: PageRankScalar> Default for PageRankWorkspace<G, S> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "alloc")]
impl<G, S: PageRankScalar> PageRankWorkspace<G, S> {
#[must_use]
pub const fn new() -> Self {
Self {
teleport: Vec::new(),
next: Vec::new(),
visible_elements: Vec::new(),
_graph: PhantomData,
}
}
#[must_use]
pub fn for_graph(graph: &G) -> Self
where
G: ElementIndex,
{
Self::with_element_bound(graph.element_bound())
}
#[must_use]
pub fn with_element_bound(element_bound: usize) -> Self {
Self {
teleport: vec![S::ZERO; element_bound],
next: vec![S::ZERO; element_bound],
visible_elements: vec![0; element_bound],
_graph: PhantomData,
}
}
#[must_use]
pub const fn element_bound_capacity(&self) -> usize {
self.teleport.len()
}
fn ensure_element_bound(&mut self, element_bound: usize) {
if self.teleport.len() < element_bound {
self.teleport.resize(element_bound, S::ZERO);
}
if self.next.len() < element_bound {
self.next.resize(element_bound, S::ZERO);
}
if self.visible_elements.len() < element_bound {
self.visible_elements.resize(element_bound, 0);
}
}
fn as_scratch(&mut self) -> PageRankScratch<'_, S> {
PageRankScratch::new(
&mut self.teleport,
&mut self.next,
&mut self.visible_elements,
)
}
}
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
pub struct HypergraphPageRankWorkspace<H, S> {
teleport: Vec<S>,
next_elements: Vec<S>,
next_relations: Vec<S>,
visible_elements: Vec<u8>,
visible_relations: Vec<u8>,
_hypergraph: PhantomData<fn() -> H>,
}
#[cfg(feature = "alloc")]
impl<H, S: PageRankScalar> Default for HypergraphPageRankWorkspace<H, S> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "alloc")]
impl<H, S: PageRankScalar> HypergraphPageRankWorkspace<H, S> {
#[must_use]
pub const fn new() -> Self {
Self {
teleport: Vec::new(),
next_elements: Vec::new(),
next_relations: Vec::new(),
visible_elements: Vec::new(),
visible_relations: Vec::new(),
_hypergraph: PhantomData,
}
}
#[must_use]
pub fn for_hypergraph(hypergraph: &H) -> Self
where
H: ElementIndex + RelationIndex,
{
Self::with_bounds(hypergraph.element_bound(), hypergraph.relation_bound())
}
#[must_use]
pub fn with_bounds(element_bound: usize, relation_bound: usize) -> Self {
let state_bound = element_bound.saturating_add(relation_bound);
Self {
teleport: vec![S::ZERO; state_bound],
next_elements: vec![S::ZERO; element_bound],
next_relations: vec![S::ZERO; relation_bound],
visible_elements: vec![0; element_bound],
visible_relations: vec![0; relation_bound],
_hypergraph: PhantomData,
}
}
#[must_use]
pub const fn element_bound_capacity(&self) -> usize {
self.next_elements.len()
}
#[must_use]
pub const fn relation_bound_capacity(&self) -> usize {
self.next_relations.len()
}
fn ensure_bounds(&mut self, element_bound: usize, relation_bound: usize, state_bound: usize) {
if self.teleport.len() < state_bound {
self.teleport.resize(state_bound, S::ZERO);
}
if self.next_elements.len() < element_bound {
self.next_elements.resize(element_bound, S::ZERO);
}
if self.next_relations.len() < relation_bound {
self.next_relations.resize(relation_bound, S::ZERO);
}
if self.visible_elements.len() < element_bound {
self.visible_elements.resize(element_bound, 0);
}
if self.visible_relations.len() < relation_bound {
self.visible_relations.resize(relation_bound, 0);
}
}
fn as_scratch(&mut self) -> HypergraphPageRankScratch<'_, S> {
HypergraphPageRankScratch::new(
&mut self.teleport,
&mut self.next_elements,
&mut self.next_relations,
&mut self.visible_elements,
&mut self.visible_relations,
)
}
}
#[cfg(feature = "alloc")]
#[expect(
clippy::too_many_arguments,
reason = "PageRank entry point keeps graph, distribution, elements, config, personalization, and output explicit"
)]
pub fn pagerank_graph<G, D, I, S>(
graph: &G,
distribution: &D,
elements: I,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
ranks: &mut [S],
) -> Result<PageRankReport<S>, PageRankError<S>>
where
G: ForwardGraph + ElementIndex,
D: OutgoingDistribution<G, S>,
I: Clone + IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
let bound = graph.element_bound();
let mut teleport = vec![S::ZERO; bound];
let mut next = vec![S::ZERO; bound];
let mut visible_elements = vec![0; bound];
pagerank_graph_with_scratch(
graph,
distribution,
elements,
config,
personalization,
ranks,
PageRankScratch::new(&mut teleport, &mut next, &mut visible_elements),
)
}
#[expect(
clippy::too_many_arguments,
clippy::needless_pass_by_value,
reason = "PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
)]
pub fn pagerank_graph_with_scratch<G, D, I, S>(
graph: &G,
distribution: &D,
elements: I,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
ranks: &mut [S],
scratch: PageRankScratch<'_, S>,
) -> Result<PageRankReport<S>, PageRankError<S>>
where
G: ForwardGraph + ElementIndex,
D: OutgoingDistribution<G, S>,
I: Clone + IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
validate_config(config)?;
let bound = graph.element_bound();
ensure_output_len(ranks.len(), bound)?;
ensure_scratch_len("teleport", scratch.teleport.len(), bound)?;
ensure_scratch_len("next", scratch.next.len(), bound)?;
ensure_scratch_len("visible_elements", scratch.visible_elements.len(), bound)?;
build_personalization_into(
elements.clone(),
bound,
personalization,
|element| graph.element_index(element),
scratch.teleport,
scratch.visible_elements,
)?;
initialize_ranks(elements.clone(), graph, scratch.teleport, ranks)?;
iterate_graph(
graph,
distribution,
elements,
config,
scratch.teleport,
scratch.visible_elements,
ranks,
scratch.next,
)
}
#[cfg(feature = "alloc")]
#[expect(
clippy::too_many_arguments,
reason = "PageRank workspace API keeps policy and reusable storage inputs explicit"
)]
pub fn pagerank_graph_with_workspace<G, D, I, S>(
graph: &G,
distribution: &D,
elements: I,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
ranks: &mut [S],
workspace: &mut PageRankWorkspace<G, S>,
) -> Result<PageRankReport<S>, PageRankError<S>>
where
G: ForwardGraph + ElementIndex,
D: OutgoingDistribution<G, S>,
I: Clone + IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
workspace.ensure_element_bound(graph.element_bound());
pagerank_graph_with_scratch(
graph,
distribution,
elements,
config,
personalization,
ranks,
workspace.as_scratch(),
)
}
#[cfg(feature = "alloc")]
#[expect(
clippy::too_many_arguments,
reason = "hypergraph PageRank entry point keeps hypergraph, distribution, state families, and output explicit"
)]
pub fn pagerank_hypergraph<H, D, IE, IR, S>(
hypergraph: &H,
distribution: &D,
elements: IE,
relations: IR,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
element_ranks: &mut [S],
relation_ranks: &mut [S],
) -> Result<PageRankReport<S>, PageRankError<S>>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
D: HypergraphOutgoingDistribution<H, S>,
IE: Clone + IntoIterator<Item = ElementId<H>>,
IR: Clone + IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
let e_bound = hypergraph.element_bound();
let r_bound = hypergraph.relation_bound();
let state_bound =
checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
let mut teleport = vec![S::ZERO; state_bound];
let mut next_elements = vec![S::ZERO; e_bound];
let mut next_relations = vec![S::ZERO; r_bound];
let mut visible_elements = vec![0; e_bound];
let mut visible_relations = vec![0; r_bound];
pagerank_hypergraph_with_scratch(
hypergraph,
distribution,
elements,
relations,
config,
personalization,
element_ranks,
relation_ranks,
HypergraphPageRankScratch::new(
&mut teleport,
&mut next_elements,
&mut next_relations,
&mut visible_elements,
&mut visible_relations,
),
)
}
#[expect(
clippy::too_many_arguments,
reason = "hypergraph PageRank scratch entry point keeps policy and storage inputs explicit"
)]
#[expect(
clippy::needless_pass_by_value,
reason = "hypergraph PageRank scratch API consumes a scratch handle and keeps policy inputs explicit"
)]
pub fn pagerank_hypergraph_with_scratch<H, D, IE, IR, S>(
hypergraph: &H,
distribution: &D,
elements: IE,
relations: IR,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
element_ranks: &mut [S],
relation_ranks: &mut [S],
scratch: HypergraphPageRankScratch<'_, S>,
) -> Result<PageRankReport<S>, PageRankError<S>>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
D: HypergraphOutgoingDistribution<H, S>,
IE: Clone + IntoIterator<Item = ElementId<H>>,
IR: Clone + IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
validate_config(config)?;
let e_bound = hypergraph.element_bound();
let r_bound = hypergraph.relation_bound();
let state_bound =
checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
ensure_scratch_len("teleport", scratch.teleport.len(), state_bound)?;
ensure_scratch_len("next_elements", scratch.next_elements.len(), e_bound)?;
ensure_scratch_len("next_relations", scratch.next_relations.len(), r_bound)?;
ensure_scratch_len("visible_elements", scratch.visible_elements.len(), e_bound)?;
ensure_scratch_len(
"visible_relations",
scratch.visible_relations.len(),
r_bound,
)?;
build_hyper_personalization_into(
hypergraph,
elements.clone(),
relations.clone(),
state_bound,
personalization,
scratch.teleport,
scratch.visible_elements,
scratch.visible_relations,
)?;
initialize_hyper_ranks(
hypergraph,
elements.clone(),
relations.clone(),
scratch.teleport,
element_ranks,
relation_ranks,
)?;
iterate_hypergraph(
hypergraph,
distribution,
elements,
relations,
config,
scratch.teleport,
scratch.visible_elements,
scratch.visible_relations,
element_ranks,
relation_ranks,
scratch.next_elements,
scratch.next_relations,
)
}
#[cfg(feature = "alloc")]
#[expect(
clippy::too_many_arguments,
reason = "hypergraph PageRank workspace entry point keeps policy and storage inputs explicit"
)]
pub fn pagerank_hypergraph_with_workspace<H, D, IE, IR, S>(
hypergraph: &H,
distribution: &D,
elements: IE,
relations: IR,
config: PageRankConfig<S>,
personalization: Option<&[S]>,
element_ranks: &mut [S],
relation_ranks: &mut [S],
workspace: &mut HypergraphPageRankWorkspace<H, S>,
) -> Result<PageRankReport<S>, PageRankError<S>>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
D: HypergraphOutgoingDistribution<H, S>,
IE: Clone + IntoIterator<Item = ElementId<H>>,
IR: Clone + IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
let e_bound = hypergraph.element_bound();
let r_bound = hypergraph.relation_bound();
let state_bound =
checked_state_bound::<S>(e_bound, r_bound, element_ranks.len(), relation_ranks.len())?;
workspace.ensure_bounds(e_bound, r_bound, state_bound);
pagerank_hypergraph_with_scratch(
hypergraph,
distribution,
elements,
relations,
config,
personalization,
element_ranks,
relation_ranks,
workspace.as_scratch(),
)
}
fn validate_config<S: PageRankScalar>(config: PageRankConfig<S>) -> Result<(), PageRankError<S>> {
if !config.damping.is_finite() || config.damping < S::ZERO || config.damping > S::ONE {
return Err(PageRankError::InvalidDamping {
damping: config.damping,
});
}
if !config.tolerance.is_finite() || config.tolerance < S::ZERO {
return Err(PageRankError::InvalidTolerance {
tolerance: config.tolerance,
});
}
if config.max_iterations == 0 {
return Err(PageRankError::InvalidMaxIterations);
}
Ok(())
}
const fn ensure_output_len<S>(actual: usize, required: usize) -> Result<(), PageRankError<S>> {
if actual < required {
Err(PageRankError::OutputTooShort { required, actual })
} else {
Ok(())
}
}
const fn ensure_scratch_len<S>(
name: &'static str,
actual: usize,
required: usize,
) -> Result<(), PageRankError<S>> {
if actual < required {
Err(PageRankError::ScratchTooShort {
name,
required,
actual,
})
} else {
Ok(())
}
}
fn checked_state_bound<S>(
e_bound: usize,
r_bound: usize,
element_output_len: usize,
relation_output_len: usize,
) -> Result<usize, PageRankError<S>> {
ensure_output_len(element_output_len, e_bound)?;
ensure_output_len(relation_output_len, r_bound)?;
e_bound
.checked_add(r_bound)
.ok_or_else(|| PageRankError::OutputTooShort {
required: usize::MAX,
actual: element_output_len.saturating_add(relation_output_len),
})
}
fn clear<S: PageRankScalar>(values: &mut [S], len: usize) {
for value in &mut values[..len] {
*value = S::ZERO;
}
}
fn clear_u8(values: &mut [u8], len: usize) {
for value in &mut values[..len] {
*value = 0;
}
}
fn mark_visible_element<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
if visible[index] != 0 {
return Err(PageRankError::DuplicateElement { index });
}
visible[index] = 1;
Ok(())
}
fn mark_visible_relation<S>(visible: &mut [u8], index: usize) -> Result<(), PageRankError<S>> {
if visible[index] != 0 {
return Err(PageRankError::DuplicateRelation { index });
}
visible[index] = 1;
Ok(())
}
fn is_visible(visible: &[u8], index: usize) -> bool {
visible.get(index).copied().unwrap_or(0) != 0
}
#[expect(
clippy::too_many_arguments,
reason = "personalization normalization keeps topology family bounds and caller buffers explicit"
)]
fn build_personalization_into<I, F, S>(
elements: I,
bound: usize,
personalization: Option<&[S]>,
index_of: F,
out: &mut [S],
visible: &mut [u8],
) -> Result<(), PageRankError<S>>
where
I: IntoIterator,
F: Fn(I::Item) -> usize,
S: PageRankScalar,
{
clear(out, bound);
clear_u8(visible, bound);
let mut count = 0_usize;
let mut sum = S::ZERO;
if let Some(input) = personalization {
if input.len() < bound {
return Err(PageRankError::PersonalizationTooShort {
required: bound,
actual: input.len(),
});
}
for element in elements {
let index = index_of(element);
check_index(index, bound)?;
mark_visible_element(visible, index)?;
let value = input[index];
check_personalization_value(index, value)?;
out[index] = value;
sum += value;
count += 1;
}
} else {
for element in elements {
let index = index_of(element);
check_index(index, bound)?;
mark_visible_element(visible, index)?;
out[index] = S::ONE;
sum += S::ONE;
count += 1;
}
}
normalize_personalization(out, count, sum)
}
enum PersonalizationSource<'a, S> {
Uniform,
FromInput(&'a [S]),
}
impl<S> PersonalizationSource<'_, S>
where
S: PageRankScalar,
{
fn value_at(&self, state_index: usize) -> Result<S, PageRankError<S>> {
match *self {
Self::Uniform => Ok(S::ONE),
Self::FromInput(input) => {
let value = input[state_index];
check_personalization_value(state_index, value)?;
Ok(value)
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "method threads separate element/relation iterables, scratch slices, and the state bound; bundling them obscures the borrow shape that callers explicitly construct"
)]
fn fill_hypergraph<H, IE, IR>(
&self,
hypergraph: &H,
elements: IE,
relations: IR,
state_bound: usize,
out: &mut [S],
visible_elements: &mut [u8],
visible_relations: &mut [u8],
) -> Result<(usize, S), PageRankError<S>>
where
H: ElementIndex + RelationIndex,
IE: IntoIterator<Item = ElementId<H>>,
IR: IntoIterator<Item = RelationId<H>>,
{
if let Self::FromInput(input) = *self
&& input.len() < state_bound
{
return Err(PageRankError::PersonalizationTooShort {
required: state_bound,
actual: input.len(),
});
}
let e_bound = hypergraph.element_bound();
let r_bound = hypergraph.relation_bound();
let mut count = 0_usize;
let mut sum = S::ZERO;
for element in elements {
let index = hypergraph.element_index(element);
check_index(index, e_bound)?;
mark_visible_element(visible_elements, index)?;
let value = self.value_at(index)?;
out[index] = value;
sum += value;
count += 1;
}
for relation in relations {
let index = hypergraph.relation_index(relation);
check_relation_index(index, r_bound)?;
mark_visible_relation(visible_relations, index)?;
let state = e_bound + index;
let value = self.value_at(state)?;
out[state] = value;
sum += value;
count += 1;
}
Ok((count, sum))
}
}
#[expect(
clippy::too_many_arguments,
reason = "helper threads separate element/relation state and caller scratch explicitly"
)]
fn build_hyper_personalization_into<H, IE, IR, S>(
hypergraph: &H,
elements: IE,
relations: IR,
state_bound: usize,
personalization: Option<&[S]>,
out: &mut [S],
visible_elements: &mut [u8],
visible_relations: &mut [u8],
) -> Result<(), PageRankError<S>>
where
H: ElementIndex + RelationIndex,
IE: IntoIterator<Item = ElementId<H>>,
IR: IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
clear(out, state_bound);
clear_u8(visible_elements, hypergraph.element_bound());
clear_u8(visible_relations, hypergraph.relation_bound());
let source = personalization.map_or(
PersonalizationSource::Uniform,
PersonalizationSource::FromInput,
);
let (count, sum) = source.fill_hypergraph(
hypergraph,
elements,
relations,
state_bound,
out,
visible_elements,
visible_relations,
)?;
normalize_personalization(out, count, sum)
}
fn normalize_personalization<S: PageRankScalar>(
out: &mut [S],
count: usize,
sum: S,
) -> Result<(), PageRankError<S>> {
if count == 0 {
return Err(PageRankError::EmptyState);
}
if sum <= S::ZERO {
return Err(PageRankError::ZeroPersonalization);
}
for value in out {
*value = *value / sum;
}
Ok(())
}
fn check_personalization_value<S: PageRankScalar>(
index: usize,
value: S,
) -> Result<(), PageRankError<S>> {
if !value.is_finite() || value < S::ZERO {
Err(PageRankError::InvalidPersonalization { index, value })
} else {
Ok(())
}
}
fn initialize_ranks<G, I, S>(
elements: I,
graph: &G,
teleport: &[S],
ranks: &mut [S],
) -> Result<(), PageRankError<S>>
where
G: ElementIndex,
I: IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
clear(ranks, graph.element_bound());
for element in elements {
let index = graph.element_index(element);
check_index(index, graph.element_bound())?;
ranks[index] = teleport[index];
}
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "initialization writes separate element and relation rank slices"
)]
fn initialize_hyper_ranks<H, IE, IR, S>(
hypergraph: &H,
elements: IE,
relations: IR,
teleport: &[S],
element_ranks: &mut [S],
relation_ranks: &mut [S],
) -> Result<(), PageRankError<S>>
where
H: ElementIndex + RelationIndex,
IE: IntoIterator<Item = ElementId<H>>,
IR: IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
clear(element_ranks, hypergraph.element_bound());
clear(relation_ranks, hypergraph.relation_bound());
for element in elements {
let index = hypergraph.element_index(element);
check_index(index, hypergraph.element_bound())?;
element_ranks[index] = teleport[index];
}
for relation in relations {
let index = hypergraph.relation_index(relation);
check_relation_index(index, hypergraph.relation_bound())?;
relation_ranks[index] = teleport[hypergraph.element_bound() + index];
}
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "graph iteration helper keeps distribution, scratch, and policy inputs explicit"
)]
#[expect(
clippy::needless_pass_by_value,
reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
)]
fn iterate_graph<G, D, I, S>(
graph: &G,
distribution: &D,
elements: I,
config: PageRankConfig<S>,
teleport: &[S],
visible: &[u8],
ranks: &mut [S],
next: &mut [S],
) -> Result<PageRankReport<S>, PageRankError<S>>
where
G: ForwardGraph + ElementIndex,
D: OutgoingDistribution<G, S>,
I: Clone + IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
let mut last_delta = S::INFINITY;
for iteration in 1..=config.max_iterations {
clear(next, graph.element_bound());
let mut dangling = S::ZERO;
for element in elements.clone() {
let index = checked_element_index(graph, element)?;
let rank = ranks[index];
dangling += distribution.distribute_outgoing(graph, element, rank, next, visible)?;
}
let delta = apply_graph_teleport(
graph,
elements.clone(),
config,
teleport,
dangling,
ranks,
next,
)?;
last_delta = delta;
if delta <= config.tolerance {
return Ok(PageRankReport {
iterations: iteration,
delta,
});
}
}
Err(PageRankError::NonConverged {
iterations: config.max_iterations,
delta: last_delta,
})
}
#[expect(
clippy::too_many_arguments,
reason = "hypergraph iteration helper keeps distribution, state families, scratch, and policy inputs explicit"
)]
#[expect(
clippy::needless_pass_by_value,
reason = "iteration helpers own cloneable iterator values and clone them each power iteration"
)]
fn iterate_hypergraph<H, D, IE, IR, S>(
hypergraph: &H,
distribution: &D,
elements: IE,
relations: IR,
config: PageRankConfig<S>,
teleport: &[S],
visible_elements: &[u8],
visible_relations: &[u8],
element_ranks: &mut [S],
relation_ranks: &mut [S],
next_elements: &mut [S],
next_relations: &mut [S],
) -> Result<PageRankReport<S>, PageRankError<S>>
where
H: DirectedVertexHyperedges
+ DirectedHyperedgeIncidences
+ IncidenceElement
+ ElementIndex
+ RelationIndex,
D: HypergraphOutgoingDistribution<H, S>,
IE: Clone + IntoIterator<Item = ElementId<H>>,
IR: Clone + IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
let mut last_delta = S::INFINITY;
for iteration in 1..=config.max_iterations {
clear(next_elements, hypergraph.element_bound());
clear(next_relations, hypergraph.relation_bound());
let mut dangling = S::ZERO;
for element in elements.clone() {
let index = checked_element_index(hypergraph, element)?;
let rank = element_ranks[index];
dangling += distribution.distribute_from_element(
hypergraph,
element,
rank,
next_relations,
visible_relations,
)?;
}
for relation in relations.clone() {
let index = checked_relation_index_for(hypergraph, relation)?;
let rank = relation_ranks[index];
dangling += distribution.distribute_from_relation(
hypergraph,
relation,
rank,
next_elements,
visible_elements,
)?;
}
let delta = apply_hyper_teleport(
hypergraph,
elements.clone(),
relations.clone(),
config,
teleport,
dangling,
element_ranks,
relation_ranks,
next_elements,
next_relations,
)?;
last_delta = delta;
if delta <= config.tolerance {
return Ok(PageRankReport {
iterations: iteration,
delta,
});
}
}
Err(PageRankError::NonConverged {
iterations: config.max_iterations,
delta: last_delta,
})
}
fn checked_element_index<G: ElementIndex, S>(
graph: &G,
element: ElementId<G>,
) -> Result<usize, PageRankError<S>> {
let index = graph.element_index(element);
check_index(index, graph.element_bound())?;
Ok(index)
}
const fn check_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
if index < bound {
Ok(())
} else {
Err(PageRankError::ElementIndexOutOfBounds { index, bound })
}
}
const fn check_relation_index<S>(index: usize, bound: usize) -> Result<(), PageRankError<S>> {
if index < bound {
Ok(())
} else {
Err(PageRankError::RelationIndexOutOfBounds { index, bound })
}
}
fn checked_relation_weight<G, W, S>(
graph: &G,
weights: &W,
relation: RelationId<G>,
) -> Result<S, PageRankError<S>>
where
G: RelationIndex,
W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
let index = graph.relation_index(relation);
check_relation_index(index, graph.relation_bound())?;
let value = weights.relation_weight(relation).into_pagerank_scalar();
if !value.is_finite() || value < S::ZERO {
Err(PageRankError::InvalidRelationWeight { index, value })
} else {
Ok(value)
}
}
fn checked_incidence_weight<H, W, S>(
hypergraph: &H,
weights: &W,
incidence: H::IncidenceId,
) -> Result<S, PageRankError<S>>
where
H: IncidenceIndex,
W: IncidenceWeight<
ElementId = H::ElementId,
RelationId = H::RelationId,
IncidenceId = H::IncidenceId,
>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
let index = hypergraph.incidence_index(incidence);
let bound = hypergraph.incidence_bound();
if index >= bound {
return Err(PageRankError::IncidenceIndexOutOfBounds { index, bound });
}
let value = weights.incidence_weight(incidence).into_pagerank_scalar();
if !value.is_finite() || value < S::ZERO {
Err(PageRankError::InvalidIncidenceWeight { index, value })
} else {
Ok(value)
}
}
fn outgoing_weight_total<G, W, S>(
graph: &G,
weights: &W,
element: ElementId<G>,
visible: &[u8],
) -> Result<S, PageRankError<S>>
where
G: ForwardGraph + ElementIndex + RelationIndex,
W: RelationWeight<ElementId = G::ElementId, RelationId = G::RelationId>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
let mut total = S::ZERO;
for edge in graph.outgoing_edges(element) {
let target = graph.target(edge);
let target_index = checked_element_index(graph, target)?;
if is_visible(visible, target_index) {
total += checked_relation_weight(graph, weights, edge)?;
}
}
Ok(total)
}
#[expect(
clippy::too_many_arguments,
reason = "teleport helper updates caller-provided rank and scratch slices"
)]
fn apply_graph_teleport<G, I, S>(
graph: &G,
elements: I,
config: PageRankConfig<S>,
teleport: &[S],
dangling: S,
ranks: &mut [S],
next: &mut [S],
) -> Result<S, PageRankError<S>>
where
G: ElementIndex,
I: IntoIterator<Item = ElementId<G>>,
S: PageRankScalar,
{
let mut delta = S::ZERO;
let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
for element in elements {
let index = checked_element_index(graph, element)?;
let value = (config.damping * next[index]) + (teleport_scale * teleport[index]);
delta += (value - ranks[index]).abs();
ranks[index] = value;
next[index] = value;
}
Ok(delta)
}
fn checked_relation_index_for<H: RelationIndex, S>(
hypergraph: &H,
relation: RelationId<H>,
) -> Result<usize, PageRankError<S>> {
let index = hypergraph.relation_index(relation);
check_relation_index(index, hypergraph.relation_bound())?;
Ok(index)
}
fn hyper_outgoing_relation_weight<H, W, S>(
hypergraph: &H,
weights: &W,
element: ElementId<H>,
visible_relations: &[u8],
) -> Result<S, PageRankError<S>>
where
H: DirectedVertexHyperedges + RelationIndex,
W: RelationWeight<ElementId = H::ElementId, RelationId = H::RelationId>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
let mut total = S::ZERO;
for relation in hypergraph.outgoing_hyperedges(element) {
let relation_index = checked_relation_index_for(hypergraph, relation)?;
if is_visible(visible_relations, relation_index) {
total += checked_relation_weight(hypergraph, weights, relation)?;
}
}
Ok(total)
}
fn hyper_target_incidence_weight<H, W, S>(
hypergraph: &H,
weights: &W,
relation: RelationId<H>,
visible_elements: &[u8],
) -> Result<S, PageRankError<S>>
where
H: DirectedHyperedgeIncidences + IncidenceElement + ElementIndex + IncidenceIndex,
W: IncidenceWeight<
ElementId = H::ElementId,
RelationId = H::RelationId,
IncidenceId = H::IncidenceId,
>,
W::Weight: IntoPageRankScalar<S>,
S: PageRankScalar,
{
let mut total = S::ZERO;
for incidence in hypergraph.target_incidences(relation) {
let target = hypergraph.incidence_element(incidence);
let target_index = checked_element_index(hypergraph, target)?;
if is_visible(visible_elements, target_index) {
total += checked_incidence_weight(hypergraph, weights, incidence)?;
}
}
Ok(total)
}
#[expect(
clippy::too_many_arguments,
reason = "hypergraph teleport updates separate element and relation slices"
)]
fn apply_hyper_teleport<H, IE, IR, S>(
hypergraph: &H,
elements: IE,
relations: IR,
config: PageRankConfig<S>,
teleport: &[S],
dangling: S,
element_ranks: &mut [S],
relation_ranks: &mut [S],
next_elements: &mut [S],
next_relations: &mut [S],
) -> Result<S, PageRankError<S>>
where
H: ElementIndex + RelationIndex,
IE: IntoIterator<Item = ElementId<H>>,
IR: IntoIterator<Item = RelationId<H>>,
S: PageRankScalar,
{
let mut delta = S::ZERO;
let e_bound = hypergraph.element_bound();
let teleport_scale = (S::ONE - config.damping) + (config.damping * dangling);
for element in elements {
let index = checked_element_index(hypergraph, element)?;
let value = (config.damping * next_elements[index]) + (teleport_scale * teleport[index]);
delta += (value - element_ranks[index]).abs();
element_ranks[index] = value;
next_elements[index] = value;
}
for relation in relations {
let index = checked_relation_index_for(hypergraph, relation)?;
let state = e_bound + index;
let value = (config.damping * next_relations[index]) + (teleport_scale * teleport[state]);
delta += (value - relation_ranks[index]).abs();
relation_ranks[index] = value;
next_relations[index] = value;
}
Ok(delta)
}