BUCToolkit 1.0b0__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.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,524 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: write_files.py
5
+ # Environment: Python 3.12
6
+
7
+ import os
8
+ import warnings
9
+ from typing import Sequence, List, Literal, Tuple
10
+ import joblib as jb
11
+ import numpy as np
12
+
13
+ # from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
14
+ from BUCToolkit.utils._print_formatter import STRING_ARRAY_FORMAT, AS_PRINT_COORDS, FLOAT_ARRAY_FORMAT
15
+
16
+
17
+ class WritePOSCARs:
18
+ """
19
+
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ cells: Sequence[Sequence],
25
+ coords: Sequence[np.ndarray],
26
+ atom_labels: Sequence[Sequence[str]],
27
+ atom_numbers: Sequence[Sequence[int]],
28
+ fixed: Sequence[np.ndarray] | None = None,
29
+ output_path: str = './',
30
+ file_name_list: str | Sequence[str] = 'POSCAR',
31
+ system_list: str | Sequence[str] = 'untitled',
32
+ coord_type: List[Literal['C', 'D']] | Literal['C', 'D'] = 'C',
33
+ ncore: int = -1
34
+ ) -> None:
35
+ """
36
+ Convert coordinates matrices to POSCAR format, and write files to the "output_path".
37
+
38
+ Args:
39
+ cells: List|ndarray, a batch of lattice vectors. shape: (n_batch, 3, 3)
40
+ atom_labels: 2D list|ndarray of str, a batch of list of element symbols. shape: (n_batch, n_atom)
41
+ atom_numbers:2D list|ndarray of int, list of atom number of each element, in the order of atom_label. shape: (n_batch, n_atom)
42
+ coords: list|ndarray, the batch of atoms coordinates, in the order of atom_label. shape: (n_batch, n_atom, 3)
43
+ fixed: list|ndarray|None, the batch of atoms fixed directions, in the order of atom_label. shape: (n_batch, n_atom, 3), dtype: int.
44
+ `0` for fixed, `1` for free. if None, `fixed` would fill with `1`.
45
+ output_path: str, the output path.
46
+ file_name_list: list(str), the list of file names.
47
+ system_list: str|list(str), the 1st line of output file i.e., the annotation or title of the file.
48
+ coord_type: List[Literal['C', 'D']]|Literal['C', 'D'], 'D' or 'C', which means whether the input coordinates are "Direct" or "Cartesian".
49
+ If only a string, `coord_type` would set to be '[`coord_type`] * n_batch'.
50
+ ncore: int, the number of CPU cores to write files in parallel.
51
+
52
+ Returns: None
53
+ """
54
+ # check vars
55
+ n_batch = len(cells)
56
+ if isinstance(file_name_list, str):
57
+ file_name_list = [file_name_list + str(i) for i in range(n_batch)]
58
+ if isinstance(system_list, str):
59
+ system_list = [system_list] * n_batch
60
+ if not (n_batch == len(coords) == len(atom_labels) == len(atom_numbers)):
61
+ raise ValueError(f'number of cells in cell vector, atom coordinates, atom labels, and atom_numbers should be the same,\
62
+ but occurred {n_batch, len(coords), len(atom_labels), len(atom_numbers)}')
63
+ if isinstance(coord_type, str):
64
+ coord_type = [coord_type] * n_batch
65
+ elif (not isinstance(coord_type, (List, Tuple))) or (len(coord_type) != n_batch):
66
+ raise ValueError(f'Invalid value of `coord_type`: type: {type(coord_type)}, length: {len(coord_type)}.'
67
+ f' It should be type: List | Tuple, length: {n_batch}')
68
+ if fixed is None:
69
+ fixed = [np.full_like(_, 1, dtype=np.int8) for _ in coords]
70
+ # check len
71
+ if isinstance(file_name_list, Sequence):
72
+ if not (len(file_name_list) == len(cells) == len(atom_labels) == len(atom_numbers) == len(coords) == len(system_list) == len(fixed)):
73
+ raise ValueError(
74
+ f'Inconsistent length of inputs.'
75
+ f'length of file_name_list, cells, atom_labels, atom_numbers, coords, system_list, fixed: '
76
+ f'{
77
+ (len(file_name_list), len(cells), len(atom_labels), len(atom_numbers), len(coords), len(system_list), len(fixed))
78
+ }'
79
+ )
80
+ # check parallel
81
+ tot_cores = jb.cpu_count()
82
+ if not isinstance(ncore, int):
83
+ raise TypeError(f'`ncore` must be an integer, but occurred {type(ncore)}.')
84
+ elif ncore == -1:
85
+ ncore = tot_cores
86
+ elif ncore > tot_cores:
87
+ warnings.warn('Input `ncore` is greater than total CPU cores and was set to total CPU cores automatically.', RuntimeWarning)
88
+ ncore = tot_cores
89
+
90
+ if ncore != 1:
91
+ _para = jb.Parallel(ncore, backend="threading")
92
+ _para(
93
+ jb.delayed(self.__write)(
94
+ cell,
95
+ coords[i],
96
+ fixed[i],
97
+ atom_labels[i],
98
+ atom_numbers[i],
99
+ output_path=output_path,
100
+ file_name=file_name_list[i],
101
+ system=system_list[i],
102
+ coord_type=coord_type[i]
103
+ )
104
+ for i, cell in enumerate(cells)
105
+ )
106
+
107
+ else:
108
+ for i in range(n_batch):
109
+ self.__write(
110
+ cells[i],
111
+ coords[i],
112
+ fixed[i],
113
+ atom_labels[i],
114
+ atom_numbers[i],
115
+ output_path=output_path,
116
+ file_name=file_name_list[i],
117
+ system=system_list[i],
118
+ coord_type=coord_type[i]
119
+ )
120
+
121
+ pass
122
+
123
+ @staticmethod
124
+ def __write(cell: Sequence,
125
+ coord: np.ndarray,
126
+ fixed: np.ndarray,
127
+ atom_label: Sequence,
128
+ atom_number: Sequence,
129
+ output_path: str,
130
+ file_name: str,
131
+ system: str,
132
+ coord_type: str) -> None:
133
+ """
134
+ Convert coordinates matrix to POSCAR format, and write a file to the output_path.
135
+
136
+ Args:
137
+ cell: list|ndarray, the lattice vector of cell.
138
+ atom_label:1D list|ndarray of str, list of element symbols.
139
+ atom_number:1D list|ndarray of int, list of atom number of each element, in the order of atom_label.
140
+ coord: list|ndarray, the coordinates of atoms, in the order of atom_label.
141
+ fixed: list|ndarray, the fixed direction of atoms.
142
+ output_path: str, the output path.
143
+ file_name: str, the file name.
144
+ system: str, the 1st line of an output file i.e., the annotation or title of the file.
145
+ coord_type: str, 'D' or 'C', which means whether the coordinates are "Direct" or "Cartesian".
146
+
147
+ Return: None
148
+ """
149
+ # check vars
150
+ if not isinstance(cell, (list, np.ndarray)):
151
+ raise TypeError(f'Unknown type of cell, type : {type(cell)}')
152
+ if isinstance(coord, List):
153
+ coord = np.asarray(coord, dtype=np.float32)
154
+ elif not isinstance(coord, np.ndarray):
155
+ raise TypeError(f'Unknown type of coord, type : {type(coord)}')
156
+ if not (isinstance(output_path, str) and isinstance(file_name, str) and isinstance(system, str)):
157
+ raise TypeError(f'output_path||file_name||system must be strings, '
158
+ f'but got type {type(output_path)}||{type(file_name)}||{type(system)}.')
159
+ elif coord_type != 'C' and coord_type != 'D':
160
+ raise ValueError(f'Unknown coord_type : "{coord_type}"')
161
+
162
+ # main
163
+ with open(os.path.join(output_path, file_name), 'w') as POSCAR:
164
+ POSCAR.write(system)
165
+ POSCAR.write('\n 1\n')
166
+ # cell
167
+ for vx in cell:
168
+ for xx in vx:
169
+ POSCAR.write(f' {xx:0< 14.8f}')
170
+ POSCAR.write('\n')
171
+ # atom element
172
+ for label in atom_label:
173
+ POSCAR.write(f' {label: <6s}')
174
+ POSCAR.write('\n')
175
+ for label in atom_number:
176
+ POSCAR.write(f' {label: <6d}')
177
+ POSCAR.write('\n')
178
+ # selective dynamics
179
+ POSCAR.write('Selective Dynamics\n')
180
+ if coord_type == 'C':
181
+ POSCAR.write('Cartesian\n')
182
+ elif coord_type == 'D':
183
+ POSCAR.write('Direct\n')
184
+ else: raise ValueError(f'Invalid `coord_type`: {coord_type}')
185
+ # atom coordinates
186
+ fixed = np.where(fixed == 1, 'T', 'F')
187
+ coord_str = AS_PRINT_COORDS(coord[:, :3])
188
+ print_arr = np.concatenate((coord_str, fixed[:, :3]), axis=1)
189
+ print_str = np.array2string(print_arr, **STRING_ARRAY_FORMAT).replace('[', ' ').replace(']', ' ')
190
+ POSCAR.write(print_str)
191
+ POSCAR.write('\n')
192
+
193
+
194
+ class Write2JDFTX:
195
+
196
+ def __init__(self, ):
197
+ pass
198
+
199
+ def write(self,
200
+ batch_structures,
201
+ output_path: str = './',
202
+ file_name_list: str | Sequence[str] = 'POSCAR',
203
+ system_list: str | Sequence[str] = 'untitled',
204
+ coord_type: str = 'C') -> None:
205
+ """
206
+ Convert coordinates matrices to POSCAR format, and write files to the "output_path".
207
+
208
+ Parameters:
209
+ batch_structures: batch of structures.
210
+ output_path: str, the output path.
211
+ file_name_list: list(str), the list of file names.
212
+ system_list: str|list(str), the 1st line of output file i.e., the annotation or title of the file.
213
+ coord_type: str, 'D' or 'C', which means whether the coordinates is "Direct" or "Cartesian".
214
+
215
+ Returns: None
216
+ """
217
+ batch_structures.generate_atom_list()
218
+ n_batch = len(batch_structures.Cells)
219
+ if isinstance(file_name_list, str):
220
+ file_name_list = [file_name_list + str(i) for i in range(n_batch)]
221
+ if isinstance(system_list, str):
222
+ system_list = [system_list] * n_batch
223
+ if not (n_batch == len(batch_structures.Coords) == len(batch_structures.Elements) == len(batch_structures.Numbers)):
224
+ raise ValueError(f'number of cells in cell vector, atom coordinates, atom labels, and atom_numbers should be the same, '
225
+ f'but occurred {n_batch, len(batch_structures.Coords), len(batch_structures.Elements), len(batch_structures.Numbers)}')
226
+
227
+ for i in range(n_batch):
228
+ self.__write(batch_structures.Cells[i],
229
+ batch_structures.Coords_type[i],
230
+ batch_structures.Coords[i],
231
+ batch_structures.Fixed[i],
232
+ batch_structures.Atom_list[i],
233
+ output_path=output_path,
234
+ file_name=file_name_list[i],
235
+ system=system_list[i],
236
+ out_coord_type=coord_type)
237
+
238
+ pass
239
+
240
+ @staticmethod
241
+ def __write(cell: Sequence,
242
+ in_coord_type: str,
243
+ coord: np.ndarray,
244
+ fixed: np.ndarray,
245
+ atom_list: List,
246
+ output_path: str,
247
+ file_name: str,
248
+ system: str,
249
+ out_coord_type: str,
250
+ ) -> None:
251
+ """
252
+ Convert coordinates matrix to POSCAR format, and write a file to the output_path.
253
+
254
+ Parameters:
255
+ cell: list|ndarray|torchTensor, the lattice vector of cell.
256
+ coord: list|ndarray|torchTensor, the coordinates of atoms, in the order of atom_label.
257
+ atom_list: list|
258
+ output_path: str, the output path.
259
+ file_name: str, the file name.
260
+ system: str, the 1st line of output file i.e., the annotation or title of the file.
261
+ out_coord_type: str, 'D' or 'C', which means whether the coordinate type is "Direct" or "Cartesian".
262
+
263
+ Return: None
264
+ """
265
+ # check vars
266
+ if not isinstance(cell, (list, np.ndarray)):
267
+ raise TypeError(f'Unknown type of cell, type : {type(cell)}')
268
+ elif not isinstance(coord, (list, np.ndarray)):
269
+ raise TypeError(f'Unknown type of coord, type : {type(coord)}')
270
+ elif not (isinstance(output_path, str) and isinstance(file_name, str) and isinstance(system, str)):
271
+ raise TypeError(
272
+ f'output_path||file_name||system must be strings, but type {type(output_path)}||{type(file_name)}||{type(system)} occurred.')
273
+ elif out_coord_type != 'C' and out_coord_type != 'D':
274
+ raise ValueError(f'Unknown coord_type : "{out_coord_type}"')
275
+
276
+ # main
277
+ with open(os.path.join(output_path, file_name), 'w') as POSCAR:
278
+ POSCAR.write('#' + system)
279
+ POSCAR.write('\n')
280
+ # cell
281
+ POSCAR.write('lattice\\\n')
282
+ cell = np.asarray(cell, dtype=np.float32) * 1.89036 # 1 Angstrom = 1.89036 Bohr. Convert from A to Bohr.
283
+ for vx in cell[:-1]:
284
+ for xx in vx:
285
+ POSCAR.write(f'{xx:>14.8f}')
286
+ POSCAR.write('\\\n')
287
+ for xx in cell[-1]:
288
+ POSCAR.write(f'{xx:>14.8f}')
289
+ POSCAR.write('\n')
290
+ # others
291
+ POSCAR.write('coulomb-interaction Slab 001\n')
292
+ POSCAR.write('coulomb-truncation-embed 0 0 0\n')
293
+ # atom coordinates
294
+ coord = np.asarray(coord, dtype=np.float32)
295
+ if out_coord_type == 'C':
296
+ POSCAR.write('coords-type Cartesian\n')
297
+ if in_coord_type == 'D':
298
+ coord = coord @ cell
299
+ elif in_coord_type == 'C':
300
+ coord = coord * 1.89036
301
+ else:
302
+ raise ValueError(f'Unknown coordinate type {in_coord_type}. It must be "C" or "D".')
303
+ elif out_coord_type == 'D':
304
+ POSCAR.write('coords-type Lattice\n')
305
+ if in_coord_type == 'D':
306
+ pass
307
+ elif in_coord_type == 'C':
308
+ coord = coord @ np.linalg.inv(cell / 1.89036)
309
+ for ind, vx in enumerate(coord[:, :3]):
310
+ POSCAR.write(f'ion {atom_list[ind]:>3s}')
311
+ for xx in vx:
312
+ POSCAR.write(f'{xx: > 14.8f}')
313
+ POSCAR.write(f' {fixed[ind][0]}') # Selective Dynamics
314
+ POSCAR.write('\n')
315
+
316
+ def write_xyz(
317
+ elements,
318
+ coords,
319
+ cells,
320
+ energies,
321
+ numbers,
322
+ forces,
323
+ output_path: str,
324
+ filename_list: List[str]|None=None,
325
+ output_xyz_type: Literal['only_position_xyz','write_position_and_force']='only_position_xyz',
326
+ n_core:int=-1
327
+ ) -> None:
328
+ """
329
+ 'only_position_xyz':output .xyz file with only position,without force
330
+ 'write_position_and_force':output .xyz file with position and force
331
+ """
332
+
333
+ if output_xyz_type not in {'only_position_xyz','write_position_and_force'}:
334
+ raise ValueError(f"`output_xyz_type` must be 'only_position_xyz' or 'write_position_and_force',"
335
+ f" but occurred {output_xyz_type}.")
336
+ # loading data
337
+ n_batch = len(cells)
338
+ if filename_list is None:
339
+ filename_list = [f'{_}.xyz' for _ in range(n_batch)]
340
+ elif not isinstance(filename_list, (List, Tuple)):
341
+ raise TypeError(f'Invalid type of `filename_list`: {type(filename_list)}')
342
+ elif len(filename_list) != n_batch:
343
+ raise ValueError(f'Number of file names ({len(filename_list)}) and structures ({n_batch}) does not match')
344
+ # check n_core
345
+ if n_core == -1:
346
+ n_core = jb.cpu_count(True)
347
+ if n_core > n_batch:
348
+ n_core = n_batch
349
+
350
+ def _write_single(
351
+ file_name: str,
352
+ elements,
353
+ coords,
354
+ cells,
355
+ energies,
356
+ numbers,
357
+ forces,
358
+ ):
359
+ with open(os.path.join(output_path, file_name), "w") as xyz: # 遍历生成不同所有文件,并进行编写
360
+ xyz.write(f"{len(coords)}\n")#total number of atoms
361
+
362
+ cells_str = np.array2string(cells, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]"," ").replace("\n","").replace(" "," ")
363
+ xyz.write(f"Lattice ='{cells_str}' Properties=species:S:1:pos:R:3:forces:R:3 energy={energies:<.7e} pbc=T T T\n")
364
+ # generate elem_list which contain the element type of every structure. (data type:list(array()),)
365
+ elem_ = [[f"{elements[i]: <2s}"] * int(numbers[i]) for i in range(len(elements))]
366
+ elem = sum(elem_, [])
367
+ elem_list = np.array(elem).reshape((len(coords), 1))
368
+
369
+ if output_xyz_type == "write_position_and_force":
370
+ coo_force_str = AS_PRINT_COORDS(np.concatenate((coords, forces), axis=1))
371
+ elem_pos_force_array = np.concatenate((elem_list, coo_force_str), axis=1)
372
+ elem_pos_force = np.array2string(elem_pos_force_array, **STRING_ARRAY_FORMAT).replace("[", " ").replace("]"," ")#.replace("'", "")
373
+ xyz.write(f"{elem_pos_force}\n")
374
+ else:
375
+ elem_pos_array = np.concatenate((elem_list, AS_PRINT_COORDS(coords)), axis=1)
376
+ elem_pos_array = np.array2string(elem_pos_array, **STRING_ARRAY_FORMAT).replace("[", " ").replace(
377
+ "]", " ") # .replace("'", "")
378
+ xyz.write(f"{elem_pos_array}\n")
379
+
380
+ if n_core == 1:
381
+ for ii, fn in enumerate(filename_list):
382
+ _write_single(
383
+ fn,
384
+ elements[ii],
385
+ coords[ii],
386
+ cells[ii],
387
+ energies[ii],
388
+ numbers[ii],
389
+ forces[ii]
390
+ )
391
+ else:
392
+ _para = jb.Parallel(n_core, )
393
+ _para(
394
+ jb.delayed(_write_single)(
395
+ fn,
396
+ elements[ii],
397
+ coords[ii],
398
+ cells[ii],
399
+ energies[ii],
400
+ numbers[ii],
401
+ forces[ii]
402
+ )
403
+ for ii, fn in enumerate(filename_list)
404
+ )
405
+
406
+
407
+ def write_cif(
408
+ cells: Sequence[Sequence],
409
+ coords: Sequence[np.ndarray],
410
+ atom_labels: Sequence[Sequence[str]],
411
+ atom_numbers: Sequence[Sequence[int]],
412
+ output_path: str = './',
413
+ file_name_list: str | Sequence[str] = 'POSCAR',
414
+ system_list: str | Sequence[str] = 'untitled',
415
+ coord_type: List[Literal['C', 'D']] | Literal['C', 'D'] = 'C',
416
+ n_core: int = -1
417
+ ) -> None:
418
+ """
419
+ Convert coordinates matrices to POSCAR format, and write files to the "output_path".
420
+
421
+ Args:
422
+ cells: List|ndarray, a batch of lattice vectors. shape: (n_batch, 3, 3)
423
+ atom_labels: 2D list|ndarray of str, a batch of list of element symbols. shape: (n_batch, n_atom)
424
+ atom_numbers:2D list|ndarray of int, list of atom number of each element, in the order of atom_label. shape: (n_batch, n_atom)
425
+ coords: list|ndarray, the batch of atoms coordinates, in the order of atom_label. shape: (n_batch, n_atom, 3)
426
+ output_path: str, the output path.
427
+ file_name_list: list(str), the list of file names.
428
+ system_list: str|list(str), the 1st line of output file i.e., the annotation or title of the file.
429
+ coord_type: List[Literal['C', 'D']]|Literal['C', 'D'], 'D' or 'C', which means whether the input coordinates are "Direct" or "Cartesian".
430
+ If only a string, `coord_type` would set to be '[`coord_type`] * n_batch'.
431
+ n_core: int, the number of CPU cores to write files in parallel.
432
+
433
+ Returns: None
434
+
435
+ """
436
+ def _write_single(
437
+ file_name,
438
+ cell,
439
+ coord,
440
+ atom_label,
441
+ atom_number,
442
+ _coord_type,
443
+ system_name: str | Sequence[str] = 'untitled',
444
+ ):
445
+ atom_label_str = ' '.join(atom_label)
446
+ formula = ''.join([f'{atom_label[_]}{atom_number[_]}' for _ in range(len(atom_number))])
447
+
448
+ atom_label_list = list()
449
+ atom_sequence_list = list()
450
+ for i, num in enumerate(atom_number):
451
+ atom_label_list.extend([atom_label[i]] * num)
452
+ atom_sequence_list.extend(range(1, num + 1))
453
+
454
+ a, b, c = np.linalg.norm(cell, axis=1)
455
+ alpha = np.rad2deg(np.arccos(cell[1] @ cell[2] / (b * c)))
456
+ beta = np.rad2deg(np.arccos(cell[0] @ cell[2] / (a * c)))
457
+ gamma = np.rad2deg(np.arccos(cell[0] @ cell[1] / (a * b)))
458
+ volume = np.linalg.det(cell)
459
+ if volume < 0.: warnings.warn(f'The cell of {file_name} is a left-hand system.', RuntimeWarning)
460
+ volume = np.abs(volume)
461
+ if _coord_type == 'C':
462
+ coord = coord @ np.linalg.inv(cell)
463
+
464
+ with open(os.path.join(output_path, file_name), "w") as cif:
465
+ cif.write(f'# {system_name}\ndata_{formula}\n')
466
+ cif.write("_symmetry_space_group_name_H-M 'P 1'\n")
467
+ cif.write(
468
+ f'_cell_length_a {a}\n'
469
+ f'_cell_length_b {b}\n'
470
+ f'_cell_length_c {c}\n'
471
+ f'_cell_angle_alpha {alpha}\n'
472
+ f'_cell_angle_beta {beta}\n'
473
+ f'_cell_angle_gamma {gamma}\n'
474
+ '_symmetry_Int_Tables_number 1\n'
475
+ f'_chemical_formula_structural \'{atom_label_str}\'\n'
476
+ f'_chemical_formula_sum {formula}\n'
477
+ f'_cell_volume {volume}\n'
478
+ 'loop_\n'
479
+ ' _symmetry_equiv_pos_site_id\n'
480
+ ' _symmetry_equiv_pos_as_xyz\n'
481
+ ' 1 \'x, y, z\'\n'
482
+ 'loop_\n'
483
+ ' _atom_site_label\n'
484
+ ' _atom_site_type_symbol\n'
485
+ ' _atom_site_symmetry_multiplicity\n'
486
+ ' _atom_site_fract_x\n'
487
+ ' _atom_site_fract_y\n'
488
+ ' _atom_site_fract_z\n'
489
+ ' _atom_site_occupancy\n'
490
+ )
491
+ for i, coo in enumerate(coord):
492
+ cif.write(
493
+ f'{atom_label_list[i]}{atom_sequence_list[i]:<4d} {atom_label_list[i]} 1 '
494
+ f'{np.array2string(coo, **FLOAT_ARRAY_FORMAT)[1:-1]} 1\n'
495
+ )
496
+
497
+ # loading data
498
+ n_batch = len(cells)
499
+ if file_name_list is None:
500
+ filename_list = [f'{_}.cif' for _ in range(n_batch)]
501
+ elif not isinstance(file_name_list, (List, Tuple)):
502
+ raise TypeError(f'Invalid type of `filename_list`: {type(file_name_list)}')
503
+ elif len(file_name_list) != n_batch:
504
+ raise ValueError(f'Number of file names ({len(file_name_list)}) and structures ({n_batch}) does not match')
505
+ # check n_core
506
+ if n_core == -1:
507
+ n_core = jb.cpu_count(True)
508
+ if n_core > n_batch:
509
+ n_core = n_batch
510
+
511
+ _para = jb.Parallel(n_core, )
512
+ _para(
513
+ jb.delayed(_write_single)(
514
+ fn,
515
+ cells[ii],
516
+ coords[ii],
517
+ atom_labels[ii],
518
+ atom_numbers[ii],
519
+ coord_type[ii],
520
+ system_list[ii]
521
+ )
522
+ for ii, fn in enumerate(file_name_list)
523
+ )
524
+