Skip to content

Commit

Permalink
DDX solvent support (#290)
Browse files Browse the repository at this point in the history
* add ddx solvent support

* make sure the correct solvent is being used

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* PR feedback extend ddx test

* fix merge

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update notes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
jthorton and pre-commit-ci[bot] authored Jul 22, 2024
1 parent 14154d6 commit 31e5a36
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 13 deletions.
1 change: 1 addition & 0 deletions devtools/conda-envs/psi4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies:
- openeye-toolkits !=2024.1.1

- psi4 >=1.9.1
- pyddx

### Core dependencies.

Expand Down
2 changes: 2 additions & 0 deletions docs/releasehistory.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Releases are given with dates in DD-MM-YYYY format.
* [PR #284:] Add `portal_client_manager` for using custom `PortalClient` settings
* [PR #288:] Adds better support for property driver psi4 calculations which require the keywords to be formatted differently (see [this issue](https://github.com/psi4/psi4/issues/3129) for an example which can be run locally). This also allows for response properties to be calculated such as the dipole polarizabilities, which is included as a new property type.
* [PR #289:] Add `workflow_components.RECAPFragmenter` to fragment molecules using the rdkit implementation of RECAP [@jthorton]
* [PR #290:] Add support for the DDX implicit solvent interface in Psi4 [@jthorton]

## 0.51.0 / 23-04-2024

Expand Down Expand Up @@ -144,6 +145,7 @@ For more information on this release, see https://github.com/openforcefield/open
[PR #285:]: https://github.com/openforcefield/openff-qcsubmit/pull/285
[PR #288:]: https://github.com/openforcefield/openff-qcsubmit/pull/288
[PR #289:]: https://github.com/openforcefield/openff-qcsubmit/pull/289
[PR #290:]: https://github.com/openforcefield/openff-qcsubmit/pull/290

[@jthorton]: https://github.com/jthorton
[@dotsdl]: https://github.com/dotsdl
Expand Down
38 changes: 33 additions & 5 deletions openff/qcsubmit/_tests/test_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from openff.qcsubmit import workflow_components
from openff.qcsubmit.common_structures import (
DDXSettings,
MoleculeAttributes,
PCMSettings,
SCFProperties,
Expand Down Expand Up @@ -345,7 +346,32 @@ def test_basic_submissions_multiple_spec(fulltest_client, conformer_water):
) == spec.dict(include={"method", "program", "basis"})


def test_basic_submissions_single_pcm_spec(fulltest_client, water):
@pytest.mark.parametrize(
"solvent_model, solvent_energy, solvent_evidence",
[
pytest.param(
PCMSettings(units="au", medium_Solvent="water"),
"pcm polarization energy",
"Solvent name: Water",
id="PCM",
),
pytest.param(
DDXSettings(ddx_solvent_epsilon=4),
"dd solvation energy",
"solvent_epsilon = 4.0",
id="DDX Epsilon",
),
pytest.param(
DDXSettings(ddx_solvent="1-bromooctane"),
"dd solvation energy",
"solvent_epsilon = 5.0244",
id="DDX Solvent",
),
],
)
def test_basic_submissions_single_solvent_spec(
fulltest_client, solvent_model, solvent_energy, solvent_evidence, water
):
"""Test submitting a basic dataset to a snowflake server with pcm water in the specification."""

client = fulltest_client
Expand All @@ -361,7 +387,7 @@ def test_basic_submissions_single_pcm_spec(fulltest_client, water):
program=program,
spec_name="default",
spec_description="testing the single points with pcm",
implicit_solvent=PCMSettings(units="au", medium_Solvent="water"),
implicit_solvent=solvent_model,
overwrite=True,
)

Expand Down Expand Up @@ -409,10 +435,12 @@ def test_basic_submissions_single_pcm_spec(fulltest_client, water):
assert record.error is None
assert record.return_result is not None
# make sure the PCM result was captured
assert record.properties["pcm polarization energy"] < 0
assert record.properties[solvent_energy] < 0
# make sure the correct solvent was used
assert solvent_evidence in record.stdout
assert record.specification.dict(
include={"method", "program", "basis"}
) == spec.dict(include={"method", "program", "basis"})
include={"method", "basis", "program"}
) == spec.dict(include={"method", "basis", "program"})


@pytest.mark.parametrize(
Expand Down
70 changes: 62 additions & 8 deletions openff/qcsubmit/common_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This file contains common starting structures which can be mixed into datasets, results and factories.
"""

import abc
import copy
import getpass
import re
Expand All @@ -14,6 +15,7 @@
ClassVar,
Dict,
List,
Literal,
Mapping,
Optional,
Set,
Expand All @@ -40,6 +42,7 @@
BaseModel,
Field,
HttpUrl,
PositiveFloat,
PositiveInt,
StrictBool,
StrictFloat,
Expand All @@ -53,6 +56,7 @@
BaseModel,
Field,
HttpUrl,
PositiveFloat,
PositiveInt,
StrictBool,
StrictFloat,
Expand Down Expand Up @@ -189,7 +193,18 @@ class TDSettings(DatasetConfig):
)


class PCMSettings(ResultsConfig):
class _BaseSolvent(ResultsConfig, abc.ABC):
"""
A base class to add new implicit solvent models
"""

@abc.abstractmethod
def add_keywords(self, keyword_data: dict) -> dict:
"""Add the keywords of this solvent model to the keyword data dict."""
...


class PCMSettings(_BaseSolvent):
"""
A class to handle PCM settings which can be used with PSi4.
"""
Expand Down Expand Up @@ -401,6 +416,46 @@ def to_string(self) -> str:
Cavity {{{cavity_str}}}"""
return pcm_string

def add_keywords(self, keyword_data: dict) -> dict:
keyword_data["pcm"] = True
keyword_data["pcm__input"] = self.to_string()
return keyword_data


class DDXSettings(_BaseSolvent):
"""
A simple settings class for the ddx solvent model to be used with Psi4
"""

ddx_model: Literal["pcm", "cosmo"] = Field(
"pcm", description="The solvation model to use."
)
ddx_radii_scaling: PositiveFloat = Field(
1.1,
description="The scaling factor for the cavity spheres. This also depends on the radii set chosen.",
)
ddx_radii_set: Literal["uff", "bondi"] = Field(
"uff", description="The atomic radii set to use."
)
ddx_solvent_epsilon: Optional[float] = Field(
None,
description="The dielectric constant of the solvent, if not specified the solvent type should be set.",
)
ddx_solvent: str = Field(
"water",
description="The name of the ddx supported solvent which should be used, the epsilon value will be determined "
"from `pyddx.data.solvent_epsilon`. Note that this value is ignored if the `ddx_solvent_epsilon` "
"is provided.",
)

def add_keywords(self, keyword_data: dict) -> dict:
keyword_data["ddx"] = True
ddx_data = self.dict()
if self.ddx_solvent_epsilon is None:
del ddx_data["ddx_solvent_epsilon"]
keyword_data.update(ddx_data)
return keyword_data


class QCSpec(ResultsConfig):
method: constr(strip_whitespace=True) = Field(
Expand All @@ -427,9 +482,9 @@ class QCSpec(ResultsConfig):
WavefunctionProtocolEnum.none,
description="The level of wavefunction detail that should be saved in QCArchive. Note that this is done for every calculation and should not be used with optimizations.",
)
implicit_solvent: Optional[PCMSettings] = Field(
implicit_solvent: Optional[Union[PCMSettings, DDXSettings]] = Field(
None,
description="If PCM is to be used with psi4 this is the full description of the settings that should be used.",
description="If PCM or DDX is to be used with psi4 this is the full description of the settings that should be used.",
)
maxiter: PositiveInt = Field(
200,
Expand Down Expand Up @@ -526,7 +581,7 @@ def __init__(
# make sure PCM is not set
if implicit_solvent is not None:
raise QCSpecificationError(
"PCM can only be used with PSI4 please set implicit solvent to None."
"PCM and DDX can only be used with PSI4 please set implicit solvent to None."
)
# we need to make sure it is valid in the above list
program_settings = settings.get(program.lower(), None)
Expand Down Expand Up @@ -612,10 +667,9 @@ def qc_keywords(self, properties: bool = False) -> Dict[str, Any]:
if self.implicit_solvent is not None:
if self.program.lower() != "psi4":
raise QCSpecificationError(
"PCM can only be used with PSI4 please set implicit solvent to None."
"PCM and DDX can only be used with PSI4 please set implicit solvent to None."
)
data["pcm"] = True
data["pcm__input"] = self.implicit_solvent.to_string()
data = self.implicit_solvent.add_keywords(keyword_data=data)
return data


Expand Down Expand Up @@ -670,7 +724,7 @@ def add_qc_spec(
spec_description: str,
store_wavefunction: str = "none",
overwrite: bool = False,
implicit_solvent: Optional[PCMSettings] = None,
implicit_solvent: Optional[Union[PCMSettings, DDXSettings]] = None,
maxiter: PositiveInt = 200,
scf_properties: Optional[List[SCFProperties]] = None,
keywords: Optional[
Expand Down

0 comments on commit 31e5a36

Please sign in to comment.