@@ -81,21 +81,20 @@ def get_func_name(n):
8181# Add BEFORE class AliasDataFrame:
8282
8383class NumpyRootMapper :
84- """Maps NumPy function names to ROOT C++ equivalents"""
84+ """Maps NumPy function names to ROOT C++ equivalents (bidirectional) """
8585
8686 # Maps function names to (numpy_attr, root_name)
87- # Some functions are aliases (asinh → arcsinh in numpy)
8887 MAPPING = {
89- # Hyperbolic functions (needed for compression)
88+ # Hyperbolic functions
9089 'sinh' : ('sinh' , 'sinh' ),
9190 'cosh' : ('cosh' , 'cosh' ),
9291 'tanh' : ('tanh' , 'tanh' ),
9392 'arcsinh' : ('arcsinh' , 'asinh' ),
9493 'arccosh' : ('arccosh' , 'acosh' ),
9594 'arctanh' : ('arctanh' , 'atanh' ),
96- 'asinh' : ('arcsinh' , 'asinh' ), # Alias: np.arcsinh
97- 'acosh' : ('arccosh' , 'acosh' ), # Alias: np.arccosh
98- 'atanh' : ('arctanh' , 'atanh' ), # Alias: np.arctanh
95+ 'asinh' : ('arcsinh' , 'asinh' ),
96+ 'acosh' : ('arccosh' , 'acosh' ),
97+ 'atanh' : ('arctanh' , 'atanh' ),
9998
10099 # Trigonometric
101100 'sin' : ('sin' , 'sin' ),
@@ -105,9 +104,10 @@ class NumpyRootMapper:
105104 'arccos' : ('arccos' , 'acos' ),
106105 'arctan' : ('arctan' , 'atan' ),
107106 'arctan2' : ('arctan2' , 'atan2' ),
108- 'asin' : ('arcsin' , 'asin' ), # Alias: np.arcsin
109- 'acos' : ('arccos' , 'acos' ), # Alias: np.arccos
110- 'atan' : ('arctan' , 'atan' ), # Alias: np.arctan
107+ 'asin' : ('arcsin' , 'asin' ),
108+ 'acos' : ('arccos' , 'acos' ),
109+ 'atan' : ('arctan' , 'atan' ),
110+ 'atan2' : ('arctan2' , 'atan2' ), # ← NEW: ROOT name maps to numpy
111111
112112 # Exponential/log
113113 'exp' : ('exp' , 'exp' ),
@@ -126,7 +126,11 @@ class NumpyRootMapper:
126126
127127 @classmethod
128128 def get_numpy_functions_for_eval (cls ):
129- """Get dict of function_name → numpy_function for evaluation"""
129+ """Get dict of function_name → numpy_function for evaluation
130+
131+ Includes both Python names (arctan2) and ROOT names (atan2)
132+ for bidirectional compatibility when reading ROOT files.
133+ """
130134 funcs = {}
131135 for name , (np_attr , _ ) in cls .MAPPING .items ():
132136 if hasattr (np , np_attr ):
@@ -176,18 +180,21 @@ def get_subframe(self, name):
176180
177181 def _default_functions (self ):
178182 import math
183+
184+ # Start with math functions (scalar fallbacks)
179185 env = {k : getattr (math , k ) for k in dir (math ) if not k .startswith ("_" )}
180186
181- # Add numpy functions (will override math versions with vectorized numpy)
187+ # CRITICAL: Override with numpy vectorized versions
188+ # This ensures both arctan2 AND atan2 map to np.arctan2
182189 env .update (NumpyRootMapper .get_numpy_functions_for_eval ())
183190
184191 env ["np" ] = np
185192 for sf_name , sf_entry in self ._subframes .items ():
186193 env [sf_name ] = sf_entry ['frame' ]
187194
188- env ["int" ] = lambda x : np .array ( x ). astype ( np .int32 )
189- env ["uint" ] = lambda x : np .array ( x ). astype ( np .uint32 )
190- env ["float" ] = lambda x : np .array ( x ). astype ( np .float32 )
195+ env ["int" ] = lambda x : np .asarray ( x , dtype = np .int32 )
196+ env ["uint" ] = lambda x : np .asarray ( x , dtype = np .uint32 )
197+ env ["float" ] = lambda x : np .asarray ( x , dtype = np .float32 )
191198 env ["round" ] = np .round
192199 env ["clip" ] = np .clip
193200
@@ -250,7 +257,27 @@ def _eval_in_namespace(self, expr):
250257 expr = self ._prepare_subframe_joins (expr )
251258 local_env = {col : self .df [col ] for col in self .df .columns }
252259 local_env .update (self ._default_functions ())
253- return eval (expr , {}, local_env )
260+
261+ try :
262+ return eval (expr , {}, local_env )
263+ except NameError as e :
264+ # Function or variable not found
265+ missing_name = str (e ).split ("'" )[1 ] if "'" in str (e ) else "unknown"
266+ available_funcs = sorted ([k for k in local_env .keys () if callable (local_env .get (k ))])[:20 ]
267+ raise NameError (
268+ f"Undefined function or variable '{ missing_name } ' in expression: { expr } \n "
269+ f"Available functions include: { ', ' .join (available_funcs )} \n "
270+ f"Hint: Common functions are available, including both 'arctan2' and 'atan2'"
271+ ) from e
272+ except TypeError as e :
273+ if "cannot convert the series" in str (e ):
274+ raise TypeError (
275+ f"Scalar function used on array data in expression: { expr } \n "
276+ f"Error: { e } \n "
277+ f"Hint: All math functions should be vectorized (numpy-based). "
278+ f"If you see this with standard functions like 'atan2', please report as a bug."
279+ ) from e
280+ raise
254281
255282 def _resolve_dependencies (self ):
256283 from collections import defaultdict
0 commit comments