Coverage for sparc/api.py: 86%
143 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-12 01:13 +0000
« 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
6import numpy as np
8curdir = Path(__file__).parent
9default_api_dir = curdir / "sparc_json_api"
10default_json_api = default_api_dir / "parameters.json"
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.
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.
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 """
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)
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"}
52 def get_parameter_dict(self, parameter):
53 """
54 Retrieves the dictionary for a specified parameter.
56 Args:
57 parameter (str): The name of the parameter.
59 Returns:
60 dict: Dictionary containing details of the parameter.
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]
72 def help_info(self, parameter):
73 """Provides a detailed information string for a given parameter.
75 Args:
76 parameter (str): The name of the parameter to get information for.
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
100 def validate_input(self, parameter, input):
101 """
102 Validates if the given input is appropriate for the specified parameter's type.
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).
108 Returns:
109 bool: True if input is valid, False otherwise.
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!")
181 def convert_string_to_value(self, parameter, string):
182 """
183 Converts a string input to the appropriate value type of the parameter.
185 Args:
186 parameter (str): The name of the parameter.
187 string (str): The string input to convert.
189 Returns:
190 The converted value, type depends on parameter's expected type.
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 """
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)
204 is_input_string = isinstance(string, str)
205 if not is_input_string:
206 raise TypeError("Please give a string input!")
208 if not self.validate_input(parameter, string):
209 raise ValueError(f"{string} is not a valid input for {parameter}")
211 pdict = self.get_parameter_dict(parameter)
212 dtype = pdict["type"]
213 allow_bool_input = pdict.get("allow_bool_input", False)
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}")
243 return value
245 def convert_value_to_string(self, parameter, value):
246 """
247 Converts a value to its string representation based on the parameter type.
249 Args:
250 parameter (str): The name of the parameter.
251 value: The value to convert.
253 Returns:
254 str: The string representation of the value.
256 Raises:
257 ValueError: If the value is not valid for the parameter.
258 """
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}")
264 # Do not conver, just return the non-padded string
265 if is_input_string:
266 return value.strip()
268 pdict = self.get_parameter_dict(parameter)
269 dtype = pdict["type"]
270 # allow_bool_input = pdict.get("allow_bool_input", False)
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}")
289 return string
292def _array_to_string(arr, format):
293 """
294 Converts an array to a string representation based on the specified format.
296 Args:
297 arr (array): The array to convert.
298 format (str): The format type ('integer array', 'double array', etc.).
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()