Skip to content

Commit 14f14bf

Browse files
Qcarchive update (#187)
* updated qcharcive code to fetch OpenFF Full Optimization Benchmark 1 to be compatible with qcportal >=0.5 * Updated torsiondrive parsing. I'm not sure this has sufficient testing. * Adding in some testing of the torsion function * Adding in some testing of the torsion function. * Updated collection type name. * fixed import issue in test * fixed parsing of the schema. * fixing a typo that was causing failure of torsion test. * Slight change to code to add in a function that uses the iterate_records and iterates_entries functions * merged with updated dgl update; removing qcportal pinning to the old version. * Made spec_name be a variable * Adding in some docstrings * Addressed Mike's comment.s * Added additional basic docstring. * Added additional basic docstring for torsion parsing * Fixed bug in iterate function; added test to catch that bug . * Added support for singlepoint datasets * fixing error in test. * Changing the dataset for singlepoint testing as we need to ensure the dataset has the smiles encoded for converting to openff.molecule * Changing the dataset for singlepoint testing as we need to ensure the dataset has the smiles encoded for converting to openff.molecule * Move the schema conversion to after checking if a dataset is supported so that it will raise the desired exception rather than failing. * Removed support for singlepoint dataset, as openff.molecule cannot parse the singlepoint records properly at this point. Other issues need to be resolved with singlepoint energy beyond this (i.e., summation of dispersion corrections). * Removed support for singlepoint dataset, as openff.molecule cannot parse the singlepoint records properly at this point. Other issues need to be resolved with singlepoint energy beyond this (i.e., summation of dispersion corrections). This PR should sufficiently reproduce the prior behavior, but with new qcportal. * minor edits. * fixing numpy comparison; adding in torchdata to environment * fixing failing tests * adding in skip test rather than return statement for when a simulation fails --------- Co-authored-by: Mike Henry <11765982+mikemhenry@users.noreply.github.com>
1 parent 540c99b commit 14f14bf

8 files changed

Lines changed: 269 additions & 64 deletions

File tree

devtools/conda-envs/espaloma.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ dependencies:
1919
- openmmforcefields >=0.11.2
2020
- tqdm
2121
- pydantic <2 # We need our deps to fix this
22+
- qcportal >=0.50
2223
- dgl =2.3.0
23-
- qcportal =0.15.8
24+
- torchdata <= 0.10.0
2425
# Testing
2526
- pytest
2627
- pytest-cov
@@ -30,6 +31,5 @@ dependencies:
3031
- nose
3132
- nose-timer
3233
- coverage
33-
- qcportal>=0.15.0
3434
- sphinx
35-
- sphinx_rtd_theme
35+
- sphinx_rtd_theme

devtools/conda-recipe/meta.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ requirements:
3636
- nose-timer
3737
- coverage
3838
- qcportal
39+
- torchdata <= 0.10.0
3940

4041
about:
4142
home: https://github.com/choderalab/perses

espaloma/data/qcarchive_utils.py

Lines changed: 166 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Tuple
66

77
import numpy as np
8-
import qcportal as ptl
8+
import qcportal
99
import torch
1010
from openmm import unit
1111
from openmm.unit import Quantity
@@ -21,48 +21,92 @@
2121
# =============================================================================
2222
# UTILITY FUNCTIONS
2323
# =============================================================================
24-
def get_client():
25-
return ptl.FractalClient()
24+
def get_client(url: str = "api.qcarchive.molssi.org") -> qcportal.client.PortalClient:
25+
"""
26+
Returns a instance of the qcportal client.
27+
28+
Parameters
29+
----------
30+
url: str, default="api.qcarchive.molssi.org"
31+
qcportal instance to connect
32+
33+
Returns
34+
-------
35+
qcportal.client.PortalClient
36+
qcportal client instance.
37+
"""
38+
# Note, this may need to be modified to include username/password for non-public servers
39+
return qcportal.PortalClient(url)
2640

2741

2842
def get_collection(
29-
client,
30-
collection_type="OptimizationDataset",
31-
name="OpenFF Full Optimization Benchmark 1",
43+
client,
44+
collection_type="optimization",
45+
name="OpenFF Full Optimization Benchmark 1",
3246
):
33-
collection = client.get_collection(
34-
collection_type,
35-
name,
47+
"""
48+
Connects to a specific dataset on qcportal
49+
50+
Parameters
51+
----------
52+
client: qcportal.client, required
53+
The qcportal client instance
54+
collection_type: str, default="optimization"
55+
The type of qcarchive collection, options are
56+
"torsiondrive", "optimization", "gridoptimization", "reaction", "singlepoint" "manybody"
57+
name: str, default="OpenFF Full Optimization Benchmark 1"
58+
Name of the dataset
59+
60+
Returns
61+
-------
62+
(qcportal dataset, list(str))
63+
Tuple with an instance of qcportal dataset and list of record names
64+
65+
"""
66+
collection = client.get_dataset(
67+
dataset_type=collection_type,
68+
dataset_name=name,
3669
)
3770

38-
record_names = list(collection.data.records)
71+
record_names = collection.entry_names
3972

4073
return collection, record_names
4174

4275

43-
def get_graph(collection, record_name):
44-
# get record and trajectory
45-
record = collection.get_record(record_name, specification="default")
46-
entry = collection.get_entry(record_name)
47-
from openff.toolkit.topology import Molecule
76+
def process_record(record, entry):
77+
"""
78+
Processes a given record/entry pair from a dataset and returns the graph
4879
49-
mol = Molecule.from_qcschema(entry)
80+
Parameters
81+
----------
82+
record: qcportal.optimization.record_models.OptimizationRecord
83+
qcportal record
84+
entry: cportal.optimization.dataset_models.OptimizationDatasetEntry
85+
qcportal entry
5086
51-
try:
52-
trajectory = record.get_trajectory()
53-
except:
54-
return None
87+
Returns
88+
-------
89+
esp.Graph
90+
"""
5591

56-
if trajectory is None:
57-
return None
92+
from openff.toolkit.topology import Molecule
5893

94+
if record.record_type == "optimization":
95+
trajectory = record.trajectory
96+
if trajectory is None:
97+
return None
98+
else:
99+
raise Exception(
100+
f"{record.record_type} is not supported: only optimization datasets can be processed."
101+
)
102+
mol = Molecule.from_qcschema(entry.dict())
59103
g = esp.Graph(mol)
60104

61105
# energy is already hartree
62106
g.nodes["g"].data["u_ref"] = torch.tensor(
63107
[
64108
Quantity(
65-
snapshot.properties.scf_total_energy,
109+
snapshot.properties["scf_total_energy"],
66110
esp.units.HARTREE_PER_PARTICLE,
67111
).value_in_unit(esp.units.ENERGY_UNIT)
68112
for snapshot in trajectory
@@ -74,7 +118,7 @@ def get_graph(collection, record_name):
74118
np.stack(
75119
[
76120
Quantity(
77-
snapshot.get_molecule().geometry,
121+
snapshot.molecule.geometry,
78122
unit.bohr,
79123
).value_in_unit(esp.units.DISTANCE_UNIT)
80124
for snapshot in trajectory
@@ -89,7 +133,7 @@ def get_graph(collection, record_name):
89133
[
90134
torch.tensor(
91135
Quantity(
92-
snapshot.dict()["return_result"],
136+
np.array(snapshot.properties["return_result"]).reshape((-1, 3)),
93137
esp.units.HARTREE_PER_PARTICLE / unit.bohr,
94138
).value_in_unit(esp.units.FORCE_UNIT),
95139
dtype=torch.get_default_dtype(),
@@ -102,21 +146,106 @@ def get_graph(collection, record_name):
102146
return g
103147

104148

105-
def fetch_td_record(record: ptl.models.torsiondrive.TorsionDriveRecord):
106-
final_molecules = record.get_final_molecules()
107-
final_results = record.get_final_results()
149+
def get_graph(collection, record_name, spec_name="default"):
150+
"""
151+
Processes the qcportal data for a given record name.
152+
153+
This supports optimization and singlepoint datasets.
154+
155+
Parameters
156+
----------
157+
collection, qcportal dataset, required
158+
The instance of the qcportal dataset
159+
record_name, str, required
160+
The name of a give record
161+
spec_name, str, default="default"
162+
Retrieve data for a given qcportal specification.
163+
Returns
164+
-------
165+
Graph
166+
"""
167+
# get record and trajectory
168+
record = collection.get_record(record_name, specification_name=spec_name)
169+
entry = collection.get_entry(record_name)
170+
171+
g = process_record(record, entry)
172+
173+
return g
174+
108175

109-
angle_keys = list(final_molecules.keys())
176+
def get_graphs(collection, record_names, spec_name="default"):
177+
"""
178+
Processes the qcportal data for a given set of record names.
179+
This uses the qcportal iteration functions which are faster than processing
180+
records one at a time.
181+
182+
This supports optimization and singlepoint datasets.
183+
184+
185+
Parameters
186+
----------
187+
collection, qcportal dataset, required
188+
The instance of the qcportal dataset
189+
record_name, List[str], required
190+
A list of the record_names of a give record
191+
spec_name, str, default="default"
192+
Retrieve data for a given qcportal specification.
193+
Returns
194+
-------
195+
list(graph)
196+
Returns a list of the corresponding graph for each record name
197+
"""
198+
g_list = []
199+
for record, entry in zip(
200+
collection.iterate_records(record_names, specification_names=[spec_name]),
201+
collection.iterate_entries(record_names),
202+
):
203+
# note iterate records returns a tuple of length 3 (name, spec_name, actual record information)
204+
205+
g = process_record(record[2], entry)
206+
g_list.append(g)
207+
208+
return g_list
209+
210+
211+
def fetch_td_record(record: qcportal.torsiondrive.record_models.TorsiondriveRecord):
212+
"""
213+
Fetches configuration, energy, and gradients for a given torsiondrive record as a function of different angles.
214+
215+
Parameters
216+
----------
217+
record: qcportal.torsiondrive.record_models.TorsiondriveRecord, required
218+
Torsiondrive record of interest
219+
Returns
220+
-------
221+
tuple, ( numpy.array, numpy.array, numpy.array,numpy.array)
222+
Returned data is a tuple of numpy arrays.
223+
The first index contains angles and subsequent arrays represent
224+
molecule coordinate, energy and gradients associated with each angle.
225+
226+
"""
227+
molecule_optimization = record.optimizations
228+
229+
angle_keys = list(molecule_optimization.keys())
110230

111231
xyzs = []
112232
energies = []
113233
gradients = []
114234

115235
for angle in angle_keys:
116-
result = final_results[angle]
117-
mol = final_molecules[angle]
236+
# NOTE: this is calling the first index of the optimization array
237+
# this gives the same value as the prior implementation.
238+
# however it seems to be that this contains multiple different initial configurations
239+
# that have been optimized. Should all conformers and energies/gradients be considered?
240+
mol = molecule_optimization[angle][0].final_molecule
241+
result = molecule_optimization[angle][0].trajectory[-1].properties
242+
243+
"""Note: force = - gradient"""
244+
245+
# TODO: attach units here? or later?
118246

119-
e, g = get_energy_and_gradient(result)
247+
e = result["current energy"]
248+
g = np.array(result["current gradient"]).reshape(-1, 3)
120249

121250
xyzs.append(mol.geometry)
122251
energies.append(e)
@@ -146,22 +275,6 @@ def fetch_td_record(record: ptl.models.torsiondrive.TorsionDriveRecord):
146275
return flat_angles, xyz_in_order, energies_in_order, gradients_in_order
147276

148277

149-
def get_energy_and_gradient(
150-
snapshot: ptl.models.records.ResultRecord,
151-
) -> Tuple[float, np.ndarray]:
152-
"""Note: force = - gradient"""
153-
154-
# TODO: attach units here? or later?
155-
156-
d = snapshot.dict()
157-
qcvars = d["extras"]["qcvars"]
158-
energy = qcvars["CURRENT ENERGY"]
159-
flat_gradient = np.array(qcvars["CURRENT GRADIENT"])
160-
num_atoms = len(flat_gradient) // 3
161-
gradient = flat_gradient.reshape((num_atoms, 3))
162-
return energy, gradient
163-
164-
165278
MolWithTargets = namedtuple(
166279
"MolWithTargets", ["offmol", "xyz", "energies", "gradients"]
167280
)
@@ -184,9 +297,9 @@ def get_smiles(x):
184297
g = esp.Graph(mol_ref)
185298

186299
u_ref = np.concatenate(group["energies"].values)
187-
u_ref_prime = np.concatenate(
188-
group["gradients"].values, axis=0
189-
).transpose(1, 0, 2)
300+
u_ref_prime = np.concatenate(group["gradients"].values, axis=0).transpose(
301+
1, 0, 2
302+
)
190303
xyz = np.concatenate(group["xyz"].values, axis=0).transpose(1, 0, 2)
191304

192305
assert u_ref_prime.shape[0] == xyz.shape[0] == mol_ref.n_atoms
@@ -229,7 +342,7 @@ def breakdown_along_time_axis(g, batch_size=32):
229342

230343
shuffle(idxs)
231344
chunks = [
232-
idxs[_idx * batch_size : (_idx + 1) * batch_size]
345+
idxs[_idx * batch_size: (_idx + 1) * batch_size]
233346
for _idx in range(n_snapshots // batch_size)
234347
]
235348

@@ -259,10 +372,7 @@ def make_batch_size_consistent(ds, batch_size=32):
259372
return esp.data.dataset.GraphDataset(
260373
list(
261374
itertools.chain.from_iterable(
262-
[
263-
breakdown_along_time_axis(g, batch_size=batch_size)
264-
for g in ds
265-
]
375+
[breakdown_along_time_axis(g, batch_size=batch_size) for g in ds]
266376
)
267377
)
268378
)

0 commit comments

Comments
 (0)