rootfig 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rootfig/errors.py ADDED
@@ -0,0 +1,73 @@
1
+ """Exception and warning types raised by rootfig.
2
+
3
+ All exceptions derive from :class:`RootfigError` so callers can catch
4
+ everything from the library with a single ``except`` clause. The more
5
+ specific subclasses also inherit from the matching built-in exception
6
+ (``ValueError``, ``KeyError``-like ``LookupError``) so generic handlers
7
+ keep working.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+
17
+
18
+ class RootfigError(Exception):
19
+ """Base class for all errors raised by rootfig."""
20
+
21
+
22
+ class ExpressionError(RootfigError, ValueError):
23
+ """An expression string could not be parsed, is not allowed, or failed to evaluate."""
24
+
25
+
26
+ class MissingBranchError(ExpressionError, LookupError):
27
+ """An expression refers to a name that is not a branch, alias, function, or constant."""
28
+
29
+ def __init__(
30
+ self,
31
+ name: str,
32
+ *,
33
+ available: Sequence[str] = (),
34
+ suggestions: Sequence[str] = (),
35
+ context: str | None = None,
36
+ ) -> None:
37
+ self.name = name
38
+ self.available = tuple(available)
39
+ self.suggestions = tuple(suggestions)
40
+ message = f"unknown name {name!r}"
41
+ if context:
42
+ message += f" in {context}"
43
+ if self.suggestions:
44
+ message += ". Did you mean " + ", ".join(repr(s) for s in self.suggestions) + "?"
45
+ elif self.available:
46
+ shown = ", ".join(repr(a) for a in self.available[:12])
47
+ more = "" if len(self.available) <= 12 else f", ... ({len(self.available)} total)"
48
+ message += f". Available names: {shown}{more}"
49
+ super().__init__(message)
50
+
51
+
52
+ class SelectionError(RootfigError, ValueError):
53
+ """Variable, selection, and weight arrays have incompatible structures."""
54
+
55
+
56
+ class IncompatibleWeightError(SelectionError):
57
+ """A weight array cannot be broadcast to the variable being histogrammed."""
58
+
59
+
60
+ class BinningError(RootfigError, ValueError):
61
+ """A binning or range specification is invalid or cannot be inferred."""
62
+
63
+
64
+ class SourceError(RootfigError):
65
+ """A file, tree, or in-memory data source cannot be opened or interpreted."""
66
+
67
+
68
+ class LuminosityError(RootfigError, ValueError):
69
+ """A sample cannot be scaled to a luminosity (no luminosity, cross section or event count)."""
70
+
71
+
72
+ class RootfigWarning(UserWarning):
73
+ """Base class for warnings emitted by rootfig (dropped values, empty selections, ...)."""
@@ -0,0 +1,34 @@
1
+ """Expression strings over branches.
2
+
3
+ rootfig expressions use ordinary Python syntax and are evaluated with NumPy
4
+ and Awkward Array semantics, so ``Muon_pt > 20`` produces a boolean array
5
+ with the same (possibly jagged) structure as ``Muon_pt``.
6
+
7
+ Supported syntax
8
+ ----------------
9
+ * Names refer to branches (``Muon_pt``). Branch names that are not valid
10
+ Python identifiers are written in backticks: ```jet1_b-tag` > 0.5``.
11
+ * Arithmetic ``+ - * / // % **`` and unary ``-``.
12
+ * Comparisons ``== != < <= > >=`` including chained comparisons
13
+ (``20 < Muon_pt < 100``), which are rewritten element-wise.
14
+ * Boolean combinations with ``&``, ``|``, ``~`` **or** with ``and``, ``or``,
15
+ ``not``; the keyword forms are rewritten to the element-wise operators.
16
+ * Indexing and slicing (``Muon_pt[:, 0]``).
17
+ * A fixed set of functions (see :data:`FUNCTIONS`): element-wise NumPy
18
+ functions such as ``abs``, ``sqrt``, ``log``, ``where``, and per-event
19
+ reductions over jagged branches: ``count``, ``sum``, ``min``, ``max``,
20
+ ``mean``, ``any``, ``all``, ``first``.
21
+ * Constants ``pi``, ``e``, ``inf``, ``nan``, ``True``, ``False``.
22
+
23
+ Everything else (attribute access, lambdas, comprehensions, string literals,
24
+ calls to unknown functions) is rejected at parse time with
25
+ :class:`~rootfig.errors.ExpressionError`.
26
+
27
+ The public entry points are :func:`parse`, :class:`Expression`, and
28
+ :func:`evaluate`.
29
+ """
30
+
31
+ from rootfig.expressions.functions import CONSTANTS, FUNCTIONS
32
+ from rootfig.expressions.parser import Expression, ExpressionLike, evaluate, parse
33
+
34
+ __all__ = ["CONSTANTS", "FUNCTIONS", "Expression", "ExpressionLike", "evaluate", "parse"]
@@ -0,0 +1,151 @@
1
+ """Functions and constants available inside expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Callable, Mapping
7
+ from types import MappingProxyType
8
+ from typing import Any, Final
9
+
10
+ import awkward as ak
11
+ import numpy as np
12
+
13
+ from rootfig.errors import ExpressionError
14
+
15
+ __all__ = ["CONSTANTS", "FUNCTIONS"]
16
+
17
+ # --------------------------------------------------------------------------------------
18
+
19
+
20
+ def _require_jagged(name: str, array: Any) -> Any:
21
+ if not isinstance(array, ak.Array):
22
+ array = ak.Array(array)
23
+ if array.layout.purelist_depth < 2:
24
+ msg = (
25
+ f"{name}() reduces over the objects in each event and needs a jagged "
26
+ f"(per-object) array, but got a flat (per-event) array"
27
+ )
28
+ raise ExpressionError(msg)
29
+ return array
30
+
31
+
32
+ def _reduction(name: str, fn: Callable[..., Any]) -> Callable[[Any], Any]:
33
+ def reduce(array: Any) -> Any:
34
+ return fn(_require_jagged(name, array), axis=-1)
35
+
36
+ reduce.__name__ = name
37
+ return reduce
38
+
39
+
40
+ def _clip(x: Any, lo: Any, hi: Any) -> Any:
41
+ return np.minimum(np.maximum(x, lo), hi)
42
+
43
+
44
+ def _first(array: Any) -> Any:
45
+ return ak.firsts(_require_jagged("first", array), axis=-1)
46
+
47
+
48
+ # -- kinematics from Cartesian components (EDM4hep stores momentum.x/y/z and energy) ----
49
+
50
+
51
+ def _pt(px: Any, py: Any) -> Any:
52
+ return np.hypot(px, py)
53
+
54
+
55
+ def _p(px: Any, py: Any, pz: Any) -> Any:
56
+ return np.sqrt(px * px + py * py + pz * pz)
57
+
58
+
59
+ def _theta(px: Any, py: Any, pz: Any) -> Any:
60
+ return np.arctan2(np.hypot(px, py), pz)
61
+
62
+
63
+ def _costheta(px: Any, py: Any, pz: Any) -> Any:
64
+ with np.errstate(divide="ignore", invalid="ignore"): # nan for a zero momentum
65
+ return pz / _p(px, py, pz)
66
+
67
+
68
+ def _eta(px: Any, py: Any, pz: Any) -> Any:
69
+ with np.errstate(divide="ignore"): # +-inf along the beam axis
70
+ return np.arcsinh(pz / np.hypot(px, py))
71
+
72
+
73
+ def _phi(px: Any, py: Any) -> Any:
74
+ return np.arctan2(py, px)
75
+
76
+
77
+ def _mass(energy: Any, px: Any, py: Any, pz: Any) -> Any:
78
+ return np.sqrt(np.maximum(energy * energy - (px * px + py * py + pz * pz), 0.0))
79
+
80
+
81
+ FUNCTIONS: Final[Mapping[str, Callable[..., Any]]] = MappingProxyType(
82
+ {
83
+ # element-wise (NumPy ufuncs dispatch to Awkward automatically)
84
+ "abs": np.abs,
85
+ "sqrt": np.sqrt,
86
+ "cbrt": np.cbrt,
87
+ "exp": np.exp,
88
+ "expm1": np.expm1,
89
+ "log": np.log,
90
+ "log10": np.log10,
91
+ "log2": np.log2,
92
+ "log1p": np.log1p,
93
+ "power": np.power,
94
+ "hypot": np.hypot,
95
+ "sin": np.sin,
96
+ "cos": np.cos,
97
+ "tan": np.tan,
98
+ "arcsin": np.arcsin,
99
+ "arccos": np.arccos,
100
+ "arctan": np.arctan,
101
+ "arctan2": np.arctan2,
102
+ "sinh": np.sinh,
103
+ "cosh": np.cosh,
104
+ "tanh": np.tanh,
105
+ "arcsinh": np.arcsinh,
106
+ "arccosh": np.arccosh,
107
+ "arctanh": np.arctanh,
108
+ "deg2rad": np.deg2rad,
109
+ "rad2deg": np.rad2deg,
110
+ "floor": np.floor,
111
+ "ceil": np.ceil,
112
+ "round": np.rint,
113
+ "trunc": np.trunc,
114
+ "sign": np.sign,
115
+ "minimum": np.minimum,
116
+ "maximum": np.maximum,
117
+ "clip": _clip,
118
+ "isnan": np.isnan,
119
+ "isinf": np.isinf,
120
+ "isfinite": np.isfinite,
121
+ "where": ak.where,
122
+ # per-event reductions over jagged branches
123
+ "count": _reduction("count", ak.num),
124
+ "len": _reduction("len", ak.num),
125
+ "sum": _reduction("sum", ak.sum),
126
+ "prod": _reduction("prod", ak.prod),
127
+ "min": _reduction("min", ak.min),
128
+ "max": _reduction("max", ak.max),
129
+ "mean": _reduction("mean", ak.mean),
130
+ "std": _reduction("std", ak.std),
131
+ "any": _reduction("any", ak.any),
132
+ "all": _reduction("all", ak.all),
133
+ "argmin": _reduction("argmin", ak.argmin),
134
+ "argmax": _reduction("argmax", ak.argmax),
135
+ "first": _first,
136
+ # kinematics from Cartesian components
137
+ "pt": _pt,
138
+ "p": _p,
139
+ "theta": _theta,
140
+ "costheta": _costheta,
141
+ "eta": _eta,
142
+ "phi": _phi,
143
+ "mass": _mass,
144
+ }
145
+ )
146
+ """Functions callable from expressions, keyed by the name used in the expression."""
147
+
148
+ CONSTANTS: Final[Mapping[str, float]] = MappingProxyType(
149
+ {"pi": math.pi, "e": math.e, "inf": math.inf, "nan": math.nan}
150
+ )
151
+ """Named constants available in expressions when no branch of that name exists."""