From 125c106a5b1c07edd29fb429cce664f919287203 Mon Sep 17 00:00:00 2001 From: Thomas Coopman Date: Mon, 26 Sep 2022 16:44:24 +0200 Subject: [PATCH 1/5] start implementing undirected methods --- lib/graph.ex | 3 +- lib/graph/undirected.ex | 91 ++++++++++++++++++++++++++++++++++ test/undirected_graph_test.exs | 32 ++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 lib/graph/undirected.ex create mode 100644 test/undirected_graph_test.exs diff --git a/lib/graph.ex b/lib/graph.ex index 78d1859..6ccb031 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1534,7 +1534,8 @@ defmodule Graph do [:d, :c, :b, :a] """ @spec reachable(t, [vertex]) :: [[vertex]] - defdelegate reachable(g, vs), to: Graph.Directed + def reachable(%Graph{type: :undirected} = g, vs), do: Graph.Undirected.reachable(g, vs) + def reachable(%Graph{} = g, vs), do: Graph.Directed.reachable(g, vs) @doc """ Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`), diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex new file mode 100644 index 0000000..0b8f678 --- /dev/null +++ b/lib/graph/undirected.ex @@ -0,0 +1,91 @@ +defmodule Graph.Undirected do + @moduledoc false + @compile {:inline, [in_neighbors: 2, in_neighbors: 3, out_neighbors: 2, out_neighbors: 3]} + + def reachable(%Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, vs) + when is_list(vs) do + vs = Enum.map(vs, vertex_identifier) + for id <- :lists.append(forest(g, &neighbors/3, vs, :first)), do: Map.get(vertices, id) + end + + def neighbors(%Graph{} = g, v, []) do + out_neighbors(g, v) ++ in_neighbors(g, v) + end + + def in_neighbors(%Graph{} = g, v, []) do + in_neighbors(g, v) + end + + def in_neighbors(%Graph{in_edges: ie}, v, vs) do + case Map.get(ie, v) do + nil -> vs + v_in -> MapSet.to_list(v_in) ++ vs + end + end + + def in_neighbors(%Graph{in_edges: ie}, v) do + case Map.get(ie, v) do + nil -> [] + v_in -> MapSet.to_list(v_in) + end + end + + def out_neighbors(%Graph{} = g, v, []) do + in_neighbors(g, v) + end + + def out_neighbors(%Graph{out_edges: oe}, v, vs) do + case Map.get(oe, v) do + nil -> vs + v_out -> MapSet.to_list(v_out) ++ vs + end + end + + def out_neighbors(%Graph{out_edges: oe}, v) do + case Map.get(oe, v) do + nil -> [] + v_out -> MapSet.to_list(v_out) + end + end + + defp forest(%Graph{vertices: vs} = g, fun) do + forest(g, fun, Map.keys(vs)) + end + + defp forest(g, fun, vs) do + forest(g, fun, vs, :first) + end + + defp forest(g, fun, vs, handle_first) do + {_, acc} = + List.foldl(vs, {MapSet.new(), []}, fn v, {visited, acc} -> + pretraverse(handle_first, v, fun, g, visited, acc) + end) + + acc + end + + defp pretraverse(:first, v, fun, g, visited, acc) do + ptraverse([v], fun, g, visited, [], acc) + end + + defp pretraverse(:not_first, v, fun, g, visited, acc) do + if MapSet.member?(visited, v) do + {visited, acc} + else + ptraverse(fun.(g, v, []), fun, g, visited, [], acc) + end + end + + defp ptraverse([v | vs], fun, g, visited, results, acc) do + if MapSet.member?(visited, v) do + ptraverse(vs, fun, g, visited, results, acc) + else + visited = MapSet.put(visited, v) + ptraverse(fun.(g, v, vs), fun, g, visited, [v | results], acc) + end + end + + defp ptraverse([], _fun, _g, visited, [], acc), do: {visited, acc} + defp ptraverse([], _fun, _g, visited, results, acc), do: {visited, [results | acc]} +end diff --git a/test/undirected_graph_test.exs b/test/undirected_graph_test.exs new file mode 100644 index 0000000..a539afb --- /dev/null +++ b/test/undirected_graph_test.exs @@ -0,0 +1,32 @@ +defmodule GraphTest do + use ExUnit.Case, async: true + alias Graph.Edge + alias Graph.Test.Generators + + describe "Graph.reachable/2" do + test "reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}]) + + assert [:a, :b, :c] = Graph.reachable(g, [:c]) + end + + test "parts reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :d}, {:e, :c}]) + + assert [:e, :c] = Graph.reachable(g, [:c]) + end + + test "nothing reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :d}]) + |> Graph.add_vertex(:c) + + assert [:c] = Graph.reachable(g, [:c]) + end + end +end From 5336ebb15a5e3d545336f46f2409b39946ad4327 Mon Sep 17 00:00:00 2001 From: Thomas Coopman Date: Mon, 26 Sep 2022 16:53:23 +0200 Subject: [PATCH 2/5] add reachable_neighbors --- lib/graph.ex | 5 +++- lib/graph/undirected.ex | 11 +++++++++ test/undirected_graph_test.exs | 43 ++++++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 6ccb031..fae2255 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1551,7 +1551,10 @@ defmodule Graph do [:d, :c, :b] """ @spec reachable_neighbors(t, [vertex]) :: [[vertex]] - defdelegate reachable_neighbors(g, vs), to: Graph.Directed + def reachable_neighbors(%Graph{type: :undirected} = g, vs), + do: Graph.Undirected.reachable_neighbors(g, vs) + + def reachable_neighbors(%Graph{} = g, vs), do: Graph.Directed.reachable_neighbors(g, vs) @doc """ Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`), diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex index 0b8f678..5b3f086 100644 --- a/lib/graph/undirected.ex +++ b/lib/graph/undirected.ex @@ -8,6 +8,17 @@ defmodule Graph.Undirected do for id <- :lists.append(forest(g, &neighbors/3, vs, :first)), do: Map.get(vertices, id) end + def reachable_neighbors( + %Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, + vs + ) + when is_list(vs) do + vs = Enum.map(vs, vertex_identifier) + + for id <- :lists.append(forest(g, &out_neighbors/3, vs, :not_first)), + do: Map.get(vertices, id) + end + def neighbors(%Graph{} = g, v, []) do out_neighbors(g, v) ++ in_neighbors(g, v) end diff --git a/test/undirected_graph_test.exs b/test/undirected_graph_test.exs index a539afb..bbf3046 100644 --- a/test/undirected_graph_test.exs +++ b/test/undirected_graph_test.exs @@ -15,9 +15,9 @@ defmodule GraphTest do test "parts reachable" do g = Graph.new(type: :undirected) - |> Graph.add_edges([{:a, :b}, {:b, :d}, {:e, :c}]) + |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}]) - assert [:e, :c] = Graph.reachable(g, [:c]) + assert [:d, :e] = Graph.reachable(g, [:e]) end test "nothing reachable" do @@ -28,5 +28,44 @@ defmodule GraphTest do assert [:c] = Graph.reachable(g, [:c]) end + + test "unknown vertex" do + g = Graph.new(type: :undirected) + + assert [nil] = Graph.reachable(g, [:a]) + end + end + + describe "Graph.reachable_neighbours/2" do + test "reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}]) + + assert [:a, :b] = Graph.reachable_neighbors(g, [:c]) + end + + test "parts reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}, {:e, :f}]) + + assert [:d, :e] = Graph.reachable_neighbors(g, [:f]) + end + + test "nothing reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :d}]) + |> Graph.add_vertex(:c) + + assert [] = Graph.reachable_neighbors(g, [:c]) + end + + test "unknown vertex" do + g = Graph.new(type: :undirected) + + assert [] = Graph.reachable_neighbors(g, [:a]) + end end end From c1df14a5af7a990978e96356e15e4fca2ed2de86 Mon Sep 17 00:00:00 2001 From: Thomas Coopman Date: Mon, 26 Sep 2022 17:01:50 +0200 Subject: [PATCH 3/5] fix compilation --- test/undirected_graph_test.exs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/undirected_graph_test.exs b/test/undirected_graph_test.exs index bbf3046..078579f 100644 --- a/test/undirected_graph_test.exs +++ b/test/undirected_graph_test.exs @@ -1,7 +1,5 @@ -defmodule GraphTest do +defmodule Graph.UndirectedTest do use ExUnit.Case, async: true - alias Graph.Edge - alias Graph.Test.Generators describe "Graph.reachable/2" do test "reachable" do From 8518bae6cd200d037b8bcb917644f828272d128b Mon Sep 17 00:00:00 2001 From: Thomas Coopman Date: Mon, 26 Sep 2022 17:02:01 +0200 Subject: [PATCH 4/5] fix implementation of neighbors --- lib/graph/undirected.ex | 52 +++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex index 5b3f086..44066c8 100644 --- a/lib/graph/undirected.ex +++ b/lib/graph/undirected.ex @@ -1,6 +1,6 @@ defmodule Graph.Undirected do @moduledoc false - @compile {:inline, [in_neighbors: 2, in_neighbors: 3, out_neighbors: 2, out_neighbors: 3]} + @compile {:inline, [neighbors: 2, neighbors: 3]} def reachable(%Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, vs) when is_list(vs) do @@ -15,47 +15,37 @@ defmodule Graph.Undirected do when is_list(vs) do vs = Enum.map(vs, vertex_identifier) - for id <- :lists.append(forest(g, &out_neighbors/3, vs, :not_first)), + for id <- :lists.append(forest(g, &neighbors/3, vs, :not_first)), do: Map.get(vertices, id) end def neighbors(%Graph{} = g, v, []) do - out_neighbors(g, v) ++ in_neighbors(g, v) + neighbors(g, v) end - def in_neighbors(%Graph{} = g, v, []) do - in_neighbors(g, v) - end - - def in_neighbors(%Graph{in_edges: ie}, v, vs) do + def neighbors(%Graph{out_edges: oe, in_edges: ie}, v, vs) do case Map.get(ie, v) do - nil -> vs - v_in -> MapSet.to_list(v_in) ++ vs + nil -> + case Map.get(oe, v) do + nil -> vs + v_out -> MapSet.to_list(v_out) ++ vs + end + + v_in -> + MapSet.to_list(v_in) ++ vs end end - def in_neighbors(%Graph{in_edges: ie}, v) do + def neighbors(%Graph{out_edges: oe, in_edges: ie}, v) do case Map.get(ie, v) do - nil -> [] - v_in -> MapSet.to_list(v_in) - end - end - - def out_neighbors(%Graph{} = g, v, []) do - in_neighbors(g, v) - end - - def out_neighbors(%Graph{out_edges: oe}, v, vs) do - case Map.get(oe, v) do - nil -> vs - v_out -> MapSet.to_list(v_out) ++ vs - end - end - - def out_neighbors(%Graph{out_edges: oe}, v) do - case Map.get(oe, v) do - nil -> [] - v_out -> MapSet.to_list(v_out) + nil -> + case Map.get(oe, v) do + nil -> [] + v_out -> MapSet.to_list(v_out) + end + + v_in -> + MapSet.to_list(v_in) end end From aac7d41c7d098cbafb8adbf5fc29bd19b6cbc0b3 Mon Sep 17 00:00:00 2001 From: Thomas Coopman Date: Tue, 27 Sep 2022 16:03:18 +0200 Subject: [PATCH 5/5] better neighbors - don't know how to fix reachable_neighbors --- lib/graph/undirected.ex | 35 +++++++++++++++++----------------- test/undirected_graph_test.exs | 4 ++++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex index 44066c8..8da54e2 100644 --- a/lib/graph/undirected.ex +++ b/lib/graph/undirected.ex @@ -24,29 +24,28 @@ defmodule Graph.Undirected do end def neighbors(%Graph{out_edges: oe, in_edges: ie}, v, vs) do - case Map.get(ie, v) do - nil -> - case Map.get(oe, v) do - nil -> vs - v_out -> MapSet.to_list(v_out) ++ vs - end - - v_in -> + case {Map.get(ie, v), Map.get(oe, v)} do + {nil, nil} -> + vs + + {v_in, nil} -> MapSet.to_list(v_in) ++ vs + + {nil, v_out} -> + MapSet.to_list(v_out) ++ vs + + {v_in, v_out} -> + s = MapSet.union(v_in, v_out) + MapSet.to_list(s) ++ vs end end def neighbors(%Graph{out_edges: oe, in_edges: ie}, v) do - case Map.get(ie, v) do - nil -> - case Map.get(oe, v) do - nil -> [] - v_out -> MapSet.to_list(v_out) - end - - v_in -> - MapSet.to_list(v_in) - end + v_in = Map.get(ie, v, MapSet.new()) + v_out = Map.get(oe, v, MapSet.new()) + + MapSet.union(v_in, v_out) + |> MapSet.to_list() end defp forest(%Graph{vertices: vs} = g, fun) do diff --git a/test/undirected_graph_test.exs b/test/undirected_graph_test.exs index 078579f..1a786e5 100644 --- a/test/undirected_graph_test.exs +++ b/test/undirected_graph_test.exs @@ -8,6 +8,7 @@ defmodule Graph.UndirectedTest do |> Graph.add_edges([{:a, :b}, {:b, :c}]) assert [:a, :b, :c] = Graph.reachable(g, [:c]) + assert [:c, :b, :a] = Graph.reachable(g, [:a]) end test "parts reachable" do @@ -16,6 +17,7 @@ defmodule Graph.UndirectedTest do |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}]) assert [:d, :e] = Graph.reachable(g, [:e]) + assert [:c, :a, :b] = Graph.reachable(g, [:b]) end test "nothing reachable" do @@ -43,12 +45,14 @@ defmodule Graph.UndirectedTest do assert [:a, :b] = Graph.reachable_neighbors(g, [:c]) end + @tag :only test "parts reachable" do g = Graph.new(type: :undirected) |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}, {:e, :f}]) assert [:d, :e] = Graph.reachable_neighbors(g, [:f]) + assert [] = Graph.reachable_neighbors(g, [:b]) end test "nothing reachable" do