LenticulumCore

Defines what a factor is. No graphs, no message passing, no concrete factors — just the interface every factor implements and the types it traffics in.

This is the analogue of LuxCore: a small package that other things agree on. If you are writing a new factor, this is the interface you implement.

What is in here

grouptypes
the factor interfaceAbstractLenticulumFactor, channels, supports_polarity, assemble
channels and rolesChannel, Polarity, Observed, Unobserved, Latent
beliefsAbstractBelief, DiracBelief, SampleBelief, TrivialBelief
the forward halfAbstractOpenModel, forward, pushforward, logdensity
the backward halfBayesianLens, invert, and the inversion kinds below
energiesAbstractEnergySpace, GradedEnergy, scalarisations

Writing a factor

A factor must provide LuxCore.initialparameters / initialstates (exactly as a Lux layer does), plus:

LenticulumCore.channels(f)               # the named ports
LenticulumCore.supports_polarity(f, p)   # can it be run this way?
LenticulumCore.supported_polarities(f)   # ...and list the ways
LenticulumCore.assemble(f, p, ps, st)    # build the lens for one polarity
LenticulumCore.energy(f, x, a, y, ps, st)

assemble is the operation with no Lux counterpart. A Lux layer is a forward/backward pair; a factor only becomes one once you have chosen a direction.

The inversion kinds

assemble returns a BayesianLens pairing a model with an inversion, and the inversion kind says how the backward direction is computed:

kindmeansused by
ExactInversionclosed formGaussianFactor, NeuralODEFactor
SolverInversionroot-findingDEQFactor
ProximalInversiona proximal / optimisation stepDiffusionFactor
AmortisedInversiona separate trained networkRatioFactor
TrivialInversionthere is nothing to invertLuxFactor, priors

Nothing requires an inversion to be exact. An approximate one is legal; the free energy is what records the cost.

Known gaps

  • SampleBelief has no belief_logdensity, so two of them cannot be pooled. See Adversarial for a route around this.
  • GaussianBelief lives in Lenticulum, not here, so packages under lib/ cannot produce one and fall back to DiracBelief.

API

Abstract types

LenticulumCore.AbstractBelief — Type
abstract type AbstractBelief

An element of $\mathcal{P}X$: a distribution over a channel's space.

Beliefs are what flows forward along the graph (as priors, propagated by pushforward) and what inversions consume. Concrete subtypes decide the representation: a Dirac, a particle set, an exponential-family natural parameter, a Gaussian.

source
LenticulumCore.AbstractEnergySpace — Type
abstract type AbstractEnergySpace

The space $E_c$ in which a factor's vector energy and entropy take values.

AutoBayes fixes $E_c = [0,\infty]$ and composes energies by addition. Lenticulum keeps the vector and composes by direct sum, recovering the paper's law under scalarisation. See Scalar and Multivariate Energy.md.

source
LenticulumCore.AbstractGradientCoupling — Type
abstract type AbstractGradientCoupling

How an edge of the graph handles the terms AutoBayes' Definition 29 drops.

Definition 29 composes gradients block-diagonally; the true Jacobian has two extra blocks, because the parameter of one factor moves the sampling distribution and the pushforward prior seen by the other. The paper calls the resulting assignment "lax" and says it "can be accounted for mechanistically by an implementation". This type is that mechanism: each edge declares which correction it applies, and the choice is exactly the paper's "different semantics functors".

See DiagonalCoupling, PathwiseCoupling, ScoreFunctionCoupling, ExactCoupling.

source
LenticulumCore.AbstractInversion — Type
abstract type AbstractInversion

The backward half $c'$ of a Bayesian lens (AutoBayes, Definition 9): a rule that turns a prior $\pi \in \mathcal{P}X$ and an observation $y \in Y$ into a belief over $X \times \llbracket c \rrbracket$.

Nothing requires an inversion to be exact. Exact, amortised, mean-field, solver-based and diffusion-based inversions are all legal inhabitants; the quality of the choice is what the free energy measures.

source
LenticulumCore.AbstractLenticulumContainerFactor — Type
abstract type AbstractLenticulumContainerFactor{factors} <: AbstractLenticulumFactor

A factor built from several sub-factors named by the tuple of symbols factors. Mirrors LuxCore.AbstractLuxContainerLayer: parameters and states are assembled into a NamedTuple keyed by factors.

This is the [Para] composition of Definition 28: parameter spaces multiply, and the product is realised as a NamedTuple.

source
LenticulumCore.AbstractLenticulumFactor — Type
abstract type AbstractLenticulumFactor

A factor: a parameterized statistical game (AutoBayes, Definition 27).

This is Lenticulum's analogue of LuxCore.AbstractLuxLayer, and the parameter/state interface is inherited verbatim: implementors must provide

  • initialparameters(rng, factor)
  • initialstates(rng, factor)

and additionally, for the statistical-game structure,

  • channels – the named ports of the factor
  • supports_polarity – which input/output splits it can answer
  • assemble – produce a Bayesian lens for a chosen polarity
  • energyspace – the energy space $E_c$
  • energy – the vector energy $\mathbf{l}^c$
  • entropy – the vector entropy $\mathbf{H}^c$

Optionally scalarisation, parameterlength, statelength, display_name.

Unlike a Lux layer, a factor has no fixed direction: assemble(factor, polarity) constructs the parametric lens on demand. See Channels and Polarity.md.

source
LenticulumCore.AbstractLenticulumWrapperFactor — Type
abstract type AbstractLenticulumWrapperFactor{factor} <: AbstractLenticulumFactor

A factor wrapping exactly one sub-factor, whose parameters and states are not nested under an extra name. Mirrors LuxCore.AbstractLuxWrapperLayer.

source
LenticulumCore.AbstractOpenModel — Type
abstract type AbstractOpenModel

An open model $c : X \nrightarrow Y$ (AutoBayes, Definition 1): a measure kernel $X \rightsquigarrow \llbracket c \rrbracket \times Y$ together with its latent space $\llbracket c \rrbracket$.

The latent space is the reason composition of open models needs no integration; see open_model.md.

source
LenticulumCore.AbstractScalarisation — Type
abstract type AbstractScalarisation

A map $\sigma : E \to \mathbb{R}$ collapsing a vector energy to a loss.

Must satisfy σ(0) == 0, non-negativity on the cone, and monotonicity. The critical trait is islinear: a linear σ commutes with the expectation in the chain rule and so composition is strict; a merely convex σ makes it lax, with the gap given by a Jensen term (a variance, for a squared norm).

source
LenticulumCore.DiagonalCoupling — Type
DiagonalCoupling()

Keep only the block-diagonal terms of Definition 29; i.e. stop-gradient through the sampling path and through the pushforward prior. Cheapest, biased.

source
LenticulumCore.ExactCoupling — Type
ExactCoupling()

The conjugate/analytic case: the full Jacobian is available in closed form and no term is dropped. Composition of gradients is then strictly functorial across this edge.

source
LenticulumCore.PathwiseCoupling — Type
PathwiseCoupling()

Differentiate through the sampler (the reparametrisation trick). Requires the inversion to be reparametrisable. Unbiased, low variance.

source
LenticulumCore.ScoreFunctionCoupling — Type
ScoreFunctionCoupling(baseline=nothing)

REINFORCE: recover the dropped term as E[F ∇ log q], optionally with a control variate. Unbiased, high variance, works for discrete inversions.

source

Channels and polarity

LenticulumCore.Channel — Type
Channel{name,S}(space)

A named port of a factor, carrying the space its values live in.

Name shadowing

This type shadows Base.Channel inside LenticulumCore and is deliberately not exported. Refer to it as LenticulumCore.Channel, or import it explicitly.

source
LenticulumCore.Latent — Type
Latent()

AutoBayes' latent space $\llbracket c \rrbracket$; the note's $P_{latent}$. Internal scratch that composition hid. Either revealed (a free retyping) or marginalised (expensive).

source
LenticulumCore.Observed — Type
Observed()

AutoBayes' observed space $Y$; the note's $P_{in}$. Clamped to data. The posterior does not range over this channel.

source
LenticulumCore.Polarity — Type
Polarity(assignment::NamedTuple, [precisions::NamedTuple])
Polarity(; channel = polarity, ...)

An assignment of an AbstractChannelPolarity to each channel of a factor, together with a precision $\rho$ per channel.

Because the assignment is a NamedTuple, the channel names live in the type, so an assembled lens specialises at compile time rather than dispatching through a Dict.

Polarity(; x = Observed(), y = Unobserved())
source
LenticulumCore.Unobserved — Type
Unobserved()

AutoBayes' unobserved space $X$; the note's $P_{out}$. This is what inference solves for, and what the inversion produces a belief over.

The names cross

"Unobserved" is the output of inference. A factor asked to predict y from x marks y as Unobserved() and x as Observed() — not the other way round.

source
LenticulumCore.assemble — Function
assemble(factor, p::Polarity, ps, st) -> (lens, st)

Produce the AbstractBayesianLens realising factor under polarity p.

This is the operation that has no counterpart in Lux, and the reason a factor cannot be a Lux layer: a Lux layer is a lens, whereas a factor only becomes one once a direction is chosen. See Lux as a Parametric Lens.md.

source
LenticulumCore.channels — Function
channels(factor) -> Tuple{Vararg{Channel}}

The named ports of factor. Must be implemented by every factor.

source
LenticulumCore.default_precision — Method
default_precision(p::AbstractChannelPolarity)

The default weight $\rho$ for a channel of this polarity, as in $P = \rho_{in}P_{in} + \rho_{out}P_{out} + \rho_{latent}P_{latent}$.

Observed defaults to Inf (a hard clamp — a categorical cup); the others to 1. A finite Observed precision is a soft clamp, which is what makes noisy observations and annealed conditioning expressible.

source
LenticulumCore.ispartition — Method
ispartition(p::Polarity)

Check $P_{in} + P_{out} + P_{latent} = \mathrm{Id}$ with pairwise-zero products: every channel gets exactly one polarity. Guaranteed by the NamedTuple representation, so this is a total function returning true; kept as a named predicate because the corresponding check on a graph-wide polarity is not trivial.

source
LenticulumCore.isunidirectional — Method
isunidirectional(factor) -> Bool

Whether factor admits exactly one polarity, i.e. has a fixed input/output split and is therefore an explicit rather than an implicit factor.

Non-learnable structural nodes are usually unidirectional: a data source only emits, a loss only absorbs.

source
LenticulumCore.select — Method
select(p::Polarity, ::Type{P}) where {P<:AbstractChannelPolarity}

The tuple of channel names carrying polarity P. This is the diagonal selection matrix $P_{in}$ / $P_{out}$ / $P_{latent}$ as a tuple of Symbols.

source
LenticulumCore.supported_polarities — Method
supported_polarities(factor) -> Tuple{Vararg{Polarity}}

The polarities factor can answer, enumerated.

supports_polarity is the predicate form; this is the listable form, which a scheduler needs in order to plan messages without guessing. The default is empty, matching supports_polarity's conservative false.

A factor with exactly one supported polarity is unidirectional: it can be evaluated in one direction only, like an ordinary Lux layer. See isunidirectional.

source
LenticulumCore.supports_polarity — Method
supports_polarity(factor, p::Polarity) -> Bool

Whether factor can answer the input/output split p.

This predicate replaces Lux's acyclicity restriction. A Lux Chain is well-formed iff the wiring is a DAG; a Lenticulum graph is well-formed iff every scheduled message uses a polarity its factor supports. The default is conservative: a factor supports nothing until it says otherwise.

source

Beliefs and open models

LenticulumCore.DiracBelief — Type
DiracBelief(value)

A point mass. What a clamped (observed) channel carries, and what a categorical cup produces (see Copiers Cups and Caps.md). Inversions against a Dirac prior typically trivialise — which is exactly AutoBayes' Example 4: supervised learning is the case where the cup has collapsed the posterior.

source
LenticulumCore.OpenModelResult — Type
OpenModelResult(observed, latent)

The result of running an open model forward: a value in $Y$ together with the value in $\llbracket c \rrbracket$ that composition would otherwise have destroyed.

This is the exact analogue of an autodiff tape entry. Reverse-mode AD caches activations so the backward pass can use them; an open model caches $\llbracket c \rrbracket$ so the inversion can. Same trade of memory for tractability, under the same chain rule.

source
LenticulumCore.SampleBelief — Type
SampleBelief(samples, [weights])

A particle representation of $\pi \in \mathcal{P}X$. The default fallback whenever no conjugate structure is available, which is most of the time.

source
LenticulumCore.TrivialBelief — Type
TrivialBelief()

The unique belief on the one-point space $1$. The prior argument of a factor with $X \cong 1$ — a prior distribution, in AutoBayes' terminology, has this as its input.

source
LenticulumCore.forward — Function
forward(model, x, ps, st) -> (OpenModelResult, st)

Sample the kernel $c : X \rightsquigarrow \llbracket c \rrbracket \times Y$ at x.

source
LenticulumCore.isexact — Method
isexact(x) -> Bool

Whether a pushforward or an inversion is exact rather than approximate. Defaults to false because approximation is the normal case and silence should not imply exactness.

source
LenticulumCore.ispure — Method
ispure(model) -> Bool

$\llbracket c \rrbracket \cong 1$: an ordinary kernel $X \rightsquigarrow Y$.

Purity is not preserved by composition — that is precisely why the latent space exists. Composing two pure models yields a model whose latent space is the intermediate space.

source
LenticulumCore.latentspace — Function
latentspace(model)
observedspace(model)
unobservedspace(model)

The three spaces $\llbracket c \rrbracket$, $Y$, $X$ of AutoBayes Definition 1.

source
LenticulumCore.logdensity — Function
logdensity(model, x, a, y, ps, st) -> (Real, st)

$\log p_c(a, y \mid x)$. Needed whenever the energy is a negative log-likelihood, which is the default choice in every example of AutoBayes' Appendix A.

source
LenticulumCore.pushforward — Function
pushforward(model, π::AbstractBelief, ps, st) -> (AbstractBelief, st)

The pushforward prior $c_*\pi$.

This is one of the two expensive operations

Computing $c_*\pi$ is marginalisation, and is about as costly as exact inversion. AutoBayes' closing discussion names belief propagation and variational message passing as the remedy, and states that they fit in the framework — that is Mycelium.jl's job. A LenticulumCore implementation may legitimately return an approximate belief, but it should say so via isexact.

source

Bayesian lenses

LenticulumCore.AmortisedInversion — Type
AmortisedInversion(net)

$c'$ realised by a learned network — a VAE encoder. net is an AbstractLuxLayer, so the inversion carries its own parameters, independent of the forward kernel's.

That a factor has two independently parametrised halves is the structural reason a factor cannot be a Lux layer.

source
LenticulumCore.BayesianLens — Type
BayesianLens(model, inversion)

The pair $(c, c')$ of AutoBayes Definition 9.

Nothing constrains inversion to be exact. Exact posteriors, amortised encoders, mean-field families, root-finding solvers and diffusion denoisers are all legal, and their relative quality is precisely what the free energy measures.

source
LenticulumCore.ComposedLens — Type
ComposedLens(first, second)

$(d, d') \diamond (c, c')$ of AutoBayes Definition 12. Priors propagate forward by pushforward; corrections propagate backward by sampling the inversions in reverse order.

source
LenticulumCore.ExactInversion — Type
ExactInversion()

$c^\dagger$ of AutoBayes Definition 10: Bayes' law applied to the model's own kernel. Available only when the model declares a tractable posterior.

Almost-sure caveat

Footnote 3 of the paper: inversions may not be fully supported and are defined only up to almost-sure equality, so $(-)^\dagger$ is only a.s. a pseudofunctor. Numerically this means guarding against conditioning on a null set.

source
LenticulumCore.ProximalInversion — Type
ProximalInversion(prox)

$c'$ realised by a proximal operator on an energy — the diffusion family (RED-Diff, ProxDM). See lib/VariationalDiffusion.jl and ImplicitREDDiff.md.

source
LenticulumCore.SolverInversion — Type
SolverInversion(solver)

$c'$ realised by root-finding on a residual: Implicit Learners.md's algebraic and equilibrium families. The backward pass is the implicit function theorem, which needs the Jacobian of the vector energy — see Scalar and Multivariate Energy.md §6.

A solver that stopped early is simply an inexact inversion, and the loss records the cost. That is a far better failure mode than a divergent unroll.

source
LenticulumCore.TensorLens — Type
TensorLens(parts::Tuple)

$(c,c') \otimes (d,d')$ of AutoBayes Definition 15.

Lossy — this is Remark 16

Parallel composition of inversions is lax: each branch only ever sees the marginal of a joint prior, so correlations between branches are discarded and the composite inversion is mean-field. The discrepancy is the mutual information between the branches (Remark 26). Track and report it; do not pretend it is zero.

source
LenticulumCore.TrivialInversion — Type
TrivialInversion()

The inversion of a prior $\pi : 1 \nrightarrow X$: there is nothing to infer.

AutoBayes Remark 24 uses exactly this to turn a prior into a statistical game with $l^\pi = -\log p_\pi$ and $H^\pi \equiv 0$, whose composite with c has the true variational free energy as its loss. The prior is a factor, not part of the model.

source
LenticulumCore.compose — Method
compose(c::AbstractBayesianLens, d::AbstractBayesianLens)

Build $d \diamond c$. Argument order follows the wiring (c then d), not the mathematical notation $d \diamond c$.

source
LenticulumCore.invert — Function
invert(lens, π::AbstractBelief, y, ps, st) -> (AbstractBelief, st)

Apply $c'_\pi$ to the observation y, returning a belief over $X \times \llbracket c \rrbracket$.

Note the return type reconstructs the latent space too, not just $X$. That is what the next factor upstream will consume, and dropping it breaks the chain rule.

source

Energies and scalarisations

LenticulumCore.EuclideanEnergySpace — Type
EuclideanEnergySpace(n)

$E = \mathbb{R}^n$. The space a vector residual $r_\theta$ lands in; n must match the number of unobserved coordinates for the implicit function theorem to apply.

source
LenticulumCore.GradedEnergy — Type
GradedEnergy(parts::NamedTuple)

An element of a GradedEnergySpace: the per-factor breakdown of a composite energy, before it is collapsed to a number.

Per-factor loss attribution is not a logging feature bolted on afterwards — it is what the composite energy is.

source
LenticulumCore.GradedEnergySpace — Type
GradedEnergySpace(parts::NamedTuple)

The direct sum $E_G = \bigoplus_{f} E_f$ graded by factor name.

This is what a composite factor's energy space is: composition does not add, it files the summands away under their factors' names — exactly as an open model files intermediate values into $\llbracket c \rrbracket$ rather than integrating them out.

source
LenticulumCore.GradedScalarisation — Type
GradedScalarisation(parts::NamedTuple)

$\sigma_{dc}(e_c, e_d) = \sigma_c(e_c) + \sigma_d(e_d)$, the scalarisation induced on a direct sum. Additivity across summands is automatic; linearity is inherited from the parts.

source
LenticulumCore.SquaredNorm — Type
SquaredNorm(M = nothing)

$\sigma(e) = \tfrac12 \|e\|_M^2$. Convex, not linear.

The natural scalarisation for a residual factor: $r_\theta(x) \approx 0$ iff $\sigma(r_\theta(x)) \approx 0$. Its differential is $\mathrm{d}\sigma(e) = Me$, so the scalar gradient is $J^\top M r$ and the Gauss–Newton metric is $J^\top M J$ — both of which need the Jacobian of the vector energy, which is exactly what is lost by scalarising early.

source
LenticulumCore.WeightedSum — Type
WeightedSum(λ)

$\sigma_\lambda(e) = \langle \lambda, e\rangle$ for λ in the dual cone. Linear.

This is where β-VAE weights, KL annealing schedules, curriculum weights and the precisions $\rho$ of ImplicitREDDiff.md live. Because the energy is kept as a vector, λ can be changed after the graph is composed — with a scalar energy each λ is a different graph.

source
LenticulumCore.energy — Function
energy(factor, x, a, y, ps, st) -> (𝐥, st)

The vector energy $\mathbf{l}^c(x, a, y)$, an element of the cone of energyspace(factor).

Pointwise: it is evaluated at samples drawn from the inversion, so it needs no normalisation. That is why energies compose by direct sum with no expectation involved.

source
LenticulumCore.energyspace — Method
energyspace(factor) -> AbstractEnergySpace

The space $E_c$ of factor's vector energy. Defaults to ScalarEnergySpace().

source
LenticulumCore.entropy — Function
entropy(factor, π, y, ps, st) -> (𝐇, st)

The vector entropy $\mathbf{H}^c(\pi, y)$, an element of the same space.

Note the argument type: unlike energy, this eats a distribution π, not a sample. That type difference is exactly why entropies chain (averaged under the downstream inversion, evaluated at the pushforward prior) while energies merely add.

Usually the entropy is scalar but must be told which coordinate of $E_c$ it regularises; returning h * u for a fixed direction u is the idiomatic way to say so.

source
LenticulumCore.islinear — Function
islinear(σ::AbstractScalarisation) -> Bool

Whether $\sigma$ commutes with expectation.

This trait is load-bearing, not decorative. With a linear σ the composite scalar loss may be accumulated eagerly factor by factor, because $\sigma(\mathbb{E}[\cdot]) = \mathbb{E}[\sigma(\cdot)]$. With a convex σ it must be deferred until the vector loss is assembled, otherwise you silently compute $\mathbb{E}[\sigma(\mathbf{F})]$ when you asked for $\sigma(\mathbb{E}[\mathbf{F}])$. The two differ by jensen_gap.

source
LenticulumCore.jensen_gap — Method
jensen_gap(σ, samples) -> Real

$\mathbb{E}[\sigma(e)] - \sigma(\mathbb{E}[e])$ over a collection of energy samples: the exact discrepancy between the scalar chain rule (AutoBayes Theorem 23) and the multivariate one.

Zero for a linear σ; for SquaredNorm it equals $\tfrac12 \operatorname{tr} \operatorname{Cov}(e)$, the posterior variance of the upstream factor's loss. Report it — it is the same kind of object as the mutual information that measures the laxness of the tensor in AutoBayes' Remark 26.

source
LenticulumCore.oplus — Method
⊕(a::AbstractEnergySpace, b::AbstractEnergySpace)
oplus(a, b)

Direct sum of energy spaces. Named summands are merged; unnamed ones are wrapped under :left / :right.

source
LenticulumCore.scalar_energy — Method
scalar_energy(factor, x, a, y, ps, st) -> (Real, st)

AutoBayes' $l^c$: the scalarisation of energy. Provided so that a factor written against the paper's interface needs no vector machinery.

source
LenticulumCore.scalarisation — Method
scalarisation(factor) -> AbstractScalarisation

The $\sigma_c$ used to turn factor's vector loss into a number. Defaults to the identity, which makes the factor behave exactly as in AutoBayes.

source

Statistical games

LenticulumCore.ComposedFactor — Type
ComposedFactor(first, second; coupling = DiagonalCoupling())

$d \diamond c$ of AutoBayes Definitions 22 and 28.

The composite's parameter space is the product $\Phi \times \Theta$, realised as the NamedTuple (first = ..., second = ...) — literally the same data structure Lux uses for Chain.

coupling records which of the gradient terms dropped by Definition 29 this edge restores.

source
LenticulumCore.TensorFactor — Type
TensorFactor(parts::NamedTuple)

$c \otimes d$ of AutoBayes Definition 25. Both energies and entropies add across summands, because in parallel there is no upstream/downstream.

Subject to the mean-field laxness of TensorLens.

source
LenticulumCore.chain_rule_defect — Method
chain_rule_defect(σ, 𝐅c_samples)

How far AutoBayes' Theorem 23 and the multivariate chain rule disagree across one edge:

\[F^{dc} - \sigma_{dc}(\mathbf{F}^{dc}) = \mathbb{E}[\sigma_c(\mathbf{F}^c)] - \sigma_c(\mathbb{E}[\mathbf{F}^c]) \ \ge 0\]

Zero for linear σ. For SquaredNorm it is $\tfrac12\operatorname{tr}\operatorname{Cov}(\mathbf{F}^c)$: how much the downstream posterior disagrees with itself about what the upstream factor should be doing.

source
LenticulumCore.compose_energy — Method
compose_energy(𝐥c, 𝐥d)

The composite vector energy of AutoBayes Definition 22, adapted:

\[\mathbf{l}^{dc}(x,a,y,b,z) = (\mathbf{l}^c(x,a,y),\ \mathbf{l}^d(y,b,z))\]

The paper writes + here; we write a pair. Applying a GradedScalarisation to the pair recovers the paper's + exactly, and does so strictly — the direct sum is where no information is lost. Laxness enters only through compose_entropy's expectation.

source
LenticulumCore.compose_entropy — Method
compose_entropy(𝐇c_samples, 𝐇d)

The composite vector entropy of AutoBayes Definition 22, adapted:

\[\mathbf{H}^{dc}(\pi,z) = \Bigl(\mathbb{E}_{(y,b)\sim d'_{c_*\pi}(z)}[\mathbf{H}^c(\pi,y)],\ \mathbf{H}^d(c_*\pi,z)\Bigr)\]

𝐇c_samples are evaluations of $\mathbf{H}^c(\pi, y)$ at samples (y,b) drawn from the downstream inversion at the pushforward prior. Getting those two adjectives right is the whole content of the definition, and the commonest place to go wrong.

source
LenticulumCore.compose_free_energy — Method
compose_free_energy(𝐅c_samples, 𝐅d)

The multivariate chain rule:

\[\mathbf{F}^{dc}(\pi,z) = \Bigl(\mathbb{E}_{(y,b)\sim d'_{c_*\pi}(z)}[\mathbf{F}^c(\pi,y)],\ \mathbf{F}^d(c_*\pi,z)\Bigr)\]

Compare AutoBayes Theorem 23, which is this composed with a scalarisation. The two agree strictly iff σ is linear; otherwise they differ by jensen_gap, and the multivariate one is the smaller (Jensen).

source
LenticulumCore.free_energy — Function
free_energy(factor, π::AbstractBelief, y, ps, st) -> (𝐅, st)

The vector loss

\[\mathbf{F}^c(\pi, y) = \mathbb{E}_{(x,a)\sim c'_\pi(y)}[\mathbf{l}^c(x,a,y)] - \mathbf{H}^c(\pi, y)\]

an element of energyspace(factor) — not a number. Collapse it with scalar_free_energy when you want a loss.

source
LenticulumCore.isfrozen — Method
isfrozen(factor) -> Bool

Whether factor's parameters exist but are pinned for this run.

The distinction from islearnable matters for the backward pass: a frozen factor still propagates cotangents through to its inputs (so upstream factors learn), whereas a factor that is simply not learnable has no parameter wire at all. Confusing the two silently detaches a subgraph.

source
LenticulumCore.islearnable — Method
islearnable(factor) -> Bool

Whether factor contributes trainable parameters.

Defaults to LuxCore.parameterlength(factor) > 0, but is a trait, not a computation: a factor with parameters that are deliberately held fixed (a pretrained encoder, a physical constant, a data clamp) should override it to false. A graph uses this to decide which nodes need an optimiser attached and which parameter cotangents may be discarded unread.

See also isfrozen.

source
LenticulumCore.setup — Method
setup(rng, factor) -> (ps, st)

As LuxCore.setup. Provided so that factors and Lux layers are set up identically.

source

Everything else

LenticulumCore.LenticulumCore — Module
LenticulumCore

Core abstractions for Lenticulum.jl: factors as parameterized statistical games.

LuxCore.jl is to Lux.jl as LenticulumCore.jl is to Lenticulum.jl, and the parameter/state interface is deliberately identical. The difference is what a node is:

Lux.jlLenticulum.jl
nodeparametric lens (Cruttwell et al., Def. 2.5)parameterized statistical game (AutoBayes, Def. 27)
categoryPara(Lens(C))Para(StatGame)
directionfixed at constructionchosen per call, via a Polarity
wiringdirected acyclic graphany weakly-connected digraph
backward passreverse derivativeBayesian inversion + free-energy accumulation

A factor carries four pieces (AutoBayes Definition 20): a forward kernel c, an inversion c', an energy and an entropy. Lenticulum keeps the energy and entropy vector-valued, with a separate scalarisation, so that Jacobians — and hence the Gauss–Newton and Fisher metrics, and the implicit function theorem — survive composition. See Scalar and Multivariate Energy.md.

Concept notes live in vault/; per-file implementation notes sit next to each source file.

source