Calling rules

You can call any rule directly, without a graph. This is how rules are tested and explored. A call gives the inputs by name, resolves the rule as an engine would, runs it, and returns a RuleResult. Inspecting rules shows which rule a call would run, without running it.

The examples on this page use a small deterministic node, out = in + c, with a normal type of its own:

using MessagePassingRulesBase

struct Gauss   # a normal, by its mean and variance
    m::Float64
    v::Float64
end

struct Shift end   # out = in + c

@define_factor_node(node = Shift, type = Deterministic, interfaces = [:out, :in, :c])

@define_message_update_rule(
    node = Shift, target = :out, args = (m[:in]::Gauss, m[:c]::Real), logscale = 0,
    body = (args) -> Gauss(args.m[:in].m + args.m[:c], args.m[:in].v),
)

@define_message_update_rule(
    node = Shift, target = :in, args = (m[:out]::Gauss, m[:c]::Real), logscale = 0,
    body = (args) -> Gauss(args.m[:out].m - args.m[:c], args.m[:out].v),
)

result = @call_message_update_rule(node = Shift, target = :out, m = (in = Gauss(1.0, 2.0), c = 3.0))
RuleResult: message of Shift towards :outbelief propagation
incoutShift
Result
valueGauss(4.0, 2.0)
typeGauss
log scale0 (declared)
Inputs
edgevalue
inmGauss(1.0, 2.0)
cm3.0
Rule
declared inputsm[:in]::Gauss, m[:c]::Real
algorithmDefaultAlgorithm()
log scale0
definedcalling.md:27
body
args->Gauss((args.m[:in]).m + args.m[:c], (args.m[:in]).v)

Calling a rule

Each kind of rule has a function and a macro. The function takes the node and the target as positional arguments. The macro takes everything by name.

The inputs are three keywords:

  • m, the messages, a NamedTuple keyed by interface name;
  • q, the marginals, keyed the same way;
  • clusters, the joint marginals.

The other keywords select the algorithm, supply the RuleContext and collect the annotations. A call does not check the context's services (see The rule context). The Keyword reference lists every keyword.

MessagePassingRulesBase.call_message_update_rule — Function
call_message_update_rule(node, target; m, q, clusters, logscale, algorithm, ctx, ann)

Run the message rule of node towards target on the inputs given, as an engine would, and return a RuleResult: getresult is the message, getlogscale its log scale. For exploring a rule at the REPL, in a notebook or in a test; @call_message_update_rule is the same call written with keywords only.

Arguments

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

  • target: the interface the message goes to: :out, or (:m, k) for member k of the group m, as (:in, 2).

Keywords

All optional.

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • logscale: the log scales that arrived with the messages, keyed like m: logscale = (μ = 0.0,). Needed only by a rule declared with reads_logscale = true, for which the call is an error without them. Default: nothing.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

source
MessagePassingRulesBase.call_marginal_update_rule — Function
call_marginal_update_rule(node, target; m, q, clusters, algorithm, ctx, ann)

Run the marginal rule of node for the cluster target on the inputs given, as an engine would, and return a RuleResult: getresult is the joint marginal, and its log scale is nothing. @call_marginal_update_rule is the same call written with keywords only.

Arguments

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

  • target: the cluster whose joint marginal to compute, its members in interface order: (:out, :μ), or (:out, (:T, 1)) with a group member.

Keywords

All optional. A marginal rule typically reads the messages on the cluster's members and the marginals of the node's other interfaces.

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

source
MessagePassingRulesBase.call_average_energy — Function
call_average_energy(node; q, clusters, m, algorithm, ctx, ann)

Compute the average energy of node under the marginals given, as an engine's free energy would, and return a RuleResult: getresult is the energy, a number, and its log scale is nothing. @call_average_energy is the same call written with keywords only.

Arguments

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

Keywords

All optional. An average energy reads marginals, q and clusters, one per cluster of the factorisation.

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

source
MessagePassingRulesBase.@call_message_update_rule — Macro
@call_message_update_rule(
    node = ..., target = ..., m = (...), q = (...), clusters = (...),
    logscale = (...), algorithm = ..., ctx = ..., ann = ...,
)

Run the message rule of a node towards one of its interfaces on the inputs given, as an engine would, and return a RuleResult: getresult is the message, getlogscale its log scale. For exploring a rule at the REPL, in a notebook or in a test. The macro takes keyword arguments only; node and target are required, the rest optional. It is call_message_update_rule with node and target given by name.

