cgcnn2 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cgcnn2/__init__.py +4 -0
- cgcnn2/cgcnn_data.py +392 -0
- cgcnn2/cgcnn_ft.py +374 -0
- cgcnn2/cgcnn_model.py +372 -0
- cgcnn2/cgcnn_utils.py +491 -0
- cgcnn2-0.2.1.dist-info/METADATA +83 -0
- cgcnn2-0.2.1.dist-info/RECORD +9 -0
- cgcnn2-0.2.1.dist-info/WHEEL +4 -0
- cgcnn2-0.2.1.dist-info/entry_points.txt +3 -0
cgcnn2/__init__.py
ADDED
cgcnn2/cgcnn_data.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import functools
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
from pymatgen.core.structure import Structure
|
|
11
|
+
from torch.utils.data import Dataset
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collate_pool(dataset_list):
|
|
15
|
+
"""
|
|
16
|
+
Collate a list of data and return a batch for predicting crystal
|
|
17
|
+
properties.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
|
|
22
|
+
dataset_list: list of tuples for each data point.
|
|
23
|
+
(atom_fea, nbr_fea, nbr_fea_idx, target)
|
|
24
|
+
|
|
25
|
+
atom_fea: torch.Tensor shape (n_i, atom_fea_len)
|
|
26
|
+
nbr_fea: torch.Tensor shape (n_i, M, nbr_fea_len)
|
|
27
|
+
nbr_fea_idx: torch.LongTensor shape (n_i, M)
|
|
28
|
+
target: torch.Tensor shape (1, )
|
|
29
|
+
cif_id: str or int
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
N = sum(n_i); N0 = sum(i)
|
|
34
|
+
|
|
35
|
+
batch_atom_fea: torch.Tensor shape (N, orig_atom_fea_len)
|
|
36
|
+
Atom features from atom type
|
|
37
|
+
batch_nbr_fea: torch.Tensor shape (N, M, nbr_fea_len)
|
|
38
|
+
Bond features of each atom's M neighbors
|
|
39
|
+
batch_nbr_fea_idx: torch.LongTensor shape (N, M)
|
|
40
|
+
Indices of M neighbors of each atom
|
|
41
|
+
crystal_atom_idx: list of torch.LongTensor of length N0
|
|
42
|
+
Mapping from the crystal idx to atom idx
|
|
43
|
+
target: torch.Tensor shape (N, 1)
|
|
44
|
+
Target value for prediction
|
|
45
|
+
batch_cif_ids: list
|
|
46
|
+
"""
|
|
47
|
+
batch_atom_fea, batch_nbr_fea, batch_nbr_fea_idx = [], [], []
|
|
48
|
+
crystal_atom_idx, batch_target = [], []
|
|
49
|
+
batch_cif_ids = []
|
|
50
|
+
base_idx = 0
|
|
51
|
+
for i, ((atom_fea, nbr_fea, nbr_fea_idx), target, cif_id) in enumerate(
|
|
52
|
+
dataset_list
|
|
53
|
+
):
|
|
54
|
+
n_i = atom_fea.shape[0] # number of atoms for this crystal
|
|
55
|
+
batch_atom_fea.append(atom_fea)
|
|
56
|
+
batch_nbr_fea.append(nbr_fea)
|
|
57
|
+
batch_nbr_fea_idx.append(nbr_fea_idx + base_idx)
|
|
58
|
+
new_idx = torch.LongTensor(np.arange(n_i) + base_idx)
|
|
59
|
+
crystal_atom_idx.append(new_idx)
|
|
60
|
+
batch_target.append(target)
|
|
61
|
+
batch_cif_ids.append(cif_id)
|
|
62
|
+
base_idx += n_i
|
|
63
|
+
return (
|
|
64
|
+
(
|
|
65
|
+
torch.cat(batch_atom_fea, dim=0),
|
|
66
|
+
torch.cat(batch_nbr_fea, dim=0),
|
|
67
|
+
torch.cat(batch_nbr_fea_idx, dim=0),
|
|
68
|
+
crystal_atom_idx,
|
|
69
|
+
),
|
|
70
|
+
torch.stack(batch_target, dim=0),
|
|
71
|
+
batch_cif_ids,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class GaussianDistance(object):
|
|
76
|
+
"""
|
|
77
|
+
Expands the distance by Gaussian basis.
|
|
78
|
+
|
|
79
|
+
Unit: angstrom
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, dmin, dmax, step, var=None):
|
|
83
|
+
"""
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
|
|
87
|
+
dmin: float
|
|
88
|
+
Minimum interatomic distance
|
|
89
|
+
dmax: float
|
|
90
|
+
Maximum interatomic distance
|
|
91
|
+
step: float
|
|
92
|
+
Step size for the Gaussian filter
|
|
93
|
+
"""
|
|
94
|
+
assert dmin < dmax
|
|
95
|
+
assert dmax - dmin > step
|
|
96
|
+
self.filter = np.arange(dmin, dmax + step, step)
|
|
97
|
+
if var is None:
|
|
98
|
+
var = step
|
|
99
|
+
self.var = var
|
|
100
|
+
|
|
101
|
+
def expand(self, distances):
|
|
102
|
+
"""
|
|
103
|
+
Apply Gaussian distance filter to a numpy distance array
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
|
|
108
|
+
distances: np.ndarray
|
|
109
|
+
A distance matrix of any shape
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
expanded_distance: shape (n+1)-d array
|
|
114
|
+
Expanded distance matrix with the last dimension of length
|
|
115
|
+
len(self.filter)
|
|
116
|
+
"""
|
|
117
|
+
return np.exp(-((distances[..., np.newaxis] - self.filter) ** 2) / self.var**2)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AtomInitializer(object):
|
|
121
|
+
"""
|
|
122
|
+
Base class for initializing the vector representation for atoms.
|
|
123
|
+
|
|
124
|
+
!!! Use one AtomInitializer per dataset !!!
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, atom_types):
|
|
128
|
+
self.atom_types = set(atom_types)
|
|
129
|
+
self._embedding = {}
|
|
130
|
+
|
|
131
|
+
def get_atom_fea(self, atom_type):
|
|
132
|
+
assert atom_type in self.atom_types
|
|
133
|
+
return self._embedding[atom_type]
|
|
134
|
+
|
|
135
|
+
def load_state_dict(self, state_dict):
|
|
136
|
+
self._embedding = state_dict
|
|
137
|
+
self.atom_types = set(self._embedding.keys())
|
|
138
|
+
self._decodedict = {
|
|
139
|
+
idx: atom_type for atom_type, idx in self._embedding.items()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def state_dict(self):
|
|
143
|
+
return self._embedding
|
|
144
|
+
|
|
145
|
+
def decode(self, idx):
|
|
146
|
+
if not hasattr(self, "_decodedict"):
|
|
147
|
+
self._decodedict = {
|
|
148
|
+
idx: atom_type for atom_type, idx in self._embedding.items()
|
|
149
|
+
}
|
|
150
|
+
return self._decodedict[idx]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AtomCustomJSONInitializer(AtomInitializer):
|
|
154
|
+
"""
|
|
155
|
+
Initialize atom feature vectors using a JSON file, which is a python
|
|
156
|
+
dictionary mapping from element number to a list representing the
|
|
157
|
+
feature vector of the element.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
|
|
162
|
+
elem_embedding_file: str
|
|
163
|
+
The path to the .json file
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, elem_embedding_file):
|
|
167
|
+
with open(elem_embedding_file) as f:
|
|
168
|
+
elem_embedding = json.load(f)
|
|
169
|
+
elem_embedding = {int(key): value for key, value in elem_embedding.items()}
|
|
170
|
+
atom_types = set(elem_embedding.keys())
|
|
171
|
+
super(AtomCustomJSONInitializer, self).__init__(atom_types)
|
|
172
|
+
for key, value in elem_embedding.items():
|
|
173
|
+
self._embedding[key] = np.array(value, dtype=float)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CIFData(Dataset):
|
|
177
|
+
"""
|
|
178
|
+
The CIFData dataset is a wrapper for a dataset where the crystal structures
|
|
179
|
+
are stored in the form of CIF files. The dataset should have the following
|
|
180
|
+
directory structure:
|
|
181
|
+
|
|
182
|
+
root_dir
|
|
183
|
+
├── id_prop.csv
|
|
184
|
+
├── atom_init.json
|
|
185
|
+
├── id0.cif
|
|
186
|
+
├── id1.cif
|
|
187
|
+
├── ...
|
|
188
|
+
|
|
189
|
+
id_prop.csv: a CSV file with two columns. The first column recodes a
|
|
190
|
+
unique ID for each crystal, and the second column recodes the value of
|
|
191
|
+
target property.
|
|
192
|
+
|
|
193
|
+
atom_init.json: a JSON file that stores the initialization vector for each
|
|
194
|
+
element.
|
|
195
|
+
|
|
196
|
+
ID.cif: a CIF file that recodes the crystal structure, where ID is the
|
|
197
|
+
unique ID for the crystal.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
|
|
202
|
+
root_dir: str
|
|
203
|
+
The path to the root directory of the dataset
|
|
204
|
+
max_num_nbr: int
|
|
205
|
+
The maximum number of neighbors while constructing the crystal graph
|
|
206
|
+
radius: float
|
|
207
|
+
The cutoff radius for searching neighbors
|
|
208
|
+
dmin: float
|
|
209
|
+
The minimum distance for constructing GaussianDistance
|
|
210
|
+
step: float
|
|
211
|
+
The step size for constructing GaussianDistance
|
|
212
|
+
random_seed: int
|
|
213
|
+
Random seed for shuffling the dataset
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
|
|
218
|
+
atom_fea: torch.Tensor shape (n_i, atom_fea_len)
|
|
219
|
+
nbr_fea: torch.Tensor shape (n_i, M, nbr_fea_len)
|
|
220
|
+
nbr_fea_idx: torch.LongTensor shape (n_i, M)
|
|
221
|
+
target: torch.Tensor shape (1, )
|
|
222
|
+
cif_id: str or int
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self, root_dir, max_num_nbr=12, radius=8, dmin=0, step=0.2, random_seed=123
|
|
227
|
+
):
|
|
228
|
+
self.root_dir = root_dir
|
|
229
|
+
self.max_num_nbr, self.radius = max_num_nbr, radius
|
|
230
|
+
assert os.path.exists(root_dir), "root_dir does not exist!"
|
|
231
|
+
id_prop_file = os.path.join(self.root_dir, "id_prop.csv")
|
|
232
|
+
assert os.path.exists(id_prop_file), "id_prop.csv does not exist!"
|
|
233
|
+
with open(id_prop_file) as f:
|
|
234
|
+
reader = csv.reader(f)
|
|
235
|
+
self.id_prop_data = [row for row in reader]
|
|
236
|
+
random.seed(random_seed)
|
|
237
|
+
random.shuffle(self.id_prop_data)
|
|
238
|
+
atom_init_file = os.path.join(self.root_dir, "atom_init.json")
|
|
239
|
+
assert os.path.exists(atom_init_file), "atom_init.json does not exist!"
|
|
240
|
+
self.ari = AtomCustomJSONInitializer(atom_init_file)
|
|
241
|
+
self.gdf = GaussianDistance(dmin=dmin, dmax=self.radius, step=step)
|
|
242
|
+
|
|
243
|
+
def __len__(self):
|
|
244
|
+
return len(self.id_prop_data)
|
|
245
|
+
|
|
246
|
+
@functools.lru_cache(maxsize=None) # Cache loaded structures
|
|
247
|
+
def __getitem__(self, idx):
|
|
248
|
+
cif_id, target = self.id_prop_data[idx]
|
|
249
|
+
crystal = Structure.from_file(os.path.join(self.root_dir, cif_id + ".cif"))
|
|
250
|
+
atom_fea = np.vstack(
|
|
251
|
+
[
|
|
252
|
+
self.ari.get_atom_fea(crystal[i].specie.number)
|
|
253
|
+
for i in range(len(crystal))
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
atom_fea = torch.Tensor(atom_fea)
|
|
257
|
+
all_nbrs = crystal.get_all_neighbors(self.radius, include_index=True)
|
|
258
|
+
all_nbrs = [sorted(nbrs, key=lambda x: x[1]) for nbrs in all_nbrs]
|
|
259
|
+
nbr_fea_idx, nbr_fea = [], []
|
|
260
|
+
for nbr in all_nbrs:
|
|
261
|
+
if len(nbr) < self.max_num_nbr:
|
|
262
|
+
warnings.warn(
|
|
263
|
+
"{} not find enough neighbors to build graph. "
|
|
264
|
+
"If it happens frequently, consider increase "
|
|
265
|
+
"radius.".format(cif_id)
|
|
266
|
+
)
|
|
267
|
+
nbr_fea_idx.append(
|
|
268
|
+
list(map(lambda x: x[2], nbr)) + [0] * (self.max_num_nbr - len(nbr))
|
|
269
|
+
)
|
|
270
|
+
nbr_fea.append(
|
|
271
|
+
list(map(lambda x: x[1], nbr))
|
|
272
|
+
+ [self.radius + 1.0] * (self.max_num_nbr - len(nbr))
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
nbr_fea_idx.append(list(map(lambda x: x[2], nbr[: self.max_num_nbr])))
|
|
276
|
+
nbr_fea.append(list(map(lambda x: x[1], nbr[: self.max_num_nbr])))
|
|
277
|
+
nbr_fea_idx, nbr_fea = np.array(nbr_fea_idx), np.array(nbr_fea)
|
|
278
|
+
nbr_fea = self.gdf.expand(nbr_fea)
|
|
279
|
+
atom_fea = torch.Tensor(atom_fea)
|
|
280
|
+
nbr_fea = torch.Tensor(nbr_fea)
|
|
281
|
+
nbr_fea_idx = torch.LongTensor(nbr_fea_idx)
|
|
282
|
+
target = torch.Tensor([float(target)])
|
|
283
|
+
return (atom_fea, nbr_fea, nbr_fea_idx), target, cif_id
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class CIFData_pred(Dataset):
|
|
287
|
+
"""
|
|
288
|
+
The CIFData dataset is a wrapper for a dataset where the crystal structures
|
|
289
|
+
are stored in the form of CIF files. The dataset should have the following
|
|
290
|
+
directory structure:
|
|
291
|
+
|
|
292
|
+
root_dir
|
|
293
|
+
├── id_prop.csv
|
|
294
|
+
├── atom_init.json
|
|
295
|
+
├── id0.cif
|
|
296
|
+
├── id1.cif
|
|
297
|
+
├── ...
|
|
298
|
+
|
|
299
|
+
id_prop.csv: a CSV file with two columns. The first column recodes a
|
|
300
|
+
unique ID for each crystal, and the second column recodes the value of
|
|
301
|
+
target property.
|
|
302
|
+
|
|
303
|
+
atom_init.json: a JSON file that stores the initialization vector for each
|
|
304
|
+
element.
|
|
305
|
+
|
|
306
|
+
ID.cif: a CIF file that recodes the crystal structure, where ID is the
|
|
307
|
+
unique ID for the crystal.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
|
|
312
|
+
root_dir: str
|
|
313
|
+
The path to the root directory of the dataset
|
|
314
|
+
max_num_nbr: int
|
|
315
|
+
The maximum number of neighbors while constructing the crystal graph
|
|
316
|
+
radius: float
|
|
317
|
+
The cutoff radius for searching neighbors
|
|
318
|
+
dmin: float
|
|
319
|
+
The minimum distance for constructing GaussianDistance
|
|
320
|
+
step: float
|
|
321
|
+
The step size for constructing GaussianDistance
|
|
322
|
+
random_seed: int
|
|
323
|
+
Random seed for shuffling the dataset
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
|
|
328
|
+
atom_fea: torch.Tensor shape (n_i, atom_fea_len)
|
|
329
|
+
nbr_fea: torch.Tensor shape (n_i, M, nbr_fea_len)
|
|
330
|
+
nbr_fea_idx: torch.LongTensor shape (n_i, M)
|
|
331
|
+
target: torch.Tensor shape (1, )
|
|
332
|
+
cif_id: str or int
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(
|
|
336
|
+
self, root_dir, max_num_nbr=12, radius=8, dmin=0, step=0.2, random_seed=123
|
|
337
|
+
):
|
|
338
|
+
self.root_dir = root_dir
|
|
339
|
+
self.max_num_nbr, self.radius = max_num_nbr, radius
|
|
340
|
+
assert os.path.exists(root_dir), "root_dir does not exist!"
|
|
341
|
+
id_prop_file = os.path.join(self.root_dir, "id_prop.csv")
|
|
342
|
+
assert os.path.exists(id_prop_file), "id_prop.csv does not exist!"
|
|
343
|
+
with open(id_prop_file) as f:
|
|
344
|
+
reader = csv.reader(f)
|
|
345
|
+
self.id_prop_data = [row for row in reader]
|
|
346
|
+
random.seed(random_seed)
|
|
347
|
+
atom_init_file = os.path.join(self.root_dir, "atom_init.json")
|
|
348
|
+
assert os.path.exists(atom_init_file), "atom_init.json does not exist!"
|
|
349
|
+
self.ari = AtomCustomJSONInitializer(atom_init_file)
|
|
350
|
+
self.gdf = GaussianDistance(dmin=dmin, dmax=self.radius, step=step)
|
|
351
|
+
|
|
352
|
+
def __len__(self):
|
|
353
|
+
return len(self.id_prop_data)
|
|
354
|
+
|
|
355
|
+
@functools.lru_cache(maxsize=None) # Cache loaded structures
|
|
356
|
+
def __getitem__(self, idx):
|
|
357
|
+
cif_id, target = self.id_prop_data[idx]
|
|
358
|
+
crystal = Structure.from_file(os.path.join(self.root_dir, cif_id + ".cif"))
|
|
359
|
+
atom_fea = np.vstack(
|
|
360
|
+
[
|
|
361
|
+
self.ari.get_atom_fea(crystal[i].specie.number)
|
|
362
|
+
for i in range(len(crystal))
|
|
363
|
+
]
|
|
364
|
+
)
|
|
365
|
+
atom_fea = torch.Tensor(atom_fea)
|
|
366
|
+
all_nbrs = crystal.get_all_neighbors(self.radius, include_index=True)
|
|
367
|
+
all_nbrs = [sorted(nbrs, key=lambda x: x[1]) for nbrs in all_nbrs]
|
|
368
|
+
nbr_fea_idx, nbr_fea = [], []
|
|
369
|
+
for nbr in all_nbrs:
|
|
370
|
+
if len(nbr) < self.max_num_nbr:
|
|
371
|
+
warnings.warn(
|
|
372
|
+
"{} not find enough neighbors to build graph. "
|
|
373
|
+
"If it happens frequently, consider increase "
|
|
374
|
+
"radius.".format(cif_id)
|
|
375
|
+
)
|
|
376
|
+
nbr_fea_idx.append(
|
|
377
|
+
list(map(lambda x: x[2], nbr)) + [0] * (self.max_num_nbr - len(nbr))
|
|
378
|
+
)
|
|
379
|
+
nbr_fea.append(
|
|
380
|
+
list(map(lambda x: x[1], nbr))
|
|
381
|
+
+ [self.radius + 1.0] * (self.max_num_nbr - len(nbr))
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
nbr_fea_idx.append(list(map(lambda x: x[2], nbr[: self.max_num_nbr])))
|
|
385
|
+
nbr_fea.append(list(map(lambda x: x[1], nbr[: self.max_num_nbr])))
|
|
386
|
+
nbr_fea_idx, nbr_fea = np.array(nbr_fea_idx), np.array(nbr_fea)
|
|
387
|
+
nbr_fea = self.gdf.expand(nbr_fea)
|
|
388
|
+
atom_fea = torch.Tensor(atom_fea)
|
|
389
|
+
nbr_fea = torch.Tensor(nbr_fea)
|
|
390
|
+
nbr_fea_idx = torch.LongTensor(nbr_fea_idx)
|
|
391
|
+
target = torch.Tensor([float(target)])
|
|
392
|
+
return (atom_fea, nbr_fea, nbr_fea_idx), target, cif_id
|