Rule Not Found Error
When using RxInfer, you might encounter a RuleNotFoundError. This error occurs during message-passing inference when the system cannot find appropriate update rules for computing messages between nodes in your factor graph. Let's understand why this happens and how to resolve it.
Why does this happen?
Message-passing inference works by exchanging messages between nodes in a factor graph. Each message represents a probability distribution, and the rules for computing these messages depend on:
- The type of the factor node (e.g.,
Normal,Gamma, etc.) - The types of incoming messages (e.g.,
Normal,PointMass, etc.) - The interface through which the message is being computed
- The inference method being used (Belief Propagation or Variational Message Passing), and the node's algorithm
The fourth point is particularly important - some message update rules may exist for Variational Message Passing (VMP) but not for Belief Propagation (BP), or vice versa. This is because BP aims to compute exact posterior distributions through message passing (when possible), while VMP approximates the posterior using the Bethe approximation. For a detailed mathematical treatment of these differences, see our Bethe Free Energy implementation guide.
For example, consider this simple model:
using RxInfer
@model function problematic_model(y)
μ ~ Normal(mean = 0.0, variance = 1.0)
τ ~ Gamma(shape = 1.0, rate = 1.0)
y ~ Normal(mean = μ, precision = τ)
endInference with belief propagation, the default, fails with a RuleNotFoundError:
try
infer(
model = problematic_model(),
data = (y = 1.0,),
)
catch err
showerror(stdout, err)
endRuleNotFoundError: no message rule for NormalMeanPrecision towards :μ under DefaultAlgorithm() takes the inputs (m[:τ]::GammaShapeRate{Float64}, q[:out]::PointMass{Float64})
no rule consumes this set of inputs under this algorithm (no rule of this shape)
what to try: the factorisation decides which inputs a rule receives (messages from the target's cluster, marginals of the other clusters): choose one that delivers the inputs a rule below takes (in RxInfer, `@constraints`), or define a rule for these inputs
near misses:
rule at ~/.julia/packages/GCVMessagePassingRules/yofPH/src/gaussian_extension.jl:18
✓ algorithm DefaultAlgorithm
✗ m[:out]::ExponentialLinearQuadratic not provided
✗ m[:τ]::PointMass got GammaShapeRate{Float64}
✗ q[:out]::PointMass{Float64} provided but not consumed
rule at ~/.julia/packages/GCVMessagePassingRules/yofPH/src/gaussian_extension.jl:23
✓ algorithm DefaultAlgorithm
✗ m[:out]::ExponentialLinearQuadratic not provided
✗ q[:τ]::Any not provided
✗ m[:τ]::GammaShapeRate{Float64} provided but not consumed
✗ q[:out]::PointMass{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:1
✓ algorithm DefaultAlgorithm
✗ m[:out]::PointMass not provided
✗ m[:τ]::PointMass got GammaShapeRate{Float64}
✗ q[:out]::PointMass{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:8
✓ algorithm DefaultAlgorithm
✗ m[:out]::Union{UnivariateNormalDistributionsFamily{T}, UnivariateGaussianDistributionsFamily{T}} where T not provided
✗ m[:τ]::PointMass got GammaShapeRate{Float64}
✗ q[:out]::PointMass{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:18
✓ algorithm DefaultAlgorithm
✓ q[:out]::PointMass got PointMass{Float64}
✗ q[:τ]::PointMass not provided
✗ m[:τ]::GammaShapeRate{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:25
✓ algorithm DefaultAlgorithm
✓ q[:out]::Any got PointMass{Float64}
✗ q[:τ]::Any not provided
✗ m[:τ]::GammaShapeRate{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:31
✓ algorithm DefaultAlgorithm
✗ m[:out]::PointMass not provided
✗ q[:τ]::Any not provided
✗ m[:τ]::GammaShapeRate{Float64} provided but not consumed
✗ q[:out]::PointMass{Float64} provided but not consumed
rule at ~/.julia/packages/StandardMessagePassingRules/OeA3e/src/rules/normal_mean_precision/mean.jl:37
✓ algorithm DefaultAlgorithm
✗ m[:out]::Union{UnivariateNormalDistributionsFamily{T}, UnivariateGaussianDistributionsFamily{T}} where T not provided
✗ q[:τ]::Any not provided
✗ m[:τ]::GammaShapeRate{Float64} provided but not consumed
✗ q[:out]::PointMass{Float64} provided but not consumedThere are no belief propagation message update rules for this combination of distributions, only variational message passing rules. Even though the model looks simple, the messages needed for exact inference do not exist in closed form.
Reading the error
The RuleNotFoundError above is a report in four parts:
- The first line names the node (
NormalMeanPrecision), the edge the message is computed for (:μ), the node's algorithm and the inputs the engine offered: here the messagem[:τ]from theGammaprior and the marginalq[:out]of the observation.m[:x]is a message arriving on edgex, andq[:x]a marginal. - The diagnosis says why nothing matched.
- The
what to tryline suggests the change that addresses the diagnosis. - The near misses list the node's rules for this edge, each with its inputs marked:
✓for an input that matches,✗ … not providedfor an input the rule needs and did not get,✗ … got Tfor an input of the wrong type, and✗ … provided but not consumedfor an input the engine offered and the rule does not take.
The diagnosis and its what to try line take one of four forms:
| Diagnosis | What to try |
|---|---|
| no rule exists for this node and target under any algorithm | load the package that defines the node's rules, or define the rule |
| a rule of this shape exists, but the input types do not fit | project the inputs onto a family the rule takes with a form constraint in @constraints, or define a rule for these types |
| rules of this shape exist under another algorithm | give the node that algorithm (see Algorithm specification) |
| no rule consumes this set of inputs under this algorithm | change the factorization in @constraints, which decides whether a rule receives a message or a marginal on each edge, or define a rule for these inputs |
In the example, the diagnosis is the last one. Two of the near misses take q[:out] and q[:τ]: the marginal of τ where the engine offered its message. A mean-field factorization q(μ, τ) = q(μ)q(τ) delivers exactly that (see Use variational inference below).
Common scenarios
You're likely to encounter this error when:
- Using non-conjugate pairs of distributions (e.g.,
Betaprior withNormallikelihood with precision parameterization) - Working with custom distributions or factor nodes without defining all necessary update rules
- Using complex transformations between variables that don't have defined message computations
- Mixing different types of distributions in ways that don't have analytical solutions
Design Philosophy
RxInfer prioritizes performance over generality in its message-passing implementation. By default, it only uses analytically derived message update rules, even in cases where numerical approximations might be possible. This design choice:
- Ensures fast and reliable inference when rules exist
- Avoids potential numerical instabilities from approximations
- Throws an error when analytical solutions don't exist
This means you may encounter RuleNotFoundError even in cases where approximate solutions could theoretically work. This is intentional - RxInfer will tell you explicitly when you need to consider alternative approaches rather than silently falling back to potentially slower or less reliable approximations. See the Solutions section below for more details.
Visualizing the message passing graph
To better understand where message passing rules are needed, let's look at a simple factor graph visualization:
In this example:
- Variables (
x,y,z) are represented as circles - The factor node (
f) is represented as a square - Messages (μ) flow along the edges between variables and factors, with subscripts indicating direction (e.g., x→f flows from x to f)
- Faded nodes (g, h) represent other parts of the factor graph that aren't relevant for this local message computation
To compute the outgoing message f→y, RxInfer needs:
- Rules for how to process incoming messages
x→fandz→f - Rules for combining these messages based on the factor
f's type - Rules for producing the outgoing message type that
yexpects
A RuleNotFoundError occurs when any of these rules are missing. For example, if x sends a Normal message but f doesn't know how to process Normal inputs, or if f can't produce the type of message that y expects.
Solutions
1. Convert to conjugate pairs
First, try to reformulate your model using conjugate prior-likelihood pairs. Conjugate pairs have analytical solutions for message passing and are well-supported in RxInfer. For example, instead of using a Normal likelihood with Beta prior on its precision, use a Normal-Gamma conjugate pair. See Conjugate prior - Wikipedia for a comprehensive list of conjugate distributions.
2. Check available rules
If conjugate pairs aren't suitable, verify if your combination of distributions and message types is supported. RxInfer provides many predefined rules, but not all combinations are possible. The rules of the standard nodes are documented on the site of StandardMessagePassingRules, and every other node has a package and a site of its own, listed on ReactiveMP's ecosystem page. RxInfer loads some of them (Delta, AR, DiscreteTransition, GCV, GaussianCoupling and SoftDot, see Rule packages); a node from another needs its package loaded, for example using PolyaMessagePassingRules.
You can also ask for the rules directly. MessagePassingRulesBase.rule_coverage lists, per edge and algorithm, how many rules a node has:
MessagePassingRulesBase.rule_coverage(NormalMeanPrecision)| DefaultAlgorithm | |
|---|---|
| → out | ✓×6 |
| → μ | ✓×8 |
| → τ | ✓×2 |
| q(out, μ, τ) | ✓×4 |
| q(out, μ) | ✓×4 |
| average energy | ✓×2 |
MessagePassingRulesBase.which_message_update_rule shows which rule would run for given inputs, the way the engine selects it:
MessagePassingRulesBase.which_message_update_rule(
NormalMeanPrecision, :μ;
q = (out = PointMass(1.0), τ = GammaShapeRate(1.0, 1.0)),
)| inputs | q[:out]::Any, q[:τ]::Any |
|---|---|
| in-place | no |
| scratch | no |
| pure | yes |
| services | none |
| log scale | none |
| defined | normal_mean_precision/mean.jl:25 |
| body | args->NormalMeanPrecision(mean(args.q[:out]), mean(args.q[:τ])) |
3. Create custom update rules
If you need specific message computations, you can define your own update rules. See Creating your own custom nodes for a detailed guide on implementing custom nodes and their update rules.
4. Use approximations
When exact message updates aren't available, consider:
- Using simpler distribution pairs that have defined rules
- Choosing a node's approximation through its algorithm, as described in Algorithm specification; a deterministic transformation takes
DeltaApproximation(method = Linearization()),Unscented()orCVIProjection()(see Deterministic nodes) - Passing
options = (rulefallback = NodeFunctionRuleFallback(),)toinfer: where no rule matches, a stochastic node sends its own log-density with every other input collapsed to its mean, an unnormalizedNodeFunctionLogPdf; a functional form constraint (see Built-in Functional Forms) turns the resulting posterior into a proper distribution
5. Use variational inference
Sometimes, adding appropriate factorization constraints can help avoid problematic message computations:
constraints = @constraints begin
q(μ, τ) = q(μ)q(τ) # Mean-field assumption
end
result = infer(
model = problematic_model(),
data = (y = 1.0,),
constraints = constraints,
initialization = @initialization(q(τ) = GammaShapeRate(1.0, 1.0)),
iterations = 10,
)
result.posteriors[:μ][end]NormalWeightedMeanPrecision{Float64}(xi=1.1120849120310916, w=2.112084912031092)When using variational constraints, you will likely need to initialize certain messages or marginals to handle loops in the factor graph. See Initialization for details on how to properly initialize your model.
For more details on constraints and variational inference, see:
- Constraints Specification for a complete guide on using constraints
- Bethe Free Energy for the mathematical background on variational inference and message passing
Implementation details
When RxInfer encounters a missing rule, it means one of these is missing:
- A message update rule for the specific message direction and input types, declared with
@define_message_update_rule - A marginal update rule for computing joint marginals, declared with
@define_marginal_update_rule - An average energy for free energy computation, declared with
@define_average_energy
For an explanation of what rules are and how they work, see Understanding Rules. You can add these using the methods described in Creating your own custom nodes.