kim-tools 0.3.6__py3-none-any.whl → 0.4.2__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.
kim_tools/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.6"
1
+ __version__ = "0.4.2"
2
2
 
3
3
  from .aflow_util import *
4
4
  from .aflow_util import __all__ as aflow_all
@@ -17,7 +17,6 @@ import numpy as np
17
17
  import numpy.typing as npt
18
18
  from ase import Atoms
19
19
  from ase.cell import Cell
20
- from ase.neighborlist import natural_cutoffs, neighbor_list
21
20
  from semver import Version
22
21
  from sympy import Symbol, linear_eq_to_matrix, matrix2numpy, parse_expr
23
22
 
@@ -30,6 +29,7 @@ from ..symmetry_util import (
30
29
  cartesian_rotation_is_in_point_group,
31
30
  get_possible_primitive_shifts,
32
31
  get_primitive_wyckoff_multiplicity,
32
+ get_smallest_nn_dist,
33
33
  get_wyck_pos_xform_under_normalizer,
34
34
  space_group_numbers_are_enantiomorphic,
35
35
  )
@@ -398,9 +398,7 @@ def get_atom_indices_for_each_wyckoff_orb(prototype_label: str) -> List[Dict]:
398
398
  orbit.
399
399
 
400
400
  Returns:
401
- The information is in this format:
402
-
403
- [{"letter":"a", "indices":[0,1]}, ... ]
401
+ The information is in this format -- ``[{"letter":"a", "indices":[0,1]}, ... ]``
404
402
  """
405
403
  return_list = []
406
404
  wyck_lists = get_wyckoff_lists_from_prototype(prototype_label)
@@ -416,6 +414,45 @@ def get_atom_indices_for_each_wyckoff_orb(prototype_label: str) -> List[Dict]:
416
414
  return return_list
417
415
 
418
416
 
417
+ def get_all_equivalent_labels(prototype_label: str) -> List[str]:
418
+ """
419
+ Return all possible permutations of the Wyckoff letters in a prototype
420
+ label under the operations of the affine normalizer.
421
+
422
+ NOTE: For now this function will not completely enumerate the possibilities
423
+ for triclinic and monoclinic space groups
424
+ """
425
+ sgnum = get_space_group_number_from_prototype(prototype_label)
426
+ prototype_label_split = prototype_label.split("_")
427
+ equivalent_labels = []
428
+ for wyck_pos_xform in get_wyck_pos_xform_under_normalizer(sgnum):
429
+ prototype_label_split_permuted = prototype_label_split[:3]
430
+ for wycksec in prototype_label_split[3:]:
431
+ # list of letters joined with their nums, e.g. ["a", "2i"]
432
+ wycksec_permuted_list = []
433
+ prev_lett_ind = -1
434
+ for i, num_or_lett in enumerate(wycksec):
435
+ if isalpha(num_or_lett):
436
+ if num_or_lett == "A":
437
+ # Wyckoff position A comes after z in sg 47
438
+ lett_index = ord("z") + 1 - ord("a")
439
+ else:
440
+ lett_index = ord(num_or_lett) - ord("a")
441
+ # The start position of the (optional) numbers +
442
+ # letter describing this position
443
+ this_pos_start_ind = prev_lett_ind + 1
444
+ permuted_lett_and_num = wycksec[this_pos_start_ind:i]
445
+ permuted_lett_and_num += wyck_pos_xform[lett_index]
446
+ wycksec_permuted_list.append(permuted_lett_and_num)
447
+ prev_lett_ind = i
448
+ wycksec_permuted_list_sorted = sorted(
449
+ wycksec_permuted_list, key=lambda x: x[-1]
450
+ )
451
+ prototype_label_split_permuted.append("".join(wycksec_permuted_list_sorted))
452
+ equivalent_labels.append("_".join(prototype_label_split_permuted))
453
+ return list(set(equivalent_labels))
454
+
455
+
419
456
  def prototype_labels_are_equivalent(
420
457
  prototype_label_1: str,
421
458
  prototype_label_2: str,
@@ -1510,7 +1547,8 @@ class AFLOW:
1510
1547
  cell_rtol: float = 0.01,
1511
1548
  rot_rtol: float = 0.01,
1512
1549
  rot_atol: float = 0.01,
1513
- ) -> Tuple[List[float], Optional[str]]:
1550
+ match_library_proto: bool = True,
1551
+ ) -> Union[List[float], Tuple[List[float], Optional[str]]]:
1514
1552
  """
