model

Where ImplicitREDDiff’s selection matrices stop being notation. The factor owns one space , its channels are blocks of that space, and a Polarity supplies and their precisions.

Implemented in lib/VariationalDiffusion.jl/src/factor.jl; see factor.

Sources: code: gaussian.jl, lens.jl

Theory (CT-ML wiki): Bayesian Inversion · Statistical Game · Bayesian Lens · Open Model · Variational Free Energy · Lens

1. The polarity is the selection matrices

The prompt’s sketch asks for diagonal with

and then . Every piece of that already existed in LenticulumCore, which is the point of Channels and Polarity — the note that reconciled the two notations before there was anything to reconcile them for:

the sketchLenticulumCore
channels marked Observed()
channels marked Unobserved()
channels marked Latent()
the partition identityispartition — free, from the NamedTuple representation
channel_precision(p, name)
(a hard clamp)default_precision(Observed()) == Inf

So precision_vector(f, p) is a reading of the polarity and the factor stores no configuration for it. Note which way the default falls: the hard clamp is the default; the soft clamp is what you opt into. An observation is evidence unless you say otherwise — and is the categorical cup of Open Models and Latent Channels, taken literally by projection rather than numerically by a large penalty.

2. One network, one state space, many directions

The NoisePredictor is over the whole , not one per channel. That is what makes this a relation rather than a bundle of conditionals: the model knows the joint distribution, so any subset of blocks can be inpainted from any other.

Concretely this is why supports_polarity accepts any assignment with at least one unobserved channel, while supported_polarities enumerates only the “one unobserved, rest observed” cases. The predicate is the truth; the enumeration is the listable subset a scheduler can plan with. channels.md says the two exist so they can differ; this is the first factor where they do, and the full set has elements.

Read against README’s table, this factor scores the “symmetry handling: no distinguished input/output” row about as well as anything in the repository — ModelingToolkit as an Acausal Relation’s LinearConstraintFactor gets it by having no direction, this one by having a prior over everything at once.

3. The inversion is a proximal operator, which the core already knew

assemble(f, polarity, ps, st) -> BayesianLens(DiffusionModel(f, polarity),
                                              ProximalInversion(f.prox))

ProximalInversion was already in lens.jl, with a docstring naming RED-Diff, ProxDM and this package, written before any of it existed. That the implementation slotted in without touching the core is the strongest evidence so far that Inversions and Bayesian Lenses’s “nothing constrains the inversion to be exact” was a real abstraction and not a hedge.

The model is not pure: latentspace returns the Latent() channels, which the prox reconstructs and nobody reads. That is exactly AutoBayes’ — internal coordinates composition hid — and it is the first factor in the project with a non-trivial one.

4. What the graph gets, and it is a Dirac

invert returns a DiracBelief, faithfully to RED-Diff’s variational family. On a single inverse problem that is unobjectionable. In a factor graph it has three consequences, and all three are real problems rather than infelicities.

4.1 The message is a posterior, not a likelihood

Every other factor returns a likelihood from factor_message, with the prior divided out, because a variable of degree would otherwise count the prior times — Messages are Inversions is built on the distinction, and gaussian.jl’s invert docstring is explicit that combine(π, message) == posterior.

A diffusion factor cannot divide its prior out. The prior is a neural network; there is no subtraction available in any representation. So:

Correct at degree 1 — one diffusion prior, one measurement, the usual inverse problem. Approximate at degree > 1, and nothing detects it.

4.2 A Dirac dominates every combine it meets

Mycelium.combine gives a DiracBelief absolute precedence (it is the limit, per Channels and Polarity). So a diffusion factor does not negotiate with its neighbours, it overrides them; and two diffusion factors disagreeing about one variable raise “contradictory hard clamps” rather than averaging.

4.3 The entropy slot holds something that is not an entropy

The Bethe free energy of Bethe Free Energy wants with a counting correction per variable. Here is a Dirac and , so the score-matching term stands in for it — structurally correct (it depends on the learned distribution) and numerically not an entropy.

Consequence: mixing a DiffusionFactor with a GaussianFactor in one graph gives a total free energy that is not for any model. The identity that The Linear Gaussian Chain §4 verifies to machine precision simply does not hold once this factor is in the graph.

5. The fix, and it is in the paper

Keep . RED-Diff’s §3 derives the general Gaussian variational family and the experiments drop it; dropping it is what produces every problem in §4. A GaussianBelief with finite precision would:

  • make the message poolable rather than dominant,
  • give a real entropy for the Bethe sum,
  • and let the prior be (approximately) divided out in canonical form, restoring the likelihood/posterior distinction.

It needs a variance update in the prox’s inner loop — a second optimisation, over — and it is the single most valuable missing piece in this family.

6. Other gaps, briefly

Two of these are closed for prox = ImplicitProx(nodes) (2026-10-02)

With the deterministic implicit solver as the inversion (implicit_factor), the free energy is deterministic (the score summand is the fixed-node quadrature), and the solver reports converged and stable through implicit_solution. It also has a backward pass, implicit_factor_pullback. The RED-Diff inversion keeps the gaps below.

  • local_free_energy re-runs the prox, because the message store keeps beliefs rather than the internal state the prox converged to. Caching on st is the obvious fix.
  • The free energy is stochastic. Calling it twice gives different numbers. Every other factor’s is a closed form, and anything in Mycelium that compares free energies across iterations will see noise.
  • No convergence report. reddiff_solve runs exactly steps iterations and returns; there is no residual to measure, so there is nothing to report. Against Mycelium’s conventions, where every inversion says whether it converged.
  • energy’s signature does not fit. The method here reads the a argument — AutoBayes’ latent slot — as the polarity, because depends on it and there is nowhere else to put it. ModelingToolkit as an Acausal Relation hit the same wall from the acausal side; two independent factor families now means it is an interface bug in LenticulumCore, not a quirk of either.

Related: The Diffusion Family, RED-Diff as a Statistical Game, The VP-SDE, Channels and Polarity, Open Models and Latent Channels, Messages are Inversions, Bethe Free Energy, factor, ImplicitREDDiff