Skip to content

Commit f77f57c

Browse files
author
miranov25
committed
Add ROOT SetAlias export and Python-to-ROOT AST translation for aliases
**Extended commit description:** * Introduced `convert_expr_to_root()` static method using `ast` to translate Python expressions into ROOT-compatible syntax, including function mapping (`mod → fmod`, `arctan2 → atan2`, etc.). * Patched `export_tree()` to: * Apply ROOT-compatible expression conversion. * Handle ROOT’s TTree::SetAlias limitations (e.g. constants) using `(<value> + 0)` workaround. * Save full Python alias metadata (`aliases`, `dtypes`, `constants`) as JSON in `TTree::GetUserInfo()`. * Patched `read_tree()` to: * Restore alias expressions and metadata from `UserInfo` JSON. * Maintain full alias context including constants and types. * Preserved full compatibility with the existing parquet export/load code. * Ensured Python remains the canonical representation; conversion is only needed for ROOT alias usage.
1 parent b188456 commit f77f57c

File tree

1 file changed

+76
-4
lines changed

1 file changed

+76
-4
lines changed

UTILS/dfextensions/AliasDataFrame.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,51 @@
1111
import matplotlib.pyplot as plt
1212
import networkx as nx
1313

14+
15+
16+
def convert_expr_to_root(expr):
17+
# Static version of AST-based translation from Python to ROOT expression
18+
import ast
19+
import re
20+
21+
class RootTransformer(ast.NodeTransformer):
22+
FUNC_MAP = {
23+
"arctan2": "atan2",
24+
"mod": "fmod",
25+
"sqrt": "sqrt",
26+
"log": "log",
27+
"log10": "log10",
28+
"exp": "exp",
29+
"abs": "abs",
30+
"power": "pow",
31+
"maximum": "TMath::Max",
32+
"minimum": "TMath::Min"
33+
}
34+
35+
def visit_Call(self, node):
36+
def get_func_name(n):
37+
if isinstance(n, ast.Attribute):
38+
return n.attr
39+
elif isinstance(n, ast.Name):
40+
return n.id
41+
return ""
42+
43+
func_name = get_func_name(node.func)
44+
root_func = self.FUNC_MAP.get(func_name, func_name)
45+
46+
node.args = [self.visit(arg) for arg in node.args]
47+
node.func = ast.Name(id=root_func, ctx=ast.Load())
48+
return node
49+
50+
try:
51+
expr_clean = re.sub(r"\bnp\.", "", expr)
52+
tree = ast.parse(expr_clean, mode='eval')
53+
tree = RootTransformer().visit(tree)
54+
ast.fix_missing_locations(tree)
55+
return ast.unparse(tree)
56+
except Exception:
57+
return expr
58+
1459
class AliasDataFrame:
1560
"""
1661
A wrapper for pandas DataFrame that supports on-demand computed columns (aliases)
@@ -270,16 +315,26 @@ def export_tree(self, filename, treename="tree", dropAliasColumns=True):
270315

271316
with uproot.recreate(filename) as f:
272317
f[treename] = export_df
318+
319+
# Write ROOT aliases and metadata
273320
f = ROOT.TFile.Open(filename, "UPDATE")
274321
tree = f.Get(treename)
275322
for alias, expr in self.aliases.items():
276-
expr_str = expr
277323
try:
278324
val = float(expr)
279-
expr_str = f"({val}+0)"
325+
expr_str = f"({val}+0)" # ROOT bug workaround for pure constants
280326
except Exception:
281-
pass
327+
expr_str = self._convert_expr_to_root(expr)
282328
tree.SetAlias(alias, expr_str)
329+
330+
# Store Python metadata as JSON string in TTree::UserInfo
331+
metadata = {
332+
"aliases": self.aliases,
333+
"dtypes": {k: v.__name__ for k, v in self.alias_dtypes.items()},
334+
"constants": list(self.constant_aliases),
335+
}
336+
jmeta = json.dumps(metadata)
337+
tree.GetUserInfo().Add(ROOT.TObjString(jmeta))
283338
tree.Write("", ROOT.TObject.kOverwrite)
284339
f.Close()
285340

@@ -288,13 +343,30 @@ def read_tree(filename, treename="tree"):
288343
with uproot.open(filename) as f:
289344
df = f[treename].arrays(library="pd")
290345
adf = AliasDataFrame(df)
291-
f = ROOT.TFile.Open(filename, "UPDATE")
346+
347+
f = ROOT.TFile.Open(filename)
292348
try:
293349
tree = f.Get(treename)
294350
if not tree:
295351
raise ValueError(f"Tree '{treename}' not found in file '{filename}'")
296352
for alias in tree.GetListOfAliases():
297353
adf.aliases[alias.GetName()] = alias.GetTitle()
354+
355+
user_info = tree.GetUserInfo()
356+
for i in range(user_info.GetEntries()):
357+
obj = user_info.At(i)
358+
if isinstance(obj, ROOT.TObjString):
359+
try:
360+
jmeta = json.loads(obj.GetString().Data())
361+
adf.aliases.update(jmeta.get("aliases", {}))
362+
adf.alias_dtypes.update({k: getattr(np, v) for k, v in jmeta.get("dtypes", {}).items()})
363+
adf.constant_aliases.update(jmeta.get("constants", []))
364+
break
365+
except Exception:
366+
pass
298367
finally:
299368
f.Close()
300369
return adf
370+
371+
def _convert_expr_to_root(self, expr):
372+
return convert_expr_to_root(expr)

0 commit comments

Comments
 (0)