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.
Files changed (110) hide show
  1. {rootfig-0.1.0 → rootfig-0.2.1}/.gitignore +1 -0
  2. {rootfig-0.1.0 → rootfig-0.2.1}/CONTRIBUTING.md +20 -11
  3. {rootfig-0.1.0 → rootfig-0.2.1}/PKG-INFO +7 -8
  4. {rootfig-0.1.0 → rootfig-0.2.1}/README.md +1 -1
  5. {rootfig-0.1.0 → rootfig-0.2.1}/docs/composable.md +8 -3
  6. {rootfig-0.1.0 → rootfig-0.2.1}/docs/ecosystem.md +4 -1
  7. {rootfig-0.1.0 → rootfig-0.2.1}/docs/expressions.md +5 -3
  8. {rootfig-0.1.0 → rootfig-0.2.1}/docs/gallery.md +21 -15
  9. rootfig-0.2.1/docs/hooks/gallery.py +98 -0
  10. rootfig-0.2.1/docs/images/gallery/arrays.png +0 -0
  11. rootfig-0.2.1/docs/images/gallery/cms_density.png +0 -0
  12. rootfig-0.2.1/docs/images/gallery/correlation.png +0 -0
  13. rootfig-0.2.1/docs/images/gallery/efficiency.png +0 -0
  14. rootfig-0.2.1/docs/images/gallery/expressions.png +0 -0
  15. rootfig-0.2.1/docs/images/gallery/fill_stats.png +0 -0
  16. rootfig-0.2.1/docs/images/gallery/hist2d.png +0 -0
  17. rootfig-0.2.1/docs/images/gallery/log_axes.png +0 -0
  18. rootfig-0.2.1/docs/images/gallery/luminosity.png +0 -0
  19. rootfig-0.2.1/docs/images/gallery/many_plots.png +0 -0
  20. rootfig-0.2.1/docs/images/gallery/object_vs_event.png +0 -0
  21. rootfig-0.2.1/docs/images/gallery/overlay_ratio.png +0 -0
  22. rootfig-0.2.1/docs/images/gallery/profile.png +0 -0
  23. rootfig-0.2.1/docs/images/gallery/quick.png +0 -0
  24. rootfig-0.2.1/docs/images/gallery/ratio_reference.png +0 -0
  25. rootfig-0.2.1/docs/images/gallery/robust_range.png +0 -0
  26. rootfig-0.2.1/docs/images/gallery/stack_data.png +0 -0
  27. rootfig-0.2.1/docs/images/gallery/style_colors.png +0 -0
  28. rootfig-0.2.1/docs/images/gallery/variable_bins.png +0 -0
  29. rootfig-0.2.1/docs/images/gallery/xbreak_ratio.png +0 -0
  30. {rootfig-0.1.0 → rootfig-0.2.1}/docs/plotting.md +41 -7
  31. rootfig-0.1.0/examples/gallery.py → rootfig-0.2.1/examples/gallery/__init__.py +115 -292
  32. rootfig-0.2.1/examples/gallery/__main__.py +17 -0
  33. rootfig-0.2.1/examples/gallery/data.py +106 -0
  34. rootfig-0.2.1/examples/gallery/registry.py +103 -0
  35. {rootfig-0.1.0 → rootfig-0.2.1}/pyproject.toml +9 -7
  36. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/__init__.py +1 -1
  37. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/api.py +66 -24
  38. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/parser.py +4 -1
  39. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/__init__.py +2 -0
  40. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/build.py +66 -15
  41. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/cutflow.py +14 -7
  42. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/efficiency.py +20 -12
  43. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/normalize.py +59 -21
  44. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/pipeline.py +50 -8
  45. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/stats.py +16 -4
  46. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/io/sources.py +26 -1
  47. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/__init__.py +1 -2
  48. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/binning.py +7 -2
  49. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/samples.py +65 -22
  50. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/variables.py +14 -1
  51. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/__init__.py +4 -0
  52. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/correlation.py +4 -1
  53. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/figure.py +94 -1
  54. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/hist1d.py +1 -1
  55. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/hist2d.py +15 -4
  56. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/result.py +9 -7
  57. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/style.py +179 -22
  58. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/selection/columns.py +20 -3
  59. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_api.py +288 -8
  60. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_gallery.py +72 -10
  61. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_histograms.py +209 -1
  62. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_io.py +14 -0
  63. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_model.py +52 -4
  64. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_plotting.py +188 -1
  65. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_selection.py +24 -0
  66. rootfig-0.1.0/CHANGELOG.md +0 -87
  67. rootfig-0.1.0/docs/hooks/gallery.py +0 -63
  68. rootfig-0.1.0/docs/images/gallery/arrays.png +0 -0
  69. rootfig-0.1.0/docs/images/gallery/cms_density.png +0 -0
  70. rootfig-0.1.0/docs/images/gallery/correlation.png +0 -0
  71. rootfig-0.1.0/docs/images/gallery/efficiency.png +0 -0
  72. rootfig-0.1.0/docs/images/gallery/expressions.png +0 -0
  73. rootfig-0.1.0/docs/images/gallery/fill_stats.png +0 -0
  74. rootfig-0.1.0/docs/images/gallery/hist2d.png +0 -0
  75. rootfig-0.1.0/docs/images/gallery/log_axes.png +0 -0
  76. rootfig-0.1.0/docs/images/gallery/luminosity.png +0 -0
  77. rootfig-0.1.0/docs/images/gallery/many_plots.png +0 -0
  78. rootfig-0.1.0/docs/images/gallery/object_vs_event.png +0 -0
  79. rootfig-0.1.0/docs/images/gallery/overlay_ratio.png +0 -0
  80. rootfig-0.1.0/docs/images/gallery/profile.png +0 -0
  81. rootfig-0.1.0/docs/images/gallery/quick.png +0 -0
  82. rootfig-0.1.0/docs/images/gallery/ratio_reference.png +0 -0
  83. rootfig-0.1.0/docs/images/gallery/robust_range.png +0 -0
  84. rootfig-0.1.0/docs/images/gallery/stack_data.png +0 -0
  85. rootfig-0.1.0/docs/images/gallery/style_colors.png +0 -0
  86. rootfig-0.1.0/docs/images/gallery/variable_bins.png +0 -0
  87. rootfig-0.1.0/docs/images/gallery/xbreak_ratio.png +0 -0
  88. {rootfig-0.1.0 → rootfig-0.2.1}/LICENSE +0 -0
  89. {rootfig-0.1.0 → rootfig-0.2.1}/docs/api.md +0 -0
  90. {rootfig-0.1.0 → rootfig-0.2.1}/docs/index.md +0 -0
  91. {rootfig-0.1.0 → rootfig-0.2.1}/docs/quickstart.md +0 -0
  92. {rootfig-0.1.0 → rootfig-0.2.1}/mkdocs.yml +0 -0
  93. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/_typing.py +0 -0
  94. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/errors.py +0 -0
  95. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/__init__.py +0 -0
  96. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/expressions/functions.py +0 -0
  97. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/histograms/ratio.py +0 -0
  98. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/io/__init__.py +0 -0
  99. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/cuts.py +0 -0
  100. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/style.py +0 -0
  101. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/model/units.py +0 -0
  102. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/annotations.py +0 -0
  103. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/points.py +0 -0
  104. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/plotting/ratio.py +0 -0
  105. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/py.typed +0 -0
  106. {rootfig-0.1.0 → rootfig-0.2.1}/src/rootfig/selection/__init__.py +0 -0
  107. {rootfig-0.1.0 → rootfig-0.2.1}/tests/conftest.py +0 -0
  108. {rootfig-0.1.0 → rootfig-0.2.1}/tests/data/split_collection.root +0 -0
  109. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_expressions.py +0 -0
  110. {rootfig-0.1.0 → rootfig-0.2.1}/tests/test_tutorials.py +0 -0
