55from functools import cached_property
66import hashlib
77from importlib import import_module
8+ import json
89from monty .serialization import loadfn
910import os
10- import numpy as np
1111from pathlib import Path
12- from pydantic import BaseModel , Field , model_validator , model_serializer , PrivateAttr
13- from typing import TYPE_CHECKING , Any , Optional
12+ from pydantic import BaseModel , Field , model_validator , model_serializer , PrivateAttr , PlainSerializer , BeforeValidator
13+ from typing import TYPE_CHECKING , Any , Annotated , TypeAlias
1414
1515from pymatgen .core import Structure
1616from pymatgen .io .vasp .inputs import POTCAR_STATS_PATH , Incar , Kpoints , Poscar , Potcar , PmgVaspPspDirError
2222
2323if TYPE_CHECKING :
2424 from typing_extensions import Self
25+ from monty .json import MSONable
2526
2627SETTINGS = IOValidationSettings ()
2728
2829
30+ def _msonable_from_str (obj : Any , cls : type [MSONable ]) -> MSONable :
31+ if isinstance (obj , str ):
32+ obj = json .loads (obj )
33+ if isinstance (obj , dict ):
34+ return cls .from_dict (obj )
35+ return obj
36+
37+
38+ IncarType : TypeAlias = Annotated [
39+ Incar ,
40+ BeforeValidator (lambda x : _msonable_from_str (x , Incar )),
41+ PlainSerializer (lambda x : json .dumps (x .as_dict ()), return_type = str ),
42+ ]
43+
44+ KpointsType : TypeAlias = Annotated [
45+ Kpoints ,
46+ BeforeValidator (lambda x : _msonable_from_str (x , Kpoints )),
47+ PlainSerializer (lambda x : json .dumps (x .as_dict ()), return_type = str ),
48+ ]
49+
50+ StructureType : TypeAlias = Annotated [
51+ Structure ,
52+ BeforeValidator (lambda x : _msonable_from_str (x , Structure )),
53+ PlainSerializer (lambda x : json .dumps (x .as_dict ()), return_type = str ),
54+ ]
55+
56+
2957class ValidationError (Exception ):
3058 """Define custom exception during validation."""
3159
@@ -62,8 +90,8 @@ class PotcarSummaryStatistics(BaseModel):
6290class PotcarSummaryStats (BaseModel ):
6391 """Schematize `PotcarSingle._summary_stats`."""
6492
65- keywords : Optional [ PotcarSummaryKeywords ] = None
66- stats : Optional [ PotcarSummaryStatistics ] = None
93+ keywords : PotcarSummaryKeywords | None = None
94+ stats : PotcarSummaryStatistics | None = None
6795 titel : str
6896 lexch : str
6997
@@ -80,23 +108,39 @@ def from_file(cls, potcar_path: os.PathLike | Potcar) -> list[Self]:
80108class LightOutcar (BaseModel ):
81109 """Schematic of pymatgen's Outcar."""
82110
83- drift : Optional [ list [list [float ]]] = Field (None , description = "The drift forces." )
84- magnetization : Optional [ list [dict [str , float ]]] = Field (
111+ drift : list [list [float ]] | None = Field (None , description = "The drift forces." )
112+ magnetization : list [dict [str , float ]] | None = Field (
85113 None , description = "The on-site magnetic moments, possibly with orbital resolution."
86114 )
87115
88116
117+ class LightElectronicStep (BaseModel ):
118+
119+ e_0_energy : float | None = None
120+ e_fr_energy : float | None = None
121+ e_wo_entrp : float | None = None
122+ eentropy : float | None = None
123+
124+
125+ class LightIonicStep (BaseModel ):
126+
127+ e_0_energy : float | None = None
128+ e_fr_energy : float | None = None
129+ forces : list [list [float ]] | None = None
130+ electronic_steps : list [LightElectronicStep ] | None = None
131+
132+
89133class LightVasprun (BaseModel ):
90134 """Lightweight version of pymatgen Vasprun."""
91135
92136 vasp_version : str = Field (description = "The dot-separated version of VASP used." )
93- ionic_steps : list [dict [str , Any ]] = Field (description = "The ionic steps in the calculation." )
94137 final_energy : float = Field (description = "The final total energy in eV." )
95- final_structure : Structure = Field (description = "The final structure." )
96- kpoints : Kpoints = Field (description = "The actual k-points used in the calculation." )
97- parameters : dict [ str , Any ] = Field (description = "The default-padded input parameters interpreted by VASP." )
138+ final_structure : StructureType = Field (description = "The final structure." )
139+ kpoints : KpointsType = Field (description = "The actual k-points used in the calculation." )
140+ parameters : IncarType = Field (description = "The default-padded input parameters interpreted by VASP." )
98141 bandgap : float = Field (description = "The bandgap - note that this field is derived from the Vasprun object." )
99- potcar_symbols : Optional [list [str ]] = Field (
142+ ionic_steps : list [LightIonicStep ] = Field ([], description = "The ionic steps in the calculation." )
143+ potcar_symbols : list [str ] | None = Field (
100144 None ,
101145 description = "Optional: if a POTCAR is unavailable, this is used to determine the functional used in the calculation." ,
102146 )
@@ -119,45 +163,18 @@ def from_vasprun(cls, vasprun: Vasprun) -> Self:
119163 bandgap = vasprun .get_band_structure (efermi = "smart" ).get_band_gap ()["energy" ],
120164 )
121165
122- @model_serializer
123- def deserialize_objects (self ) -> dict [str , Any ]:
124- """Ensure all pymatgen objects are deserialized."""
125- model_dumped = {k : getattr (self , k ) for k in self .__class__ .model_fields }
126- for k in ("final_structure" , "kpoints" ):
127- model_dumped [k ] = model_dumped [k ].as_dict ()
128- for iion , istep in enumerate (model_dumped ["ionic_steps" ]):
129- if (istruct := istep .get ("structure" )) and isinstance (istruct , Structure ):
130- model_dumped ["ionic_steps" ][iion ]["structure" ] = istruct .as_dict ()
131- for k in ("forces" , "stress" ):
132- if (val := istep .get (k )) is not None and isinstance (val , np .ndarray ):
133- model_dumped ["ionic_steps" ][iion ][k ] = val .tolist ()
134- return model_dumped
135-
136166
137167class VaspInputSafe (BaseModel ):
138168 """Stricter VaspInputSet with no POTCAR info."""
139169
140- incar : Incar = Field (description = "The INCAR used in the calculation." )
141- structure : Structure = Field (description = "The structure associated with the calculation." )
142- kpoints : Optional [Kpoints ] = Field (None , description = "The optional KPOINTS or IBZKPT file used in the calculation." )
143- potcar : Optional [list [PotcarSummaryStats ]] = Field (None , description = "The optional POTCAR used in the calculation." )
144- potcar_functional : Optional [str ] = Field (None , description = "The pymatgen-labelled POTCAR library release." )
145- _pmg_vis : Optional [VaspInputSet ] = PrivateAttr (None )
146-
147- @model_serializer
148- def deserialize_objects (self ) -> dict [str , Any ]:
149- """Ensure all pymatgen objects are deserialized."""
150- model_dumped : dict [str , Any ] = {}
151- if self .potcar :
152- model_dumped ["potcar" ] = [p .model_dump () for p in self .potcar ]
153- for k in (
154- "incar" ,
155- "structure" ,
156- "kpoints" ,
157- ):
158- if pmg_obj := getattr (self , k ):
159- model_dumped [k ] = pmg_obj .as_dict ()
160- return model_dumped
170+ incar : IncarType = Field (description = "The INCAR used in the calculation." )
171+ structure : StructureType = Field (description = "The structure associated with the calculation." )
172+ kpoints : KpointsType | None = Field (
173+ None , description = "The optional KPOINTS or IBZKPT file used in the calculation."
174+ )
175+ potcar : list [PotcarSummaryStats ] | None = Field (None , description = "The optional POTCAR used in the calculation." )
176+ potcar_functional : str | None = Field (None , description = "The pymatgen-labelled POTCAR library release." )
177+ _pmg_vis : VaspInputSet | None = PrivateAttr (None )
161178
162179 @classmethod
163180 def from_vasp_input_set (cls , vis : VaspInputSet ) -> Self :
0 commit comments