From a96b5852de672f59bc4fc293ff14f2f7a7e7faca Mon Sep 17 00:00:00 2001 From: zblanco Date: Sun, 18 Aug 2024 17:28:10 -0600 Subject: [PATCH 01/18] approach multigraph indexing with edge properties --- .gitignore | 1 + lib/edge.ex | 9 ++++++--- lib/graph.ex | 45 +++++++++++++++++++++++++++++++++++++++++---- lib/graph/utils.ex | 2 ++ test/graph_test.exs | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 735ed1d..a2eb72c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ erl_crash.dump # QuickCheck files /.eqc-info /*.eqc +.elixir_ls/ \ No newline at end of file diff --git a/lib/edge.ex b/lib/edge.ex index 4563ccc..d6cec16 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -8,13 +8,15 @@ defmodule Graph.Edge do defstruct v1: nil, v2: nil, weight: 1, - label: nil + label: nil, + properties: %{} @type t :: %__MODULE__{ v1: Graph.vertex(), v2: Graph.vertex(), weight: integer | float, - label: term + label: term, + properties: map } @type edge_opt :: {:weight, integer | float} @@ -40,7 +42,8 @@ defmodule Graph.Edge do v1: v1, v2: v2, weight: Keyword.get(opts, :weight, 1), - label: Keyword.get(opts, :label) + label: Keyword.get(opts, :label), + properties: Keyword.get(opts, :properties, %{}) } end diff --git a/lib/graph.ex b/lib/graph.ex index b66670f..7e25fd3 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -28,10 +28,13 @@ defmodule Graph do defstruct in_edges: %{}, out_edges: %{}, edges: %{}, + edge_index: %{}, vertex_labels: %{}, vertices: %{}, type: :directed, - vertex_identifier: &Graph.Utils.vertex_id/1 + vertex_identifier: &Graph.Utils.vertex_id/1, + edge_indexer: &Graph.Utils.edge_label/1, + multigraph: false alias Graph.{Edge, EdgeSpecificationError} @@ -43,17 +46,26 @@ defmodule Graph do @type label :: term @type edge_weight :: integer | float @type edge_key :: {vertex_id, vertex_id} - @type edge_value :: %{label => edge_weight} + # @type edge_value :: %{label => edge_weight} + @type edge_index_key :: label | term + @type edge_properties :: %{ + label: label, + weight: edge_weight + } + @type edge_value :: %{label => edge_properties()} @type graph_type :: :directed | :undirected @type vertices :: %{vertex_id => vertex} @type t :: %__MODULE__{ in_edges: %{vertex_id => MapSet.t()}, out_edges: %{vertex_id => MapSet.t()}, edges: %{edge_key => edge_value}, + edge_index: %{edge_index_key => MapSet.t()}, vertex_labels: %{vertex_id => term}, vertices: %{vertex_id => vertex}, type: graph_type, - vertex_identifier: (vertex() -> term()) + vertex_identifier: (vertex() -> term()), + edge_indexer: (Edge.t() -> edge_index_key), + multigraph: boolean() } @type graph_info :: %{ :num_edges => non_neg_integer(), @@ -70,6 +82,23 @@ defmodule Graph do - `type: :directed | :undirected`, specifies what type of graph this is. Defaults to a `:directed` graph. - `vertex_identifier`: a function which accepts a vertex and returns a unique identifier of said vertex. Defaults to `Graph.Utils.vertex_id/1`, a hash of the whole vertex utilizing `:erlang.phash2/2`. + - `multigraph: true | false | fn edge -> key end`, enables edge indexing by a key. + - When `true`, the key is the edge label itself. + - When `false` no additional memory is used for sets of . + - `edge_indexer`: a function which accepts an `%Edge{}` and returns a unique identifier of said edge. + Defaults to `Graph.Utils.edge_label/1`, the edge label itself. + + ### Multigraph Edge Indexing + + Indexing edges trades space for time to access only edges of a kind. + + When `multigraph: true` is enabled the `edge_indexer` of the graph is used to build a a set of edge keys (`{vertex_id, vertex_id}`) under a separate key. + + This can be a useful trade-off when traversing a graph where many different kinds of edges exist between the same vertices and + you want to avoid iterating over the set of all edges. I.e. a [multigraph](https://en.wikipedia.org/wiki/Multigraph). + The index provides allows map access time to to a set of edges when managing the graph. + + By default edges are indexed by the label but only when multigraph is toggled true. ## Example @@ -91,7 +120,15 @@ defmodule Graph do def new(opts \\ []) do type = Keyword.get(opts, :type) || :directed vertex_identifier = Keyword.get(opts, :vertex_identifier) || (&Graph.Utils.vertex_id/1) - %__MODULE__{type: type, vertex_identifier: vertex_identifier} + edge_indexer = Keyword.get(opts, :edge_indexer) || (&Graph.Utils.edge_label/1) + multigraph = Keyword.get(opts, :multigraph, false) + + %__MODULE__{ + type: type, + vertex_identifier: vertex_identifier, + edge_indexer: edge_indexer, + multigraph: multigraph + } end @doc """ diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex index f53b987..b5a8f5d 100644 --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -110,4 +110,6 @@ defmodule Graph.Utils do # 2^32 @max_phash 4_294_967_296 def vertex_id(v), do: :erlang.phash2(v, @max_phash) + + def edge_label(%{label: label}), do: label end diff --git a/test/graph_test.exs b/test/graph_test.exs index 2b73f02..63cb97c 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -20,6 +20,43 @@ defmodule GraphTest do assert Graph.has_vertex?(g_with_custom_vertex_identifier, :v1) end + describe "multigraphs" do + test "`multigraph: true` option enables vertex indexing on edge labels" do + graph = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + assert Enum.count(Graph.out_edges(graph, :a)) == 3 + assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) + assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, :foo) + assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, :bar) + assert [%Edge{label: :nil}] = Graph.out_edges(graph, :a, nil) + assert [] == Graph.out_edges(graph, :a, :foobar) + end + + test "custom vertex indexing function on edge labels" do + + end + + test "traversal using indexed labels" do + + end + + test "updating edge properties" do + + end + + test "removing edges" do + + end + end + test "delete vertex" do g = Graph.new() g = Graph.add_vertex(g, :v1, :labelA) From 3b5d4c465a814c212742530ddd481331d1975e8c Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 26 Aug 2024 19:55:53 -0600 Subject: [PATCH 02/18] wip --- .formatter.exs | 0 .github/workflows/elixir.yml | 0 .gitignore | 0 .travis.yml | 0 LICENSE | 0 Makefile | 0 README.md | 0 bench/cliques.exs | 0 bench/create.exs | 0 bench/k_core.exs | 0 bench/shortest_path.exs | 0 bench/topsort.exs | 0 coveralls.json | 0 lib/edge.ex | 17 ++-- lib/graph.ex | 84 ++++++++++++++++--- lib/graph/directed.ex | 0 lib/graph/edge_specification_error.ex | 0 lib/graph/inspect.ex | 0 lib/graph/pathfinding.ex | 3 +- lib/graph/pathfindings/bellman_ford.ex | 0 lib/graph/reducer.ex | 0 lib/graph/reducers/bfs.ex | 0 lib/graph/reducers/dfs.ex | 0 lib/graph/serializer.ex | 0 lib/graph/serializers/dot.ex | 4 +- lib/graph/serializers/edgelist.ex | 0 lib/graph/serializers/flowchart.ex | 0 lib/graph/utils.ex | 6 +- lib/priority_queue.ex | 0 mix.exs | 0 mix.lock | 0 test/fixtures/email-Enron.txt | 0 .../README.petster-friendships-hamster | 0 test/fixtures/petster/edges.txt | 0 test/fixtures/petster/metadata.txt | 0 test/fixtures/petster/vertices.txt | 0 test/graph_test.exs | 26 ++++-- test/model_test.exs | 0 test/priority_queue_test.exs | 0 test/reducer_test.exs | 24 +++--- test/serializer_test.exs | 0 test/support/generators.ex | 0 test/support/parser.ex | 0 test/test_helper.exs | 0 test/utils_test.exs | 0 45 files changed, 122 insertions(+), 42 deletions(-) mode change 100644 => 100755 .formatter.exs mode change 100644 => 100755 .github/workflows/elixir.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .travis.yml mode change 100644 => 100755 LICENSE mode change 100644 => 100755 Makefile mode change 100644 => 100755 README.md mode change 100644 => 100755 bench/cliques.exs mode change 100644 => 100755 bench/create.exs mode change 100644 => 100755 bench/k_core.exs mode change 100644 => 100755 bench/shortest_path.exs mode change 100644 => 100755 bench/topsort.exs mode change 100644 => 100755 coveralls.json mode change 100644 => 100755 lib/edge.ex mode change 100644 => 100755 lib/graph.ex mode change 100644 => 100755 lib/graph/directed.ex mode change 100644 => 100755 lib/graph/edge_specification_error.ex mode change 100644 => 100755 lib/graph/inspect.ex mode change 100644 => 100755 lib/graph/pathfinding.ex mode change 100644 => 100755 lib/graph/pathfindings/bellman_ford.ex mode change 100644 => 100755 lib/graph/reducer.ex mode change 100644 => 100755 lib/graph/reducers/bfs.ex mode change 100644 => 100755 lib/graph/reducers/dfs.ex mode change 100644 => 100755 lib/graph/serializer.ex mode change 100644 => 100755 lib/graph/serializers/dot.ex mode change 100644 => 100755 lib/graph/serializers/edgelist.ex mode change 100644 => 100755 lib/graph/serializers/flowchart.ex mode change 100644 => 100755 lib/graph/utils.ex mode change 100644 => 100755 lib/priority_queue.ex mode change 100644 => 100755 mix.exs mode change 100644 => 100755 mix.lock mode change 100644 => 100755 test/fixtures/email-Enron.txt mode change 100644 => 100755 test/fixtures/petster/README.petster-friendships-hamster mode change 100644 => 100755 test/fixtures/petster/edges.txt mode change 100644 => 100755 test/fixtures/petster/metadata.txt mode change 100644 => 100755 test/fixtures/petster/vertices.txt mode change 100644 => 100755 test/graph_test.exs mode change 100644 => 100755 test/model_test.exs mode change 100644 => 100755 test/priority_queue_test.exs mode change 100644 => 100755 test/reducer_test.exs mode change 100644 => 100755 test/serializer_test.exs mode change 100644 => 100755 test/support/generators.ex mode change 100644 => 100755 test/support/parser.ex mode change 100644 => 100755 test/test_helper.exs mode change 100644 => 100755 test/utils_test.exs diff --git a/.formatter.exs b/.formatter.exs old mode 100644 new mode 100755 diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/bench/cliques.exs b/bench/cliques.exs old mode 100644 new mode 100755 diff --git a/bench/create.exs b/bench/create.exs old mode 100644 new mode 100755 diff --git a/bench/k_core.exs b/bench/k_core.exs old mode 100644 new mode 100755 diff --git a/bench/shortest_path.exs b/bench/shortest_path.exs old mode 100644 new mode 100755 diff --git a/bench/topsort.exs b/bench/topsort.exs old mode 100644 new mode 100755 diff --git a/coveralls.json b/coveralls.json old mode 100644 new mode 100755 diff --git a/lib/edge.ex b/lib/edge.ex old mode 100644 new mode 100755 index d6cec16..c1908ac --- a/lib/edge.ex +++ b/lib/edge.ex @@ -21,6 +21,7 @@ defmodule Graph.Edge do @type edge_opt :: {:weight, integer | float} | {:label, term} + | {:properties, map} @type edge_opts :: [edge_opt] @doc """ @@ -38,11 +39,14 @@ defmodule Graph.Edge do @spec new(Graph.vertex(), Graph.vertex()) :: t @spec new(Graph.vertex(), Graph.vertex(), [edge_opt]) :: t | no_return def new(v1, v2, opts \\ []) when is_list(opts) do + {weight, opts} = Keyword.pop(opts, :weight, 1) + {label, opts} = Keyword.pop(opts, :label) + %__MODULE__{ v1: v1, v2: v2, - weight: Keyword.get(opts, :weight, 1), - label: Keyword.get(opts, :label), + weight: weight, + label: label, properties: Keyword.get(opts, :properties, %{}) } end @@ -51,12 +55,13 @@ defmodule Graph.Edge do def options_to_meta(opts) when is_list(opts) do label = Keyword.get(opts, :label) weight = Keyword.get(opts, :weight, 1) + properties = Keyword.get(opts, :properties, %{}) - case {label, weight} do - {_, w} = meta when is_number(w) -> - meta + case {label, %{weight: weight, properties: properties}} do + {label, %{weight: w} = meta} when is_number(w) -> + {label, meta} - {_, _} -> + _ -> raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex old mode 100644 new mode 100755 index 7e25fd3..8349484 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -50,7 +50,8 @@ defmodule Graph do @type edge_index_key :: label | term @type edge_properties :: %{ label: label, - weight: edge_weight + weight: edge_weight, + properties: map } @type edge_value :: %{label => edge_properties()} @type graph_type :: :directed | :undirected @@ -509,8 +510,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v2, v, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v2, v, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -524,8 +529,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v, v2, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v2, v, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -627,8 +636,8 @@ defmodule Graph do v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, {:ok, edge_meta} <- Map.fetch(meta, edge_key), - {:ok, weight} <- Map.fetch(edge_meta, label) do - Edge.new(v1, v2, label: label, weight: weight) + {:ok, %{weight: weight, properties: properties}} <- Map.fetch(edge_meta, label) do + Edge.new(v1, v2, label: label, weight: weight, properties: properties) else _ -> nil @@ -1034,8 +1043,24 @@ defmodule Graph do end edge_meta = Map.get(meta, {v1_id, v2_id}, %{}) - {label, weight} = Edge.options_to_meta(opts) - edge_meta = Map.put(edge_meta, label, weight) + {label, options_meta} = Edge.options_to_meta(opts) + edge_meta = Map.put(edge_meta, label, options_meta) + + g = + if g.multigraph do + edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) + + partition = g.edge_indexer.(edge) + key = {v1_id, partition} + set = Map.get(g.edge_index, key, MapSet.new()) + + %__MODULE__{ + g + | edge_index: Map.put(g.edge_index, key, MapSet.put(set, {v1_id, v2_id})) + } + else + g + end %__MODULE__{ g @@ -1050,7 +1075,7 @@ defmodule Graph do in a few different ways to make it easy to generate graphs succinctly. Edges must be provided as a list of `Edge` structs, `{vertex, vertex}` pairs, or - `{vertex, vertex, edge_opts :: [label: term, weight: integer]}`. + `{vertex, vertex, edge_opts :: [label: term, weight: integer, properties: map]}`. See the docs for `Graph.Edge.new/2` or `Graph.Edge.new/3` for more info on creating Edge structs, and `add_edge/3` for information on edge options. @@ -2242,8 +2267,12 @@ defmodule Graph do Enum.flat_map(v_out, fn v2_id -> v2 = Map.get(vs, v2_id) - Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, weight} -> - Edge.new(v, v2, label: label, weight: weight) + Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, edge_meta} -> + Edge.new(v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) else @@ -2252,6 +2281,37 @@ defmodule Graph do end end + def out_edges( + %__MODULE__{ + vertices: vs, + edges: edges, + multigraph: true, + edge_index: edge_index, + vertex_identifier: vertex_identifier, + edge_indexer: edge_indexer + }, + v, + partition + ) do + v1_id = vertex_identifier.(v) + key = {v1_id, partition} + # only return out_edges for which the index key returns a subset + edge_index + |> Map.get(key, MapSet.new()) + |> Enum.flat_map(fn {_v1_id, v2_id} = edge_key -> + v2 = Map.get(vs, v2_id) + + edges + |> Map.get(edge_key, []) + |> Enum.map(fn {label, edge_meta} -> + Edge.new(v, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + end) + |> Enum.filter(fn edge -> + edge_indexer.(edge) == partition + end) + end) + end + @doc """ Builds a maximal subgraph of `g` which includes all of the vertices in `vs` and the edges which connect them. diff --git a/lib/graph/directed.ex b/lib/graph/directed.ex old mode 100644 new mode 100755 diff --git a/lib/graph/edge_specification_error.ex b/lib/graph/edge_specification_error.ex old mode 100644 new mode 100755 diff --git a/lib/graph/inspect.ex b/lib/graph/inspect.ex old mode 100644 new mode 100755 diff --git a/lib/graph/pathfinding.ex b/lib/graph/pathfinding.ex old mode 100644 new mode 100755 index c7f7fa6..b21724a --- a/lib/graph/pathfinding.ex +++ b/lib/graph/pathfinding.ex @@ -6,7 +6,8 @@ defmodule Graph.Pathfinding do @type heuristic_fun :: (Graph.vertex() -> integer) - @spec bellman_ford(Graph.t, Graph.vertex) :: %{Graph.vertex() => integer() | :infinity} | nil + @spec bellman_ford(Graph.t(), Graph.vertex()) :: + %{Graph.vertex() => integer() | :infinity} | nil def bellman_ford(g, a), do: Graph.Pathfindings.BellmanFord.call(g, a) @doc """ diff --git a/lib/graph/pathfindings/bellman_ford.ex b/lib/graph/pathfindings/bellman_ford.ex old mode 100644 new mode 100755 diff --git a/lib/graph/reducer.ex b/lib/graph/reducer.ex old mode 100644 new mode 100755 diff --git a/lib/graph/reducers/bfs.ex b/lib/graph/reducers/bfs.ex old mode 100644 new mode 100755 diff --git a/lib/graph/reducers/dfs.ex b/lib/graph/reducers/dfs.ex old mode 100644 new mode 100755 diff --git a/lib/graph/serializer.ex b/lib/graph/serializer.ex old mode 100644 new mode 100755 diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex old mode 100644 new mode 100755 index 0df88c7..2266795 --- a/lib/graph/serializers/dot.ex +++ b/lib/graph/serializers/dot.ex @@ -14,7 +14,9 @@ defmodule Graph.Serializers.DOT do defp serialize_nodes(%Graph{vertices: vertices} = g) do Enum.reduce(vertices, "", fn {id, v}, acc -> - acc <> Serializer.indent(1) <> "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" + acc <> + Serializer.indent(1) <> + "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" end) end diff --git a/lib/graph/serializers/edgelist.ex b/lib/graph/serializers/edgelist.ex old mode 100644 new mode 100755 diff --git a/lib/graph/serializers/flowchart.ex b/lib/graph/serializers/flowchart.ex old mode 100644 new mode 100755 diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex old mode 100644 new mode 100755 index b5a8f5d..2880961 --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -83,7 +83,7 @@ defmodule Graph.Utils do def edge_weight(%Graph{type: :directed, edges: meta}, a, b) do Map.fetch!(meta, {a, b}) - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end @@ -96,13 +96,13 @@ defmodule Graph.Utils do edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end end diff --git a/lib/priority_queue.ex b/lib/priority_queue.ex old mode 100644 new mode 100755 diff --git a/mix.exs b/mix.exs old mode 100644 new mode 100755 diff --git a/mix.lock b/mix.lock old mode 100644 new mode 100755 diff --git a/test/fixtures/email-Enron.txt b/test/fixtures/email-Enron.txt old mode 100644 new mode 100755 diff --git a/test/fixtures/petster/README.petster-friendships-hamster b/test/fixtures/petster/README.petster-friendships-hamster old mode 100644 new mode 100755 diff --git a/test/fixtures/petster/edges.txt b/test/fixtures/petster/edges.txt old mode 100644 new mode 100755 diff --git a/test/fixtures/petster/metadata.txt b/test/fixtures/petster/metadata.txt old mode 100644 new mode 100755 diff --git a/test/fixtures/petster/vertices.txt b/test/fixtures/petster/vertices.txt old mode 100644 new mode 100755 diff --git a/test/graph_test.exs b/test/graph_test.exs old mode 100644 new mode 100755 index 63cb97c..e1a9b00 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -31,29 +31,39 @@ defmodule GraphTest do {:b, :c, weight: 3}, {:b, :a, label: {:complex, :label}} ]) + |> IO.inspect(structs: false) assert Enum.count(Graph.out_edges(graph, :a)) == 3 assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, :foo) assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, :bar) - assert [%Edge{label: :nil}] = Graph.out_edges(graph, :a, nil) + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, nil) assert [] == Graph.out_edges(graph, :a, :foobar) end - test "custom vertex indexing function on edge labels" do + test "custom edge indexing function" do + graph = + Graph.new(multigraph: true, edge_indexer: fn edge -> edge.weight end) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, weight: 6} + ]) + assert Enum.count(Graph.out_edges(graph, :b)) == 2 + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 6) + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 3) end - test "traversal using indexed labels" do - + test "traversal using indexed keys" do end - test "updating edge properties" do - + test "edge properties" do end test "removing edges" do - end end @@ -673,7 +683,7 @@ defmodule GraphTest do end defp build_complex_signed_graph do - Graph.new + Graph.new() |> Graph.add_edge(:a, :b, weight: -1) |> Graph.add_edge(:b, :e, weight: 2) |> Graph.add_edge(:e, :d, weight: -3) diff --git a/test/model_test.exs b/test/model_test.exs old mode 100644 new mode 100755 diff --git a/test/priority_queue_test.exs b/test/priority_queue_test.exs old mode 100644 new mode 100755 diff --git a/test/reducer_test.exs b/test/reducer_test.exs old mode 100644 new mode 100755 index 713e32f..428024f --- a/test/reducer_test.exs +++ b/test/reducer_test.exs @@ -35,17 +35,19 @@ defmodule Graph.Reducer.Test do end test "can walk a graph breadth-first, when the starting points had their in-edges deleted" do - g = Graph.new - |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) - |> Graph.add_edge(:a, :b) - |> Graph.add_edge(:a, :d) - |> Graph.add_edge(:b, :c) - |> Graph.add_edge(:b, :d) - |> Graph.add_edge(:c, :e) - |> Graph.add_edge(:d, :f) - |> Graph.add_edge(:f, :g) - |> Graph.add_edge(:b, :a) # Add this edge and then remove it - |> Graph.delete_edge(:b, :a) + g = + Graph.new() + |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:a, :d) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:b, :d) + |> Graph.add_edge(:c, :e) + |> Graph.add_edge(:d, :f) + |> Graph.add_edge(:f, :g) + # Add this edge and then remove it + |> Graph.add_edge(:b, :a) + |> Graph.delete_edge(:b, :a) expected = [:a, :b, :d, :c, :f, :e, :g] assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end) diff --git a/test/serializer_test.exs b/test/serializer_test.exs old mode 100644 new mode 100755 diff --git a/test/support/generators.ex b/test/support/generators.ex old mode 100644 new mode 100755 diff --git a/test/support/parser.ex b/test/support/parser.ex old mode 100644 new mode 100755 diff --git a/test/test_helper.exs b/test/test_helper.exs old mode 100644 new mode 100755 diff --git a/test/utils_test.exs b/test/utils_test.exs old mode 100644 new mode 100755 From 8a23cd3fa8e886f2a9d7bf4027d6aaf1d1ca6595 Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 26 Aug 2024 20:07:46 -0600 Subject: [PATCH 03/18] wip --- .gitignore | 0 lib/edge.ex | 17 +++----- lib/graph.ex | 84 ++++++------------------------------ lib/graph/pathfinding.ex | 3 +- lib/graph/serializers/dot.ex | 4 +- lib/graph/utils.ex | 6 +-- test/graph_test.exs | 26 ++++------- test/reducer_test.exs | 24 +++++------ 8 files changed, 42 insertions(+), 122 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 lib/edge.ex mode change 100755 => 100644 lib/graph.ex mode change 100755 => 100644 lib/graph/pathfinding.ex mode change 100755 => 100644 lib/graph/serializers/dot.ex mode change 100755 => 100644 lib/graph/utils.ex mode change 100755 => 100644 test/graph_test.exs mode change 100755 => 100644 test/reducer_test.exs diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/lib/edge.ex b/lib/edge.ex old mode 100755 new mode 100644 index c1908ac..d6cec16 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -21,7 +21,6 @@ defmodule Graph.Edge do @type edge_opt :: {:weight, integer | float} | {:label, term} - | {:properties, map} @type edge_opts :: [edge_opt] @doc """ @@ -39,14 +38,11 @@ defmodule Graph.Edge do @spec new(Graph.vertex(), Graph.vertex()) :: t @spec new(Graph.vertex(), Graph.vertex(), [edge_opt]) :: t | no_return def new(v1, v2, opts \\ []) when is_list(opts) do - {weight, opts} = Keyword.pop(opts, :weight, 1) - {label, opts} = Keyword.pop(opts, :label) - %__MODULE__{ v1: v1, v2: v2, - weight: weight, - label: label, + weight: Keyword.get(opts, :weight, 1), + label: Keyword.get(opts, :label), properties: Keyword.get(opts, :properties, %{}) } end @@ -55,13 +51,12 @@ defmodule Graph.Edge do def options_to_meta(opts) when is_list(opts) do label = Keyword.get(opts, :label) weight = Keyword.get(opts, :weight, 1) - properties = Keyword.get(opts, :properties, %{}) - case {label, %{weight: weight, properties: properties}} do - {label, %{weight: w} = meta} when is_number(w) -> - {label, meta} + case {label, weight} do + {_, w} = meta when is_number(w) -> + meta - _ -> + {_, _} -> raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex old mode 100755 new mode 100644 index 8349484..7e25fd3 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -50,8 +50,7 @@ defmodule Graph do @type edge_index_key :: label | term @type edge_properties :: %{ label: label, - weight: edge_weight, - properties: map + weight: edge_weight } @type edge_value :: %{label => edge_properties()} @type graph_type :: :directed | :undirected @@ -510,12 +509,8 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, meta_value} <- edge_meta do - Edge.new(v2, v, - label: label, - weight: meta_value.weight, - properties: meta_value.properties - ) + for {label, weight} <- edge_meta do + Edge.new(v2, v, label: label, weight: weight) end end end) @@ -529,12 +524,8 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, meta_value} <- edge_meta do - Edge.new(v2, v, - label: label, - weight: meta_value.weight, - properties: meta_value.properties - ) + for {label, weight} <- edge_meta do + Edge.new(v, v2, label: label, weight: weight) end end end) @@ -636,8 +627,8 @@ defmodule Graph do v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, {:ok, edge_meta} <- Map.fetch(meta, edge_key), - {:ok, %{weight: weight, properties: properties}} <- Map.fetch(edge_meta, label) do - Edge.new(v1, v2, label: label, weight: weight, properties: properties) + {:ok, weight} <- Map.fetch(edge_meta, label) do + Edge.new(v1, v2, label: label, weight: weight) else _ -> nil @@ -1043,24 +1034,8 @@ defmodule Graph do end edge_meta = Map.get(meta, {v1_id, v2_id}, %{}) - {label, options_meta} = Edge.options_to_meta(opts) - edge_meta = Map.put(edge_meta, label, options_meta) - - g = - if g.multigraph do - edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) - - partition = g.edge_indexer.(edge) - key = {v1_id, partition} - set = Map.get(g.edge_index, key, MapSet.new()) - - %__MODULE__{ - g - | edge_index: Map.put(g.edge_index, key, MapSet.put(set, {v1_id, v2_id})) - } - else - g - end + {label, weight} = Edge.options_to_meta(opts) + edge_meta = Map.put(edge_meta, label, weight) %__MODULE__{ g @@ -1075,7 +1050,7 @@ defmodule Graph do in a few different ways to make it easy to generate graphs succinctly. Edges must be provided as a list of `Edge` structs, `{vertex, vertex}` pairs, or - `{vertex, vertex, edge_opts :: [label: term, weight: integer, properties: map]}`. + `{vertex, vertex, edge_opts :: [label: term, weight: integer]}`. See the docs for `Graph.Edge.new/2` or `Graph.Edge.new/3` for more info on creating Edge structs, and `add_edge/3` for information on edge options. @@ -2267,12 +2242,8 @@ defmodule Graph do Enum.flat_map(v_out, fn v2_id -> v2 = Map.get(vs, v2_id) - Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, edge_meta} -> - Edge.new(v, v2, - label: label, - weight: edge_meta.weight, - properties: edge_meta.properties - ) + Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, weight} -> + Edge.new(v, v2, label: label, weight: weight) end) end) else @@ -2281,37 +2252,6 @@ defmodule Graph do end end - def out_edges( - %__MODULE__{ - vertices: vs, - edges: edges, - multigraph: true, - edge_index: edge_index, - vertex_identifier: vertex_identifier, - edge_indexer: edge_indexer - }, - v, - partition - ) do - v1_id = vertex_identifier.(v) - key = {v1_id, partition} - # only return out_edges for which the index key returns a subset - edge_index - |> Map.get(key, MapSet.new()) - |> Enum.flat_map(fn {_v1_id, v2_id} = edge_key -> - v2 = Map.get(vs, v2_id) - - edges - |> Map.get(edge_key, []) - |> Enum.map(fn {label, edge_meta} -> - Edge.new(v, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) - end) - |> Enum.filter(fn edge -> - edge_indexer.(edge) == partition - end) - end) - end - @doc """ Builds a maximal subgraph of `g` which includes all of the vertices in `vs` and the edges which connect them. diff --git a/lib/graph/pathfinding.ex b/lib/graph/pathfinding.ex old mode 100755 new mode 100644 index b21724a..c7f7fa6 --- a/lib/graph/pathfinding.ex +++ b/lib/graph/pathfinding.ex @@ -6,8 +6,7 @@ defmodule Graph.Pathfinding do @type heuristic_fun :: (Graph.vertex() -> integer) - @spec bellman_ford(Graph.t(), Graph.vertex()) :: - %{Graph.vertex() => integer() | :infinity} | nil + @spec bellman_ford(Graph.t, Graph.vertex) :: %{Graph.vertex() => integer() | :infinity} | nil def bellman_ford(g, a), do: Graph.Pathfindings.BellmanFord.call(g, a) @doc """ diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex old mode 100755 new mode 100644 index 2266795..0df88c7 --- a/lib/graph/serializers/dot.ex +++ b/lib/graph/serializers/dot.ex @@ -14,9 +14,7 @@ defmodule Graph.Serializers.DOT do defp serialize_nodes(%Graph{vertices: vertices} = g) do Enum.reduce(vertices, "", fn {id, v}, acc -> - acc <> - Serializer.indent(1) <> - "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" + acc <> Serializer.indent(1) <> "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" end) end diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex old mode 100755 new mode 100644 index 2880961..b5a8f5d --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -83,7 +83,7 @@ defmodule Graph.Utils do def edge_weight(%Graph{type: :directed, edges: meta}, a, b) do Map.fetch!(meta, {a, b}) - |> Enum.map(fn {_label, %{weight: weight}} -> weight end) + |> Enum.map(fn {_label, weight} -> weight end) |> Enum.min() end @@ -96,13 +96,13 @@ defmodule Graph.Utils do edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, %{weight: weight}} -> weight end) + |> Enum.map(fn {_label, weight} -> weight end) |> Enum.min() end edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, %{weight: weight}} -> weight end) + |> Enum.map(fn {_label, weight} -> weight end) |> Enum.min() end end diff --git a/test/graph_test.exs b/test/graph_test.exs old mode 100755 new mode 100644 index e1a9b00..63cb97c --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -31,39 +31,29 @@ defmodule GraphTest do {:b, :c, weight: 3}, {:b, :a, label: {:complex, :label}} ]) - |> IO.inspect(structs: false) assert Enum.count(Graph.out_edges(graph, :a)) == 3 assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, :foo) assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, :bar) - assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, nil) + assert [%Edge{label: :nil}] = Graph.out_edges(graph, :a, nil) assert [] == Graph.out_edges(graph, :a, :foobar) end - test "custom edge indexing function" do - graph = - Graph.new(multigraph: true, edge_indexer: fn edge -> edge.weight end) - |> Graph.add_edges([ - {:a, :b}, - {:a, :b, label: :foo}, - {:a, :b, label: :bar}, - {:b, :c, weight: 3}, - {:b, :a, weight: 6} - ]) + test "custom vertex indexing function on edge labels" do - assert Enum.count(Graph.out_edges(graph, :b)) == 2 - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 6) - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 3) end - test "traversal using indexed keys" do + test "traversal using indexed labels" do + end - test "edge properties" do + test "updating edge properties" do + end test "removing edges" do + end end @@ -683,7 +673,7 @@ defmodule GraphTest do end defp build_complex_signed_graph do - Graph.new() + Graph.new |> Graph.add_edge(:a, :b, weight: -1) |> Graph.add_edge(:b, :e, weight: 2) |> Graph.add_edge(:e, :d, weight: -3) diff --git a/test/reducer_test.exs b/test/reducer_test.exs old mode 100755 new mode 100644 index 428024f..713e32f --- a/test/reducer_test.exs +++ b/test/reducer_test.exs @@ -35,19 +35,17 @@ defmodule Graph.Reducer.Test do end test "can walk a graph breadth-first, when the starting points had their in-edges deleted" do - g = - Graph.new() - |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) - |> Graph.add_edge(:a, :b) - |> Graph.add_edge(:a, :d) - |> Graph.add_edge(:b, :c) - |> Graph.add_edge(:b, :d) - |> Graph.add_edge(:c, :e) - |> Graph.add_edge(:d, :f) - |> Graph.add_edge(:f, :g) - # Add this edge and then remove it - |> Graph.add_edge(:b, :a) - |> Graph.delete_edge(:b, :a) + g = Graph.new + |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:a, :d) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:b, :d) + |> Graph.add_edge(:c, :e) + |> Graph.add_edge(:d, :f) + |> Graph.add_edge(:f, :g) + |> Graph.add_edge(:b, :a) # Add this edge and then remove it + |> Graph.delete_edge(:b, :a) expected = [:a, :b, :d, :c, :f, :e, :g] assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end) From 937494722efdddefffe79a99a49e9f20cd912ad1 Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 26 Aug 2024 20:09:08 -0600 Subject: [PATCH 04/18] Revert "wip" This reverts commit 8a23cd3fa8e886f2a9d7bf4027d6aaf1d1ca6595. --- .gitignore | 0 lib/edge.ex | 17 +++++--- lib/graph.ex | 84 ++++++++++++++++++++++++++++++------ lib/graph/pathfinding.ex | 3 +- lib/graph/serializers/dot.ex | 4 +- lib/graph/utils.ex | 6 +-- test/graph_test.exs | 26 +++++++---- test/reducer_test.exs | 24 ++++++----- 8 files changed, 122 insertions(+), 42 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 lib/edge.ex mode change 100644 => 100755 lib/graph.ex mode change 100644 => 100755 lib/graph/pathfinding.ex mode change 100644 => 100755 lib/graph/serializers/dot.ex mode change 100644 => 100755 lib/graph/utils.ex mode change 100644 => 100755 test/graph_test.exs mode change 100644 => 100755 test/reducer_test.exs diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/lib/edge.ex b/lib/edge.ex old mode 100644 new mode 100755 index d6cec16..c1908ac --- a/lib/edge.ex +++ b/lib/edge.ex @@ -21,6 +21,7 @@ defmodule Graph.Edge do @type edge_opt :: {:weight, integer | float} | {:label, term} + | {:properties, map} @type edge_opts :: [edge_opt] @doc """ @@ -38,11 +39,14 @@ defmodule Graph.Edge do @spec new(Graph.vertex(), Graph.vertex()) :: t @spec new(Graph.vertex(), Graph.vertex(), [edge_opt]) :: t | no_return def new(v1, v2, opts \\ []) when is_list(opts) do + {weight, opts} = Keyword.pop(opts, :weight, 1) + {label, opts} = Keyword.pop(opts, :label) + %__MODULE__{ v1: v1, v2: v2, - weight: Keyword.get(opts, :weight, 1), - label: Keyword.get(opts, :label), + weight: weight, + label: label, properties: Keyword.get(opts, :properties, %{}) } end @@ -51,12 +55,13 @@ defmodule Graph.Edge do def options_to_meta(opts) when is_list(opts) do label = Keyword.get(opts, :label) weight = Keyword.get(opts, :weight, 1) + properties = Keyword.get(opts, :properties, %{}) - case {label, weight} do - {_, w} = meta when is_number(w) -> - meta + case {label, %{weight: weight, properties: properties}} do + {label, %{weight: w} = meta} when is_number(w) -> + {label, meta} - {_, _} -> + _ -> raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex old mode 100644 new mode 100755 index 7e25fd3..8349484 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -50,7 +50,8 @@ defmodule Graph do @type edge_index_key :: label | term @type edge_properties :: %{ label: label, - weight: edge_weight + weight: edge_weight, + properties: map } @type edge_value :: %{label => edge_properties()} @type graph_type :: :directed | :undirected @@ -509,8 +510,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v2, v, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v2, v, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -524,8 +529,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v, v2, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v2, v, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -627,8 +636,8 @@ defmodule Graph do v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, {:ok, edge_meta} <- Map.fetch(meta, edge_key), - {:ok, weight} <- Map.fetch(edge_meta, label) do - Edge.new(v1, v2, label: label, weight: weight) + {:ok, %{weight: weight, properties: properties}} <- Map.fetch(edge_meta, label) do + Edge.new(v1, v2, label: label, weight: weight, properties: properties) else _ -> nil @@ -1034,8 +1043,24 @@ defmodule Graph do end edge_meta = Map.get(meta, {v1_id, v2_id}, %{}) - {label, weight} = Edge.options_to_meta(opts) - edge_meta = Map.put(edge_meta, label, weight) + {label, options_meta} = Edge.options_to_meta(opts) + edge_meta = Map.put(edge_meta, label, options_meta) + + g = + if g.multigraph do + edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) + + partition = g.edge_indexer.(edge) + key = {v1_id, partition} + set = Map.get(g.edge_index, key, MapSet.new()) + + %__MODULE__{ + g + | edge_index: Map.put(g.edge_index, key, MapSet.put(set, {v1_id, v2_id})) + } + else + g + end %__MODULE__{ g @@ -1050,7 +1075,7 @@ defmodule Graph do in a few different ways to make it easy to generate graphs succinctly. Edges must be provided as a list of `Edge` structs, `{vertex, vertex}` pairs, or - `{vertex, vertex, edge_opts :: [label: term, weight: integer]}`. + `{vertex, vertex, edge_opts :: [label: term, weight: integer, properties: map]}`. See the docs for `Graph.Edge.new/2` or `Graph.Edge.new/3` for more info on creating Edge structs, and `add_edge/3` for information on edge options. @@ -2242,8 +2267,12 @@ defmodule Graph do Enum.flat_map(v_out, fn v2_id -> v2 = Map.get(vs, v2_id) - Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, weight} -> - Edge.new(v, v2, label: label, weight: weight) + Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, edge_meta} -> + Edge.new(v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) else @@ -2252,6 +2281,37 @@ defmodule Graph do end end + def out_edges( + %__MODULE__{ + vertices: vs, + edges: edges, + multigraph: true, + edge_index: edge_index, + vertex_identifier: vertex_identifier, + edge_indexer: edge_indexer + }, + v, + partition + ) do + v1_id = vertex_identifier.(v) + key = {v1_id, partition} + # only return out_edges for which the index key returns a subset + edge_index + |> Map.get(key, MapSet.new()) + |> Enum.flat_map(fn {_v1_id, v2_id} = edge_key -> + v2 = Map.get(vs, v2_id) + + edges + |> Map.get(edge_key, []) + |> Enum.map(fn {label, edge_meta} -> + Edge.new(v, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + end) + |> Enum.filter(fn edge -> + edge_indexer.(edge) == partition + end) + end) + end + @doc """ Builds a maximal subgraph of `g` which includes all of the vertices in `vs` and the edges which connect them. diff --git a/lib/graph/pathfinding.ex b/lib/graph/pathfinding.ex old mode 100644 new mode 100755 index c7f7fa6..b21724a --- a/lib/graph/pathfinding.ex +++ b/lib/graph/pathfinding.ex @@ -6,7 +6,8 @@ defmodule Graph.Pathfinding do @type heuristic_fun :: (Graph.vertex() -> integer) - @spec bellman_ford(Graph.t, Graph.vertex) :: %{Graph.vertex() => integer() | :infinity} | nil + @spec bellman_ford(Graph.t(), Graph.vertex()) :: + %{Graph.vertex() => integer() | :infinity} | nil def bellman_ford(g, a), do: Graph.Pathfindings.BellmanFord.call(g, a) @doc """ diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex old mode 100644 new mode 100755 index 0df88c7..2266795 --- a/lib/graph/serializers/dot.ex +++ b/lib/graph/serializers/dot.ex @@ -14,7 +14,9 @@ defmodule Graph.Serializers.DOT do defp serialize_nodes(%Graph{vertices: vertices} = g) do Enum.reduce(vertices, "", fn {id, v}, acc -> - acc <> Serializer.indent(1) <> "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" + acc <> + Serializer.indent(1) <> + "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" end) end diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex old mode 100644 new mode 100755 index b5a8f5d..2880961 --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -83,7 +83,7 @@ defmodule Graph.Utils do def edge_weight(%Graph{type: :directed, edges: meta}, a, b) do Map.fetch!(meta, {a, b}) - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end @@ -96,13 +96,13 @@ defmodule Graph.Utils do edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end end diff --git a/test/graph_test.exs b/test/graph_test.exs old mode 100644 new mode 100755 index 63cb97c..e1a9b00 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -31,29 +31,39 @@ defmodule GraphTest do {:b, :c, weight: 3}, {:b, :a, label: {:complex, :label}} ]) + |> IO.inspect(structs: false) assert Enum.count(Graph.out_edges(graph, :a)) == 3 assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, :foo) assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, :bar) - assert [%Edge{label: :nil}] = Graph.out_edges(graph, :a, nil) + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, nil) assert [] == Graph.out_edges(graph, :a, :foobar) end - test "custom vertex indexing function on edge labels" do + test "custom edge indexing function" do + graph = + Graph.new(multigraph: true, edge_indexer: fn edge -> edge.weight end) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, weight: 6} + ]) + assert Enum.count(Graph.out_edges(graph, :b)) == 2 + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 6) + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 3) end - test "traversal using indexed labels" do - + test "traversal using indexed keys" do end - test "updating edge properties" do - + test "edge properties" do end test "removing edges" do - end end @@ -673,7 +683,7 @@ defmodule GraphTest do end defp build_complex_signed_graph do - Graph.new + Graph.new() |> Graph.add_edge(:a, :b, weight: -1) |> Graph.add_edge(:b, :e, weight: 2) |> Graph.add_edge(:e, :d, weight: -3) diff --git a/test/reducer_test.exs b/test/reducer_test.exs old mode 100644 new mode 100755 index 713e32f..428024f --- a/test/reducer_test.exs +++ b/test/reducer_test.exs @@ -35,17 +35,19 @@ defmodule Graph.Reducer.Test do end test "can walk a graph breadth-first, when the starting points had their in-edges deleted" do - g = Graph.new - |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) - |> Graph.add_edge(:a, :b) - |> Graph.add_edge(:a, :d) - |> Graph.add_edge(:b, :c) - |> Graph.add_edge(:b, :d) - |> Graph.add_edge(:c, :e) - |> Graph.add_edge(:d, :f) - |> Graph.add_edge(:f, :g) - |> Graph.add_edge(:b, :a) # Add this edge and then remove it - |> Graph.delete_edge(:b, :a) + g = + Graph.new() + |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:a, :d) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:b, :d) + |> Graph.add_edge(:c, :e) + |> Graph.add_edge(:d, :f) + |> Graph.add_edge(:f, :g) + # Add this edge and then remove it + |> Graph.add_edge(:b, :a) + |> Graph.delete_edge(:b, :a) expected = [:a, :b, :d, :c, :f, :e, :g] assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end) From 53a74f502a9fac27433fd50c3e1fbcda1a8621ca Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 26 Aug 2024 20:55:38 -0600 Subject: [PATCH 05/18] undo file permission --- .formatter.exs | 0 .github/workflows/elixir.yml | 0 .gitignore | 0 .travis.yml | 0 LICENSE | 0 Makefile | 0 README.md | 0 bench/cliques.exs | 0 bench/create.exs | 0 bench/k_core.exs | 0 bench/shortest_path.exs | 0 bench/topsort.exs | 0 coveralls.json | 0 lib/edge.ex | 0 lib/graph.ex | 0 lib/graph/directed.ex | 0 lib/graph/edge_specification_error.ex | 0 lib/graph/inspect.ex | 0 lib/graph/pathfinding.ex | 0 lib/graph/pathfindings/bellman_ford.ex | 0 lib/graph/reducer.ex | 0 lib/graph/reducers/bfs.ex | 0 lib/graph/reducers/dfs.ex | 0 lib/graph/serializer.ex | 0 lib/graph/serializers/dot.ex | 0 lib/graph/serializers/edgelist.ex | 0 lib/graph/serializers/flowchart.ex | 0 lib/graph/utils.ex | 0 lib/priority_queue.ex | 0 mix.exs | 0 mix.lock | 0 test/fixtures/email-Enron.txt | 0 test/fixtures/petster/README.petster-friendships-hamster | 0 test/fixtures/petster/edges.txt | 0 test/fixtures/petster/metadata.txt | 0 test/fixtures/petster/vertices.txt | 0 test/graph_test.exs | 0 test/model_test.exs | 0 test/priority_queue_test.exs | 0 test/reducer_test.exs | 0 test/serializer_test.exs | 0 test/support/generators.ex | 0 test/support/parser.ex | 0 test/test_helper.exs | 0 test/utils_test.exs | 0 45 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .formatter.exs mode change 100755 => 100644 .github/workflows/elixir.yml mode change 100755 => 100644 .gitignore mode change 100755 => 100644 .travis.yml mode change 100755 => 100644 LICENSE mode change 100755 => 100644 Makefile mode change 100755 => 100644 README.md mode change 100755 => 100644 bench/cliques.exs mode change 100755 => 100644 bench/create.exs mode change 100755 => 100644 bench/k_core.exs mode change 100755 => 100644 bench/shortest_path.exs mode change 100755 => 100644 bench/topsort.exs mode change 100755 => 100644 coveralls.json mode change 100755 => 100644 lib/edge.ex mode change 100755 => 100644 lib/graph.ex mode change 100755 => 100644 lib/graph/directed.ex mode change 100755 => 100644 lib/graph/edge_specification_error.ex mode change 100755 => 100644 lib/graph/inspect.ex mode change 100755 => 100644 lib/graph/pathfinding.ex mode change 100755 => 100644 lib/graph/pathfindings/bellman_ford.ex mode change 100755 => 100644 lib/graph/reducer.ex mode change 100755 => 100644 lib/graph/reducers/bfs.ex mode change 100755 => 100644 lib/graph/reducers/dfs.ex mode change 100755 => 100644 lib/graph/serializer.ex mode change 100755 => 100644 lib/graph/serializers/dot.ex mode change 100755 => 100644 lib/graph/serializers/edgelist.ex mode change 100755 => 100644 lib/graph/serializers/flowchart.ex mode change 100755 => 100644 lib/graph/utils.ex mode change 100755 => 100644 lib/priority_queue.ex mode change 100755 => 100644 mix.exs mode change 100755 => 100644 mix.lock mode change 100755 => 100644 test/fixtures/email-Enron.txt mode change 100755 => 100644 test/fixtures/petster/README.petster-friendships-hamster mode change 100755 => 100644 test/fixtures/petster/edges.txt mode change 100755 => 100644 test/fixtures/petster/metadata.txt mode change 100755 => 100644 test/fixtures/petster/vertices.txt mode change 100755 => 100644 test/graph_test.exs mode change 100755 => 100644 test/model_test.exs mode change 100755 => 100644 test/priority_queue_test.exs mode change 100755 => 100644 test/reducer_test.exs mode change 100755 => 100644 test/serializer_test.exs mode change 100755 => 100644 test/support/generators.ex mode change 100755 => 100644 test/support/parser.ex mode change 100755 => 100644 test/test_helper.exs mode change 100755 => 100644 test/utils_test.exs diff --git a/.formatter.exs b/.formatter.exs old mode 100755 new mode 100644 diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml old mode 100755 new mode 100644 diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.travis.yml b/.travis.yml old mode 100755 new mode 100644 diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/bench/cliques.exs b/bench/cliques.exs old mode 100755 new mode 100644 diff --git a/bench/create.exs b/bench/create.exs old mode 100755 new mode 100644 diff --git a/bench/k_core.exs b/bench/k_core.exs old mode 100755 new mode 100644 diff --git a/bench/shortest_path.exs b/bench/shortest_path.exs old mode 100755 new mode 100644 diff --git a/bench/topsort.exs b/bench/topsort.exs old mode 100755 new mode 100644 diff --git a/coveralls.json b/coveralls.json old mode 100755 new mode 100644 diff --git a/lib/edge.ex b/lib/edge.ex old mode 100755 new mode 100644 diff --git a/lib/graph.ex b/lib/graph.ex old mode 100755 new mode 100644 diff --git a/lib/graph/directed.ex b/lib/graph/directed.ex old mode 100755 new mode 100644 diff --git a/lib/graph/edge_specification_error.ex b/lib/graph/edge_specification_error.ex old mode 100755 new mode 100644 diff --git a/lib/graph/inspect.ex b/lib/graph/inspect.ex old mode 100755 new mode 100644 diff --git a/lib/graph/pathfinding.ex b/lib/graph/pathfinding.ex old mode 100755 new mode 100644 diff --git a/lib/graph/pathfindings/bellman_ford.ex b/lib/graph/pathfindings/bellman_ford.ex old mode 100755 new mode 100644 diff --git a/lib/graph/reducer.ex b/lib/graph/reducer.ex old mode 100755 new mode 100644 diff --git a/lib/graph/reducers/bfs.ex b/lib/graph/reducers/bfs.ex old mode 100755 new mode 100644 diff --git a/lib/graph/reducers/dfs.ex b/lib/graph/reducers/dfs.ex old mode 100755 new mode 100644 diff --git a/lib/graph/serializer.ex b/lib/graph/serializer.ex old mode 100755 new mode 100644 diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex old mode 100755 new mode 100644 diff --git a/lib/graph/serializers/edgelist.ex b/lib/graph/serializers/edgelist.ex old mode 100755 new mode 100644 diff --git a/lib/graph/serializers/flowchart.ex b/lib/graph/serializers/flowchart.ex old mode 100755 new mode 100644 diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex old mode 100755 new mode 100644 diff --git a/lib/priority_queue.ex b/lib/priority_queue.ex old mode 100755 new mode 100644 diff --git a/mix.exs b/mix.exs old mode 100755 new mode 100644 diff --git a/mix.lock b/mix.lock old mode 100755 new mode 100644 diff --git a/test/fixtures/email-Enron.txt b/test/fixtures/email-Enron.txt old mode 100755 new mode 100644 diff --git a/test/fixtures/petster/README.petster-friendships-hamster b/test/fixtures/petster/README.petster-friendships-hamster old mode 100755 new mode 100644 diff --git a/test/fixtures/petster/edges.txt b/test/fixtures/petster/edges.txt old mode 100755 new mode 100644 diff --git a/test/fixtures/petster/metadata.txt b/test/fixtures/petster/metadata.txt old mode 100755 new mode 100644 diff --git a/test/fixtures/petster/vertices.txt b/test/fixtures/petster/vertices.txt old mode 100755 new mode 100644 diff --git a/test/graph_test.exs b/test/graph_test.exs old mode 100755 new mode 100644 diff --git a/test/model_test.exs b/test/model_test.exs old mode 100755 new mode 100644 diff --git a/test/priority_queue_test.exs b/test/priority_queue_test.exs old mode 100755 new mode 100644 diff --git a/test/reducer_test.exs b/test/reducer_test.exs old mode 100755 new mode 100644 diff --git a/test/serializer_test.exs b/test/serializer_test.exs old mode 100755 new mode 100644 diff --git a/test/support/generators.ex b/test/support/generators.ex old mode 100755 new mode 100644 diff --git a/test/support/parser.ex b/test/support/parser.ex old mode 100755 new mode 100644 diff --git a/test/test_helper.exs b/test/test_helper.exs old mode 100755 new mode 100644 diff --git a/test/utils_test.exs b/test/utils_test.exs old mode 100755 new mode 100644 From 7a14cd51496afd675cd74a85f7916b5f4c326a60 Mon Sep 17 00:00:00 2001 From: zack Date: Sat, 31 Aug 2024 09:45:05 -0600 Subject: [PATCH 06/18] match weights from adjusted meta --- lib/edge.ex | 3 +- lib/graph.ex | 48 +++++++++++++++----------- lib/graph/pathfindings/bellman_ford.ex | 3 +- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/edge.ex b/lib/edge.ex index c1908ac..324c4e0 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -61,7 +61,8 @@ defmodule Graph.Edge do {label, %{weight: w} = meta} when is_number(w) -> {label, meta} - _ -> + other -> + IO.inspect(other) raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex index 8349484..210570d 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -465,8 +465,8 @@ defmodule Graph do target = Map.get(vs, out_neighbor) meta = Map.get(meta, {source_id, out_neighbor}) - Enum.map(meta, fn {label, weight} -> - Edge.new(source, target, label: label, weight: weight) + Enum.map(meta, fn {label, %{weight: weight, properties: properties}} -> + Edge.new(source, target, label: label, weight: weight, properties: properties) end) end) end) @@ -555,7 +555,7 @@ defmodule Graph do iex> g = Graph.new(type: :undirected) |> Graph.add_edge(:a, :b, label: :uses) ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :contains}, %Graph.Edge{v1: :a, v2: :b, label: :uses}] + [%Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}, %Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}] """ @spec edges(t, vertex, vertex) :: [Edge.t()] def edges(%__MODULE__{type: type, edges: meta, vertex_identifier: vertex_identifier}, v1, v2) do @@ -577,18 +577,18 @@ defmodule Graph do end defp edge_list(v1, v2, edge_meta, :undirected) do - for {label, weight} <- edge_meta do + for {label, %{weight: weight, properties: properties}} <- edge_meta do if v1 > v2 do - Edge.new(v2, v1, label: label, weight: weight) + Edge.new(v2, v1, label: label, weight: weight, properties: properties) else - Edge.new(v1, v2, label: label, weight: weight) + Edge.new(v1, v2, label: label, weight: weight, properties: properties) end end end defp edge_list(v1, v2, edge_meta, _) do - for {label, weight} <- edge_meta do - Edge.new(v1, v2, label: label, weight: weight) + for {label, %{weight: weight, properties: properties}} <- edge_meta do + Edge.new(v1, v2, label: label, weight: weight, properties: properties) end end @@ -1169,10 +1169,10 @@ defmodule Graph do g = add_vertex(g, v3) - Enum.reduce(meta, g, fn {label, weight}, acc -> + Enum.reduce(meta, g, fn {label, %{weight: weight, properties: properties}}, acc -> acc - |> add_edge(v1, v3, label: label, weight: weight) - |> add_edge(v3, v2, label: label, weight: weight) + |> add_edge(v1, v3, label: label, weight: weight, properties: properties) + |> add_edge(v3, v2, label: label, weight: weight, properties: properties) end) else _ -> {:error, :no_such_edge} @@ -1241,18 +1241,18 @@ defmodule Graph do edge_key <- {v1_id, v2_id}, {:ok, meta} <- Map.fetch(em, edge_key), {:ok, _} <- Map.fetch(meta, old_label), - {new_label, new_weight} <- Edge.options_to_meta(opts) do + {new_label, new_attrs} <- Edge.options_to_meta(opts) do case new_label do ^old_label -> - new_meta = Map.put(meta, old_label, new_weight) + new_meta = Map.put(meta, old_label, new_attrs) %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} nil -> - new_meta = Map.put(meta, old_label, new_weight) + new_meta = Map.put(meta, old_label, new_attrs) %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} _ -> - new_meta = Map.put(Map.delete(meta, old_label), new_label, new_weight) + new_meta = Map.put(Map.delete(meta, old_label), new_label, new_attrs) %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} end else @@ -2200,8 +2200,12 @@ defmodule Graph do Enum.flat_map(v_in, fn v1_id -> v1 = Map.get(vs, v1_id) - Enum.map(Map.get(meta, {v1_id, v_id}), fn {label, weight} -> - Edge.new(v1, v, label: label, weight: weight) + Enum.map(Map.get(meta, {v1_id, v_id}), fn {label, edge_meta} -> + Edge.new(v1, v, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) else @@ -2246,7 +2250,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:b, :c}]) ...> Graph.out_edges(g, :a) - [%Graph.Edge{v1: :a, v2: :b, label: :foo}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, label: :foo, properties: %{}}, %Graph.Edge{v1: :a, v2: :b}] """ @spec out_edges(t, vertex) :: Edge.t() def out_edges(%__MODULE__{type: :undirected} = g, v) do @@ -2348,8 +2352,12 @@ defmodule Graph do |> Enum.reduce(sg, fn v2_id, sg -> v2 = Map.get(vertices, v2_id) - Enum.reduce(Map.get(meta, {v_id, v2_id}), sg, fn {label, weight}, sg -> - Graph.add_edge(sg, v, v2, label: label, weight: weight) + Enum.reduce(Map.get(meta, {v_id, v2_id}), sg, fn {label, edge_meta}, sg -> + Graph.add_edge(sg, v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) end) diff --git a/lib/graph/pathfindings/bellman_ford.ex b/lib/graph/pathfindings/bellman_ford.ex index d5a421b..985018c 100644 --- a/lib/graph/pathfindings/bellman_ford.ex +++ b/lib/graph/pathfindings/bellman_ford.ex @@ -51,7 +51,8 @@ defmodule Graph.Pathfindings.BellmanFord do end @spec edge_weight(term) :: float - defp edge_weight({e, edge_value}), do: {e, edge_value |> Map.values() |> List.first()} + defp edge_weight({e, edge_value}), + do: {e, edge_value |> Map.values() |> List.first() |> Map.get(:weight)} defp has_negative_cycle?(distances, meta) do Enum.any?(meta, fn {{u, v}, weight} -> From 91bcbb2b00832d95eeb3ef8302c6b54f38032d40 Mon Sep 17 00:00:00 2001 From: zack Date: Sat, 31 Aug 2024 12:44:24 -0600 Subject: [PATCH 07/18] more passing tests && duplicate index for in_edges partition --- lib/edge.ex | 3 +- lib/graph.ex | 73 ++++++++++++++++++++++-------- lib/graph/serializers/dot.ex | 4 +- lib/graph/serializers/flowchart.ex | 4 +- test/graph_test.exs | 10 ++-- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/lib/edge.ex b/lib/edge.ex index 324c4e0..7236374 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -61,8 +61,7 @@ defmodule Graph.Edge do {label, %{weight: w} = meta} when is_number(w) -> {label, meta} - other -> - IO.inspect(other) + _other -> raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex index 210570d..5d29f74 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -33,7 +33,7 @@ defmodule Graph do vertices: %{}, type: :directed, vertex_identifier: &Graph.Utils.vertex_id/1, - edge_indexer: &Graph.Utils.edge_label/1, + partition_by: &Graph.Utils.edge_label/1, multigraph: false alias Graph.{Edge, EdgeSpecificationError} @@ -65,7 +65,7 @@ defmodule Graph do vertices: %{vertex_id => vertex}, type: graph_type, vertex_identifier: (vertex() -> term()), - edge_indexer: (Edge.t() -> edge_index_key), + partition_by: (Edge.t() -> edge_index_key), multigraph: boolean() } @type graph_info :: %{ @@ -121,13 +121,13 @@ defmodule Graph do def new(opts \\ []) do type = Keyword.get(opts, :type) || :directed vertex_identifier = Keyword.get(opts, :vertex_identifier) || (&Graph.Utils.vertex_id/1) - edge_indexer = Keyword.get(opts, :edge_indexer) || (&Graph.Utils.edge_label/1) + partition_by = Keyword.get(opts, :partition_by) || (&Graph.Utils.edge_label/1) multigraph = Keyword.get(opts, :multigraph, false) %__MODULE__{ type: type, vertex_identifier: vertex_identifier, - edge_indexer: edge_indexer, + partition_by: partition_by, multigraph: multigraph } end @@ -550,12 +550,12 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edge(:a, :b, label: :uses) ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :contains}, %Graph.Edge{v1: :a, v2: :b, label: :uses}] + [%Graph.Edge{v1: :a, v2: :b, label: :uses}, %Graph.Edge{v1: :a, v2: :b, label: :contains}] iex> g = Graph.new(type: :undirected) |> Graph.add_edge(:a, :b, label: :uses) ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) - [%Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}, %Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}, %Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}] """ @spec edges(t, vertex, vertex) :: [Edge.t()] def edges(%__MODULE__{type: type, edges: meta, vertex_identifier: vertex_identifier}, v1, v2) do @@ -1050,13 +1050,18 @@ defmodule Graph do if g.multigraph do edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) - partition = g.edge_indexer.(edge) - key = {v1_id, partition} - set = Map.get(g.edge_index, key, MapSet.new()) + partition = g.partition_by.(edge) + v1_key = {v1_id, partition} + v2_key = {v2_id, partition} + v1_set = Map.get(g.edge_index, v1_key, MapSet.new()) + v2_set = Map.get(g.edge_index, v2_key, MapSet.new()) %__MODULE__{ g - | edge_index: Map.put(g.edge_index, key, MapSet.put(set, {v1_id, v2_id})) + | edge_index: + g.edge_index + |> Map.put(v1_key, MapSet.put(v1_set, {v1_id, v2_id})) + |> Map.put(v2_key, MapSet.put(v2_set, {v1_id, v2_id})) } else g @@ -1092,7 +1097,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:a, :b, label: :foo, weight: 2}]) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] iex> Graph.new |> Graph.add_vertices([:a, :b, :c]) |> Graph.add_edges([:a, :b]) ** (Graph.EdgeSpecificationError) Expected a valid edge specification, but got: :a @@ -1190,7 +1195,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_edge(g, :a, :b, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :bar}, %Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}] + [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b, label: :bar}] """ @spec update_edge(t, vertex, vertex, Edge.edge_opts()) :: t | {:error, :no_such_edge} def update_edge(%__MODULE__{} = g, v1, v2, opts) when is_list(opts) do @@ -1207,12 +1212,12 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_labelled_edge(g, :a, :b, :bar, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] iex> g = Graph.new(type: :undirected) |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_labelled_edge(g, :a, :b, :bar, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] """ @spec update_labelled_edge(t, vertex, vertex, label, Edge.edge_opts()) :: t | {:error, :no_such_edge} @@ -2179,7 +2184,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:b, :c}]) ...> Graph.in_edges(g, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :foo}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 1}] """ @spec in_edges(t, vertex) :: Edge.t() def in_edges(%__MODULE__{type: :undirected} = g, v) do @@ -2213,6 +2218,38 @@ defmodule Graph do end end + def in_edges( + %__MODULE__{ + vertices: vs, + edges: edges, + multigraph: true, + vertex_identifier: vertex_identifier, + edge_index: edge_index, + partition_by: partition_by + }, + v, + partition + ) do + v2_id = vertex_identifier.(v) + key = {v2_id, partition} + + edge_index + |> Map.get(key, MapSet.new()) + |> IO.inspect(label: "edge_index_keys") + |> Enum.flat_map(fn {v1_id, _v2_id} = edge_key -> + v1 = Map.get(vs, v1_id) + + edges + |> Map.get(edge_key, []) + |> Enum.map(fn {label, edge_meta} -> + Edge.new(v1, v, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + end) + |> Enum.filter(fn edge -> + partition_by.(edge) == partition + end) + end) + end + @doc """ Returns a list of vertices which the given vertex `v` has edges going to. @@ -2250,7 +2287,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:b, :c}]) ...> Graph.out_edges(g, :a) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, properties: %{}}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 1}] """ @spec out_edges(t, vertex) :: Edge.t() def out_edges(%__MODULE__{type: :undirected} = g, v) do @@ -2292,7 +2329,7 @@ defmodule Graph do multigraph: true, edge_index: edge_index, vertex_identifier: vertex_identifier, - edge_indexer: edge_indexer + partition_by: partition_by }, v, partition @@ -2311,7 +2348,7 @@ defmodule Graph do Edge.new(v, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) end) |> Enum.filter(fn edge -> - edge_indexer.(edge) == partition + partition_by.(edge) == partition end) end) end diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex index 2266795..baf5dd4 100644 --- a/lib/graph/serializers/dot.ex +++ b/lib/graph/serializers/dot.ex @@ -28,10 +28,10 @@ defmodule Graph.Serializers.DOT do |> Map.get(id, MapSet.new()) |> Enum.flat_map(fn id2 -> Enum.map(Map.fetch!(em, {id, id2}), fn - {nil, weight} -> + {nil, %{weight: weight}} -> {id, id2, weight} - {label, weight} -> + {label, %{weight: weight}} -> {id, id2, weight, Serializer.encode_label(label)} end) end) diff --git a/lib/graph/serializers/flowchart.ex b/lib/graph/serializers/flowchart.ex index 392a940..7a65cb8 100644 --- a/lib/graph/serializers/flowchart.ex +++ b/lib/graph/serializers/flowchart.ex @@ -38,8 +38,8 @@ defmodule Graph.Serializers.Flowchart do g.edges |> Map.fetch!({id, out_edge_id}) |> Enum.map(fn - {nil, weight} -> {id, out_edge_id, weight} - {label, weight} -> {id, out_edge_id, weight, encode_label(label)} + {nil, %{weight: weight}} -> {id, out_edge_id, weight} + {label, %{weight: weight}} -> {id, out_edge_id, weight, encode_label(label)} end) end) |> case do diff --git a/test/graph_test.exs b/test/graph_test.exs index e1a9b00..c5d395a 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -21,7 +21,7 @@ defmodule GraphTest do end describe "multigraphs" do - test "`multigraph: true` option enables vertex indexing on edge labels" do + test "`multigraph: true` option enables edge indexing on edge labels" do graph = Graph.new(multigraph: true) |> Graph.add_edges([ @@ -43,7 +43,7 @@ defmodule GraphTest do test "custom edge indexing function" do graph = - Graph.new(multigraph: true, edge_indexer: fn edge -> edge.weight end) + Graph.new(multigraph: true, partition_by: fn edge -> edge.weight end) |> Graph.add_edges([ {:a, :b}, {:a, :b, label: :foo}, @@ -54,7 +54,7 @@ defmodule GraphTest do assert Enum.count(Graph.out_edges(graph, :b)) == 2 assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 6) - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 3) + assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, 3) end test "traversal using indexed keys" do @@ -123,12 +123,12 @@ defmodule GraphTest do # pretty printed str = "#{inspect(g)}" - assert "#Graph :b, :a -> :b, :b -[{:complex, :label}]-> :a, :b -> :c]>" = + assert "#Graph :b, :a -[foo]-> :b, :b -[{:complex, :label}]-> :a, :b -> :c]>" = str ustr = "#{inspect(ug)}" - assert "#Graph :b, :a <-> :b, :a <-[{:complex, :label}]-> :b, :b <-> :c]>" = + assert "#Graph :b, :a <-[foo]-> :b, :a <-[{:complex, :label}]-> :b, :b <-> :c]>" = ustr # large graph From 1cadec78dcfedc042a5475bd76e944e5f6bb1938 Mon Sep 17 00:00:00 2001 From: zack Date: Sun, 1 Sep 2024 08:18:04 -0600 Subject: [PATCH 08/18] prune on edge deletion --- lib/graph.ex | 57 ++++++++++++++++++++++++++++++++++++++++----- test/graph_test.exs | 49 ++++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 5d29f74..deec692 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -86,21 +86,19 @@ defmodule Graph do - `multigraph: true | false | fn edge -> key end`, enables edge indexing by a key. - When `true`, the key is the edge label itself. - When `false` no additional memory is used for sets of . - - `edge_indexer`: a function which accepts an `%Edge{}` and returns a unique identifier of said edge. - Defaults to `Graph.Utils.edge_label/1`, the edge label itself. + - `partition_by`: a function which accepts an `%Edge{}` and returns a unique identifier of said edge. + Defaults to `Graph.Utils.edge_label/1`, the edge label itself when multigraphs are enabled. ### Multigraph Edge Indexing Indexing edges trades space for time to access only edges of a kind. - When `multigraph: true` is enabled the `edge_indexer` of the graph is used to build a a set of edge keys (`{vertex_id, vertex_id}`) under a separate key. + When `multigraph: true` is enabled the `partition_by` of the graph is used to build a set of edge keys (`{vertex_id, vertex_id}`) under a separate key. This can be a useful trade-off when traversing a graph where many different kinds of edges exist between the same vertices and you want to avoid iterating over the set of all edges. I.e. a [multigraph](https://en.wikipedia.org/wiki/Multigraph). The index provides allows map access time to to a set of edges when managing the graph. - By default edges are indexed by the label but only when multigraph is toggled true. - ## Example iex> Graph.new() @@ -1308,6 +1306,8 @@ defmodule Graph do v1, v2 ) do + g = prune_edge_index(g, v1, v2, nil) + with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, @@ -1328,6 +1328,50 @@ defmodule Graph do end end + defp prune_edge_index( + %__MODULE__{ + multigraph: true, + edge_index: edge_index, + edges: meta, + partition_by: partition_by, + vertex_identifier: vertex_identifier + } = g, + v1, + v2, + label + ) do + v1_id = vertex_identifier.(v1) + v2_id = vertex_identifier.(v2) + + {_label, edge_meta} = + meta + |> Map.get({v1_id, v2_id}) + |> Enum.filter(fn {edge_label, _} -> edge_label == label end) + |> List.first() + + edge = + Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + + edge_p = partition_by.(edge) + + v1_key = {v1_id, edge_p} + v2_key = {v2_id, edge_p} + + edge_index = + edge_index + |> Map.delete(v1_key) + |> Map.delete(v2_key) + + %__MODULE__{ + g + | edge_index: edge_index + } + end + + defp prune_edge_index(%__MODULE__{multigraph: false} = g, _v1, _v2, _label) do + g + end + @doc """ Removes an edge connecting `v1` to `v2`. A label can be specified to disambiguate the specific edge you wish to delete, if not provided, the unlabelled edge, if one exists, @@ -1379,6 +1423,8 @@ defmodule Graph do v2, label ) do + g = prune_edge_index(g, v1, v2, label) + with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, @@ -2235,7 +2281,6 @@ defmodule Graph do edge_index |> Map.get(key, MapSet.new()) - |> IO.inspect(label: "edge_index_keys") |> Enum.flat_map(fn {v1_id, _v2_id} = edge_key -> v1 = Map.get(vs, v1_id) diff --git a/test/graph_test.exs b/test/graph_test.exs index c5d395a..0abae58 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -31,7 +31,7 @@ defmodule GraphTest do {:b, :c, weight: 3}, {:b, :a, label: {:complex, :label}} ]) - |> IO.inspect(structs: false) + |> IO.inspect(structs: false, label: "multigraph") assert Enum.count(Graph.out_edges(graph, :a)) == 3 assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) @@ -41,7 +41,7 @@ defmodule GraphTest do assert [] == Graph.out_edges(graph, :a, :foobar) end - test "custom edge indexing function" do + test "custom edge partition_by function" do graph = Graph.new(multigraph: true, partition_by: fn edge -> edge.weight end) |> Graph.add_edges([ @@ -57,13 +57,54 @@ defmodule GraphTest do assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, 3) end + test "removing edges prunes index" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.delete_edges(g, [{:b, :c}, {:b, :a}]) + refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), {:complex, :label}}) + end + test "traversal using indexed keys" do end + end - test "edge properties" do + describe "edge properties" do + test "setting edge properties" do + g = + Graph.new() + |> Graph.add_edges([ + {:a, :b, properties: %{foo: :bar}}, + {:a, :b, label: :foo, properties: %{bar: :foo}} + ]) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{foo: :bar}}, + %Edge{v1: :a, v2: :b, label: :foo, properties: %{bar: :foo}} + ] = Graph.out_edges(g, :a) end - test "removing edges" do + test "updating edge properties" do + g = + Graph.new() + |> Graph.add_edges([ + {:a, :b, properties: %{foo: :bar}}, + {:a, :b, label: :foo, properties: %{bar: :foo}} + ]) + |> Graph.update_edge(:a, :b, properties: %{ham: :potato}) + |> Graph.update_labelled_edge(:a, :b, :foo, properties: %{potato: :ham}) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{ham: :potato}}, + %Edge{v1: :a, v2: :b, label: :foo, properties: %{potato: :ham}} + ] = Graph.out_edges(g, :a) end end From 89dfe11ac7cf2762cf5ebd3481bd7463c9041137 Mon Sep 17 00:00:00 2001 From: Zack White Date: Sat, 11 Jan 2025 14:28:17 -0700 Subject: [PATCH 09/18] try latest in CI --- .github/workflows/elixir.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 95a4794..3168018 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -9,11 +9,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - otp: ['24.2', '25.0'] - elixir: ['1.12.3', '1.13.3', '1.14.0'] - exclude: - - otp: '25.0' - elixir: '1.12.3' + include: + - otp: '24.2' + elixir: '1.12.3' + + - otp: '25.0' + elixir: '1.13.3' + + - otp: '27.2' + elixir: '1.18.1' steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 From 2b9cc73f091ef9fb6984cd82efe2cd3883e8fb91 Mon Sep 17 00:00:00 2001 From: Zack White Date: Sat, 11 Jan 2025 14:32:43 -0700 Subject: [PATCH 10/18] try latest in CI --- .github/workflows/elixir.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 3168018..ef7b1df 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -10,14 +10,17 @@ jobs: strategy: matrix: include: - - otp: '24.2' - elixir: '1.12.3' + - elixir: 1.14.5 + otp: 24.3 - - otp: '25.0' - elixir: '1.13.3' + - elixir: 1.15.4 + otp: 25.3 - - otp: '27.2' - elixir: '1.18.1' + - elixir: 1.16.3 + otp: 26.2 + + - otp: 27.2 + elixir: 1.18.1 steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 From 07b018d6307b4d29ce5de8c9dffabab6e0a411d9 Mon Sep 17 00:00:00 2001 From: Zack White Date: Fri, 17 Jan 2025 15:01:26 -0700 Subject: [PATCH 11/18] edge adjacency index pruning --- lib/graph.ex | 57 +++++++++++++++++++++++++++--------- test/graph_test.exs | 17 ++++++++++- test/priority_queue_test.exs | 2 +- test/utils_test.exs | 2 +- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index deec692..6a3566b 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1306,13 +1306,12 @@ defmodule Graph do v1, v2 ) do - g = prune_edge_index(g, v1, v2, nil) - with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, {:ok, v1_out} <- Map.fetch(oe, v1_id), {:ok, v2_in} <- Map.fetch(ie, v2_id) do + g = prune_edge_index(g, {v1_id, v1}, {v2_id, v2}, nil) v1_out = MapSet.delete(v1_out, v2_id) v2_in = MapSet.delete(v2_in, v1_id) meta = Map.delete(meta, edge_key) @@ -1333,21 +1332,52 @@ defmodule Graph do multigraph: true, edge_index: edge_index, edges: meta, - partition_by: partition_by, - vertex_identifier: vertex_identifier + partition_by: partition_by } = g, - v1, - v2, - label + {v1_id, v1}, + {v2_id, v2}, + nil ) do - v1_id = vertex_identifier.(v1) - v2_id = vertex_identifier.(v2) + meta + |> Map.get({v1_id, v2_id}) + |> Enum.reduce(g, fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + + edge_p = partition_by.(edge) + + v1_key = {v1_id, edge_p} + v2_key = {v2_id, edge_p} + + edge_index = + edge_index + |> Map.delete(v1_key) + |> Map.delete(v2_key) + + %__MODULE__{ + acc + | edge_index: edge_index + } + end) + end - {_label, edge_meta} = + defp prune_edge_index( + %__MODULE__{ + multigraph: true, + edge_index: edge_index, + edges: meta, + partition_by: partition_by + } = g, + {v1_id, v1}, + {v2_id, v2}, + label + ) do + [{_label, edge_meta} | _] = meta |> Map.get({v1_id, v2_id}) - |> Enum.filter(fn {edge_label, _} -> edge_label == label end) - |> List.first() + |> Enum.filter(fn {edge_label, _v} -> + edge_label == label + end) edge = Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) @@ -1423,8 +1453,6 @@ defmodule Graph do v2, label ) do - g = prune_edge_index(g, v1, v2, label) - with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, @@ -1432,6 +1460,7 @@ defmodule Graph do {:ok, v2_in} <- Map.fetch(ie, v2_id), {:ok, edge_meta} <- Map.fetch(meta, edge_key), {:ok, _} <- Map.fetch(edge_meta, label) do + g = prune_edge_index(g, {v1_id, v1}, {v2_id, v2}, label) edge_meta = Map.delete(edge_meta, label) case map_size(edge_meta) do diff --git a/test/graph_test.exs b/test/graph_test.exs index 0abae58..194f3fa 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -31,7 +31,6 @@ defmodule GraphTest do {:b, :c, weight: 3}, {:b, :a, label: {:complex, :label}} ]) - |> IO.inspect(structs: false, label: "multigraph") assert Enum.count(Graph.out_edges(graph, :a)) == 3 assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) @@ -72,6 +71,22 @@ defmodule GraphTest do refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), {:complex, :label}}) end + test "delete_edge/3 removes only a multigraph's properties and index for the given partition key" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.delete_edge(g, :a, :b, :foo) |> IO.inspect(structs: false) + refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:a), :foo}) + refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), :foo}) + end + test "traversal using indexed keys" do end end diff --git a/test/priority_queue_test.exs b/test/priority_queue_test.exs index ab6b632..20d5d40 100644 --- a/test/priority_queue_test.exs +++ b/test/priority_queue_test.exs @@ -10,7 +10,7 @@ defmodule PriorityQueue.Test do end) str = "#{inspect(pq)}" - assert "#PriorityQueue" = str + assert "#PriorityQueue" = str end test "can enqueue random elements and pull them out in priority order" do diff --git a/test/utils_test.exs b/test/utils_test.exs index 7a9a3f3..f72724c 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -7,7 +7,7 @@ defmodule Graph.UtilsTest do test "sizeof/1" do assert 64 = sizeof({1, :foo, "bar"}) - assert 440 = sizeof(String.duplicate("bar", 128)) + assert 456 = sizeof(String.duplicate("bar", 128)) assert 8 = sizeof([]) assert 24 = sizeof([1 | 2]) assert 56 = sizeof([1, 2, 3]) From ab3ee73157a4aad17edcbb10f8b3a271c16a8dbf Mon Sep 17 00:00:00 2001 From: Zack White Date: Sat, 1 Mar 2025 13:18:07 -0700 Subject: [PATCH 12/18] add multigraph edge partitioning & filtering --- lib/graph.ex | 301 ++++++++++++++++++++++++++++++++++++++------ test/graph_test.exs | 42 +++++-- 2 files changed, 294 insertions(+), 49 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 6a3566b..65c30f3 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -483,7 +483,21 @@ defmodule Graph do ...> Graph.edges(g, :d) [] """ - @spec edges(t, vertex) :: [Edge.t()] + @spec edges(t, vertex | keyword()) :: [Edge.t()] + + def edges(%__MODULE__{multigraph: true} = g, opts) when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + edges_in_partitions(g, partitions, where_fun) + else + g + |> edges() + |> filter_edges(where_fun) + end + end + def edges( %__MODULE__{ in_edges: ie, @@ -555,7 +569,25 @@ defmodule Graph do ...> Graph.edges(g, :a, :b) [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}, %Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}] """ - @spec edges(t, vertex, vertex) :: [Edge.t()] + @spec edges(t, vertex, vertex | keyword()) :: [Edge.t()] + def edges( + %__MODULE__{multigraph: true} = g, + v1, + opts + ) + when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + edges_in_partitions(g, v1, partitions, where_fun) + else + g + |> edges(v1) + |> filter_edges(where_fun) + end + end + def edges(%__MODULE__{type: type, edges: meta, vertex_identifier: vertex_identifier}, v1, v2) do with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), @@ -574,6 +606,100 @@ defmodule Graph do end end + defp edges_in_partitions(g, partitions, where_fun) do + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + g.edge_index + |> Map.get(partition, %{}) + |> Map.values() + |> Enum.reduce(acc, fn partitioned_set, pacc -> + MapSet.union(partitioned_set, pacc) + end) + end) + |> Enum.flat_map(fn {v1_id, v2_id} = edge_key -> + v1 = Map.get(g.vertices, v1_id) + v2 = Map.get(g.vertices, v2_id) + + g.edges + |> Map.get(edge_key, []) + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partition = g.partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + [edge | acc] + else + acc + end + end) + end) + end + + defp edges_in_partitions(g, v1, partitions, where_fun) do + v1_id = g.vertex_identifier.(v1) + + out_edges_set = + g.out_edges + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v1_id, v2_id} + end) + + in_edges_set = + g.in_edges + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v2_id, v1_id} + end) + + edges = MapSet.union(out_edges_set, in_edges_set) + + edge_adjacency_set = + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + g.edge_index + |> Map.get(partition, %{}) + |> Map.get(v1_id, MapSet.new()) + |> MapSet.union(acc) + end) + |> MapSet.intersection(edges) + + Enum.flat_map(edge_adjacency_set, fn {_v1_id, v2_id} = edge_key -> + v2 = Map.get(g.verticies, v2_id) + + edges + |> Map.get(edge_key, []) + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partition = g.partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + [edge | acc] + else + acc + end + end) + end) + end + + defp filter_edges(edges, nil), do: edges + + defp filter_edges(edges, where_fun) do + Enum.filter(edges, where_fun) + end + defp edge_list(v1, v2, edge_meta, :undirected) do for {label, %{weight: weight, properties: properties}} <- edge_meta do if v1 > v2 do @@ -1049,17 +1175,28 @@ defmodule Graph do edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) partition = g.partition_by.(edge) - v1_key = {v1_id, partition} - v2_key = {v2_id, partition} - v1_set = Map.get(g.edge_index, v1_key, MapSet.new()) - v2_set = Map.get(g.edge_index, v2_key, MapSet.new()) + + edge_partition = Map.get(g.edge_index, partition, %{}) + + v1_set = Map.get(edge_partition, v1_id, MapSet.new()) + v2_set = Map.get(edge_partition, v2_id, MapSet.new()) + + new_edge_partition = + edge_partition + |> Map.put( + v1_id, + MapSet.put(v1_set, {v1_id, v2_id}) + ) + |> Map.put( + v2_id, + MapSet.put(v2_set, {v1_id, v2_id}) + ) %__MODULE__{ g | edge_index: g.edge_index - |> Map.put(v1_key, MapSet.put(v1_set, {v1_id, v2_id})) - |> Map.put(v2_key, MapSet.put(v2_set, {v1_id, v2_id})) + |> Map.put(partition, new_edge_partition) } else g @@ -2297,30 +2434,52 @@ defmodule Graph do %__MODULE__{ vertices: vs, edges: edges, + in_edges: ie, multigraph: true, vertex_identifier: vertex_identifier, edge_index: edge_index, partition_by: partition_by }, v, - partition + by: partition ) do v2_id = vertex_identifier.(v) - key = {v2_id, partition} - edge_index - |> Map.get(key, MapSet.new()) - |> Enum.flat_map(fn {v1_id, _v2_id} = edge_key -> + in_edges_set = + ie + |> Map.get(v2_id, MapSet.new()) + |> MapSet.new(fn v1_id -> + {v1_id, v2_id} + end) + + in_edge_adjacency_set = + edge_index + |> Map.get(partition, %{}) + |> Map.get(v2_id, MapSet.new()) + |> MapSet.intersection(in_edges_set) + + Enum.flat_map(in_edge_adjacency_set, fn {v1_id, _v2_id} = edge_key -> v1 = Map.get(vs, v1_id) edges |> Map.get(edge_key, []) |> Enum.map(fn {label, edge_meta} -> - Edge.new(v1, v, label: label, weight: edge_meta.weight, properties: edge_meta.properties) - end) - |> Enum.filter(fn edge -> - partition_by.(edge) == partition + edge = + Edge.new(v1, v, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partition = partition_by.(edge) + + if edge_partition == partition do + edge + else + nil + end end) + |> Enum.reject(&is_nil/1) end) end @@ -2396,37 +2555,101 @@ defmodule Graph do end end - def out_edges( - %__MODULE__{ - vertices: vs, - edges: edges, - multigraph: true, - edge_index: edge_index, - vertex_identifier: vertex_identifier, - partition_by: partition_by - }, - v, - partition - ) do + @spec out_edges(Graph.t(), any(), [{:by, any()}, ...]) :: list() + def out_edges(%__MODULE__{multigraph: true} = g, v, opts) + when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + + out_edges_in_partitions(g, v, partitions, where_fun) + else + g + |> out_edges(v) + |> filter_edges(where_fun) + end + end + + defp partition_for_opts(partition) when is_list(partition) do + partition + end + + defp partition_for_opts(partition) do + [partition] + end + + defp out_edges_in_partitions( + %__MODULE__{ + vertices: vs, + edges: edges, + out_edges: oe, + multigraph: true, + edge_index: edge_index, + vertex_identifier: vertex_identifier, + partition_by: partition_by + }, + v, + partitions, + where_fun + ) do v1_id = vertex_identifier.(v) - key = {v1_id, partition} - # only return out_edges for which the index key returns a subset - edge_index - |> Map.get(key, MapSet.new()) - |> Enum.flat_map(fn {_v1_id, v2_id} = edge_key -> + + out_edges_set = + oe + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v1_id, v2_id} + end) + + out_edge_adjacency_set = + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + edge_index + |> Map.get(partition, %{}) + |> Map.get(v1_id, MapSet.new()) + |> MapSet.union(acc) + end) + |> MapSet.intersection(out_edges_set) + + Enum.flat_map(out_edge_adjacency_set, fn {_v1_id, v2_id} = edge_key -> v2 = Map.get(vs, v2_id) edges |> Map.get(edge_key, []) - |> Enum.map(fn {label, edge_meta} -> - Edge.new(v, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) - end) - |> Enum.filter(fn edge -> - partition_by.(edge) == partition + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partition = partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + [edge | acc] + else + acc + end end) end) end + defp include_edge_for_filtered_partitions?(_edge, edge_partition, partitions, nil = _where_fun) do + edge_partition in partitions + end + + defp include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) + when is_function(where_fun) do + edge_partition in partitions and where_fun.(edge) + end + + defp include_edge_for_filtered_partitions?(edge, _edge_partition, _partitions, where_fun) + when is_function(where_fun) do + where_fun.(edge) + end + @doc """ Builds a maximal subgraph of `g` which includes all of the vertices in `vs` and the edges which connect them. diff --git a/test/graph_test.exs b/test/graph_test.exs index 194f3fa..dd13988 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -33,16 +33,38 @@ defmodule GraphTest do ]) assert Enum.count(Graph.out_edges(graph, :a)) == 3 - assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, :foo) - assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, :foo) - assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, :bar) - assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, nil) - assert [] == Graph.out_edges(graph, :a, :foobar) + assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, by: [:foo]) + assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, by: :foo) + assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, by: :foo) + assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, by: :bar) + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, by: nil) + + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, by: nil) + + assert [%Edge{label: {:complex, :label}}] = + Graph.out_edges(graph, :b, + where: fn edge -> edge.label == {:complex, :label} or edge.label == :bar end + ) + + assert 1 == graph |> Graph.edges(by: :foo) |> Enum.count() + assert 1 == graph |> Graph.edges(where: fn edge -> edge.weight > 2 end) |> Enum.count() + + assert 2 == + graph + |> Graph.edges(by: [:foo, :bar]) + |> Enum.count() + + assert 1 == + graph + |> Graph.edges(by: [:foo, :bar], where: fn edge -> edge.label == :bar end) + |> Enum.count() + + assert [] == Graph.out_edges(graph, :a, by: :foobar) end test "custom edge partition_by function" do graph = - Graph.new(multigraph: true, partition_by: fn edge -> edge.weight end) + Graph.new(multigraph: true, partition_where: fn edge -> edge.weight end) |> Graph.add_edges([ {:a, :b}, {:a, :b, label: :foo}, @@ -52,8 +74,8 @@ defmodule GraphTest do ]) assert Enum.count(Graph.out_edges(graph, :b)) == 2 - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, 6) - assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, 3) + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, where: 6) + assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, where: 3) end test "removing edges prunes index" do @@ -71,7 +93,7 @@ defmodule GraphTest do refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), {:complex, :label}}) end - test "delete_edge/3 removes only a multigraph's properties and index for the given partition key" do + test "delete_edge/3 removes only a multigraph's properties and index for the given partition key/label" do g = Graph.new(multigraph: true) |> Graph.add_edges([ @@ -82,7 +104,7 @@ defmodule GraphTest do {:b, :a, label: {:complex, :label}} ]) - g = Graph.delete_edge(g, :a, :b, :foo) |> IO.inspect(structs: false) + g = Graph.delete_edge(g, :a, :b, :foo) refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:a), :foo}) refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), :foo}) end From 971c9a5c9cedd300c9552b95d251052b944baee0 Mon Sep 17 00:00:00 2001 From: Zack White Date: Sat, 1 Mar 2025 13:23:31 -0700 Subject: [PATCH 13/18] use opt --- test/graph_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/graph_test.exs b/test/graph_test.exs index dd13988..f8109cb 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -64,7 +64,7 @@ defmodule GraphTest do test "custom edge partition_by function" do graph = - Graph.new(multigraph: true, partition_where: fn edge -> edge.weight end) + Graph.new(multigraph: true, partition_by: fn edge -> edge.weight end) |> Graph.add_edges([ {:a, :b}, {:a, :b, label: :foo}, From 7d06a074f489daab385e495d43cd2afb76d684b4 Mon Sep 17 00:00:00 2001 From: Zack White Date: Sat, 1 Mar 2025 13:26:00 -0700 Subject: [PATCH 14/18] use where filter fn --- test/graph_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/graph_test.exs b/test/graph_test.exs index f8109cb..469ea8b 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -74,8 +74,8 @@ defmodule GraphTest do ]) assert Enum.count(Graph.out_edges(graph, :b)) == 2 - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, where: 6) - assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, where: 3) + assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 6 end) + assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 3 end) end test "removing edges prunes index" do From 3d65693271654af796f65790a857337a4abdf3a9 Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 3 Mar 2025 13:06:57 -0700 Subject: [PATCH 15/18] fix edges/3 --- lib/graph.ex | 48 +++++++++++++++++++++++++++++++++++---------- test/graph_test.exs | 13 ++++++++++-- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 65c30f3..a5cdfb1 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -91,13 +91,11 @@ defmodule Graph do ### Multigraph Edge Indexing - Indexing edges trades space for time to access only edges of a kind. + When `multigraph: true` is enabled the `partition_by` function maintains sets of edges for the partition. + This option enables a space for time trade-off for Map access retrieval partitioned edges of a kind i.e. [multigraph](https://en.wikipedia.org/wiki/Multigraph) capabilities. - When `multigraph: true` is enabled the `partition_by` of the graph is used to build a set of edge keys (`{vertex_id, vertex_id}`) under a separate key. - - This can be a useful trade-off when traversing a graph where many different kinds of edges exist between the same vertices and - you want to avoid iterating over the set of all edges. I.e. a [multigraph](https://en.wikipedia.org/wiki/Multigraph). - The index provides allows map access time to to a set of edges when managing the graph. + This edge adjacency index can be useful for graphs where many different kinds of edges exist between the same vertices and + iteration over all edges is prohibitive. ## Example @@ -471,7 +469,12 @@ defmodule Graph do end @doc """ - Returns a list of all edges inbound or outbound from vertex `v`. + Returns a list of all edges inbound or outbound from vertex `v` or by multigraph traversal options. + + ## Options when `multigraph: true` + + - `:where` - a function that accepts an edge and must return a boolean to include the edge. + - `:by` - a keyword list of partitions to traverse. If not provided, all edges are traversed. ## Example @@ -482,6 +485,16 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:b, :c}]) ...> Graph.edges(g, :d) [] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains) + ...> Graph.edges(g, :a, by: [:contains]) + [%Graph.Edge{v1: :a, v2: :b, label: :contains}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains, weight: 2) + ...> Graph.edges(g, :a, where: fn edge -> edge.weight == 2 end) + [%Graph.Edge{v1: :a, v2: :b, label: :contains, weight: 2}] """ @spec edges(t, vertex | keyword()) :: [Edge.t()] @@ -555,7 +568,12 @@ defmodule Graph do end @doc """ - Returns a list of all edges between `v1` and `v2`. + Returns a list of all edges between `v1` and `v2` or connected to `v1` given multigraph options. + + ## Options when `multigraph: true` + + - `:where` - a function that accepts an edge and must return a boolean to include the edge. + - `:by` - a single partition or list of partitions to traverse. If not provided, all edges are traversed. ## Example @@ -568,6 +586,16 @@ defmodule Graph do ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}, %Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains) + ...> Graph.edges(g, :a, by: :contains) + [%Graph.Edge{v1: :a, v2: :b, label: :contains}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains, weight: 2) + ...> Graph.edges(g, :a, by: :contains, where: fn edge -> edge.weight == 2 end) + [%Graph.Edge{v1: :a, v2: :b, label: :contains, weight: 2}] """ @spec edges(t, vertex, vertex | keyword()) :: [Edge.t()] def edges( @@ -671,9 +699,9 @@ defmodule Graph do |> MapSet.intersection(edges) Enum.flat_map(edge_adjacency_set, fn {_v1_id, v2_id} = edge_key -> - v2 = Map.get(g.verticies, v2_id) + v2 = Map.get(g.vertices, v2_id) - edges + g.edges |> Map.get(edge_key, []) |> Enum.reduce([], fn {label, edge_meta}, acc -> edge = diff --git a/test/graph_test.exs b/test/graph_test.exs index 469ea8b..da91d18 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -49,6 +49,11 @@ defmodule GraphTest do assert 1 == graph |> Graph.edges(by: :foo) |> Enum.count() assert 1 == graph |> Graph.edges(where: fn edge -> edge.weight > 2 end) |> Enum.count() + assert 1 == + graph + |> Graph.edges(:a, by: :foo) + |> Enum.count() + assert 2 == graph |> Graph.edges(by: [:foo, :bar]) @@ -74,8 +79,12 @@ defmodule GraphTest do ]) assert Enum.count(Graph.out_edges(graph, :b)) == 2 - assert [%Edge{weight: 6}] = Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 6 end) - assert [%Edge{weight: 3}] = Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 3 end) + + assert [%Edge{weight: 6}] = + Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 6 end) + + assert [%Edge{weight: 3}] = + Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 3 end) end test "removing edges prunes index" do From ab4305ede2c9bc11feb5203a0a98e9c9a2e48d82 Mon Sep 17 00:00:00 2001 From: Zack White Date: Fri, 21 Mar 2025 14:47:39 -0600 Subject: [PATCH 16/18] update multigraph index on update/delete --- lib/graph.ex | 88 +++++++++++++++++++++++++++++---------------- test/graph_test.exs | 23 ++++++++++-- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index a5cdfb1..49784f3 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1202,30 +1202,7 @@ defmodule Graph do if g.multigraph do edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) - partition = g.partition_by.(edge) - - edge_partition = Map.get(g.edge_index, partition, %{}) - - v1_set = Map.get(edge_partition, v1_id, MapSet.new()) - v2_set = Map.get(edge_partition, v2_id, MapSet.new()) - - new_edge_partition = - edge_partition - |> Map.put( - v1_id, - MapSet.put(v1_set, {v1_id, v2_id}) - ) - |> Map.put( - v2_id, - MapSet.put(v2_set, {v1_id, v2_id}) - ) - - %__MODULE__{ - g - | edge_index: - g.edge_index - |> Map.put(partition, new_edge_partition) - } + index_multigraph_edge(g, {v1_id, v2_id}, edge) else g end @@ -1238,6 +1215,37 @@ defmodule Graph do } end + defp index_multigraph_edge( + %__MODULE__{multigraph: true, edge_index: edge_index} = g, + {v1_id, v2_id}, + %Edge{} = edge + ) do + partition = g.partition_by.(edge) + + edge_partition = Map.get(edge_index, partition, %{}) + + v1_set = Map.get(edge_partition, v1_id, MapSet.new()) + v2_set = Map.get(edge_partition, v2_id, MapSet.new()) + + new_edge_partition = + edge_partition + |> Map.put( + v1_id, + MapSet.put(v1_set, {v1_id, v2_id}) + ) + |> Map.put( + v2_id, + MapSet.put(v2_set, {v1_id, v2_id}) + ) + + %__MODULE__{ + g + | edge_index: + edge_index + |> Map.put(partition, new_edge_partition) + } + end + @doc """ This function is like `add_edge/3`, but for multiple edges at once, it also accepts edge specifications in a few different ways to make it easy to generate graphs succinctly. @@ -1421,7 +1429,20 @@ defmodule Graph do _ -> new_meta = Map.put(Map.delete(meta, old_label), new_label, new_attrs) - %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + + if g.multigraph do + g = + g + |> prune_edge_index({v1_id, v1}, {v2_id, v2}, old_label) + |> index_multigraph_edge( + {v1_id, v2_id}, + Edge.new(v1, v2, label: new_label, weight: new_attrs.weight, properties: opts) + ) + + %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + else + %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + end end else _ -> @@ -1549,13 +1570,20 @@ defmodule Graph do edge_p = partition_by.(edge) - v1_key = {v1_id, edge_p} - v2_key = {v2_id, edge_p} + partition = + edge_index + |> Map.get(edge_p, %{}) + |> Map.reject(fn {k, v} -> + (k == v1_id and MapSet.member?(v, {v1_id, v2_id})) or + (k == v2_id and MapSet.member?(v, {v1_id, v2_id})) + end) edge_index = - edge_index - |> Map.delete(v1_key) - |> Map.delete(v2_key) + if not Enum.empty?(partition) do + Map.put(edge_index, edge_p, partition) + else + Map.delete(edge_index, edge_p) + end %__MODULE__{ g diff --git a/test/graph_test.exs b/test/graph_test.exs index da91d18..c3a59c9 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -114,8 +114,27 @@ defmodule GraphTest do ]) g = Graph.delete_edge(g, :a, :b, :foo) - refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:a), :foo}) - refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), :foo}) + + refute Map.has_key?(g.edge_index, :foo) + assert Enum.empty?(Graph.out_edges(g, :a, by: :foo)) + assert Enum.empty?(Graph.edges(g, by: :foo)) + end + + test "update_labelled_edge/3 updates an indexed adge with new label" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.update_labelled_edge(g, :a, :b, :foo, label: :baz) + + refute Map.has_key?(g.edge_index, :foo) + assert Map.has_key?(g.edge_index[:baz], g.vertex_identifier.(:a)) end test "traversal using indexed keys" do From 5aaabff91d1dc3378d17c633ab4ba1427284875b Mon Sep 17 00:00:00 2001 From: Zack White Date: Tue, 6 May 2025 13:09:27 -0600 Subject: [PATCH 17/18] partition_by many keys && fix edges reflection vertex order --- lib/graph.ex | 146 +++++++++++++++++++++++--------------------- lib/graph/utils.ex | 2 +- test/graph_test.exs | 33 +++++++--- 3 files changed, 104 insertions(+), 77 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 49784f3..0dd8a02 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -33,7 +33,7 @@ defmodule Graph do vertices: %{}, type: :directed, vertex_identifier: &Graph.Utils.vertex_id/1, - partition_by: &Graph.Utils.edge_label/1, + partition_by: &Graph.Utils.by_edge_label/1, multigraph: false alias Graph.{Edge, EdgeSpecificationError} @@ -65,7 +65,7 @@ defmodule Graph do vertices: %{vertex_id => vertex}, type: graph_type, vertex_identifier: (vertex() -> term()), - partition_by: (Edge.t() -> edge_index_key), + partition_by: (Edge.t() -> list(edge_index_key)), multigraph: boolean() } @type graph_info :: %{ @@ -86,8 +86,8 @@ defmodule Graph do - `multigraph: true | false | fn edge -> key end`, enables edge indexing by a key. - When `true`, the key is the edge label itself. - When `false` no additional memory is used for sets of . - - `partition_by`: a function which accepts an `%Edge{}` and returns a unique identifier of said edge. - Defaults to `Graph.Utils.edge_label/1`, the edge label itself when multigraphs are enabled. + - `partition_by`: a function which accepts an `%Edge{}` and returns a list of unique identifiers used as the partition keys. + Defaults to `Graph.Utils.by_edge_label/1`, which partitions edges by the label when multigraphs are enabled. ### Multigraph Edge Indexing @@ -113,11 +113,15 @@ defmodule Graph do iex> g = Graph.new(vertex_identifier: fn v -> :erlang.phash2(v) end) |> Graph.add_edges([{:a, :b}, {:b, :a}]) ...> Graph.edges(g) [%Graph.Edge{v1: :a, v2: :b}, %Graph.Edge{v1: :b, v2: :a}] + + iex> g = Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight] end) |> Graph.add_edges([{:a, :b, weight: 1}, {:b, :a, weight: 2}]) + ...> Graph.edges(g, by: 1) + [%Graph.Edge{v1: :a, v2: :b, weight: 1}] """ def new(opts \\ []) do type = Keyword.get(opts, :type) || :directed vertex_identifier = Keyword.get(opts, :vertex_identifier) || (&Graph.Utils.vertex_id/1) - partition_by = Keyword.get(opts, :partition_by) || (&Graph.Utils.edge_label/1) + partition_by = Keyword.get(opts, :partition_by) || (&Graph.Utils.by_edge_label/1) multigraph = Keyword.get(opts, :multigraph, false) %__MODULE__{ @@ -555,7 +559,7 @@ defmodule Graph do v2 = Map.get(vs, v2_id) for {label, meta_value} <- edge_meta do - Edge.new(v2, v, + Edge.new(v, v2, label: label, weight: meta_value.weight, properties: meta_value.properties @@ -658,9 +662,9 @@ defmodule Graph do properties: edge_meta.properties ) - edge_partition = g.partition_by.(edge) + edge_partitions = g.partition_by.(edge) - if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + if include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) do [edge | acc] else acc @@ -711,9 +715,9 @@ defmodule Graph do properties: edge_meta.properties ) - edge_partition = g.partition_by.(edge) + edge_partitions = g.partition_by.(edge) - if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + if include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) do [edge | acc] else acc @@ -1216,34 +1220,36 @@ defmodule Graph do end defp index_multigraph_edge( - %__MODULE__{multigraph: true, edge_index: edge_index} = g, + %__MODULE__{multigraph: true} = graph, {v1_id, v2_id}, %Edge{} = edge ) do - partition = g.partition_by.(edge) + partitions = graph.partition_by.(edge) - edge_partition = Map.get(edge_index, partition, %{}) + Enum.reduce(partitions, graph, fn partition, g -> + edge_partition = Map.get(g.edge_index, partition, %{}) - v1_set = Map.get(edge_partition, v1_id, MapSet.new()) - v2_set = Map.get(edge_partition, v2_id, MapSet.new()) + v1_set = Map.get(edge_partition, v1_id, MapSet.new()) + v2_set = Map.get(edge_partition, v2_id, MapSet.new()) - new_edge_partition = - edge_partition - |> Map.put( - v1_id, - MapSet.put(v1_set, {v1_id, v2_id}) - ) - |> Map.put( - v2_id, - MapSet.put(v2_set, {v1_id, v2_id}) - ) + new_edge_partition = + edge_partition + |> Map.put( + v1_id, + MapSet.put(v1_set, {v1_id, v2_id}) + ) + |> Map.put( + v2_id, + MapSet.put(v2_set, {v1_id, v2_id}) + ) - %__MODULE__{ - g - | edge_index: - edge_index - |> Map.put(partition, new_edge_partition) - } + %__MODULE__{ + g + | edge_index: + g.edge_index + |> Map.put(partition, new_edge_partition) + } + end) end @doc """ @@ -1530,20 +1536,22 @@ defmodule Graph do edge = Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) - edge_p = partition_by.(edge) + edge_partitions = partition_by.(edge) - v1_key = {v1_id, edge_p} - v2_key = {v2_id, edge_p} + Enum.reduce(edge_partitions, acc, fn edge_p, acc -> + v1_key = {v1_id, edge_p} + v2_key = {v2_id, edge_p} - edge_index = - edge_index - |> Map.delete(v1_key) - |> Map.delete(v2_key) + edge_index = + edge_index + |> Map.delete(v1_key) + |> Map.delete(v2_key) - %__MODULE__{ - acc - | edge_index: edge_index - } + %__MODULE__{ + acc + | edge_index: edge_index + } + end) end) end @@ -1568,27 +1576,29 @@ defmodule Graph do edge = Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) - edge_p = partition_by.(edge) + edge_partitions = partition_by.(edge) - partition = - edge_index - |> Map.get(edge_p, %{}) - |> Map.reject(fn {k, v} -> - (k == v1_id and MapSet.member?(v, {v1_id, v2_id})) or - (k == v2_id and MapSet.member?(v, {v1_id, v2_id})) - end) + Enum.reduce(edge_partitions, g, fn edge_p, acc -> + partition = + edge_index + |> Map.get(edge_p, %{}) + |> Map.reject(fn {k, v} -> + (k == v1_id and MapSet.member?(v, {v1_id, v2_id})) or + (k == v2_id and MapSet.member?(v, {v1_id, v2_id})) + end) - edge_index = - if not Enum.empty?(partition) do - Map.put(edge_index, edge_p, partition) - else - Map.delete(edge_index, edge_p) - end + edge_index = + if not Enum.empty?(partition) do + Map.put(edge_index, edge_p, partition) + else + Map.delete(edge_index, edge_p) + end - %__MODULE__{ - g - | edge_index: edge_index - } + %__MODULE__{ + acc + | edge_index: edge_index + } + end) end defp prune_edge_index(%__MODULE__{multigraph: false} = g, _v1, _v2, _label) do @@ -2527,9 +2537,9 @@ defmodule Graph do properties: edge_meta.properties ) - edge_partition = partition_by.(edge) + edge_partitions = partition_by.(edge) - if edge_partition == partition do + if Enum.any?(edge_partitions, fn edge_partition -> edge_partition == partition end) do edge else nil @@ -2681,9 +2691,9 @@ defmodule Graph do properties: edge_meta.properties ) - edge_partition = partition_by.(edge) + edges_in_partitions = partition_by.(edge) - if include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) do + if include_edge_for_filtered_partitions?(edge, edges_in_partitions, partitions, where_fun) do [edge | acc] else acc @@ -2692,13 +2702,13 @@ defmodule Graph do end) end - defp include_edge_for_filtered_partitions?(_edge, edge_partition, partitions, nil = _where_fun) do - edge_partition in partitions + defp include_edge_for_filtered_partitions?(_edge, edge_partitions, partitions, nil = _where_fun) do + Enum.any?(edge_partitions, fn ep -> ep in partitions end) end - defp include_edge_for_filtered_partitions?(edge, edge_partition, partitions, where_fun) + defp include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) when is_function(where_fun) do - edge_partition in partitions and where_fun.(edge) + Enum.any?(edge_partitions, fn ep -> ep in partitions and where_fun.(edge) end) end defp include_edge_for_filtered_partitions?(edge, _edge_partition, _partitions, where_fun) diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex index 2880961..2260fc2 100644 --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -111,5 +111,5 @@ defmodule Graph.Utils do @max_phash 4_294_967_296 def vertex_id(v), do: :erlang.phash2(v, @max_phash) - def edge_label(%{label: label}), do: label + def by_edge_label(%{label: label}), do: [label] end diff --git a/test/graph_test.exs b/test/graph_test.exs index c3a59c9..fd10db1 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -69,7 +69,7 @@ defmodule GraphTest do test "custom edge partition_by function" do graph = - Graph.new(multigraph: true, partition_by: fn edge -> edge.weight end) + Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight] end) |> Graph.add_edges([ {:a, :b}, {:a, :b, label: :foo}, @@ -87,6 +87,25 @@ defmodule GraphTest do Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 3 end) end + test "custom partition_by supports indexing to more than one partition" do + graph = + Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight, edge.label] end) + |> Graph.add_edges([ + {:a, :b}, + {:a, :d, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, weight: 6, label: :foo} + ]) + + assert Enum.count(Graph.out_edges(graph, :b)) == 2 + + assert [%Edge{weight: 6, label: :foo}] = + Graph.out_edges(graph, :b, by: 6) + + assert Enum.count(Graph.edges(graph, by: [:foo])) == 2 + end + test "removing edges prunes index" do g = Graph.new(multigraph: true) @@ -300,13 +319,11 @@ defmodule GraphTest do ]) |> Graph.edges(:a) - expected_result = [ - %Graph.Edge{label: "label3", v1: :b, v2: :a, weight: 1}, - %Graph.Edge{label: "label1", v1: :a, v2: :b, weight: 1}, - %Graph.Edge{label: "label2", v1: :a, v2: :b, weight: 1} - ] - - assert generated_result == expected_result + for edge <- generated_result do + assert edge.label in ["label1", "label2", "label3"] and + ((edge.v1 == :a and edge.v2 == :b) or + (edge.v1 == :b and edge.v2 == :a)) + end end test "is_subgraph?" do From 416f99e002cebe82f5640014bc2c79dcf318dcd1 Mon Sep 17 00:00:00 2001 From: Zack White Date: Mon, 22 Sep 2025 11:12:38 -0600 Subject: [PATCH 18/18] pass properties through add_edge of struct --- lib/graph.ex | 10 ++++++++-- test/graph_test.exs | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/graph.ex b/lib/graph.ex index 0dd8a02..285689f 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1139,8 +1139,14 @@ defmodule Graph do [%Graph.Edge{v1: :a, v2: :b}] """ @spec add_edge(t, Edge.t()) :: t - def add_edge(%__MODULE__{} = g, %Edge{v1: v1, v2: v2, label: label, weight: weight}) do - add_edge(g, v1, v2, label: label, weight: weight) + def add_edge(%__MODULE__{} = g, %Edge{ + v1: v1, + v2: v2, + label: label, + weight: weight, + properties: properties + }) do + add_edge(g, v1, v2, label: label, weight: weight, properties: properties) end @doc """ diff --git a/test/graph_test.exs b/test/graph_test.exs index fd10db1..679eb17 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -156,8 +156,30 @@ defmodule GraphTest do assert Map.has_key?(g.edge_index[:baz], g.vertex_identifier.(:a)) end - test "traversal using indexed keys" do - end + # test "BFS traversal using multigraph partitions" do + # graph = + # Graph.new(multigraph: true) + # |> Graph.add_edges([ + # {:a, :b, label: :foo}, + # {:a, :b, label: :bar}, + # {:b, :c, weight: 3}, + # {:b, :a, label: {:complex, :label}} + # ]) + + # assert Graph.bfs(graph, :a) == [:a, :b, :c] + # end + + # test "DFS traversal using multigraph partitions" do + # end + + # test "Dijkstra traversal using multigraph partitions" do + # end + + # test "A* traversal using multigraph partitions" do + # end + + # test "Bellman-Ford traversal using multigraph partitions" do + # end end describe "edge properties" do @@ -190,6 +212,19 @@ defmodule GraphTest do %Edge{v1: :a, v2: :b, label: :foo, properties: %{potato: :ham}} ] = Graph.out_edges(g, :a) end + + test "adding edge struct with properties" do + g = + Graph.new() + + edge = Edge.new(:a, :b, properties: %{foo: :bar}) + + g = Graph.add_edge(g, edge) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{foo: :bar}} + ] = Graph.out_edges(g, :a) + end end test "delete vertex" do