Implement feature: adjustable maximum displacement in Monte-Carlo#52
Implement feature: adjustable maximum displacement in Monte-Carlo#52Luthaf merged 3 commits intolumol-org:masterfrom
Conversation
src/core/src/sim/mc/moves/mod.rs
Outdated
| /// Current sample range | ||
| pub delta_range: Range<f64>, | ||
| /// Number of move calls | ||
| pub ncalls: u64, |
There was a problem hiding this comment.
What is the difference between ncall and nattempted?
There was a problem hiding this comment.
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.
src/core/src/sim/mc/moves/mod.rs
Outdated
| /// Current maximum displacement | ||
| delta: f64, | ||
| /// Current sample range | ||
| pub delta_range: Range<f64>, |
src/core/src/sim/mc/moves/mod.rs
Outdated
| } | ||
| } | ||
|
|
||
| /// Change `update_frequency` to zero, which will prevent further updates. |
There was a problem hiding this comment.
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_frequencyis done with a non zero value
There was a problem hiding this comment.
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.
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.
src/core/src/sim/mc/moves/mod.rs
Outdated
| 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.
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.
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.
src/core/src/sim/mc/moves/rotate.rs
Outdated
|
|
||
| use std::usize; | ||
| use std::f64; | ||
| use std::f64::consts::{PI}; |
src/core/src/sim/mc/moves/rotate.rs
Outdated
| /// 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.
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.
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.
Yes, this feels more clean to me.
src/core/src/sim/mc/moves/rotate.rs
Outdated
| /// 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.
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:
|
Luthaf
left a comment
There was a problem hiding this comment.
I like this version better!
I just have some comments about naming improvement, tell me what you think about them.
src/core/src/sim/mc/monte_carlo.rs
Outdated
| 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.
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?
src/core/src/sim/mc/monte_carlo.rs
Outdated
| 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.
The target parameter should be a f64 here. there is already a function for passing None.
src/core/src/sim/mc/moves/mod.rs
Outdated
| 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.
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?
src/core/src/sim/mc/moves/rotate.rs
Outdated
| theta: f64, | ||
| /// Range distribution, for generation of the angle | ||
| angle_rng: Range<f64>, | ||
| theta_range: Range<f64>, |
src/core/src/sim/mc/moves/rotate.rs
Outdated
|
|
||
| return rotation; | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Please leave an empty new line at the end of the files.
| /// Maximum displacement value | ||
| delta: f64, | ||
| /// Translation range for random number generation | ||
| delta_range: Range<f64>, |
| /// 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.
I think you need to keep this division by sqrt(3) to ensure that the vector norm will be lower than the given dr.
| } | ||
|
|
||
| self.delta = Vector3D::new( | ||
| let displace_vec = Vector3D::new( |
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 inputcrate.I'd appreciate feedback/critique/suggestions and questions.