Skip to content

Commit 35dd50b

Browse files
committed
Merge branch 'master' of github.com:materialsproject/pymatgen
2 parents 00a51fc + 4f536fc commit 35dd50b

File tree

6 files changed

+137
-13
lines changed

6 files changed

+137
-13
lines changed

src/pymatgen/io/jdftx/generic_tags.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ class FloatTag(AbstractTag):
430430
"""
431431

432432
prec: int | None = None
433+
minval: float | None = None
433434

434435
def validate_value_type(self, tag: str, value: Any, try_auto_type_fix: bool = False) -> tuple[str, bool, Any]:
435436
"""Validate the type of the value for this tag.
@@ -472,6 +473,11 @@ def write(self, tag: str, value: Any) -> str:
472473
Returns:
473474
str: The tag and its value as a string.
474475
"""
476+
# Returning an empty string instead of raising an error as value == self.minval
477+
# will cause JDFTx to throw an error, but the internal infile dumps the value as
478+
# as the minval if not set by the user.
479+
if (self.minval is not None) and (not value > self.minval):
480+
return ""
475481
# pre-convert to string: self.prec+3 is minimum room for:
476482
# - sign, 1 integer left of decimal, decimal, and precision.
477483
# larger numbers auto add places to left of decimal

src/pymatgen/io/jdftx/inputs.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ class is written.
4646

4747
# TODO: Add check for whether all ions have or lack velocities.
4848
# TODO: Add default value filling like JDFTx does.
49+
# TODO: Add more robust checking for if two repeatable tag values represent the
50+
# same information. This is likely fixed by implementing filling of default values.
51+
# TODO: Incorporate something to collapse repeated dump tags of the same frequency
52+
# into a single value.
4953

5054

5155
class JDFTXInfile(dict, MSONable):
@@ -80,18 +84,34 @@ def __add__(self, other: JDFTXInfile) -> JDFTXInfile:
8084
"""Add existing JDFTXInfile object to method caller JDFTXInfile object.
8185
8286
Add all the values of another JDFTXInfile object to this object. Facilitate the use of "standard" JDFTXInfiles.
87+
Repeatable tags are appended together. Non-repeatable tags are replaced with their value from the other object.
8388
8489
Args:
8590
other (JDFTXInfile): JDFTXInfile object to add to the method caller object.
8691
8792
Returns:
8893
JDFTXInfile: The combined JDFTXInfile object.
8994
"""
90-
params: dict[str, Any] = dict(self.items())
95+
# Deepcopy needed here, or else in `jif1 = jif2 + jif3`, `jif1` will become a reference to `jif2`
96+
params: dict[str, Any] = deepcopy(dict(self.items()))
9197
for key, val in other.items():
92-
if key in self and val != self[key]:
93-
raise ValueError(f"JDFTXInfiles have conflicting values for {key}: {self[key]} != {val}")
94-
params[key] = val
98+
if key in self:
99+
if val is params[key]:
100+
# Unlinking the two objects fully cannot be done by deepcopy for some reason
101+
continue
102+
tag_object = get_tag_object(key)
103+
if tag_object.can_repeat:
104+
if isinstance(val, list):
105+
for subval in list(val):
106+
# Minimum effort to avoid duplicates
107+
if subval not in params[key]:
108+
params[key].append(subval)
109+
else:
110+
params[key].append(val)
111+
else:
112+
params[key] = val
113+
else:
114+
params[key] = val
95115
return type(self)(params)
96116

97117
def as_dict(self, sort_tags: bool = True, skip_module_keys: bool = False) -> dict:
@@ -553,6 +573,17 @@ def validate_tags(
553573
warnmsg += "(Check earlier warnings for more details)\n"
554574
warnings.warn(warnmsg, stacklevel=2)
555575

576+
def strip_structure_tags(self) -> None:
577+
"""Strip all structural tags from the JDFTXInfile.
578+
579+
Strip all structural tags from the JDFTXInfile. This is useful for preparing a JDFTXInfile object
580+
from a pre-existing calculation for a new structure.
581+
"""
582+
strucural_tags = ["lattice", "ion", "lattice-scale", "coords-type"]
583+
for tag in strucural_tags:
584+
if tag in self:
585+
del self[tag]
586+
556587
def __setitem__(self, key: str, value: Any) -> None:
557588
"""Set an item in the JDFTXInfile.
558589