Required keywords

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

  • target: the interface the message goes to: :out, or (:m, k) for member k of the group m, as (:in, 2).

Optional keywords

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • logscale: the log scales that arrived with the messages, keyed like m: logscale = (μ = 0.0,). Needed only by a rule declared with reads_logscale = true, for which the call is an error without them. Default: nothing.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

The result

A RuleResult: getresult the message; getlogscale its log scale, a number, or an UndefinedLogScale when the rule declares none; getrule the RuleSpec that ran; getannotations what it annotated. In the terminal it shows a report of the call, one line per edge of the node, and in a notebook a card with the node drawn.

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

Example

julia> struct Shift end

julia> @define_factor_node(node = Shift, type = Deterministic, interfaces = [:out, :in])

julia> @define_message_update_rule(
           node = Shift,
           target = :out,
           args = (m[:in]::Real,),
           logscale = 0,
           body = (args) -> args.m[:in] + 1,
       )

julia> result = @call_message_update_rule(node = Shift, target = :out, m = (in = 1.0,));

julia> getresult(result), getlogscale(result)
(2.0, 0)
source
MessagePassingRulesBase.@call_marginal_update_rule — Macro
@call_marginal_update_rule(
    node = ..., target = (:y, :x), m = (...), q = (...), clusters = (...),
    algorithm = ..., ctx = ..., ann = ...,
)

Run the marginal rule of a node for one of its clusters on the inputs given, as an engine would, and return a RuleResult: getresult is the joint marginal. The macro takes keyword arguments only; node and target are required, the rest optional. It is call_marginal_update_rule with node and target given by name. A marginal has no log scale, so logscale is not accepted.

Required keywords

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

  • target: the cluster whose joint marginal to compute, its members in interface order: (:out, :μ), or (:out, (:T, 1)) with a group member.

Optional keywords

A marginal rule typically reads the messages on the cluster's members and the marginals of the node's other interfaces.

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

The result

A RuleResult: getresult the joint marginal; getlogscale is nothing; getrule the RuleSpec that ran; getannotations what it annotated.

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

Example

result = @call_marginal_update_rule(
    node   = NormalMeanVariance,
    target = (:out, :μ),
    m      = (out = NormalMeanVariance(1.0, 1.0), μ = NormalMeanVariance(0.0, 2.0)),
    q      = (v = PointMass(1.0),),
)
getresult(result)
source
MessagePassingRulesBase.@call_average_energy — Macro
@call_average_energy(node = ..., q = (...), clusters = (...), m = (...), algorithm = ..., ctx = ..., ann = ...)

Compute the average energy of a node under the marginals given, as an engine's free energy would, and return a RuleResult: getresult is the energy, a number. The macro takes keyword arguments only; node is required, the rest optional. It is call_average_energy with node given by name. An average energy has no target and no log scale, so target and logscale are not accepted.

Required keywords

  • node: the node, as declared with @define_factor_node: a type, NormalMeanVariance, or a function, +. A function node is the function itself: its type, typeof(+), is an ArgumentError saying so.

Optional keywords

An average energy reads marginals, q and clusters, one per cluster of the factorisation.

  • m: the inbound messages, a NamedTuple keyed by the interfaces' declared names: m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(1.0)). A group is a tuple of its members in order, m = (in = (m₁, m₂),); for a rule that selects some members, nothing may stand for the others, as an engine gives them. Default: none.

  • q: the marginals of single interfaces, keyed the same way: q = (v = Gamma(1.0, 1.0),). Default: none.

  • clusters: the joint marginals of structural clusters, a tuple of pairs from the cluster's members, in interface order, to its joint: clusters = ((:y, :x) => q_yx,). (:in,) => q is the joint over the group in. Default: none.

  • algorithm: the algorithm value to run under, with its parameters: algorithm = ARVMP(Multivariate, 2, ARsafe()). Rule lookup selects the rules of its type, and for a DefaultAlgorithmExtension the default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's, default_algorithm(node).

  • ctx: the RuleContext of services the rule reads as ctx.name: ctx = MessagePassingRulesBase.RuleContext(rng = Xoshiro(1)). Default: an empty context. Its services are not checked: one the rule declares and ctx does not supply reads as nothing inside the rule, so an unset matrix_correction gives the rule's own default. An engine checks them when it resolves a rule; call MessagePassingRulesBase.check_services(getrule(result), ctx) for the same guarantee.

  • ann: where the annotations go. Default: nothing, and what the rule annotates is dropped. A MessagePassingRulesBase.AnnotationStore() collects what the rule writes with annotate!, returned by getannotations(result). A MessagePassingRulesBase.RuleAnnotations(m = …, q = …, out = AnnotationStore()) also gives the annotations that arrived with the inputs, which the rule reads as ann.m[:μ].

