1+ /*
2+ * Copyright (c) 2019 - 2025 Geode-solutions
3+ *
4+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5+ * of this software and associated documentation files (the "Software"), to deal
6+ * in the Software without restriction, including without limitation the rights
7+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+ * copies of the Software, and to permit persons to whom the Software is
9+ * furnished to do so, subject to the following conditions:
10+ *
11+ * The above copyright notice and this permission notice shall be included in
12+ * all copies or substantial portions of the Software.
13+ *
14+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+ * SOFTWARE.
21+ *
22+ */
23+
24+ #pragma once
25+
26+ #include < geode/stochastic/common.hpp>
27+ #include < geode/stochastic/sampling/mcmc/models/gibbs_energy.hpp>
28+ #include < geode/stochastic/sampling/mcmc/proposal/proposal_kernel.hpp>
29+
30+ namespace geode
31+ {
32+ enum struct MHDecision
33+ {
34+ Accepted,
35+ Rejected,
36+ Undecided
37+ };
38+
39+ template < typename Geometry >
40+ struct StepResult
41+ {
42+ MHDecision decision{ MHDecision::Undecided };
43+ typename Proposal< Geometry >::Type move_type{
44+ Proposal< Geometry >::Type::Invalid
45+ };
46+ double log_accept{ -std::numeric_limits< double >::infinity () };
47+ double delta_log_energy{ 0.0 };
48+ };
49+
50+ template < typename Geometry >
51+ class MetropolisHastings
52+ {
53+ public:
54+ MetropolisHastings ( GibbsEnergy< Geometry >& energy,
55+ std::unique_ptr< ProposalKernel< Geometry > > proposal_kernel )
56+ : energy_( energy ),
57+ proposal_kernel_ ( std::move( proposal_kernel ) )
58+ {
59+ OPENGEODE_ASSERT (
60+ proposal_kernel_ != nullptr , " [MH] null proposal kernel" );
61+ }
62+
63+ StepResult< Geometry > step (
64+ Configuration< Geometry >& state, RandomEngine& engine ) const
65+ {
66+ Proposal< Geometry > proposal =
67+ proposal_kernel_->propose ( state, engine );
68+
69+ if ( proposal.type == Proposal< Geometry >::Type::Birth )
70+ {
71+ return birth_step ( proposal, state, engine );
72+ }
73+ if ( proposal.type == Proposal< Geometry >::Type::Death )
74+ {
75+ return death_step ( proposal, state, engine );
76+ }
77+ if ( proposal.type == Proposal< Geometry >::Type::Change )
78+ {
79+ return change_step ( proposal, state, engine );
80+ }
81+ return StepResult< Geometry >{};
82+ }
83+
84+ double beta () const
85+ {
86+ return beta_;
87+ }
88+ void set_beta ( double b )
89+ {
90+ OPENGEODE_EXCEPTION ( b >= 0.0 , " [MH] beta must be >= 0" );
91+ if ( b == 0 )
92+ {
93+ geode::Logger::info (
94+ " [Metropolis Hastings] - beta == 0 all move will be "
95+ " accepted - Uniform sampling." );
96+ }
97+ if ( b < 1 )
98+ {
99+ geode::Logger::info (
100+ " [Metropolis Hastings] - beta < 1 moves that increase "
101+ " energy are "
102+ " more likely to be accepted - Hot system introduce "
103+ " randomness for exploration." );
104+ }
105+ if ( b == 1 )
106+ {
107+ geode::Logger::info ( " [Metropolis Hastings] - beta == 1 "
108+ " default choice no temperature "
109+ " - only consider energy." );
110+ }
111+ if ( b > 1 )
112+ {
113+ geode::Logger::info (
114+ " [Metropolis Hastings] - beta > 1 moves that increase "
115+ " energy are "
116+ " less likely to be accepted - Cold system to ensure "
117+ " convergence but may find local minimum randomness." );
118+ }
119+ beta_ = b;
120+ }
121+
122+ static double acceptance_prob_helper ( double log_accept )
123+ {
124+ if ( std::isnan ( log_accept ) )
125+ return 0.0 ;
126+ if ( log_accept >= 0.0 )
127+ return 1.0 ;
128+ // prevent expoential overflow
129+ constexpr double LOG_MIN = -745.0 ;
130+ if ( log_accept < LOG_MIN )
131+ return 0.0 ;
132+ return std::exp ( log_accept );
133+ }
134+
135+ private:
136+ const double compute_log_accept (
137+ const double deltaU, const Proposal< Geometry >& proposal ) const
138+ {
139+ OPENGEODE_ASSERT (
140+ std::isfinite ( proposal.log_forward_prob )
141+ && std::isfinite ( proposal.log_backward_prob ),
142+ " [MH] Non-finite proposal log-probabilities" );
143+ return -beta_ * deltaU + proposal.log_backward_prob
144+ - proposal.log_forward_prob ;
145+ }
146+
147+ template < typename ApplyMove >
148+ StepResult< Geometry > accept_or_reject ( Proposal< Geometry >& proposal,
149+ Configuration< Geometry >& state,
150+ RandomEngine& engine,
151+ const double delta_log_energy,
152+ ApplyMove&& apply_move ) const
153+ {
154+ StepResult< Geometry > step_result;
155+ step_result.move_type = proposal.type ;
156+ step_result.delta_log_energy = delta_log_energy;
157+ step_result.log_accept =
158+ compute_log_accept ( delta_log_energy, proposal );
159+
160+ double log_u = engine.sample_log ();
161+ step_result.decision = ( log_u < step_result.log_accept )
162+ ? MHDecision::Accepted
163+ : MHDecision::Rejected;
164+ if ( step_result.decision == MHDecision::Accepted )
165+ apply_move ( state, proposal );
166+
167+ return step_result;
168+ }
169+
170+ StepResult< Geometry > birth_step ( Proposal< Geometry >& proposal,
171+ Configuration< Geometry >& state,
172+ RandomEngine& engine ) const
173+ {
174+ OPENGEODE_ASSERT ( proposal.new_object .has_value (),
175+ " [MH] Birth proposal has no new_object" );
176+ const auto delta_log_energy = energy_.delta_log_energy_add (
177+ state, proposal.new_object .value () );
178+ return accept_or_reject ( proposal, state, engine, delta_log_energy,
179+ []( auto & s, auto & p ) {
180+ s.add_object ( std::move ( p.new_object .value () ) );
181+ } );
182+ };
183+
184+ StepResult< Geometry > death_step ( Proposal< Geometry >& proposal,
185+ Configuration< Geometry >& state,
186+ RandomEngine& engine ) const
187+ {
188+ OPENGEODE_ASSERT ( proposal.index .has_value (),
189+ " [MH] Death proposal has no index" );
190+ const auto delta_log_energy = energy_.delta_log_energy_remove (
191+ state, proposal.index .value () );
192+ return accept_or_reject ( proposal, state, engine, delta_log_energy,
193+ []( auto & s, auto & p ) {
194+ s.remove_object ( p.index .value () );
195+ } );
196+ };
197+
198+ StepResult< Geometry > change_step ( Proposal< Geometry >& proposal,
199+ Configuration< Geometry >& state,
200+ RandomEngine& engine ) const
201+ {
202+ OPENGEODE_ASSERT ( proposal.new_object .has_value (),
203+ " [MH] Change proposal has no new_object" );
204+ OPENGEODE_ASSERT ( proposal.index .has_value (),
205+ " [MH] Change proposal has no index" );
206+ const auto delta_log_energy = energy_.delta_log_energy_change (
207+ state, proposal.index .value (), proposal.new_object .value () );
208+ return accept_or_reject ( proposal, state, engine, delta_log_energy,
209+ []( auto & s, auto & p ) {
210+ s.change_object (
211+ p.index .value (), std::move ( p.new_object .value () ) );
212+ } );
213+ };
214+
215+ private:
216+ const GibbsEnergy< Geometry >& energy_;
217+ std::unique_ptr< ProposalKernel< Geometry > > proposal_kernel_;
218+ double beta_{ 1.0 };
219+ };
220+ } // namespace geode
0 commit comments