Advanced usage¶
This Jupyter notebook shows some advanced features of the Python package mevis. The .ipynb file can be found here.
The central function of this package is mv.plot
for visualizing OpenCog’s AtomSpaces. Internally it calls three functions that are also relevant for users:
mv.filter
to reduce the AtomSpace to a selection of some Atoms of interest.Its arguments enable various ways of filtering:
target
select some Atomscontext
decides whether the selection is expanded to some context around the Atoms"atom"
: no extension of the selection"in"
: extension to incoming Atoms, can be donen
times by passing a tuple("in", n)
"out"
: extension to outgoing Atoms, can be donen
times with("out", n)
"in_out"
: extension to incoming and outgoing Atoms, can be donen
times with("in_out", n)
, which means an extension to a neighborhood of sizen
"subgraph"
: extension to the entire subgraph, which means using the selected Atoms as roots and following their outgoing edges until reaching only leaves
mode
decides whether the selected Atoms are included to or excluded from the output
mv.convert
to transform an AtomSpace to a normal graph with two types of nodes, which correspond to OpenCog’s Node and Link types.Several arguments allow to add annotations to the graph, e.g. node label, size, color and shape, or edge label and color. These annotations are recognized by the plotting function and translated into visual elements and their appearance.
mv.layout
to calculate x and y coordinates for each node in the graph.Several layout methods are available. Some depend on Graphviz (e.g.
neato
,dot
,twopi
), others come with NetworkX (e.g.bipartite
,shell
,spring
).
These three functions can also be called by the user, for example to apply multiple filtering steps one after another, or to export the resulting annotated graph to a gml file for external tools. Note that mv.plot
can also use a graph object as input, but most arguments are ignored in that case, because no filter, convert and layout steps are performed in that case.
[1]:
import mevis as mv
from opencog.atomspace import types
Load an AtomSpace¶
[2]:
atomspace = mv.load('moses.scm')
mv.plot(atomspace, 'vis', 'dot')
[2]:
Filter an AtomSpace¶
The filter function needs an AtomSpace or a list of Atoms as input and returns a list of Atoms. There are multiple ways to specify which Atoms shall be filtered.
1) By an Atom or a list of Atoms¶
[3]:
atoms = atomspace.get_atoms_by_type(types.Link)
atoms = mv.filter(atomspace, target=atoms)
mv.plot(atoms, 'vis', 'dot')
[3]:
[4]:
atom = list(atomspace)[7]
print(atom)
(AndLink
(PredicateNode "$4")
(NotLink
(PredicateNode "$3")))
[5]:
for context in ('atom', 'in', 'out', 'both', 'in-tree', 'out-tree', ('in', 2), ('out', 2), ('both', 2)):
# Print current context
print()
print('Filter context: "{}"'.format(context))
# Apply filter with current context
atoms = mv.filter(atomspace, target=atom, context=context)
# Convert atoms to graph, increase size of the root atom from which the selection is expanded to some context
graph = mv.convert(atoms, node_size=lambda a: 18 if a == atom else 10)
# Calculate a hierarchical layout with dot from Graphviz
graph = mv.layout(graph, 'dot')
# Plot and display
fig = mv.plot(graph)
fig.display(inline='True')
Filter context: "atom"
Filter context: "in"
Filter context: "out"
Filter context: "both"
Filter context: "in-tree"
Filter context: "out-tree"
Filter context: "('in', 2)"
Filter context: "('out', 2)"
Filter context: "('both', 2)"
2) By an Atom name or a list of Atom names¶
[6]:
atoms = mv.filter(atomspace, target=['$2', '$3'], context='in')
mv.plot(atoms, 'vis', 'dot')
[6]:
[7]:
for context_size in (0, 1, 2, 3):
print('Context size {}'.format(context_size))
atoms = mv.filter(atomspace, target='$2', context=('in', context_size))
mv.plot(atoms, 'vis', 'dot').display(inline='True')
Context size 0
Context size 1
Context size 2
Context size 3
3) By an Atom type or a list of Atom types¶
[8]:
atoms = mv.filter(atomspace, target=['AndLink', 'OrLink', 'NotLink'])
mv.plot(atoms, 'vis', 'dot')
[8]:
[9]:
atoms = mv.filter(atomspace, target=types.PredicateNode, mode='exclude')
mv.plot(atoms, 'vis', 'dot')
[9]:
4) By a function that returns True or False¶
The function gets an Atom as input and needs to return True or False, which causes the Atom to be selected or deselected, respectively.
[10]:
def select(atom):
if atom.name.startswith('$'):
return False
return True
atoms = mv.filter(atomspace, target=select)
mv.plot(atoms, 'vis', 'dot')
[10]:
[11]:
atoms = mv.filter(atomspace, target=select, mode='exclude')
mv.plot(atoms, 'vis', 'dot')
[11]:
Python’s lambda functions are also accepted and often more concise. Essentially they are unnamed functions and don’t use the return keyword.
[12]:
atoms = mv.filter(atomspace, target=lambda atom: atom.is_link())
mv.plot(atoms, 'vis', 'dot')
[12]:
5) By a combination of the previous¶
The result of one call can be used as input for another call. This enables sequential filtering, i.e. changing the subset of selected Atoms step-by-step.
[13]:
atoms = mv.filter(atomspace, target='OrLink', context='out-tree')
atoms = mv.filter(atoms, target=lambda atom: atom.name.startswith('$'), mode='exclude')
mv.plot(atoms, 'vis', 'dot')
[13]:
Convert an AtomSpace to a graph¶
The convert
function needs an AtomSpace or a list of Atoms as input and returns a DiGraph or Graph object from the NetworkX library, depending on whether the graph_directed
argument is set to True or False, respectively. By default it adds some annotations to the graph like node_color
, but it can be turned off by setting graph_annotated=False
.
[14]:
graph = mv.convert(atomspace)
mv.plot(graph)
[14]:
[15]:
graph = mv.convert(atomspace, graph_annotated=False, graph_directed=False)
mv.plot(graph)
[15]:
[16]:
graph = mv.convert(
atomspace, node_color='#000099', node_shape='hexagon', node_size=20,
edge_color='lightgray', edge_size=4)
mv.plot(graph)
[16]:
[17]:
def calc_node_color(atom):
if atom.is_node():
return 'red'
elif atom.type_name == 'AndLink':
return 'blue'
elif atom.type_name == 'OrLink':
return 'green'
elif atom.type_name == 'NotLink':
return 'orange'
graph = mv.convert(atomspace, graph_directed=False, node_color=calc_node_color, node_size=16, edge_color='gray')
mv.plot(graph)
[17]:
[18]:
def calc_color(atom):
if atom.is_node():
return 'red'
if atom.type_name == 'AndLink':
return 'blue'
if atom.type_name == 'OrLink':
return 'green'
return 'orange'
def calc_shape(atom):
if atom.type_name == 'AndLink':
return 'rectangle'
if atom.type_name == 'OrLink':
return 'hexagon'
return 'circle'
graph = mv.convert(
atomspace,
graph_directed=False,
node_label=lambda atom: atom.name if atom.is_node() else atom.type_name.replace('Link', ''),
node_color=calc_color,
node_opacity=0.9,
node_size=lambda atom: 20 if atom.type_name in ['AndLink', 'OrLink'] else 12,
node_shape=calc_shape,
node_border_color='white',
node_border_size=2.0,
node_label_color=calc_color,
node_label_size=12.0,
node_hover=lambda atom: 'A {} with Atomese code:\n{}'.format(atom.type_name, atom.short_string()),
node_click=lambda atom: atom.short_string(),
node_image=None,
node_properties=lambda atom: dict(x=-300) if atom.is_node() else dict(x=-300+200*len(atom.out)),
edge_label=lambda atom1, atom2: '{}{}'.format(atom1.type_name[0], atom2.type_name[0]),
edge_color=lambda atom1, atom2: 'lightgray' if atom2.is_node() else calc_color(atom1),
edge_opacity=0.5,
edge_size=lambda atom1, atom2: 5 if atom2.is_node() else 2.5,
edge_label_color=lambda atom1, atom2: calc_color(atom1),
edge_label_size=8,
edge_hover=lambda atom1, atom2: '{} to {}'.format(atom1.type_name, atom2.type_name),
edge_click=lambda atom1, atom2: 'Edge connects {} with {}'.format(atom1.type_name, atom2.type_name),
)
mv.plot(graph, 'd3', edge_curvature=0.2, show_edge_label=True, many_body_force_strength=-1000)
[18]:
Layout a graph¶
The layout function needs a DiGraph or Graph object from NetworkX as input, but also accepts an AtomSpace or list of Atoms, which it passes through the convert
function to get a graph. It returns a graph with x and y coordinates as node annotations.
[19]:
graph = mv.layout(atomspace, 'dot')
mv.plot(graph)
[19]:
[20]:
graph = mv.layout(graph, 'twopi')
mv.plot(graph)
[20]:
Explicitly use filter, convert and layout¶
Calling these functions explicitly allows to use the resulting graph for different purposes, such as inspecting, plotting or exporting it.
[21]:
# filter
atoms = atomspace.get_atoms_by_type(types.Node) # an efficient OpenCog function, it includes subtypes by default!
atoms = mv.filter(atoms, target=atoms, context=('in', 1))
# convert
graph = mv.convert(
atoms,
node_label=lambda atom: atom.name if atom.is_node() else atom.type_name.replace('Link', ''),
node_size=lambda atom: 3 * (len(atom.out) + 2),
node_shape=lambda atom: 'rectangle' if atom.is_node() else 'circle',
edge_color='#bbbbbb')
# layout
graph = mv.layout(graph, 'neato')
# export: possible because the individual functions were used and only the result is put into plot()
mv.export(graph, 'moses_filtered_annotated.gml', overwrite=True)
# plot
mv.plot(graph)
[21]:
Implicitly use filter, convert and layout¶
Calling these functions implicitly during plotting can be done by providing corresponding arguments. It does not allow to use the resulting graph for other purposes, and it does not enable sequential filtering steps, but the basic use case of filter → convert → layout → plot becomes easier.
[22]:
# shorter: plot with arguments for the internally called filter, convert and layout functions
mv.plot(
atomspace,
layout_method='neato',
filter_target=lambda atom: atom.is_node(),
filter_context=('in', 1),
node_label=lambda atom: atom.name if atom.is_node() else atom.type_name.replace('Link', ''),
node_size=lambda atom: 3 * (len(atom.out) + 2),
node_shape=lambda atom: 'rectangle' if atom.is_node() else 'circle',
edge_color='#bbbbbb',
)
[22]: