Arithmetic

The arithmetic nodes +, -, * and dot are deterministic nodes: their output is a function of their inputs, such as out = in[1] + in[2]. Base's and LinearAlgebra's functions are the nodes themselves, so an RxInfer model writes y ~ x + z or y ~ A * x, and rule_coverage(+) takes the function. The package re-exports LinearAlgebra's dot for models to use.

A deterministic node's clusters are always out and the joint over its inputs, whatever the factorisation. Every rule here is therefore belief propagation over messages. The message towards out is the pushforward of the input messages, the distribution of f(in...). The message towards an input inverts the function against the other messages. For normal messages, with one factor of * or dot known, these messages are exact and normal. Between two uncertain factors of * the result has no closed form, and the rules return log-densities or samples instead. None of the nodes has an average energy of its own.

+, - and dot run under DefaultAlgorithm, and * runs under MultiplicationSampling, its default. No model names an algorithm for them.

Example

The sum of a normal and a known value shifts the normal. A known factor of * scales the mean, and it scales the variance by its square:

julia> using StandardMessagePassingRules, MessagePassingRulesBase, ExponentialFamily, BayesBase

julia> sum_message = getresult(@call_message_update_rule(
           node = +, target = :out,
           m = (in = (NormalMeanVariance(1.0, 1.0), PointMass(2.0)),),
       ));

julia> mean(sum_message) ≈ 3.0 && var(sum_message) ≈ 1.0
true

julia> scaled = getresult(@call_message_update_rule(
           node = *, target = :out,
           m = (A = PointMass(2.0), in = NormalMeanVariance(1.0, 1.0)),
       ));

julia> mean(scaled) ≈ 2.0 && var(scaled) ≈ 4.0
true

Addition, +

\[\mathrm{out} = \mathrm{in}_1 + \mathrm{in}_2 + \dots + \mathrm{in}_n, \quad n \ge 2\]

interfacemeaningmessages its rules take
outthe suma normal or a PointMass
inthe terms, a group of two or morea normal or a PointMass each

One node sums any number of terms, so a + b + c, which Julia parses as one call of +, is one node, with no variable for an intermediate sum. That matters in a model with loops, where an intermediate sum needs initial messages of its own and changes the schedule.

MessagePassingRulesBase.nodespec(+)
NodeSpec: +
in…out+deterministic
interfacealiases
out
ina group of any number of members
default algorithmMessagePassingRulesBase.DefaultAlgorithm()
static inputsnone
min group length2
definednodes/addition.jl:3
MessagePassingRulesBase.rule_coverage(+)
Rule coverage for +
DefaultAlgorithm
→ out✓×3
→ (in, k)✓
q(in)✓×2

The message towards out adds the means and the variances of the terms:

@call_message_update_rule(
    node = +, target = :out,
    m = (in = (NormalMeanVariance(1.0, 1.0), NormalMeanVariance(2.0, 3.0), PointMass(1.0)),),
)
RuleResult: message of + towards :outbelief propagation
in[1]in[2]in[3]out+
Result
valueExponentialFamily.NormalMeanVariance{Float64}(μ=4.0, v=4.0)
typeExponentialFamily.NormalMeanVariance{Float64}
log scale0 (declared)
Inputs
edgevalue
in[1]mExponentialFamily.NormalMeanVariance{Float64}(μ=1.0, v=1.0)
in[2]mExponentialFamily.NormalMeanVariance{Float64}(μ=2.0, v=3.0)
in[3]mPointMass{Float64}(1.0)
Rule
declared inputsm[:in...]::Union{BayesBase.PointMass, ExponentialFamily.NormalDistributionsFamily, ExponentialFamily.GaussianDistributionsFamily}
algorithmDefaultAlgorithm()
log scale0
definedaddition/out.jl:4
body
args->sum_of_messages(args.m[:in])
Other rules for this target (2)
addition/out.jl:6
✓algorithm MessagePassingRulesBase.DefaultAlgorithm
✗m[:in...]::Distributions.Distribution member 3 is a BayesBase.PointMass{Float64}, not a Distributions.Distribution
addition/out.jl:8
✓algorithm MessagePassingRulesBase.DefaultAlgorithm
✗m[:in...]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T member 3 is a BayesBase.PointMass{Float64}, not a Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T

Towards a term, the rule subtracts the others from out. With out observed at 5, the message towards the second of two terms is a normal around 5 - 1. The group's tuple holds nothing at the target's own position:

@call_message_update_rule(
    node = +, target = (:in, 2),
    m = (out = PointMass(5.0), in = (NormalMeanVariance(1.0, 1.0), nothing)),
)
RuleResult: message of + towards (:in, 2)belief propagation
outin[1]in[2]+
Result
valueExponentialFamily.NormalMeanVariance{Float64}(μ=4.0, v=1.0)
typeExponentialFamily.NormalMeanVariance{Float64}
log scale0 (declared)
Inputs
edgevalue
outmPointMass{Float64}(5.0)
in[1]mExponentialFamily.NormalMeanVariance{Float64}(μ=1.0, v=1.0)
Rule
declared inputsm[:out]::Union{BayesBase.PointMass, ExponentialFamily.NormalDistributionsFamily, ExponentialFamily.GaussianDistributionsFamily}, m[:in][!k]::Union{BayesBase.PointMass, ExponentialFamily.NormalDistributionsFamily, ExponentialFamily.GaussianDistributionsFamily}
algorithmDefaultAlgorithm()
log scale0
definedaddition/in.jl:4
body
args->difference_message(args.m[:out], sum_of_messages(args.m[:in]))

The messages are normal whenever any term is normal, and a PointMass when every term is known. A normal shifted by a known value keeps its mean–precision or weighted-mean form. Any other result is in mean–variance or mean–covariance form. Towards out, Distributions' convolve sums any other distributions, and it throws a MethodError for a pair it does not know. Every rule has log scale zero.

The joint marginal of the terms, which the free energy takes the entropy of, depends on out's message:

  • with a normal out, the normal terms are one joint normal, their messages times out's message of their sum, and each known term is a block of its own;
  • with out known, the terms lie on the plane where they sum to it. Every normal term but the last is free, and the last is the rest of the sum: an StandardMessagePassingRules.InputsGivenSum.
@call_marginal_update_rule(
    node = +, target = (:in,),
    m = (out = PointMass(3.0), in = (NormalMeanVariance(2.0, 2.0), PointMass(2.0), NormalMeanVariance(0.0, 1.0))),
)
RuleResult: marginal of + over (:in,)marginal
outin[1]in[2]in[3]q(in)+
Result
valueInputsGivenSum{NormalWeightedMeanPrecision{Float64}, Tuple{PointMass{Float64}}, PointMass{Float64}}(ExponentialFamily.NormalWeightedMeanPrecision{Float64}(xi=2.0, w=1.5), (PointMass{Float64}(2.0),), PointMass{Float64}(3.0))
typeStandardMessagePassingRules.InputsGivenSum{ExponentialFamily.NormalWeightedMeanPrecision{Float64}, Tuple{BayesBase.PointMass{Float64}}, BayesBase.PointMass{Float64}}
Inputs
edgevalue
outmPointMass{Float64}(3.0)
in[1]mExponentialFamily.NormalMeanVariance{Float64}(μ=2.0, v=2.0)
in[2]mPointMass{Float64}(2.0)
in[3]mExponentialFamily.NormalMeanVariance{Float64}(μ=0.0, v=1.0)
Rule
declared inputsm[:out]::BayesBase.PointMass, m[:in...]::Union{BayesBase.PointMass, ExponentialFamily.NormalDistributionsFamily, ExponentialFamily.GaussianDistributionsFamily}
algorithmDefaultAlgorithm()
definedaddition/marginals.jl:9
body
args->sum_inputs_joint(args.m[:out], args.m[:in])
Other rules for this target (1)
addition/marginals.jl:3
✓algorithm MessagePassingRulesBase.DefaultAlgorithm
✗m[:out]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got BayesBase.PointMass{Float64}
✓m[:in...]::Union{BayesBase.PointMass, ExponentialFamily.NormalDistributionsFamily, ExponentialFamily.GaussianDistributionsFamily} got Tuple{ExponentialFamily.NormalMeanVariance{Float64}, BayesBase.PointMass{Float64}, ExponentialFamily.NormalMeanVariance{Float64}}
StandardMessagePassingRules.InputsGivenSum — Type
StandardMessagePassingRules.InputsGivenSum

The joint of the inputs of + when their sum is known, the marginal rule's result for a PointMass on out: the inputs then lie on the plane where they sum to it. The point-mass inputs are known, and of the Gaussian ones every member but the last is free; the last is their difference from the sum. Its entropy is the free members' joint entropy and one point-mass entropy for each known input and for the sum, which the free energy needs.

Fields

  • free: the joint of the free Gaussian members, their messages times the last member's message of the difference, or nothing when at most one input is Gaussian;
  • points: the point-mass inputs, a tuple;
  • sum: the sum, the PointMass on out.
source

Subtraction, -

\[\mathrm{out} = \mathrm{in}_1 - \mathrm{in}_2\]

interfacemeaningmessages its rules take
outthe differencea normal or a PointMass
in1the minuenda normal or a PointMass
in2the subtrahenda normal or a PointMass
MessagePassingRulesBase.rule_coverage(-)
Rule coverage for -
DefaultAlgorithm
→ out✓
→ in1✓×3
→ in2✓
q(in1, in2)✓×3

