kim-tools 0.3.13__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.13"
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
  )
@@ -1605,20 +1605,12 @@ class AFLOW:
1605
1605
  """
1606
1606
  # If max_resid not provided, determine it from neighborlist
1607
1607
  if max_resid is None:
1608
- nl_len = 0
1609
- cov_mult = 1
1610
- while nl_len == 0:
1611
- logger.info(
1612
- "Attempting to find NN distance by searching "
1613
- f"within covalent radii times {cov_mult}"
1614
- )
1615
- nl = neighbor_list("d", atoms, natural_cutoffs(atoms, mult=cov_mult))
1616
- nl_len = nl.size
1617
- cov_mult += 1
1618
1608
  # set the maximum error to 1% of NN distance to follow AFLOW convention
1619
1609
  # rescale by cube root of cell volume to get rough conversion from
1620
1610
  # cartesian to fractional
1621
- 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
+ )
1622
1614
  logger.info(
1623
1615
  "Automatically set max fractional residual for solving position "
1624
1616
  f"equations to {max_resid}"
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)
@@ -9,7 +9,6 @@ from itertools import product
9
9
  from math import ceil
10
10
  from typing import Dict, List, Optional, Tuple, Union
11
11
 
12
- import matplotlib.pyplot as plt
13
12
  import numpy as np
14
13
  import numpy.typing as npt
15
14
  import sympy as sp
@@ -17,11 +16,9 @@ from ase import Atoms
17
16
  from ase.cell import Cell
18
17
  from ase.constraints import FixSymmetry
19
18
  from ase.geometry import get_distances, get_duplicate_atoms
20
- from matplotlib.backends.backend_pdf import PdfPages
19
+ from ase.neighborlist import natural_cutoffs, neighbor_list
21
20
  from pymatgen.core.operations import SymmOp
22
21
  from pymatgen.core.tensors import Tensor
23
- from scipy.stats import kstest
24
- from sklearn.decomposition import PCA
25
22
  from sympy import Matrix, cos, matrix2numpy, sin, sqrt, symbols
26
23
  from sympy.tensor.array.expressions import ArrayContraction, ArrayTensorProduct
27
24
 
@@ -50,6 +47,7 @@ __all__ = [
50
47
  "change_of_basis_atoms",
51
48
  "get_possible_primitive_shifts",
52
49
  "get_primitive_genpos_ops",
50
+ "get_smallest_nn_dist",
53
51
  ]
54
52
 
55
53
  C_CENTERED_ORTHORHOMBIC_GROUPS = (20, 21, 35, 36, 37, 63, 64, 65, 66, 67, 68)
@@ -516,7 +514,9 @@ def get_change_of_basis_matrix_to_conventional_cell_from_formal_bravais_lattice(
516
514
  return np.round(change_of_basis_matrix)
517
515
 
518
516
 
519
- def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms:
517
+ def change_of_basis_atoms(
518
+ atoms: Atoms, change_of_basis: npt.ArrayLike, cutoff: Optional[float] = None
519
+ ) -> Atoms:
520
520
  """
521
521
  Perform an arbitrary basis change on an ``Atoms`` object, duplicating or cropping
522
522
  atoms as needed. A basic check is made that the determinant of ``change_of_basis``
@@ -524,7 +524,7 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
524
524
  that ``change_of_basis`` is appropriate for the particuar crystal described by
525
525
  ``atoms``, which is up to the user.
526
526
 
527
- TODO: Incorporate :func:`kstest_reduced_distances` into this function
527
+ TODO: Incorporate period extension test into this function
528
528
 
529
529
  Args:
530
530
  atoms:
@@ -543,6 +543,9 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
543
543
 
544
544
  Relationship between fractional coordinates in each basis:
545
545
  **x** = **P** **x**'
546
+ cutoff:
547
+ The cutoff to use for deleting duplicate atoms. If not specified,
548
+ the AFLOW tolerance of 0.01*(smallest NN distance) is used.
546
549
 
547
550
  Returns:
548
551
  The transformed ``Atoms`` object, containing the original number of
@@ -577,7 +580,9 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
577
580
  new_atoms = atoms.repeat(repeat)
