-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathprogram.py
More file actions
777 lines (716 loc) · 38.1 KB
/
program.py
File metadata and controls
777 lines (716 loc) · 38.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# -*- coding: utf-8 -*-
# QuickFF is a code to quickly derive accurate force fields from ab initio input.
# Copyright (C) 2012 - 2019 Louis Vanduyfhuys <Louis.Vanduyfhuys@UGent.be>
# Steven Vandenbrande <Steven.Vandenbrande@UGent.be>,
# Jelle Wieme <Jelle.Wieme@UGent.be>,
# Toon Verstraelen <Toon.Verstraelen@UGent.be>, Center for Molecular Modeling
# (CMM), Ghent University, Ghent, Belgium; all rights reserved unless otherwise
# stated.
#
# This file is part of QuickFF.
#
# QuickFF is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# QuickFF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>
#
#--
from __future__ import print_function, absolute_import
from molmod.units import *
from quickff.valence import ValenceFF
from quickff.perturbation import RelaxedStrain
from quickff.cost import HessianFCCost
from quickff.paracontext import paracontext
from quickff.io import dump_charmm22_prm, dump_charmm22_psf, dump_yaff
from quickff.log import log
from quickff.tools import chebychev
from yaff.system import System
from yaff.pes.vlist import Cosine, Harmonic, Chebychev1, Chebychev4
from yaff.pes.iclist import BendAngle, BendCos, OopDist
import os, pickle, numpy as np, datetime, re
__all__ = [
'BaseProgram', 'MakeTrajectories', 'PlotTrajectories', 'DeriveFF',
]
class BaseProgram(object):
'''
Base program which implements all possible steps of a force field
fitting program. The actual sequence of the steps are defined in the
deriving classes.
'''
def __init__(self, system, ai, settings, ffrefs=[]):
'''
**Arguments**
system
a Yaff `System` instance defining the system
ai
a `Reference` instance corresponding to the ab initio input data
settings
a `Settings` instance defining all QuickFF settings
**Optional Arguments**
ffrefs
a list of `Reference` instances defining the a-priori force
field contributions.
'''
with log.section('INIT', 1, timer='Initializing'):
log.dump('Initializing program')
self.settings = settings
self.system = system
self.ai = ai
self.ffrefs = ffrefs
self.valence = ValenceFF(system, settings)
self.perturbation = RelaxedStrain(system, self.valence, settings)
self.trajectories = None
self.print_system()
def print_system(self):
'''
dump overview of atoms (and associated parameters) in the system
'''
with log.section('SYS', 3, timer='Initializing'):
log.dump('Atomic configuration of the system:')
log.dump('')
log.dump(' index | x [A] | y [A] | z [A] | ffatype | q | R [A] ')
log.dump('---------------------------------------------------------------------')
for i in range(len(self.system.numbers)):
x, y, z = self.system.pos[i,0], self.system.pos[i,1], self.system.pos[i,2]
if self.system.charges is not None:
q = self.system.charges[i]
else:
q = np.nan
if self.system.radii is not None:
R = self.system.radii[i]
else:
R = np.nan
log.dump(' %4i | % 7.3f | % 7.3f | % 7.3f | %6s | % 7.3f | % 7.3f ' %(
i, x/angstrom, y/angstrom, z/angstrom,
self.system.ffatypes[self.system.ffatype_ids[i]],
q, R/angstrom
))
def reset_system(self):
'''
routine to reset the system coords to the ai equilbrium
'''
log.dump('Resetting system coordinates to ab initio ref')
self.system.pos = self.ai.coords0.copy()
self.valence.dlist.forward()
self.valence.iclist.forward()
def update_trajectory_terms(self):
'''
Routine to make ``self.valence.terms`` and the term attribute of each
trajectory in ``self.trajectories`` consistent again. This is usefull
if the trajectory were read from a file and the ``valenceFF`` instance
was modified.
'''
log.dump('Updating terms of trajectories to current valenceFF terms')
with log.section('PTUPD', 3):
#update the terms in the trajectories to match the terms in
#self.valence
for traj in self.trajectories:
found = False
for term in self.valence.iter_terms():
if traj.term.get_atoms()==term.get_atoms():
if found: raise ValueError('Found two terms for trajectory %s with atom indices %s' %(traj.term.basename, str(traj.term.get_atoms())))
traj.term = term
if 'PT_ALL' not in term.tasks:
log.dump('PT_ALL not in tasks of %s-%i, deactivated PT' %(term.basename, term.index))
traj.active = False
found = True
if not found:
log.warning('No term found for trajectory %s with atom indices %s, deactivating trajectory' %(traj.term.basename, str(traj.term.get_atoms())))
traj.active = False
#check if every term with task PT_ALL has a trajectory associated
#with it. It a trajectory is missing, generate it.
# check with dont_traj which terms were meant to be missing
only = self.settings.only_traj
dont_traj = self.settings.dont_traj
if sum([only is None, dont_traj is None]) == 0:
raise AssertionError('The settings only_traj and dont_traj cannot be specified both')
dont_terms = self.generate_dont_terms()
for term in self.valence.iter_terms():
if 'PT_ALL' not in term.tasks: continue
if term in dont_terms:
with log.section('PTNOT', 3):
log.dump('Taking AI equilibrium rest value instead of generating perturbation trajectory for %s' %term.basename)
vterm = self.valence.vlist.vtab[term.index]
self.valence.set_params(term.index, fc=0, rv0=self.valence.iclist.ictab[vterm['ic0']]['value'])
continue
found = False
for traj in self.trajectories:
if term.get_atoms()==traj.term.get_atoms():
if found: raise ValueError('Found two trajectories for term %s with atom indices %s' %(term.basename, str(term.get_atoms())))
found = True
if not found:
log.warning('No trajectory found for term %s with atom indices %s. Generating it now.' %(term.basename, str(term.get_atoms())))
trajectory = self.perturbation.prepare([term])[term.index]
self.perturbation.generate(trajectory)
self.trajectories.append(trajectory)
def average_pars(self):
'''
Average force field parameters over master and slaves.
'''
log.dump('Averaging force field parameters over master and slaves')
for master in self.valence.iter_masters():
npars = len(self.valence.get_params(master.index))
pars = np.zeros([len(master.slaves)+1, npars], float)
pars[0,:] = np.array(self.valence.get_params(master.index))
for i, islave in enumerate(master.slaves):
pars[1+i,:] = np.array(self.valence.get_params(islave))
if master.kind in [0,2,11,12]:#harmonic,fues,MM3Quartic,MM3Bend
fc, rv = pars.mean(axis=0)
self.valence.set_params(master.index, fc=fc, rv0=rv)
for islave in master.slaves:
self.valence.set_params(islave, fc=fc, rv0=rv)
elif master.kind==1:
a0, a1, a2, a3 = pars.mean(axis=0)
self.valence.set_params(master.index, a0=a0, a1=a1, a2=a2, a3=a3)
for islave in master.slaves:
self.valence.set_params(islave, a0=a0, a1=a1, a2=a2, a3=a3)
elif master.kind==3:#cross
fc, rv0, rv1 = pars.mean(axis=0)
self.valence.set_params(master.index, fc=fc, rv0=rv0, rv1=rv1)
for islave in master.slaves:
self.valence.set_params(islave, fc=fc, rv0=rv0, rv1=rv1)
elif master.kind==4:#cosine
assert pars[:,0].std()<1e-6, 'dihedral multiplicity not unique'
m, fc, rv = pars.mean(axis=0)
self.valence.set_params(master.index, fc=fc, rv0=rv, m=m)
for islave in master.slaves:
self.valence.set_params(islave, fc=fc, rv0=rv, m=m)
elif master.kind in [5, 6, 7, 8, 9]:#chebychev
assert pars.shape[1]==2
fc = pars[:,0].mean()
self.valence.set_params(master.index, fc=fc)
for islave in master.slaves:
self.valence.set_params(islave, fc=fc)
else:
raise NotImplementedError
def make_output(self):
'''
Dump Yaff parameters, Yaff system, plot energy contributions along
perturbation trajectories and dump perturbation trajectories to XYZ
files.
'''
if self.settings.fn_yaff is not None:
dump_yaff(self.valence, self.settings.fn_yaff)
if self.settings.fn_charmm22_prm is not None:
dump_charmm22_prm(self.valence, self.settings.fn_charmm22_prm)
if self.settings.fn_charmm22_psf is not None:
dump_charmm22_psf(self.system, self.valence, self.settings.fn_charmm22_psf)
if self.settings.fn_sys is not None:
self.system.to_file(self.settings.fn_sys)
if self.settings.plot_traj is not None and self.settings.plot_traj.lower() in ['Ehc3', 'final', 'all']:
self.plot_trajectories(do_valence=True, suffix='_Ehc3')
if self.settings.xyz_traj:
self.write_trajectories()
def plot_trajectories(self, do_valence=False, suffix=''):
'''
Plot energy contributions along perturbation trajectories and
'''
only = self.settings.only_traj
if not isinstance(only, list): only = [only]
with log.section('PLOT', 3, timer='PT plot energy'):
valence = None
if do_valence: valence=self.valence
for trajectory in self.trajectories:
if trajectory is None: continue
for pattern in only:
if pattern in ['PT_ALL', 'pt_all', None] or pattern in trajectory.term.basename:
log.dump('Plotting trajectory for %s' %trajectory.term.basename)
trajectory.plot(self.ai, ffrefs=self.ffrefs, valence=valence, suffix=suffix)
def write_trajectories(self):
'''
Write perturbation trajectories to XYZ files.
'''
only = self.settings.only_traj
if not isinstance(only, list): only = [only]
with log.section('XYZ', 3, timer='PT dump XYZ'):
for trajectory in self.trajectories:
if trajectory is None: continue
for pattern in only:
if pattern in ['PT_ALL', 'pt_all', None] or pattern in trajectory.term.basename:
log.dump('Writing XYZ trajectory for %s' %trajectory.term.basename)
trajectory.to_xyz()
def generate_dont_terms(self):
'''
Generate terms for which no perturbation trajectory should be generated based on dont_traj setting
'''
if self.settings.dont_traj:
return []
kind2string = {0:'bond', 2:'bend', 11:'oopdist' , 12:'dihedral'}
ffatypes = [self.system.ffatypes[fid] for fid in self.system.ffatype_ids]
dont_patterns = self.settings.dont_traj.split(',') # split patterns
dont_terms = []
for term in self.valence.terms:
if term.kind in [0,2,11,12]:
types = term.basename.split('/')[1].split('.')
option1 = '.'.join(types)
option2 = '.'.join(types[::-1])
for dp in dont_patterns:
pattern = re.compile(dp, re.IGNORECASE)
if pattern.match(option1) or pattern.match(option2):
dont_terms.append(term)
return dont_terms
def do_pt_generate(self):
'''
Generate perturbation trajectories.
'''
with log.section('PTGEN', 2, timer='PT Generate'):
#read if an existing file was specified through fn_traj
fn_traj = self.settings.fn_traj
if fn_traj is not None and os.path.isfile(fn_traj):
self.trajectories = pickle.load(open(fn_traj, 'rb'))
log.dump('Trajectories read from file %s' %fn_traj)
self.update_trajectory_terms()
newname = 'updated_'+fn_traj.split('/')[-1]
pickle.dump(self.trajectories, open(newname, 'wb'))
return
#configure
self.reset_system()
only = self.settings.only_traj
dont_traj = self.settings.dont_traj
if sum([only is None, dont_traj is None]) == 0:
raise AssertionError('The settings only_traj and dont_traj cannot be specified both')
if (only is None or only=='PT_ALL' or only=='pt_all') and dont_traj is None: # only=None is equivalent to PT_ALL
do_terms = [term for term in self.valence.terms if term.kind in [0,2,11,12]]
elif only is None and dont_traj is not None:
dont_terms = self.generate_dont_terms()
do_terms = [term for term in self.valence.terms if term.kind in [0,2,11,12] and term not in dont_terms]
with log.section('PTNOT', 3):
for term in dont_terms:
log.dump('Taking AI equilibrium rest value instead of generating perturbation trajectory for %s' %term.basename)
vterm = self.valence.vlist.vtab[term.index]
self.valence.set_params(term.index, fc=0, rv0=self.valence.iclist.ictab[vterm['ic0']]['value'])
else:
if isinstance(only, str): only = [only]
do_terms = []
for pattern in only:
for term in self.valence.iter_terms(pattern):
if term.kind in [0,2,11,12]:
do_terms.append(term)
trajectories = self.perturbation.prepare(do_terms)
#compute
log.dump('Constructing trajectories')
self.trajectories = paracontext.map(self.perturbation.generate, [traj for traj in trajectories if traj.active])
#write the trajectories to the non-existing file fn_traj
if fn_traj is not None:
assert not os.path.isfile(fn_traj)
pickle.dump(self.trajectories, open(fn_traj, 'wb'))
log.dump('Trajectories stored to file %s' %fn_traj)
def do_pt_estimate(self, do_valence=False, energy_noise=None, logger_level=3):
'''
Estimate force constants and rest values from the perturbation
trajectories
**Optional Arguments**
do_valence
if set to True, the current valence force field will be used to
estimate the contribution of all other valence terms.
'''
with log.section('PTEST', 2, timer='PT Estimate'):
self.reset_system()
message = 'Estimating FF parameters from perturbation trajectories'
if do_valence: message += ' with valence reference'
log.dump(message)
#compute fc and rv from trajectory
only = self.settings.only_traj
for traj in self.trajectories:
if traj is None: continue
if not (only is None or only=='PT_ALL' or only=='pt_all'):
if isinstance(only, str): only = [only]
basename = self.valence.terms[traj.term.master].basename
if basename not in only: continue
self.perturbation.estimate(traj, self.ai, ffrefs=self.ffrefs, do_valence=do_valence, energy_noise=energy_noise)
#set force field parameters to computed fc and rv
for traj in self.trajectories:
if traj is None: continue
if not (only is None or only=='PT_ALL' or only=='pt_all'):
if isinstance(only, str): only = [only]
basename = self.valence.terms[traj.term.master].basename
if basename not in only: continue
self.valence.set_params(traj.term.index, fc=traj.fc, rv0=traj.rv)
#output
self.valence.dump_logger(print_level=logger_level)
#do not add average here since the fluctuation on the parameters is
#required for do_pt_postprocess. Average will be done at the end of
#do_pt_postprocess
def do_pt_postprocess(self):
'''
Do some first post processing of the ff parameters estimated from
the perturbation trajectories including:
* detecting bend patterns with rest values of 90 and 180 deg
* detecting bend patterns with rest values only close to 180 deg
* transforming SqOopDist with rv=0.0 to OopDist
* averaging parameters
'''
with log.section('PTPOST', 2, timer='PT Post process'):
if self.settings.do_squarebend:
self.do_squarebend()
if self.settings.do_bendclin:
self.do_bendclin()
if self.settings.do_sqoopdist_to_oopdist:
self.do_sqoopdist_to_oopdist()
self.average_pars()
def do_eq_setrv(self, tasks, logger_level=3):
'''
Set the rest values to their respective AI equilibrium values.
'''
with log.section('EQSET', 2, timer='Equil Set RV'):
self.reset_system()
log.dump('Setting rest values to AI equilibrium values for tasks %s' %' '.join(tasks))
for term in self.valence.terms:
vterm = self.valence.vlist.vtab[term.index]
if np.array([task in term.tasks for task in tasks]).any():
if term.kind==3:#cross term
ic0 = self.valence.iclist.ictab[vterm['ic0']]
ic1 = self.valence.iclist.ictab[vterm['ic1']]
self.valence.set_params(term.index, rv0=ic0['value'], rv1=ic1['value'])
elif term.kind==4 and term.ics[0].kind==4:#Cosine of DihedAngle
ic = self.valence.iclist.ictab[vterm['ic0']]
m = self.valence.get_params(term.index, only='m')
rv = ic['value']%(360.0*deg/m)
with log.section('EQSET', 4, timer='Equil Set RV'):
log.dump('Set rest value of %s(%s) (eq=%.3f deg) to %.3f deg' %(
term.basename,
'.'.join([str(at) for at in term.get_atoms()]),
ic['value']/deg, rv/deg
))
self.valence.set_params(term.index, rv0=rv)
else:
rv = self.valence.iclist.ictab[vterm['ic0']]['value']
self.valence.set_params(term.index, rv0=rv)
self.valence.dump_logger(print_level=logger_level)
self.average_pars()
def do_hc_estimatefc(self, tasks, logger_level=3, do_svd=False, svd_rcond=0.0, do_mass_weighting=True):
'''
Refine force constants using Hessian Cost function.
**Arguments**
tasks
A list of strings identifying which terms should have their
force constant estimated from the hessian cost function. Using
such a flag, one can distinguish between for example force
constant refinement (flag=HC_FC_DIAG) of the diagonal terms and
force constant estimation of the cross terms (flag=HC_FC_CROSS).
If the string 'all' is present in tasks, all fc's will be
estimated.
**Optional Arguments**
logger_level
print level at which the resulting parameters should be dumped to
the logger. By default, the parameters will only be dumped at
the highest log level.
do_svd
whether or not to do an SVD decomposition before solving the
set of equations and explicitly throw out the degrees of
freedom that correspond to the lowest singular values.
do_mass_weighting
whether or not to apply mass weighing to the ab initio hessian
and the force field contributions before doing the fitting.
'''
with log.section('HCEST', 2, timer='HC Estimate FC'):
self.reset_system()
log.dump('Estimating force constants from Hessian cost for tasks %s' %' '.join(tasks))
term_indices = []
for index in range(self.valence.vlist.nv):
term = self.valence.terms[index]
flagged = False
for flag in tasks:
if flag in term.tasks:
flagged = True
break
if flagged:
#first check if all rest values and multiplicities have been defined
if term.kind==0: self.valence.check_params(term, ['rv'])
if term.kind==1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3'])
if term.kind==3: self.valence.check_params(term, ['rv0','rv1'])
if term.kind==4: self.valence.check_params(term, ['rv', 'm'])
if term.is_master():
term_indices.append(index)
else:
#first check if all pars have been defined
if term.kind==0: self.valence.check_params(term, ['fc', 'rv'])
if term.kind==1: self.valence.check_params(term, ['a0', 'a1', 'a2', 'a3'])
if term.kind==3: self.valence.check_params(term, ['fc', 'rv0','rv1'])
if term.kind==4: self.valence.check_params(term, ['fc', 'rv', 'm'])
if len(term_indices)==0:
log.dump('No terms (with task in %s) found to estimate FC from HC' %(str(tasks)))
return
# Try to estimate force constants; if the remove_dysfunctional_cross
# keyword is True, a loop is performed which checks whether there
# are cross terms for which corresponding diagonal terms have zero
# force constants. If this is the case, those cross terms are removed
# from the fit and we try again until such cases do no longer occur
max_iter = 100
niter = 0
while niter<max_iter:
cost = HessianFCCost(self.system, self.ai, self.valence, term_indices, ffrefs=self.ffrefs, do_mass_weighting=do_mass_weighting)
fcs = cost.estimate(do_svd=do_svd, svd_rcond=svd_rcond)
# No need to continue, if cross terms with corresponding diagonal
# terms with negative force constants are allowed
if self.settings.remove_dysfunctional_cross is False: break
to_remove = []
for index, fc in zip(term_indices, fcs):
term = self.valence.terms[index]
if term.basename.startswith('Cross'):
# Find force constants of corresponding diagonal terms
diag_fcs = np.zeros((2))
for idiag in range(2):
diag_index = term.diag_term_indexes[idiag]
if diag_index in term_indices:
fc_diag = fcs[term_indices.index(diag_index)]
else:
fc_diag = self.valence.get_params(diag_index, only='fc')
diag_fcs[idiag] = fc_diag
# If a force constant from any corresponding diagonal term is negative,
# we remove the cross term for the next iteration
if np.any(diag_fcs<=0.0):
to_remove.append(index)
self.valence.set_params(index, fc=0.0)
log.dump('WARNING! Dysfunctional cross term %s detected, removing from the hessian fit.'%term.basename)
if len(to_remove)==0: break
else:
for index in to_remove:
term_indices.remove(index)
niter += 1
assert niter<max_iter, "Could not remove all dysfunctional cross terms in %d iterations, something is seriously wrong"%max_iter
for index, fc in zip(term_indices, fcs):
master = self.valence.terms[index]
assert master.is_master()
self.valence.set_params(index, fc=fc)
for islave in master.slaves:
self.valence.set_params(islave, fc=fc)
self.valence.dump_logger(print_level=logger_level)
def do_cross_init(self):
'''
Add cross terms to the valence list and initialize parameters.
'''
with log.section('VAL', 2, 'Initializing'):
self.reset_system()
self.valence.init_cross_angle_terms()
if self.settings.do_cross_DSS or self.settings.do_cross_DSD or self.settings.do_cross_DAD or self.settings.do_cross_DAA:
self.valence.init_cross_dihed_terms()
self.update_cross_pars()
def update_cross_pars(self):
'''
Set the rest values of cross terms to the rest values of the
corresponding diagonal terms. Set the force constants to zero.
'''
with log.section('VAL', 2, 'Initializing'):
def find_rest_value(iterm):
term = self.valence.terms[iterm]
if term.basename.startswith('TorsCheby') or term.basename.startswith('BendCheby'):
return -self.valence.get_params(iterm, only='sign')
else:
return self.valence.get_params(iterm, only='rv')
# Bond-Bond Cross terms
cases = [('Cross','bb',3),('Cross','b0a',3),('Cross','b1a',3)]
# Bond-Dihedral Cross terms
for m in [1,2,3,4,6]:
for suffix in ['bb','b0d','b1d','b2d']:
case = ('CrossBondDih%i'%m,suffix,4)
cases.append(case)
# Angle-Dihedral Cross terms
for m in [1,2,3,4,6]:
for suffix in ['aa','a0d','a1d']:
case = ('CrossBendDih%i'%m,suffix,4)
cases.append(case)
for suffix in ['a0d','a1d']:
case = ('CrossCBendDih%i'%m,suffix,4)
cases.append(case)
# Loop over all cases
for prefix, suffix, ntypes in cases:
# Loop over all cross terms belonging to this case
for term in self.valence.iter_masters('^%s/.*/%s$'%(prefix,suffix), use_re=True):
types = term.basename.split('/')[1].split('.')
assert len(types)==ntypes, 'Found cross term with %d atom types, expected %d'%(len(types),ntype)
rv0 = find_rest_value(term.diag_term_indexes[0])
rv1 = find_rest_value(term.diag_term_indexes[1])
self.valence.set_params(term.index, fc=0.0, rv0=rv0, rv1=rv1)
for index in term.slaves: self.valence.set_params(index, fc=0.0, rv0=rv0, rv1=rv1)
def do_squarebend(self, thresshold=20*deg):
'''
Identify bend patterns in which 4 atoms of type A surround a central
atom of type B with A-B-A angles of 90/180 degrees. A simple
harmonic pattern will not be adequate since a rest value of 90 and
180 degrees is possible for the same A-B-A term. Therefore, a
cosine term with multiplicity of 4 is used (which corresponds to a
chebychev4 potential with sign=-1):
V = K/2*[1-cos(4*theta)]
To identify the patterns, it is assumed that the rest values have
already been estimated from the perturbation trajectories. For each
master and slave of a BENDAHARM term, its rest value is computed and
checked if it lies either the interval [90-thresshold,90+thresshold]
or [180-thresshold,180]. If this is the case, the new cosine term
is used.
**Optional arguments**
thresshold
the (half) the width of the interval around 180 deg (90 degrees)
to check if a square BA4
'''
for master in self.valence.iter_masters(label='BendAHarm'):
rvs = np.zeros([len(master.slaves)+1], float)
rvs[0] = self.valence.get_params(master.index, only='rv')
for i, islave in enumerate(master.slaves):
rvs[1+i] = self.valence.get_params(islave, only='rv')
n90 = 0
n180 = 0
nother = 0
for i, rv in enumerate(rvs):
if 90*deg-thresshold<=rv and rv<=90*deg+thresshold: n90 += 1
elif 180*deg-thresshold<=rv and rv<=180*deg+thresshold: n180 += 1
else: nother += 1
if n90>0 and n180>0:
log.dump('%s has rest values around 90 deg and 180 deg, converted to BendCheby4' %master.basename)
#modify master and slaves
indices = [master.index]
for slave in master.slaves: indices.append(slave)
for index in indices:
term = self.valence.terms[index]
self.valence.modify_term(
index,
Chebychev4, [BendCos(*term.get_atoms())],
term.basename.replace('BendAHarm', 'BendCheby4'),
['HC_FC_DIAG'], ['kjmol', 'au']
)
self.valence.set_params(index, sign=-1)
for traj in self.trajectories:
if traj.term.index==index:
traj.active = False
traj.fc = None
traj.rv = None
def do_bendclin(self, thresshold=5*deg):
'''
No Harmonic bend can have a rest value equal to are large than
180 deg - thresshold. If a master (or its slaves) has such a rest
value, convert master and all slaves to BendCLin (which corresponds
to a chebychev1 potential with sign=+1):
0.5*K*[1+cos(theta)]
'''
for master in self.valence.iter_masters(label='BendAHarm'):
indices = [master.index]
for slave in master.slaves: indices.append(slave)
found = False
for index in indices:
rv = self.valence.get_params(index, only='rv')
if rv>=180.0*deg-thresshold:
found = True
break
if found:
log.dump('%s has rest value > 180-%.0f deg, converted to BendCheby1' %(master.basename, thresshold/deg))
for index in indices:
term = self.valence.terms[index]
self.valence.modify_term(
index,
Chebychev1, [BendCos(*term.get_atoms())],
term.basename.replace('BendAHarm', 'BendCheby1'),
['HC_FC_DIAG'], ['kjmol', 'au']
)
self.valence.set_params(index, fc=0.0, sign=1.0)
for traj in self.trajectories:
if traj.term.index==index:
traj.rv = None
traj.fc = None
traj.active = False
def do_sqoopdist_to_oopdist(self, thresshold=1e-4*angstrom):
'''
Transform a SqOopdist term with a rest value that has been set to
zero, to a term Oopdist (harmonic in Oopdist instead of square of
Oopdist) with a rest value of 0.0 A.
'''
for master in self.valence.iter_masters(label='SqOopdist'):
indices = [master.index]
for slave in master.slaves: indices.append(slave)
found = False
for index in indices:
rv = self.valence.get_params(index, only='rv')
if rv<=thresshold:
found = True
break
if found:
log.dump('%s has rest value <= %.0f A^2, converted to Oopdist with d0=0' %(master.basename, thresshold/angstrom))
for index in indices:
term = self.valence.terms[index]
self.valence.modify_term(
index,
Harmonic, [OopDist(*term.get_atoms())],
term.basename.replace('SqOopdist', 'Oopdist'),
['HC_FC_DIAG'], ['kjmol/A**2', 'A']
)
self.valence.set_params(index, fc=0.0, rv0=0.0)
def run(self):
'''
Sequence of instructions, should be implemented in the inheriting
classes. The various inheriting classes distinguish themselves by
means of the instructions implemented in this routine.
'''
raise NotImplementedError
class MakeTrajectories(BaseProgram):
'''
Construct the perturbation trajectories and store them. This program
does not derive the force field.
'''
def run(self):
with log.section('PROGRAM', 2):
fn_traj = self.settings.fn_traj
assert fn_traj is not None, 'It is useless to run the MakeTrajectories program without specifying a trajectory filename fn_traj!'
assert not os.path.isfile(fn_traj), 'Given file %s to store trajectories to already exists!' %fn_traj
self.do_pt_generate()
class PlotTrajectories(BaseProgram):
'''
Read the perturbation trajectories, dump to XYZ files and plot the
energy contributions. This program does not derive the force field.
'''
def run(self):
with log.section('PROGRAM', 2):
fn_traj = self.settings.fn_traj
assert fn_traj is not None, 'The PlotTrajectories program requires a trajectory filename fn_traj!'
assert os.path.isfile(fn_traj), 'Given file %s to read trajectories does not exists!' %fn_traj
self.settings.set('xyz_traj', True)
self.settings.set('plot_traj', 'all')
self.do_pt_generate()
self.do_pt_estimate()
self.plot_trajectories(suffix='_Apt1')
self.write_trajectories()
class DeriveFF(BaseProgram):
'''
Derive a force field for the given system. After the hessian fit of the
force constants, the rest values are refined by revisiting the
perturbation trajectories with an extra a priori term representing the
current valence contribution. Finally, the force constants are refined
by means of a final hessian fit.
'''
def run(self):
with log.section('PROGRAM', 2):
self.do_eq_setrv(['EQ_RV'])
self.do_pt_generate()
self.do_pt_estimate(energy_noise=self.settings.pert_traj_energy_noise)
if self.settings.plot_traj is not None and (self.settings.plot_traj.lower() in ['Apt1', 'all']):
self.plot_trajectories(do_valence=False, suffix='_Apt1')
if self.settings.xyz_traj is not None and self.settings.xyz_traj:
self.write_trajectories()
self.do_pt_postprocess()
self.do_cross_init()
self.do_hc_estimatefc(['HC_FC_DIAG', 'HC_FC_CROSS_ASS', 'HC_FC_CROSS_ASA'], do_mass_weighting=self.settings.do_hess_mass_weighting)
if self.settings.plot_traj is not None and (self.settings.plot_traj.lower() in ['Bhc1', 'all']):
self.plot_trajectories(do_valence=True, suffix='_Bhc1')
self.do_pt_estimate(do_valence=True, energy_noise=self.settings.pert_traj_energy_noise)
if self.settings.plot_traj is not None and (self.settings.plot_traj.lower() in ['Cpt2', 'all']):
self.plot_trajectories(do_valence=True, suffix='_Cpt2')
self.do_pt_postprocess()
if self.settings.consistent_cross_rvs:
# The rest values of the diagonal terms have been updated from
# the perturbation trajectories; update the corresponding rest
# values for the cross terms
self.update_cross_pars()
self.do_hc_estimatefc(['HC_FC_DIAG', 'HC_FC_CROSS_ASS', 'HC_FC_CROSS_ASA'], do_mass_weighting=self.settings.do_hess_mass_weighting)
if self.settings.plot_traj is not None and (self.settings.plot_traj.lower() in ['Dhc2', 'all']):
self.plot_trajectories(do_valence=True, suffix='_Dhc2')
self.do_hc_estimatefc([
'HC_FC_CROSS_ASS', 'HC_FC_CROSS_ASA', 'HC_FC_CROSS_DSS',
'HC_FC_CROSS_DSD', 'HC_FC_CROSS_DAA', 'HC_FC_CROSS_DAD'
], logger_level=1, do_mass_weighting=self.settings.do_hess_mass_weighting, do_svd=self.settings.do_cross_svd, svd_rcond=self.settings.cross_svd_rcond)
self.make_output()