rootfig 0.2.0__tar.gz → 0.2.2__tar.gz
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-0.2.0 → rootfig-0.2.2}/CONTRIBUTING.md +11 -3
- {rootfig-0.2.0 → rootfig-0.2.2}/PKG-INFO +58 -62
- rootfig-0.2.2/README.md +164 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/gallery.md +21 -15
- rootfig-0.2.2/docs/hooks/gallery.py +98 -0
- rootfig-0.2.2/docs/images/gallery/overlay_ratio.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.2.2/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.2.2/docs/images/gallery/xbreak_ratio.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/plotting.md +8 -1
- rootfig-0.2.0/examples/gallery.py → rootfig-0.2.2/examples/gallery/__init__.py +115 -292
- rootfig-0.2.2/examples/gallery/__main__.py +17 -0
- rootfig-0.2.2/examples/gallery/data.py +106 -0
- rootfig-0.2.2/examples/gallery/registry.py +103 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/pyproject.toml +1 -1
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/__init__.py +1 -1
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/api.py +4 -2
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/figure.py +68 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/style.py +79 -7
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_api.py +62 -7
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_gallery.py +71 -9
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_plotting.py +178 -0
- rootfig-0.2.0/README.md +0 -168
- rootfig-0.2.0/docs/hooks/gallery.py +0 -63
- rootfig-0.2.0/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.2.0/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.2.0/docs/images/gallery/xbreak_ratio.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/.gitignore +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/LICENSE +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/api.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/composable.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/ecosystem.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/expressions.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/arrays.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/cms_density.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/correlation.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/efficiency.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/expressions.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/fill_stats.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/hist2d.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/log_axes.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/luminosity.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/many_plots.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/object_vs_event.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/profile.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/quick.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/robust_range.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/stack_data.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/images/gallery/style_colors.png +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/index.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/docs/quickstart.md +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/mkdocs.yml +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/_typing.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/errors.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/expressions/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/expressions/functions.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/expressions/parser.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/build.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/cutflow.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/efficiency.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/normalize.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/pipeline.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/ratio.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/histograms/stats.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/io/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/io/sources.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/binning.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/cuts.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/samples.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/style.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/units.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/model/variables.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/annotations.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/correlation.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/hist1d.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/hist2d.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/points.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/ratio.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/plotting/result.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/py.typed +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/selection/__init__.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/src/rootfig/selection/columns.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/conftest.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/data/split_collection.root +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_expressions.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_histograms.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_io.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_model.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_selection.py +0 -0
- {rootfig-0.2.0 → rootfig-0.2.2}/tests/test_tutorials.py +0 -0
|
@@ -89,7 +89,9 @@ f.Close()
|
|
|
89
89
|
|
|
90
90
|
## Figures and the gallery
|
|
91
91
|
|
|
92
|
-
`examples/gallery
|
|
92
|
+
The `examples/gallery` package is both the showcase and the image-regression
|
|
93
|
+
suite: `__init__.py` holds the shared `define()` block and the examples, `data.py`
|
|
94
|
+
writes the toy files and `registry.py` extracts the source shown in the docs. Each
|
|
93
95
|
example is a small function returning a `Plot`; `docs/gallery.md` shows every
|
|
94
96
|
figure next to that function's source (a MkDocs hook, `docs/hooks/gallery.py`),
|
|
95
97
|
and `tests/test_gallery.py` renders all of them and, with `--mpl`, compares
|
|
@@ -97,7 +99,7 @@ them pixel-wise (pytest-mpl, RMS tolerance 2) against `docs/images/gallery/`.
|
|
|
97
99
|
Those PNGs are therefore the documentation images *and* the baselines.
|
|
98
100
|
|
|
99
101
|
```bash
|
|
100
|
-
MPLBACKEND=Agg uv run python examples/gallery
|
|
102
|
+
MPLBACKEND=Agg uv run python examples/gallery # look at examples/out/*.png
|
|
101
103
|
uv run pytest tests/test_gallery.py --mpl # compare against the baselines
|
|
102
104
|
uv run pytest tests/test_gallery.py --mpl-generate-path=docs/images/gallery # accept changes
|
|
103
105
|
```
|
|
@@ -107,7 +109,13 @@ them by eye, and commit them with the code. CI compares on Linux only (fonts
|
|
|
107
109
|
differ elsewhere) and, when a comparison fails, uploads an HTML report with
|
|
108
110
|
baseline, result and difference images as the `mpl-results-*` artifact. To add
|
|
109
111
|
an example, register a function with `@example(name, title)` and give it a
|
|
110
|
-
docstring; the test suite fails until its baseline image exists.
|
|
112
|
+
docstring; the test suite fails until its baseline image exists. Its parameters
|
|
113
|
+
are attribute names of `Dataset`, it runs inside the directory holding the toy
|
|
114
|
+
files (so name them `"signal.root"`, never through a variable), and the hook
|
|
115
|
+
prints only the body (blank lines and comments included) — write it as a user
|
|
116
|
+
would. Put an object into `define()` — the *Setup* block of the docs page — only
|
|
117
|
+
when several examples use it; anything a single example needs belongs in that
|
|
118
|
+
example.
|
|
111
119
|
|
|
112
120
|
## Pull requests
|
|
113
121
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rootfig
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Publication-quality figures straight from ROOT trees, without ROOT: uproot + Awkward + hist + mplhep with a TTree::Draw-like API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jbeirer/rootfig
|
|
6
|
-
Project-URL: Documentation, https://github.
|
|
6
|
+
Project-URL: Documentation, https://jbeirer.github.io/rootfig/
|
|
7
7
|
Project-URL: Repository, https://github.com/jbeirer/rootfig
|
|
8
8
|
Project-URL: Issues, https://github.com/jbeirer/rootfig/issues
|
|
9
9
|
Author-email: Joshua Falco Beirer <jbeirer@cern.ch>
|
|
@@ -35,41 +35,42 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
|
|
36
36
|
**Publication-quality figures straight from ROOT trees, without ROOT.**
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
give it ROOT files, a tree, an expression, a selection and a weight, and get a
|
|
40
|
-
styled matplotlib figure back in one call. It reads with
|
|
41
|
-
[uproot](https://github.com/scikit-hep/uproot5), computes with
|
|
42
|
-
[Awkward Array](https://github.com/scikit-hep/awkward), fills
|
|
43
|
-
[hist](https://github.com/scikit-hep/hist) histograms and draws with
|
|
44
|
-
[mplhep](https://github.com/scikit-hep/mplhep). It adds the missing glue:
|
|
45
|
-
predictable per-event/per-object selection semantics, weights, shared
|
|
46
|
-
binning across samples, normalisation, ratio panels and good defaults.
|
|
47
|
-
|
|
38
|
+
[](https://jbeirer.github.io/rootfig/)
|
|
48
39
|
[](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
|
|
49
40
|
[](https://codecov.io/gh/jbeirer/rootfig)
|
|
50
41
|
[](https://pypi.org/project/rootfig/)
|
|
51
42
|
[](https://pypi.org/project/rootfig/)
|
|
52
43
|
[](LICENSE)
|
|
53
44
|
|
|
45
|
+
**[Documentation](https://jbeirer.github.io/rootfig/) ·
|
|
46
|
+
[Gallery](https://jbeirer.github.io/rootfig/gallery/) ·
|
|
47
|
+
[Quick start](https://jbeirer.github.io/rootfig/quickstart/)**
|
|
48
|
+
|
|
49
|
+
Go from ROOT files to a styled figure in one call. Choose a variable, add a
|
|
50
|
+
selection, and plot:
|
|
51
|
+
|
|
54
52
|
```python
|
|
55
53
|
import rootfig as rf
|
|
56
54
|
|
|
57
55
|
rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
|
|
58
56
|
```
|
|
59
57
|
|
|
58
|
+
Start with a single distribution; add samples, weights, stacks and ratio
|
|
59
|
+
panels as your analysis grows. Every plot gives you a matplotlib figure to
|
|
60
|
+
customise and save. No ROOT installation required.
|
|
61
|
+
|
|
60
62
|
<p align="center">
|
|
61
|
-
<img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%">
|
|
62
|
-
<img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%">
|
|
63
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%"></a>
|
|
64
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%"></a>
|
|
63
65
|
</p>
|
|
64
66
|
<p align="center">
|
|
65
|
-
<img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%">
|
|
66
|
-
<img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%">
|
|
67
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%"></a>
|
|
68
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%"></a>
|
|
67
69
|
</p>
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
draws every example in a few seconds; the same figures are pixel-compared in CI.
|
|
71
|
+
**[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
|
|
72
|
+
See each figure alongside the code that makes it, from simple overlays to
|
|
73
|
+
stacked data/MC comparisons, broken axes and 2D histograms.
|
|
73
74
|
|
|
74
75
|
## Installation
|
|
75
76
|
|
|
@@ -82,7 +83,7 @@ uv add rootfig
|
|
|
82
83
|
Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
|
|
83
84
|
files are both supported.
|
|
84
85
|
|
|
85
|
-
##
|
|
86
|
+
## Compare samples in one call
|
|
86
87
|
|
|
87
88
|
```python
|
|
88
89
|
import rootfig as rf
|
|
@@ -100,8 +101,9 @@ rf.plot(
|
|
|
100
101
|
)
|
|
101
102
|
```
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
## Build up to a full analysis
|
|
105
|
+
|
|
106
|
+
Define samples, variables, cuts and styles once, then reuse them across plots:
|
|
105
107
|
|
|
106
108
|
```python
|
|
107
109
|
import rootfig as rf
|
|
@@ -132,56 +134,50 @@ Everything you get back is a standard object: `p.fig` and `p.ax` are
|
|
|
132
134
|
matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
|
|
133
135
|
`rf.load(...)` returns Awkward arrays.
|
|
134
136
|
|
|
135
|
-
##
|
|
136
|
-
|
|
137
|
-
- **
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- **
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- **
|
|
144
|
-
|
|
145
|
-
- **
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
(`Sample(xsec="0.2 pb", ngen="eventsProcessed")`, `lumi="10.8 ab^-1"`).
|
|
152
|
-
- **Analysis tables and panels**: cut flows with yields and efficiencies,
|
|
153
|
-
significance panels (S/√B), efficiency-versus-variable plots with binomial
|
|
154
|
-
intervals, profiles and resolutions.
|
|
155
|
-
- **Experiment-neutral defaults**, with mplhep styles and labels for ATLAS,
|
|
156
|
-
CMS, LHCb, ALICE and DUNE one keyword away; any other experiment name, GeV
|
|
157
|
-
and ab⁻¹ work too.
|
|
158
|
-
- **EDM4hep-friendly**: sub-branches of split collections are addressed as
|
|
159
|
-
`ReconstructedParticles.momentum.x`, with `pt`, `p`, `theta`, `costheta`,
|
|
160
|
-
`eta`, `phi` and `mass` helpers.
|
|
161
|
-
- **Also**: 2D histograms, summary statistics tables, statistics boxes,
|
|
162
|
-
correlation matrices, multi-file globs, entry ranges for quick looks.
|
|
137
|
+
## What you can do
|
|
138
|
+
|
|
139
|
+
- **Select events and objects with readable expressions.** Write cuts such as
|
|
140
|
+
`count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
|
|
141
|
+
explicit rules, and event weights carry through to each selected object.
|
|
142
|
+
- **Compare samples with a few keywords.** Overlays, stacks, data points and
|
|
143
|
+
ratio panels share binning and propagate histogram uncertainties.
|
|
144
|
+
Normalise to unity, density, bin width or luminosity.
|
|
145
|
+
- **Style figures for your analysis.** Add experiment labels, units, log axes
|
|
146
|
+
and broken axes, then refine the result with matplotlib.
|
|
147
|
+
- **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
|
|
148
|
+
profiles, resolutions and significance panels; produce cut flows and
|
|
149
|
+
summary statistics from the same inputs.
|
|
150
|
+
- **Work directly with your files.** Read `TTree` and `RNTuple` data, combine
|
|
151
|
+
files with globs, limit entry ranges for quick checks, and use EDM4hep
|
|
152
|
+
split collections. Only the branches your expressions need are read.
|
|
163
153
|
|
|
164
154
|
## Documentation
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- [
|
|
156
|
+
**[Read the docs](https://jbeirer.github.io/rootfig/)** or
|
|
157
|
+
**[browse the gallery](https://jbeirer.github.io/rootfig/gallery/)** for examples
|
|
158
|
+
with figures and code.
|
|
159
|
+
|
|
160
|
+
- [Quick start](https://jbeirer.github.io/rootfig/quickstart/): your first plot, selections and weights.
|
|
161
|
+
- [Expressions and selections](https://jbeirer.github.io/rootfig/expressions/): syntax and event/object rules.
|
|
162
|
+
- [Samples, variables, cuts and styles](https://jbeirer.github.io/rootfig/composable/): reusable analysis definitions.
|
|
163
|
+
- [Plotting options](https://jbeirer.github.io/rootfig/plotting/): binning, normalisation, panels and styling.
|
|
164
|
+
- [API reference](https://jbeirer.github.io/rootfig/api/): full signatures and options.
|
|
171
165
|
|
|
172
166
|
## Relation to the ecosystem
|
|
173
167
|
|
|
174
|
-
`rootfig`
|
|
168
|
+
`rootfig` brings a `TTree::Draw`-like workflow to the Scientific Python HEP
|
|
169
|
+
stack, building on familiar libraries:
|
|
175
170
|
|
|
176
171
|
| Task | Library | What rootfig adds |
|
|
177
172
|
| --- | --- | --- |
|
|
178
|
-
| Reading ROOT files | uproot | file globs, tree auto-detection, reading only the required branches |
|
|
179
|
-
| Jagged arrays | Awkward Array | the per-event/per-object rules for cuts and weights |
|
|
180
|
-
| Histograms | hist / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
|
|
181
|
-
| Drawing | mplhep + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
|
|
173
|
+
| Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
|
|
174
|
+
| Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
|
|
175
|
+
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
|
|
176
|
+
| Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
|
|
182
177
|
|
|
183
178
|
If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
|
|
184
|
-
the same options. If you want the arrays, `rf.load` returns them.
|
|
179
|
+
the same options. If you want the arrays, `rf.load` returns them. See
|
|
180
|
+
[the ecosystem guide](https://jbeirer.github.io/rootfig/ecosystem/) for details.
|
|
185
181
|
|
|
186
182
|
## Development
|
|
187
183
|
|
rootfig-0.2.2/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# rootfig
|
|
2
|
+
|
|
3
|
+
**Publication-quality figures straight from ROOT trees, without ROOT.**
|
|
4
|
+
|
|
5
|
+
[](https://jbeirer.github.io/rootfig/)
|
|
6
|
+
[](https://github.com/jbeirer/rootfig/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/jbeirer/rootfig)
|
|
8
|
+
[](https://pypi.org/project/rootfig/)
|
|
9
|
+
[](https://pypi.org/project/rootfig/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
**[Documentation](https://jbeirer.github.io/rootfig/) ·
|
|
13
|
+
[Gallery](https://jbeirer.github.io/rootfig/gallery/) ·
|
|
14
|
+
[Quick start](https://jbeirer.github.io/rootfig/quickstart/)**
|
|
15
|
+
|
|
16
|
+
Go from ROOT files to a styled figure in one call. Choose a variable, add a
|
|
17
|
+
selection, and plot:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import rootfig as rf
|
|
21
|
+
|
|
22
|
+
rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Start with a single distribution; add samples, weights, stacks and ratio
|
|
26
|
+
panels as your analysis grows. Every plot gives you a matplotlib figure to
|
|
27
|
+
customise and save. No ROOT installation required.
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/stack_data.png" alt="Stacked simulation with data and a ratio panel" width="48%"></a>
|
|
31
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="48%"></a>
|
|
32
|
+
</p>
|
|
33
|
+
<p align="center">
|
|
34
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/object_vs_event.png" alt="Per-object versus per-event selections" width="48%"></a>
|
|
35
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="48%"></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
**[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
|
|
39
|
+
See each figure alongside the code that makes it, from simple overlays to
|
|
40
|
+
stacked data/MC comparisons, broken axes and 2D histograms.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install rootfig
|
|
46
|
+
# or
|
|
47
|
+
uv add rootfig
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Python 3.12 or newer. No ROOT installation is needed; `TTree` and `RNTuple`
|
|
51
|
+
files are both supported.
|
|
52
|
+
|
|
53
|
+
## Compare samples in one call
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import rootfig as rf
|
|
57
|
+
|
|
58
|
+
# Overlay two samples, normalised to unity, with a ratio panel.
|
|
59
|
+
rf.plot(
|
|
60
|
+
["signal.root", "background.root"],
|
|
61
|
+
"Muon_pt",
|
|
62
|
+
tree="events",
|
|
63
|
+
selection="abs(Muon_eta) < 2.5",
|
|
64
|
+
weight="event_weight",
|
|
65
|
+
bins=(50, 0, 200),
|
|
66
|
+
normalize=True,
|
|
67
|
+
ratio=True,
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Build up to a full analysis
|
|
72
|
+
|
|
73
|
+
Define samples, variables, cuts and styles once, then reuse them across plots:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import rootfig as rf
|
|
77
|
+
|
|
78
|
+
signal = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight")
|
|
79
|
+
background = rf.Sample("bkg.root", tree="events", label="Background", weight="mc_weight")
|
|
80
|
+
data = rf.Sample("data.root", tree="events", label="Data", is_data=True)
|
|
81
|
+
|
|
82
|
+
pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
|
|
83
|
+
baseline = rf.Cut("nMuon >= 1") & "abs(Muon_eta) < 2.5"
|
|
84
|
+
style = rf.Style(experiment="ATLAS", status="Internal", lumi=140, com=13.6)
|
|
85
|
+
|
|
86
|
+
p = rf.plot(
|
|
87
|
+
[background, signal],
|
|
88
|
+
pt,
|
|
89
|
+
observed=data,
|
|
90
|
+
selection=baseline,
|
|
91
|
+
stack=True,
|
|
92
|
+
ratio=True,
|
|
93
|
+
logy=True,
|
|
94
|
+
style=style,
|
|
95
|
+
)
|
|
96
|
+
p.ax.set_ylim(top=1e5) # it is a normal matplotlib Axes
|
|
97
|
+
p.save("muon_pt.pdf")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Everything you get back is a standard object: `p.fig` and `p.ax` are
|
|
101
|
+
matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
|
|
102
|
+
`rf.load(...)` returns Awkward arrays.
|
|
103
|
+
|
|
104
|
+
## What you can do
|
|
105
|
+
|
|
106
|
+
- **Select events and objects with readable expressions.** Write cuts such as
|
|
107
|
+
`count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
|
|
108
|
+
explicit rules, and event weights carry through to each selected object.
|
|
109
|
+
- **Compare samples with a few keywords.** Overlays, stacks, data points and
|
|
110
|
+
ratio panels share binning and propagate histogram uncertainties.
|
|
111
|
+
Normalise to unity, density, bin width or luminosity.
|
|
112
|
+
- **Style figures for your analysis.** Add experiment labels, units, log axes
|
|
113
|
+
and broken axes, then refine the result with matplotlib.
|
|
114
|
+
- **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
|
|
115
|
+
profiles, resolutions and significance panels; produce cut flows and
|
|
116
|
+
summary statistics from the same inputs.
|
|
117
|
+
- **Work directly with your files.** Read `TTree` and `RNTuple` data, combine
|
|
118
|
+
files with globs, limit entry ranges for quick checks, and use EDM4hep
|
|
119
|
+
split collections. Only the branches your expressions need are read.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
**[Read the docs](https://jbeirer.github.io/rootfig/)** or
|
|
124
|
+
**[browse the gallery](https://jbeirer.github.io/rootfig/gallery/)** for examples
|
|
125
|
+
with figures and code.
|
|
126
|
+
|
|
127
|
+
- [Quick start](https://jbeirer.github.io/rootfig/quickstart/): your first plot, selections and weights.
|
|
128
|
+
- [Expressions and selections](https://jbeirer.github.io/rootfig/expressions/): syntax and event/object rules.
|
|
129
|
+
- [Samples, variables, cuts and styles](https://jbeirer.github.io/rootfig/composable/): reusable analysis definitions.
|
|
130
|
+
- [Plotting options](https://jbeirer.github.io/rootfig/plotting/): binning, normalisation, panels and styling.
|
|
131
|
+
- [API reference](https://jbeirer.github.io/rootfig/api/): full signatures and options.
|
|
132
|
+
|
|
133
|
+
## Relation to the ecosystem
|
|
134
|
+
|
|
135
|
+
`rootfig` brings a `TTree::Draw`-like workflow to the Scientific Python HEP
|
|
136
|
+
stack, building on familiar libraries:
|
|
137
|
+
|
|
138
|
+
| Task | Library | What rootfig adds |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
|
|
141
|
+
| Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
|
|
142
|
+
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
|
|
143
|
+
| Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
|
|
144
|
+
|
|
145
|
+
If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
|
|
146
|
+
the same options. If you want the arrays, `rf.load` returns them. See
|
|
147
|
+
[the ecosystem guide](https://jbeirer.github.io/rootfig/ecosystem/) for details.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
git clone https://github.com/jbeirer/rootfig
|
|
153
|
+
cd rootfig
|
|
154
|
+
uv sync --all-groups
|
|
155
|
+
uv run pytest
|
|
156
|
+
uv run ruff check . && uv run ruff format --check .
|
|
157
|
+
uv run mypy
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
# Gallery
|
|
2
2
|
|
|
3
3
|
Every figure on this page is made by
|
|
4
|
-
[`examples/gallery
|
|
4
|
+
[`examples/gallery`](https://github.com/jbeirer/rootfig/blob/main/examples/gallery/__init__.py).
|
|
5
5
|
The script writes a toy dataset (three simulated processes and one "observed"
|
|
6
|
-
sample with muons, jets and event-level quantities, as `TTree`s)
|
|
7
|
-
examples in a few seconds:
|
|
6
|
+
sample with muons, jets and event-level quantities, as `TTree`s) into a
|
|
7
|
+
directory and runs all examples there in a few seconds:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
python examples/gallery
|
|
10
|
+
python examples/gallery # everything ends up in examples/out/
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The code next to each figure is the source of that example, and the image is
|
|
14
14
|
the reference picture the test suite compares against, so what you see is what
|
|
15
|
-
the current release draws.
|
|
15
|
+
the current release draws. File names such as `signal.root` are those toy
|
|
16
|
+
files, relative to that directory, and every example assumes
|
|
16
17
|
|
|
17
18
|
```python
|
|
18
|
-
import matplotlib.pyplot as plt
|
|
19
|
-
import numpy as np
|
|
20
|
-
|
|
21
19
|
import rootfig as rf
|
|
22
20
|
```
|
|
23
21
|
|
|
22
|
+
<!-- gallery: quick -->
|
|
23
|
+
|
|
24
24
|
## Setup
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
Plain strings and `(bins, low, high)` tuples are accepted everywhere, as the
|
|
27
|
+
one-liner shows. Once you draw more than one plot it pays to name the pieces:
|
|
28
|
+
`Sample`, `Variable`, `Cut` and `Style` are small frozen dataclasses (see
|
|
29
|
+
[Samples, variables, cuts and styles](composable.md)). The rest of this page
|
|
30
|
+
shares four samples, three variables and one style, defined once:
|
|
28
31
|
|
|
29
32
|
<!-- gallery-setup -->
|
|
30
33
|
|
|
34
|
+
Anything a single example needs is defined inside that example, so every block
|
|
35
|
+
below is complete given the names above (plus `matplotlib.pyplot as plt` and
|
|
36
|
+
`numpy as np` where they appear).
|
|
37
|
+
|
|
31
38
|
<!-- gallery -->
|
|
32
39
|
|
|
33
40
|
## Beyond figures
|
|
@@ -38,21 +45,20 @@ The same inputs feed tables and arrays:
|
|
|
38
45
|
# entries, mean, std, sem, skewness, min, max per sample and variable
|
|
39
46
|
print(rf.summarize(mc, ["MET", "Muon_pt"], selection="nMuon > 0"))
|
|
40
47
|
|
|
41
|
-
# a cut flow: yields
|
|
48
|
+
# a cut flow: yields, raw counts and step efficiencies per sample
|
|
42
49
|
print(
|
|
43
50
|
rf.cutflow(
|
|
44
|
-
|
|
51
|
+
mc,
|
|
45
52
|
["nMuon >= 2", rf.Cut("MET > 50", label="MET > 50 GeV"), "any(Jet_btag > 0.8)"],
|
|
46
|
-
lumi="10.8 ab^-1",
|
|
47
53
|
)
|
|
48
54
|
)
|
|
49
55
|
|
|
50
56
|
# evaluated expressions as an Awkward record array
|
|
51
|
-
events = rf.load(
|
|
57
|
+
events = rf.load(signal, ["MET", "count(Muon_pt)", "first(Muon_pt)"], selection="nJet >= 2")
|
|
52
58
|
events["MET"]
|
|
53
59
|
|
|
54
60
|
# a plain hist.Hist to feed into your own code
|
|
55
|
-
h = rf.histogram(
|
|
61
|
+
h = rf.histogram(signal, "MET", bins=(40, 0, 400), selection="nJet >= 2")
|
|
56
62
|
```
|
|
57
63
|
|
|
58
64
|
Cuts compose with `&`, `|` and `~`, carry optional labels, and combine with
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""MkDocs hook that fills ``docs/gallery.md`` from the ``examples/gallery`` package.
|
|
2
|
+
|
|
3
|
+
Two kinds of marker are expanded when the page is rendered:
|
|
4
|
+
|
|
5
|
+
``<!-- gallery-setup -->``
|
|
6
|
+
the body of ``define()``: the samples, variables and style shared by the
|
|
7
|
+
examples;
|
|
8
|
+
``<!-- gallery -->`` / ``<!-- gallery: name ... -->``
|
|
9
|
+
one section per example with its title, description, image
|
|
10
|
+
(``docs/images/gallery/<name>.png``) and the source of the example
|
|
11
|
+
function. A marker naming examples renders exactly those, so a few can be
|
|
12
|
+
shown before the setup section; a bare marker renders everything that no
|
|
13
|
+
earlier marker on the page has shown yet, in registration order.
|
|
14
|
+
|
|
15
|
+
The gallery module is imported, never executed, so building the docs needs no
|
|
16
|
+
data and draws nothing. Because the images are also the baselines of
|
|
17
|
+
``tests/test_gallery.py``, picture and code cannot drift apart.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib.util
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from types import ModuleType
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
30
|
+
GALLERY_DIR = ROOT / "examples" / "gallery"
|
|
31
|
+
SETUP_MARKER = "<!-- gallery-setup -->"
|
|
32
|
+
GALLERY_MARKER = re.compile(r"<!-- gallery(?::\s*(?P<names>[\w\s,]+?))?\s*-->")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_gallery(name: str = "rootfig_gallery_docs") -> ModuleType:
|
|
36
|
+
"""Import ``examples/gallery`` as a fresh package, without ``examples/`` on ``sys.path``.
|
|
37
|
+
|
|
38
|
+
Every call re-executes the package (``mkdocs serve`` renders the page on each
|
|
39
|
+
rebuild), so its submodules are evicted from ``sys.modules`` first: a cached
|
|
40
|
+
``registry`` would keep its ``EXAMPLES`` list and the re-run decorators would
|
|
41
|
+
register every example a second time.
|
|
42
|
+
"""
|
|
43
|
+
for cached in [m for m in sys.modules if m == name or m.startswith(f"{name}.")]:
|
|
44
|
+
del sys.modules[cached]
|
|
45
|
+
spec = importlib.util.spec_from_file_location(
|
|
46
|
+
name, GALLERY_DIR / "__init__.py", submodule_search_locations=[str(GALLERY_DIR)]
|
|
47
|
+
)
|
|
48
|
+
if spec is None or spec.loader is None:
|
|
49
|
+
msg = f"cannot load {GALLERY_DIR}"
|
|
50
|
+
raise RuntimeError(msg)
|
|
51
|
+
module = importlib.util.module_from_spec(spec)
|
|
52
|
+
sys.modules[name] = module
|
|
53
|
+
spec.loader.exec_module(module)
|
|
54
|
+
return module
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_section(gallery: ModuleType, example: Any) -> str:
|
|
58
|
+
"""Markdown for one example: heading, description, image, code."""
|
|
59
|
+
code = gallery.body_source(example.func)
|
|
60
|
+
return (
|
|
61
|
+
f"## {example.title}\n\n"
|
|
62
|
+
f"{example.description}\n\n"
|
|
63
|
+
f'{{ width="75%" }}\n\n'
|
|
64
|
+
f"```python\n{code}```\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def expand_markers(markdown: str, gallery: ModuleType) -> str:
|
|
69
|
+
"""Replace every gallery marker, each example going to the first marker that asks."""
|
|
70
|
+
shown: set[str] = set()
|
|
71
|
+
by_name = {example.name: example for example in gallery.EXAMPLES}
|
|
72
|
+
|
|
73
|
+
def replace(match: re.Match[str]) -> str:
|
|
74
|
+
names = match.group("names")
|
|
75
|
+
if names is None:
|
|
76
|
+
wanted = [ex.name for ex in gallery.EXAMPLES if ex.name not in shown]
|
|
77
|
+
else:
|
|
78
|
+
asked = names.replace(",", " ").split()
|
|
79
|
+
unknown = [name for name in asked if name not in by_name]
|
|
80
|
+
if unknown:
|
|
81
|
+
msg = f"unknown gallery example(s) {unknown} in {match.group(0)!r}"
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
# first marker wins: skip what an earlier marker (or this one) already rendered
|
|
84
|
+
wanted = list(dict.fromkeys(name for name in asked if name not in shown))
|
|
85
|
+
shown.update(wanted)
|
|
86
|
+
return "\n".join(render_section(gallery, by_name[name]) for name in wanted)
|
|
87
|
+
|
|
88
|
+
return GALLERY_MARKER.sub(replace, markdown)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def on_page_markdown(markdown: str, **_: Any) -> str:
|
|
92
|
+
"""Expand the gallery markers (MkDocs ``on_page_markdown`` event)."""
|
|
93
|
+
if SETUP_MARKER not in markdown and not GALLERY_MARKER.search(markdown):
|
|
94
|
+
return markdown
|
|
95
|
+
gallery = _load_gallery()
|
|
96
|
+
setup = gallery.body_source(gallery.define, returns="omit")
|
|
97
|
+
markdown = markdown.replace(SETUP_MARKER, f"```python\n{setup}```")
|
|
98
|
+
return expand_markers(markdown, gallery)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -79,7 +79,10 @@ the signal. The values are returned as a `Ratio` in `Plot.ratios`.
|
|
|
79
79
|
|
|
80
80
|
`ratio_ylim` and
|
|
81
81
|
`ratio_label` override the automatic range (at least 0.5 to 1.5, widened to
|
|
82
|
-
cover the bulk of the points) and label (`Ratio to X` or `Data / MC`).
|
|
82
|
+
cover the bulk of the points) and label (`Ratio to X` or `Data / MC`). A
|
|
83
|
+
rotated y label is bounded by the height of the short ratio panel, so a long
|
|
84
|
+
one is shrunk and, if that is not enough, wrapped onto two lines; pass a
|
|
85
|
+
shorter `ratio_label` such as `"Ratio"` to keep it at full size. The
|
|
83
86
|
computed values are returned in `Plot.ratios` as
|
|
84
87
|
[`Ratio`][rootfig.Ratio] objects (`values`, `errors`, `band`, `edges`).
|
|
85
88
|
|
|
@@ -134,6 +137,10 @@ computed values are returned in `Plot.ratios` as
|
|
|
134
137
|
later renders exactly the layout that was computed, in the style's fonts.
|
|
135
138
|
- `Plot.fig`, `Plot.ax`, `Plot.ratio_ax` are plain matplotlib objects;
|
|
136
139
|
`Plot.histograms` wrap the `hist.Hist` objects with labels and statistics.
|
|
140
|
+
- In a notebook the figure is displayed automatically — it is a pyplot figure,
|
|
141
|
+
flushed by the inline backend at the end of the cell, so `%matplotlib inline`
|
|
142
|
+
is not needed. End the call with `;` to hide the `Plot` repr, and use
|
|
143
|
+
`Plot.close()` in loops that make many figures.
|
|
137
144
|
|
|
138
145
|
## 2D histograms and correlations
|
|
139
146
|
|