Yog.Operation (YogEx v0.97.0)

Copy Markdown View Source

Graph operations - Set-theoretic operations, composition, and structural comparison.

This module implements binary operations that treat graphs as sets of nodes and edges, following NetworkX's "Graph as a Set" philosophy. These operations allow you to combine, compare, and analyze structural differences between graphs.

Set-Theoretic Operations

FunctionDescriptionUse Case
union/2All nodes and edges from both graphsCombine graph data
intersection/2Only nodes and edges common to bothFind common structure
difference/2Nodes/edges in first but not secondFind unique structure
symmetric_difference/2Edges in exactly one graphFind differing structure

Composition & Joins

FunctionDescriptionUse Case
disjoint_union/2Combine with automatic ID re-indexingSafe graph combination
cartesian_product/4Multiply graphs (grids, hypercubes)Generate complex structures
compose/2Merge overlapping graphs with combined edgesLayered systems
power/2k-th power (connect nodes within distance k)Reachability analysis

Structural Comparison

FunctionDescriptionUse Case
subgraph?/2Check if first is subset of secondValidation, pattern matching
isomorphic?/2Check if graphs are structurally identicalGraph comparison

Examples

# Two triangle graphs with overlapping IDs
iex> triangle1 = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 0, with: 1)
iex> triangle2 = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 0, with: 1)
iex> # disjoint_union re-indexes the second graph automatically
...> combined = Yog.Operation.disjoint_union(triangle1, triangle2)
iex> # Result: 6 nodes (0-5), two separate triangles
...> Yog.Model.order(combined)
6

# Finding common structure
iex> graph_a = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> graph_b = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> common = Yog.Operation.intersection(graph_a, graph_b)
iex> Yog.Model.order(common)
2

Summary

Functions

Returns the Cartesian product of two graphs.

Composes two graphs by merging overlapping nodes and combining their edges.

Returns a graph containing nodes and edges that exist in the first graph but not in the second.

Computes the disjoint union of two graphs.

Returns a graph containing only nodes and edges that exist in both input graphs.

Checks if two graphs are isomorphic (structurally identical).

Returns the line graph of a graph.

Returns the k-th power of a graph.

Checks if the first graph is a subgraph of the second graph.

Returns a graph containing edges that exist in exactly one of the input graphs.

Returns a graph containing all nodes and edges from both input graphs.

Functions

cartesian_product(first, second, default_first, default_second)

@spec cartesian_product(Yog.Graph.t(), Yog.Graph.t(), any(), any()) :: Yog.Graph.t()

Returns the Cartesian product of two graphs.

Creates a new graph where each node represents a pair of nodes from the input graphs. Useful for generating grids, hypercubes, and other complex structures.

Time Complexity: O(V₁ × V₂ + E₁ × V₂ + E₂ × V₁)

Parameters

  • first - First input graph
  • second - Second input graph
  • default_first - Default edge data for edges derived from first
  • default_second - Default edge data for edges derived from second

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
iex> product = Yog.Operation.cartesian_product(g1, g2, 0, 0)
iex> # 2x2 grid structure: 4 nodes
...> Yog.Model.order(product)
4

compose(first, second)

