-
Notifications
You must be signed in to change notification settings - Fork 18
Defining and Populating Graphs
Declarative specification of the graph is via the definition of the hyper graph, i.e. the user specifies sets of nodes (hyper-nodes) and sets of edges between these node sets (hyper-edges).
Everything below should be surrounded by:
trait MyModel extends DataModel {
// everything below
}
Defining a hypernode just requires the type of the underlying instances.
val n1 = node[T1]
val n2 = node[T2]
We can also create join nodes, i.e. nodes that contain pairs of instances, using a specific matcher.
val j = join(n1, n2)( (i1,i2) => i1.key == i2.key) // is of type Node[(T1,T2)]
We just send the instances to populate.
val t1s: Iterable[T1] = ...
n1.populate(t1s)
// for join nodes, you can directly, add, or through a child node
j.populate(t1s zip t2s) // works
n2.populate(t2s) // automatically populates j as well
Note: that instances are deduplicated inside populate, i.e. only distinct instances can be stored in nodes.
You can also specify train
or test
.
n1.populate(t1s, true) // is training data
n1.populate(t1s, false) // is test data
Defining the edge requires the source and target nodes.
val e1 = edge(n1, n2)
val e2 = edge(n1, n1) // self-loops okay, but it is still asymmetric (think parents)
// for symmetric edges (think siblings, spouses, etc.)
val e3 = symmEdge(n1,n1) // can be different nodes, but with same base types
// can be given an optional 'name'
val e4 = edge(n2, n1, 'myedge)
To define which edges to instantiate within a hyper-edge, we define sensors and matchers to identify the links.
Note: defining the sensors doesn't actually add the instances, till node.populate()
is called.
Doesn't add new instances to the nodes, just links existing instances
// can be used for any configuration: one-to-one, many-to-many, etc.
e1.addSensor((i1,i2) => i1.id == i2.fid)
Given one of the instances in the neighboring node, create instances in the other node, and link them.
e1.addSensor(i1 => i1.neighs) // i1.neighs: Iterable[T2]
e1.addSensor(i1 => i1.parent) // i1.parent: T2
Sometimes, for asymmetric edges, it is often easier to specify the sensor from the target node to source node, for example, to specify a many-to-one relation where the neighbors are stored at the destination.
// reminder: e4 = edge(n2, n1)
e4.addReverseSensor(i1 => i1.neighs)
e4.addReverseSensor(i1 => i1.parent)
You cannot directly populate edges, instead populate nodes, which automatically calls the sensors of the edges connected to the nodes.