Supported Expressions
This page is the reference for what formulate accepts and what it produces: the operators of each language, the functions and constants it knows, and the handful of constructs it deliberately refuses.
Three languages are involved. ROOT and NumExpr can each be both an input and an
output; Python is output-only, and is rendered with NumPy function names, so it
is meant to be evaluated somewhere numpy is imported as np.
Two things are true of every conversion. Output is fully parenthesized, because the languages disagree about precedence and formulate would rather be explicit than clever:
print(formulate.from_root("a + b * c").to_numexpr())
(a + (b * c))
And a parsed expression is immutable and reusable — parse once, render as many times as you like:
expr = formulate.from_root("TMath::Sqrt(px**2 + py**2)")
print(expr.to_root())
print(expr.to_numexpr())
print(expr.to_python())
TMath::Sqrt(((px ** 2) + (py ** 2)))
sqrt(((px ** 2) + (py ** 2)))
np.sqrt(((px ** 2) + (py ** 2)))
Operators
Arithmetic
Operation |
ROOT |
NumExpr |
Python |
|---|---|---|---|
Addition |
|
|
|
Subtraction |
|
|
|
Multiplication |
|
|
|
Division |
|
|
|
Power |
|
|
|
Modulo |
|
|
|
Unary plus / minus |
|
|
|
Power is the only operator that groups from the right, in all three languages:
a**b**c is a**(b**c).
Warning
ROOT and NumExpr disagree about what % computes, and formulate converts
it in either direction without complaint. It is the one construct that can
silently change meaning. See % does not mean the same thing in ROOT and numexpr.
Comparisons
==, !=, >, <, >= and <= are spelled the same way in all
three languages.
NumExpr does not support chained comparisons, so a < b < c is rejected on
input; write (a < b) & (b < c). ROOT accepts chains, because C++ does, but
they mean (a < b) < c there rather than what they mean in Python — so they
are almost always a mistake worth rewriting too.
Logical operators
Operation |
ROOT |
NumExpr |
Python |
|---|---|---|---|
AND |
|
|
|
OR |
|
|
|
NOT |
|
|
|
XOR |
— |
|
|
Each parser accepts only its own language’s spelling, and tells you which one an expression needs if you get it wrong:
try:
formulate.from_numexpr("a && b")
except formulate.ParseError as error:
print(error)
There was an error parsing the expression at or near this location
a && b
^
Here are some suggestions for how to fix the error:
- Use '&' instead of '&&' or 'and'.
Here is the Lark error message:
Unexpected token Token('AMPERSAND', '&') at line 1, column 4.
Expected one of:
* NUMBER
* MINUS
* PLUS
* NAME
* LPAR
* TILDE
Previous tokens: [Token('AMPERSAND', '&')]
Two differences are worth internalising. The operators bind differently
against comparisons — ROOT’s are logical and bind looser, NumExpr’s are bitwise
and bind tighter (see Logical operators bind differently in the two languages) — and ROOT has no XOR,
since it spells exponentiation with ^, so a NumExpr expression using XOR
cannot be converted to ROOT.
Python’s NOT is rendered as np.logical_not rather than ~ on purpose:
ROOT’s ! is a logical negation, whereas NumPy’s ~ is a bitwise
inversion, and they disagree on anything that is not a boolean (!5 is 0
but ~5 is -6).
The ROOT multi-output operator
TTreeFormula uses : to separate the several expressions of a
multi-dimensional draw. formulate parses it, and renders it in Python as the
comma-separated list that Python reads as a tuple:
print(formulate.from_root("px : py : pz").to_root())
print(formulate.from_root("px : py : pz").to_python())
px : py : pz
px, py, pz
NumExpr evaluates a single expression and has no equivalent, so converting one of these raises.
: separates whole expressions rather than combining two values, so it is
only accepted between them – a:b and a+1 : b*2 are fine, but
(a:b)+c and sqrt(a:b) are rejected, as they are by ROOT. This is also
what lets it be the one operator that is never parenthesized: were it allowed
to nest, (a:b)+c would have to serialize as a : b + c and would read
back as a:(b+c).
Indexing
ROOT indexes with one bracket pair per dimension and Python with a single comma-separated pair; formulate translates between the two spellings:
print(formulate.from_root("arr[0]").to_root())
print(formulate.from_root("energies[i][j]").to_python())
arr[0]
energies[i, j]
NumExpr has no indexing at all — arrays are passed in whole, already sliced — so an indexed expression cannot be converted to it.
Functions
Names are matched case-insensitively and through a table of aliases, so
TMath::ATan2(y, x), atan2(y, x) and arctan2(y, x) all parse to the
same thing. The aliases are ln for log, power for pow, and the
asin/acos/atan/atan2/asinh/acosh/atanh spellings of
the inverse trigonometric functions.
A dash below means the language has no faithful equivalent, and converting such
an expression to it raises ValueError rather than approximating.
Common functions
Canonical name |
ROOT |
NumExpr |
Python |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
— |
|
|
|
|
|
— |
|
|
|
— |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pow has no function spelling in NumExpr, where it is written as the **
operator; TMath::Power(x, y) therefore converts to (x ** y), and a
pow call with any other number of arguments has nothing to convert to.
Array reductions
These take one array and return a scalar. ROOT spells them with a trailing
$.
Canonical name |
ROOT |
NumExpr |
Python |
|---|---|---|---|
|
|
|
|
|
— |
|
|
|
|
|
|
|
|
|
|
|
|
— |
— |
Element-wise minimum and maximum
TMath::Min and TMath::Max take two arguments and compare them
element-wise, which is a different operation from the reductions above. They are
tracked separately so that Min$(arr) and TMath::Min(a, b) do not collide.
Canonical name |
ROOT |
NumExpr |
Python |
|---|---|---|---|
|
|
— |
|
|
|
— |
|
NumExpr’s min and max are the reductions, not these, so there is nothing
to convert them to; the equivalent would be where(a < b, a, b), which is an
expression rather than a function name:
try:
formulate.from_root("TMath::Min(a, b)").to_numexpr()
except ValueError as error:
print(error)
Function "TMath::Min" is not supported in NumExpr.
NumExpr-specific functions
Canonical name |
ROOT |
NumExpr |
Python |
|---|---|---|---|
|
— |
|
|
|
— |
|
|
|
— |
|
|
|
— |
|
|
|
— |
|
— |
|
— |
|
— |
contains is a substring test, and NumPy has no equivalent that can be
written as a single function name, so it converts to neither of the other two.
complex is the same story. np.complex128 looks like the counterpart,
but it is a scalar type constructor rather than an element-wise function: it
accepts 0-d input only and raises TypeError on arrays, which is what these
expressions are almost always evaluated against. The element-wise form is
a + 1j*b, an expression rather than a name, so to_python() refuses it
instead of emitting something that works only for single values.
ROOT-specific functions
The rest of TMath that formulate knows about. None of these have NumExpr or
NumPy equivalents, so they can only be converted back to ROOT — but they parse,
which is what makes variables usable on any ROOT
expression.
Arguments |
|
|---|---|
One |
|
Two |
|
Three |
|
Four or more |
|
Note
formulate does not check how many arguments a function is given — the tables
above record what ROOT’s signatures are, but TMath::Erf(a, b) will parse
and convert. The one exception is pow converted to NumExpr, because the
** operator it becomes has nowhere to put a third argument.
Constants
Constants are recognised by name, and in ROOT also in their TMath::X() call
form. They are stored canonically, so TMath::E(), e_num and ℯ are
the same constant and all report as exp1.
NumExpr has no symbolic constants, so converting to it — and to Python, for the numeric ones — substitutes the value. This is a one-way street: see Named constants do not survive a round trip through numexpr.
A few constants have no single-name spelling in a backend — hbarc is a
product, eminus and neginf are negations — and those are emitted
parenthesized, exactly as shown below, so that they keep binding as one atom
under **.
Mathematical constants
Canonical name |
Also accepted as |
ROOT |
NumExpr and Python |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
|
|
|
— |
|
|
|
|
|
|
|
— |
|
|
|
— |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Physical constants
Values come from hepunits, in SI units, matching what TMath returns.
Canonical name |
Also accepted as |
ROOT |
NumExpr and Python |
Unit |
|---|---|---|---|---|
|
|
|
|
m/s |
|
|
|
|
J·s |
|
|
|
|
J·s |
|
|
|
|
J·m |
|
|
|
|
J/K |
|
|
|
|
1/mol |
|
|
|
|
C |
|
|
|
|
C |
Note
The single-letter forms — e, c, h, k, na, qe — are
recognised only in their call form, c() or TMath::C(). A bare c
is a variable, which is what you want when c is a branch name. This
changed in v1.0.1; expressions written for older versions that relied on a
bare c meaning the speed of light need c_light.
Booleans and special values
Canonical name |
Also accepted as |
ROOT |
NumExpr |
Python |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
|
|
|
|
— |
|
|
|
|
— |
|
NumExpr has no literal for the non-finite values, so those three cannot be converted to it.
Inspecting an expression
Beyond conversion, a parsed expression reports what it refers to. This is what you want when deciding which branches to read from a file:
expr = formulate.from_root("TMath::Sqrt(px**2 + py**2) > 5 * TMath::Pi() + 1.5")
print(list(expr.variables))
print(list(expr.named_constants))
print(list(expr.unnamed_constants))
['px', 'py']
['pi']
[2, 5, 1.5]
Names come out in the order they first appear, and each is reported once. They
are reported as ROOT spells them, which for a dotted branch name is not how
to_numexpr() writes it — numexpr cannot take a dot, so those names are
hex-encoded on the way out and it is the encoded name you must supply when you
evaluate. See Dotted branch names are hex-encoded for numexpr.
str() on the expression shows the parsed structure in canonical names, which
is the quickest way to check how something was grouped:
print(formulate.from_root("a && b < c"))
print(formulate.from_numexpr("a & b < c"))
and(a, lt(b, c))
lt(and(a, b), c)
Limitations
Anything with no faithful equivalent in the target language raises
ValueError rather than converting to something subtly different — the
dashes throughout this page are all instances of that. The Common Issues page
covers the cases people hit most.
Beyond those:
Not every function is known. The tables above are hand-maintained, and a name that is not in them raises rather than being passed through, so that a typo does not become a mysterious failure in the target engine. If something is missing, please open an issue — or add it: a function is one entry per table, and Contributing to Formulate describes how.
User-defined functions are not supported, for the same reason.
Argument counts are not validated, except for
pow.Strings are not supported. NumExpr’s
containstakes them, but formulate’s grammars only accept numbers, names and operators, so an expression containing a string literal will not parse.Types are not tracked. formulate translates syntax; whether a branch is an integer or a float, a scalar or an array, is something only the target engine knows. This is what makes
%dangerous — see % does not mean the same thing in ROOT and numexpr.