The result

A RuleResult: getresult the energy; getlogscale is nothing; getrule the RuleSpec that ran.

When no rule fits, the call throws a RuleNotFoundError, which lists the closest rules and, input by input, why each does not fit. An unknown keyword is an error. Test tooling counts a rule called this way as tested, for its rule-coverage gate.

Example

result = @call_average_energy(
    node = NormalMeanVariance,
    q    = (out = NormalMeanVariance(1.0, 1.0), μ = NormalMeanVariance(0.0, 2.0), v = PointMass(1.0)),
)
getresult(result)
source

Reading the result

A RuleResult holds the result together with everything that produced it. On these pages, and in a notebook, it shows itself as a card with the node drawn, as the result above does:

  • the inputs are arrows in, solid for messages and dashed for marginals;
  • the target is the arrow out;
  • below the drawing are the result, its log scale, the rule that ran and the other rules for the same target.

In the terminal, it shows itself as a report with one line per edge of the node, giving what the edge carried into the rule. The accessors below read its parts:

getresult(result), getlogscale(result)
(Main.Gauss(4.0, 2.0), 0)
MessagePassingRulesBase.RuleResult — Type
RuleResult

What a rule called by hand returns: the call_* functions (call_message_update_rule, call_marginal_update_rule, call_average_energy), their @call_* macros, and the message_passing_* functions (message_passing_rule and its siblings). It holds the rule's result together with everything that produced it. Read it with its getters:

It shows itself as a report in the terminal (text/plain), one line per edge of the node, and as a card with the node drawn in a notebook or in documentation (text/html): the inputs, the result and its log scale, the rule that ran and the other rules for the same target. The two-argument show is compact, RuleResult(2.0, logscale = 0). An engine runs rules through execute_rule without building one.

source
MessagePassingRulesBase.getresult — Function
getresult(r::RuleResult)

The rule's result: the message, the joint marginal or the average energy. For an in-place rule, the buffer it wrote into.

source

A marginal rule whose cluster factorises returns a FactorizedCluster of its blocks. Each block is labelled with the members it covers, and an engine hands each block to its members.

MessagePassingRulesBase.FactorizedCluster — Type
FactorizedCluster(block => distribution, ...)

The result of a marginal rule whose cluster factorises into independent blocks: FactorizedCluster((:out, :μ) => q_outμ, (:v,) => q_v) for q(out, μ, v) = q(out, μ) q(v).

It is a BayesBase FactorizedJoint of the blocks, labelled with the members each block covers. The joint is the distribution, and BayesBase supplies its entropy and float-type conversion. The labels are what an engine needs to hand each block on and to score it. Each label is the tuple of the block's members, in the cluster's order; a member of a group is written as in a cluster's key, (:T, 1). The labels are carried in the type, so a block is read as fc[(:out, :μ)] with no lookup at run time, and no names are joined together, so interface names may contain underscores. pairs(fc) gives block => distribution pairs, and BayesBase.components(fc) the distributions.

Throws

  • ArgumentError when given no block;
  • KeyError when indexed by a block it does not have.

Examples

julia> fc = FactorizedCluster((:out, :μ) => 1.0, (:v,) => 2.0);

julia> fc[(:v,)]
2.0

julia> MessagePassingRulesBase.cluster_blocks(fc)
((:out, :μ), (:v,))

See also cluster_blocks, check_factorized_cluster.

source
MessagePassingRulesBase.check_factorized_cluster — Function
check_factorized_cluster(target::ClusterTarget, fc::FactorizedCluster) -> FactorizedCluster

Check that the blocks of fc partition the members of target, every member in exactly one block and each block listing its members in the cluster's order, and return fc. For a marginal rule's tests; the blocks may come in any order.

Throws

ArgumentError naming the problem: a member outside the cluster, a member in two blocks, a block out of order, or a member no block covers.

source

When no rule fits