src/pymatgen/io/jdftx/jdftxinfile_ref_options.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -792,15 +792,15 @@
792792
"knormThreshold": FloatTag(),
793793
"linminMethod": StrTag(options=["CubicWolfe", "DirUpdateRecommended", "Quad", "Relax"]),
794794
"maxThreshold": BoolTag(),
795-
"nAlphaAdjustMax": FloatTag(),
795+
"nAlphaAdjustMax": IntTag(),
796796
"nEnergyDiff": IntTag(),
797797
"nIterations": IntTag(),
798798
"updateTestStepSize": BoolTag(),
799799
"wolfeEnergy": FloatTag(),
800800
"wolfeGradient": FloatTag(),
801801
}
802802
jdftxfluid_subtagdict = {
803-
"epsBulk": FloatTag(),
803+
"epsBulk": FloatTag(minval=1.0),
804804
"epsInf": FloatTag(),
805805
"epsLJ": FloatTag(),
806806
"Nnorm": FloatTag(),
@@ -814,12 +814,12 @@
814814
"A0": FloatTag(write_tagname=False, optional=False),
815815
},
816816
),
817-
"Pvap": FloatTag(),
817+
"Pvap": FloatTag(minval=0.0),
818818
"quad_nAlpha": FloatTag(),
819819
"quad_nBeta": FloatTag(),
820820
"quad_nGamma": FloatTag(),
821821
"representation": TagContainer(subtags={"MuEps": FloatTag(), "Pomega": FloatTag(), "PsiAlpha": FloatTag()}),
822-
"Res": FloatTag(),
822+
"Res": FloatTag(minval=0.0),
823823
"Rvdw": FloatTag(),
824824
"s2quadType": StrTag(
825825
options=[
@@ -844,7 +844,7 @@
844844
"Tetrahedron",
845845
]
846846
),
847-
"sigmaBulk": FloatTag(),
847+
"sigmaBulk": FloatTag(minval=0.0),
848848
"tauNuc": FloatTag(),
849849
"translation": StrTag(options=["ConstantSpline", "Fourier", "LinearSpline"]),
850850
}

src/pymatgen/io/jdftx/jdftxoutfileslice.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,10 @@ def _set_internal_infile(self, text: list[str]) -> None:
391391
Args:
392392
text (list[str]): Output of read_file for out file.
393393
"""
394-
start_line_idx = find_key("Input parsed successfully", text) + 2
394+
start_line_idx = find_key("Input parsed successfully", text)
395+
if start_line_idx is None:
396+
raise ValueError("JDFTx input parsing failed on most recent call.")
397+
start_line_idx += 2
395398
end_line_idx = None
396399
for i in range(start_line_idx, len(text)):
397400
if not len(text[i].strip()):
@@ -1157,6 +1160,24 @@ def determine_is_metal(self) -> bool | None:
11571160
raise ValueError("Cannot determine if system is metal - self.nspin undefined")
11581161
return None
11591162

1163+
def to_jdftxinfile(self) -> JDFTXInfile:
1164+
"""
1165+
Convert the JDFTXOutfile object to a JDFTXInfile object with the most recent structure.
1166+
If the input structure is desired, simply fetch JDFTXOutfile.infile
1167+
1168+
Returns:
1169+
JDFTXInfile: A JDFTXInfile object representing the input parameters of the JDFTXOutfile.
1170+
"""
1171+
# Use internal infile as a reference for calculation parameters
1172+
base_infile = self.infile.copy()
1173+
# Strip references to the input
1174+
base_infile.strip_structure_tags()
1175+
if self.structure is None:
1176+
return base_infile
1177+
infile = JDFTXInfile.from_structure(self.structure)
1178+
infile += base_infile
1179+
return infile
1180+
11601181
def _check_solvation(self) -> bool:
11611182
"""Check for implicit solvation.
11621183

src/pymatgen/io/jdftx/outputs.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class is written.
2727
if TYPE_CHECKING:
2828
from pymatgen.core.structure import Structure
2929
from pymatgen.core.trajectory import Trajectory
30+
from pymatgen.io.jdftx.inputs import JDFTXInfile
3031
from pymatgen.io.jdftx.jelstep import JElSteps
3132
from pymatgen.io.jdftx.jminsettings import (
3233
JMinSettingsElectronic,
@@ -287,8 +288,43 @@ def _store_eigenvals(self):
287288
"electronic_output",
288289
"t_s",
289290
"ecomponents",
291+
"infile",
290292
)
291293

294+
# TODO: Remove references to the deprecated 'jsettings_*' attributes in `JDFTXOutfile` and `JDFTXOutfileSlice`
295+
# TODO: JDFTXOutfile and JDFTXOutfileSlice are VERY bloated. The following attributes are redundant
296+
# to fields of the `Structure` object and their removal likely won't cause any confusion.
297+
# - lattice
298+
# - lattice_final
299+
# - atom_coords
300+
# - atom_coords_final
301+
# - a/b/c
302+
# The following attributes are redundant to the internal `JDFTXInfile` object, but since the JDFTXInfile
303+
# object is not a standard pymatgen object, it may be better to decorate them with a deprecated decorator
304+
# until end of year. Additionally, it should be made certain which of these are dumped in the outfile's
305+
# input section regardless of what was set in the input file - for those that are not, they need to be kept.
306+
# - lattice_initial
307+
# - atom_coords_initial
308+
# - broadening
309+
# - broadening_type
310+
# - kgrid
311+
# - truncation_type
312+
# - truncation_radius
313+
# - fluid
314+
# - pwcut
315+
# - rhocut
316+
# The following are attributes that come from the electronic/fluid/ionic/lattice optimization step
317+
# logs. I am not sure if it is actually helpful to have access to these from the `JDFTXOutfile` object,
318+
# as they are only informative for analyzing optimization convergence. Additionally, the fields are
319+
# less universal than I thought, and can change depending on the optimization settings, so it might
320+
# be smarter to store them in a dictionary of arbitrary keys instead within the contained substructures.
321+
# - grad_k
322+
# - alpha
323+
# - linmin
324+
# - elec_grad_k
325+
# - elec_alpha
326+
# - elec_linmin
327+
292328

