|
1 | 1 | ###################################################################### |
2 | 2 | # SOMD2: GPU accelerated alchemical free-energy engine. |
3 | 3 | # |
4 | | -# Copyright: 2023-2024 |
| 4 | +# Copyright: 2023-2025 |
5 | 5 | # |
6 | 6 | # Authors: The OpenBioSim Team <team@openbiosim.org> |
7 | 7 | # |
|
19 | 19 | # along with SOMD2. If not, see <http://www.gnu.org/licenses/>. |
20 | 20 | ##################################################################### |
21 | 21 |
|
| 22 | +__all__ = ["apply_pert", "make_compatible", "reconstruct_system"] |
| 23 | + |
22 | 24 | from sire.system import System as _System |
23 | 25 | from sire.legacy.System import System as _LegacySystem |
24 | 26 |
|
25 | 27 | import sire.legacy.MM as _SireMM |
26 | 28 | import sire.legacy.Mol as _SireMol |
27 | 29 |
|
28 | 30 |
|
29 | | -def _make_compatible(system): |
| 31 | +def apply_pert(system, pert_file): |
| 32 | + """ |
| 33 | + Helper function to apply a perturbation to a reference system. |
| 34 | +
|
| 35 | + Parameters |
| 36 | + ---------- |
| 37 | +
|
| 38 | + system: sr.system.System |
| 39 | + The reference system. |
| 40 | +
|
| 41 | + pert_file: str |
| 42 | + Path to a stream file containing the perturbation to apply to the |
| 43 | + reference system. |
| 44 | +
|
| 45 | + Returns |
| 46 | + ------- |
| 47 | +
|
| 48 | + system: sire.system.System |
| 49 | + The perturbable system. |
| 50 | + """ |
| 51 | + |
| 52 | + if not isinstance(system, _System): |
| 53 | + raise TypeError("'system' must be of type 'sr.system.System'.") |
| 54 | + |
| 55 | + if not isinstance(pert_file, str): |
| 56 | + raise TypeError("'pert_file' must be of type 'str'.") |
| 57 | + |
| 58 | + from sire import morph as _morph |
| 59 | + |
| 60 | + # Get the non-water molecules in the system. |
| 61 | + non_waters = system["not water"].molecules() |
| 62 | + |
| 63 | + # Try to apply the perturbation to each non-water molecule. |
| 64 | + is_pert = False |
| 65 | + for mol in non_waters: |
| 66 | + # Exclude ions. |
| 67 | + if mol.num_atoms() > 1: |
| 68 | + try: |
| 69 | + pert_mol = _morph.create_from_pertfile(mol, pert_file) |
| 70 | + is_pert = True |
| 71 | + break |
| 72 | + except: |
| 73 | + pass |
| 74 | + |
| 75 | + if not is_pert: |
| 76 | + raise ValueError(f"Failed to apply the perturbation in '{pert_file}'.") |
| 77 | + |
| 78 | + # Update the molecule. |
| 79 | + system.update(pert_mol) |
| 80 | + |
| 81 | + # Link to the reference state. |
| 82 | + system = _morph.link_to_reference(system) |
| 83 | + |
| 84 | + return system |
| 85 | + |
| 86 | + |
| 87 | +def make_compatible(system): |
30 | 88 | """ |
31 | 89 | Makes a perturbation SOMD1 compatible. |
32 | 90 |
|
@@ -547,54 +605,108 @@ def _make_compatible(system): |
547 | 605 | return system |
548 | 606 |
|
549 | 607 |
|
550 | | -def _apply_pert(system, pert_file): |
| 608 | +def reconstruct_system(system): |
551 | 609 | """ |
552 | | - Helper function to apply a perturbation to a reference system. |
| 610 | + Reconstruct a perturbable system to its original state, i.e. extract the |
| 611 | + end states for each perturbable molecule and re-merge them using the original |
| 612 | + mapping. This removes any ghost atom modifications applied via a perturbation |
| 613 | + file. |
553 | 614 |
|
554 | 615 | Parameters |
555 | 616 | ---------- |
556 | 617 |
|
557 | | - system: sr.system.System |
558 | | - The reference system. |
559 | | -
|
560 | | - pert_file: str |
561 | | - Path to a stream file containing the perturbation to apply to the |
562 | | - reference system. |
| 618 | + system : sire.system.System, sire.legacy.System.System |
| 619 | + The system containing the molecules to be perturbed. |
563 | 620 |
|
564 | 621 | Returns |
565 | 622 | ------- |
566 | 623 |
|
567 | | - system: sire.system.System |
568 | | - The perturbable system. |
| 624 | + system : sire.system.System |
| 625 | + The updated system. |
569 | 626 | """ |
570 | 627 |
|
571 | | - if not isinstance(system, _System): |
572 | | - raise TypeError("'system' must be of type 'sr.system.System'.") |
573 | | - |
574 | | - if not isinstance(pert_file, str): |
575 | | - raise TypeError("'pert_file' must be of type 'str'.") |
| 628 | + import BioSimSpace as _BSS |
576 | 629 |
|
577 | 630 | from sire import morph as _morph |
578 | 631 |
|
579 | | - # Get the non-water molecules in the system. |
580 | | - non_waters = system["not water"].molecules() |
| 632 | + # Check the system is a Sire system. |
| 633 | + if not isinstance(system, (_System, _LegacySystem)): |
| 634 | + raise TypeError( |
| 635 | + "'system' must of type 'sire.system.System' or 'sire.legacy.System.System'" |
| 636 | + ) |
581 | 637 |
|
582 | | - # Try to apply the perturbation to each non-water molecule. |
583 | | - is_pert = False |
584 | | - for mol in non_waters: |
585 | | - # Exclude ions. |
586 | | - if mol.num_atoms() > 1: |
587 | | - try: |
588 | | - pert_mol = _morph.create_from_pertfile(mol, pert_file) |
589 | | - is_pert = True |
590 | | - break |
591 | | - except: |
592 | | - pass |
| 638 | + # Extract the legacy system. |
| 639 | + if isinstance(system, _LegacySystem): |
| 640 | + system = _System(system) |
593 | 641 |
|
594 | | - if not is_pert: |
595 | | - raise ValueError(f"Failed to apply the perturbation in '{pert_file}'.") |
| 642 | + # Clone the system. |
| 643 | + system = system.clone() |
596 | 644 |
|
597 | | - # Update the molecule. |
598 | | - system.update(pert_mol) |
| 645 | + # Search for perturbable molecules. |
| 646 | + try: |
| 647 | + pert_mols = system.molecules("property is_perturbable") |
| 648 | + except KeyError: |
| 649 | + raise KeyError("No perturbable molecules in the system") |
| 650 | + |
| 651 | + # Store a dummy element for ghost atoms. |
| 652 | + ghost = _SireMol.Element(0) |
| 653 | + |
| 654 | + # Loop over all perturbable molecules. |
| 655 | + for mol in pert_mols: |
| 656 | + |
| 657 | + # Extract the end states. |
| 658 | + ref = _morph.extract_reference(mol) |
| 659 | + pert = _morph.extract_perturbed(mol) |
| 660 | + |
| 661 | + # Find the indices for non-ghost atoms. |
| 662 | + ref_idxs = [] |
| 663 | + for x, (a, e) in enumerate( |
| 664 | + zip(ref.property("ambertype"), ref.property("element")) |
| 665 | + ): |
| 666 | + if a == "du" or e == ghost: |
| 667 | + continue |
| 668 | + else: |
| 669 | + ref_idxs.append(x) |
| 670 | + pert_idxs = [] |
| 671 | + for x, (a, e) in enumerate( |
| 672 | + zip(pert.property("ambertype"), pert.property("element")) |
| 673 | + ): |
| 674 | + if a == "du" or e == ghost: |
| 675 | + continue |
| 676 | + else: |
| 677 | + pert_idxs.append(x) |
| 678 | + |
| 679 | + # Convert to BioSimSpace molecules and extract the non-ghost atoms. |
| 680 | + ref = _BSS._SireWrappers.Molecule(ref).extract(ref_idxs) |
| 681 | + pert = _BSS._SireWrappers.Molecule(pert).extract(pert_idxs) |
| 682 | + |
| 683 | + # Work out the mapping. |
| 684 | + idx0 = 0 |
| 685 | + idx1 = 0 |
| 686 | + mapping = {} |
| 687 | + for x, atom in enumerate(mol.atoms()): |
| 688 | + at0 = atom.property("ambertype0") |
| 689 | + at1 = atom.property("ambertype1") |
| 690 | + |
| 691 | + if at0 != "du" and at1 != "du": |
| 692 | + mapping[idx0] = idx1 |
| 693 | + |
| 694 | + if at0 != "du": |
| 695 | + idx0 += 1 |
| 696 | + |
| 697 | + if at1 != "du": |
| 698 | + idx1 += 1 |
| 699 | + |
| 700 | + # Re-merge the molecules. |
| 701 | + merged = _BSS.Align.merge(ref, pert, mapping=mapping, force=True) |
| 702 | + |
| 703 | + # Give the molecule the same number as the original. |
| 704 | + merged = merged._sire_object.edit().renumber(mol.number()).molecule().commit() |
| 705 | + |
| 706 | + # Update the system. |
| 707 | + system.update(merged) |
| 708 | + |
| 709 | + # Link to the reference state. |
| 710 | + system = _morph.link_to_reference(system) |
599 | 711 |
|
600 | 712 | return system |
0 commit comments