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/__init__.py +97 -0
- rootfig/_typing.py +21 -0
- rootfig/api.py +1297 -0
- rootfig/errors.py +73 -0
- rootfig/expressions/__init__.py +34 -0
- rootfig/expressions/functions.py +151 -0
- rootfig/expressions/parser.py +462 -0
- rootfig/histograms/__init__.py +67 -0
- rootfig/histograms/build.py +197 -0
- rootfig/histograms/cutflow.py +195 -0
- rootfig/histograms/efficiency.py +214 -0
- rootfig/histograms/normalize.py +124 -0
- rootfig/histograms/pipeline.py +186 -0
- rootfig/histograms/ratio.py +161 -0
- rootfig/histograms/stats.py +183 -0
- rootfig/io/__init__.py +12 -0
- rootfig/io/sources.py +487 -0
- rootfig/model/__init__.py +27 -0
- rootfig/model/binning.py +237 -0
- rootfig/model/cuts.py +85 -0
- rootfig/model/samples.py +284 -0
- rootfig/model/style.py +144 -0
- rootfig/model/units.py +107 -0
- rootfig/model/variables.py +95 -0
- rootfig/plotting/__init__.py +78 -0
- rootfig/plotting/annotations.py +165 -0
- rootfig/plotting/correlation.py +93 -0
- rootfig/plotting/figure.py +458 -0
- rootfig/plotting/hist1d.py +421 -0
- rootfig/plotting/hist2d.py +59 -0
- rootfig/plotting/points.py +78 -0
- rootfig/plotting/ratio.py +163 -0
- rootfig/plotting/result.py +131 -0
- rootfig/plotting/style.py +308 -0
- rootfig/py.typed +0 -0
- rootfig/selection/__init__.py +23 -0
- rootfig/selection/columns.py +479 -0
- rootfig-0.1.0.dist-info/METADATA +202 -0
- rootfig-0.1.0.dist-info/RECORD +41 -0
- rootfig-0.1.0.dist-info/WHEEL +4 -0
- rootfig-0.1.0.dist-info/licenses/LICENSE +21 -0
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."""
|