@@ -9,6 +9,7 @@ build/
9
9
  .ruff_cache/
10
10
  .coverage
11
11
  coverage.xml
12
+ junit.xml
12
13
  htmlcov/
13
14
  site/
14
15
  *.root
@@ -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. Tests that use ROOT's tutorial files run only when
30
- `root-config` is available locally and are skipped otherwise; do not add
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.py` is both the showcase and the image-regression suite. Each
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.py # look at examples/out/*.png
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` and move the changelog
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 and publishes it to
124
- PyPI via Trusted Publishing (configure the publisher on PyPI first:
125
- repository `jbeirer/rootfig`, workflow `release.yml`, environment `pypi`).
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.0
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.6
28
- Requires-Dist: hist>=2.7
29
- Requires-Dist: matplotlib>=3.10
30
- Requires-Dist: mplhep>=0.3.50
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.3
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.py`](examples/gallery.py), which writes toy ROOT files and
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.py`](examples/gallery.py), which writes toy ROOT files and
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
- - `sample.with_(label="...")` returns a modified copy.
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. Attribute access, lambdas,
20
- comprehensions, string literals and calls to anything but the functions
21
- below are rejected at parse time with an [`ExpressionError`][rootfig.ExpressionError].
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.py`](https://github.com/jbeirer/rootfig/blob/main/examples/gallery.py).
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) and runs all
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.py # figures end up in examples/out/
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
- The examples share a few definitions, made once (`out` is the directory with
27
- the toy files). Everything below is optional: plain strings work everywhere.
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 at a luminosity, raw counts and step efficiencies per sample
48
+ # a cut flow: yields, raw counts and step efficiencies per sample
42
49
  print(
43
50
  rf.cutflow(
44
- [zh, ww, zz],
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(sig, ["MET", "count(Muon_pt)", "first(Muon_pt)"], selection="nJet >= 2")
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(sig, "MET", bins=(40, 0, 400), selection="nJet >= 2")
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'![{example.title}](images/gallery/{example.name}.png){{ 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)
@@ -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
- - with data and overlaid samples: data / first sample;
55
- - otherwise: every sample / the first sample, uncertainties of both
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`). The
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