Its rules are +'s, rearranged. Towards out, the message is the difference of the inputs. Towards in1, it is the sum of out and in2, and it takes any two distributions through convolve, as + does towards out. Towards in2, it is the difference of in1 and out.

Multiplication, *

\[\mathrm{out} = A \cdot \mathrm{in}\]

interfacemeaningmessages its rules take
outthe producta normal, a Gamma, a PointMass, or any univariate distribution
Athe left factora PointMass (scalar, vector, matrix or UniformScaling), a univariate normal, or any univariate distribution
inthe right factora PointMass, a normal, a Gamma, or any univariate distribution
MessagePassingRulesBase.nodespec(*)
NodeSpec: *
Ainout*deterministic
interfacealiases
out
A
in
default algorithmMultiplicationSampling(3000)
static inputsnone
definednodes/multiplication.jl:36
MessagePassingRulesBase.rule_coverage(*)
Rule coverage for *
MultiplicationSampling
→ out✓×11
→ A✓×6
→ in✓×7
q(A, in)✓×2

With a known matrix A, the message towards out maps the normal message on in through A: the mean becomes A μ and the covariance A Σ Aᵀ:

@call_message_update_rule(
    node = *, target = :out,
    m = (A = PointMass([1.0 1.0; 0.0 1.0]), in = MvNormalMeanCovariance([1.0, 2.0], diageye(2))),
)
RuleResult: message of * towards :outmessages
Ainout*
Result
valueMvNormalMeanCovariance( μ: [3.0, 2.0] Σ: [2.0 1.0; 1.0 1.0] )
typeExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
log scale0 (declared)
Inputs
edgevalue
AmPointMass{Matrix{Float64}}([1.0 1.0; 0.0 1.0])
inmMvNormalMeanCovariance( μ: [1.0, 2.0] Σ: [1.0 0.0; 0.0 1.0] )
Rule
declared inputsm[:A]::BayesBase.PointMass{<:AbstractMatrix}, m[:in]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T
algorithmMultiplicationSampling(3000)
log scale0
definedmultiplication/out.jl:9
body
args->scaled(mean(args.m[:A]), args.m[:in])
Other rules for this target (10)
multiplication/out.jl:3
✓algorithm MultiplicationSampling
✓m[:A]::BayesBase.PointMass got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::BayesBase.PointMass got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:5
✓algorithm MultiplicationSampling
✗m[:A]::BayesBase.PointMass{<:Real} got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::ExponentialFamily.GammaDistributionsFamily got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:7
✓algorithm MultiplicationSampling
✗m[:A]::ExponentialFamily.GammaDistributionsFamily got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::BayesBase.PointMass{<:Real} got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:17
✓algorithm MultiplicationSampling
✗m[:A]::BayesBase.PointMass{<:AbstractVector} got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:24
✓algorithm MultiplicationSampling
✗m[:A]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::BayesBase.PointMass{<:AbstractVector} got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:31
✓algorithm MultiplicationSampling
✗m[:A]::BayesBase.PointMass{<:Real} got BayesBase.PointMass{Matrix{Float64}}
✓m[:in]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:38
✓algorithm MultiplicationSampling
✗m[:A]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::BayesBase.PointMass{<:Real} got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:45
✓algorithm MultiplicationSampling
✗m[:A]::BayesBase.PointMass{<:LinearAlgebra.UniformScaling} got BayesBase.PointMass{Matrix{Float64}}
✓m[:in]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:53
✓algorithm MultiplicationSampling
✗m[:A]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
multiplication/out.jl:63
✓algorithm MultiplicationSampling
✗m[:A]::Distributions.UnivariateDistribution got BayesBase.PointMass{Matrix{Float64}}
✗m[:in]::Distributions.UnivariateDistribution got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}

Towards in, the message is a normal in weighted-mean form, with precision Aᵀ Σ⁻¹ A and weighted mean Aᵀ Σ⁻¹ m from the message N(m, Σ) on out. The card lists the service the rule reads, matrix_correction:

