"""
Versatile utility.
This module contains the utilities mainly for string and list.
"""
import numpy as np
from fractions import Fraction
from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication, rationalize
from sympy import SympifyError, Symbol, Basic
# ==================================================
[docs]
def str_to_list(s):
"""
Convert a string to a list of strings.
Args:
s (str): a string with irregular-shaped list format.
Returns:
- (list) -- a list of strings.
Note:
- in case of no bracket, return as it is.
- raise ValueError for invalid brackets.
"""
if s.count("[") != s.count("]"):
raise ValueError(f"invalid blackets in '{s}'.")
if s.count("[") == 0:
return s
nested_list = []
stack = []
current_word = ""
for char in s:
if char == "[":
if current_word.strip(): # remove space, and append it for non-null string.
stack[-1].append(current_word.strip())
current_word = ""
stack.append([])
elif char == "]":
if current_word.strip(): # remove space, and append it for non-null string.
stack[-1].append(current_word.strip())
current_word = ""
if stack:
popped = stack.pop()
if stack:
stack[-1].append(popped)
else:
nested_list.append(popped)
elif char == ",":
if current_word.strip(): # remove space, and append it for non-null string.
stack[-1].append(current_word.strip())
current_word = ""
else:
current_word += char
if current_word.strip(): # parse last word.
if stack:
stack[-1].append(current_word.strip())
else:
nested_list.append(current_word.strip())
return nested_list[0]
# ==================================================
[docs]
def is_regular_list(lst):
"""
Is regular-shaped list ?
Args:
lst (list): a list.
Returns:
- (bool) -- is regular-shaped list ?
"""
try:
np.array(lst)
return True
except ValueError:
return False
# ==================================================
[docs]
def apply_to_list(func, lst):
"""
Apply a function to each element of a list.
Args:
func (function): a function.
lst (list): a list.
Returns:
- (list) -- an applied list.
Note:
- applicable to an irregular-shaped list.
"""
if not isinstance(lst, list):
return func(lst)
result = []
for sub_lst in lst:
if isinstance(sub_lst, list):
# apply function to sub list recursively.
result.append(apply_to_list(func, sub_lst))
else:
# apply function to non list.
result.append(func(sub_lst))
return result
# ==================================================
[docs]
def apply_to_numpy(func, lst):
"""
Apply a function to each element of ndarray.
Args:
func (function): a function.
lst (ndarray): a numpy array.
Returns:
- (ndarray) -- an applied array.
Note:
- applicable only to a regular-shaped ndarray.
"""
return np.vectorize(func)(lst)
# ==================================================
[docs]
def to_fraction(x, max_denominator=1000000):
"""
Convert a float number to a fractional one.
Args:
x (float): a float number.
max_denominator (int, optional): max. of denominator.
Returns:
- (str) -- a fractional string.
"""
return str(Fraction(x).limit_denominator(max_denominator))
# ==================================================
[docs]
def get_variable(sp_ex):
"""
Get variables used in a sympy expression.
Args:
sp_ex (sympy): a sympy expression (except for Matrix).
Returns:
- (list) -- variable strings (sorted).
"""
var = set()
if isinstance(sp_ex, Basic):
var.update(set(map(str, sp_ex.atoms(Symbol))))
return sorted(var)
# ==================================================
[docs]
def str_to_sympy(s, check_var=None, rational=True, subs=None):
"""
Convert a string to a sympy.
Args:
s (str): a string.
local (dict, optional): variables to replace.
check_var (list, optional): variables to accept.
rational (bool, optional): use rational number ?
subs (dict, optional): replace dict for local variables.
Returns:
- (sympy) -- a sympy.
Notes:
- if format error occurs, return None.
- if s cannot be converted to a sympy, return None.
"""
if check_var is None:
check_var = []
check_var = set(check_var)
transformations = standard_transformations + (implicit_multiplication,)
if rational:
transformations += (rationalize,)
try:
expression = parse_expr(s, transformations=transformations, local_dict=subs)
except (SympifyError, SyntaxError, TypeError):
return None
var = set(get_variable(expression))
if len(check_var) != 0 and not (var <= check_var):
return None
return expression
# ==================================================
[docs]
def str_to_numpy(s, digit=None, check_var=None, check_shape=None):
"""
Convert a string (list) to a numpy array.
Args:
s (str): a string (list).
digit (int, optional): accuracy digit (only for numerical vector).
check_var (list, optional): variables to accept.
check_shape (tuple, optional): shape to check.
Returns:
- (ndarray): a numpy array.
Note:
- when parse error occurrs, return None.
- in check_shape, '0' means no check.
"""
try:
sl = str_to_list(s)
except:
return None
if sl is None:
return None
sl = np.array(sl)
if check_shape is not None:
if sl.ndim != len(check_shape):
return None
shape = sl.shape
for i in range(len(shape)):
if check_shape[i] > 0 and shape[i] != check_shape[i]:
return None
try:
is_ex = digit == None
sl = apply_to_numpy(lambda x: str_to_sympy(x, check_var, rational=is_ex), sl)
if np.any(sl == None):
return None
if not is_ex:
sl = apply_to_numpy(float, sl).round(digit)
except:
return None
return sl
# ==================================================
[docs]
def affine_trans(v, s=None, A=None, digit=None, check_var=None):
"""
Affine transformation, A.v + s.
Args:
v (str): site/vector or a list of site/vector.
s (str, optional): shift vector or a list of shift vector.
A (str, optional): rotaional matrix (3x3), [a1, a2, a3].
digit (int, optional): accuracy digit (only for numerical vector).
check_var (list, optional): variables to accept.
Returns:
- (ndarray) -- transformed vector.
"""
v = str_to_numpy(v, digit, check_var)
if v is None:
return None
if v.ndim != 1 and v.ndim != 2:
return None
if v.ndim == 1 and v.shape[0] != 3:
return None
if v.ndim == 2 and v.shape[1] != 3:
return None
if A is not None:
A = str_to_numpy(A, digit, check_var, (3, 3))
if A is None:
return None
v = v @ A.T
if s is not None:
s = str_to_numpy(s, digit, check_var)
if s is None:
return None
if s.ndim != 1 and s.ndim != 2:
return None
if s.ndim == 1 and s.shape[0] != 3:
return None
if s.ndim == 2 and s.shape[1] != 3:
return None
if v.ndim == s.ndim:
v += s
else:
if v.ndim == 1:
v = np.tile(v, (s.shape[0], 1)) + s
else:
v = v + np.tile(s, (v.shape[0], 1))
return v
# ==================================================
[docs]
def str_to_sympy1(s, check_var=None, rational=True, subs=None):
"""
Convert a string to a sympy (new version).
Args:
s (str): a string.
check_var (list, optional): variables to accept.
rational (bool, optional): use rational number ?
subs (dict, optional): replace dict for local variables.
Returns:
- (ndarray) -- (list of) sympy.
Notes:
- if format error occurs, raise ValueError.
- if s cannot be converted to a sympy, raise ValueError.
"""
if check_var is None:
check_var = []
check_var = set(check_var)
transformations = standard_transformations + (implicit_multiplication,)
if rational:
transformations += (rationalize,)
try:
expression = parse_expr(s, transformations=transformations, local_dict=subs)
except (SympifyError, SyntaxError, TypeError):
raise ValueError(f"invalid string '{s}'.")
var = set(get_variable(expression))
if len(check_var) != 0 and not (var <= check_var):
raise ValueError(f"invalid variable in '{s}'.")
expression = np.asarray(expression)
return expression