@spec compose(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Composes two graphs by merging overlapping nodes and combining their edges.

This is equivalent to union/2 - both graphs are merged together with other's data taking precedence on conflicts.

Time Complexity: O(V₁ + V₂ + E₁ + E₂)

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> composed = Yog.Operation.compose(g1, g2)
iex> Yog.Model.order(composed)
3

difference(first, second)

@spec difference(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Returns a graph containing nodes and edges that exist in the first graph but not in the second.

Any node that appears in second is removed from the result, along with all its incident edges. Of the remaining nodes, only edges that do not appear in second are kept.

Time Complexity: O(V + E)

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(3, nil)
iex> diff = Yog.Operation.difference(g1, g2)
iex> Yog.Model.order(diff)
2
iex> Yog.Model.has_edge?(diff, 1, 2)
true

disjoint_union(graph_a, graph_b)

@spec disjoint_union(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Computes the disjoint union of two graphs.

Unlike a simple join, this function guarantees that nodes from Graph A and Graph B remain distinct by tagging their IDs as {0, id} and {1, id}, even if they share the same original ID.

The resulting graph uses the kind (:directed or :undirected) from graph_a. Combining graphs of different kinds may lead to unexpected edge behavior.

Time Complexity: O(V₁ + V₂ + E₁ + E₂)

Example

iex> g1 = Yog.directed() |> Yog.add_node("root", "Data A")
iex> g2 = Yog.directed() |> Yog.add_node("root", "Data B")
iex> union = Yog.Operation.disjoint_union(g1, g2)
iex> Yog.Model.node_count(union)
2
iex> Yog.Model.node(union, {0, "root"})
"Data A"

intersection(first, second)

@spec intersection(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Returns a graph containing only nodes and edges that exist in both input graphs.

For directed graphs, a directed edge must exist in both graphs to be kept. For undirected graphs, an undirected edge must exist in both graphs.

Time Complexity: O(V + E)

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> intersection = Yog.Operation.intersection(g1, g2)
iex> Yog.Model.order(intersection)
3

isomorphic?(first, second)

@spec isomorphic?(Yog.Graph.t(), Yog.Graph.t()) :: boolean()

Checks if two graphs are isomorphic (structurally identical).

Two graphs are isomorphic if there exists a bijection between their node sets that preserves adjacency. This implementation uses degree sequence comparison and backtracking to test for isomorphism.

Time Complexity: O(V log V + E) for the fast checks; exponential in the worst case due to backtracking (not recommended for large graphs).

Examples

# Two identical triangles are isomorphic
iex> g1 = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 0, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(10, nil)
...> |> Yog.add_node(20, nil)
...> |> Yog.add_node(30, nil)
...> |> Yog.add_edge_ensure(from: 10, to: 20, with: 1)
...> |> Yog.add_edge_ensure(from: 20, to: 30, with: 1)
...> |> Yog.add_edge_ensure(from: 30, to: 10, with: 1)
iex> Yog.Operation.isomorphic?(g1, g2)
true
iex> # Triangle is not isomorphic to a path
iex> path = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> Yog.Operation.isomorphic?(g1, path)
false

line_graph(graph, default_weight \\ 1)

@spec line_graph(Yog.Graph.t(), term()) :: Yog.Graph.t()

Returns the line graph of a graph.

The line graph L(G) is a graph where each node represents an edge of G, and two nodes are adjacent if and only if their corresponding edges share a common endpoint in G.

For directed graphs, two edges (u, v) and (x, y) are adjacent in the line graph if and only if v == x (the head of the first edge matches the tail of the second edge). This is the standard line digraph definition.

For undirected graphs, two edges {u, v} and {x, y} are adjacent if and only if they share at least one endpoint.

Line graph nodes are represented as {u, v} tuples. For undirected graphs, the tuple follows the same ordering convention as Yog.Model.all_edges/1 (u <= v using Erlang term ordering).

Time Complexity: O(E²) where E is the number of edges in the original graph

Parameters

  • graph - The input graph
  • default_weight - Weight for edges in the line graph (default: 1)

Examples

iex> path = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 10)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 20)
iex> lg = Yog.Operation.line_graph(path, 1)
iex> # Line graph of a path has 2 nodes ({0,1} and {1,2}) and 1 edge
iex> Yog.Model.order(lg)
2
iex> Yog.Model.has_edge?(lg, {0, 1}, {1, 2})
true

power(graph, k, default_weight)

@spec power(Yog.Graph.t(), integer(), any()) :: Yog.Graph.t()

Returns the k-th power of a graph.

The k-th power of a graph G, denoted G^k, is a graph where two nodes are adjacent if and only if their distance in G is at most k.

Self-loops are never added.

Time Complexity: O(V × (V + E)) in the worst case

Parameters

  • graph - The input graph
  • k - The power (distance threshold)
  • default_weight - Weight for newly created edges

Examples

iex> path = Yog.undirected()
...> |> Yog.add_node(0, nil)
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 0, to: 1, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> # G^2 connects nodes at distance <= 2
...> power = Yog.Operation.power(path, 2, 1)
iex> # Node 0 and 2 should now be connected (distance 2 in original)
...> Yog.Model.has_edge?(power, 0, 2)
true

subgraph?(potential, container)

@spec subgraph?(Yog.Graph.t(), Yog.Graph.t()) :: boolean()

Checks if the first graph is a subgraph of the second graph.

Returns true if all nodes and edges in the first graph exist in the second.

Time Complexity: O(Vₚ + Eₚ) where Vₚ and Eₚ are the nodes and edges of the potential subgraph

Examples

iex> container = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> potential = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> Yog.Operation.subgraph?(potential, container)
true
iex> not_subgraph = Yog.undirected()
...> |> Yog.add_node(4, nil)
...> |> Yog.add_node(5, nil)
...> |> Yog.add_edge_ensure(from: 4, to: 5, with: 1)
iex> Yog.Operation.subgraph?(not_subgraph, container)
false

symmetric_difference(first, second)

@spec symmetric_difference(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Returns a graph containing edges that exist in exactly one of the input graphs.

The result is the union of difference(first, second) and difference(second, first). Nodes that have no incident unique edges will not appear in the result.

Time Complexity: O(V₁ + V₂ + E₁ + E₂)

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> sym_diff = Yog.Operation.symmetric_difference(g1, g2)
iex> Yog.Model.order(sym_diff)
1
iex> Yog.Model.edge_count(sym_diff)
0

union(base, other)

@spec union(Yog.Graph.t(), Yog.Graph.t()) :: Yog.Graph.t()

Returns a graph containing all nodes and edges from both input graphs.

Node data and edge weights from other take precedence on conflicts. Both graphs must have the same kind (:directed or :undirected); the result inherits the kind from base.

Time Complexity: O(V₁ + V₂ + E₁ + E₂)

Examples

iex> g1 = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
iex> g2 = Yog.undirected()
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> union = Yog.Operation.union(g1, g2)
iex> Yog.Model.order(union)
3