defmodule Hypergraph do @moduledoc """ A module for creating and manipulating hypergraphs. A hypergraph is a generalization of a graph where edges (hyperedges) can connect any number of vertices, not just two. """ defstruct hyperedges: [] @type vertex :: any() @type hyperedge :: [vertex()] @type t :: %__MODULE__{ hyperedges: [hyperedge()] } @doc """ Creates a new empty hypergraph. """ @spec new() :: t() def new do %__MODULE__{} end @doc """ Creates a new hypergraph with the given vertices and hyperedges. """ @spec new([vertex()], [hyperedge()]) :: t() def new(_vertices, hyperedges) do %__MODULE__{hyperedges: hyperedges} end @doc """ Adds a hyperedge to the hypergraph. The hyperedge connects all vertices in the given list. Automatically adds any new vertices to the hypergraph. """ @spec add_hyperedge(t(), hyperedge()) :: t() def add_hyperedge(%__MODULE__{} = hypergraph, vertices) when is_list(vertices) do %{hypergraph | hyperedges: hypergraph.hyperedges ++ [vertices]} end @doc """ Removes a vertex from the hypergraph and all hyperedges containing it. """ @spec remove_vertex(t(), vertex()) :: t() def remove_vertex(%__MODULE__{} = hypergraph, vertex) do new_hyperedges = hypergraph.hyperedges |> Enum.map(&List.delete(&1, vertex)) |> Enum.reject(&(&1 == [])) %{hypergraph | hyperedges: new_hyperedges} end @doc """ Removes a hyperedge from the hypergraph. """ @spec remove_hyperedge(t(), hyperedge()) :: t() def remove_hyperedge(%__MODULE__{} = hypergraph, vertices) when is_list(vertices) do new_hyperedges = List.delete(hypergraph.hyperedges, vertices) %{hypergraph | hyperedges: new_hyperedges} end @doc """ Returns all vertices in the hypergraph. """ @spec vertices(t()) :: MapSet.t(vertex()) def vertices(%__MODULE__{} = hypergraph) do Enum.reduce(hypergraph.hyperedges, MapSet.new(), fn edge, acc -> MapSet.union(acc, MapSet.new(edge)) end) end @doc """ Returns all hyperedges in the hypergraph. """ @spec hyperedges(t()) :: [hyperedge()] def hyperedges(%__MODULE__{} = hypergraph) do hypergraph.hyperedges end @doc """ Returns the degree of a vertex (number of hyperedges containing it). """ @spec degree(t(), vertex()) :: non_neg_integer() def degree(%__MODULE__{} = hypergraph, vertex) do hypergraph.hyperedges |> Enum.count(&(vertex in &1)) end @doc """ Returns all vertices that share at least one hyperedge with the given vertex. """ @spec neighbors(t(), vertex()) :: MapSet.t(vertex()) def neighbors(%__MODULE__{} = hypergraph, vertex) do hypergraph.hyperedges |> Enum.filter(&(vertex in &1)) |> Enum.reduce(MapSet.new(), fn edge, acc -> MapSet.union(acc, MapSet.new(edge)) end) |> MapSet.delete(vertex) end @doc """ Returns hyperedges that contain the given vertex. """ @spec incident_hyperedges(t(), vertex()) :: [hyperedge()] def incident_hyperedges(%__MODULE__{} = hypergraph, vertex) do hypergraph.hyperedges |> Enum.filter(&(vertex in &1)) end @doc """ Returns the size of a hyperedge (number of vertices it contains). """ @spec hyperedge_size(hyperedge()) :: non_neg_integer() def hyperedge_size(hyperedge) when is_list(hyperedge) do length(hyperedge) end @doc """ Checks if two vertices are connected by at least one hyperedge. """ @spec connected?(t(), vertex(), vertex()) :: boolean() def connected?(%__MODULE__{} = hypergraph, vertex1, vertex2) do hypergraph.hyperedges |> Enum.any?(fn hyperedge -> vertex1 in hyperedge and vertex2 in hyperedge end) end @doc """ Returns the number of vertices in the hypergraph. """ @spec vertex_count(t()) :: non_neg_integer() def vertex_count(%__MODULE__{} = hypergraph) do hypergraph |> vertices() |> MapSet.size() end @doc """ Returns the number of hyperedges in the hypergraph. """ @spec hyperedge_count(t()) :: non_neg_integer() def hyperedge_count(%__MODULE__{} = hypergraph) do length(hypergraph.hyperedges) end @doc """ Converts the hypergraph to a regular graph by creating pairwise edges for every pair of vertices that share a hyperedge. Returns a list of 2-element tuples representing edges. """ @spec to_graph(t()) :: [{vertex(), vertex()}] def to_graph(%__MODULE__{} = hypergraph) do hypergraph.hyperedges |> Enum.flat_map(fn hyperedge -> unique = Enum.uniq(hyperedge) for v1 <- unique, v2 <- unique, v1 < v2, do: {v1, v2} end) |> Enum.uniq() end @doc """ Returns statistics about the hypergraph. """ @spec stats(t()) :: map() def stats(%__MODULE__{} = hypergraph) do hyperedge_sizes = Enum.map(hypergraph.hyperedges, &length/1) degrees = hypergraph |> vertices() |> Enum.map(°ree(hypergraph, &1)) %{ vertex_count: vertex_count(hypergraph), hyperedge_count: hyperedge_count(hypergraph), max_hyperedge_size: if(hyperedge_sizes == [], do: 0, else: Enum.max(hyperedge_sizes)), min_hyperedge_size: if(hyperedge_sizes == [], do: 0, else: Enum.min(hyperedge_sizes)), avg_hyperedge_size: if(hyperedge_sizes == [], do: 0, else: Enum.sum(hyperedge_sizes) / length(hyperedge_sizes) ), max_degree: if(degrees == [], do: 0, else: Enum.max(degrees)), min_degree: if(degrees == [], do: 0, else: Enum.min(degrees)), avg_degree: if(degrees == [], do: 0, else: Enum.sum(degrees) / length(degrees)) } end end