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.
@@ -30,12 +30,16 @@
30
30
  Helper classes for KIM Test Drivers
31
31
 
32
32
  """
33
+ import glob
34
+ import json
33
35
  import logging
34
36
  import os
35
37
  import shutil
38
+ import tarfile
36
39
  from abc import ABC, abstractmethod
37
40
  from copy import deepcopy
38
41
  from pathlib import Path
42
+ from secrets import token_bytes
39
43
  from tempfile import NamedTemporaryFile, TemporaryDirectory
40
44
  from typing import IO, Any, Dict, List, Optional, Union
41
45
 
@@ -44,7 +48,9 @@ import kim_edn
44
48
  import numpy as np
45
49
  import numpy.typing as npt
46
50
  from ase import Atoms
51
+ from ase.build import bulk
47
52
  from ase.calculators.calculator import Calculator
53
+ from ase.calculators.kim import get_model_supported_species
48
54
  from ase.constraints import FixSymmetry
49
55
  from ase.data import atomic_masses
50
56
  from ase.filters import FrechetCellFilter, UnitCellFilter
@@ -63,6 +69,7 @@ from kim_query import raw_query
63
69
  from ..aflow_util import (
64
70
  AFLOW,
65
71
  get_space_group_number_from_prototype,
72
+ get_stoich_reduced_list_from_prototype,
66
73
  prototype_labels_are_equivalent,
67
74
  )
68
75
  from ..aflow_util.core import AFLOW_EXECUTABLE, get_atom_indices_for_each_wyckoff_orb
@@ -91,6 +98,8 @@ __all__ = [
91
98
  "detect_unique_crystal_structures",
92
99
  "get_deduplicated_property_instances",
93
100
  "minimize_wrapper",
101
+ "crystal_input_from_test_generator_line",
102
+ "get_supported_lammps_atom_style",
94
103
  ]
95
104
 
96
105
  # Force tolerance for the optional initial relaxation of the provided cell
@@ -102,6 +111,50 @@ PROP_SEARCH_PATHS_INFO = (
102
111
  "- $PWD/local-props/**/\n"
103
112
  "- $PWD/local_props/**/"
104
113
  )
114
+ TOKEN_NAME = "kim-tools.token"
115
+ TOKENPATH = os.path.join("output", TOKEN_NAME)
116
+
117
+
118
+ def get_supported_lammps_atom_style(model: str) -> str:
119
+ """
120
+ Get the supported LAMMPS atom_style of a KIM model
121
+ """
122
+ from lammps import lammps
123
+
124
+ candidate_atom_styles = ("atomic", "charge", "full")
125
+ banned_species = "electron" # Species in KIM models not understood by ASE
126
+ supported_species = get_model_supported_species(model)
127
+ test_species = None
128
+ for species in supported_species:
129
+ if species not in banned_species:
130
+ test_species = species
131
+ break
132
+ if test_species is None:
133
+ raise RuntimeError(
134
+ "Model appears to only support species not understood by ASE:\n"
135
+ + str(supported_species)
136
+ )
137
+ atoms = bulk(test_species, "fcc", 10.0) # Very low-density FCC lattice
138
+ for atom_style in candidate_atom_styles:
139
+ with lammps(cmdargs=["-sc", "none"]) as lmp, NamedTemporaryFile() as f:
140
+ lmp.command(f"kim init {model} metal unit_conversion_mode")
141
+ atoms.write(f.name, format="lammps-data", atom_style=atom_style)
142
+ try:
143
+ lmp.command(f"read_data {f.name}")
144
+ return atom_style
145
+ except Exception as e:
146
+ if str(e).startswith(
147
+ "ERROR: Incorrect format in Atoms section of data file:"
148
+ ):
149
+ continue
150
+ else:
151
+ msg = (
152
+ "The following unexpected exception was encountered when trying"
153
+ " to determine atom_style:\n" + repr(e)
154
+ )
155
+ print(msg)
156
+ raise e
157
+ raise RuntimeError("Unable to determine supported atom type")
105
158
 
106
159
 
107
160
  def _get_optional_source_value(property_instance: Dict, key: str) -> Any:
@@ -137,8 +190,8 @@ def minimize_wrapper(
137
190
  steps: int = MAXSTEPS_INITIAL,
138
191
  variable_cell: bool = True,
139
192
  logfile: Optional[Union[str, IO]] = "kim-tools.log",
140
- algorithm: Optimizer = LBFGSLineSearch,
141
- cell_filter: UnitCellFilter = FrechetCellFilter,
193
+ algorithm: type[Optimizer] = LBFGSLineSearch,
194
+ cell_filter: type[UnitCellFilter] = FrechetCellFilter,
142
195
  fix_symmetry: Union[bool, FixSymmetry] = False,
143
196
  opt_kwargs: Dict = {},
144
197
  flt_kwargs: Dict = {},
@@ -189,12 +242,13 @@ def minimize_wrapper(
189
242
  Returns:
190
243
  Whether the minimization succeeded
191
244
  """