Every call throws a RuleNotFoundError when no rule fits. Its message diagnoses the call. It lists every rule for the node and target and says, slot by slot, why each one does not fit. Here c is given as a String:

try
    @call_message_update_rule(node = Shift, target = :out, m = (in = Gauss(1.0, 2.0), c = "3"))
catch err
    showerror(stdout, err)
end
RuleNotFoundError: no message rule for Shift towards :out under DefaultAlgorithm() takes the inputs (m[:c]::String, m[:in]::Gauss)
  a rule of this shape exists, but the input types do not fit (type mismatch)
  what to try: the inputs arrive with other types than the rule takes. If a rule below takes such an input as a marginal `q`, a factorisation that separates it delivers one (in RxInfer, `@constraints`); a form constraint on a variable projects its marginal onto a family a rule takes; otherwise, define a rule for these types
  near misses:
    rule at calling.md:27
      ✓ algorithm DefaultAlgorithm
      ✓ m[:in]::Gauss  got Gauss
      ✗ m[:c]::Real  got String
MessagePassingRulesBase.RuleNotFoundError — Type
RuleNotFoundError(notfound::RuleNotFound)

The error every call in this package throws when no rule fits: the call_* functions and macros, the which_* queries and the message_passing_* functions. It wraps the RuleNotFound resolution returned.

Its message reads, from the top:

  • the call: the kind of rule, the node, the target, the algorithm, and the inputs given, each as m[:name]::Type or q[:name]::Type;
  • a diagnosis: no rule exists for this node and target under any algorithm; a rule of this shape exists, but the input types do not fit (a rule takes exactly these inputs, and some type differs); rules of this shape exist under another algorithm; or no rule consumes this set of inputs (the rules take other inputs);
  • what to try for that diagnosis: loading the package that defines the rules, a form constraint, the node's algorithm, or another factorisation;
  • the near misses, every rule for the node and target, by file and line, each with a line per slot: ✓ or ✗ for the algorithm, then for each input the rule takes, what was given for it or not provided, and each input given that the rule does not take, provided but not consumed.

For a node Shift with interfaces out and in and a single rule, towards out:

julia> @call_message_update_rule(node = Shift, target = :in, m = (out = 1.0,))
ERROR: RuleNotFoundError: no message rule for Shift towards :in under DefaultAlgorithm() takes the inputs (m[:out]::Float64)
  no rule exists for this node and target under any algorithm
  what to try: no loaded package defines this rule: load the package that defines the node's rules, or define the rule with `@define_message_update_rule`
source
MessagePassingRulesBase.RuleNotFound — Type
RuleNotFound

What resolution (find_message_rule, find_marginal_rule, find_average_energy) returns when no rule matches; resolution never throws. Fields: kind (:message, :marginal or :average_energy), node, target (nothing for an average energy), algorithm, the value the call asked for, and args, the RuleArgs that found nothing.

A caller that needs a rule reports it by throwing a RuleNotFoundError, as every call in this package does; an engine may first consult a rule fallback.

source
MessagePassingRulesBase.rule_not_found_hint — Function
rule_not_found_hint(node, notfound::RuleNotFound) -> Union{String, Nothing}

A sentence a node's package adds to the report of a RuleNotFoundError for node, after the generic hint, or nothing, the default. A package extends it for its node where it knows the usual cause, as the Delta node's does for a missing approximation method:

MessagePassingRulesBase.rule_not_found_hint(::Type{<:MyNode}, notfound) =
    notfound.algorithm isa MyAlgorithm ? nothing : "MyNode runs under `MyAlgorithm(...)`"
source

A rule that was found can still refuse its inputs, when they fail the check its definition declares with args_check (Checking inputs). The call then throws a RuleInputError, which names the rule and says why.

MessagePassingRulesBase.RuleInputError — Type
RuleInputError <: Exception

A rule refused its inputs: their types matched its args, so it was selected, but they failed the check its definition declares with the args_check keyword. Its message names the rule, its node, target and algorithm, where it is defined, the inputs it was given, and why: the string the check returned, or, where it returned false, the check's own source.

A failed check is an error, not a reason to select another rule: resolution has already chosen this one. A rule whose inputs a check refuses would compute something wrong or fail inside its body; the error says so where it happens.

Fields: rule, the RuleSpec; reason, the string the check returned, or nothing where it returned false; args, the RuleArgs it was given.

source

Resolving without the interactive layer

