MessagePassingRulesTestUtils

A rule package defines factor nodes and their rules with MessagePassingRulesBase. This package tests those rules on their own, the way they are defined, without building a graph. It is a test dependency only: nothing needs it at run time.

This site is for rule authors, who test the rules of a package of their own. It assumes you have written a rule, as MessagePassingRulesBase's Your first node does.

MessagePassingRulesTestUtils — Module
MessagePassingRulesTestUtils

Tools for testing message passing rules on their own, without a graph, the way a rule package defines them with @define_message_update_rule and its siblings. Every check is a Test assertion, reported against the line of the test that made it, so the tools go inside ordinary @testsets or @testitems.

A rule package's suite uses them in four layers:

  1. Table tests, @test_message_update_rule, @test_marginal_update_rule and @test_average_energy: each case gives a rule's inputs and the result it must return. Every case is also run with its inputs converted to other float types, through rule! for an in-place rule and on a poisoned scratch for a rule with scratch.
  2. Verification against the node, @verify_message_update_rule: a message rule's output, and its log scale, against the node's own log-density, integrated numerically.
  3. Derivative checks, @test_rule_derivatives: automatic differentiation through a rule against finite differences.
  4. The rule-coverage gate, check_rule_coverage: after an unfiltered run of the suite, every rule the package defines must have been selected by some test.

Examples

julia> struct Gauss  # a toy node: out ~ Normal(μ, σ)
           μ::Float64
           σ::Float64
       end

julia> BayesBase.logpdf(d::Gauss, x) = logpdf(Normal(d.μ, d.σ), x);

julia> @define_factor_node(node = Gauss, type = Stochastic, interfaces = [:out, :μ, :σ]);

julia> @define_message_update_rule(
           node = Gauss, target = :out, args = (m[:μ]::PointMass, m[:σ]::PointMass),
           body = (args) -> Normal(mean(args.m[:μ]), mean(args.m[:σ])),
       );

julia> @test_message_update_rule(
           node = Gauss, target = :out,
           cases = [(m = (μ = PointMass(1.0), σ = PointMass(2.0)),) => Normal(1.0, 2.0)],
       )

julia> @verify_message_update_rule(node = Gauss, target = :out, m = (μ = PointMass(1.0), σ = PointMass(2.0)));

julia> isempty(check_rule_coverage(@__MODULE__))
true
source

A first test

A node with one rule, the belief propagation message towards the output of a normal distribution with a known variance, and a table with one case for it:

using MessagePassingRulesBase, MessagePassingRulesTestUtils
using BayesBase, ExponentialFamily, Distributions, Test

struct Gaussian end   # out ~ Normal(μ, v)
Gaussian(μ, v) = NormalMeanVariance(μ, v)

@define_factor_node(node = Gaussian, type = Stochastic, interfaces = [:out, :μ, :v])

@define_message_update_rule(
    node = Gaussian, target = :out,
    args = (m[:μ]::NormalMeanVariance, m[:v]::PointMass),
    logscale = 0,
    body = (args) -> NormalMeanVariance(mean(args.m[:μ]), var(args.m[:μ]) + mean(args.m[:v])),
)

@testset "Gaussian: out" begin
    @test_message_update_rule(
        node = Gaussian, target = :out,
        cases = [(m = (μ = NormalMeanVariance(1.0, 2.0), v = PointMass(0.5)),) => NormalMeanVariance(1.0, 2.5)],
    )
end
Test Summary: | Pass  Total  Time
Gaussian: out |   11     11  0.3s

The case gives the rule its inbound messages and the message it must return. The summary counts eleven checks for the one case: the table found the rule, compared its result with the expected one, and ran the case again with its inputs converted to other float types. Testing your first rule explains each check.

Where these rules run

The ReactiveMP engine runs these rules on a factor graph, which RxInfer builds from a model. The tests on this site call the rules directly, never inside a model.

The site

  • Tutorials: Testing your first rule writes tables for a node's belief propagation and variational rules, and reads what a failing case reports. Testing a rule package adds verification against the node, derivative checks and the rule-coverage gate, as every rule package's suite has them.
  • Table tests: every check a case makes, tables for marginal rules and average energies, and the tolerances.
  • Verification against the node: a message rule against the node's log-density, integrated numerically, and what it can integrate.
  • Derivatives: automatic differentiation through a rule, against finite differences.
  • The rule-coverage gate: every rule selected by some test, and how a suite runs the gate.
  • Reading a failure: what each failing check reports, and what to do about it.