245
+ existing_constraints = atoms.constraints
192
246
  if fix_symmetry is not False:
193
247
  if fix_symmetry is True:
194
248
  symmetry = FixSymmetry(atoms)
195
249
  else:
196
250
  symmetry = fix_symmetry
197
- atoms.set_constraint(symmetry)
251
+ atoms.set_constraint([symmetry] + existing_constraints)
198
252
  if variable_cell:
199
253
  supercell_wrapped = cell_filter(atoms, **flt_kwargs)
200
254
  opt = algorithm(supercell_wrapped, logfile=logfile, **opt_kwargs)
@@ -226,7 +280,7 @@ def minimize_wrapper(
226
280
  + " steps."
227
281
  )
228
282
 
229
- del atoms.constraints
283
+ atoms.set_constraint(existing_constraints)
230
284
 
231
285
  if minimization_stalled or iteration_limits_reached:
232
286
  try:
@@ -240,7 +294,7 @@ def minimize_wrapper(
240
294
  "trying to evaluate final forces and stress:"
241
295
  )
242
296
  logger.info(repr(e))
243
- return False
297
+ return False
244
298
  else:
245
299
  return True
246
300
 
@@ -345,8 +399,15 @@ def _add_property_instance(
345
399
  property_name = path_str
346
400
  found_custom_property = True
347
401
  break
348
- except Exception:
349
- 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)
350
411
 
351
412
  if not found_custom_property:
352
413
  raise KIMTestDriverError(
@@ -500,6 +561,14 @@ class KIMTestDriver(ABC):
500
561
  __output_property_instances (str):
501
562
  Property instances, possibly accumulated over multiple invocations of
502
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
503
572
  """
504
573
 
505
574
  class NonKIMModelError(Exception):
@@ -509,11 +578,15 @@ class KIMTestDriver(ABC):
509
578
  requires a KIM model (e.g. a LAMMPS TD) with a non-KIM Calculator
510
579
  """
511
580
 
512
- def __init__(self, model: Union[str, Calculator]) -> None:
581
+ def __init__(
582
+ self, model: Union[str, Calculator], suppr_sm_lmp_log: bool = False
583
+ ) -> None:
513
584
  """
514
585
  Args:
515
586
  model:
516
587
  ASE calculator or KIM model name to use
588
+ suppr_sm_lmp_log:
589
+ Suppress writing a lammps.log
517
590
  """
518
591
  if isinstance(model, Calculator):
519
592
  self.__calc = model
@@ -523,8 +596,13 @@ class KIMTestDriver(ABC):
523
596
 
524
597
  self.__kim_model_name = model
525
598
  self.__calc = KIM(self.__kim_model_name)
599
+ if suppr_sm_lmp_log:
600
+ if hasattr(self.__calc.parameters, "log_file"):
601
+ self.__calc.parameters.log_file = None
526
602
 
527
603
  self.__output_property_instances = "[]"
604
+ self.__files_to_keep_in_output = []
605
+ self.__token = None
528
606
 
529
607
  def _setup(self, material, **kwargs) -> None:
530
608
  """
@@ -532,6 +610,106 @@ class KIMTestDriver(ABC):
532
610
  """
533
611
  pass
534
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
+
535
713
  @abstractmethod
536
714
  def _calculate(self, **kwargs) -> None:
537
715
  """
@@ -546,8 +724,10 @@ class KIMTestDriver(ABC):
546
724
 
547
725
  * Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
548
726
  version, derived classes may override)
727
+ * Call :func:`~KIMTestDriver._init_output_dir`
549
728
  * Call :func:`~KIMTestDriver._calculate` (implemented by each individual
550
729
  Test Driver)
730
+ * Call :func:`~KIMTestDriver._archive_aux_files`
551
731
 
552
732
  Args:
553
733
  material:
@@ -557,8 +737,12 @@ class KIMTestDriver(ABC):
557
737
  Returns:
558
738
  The property instances calculated during the current run
559
739
  """
740
+
560
741
  # count how many instances we had before we started
561
- previous_properties_end = len(kim_edn.loads(self.__output_property_instances))
742
+ previous_properties_end = len(self.property_instances)
743
+
744
+ # Set up the output directory
745
+ self._init_output_dir()
562
746
 
563
747
  # _setup is likely overridden by an derived class
564
748
  self._setup(material, **kwargs)
@@ -566,9 +750,12 @@ class KIMTestDriver(ABC):
566
750
  # implemented by each individual Test Driver
567
751
  self._calculate(**kwargs)
568
752
 
753
+ # Postprocess output directory for this invocation
754
+ self._archive_aux_files()
755
+
569
756
  # The current invocation returns a Python list of dictionaries containing all
570
757
  # properties computed during this run
571
- return kim_edn.loads(self.__output_property_instances)[previous_properties_end:]
758
+ return self.property_instances[previous_properties_end:]
572
759
 
573
760
  def _add_property_instance(
574
761
  self, property_name: str, disclaimer: Optional[str] = None
@@ -686,7 +873,7 @@ class KIMTestDriver(ABC):
686
873
 
687
874
  input_name = filename_path.name
688
875
  if add_instance_id:
689
- current_instance_id = len(kim_edn.loads(self.__output_property_instances))
876
+ current_instance_id = len(self.property_instances)
690
877
  root, ext = os.path.splitext(input_name)
691
878
  root = root + "-" + str(current_instance_id)
692
879
  final_name = root + ext
@@ -699,10 +886,18 @@ class KIMTestDriver(ABC):
699
886
 
700
887
  shutil.move(filename, final_path)
701
888
 
889
+ output_relpath = os.path.relpath(final_path, output_path)
702
890
  # Filenames are reported relative to $CWD/output
703
- self._add_key_to_current_property_instance(
704
- name, os.path.relpath(final_path, output_path)
705
- )
891
+ self._add_key_to_current_property_instance(name, output_relpath)
892
+ self.__files_to_keep_in_output.append(output_relpath)
893
+
894
+ def _get_supported_lammps_atom_style(self) -> str:
895
+ """
896
+ Return the atom_style that should be used when writing LAMMPS data files.
897
+ This atom_style will be compatible with the KIM model this object
898
+ was instantiated with.
899
+ """
900
+ return get_supported_lammps_atom_style(self.kim_model_name)
706
901
 
707
902
  @property
708
903
  def kim_model_name(self) -> Optional[str]:
@@ -727,8 +922,20 @@ class KIMTestDriver(ABC):
727
922
  @property
728
923
  def _calc(self) -> Optional[Calculator]:
729
924
  """
730
- Get the ASE calculator
925
+ Get the ASE calculator. Reinstantiate it if it's a KIM SM
731
926
  """
927
+ if self.__kim_model_name is not None:
928
+ reinst = False
929
+ if hasattr(self.__calc, "clean"):
930
+ self.__calc.clean()
931
+ reinst = True
932
+ if hasattr(self.__calc, "__del__"):
933
+ self.__calc.__del__()
934
+ reinst = True
935
+ if reinst:
936
+ from ase.calculators.kim.kim import KIM
937
+
938
+ self.__calc = KIM(self.__kim_model_name)
732
939
  return self.__calc
733
940
 
734
941
  def _get_serialized_property_instances(self) -> str:
@@ -746,17 +953,23 @@ class KIMTestDriver(ABC):
746
953
  def write_property_instances_to_file(self, filename="output/results.edn") -> None:
747
954
  """
748
955
  Write internal property instances (possibly accumulated over several calls to
749
- the Test Driver) to a file at the requested path. Also dumps any cached files to
750
- the same directory.
956
+ the Test Driver) to a file at the requested path.
751
957
 
752
958
  Args:
753
959
  filename: path to write the file
754
960
  """
755
-
756
- with open(filename, "w") as f:
757
- kim_property_dump(
758
- self.__output_property_instances, f
759
- ) # serialize the dictionary to string first
961
+ filename_parent = Path(filename).parent.resolve()
962
+ os.makedirs(filename_parent, exist_ok=True)
963
+ kim_property_dump(self._get_serialized_property_instances(), filename)
964
+ if filename_parent != Path("output").resolve():
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"))
760
973
 
761
974
  def get_isolated_energy_per_atom(self, symbol: str) -> float:
762
975
  """
@@ -791,6 +1004,7 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
791
1004
  temperature_unit: Optional[str] = "K",
792
1005
  crystal_genome_source_structure_id: Optional[List[List[str]]] = None,
793
1006
  aflow_executable: str = AFLOW_EXECUTABLE,
1007
+ omit_keys: Optional[List[str]] = None,
794
1008
  ) -> str:
795
1009
  """
796
1010
  Write common Crystal Genome keys to the last element of ``property_instances``. See
@@ -804,19 +1018,26 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
804
1018
  The key will be added to the last dictionary in the list
805
1019
  aflow_executable:
806
1020
  Path to the AFLOW executable
1021
+ omit_keys:
1022
+ Which keys to omit writing
807
1023
 
808
1024
  Returns:
809
1025
  Updated EDN-serialized list of property instances
810
1026
  """
811
- property_instances = _add_key_to_current_property_instance(
812
- property_instances, "prototype-label", prototype_label
813
- )
814
- property_instances = _add_key_to_current_property_instance(
815
- property_instances, "stoichiometric-species", stoichiometric_species
816
- )
817
- property_instances = _add_key_to_current_property_instance(
818
- property_instances, "a", a, a_unit
819
- )
1027
+ if omit_keys is None:
1028
+ omit_keys = []
1029
+ if "prototype-label" not in omit_keys:
1030
+ property_instances = _add_key_to_current_property_instance(
1031
+ property_instances, "prototype-label", prototype_label
1032
+ )
1033
+ if "stoichiometric-species" not in omit_keys:
1034
+ property_instances = _add_key_to_current_property_instance(
1035
+ property_instances, "stoichiometric-species", stoichiometric_species
1036
+ )
1037
+ if "a" not in omit_keys:
1038
+ property_instances = _add_key_to_current_property_instance(
1039
+ property_instances, "a", a, a_unit
1040
+ )
820
1041
 
821
1042
  # get parameter names
822
1043
  aflow = AFLOW(aflow_executable=aflow_executable)
@@ -833,24 +1054,28 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
833
1054
  "Incorrect number of parameter_values (i.e. dimensionless parameters "
834
1055
  "besides a) for the provided prototype"
835
1056
  )
836
- property_instances = _add_key_to_current_property_instance(
837
- property_instances, "parameter-names", aflow_parameter_names[1:]
838
- )
839
- property_instances = _add_key_to_current_property_instance(
840
- property_instances, "parameter-values", parameter_values
841
- )
1057
+ if "parameter-names" not in omit_keys:
1058
+ property_instances = _add_key_to_current_property_instance(
1059
+ property_instances, "parameter-names", aflow_parameter_names[1:]
1060
+ )
1061
+ if "parameter-values" not in omit_keys:
1062
+ property_instances = _add_key_to_current_property_instance(
1063
+ property_instances, "parameter-values", parameter_values
1064
+ )
842
1065
 
843
1066
  if short_name is not None:
844
1067
  if not isinstance(short_name, list):
845
1068
  short_name = [short_name]
846
- property_instances = _add_key_to_current_property_instance(
847
- property_instances, "short-name", short_name
848
- )
1069
+ if "short-name" not in omit_keys:
1070
+ property_instances = _add_key_to_current_property_instance(
1071
+ property_instances, "short-name", short_name
1072
+ )
849
1073
 
850
1074
  if library_prototype_label is not None:
851
- property_instances = _add_key_to_current_property_instance(
852
- property_instances, "library-prototype-label", library_prototype_label
853
- )
1075
+ if "library-prototype-label" not in omit_keys:
1076
+ property_instances = _add_key_to_current_property_instance(
1077
+ property_instances, "library-prototype-label", library_prototype_label
1078
+ )
854
1079
 
855
1080
  if cell_cauchy_stress is not None:
856
1081
  if len(cell_cauchy_stress) != 6:
@@ -859,27 +1084,30 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
859
1084
  "order [xx, yy, zz, yz, xz, xy]"
860
1085
  )
861
1086
  if cell_cauchy_stress_unit is None:
862
- raise KIMTestDriver("Please provide a `cell_cauchy_stress_unit`")
863
- property_instances = _add_key_to_current_property_instance(
864
- property_instances,
865
- "cell-cauchy-stress",
866
- cell_cauchy_stress,
867
- cell_cauchy_stress_unit,
868
- )
1087
+ raise KIMTestDriverError("Please provide a `cell_cauchy_stress_unit`")
1088
+ if "cell-cauchy-stress" not in omit_keys:
1089
+ property_instances = _add_key_to_current_property_instance(
1090
+ property_instances,
1091
+ "cell-cauchy-stress",
1092
+ cell_cauchy_stress,
1093
+ cell_cauchy_stress_unit,
1094
+ )
869
1095
 
870
1096
  if temperature is not None:
871
1097
  if temperature_unit is None:
872
- raise KIMTestDriver("Please provide a `temperature_unit`")
873
- property_instances = _add_key_to_current_property_instance(
874
- property_instances, "temperature", temperature, temperature_unit
875
- )
1098
+ raise KIMTestDriverError("Please provide a `temperature_unit`")
1099
+ if "temperature" not in omit_keys:
1100
+ property_instances = _add_key_to_current_property_instance(
1101
+ property_instances, "temperature", temperature, temperature_unit
1102
+ )
876
1103
 
877
1104
  if crystal_genome_source_structure_id is not None:
878
- property_instances = _add_key_to_current_property_instance(
879
- property_instances,
880
- "crystal-genome-source-structure-id",
881
- crystal_genome_source_structure_id,
882
- )
1105
+ if "crystal-genome-source-structure-id" not in omit_keys:
1106
+ property_instances = _add_key_to_current_property_instance(
1107
+ property_instances,
1108
+ "crystal-genome-source-structure-id",
1109
+ crystal_genome_source_structure_id,
1110
+ )
883
1111
 
884
1112
  return property_instances
885
1113
 
@@ -901,6 +1129,7 @@ def _add_property_instance_and_common_crystal_genome_keys(
901
1129
  disclaimer: Optional[str] = None,
902
1130
  property_instances: Optional[str] = None,
903
1131
  aflow_executable: str = AFLOW_EXECUTABLE,
1132
+ omit_keys: Optional[List[str]] = None,
904
1133
  ) -> str:
905
1134
  """
906
1135
  Initialize a new property instance to ``property_instances`` (an empty
@@ -923,6 +1152,8 @@ def _add_property_instance_and_common_crystal_genome_keys(
923
1152
  A pre-existing EDN-serialized list of KIM Property instances to add to
924
1153
  aflow_executable:
925
1154
  Path to the AFLOW executable
1155
+ omit_keys:
1156
+ Which keys to omit writing
926
1157
 
927
1158
  Returns:
928
1159
  Updated EDN-serialized list of property instances
@@ -945,6 +1176,7 @@ def _add_property_instance_and_common_crystal_genome_keys(
945
1176
  cell_cauchy_stress_unit=cell_cauchy_stress_unit,
946
1177
  cell_cauchy_stress=cell_cauchy_stress,
947
1178
  aflow_executable=aflow_executable,
1179
+ omit_keys=omit_keys,
948
1180
  )
949
1181
 
950
1182
 
@@ -1193,17 +1425,22 @@ class SingleCrystalTestDriver(KIMTestDriver):
1193
1425
  """
1194
1426
 
1195
1427
  def __init__(
1196
- self, model: Union[str, Calculator], aflow_executable: str = AFLOW_EXECUTABLE
1428
+ self,
1429
+ model: Union[str, Calculator],
1430
+ suppr_sm_lmp_log: bool = False,
1431
+ aflow_executable: str = AFLOW_EXECUTABLE,
1197
1432
  ) -> None:
1198
1433
  """
1199
1434
  Args:
1200
1435
  model:
1201
1436
  ASE calculator or KIM model name to use
1437
+ suppr_sm_lmp_log:
1438
+ Suppress writing a lammps.log
1202
1439
  aflow_executable:
1203
1440
  Path to AFLOW executable
1204
1441
  """
1205
1442
  self.aflow_executable = aflow_executable
1206
- super().__init__(model)
1443
+ super().__init__(model, suppr_sm_lmp_log=suppr_sm_lmp_log)
1207
1444
 
1208
1445
  def _setup(
1209
1446
  self,
@@ -1273,17 +1510,28 @@ class SingleCrystalTestDriver(KIMTestDriver):
1273
1510
  crystal_structure = get_crystal_structure_from_atoms(
1274
1511
  atoms=material, aflow_executable=self.aflow_executable
1275
1512
  )
1513
+ aflow = AFLOW()
1514
+ atoms_rebuilt = get_atoms_from_crystal_structure(crystal_structure)
1515
+ _, self.__input_rotation, _, _ = (
1516
+ aflow.get_basistransformation_rotation_originshift_atom_map_from_atoms(
1517
+ atoms_rebuilt,
1518
+ material,
1519
+ )
1520
+ )
1276
1521
  msg = (
1277
- "Rebuilding atoms object in a standard setting defined by "
1522
+ "Rebuilding Atoms object in a standard setting defined by "
1278
1523
  "doi.org/10.1016/j.commatsci.2017.01.017. See log file or computed "
1279
1524
  "properties for the (possibly re-oriented) primitive cell that "
1280
- "computations will be based on."
1525
+ "computations will be based on. To obtain the rotation of this "
1526
+ "cell relative to the Atoms object you provided, use "
1527
+ f"{self.__class__.__name__}.get_input_rotation()"
1281
1528
  )
1282
1529
  logger.info(msg)
1283
1530
  print()
1284
1531
  print(msg)
1285
1532
  print()
1286
1533
  else:
1534
+ self.__input_rotation = None
1287
1535
  crystal_structure = material
1288
1536
 
1289
1537
  # Pop the temperature and stress keys in case they came along with a query
@@ -1337,6 +1585,7 @@ class SingleCrystalTestDriver(KIMTestDriver):
1337
1585
  cell_rtol: float = 0.01,
1338
1586
  rot_rtol: float = 0.01,
1339
1587
  rot_atol: float = 0.01,
1588
+ match_library_proto: bool = True,
1340
1589
  ) -> None:
1341
1590
  """
1342
1591
  Update the nominal parameter values of the nominal crystal structure from the
@@ -1381,6 +1630,8 @@ class SingleCrystalTestDriver(KIMTestDriver):
1381
1630
  Parameter to pass to :func:`numpy.allclose` for compariong fractional
1382
1631
  rotations. Default value chosen to be commensurate with AFLOW
1383
1632
  default distance tolerance of 0.01*(NN distance)
1633
+ match_library_proto:
1634
+ Whether to match to library prototypes
1384
1635
 
1385
1636
  Raises:
1386
1637
  AFLOW.FailedToMatchException:
@@ -1393,18 +1644,29 @@ class SingleCrystalTestDriver(KIMTestDriver):
1393
1644
  """
1394
1645
  aflow = AFLOW(aflow_executable=self.aflow_executable)
1395
1646
  try:
1396
- (aflow_parameter_values, library_prototype_label, short_name) = (
1397
- aflow.solve_for_params_of_known_prototype(
1647
+ if match_library_proto:
1648
+ (aflow_parameter_values, library_prototype_label, short_name) = (
1649
+ aflow.solve_for_params_of_known_prototype(
1650
+ atoms=atoms,
1651
+ prototype_label=self.get_nominal_prototype_label(),
1652
+ max_resid=max_resid,
1653
+ cell_rtol=cell_rtol,
1654
+ rot_rtol=rot_rtol,
1655
+ rot_atol=rot_atol,
1656
+ )
1657
+ )
1658
+ else:
1659
+ aflow_parameter_values = aflow.solve_for_params_of_known_prototype(
1398
1660
  atoms=atoms,
1399
- prototype_label=self.__nominal_crystal_structure_npt[
1400
- "prototype-label"
1401
- ]["source-value"],
1661
+ prototype_label=self.get_nominal_prototype_label(),
1402
1662
  max_resid=max_resid,
1403
1663
  cell_rtol=cell_rtol,
1404
1664
  rot_rtol=rot_rtol,
1405
1665
  rot_atol=rot_atol,
1666
+ match_library_proto=False,
1406
1667
  )
1407
- )
1668
+ library_prototype_label = None
1669
+ short_name = None
1408
1670
  except (AFLOW.FailedToMatchException, AFLOW.ChangedSymmetryException) as e:
1409
1671
  raise type(e)(
1410
1672
  "Encountered an error that MAY be the result of the nominal crystal "
@@ -1498,6 +1760,7 @@ class SingleCrystalTestDriver(KIMTestDriver):
1498
1760
  stress_unit: Optional[str] = None,
1499
1761
  temp_unit: str = "K",
1500
1762
  disclaimer: Optional[str] = None,
1763
+ omit_keys: Optional[List[str]] = None,
1501
1764
  ) -> None:
1502
1765
  """
1503
1766
  Initialize a new property instance to ``self.property_instances``. It will
@@ -1528,6 +1791,8 @@ class SingleCrystalTestDriver(KIMTestDriver):
1528
1791
  disclaimer:
1529
1792
  An optional disclaimer commenting on the applicability of this result,
1530
1793
  e.g. "This relaxation did not reach the desired tolerance."
1794
+ omit_keys:
1795
+ Which keys to omit writing
1531
1796
  """
1532
1797
  crystal_structure = self.__nominal_crystal_structure_npt
1533
1798
 
@@ -1626,17 +1891,22 @@ class SingleCrystalTestDriver(KIMTestDriver):
1626
1891
  disclaimer=disclaimer,
1627
1892
  property_instances=super()._get_serialized_property_instances(),
1628
1893
  aflow_executable=self.aflow_executable,
1894
+ omit_keys=omit_keys,
1629
1895
  )
1630
1896
  )
1631
1897
 
1632
- self.__add_poscar_to_curr_prop_inst(
1633
- "primitive", "instance.poscar", "coordinates-file"
1634
- )
1635
- self.__add_poscar_to_curr_prop_inst(
1636
- "conventional",
1637
- "conventional.instance.poscar",
1638
- "coordinates-file-conventional",
1639
- )
1898
+ if omit_keys is None:
1899
+ omit_keys = []
1900
+ if "coordinates-file" not in omit_keys:
1901
+ self.__add_poscar_to_curr_prop_inst(
1902
+ "primitive", "instance.poscar", "coordinates-file"
1903
+ )
1904
+ if "coordinates-file-conventional" not in omit_keys:
1905
+ self.__add_poscar_to_curr_prop_inst(
1906
+ "conventional",
1907
+ "conventional.instance.poscar",
1908
+ "coordinates-file-conventional",
1909
+ )
1640
1910
 
1641
1911
  def _get_temperature(self, unit: str = "K") -> float:
1642
1912
  """
@@ -1872,18 +2142,65 @@ class SingleCrystalTestDriver(KIMTestDriver):
1872
2142
  "source-value"
1873
2143
  ]
1874
2144
 
2145
+ def get_nominal_space_group_number(self) -> int:
2146
+ return get_space_group_number_from_prototype(self.get_nominal_prototype_label())
2147
+
2148
+ def get_nominal_stoichiometric_species(self) -> List[str]:
2149
+ return self._get_nominal_crystal_structure_npt()["stoichiometric-species"][
2150
+ "source-value"
2151
+ ]
2152
+
2153
+ def get_nominal_stoichiometry(self) -> List[int]:
2154
+ return get_stoich_reduced_list_from_prototype(
2155
+ self.get_nominal_prototype_label()
2156
+ )
2157
+
2158
+ def get_nominal_a(self) -> float:
2159
+ return self._get_nominal_crystal_structure_npt()["a"]["source-value"]
2160
+
2161
+ def get_nominal_parameter_names(self) -> List[str]:
2162
+ return _get_optional_source_value(
2163
+ self._get_nominal_crystal_structure_npt(), "parameter-names"
2164
+ )
2165
+
2166
+ def get_nominal_parameter_values(self) -> List[float]:
2167
+ return _get_optional_source_value(
2168
+ self._get_nominal_crystal_structure_npt(), "parameter-values"
2169
+ )
2170
+
2171
+ def get_nominal_short_name(self) -> List[str]:
2172
+ return _get_optional_source_value(
2173
+ self._get_nominal_crystal_structure_npt(), "short-name"
2174
+ )
2175
+
2176
+ def get_nominal_library_prototype_label(self) -> str:
2177
+ return _get_optional_source_value(
2178
+ self._get_nominal_crystal_structure_npt(), "library-prototype-label"
2179
+ )
2180
+
1875
2181
  def get_atom_indices_for_each_wyckoff_orb(self) -> List[Dict]:
1876
2182
  """
1877
2183
  Get a list of dictionaries containing the atom indices of each Wyckoff
1878
2184
  orbit.
1879
2185
 
1880
2186
  Returns:
1881
- The information is in this format:
1882
-
1883
- [{"letter":"a", "indices":[0,1]}, ... ]
2187
+ The information is in this format --
2188
+ ``[{"letter":"a", "indices":[0,1]}, ... ]``
1884
2189
  """
1885
2190
  return get_atom_indices_for_each_wyckoff_orb(self.get_nominal_prototype_label())
1886
2191
 
2192
+ def get_input_rotation(self) -> Optional[npt.ArrayLike]:
2193
+ """
2194
+ Returns:
2195
+ If the Test Driver was called with an Atoms object, the nominal crystal
2196
+ structure may be rotated w.r.t. the input.
2197
+ This returns the Cartesian rotation to transform the Atoms input to the
2198
+ internal nominal crystal structure. I.e., if you want to get computed
2199
+ tensor properties in the same orientation as your input, you should
2200
+ rotate the reported tensors by the transpose of this rotation.
2201
+ """
2202
+ return self.__input_rotation
2203
+
1887
2204
 
1888
2205
  def query_crystal_structures(
1889
2206
  stoichiometric_species: List[str],
@@ -2236,3 +2553,42 @@ def get_deduplicated_property_instances(
2236
2553
  property_instances_deduplicated.sort(key=lambda a: a["instance-id"])
2237
2554
 
2238
2555
  return property_instances_deduplicated
2556
+
2557
+
2558
+ def crystal_input_from_test_generator_line(
2559
+ test_generator_line: str, kim_model_name: str
2560
+ ) -> List[Dict]:
2561
+ """
2562
+ Produce a list of dictionaries of kwargs for a Crystal Genome Test Driver invocation
2563
+ from a line in its ``test_generator.json``
2564
+ """
2565
+ test_generator_dict = json.loads(test_generator_line)
2566
+ stoichiometric_species = test_generator_dict["stoichiometric_species"]
2567
+ prototype_label = test_generator_dict["prototype_label"]
2568
+ cell_cauchy_stress_eV_angstrom3 = test_generator_dict.get(
2569
+ "cell_cauchy_stress_eV_angstrom3"
2570
+ )
2571
+ temperature_K = test_generator_dict.get("temperature_K")
2572
+ crystal_genome_test_args = test_generator_dict.get("crystal_genome_test_args")
2573
+ equilibria = query_crystal_structures(
2574
+ stoichiometric_species=stoichiometric_species,
2575
+ prototype_label=prototype_label,
2576
+ kim_model_name=kim_model_name,
2577
+ )
2578
+ inputs = []
2579
+ for equilibrium in equilibria:
2580
+ inputs.append(
2581
+ {
2582
+ "material": equilibrium,
2583
+ }
2584
+ )
2585
+ if cell_cauchy_stress_eV_angstrom3 is not None:
2586
+ inputs[-1][
2587
+ "cell_cauchy_stress_eV_angstrom3"
2588
+ ] = cell_cauchy_stress_eV_angstrom3
2589
+ if temperature_K is not None:
2590
+ inputs[-1]["temperature_K"] = temperature_K
2591
+ if crystal_genome_test_args is not None:
2592
+ inputs[-1].update(crystal_genome_test_args)
2593
+
2594
+ return inputs