import json
from io import StringIO
from pathlib import Path
from warnings import warn
import numpy as np
curdir = Path(__file__).parent
default_api_dir = curdir / "sparc_json_api"
default_json_api = default_api_dir / "parameters.json"
[docs]
class SparcAPI:
"""
An interface to the parameter settings in SPARC-X calculator. User can use the
SparcAPI instance to validate and translate parameters that matches a certain
version of the SPARC-X code.
Attributes:
sparc_version (str): Version of SPARC.
categories (dict): Categories of parameters.
parameters (dict): Detailed parameters information.
other_parameters (dict): Additional parameters.
data_types (dict): Supported data types.
Methods:
get_parameter_dict(parameter): Retrieves dictionary for a specific parameter.
help_info(parameter): Provides detailed information about a parameter.
validate_input(parameter, input): Validates user input against the expected parameter type.
convert_string_to_value(parameter, string): Converts string input to the appropriate data type.
convert_value_to_string(parameter, value): Converts a value to a string representation.
"""
def __init__(self, json_api=None):
""" """
if json_api is None:
json_api = Path(default_json_api)
else:
json_api = Path(json_api)
json_data = json.load(open(json_api, "r"))
self.sparc_version = json_data["sparc_version"]
self.categories = json_data["categories"]
self.parameters = json_data["parameters"]
self.other_parameters = json_data["other_parameters"]
self.data_types = json_data["data_types"]
# TT: 2024-10-31 add the sources to trace the origin
# locate_api can modify self.source if it is deferred from LaTeX
# at runtime
self.source = {"path": json_api.as_posix(), "type": "json"}
[docs]
def get_parameter_dict(self, parameter):
"""
Retrieves the dictionary for a specified parameter.
Args:
parameter (str): The name of the parameter.
Returns:
dict: Dictionary containing details of the parameter.
Raises:
KeyError: If the parameter is not known to the SPARC version.
"""
parameter = parameter.upper()
if parameter not in self.parameters.keys():
raise KeyError(
f"Parameter {parameter} is not known to " f"SPARC {self.sparc_version}!"
)
return self.parameters[parameter]
[docs]
def help_info(self, parameter):
"""Provides a detailed information string for a given parameter.
Args:
parameter (str): The name of the parameter to get information for.
Returns:
str: A formatted string with detailed information about the parameter.
"""
pdict = self.get_parameter_dict(parameter)
message = "\n".join(
[
f"{key}: {pdict[key]}"
for key in (
"symbol",
"category",
"type",
"unit",
"default",
"example",
"description",
"remark",
"allow_bool_input",
)
]
)
return message
[docs]
def convert_string_to_value(self, parameter, string):
"""
Converts a string input to the appropriate value type of the parameter.
Args:
parameter (str): The name of the parameter.
string (str): The string input to convert.
Returns:
The converted value, type depends on parameter's expected type.
Raises:
TypeError: If the input is not a string.
ValueError: If the string is not a valid input for the parameter.
"""
# Special case, the string may be a multiline string-array!
if isinstance(string, list):
# Make sure there is a line break at the end, for cases like ["2."]
string.append("")
string = [s.strip() for s in string]
string = "\n".join(string)
is_input_string = isinstance(string, str)
if not is_input_string:
raise TypeError("Please give a string input!")
if not self.validate_input(parameter, string):
raise ValueError(f"{string} is not a valid input for {parameter}")
pdict = self.get_parameter_dict(parameter)
dtype = pdict["type"]
allow_bool_input = pdict.get("allow_bool_input", False)
if dtype == "string":
value = string.strip()
elif dtype == "integer":
value = int(string)
if allow_bool_input:
value = bool(value)
elif dtype == "double":
# Some inputs, like TARGET_PRESSURE, may be accepted with a unit
# like 0.0 GPa. Only accept the first part
try:
value = float(string)
except ValueError as e:
try:
value = float(string.split()[0])
except Exception:
raise e
elif dtype == "integer array":
value = np.genfromtxt(string.splitlines(), dtype=int, ndmin=1)
if allow_bool_input:
value = value.astype(bool)
elif dtype == "double array":
value = np.genfromtxt(string.splitlines(), dtype=float, ndmin=1)
elif dtype == "other":
value = string
# should not happen since validate_input has gatekeeping
else:
raise ValueError(f"Unsupported type {dtype}")
return value
[docs]
def convert_value_to_string(self, parameter, value):
"""
Converts a value to its string representation based on the parameter type.
Args:
parameter (str): The name of the parameter.
value: The value to convert.
Returns:
str: The string representation of the value.
Raises:
ValueError: If the value is not valid for the parameter.
"""
is_input_string = isinstance(value, str)
if not self.validate_input(parameter, value):
raise ValueError(f"{value} is not a valid input for {parameter}")
# Do not conver, just return the non-padded string
if is_input_string:
return value.strip()
pdict = self.get_parameter_dict(parameter)
dtype = pdict["type"]
# allow_bool_input = pdict.get("allow_bool_input", False)
if dtype == "string":
string = str(value).strip()
elif dtype == "integer":
# Be aware of bool values!
string = str(int(value))
elif dtype == "double":
string = "{:.14f}".format(float(value))
elif dtype in ("integer array", "double array"):
string = _array_to_string(value, dtype)
elif dtype == "other":
if not is_input_string:
raise ValueError("Only support string value when datatype is other")
string = value
else:
# should not happen since validate_input has gatekeeping
raise ValueError(f"Unsupported type {dtype}")
return string
def _array_to_string(arr, format):
"""
Converts an array to a string representation based on the specified format.
Args:
arr (array): The array to convert.
format (str): The format type ('integer array', 'double array', etc.).
Returns:
str: String representation of the array.
"""
arr = np.array(arr)
if arr.ndim == 1:
arr = arr.reshape(1, -1)
buf = StringIO()
if format in ("integer array", "integer"):
fmt = "%d"
elif format in ("double array", "double"):
fmt = "%.14f"
np.savetxt(buf, arr, delimiter=" ", fmt=fmt, header="", footer="", newline="\n")
# Return the string output of the buffer with
# whitespaces removed
return buf.getvalue().strip()