Table tests
A table lists cases. Each case is the inputs of one call to a rule and the result the rule must return. A table is the main test of every rule: it is cheap to write, it takes one line per case, and it checks each case in several ways at once.
This page tests the Gaussian node of the first tutorial, a normal distribution with a known variance, with a marginal rule and an average energy added:
using MessagePassingRulesBase, MessagePassingRulesTestUtils
using BayesBase, ExponentialFamily, Distributions, Test
struct Gaussian end
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])),
)
@define_message_update_rule(
node = Gaussian, target = :out,
args = (q[:μ]::Any, q[:v]::PointMass),
body = (args) -> NormalMeanVariance(mean(args.q[:μ]), mean(args.q[:v])),
)
# The joint of `out` and `μ` under q(y, x) q(v), in the information form.
@define_marginal_update_rule(
node = Gaussian, target = (:out, :μ),
args = (m[:out]::NormalMeanVariance, m[:μ]::NormalMeanVariance, q[:v]::PointMass),
body = (args) -> begin
(my, vy), (mx, vx), v = mean_var(args.m[:out]), mean_var(args.m[:μ]), mean(args.q[:v])
MvNormalWeightedMeanPrecision([my / vy, mx / vx], [1 / vy + 1 / v -1 / v; -1 / v 1 / vx + 1 / v])
end,
)
@define_average_energy(
node = Gaussian,
args = (q[:out]::Any, q[:μ]::Any, q[:v]::PointMass),
body = (args) -> begin
y, x, v = args.q[:out], args.q[:μ], mean(args.q[:v])
(log(2v * π) + (var(y) + var(x) + (mean(y) - mean(x))^2) / v) / 2
end,
)A table for the message rules towards out:
@testset "Gaussian: out" begin
@test_message_update_rule(
node = Gaussian, target = :out,
cases = [
(m = (μ = NormalMeanVariance(1.0, 2.0), v = PointMass(3.0)),) => NormalMeanVariance(1.0, 5.0),
(q = (μ = PointMass(1.0), v = PointMass(2.0)),) => NormalMeanVariance(1.0, 2.0),
],
)
endTest Summary: | Pass Total Time
Gaussian: out | 22 22 1.1sThe first case gives messages and selects the belief propagation rule. The second gives marginals and selects the variational one. Each case makes several checks, which the summary counts.
Cases
A case is inputs => expected. The inputs are a NamedTuple with any of these entries:
m, the inbound messages, keyed by the interfaces' declared names.q, the marginals of single interfaces, keyed the same way. A group is a tuple of its members in order, withnothingwhere the rule does not take a member:m = (T = (PointMass(2.0), nothing),).clusters, the joint marginals of clusters, as pairs from the members to the joint:clusters = ((:out, :μ) => q_outμ,).ctx, theRuleContextof services a rule reads, such as a random number generator. The table does not check its services.logscale, the log scales that arrived with the messages, for a rule declared withreads_logscale = true, such as a mixture's:logscale = (in = 0.5,).
The expected value is the rule's result. approximately_equal compares it with what the rule returns. The type must match exactly, a Normal{Float64} for a Normal{Float64}, and the values must agree within the tolerances.
Two wrappers check more than the result. An ExpectedWithLogScale checks the log scale a message rule declares as well. An ExpectedWithAnnotations checks the annotations a rule writes.
@testset "Gaussian: out, with its log scale" begin
@test_message_update_rule(
node = Gaussian, target = :out,
cases = [
(m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(2.0)),) => ExpectedWithLogScale(NormalMeanVariance(0.0, 3.0), 0),
],
)
endTest Summary: | Pass Total Time
Gaussian: out, with its log scale | 12 12 0.1sMarginal rules and average energies
A marginal rule's table names a cluster as its target. This one expects the joint of out and μ:
@testset "Gaussian: (out, μ)" begin
@test_marginal_update_rule(
node = Gaussian, target = (:out, :μ),
cases = [
(m = (out = NormalMeanVariance(1.0, 1.0), μ = NormalMeanVariance(0.0, 2.0)), q = (v = PointMass(0.5),)) =>
MvNormalWeightedMeanPrecision([1.0, 0.0], [3.0 -2.0; -2.0 2.5]),
],
)
endTest Summary: | Pass Total Time
Gaussian: (out, μ) | 14 14 2.6sAn average energy's table has no target, and it expects a number:
@testset "Gaussian: average energy" begin
@test_average_energy(
node = Gaussian,
cases = [
(q = (out = NormalMeanVariance(0.0, 1.0), μ = NormalMeanVariance(1.0, 1.0), v = PointMass(1.0)),) => 1.5 + log(2π) / 2,
],
)
endTest Summary: | Pass Total Time
Gaussian: average energy | 14 14 0.9sWhat every case checks
Beyond the result itself, each case checks:
- Float-type promotion. The case runs again with its inputs converted to
Float32,Float64andBigFloat, thefloat_types. It converts all the inputs at once, then each alone. Withcheck_type_promotion = :exhaustive, it converts every subset. The result must have the promoted type. A rule that hard-codesFloat64fails, and so does a computed log scale that ignores the inputs' precision. - In-place rules. A rule declared
inplace = trueruns again throughrule!, into a buffer from itspreallocate. It must write into that buffer, and the result must agree. - Scratch. A rule declared with
scratchruns again on a scratch thatpoison!filled withNaN. It must give the same result, so a rule that reads its scratch before writing it fails. - Allocations. With
check_nonallocating = true, the rule's call through the engine's entry point must allocate nothing.
The same average-energy case makes more checks when every subset of its three inputs is converted and its allocations are measured. Compare the counts with the table above:
@testset "Gaussian: average energy, exhaustively" begin
@test_average_energy(
node = Gaussian,
cases = [
(q = (out = NormalMeanVariance(0.0, 1.0), μ = NormalMeanVariance(1.0, 1.0), v = PointMass(1.0)),) => 1.5 + log(2π) / 2,
],
check_type_promotion = :exhaustive, check_nonallocating = true,
)
endTest Summary: | Pass Total Time
Gaussian: average energy, exhaustively | 24 24 0.8sThe promotion check shapes how a rule is written. The average energy computes log(2v * π), not log(2π * v): 2π is a Float64, which would turn a Float32 energy into a Float64 one, and the promoted runs would fail.
The tolerances are atol and rtol. Each is a number, or a dictionary from float type to number. By default, atol is 1e-4, 1e-6 and 1e-8 for Float32, Float64 and BigFloat, and rtol is 0. Every case also records the rule it selected, for the rule-coverage gate. Reading a failure shows what each check reports when it fails.
API
The macros are the usual form. They report failures at the line of the table.
MessagePassingRulesTestUtils.@test_message_update_rule — Macro
@test_message_update_rule(node = ..., target = ..., cases = [inputs => expected, ...], ...)Check the message rule of a node towards one of its interfaces against a table of cases, each the inputs of one call and the message it must return. It takes the arguments and keywords of test_message_update_rule, where they are described, all by name.
The macro takes keyword arguments only, written name = value, and passes them to test_message_update_rule; node and target are required, and a positional argument or a missing required keyword is an error when the macro expands. Every check is reported against the line of the macro call, so a failure points at the call that made it.
Examples
@test_message_update_rule(
node = NormalMeanVariance,
target = :out,
cases = [
(m = (μ = PointMass(1.0), v = PointMass(2.0)),) => NormalMeanVariance(1.0, 2.0),
(q = (μ = NormalMeanVariance(1.0, 2.0), v = InverseGamma(3.0, 4.0)),) => NormalMeanVariance(1.0, 4 / 3),
(m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(2.0)),) => ExpectedWithLogScale(NormalMeanVariance(0.0, 3.0), 0),
],
)MessagePassingRulesTestUtils.@test_marginal_update_rule — Macro
@test_marginal_update_rule(node = ..., target = (:out, :μ), cases = [inputs => expected, ...], ...)Check the marginal rule of a node for one of its clusters against a table of cases, each the inputs of one call and the joint marginal it must return. It takes the arguments and keywords of test_marginal_update_rule, where they are described, all by name.
The macro takes keyword arguments only, written name = value, and passes them to test_marginal_update_rule; node and target are required, and a positional argument or a missing required keyword is an error when the macro expands. Every check is reported against the line of the macro call, so a failure points at the call that made it.
Examples
@test_marginal_update_rule(
node = NormalMeanVariance,
target = (:out, :μ),
cases = [
(m = (out = NormalMeanVariance(1.0, 1.0), μ = NormalMeanVariance(2.0, 1.0)), q = (v = PointMass(1.0),)) => q_outμ,
],
)MessagePassingRulesTestUtils.@test_average_energy — Macro
@test_average_energy(node = ..., cases = [inputs => expected, ...], ...)Check the average energy of a node against a table of cases, each the marginals of one call and the energy it must return. It takes the arguments and keywords of test_average_energy, where they are described, all by name.
The macro takes keyword arguments only, written name = value, and passes them to test_average_energy; node is required, and a positional argument or a missing required keyword is an error when the macro expands. Every check is reported against the line of the macro call, so a failure points at the call that made it.
Examples
@test_average_energy(
node = NormalMeanVariance,
cases = [
(q = (out = NormalMeanVariance(0.0, 1.0), μ = NormalMeanVariance(1.0, 1.0), v = PointMass(1.0)),) => 1.5 + log(2π) / 2,
],
)The functions behind them take the node and the target positionally. Their docstrings describe every keyword.
MessagePassingRulesTestUtils.test_message_update_rule — Function
test_message_update_rule(node, target; cases, algorithm = default_algorithm(node), atol = nothing,
rtol = nothing, check_type_promotion = true, float_types = (Float32, Float64, BigFloat),
check_nonallocating = false) -> nothingCheck the message rule of node towards target against a table of cases, each the inputs of one call and the message it must return. @test_message_update_rule is the same check written with keywords, and reports failures at its own line.
Arguments
node: the node, as declared with@define_factor_node.target: the interface the message goes to::out, or(:m, k)for memberkof the groupm.
Keywords
cases: the table, a vector ofinputs => expectedpairs. Required.inputsis aNamedTuplewith any of these entries:m: the inbound messages, keyed by the interfaces' declared names:m = (μ = PointMass(1.0), v = PointMass(2.0)). A group is a tuple of its members in order, withnothingfor a member the rule does not take.q: the marginals of single interfaces, keyed the same way.clusters: the joint marginals of clusters, a tuple of pairs from the cluster's members, in interface order, to its joint:clusters = ((:out, :μ) => q_outμ,).ctx: theRuleContextof services the rule reads. Default: an empty one. Its services are not checked: one the rule declares andctxlacks reads asnothinginside the rule.logscale: the log scales that arrived with the messages, keyed likem:logscale = (in = 0.5,), for a rule declared withreads_logscale = true.
expectedis the message: a plain value, anExpectedWithLogScaleto check the log scale the rule declares as well, or anExpectedWithAnnotationsto check what it annotates.algorithm: the algorithm value to run under. Default:default_algorithm(node). Under aDefaultAlgorithmExtensionthe extension's rules are selected, and the default's where it has none; an inherited rule runs with the value it was written for, as an engine runs it.atol: the absolute tolerance, a number, or aDictfrom float type to number, looked up by the float type of the expected value. Default:nothing, which is1e-4forFloat32,1e-6forFloat64,1e-8forBigFloatand1e-6for any other type; aDictwithout an entry for a type falls back to the same values.rtol: the relative tolerance, in the same forms. Default:nothing, which is0. ADictwithout an entry for a type gives0for it.check_type_promotion: whether to run each case again with inputs of other float types.true(the default) runs it, for every typeTinfloat_types, with all its inputs converted toTand, when it has more than one, with each input alone converted;:exhaustiveconverts every non-empty subset of the inputs instead;falseskips it. An input is one entry ofm,qorclusters, a group's tuple counting as one, converted withBayesBase.convert_paramfloattype;ctxandlogscalestay as they are. The expected value is converted to the promoted float type of all the inputs, and the result must equal it, type included.float_types: the types promotion converts to. Default:(Float32, Float64, BigFloat).check_nonallocating: whether the rule must allocate nothing. Default:false. Whentrue, the rule is run as an engine runs it: resolved, then called withexecute_ruleon a scratch built once and kept between calls, and, for an in-place rule, a buffer preallocated once. The call is measured with@allocatedon its second run, after compilation, and must allocate 0 bytes.source: the line checks are reported against. Default:LineNumberNode(0, :unknown); the macro form passes its own line.
Checks
Each case makes these checks, each a Test assertion:
- A rule is found for the inputs and runs. If none is found, the case fails with the
RuleNotFoundErrortext, which lists the closest rules; if the rule throws, the case fails with the exception and where it was thrown. Either way its other checks are skipped, and the table's other cases still run. The rule found is recorded as selected forcheck_rule_coverage. - The result equals the expected value by
approximately_equal: same type, values within the tolerances. - The annotations an
ExpectedWithAnnotationsnames, and the log scale anExpectedWithLogScalegives. - For an in-place rule,
rule!into a buffer from the rule'spreallocatereturns that buffer, and agrees withrule. - For a rule with scratch, two more runs on one scratch from the rule's
scratchfunction, the first on the scratch as it is and the second afterpoison!, each into a fresh output, agree with the first result. - With
check_nonallocating = true, the rule allocates nothing. - With
check_type_promotion, every promoted run finds a rule and returns the promoted expected value, and a floating-point log scale of the promoted type when the case expects one. Annotations,rule!and scratch are checked on the case's own inputs only.
A rule that throws in any of these runs, rule!, the scratch runs, the measured run or a promoted one, fails that check with the exception, and the others still run.
Throws
ArgumentErrorwhencheck_type_promotionis nottrue,falseor:exhaustive, when a case is not aPair, or when a case's inputs have an entry other thanm,q,clusters,ctxandlogscale. A rule that is missing, wrong or throws is a test failure, not an error.
Examples
test_message_update_rule(
NormalMeanVariance, :out;
cases = [
(m = (μ = PointMass(1.0), v = PointMass(2.0)),) => NormalMeanVariance(1.0, 2.0),
(m = (μ = NormalMeanVariance(0.0, 1.0), v = PointMass(2.0)),) => ExpectedWithLogScale(NormalMeanVariance(0.0, 3.0), 0),
],
)See also test_marginal_update_rule, test_average_energy, check_rule_coverage.
MessagePassingRulesTestUtils.test_marginal_update_rule — Function
test_marginal_update_rule(node, target; cases, algorithm = default_algorithm(node), atol = nothing,
rtol = nothing, check_type_promotion = true, float_types = (Float32, Float64, BigFloat),
check_nonallocating = false) -> nothingCheck the marginal rule of node for the cluster target against a table of cases, each the inputs of one call and the joint marginal it must return. @test_marginal_update_rule is the same check written with keywords, and reports failures at its own line.
Arguments
node: the node, as declared with@define_factor_node.target: the cluster whose joint marginal the rule computes, its members in interface order:(:out, :μ), or(:out, (:T, 1))with a group member.
Keywords
cases: the table, a vector ofinputs => expectedpairs. Required.inputsis aNamedTuplewith any of these entries:m: the inbound messages, keyed by the interfaces' declared names:m = (μ = PointMass(1.0), v = PointMass(2.0)). A group is a tuple of its members in order, withnothingfor a member the rule does not take.q: the marginals of single interfaces, keyed the same way.clusters: the joint marginals of clusters, a tuple of pairs from the cluster's members, in interface order, to its joint:clusters = ((:out, :μ) => q_outμ,).ctx: theRuleContextof services the rule reads. Default: an empty one. Its services are not checked: one the rule declares andctxlacks reads asnothinginside the rule.logscale: the log scales that arrived with the messages, keyed likem:logscale = (in = 0.5,), for a rule declared withreads_logscale = true.
expectedis the joint marginal, a plain value or anExpectedWithAnnotations; a cluster that factorises is expected as aFactorizedCluster. A marginal has no log scale, so anExpectedWithLogScalefails.algorithm: the algorithm value to run under. Default:default_algorithm(node). Under aDefaultAlgorithmExtensionthe extension's rules are selected, and the default's where it has none; an inherited rule runs with the value it was written for, as an engine runs it.atol: the absolute tolerance, a number, or aDictfrom float type to number, looked up by the float type of the expected value. Default:nothing, which is1e-4forFloat32,1e-6forFloat64,1e-8forBigFloatand1e-6for any other type; aDictwithout an entry for a type falls back to the same values.rtol: the relative tolerance, in the same forms. Default:nothing, which is0. ADictwithout an entry for a type gives0for it.check_type_promotion: whether to run each case again with inputs of other float types.true(the default) runs it, for every typeTinfloat_types, with all its inputs converted toTand, when it has more than one, with each input alone converted;:exhaustiveconverts every non-empty subset of the inputs instead;falseskips it. An input is one entry ofm,qorclusters, a group's tuple counting as one, converted withBayesBase.convert_paramfloattype;ctxandlogscalestay as they are. The expected value is converted to the promoted float type of all the inputs, and the result must equal it, type included.float_types: the types promotion converts to. Default:(Float32, Float64, BigFloat).check_nonallocating: whether the rule must allocate nothing. Default:false. Whentrue, the rule is run as an engine runs it: resolved, then called withexecute_ruleon a scratch built once and kept between calls, and, for an in-place rule, a buffer preallocated once. The call is measured with@allocatedon its second run, after compilation, and must allocate 0 bytes.source: the line checks are reported against. Default:LineNumberNode(0, :unknown); the macro form passes its own line.
Checks
Each case makes these checks, each a Test assertion:
- A rule is found for the inputs and runs. If none is found, the case fails with the
RuleNotFoundErrortext, which lists the closest rules; if the rule throws, the case fails with the exception and where it was thrown. Either way its other checks are skipped, and the table's other cases still run. The rule found is recorded as selected forcheck_rule_coverage. - The result equals the expected value by
approximately_equal: same type, values within the tolerances. - The annotations an
ExpectedWithAnnotationsnames, and the log scale anExpectedWithLogScalegives. - For an in-place rule,
rule!into a buffer from the rule'spreallocatereturns that buffer, and agrees withrule. - For a rule with scratch, two more runs on one scratch from the rule's
scratchfunction, the first on the scratch as it is and the second afterpoison!, each into a fresh output, agree with the first result. - With
check_nonallocating = true, the rule allocates nothing. - With
check_type_promotion, every promoted run finds a rule and returns the promoted expected value, and a floating-point log scale of the promoted type when the case expects one. Annotations,rule!and scratch are checked on the case's own inputs only.
A rule that throws in any of these runs, rule!, the scratch runs, the measured run or a promoted one, fails that check with the exception, and the others still run.
Throws
ArgumentErrorwhencheck_type_promotionis nottrue,falseor:exhaustive, when a case is not aPair, or when a case's inputs have an entry other thanm,q,clusters,ctxandlogscale. A rule that is missing, wrong or throws is a test failure, not an error.
See also test_message_update_rule, test_average_energy.
MessagePassingRulesTestUtils.test_average_energy — Function
test_average_energy(node; cases, algorithm = default_algorithm(node), atol = nothing,
rtol = nothing, check_type_promotion = true, float_types = (Float32, Float64, BigFloat),
check_nonallocating = false) -> nothingCheck the average energy of node against a table of cases, each the marginals of one call and the energy it must return. @test_average_energy is the same check written with keywords, and reports failures at its own line.
Arguments
node: the node, as declared with@define_factor_node.
Keywords
cases: the table, a vector ofinputs => expectedpairs. Required.inputsis aNamedTuplewith any of these entries:m: the inbound messages, keyed by the interfaces' declared names:m = (μ = PointMass(1.0), v = PointMass(2.0)). A group is a tuple of its members in order, withnothingfor a member the rule does not take.q: the marginals of single interfaces, keyed the same way.clusters: the joint marginals of clusters, a tuple of pairs from the cluster's members, in interface order, to its joint:clusters = ((:out, :μ) => q_outμ,).ctx: theRuleContextof services the rule reads. Default: an empty one. Its services are not checked: one the rule declares andctxlacks reads asnothinginside the rule.logscale: the log scales that arrived with the messages, keyed likem:logscale = (in = 0.5,), for a rule declared withreads_logscale = true.
An average energy reads marginals,
qandclusters, one per cluster of the factorisation.expectedis the energy, a number, or anExpectedWithAnnotations; an average energy has no log scale, so anExpectedWithLogScalefails.algorithm: the algorithm value to run under. Default:default_algorithm(node). Under aDefaultAlgorithmExtensionthe extension's rules are selected, and the default's where it has none; an inherited rule runs with the value it was written for, as an engine runs it.atol: the absolute tolerance, a number, or aDictfrom float type to number, looked up by the float type of the expected value. Default:nothing, which is1e-4forFloat32,1e-6forFloat64,1e-8forBigFloatand1e-6for any other type; aDictwithout an entry for a type falls back to the same values.rtol: the relative tolerance, in the same forms. Default:nothing, which is0. ADictwithout an entry for a type gives0for it.check_type_promotion: whether to run each case again with inputs of other float types.true(the default) runs it, for every typeTinfloat_types, with all its inputs converted toTand, when it has more than one, with each input alone converted;:exhaustiveconverts every non-empty subset of the inputs instead;falseskips it. An input is one entry ofm,qorclusters, a group's tuple counting as one, converted withBayesBase.convert_paramfloattype;ctxandlogscalestay as they are. The expected value is converted to the promoted float type of all the inputs, and the result must equal it, type included.float_types: the types promotion converts to. Default:(Float32, Float64, BigFloat).check_nonallocating: whether the rule must allocate nothing. Default:false. Whentrue, the rule is run as an engine runs it: resolved, then called withexecute_ruleon a scratch built once and kept between calls, and, for an in-place rule, a buffer preallocated once. The call is measured with@allocatedon its second run, after compilation, and must allocate 0 bytes.source: the line checks are reported against. Default:LineNumberNode(0, :unknown); the macro form passes its own line.
Checks
Each case makes these checks, each a Test assertion:
- A rule is found for the inputs and runs. If none is found, the case fails with the
RuleNotFoundErrortext, which lists the closest rules; if the rule throws, the case fails with the exception and where it was thrown. Either way its other checks are skipped, and the table's other cases still run. The rule found is recorded as selected forcheck_rule_coverage. - The result equals the expected value by
approximately_equal: same type, values within the tolerances. - The annotations an
ExpectedWithAnnotationsnames, and the log scale anExpectedWithLogScalegives. - For an in-place rule,
rule!into a buffer from the rule'spreallocatereturns that buffer, and agrees withrule. - For a rule with scratch, two more runs on one scratch from the rule's
scratchfunction, the first on the scratch as it is and the second afterpoison!, each into a fresh output, agree with the first result. - With
check_nonallocating = true, the rule allocates nothing. - With
check_type_promotion, every promoted run finds a rule and returns the promoted expected value, and a floating-point log scale of the promoted type when the case expects one. Annotations,rule!and scratch are checked on the case's own inputs only.
A rule that throws in any of these runs, rule!, the scratch runs, the measured run or a promoted one, fails that check with the exception, and the others still run.
Throws
ArgumentErrorwhencheck_type_promotionis nottrue,falseor:exhaustive, when a case is not aPair, or when a case's inputs have an entry other thanm,q,clusters,ctxandlogscale. A rule that is missing, wrong or throws is a test failure, not an error.
See also test_message_update_rule, test_marginal_update_rule.
An expected value may carry more than the result.
MessagePassingRulesTestUtils.ExpectedWithLogScale — Type
ExpectedWithLogScale(value, logscale::Real)The expected message of a table case together with the log scale its rule must declare: ExpectedWithLogScale(Beta(2.0, 1.0), -log(2)). For message rules only: marginal rules and average energies have no log scale, and a case expecting one fails.
The log scale must be a number, so a rule that declares none fails, and it is compared with logscale within the table's tolerances for the float type of logscale. On every promoted run of the case (see check_type_promotion), a floating-point log scale must also be of the inputs' promoted float type, as the message is: a computed log scale follows the inputs' precision. A declared integer or irrational constant, logscale = 0, passes that check.
MessagePassingRulesTestUtils.ExpectedWithAnnotations — Type
ExpectedWithAnnotations(value; annotations...)
ExpectedWithAnnotations(value, annotations::NamedTuple)The expected result of a table case together with the annotations the rule must write: ExpectedWithAnnotations(NormalMeanVariance(0.0, 1.0); input_count = 2). The result is compared as a plain expected value is; each annotation is looked up by name among those the rule wrote with annotate!, must be present, and is compared by value within the table's tolerances, not by type. Annotations are checked on the case's own inputs, not on its promoted runs.
Throws
ArgumentErrorwhenlogscaleis among the annotations: a log scale is part of the message, not an annotation, and is expected withExpectedWithLogScale.
The comparison the tables make, and the scratch poisoning, are public, for tests of your own.
MessagePassingRulesTestUtils.approximately_equal — Function
approximately_equal(a, b; atol, rtol) -> BoolWhether two rule outputs agree, the comparison every table case, in-place and scratch check makes. The types must match exactly: a Normal{Float32} never equals a Normal{Float64}. Then the values are compared by what they hold:
- numbers by
isapprox, and arrays, tuples and named tuples (with the same keys) element by element; - a
PointMassby its location, and any otherDistributionby itsparams; - a
FactorizedClusterby its blocks, which must be the same, and then its components; nothingequalsnothing;- any other immutable struct with fields field by field, such as ExponentialFamily's
JointNormal, which is not aDistribution; everything else by==.
Keywords
atol,rtol: the absolute and relative tolerances ofisapprox, both required.
Examples
julia> MessagePassingRulesTestUtils.approximately_equal(Normal(0.0, 1.0), Normal(1e-8, 1.0); atol = 1e-6, rtol = 0)
true
julia> MessagePassingRulesTestUtils.approximately_equal(Normal(0.0, 1.0), Normal(0.0f0, 1.0f0); atol = 1e-6, rtol = 0)
falseMessagePassingRulesTestUtils.poison! — Function
poison!(scratch) -> scratchOverwrite a rule's scratch with values no correct computation reads: NaN in every floating-point array, found through arrays of arrays, tuples and named tuples; any other storage is left as it is. Table tests run a rule that has scratch once more after poisoning its scratch, and the result must not change: a rule that reads its scratch before writing it fails. A package whose scratch holds another kind of storage adds a method.
Examples
julia> scratch = (work = zeros(2), counts = [1, 2]);
julia> MessagePassingRulesTestUtils.poison!(scratch) === scratch
true
julia> all(isnan, scratch.work), scratch.counts
(true, [1, 2])