# RaGraph basics tutorial¶

## Create a Graph¶

Let's start by building a graph! A graph consists of nodes and edges, sometimes respectively called vertices and arcs but we use the former. We can start with an empty Graph object:

 1 2 3 from ragraph.graph import Graph g = Graph() 

You can slowly populate the empty Graph object or load nodes and edges in bulk, both of which we'll see later this tutorial and how-to guides.

When creating a Node, all we need is a name. Let's create a node called "A".

 1 2 3 4 5 6 7 8 from ragraph.graph import Graph from ragraph.node import Node g = Graph() a = Node("A") g.add_node(a) assert g["A"] == a, "It's in!" 

What you see here, is that we create a Node object and add it to the Graph. We can fetch the node from the graph via its name, which has to be unique within the Graph. Also, there are quite some attributes attached to the Node by default. These are mostly metadata which you can safely ignore for now. The important thing is that it got our name right!

An edge runs from a source node to a target node, which means that it is directed. Those two nodes are the only required parameters to create one! Lets create a second node called "B" and an edge running from "A" to "B".

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 from ragraph.graph import Graph from ragraph.node import Node from ragraph.edge import Edge # Or from ragraph.graph import Node, Edge, Graph g = Graph() a = Node("A") b = Node("B") g.add_node(a) g.add_node(b) ab = Edge(a, b) g.add_edge(ab) assert g["A", "B"] == [ab], "All hooked up!" 

So that concludes our first Edge! You can query all edges between two nodes (you can add any amount!) by supplying both a source and target Node name as a tuple. Again, the same metadata properties have been added as we've seen before, which you can safely ignore.

Note

Because we did not supply a name to the Edge, it has been assigned a UUID (Universally Unique IDentifier) to recognize it by.

## Create a hierarchical Graph¶

Suppose we want to create a hierarchial Graph where nodes have parent-child relationships. You can! Let's create a Graph with four children and then create a parent-child relationship.

 1 2 3 4 5 6 7 8 from ragraph.graph import Graph from ragraph.node import Node g = Graph(nodes=[Node(i) for i in "ABCD"]) # Creates four nodes. g["A"].children = [g[i] for i in "BCD"] # Set children of "A". assert g["B"] in g["A"].children, "Yup, B is part of A's children!" assert g["B"].parent == g["A"], "B got a parent node!" 

Which means the children have been added to A's children property. The parent relationship is updated automatically, too, though!

It's perfectly possible to add edges to hierarchical graphs. There are no restrictions as to which source nodes can target which target nodes, as long as both exist in the graph.

Note

Some algorithms leverage parent-child relationships and the edges between descendant when calculating weights between nodes, so make sure you understand how the weights or relations between nodes are calculated in an algorithm so you provide it with the correct input.

In RaGraph, both Node, Edge and Graph objects support an identical metadata structure. This structure consists of the following elements:

• kind: The main category or domain of a node or edge.
• labels: A list of labels you can to attach to any node or edge.
• weights: A dictionary of keys to (numeric) values. For instance a cost property for a node or the strength of an edge.
• annotations: A rather flexible ragraph.generic.Annotations object you can store pretty much any additional information in. You can initialize it using a dictionary as you will see in the following example.

An example of a fully annotated Node or Edge would then be:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from ragraph.graph import Graph from ragraph.node import Node from ragraph.edge import Edge fancy_node = Node( name="fancy node", kind="exquisite", labels=["grand", "grotesque"], weights={"cost": 1e6}, annotations={"comment": "Some additional information."}, ) fancy_edge = Edge( source=fancy_node, target=fancy_node, name="fancy edge", kind="exquisite", labels=["grand", "grotesque"], weights={"cost": 1e6}, annotations={"comment": "Some additional information."}, ) fancy_graph = Graph(nodes=[fancy_node], edges=[fancy_edge]) assert fancy_graph["fancy node"].annotations.comment == "Some additional information." 

Where most properties are fairly explanatory, the Annotations object might need a little explaining. It's essentially a class you can supply any (keyword) arguments or a dictionary to. The keys are used to form property names. Keep in mind that it is recommended to only add serializable objects to this class, so you can export and import your data with ease.

