ChemicalEnvironments were designed for the purpose of making moves/changes to the chemical perception of a SMIRNOFF forcefield. In this notebook we show how to use the environments to make these types of changes.
Authors
In [1]:
    
#!/usr/bin/env python
# generic scientific/ipython header
from __future__ import print_function
from __future__ import division
import os, sys
import copy
import numpy as np
    
In [2]:
    
import openeye.oechem as oechem
    
In [3]:
    
from openforcefield.typing.chemistry import environment
    
In [4]:
    
chemGroups = [ ('ewg1', '[#7,#8!-1,#16!-1,F,Cl,Br,I]'),
               ('ewg1di', '[#7!X1,#8!X1,#16!X1]') ]
    
In [5]:
    
# specific torsions from SMIRKS
torsionEnv1 = environment.TorsionChemicalEnvironment( smirks = "[#6X4:1]~[#6X4:2]-[#8X2;H1;+0:3]~[*:4]" )
torsionEnv2 = environment.TorsionChemicalEnvironment( smirks = "[#8X2;H0;+0:1]~[#6X4:2]-[#8X2;H1;+0:3]~[*:4]" )
torsionEnv3 = environment.TorsionChemicalEnvironment( smirks = "[#8X2;H0;+0:1]~[#6X4:2]-[#8:3]~[*:4]" )
print( torsionEnv3.asSMIRKS() )
    
    
In [6]:
    
# specific angles from sublists 
angEnv1 = environment.AngleChemicalEnvironment( smirks = "[#8X2;H0;+0:1]~[#6X4:2]-[#8X2;H1;+0:3]")
angEnv2 = environment.AngleChemicalEnvironment( smirks = "[#6X4:1]~[#6X4:2]~[#6X4:3]")
angEnv3 = environment.AngleChemicalEnvironment( smirks = "[#6X4:1]~[#6X4:2]~;!@[#6X4:3]")
print( angEnv3.asSMIRKS() )
    
    
In [7]:
    
# specific bonds from sublists 
bondEnv1 = environment.BondChemicalEnvironment( smirks = "[#8X2;H0;+0:1]~[#6X4:2]" )
bondEnv2 = environment.BondChemicalEnvironment( smirks = "[#8X2;H0;+0:1]~;!@[#6X4:2]" )
bondEnv3 = environment.BondChemicalEnvironment( smirks = "[H:1]-[#6X4:2]" )
bondEnv4 = environment.ChemicalEnvironment( smirks = '[*:2]~[#8:1]-[#6X4]~[#8X2;H0;+0]' )
print( bondEnv4.asSMIRKS() )
    
    
In [8]:
    
# specific vdW from sublists 
atomEnv1 = environment.AtomChemicalEnvironment( smirks = "[#8X2;H0;+0:1]")
atomEnv2 = environment.AtomChemicalEnvironment( smirks = "[#6X4:1]")
atomEnv3 = environment.AtomChemicalEnvironment( smirks = "[H:1]")
atomH3 = environment.ChemicalEnvironment( 
    smirks = '[#1:1]-[#6](-[$ewg1])(-[$ewg1])-[$ewg1]', replacements = chemGroups)
print( atomH3.asSMIRKS() )
    
    
In [9]:
    
# Testing API for retrieval of atoms and bonds
# This will be used to make moves in chemical space (change the environment)
torsion = torsionEnv3
print( torsion.asSMIRKS() )
print( torsion.atom1.getORtypes() )
print( torsion.atom1.getANDtypes() )
atom4 = torsion.selectAtom( 4)
print('Selected atom : ',atom4.index, atom4.getORtypes(), atom4.getANDtypes() )
atmlist = torsion.getAtoms()
for atom in atmlist:
    print(atom.index, atom.getORtypes(), atom.getANDtypes() )
bond2 = torsion.getBond(torsion.atom2,torsion.atom3)
print( bond2.getORtypes() )
print( bond2.getANDtypes() )
    
    
In [10]:
    
