-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement feature: adjustable maximum displacement in Monte-Carlo #52
Conversation
/// Current sample range | ||
pub delta_range: Range<f64>, | ||
/// Number of move calls | ||
pub ncalls: u64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between ncall
and nattempted
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nattempted
(as well as naccepted
) will be set to zero after an "successful" update. ncalls
continues to count. Currently, ncalls
is only used to check if the displacement should be updated (and the information about the total numbers of calls to the move). We could also use it in conjunction with a nequilibration
variable which automatically switches off updates after a certain number of trials.
We could eliminate ncalls
and compare the frequency directly to nattempts
in update
.
/// Current maximum displacement | ||
delta: f64, | ||
/// Current sample range | ||
pub delta_range: Range<f64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could just be called range
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
} | ||
} | ||
|
||
/// Change `update_frequency` to zero, which will prevent further updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also have a way to change the update frequency (pub fn update_frequency(&mut self, frequency: usize)
) here.
I would also prefer the documentation not to refer internal implementation details. How about:
Disable future updates of the move amplitude, until a call to
update_frequency
is done with a non zero value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that as well. The question is, where should the user call the function? After adding the move to MonteCarlo
and while running the simulation, the user currently has no control over the states of the moves.
I will change the documentation as you requested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be called before adding the move to the Monte-Carlo simulation. After adding it, you are right that there is no more control over the moves. See also how MonteCarlo::add
send an error if a move is added after the simulation is started.
let quotient = current_acceptance / self.target_acceptance; | ||
match quotient { | ||
_ if quotient > 1.5 => self.delta *= 1.5, | ||
_ if quotient < 0.5 => self.delta *= 0.5, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
50% seems to be a very big change. I have seen 10% in other software, do you have a specific reason to pick 50% ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, I use it in my code. It only limits the change though. We can easily run tests on what would be the most efficient number here (which would heavily depend on the system I guess).
With a large value, initially there will be very large changes which can lead to faster convergence to the target acceptance.
@@ -5,9 +5,11 @@ use rand::Rng; | |||
|
|||
use std::usize; | |||
use std::f64; | |||
use std::f64::consts::{PI}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: no need for brackets here
} | ||
|
||
impl Rotate { | ||
/// Create a new `Rotate` move, with maximum angular displacement of `theta`, | ||
/// rotating all the molecules in the system. | ||
pub fn new(theta: f64) -> Rotate { | ||
Rotate::create(theta, None) | ||
pub fn new(theta: f64, update_freq: u64, target_acceptance: f64) -> Rotate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not add all the parameters to the constructor, but rather use the default and have setters when deviating from the default.
Something like this:
pub fn new(theta: f64) -> Rotate {
// same as before, using Displacement::default for the initial value of displacement
}
pub fn set_target_acceptance(&mut self, target: f64) { /* ... */ }
pub fn set_update_frequency(&mut self, frequency: f64) { /* ... */ }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. You would then create a new move and set everything explicitly before adding it to the propagator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this feels more clean to me.
/// Returns a random unit vector, | ||
/// i.e. a random point on an unit sphere. | ||
/// @TODO: Maybe move this to types::Vector3D? | ||
fn random_unit_vector3d (rng: &mut Box<Rng>) -> Vector3D { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The algorithm changed here. Why not use the nice 3D normal distribution => uniform distribution on the R3 sphere feature?
From the design point of view, I still prefer to have the
I would go for something like the following. PS: This was going to be small, but turned into a huge pile of code. Ask me if you So we start with a counter for moves frequency: struct MoveCounter {
pub ncalls: u64,
pub naccepted: u64,
pub nattempted: u64,
target_acceptance: f64,
} Then the MCMove win a new method to update itself in a given direction pub enum UpdateDirection {
/// We need the acceptance to raise by the given ratio
More(f64),
/// We need the acceptance to lower by the given ratio
Less(f64),
/// The acceptance does not need to change
Same,
}
pub trait MCMove {
// ...
fn update_acceptance(&mut self, direction: UpdateDirection);
} Method that can be implemented like this impl MCMove for Translate {
// ...
fn update_acceptance(&mut self, direction: UpdateDirection) {
match direction {
UpdateDirection::Same => {return}
UpdateDirection::More(mut ratio) => {
debug_assert!(ratio > 1.0);
if ratio > 1.5 {ratio = 1.5}
self.delta *= ratio;
}
UpdateDirection::Less(mut ratio) => {
debug_assert!(ratio < 1.0);
assert!(ratio > 0.0);
if ratio < 0.5 {ratio = 0.5}
self.delta *= ratio;
}
}
self.range = Range(-self.delta, self.delta);
}
} And finally we can use this in the MonteCarlo struct pub struct MonteCarlo {
// ...
/// List of possible Monte-Carlo moves and associated counters
moves: Vec<(Box<MCMove>, MoveCounter)>,
/// Use the same update frequency for all the moves
update_frequency: u64,
/// Number of moves counter
nmoves: u64,
}
impl Propagator for MonteCarlo {
fn propagate(&mut self, system: &mut System) {
let mcmove = {
let probability = self.rng.next_f64();
// Get the index of the first move with frequency >= probability.
let (i, _) = self.frequencies.iter()
.enumerate()
.find(|&(_, f)| probability <= *f)
.expect("Could not find a move in MonteCarlo moves list");
&mut self.moves[i]
};
// Update the counters
mcmove.1.nattempted += 1;
self.nmoves += 1;
trace!("Selected move is '{}'", mcmove.0.describe());
if !mcmove.0.prepare(system, &mut self.rng) {
trace!(" --> Can not perform the move");
return;
}
let cost = mcmove.0.cost(system, self.beta, &mut self.cache);
trace!(" --> Move cost is {}", cost);
let accepted = cost <= 0.0 || self.rng.next_f64() < f64::exp(-cost);
if accepted {
// Update the counters
mcmove.1.naccepted += 1;
trace!(" --> Move was accepted");
mcmove.0.apply(system);
self.cache.update(system);
} else {
trace!(" --> Move was rejected");
mcmove.0.restore(system);
}
// Do the adjustments in moves amplitude as needed
if self.nmoves == self.update_frequency {
for mcmove in &mut self.moves {
let direction = mcmove.1.direction();
mcmove.0.update_acceptance(direction);
}
self.nmoves = 0;
}
}
} |
All you wrote makes sense. Shifting counting into
That is true, future moves could have all kinds of adjustable parameters (number of configurational bias moves, number of MD steps,...). But aren't you shifting the problem towards How do we progress? I could implement your suggestions and commit the updates. |
Yeah maybe. I think we need to pass the acceptance ratio, because some moves will need it. if other moves do not need the ratio, they always can ignore the value. But the fact that I needed to write
Go for it! We always can rebase this branch later to clean the git history. I'll also have some time on Tuesday evening if you need some help. |
Still WIP. Maybe you can have a look at the changes. I decided to compute the scaling factor for the updates as method of Other option: hand over the acceptance ratio and the target acceptance ... TODO:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this version better!
I just have some comments about naming improvement, tell me what you think about them.
self.frequencies.push(frequency); | ||
} | ||
|
||
pub fn add_with_acceptance(&mut self, mcmove: Box<MCMove>, frequency: f64, target: Option<f64>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the name of this function should start with add_move
, but I can not find a nice one. I though about add_move_with_acceptance
, add_move_auto_update
, ...
What do you think?
self.frequencies.push(frequency); | ||
} | ||
|
||
pub fn add_with_acceptance(&mut self, mcmove: Box<MCMove>, frequency: f64, target: Option<f64>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The target
parameter should be a f64 here. there is already a function for passing None
.
/// `prepare`. | ||
fn restore(&mut self, system: &mut System); | ||
|
||
/// Update the displacement | ||
fn change_range(&mut self, scaling_factor: Option<f64>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could have a better name, how about something like update_amplitude(&mut self, factor: Option<f64>)
.
Also, what does the Option means here? If it is None
, no changes should be performed?
EDIT: looks like this. Can you confirm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you are right.
/// Range distribution, for generation of the angle | ||
angle_rng: Range<f64>, | ||
theta_range: Range<f64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could just be range
@@ -153,4 +163,4 @@ fn rotation_matrix(axis: &Vector3D, angle: f64) -> Matrix3 { | |||
); | |||
|
|||
return rotation; | |||
} | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please leave an empty new line at the end of the files.
/// Translation vector | ||
delta: Vector3D, | ||
/// Maximum displacement value | ||
delta: f64, | ||
/// Translation range for random number generation | ||
delta_range: Range<f64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could just be range
too
pub fn with_moltype(dr: f64, moltype: u64) -> Translate { | ||
Translate::create(dr, Some(moltype)) | ||
} | ||
|
||
/// Factorizing the constructors | ||
fn create(dr: f64, moltype: Option<u64>) -> Translate { | ||
assert!(dr > 0.0, "dr must be positive in Translate move"); | ||
let dr = dr / f64::sqrt(3.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to keep this division by sqrt(3) to ensure that the vector norm will be lower than the given dr.
@@ -72,15 +71,15 @@ impl MCMove for Translate { | |||
return false; | |||
} | |||
|
|||
self.delta = Vector3D::new( | |||
let displace_vec = Vector3D::new( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming: displacement? delta?
What is wrong with a simple
This can be made in a separated PR. That was part of the idea for separating the crates, you do not always need to update all of them at the same time. |
I need to further work on the documentation. Skimming through the API doc, there is some confusing stuff. Also, I should add some examples. |
773c8d1
to
f5d8e52
Compare
So Travis is failing on the You can run the tests locally by doing |
You will also need to add a few unit tests for the codecov check to pass. For example, you can test that |
The codecov |
I'd be happy if you merge if you think the coverage is ok. |
Merged 🎉 ! |
This implementation addresses issue #23 .
There is still work to do to make the changes work with the
lumol input
crate.I'd appreciate feedback/critique/suggestions and questions.