Coverage for sparc/api.py: 86%

143 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-12 01:13 +0000

1import json 

2from io import StringIO 

3from pathlib import Path 

4from warnings import warn 

5 

6import numpy as np 

7 

8curdir = Path(__file__).parent 

9default_api_dir = curdir / "sparc_json_api" 

10default_json_api = default_api_dir / "parameters.json" 

11 

12 

13class SparcAPI: 

14 """ 

15 An interface to the parameter settings in SPARC-X calculator. User can use the 

16 SparcAPI instance to validate and translate parameters that matches a certain 

17 version of the SPARC-X code. 

18 

19 Attributes: 

20 sparc_version (str): Version of SPARC. 

21 categories (dict): Categories of parameters. 

22 parameters (dict): Detailed parameters information. 

23 other_parameters (dict): Additional parameters. 

24 data_types (dict): Supported data types. 

25 

26 Methods: 

27 get_parameter_dict(parameter): Retrieves dictionary for a specific parameter. 

28 help_info(parameter): Provides detailed information about a parameter. 

29 validate_input(parameter, input): Validates user input against the expected parameter type. 

30 convert_string_to_value(parameter, string): Converts string input to the appropriate data type. 

31 convert_value_to_string(parameter, value): Converts a value to a string representation. 

32 """ 

33 

34 def __init__(self, json_api=None): 

35 """ """ 

36 if json_api is None: 

37 json_api = Path(default_json_api) 

38 else: 

39 json_api = Path(json_api) 

40 

41 json_data = json.load(open(json_api, "r")) 

42 self.sparc_version = json_data["sparc_version"] 

43 self.categories = json_data["categories"] 

44 self.parameters = json_data["parameters"] 

45 self.other_parameters = json_data["other_parameters"] 

46 self.data_types = json_data["data_types"] 

47 # TT: 2024-10-31 add the sources to trace the origin 

48 # locate_api can modify self.source if it is deferred from LaTeX 

49 # at runtime 

50 self.source = {"path": json_api.as_posix(), "type": "json"} 

51 

52 def get_parameter_dict(self, parameter): 

53 """ 

54 Retrieves the dictionary for a specified parameter. 

55 

56 Args: 

57 parameter (str): The name of the parameter. 

58 

59 Returns: 

60 dict: Dictionary containing details of the parameter. 

61 

62 Raises: 

63 KeyError: If the parameter is not known to the SPARC version. 

64 """ 

65 parameter = parameter.upper() 

66 if parameter not in self.parameters.keys(): 

67 raise KeyError( 

68 f"Parameter {parameter} is not known to " f"SPARC {self.sparc_version}!" 

69 ) 

70 return self.parameters[parameter] 

71 

72 def help_info(self, parameter): 

73 """Provides a detailed information string for a given parameter. 

74 

75 Args: 

76 parameter (str): The name of the parameter to get information for. 

77 

78 Returns: 

79 str: A formatted string with detailed information about the parameter. 

80 """ 

81 pdict = self.get_parameter_dict(parameter) 

82 message = "\n".join( 

83 [ 

84 f"{key}: {pdict[key]}" 

85 for key in ( 

86 "symbol", 

87 "category", 

88 "type", 

89 "unit", 

90 "default", 

91 "example", 

92 "description", 

93 "remark", 

94 "allow_bool_input", 

95 ) 

96 ] 

97 ) 

98 return message 

99 

100 def validate_input(self, parameter, input): 

101 """ 

102 Validates if the given input is appropriate for the specified parameter's type. 

103 

104 Args: 

105 parameter (str): The name of the parameter. 

106 input: The input to validate, can be of various types (string, int, float, numpy types). 

107 

108 Returns: 

109 bool: True if input is valid, False otherwise. 

110 

111 Raises: 

112 ValueError: If the data type of the parameter is not supported. 

113 """ 

114 is_input_string = isinstance(input, str) 

115 pdict = self.get_parameter_dict(parameter) 

116 dtype = pdict["type"] 

117 if dtype == "string": 

118 return is_input_string 

119 elif dtype == "other": 

120 # Do nother for the "other" types but simply 

121 # reply on the str() method 

122 if not is_input_string: 

123 warn( 

124 f"Parameter {parameter} has 'other' data type " 

125 "and your input is not a string. " 

126 "I hope you know what you're doing!" 

127 ) 

128 return True 

129 elif dtype == "integer": 

130 try: 

131 int(input) 

132 return True 

133 except (TypeError, ValueError): 

134 return False 

135 elif dtype == "double": 

136 try: 

137 float(input) 

138 return True 

139 except (TypeError, ValueError): 

140 try: 

141 float(input.split()[0]) 

142 return True 

143 except Exception: 

144 return False 

145 elif "array" in dtype: 

146 if is_input_string: 

147 if ("." in input) and ("integer" in dtype): 

148 warn( 

149 ( 

150 f"Input {input} for parameter " 

151 f"{parameter} it not strictly integer. " 

152 "I may still perform the conversion " 

153 "but be aware of data loss" 

154 ) 

155 ) 

156 try: 

157 arr = np.genfromtxt(input.splitlines(), dtype=float, ndmin=1) 

158 # In valid input with nan 

159 if np.isnan(arr).any(): 

