Skip to content

Commit afbf37f

Browse files
DanielBoigkclaude
andcommitted
Products of learned relations: intersections via ProductRelation
AbstractImplicitRelation as supertype for the query functions; the pullback's parameter loop moves into _params_cotangent. ProductRelation sums weighted fields (a product of experts), so its stable roots are the intersections of the relations; infer, roots, Laplace, pullback and energy work unchanged. Tests: circle∩line, circle∩ellipse, adjoint vs FD. Notes: product.md, vault "Composing Diffusion Factors", open problems T9/I8, Map of Content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQTuuzmdaTgshFtQCW3Bb9
1 parent bf11f26 commit afbf37f

11 files changed

Lines changed: 339 additions & 22 deletions

‎lib/VariationalDiffusion.jl/src/VariationalDiffusion.jl‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ include("kernel.jl")
5757
include("implicit.jl")
5858
include("energy_network.jl")
5959
include("answers.jl")
60+
include("product.jl")
6061
include("factor.jl")
6162
include("implicit_factor.jl")
6263
include("proxdm.jl")
@@ -88,6 +89,7 @@ export ImplicitProx, implicit_solution, implicit_factor_pullback
8889
# --- Proximal diffusion models (ProxDM) ----------------------------------------
8990
export EnergyNetwork, energy, implicit_energy, denoising_gradient
9091
export implicit_roots, implicit_laplace, density_lambda
92+
export AbstractImplicitRelation, ProductRelation
9193
export AbstractProximalPredictor, proximal, ProxNetwork, MixtureProx
9294
export proxdm_sample, prox_infer, proximal_matching_loss
9395