param = bondEnv4
print( param.asSMIRKS() )
# bond1 is the bond between indexed atoms 1 and 2.
# You can get this bond directly with selectBond:
print("Finding bond 1 directly...")
bond1 = param.selectBond(1)
print(bond1.getORtypes())
print("\nFinding bond 1 from atom1 and atom2...")
# OR you can get this bond from the atoms on either side:
firstAtom = param.selectAtom(1)
secondAtom = param.selectAtom(2)
print( 'firstAtom', firstAtom.index, firstAtom.getORtypes(), firstAtom.getANDtypes())
print( 'secondAtom', secondAtom.index, secondAtom.getORtypes(), secondAtom.getANDtypes())
bond1 = param.getBond(firstAtom, secondAtom)
print(bond1.getORtypes())
    
    
In [11]:
    
# Generic move lists without odds (initial guess), reformat below
ChemComponents = {}
ChemComponents['AtmBase'] = ['','#1','#5','#6','#7','#8','#9','#15','#16','#17','#35','#53']
ChemComponents['AtmORdecs'] = ['','X4','X3','X2','X1']
ChemComponents['AtmAndDecs'] = ['H0','+0',]
ChemComponents['BondBase'] = ['-',':','=','#','~']
ChemComponents['BondANDDecs'] = ['@','!@','!#']
    
In [12]:
    
# To reformat ChemComponents dict as tuples representing odds for each item,
# uncomment and run this block.
#for key in ChemComponents.keys():
#    print('ChemComponentsWithOdds[\'%s\'] = [' % key)
#    for item in ChemComponents[key]:
#        print(' (\'%s\', 1),' % item )
#    print(' ]')
    
Decorators need weights, and they need to be weighted differently for different atomic elements. Guessed examples based on experience with organic chemistry:
In [13]:
    
# General
atomComponentsWithOdds = {}
atomComponentsWithOdds['Basetypes'] = [
 ('#1', 10),
 ('#5', 10),
 ('#6', 10),
 ('#7', 10),
 ('#8', 10),
 ('#9', 1),
 ('#15', 2),
 ('#16', 4),
 ('#17', 1),
 ('#35', 1),
 ('#53', 1),
 ]
atomComponentsWithOdds['ORdecs'] = [ ('', 4), 
                                       ('X4', 4), ('X3', 4), ('X2', 4), ('X1', 4), 
                                       ('H3', 1), ('H2', 1), ('H1', 1), ('H0', 1)
                                      ]
# these AtmAndDecs are dummies just to get going; real chemistry puts H-count on each atom
atomComponentsWithOdds['ANDdecs'] = [ ('H0', 1), ('+0', 1), ]
bondComponentsWithOdds = {}
bondComponentsWithOdds['Basetypes'] = [ ('-', 1), (':', 1), ('=', 1), ('#', 1), ('~', 1), ]
bondComponentsWithOdds['ORdecs'] = []
bondComponentsWithOdds['ANDdecs'] = [ ('@', 1), ('!@', 1), ('!#', 1), ]
    
In [14]:
    
# for AlkEthOH
atomComponentsWithOdds = {}
atomComponentsWithOdds['Basetypes'] = [
 ('#1', 1),
 ('#5', 0),
 ('#6', 1),
 ('#7', 0),
 ('#8', 1),
 ('#9', 0),
 ('#15', 0),
 ('#16', 0),
 ('#17', 0),
 ('#35', 0),
 ('#53', 0),
 ]
atomComponentsWithOdds['ORdecs'] = [ ('', 4), 
                                       ('X4', 4), ('X3', 4), ('X2', 4), ('X1', 4), 
                                       ('H3', 1), ('H2', 1), ('H1', 1), ('H0', 1)
                                      ]
# these AtmAndDecs are dummies just to get going; real chemistry puts H-count on each atom
atomComponentsWithOdds['ANDdecs'] = [ ('H0', 1), ('+0', 1), ]
bondComponentsWithOdds = {}
bondComponentsWithOdds['Basetypes'] = [ ('-', 1), (':', 0), ('=', 0), ('#', 0), ('~', 1), ]
# TODO: determine how to handle ~ case, do we want to have it in the list or have a None option of some kind?
bondComponentsWithOdds['ORdecs'] = []
bondComponentsWithOdds['ANDdecs'] = [ ('@', 1), ('!@', 1), ('!#', 0), ]
    
In [15]:
    
