11import json
22import os
3+ from collections import Counter
34from enum import Enum
45
6+ import click
57import numpy as np
6-
7- try :
8- import yaml
9- except ImportError :
10- yaml = None
8+ import yaml
119
1210from vasp_snake .force import parse_forces_and_check_zero
1311
@@ -21,20 +19,20 @@ class JobStatus(Enum):
2119
2220
2321def classify_folders (root = "." , atol = 1e-6 ):
24- status = {}
22+ details = {}
2523 for folder in sorted (os .listdir (root )):
2624 folder_path = os .path .join (root , folder )
2725 if not os .path .isdir (folder_path ) or folder .startswith ("." ):
2826 continue
2927 outcar = os .path .join (folder_path , "OUTCAR" )
3028 if not os .path .exists (outcar ):
31- forces_sum = [np .nan ] * 3
29+ forces_sum = [np .nan , np . nan , np . nan ]
3230 job_status = JobStatus .PENDING
3331 reason = "OUTCAR missing"
3432 else :
3533 forces_sum , is_converged = parse_forces_and_check_zero (outcar , atol = atol )
3634 if forces_sum is None :
37- forces_sum = [np .nan ] * 3
35+ forces_sum = [np .nan , np . nan , np . nan ]
3836 job_status = JobStatus .NOT_CONVERGED
3937 reason = "No force block found"
4038 elif is_converged :
@@ -49,52 +47,71 @@ def classify_folders(root=".", atol=1e-6):
4947 float (f )
5048 for f in (forces_sum if forces_sum is not None else [np .nan ] * 3 )
5149 ]
52- status [folder ] = {
50+ details [folder ] = {
5351 "status" : job_status .value ,
5452 "forces_sum" : forces_sum ,
5553 "reason" : reason ,
5654 }
57- return status
55+ # Compute summary percentages
56+ status_list = [v ["status" ] for v in details .values ()]
57+ total = len (status_list )
58+ counter = Counter (status_list )
59+ summary = {
60+ status : counter .get (status , 0 ) / total if total else 0.0
61+ for status in [s .value for s in JobStatus ]
62+ }
63+ return {"summary" : summary , "details" : details }
64+
5865
66+ def write_status_report (status_dict , filename ):
67+ # Numpy floats/nans don't serialize well with json/yaml, so convert
68+ def convert (obj ):
69+ if isinstance (obj , float ) and np .isnan (obj ):
70+ return None # Or "NaN" if you want the string
71+ if isinstance (obj , (np .generic , np .ndarray )):
72+ return obj .tolist ()
73+ return obj
74+
75+ # Recursively convert NaN in all nested structures
76+ def recursive_convert (o ):
77+ if isinstance (o , dict ):
78+ return {k : recursive_convert (v ) for k , v in o .items ()}
79+ elif isinstance (o , list ):
80+ return [recursive_convert (x ) for x in o ]
81+ else :
82+ return convert (o )
5983
60- def write_status_report (status_dict , filename , out_format = "json" ):
61- if out_format == "json" :
84+ output = recursive_convert (status_dict )
85+ ext = os .path .splitext (filename )[- 1 ].lower ()
86+ if ext in [".json" ]:
6287 with open (filename , "w" ) as f :
63- json .dump (status_dict , f , indent = 2 )
64- elif out_format == "yaml" :
65- if yaml is None :
66- raise ImportError ("pyyaml is required for YAML output." )
88+ json .dump (output , f , indent = 2 )
89+ elif ext in [".yaml" , ".yml" ]:
6790 with open (filename , "w" ) as f :
68- yaml .dump (status_dict , f , sort_keys = False )
91+ yaml .dump (output , f , sort_keys = False )
6992 else :
70- raise ValueError ("Unsupported format : {}" . format ( out_format ) )
93+ raise ValueError (f"Unknown file extension : { ext } " )
7194
7295
73- # Optional: CLI interface
74- if __name__ == "__main__" :
75- import click
96+ @click .command ()
97+ @click .option (
98+ "--output" ,
99+ default = "vasp_status.json" ,
100+ help = "Output file name (default: vasp_status.json)" ,
101+ )
102+ @click .option ("--folders" , default = "." , help = "Root directory containing folders" )
103+ @click .option (
104+ "--atol" ,
105+ default = 1e-6 ,
106+ type = float ,
107+ show_default = True ,
108+ help = "Convergence tolerance for force sum norm" ,
109+ )
110+ def main (output , folders , atol ):
111+ status = classify_folders (folders , atol = atol )
112+ write_status_report (status , output )
76113
77- @click .command ()
78- @click .option (
79- "--format" ,
80- "out_format" ,
81- type = click .Choice (["json" , "yaml" ], case_sensitive = False ),
82- default = "json" ,
83- help = "Output format (json or yaml)" ,
84- )
85- @click .option ("--output" , default = None , help = "Output file name (optional)" )
86- @click .option ("--folders" , default = "." , help = "Root directory containing folders" )
87- @click .option (
88- "--atol" ,
89- default = 1e-6 ,
90- type = float ,
91- show_default = True ,
92- help = "Convergence tolerance for force sum norm" ,
93- )
94- def main (out_format , output , folders , atol ):
95- status = classify_folders (folders , atol = atol )
96- if output is None :
97- output = f"vasp_status.{ out_format } "
98- write_status_report (status , output , out_format )
99114
115+ # Optional: CLI interface
116+ if __name__ == "__main__" :
100117 main ()
0 commit comments