Skip to content

k4bench.analysis.expression

k4bench.analysis.expression

Per-event quantities written as expressions.

An expression is one number per event, computed from the event's generator particles (:meth:k4bench.analysis.event_source.EventSource.particles) and its per-event columns (what each collection holds, the canned quantities of :mod:k4bench.analysis.event_drivers)::

count(final & charged & (pt > 1))
sum(E, final & (pdg == 22)) / visible_E
n_VertexBarrelCollection + n_InnerTrackerBarrelCollection

Outside a reduction, a name is a per-event column. Inside one, a name is a particle field, and the reduction folds the particles of each event into one number:

  • count(mask): the particles the mask selects;
  • sum(x), mean(x), max(x), min(x), or with a mask as their second argument, sum(x, mask): over the particles selected, NaN values skipped. An event with none selected has a count and sum of 0 and a mean, max and min of NaN.

Also allowed: numbers, + - * / ** (a power only to a constant of magnitude at most :data:MAX_EXPONENT), comparisons, which may chain (0.5 < vr < 10), &, | and ~ between conditions, unary minus, and abs, sqrt and log10. and, or and not are not: a condition is an array, so it combines with &, | and ~, the comparisons in parentheses. A condition used as a number is 1 or 0.

:func:evaluate never hands the text to Python's eval. It parses it (:mod:ast) and computes only the node types above, in floating point under np.errstate(all="ignore"): an expression is at most :data:MAX_LENGTH characters, :data:MAX_NODES nodes and :data:MAX_DEPTH deep, so none is dear to compute, and an invalid operation gives NaN rather than an error. A result that is not finite is NaN.

ExpressionError

Bases: ValueError

An expression that cannot be computed, with why in words.

evaluate

evaluate(expression: str, events: DataFrame, particles: Mapping[str, ndarray] | None = None, event_index: ndarray | None = None) -> Series

expression's value in each event: one number per row of events, whose columns are the per-event names, indexed as events.

particles holds one array per particle field, and event_index the position in events of each particle's event; without them, an expression with a reduction cannot be computed. Raises :class:ExpressionError, saying why, for one that cannot be.

Source code in k4bench/analysis/expression.py
def evaluate(
    expression: str,
    events: pd.DataFrame,
    particles: Mapping[str, np.ndarray] | None = None,
    event_index: np.ndarray | None = None,
) -> pd.Series:
    """*expression*'s value in each event: one number per row of *events*,
    whose columns are the per-event names, indexed as *events*.

    *particles* holds one array per particle field, and *event_index* the
    position in *events* of each particle's event; without them, an
    expression with a reduction cannot be computed. Raises
    :class:`ExpressionError`, saying why, for one that cannot be.
    """
    tree = parse(expression)
    if particles is not None and (event_index is None or any(
        len(values) != len(event_index) for values in particles.values()
    )):
        raise ValueError("every particle field needs one value per particle of event_index")
    scope = _Scope(events, particles, event_index)
    with np.errstate(all="ignore"):
        value = scope.event(tree.body)
        values = np.broadcast_to(np.asarray(value, dtype=float), (len(events),))
        values = np.where(np.isfinite(values), values, np.nan)
    return pd.Series(values, index=events.index, name=expression.strip())

parse

parse(expression: str) -> Expression

expression parsed and checked against the size limits, without being computed; :class:ExpressionError when it is no expression or is too large.

Source code in k4bench/analysis/expression.py
def parse(expression: str) -> ast.Expression:
    """*expression* parsed and checked against the size limits, without
    being computed; :class:`ExpressionError` when it is no expression or is
    too large."""
    text = expression.strip()
    if not text:
        raise ExpressionError("the expression is empty")
    if len(text) > MAX_LENGTH:
        raise ExpressionError(f"the expression is longer than {MAX_LENGTH} characters")
    try:
        tree = ast.parse(text, mode="eval")
    except SyntaxError as exc:
        raise ExpressionError(f"not an expression: {exc.msg}") from None
    nodes = sum(1 for _ in ast.walk(tree))
    if nodes > MAX_NODES:
        raise ExpressionError(f"the expression has {nodes} parts, more than {MAX_NODES}")
    if _depth(tree) > MAX_DEPTH:
        raise ExpressionError(f"the expression nests deeper than {MAX_DEPTH}")
    return tree