pub struct Subgraph { /* private fields */ }Expand description
A transient, in-memory graph loaded from links_current.
The maps are BTreeMap, not HashMap, so iteration follows node id order.
Every algorithm in super::algorithms inherits its determinism from that
choice, and Louvain in particular returns a different partition under a
randomised iteration order.
§Closure
Every id appearing in out_adj or in_adj — as a key or as an
EdgeRef::node — is a key of nodes. drop_dangling_adjacency — private,
and named here because it is the sole establisher — establishes it and
Subgraph::is_closed checks it; every algorithm in
super::algorithms is written assuming it and none of them re-checks.
It did not hold before Wave 1 (defect Z), and the way it failed is the reason
it is now stated on the type rather than left to the loader. Adjacency comes
from links_current, which carries edges to retired concepts; hydrate
filters retired = 0. So a retired neighbour left an EdgeRef pointing at
an id with no NodeData, and the five algorithms each met that differently:
louvain panicked on the missing map entry, scc emitted the absent node as
a phantom component of its own, k_core counted a degree of 2 where one edge
was in the graph, and dijkstra returned a finite distance to a node the
caller could not then look up. Four handlings of one violated invariant, none
of them chosen — and the panic was the least damaging, because the other
three answer.
Dangling entries are dropped rather than admitted with a tombstone node. A retired concept is not visible (§4.1), analytics over a graph is analytics over what is visible, and the alternative pushes a three-state node onto every present and future algorithm to preserve edges whose endpoint the caller is not entitled to read. Retirement is the supported path — concepts are never deleted (D-022) — so this is ordinary use, not a corner.
§Why the fields are private (0.8.0, B1, D-114)
They were pub through 0.7.0, and the three maps were the crate’s most
widely read data structure. That made every detail of the representation
part of the public API — the BTreeMap, the String keys, the fact that
adjacency is stored as two maps at all — none of which was ever a promise
anyone intended to make.
The immediate reason is D-087: interning the keys to u32 cannot be done
at all while EdgeRef::node is a public String. The break is taken once, here, with the representation
unchanged, so that anything depending on the old shape fails against code
that still behaves identically.
Accessors return borrowed views, so nothing here costs an allocation that field access did not.
Implementations§
Source§impl Subgraph
impl Subgraph
Sourcepub fn contains_node(&self, id: &str) -> bool
pub fn contains_node(&self, id: &str) -> bool
Whether id is a hydrated node of this graph.
By the closure invariant this is also the answer to “may an algorithm look this id up”, which is why every algorithm asks it rather than probing adjacency.
Sourcepub fn node(&self, id: &str) -> Option<&NodeData>
pub fn node(&self, id: &str) -> Option<&NodeData>
The attributes of id, or None when it is not in the graph.
Sourcepub fn node_ids(&self) -> impl ExactSizeIterator<Item = &str> + '_
pub fn node_ids(&self) -> impl ExactSizeIterator<Item = &str> + '_
Node ids in ascending order.
The order is BTreeMap’s and is load-bearing rather than incidental:
Louvain breaks ties by first-seen community and returns a different
partition under a randomised order.
pub fn node_count(&self) -> usize
Sourcepub fn nodes(&self) -> impl ExactSizeIterator<Item = (&str, &NodeData)> + '_
pub fn nodes(&self) -> impl ExactSizeIterator<Item = (&str, &NodeData)> + '_
Every node with its attributes, in id order.
Sourcepub fn out_adjacency(&self) -> impl Iterator<Item = (&str, &[EdgeRef])> + '_
pub fn out_adjacency(&self) -> impl Iterator<Item = (&str, &[EdgeRef])> + '_
The outgoing index: each node that has outgoing edges, with them.
For one node prefer Self::out_edges. This exists for callers that
must walk the whole index — the Python to_dict, and the diagnostics.
Sourcepub fn in_adjacency(&self) -> impl Iterator<Item = (&str, &[EdgeRef])> + '_
pub fn in_adjacency(&self) -> impl Iterator<Item = (&str, &[EdgeRef])> + '_
The incoming index. See Self::out_adjacency.
Sourcepub fn insert_node(
&mut self,
id: impl Into<String>,
data: NodeData,
) -> Option<NodeData>
pub fn insert_node( &mut self, id: impl Into<String>, data: NodeData, ) -> Option<NodeData>
Add or replace a node, returning what was there before.
Public so that callers who build a graph by hand — the test fixtures, the diagnostics — can still do so now the fields are private. It does not establish the closure invariant on its own: adjacency naming an id never inserted is still dangling, exactly as before.
Sourcepub fn out_edges(&self, node: &str) -> &[EdgeRef]
pub fn out_edges(&self, node: &str) -> &[EdgeRef]
Outgoing edges of node, empty when it has none or is absent.
Sourcepub fn in_edges(&self, node: &str) -> &[EdgeRef]
pub fn in_edges(&self, node: &str) -> &[EdgeRef]
Incoming edges of node, empty when it has none or is absent.
Sourcepub fn degree(&self, node: &str) -> usize
pub fn degree(&self, node: &str) -> usize
Undirected edge count incident to node, counting parallel edges once
each and a self-loop twice.
Sourcepub fn weighted_degree(&self, node: &str) -> f64
pub fn weighted_degree(&self, node: &str) -> f64
Undirected weight incident to node. Summed over both directions, so
summing this over all nodes gives 2 * total_weight.
Sourcepub fn total_weight(&self) -> f64
pub fn total_weight(&self) -> f64
Total edge weight, each edge counted once — the m of the modularity
formulas.
pub fn edge_count(&self) -> usize
Sourcepub fn is_closed(&self) -> bool
pub fn is_closed(&self) -> bool
Whether the closure invariant holds. Used by tests and debug_asserts.
Cheap enough to call in a test and O(V + E), so not on any hot path.
The debug_asserts were only a claim until 0.10.0 (W4.8). This
sentence shipped in 0.6.0 and none existed in src/; they now sit at the
entry of dijkstra, astar, scc, k_core and louvain
(algorithms::CLOSURE). Writing them was the fix rather than weakening
the sentence: the type docs above say every algorithm assumes closure and
none re-checks it, and an assert is the auditable form of that.
Sourcepub fn add_edge(
&mut self,
source: &str,
target: &str,
edge_type: &str,
weight: f64,
valid_from: &str,
valid_to: &str,
) -> usize
pub fn add_edge( &mut self, source: &str, target: &str, edge_type: &str, weight: f64, valid_from: &str, valid_to: &str, ) -> usize
Record an edge in both directions.
Both indices are maintained together because every undirected quantity
here — degree, k-core peeling, Louvain’s k_i — reads them as a pair. An
in_adj that lags out_adj would not fail loudly; it would return a
plausible wrong number.
Public since 0.8.0. The callers that used to push into both maps by
hand cannot now the fields are private, and routing them through the one
function that maintains the pair is the point rather than a consolation:
hand-written adjacency was two chances to get the reverse edge wrong,
and every such call site was already doing the back.node = source
dance itself. edge.node is expected to be target; the reverse entry
is derived here.
Returns the bytes this edge added to Self::estimated_bytes — the two
fixed-size entries plus whatever strings were genuinely new. The loader
charges its budget with it, and it is O(1) by construction.
Sourcepub fn estimated_bytes(&self) -> usize
pub fn estimated_bytes(&self) -> usize
Estimated heap footprint (D-007).
Deliberately an estimate of the payload, not a precise size_of walk:
the budget exists to stop a dense neighbourhood exhausting memory, and a
figure that tracks string bytes and per-item overhead is accurate enough
for that.
O(V + E), and therefore not for use inside a loop over rows. The loader used to call this per row, which made loading O(E²): 500 edges in 26 ms, 1,000 in 76 ms, 2,000 in 231 ms — time tripling for each doubling. The byte budget is what bounds a load, and the budget check was the thing that did not scale (D-047).
Sourcepub async fn write_back_annotations(
&self,
db: &Database,
label: &str,
values: &BTreeMap<String, String>,
) -> Result<usize>
pub async fn write_back_annotations( &self, db: &Database, label: &str, values: &BTreeMap<String, String>, ) -> Result<usize>
Write one derived result per node under label (§5.4, D-041).
Goes through Database::write_analytics_annotations, which chunks at
crate::connection::chunk_rows::ANNOTATIONS and sends on the
low-priority channel,
so a community assignment over a large subgraph cannot starve interactive
writes.
Rows land in analytics_annotations, which carries no log trigger.
Before 0.5.4 this method built a ConceptUpsert per node and put the
value in content, so writing back a partition overwrote every
annotated concept’s document text — and, because the write went through
the ledger, recorded each rerun of the algorithm as a fresh version of a
world that had not changed. The old doc comment defended that as “a
normal bitemporal write,” which was true of the mechanism and false of
the intent: it is the right mechanism for a domain fact, and a community
label is not one.
values is keyed by node id; nodes absent from it are not annotated.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Subgraph
impl RefUnwindSafe for Subgraph
impl Send for Subgraph
impl Sync for Subgraph
impl Unpin for Subgraph
impl UnsafeUnpin for Subgraph
impl UnwindSafe for Subgraph
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request