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:
parent
9cb9cb17e0
commit
0232790b4e
|
|
@ -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| {
|
||||||
.iter()
|
positions
|
||||||
.any(|edge| edge.from == from && edge.to == to)
|
.iter()
|
||||||
|
.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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue