Skip to main content

Subgraph

Struct Subgraph 

Source
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

Source

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.

Source

pub fn node(&self, id: &str) -> Option<&NodeData>

The attributes of id, or None when it is not in the graph.

Source

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.

Source

pub fn node_count(&self) -> usize

Source

pub fn nodes(&self) -> impl ExactSizeIterator<Item = (&str, &NodeData)> + '_

Every node with its attributes, in id order.

Source

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.

Source

pub fn in_adjacency(&self) -> impl Iterator<Item = (&str, &[EdgeRef])> + '_

The incoming index. See Self::out_adjacency.

Source

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.

Source

pub fn out_edges(&self, node: &str) -> &[EdgeRef]

Outgoing edges of node, empty when it has none or is absent.

Source

pub fn in_edges(&self, node: &str) -> &[EdgeRef]

Incoming edges of node, empty when it has none or is absent.

Source

pub fn degree(&self, node: &str) -> usize

Undirected edge count incident to node, counting parallel edges once each and a self-loop twice.

Source

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.

Source

pub fn total_weight(&self) -> f64

Total edge weight, each edge counted once — the m of the modularity formulas.

Source

pub fn edge_count(&self) -> usize

Source

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.

Source

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.

Source

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).

Source

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§

Source§

impl Clone for Subgraph

Source§

fn clone(&self) -> Subgraph

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Subgraph

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Subgraph

Source§

fn default() -> Subgraph

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more