├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bench ├── cliques.exs ├── create.exs ├── k_core.exs ├── shortest_path.exs └── topsort.exs ├── coveralls.json ├── lib ├── edge.ex ├── graph.ex ├── graph │ ├── directed.ex │ ├── edge_specification_error.ex │ ├── inspect.ex │ ├── pathfinding.ex │ ├── pathfindings │ │ └── bellman_ford.ex │ ├── reducer.ex │ ├── reducers │ │ ├── bfs.ex │ │ └── dfs.ex │ ├── serializer.ex │ ├── serializers │ │ ├── dot.ex │ │ ├── edgelist.ex │ │ └── flowchart.ex │ └── utils.ex └── priority_queue.ex ├── mix.exs ├── mix.lock └── test ├── fixtures ├── email-Enron.txt └── petster │ ├── README.petster-friendships-hamster │ ├── edges.txt │ ├── metadata.txt │ └── vertices.txt ├── graph_test.exs ├── model_test.exs ├── priority_queue_test.exs ├── reducer_test.exs ├── serializer_test.exs ├── support ├── generators.ex └── parser.ex ├── test_helper.exs └── utils_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: elixir 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | otp: ['24.2', '25.0'] 13 | elixir: ['1.12.3', '1.13.3', '1.14.0'] 14 | exclude: 15 | - otp: '25.0' 16 | elixir: '1.12.3' 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: erlef/setup-beam@v1 20 | with: 21 | otp-version: ${{matrix.otp}} 22 | elixir-version: ${{matrix.elixir}} 23 | - run: mix deps.get 24 | - run: mix test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # QuickCheck files 23 | /.eqc-info 24 | /*.eqc 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: false 3 | elixir: 4 | - 1.8 5 | - 1.9 6 | otp_release: 7 | - 21.0 8 | - 22.0 9 | matrix: 10 | exclude: 11 | - elixir: 1.8 12 | otp_release: 18.3 13 | - elixir: 1.9 14 | otp_release: 18.3 15 | - elixir: 1.8 16 | otp_release: 19.1 17 | - elixir: 1.9 18 | otp_release: 19.1 19 | - elixir: 1.9 20 | otp_release: 20.0 21 | env: 22 | - MIX_ENV=test 23 | script: 24 | - mix eqc.install --mini 25 | - mix coveralls.travis 26 | notifications: 27 | recipients: 28 | - paulschoenfelder@fastmail.com 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | ## Copyright (c) 2017 Paul Schoenfelder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help bench test bench/%.exs 2 | 3 | APP_NAME ?= libgraph 4 | VERSION ?= `cat mix.exs | grep "version:" | cut -d '"' -f2` 5 | BENCHMARKS := $(wildcard bench/*.exs) 6 | 7 | help: 8 | @echo "$(APP_NAME):$(VERSION)" 9 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 10 | 11 | test: ## Run tests 12 | mix test 13 | 14 | bench/%.exs: 15 | mix run $@ 16 | 17 | $(BENCHMARKS): bench/%.exs 18 | 19 | bench: $(BENCHMARKS) ## Run benchmarks 20 | @echo "Benchmarks complete!" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libgraph 2 | 3 | [![Master](https://travis-ci.org/bitwalker/libgraph.svg?branch=master)](https://travis-ci.org/bitwalker/libgraph) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/libgraph.svg?style=flat)](https://hex.pm/packages/libgraph) 5 | [![Coverage Status](https://coveralls.io/repos/github/bitwalker/libgraph/badge.svg?branch=master)](https://coveralls.io/github/bitwalker/libgraph?branch=master) 6 | 7 | [Documentation](https://hexdocs.pm/libgraph) 8 | 9 | ## About 10 | 11 | This library provides: 12 | 13 | - An implementation of a graph datastructure, `Graph`, designed for both directed and undirected graphs. The API supports 14 | undirected graphs, but I'm still getting the tests updated to cover properties of undirected graphs. 15 | - A priority queue implementation `PriorityQueue`, oriented towards graphs (it prioritizes lower integer values over high), 16 | it is the fastest priority queue I know of which allows arbitrary priorities, and is more or less at parity with 17 | `pqueue3` from [the pqueue library](https://github.com/okeuday/pqueue/), which supports priorities from 0 to 65535. 18 | - An idiomatic Elixir API for creating, modifying, and querying its graph structure. Creating and modifying a graph 19 | can be done in a single pipeline, and all queries take a Graph as their first parameter (one of my complaints with `:digraph` 20 | is that there is some inconsistency with the API between `:digraph` and `:digraph_utils` for no apparent reason). 21 | - Two "Reducer" implementations for mapping/reducing over a graph. I am trying to figure out the best way to make these 22 | extendable and part of the API, so that you can drop in your own shortest path algorithms, etc - but I have yet to come up with an 23 | approach that feels good on that front. 24 | - A `Serializer` behaviour, for defining custom serialization of graphs, with a Graphviz DOT format serializer 25 | provided out of the box. 26 | 27 | It is backed by a large suite of tests, including several QuickCheck properties for the graph model. Its 28 | API shares some similarity with `:digraph`, but diverges in favor of a more idiomatic Elixir interface. In 29 | addition, over time I'm adding new functions to query the graph in ways not previously supported via `:digraph`, 30 | and introducing support for classifying a graph as undirected if so desired, so that queries over such graphs 31 | become easier. 32 | 33 | If you are interested in reading more about how you can make use of `libgraph`, 34 | there is an [excellent blog post](https://medium.com/@tonyhammond/native-graph-data-in-elixir-8c0bb325d451) written by Tony Hammond 35 | which is a very helpful walkthrough of the library and what can be built with it. 36 | 37 | ## Installation 38 | 39 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 40 | by adding `libgraph` to your list of dependencies in `mix.exs`: 41 | 42 | ```elixir 43 | def deps do 44 | [{:libgraph, "~> 0.16.0"}] 45 | end 46 | ``` 47 | 48 | ## Rationale 49 | 50 | The original motivation for me to start working on this library is the fact that `:digraph` requires a 51 | minimum of 3 ETS tables per graph, and up to 6 depending on the operations you are performing on the graph. 52 | If you are working with a lot of graphs concurrently, as I am, this means you can find yourself in a situation 53 | where you hit the system limit for the maximum number of ETS table, and bring your system down. Seeing as how 54 | it is ridiculous that trying to use a simple graph could potentially kill my system, and not wanting to hack 55 | around the problem, I decided to see if I could build an alternative which was competitive performance-wise, 56 | without requiring any ETS tables at all. 57 | 58 | The result turned out better than I hoped - it is possible to build a graph datastructure without ETS that 59 | is both equally performant (and in many of my benchmarks, better performing), and supports all of the same 60 | functionality. 61 | 62 | Additionally, I also had a few other things I wanted to address: 63 | 64 | - Inconsistency with argument order in the API between `:digraph` and `:digraph_utils` 65 | - The fact that there are two modules to work with the same datastructure to begin with, and trying to remember 66 | what lives where. 67 | - The lack of extensibility, for example, there is no API with which you can implement your own 68 | traversal algorithms. This means you are stuck with whatever way the Erlang maintainers decided was 69 | ideal, regardless of whether it suits your use case or not. A great example is single-source shortest path 70 | algorithms, where you may want a simple breadth-first search, or perhaps you want to use Dijkstra's algorithm - 71 | you are stuck with just one approach with `:digraph`, which as I understand it, is a breadth-first search. 72 | - `:digraph` as the name implies, only supports directed graphs 73 | - `:digraph` graphs are unweighted, with no way to supported weighted graphs 74 | - `:digraph` graphs are not "inspect-friendly", you get a tuple with the underlying ETS table ids, but that's it, 75 | not necessarily a big deal, but it's nice for playing around in the shell if you can see how your code affects the 76 | structure. 77 | 78 | My speculation as to why `:digraph` is the way it is, is that when `:digraph` was originally written, there was 79 | no efficient key/value datastructure in Erlang that could support large numbers of keys. At that time, maps 80 | weren't even a speck in the eye of the language maintainers. Even after the initial introduction of maps in OTP 18, 81 | maps still weren't efficient enough to work with large numbers of keys. It wasn't until OTP 19 that the performance 82 | of maps with millions of keys became reasonable. So, it's not that `:digraph` sucks - it was the best possible implementation 83 | at the time; but now that the language has come so far, we can take advantage of some of the new hotness and reinvent 84 | it from the ground up :). 85 | 86 | ## Benchmarks 87 | 88 | Feel free to take a look under the `bench` folder in the project root. There a few benchmarks I threw together to 89 | keep an eye on a few key areas I wanted to ensure parity with `:digraph` on. You can run them yourself as well, but 90 | I would encourage you to use them as a template to construct a benchmark based on your own use case, and compare them 91 | that way, as it will give you a better basis to make your decision on. However, if you do find that `libgraph` is behind 92 | `:digraph` with a benchmark, please let me know so that I can improve the library! 93 | 94 | NOTE: While this library is primarily focused on the `Graph` data structure it defines, it also contains an implementation 95 | of a priority queue (you can find it under the `PriorityQueue` module), designed for use with graphs specifically, as it 96 | considers lower integer values higher priority, which is perfect for the kinds of graph algorithms you need a priority queue for. 97 | 98 | ## Contributing 99 | 100 | To run the test suite you will need to run `mix eqc.install --mini` once you've cloned the repo and fetched dependencies. 101 | 102 | If you have changes in mind that are significant or potentially time consuming, please open a RFC-style PR first, where we 103 | can discuss your plans first. I don't want you to spend all your time crafting a PR that I ultimately reject because I don't 104 | think it's a good fit or is too large for me to review. Not that I plan to reject PRs in general, but I have to be careful to 105 | balance features with maintenance burden, or I will quickly be unable to manage the project. 106 | 107 | Please ensure that you adhere to a commit style where logically related changes are in a single commit, or broken up in a way that 108 | eases review if necessary. Keep commit subject lines informative, but short, and provide additional detail in the extended message text 109 | if needed. If you can, mention relevant issue numbers in either the subject or the extended message. 110 | 111 | ## Roadmap 112 | 113 | Please open an issue if you have a feature request! 114 | 115 | ## License 116 | 117 | MIT (See the LICENSE file) 118 | -------------------------------------------------------------------------------- /bench/cliques.exs: -------------------------------------------------------------------------------- 1 | Code.require_file(Path.join([__DIR__, "..", "test", "support", "parser.ex"])) 2 | 3 | alias Graph.Test.Fixtures.Parser 4 | 5 | opts = [ 6 | time: 10, 7 | inputs: %{ 8 | "Enron emails" => Path.join([__DIR__, "..", "test", "fixtures", "email-Enron.txt"]), 9 | } 10 | ] 11 | 12 | # This currently takes ages, only uncomment when you really want to run it 13 | Benchee.run( 14 | # %{ 15 | # "libgraph" => fn path -> 16 | # g = Parser.parse(path) 17 | # _ = Graph.cliques(g) 18 | # end 19 | # }, 20 | opts 21 | ) 22 | -------------------------------------------------------------------------------- /bench/create.exs: -------------------------------------------------------------------------------- 1 | opts = [ 2 | time: 10, 3 | inputs: %{ 4 | "1M vertices, 500k edges" => 1_000_000, 5 | "100k vertices, 50k edges" => 100_000, 6 | "10k vertices, 5k edges" => 10_000 7 | } 8 | ] 9 | 10 | Benchee.run( 11 | %{ 12 | "digraph" => fn size -> 13 | dg = :digraph.new 14 | for i <- 1..size, do: :digraph.add_vertex(dg, %{num: i}) 15 | for i <- 1..size, rem(i, 2) == 0, do: :digraph.add_edge(dg, %{num: i}, %{num: i - 1}) 16 | end, 17 | "libgraph" => fn size -> 18 | g = Graph.new 19 | ga = Enum.reduce(1..size, g, fn i, g -> Graph.add_vertex(g, %{num: i}) end) 20 | Enum.reduce(1..size, ga, fn 21 | i, g when rem(i, 2) == 0 -> 22 | Graph.add_edge(g, %{num: i}, %{num: i-1}) 23 | _, g -> 24 | g 25 | end) 26 | end 27 | }, 28 | opts 29 | ) 30 | -------------------------------------------------------------------------------- /bench/k_core.exs: -------------------------------------------------------------------------------- 1 | Code.require_file(Path.join([__DIR__, "..", "test", "support", "parser.ex"])) 2 | 3 | alias Graph.Test.Fixtures.Parser 4 | 5 | opts = [ 6 | time: 10, 7 | inputs: %{ 8 | "Enron emails" => Path.join([__DIR__, "..", "test", "fixtures", "email-Enron.txt"]), 9 | } 10 | ] 11 | 12 | Benchee.run( 13 | %{ 14 | "libgraph" => fn path -> 15 | g = Parser.parse(path) 16 | 43 = Graph.degeneracy(g) 17 | end 18 | }, 19 | opts 20 | ) 21 | -------------------------------------------------------------------------------- /bench/shortest_path.exs: -------------------------------------------------------------------------------- 1 | Code.require_file(Path.join([__DIR__, "..", "test", "support", "generators.ex"])) 2 | 3 | alias Graph.Test.Generators 4 | 5 | g = Generators.dag(1_000) 6 | dg = Generators.libgraph_to_digraph(g) 7 | 8 | g2 = Generators.biased_dag(1_000) 9 | dg2 = Generators.libgraph_to_digraph(g2) 10 | 11 | opts = [ 12 | time: 15, 13 | warmup: 5, 14 | inputs: %{"unbiased" => {g, dg}, "biased" => {g2, dg2}} 15 | ] 16 | 17 | Benchee.run( 18 | %{ 19 | "digraph (get_short_path)" => fn {_, dg} -> 20 | length(:digraph.get_short_path(dg, 1, 1_000)) != 0 21 | end, 22 | "libgraph (dijkstra)" => fn {g, _} -> 23 | length(Graph.dijkstra(g, 1, 1_000)) != 0 24 | end, 25 | "libgraph (a_star)" => fn {g, _} -> 26 | length(Graph.a_star(g, 1, 1_000, fn v -> 27 | case Graph.vertex_label(g, v) do 28 | nil -> 1_000 29 | hint -> hint 30 | end 31 | end)) != 0 32 | end, 33 | }, 34 | opts 35 | ) 36 | -------------------------------------------------------------------------------- /bench/topsort.exs: -------------------------------------------------------------------------------- 1 | Code.require_file(Path.join([__DIR__, "..", "test", "support", "generators.ex"])) 2 | 3 | alias Graph.Test.Generators 4 | 5 | g = Generators.dag(10_000) 6 | dg = Generators.libgraph_to_digraph(g) 7 | 8 | opts = [ 9 | time: 10, 10 | inputs: %{ 11 | "10k vertices, #{map_size(g.out_edges)} edges" => {g, dg} 12 | } 13 | ] 14 | 15 | Benchee.run( 16 | %{ 17 | "digraph (topsort)" => fn {_, dg}-> 18 | :digraph_utils.topsort(dg) 19 | end, 20 | "libgraph (topsort)" => fn {g, _} -> 21 | Graph.topsort(g) 22 | end 23 | }, 24 | opts 25 | ) 26 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_stop_words": [ 3 | "defstruct", 4 | "defmacro.+(.+\/\/.+).+do" 5 | ], 6 | "coverage_options": { 7 | "treat_no_relevant_lines_as_covered": true 8 | }, 9 | "skip_files": [ 10 | "test/support" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/edge.ex: -------------------------------------------------------------------------------- 1 | defmodule Graph.Edge do 2 | @moduledoc """ 3 | This module defines the struct used to represent edges and associated metadata about them. 4 | 5 | Used internally, `v1` and `v2` typically hold vertex ids, not the vertex itself, but all 6 | public APIs which return `Graph.Edge` structs, return them with the actual vertices. 7 | """ 8 | defstruct v1: nil, 9 | v2: nil, 10 | weight: 1, 11 | label: nil 12 | 13 | @type t :: %__MODULE__{ 14 | v1: Graph.vertex(), 15 | v2: Graph.vertex(), 16 | weight: integer | float, 17 | label: term 18 | } 19 | @type edge_opt :: 20 | {:weight, integer | float} 21 | | {:label, term} 22 | @type edge_opts :: [edge_opt] 23 | 24 | @doc """ 25 | Defines a new edge and accepts optional values for weight and label. 26 | The defaults of a weight of 1 and no label will be used if the options do 27 | not specify otherwise. 28 | 29 | An error will be thrown if weight is not an integer or float. 30 | 31 | ## Example 32 | 33 | iex> Graph.new |> Graph.add_edge(Graph.Edge.new(:a, :b, weight: "1")) 34 | ** (ArgumentError) invalid value for :weight, must be an integer 35 | """ 36 | @spec new(Graph.vertex(), Graph.vertex()) :: t 37 | @spec new(Graph.vertex(), Graph.vertex(), [edge_opt]) :: t | no_return 38 | def new(v1, v2, opts \\ []) when is_list(opts) do 39 | %__MODULE__{ 40 | v1: v1, 41 | v2: v2, 42 | weight: Keyword.get(opts, :weight, 1), 43 | label: Keyword.get(opts, :label) 44 | } 45 | end 46 | 47 | @doc false 48 | def options_to_meta(opts) when is_list(opts) do 49 | label = Keyword.get(opts, :label) 50 | weight = Keyword.get(opts, :weight, 1) 51 | 52 | case {label, weight} do 53 | {_, w} = meta when is_number(w) -> 54 | meta 55 | 56 | {_, _} -> 57 | raise ArgumentError, message: "invalid value for :weight, must be an integer" 58 | end 59 | end 60 | 61 | def options_to_meta(nil), do: nil 62 | 63 | @doc false 64 | def to_meta(%__MODULE__{label: label, weight: weight}), do: {label, weight} 65 | end 66 | -------------------------------------------------------------------------------- /lib/graph.ex: -------------------------------------------------------------------------------- 1 | defmodule Graph do 2 | @moduledoc """ 3 | This module defines a graph data structure, which supports directed and undirected graphs, in both acyclic and cyclic forms. 4 | It also defines the API for creating, manipulating, and querying that structure. 5 | 6 | As far as memory usage is concerned, `Graph` should be fairly compact in memory, but if you want to do a rough 7 | comparison between the memory usage for a graph between `libgraph` and `digraph`, use `:digraph.info/1` and 8 | `Graph.info/1` on the two graphs, and both results will contain memory usage information. Keep in mind we don't have a precise 9 | way to measure the memory usage of a term in memory, whereas ETS is able to give a more precise answer, but we do have 10 | a fairly good way to estimate the usage of a term, and we use that method within `libgraph`. 11 | 12 | The Graph struct is structured like so: 13 | 14 | - A map of vertex ids to vertices (`vertices`) 15 | - A map of vertex ids to their out neighbors (`out_edges`), 16 | - A map of vertex ids to their in neighbors (`in_edges`), effectively the transposition of `out_edges` 17 | - A map of vertex ids to vertex labels (`vertex_labels`), (labels are only stored if a non-nil label was provided) 18 | - A map of edge ids (where an edge id is simply a tuple of `{vertex_id, vertex_id}`) to a map of edge metadata (`edges`) 19 | - Edge metadata is a map of `label => weight`, and each entry in that map represents a distinct edge. This allows 20 | us to support multiple edges in the same direction between the same pair of vertices, but for many purposes simply 21 | treat them as a single logical edge. 22 | 23 | This structure is designed to be as efficient as possible once a graph is built, but it turned out that it is also 24 | quite efficient for manipulating the graph as well. For example, splitting an edge and introducing a new vertex on that 25 | edge can be done with very little effort. We use vertex ids everywhere because we can generate them without any lookups, 26 | we don't incur any copies of the vertex structure, and they are very efficient as keys in a map. 27 | """ 28 | defstruct in_edges: %{}, 29 | out_edges: %{}, 30 | edges: %{}, 31 | vertex_labels: %{}, 32 | vertices: %{}, 33 | type: :directed, 34 | vertex_identifier: &Graph.Utils.vertex_id/1 35 | 36 | alias Graph.{Edge, EdgeSpecificationError} 37 | 38 | @typedoc """ 39 | Identifier of a vertex. By default a non_neg_integer from `Graph.Utils.vertex_id/1` utilizing `:erlang.phash2`. 40 | """ 41 | @type vertex_id :: non_neg_integer() | term() 42 | @type vertex :: term 43 | @type label :: term 44 | @type edge_weight :: integer | float 45 | @type edge_key :: {vertex_id, vertex_id} 46 | @type edge_value :: %{label => edge_weight} 47 | @type graph_type :: :directed | :undirected 48 | @type vertices :: %{vertex_id => vertex} 49 | @type t :: %__MODULE__{ 50 | in_edges: %{vertex_id => MapSet.t()}, 51 | out_edges: %{vertex_id => MapSet.t()}, 52 | edges: %{edge_key => edge_value}, 53 | vertex_labels: %{vertex_id => term}, 54 | vertices: %{vertex_id => vertex}, 55 | type: graph_type, 56 | vertex_identifier: (vertex() -> term()) 57 | } 58 | @type graph_info :: %{ 59 | :num_edges => non_neg_integer(), 60 | :num_vertices => non_neg_integer(), 61 | :size_in_bytes => number(), 62 | :type => :directed | :undirected 63 | } 64 | 65 | @doc """ 66 | Creates a new graph using the provided options. 67 | 68 | ## Options 69 | 70 | - `type: :directed | :undirected`, specifies what type of graph this is. Defaults to a `:directed` graph. 71 | - `vertex_identifier`: a function which accepts a vertex and returns a unique identifier of said vertex. 72 | Defaults to `Graph.Utils.vertex_id/1`, a hash of the whole vertex utilizing `:erlang.phash2/2`. 73 | 74 | ## Example 75 | 76 | iex> Graph.new() 77 | #Graph 78 | 79 | iex> g = Graph.new(type: :undirected) |> Graph.add_edges([{:a, :b}, {:b, :a}]) 80 | ...> Graph.edges(g) 81 | [%Graph.Edge{v1: :a, v2: :b}] 82 | 83 | iex> g = Graph.new(type: :directed) |> Graph.add_edges([{:a, :b}, {:b, :a}]) 84 | ...> Graph.edges(g) 85 | [%Graph.Edge{v1: :a, v2: :b}, %Graph.Edge{v1: :b, v2: :a}] 86 | 87 | iex> g = Graph.new(vertex_identifier: fn v -> :erlang.phash2(v) end) |> Graph.add_edges([{:a, :b}, {:b, :a}]) 88 | ...> Graph.edges(g) 89 | [%Graph.Edge{v1: :a, v2: :b}, %Graph.Edge{v1: :b, v2: :a}] 90 | """ 91 | def new(opts \\ []) do 92 | type = Keyword.get(opts, :type) || :directed 93 | vertex_identifier = Keyword.get(opts, :vertex_identifier) || (&Graph.Utils.vertex_id/1) 94 | %__MODULE__{type: type, vertex_identifier: vertex_identifier} 95 | end 96 | 97 | @doc """ 98 | Returns a map of summary information about this graph. 99 | 100 | NOTE: The `size_in_bytes` value is an estimate, not a perfectly precise value, but 101 | should be close enough to be useful. 102 | 103 | ## Example 104 | 105 | iex> g = Graph.new |> Graph.add_vertices([:a, :b, :c, :d]) 106 | ...> g = g |> Graph.add_edges([{:a, :b}, {:b, :c}]) 107 | ...> match?(%{type: :directed, num_vertices: 4, num_edges: 2}, Graph.info(g)) 108 | true 109 | """ 110 | @spec info(t) :: graph_info() 111 | def info(%__MODULE__{type: type} = g) do 112 | %{ 113 | type: type, 114 | num_edges: num_edges(g), 115 | num_vertices: num_vertices(g), 116 | size_in_bytes: Graph.Utils.sizeof(g) 117 | } 118 | end 119 | 120 | @doc """ 121 | Converts the given Graph to DOT format, which can then be converted to 122 | a number of other formats via Graphviz, e.g. `dot -Tpng out.dot > out.png`. 123 | 124 | If labels are set on a vertex, then those labels are used in the DOT output 125 | in place of the vertex itself. If no labels were set, then the vertex is 126 | stringified if it's a primitive type and inspected if it's not, in which 127 | case the inspect output will be quoted and used as the vertex label in the DOT file. 128 | 129 | Edge labels and weights will be shown as attributes on the edge definitions, otherwise 130 | they use the same labelling scheme for the involved vertices as described above. 131 | 132 | NOTE: Currently this function assumes graphs are directed graphs, but in the future 133 | it will support undirected graphs as well. 134 | 135 | NOTE: Currently this function assumes graphs are directed graphs, but in the future 136 | it will support undirected graphs as well. 137 | 138 | NOTE 2: To avoid to overwrite vertices with the same label, output is 139 | generated using the internal numeric ID as vertex label. 140 | Original label is expressed as `id[label="