From 38c7a51d3c3d59dd4b20651a9b929fef48991956 Mon Sep 17 00:00:00 2001 From: Boris Okner Date: Tue, 15 Apr 2025 01:07:34 -0400 Subject: [PATCH 1/2] Subgraph (wip, more TBD on syncing adjacency with edge lists --- lib/bitgraph.ex | 57 +++++++++++++++++++++++++++++++++++++----- lib/edge.ex | 22 ++++++++-------- test/bitgraph_test.exs | 56 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/lib/bitgraph.ex b/lib/bitgraph.ex index 6ca108c..a6b6969 100644 --- a/lib/bitgraph.ex +++ b/lib/bitgraph.ex @@ -21,6 +21,33 @@ defmodule BitGraph do Map.put(graph, :adjacency, Adjacency.copy(adjacency, graph.edges)) end + @doc """ + Creates a subgraph on `subgraph_vertices` + `shared?` signifies whether adjacency matrix is shared with parent graph. + That is, `shared? = true` implies that all destructive operations on parent graph will + affect a subgraph, and vice versa. + """ + def subgraph(graph, subgraph_vertices, shared? \\ false) do + graph = (shared? && graph || copy(graph)) + + graph + |> BitGraph.vertices() + |> Enum.reduce( + Map.put(graph, :shared?, shared?), + fn existing_vertex, acc -> + if existing_vertex not in subgraph_vertices do + BitGraph.delete_vertex(acc, existing_vertex) + else + acc + end + end + ) + end + + def shared?(graph) do + graph[:shared?] + end + def default_opts() do [max_vertices: 1024] end @@ -97,10 +124,8 @@ defmodule BitGraph do from_index = V.get_vertex_index(graph, from) to_index = V.get_vertex_index(graph, to) from_index && to_index && - ( E.delete_edge(graph, from_index, to_index) - Map.update(graph, :edges, %{}, fn edges -> Map.delete(edges, {from_index, to_index}) end) - ) || graph + || graph end def delete_edges(graph, edges) do @@ -140,6 +165,10 @@ defmodule BitGraph do out_edges(graph, vertex, fn _from, to, graph -> V.get_vertex(graph, to) end) end + def neighbors(graph, vertex) do + MapSet.union(out_neighbors(graph, vertex), in_neighbors(graph, vertex)) + end + def out_edges(graph, vertex, edge_fun \\ &default_edge_info/3) do edges_impl(graph, V.get_vertex_index(graph, vertex), edge_fun, :out_edges) end @@ -148,6 +177,10 @@ defmodule BitGraph do in_neighbors(graph, vertex) |> MapSet.size() end + def degree(graph, vertex) do + in_degree(graph, vertex) + out_degree(graph, vertex) + end + def out_degree(graph, vertex) do out_neighbors(graph, vertex) |> MapSet.size() end @@ -156,7 +189,10 @@ defmodule BitGraph do neighbor_indices = neighbors_impl(graph, vertex_index, direction) Enum.reduce(neighbor_indices, MapSet.new(), fn neighbor_index, acc -> {from_vertex, to_vertex} = edge_vertices(vertex_index, neighbor_index, direction) - MapSet.put(acc, edge_info_fun.(from_vertex, to_vertex, graph)) + case edge_info_fun.(from_vertex, to_vertex, graph) do + nil -> acc + edge_info -> MapSet.put(acc, edge_info) + end end) end @@ -169,11 +205,20 @@ defmodule BitGraph do end defp neighbors_impl(graph, vertex_index, :out_edges) do - E.out_neighbors(graph, vertex_index) + (if shared?(graph) do + V.get_vertex(graph, vertex_index) && E.out_neighbors(graph, vertex_index) + else + E.out_neighbors(graph, vertex_index) + end) || MapSet.new() end defp neighbors_impl(graph, vertex_index, :in_edges) do - E.in_neighbors(graph, vertex_index) + (if shared?(graph) do + V.get_vertex(graph, vertex_index) && E.in_neighbors(graph, vertex_index) + else + E.in_neighbors(graph, vertex_index) + end) || MapSet.new() + end defp edge_vertices(v1, v2, :out_edges) do diff --git a/lib/edge.ex b/lib/edge.ex index d65b479..2521606 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -50,21 +50,21 @@ defmodule BitGraph.E do in_neighbors(graph, vertex) |> MapSet.size() end - def delete_edge(%{adjacency: adjacency} = _graph, from, to) when is_integer(from) and is_integer(to) do - Adjacency.clear(adjacency, from, to) + def delete_edge(%{adjacency: adjacency, edges: edges} = graph, from, to) when is_integer(from) and is_integer(to) do + BitGraph.shared?(graph) || Adjacency.clear(adjacency, from, to) + edges + |> Map.delete({from, to}) + |> then(fn updated_edges -> Map.put(graph, :edges, updated_edges) end) end - def delete_edges(%{edges: edges, adjacency: adjacency} = graph, vertex) when is_integer(vertex) do - Enum.reduce(Adjacency.row(adjacency, vertex), edges, fn out_neighbor, acc -> - Adjacency.clear(adjacency, vertex, out_neighbor) - Map.delete(acc, {vertex, out_neighbor}) + def delete_edges(%{adjacency: adjacency} = graph, vertex) when is_integer(vertex) do + Enum.reduce(Adjacency.row(adjacency, vertex), graph, fn out_neighbor, acc -> + delete_edge(acc, vertex, out_neighbor) end) - |> then(fn edges1 -> - edges2 = Enum.reduce(Adjacency.column(adjacency, vertex), edges1, fn in_neighbor, acc -> - Adjacency.clear(adjacency, in_neighbor, vertex) - Map.delete(acc, {in_neighbor, vertex}) + |> then(fn graph -> + Enum.reduce(Adjacency.column(adjacency, vertex), graph, fn in_neighbor, acc -> + delete_edge(acc, in_neighbor, vertex) end) - Map.put(graph, :edges, edges2) end) end end diff --git a/test/bitgraph_test.exs b/test/bitgraph_test.exs index 10e6a86..4177abf 100644 --- a/test/bitgraph_test.exs +++ b/test/bitgraph_test.exs @@ -134,4 +134,60 @@ defmodule BitGraphTest do end end + + test "subgraph (detached)" do + graph = BitGraph.new() |> BitGraph.add_vertices([:a, :b, :c, :d]) |> BitGraph.add_edges( + [ + {:a, :b}, {:a, :c}, {:a, :d}, + {:b, :c}, {:b, :d}, + {:c, :d} + ]) + + ## "Detached" subgraph (no sharing with parent) + detached_subgraph = BitGraph.subgraph(graph, [:a, :b, :c]) + assert_subgraph(detached_subgraph) + ## The parent graph is not affected + assert BitGraph.num_vertices(graph) == 4 + assert BitGraph.num_edges(graph) == 6 + ## Removing vertex from parent graph does not affect a detached subgraph + graph2 = BitGraph.delete_vertex(graph, :a) + assert BitGraph.num_vertices(graph2) == 3 + assert_subgraph(detached_subgraph) + ## Removing vertex from detached subgraph does not affect the parent graph + _detached_subgraph2 = BitGraph.delete_vertex(detached_subgraph, :a) + assert BitGraph.num_vertices(graph) == 4 + + end + + test "subgraph (shared)" do + graph = BitGraph.new() |> BitGraph.add_vertices([:a, :b, :c, :d]) |> BitGraph.add_edges( + [ + {:a, :b}, {:a, :c}, {:a, :d}, + {:b, :c}, {:b, :d}, + {:c, :d} + ]) + + ## "Shared" subgraph (destructive operations on either parent or subgraph may affect both) + shared_subgraph = BitGraph.subgraph(graph, [:a, :b, :c], true) + assert_subgraph(shared_subgraph) + ## Initially, the parent graph is not affected + assert BitGraph.num_vertices(graph) == 4 + assert BitGraph.num_edges(graph) == 6 + ## Removing shared vertex from parent graph affects a detached subgraph + graph2 = BitGraph.delete_vertex(graph, :a) + assert BitGraph.num_edges(graph2) == 3 + assert BitGraph.degree(shared_subgraph, :a) == 0 + assert BitGraph.neighbors(shared_subgraph, :b) == MapSet.new([:c]) + assert BitGraph.neighbors(shared_subgraph, :c) == MapSet.new([:b]) + #assert BitGraph.out_edges(shared_subgraph, :c) |> map_size() == 1 + end + + defp assert_subgraph(subgraph) do + assert Enum.sort(BitGraph.vertices(subgraph)) == Enum.sort([:a, :b, :c]) + assert BitGraph.num_edges(subgraph) == 3 + assert BitGraph.out_neighbors(subgraph, :a) == MapSet.new([:c, :b]) + assert BitGraph.out_neighbors(subgraph, :b) == MapSet.new([:c]) + assert BitGraph.in_neighbors(subgraph, :b) == MapSet.new([:a]) + assert BitGraph.in_neighbors(subgraph, :c) == MapSet.new([:a, :b]) + end end From 359a351eb36febbc5c23b57cb04a11cb54a8f7cf Mon Sep 17 00:00:00 2001 From: Boris Okner Date: Tue, 15 Apr 2025 12:55:17 -0400 Subject: [PATCH 2/2] Remove 'shared' version of subgraph --- lib/bitgraph.ex | 29 ++++------------------------- lib/edge.ex | 2 +- test/bitgraph_test.exs | 35 ++++++----------------------------- 3 files changed, 11 insertions(+), 55 deletions(-) diff --git a/lib/bitgraph.ex b/lib/bitgraph.ex index a6b6969..37933e2 100644 --- a/lib/bitgraph.ex +++ b/lib/bitgraph.ex @@ -23,17 +23,9 @@ defmodule BitGraph do @doc """ Creates a subgraph on `subgraph_vertices` - `shared?` signifies whether adjacency matrix is shared with parent graph. - That is, `shared? = true` implies that all destructive operations on parent graph will - affect a subgraph, and vice versa. """ - def subgraph(graph, subgraph_vertices, shared? \\ false) do - graph = (shared? && graph || copy(graph)) - - graph - |> BitGraph.vertices() - |> Enum.reduce( - Map.put(graph, :shared?, shared?), + def subgraph(graph, subgraph_vertices) do + Enum.reduce(BitGraph.vertices(graph), copy(graph), fn existing_vertex, acc -> if existing_vertex not in subgraph_vertices do BitGraph.delete_vertex(acc, existing_vertex) @@ -44,10 +36,6 @@ defmodule BitGraph do ) end - def shared?(graph) do - graph[:shared?] - end - def default_opts() do [max_vertices: 1024] end @@ -205,20 +193,11 @@ defmodule BitGraph do end defp neighbors_impl(graph, vertex_index, :out_edges) do - (if shared?(graph) do - V.get_vertex(graph, vertex_index) && E.out_neighbors(graph, vertex_index) - else - E.out_neighbors(graph, vertex_index) - end) || MapSet.new() + E.out_neighbors(graph, vertex_index) end defp neighbors_impl(graph, vertex_index, :in_edges) do - (if shared?(graph) do - V.get_vertex(graph, vertex_index) && E.in_neighbors(graph, vertex_index) - else - E.in_neighbors(graph, vertex_index) - end) || MapSet.new() - + E.in_neighbors(graph, vertex_index) end defp edge_vertices(v1, v2, :out_edges) do diff --git a/lib/edge.ex b/lib/edge.ex index 2521606..38c68cf 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -51,7 +51,7 @@ defmodule BitGraph.E do end def delete_edge(%{adjacency: adjacency, edges: edges} = graph, from, to) when is_integer(from) and is_integer(to) do - BitGraph.shared?(graph) || Adjacency.clear(adjacency, from, to) + Adjacency.clear(adjacency, from, to) edges |> Map.delete({from, to}) |> then(fn updated_edges -> Map.put(graph, :edges, updated_edges) end) diff --git a/test/bitgraph_test.exs b/test/bitgraph_test.exs index 4177abf..63d09a5 100644 --- a/test/bitgraph_test.exs +++ b/test/bitgraph_test.exs @@ -135,7 +135,7 @@ defmodule BitGraphTest do end - test "subgraph (detached)" do + test "subgraph" do graph = BitGraph.new() |> BitGraph.add_vertices([:a, :b, :c, :d]) |> BitGraph.add_edges( [ {:a, :b}, {:a, :c}, {:a, :d}, @@ -143,44 +143,21 @@ defmodule BitGraphTest do {:c, :d} ]) - ## "Detached" subgraph (no sharing with parent) - detached_subgraph = BitGraph.subgraph(graph, [:a, :b, :c]) - assert_subgraph(detached_subgraph) + subgraph = BitGraph.subgraph(graph, [:a, :b, :c]) + assert_subgraph(subgraph) ## The parent graph is not affected assert BitGraph.num_vertices(graph) == 4 assert BitGraph.num_edges(graph) == 6 ## Removing vertex from parent graph does not affect a detached subgraph graph2 = BitGraph.delete_vertex(graph, :a) assert BitGraph.num_vertices(graph2) == 3 - assert_subgraph(detached_subgraph) - ## Removing vertex from detached subgraph does not affect the parent graph - _detached_subgraph2 = BitGraph.delete_vertex(detached_subgraph, :a) + assert_subgraph(subgraph) + ## Removing vertex from subgraph does not affect the parent graph + _subgraph2 = BitGraph.delete_vertex(subgraph, :a) assert BitGraph.num_vertices(graph) == 4 end - test "subgraph (shared)" do - graph = BitGraph.new() |> BitGraph.add_vertices([:a, :b, :c, :d]) |> BitGraph.add_edges( - [ - {:a, :b}, {:a, :c}, {:a, :d}, - {:b, :c}, {:b, :d}, - {:c, :d} - ]) - - ## "Shared" subgraph (destructive operations on either parent or subgraph may affect both) - shared_subgraph = BitGraph.subgraph(graph, [:a, :b, :c], true) - assert_subgraph(shared_subgraph) - ## Initially, the parent graph is not affected - assert BitGraph.num_vertices(graph) == 4 - assert BitGraph.num_edges(graph) == 6 - ## Removing shared vertex from parent graph affects a detached subgraph - graph2 = BitGraph.delete_vertex(graph, :a) - assert BitGraph.num_edges(graph2) == 3 - assert BitGraph.degree(shared_subgraph, :a) == 0 - assert BitGraph.neighbors(shared_subgraph, :b) == MapSet.new([:c]) - assert BitGraph.neighbors(shared_subgraph, :c) == MapSet.new([:b]) - #assert BitGraph.out_edges(shared_subgraph, :c) |> map_size() == 1 - end defp assert_subgraph(subgraph) do assert Enum.sort(BitGraph.vertices(subgraph)) == Enum.sort([:a, :b, :c])