Factor nodes
A factor node is one factor of a factorised model. The engine creates a FactorNode for each factor and connects it to its variables through interfaces. When you activate the graph, the engine wires each of the node's outbound messages to the rule that computes it.
The engine defines no node. You declare a node with @define_factor_node and its rules with the other macros of MessagePassingRulesBase. The standard nodes, the distributions and the arithmetic, come from StandardMessagePassingRules, and the others from packages of their own (The ecosystem).
The examples use the Gaussian node of The example node.
Creating a node
factornode takes the node type, the variables it connects and a factorisation. Each interface is a (name, variable) pair. The factorisation is a tuple of clusters, each a tuple of interface names:
x, y = randomvar(label = :x), randomvar(label = :y)
node = factornode(Gaussian, [(:v, constvar(1.0)), (:out, y), (:μ, x)], ((:out, :μ), (:v,)))FactorNode Gaussian (stochastic)
out ── y random
μ ── x random
v ── 1.0 constant
clusters: (out, μ) (v)The node draws its interfaces, the variables they connect to and its clusters. The engine puts interfaces and clusters in declaration order, whatever order you give them in.
Without a factorisation, a node has one cluster over every interface, and its rules are those of belief propagation. With one cluster per interface, ((:out,), (:μ,), (:v,)), they are those of mean-field variational message passing. Every interface belongs to exactly one cluster.
Each cluster has a local marginal, keyed by its members. A joint cluster's key is its member tuple, (:out, :μ):
ReactiveMP.get_node_local_marginals(ReactiveMP.getlocalclusters(node))(FactorNodeLocalMarginal((:out, :μ)), FactorNodeLocalMarginal(:v))A node with a group, an interface with any number of members, takes each member as ((group, k), variable). A cluster may hold the whole group, keyed by its name, (:in,), or some of its members, each keyed with its index, (:out, (:in, 1)). The static inputs example below creates a node with a group.
ReactiveMP.AbstractFactorNode — Type
ReactiveMP.AbstractFactorNodeThe supertype of the engine's factor nodes, FactorNode: the type a graph stores its nodes as, as RxInfer's model does.
ReactiveMP.FactorNode — Type
FactorNodeA factor node of the graph: its node type, its interfaces in declaration order, its local clusters, and the function it computes, when it was given one. Create one with factornode, and wire it with ReactiveMP.activate!.
A node type is anything declared with @define_factor_node: a type, such as NormalMeanVariance, or a function, such as +. A group's members are ReactiveMP.IndexedNodeInterfaces, by their index.
See also functionalform, getinterfaces.
ReactiveMP.factornode — Function
factornode(fform, interfaces, factorisation = nothing; nodefn = nothing) -> FactorNodeCreate a factor node of type fform connected to the variables in interfaces. Each connection allocates the variable's stream for the node's messages; nothing is computed until the node is activated with ReactiveMP.activate!.
Arguments
fform: the node type, declared with@define_factor_node;interfaces: a collection of(key, variable)pairs,keybeing the interface's name,:out, or(:m, k)for memberkof a groupm; a name may be an alias. Every declared interface is given, a group's members as1:n, in any order;factorisation: the clusters of the node's local marginals, a tuple of tuples of keys,((:out, :μ), (:v,)).nothingmeans one cluster over every interface. Every interface is in exactly one cluster. A deterministic node ignores it: its clusters are its output alone and the joint over its inputs.
Keywords
nodefn: the function the node computes, which a rule reaches withgetnodefn(ctx.node, Target(:out)). Defaultnothing, none. A node declared withstatic_inputs = :foldneeds it: the members of its group connected to a constant or to data are folded into it (seeReactiveMP.StaticFold), and the others are numbered1:nin their order.
The interfaces and the clusters are kept in declaration order, whatever order they are given in, so a cluster, a dependency and an emission never depend on it.
Returns
The FactorNode.
Throws
ArgumentError, naming the node, when:
fformis not a declared node;- an interface is missing, unknown, given twice, or a group's members are not
1:n; - the node's declaration is not met: its
matched_groupsmust have as many members as each other, each group at leastmin_group_length, and a node declaredfactorisation = :meanfieldaccepts only clusters of one interface each; - the factorisation names an unknown interface, puts one in two clusters, or leaves one out;
- a node that folds its static inputs has no
nodefn, or every member of its group is static.
Examples
using ReactiveMP, StandardMessagePassingRules
x, y = randomvar(), datavar()
node = factornode(NormalMeanVariance, [(:out, y), (:μ, x), (:v, constvar(1.0))], ((:out,), (:μ,), (:v,)))ReactiveMP.functionalform — Function
functionalform(factornode::FactorNode)The node type factornode was created with: the type or function its @define_factor_node declaration names, such as NormalMeanVariance or +.
ReactiveMP.getinterfaces — Function
getinterfaces(factornode::FactorNode) -> VectorThe interfaces of factornode, ReactiveMP.NodeInterfaces and, for a group's members, ReactiveMP.IndexedNodeInterfaces, in declaration order, a group's members in member order.
ReactiveMP.getinterface — Function
ReactiveMP.getinterface(factornode::FactorNode, index)The interface of factornode at position index of getinterfaces.
ReactiveMP.getlocalclusters — Function
ReactiveMP.getlocalclusters(factornode::FactorNode) -> ReactiveMP.FactorNodeLocalClustersThe clusters of factornode's factorisation and their local marginals, which ReactiveMP.get_node_local_marginals lists.
ReactiveMP.get_node_local_marginals — Function
ReactiveMP.get_node_local_marginals(clusters::FactorNodeLocalClusters)The local marginals of a node's clusters, one ReactiveMP.FactorNodeLocalMarginal per cluster of its factorisation, in the factorisation's order: get_node_local_marginals(getlocalclusters(node)).
ReactiveMP.FactorNodeLocalMarginal — Type
ReactiveMP.FactorNodeLocalMarginal(key)The marginal of one cluster of a factor node's factorisation, and the stream it arrives on once the node is activated. Its key is what a rule reads it by: the interface name for a cluster of one interface, :μ, and the tuple of member names for a joint, (:out, :μ) (see ReactiveMP.clusterkey). A cluster of one interface shares the stream of its variable's marginal; a joint's stream is computed by the node's marginal rule.
Interfaces
Every edge of a factor node, its connection to one variable, is a ReactiveMP.NodeInterface:
getinterfaces(node)3-element Vector{ReactiveMP.NodeInterface}:
Interface(out)
Interface(μ)
Interface(v)Creating an interface allocates a stream for the node's messages among the variable's inbound messages. So the interface's outbound message is the variable's inbound one, and the interface's inbound message is the variable's outbound one. The members of a group are ReactiveMP.IndexedNodeInterfaces, which add the member's index.
interface = ReactiveMP.getinterface(node, 3)
ReactiveMP.name(interface), ReactiveMP.getvariable(interface)(:v, ConstVariable(1.0))ReactiveMP.NodeInterface — Type
ReactiveMP.NodeInterface(name::Symbol, variable)One edge of a factor node: the interface name and the variable it connects to. Creating it calls ReactiveMP.create_new_stream_of_inbound_messages! on the variable, and keeps the stream it returns as the interface's outbound message stream: the node's message towards the variable is the variable's inbound message on this connection. variable is converted to an AbstractVariable when it is not one.
The streams are lazy until the node is activated, which connects the outbound stream to the rule's messages with ReactiveMP.set_stream_of_outbound_messages!.
See also ReactiveMP.IndexedNodeInterface, ReactiveMP.get_stream_of_outbound_messages, ReactiveMP.get_stream_of_inbound_messages.
ReactiveMP.IndexedNodeInterface — Type
ReactiveMP.IndexedNodeInterface(index::Int, interface::NodeInterface)A member of an interface group: a ReactiveMP.NodeInterface with its index in the group, such as member k of the means m and of the precisions p of a NormalMixture, keyed (:m, k). Its name is the group's, and its streams and variable are the wrapped interface's.
See also ReactiveMP.NodeInterface.
ReactiveMP.name — Function
ReactiveMP.name(interface::NodeInterface) -> Symbol
ReactiveMP.name(interface::IndexedNodeInterface) -> Symbol
ReactiveMP.name(localmarginal::FactorNodeLocalMarginal)The name of an interface, :out or a group's name for a member of the group; the key of a cluster's local marginal, a name or a tuple of member names. A Symbol is its own name.
ReactiveMP.getvariable — Function
ReactiveMP.getvariable(interface) -> AbstractVariableThe variable interface connects to.
ReactiveMP.get_stream_of_outbound_messages — Function
ReactiveMP.get_stream_of_outbound_messages(interface)The stream of the messages the node sends along interface, towards its variable: a ReactiveMP.MessageObservable, which is also the variable's inbound stream on this connection.
ReactiveMP.get_stream_of_inbound_messages — Function
ReactiveMP.get_stream_of_inbound_messages(interface)The stream of the messages the variable sends along interface, towards the node: the variable's outbound stream on this connection, which is what the node's rules read as m[:name]. For a constant it is the constant's message; for a data variable, its observations.
ReactiveMP.set_stream_of_outbound_messages! — Function
ReactiveMP.set_stream_of_outbound_messages!(interface, stream)Connect the outbound message stream of interface to stream, the node's messages towards the variable, which activation does.
Activation
Activation connects the node's lazy message and marginal streams into a live network. You activate a node after its variables. For each interface on a random or a data variable, the engine finds the inputs its rule needs, from the dependencies the node's algorithm declares or from the default scheme. It subscribes to them in declaration order, which in variational message passing is the update schedule.
x, y = randomvar(label = :x), randomvar(label = :y)
prior = factornode(Gaussian, [(:out, x), (:μ, constvar(0.0)), (:v, constvar(10.0))])
likelihood = factornode(Gaussian, [(:out, y), (:μ, x), (:v, constvar(1.0))])
foreach(v -> activate!(v, RandomVariableActivationOptions()), (x, y))
foreach(n -> activate!(n, FactorNodeActivationOptions()), (prior, likelihood))
subscription = subscribe!(get_stream_of_marginals(y), (q) -> println("q(y) = ", q))q(y) = Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=11.0))The variables are new ones, since a variable waits for a message from every node connected to it. Here y is latent, and its marginal is the prior predictive, $\mathcal{N}(0, 10 + 1)$. The node runs with its ReactiveMP.FactorNodeActivationOptions: the algorithm, callbacks, annotations, diagnostics, services, a rule fallback and log scales, described on Activation options.
ReactiveMP.activate! — Method
ReactiveMP.activate!(factornode::FactorNode, options::FactorNodeActivationOptions)Wire the message and marginal streams of a factor node, after its variables are activated.
- Clusters: each cluster of the factorisation gets a
ReactiveMP.FactorNodeLocalMarginal. A cluster of one interface shares the variable's own marginal stream; a joint, keyed by a tuple, is computed by the node's marginal rule (ReactiveMP.MarginalMapping). - Initial messages: the option
initial_messagessets its messages on the node's inbound messages, in place of anything set before; then a node that declaresinitial_messageshas them set on each interface where nothing is set yet. - Outbound messages: for every interface connected to a random or a data variable, the inputs its rule needs are combined, and each update gives a
DeferredMessagecomputed by aReactiveMP.MessageMapping, which resolves and runs the rule. An interface on a constant sends no message.
What a rule needs is what the node's algorithm declares (dependencies_spec), or else the engine's default scheme (see ReactiveMP.default_dependencies). Its inputs are subscribed to in the order they are declared, which in variational message passing is the update schedule. A group reaches a rule as one tuple in member order, with nothing for the members it does not depend on.
Throws
ArgumentErrorwhen the algorithm declares a free-energy partition the factorisation is not, or declares no dependencies for a target, or names a cluster the factorisation does not have.
See also ReactiveMP.FactorNodeActivationOptions.
The Internals page describes how the engine chooses and labels a node's inputs.
Static inputs
A deterministic node declared with static_inputs = :fold folds the inputs whose values are known, constants and data, into its node function. Its rules see only the random inputs. You give the function when you create the node, factornode(…; nodefn = f), and a rule reaches it with getnodefn. The Delta node of DeltaMessagePassingRules works this way.
The node below computes out = f(ins...) for an affine f. Its rule pushes a normal message through the folded function:
struct Affine end
@define_factor_node(node = Affine, type = Deterministic, interfaces = [:out, :ins...], static_inputs = :fold)
@define_message_update_rule(
node = Affine, target = :out, args = (m[:ins...]::NormalMeanVariance,), ctx = (:node,),
body = (ctx, args) -> begin
f = getnodefn(ctx.node, MessagePassingRulesBase.Target(:out))
x = only(args.m[:ins])
slope = f(mean(x) + 1) - f(mean(x))
NormalMeanVariance(f(mean(x)), slope^2 * var(x))
end,
)
a, b = randomvar(label = :a), randomvar(label = :b)
source = factornode(Gaussian, [(:out, a), (:μ, constvar(0.0)), (:v, constvar(1.0))])
affine = factornode(Affine, [(:out, b), ((:ins, 1), a), ((:ins, 2), constvar(3.0))]; nodefn = (a, k) -> k * a + 1)FactorNode Affine (deterministic)
out ── b random
ins[1] ── a random
clusters: (out) (ins[1])The constant 3.0 is folded into the function, so the node shows one member of ins, and the rule sees one input. The function it reaches is a -> 3.0 * a + 1, and it sends $\mathcal{N}(3 \cdot 0 + 1, 3^2 \cdot 1)$ towards b:
foreach(v -> activate!(v, RandomVariableActivationOptions()), (a, b))
foreach(n -> activate!(n, FactorNodeActivationOptions()), (source, affine))
affine_subscription = subscribe!(get_stream_of_marginals(b), (q) -> println("q(b) = ", q))q(b) = Marginal(ExponentialFamily.NormalMeanVariance{Float64}(μ=1.0, v=9.0))The Internals page describes ReactiveMP.StaticFold, which holds the folded function.
Node kinds
Each factor node is either deterministic or stochastic. A stochastic node is a density over its interfaces. A deterministic node computes its output as a function of its inputs. The kind decides how the node enters the free energy: a deterministic node has no average energy, and its clusters are always its output and the joint over its inputs.
isstochastic(sdtype(Gaussian)), isdeterministic(sdtype(Affine))(true, true)ReactiveMP.sdtype — Function
sdtype(fform) -> Union{Stochastic, Deterministic}
sdtype(factornode::FactorNode) -> Union{Stochastic, Deterministic}The kind of a node type, or of a factor node, as its @define_factor_node declaration says: Stochastic() for a distribution, Deterministic() for a function of the inputs. A deterministic node's clusters are its output and the joint over its inputs, and its contribution to the free energy has no average energy.
See also isdeterministic, isstochastic.
ReactiveMP.isdeterministic — Function
isdeterministic(kind::Union{Deterministic, Stochastic}) -> Bool
isdeterministic(node) -> BoolWhether a node is Deterministic: given its kind, as sdtype returns it, or the kind's type, or a node, a factor node or what a node is of (NormalMeanVariance, +), asked through its sdtype.
Examples
julia> isdeterministic(Deterministic()), isdeterministic(Stochastic)
(true, false)ReactiveMP.isstochastic — Function
isstochastic(kind::Union{Stochastic, Deterministic}) -> Bool
isstochastic(node) -> BoolWhether a node is Stochastic: given its kind, as sdtype returns it, or the kind's type, or a node, a factor node or what a node is of, asked through its sdtype.