@@ -291,6 +291,219 @@ def test_acetone_to_propenol():
291291 assert str (p .function ()) == expression
292292
293293
294+ def test_ejm49_to_ejm31 ():
295+ """
296+ Test ghost atom modifications for the TYK ligands EJM 49 to 31. This is assert
297+ more complex perturbation.
298+ """
299+
300+ # Load the system. Here pruned means that the atom mapping has pruned
301+ # atoms where the constraint changes between the end states, which is
302+ # what is used by OpenFE.
303+ mols = sr .load_test_files ("ejm49_ejm31_pruned.bss" )
304+
305+ # Store the orginal angles and dihedrals at lambda = 0 and lambda = 1.
306+ angles0 = mols [0 ].property ("angle0" )
307+ angles1 = mols [0 ].property ("angle1" )
308+ dihedrals0 = mols [0 ].property ("dihedral0" )
309+ dihedrals1 = mols [0 ].property ("dihedral1" )
310+ improper0 = mols [0 ].property ("improper0" )
311+ improper1 = mols [0 ].property ("improper1" )
312+
313+ # Apply the ghost atom modifications.
314+ new_mols , _ = modify (mols )
315+
316+ # Get the new angles and dihedrals.
317+ new_angles0 = new_mols [0 ].property ("angle0" )
318+ new_angles1 = new_mols [0 ].property ("angle1" )
319+ new_dihedrals0 = new_mols [0 ].property ("dihedral0" )
320+ new_dihedrals1 = new_mols [0 ].property ("dihedral1" )
321+ new_improper0 = new_mols [0 ].property ("improper0" )
322+ new_improper1 = new_mols [0 ].property ("improper1" )
323+
324+ # The number of angles should remain the same at lambda = 0.
325+ assert angles0 .num_functions () == new_angles0 .num_functions ()
326+
327+ # The number of dihedrals should be five fewer at lambda = 0.
328+ assert dihedrals0 .num_functions () - 5 == new_dihedrals0 .num_functions ()
329+
330+ # The number of impropers shoudld be three fewer at lambda = 0.
331+ assert improper0 .num_functions () - 3 == new_improper0 .num_functions ()
332+
333+ # The number of angles should remain the same at lambda = 1.
334+ assert angles1 .num_functions () == new_angles1 .num_functions ()
335+
336+ # The number of dihedrals should be four fewer at lambda = 1.
337+ assert dihedrals1 .num_functions () - 4 == new_dihedrals1 .num_functions ()
338+
339+ # The number of impropers should be six fewer at lambda = 1.
340+ assert improper1 .num_functions () - 6 == new_improper1 .num_functions ()
341+
342+ # Create dihedral IDs for the missing dihedrals at lambda = 0.
343+
344+ from sire .legacy .Mol import AtomIdx
345+
346+ missing_dihedrals0 = [
347+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (39 ), AtomIdx (40 )),
348+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (39 ), AtomIdx (41 )),
349+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (39 ), AtomIdx (42 )),
350+ (AtomIdx (33 ), AtomIdx (16 ), AtomIdx (17 ), AtomIdx (39 )),
351+ (AtomIdx (14 ), AtomIdx (16 ), AtomIdx (17 ), AtomIdx (39 )),
352+ ]
353+
354+ # Store the molecular info.
355+ info = mols [0 ].info ()
356+
357+ # Check that the missing dihedrals are in the original dihedrals at lambda = 0.
358+ assert (
359+ all (
360+ check_dihedral (info , dihedrals0 .potentials (), * dihedral )
361+ for dihedral in missing_dihedrals0
362+ )
363+ == True
364+ )
365+
366+ # Check that the missing dihedrals are not in the new dihedrals at lambda = 0.
367+ assert (
368+ all (
369+ check_dihedral (info , new_dihedrals0 .potentials (), * dihedral )
370+ for dihedral in missing_dihedrals0
371+ )
372+ == False
373+ )
374+
375+ # Create dihedral IDs for the missing dihedrals at lambda = 1.
376+ missing_dihedrals1 = [
377+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (20 ), AtomIdx (21 )),
378+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (20 ), AtomIdx (25 )),
379+ (AtomIdx (20 ), AtomIdx (17 ), AtomIdx (16 ), AtomIdx (33 )),
380+ (AtomIdx (14 ), AtomIdx (16 ), AtomIdx (17 ), AtomIdx (20 )),
381+ ]
382+
383+ # Check that the missing dihedrals are in the original dihedrals at lambda = 1.
384+ assert (
385+ all (
386+ check_dihedral (info , dihedrals1 .potentials (), * dihedral )
387+ for dihedral in missing_dihedrals1
388+ )
389+ == True
390+ )
391+
392+ # Check that the missing dihedrals are not in the new dihedrals at lambda = 1.
393+ assert (
394+ all (
395+ check_dihedral (info , new_dihedrals1 .potentials (), * dihedral )
396+ for dihedral in missing_dihedrals1
397+ )
398+ == False
399+ )
400+
401+ # Create angle IDs for the modified angles at lambda = 0.
402+ modified_angles0 = [
403+ (AtomIdx (16 ), AtomIdx (17 ), AtomIdx (39 )),
404+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (39 )),
405+ ]
406+
407+ # Functional form of the modified angles.
408+ expression = "100 [theta - 1.5708]^2"
409+
410+ # Check that the original angles don't have the modified functional form.
411+ for p in angles0 .potentials ():
412+ idx0 = info .atom_idx (p .atom0 ())
413+ idx1 = info .atom_idx (p .atom1 ())
414+ idx2 = info .atom_idx (p .atom2 ())
415+
416+ if (idx0 , idx1 , idx2 ) in modified_angles0 :
417+ assert str (p .function ()) != expression
418+
419+ # Check that the modified angles have the correct functional form.
420+ for p in new_angles0 .potentials ():
421+ idx0 = info .atom_idx (p .atom0 ())
422+ idx1 = info .atom_idx (p .atom1 ())
423+ idx2 = info .atom_idx (p .atom2 ())
424+
425+ if (idx0 , idx1 , idx2 ) in modified_angles0 :
426+ assert str (p .function ()) == expression
427+
428+ # Create angle IDs for the modified angles at lambda = 1.
429+ modified_angles1 = [
430+ (AtomIdx (16 ), AtomIdx (17 ), AtomIdx (20 )),
431+ (AtomIdx (18 ), AtomIdx (17 ), AtomIdx (20 )),
432+ ]
433+
434+ # Check that the original angles don't have the modified functional form.
435+ for p in angles1 .potentials ():
436+ idx0 = info .atom_idx (p .atom0 ())
437+ idx1 = info .atom_idx (p .atom1 ())
438+ idx2 = info .atom_idx (p .atom2 ())
439+
440+ if (idx0 , idx1 , idx2 ) in modified_angles1 :
441+ assert str (p .function ()) != expression
442+
443+ # Check that the modified angles have the correct functional form.
444+ for p in new_angles1 .potentials ():
445+ idx0 = info .atom_idx (p .atom0 ())
446+ idx1 = info .atom_idx (p .atom1 ())
447+ idx2 = info .atom_idx (p .atom2 ())
448+
449+ if (idx0 , idx1 , idx2 ) in modified_angles1 :
450+ assert str (p .function ()) == expression
451+
452+ # Create improper IDs for the missing impropers at lambda = 0.
453+ missing_impropers0 = [
454+ (AtomIdx (17 ), AtomIdx (16 ), AtomIdx (18 ), AtomIdx (39 )),
455+ (AtomIdx (16 ), AtomIdx (39 ), AtomIdx (18 ), AtomIdx (17 )),
456+ (AtomIdx (17 ), AtomIdx (39 ), AtomIdx (16 ), AtomIdx (18 )),
457+ ]
458+
459+ # Check that the missing impropers are in the original impropers at lambda = 0.
460+ assert (
461+ all (
462+ check_improper (info , improper0 .potentials (), * improper )
463+ for improper in missing_impropers0
464+ )
465+ == True
466+ )
467+
468+ # Check that the missing impropers are not in the new impropers at lambda = 0.
469+ assert (
470+ all (
471+ check_improper (info , new_improper0 .potentials (), * improper )
472+ for improper in missing_impropers0
473+ )
474+ == False
475+ )
476+
477+ # Create improper IDs for the missing impropers at lambda = 1.
478+ missing_impropers1 = [
479+ (AtomIdx (17 ), AtomIdx (25 ), AtomIdx (21 ), AtomIdx (20 )),
480+ (AtomIdx (17 ), AtomIdx (20 ), AtomIdx (16 ), AtomIdx (18 )),
481+ (AtomIdx (16 ), AtomIdx (20 ), AtomIdx (18 ), AtomIdx (17 )),
482+ (AtomIdx (17 ), AtomIdx (20 ), AtomIdx (16 ), AtomIdx (18 )),
483+ (AtomIdx (20 ), AtomIdx (25 ), AtomIdx (17 ), AtomIdx (21 )),
484+ (AtomIdx (16 ), AtomIdx (20 ), AtomIdx (18 ), AtomIdx (17 )),
485+ (AtomIdx (20 ), AtomIdx (17 ), AtomIdx (21 ), AtomIdx (25 )),
486+ ]
487+
488+ # Check that the missing impropers are in the original impropers at lambda = 1.
489+ assert (
490+ all (
491+ check_improper (info , improper1 .potentials (), * improper )
492+ for improper in missing_impropers1
493+ )
494+ == True
495+ )
496+
497+ # Check that the missing impropers are not in the new impropers at lambda = 1.
498+ assert (
499+ all (
500+ check_improper (info , new_improper1 .potentials (), * improper )
501+ for improper in missing_impropers1
502+ )
503+ == False
504+ )
505+
506+
294507def check_angle (info , potentials , idx0 , idx1 , idx2 ):
295508 """
296509 Check if an angle potential is in a list of potentials.
@@ -322,3 +535,20 @@ def check_dihedral(info, potentials, idx0, idx1, idx2, idx3):
322535 return True
323536
324537 return False
538+
539+
540+ def check_improper (info , potentials , idx0 , idx1 , idx2 , idx3 ):
541+ """
542+ Check if an improper potential is in a list of potentials.
543+ """
544+
545+ for p in potentials :
546+ if (
547+ idx0 == info .atom_idx (p .atom0 ())
548+ and idx1 == info .atom_idx (p .atom1 ())
549+ and idx2 == info .atom_idx (p .atom2 ())
550+ and idx3 == info .atom_idx (p .atom3 ())
551+ ):
552+ return True
553+
554+ return False
0 commit comments