Tutorial#

In this tutorial, you learn

  • how to create a Hypergraph;

  • how to add nodes to a Hypergraph and access node data attributes;

  • how to add hyperedges to a Hypergraph and access hyperedge data attributes; and

  • how to remove nodes and hyperedges from a Hypergraph.

Creating a hypergraph#

Start by creating an empty hypergraph.

>>> import pypergraph as ppg
>>> h = ppg.Hypergraph()

Adding nodes#

Add a node using Hypergraph.add_node(). Nodes are identified by a node name, which must be a hashable object.

>>> h.add_node("A")
>>> h.add_node(0)
>>> try:
...     h.add_node(["not hashable"])
... except TypeError:
...     print("Cannot use unhashable object as node name")
...
Cannot use unhashable object as node name

Multiple nodes can be added at once using Hypergraph.add_nodes().

>>> h.add_nodes(1, 2)
>>> h.nodes
NodeDataAccess(['A', 0, 1, 2])

A node may carry data attributes. Data attributes can be specified when using Hypergraph.add_node() but not Hypergraph.add_nodes().

>>> h.add_node("B", attr_dict={"special": True})

A node has a canonical data attribute inner. It may be used to store any underlying object represented by a node. It defaults to the node name and need not be hashable.

>>> inner_view = h.nodes.data()
>>> inner_view["B"]
'B'
>>> h.add_node("C", attr_dict={"inner": "This is the underlying object."})
>>> inner_view["C"]
'This is the underlying object.'

Data attributes can be accessed after node creation with Hypergraph.nodes.

>>> special_view = h.nodes.data(attrs="special")
>>> special_view["B"]
True
>>> try:
...     special_view["C"]
... except KeyError:
...     print("Attribute was not defined for this node")
...
Attribute was not defined for this node
>>> special_view_with_default = h.nodes.data(attrs="special", default=False)
>>> special_view_with_default["B"]
True
>>> special_view_with_default["C"]
False

Data attributes can be modified by dictionary mutation.

>>> attr_mod = h.nodes.data(attrs=True)  # `True` to allow mutation
>>> attr_mod["A"]
{}
>>> attr_mod["B"]
{'special': True}
>>> attr_mod["C"]
{'inner': 'This is the underlying object.'}
>>> attr_mod["B"]["inner"] = 5
>>> inner_view["B"]
5
>>> attr_mod[0]["special"] = True
>>> special_view[0]
True

Adding hyperedges#

Add an undirected hyperedge using Hypergraph.add_edge(). Incident nodes are denoted by the shorthand V.

>>> V = ppg.V
>>> anon_label0 = h.add_edge(V(0, 1, 2))

Every hyperedge has a hyperedge label as an identifier. A label is provisioned by default but can be provided explicitly. Labels must be hashable.

>>> h.add_edge(V(0, 2), label="explicit")
'explicit'
>>> try:
...     h.add_edge(V(0, 2), label=["item1", "item2"])
... except TypeError:
...     print("Cannot use unhashable object as hyperedge label")
...
Cannot use unhashable object as hyperedge label

Directed hyperedge creation uses the right shift operator >> to indicate direction.

>>> anon_label1 = h.add_edge(V(0, 2) >> V(1))  # Hyperedge from {0, 2} to {1}

Multiple hyperedges can be added at once using Hypergraph.add_edges().

>>> (anon_label2, anon_label3) = h.add_edges(V(1) >> V(0), V(0) >> V(1))

Hyperedges may carry data attributes. Data attributes can be specified when using Hypergraph.add_edge() but not Hypergraph.add_edges().

>>> h.add_edge(V(0) >> V(0), label="loop", attr_dict={"dummy": True})
'loop'

In the absence of parallel hyperedges, access data attributes via Hypergraph.edges.

>>> h.edges[V(0) >> V(0)]["dummy"]
True

A hyperedge has a canonical data attribute inner. It may be used to store any underlying object represented by a hyperedge. It defaults to the hyperedge label, which may have been provisioned automatically, and need not be hashable.

>>> h.edges[V(0) >> V(0)].inner
'loop'
>>> h.add_edge(
...     V(1) >> V(1),
...     label="second_loop",
...     attr_dict={"inner": "This is a loop."},
... )
'second_loop'
>>> h.edges[V(1) >> V(1)].inner
'This is a loop.'

Note that Hypergraph.edges is generally only suitable for hypergraphs without parallel hyperedges. When there are parallel hyperedges, Hypergraph.edge_labels should be used instead.

>>> for i in range(3):
...     h.add_edge(
...         V(2) >> V(0, 1),
...         label=f"e_{i}",
...         attr_dict={"iteration": i, "next": i + 1},
...     )
...
'e_0'
'e_1'
'e_2'
>>> iteration_view = h.edge_labels.data(attrs="iteration")
>>> iteration_view["e_0"]
0
>>> attr_mod = h.edge_labels.data(attrs=True)
>>> attr_mod["e_1"]
{'iteration': 1, 'next': 2}

Creation of hyperedges will implicitly create nodes as necessary.

>>> h2 = ppg.Hypergraph()  # Empty hypergraph
>>> _ = h2.add_edge(V(1, 3) >> V(2))
>>> h2.nodes
NodeDataAccess([1, 3, 2])

Removing nodes and hyperedges#

Remove nodes using Hypergraph.remove_nodes(). Remove hyperedges using Hypergraph.remove_edges() or Hypergraph.remove_parallel_edges(). Note that removal of nodes also removes any incident hyperedges.

>>> h3 = ppg.Hypergraph()
>>> h3.add_nodes(*"ABCDE")
>>> _ = h3.add_edges(
...     V("A") >> V("B"),
...     V("B") >> V("C"),
...     V("C") >> V("D"),
...     V(*"ABCD"),
...     V("D") >> V("E"),
...     V("E") >> V(*"ABCD"),
... )
>>> h3.add_edge(V("A") >> V("B"), label="second_AB")
'second_AB'
>>> h3.add_edge(V("B") >> V("C"), label="second_BC")
'second_BC'
>>> h3.nodes
NodeDataAccess(['A', 'B', 'C', 'D', 'E'])
>>> h3.remove_nodes("E")  # Also removes two hyperedges
>>> h3.nodes
NodeDataAccess(['A', 'B', 'C', 'D'])
>>> h3.edges
EdgeIncidenceView([V('A') >> V('B'), V('B') >> V('C'), V('C') >> V('D'), V('A', 'B', 'C', 'D')])
>>> h3.remove_edges("second_AB")
>>> h3.remove_parallel_edges(V("B") >> V("C"))
>>> h3.edges
EdgeIncidenceView([V('A') >> V('B'), V('C') >> V('D'), V('A', 'B', 'C', 'D')])