@@ -21,7 +21,14 @@ class is written.
2121from pymatgen .core import Lattice , Structure
2222from pymatgen .core .periodic_table import Element
2323from pymatgen .core .units import bohr_to_ang
24- from pymatgen .io .jdftx .generic_tags import AbstractTag , BoolTagContainer , DumpTagContainer , MultiformatTag , TagContainer
24+ from pymatgen .io .jdftx .generic_tags import (
25+ AbstractTag ,
26+ BoolTagContainer ,
27+ DumpTagContainer ,
28+ FloatTag ,
29+ MultiformatTag ,
30+ TagContainer ,
31+ )
2532from pymatgen .io .jdftx .jdftxinfile_default_inputs import default_inputs
2633from pymatgen .io .jdftx .jdftxinfile_master_format import (
2734 __PHONON_TAGS__ ,
@@ -45,14 +52,15 @@ class is written.
4552
4653__author__ = "Jacob Clary, Ben Rich"
4754
55+
4856# TODO: Add check for whether all ions have or lack velocities.
4957# TODO: Add default value filling like JDFTx does.
5058# TODO: Add more robust checking for if two repeatable tag values represent the
5159# same information. This is likely fixed by implementing filling of default values.
5260# TODO: Incorporate something to collapse repeated dump tags of the same frequency
5361# into a single value.
54-
55-
62+ # TODO: Add a method to strip all tags that contain their default values for simpler
63+ # files written (especially when a `JDFTXInfile` is created from `JDFTXOutfileSlice`)
5664class JDFTXInfile (dict , MSONable ):
5765 """Class for reading/writing JDFtx input files.
5866
@@ -270,6 +278,32 @@ def from_jdftxstructure(
270278 jstr = jdftxstructure .get_str ()
271279 return cls .from_str (jstr )
272280
281+ def read_line (
282+ self ,
283+ line : str ,
284+ validate_value_boundaries : bool = True ,
285+ autofix : bool = True ,
286+ overwrite_nonrepeatable : bool = True ,
287+ ) -> None :
288+ """Read a single line and update the JDFTXInfile object.
289+
290+ Convenience method for reading a single line and updating the JDFTXInfile object.
291+
292+ Args:
293+ line (str): Line to read.
294+ """
295+ line = line .strip ()
296+ tag_object , tag , value = self ._preprocess_line (line )
297+ if not tag_object .can_repeat and overwrite_nonrepeatable and tag in self :
298+ del self [tag ]
299+ processed_value = tag_object .read (tag , value )
300+ _params = self .as_dict (skip_module_keys = True )
301+ _params = self ._store_value (_params , tag_object , tag , processed_value )
302+ self .update (_params )
303+ self .validate_tags (try_auto_type_fix = autofix , error_on_failed_fix = True )
304+ if validate_value_boundaries :
305+ self .validate_boundaries ()
306+
273307 @classmethod
274308 def from_str (
275309 cls ,
@@ -397,7 +431,9 @@ def copy(self) -> JDFTXInfile:
397431 Returns:
398432 JDFTXInfile: Copy of the JDFTXInfile object.
399433 """
400- return type (self )(self )
434+ # Wasn't working before
435+ # return type(self)(self)
436+ return self .from_dict (self .as_dict (skip_module_keys = True ), validate_value_boundaries = False )
401437
402438 def get_text_list (self ) -> list [str ]:
403439 """Get a list of strings representation of the JDFTXInfile.
@@ -426,14 +462,24 @@ def get_text_list(self) -> list[str]:
426462 text .append ("" )
427463 return text
428464
429- def write_file (self , filename : PathLike ) -> None :
465+ # TODO: JDFTXInfile can accept nan for values, as this is occasionally what is stored
466+ # for unused variables, but JDFTx has no way read nan for an input value. All subtags
467+ # with nan values should be removed before writing to file.
468+ # TODO: Detect for and warn for tags that can be used together but likely shouldn't be,
469+ # ie (ion-width being 0 while fluid is not None)
470+ def write_file (self , filename : PathLike , strip_nan : bool = False ) -> None :
430471 """Write JDFTXInfile to an in file.
431472
432473 Args:
433474 filename (PathLike): Filename to write to.
475+ strip_nan (bool, optional): Whether to strip all subtags with nan values before writing.
476+ Defaults to False. WARNING - VERY JANKY RIGHT NOW
434477 """
478+ write_infile = self
479+ if strip_nan :
480+ write_infile = clean_infile_of_nans (self )
435481 with open (filename , mode = "w" ) as file :
436- file .write (str (self ))
482+ file .write (str (write_infile ))
437483
438484 @classmethod
439485 def to_jdftxstructure (
@@ -912,6 +958,107 @@ def movescale_array_to_selective_dynamics_site_prop(movescale: ArrayLike[int | f
912958 return selective_dynamics
913959
914960
961+ # def _strip_nans(infile: JDFTXInfile) -> JDFTXInfile:
962+ # for k, v in infile.items():
963+ # tag_object = get_tag_object_on_val(k, v)
964+ # if isinstance(v, dict):
965+ # infile[k] = _strip_nans(v)
966+ # if isinstance(v, float) and np.isnan(v):
967+ # infile[k] = None
968+ # elif isinstance(v, list):
969+ # infile[k] = [x for x in v if not (isinstance(x, float) and np.isnan(x))]
970+ # elif isinstance(v, dict):
971+ # infile[k] = _strip_nans(v)
972+
973+ # def _has_nans(value: float | list) -> bool:
974+ # if isinstance(value, float) and np.isnan(value):
975+ # return True
976+ # elif isinstance(value, list):
977+ # return any(_has_nans(x) for x in value)
978+ # return False
979+
980+
981+ def _isnan (x ):
982+ try :
983+ return np .isnan (x )
984+ except TypeError :
985+ return False
986+
987+
988+ def _check_tagcontainer_for_nan (tag_container : TagContainer , val_dict : dict ):
989+ hasnans = []
990+ for kk , vv in val_dict .items ():
991+ tag = tag_container .subtags [kk ]
992+ if not isinstance (tag , TagContainer ):
993+ if _isnan (vv ):
994+ print (f"Tag { kk } has nan value" )
995+ hasnans .append (kk )
996+ elif not tag .can_repeat :
997+ _hasnans = _check_tagcontainer_for_nan (tag , vv )
998+ if len (_hasnans ) > 0 :
999+ hasnans .append ({kk : _hasnans })
1000+ return hasnans
1001+
1002+
1003+ def has_nan_in_required_subtag (tag_container : TagContainer , val_dict : dict ):
1004+ for kk , vv in val_dict .items ():
1005+ tag = tag_container .subtags [kk ]
1006+ if not isinstance (tag , TagContainer ):
1007+ if _isnan (vv ) and not tag .optional :
1008+ return True
1009+ elif not tag .can_repeat and has_nan_in_required_subtag (tag , vv ):
1010+ return True
1011+ return False
1012+
1013+
1014+ def clean_tagcontainer_of_nans (tag_container : TagContainer , val_dict : dict ):
1015+ subtags_to_delete = []
1016+ for kk , vv in val_dict .items ():
1017+ tag = tag_container .subtags [kk ]
1018+ if not isinstance (tag , TagContainer ):
1019+ if _isnan (vv ):
1020+ print (f"Removing tag { kk } with nan value" )
1021+ subtags_to_delete .append (kk )
1022+ elif not tag .can_repeat :
1023+ if has_nan_in_required_subtag (tag , vv ) and tag_container .optional :
1024+ subtags_to_delete .append (kk )
1025+ else :
1026+ clean_tagcontainer_of_nans (tag , vv )
1027+ if len (tag .subtags ) == 0 :
1028+ print (f"Removing empty tag container { kk } " )
1029+ subtags_to_delete .append (kk )
1030+ clean_tagcontainer_of_nans (tag , vv )
1031+ for kk in subtags_to_delete :
1032+ val_dict .pop (kk )
1033+
1034+
1035+ def clean_infile_of_nans (infile : JDFTXInfile ) -> JDFTXInfile :
1036+ hasnans = []
1037+ for k , v in infile .items ():
1038+ tag = get_tag_object_on_val (k , v )
1039+ if isinstance (tag , FloatTag ) and _isnan (v ):
1040+ print (f"Tag { k } has nan value" )
1041+ elif isinstance (tag , TagContainer ) and not tag .can_repeat :
1042+ _hasnans = _check_tagcontainer_for_nan (tag , v )
1043+ if len (_hasnans ) > 0 :
1044+ hasnans .append ({k : _hasnans })
1045+ infile_cleaned = JDFTXInfile .from_dict (infile .as_dict ())
1046+
1047+ # infile_cleaned = JDFTXInfile(infile)
1048+
1049+ for h in hasnans :
1050+ if isinstance (h , str ):
1051+ infile_cleaned .pop (h )
1052+ elif isinstance (h , dict ):
1053+ for _h in h :
1054+ tag = get_tag_object_on_val (_h , infile_cleaned [_h ])
1055+ if _isnan (infile_cleaned [_h ]):
1056+ infile_cleaned .pop (_h )
1057+ else :
1058+ clean_tagcontainer_of_nans (tag , infile_cleaned [_h ])
1059+ return infile_cleaned
1060+
1061+
9151062@dataclass
9161063class JDFTXStructure (MSONable ):
9171064 """Object for representing the data in JDFTXStructure tags.
0 commit comments