cgcnn2 0.2.1__tar.gz

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-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.3
2
+ Name: cgcnn2
3
+ Version: 0.2.1
4
+ Summary: Crystal Graph Convolutional Neural Networks
5
+ License: MIT
6
+ Keywords: python,gnn,vasp,crystal
7
+ Author: Jiacheng Wang
8
+ Author-email: jiachengwang@umass.edu
9
+ Maintainer: Jiacheng Wang
10
+ Requires-Python: >=3.10,<3.13
11
+ Classifier: Development Status :: 1 - Planning
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Dist: ase (>=3.23.0,<4.0.0)
20
+ Requires-Dist: numpy
21
+ Requires-Dist: pandas
22
+ Requires-Dist: pymatgen (>=2025.1.0,<2026.0.0)
23
+ Requires-Dist: pymatviz (>=0.15.0,<0.16.0)
24
+ Requires-Dist: scikit-learn
25
+ Requires-Dist: torch (>=2.5.1,<3.0.0)
26
+ Project-URL: Homepage, https://github.com/jcwang587/cgcnn2/
27
+ Project-URL: Repository, https://github.com/jcwang587/cgcnn2/
28
+ Description-Content-Type: text/markdown
29
+
30
+ # CGCNN2
31
+
32
+ As the original Crystal Graph Convolutional Neural Networks (CGCNN) repository is no longer actively maintained, this repository is a reproduction of [CGCNN](https://github.com/txie-93/cgcnn) by Xie et al. It includes necessary updates for deprecated components and a few additional functions to ensure smooth operation. Despite its age, CGCNN remains a straightforward and fast deep learning framework that is easy to learn and use.
33
+
34
+ The package provides following major functions:
35
+
36
+ - **Training** a CGCNN model with a customized dataset.
37
+ - **Predicting** material properties with a pre-trained CGCNN model.
38
+ - **Fine-tuning** a pre-trained CGCNN model on a new dataset.
39
+ - **Extracting** atomic features as descriptors for the downstream task.
40
+
41
+ ## Installation
42
+
43
+ Make sure you have a Python interpreter, preferably version 3.10 or higher. Then, you can simply install xdatbus from
44
+ PyPI using `pip`:
45
+
46
+ ```bash
47
+ pip install cgcnn2
48
+ ```
49
+
50
+ If you'd like to use the latest unreleased version on the main branch, you can install it directly from GitHub:
51
+
52
+ ```bash
53
+ pip install git+https://github.com/jcwang587/cgcnn2
54
+ ```
55
+
56
+ ## Get Started
57
+
58
+ ```bash
59
+ cgcnn-ft --help
60
+ ```
61
+
62
+
63
+ ## References
64
+
65
+ The original paper describes the details of the CGCNN framework:
66
+
67
+ ```bibtex
68
+ @article{PhysRevLett.120.145301,
69
+ title = {Crystal Graph Convolutional Neural Networks for an Accurate and Interpretable Prediction of Material Properties},
70
+ author = {Xie, Tian and Grossman, Jeffrey C.},
71
+ journal = {Phys. Rev. Lett.},
72
+ volume = {120},
73
+ issue = {14},
74
+ pages = {145301},
75
+ numpages = {6},
76
+ year = {2018},
77
+ month = {Apr},
78
+ publisher = {American Physical Society},
79
+ doi = {10.1103/PhysRevLett.120.145301},
80
+ url = {https://link.aps.org/doi/10.1103/PhysRevLett.120.145301}
81
+ }
82
+ ```
83
+
cgcnn2-0.2.1/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # CGCNN2
2
+
3
+ As the original Crystal Graph Convolutional Neural Networks (CGCNN) repository is no longer actively maintained, this repository is a reproduction of [CGCNN](https://github.com/txie-93/cgcnn) by Xie et al. It includes necessary updates for deprecated components and a few additional functions to ensure smooth operation. Despite its age, CGCNN remains a straightforward and fast deep learning framework that is easy to learn and use.
4
+
5
+ The package provides following major functions:
6
+
7
+ - **Training** a CGCNN model with a customized dataset.
8
+ - **Predicting** material properties with a pre-trained CGCNN model.
9
+ - **Fine-tuning** a pre-trained CGCNN model on a new dataset.
10
+ - **Extracting** atomic features as descriptors for the downstream task.
11
+
12
+ ## Installation
13
+
14
+ Make sure you have a Python interpreter, preferably version 3.10 or higher. Then, you can simply install xdatbus from
15
+ PyPI using `pip`:
16
+
17
+ ```bash
18
+ pip install cgcnn2
19
+ ```
20
+
21
+ If you'd like to use the latest unreleased version on the main branch, you can install it directly from GitHub:
22
+
23
+ ```bash
24
+ pip install git+https://github.com/jcwang587/cgcnn2
25
+ ```
26
+
27
+ ## Get Started
28
+
29
+ ```bash
30
+ cgcnn-ft --help
31
+ ```
32
+
33
+
34
+ ## References
35
+
36
+ The original paper describes the details of the CGCNN framework:
37
+
38
+ ```bibtex
39
+ @article{PhysRevLett.120.145301,
40
+ title = {Crystal Graph Convolutional Neural Networks for an Accurate and Interpretable Prediction of Material Properties},
41
+ author = {Xie, Tian and Grossman, Jeffrey C.},
42
+ journal = {Phys. Rev. Lett.},
43
+ volume = {120},
44
+ issue = {14},
45
+ pages = {145301},
46
+ numpages = {6},
47
+ year = {2018},
48
+ month = {Apr},
49
+ publisher = {American Physical Society},
50
+ doi = {10.1103/PhysRevLett.120.145301},
51
+ url = {https://link.aps.org/doi/10.1103/PhysRevLett.120.145301}
52
+ }
53
+ ```
@@ -0,0 +1,4 @@
1
+ from .cgcnn_data import *
2
+ from .cgcnn_model import *
3
+ from .cgcnn_utils import *
4
+ from .cgcnn_ft import *
@@ -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