topology: index the outgoing edges

Asking whether an edge already exists scanned the whole edge list, so
building a graph of E edges cost O(E^2). A visibility sweep adds a quadratic
number of edges, so the whole construction was cubic: 600 satellites took
twelve seconds. An index of outgoing edge positions makes the membership
test constant time, and the same index answers neighbours and out-degree
without a scan. Removal rebuilds the index in one pass, since it renumbers
every later position. Six hundred satellites now take 130 ms, a factor of
ninety five.
This commit is contained in:
ApsisRS contributors 2026-09-30 18:29:18 +08:00
parent 9e2f820257
commit 4cf0d237cb
1 changed files with 57 additions and 19 deletions

View File

@ -48,14 +48,30 @@ impl Edge {
pub struct Topology { pub struct Topology {
node_count: usize, node_count: usize,
edges: Vec<Edge>, edges: Vec<Edge>,
/// The positions in `edges` of the edges leaving each node.
///
/// The index is what makes membership a constant time test. Without it,
/// asking whether an edge already exists scans the whole edge list, and
/// building a graph of `E` edges costs `O(E^2)`, which turns the
/// visibility sweep of a constellation, quadratic in the number of
/// satellites, into cubic work. The index holds positions rather than
/// destinations so that `remove_edge`, which invalidates positions, can
/// repair it in one pass.
outgoing: Vec<Vec<usize>>,
} }
impl Topology { impl Topology {
/// Creates an empty topology with the given number of nodes. /// Creates an empty topology with the given number of nodes.
pub const fn new(node_count: usize) -> Self { ///
/// This is not a `const fn` because it sizes the adjacency index, which
/// allocates.
pub fn new(node_count: usize) -> Self {
let mut outgoing = Vec::new();
outgoing.resize_with(node_count, Vec::new);
Self { Self {
node_count, node_count,
edges: Vec::new(), edges: Vec::new(),
outgoing,
} }
} }
@ -98,6 +114,7 @@ impl Topology {
return Err(TopologyError::new(TopologyErrorKind::DuplicateEdge) return Err(TopologyError::new(TopologyErrorKind::DuplicateEdge)
.with_detail(format!("edge {from} to {to}"))); .with_detail(format!("edge {from} to {to}")));
} }
self.outgoing[from].push(self.edges.len());
self.edges.push(Edge::new(from, to, cost)); self.edges.push(Edge::new(from, to, cost));
Ok(()) Ok(())
} }
@ -116,8 +133,7 @@ impl Topology {
self.add_edge(first, second, cost)?; self.add_edge(first, second, cost)?;
if let Err(error) = self.add_edge(second, first, cost) { if let Err(error) = self.add_edge(second, first, cost) {
// Remove the first direction so the topology is unchanged. // Remove the first direction so the topology is unchanged.
self.edges self.remove_edge_unchecked(first, second);
.retain(|edge| !(edge.from == first && edge.to == second));
return Err(error); return Err(error);
} }
Ok(()) Ok(())
@ -125,50 +141,72 @@ impl Topology {
/// Returns `true` when an edge exists from one node to another. /// Returns `true` when an edge exists from one node to another.
pub fn has_edge(&self, from: usize, to: usize) -> bool { pub fn has_edge(&self, from: usize, to: usize) -> bool {
self.edges self.outgoing.get(from).is_some_and(|positions| {
positions
.iter() .iter()
.any(|edge| edge.from == from && edge.to == to) .any(|&position| self.edges[position].to == to)
})
} }
/// Returns the cost of the edge between two nodes. /// Returns the cost of the edge between two nodes.
/// ///
/// Returns `None` when there is no such edge. /// Returns `None` when there is no such edge.
pub fn cost(&self, from: usize, to: usize) -> Option<f64> { pub fn cost(&self, from: usize, to: usize) -> Option<f64> {
self.edges let positions = self.outgoing.get(from)?;
positions
.iter() .iter()
.find(|edge| edge.from == from && edge.to == to) .find(|&&position| self.edges[position].to == to)
.map(|edge| edge.cost) .map(|&position| self.edges[position].cost)
} }
/// Removes the edge between two nodes. /// Removes the edge between two nodes.
/// ///
/// Returns an error when there is no such edge. /// Returns an error when there is no such edge.
pub fn remove_edge(&mut self, from: usize, to: usize) -> Result<(), TopologyError> { pub fn remove_edge(&mut self, from: usize, to: usize) -> Result<(), TopologyError> {
let before = self.edges.len(); if !self.has_edge(from, to) {
self.edges
.retain(|edge| !(edge.from == from && edge.to == to));
if self.edges.len() == before {
return Err(TopologyError::new(TopologyErrorKind::NoSuchEdge) return Err(TopologyError::new(TopologyErrorKind::NoSuchEdge)
.with_detail(format!("edge {from} to {to}"))); .with_detail(format!("edge {from} to {to}")));
} }
self.remove_edge_unchecked(from, to);
Ok(()) Ok(())
} }
/// Removes an edge the caller has already established exists.
///
/// Removing an edge renumbers every position after it, so the index is
/// rebuilt from the surviving edges in one pass. Removal is rarer than
/// insertion, so paying for a rebuild here is cheaper than keeping the
/// index correct incrementally.
fn remove_edge_unchecked(&mut self, from: usize, to: usize) {
self.edges
.retain(|edge| !(edge.from == from && edge.to == to));
self.rebuild_index();
}
/// Rebuilds the adjacency index from the current edge list.
fn rebuild_index(&mut self) {
for positions in &mut self.outgoing {
positions.clear();
}
for (position, edge) in self.edges.iter().enumerate() {
self.outgoing[edge.from].push(position);
}
}
/// Returns the indices of the nodes reachable from a node in one hop. /// Returns the indices of the nodes reachable from a node in one hop.
/// ///
/// The result is in the order the edges were added, so it is /// The result is in the order the edges were added, so it is
/// deterministic for a given construction sequence. /// deterministic for a given construction sequence.
pub fn neighbours(&self, node: usize) -> Vec<usize> { pub fn neighbours(&self, node: usize) -> Vec<usize> {
self.edges self.outgoing
.iter() .get(node)
.filter(|edge| edge.from == node) .map(|positions| positions.iter().map(|&p| self.edges[p].to).collect())
.map(|edge| edge.to) .unwrap_or_default()
.collect()
} }
/// Returns the number of edges leaving a node. /// Returns the number of edges leaving a node.
pub fn out_degree(&self, node: usize) -> usize { pub fn out_degree(&self, node: usize) -> usize {
self.edges.iter().filter(|edge| edge.from == node).count() self.outgoing.get(node).map_or(0, Vec::len)
} }
/// Returns the number of edges arriving at a node. /// Returns the number of edges arriving at a node.