-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathrelative.py
4484 lines (3674 loc) · 265 KB
/
relative.py
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
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import simtk.openmm as openmm
import simtk.unit as unit
import mdtraj as md
import numpy as np
import copy
import enum
InteractionGroup = enum.Enum("InteractionGroup", ['unique_old', 'unique_new', 'core', 'environment'])
#######LOGGING#############################
import logging
logging.basicConfig(level = logging.NOTSET)
_logger = logging.getLogger("relative")
_logger.setLevel(logging.INFO)
###########################################
class HybridTopologyFactory(object):
"""
This class generates a hybrid topology based on a perses topology proposal. This class treats atoms
in the resulting hybrid system as being from one of four classes:
unique_old_atom : these atoms are not mapped and only present in the old system. Their interactions will be on for
lambda=0, off for lambda=1
unique_new_atom : these atoms are not mapped and only present in the new system. Their interactions will be off
for lambda=0, on for lambda=1
core_atom : these atoms are mapped, and are part of a residue that is changing. Their interactions will be those
corresponding to the old system at lambda=0, and those corresponding to the new system at lambda=1
environment_atom : these atoms are mapped, and are not part of a changing residue. Their interactions are always
on and are alchemically unmodified.
Here are the forces in the hybrid system:
- CustomBondForce -- handles bonds involving `core_atoms` (these are interpolated)
- HarmonicBondForce -- handles bonds involving `environment_atoms`, `unique_old_atoms` and `unique_new_atoms` (these are never scaled)
- CustomAngleForce -- handles angles involving `core_atoms` (these are interpolated)
- HarmonicAngleForce -- handles angles involving `environment_atoms`, `unique_old_atoms` and `unique_new_atoms` (these are never scaled)
- CustomTorsionForce -- handles torsions involving `core_atoms` (these are interpolated)
- PeriodicTorsionForce -- handles torsions involving `environment_atoms`, `unique_old_atoms` and `unique_new_atoms` (these are never scaled)
- NonbondedForce -- handles all electrostatic interactions, environment-environment steric interactions
- CustomNonbondedForce -- handle all non environment-environment sterics
- CustomBondForce_exceptions -- handles all electrostatics and sterics exceptions involving unique old/new atoms when interpolate_14s is True, otherwise the electrostatics/sterics exception is in the NonbondedForce
* where `interactions` refers to any pair of atoms that is not 1-2, 1-3, 1-4
This class can be tested using perses.tests.utils.validate_endstate_energies(), as is done by perses.tests.test_relative.compare_energies
Properties
----------
hybrid_system : openmm.System
The hybrid system for simulation
new_to_hybrid_atom_map : dict of int : int
The mapping of new system atoms to hybrid atoms
old_to_hybrid_atom_map : dict of int : int
The mapping of old system atoms to hybrid atoms
hybrid_positions : [n, 3] np.ndarray
The positions of the hybrid system
hybrid_topology : mdtraj.Topology
The topology of the hybrid system
omm_hybrid_topology : openmm.app.Topology
The OpenMM topology object corresponding to the hybrid system
.. warning :: This API is experimental and subject to change.
"""
_known_forces = {'HarmonicBondForce', 'HarmonicAngleForce', 'PeriodicTorsionForce', 'NonbondedForce', 'MonteCarloBarostat'}
def __init__(self,
topology_proposal,
current_positions,
new_positions,
use_dispersion_correction=False,
functions=None,
softcore_alpha=None,
bond_softening_constant=1.0,
angle_softening_constant=1.0,
soften_only_new=False,
neglected_new_angle_terms=[],
neglected_old_angle_terms=[],
softcore_LJ_v2=True,
softcore_electrostatics=True,
softcore_LJ_v2_alpha=0.85,
softcore_electrostatics_alpha=0.3,
softcore_sigma_Q=1.0,
interpolate_old_and_new_14s=False,
omitted_terms=None,
flatten_torsions=False,
rmsd_restraint=False,
impose_virtual_bonds=True,
endstate=None,
**kwargs):
"""
Initialize the Hybrid topology factory.
Parameters
----------
topology_proposal : perses.rjmc.topology_proposal.TopologyProposal object
TopologyProposal object rendered by the ProposalEngine
current_positions : [n,3] np.ndarray of float
The positions of the "old system"
new_positions : [m,3] np.ndarray of float
The positions of the "new system"
use_dispersion_correction : bool, default False
Whether to use the long range correction in the custom sterics force. This is very expensive for NCMC.
functions : dict, default None
Alchemical functions that determine how each force is scaled with lambda. The keys must be strings with
names beginning with ``lambda_`` and ending with each of bonds, angles, torsions, sterics, electrostatics.
If functions is none, then the integrator will need to set each of these and parameter derivatives will be unavailable.
If functions is not None, all lambdas must be specified.
softcore_alpha: float, default None
"alpha" parameter of softcore sterics. If None is provided, value will be set to 0.5
bond_softening_constant : float
For bonds between unique atoms and unique-core atoms, soften the force constant at the "dummy" endpoint by this factor.
If 1.0, do not soften
angle_softening_constant : float
For bonds between unique atoms and unique-core atoms, soften the force constant at the "dummy" endpoint by this factor.
If 1.0, do not soften
neglected_new_angle_terms : list
list of indices from the HarmonicAngleForce of the new_system for which the geometry engine neglected.
Hence, these angles must be alchemically grown in for the unique new atoms (forward lambda protocol)
neglected_old_angle_terms : list
list of indices from the HarmonicAngleForce of the old_system for which the geometry engine neglected.
Hence, these angles must be alchemically deleted for the unique old atoms (reverse lambda protocol)
softcore_LJ_v2 : bool, default True
implement a new softcore LJ: citation below.
Gapsys, Vytautas, Daniel Seeliger, and Bert L. de Groot. "New soft-core potential function for molecular dynamics based alchemical free energy calculations." Journal of chemical theory and computation 8.7 (2012): 2373-2382.
softcore_electrostatics : bool, default True
softcore electrostatics: citation below.
Gapsys, Vytautas, Daniel Seeliger, and Bert L. de Groot. "New soft-core potential function for molecular dynamics based alchemical free energy calculations." Journal of chemical theory and computation 8.7 (2012): 2373-2382.
softcore_LJ_v2_alpha : float, default 0.85
softcore alpha parameter for LJ v2
softcore_electrostatics_alpha : float, default 0.3
softcore alpha parameter for softcore electrostatics.
softcore_sigma_Q : float, default 1.0
softcore sigma parameter for softcore electrostatics.
interpolate_old_and_new_14s : bool, default False
whether to turn off interactions for new exceptions (not just 1,4s) at lambda = 0 and old exceptions at lambda = 1; if False, they are present in the nonbonded force
omitted_terms : dict
dictionary of terms (by new topology index) that must be annealed in over a lambda protocol
rmsd_restraint : bool, optional, default=False
If True, impose an RMSD restraint between core heavy atoms and protein CA atoms
impose_virtual_bonds : bool, optional, default=True
If True, impose virtual bonds to ensure components of the system are imaged together
flatten_torsions : bool, default False
if True, torsion terms involving `unique_new_atoms` will be scaled such that at lambda=0,1, the torsion term is turned off/on respectively
the opposite is true for `unique_old_atoms`.
endstate : int
the lambda endstate to parameterize. should always be None for HybridTopologyFactory, but must be 0 or 1 for the RepartitionedHybridTopologyFactory
TODO: Document how positions for hybrid system are constructed
TODO: allow support for annealing in omitted terms
"""
if endstate == 0 or endstate == 1:
raise Exception("endstate must be none! Aborting!")
elif endstate is None:
_logger.info("*** Generating vanilla HybridTopologyFactory ***")
_logger.info("Beginning nonbonded method, total particle, barostat, and exceptions retrieval...")
self._topology_proposal = topology_proposal
self._old_system = copy.deepcopy(topology_proposal.old_system)
self._new_system = copy.deepcopy(topology_proposal.new_system)
self._old_to_hybrid_map = {}
self._new_to_hybrid_map = {}
self._hybrid_system_forces = dict()
self._old_positions = current_positions
self._new_positions = new_positions
self._soften_only_new = soften_only_new
self._interpolate_14s = interpolate_old_and_new_14s
self.omitted_terms = omitted_terms
self._flatten_torsions = flatten_torsions
if self._flatten_torsions:
_logger.info("Flattening torsions of unique new/old at lambda = 0/1")
if self._interpolate_14s:
_logger.info("Flattening exceptions of unique new/old at lambda = 0/1")
if omitted_terms is not None:
raise Exception(f"annealing of omitted terms is not currently supported. Aborting!")
# New attributes from the modified geometry engine
if neglected_old_angle_terms:
self.neglected_old_angle_terms = neglected_old_angle_terms
else:
self.neglected_old_angle_terms = []
if neglected_new_angle_terms:
self.neglected_new_angle_terms = neglected_new_angle_terms
else:
self.neglected_new_angle_terms = []
if bond_softening_constant != 1.0:
self._bond_softening_constant = bond_softening_constant
self._soften_bonds = True
else:
self._soften_bonds = False
if angle_softening_constant != 1.0:
self._angle_softening_constant = angle_softening_constant
self._soften_angles = True
else:
self._soften_angles = False
self._use_dispersion_correction = use_dispersion_correction
self._softcore_LJ_v2 = softcore_LJ_v2
if self._softcore_LJ_v2:
self._softcore_LJ_v2_alpha = softcore_LJ_v2_alpha
assert self._softcore_LJ_v2_alpha >= 0.0 and self._softcore_LJ_v2_alpha <= 1.0, f"softcore_LJ_v2_alpha: ({self._softcore_LJ_v2_alpha}) is not in [0,1]"
self._softcore_electrostatics = softcore_electrostatics
if self._softcore_electrostatics:
self._softcore_electrostatics_alpha = softcore_electrostatics_alpha
self._softcore_sigma_Q = softcore_sigma_Q
assert self._softcore_electrostatics_alpha >= 0.0 and self._softcore_electrostatics_alpha <= 1.0, f"softcore_electrostatics_alpha: ({self._softcore_electrostatics_alpha}) is not in [0,1]"
assert self._softcore_sigma_Q >= 0.0 and self._softcore_sigma_Q <= 1.0, f"softcore_sigma_Q : {self._softcore_sigma_Q} is not in [0, 1]"
if softcore_alpha is None:
self.softcore_alpha = 0.5
else:
# TODO: Check that softcore_alpha is in a valid range
self.softcore_alpha = softcore_alpha
if functions:
self._functions = functions
self._has_functions = True
else:
self._has_functions = False
# Prepare dicts of forces, which will be useful later
# TODO: Store this as self._system_forces[name], name in ('old', 'new', 'hybrid') for compactness
self._old_system_forces = {type(force).__name__ : force for force in self._old_system.getForces()}
self._new_system_forces = {type(force).__name__ : force for force in self._new_system.getForces()}
_logger.info(f"Old system forces: {self._old_system_forces.keys()}")
_logger.info(f"New system forces: {self._new_system_forces.keys()}")
# Check that there are no unknown forces in the new and old systems:
for system_name in ('old', 'new'):
force_names = getattr(self, '_{}_system_forces'.format(system_name)).keys()
unknown_forces = set(force_names) - set(self._known_forces)
if len(unknown_forces) > 0:
raise ValueError(f"Unknown forces {unknown_forces} encountered in {system_name} system")
_logger.info("No unknown forces.")
# Get and store the nonbonded method from the system:
self._nonbonded_method = self._old_system_forces['NonbondedForce'].getNonbondedMethod()
_logger.info(f"Nonbonded method to be used (i.e. from old system): {self._nonbonded_method}")
# Start by creating an empty system. This will become the hybrid system.
self._hybrid_system = openmm.System()
# Begin by copying all particles in the old system to the hybrid system. Note that this does not copy the
# interactions. It does, however, copy the particle masses. In general, hybrid index and old index should be
# the same.
# TODO: Refactor this into self._add_particles()
_logger.info("Adding and mapping old atoms to hybrid system...")
for particle_idx in range(self._topology_proposal.n_atoms_old):
particle_mass_old = self._old_system.getParticleMass(particle_idx)
if particle_idx in self._topology_proposal.old_to_new_atom_map.keys():
particle_index_in_new_system = self._topology_proposal.old_to_new_atom_map[particle_idx]
particle_mass_new = self._new_system.getParticleMass(particle_index_in_new_system)
particle_mass = (particle_mass_old + particle_mass_new) / 2 # Take the average of the masses if the atom is mapped
else:
particle_mass = particle_mass_old
hybrid_idx = self._hybrid_system.addParticle(particle_mass)
self._old_to_hybrid_map[particle_idx] = hybrid_idx
# If the particle index in question is mapped, make sure to add it to the new to hybrid map as well.
if particle_idx in self._topology_proposal.old_to_new_atom_map.keys():
self._new_to_hybrid_map[particle_index_in_new_system] = hybrid_idx
# Next, add the remaining unique atoms from the new system to the hybrid system and map accordingly.
# As before, this does not copy interactions, only particle indices and masses.
_logger.info("Adding and mapping new atoms to hybrid system...")
for particle_idx in self._topology_proposal.unique_new_atoms:
particle_mass = self._new_system.getParticleMass(particle_idx)
hybrid_idx = self._hybrid_system.addParticle(particle_mass)
self._new_to_hybrid_map[particle_idx] = hybrid_idx
# Check that if there is a barostat in the original system, it is added to the hybrid.
# We copy the barostat from the old system.
if "MonteCarloBarostat" in self._old_system_forces.keys():
barostat = copy.deepcopy(self._old_system_forces["MonteCarloBarostat"])
self._hybrid_system.addForce(barostat)
_logger.info("Added MonteCarloBarostat.")
else:
_logger.info("No MonteCarloBarostat added.")
# Copy over the box vectors:
box_vectors = self._old_system.getDefaultPeriodicBoxVectors()
self._hybrid_system.setDefaultPeriodicBoxVectors(*box_vectors)
_logger.info(f"getDefaultPeriodicBoxVectors added to hybrid: {box_vectors}")
# Create the opposite atom maps for use in nonbonded force processing; let's omit this from logger
self._hybrid_to_old_map = {value : key for key, value in self._old_to_hybrid_map.items()}
self._hybrid_to_new_map = {value : key for key, value in self._new_to_hybrid_map.items()}
# Assign atoms to one of the classes described in the class docstring
self._atom_classes = self._determine_atom_classes()
_logger.info("Determined atom classes.")
# Construct dictionary of exceptions in old and new systems
_logger.info("Generating old system exceptions dict...")
self._old_system_exceptions = self._generate_dict_from_exceptions(self._old_system_forces['NonbondedForce'])
_logger.info("Generating new system exceptions dict...")
self._new_system_exceptions = self._generate_dict_from_exceptions(self._new_system_forces['NonbondedForce'])
self._validate_disjoint_sets()
# Copy constraints, checking to make sure they are not changing
_logger.info("Handling constraints...")
self._handle_constraints()
# Copy over relevant virtual sites
_logger.info("Handling virtual sites...")
self._handle_virtual_sites()
# Call each of the methods to add the corresponding force terms and prepare the forces:
_logger.info("Adding bond force terms...")
self._add_bond_force_terms()
_logger.info("Adding angle force terms...")
self._add_angle_force_terms()
_logger.info("Adding torsion force terms...")
self._add_torsion_force_terms()
if 'NonbondedForce' in self._old_system_forces or 'NonbondedForce' in self._new_system_forces:
_logger.info("Adding nonbonded force terms...")
self._add_nonbonded_force_terms()
# Call each force preparation method to generate the actual interactions that we need:
_logger.info("Handling harmonic bonds...")
self.handle_harmonic_bonds()
_logger.info("Handling harmonic angles...")
self.handle_harmonic_angles()
_logger.info("Handling torsion forces...")
self.handle_periodic_torsion_force()
if 'NonbondedForce' in self._old_system_forces or 'NonbondedForce' in self._new_system_forces:
_logger.info("Handling nonbonded forces...")
self.handle_nonbonded()
if 'NonbondedForce' in self._old_system_forces or 'NonbondedForce' in self._new_system_forces:
_logger.info("Handling unique_new/old interaction exceptions...")
if len(self._old_system_exceptions.keys()) == 0 and len(self._new_system_exceptions.keys()) == 0:
_logger.info("There are no old/new system exceptions.")
else:
_logger.info("There are old or new system exceptions...proceeding.")
self.handle_old_new_exceptions()
# Get positions for the hybrid
self._hybrid_positions = self._compute_hybrid_positions()
# Generate the topology representation
self._hybrid_topology = self._create_topology()
# Impose RMSD restraint, if requested
if rmsd_restraint:
_logger.info("Attempting to impose RMSD restraints.")
self._impose_rmsd_restraint()
# Impose virtual bonds to ensure system is imaged together.
if impose_virtual_bonds:
_logger.info("Imposing virtual bonds to ensure system is imaged together.")
self._impose_virtual_bonds()
def _validate_disjoint_sets(self):
"""
Conduct a sanity check to make sure that the hybrid maps of the old and new system exception dict keys do not contain both environment and unique_old/new atoms
"""
for old_indices in self._old_system_exceptions.keys():
hybrid_indices = (self._old_to_hybrid_map[old_indices[0]], self._old_to_hybrid_map[old_indices[1]])
if set(old_indices).intersection(self._atom_classes['environment_atoms']) != set():
assert set(old_indices).intersection(self._atom_classes['unique_old_atoms']) == set(), f"old index exceptions {old_indices} include unique old and environment atoms, which is disallowed"
for new_indices in self._new_system_exceptions.keys():
hybrid_indices = (self._new_to_hybrid_map[new_indices[0]], self._new_to_hybrid_map[new_indices[1]])
if set(hybrid_indices).intersection(self._atom_classes['environment_atoms']) != set():
assert set(hybrid_indices).intersection(self._atom_classes['unique_new_atoms']) == set(), f"new index exceptions {new_indices} include unique new and environment atoms, which is disallowed"
def _handle_virtual_sites(self):
"""
Ensure that all virtual sites in old and new system are copied over to the hybrid system. Note that we do not
support virtual sites in the changing region.
"""
for system_name in ('old', 'new'):
system = getattr(self._topology_proposal, '{}_system'.format(system_name))
hybrid_atom_map = getattr(self, '_{}_to_hybrid_map'.format(system_name))
# Loop through virtual sites
numVirtualSites = 0
for particle_idx in range(system.getNumParticles()):
if system.isVirtualSite(particle_idx):
numVirtualSites += 1
# If it's a virtual site, make sure it is not in the unique or core atoms, since this is currently unsupported
hybrid_idx = hybrid_atom_map[particle_idx]
if hybrid_idx not in self._atom_classes['environment_atoms']:
raise Exception("Virtual sites in changing residue are unsupported.")
else:
virtual_site = system.getVirtualSite(particle_idx)
self._hybrid_system.setVirtualSite(hybrid_idx, virtual_site)
_logger.info(f"\t_handle_virtual_sites: numVirtualSites: {numVirtualSites}")
def _determine_core_atoms_in_topology(self, topology, unique_atoms, mapped_atoms, hybrid_map, residue_to_switch):
"""
Given a topology and its corresponding unique and mapped atoms, return the set of atom indices in the
hybrid system which would belong to the "core" atom class
Parameters
----------
topology : simtk.openmm.app.Topology
An OpenMM topology representing a system of interest
unique_atoms : set of int
A set of atoms that are unique to this topology
mapped_atoms : set of int
A set of atoms that are mapped to another topology
residue_to_switch : str
string name of a residue that is being mutated
Returns
-------
core_atoms : set of int
set of core atom indices in hybrid topology
"""
core_atoms = set()
# Loop through the residues to look for ones with unique atoms
for residue in topology.residues():
atom_indices_old_system = {atom.index for atom in residue.atoms()}
# If the residue contains an atom index that is unique, then the residue is changing.
# We determine this by checking if the atom indices of the residue have any intersection with the unique atoms
# likewise, if the name of the residue matches the residue_to_match, then we look for mapped atoms
if len(atom_indices_old_system.intersection(unique_atoms)) > 0 or residue_to_switch == residue.name:
# We can add the atoms in this residue which are mapped to the core_atoms set:
for atom_index in atom_indices_old_system:
if atom_index in mapped_atoms:
# We specifically want to add the hybrid atom.
hybrid_index = hybrid_map[atom_index]
core_atoms.add(hybrid_index)
assert len(core_atoms) >= 3, 'Cannot run a simulation with fewer than 3 core atoms. System has {len(core_atoms)}'
return core_atoms
def _determine_atom_classes(self):
"""
This method determines whether each atom belongs to unique old, unique new, core, or environment, as defined above.
All the information required is contained in the TopologyProposal passed to the constructor. All indices are
indices in the hybrid system.
Returns
-------
atom_classes : dict of list
A dictionary of the form {'core' :core_list} etc.
"""
atom_classes = {'unique_old_atoms' : set(), 'unique_new_atoms' : set(), 'core_atoms' : set(), 'environment_atoms' : set()}
# First, find the unique old atoms, as this is the most straightforward:
for atom_idx in self._topology_proposal.unique_old_atoms:
hybrid_idx = self._old_to_hybrid_map[atom_idx]
atom_classes['unique_old_atoms'].add(hybrid_idx)
# Then the unique new atoms (this is substantially the same as above)
for atom_idx in self._topology_proposal.unique_new_atoms:
hybrid_idx = self._new_to_hybrid_map[atom_idx]
atom_classes['unique_new_atoms'].add(hybrid_idx)
# The core atoms:
core_atoms = []
for new_idx, old_idx in self._topology_proposal._core_new_to_old_atom_map.items():
new_to_hybrid_idx, old_to_hybrid_index = self._new_to_hybrid_map[new_idx], self._old_to_hybrid_map[old_idx]
assert new_to_hybrid_idx == old_to_hybrid_index, f"there is a -to_hybrid naming collision in topology proposal core atom map: {self._topology_proposal._core_new_to_old_atom_map}"
core_atoms.append(new_to_hybrid_idx)
new_to_hybrid_environment_atoms = set([self._new_to_hybrid_map[idx] for idx in self._topology_proposal._new_environment_atoms])
old_to_hybrid_environment_atoms = set([self._old_to_hybrid_map[idx] for idx in self._topology_proposal._old_environment_atoms])
assert new_to_hybrid_environment_atoms == old_to_hybrid_environment_atoms, f"there is a -to_hybrid naming collisions in topology proposal environment atom map: new_to_hybrid: {new_to_hybrid_environment_atoms}; old_to_hybrid: {old_to_hybrid_environment_atoms}"
atom_classes['core_atoms'] = set(core_atoms)
atom_classes['environment_atoms'] = new_to_hybrid_environment_atoms # since we asserted this is identical to old_to_hybrid_environment_atoms
return atom_classes
def _translate_nonbonded_method_to_custom(self, standard_nonbonded_method):
"""
Utility function to translate the nonbonded method enum from the standard nonbonded force to the custom version
`CutoffPeriodic`, `PME`, and `Ewald` all become `CutoffPeriodic`; `NoCutoff` becomes `NoCutoff`; `CutoffNonPeriodic` becomes `CutoffNonPeriodic`
Parameters
----------
standard_nonbonded_method : openmm.NonbondedForce.NonbondedMethod
the nonbonded method of the standard force
Returns
-------
custom_nonbonded_method : openmm.CustomNonbondedForce.NonbondedMethod
the nonbonded method for the equivalent customnonbonded force
"""
if standard_nonbonded_method in [openmm.NonbondedForce.CutoffPeriodic, openmm.NonbondedForce.PME, openmm.NonbondedForce.Ewald]:
return openmm.CustomNonbondedForce.CutoffPeriodic
elif standard_nonbonded_method == openmm.NonbondedForce.NoCutoff:
return openmm.CustomNonbondedForce.NoCutoff
elif standard_nonbonded_method == openmm.NonbondedForce.CutoffNonPeriodic:
return openmm.CustomNonbondedForce.CutoffNonPeriodic
else:
raise NotImplementedError("This nonbonded method is not supported.")
def _handle_constraints(self):
"""
This method adds relevant constraints from the old and new systems.
First, all constraints from the old systenm are added.
Then, constraints to atoms unique to the new system are added.
"""
constraint_lengths = dict() # lengths of constraints already added
for system_name in ('old', 'new'):
system = getattr(self._topology_proposal, '{}_system'.format(system_name))
hybrid_map = getattr(self, '_{}_to_hybrid_map'.format(system_name))
for constraint_idx in range(system.getNumConstraints()):
atom1, atom2, length = system.getConstraintParameters(constraint_idx)
hybrid_atoms = tuple(sorted([hybrid_map[atom1], hybrid_map[atom2]]))
if hybrid_atoms not in constraint_lengths.keys():
self._hybrid_system.addConstraint(hybrid_atoms[0], hybrid_atoms[1], length)
constraint_lengths[hybrid_atoms] = length
else:
# TODO: We can skip this if we have already checked for constraints changing lengths
if constraint_lengths[hybrid_atoms] != length:
raise Exception('Constraint length is changing for atoms {} in hybrid system: old {} new {}'.format(hybrid_atoms, constraint_lengths[hybrid_atoms], length))
_logger.debug(f"\t_handle_constraints: constraint_lengths dict: {constraint_lengths}")
def _determine_interaction_group(self, atoms_in_interaction):
"""
This method determines which interaction group the interaction should fall under. There are four groups:
Those involving unique old atoms: any interaction involving unique old atoms should be completely on at lambda=0
and completely off at lambda=1
Those involving unique new atoms: any interaction involving unique new atoms should be completely off at lambda=0
and completely on at lambda=1
Those involving core atoms and/or environment atoms: These interactions change their type, and should be the old
character at lambda=0, and the new character at lambda=1
Those involving only environment atoms: These interactions are unmodified.
Parameters
----------
atoms_in_interaction : list of int
List of (hybrid) indices of the atoms in this interaction
Returns
-------
interaction_group : InteractionGroup enum
The group to which this interaction should be assigned
"""
# Make the interaction list a set to facilitate operations
atom_interaction_set = set(atoms_in_interaction)
# Check if the interaction contains unique old atoms
if len(atom_interaction_set.intersection(self._atom_classes['unique_old_atoms'])) > 0:
return InteractionGroup.unique_old
# Do the same for new atoms
elif len(atom_interaction_set.intersection(self._atom_classes['unique_new_atoms'])) > 0:
return InteractionGroup.unique_new
# If the interaction set is a strict subset of the environment atoms, then it is in the environment group
# and should not be alchemically modified at all.
elif atom_interaction_set.issubset(self._atom_classes['environment_atoms']):
return InteractionGroup.environment
# Having covered the cases of all-environment, unique old-containing, and unique-new-containing, anything else
# should belong to the last class--contains core atoms but not any unique atoms.
else:
return InteractionGroup.core
def _add_bond_force_terms(self):
"""
This function adds the appropriate bond forces to the system (according to groups defined above). Note that it
does _not_ add the particles to the force. It only adds the force to facilitate another method adding the
particles to the force.
"""
core_energy_expression = '(K/2)*(r-length)^2;'
core_energy_expression += 'K = (1-lambda_bonds)*K1 + lambda_bonds*K2;' # linearly interpolate spring constant
core_energy_expression += 'length = (1-lambda_bonds)*length1 + lambda_bonds*length2;' # linearly interpolate bond length
if self._has_functions:
try:
core_energy_expression += 'lambda_bonds = ' + self._functions['lambda_bonds']
except KeyError as e:
print("Functions were provided, but no term was provided for the bonds")
raise e
# Create the force and add the relevant parameters
custom_core_force = openmm.CustomBondForce(core_energy_expression)
custom_core_force.addPerBondParameter('length1') # old bond length
custom_core_force.addPerBondParameter('K1') # old spring constant
custom_core_force.addPerBondParameter('length2') # new bond length
custom_core_force.addPerBondParameter('K2') # new spring constant
if self._has_functions:
custom_core_force.addGlobalParameter('lambda', 0.0)
custom_core_force.addEnergyParameterDerivative('lambda')
else:
custom_core_force.addGlobalParameter('lambda_bonds', 0.0)
self._hybrid_system.addForce(custom_core_force)
self._hybrid_system_forces['core_bond_force'] = custom_core_force
# Add a bond force for environment and unique atoms (bonds are never scaled for these):
standard_bond_force = openmm.HarmonicBondForce()
self._hybrid_system.addForce(standard_bond_force)
self._hybrid_system_forces['standard_bond_force'] = standard_bond_force
def _add_angle_force_terms(self):
"""
This function adds the appropriate angle force terms to the hybrid system. It does not add particles
or parameters to the force; this is done elsewhere.
"""
energy_expression = '(K/2)*(theta-theta0)^2;'
energy_expression += 'K = (1.0-lambda_angles)*K_1 + lambda_angles*K_2;' # linearly interpolate spring constant
energy_expression += 'theta0 = (1.0-lambda_angles)*theta0_1 + lambda_angles*theta0_2;' # linearly interpolate equilibrium angle
if self._has_functions:
try:
energy_expression += 'lambda_angles = ' + self._functions['lambda_angles']
except KeyError as e:
print("Functions were provided, but no term was provided for the angles")
raise e
# Create the force and add relevant parameters
custom_core_force = openmm.CustomAngleForce(energy_expression)
custom_core_force.addPerAngleParameter('theta0_1') # molecule1 equilibrium angle
custom_core_force.addPerAngleParameter('K_1') # molecule1 spring constant
custom_core_force.addPerAngleParameter('theta0_2') # molecule2 equilibrium angle
custom_core_force.addPerAngleParameter('K_2') # molecule2 spring constant
# Create the force for neglected angles and relevant parameters; the K_1 term will be set to 0
if len(self.neglected_new_angle_terms) > 0: # if there is at least one neglected angle term from the geometry engine
_logger.info("\t_add_angle_force_terms: there are > 0 neglected new angles: adding CustomAngleForce")
custom_neglected_new_force = openmm.CustomAngleForce(energy_expression)
custom_neglected_new_force.addPerAngleParameter('theta0_1') # molecule1 equilibrium angle
custom_neglected_new_force.addPerAngleParameter('K_1') # molecule1 spring constant
custom_neglected_new_force.addPerAngleParameter('theta0_2') # molecule2 equilibrium angle
custom_neglected_new_force.addPerAngleParameter('K_2') # molecule2 spring constant
if len(self.neglected_old_angle_terms) > 0: # if there is at least one neglected angle term from the geometry engine
_logger.info("\t_add_angle_force_terms: there are > 0 neglected old angles: adding CustomAngleForce")
custom_neglected_old_force = openmm.CustomAngleForce(energy_expression)
custom_neglected_old_force.addPerAngleParameter('theta0_1') # molecule1 equilibrium angle
custom_neglected_old_force.addPerAngleParameter('K_1') # molecule1 spring constant
custom_neglected_old_force.addPerAngleParameter('theta0_2') # molecule2 equilibrium angle
custom_neglected_old_force.addPerAngleParameter('K_2') # molecule2 spring constant
if self._has_functions:
custom_core_force.addGlobalParameter('lambda', 0.0)
custom_core_force.addEnergyParameterDerivative('lambda')
if len(self.neglected_new_angle_terms) > 0:
custom_neglected_new_force.addGlobalParameter('lambda', 0.0)
custom_neglected_new_force.addEnergyParameterDerivative('lambda')
if len(self.neglected_old_angle_terms) > 0:
custom_neglected_old_force.addGlobalParameter('lambda', 0.0)
custom_neglected_old_force.addEnergyParameterDerivative('lambda')
else:
custom_core_force.addGlobalParameter('lambda_angles', 0.0)
if len(self.neglected_new_angle_terms) > 0:
custom_neglected_new_force.addGlobalParameter('lambda_angles', 0.0)
if len(self.neglected_old_angle_terms) > 0:
custom_neglected_old_force.addGlobalParameter('lambda_angles', 0.0)
# Add the force to the system and the force dict.
self._hybrid_system.addForce(custom_core_force)
self._hybrid_system_forces['core_angle_force'] = custom_core_force
if len(self.neglected_new_angle_terms) > 0:
self._hybrid_system.addForce(custom_neglected_new_force)
self._hybrid_system_forces['custom_neglected_new_angle_force'] = custom_neglected_new_force
if len(self.neglected_old_angle_terms) > 0:
self._hybrid_system.addForce(custom_neglected_old_force)
self._hybrid_system_forces['custom_neglected_old_angle_force'] = custom_neglected_old_force
# Add an angle term for environment/unique interactions--these are never scaled
standard_angle_force = openmm.HarmonicAngleForce()
self._hybrid_system.addForce(standard_angle_force)
self._hybrid_system_forces['standard_angle_force'] = standard_angle_force
def _add_torsion_force_terms(self, add_custom_core_force=True, add_unique_atom_torsion_force=True):
"""
This function adds the appropriate PeriodicTorsionForce terms to the system. Core torsions are interpolated,
while environment and unique torsions are always on.
"""
energy_expression = '(1-lambda_torsions)*U1 + lambda_torsions*U2;'
energy_expression += 'U1 = K1*(1+cos(periodicity1*theta-phase1));'
energy_expression += 'U2 = K2*(1+cos(periodicity2*theta-phase2));'
if self._has_functions:
try:
energy_expression += 'lambda_torsions = ' + self._functions['lambda_torsions']
except KeyError as e:
print("Functions were provided, but no term was provided for torsions")
raise e
# Create the force and add the relevant parameters
custom_core_force = openmm.CustomTorsionForce(energy_expression)
custom_core_force.addPerTorsionParameter('periodicity1') # molecule1 periodicity
custom_core_force.addPerTorsionParameter('phase1') # molecule1 phase
custom_core_force.addPerTorsionParameter('K1') # molecule1 spring constant
custom_core_force.addPerTorsionParameter('periodicity2') # molecule2 periodicity
custom_core_force.addPerTorsionParameter('phase2') # molecule2 phase
custom_core_force.addPerTorsionParameter('K2') # molecule2 spring constant
if self._has_functions:
custom_core_force.addGlobalParameter('lambda', 0.0)
custom_core_force.addEnergyParameterDerivative('lambda')
else:
custom_core_force.addGlobalParameter('lambda_torsions', 0.0)
# Add the force to the system
if add_custom_core_force:
self._hybrid_system.addForce(custom_core_force)
self._hybrid_system_forces['custom_torsion_force'] = custom_core_force
# Create and add the torsion term for unique/environment atoms
if add_unique_atom_torsion_force:
unique_atom_torsion_force = openmm.PeriodicTorsionForce()
self._hybrid_system.addForce(unique_atom_torsion_force)
self._hybrid_system_forces['unique_atom_torsion_force'] = unique_atom_torsion_force
def _add_nonbonded_force_terms(self, add_custom_sterics_force=True):
"""
Add the nonbonded force terms to the hybrid system. Note that as with the other forces,
this method does not add any interactions. It only sets up the forces.
Parameters
----------
nonbonded_method : int
One of the openmm.NonbondedForce nonbonded methods.
"""
# Add a regular nonbonded force for all interactions that are not changing.
standard_nonbonded_force = openmm.NonbondedForce()
self._hybrid_system.addForce(standard_nonbonded_force)
_logger.info(f"\t_add_nonbonded_force_terms: {standard_nonbonded_force} added to hybrid system")
self._hybrid_system_forces['standard_nonbonded_force'] = standard_nonbonded_force
# Create a CustomNonbondedForce to handle alchemically interpolated nonbonded parameters.
# Select functional form based on nonbonded method.
# TODO: check _nonbonded_custom_ewald and _nonbonded_custom_cutoff since they take arguments that are never used...
if self._nonbonded_method in [openmm.NonbondedForce.NoCutoff]:
_logger.info("\t_add_nonbonded_force_terms: nonbonded_method is NoCutoff")
sterics_energy_expression = self._nonbonded_custom(self._softcore_LJ_v2)
elif self._nonbonded_method in [openmm.NonbondedForce.CutoffPeriodic, openmm.NonbondedForce.CutoffNonPeriodic]:
_logger.info("\t_add_nonbonded_force_terms: nonbonded_method is Cutoff(Periodic or NonPeriodic)")
epsilon_solvent = self._old_system_forces['NonbondedForce'].getReactionFieldDielectric()
r_cutoff = self._old_system_forces['NonbondedForce'].getCutoffDistance()
sterics_energy_expression = self._nonbonded_custom(self._softcore_LJ_v2)
standard_nonbonded_force.setReactionFieldDielectric(epsilon_solvent)
standard_nonbonded_force.setCutoffDistance(r_cutoff)
elif self._nonbonded_method in [openmm.NonbondedForce.PME, openmm.NonbondedForce.Ewald]:
_logger.info("\t_add_nonbonded_force_terms: nonbonded_method is PME or Ewald")
[alpha_ewald, nx, ny, nz] = self._old_system_forces['NonbondedForce'].getPMEParameters()
delta = self._old_system_forces['NonbondedForce'].getEwaldErrorTolerance()
r_cutoff = self._old_system_forces['NonbondedForce'].getCutoffDistance()
sterics_energy_expression = self._nonbonded_custom(self._softcore_LJ_v2)
standard_nonbonded_force.setPMEParameters(alpha_ewald, nx, ny, nz)
standard_nonbonded_force.setEwaldErrorTolerance(delta)
standard_nonbonded_force.setCutoffDistance(r_cutoff)
else:
raise Exception("Nonbonded method %s not supported yet." % str(self._nonbonded_method))
standard_nonbonded_force.setNonbondedMethod(self._nonbonded_method)
_logger.info(f"\t_add_nonbonded_force_terms: {self._nonbonded_method} added to standard nonbonded force")
sterics_energy_expression += self._nonbonded_custom_sterics_common()
sterics_mixing_rules = self._nonbonded_custom_mixing_rules()
custom_nonbonded_method = self._translate_nonbonded_method_to_custom(self._nonbonded_method)
total_sterics_energy = "U_sterics;" + sterics_energy_expression + sterics_mixing_rules
if self._has_functions:
try:
total_sterics_energy += 'lambda_sterics = ' + self._functions['lambda_sterics']
except KeyError as e:
print("Functions were provided, but there is no entry for sterics")
raise e
sterics_custom_nonbonded_force = openmm.CustomNonbondedForce(total_sterics_energy)
if self._softcore_LJ_v2:
sterics_custom_nonbonded_force.addGlobalParameter("softcore_alpha", self._softcore_LJ_v2_alpha)
else:
sterics_custom_nonbonded_force.addGlobalParameter("softcore_alpha", self.softcore_alpha)
sterics_custom_nonbonded_force.addPerParticleParameter("sigmaA") # Lennard-Jones sigma initial
sterics_custom_nonbonded_force.addPerParticleParameter("epsilonA") # Lennard-Jones epsilon initial
sterics_custom_nonbonded_force.addPerParticleParameter("sigmaB") # Lennard-Jones sigma final
sterics_custom_nonbonded_force.addPerParticleParameter("epsilonB") # Lennard-Jones epsilon final
sterics_custom_nonbonded_force.addPerParticleParameter("unique_old") # 1 = hybrid old atom, 0 otherwise
sterics_custom_nonbonded_force.addPerParticleParameter("unique_new") # 1 = hybrid new atom, 0 otherwise
if self._has_functions:
sterics_custom_nonbonded_force.addGlobalParameter('lambda', 0.0)
sterics_custom_nonbonded_force.addEnergyParameterDerivative('lambda')
else:
sterics_custom_nonbonded_force.addGlobalParameter("lambda_sterics_core", 0.0)
sterics_custom_nonbonded_force.addGlobalParameter("lambda_electrostatics_core", 0.0)
sterics_custom_nonbonded_force.addGlobalParameter("lambda_sterics_insert", 0.0)
sterics_custom_nonbonded_force.addGlobalParameter("lambda_sterics_delete", 0.0)
sterics_custom_nonbonded_force.setNonbondedMethod(custom_nonbonded_method)
_logger.info(f"\t_add_nonbonded_force_terms: {custom_nonbonded_method} added to sterics_custom_nonbonded force")
if add_custom_sterics_force:
self._hybrid_system.addForce(sterics_custom_nonbonded_force)
self._hybrid_system_forces['core_sterics_force'] = sterics_custom_nonbonded_force
_logger.info(f"\t_add_nonbonded_force_terms: {sterics_custom_nonbonded_force} added to hybrid system")
# Set the use of dispersion correction to be the same between the new nonbonded force and the old one:
# These will be ignored from the _logger for the time being
if self._old_system_forces['NonbondedForce'].getUseDispersionCorrection():
self._hybrid_system_forces['standard_nonbonded_force'].setUseDispersionCorrection(True)
if self._use_dispersion_correction:
sterics_custom_nonbonded_force.setUseLongRangeCorrection(True)
else:
self._hybrid_system_forces['standard_nonbonded_force'].setUseDispersionCorrection(False)
if self._old_system_forces['NonbondedForce'].getUseSwitchingFunction():
switching_distance = self._old_system_forces['NonbondedForce'].getSwitchingDistance()
standard_nonbonded_force.setUseSwitchingFunction(True)
standard_nonbonded_force.setSwitchingDistance(switching_distance)
sterics_custom_nonbonded_force.setUseSwitchingFunction(True)
sterics_custom_nonbonded_force.setSwitchingDistance(switching_distance)
else:
standard_nonbonded_force.setUseSwitchingFunction(False)
sterics_custom_nonbonded_force.setUseSwitchingFunction(False)
def _nonbonded_custom_sterics_common(self):
"""
Get a custom sterics expression using amber softcore expression
Returns
-------
sterics_addition : str
The common softcore sterics energy expression
"""
sterics_addition = "epsilon = (1-lambda_sterics)*epsilonA + lambda_sterics*epsilonB;" # interpolation
sterics_addition += "reff_sterics = sigma*((softcore_alpha*lambda_alpha + (r/sigma)^6))^(1/6);" # effective softcore distance for sterics
sterics_addition += "sigma = (1-lambda_sterics)*sigmaA + lambda_sterics*sigmaB;"
sterics_addition += "lambda_alpha = new_interaction*(1-lambda_sterics_insert) + old_interaction*lambda_sterics_delete;"
sterics_addition += "lambda_sterics = core_interaction*lambda_sterics_core + new_interaction*lambda_sterics_insert + old_interaction*lambda_sterics_delete;"
sterics_addition += "core_interaction = delta(unique_old1+unique_old2+unique_new1+unique_new2);new_interaction = max(unique_new1, unique_new2);old_interaction = max(unique_old1, unique_old2);"
return sterics_addition
def _nonbonded_custom(self, v2):
"""
Get a part of the nonbonded energy expression when there is no cutoff.
Returns
-------
sterics_energy_expression : str
The energy expression for U_sterics
electrostatics_energy_expression : str
The energy expression for electrostatics
"""
# Soft-core Lennard-Jones
if v2:
sterics_energy_expression = "U_sterics = select(step(r - r_LJ), 4*epsilon*x*(x-1.0), U_sterics_quad);"
sterics_energy_expression += f"U_sterics_quad = Force*(((r - r_LJ)^2)/2 - (r - r_LJ)) + U_sterics_cut;"
sterics_energy_expression += f"U_sterics_cut = 4*epsilon*((sigma/r_LJ)^6)*(((sigma/r_LJ)^6) - 1.0);"
sterics_energy_expression += f"Force = -4*epsilon*((-12*sigma^12)/(r_LJ^13) + (6*sigma^6)/(r_LJ^7));"
sterics_energy_expression += f"x = (sigma/r)^6;"
sterics_energy_expression += f"r_LJ = softcore_alpha*((26/7)*(sigma^6)*lambda_sterics_deprecated)^(1/6);"
sterics_energy_expression += f"lambda_sterics_deprecated = new_interaction*(1.0 - lambda_sterics_insert) + old_interaction*lambda_sterics_delete;"
else:
sterics_energy_expression = "U_sterics = 4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;"
return sterics_energy_expression
def _nonbonded_custom_mixing_rules(self):
"""
Mixing rules for the custom nonbonded force.
Returns
-------
sterics_mixing_rules : str
The mixing expression for sterics
electrostatics_mixing_rules : str
The mixiing rules for electrostatics
"""
# Define mixing rules.
sterics_mixing_rules = "epsilonA = sqrt(epsilonA1*epsilonA2);" # mixing rule for epsilon
sterics_mixing_rules += "epsilonB = sqrt(epsilonB1*epsilonB2);" # mixing rule for epsilon
sterics_mixing_rules += "sigmaA = 0.5*(sigmaA1 + sigmaA2);" # mixing rule for sigma
sterics_mixing_rules += "sigmaB = 0.5*(sigmaB1 + sigmaB2);" # mixing rule for sigma
return sterics_mixing_rules
def _find_bond_parameters(self, bond_force, index1, index2):
"""
This is a convenience function to find bond parameters in another system given the two indices.
Parameters
----------
bond_force : openmm.HarmonicBondForce
The bond force where the parameters should be found
index1 : int
Index1 (order does not matter) of the bond atoms
index2 : int
Index2 (order does not matter) of the bond atoms
Returns
-------
bond_parameters : list
List of relevant bond parameters
"""
index_set = {index1, index2}
# Loop through all the bonds:
for bond_index in range(bond_force.getNumBonds()):
parms = bond_force.getBondParameters(bond_index)
if index_set=={parms[0], parms[1]}:
return parms
return []
def handle_harmonic_bonds(self):
"""
This method adds the appropriate interaction for all bonds in the hybrid system. The scheme used is:
1) If the two atoms are both in the core, then we add to the CustomBondForce and interpolate between the two
parameters
2) If one of the atoms is in core and the other is environment, we have to assert that the bond parameters do not change between
the old and the new system; then, the parameters are added to the regular bond force
3) Otherwise, we add the bond to a regular bond force.
"""
old_system_bond_force = self._old_system_forces['HarmonicBondForce']
new_system_bond_force = self._new_system_forces['HarmonicBondForce']
# Make a dict to check the environment-core bonds for consistency between the old and new systems
# key: hybrid_index_set, value: [(r0_old, k_old)]
old_core_env_indices = {}
# First, loop through the old system bond forces and add relevant terms
_logger.info("\thandle_harmonic_bonds: looping through old_system to add relevant terms...")
for bond_index in range(old_system_bond_force.getNumBonds()):
# Get each set of bond parameters
[index1_old, index2_old, r0_old, k_old] = old_system_bond_force.getBondParameters(bond_index)
_logger.debug(f"\t\thandle_harmonic_bonds: old bond_index {bond_index} with old indices {index1_old, index2_old}")
# Map the indices to the hybrid system, for which our atom classes are defined.
index1_hybrid = self._old_to_hybrid_map[index1_old]
index2_hybrid = self._old_to_hybrid_map[index2_old]
index_set = {index1_hybrid, index2_hybrid}
# Now check if it is a subset of the core atoms (that is, both atoms are in the core)
# If it is, we need to find the parameters in the old system so that we can interpolate
if index_set.issubset(self._atom_classes['core_atoms']):
_logger.debug(f"\t\thandle_harmonic_bonds: bond_index {bond_index} is a core (to custom bond force).")
index1_new = self._topology_proposal.old_to_new_atom_map[index1_old]
index2_new = self._topology_proposal.old_to_new_atom_map[index2_old]
new_bond_parameters = self._find_bond_parameters(new_system_bond_force, index1_new, index2_new)
if not new_bond_parameters:
r0_new = r0_old
k_new = 0.0*unit.kilojoule_per_mole/unit.angstrom**2
else:
[index1, index2, r0_new, k_new] = self._find_bond_parameters(new_system_bond_force, index1_new, index2_new)
self._hybrid_system_forces['core_bond_force'].addBond(index1_hybrid, index2_hybrid,[r0_old, k_old, r0_new, k_new])
# Check if the index set is a subset of anything besides environemnt (in the case of environment, we just add the bond to the regular bond force)
# that would mean that this bond is core-unique_old or unique_old-unique_old
elif index_set.issubset(self._atom_classes['unique_old_atoms']) or (len(index_set.intersection(self._atom_classes['unique_old_atoms'])) == 1 and len(index_set.intersection(self._atom_classes['core_atoms'])) == 1):
_logger.debug(f"\t\thandle_harmonic_bonds: bond_index {bond_index} is a core-unique_old or unique_old-unique old...")
# If we're not softening bonds, we can just add it to the regular bond force. Likewise if we are only softening new bonds
if not self._soften_bonds or self._soften_only_new:
_logger.debug(f"\t\t\thandle_harmonic_bonds: no softening (to standard bond force)")
self._hybrid_system_forces['standard_bond_force'].addBond(index1_hybrid, index2_hybrid, r0_old,
k_old)
# Otherwise, we will need to soften one of the endpoints. For unique old atoms, the softening endpoint is at lambda =1
else:
r0_new = r0_old # The bond length won't change
k_new = self._bond_softening_constant * k_old # We multiply the endpoint by the bond softening constant