## As a dictionary¶

All of the Node, Edge, Annotations, and Graph classes feature a json_dict property which is a regular Python dictionary containing only serializable data that's readily exportable using Python's default json module or most other Python I/O packages.

Let's review the it for some of the previously introduced variables:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from ragraph.graph import Graph from ragraph.node import Node from ragraph.edge import Edge fancy_node = Node( name="fancy node", kind="exquisite", labels=["grand", "grotesque"], weights={"cost": 1e6}, annotations={"comment": "Some additional information."}, ) fancy_edge = Edge( source=fancy_node, target=fancy_node, name="fancy edge", kind="exquisite", labels=["grand", "grotesque"], weights={"cost": 1e6}, annotations={"comment": "Some additional information."}, ) fancy_graph = Graph(nodes=[fancy_node], edges=[fancy_edge]) assert fancy_node.as_dict() == { "name": "fancy node", "parent": None, "children": [], "is_bus": False, "kind": "exquisite", "labels": ["grand", "grotesque"], "weights": {"cost": 1000000.0}, "annotations": {"comment": "Some additional information."}, } assert fancy_edge.as_dict() == { "source": "fancy node", "target": "fancy node", "name": "fancy edge", "kind": "exquisite", "labels": ["grand", "grotesque"], "weights": {"cost": 1000000.0}, "annotations": {"comment": "Some additional information."}, } 

Note

This works for Graph's themselves, too, but you get the point.

## Node properties¶

Nodes have specific properties such as width, depth, and height to name a few. These often come into play when analyzing hierarchies of nodes using clustering or bus detection algorithms.

A short summary of most of the available properties:

## Graph utilities¶

Up until now we've more or less treated the Graph object as a Node and Edge store but it is more than that! The Graph object has several useful methods, too, which we will explore in this section.

### Kinds, labels, weights¶

To check what kinds, labels, or weights have been used, you can use any of the following properties on any Graph object:

### Querying nodes¶

So far, we've discussed getting nodes by name using graph["node name"]. However, you can use any of the following methods to retrieve specific nodes as well:

### Querying edges¶

Previously, we have retrieved edges using graph["source name", "target name"] or via their edge ID using graph.id_to_edge[id]. Similarly to the nodes, we have the following methods to retrieve specific edges:

An adjacency matrix represents the sum of edge weights between sets of nodes. The nodes are identically ordered on both the matrix' axes. A cell value on [i, j] then denotes the sum of edge weights of edges going from the j node (column) to the i node (row). This follows the IR/FAD (inputs in rows, feedback above diagonal) convention. Here is a little example of the graph.get_adjacency_matrix method:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 from ragraph.graph import Graph from ragraph.node import Node from ragraph.edge import Edge a = Node("a") b = Node("b") nodes = [a, b] edges = [ Edge(a, a, weights={"strength": 1}), Edge(b, a, weights={"flow": 3}), Edge(a, b, weights={"strength": 9}), ] g = Graph(nodes=nodes, edges=edges) g.get_adjacency_matrix(loops=True) 

Or, if you want to omit self loops and only look at the flow weights:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from ragraph.graph import Graph from ragraph.node import Node from ragraph.edge import Edge a = Node("a") b = Node("b") nodes = [a, b] edges = [ Edge(a, a, weights={"strength": 1}), Edge(b, a, weights={"flow": 3}), Edge(a, b, weights={"strength": 9}), ] g = Graph(nodes=nodes, edges=edges) g.get_adjacency_matrix(loops=True) adj = g.get_adjacency_matrix(loops=False, only=["flow"]) assert adj.tolist() == [ [0, 3], [0, 0], ] 

Please take a look at the method's documentation for more information: ragraph.graph.Graph.get_adjacency_matrix.

Note

Similarly, you can calculate a mapping matrix using ragraph.graph.Graph.get_mapping_matrix where the rows and columns do not need to be symmetrical. This is commonly used to calculate a mapping from nodes of one domain (node kind) to another.

## Where to next?¶

Feel free to check either the How-to guides for more specific use cases, or dive straight into the Reference for some nicely formatted reference documentation and get coding!