rootfig 0.1.0__tar.gz → 0.2.0__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.0}/.gitignore +1 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/CONTRIBUTING.md +9 -8
- {rootfig-0.1.0 → rootfig-0.2.0}/PKG-INFO +6 -7
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/composable.md +8 -3
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/ecosystem.md +4 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/expressions.md +5 -3
- rootfig-0.2.0/docs/images/gallery/arrays.png +0 -0
- rootfig-0.2.0/docs/images/gallery/cms_density.png +0 -0
- rootfig-0.2.0/docs/images/gallery/correlation.png +0 -0
- rootfig-0.2.0/docs/images/gallery/efficiency.png +0 -0
- rootfig-0.2.0/docs/images/gallery/expressions.png +0 -0
- rootfig-0.2.0/docs/images/gallery/fill_stats.png +0 -0
- rootfig-0.2.0/docs/images/gallery/hist2d.png +0 -0
- rootfig-0.2.0/docs/images/gallery/log_axes.png +0 -0
- rootfig-0.2.0/docs/images/gallery/luminosity.png +0 -0
- rootfig-0.2.0/docs/images/gallery/many_plots.png +0 -0
- rootfig-0.2.0/docs/images/gallery/object_vs_event.png +0 -0
- rootfig-0.2.0/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.2.0/docs/images/gallery/profile.png +0 -0
- rootfig-0.2.0/docs/images/gallery/quick.png +0 -0
- rootfig-0.2.0/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.2.0/docs/images/gallery/robust_range.png +0 -0
- rootfig-0.2.0/docs/images/gallery/stack_data.png +0 -0
- rootfig-0.2.0/docs/images/gallery/style_colors.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.1.0 → rootfig-0.2.0}/docs/plotting.md +33 -6
- {rootfig-0.1.0 → rootfig-0.2.0}/pyproject.toml +9 -7
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/__init__.py +1 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/api.py +63 -23
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/expressions/parser.py +4 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/__init__.py +2 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/build.py +66 -15
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/cutflow.py +14 -7
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/efficiency.py +20 -12
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/normalize.py +59 -21
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/pipeline.py +50 -8
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/stats.py +16 -4
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/io/sources.py +26 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/__init__.py +1 -2
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/binning.py +7 -2
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/samples.py +65 -22
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/variables.py +14 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/__init__.py +4 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/correlation.py +4 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/figure.py +26 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/hist1d.py +1 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/hist2d.py +15 -4
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/result.py +9 -7
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/style.py +106 -21
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/selection/columns.py +20 -3
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_api.py +227 -2
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_gallery.py +1 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_histograms.py +209 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_io.py +14 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_model.py +52 -4
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_plotting.py +10 -1
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_selection.py +24 -0
- rootfig-0.1.0/CHANGELOG.md +0 -87
- 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.0}/LICENSE +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/README.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/api.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/gallery.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/hooks/gallery.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/index.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/docs/quickstart.md +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/examples/gallery.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/mkdocs.yml +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/_typing.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/errors.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/expressions/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/expressions/functions.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/histograms/ratio.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/io/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/cuts.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/style.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/model/units.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/annotations.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/points.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/plotting/ratio.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/py.typed +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/src/rootfig/selection/__init__.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/conftest.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/data/split_collection.root +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/tests/test_expressions.py +0 -0
- {rootfig-0.1.0 → rootfig-0.2.0}/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
|
|
@@ -111,18 +113,17 @@ docstring; the test suite fails until its baseline image exists.
|
|
|
111
113
|
|
|
112
114
|
- Add tests for behaviour changes; unit tests assert histogram contents and
|
|
113
115
|
matplotlib structure. Rendered output is covered by the gallery (below).
|
|
114
|
-
- Update `CHANGELOG.md` under "Unreleased".
|
|
115
116
|
- Run the checks above before pushing; `pre-commit run --all-files` does most
|
|
116
117
|
of it.
|
|
117
118
|
|
|
118
119
|
## Releasing
|
|
119
120
|
|
|
120
|
-
1. Update the version in `src/rootfig/__init__.py
|
|
121
|
-
entries under a new heading.
|
|
121
|
+
1. Update the version in `src/rootfig/__init__.py`.
|
|
122
122
|
2. Commit, tag `vX.Y.Z`, push the tag.
|
|
123
|
-
3. The `release.yml` workflow builds the distribution
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
3. The `release.yml` workflow builds the distribution, refuses a tag that does
|
|
124
|
+
not match `rootfig.__version__`, and publishes it to PyPI via Trusted
|
|
125
|
+
Publishing (configure the publisher on PyPI first: repository
|
|
126
|
+
`jbeirer/rootfig`, workflow `release.yml`, environment `pypi`).
|
|
126
127
|
|
|
127
128
|
The documentation is published by the `docs` job of `ci.yml` on every push to
|
|
128
129
|
`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.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -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
|
|
|
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
|
|
@@ -113,6 +126,12 @@ computed values are returned in `Plot.ratios` as
|
|
|
113
126
|
`(main, ratio)` for ratio plots), so several rootfig plots can share a figure.
|
|
114
127
|
- `save="file.pdf"` saves immediately; `Plot.save(path)` accepts a directory
|
|
115
128
|
(file named after the variable) and `formats=["pdf", "png"]`.
|
|
129
|
+
- Figures use matplotlib's constrained layout, so labels, legends and colour
|
|
130
|
+
bars fit inside the canvas and a saved file has exactly the `figsize`
|
|
131
|
+
dimensions: 1D, 2D and ratio plots of one size share one shape. Figures
|
|
132
|
+
drawn into your own `ax` are saved with a tight bounding box instead.
|
|
133
|
+
- Fonts are fixed on the figure when it is made, so saving or displaying it
|
|
134
|
+
later renders exactly the layout that was computed, in the style's fonts.
|
|
116
135
|
- `Plot.fig`, `Plot.ax`, `Plot.ratio_ax` are plain matplotlib objects;
|
|
117
136
|
`Plot.histograms` wrap the `hist.Hist` objects with labels and statistics.
|
|
118
137
|
|
|
@@ -126,6 +145,8 @@ rf.correlation(sample, ["MET", "nJet", "HT"], selection="nJet >= 2", percent=Tru
|
|
|
126
145
|
`rf.plot2d` options: `logz`, `logx`, `logy`, `cmap` (any matplotlib colour
|
|
127
146
|
map, default `viridis`), `colorbar=False`, `zlabel` (default `Events` or the
|
|
128
147
|
normalisation), `normalize`, `title`, `text`, `style`, `figsize`, `ax`, `save`.
|
|
148
|
+
The figure has the same size as a 1D plot; the colour bar takes its space from
|
|
149
|
+
the axes.
|
|
129
150
|
|
|
130
151
|
`rf.correlation` options: `labels` (tick labels, default the variable labels),
|
|
131
152
|
`percent=True` (integer percentages instead of two-decimal coefficients),
|
|
@@ -168,7 +189,9 @@ Negative weights (NLO samples) can make a weighted variance negative or an
|
|
|
168
189
|
efficiency leave `[0, 1]`. rootfig then reports `nan` for the standard
|
|
169
190
|
deviation, the profile error or the confidence interval (with a warning for
|
|
170
191
|
efficiencies) rather than a made-up uncertainty; means, yields and histogram
|
|
171
|
-
contents are unaffected.
|
|
192
|
+
contents are unaffected. Bins whose total weight is negative keep their mean
|
|
193
|
+
or efficiency (the plain ratio) but get no uncertainty; bins whose weights
|
|
194
|
+
cancel to exactly zero count as empty (`nan`).
|
|
172
195
|
|
|
173
196
|
## Cut flows
|
|
174
197
|
|
|
@@ -183,6 +206,10 @@ table.get("ZH").efficiencies # relative to the previous step
|
|
|
183
206
|
table.get("ZH").absolute_efficiencies
|
|
184
207
|
```
|
|
185
208
|
|
|
209
|
+
Step efficiencies are plain ratios of weighted yields (`nan` after a zero
|
|
210
|
+
yield). With signed (NLO) weights a yield can be negative and a ratio can lie
|
|
211
|
+
outside `[0, 1]`; it is reported as is.
|
|
212
|
+
|
|
186
213
|
Cuts apply cumulatively; a sample's own selection is the first row. Per-object
|
|
187
214
|
cuts pass an event when any object passes. `weight`, `lumi` and `nonfinite`
|
|
188
215
|
work as in `plot()`: events with a missing or non-finite weight are excluded
|
|
@@ -26,13 +26,16 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
27
27
|
"Typing :: Typed",
|
|
28
28
|
]
|
|
29
|
+
# Floors are the oldest versions the test suite passes with (CI job "minimum-versions"):
|
|
30
|
+
# uproot 5.7.4 reads nested RNTuple fields and entry ranges across files and writes the
|
|
31
|
+
# fixed-size test arrays; mplhep 1.3 has exp_label(text=) and hist2dplot(mask=).
|
|
29
32
|
dependencies = [
|
|
30
|
-
"uproot>=5.
|
|
31
|
-
"awkward>=2.
|
|
33
|
+
"uproot>=5.7.4",
|
|
34
|
+
"awkward>=2.8",
|
|
32
35
|
"numpy>=1.26",
|
|
33
|
-
"hist>=2.7",
|
|
34
|
-
"mplhep>=
|
|
35
|
-
"matplotlib>=3.
|
|
36
|
+
"hist>=2.7.2",
|
|
37
|
+
"mplhep>=1.3",
|
|
38
|
+
"matplotlib>=3.11",
|
|
36
39
|
]
|
|
37
40
|
|
|
38
41
|
[project.urls]
|
|
@@ -40,7 +43,6 @@ Homepage = "https://github.com/jbeirer/rootfig"
|
|
|
40
43
|
Documentation = "https://github.com/jbeirer/rootfig#readme"
|
|
41
44
|
Repository = "https://github.com/jbeirer/rootfig"
|
|
42
45
|
Issues = "https://github.com/jbeirer/rootfig/issues"
|
|
43
|
-
Changelog = "https://github.com/jbeirer/rootfig/blob/main/CHANGELOG.md"
|
|
44
46
|
|
|
45
47
|
[dependency-groups]
|
|
46
48
|
dev = [
|
|
@@ -62,7 +64,7 @@ path = "src/rootfig/__init__.py"
|
|
|
62
64
|
[tool.hatch.build.targets.sdist]
|
|
63
65
|
include = [
|
|
64
66
|
"src/rootfig", "tests", "docs", "examples", "mkdocs.yml",
|
|
65
|
-
"README.md", "
|
|
67
|
+
"README.md", "CONTRIBUTING.md", "LICENSE", "pyproject.toml",
|
|
66
68
|
]
|
|
67
69
|
|
|
68
70
|
[tool.hatch.build.targets.wheel]
|
|
@@ -14,11 +14,10 @@ from dataclasses import dataclass
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
import awkward as ak
|
|
17
|
-
import matplotlib.pyplot as plt
|
|
18
17
|
import numpy as np
|
|
19
18
|
|
|
20
19
|
from rootfig._typing import FloatArray, Hist
|
|
21
|
-
from rootfig.errors import RootfigWarning, SelectionError, SourceError
|
|
20
|
+
from rootfig.errors import BinningError, RootfigWarning, SelectionError, SourceError
|
|
22
21
|
from rootfig.expressions import parse
|
|
23
22
|
from rootfig.histograms import (
|
|
24
23
|
SIGNIFICANCE_KINDS,
|
|
@@ -36,6 +35,7 @@ from rootfig.histograms import (
|
|
|
36
35
|
correlation_matrix,
|
|
37
36
|
describe_table,
|
|
38
37
|
load_columns,
|
|
38
|
+
load_columns_each,
|
|
39
39
|
read_arrays,
|
|
40
40
|
significance,
|
|
41
41
|
)
|
|
@@ -76,6 +76,7 @@ from rootfig.plotting import (
|
|
|
76
76
|
draw_ratio_panel,
|
|
77
77
|
draw_significance_panel,
|
|
78
78
|
envelope,
|
|
79
|
+
finalize_figure,
|
|
79
80
|
finish_axes,
|
|
80
81
|
fold_flow_bins,
|
|
81
82
|
label_flow_bins,
|
|
@@ -345,9 +346,11 @@ def plot(
|
|
|
345
346
|
stack
|
|
346
347
|
Stack the non-data samples.
|
|
347
348
|
ratio
|
|
348
|
-
``True`` for a ratio panel
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
``True`` for a ratio panel: data / total MC for a stack (needs
|
|
350
|
+
``observed=``), data / the first non-data sample when data is overlaid,
|
|
351
|
+
otherwise every further sample over the first; a sample label to use
|
|
352
|
+
as the reference (all other histograms, data included, are divided by
|
|
353
|
+
it); or a significance panel: ``"significance"`` (``S/sqrt(B)``),
|
|
351
354
|
``"s/sqrt(b)"`` or ``"s/sqrt(s+b)"``, where the signal is the last
|
|
352
355
|
non-data sample (the top of a stack) and the background the sum of the
|
|
353
356
|
others; ``("s/sqrt(b)", "Signal")`` names the signal sample.
|
|
@@ -462,6 +465,7 @@ def plot_histograms(
|
|
|
462
465
|
style: StyleLike = None,
|
|
463
466
|
figsize: tuple[float, float] | None = None,
|
|
464
467
|
ax: AxesLike = None,
|
|
468
|
+
assume_poisson: bool = False,
|
|
465
469
|
save: str | None = None,
|
|
466
470
|
) -> Plot:
|
|
467
471
|
"""Draw already-filled histograms (``hist.Hist`` or :class:`~rootfig.histograms.Histogram`).
|
|
@@ -472,10 +476,16 @@ def plot_histograms(
|
|
|
472
476
|
:class:`~rootfig.histograms.Histogram` objects with ``is_data=True``.
|
|
473
477
|
Overlaid histograms may have different binnings; stacks, ratio panels and
|
|
474
478
|
``flow="show"`` need identical bin edges.
|
|
479
|
+
|
|
480
|
+
A count-storage histogram that was filled with weights (or rescaled) has
|
|
481
|
+
lost its sum of squared weights and is rejected with a ``ValueError``;
|
|
482
|
+
``assume_poisson=True`` draws it anyway with the absolute bin contents as
|
|
483
|
+
variances (a warning is issued). Fill with ``hist.storage.Weight()`` to keep
|
|
484
|
+
the real uncertainties.
|
|
475
485
|
"""
|
|
476
486
|
if logx is None:
|
|
477
487
|
logx = variable.log if variable is not None else False
|
|
478
|
-
histograms_ = _wrap_hists(hists, labels)
|
|
488
|
+
histograms_ = _wrap_hists(hists, labels, assume_poisson=assume_poisson)
|
|
479
489
|
if not histograms_:
|
|
480
490
|
msg = "no histograms to draw"
|
|
481
491
|
raise ValueError(msg)
|
|
@@ -644,6 +654,7 @@ def plot_histograms(
|
|
|
644
654
|
if layout.ratio is not None and layout.ratio_right is not None:
|
|
645
655
|
apply_xbreak(layout.ratio, layout.ratio_right, *segments)
|
|
646
656
|
|
|
657
|
+
finalize_figure(layout.fig, layout.main) # last: fonts and label anchoring
|
|
647
658
|
result = Plot(
|
|
648
659
|
fig=layout.fig,
|
|
649
660
|
ax=layout.main,
|
|
@@ -693,10 +704,11 @@ def plot2d(
|
|
|
693
704
|
|
|
694
705
|
``x`` and ``y`` must have the same structure (both per-event, or both
|
|
695
706
|
per-object from the same collection). ``bins`` applies to both axes unless
|
|
696
|
-
it is a pair of binning specifications, one per axis
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
707
|
+
it is a pair of binning specifications, one per axis (so ``(40, 20)`` is
|
|
708
|
+
two bin counts, never a range; a range needs ``(n, low, high)``); per-axis
|
|
709
|
+
ranges, labels and logarithmic scales (``logx``/``logy`` default to the
|
|
710
|
+
variables' ``log`` flags) are best given through
|
|
711
|
+
:class:`~rootfig.model.Variable` objects.
|
|
700
712
|
"""
|
|
701
713
|
sample = _single_sample(data, tree=tree)
|
|
702
714
|
x_bins, y_bins = _split_bins(bins)
|
|
@@ -711,11 +723,9 @@ def plot2d(
|
|
|
711
723
|
histogram_ = _normalize_for_plot(histogram_, normalize)
|
|
712
724
|
resolved_style = _style_for(style, text, lumi)
|
|
713
725
|
with style_context(resolved_style) as st:
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
default_size = (width * 1.15, height)
|
|
718
|
-
layout = make_figure(st, ratio=False, ax=ax, figsize=default_size)
|
|
726
|
+
# Same canvas as a 1D plot; the colour bar takes its space from the main axes
|
|
727
|
+
# (draw_hist2d stops mplhep from widening the figure).
|
|
728
|
+
layout = make_figure(st, ratio=False, ax=ax, figsize=figsize or st.figsize)
|
|
719
729
|
fig, main_ax = layout.fig, layout.main
|
|
720
730
|
draw_hist2d(
|
|
721
731
|
histogram_,
|
|
@@ -732,6 +742,7 @@ def plot2d(
|
|
|
732
742
|
if title:
|
|
733
743
|
main_ax.set_title(title)
|
|
734
744
|
add_experiment_label(main_ax, st, has_data=sample.is_data)
|
|
745
|
+
finalize_figure(fig, main_ax) # last: fonts and label anchoring
|
|
735
746
|
result = Plot(fig=fig, ax=main_ax, histograms=[histogram_], variable=var_x)
|
|
736
747
|
if save:
|
|
737
748
|
result.save(save)
|
|
@@ -812,10 +823,11 @@ def summarize(
|
|
|
812
823
|
var_list = [variables] if isinstance(variables, str | Variable) else list(variables)
|
|
813
824
|
rows: list[tuple[str, str, Summary]] = []
|
|
814
825
|
for sample in samples:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
826
|
+
# One read per sample: the branches of all variables are fetched together.
|
|
827
|
+
per_variable = load_columns_each(
|
|
828
|
+
sample, var_list, selection=selection, weight=weight, lumi=lumi, nonfinite=nonfinite
|
|
829
|
+
)
|
|
830
|
+
for var, columns in zip(var_list, per_variable, strict=True):
|
|
819
831
|
rows.append((sample.label, as_variable(var).expression, summarize_columns(columns)))
|
|
820
832
|
return SummaryTable(tuple(rows))
|
|
821
833
|
|
|
@@ -868,6 +880,7 @@ def correlation(
|
|
|
868
880
|
matrix, tick_labels, main_ax, cmap=cmap, annotate=annotate, percent=percent
|
|
869
881
|
)
|
|
870
882
|
main_ax.set_title(title if title is not None else f"{sample.label}: correlation")
|
|
883
|
+
finalize_figure(fig, main_ax)
|
|
871
884
|
result = Plot(fig=fig, ax=main_ax, matrix=matrix)
|
|
872
885
|
if save:
|
|
873
886
|
result.save(save)
|
|
@@ -1013,6 +1026,7 @@ def efficiency(
|
|
|
1013
1026
|
logy=False,
|
|
1014
1027
|
floating=[legend_artist] if floating and legend_artist is not None else [],
|
|
1015
1028
|
)
|
|
1029
|
+
finalize_figure(layout.fig, layout.main) # last: fonts and label anchoring
|
|
1016
1030
|
result = Plot(
|
|
1017
1031
|
fig=layout.fig,
|
|
1018
1032
|
ax=layout.main,
|
|
@@ -1062,7 +1076,8 @@ def profile(
|
|
|
1062
1076
|
e.g. ``"(reco_pt - true_pt) / true_pt"``). ``x`` and ``y`` must have the same
|
|
1063
1077
|
structure (both per-event or both per-object of one collection). ``xlabel``
|
|
1064
1078
|
and ``unit`` describe the x axis; ``logx``/``logy`` default to the
|
|
1065
|
-
variables' ``log`` flags. With negative weights a bin whose
|
|
1079
|
+
variables' ``log`` flags. With negative weights a bin whose total weight
|
|
1080
|
+
is negative keeps its mean but has no error, and a bin whose weighted
|
|
1066
1081
|
variance is negative has no standard deviation (``nan``). The
|
|
1067
1082
|
:class:`~rootfig.histograms.Profile` objects are returned in ``Plot.profiles``.
|
|
1068
1083
|
|
|
@@ -1142,6 +1157,7 @@ def profile(
|
|
|
1142
1157
|
logy=logy,
|
|
1143
1158
|
floating=[legend_artist] if floating and legend_artist is not None else [],
|
|
1144
1159
|
)
|
|
1160
|
+
finalize_figure(layout.fig, layout.main) # last: fonts and label anchoring
|
|
1145
1161
|
result = Plot(fig=layout.fig, ax=layout.main, variable=var_x, profiles=profiles)
|
|
1146
1162
|
if save:
|
|
1147
1163
|
result.save(save)
|
|
@@ -1221,7 +1237,12 @@ RatioSpec = bool | str | tuple[str, str]
|
|
|
1221
1237
|
"""What ``ratio=`` accepts: a flag, a reference label, a significance kind, or (kind, signal)."""
|
|
1222
1238
|
|
|
1223
1239
|
|
|
1224
|
-
def _wrap_hists(
|
|
1240
|
+
def _wrap_hists(
|
|
1241
|
+
hists: Sequence[Histogram | Hist],
|
|
1242
|
+
labels: Sequence[str] | None,
|
|
1243
|
+
*,
|
|
1244
|
+
assume_poisson: bool = False,
|
|
1245
|
+
) -> list[Histogram]:
|
|
1225
1246
|
if labels is not None and len(labels) != len(hists):
|
|
1226
1247
|
msg = f"got {len(labels)} labels for {len(hists)} histograms"
|
|
1227
1248
|
raise ValueError(msg)
|
|
@@ -1235,7 +1256,8 @@ def _wrap_hists(hists: Sequence[Histogram | Hist], labels: Sequence[str] | None)
|
|
|
1235
1256
|
else:
|
|
1236
1257
|
axis_name = item.axes[0].name if item.ndim == 1 else ""
|
|
1237
1258
|
label = axis_name or f"hist {index + 1}"
|
|
1238
|
-
|
|
1259
|
+
converted = as_weight_storage(item, assume_poisson=assume_poisson)
|
|
1260
|
+
wrapped.append(Histogram(converted, label=str(label)))
|
|
1239
1261
|
return wrapped
|
|
1240
1262
|
|
|
1241
1263
|
|
|
@@ -1289,9 +1311,27 @@ def _ratio_setup(
|
|
|
1289
1311
|
|
|
1290
1312
|
|
|
1291
1313
|
def _split_bins(bins: Bins | tuple[Bins, Bins] | None) -> tuple[Any, Any]:
|
|
1292
|
-
"""Interpret ``bins`` for two axes: a pair of specifications, or one spec for both.
|
|
1314
|
+
"""Interpret ``bins`` for two axes: a pair of specifications, or one spec for both.
|
|
1315
|
+
|
|
1316
|
+
Raises
|
|
1317
|
+
------
|
|
1318
|
+
BinningError
|
|
1319
|
+
If the pair consists of two numbers that are not both integers: that
|
|
1320
|
+
reads like a ``(low, high)`` range, which a pair never is.
|
|
1321
|
+
"""
|
|
1293
1322
|
if bins is None:
|
|
1294
1323
|
return None, None
|
|
1295
1324
|
if isinstance(bins, tuple) and len(bins) == 2:
|
|
1325
|
+
numbers = all(
|
|
1326
|
+
isinstance(b, int | float | np.number) and not isinstance(b, bool) for b in bins
|
|
1327
|
+
)
|
|
1328
|
+
if numbers and not all(isinstance(b, int | np.integer) for b in bins):
|
|
1329
|
+
msg = (
|
|
1330
|
+
f"bins={bins!r}: for plot2d a 2-tuple is (x_bins, y_bins), one specification "
|
|
1331
|
+
"per axis, so two numbers are two bin counts, not a range. Give the range per "
|
|
1332
|
+
f"axis, e.g. bins=((50, {bins[0]}, {bins[1]}), (50, {bins[0]}, {bins[1]})), or "
|
|
1333
|
+
"use rf.Variable(x, bins=50, range=(low, high))"
|
|
1334
|
+
)
|
|
1335
|
+
raise BinningError(msg)
|
|
1296
1336
|
return bins[0], bins[1]
|
|
1297
1337
|
return bins, bins
|
|
@@ -349,7 +349,10 @@ def parse(expression: ExpressionLike) -> Expression:
|
|
|
349
349
|
------
|
|
350
350
|
ExpressionError
|
|
351
351
|
If the text is not a single Python expression or uses a disallowed
|
|
352
|
-
construct (
|
|
352
|
+
construct (unknown functions, string literals, lambdas, comprehensions,
|
|
353
|
+
conditional expressions, ...). Dotted names such as
|
|
354
|
+
``Collection.field.sub`` are read as one branch name (podio/EDM4hep
|
|
355
|
+
files), not as attribute access; attributes of anything else are rejected.
|
|
353
356
|
"""
|
|
354
357
|
if isinstance(expression, Expression):
|
|
355
358
|
return expression
|
|
@@ -15,6 +15,7 @@ from rootfig.histograms.pipeline import (
|
|
|
15
15
|
combined_selection,
|
|
16
16
|
combined_weight,
|
|
17
17
|
load_columns,
|
|
18
|
+
load_columns_each,
|
|
18
19
|
read_arrays,
|
|
19
20
|
source_length,
|
|
20
21
|
)
|
|
@@ -55,6 +56,7 @@ __all__ = [
|
|
|
55
56
|
"efficiency",
|
|
56
57
|
"fill",
|
|
57
58
|
"load_columns",
|
|
59
|
+
"load_columns_each",
|
|
58
60
|
"normalization_label",
|
|
59
61
|
"normalize",
|
|
60
62
|
"normalize_hist",
|