Defining nodes
You declare a factor node once, with @define_factor_node, before any of its rules. The declaration names the node and says whether it is stochastic or deterministic. It lists the node's interfaces, its named edges. It may also give the node's own algorithm, its dependencies, its initial messages and what the node requires of a graph.
using MessagePassingRulesBase
struct Mixture end
@define_factor_node(
node = Mixture,
type = Stochastic,
interfaces = [:out, (:switch, aliases = [:s]), :inputs...],
min_group_length = 2,
)
MessagePassingRulesBase.nodespec(Mixture)| interface | aliases | |
|---|---|---|
out | ||
switch | s | |
inputs | a group of any number of members |
| default algorithm | DefaultAlgorithm() |
|---|---|
| static inputs | none |
| min group length | 2 |
| defined | nodes.md:19 |
The node itself is a type, as Mixture here or NormalMeanVariance in a rule package, or a function, as +. The same value names the node in every rule and in a graph. The declaration draws itself: this node has an output, an interface switch with the alias s, and a group of inputs with at least two members. Your first node declares a node and writes its rules step by step. The Keyword reference lists every keyword of the macro.
Interfaces
You list the interfaces in order, the output first by convention. An interface may have aliases, other names a graph may use for it: (:μ, aliases = [:mean]).
A trailing ... declares a group. A group has any number of members, (:inputs, 1), (:inputs, 2) and so on, and a graph gives them as a whole. A mixture's components and a sum's summands are groups. A rule targets any member with target = (:inputs, k), and it reads a group as a tuple in member order. A deterministic node with a group writes the rules of such a node.
Names may contain underscores, since nothing in the package joins names together.
Kinds
A Stochastic node has a density over its interfaces, f(out | inputs). Its clusters follow the graph's factorisation, and it has an average energy. When a stochastic node has no groups, the macro also defines its log-density as nodefunction. Rule verification and the NodeFunctionRuleFallback use it.
A Deterministic node computes its output from its inputs, out = f(inputs). Its clusters are always its output and the joint over its inputs, whatever the factorisation. A rule reaches the function through getnodefn(ctx.node, Target(:out)). The engine implements that call, because the engine's node owns the function and any static inputs folded into it.
MessagePassingRulesBase.@define_factor_node — Macro
@define_factor_node(
node = ..., type = Stochastic | Deterministic, interfaces = [...],
algorithm = ..., dependencies = [...], initial_messages = [...], static_inputs = ...,
matched_groups = [...], min_group_length = ..., factorisation = ...,
)Declare a factor node: its interfaces, its kind, and the algorithm its rules run under. Rules are then defined for it with @define_message_update_rule, @define_marginal_update_rule and @define_average_energy, and an engine builds the node in a graph from this declaration (nodespec). The macro takes keyword arguments only; node, type and interfaces are required, the rest optional. An unknown or repeated keyword is an error at definition time, naming the valid ones.
For a stochastic node without groups, the macro also defines nodefunction(node), the log-density (; out, μ, v) -> logpdf(node(μ, v), out), which the rule verification and the rule fallbacks use; node must then be callable as a distribution of the other interfaces.
Required keywords
node: what the node is, a type,NormalMeanVariance, or a function,+. The same value names it in every rule and in a graph.type:Stochastic, for a node with a densityf(out | inputs), orDeterministic, forout = f(inputs). A deterministic node's clusters are always its output and the joint over its inputs, whatever the graph's factorisation.interfaces: the node's interfaces, a vector, the output first by convention::μ, a single interface. Names may contain underscores;:inputs..., a group of any number of members,(:inputs, 1),(:inputs, 2), …, which a graph gives as a whole. A group may be empty unlessmin_group_lengthsays otherwise;(:μ, aliases = [:mean]), an interface with other names a graph may use for it.
Optional keywords
algorithm: the algorithm the node's rules run under unless a call or a graph asks for another, a type, instantiated with no arguments, or a value. Default:DefaultAlgorithm(), as it should be for almost every node: under it, whether a rule is belief propagation, variational message passing or their structured form follows from the factorisation. A node declares its own only when its rules ignore the factorisation, as a mixture's do.dependencies: what each rule consumes under that algorithm, in place of the default scheme.A vector of
target => (inputs...)pairs, one per target whose inputs are declared:dependencies = [:out => (m[:μ], q[:v]), :μ => (m[:out], q[:v])]. A target is:out, or(:m, k)for every member of the groupm, bindingkfor the inputs to select by. The inputs are written as in a rule'sargs, without types:m[:μ],q[:μ]: the message or the marginal of a single interface;q[:y, :x], orq[(:y, :x)]: the joint marginal of a cluster, its members in interface order;q[(:in,)]is the joint over the groupin;- for a group:
m[:in...](every member),m[:in][k](the target's own member),m[:in][!k](all but it), orm[:in][select_group_members(f; arity)](the membersf(k)returns, alwaysarityof them, seeselect_group_members); default: the default scheme's inputs for that target, which follow the factorisation (the messages of the target's own cluster, the marginals of the others).:a => (default, q[:a])is the default scheme's inputs plusq(a), and:y => (default,)the default scheme alone. An input listed besidedefaultis a single interface's message or marginal, placed among the default scheme's in interface order, and consumed without being scored.
A target with no inputs is
target => (). Every target a graph connects must be declared: one left out is an error when an engine activates the node, so writetarget => (default,)for one that follows the default scheme. The inputs are subscribed to in the order written, which under variational message passing is the update schedule: it changes how fast a node converges, not where to. The declaration is checked against the node's interfaces when it is loaded: unknown names, a group written as a single interface or the other way round, a target or an input given twice, and a cluster out of interface order are errors.Default: none, and every target follows the default scheme. Declarations for the node's other algorithms, and a
free_energy_partition, go in@define_dependencies.initial_messages: messages an engine seeds on the node's inbound interfaces before inference, where the graph sets none, a vector of:name => messagepairs:initial_messages = [:in => NormalMeanPrecision(0.0, 100.0)]. For a rule that depends on its own edge, which would otherwise wait forever. One per single interface; a group has none. Default:[]. Seeinitial_messages.static_inputs: how the node treats inputs connected to constants and data.:none, the default, treats them like any other input;:foldfolds them into the node function, reached asgetnodefn(ctx.node, target), and every update waits until they are available. An engine builds such a node with its function,factornode(…; nodefn = f); ReactiveMP folds the members of a group, so it requires such a node to have exactly one group. Seestatic_inputs.matched_groups: groups that must have as many members as each other, a vector of tuples of group names:matched_groups = [(:m, :p)]for a mixture whose means and precisions come in pairs. Default:[]. Seematched_groups.min_group_length: the fewest members every group may have, a non-negative integer:min_group_length = 2for a mixture of at least two components. Needs a group. Default:1. Seemin_group_length.factorisation::meanfieldaccepts only graphs that give every interface a cluster of its own;:any, the default, accepts every factorisation. Not for a deterministic node. Seerequired_factorisation.
An engine checks matched_groups, min_group_length and factorisation when it creates the node, so a malformed graph is an error there rather than a rule silently reading fewer components.
Examples
@define_factor_node(node = NormalMeanVariance, type = Stochastic, interfaces = [:out, (:μ, aliases = [:mean]), (:v, aliases = [:var])])
@define_factor_node(node = +, type = Deterministic, interfaces = [:out, :in1, :in2])
# A node whose rules ignore the factorisation, and so declares an algorithm of its own and
# what each rule consumes; the precisions come before the means, the update schedule:
@define_factor_node(
node = NormalMixture,
type = Stochastic,
interfaces = [:out, :switch, :m..., :p...],
algorithm = NormalMixtureVMP,
matched_groups = [(:m, :p)],
min_group_length = 2,
factorisation = :meanfield,
dependencies = [
:out => (q[:switch], q[:p...], q[:m...]),
:switch => (q[:out], q[:p...], q[:m...]),
(:m, k) => (q[:out], q[:switch], q[:p][k]),
(:p, k) => (q[:out], q[:switch], q[:m][k]),
],
)MessagePassingRulesBase.Stochastic — Type
Stochastic()The kind of a node with a density over its interfaces, f(out | inputs), written type = Stochastic in @define_factor_node. Such a node has an average energy, and, when it has no groups, a nodefunction. Its clusters follow the graph's factorisation. sdtype(node) returns Stochastic() for it.
See also Deterministic.
MessagePassingRulesBase.Deterministic — Type
Deterministic()The kind of a node whose output is a function of its inputs, out = f(inputs), written type = Deterministic in @define_factor_node. Its clusters are always its output and the joint over its inputs, whatever the graph's factorisation, so it cannot require factorisation = :meanfield. A rule reaches its function through getnodefn. sdtype(node) returns Deterministic() for it.
See also Stochastic.
MessagePassingRulesBase.getnodefn — Function
getnodefn(node, target)The function a deterministic node computes, as a rule body needs it: for Target(:out) the forward function of the free inputs, with any static inputs already folded in. A rule reaches it as getnodefn(ctx.node, Target(:out)) after declaring ctx = (:node,).
The base package declares it and defines no methods: node is the engine's own node object, which owns the function and its static inputs, so the engine implements it. A known inverse is not the node's but its algorithm's, which a rule reads from algo.
What a node requires of a graph
A node may state what it requires of the graph it is placed in. An engine checks these requirements when it creates the node. A malformed graph is then an error at creation, rather than a rule that silently reads fewer components.
matched_groups = [(:m, :p)]: the named groups have as many members as each other.min_group_length = 2: every group has at least two members.0allows an empty group.factorisation = :meanfield: the graph gives every interface a cluster of its own. A node whose rules are variational whatever the factorisation declares it.
static_inputs = :fold asks the engine to fold the inputs that are connected to constants and data into the node's function, which a rule reads through getnodefn. An engine builds such a node with its function.
The declaration as data
The macro produces a NodeSpec, which nodespec returns. The card at the top of this page is that NodeSpec. Every query below reads it:
julia> MessagePassingRulesBase.interfaces(Mixture)(:out, :switch, :inputs)julia> MessagePassingRulesBase.interface_groups(Mixture)(:inputs,)julia> MessagePassingRulesBase.alias_interface(Mixture, :s):switch
MessagePassingRulesBase.NodeSpec — Type
NodeSpecA node as data, produced by @define_factor_node and returned by nodespec. An engine builds the node in a graph from it. The queries below read its fields; each keyword of the macro is one field:
node: the node itself, a type or a function;type:Stochastic()orDeterministic(), read bysdtype;interfaces: theInterfaceSpecs in declaration order, read byinterfaces,interface_groupsandalias_interface;algorithm: the algorithm value rules run under by default, read bydefault_algorithm;static_inputs::noneor:fold, read bystatic_inputs;matched_groups,min_group_length,factorisation: what the node requires of a graph, read bymatched_groups,min_group_lengthandrequired_factorisation;initial_messages:name => messagepairs, read byinitial_messages;file,line: where it was declared.
It shows itself as a summary at the REPL and as a table in a notebook.
MessagePassingRulesBase.InterfaceSpec — Type
InterfaceSpecOne interface of a NodeSpec, as @define_factor_node declares it. Fields:
name::Symbol: the interface's name,:μ, or the group's,:inputsfor:inputs...;group::Bool: whether it is a group of any number of members;aliases::Tuple{Vararg{Symbol}}: the other names a graph may use for it,(:mean,).
MessagePassingRulesBase.nodespec — Function
nodespec(node) -> NodeSpecThe NodeSpec @define_factor_node declared for node, a type or a function. Every other node query reads it.
Throws
MethodError for a node that was never declared.
MessagePassingRulesBase.interfaces — Function
interfaces(node) -> Tuple{Vararg{Symbol}}The interface names of node, in declaration order; a group appears once, by its name. Throws a MethodError for an undeclared node, as every node query does.
julia> struct Mix end
julia> @define_factor_node(node = Mix, type = Stochastic, interfaces = [:out, :switch, :m...])
julia> MessagePassingRulesBase.interfaces(Mix), MessagePassingRulesBase.interface_groups(Mix)
((:out, :switch, :m), (:m,))MessagePassingRulesBase.interface_groups — Function
interface_groups(node) -> Tuple{Vararg{Symbol}}The names of the groups of node, the interfaces declared as :m..., in declaration order; empty for a node without groups.
MessagePassingRulesBase.alias_interface — Function
alias_interface(node, name::Symbol) -> SymbolThe declared interface name refers to: name itself for an interface, or the interface it is an alias of. An engine calls it on the names a graph uses.
Throws
ArgumentError naming the node's interfaces when name is neither an interface nor an alias.
MessagePassingRulesBase.sdtype — Function
sdtype(node) -> Union{Stochastic, Deterministic}The kind of node: Stochastic() or Deterministic(), as declared.
MessagePassingRulesBase.static_inputs — Function
static_inputs(node) -> SymbolHow the node treats inputs connected to constants and data. :none treats them like any other input. :fold folds them into the node function, reached as getnodefn(ctx.node, target), and every update waits until they are available. Which inputs are static is known only from the graph, so the engine does the folding and the waiting.
MessagePassingRulesBase.matched_groups — Function
matched_groups(node) -> TupleThe sets of groups that must have as many members as each other, as tuples of group names: ((:m, :p),) for a mixture whose means and precisions come in pairs. Empty when the node declares none.
MessagePassingRulesBase.min_group_length — Function
min_group_length(node) -> IntThe fewest members any group of node may have: 1 unless declared, 2 for a mixture, 0 for a node whose group may be empty, as DiscreteTransition's T.
MessagePassingRulesBase.required_factorisation — Function
required_factorisation(node) -> SymbolThe factorisations node accepts. :any accepts every one; :meanfield only clusters of one interface each, for a node whose rules are variational whatever the factorisation, such as a mixture. The engine checks it when it creates the node.
MessagePassingRulesBase.nodefunction — Function
nodefunction(node) -> FunctionThe log-density of a stochastic node without groups, as a function of keyword arguments named after its interfaces: (; out, μ, v) -> logpdf(node(μ, v), out). @define_factor_node defines it for such a node, which must be callable as a distribution of its other interfaces. The rule fallback NodeFunctionRuleFallback and rule verification use it.
Throws
MethodError for a deterministic node, a node with groups, or an undeclared one.
f = MessagePassingRulesBase.nodefunction(NormalMeanVariance)
f(out = 1.0, μ = 0.0, v = 2.0) # logpdf(NormalMeanVariance(0.0, 2.0), 1.0)The node's algorithm, its dependencies and its initial messages are on Algorithms and dependencies.