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)
NodeSpec: Mixture
switch (s)inputs…outMixturestochastic
interfacealiases
out
switchs
inputsa group of any number of members
default algorithmDefaultAlgorithm()
static inputsnone
min group length2
definednodes.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 density f(out | inputs), or Deterministic, for out = 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 unless min_group_length says 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 group m, binding k for the inputs to select by. The inputs are written as in a rule's args, without types:

    • m[:μ], q[:μ]: the message or the marginal of a single interface;
    • q[:y, :x], or q[(:y, :x)]: the joint marginal of a cluster, its members in interface order; q[(:in,)] is the joint over the group in;
    • for a group: m[:in...] (every member), m[:in][k] (the target's own member), m[:in][!k] (all but it), or m[:in][select_group_members(f; arity)] (the members f(k) returns, always arity of them, see select_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 plus q(a), and :y => (default,) the default scheme alone. An input listed beside default is 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 write target => (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 => message pairs: 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: []. See initial_messages.

  • static_inputs: how the node treats inputs connected to constants and data. :none, the default, 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. 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. See static_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: []. See matched_groups.

  • min_group_length: the fewest members every group may have, a non-negative integer: min_group_length = 2 for a mixture of at least two components. Needs a group. Default: 1. See min_group_length.

  • factorisation: :meanfield accepts only graphs that give every interface a cluster of its own; :any, the default, accepts every factorisation. Not for a deterministic node. See required_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]),
    ],
)
source
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.

source
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.

source

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. 0 allows 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
NodeSpec

A 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:

It shows itself as a summary at the REPL and as a table in a notebook.

source
MessagePassingRulesBase.InterfaceSpec — Type
InterfaceSpec

One interface of a NodeSpec, as @define_factor_node declares it. Fields:

  • name::Symbol: the interface's name, :μ, or the group's, :inputs for :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,).
source
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,))
source
MessagePassingRulesBase.alias_interface — Function
alias_interface(node, name::Symbol) -> Symbol

The 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.

source
MessagePassingRulesBase.static_inputs — Function
static_inputs(node) -> Symbol

How 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.

source
MessagePassingRulesBase.matched_groups — Function
matched_groups(node) -> Tuple

The 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.

source
MessagePassingRulesBase.min_group_length — Function
min_group_length(node) -> Int

The 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.

source
MessagePassingRulesBase.required_factorisation — Function
required_factorisation(node) -> Symbol

The 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.

source
MessagePassingRulesBase.nodefunction — Function
nodefunction(node) -> Function

The 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)
source

The node's algorithm, its dependencies and its initial messages are on Algorithms and dependencies.