# function copied from EnvMovesTypesAndWeights
def movesWithWeightsFromOdds( MovesWithOdds):
    '''Processes a dictionary of movesWithOdds (lists of string/integer tuples)
    into a dictionary of movesWithWeights usable to perform weighted
    random choices with numpy's random.choice() function.
    Argument: a MovesWithOdds dictionary of lists of string/integer tuples
    Returns: a MovesWithWeights dictionary of pairs of a moveType-list with a 
            probabilites-list, the latter used by numpy's random.choice() function.'''
    movesWithWeights = {}
    for key in MovesWithOdds.keys():
        moves = [ item[0] for item in MovesWithOdds[key] ]
        odds =  [ item[1] for item in MovesWithOdds[key] ]
        weights = odds/np.sum(odds)
        #print( key, moves, odds, weights)
        movesWithWeights[key] = ( moves, weights)
    return movesWithWeights
    
In [16]:
    
#print( ChemComponentsWithWeights)
atomComponentsWithWeights = movesWithWeightsFromOdds(atomComponentsWithOdds)
#print( 'atomComponentsWithWeights', atomComponentsWithWeights)
bondComponentsWithWeights = movesWithWeightsFromOdds(bondComponentsWithOdds)
#print( 'bondComponentsWithWeights', bondComponentsWithWeights)
masterComponentsWithWeights = { 'atom':atomComponentsWithWeights,
                                'bond':bondComponentsWithWeights}
#print( 'masterComponentsWithWeights', masterComponentsWithWeights)
    
In [17]:
    
def EnvMoveIsWellFormed( moveDict, msg=''):
    '''Checks moveDict (dict of proposed chem env micro-moves) to see if
    it is well-formed (i.e. before even looking at the chemical graph),
    returning True unless there is an obvious problem.
    Arguments: 
        moveDict: a dict of strings constituting proposed micro-moves of a chem env move.
        msg: an informative message about the nature of the problem.
    Returns True unless it finds a problem'''
    if moveDict['action']=='joinAtom':
        if moveDict['atomOrBond']=='bond':
            msg = 'cannot join another atom to an existing bond'
            return False
        elif moveDict['ANDorOR']=='ANDtype':
            msg = 'can only join another atom as an ORtype'
            return False
    return True
    
In [18]:
    
def getMoveDictDB(moveDictFilename):
    """
    Processes moveTrees.uniq.*.txt files
    """
    moveDictFile = open(moveDictFilename)
    moveDictdb = []
    for line in moveDictFile:
        fields = line.split()
        prob = fields[0]
        moveDict = { 'action' : fields[1],
                   'atomOrBond' : fields[2],
                   'whichAtmBnd' : fields[3],
                   'ANDorOR' : fields[4],
                  }
        if EnvMoveIsWellFormed(moveDict):
            moveDictdb.append( (moveDict, prob) )
    moveDictFile.close()    
    return moveDictdb
    
In [19]:
    
moveDictdb_byParam = dict()
for param_name in ['VdW', 'Bond', 'Angle', 'Torsion', 'Improper']:
    moveDictdb_byParam[param_name] = getMoveDictDB("moveTrees.uniq.%s.txt" % param_name)
    print(param_name, len(moveDictdb_byParam[param_name]))
    
    
In [20]:
    
# function copied from EnvMovesTypesAndWeights
def PickMoveItemWithProb( moveType, moveWithWeights):
    '''
    Picks a moveItem based on a moveType and a dictionary of moveTypes with associated probabilities
    
    Parameters
    ----------
        moveType: string corresponding to a key in the moveWithWeights dictionary, e.g. atomTor
        moveWithWeights: a dictionary based on moveType keys which each point to a list of probabilites
           associated with the position in the list
    Returns
    -------
        the randomly-chosen position in the list, based on the probability, together with the probability'''
    listOfIndexes = range(0, len( moveWithWeights[moveType][1]) )
    listIndex = np.random.choice(listOfIndexes, p= moveWithWeights[moveType][1])
    return moveWithWeights[moveType][0][listIndex], moveWithWeights[moveType][1][listIndex]
    
In [40]:
    