1515
1553
  Given an Atoms object that is a primitive cell of its Bravais lattice as
1516
1554
  defined in doi.org/10.1016/j.commatsci.2017.01.017, and its presumed prototype
@@ -1547,12 +1585,16 @@ class AFLOW:
1547
1585
  Parameter to pass to :func:`numpy.allclose` for compariong fractional
1548
1586
  rotations. Default value chosen to be commensurate with AFLOW
1549
1587
  default distance tolerance of 0.01*(NN distance)
1588
+ match_library_proto:
1589
+ Whether to attempt matching to library prototypes
1550
1590
 
1551
1591
  Returns:
1552
1592
  * List of free parameters that will regenerate `atoms` (up to permutations,
1553
1593
  rotations, and translations) when paired with `prototype_label`
1554
- * Library prototype label from the AFLOW prototype encyclopedia, if any
1555
- * Title of library prototype from the AFLOW prototype encyclopedia, if any
1594
+ * Additionally, if 'match_library_proto' is True (default):
1595
+ * Library prototype label from the AFLOW prototype encyclopedia, if any
1596
+ * Title of library prototype from the AFLOW prototype encyclopedia,
1597
+ if any
1556
1598
 
1557
1599
  Raises:
1558
1600
  AFLOW.ChangedSymmetryException:
@@ -1563,20 +1605,12 @@ class AFLOW:
1563
1605
  """
1564
1606
  # If max_resid not provided, determine it from neighborlist
1565
1607
  if max_resid is None:
1566
- nl_len = 0
1567
- cov_mult = 1
1568
- while nl_len == 0:
1569
- logger.info(
1570
- "Attempting to find NN distance by searching "
1571
- f"within covalent radii times {cov_mult}"
1572
- )
1573
- nl = neighbor_list("d", atoms, natural_cutoffs(atoms, mult=cov_mult))
1574
- nl_len = nl.size
1575
- cov_mult += 1
1576
1608
  # set the maximum error to 1% of NN distance to follow AFLOW convention
1577
1609
  # rescale by cube root of cell volume to get rough conversion from
1578
1610
  # cartesian to fractional
1579
- max_resid = nl.min() * 0.01 * atoms.get_volume() ** (-1 / 3)
1611
+ max_resid = (
1612
+ get_smallest_nn_dist(atoms) * 0.01 * atoms.get_volume() ** (-1 / 3)
1613
+ )
1580
1614
  logger.info(
1581
1615
  "Automatically set max fractional residual for solving position "
1582
1616
  f"equations to {max_resid}"
@@ -1599,10 +1633,25 @@ class AFLOW:
1599
1633
  "aflow_prototype_label"
1600
1634
  ]
1601
1635
 
1602
- library_prototype_label, short_name = (
1603
- self.get_library_prototype_label_and_shortname_from_atoms(atoms)
1604
- )
1636
+ if match_library_proto:
1637
+ try:
1638
+ library_prototype_label, short_name = (
1639
+ self.get_library_prototype_label_and_shortname_from_atoms(atoms)
1640
+ )
1641
+ except subprocess.CalledProcessError:
1642
+ library_prototype_label = None
1643
+ short_name = None
1644
+ msg = (
1645
+ "WARNING: aflow --compare2prototypes returned error, skipping "
1646
+ "library matching"
1647
+ )
1648
+ print()
1649
+ print(msg)
1650
+ print()
1651
+ logger.warning(msg)
1605
1652
 
1653
+ # NOTE: Because of below, this only works if the provided prototype label is
1654
+ # correctly alphabetized. Change this?
1606
1655
  if not prototype_labels_are_equivalent(
1607
1656
  prototype_label, prototype_label_detected
1608
1657
  ):
@@ -1665,7 +1714,7 @@ class AFLOW:
1665
1714
  )
1666
1715
 
1667
1716
  position_set_list = get_equivalent_atom_sets_from_prototype_and_atom_map(
1668
- atoms, prototype_label, atom_map, sort_atoms=True
1717
+ atoms, prototype_label_detected, atom_map, sort_atoms=True
1669
1718
  )
1670
1719
 
1671
1720
  # get equation sets
@@ -1785,11 +1834,14 @@ class AFLOW:
1785
1834
  f"Found set of parameters for prototype {prototype_label} "
1786
1835
  "that is unrotated"
1787
1836
  )
1788
- return (
1789
- candidate_prototype_param_values,
1790
- library_prototype_label,
1791
- short_name,
1792
- )
1837
+ if match_library_proto:
1838
+ return (
1839
+ candidate_prototype_param_values,
1840
+ library_prototype_label,
1841
+ short_name,
1842
+ )
1843
+ else:
1844
+ return candidate_prototype_param_values
1793
1845
  else:
1794
1846
  logger.info(
1795
1847
  f"Found set of parameters for prototype {prototype_label}, "
@@ -1863,7 +1915,9 @@ class AFLOW:
1863
1915
  cell_lengths_and_angles = ref_atoms.cell.cellpar()
1864
1916
 
1865
1917
  test_atoms_copy = test_atoms.copy()
1918
+ del test_atoms_copy.constraints
1866
1919
  ref_atoms_copy = ref_atoms.copy()
1920
+ del ref_atoms_copy.constraints
1867
1921
 
1868
1922
  test_atoms_copy.set_cell(
1869
1923
  Cell.fromcellpar(cell_lengths_and_angles), scale_atoms=True
kim_tools/ase/core.py CHANGED
@@ -34,7 +34,6 @@ Helper routines for KIM Tests and Verification Checks
34
34
  import itertools
35
35
  import logging
36
36
  import random
37
- from typing import Union
38
37
 
39
38
  import numpy as np
40
39
  from ase import Atoms
@@ -208,45 +207,137 @@ def randomize_positions(atoms, pert_amp, seed=None):
208
207
 
209
208
 
210
209
  ################################################################################
211
- def get_isolated_energy_per_atom(model: Union[str, Calculator], symbol: str) -> float:
210
+ def get_isolated_energy_per_atom(
211
+ model,
212
+ symbol,
213
+ initial_separation=1.0,
214
+ max_separation=15.0,
215
+ separation_neg_exponent=4,
216
+ quit_early_after_convergence=True,
217
+ energy_tolerance=1e-12,
218
+ ):
212
219
  """
213
220
  Construct a non-periodic cell containing a single atom and compute its energy.
221
+ It tries to iteratively finetune the atomic separation for a dimer up to a
222
+ specified precision (separation_neg_exponent). If between two successive phases
223
+ the energy difference is less than energy_tolerance, it stops early, i.e. if
224
+ 4.0x and 4.00x are within energy_tolerance, it stops at 4.00x.
225
+ All separations are in Angstroms.
214
226
 
215
227
  Args:
