rootfig 0.1.0__tar.gz → 0.2.1__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.1.0 → rootfig-0.2.1}/.gitignore +1 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/CONTRIBUTING.md +20 -11
- {rootfig-0.1.0 → rootfig-0.2.1}/PKG-INFO +7 -8
- {rootfig-0.1.0 → rootfig-0.2.1}/README.md +1 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/composable.md +8 -3
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/ecosystem.md +4 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/expressions.md +5 -3
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/gallery.md +21 -15
- rootfig-0.2.1/docs/hooks/gallery.py +98 -0
- rootfig-0.2.1/docs/images/gallery/arrays.png +0 -0
- rootfig-0.2.1/docs/images/gallery/cms_density.png +0 -0
- rootfig-0.2.1/docs/images/gallery/correlation.png +0 -0
- rootfig-0.2.1/docs/images/gallery/efficiency.png +0 -0
- rootfig-0.2.1/docs/images/gallery/expressions.png +0 -0
- rootfig-0.2.1/docs/images/gallery/fill_stats.png +0 -0
- rootfig-0.2.1/docs/images/gallery/hist2d.png +0 -0
- rootfig-0.2.1/docs/images/gallery/log_axes.png +0 -0
- rootfig-0.2.1/docs/images/gallery/luminosity.png +0 -0
- rootfig-0.2.1/docs/images/gallery/many_plots.png +0 -0
- rootfig-0.2.1/docs/images/gallery/object_vs_event.png +0 -0
- rootfig-0.2.1/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.2.1/docs/images/gallery/profile.png +0 -0
- rootfig-0.2.1/docs/images/gallery/quick.png +0 -0
- rootfig-0.2.1/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.2.1/docs/images/gallery/robust_range.png +0 -0
- rootfig-0.2.1/docs/images/gallery/stack_data.png +0 -0
- rootfig-0.2.1/docs/images/gallery/style_colors.png +0 -0
- rootfig-0.2.1/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.2.1/docs/images/gallery/xbreak_ratio.png +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/plotting.md +41 -7
- rootfig-0.1.0/examples/gallery.py → rootfig-0.2.1/examples/gallery/__init__.py +115 -292
- rootfig-0.2.1/examples/gallery/__main__.py +17 -0
- rootfig-0.2.1/examples/gallery/data.py +106 -0
- rootfig-0.2.1/examples/gallery/registry.py +103 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/pyproject.toml +9 -7
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/__init__.py +1 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/api.py +66 -24
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/parser.py +4 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/__init__.py +2 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/build.py +66 -15
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/cutflow.py +14 -7
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/efficiency.py +20 -12
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/normalize.py +59 -21
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/pipeline.py +50 -8
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/stats.py +16 -4
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/io/sources.py +26 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/__init__.py +1 -2
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/binning.py +7 -2
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/samples.py +65 -22
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/variables.py +14 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/__init__.py +4 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/correlation.py +4 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/figure.py +94 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/hist1d.py +1 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/hist2d.py +15 -4
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/result.py +9 -7
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/style.py +179 -22
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/selection/columns.py +20 -3
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_api.py +288 -8
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_gallery.py +72 -10
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_histograms.py +209 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_io.py +14 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_model.py +52 -4
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_plotting.py +188 -1
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_selection.py +24 -0
- rootfig-0.1.0/CHANGELOG.md +0 -87
- rootfig-0.1.0/docs/hooks/gallery.py +0 -63
- rootfig-0.1.0/docs/images/gallery/arrays.png +0 -0
- rootfig-0.1.0/docs/images/gallery/cms_density.png +0 -0
- rootfig-0.1.0/docs/images/gallery/correlation.png +0 -0
- rootfig-0.1.0/docs/images/gallery/efficiency.png +0 -0
- rootfig-0.1.0/docs/images/gallery/expressions.png +0 -0
- rootfig-0.1.0/docs/images/gallery/fill_stats.png +0 -0
- rootfig-0.1.0/docs/images/gallery/hist2d.png +0 -0
- rootfig-0.1.0/docs/images/gallery/log_axes.png +0 -0
- rootfig-0.1.0/docs/images/gallery/luminosity.png +0 -0
- rootfig-0.1.0/docs/images/gallery/many_plots.png +0 -0
- rootfig-0.1.0/docs/images/gallery/object_vs_event.png +0 -0
- rootfig-0.1.0/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.1.0/docs/images/gallery/profile.png +0 -0
- rootfig-0.1.0/docs/images/gallery/quick.png +0 -0
- rootfig-0.1.0/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.1.0/docs/images/gallery/robust_range.png +0 -0
- rootfig-0.1.0/docs/images/gallery/stack_data.png +0 -0
- rootfig-0.1.0/docs/images/gallery/style_colors.png +0 -0
- rootfig-0.1.0/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.1.0/docs/images/gallery/xbreak_ratio.png +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/LICENSE +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/api.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/index.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/docs/quickstart.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/mkdocs.yml +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/_typing.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/errors.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/functions.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/ratio.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/io/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/cuts.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/style.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/units.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/annotations.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/points.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/ratio.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/py.typed +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/selection/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/conftest.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/data/split_collection.root +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_expressions.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_tutorials.py +0 -0
|
@@ -26,8 +26,10 @@ uv build && uvx twine check dist/* # packaging
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
CI runs all of these on Python 3.12, 3.13 and 3.14 (Linux), plus macOS
|
|
29
|
-
on 3.13
|
|
30
|
-
`
|
|
29
|
+
on 3.13, and once more with every direct dependency at the minimum version
|
|
30
|
+
declared in `pyproject.toml` (`uv sync --resolution lowest-direct`). Tests
|
|
31
|
+
that use ROOT's tutorial files run only when `root-config` is available
|
|
32
|
+
locally and are skipped otherwise; do not add
|
|
31
33
|
tests that require ROOT or network access.
|
|
32
34
|
|
|
33
35
|
## Layout
|
|
@@ -87,7 +89,9 @@ f.Close()
|
|
|
87
89
|
|
|
88
90
|
## Figures and the gallery
|
|
89
91
|
|
|
90
|
-
`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
|
|
91
95
|
example is a small function returning a `Plot`; `docs/gallery.md` shows every
|
|
92
96
|
figure next to that function's source (a MkDocs hook, `docs/hooks/gallery.py`),
|
|
93
97
|
and `tests/test_gallery.py` renders all of them and, with `--mpl`, compares
|
|
@@ -95,7 +99,7 @@ them pixel-wise (pytest-mpl, RMS tolerance 2) against `docs/images/gallery/`.
|
|
|
95
99
|
Those PNGs are therefore the documentation images *and* the baselines.
|
|
96
100
|
|
|
97
101
|
```bash
|
|
98
|
-
MPLBACKEND=Agg uv run python examples/gallery
|
|
102
|
+
MPLBACKEND=Agg uv run python examples/gallery # look at examples/out/*.png
|
|
99
103
|
uv run pytest tests/test_gallery.py --mpl # compare against the baselines
|
|
100
104
|
uv run pytest tests/test_gallery.py --mpl-generate-path=docs/images/gallery # accept changes
|
|
101
105
|
```
|
|
@@ -105,24 +109,29 @@ them by eye, and commit them with the code. CI compares on Linux only (fonts
|
|
|
105
109
|
differ elsewhere) and, when a comparison fails, uploads an HTML report with
|
|
106
110
|
baseline, result and difference images as the `mpl-results-*` artifact. To add
|
|
107
111
|
an example, register a function with `@example(name, title)` and give it a
|
|
108
|
-
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.
|
|
109
119
|
|
|
110
120
|
## Pull requests
|
|
111
121
|
|
|
112
122
|
- Add tests for behaviour changes; unit tests assert histogram contents and
|
|
113
123
|
matplotlib structure. Rendered output is covered by the gallery (below).
|
|
114
|
-
- Update `CHANGELOG.md` under "Unreleased".
|
|
115
124
|
- Run the checks above before pushing; `pre-commit run --all-files` does most
|
|
116
125
|
of it.
|
|
117
126
|
|
|
118
127
|
## Releasing
|
|
119
128
|
|
|
120
|
-
1. Update the version in `src/rootfig/__init__.py
|
|
121
|
-
entries under a new heading.
|
|
129
|
+
1. Update the version in `src/rootfig/__init__.py`.
|
|
122
130
|
2. Commit, tag `vX.Y.Z`, push the tag.
|
|
123
|
-
3. The `release.yml` workflow builds the distribution
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
3. The `release.yml` workflow builds the distribution, refuses a tag that does
|
|
132
|
+
not match `rootfig.__version__`, and publishes it to PyPI via Trusted
|
|
133
|
+
Publishing (configure the publisher on PyPI first: repository
|
|
134
|
+
`jbeirer/rootfig`, workflow `release.yml`, environment `pypi`).
|
|
126
135
|
|
|
127
136
|
The documentation is published by the `docs` job of `ci.yml` on every push to
|
|
128
137
|
`main` (`mkdocs gh-deploy` to the `gh-pages` branch). Once, in the repository
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rootfig
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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
6
|
Project-URL: Documentation, https://github.com/jbeirer/rootfig#readme
|
|
7
7
|
Project-URL: Repository, https://github.com/jbeirer/rootfig
|
|
8
8
|
Project-URL: Issues, https://github.com/jbeirer/rootfig/issues
|
|
9
|
-
Project-URL: Changelog, https://github.com/jbeirer/rootfig/blob/main/CHANGELOG.md
|
|
10
9
|
Author-email: Joshua Falco Beirer <jbeirer@cern.ch>
|
|
11
10
|
License-Expression: MIT
|
|
12
11
|
License-File: LICENSE
|
|
@@ -24,12 +23,12 @@ Classifier: Topic :: Scientific/Engineering :: Physics
|
|
|
24
23
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
25
24
|
Classifier: Typing :: Typed
|
|
26
25
|
Requires-Python: >=3.12
|
|
27
|
-
Requires-Dist: awkward>=2.
|
|
28
|
-
Requires-Dist: hist>=2.7
|
|
29
|
-
Requires-Dist: matplotlib>=3.
|
|
30
|
-
Requires-Dist: mplhep>=
|
|
26
|
+
Requires-Dist: awkward>=2.8
|
|
27
|
+
Requires-Dist: hist>=2.7.2
|
|
28
|
+
Requires-Dist: matplotlib>=3.11
|
|
29
|
+
Requires-Dist: mplhep>=1.3
|
|
31
30
|
Requires-Dist: numpy>=1.26
|
|
32
|
-
Requires-Dist: uproot>=5.
|
|
31
|
+
Requires-Dist: uproot>=5.7.4
|
|
33
32
|
Description-Content-Type: text/markdown
|
|
34
33
|
|
|
35
34
|
# rootfig
|
|
@@ -69,7 +68,7 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
|
|
|
69
68
|
|
|
70
69
|
These and a dozen more figures, each next to the code that made it, are in the
|
|
71
70
|
[gallery](https://jbeirer.github.io/rootfig/gallery/). All of them come from
|
|
72
|
-
[`examples/gallery
|
|
71
|
+
[`examples/gallery`](examples/gallery/__init__.py), which writes toy ROOT files and
|
|
73
72
|
draws every example in a few seconds; the same figures are pixel-compared in CI.
|
|
74
73
|
|
|
75
74
|
## Installation
|
|
@@ -35,7 +35,7 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
|
|
|
35
35
|
|
|
36
36
|
These and a dozen more figures, each next to the code that made it, are in the
|
|
37
37
|
[gallery](https://jbeirer.github.io/rootfig/gallery/). All of them come from
|
|
38
|
-
[`examples/gallery
|
|
38
|
+
[`examples/gallery`](examples/gallery/__init__.py), which writes toy ROOT files and
|
|
39
39
|
draws every example in a few seconds; the same figures are pixel-compared in CI.
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
@@ -44,8 +44,12 @@ mem = rf.Sample({"x": awkward_array, "w": weights}, label="in memory")
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Without a luminosity such samples raise a [`LuminosityError`][rootfig.LuminosityError].
|
|
47
|
-
- `entry_start`/`entry_stop` restrict reading for quick looks at large files
|
|
48
|
-
|
|
47
|
+
- `entry_start`/`entry_stop` restrict reading for quick looks at large files
|
|
48
|
+
(a plot reads every needed branch of every file into memory at once). For a
|
|
49
|
+
ready-made `FileSource`, give the range to the source itself; passing it to
|
|
50
|
+
`Sample` afterwards raises a [`SourceError`][rootfig.SourceError].
|
|
51
|
+
- `sample.with_(label="...")` returns a modified copy; replacement values are
|
|
52
|
+
validated like constructor arguments.
|
|
49
53
|
|
|
50
54
|
Passing a list of files to `plot()` creates one sample per file. To merge
|
|
51
55
|
several files into *one* sample, use a glob or a `Sample`.
|
|
@@ -67,7 +71,8 @@ met = rf.Variable(
|
|
|
67
71
|
`"robust"` (ignores far outliers such as `-999` sentinels).
|
|
68
72
|
- `label` and `unit` form the axis label `label [unit]`; the unit also appears
|
|
69
73
|
in the automatic y label (`Events / 4 GeV`).
|
|
70
|
-
- `name` is used for file names by `Plot.save(directory)
|
|
74
|
+
- `name` is used for file names by `Plot.save(directory)`; it must be a plain
|
|
75
|
+
file stem (no path separators).
|
|
71
76
|
|
|
72
77
|
`bins`, `range`, `xlabel` and `unit` given to `plot()` override the variable.
|
|
73
78
|
|
|
@@ -54,6 +54,9 @@ would do to a matplotlib figure, you can do to `Plot.fig` and `Plot.ax`.
|
|
|
54
54
|
- You already have flat NumPy columns or `boost-histogram` objects and want
|
|
55
55
|
rich comparison panels: [plothist](https://plothist.readthedocs.io).
|
|
56
56
|
- You need a full columnar analysis framework with lazy, distributed
|
|
57
|
-
processing: [coffea](https://coffeateam.github.io/coffea/).
|
|
57
|
+
processing: [coffea](https://coffeateam.github.io/coffea/). rootfig reads
|
|
58
|
+
every branch a plot needs from every file into memory at once (only the
|
|
59
|
+
required branches, optionally an entry range), which suits quick looks and
|
|
60
|
+
moderate ntuples, not multi-terabyte datasets.
|
|
58
61
|
- You want to build fit templates and workspaces: [cabinetry](https://cabinetry.readthedocs.io).
|
|
59
62
|
- You want a quick terminal look at a branch: [histoprint](https://github.com/scikit-hep/histoprint).
|
|
@@ -16,9 +16,11 @@ syntax evaluated with NumPy/Awkward semantics over the branches of the tree.
|
|
|
16
16
|
| Constants | `pi`, `e`, `inf`, `nan`, `True`, `False` |
|
|
17
17
|
|
|
18
18
|
`and`, `or`, `not` and chained comparisons are rewritten to element-wise
|
|
19
|
-
operations, so they work on arrays.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
operations, so they work on arrays. Dotted names such as
|
|
20
|
+
`ReconstructedParticles.momentum.x` are read as one branch name (see below);
|
|
21
|
+
any other attribute access, lambdas, comprehensions, string literals and calls
|
|
22
|
+
to anything but the functions below are rejected at parse time with an
|
|
23
|
+
[`ExpressionError`][rootfig.ExpressionError].
|
|
22
24
|
An unknown branch raises [`MissingBranchError`][rootfig.MissingBranchError]
|
|
23
25
|
with close-match suggestions.
|
|
24
26
|
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -43,19 +43,32 @@ dataset scaled to the full one, for instance).
|
|
|
43
43
|
Variances are scaled consistently. Flow bins scale with the same factor; for
|
|
44
44
|
`"width"` and `"density"` they are divided by the width of the neighbouring
|
|
45
45
|
visible bin. Plain `hist.Hist` objects with a count storage passed to
|
|
46
|
-
`plot_histograms` are converted to `Weight` storage first.
|
|
46
|
+
`plot_histograms` are converted to `Weight` storage first. If such a histogram
|
|
47
|
+
was filled with weights (or rescaled) its sum of squared weights is lost, and
|
|
48
|
+
rootfig refuses it with a `ValueError` rather than invent uncertainties; pass
|
|
49
|
+
`assume_poisson=True` to use the absolute bin contents as variances (with a
|
|
50
|
+
warning), or fill with `hist.storage.Weight()` in the first place.
|
|
51
|
+
|
|
52
|
+
The rescaling modes divide by the signed sum of the visible bins: a histogram
|
|
53
|
+
dominated by negative weights still sums to the target, its shape flips sign,
|
|
54
|
+
and a warning says so. An empty histogram, or one whose positive and negative
|
|
55
|
+
weights cancel exactly, is left unchanged with a warning and keeps the plain
|
|
56
|
+
`Events` label (`Histogram.normalization` stays `None`).
|
|
47
57
|
|
|
48
58
|
## Ratio panel
|
|
49
59
|
|
|
50
60
|
`ratio=True` adds a lower panel sharing the x axis:
|
|
51
61
|
|
|
52
62
|
- with a stack: data / total MC, error bars from the data, grey band for the
|
|
53
|
-
MC statistical uncertainty (`ratio_uncertainty="numerator"`);
|
|
54
|
-
|
|
55
|
-
-
|
|
63
|
+
MC statistical uncertainty (`ratio_uncertainty="numerator"`); a stacked
|
|
64
|
+
ratio needs an `observed=` sample;
|
|
65
|
+
- with data and overlaid samples: data / the first non-data sample (only the
|
|
66
|
+
data appears in the panel);
|
|
67
|
+
- otherwise: every further sample / the first sample, uncertainties of both
|
|
56
68
|
propagated in quadrature (`ratio_uncertainty="propagate"`).
|
|
57
69
|
|
|
58
|
-
`ratio="Background"` picks the reference by label
|
|
70
|
+
`ratio="Background"` picks the reference by label; all other histograms, data
|
|
71
|
+
included, are divided by it.
|
|
59
72
|
|
|
60
73
|
`ratio="significance"` (or `"s/sqrt(b)"`, `"s/sqrt(s+b)"`) draws a
|
|
61
74
|
**significance panel** instead: per bin, the signal over the square root of
|
|
@@ -66,7 +79,10 @@ the signal. The values are returned as a `Ratio` in `Plot.ratios`.
|
|
|
66
79
|
|
|
67
80
|
`ratio_ylim` and
|
|
68
81
|
`ratio_label` override the automatic range (at least 0.5 to 1.5, widened to
|
|
69
|
-
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
|
|
70
86
|
computed values are returned in `Plot.ratios` as
|
|
71
87
|
[`Ratio`][rootfig.Ratio] objects (`values`, `errors`, `band`, `edges`).
|
|
72
88
|
|
|
@@ -113,8 +129,18 @@ computed values are returned in `Plot.ratios` as
|
|
|
113
129
|
`(main, ratio)` for ratio plots), so several rootfig plots can share a figure.
|
|
114
130
|
- `save="file.pdf"` saves immediately; `Plot.save(path)` accepts a directory
|
|
115
131
|
(file named after the variable) and `formats=["pdf", "png"]`.
|
|
132
|
+
- Figures use matplotlib's constrained layout, so labels, legends and colour
|
|
133
|
+
bars fit inside the canvas and a saved file has exactly the `figsize`
|
|
134
|
+
dimensions: 1D, 2D and ratio plots of one size share one shape. Figures
|
|
135
|
+
drawn into your own `ax` are saved with a tight bounding box instead.
|
|
136
|
+
- Fonts are fixed on the figure when it is made, so saving or displaying it
|
|
137
|
+
later renders exactly the layout that was computed, in the style's fonts.
|
|
116
138
|
- `Plot.fig`, `Plot.ax`, `Plot.ratio_ax` are plain matplotlib objects;
|
|
117
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.
|
|
118
144
|
|
|
119
145
|
## 2D histograms and correlations
|
|
120
146
|
|
|
@@ -126,6 +152,8 @@ rf.correlation(sample, ["MET", "nJet", "HT"], selection="nJet >= 2", percent=Tru
|
|
|
126
152
|
`rf.plot2d` options: `logz`, `logx`, `logy`, `cmap` (any matplotlib colour
|
|
127
153
|
map, default `viridis`), `colorbar=False`, `zlabel` (default `Events` or the
|
|
128
154
|
normalisation), `normalize`, `title`, `text`, `style`, `figsize`, `ax`, `save`.
|
|
155
|
+
The figure has the same size as a 1D plot; the colour bar takes its space from
|
|
156
|
+
the axes.
|
|
129
157
|
|
|
130
158
|
`rf.correlation` options: `labels` (tick labels, default the variable labels),
|
|
131
159
|
`percent=True` (integer percentages instead of two-decimal coefficients),
|
|
@@ -168,7 +196,9 @@ Negative weights (NLO samples) can make a weighted variance negative or an
|
|
|
168
196
|
efficiency leave `[0, 1]`. rootfig then reports `nan` for the standard
|
|
169
197
|
deviation, the profile error or the confidence interval (with a warning for
|
|
170
198
|
efficiencies) rather than a made-up uncertainty; means, yields and histogram
|
|
171
|
-
contents are unaffected.
|
|
199
|
+
contents are unaffected. Bins whose total weight is negative keep their mean
|
|
200
|
+
or efficiency (the plain ratio) but get no uncertainty; bins whose weights
|
|
201
|
+
cancel to exactly zero count as empty (`nan`).
|
|
172
202
|
|
|
173
203
|
## Cut flows
|
|
174
204
|
|
|
@@ -183,6 +213,10 @@ table.get("ZH").efficiencies # relative to the previous step
|
|
|
183
213
|
table.get("ZH").absolute_efficiencies
|
|
184
214
|
```
|
|
185
215
|
|
|
216
|
+
Step efficiencies are plain ratios of weighted yields (`nan` after a zero
|
|
217
|
+
yield). With signed (NLO) weights a yield can be negative and a ratio can lie
|
|
218
|
+
outside `[0, 1]`; it is reported as is.
|
|
219
|
+
|
|
186
220
|
Cuts apply cumulatively; a sample's own selection is the first row. Per-object
|
|
187
221
|
cuts pass an event when any object passes. `weight`, `lumi` and `nonfinite`
|
|
188
222
|
work as in `plot()`: events with a missing or non-finite weight are excluded
|