def GenerateORtype( ComponentsWithWeights):
    '''Makes a weighted random choice of a new ORtype  for an Atom or Bond from lists of candidate components.
    These ORtypes are composites of a basetype (atomic number for atom, bond order for bond) and
    an ORdec (decorator) which can associate a property (such as 'X3' meaning 3 connections on
    the atom). It also calculates and associates the cumulative probability for that composite
    ORtype based on the probabilities of each component used in making the choice.
    
    Parameters
    ----------
        ComponentsWithWeights: a MovesWithWeights dictionary of pairs of a moveType-list
            with a probabilites-list
    
    Returns
    -------
        newORtype, prob: a tuple pair with first element being a new ORtype and the second
            being the cumulative probability of that ORtype.
        None: if the attempt fails
    '''
    basetypeList = ComponentsWithWeights['Basetypes'][0]
    #print( 'Basetypes:', ComponentsWithWeights['Basetypes'][0])
    #print( 'Basetypes weights:', ComponentsWithWeights['Basetypes'][1])
    if len(basetypeList)<1:
        return None
    newBasetype, prob = PickMoveItemWithProb( 'Basetypes', ComponentsWithWeights)
    cumProb = prob
    
    ORtypeList = ComponentsWithWeights['ORdecs'][0]
    #print( 'ORdecs:', ORtypeList)
    #print( 'ORdecs weights:', ComponentsWithWeights['ORdecs'][1])
    if len(ORtypeList)<1:
        return (newBasetype, []), prob
    newORdecorator, prob = PickMoveItemWithProb( 'ORdecs', ComponentsWithWeights)
    newORtype = (newBasetype, [newORdecorator])
    cumProb *= prob
    return newORtype, prob
    
In [41]:
    
def GenerateANDtype( ComponentsWithWeights):
    '''Makes a weighted random choice of a new ANDtype for an Atom or Bond from  a list of candidate
    components.It also calculates and associates the cumulative probability for that
    composite ANDtype based on the probabilities of each component used in making the choice.
    
    Parameters
    ----------
        ComponentsWithWeights: a MovesWithWeights dictionary of pairs of a moveType-list
            with a probabilites-list
            
    Returns
    -------
        newANDtype, prob: a tuple pair with first element being a new ANDtype and the second
            being the cumulative probability of that ANDtype.
        None: if the attempt fails
    '''
    ANDtypeList = ComponentsWithWeights['ANDdecs'][0]
    #print( 'ANDdecs:', ANDtypeList)
    #print( 'ANDdecs weights:', ComponentsWithWeights['ANDdecs'][1])
    if len(ANDtypeList)<1:
        return None
    return PickMoveItemWithProb( 'ANDdecs', ComponentsWithWeights)
    
In [42]:
    
# test GenerateNwxfragORtype and GenerateNwxfragANDtype
nSamples = 3
for i in range(0,nSamples):
    print( 'atom ORtypes:', GenerateORtype( masterComponentsWithWeights['atom']) )
for i in range(0,nSamples):
    print( 'atom ANDtypes:', GenerateANDtype( masterComponentsWithWeights['atom']) )
for i in range(0,nSamples):
    print( 'bond ORtypes:', GenerateORtype( masterComponentsWithWeights['bond']) )
for i in range(0,nSamples):
    print( 'bond ANDtypes:', GenerateANDtype( masterComponentsWithWeights['bond']) )
    
    
In [43]:
    
def GetEnvBondChangeling( moveDict, Env):
    '''
    Returns a Bond object from the provided ChemicalEnvironment containing ORtypes and
    ANDtypes. Whether a specific bond object or a randomly chosed one gets returned is based
    on information in the moveDict (specifically moveDict['whichAtmBnd']).
    If no bond object matching the moveDict requirements is found, it returns None.
    
    Parameters
    -----------
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves
        Env: An openforcefield chemicalEnvironment
        
    Returns
    -------
    an OpenForcefield ChemicalEnvironment bond object or None
    
    Note - when this method was originally written selecting bonds was more difficult!
    '''
    if moveDict['atomOrBond']!='bond':
        print( 'GetEnvBondChangeling: mistaken call to this function')
        return None
    # MoveDicts have two options at whichAtmBnd 'bond' or 'atom' followed by a number 
    # or the word unindexed
    try:
        index = int(moveDict['whichAtmBnd'][-1])
        return Env.selectBond(index) # returns None if there is no bond at that index
    
    except: # moveDict['whichAtmBnd'] should be the word unindexed
        return Env.selectBond(moveDict['whichAtmBnd'])
    
In [44]:
    
