From b124e58b380fc57fa252d6cf8d1c9a3d5d234f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 12 Oct 2026 21:33:49 +0200 Subject: [PATCH] Change GraphTopologyConversion::convert return value to include the new GraphIsomorphism This allows to convert maps for the converted graph. --- src/lib.rs | 4 +- src/maps.rs | 29 ++++ src/models/append_graph.rs | 14 +- src/models/frozen_graph.rs | 30 ++-- src/models/graph.rs | 15 +- src/testing/fixtures.rs | 44 ++--- ...ph_topology_addition_conversion_testing.rs | 4 +- .../graph_topology_conversion_testing.rs | 6 +- src/traits.rs | 161 ++++++++++++++++-- 9 files changed, 228 insertions(+), 79 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8a0b27d..843225d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,8 +146,8 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { pub use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, - Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, + GraphTopologyDeletion, Incidence, IncidenceCursor, }; } diff --git a/src/maps.rs b/src/maps.rs index 82a693c..bb45b09 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -94,6 +94,35 @@ impl ElementMap { to_index: self.to_index, } } + + /// Creates a new `ElementMap` indexed by elements of type `F`, such that for each element *e* + /// of this map that `mapping` maps to some *f*, the new map contains at *f* a clone of the + /// value this map contains at *e*. + /// + /// A slot of `mapping` counts as unmapped if it holds the default value of `mapping`, which + /// must therefore never be a valid element of `F`. Unmapped slots are skipped. Slots of the new + /// map that no element is mapped to, and slots of this map beyond the length of `mapping` or + /// vice versa, are left at this map's default value. The `template` is only used to determine + /// the length and the index conversion function of the new map. + /// + /// Iterates the raw slots of both maps, so it neither needs the graph nor the element handles. + pub(crate) fn remap( + &self, + mapping: &ElementMap, + template: &ElementMap, + ) -> ElementMap { + let mut result = ElementMap { + data: vec![self.default.clone(); template.data.len()], + default: self.default.clone(), + to_index: template.to_index, + }; + for (value, &f) in self.data.iter().zip(&mapping.data) { + if f != mapping.default { + result.data[(template.to_index)(f)] = value.clone(); + } + } + result + } } impl Index for ElementMap { diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 96d36d1..27c8742 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -5,7 +5,8 @@ use std::num::NonZeroUsize; use crate::maps::ElementMap; use crate::models; use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence, + IncidenceCursor, }; /// An opaque handle identifying a vertex in an [`AppendGraph`]. @@ -261,17 +262,12 @@ impl GraphTopologyAddition for AppendGraph { } impl GraphTopologyConversion for AppendGraph { - fn convert( - source: &H, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { let mut edge_map = source.edge_map(Edge(NonZeroUsize::MAX)); let (result, vertex_map) = models::convert_to_addition(source, Vertex(usize::MAX), |e, new_e| edge_map[e] = new_e); - (result, vertex_map, edge_map) + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 4db2439..9167f69 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -3,7 +3,9 @@ use std::ops::IndexMut; use crate::maps::ElementMap; -use crate::traits::{GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor}; +use crate::traits::{ + GraphIsomorphism, GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor, +}; /// An opaque handle identifying a vertex in a [`FrozenGraph`]. /// @@ -77,10 +79,10 @@ impl IncidenceCursor for FrozenGraphIncidenceCursor { /// let u = graph.add_vertex(); /// let v = graph.add_vertex(); /// let e = graph.add_edge(u, v); -/// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&graph); +/// let (graph, isomorphism) = FrozenGraph::convert(&graph); /// /// // Queries the FrozenGraph instance via the returned map. -/// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); +/// assert!(graph.are_adjacent(isomorphism.vertex(u), isomorphism.vertex(v))); /// ``` /// /// # Time and space complexity @@ -193,13 +195,7 @@ impl GraphTopology for FrozenGraph { } impl GraphTopologyConversion for FrozenGraph { - fn convert( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { // Builds vertex list with sentinel and vertex map. let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); let mut vertices = Vec::with_capacity(source.vertex_count() + 1); @@ -237,14 +233,12 @@ impl GraphTopologyConversion for FrozenGraph { } } - ( - Self { - vertices, - incidences, - }, - vertex_map, - edge_map, - ) + let result = Self { + vertices, + incidences, + }; + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/models/graph.rs b/src/models/graph.rs index a95e1c9..6677da0 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -6,8 +6,8 @@ use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; use crate::models; use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, - Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, + GraphTopologyDeletion, Incidence, IncidenceCursor, }; /// An opaque handle identifying a vertex in a [`Graph`]. @@ -399,19 +399,14 @@ impl GraphTopologyDeletion for Graph { } impl GraphTopologyConversion for Graph { - fn convert( - source: &H, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX)); let (result, vertex_map) = models::convert_to_addition(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { edge_map[e] = new_e }); - (result, vertex_map, edge_map) + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index b5dc20c..5689c28 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -185,16 +185,16 @@ impl MakeTestGraph for FrozenGraph { [Vec>; 10], ) { let (build, vertices, edges, incidences) = AppendGraph::standard(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|(e, u, v)| (edge_map[e], vertex_map[u], vertex_map[v])), + vertices.map(|v| iso.vertex(v)), + edges.map(|(e, u, v)| (iso.edge(e), iso.vertex(u), iso.vertex(v))), incidences.map(|vec| { vec.iter() .map(|i| Incidence { - vertex: vertex_map[i.vertex], - edge: edge_map[i.edge], + vertex: iso.vertex(i.vertex), + edge: iso.edge(i.edge), }) .collect() }), @@ -207,55 +207,55 @@ impl MakeTestGraph for FrozenGraph { fn single_vertex() -> (Self, Self::Vertex) { let (build, v) = AppendGraph::single_vertex(); - let (graph, vertex_map, _) = FrozenGraph::convert(&build); - (graph, vertex_map[v]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, iso.vertex(v)) } fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { let (build, vertices, e) = AppendGraph::single_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); - (graph, vertices.map(|v| vertex_map[v]), edge_map[e]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, vertices.map(|v| iso.vertex(v)), iso.edge(e)) } fn disconnected() -> (Self, [Self::Vertex; 3]) { let (build, vertices) = AppendGraph::disconnected(); - let (graph, vertex_map, _) = FrozenGraph::convert(&build); - (graph, vertices.map(|v| vertex_map[v])) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, vertices.map(|v| iso.vertex(v))) } fn loop_edge() -> (Self, Self::Vertex, Self::Edge) { let (build, v, e) = AppendGraph::loop_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); - (graph, vertex_map[v], edge_map[e]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, iso.vertex(v), iso.edge(e)) } fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { let (build, vertices, edges) = AppendGraph::multiple_edges(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { let (build, vertices, edges) = AppendGraph::two_edge_path(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { let (build, vertices, edges) = AppendGraph::two_edge_path_with_loops(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } } diff --git a/src/testing/graph_topology_addition_conversion_testing.rs b/src/testing/graph_topology_addition_conversion_testing.rs index 171573d..f3a423b 100644 --- a/src/testing/graph_topology_addition_conversion_testing.rs +++ b/src/testing/graph_topology_addition_conversion_testing.rs @@ -27,6 +27,6 @@ where H: MakeTestGraph + GraphTopologyAddition, { let (mut source, vertices, _) = H::single_edge(); - let (mut graph, vertex_map, _) = G::convert(&source); - fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| vertex_map[v]); + let (mut graph, iso) = G::convert(&source); + fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| iso.vertex(v)); } diff --git a/src/testing/graph_topology_conversion_testing.rs b/src/testing/graph_topology_conversion_testing.rs index 41f8de8..3c5e22c 100644 --- a/src/testing/graph_topology_conversion_testing.rs +++ b/src/testing/graph_topology_conversion_testing.rs @@ -31,13 +31,13 @@ where H::Edge: Debug, { let (source, vertices, edges, _) = H::standard(); - let (graph, vertex_map, edge_map) = G::convert(&source); + let (graph, iso) = G::convert(&source); fixtures::assert_isomorphic( &source, &graph, &vertices, - |v| vertex_map[v], + |v| iso.vertex(v), &edges, - |e| edge_map[e], + |e| iso.edge(e), ); } diff --git a/src/traits.rs b/src/traits.rs index 06ca661..0d5414b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -362,14 +362,14 @@ pub struct Incidence { pub edge: E, } -// TODO-0.3.0: Provide and document a way to transform an ElementMap for source to an ElementMap for the copy. /// A trait for constructing an undirected graph topology by converting another graph /// implementation to this one. pub trait GraphTopologyConversion: GraphTopology { /// Creates a new graph instance that is isomorphic to `source`. /// - /// Returns the converted graph instance and two mappings from the vertices and edges of - /// `source` to the corresponding vertices and edges of the returned graph. + /// Returns the converted graph instance and a [`GraphIsomorphism`] instance that maps the + /// vertices and edges of `source` to the corresponding vertices and edges of the returned + /// graph. /// /// # Examples /// @@ -380,18 +380,153 @@ pub trait GraphTopologyConversion: GraphTopology { /// let mut source = AppendGraph::new(); /// let u = source.add_vertex(); /// let v = source.add_vertex(); - /// let e = source.add_edge(u, v); - /// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&source); + /// source.add_edge(u, v); + /// let (graph, iso) = FrozenGraph::convert(&source); /// - /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); + /// assert!(graph.are_adjacent(iso.vertex(u), iso.vertex(v))); /// ``` - fn convert( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) + /// + /// The isomorphism can also convert [`ElementMap`] instances defined on `source` so that they + /// can be indexed by vertex or edge handles of the new graph. + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// + /// # let mut source = AppendGraph::new(); + /// # let u = source.add_vertex(); + /// # let v = source.add_vertex(); + /// # let (graph, isomorphism) = FrozenGraph::convert(&source); + /// + /// let mut source_map = source.vertex_map(0); + /// source_map[u] = 7; + /// let map = isomorphism.convert_vertex_map(&source_map); + /// + /// assert_eq!(map[isomorphism.vertex(u)], 7); + /// ``` + fn convert(source: &G) -> (Self, GraphIsomorphism) where Self: Sized; } + +/// An explicit isomorphism between a source graph of type `G` and a graph of type `H` converted +/// from it. +/// +/// Returned by [`GraphTopologyConversion::convert`]. It maps each vertex and edge handle of the +/// source graph to the corresponding handle of the converted graph, and can transfer +/// [`ElementMap`] instances defined on the source over to the converted graph. +/// +/// The isomorphism describes the source as it was when it was converted. Handle lookups stay +/// correct for handles that were valid then. If the source graph type reuses storage after +/// deletions (see [`GraphTopologyDeletion`]), do not delete vertices or edges from the source +/// before converting maps, since a new element could take the place of a deleted one and be paired +/// with it. +pub struct GraphIsomorphism { + vertices: ElementMap, + edges: ElementMap, + vertex_map_template: ElementMap, + edge_map_template: ElementMap, +} + +impl GraphIsomorphism { + /// Creates an isomorphism from the given handle mappings, for use by implementations of + /// [`GraphTopologyConversion::convert`]. + /// + /// `vertices` and `edges` must map every vertex and edge of the source to the corresponding + /// vertex and edge of `target`, respectively. Their default values mark unmapped slots, so they + /// must be handles that are never valid in `target`, and must not have been assigned to any + /// mapped slot. Passing mappings that are not an isomorphism results in unspecified, but + /// memory-safe, behaviour. + pub fn new( + target: &H, + vertices: ElementMap, + edges: ElementMap, + ) -> Self { + Self { + vertices, + edges, + vertex_map_template: target.vertex_map(()), + edge_map_template: target.edge_map(()), + } + } + + /// Returns the vertex of the converted graph that corresponds to the source vertex `v`. + /// + /// `v` must have been a vertex of the source graph when it was converted. Otherwise, the + /// returned handle may be invalid, or this method may panic. + pub fn vertex(&self, v: G::Vertex) -> H::Vertex { + self.vertices[v] + } + + /// Returns the edge of the converted graph that corresponds to the source edge `e`. + /// + /// `e` must have been an edge of the source graph when it was converted. Otherwise, the + /// returned handle may be invalid, or this method may panic. + pub fn edge(&self, e: G::Edge) -> H::Edge { + self.edges[e] + } + + /// Converts a vertex map of the source into an equivalent vertex map of the converted graph. + /// + /// For each source vertex *v*, the returned map holds a clone of `map[v]` at `vertex(v)`. It + /// has the same default value as `map`, and it covers the vertices of the converted graph at + /// the time of conversion. Use [`GraphTopologyAddition::sync_vertex_map`] before accessing + /// vertices added to the converted graph afterwards. Missing slots of `map`, for instance + /// because `map` was created before the source grew, are treated as holding the default value. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let (graph, iso) = FrozenGraph::convert(&source); + /// + /// let mut labels = source.vertex_map("none"); + /// labels[u] = "start"; + /// let converted = iso.convert_vertex_map(&labels); + /// assert_eq!(converted[iso.vertex(u)], "start"); + /// assert_eq!(converted[iso.vertex(v)], "none"); + /// ``` + pub fn convert_vertex_map( + &self, + map: &ElementMap, + ) -> ElementMap { + map.remap(&self.vertices, &self.vertex_map_template) + } + + /// Converts an edge map of the source into an equivalent edge map of the converted graph. + /// + /// For each source edge *e*, the returned map holds a clone of `map[e]` at `edge(e)`. It has + /// the same default value as `map`, and it covers the edges of the converted graph at the time + /// of conversion. Use [`GraphTopologyAddition::sync_edge_map`] before accessing edges added to + /// the converted graph afterwards. Missing slots of `map`, for instance because `map` was + /// created before the source grew, are treated as holding the default value. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let e = source.add_edge(u, v); + /// let f = source.add_edge(u, v); + /// let (graph, iso) = FrozenGraph::convert(&source); + /// + /// let mut weights = source.edge_map(1); + /// weights[e] = 5; + /// let converted = iso.convert_edge_map(&weights); + /// assert_eq!(converted[iso.edge(e)], 5); + /// assert_eq!(converted[iso.edge(f)], 1); + /// ``` + pub fn convert_edge_map( + &self, + map: &ElementMap, + ) -> ElementMap { + map.remap(&self.edges, &self.edge_map_template) + } +}