Inspecting rules
This page covers three questions: which rule a call would run, which rules a node has, and whether the rules are consistent. A rule author asks them at the REPL, and a rule package's tests ask them too. Drawings draws a node or a rule as an image of its own, for slides and notes.
The examples use the Shift node of Calling rules, out = in + c, with rules towards out and in:
Which rule would run
The which_* queries resolve a rule without running it. They return the rule's RuleSpec:
which_message_update_rule(Shift, :in; m = (out = Gauss(4.0, 2.0), c = 3.0))| inputs | m[:out]::Gauss, m[:c]::Real |
|---|---|
| in-place | no |
| scratch | no |
| pure | yes |
| services | none |
| log scale | 0 |
| defined | string:6 |
| body | args->Gauss((args.m[:out]).m - args.m[:c], (args.m[:out]).v) |
The RuleSpec shows the rule's inputs, its flags, its log scale declaration, where it was defined, and its body.
MessagePassingRulesBase.which_message_update_rule — Function
which_message_update_rule(node, target; m, q, clusters, algorithm)The RuleSpec that call_message_update_rule would run for these inputs, without running it. It displays the rule with its source, file and line. @which_message_update_rule is the same query 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 anArgumentErrorsaying so.target: the interface the message goes to::out, or(:m, k)for memberkof the groupm, as(:in, 2).
Keywords
All optional; only the inputs' types matter, as they are what selects a rule.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no rule fits, the query throws a RuleNotFoundError, which lists the closest rules and why each does not fit.
MessagePassingRulesBase.which_marginal_update_rule — Function
which_marginal_update_rule(node, target; m, q, clusters, algorithm)The RuleSpec that call_marginal_update_rule would run for these inputs, without running it. It displays the rule with its source, file and line. @which_marginal_update_rule is the same query 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 anArgumentErrorsaying 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; only the inputs' types matter, as they are what selects a rule.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no rule fits, the query throws a RuleNotFoundError, which lists the closest rules and why each does not fit.
MessagePassingRulesBase.which_average_energy — Function
which_average_energy(node; q, clusters, m, algorithm)The RuleSpec that call_average_energy would run for these marginals, without running it. It displays the energy with its source, file and line. @which_average_energy is the same query 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 anArgumentErrorsaying so.
Keywords
All optional; only the inputs' types matter, as they are what selects the energy.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no energy fits, the query throws a RuleNotFoundError, which lists the closest ones and why each does not fit.
MessagePassingRulesBase.@which_message_update_rule — Macro
@which_message_update_rule(node = ..., target = ..., m = (...), q = (...), clusters = (...), algorithm = ...)The RuleSpec that @call_message_update_rule would run for these inputs, without running it; it displays the rule with its source, file and line. The macro takes keyword arguments only; node and target are required, the rest optional. It is which_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 anArgumentErrorsaying so.target: the interface the message goes to::out, or(:m, k)for memberkof the groupm, as(:in, 2).
Optional keywords
Only the inputs' types matter, as they are what selects a rule.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no rule fits, the query throws a RuleNotFoundError, which lists the closest rules and why each does not fit.
MessagePassingRulesBase.@which_marginal_update_rule — Macro
@which_marginal_update_rule(node = ..., target = (:y, :x), m = (...), q = (...), clusters = (...), algorithm = ...)The RuleSpec that @call_marginal_update_rule would run for these inputs, without running it; it displays the rule with its source, file and line. The macro takes keyword arguments only; node and target are required, the rest optional. It is which_marginal_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 anArgumentErrorsaying 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
Only the inputs' types matter, as they are what selects a rule.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no rule fits, the query throws a RuleNotFoundError, which lists the closest rules and why each does not fit.
MessagePassingRulesBase.@which_average_energy — Macro
@which_average_energy(node = ..., q = (...), clusters = (...), m = (...), algorithm = ...)The RuleSpec that @call_average_energy would run for these marginals, without running it; it displays the energy with its source, file and line. The macro takes keyword arguments only; node is required, the rest optional. It is which_average_energy with node 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 anArgumentErrorsaying so.
Optional keywords
Only the inputs' types matter, as they are what selects the energy.
m: the inbound messages, aNamedTuplekeyed 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,nothingmay 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,) => qis the joint over the groupin. 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 aDefaultAlgorithmExtensionthe default's as well; an inherited rule runs with the value it was written for (rule_algorithm). Default: the node's,default_algorithm(node).
When no energy fits, the query throws a RuleNotFoundError, which lists the closest ones and why each does not fit.
MessagePassingRulesBase.RuleSpec — Type
RuleSpecA rule as data, and the thing that runs: what a definition macro builds, what resolution (find_message_rule and its siblings) returns, and what execute_rule runs. Its body, preallocation, scratch and log-scale functions and its algorithm type are type parameters, RuleSpec{B, P, S, L, A}: where resolution is inferred, as at an engine's call site, which reaches one rule, it returns one concrete spec, and the body call is static. It shows itself at the REPL with its inputs, flags, source, file and line.
What it declares, read as fields:
kind::message,:marginalor:average_energy;node,algorithm: the node, and the algorithm type the rule is defined for;target: the target's type, aTarget,IndexedTargetorClusterTargettype, orNothingfor an average energy;inputs: its typed inputs,InputSpecs; for a rule declared withdefaultamong itsargs(defaultistrue), only the typed ones beside it;inplace,pure,services: whether it writes into a given buffer, whether it is pure (declared, or its algorithm'sispure), and the context services it declares;annotates: whether its body takes theannslot, and so may write annotations on its result; an engine gives a rule that does not an annotation store nobody writes to;logscale: what a message rule declares about its result's log scale:nothingfor none, a number, a function,from_bodyorimproper;reads_logscale: whether it reads its inbound messages' log scales;args_check: the source of the check its inputs must pass as its body starts, ornothingfor none (the definition macros'args_checkkeyword; a failure is aRuleInputError);source,file,line: its body's source and where it was defined.
The other fields are internal: signature, the RuleArgs type it dispatches on; body, prealloc and scratch, the macro-generated functions over the full slot lists, the body over (output, scratch, algo, ctx, args, ann, target) and the others over (algo, ctx, args, target). Run a rule through execute_rule or a call, never by calling them.
MessagePassingRulesBase.InputSpec — Type
InputSpecOne typed input a rule declares in its args, as a RuleSpec records it. Fields:
container::Symbol::mfor a message,:qfor a marginal;key: the interface's name, or a cluster's tuple of members;selection::Symbol::singlefor an interface,:clusterfor a joint, and for a group:all(m[:in...]),:aligned(m[:in][k]) or:allbutself(m[:in][!k]);type: the declared type, of each member for a group.
Which rules exist
rule_coverage tabulates what a node can compute, and under which algorithm. Node packages show it on their pages.
MessagePassingRulesBase.rule_coverage(Shift)| DefaultAlgorithm | |
|---|---|
| → out | ✓ |
| → in | ✓ |
| → c |
Shift has a rule towards out and one towards in, both under DefaultAlgorithm. It has no rule towards c. list_rules returns the rules themselves:
julia> MessagePassingRulesBase.list_rules(Shift, :in)1-element Vector{MessagePassingRulesBase.RuleSpec}: RuleSpec(message rule for Shift towards :in under DefaultAlgorithm @ string:6)
MessagePassingRulesBase.list_rules — Function
list_rules(node, edge::Union{Nothing, Symbol} = nothing; algorithm = nothing) -> Vector{RuleSpec}The rules defined for node in every loaded module, as RuleSpecs: all of them, message and marginal rules and average energies, or, with edge, only the message rules towards edge, a group's name standing for all its members.
Keywords
algorithm: an algorithm value, keeping only the rules it can select: its own, and for aDefaultAlgorithmExtensionthe default's as well. Default:nothing, every algorithm.
MessagePassingRulesBase.list_rules(NormalMeanVariance, :out) # every rule towards `out`
MessagePassingRulesBase.list_rules(NormalMeanVariance; algorithm = DefaultAlgorithm())MessagePassingRulesBase.RuleCoverage — Type
RuleCoverageWhich rules exist for a node, as rule_coverage makes it: rows are message targets (→ out, and → (m, k) for a group), marginal clusters (q(out, μ)) and the average energy; columns are algorithms, the node's default first; each cell counts the rules. It shows itself as a text table at the REPL, ✓ for one rule and ✓×n for several, and as an HTML table in a notebook or on a documentation page.
MessagePassingRulesBase.rule_coverage — Function
rule_coverage(node) -> RuleCoverageThe RuleCoverage table of node: what can be computed, under which algorithm, from the rules in every loaded module. Every interface of a declared node has a row, so a target without rules shows as an empty row.
A marginal rule over any cluster, declared target = members, has the row q(any cluster). A column is an algorithm type, labelled with its parameters, so the variants of a parametric algorithm are told apart; the node's own algorithm has a column even without rules, unless the rules are declared on a type that covers it.
For a node Shift with interfaces out and in and a single rule, towards out:
julia> MessagePassingRulesBase.rule_coverage(Shift)
Rule coverage for Shift
│ DefaultAlgorithm
→ out │ ✓
→ in │MessagePassingRulesBase.prettify_modules — Function
prettify_modules(text::AbstractString) -> Stringtext, a printed name, type or value, without the modules Julia generates: Documenter's sandboxes (Main.var"__atexample__named__page".Gaussian becomes Gaussian), test items' and gensym'd modules. A package's module stays. The displays of this package print through it.
Drawings
A node's declaration, a rule, a rule's result and a node's dependencies show as cards in a notebook, each with the node drawn. drawing gives you that drawing on its own, as an SVG image:
d = MessagePassingRulesBase.drawing(MessagePassingRulesBase.nodespec(Shift))The image carries its colours on its elements, so it needs no stylesheet. write saves it to a file, which a slide, a document or a vector editor opens as it is:
write(joinpath(mktempdir(), "shift.svg"), d)2508The result is the number of bytes written. A rule draws what it consumes and what it computes:
MessagePassingRulesBase.drawing(which_message_update_rule(Shift, :in; m = (out = Gauss(4.0, 2.0), c = 3.0)))MessagePassingRulesBase.drawing — Function
drawing(x) -> Drawingx drawn as a standalone SVG, the drawing its card shows: a NodeSpec as its node and interfaces, a RuleSpec or a RuleResult as what the rule consumes and computes, and a DependenciesSpec as one small node per target. The Drawing shows as an image in notebooks and in the documentation; write("node.svg", drawing(x)) saves it, for slides or notes. Its colours are the light palette's, on a white background.
MessagePassingRulesBase.Drawing — Type
DrawingThe drawing drawing returns: a standalone SVG document, with its colours written on its elements, so that it needs no stylesheet and looks the same in a browser, a slide or a document. It shows as image/svg+xml in notebooks and in the documentation, and write saves it, write("node.svg", d).
Checking the rules
A rule package's tests check its rules in two ways:
- against their nodes' declarations, with
check_rules; - against each other, with
check_rule_ambiguities. Two rules that some call matches equally well make resolution throw aMethodError.
Each check takes the modules to check, every loaded module by default, and returns the problems it finds. Both lists are empty for this page's module:
julia> MessagePassingRulesBase.check_rules(@__MODULE__)MessagePassingRulesBase.RuleIssue[]julia> MessagePassingRulesBase.check_rule_ambiguities(@__MODULE__)Tuple{MessagePassingRulesBase.RuleSpec, MessagePassingRulesBase.RuleSpec}[]
MessagePassingRulesBase.check_rules — Function
check_rules(modules::Module...) -> Vector{RuleIssue}Check every rule defined in modules, by default in every loaded module, against its node's declaration and the dependency declarations, and return the problems as RuleIssues; an empty vector means none. It checks:
- that the rule's node is declared;
- its target: an interface of the node, a group written
(:m, k)and a single interface not, and a cluster's members existing, in interface order, a group's members by index, and not a lone single interface, whose marginal is writtenq[:x]; - each input: an existing interface, a group selected as
[:m...],[k]or[!k]and a single interface not, and a cluster as for the target; - for a message rule whose algorithm has a dependency declaration (its own, or for a
DefaultAlgorithmExtensionthat declares none, the default's) listing its target: that it consumes exactly the declared inputs, or, for a target declared withdefault, at least the added ones. A rule declared withdefaultin itsargsis not compared; - for a message rule whose algorithm has no dependency declaration, that it does not read the message on its own edge,
m[:e]towards:eorm[:g][k]towards(:g, k), which the default scheme never delivers.
A package's tests call it on the package's own module.
MessagePassingRulesBase.RuleIssue — Type
RuleIssueA problem check_rules found with one rule. Fields: rule, the RuleSpec, and message, a sentence saying what is wrong. It shows itself as RuleIssue(file:line: message).
MessagePassingRulesBase.check_rule_ambiguities — Function
check_rule_ambiguities(modules::Module...) -> Vector{Tuple{RuleSpec, RuleSpec}}The pairs of rules defined in modules, by default in every loaded module, that some call could match equally well, so that resolution would throw a MethodError; an empty vector means none. Only rules consuming the same set of inputs can overlap, so candidates are grouped by kind, node, target and input names first. Within a group, each rule's own method of find_message_rule, find_marginal_rule or find_average_energy is compared with Julia's Base.isambiguous.
Julia's check, not a hand-made typeintersect, because the signatures bound their inputs as Messages{N, <:Tuple{…}}: when a slot is disjoint, the intersection is a valid but empty type such as Messages{N, Union{}} or PointMass{Union{}}, never Union{} itself, so an intersection test reports every disjoint pair. Base.isambiguous ignores ambiguities only a Union{} parameter could trigger.
MessagePassingRulesBase.duplicate_rules — Function
duplicate_rules() -> Vector{Vector{Pair{Module, RuleSpec}}}Rules with identical signatures defined in more than one loaded module, one group per signature, each rule with its module. Within one module Julia itself rejects the second definition during precompilation; across modules the later method silently replaces the earlier, which this reports. Ambiguity, overlapping but different signatures, is a separate question, answered by check_rule_ambiguities.
What the registry is for
Each module that defines nodes or rules keeps a Registry of what it defined: its RuleSpecs, NodeSpecs and dependency declarations. The definition macros create it in the module, as the constant __message_passing_registry__, and fill it as the module loads. Each module keeps its own so that a package's precompiled image carries its own entries; registries gathers them from every loaded module.
Resolution does not use the registry. Julia's dispatch finds the rule for a call, through the methods the macros define. Dispatch cannot say which rules exist, though, and that is what the registry is for: everything that lists, counts or checks rules reads it. Take a module that defines a node and two rules:
using MessagePassingRulesBase
module Coins
using MessagePassingRulesBase
struct Coin end # out ~ Bernoulli(p)
@define_factor_node(node = Coin, type = Stochastic, interfaces = [:out, :p])
@define_message_update_rule(node = Coin, target = :out, args = (m[:p]::Real,), body = (args) -> args.m[:p])
@define_message_update_rule(node = Coin, target = :p, args = (m[:out]::Real,), body = (args) -> args.m[:out])
end
length(MessagePassingRulesBase.registered_rules(Coins))2The registry lists the module's two rules. list_rules and rule_coverage read it, and so do check_rules and a test suite's rule-coverage gate:
MessagePassingRulesBase.rule_coverage(Coins.Coin)| DefaultAlgorithm | |
|---|---|
| → out | ✓ |
| → p | ✓ |
A call that no rule takes fails with a RuleNotFoundError, and its near misses, the rules that come closest, come from the registry too:
try
@call_message_update_rule(node = Coins.Coin, target = :out, m = (p = "half",))
catch err
showerror(stdout, err)
endRuleNotFoundError: no message rule for Coin towards :out under DefaultAlgorithm() takes the inputs (m[:p]::String)
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 inspecting.md:141
✓ algorithm DefaultAlgorithm
✗ m[:p]::Real got StringA call that matches never consults it. A rule a registry missed would still run; it just would not be listed, counted, checked or suggested.
MessagePassingRulesBase.Registry — Type
RegistryThe rules, nodes and dependency declarations one module defines, in its fields rules (RuleSpecs), nodes (NodeSpecs) and dependencies (DependenciesSpecs). The definition macros create it in the defining module, as the constant __message_passing_registry__, and fill it at the module's top level, so a package's precompile image carries its own entries. A redefinition, at the REPL say, replaces the entry with the same key.
It is for introspection only: listing, checking and displaying rules, and suggesting candidates when no rule fits. Resolution, finding which rule runs, does not use it: Julia's dispatch finds the rule. See What the registry is for.
See also registries, registered_rules.
MessagePassingRulesBase.registries — Function
registries(modules::Module...) -> Vector{Pair{Module, Registry}}Every Registry in modules and their submodules, each with the module that owns it; with no argument, in Main and every loaded module.
MessagePassingRulesBase.registered_rules — Function
registered_rules(modules::Module...) -> Vector{RuleSpec}Every rule defined in modules and their submodules; with no argument, in every loaded module.
MessagePassingRulesBase.registered_nodes — Function
registered_nodes(modules::Module...) -> Vector{NodeSpec}Every node declared in modules and their submodules; with no argument, in every loaded module.
MessagePassingRulesBase.registered_dependencies — Function
registered_dependencies(modules::Module...) -> Vector{DependenciesSpec}Every DependenciesSpec declared in modules and their submodules; with no argument, in every loaded module.