# get MoveDictdb for this parameter:
moveDictdb = moveDictdb_byParam['Bond']
# test GetEnvBondChangeling
startParam = environment.ChemicalEnvironment( '[*:2]!@[#8:1]-[#6X3]=[#8X1;H0;+0]' )
param = copy.deepcopy( startParam)
moveDict = moveDictdb[9][0]
print( param.asSMIRKS())
print( moveDict)
changeling = GetEnvBondChangeling( moveDict, param)
if changeling is not None:
    print( 'changeling.getORtypes():', changeling.getORtypes() )
else:
    print( "No bond found with those requirements")
    
    
In [45]:
    
def GetEnvAtomChangeling( moveDict, Env):
    '''Returns an Atom object from the provided ChemicalEnvironment.
    Whether a specific atom object or a randomly chosed one gets returned is based
    on information in the moveDict (specifically moveDict['whichAtmBnd']).
    If no atom object is found, it returns None.
    
    Parameters
    -----------
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves
        Env: Openforcefield ChemicalEnvironment
        
    Returns
    -------
        Atom: an atom from the Env matching moveDict requirements 
        (or None if no atom with specifications was found)
    '''
    if moveDict['atomOrBond']!='atom':
        print( 'GetEnvAtomChangeling: mistaken call to this function')
        return None
    try:
        index = int(moveDict['whichAtmBnd'][-1])
        return Env.selectAtom( index )
    except:
        return Env.selectAtom( moveDict['whichAtmBnd'] )
    
In [46]:
    
def GetEnvChangeling( moveDict, Env):
    '''
    Parent function to broker the getting of an Atom or Bond from the specified ChemicalEnvironment
    Arguments
    ----------
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves
        Env: Openforcefield ChemicalEnvironment
    
    Returns
    --------
        changeline: an atom or bond meeting requirements in the moveDict
    '''
    atomOrBond = moveDict['atomOrBond']
    if atomOrBond=='atom':
        return GetEnvAtomChangeling( moveDict, Env)
    elif atomOrBond=='bond':
        return GetEnvBondChangeling( moveDict, Env)
    else:
        return None
    
In [47]:
    
# test GetEnvChangeling
startParam = atomH3
param = copy.deepcopy( startParam)
moveDict = moveDictdb[5][0]
print( 'Attempting to apply this proposed move:\n', moveDict )
print( 'To a ChemicalEnvironment that generates this SMIRKS string:\n', param.asSMIRKS() )
#for key in moveDict.keys():
#    print( key, ':', moveDict[key] )
changeling = GetEnvChangeling( moveDict, param)
if changeling!=None:
    print( 'changeling properties:', changeling.getORtypes(), changeling.getANDtypes() )
else:
    print( 'GetEnvChangeling returned none. Was the proposed move viable?')
    
    
In [48]:
    
def ProposeNewType( moveDict, ComponentsWithWeights):
    '''Proposes a new ORtype or ANDtype for a bond or atom object  
    based on a weighted random choice from  a list of
    candidate components. It also calculates and associates the cumulative probability
    for that choice based on the probabilities of the component used in making the choice.
    If the attempt to propose does not work, it returns None.
    Arguments:
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves
        ComponentsWithWeights: a MovesWithWeights dictionary of pairs of an ORtype or
            ANDtype components with a probabilites-list.
    Returns: 
        newType, prob: a tuple pair with first element being the proposed new type and
            the second being the cumulative probability of that ORtype.
        None: if the attempt fails
    '''
    #print( 'ProposeNewType moveDict:', moveDict )
    atomOrBond = moveDict['atomOrBond']
    if moveDict['ANDorOR']=='ORtype':
        newPair = GenerateORtype( ComponentsWithWeights[atomOrBond])
    elif moveDict['ANDorOR']=='ANDtype':
        newPair = GenerateANDtype( ComponentsWithWeights[atomOrBond])
    return newPair
    
In [49]:
    
# test ProposeNewType
for moveDict in moveDictdb:
    print( moveDict[0]['atomOrBond'], moveDict[0]['ANDorOR'], ':',
         ProposeNewType( moveDict[0], masterComponentsWithWeights))
    
    
In [50]:
    
