Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/hdfmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
'set_all_logging_level', 'version_info', 'module_info'
]

__version__ = "1.3.0"
__date__ = "2026/08/14"
__version__ = "1.4.0"
__date__ = "2026/08/24"
__author__ = "Dan Porter"


Expand Down
64 changes: 44 additions & 20 deletions src/hdfmap/eval_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
re_long_floats = re.compile(r'\d+\.\d{5,}') # finds floats with long trailing decimals
re_dataset_attributes = re.compile(r'([a-zA-Z_]\w*)@([a-zA-Z_]\w*)') # finds 'name@attribute' in expressions
re_dataset_default = re.compile(r'(\w+)\?\((.+?)\)') # finds 'name?('noname'), return (name, 'noname')
re_dataset_alternate = re.compile(r'\((\w\S*\|\w\S*)\)') # finds '(name1|name2|name3)', return 'name1|name2|name3'
# fromisoformat requires python 3.11+
datetime_converter = np.vectorize(lambda x: datetime.datetime.fromisoformat(x.decode() if hasattr(x, 'decode') else x))

Expand Down Expand Up @@ -239,6 +238,34 @@ def find_identifiers(expression: str) -> list[str]:
return [name for name in asteval.get_ast_names(ast.parse(expression))]


def find_or_expressions(expr: str) -> set[str]:
"""
Returns instances of OR-expressions within the expression,

An OR-expression is given as '(a|b|c)'

Brackets are counted correctly so only the OR-expression will be returned.

:param expr: string expression
:return: set of unique string expressions
"""
stack = []
alternates = []

for i, ch in enumerate(expr):
if ch == '(':
stack.append(i)
elif ch == ')':
if not stack:
continue
start = stack.pop()
sub_str = expr[start:i + 1]
if '|' in sub_str:
alternates.append(sub_str)
stack.clear()
return set(alternates)


def replace_expression_vars(expr: str, mapping: dict[str, str]) -> str:
"""
Replace variable names in an expression
Expand All @@ -249,23 +276,20 @@ def replace_expression_vars(expr: str, mapping: dict[str, str]) -> str:
:param mapping: mapping from variable names to replacements
:return: replaced expression
"""
# Remove (alternate|names) if any part would be replaced
hidden = []
def hide(match):
sub_str = match.group(0)
if any(name in sub_str for name in mapping):
hidden.append(match.group(0))
return f"__ALT{len(hidden)-1}__"
return sub_str
expr = re_dataset_alternate.sub(hide, expr)
# Find OR expressions (a|b), don't replace anything in these
sub_expr = find_or_expressions(expr)

for n, sub_str in enumerate(sub_expr):
expr = expr.replace(sub_str, f"__ALT{n}__")

# Replace names in mapping
for name, repl in mapping.items():
pattern = r'\b' + re.escape(name) + r'\b'
expr = re.sub(pattern, repl, expr)

# recover original expression parts
for ii, match in enumerate(hidden):
expr = expr.replace(f"__ALT{ii}__", match)
for n, sub_str in enumerate(sub_expr):
expr = expr.replace(f"__ALT{n}__", sub_str)
return expr


Expand Down Expand Up @@ -383,14 +407,14 @@ def prepare_expression(hdf_file: h5py.File, expression: str, hdf_namespace: dict
else:
expression = expression.replace(match.group(), name)
# find alternate names '(opt1|opt2|opt3)'
for alt_names in re_dataset_alternate.findall(expression): # alt_names = 'opt1|opt2|opt3'
names = alt_names.split('|')
# first available name in hdf_namespace or last name
name = next(
(n for n in names if n in attributes),
next((n for n in names if hdf_namespace.get(n, '') in hdf_file), names[-1])
)
expression = expression.replace(f"({alt_names})", name) # replace parentheses
while or_expressions := find_or_expressions(expression):
for or_exp in or_expressions:
names = or_exp.strip('()').split('|')
name = next(
(n for n in names if n in attributes),
next((n for n in names if hdf_namespace.get(n, '') in hdf_file), names[-1])
)
expression = expression.replace(or_exp, name)
return expression


Expand Down
17 changes: 17 additions & 0 deletions tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import hdfmap
import hdfmap.hdf_loader
from pytest import approx

from . import only_dls_file_system

Expand Down Expand Up @@ -171,3 +172,19 @@ def test_i06_pol_scan():
assert len(axes_paths) == len(axes_names)
# assert axes_names[1] == 'ds'


@only_dls_file_system
def test_complex_eval():
f = '/dls/science/groups/das/ExampleData/hdfmap_tests/i16/1109527.nxs'
m = hdfmap.create_nexus_map(f)

m.add_named_expression(**{
'_t': '(count_time|counttime|t?(1.0))',
'_cmd': '(cmd|user_input_command|user_command|scan_command)',
'cmd': '(cmd|user_input_command|user_command|scan_command)',
})

with m.load_hdf() as hdf:
assert m.eval(hdf, '_cmd') == 'flyscancn eta_fly 0.005 61 pil3_100k 0.1 0.5 roi1 roi2'
assert m.eval(hdf, 'max(signal / Transmission / (rc/300.) / _t)') == approx(1215483134.5953412)
assert m.eval(hdf, 'cmd') == 'flyscancn eta_fly 0.005 61 pil3_100k 0.1 0.5 roi1 roi2'
5 changes: 5 additions & 0 deletions tests/test_eval_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def test_replace_expression_vars():
new_expr = replace_expression_vars(expression, mapping)
assert new_expr == "(cmd|command|scan_command?(''))\nstr((cmd|command|scan_command?('no_cmd')))\npath"

expression = "max(_t)"
mapping = {'_t': '(count_time|counttime|t?(1.0))'}
new_expr = replace_expression_vars(expression, mapping)
assert new_expr == "max((count_time|counttime|t?(1.0)))"


def test_prepare_expression_load_data():
expression = 'idgap@units, x*y'
Expand Down
Loading