@call_message_update_rule(
    node = *, target = :in,
    m = (out = MvNormalMeanCovariance([3.0, 2.0], diageye(2)), A = PointMass([1.0 1.0; 0.0 1.0])),
)
RuleResult: message of * towards :inmessages
outAin*
Result
valueMvNormalWeightedMeanPrecision( xi: [3.0, 5.0] Λ: [1.0 1.0; 1.0 2.0] )
typeExponentialFamily.MvNormalWeightedMeanPrecision{Float64, Vector{Float64}, Matrix{Float64}}
log scaleundefined: the message rule for * towards :in under MultiplicationSampling declares no `logscale`
Inputs
edgevalue
outmMvNormalMeanCovariance( μ: [3.0, 2.0] Σ: [1.0 0.0; 0.0 1.0] )
AmPointMass{Matrix{Float64}}([1.0 1.0; 0.0 1.0])
Rule
declared inputsm[:out]::Union{ExponentialFamily.MultivariateNormalDistributionsFamily{T}, ExponentialFamily.MultivariateGaussianDistributionsFamily{T}} where T, m[:A]::BayesBase.PointMass{<:Union{AbstractMatrix, AbstractVector}}
algorithmMultiplicationSampling(3000)
servicesmatrix_correction
log scalenone
definedmultiplication/in.jl:5
body
(ctx, args)->unscaled(ctx, args.m[:out], mean(args.m[:A]))
Other rules for this target (6)
multiplication/in.jl:1
✓algorithm MultiplicationSampling
✗m[:out]::BayesBase.PointMass got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✓m[:A]::BayesBase.PointMass got BayesBase.PointMass{Matrix{Float64}}
multiplication/in.jl:3
✓algorithm MultiplicationSampling
✗m[:out]::ExponentialFamily.GammaDistributionsFamily got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✗m[:A]::BayesBase.PointMass{<:Real} got BayesBase.PointMass{Matrix{Float64}}
multiplication/in.jl:11
✓algorithm MultiplicationSampling
✓m[:out]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✗m[:A]::BayesBase.PointMass{<:Real} got BayesBase.PointMass{Matrix{Float64}}
multiplication/in.jl:18
✓algorithm MultiplicationSampling
✓m[:out]::Union{ExponentialFamily.NormalDistributionsFamily{T}, ExponentialFamily.GaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✗m[:A]::BayesBase.PointMass{<:LinearAlgebra.UniformScaling} got BayesBase.PointMass{Matrix{Float64}}
multiplication/in.jl:25
✓algorithm MultiplicationSampling
✗m[:out]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✗m[:A]::Union{ExponentialFamily.UnivariateNormalDistributionsFamily{T}, ExponentialFamily.UnivariateGaussianDistributionsFamily{T}} where T got BayesBase.PointMass{Matrix{Float64}}
multiplication/in.jl:31
✓algorithm MultiplicationSampling
✗m[:out]::Distributions.UnivariateDistribution got ExponentialFamily.MvNormalMeanCovariance{Float64, Vector{Float64}, Matrix{Float64}}
✗m[:A]::Distributions.UnivariateDistribution got BayesBase.PointMass{Matrix{Float64}}

With one factor known, the rules are in closed form. Towards out, the message is a scaled normal or Gamma. Towards the unknown factor, it is a normal in weighted-mean form or a Gamma, with the log scale -d log |c| of a known scalar c. A known factor may be a scalar, a vector or a matrix. With a vector, the other factor is a scalar. With a matrix, the rules compute only A * in, never in * A, so they refuse a matrix in towards A. The precision matrices that the rules towards an input build may be singular. The rules correct them with the context's matrix_correction, ReplaceZeroDiagonalEntries(tiny) by default.

Between two univariate normals, the messages are log-densities, a ContinuousUnivariateLogPdf. Towards an input, the rule computes the exact integral. Towards out, it sums a Bessel series truncated at ten terms. Between two other univariate distributions, the rules draw samples from the context's rng, as many as the algorithm says. These messages are unnormalised and declare no log scale.

StandardMessagePassingRules.MultiplicationSampling — Type
MultiplicationSampling(; samples::Int = 3000)

The algorithm of the node *, out = A * in, and its default, so a model need not name it. With a known factor the rules are closed-form. Between two univariate normals they return a ContinuousUnivariateLogPdf: towards an input the exact integral, towards out a Bessel series truncated at ten terms. Between two other univariate distributions, one rule towards each interface draws samples values of one factor from the rule context's rng and returns an unnormalised ContinuousUnivariateLogPdf.

It extends DefaultAlgorithm, as a DefaultAlgorithmExtension, so a rule another package defines for * under the default still applies.

Keywords

  • samples: how many values the sampling rules draw, a positive Int. Default 3000.

Examples

julia> MultiplicationSampling().samples
3000

julia> MultiplicationSampling(samples = 500).samples
500
source

Dot product, dot

\[\mathrm{out} = \mathrm{in}_1^\top \mathrm{in}_2\]

interfacemeaningmessages its rules take
outthe scalar producta univariate normal
in1the first vectora PointMass or a normal
in2the second vectora PointMass or a normal
MessagePassingRulesBase.rule_coverage(dot)
Rule coverage for dot
DefaultAlgorithm
→ out✓×3
→ in1✓×2
→ in2✓×2
q(in1, in2)✓×2

Limitations. One input must be known. Two normal inputs have no closed form. The rules for that case exist only to throw an error that points to the SoftDot node, whose package handles it. The precision a w aᵀ towards an input has rank one, and the rule corrects it with the context's matrix_correction, ReplaceZeroDiagonalEntries(tiny) by default.