def IsActionViable( moveDict, existing, proposal, msg):
    '''Checks the viability a proposed chemical move against the existing content of
    the bond or atom in the ChemicalEnvironment the move is to
    operate upon. Depending on the nature of the move and the changeling (atom or bond) object, the
    proposed move may be a priori impossible (e.g. deleting an ANDtype when the
    ANDtype list is already empty). If such a circumstance is found, this function
    returns False with a message. Otherwise it returns true with an empty string
    for the message.
    
    Parameters
    ----------
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves.
        existing: The Atom or Bond's existing ORtype or ANDtype list of strings targeted by the move in moveDict.
        proposal: a string; the proposed change to the existing ORtype or ANDtype list.
        msg: a string; a message stating why the move is deemed not viable.'''
    action = moveDict['action']
    #print('IsActionViable:  action:',action,'; existing types:',existing,'; proposed newType:',proposal)
    if action=='add':
        if proposal=='None':
            print('proposed newType is null; willnot add')
            return False
        if proposal in existing:
            print('proposed newType is in existing list; will not add')
            return False
    elif action=='delete':
        # ToDo: check to make sure not completely deleting whole atom which connects at least two others
        # Environments already do this - unnecessary
        if len(existing)<1:
            #msg = 'existing list is already empty; will not delete'
            #print('IsActionViable: msg is', msg)
            print('existing list is already empty; will not delete')
            return False
        if len(existing)==1 and moveDict['whichAtmBnd'][4]!='d':
            # 
            print('will not delete indexed atom or bond with only one type')
            return False
        if len(existing)==1 and moveDict['atomOrBond']=='bond':
            print('will not delete last bond joining two atoms')
            return False
    elif action=='swap':
        if len(existing)<1 or proposal=='None':
            print('missing one of existing type or newType; will not swap')
            return False
        if len(existing)==1 and existing[0]==proposal:
            print('Swapping identical types is pointless; will not swap')
            return False
    elif action=='joinAtom':
        # ToDo: insert valence check (don't join to atom with full valence)
            # I think this should also be inside the environments, but it would probably treat everything 
            # with the same max valence, such as 4? 6? might need special cses? 
        # ToDo: insert subst position check (don't join to beta substituent)
            # This is also in my environment TODO list, I think it makes more sense there
        if len(existing)<1 or proposal=='None':
            print('missing one of existing atom or new atom; will not join atom')
            return False
    return True
    
In [51]:
    
def SetEnvNewList( moveDict, changeling, newList):
    '''Makes the move on the selected ChemicalEnvironment atom or bond object 
    by setting the selected ORtype or ANDtype list.
    
    Parameters
    -----------
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves.
        changeling: the Atom or Bond object targeted by the move.
        newList: the new list of strings to replace the existing one in the changeling.
        
    Returns 
    -------
    True if the move is successful, False otherwise.
        '''
    if moveDict['ANDorOR']=='ORtype':
        changeling.setORtypes( newList )
        return True
    elif moveDict['ANDorOR']=='ANDtype':
        changeling.setANDtypes( newList)
        return True
    else:
        return False
    
In [52]:
    
def EffectMoveOnEnvList( moveDict, Env, changeling, existing, proposal):
    '''Effects the action requested in the chemical move on the selected Atom or Bond
    in the provided ChemicalEnvironment.
    Arguments:
        moveDict: a dict of strings constituting a sequence of proposed chem env micro-moves.
        Env: an OpenForcefield ChemicalEnvironment (representing a force field parameter)
        changeling: the Atom or Bond object in the Env targeted by the move.
        existing: the OpenForcefield Networkx molecular changeling object's 
            existing ORtype or ANDtype list of strings targeted by the move in moveDict.
        proposal: a string; the proposed change to the existing ORtype or ANDtype list.
    Returns: True if the move is successful, False otherwise.
        '''
    action = moveDict['action']
    newList = copy.deepcopy(existing)
    #
    # begin section for action delete and swap (swap is delete followed by add)
    # begin with special case for actually removing an atom completely, ie delete the last atom ORtype
    if action=='delete' and len(newList)==1 and moveDict['atomOrBond']=='atom'and moveDict['ANDorOR']=='ORtype':
        # for time being just remove the whole thing; worry about detailed balance later
        onlyEmpty = False
        return Env.removeAtom( changeling, onlyEmpty)
    
    # now the more general case for removing the last type from a list: make an empty list
    if action=='delete' or action=='swap':
        if len(newList)==1:
            newList = []
        # now to remove a random item from a list of more than one
        else:
            listOfIndexes = range(0, len( newList) )
            idxToDelete = np.random.choice(listOfIndexes)
            del newList[idxToDelete]
    #
    # begin section for action add and swap (swap is delete followed by add)
    if action=='add' or action=='swap':
        newList.append( proposal)
        if not SetEnvNewList( moveDict, changeling, newList):
            print('EffectMoveOnEnvList newList failed:', newList)
            return False
    #
    # begin section for joinAtom
    if action=='joinAtom':
        print('EffectMoveOnEnvList: attaching [', proposal,'] to existing atom:', existing)
        Env.addAtom( changeling, None, None, [proposal], None, None)
    return True
    