578
581
  new_atoms.set_cell(new_cell)
579
582
  new_atoms.wrap()
580
- get_duplicate_atoms(new_atoms, delete=True)
583
+ if cutoff is None:
584
+ cutoff = get_smallest_nn_dist(atoms) * 0.01
585
+ get_duplicate_atoms(new_atoms, cutoff=cutoff, delete=True)
581
586
 
582
587
  volume_change = np.linalg.det(change_of_basis)
583
588
  if not np.isclose(len(atoms) * volume_change, len(new_atoms)):
@@ -634,14 +639,16 @@ def transform_atoms(atoms: Atoms, op: Dict) -> Atoms:
634
639
  return atoms_transformed
635
640
 
636
641
 
637
- def reduce_and_avg(
638
- atoms: Atoms, repeat: Tuple[int, int, int]
639
- ) -> Tuple[Atoms, npt.ArrayLike]:
642
+ def reduce_and_avg(atoms: Atoms, repeat: Tuple[int, int, int]) -> Atoms:
640
643
  """
641
644
  TODO: Upgrade :func:`change_of_basis_atoms` to provide the distances
642
645
  array, obviating this function
643
646
 
644
- Function to reduce all atoms to the original unit cell position.
647
+ Function to reduce all atoms to the original unit cell position,
648
+ assuming the supercell is built from contiguous repeats of the unit cell
649
+ (i.e. atoms 0 to N-1 in the supercell are the original unit cell, atoms N to
650
+ 2*[N-1] are the original unit cell shifted by an integer multiple of
651
+ the lattice vectors, and so on)
645
652
 
646
653
  Args:
647
654
  atoms:
@@ -651,10 +658,13 @@ def reduce_and_avg(
651
658
  provided supercell
652
659
 
653
660
  Returns:
654
- * The reduced unit cell
655
- * An array of displacement vectors. First dimension: index of reference atom
656
- in reduced cell. Second dimension: index of atom in provided supercell.
657
- Third dimension: x, y, z
661
+ The reduced unit cell
662
+
663
+ Raises:
664
+ PeriodExtensionException:
665
+ If two atoms that should be identical by translational symmetry
666
+ are further than 0.01*(smallest NN distance) apart when
667
+ reduced to the unit cell
658
668
  """
659
669
  new_atoms = atoms.copy()
660
670
 
