kim-tools 0.2.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.
@@ -0,0 +1,1782 @@
1
+ """Tools for working with crystal prototypes using the AFLOW command line tool"""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from curses.ascii import isalpha, isdigit
10
+ from dataclasses import dataclass
11
+ from math import acos, cos, degrees, radians, sin, sqrt
12
+ from os import PathLike
13
+ from random import random
14
+ from tempfile import NamedTemporaryFile
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
+
17
+ import ase
18
+ import numpy as np
19
+ from ase import Atoms
20
+ from ase.cell import Cell
21
+ from numpy.typing import ArrayLike
22
+ from sympy import Symbol, linear_eq_to_matrix, matrix2numpy, parse_expr
23
+
24
+ from ..symmetry_util import (
25
+ A_CENTERED_ORTHORHOMBIC_GROUPS,
26
+ C_CENTERED_ORTHORHOMBIC_GROUPS,
27
+ CENTERING_DIVISORS,
28
+ IncorrectNumAtomsException,
29
+ are_in_same_wyckoff_set,
30
+ cartesian_rotation_is_in_point_group,
31
+ get_possible_primitive_shifts,
32
+ get_primitive_wyckoff_multiplicity,
33
+ get_wyck_pos_xform_under_normalizer,
34
+ space_group_numbers_are_enantiomorphic,
35
+ )
36
+
37
+ logger = logging.getLogger(__name__)
38
+ logging.basicConfig(filename="kim-tools.log", level=logging.INFO, force=True)
39
+
40
+
41
+ __author__ = ["ilia Nikiforov", "Ellad Tadmor"]
42
+ __all__ = [
43
+ "EquivalentEqnSet",
44
+ "EquivalentAtomSet",
45
+ "write_tmp_poscar_from_atoms_and_run_function",
46
+ "get_equivalent_atom_sets_from_prototype_and_atom_map",
47
+ "IncorrectSpaceGroupException",
48
+ "IncorrectSpeciesException",
49
+ "InconsistentWyckoffException",
50
+ "check_number_of_atoms",
51
+ "split_parameter_array",
52
+ "internal_parameter_sort_key",
53
+ "get_stoich_reduced_list_from_prototype",
54
+ "get_wyckoff_lists_from_prototype",
55
+ "prototype_labels_are_equivalent",
56
+ "get_space_group_number_from_prototype",
57
+ "get_pearson_symbol_from_prototype",
58
+ "get_bravais_lattice_from_prototype",
59
+ "read_shortnames",
60
+ "get_real_to_virtual_species_map",
61
+ "solve_for_aflow_cell_params_from_primitive_ase_cell_params",
62
+ "AFLOW",
63
+ ]
64
+
65
+
66
+ class IncorrectSpaceGroupException(Exception):
67
+ """
68
+ Raised when spglib or aflow --sgdata detects a different space group than the one
69
+ specified in the prototype label
70
+ """
71
+
72
+
73
+ class IncorrectSpeciesException(Exception):
74
+ """
75
+ Raised when number or identity of species is inconsistent
76
+ """
77
+
78
+
79
+ class InconsistentWyckoffException(Exception):
80
+ """
81
+ Raised when an insonsistency in Wyckoff positions is detected
82
+ """
83
+
84
+
85
+ @dataclass
86
+ class EquivalentEqnSet:
87
+ """
88
+ Set of equations representing the fractional positions of equivalent atoms
89
+ """
90
+
91
+ species: str
92
+ wyckoff_letter: str
93
+ # The n free parameters associated with this Wyckoff postition, 0 <= n <= 3
94
+ param_names: List[str]
95
+ # m x 3 x n matrices of coefficients, where m is the multiplicity of the Wyckoff
96
+ # position
97
+ coeff_matrix_list: List[ArrayLike]
98
+ # m x 3 x 1 columns of constant terms in the coordinates. This gets subtracted from
99
+ # the RHS when solving
100
+ const_terms_list: List[ArrayLike]
101
+
102
+
103
+ @dataclass
104
+ class EquivalentAtomSet:
105
+ """
106
+ Set of equivalent atoms
107
+ """
108
+
109
+ species: str
110
+ wyckoff_letter: str
111
+ frac_position_list: List[ArrayLike] # m x 3 x 1 columns
112
+
113
+
114
+ def write_tmp_poscar_from_atoms_and_run_function(
115
+ atoms: Atoms, function: callable, *args, **kwargs
116
+ ) -> Any:
117
+ """
118
+ Write the Atoms file to a NamedTemporaryFile and run 'function' on it.
119
+
120
+ Args:
121
+ atoms:
122
+ The atoms object that will be written to a POSCAR file and fed as the
123
+ first argument to function
124
+ function: A function that takes a POSCAR file as the first argument
125
+
126
+ Returns:
127
+ Whatever `function` returns
128
+ """
129
+ with NamedTemporaryFile("w+") as fp:
130
+ atoms.write(fp, sort=True, format="vasp")
131
+ fp.seek(0)
132
+ return function(fp.name, *args, **kwargs)
133
+
134
+
135
+ def check_number_of_atoms(
136
+ atoms: Atoms, prototype_label: str, primitive_cell: bool = True
137
+ ) -> None:
138
+ """
139
+ Check if the Atoms object (which must be a conventional or primitive unit cell)
140
+ has the correct number of atoms according to prototype_label
141
+
142
+ Raises:
143
+ IncorrectNumAtomsException
144
+ """
145
+ prototype_label_list = prototype_label.split("_")
146
+ pearson = prototype_label_list[1]
147
+
148
+ # get the number of atoms in conventional cell from the Pearson symbol
149
+ num_conv_cell = 0
150
+ for character in pearson:
151
+ if character.isdigit():
152
+ num_conv_cell *= 10
153
+ num_conv_cell += int(character)
154
+
155
+ centering = pearson[1]
156
+
157
+ if centering == "R":
158
+ num_conv_cell *= 3
159
+
160
+ if not primitive_cell:
161
+ num_lattice = 1
162
+ else:
163
+ num_lattice = CENTERING_DIVISORS[centering]
164
+
165
+ # This check is probably really extraneous, but better safe than sorry
166
+ if num_conv_cell % num_lattice != 0:
167
+ raise IncorrectNumAtomsException(
168
+ f"WARNING: Number of atoms in conventional cell {num_conv_cell} derived "
169
+ f"from Pearson symbol of prototype {prototype_label} is not divisible by "
170
+ f"the number of lattice points {num_lattice}"
171
+ )
172
+
173
+ num_cell = num_conv_cell / num_lattice
174
+
175
+ if len(atoms) != num_cell:
176
+ raise IncorrectNumAtomsException(
177
+ f"WARNING: Number of ASE atoms {len(atoms)} does not match Pearson symbol "
178
+ f" of prototype {prototype_label}"
179
+ )
180
+
181
+
182
+ def split_parameter_array(
183
+ parameter_names: List[str], list_to_split: Optional[List] = None
184
+ ) -> Tuple[List, List]:
185
+ """
186
+ Split a list of parameters into cell and internal parameters.
187
+
188
+ Args:
189
+ parameter_names:
190
+ List of AFLOW parameter names, e.g.
191
+ `["a", "c/a", "x1", "x2", "y2", "z2"]`
192
+ Proper AFLOW order is assumed, i.e. cell parameters
193
+ first, then internal.
194
+ list_to_split:
195
+ List to split, must be same length as `parameter_names`
196
+ If omitted, `parameter_names` itself will be split
197
+
198
+ Returns:
199
+ `list_to_split` (or `parameter_names` if `list_to_split` is omitted),
200
+ split into lists corresponding to the split between cell and internal parameters
201
+
202
+ Raises:
203
+ AssertionError:
204
+ If lengths are incompatible or if `parameter_names` fails an (incomplete)
205
+ check that it is a sensible list of AFLOW parameters
206
+ """
207
+ if list_to_split is None:
208
+ list_to_split = parameter_names
209
+
210
+ assert len(list_to_split) == len(
211
+ parameter_names
212
+ ), "`list_to_split` must have the same length as `parameter_names`"
213
+
214
+ in_internal_part = False
215
+
216
+ cell_part = []
217
+ internal_part = []
218
+
219
+ CARTESIAN_AXES = ["x", "y", "z"]
220
+
221
+ for name, value in zip(parameter_names, list_to_split):
222
+ assert isinstance(
223
+ name, str
224
+ ), "At least one element of `parameter_names` is not a string."
225
+ if not in_internal_part:
226
+ if name[0] in CARTESIAN_AXES:
227
+ in_internal_part = True
228
+ else:
229
+ # means we have already encountered
230
+ # an internal coordinate in a past iteration
231
+ assert name[0] in CARTESIAN_AXES, (
232
+ "`parameter_names` seems to have an internal parameter "
233
+ "followed by a non-internal one"
234
+ )
235
+
236
+ if in_internal_part:
237
+ internal_part.append(value)
238
+ else:
239
+ cell_part.append(value)
240
+
241
+ return cell_part, internal_part
242
+
243
+
244
+ def internal_parameter_sort_key(parameter_name: Union[Symbol, str]) -> int:
245
+ """
246
+ Sorting key for internal free parameters. Sort by number first, then letter
247
+ """
248
+ parameter_name_str = str(parameter_name)
249
+ axis = parameter_name_str[0]
250
+ assert (
251
+ axis == "x" or axis == "y" or axis == "z"
252
+ ), "Parameter name must start with x, y, or z"
253
+ number = int(parameter_name_str[1:])
254
+ return 1000 * number + ord(axis)
255
+
256
+
257
+ def get_equivalent_atom_sets_from_prototype_and_atom_map(
258
+ atoms: Atoms, prototype_label: str, atom_map: List[int], sort_atoms: bool = False
259
+ ) -> List[EquivalentAtomSet]:
260
+ """
261
+ Get a list of objects representing sets of equivalent atoms from the atom_map of an
262
+ AFLOW comparison
263
+
264
+ The AFLOW comparison should be between `atoms` and `atoms_rebuilt`, in that order,
265
+ where `atoms_rebuilt` is regenerated from the prototype designation detected from
266
+ `atoms`, and `prototype_label` is the detected prototype label
267
+
268
+ Args:
269
+ sort_atoms: If `atom_map` was obtained by sorting `atoms` before writing it to
270
+ POSCAR, set this to True
271
+ """
272
+
273
+ if sort_atoms:
274
+ with NamedTemporaryFile("w+") as f:
275
+ atoms.write(f.name, format="vasp", sort=True)
276
+ f.seek(0)
277
+ atoms_in_correct_order = ase.io.read(f.name, format="vasp")
278
+ else:
279
+ atoms_in_correct_order = atoms
280
+
281
+ # initialize return list
282
+ equivalent_atom_set_list = []
283
+
284
+ redetected_wyckoff_lists = get_wyckoff_lists_from_prototype(prototype_label)
285
+ sg_num = get_space_group_number_from_prototype(prototype_label)
286
+
287
+ index_in_atoms_rebuilt = 0
288
+ for i_species, species_wyckoff_list in enumerate(redetected_wyckoff_lists):
289
+ virtual_species_letter = chr(65 + i_species)
290
+ for wyckoff_letter in species_wyckoff_list:
291
+ equivalent_atom_set_list.append(
292
+ EquivalentAtomSet(virtual_species_letter, wyckoff_letter, [])
293
+ )
294
+ for _ in range(get_primitive_wyckoff_multiplicity(sg_num, wyckoff_letter)):
295
+ # atom_map[index_in_atoms] = index_in_atoms_rebuilt
296
+ index_in_atoms = atom_map.index(index_in_atoms_rebuilt)
297
+ equivalent_atom_set_list[-1].frac_position_list.append(
298
+ atoms_in_correct_order.get_scaled_positions()[
299
+ index_in_atoms
300
+ ].reshape(3, 1)
301
+ )
302
+ index_in_atoms_rebuilt += 1
303
+
304
+ return equivalent_atom_set_list
305
+
306
+
307
+ def get_stoich_reduced_list_from_prototype(prototype_label: str) -> List[int]:
308
+ """
309
+ Get numerical list of stoichiometry from prototype label, i.e. "AB3_hP8..." -> [1,3]
310
+
311
+ Args:
312
+ prototype_label:
313
+ AFLOW prototype label
314
+
315
+ Returns:
316
+ List of reduced stoichiometric numbers
317
+ """
318
+ stoich_reduced_formula = prototype_label.split("_")[0]
319
+ stoich_reduced_list = []
320
+ stoich_reduced_curr = None
321
+ for char in stoich_reduced_formula:
322
+ if isalpha(char):
323
+ if stoich_reduced_curr is not None:
324
+ if stoich_reduced_curr == 0:
325
+ stoich_reduced_curr = 1
326
+ stoich_reduced_list.append(stoich_reduced_curr)
327
+ stoich_reduced_curr = 0
328
+ else:
329
+ assert isdigit(char)
330
+ # will throw an error if we haven't encountered an alphabetical letter, good
331
+ stoich_reduced_curr *= 10
332
+ stoich_reduced_curr += int(char)
333
+ # write final number
334
+ if stoich_reduced_curr == 0:
335
+ stoich_reduced_curr = 1
336
+ stoich_reduced_list.append(stoich_reduced_curr)
337
+ return stoich_reduced_list
338
+
339
+
340
+ def get_wyckoff_lists_from_prototype(prototype_label: str) -> List[str]:
341
+ """
342
+ Expand the list of Wyckoff letters in the prototype to account for each individual
343
+ letter instead of using numerical multipliers for repeated letters.
344
+ e.g. A2B3C_mC48_15_aef_3f_2e -> ['aef','fff','ee']
345
+ """
346
+ expanded_wyckoff_letters = []
347
+ prototype_label_split = prototype_label.split("_")
348
+ for species_wyckoff_string in prototype_label_split[3:]:
349
+ expanded_wyckoff_letters.append("")
350
+ curr_wyckoff_count = 0
351
+ for char in species_wyckoff_string:
352
+ if isalpha(char):
353
+ if curr_wyckoff_count == 0:
354
+ curr_wyckoff_count = 1
355
+ expanded_wyckoff_letters[-1] += char * curr_wyckoff_count
356
+ curr_wyckoff_count = 0
357
+ else:
358
+ assert isdigit(char)
359
+ curr_wyckoff_count *= 10 # if it's zero, we're all good
360
+ curr_wyckoff_count += int(char)
361
+ return expanded_wyckoff_letters
362
+
363
+
364
+ def prototype_labels_are_equivalent(
365
+ prototype_label_1: str,
366
+ prototype_label_2: str,
367
+ allow_enantiomorph: bool = False,
368
+ allow_species_permutation: bool = False,
369
+ ) -> bool:
370
+ """
371
+ Checks if two prototype labels are equivalent
372
+ """
373
+ if allow_species_permutation:
374
+ # TODO: Possibly add this (for checking library prototype labels)
375
+ raise NotImplementedError("Species permutations not implemented")
376
+
377
+ if prototype_label_1 == prototype_label_2:
378
+ return True
379
+
380
+ # Check stoichiometry
381
+ stoich_reduced_list_1 = get_stoich_reduced_list_from_prototype(prototype_label_1)
382
+
383
+ if not stoich_reduced_list_1 == get_stoich_reduced_list_from_prototype(
384
+ prototype_label_2
385
+ ):
386
+ logger.info(
387
+ "Found non-matching stoichiometry in labels "
388
+ f"{prototype_label_1} and {prototype_label_2}"
389
+ )
390
+ return False
391
+
392
+ # Check Pearson symbol
393
+ if not get_pearson_symbol_from_prototype(
394
+ prototype_label_1
395
+ ) == get_pearson_symbol_from_prototype(prototype_label_2):
396
+ logger.info(
397
+ "Found non-matching Pearson symbol in labels "
398
+ f"{prototype_label_1} and {prototype_label_2}"
399
+ )
400
+ return False
401
+
402
+ # Check space group number
403
+ sg_num_1 = get_space_group_number_from_prototype(prototype_label_1)
404
+ sg_num_2 = get_space_group_number_from_prototype(prototype_label_2)
405
+ if allow_enantiomorph and not space_group_numbers_are_enantiomorphic(
406
+ sg_num_2, sg_num_1
407
+ ):
408
+ logger.info(
409
+ "Found non-matching Space group in labels "
410
+ f"{prototype_label_1} and {prototype_label_2}"
411
+ )
412
+ return False
413
+ elif sg_num_2 != sg_num_1:
414
+ logger.info(
415
+ "Found non-matching Space group in labels "
416
+ f"{prototype_label_1} and {prototype_label_2}"
417
+ )
418
+ return False
419
+
420
+ # OK, so far everything matches, now check the Wyckoff letters
421
+ # Get lists of Wyckoff letters for each species,
422
+ # e.g. A2B3C_mC48_15_aef_3f_2e -> ['aef', 'fff', 'ee']
423
+ wyckoff_lists_1 = get_wyckoff_lists_from_prototype(prototype_label_1)
424
+ wyckoff_lists_2 = get_wyckoff_lists_from_prototype(prototype_label_2)
425
+ num_species = len(stoich_reduced_list_1)
426
+ assert len(wyckoff_lists_1) == len(wyckoff_lists_2) == num_species, (
427
+ "Somehow I got non-matching lists of Wyckoff letters, "
428
+ "the prototype labels are probably malformed"
429
+ )
430
+
431
+ if sg_num_1 >= 16:
432
+ # Theoretically, unless we are allowing species permutations, orthorhombic and
433
+ # higher SGs should always have identical prototype labels due to minimal
434
+ # Wyckoff enumeration. However, there are bugs in AFLOW making this untrue.
435
+
436
+ wyck_pos_xform_list = get_wyck_pos_xform_under_normalizer(sg_num_1)
437
+ for wyck_pos_xform in wyck_pos_xform_list:
438
+ wyckoff_lists_match_for_each_species = True
439
+ for i in range(num_species):
440
+ wyckoff_list_ref = wyckoff_lists_1[i]
441
+ wyckoff_list_test = ""
442
+ for test_letter in wyckoff_lists_2[i]:
443
+ if test_letter == "A":
444
+ # SG47 runs out of the alphabet and uses capital A for its
445
+ # general position (which is unchanged under any transformation
446
+ # in the normalizer). However, we need "A" to be last, so we
447
+ # make it "{" to make it sort after all lowercase letters and
448
+ # replace it after
449
+ wyckoff_list_test += "{"
450
+ else:
451
+ test_letter_index = ord(test_letter) - 97
452
+ wyckoff_list_test += wyck_pos_xform[test_letter_index]
453
+ wyckoff_list_test = "".join(sorted(wyckoff_list_test))
454
+ wyckoff_list_test = wyckoff_list_test.replace("{", "A")
455
+ if wyckoff_list_test != wyckoff_list_ref:
456
+ wyckoff_lists_match_for_each_species = False
457
+ break
458
+ if wyckoff_lists_match_for_each_species:
459
+ logger.warning(
460
+ f"Labels {prototype_label_1} and {prototype_label_2} were found to "
461
+ "be equivalent despite being non-identical. "
462
+ "This indicates a failure to find the lowest Wyckoff enumeration."
463
+ )
464
+ return True
465
+ logger.info(
466
+ f"Labels {prototype_label_1} and {prototype_label_2} were not found to be "
467
+ "equivalent under any permutations allowable by the normalizer."
468
+ )
469
+ return False
470
+ else:
471
+ for wyckoff_list_1, wyckoff_list_2 in zip(wyckoff_lists_1, wyckoff_lists_2):
472
+ for letter_1, letter_2 in zip(wyckoff_list_1, wyckoff_list_2):
473
+ # Wyckoff sets are alphabetically contiguous in SG1-15, there is no
474
+ # need to re-sort anything.
475
+ # This is NOT true for all SGs (e.g. #200, Wyckoff set eh )
476
+ if not are_in_same_wyckoff_set(letter_1, letter_2, sg_num_1):
477
+ logger.info(
478
+ f"Labels {prototype_label_1} and {prototype_label_2} have "
479
+ f"corresponding letters {letter_1} and {letter_2} that are not "
480
+ "in the same Wyckoff set"
481
+ )
482
+ return False
483
+ logger.info(
484
+ f"Labels {prototype_label_1} and {prototype_label_2} were found to be "
485
+ "equivalent despite being non-identical. This is a normal occurrence for "
486
+ "triclinic and monoclinic space groups such as this."
487
+ )
488
+ return True
489
+
490
+
491
+ def get_space_group_number_from_prototype(prototype_label: str) -> int:
492
+ return int(prototype_label.split("_")[2])
493
+
494
+
495
+ def get_pearson_symbol_from_prototype(prototype_label: str) -> str:
496
+ return prototype_label.split("_")[1]
497
+
498
+
499
+ def get_bravais_lattice_from_prototype(prototype_label: str) -> str:
500
+ return get_pearson_symbol_from_prototype(prototype_label)[:2]
501
+
502
+
503
+ def read_shortnames() -> Dict:
504
+ """
505
+ This function parses "README_PROTO.TXT". It finds each line that (after stripping
506
+ whitespace) starts with "ANRL Label". These are headers of sections of prototype
507
+ listings. It finds the column of the word "notes". This will be the column that the
508
+ shortnames are in. Skipping various non-prototype lines, the first column in each
509
+ prototype line (before the ".") is the prototype, while the end of the line
510
+ starting from the "notes" column, cleaned up to remove whitespace and
511
+ end-of-shortname comments (i.e. "(part 3)"), is the shortname.
512
+
513
+ Returns:
514
+ A dictionary where the keys are the prototype strings, and the values are the
515
+ shortnames found in the corresponding lines.
516
+ """
517
+ shortnames = {}
518
+ shortname_file = "data/README_PROTO.TXT"
519
+ notes_index = None
520
+ with open(
521
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), shortname_file),
522
+ encoding="utf-8",
523
+ ) as f:
524
+ for line in f:
525
+ line = line.strip()
526
+ if line.startswith("ANRL Label"):
527
+ try:
528
+ notes_index = line.index("notes")
529
+ continue
530
+ except Exception:
531
+ print("ERROR: ANRL Label line without notes header")
532
+ print(line)
533
+ sys.exit()
534
+ # Skip this line if it's before the first ANRL label
535
+ if notes_index is None:
536
+ continue
537
+ # Skip this line if it's empty, a comment, or a divider
538
+ if (
539
+ line == ""
540
+ or line == "\n"
541
+ or line.startswith("*")
542
+ or line.startswith("-")
543
+ or line.startswith("ANRL")
544
+ ):
545
+ continue
546
+ # Skip this line if it only has content in the first column
547
+ # (prototype runover from previous line)
548
+ try:
549
+ _ = line.split(" ")[1]
550
+ except Exception:
551
+ continue
552
+ # Clean up prototype (remove decorations suffix)
553
+ prototype = line.split(" ")[0]
554
+ if "." in prototype:
555
+ idx = prototype.index(".")
556
+ prototype = prototype[:idx]
557
+ # Clean up short name
558
+ sname = line[notes_index:]
559
+ if "(part " in sname:
560
+ idx = sname.index("(part")
561
+ sname = sname[:idx]
562
+ sname = sname.replace(", part 3", "")
563
+ if "ICSD" in sname:
564
+ idx = sname.index("ICSD")
565
+ tmp = sname[idx:].split(" ")[1]
566
+ if tmp.endswith(","):
567
+ sname = sname[:idx] + "ICSD " + tmp[:-1]
568
+ if " similar to" in sname:
569
+ idx = sname.index(" similar to")
570
+ sname = sname[:idx]
571
+ if " equivalent to" in sname:
572
+ idx = sname.index(" equivalent to")
573
+ sname = sname[:idx]
574
+ if sname.endswith(","):
575
+ sname = sname[:-1]
576
+ # add prototype to shortnames dictionary
577
+ shortnames[prototype] = sname.rstrip()
578
+ return shortnames
579
+
580
+
581
+ def get_real_to_virtual_species_map(input: Union[List[str], Atoms]) -> Dict:
582
+ """
583
+ Map real species to virtual species according to (alphabetized) AFLOW convention,
584
+ e.g. for SiC return {'C':'A', 'Si':'B'}
585
+ """
586
+ if isinstance(input, Atoms):
587
+ species = sorted(list(set(input.get_chemical_symbols())))
588
+ else:
589
+ species = input
590
+
591
+ real_to_virtual_species_map = {}
592
+ for i, symbol in enumerate(species):
593
+ real_to_virtual_species_map[symbol] = chr(65 + i)
594
+
595
+ return real_to_virtual_species_map
596
+
597
+
598
+ def solve_for_aflow_cell_params_from_primitive_ase_cell_params(
599
+ cellpar_prim: ArrayLike, prototype_label: str
600
+ ) -> List[float]:
601
+ """
602
+ Get conventional cell parameters from primitive cell parameters. It is assumed that
603
+ the primitive cell is related to the conventional cell as specified in
604
+ 10.1016/j.commatsci.2017.01.017. Equations obtained from Wolfram notebook in
605
+ ``scripts/cell_param_solver.nb``
606
+
607
+ Args:
608
+ cellpar_prim:
609
+ The 6 cell parameters of the primitive unit cell:
610
+ [a, b, c, alpha, beta, gamma]
611
+ prototype_label:
612
+ The AFLOW prototype label of the crystal
613
+
614
+ Returns:
615
+ The cell parameters expected by AFLOW for the prototype label provided. The
616
+ first parameter is always "a" and is given in the same units as
617
+ ``cellpar_prim``, the others are fractional parameters in terms of "a", or
618
+ angles in degrees. For example, if the ``prototype_label`` provided indicates a
619
+ monoclinic crystal, this function will return the values of [a, b/a, c/a, beta]
620
+ """
621
+ bravais_lattice = get_bravais_lattice_from_prototype(prototype_label)
622
+
623
+ assert len(cellpar_prim) == 6, "Got a number of cell parameters that is not 6"
624
+
625
+ for length in cellpar_prim[0:3]:
626
+ assert length > 0, "Got a negative cell size"
627
+ for angle in cellpar_prim[3:]:
628
+ assert 0 < angle < 180, "Got a cell angle outside of (0,180)"
629
+
630
+ aprim = cellpar_prim[0]
631
+ bprim = cellpar_prim[1]
632
+ cprim = cellpar_prim[2]
633
+ alphaprim = cellpar_prim[3]
634
+ betaprim = cellpar_prim[4]
635
+ gammaprim = cellpar_prim[5]
636
+
637
+ if bravais_lattice == "aP":
638
+ return [aprim, bprim / aprim, cprim / aprim, alphaprim, betaprim, gammaprim]
639
+ elif bravais_lattice == "mP":
640
+ return [aprim, bprim / aprim, cprim / aprim, betaprim]
641
+ elif bravais_lattice == "oP":
642
+ return [aprim, bprim / aprim, cprim / aprim]
643
+ elif bravais_lattice == "tP" or bravais_lattice == "hP":
644
+ return [aprim, cprim / aprim]
645
+ elif bravais_lattice == "cP":
646
+ return [aprim]
647
+ elif bravais_lattice == "mC":
648
+ cos_alphaprim = cos(radians(alphaprim))
649
+ cos_gammaprim = cos(radians(gammaprim))
650
+ a = aprim * sqrt(2 + 2 * cos_gammaprim)
651
+ b = aprim * sqrt(2 - 2 * cos_gammaprim)
652
+ c = cprim
653
+ beta = degrees(acos(cos_alphaprim / sqrt((1 + cos_gammaprim) / 2)))
654
+ return [a, b / a, c / a, beta]
655
+ elif bravais_lattice == "oC":
656
+ # the 'C' is colloquial, and can refer to either C or A-centering
657
+ space_group_number = get_space_group_number_from_prototype(prototype_label)
658
+ if space_group_number in C_CENTERED_ORTHORHOMBIC_GROUPS:
659
+ cos_gammaprim = cos(radians(gammaprim))
660
+ a = bprim * sqrt(2 + 2 * cos_gammaprim)
661
+ b = bprim * sqrt(2 - 2 * cos_gammaprim)
662
+ c = cprim
663
+ elif space_group_number in A_CENTERED_ORTHORHOMBIC_GROUPS:
664
+ cos_alphaprim = cos(radians(alphaprim))
665
+ a = aprim
666
+ b = bprim * sqrt(2 + 2 * cos_alphaprim)
667
+ c = bprim * sqrt(2 - 2 * cos_alphaprim)
668
+ else:
669
+ raise IncorrectSpaceGroupException(
670
+ f"Space group in prototype label {prototype_label} not found in lists "
671
+ "of side-centered orthorhombic groups"
672
+ )
673
+ return [a, b / a, c / a]
674
+ elif bravais_lattice == "oI":
675
+ cos_alphaprim = cos(radians(alphaprim))
676
+ cos_betaprim = cos(radians(betaprim))
677
+ a = aprim * sqrt(2 + 2 * cos_alphaprim)
678
+ b = aprim * sqrt(2 + 2 * cos_betaprim)
679
+ # I guess the cosines must sum to a negative number!?
680
+ # Will raise a ValueError: math domain error if not
681
+ c = aprim * sqrt(-2 * (cos_alphaprim + cos_betaprim))
682
+ return [a, b / a, c / a]
683
+ elif bravais_lattice == "oF":
684
+ aprimsq = aprim * aprim
685
+ bprimsq = bprim * bprim
686
+ cprimsq = cprim * cprim
687
+ a = sqrt(2 * (-aprimsq + bprimsq + cprimsq))
688
+ b = sqrt(2 * (aprimsq - bprimsq + cprimsq))
689
+ c = sqrt(2 * (aprimsq + bprimsq - cprimsq))
690
+ return [a, b / a, c / a]
691
+ elif bravais_lattice == "tI":
692
+ cos_alphaprim = cos(radians(alphaprim))
693
+ a = aprim * sqrt(2 + 2 * cos_alphaprim)
694
+ # I guess primitive alpha is always obtuse!? Will raise a
695
+ # ValueError: math domain error if not
696
+ c = 2 * aprim * sqrt(-cos_alphaprim)
697
+ return [a, c / a]
698
+ elif bravais_lattice == "hR":
699
+ cos_alphaprim = cos(radians(alphaprim))
700
+ a = aprim * sqrt(2 - 2 * cos_alphaprim)
701
+ c = aprim * sqrt(3 + 6 * cos_alphaprim)
702
+ return [a, c / a]
703
+ elif bravais_lattice == "cF":
704
+ return [aprim * sqrt(2)]
705
+ elif bravais_lattice == "cI":
706
+ return [aprim * 2 / sqrt(3)]
707
+
708
+
709
+ class AFLOW:
710
+ """
711
+ Class enabling access to the AFLOW executable
712
+
713
+ Attributes:
714
+ aflow_executable (str): Name of the AFLOW executable
715
+ aflow_work_dir (str): Path to the work directory
716
+ np (int):
717
+ Number of processors to use, passed to the AFLOW executable using the
718
+ ``--np=...`` argument
719
+
720
+ """
721
+
722
+ class ChangedSymmetryException(Exception):
723
+ """
724
+ Raised when an unexpected symmetry change is detected
725
+ """
726
+
727
+ class FailedToMatchException(Exception):
728
+ """
729
+ Raised when ``aflow --compare...`` fails to match
730
+ """
731
+
732
+ class FailedToSolveException(Exception):
733
+ """
734
+ Raised when solution algorithm fails
735
+ """
736
+
737
+ def __init__(
738
+ self, aflow_executable: str = "aflow", aflow_work_dir: str = "", np: int = 4
739
+ ):
740
+ """
741
+ Args:
742
+ aflow_executable: Sets :attr:`aflow_executable`
743
+ aflow_work_dir: Sets :attr:`aflow_work_dir`
744
+ np: Sets :attr:`np`
745
+ """
746
+ self.aflow_executable = aflow_executable
747
+ self.np = np
748
+ if aflow_work_dir != "" and not aflow_work_dir.endswith("/"):
749
+ self.aflow_work_dir = aflow_work_dir + "/"
750
+ else:
751
+ self.aflow_work_dir = aflow_work_dir
752
+
753
+ def aflow_command(self, cmd: Union[str, List[str]], verbose=True) -> str:
754
+ """
755
+ Run AFLOW executable with specified arguments and return the output, possibly
756
+ multiple times piping outputs to each other
757
+
758
+ Args:
759
+ cmd:
760
+ List of arguments to pass to each AFLOW executable.
761
+ If it's longer than 1, multiple commands will be piped to each other
762
+ verbose: Whether to echo command to log file
763
+
764
+ Raises:
765
+ AFLOW.ChangedSymmetryException:
766
+ if an ``aflow --proto=`` command complains that
767
+ "the structure has a higher symmetry than indicated by the label"
768
+
769
+ Returns:
770
+ Output of the AFLOW command
771
+ """
772
+ if not isinstance(cmd, list):
773
+ cmd = [cmd]
774
+
775
+ cmd_list = [
776
+ self.aflow_executable + " --np=" + str(self.np) + " " + cmd_inst
777
+ for cmd_inst in cmd
778
+ ]
779
+ cmd_str = " | ".join(cmd_list)
780
+ if verbose:
781
+ logger.info(cmd_str)
782
+ try:
783
+ return subprocess.check_output(
784
+ cmd_str, shell=True, stderr=subprocess.PIPE, encoding="utf-8"
785
+ )
786
+ except subprocess.CalledProcessError as exc:
787
+ if "--proto=" in cmd_str and (
788
+ "The structure has a higher symmetry than indicated by the "
789
+ "label. The correct label and parameters for this structure are:"
790
+ ) in str(exc.stderr):
791
+ warn_str = (
792
+ "WARNING: the following command refused to write a POSCAR because "
793
+ f"it detected a higher symmetry: {cmd_str}. "
794
+ f"AFLOW error follows:\n{str(exc.stderr)}"
795
+ )
796
+ logger.warning(warn_str)
797
+ raise self.ChangedSymmetryException(warn_str)
798
+ else:
799
+ raise exc
800
+
801
+ def write_poscar_from_prototype(
802
+ self,
803
+ prototype_label: str,
804
+ species: Optional[List[str]] = None,
805
+ parameter_values: Optional[List[float]] = None,
806
+ output_file: Optional[str] = None,
807
+ verbose: bool = True,
808
+ addtl_args: str = "",
809
+ ) -> Optional[str]:
810
+ """
811
+ Run the ``aflow --proto`` command to write a POSCAR coordinate file
812
+ corresponding to the provided AFLOW prototype designation.
813
+ This file will have fractional coordinates.
814
+
815
+ Args:
816
+ prototype_label:
817
+ An AFLOW prototype label, with or without an enumeration suffix
818
+ species:
819
+ List of stoichiometric species of the crystal. If this is omitted,
820
+ the file will be written without species info
821
+ parameter_values:
822
+ The free parameters of the AFLOW prototype designation. If an
823
+ enumeration suffix is not included in `prototype_label`
824
+ and the prototype has free parameters besides `a`, this must be provided
825
+ output_file:
826
+ Name of the output file. If not provided,
827
+ the output is returned as a string
828
+ verbose: Whether to echo command to log file
829
+ addtl_args:
830
+ additional arguments to pass, e.g. ``--equations_only`` to get equations
831
+
832
+ Returns:
833
+ The output of the command or None if an `output_file` was given
834
+
835
+ Raises:
836
+ AFLOW.ChangedSymmetryException:
837
+ if an ``aflow --proto=`` command complains that
838
+ "the structure has a higher symmetry than indicated by the label"
839
+ """
840
+ command = " --proto=" + prototype_label
841
+ if parameter_values:
842
+ command += " --params=" + ",".join(
843
+ [str(param) for param in parameter_values]
844
+ )
845
+
846
+ command += " " + addtl_args
847
+
848
+ try:
849
+ poscar_string_no_species = self.aflow_command(command, verbose=verbose)
850
+ except self.ChangedSymmetryException as e:
851
+ # re-raise, just indicating that this function knows about this exception
852
+ raise e
853
+
854
+ if species is None:
855
+ poscar_string = poscar_string_no_species
856
+ else:
857
+ poscar_string = ""
858
+ for i, line in enumerate(
859
+ poscar_string_no_species.splitlines(keepends=True)
860
+ ):
861
+ poscar_string += line
862
+ if i == 4:
863
+ poscar_string += " ".join(species) + "\n"
864
+ if output_file is None:
865
+ return poscar_string
866
+ else:
867
+ with open(self.aflow_work_dir + output_file, "w") as f:
868
+ f.write(poscar_string)
869
+
870
+ def build_atoms_from_prototype(
871
+ self,
872
+ prototype_label: str,
873
+ species: List[str],
874
+ parameter_values: Optional[List[float]] = None,
875
+ proto_file: Optional[str] = None,
876
+ verbose: bool = True,
877
+ ) -> Atoms:
878
+ """
879
+ Build an atoms object from an AFLOW prototype designation
880
+
881
+ Args:
882
+ prototype_label:
883
+ An AFLOW prototype label, with or without an enumeration suffix
884
+ species:
885
+ Stoichiometric species, e.g. ["Mo", "S"] corresponding to A and B
886
+ respectively for prototype label AB2_hP6_194_c_f indicating molybdenite
887
+ parameter_values:
888
+ The free parameters of the AFLOW prototype designation. If an
889
+ enumeration suffix is not included in `prototype_label`
890
+ and the prototype has free parameters besides `a`, this must be provided
891
+ proto_file:
892
+ Write the POSCAR to this permanent file for debugging
893
+ instead of a temporary file
894
+ verbose:
895
+ Print details in the log file
896
+
897
+ Returns:
898
+ Object representing unit cell of the material
899
+
900
+ Raises:
901
+ AFLOW.ChangedSymmetryException:
902
+ if an ``aflow --proto=`` command complains that
903
+ "the structure has a higher symmetry than indicated by the label"
904
+ """
905
+ try:
906
+ poscar_string = self.write_poscar_from_prototype(
907
+ prototype_label=prototype_label,
908
+ species=species,
909
+ parameter_values=parameter_values,
910
+ verbose=verbose,
911
+ )
912
+ except self.ChangedSymmetryException as e:
913
+ # re-raise, just indicating that this function knows about this exception
914
+ raise e
915
+
916
+ with (
917
+ NamedTemporaryFile(mode="w+")
918
+ if proto_file is None
919
+ else open(proto_file, mode="w+")
920
+ ) as f:
921
+ f.write(poscar_string)
922
+ f.seek(0)
923
+ atoms = ase.io.read(f.name, format="vasp")
924
+ check_number_of_atoms(atoms, prototype_label)
925
+ atoms.wrap()
926
+ return atoms
927
+
928
+ def compare_materials_dir(
929
+ self, materials_subdir: str, no_scale_volume: bool = True
930
+ ) -> List[Dict]:
931
+ """
932
+ Compare a directory of materials using the aflow --compare_materials -D tool
933
+
934
+ Args:
935
+ materials_subdir:
936
+ Path to the directory to compare from self.aflow_work_dir
937
+ no_scale_volume:
938
+ If `True`, the default behavior of allowing arbitrary scaling of
939
+ structures before comparison is turned off
940
+
941
+ Returns:
942
+ Attributes of representative structures, their duplicates, and groups
943
+ as a whole
944
+ """
945
+ # TODO: For efficiency, it is possible to --add_aflow_prototype_designation to
946
+ # the representative structures. This does not help if we need duplicate
947
+ # prototypes (for refdata), nor for library protos (as they are not ranked by
948
+ # match like we need)
949
+ command = " --compare_materials -D "
950
+ command += self.aflow_work_dir + materials_subdir
951
+ if no_scale_volume:
952
+ command += " --no_scale_volume"
953
+ output = self.aflow_command([command + " --screen_only --quiet --print=json"])
954
+ res_json = json.loads(output)
955
+ return res_json
956
+
957
+ def get_aflow_version(self) -> str:
958
+ """
959
+ Run the ``aflow --version`` command to get the aflow version
960
+
961
+ Returns:
962
+ aflow++ executable version
963
+ """
964
+ command = " --version"
965
+ output = self.aflow_command([command])
966
+ return output.strip().split()[2]
967
+
968
+ def compare_to_prototypes(self, input_file: str, prim: bool = True) -> List[Dict]:
969
+ """
970
+ Run the ``aflow --compare2prototypes`` command to compare the input structure
971
+ to the AFLOW library of curated prototypes
972
+
973
+ Args:
974
+ input_file: path to the POSCAR file containing the structure to compare
975
+ prim: whether to primitivize the structure first
976
+
977
+ Returns:
978
+ JSON list of dictionaries containing information about matching prototypes.
979
+ In practice, this list should be of length zero or 1
980
+ """
981
+ if prim:
982
+ command = [
983
+ " --prim < " + self.aflow_work_dir + input_file,
984
+ " --compare2prototypes --catalog=anrl --quiet --print=json",
985
+ ]
986
+ else:
987
+ command = (
988
+ " --compare2prototypes --catalog=anrl --quiet --print=json < "
989
+ + self.aflow_work_dir
990
+ + input_file
991
+ )
992
+
993
+ output = self.aflow_command(command)
994
+ res_json = json.loads(output)
995
+ return res_json
996
+
997
+ def get_prototype_designation_from_file(
998
+ self,
999
+ input_file: str,
1000
+ prim: bool = True,
1001
+ force_wyckoff: bool = False,
1002
+ verbose: bool = False,
1003
+ ) -> Dict:
1004
+ """
1005
+ Run the ``aflow --prototype`` command to get the AFLOW prototype designation
1006
+ of the input structure
1007
+
1008
+ Args:
1009
+ input_file: path to the POSCAR file containing the structure to analyze
1010
+ prim: whether to primitivize the structure first. Faster
1011
+ force_wyckoff:
1012
+ If the input is cif, do this to avoid re-analysis and just take the
1013
+ parameters as-is
1014
+ verbose: Whether to echo command to log file
1015
+
1016
+ Returns:
1017
+ Dictionary describing the AFLOW prototype designation
1018
+ (label and parameters) of the input structure.
1019
+ """
1020
+ if prim:
1021
+ command = [
1022
+ " --prim < " + self.aflow_work_dir + input_file,
1023
+ " --prototype --print=json",
1024
+ ]
1025
+ assert not force_wyckoff, "Must specify prim=False with force_wyckoff"
1026
+ else:
1027
+ command = [
1028
+ " --prototype --print=json < " + self.aflow_work_dir + input_file
1029
+ ]
1030
+
1031
+ if force_wyckoff:
1032
+ command[-1] += " --force_Wyckoff"
1033
+
1034
+ output = self.aflow_command(command, verbose=verbose)
1035
+ res_json = json.loads(output)
1036
+ return res_json
1037
+
1038
+ def get_prototype_designation_from_atoms(
1039
+ self, atoms: Atoms, prim: bool = True, verbose: bool = False
1040
+ ) -> Dict:
1041
+ """
1042
+ Run the ``aflow --prototype`` command to get the AFLOW prototype designation
1043
+
1044
+ Args:
1045
+ atoms: atoms object to analyze
1046
+ prim: whether to primitivize the structure first
1047
+ verbose: Whether to echo command to log file
1048
+
1049
+ Returns:
1050
+ Dictionary describing the AFLOW prototype designation
1051
+ (label and parameters) of the input structure.
1052
+ """
1053
+ return write_tmp_poscar_from_atoms_and_run_function(
1054
+ atoms, self.get_prototype_designation_from_file, prim=prim, verbose=verbose
1055
+ )
1056
+
1057
+ def get_library_prototype_label_and_shortname_from_file(
1058
+ self, poscar_file: str, prim: bool = True, shortnames: Dict = read_shortnames()
1059
+ ) -> Tuple[Union[str, None], Union[str, None]]:
1060
+ """
1061
+ Use the aflow command line tool to determine the library prototype label for a
1062
+ structure and look up its human-readable shortname. In the case of multiple
1063
+ results, the enumeration with the smallest misfit that is in the prototypes
1064
+ list is returned. If none of the results are in the matching prototypes list,
1065
+ then the prototype with the smallest misfit is returned.
1066
+
1067
+ Args:
1068
+ poscar_file:
1069
+ Path to input coordinate file
1070
+ prim: whether to primitivize the structure first
1071
+ shortnames:
1072
+ Dictionary with library prototype labels as keys and human-readable
1073
+ shortnames as values.
1074
+
1075
+ Returns:
1076
+ * The library prototype label for the provided compound.
1077
+ * Shortname corresponding to this prototype
1078
+ """
1079
+
1080
+ comparison_results = self.compare_to_prototypes(poscar_file, prim=prim)
1081
+ if len(comparison_results) > 1:
1082
+ # If zero results are returned it means the prototype is not in the
1083
+ # encyclopedia at all.
1084
+ # Not expecting a case where the number of results is greater than 1.
1085
+ raise RuntimeError(
1086
+ f"{comparison_results} results returned from comparison instead of "
1087
+ "zero or one as expected"
1088
+ )
1089
+ elif len(comparison_results) == 0:
1090
+ return None, None
1091
+
1092
+ # Try to find the result with the smallest misfit that is in the matching
1093
+ # prototype list, otherwise return result with smallest misfit
1094
+ misfit_min_overall = 1e60
1095
+ found_overall = False
1096
+ misfit_min_inlist = 1e60
1097
+ found_inlist = False
1098
+
1099
+ shortname = None
1100
+ for struct in comparison_results[0]["structures_duplicate"]:
1101
+ if struct["misfit"] < misfit_min_overall:
1102
+ misfit_min_overall = struct["misfit"]
1103
+ library_proto_overall = struct["name"]
1104
+ found_overall = True
1105
+ if struct["misfit"] < misfit_min_inlist and any(
1106
+ proto in struct["name"] for proto in shortnames
1107
+ ):
1108
+ misfit_min_inlist = struct["misfit"]
1109
+ library_proto_inlist = struct["name"]
1110
+ found_inlist = True
1111
+ if found_inlist:
1112
+ matching_library_prototype_label = library_proto_inlist
1113
+ shortname = shortnames[matching_library_prototype_label]
1114
+ elif found_overall:
1115
+ matching_library_prototype_label = library_proto_overall
1116
+ else:
1117
+ matching_library_prototype_label = None
1118
+
1119
+ return matching_library_prototype_label, shortname
1120
+
1121
+ def get_library_prototype_label_and_shortname_from_atoms(
1122
+ self, atoms: Atoms, prim: bool = True, shortnames: Dict = read_shortnames()
1123
+ ) -> Tuple[Union[str, None], Union[str, None]]:
1124
+ """
1125
+ Use the aflow command line tool to determine the library prototype label for a
1126
+ structure and look up its human-readable shortname. In the case of multiple
1127
+ results, the enumeration with the smallest misfit that is in the prototypes
1128
+ list is returned. If none of the results are in the matching prototypes list,
1129
+ then the prototype with the smallest misfit is returned.
1130
+
1131
+ Args:
1132
+ atoms:
1133
+ Atoms object to compare
1134
+ prim: whether to primitivize the structure first
1135
+ shortnames:
1136
+ Dictionary with library prototype labels as keys and human-readable
1137
+ shortnames as values.
1138
+
1139
+ Returns:
1140
+ * The library prototype label for the provided compound.
1141
+ * Shortname corresponding to this prototype
1142
+ """
1143
+ return write_tmp_poscar_from_atoms_and_run_function(
1144
+ atoms,
1145
+ self.get_library_prototype_label_and_shortname_from_file,
1146
+ prim=prim,
1147
+ shortnames=shortnames,
1148
+ )
1149
+
1150
+ def _compare_poscars(self, poscar1: PathLike, poscar2: PathLike) -> Dict:
1151
+ return json.loads(
1152
+ self.aflow_command(
1153
+ [
1154
+ f" --print=JSON --compare_materials={poscar1},{poscar2} "
1155
+ "--screen_only --no_scale_volume --optimize_match --quiet"
1156
+ ],
1157
+ verbose=False,
1158
+ )
1159
+ )
1160
+
1161
+ def _compare_Atoms(
1162
+ self,
1163
+ atoms1: Atoms,
1164
+ atoms2: Atoms,
1165
+ sort_atoms1: bool = True,
1166
+ sort_atoms2: bool = True,
1167
+ ) -> Dict:
1168
+ with NamedTemporaryFile() as f1, NamedTemporaryFile() as f2:
1169
+ atoms1.write(f1.name, "vasp", sort=sort_atoms1)
1170
+ atoms2.write(f2.name, "vasp", sort=sort_atoms2)
1171
+ f1.seek(0)
1172
+ f2.seek(0)
1173
+ compare = self._compare_poscars(f1.name, f2.name)
1174
+ return compare
1175
+
1176
+ def get_basistransformation_rotation_originshift_atom_map_from_atoms(
1177
+ self,
1178
+ atoms1: Atoms,
1179
+ atoms2: Atoms,
1180
+ sort_atoms1: bool = True,
1181
+ sort_atoms2: bool = True,
1182
+ ) -> Tuple[
1183
+ Optional[ArrayLike],
1184
+ Optional[ArrayLike],
1185
+ Optional[ArrayLike],
1186
+ Optional[List[int]],
1187
+ ]:
1188
+ """
1189
+ Get operations to transform atoms2 to atoms1
1190
+
1191
+ Args:
1192
+ sort_atoms1:
1193
+ Whether to sort atoms1 before comparing. If species are not
1194
+ alphabetized, this is REQUIRED. However, the `atom_map` returned will
1195
+ be w.r.t the sorted order, use with care!
1196
+ sort_atoms2: Whether to sort atoms2 before comparing.
1197
+
1198
+ Returns:
1199
+ Tuple of arrays in the order:
1200
+ basis transformation, rotation, origin shift, atom_map
1201
+ atom_map[index_in_structure_1] = index_in_structure_2
1202
+
1203
+ Raises:
1204
+ AFLOW.FailedToMatchException: if AFLOW fails to match the crystals
1205
+ """
1206
+ comparison_result = self._compare_Atoms(
1207
+ atoms1, atoms2, sort_atoms1, sort_atoms2
1208
+ )
1209
+ if (
1210
+ "structures_duplicate" in comparison_result[0]
1211
+ and comparison_result[0]["structures_duplicate"] != []
1212
+ ):
1213
+ return (
1214
+ np.asarray(
1215
+ comparison_result[0]["structures_duplicate"][0][
1216
+ "basis_transformation"
1217
+ ]
1218
+ ),
1219
+ np.asarray(comparison_result[0]["structures_duplicate"][0]["rotation"]),
1220
+ np.asarray(
1221
+ comparison_result[0]["structures_duplicate"][0]["origin_shift"]
1222
+ ),
1223
+ comparison_result[0]["structures_duplicate"][0]["atom_map"],
1224
+ )
1225
+ else:
1226
+ msg = "AFLOW was unable to match the provided crystals"
1227
+ logger.info(msg)
1228
+ raise self.FailedToMatchException(msg)
1229
+
1230
+ def get_param_names_from_prototype(self, prototype_label: str) -> List[str]:
1231
+ """
1232
+ Get the parameter names by parsing the error message from AFLOW
1233
+ """
1234
+ try:
1235
+ self.write_poscar_from_prototype(prototype_label, parameter_values=[1.0])
1236
+ # if no exception, it's a cubic crystal with no internal free params
1237
+ return ["a"]
1238
+ except subprocess.CalledProcessError as exc:
1239
+ re_match = re.search(r"parameter_list=(.*) - \[dir", str(exc.stderr))
1240
+ if re_match is not None:
1241
+ return re_match.groups()[0].split(",")
1242
+ # if we got here, the exception didn't have the format we expected, re-raise
1243
+ raise exc
1244
+
1245
+ def get_equation_sets_from_prototype(
1246
+ self, prototype_label: str
1247
+ ) -> List[EquivalentEqnSet]:
1248
+ """
1249
+ Get the symbolic equations for the fractional positions in the unit cell of an
1250
+ AFLOW prototype
1251
+
1252
+ Args:
1253
+ prototype_label:
1254
+ An AFLOW prototype label, without an enumeration suffix, without
1255
+ specified atomic species
1256
+
1257
+ Returns:
1258
+ List of EquivalentEqnSet objects
1259
+
1260
+ Each EquivalentEqnSet contains:
1261
+ - species: The species of the atoms in this set.
1262
+ - wyckoff_letter: The Wyckoff letter corresponding to this set.
1263
+ - param_names: The names of the free parameters associated with this
1264
+ Wyckoff position.
1265
+ - coeff_matrix_list: A list of 3 x n matrices of coefficients for the
1266
+ free parameters.
1267
+ - const_terms_list: A list of 3 x 1 columns of constant terms in the
1268
+ coordinates.
1269
+ """
1270
+ param_names = self.get_param_names_from_prototype(prototype_label)
1271
+ MAX_ATTEMPTS = 20
1272
+ ANGLE_NAMES = ["alpha", "beta", "gamma"]
1273
+ for i in range(MAX_ATTEMPTS):
1274
+ param_values = []
1275
+ triclinic = False
1276
+ if all([angle_name in param_names for angle_name in ANGLE_NAMES]):
1277
+ triclinic = True
1278
+ beta = 5 + random() * 165
1279
+ gamma = 5 + random() * 165
1280
+ beta_rad = radians(beta)
1281
+ gamma_rad = radians(gamma)
1282
+ max_cosalpha = abs(sin(beta_rad) * sin(gamma_rad)) + cos(
1283
+ beta_rad
1284
+ ) * cos(gamma_rad)
1285
+ min_cosalpha = -abs(sin(beta_rad) * sin(gamma_rad)) + cos(
1286
+ beta_rad
1287
+ ) * cos(gamma_rad)
1288
+ cosalpha = min_cosalpha + random() * (max_cosalpha - min_cosalpha)
1289
+ alpha = degrees(acos(cosalpha))
1290
+ angles_dict = {"alpha": alpha, "beta": beta, "gamma": gamma}
1291
+
1292
+ for pname in param_names:
1293
+ if pname in ANGLE_NAMES:
1294
+ if triclinic:
1295
+ param_values.append(angles_dict[pname])
1296
+ else:
1297
+ param_values.append(180.0 * random())
1298
+ else:
1299
+ param_values.append(random())
1300
+
1301
+ try:
1302
+ equation_poscar = self.write_poscar_from_prototype(
1303
+ prototype_label,
1304
+ parameter_values=param_values,
1305
+ addtl_args="--equations_only",
1306
+ )
1307
+ break
1308
+ except subprocess.CalledProcessError:
1309
+ if i == MAX_ATTEMPTS - 1:
1310
+ raise RuntimeError(
1311
+ "Random parameters failed to pass "
1312
+ "AFLOW checks 20 times in a row"
1313
+ )
1314
+ else:
1315
+ pass
1316
+
1317
+ # get a string with one character per Wyckoff position
1318
+ # (with possible repeated letters for positions with free params)
1319
+ wyckoff_lists = get_wyckoff_lists_from_prototype(prototype_label)
1320
+ wyckoff_joined_list = "".join(wyckoff_lists)
1321
+
1322
+ coord_lines = equation_poscar.splitlines()[7:]
1323
+ coord_iter = iter(coord_lines)
1324
+
1325
+ space_group_number = get_space_group_number_from_prototype(prototype_label)
1326
+
1327
+ equation_sets = []
1328
+
1329
+ for wyckoff_letter in wyckoff_joined_list:
1330
+ species = None # have not seen a line yet, so don't know what species it is
1331
+ param_names = None # same as above.
1332
+ coeff_matrix_list = []
1333
+ const_terms_list = []
1334
+ # the next n positions should be equivalent
1335
+ # corresponding to this Wyckoff position
1336
+ for _ in range(
1337
+ get_primitive_wyckoff_multiplicity(space_group_number, wyckoff_letter)
1338
+ ):
1339
+ line_split = next(coord_iter).split()
1340
+ if species is None:
1341
+ species = line_split[3]
1342
+ elif line_split[3] != species:
1343
+ raise InconsistentWyckoffException(
1344
+ "Encountered different species within what I thought should be "
1345
+ f"the lines corresponding to Wyckoff position {wyckoff_letter}"
1346
+ f"\nEquations obtained from prototype label {prototype_label}:"
1347
+ f"\n{equation_poscar}"
1348
+ )
1349
+ # first, get the free parameters of this line
1350
+ curr_line_free_params = set() # sympy.Symbol
1351
+ coordinate_expr_list = []
1352
+ for expression_string in line_split[:3]:
1353
+ coordinate_expr = parse_expr(expression_string)
1354
+ curr_line_free_params.update(coordinate_expr.free_symbols)
1355
+ coordinate_expr_list.append(coordinate_expr)
1356
+
1357
+ # They should all have the same number, i.e. x2, y2, z2 or x14, z14,
1358
+ # so we can just string sort them
1359
+ curr_line_free_params = list(curr_line_free_params)
1360
+ curr_line_free_params.sort(key=lambda param: str(param))
1361
+
1362
+ # Each line within a Wyckoff position should
1363
+ # have the same set of free parameters
1364
+ if param_names is None:
1365
+ param_names = curr_line_free_params
1366
+ elif param_names != curr_line_free_params:
1367
+ raise InconsistentWyckoffException(
1368
+ "Encountered different free params within what I thought "
1369
+ "should be the lines corresponding to Wyckoff position "
1370
+ f"{wyckoff_letter}\nEquations obtained from prototype label "
1371
+ f"{prototype_label}:\n{equation_poscar}"
1372
+ )
1373
+
1374
+ # Transform to matrices and vectors
1375
+ a, b = linear_eq_to_matrix(coordinate_expr_list, param_names)
1376
+
1377
+ assert a.shape == (3, len(param_names))
1378
+ assert b.shape == (3, 1)
1379
+
1380
+ coeff_matrix_list.append(matrix2numpy(a, dtype=np.float64))
1381
+ const_terms_list.append(matrix2numpy(-b, dtype=np.float64))
1382
+
1383
+ # Done looping over this set of equivalent positions
1384
+ equation_sets.append(
1385
+ EquivalentEqnSet(
1386
+ species=species,
1387
+ wyckoff_letter=wyckoff_letter,
1388
+ param_names=[str(param_name) for param_name in param_names],
1389
+ coeff_matrix_list=coeff_matrix_list,
1390
+ const_terms_list=const_terms_list,
1391
+ )
1392
+ )
1393
+
1394
+ # do some checks
1395
+ equation_sets_iter = iter(equation_sets)
1396
+ species = None
1397
+ for species_wyckoff_list in wyckoff_lists:
1398
+ species_must_change = True
1399
+ for _ in species_wyckoff_list:
1400
+ equation_set = next(equation_sets_iter)
1401
+ if species_must_change:
1402
+ if equation_set.species == species:
1403
+ raise InconsistentWyckoffException(
1404
+ "The species in the equations obtained below are "
1405
+ "inconsistent with the number and multiplicity of Wyckoff "
1406
+ f"positions in prototype label {prototype_label}\n"
1407
+ f"{equation_poscar}"
1408
+ )
1409
+ species = equation_set.species
1410
+ species_must_change = False
1411
+
1412
+ return equation_sets
1413
+
1414
+ def solve_for_params_of_known_prototype(
1415
+ self, atoms: Atoms, prototype_label: str, max_resid: float = 1e-5
1416
+ ) -> List[float]:
1417
+ """
1418
+ Given an Atoms object that is a primitive cell of its Bravais lattice as
1419
+ defined in doi.org/10.1016/j.commatsci.2017.01.017, and its presumed prototype
1420
+ label, solves for the free parameters of the prototype label. Raises an error
1421
+ if the solution fails (likely indicating that the Atoms object provided does
1422
+ not conform to the provided prototype label.) The Atoms object may be rotated,
1423
+ translated, and permuted, but the identity of the lattice vectors must be
1424
+ unchanged w.r.t. the crystallographic prototype. In other words, there must
1425
+ exist a permutation and translation of the fractional coordinates that enables
1426
+ them to match the equations defined by the prototype label.
1427
+
1428
+ Args:
1429
+ atoms: The Atoms object to analyze
1430
+ prototype_label:
1431
+ The assumed AFLOW prototype label. `atoms` must be a primitive cell
1432
+ (with lattice vectors defined in
1433
+ doi.org/10.1016/j.commatsci.2017.01.017) conforming to the symmetry
1434
+ defined by this prototype label. Rigid body rotations, translations,
1435
+ and permutations of `atoms` relative to the AFLOW setting are allowed,
1436
+ but the identity of the lattice vectors must be unchanged.
1437
+ max_resid:
1438
+ Maximum residual allowed when attempting to match the fractional
1439
+ positions of the atoms to the crystallographic equations
1440
+
1441
+ Returns:
1442
+ List of free parameters that will regenerate `atoms` (up to permutations,
1443
+ rotations, and translations) when paired with `prototype_label`
1444
+
1445
+ Raises:
1446
+ AFLOW.ChangedSymmetryException:
1447
+ if the symmetry of the atoms object is different from `prototype_label`
1448
+ AFLOW.FailedToMatchException:
1449
+ if AFLOW fails to match the re-generated crystal to the input crystal
1450
+
1451
+ """
1452
+ # solve for cell parameters
1453
+ cell_params = solve_for_aflow_cell_params_from_primitive_ase_cell_params(
1454
+ atoms.cell.cellpar(), prototype_label
1455
+ )
1456
+ species = sorted(list(set(atoms.get_chemical_symbols())))
1457
+
1458
+ # First, redetect the prototype label. We can't use this as-is because it may
1459
+ # be rotated by an operation that's within the normalizer but not
1460
+ # in the space group itself
1461
+ detected_prototype_designation = self.get_prototype_designation_from_atoms(
1462
+ atoms
1463
+ )
1464
+
1465
+ prototype_label_detected = detected_prototype_designation[
1466
+ "aflow_prototype_label"
1467
+ ]
1468
+
1469
+ if not prototype_labels_are_equivalent(
1470
+ prototype_label, prototype_label_detected
1471
+ ):
1472
+ msg = (
1473
+ f"Redetected prototype label {prototype_label_detected} does not match "
1474
+ f"nominal {prototype_label}."
1475
+ )
1476
+ logger.info(msg)
1477
+ raise self.ChangedSymmetryException(msg)
1478
+
1479
+ # rebuild the atoms
1480
+ try:
1481
+ atoms_rebuilt = self.build_atoms_from_prototype(
1482
+ prototype_label=prototype_label_detected,
1483
+ species=species,
1484
+ parameter_values=detected_prototype_designation[
1485
+ "aflow_prototype_params_values"
1486
+ ],
1487
+ )
1488
+ except self.ChangedSymmetryException as e:
1489
+ # re-raise, just indicating that this function knows about this exception
1490
+ raise e
1491
+
1492
+ # We want the negative of the origin shift from ``atoms_rebuilt`` to ``atoms``,
1493
+ # because the origin shift is the last operation to happen, so it will be in
1494
+ # the ``atoms`` frame.
1495
+
1496
+ # This function gets the transformation from its second argument to its first.
1497
+ # The origin shift is Cartesian if the POSCARs are Cartesian, which they are
1498
+ # when made from Atoms
1499
+
1500
+ # Sort atoms, but do not sort atoms_rebuilt
1501
+ try:
1502
+ _, _, neg_initial_origin_shift_cart, atom_map = (
1503
+ self.get_basistransformation_rotation_originshift_atom_map_from_atoms(
1504
+ atoms, atoms_rebuilt, sort_atoms1=True, sort_atoms2=False
1505
+ )
1506
+ )
1507
+ except self.FailedToMatchException:
1508
+ # Re-raise with a more informative error message
1509
+ msg = (
1510
+ "Execution cannot continue because AFLOW failed to match the crystal "
1511
+ "with its representation re-generated from the detected prototype "
1512
+ "designation.\nRarely, this can happen if the structure is on the edge "
1513
+ "of a symmetry increase (e.g. a BCT structure with c/a very close to 1)"
1514
+ )
1515
+ logger.error(msg)
1516
+ raise self.FailedToMatchException(msg)
1517
+
1518
+ # Transpose the change of basis equation for row vectors
1519
+ initial_origin_shift_frac = (-neg_initial_origin_shift_cart) @ np.linalg.inv(
1520
+ atoms.cell
1521
+ )
1522
+
1523
+ logger.info(
1524
+ "Initial shift (to SOME standard origin, not necessarily the desired one): "
1525
+ f"{-neg_initial_origin_shift_cart} (Cartesian), "
1526
+ f"{initial_origin_shift_frac} (fractional)"
1527
+ )
1528
+
1529
+ position_set_list = get_equivalent_atom_sets_from_prototype_and_atom_map(
1530
+ atoms, prototype_label, atom_map, sort_atoms=True
1531
+ )
1532
+
1533
+ # get equation sets
1534
+ equation_set_list = self.get_equation_sets_from_prototype(prototype_label)
1535
+
1536
+ if len(position_set_list) != len(equation_set_list):
1537
+ raise InconsistentWyckoffException(
1538
+ "Number of equivalent positions detected in Atoms object "
1539
+ "did not match the number of equivalent equations given"
1540
+ )
1541
+
1542
+ space_group_number = get_space_group_number_from_prototype(prototype_label)
1543
+ for prim_shift in get_possible_primitive_shifts(space_group_number):
1544
+ logger.info(
1545
+ "Additionally shifting atoms by "
1546
+ f"internal fractional translation {prim_shift}"
1547
+ )
1548
+
1549
+ free_params_dict = {}
1550
+ position_set_matched_list = [False] * len(position_set_list)
1551
+
1552
+ for equation_set in equation_set_list:
1553
+ # Because both equations and positions are sorted by species and
1554
+ # wyckoff letter, this should be pretty efficient
1555
+ matched_this_equation_set = False
1556
+ for i, position_set in enumerate(position_set_list):
1557
+ if position_set_matched_list[i]:
1558
+ continue
1559
+ if (
1560
+ position_set.species != equation_set.species
1561
+ ): # These are virtual species
1562
+ continue
1563
+ if not are_in_same_wyckoff_set(
1564
+ equation_set.wyckoff_letter,
1565
+ position_set.wyckoff_letter,
1566
+ space_group_number,
1567
+ ):
1568
+ continue
1569
+ for coeff_matrix, const_terms in zip(
1570
+ equation_set.coeff_matrix_list, equation_set.const_terms_list
1571
+ ):
1572
+ for frac_position in position_set.frac_position_list:
1573
+ # Here we use column coordinate vectors
1574
+ frac_position_shifted = (
1575
+ frac_position
1576
+ + np.asarray(prim_shift).reshape(3, 1)
1577
+ + initial_origin_shift_frac.reshape(3, 1)
1578
+ ) % 1
1579
+ possible_shifts = (-1, 0, 1)
1580
+ # explore all possible shifts around zero
1581
+ # to bring back in cell.
1582
+ # TODO: if this is too slow (27 possibilities), write an
1583
+ # algorithm to determine which shifts are possible
1584
+ for shift_list in [
1585
+ (x, y, z)
1586
+ for x in possible_shifts
1587
+ for y in possible_shifts
1588
+ for z in possible_shifts
1589
+ ]:
1590
+ shift_array = np.asarray(shift_list).reshape(3, 1)
1591
+ candidate_internal_param_values, resid, _, _ = (
1592
+ np.linalg.lstsq(
1593
+ coeff_matrix,
1594
+ frac_position_shifted
1595
+ - const_terms
1596
+ - shift_array,
1597
+ )
1598
+ )
1599
+ if len(resid) == 0 or np.max(resid) < max_resid:
1600
+ assert len(candidate_internal_param_values) == len(
1601
+ equation_set.param_names
1602
+ )
1603
+ for param_name, param_value in zip(
1604
+ equation_set.param_names,
1605
+ candidate_internal_param_values,
1606
+ ):
1607
+ assert param_name not in free_params_dict
1608
+ free_params_dict[param_name] = (
1609
+ param_value[0] % 1
1610
+ ) # wrap to [0,1)
1611
+ # should only need one to match to check off this
1612
+ # Wyckoff position
1613
+ position_set_matched_list[i] = True
1614
+ matched_this_equation_set = True
1615
+ break
1616
+ # end loop over shifts
1617
+ if matched_this_equation_set:
1618
+ break
1619
+ # end loop over positions within a position set
1620
+ if matched_this_equation_set:
1621
+ break
1622
+ # end loop over equations within an equation set
1623
+ if matched_this_equation_set:
1624
+ break
1625
+ # end loop over position sets
1626
+ # end loop over equation sets
1627
+
1628
+ if all(position_set_matched_list):
1629
+ candidate_prototype_param_values = cell_params + [
1630
+ free_params_dict[key]
1631
+ for key in sorted(
1632
+ free_params_dict.keys(), key=internal_parameter_sort_key
1633
+ )
1634
+ ]
1635
+ # The internal shift may have taken us to an internal parameter
1636
+ # solution that represents a rotation, so we need to check
1637
+ if self.confirm_unrotated_prototype_designation(
1638
+ atoms, species, prototype_label, candidate_prototype_param_values
1639
+ ):
1640
+ logger.info(
1641
+ f"Found set of parameters for prototype {prototype_label} "
1642
+ "that is unrotated"
1643
+ )
1644
+ return candidate_prototype_param_values
1645
+ else:
1646
+ logger.info(
1647
+ f"Found set of parameters for prototype {prototype_label}, "
1648
+ "but it was rotated relative to the original cell"
1649
+ )
1650
+ else:
1651
+ logger.info(
1652
+ f"Failed to solve equations for prototype {prototype_label} "
1653
+ "on this shift attempt"
1654
+ )
1655
+
1656
+ msg = (
1657
+ f"Failed to solve equations for prototype {prototype_label} "
1658
+ "on any shift attempt"
1659
+ )
1660
+ logger.info(msg)
1661
+ raise self.FailedToSolveException(msg)
1662
+
1663
+ def confirm_atoms_unrotated_when_cells_aligned(
1664
+ self,
1665
+ test_atoms: Atoms,
1666
+ ref_atoms: Atoms,
1667
+ sgnum: Union[int, str],
1668
+ rtol: float = 1.0e-4,
1669
+ atol: float = 1.0e-8,
1670
+ ) -> bool:
1671
+ """
1672
+ Check whether `test_atoms` and `reference_atoms` are unrotated as follows:
1673
+ When the cells are in :meth:`ase.cell.Cell.standard_form`, the cells are
1674
+ identical. When both crystals are rotated to standard form (rotating the cell
1675
+ and keeping the fractional coordinates unchanged), the rotation part of the
1676
+ mapping the two crystals to each other found by AFLOW is in the point group of
1677
+ the reference crystal (using the generated crystal would give the same result).
1678
+ In other words, the crystals are identically oriented (but possibly translated)
1679
+ in reference to their lattice vectors, which in turn must be identical up to a
1680
+ rotation in reference to some Cartesian coordinate system.
1681
+ The crystals must be primitive cells as defined in
1682
+ https://doi.org/10.1016/j.commatsci.2017.01.017.
1683
+
1684
+ Args:
1685
+ test_atoms:
1686
+ Primitive cell of a crystal
1687
+ ref_atoms:
1688
+ Primitive cell of a crystal
1689
+ sgnum:
1690
+ Space group number
1691
+ rtol:
1692
+ Parameter to pass to :func:`numpy.allclose` for comparing cell params
1693
+ atol:
1694
+ Parameter to pass to :func:`numpy.allclose` for comparing cell params
1695
+ """
1696
+ if not np.allclose(
1697
+ ref_atoms.cell.cellpar(), test_atoms.cell.cellpar(), atol=atol, rtol=rtol
1698
+ ):
1699
+ logger.info(
1700
+ "Cell lengths and angles do not match.\n"
1701
+ f"Original: {ref_atoms.cell.cellpar()}\n"
1702
+ f"Regenerated: {test_atoms.cell.cellpar()}"
1703
+ )
1704
+ return False
1705
+ else:
1706
+ cell_lengths_and_angles = ref_atoms.cell.cellpar()
1707
+
1708
+ test_atoms_copy = test_atoms.copy()
1709
+ ref_atoms_copy = ref_atoms.copy()
1710
+
1711
+ test_atoms_copy.set_cell(
1712
+ Cell.fromcellpar(cell_lengths_and_angles), scale_atoms=True
1713
+ )
1714
+ ref_atoms_copy.set_cell(
1715
+ Cell.fromcellpar(cell_lengths_and_angles), scale_atoms=True
1716
+ )
1717
+
1718
+ # the rotation below is Cartesian.
1719
+ try:
1720
+ _, cart_rot, _, _ = (
1721
+ self.get_basistransformation_rotation_originshift_atom_map_from_atoms(
1722
+ test_atoms_copy, ref_atoms_copy
1723
+ )
1724
+ )
1725
+ except self.FailedToMatchException:
1726
+ logger.info("AFLOW failed to match the recreated crystal to reference")
1727
+ return False
1728
+
1729
+ return cartesian_rotation_is_in_point_group(
1730
+ cart_rot, sgnum, test_atoms_copy.cell
1731
+ )
1732
+
1733
+ def confirm_unrotated_prototype_designation(
1734
+ self,
1735
+ reference_atoms: Atoms,
1736
+ species: List[str],
1737
+ prototype_label: str,
1738
+ parameter_values: List[float],
1739
+ rtol: float = 1.0e-4,
1740
+ atol: float = 1.0e-8,
1741
+ ) -> bool:
1742
+ """
1743
+ Check whether the provided prototype designation recreates ``reference_atoms``
1744
+ as follows: When the cells are in :meth:`ase.cell.Cell.standard_form`, the
1745
+ cells are identical. When both crystals are rotated to standard form (rotating
1746
+ the cell and keeping the fractional coordinates unchanged), the rotation part
1747
+ of the mapping the two crystals to each other found by AFLOW is in the point
1748
+ group of the reference crystal (using the generated crystal would give the same
1749
+ result). In other words, the crystals are identically oriented (but possibly
1750
+ translated) in reference to their lattice vectors, which in turn must be
1751
+ identical up to a rotation in reference to some Cartesian coordinate system.
1752
+
1753
+ Args:
1754
+ species:
1755
+ Stoichiometric species, e.g. ["Mo", "S"] corresponding to A and B
1756
+ respectively for prototype label AB2_hP6_194_c_f indicating molybdenite
1757
+ prototype_label:
1758
+ An AFLOW prototype label, without an enumeration suffix,
1759
+ without specified atomic species
1760
+ parameter_values:
1761
+ The free parameters of the AFLOW prototype designation
1762
+ rtol:
1763
+ Parameter to pass to :func:`numpy.allclose` for comparing cell params
1764
+ atol:
1765
+ Parameter to pass to :func:`numpy.allclose` for comparing cell params
1766
+
1767
+ Returns:
1768
+ Whether or not the crystals match
1769
+ """
1770
+ test_atoms = self.build_atoms_from_prototype(
1771
+ prototype_label=prototype_label,
1772
+ species=species,
1773
+ parameter_values=parameter_values,
1774
+ )
1775
+
1776
+ return self.confirm_atoms_unrotated_when_cells_aligned(
1777
+ test_atoms,
1778
+ reference_atoms,
1779
+ get_space_group_number_from_prototype(prototype_label),
1780
+ rtol,
1781
+ atol,
1782
+ )