In [53]:
    
def MoveEnv( Env, moveDict, weightdParts, msg=''):
    '''Modifies a ChemicalEnvironment by applying a move described by
    a moveDict (dict of proposed chem env micro-moves), based on weighted random choices of
    chemical components in weightdParts. If the proposed move is not viable, it returns False.
    Arguments:
        Env: an OpenForcefield ChemicalEnvironment (representing a force field parameter)
        moveDict: a list of strings constituting a sequence of proposed chem env micro-moves
        weightdParts: dictionary of chemical components with probabilities of being chosen
        msg: an informative message about the nature of the problem
    Returns True unless it finds a problem'''
    print( Env.asSMIRKS(), moveDict )
    
    # stage 1: get the atom or bond we are going to work on
    atomOrBond = moveDict['atomOrBond']
    changeling = GetEnvChangeling( moveDict, Env)
    if changeling==None:
        msg = 'MoveEnv: changeling is None, returning false'
        return False
    print( 'MoveEnv changeling:', changeling.getORtypes(), changeling.getANDtypes() )
    
    # stage 2: get the current list we need to modify
    ANDorOR = moveDict['ANDorOR']
    if ANDorOR=='ORtype':
        currlist = changeling.getORtypes()
        print('MoveEnv got ORtypes:', currlist)
    elif ANDorOR=='ANDtype':
        currlist = changeling.getANDtypes()
        print('MoveEnv got ANDtypes:', currlist)
    else:
        msg = 'MoveEnv: could not retrieve requested ORtypes or ANDtypes'
        return False
    
    # stage 3: get the component list and associated weights
    proposed = ProposeNewType( moveDict, masterComponentsWithWeights)
    print("Proposed list: ", proposed)
    if proposed==None:
        msg = 'MoveEnv: proposed (newType, probability) is None, returning false'
        return False
    
    #stage 4: Test viability of desired action
    mesg = ''
    IsViable = IsActionViable( moveDict, currlist, proposed[0], mesg)
    if not IsViable:
        msg = 'MoveEnv: proposal is not viable, returning false. Details:\n'+mesg
        return False
    
    #stage 5: perform action
    if not EffectMoveOnEnvList( moveDict, Env, changeling, currlist, proposed[0]):
        msg = 'MoveEnv: problem effecting the move'
        return False
    
    return True
    
In [54]:
    
# test MoveEnv
startParam = angEnv2
param = copy.deepcopy( startParam)
moveDict = moveDictdb[6][0]
#print( param.asSMIRKS(), moveDict )
msg = ''
if not MoveEnv( param, moveDict, masterComponentsWithWeights, msg):
    print('move failed, message is:', msg)
else:
    print('move worked, new SMIRKS is:', param.asSMIRKS())
    
    
In [57]:
    
# test MoveEnv
startParam = angEnv2
moveDictdb = moveDictdb_byParam['Angle']
param = copy.deepcopy( startParam)
print( 'Starting with', param.asSMIRKS())
#print( len(moveDictdb) )
for i, moveDict in enumerate( moveDictdb):
    param = copy.deepcopy( startParam)
    moves =  moveDict[0]
    print( '\nworking on %2d %8s %4s %10s %7s :' % 
          (i, moves['action'], moves['atomOrBond'], moves['whichAtmBnd'], moves['ANDorOR']))
    if not MoveEnv( param, moves, masterComponentsWithWeights, msg):
        print('  Failed')
    else:
        print('  Succeeded : %s' % ( param.asSMIRKS()))
    
    
In [ ]: