Skip to content

Commit b6b03f4

Browse files
committed
- example script for explicit solvation with counterion
- calc_hessian_xtb: choose between xtbmethods - define_topology and write_pdbfile_openmm (resname can be chosen) - insert_solute_into_solvent: now 2 solutes can be inserted at the same time, convenient for counterions - geometric: re-enabled first and each Hessian options for TSOpt jobs - NEBTS: Enabled single-iter NEBTS with partial Hessian. Added keywords: partial_hessian_atoms and partial_npoint_choice
1 parent 42b7e42 commit b6b03f4

File tree

5 files changed

+251
-50
lines changed

5 files changed

+251
-50
lines changed

ash/interfaces/interface_geometric_new.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,15 @@ def hessian_option(self,fragment,actatoms,theory,charge,mult,modelhessian):
383383
elif "file:" in self.hessian:
384384
hessianfile = self.hessian.replace("file:","")
385385

386-
print("Checking that defined Hessian is compatible with active-region")
387-
hessian_read = read_hessian(hessianfile)
388-
print("actatoms:", actatoms)
389-
if hessian_read.shape[0] != 3*len(atomsused):
390-
print(f"Error: Hessian shape is {hessian_read.shape} which is incompatible with the number of active atoms present ({len(atomsused)})")
391-
print(f"Hessian should have dimension of 3*N x 3*N where N is the number of active-atoms of the system (should be : {3*len(atomsused)} x {3*len(atomsused)})")
392-
ashexit()
386+
#Allow first and each options still
387+
if self.hessian not in ['first','each']:
388+
print("Checking that defined Hessian is compatible with active-region")
389+
hessian_read = read_hessian(hessianfile)
390+
print("actatoms:", actatoms)
391+
if hessian_read.shape[0] != 3*len(atomsused):
392+
print(f"Error: Hessian shape is {hessian_read.shape} which is incompatible with the number of active atoms present ({len(atomsused)})")
393+
print(f"Hessian should have dimension of 3*N x 3*N where N is the number of active-atoms of the system (should be : {3*len(atomsused)} x {3*len(atomsused)})")
394+
ashexit()
393395

394396
elif self.hessian == None:
395397
if self.printlevel >= 1:

ash/interfaces/interface_knarr.py

+63-29
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def NEBTS(reactant=None, product=None, theory=None, images=8, CI=True, free_end=
7777
tol_turn_on_ci=1.0, runmode='serial', numcores=1, charge=None, mult=None, printlevel=1, ActiveRegion=False, actatoms=None,
7878
interpolation="IDPP", idpp_maxiter=700, idpp_springconst=5.0, restart_file=None, TS_guess_file=None, mofilesdir=None,
7979
OptTS_maxiter=100, OptTS_print_atoms_list=None, OptTS_convergence_setting=None, OptTS_conv_criteria=None, OptTS_coordsystem='tric',
80-
hessian_for_TS=None, modelhessian='unit', tsmode_tangent_threshold=0.1, subfrctor=1):
80+
hessian_for_TS=None, modelhessian='unit', tsmode_tangent_threshold=0.1, subfrctor=1,
81+
partial_hessian_atoms=None, partial_npoint_choice=2):
8182