216
- model:
217
- A KIM model ID or an ASE calculator for computing the energy
218
- symbol:
219
- The chemical species
220
-
221
- Returns:
222
- The isolated energy of a single atom
228
+ model: KIM model to use for calculations
229
+ symbol: Chemical symbol
230
+ initial_separation: Initial separation for dimer calculations
231
+ max_separation: maximum separation for dimer calculations
232
+ separation_neg_exponent: Number of decimal places to refine the separation
233
+ quit_early_after_convergence: Whether to stop early if energy converges
234
+ energy_tolerance: Energy difference tolerance for convergence check
223
235
  """
224
- single_atom = Atoms(
225
- symbol,
226
- positions=[(0.1, 0.1, 0.1)],
227
- cell=(20, 20, 20),
228
- pbc=(False, False, False),
229
- )
230
- if isinstance(model, str):
231
- calc = KIM(model)
232
- elif isinstance(model, Calculator):
233
- calc = model
234
- else:
235
- raise KIMASEError(
236
- "`model` argument must be a string indicating a KIM model "
237
- f"or an ASE Calculator. Instead got an object of type {type(model)}."
236
+ try:
237
+ single_atom = Atoms(
238
+ symbol,
239
+ positions=[(0.1, 0.1, 0.1)],
240
+ cell=(20, 20, 20),
241
+ pbc=(False, False, False),
238
242
  )
239
- single_atom.calc = calc
240
- energy_per_atom = single_atom.get_potential_energy()
241
- # if we are attaching an existing LAMMPS calculator to an atoms object,
242
- # we can't delete it. Only do so if we are making a new one from a KIM ID.
243
- if isinstance(model, str):
243
+ if isinstance(model, str):
244
+ calc = KIM(model)
245
+ elif isinstance(model, Calculator):
246
+ calc = model
247
+
248
+ single_atom.calc = calc
249
+ energy_per_atom = single_atom.get_potential_energy()
250
+
251
+ # Clean up
244
252
  if hasattr(calc, "clean"):
245
253
  calc.clean()
246
254
  if hasattr(calc, "__del__"):
247
255
  calc.__del__()
248
- del single_atom
249
- return energy_per_atom
256
+ del single_atom
257
+
258
+ return energy_per_atom
259
+
260
+ except Exception:
261
+
262
+ def _try_dimer_energy(separation):
263
+ try:
264
+ dimer = Atoms(
265
+ [symbol, symbol],
266
+ positions=[(0.1, 0.1, 0.1), (0.1 + separation, 0.1, 0.1)],
267
+ cell=(max(20, separation + 10), 20, 20),
268
+ pbc=(False, False, False),
269
+ )
270
+ calc = KIM(model)
271
+ dimer.calc = calc
272
+
273
+ total_energy = dimer.get_potential_energy()
274
+ energy_per_atom = total_energy / 2.0
275
+
276
+ if hasattr(calc, "clean"):
277
+ calc.clean()
278
+ if hasattr(calc, "__del__"):
279
+ calc.__del__()
280
+ del dimer
281
+
282
+ return energy_per_atom
283
+
284
+ except Exception:
285
+ try:
286
+ if hasattr(calc, "clean"):
287
+ calc.clean()
288
+ if hasattr(calc, "__del__"):
289
+ calc.__del__()
290
+ if "dimer" in locals():
291
+ del dimer
292
+ except Exception:
293
+ pass
294
+ return None
295
+
296
+ # Start with integer separations: 1.0, 2.0, 3.0, 4.0, ...
297
+ last_successful_separation = None
298
+ last_successful_energy = None
299
+
300
+ separation = initial_separation
301
+ while separation <= max_separation:
302
+ energy = _try_dimer_energy(separation)
303
+ if energy is not None:
304
+ last_successful_separation = separation
305
+ last_successful_energy = energy
306
+ separation += 1.0
307
+ else:
308
+ break
309
+
310
+ if last_successful_separation is None:
311
+ raise RuntimeError(
312
+ f"Failed to obtain isolated energy for {symbol} - no separations worked"
313
+ )
314
+
315
+ # refine
316
+ current_separation = last_successful_separation
317
+ previous_phase_energy = last_successful_energy
318
+
319
+ for decimal_place in range(1, separation_neg_exponent + 1):
320
+
321
+ step_size = 10 ** (-decimal_place)
322
+
323
+ for i in range(1, 10):
324
+ test_sep = current_separation + step_size
325
+ energy = _try_dimer_energy(test_sep)
326
+
327
+ if energy is not None:
328
+ current_separation = test_sep
329
+ last_successful_energy = energy
330
+ else:
331
+ break
332
+
333
+ if quit_early_after_convergence:
334
+ energy_diff = abs(last_successful_energy - previous_phase_energy)
335
+ if energy_diff <= energy_tolerance:
336
+ return last_successful_energy
337
+
338
+ previous_phase_energy = last_successful_energy
339
+
340
+ return last_successful_energy
250
341
 
251
342
 
252
343
  ################################################################################
@@ -434,9 +525,7 @@ def check_if_atoms_interacting(
434
525
  atoms_interacting_energy = check_if_atoms_interacting_energy(
435
526
  model, symbols, etol
436
527
  )
437
- atoms_interacting_force = check_if_atoms_interacting_energy(
438
- model, symbols, ftol
439
- )
528
+ atoms_interacting_force = check_if_atoms_interacting_force(model, symbols, ftol)
440
529
  return atoms_interacting_energy, atoms_interacting_force
441
530
 
442
531
 
kim_tools/kimunits.py CHANGED
@@ -12,6 +12,8 @@ import re
12
12
  import subprocess
13
13
  import warnings
14
14
 
15
+ import numpy as np
16
+
15
17
  warnings.simplefilter("ignore")
16
18
 
17
19
 
@@ -24,17 +26,31 @@ _units_output_expression = re.compile(
24
26
  )
25
27
 
26
28
 
27
- def check_units_util():
29
+ def check_units_util() -> str:
28
30
  """
