1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! The integer-indexed view the algorithms actually run on (0.13.28, W10.3b,
//! [D-201]).
//!
//! [`Subgraph`] is keyed by `String` and stays that way: the maps, their order
//! and the `BTreeMap<String, _>` return types are the public surface, and
//! [D-063] makes node order a structural property rather than a sort applied at
//! the end. What changes here is what happens *between* those two boundaries.
//!
//! # Why this is in the crate and not in the caller
//!
//! §2.5 of the 0.12.0 review proposed exactly this — "build a dense
//! `Vec`-indexed view once, run on integers, and translate back at the
//! boundary" — and [D-200] measured it built at the boundary, where it is
//! 1.8×–2.1× on `louvain` and **a loss** on `dijkstra`. The reason is the
//! build: from outside, mapping an edge's far end to an index costs a
//! `BTreeMap<&str, _>` lookup **per edge endpoint**, which is the very cost
//! being removed, paid once for the graph instead of once per sweep. One pass
//! never earns that back.
//!
//! In here it is not that. [D-115] already interned every string an
//! [`EdgeRef`](super::EdgeRef) carries, so an edge's far end is *already* a
//! `u32` — and mapping the pool onto dense indices costs one string lookup
//! **per node**, not per endpoint. The build is therefore O(V log V) in string
//! comparisons and O(V + E) in integer work, and the per-edge term has no
//! strings in it at all.
//!
//! # Why CSR and not `Vec<Vec<_>>`
//!
//! The first version of this was a `Vec<Vec<(u32, f64)>>` per direction, which
//! is the obvious shape and was measured to be the wrong one: two heap
//! allocations per node, 98,304 of them at the byte budget's ceiling, and the
//! build became the dominant term for every algorithm that makes a single pass.
//! `k_core` came out **1.8× slower than the string-keyed version it replaced**
//! — its own work is one degree count per node, so it was paying an O(V)
//! allocation storm to avoid an O(V log V) lookup.
//!
//! Flat arrays with an offset table fix that: two allocations per direction for
//! the whole graph. §2.5 said "CSR adjacency" and meant it.
//!
//! # What it does not change
//!
//! Dense indices are assigned in [`Subgraph::node_ids`] order, which is
//! `nodes`' `BTreeMap` order. So `u < v` **iff** `ids[u] < ids[v]`, and every
//! tie the algorithms break by node id can be broken by index instead without
//! moving an answer: heap entries at equal distance, `scc`'s sorted components
//! and their sorted order, Louvain's lowest-community-index rule. That
//! equivalence is why the rewrite is behaviour-preserving by construction
//! rather than only by test — and
//! `the_interior_may_change_but_these_answers_may_not` is the test that holds
//! it anyway.
//!
//! # Who does not use this
//!
//! `astar`, and only `astar` ([D-202]). Everything above is amortised by
//! settling every node, and `astar` exists precisely to avoid doing that: on a
//! one-hop goal it settles six nodes and would pay a build over 49,152. A view
//! whose cost is proportional to the graph cannot be free for a query that is
//! deliberately not. Adding a seventh algorithm that stops early means asking
//! the same question again, and `subgraph_interior`'s astar section is the
//! shape the answer takes.
//!
//! [D-063]: ../../docs/architecture/s13-decision-register.md#d-063
//! [D-115]: ../../docs/architecture/s13-decision-register.md#d-115
//! [D-200]: ../../docs/architecture/s13-decision-register.md#d-200
//! [D-201]: ../../docs/architecture/s13-decision-register.md#d-201
//! [D-202]: ../../docs/architecture/s13-decision-register.md#d-202
use BTreeMap;
use Subgraph;
/// A dense, integer-indexed view of one [`Subgraph`], borrowed from it.
///
/// Built per call rather than cached on the graph. A cache would have to be
/// invalidated by [`Subgraph::add_edge`] and [`Subgraph::insert_node`], which
/// are public — and, more decisively, it would roughly double the retained
/// footprint of the one structure in this crate with an explicit **byte
/// budget** ([D-007], [D-047]). Bounding that footprint is what
/// `load_subgraph` refuses loads to protect; silently doubling it to save a
/// build measured in tens of milliseconds is the wrong side of that trade.
///
/// [D-007]: ../../docs/architecture/s13-decision-register.md#d-007
/// [D-047]: ../../docs/architecture/s13-decision-register.md#d-047
pub
/// `pool_to_dense` entry for a pooled string that is not a hydrated node id —
/// an edge type, a timestamp, or a node the loader filtered out.
const NOT_A_NODE: u32 = u32MAX;