2 * @file metropolisHastingsSampler.tpp
3 * @brief MetropolisHastingsSampler template implementation.
4 * @details Implements the Metropolis-Hastings MCMC algorithm with
5 * symmetric random walk proposal and detailed balance verification.
9// Created by Giacomo Merlo on 11/01/26.
23 * @brief Construct and validate the Metropolis-Hastings sampler.
24 * @tparam dim Template dimension parameter.
25 * @param d Integration domain constraint.
26 * @param p Target probability density function.
27 * @param x0 Initial state of the Markov chain.
28 * @param deviation Standard deviation of random walk proposal.
30 * @throws std::invalid_argument If initial point x0 is outside the domain.
32 * @details Initializes the sampler with:
33 * - Reference to the domain constraint (used in next() for validation)
34 * - Target density function p (called at each proposal)
35 * - Current state = x0 (starting point of chain)
36 * - Random walk parameters: N(0, deviation²) for each coordinate
37 * - Acceptance statistics: n_steps = 0, n_accept = 0
39 * The constructor validates that x0 is within the domain; otherwise
40 * the Markov chain cannot start in a valid state.
43MetropolisHastingsSampler<dim>::MetropolisHastingsSampler(
44 const mc::domains::IntegrationDomain<dim>& d,
45 const std::function<double(const mc::geom::Point<dim>&)>& p,
46 mc::geom::Point<dim> x0,
51 , rw_normal(0.0, deviation)
54 if (!domain.isInside(current)) {
55 throw std::invalid_argument("MetropolisHastingsSampler: x0 is outside the domain.");
60 * @brief Generate the next sample in the Markov chain.
61 * @tparam dim Template dimension parameter.
62 * @param rng Random number generator engine.
63 * @return The current state after update: either y (accepted) or x (rejected).
65 * @throws std::runtime_error If target(current) becomes ≤ 0 or non-finite.
66 * This indicates an algorithm failure.
68 * @details Implements one step of Metropolis-Hastings:
70 * **Step 1: Proposal**
71 * - Generate y = current + ε where ε ~ N(0, σ²) for each coordinate
72 * - σ is the deviation parameter specified at construction
74 * **Step 2: Domain & Validity Check**
75 * - Evaluate π(y). If non-finite or ≤ 0, reject automatically
76 * - (Relies on target density returning 0 outside domain, not domain checks)
78 * **Step 3: Acceptance Ratio**
79 * - Evaluate π(current). Must be finite and > 0 (invariant)
80 * - Compute α = min(1, π(y)/π(current))
82 * **Step 4: Accept/Reject**
83 * - Draw u ~ Unif[0,1]
84 * - If u < α: accept y (current = y, n_accept++)
85 * - Else: reject y (current unchanged)
87 * **Step 5: Statistics**
89 * - Return updated current
91 * @note Symmetric proposal (N(0,σ²)) ensures detailed balance holds.\n
92 * The ratio simplifies to π(y)/π(current) without proposal ratio.\n
93 * Domain handling via target density return value adds flexibility.
95 * @see acceptance_rate() for monitoring chain behavior
99MetropolisHastingsSampler<dim>::next(std::mt19937& rng)
103 mc::geom::Point<dim> y = current;
104 for (std::size_t k = 0; k < dim; ++k)
105 y[k] += rw_normal(rng);
107 // NOTE: Domain handling is delegated to the target density function.
108 // The target should return 0 outside the valid domain rather than
109 // throwing exceptions. This approach offers greater flexibility compared
110 // to explicit domain checking within the sampler.
112 const double px = target(current);
113 const double py = target(y);
115 if (!std::isfinite(py) || py <= 0.0) {
116 return current; // reject
121 if (!std::isfinite(px) || px <= 0.0) {
122 throw std::runtime_error("MH: target(current) must be finite and > 0.");
124 alpha = std::min(1.0, py / px);
127 if (uni(rng) < alpha) {
135 * @brief Evaluate the target probability density at a point.
136 * @tparam dim Template dimension parameter.
137 * @param x The query point.
138 * @return π(x), the target density value at x.
140 * @details Simple accessor that delegates to the target density function
141 * stored at construction. This allows external code to evaluate π(x)
142 * without direct access to the stored function object.
146MetropolisHastingsSampler<dim>::target_pdf(const mc::geom::Point<dim>& x)
152 * @brief Get the current acceptance rate of the Markov chain.
153 * @tparam dim Template dimension parameter.
154 * @return Acceptance rate: n_accept / n_steps, or 0.0 if n_steps == 0.
156 * @details Returns the fraction of proposed moves (calls to next()) that were
157 * accepted by the Metropolis-Hastings criterion. This cumulative metric helps
158 * tune the proposal deviation for better exploration.
160 * **Interpretation**:
161 * - Acceptance rate = n_accept / n_steps
162 * - Theoretically optimal ~23.4% for high-dimensional problems (d > 5)
163 * - In practice, 20-40% is often reasonable depending on dimension and target
166 * - If rate > 50%: increase deviation to propose larger moves
167 * - If rate < 10%: decrease deviation to propose smaller, safer moves
168 * - If rate ≈ 23.4%: well-tuned for high dimensions
172MetropolisHastingsSampler<dim>::acceptance_rate() const
174 return (n_steps == 0) ? 0.0 : static_cast<double>(n_accept) / static_cast<double>(n_steps);