CMP++: Uncertainty Quantification & Bayesian Calibration
Loading...
Searching...
No Matches
mcmc.h
Go to the documentation of this file.
1#ifndef MCMC_H
2#define MCMC_H
3
4#include "cmp_defines.h"
5#include "distribution.h"
6#include <algorithm>
7#include <cmath>
8#include <iostream>
9#include <iomanip>
10#include <vector>
11#include <limits>
12#include <random>
13#include <Eigen/Dense>
14
19namespace cmp::mcmc {
20
21// ==============================================================================
22// HELPER FUNCTIONS
23// ==============================================================================
24
25// Numerically safe log(1 - α)
26inline double log1m(double a) {
27 return (a < 1.0) ? std::log1p(-a) : -std::numeric_limits<double>::infinity();
28}
29
30// ==============================================================================
31// MARKOV CHAIN CLASS TEMPLATE
32// ==============================================================================
33
34
74template <typename ProposalType>
76 protected:
77 double score_{0.0};
78 size_t dim_;
79 size_t nSteps_{0};
80 size_t nAccepts_{0};
81
82 Eigen::VectorXd mean_;
83 Eigen::MatrixXd cov_;
84
85 ProposalType proposal_;
86
87 std::uniform_real_distribution<double> distU_{0.0, 1.0};
88 std::default_random_engine rng_;
89
90 double scale_;
92
93 public:
101 MarkovChain(const ProposalType &proposal,
102 std::default_random_engine &rng,
103 const double &targetAcceptanceRatio = 0.234)
104 : rng_(rng),
105 proposal_(proposal),
106 score_(-std::numeric_limits<double>::infinity()),
107 dim_(proposal.get().size()),
108 targetAcceptanceRatio_(targetAcceptanceRatio),
109 mean_(Eigen::VectorXd::Zero(dim_)),
110 cov_(Eigen::MatrixXd::Zero(dim_, dim_)),
111 scale_(5.6644 / static_cast<double>(dim_)) // 2.38^2 = 5.6644
112 {}
113
114 MarkovChain() = default;
115 ~MarkovChain() = default;
116
121 nSteps_++;
122 }
123
130 bool accept(const Eigen::Ref<const Eigen::VectorXd> &par, double score) {
131 if((score - score_) > std::log(distU_(rng_))) {
132 proposal_.set(par);
133 score_ = score;
134 nAccepts_++;
135 return true;
136 }
137 return false;
138 }
139
143 void step(const score_t &getScore, const bool &delayedRejection = false, const double& gamma = 0.1) {
145
146 // ========================================================================
147 // STAGE 1: Standard Proposal
148 // ========================================================================
149 Eigen::VectorXd cand_prev = proposal_.sample(rng_, 1.0);
150 double score_prev = getScore(cand_prev);
151
152 // Calculate Stage 1 continuous acceptance probability (alpha_1_fwd)
153 double log_alpha_1_fwd = std::min(0.0, score_prev - score_);
154 double alpha_1_fwd = std::exp(log_alpha_1_fwd);
155
156 bool accepted = accept(cand_prev, score_prev);
157
158 // Robbins-Monro Global Scale Update (Tuning to First-Stage efficiency)
159 double rm_gamma = 1.0 / std::sqrt(static_cast<double>(nSteps_));
160 scale_ *= std::exp(rm_gamma * (alpha_1_fwd - targetAcceptanceRatio_));
161
162 // ========================================================================
163 // STAGE 2: Delayed Rejection (DRAM Detailed Balance)
164 // ========================================================================
165 if(!accepted && delayedRejection) {
166
167 // Use the first gamma for the narrower DR backup proposal
168 Eigen::VectorXd cand_new = proposal_.sample(rng_, gamma);
169 double score_new = getScore(cand_new);
170
171 // 1. The Backward Rejection Probability
172 double log_alpha_1_bwd = std::min(0.0, score_prev - score_new);
173 double alpha_1_bwd = std::exp(log_alpha_1_bwd);
174
175 double log_rej_fwd = log1m(alpha_1_fwd);
176 double log_rej_bwd = log1m(alpha_1_bwd);
177
178 // 2. The Proposal Asymmetry
179 Eigen::VectorXd jump_fwd = cand_prev - getCurrent();
180 Eigen::VectorXd jump_bwd = cand_prev - cand_new;
181
182 double log_q1_fwd = -0.5 * proposal_.squaredMahalanobis(jump_fwd);
183 double log_q1_bwd = -0.5 * proposal_.squaredMahalanobis(jump_bwd);
184
185 // 3. The True DRAM Stage-2 Acceptance Ratio
186 double log_ratio_stage2 = (score_new - score_) + // Pi ratio
187 (log_q1_bwd - log_q1_fwd) + // Proposal ratio
188 (log_rej_bwd - log_rej_fwd); // Rejection ratio
189
190 double alpha_2 = std::exp(std::min(0.0, log_ratio_stage2));
191
192 // Final Stage-2 Coin Flip
193 if(std::log(distU_(rng_)) < std::log(alpha_2)) {
194 proposal_.set(cand_new);
195 score_ = score_new;
196 accepted = true;
197 }
198 }
199
200 update();
201 }
202
206 void update() {
207 Eigen::VectorXd par = proposal_.get();
208
209 if(nSteps_ == 1) {
210 mean_ = par;
211 cov_.setZero();
212 } else {
213 // Welford's stable 1-pass multi-dimensional algorithm
214 Eigen::VectorXd delta_old = par - mean_;
215 mean_ += delta_old / static_cast<double>(nSteps_);
216 Eigen::VectorXd delta_new = par - mean_;
217
218 cov_ += delta_old * delta_new.transpose();
219 }
220 }
221
225 Eigen::MatrixXd getAdaptedCovariance() const {
226 double epsilon = 1e-6; // Regularization
227 Eigen::MatrixXd identity = Eigen::MatrixXd::Identity(dim_, dim_);
228 return scale_ * getCovariance() + scale_ * epsilon * identity;
229 }
230
234 void reset() {
235 nSteps_ = 0;
236 nAccepts_ = 0;
237 mean_.setZero();
238 cov_.setZero();
239 scale_ = 5.6644 / static_cast<double>(dim_);
240 }
241
246 Eigen::VectorXd getCurrent() const {
247 return proposal_.get();
248 }
249
254 double getScore() const {
255 return score_;
256 }
257
262 size_t getDim() const {
263 return dim_;
264 }
265
270 size_t getSteps() const {
271 return nSteps_;
272 }
273
278 double getAcceptanceRatio() const {
279 return static_cast<double>(nAccepts_) / static_cast<double>(nSteps_);
280 }
281
286 Eigen::VectorXd getMean() const {
287 return mean_;
288 }
289
294 Eigen::MatrixXd getCovariance() const {
295 if(nSteps_ <= 1) {
296 return Eigen::MatrixXd::Zero(dim_, dim_);
297 }
298 return cov_ / (static_cast<double>(nSteps_) - 1.0);
299 }
300
304 void info() const {
305 std::cout << "run " << nSteps_ << " steps\n"
306 << "acceptance ratio: " << std::fixed << std::setprecision(3) << getAcceptanceRatio() << "\n"
307 << "Data covariance: \n" << getCovariance() << "\n"
308 << "Data mean: \n" << getMean() << std::endl;
309 }
310};
311
312// ==============================================================================
313// CHAIN DIAGNOSTICS (Also templated)
314// ==============================================================================
315
337template <typename ProposalType>
338double multiChainDiagnosis(const std::vector<MarkovChain<ProposalType>> &chains) {
339 if(chains.empty()) return 0.0;
340
341 size_t dim_chain = chains[0].getDim();
342 size_t num_chains = chains.size();
343
344 Eigen::VectorXd chainwise_mean = Eigen::VectorXd::Zero(dim_chain);
345 Eigen::VectorXd chainwise_cov = Eigen::VectorXd::Zero(dim_chain);
346 Eigen::VectorXd chainwise_mean_cov = Eigen::VectorXd::Zero(dim_chain);
347
348 for(const auto &chain : chains) {
349 Eigen::VectorXd current_mean = chain.getMean();
350 Eigen::MatrixXd current_cov = chain.getCovariance();
351
352 chainwise_mean += current_mean;
353 chainwise_cov += current_cov.diagonal();
354 chainwise_mean_cov += current_mean.cwiseProduct(current_mean);
355 }
356
357 chainwise_mean /= static_cast<double>(num_chains);
358 chainwise_cov /= static_cast<double>(num_chains);
359
360 chainwise_mean_cov = chainwise_mean_cov / static_cast<double>(num_chains) - chainwise_mean.cwiseProduct(chainwise_mean);
361
362 if(num_chains > 1) {
363 chainwise_mean_cov *= static_cast<double>(num_chains) / static_cast<double>(num_chains - 1);
364 }
365
366 Eigen::VectorXd var_chain = chainwise_cov + chainwise_mean_cov;
367
368 // Prevent division by zero if variance is perfectly flat
369 Eigen::VectorXd r_hat = Eigen::VectorXd::Zero(dim_chain);
370 for(size_t i = 0; i < dim_chain; ++i) {
371 if(chainwise_cov(i) > 0.0) {
372 r_hat(i) = std::sqrt(var_chain(i) / chainwise_cov(i));
373 } else {
374 r_hat(i) = 1.0;
375 }
376 }
377
378 return r_hat.maxCoeff();
379}
380
381
402 protected:
403 size_t nChains_{0};
404 size_t dim_{0};
405 std::vector<Eigen::VectorXd> chainSamples_;
406 std::vector<double> chainScores_;
407
408 // Random distribution to pick a random chain
409 std::uniform_int_distribution<size_t> distChain_;
410
411 // Random distribution to pick a random number
412 std::uniform_real_distribution<double> distU_{0, 1};
413 std::normal_distribution<double> distN_{0, 1};
414
415 double nugget_{1e-6};
416
417 public:
419
420
427 EvolutionaryMarkovChain(std::vector<Eigen::VectorXd> initialSamples, std::vector<double> initialScores, double nugget = 1e-6);
428
435 void step(const score_t &getScore, std::default_random_engine &rng, double gamma = 0.2);
436
441 std::vector<Eigen::VectorXd> getCurrent() const {
442 return chainSamples_;
443 }
444
449 std::vector<double> getScores() const {
450 return chainScores_;
451 }
452};
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470}
471
474#endif
Implements an Evolutionary Markov Chain Monte Carlo sampler.
Definition mcmc.h:401
std::uniform_real_distribution< double > distU_
Uniform distribution helper.
Definition mcmc.h:412
size_t dim_
Dimension of the parameter space.
Definition mcmc.h:404
std::vector< double > getScores() const
Gets the current log probability scores of all chains.
Definition mcmc.h:449
std::uniform_int_distribution< size_t > distChain_
Discrete uniform distribution helper to pick random chain.
Definition mcmc.h:409
std::vector< Eigen::VectorXd > getCurrent() const
Gets the current states of all chains.
Definition mcmc.h:441
size_t nChains_
Number of active parallel chains.
Definition mcmc.h:403
double nugget_
Crossover mutation noise scaling.
Definition mcmc.h:415
std::vector< Eigen::VectorXd > chainSamples_
Current parameter samples for each chain.
Definition mcmc.h:405
std::normal_distribution< double > distN_
Normal distribution helper.
Definition mcmc.h:413
void step(const score_t &getScore, std::default_random_engine &rng, double gamma=0.2)
Performs one crossover and mutation step for all chains.
Definition mcmc.cpp:30
std::vector< double > chainScores_
Current score (log probability) for each chain.
Definition mcmc.h:406
Represents a single Markov Chain utilizing Delayed Rejection Adaptive Metropolis (DRAM) for sampling.
Definition mcmc.h:75
std::default_random_engine rng_
Random engine.
Definition mcmc.h:88
Eigen::MatrixXd cov_
Parameter sample running covariance matrix.
Definition mcmc.h:83
size_t nSteps_
Number of steps taken.
Definition mcmc.h:79
ProposalType proposal_
Proposal distribution model (copied by value).
Definition mcmc.h:85
size_t getSteps() const
Gets the total number of steps run in this chain.
Definition mcmc.h:270
MarkovChain(const ProposalType &proposal, std::default_random_engine &rng, const double &targetAcceptanceRatio=0.234)
Construct a new mcmc chain object.
Definition mcmc.h:101
void increaseSteps()
Increase the number of steps.
Definition mcmc.h:120
double getScore() const
Gets the current log probability score of the chain.
Definition mcmc.h:254
Eigen::VectorXd getMean() const
Gets the running mean of the parameter samples.
Definition mcmc.h:286
void step(const score_t &getScore, const bool &delayedRejection=false, const double &gamma=0.1)
Perform a single MCMC step.
Definition mcmc.h:143
double targetAcceptanceRatio_
Target acceptance ratio for adaptive tuning.
Definition mcmc.h:91
size_t getDim() const
Gets the dimensionality of the parameter space.
Definition mcmc.h:262
double getAcceptanceRatio() const
Gets the current acceptance ratio of proposals.
Definition mcmc.h:278
Eigen::MatrixXd getCovariance() const
Gets the running covariance of the parameter samples.
Definition mcmc.h:294
void update()
Updates the value of the mean and covariance matrix.
Definition mcmc.h:206
size_t nAccepts_
Number of accepted proposals.
Definition mcmc.h:80
double score_
Current log probability score value.
Definition mcmc.h:77
size_t dim_
Dimension of parameter space.
Definition mcmc.h:78
Eigen::VectorXd mean_
Parameter sample running mean vector.
Definition mcmc.h:82
double scale_
Proposal covariance scale parameter.
Definition mcmc.h:90
bool accept(const Eigen::Ref< const Eigen::VectorXd > &par, double score)
Accept or reject a candidate.
Definition mcmc.h:130
void reset()
Definition mcmc.h:234
void info() const
Definition mcmc.h:304
Eigen::VectorXd getCurrent() const
Gets the current parameters of the chain.
Definition mcmc.h:246
std::uniform_real_distribution< double > distU_
Uniform distribution helper.
Definition mcmc.h:87
Eigen::MatrixXd getAdaptedCovariance() const
Get a covariance matrix adapted to the samples.
Definition mcmc.h:225
Definition cmp_defines.h:19
Definition hmc.h:10
double log1m(double a)
Definition mcmc.h:26
double multiChainDiagnosis(const std::vector< MarkovChain< ProposalType > > &chains)
Computes the Gelman-Rubin convergence diagnostic metric (R-hat).
Definition mcmc.h:338
std::function< double(const Eigen::VectorXd &)> score_t
The score_t type A function that takes a const reference to an Eigen::VectorXd and returns a double A...
Definition cmp_defines.h:82