@@ -84,13 +84,14 @@ abstract type ZygoteProtocol <: AutodiffProtocol end
8484abstract type EnzymeProtocol <: AutodiffProtocol end
8585=#
8686
87- function _get_proposal (encoded_prior:: ParameterDistribution , encoder_schedule:: VV ) where {VV <: AbstractVector }
88- # We use the prior covariance to shape the proposal (in the encoded space),
89- # as proposals are based on increments we do not need to shift the mean too
87+ function _get_cholesky_factor (encoded_prior:: ParameterDistribution , encoder_schedule:: VV ) where {VV <: AbstractVector }
88+ # We use the prior covariance to shape the proposal (in the encoded space),
89+ # as proposals are based on increments we do not need to shift the mean too.
90+ # The Cholesky factor L (C = LL') is the single source of truth every sampler below
91+ # uses both to draw its noise and to evaluate its transition (log-)density, so the
92+ # two can never drift apart.
9093 C = cov (encoded_prior)
91- Σ = cholesky (Symmetric (C))
92-
93- return AdvancedMH. RandomWalkProposal (Σ. L * MvNormal (zeros (size (Σ, 1 )), I))
94+ return cholesky (Symmetric (C)). L
9495end
9596
9697
@@ -112,15 +113,10 @@ struct RWMHSampling{T <: AutodiffProtocol} <: MCMCProtocol end
112113
113114RWMHSampling () = RWMHSampling {GradFreeProtocol} ()
114115
115- struct RWMetropolisHastings{PT, ADT <: AutodiffProtocol } <: AdvancedMH.MHSampler
116- proposal:: PT
116+ struct RWMetropolisHastings{LT, ADT <: AutodiffProtocol } <: AdvancedMH.MHSampler
117+ " Lower Cholesky factor `L` of the (encoded) prior covariance `C = LL'`, shaping the proposal noise."
118+ cholesky_L:: LT
117119end
118- # Define method needed by AdvancedMH for new Sampler
119- AdvancedMH. logratio_proposal_density (
120- sampler:: RWMetropolisHastings ,
121- transition_prev:: AdvancedMH.AbstractTransition ,
122- candidate,
123- ) = AdvancedMH. logratio_proposal_density (sampler. proposal, transition_prev. params, candidate)
124120
125121"""
126122$(TYPEDSIGNATURES)
@@ -142,8 +138,8 @@ function MetropolisHastingsSampler(
142138 encoded_prior:: ParameterDistribution ,
143139 encoder_schedule:: VV ,
144140) where {T <: AutodiffProtocol , VV <: AbstractVector }
145- proposal = _get_proposal (encoded_prior, encoder_schedule)
146- return RWMetropolisHastings {typeof(proposal ), T} (proposal )
141+ L = _get_cholesky_factor (encoded_prior, encoder_schedule)
142+ return RWMetropolisHastings {typeof(L ), T} (L )
147143end
148144
149145"""
@@ -157,22 +153,17 @@ on the covariance of `prior`.
157153struct pCNMHSampling{T <: AutodiffProtocol } <: MCMCProtocol end
158154pCNMHSampling () = pCNMHSampling {GradFreeProtocol} ()
159155
160- struct pCNMetropolisHastings{D, T <: AutodiffProtocol } <: AdvancedMH.MHSampler
161- proposal:: D
156+ struct pCNMetropolisHastings{LT, T <: AutodiffProtocol } <: AdvancedMH.MHSampler
157+ " Lower Cholesky factor `L` of the (encoded) prior covariance `C = LL'`, shaping the proposal noise."
158+ cholesky_L:: LT
162159end
163- # Define method needed by AdvancedMH for new Sampler
164- AdvancedMH. logratio_proposal_density (
165- sampler:: pCNMetropolisHastings ,
166- transition_prev:: AdvancedMH.AbstractTransition ,
167- candidate,
168- ) = AdvancedMH. logratio_proposal_density (sampler. proposal, transition_prev. params, candidate)
169160function MetropolisHastingsSampler (
170161 :: pCNMHSampling{T} ,
171162 encoded_prior:: ParameterDistribution ,
172163 encoder_schedule:: VV ,
173164) where {T <: AutodiffProtocol , VV <: AbstractVector }
174- proposal = _get_proposal (encoded_prior, encoder_schedule)
175- return pCNMetropolisHastings {typeof(proposal ), T} (proposal )
165+ L = _get_cholesky_factor (encoded_prior, encoder_schedule)
166+ return pCNMetropolisHastings {typeof(L ), T} (L )
176167end
177168
178169# ------ The following are gradient-based samplers
@@ -186,23 +177,65 @@ new parameters according to the Barker proposal.
186177struct BarkerSampling{T <: AutodiffProtocol } <: MCMCProtocol end
187178BarkerSampling () = BarkerSampling {ForwardDiffProtocol} ()
188179
189- struct BarkerMetropolisHastings{D, T <: AutodiffProtocol } <: AdvancedMH.MHSampler
190- proposal:: D
180+ struct BarkerMetropolisHastings{LT, T <: AutodiffProtocol } <: AdvancedMH.MHSampler
181+ " Lower Cholesky factor `L` of the (encoded) prior covariance `C = LL'`, shaping the proposal noise."
182+ cholesky_L:: LT
191183end
192- # Define method needed by AdvancedMH for new Sampler
193- AdvancedMH. logratio_proposal_density (
194- sampler:: BarkerMetropolisHastings ,
195- transition_prev:: AdvancedMH.AbstractTransition ,
196- candidate,
197- ) = AdvancedMH. logratio_proposal_density (sampler. proposal, transition_prev. params, candidate)
198184
199185function MetropolisHastingsSampler (
200186 :: BarkerSampling{T} ,
201187 encoded_prior:: ParameterDistribution ,
202188 encoder_schedule:: VV ,
203189) where {T <: AutodiffProtocol , VV <: AbstractVector }
204- proposal = _get_proposal (encoded_prior, encoder_schedule)
205- return BarkerMetropolisHastings {typeof(proposal), T} (proposal)
190+ L = _get_cholesky_factor (encoded_prior, encoder_schedule)
191+ return BarkerMetropolisHastings {typeof(L), T} (L)
192+ end
193+
194+ # ------------------------------------------------------------------------------------------
195+ # Shared Metropolis-Hastings transition-density interface.
196+ #
197+ # Every sampler above is required to implement ONE function, log_transition_density(sampler,
198+ # model, θ_from, θ_to; stepsize) = log q(θ_to | θ_from) — the honest forward transition
199+ # log-density of whatever Markov kernel that sampler's `propose` actually draws from. There is
200+ # no default/fallback implementation: a sampler that doesn't implement it errors loudly instead
201+ # of silently inheriting a (possibly wrong) symmetric random-walk correction.
202+ #
203+ # AdvancedMH.logratio_proposal_density, which feeds directly into the MH acceptance ratio, is
204+ # then defined ONCE, generically over every AdvancedMH.MHSampler (so it applies automatically to
205+ # any sampler added to this module in future, not just the three below), as the textbook
206+ # Hastings correction log q(θ_from | θ_to) − log q(θ_to | θ_from) (reverse − forward), by
207+ # evaluating log_transition_density in both directions. A sampler can never update its `propose`
208+ # without also updating the quantity that determines its acceptance ratio, because there is only
209+ # one function (log_transition_density) doing double duty for both.
210+ function log_transition_density (
211+ sampler:: MHS ,
212+ model,
213+ θ_from,
214+ θ_to;
215+ stepsize:: FT = 1.0 ,
216+ ) where {MHS <: AdvancedMH.MHSampler , FT <: AbstractFloat }
217+ throw (
218+ ArgumentError (
219+ " log_transition_density not implemented for $(MHS) . Every MHSampler in this module " *
220+ " must implement its own log_transition_density(sampler, model, θ_from, θ_to; stepsize) " *
221+ " = log q(θ_to | θ_from); there is no generic/symmetric fallback, since silently assuming " *
222+ " one caused the prior-double-counting bug this interface replaces." ,
223+ ),
224+ )
225+ end
226+
227+ function AdvancedMH. logratio_proposal_density (
228+ sampler:: MHS ,
229+ model:: AdvancedMH.DensityModel ,
230+ transition_prev:: AdvancedMH.AbstractTransition ,
231+ candidate;
232+ stepsize:: FT = 1.0 ,
233+ ) where {MHS <: AdvancedMH.MHSampler , FT <: AbstractFloat }
234+ θ_from = transition_prev. params
235+ θ_to = candidate
236+ log_q_forward = log_transition_density (sampler, model, θ_from, θ_to; stepsize = stepsize)
237+ log_q_reverse = log_transition_density (sampler, model, θ_to, θ_from; stepsize = stepsize)
238+ return log_q_reverse - log_q_forward
206239end
207240
208241
@@ -336,7 +369,20 @@ function AdvancedMH.propose(
336369 current_state:: MCMCState ;
337370 stepsize:: FT = 1.0 ,
338371) where {FT <: AbstractFloat }
339- return current_state. params + stepsize * rand (rng, sampler. proposal)
372+ L = sampler. cholesky_L
373+ return current_state. params .+ stepsize .* (L * randn (rng, size (L, 1 )))
374+ end
375+
376+ # θ_to | θ_from ~ N(θ_from, stepsize² C), the symmetric kernel that propose() above draws from.
377+ function log_transition_density (
378+ sampler:: RWMetropolisHastings ,
379+ model:: AdvancedMH.DensityModel ,
380+ θ_from,
381+ θ_to;
382+ stepsize:: FT = 1.0 ,
383+ ) where {FT <: AbstractFloat }
384+ L = sampler. cholesky_L
385+ return logpdf (MvNormal (θ_from, Symmetric ((stepsize^ 2 ) .* (L * L' ))), θ_to)
340386end
341387
342388# method extending AdvancedMH.propose() for preconditioned Crank-Nicholson
@@ -347,10 +393,27 @@ function AdvancedMH.propose(
347393 current_state:: MCMCState ;
348394 stepsize:: FT = 1.0 ,
349395) where {FT <: AbstractFloat }
350- # Use prescription in Beskos et al (2017) "Geometric MCMC for infinite-dimensional
396+ # Use prescription in Beskos et al (2017) "Geometric MCMC for infinite-dimensional
351397 # inverse problems." for relating ρ to Euler stepsize:
352398 ρ = (1 - stepsize / 4 ) / (1 + stepsize / 4 )
353- return ρ * current_state. params .+ sqrt (1 - ρ^ 2 ) * rand (rng, sampler. proposal)
399+ L = sampler. cholesky_L
400+ return ρ .* current_state. params .+ sqrt (1 - ρ^ 2 ) .* (L * randn (rng, size (L, 1 )))
401+ end
402+
403+ # θ_to | θ_from ~ N(ρθ_from, (1-ρ²)C), the asymmetric kernel that propose() above draws from.
404+ # Evaluating this honestly in both directions (via the generic logratio_proposal_density) is
405+ # what recovers the pCN cancellation log q(θ_from|θ_to) - log q(θ_to|θ_from) = logprior(θ_from) -
406+ # logprior(θ_to), instead of the silently-symmetric 0 the previous implementation returned.
407+ function log_transition_density (
408+ sampler:: pCNMetropolisHastings ,
409+ model:: AdvancedMH.DensityModel ,
410+ θ_from,
411+ θ_to;
412+ stepsize:: FT = 1.0 ,
413+ ) where {FT <: AbstractFloat }
414+ ρ = (1 - stepsize / 4 ) / (1 + stepsize / 4 )
415+ L = sampler. cholesky_L
416+ return logpdf (MvNormal (ρ .* θ_from, Symmetric ((1 - ρ^ 2 ) .* (L * L' ))), θ_to)
354417end
355418
356419# method extending AdvancedMH.propose() for the Barker proposal
@@ -361,12 +424,41 @@ function AdvancedMH.propose(
361424 current_state:: MCMCState ;
362425 stepsize:: FT = 1.0 ,
363426) where {FT <: AbstractFloat }
364- # Livingstone and Zanella (2022)
365- # Compute the gradient of the log-density at the current state
366- n = length (current_state. params)
367- log_gradient = autodiff_gradient (model, current_state. params, sampler)
368- xi = rand (rng, sampler. proposal)
369- return current_state. params .+ (stepsize .* ((rand (rng, n) .< 1 ./ (1 .+ exp .(- log_gradient .* xi))) .* xi))
427+ # Livingstone and Zanella (2022). The elementwise sigmoid selection that defines the Barker
428+ # proposal is only reversible for *independent* coordinates, so we apply it in the whitened
429+ # coordinate system u = L⁻¹θ (where the prior-covariance preconditioning L is decorrelated
430+ # to the identity) and map the increment back with L: the gradient is rotated into whitened
431+ # coordinates via L'∇log π(θ) (chain rule for θ = θ₀ + Lu). We also flip the sign of the
432+ # whitened noise η rather than zeroing it out — the standard Barker kernel moves by ±η, never
433+ # by exactly 0, so its transition density (below) is an ordinary, atom-free density; zeroing
434+ # out (keep-or-drop) instead creates a mixed discrete/continuous kernel with no closed form.
435+ θ = current_state. params
436+ n = length (θ)
437+ L = sampler. cholesky_L
438+ grad_white = L' * autodiff_gradient (model, θ, sampler)
439+ η = randn (rng, n)
440+ flip_prob = 1 ./ (1 .+ exp .(- grad_white .* η))
441+ sign = 2 .* (rand (rng, n) .< flip_prob) .- 1
442+ ζ = stepsize .* sign .* η
443+ return θ .+ L * ζ
444+ end
445+
446+ # For a symmetric noise density g (here standard normal) and whitened increment e = ζ/stepsize,
447+ # the marginal density of the ± flip-sign move is q(e | θ_from) = 2 g(e) σ(grad_white·e) (Barker,
448+ # 1965; Livingstone & Zanella, 2022): summing the probability that e itself was kept (w.p.
449+ # σ(grad_white·e)) with the probability that -e was drawn and flipped (w.p. σ(grad_white·e) too,
450+ # since g(-e) = g(e)). See propose() above for how θ_to is generated from θ_from.
451+ function log_transition_density (
452+ sampler:: BarkerMetropolisHastings ,
453+ model:: AdvancedMH.DensityModel ,
454+ θ_from,
455+ θ_to;
456+ stepsize:: FT = 1.0 ,
457+ ) where {FT <: AbstractFloat }
458+ L = sampler. cholesky_L
459+ grad_white = L' * autodiff_gradient (model, θ_from, sampler)
460+ e = (L \ (θ_to .- θ_from)) ./ stepsize
461+ return sum (log (2 ) .+ logpdf .(Normal (), e) .+ log .(1 ./ (1 .+ exp .(- grad_white .* e)))) - length (e) * log (stepsize)
370462end
371463
372464# Copy a MCMCState and set accepted = false
@@ -393,7 +485,8 @@ function AbstractMCMC.step(
393485 AdvancedMH. logdensity (model, current_state)
394486
395487 log_α =
396- new_log_density - current_log_density + AdvancedMH. logratio_proposal_density (sampler, current_state, new_params)
488+ new_log_density - current_log_density +
489+ AdvancedMH. logratio_proposal_density (sampler, model, current_state, new_params; stepsize = stepsize)
397490
398491 # Decide whether to return the previous params or the new one.
399492 new_state = if - Random. randexp (rng) < log_α
0 commit comments