From b6d5ba9b46adc582bc3735a40234778d94e98add Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Oct 2022 11:08:38 -0700 Subject: [PATCH 01/34] update evaluatir modules --- paprika/evaluator/analyze.py | 107 ++++++++++++++++---------------- paprika/evaluator/setup.py | 40 +++++++----- paprika/tests/test_evaluator.py | 5 +- 3 files changed, 83 insertions(+), 69 deletions(-) diff --git a/paprika/evaluator/analyze.py b/paprika/evaluator/analyze.py index de7149f..12b2e1a 100644 --- a/paprika/evaluator/analyze.py +++ b/paprika/evaluator/analyze.py @@ -3,12 +3,14 @@ """ import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import numpy as np +from openff.units import unit as openff_unit from paprika.analysis import fe_calc from paprika.restraints import DAT_restraint +from paprika.utils import check_unit logger = logging.getLogger(__name__) @@ -75,10 +77,10 @@ def compute_phase_free_energy( @classmethod def compute_ref_state_work( cls, - temperature: float, + temperature: [float, openff_unit.Quantity], guest_restraints: List[DAT_restraint], - ) -> float: - """Computes the reference state work of the attach phase. + ) -> openff_unit.Quantity: + """Computes the reference state work of the 'attach' phase. Parameters ---------- @@ -95,51 +97,49 @@ def compute_ref_state_work( analysis = fe_calc() analysis.temperature = temperature - distance_restraint = next( - ( - restraint - for restraint in guest_restraints - if "DM" in restraint.mask1 - and restraint.mask2 is not None - and restraint.mask3 is None - and restraint.mask4 is None - ), - None, - ) - - theta_restraint = next( - ( - restraint - for restraint in guest_restraints - if "DM" in restraint.mask1 - and "DM" in restraint.mask2 - and restraint.mask3 is not None - and restraint.mask4 is None - ), - None, - ) - beta_restraint = next( - ( - restraint - for restraint in guest_restraints - if "DM" in restraint.mask1 - and "DM" not in restraint.mask2 - and restraint.mask3 is not None - and restraint.mask4 is None - ), - None, - ) - - if not distance_restraint or not theta_restraint or not beta_restraint: - + boresch_restraints = { + "r": None, + "theta": None, + "phi": None, + "alpha": None, + "beta": None, + "gamma": None, + } + + for restraint in guest_restraints: + # Distance + if not restraint.mask3 and not restraint.mask4: + boresch_restraints["r"] = restraint + + # Angle + elif restraint.mask3 and not restraint.mask4: + if "DM2" in restraint.mask1 and "DM1" in restraint.mask2: + boresch_restraints["theta"] = restraint + elif "DM1" in restraint.mask1 and "DM" not in restraint.mask2: + boresch_restraints["beta"] = restraint + + # Dihedral restraints + if restraint.mask4: + if ( + "DM3" in restraint.mask1 + and "DM2" in restraint.mask2 + and "DM1" in restraint.mask3 + ): + boresch_restraints["phi"] = restraint + + elif "DM2" in restraint.mask1 and "DM1" in restraint.mask2: + boresch_restraints["beta"] = restraint + + elif "DM1" in restraint.mask1: + boresch_restraints["gamma"] = restraint + + if boresch_restraints["r"] is None: raise RuntimeError( - "Could not determine the r, θ, or β restraint for computing the " - "analytic release step." + "Could not determine the `r` restraint! Need at least `r` restraint " + "for computing the analytic release step." ) - analysis.compute_ref_state_work( - [distance_restraint, theta_restraint, None, None, beta_restraint, None] - ) + analysis.compute_ref_state_work(boresch_restraints) return analysis.results["ref_state_work"] @@ -147,19 +147,19 @@ def compute_ref_state_work( def symmetry_correction( cls, n_microstates: int, - temperature: float, - ) -> float: + temperature: Union[float, openff_unit.Quantity], + ) -> openff_unit.Quantity: """Computes the free energy corrections to apply to symmetrical guest when the guest is restrained to just one of several possible symmetrical configurations (e.g. butane restrained in a cyclic host). Parameters ---------- - temperature - The temperature at which the calculation was performed in units of kelvin. n_microstates The number of different symmetrical microstates that the guest can exist in (e.g. for butane this is two). + temperature + The temperature at which the calculation was performed in units of kelvin. Returns ------- @@ -169,6 +169,9 @@ def symmetry_correction( assert n_microstates > 0 if n_microstates == 1: - return 0.0 + return 0.0 * openff_unit.kcal / openff_unit.mol + + k_B = 1.987204118e-3 * openff_unit.kcal / openff_unit.mol / openff_unit.kelvin + temperature = check_unit(temperature, base_unit=openff_unit.kelvin) - return -temperature * 0.001987204258640832 * np.log(n_microstates) + return -temperature * k_B * np.log(n_microstates) diff --git a/paprika/evaluator/setup.py b/paprika/evaluator/setup.py index 0c4f885..e52c1ca 100644 --- a/paprika/evaluator/setup.py +++ b/paprika/evaluator/setup.py @@ -315,7 +315,7 @@ def build_conformational_restraints( The path to the coordinate file which the restraints will be applied to. This should contain either the host or the complex, the dummy atoms and solvent. attach_lambdas - The values 'lambda' being used during the attach phase of the APR + The values 'lambda' being used during the 'attach' phase of the APR calculation. n_pull_windows The total number of pull windows being used in the APR calculation. @@ -394,7 +394,7 @@ def build_symmetry_restraints( * a ``force_constant`` entry which specifies the force constant of the restraint. - These 'schemas` map directly to the 'restraints -> symmetry_correction + These `schemas` map directly to the 'restraints -> symmetry_correction -> restraint' dictionaries specified in the `taproom` guest YAML files. Parameters @@ -402,7 +402,7 @@ def build_symmetry_restraints( coordinate_path The path to the coordinate file which the restraints will be applied to. This should contain either the host or the complex, the dummy atoms and - and solvent. + solvent. n_attach_windows The total number of attach windows being used in the APR calculation. restraint_schemas @@ -435,12 +435,12 @@ def build_symmetry_restraints( # This target will be overridden by the custom values. restraint.attach["target"] = 91 * openff_unit.degrees - restraint.custom_restraint_values["r2"] = 91 * openff_unit.degrees - restraint.custom_restraint_values["r3"] = 91 * openff_unit.degrees + restraint.custom_restraint_values["r2"] = restraint.attach["target"] + restraint.custom_restraint_values["r3"] = restraint.attach["target"] # 0 force constant between 91 degrees and 180 degrees. restraint.custom_restraint_values["rk3"] = ( - 0.0 * openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 + 0.0 * restraint_schema["force_constant"].units ) restraint.initialize() @@ -467,7 +467,7 @@ def build_wall_restraints( restraint. * a ``target`` entry which specifies the target value of the restraint. - These 'schemas` map directly to the 'restraints -> wall_restraints -> restraint' + These `schemas` map directly to the 'restraints -> wall_restraints -> restraint' dictionaries specified in the `taproom` guest YAML files. Parameters @@ -475,7 +475,7 @@ def build_wall_restraints( coordinate_path The path to the coordinate file which the restraints will be applied to. This should contain either the host or the complex, the dummy atoms and - and solvent. + solvent. n_attach_windows The total number of attach windows being used in the APR calculation. restraint_schemas @@ -494,21 +494,29 @@ def build_wall_restraints( for restraint_schema in restraint_schemas: + mask = restraint_schema["atoms"].split() + restraint = DAT_restraint() restraint.auto_apr = True restraint.continuous_apr = False restraint.amber_index = use_amber_indices restraint.topology = coordinate_path - restraint.mask1 = restraint_schema["atoms"].split()[0] - restraint.mask2 = restraint_schema["atoms"].split()[1] + restraint.mask1 = mask[0] + restraint.mask2 = mask[1] + restraint.mask3 = mask[2] if len(mask) > 2 else None + restraint.mask4 = mask[3] if len(mask) > 3 else None restraint.attach["fc_final"] = restraint_schema["force_constant"] restraint.attach["fraction_list"] = [1.0] * n_attach_windows restraint.attach["target"] = restraint_schema["target"] - # Minimum distance is 0 Angstrom - restraint.custom_restraint_values["r1"] = 0 * openff_unit.degrees - restraint.custom_restraint_values["r2"] = 0 * openff_unit.degrees + # Set lower bounds to zero + restraint.custom_restraint_values["r1"] = ( + 0 * restraint.attach["target"].units + ) + restraint.custom_restraint_values["r2"] = ( + 0 * restraint.attach["target"].units + ) # Harmonic force constant beyond target distance. restraint.custom_restraint_values["rk2"] = restraint_schema[ @@ -546,7 +554,7 @@ def build_guest_restraints( restraint. * a ``target`` entry which specifies the target value of the restraint. - These 'schemas` map directly to the 'restraints -> guest -> restraint' + These `schemas` map directly to the 'restraints -> guest -> restraint' dictionaries specified in the `taproom` guest YAML files. Parameters @@ -554,9 +562,9 @@ def build_guest_restraints( coordinate_path The path to the coordinate file which the restraints will be applied to. This should contain either the host or the complex, the dummy atoms and - and solvent. + solvent. attach_lambdas - The values 'lambda' being used during the attach phase of the APR + The values 'lambda' being used during the 'attach' phase of the APR calculation. n_pull_windows The total number of pull windows being used in the APR calculation. diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index 11fd4fa..29f14ac 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -234,7 +234,10 @@ def test_evaluator_gaff(clean_files): ) # fmt: off - butane_atom_type = ["c3", "hc", "hc", "hc", "c3", "hc", "c3", "hc", "hc", "hc", "c3", "hc", "hc", "hc"] + butane_atom_type = [ + "c3", "hc", "hc", "hc", "c3", "hc", "c3", + "hc", "hc", "hc", "c3", "hc", "hc", "hc", + ] # fmt: on residue_names = [] From 24833e47dffd9b75c85659a07597caddf241f2ac Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Oct 2022 14:20:38 -0700 Subject: [PATCH 02/34] update read_yaml module for taproom --- paprika/evaluator/setup.py | 24 +++++++++++++------- paprika/restraints/read_yaml.py | 40 ++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/paprika/evaluator/setup.py b/paprika/evaluator/setup.py index e52c1ca..424e3a2 100644 --- a/paprika/evaluator/setup.py +++ b/paprika/evaluator/setup.py @@ -432,16 +432,24 @@ def build_symmetry_restraints( restraint.attach["fc_final"] = restraint_schema["force_constant"] restraint.attach["fraction_list"] = [1.0] * n_attach_windows + restraint.attach["target"] = restraint_schema["target"] - # This target will be overridden by the custom values. - restraint.attach["target"] = 91 * openff_unit.degrees - restraint.custom_restraint_values["r2"] = restraint.attach["target"] - restraint.custom_restraint_values["r3"] = restraint.attach["target"] - - # 0 force constant between 91 degrees and 180 degrees. - restraint.custom_restraint_values["rk3"] = ( - 0.0 * restraint_schema["force_constant"].units + # Set upper bounds to zero + restraint.custom_restraint_values["r3"] = ( + 0 * restraint.attach["target"].units ) + restraint.custom_restraint_values["r4"] = ( + 0 * restraint.attach["target"].units + ) + + # Harmonic force constant beyond target distance. + restraint.custom_restraint_values["rk2"] = restraint_schema[ + "force_constant" + ] + restraint.custom_restraint_values["rk3"] = restraint_schema[ + "force_constant" + ] + restraint.initialize() restraints.append(restraint) diff --git a/paprika/restraints/read_yaml.py b/paprika/restraints/read_yaml.py index a32c392..c16c43f 100644 --- a/paprika/restraints/read_yaml.py +++ b/paprika/restraints/read_yaml.py @@ -2,11 +2,12 @@ import re import yaml +from openff.units import unit as openff_unit logger = logging.getLogger(__name__) -def read_yaml(file): +def read_taproom_yaml(file): """ Read `Taproom `_ -style YAML-formatted instructions for preparing host-guest systems. @@ -23,14 +24,20 @@ def read_yaml(file): """ + # Read YAML file with open(file, "r") as f: yaml_data = yaml.safe_load(f) logger.debug(yaml_data) + # Convert aliases to atom masks if "aliases" in yaml_data.keys(): logger.debug("Dealiasing atom masks...") yaml_data = de_alias(yaml_data) + # Convert all string to OpenFF Quantity + logger.debug("Converting string to unit.Quantity...") + convert_string_to_quantity(yaml_data) + return yaml_data @@ -67,9 +74,36 @@ def de_alias(yaml_data): if "symmetry_correction" in yaml_data.keys(): for restraint in yaml_data["symmetry_correction"]["restraints"]: - atoms = restraint["atoms"] + atoms = restraint["restraint"]["atoms"] mapped_atoms = multiple_replace(mapping_dictionary, atoms) logger.info(f"{atoms} → {mapped_atoms}") - restraint["atoms"] = mapped_atoms + restraint["restraint"]["atoms"] = mapped_atoms return yaml_data + + +def convert_string_to_quantity(yaml_data): + """ + Convert strings for 'force_constant' and 'targets' to `unit.Quantity` + """ + + def _to_quantity(string, key): + if string in key: + value = key[string] + key[string] = openff_unit.Quantity(value) + + for restraint_type, restraint_type_list in yaml_data["restraints"].items(): + for restraint in restraint_type_list: + _to_quantity("force_constant", restraint["restraint"]) + _to_quantity("target", restraint["restraint"]) + if "attach" in restraint["restraint"]: + _to_quantity("force_constant", restraint["restraint"]["attach"]) + _to_quantity("target", restraint["restraint"]["attach"]) + if "pull" in restraint["restraint"]: + _to_quantity("force_constant", restraint["restraint"]["pull"]) + _to_quantity("target", restraint["restraint"]["pull"]) + + if "symmetry_correction" in yaml_data.keys(): + for restraint in yaml_data["symmetry_correction"]["restraints"]: + _to_quantity("force_constant", restraint["restraint"]) + _to_quantity("target", restraint["restraint"]) From d8d85a3d628ce6f12b1a1879e54f13a4c6c6300c Mon Sep 17 00:00:00 2001 From: jeff231li Date: Wed, 2 Nov 2022 11:07:42 -0700 Subject: [PATCH 03/34] refactor code --- paprika/restraints/{read_yaml.py => taproom.py} | 17 +++-------------- paprika/utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) rename paprika/restraints/{read_yaml.py => taproom.py} (88%) diff --git a/paprika/restraints/read_yaml.py b/paprika/restraints/taproom.py similarity index 88% rename from paprika/restraints/read_yaml.py rename to paprika/restraints/taproom.py index c16c43f..8dd2ee2 100644 --- a/paprika/restraints/read_yaml.py +++ b/paprika/restraints/taproom.py @@ -1,13 +1,14 @@ import logging -import re import yaml from openff.units import unit as openff_unit +from paprika.utils import multiple_replace + logger = logging.getLogger(__name__) -def read_taproom_yaml(file): +def read_yaml_schema(file): """ Read `Taproom `_ -style YAML-formatted instructions for preparing host-guest systems. @@ -41,18 +42,6 @@ def read_taproom_yaml(file): return yaml_data -def multiple_replace(dct, text): - """ - Create a regular expression to do multiple find and replace. - """ - - # Create a regular expression from the dictionary keys - regex = re.compile("(%s)" % "|".join(map(re.escape, dct.keys()))) - - # For each match, look-up corresponding value in dictionary - return regex.sub(lambda mo: dct[mo.string[mo.start() : mo.end()]], text) - - def de_alias(yaml_data): """ Replace aliased atoms in a ``taproom`` recipe. diff --git a/paprika/utils.py b/paprika/utils.py index d366e54..46ee496 100644 --- a/paprika/utils.py +++ b/paprika/utils.py @@ -1,5 +1,6 @@ import logging import os as os +import re import shutil from datetime import datetime from functools import lru_cache @@ -349,3 +350,15 @@ def check_unit(variable, base_unit): raise KeyError("``variable`` should be a float or openff.unit.Quantity.") return quantity + + +def multiple_replace(dct, text): + """ + Create a regular expression to do multiple find and replace. + """ + + # Create a regular expression from the dictionary keys + regex = re.compile("(%s)" % "|".join(map(re.escape, dct.keys()))) + + # For each match, look-up corresponding value in dictionary + return regex.sub(lambda mo: dct[mo.string[mo.start() : mo.end()]], text) From cd9b95228911b19de3d1e36a97c5d3e8cc8d1b9f Mon Sep 17 00:00:00 2001 From: jeff231li Date: Wed, 2 Nov 2022 11:08:04 -0700 Subject: [PATCH 04/34] add unit tests --- paprika/tests/test_evaluator.py | 421 ++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index 29f14ac..060986e 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -9,11 +9,17 @@ import parmed as pmd import pytest import pytraj as pt +import yaml from openff.units import unit as openff_unit from paprika.evaluator import Analyze, Setup from paprika.evaluator.amber import generate_gaff from paprika.restraints import DAT_restraint +from paprika.restraints.taproom import ( + convert_string_to_quantity, + de_alias, + read_yaml_schema, +) logger = logging.getLogger(__name__) @@ -29,6 +35,300 @@ def clean_files(directory=os.path.join(os.path.dirname(__file__), "tmp")): shutil.rmtree(directory) +@pytest.fixture() +def complex_file(): + complex_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") + + butane_molecule = [] + structure = pmd.load_file(complex_pdb, structure=True) + for atom in structure.topology.atoms(): + if atom.residue.name == "BUT": + butane_molecule.append(atom.index) + + G1 = ":BUT@C" + G2 = ":BUT@C3" + + host_guest = Setup.prepare_complex_structure( + complex_pdb, + butane_molecule, + f"{G1} {G2}", + 24.0, + 0, + 46, + ) + + Setup.add_dummy_atoms_to_structure( + host_guest, + [ + np.array([0, 0, 0]), + np.array([0, 0, -3.0]), + np.array([0, 2.2, -5.2]), + ], + np.zeros(3), + ) + + return host_guest + + +@pytest.fixture(scope="module") +def yaml_restraint_schema(): + yaml_file = """name: bam +structure: bam.mol2 +complex: a-bam.pdb +net_charge: +1e +aliases: + - D1: :DM1 + - D2: :DM2 + - D3: :DM3 + - G1: :BAM@C4 + - G2: :BAM@N1 +restraints: + guest: + - restraint: + atoms: D1 G1 + attach: + # During the 'attach' phase, the `force_constant` argument is the + # final force constant. + force_constant: 5.0 * kilocalorie / mole / angstrom**2 + target: 6.0 * angstrom + pull: + # During the 'pull' phase, the `target` argument is the final value of + # the restraint. + force_constant: 5.0 * kilocalorie / mole / angstrom**2 + target: 24.0 * angstrom + - restraint: + atoms: D2 D1 G1 + attach: + force_constant: 100.0 * kilocalorie / mole / radians**2 + target: 180.0 * degrees + pull: + force_constant: 100.0 * kilocalorie / mole / radians**2 + target: 180.0 * degrees + - restraint: + atoms: D1 G1 G2 + attach: + force_constant: 100.0 * kilocalorie / mole / radians**2 + target: 180.0 * degrees + pull: + force_constant: 100.0 * kilocalorie / mole / radians**2 + target: 180.0 * degrees + + wall_restraints: + - restraint: + atoms: ":1@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":2@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":3@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":4@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":5@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":6@O2 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 9.3 * angstrom + - restraint: + atoms: ":1@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + - restraint: + atoms: ":2@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + - restraint: + atoms: ":3@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + - restraint: + atoms: ":4@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + - restraint: + atoms: ":5@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + - restraint: + atoms: ":6@O6 G1" + force_constant: 50.0 * kilocalorie / mole / angstrom**2 + target: 11.3 * angstrom + +symmetry_correction: + restraints: + - restraint: + atoms: D2 G1 G2 + force_constant: 200.0 * kilocalorie / mole / radian**2 + target: 91 * degrees + # Do not attempt to automatically correct for the symmetry restraint by adding -RT \ln (microstates). + # Instead, we will apply the symmetry restraint, which locks in a particular binding orientation, and then + # perform separate calculations. + microstates: 1""" + return yaml_file + + +@pytest.fixture(scope="module") +def restraints_schema(): + schema = { + "static": [ + { + "atoms": ":DM1 :CB6@O", + "force_constant": 5.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.angstrom**2, + } + ], + "conformational": [ + { + "atoms": ":CB6@O :CB6@O2 :CB6@O4 :CB6@O6", + "force_constant": 6.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.radians**2, + "target": 104.3 * openff_unit.degrees, + } + ], + "symmetry": [ + { + "atoms": ":DM2 :BUT@C :BUT@C3", + "force_constant": 50.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.radians**2, + "target": 11.0 * openff_unit.degrees, + } + ], + "wall": [ + { + "atoms": ":CB6@O :BUT@C", + "force_constant": 50.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.angstrom**2, + "target": 11.0 * openff_unit.angstrom, + } + ], + "guest": [ + { + "atoms": ":DM1 :BUT@C", + "attach": { + "force_constant": 5.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.angstrom**2, + "target": 6.0 * openff_unit.angstrom, + }, + "pull": { + "force_constant": 5.0 + * openff_unit.kcal + / openff_unit.mol + / openff_unit.angstrom**2, + "target": 24.0 * openff_unit.angstrom, + }, + } + ], + } + return schema + + +def test_taproom_yaml(clean_files, yaml_restraint_schema): + temporary_directory = os.path.join(os.path.dirname(__file__), "tmp") + + k_dist = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 + k_wall = 50.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 + k_angle = 100.0 * openff_unit.kcal / openff_unit.mole / openff_unit.radian**2 + r_initial = 6.0 * openff_unit.angstrom + r_final = 24.0 * openff_unit.angstrom + angle = 180.0 * openff_unit.degrees + + # Write yaml string to file + with open(f"{temporary_directory}/guest.yaml", "w") as f: + f.write(yaml_restraint_schema) + + # Check de_alias + with open(f"{temporary_directory}/guest.yaml", "r") as f: + yaml_data = yaml.safe_load(f) + + if "aliases" in yaml_data.keys(): + yaml_data = de_alias(yaml_data) + + distance_restraint = yaml_data["restraints"]["guest"][0]["restraint"] + theta_restraint = yaml_data["restraints"]["guest"][1]["restraint"] + beta_restraint = yaml_data["restraints"]["guest"][2]["restraint"] + symmetry_restraint = yaml_data["symmetry_correction"]["restraints"][0]["restraint"] + + assert distance_restraint["atoms"] == ":DM1 :BAM@C4" + assert theta_restraint["atoms"] == ":DM2 :DM1 :BAM@C4" + assert beta_restraint["atoms"] == ":DM1 :BAM@C4 :BAM@N1" + assert symmetry_restraint["atoms"] == ":DM2 :BAM@C4 :BAM@N1" + + # Check convert string to quantity + convert_string_to_quantity(yaml_data) + + assert distance_restraint["attach"]["force_constant"] == k_dist + assert theta_restraint["attach"]["force_constant"] == k_angle + assert beta_restraint["attach"]["force_constant"] == k_angle + + assert distance_restraint["pull"]["force_constant"] == k_dist + assert theta_restraint["pull"]["force_constant"] == k_angle + assert beta_restraint["pull"]["force_constant"] == k_angle + + assert distance_restraint["attach"]["target"] == r_initial + assert distance_restraint["pull"]["target"] == r_final + + assert theta_restraint["attach"]["target"] == angle + assert theta_restraint["pull"]["target"] == angle + assert beta_restraint["attach"]["target"] == angle + assert beta_restraint["pull"]["target"] == angle + + for i, restraint in enumerate(yaml_data["restraints"]["wall_restraints"]): + assert restraint["restraint"]["force_constant"] == k_wall + if i < 6: + assert restraint["restraint"]["target"] == 9.3 * openff_unit.angstrom + else: + assert restraint["restraint"]["target"] == 11.3 * openff_unit.angstrom + + # Check full conversion + guest_spec = read_yaml_schema(f"{temporary_directory}/guest.yaml") + + distance_restraint = guest_spec["restraints"]["guest"][0]["restraint"] + theta_restraint = guest_spec["restraints"]["guest"][1]["restraint"] + beta_restraint = guest_spec["restraints"]["guest"][2]["restraint"] + + assert distance_restraint["attach"]["force_constant"] == k_dist + assert theta_restraint["attach"]["force_constant"] == k_angle + assert beta_restraint["attach"]["force_constant"] == k_angle + + assert distance_restraint["pull"]["force_constant"] == k_dist + assert theta_restraint["pull"]["force_constant"] == k_angle + assert beta_restraint["pull"]["force_constant"] == k_angle + + assert distance_restraint["attach"]["target"] == r_initial + assert distance_restraint["pull"]["target"] == r_final + + assert theta_restraint["attach"]["target"] == angle + assert theta_restraint["pull"]["target"] == angle + assert beta_restraint["attach"]["target"] == angle + assert beta_restraint["pull"]["target"] == angle + + for i, restraint in enumerate(guest_spec["restraints"]["wall_restraints"]): + assert restraint["restraint"]["force_constant"] == k_wall + if i < 6: + assert restraint["restraint"]["target"] == 9.3 * openff_unit.angstrom + else: + assert restraint["restraint"]["target"] == 11.3 * openff_unit.angstrom + + def test_evaluator_setup_structure(clean_files): temporary_directory = os.path.join(os.path.dirname(__file__), "tmp") @@ -121,6 +421,127 @@ def test_evaluator_setup_structure(clean_files): assert pytest.approx(host_structure[":DM3"].coordinates[0][2], abs=1e-3) == -5.2 +def test_evaluator_setup_restraints(clean_files, complex_file, restraints_schema): + complex_file_path = os.path.join(os.path.dirname(__file__), "complex.pdb") + complex_file.save(complex_file_path, overwrite=True) + + attach_lambdas = list(np.linspace(0, 1, 15)) + n_attach = 15 + + static_restraints = Setup.build_static_restraints( + complex_file_path, + n_attach, + None, + None, + restraints_schema["static"], + use_amber_indices=False, + ) + assert static_restraints[0].mask1 == ":DM1" + assert static_restraints[0].mask2 == ":CB6@O" + for k in static_restraints[0].phase["attach"]["force_constants"]: + assert k == restraints_schema["static"][0]["force_constant"] + + conformational_restraint = Setup.build_conformational_restraints( + complex_file_path, + attach_lambdas, + None, + None, + restraints_schema["conformational"], + use_amber_indices=False, + ) + assert conformational_restraint[0].mask1 == ":CB6@O" + assert conformational_restraint[0].mask2 == ":CB6@O2" + assert conformational_restraint[0].mask3 == ":CB6@O4" + assert conformational_restraint[0].mask4 == ":CB6@O6" + for i, (k, target) in enumerate( + zip( + conformational_restraint[0].phase["attach"]["force_constants"], + conformational_restraint[0].phase["attach"]["targets"], + ) + ): + assert ( + k + == attach_lambdas[i] + * restraints_schema["conformational"][0]["force_constant"] + ) + assert target == restraints_schema["conformational"][0]["target"] + + symmetry_restraints = Setup.build_symmetry_restraints( + complex_file_path, + n_attach, + restraints_schema["symmetry"], + use_amber_indices=False, + ) + assert symmetry_restraints[0].mask1 == ":DM2" + assert symmetry_restraints[0].mask2 == ":BUT@C" + assert symmetry_restraints[0].mask3 == ":BUT@C3" + assert symmetry_restraints[0].custom_restraint_values["r1"] is None + assert symmetry_restraints[0].custom_restraint_values["r2"] is None + assert ( + symmetry_restraints[0].custom_restraint_values["r3"] + == 0.0 * openff_unit.degrees + ) + assert ( + symmetry_restraints[0].custom_restraint_values["r4"] + == 0.0 * openff_unit.degrees + ) + assert ( + symmetry_restraints[0].custom_restraint_values["rk2"] + == restraints_schema["symmetry"][0]["force_constant"] + ) + assert ( + symmetry_restraints[0].custom_restraint_values["rk3"] + == restraints_schema["symmetry"][0]["force_constant"] + ) + + wall_restraints = Setup.build_wall_restraints( + complex_file_path, + n_attach, + restraints_schema["wall"], + use_amber_indices=False, + ) + assert wall_restraints[0].mask1 == ":CB6@O" + assert wall_restraints[0].mask2 == ":BUT@C" + assert ( + wall_restraints[0].custom_restraint_values["r1"] == 0.0 * openff_unit.angstrom + ) + assert ( + wall_restraints[0].custom_restraint_values["r2"] == 0.0 * openff_unit.angstrom + ) + assert wall_restraints[0].custom_restraint_values["r3"] is None + assert wall_restraints[0].custom_restraint_values["r4"] is None + assert ( + wall_restraints[0].custom_restraint_values["rk2"] + == restraints_schema["wall"][0]["force_constant"] + ) + assert ( + wall_restraints[0].custom_restraint_values["rk3"] + == restraints_schema["wall"][0]["force_constant"] + ) + + guest_restraints = Setup.build_guest_restraints( + complex_file_path, + attach_lambdas, + None, + restraints_schema["guest"], + use_amber_indices=False, + ) + assert guest_restraints[0].mask1 == ":DM1" + assert guest_restraints[0].mask2 == ":BUT@C" + for i, (k, target) in enumerate( + zip( + guest_restraints[0].phase["attach"]["force_constants"], + guest_restraints[0].phase["attach"]["targets"], + ) + ): + assert ( + k + == attach_lambdas[i] + * restraints_schema["guest"][0]["attach"]["force_constant"] + ) + assert target == restraints_schema["guest"][0]["attach"]["target"] + + def test_evaluator_analyze(clean_files): input_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") structure = pmd.load_file(input_pdb, structure=True) From 962693dc8fd9653308b1b80d3d802ed18a6e2bd6 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Wed, 2 Nov 2022 11:52:45 -0700 Subject: [PATCH 05/34] add more test --- paprika/tests/test_evaluator.py | 60 +++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index 060986e..3984760 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -569,6 +569,13 @@ def test_evaluator_analyze(clean_files): np.zeros(3), ) + angle = 180 * openff_unit.degrees + k_angle = 100 * openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 + r_initial = 6.0 * openff_unit.angstrom + r_final = 24.0 * openff_unit.angstrom + k_r = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 + attach_fractions = [0.00, 0.04, 0.181, 0.496, 1.000] + # Distance restraint rest1 = DAT_restraint() rest1.continuous_apr = True @@ -576,12 +583,12 @@ def test_evaluator_analyze(clean_files): rest1.topology = host_guest_structure rest1.mask1 = ":DM1" rest1.mask2 = ":BUT@C" - rest1.attach["target"] = 6.0 - rest1.attach["fraction_list"] = [0.00, 0.04, 0.181, 0.496, 1.000] - rest1.attach["fc_final"] = 5.0 + rest1.attach["target"] = r_initial + rest1.attach["fraction_list"] = attach_fractions + rest1.attach["fc_final"] = k_r rest1.pull["fc"] = rest1.attach["fc_final"] rest1.pull["target_initial"] = rest1.attach["target"] - rest1.pull["target_final"] = 24.0 + rest1.pull["target_final"] = r_final rest1.pull["num_windows"] = 19 rest1.initialize() @@ -593,9 +600,9 @@ def test_evaluator_analyze(clean_files): rest2.mask1 = ":DM2" rest2.mask2 = ":DM1" rest2.mask3 = ":BUT@C" - rest2.attach["target"] = 180.0 - rest2.attach["fraction_list"] = [0.00, 0.04, 0.181, 0.496, 1.000] - rest2.attach["fc_final"] = 100.0 + rest2.attach["target"] = angle + rest2.attach["fraction_list"] = attach_fractions + rest2.attach["fc_final"] = k_angle rest2.pull["fc"] = rest2.attach["fc_final"] rest2.pull["target_initial"] = rest2.attach["target"] rest2.pull["target_final"] = rest2.attach["target"] @@ -610,17 +617,19 @@ def test_evaluator_analyze(clean_files): rest3.mask1 = ":DM1" rest3.mask2 = ":BUT@C" rest3.mask3 = ":BUT@C3" - rest3.attach["target"] = 180.0 - rest3.attach["fraction_list"] = [0.00, 0.04, 0.181, 0.496, 1.000] - rest3.attach["fc_final"] = 100.0 + rest3.attach["target"] = angle + rest3.attach["fraction_list"] = attach_fractions + rest3.attach["fc_final"] = k_angle rest3.pull["fc"] = rest2.attach["fc_final"] rest3.pull["target_initial"] = rest2.attach["target"] rest3.pull["target_final"] = rest2.attach["target"] rest3.pull["num_windows"] = 19 rest3.initialize() - temperature = 298.15 + temperature = 298.15 * openff_unit.kelvin guest_restraints = [rest1, rest2, rest3] + + # Test reference state work ref_state_work = Analyze.compute_ref_state_work(temperature, guest_restraints) assert ( pytest.approx( @@ -629,11 +638,40 @@ def test_evaluator_analyze(clean_files): == -7.14151 ) + # Test symmetry correction fe_sym = Analyze.symmetry_correction(n_microstates=1, temperature=298.15) assert fe_sym == 0.0 fe_sym = Analyze.symmetry_correction(n_microstates=2, temperature=298.15) assert pytest.approx(fe_sym, abs=1e-3) == -0.410679 + # Test APR phase + guest_restraints[0].mask1 = ":CB6@O" + guest_restraints[1].mask1 = ":CB6@O2" + guest_restraints[1].mask2 = ":CB6@O" + guest_restraints[2].mask1 = ":CB6@O" + for restraint in guest_restraints: + restraint.initialize() + + apr_data_path = os.path.join(os.path.dirname(__file__), "../data/cb6-but-apr/") + tmp_path = os.path.join(os.path.dirname(__file__), "tmp") + window_list = ["a000", "a001", "a002", "a003"] + [f"p{i:03}" for i in range(19)] + for window in window_list: + shutil.copytree(f"{apr_data_path}/{window}", f"{tmp_path}/{window}") + shutil.copy(f"{apr_data_path}/vac.pdb", f"{tmp_path}/{window}/vac.pdb") + + results = Analyze.compute_phase_free_energy( + phase="attach", + restraints=guest_restraints, + windows_directory=tmp_path, + topology_name="vac.pdb", + trajectory_mask="*.nc", + analysis_method="ti-block", + ) + + # loose comparison due to short trajectory + assert pytest.approx(results["attach"]["ti-block"]["fe"].magnitude, abs=2) == 946 + assert pytest.approx(results["attach"]["ti-block"]["sem"].magnitude, abs=2) == 10 + def test_evaluator_gaff(clean_files): temporary_directory = os.path.join(os.path.dirname(__file__), "tmp") From 02b9d3519de03c4303c670b974b96648fc799b79 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Thu, 3 Nov 2022 09:18:15 -0700 Subject: [PATCH 06/34] add warnings about pint unit --- paprika/analysis.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/paprika/analysis.py b/paprika/analysis.py index 9c801f0..bfa29fc 100644 --- a/paprika/analysis.py +++ b/paprika/analysis.py @@ -646,7 +646,9 @@ def read_trajectories(self, single_topology=False): def prepare_data(self, phase): number_of_windows = len(self.simulation_data[phase]) - data_points = [len(np.asarray(x).T) for x in self.simulation_data[phase]] + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + data_points = [len(np.asarray(x).T) for x in self.simulation_data[phase]] max_data_points = max(data_points) active_restraints = list( compress(self.restraint_list, self.changing_restraints[phase]) @@ -728,8 +730,10 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): [force_constants[r][0].units for r in range(len(active_rest))] ) - force_constants_T = np.asarray(force_constants).T * force_units - targets_T = np.asarray(targets).T * target_units + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + force_constants_T = np.asarray(force_constants).T * force_units + targets_T = np.asarray(targets).T * target_units # Note, the organization of k = coordinate windows, l = potential windows # seems to be opposite of the documentation. But I got wrong numbers @@ -970,8 +974,10 @@ def run_ti(self, phase, prepared_data, method): # Transpose force_constants and targets into "per window" format, instead of # the "per restraint" format. # print(targets) - force_constants_T = np.asarray(force_constants).T * force_units - targets_T = np.asarray(targets).T * target_units + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + force_constants_T = np.asarray(force_constants).T * force_units + targets_T = np.asarray(targets).T * target_units # For each window: do dihedral wrapping, compute forces, append dl_intp for k in range(num_win): # Coordinate windows From 43a1f6b7d145855aa9b82ef47a7a27eb236c074d Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 28 Apr 2023 15:11:36 -0700 Subject: [PATCH 07/34] lint --- paprika/analysis/analysis.py | 18 +++++++-------- paprika/evaluator/setup.py | 1 - paprika/restraints/amber.py | 4 ++-- paprika/restraints/colvars.py | 6 ++--- paprika/restraints/openmm.py | 2 +- paprika/restraints/plumed.py | 2 +- paprika/restraints/restraints.py | 8 +++---- paprika/tests/test_evaluator.py | 22 +++++++++--------- paprika/tests/test_restraints.py | 38 ++++++++++++++++---------------- paprika/tests/test_tleap.py | 4 ++-- 10 files changed, 52 insertions(+), 53 deletions(-) diff --git a/paprika/analysis/analysis.py b/paprika/analysis/analysis.py index be45f6a..0de8b26 100644 --- a/paprika/analysis/analysis.py +++ b/paprika/analysis/analysis.py @@ -678,13 +678,13 @@ def prepare_data(self, phase): if rest.mask3 is None and rest.mask4 is None: ordered_targets[i] = ordered_targets[i].to(self.distance_unit) ordered_force_constants[i] = ordered_force_constants[i].to( - self.energy_unit / self.distance_unit ** 2 + self.energy_unit / self.distance_unit**2 ) # Angles elif rest.mask3 is not None or rest.mask4 is not None: ordered_targets[i] = ordered_targets[i].to(self.angle_unit) ordered_force_constants[i] = ordered_force_constants[i].to( - self.energy_unit / self.angle_unit ** 2 + self.energy_unit / self.angle_unit**2 ) return ( @@ -801,7 +801,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): nearest_max = get_nearest_max(N_k[k]) sem = get_block_sem(u_kln[k, l, 0:nearest_max]) variance = np.var(u_kln[k, l, 0 : N_k[k]]) - g_k[k] = N_k[k] * (sem ** 2) / variance + g_k[k] = N_k[k] * (sem**2) / variance if method == "mbar-autoc" or method == "mbar-boot": for k in range(num_win): @@ -1602,10 +1602,10 @@ def ref_state_work( # Distance Integration Function def dist_int(RT, fc, targ): def potential(arange, RT, fc, targ): - return (arange ** 2) * np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) + return (arange**2) * np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) targ = targ.to(distance_unit) - fc = fc.to(energy_unit / distance_unit ** 2) + fc = fc.to(energy_unit / distance_unit**2) arange = (np.arange(0.0, 100.0, 0.0001) * openff_unit.angstrom).to( distance_unit ) @@ -1618,7 +1618,7 @@ def potential(arange, RT, fc, targ): return np.sin(arange) * np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) targ = targ.to(angle_unit) - fc = fc.to(energy_unit / angle_unit ** 2) + fc = fc.to(energy_unit / angle_unit**2) arange = (np.arange(0.0, np.pi, 0.00005) * openff_unit.radians).to(angle_unit) return np.trapz(potential(arange, RT, fc, targ), arange) @@ -1630,7 +1630,7 @@ def potential(arange, RT, fc, targ): # Note, because of periodicity, I'm gonna wrap +/- pi around target for integration. targ = targ.to(angle_unit) - fc = fc.to(energy_unit / angle_unit ** 2) + fc = fc.to(energy_unit / angle_unit**2) arange = ( np.arange(targ.magnitude - np.pi, targ.magnitude + np.pi, 0.00005) * openff_unit.radians @@ -1675,11 +1675,11 @@ def potential(arange, RT, fc, targ): g_int = tors_int(RT, g_fc, g_tg) # Concentration term - V0 = 1660.5392 * openff_unit.angstrom ** 3 + V0 = 1660.5392 * openff_unit.angstrom**3 translational = r_int * th_int * ph_int * (1.0 / V0) # C^o = 1/V^o # Orientational term - rotational_volume = 8.0 * np.pi ** 2 + rotational_volume = 8.0 * np.pi**2 orientational = a_int * b_int * g_int / rotational_volume # Return the free energy diff --git a/paprika/evaluator/setup.py b/paprika/evaluator/setup.py index 7d063c5..65398d8 100644 --- a/paprika/evaluator/setup.py +++ b/paprika/evaluator/setup.py @@ -495,7 +495,6 @@ def build_wall_restraints( restraints = [] for restraint_schema in restraint_schemas: - mask = restraint_schema["atoms"].split() restraint = DAT_restraint() diff --git a/paprika/restraints/amber.py b/paprika/restraints/amber.py index 8579a40..d829fa3 100644 --- a/paprika/restraints/amber.py +++ b/paprika/restraints/amber.py @@ -89,9 +89,9 @@ def amber_restraint_line(restraint, window): if restraint.restraint_type == "distance" else openff_unit.degrees ) - force_constant_unit = energy_unit / target_unit ** 2 + force_constant_unit = energy_unit / target_unit**2 if not restraint.restraint_type == "distance": - force_constant_unit = energy_unit / openff_unit.radians ** 2 + force_constant_unit = energy_unit / openff_unit.radians**2 # Prepare AMBER NMR-style restraint atoms = "".join([iat1, iat2, iat3, iat4]) diff --git a/paprika/restraints/colvars.py b/paprika/restraints/colvars.py index 11d6d6a..1753782 100644 --- a/paprika/restraints/colvars.py +++ b/paprika/restraints/colvars.py @@ -162,7 +162,7 @@ def dump_to_file(self): if restraint.restraint_type == "distance": target = target.to(openff_unit.angstrom) force_constant = force_constant.to( - energy_units / openff_unit.angstrom ** 2 + energy_units / openff_unit.angstrom**2 ) elif ( restraint.restraint_type == "angle" @@ -170,7 +170,7 @@ def dump_to_file(self): ): target = target.to(openff_unit.degrees) force_constant = force_constant.to( - energy_units / openff_unit.degrees ** 2 + energy_units / openff_unit.degrees**2 ) # Append cv to list @@ -346,7 +346,7 @@ def _write_dummy_to_file(file, dummy_atoms, kpos=100.0): # Check k units kpos = check_unit( kpos, - base_unit=openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2, + base_unit=openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2, ) # Get dummy atom indices diff --git a/paprika/restraints/openmm.py b/paprika/restraints/openmm.py index 9152d09..4aaeba3 100644 --- a/paprika/restraints/openmm.py +++ b/paprika/restraints/openmm.py @@ -71,7 +71,7 @@ def apply_positional_restraints( k = ( k_pos * openmm_unit.kilocalories_per_mole - / openmm_unit.angstroms ** 2 + / openmm_unit.angstroms**2 ) elif isinstance(k_pos, openmm_unit.Quantity): k = k_pos diff --git a/paprika/restraints/plumed.py b/paprika/restraints/plumed.py index 23fb459..aaf03b4 100644 --- a/paprika/restraints/plumed.py +++ b/paprika/restraints/plumed.py @@ -285,7 +285,7 @@ def dump_to_file(self): ): target = target.to(openff_unit.radians) force_constant = force_constant.to( - self.output_units["energy"] / openff_unit.radians ** 2 + self.output_units["energy"] / openff_unit.radians**2 ) # Append cv strings to lists diff --git a/paprika/restraints/restraints.py b/paprika/restraints/restraints.py index ff8e155..0f1a63c 100644 --- a/paprika/restraints/restraints.py +++ b/paprika/restraints/restraints.py @@ -578,10 +578,10 @@ def initialize(self): # Set default units (Based on Amber) energy_unit = openff_unit.kcal / openff_unit.mole target_unit = openff_unit.angstrom - force_constant_unit = energy_unit / openff_unit.angstrom ** 2 + force_constant_unit = energy_unit / openff_unit.angstrom**2 if self.mask3 or self.mask4: target_unit = openff_unit.degrees - force_constant_unit = energy_unit / openff_unit.radians ** 2 + force_constant_unit = energy_unit / openff_unit.radians**2 # Check attach/release units for phase in [self._attach, self._release]: @@ -946,9 +946,9 @@ def static_DAT_restraint( force_constant = check_unit( force_constant, base_unit=( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 if len(restraint_mask_list) == 2 - else openff_unit.kcal / openff_unit.mole / openff_unit.radians ** 2 + else openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 ), ) diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index ef1def0..3984760 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -185,7 +185,7 @@ def restraints_schema(): "force_constant": 5.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.angstrom ** 2, + / openff_unit.angstrom**2, } ], "conformational": [ @@ -194,7 +194,7 @@ def restraints_schema(): "force_constant": 6.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.radians ** 2, + / openff_unit.radians**2, "target": 104.3 * openff_unit.degrees, } ], @@ -204,7 +204,7 @@ def restraints_schema(): "force_constant": 50.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.radians ** 2, + / openff_unit.radians**2, "target": 11.0 * openff_unit.degrees, } ], @@ -214,7 +214,7 @@ def restraints_schema(): "force_constant": 50.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.angstrom ** 2, + / openff_unit.angstrom**2, "target": 11.0 * openff_unit.angstrom, } ], @@ -225,14 +225,14 @@ def restraints_schema(): "force_constant": 5.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.angstrom ** 2, + / openff_unit.angstrom**2, "target": 6.0 * openff_unit.angstrom, }, "pull": { "force_constant": 5.0 * openff_unit.kcal / openff_unit.mol - / openff_unit.angstrom ** 2, + / openff_unit.angstrom**2, "target": 24.0 * openff_unit.angstrom, }, } @@ -244,9 +244,9 @@ def restraints_schema(): def test_taproom_yaml(clean_files, yaml_restraint_schema): temporary_directory = os.path.join(os.path.dirname(__file__), "tmp") - k_dist = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 - k_wall = 50.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 - k_angle = 100.0 * openff_unit.kcal / openff_unit.mole / openff_unit.radian ** 2 + k_dist = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 + k_wall = 50.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 + k_angle = 100.0 * openff_unit.kcal / openff_unit.mole / openff_unit.radian**2 r_initial = 6.0 * openff_unit.angstrom r_final = 24.0 * openff_unit.angstrom angle = 180.0 * openff_unit.degrees @@ -570,10 +570,10 @@ def test_evaluator_analyze(clean_files): ) angle = 180 * openff_unit.degrees - k_angle = 100 * openff_unit.kcal / openff_unit.mole / openff_unit.radians ** 2 + k_angle = 100 * openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 r_initial = 6.0 * openff_unit.angstrom r_final = 24.0 * openff_unit.angstrom - k_r = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + k_r = 5.0 * openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 attach_fractions = [0.00, 0.04, 0.181, 0.496, 1.000] # Distance restraint diff --git a/paprika/tests/test_restraints.py b/paprika/tests/test_restraints.py index d67a31f..ad29bc8 100644 --- a/paprika/tests/test_restraints.py +++ b/paprika/tests/test_restraints.py @@ -58,7 +58,7 @@ def test_DAT_restraint(): rest1.initialize() target_units = openff_unit.angstrom - force_constant_units = openff_unit.kcal / openff_unit.mole / target_units ** 2 + force_constant_units = openff_unit.kcal / openff_unit.mole / target_units**2 assert rest1.index1 == [13, 31, 49, 67, 85, 103] assert rest1.index2 == [119] assert rest1.index3 is None @@ -128,7 +128,7 @@ def test_DAT_restraint(): target_units = openff_unit.degrees force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.radians ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 ) assert rest2.index1 == [13, 31, 49, 67, 85, 103] assert rest2.index2 == [119] @@ -198,7 +198,7 @@ def test_DAT_restraint(): target_units = openff_unit.degrees force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.radians ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 ) assert rest3.index1 == [31] assert rest3.index2 == [13] @@ -270,7 +270,7 @@ def test_DAT_restraint(): target_units = openff_unit.degrees force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.radians ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.radians**2 ) assert rest4.index1 == [31] assert rest4.index2 == [13] @@ -340,7 +340,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest5.index1 == [13, 31, 49, 67, 85, 103] assert rest5.index2 == [109, 113, 115, 119] @@ -409,7 +409,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest6.index1 == [13, 31, 49, 67, 85, 103] assert rest6.index2 == [109, 113, 115, 119] @@ -478,7 +478,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest7.index1 == [13, 14, 111] assert rest7.index2 == [3] @@ -541,7 +541,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest8.index1 == [13] assert rest8.index2 == [119] @@ -581,7 +581,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest9.index1 == [13] assert rest9.index2 == [119] @@ -621,7 +621,7 @@ def test_DAT_restraint(): target_units = openff_unit.angstrom force_constant_units = ( - openff_unit.kcal / openff_unit.mole / openff_unit.angstrom ** 2 + openff_unit.kcal / openff_unit.mole / openff_unit.angstrom**2 ) assert rest10.index1 == [13] assert rest10.index2 == [119] @@ -1263,7 +1263,7 @@ def test_restraints_output_modules(clean_files): ) # Create restraints for OpenMM system - k_pos = 50.0 * openmm_unit.kilocalories_per_mole / openmm_unit.angstrom ** 2 + k_pos = 50.0 * openmm_unit.kilocalories_per_mole / openmm_unit.angstrom**2 apply_positional_restraints( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), system, @@ -1289,7 +1289,7 @@ def test_restraints_output_modules(clean_files): for i, force in enumerate(positional_restraints): particle, parameters = force.getParticleParameters(0) assert pytest.approx(parameters[0], abs=1e-3) == k_pos.value_in_unit( - openmm_unit.kilojoule_per_mole / openmm_unit.nanometer ** 2 + openmm_unit.kilojoule_per_mole / openmm_unit.nanometer**2 ) assert pytest.approx(parameters[1], abs=1e-3) == dummy_atoms[i]["x"] / 10 assert pytest.approx(parameters[2], abs=1e-3) == dummy_atoms[i]["y"] / 10 @@ -1443,8 +1443,8 @@ def test_openmm_centroid_and_wall(clean_files): logger.info("### Testing OpenMM distance centroid selection.") distance_target = 3.0 * openff_unit.angstrom - k_initial = 0.0 * openff_unit.kcal / openff_unit.mol / openff_unit.angstrom ** 2 - k_final = 3.0 * openff_unit.kcal / openff_unit.mol / openff_unit.angstrom ** 2 + k_initial = 0.0 * openff_unit.kcal / openff_unit.mol / openff_unit.angstrom**2 + k_final = 3.0 * openff_unit.kcal / openff_unit.mol / openff_unit.angstrom**2 rest1 = DAT_restraint() rest1.amber_index = False @@ -1482,7 +1482,7 @@ def test_openmm_centroid_and_wall(clean_files): assert ( pytest.approx(params[0], abs=1e-3) == k_final.to( - openff_unit.kJ / openff_unit.mol / openff_unit.nanometer ** 2 + openff_unit.kJ / openff_unit.mol / openff_unit.nanometer**2 ).magnitude ) assert ( @@ -1526,8 +1526,8 @@ def test_openmm_centroid_and_wall(clean_files): logger.info("### Testing OpenMM angle centroid selection.") angle_target = 180.0 * openff_unit.degrees - k_initial = 0.0 * openff_unit.kcal / openff_unit.mol / openff_unit.radians ** 2 - k_final = 100.0 * openff_unit.kcal / openff_unit.mol / openff_unit.radians ** 2 + k_initial = 0.0 * openff_unit.kcal / openff_unit.mol / openff_unit.radians**2 + k_final = 100.0 * openff_unit.kcal / openff_unit.mol / openff_unit.radians**2 rest2 = DAT_restraint() rest2.amber_index = False @@ -1567,7 +1567,7 @@ def test_openmm_centroid_and_wall(clean_files): assert ( pytest.approx(params[0], abs=1e-3) == k_final.to( - openff_unit.kJ / openff_unit.mol / openff_unit.radians ** 2 + openff_unit.kJ / openff_unit.mol / openff_unit.radians**2 ).magnitude ) assert ( @@ -1659,7 +1659,7 @@ def test_openmm_centroid_and_wall(clean_files): assert ( pytest.approx(params[0], abs=1e-3) == k_final.to( - openff_unit.kJ / openff_unit.mol / openff_unit.radians ** 2 + openff_unit.kJ / openff_unit.mol / openff_unit.radians**2 ).magnitude ) assert ( diff --git a/paprika/tests/test_tleap.py b/paprika/tests/test_tleap.py index d100866..d12dcaf 100644 --- a/paprika/tests/test_tleap.py +++ b/paprika/tests/test_tleap.py @@ -283,8 +283,8 @@ def test_solvation_by_M_and_m(clean_files): volume = sys.get_volume() volume_in_liters = volume * ANGSTROM_CUBED_TO_LITERS - calc_num_na = np.ceil((6.022 * 10 ** 23) * 0.150 * volume_in_liters) - calc_num_cl = np.ceil((6.022 * 10 ** 23) * 0.150 * volume_in_liters) + calc_num_na = np.ceil((6.022 * 10**23) * 0.150 * volume_in_liters) + calc_num_cl = np.ceil((6.022 * 10**23) * 0.150 * volume_in_liters) assert int(obs_num_na) == int(calc_num_na) assert int(obs_num_cl) == int(calc_num_cl) From 53cffadf63c25b1d26b3810b124376c7bf33b6da Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 28 Apr 2023 15:15:54 -0700 Subject: [PATCH 08/34] fix import statements --- paprika/evaluator/amber.py | 4 ++-- paprika/evaluator/analyze.py | 2 +- paprika/evaluator/setup.py | 19 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/paprika/evaluator/amber.py b/paprika/evaluator/amber.py index e2e4ef2..5eaa9ed 100644 --- a/paprika/evaluator/amber.py +++ b/paprika/evaluator/amber.py @@ -8,7 +8,7 @@ from typing import Optional import numpy as np -import parmed as pmd +import parmed logger = logging.getLogger(__name__) _PI_ = np.pi @@ -59,7 +59,7 @@ def generate_gaff( "Checking to see if we have a multi-residue MOL2 file that should be converted " "to single-residue..." ) - structure = pmd.load_file( + structure = parmed.load_file( os.path.join(directory_path, f"{output_name}.{gaff_version}.mol2"), structure=True, ) diff --git a/paprika/evaluator/analyze.py b/paprika/evaluator/analyze.py index 12b2e1a..4ba3a2d 100644 --- a/paprika/evaluator/analyze.py +++ b/paprika/evaluator/analyze.py @@ -77,7 +77,7 @@ def compute_phase_free_energy( @classmethod def compute_ref_state_work( cls, - temperature: [float, openff_unit.Quantity], + temperature: Union[float, openff_unit.Quantity], guest_restraints: List[DAT_restraint], ) -> openff_unit.Quantity: """Computes the reference state work of the 'attach' phase. diff --git a/paprika/evaluator/setup.py b/paprika/evaluator/setup.py index 65398d8..211ad3a 100644 --- a/paprika/evaluator/setup.py +++ b/paprika/evaluator/setup.py @@ -6,8 +6,7 @@ from typing import Any, Dict, List, Optional import numpy as np -import parmed as pmd -from openff.units import unit as openff_unit +import parmed from paprika.build import align from paprika.restraints import DAT_restraint, static_DAT_restraint @@ -25,7 +24,7 @@ class Setup: @classmethod def prepare_host_structure( cls, coordinate_path: str, host_atom_indices: Optional[List[int]] = None - ) -> pmd.Structure: + ) -> parmed.Structure: """Prepares the coordinates of a host molecule ready for the release phase of an APR calculation. This currently involves aligning the cavity of the host along the z-axis, and @@ -53,7 +52,7 @@ def prepare_host_structure( """ # noinspection PyTypeChecker - structure = pmd.load_file(coordinate_path, structure=True) + structure = parmed.load_file(coordinate_path, structure=True) # Extract the host from the full structure. if not host_atom_indices: @@ -64,7 +63,7 @@ def prepare_host_structure( ] # noinspection PyTypeChecker - center_of_mass: np.ndarray = pmd.geometry.center_of_mass( + center_of_mass: np.ndarray = parmed.geometry.center_of_mass( host_structure.coordinates, masses=np.ones(len(host_structure.coordinates)) ) @@ -100,7 +99,7 @@ def prepare_host_structure( # have not been changed and dummy atoms not added. # noinspection PyTypeChecker - structure: pmd.Structure = pmd.load_file(coordinate_path, structure=True) + structure: parmed.Structure = parmed.load_file(coordinate_path, structure=True) structure.coordinates = aligned_structure["!:DM1&!:DM2"].coordinates return structure @@ -114,7 +113,7 @@ def prepare_complex_structure( pull_distance: float, pull_window_index: int, n_pull_windows: int, - ) -> pmd.Structure: + ) -> parmed.Structure: """Prepares the coordinates of a host molecule ready for the pull (+ attach) phase of an APR calculation. @@ -158,7 +157,7 @@ def prepare_complex_structure( # Align the host-guest complex so the first guest atom is at (0, 0, 0) and the # second guest atom lies along the positive z-axis. # noinspection PyTypeChecker - structure: pmd.Structure = pmd.load_file(coordinate_path, structure=True) + structure: parmed.Structure = parmed.load_file(coordinate_path, structure=True) ( guest_orientation_mask_0, @@ -181,7 +180,7 @@ def prepare_complex_structure( @staticmethod def add_dummy_atoms_to_structure( - structure: pmd.Structure, + structure: parmed.Structure, dummy_atom_offsets: List[np.ndarray], offset_coordinates: Optional[np.ndarray] = None, ): @@ -214,7 +213,7 @@ def add_dummy_atoms_to_structure( ) for index in range(len(dummy_atom_offsets)): - structure.add_atom(pmd.Atom(name="DUM"), f"DM{index + 1}", 1) + structure.add_atom(parmed.Atom(name="DUM"), f"DM{index + 1}", 1) structure.positions = full_coordinates From 6097b2dcb4896b0737c51c49941056fc2d8f895c Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 28 Apr 2023 15:51:18 -0700 Subject: [PATCH 09/34] update import package name --- paprika/evaluator/amber.py | 5 ++- paprika/evaluator/analyze.py | 4 +-- paprika/evaluator/setup.py | 31 +++++++++--------- paprika/tests/test_evaluator.py | 56 +++++++++++++++++---------------- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/paprika/evaluator/amber.py b/paprika/evaluator/amber.py index 5eaa9ed..a633948 100644 --- a/paprika/evaluator/amber.py +++ b/paprika/evaluator/amber.py @@ -7,11 +7,11 @@ import subprocess from typing import Optional -import numpy as np +import numpy import parmed logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi def generate_gaff( @@ -129,7 +129,6 @@ def _generate_gaff_atom_types( cwd=directory_path, ) p.communicate() - print(p) remove_files = [ "ANTECHAMBER_AC.AC", diff --git a/paprika/evaluator/analyze.py b/paprika/evaluator/analyze.py index 4ba3a2d..7dd879f 100644 --- a/paprika/evaluator/analyze.py +++ b/paprika/evaluator/analyze.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict, List, Union -import numpy as np +import numpy from openff.units import unit as openff_unit from paprika.analysis import fe_calc @@ -174,4 +174,4 @@ def symmetry_correction( k_B = 1.987204118e-3 * openff_unit.kcal / openff_unit.mol / openff_unit.kelvin temperature = check_unit(temperature, base_unit=openff_unit.kelvin) - return -temperature * k_B * np.log(n_microstates) + return -temperature * k_B * numpy.log(n_microstates) diff --git a/paprika/evaluator/setup.py b/paprika/evaluator/setup.py index 211ad3a..10a4a8e 100644 --- a/paprika/evaluator/setup.py +++ b/paprika/evaluator/setup.py @@ -5,14 +5,14 @@ import logging from typing import Any, Dict, List, Optional -import numpy as np +import numpy import parmed from paprika.build import align from paprika.restraints import DAT_restraint, static_DAT_restraint logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi class Setup: @@ -63,8 +63,9 @@ def prepare_host_structure( ] # noinspection PyTypeChecker - center_of_mass: np.ndarray = parmed.geometry.center_of_mass( - host_structure.coordinates, masses=np.ones(len(host_structure.coordinates)) + center_of_mass: numpy.ndarray = parmed.geometry.center_of_mass( + host_structure.coordinates, + masses=numpy.ones(len(host_structure.coordinates)), ) # Remove the COM from the host coordinates to make alignment easier. @@ -74,19 +75,21 @@ def prepare_host_structure( # Find the principal components of the host, take the two largest, and find # the vector orthogonal to that. Use that vector to align with the z-axis. # This may not generalize to non-radially-symmetric host molecules. - inertia_tensor = np.dot( + inertia_tensor = numpy.dot( host_structure.coordinates.transpose(), host_structure.coordinates ) - eigenvalues, eigenvectors = np.linalg.eig(inertia_tensor) - order = np.argsort(eigenvalues) + eigenvalues, eigenvectors = numpy.linalg.eig(inertia_tensor) + order = numpy.argsort(eigenvalues) _, axis_2, axis_1 = eigenvectors[:, order].transpose() - cavity_axis = np.cross(axis_1, axis_2) + cavity_axis = numpy.cross(axis_1, axis_2) # Add dummy atoms which will be used to align the structure. - cls.add_dummy_atoms_to_structure(structure, [np.array([0, 0, 0]), cavity_axis]) + cls.add_dummy_atoms_to_structure( + structure, [numpy.array([0, 0, 0]), cavity_axis] + ) # Give atoms uniform mass so that the align code uses the center # of geometry rather than the center of mass. @@ -168,7 +171,7 @@ def prepare_complex_structure( structure, guest_orientation_mask_0, guest_orientation_mask_1 ) - target_distance = np.linspace(0.0, pull_distance, n_pull_windows)[ + target_distance = numpy.linspace(0.0, pull_distance, n_pull_windows)[ pull_window_index ] target_difference = target_distance @@ -181,8 +184,8 @@ def prepare_complex_structure( @staticmethod def add_dummy_atoms_to_structure( structure: parmed.Structure, - dummy_atom_offsets: List[np.ndarray], - offset_coordinates: Optional[np.ndarray] = None, + dummy_atom_offsets: List[numpy.ndarray], + offset_coordinates: Optional[numpy.ndarray] = None, ): """A convenience method to add a number of dummy atoms to an existing ParmEd structure, and to position those atoms at a specified set of positions. @@ -200,9 +203,9 @@ def add_dummy_atoms_to_structure( """ if offset_coordinates is None: - offset_coordinates = np.zeros(3) + offset_coordinates = numpy.zeros(3) - full_coordinates = np.vstack( + full_coordinates = numpy.vstack( [ structure.coordinates, *[ diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index 3984760..3737196 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -5,10 +5,10 @@ import os import shutil -import numpy as np -import parmed as pmd +import numpy +import parmed import pytest -import pytraj as pt +import pytraj import yaml from openff.units import unit as openff_unit @@ -40,7 +40,7 @@ def complex_file(): complex_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") butane_molecule = [] - structure = pmd.load_file(complex_pdb, structure=True) + structure = parmed.load_file(complex_pdb, structure=True) for atom in structure.topology.atoms(): if atom.residue.name == "BUT": butane_molecule.append(atom.index) @@ -60,11 +60,11 @@ def complex_file(): Setup.add_dummy_atoms_to_structure( host_guest, [ - np.array([0, 0, 0]), - np.array([0, 0, -3.0]), - np.array([0, 2.2, -5.2]), + numpy.array([0, 0, 0]), + numpy.array([0, 0, -3.0]), + numpy.array([0, 2.2, -5.2]), ], - np.zeros(3), + numpy.zeros(3), ) return host_guest @@ -338,7 +338,7 @@ def test_evaluator_setup_structure(clean_files): # Test prepare_complex_structure host_guest_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") guest_atom_indices = [] - structure = pmd.load_file(host_guest_pdb, structure=True) + structure = parmed.load_file(host_guest_pdb, structure=True) for atom in structure.topology.atoms(): if atom.residue.name == "BUT": guest_atom_indices.append(atom.index) @@ -367,12 +367,14 @@ def test_evaluator_setup_structure(clean_files): cG1 = host_guest_structure_final[G1].coordinates[0] cG2 = host_guest_structure_final[G2].coordinates[0] vec = cG2 - cG1 - axis = np.array([0, 0, 1]) - theta = np.arccos(np.dot(vec, axis) / (np.linalg.norm(vec) * np.linalg.norm(axis))) + axis = numpy.array([0, 0, 1]) + theta = numpy.arccos( + numpy.dot(vec, axis) / (numpy.linalg.norm(vec) * numpy.linalg.norm(axis)) + ) assert theta == 0.0 # Test prepare_host_structure - structure = pmd.load_file(host_guest_pdb, structure=True) + structure = parmed.load_file(host_guest_pdb, structure=True) structure[":CB6"].save(os.path.join(temporary_directory, "cb6.pdb")) host_pdb = os.path.join(temporary_directory, "cb6.pdb") host_atom_indices = [] @@ -384,17 +386,17 @@ def test_evaluator_setup_structure(clean_files): host_pdb, host_atom_indices, ) - center_of_mass = pmd.geometry.center_of_mass( - host_structure.coordinates, masses=np.ones(len(host_structure.coordinates)) + center_of_mass = parmed.geometry.center_of_mass( + host_structure.coordinates, masses=numpy.ones(len(host_structure.coordinates)) ) assert pytest.approx(center_of_mass[0], abs=1e-3) == 0.0 assert pytest.approx(center_of_mass[1], abs=1e-3) == 0.0 assert pytest.approx(center_of_mass[2], abs=1e-3) == 0.0 - inertia_tensor = np.dot( + inertia_tensor = numpy.dot( host_structure.coordinates.transpose(), host_structure.coordinates ) - eig_val, eig_vec = np.linalg.eig(inertia_tensor) + eig_val, eig_vec = numpy.linalg.eig(inertia_tensor) assert pytest.approx(eig_vec[0, -1], abs=1e-3) == 0.0 assert pytest.approx(eig_vec[1, -1], abs=1e-3) == 0.0 assert pytest.approx(eig_vec[2, -1], abs=1e-3) == 1.0 @@ -403,11 +405,11 @@ def test_evaluator_setup_structure(clean_files): Setup.add_dummy_atoms_to_structure( host_structure, [ - np.array([0, 0, 0]), - np.array([0, 0, -3.0]), - np.array([0, 2.2, -5.2]), + numpy.array([0, 0, 0]), + numpy.array([0, 0, -3.0]), + numpy.array([0, 2.2, -5.2]), ], - np.zeros(3), + numpy.zeros(3), ) dummy_atoms = [] for atom in host_structure.topology.atoms(): @@ -425,7 +427,7 @@ def test_evaluator_setup_restraints(clean_files, complex_file, restraints_schema complex_file_path = os.path.join(os.path.dirname(__file__), "complex.pdb") complex_file.save(complex_file_path, overwrite=True) - attach_lambdas = list(np.linspace(0, 1, 15)) + attach_lambdas = list(numpy.linspace(0, 1, 15)) n_attach = 15 static_restraints = Setup.build_static_restraints( @@ -544,7 +546,7 @@ def test_evaluator_setup_restraints(clean_files, complex_file, restraints_schema def test_evaluator_analyze(clean_files): input_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") - structure = pmd.load_file(input_pdb, structure=True) + structure = parmed.load_file(input_pdb, structure=True) guest_atom_indices = [] for atom in structure.topology.atoms(): @@ -562,11 +564,11 @@ def test_evaluator_analyze(clean_files): Setup.add_dummy_atoms_to_structure( host_guest_structure, [ - np.array([0, 0, 0]), - np.array([0, 0, -3.0]), - np.array([0, 2.2, -5.2]), + numpy.array([0, 0, 0]), + numpy.array([0, 0, -3.0]), + numpy.array([0, 2.2, -5.2]), ], - np.zeros(3), + numpy.zeros(3), ) angle = 180 * openff_unit.degrees @@ -688,7 +690,7 @@ def test_evaluator_gaff(clean_files): directory_path=temporary_directory, ) - structure = pt.iterload( + structure = pytraj.iterload( os.path.join(temporary_directory, f"but.{gaff_version}.mol2") ) From 91615105e2574484e7106dd5ebd3651947e59ed7 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sun, 30 Apr 2023 16:57:35 -0700 Subject: [PATCH 10/34] add more tests --- paprika/analysis/analysis.py | 51 +++--- paprika/tests/test_analysis.py | 282 +++++++++++++++++++++++++++++-- paprika/tests/test_evaluator.py | 55 +++++- paprika/tests/test_restraints.py | 111 ++++++++++++ paprika/tests/test_utils.py | 4 + paprika/tests/tmp/test.txt | 19 +++ 6 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 paprika/tests/tmp/test.txt diff --git a/paprika/analysis/analysis.py b/paprika/analysis/analysis.py index 0de8b26..40a1c3a 100644 --- a/paprika/analysis/analysis.py +++ b/paprika/analysis/analysis.py @@ -432,23 +432,6 @@ def collect_data(self, single_topology=False): self.orders = self.determine_window_order() self.simulation_data = self.read_trajectories(single_topology=single_topology) - def collect_data_from_json(self, filepath): - """ - Read in simulation data from a JSON file. - - Parameters - ---------- - filepath: os.PathLike - The name of the JSON file. - """ - with open(filepath, "r") as f: - json_data = f.read() - data = json.loads(json_data, cls=PaprikaDecoder) - - self.changing_restraints = data["changing_restraints"] - self.orders = data["orders"] - self.simulation_data = data["simulation_data"] - def identify_changing_restraints(self): """Figure out which restraints change during each phase of the calculation. @@ -1089,7 +1072,7 @@ def run_ti(self, phase, prepared_data, method): g[k] = N_k[k] / dU_Nunc[k] # Create the interpolation by appending 100 points between each window. - # Start with k=1 so we don't double count. + # Start with k=1, so we don't double count. if k > 0: dl_intp = np.append( dl_intp, @@ -1118,12 +1101,12 @@ def run_ti(self, phase, prepared_data, method): # rather than estimating it from the standard deviation (dU_stdv) and number of # uncorrelated data points (dU_Nunc) from the total data set. if method == "ti-block" and self.exact_sem_each_ti_fraction: - frac_dU_sems = np.zero([k], np.float64) + frac_dU_sems = np.zeros([num_win], np.float64) for k in range(num_win): nearest_max = get_nearest_max(int(fraction * N_k[k])) frac_dU_sems[k] = get_block_sem(dU[k, 0:nearest_max]) elif method == "ti-nocor" and self.exact_sem_each_ti_fraction: - frac_dU_sems = np.zero([k], np.float64) + frac_dU_sems = np.zeros([num_win], np.float64) for k in range(num_win): frac_dU_sems[k] = np.std( dU[k, 0 : int(fraction * N_k[k])] @@ -1257,7 +1240,7 @@ def compute_free_energy(self, phases=["attach", "pull", "release"], seed=None): or method == "mbar-boot" ): self.run_mbar(phase, prepared_data, method) - elif method == "ti-block": + elif method == "ti-block" or method == "ti-nocor": self.run_ti(phase, prepared_data, method) else: raise NotImplementedError( @@ -1465,8 +1448,7 @@ def save_results(self, filepath="results.json", overwrite=False): dumped = json.dumps(self.results, cls=PaprikaEncoder) f.write(dumped) - @staticmethod - def load_results(filepath): + def load_results(self, filepath): """ Read in a JSON file for the results. @@ -1478,9 +1460,11 @@ def load_results(filepath): with open(filepath, "r") as f: data = f.read() - return json.loads(data, cls=PaprikaDecoder) + self.results = json.loads(data, cls=PaprikaDecoder) - def save_data(self, filepath="simulation_data.json", overwrite=False): + def save_simulation_data_to_json( + self, filepath="simulation_data.json", overwrite=False + ): """ Save the simulation data (DAT values) to a JSON file. @@ -1505,6 +1489,23 @@ def save_data(self, filepath="simulation_data.json", overwrite=False): ) f.write(dumped) + def load_simulation_data_from_json(self, filepath): + """ + Read in simulation data from a JSON file. + + Parameters + ---------- + filepath: os.PathLike + The name of the JSON file. + """ + with open(filepath, "r") as f: + json_data = f.read() + data = json.loads(json_data, cls=PaprikaDecoder) + + self.changing_restraints = data["changing_restraints"] + self.orders = data["orders"] + self.simulation_data = data["simulation_data"] + def ref_state_work( temperature, diff --git a/paprika/tests/test_analysis.py b/paprika/tests/test_analysis.py index 7086cfd..d4dbf9f 100644 --- a/paprika/tests/test_analysis.py +++ b/paprika/tests/test_analysis.py @@ -1,19 +1,25 @@ import logging import os import shutil +from copy import deepcopy import numpy as np import parmed as pmd import pytest +from openff.units import unit as openff_unit from pytest import approx from paprika import analysis, log, restraints from paprika.analysis import utils +from paprika.utils import is_file_and_not_empty log.config_root_logger(verbose=True) logger = logging.getLogger(__name__) +random_seed = 12345 + + @pytest.fixture(scope="module", autouse=True) def clean_files(directory="tmp"): # This happens before the test function call @@ -22,7 +28,7 @@ def clean_files(directory="tmp"): os.makedirs(directory) yield # This happens after the test function call - shutil.rmtree(directory) + # shutil.rmtree(directory) @pytest.fixture(scope="module", autouse=True) @@ -85,8 +91,7 @@ def setup_free_energy_calculation(): # Create window directories restraints.restraints.create_window_list([rest1, rest2, rest3]) - seed = 12345 - + # Create Analysis Instance fecalc = analysis.fe_calc() fecalc.topology = os.path.join( os.path.dirname(__file__), "../data/cb6-but-apr/vac.prmtop" @@ -94,26 +99,68 @@ def setup_free_energy_calculation(): fecalc.trajectory = "*.nc" fecalc.path = os.path.join(os.path.dirname(__file__), "../data/cb6-but-apr/") fecalc.restraint_list = [rest1, rest2, rest3] - fecalc.methods = ["ti-block", "mbar-block", "mbar-autoc", "mbar-boot"] + # fecalc.methods = ["ti-block", "mbar-block", "mbar-autoc", "mbar-boot"] fecalc.boot_cycles = 100 fecalc.ti_matrix = "diagonal" fecalc.compute_largest_neighbor = True fecalc.compute_roi = True + fecalc.conservative_subsample = False + fecalc.exact_sem_each_ti_fraction = False + fecalc.fractions = [0.2, 0.4, 0.6, 0.8, 1.0] + fecalc.energy_unit = openff_unit.kcal / openff_unit.mole + fecalc.distance_unit = openff_unit.angstrom + fecalc.angle_unit = openff_unit.degrees + fecalc.temperature_unit = openff_unit.kelvin fecalc.collect_data(single_topology=True) - fecalc.compute_free_energy(seed=seed) + # fecalc.compute_free_energy(seed=seed) fecalc.compute_ref_state_work([rest1, rest2, rest3, None, None, None]) return fecalc def test_setup(clean_files, setup_free_energy_calculation): - pass + assert setup_free_energy_calculation.temperature_unit == openff_unit.kelvin + assert setup_free_energy_calculation.distance_unit == openff_unit.angstrom + assert setup_free_energy_calculation.angle_unit == openff_unit.degrees + assert ( + setup_free_energy_calculation.energy_unit == openff_unit.kcal / openff_unit.mole + ) + assert setup_free_energy_calculation.fractions == [0.2, 0.4, 0.6, 0.8, 1.0] + results = deepcopy(setup_free_energy_calculation.results) + setup_free_energy_calculation.results = results + assert setup_free_energy_calculation.results == results + assert setup_free_energy_calculation.exact_sem_each_ti_fraction is False + assert setup_free_energy_calculation.conservative_subsample is False + + # Test save and load results -- JSON + results = deepcopy(setup_free_energy_calculation.results) + setup_free_energy_calculation.save_results("tmp/results.json", overwrite=True) + assert is_file_and_not_empty("tmp/results.json") + setup_free_energy_calculation.results = {} + setup_free_energy_calculation.load_results("tmp/results.json") + assert len(setup_free_energy_calculation.results) == len(results) + + # Test save and load simulation data -- JSON + setup_free_energy_calculation.save_simulation_data_to_json( + "tmp/simulation.json", overwrite=True + ) + assert is_file_and_not_empty("tmp/simulation.json") + setup_free_energy_calculation.changing_restraints = None + setup_free_energy_calculation.orders = None + setup_free_energy_calculation.simulation_data = None + setup_free_energy_calculation.load_simulation_data_from_json("tmp/simulation.json") + assert setup_free_energy_calculation.changing_restraints is not None + assert setup_free_energy_calculation.orders is not None + assert setup_free_energy_calculation.simulation_data is not None def test_mbar_block(clean_files, setup_free_energy_calculation): - results = setup_free_energy_calculation.results method = "mbar-block" + setup_free_energy_calculation.methods = [method] + setup_free_energy_calculation.compute_free_energy(seed=random_seed) + results = setup_free_energy_calculation.results + # Test mbar-block free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, @@ -157,11 +204,65 @@ def test_mbar_block(clean_files, setup_free_energy_calculation): assert reference_values == approx(test_vals, abs=0.01) -def test_ti_block(clean_files, setup_free_energy_calculation): +def test_mbar_autoc(clean_files, setup_free_energy_calculation): + method = "mbar-autoc" + + setup_free_energy_calculation.methods = [method] + setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results + # Test mbar-autoc free energies and uncertainties + test_vals = [ + results["attach"][method]["fe"].magnitude, + results["attach"][method]["sem"].magnitude, + results["pull"][method]["fe"].magnitude, + results["pull"][method]["sem"].magnitude, + ] + reference_values = [13.267731176, 0.080830, -2.1791430735, 0.696944] + assert reference_values == approx(test_vals, abs=0.01) + + # Test attach mbar-autoc largest_neighbor values + test_vals = results["attach"][method]["largest_neighbor"].magnitude + reference_values = np.array([0.0198918, 0.0259152, 0.0336971, 0.0383718, 0.0383718]) + assert reference_values == approx(test_vals, abs=0.01) + + # Test pull mbar-autoc largest_neighbor values + test_vals = results["pull"][method]["largest_neighbor"].magnitude + np.savetxt("tmp/test.txt", test_vals, fmt="%.5f") + reference_values = np.array( + [ + 0.10274, + 0.11361, + 0.13074, + 0.14136, + 0.38928, + 0.38928, + 0.11215, + 0.12951, + 0.13145, + 0.13145, + 0.13113, + 0.13113, + 0.13751, + 0.14186, + 0.14186, + 0.12847, + 0.13550, + 0.13550, + 0.13404, + ] + ) + assert reference_values == approx(test_vals, abs=0.01) + + +def test_ti_block(clean_files, setup_free_energy_calculation): method = "ti-block" + setup_free_energy_calculation.methods = [method] + setup_free_energy_calculation.exact_sem_each_ti_fraction = True + setup_free_energy_calculation.compute_free_energy(seed=random_seed) + results = setup_free_energy_calculation.results + # Test ti-block free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, @@ -169,7 +270,8 @@ def test_ti_block(clean_files, setup_free_energy_calculation): results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - reference_values = np.array([13.35, 0.26, -1.85, 0.78]) + # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) + reference_values = np.array([13.31, 0.25, -1.62, 0.87]) assert reference_values == approx(test_vals, abs=0.01) # ROI only runs during TI. @@ -186,14 +288,14 @@ def test_ti_block(clean_files, setup_free_energy_calculation): [ 0.33156402, 0.33156402, - 0.22515133, + 0.2150947, 0.2219127, 0.2219127, - 0.1311959, + 0.10746089, 0.13514015, 0.15078472, 0.15078472, - 0.12448228, + 0.15518025, 0.10678047, 0.10678047, 0.10157904, @@ -207,6 +309,162 @@ def test_ti_block(clean_files, setup_free_energy_calculation): ) assert reference_values == approx(test_vals, abs=0.01) + # No-exact sem + setup_free_energy_calculation.exact_sem_each_ti_fraction = False + setup_free_energy_calculation.compute_free_energy(seed=random_seed) + results = setup_free_energy_calculation.results + + # Test ti-block free energies and uncertainties + test_vals = [ + results["attach"][method]["fe"].magnitude, + results["attach"][method]["sem"].magnitude, + results["pull"][method]["fe"].magnitude, + results["pull"][method]["sem"].magnitude, + ] + # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) + reference_values = np.array([13.31, 0.25, -1.62, 0.87]) + assert reference_values == approx(test_vals, abs=0.01) + + # ROI only runs during TI. + + # Test attach ti-block largest_neighbor values + test_vals = results["attach"][method]["largest_neighbor"].magnitude + reference_values = np.array([0.03, 0.07, 0.10, 0.18, 0.18]) + assert reference_values == approx(test_vals, abs=0.01) + + # Test pull ti-block largest_neighbor values + test_vals = results["pull"][method]["largest_neighbor"].magnitude + + reference_values = np.array( + [ + 0.33156402, + 0.33156402, + 0.2150947, + 0.2219127, + 0.2219127, + 0.10746089, + 0.13514015, + 0.15078472, + 0.15078472, + 0.15518025, + 0.10678047, + 0.10678047, + 0.10157904, + 0.14122943, + 0.16608568, + 0.16608568, + 0.14718857, + 0.14090383, + 0.11005729, + ] + ) + assert reference_values == approx(test_vals, abs=0.01) + + +def test_ti_nocor(clean_files, setup_free_energy_calculation): + method = "ti-nocor" + + setup_free_energy_calculation.methods = [method] + setup_free_energy_calculation.exact_sem_each_ti_fraction = True + setup_free_energy_calculation.compute_free_energy(seed=random_seed) + results = setup_free_energy_calculation.results + + # Test ti-block free energies and uncertainties + test_vals = [ + results["attach"][method]["fe"].magnitude, + results["attach"][method]["sem"].magnitude, + results["pull"][method]["fe"].magnitude, + results["pull"][method]["sem"].magnitude, + ] + reference_values = np.array([13.34, 0.09, -1.71, 0.56]) + assert reference_values == approx(test_vals, abs=0.01) + + # ROI only runs during TI. + + # Test attach ti-nocor largest_neighbor values + test_vals = results["attach"][method]["largest_neighbor"].magnitude + reference_values = np.array([0.014, 0.036, 0.055, 0.066, 0.066]) + assert reference_values == approx(test_vals, abs=0.01) + + # Test pull ti-block largest_neighbor values + test_vals = results["pull"][method]["largest_neighbor"].magnitude + + reference_values = np.array( + [ + 0.09991857695378108, + 0.09991857695378108, + 0.08597658940932379, + 0.10866283883728353, + 0.10866283883728353, + 0.08704110657500748, + 0.10761246555307555, + 0.10761246555307555, + 0.11871918480320433, + 0.11871918480320433, + 0.10678047, + 0.10678047, + 0.10157904, + 0.09111759946004389, + 0.1034237670447086, + 0.1034237670447086, + 0.10319382363600771, + 0.10607790116613745, + 0.11005729, + ] + ) + assert reference_values == approx(test_vals, abs=0.01) + + # No-exact sem + setup_free_energy_calculation.exact_sem_each_ti_fraction = False + setup_free_energy_calculation.compute_free_energy(seed=random_seed) + results = setup_free_energy_calculation.results + + # Test ti-block free energies and uncertainties + test_vals = [ + results["attach"][method]["fe"].magnitude, + results["attach"][method]["sem"].magnitude, + results["pull"][method]["fe"].magnitude, + results["pull"][method]["sem"].magnitude, + ] + # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) + reference_values = np.array([13.34, 0.098, -1.71, 0.56]) + assert reference_values == approx(test_vals, abs=0.01) + + # ROI only runs during TI. + + # Test attach ti-block largest_neighbor values + test_vals = results["attach"][method]["largest_neighbor"].magnitude + reference_values = np.array([0.0138, 0.0362, 0.055, 0.0657, 0.0657]) + assert reference_values == approx(test_vals, abs=0.01) + + # Test pull ti-block largest_neighbor values + test_vals = results["pull"][method]["largest_neighbor"].magnitude + + reference_values = np.array( + [ + 0.09991857695378108, + 0.09991857695378108, + 0.08597658940932379, + 0.10866283883728353, + 0.10866283883728353, + 0.08704110657500748, + 0.10761246555307555, + 0.10761246555307555, + 0.11871918480320433, + 0.11871918480320433, + 0.10678047, + 0.10678047, + 0.10157904, + 0.09111759946004389, + 0.1034237670447086, + 0.1034237670447086, + 0.10319382363600771, + 0.10607790116613745, + 0.11005729, + ] + ) + assert reference_values == approx(test_vals, abs=0.01) + def test_reference_state_work(clean_files, setup_free_energy_calculation): results = setup_free_energy_calculation.results diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index 3737196..b6128a3 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -393,6 +393,17 @@ def test_evaluator_setup_structure(clean_files): assert pytest.approx(center_of_mass[1], abs=1e-3) == 0.0 assert pytest.approx(center_of_mass[2], abs=1e-3) == 0.0 + host_structure = Setup.prepare_host_structure( + host_pdb, + host_atom_indices=None, + ) + center_of_mass = parmed.geometry.center_of_mass( + host_structure.coordinates, masses=numpy.ones(len(host_structure.coordinates)) + ) + assert pytest.approx(center_of_mass[0], abs=1e-3) == 0.0 + assert pytest.approx(center_of_mass[1], abs=1e-3) == 0.0 + assert pytest.approx(center_of_mass[2], abs=1e-3) == 0.0 + inertia_tensor = numpy.dot( host_structure.coordinates.transpose(), host_structure.coordinates ) @@ -429,12 +440,14 @@ def test_evaluator_setup_restraints(clean_files, complex_file, restraints_schema attach_lambdas = list(numpy.linspace(0, 1, 15)) n_attach = 15 + n_pull = 46 + n_release = 15 static_restraints = Setup.build_static_restraints( complex_file_path, n_attach, - None, - None, + n_pull, + n_release, restraints_schema["static"], use_amber_indices=False, ) @@ -545,6 +558,9 @@ def test_evaluator_setup_restraints(clean_files, complex_file, restraints_schema def test_evaluator_analyze(clean_files): + # ---------------------------------------------------------------- # + # Configure system and restraints + # ---------------------------------------------------------------- # input_pdb = os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") structure = parmed.load_file(input_pdb, structure=True) @@ -628,25 +644,58 @@ def test_evaluator_analyze(clean_files): rest3.pull["num_windows"] = 19 rest3.initialize() + # Angle 2 + rest4 = DAT_restraint() + rest4.continuous_apr = True + rest4.amber_index = True + rest4.topology = input_pdb + rest4.mask1 = ":DM1" + rest4.mask2 = ":BUT@C" + rest4.mask3 = ":BUT@C2" + rest4.mask4 = ":BUT@C3" + rest4.attach["target"] = angle + rest4.attach["fraction_list"] = attach_fractions + rest4.attach["fc_final"] = k_angle + rest4.pull["fc"] = rest2.attach["fc_final"] + rest4.pull["target_initial"] = rest2.attach["target"] + rest4.pull["target_final"] = rest2.attach["target"] + rest4.pull["num_windows"] = 19 + rest4.initialize() + temperature = 298.15 * openff_unit.kelvin guest_restraints = [rest1, rest2, rest3] + # ---------------------------------------------------------------- # # Test reference state work - ref_state_work = Analyze.compute_ref_state_work(temperature, guest_restraints) + # ---------------------------------------------------------------- # + ref_state_work = Analyze.compute_ref_state_work(temperature, [rest1, rest2, rest3]) assert ( pytest.approx( ref_state_work.to(openff_unit.kcal / openff_unit.mole).magnitude, abs=1e-3 ) == -7.14151 ) + ref_state_work = Analyze.compute_ref_state_work( + temperature, [rest1, rest2, rest3, rest4] + ) + assert ( + pytest.approx( + ref_state_work.to(openff_unit.kcal / openff_unit.mole).magnitude, abs=1e-3 + ) + == -9.41062 + ) + # ---------------------------------------------------------------- # # Test symmetry correction + # ---------------------------------------------------------------- # fe_sym = Analyze.symmetry_correction(n_microstates=1, temperature=298.15) assert fe_sym == 0.0 fe_sym = Analyze.symmetry_correction(n_microstates=2, temperature=298.15) assert pytest.approx(fe_sym, abs=1e-3) == -0.410679 + # ---------------------------------------------------------------- # # Test APR phase + # ---------------------------------------------------------------- # guest_restraints[0].mask1 = ":CB6@O" guest_restraints[1].mask1 = ":CB6@O2" guest_restraints[1].mask2 = ":CB6@O" diff --git a/paprika/tests/test_restraints.py b/paprika/tests/test_restraints.py index 3f08e99..0285c2e 100644 --- a/paprika/tests/test_restraints.py +++ b/paprika/tests/test_restraints.py @@ -1254,6 +1254,78 @@ def test_restraints_output_modules(clean_files): r.initialize() guest_restraints.append(r) + # Guest - Dihedral + r = DAT_restraint() + r.amber_index = True + r.continuous_apr = True + r.auto_apr = True + r.topology = os.path.join( + os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb" + ) + r.mask1 = ":CB6@C13" + r.mask2 = ":CB6@C3" + r.mask3 = ":CB6@C" + r.mask4 = ":BUT@C1" + r.attach["target"] = 180.0 + r.attach["num_windows"] = 15 + r.attach["fc_initial"] = 0.0 + r.attach["fc_final"] = 100.0 + r.pull["fc"] = r.attach["fc_final"] + r.pull["num_windows"] = 46 + r.pull["target_initial"] = r.attach["target"] + r.pull["target_final"] = 180.0 + r.release["target"] = r.pull["target_final"] + r.release["num_windows"] = r.attach["num_windows"] + r.release["fc_initial"] = r.attach["fc_initial"] + r.release["fc_final"] = r.attach["fc_final"] + r.initialize() + guest_restraints.append(r) + + # Guest - Group + r = DAT_restraint() + r.amber_index = True + r.continuous_apr = True + r.auto_apr = True + r.topology = os.path.join( + os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb" + ) + r.mask1 = ":CB6@C13,C12" + r.mask2 = ":CB6@C3,C2" + r.mask3 = ":CB6@C,C1" + r.mask4 = ":BUT@C1,C2" + r.attach["target"] = 180.0 + r.attach["num_windows"] = 15 + r.attach["fc_initial"] = 0.0 + r.attach["fc_final"] = 100.0 + r.pull["fc"] = r.attach["fc_final"] + r.pull["num_windows"] = 46 + r.pull["target_initial"] = r.attach["target"] + r.pull["target_final"] = 180.0 + r.release["target"] = r.pull["target_final"] + r.release["num_windows"] = r.attach["num_windows"] + r.release["fc_initial"] = r.attach["fc_initial"] + r.release["fc_final"] = r.attach["fc_final"] + r.initialize() + guest_restraints.append(r) + + # Guest - None + r_empty = DAT_restraint() + r_empty.amber_index = True + r_empty.continuous_apr = True + r_empty.auto_apr = True + r_empty.topology = os.path.join( + os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb" + ) + r_empty.mask1 = ":CB6@C13,C12" + r_empty.mask2 = ":CB6@C3,C2" + r_empty.mask3 = ":CB6@C,C1" + r_empty.mask4 = ":BUT@C1,C2" + r_empty.attach["target"] = 180.0 + r_empty.attach["num_windows"] = 15 + r_empty.attach["fc_initial"] = 0.0 + r_empty.attach["fc_final"] = 100.0 + r_empty.initialize() + # Create OpenMM System system = structure.createSystem( nonbondedMethod=app.NoCutoff, @@ -1283,7 +1355,9 @@ def test_restraints_output_modules(clean_files): or isinstance(force, openmm.CustomTorsionForce) ] + # ---------------------------------------------------------------- # # Test dummy atom positional restraint + # ---------------------------------------------------------------- # for i, force in enumerate(positional_restraints): particle, parameters = force.getParticleParameters(0) assert pytest.approx(parameters[0], abs=1e-3) == k_pos.value_in_unit( @@ -1293,7 +1367,9 @@ def test_restraints_output_modules(clean_files): assert pytest.approx(parameters[2], abs=1e-3) == dummy_atoms[i]["y"] / 10 assert pytest.approx(parameters[3], abs=1e-3) == dummy_atoms[i]["z"] / 10 + # ---------------------------------------------------------------- # # Test Amber NMR-style restraints + # ---------------------------------------------------------------- # atom1, atom2, parameters = DAT_restraint_list[0].getBondParameters(0) assert pytest.approx(parameters[0]) == 2092.0 assert pytest.approx(parameters[1]) == 0.6 @@ -1306,7 +1382,9 @@ def test_restraints_output_modules(clean_files): assert pytest.approx(parameters[0]) == 418.4 assert pytest.approx(parameters[1]) == numpy.pi + # ---------------------------------------------------------------- # # Test Amber restraints + # ---------------------------------------------------------------- # r_string = amber_restraint_line(guest_restraints[0], window) assert r_string.split()[2].split(",")[0] == "123" assert r_string.split()[2].split(",")[1] == "119" @@ -1328,7 +1406,38 @@ def test_restraints_output_modules(clean_files): assert float(theta_string.split()[12].split(",")[0]) == 100.0 assert float(theta_string.split()[14].split(",")[0]) == 100.0 + dihedral_string = amber_restraint_line(guest_restraints[3], window) + assert dihedral_string.split()[2].split(",")[0] == "38" + assert dihedral_string.split()[2].split(",")[1] == "10" + assert dihedral_string.split()[2].split(",")[2] == "1" + assert dihedral_string.split()[2].split(",")[3] == "113" + + group_string = amber_restraint_line(guest_restraints[4], window) + assert group_string.split()[2].split(",")[0] == "-1" + assert group_string.split()[2].split(",")[1] == "-1" + assert group_string.split()[2].split(",")[2] == "-1" + assert group_string.split()[2].split(",")[3] == "-1" + assert float(group_string.split()[4].split(",")[0]) == 0.0 + assert float(group_string.split()[6].split(",")[0]) == 180.0 + assert float(group_string.split()[8].split(",")[0]) == 180.0 + assert float(group_string.split()[10].split(",")[0]) == 360.0 + assert float(group_string.split()[12].split(",")[0]) == 100.0 + assert float(group_string.split()[14].split(",")[0]) == 100.0 + assert group_string.split()[15] == "igr1=" + assert group_string.split()[16] == "37,38," + assert group_string.split()[17] == "igr2=" + assert group_string.split()[18] == "9,10," + assert group_string.split()[19] == "igr3=" + assert group_string.split()[20] == "1,2," + assert group_string.split()[21] == "igr4=" + assert group_string.split()[22] == "113,115," + + empty_string = amber_restraint_line(r_empty, window) + assert empty_string == "" + + # ---------------------------------------------------------------- # # Test Plumed output + # ---------------------------------------------------------------- # plumed = Plumed() plumed.path = "tmp" plumed.file_name = "plumed.dat" @@ -1370,7 +1479,9 @@ def test_restraints_output_modules(clean_files): assert float(restraint_line[2].split("=")[1]) == 3.1416 assert float(restraint_line[3].split("=")[1]) == 200.0 + # ---------------------------------------------------------------- # # Test Colvar output + # ---------------------------------------------------------------- # colvar = Colvars() colvar.path = "tmp" colvar.file_name = "colvars.dat" diff --git a/paprika/tests/test_utils.py b/paprika/tests/test_utils.py index 6e6b0b9..40e15ce 100644 --- a/paprika/tests/test_utils.py +++ b/paprika/tests/test_utils.py @@ -28,6 +28,10 @@ def test_mkdirs(): for window in window_list: assert os.path.exists(os.path.join("tmp", "windows", window)) + make_window_dirs(window_list, stash_existing=True, path="tmp") + for window in window_list: + assert os.path.exists(os.path.join("tmp", "windows", window)) + def test_strip_prmtop(): """Test that we can remove items from structures.""" diff --git a/paprika/tests/tmp/test.txt b/paprika/tests/tmp/test.txt new file mode 100644 index 0000000..aa73033 --- /dev/null +++ b/paprika/tests/tmp/test.txt @@ -0,0 +1,19 @@ +0.10274 +0.11361 +0.13074 +0.14136 +0.38928 +0.38928 +0.11215 +0.12951 +0.13145 +0.13145 +0.13113 +0.13113 +0.13751 +0.14186 +0.14186 +0.12847 +0.13550 +0.13550 +0.13404 From 81c9d36add955e442f7b0da2ea0085707837678a Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 1 May 2023 09:50:18 -0700 Subject: [PATCH 11/34] add more test and change import namespace --- paprika/analysis/analysis.py | 187 ++++++++++---------- paprika/analysis/bootstrap.py | 163 +++++++++--------- paprika/analysis/utils.py | 32 ++-- paprika/build/align.py | 146 ++++++++-------- paprika/build/dummy.py | 4 +- paprika/build/system/tleap.py | 100 +++++------ paprika/io.py | 52 +++--- paprika/restraints/colvars.py | 4 +- paprika/restraints/openmm.py | 8 +- paprika/restraints/plumed.py | 4 +- paprika/restraints/utils.py | 4 +- paprika/simulate/amber.py | 8 +- paprika/simulate/gromacs.py | 14 +- paprika/simulate/namd.py | 16 +- paprika/tests/test_analysis.py | 304 ++++++++++++--------------------- paprika/utils.py | 19 ++- 16 files changed, 506 insertions(+), 559 deletions(-) diff --git a/paprika/analysis/analysis.py b/paprika/analysis/analysis.py index 40a1c3a..9f5592c 100644 --- a/paprika/analysis/analysis.py +++ b/paprika/analysis/analysis.py @@ -4,7 +4,7 @@ import warnings from itertools import compress -import numpy as np +import numpy import pymbar from openff.units import unit as openff_unit @@ -451,7 +451,7 @@ def identify_changing_restraints(self): for restraint in self.restraint_list: if restraint.phase[phase][changing_parameter] is not None: static = all( - np.isclose(x, restraint.phase[phase][changing_parameter][0]) + numpy.isclose(x, restraint.phase[phase][changing_parameter][0]) for x in restraint.phase[phase][changing_parameter] ) else: @@ -488,40 +488,40 @@ def determine_window_order(self): for restraint in active_attach_restraints: attach_orders.append( - np.argsort(restraint.phase["attach"]["force_constants"]) + numpy.argsort(restraint.phase["attach"]["force_constants"]) ) - if not all([np.array_equal(attach_orders[0], i) for i in attach_orders]): + if not all([numpy.array_equal(attach_orders[0], i) for i in attach_orders]): raise Exception( "The order of increasing force constants is not the same in all restraints." ) elif attach_orders: orders["attach"] = attach_orders[0] else: - orders["attach"] = np.empty(0) + orders["attach"] = numpy.empty(0) for restraint in active_pull_restraints: - pull_orders.append(np.argsort(restraint.phase["pull"]["targets"])) - if not all([np.array_equal(pull_orders[0], i) for i in pull_orders]): + pull_orders.append(numpy.argsort(restraint.phase["pull"]["targets"])) + if not all([numpy.array_equal(pull_orders[0], i) for i in pull_orders]): raise Exception( "The order of increasing target distances is not the same in all restraints." ) elif pull_orders: orders["pull"] = pull_orders[0] else: - orders["pull"] = np.empty(0) + orders["pull"] = numpy.empty(0) for restraint in active_release_restraints: release_orders.append( - np.argsort(restraint.phase["release"]["force_constants"]) + numpy.argsort(restraint.phase["release"]["force_constants"]) ) - if not all([np.array_equal(release_orders[0], i) for i in release_orders]): + if not all([numpy.array_equal(release_orders[0], i) for i in release_orders]): raise Exception( "The order of increasing force constants is not the same in all restraints." ) elif release_orders: orders["release"] = release_orders[0] else: - orders["release"] = np.empty(0) + orders["release"] = numpy.empty(0) return orders @@ -558,13 +558,13 @@ def read_trajectories(self, single_topology=False): if i is not None ] - active_attach_restraints = np.asarray(self.restraint_list)[ + active_attach_restraints = numpy.asarray(self.restraint_list)[ self.changing_restraints["attach"] ] - active_pull_restraints = np.asarray(self.restraint_list)[ + active_pull_restraints = numpy.asarray(self.restraint_list)[ self.changing_restraints["pull"] ] - active_release_restraints = np.asarray(self.restraint_list)[ + active_release_restraints = numpy.asarray(self.restraint_list)[ self.changing_restraints["release"] ] @@ -642,13 +642,13 @@ def prepare_data(self, phase): number_of_windows = len(self.simulation_data[phase]) with warnings.catch_warnings(): warnings.simplefilter("ignore") - data_points = [len(np.asarray(x).T) for x in self.simulation_data[phase]] + data_points = [len(numpy.asarray(x).T) for x in self.simulation_data[phase]] max_data_points = max(data_points) active_restraints = list( compress(self.restraint_list, self.changing_restraints[phase]) ) force_constants = [ - np.copy(i.phase[phase]["force_constants"]) for i in active_restraints + numpy.copy(i.phase[phase]["force_constants"]) for i in active_restraints ] targets = [i.phase[phase]["targets"] for i in active_restraints] @@ -689,7 +689,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): ---------- phase: str The phase of the calculation to analyze. - prepared_data: :class:`np.array` + prepared_data: :class:`numpy.array` The list of "prepared data" including the number of windows, data points, which restraints are changing, their force constants and targets, and well as the order of the windows. This probably ought to be redesigned. @@ -711,23 +711,25 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): ) = prepared_data # Number of data points in each restraint value array - N_k = np.array(data_points) + N_k = numpy.array(data_points) # Set up the reduced potential energy array. ie, the potential of each window's # coordinates in each window's potential function - u_kln = np.zeros([num_win, num_win, max_data_points], np.float64) + u_kln = numpy.zeros([num_win, num_win, max_data_points], numpy.float64) # Transpose force_constants and targets into "per window" format, instead of # the "per restraint" format. - target_units = np.array([targets[r][0].units for r in range(len(active_rest))]) - force_units = np.array( + target_units = numpy.array( + [targets[r][0].units for r in range(len(active_rest))] + ) + force_units = numpy.array( [force_constants[r][0].units for r in range(len(active_rest))] ) with warnings.catch_warnings(): warnings.simplefilter("ignore") - force_constants_T = np.asarray(force_constants).T * force_units - targets_T = np.asarray(targets).T * target_units + force_constants_T = numpy.asarray(force_constants).T * force_units + targets_T = numpy.asarray(targets).T * target_units # Note, the organization of k = coordinate windows, l = potential windows # seems to be opposite of the documentation. But I got wrong numbers @@ -766,7 +768,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): ] ).magnitude - g_k = np.ones([num_win], np.float64) + g_k = numpy.ones([num_win], numpy.float64) # Should I subsample based on the restraint coordinate values? Here I'm # doing it on the potential. Should be pretty close .... if method == "mbar-block": @@ -783,7 +785,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): # Now compute statistical inefficiency: g = N*(SEM**2)/variance nearest_max = get_nearest_max(N_k[k]) sem = get_block_sem(u_kln[k, l, 0:nearest_max]) - variance = np.var(u_kln[k, l, 0 : N_k[k]]) + variance = numpy.var(u_kln[k, l, 0 : N_k[k]]) g_k[k] = N_k[k] * (sem**2) / variance if method == "mbar-autoc" or method == "mbar-boot": @@ -796,7 +798,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): # then subsampling will return identical indices to original # (hopefully) ss_indices = [] - N_ss = np.zeros([num_win], np.int32) # N_subsample + N_ss = numpy.zeros([num_win], numpy.int32) # N_subsample for k in range(num_win): ss_indices.append( get_subsampled_indices( @@ -815,7 +817,7 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): # To estimate the free energy, we won't do subsampling. We'll do # another MBAR calculation later with subsampling to estimate the # uncertainty. - frac_N_k = np.array([int(fraction * n) for n in N_k], dtype=np.int32) + frac_N_k = numpy.array([int(fraction * n) for n in N_k], dtype=numpy.int32) mbar = pymbar.MBAR(u_kln, frac_N_k, verbose=verbose) try: # pymbar >= 4 @@ -837,11 +839,15 @@ def run_mbar(self, phase, prepared_data, method, verbose=False): # Estimate uncertainty from decorrelated samples # Create subsampled indices and count their lengths - frac_N_ss = np.array([int(fraction * n) for n in N_ss], dtype=np.int32) + frac_N_ss = numpy.array( + [int(fraction * n) for n in N_ss], dtype=numpy.int32 + ) # Create a new potential array for the uncertainty calculation # (are we using too much memory?) - u_kln_err = np.zeros([num_win, num_win, np.max(frac_N_ss)], np.float64) + u_kln_err = numpy.zeros( + [num_win, num_win, numpy.max(frac_N_ss)], numpy.float64 + ) # Populate the subsampled array, drawing the appropriate # fraction of subsamples from the original @@ -927,7 +933,7 @@ def run_ti(self, phase, prepared_data, method): ---------- phase: str The phase of the calculation to analyze. - prepared_data: :class:`np.array` + prepared_data: :class:`numpy.array` The list of "prepared data" including the number of windows, data points, which restraints are changing, their force constants and targets, and well as the order of the windows. This probably ought to be redesigned. @@ -955,39 +961,44 @@ def run_ti(self, phase, prepared_data, method): ) = prepared_data # Number of data points in each restraint value array - N_k = np.array(data_points) + N_k = numpy.array(data_points) # The dU array to store the partial derivative of the potential with respect lambda or target, # depending on the whether attach/release or pull. Data stored for each frame. This just a # temporary storage space. - dU = np.zeros([num_win, max_data_points], np.float64) + dU = numpy.zeros([num_win, max_data_points], numpy.float64) # The mean, SEM, standard deviation, and number of uncorrelated dU values for each window. - dU_avgs = np.zeros([num_win], np.float64) - dU_sems = np.zeros([num_win], np.float64) - dU_stdv = np.zeros([num_win], np.float64) - dU_Nunc = np.zeros([num_win], np.float64) + dU_avgs = numpy.zeros([num_win], numpy.float64) + dU_sems = numpy.zeros([num_win], numpy.float64) + dU_stdv = numpy.zeros([num_win], numpy.float64) + dU_Nunc = numpy.zeros([num_win], numpy.float64) # The statistical inefficiency - g = np.zeros([num_win], np.float64) + g = numpy.zeros([num_win], numpy.float64) # Array for values of the changing coordinate (x-axis), either lambda or target. # I'll name them dl_vals for dlambda values. - dl_vals = np.zeros([num_win], np.float64) + dl_vals = numpy.zeros([num_win], numpy.float64) # Setup interpolation array for the dLambda (dl) coordinate. We're gonna create # this progressively by appending ... - dl_intp = np.zeros([0], np.float64) + dl_intp = numpy.zeros([0], numpy.float64) # Get units - target_units = np.array([targets[r][0].units for r in range(len(active_rest))]) - force_units = np.array( + target_units = numpy.array( + [targets[r][0].units for r in range(len(active_rest))] + ) + force_units = numpy.array( [force_constants[r][0].units for r in range(len(active_rest))] ) # Store the max force constant value for each restraint. max_force_constants = ( - np.array( - [np.max(force_constants[r]).magnitude for r in range(len(active_rest))] + numpy.array( + [ + numpy.max(force_constants[r]).magnitude + for r in range(len(active_rest)) + ] ) * force_units ) @@ -997,8 +1008,8 @@ def run_ti(self, phase, prepared_data, method): # print(targets) with warnings.catch_warnings(): warnings.simplefilter("ignore") - force_constants_T = np.asarray(force_constants).T * force_units - targets_T = np.asarray(targets).T * target_units + force_constants_T = numpy.asarray(force_constants).T * force_units + targets_T = numpy.asarray(targets).T * target_units # For each window: do dihedral wrapping, compute forces, append dl_intp for k in range(num_win): # Coordinate windows @@ -1059,28 +1070,28 @@ def run_ti(self, phase, prepared_data, method): # Compute standard deviations and SEMs, unless we're going to do # exact_sem_each_ti_fraction - dU_avgs[k] = np.mean(dU[k, 0 : N_k[k]]) - dU_stdv[k] = np.std(dU[k, 0 : N_k[k]]) + dU_avgs[k] = numpy.mean(dU[k, 0 : N_k[k]]) + dU_stdv[k] = numpy.std(dU[k, 0 : N_k[k]]) if method == "ti-block": nearest_max = get_nearest_max(N_k[k]) dU_sems[k] = get_block_sem(dU[k, 0:nearest_max]) # Rearrange SEM = StdDev/sqrt(N) to get N_uncorrelated dU_Nunc[k] = (dU_stdv[k] / dU_sems[k]) ** 2 elif method == "ti-nocor": - dU_sems[k] = dU_stdv[k] / np.sqrt(N_k[k]) + dU_sems[k] = dU_stdv[k] / numpy.sqrt(N_k[k]) dU_Nunc[k] = N_k[k] g[k] = N_k[k] / dU_Nunc[k] # Create the interpolation by appending 100 points between each window. # Start with k=1, so we don't double count. if k > 0: - dl_intp = np.append( + dl_intp = numpy.append( dl_intp, - np.linspace(dl_vals[k - 1], dl_vals[k], num=100, endpoint=False), + numpy.linspace(dl_vals[k - 1], dl_vals[k], num=100, endpoint=False), ) # Tack on the final value to the dl interpolation - dl_intp = np.append(dl_intp, dl_vals[-1]) + dl_intp = numpy.append(dl_intp, dl_vals[-1]) logger.debug("Running bootstrap calculations...") @@ -1093,28 +1104,28 @@ def run_ti(self, phase, prepared_data, method): logger.debug("Working on fraction ... {}".format(fraction)) # Compute means for this fraction. - frac_dU_avgs = np.array( - [np.mean(dU[k, 0 : int(fraction * n)]) for k, n in enumerate(N_k)] + frac_dU_avgs = numpy.array( + [numpy.mean(dU[k, 0 : int(fraction * n)]) for k, n in enumerate(N_k)] ) # If self.exact_sem_each_ti_fraction, we're gonna recompute the SEM for each fraction # rather than estimating it from the standard deviation (dU_stdv) and number of # uncorrelated data points (dU_Nunc) from the total data set. if method == "ti-block" and self.exact_sem_each_ti_fraction: - frac_dU_sems = np.zeros([num_win], np.float64) + frac_dU_sems = numpy.zeros([num_win], numpy.float64) for k in range(num_win): nearest_max = get_nearest_max(int(fraction * N_k[k])) frac_dU_sems[k] = get_block_sem(dU[k, 0:nearest_max]) elif method == "ti-nocor" and self.exact_sem_each_ti_fraction: - frac_dU_sems = np.zeros([num_win], np.float64) + frac_dU_sems = numpy.zeros([num_win], numpy.float64) for k in range(num_win): - frac_dU_sems[k] = np.std( + frac_dU_sems[k] = numpy.std( dU[k, 0 : int(fraction * N_k[k])] - ) / np.sqrt(int(fraction * N_k[k])) + ) / numpy.sqrt(int(fraction * N_k[k])) else: - frac_dU_sems = dU_stdv / np.sqrt(fraction * dU_Nunc) + frac_dU_sems = dU_stdv / numpy.sqrt(fraction * dU_Nunc) - dU_samples = np.random.normal( + dU_samples = numpy.random.normal( frac_dU_avgs, frac_dU_sems, size=(self.boot_cycles, frac_dU_avgs.size) ) @@ -1142,12 +1153,12 @@ def run_ti(self, phase, prepared_data, method): if self.compute_roi: logger.info(phase + ": computing ROI for " + method) # Do ROI calc - max_fraction = np.max(self.fractions) + max_fraction = numpy.max(self.fractions) # If we didn't compute fe/sem for fraction 1.0 already, do it now - dU_samples = np.random.normal( + dU_samples = numpy.random.normal( dU_avgs, dU_sems, size=(self.boot_cycles, dU_avgs.size) ) - if not np.isclose(max_fraction, 1.0): + if not numpy.isclose(max_fraction, 1.0): junk_fe, total_sem_matrix = integrate_bootstraps( dl_vals, dU_samples, x_intp=dl_intp, matrix=self.ti_matrix ) @@ -1155,12 +1166,12 @@ def run_ti(self, phase, prepared_data, method): total_sem_matrix = self.results[phase][method]["fraction_sem_matrix"][ max_fraction ].magnitude - self.results[phase][method]["roi"] = np.zeros([num_win], np.float64) + self.results[phase][method]["roi"] = numpy.zeros([num_win], numpy.float64) for k in range(num_win): # Compute overall integrated SEM with 10% smaller SEM for dU[k] - cnvg_dU_samples = np.array(dU_samples) - cnvg_dU_samples[:, k] = np.random.normal( + cnvg_dU_samples = numpy.array(dU_samples) + cnvg_dU_samples[:, k] = numpy.random.normal( dU_avgs[k], 0.9 * dU_sems[k], self.boot_cycles ) junk_fe, cnvg_sem_matrix = integrate_bootstraps( @@ -1218,7 +1229,7 @@ def compute_free_energy(self, phases=["attach", "pull", "release"], seed=None): for method in self.methods: if seed is not None: - np.random.seed(seed) + numpy.random.seed(seed) logger.debug(f"Setting random number seed = {seed}") self.results[phase][method] = {} @@ -1228,7 +1239,7 @@ def compute_free_energy(self, phases=["attach", "pull", "release"], seed=None): logger.debug("Skipping free energy calculation for %s" % phase) continue prepared_data = self.prepare_data(phase) - self.results[phase][method]["n_frames"] = np.sum(prepared_data[1]) + self.results[phase][method]["n_frames"] = numpy.sum(prepared_data[1]) logger.debug( "Running {} analysis on {} phase ...".format(method, phase) @@ -1269,7 +1280,7 @@ def compute_free_energy(self, phases=["attach", "pull", "release"], seed=None): # Set these higher level (total) values, which will be slightly # easier to access - max_fraction = np.max(self.fractions) + max_fraction = numpy.max(self.fractions) self.results[phase][method]["fe_matrix"] = self.results[phase][method][ "fraction_fe_matrix" ][max_fraction] @@ -1290,7 +1301,7 @@ def compute_free_energy(self, phases=["attach", "pull", "release"], seed=None): self.results[phase][method][ "largest_neighbor" ] = openff_unit.Quantity( - np.ones([windows], np.float64) * -1.0, + numpy.ones([windows], numpy.float64) * -1.0, units=self.energy_unit, ) logger.info(f"{phase}: computing largest_neighbor for {method}...") @@ -1394,12 +1405,12 @@ def compute_ref_state_work(self, restraints, state="final"): force_index = 0 fcs.append( - np.sort(restraint.phase[phase]["force_constants"])[ + numpy.sort(restraint.phase[phase]["force_constants"])[ force_index ] ) targs.append( - np.sort(restraint.phase[phase]["targets"])[target_index] + numpy.sort(restraint.phase[phase]["targets"])[target_index] ) target_and_force_exist = True @@ -1589,7 +1600,7 @@ def ref_state_work( Returns ------- - RT * np.log(trans * orient): openff.units.unit.Quantity + RT * numpy.log(trans * orient): openff.units.unit.Quantity The free energy associated with releasing the restraints (in kcal/mol openff units). """ @@ -1603,41 +1614,45 @@ def ref_state_work( # Distance Integration Function def dist_int(RT, fc, targ): def potential(arange, RT, fc, targ): - return (arange**2) * np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) + return (arange**2) * numpy.exp((-1.0 / RT) * fc * (arange - targ) ** 2) targ = targ.to(distance_unit) fc = fc.to(energy_unit / distance_unit**2) - arange = (np.arange(0.0, 100.0, 0.0001) * openff_unit.angstrom).to( + arange = (numpy.arange(0.0, 100.0, 0.0001) * openff_unit.angstrom).to( distance_unit ) - return np.trapz(potential(arange, RT, fc, targ), arange) + return numpy.trapz(potential(arange, RT, fc, targ), arange) # Angle Integration Function def ang_int(RT, fc, targ): def potential(arange, RT, fc, targ): - return np.sin(arange) * np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) + return numpy.sin(arange) * numpy.exp( + (-1.0 / RT) * fc * (arange - targ) ** 2 + ) targ = targ.to(angle_unit) fc = fc.to(energy_unit / angle_unit**2) - arange = (np.arange(0.0, np.pi, 0.00005) * openff_unit.radians).to(angle_unit) + arange = (numpy.arange(0.0, numpy.pi, 0.00005) * openff_unit.radians).to( + angle_unit + ) - return np.trapz(potential(arange, RT, fc, targ), arange) + return numpy.trapz(potential(arange, RT, fc, targ), arange) # Torsion Integration Function def tors_int(RT, fc, targ): def potential(arange, RT, fc, targ): - return np.exp((-1.0 / RT) * fc * (arange - targ) ** 2) + return numpy.exp((-1.0 / RT) * fc * (arange - targ) ** 2) # Note, because of periodicity, I'm gonna wrap +/- pi around target for integration. targ = targ.to(angle_unit) fc = fc.to(energy_unit / angle_unit**2) arange = ( - np.arange(targ.magnitude - np.pi, targ.magnitude + np.pi, 0.00005) + numpy.arange(targ.magnitude - numpy.pi, targ.magnitude + numpy.pi, 0.00005) * openff_unit.radians ).to(angle_unit) - return np.trapz(potential(arange, RT, fc, targ), arange) + return numpy.trapz(potential(arange, RT, fc, targ), arange) # Distance restraint, r if None in [r_fc, r_tg]: @@ -1653,13 +1668,13 @@ def potential(arange, RT, fc, targ): # Torsion restraint, phi if None in [ph_fc, ph_tg]: - ph_int = 2.0 * np.pi * openff_unit.radians + ph_int = 2.0 * numpy.pi * openff_unit.radians else: ph_int = tors_int(RT, ph_fc, ph_tg) # Torsion restraint, alpha if None in [a_fc, a_tg]: - a_int = 2.0 * np.pi * openff_unit.radians + a_int = 2.0 * numpy.pi * openff_unit.radians else: a_int = tors_int(RT, a_fc, a_tg) @@ -1671,7 +1686,7 @@ def potential(arange, RT, fc, targ): # Torsion restraint, gamma if None in [g_fc, g_tg]: - g_int = 2.0 * np.pi * openff_unit.radians + g_int = 2.0 * numpy.pi * openff_unit.radians else: g_int = tors_int(RT, g_fc, g_tg) @@ -1680,8 +1695,8 @@ def potential(arange, RT, fc, targ): translational = r_int * th_int * ph_int * (1.0 / V0) # C^o = 1/V^o # Orientational term - rotational_volume = 8.0 * np.pi**2 + rotational_volume = 8.0 * numpy.pi**2 orientational = a_int * b_int * g_int / rotational_volume # Return the free energy - return RT * np.log(translational * orientational) + return RT * numpy.log(translational * orientational) diff --git a/paprika/analysis/bootstrap.py b/paprika/analysis/bootstrap.py index adbe1c7..637dc55 100644 --- a/paprika/analysis/bootstrap.py +++ b/paprika/analysis/bootstrap.py @@ -1,6 +1,6 @@ from typing import Dict, List, Union -import numpy as np +import numpy import openmm.unit as openmm_unit from openff.units import unit from openff.units.openmm import from_openmm @@ -20,13 +20,13 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): Parameters ---------- - x: :class:`np.array` + x: :class:`numpy.array` The x coordinate of the curve to be integrated. - ys: :class:`np.array` + ys: :class:`numpy.array` Two dimensional array in which the first dimension is boot_cycles and the second dimension contains the arrays of y values which correspond to the x values and will be used for integration. The shape of this is :code:`(boot_cycles, len(x))`. - x_intp: :class:`np.array`, optional, default=None + x_intp: :class:`numpy.array`, optional, default=None An array which finely interpolates the x values. If not provided, it will be generated by adding 100 evenly spaced points between each x value. Default: None. matrix: str, optional, default='full` @@ -37,9 +37,9 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): Returns ------- - avg_matrix: :class:`np.array` + avg_matrix: :class:`numpy.array` Matrix of the integration mean between each x value (as specified by 'matrix') - sem_matrix: :class:`np.array` + sem_matrix: :class:`numpy.array` Matrix of the uncertainty (SEM) between each x value (as specified by 'matrix') """ @@ -47,25 +47,25 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): num_x = len(x) # Prepare to store the index location of the x values in the x_intp array - x_idxs = np.zeros([num_x], np.int32) + x_idxs = numpy.zeros([num_x], numpy.int32) # If not provided, generate x interpolation with 100 inpolated points between # each x value. Store the index locations of the x values in the x_intp # array. if x_intp is None: - x_intp = np.zeros([0], np.float64) + x_intp = numpy.zeros([0], numpy.float64) for i in range(1, num_x): - x_intp = np.append( - x_intp, np.linspace(x[i - 1], x[i], num=100, endpoint=False) + x_intp = numpy.append( + x_intp, numpy.linspace(x[i - 1], x[i], num=100, endpoint=False) ) x_idxs = len(x_intp) # Tack on the final value onto the interpolation - x_intp = np.append(x_intp, x[-1]) + x_intp = numpy.append(x_intp, x[-1]) # If x_intp is provided, find the locations of x values in x_intp else: i = 0 for j in range(len(x_intp)): - if np.isclose(x[i], x_intp[j]): + if numpy.isclose(x[i], x_intp[j]): x_idxs[i] = j i += 1 if i != num_x: @@ -77,7 +77,7 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): cycles = len(ys) # Setup array to store integration bootstraps - int_matrix = np.zeros([num_x, num_x, cycles], np.float64) + int_matrix = numpy.zeros([num_x, num_x, cycles], numpy.float64) # Do the integration bootstraps. Originally, I had matrix=endpoints in the loop # below with everything else, but I'll split it out here in case that's faster @@ -88,8 +88,8 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): y_intp = intp_func(x_intp) # for i in range(0, num_x): # for j in range(i+1, num_x): - # int_matrix[i, j, cycle] = np.trapz( y_intp, x_intp ) - int_matrix[0, num_x - 1, cycle] = np.trapz(y_intp, x_intp) + # int_matrix[i, j, cycle] = numpy.trapz( y_intp, x_intp ) + int_matrix[0, num_x - 1, cycle] = numpy.trapz(y_intp, x_intp) else: for cycle in range(cycles): intp_func = Akima1DInterpolator(x, ys[cycle]) @@ -100,12 +100,14 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): continue beg = x_idxs[i] end = x_idxs[j] - int_matrix[i, j, cycle] = np.trapz(y_intp[beg:end], x_intp[beg:end]) + int_matrix[i, j, cycle] = numpy.trapz( + y_intp[beg:end], x_intp[beg:end] + ) # Setup matrices to store the average/sem values. # Is it bad that the default is 0.0 rather than None? - avg_matrix = np.zeros([num_x, num_x], np.float64) - sem_matrix = np.zeros([num_x, num_x], np.float64) + avg_matrix = numpy.zeros([num_x, num_x], numpy.float64) + sem_matrix = numpy.zeros([num_x, num_x], numpy.float64) # Second pass to compute the mean and standard deviation. for i in range(0, num_x): @@ -116,9 +118,9 @@ def integrate_bootstraps(x, ys, x_intp=None, matrix="full"): continue if matrix == "endpoints" and i != 0 and j != num_x - 1: continue - avg_matrix[i, j] = np.mean(int_matrix[i, j]) + avg_matrix[i, j] = numpy.mean(int_matrix[i, j]) avg_matrix[j, i] = -1.0 * avg_matrix[i, j] - sem_matrix[i, j] = np.std(int_matrix[i, j]) + sem_matrix[i, j] = numpy.std(int_matrix[i, j]) sem_matrix[j, i] = sem_matrix[i, j] return avg_matrix, sem_matrix @@ -155,56 +157,56 @@ def regression_bootstrap( Returns the mean, sem, ci_low, ci_high for [slope, intercept, R, R^2, RMSE, MSE, MUE, Kendall's Tau] """ - summary_statistics = np.empty((cycles, 8)) + summary_statistics = numpy.empty((cycles, 8)) # Bootstrapping for cycle in range(cycles): - new_x = np.empty_like(x_data) - new_y = np.empty_like(y_data) + new_x = numpy.empty_like(x_data) + new_y = numpy.empty_like(y_data) for index in range(len(x_data)): if with_replacement: - j = np.random.randint(len(x_data)) + j = numpy.random.randint(len(x_data)) else: j = index if with_uncertainty and x_sem is not None: - new_x[index] = np.random.normal(x_data[j], x_sem[j]) + new_x[index] = numpy.random.normal(x_data[j], x_sem[j]) elif with_uncertainty and x_sem is None: new_x[index] = x_data[j] if with_uncertainty and y_sem is not None: - new_y[index] = np.random.normal(y_data[j], y_sem[j]) + new_y[index] = numpy.random.normal(y_data[j], y_sem[j]) elif with_uncertainty and y_sem is None: new_y[index] = y_data[j] summary_statistics[cycle] = summarize_statistics(new_x, new_y) # Confidence interval - ci = np.empty((8, 2)) + ci = numpy.empty((8, 2)) for statistic in range(8): - sorted_statistic = np.sort(summary_statistics[:, statistic]) + sorted_statistic = numpy.sort(summary_statistics[:, statistic]) ci[statistic][0] = sorted_statistic[int(0.025 * cycles)] ci[statistic][1] = sorted_statistic[int(0.975 * cycles)] # Summarize results results = { "mean": { - "slope": np.mean(summary_statistics[:, 0]), - "intercept": np.mean(summary_statistics[:, 1]), - "R": np.mean(summary_statistics[:, 2]), - "R**2": np.mean(summary_statistics[:, 3]), - "RMSE": np.mean(summary_statistics[:, 4]), - "MSE": np.mean(summary_statistics[:, 5]), - "MUE": np.mean(summary_statistics[:, 6]), - "Tau": np.mean(summary_statistics[:, 7]), + "slope": numpy.mean(summary_statistics[:, 0]), + "intercept": numpy.mean(summary_statistics[:, 1]), + "R": numpy.mean(summary_statistics[:, 2]), + "R**2": numpy.mean(summary_statistics[:, 3]), + "RMSE": numpy.mean(summary_statistics[:, 4]), + "MSE": numpy.mean(summary_statistics[:, 5]), + "MUE": numpy.mean(summary_statistics[:, 6]), + "Tau": numpy.mean(summary_statistics[:, 7]), }, "sem": { - "slope": np.std(summary_statistics[:, 0]), - "intercept": np.std(summary_statistics[:, 1]), - "R": np.std(summary_statistics[:, 2]), - "R**2": np.std(summary_statistics[:, 3]), - "RMSE": np.std(summary_statistics[:, 4]), - "MSE": np.std(summary_statistics[:, 5]), - "MUE": np.std(summary_statistics[:, 6]), - "Tau": np.std(summary_statistics[:, 7]), + "slope": numpy.std(summary_statistics[:, 0]), + "intercept": numpy.std(summary_statistics[:, 1]), + "R": numpy.std(summary_statistics[:, 2]), + "R**2": numpy.std(summary_statistics[:, 3]), + "RMSE": numpy.std(summary_statistics[:, 4]), + "MSE": numpy.std(summary_statistics[:, 5]), + "MUE": numpy.std(summary_statistics[:, 6]), + "Tau": numpy.std(summary_statistics[:, 7]), }, "ci_low": { "slope": ci[0][0], @@ -232,10 +234,10 @@ def regression_bootstrap( def dG_bootstrap( - x_data: Union[List, np.array], - x_sem: Union[List, np.array], - y_data: Union[List, np.array], - y_sem: Union[List, np.array], + x_data: Union[List, numpy.array], + x_sem: Union[List, numpy.array], + y_data: Union[List, numpy.array], + y_sem: Union[List, numpy.array], cycles: int = 1000, temperature: Union[float, unit.Quantity] = 298.15 * unit.kelvin, with_uncertainty: bool = True, @@ -276,35 +278,35 @@ def dG_bootstrap( y_sem = check_unit(y_sem, base_unit=unit.kilocalorie_per_mole).magnitude temperature = check_unit(temperature, base_unit=unit.kelvin) - summary_statistics = np.empty((cycles)) + summary_statistics = numpy.empty((cycles)) RT = (R_gas * temperature).to(unit.kilocalorie_per_mole).magnitude beta = 1.0 / RT - ci = np.empty((2)) + ci = numpy.empty((2)) for cycle in range(cycles): - new_x = np.empty_like(x_data) - new_y = np.empty_like(y_data) + new_x = numpy.empty_like(x_data) + new_y = numpy.empty_like(y_data) if with_uncertainty and x_sem is not None: - new_x = np.random.normal(x_data, x_sem) + new_x = numpy.random.normal(x_data, x_sem) elif with_uncertainty and x_sem is None: new_x = x_data if with_uncertainty and y_sem is not None: - new_y = np.random.normal(y_data, y_sem) + new_y = numpy.random.normal(y_data, y_sem) elif with_uncertainty and y_sem is None: new_y = y_data - summary_statistics[cycle] = -RT * np.log( - np.exp(-beta * new_x) + np.exp(-beta * new_y) + summary_statistics[cycle] = -RT * numpy.log( + numpy.exp(-beta * new_x) + numpy.exp(-beta * new_y) ) # Get confidence interval - sorted_statistic = np.sort(summary_statistics) + sorted_statistic = numpy.sort(summary_statistics) ci[0] = sorted_statistic[int(0.025 * cycles)] ci[1] = sorted_statistic[int(0.975 * cycles)] results = { - "mean": np.mean(summary_statistics), - "sem": np.std(summary_statistics), + "mean": numpy.mean(summary_statistics), + "sem": numpy.std(summary_statistics), "ci": ci, } @@ -376,50 +378,51 @@ def dH_bootstrap( dG_y_sem = check_unit(dG_y_sem, base_unit=unit.kilocalorie_per_mole).magnitude temperature = check_unit(temperature, base_unit=unit.kelvin) - summary_statistics = np.empty((cycles)) + summary_statistics = numpy.empty((cycles)) RT = (R_gas * temperature).to(unit.kcal / unit.mole).magnitude beta = 1.0 / RT - ci = np.empty((2)) + ci = numpy.empty((2)) for cycle in range(cycles): - new_dH_x = np.empty_like(dH_x_data) - new_dH_y = np.empty_like(dH_y_data) + new_dH_x = numpy.empty_like(dH_x_data) + new_dH_y = numpy.empty_like(dH_y_data) - new_dG_x = np.empty_like(dG_x_data) - new_dG_y = np.empty_like(dG_y_data) + new_dG_x = numpy.empty_like(dG_x_data) + new_dG_y = numpy.empty_like(dG_y_data) # Resample dH if with_uncertainty and dH_x_sem is not None: - new_dH_x = np.random.normal(dH_x_data, dH_x_sem) + new_dH_x = numpy.random.normal(dH_x_data, dH_x_sem) elif with_uncertainty and dH_x_sem is None: new_dH_x = dH_x_data if with_uncertainty and dH_y_sem is not None: - new_dH_y = np.random.normal(dH_y_data, dH_y_sem) + new_dH_y = numpy.random.normal(dH_y_data, dH_y_sem) elif with_uncertainty and dH_y_sem is None: new_dH_y = dH_y_data # Resample dG if with_uncertainty and dG_x_sem is not None: - new_dG_x = np.random.normal(dG_x_data, dG_x_sem) + new_dG_x = numpy.random.normal(dG_x_data, dG_x_sem) elif with_uncertainty and dG_x_sem is None: new_dG_x = dG_x_data if with_uncertainty and dG_y_sem is not None: - new_dG_y = np.random.normal(dG_y_data, dG_y_sem) + new_dG_y = numpy.random.normal(dG_y_data, dG_y_sem) elif with_uncertainty and dG_y_sem is None: new_dG_y = dG_y_data summary_statistics[cycle] = ( - new_dH_x * np.exp(-beta * new_dG_x) + new_dH_y * np.exp(-beta * new_dG_y) - ) / (np.exp(-beta * new_dG_x) + np.exp(-beta * new_dG_y)) + new_dH_x * numpy.exp(-beta * new_dG_x) + + new_dH_y * numpy.exp(-beta * new_dG_y) + ) / (numpy.exp(-beta * new_dG_x) + numpy.exp(-beta * new_dG_y)) # Confidence interval - sorted_statistic = np.sort(summary_statistics) + sorted_statistic = numpy.sort(summary_statistics) ci[0] = sorted_statistic[int(0.025 * cycles)] ci[1] = sorted_statistic[int(0.975 * cycles)] results = { - "mean": np.mean(summary_statistics), - "sem": np.std(summary_statistics), + "mean": numpy.mean(summary_statistics), + "sem": numpy.std(summary_statistics), "ci": ci, } @@ -436,9 +439,9 @@ def summarize_statistics(x, y): Parameters ---------- - x: np.array + x: numpy.array X data - y: np.array + y: numpy.array Y data Returns @@ -453,7 +456,7 @@ def summarize_statistics(x, y): * MUE - Mean Unsigned Error or Mean Absolute Error * Tau - Kendall's Tau """ - summary_statistics = np.empty(8) + summary_statistics = numpy.empty(8) # Slope, intercept, R - Pearson correlation coefficient ( summary_statistics[0], @@ -467,13 +470,13 @@ def summarize_statistics(x, y): summary_statistics[3] = summary_statistics[2] ** 2 # RMSE - Root-Mean-Squared-Error - summary_statistics[4] = np.sqrt(np.mean((y - x) ** 2)) + summary_statistics[4] = numpy.sqrt(numpy.mean((y - x) ** 2)) # MSE - Mean Signed Error - summary_statistics[5] = np.mean(y - x) + summary_statistics[5] = numpy.mean(y - x) # MUE - Mean Unsigned Error - summary_statistics[6] = np.mean(np.absolute(y - x)) + summary_statistics[6] = numpy.mean(numpy.absolute(y - x)) # Tau - Kendall's Tau summary_statistics[7], prob = statistics.kendalltau(x, y) diff --git a/paprika/analysis/utils.py b/paprika/analysis/utils.py index 805a9e1..a2afca8 100644 --- a/paprika/analysis/utils.py +++ b/paprika/analysis/utils.py @@ -1,4 +1,4 @@ -import numpy as np +import numpy def get_factors(n): @@ -17,7 +17,7 @@ def get_factors(n): """ factors = [] - sqrt_n = int(round(np.sqrt(n) + 0.5)) + sqrt_n = int(round(numpy.sqrt(n) + 0.5)) i = 1 while i <= sqrt_n: if n % i == 0: @@ -26,6 +26,7 @@ def get_factors(n): if j != i: factors.append(int(j)) i += 1 + return sorted(factors, key=int) @@ -45,19 +46,24 @@ def get_nearest_max(n): """ max_factors = 0 + if n % 2 == 0: beg = n - 100 end = n else: beg = n - 101 end = n - 1 + if beg < 0: beg = 0 + + most_factors = 0 for i in range(beg, end + 2, 2): num_factors = len(get_factors(i)) if num_factors >= max_factors: max_factors = num_factors most_factors = i + return most_factors @@ -72,12 +78,12 @@ def get_block_sem(data_array): Parameters ---------- - data_array: :class:`np.array` + data_array: :class:`numpy.array` Array containing data values. Returns ------- - np.max(sems): float + numpy.max(sems): float The maximum SEM obtained from te blocking curve. """ @@ -86,11 +92,11 @@ def get_block_sem(data_array): block_sizes = get_factors(len(data_array)) # An array to store means for each block ... make it bigger than we need. - block_means = np.zeros([block_sizes[-1]], np.float64) + block_means = numpy.zeros([block_sizes[-1]], numpy.float64) # Store the SEM for each block size, except the last two size for which # there will only be two or one blocks total and thus very noisy. - sems = np.zeros([len(block_sizes) - 2], np.float64) + sems = numpy.zeros([len(block_sizes) - 2], numpy.float64) # Check each block size except the last two. for size_idx in range(len(block_sizes) - 2): @@ -102,15 +108,15 @@ def get_block_sem(data_array): data_beg_idx = blk_idx * block_sizes[size_idx] data_end_idx = (blk_idx + 1) * block_sizes[size_idx] # Compute the mean of this block and store in array - block_means[blk_idx] = np.mean(data_array[data_beg_idx:data_end_idx]) + block_means[blk_idx] = numpy.mean(data_array[data_beg_idx:data_end_idx]) # Compute the standard deviation across all blocks, devide by # num_blocks-1 for SEM - sems[size_idx] = np.std(block_means[0:num_blocks], ddof=0) / np.sqrt( + sems[size_idx] = numpy.std(block_means[0:num_blocks], ddof=0) / numpy.sqrt( num_blocks - 1 ) # Hmm or should ddof=1? I think 0, see Flyvbjerg -----^ - return np.max(sems) + return numpy.max(sems) def get_subsampled_indices(N, g, conservative=False): @@ -120,7 +126,7 @@ def get_subsampled_indices(N, g, conservative=False): ---------- N: int The length of the array to be indexed. - g: int + g: float The statistical inefficiency of the data. conservative: bool, optional, default=False Whether `g` should be rounded up to the nearest integer. @@ -138,16 +144,16 @@ def get_subsampled_indices(N, g, conservative=False): # if conservative, assume integer g and round up if conservative: - g = np.ceil(g) + g = numpy.ceil(g) # initialize indices = [0] g_idx = 1.0 - int_step = int(np.round(g_idx * g)) + int_step = int(numpy.round(g_idx * g)) while int_step < N: indices.append(int_step) g_idx += 1.0 - int_step = int(np.round(g_idx * g)) + int_step = int(numpy.round(g_idx * g)) return indices diff --git a/paprika/build/align.py b/paprika/build/align.py index 75daa16..981a6da 100644 --- a/paprika/build/align.py +++ b/paprika/build/align.py @@ -1,7 +1,7 @@ import logging -import numpy as np -import parmed as pmd +import numpy +import parmed from openff.units import unit as openff_unit from paprika.utils import check_unit @@ -47,15 +47,15 @@ def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): # Mask1 mask1_coordinates = structure[mask1].coordinates mask1_masses = [atom.mass for atom in structure[mask1].atoms] - mask1_com = pmd.geometry.center_of_mass( - np.asarray(mask1_coordinates), np.asarray(mask1_masses) + mask1_com = parmed.geometry.center_of_mass( + numpy.asarray(mask1_coordinates), numpy.asarray(mask1_masses) ) # Mask2 mask2_coordinates = structure[mask2].coordinates mask2_masses = [atom.mass for atom in structure[mask2].atoms] - mask2_com = pmd.geometry.center_of_mass( - np.asarray(mask2_coordinates), np.asarray(mask2_masses) + mask2_com = parmed.geometry.center_of_mass( + numpy.asarray(mask2_coordinates), numpy.asarray(mask2_masses) ) logger.info( @@ -74,10 +74,10 @@ def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): rotation_matrix = get_rotation_matrix(mask2_com, axis) # This is certainly not the fastest approach, but it is explicit. - aligned_coords = np.empty_like(structure.coordinates) + aligned_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): aligned_coords[atom] = structure.coordinates[atom] + -1.0 * mask1_com - aligned_coords[atom] = np.dot(rotation_matrix, aligned_coords[atom]) + aligned_coords[atom] = numpy.dot(rotation_matrix, aligned_coords[atom]) structure.coordinates = aligned_coords if save: @@ -150,10 +150,10 @@ def align_principal_axes(structure, atom_mask=None, principal_axis=1, axis="z"): rotation_matrix = get_rotation_matrix(p_axis, axis) # Align the principal axis to specified axis - aligned_coords = np.empty_like(structure.coordinates) + aligned_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): aligned_coords[atom] = structure.coordinates[atom] - aligned_coords[atom] = np.dot(rotation_matrix, aligned_coords[atom]) + aligned_coords[atom] = numpy.dot(rotation_matrix, aligned_coords[atom]) structure.coordinates = aligned_coords return structure @@ -192,63 +192,63 @@ def rotate_around_axis(structure, axis, angle): # Convert angle to radians (temporary until Pint integration) angle = check_unit(angle, base_unit=openff_unit.radians).to(openff_unit.radians).magnitude - if np.array_equal(axis, np.array([1.0, 0.0, 0.0])): - rotation_matrix = np.array( + if numpy.array_equal(axis, numpy.array([1.0, 0.0, 0.0])): + rotation_matrix = numpy.array( [ [1, 0, 0], - [0, np.cos(angle), -np.sin(angle)], - [0, np.sin(angle), np.cos(angle)], + [0, numpy.cos(angle), -numpy.sin(angle)], + [0, numpy.sin(angle), numpy.cos(angle)], ] ) - elif np.array_equal(axis, np.array([0.0, 1.0, 0.0])): - rotation_matrix = np.array( + elif numpy.array_equal(axis, numpy.array([0.0, 1.0, 0.0])): + rotation_matrix = numpy.array( [ - [np.cos(angle), 0, np.sin(angle)], + [numpy.cos(angle), 0, numpy.sin(angle)], [0, 1, 0], - [-np.sin(angle), 0, np.cos(angle)], + [-numpy.sin(angle), 0, numpy.cos(angle)], ] ) - elif np.array_equal(axis, np.array([0.0, 0.0, 1.0])): - rotation_matrix = np.array( + elif numpy.array_equal(axis, numpy.array([0.0, 0.0, 1.0])): + rotation_matrix = numpy.array( [ - [np.cos(angle), -np.sin(angle), 0], - [np.sin(angle), np.cos(angle), 0], + [numpy.cos(angle), -numpy.sin(angle), 0], + [numpy.sin(angle), numpy.cos(angle), 0], [0, 0, 1], ] ) else: # Normalize axis vector - axis = axis / np.linalg.norm(axis) + axis = axis / numpy.linalg.norm(axis) u_x = axis[0] u_y = axis[1] u_z = axis[2] - rotation_matrix = np.array( + rotation_matrix = numpy.array( [ [ - np.cos(angle) + u_x ** 2 * (1 - np.cos(angle)), - u_x * u_y * (1 - np.cos(angle)) - u_z * np.sin(angle), - u_x * u_z * (1 - np.cos(angle)) + u_y * np.sin(angle), + numpy.cos(angle) + u_x ** 2 * (1 - numpy.cos(angle)), + u_x * u_y * (1 - numpy.cos(angle)) - u_z * numpy.sin(angle), + u_x * u_z * (1 - numpy.cos(angle)) + u_y * numpy.sin(angle), ], [ - u_y * u_x * (1 - np.cos(angle)) + u_z * np.sin(angle), - np.cos(angle) + u_y ** 2 * (1 - np.cos(angle)), - u_y * u_z * (1 - np.cos(angle)) - u_x * np.sin(angle), + u_y * u_x * (1 - numpy.cos(angle)) + u_z * numpy.sin(angle), + numpy.cos(angle) + u_y ** 2 * (1 - numpy.cos(angle)), + u_y * u_z * (1 - numpy.cos(angle)) - u_x * numpy.sin(angle), ], [ - u_z * u_x * (1 - np.cos(angle)) - u_y * np.sin(angle), - u_z * u_y * (1 - np.cos(angle)) + u_x * np.sin(angle), - np.cos(angle) + u_z ** 2 * (1 - np.cos(angle)), + u_z * u_x * (1 - numpy.cos(angle)) - u_y * numpy.sin(angle), + u_z * u_y * (1 - numpy.cos(angle)) + u_x * numpy.sin(angle), + numpy.cos(angle) + u_z ** 2 * (1 - numpy.cos(angle)), ], ] ) # Align the principal axis to specified axis - aligned_coords = np.empty_like(structure.coordinates) + aligned_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): aligned_coords[atom] = structure.coordinates[atom] - aligned_coords[atom] = np.dot(rotation_matrix, aligned_coords[atom]) + aligned_coords[atom] = numpy.dot(rotation_matrix, aligned_coords[atom]) structure.coordinates = aligned_coords return structure @@ -281,23 +281,23 @@ def get_theta(structure, mask1, mask2, axis): # Centroid of mask1 mask1_coordinates = structure[mask1].coordinates mask1_masses = [atom.mass for atom in structure[mask1].atoms] - mask1_com = pmd.geometry.center_of_mass( - np.asarray(mask1_coordinates), np.asarray(mask1_masses) + mask1_com = parmed.geometry.center_of_mass( + numpy.asarray(mask1_coordinates), numpy.asarray(mask1_masses) ) # Centroid of mask2 mask2_coordinates = structure[mask2].coordinates mask2_masses = [atom.mass for atom in structure[mask2].atoms] - mask2_com = pmd.geometry.center_of_mass( - np.asarray(mask2_coordinates), np.asarray(mask2_masses) + mask2_com = parmed.geometry.center_of_mass( + numpy.asarray(mask2_coordinates), numpy.asarray(mask2_masses) ) # Vector mask1-mask2 vector = mask2_com + -1.0 * mask1_com # Angle between vectors - theta = np.arccos( - np.dot(vector, axis) / (np.linalg.norm(vector) * np.linalg.norm(axis)) + theta = numpy.arccos( + numpy.dot(vector, axis) / (numpy.linalg.norm(vector) * numpy.linalg.norm(axis)) ) return openff_unit.Quantity(theta, units=openff_unit.radians) @@ -321,28 +321,28 @@ def get_rotation_matrix(vector, ref_vector): """ # If the structures are already aligned (cross product is zero), return 3x3 identity matrix - if np.linalg.norm(np.cross(vector, ref_vector)) == 0: + if numpy.linalg.norm(numpy.cross(vector, ref_vector)) == 0: logger.info("The structure is already aligned and the denominator is invalid, returning identity matrix.") - return np.identity(3) + return numpy.identity(3) # Find axis between the mask vector and the axis using cross and dot products. - x = np.cross(vector, ref_vector) / np.linalg.norm(np.cross(vector, ref_vector)) + x = numpy.cross(vector, ref_vector) / numpy.linalg.norm(numpy.cross(vector, ref_vector)) - theta = np.arccos( - np.dot(vector, ref_vector) - / (np.linalg.norm(vector) * np.linalg.norm(ref_vector)) + theta = numpy.arccos( + numpy.dot(vector, ref_vector) + / (numpy.linalg.norm(vector) * numpy.linalg.norm(ref_vector)) ) # https://math.stackexchange.com/questions/293116/rotating-one-3d-vector-to-another - A = np.array( + A = numpy.array( [[0, -1.0 * x[2], x[1]], [x[2], 0, -1.0 * x[0]], [-1.0 * x[1], x[0], 0]] ) # Rotation matrix rotation_matrix = ( - np.identity(3) - + np.dot(np.sin(theta), A) - + np.dot((1.0 - np.cos(theta)), np.dot(A, A)) + numpy.identity(3) + + numpy.dot(numpy.sin(theta), A) + + numpy.dot((1.0 - numpy.cos(theta)), numpy.dot(A, A)) ) return rotation_matrix @@ -369,13 +369,13 @@ def get_principal_axis_vector(structure, principal_axis=1, atom_mask=None): """ # Get coordinates and masses coordinates = structure.coordinates - masses = np.asarray([atom.mass for atom in structure.atoms]) + masses = numpy.asarray([atom.mass for atom in structure.atoms]) if atom_mask: coordinates = structure[atom_mask].coordinates - masses = np.asarray([atom.mass for atom in structure[atom_mask].atoms]) + masses = numpy.asarray([atom.mass for atom in structure[atom_mask].atoms]) # Calculate center of mass - centroid = pmd.geometry.center_of_mass(coordinates, masses) + centroid = parmed.geometry.center_of_mass(coordinates, masses) # Construct Inertia tensor Ixx = Ixy = Ixz = Iyy = Iyz = Izz = 0 @@ -388,10 +388,10 @@ def get_principal_axis_vector(structure, principal_axis=1, atom_mask=None): Iyz -= mass * (xyz[1] * xyz[2]) Izz += mass * (xyz[0] * xyz[0] + xyz[1] * xyz[1]) - inertia = np.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) + inertia = numpy.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) # Principal axis - evals, evecs = np.linalg.eig(inertia) + evals, evecs = numpy.linalg.eig(inertia) evecs = evecs[ :, evals.argsort()[::-1] ] # <-- Numpy may not sort the eigenvalues properly @@ -412,15 +412,15 @@ def check_coordinates(structure, mask): Returns ------- - mask_com : :class:`np.array` + mask_com : :class:`numpy.array` Coordinates of the selection center of mass. """ mask_coordinates = structure[mask].coordinates mask_masses = [atom.mass for atom in structure[mask].atoms] - mask_com = pmd.geometry.center_of_mass( - np.asarray(mask_coordinates), np.asarray(mask_masses) + mask_com = parmed.geometry.center_of_mass( + numpy.asarray(mask_coordinates), numpy.asarray(mask_masses) ) return mask_com @@ -452,13 +452,13 @@ def offset_structure(structure, offset, dimension=None): >>> structure = offset_structure(structure, 3.0, dimension='z') >>> >>> # Offset a structure using a numpy array - >>> offset = np.array([0.0, 5.0, 2.0]) + >>> offset = numpy.array([0.0, 5.0, 2.0]) >>> structure = offset_structure(structure, offset) """ # Dimension mask if dimension is None: - mask = np.array([1, 1, 1]) + mask = numpy.array([1, 1, 1]) else: mask = _return_array(dimension) @@ -467,7 +467,7 @@ def offset_structure(structure, offset, dimension=None): offset = offset.to(openff_unit.angstrom).magnitude # Offset coordinates - offset_coords = np.empty_like(structure.coordinates) + offset_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): offset_coords[atom] = structure.coordinates[atom] + offset structure.coordinates = offset_coords @@ -513,30 +513,30 @@ def translate_to_origin(structure, weight="mass", atom_mask=None, dimension=None # Dimension mask if dimension is None: - mask = np.array([1, 1, 1]) + mask = numpy.array([1, 1, 1]) else: mask = _return_array(dimension) # Atomic coordinates and masses if atom_mask is None: coordinates = structure.coordinates - masses = np.asarray([atom.mass for atom in structure.atoms]) + masses = numpy.asarray([atom.mass for atom in structure.atoms]) else: coordinates = structure[atom_mask].coordinates - masses = np.asarray([atom.mass for atom in structure[atom_mask].atoms]) + masses = numpy.asarray([atom.mass for atom in structure[atom_mask].atoms]) # Equal weights if geometric center is preferred if weight == "geo": - masses = np.ones(len(coordinates)) + masses = numpy.ones(len(coordinates)) # Centroid coordinates - centroid = pmd.geometry.center_of_mass(coordinates, masses) + centroid = parmed.geometry.center_of_mass(coordinates, masses) if mask is not None: centroid *= mask # Translate coordinates - aligned_coords = np.empty_like(structure.coordinates) + aligned_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): aligned_coords[atom] = structure.coordinates[atom] - centroid structure.coordinates = aligned_coords @@ -551,20 +551,20 @@ def _return_array(var): if isinstance(var, str): if "x" in var.lower(): - array = np.array([1.0, 0.0, 0.0]) + array = numpy.array([1.0, 0.0, 0.0]) elif "y" in var.lower(): - array = np.array([0.0, 1.0, 0.0]) + array = numpy.array([0.0, 1.0, 0.0]) elif "z" in var.lower(): - array = np.array([0.0, 0.0, 1.0]) + array = numpy.array([0.0, 0.0, 1.0]) else: raise KeyError(f"Cannot understand the variable: {var}.") elif isinstance(var, list): if len(var) != 3: raise ValueError('"var" must be a list with 3 elements.') - array = np.array(var) + array = numpy.array(var) - elif isinstance(var, np.ndarray): + elif isinstance(var, numpy.ndarray): if len(var) != 3: raise ValueError('"var" must be a numpy array with 3 elements.') diff --git a/paprika/build/dummy.py b/paprika/build/dummy.py index 8a6f1c6..b27093e 100644 --- a/paprika/build/dummy.py +++ b/paprika/build/dummy.py @@ -1,6 +1,6 @@ import os as os -import parmed as pmd +import parmed from parmed.structure import Structure as ParmedStructureClass from paprika import utils @@ -62,7 +62,7 @@ def add_dummy( ) # Create an atom object - dum = pmd.topologyobjects.Atom() + dum = parmed.topologyobjects.Atom() dum.name = atom_name dum.mass = mass.to(openff_unit.dalton).magnitude dum.atomic_number = atomic_number diff --git a/paprika/build/system/tleap.py b/paprika/build/system/tleap.py index ac585dd..510ac44 100644 --- a/paprika/build/system/tleap.py +++ b/paprika/build/system/tleap.py @@ -1,10 +1,10 @@ -import logging as log -import os as os -import re as re -import subprocess as sp +import logging +import os +import re +import subprocess -import numpy as np -import parmed as pmd +import numpy +import parmed from paprika.build.system.utils import ( ANGSTROM_CUBED_TO_LITERS, @@ -13,6 +13,8 @@ PBCBox, ) +logger = logging.getLogger(__name__) + # TODO: refactor TLeap class, implement a build class for PSFGEN, PackMol and add TopoTools/VMD support @@ -405,7 +407,7 @@ def build(self, clean_files: bool = True): Whether to delete log files after completion. """ - log.debug("Running tleap.build() in {}".format(self.output_path)) + logger.debug("Running tleap.build() in {}".format(self.output_path)) # Check input if self.template_file and self.template_lines: @@ -463,7 +465,7 @@ def filter_template(self): elif re.search("combine", line): words = line.rstrip().replace("=", " ").split() self.unit = words[0] - log.debug( + logger.debug( f"Found `combine` keyword and reassigning `self.unit` to {self.unit}..." ) filtered_lines.append(line) @@ -610,10 +612,10 @@ def run(self): self.check_for_leap_log() file_name = self.output_prefix + ".tleap.in" - output = sp.Popen( + output = subprocess.Popen( ["tleap", "-s ", "-f ", file_name], - stdout=sp.PIPE, - stderr=sp.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=self.output_path, ) output = output.stdout.read().decode().splitlines() @@ -631,7 +633,7 @@ def grep_leap_log(self): if re.search( "ERROR|WARNING|Warning|duplicate|FATAL|Could|Fatal|Error", line ): - log.warning( + logger.warning( "It appears there was a problem with solvation: check `leap.log`..." ) except BaseException: @@ -649,7 +651,7 @@ def check_for_leap_log(self, log_file="leap.log"): log_file_path = os.path.join(self.output_path, log_file) try: os.remove(log_file_path) - log.debug("Deleted existing leap logfile: " + log_file_path) + logger.debug("Deleted existing leap logfile: " + log_file_path) except OSError: pass @@ -674,10 +676,10 @@ def solvate(self): # to the target_waters. This will control when we can start manually deleting # waters rather than adjusting the buffer_value. if self.manual_switch_thresh is None: - self.manual_switch_thresh = int(np.ceil(self.target_waters ** (1.0 / 3.0))) + self.manual_switch_thresh = int(numpy.ceil(self.target_waters ** (1.0 / 3.0))) if self.manual_switch_thresh < 12: self.manual_switch_thresh = 12 - log.debug( + logger.debug( "manual_switch_thresh is set to: {:.0f}".format( self.manual_switch_thresh ) @@ -703,7 +705,7 @@ def solvate(self): self.waters_added_history.append(waters) # noinspection PyTypeChecker self.buffer_value_history.append(self.buffer_value) - log.debug( + logger.debug( "Cycle {:02.0f} {:.0f} {:10.7f} {:6.0f} ({:6.0f})".format( cycle, self.exponent, self.buffer_value, waters, self.target_waters ) @@ -736,7 +738,7 @@ def solvate(self): cycle += 1 if cycle >= self.max_cycles and waters > self.target_waters: - log.debug( + logger.debug( "The added waters ({}) didn't reach the manual_switch_thresh ({}) with max_cycles ({}), " "but we'll try manual removal anyway.".format( waters, self.target_waters, self.max_cycles @@ -776,7 +778,7 @@ def final_solvation_run(self): raise Exception( "Unable to add the correct waters at 50 cycles during final_solvation_run()" ) - log.info( + logger.info( "The final solvation step added the wrong number of waters. Repeating ..." ) @@ -832,10 +834,10 @@ def count_residues(self, print_results=False): + " Investigate the leap.log for errors." ) - # log.debug(residues) + # logger.debug(residues) if print_results: for key, value in sorted(residues.items()): - log.info("{:10s} {:10.0f}".format(key, value)) + logger.info("{:10s} {:10.0f}".format(key, value)) return residues @@ -869,7 +871,7 @@ def set_additional_ions(self): # number to add = (molality) x (number waters) x (kg/mol # solvent) number_to_add = int( - np.ceil( + numpy.ceil( float(amount[:-1]) * self.target_waters * self.solvent_molecular_mass @@ -886,7 +888,7 @@ def set_additional_ions(self): ) number_of_atoms = float(amount[:-1]) * N_A liters = volume * ANGSTROM_CUBED_TO_LITERS - number_to_add = int(np.ceil(number_of_atoms * liters)) + number_to_add = int(numpy.ceil(number_of_atoms * liters)) self.add_ion_residues.append(number_to_add) else: raise Exception("Unanticipated error calculating how many ions to add.") @@ -909,7 +911,7 @@ def get_volume(self): match = re.search("Volume(.*)", line) volume = float(match.group(1)[1:-4]) return volume - log.warning("Could not determine total simulation volume.") + logger.warning("Could not determine total simulation volume.") return None def remove_waters_manually(self): @@ -932,13 +934,13 @@ def remove_waters_manually(self): if cycle > 5: additional_water = int(float(cycle) / 5.0) water_surplus += additional_water - log.debug( + logger.debug( "Detected trouble with manually removing water. Increasing the number" "of surplus waters by {}".format(additional_water) ) self.waters_to_remove = water_residues[-1 * water_surplus :] - log.debug("Manually removing waters... {}".format(self.waters_to_remove)) + logger.debug("Manually removing waters... {}".format(self.waters_to_remove)) # Get counts for all residues residues = self.count_residues() @@ -999,14 +1001,14 @@ def adjust_buffer_value(self): # If its been more than one round since last exponent change, # change exponent if self.cycles_since_last_exp_change > 1: - log.debug("Adjustment loop 1a") + logger.debug("Adjustment loop 1a") self.exponent -= 1 self.buffer_value = self.buffer_value_history[-1] + -5 * ( 10 ** self.exponent ) self.cycles_since_last_exp_change = 0 else: - log.debug("Adjustment loop 1b") + logger.debug("Adjustment loop 1b") self.buffer_value = self.buffer_value_history[-1] + -1 * ( 10 ** self.exponent ) @@ -1021,14 +1023,14 @@ def adjust_buffer_value(self): # If its been more than one round since last exponent change, # change exponent if self.cycles_since_last_exp_change > 1: - log.debug("Adjustment loop 2a") + logger.debug("Adjustment loop 2a") self.exponent -= 1 self.buffer_value = self.buffer_value_history[-1] + 5 * ( 10 ** self.exponent ) self.cycles_since_last_exp_change = 0 else: - log.debug("Adjustment loop 2b") + logger.debug("Adjustment loop 2b") self.buffer_value = self.buffer_value_history[-1] + 1 * ( 10 ** self.exponent ) @@ -1039,7 +1041,7 @@ def adjust_buffer_value(self): self.waters_added_history[-2] > self.target_waters and self.waters_added_history[-1] > self.target_waters ): - log.debug("Adjustment loop 3") + logger.debug("Adjustment loop 3") self.buffer_value = self.buffer_value_history[-1] + -1 * ( 10 ** self.exponent ) @@ -1050,7 +1052,7 @@ def adjust_buffer_value(self): self.waters_added_history[-2] < self.target_waters and self.waters_added_history[-1] < self.target_waters ): - log.debug("Adjustment loop 4") + logger.debug("Adjustment loop 4") self.buffer_value = self.buffer_value_history[-1] + 1 * ( 10 ** self.exponent ) @@ -1083,10 +1085,10 @@ def repartition_hydrogen_mass(self, options=None): prmtop = os.path.join(self.output_path, self.output_prefix + ".prmtop") - structure = pmd.load_file(prmtop, structure=True) + structure = parmed.load_file(prmtop, structure=True) # noinspection PyTypeChecker - pmd.tools.actions.HMassRepartition(structure, arg_list=options).execute() + parmed.tools.actions.HMassRepartition(structure, arg_list=options).execute() structure.save(prmtop, overwrite=True) @@ -1124,7 +1126,7 @@ def convert_to_gromacs( file_name = os.path.join(output_path, output_prefix) # Check if Amber Topology file(s) exist - topology = np.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) + topology = numpy.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) check_topology = [os.path.isfile(file) for file in topology] if not any(check_topology): raise FileNotFoundError("Cannot find any AMBER topology file.") @@ -1134,14 +1136,14 @@ def convert_to_gromacs( # Check if Amber Coordinate file(s) exist if toolkit == ConversionToolkit.ParmEd: - coordinates = np.array( + coordinates = numpy.array( [ f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd", "pdb"] ] ) else: # InterMol does not support PDB for loading Amber files - coordinates = np.array( + coordinates = numpy.array( [f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd"]] ) check_coordinates = [os.path.isfile(file) for file in coordinates] @@ -1158,7 +1160,7 @@ def convert_to_gromacs( # Convert with ParmEd if toolkit == ConversionToolkit.ParmEd: # Load Amber files - structure = pmd.load_file(prmtop, inpcrd, structure=True) + structure = parmed.load_file(prmtop, inpcrd, structure=True) if overwrite: structure.save(top_file, format="gromacs", overwrite=True) @@ -1167,12 +1169,12 @@ def convert_to_gromacs( if not os.path.isfile(top_file): structure.save(top_file, format="gromacs") else: - log.info(f"Topology file {top_file} exists, skipping writing file.") + logger.info(f"Topology file {top_file} exists, skipping writing file.") if not os.path.isfile(gro_file): structure.save(gro_file, format="gro") else: - log.info( + logger.info( f"Coordinates file {gro_file} exists, skipping writing file." ) @@ -1228,7 +1230,7 @@ def convert_to_charmm( file_name = os.path.join(output_path, output_prefix) # Check if Amber Topology file(s) exist - topology = np.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) + topology = numpy.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) check_topology = [os.path.isfile(file) for file in topology] if not any(check_topology): raise FileNotFoundError("Cannot find any AMBER topology file.") @@ -1238,14 +1240,14 @@ def convert_to_charmm( # Check if Amber Coordinate file(s) exist if toolkit == ConversionToolkit.ParmEd: - coordinates = np.array( + coordinates = numpy.array( [ f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd", "pdb"] ] ) else: # InterMol does not support PDB for loading Amber files - coordinates = np.array( + coordinates = numpy.array( [f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd"]] ) check_coordinates = [os.path.isfile(file) for file in coordinates] @@ -1262,7 +1264,7 @@ def convert_to_charmm( # Convert with ParmEd if toolkit == ConversionToolkit.ParmEd: # Load Amber files - structure = pmd.load_file(prmtop, inpcrd, structure=True) + structure = parmed.load_file(prmtop, inpcrd, structure=True) if overwrite: structure.save(psf_file, format="psf", overwrite=True) @@ -1271,12 +1273,12 @@ def convert_to_charmm( if not os.path.isfile(psf_file): structure.save(psf_file, format="psf") else: - log.info(f"Topology file {psf_file} exists, skipping writing file.") + logger.info(f"Topology file {psf_file} exists, skipping writing file.") if not os.path.isfile(crd_file): structure.save(crd_file) else: - log.info( + logger.info( f"Coordinates file {crd_file} exists, skipping writing file." ) @@ -1328,7 +1330,7 @@ def convert_to_lammps( file_name = os.path.join(output_path, output_prefix) # Check if Amber Topology file(s) exist - topology = np.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) + topology = numpy.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) check_topology = [os.path.isfile(file) for file in topology] if not any(check_topology): raise FileNotFoundError("Cannot find any AMBER topology file.") @@ -1337,7 +1339,7 @@ def convert_to_lammps( prmtop = topology[check_topology][0] # Check if Amber Coordinate file(s) exist - coordinates = np.array( + coordinates = numpy.array( [f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd"]] ) check_coordinates = [os.path.isfile(file) for file in coordinates] @@ -1397,7 +1399,7 @@ def convert_to_desmond( file_name = os.path.join(output_path, output_prefix) # Check if Amber Topology file(s) exist - topology = np.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) + topology = numpy.array([f"{file_name}.{ext}" for ext in ["prmtop", "parm7"]]) check_topology = [os.path.isfile(file) for file in topology] if not any(check_topology): raise FileNotFoundError("Cannot find any AMBER topology file.") @@ -1406,7 +1408,7 @@ def convert_to_desmond( prmtop = topology[check_topology][0] # Check if Amber Coordinate file(s) exist - coordinates = np.array( + coordinates = numpy.array( [f"{file_name}.{ext}" for ext in ["rst7", "inpcrd", "rst", "crd"]] ) check_coordinates = [os.path.isfile(file) for file in coordinates] diff --git a/paprika/io.py b/paprika/io.py index 3de0c83..e9771c0 100644 --- a/paprika/io.py +++ b/paprika/io.py @@ -4,8 +4,8 @@ import os import traceback -import numpy as np -import pytraj as pt +import numpy +import pytraj from openff.units import unit as openff_unit from parmed import Structure from parmed.amber import AmberParm @@ -42,11 +42,11 @@ def default(self, obj): logging.warning("Encountered Structure, which does not store filename.") return "" - if isinstance(obj, np.ndarray): + if isinstance(obj, numpy.ndarray): if obj.flags["C_CONTIGUOUS"]: obj_data = obj.data else: - cont_obj = np.ascontiguousarray(obj) + cont_obj = numpy.ascontiguousarray(obj) assert cont_obj.flags["C_CONTIGUOUS"] obj_data = cont_obj.data data_b64 = base64.b64encode(obj_data) @@ -59,23 +59,25 @@ def default(self, obj): elif isinstance( obj, ( - np.int_, - np.intc, - np.intp, - np.int8, - np.int16, - np.int32, - np.int64, - np.uint8, - np.uint16, - np.uint32, - np.uint64, + numpy.int_, + numpy.intc, + numpy.intp, + numpy.int8, + numpy.int16, + numpy.int32, + numpy.int64, + numpy.uint8, + numpy.uint16, + numpy.uint32, + numpy.uint64, ), ): return int(obj) - elif isinstance(obj, (np.float_, np.float16, np.float32, np.float64)): + elif isinstance( + obj, (numpy.float_, numpy.float16, numpy.float32, numpy.float64) + ): return float(obj) - elif isinstance(obj, (np.ndarray,)): + elif isinstance(obj, (numpy.ndarray,)): return obj.tolist() elif isinstance(obj, openff_unit.Quantity): return serialize_quantity(obj) @@ -99,7 +101,7 @@ def __init__(self, *args, **kwargs): def custom_object_hook(self, obj): if "__ndarray__" in obj: data = base64.b64decode(obj["__ndarray__"]) - return np.frombuffer(data, obj["dtype"]).reshape(obj["shape"]) + return numpy.frombuffer(data, obj["dtype"]).reshape(obj["shape"]) if "@type" in obj: return deserialize_quantity(obj) @@ -269,7 +271,7 @@ def load_trajectory(window, trajectory, topology, single_topology=False): ) logger.debug(f"Loading {os.path.join(window, topology)} and {trajectory_path}") try: - traj = pt.iterload(trajectory_path, os.path.join(window, topology)) + traj = pytraj.iterload(trajectory_path, os.path.join(window, topology)) except ValueError as e: formatted_exception = traceback.format_exception(None, e, e.__traceback__) logger.info( @@ -277,10 +279,10 @@ def load_trajectory(window, trajectory, topology, single_topology=False): f"{formatted_exception}" ) elif isinstance(topology, str) and single_topology: - traj = pt.iterload(trajectory_path, os.path.join(topology)) + traj = pytraj.iterload(trajectory_path, os.path.join(topology)) else: try: - traj = pt.iterload(trajectory_path, topology) + traj = pytraj.iterload(trajectory_path, topology) except BaseException: raise Exception("Tried to load `topology` object directly and failed.") @@ -310,7 +312,7 @@ def read_restraint_data( Returns ------- - data: :class:`np.array` + data: :class:`numpy.array` The values for this restraint in this window """ @@ -323,7 +325,7 @@ def read_restraint_data( and not restraint.mask4 ): data = openff_unit.Quantity( - pt.distance( + pytraj.distance( trajectory, " ".join([restraint.mask1, restraint.mask2]), image=True ), units=openff_unit.angstrom, @@ -333,7 +335,7 @@ def read_restraint_data( restraint.mask1 and restraint.mask2 and restraint.mask3 and not restraint.mask4 ): data = openff_unit.Quantity( - pt.angle( + pytraj.angle( trajectory, " ".join([restraint.mask1, restraint.mask2, restraint.mask3]), ), @@ -342,7 +344,7 @@ def read_restraint_data( elif restraint.mask1 and restraint.mask2 and restraint.mask3 and restraint.mask4: data = openff_unit.Quantity( - pt.dihedral( + pytraj.dihedral( trajectory, " ".join( [restraint.mask1, restraint.mask2, restraint.mask3, restraint.mask4] diff --git a/paprika/restraints/colvars.py b/paprika/restraints/colvars.py index 1753782..b752938 100644 --- a/paprika/restraints/colvars.py +++ b/paprika/restraints/colvars.py @@ -1,7 +1,7 @@ import logging import os -import numpy as np +import numpy from openff.units import unit as openff_unit from paprika.restraints.plumed import Plumed @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi class Colvars(Plumed): diff --git a/paprika/restraints/openmm.py b/paprika/restraints/openmm.py index 4aaeba3..e192d09 100644 --- a/paprika/restraints/openmm.py +++ b/paprika/restraints/openmm.py @@ -1,7 +1,7 @@ """A module aimed at applying restraints directly to OpenMM systems.""" import logging -import numpy as np +import numpy try: import openmm @@ -12,7 +12,7 @@ from typing import Optional, Union -import parmed as pmd +import parmed from openff.units import unit as openff_unit from openff.units.openmm import to_openmm @@ -20,7 +20,7 @@ from paprika.restraints.utils import get_bias_potential_type logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi def apply_positional_restraints( @@ -51,7 +51,7 @@ def apply_positional_restraints( """ # noinspection PyTypeChecker - structure: pmd.Structure = pmd.load_file(coordinate_path, structure=True) + structure: parmed.Structure = parmed.load_file(coordinate_path, structure=True) for atom in structure.atoms: if atom.name == atom_name: diff --git a/paprika/restraints/plumed.py b/paprika/restraints/plumed.py index aaf03b4..feef1b7 100644 --- a/paprika/restraints/plumed.py +++ b/paprika/restraints/plumed.py @@ -1,7 +1,7 @@ import logging import os -import numpy as np +import numpy from openff.units import unit as openff_unit from parmed.structure import Structure as ParmedStructureClass @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi _plumed_unit_dict = { openff_unit.kcal / openff_unit.mole: "kcal/mol", diff --git a/paprika/restraints/utils.py b/paprika/restraints/utils.py index b578e30..0880cd2 100644 --- a/paprika/restraints/utils.py +++ b/paprika/restraints/utils.py @@ -1,13 +1,13 @@ import logging -import numpy as np +import numpy from openff.units import unit as openff_unit from paprika.utils import override_dict logger = logging.getLogger(__name__) -_PI_ = np.pi +_PI_ = numpy.pi def parse_window(window): diff --git a/paprika/simulate/amber.py b/paprika/simulate/amber.py index 08ac89f..6faed27 100644 --- a/paprika/simulate/amber.py +++ b/paprika/simulate/amber.py @@ -1,7 +1,7 @@ import abc import logging import os -import subprocess as sp +import subprocess from collections import OrderedDict from enum import Enum @@ -633,11 +633,11 @@ def run(self, soft_minimize=False, overwrite=False, fail_ok=False): logger.debug("Exec line: " + " ".join(exec_list)) # Execute - amber_output = sp.Popen( + amber_output = subprocess.Popen( exec_list, cwd=self.path, - stdout=sp.PIPE, - stderr=sp.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=os.environ, ) diff --git a/paprika/simulate/gromacs.py b/paprika/simulate/gromacs.py index 09ca00b..2f66575 100644 --- a/paprika/simulate/gromacs.py +++ b/paprika/simulate/gromacs.py @@ -2,7 +2,7 @@ import glob import logging import os -import subprocess as sp +import subprocess from collections import OrderedDict from enum import Enum @@ -623,11 +623,11 @@ def run(self, run_grompp=True, overwrite=False, fail_ok=False): grompp_list += ["-n", self.index_file] # Run GROMPP - grompp_output = sp.Popen( + grompp_output = subprocess.Popen( grompp_list, cwd=self.path, - stdout=sp.PIPE, - stderr=sp.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=os.environ, ) grompp_stdout = grompp_output.stdout.read().splitlines() @@ -707,11 +707,11 @@ def run(self, run_grompp=True, overwrite=False, fail_ok=False): mdrun_list += ["-plumed", self.plumed_file] # Run MDRUN - mdrun_output = sp.Popen( + mdrun_output = subprocess.Popen( mdrun_list, cwd=self.path, - stdout=sp.PIPE, - stderr=sp.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=os.environ, ) mdrun_out = mdrun_output.stdout.read().splitlines() diff --git a/paprika/simulate/namd.py b/paprika/simulate/namd.py index 2f832c8..0327898 100644 --- a/paprika/simulate/namd.py +++ b/paprika/simulate/namd.py @@ -1,12 +1,12 @@ import abc import logging import os -import subprocess as sp +import subprocess from collections import OrderedDict from enum import Enum -import numpy as np -import parmed as pmd +import numpy +import parmed from paprika.utils import get_dict_without_keys @@ -584,14 +584,14 @@ def _get_cell_basis_vectors(self): """ Function to calculate the PBC cell basis vectors (needed when running a simulation for the first time). """ - structure = pmd.load_file( + structure = parmed.load_file( os.path.join(self.path, self.topology), os.path.join(self.path, self.coordinates), structure=True, ) coordinates = structure.coordinates - masses = np.ones(len(coordinates)) - center = pmd.geometry.center_of_mass(coordinates, masses) + masses = numpy.ones(len(coordinates)) + center = parmed.geometry.center_of_mass(coordinates, masses) self.cell_basis_vectors["cellOrigin"] = list(center) @@ -878,11 +878,11 @@ def run(self, overwrite=True, fail_ok=False): logger.debug("Exec line: " + " ".join(exec_list)) # Execute - namd_output = sp.Popen( + namd_output = subprocess.Popen( exec_list, cwd=self.path, stdout=open(os.path.join(self.path, self.logfile), "w"), - stderr=sp.PIPE, + stderr=subprocess.PIPE, env=os.environ, ) namd_stderr = namd_output.stderr.read().splitlines() diff --git a/paprika/tests/test_analysis.py b/paprika/tests/test_analysis.py index d4dbf9f..587ef8c 100644 --- a/paprika/tests/test_analysis.py +++ b/paprika/tests/test_analysis.py @@ -3,8 +3,8 @@ import shutil from copy import deepcopy -import numpy as np -import parmed as pmd +import numpy +import parmed import pytest from openff.units import unit as openff_unit from pytest import approx @@ -28,12 +28,12 @@ def clean_files(directory="tmp"): os.makedirs(directory) yield # This happens after the test function call - # shutil.rmtree(directory) + shutil.rmtree(directory) @pytest.fixture(scope="module", autouse=True) def setup_free_energy_calculation(): - input_pdb = pmd.load_file( + input_pdb = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") ) @@ -99,7 +99,6 @@ def setup_free_energy_calculation(): fecalc.trajectory = "*.nc" fecalc.path = os.path.join(os.path.dirname(__file__), "../data/cb6-but-apr/") fecalc.restraint_list = [rest1, rest2, rest3] - # fecalc.methods = ["ti-block", "mbar-block", "mbar-autoc", "mbar-boot"] fecalc.boot_cycles = 100 fecalc.ti_matrix = "diagonal" fecalc.compute_largest_neighbor = True @@ -112,7 +111,6 @@ def setup_free_energy_calculation(): fecalc.angle_unit = openff_unit.degrees fecalc.temperature_unit = openff_unit.kelvin fecalc.collect_data(single_topology=True) - # fecalc.compute_free_energy(seed=seed) fecalc.compute_ref_state_work([rest1, rest2, rest3, None, None, None]) return fecalc @@ -157,48 +155,36 @@ def test_setup(clean_files, setup_free_energy_calculation): def test_mbar_block(clean_files, setup_free_energy_calculation): method = "mbar-block" + # Estimate FE with `mbar-block` setup_free_energy_calculation.methods = [method] setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results - # Test mbar-block free energies and uncertainties + # Test `mbar-block` free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, results["attach"][method]["sem"].magnitude, results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - reference_values = [13.267731176, 0.16892084090, -2.1791430735, 0.93638948302] + reference_values = [13.2677, 0.1689, -2.1791, 0.9364] assert reference_values == approx(test_vals, abs=0.01) - # Test attach mbar-block largest_neighbor values + # Test attach `mbar-block` largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.0198918, 0.0451676, 0.0564517, 0.1079282, 0.1079282]) + reference_values = numpy.array([0.0199, 0.0452, 0.0565, 0.1079, 0.1079]) assert reference_values == approx(test_vals, abs=0.01) - # Test pull mbar-block largest_neighbor values + # Test pull `mbar-block` largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - reference_values = np.array( + reference_values = numpy.array( [ - 0.2053769, - 0.2053769, - 0.1617423, - 0.1747668, - 0.5255023, - 0.5255023, - 0.1149945, - 0.1707901, - 0.2129136, - 0.2129136, - 0.1942189, - 0.1768906, - 0.1997338, - 0.1997338, - 0.2014766, - 0.2014766, - 0.1470727, - 0.1442517, - 0.1434395, + # fmt: off + 0.20538, 0.20538, 0.16174, 0.17477, 0.52550, + 0.52550, 0.11499, 0.17079, 0.21291, 0.21291, + 0.19422, 0.17689, 0.19973, 0.19973, 0.20148, + 0.20148, 0.14707, 0.14425, 0.14344 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) @@ -211,45 +197,32 @@ def test_mbar_autoc(clean_files, setup_free_energy_calculation): setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results - # Test mbar-autoc free energies and uncertainties + # Test `mbar-autoc` free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, results["attach"][method]["sem"].magnitude, results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - reference_values = [13.267731176, 0.080830, -2.1791430735, 0.696944] + reference_values = [13.26773, 0.0808, -2.1791, 0.6969] assert reference_values == approx(test_vals, abs=0.01) - # Test attach mbar-autoc largest_neighbor values + # Test attach `mbar-autoc` largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.0198918, 0.0259152, 0.0336971, 0.0383718, 0.0383718]) + reference_values = numpy.array([0.0199, 0.0259, 0.0336, 0.0383, 0.0383]) assert reference_values == approx(test_vals, abs=0.01) - # Test pull mbar-autoc largest_neighbor values + # Test pull `mbar-autoc` largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - np.savetxt("tmp/test.txt", test_vals, fmt="%.5f") - reference_values = np.array( + numpy.savetxt("tmp/test.txt", test_vals, fmt="%.5f") + reference_values = numpy.array( [ - 0.10274, - 0.11361, - 0.13074, - 0.14136, - 0.38928, - 0.38928, - 0.11215, - 0.12951, - 0.13145, - 0.13145, - 0.13113, - 0.13113, - 0.13751, - 0.14186, - 0.14186, - 0.12847, - 0.13550, - 0.13550, - 0.13404, + # fmt: off + 0.10274, 0.11361, 0.13074, 0.14136, 0.38928, + 0.38928, 0.11215, 0.12951, 0.13145, 0.13145, + 0.13113, 0.13113, 0.13751, 0.14186, 0.14186, + 0.12847, 0.13550, 0.13550, 0.13404 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) @@ -258,6 +231,7 @@ def test_mbar_autoc(clean_files, setup_free_energy_calculation): def test_ti_block(clean_files, setup_free_energy_calculation): method = "ti-block" + # Estimate FE with exact sem setup_free_energy_calculation.methods = [method] setup_free_energy_calculation.exact_sem_each_ti_fraction = True setup_free_energy_calculation.compute_free_energy(seed=random_seed) @@ -270,46 +244,32 @@ def test_ti_block(clean_files, setup_free_energy_calculation): results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) - reference_values = np.array([13.31, 0.25, -1.62, 0.87]) + reference_values = numpy.array([13.31, 0.25, -1.62, 0.87]) assert reference_values == approx(test_vals, abs=0.01) # ROI only runs during TI. # Test attach ti-block largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.03, 0.07, 0.10, 0.18, 0.18]) + reference_values = numpy.array([0.03, 0.07, 0.10, 0.18, 0.18]) assert reference_values == approx(test_vals, abs=0.01) # Test pull ti-block largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - reference_values = np.array( + reference_values = numpy.array( [ - 0.33156402, - 0.33156402, - 0.2150947, - 0.2219127, - 0.2219127, - 0.10746089, - 0.13514015, - 0.15078472, - 0.15078472, - 0.15518025, - 0.10678047, - 0.10678047, - 0.10157904, - 0.14122943, - 0.16608568, - 0.16608568, - 0.14718857, - 0.14090383, - 0.11005729, + # fmt: off + 0.33156, 0.33156, 0.21509, 0.22191, 0.22191, + 0.10746, 0.13514, 0.15078, 0.15078, 0.15518, + 0.10678, 0.10678, 0.10158, 0.14123, 0.16609, + 0.16609, 0.14719, 0.14090, 0.11006 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) - # No-exact sem + # Estimate FE without exact sem setup_free_energy_calculation.exact_sem_each_ti_fraction = False setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results @@ -321,41 +281,27 @@ def test_ti_block(clean_files, setup_free_energy_calculation): results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) - reference_values = np.array([13.31, 0.25, -1.62, 0.87]) + reference_values = numpy.array([13.31, 0.25, -1.62, 0.87]) assert reference_values == approx(test_vals, abs=0.01) # ROI only runs during TI. # Test attach ti-block largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.03, 0.07, 0.10, 0.18, 0.18]) + reference_values = numpy.array([0.03, 0.07, 0.10, 0.18, 0.18]) assert reference_values == approx(test_vals, abs=0.01) # Test pull ti-block largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - reference_values = np.array( + reference_values = numpy.array( [ - 0.33156402, - 0.33156402, - 0.2150947, - 0.2219127, - 0.2219127, - 0.10746089, - 0.13514015, - 0.15078472, - 0.15078472, - 0.15518025, - 0.10678047, - 0.10678047, - 0.10157904, - 0.14122943, - 0.16608568, - 0.16608568, - 0.14718857, - 0.14090383, - 0.11005729, + # fmt: off + 0.33156, 0.33156, 0.21509, 0.22191, 0.22191, + 0.10746, 0.13514, 0.15078, 0.15078, 0.15518, + 0.10678, 0.10678, 0.10157, 0.14122, 0.16608, + 0.16608, 0.14718, 0.14090, 0.11005 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) @@ -364,52 +310,40 @@ def test_ti_block(clean_files, setup_free_energy_calculation): def test_ti_nocor(clean_files, setup_free_energy_calculation): method = "ti-nocor" + # Estimate FE with exact sem setup_free_energy_calculation.methods = [method] setup_free_energy_calculation.exact_sem_each_ti_fraction = True setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results - # Test ti-block free energies and uncertainties + # Test `ti-nocor` free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, results["attach"][method]["sem"].magnitude, results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - reference_values = np.array([13.34, 0.09, -1.71, 0.56]) + reference_values = numpy.array([13.34, 0.09, -1.71, 0.56]) assert reference_values == approx(test_vals, abs=0.01) # ROI only runs during TI. - # Test attach ti-nocor largest_neighbor values + # Test attach `ti-nocor` largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.014, 0.036, 0.055, 0.066, 0.066]) + reference_values = numpy.array([0.014, 0.036, 0.055, 0.066, 0.066]) assert reference_values == approx(test_vals, abs=0.01) - # Test pull ti-block largest_neighbor values + # Test pull `ti-nocor` largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - reference_values = np.array( + reference_values = numpy.array( [ - 0.09991857695378108, - 0.09991857695378108, - 0.08597658940932379, - 0.10866283883728353, - 0.10866283883728353, - 0.08704110657500748, - 0.10761246555307555, - 0.10761246555307555, - 0.11871918480320433, - 0.11871918480320433, - 0.10678047, - 0.10678047, - 0.10157904, - 0.09111759946004389, - 0.1034237670447086, - 0.1034237670447086, - 0.10319382363600771, - 0.10607790116613745, - 0.11005729, + # fmt: off + 0.09992, 0.09992, 0.08598, 0.10866, 0.10866, + 0.08704, 0.10761, 0.10761, 0.11872, 0.11872, + 0.10678, 0.10678, 0.10158, 0.09112, 0.10342, + 0.10342, 0.10319, 0.10608, 0.11005 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) @@ -419,48 +353,34 @@ def test_ti_nocor(clean_files, setup_free_energy_calculation): setup_free_energy_calculation.compute_free_energy(seed=random_seed) results = setup_free_energy_calculation.results - # Test ti-block free energies and uncertainties + # Test `ti-nocor` free energies and uncertainties test_vals = [ results["attach"][method]["fe"].magnitude, results["attach"][method]["sem"].magnitude, results["pull"][method]["fe"].magnitude, results["pull"][method]["sem"].magnitude, ] - # reference_values = np.array([13.35, 0.26, -1.85, 0.78]) - reference_values = np.array([13.34, 0.098, -1.71, 0.56]) + reference_values = numpy.array([13.34, 0.098, -1.71, 0.56]) assert reference_values == approx(test_vals, abs=0.01) # ROI only runs during TI. - # Test attach ti-block largest_neighbor values + # Test attach `ti-nocor` largest_neighbor values test_vals = results["attach"][method]["largest_neighbor"].magnitude - reference_values = np.array([0.0138, 0.0362, 0.055, 0.0657, 0.0657]) + reference_values = numpy.array([0.0138, 0.0362, 0.055, 0.0657, 0.0657]) assert reference_values == approx(test_vals, abs=0.01) - # Test pull ti-block largest_neighbor values + # Test pull `ti-nocor` largest_neighbor values test_vals = results["pull"][method]["largest_neighbor"].magnitude - reference_values = np.array( + reference_values = numpy.array( [ - 0.09991857695378108, - 0.09991857695378108, - 0.08597658940932379, - 0.10866283883728353, - 0.10866283883728353, - 0.08704110657500748, - 0.10761246555307555, - 0.10761246555307555, - 0.11871918480320433, - 0.11871918480320433, - 0.10678047, - 0.10678047, - 0.10157904, - 0.09111759946004389, - 0.1034237670447086, - 0.1034237670447086, - 0.10319382363600771, - 0.10607790116613745, - 0.11005729, + # fmt: off + 0.09992, 0.09992, 0.08598, 0.10867, 0.10866, + 0.08704, 0.10761, 0.10761, 0.11872, 0.11872, + 0.10678, 0.10678, 0.10158, 0.09112, 0.10342, + 0.10342, 0.10319, 0.10608, 0.11006 + # fmt: on ] ) assert reference_values == approx(test_vals, abs=0.01) @@ -468,11 +388,11 @@ def test_ti_nocor(clean_files, setup_free_energy_calculation): def test_reference_state_work(clean_files, setup_free_energy_calculation): results = setup_free_energy_calculation.results - assert np.isclose(-4.34372240, results["ref_state_work"].magnitude) + assert numpy.isclose(-4.34372, results["ref_state_work"].magnitude) def test_temperature(clean_files): - input_pdb = pmd.load_file( + input_pdb = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb") ) @@ -555,71 +475,69 @@ def test_bootstrap(): """Test the utility modules in `analysis`""" # Test regression statistics - x = np.linspace(0, 10, 11) - y = np.linspace(0, 10, 11) + x = numpy.linspace(0, 10, 11) + y = numpy.linspace(0, 10, 11) stats = analysis.summarize_statistics(x, y) - assert all(stats == np.array([1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0])) + assert all(stats == numpy.array([1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0])) # Test Regression bootstrap - x_sem = np.ones_like(x) - y_sem = np.ones_like(y) + x_sem = numpy.ones_like(x) + y_sem = numpy.ones_like(y) - np.random.seed(0) + numpy.random.seed(0) results = analysis.regression_bootstrap(x, x_sem, y, y_sem, cycles=1) + # fmt: off compare = { - "slope": 0.7703030693742714, - "intercept": 0.7777706430286471, - "R": 0.9184956779750857, - "R**2": 0.8436343104589124, - "RMSE": 1.637675285465762, - "MSE": -0.40918918963339473, - "MUE": 1.2814477376354574, - "Tau": 0.8545454545454545, + "slope": 0.77030, + "intercept": 0.77778, + "R": 0.91850, + "R**2": 0.84363, + "RMSE": 1.63768, + "MSE": -0.40919, + "MUE": 1.28145, + "Tau": 0.85454, } + # fmt: on for stat in results["mean"]: assert pytest.approx(results["mean"][stat], abs=1e-3) == compare[stat] # Test dG Bootstrap - np.random.seed(0) + numpy.random.seed(0) results = analysis.dG_bootstrap(-12, 2, -12, 2, cycles=1, with_uncertainty=True) - assert pytest.approx(results["mean"], abs=1e-3) == -11.205587976469898 + assert pytest.approx(results["mean"], abs=1e-3) == -11.20559 assert pytest.approx(results["sem"], abs=1e-3) == 0.0 - assert pytest.approx(results["ci"][0], abs=1e-3) == -11.20558798 - assert pytest.approx(results["ci"][1], abs=1e-3) == -11.20558798 + assert pytest.approx(results["ci"][0], abs=1e-3) == -11.20559 + assert pytest.approx(results["ci"][1], abs=1e-3) == -11.20559 results = analysis.dG_bootstrap(-12, 2, -12, 2, cycles=1, with_uncertainty=False) - assert pytest.approx(results["mean"], abs=1e-3) == -0.4106792724182964 + assert pytest.approx(results["mean"], abs=1e-3) == -0.41068 assert pytest.approx(results["sem"], abs=1e-3) == 0.0 - assert pytest.approx(results["ci"][0], abs=1e-3) == -0.41067927 - assert pytest.approx(results["ci"][1], abs=1e-3) == -0.41067927 + assert pytest.approx(results["ci"][0], abs=1e-3) == -0.41068 + assert pytest.approx(results["ci"][1], abs=1e-3) == -0.41068 # Test dH Bootstrap - np.random.seed(0) + numpy.random.seed(0) + # fmt: off results = analysis.dH_bootstrap( - -15, - 2, - -15, - 2, - -10, - 2, - -10, - 2, + -15, 2, -15, 2, -10, 2, -10, 2, cycles=1, with_uncertainty=True, ) - assert pytest.approx(results["mean"], abs=1e-3) == -11.50986102184404 + # fmt: on + assert pytest.approx(results["mean"], abs=1e-3) == -11.50986 assert pytest.approx(results["sem"], abs=1e-3) == 0.0 - assert pytest.approx(results["ci"][0], abs=1e-3) == -11.5098610 - assert pytest.approx(results["ci"][1], abs=1e-3) == -11.5098610 + assert pytest.approx(results["ci"][0], abs=1e-3) == -11.50986 + assert pytest.approx(results["ci"][1], abs=1e-3) == -11.50986 def test_utils(): """Test the utility modules in `analysis`""" assert utils.get_factors(10) == [1, 2, 5, 10] assert utils.get_nearest_max(100) == 90 - np.random.seed(0) - results = utils.get_block_sem(np.random.normal(10.0, 2.0, 100)) - assert pytest.approx(results, abs=1e-3) == 0.3916368835724714 + + numpy.random.seed(0) + results = utils.get_block_sem(numpy.random.normal(10.0, 2.0, 100)) + assert pytest.approx(results, abs=1e-3) == 0.39164 assert utils.get_subsampled_indices(10, 2.0) == [0, 2, 4, 6, 8] diff --git a/paprika/utils.py b/paprika/utils.py index 0af45dc..0a381e3 100644 --- a/paprika/utils.py +++ b/paprika/utils.py @@ -1,13 +1,13 @@ import logging -import os as os +import os import re import shutil from datetime import datetime from functools import lru_cache -import numpy as np -import parmed as pmd -import pytraj as pt +import numpy +import parmed +import pytraj from openff.units import unit as openff_unit from parmed.structure import Structure as ParmedStructureClass @@ -103,7 +103,7 @@ def return_parmed_structure(filename): # `parmed` can read both PDBs and # .inpcrd/.prmtop files with the same function call. try: - structure = pmd.load_file(filename) + structure = parmed.load_file(filename) logger.info("Loaded {}...".format(filename)) except IOError: logger.error("Unable to load file: {}".format(filename)) @@ -145,7 +145,8 @@ def index_from_mask(structure, mask, amber_index=False): ) # http://parmed.github.io/ParmEd/html/api/parmed/parmed.amber.mask.html?highlight=mask#module-parmed.amber.mask indices = [ - i + index_offset for i in pmd.amber.mask.AmberMask(structure, mask).Selected() + i + index_offset + for i in parmed.amber.mask.AmberMask(structure, mask).Selected() ] logger.debug("There are {} atoms in the mask {} ...".format(len(indices), mask)) return indices @@ -200,8 +201,8 @@ def strip_prmtop(prmtop, mask=":WAT,:Na+,:Cl-"): """ - structure = pt.load_topology(os.path.normpath(prmtop)) - stripped = pt.strip(mask, structure) + structure = pytraj.load_topology(os.path.normpath(prmtop)) + stripped = pytraj.strip(mask, structure) # stripped_name = os.path.join(os.path.splitext(prmtop)[0], '-stripped', os.path.splitext(prmtop)[1]) # stripped.save(filename=stripped_name) # logger.debug('Stripping {} from parameter file and writing {}...'.format(mask, stripped_name)) @@ -346,7 +347,7 @@ def check_unit(variable, base_unit): raise KeyError( "Please make my life easier by either specifying a list of all float or all openff.unit.Quantity." ) - elif isinstance(variable, np.ndarray): + elif isinstance(variable, numpy.ndarray): quantity = openff_unit.Quantity(variable, units=base_unit) else: raise KeyError("``variable`` should be a float or openff.unit.Quantity.") From 3bfb5e77659590efcdef0b2263c90227a2ae5cfc Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 1 May 2023 09:53:57 -0700 Subject: [PATCH 12/34] update unit import --- paprika/analysis/bootstrap.py | 62 ++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/paprika/analysis/bootstrap.py b/paprika/analysis/bootstrap.py index 637dc55..584b124 100644 --- a/paprika/analysis/bootstrap.py +++ b/paprika/analysis/bootstrap.py @@ -2,7 +2,7 @@ import numpy import openmm.unit as openmm_unit -from openff.units import unit +from openff.units import unit as openff_unit from openff.units.openmm import from_openmm from scipy import stats as statistics from scipy.interpolate import Akima1DInterpolator @@ -239,9 +239,9 @@ def dG_bootstrap( y_data: Union[List, numpy.array], y_sem: Union[List, numpy.array], cycles: int = 1000, - temperature: Union[float, unit.Quantity] = 298.15 * unit.kelvin, + temperature: Union[float, openff_unit.Quantity] = 298.15 * openff_unit.kelvin, with_uncertainty: bool = True, - energy_units: unit.Quantity = None, + energy_units: openff_unit.Quantity = None, ): """ Combine dG when for multiple binding poses. This is from Eq(A12) and Eq(A13) from @@ -263,7 +263,7 @@ def dG_bootstrap( Temperature of the simulation with_uncertainty: bool If true, generate samples from normal distribution based on mean=data, mu=sem - energy_units: unit.Quantity + energy_units: openff.units.unit.Quantity If true, return values as openff.units.unit.Quantity. Default is kcal/mol. Return @@ -272,14 +272,14 @@ def dG_bootstrap( Returns the mean, standard deviation and confidence interval from bootstrapping. """ - x_data = check_unit(x_data, base_unit=unit.kilocalorie_per_mole).magnitude - x_sem = check_unit(x_sem, base_unit=unit.kilocalorie_per_mole).magnitude - y_data = check_unit(y_data, base_unit=unit.kilocalorie_per_mole).magnitude - y_sem = check_unit(y_sem, base_unit=unit.kilocalorie_per_mole).magnitude - temperature = check_unit(temperature, base_unit=unit.kelvin) + x_data = check_unit(x_data, base_unit=openff_unit.kilocalorie_per_mole).magnitude + x_sem = check_unit(x_sem, base_unit=openff_unit.kilocalorie_per_mole).magnitude + y_data = check_unit(y_data, base_unit=openff_unit.kilocalorie_per_mole).magnitude + y_sem = check_unit(y_sem, base_unit=openff_unit.kilocalorie_per_mole).magnitude + temperature = check_unit(temperature, base_unit=openff_unit.kelvin) summary_statistics = numpy.empty((cycles)) - RT = (R_gas * temperature).to(unit.kilocalorie_per_mole).magnitude + RT = (R_gas * temperature).to(openff_unit.kilocalorie_per_mole).magnitude beta = 1.0 / RT ci = numpy.empty((2)) @@ -327,9 +327,9 @@ def dH_bootstrap( dG_y_data, dG_y_sem, cycles=1000, - temperature=298.15 * unit.kelvin, + temperature=298.15 * openff_unit.kelvin, with_uncertainty=True, - energy_units: unit.Quantity = None, + energy_units: openff_unit.Quantity = None, ): """ Combine dH when for multiple binding poses. This is from Eq(A16) and Eq(A17) from @@ -359,7 +359,7 @@ def dH_bootstrap( Temperature of the simulation with_uncertainty: bool If true, generate samples from normal distribution based on mean=data, mu=sem - energy_units: unit.Quantity + energy_units: openff.units.unit.Quantity If true, return values as openff.units.unit.Quantity. Default is kcal/mol. Return @@ -368,18 +368,34 @@ def dH_bootstrap( Returns the mean, standard deviation and confidence interval from bootstrapping. """ - dH_x_data = check_unit(dH_x_data, base_unit=unit.kilocalorie_per_mole).magnitude - dH_x_sem = check_unit(dH_x_sem, base_unit=unit.kilocalorie_per_mole).magnitude - dH_y_data = check_unit(dH_y_data, base_unit=unit.kilocalorie_per_mole).magnitude - dH_y_sem = check_unit(dH_y_sem, base_unit=unit.kilocalorie_per_mole).magnitude - dG_x_data = check_unit(dG_x_data, base_unit=unit.kilocalorie_per_mole).magnitude - dG_x_sem = check_unit(dG_x_sem, base_unit=unit.kilocalorie_per_mole).magnitude - dG_y_data = check_unit(dG_y_data, base_unit=unit.kilocalorie_per_mole).magnitude - dG_y_sem = check_unit(dG_y_sem, base_unit=unit.kilocalorie_per_mole).magnitude - temperature = check_unit(temperature, base_unit=unit.kelvin) + dH_x_data = check_unit( + dH_x_data, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dH_x_sem = check_unit( + dH_x_sem, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dH_y_data = check_unit( + dH_y_data, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dH_y_sem = check_unit( + dH_y_sem, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dG_x_data = check_unit( + dG_x_data, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dG_x_sem = check_unit( + dG_x_sem, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dG_y_data = check_unit( + dG_y_data, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + dG_y_sem = check_unit( + dG_y_sem, base_unit=openff_unit.kilocalorie_per_mole + ).magnitude + temperature = check_unit(temperature, base_unit=openff_unit.kelvin) summary_statistics = numpy.empty((cycles)) - RT = (R_gas * temperature).to(unit.kcal / unit.mole).magnitude + RT = (R_gas * temperature).to(openff_unit.kcal / openff_unit.mole).magnitude beta = 1.0 / RT ci = numpy.empty((2)) From 1b0e5363fc71d51c9544ee5811888eaca433b110 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 26 May 2023 10:30:20 -0700 Subject: [PATCH 13/34] update align modules --- paprika/build/align.py | 178 ++++++++++++++++++++---------------- paprika/tests/test_align.py | 79 +++++++++------- 2 files changed, 147 insertions(+), 110 deletions(-) diff --git a/paprika/build/align.py b/paprika/build/align.py index 981a6da..c3c456b 100644 --- a/paprika/build/align.py +++ b/paprika/build/align.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): +def zalign(structure, mask1, mask2, axis="z", weight="mass", save=False, filename=None): """Aligns the vector formed by atom mask1--mask2 to a reference axis. Parameters @@ -22,6 +22,8 @@ def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): axis : str or list or :class:`numpy.ndarray`, optional, default='z' Cartesian axis as a string: `x`, `y`, and `z`, or an arbitrary vector as a list. If an array is specified, the axis-vector will be normalized automatically. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). save : bool, optional, default=False Whether to save the coordinates (the default is False, which does nothing). filename : str, optional, default=None @@ -44,39 +46,27 @@ def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): # Check axis axis = _return_array(axis) - # Mask1 - mask1_coordinates = structure[mask1].coordinates - mask1_masses = [atom.mass for atom in structure[mask1].atoms] - mask1_com = parmed.geometry.center_of_mass( - numpy.asarray(mask1_coordinates), numpy.asarray(mask1_masses) - ) - - # Mask2 - mask2_coordinates = structure[mask2].coordinates - mask2_masses = [atom.mass for atom in structure[mask2].atoms] - mask2_com = parmed.geometry.center_of_mass( - numpy.asarray(mask2_coordinates), numpy.asarray(mask2_masses) - ) + # Centroids + mask1_centroid = get_centroid(structure, atom_mask=mask1, weight=weight) + mask2_centroid = get_centroid(structure, atom_mask=mask2, weight=weight) logger.info( - "Moving {} ({} atoms) to the origin...".format(mask1, len(mask1_coordinates)) + f"Moving {mask1} ({len(structure[mask1].atoms)} atoms) to the origin..." ) logger.info( - "Aligning {} ({} atoms) with the z axis...".format( - mask2, len(mask2_coordinates) - ) + f"Aligning {mask2} ({len(structure[mask2].atoms)} atoms) with the z axis..." ) # Define the vector from mask1 to mask2. - mask2_com = mask2_com + -1.0 * mask1_com + vector = mask2_centroid + -1.0 * mask1_centroid # Rotation matrix - rotation_matrix = get_rotation_matrix(mask2_com, axis) + rotation_matrix = get_rotation_matrix(vector, axis) # This is certainly not the fastest approach, but it is explicit. aligned_coords = numpy.empty_like(structure.coordinates) for atom in range(len(structure.atoms)): - aligned_coords[atom] = structure.coordinates[atom] + -1.0 * mask1_com + aligned_coords[atom] = structure.coordinates[atom] + -1.0 * mask1_centroid aligned_coords[atom] = numpy.dot(rotation_matrix, aligned_coords[atom]) structure.coordinates = aligned_coords @@ -86,15 +76,17 @@ def zalign(structure, mask1, mask2, axis="z", save=False, filename=None): "Unable to save aligned coordinates (no filename provided)..." ) else: - logger.info("Saved aligned coordinates to {}".format(filename)) + logger.info(f"Saved aligned coordinates to {filename}") # This seems to write out HETATM in place of ATOM # We should offer the option of writing a mol2 file, directly. - structure.write_pdb(filename) + structure.save(filename, overwrite=True) return structure -def align_principal_axes(structure, atom_mask=None, principal_axis=1, axis="z"): +def align_principal_axes( + structure, atom_mask=None, principal_axis=1, axis="z", weight="mass" +): """Aligns a chosen principal axis of a system to a user specified axis. This function is based on the method given in the link: https://www.ks.uiuc.edu/Research/vmd/script_library/scripts/orient/ @@ -104,27 +96,29 @@ def align_principal_axes(structure, atom_mask=None, principal_axis=1, axis="z"): Molecular structure containing coordinates. atom_mask : str, optional, default=None A mask that filter specific atoms for calculating the moment of inertia. This is useful if the molecule is - not radially symmetric and you may want to select a subset of atoms that is symmetric for alignment. + not radially symmetric, and you may want to select a subset of atoms that is symmetric for alignment. principal_axis : int, optional, default=1 The particular principal axis to align to (The choices are `1`, `2` or `3` with `1` being the principal axis with the largest eigenvalue and `3` the lowest.). axis: str or list or :class:`numpy.ndarray`, optional, default='z' The axis vector to align the system to (by default the function aligns the principal axes with the largest eigenvalue to the z-axis). If an array is specified, the axis-vector will be normalized automatically. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). Returns ------- structure : :class:`parmed.Structure` - A molecular structure with it's principal axis aligned to a vector. + A molecular structure with its principal axis aligned to a vector. Examples -------- The commands below mimics the example given in the link above in VMD. - >>> # Align largest principal axes to the z-axis + >>> # Aligns the largest principal axes to the z-axis >>> structure = align_principal_axes(structure, principal_axis=1, axis='z') >>> - >>> # Align second largest principal axes to the y-axis + >>> # Aligns the second-largest principal axes to the y-axis >>> structure = align_principal_axes(structure, principal_axis=2, axis='y') It is also possible to use only a subset of atoms to determine the principal axes @@ -144,7 +138,9 @@ def align_principal_axes(structure, atom_mask=None, principal_axis=1, axis="z"): axis = _return_array(axis) # Principal axis vector - p_axis = get_principal_axis_vector(structure, principal_axis, atom_mask) + p_axis = get_principal_axis_vector( + structure, principal_axis, atom_mask=atom_mask, weight=weight + ) # Calculate Rotation matrix rotation_matrix = get_rotation_matrix(p_axis, axis) @@ -169,13 +165,13 @@ def rotate_around_axis(structure, axis, angle): Molecular structure containing coordinates. axis: str or list or :class:`numpy.ndarray` The axis of rotation. If an array is specified, the axis-vector will be normalized automatically. - angle: float or pint.unit.Quantity + angle: float or openff.units.unit.Quantity The angle of rotation in degrees. Returns ------- structure : :class:`parmed.Structure` - A molecular structure with it's principal axis aligned to a vector. + A molecular structure with its principal axis aligned to a vector. Examples -------- @@ -190,7 +186,7 @@ def rotate_around_axis(structure, axis, angle): axis = _return_array(axis) # Convert angle to radians (temporary until Pint integration) - angle = check_unit(angle, base_unit=openff_unit.radians).to(openff_unit.radians).magnitude + angle = check_unit(angle, base_unit=openff_unit.radians).m_as(openff_unit.radians) if numpy.array_equal(axis, numpy.array([1.0, 0.0, 0.0])): rotation_matrix = numpy.array( @@ -227,19 +223,19 @@ def rotate_around_axis(structure, axis, angle): rotation_matrix = numpy.array( [ [ - numpy.cos(angle) + u_x ** 2 * (1 - numpy.cos(angle)), + numpy.cos(angle) + u_x**2 * (1 - numpy.cos(angle)), u_x * u_y * (1 - numpy.cos(angle)) - u_z * numpy.sin(angle), u_x * u_z * (1 - numpy.cos(angle)) + u_y * numpy.sin(angle), ], [ u_y * u_x * (1 - numpy.cos(angle)) + u_z * numpy.sin(angle), - numpy.cos(angle) + u_y ** 2 * (1 - numpy.cos(angle)), + numpy.cos(angle) + u_y**2 * (1 - numpy.cos(angle)), u_y * u_z * (1 - numpy.cos(angle)) - u_x * numpy.sin(angle), ], [ u_z * u_x * (1 - numpy.cos(angle)) - u_y * numpy.sin(angle), u_z * u_y * (1 - numpy.cos(angle)) + u_x * numpy.sin(angle), - numpy.cos(angle) + u_z ** 2 * (1 - numpy.cos(angle)), + numpy.cos(angle) + u_z**2 * (1 - numpy.cos(angle)), ], ] ) @@ -254,7 +250,7 @@ def rotate_around_axis(structure, axis, angle): return structure -def get_theta(structure, mask1, mask2, axis): +def get_theta(structure, mask1, mask2, axis, weight="mass"): """Get the angle (theta) between the vector formed by atom mask1--mask2 and a Cartesian axis. Parameters @@ -268,6 +264,8 @@ def get_theta(structure, mask1, mask2, axis): axis : str or list or :class:`numpy.ndarray` Cartesian axis as a string: `x`, `y`, and `z`, or an arbitrary vector as a list. If an array is specified, the axis vector need not be normalized. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). Returns ------- @@ -278,22 +276,12 @@ def get_theta(structure, mask1, mask2, axis): # Check axis axis = _return_array(axis) - # Centroid of mask1 - mask1_coordinates = structure[mask1].coordinates - mask1_masses = [atom.mass for atom in structure[mask1].atoms] - mask1_com = parmed.geometry.center_of_mass( - numpy.asarray(mask1_coordinates), numpy.asarray(mask1_masses) - ) - - # Centroid of mask2 - mask2_coordinates = structure[mask2].coordinates - mask2_masses = [atom.mass for atom in structure[mask2].atoms] - mask2_com = parmed.geometry.center_of_mass( - numpy.asarray(mask2_coordinates), numpy.asarray(mask2_masses) - ) + # Centroids + mask1_centroid = get_centroid(structure, mask1, weight=weight) + mask2_centroid = get_centroid(structure, mask2, weight=weight) # Vector mask1-mask2 - vector = mask2_com + -1.0 * mask1_com + vector = mask2_centroid + -1.0 * mask1_centroid # Angle between vectors theta = numpy.arccos( @@ -322,11 +310,15 @@ def get_rotation_matrix(vector, ref_vector): # If the structures are already aligned (cross product is zero), return 3x3 identity matrix if numpy.linalg.norm(numpy.cross(vector, ref_vector)) == 0: - logger.info("The structure is already aligned and the denominator is invalid, returning identity matrix.") + logger.info( + "The structure is already aligned and the denominator is invalid, returning identity matrix." + ) return numpy.identity(3) # Find axis between the mask vector and the axis using cross and dot products. - x = numpy.cross(vector, ref_vector) / numpy.linalg.norm(numpy.cross(vector, ref_vector)) + x = numpy.cross(vector, ref_vector) / numpy.linalg.norm( + numpy.cross(vector, ref_vector) + ) theta = numpy.arccos( numpy.dot(vector, ref_vector) @@ -348,7 +340,9 @@ def get_rotation_matrix(vector, ref_vector): return rotation_matrix -def get_principal_axis_vector(structure, principal_axis=1, atom_mask=None): +def get_principal_axis_vector( + structure, principal_axis=1, atom_mask=None, weight="mass" +): """Return the principal axis vector given a structure. Parameters @@ -360,22 +354,20 @@ def get_principal_axis_vector(structure, principal_axis=1, atom_mask=None): principal axis with the largest eigenvalue and `3` the lowest). atom_mask: str, optional, default=None A mask that filter specific atoms for calculating the moment of inertia. This is useful if the molecule is - not radially symmetric and you may want to select a subset of atoms that is symmetric for alignment. + not radially symmetric, and you may want to select a subset of atoms that is symmetric for alignment. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). Returns ------- p_axis: :class:`numpy.ndarray` Principal axis vector. """ - # Get coordinates and masses - coordinates = structure.coordinates - masses = numpy.asarray([atom.mass for atom in structure.atoms]) - if atom_mask: - coordinates = structure[atom_mask].coordinates - masses = numpy.asarray([atom.mass for atom in structure[atom_mask].atoms]) # Calculate center of mass - centroid = parmed.geometry.center_of_mass(coordinates, masses) + centroid, coordinates, masses = get_centroid( + structure, atom_mask=atom_mask, weight=weight, return_xyz=True, return_mass=True + ) # Construct Inertia tensor Ixx = Ixy = Ixz = Iyy = Iyz = Izz = 0 @@ -400,15 +392,23 @@ def get_principal_axis_vector(structure, principal_axis=1, atom_mask=None): return p_axis -def check_coordinates(structure, mask): +def get_centroid( + structure, atom_mask=None, weight="mass", return_xyz=False, return_mass=False +): """Return the coordinates of an atom selection. Parameters ---------- structure : :class:`parmed.Structure` Molecular structure containing coordinates. - mask : str - Amber-style atom selection. + atom_mask : str, optional, default=None + Selection of atom(s) if a particular subset is preferred to estimate the centroid. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). + return_xyz: bool, optional, default=False + Option to return coordinates + return_mass: bool, optional, default=False + Option to return masses Returns ------- @@ -416,23 +416,47 @@ def check_coordinates(structure, mask): Coordinates of the selection center of mass. """ + # Check if weight variable is properly chosen + if weight not in ["mass", "geo"]: + raise ValueError("`weight` must either be `mass` or `geo`.") - mask_coordinates = structure[mask].coordinates - mask_masses = [atom.mass for atom in structure[mask].atoms] - mask_com = parmed.geometry.center_of_mass( - numpy.asarray(mask_coordinates), numpy.asarray(mask_masses) - ) - return mask_com + # Coordinates + if atom_mask is None: + coordinates = numpy.asarray(structure.coordinates) + else: + coordinates = numpy.asarray(structure[atom_mask].coordinates) + + # Mass or weights + masses = numpy.ones(len(coordinates)) + if weight == "mass": + if atom_mask is None: + masses = numpy.asarray([atom.mass for atom in structure.atoms]) + else: + masses = numpy.asarray([atom.mass for atom in structure[atom_mask].atoms]) + + if all(masses == 0.0): + masses[:] = 1.0 + + centroid = parmed.geometry.center_of_mass(coordinates, masses) + + if return_xyz and return_mass: + return centroid, coordinates, masses + elif return_xyz and not return_mass: + return centroid, coordinates, masses + elif not return_xyz and return_mass: + return centroid, masses + + return centroid -def offset_structure(structure, offset, dimension=None): +def shift_structure(structure, offset, dimension=None): """Return a structure whose coordinates have been shifted by ``offset``. Parameters ---------- structure : :class:`parmed.Structure` Molecular structure containing coordinates. - offset : float or :class:`numpy.ndarray` or pint.unit.Quantity + offset : float or :class:`numpy.ndarray` or openff.units.unit.Quantity The offset that will be added to *every* atom in the structure. dimension : str or list or :class:`numpy.ndarray`, optional, default=None By default the structure will be moved by ``offset`` in all direction if it is a single number, i.e. ``xyz + @@ -446,14 +470,14 @@ def offset_structure(structure, offset, dimension=None): Examples -------- >>> # Offset a structure in the y-axis by 5 Angstrom - >>> structure = offset_structure(structure, 5.0, dimension='y') + >>> structure = shift_structure(structure, 5.0, dimension='y') >>> >>> # Offset a structure in the x- and z-axis by 3 Angstrom - >>> structure = offset_structure(structure, 3.0, dimension='z') + >>> structure = shift_structure(structure, 3.0, dimension='z') >>> >>> # Offset a structure using a numpy array >>> offset = numpy.array([0.0, 5.0, 2.0]) - >>> structure = offset_structure(structure, offset) + >>> structure = shift_structure(structure, offset) """ # Dimension mask @@ -471,7 +495,7 @@ def offset_structure(structure, offset, dimension=None): for atom in range(len(structure.atoms)): offset_coords[atom] = structure.coordinates[atom] + offset structure.coordinates = offset_coords - logger.info("Added offset of {} to atomic coordinates...".format(offset)) + logger.info(f"Added offset of {offset} to atomic coordinates...") return structure @@ -509,7 +533,7 @@ def translate_to_origin(structure, weight="mass", atom_mask=None, dimension=None """ # Check if weight variable is properly chosen if weight not in ["mass", "geo"]: - raise ValueError('"weight" must either be "mass" or "geo".') + raise ValueError("`weight` must either be `mass` or `geo`.") # Dimension mask if dimension is None: diff --git a/paprika/tests/test_align.py b/paprika/tests/test_align.py index e25f55f..a48e9d3 100644 --- a/paprika/tests/test_align.py +++ b/paprika/tests/test_align.py @@ -4,17 +4,17 @@ import os -import numpy as np -import parmed as pmd +import numpy +import parmed import pytest from openff.units import unit as openff_unit from paprika.build.align import ( align_principal_axes, - check_coordinates, + get_centroid, get_principal_axis_vector, get_theta, - offset_structure, + shift_structure, rotate_around_axis, translate_to_origin, zalign, @@ -23,29 +23,37 @@ def test_center_mask(): """Test that the first mask is centered.""" - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") ) - aligned_cb6 = zalign(cb6, ":CB6", ":BUT") - test_coordinates = check_coordinates(aligned_cb6, ":CB6") - assert np.allclose(test_coordinates, np.zeros(3)) + aligned_cb6 = zalign(cb6, ":CB6", ":BUT", weight="mass") + test_coordinates = get_centroid(aligned_cb6, ":CB6", weight="mass") + assert numpy.allclose(test_coordinates, numpy.zeros(3)) + test_coordinates = get_centroid(aligned_cb6, ":CB6", weight="geo") + assert numpy.allclose( + test_coordinates, numpy.array([-0.0002163, 0.00113288, -0.00072443]) + ) def test_alignment_after_offset(): """Test that molecule is properly aligned after random offset.""" - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") ) - random_coordinates = np.random.randint(10) * np.random.rand(1, 3) - cb6_offset = offset_structure(cb6, random_coordinates) + random_coordinates = numpy.random.randint(10) * numpy.random.rand(1, 3) + cb6_offset = shift_structure(cb6, random_coordinates) aligned_cb6 = zalign(cb6_offset, ":CB6", ":BUT") - test_coordinates = check_coordinates(aligned_cb6, ":CB6") - assert np.allclose(test_coordinates, np.zeros(3)) + test_coordinates = get_centroid(aligned_cb6, ":CB6", weight="mass") + assert numpy.allclose(test_coordinates, numpy.zeros(3)) + test_coordinates = get_centroid(aligned_cb6, ":CB6", weight="geo") + assert numpy.allclose( + test_coordinates, numpy.array([-0.0002163, 0.00113288, -0.00072443]) + ) def test_theta_after_alignment(): """Test that molecule is properly aligned after random offset.""" - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb") ) aligned_cb6 = zalign(cb6, ":CB6", ":BUT") @@ -77,7 +85,7 @@ def test_theta_after_alignment(): def test_translate_to_origin(): """Test that molecule is properly aligned after translated to the origin.""" - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/vac.pdb"), structure=True, ) @@ -85,30 +93,30 @@ def test_translate_to_origin(): # Translate molecule to origin translated_cb6 = translate_to_origin(cb6) coordinates = translated_cb6.coordinates - masses = np.asarray([atom.mass for atom in translated_cb6.atoms]) - centroid = pmd.geometry.center_of_mass(coordinates, masses) + masses = numpy.asarray([atom.mass for atom in translated_cb6.atoms]) + centroid = parmed.geometry.center_of_mass(coordinates, masses) assert pytest.approx(centroid[0], abs=1e-3) == 0.0 assert pytest.approx(centroid[1], abs=1e-3) == 0.0 assert pytest.approx(centroid[2], abs=1e-3) == 0.0 # Shift then translate only in the z-axis - cb6_offset = offset_structure(cb6, np.array([3, 5, 10])) + cb6_offset = shift_structure(cb6, numpy.array([3, 5, 10])) translated_cb6 = translate_to_origin(cb6_offset, dimension="z") coordinates = translated_cb6.coordinates - masses = np.asarray([atom.mass for atom in translated_cb6.atoms]) - centroid = pmd.geometry.center_of_mass(coordinates, masses) + masses = numpy.asarray([atom.mass for atom in translated_cb6.atoms]) + centroid = parmed.geometry.center_of_mass(coordinates, masses) assert pytest.approx(centroid[0], abs=1e-3) != 0.0 assert pytest.approx(centroid[1], abs=1e-3) != 0.0 assert pytest.approx(centroid[2], abs=1e-3) == 0.0 # Randomly shift then translate only in the x- and z-axis - cb6_offset = offset_structure(cb6, np.array([3, 5, 10])) + cb6_offset = shift_structure(cb6, numpy.array([3, 5, 10])) translated_cb6 = translate_to_origin(cb6_offset, dimension=[1, 0, 1]) coordinates = translated_cb6.coordinates - masses = np.asarray([atom.mass for atom in translated_cb6.atoms]) - centroid = pmd.geometry.center_of_mass(coordinates, masses) + masses = numpy.asarray([atom.mass for atom in translated_cb6.atoms]) + centroid = parmed.geometry.center_of_mass(coordinates, masses) assert pytest.approx(centroid[0], abs=1e-3) == 0.0 assert pytest.approx(centroid[1], abs=1e-3) != 0.0 @@ -116,7 +124,7 @@ def test_translate_to_origin(): def test_get_principal_axis(): - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), structure=True, ) @@ -128,7 +136,7 @@ def test_get_principal_axis(): def test_align_principal_axes(): - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), structure=True, ) @@ -160,7 +168,7 @@ def test_align_principal_axes(): def test_rotate_around_axis(): # Cartesian axes - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), structure=True, ) @@ -193,7 +201,7 @@ def test_rotate_around_axis(): assert pytest.approx(angle, abs=1e-1) == 180.0 # Arbitrary axes - cb6 = pmd.load_file( + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), structure=True, ) @@ -214,12 +222,17 @@ def test_rotate_around_axis(): assert pytest.approx(angle, abs=1e-1) == 54.7 -def test_check_coordinates(): - cb6 = pmd.load_file( +def test_get_centroid(): + cb6 = parmed.load_file( os.path.join(os.path.dirname(__file__), "../data/cb6-but/cb6-but-dum.pdb"), structure=True, ) - com = check_coordinates(cb6, mask=":BUT") - assert pytest.approx(com[0], abs=1e-3) == 0.0 - assert pytest.approx(com[1], abs=1e-3) == 0.0 - assert pytest.approx(com[2], abs=1e-1) == 1.9 + centroid = get_centroid(cb6, atom_mask=":BUT", weight="mass") + assert pytest.approx(centroid[0], abs=1e-3) == 0.0 + assert pytest.approx(centroid[1], abs=1e-3) == 0.0 + assert pytest.approx(centroid[2], abs=1e-3) == 1.918 + + centroid = get_centroid(cb6, atom_mask=":BUT", weight="geo") + assert pytest.approx(centroid[0], abs=1e-3) == 0.0 + assert pytest.approx(centroid[1], abs=1e-3) == 0.0 + assert pytest.approx(centroid[2], abs=1e-3) == 1.918 From 2eedf89190146abd3c3234f8f2e5867739dbe255 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 26 May 2023 10:33:08 -0700 Subject: [PATCH 14/34] fix argument order --- paprika/build/align.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paprika/build/align.py b/paprika/build/align.py index c3c456b..b07a276 100644 --- a/paprika/build/align.py +++ b/paprika/build/align.py @@ -500,17 +500,17 @@ def shift_structure(structure, offset, dimension=None): return structure -def translate_to_origin(structure, weight="mass", atom_mask=None, dimension=None): +def translate_to_origin(structure, atom_mask=None, weight="mass", dimension=None): """Translate a structure to the origin based on the centroid of the whole system or a subset of atom(s). Parameters ---------- structure : str or :class:`parmed.Structure` Molecular structure containing coordinates. - weight : str, optional, default="mass" - Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). atom_mask : str, optional, default=None Selection of atom(s) if a particular subset is preferred to estimate the centroid. + weight : str, optional, default="mass" + Calculate the centroid based on either atomic masses (``mass`` default) or geometric center (``geo``). dimension : str or list or :class:`numpy.ndarray`, optional, default=None A mask that will filter the dimensions to which the translation will be applied (by default the system will be translated in all dimensions). From 0d4b3ebe89424abe059481ec1efe4e393b1d284e Mon Sep 17 00:00:00 2001 From: jeff231li Date: Fri, 26 May 2023 11:23:13 -0700 Subject: [PATCH 15/34] fix bug in align --- paprika/build/align.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/paprika/build/align.py b/paprika/build/align.py index b07a276..8b49dd8 100644 --- a/paprika/build/align.py +++ b/paprika/build/align.py @@ -535,29 +535,17 @@ def translate_to_origin(structure, atom_mask=None, weight="mass", dimension=None if weight not in ["mass", "geo"]: raise ValueError("`weight` must either be `mass` or `geo`.") + # Centroid coordinates + centroid = get_centroid(structure, atom_mask=atom_mask, weight=weight) + # Dimension mask if dimension is None: - mask = numpy.array([1, 1, 1]) + dimension_mask = numpy.array([1, 1, 1]) else: - mask = _return_array(dimension) - - # Atomic coordinates and masses - if atom_mask is None: - coordinates = structure.coordinates - masses = numpy.asarray([atom.mass for atom in structure.atoms]) - else: - coordinates = structure[atom_mask].coordinates - masses = numpy.asarray([atom.mass for atom in structure[atom_mask].atoms]) - - # Equal weights if geometric center is preferred - if weight == "geo": - masses = numpy.ones(len(coordinates)) - - # Centroid coordinates - centroid = parmed.geometry.center_of_mass(coordinates, masses) + dimension_mask = _return_array(dimension) - if mask is not None: - centroid *= mask + if dimension_mask is not None: + centroid *= dimension_mask # Translate coordinates aligned_coords = numpy.empty_like(structure.coordinates) From 1f3d49fc2be049acef8b173362a84835ea507a3f Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sat, 27 May 2023 14:55:03 -0700 Subject: [PATCH 16/34] lint --- paprika/tests/test_align.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paprika/tests/test_align.py b/paprika/tests/test_align.py index a48e9d3..78df716 100644 --- a/paprika/tests/test_align.py +++ b/paprika/tests/test_align.py @@ -14,8 +14,8 @@ get_centroid, get_principal_axis_vector, get_theta, - shift_structure, rotate_around_axis, + shift_structure, translate_to_origin, zalign, ) From 6db6b4bd28b57a1e6e2d02787350f094a9aa566c Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sun, 4 Jun 2023 12:42:25 -0700 Subject: [PATCH 17/34] fix import namespace --- paprika/tests/test_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paprika/tests/test_analysis.py b/paprika/tests/test_analysis.py index c634b37..1be3c70 100644 --- a/paprika/tests/test_analysis.py +++ b/paprika/tests/test_analysis.py @@ -398,7 +398,7 @@ def test_save_and_loading(clean_files, setup_free_energy_calculation): # Load results fe_calc = analysis.fe_calc() fe_calc.load_results("tmp/results.json") - assert np.isclose(-4.34372240, fe_calc.results["ref_state_work"].magnitude) + assert numpy.isclose(-4.34372240, fe_calc.results["ref_state_work"].magnitude) # Save Simulation data to file setup_free_energy_calculation.save_data("tmp/simulation_data.json") From 67d937e7224610f15c8bd94be9a19d2f556c59df Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sun, 4 Jun 2023 12:45:16 -0700 Subject: [PATCH 18/34] remove tmp folder --- paprika/tests/tmp/test.txt | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 paprika/tests/tmp/test.txt diff --git a/paprika/tests/tmp/test.txt b/paprika/tests/tmp/test.txt deleted file mode 100644 index aa73033..0000000 --- a/paprika/tests/tmp/test.txt +++ /dev/null @@ -1,19 +0,0 @@ -0.10274 -0.11361 -0.13074 -0.14136 -0.38928 -0.38928 -0.11215 -0.12951 -0.13145 -0.13145 -0.13113 -0.13113 -0.13751 -0.14186 -0.14186 -0.12847 -0.13550 -0.13550 -0.13404 From f28dd36392f1a2afeae03570db2099dcf3c9b417 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sun, 4 Jun 2023 12:50:48 -0700 Subject: [PATCH 19/34] refactor taproom stuff --- paprika/evaluator/utils.py | 13 ----- paprika/taproom/__init__.py | 6 +++ paprika/taproom/taproom.py | 54 +++++++++++++++++++ .../taproom.py => taproom/utils.py} | 35 ------------ paprika/tests/test_evaluator.py | 7 +-- 5 files changed, 62 insertions(+), 53 deletions(-) delete mode 100644 paprika/evaluator/utils.py create mode 100644 paprika/taproom/__init__.py create mode 100644 paprika/taproom/taproom.py rename paprika/{restraints/taproom.py => taproom/utils.py} (73%) diff --git a/paprika/evaluator/utils.py b/paprika/evaluator/utils.py deleted file mode 100644 index a602ab2..0000000 --- a/paprika/evaluator/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -import pkg_resources - - -def get_benchmarks(): - """ - Determine the installed ``taproom`` benchmarks. - """ - installed_benchmarks = {} - - for entry_point in pkg_resources.iter_entry_points(group="taproom.benchmarks"): - installed_benchmarks[entry_point.name] = entry_point.load() - - return installed_benchmarks diff --git a/paprika/taproom/__init__.py b/paprika/taproom/__init__.py new file mode 100644 index 0000000..2c3e1e4 --- /dev/null +++ b/paprika/taproom/__init__.py @@ -0,0 +1,6 @@ +from .taproom import get_benchmarks, read_yaml_schema + +__all__ = [ + "get_benchmarks", + "read_yaml_schema", +] diff --git a/paprika/taproom/taproom.py b/paprika/taproom/taproom.py new file mode 100644 index 0000000..171b7db --- /dev/null +++ b/paprika/taproom/taproom.py @@ -0,0 +1,54 @@ +import logging + +import pkg_resources +import yaml + +from paprika.taproom.utils import convert_string_to_quantity, de_alias + +logger = logging.getLogger(__name__) + + +def get_benchmarks(): + """ + Determine the installed ``taproom`` benchmarks. + """ + installed_benchmarks = {} + + for entry_point in pkg_resources.iter_entry_points(group="taproom.benchmarks"): + installed_benchmarks[entry_point.name] = entry_point.load() + + return installed_benchmarks + + +def read_yaml_schema(file): + """ + Read `Taproom `_ -style YAML-formatted instructions for + preparing host-guest systems. + + Parameters + ---------- + file: os.PathLike + A YAML-formatted file. + + Returns + ------- + yaml_data: dict + Dictionary containing simulation setup parameters. + + """ + + # Read YAML file + with open(file, "r") as f: + yaml_data = yaml.safe_load(f) + logger.debug(yaml_data) + + # Convert aliases to atom masks + if "aliases" in yaml_data.keys(): + logger.debug("Dealiasing atom masks...") + yaml_data = de_alias(yaml_data) + + # Convert all string to OpenFF Quantity + logger.debug("Converting string to unit.Quantity...") + convert_string_to_quantity(yaml_data) + + return yaml_data diff --git a/paprika/restraints/taproom.py b/paprika/taproom/utils.py similarity index 73% rename from paprika/restraints/taproom.py rename to paprika/taproom/utils.py index 8dd2ee2..229e1ec 100644 --- a/paprika/restraints/taproom.py +++ b/paprika/taproom/utils.py @@ -1,6 +1,5 @@ import logging -import yaml from openff.units import unit as openff_unit from paprika.utils import multiple_replace @@ -8,40 +7,6 @@ logger = logging.getLogger(__name__) -def read_yaml_schema(file): - """ - Read `Taproom `_ -style YAML-formatted instructions for - preparing host-guest systems. - - Parameters - ---------- - file: os.PathLike - A YAML-formatted file. - - Returns - ------- - yaml_data: dict - Dictionary containing simulation setup parameters. - - """ - - # Read YAML file - with open(file, "r") as f: - yaml_data = yaml.safe_load(f) - logger.debug(yaml_data) - - # Convert aliases to atom masks - if "aliases" in yaml_data.keys(): - logger.debug("Dealiasing atom masks...") - yaml_data = de_alias(yaml_data) - - # Convert all string to OpenFF Quantity - logger.debug("Converting string to unit.Quantity...") - convert_string_to_quantity(yaml_data) - - return yaml_data - - def de_alias(yaml_data): """ Replace aliased atoms in a ``taproom`` recipe. diff --git a/paprika/tests/test_evaluator.py b/paprika/tests/test_evaluator.py index b6128a3..d4cadc3 100644 --- a/paprika/tests/test_evaluator.py +++ b/paprika/tests/test_evaluator.py @@ -15,11 +15,8 @@ from paprika.evaluator import Analyze, Setup from paprika.evaluator.amber import generate_gaff from paprika.restraints import DAT_restraint -from paprika.restraints.taproom import ( - convert_string_to_quantity, - de_alias, - read_yaml_schema, -) +from paprika.taproom.taproom import read_yaml_schema +from paprika.taproom.utils import convert_string_to_quantity, de_alias logger = logging.getLogger(__name__) From 110987bdf13410390f8537142ec75e245686cc3d Mon Sep 17 00:00:00 2001 From: jeff231li Date: Sun, 4 Jun 2023 13:04:02 -0700 Subject: [PATCH 20/34] update test_analysis --- paprika/tests/test_analysis.py | 61 +++++++++++----------------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/paprika/tests/test_analysis.py b/paprika/tests/test_analysis.py index 1be3c70..5787605 100644 --- a/paprika/tests/test_analysis.py +++ b/paprika/tests/test_analysis.py @@ -127,27 +127,6 @@ def test_setup(clean_files, setup_free_energy_calculation): assert setup_free_energy_calculation.exact_sem_each_ti_fraction is False assert setup_free_energy_calculation.conservative_subsample is False - # Test save and load results -- JSON - results = deepcopy(setup_free_energy_calculation.results) - setup_free_energy_calculation.save_results("tmp/results.json", overwrite=True) - assert is_file_and_not_empty("tmp/results.json") - setup_free_energy_calculation.results = {} - setup_free_energy_calculation.load_results("tmp/results.json") - assert len(setup_free_energy_calculation.results) == len(results) - - # Test save and load simulation data -- JSON - setup_free_energy_calculation.save_simulation_data_to_json( - "tmp/simulation.json", overwrite=True - ) - assert is_file_and_not_empty("tmp/simulation.json") - setup_free_energy_calculation.changing_restraints = None - setup_free_energy_calculation.orders = None - setup_free_energy_calculation.simulation_data = None - setup_free_energy_calculation.load_simulation_data_from_json("tmp/simulation.json") - assert setup_free_energy_calculation.changing_restraints is not None - assert setup_free_energy_calculation.orders is not None - assert setup_free_energy_calculation.simulation_data is not None - def test_mbar_block(clean_files, setup_free_energy_calculation): method = "mbar-block" @@ -389,28 +368,26 @@ def test_reference_state_work(clean_files, setup_free_energy_calculation): def test_save_and_loading(clean_files, setup_free_energy_calculation): - # Save FE results to file - setup_free_energy_calculation.save_results("tmp/results.json") - assert is_file_and_not_empty("tmp/results.json") is True + # Test save and load results -- JSON + results = deepcopy(setup_free_energy_calculation.results) setup_free_energy_calculation.save_results("tmp/results.json", overwrite=True) - assert is_file_and_not_empty("tmp/results.json") is True - - # Load results - fe_calc = analysis.fe_calc() - fe_calc.load_results("tmp/results.json") - assert numpy.isclose(-4.34372240, fe_calc.results["ref_state_work"].magnitude) - - # Save Simulation data to file - setup_free_energy_calculation.save_data("tmp/simulation_data.json") - assert is_file_and_not_empty("tmp/simulation_data.json") is True - setup_free_energy_calculation.save_data("tmp/simulation_data.json", overwrite=True) - assert is_file_and_not_empty("tmp/simulation_data.json") is True - - # Load simulation data - fe_calc.load_data("tmp/simulation_data.json") - assert fe_calc.simulation_data is not None - assert fe_calc.changing_restraints is not None - assert fe_calc.orders is not None + assert is_file_and_not_empty("tmp/results.json") + setup_free_energy_calculation.results = {} + setup_free_energy_calculation.load_results("tmp/results.json") + assert len(setup_free_energy_calculation.results) == len(results) + + # Test save and load simulation data -- JSON + setup_free_energy_calculation.save_simulation_data_to_json( + "tmp/simulation.json", overwrite=True + ) + assert is_file_and_not_empty("tmp/simulation.json") + setup_free_energy_calculation.changing_restraints = None + setup_free_energy_calculation.orders = None + setup_free_energy_calculation.simulation_data = None + setup_free_energy_calculation.load_simulation_data_from_json("tmp/simulation.json") + assert setup_free_energy_calculation.changing_restraints is not None + assert setup_free_energy_calculation.orders is not None + assert setup_free_energy_calculation.simulation_data is not None def test_temperature(clean_files): From fcdff493921a5238fbdd150cc9e6eee1774b70ef Mon Sep 17 00:00:00 2001 From: Jeffry Setiadi Date: Tue, 11 Jul 2023 17:09:14 -0700 Subject: [PATCH 21/34] add module to build APR from Taproom --- paprika/build/system/__init__.py | 2 + paprika/build/system/taproom.py | 827 +++++++++++++++++++++++++++++++ paprika/restraints/__init__.py | 3 +- paprika/taproom/taproom.py | 4 +- paprika/utils.py | 2 +- 5 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 paprika/build/system/taproom.py diff --git a/paprika/build/system/__init__.py b/paprika/build/system/__init__.py index 148d7b7..448b125 100644 --- a/paprika/build/system/__init__.py +++ b/paprika/build/system/__init__.py @@ -1,8 +1,10 @@ from paprika.build.system.tleap import TLeap from paprika.build.system.utils import ConversionToolkit, PBCBox +from paprika.build.system.taproom import BuildTaproomAPR __all__ = [ "ConversionToolkit", "PBCBox", "TLeap", + "BuildTaproomAPR", ] diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py new file mode 100644 index 0000000..eb5ffbe --- /dev/null +++ b/paprika/build/system/taproom.py @@ -0,0 +1,827 @@ +import json +import os +import shutil +from typing import Any, Dict, List + +import mdtraj +import numpy +import openmm +import openmm.app as app +import parmed +from joblib import Parallel, delayed +from openff.interchange import Interchange +from openff.interchange.components._packmol import pack_box +from openff.toolkit import ForceField, Molecule, Topology +from openff.units import unit +from tqdm.auto import tqdm + +from paprika.evaluator import Setup +from paprika.io import PaprikaEncoder, save_restraints +from paprika.restraints import DAT_restraint, create_window_list, parse_window +from paprika.restraints.openmm import apply_dat_restraint, apply_positional_restraints +from paprika.taproom import get_benchmarks, read_yaml_schema +from paprika.utils import index_from_mask + + +class BuildTaproomAPR: + """A class to generate APR files from Taproom database in pAPRika. + + TODO: Implement an option to create files with implicit solvent. + TODO: Implement an option to use GAFF force field. + + Parameters + ---------- + host_code: str + The 3-letter Taproom code for the host molecule. + guest_code: str + The 3-letter Taproom code for the guest molecule. + n_water: int + Number of Water molecules. + build_folder: str + Temporary folder to save intermediate files. + working_folder: str + The main folder to write the APR structure files. + + Example + ------- + >>> from paprika.build.system import BuildTaproomAPR + >>> from openff.toolkit import ForceField + >>> + >>> # Select OpenFF force field version + >>> force_field = ForceField("openff-2.0.0.offxml") + >>> + >>> # Initiate system object + >>> system = BuildTaproomAPR(host_code="bcd", guest_code="hex", n_water=3000, force_field=force_field) + >>> + >>> # Build APR files + >>> system.build_system() + >>> + >>> # We can extend the default `r_final` specified in Taproom + >>> from openff.units import unit + >>> system.extend_pull_distance(extend_by=6*unit.angstrom) + >>> system.build_system() + >>> + >>> # Creating these files can take 10-20 minutes on one core. We can speed things up with more CPU cores + >>> system.build_system(n_cpus=4) + """ + + def __init__( + self, + host_code: str, + guest_code: str, + n_water: int, + force_field: ForceField, + build_folder: str = "build_files", + working_folder: str = "simulations", + ): + self._host_code = host_code + self._guest_code = guest_code + self._n_water = n_water + self._force_field = force_field + self._build_folder = build_folder + self._working_folder = working_folder + self._host_metadata = None + self._guest_metadata = None + self._orientations = None + self._water_mol = None + self._water_intrcg = None + self._restraints = None + self._n_cpus = 1 + + self._initialize() + + def _initialize(self): + # Create folder + os.makedirs(self._build_folder, exist_ok=True) + os.makedirs(self._working_folder, exist_ok=True) + + # Load Host-Guest system from Taproom + taproom = get_benchmarks() + self._host_metadata = taproom["host_guest_systems"][self._host_code] + self._guest_metadata = taproom["host_guest_systems"][self._host_code][ + self._guest_code + ] + self._orientations = list(self._host_metadata["yaml"].keys()) + self._host_yaml_schema = read_yaml_schema( + self._host_metadata["yaml"][self._orientations[0]] + ) + self._guest_yaml_schema = read_yaml_schema(self._guest_metadata["yaml"]) + + # Water Molecule + self._water_mol = Molecule.from_smiles("O") + self._water_intrcg = Interchange.from_smirnoff( + force_field=self._force_field, + topology=[self._water_mol] * self._n_water, + ) + + # Load restraints + self._restraints = { + "static_restraints": self._unnest_restraint_specs( + self._host_yaml_schema["restraints"]["static"] + ), + "conformational_restraints": self._unnest_restraint_specs( + self._host_yaml_schema["restraints"]["conformational"] + ), + "guest_restraints": self._unnest_restraint_specs( + self._guest_yaml_schema["restraints"]["guest"] + ), + "wall_restraints": self._unnest_restraint_specs( + self._guest_yaml_schema["restraints"]["wall_restraints"] + ), + "symmetry_restraints": self._unnest_restraint_specs( + self._guest_yaml_schema["symmetry_correction"]["restraints"] + ), + } + + @staticmethod + def _unnest_restraint_specs( + restraint_specs: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """A helper method to un-nest restraint lists parsed from a taproom + yaml file. + + Parameters + ---------- + restraint_specs + The restraint specs to un-nest. + """ + return [ + value["restraint"] + for value in restraint_specs + if value["restraint"] is not None + ] + + @staticmethod + def _restraints_to_dict(restraints: List[DAT_restraint]): + """Converts a list of ``paprika`` restraint objects to + a list of JSON compatible dictionary representations + """ + + return [ + json.loads(json.dumps(restraint.__dict__, cls=PaprikaEncoder)) + for restraint in restraints + ] + + def _solvate_and_add_dummy( + self, + complex_path: str, + solvated_path: str, + unique_molecules: List[Molecule], + offset: float, + ): + """Solvate a PDB file with PackMol through OpenFF-Interchange. + + Parameters + ---------- + complex_path: str + The file path of the complex PDB. + solvated_path: str + The output file path for the solvated complex. + unique_molecules: List[Molecule] + List of unique molecules (to generate the OpenFF Topology) + offset: float + An offset to place the dummy atoms. + + Returns + ------- + system_intrcg: openff.interchange.Interchange + The solvated system as OpenFF-Interchange object. + """ + rectangular_box = numpy.asarray( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 2.0], + ] + ) + + # 01 - Solvate structure + pdbfile = app.PDBFile(complex_path) + solute_topology = Topology.from_openmm( + pdbfile.topology, unique_molecules=unique_molecules + ) + solute_intrcg = Interchange.from_smirnoff( + force_field=self._force_field, + topology=solute_topology, + charge_from_molecules=unique_molecules, + ) + solvated_topology = pack_box( + molecules=[self._water_mol], + number_of_copies=[self._n_water], + solute=solute_intrcg.topology, + box_shape=rectangular_box, + mass_density=0.95 * unit.grams / unit.milliliters, + center_solute="ORIGIN", + ) + solute_intrcg.box = solvated_topology.box_vectors + self._water_intrcg.box = solvated_topology.box_vectors + solvated_topology.to_file(solvated_path) + + # 02 - Add Dummy Atoms to PDB + input_structure = parmed.load_file(solvated_path, structure=True) + Setup.add_dummy_atoms_to_structure( + input_structure, + dummy_atom_offsets=[ + numpy.array([0, 0, -offset]), + numpy.array([0, 0, -3.0 - offset]), + numpy.array([0, 2.2, -5.2 - offset]), + ], + offset_coordinates=numpy.zeros(3), + ) + + # 03 - Shift structure to avoid issues with PBC + input_structure.coordinates += numpy.array( + [ + input_structure.box[0] * 0.5, + input_structure.box[1] * 0.5, + -input_structure.coordinates[-1, 2] + 5.0, + ] + ) + + # 04 - Write PDB for solvated system + with open(solvated_path, "w") as f: + app.PDBFile.writeFile( + input_structure.topology, + input_structure.positions, + f, + keepIds=True, + ) + + # 05 - Combine interchange objects + system_intrcg = solute_intrcg + self._water_intrcg + system_intrcg.box = solvated_topology.box_vectors + + return system_intrcg + + @staticmethod + def _create_system_and_add_dummy( + system_intrcg: Interchange, system_output_path: str + ): + """Convert `Interchange` object to OpenMM System and add dummy atoms. + + Parameters + ---------- + system_intrcg: Interchange + The Interchange object to convert. + system_output_path: str + The file path to save the OpenMM System to XML file. + """ + openmm_system = system_intrcg.to_openmm() + + for _ in range(3): + openmm_system.addParticle(mass=207) + + for i, force in enumerate(openmm_system.getForces()): + if isinstance(force, openmm.NonbondedForce): + force.addParticle(0.0, 1.0, 0.0) + force.addParticle(0.0, 1.0, 0.0) + force.addParticle(0.0, 1.0, 0.0) + + with open(system_output_path, "w") as f: + f.write(openmm.XmlSerializer.serialize(openmm_system)) + + def _build_pull_structures( + self, + i, + orient, + complex_path, + guest_atom_indices, + guest_orientation_mask, + pull_distance, + offset, + n_windows, + unique_molecules, + ): + """Function that translates guest molecules from host that is to be wrapped in `delayed` for parallelism.""" + + folder = f"{self._working_folder}/pull-{orient}/p{i:03}" + os.makedirs(folder, exist_ok=True) + + # 01 - Prepare complex structure + structure = Setup.prepare_complex_structure( + complex_path, + guest_atom_indices, + guest_orientation_mask, + pull_distance=pull_distance, + pull_window_index=i, + n_pull_windows=n_windows["pull"], + ) + complex_prepared_path = ( + f"{folder}/{self._host_code}-{self._guest_code}-{orient}.pdb" + ) + with open(complex_prepared_path, "w") as f: + app.PDBFile.writeFile( + structure.topology, + structure.positions, + f, + keepIds=True, + ) + + # 02 - Solvate structure + complex_solvated_path = f"{folder}/restrained.pdb" + host_guest_system_intrcg = self._solvate_and_add_dummy( + complex_prepared_path, + complex_solvated_path, + unique_molecules=unique_molecules, + offset=offset, + ) + + if i == 0: + # 03 - Create Host-Guest OpenMM System with Dummy Atoms + system_output_path = f"{self._build_folder}/{self._host_code}-{self._guest_code}-dum-solv.xml" + self._create_system_and_add_dummy( + host_guest_system_intrcg, system_output_path + ) + + # 04 - Clean up + os.remove(complex_prepared_path) + + def _build_apr_structures(self): + """Build and prepare the APR structures and windows.""" + host_resname = self._host_yaml_schema["resname"] + n_windows = self._host_yaml_schema["calculation"]["windows"] + + # Create OpenFF Molecule instances of molecules + guest_mol = Molecule.from_file( + str( + self._host_metadata["path"] + .joinpath(self._guest_yaml_schema["name"]) + .joinpath(self._guest_yaml_schema["structure"]["sdf"]) + ) + ) + host_mol = Molecule.from_file( + str( + self._host_metadata["path"].joinpath( + self._host_yaml_schema["structure"]["sdf"] + ) + ) + ) + + # --------------------------------------------------------------------- # + # Prepare Host-Guest Complex + # --------------------------------------------------------------------- # + print("Generating files for the `pull` phase.") + for orient in self._orientations: + # 01 - Load complex structure + complex_path = str( + self._host_metadata["path"] + .joinpath(self._guest_yaml_schema["name"]) + .joinpath(self._guest_yaml_schema["complex"]) + ).replace(".pdb", f"-{orient}.pdb") + structure = parmed.load_file(complex_path, structure=True) + + # 02 - Get Guest indices and mask + guest_atom_indices = index_from_mask( + structure, f":{self._guest_yaml_schema['name'].upper()}" + ) + G1 = self._guest_yaml_schema["aliases"][3]["G1"] + G2 = self._guest_yaml_schema["aliases"][4]["G2"] + guest_orientation_mask = f"{G1} {G2}" + + # 03 - Initial `r` values + r_initial = self._guest_yaml_schema["restraints"]["guest"][0]["restraint"][ + "attach" + ]["target"] + r_final = self._guest_yaml_schema["restraints"]["guest"][0]["restraint"][ + "pull" + ]["target"] + pull_distance = (r_final - r_initial).m_as(unit.angstrom) + offset = self._guest_yaml_schema["restraints"]["guest"][0]["restraint"][ + "attach" + ]["target"].m_as(unit.angstrom) + + # --------------------------------------------------------------------- # + # Prepare `pull` windows + # --------------------------------------------------------------------- # + Parallel(n_jobs=self._n_cpus)( + delayed(self._build_pull_structures)( + i, + orient, + complex_path, + guest_atom_indices, + guest_orientation_mask, + pull_distance, + offset, + n_windows, + [host_mol, guest_mol], + ) + for i in tqdm(range(n_windows["pull"])) + ) + + # --------------------------------------------------------------------- # + # Prepare `attach` windows - Copy PDB from p000 + # --------------------------------------------------------------------- # + print("Generating files for the `attach` phase.") + for i in tqdm(range(n_windows["attach"])): + folder = f"{self._working_folder}/attach-{orient}/a{i:03}" + os.makedirs(folder, exist_ok=True) + shutil.copy( + f"{self._working_folder}/pull-{orient}/p000/restrained.pdb", + f"{folder}/restrained.pdb", + ) + + # --------------------------------------------------------------------- # + # Prepare Host-only Structure + # --------------------------------------------------------------------- # + print("Generating files for the `release` phase.") + # 01 - Remove Guest molecule from complex structure + complex_solvate_path = f"{self._working_folder}/pull-p/p000/restrained.pdb" + complex_structure = parmed.load_file(complex_solvate_path, structure=True) + host_atom_indices = index_from_mask(complex_structure, mask=f":{host_resname}") + + mdtraj_trajectory = mdtraj.load_pdb(complex_solvate_path) + host_trajectory = mdtraj_trajectory.atom_slice(host_atom_indices) + host_trajectory.save(f"{self._build_folder}/host_input.pdb") + + # 02 - Align host molecule + host_structure = Setup.prepare_host_structure( + f"{self._build_folder}/host_input.pdb" + ) + output_coordinate_path = f"{self._build_folder}/host_input_aligned.pdb" + with open(output_coordinate_path, "w") as file: + app.PDBFile.writeFile( + host_structure.topology, host_structure.positions, file, True + ) + + # 03 - Solvate host molecule and add dummy atoms + host_solvated_path = f"{self._build_folder}/{self._host_code}-dum-solv.pdb" + host_system_intrcg = self._solvate_and_add_dummy( + output_coordinate_path, + host_solvated_path, + unique_molecules=[host_mol], + offset=offset, + ) + + # 04 - Create Host-only OpenMM System with Dummy Atoms + system_output_path = f"{self._build_folder}/{self._host_code}-dum-solv.xml" + self._create_system_and_add_dummy(host_system_intrcg, system_output_path) + + # --------------------------------------------------------------------- # + # Prepare `release` windows - Copy PDB + # --------------------------------------------------------------------- # + for i in tqdm(range(n_windows["release"])): + folder = f"{self._working_folder}/release/r{i:03}" + os.makedirs(folder, exist_ok=True) + shutil.copy( + host_solvated_path, + f"{folder}/restrained.pdb", + ) + + def _apply_attach_restraints(self): + """Apply restraints for the `attach` phase.""" + + print("Applying restraints for `attach` phase...") + attach_lambdas = self._host_yaml_schema["calculation"]["lambda"]["attach"] + n_windows = self._host_yaml_schema["calculation"]["windows"] + + for orient in self._orientations: + attach_folder = f"{self._working_folder}/attach-{orient}" + complex_path = f"{self._working_folder}/pull-{orient}/p000/restrained.pdb" + + static_restraints = Setup.build_static_restraints( + complex_path, + n_attach_windows=n_windows["attach"], + n_pull_windows=None, + n_release_windows=None, + restraint_schemas=self._restraints["static_restraints"], + ) + conformational_restraints = Setup.build_conformational_restraints( + complex_path, + attach_lambdas=attach_lambdas, + n_pull_windows=None, + release_lambdas=None, + restraint_schemas=self._restraints["conformational_restraints"], + ) + guest_restraints = Setup.build_guest_restraints( + complex_path, + attach_lambdas=attach_lambdas, + n_pull_windows=None, + restraint_schemas=self._restraints["guest_restraints"], + ) + symmetry_restraints = Setup.build_symmetry_restraints( + complex_path, + n_attach_windows=n_windows["attach"], + restraint_schemas=self._restraints["symmetry_restraints"], + ) + wall_restraints = Setup.build_wall_restraints( + complex_path, + n_attach_windows=n_windows["attach"], + restraint_schemas=self._restraints["wall_restraints"], + ) + + symmetry_restraints = ( + [] if symmetry_restraints is None else symmetry_restraints + ) + wall_restraints = [] if wall_restraints is None else wall_restraints + guest_restraints = [] if guest_restraints is None else guest_restraints + + restraints_dictionary = { + "static": self._restraints_to_dict(static_restraints), + "conformational": self._restraints_to_dict(conformational_restraints), + "symmetry": self._restraints_to_dict(symmetry_restraints), + "wall": self._restraints_to_dict(wall_restraints), + "guest": self._restraints_to_dict(guest_restraints), + } + + with open(f"{attach_folder}/restraints.json", "w") as file: + json.dump(restraints_dictionary, file) + + save_restraints( + conformational_restraints + guest_restraints, + filepath=f"{attach_folder}/apr_restraints.json", + ) + + # Apply restraints + attach_windows = create_window_list(guest_restraints) + Parallel(n_jobs=self._n_cpus)( + delayed(self._apply_attach_to_system)( + window, + complex_path, + attach_folder, + static_restraints, + conformational_restraints, + guest_restraints, + symmetry_restraints, + wall_restraints, + ) + for window in tqdm(attach_windows) + ) + + def _apply_pull_restraints(self): + """Apply restraints for the `pull` phase.""" + + print("Applying restraints for `pull` phase...") + attach_lambdas = self._host_yaml_schema["calculation"]["lambda"]["attach"] + n_windows = self._host_yaml_schema["calculation"]["windows"] + + for orient in self._orientations: + pull_folder = f"{self._working_folder}/pull-{orient}" + complex_path = f"{self._working_folder}/pull-{orient}/p000/restrained.pdb" + + static_restraints = Setup.build_static_restraints( + complex_path, + n_attach_windows=n_windows["attach"], + n_pull_windows=n_windows["pull"], + n_release_windows=None, + restraint_schemas=self._restraints["static_restraints"], + ) + conformational_restraints = Setup.build_conformational_restraints( + complex_path, + attach_lambdas=attach_lambdas, + n_pull_windows=n_windows["pull"], + release_lambdas=None, + restraint_schemas=self._restraints["conformational_restraints"], + ) + guest_restraints = Setup.build_guest_restraints( + complex_path, + attach_lambdas=attach_lambdas, + n_pull_windows=n_windows["pull"], + restraint_schemas=self._restraints["guest_restraints"], + ) + guest_restraints = [] if guest_restraints is None else guest_restraints + + # Remove the `attach` phases from the restraints as these restraints are + # only being used for the pull phase. + for restraint in ( + static_restraints + conformational_restraints + guest_restraints + ): + for key in restraint.phase["attach"]: + restraint.phase["attach"][key] = None + + restraints_dictionary = { + "static": self._restraints_to_dict(static_restraints), + "conformational": self._restraints_to_dict(conformational_restraints), + "symmetry": None, + "wall": None, + "guest": self._restraints_to_dict(guest_restraints), + } + + with open(f"{pull_folder}/restraints.json", "w") as file: + json.dump(restraints_dictionary, file) + + save_restraints( + conformational_restraints + guest_restraints, + filepath=f"{pull_folder}/apr_restraints.json", + ) + + # Apply restraints + pull_windows = create_window_list(guest_restraints) + Parallel(n_jobs=self._n_cpus)( + delayed(self._apply_pull_to_system)( + window, + complex_path, + pull_folder, + static_restraints, + conformational_restraints, + guest_restraints, + ) + for window in tqdm(pull_windows) + ) + + def _apply_release_restraints(self): + """Apply restraints for the `release` phase.""" + + print("Applying restraints for `release` phase...") + release_lambdas = self._host_yaml_schema["calculation"]["lambda"]["release"] + n_windows = self._host_yaml_schema["calculation"]["windows"] + + release_folder = f"{self._working_folder}/release" + host_solvated_path = f"{release_folder}/r000/restrained.pdb" + + static_restraints = Setup.build_static_restraints( + host_solvated_path, + n_attach_windows=None, + n_pull_windows=None, + n_release_windows=n_windows["release"], + restraint_schemas=self._restraints["static_restraints"], + ) + conformational_restraints = Setup.build_conformational_restraints( + host_solvated_path, + attach_lambdas=None, + n_pull_windows=None, + release_lambdas=release_lambdas, + restraint_schemas=self._restraints["conformational_restraints"], + ) + + restraints_dictionary = { + "static": self._restraints_to_dict(static_restraints), + "conformational": self._restraints_to_dict(conformational_restraints), + "symmetry": None, + "wall": None, + "guest": None, + } + + with open(f"{release_folder}/restraints.json", "w") as file: + json.dump(restraints_dictionary, file) + + save_restraints( + conformational_restraints, + filepath=f"{release_folder}/apr_restraints.json", + ) + + # Apply restraints + system_path = f"{self._build_folder}/{self._host_code}-dum-solv.xml" + release_windows = create_window_list(conformational_restraints) + Parallel(n_jobs=self._n_cpus)( + delayed(self._apply_release_to_system)( + window, + host_solvated_path, + system_path, + release_folder, + static_restraints, + conformational_restraints, + ) + for window in tqdm(release_windows) + ) + + @staticmethod + def _apply_attach_to_system( + window, + complex_path, + attach_folder, + static_restraints, + conformational_restraints, + guest_restraints, + symmetry_restraints, + wall_restraints, + ): + """Function that applies `attach` restraints that is to be wrapped in `delayed` for parallelism.""" + + window_number, phase = parse_window(window) + folder = f"{attach_folder}/{window}" + os.makedirs(folder, exist_ok=True) + + system_path = f"{folder}/restrained.xml" + with open(system_path, "r") as file: + system = openmm.XmlSerializer.deserialize(file.read()) + + for restraint in static_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=10) + for restraint in conformational_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=11) + for restraint in guest_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=12) + for restraint in symmetry_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=13) + for restraint in wall_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=14) + + apply_positional_restraints(complex_path, system, force_group=15) + + new_system_path = f"{folder}/restrained.xml" + with open(new_system_path, "w") as file: + file.write(openmm.XmlSerializer.serialize(system)) + + @staticmethod + def _apply_pull_to_system( + window, + complex_path, + pull_folder, + static_restraints, + conformational_restraints, + guest_restraints, + ): + """Function that applies `pull` restraints that is to be wrapped in `delayed` for parallelism.""" + + window_number, phase = parse_window(window) + folder = f"{pull_folder}/{window}" + os.makedirs(folder, exist_ok=True) + + system_path = f"{folder}/restrained.xml" + with open(system_path, "r") as file: + system = openmm.XmlSerializer.deserialize(file.read()) + + for restraint in static_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=10) + for restraint in conformational_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=11) + for restraint in guest_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=12) + + apply_positional_restraints(complex_path, system, force_group=15) + + new_system_path = f"{folder}/restrained.xml" + with open(new_system_path, "w") as file: + file.write(openmm.XmlSerializer.serialize(system)) + + @staticmethod + def _apply_release_to_system( + window, + host_solvated_path, + system_path, + release_folder, + static_restraints, + conformational_restraints, + ): + """Function that applies `release` restraints that is to be wrapped in `delayed` for parallelism.""" + + window_number, phase = parse_window(window) + folder = f"{release_folder}/{window}" + os.makedirs(folder, exist_ok=True) + + with open(system_path, "r") as file: + system = openmm.XmlSerializer.deserialize(file.read()) + + for restraint in static_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=10) + for restraint in conformational_restraints: + apply_dat_restraint(system, restraint, phase, window_number, force_group=11) + + apply_positional_restraints(host_solvated_path, system, force_group=15) + + new_system_path = f"{folder}/restrained.xml" + with open(new_system_path, "w") as file: + file.write(openmm.XmlSerializer.serialize(system)) + + def extend_pull_distance(self, extend_by: unit.Quantity): + """Extend the pull distance further than what is in the original Taproom Metadata. + + Parameters + ---------- + extend_by: openff.unit.units.Quantity + The extended distance to pull the guest molecule to. + """ + + # Determine dr between windows + pull_distance = ( + self._restraints["guest_restraints"][0]["pull"]["target"] + - self._restraints["guest_restraints"][0]["attach"]["target"] + ).m_as(unit.angstrom) + n_pull_windows = self._host_yaml_schema["calculation"]["windows"]["pull"] + dr = pull_distance / n_pull_windows + + # Update distance + self._restraints["guest_restraints"][0]["pull"]["target"] = extend_by + new_pull_distance = ( + self._restraints["guest_restraints"][0]["pull"]["target"] + - self._restraints["guest_restraints"][0]["attach"]["target"] + ).m_as(unit.angstrom) + updated_n_windows = int(new_pull_distance / dr) + + self._host_yaml_schema["calculation"]["windows"]["pull"] = updated_n_windows + + def build_system(self, n_cpus: int = 1, clean_files: bool = False): + """Build the APR files in the order: + + (1) Generate Structures and add dummy atoms + (2) Apply `attach` restraints + (3) Apply `pull` restraints + (4) Apply `release` restraints + + Parameters + ---------- + n_cpus: int + Number of CPUs to spread the workload. + clean_files: bool + Option to delete temporary files. + """ + + self._n_cpus = n_cpus + + self._build_apr_structures() + self._apply_attach_restraints() + self._apply_pull_restraints() + self._apply_release_restraints() + + if clean_files: + shutil.rmtree(self._build_folder) diff --git a/paprika/restraints/__init__.py b/paprika/restraints/__init__.py index 40c473b..645264e 100644 --- a/paprika/restraints/__init__.py +++ b/paprika/restraints/__init__.py @@ -7,7 +7,7 @@ RestraintType, static_DAT_restraint, ) -from .utils import create_window_list +from .utils import create_window_list, parse_window __all__ = [ "BiasPotentialType", @@ -18,4 +18,5 @@ "Plumed", "Colvars", "create_window_list", + "parse_window", ] diff --git a/paprika/taproom/taproom.py b/paprika/taproom/taproom.py index 171b7db..0fb7faf 100644 --- a/paprika/taproom/taproom.py +++ b/paprika/taproom/taproom.py @@ -1,6 +1,6 @@ import logging -import pkg_resources +import importlib.metadata import yaml from paprika.taproom.utils import convert_string_to_quantity, de_alias @@ -14,7 +14,7 @@ def get_benchmarks(): """ installed_benchmarks = {} - for entry_point in pkg_resources.iter_entry_points(group="taproom.benchmarks"): + for entry_point in importlib.metadata.entry_points(group="taproom.benchmarks"): installed_benchmarks[entry_point.name] = entry_point.load() return installed_benchmarks diff --git a/paprika/utils.py b/paprika/utils.py index 8c8f88c..9d9d23f 100644 --- a/paprika/utils.py +++ b/paprika/utils.py @@ -126,7 +126,7 @@ def index_from_mask(structure, mask, amber_index=False): Returns ------- - indices : int + indices : List[int] Atom index or indices corresponding to the mask. """ From fbdddc1f48a583a3eaabca2220be7d499386da6c Mon Sep 17 00:00:00 2001 From: jeff231li Date: Tue, 11 Jul 2023 17:12:02 -0700 Subject: [PATCH 22/34] lint --- paprika/taproom/taproom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paprika/taproom/taproom.py b/paprika/taproom/taproom.py index 0fb7faf..264455c 100644 --- a/paprika/taproom/taproom.py +++ b/paprika/taproom/taproom.py @@ -1,6 +1,6 @@ +import importlib.metadata import logging -import importlib.metadata import yaml from paprika.taproom.utils import convert_string_to_quantity, de_alias From 571d80fb7c31019bdde01e13e15ece5978df512e Mon Sep 17 00:00:00 2001 From: jeff231li Date: Tue, 11 Jul 2023 17:19:54 -0700 Subject: [PATCH 23/34] include interchange in devtools --- devtools/conda-envs/test_env.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index c2c65eb..6d047c0 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -16,6 +16,7 @@ dependencies: - pyyaml - plumed - intermol + - openff-interchange >=0.3.7 - openff-units >=0.2.0 - openff-utilities From dc78db207fb97fac433d7356488ed2e7990f5b56 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Tue, 11 Jul 2023 17:32:30 -0700 Subject: [PATCH 24/34] update taproom code --- paprika/build/system/taproom.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index eb5ffbe..353a90d 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -532,11 +532,13 @@ def _apply_attach_restraints(self): ) # Apply restraints + system_path = f"{self._build_folder}/{self._host_code}-{self._guest_code}-dum-solv.xml" attach_windows = create_window_list(guest_restraints) Parallel(n_jobs=self._n_cpus)( delayed(self._apply_attach_to_system)( window, complex_path, + system_path, attach_folder, static_restraints, conformational_restraints, @@ -605,11 +607,13 @@ def _apply_pull_restraints(self): ) # Apply restraints + system_path = f"{self._build_folder}/{self._host_code}-{self._guest_code}-dum-solv.xml" pull_windows = create_window_list(guest_restraints) Parallel(n_jobs=self._n_cpus)( delayed(self._apply_pull_to_system)( window, complex_path, + system_path, pull_folder, static_restraints, conformational_restraints, @@ -678,6 +682,7 @@ def _apply_release_restraints(self): def _apply_attach_to_system( window, complex_path, + system_path, attach_folder, static_restraints, conformational_restraints, @@ -691,7 +696,6 @@ def _apply_attach_to_system( folder = f"{attach_folder}/{window}" os.makedirs(folder, exist_ok=True) - system_path = f"{folder}/restrained.xml" with open(system_path, "r") as file: system = openmm.XmlSerializer.deserialize(file.read()) @@ -716,6 +720,7 @@ def _apply_attach_to_system( def _apply_pull_to_system( window, complex_path, + system_path, pull_folder, static_restraints, conformational_restraints, @@ -727,7 +732,6 @@ def _apply_pull_to_system( folder = f"{pull_folder}/{window}" os.makedirs(folder, exist_ok=True) - system_path = f"{folder}/restrained.xml" with open(system_path, "r") as file: system = openmm.XmlSerializer.deserialize(file.read()) From 9bd5ac09f26f9a1b9fd7c70d7f1a712bff94de5f Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 11:39:30 -0700 Subject: [PATCH 25/34] update taproom modules --- paprika/build/system/taproom.py | 79 +++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index 353a90d..4066bcc 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -1,7 +1,7 @@ import json import os import shutil -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import mdtraj import numpy @@ -26,8 +26,11 @@ class BuildTaproomAPR: """A class to generate APR files from Taproom database in pAPRika. - TODO: Implement an option to create files with implicit solvent. - TODO: Implement an option to use GAFF force field. + As of now this class can generate files for a single host-guest pair with + explicit and implicit solvent based on OpenFF specifications. + + TODO: Implement an option to build an array of host-guest pairs from Taproom. + TODO: Implement an option to use GAFF force field (possible through the OpenMMForceFields package). Parameters ---------- @@ -36,7 +39,7 @@ class BuildTaproomAPR: guest_code: str The 3-letter Taproom code for the guest molecule. n_water: int - Number of Water molecules. + Number of Water molecules. If set as `0` or `None` then the system will be built without water. build_folder: str Temporary folder to save intermediate files. working_folder: str @@ -61,6 +64,21 @@ class BuildTaproomAPR: >>> system.extend_pull_distance(extend_by=6*unit.angstrom) >>> system.build_system() >>> + >>> # Create APR systems in a vacuum + >>> system = BuildTaproomAPR(host_code="bcd", guest_code="hex", n_water=None, force_field=force_field) + >>> system.build_system() + >>> + >>> # Create APR systems in OBC2 implicit solvent + >>> from pkg_resources import resource_filename + >>> GBSA = resource_filename( + >>> " openff.toolkit", + >>> os.path.join("data", "test_forcefields", "GBSA_OBC2-1.0.offxml"), + >>> ) + >>> force_field = ForceField("openff-2.0.0.offxml", GBSA) + >>> + >>> system = BuildTaproomAPR(host_code="bcd", guest_code="hex", n_water=None, force_field=force_field) + >>> system.build_system() + >>> >>> # Creating these files can take 10-20 minutes on one core. We can speed things up with more CPU cores >>> system.build_system(n_cpus=4) """ @@ -69,14 +87,18 @@ def __init__( self, host_code: str, guest_code: str, - n_water: int, + n_water: Union[int, None], force_field: ForceField, + use_taproom_mol2: bool = True, build_folder: str = "build_files", working_folder: str = "simulations", ): self._host_code = host_code self._guest_code = guest_code self._n_water = n_water + if n_water == 0 or n_water is None: + self._n_water = None + self._use_taproom_mol2 = use_taproom_mol2 self._force_field = force_field self._build_folder = build_folder self._working_folder = working_folder @@ -108,11 +130,12 @@ def _initialize(self): self._guest_yaml_schema = read_yaml_schema(self._guest_metadata["yaml"]) # Water Molecule - self._water_mol = Molecule.from_smiles("O") - self._water_intrcg = Interchange.from_smirnoff( - force_field=self._force_field, - topology=[self._water_mol] * self._n_water, - ) + if self._n_water is not None: + self._water_mol = Molecule.from_smiles("O") + self._water_intrcg = Interchange.from_smirnoff( + force_field=self._force_field, + topology=[self._water_mol] * self._n_water, + ) # Load restraints self._restraints = { @@ -203,19 +226,23 @@ def _solvate_and_add_dummy( solute_intrcg = Interchange.from_smirnoff( force_field=self._force_field, topology=solute_topology, - charge_from_molecules=unique_molecules, + charge_from_molecules=unique_molecules if self._use_taproom_mol2 else None, ) - solvated_topology = pack_box( - molecules=[self._water_mol], - number_of_copies=[self._n_water], - solute=solute_intrcg.topology, - box_shape=rectangular_box, - mass_density=0.95 * unit.grams / unit.milliliters, - center_solute="ORIGIN", - ) - solute_intrcg.box = solvated_topology.box_vectors - self._water_intrcg.box = solvated_topology.box_vectors - solvated_topology.to_file(solvated_path) + if self._n_water is not None: + solvated_topology = pack_box( + molecules=[self._water_mol], + number_of_copies=[self._n_water], + solute=solute_intrcg.topology, + box_shape=rectangular_box, + mass_density=0.95 * unit.grams / unit.milliliters, + center_solute="ORIGIN", + ) + solute_intrcg.box = solvated_topology.box_vectors + self._water_intrcg.box = solvated_topology.box_vectors + solvated_topology.to_file(solvated_path) + else: + solute_intrcg.box = None + solute_intrcg.topology.to_file(solvated_path) # 02 - Add Dummy Atoms to PDB input_structure = parmed.load_file(solvated_path, structure=True) @@ -248,8 +275,12 @@ def _solvate_and_add_dummy( ) # 05 - Combine interchange objects - system_intrcg = solute_intrcg + self._water_intrcg - system_intrcg.box = solvated_topology.box_vectors + if self._n_water is not None: + system_intrcg = solute_intrcg + self._water_intrcg + system_intrcg.box = solvated_topology.box_vectors + else: + system_intrcg = solute_intrcg + system_intrcg.box = None return system_intrcg From b44990f4c4b9e786c39ad87a0e5afca27e911692 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 11:47:06 -0700 Subject: [PATCH 26/34] update docstring --- paprika/build/system/taproom.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index 4066bcc..716fad2 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -30,7 +30,7 @@ class BuildTaproomAPR: explicit and implicit solvent based on OpenFF specifications. TODO: Implement an option to build an array of host-guest pairs from Taproom. - TODO: Implement an option to use GAFF force field (possible through the OpenMMForceFields package). + TODO: Implement an option to use the GAFF force field (possible through the OpenMMForceFields package). Parameters ---------- @@ -45,12 +45,12 @@ class BuildTaproomAPR: working_folder: str The main folder to write the APR structure files. - Example - ------- + Examples + -------- >>> from paprika.build.system import BuildTaproomAPR >>> from openff.toolkit import ForceField >>> - >>> # Select OpenFF force field version + >>> # Select OpenFF force field version 2.0.0 >>> force_field = ForceField("openff-2.0.0.offxml") >>> >>> # Initiate system object @@ -59,16 +59,16 @@ class BuildTaproomAPR: >>> # Build APR files >>> system.build_system() >>> - >>> # We can extend the default `r_final` specified in Taproom + >>> # We can also extend the default `r_final` specified in Taproom if it's not far enough >>> from openff.units import unit >>> system.extend_pull_distance(extend_by=6*unit.angstrom) >>> system.build_system() >>> - >>> # Create APR systems in a vacuum + >>> # We can create the APR system in a vacuum (useful if you want to add your own custom implicit solvent later on) >>> system = BuildTaproomAPR(host_code="bcd", guest_code="hex", n_water=None, force_field=force_field) >>> system.build_system() >>> - >>> # Create APR systems in OBC2 implicit solvent + >>> # We can create the APR system with the OBC2 implicit solvent >>> from pkg_resources import resource_filename >>> GBSA = resource_filename( >>> " openff.toolkit", @@ -79,7 +79,8 @@ class BuildTaproomAPR: >>> system = BuildTaproomAPR(host_code="bcd", guest_code="hex", n_water=None, force_field=force_field) >>> system.build_system() >>> - >>> # Creating these files can take 10-20 minutes on one core. We can speed things up with more CPU cores + >>> # Creating these files can take 10-20 minutes on one core (especially with explicit solvent). + >>> # We can speed things up running these in parallel. >>> system.build_system(n_cpus=4) """ From 976bd8058a882dd8e91a9eb104d57b6e8d123eb5 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 12:26:50 -0700 Subject: [PATCH 27/34] add stuff --- paprika/build/system/taproom.py | 57 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index 716fad2..cabafb7 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -38,6 +38,8 @@ class BuildTaproomAPR: The 3-letter Taproom code for the host molecule. guest_code: str The 3-letter Taproom code for the guest molecule. + host_guest_codes: dict + Selection of the host-guest pairs as a dictionary. n_water: int Number of Water molecules. If set as `0` or `None` then the system will be built without water. build_folder: str @@ -86,16 +88,24 @@ class BuildTaproomAPR: def __init__( self, - host_code: str, - guest_code: str, - n_water: Union[int, None], - force_field: ForceField, + host_code: Union[str, None] = None, + guest_code: Union[str, None] = None, + host_guest_codes: Union[Dict[str, List[str]], None] = None, + n_water: Union[int, None] = None, + force_field: Union[ForceField, None] = None, use_taproom_mol2: bool = True, build_folder: str = "build_files", working_folder: str = "simulations", + disable_progress: bool = True, ): + if force_field is None: + raise ValueError( + "The option `force_field` cannot be a None. Please specify an OpenFF `ForceField` object." + ) + self._host_code = host_code self._guest_code = guest_code + self._host_guest_code = host_guest_codes self._n_water = n_water if n_water == 0 or n_water is None: self._n_water = None @@ -103,6 +113,7 @@ def __init__( self._force_field = force_field self._build_folder = build_folder self._working_folder = working_folder + self._disable_progress = disable_progress self._host_metadata = None self._guest_metadata = None self._orientations = None @@ -110,10 +121,17 @@ def __init__( self._water_intrcg = None self._restraints = None self._n_cpus = 1 + self._build_array = False - self._initialize() + if self._host_code is not None and self._guest_code is not None: + self._initialize_single() + else: + self._build_array = True + raise NotImplementedError( + "Creating APR files for an array of host-guest pairs is not implemented yet." + ) - def _initialize(self): + def _initialize_single(self): # Create folder os.makedirs(self._build_folder, exist_ok=True) os.makedirs(self._working_folder, exist_ok=True) @@ -157,6 +175,10 @@ def _initialize(self): ), } + def _initialize_array(self): + """Not implemented yet.""" + pass + @staticmethod def _unnest_restraint_specs( restraint_specs: List[Dict[str, Any]] @@ -437,14 +459,14 @@ def _build_apr_structures(self): n_windows, [host_mol, guest_mol], ) - for i in tqdm(range(n_windows["pull"])) + for i in tqdm(range(n_windows["pull"]), disable=self._disable_progress) ) # --------------------------------------------------------------------- # # Prepare `attach` windows - Copy PDB from p000 # --------------------------------------------------------------------- # print("Generating files for the `attach` phase.") - for i in tqdm(range(n_windows["attach"])): + for i in tqdm(range(n_windows["attach"]), disable=self._disable_progress): folder = f"{self._working_folder}/attach-{orient}/a{i:03}" os.makedirs(folder, exist_ok=True) shutil.copy( @@ -491,7 +513,7 @@ def _build_apr_structures(self): # --------------------------------------------------------------------- # # Prepare `release` windows - Copy PDB # --------------------------------------------------------------------- # - for i in tqdm(range(n_windows["release"])): + for i in tqdm(range(n_windows["release"]), disable=self._disable_progress): folder = f"{self._working_folder}/release/r{i:03}" os.makedirs(folder, exist_ok=True) shutil.copy( @@ -578,7 +600,7 @@ def _apply_attach_restraints(self): symmetry_restraints, wall_restraints, ) - for window in tqdm(attach_windows) + for window in tqdm(attach_windows, disable=self._disable_progress) ) def _apply_pull_restraints(self): @@ -651,7 +673,7 @@ def _apply_pull_restraints(self): conformational_restraints, guest_restraints, ) - for window in tqdm(pull_windows) + for window in tqdm(pull_windows, disable=self._disable_progress) ) def _apply_release_restraints(self): @@ -707,7 +729,7 @@ def _apply_release_restraints(self): static_restraints, conformational_restraints, ) - for window in tqdm(release_windows) + for window in tqdm(release_windows, disable=self._disable_progress) ) @staticmethod @@ -854,10 +876,13 @@ def build_system(self, n_cpus: int = 1, clean_files: bool = False): self._n_cpus = n_cpus - self._build_apr_structures() - self._apply_attach_restraints() - self._apply_pull_restraints() - self._apply_release_restraints() + if not self._build_array: + self._build_apr_structures() + self._apply_attach_restraints() + self._apply_pull_restraints() + self._apply_release_restraints() + else: + self._build_apr_structures() if clean_files: shutil.rmtree(self._build_folder) From cca2037361de4a9746e3916484c2710b89158715 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 15:13:41 -0700 Subject: [PATCH 28/34] fix system preparation --- paprika/build/system/taproom.py | 87 ++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index cabafb7..a264e4c 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -3,7 +3,6 @@ import shutil from typing import Any, Dict, List, Union -import mdtraj import numpy import openmm import openmm.app as app @@ -15,6 +14,7 @@ from openff.units import unit from tqdm.auto import tqdm +from paprika.build import align from paprika.evaluator import Setup from paprika.io import PaprikaEncoder, save_restraints from paprika.restraints import DAT_restraint, create_window_list, parse_window @@ -212,8 +212,10 @@ def _solvate_and_add_dummy( self, complex_path: str, solvated_path: str, + host_resname: str, unique_molecules: List[Molecule], offset: float, + G1_mask: Union[str, None] = None, ): """Solvate a PDB file with PackMol through OpenFF-Interchange. @@ -268,7 +270,20 @@ def _solvate_and_add_dummy( solute_intrcg.topology.to_file(solvated_path) # 02 - Add Dummy Atoms to PDB - input_structure = parmed.load_file(solvated_path, structure=True) + input_structure = parmed.load_file( + solvated_path if self._n_water is not None else complex_path, + structure=True, + ) + if self._n_water is None: + input_structure = align.translate_to_origin( + input_structure, atom_mask=f":{host_resname}", weight="geo" + ) + + offset_array = numpy.array([0.0, 0.0, 0.0]) + if G1_mask is not None: + G1_coordinates = input_structure[G1_mask].coordinates + offset_array = numpy.array([0.0, 0.0, G1_coordinates[-1][-1]]) + Setup.add_dummy_atoms_to_structure( input_structure, dummy_atom_offsets=[ @@ -276,17 +291,18 @@ def _solvate_and_add_dummy( numpy.array([0, 0, -3.0 - offset]), numpy.array([0, 2.2, -5.2 - offset]), ], - offset_coordinates=numpy.zeros(3), + offset_coordinates=offset_array, ) # 03 - Shift structure to avoid issues with PBC - input_structure.coordinates += numpy.array( - [ - input_structure.box[0] * 0.5, - input_structure.box[1] * 0.5, - -input_structure.coordinates[-1, 2] + 5.0, - ] - ) + if self._n_water is not None: + input_structure.coordinates += numpy.array( + [ + input_structure.box[0] * 0.5, + input_structure.box[1] * 0.5, + -input_structure.coordinates[-1, 2] + 5.0, + ] + ) # 04 - Write PDB for solvated system with open(solvated_path, "w") as f: @@ -342,7 +358,9 @@ def _build_pull_structures( guest_atom_indices, guest_orientation_mask, pull_distance, + host_resname, offset, + G1_mask, n_windows, unique_molecules, ): @@ -376,8 +394,10 @@ def _build_pull_structures( host_guest_system_intrcg = self._solvate_and_add_dummy( complex_prepared_path, complex_solvated_path, + host_resname=host_resname, unique_molecules=unique_molecules, offset=offset, + G1_mask=G1_mask, ) if i == 0: @@ -388,7 +408,7 @@ def _build_pull_structures( ) # 04 - Clean up - os.remove(complex_prepared_path) + # os.remove(complex_prepared_path) def _build_apr_structures(self): """Build and prepare the APR structures and windows.""" @@ -414,7 +434,7 @@ def _build_apr_structures(self): # --------------------------------------------------------------------- # # Prepare Host-Guest Complex # --------------------------------------------------------------------- # - print("Generating files for the `pull` phase.") + # print("Generating files for the `pull` phase.") for orient in self._orientations: # 01 - Load complex structure complex_path = str( @@ -455,7 +475,9 @@ def _build_apr_structures(self): guest_atom_indices, guest_orientation_mask, pull_distance, + host_resname, offset, + G1, n_windows, [host_mol, guest_mol], ) @@ -465,7 +487,7 @@ def _build_apr_structures(self): # --------------------------------------------------------------------- # # Prepare `attach` windows - Copy PDB from p000 # --------------------------------------------------------------------- # - print("Generating files for the `attach` phase.") + # print("Generating files for the `attach` phase.") for i in tqdm(range(n_windows["attach"]), disable=self._disable_progress): folder = f"{self._working_folder}/attach-{orient}/a{i:03}" os.makedirs(folder, exist_ok=True) @@ -477,15 +499,32 @@ def _build_apr_structures(self): # --------------------------------------------------------------------- # # Prepare Host-only Structure # --------------------------------------------------------------------- # - print("Generating files for the `release` phase.") - # 01 - Remove Guest molecule from complex structure + # print("Generating files for the `release` phase.") + # 01 - Remove Guest molecule and Dummy atoms from complex structure complex_solvate_path = f"{self._working_folder}/pull-p/p000/restrained.pdb" - complex_structure = parmed.load_file(complex_solvate_path, structure=True) - host_atom_indices = index_from_mask(complex_structure, mask=f":{host_resname}") + host_pdb = app.PDBFile( + str( + self._host_metadata["path"].joinpath( + self._host_yaml_schema["structure"]["pdb"] + ) + ) + ) + pdbfile = app.PDBFile(complex_solvate_path) + modeller = app.Modeller(pdbfile.topology, pdbfile.positions) + guest_atoms = [ + atom + for atom in pdbfile.topology.atoms() + if atom.residue.name != host_resname + ] + modeller.delete(guest_atoms) - mdtraj_trajectory = mdtraj.load_pdb(complex_solvate_path) - host_trajectory = mdtraj_trajectory.atom_slice(host_atom_indices) - host_trajectory.save(f"{self._build_folder}/host_input.pdb") + with open(f"{self._build_folder}/host_input.pdb", "w") as f: + app.PDBFile.writeFile( + host_pdb.topology, + modeller.positions, + f, + keepIds=False, + ) # 02 - Align host molecule host_structure = Setup.prepare_host_structure( @@ -502,8 +541,10 @@ def _build_apr_structures(self): host_system_intrcg = self._solvate_and_add_dummy( output_coordinate_path, host_solvated_path, + host_resname=host_resname, unique_molecules=[host_mol], offset=offset, + G1_mask=None, ) # 04 - Create Host-only OpenMM System with Dummy Atoms @@ -524,7 +565,7 @@ def _build_apr_structures(self): def _apply_attach_restraints(self): """Apply restraints for the `attach` phase.""" - print("Applying restraints for `attach` phase...") + # print("Applying restraints for `attach` phase...") attach_lambdas = self._host_yaml_schema["calculation"]["lambda"]["attach"] n_windows = self._host_yaml_schema["calculation"]["windows"] @@ -606,7 +647,7 @@ def _apply_attach_restraints(self): def _apply_pull_restraints(self): """Apply restraints for the `pull` phase.""" - print("Applying restraints for `pull` phase...") + # print("Applying restraints for `pull` phase...") attach_lambdas = self._host_yaml_schema["calculation"]["lambda"]["attach"] n_windows = self._host_yaml_schema["calculation"]["windows"] @@ -679,7 +720,7 @@ def _apply_pull_restraints(self): def _apply_release_restraints(self): """Apply restraints for the `release` phase.""" - print("Applying restraints for `release` phase...") + # print("Applying restraints for `release` phase...") release_lambdas = self._host_yaml_schema["calculation"]["lambda"]["release"] n_windows = self._host_yaml_schema["calculation"]["windows"] From bf29ead9c710e5e326c3b6be33c0cfbeee261f89 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 15:16:43 -0700 Subject: [PATCH 29/34] uncomment cleanup --- paprika/build/system/taproom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index a264e4c..94d4ff3 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -408,7 +408,7 @@ def _build_pull_structures( ) # 04 - Clean up - # os.remove(complex_prepared_path) + os.remove(complex_prepared_path) def _build_apr_structures(self): """Build and prepare the APR structures and windows.""" From 378fd85a3e1f45d23e70ff51498b334ff409d236 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 31 Jul 2023 22:29:22 -0700 Subject: [PATCH 30/34] fix bug --- paprika/build/system/taproom.py | 50 +++++++++++++++++---------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/paprika/build/system/taproom.py b/paprika/build/system/taproom.py index 94d4ff3..7407912 100644 --- a/paprika/build/system/taproom.py +++ b/paprika/build/system/taproom.py @@ -214,8 +214,8 @@ def _solvate_and_add_dummy( solvated_path: str, host_resname: str, unique_molecules: List[Molecule], - offset: float, - G1_mask: Union[str, None] = None, + initial_distance: float, + offset_mask: Union[str, None] = None, ): """Solvate a PDB file with PackMol through OpenFF-Interchange. @@ -227,8 +227,10 @@ def _solvate_and_add_dummy( The output file path for the solvated complex. unique_molecules: List[Molecule] List of unique molecules (to generate the OpenFF Topology) - offset: float + initial_distance: float An offset to place the dummy atoms. + offset_mask: str + The AMBER atom mask for G1 - used for offsetting Dummy atoms Returns ------- @@ -280,16 +282,16 @@ def _solvate_and_add_dummy( ) offset_array = numpy.array([0.0, 0.0, 0.0]) - if G1_mask is not None: - G1_coordinates = input_structure[G1_mask].coordinates - offset_array = numpy.array([0.0, 0.0, G1_coordinates[-1][-1]]) + if offset_mask is not None: + coordinates_z = input_structure[offset_mask].coordinates[-1][-1] + offset_array = numpy.array([0.0, 0.0, coordinates_z]) Setup.add_dummy_atoms_to_structure( input_structure, dummy_atom_offsets=[ - numpy.array([0, 0, -offset]), - numpy.array([0, 0, -3.0 - offset]), - numpy.array([0, 2.2, -5.2 - offset]), + numpy.array([0, 0, -initial_distance]), + numpy.array([0, 0, -3.0 - initial_distance]), + numpy.array([0, 2.2, -5.2 - initial_distance]), ], offset_coordinates=offset_array, ) @@ -357,10 +359,9 @@ def _build_pull_structures( complex_path, guest_atom_indices, guest_orientation_mask, - pull_distance, + pulling_distance, host_resname, - offset, - G1_mask, + initial_distance, n_windows, unique_molecules, ): @@ -374,7 +375,7 @@ def _build_pull_structures( complex_path, guest_atom_indices, guest_orientation_mask, - pull_distance=pull_distance, + pull_distance=pulling_distance, pull_window_index=i, n_pull_windows=n_windows["pull"], ) @@ -390,14 +391,16 @@ def _build_pull_structures( ) # 02 - Solvate structure + r_i = numpy.linspace(0.0, pulling_distance, n_windows["pull"])[i] complex_solvated_path = f"{folder}/restrained.pdb" + offset_mask = guest_orientation_mask.split(" ")[0] host_guest_system_intrcg = self._solvate_and_add_dummy( complex_prepared_path, complex_solvated_path, host_resname=host_resname, unique_molecules=unique_molecules, - offset=offset, - G1_mask=G1_mask, + initial_distance=initial_distance + r_i, + offset_mask=offset_mask, ) if i == 0: @@ -459,10 +462,10 @@ def _build_apr_structures(self): r_final = self._guest_yaml_schema["restraints"]["guest"][0]["restraint"][ "pull" ]["target"] - pull_distance = (r_final - r_initial).m_as(unit.angstrom) - offset = self._guest_yaml_schema["restraints"]["guest"][0]["restraint"][ - "attach" - ]["target"].m_as(unit.angstrom) + pulling_distance = (r_final - r_initial).m_as(unit.angstrom) + initial_distance = self._guest_yaml_schema["restraints"]["guest"][0][ + "restraint" + ]["attach"]["target"].m_as(unit.angstrom) # --------------------------------------------------------------------- # # Prepare `pull` windows @@ -474,10 +477,9 @@ def _build_apr_structures(self): complex_path, guest_atom_indices, guest_orientation_mask, - pull_distance, + pulling_distance, host_resname, - offset, - G1, + initial_distance, n_windows, [host_mol, guest_mol], ) @@ -543,8 +545,8 @@ def _build_apr_structures(self): host_solvated_path, host_resname=host_resname, unique_molecules=[host_mol], - offset=offset, - G1_mask=None, + initial_distance=initial_distance, + offset_mask=None, ) # 04 - Create Host-only OpenMM System with Dummy Atoms From b72b25cfca03cc094ade6e31d9c4e1af28298878 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 28 Aug 2023 14:46:40 -0700 Subject: [PATCH 31/34] allow getting taproom with different Python versions --- paprika/taproom/taproom.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/paprika/taproom/taproom.py b/paprika/taproom/taproom.py index 264455c..81cdcca 100644 --- a/paprika/taproom/taproom.py +++ b/paprika/taproom/taproom.py @@ -1,10 +1,16 @@ -import importlib.metadata import logging +import sys import yaml from paprika.taproom.utils import convert_string_to_quantity, de_alias +if sys.version_info.minor < 10: + from pkg_resources import iter_entry_points as entry_points +else: + import importlib.metadata.entry_points as entry_points + + logger = logging.getLogger(__name__) @@ -14,7 +20,7 @@ def get_benchmarks(): """ installed_benchmarks = {} - for entry_point in importlib.metadata.entry_points(group="taproom.benchmarks"): + for entry_point in entry_points(group="taproom.benchmarks"): installed_benchmarks[entry_point.name] = entry_point.load() return installed_benchmarks From 8adc38ae2ee2b5cdc804a8dbb29560999f07f3d3 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Mon, 28 Aug 2023 15:47:41 -0700 Subject: [PATCH 32/34] add tutorial for generating taproom systems --- .../08-generating-taproom-systems.ipynb | 872 ++++++++++++++++++ 1 file changed, 872 insertions(+) create mode 100644 docs/tutorials/08-generating-taproom-systems.ipynb diff --git a/docs/tutorials/08-generating-taproom-systems.ipynb b/docs/tutorials/08-generating-taproom-systems.ipynb new file mode 100644 index 0000000..aea518f --- /dev/null +++ b/docs/tutorials/08-generating-taproom-systems.ipynb @@ -0,0 +1,872 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4fa4e04a-c418-46d6-b4c0-92c4f20a484a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "LICENSE: No product keys!\n", + "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bfebf13af9904306a958d6a4999d0977", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import os\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(action=\"ignore\")\n", + "\n", + "from openff.toolkit.typing.engines.smirnoff import ForceField\n", + "from paprika.build.system.taproom import BuildTaproomAPR\n", + "from pkg_resources import resource_filename\n", + "from tqdm.auto import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e9e0c9bd-1771-4c52-9c46-b5ece3d8c7c2", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"INTERCHANGE_EXPERIMENTAL\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "id": "e5140dbf", + "metadata": {}, + "source": [ + "* The `BuildTaproomAPR` class requires the OpenFF-Interchange modules. Install in your conda environment with:\n", + "\n", + "`conda install -c conda-forge openff-interchange`" + ] + }, + { + "cell_type": "markdown", + "id": "ac82a11a", + "metadata": {}, + "source": [ + "## 01) Build System in explicit solvent" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b8c3ab15", + "metadata": {}, + "outputs": [], + "source": [ + "# Define force field (OpenFF)\n", + "force_field = ForceField(\"openff-2.0.0.offxml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "50986520", + "metadata": {}, + "outputs": [], + "source": [ + "system = BuildTaproomAPR(\n", + " host_code=\"acd\",\n", + " guest_code=\"bam\",\n", + " n_water=3000,\n", + " force_field=force_field,\n", + " working_folder=\"explicit_solvent\",\n", + " disable_progress=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2c92a6bc", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5b28227a1daf4aa09543454f6355a51b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/46 [00:00:228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e0146ce9477042388639352d4e24e76e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/15 [00:00 Date: Tue, 29 Aug 2023 07:51:42 -0700 Subject: [PATCH 33/34] update tutorial --- .../08-generating-taproom-systems.ipynb | 197 ++++++++++-------- 1 file changed, 108 insertions(+), 89 deletions(-) diff --git a/docs/tutorials/08-generating-taproom-systems.ipynb b/docs/tutorials/08-generating-taproom-systems.ipynb index aea518f..f4ec791 100644 --- a/docs/tutorials/08-generating-taproom-systems.ipynb +++ b/docs/tutorials/08-generating-taproom-systems.ipynb @@ -26,7 +26,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "bfebf13af9904306a958d6a4999d0977", + "model_id": "61ce7d2e2022466cb18b85a279cb6b52", "version_major": 2, "version_minor": 0 }, @@ -43,9 +43,10 @@ "warnings.filterwarnings(action=\"ignore\")\n", "\n", "from openff.toolkit.typing.engines.smirnoff import ForceField\n", - "from paprika.build.system.taproom import BuildTaproomAPR\n", "from pkg_resources import resource_filename\n", - "from tqdm.auto import tqdm" + "from tqdm.auto import tqdm\n", + "\n", + "from paprika.build.system.taproom import BuildTaproomAPR" ] }, { @@ -78,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "b8c3ab15", "metadata": {}, "outputs": [], @@ -89,14 +90,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "50986520", "metadata": {}, "outputs": [], "source": [ "system = BuildTaproomAPR(\n", - " host_code=\"acd\",\n", - " guest_code=\"bam\",\n", + " host_code=\"bcd\",\n", + " guest_code=\"hex\",\n", " n_water=3000,\n", " force_field=force_field,\n", " working_folder=\"explicit_solvent\",\n", @@ -106,14 +107,16 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "2c92a6bc", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5b28227a1daf4aa09543454f6355a51b", + "model_id": "a34b780a4b4243da857665d4bcd0ecec", "version_major": 2, "version_minor": 0 }, @@ -124,10 +127,80 @@ "metadata": {}, "output_type": "display_data" }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", + "LICENSE: N.B. OE_LICENSE environment variable is not set\n", + "LICENSE: N.B. OE_DIR environment variable is not set\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "LICENSE: No product keys!\n", + "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", + "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/_experimental.py:35: UserWarning: Interchange object combination is experimental and likely to produce strange results. Any workflow using this method is not guaranteed to be suitable for production. Use with extreme caution and thoroughly validate results!\n", + " return func(*args, **kwargs)\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/_experimental.py:35: UserWarning: Interchange object combination is experimental and likely to produce strange results. Any workflow using this method is not guaranteed to be suitable for production. Use with extreme caution and thoroughly validate results!\n", + " return func(*args, **kwargs)\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/_experimental.py:35: UserWarning: Interchange object combination is experimental and likely to produce strange results. Any workflow using this method is not guaranteed to be suitable for production. Use with extreme caution and thoroughly validate results!\n", + " return func(*args, **kwargs)\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/_experimental.py:35: UserWarning: Interchange object combination is experimental and likely to produce strange results. Any workflow using this method is not guaranteed to be suitable for production. Use with extreme caution and thoroughly validate results!\n", + " return func(*args, **kwargs)\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/components/interchange.py:844: UserWarning: Setting positions to None because one or both objects added together were missing positions.\n", + " warnings.warn(\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/components/interchange.py:844: UserWarning: Setting positions to None because one or both objects added together were missing positions.\n", + " warnings.warn(\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/components/interchange.py:844: UserWarning: Setting positions to None because one or both objects added together were missing positions.\n", + " warnings.warn(\n", + "/Users/jsetiadi/opt/anaconda3/envs/paprika-dev/lib/python3.9/site-packages/openff/interchange/components/interchange.py:844: UserWarning: Setting positions to None because one or both objects added together were missing positions.\n", + " warnings.warn(\n" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "93eef04651894158afa9faf328595ede", + "model_id": "e60193dde26d4eee918037f8300d7367", "version_major": 2, "version_minor": 0 }, @@ -141,7 +214,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9b8b9ebc8404461198677831fc64dc6c", + "model_id": "2671d71b2f0f40d8b6724b5e228d60ce", "version_major": 2, "version_minor": 0 }, @@ -155,7 +228,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "26e81193bac546999271dba5f7e10da4", + "model_id": "f154c1725a6a49eba82ffde6f2966186", "version_major": 2, "version_minor": 0 }, @@ -169,7 +242,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "23506c11292f44db85275262c28000c2", + "model_id": "9dd0d6617abc4b5b823f8616299a25bb", "version_major": 2, "version_minor": 0 }, @@ -195,7 +268,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f9d6baa0fab94fbe9b68a0649c3606a3", + "model_id": "6cdea4e5c3704184927a82cee5ab9a8b", "version_major": 2, "version_minor": 0 }, @@ -221,7 +294,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e8a024f3a5cc49d783e175b31be9bc64", + "model_id": "d4699034b0c0404b95ec767fc87a4210", "version_major": 2, "version_minor": 0 }, @@ -247,7 +320,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "162288f9d4584838b78a262922b3f69d", + "model_id": "b59dfe7ecd0a45f28d5becc7db5908ce", "version_major": 2, "version_minor": 0 }, @@ -273,7 +346,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9d62168fd87340b6bab869bb81b0c484", + "model_id": "14d7a0a100b6420d94995dffbaa98caf", "version_major": 2, "version_minor": 0 }, @@ -299,7 +372,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c44a82966dfa456e94f0e25e1ee396fe", + "model_id": "279153e7188a485aba43293c7204e539", "version_major": 2, "version_minor": 0 }, @@ -326,7 +399,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "id": "cde8cd8e", "metadata": {}, "outputs": [], @@ -341,7 +414,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "id": "57d2f8af-9a34-4395-9b72-aaea82c0b7d6", "metadata": {}, "outputs": [], @@ -349,7 +422,7 @@ "system = BuildTaproomAPR(\n", " host_code=\"acd\",\n", " guest_code=\"bam\",\n", - " n_water=None,\n", + " n_water=None, #<-- Set this to None to build system without water molecules\n", " force_field=force_field,\n", " working_folder=\"implicit_solvent\",\n", " disable_progress=False,\n", @@ -358,14 +431,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "04dbf25d-01cd-4bbd-b8e3-93576869edc4", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "124183e38fe14fb9b4def6134cd3a2f2", + "model_id": "d591dc780fd54b77af9194779893874b", "version_major": 2, "version_minor": 0 }, @@ -376,64 +449,10 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", - "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", - "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", - "Warning on use of the timeseries module: If the inherent timescales of the system are long compared to those being analyzed, this statistical inefficiency may be an underestimate. The estimate presumes the use of many statistically independent samples. Tests should be performed to assess whether this condition is satisfied. Be cautious in the interpretation of the data.\n", - ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", - ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", - ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", - ":228: RuntimeWarning: scipy._lib.messagestream.MessageStream size changed, may indicate binary incompatibility. Expected 56 from C header, got 64 from PyObject\n", - "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", - "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", - "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", - "Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead.\n", - "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", - "LICENSE: N.B. OE_LICENSE environment variable is not set\n", - "LICENSE: N.B. OE_DIR environment variable is not set\n", - "LICENSE: No product keys!\n", - "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", - "LICENSE: N.B. OE_LICENSE environment variable is not set\n", - "LICENSE: N.B. OE_DIR environment variable is not set\n", - "LICENSE: No product keys!\n", - "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", - "LICENSE: N.B. OE_LICENSE environment variable is not set\n", - "LICENSE: N.B. OE_DIR environment variable is not set\n", - "LICENSE: No product keys!\n", - "LICENSE: Could not open license file \"oe_license.txt\" in local directory\n", - "LICENSE: N.B. OE_LICENSE environment variable is not set\n", - "LICENSE: N.B. OE_DIR environment variable is not set\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "LICENSE: No product keys!\n", - "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", - "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", - "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", - "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", - "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", - "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n", - "The OpenEye Toolkits are found to be installed but not licensed and therefore will not be used.\n", - "The OpenEye Toolkits require a (free for academics) license, see https://docs.eyesopen.com/toolkits/python/quickstart-python/license.html\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e0146ce9477042388639352d4e24e76e", + "model_id": "b46e350fae79469c8fe2bcf0e162305d", "version_major": 2, "version_minor": 0 }, @@ -447,7 +466,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8e45dc05e2064cd38061a6d59fe876e3", + "model_id": "ddf0769669984debadbefe190f55604f", "version_major": 2, "version_minor": 0 }, @@ -461,7 +480,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9a99d6de204c4fa39b50682d9bf67051", + "model_id": "d2590d4315d34d93b98fb47c42322efa", "version_major": 2, "version_minor": 0 }, @@ -475,7 +494,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e205590cb02f49ddbaa340e6a3decd00", + "model_id": "54fe2e5469134d1ca9614762f0b22d88", "version_major": 2, "version_minor": 0 }, @@ -501,7 +520,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0215efe498464534a90ba81719c87fed", + "model_id": "af06f67277924263ae9daa9e773ac5ba", "version_major": 2, "version_minor": 0 }, @@ -527,7 +546,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "dc0618eb40e648bdb018ed44a6222131", + "model_id": "5371bacaa4094ea98bd33e64817b0820", "version_major": 2, "version_minor": 0 }, @@ -553,7 +572,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "946bfd9e55d64a99930e3200bcb0fb13", + "model_id": "eec351bc51394e27affd30945a960a6a", "version_major": 2, "version_minor": 0 }, @@ -579,7 +598,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6f46d63d9c994952916c1011c655c094", + "model_id": "9d01adff2f0a4f8fa8c3fc8cedae04b2", "version_major": 2, "version_minor": 0 }, @@ -605,7 +624,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d67faebdba754dbfa90b049772e6acdf", + "model_id": "8df34eafc2d446249997447aa911f602", "version_major": 2, "version_minor": 0 }, @@ -645,14 +664,14 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "4fa40b02", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "89f86a66a12d49c484b61bf949be46f8", + "model_id": "89f741a6f65a4b85a06c7a71cfc7945f", "version_major": 2, "version_minor": 0 }, From 65e47354bf6966297a023ef6278e0d1f584d7e18 Mon Sep 17 00:00:00 2001 From: jeff231li Date: Wed, 30 Aug 2023 10:02:54 -0700 Subject: [PATCH 34/34] fix import bug --- paprika/taproom/taproom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paprika/taproom/taproom.py b/paprika/taproom/taproom.py index 81cdcca..6a52045 100644 --- a/paprika/taproom/taproom.py +++ b/paprika/taproom/taproom.py @@ -8,7 +8,7 @@ if sys.version_info.minor < 10: from pkg_resources import iter_entry_points as entry_points else: - import importlib.metadata.entry_points as entry_points + from importlib.metadata import entry_points logger = logging.getLogger(__name__)