Source code for NekUpload.utils.parsing

import math
import h5py
from sympy import sympify

#these are as defined in Nektar User Guide under Expressions
#https://doc.nektar.info/userguide/latest/user-guidese13.html
MATH_CONSTANTS: dict[str,float] = {
    "E": math.e,
    "PI": math.pi,
    "GAMMA": 0.57721566490153286060,
    "DEG": 180 / math.pi,  # Degrees per radian
    "PHI": (1 + math.sqrt(5)) / 2,  # Golden ratio
    "LOG2E": math.log2(math.e),
    "LOG10E": math.log10(math.e),
    "LN2": math.log(2),
    "PI_2": math.pi / 2,
    "PI_4": math.pi / 4,
    "1_PI": 1 / math.pi,
    "2_PI": 2 / math.pi,
    "2_SQRTPI": 2 / math.sqrt(math.pi),
    "SQRT2": math.sqrt(2),
    "SQRT1_2": math.sqrt(0.5),
}

[docs] def get_both_sides_of_equals(text_with_equals: str) -> tuple[str, str]: """Given a string "LHS = RHS", acquire LHS and RHS Args: text_with_equals (str): String containing an equality "LHS = RHS" Returns: tuple[str, str]: Returns a tuple containing LHS string and RHS string, trimmed of leading and trailing whitespace """ left, right = text_with_equals.split("=") return left.strip(), right.strip()
[docs] def safe_resolve(param_name: str, params: dict[str, str], seen: set) -> float | str: """Recursively resolves and evaluates expressions in params. For example, the following: data = {"PARAM1": "10","PARAM2" : "PARAM1"} will be resolved into the following form due to the references: expected = {"PARAM1": 10,"PARAM2" : 10} Mathematical operators can also be evaluated, the following also works: data = {"PARAM1": "10","PARAM2" : "PARAM1 + 5"} -> expected = {"PARAM1": 10,"PARAM2" : 15} Args: param_name (str): The name of the parameter to resolve. params (dict[str, str]): Dictionary of parameters mapping names to expressions. seen (set): Set of already visited parameters (to prevent cycles). Returns: Union[float, str]: Evaluated numerical result or original string if resolution fails. """ if param_name in seen: raise ValueError(f"Circular reference detected: {' -> '.join(seen)} -> {param_name}") seen.add(param_name) # Track visited parameters # Get the value (expression) of the parameter expr = params.get(param_name, param_name) # Try converting to number directly try: return float(expr) except ValueError: pass # Not a direct number, so evaluate it # Replace references to other parameters for key in params: if key in expr: resolved_value = safe_resolve(key, params, seen.copy()) expr = expr.replace(key, str(resolved_value)) # Evaluate safely using sympy try: return float(sympify(expr, locals=MATH_CONSTANTS)) except Exception: return expr # Return as string if evaluation fails
[docs] def evaluate_parameters(params: dict[str, str]) -> dict[str, float]: """Evaluates all parameters in a dictionary. For example, the following: data = {"PARAM1": "10","PARAM2" : "PARAM1"} will be resolved into the following form due to the references: expected = {"PARAM1": 10,"PARAM2" : 10} Mathematical operators can also be evaluated, the following also works: data = {"PARAM1": "10","PARAM2" : "PARAM1 + 5"} -> expected = {"PARAM1": 10,"PARAM2" : 15} Args: params (dict[str, str]): Dictionary mapping parameter names to expressions. Returns: dict[str, float]: Evaluated parameters with numerical values. """ evaluated = {} for param in params: evaluated[param] = safe_resolve(param, params, set()) return evaluated
[docs] def get_all_files_with_extension(files: list[str],extension: str) -> list[str]: """Return all files in a list with the specified file extension Args: files (list[str]): list containing the files extension (str): File extension to be found Returns: list[str]: list of files containing the extension """ # Ensure extension starts with "." if not extension.startswith("."): extension = f".{extension}" return [f for f in files if f.lower().endswith(extension.lower())]
[docs] def get_hdf5_groups_with_depth_limit(hdf5_file: h5py.File,max_depth: int,start_path: str = "",max_groups: int = 100) -> list[str]: """Traverses the HDF5 hierarchy and returns group paths up to a specified depth. Will find "" as first group. Args: hdf5_file (h5py.File): An open h5py.File object. max_depth (int): The maximum depth to traverse. start_path (str): the path to start the search from. max_groups (int): If defined, specifies maximum number of groups to be found. Once exceeded, function will return. Returns: list[str]: A list of group paths. """ group_paths = [] def _traverse(group: h5py.Group | h5py.Dataset, current_path, current_depth): if current_depth > max_depth: return if len(group_paths) >= max_groups: return group_paths.append(current_path) for name, obj in group.items(): if isinstance(obj, h5py.Group): new_path = f"{current_path}/{name}" if current_path else name _traverse(obj, new_path, current_depth + 1) if start_path == "": _traverse(hdf5_file, "", 0) else: _traverse(hdf5_file[start_path], start_path, 0) return group_paths
[docs] def get_hdf5_datasets_with_depth_limit(hdf5_file: h5py.File,max_depth: int,start_path: str = "",max_datasets: int=100) -> list[str]: """Traverses the HDF5 hierarchy and returns dataset paths up to a specified depth. Args: hdf5_file (h5py.File): An open h5py.File object. max_depth (int): The maximum depth to traverse. start_path (str): the path to start the search from. max_groups (int): If defined, specifies maximum number of groups to be found. Once exceeded, function will return. Returns: list[str]: A list of group paths. """ dataset_paths = [] def _traverse(group, current_path, current_depth): if current_depth > max_depth: return for name, obj in group.items(): if len(dataset_paths) >= max_datasets: return new_path = f"{current_path}/{name}" if current_path else name if isinstance(obj, h5py.Dataset): dataset_paths.append(new_path) elif isinstance(obj, h5py.Group): _traverse(obj, new_path, current_depth + 1) if start_path == "": _traverse(hdf5_file, "", 0) else: _traverse(hdf5_file[start_path], start_path, 0) return dataset_paths
if __name__ == "__main__": params = { "A": "2", "NEK": "A + PI_2", "C": "NEK*2" } print(evaluate_parameters(params))