293329
@dataclass
294330
class JDFTXOutfile:
@@ -420,6 +456,7 @@ class JDFTXOutfile:
420456
elec_alpha (float): The step size of the final electronic step in the most recent JDFTx call.
421457
elec_linmin (float): The final normalized projection of the electronic step direction onto the gradient for the
422458
most recent JDFTx call.
459+
infile (JDFTXInfile): The JDFTXInfile object representing the input parameters of the JDFTXOutfile.
423460
424461
Magic Methods:
425462
__getitem__(key: str | int) -> Any: Decides behavior of how JDFTXOutfile objects are indexed. If the key is a
@@ -506,6 +543,7 @@ class JDFTXOutfile:
506543
elec_alpha: float = field(init=False)
507544
elec_linmin: float = field(init=False)
508545
electronic_output: float = field(init=False)
546+
infile: JDFTXInfile = field(init=False)
509547

510548
@classmethod
511549
def from_calc_dir(
@@ -558,6 +596,17 @@ def from_file(
558596
]
559597
return cls(slices=slices)
560598

599+
# TODO: Write testing for this function
600+
def to_jdftxinfile(self) -> JDFTXInfile:
601+
"""
602+
Convert the JDFTXOutfile object to a JDFTXInfile object with the most recent structure.
603+
If the input structure is desired, simply fetch JDFTXOutfile.infile
604+
605+
Returns:
606+
JDFTXInfile: A JDFTXInfile object representing the input parameters of the JDFTXOutfile.
607+
"""
608+
return self.slices[-1].to_jdftxinfile()
609+
561610
def __post_init__(self):
562611
last_slice = None
563612
for slc in self.slices[::-1]:

tests/io/jdftx/test_jdftxinfile.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ def test_JDFTXInfile_expected_exceptions():
186186
jif[1.2] = 3.4
187187

188188

189+
def test_JDFTXInfile_strip_structure():
190+
jif = JDFTXInfile.from_file(ex_infile1_fname)
191+
structural_tags = ["lattice", "ion", "coords-type"]
192+
assert all(tag in jif for tag in structural_tags)
193+
jif.strip_structure_tags()
194+
assert all(tag not in jif for tag in structural_tags)
195+
196+
189197
def test_JDFTXInfile_niche_cases():
190198
jif = JDFTXInfile.from_file(ex_infile1_fname)
191199
tag_object, tag, value = jif._preprocess_line("dump-only")
@@ -214,6 +222,7 @@ def test_JDFTXInfile_add_method():
214222
# repeatable values, repeatable tags are not append to each other
215223
jif = JDFTXInfile.from_file(ex_infile1_fname)
216224
jif2 = jif.copy()
225+
assert jif2 is not jif # Testing robustness of copy method while we are at it
217226
jif3 = jif + jif2
218227
assert_idential_jif(jif, jif3)
219228
# If a tag is repeated, the values must be the same since choice of value is ambiguous
@@ -222,9 +231,17 @@ def test_JDFTXInfile_add_method():
222231
val_new = "lda"
223232
assert val_old != val_new
224233
jif2[key] = val_new
225-
err_str = f"JDFTXInfiles have conflicting values for {key}: {val_old} != {val_new}"
226-
with pytest.raises(ValueError, match=re.escape(err_str)):
227-
jif3 = jif + jif2
234+
jif4 = jif + jif2
235+
assert_same_value(jif4[key], val_new) # Make sure addition chooses second value for non-repeatable tags
236+
del jif4
237+
jif2.append_tag("dump", "Fluid State")
238+
jif.append_tag("dump", "Fluid Berry")
239+
jif4 = jif + jif2
240+
assert len(jif4["dump"]) == len(jif2["dump"]) + 1
241+
assert {"Fluid": {"State": True}} in jif4["dump"]
242+
assert {"Fluid": {"State": True}} not in jif["dump"]
243+
assert {"Fluid": {"Berry": True}} in jif4["dump"]
244+
assert {"Fluid": {"Berry": True}} not in jif2["dump"]
228245
# Normal expected behavior
229246
key_add = "target-mu"
230247
val_add = 0.5

0 commit comments

Comments
 (0)