160 arr = np.array(0.0) 

161 except Exception: 

162 arr = np.array(0.0) 

163 else: 

164 try: 

165 arr = np.atleast_1d(np.asarray(input)) 

166 if (arr.dtype not in (int, bool)) and ("integer" in dtype): 

167 warn( 

168 ( 

169 f"Input {input} for parameter {parameter} is" 

170 " not strictly integer. " 

171 "I can still perform the conversion but " 

172 "be aware of data loss" 

173 ) 

174 ) 

175 except Exception: 

176 arr = np.array(0.0) 

177 return len(arr.shape) > 0 

178 else: 

179 raise ValueError(f"Data type {dtype} is not supported!") 

180 

181 def convert_string_to_value(self, parameter, string): 

182 """ 

183 Converts a string input to the appropriate value type of the parameter. 

184 

185 Args: 

186 parameter (str): The name of the parameter. 

187 string (str): The string input to convert. 

188 

189 Returns: 

190 The converted value, type depends on parameter's expected type. 

191 

192 Raises: 

193 TypeError: If the input is not a string. 

194 ValueError: If the string is not a valid input for the parameter. 

195 """ 

196 

197 # Special case, the string may be a multiline string-array! 

198 if isinstance(string, list): 

199 # Make sure there is a line break at the end, for cases like ["2."] 

200 string.append("") 

201 string = [s.strip() for s in string] 

202 string = "\n".join(string) 

203 

204 is_input_string = isinstance(string, str) 

205 if not is_input_string: 

206 raise TypeError("Please give a string input!") 

207 

208 if not self.validate_input(parameter, string): 

209 raise ValueError(f"{string} is not a valid input for {parameter}") 

210 

211 pdict = self.get_parameter_dict(parameter) 

212 dtype = pdict["type"] 

213 allow_bool_input = pdict.get("allow_bool_input", False) 

214 

215 if dtype == "string": 

216 value = string.strip() 

217 elif dtype == "integer": 

218 value = int(string) 

219 if allow_bool_input: 

220 value = bool(value) 

221 elif dtype == "double": 

222 # Some inputs, like TARGET_PRESSURE, may be accepted with a unit 

223 # like 0.0 GPa. Only accept the first part 

224 try: 

225 value = float(string) 

226 except ValueError as e: 

227 try: 

228 value = float(string.split()[0]) 

229 except Exception: 

230 raise e 

231 elif dtype == "integer array": 

232 value = np.genfromtxt(string.splitlines(), dtype=int, ndmin=1) 

233 if allow_bool_input: 

234 value = value.astype(bool) 

235 elif dtype == "double array": 

236 value = np.genfromtxt(string.splitlines(), dtype=float, ndmin=1) 

237 elif dtype == "other": 

238 value = string 

239 # should not happen since validate_input has gatekeeping 

240 else: 

241 raise ValueError(f"Unsupported type {dtype}") 

242 

243 return value 

244 

245 def convert_value_to_string(self, parameter, value): 

246 """ 

247 Converts a value to its string representation based on the parameter type. 

248 

249 Args: 

250 parameter (str): The name of the parameter. 

251 value: The value to convert. 

252 

253 Returns: 

254 str: The string representation of the value. 

255 

256 Raises: 

257 ValueError: If the value is not valid for the parameter. 

258 """ 

259 

260 is_input_string = isinstance(value, str) 

261 if not self.validate_input(parameter, value): 

262 raise ValueError(f"{value} is not a valid input for {parameter}") 

263 

264 # Do not conver, just return the non-padded string 

265 if is_input_string: 

266 return value.strip() 

267 

268 pdict = self.get_parameter_dict(parameter) 

269 dtype = pdict["type"] 

270 # allow_bool_input = pdict.get("allow_bool_input", False) 

271 

272 if dtype == "string": 

273 string = str(value).strip() 

274 elif dtype == "integer": 

275 # Be aware of bool values! 

276 string = str(int(value)) 

277 elif dtype == "double": 

278 string = "{:.14f}".format(float(value)) 

279 elif dtype in ("integer array", "double array"): 

280 string = _array_to_string(value, dtype) 

281 elif dtype == "other": 

282 if not is_input_string: 

283 raise ValueError("Only support string value when datatype is other") 

284 string = value 

285 else: 

286 # should not happen since validate_input has gatekeeping 

287 raise ValueError(f"Unsupported type {dtype}") 

288 

289 return string 

290 

291 

292def _array_to_string(arr, format): 

293 """ 

294 Converts an array to a string representation based on the specified format. 

295 

296 Args: 

297 arr (array): The array to convert. 

298 format (str): The format type ('integer array', 'double array', etc.). 

299 

300 Returns: 

301 str: String representation of the array. 

302 """ 

303 arr = np.array(arr) 

304 if arr.ndim == 1: 

305 arr = arr.reshape(1, -1) 

306 buf = StringIO() 

307 if format in ("integer array", "integer"): 

308 fmt = "%d" 

309 elif format in ("double array", "double"): 

310 fmt = "%.14f" 

311 np.savetxt(buf, arr, delimiter=" ", fmt=fmt, header="", footer="", newline="\n") 

312 # Return the string output of the buffer with 

313 # whitespaces removed 

314 return buf.getvalue().strip()