5
5
from functools import cached_property
6
6
import hashlib
7
7
from importlib import import_module
8
+ import json
8
9
from monty .serialization import loadfn
9
10
import os
10
- import numpy as np
11
11
from 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
14
14
15
15
from pymatgen .core import Structure
16
16
from pymatgen .io .vasp .inputs import POTCAR_STATS_PATH , Incar , Kpoints , Poscar , Potcar , PmgVaspPspDirError
22
22
23
23
if TYPE_CHECKING :
24
24
from typing_extensions import Self
25
+ from monty .json import MSONable
25
26
26
27
SETTINGS = IOValidationSettings ()
27
28
28
29
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
+
29
57
class ValidationError (Exception ):
30
58
"""Define custom exception during validation."""
31
59
@@ -62,8 +90,8 @@ class PotcarSummaryStatistics(BaseModel):
62
90
class PotcarSummaryStats (BaseModel ):
63
91
"""Schematize `PotcarSingle._summary_stats`."""
64
92
65
- keywords : Optional [ PotcarSummaryKeywords ] = None
66
- stats : Optional [ PotcarSummaryStatistics ] = None
93
+ keywords : PotcarSummaryKeywords | None = None
94
+ stats : PotcarSummaryStatistics | None = None
67
95
titel : str
68
96
lexch : str
69
97
@@ -80,23 +108,39 @@ def from_file(cls, potcar_path: os.PathLike | Potcar) -> list[Self]:
80
108
class LightOutcar (BaseModel ):
81
109
"""Schematic of pymatgen's Outcar."""
82
110
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 (
85
113
None , description = "The on-site magnetic moments, possibly with orbital resolution."
86
114
)
87
115
88
116
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
+
89
133
class LightVasprun (BaseModel ):
90
134
"""Lightweight version of pymatgen Vasprun."""
91
135
92
136
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." )
94
137
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." )
98
141
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 (
100
144
None ,
101
145
description = "Optional: if a POTCAR is unavailable, this is used to determine the functional used in the calculation." ,
102
146
)
@@ -119,45 +163,18 @@ def from_vasprun(cls, vasprun: Vasprun) -> Self:
119
163
bandgap = vasprun .get_band_structure (efermi = "smart" ).get_band_gap ()["energy" ],
120
164
)
121
165
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
-
136
166
137
167
class VaspInputSafe (BaseModel ):
138
168
"""Stricter VaspInputSet with no POTCAR info."""
139
169
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 )
161
178
162
179
@classmethod
163
180
def from_vasp_input_set (cls , vis : VaspInputSet ) -> Self :
0 commit comments