Code that builds its own RuleArgs resolves a rule with the find_* functions. They never throw: they return a RuleNotFound when nothing fits. The code then runs the rule with the message_passing_* functions, which do throw. Julia's dispatch does the resolution, over every loaded package.

MessagePassingRulesBase.find_message_rule — Function
find_message_rule(node, target, algorithm, args) -> Union{RuleSpec, RuleNotFound}

Resolve the message rule of node towards target under algorithm for the inputs args, without running it.

Every @define_message_update_rule adds a method, so resolution is Julia's dispatch over every loaded package; it never throws.

Arguments

  • node: the node, a type or a function, as declared;
  • target: a Target or an IndexedTarget;
  • algorithm: the algorithm value, whose type selects the rules; for a DefaultAlgorithmExtension without a rule of its own, the default's rule is returned;
  • args: the inputs, a RuleArgs; their keys and types select the rule.

Returns

The RuleSpec, or a RuleNotFound that names the algorithm the call asked for. Run the spec with rule_algorithm(spec, algorithm), which differs from algorithm for an inherited rule.

See also find_marginal_rule, find_average_energy, which_message_update_rule.

source
MessagePassingRulesBase.find_marginal_rule — Function
find_marginal_rule(node, cluster::ClusterTarget, algorithm, args) -> Union{RuleSpec, RuleNotFound}

Resolve the marginal rule of node for the structural cluster under algorithm for the inputs args, a RuleArgs, without running it. Every @define_marginal_update_rule adds a method; it never throws. For a DefaultAlgorithmExtension without a rule of its own, the default's rule is returned.

Returns

The RuleSpec, or a RuleNotFound that names the algorithm the call asked for.

See also find_message_rule, which_marginal_update_rule.

source
MessagePassingRulesBase.find_average_energy — Function
find_average_energy(node, algorithm, args) -> Union{RuleSpec, RuleNotFound}

Resolve the average energy of node under algorithm for the marginals in args, a RuleArgs, without computing it. Every @define_average_energy adds a method; it never throws. For a DefaultAlgorithmExtension without an energy of its own, the default's is returned.

Returns

The RuleSpec, or a RuleNotFound whose target is nothing.

See also find_message_rule, which_average_energy.

source
MessagePassingRulesBase.message_passing_rule — Function
message_passing_rule(node, target, algorithm, args, ctx = RuleContext(), ann = NoAnnotations()) -> RuleResult

Resolve the message rule of node towards target and run it, allocating its result: the positional, non-interactive form of call_message_update_rule, for code that builds its RuleArgs itself. An engine resolves once and calls execute_rule instead.

Arguments

Returns

A RuleResult: getresult is the message, getlogscale its log scale.

Throws

Examples

julia> using MessagePassingRulesBase: Target, RuleArgs

julia> struct Shift end

julia> @define_factor_node(node = Shift, type = Deterministic, interfaces = [:out, :in])

julia> @define_message_update_rule(node = Shift, target = :out, args = (m[:in]::Real,), body = (args) -> args.m[:in] + 1)

julia> getresult(message_passing_rule(Shift, Target(:out), DefaultAlgorithm(), RuleArgs(m = (in = 1.0,))))
2.0
source
MessagePassingRulesBase.message_passing_rule! — Function
message_passing_rule!(output, node, target, algorithm, args, ctx = RuleContext(), ann = NoAnnotations()) -> RuleResult

Resolve the in-place message rule of node towards target and run it into the buffer output, which getresult then returns.

Arguments

  • output: the buffer the rule writes into, shaped as its preallocate builds it.
  • node: the node, as declared with @define_factor_node.
  • target: a Target or an IndexedTarget.
  • algorithm: the algorithm value to run under. For a DefaultAlgorithmExtension without a rule of its own, the default's rule runs, with DefaultAlgorithm() (rule_algorithm).
  • args: the inputs, a RuleArgs; their keys and types select the rule.
  • ctx: the RuleContext the rule runs with. Default: an empty context. Its services are not checked: a service the rule declares and ctx does not supply reads as nothing inside the rule. Call check_services(getrule(result), ctx), or check the spec from find_message_rule first, for an engine's guarantee.
  • ann: the rule's ann, a RuleAnnotations; or, for a rule that only writes annotations, an AnnotationStore or a NoAnnotations. Default: NoAnnotations(), dropping what the rule annotates.

Returns

