This is a continuation of discussion from #251. Where that left off, I think we'd agreed that we should separate acceptance from generation of trials in our Monte Carlo. Here we should discuss details on the implementation of that.
One thing we'll have to keep coming back to is the question of detailed balance. Maintaining detailed balance MUST by the job of the PathMoveAcceptor. New tools implemented as part of MoveScheme should make that easier to accomplish.
Detailed balance
Fundamentally, we need to know the probability of generating the trial state from the original state and vice versa. Call these P_o_n and P_n_o. At each decision point in the move decision tree, we modify these probabilities. Note that we can mark some level of the move tree as the "official mover", and these probabilities split into a probability of choosing a given "official mover" and the probability of making the change within that "official mover." I will call the probability of choosing a specific "official mover" the "choice probability". I will call the probability of generating a given trial from a given initial condition using that official mover the "trial generation probability". The product of these two gives us P_o_n or P_n_o.
Since we're usually interested in the ratio of P_o_n and P_n_o, we can often obtain shortcuts by directly calculating that ratio. However, we should leave other possibilities open as well.
Each path through the move decision tree has a "balance partner". These partners are pairs that can satisfy microscopic reversibility. If a given mover is used to calculate P_o_n, then you must use its balance partner to calculate P_n_o. Note that in many cases (shooting, replica exchange) a mover is its own balance partner: basically, any time the input ensembles equal the output ensembles. However, in cases like ensemble hopping, a different mover is the balance partner.
In principle, a mover can have more than one balance partner. For now, we restrict it to only one.
Proposal: where to put the "official movers" split
In MoveScheme, I've defined groups of movers based on what we'd normally consider the move type, such as "shooting", "replica exchange", "path reversal", "minus move", etc. I propose that the movers included in these lists be the "official movers" in the sense discussed above.
Proposal: how to find the "choice probability" and "balance partners"
For the root mover
Using the "official mover" split I describe above, the MoveScheme automatically determines the choice probability and the balance partners for the root mover.
For bundled moves
If creating what I have previously termed a "bundled" move, we again need to define some "official submove" point on the subsequent subtree. My suggestion is that by default each (topmost) submove of the bundled move is to be considered the "official submove". In that case, each submove must be its own balance partner, and we only need to calculate the trial generation probability.
Note that if the user wants to create a much more complicated subtree within one of these moves, they can use the MoveScheme/MoveStrategy framework to do so. The you would end up with an acceptor from the bundler wrapping an acceptor from the MoveScheme: technically a bit wasteful, but it simplifies a lot. In the acceptor from the MoveScheme, all the choice probabilities and balance partners for the subtree would be set.
Without MoveSchemes
The MoveScheme provides an easy way to set the variables acceptor.choice_probability and acceptor.balance_partners. The user could also manually set these if desired. The only rules are to make it quack the same way MoveScheme does. Primarily:
choice_probability is a dictionary of { mover : probablity}, where probability is normalized
balance_partners is a dictionary of { mover : list_of_partners}, where currently list_of_partners must be length 1
Note that this does place the restriction that a given mover can only appear once at the level of "official mover" in the subtree. If that's a problem, you'll have to redefine where you set your "official mover". (And any user who is trying to do something that complicated, when we have all these easier approaches available, should be advanced enough to grasp the intricacies here.)
Proposal: how to find the trial generation probability
General case
The trial generation probability is just the product of the probability of each "decision" made along the way. Some of these probabilities can't be normalized to 1, so this isn't a true probability, but the key is that any two "probabilities" from the same function must have the same normalization constant, even if it isn't 1.
My suggestion is that this is implemented by ensuring that sufficient information is stored in the pmc to determine the probability. The process looks like this: from the root mover, we go down a path of the tree, picking a correct move. Then we return the pmc back up the same path (popping stack frames) until we hit an PathMoveAcceptor. From the acceptor, we can call official_mover.trial_probability(pmc, reverse=False) and self.balance_partners[official_mover][0].trial_probability(pmc, reverse=True), which, when multiplying the former by self.choice_probability[official_mover] and the latter by self.choice_probability[self.balance_partners[official_mover][0]], gives P_o_n and P_n_o, respectively.
Shortcut for trial_probability_ratio
In Metropolis MC, we really need the ratio of the two probabilities. And in some cases, the ratio might be easier to calculate than the individual values, since some terms may cancel out. We implement a function called pathmover.trial_probability_ratio(pmc) to do this. This acts the same as trial_probability in usage, except we only have the one call, and the individual parts can benefit from shortcuts.
The default implementation is, of course, to just call the top and bottom parts of the ratio independently, as described in the general case.
Since the acceptor decides whether to use the ratio version or not, we need to go back up the tree to it before figuring out the trial_probability stuff.
Required implementation for the proposal
- For every path mover, a function to calculate
trial_probability(pmc)
- Shortcuts for the
trial_probability_ratio(pmc) in all path movers we've implemented, using a direct call to trial_probability
PathMoveAcceptor (and separation of acceptance from trials in current)
MetropolisAcceptor as a specific subclass of PathMoveAcceptor: calls trial_probability_ratio of its root mover
- Modify
MoveScheme to take an acceptor=MetropolisAcceptor() argument; and scheme.move_decision_tree() should change root to wrap the current root in the acceptor
I haven't implemented any of this (except the helper tools in MoveScheme) but I think something like this will be the way to, with relative ease, implement this capability. The only part I haven't fully worked out is exactly how the trial_probability calculations work in each mover. This will require some changes to the shooting mover to handle this correctly; I think all the others only need relatively trivial additions.
This is a continuation of discussion from #251. Where that left off, I think we'd agreed that we should separate acceptance from generation of trials in our Monte Carlo. Here we should discuss details on the implementation of that.
One thing we'll have to keep coming back to is the question of detailed balance. Maintaining detailed balance MUST by the job of the
PathMoveAcceptor. New tools implemented as part ofMoveSchemeshould make that easier to accomplish.Detailed balance
Fundamentally, we need to know the probability of generating the trial state from the original state and vice versa. Call these
P_o_nandP_n_o. At each decision point in the move decision tree, we modify these probabilities. Note that we can mark some level of the move tree as the "official mover", and these probabilities split into a probability of choosing a given "official mover" and the probability of making the change within that "official mover." I will call the probability of choosing a specific "official mover" the "choice probability". I will call the probability of generating a given trial from a given initial condition using that official mover the "trial generation probability". The product of these two gives usP_o_norP_n_o.Since we're usually interested in the ratio of
P_o_nandP_n_o, we can often obtain shortcuts by directly calculating that ratio. However, we should leave other possibilities open as well.Each path through the move decision tree has a "balance partner". These partners are pairs that can satisfy microscopic reversibility. If a given mover is used to calculate
P_o_n, then you must use its balance partner to calculateP_n_o. Note that in many cases (shooting, replica exchange) a mover is its own balance partner: basically, any time the input ensembles equal the output ensembles. However, in cases like ensemble hopping, a different mover is the balance partner.In principle, a mover can have more than one balance partner. For now, we restrict it to only one.
Proposal: where to put the "official movers" split
In
MoveScheme, I've defined groups of movers based on what we'd normally consider the move type, such as "shooting", "replica exchange", "path reversal", "minus move", etc. I propose that the movers included in these lists be the "official movers" in the sense discussed above.Proposal: how to find the "choice probability" and "balance partners"
For the root mover
Using the "official mover" split I describe above, the
MoveSchemeautomatically determines the choice probability and the balance partners for the root mover.For bundled moves
If creating what I have previously termed a "bundled" move, we again need to define some "official submove" point on the subsequent subtree. My suggestion is that by default each (topmost) submove of the bundled move is to be considered the "official submove". In that case, each submove must be its own balance partner, and we only need to calculate the trial generation probability.
Note that if the user wants to create a much more complicated subtree within one of these moves, they can use the
MoveScheme/MoveStrategyframework to do so. The you would end up with an acceptor from the bundler wrapping an acceptor from theMoveScheme: technically a bit wasteful, but it simplifies a lot. In the acceptor from theMoveScheme, all the choice probabilities and balance partners for the subtree would be set.Without
MoveSchemesThe
MoveSchemeprovides an easy way to set the variablesacceptor.choice_probabilityandacceptor.balance_partners. The user could also manually set these if desired. The only rules are to make it quack the same wayMoveSchemedoes. Primarily:choice_probabilityis a dictionary of{ mover : probablity}, whereprobabilityis normalizedbalance_partnersis a dictionary of{ mover : list_of_partners}, where currentlylist_of_partnersmust be length 1Note that this does place the restriction that a given mover can only appear once at the level of "official mover" in the subtree. If that's a problem, you'll have to redefine where you set your "official mover". (And any user who is trying to do something that complicated, when we have all these easier approaches available, should be advanced enough to grasp the intricacies here.)
Proposal: how to find the trial generation probability
General case
The trial generation probability is just the product of the probability of each "decision" made along the way. Some of these probabilities can't be normalized to 1, so this isn't a true probability, but the key is that any two "probabilities" from the same function must have the same normalization constant, even if it isn't 1.
My suggestion is that this is implemented by ensuring that sufficient information is stored in the
pmcto determine the probability. The process looks like this: from the root mover, we go down a path of the tree, picking a correct move. Then we return thepmcback up the same path (popping stack frames) until we hit anPathMoveAcceptor. From the acceptor, we can callofficial_mover.trial_probability(pmc, reverse=False)andself.balance_partners[official_mover][0].trial_probability(pmc, reverse=True), which, when multiplying the former byself.choice_probability[official_mover]and the latter byself.choice_probability[self.balance_partners[official_mover][0]], givesP_o_nandP_n_o, respectively.Shortcut for
trial_probability_ratioIn Metropolis MC, we really need the ratio of the two probabilities. And in some cases, the ratio might be easier to calculate than the individual values, since some terms may cancel out. We implement a function called
pathmover.trial_probability_ratio(pmc)to do this. This acts the same astrial_probabilityin usage, except we only have the one call, and the individual parts can benefit from shortcuts.The default implementation is, of course, to just call the top and bottom parts of the ratio independently, as described in the general case.
Since the
acceptordecides whether to use theratioversion or not, we need to go back up the tree to it before figuring out thetrial_probabilitystuff.Required implementation for the proposal
trial_probability(pmc)trial_probability_ratio(pmc)in all path movers we've implemented, using a direct call totrial_probabilityPathMoveAcceptor(and separation of acceptance from trials in current)MetropolisAcceptoras a specific subclass ofPathMoveAcceptor: callstrial_probability_ratioof its root moverMoveSchemeto take anacceptor=MetropolisAcceptor()argument; andscheme.move_decision_tree()should changerootto wrap the current root in the acceptorI haven't implemented any of this (except the helper tools in
MoveScheme) but I think something like this will be the way to, with relative ease, implement this capability. The only part I haven't fully worked out is exactly how thetrial_probabilitycalculations work in each mover. This will require some changes to the shooting mover to handle this correctly; I think all the others only need relatively trivial additions.