weavatrix_graph/algo/network/centrality/
hits.rs1#![allow(clippy::cast_precision_loss)]
2
3use super::math::square_root;
4use crate::{GraphError, IndexGraphView, Result, String, Vec};
5
6pub type HitsScores<Node> = Vec<(Node, f64)>;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct Hits<Node> {
12 hubs: HitsScores<Node>,
13 authorities: HitsScores<Node>,
14 iterations: usize,
15 converged: bool,
16}
17
18impl<Node> Hits<Node> {
19 #[must_use]
21 pub fn hubs(&self) -> &[(Node, f64)] {
22 &self.hubs
23 }
24
25 #[must_use]
27 pub fn authorities(&self) -> &[(Node, f64)] {
28 &self.authorities
29 }
30
31 #[must_use]
33 pub fn iterations(&self) -> usize {
34 self.iterations
35 }
36
37 #[must_use]
39 pub fn converged(&self) -> bool {
40 self.converged
41 }
42
43 #[must_use]
45 pub fn into_scores(self) -> (HitsScores<Node>, HitsScores<Node>) {
46 (self.hubs, self.authorities)
47 }
48}
49
50impl<Node> Hits<Node>
51where
52 Node: Copy + Eq,
53{
54 #[must_use]
56 pub fn hub(&self, node: Node) -> Option<f64> {
57 score(&self.hubs, node)
58 }
59
60 #[must_use]
62 pub fn authority(&self, node: Node) -> Option<f64> {
63 score(&self.authorities, node)
64 }
65}
66
67pub fn hits<G>(graph: &G, max_iterations: usize, tolerance: f64) -> Result<Hits<G::Node>>
77where
78 G: IndexGraphView,
79{
80 hits_filtered(graph, max_iterations, tolerance, |_| true)
81}
82
83pub fn hits_filtered<G, F>(
92 graph: &G,
93 max_iterations: usize,
94 tolerance: f64,
95 allows_edge: F,
96) -> Result<Hits<G::Node>>
97where
98 G: IndexGraphView,
99 F: Fn(G::Edge) -> bool,
100{
101 validate(max_iterations, tolerance)?;
102 let mut nodes = graph.node_indices().collect::<Vec<_>>();
103 nodes.sort_unstable_by_key(|node| G::node_slot(*node));
104 let slots = nodes
105 .iter()
106 .map(|node| G::node_slot(*node))
107 .collect::<Vec<_>>();
108 let dense = slots
109 .iter()
110 .enumerate()
111 .all(|(expected, &slot)| expected == slot);
112 let edges = accepted_edges(graph, allows_edge);
113 if nodes.is_empty() || edges.is_empty() {
114 return Ok(zero_result(nodes));
115 }
116 let initial = 1.0 / square_root(nodes.len() as f64);
117 let mut hubs = vec![initial; graph.node_bound()];
118 let mut authorities = vec![initial; graph.node_bound()];
119 let mut next_hubs = vec![0.0; graph.node_bound()];
120 let mut next_authorities = vec![0.0; graph.node_bound()];
121 for iteration in 1..=max_iterations {
122 next_authorities.fill(0.0);
123 for &(source, target) in &edges {
124 next_authorities[target] += hubs[source];
125 }
126 normalize(&mut next_authorities, &slots, dense);
127 next_hubs.fill(0.0);
128 for &(source, target) in &edges {
129 next_hubs[source] += next_authorities[target];
130 }
131 normalize(&mut next_hubs, &slots, dense);
132 if vectors_converged(
133 &hubs,
134 &next_hubs,
135 &authorities,
136 &next_authorities,
137 &slots,
138 dense,
139 tolerance,
140 ) {
141 return Ok(result(
142 &nodes,
143 &slots,
144 &next_hubs,
145 &next_authorities,
146 iteration,
147 true,
148 ));
149 }
150 core::mem::swap(&mut hubs, &mut next_hubs);
151 core::mem::swap(&mut authorities, &mut next_authorities);
152 }
153 Ok(result(
154 &nodes,
155 &slots,
156 &hubs,
157 &authorities,
158 max_iterations,
159 false,
160 ))
161}
162
163fn accepted_edges<G, F>(graph: &G, allows_edge: F) -> Vec<(usize, usize)>
164where
165 G: IndexGraphView,
166 F: Fn(G::Edge) -> bool,
167{
168 let mut edges = graph
169 .edge_references()
170 .filter(|(edge, _)| allows_edge(*edge))
171 .filter_map(|(_, endpoints)| {
172 let source = G::node_slot(endpoints.source());
173 let target = G::node_slot(endpoints.target());
174 (source != target).then_some((source, target))
175 })
176 .collect::<Vec<_>>();
177 if !edges.is_sorted() {
178 edges.sort_unstable();
179 }
180 edges.dedup();
181 edges
182}
183
184fn normalize(values: &mut [f64], slots: &[usize], dense: bool) {
185 let norm = if dense {
186 square_root(
187 values[..slots.len()]
188 .iter()
189 .map(|value| value * value)
190 .sum(),
191 )
192 } else {
193 square_root(slots.iter().map(|&slot| values[slot] * values[slot]).sum())
194 };
195 if norm > 0.0 {
196 if dense {
197 for value in &mut values[..slots.len()] {
198 *value /= norm;
199 }
200 } else {
201 for &slot in slots {
202 values[slot] /= norm;
203 }
204 }
205 }
206}
207
208fn vectors_converged(
209 hubs: &[f64],
210 next_hubs: &[f64],
211 authorities: &[f64],
212 next_authorities: &[f64],
213 slots: &[usize],
214 dense: bool,
215 tolerance: f64,
216) -> bool {
217 if dense {
218 hubs.iter()
219 .zip(next_hubs)
220 .zip(authorities.iter().zip(next_authorities))
221 .take(slots.len())
222 .all(|((hub, next_hub), (authority, next_authority))| {
223 (hub - next_hub).abs() <= tolerance
224 && (authority - next_authority).abs() <= tolerance
225 })
226 } else {
227 slots.iter().all(|&slot| {
228 (hubs[slot] - next_hubs[slot]).abs() <= tolerance
229 && (authorities[slot] - next_authorities[slot]).abs() <= tolerance
230 })
231 }
232}
233
234fn result<Node>(
235 nodes: &[Node],
236 slots: &[usize],
237 hubs: &[f64],
238 authorities: &[f64],
239 iterations: usize,
240 converged: bool,
241) -> Hits<Node>
242where
243 Node: Copy,
244{
245 Hits {
246 hubs: nodes
247 .iter()
248 .zip(slots)
249 .map(|(&node, &slot)| (node, hubs[slot]))
250 .collect(),
251 authorities: nodes
252 .iter()
253 .zip(slots)
254 .map(|(&node, &slot)| (node, authorities[slot]))
255 .collect(),
256 iterations,
257 converged,
258 }
259}
260
261fn zero_result<Node>(nodes: Vec<Node>) -> Hits<Node>
262where
263 Node: Copy,
264{
265 Hits {
266 hubs: nodes.iter().map(|&node| (node, 0.0)).collect(),
267 authorities: nodes.into_iter().map(|node| (node, 0.0)).collect(),
268 iterations: 0,
269 converged: true,
270 }
271}
272
273fn score<Node>(scores: &[(Node, f64)], node: Node) -> Option<f64>
274where
275 Node: Copy + Eq,
276{
277 scores
278 .iter()
279 .find_map(|(candidate, value)| (*candidate == node).then_some(*value))
280}
281
282fn validate(max_iterations: usize, tolerance: f64) -> Result<()> {
283 if max_iterations == 0 || !tolerance.is_finite() || tolerance <= 0.0 {
284 return Err(GraphError::InvalidAlgorithmParameter {
285 algorithm: "hits",
286 parameter: "max_iterations/tolerance",
287 value: String::from("must be finite, positive, and non-zero"),
288 });
289 }
290 Ok(())
291}