A RuleResult: getresult is the message, getlogscale its log scale.

Throws

  • ArgumentError when the rule has no in-place form;
  • RuleNotFoundError when no rule fits;
  • ArgumentError when the rule reads log scales and args carries none (check_reads_logscale);
  • whatever the rule throws, unchanged.

See also message_passing_rule.

source
MessagePassingRulesBase.message_passing_marginalrule — Function
message_passing_marginalrule(node, cluster, algorithm, args, ctx = RuleContext(), ann = NoAnnotations()) -> RuleResult

Resolve the marginal rule of node for cluster and run it, allocating its result: the positional form of call_marginal_update_rule.

Arguments

Returns

A RuleResult: getresult is the joint marginal; its log scale is nothing.

Throws

See also message_passing_marginalrule!.

source
MessagePassingRulesBase.message_passing_marginalrule! — Function
message_passing_marginalrule!(output, node, cluster, algorithm, args, ctx = RuleContext(), ann = NoAnnotations()) -> RuleResult

Resolve the in-place marginal rule of node for cluster and run it into the buffer output, which getresult then returns.

Arguments

  • output: the buffer the rule writes into, shaped as its preallocate builds it.
  • node: the node, as declared with @define_factor_node.
  • cluster: a ClusterTarget.
  • algorithm: the algorithm value to run under. For a DefaultAlgorithmExtension without a rule of its own, the default's rule runs, with DefaultAlgorithm() (rule_algorithm).
  • args: the inputs, a RuleArgs; their keys and types select the rule.
  • ctx: the RuleContext the rule runs with. Default: an empty context. Its services are not checked: a service the rule declares and ctx does not supply reads as nothing inside the rule. Call check_services(getrule(result), ctx), or check the spec from find_message_rule first, for an engine's guarantee.
  • ann: the rule's ann, a RuleAnnotations; or, for a rule that only writes annotations, an AnnotationStore or a NoAnnotations. Default: NoAnnotations(), dropping what the rule annotates.

Returns

A RuleResult: getresult is the joint marginal; its log scale is nothing.

Throws

  • ArgumentError when the rule has no in-place form;
  • RuleNotFoundError when no rule fits;
  • ArgumentError when the rule reads log scales and args carries none (check_reads_logscale);
  • whatever the rule throws, unchanged.

See also message_passing_marginalrule.

source
MessagePassingRulesBase.message_passing_average_energy — Function
message_passing_average_energy(node, algorithm, args, ctx = RuleContext(), ann = NoAnnotations()) -> RuleResult

Resolve the average energy of node and compute it: the positional form of call_average_energy.

Arguments

  • node: the node, as declared with @define_factor_node.
  • algorithm: the algorithm value to run under. For a DefaultAlgorithmExtension without a rule of its own, the default's rule runs, with DefaultAlgorithm() (rule_algorithm).
  • args: the inputs, a RuleArgs; their keys and types select the rule.
  • ctx: the RuleContext the rule runs with. Default: an empty context. Its services are not checked: a service the rule declares and ctx does not supply reads as nothing inside the rule. Call check_services(getrule(result), ctx), or check the spec from find_message_rule first, for an engine's guarantee.
  • ann: the rule's ann, a RuleAnnotations; or, for a rule that only writes annotations, an AnnotationStore or a NoAnnotations. Default: NoAnnotations(), dropping what the rule annotates.

Returns

A RuleResult: getresult is the energy, a number; its log scale and its target are nothing.

Throws

source

For tools

A tool such as the test tooling calls rules the way the interactive functions do. It reads its inputs with the same functions, and it can observe which rule each call selects.

MessagePassingRulesBase.interactive_args — Function
interactive_args(m, q, clusters, logscale = nothing) -> RuleArgs

The RuleArgs an interactive call builds from its keywords: the messages m and marginals q as named tuples, the joint marginals clusters as a collection of members => marginal pairs, and the incoming log scales logscale, or nothing. For tools that take a rule's inputs the way call_message_update_rule does.

source
MessagePassingRulesBase.add_selection_observer! — Function
add_selection_observer!(f) -> nothing

Call f(spec) with the RuleSpec of every rule an interactive call selects, a call_* or @call_* call, before the rule runs; a message_passing_* call is not observed. Test tooling registers one to count a rule a test calls by hand as tested. An engine resolves its rules itself, so its calls are not observed. Registering the same f twice registers it once.

source