Skip to content

Commit 23c514e

Browse files
authored
Merge pull request #8 from Geode-solutions/mh_sampler
Mh sampler
2 parents bcad127 + 0dbf997 commit 23c514e

24 files changed

Lines changed: 1122 additions & 230 deletions

bindings/python/tests/CMakeLists.txt

100755100644
File mode changed.

bindings/python/tests/stochastic/CMakeLists.txt

100755100644
File mode changed.

include/geode/stochastic/configuration/marked_object.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ namespace geode
5252
{
5353
public:
5454
MarkedObject( Geometry&& geometry );
55-
MarkedObject( Geometry&& geometry, Mark mark );
55+
MarkedObject( Geometry&& geometry, std::optional< Mark > mark );
5656

5757
void set_geometry( const Geometry& geometry );
5858
void set_geometry( Geometry&& geometry );
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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

include/geode/stochastic/sampling/mcmc/models/components/energy_term.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ namespace geode
8282
return neg_log_parameter_.param();
8383
}
8484

85-
virtual double log_total(
85+
virtual double total_log(
8686
const Configuration< Geometry >& state ) const = 0;
8787

88-
virtual double log_delta_add( const Configuration< Geometry >& state,
88+
virtual double delta_log_add( const Configuration< Geometry >& state,
8989
const MarkedObject< Geometry >& sample ) const = 0;
9090

91-
virtual double log_delta_remove( const Configuration< Geometry >& state,
91+
virtual double delta_log_remove( const Configuration< Geometry >& state,
9292
index_t sample_id ) const = 0;
9393

94-
virtual double log_delta_change( const Configuration< Geometry >& state,
94+
virtual double delta_log_change( const Configuration< Geometry >& state,
9595
index_t old_sample_id,
9696
const MarkedObject< Geometry >& new_sample ) const = 0;
9797

include/geode/stochastic/sampling/mcmc/models/components/intensity_term.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,25 @@ namespace geode
3737
{
3838
}
3939

40-
double log_total( const Configuration< Geometry >& state ) const final
40+
double total_log( const Configuration< Geometry >& state ) const final
4141
{
4242
const auto n = static_cast< double >( number_of_objects( state ) );
4343
return this->neg_log_parameter_.scale( n );
4444
}
4545

46-
double log_delta_add( const Configuration< Geometry >& state,
46+
double delta_log_add( const Configuration< Geometry >& state,
4747
const MarkedObject< Geometry >& sample ) const final
4848
{
4949
return this->neg_log_parameter_.scale( 1. );
5050
}
5151

52-
double log_delta_remove( const Configuration< Geometry >& state,
52+
double delta_log_remove( const Configuration< Geometry >& state,
5353
index_t sample_id ) const final
5454
{
5555
return this->neg_log_parameter_.scale( -1. );
5656
}
5757

58-
double log_delta_change( const Configuration< Geometry >& state,
58+
double delta_log_change( const Configuration< Geometry >& state,
5959
index_t old_sample_id,
6060
const MarkedObject< Geometry >& new_sample ) const final
6161
{

include/geode/stochastic/sampling/mcmc/models/components/pairwise_term.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ namespace geode
3939
{
4040
}
4141

42-
double log_total( const Configuration< Geometry >& state ) const final
42+
double total_log( const Configuration< Geometry >& state ) const final
4343
{
4444
double interaction_weight = 0.0;
4545
for( const auto obj_id : geode::Range{ state.size() } )
@@ -54,7 +54,7 @@ namespace geode
5454
return this->neg_log_parameter_.scale( interaction_weight );
5555
}
5656

57-
double log_delta_add( const Configuration< Geometry >& state,
57+
double delta_log_add( const Configuration< Geometry >& state,
5858
const MarkedObject< Geometry >& new_object ) const final
5959
{
6060
double interaction_weight = 0.0;
@@ -66,7 +66,7 @@ namespace geode
6666
return this->neg_log_parameter_.scale( interaction_weight );
6767
}
6868

69-
double log_delta_remove( const Configuration< Geometry >& state,
69+
double delta_log_remove( const Configuration< Geometry >& state,
7070
index_t object_id ) const final
7171
{
7272
const auto& removed = state[object_id];
@@ -83,7 +83,7 @@ namespace geode
8383
return this->neg_log_parameter_.scale( -interaction_weight );
8484
}
8585

86-
double log_delta_change( const Configuration< Geometry >& state,
86+
double delta_log_change( const Configuration< Geometry >& state,
8787
index_t old_object_id,
8888
const MarkedObject< Geometry >& new_object ) const final
8989
{

include/geode/stochastic/sampling/mcmc/models/gibbs_energy.hpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,47 +66,47 @@ namespace geode
6666
return vector_string( values );
6767
}
6868

69-
double log_energy_total( const Configuration< Geometry > state )
69+
double total_log_energy( const Configuration< Geometry > state ) const
7070
{
7171
double log_energy{ 0.0 };
7272
for( const auto& term : energy_terms_ )
7373
{
74-
log_energy += term->log_total( state );
74+
log_energy += term->total_log( state );
7575
}
7676
return log_energy;
7777
}
7878

79-
double log_energy_delta_add( const Configuration< Geometry > state,
80-
const MarkedObject< Geometry >& sample )
79+
double delta_log_energy_add( const Configuration< Geometry > state,
80+
const MarkedObject< Geometry >& sample ) const
8181
{
8282
double log_energy{ 0.0 };
8383
for( const auto& term : energy_terms_ )
8484
{
85-
log_energy += term->log_delta_add( state, sample );
85+
log_energy += term->delta_log_add( state, sample );
8686
}
8787
return log_energy;
8888
}
8989

90-
double log_energy_delta_remove(
91-
const Configuration< Geometry > state, index_t sample_id )
90+
double delta_log_energy_remove(
91+
const Configuration< Geometry > state, index_t sample_id ) const
9292
{
9393
double log_energy{ 0.0 };
9494
for( const auto& term : energy_terms_ )
9595
{
96-
log_energy += term->log_delta_remove( state, sample_id );
96+
log_energy += term->delta_log_remove( state, sample_id );
9797
}
9898
return log_energy;
9999
}
100100

101-
double log_energy_delta_change( const Configuration< Geometry > state,
101+
double delta_log_energy_change( const Configuration< Geometry > state,
102102
index_t old_sample_id,
103-
const MarkedObject< Geometry >& new_sample )
103+
const MarkedObject< Geometry >& new_sample ) const
104104
{
105105
double log_energy{ 0.0 };
106106
for( const auto& term : energy_terms_ )
107107
{
108108
log_energy +=
109-
term->log_delta_change( state, old_sample_id, new_sample );
109+
term->delta_log_change( state, old_sample_id, new_sample );
110110
}
111111
return log_energy;
112112
}

0 commit comments

Comments
 (0)