8283
print_line_with_mainheader("NEB+TS")
8384
module_init_time=time.time()
@@ -181,8 +182,22 @@ def NEBTS(reactant=None, product=None, theory=None, images=8, CI=True, free_end=
181182
elif hessian_for_TS == 'each':
182183
hessianoption="each"
183184
#xTB Hessian option
184-
elif hessian_for_TS == 'xtb':
185-
hessianfile = calc_hessian_xtb(fragment=SP, runmode='serial', actatoms=actatoms, numcores=max_cores, use_xtb_feature=True)
185+
elif 'xtb' in hessian_for_TS.lower():
186+
print("xtB-type Hessian chosen (xtb found in string)")
187+
if 'gfn1' in hessian_for_TS.lower():
188+
print("Using GFN1-xTB Hessian")
189+
xtbmethod="GFN1"
190+
elif 'gfn2' in hessian_for_TS.lower():
191+
print("Using GFN2-xTB Hessian")
192+
xtbmethod="GFN2"
193+
else:
194+
print("Neither gfn1 or gfn2 was chosen (control this by choosing hessian_for_TS='GFN1-xTB' or hessian_for_TS='GFN2-xTB' )")
195+
print("Choosing xtbmethod to be GFN2-xTB by default")
196+
xtbmethod="GFN2"
197+
198+
199+
hessianfile = calc_hessian_xtb(fragment=SP, runmode='serial', actatoms=actatoms, numcores=max_cores, use_xtb_feature=True,
200+
xtbmethod=xtbmethod)
186201
hessianoption='file:'+str(hessianfile)
187202
#Cheap model Hessian
188203
#NOTE: None of these work well. Need to use tangent to modify
@@ -207,35 +222,54 @@ def NEBTS(reactant=None, product=None, theory=None, images=8, CI=True, free_end=
207222
# Add connecting atoms and erform partial Hessian optimization
208223
elif hessian_for_TS == 'partial':
209224
print("hessian_for_TS option: partial")
210-
print(f"Using climbing image tangent to find dominant atoms in approximate TS mode (tsmode_tangent_threshold={tsmode_tangent_threshold})")
211-
212-
#Getting tangent and atoms that contribute at least X to tangent where X is tsmode_tangent_threshold=0.1 (default)
213-
tangent = read_tangent("CItangent.xyz")
214-
TSmodeatoms = list(np.where(np.any(abs(tangent)>tsmode_tangent_threshold, axis=1))[0])
215-
#Convert activeregion atom indices to full system indices
216-
print("Determining atoms contributing the most to TS mode")
217-
if ActiveRegion is True:
218-
print("TSmodeatoms (active region):", TSmodeatoms)
219-
TSmodeatoms = [actatoms[a] for a in TSmodeatoms]
220-
print("TSmodeatoms (full system):", TSmodeatoms)
225+
print("Using partial Hessian to approximate Hessian for saddle-point optimization.")
226+
227+
228+
if Singleiter is True:
229+
print("This is a Singleiter NEBTS job.")
230+
if partial_hessian_atoms is None:
231+
print("Error: partial_hessian_atoms is not set")
232+
print("In Singleiter NEBTS jobs no climbing image tangent is available. Setting the partial_hessian_atoms list is necessary.")
233+
print("Example: partial_hessian_atoms=[15,16,18,19] where the numbers are atom indices believed to contribute the most to the TS mode")
234+
print("Exiting")
235+
ashexit()
236+
Final_partial_hessatoms=partial_hessian_atoms
237+
print(f"Performing partial Hessian calculation using atom-list: {Final_partial_hessatoms}")
221238
else:
222-
print("TSmodeatoms (full system):", TSmodeatoms)
223-
#Now finding the atoms that TSmodeatoms are connected to, for both R, P and SP
224-
print("Now finding connected atoms to TSmode-atoms")
225-
result_R = get_conn_atoms_for_list(fragment=reactant, atoms=TSmodeatoms)
226-
print("result_R:", result_R)
227-
result_P = get_conn_atoms_for_list(fragment=product, atoms=TSmodeatoms)
228-
print("result_P:", result_P)
229-
result_SP = get_conn_atoms_for_list(fragment=SP, atoms=TSmodeatoms)
230-
print("result_SP:", result_SP)
231-
#Combining
232-
Final_partial_hessatoms = np.unique(result_R + result_P + result_SP).tolist()
233-
234-
print(f"Performing partial Hessian calculation using atom-list: {Final_partial_hessatoms}")
235-
print("This corresponds to atoms contributing to the TS-mode and connected atoms")
239+
print("This is a regular NEBTS job. Partial Hessian can either be approximated from tangent or explicity set via partial_hessian_atoms keyword")
240+
if partial_hessian_atoms is None:
241+
print("partial_hessian_atoms list is empty. Will now try to use climbing image tangent to guess the important atoms")
242+
print(f"Using climbing image tangent to find dominant atoms in approximate TS mode (tsmode_tangent_threshold={tsmode_tangent_threshold})")
243+
244+
#Getting tangent and atoms that contribute at least X to tangent where X is tsmode_tangent_threshold=0.1 (default)
245+
tangent = read_tangent("CItangent.xyz")
246+
TSmodeatoms = list(np.where(np.any(abs(tangent)>tsmode_tangent_threshold, axis=1))[0])
247+
#Convert activeregion atom indices to full system indices
248+
print("Determining atoms contributing the most to TS mode")
249+
if ActiveRegion is True:
250+
print("TSmodeatoms (active region):", TSmodeatoms)
251+
TSmodeatoms = [actatoms[a] for a in TSmodeatoms]
252+
print("TSmodeatoms (full system):", TSmodeatoms)
253+
else:
254+
print("TSmodeatoms (full system):", TSmodeatoms)
255+
#Now finding the atoms that TSmodeatoms are connected to, for both R, P and SP
256+
print("Now finding connected atoms to TSmode-atoms")
257+
result_R = get_conn_atoms_for_list(fragment=reactant, atoms=TSmodeatoms)
258+
print("result_R:", result_R)
259+
result_P = get_conn_atoms_for_list(fragment=product, atoms=TSmodeatoms)
260+
print("result_P:", result_P)
261+
result_SP = get_conn_atoms_for_list(fragment=SP, atoms=TSmodeatoms)
262+
print("result_SP:", result_SP)
263+
#Combining
264+
Final_partial_hessatoms = np.unique(result_R + result_P + result_SP).tolist()
265+
print(f"Performing partial Hessian calculation using atom-list: {Final_partial_hessatoms}")
266+
print("This corresponds to atoms contributing to the TS-mode and connected atoms")
267+
else:
268+
Final_partial_hessatoms=partial_hessian_atoms
269+
print(f"Performing partial Hessian calculation using atom-list: {Final_partial_hessatoms}")
236270
#TODO: Option to run this in parallel ?
237271
#Or just enable theory parallelization
238-
result_freq = ash.NumFreq(theory=theory, fragment=SP, printlevel=0, npoint=2, hessatoms=Final_partial_hessatoms, runmode=runmode, numcores=numcores)
272+
result_freq = ash.NumFreq(theory=theory, fragment=SP, printlevel=0, npoint=partial_npoint_choice, hessatoms=Final_partial_hessatoms, runmode=runmode, numcores=numcores)
239273

240274
#Combine partial exact Hessian with model Hessian(Almloef, Lindh, Schlegel or unit)
241275
#Large Hessian is the actatoms Hessian if actatoms provided

ash/modules/module_coords.py

+70-12
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ def write_pdbfile(self,filename="Fragment"):
760760
return f"{filename}.pdb"
761761

762762
# Create new topology from scratch if none is defined (defined automatically when reading PDB-files by OpenMM)
763-
def define_topology(self, scale=1.0, tol=0.1):
763+
def define_topology(self, scale=1.0, tol=0.1, resname="MOL"):
764764
try:
765765
import openmm.app
766766
except ImportError:
@@ -769,7 +769,7 @@ def define_topology(self, scale=1.0, tol=0.1):
769769
print("Defining new basic single-chain, single-residue topology")
770770
self.pdb_topology = openmm.app.Topology()
771771
chain = self.pdb_topology.addChain()
772-
residue = self.pdb_topology.addResidue("MOL", chain)
772+
residue = self.pdb_topology.addResidue(resname, chain)
773773

774774
# Defaultdictionary to keep track of unique element-atomnames
775775
atomnames_dict=defaultdict(int)
@@ -788,7 +788,7 @@ def define_topology(self, scale=1.0, tol=0.1):
788788

789789
# Write PDB-file via OpenMM
790790
def write_pdbfile_openmm(self,filename="Fragment", calc_connectivity=False, pdb_topology=None,
791-
skip_connectivity=False):
791+
skip_connectivity=False, resname="MOL"):
792792
print("write_pdbfile_openmm\n")
793793
try:
794794
import openmm.app
@@ -807,7 +807,7 @@ def write_pdbfile_openmm(self,filename="Fragment", calc_connectivity=False, pdb_
807807
print("Warning: ASH Fragment has no PDB-file topology defined (required for PDB-file writing)")
808808
print("Now defining new topology from scratch")
809809
if pdb_topology is None:
810-
self.define_topology() #Creates self.pdb_topology
810+
self.define_topology(resname=resname) #Creates self.pdb_topology
811811
else:
812812
print("Using pdbtopology found in ASH fragment")
813813

@@ -4043,13 +4043,29 @@ def bounding_box_dimensions(coordinates,shift=0.0):
40434043
final_dims = dimensions + shift
40444044
return dimensions # Return the dimensions of the bounding box
40454045

4046+
# Combien and place 2 fragments
4047+
def combine_and_place_fragments(ref_frag, trans_frag):
4048+
4049+
for displacement in [0.0, 2.0, 4.0, 6.0,8.0,10.0,12.0,14.0]:
4050+
#Translating 2nd frag by displacement in
4051+
trans_frag.coords[:,-1] +=displacement
4052+
members = get_molecule_members_loop_np2(np.vstack((ref_frag.coords,trans_frag.coords)), ref_frag.elems+trans_frag.elems,
4053+
10, 1.0, 0.4, atomindex=0, membs=None)
4054+
if len(members) == ref_frag.numatoms:
4055+
print("Molecules are sufficiently far apart")
4056+
break
4057+
4058+
combined_solute = Fragment(elems=ref_frag.elems+trans_frag.elems, coords = np.vstack((ref_frag.coords,trans_frag.coords)),
4059+
printlevel=2)
4060+
return combined_solute
4061+
40464062

40474063
#Simple function to combine 2 ASH fragments where one is assumed to be a solute (fewer atoms) and the other assumed to be
40484064
#some kind of solvent system (box,sphere etc.)
40494065
#Use tolerance (tol) e.g. to control how many solvent molecules around get deleted
40504066
#Currently using 0.4 as default based on threonine in acetonitrile example
4051-
def insert_solute_into_solvent(solute=None, solvent=None, scale=1.0, tol=0.4, write_pdb=False, write_solute_connectivity=True,
4052-
solute_pdb=None, solvent_pdb=None, outputname="solution.pdb", write_PBC_info=True):
4067+
def insert_solute_into_solvent(solute=None, solute2=None, solvent=None, scale=1.0, tol=0.4, write_pdb=False, write_solute_connectivity=True,
4068+
solute_pdb=None, solute2_pdb=None, solvent_pdb=None, outputname="solution.pdb", write_PBC_info=True):
40534069
print("\ninsert_solute_into_solvent\n")
40544070
#Early exits
40554071
if write_pdb:
@@ -4060,32 +4076,63 @@ def insert_solute_into_solvent(solute=None, solvent=None, scale=1.0, tol=0.4, wr
40604076
if solute is None and solute_pdb is not None:
40614077
print("No solute fragment provided but solute_pdb is set. Reading solute fragment from PDB-file")
40624078
solute = Fragment(pdbfile=solute_pdb)
4079+
if solute2 is None and solute2_pdb is not None:
4080+
print("No solute2 fragment provided but solute2_pdb is set. Reading solute2 fragment from PDB-file")
4081+
solute2 = Fragment(pdbfile=solute2_pdb)
40634082
if solvent is None and solvent_pdb is not None:
40644083
print("No solvent fragment provided but solvent_pdb is set. Reading solvent fragment from PDB-file")
40654084
solvent = Fragment(pdbfile=solvent_pdb)
40664085

40674086
#Get centers
40684087
com_box = solvent.get_coordinate_center()
4069-
com_solute = solute.get_coordinate_center()
40704088

4071-
#Translating solute coords
4072-
trans_coord=np.array(com_box)-np.array(com_solute)
4073-
solute.coords=solute.coords+trans_coord
4089+
if solute2 is None:
4090+
com_solute = solute.get_coordinate_center()
4091+
#Translating solute coords
4092+
trans_coord=np.array(com_box)-np.array(com_solute)
4093+
solute.coords=solute.coords+trans_coord
4094+
else:
4095+
#Combine and Translate solute2 so that it is close to solute1
4096+
combined_solute = combine_and_place_fragments(ref_frag=solute, trans_frag=solute2)
4097+
4098+
#COM and trans
4099+
com_solute = combined_solute.get_coordinate_center()
4100+
trans_coord=np.array(com_box)-np.array(com_solute)
4101+
#Translate combined solute fragment
4102+
combined_solute.coords=combined_solute.coords+trans_coord
4103+
40744104

40754105
#New Fragment by combining element lists and coordinates
4076-
new_frag = Fragment(elems=solute.elems+solvent.elems, coords = np.vstack((solute.coords,solvent.coords)), printlevel=2)
4106+
if solute2 is None:
4107+
print("Combining solute and solvent")
4108+
new_frag = Fragment(elems=solute.elems+solvent.elems, coords = np.vstack((solute.coords,solvent.coords)), printlevel=2)
4109+
else:
4110+
print("Combining combined_solute and solvent")
4111+
new_frag = Fragment(elems=combined_solute.elems+solvent.elems, coords = np.vstack((combined_solute.coords,solvent.coords)), printlevel=2)
40774112

40784113
new_frag.write_xyzfile(xyzfilename="solution-pre.xyz")
40794114
#Trim by removing clashing atoms
40804115
new_frag.printlevel=1
4116+
40814117
#Find atoms connected to solute index 0. Uses scale and tol
40824118
membs = get_molecule_members_loop_np2(new_frag.coords, new_frag.elems, 20, scale, tol, atomindex=0, membs=None)
4083-
print("Found clashing solvent atoms:", membs)
40844119
delatoms=[]
40854120
for i in membs:
40864121
if i >= solute.numatoms:
40874122
delatoms.append(i)
4123+
print("First delatoms:", delatoms)
4124+
if solute2 is not None:
4125+
membs2 = get_molecule_members_loop_np2(new_frag.coords, new_frag.elems, 20, scale, tol, atomindex=solute.numatoms, membs=None)
4126+
print("membs2:", membs2)
4127+
for j in membs2:
4128+
if j >= solute.numatoms+solute2.numatoms:
4129+
if j not in delatoms:
4130+
delatoms.append(j)
4131+
print("Final delatoms:", delatoms)
4132+
4133+
# Deleting
40884134
delatoms.sort(reverse=True)
4135+
print("Found clashing solvent atoms:", delatoms)
40894136
for d in delatoms:
40904137
new_frag.delete_atom(d)
40914138
new_frag.printlevel=2
@@ -4113,7 +4160,18 @@ def insert_solute_into_solvent(solute=None, solvent=None, scale=1.0, tol=0.4, wr
41134160

41144161
#Create modeller object
41154162
modeller = openmm.app.Modeller(pdb1.topology, pdb1.positions) #Add pdbfile1
4163+
4164+
#solute2
4165+
if solute2 is not None:
4166+
print("Adding solute2")
4167+
pdb_solute2 = openmm.app.PDBFile(solute2_pdb)
4168+
solute2_resname= list(pdb_solute2.topology.residues())[0].name
4169+
print("solute2_resname:", solute2_resname)
4170+
modeller.add(pdb_solute2.topology, pdb_solute2.positions) #Add pdbfile2
4171+
print("Adding solvent")
41164172
modeller.add(pdb2.topology, pdb2.positions) #Add pdbfile2
4173+
4174+
41174175
#Delete clashing atoms from topology
41184176
toDelete = [r for j,r in enumerate(modeller.topology.atoms()) if j in delatoms]
41194177
modeller.delete(toDelete)

ash/modules/module_freq.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1874,7 +1874,7 @@ def read_hessian(file):
18741874

18751875
#Calculate Hessian (GFN1) for a fragment.
18761876
def calc_hessian_xtb(fragment=None, runmode='serial', actatoms=None, numcores=1, use_xtb_feature=True,
1877-
charge=None, mult=None):
1877+
charge=None, mult=None, xtbmethod="GFN1"):
18781878

18791879
print_line_with_mainheader("calc_hessian_xtb")
18801880

@@ -1890,7 +1890,7 @@ def calc_hessian_xtb(fragment=None, runmode='serial', actatoms=None, numcores=1,
18901890
fragment = ash.Fragment(elems=subelems,coords=subcoords, printlevel=0)
18911891
print("Will now calculate xTB Hessian")
18921892
#Creating xtb theory object
1893-
xtb = ash.xTBTheory(xtbmethod='GFN1', numcores=numcores)
1893+
xtb = ash.xTBTheory(xtbmethod=xtbmethod, numcores=numcores)
18941894

18951895
#Get Hessian from xTB directly (avoiding ASH NumFreq)
18961896
if use_xtb_feature == True:

0 commit comments

Comments
 (0)