‎lib/VariationalDiffusion.jl/src/VariationalDiffusion.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
>
88
> Theory (CT-ML wiki): [Bayesian Inversion](https://mathstruct.org/CategoryTheory-ML-Wiki/Bayesian-Inversion) · [Statistical Game](https://mathstruct.org/CategoryTheory-ML-Wiki/Statistical-Game) · [Lens](https://mathstruct.org/CategoryTheory-ML-Wiki/Lens)
99
10-
## The chain, in ten files
10+
## The chain, in eleven files
1111

1212
| file | note | supplies |
1313
|---|---|---|
@@ -19,6 +19,7 @@
1919
| `kernel.jl` | [[kernel]] | kernel density estimates as relations: batch KDE with bandwidth selection, an online forgetting KDE |
2020
| `implicit.jl` | [[implicit]] | deterministic implicit inference and its adjoint backward pass |
2121
| `energy_network.jl` | [[energy_network]] | energy-parametrised predictors: $\varepsilon = \sigma_t\nabla_x E_\theta$, a conservative score, an energy for the relation |
22+
| `product.jl` | [[product]] | products of relations on the same variables: intersections, with the adjoint into every factor |
2223
| `implicit_factor.jl` | [[implicit_factor]] | `DiffusionFactor` with `ImplicitProx`: report and per-channel pullback |
2324
| `proxdm.jl` | [[proxdm]] | proximal diffusion models: prox interface, exact oracle, sampler, proximal inference |
2425
| `ext/` | [[backends]] | the network's derivatives through any AD backend (DifferentiationInterface, Reactant) |

‎lib/VariationalDiffusion.jl/src/answers.jl‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ predictor is an [`EnergyNetwork`](@ref) (lowest first, i.e. the most plausible a
2727
and otherwise in the order found. There is no guarantee that every stable answer is found;
2828
more starts and a larger `spread` find more.
2929
"""
30-
function implicit_roots(m::ImplicitDiffusion, z₀, ρ, ps, st; nstarts::Integer = 16, spread = 1.0,
30+
function implicit_roots(m::AbstractImplicitRelation, z₀, ρ, ps, st; nstarts::Integer = 16, spread = 1.0,
3131
starts = nothing, rng::AbstractRNG = Random.Xoshiro(0), unique_tol = 1e-3, kwargs...)
3232
free = .!isinf.(ρ)
3333
inits = if starts === nothing
@@ -44,13 +44,16 @@ function implicit_roots(m::ImplicitDiffusion, z₀, ρ, ps, st; nstarts::Integer
4444
push!(roots, sol)
4545
end
4646
roots = [r for r in roots] # concrete element type
47-
if m.predictor isa NoisePredictor{<:EnergyNetwork} && length(roots) > 1
47+
if _has_energy(m) && length(roots) > 1
4848
energies = [_query_energy(m, r.z, z₀, ρ, ps, st) for r in roots]
4949
roots = roots[sortperm(energies)]
5050
end
5151
return (roots, st)
5252
end
5353

54+
_has_energy(m) = false
55+
_has_energy(::ImplicitDiffusion{<:NoisePredictor{<:EnergyNetwork}}) = true
56+
5457
function _query_energy(m, z, z₀, ρ, ps, st)
5558
U, _ = implicit_energy(m, z, ps, st)
5659
soft = .!isinf.(ρ)
@@ -93,7 +96,7 @@ covariance is then calibrated to the density *smoothed at the field's noise leve
9396
the data distribution by those levels). With another λ it is a relative measure of how sharply
9497
the answer is determined. Throws unless `sol` converged to a stable answer.
9598
"""
96-
function implicit_laplace(m::ImplicitDiffusion, sol::ImplicitSolution, ρ, ps, st)
99+
function implicit_laplace(m::AbstractImplicitRelation, sol::ImplicitSolution, ρ, ps, st)
97100
(sol.converged && sol.stable) ||
98101
throw(ArgumentError("the Laplace approximation needs a converged, stable answer (a local minimum)"))
99102
free = .!isinf.(ρ)

‎lib/VariationalDiffusion.jl/src/implicit.jl‎

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,24 @@ is the denoiser (`Deterministic Relaxation.md`).
7474
"""
7575
noisefree_nodes(n::Integer, t::Real) = FieldNodes([float(t)], zeros(n, 1), [1.0])
7676

77+
"""
78+
AbstractImplicitRelation
79+
80+
A learned relation that can be queried by root-finding: anything with a field
81+
[`prior_field`](@ref) and its Jacobian [`prior_jacobian`](@ref). [`ImplicitDiffusion`](@ref) is one
82+
diffusion model; [`ProductRelation`](@ref) pools several. The query functions
83+
([`implicit_infer`](@ref), [`implicit_roots`](@ref), [`implicit_laplace`](@ref),
84+
[`implicit_pullback`](@ref)) accept any of them.
85+
"""
86+
abstract type AbstractImplicitRelation end
87+
7788
"""
7889
ImplicitDiffusion(predictor, nodes; λ = 1.0)
7990
8091
The relation ``R_\\theta = \\{z : r(z) = 0\\}`` defined by a noise predictor and a fixed node
8192
set, with RED-Diff's weighting ``\\lambda_t = \\lambda\\sigma_t/\\alpha_t``.
8293
"""
83-
struct ImplicitDiffusion{P<:NoisePredictor,T<:Real}
94+
struct ImplicitDiffusion{P<:NoisePredictor,T<:Real} <: AbstractImplicitRelation
8495
predictor::P
8596
nodes::FieldNodes{T}
8697
λ::T
@@ -130,7 +141,7 @@ end
130141
``r = g_\\theta(z) + \\rho^2\\odot(z - z_0)`` on the free coordinates (finite `ρ`), zero on the
131142
hard-clamped ones (``\\rho_i = \\infty``), where the constraint is ``z_i = z_{0,i}`` instead.
132143
"""
133-
function implicit_residual(m::ImplicitDiffusion, z, z₀, ρ, ps, st)
144+
function implicit_residual(m::AbstractImplicitRelation, z, z₀, ρ, ps, st)
134145
g, st = prior_field(m, z, ps, st)
135146
free = .!isinf.(ρ)
136147
r = zero(g)
@@ -177,7 +188,7 @@ is only trusted in locally convex regions so that it is not drawn to saddles and
177188
`z₀` holds the clamp values on hard coordinates and the soft targets elsewhere; `ρ` is the
178189
precision vector, `Inf` for a hard clamp (cf. [`precision_vector`](@ref)).
179190
"""
180-
function implicit_infer(m::ImplicitDiffusion, z₀, ρ, ps, st;
191+
function implicit_infer(m::AbstractImplicitRelation, z₀, ρ, ps, st;
181192
z_init = z₀, tol = 1e-9, maxiters::Integer = 200, step = 0.05)
182193
free = .!isinf.(ρ)
183194
z = float.(copy(z_init))
@@ -244,7 +255,7 @@ the Jacobians are evaluated at ``z^\\star`` afresh. Throws if `sol` did not conv
244255
implicit function theorem says nothing about a point that is not a root
245256
(`Backpropagation through Implicit Inference.md` §6).
246257
"""
247-
function implicit_pullback(m::ImplicitDiffusion, sol::ImplicitSolution, z₀, ρ, z̄, ps, st)
258+
function implicit_pullback(m::AbstractImplicitRelation, sol::ImplicitSolution, z₀, ρ, z̄, ps, st)
248259
sol.converged || throw(ArgumentError(
249260
"implicit_pullback at a non-converged state (residual $(sol.residual)): the implicit " *
250261
"function theorem does not apply. Re-solve, or use an unrolled/one-step surrogate."))
@@ -256,12 +267,7 @@ function implicit_pullback(m::ImplicitDiffusion, sol::ImplicitSolution, z₀, ρ
256267
λF = -(Jff' \ z̄[free])
257268
λ = zero(float.(z)); λ[free] .= λF
258269

259-
ps̄ = _zero(ps)
260-
for k in (_hasparams(ps) ? eachindex(m.nodes.t) : ())
261-
t = m.nodes.t[k]
262-
g = epsilon_vjp_params(m.predictor, _node_input(m, z, k), t, ps, st, λ)
263-
ps̄ = _axpy(m.nodes.w[k] * _weight(m, t), g, ps̄)
264-
end
270+
ps̄ = _params_cotangent(m, z, λ, ps, st)
265271

266272
z̄₀ = zero(float.(z))
267273
z̄₀[free] .= .-(ρ[free] .^ 2) .* λF
@@ -271,6 +277,17 @@ function implicit_pullback(m::ImplicitDiffusion, sol::ImplicitSolution, z₀, ρ
271277
return (z₀ = z̄₀, ρ = ρ̄, ps = ps̄)
272278
end
273279

280+
# (∂_θ g)ᵀ λ: the parameter part of the adjoint, one VJP per node
281+
function _params_cotangent(m::ImplicitDiffusion, z, λ, ps, st)
282+
ps̄ = _zero(ps)
283+
for k in (_hasparams(ps) ? eachindex(m.nodes.t) : ())
284+
t = m.nodes.t[k]
285+
g = epsilon_vjp_params(m.predictor, _node_input(m, z, k), t, ps, st, λ)
286+
ps̄ = _axpy(m.nodes.w[k] * _weight(m, t), g, ps̄)
287+
end
288+
return ps̄
289+
end
290+
274291
# --- As a factor's inversion ----------------------------------------------------
275292

276293
"""
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# ---------------------------------------------------------------------------
2+
# Several learned relations on the same variables, pooled: the product of their densities.
3+
#
4+
# Each implicit learner's field g_i is (for an ideal model) the gradient of a smoothed negative
5+
# log-density, so the weighted sum Σ β_i g_i is the field of the product Π p_i^{β_i}: its stable
6+
# roots are where all relations hold at once, e.g. the intersection points of a learned circle
7+
# and a learned line. Every query function works on the product unchanged.
8+
#
9+
# See `product.md` and `Composing Diffusion Factors` in the vault.
10+
# ---------------------------------------------------------------------------
11+
12+
"""
13+
ProductRelation(relations...; weights = ones)
14+
15+
Several implicit relations on the **same** joint space, pooled: the field is the weighted sum
16+
``\\sum_i \\beta_i\\,g_i`` of the relations' fields, i.e. the field of the product of their densities
17+
``\\prod_i p_i^{\\beta_i}``. Its stable roots are the configurations that satisfy all relations
18+
together, e.g. the intersection points of a learned circle and a learned line. The weights
19+
``\\beta_i`` set how much each relation is trusted (a tempering exponent).
20+
21+
Parameters and states are **tuples**, one entry per relation, in order: `ps = (ps₁, ps₂)`. Every
22+
query function accepts a product: [`implicit_infer`](@ref), [`implicit_roots`](@ref),
23+
[`implicit_laplace`](@ref), and [`implicit_pullback`](@ref) (which returns a tuple of parameter
24+
cotangents). Products may be nested.
25+
26+
Pooling smoothed densities is exact only at zero noise: at the field's noise levels the
27+
product of the smoothed densities is not the smoothed product, so intersections are found
28+
with the same kind of smoothing bias as single relations (`product.md` §3).
29+
"""
30+
struct ProductRelation{M<:Tuple,W<:AbstractVector} <: AbstractImplicitRelation
31+
relations::M
32+
weights::W
33+
end
34+
function ProductRelation(relations::AbstractImplicitRelation...; weights = ones(length(relations)))
35+
isempty(relations) && throw(ArgumentError("a product needs at least one relation"))
36+
length(weights) == length(relations) || throw(ArgumentError("need one weight per relation"))
37+
all(>(0), weights) || throw(ArgumentError("weights must be positive"))
38+
return ProductRelation(relations, float.(collect(weights)))
39+
end
40+
41+
_check_tuple(m::ProductRelation, x, what) = (x isa Tuple && length(x) == length(m.relations)) ||
42+
throw(ArgumentError("a ProductRelation of $(length(m.relations)) relations needs `$what` as a tuple with one entry per relation"))
43+
44+
function prior_field(m::ProductRelation, z, ps, st)
45+
_check_tuple(m, ps, "ps"); _check_tuple(m, st, "st")
46+
g = zero(float.(z))
47+
sts = map(eachindex(m.relations)) do i
48+
gi, sti = prior_field(m.relations[i], z, ps[i], st[i])
49+
g = g .+ m.weights[i] .* gi
50+
sti
51+
end
52+
return (g, Tuple(sts))
53+
end
54+
55+
function prior_jacobian(m::ProductRelation, z, ps, st)
56+
_check_tuple(m, ps, "ps"); _check_tuple(m, st, "st")
57+
return sum(m.weights[i] .* prior_jacobian(m.relations[i], z, ps[i], st[i]) for i in eachindex(m.relations))
58+
end
59+
60+
_params_cotangent(m::ProductRelation, z, λ, ps, st) =
61+
Tuple(_params_cotangent(m.relations[i], z, m.weights[i] .* λ, ps[i], st[i]) for i in eachindex(m.relations))
62+
63+
# a product of energy-parametrised relations has the weighted sum of their energies
64+
_has_energy(m::ProductRelation) = all(_has_energy, m.relations)
65+
66+
function implicit_energy(m::ProductRelation, z, ps, st)
67+
_has_energy(m) || throw(ArgumentError("implicit_energy needs every relation of the product to be energy-parametrised"))
68+
U = zero(float(eltype(z)))
69+
sts = map(eachindex(m.relations)) do i
70+
Ui, sti = implicit_energy(m.relations[i], z, ps[i], st[i])
71+
U += m.weights[i] * Ui
72+
sti
73+
end
74+
return (U, Tuple(sts))
75+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#implementation
2+
3+
> Products of relations. Several implicit learners on the **same** variables, pooled: the field
4+
> of the product is the weighted sum of their fields, so its stable roots are where all the
5+
> relations hold at once, e.g. the two points where a learned circle meets a learned line.
6+
> `ProductRelation` is one more `AbstractImplicitRelation`, so every query function works on it
7+
> unchanged, and gradients flow back into each factor.
8+
9+
> Sources: code: `product.jl`, `implicit.jl` (`AbstractImplicitRelation`, `_params_cotangent`), `answers.jl`; Hinton, *Training Products of Experts by Minimizing Contrastive Divergence*, Neural Computation 2002; Du et al., *Reduce, Reuse, Recycle: Compositional Generation with Energy-Based Diffusion Models and MCMC*, ICML 2023 (adding scores, and why it is not exact at $t > 0$)
10+
>
11+
> Bibliography: [[Bibliography#^hinton2002poe|Hinton 2002]] · [[Bibliography#^du2023reduce|Du et al. 2023]]
12+
>
13+
> Theory: [[Composing Diffusion Factors]] · [[Implicit Diffusion Learners]] · [[Open Problems in Implicit Diffusion Learning]]
14+
15+
## 1. The construction
16+
17+
An implicit learner's field $g(z) = \sum_k w_k\lambda_k\,(\varepsilon_\theta(\alpha_k z + \sigma_k\varepsilon_k, t_k) - \varepsilon_k)$
18+
is, for an ideal predictor, the gradient of a smoothed negative log-density. A weighted sum of
19+
such fields is therefore the field of the product of the densities:
20+
21+
$$g_\Pi(z) = \sum_i \beta_i\, g_i(z) \quad\longleftrightarrow\quad p_\Pi \propto \prod_i p_i^{\beta_i}.$$
22+
23+
`ProductRelation(m₁, m₂, …; weights = β)` implements exactly the three functions the solvers
24+
use:
25+
26+
| function | product |
27+
|---|---|
28+
| `prior_field` | $\sum_i \beta_i g_i$ |
29+
| `prior_jacobian` | $\sum_i \beta_i J_i$ |
30+
| `_params_cotangent` | a tuple: factor $i$ gets the adjoint $\beta_i\lambda$ |
31+
32+
Parameters and states are tuples, one entry per factor: `ps = (ps₁, ps₂)`. Passing a single
33+
parameter set raises an `ArgumentError`. Products nest, because a product is itself an
34+
`AbstractImplicitRelation`.
35+
36+
## 2. What works on it unchanged
37+
38+
- `implicit_infer`: one answer, with clamps (`ρ = Inf`) and soft anchors as usual;
39+
- `implicit_roots`: all answers by multi-start, e.g. the intersection points;
40+
- `implicit_laplace`: a covariance per answer;
41+
- `implicit_pullback`: the IFT adjoint. The cotangent of the parameters is a tuple, so a loss
42+
on the intersection trains every factor;
43+
- `implicit_energy`, and ordering roots by energy, when every factor is an `EnergyNetwork`
44+
(the energy of the product is $\sum_i\beta_i U_i$).
45+
46+
The refactor that made this possible is small. `implicit.jl` now types its functions with
47+
the abstract supertype, and moves the parameter loop of the pullback into `_params_cotangent`,
48+
the one place where a product differs.
49+
50+
## 3. Accuracy: the smoothing bias
51+
52+
Pooling is exact only at zero noise. At the field's noise levels the product of the smoothed
53+
densities is not the smoothed product, $q_t * (p_1p_2) \ne (q_t * p_1)(q_t * p_2)$
54+
([[Open Problems in Implicit Diffusion Learning]] T9). For root finding this produces the same kind of
55+
bias that a single relation already has (its ridge sits slightly on the inside of a bend), not a
56+
failure. Closed-form mixtures (`GaussianMixtureEps`, $s = 0.05$), the default `field_nodes`:
57+
58+
| product | answers found | exact | largest distance to exact |
59+
|---|---|---|---|
60+
| circle ∩ line $y = x$ | 2 | 2 | 0.022 |
61+
| circle ∩ ellipse (1.5, 0.6) | 4 | 4 | 0.029 |
62+
63+
The adjoint is exact for the field that is actually solved. Clamping $x = 0.95$ on circle ∩
64+
ellipse, $\partial y/\partial x$ is −0.65669290204 by the adjoint and −0.65669290206 by finite
65+
differences. A parameter of the ellipse gives 0.0220369733 by both.
66+
67+
## 4. Limits
68+
69+
- **Same variables only.** All factors see the whole joint space. Factors on *different* but
70+
overlapping variable sets (a product with a separator, $p_Ap_B/p_S$) are not covered.
71+
- **Tangent intersections** are ill-conditioned: the Jacobian of the sum is nearly singular,
72+
so the Laplace covariance blows up along the common tangent. This is the right answer, but
73+
slow to converge.
74+
- **Empty intersections.** If the relations do not meet, the stable roots are compromises
75+
between them, weighted by β. They are still returned; nothing flags them yet.
76+
- **No correction steps.** The MCMC corrections of Du et al. are for sampling and are not built
77+
(I8).
78+
79+
## 5. Tests
80+
81+
`test/runtests.jl`, "products of relations: intersections":
82+
83+
- the field equals the weighted sum;
84+
- a non-tuple `ps` is rejected;
85+
- two and four roots within 0.05 of the exact intersections;
86+
- the adjoint for the clamp and for one ellipse parameter matches central differences
87+
(rtol 1e-6);
88+
- the parameter cotangent is a tuple.

‎lib/VariationalDiffusion.jl/test/runtests.jl‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,41 @@ end
667667
@test 1.1 < answer(remember) < 1.35 # averages the whole history
668668
end
669669

670+
@testset "products of relations: intersections" begin
671+
curve(f, n) = reduce(hcat, [collect(f(θ)) for θ in range(0, 2π; length = n + 1)[1:n]])
672+
nodes = field_nodes(Xoshiro(1), 2; samples = 8)
673+
function model(c)
674+
p = NoisePredictor(GaussianMixtureEps(SCHED, c; s = 0.05), SCHED)
675+
return ImplicitDiffusion(p, nodes), LuxCore.setup(Xoshiro(0), p)...
676+
end
677+
m1, p1, s1 = model(curve(θ -> (cos(θ), sin(θ)), 64)) # unit circle
678+
m2, p2, s2 = model(reduce(hcat, [[u, u] for u in range(-1.6, 1.6; length = 65)])) # line y = x
679+
m3, p3, s3 = model(curve(θ -> (1.5cos(θ), 0.6sin(θ)), 96)) # ellipse
680+
# the field of a product is the weighted sum of the fields
681+
prod = ProductRelation(m1, m2; weights = [1.0, 2.0])
682+
z = [0.3, -0.2]
683+
@test first(prior_field(prod, z, (p1, p2), (s1, s2))) ≈
684+
first(prior_field(m1, z, p1, s1)) .+ 2 .* first(prior_field(m2, z, p2, s2))
685+
@test_throws ArgumentError prior_field(prod, z, p1, s1) # ps must be a tuple
686+
# circle ∩ line: two answers, circle ∩ ellipse: four, near the exact intersections
687+
for (other, ps, st, exact) in ((m2, p2, s2, [[0.7071, 0.7071], [-0.7071, -0.7071]]),
688+
(m3, p3, s3, [[sx * 0.873, sy * 0.488] for sx in (1, -1) for sy in (1, -1)]))
689+
roots, _ = implicit_roots(ProductRelation(m1, other), [0.0, 0.0], [0.0, 0.0], (p1, ps), (s1, st);
690+
nstarts = 40, spread = 1.5, maxiters = 400)
691+
@test length(roots) == length(exact)
692+
@test all(r -> minimum(norm(r.z .- e) for e in exact) < 0.05, roots)
693+
end
694+
# the adjoint through a product: w.r.t. the clamped input and a parameter of the second factor
695+
pe = ProductRelation(m1, m3); ρ = [Inf, 0.0]; h = 1e-6
696+
sol, _ = implicit_infer(pe, [0.95, 0.4], ρ, (p1, p3), (s1, s3); tol = 1e-12)
697+
b = implicit_pullback(pe, sol, [0.95, 0.4], ρ, [0.0, 1.0], (p1, p3), (s1, s3))
698+
y(x, μ) = first(implicit_infer(pe, [x, 0.4], ρ, (p1, (μ = μ,)), (s1, s3); tol = 1e-12)).z[2]
699+
@test b.z₀[1] ≈ (y(0.95 + h, p3.μ) - y(0.95 - h, p3.μ)) / 2h rtol = 1e-6
700+
μ₊ = copy(p3.μ); μ₊[2, 10] += h; μ₋ = copy(p3.μ); μ₋[2, 10] -= h
701+
@test b.ps[2].μ[2, 10] ≈ (y(0.95, μ₊) - y(0.95, μ₋)) / 2h rtol = 1e-6
702+
@test b.ps isa Tuple && length(b.ps) == 2
703+
end
704+
670705
@testset "all answers of a query, and their Laplace uncertainty" begin
671706
# Gaussian data: the curvature is RED-Diff's κ, so the Laplace covariance has a closed form
672707
v0 = 0.5

0 commit comments

Comments
 (0)