@@ -685,126 +695,49 @@ def reduce_and_avg(
685
695
  # Start from end of the atoms
686
696
  # because we will remove all atoms except the reference ones.
687
697
  for i in reversed(range(number_atoms)):
698
+ reference_atom_index = i % original_number_atoms
688
699
  if i >= original_number_atoms:
689
700
  # Get the distance to the reference atom in the original unit cell with the
690
701
  # minimum image convention.
691
702
  distance = new_atoms.get_distance(
692
- i % original_number_atoms, i, mic=True, vector=True
703
+ reference_atom_index, i, mic=True, vector=True
693
704
  )
694
705
  # Get the position that has the closest distance to
695
706
  # the reference atom in the original unit cell.
696
- position_i = positions[i % original_number_atoms] + distance
707
+ position_i = positions[reference_atom_index] + distance
697
708
  # Remove atom from atoms object.
698
709
  new_atoms.pop()
699
710
  else:
700
711
  # Atom was part of the original unit cell.
701
712
  position_i = positions[i]
702
713
  # Average
703
- avg_positions_in_prim_cell[i % original_number_atoms] += position_i / M
714
+ avg_positions_in_prim_cell[reference_atom_index] += position_i / M
704
715
  positions_in_prim_cell[i] = position_i
705
716
 
706
717
  new_atoms.set_positions(avg_positions_in_prim_cell)
707
718
 
708
- # Calculate the distances.
709
- distances = np.empty((original_number_atoms, M, 3))
710
- for i in range(number_atoms):
711
- dr, _ = get_distances(
712
- positions_in_prim_cell[i],
713
- avg_positions_in_prim_cell[i % original_number_atoms],
719
+ # Check that all atoms are within tolerance of their translational images
720
+ cutoff = get_smallest_nn_dist(new_atoms) * 0.01
721
+ logger.info(f"Cutoff for period extension test is {cutoff}")
722
+ for i in range(original_number_atoms):
723
+ positions_of_all_images_of_atom_i = [
724
+ positions_in_prim_cell[j * original_number_atoms + i] for j in range(M)
725
+ ]
726
+ _, r = get_distances(
727
+ positions_of_all_images_of_atom_i,
714
728
  cell=new_atoms.get_cell(),
715
729
  pbc=True,
716
730
  )
717
- # dr is a distance matrix, here we only have one distance
718
- assert dr.shape == (1, 1, 3)
719
- distances[i % original_number_atoms, i // original_number_atoms] = dr[0][0]
720
-
721
- return new_atoms, distances
722
-
723
-
724
- def kstest_reduced_distances(
725
- reduced_distances: npt.ArrayLike,
726
- significance_level: float = 0.05,
727
- plot_filename: Optional[str] = None,
728
- number_bins: Optional[int] = None,
729
- ) -> None:
730
- """
731
- TODO: Incorporate this into :func:`change_of_basis_atoms`
732
-
733
- Function to test whether the reduced atom positions are normally distributed
734
- around their average.
735
-
736
- Args:
737
- reduced_distances:
738
- Distance array provided by :func:`reduce_and_avg`
739
- significance_level:
740
- Significance level for Kolmogorov-Smirnov
741
- plot_filename:
742
- number_bins:
743
- Number of bins for plot
744
-
745
- Raises:
746
- PeriodExtensionException:
747
- If a non-normal distribution is detected
748
- """
749
- assert len(reduced_distances.shape) == 3
750
- assert reduced_distances.shape[2] == 3
751
-
752
- if plot_filename is not None:
753
- if number_bins is None:
754
- raise ValueError(
755
- "number_bins must be specified if plot_filename is specified"
731
+ # Checking full MxM matrix, could probably speed up by
732
+ # checking upper triangle only. Could also save memory
733
+ # by looping over individual distances instead of
734
+ # checking the max of a giant matrix
735
+ assert r.shape == (M, M)
736
+ if r.max() > cutoff:
737
+ raise PeriodExtensionException(
738
+ f"At least one image of atom {i} is outside of tolerance"
756
739
  )
757
- if not plot_filename.endswith(".pdf"):
758
- raise ValueError(f"{plot_filename} is not a PDF file")
759
- with PdfPages(plot_filename) as pdf:
760
- for i in range(reduced_distances.shape[0]):
761
- fig, axs = plt.subplots(1, 3, figsize=(10.0, 4.0))
762
- for j in range(reduced_distances.shape[2]):
763
- axs[j].hist(reduced_distances[i, :, j], bins=number_bins)
764
- axs[j].set_xlabel(f"$x_{j}$")
765
- axs[0].set_ylabel("Counts")
766
- fig.suptitle(f"Atom {i}")
767
- pdf.savefig()
768
- else:
769
- if number_bins is not None:
770
- raise ValueError(
771
- "number_bins must not be specified if plot_filename is not specified"
772
- )
773
-
774
- p_values = np.empty((reduced_distances.shape[0], reduced_distances.shape[2]))
775
- for i in range(reduced_distances.shape[0]):
776
- atom_distances = reduced_distances[i]
777
-
778
- # Perform PCA on the xyz distribution.
779
- pca = PCA(n_components=atom_distances.shape[1])
780
- pca_components = pca.fit_transform(atom_distances)
781
- assert (
782
- pca_components.shape == atom_distances.shape == reduced_distances.shape[1:]
783
- )
784
-
785
- # Test each component with a KS test.
786
- for j in range(pca_components.shape[1]):
787
- component = pca_components[:, j]
788
- component_mean = np.mean(component)
789
- assert abs(component_mean) < 1.0e-7
790
- component_std = np.std(component)
791
- # Normalize component
792
- normalized_component = (component - component_mean) / component_std
793
- assert abs(np.mean(normalized_component)) < 1.0e-7
794
- assert abs(np.std(normalized_component) - 1.0) < 1.0e-7
795
- res = kstest(normalized_component, "norm")
796
- p_values[i, j] = res.pvalue
797
-
798
- if np.any(p_values <= significance_level):
799
- raise PeriodExtensionException(
800
- "Detected non-normal distribution of reduced atom positions around their "
801
- f"average (smallest p value {np.min(p_values)})."
802
- )
803
- else:
804
- print(
805
- "Detected normal distribution or reduced atom positions around their "
806
- f"average (smallest p value {np.min(p_values)})."
807
- )
740
+ return new_atoms
808
741
 
809
742
 
810
743
  def voigt_to_full_symb(voigt_input: sp.Array) -> sp.MutableDenseNDimArray:
@@ -1073,6 +1006,23 @@ def fit_voigt_tensor_to_cell_and_space_group(
1073
1006
  return t_symmetrized.voigt
1074
1007
 
1075
1008
 
1009
+ def get_smallest_nn_dist(atoms: Atoms) -> float:
1010
+ """
1011
+ Get the smallest NN distance in an Atoms object
1012
+ """
1013
+ nl_len = 0
1014
+ cov_mult = 1
1015
+ while nl_len == 0:
1016
+ logger.info(
1017
+ "Attempting to find NN distance by searching "
1018
+ f"within covalent radii times {cov_mult}"
1019
+ )
1020
+ nl = neighbor_list("d", atoms, natural_cutoffs(atoms, mult=cov_mult))
1021
+ nl_len = nl.size
1022
+ cov_mult += 1
1023
+ return nl.min()
1024
+
1025
+
1076
1026
  class FixProvidedSymmetry(FixSymmetry):
1077
1027
  """
1078
1028
  A modification of :obj:`~ase.constraints.FixSymmetry` that takes
@@ -35,9 +35,11 @@ import json
35
35
  import logging
36
36
  import os
37
37
  import shutil
38
+ import tarfile
38
39
  from abc import ABC, abstractmethod
39
40
  from copy import deepcopy
40
41
  from pathlib import Path
42
+ from secrets import token_bytes
41
43
  from tempfile import NamedTemporaryFile, TemporaryDirectory
42
44
  from typing import IO, Any, Dict, List, Optional, Union
43
45
 
@@ -109,6 +111,8 @@ PROP_SEARCH_PATHS_INFO = (
109
111
  "- $PWD/local-props/**/\n"
110
112
  "- $PWD/local_props/**/"
111
113
  )
114
+ TOKEN_NAME = "kim-tools.token"
115
+ TOKENPATH = os.path.join("output", TOKEN_NAME)
112
116
 
113
117
 
114
118
  def get_supported_lammps_atom_style(model: str) -> str:
@@ -395,8 +399,15 @@ def _add_property_instance(
395
399
  property_name = path_str
396
400
  found_custom_property = True
397
401
  break
398
- except Exception:
399
- pass
402
+ except Exception as e:
403
+ msg = (
404
+ "MESSAGE: Trying to load a property from the .edn file at\n"
405
+ f"{path}\n"
406
+ "failed with the following exception:\n"
407
+ f"{repr(e)}"
408
+ )
409
+ logger.info(msg)
410
+ print(msg)
400
411
 
401
412
  if not found_custom_property:
402
413
  raise KIMTestDriverError(
@@ -550,6 +561,14 @@ class KIMTestDriver(ABC):
550
561
  __output_property_instances (str):
551
562
  Property instances, possibly accumulated over multiple invocations of
552
563
  the Test Driver
564
+ __files_to_keep_in_output (List[PathLike]):
565
+ List of files that were written by this class explicitly, that we won't
566
+ touch when cleaning and backing up the output directory. Specified
567
+ relative to 'output' directory.
568
+ __token (Optional[bytes]):
569
+ Token that is written to TOKENPATH upon first evaluation. This
570
+ is used to check that multiple Test Drivers are not being called
571
+ concurrently, causing potential conflicts in the output directory
553
572
  """
554
573
 
555
574
  class NonKIMModelError(Exception):
@@ -582,6 +601,8 @@ class KIMTestDriver(ABC):
582
601
  self.__calc.parameters.log_file = None
583
602
 
584
603
  self.__output_property_instances = "[]"
604
+ self.__files_to_keep_in_output = []
605
+ self.__token = None
585
606
 
586
607
  def _setup(self, material, **kwargs) -> None:
587
608
  """
@@ -589,6 +610,106 @@ class KIMTestDriver(ABC):
589
610
  """
590
611
  pass
591
612
 
613
+ def _init_output_dir(self) -> None:
614
+ """
615
+ Initialize the output directory
616
+ """
617
+ if self.__token is None:
618
+ # First time we've called this instance of the class
619
+ assert len(self.property_instances) == 0
620
+
621
+ os.makedirs("output", exist_ok=True)
622
+
623
+ # Move all top-level non-hidden files and directories
624
+ # to backup
625
+ output_glob = glob.glob("output/*")
626
+ if len(output_glob) > 0:
627
+ i = 0
628
+ while os.path.exists(f"output.{i}"):
629
+ i += 1
630
+ output_bak_name = f"output.{i}"
631
+ msg = (
632
+ "'output' directory is non-empty, backing up all "
633
+ f"non-hidden files and directories to {output_bak_name}"
634
+ )
635
+ print(msg)
636
+ logger.info(msg)
637
+ os.mkdir(output_bak_name)
638
+ for file_in_output in output_glob:
639
+ shutil.move(file_in_output, output_bak_name)
640
+
641
+ # Create token
642
+ self.__token = token_bytes(16)
643
+ with open(TOKENPATH, "wb") as f:
644
+ f.write(self.__token)
645
+ self.__files_to_keep_in_output.append(TOKEN_NAME)
646
+ else:
647
+ # Token is stored, check that it matches the token file
648
+ if not os.path.isfile(TOKENPATH):
649
+ raise KIMTestDriverError(
650
+ f"Token file at {TOKENPATH} was not found,"
651
+ "can't confirm non-interference of Test Drivers. Did something "
652
+ "edit the 'output' directory between calls to this Test Driver?"
653
+ )
654
+ else:
655
+ with open(TOKENPATH, "rb") as f:
656
+ if self.__token != f.read():
657
+ raise KIMTestDriverError(
658
+ f"Token file at {TOKENPATH} does not match this object's "
659
+ "token. This likely means that a different KIMTestDriver "
660
+ "instance was called between calls to this one. In order to"
661
+ " prevent conflicts in the output directory, this is not "
662
+ "allowed."
663
+ )
664
+
665
+ # We should have a record of all non-hidden files in output. If any
666
+ # untracked files are present, raise an error
667
+ output_glob = glob.glob("output/**", recursive=True)
668
+ for filepath in output_glob:
669
+ if os.path.isfile(filepath): # not tracking directories
670
+ if (
671
+ os.path.relpath(filepath, "output")
672
+ not in self.__files_to_keep_in_output
673
+ ):
674
+ raise KIMTestDriverError(
675
+ f"Unknown file {filepath} in 'output' directory appeared "
676
+ "between calls to this Test Driver. This is not allowed "
677
+ "because stray files can cause issues."
678
+ )
679
+
680
+ def _archive_aux_files(self) -> None:
681
+ """
682
+ Archive aux files after a run
683
+ """
684
+ # Archive untracked files as aux files
685
+ i = 0
686
+ while f"aux_files.{i}.txz" in self.__files_to_keep_in_output:
687
+ assert os.path.isfile(f"output/aux_files.{i}.txz")
688
+ i += 1
689
+ tar_prefix = f"aux_files.{i}"
690
+ output_glob = glob.glob("output/**", recursive=True)
691
+ archived_files = [] # For deleting them later
692
+ with tarfile.open(f"output/{tar_prefix}.txz", "w:xz") as tar:
693
+ for filepath in output_glob:
694
+ if os.path.isfile(filepath): # not tracking directories
695
+ output_relpath = os.path.relpath(filepath, "output")
696
+ if output_relpath not in self.__files_to_keep_in_output:
697
+ tar.add(
698
+ os.path.join(filepath),
699
+ os.path.join(tar_prefix, output_relpath),
700
+ )
701
+ archived_files.append(filepath)
702
+ self.__files_to_keep_in_output.append(f"{tar_prefix}.txz")
703
+ for filepath in archived_files:
704
+ os.remove(filepath)
705
+ try:
706
+ os.removedirs(os.path.dirname(filepath))
707
+ except OSError:
708
+ pass # might not be empty yet
709
+
710
+ # should not have removed output dir in any situation
711
+ assert os.path.isdir("output")
712
+
592
713
  @abstractmethod
593
714
  def _calculate(self, **kwargs) -> None:
594
715
  """
@@ -603,8 +724,10 @@ class KIMTestDriver(ABC):
603
724
 
604
725
  * Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
605
726
  version, derived classes may override)
727
+ * Call :func:`~KIMTestDriver._init_output_dir`
606
728
  * Call :func:`~KIMTestDriver._calculate` (implemented by each individual
607
729
  Test Driver)
730
+ * Call :func:`~KIMTestDriver._archive_aux_files`
608
731
 
609
732
  Args:
610
733
  material:
@@ -614,10 +737,12 @@ class KIMTestDriver(ABC):
614
737
  Returns:
615
738
  The property instances calculated during the current run
616
739
  """
740
+
617
741
  # count how many instances we had before we started
618
- previous_properties_end = len(kim_edn.loads(self.__output_property_instances))
742
+ previous_properties_end = len(self.property_instances)
619
743
 
620
- os.makedirs("output", exist_ok=True)
744
+ # Set up the output directory
745
+ self._init_output_dir()
621
746
 
622
747
  # _setup is likely overridden by an derived class
623
748
  self._setup(material, **kwargs)
@@ -625,9 +750,12 @@ class KIMTestDriver(ABC):
625
750
  # implemented by each individual Test Driver
626
751
  self._calculate(**kwargs)
627
752
 
753
+ # Postprocess output directory for this invocation
754
+ self._archive_aux_files()
755
+
628
756
  # The current invocation returns a Python list of dictionaries containing all
629
757
  # properties computed during this run
630
- return kim_edn.loads(self.__output_property_instances)[previous_properties_end:]
758
+ return self.property_instances[previous_properties_end:]
631
759
 
632
760
  def _add_property_instance(
633
761
  self, property_name: str, disclaimer: Optional[str] = None
@@ -745,7 +873,7 @@ class KIMTestDriver(ABC):
745
873
 
746
874
  input_name = filename_path.name
747
875
  if add_instance_id:
748
- current_instance_id = len(kim_edn.loads(self.__output_property_instances))
876
+ current_instance_id = len(self.property_instances)
749
877
  root, ext = os.path.splitext(input_name)
750
878
  root = root + "-" + str(current_instance_id)
751
879
  final_name = root + ext
@@ -758,10 +886,10 @@ class KIMTestDriver(ABC):
758
886
 
759
887
  shutil.move(filename, final_path)
760
888
 
889
+ output_relpath = os.path.relpath(final_path, output_path)
761
890
  # Filenames are reported relative to $CWD/output
762
- self._add_key_to_current_property_instance(
763
- name, os.path.relpath(final_path, output_path)
764
- )
891
+ self._add_key_to_current_property_instance(name, output_relpath)
892
+ self.__files_to_keep_in_output.append(output_relpath)
765
893
 
766
894
  def _get_supported_lammps_atom_style(self) -> str:
767
895
  """
@@ -834,17 +962,14 @@ class KIMTestDriver(ABC):
834
962
  os.makedirs(filename_parent, exist_ok=True)
835
963
  kim_property_dump(self._get_serialized_property_instances(), filename)
836
964
  if filename_parent != Path("output").resolve():
837
- for file_in_output in glob.glob("output/*"):
838
- file_in_output_name = str(Path(file_in_output).name)
839
- for instance in self.property_instances:
840
- for key in instance:
841
- if isinstance(instance[key], dict):
842
- if file_in_output_name == instance[key]["source-value"]:
843
- shutil.move(file_in_output, filename_parent)
844
- elif isinstance(instance[key]["source-value"], list):
845
- for file_in_property in instance[key]["source-value"]:
846
- if file_in_output_name == file_in_property:
847
- shutil.move(file_in_output, filename_parent)
965
+ msg = (
966
+ f"Writing properties .edn file to non-standard location {filename}. "
967
+ "note that all other files remain in 'output' directory."
968
+ )
969
+ print(msg)
970
+ logger.info(msg)
971
+ else:
972
+ self.__files_to_keep_in_output.append(os.path.relpath(filename, "output"))
848
973
 
849
974
  def get_isolated_energy_per_atom(self, symbol: str) -> float:
850
975
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kim-tools
3
- Version: 0.3.13
3
+ Version: 0.4.2
4
4
  Summary: Base classes and helper routines for writing KIM Tests
5
5
  Author-email: ilia Nikiforov <nikif002@umn.edu>, Ellad Tadmor <tadmor@umn.edu>, Claire Waters <bwaters@umn.edu>, "Daniel S. Karls" <karl0100umn@gmail.com>, Matt Bierbaum <matt.bierbaum@gmail.com>, Eric Fuemmeler <efuemmel@umn.edu>, Philipp Hoellmer <ph2484@nyu.edu>, Guanming Zhang <gz2241@nyu.edu>, Tom Egg <tje3676@nyu.edu>, Navaneeth Mohan <mohan227@umn.edu>
6
6
  Maintainer-email: ilia Nikiforov <nikif002@umn.edu>
@@ -35,6 +35,7 @@ Dynamic: license-file
35
35
  [![Testing](https://github.com/openkim/kim-tools/actions/workflows/testing.yml/badge.svg)](https://github.com/openkim/kim-tools/actions/workflows/testing.yml)
36
36
  [![docs](https://app.readthedocs.org/projects/kim-tools/badge/?version=latest)](https://kim-tools.readthedocs.io/en/latest/)
37
37
  [![PyPI](https://img.shields.io/pypi/v/kim-tools.svg)](https://pypi.org/project/kim-tools/)
38
+ [![codecov](https://codecov.io/gh/openkim/kim-tools/graph/badge.svg?token=G57VDZYY0F)](https://codecov.io/gh/openkim/kim-tools)
38
39
 
39
40
  KIMTestDriver and SingleCrystalTestDriver classes for creating OpenKIM Test Drivers, and helper routines for writing
40
41
  KIM Tests and Verification Checks. Documentation at https://kim-tools.readthedocs.io.
@@ -1,7 +1,7 @@
1
- kim_tools/__init__.py,sha256=z9M4HWOpT0qCOUbGIKeUY-Oskrw_ZG6xeXli9iLTyzo,434
2
- kim_tools/kimunits.py,sha256=jOxBv9gRVhxPE6ygAIUxOzCAfPI6tT6sBaF_FNl9m-M,5387
1
+ kim_tools/__init__.py,sha256=ATvBQMEr5NBWxGzIDxLFJxMZIXTwxIx5RVS5-OO4H98,433
2
+ kim_tools/kimunits.py,sha256=aCouh7z6fQDR1rcbRTO72l-_RuccuznrKrNF18F-cEQ,6076
3
3
  kim_tools/aflow_util/__init__.py,sha256=lJnQ8fZCma80QVRQeKvY4MQ87oCWu-9KATV3dKJfpDc,80
4
- kim_tools/aflow_util/core.py,sha256=mMS2r9ayJJ6ApOFTAIkCZ72Dg3g1EnREbrqe2YEipMo,81273
4
+ kim_tools/aflow_util/core.py,sha256=OmU4-m2jl81jfs0ni4bSECpC8GYt7-hEEH7cfKmrdoM,80886
5
5
  kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A108B24C11D24_cP334_222_h4i_i_bf_i-001/info.json,sha256=IsFiO9X2Ko7yoq2QkDurUVP7k1BE4WFgblu7oxl6iZs,2013
6
6
  kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A10B11_tI84_139_dehim_eh2n-001/info.json,sha256=f1EdtouuSL2y9NNw40Rvz2J9ZZcsqQBcyEmlHj6XoW8,1186
7
7
  kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A10B2C_hP39_171_5c_c_a-001/info.json,sha256=vD1xjZKWShL0E6XNsSlmIhilGcGNefl56oQDLQlHO1M,1596
@@ -2004,7 +2004,7 @@ kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A_tP50_134_a2m2n-001/info
2004
2004
  kim_tools/ase/__init__.py,sha256=1i6ko5tNr0VZC3T7hoEzq4fnSU0DdxNpxXcSaWMcJWc,76
2005
2005
  kim_tools/ase/core.py,sha256=umJY0LV3_zrEZLOXAFoz6AFigxA69sAOBzAGoBTDzxA,34335
2006
2006
  kim_tools/symmetry_util/__init__.py,sha256=uu-ZSUDUTe2P81rkAS3tXverx31s_uZ3wL4SD_dn5aI,86
2007
- kim_tools/symmetry_util/core.py,sha256=eMGgVt9MPcaLi_8jVMJ5UyQHvYbiiAFQ4HdJotnp8dk,43277
2007
+ kim_tools/symmetry_util/core.py,sha256=y9LGnUYxWE5e5KMxzM96t_UzrC-B_aT3XnntW8p_jRA,41392
2008
2008
  kim_tools/symmetry_util/elasticity.py,sha256=VxJ8wUcsSOPyJ8id6OpKmVlRAbIUIWxtzYJtkvVJIVs,13328
2009
2009
  kim_tools/symmetry_util/data/elast_cubic.svg,sha256=UpN4XcoLoOwv8a7KE0WyINkSH5AU2DPVZ08eGQf-Cds,8953
2010
2010
  kim_tools/symmetry_util/data/elast_hexagonal.svg,sha256=wOkw5IO3fZy039tLHhvtwrkatVYs5XzigUormLKtRlw,8705
@@ -2023,11 +2023,11 @@ kim_tools/symmetry_util/data/wyck_pos_xform_under_normalizer.json,sha256=6g1YuYh
2023
2023
  kim_tools/symmetry_util/data/wyckoff_multiplicities.json,sha256=qG2RPBd_-ejDIfz-E4ZhkHyRpIboxRy7oiXkdDf5Eg8,32270
2024
2024
  kim_tools/symmetry_util/data/wyckoff_sets.json,sha256=f5ZpHKDHo6_JWki1b7KUGoYLlhU-44Qikw_-PtbLssw,9248
2025
2025
  kim_tools/test_driver/__init__.py,sha256=KOiceeZNqkfrgZ66CiRiUdniceDrCmmDXQkOw0wXaCQ,92
2026
- kim_tools/test_driver/core.py,sha256=vHmhSWix5t95SCmJEapdpZXfJS0ZlbG-8bcmV9O9v9M,101261
2026
+ kim_tools/test_driver/core.py,sha256=ugPL_wDzKwT7ch7Xh6SFMCLTF1Qf8x405HqcGJ29fd4,106587
2027
2027
  kim_tools/vc/__init__.py,sha256=zXjhxXCKVMLBMXXWYG3if7VOpBnsFrn_RjVpnohDm5c,74
2028
2028
  kim_tools/vc/core.py,sha256=BIjzEExnQAL2S90a_npptRm3ACqAo4fZBtvTDBMWMdw,13963
2029
- kim_tools-0.3.13.dist-info/licenses/LICENSE.CDDL,sha256=I2luEED_SHjuZ01B4rYG-AF_135amL24JpHvZ1Jhqe8,16373
2030
- kim_tools-0.3.13.dist-info/METADATA,sha256=7ldqSmsLFyzrEtIeV-wJacw1BEJoj55DgMOupJlycIs,2069
2031
- kim_tools-0.3.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
2032
- kim_tools-0.3.13.dist-info/top_level.txt,sha256=w_YCpJ5ERigj9te74ln7k64tqj1VumOzM_s9dsalIWY,10
2033
- kim_tools-0.3.13.dist-info/RECORD,,
2029
+ kim_tools-0.4.2.dist-info/licenses/LICENSE.CDDL,sha256=I2luEED_SHjuZ01B4rYG-AF_135amL24JpHvZ1Jhqe8,16373
2030
+ kim_tools-0.4.2.dist-info/METADATA,sha256=dCmTQkNmZhR6y-2GtlwbgMnFMh14kgKgTlkvGL4UHpw,2196
2031
+ kim_tools-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
2032
+ kim_tools-0.4.2.dist-info/top_level.txt,sha256=w_YCpJ5ERigj9te74ln7k64tqj1VumOzM_s9dsalIWY,10
2033
+ kim_tools-0.4.2.dist-info/RECORD,,