LuxFactor: anyAbstractLuxLayeras a factor. One polarity. This is the wrapper that works today on everything in the SciML ecosystem, and the demonstration of why that is not enough.
Sources: code:
luxfactor.jlTheory (CT-ML wiki): Bayesian Inversion · Parametric Lens · Variational Free Energy · Lens
1. It really does wrap anything
DeepEquilibriumNetwork, NeuralODE, NeuralDSDE, Chain, a Boltz backbone — all are
AbstractLuxLayers, so all of them satisfy the only interface this wrapper uses. No SciML
dependency is required to wrap a SciML model, because LuxCore is the contract they all
already meet.
postprocess handles the one wrinkle: some of those layers return a solution object rather
than an array.
LuxFactor(NeuralODE(net, (0.0,1.0), Tsit5()), :x => :y;
postprocess = sol -> Array(sol)[:, end])
LuxFactor(DeepEquilibriumNetwork(cell, NewtonRaphson()), :x => :z)2. And it gets you one polarity
isunidirectional(::LuxFactor) == true, always. Lux as a Parametric Lens says why:
A Lux layer is a lens, whereas a factor only becomes one once a direction is chosen.
A layer has already chosen. So assemble returns a lens with TrivialInversion — the same
inversion lens.md gives a prior, on the same grounds that there is nothing to infer — and
factor_message on the input channel throws with a message naming DEQFactor and
NeuralODEFactor as the alternatives.
What wrapping does buy: graph membership, parameter and state management through the
LuxCore interface, participation in the free-energy accounting, and a channel-named place
in a FactorGraph. That is not nothing. It is just not bidirectionality.
The test suite makes the comparison on one object — the same LinearCell wrapped both ways:
length(supported_polarities(LuxFactor(CELL, :in => :out))) == 1
length(supported_polarities(DEQFactor(CELL, (x = 2, z = 2)))) == 2Same network, same parameter count, twice the directions. That pair of numbers is the shortest statement of what this package is for.
3. Implementation difficulties
3.1 The energy is zero, which is a placeholder rather than a fact
A function has no residual of its own — there is nothing to be “approximately satisfied”. So
energy returns 0.0 and local_free_energy likewise, on the reasoning that a LuxFactor’s
contribution to a graph’s loss comes from a downstream LossFactor
(Everything is a Factor).
That is defensible and it is also how a factor silently contributes nothing to a free energy
that is supposed to total . A LuxFactor in a graph makes the Bethe sum wrong in
a way no assertion catches.
3.2 dims are optional and unchecked
LenticulumCore.Channel accepts nothing for its space, so LuxFactor defaults both
dimensions to nothing and never validates the shapes it passes through. validate(g) will
therefore accept a graph wiring a 4-vector into a layer expecting 10, and the error surfaces
inside the user’s model.
3.3 postprocess runs on the output only
There is no preprocess. A layer wanting a NamedTuple or a tuple input has to be adapted by
the caller before it reaches the factor. Asymmetric, and only because the output case (SciML
solution objects) is the one that actually comes up.
Related: deq, neuralode, DEQ as a Relation, Lux as a Parametric Lens, The Equilibrium Family