29
- Check that units util can be found
31
+ Figure out if units (first choice) or gunits (second choice) works
32
+ with the options that we use
30
33
  """
34
+ args = ["-o", r"%1.15e", "-qt1", "0.0 eV/angstrom^3 bar"]
31
35
  try:
32
- subprocess.check_output(["units", "--help"])
36
+ output = subprocess.check_output(["units"] + args, encoding="utf-8")
37
+ assert np.isclose(float(output), 0)
38
+ units_command = "units"
33
39
  except Exception:
34
- raise UnitConversion(
35
- "Failed to run a 'units' test command. It is likely "
36
- "that the 'units' executable was not found."
37
- )
40
+ try:
41
+ output = subprocess.check_output(["gunits"] + args, encoding="utf-8")
42
+ assert np.isclose(float(output), 0)
43
+ units_command = "gunits"
44
+ except Exception:
45
+ raise UnitConversion(
46
+ "Neither "
47
+ r"units -o %1.15e -qt1 '0.0 eV/angstrom^3 bar'"
48
+ " nor "
49
+ r"gunits -o %1.15e -qt1 '0.0 eV/angstrom^3 bar'"
50
+ " successfully ran and returned 0.e0. Please install a "
51
+ "compatible version of units."
52
+ )
53
+ return units_command
38
54
 
39
55
 
40
56
  def linear_fit(x, y):
@@ -68,7 +84,7 @@ def islinear(unit, to_unit=None):
68
84
 
69
85
  def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
70
86
  """Works with 'units' utility"""
71
- check_units_util()
87
+ units_util = check_units_util()
72
88
  from_sign = from_value < 0
73
89
  from_value = str(abs(from_value))
74
90
  from_unit = str(from_unit)
@@ -77,7 +93,7 @@ def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
77
93
 
78
94
  if from_unit in TEMPERATURE_FUNCTION_UNITS:
79
95
  args = [
80
- "units",
96
+ units_util,
81
97
  "-o",
82
98
  "%1.15e",
83
99
  "-qt1",
@@ -85,7 +101,7 @@ def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
85
101
  ]
86
102
 
87
103
  else:
88
- args = ["units", "-o", "%1.15e", "-qt1", " ".join((from_value, from_unit))]
104
+ args = [units_util, "-o", "%1.15e", "-qt1", " ".join((from_value, from_unit))]
89
105
 
90
106
  if wanted_unit:
91
107
  args.append(wanted_unit)