awkward-monetizer 0.1.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.
- awkward_monetizer-0.1.1/.gitignore +50 -0
- awkward_monetizer-0.1.1/LICENSE +28 -0
- awkward_monetizer-0.1.1/PKG-INFO +256 -0
- awkward_monetizer-0.1.1/README.md +225 -0
- awkward_monetizer-0.1.1/data/README.md +20 -0
- awkward_monetizer-0.1.1/examples/analysis.py +143 -0
- awkward_monetizer-0.1.1/examples/benchmark.py +31 -0
- awkward_monetizer-0.1.1/examples/make_sample_nanoaod.py +24 -0
- awkward_monetizer-0.1.1/pyproject.toml +59 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/__init__.py +35 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/adl.py +139 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/benchmark.py +408 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/cli.py +269 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/datasets.py +123 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/db.py +87 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/ingest.py +210 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/physics.py +49 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/reconstruct.py +135 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/roundtrip.py +88 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/sampledata.py +50 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/dimuon.sql +26 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/jets.sql +10 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/nanoaod.sql +34 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/udf_dimuon.sql +15 -0
- awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/udf_trijet.sql +35 -0
- awkward_monetizer-0.1.1/tests/conftest.py +30 -0
- awkward_monetizer-0.1.1/tests/test_adl.py +58 -0
- awkward_monetizer-0.1.1/tests/test_benchmark.py +49 -0
- awkward_monetizer-0.1.1/tests/test_cli.py +60 -0
- awkward_monetizer-0.1.1/tests/test_ingest.py +33 -0
- awkward_monetizer-0.1.1/tests/test_reconstruct.py +25 -0
- awkward_monetizer-0.1.1/tests/test_roundtrip.py +48 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# --- Data (not committed; see data/README.md) ---
|
|
2
|
+
/data/*.root
|
|
3
|
+
*.root
|
|
4
|
+
*.csv
|
|
5
|
+
*.parquet
|
|
6
|
+
*.arrow
|
|
7
|
+
*.dbfarm/
|
|
8
|
+
hep-dbfarm/
|
|
9
|
+
|
|
10
|
+
# --- Local awkward source checkout (built via pixi path deps) ---
|
|
11
|
+
/awkward/
|
|
12
|
+
|
|
13
|
+
# --- pixi ---
|
|
14
|
+
/.pixi/
|
|
15
|
+
# Remove the next line to commit the lockfile for reproducibility.
|
|
16
|
+
pixi.lock
|
|
17
|
+
|
|
18
|
+
# --- Python / packaging ---
|
|
19
|
+
__pycache__/
|
|
20
|
+
*.py[cod]
|
|
21
|
+
*$py.class
|
|
22
|
+
build/
|
|
23
|
+
dist/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.eggs/
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
env/
|
|
29
|
+
src/*/_version.py
|
|
30
|
+
|
|
31
|
+
# --- Test / tooling caches ---
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.ruff_cache/
|
|
34
|
+
.mypy_cache/
|
|
35
|
+
htmlcov/
|
|
36
|
+
.coverage
|
|
37
|
+
|
|
38
|
+
# --- Notebooks / outputs ---
|
|
39
|
+
.ipynb_checkpoints/
|
|
40
|
+
__marimo__/
|
|
41
|
+
*.png
|
|
42
|
+
|
|
43
|
+
# --- Cowork desktop app ---
|
|
44
|
+
/Claude outputs/
|
|
45
|
+
|
|
46
|
+
# --- Editors / OS ---
|
|
47
|
+
.vscode/
|
|
48
|
+
.idea/
|
|
49
|
+
.DS_Store
|
|
50
|
+
*.swp
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Ianna Osborne
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: awkward-monetizer
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Hybrid HEP analysis engine: Awkward Array + MonetDB + Apache Arrow
|
|
5
|
+
Project-URL: Homepage, https://github.com/ianna/awkward-monetizer
|
|
6
|
+
Project-URL: Issues, https://github.com/ianna/awkward-monetizer/issues
|
|
7
|
+
Author: Ianna Osborne
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: awkward,columnar,hep,monetdb,scikit-hep,uproot
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: awkward>=2
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pandas>=2
|
|
15
|
+
Requires-Dist: pyarrow>=12
|
|
16
|
+
Requires-Dist: pymonetdb>=1.7
|
|
17
|
+
Requires-Dist: uproot>=5
|
|
18
|
+
Requires-Dist: vector>=1
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
23
|
+
Provides-Extra: embedded
|
|
24
|
+
Requires-Dist: monetdbe; extra == 'embedded'
|
|
25
|
+
Provides-Extra: notebook
|
|
26
|
+
Requires-Dist: hist; extra == 'notebook'
|
|
27
|
+
Requires-Dist: marimo; extra == 'notebook'
|
|
28
|
+
Requires-Dist: matplotlib; extra == 'notebook'
|
|
29
|
+
Requires-Dist: mplhep; extra == 'notebook'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# awkward-monetizer
|
|
33
|
+
|
|
34
|
+
A hybrid High-Energy Physics (HEP) analysis engine that combines:
|
|
35
|
+
|
|
36
|
+
- **[MonetDB](https://www.monetdb.org/)** — fast, columnar SQL over event-level data
|
|
37
|
+
- **[Awkward Array](https://awkward-array.org/)** — high-performance nested (NF2) physics analysis
|
|
38
|
+
- **[Apache Arrow](https://arrow.apache.org/)** — the bridge between the two
|
|
39
|
+
|
|
40
|
+
Interactive exploration is done in [Marimo](https://marimo.io/) notebooks; the aim
|
|
41
|
+
is a Python-native alternative to [ROOT/RDataFrame](https://root.cern/doc/master/classROOT_1_1RDataFrame.html).
|
|
42
|
+
|
|
43
|
+
The idea: flatten nested ROOT events into relational tables, push selections down
|
|
44
|
+
to SQL, then reconstruct the nested structure in Awkward for the physics. It's
|
|
45
|
+
motivated by *Evaluating Query Languages and Systems for High-Energy Physics
|
|
46
|
+
Data* (Graur et al., PVLDB vol. 15, 2022; [arXiv:2104.12615](https://arxiv.org/abs/2104.12615)),
|
|
47
|
+
which found SQL systems struggle with nested data while ROOT stays dominant
|
|
48
|
+
thanks to its NF2-friendly execution model.
|
|
49
|
+
|
|
50
|
+
## Pipeline
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
ROOT ──uproot──▶ Awkward ──flatten──▶ pandas ──▶ MonetDB
|
|
54
|
+
│
|
|
55
|
+
Awkward (NF2) ◀──unflatten── pandas ◀──SQL slice──┘
|
|
56
|
+
│
|
|
57
|
+
└─▶ physics: invariant mass, ΔR, combinatorics ─▶ plots
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Layout
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/awkward_monetizer/
|
|
64
|
+
datasets.py dataset registry (dimuon, nanoaod) + collection types
|
|
65
|
+
ingest.py ROOT → Awkward → flat tables → MonetDB
|
|
66
|
+
reconstruct.py flat tables → Awkward NF2 (ak.unflatten)
|
|
67
|
+
db.py MonetDB connections (server / embedded) + packaged schema
|
|
68
|
+
physics.py 4-vectors (scikit-hep vector), invariant mass, charge pairs
|
|
69
|
+
adl.py ADL Q1–Q8 benchmark queries
|
|
70
|
+
roundtrip.py full ingest → DB → slice → reconstruct → validate cycle
|
|
71
|
+
sampledata.py synthetic NanoAOD generator
|
|
72
|
+
cli.py `awkward-monetizer` command-line interface
|
|
73
|
+
schemas/ packaged SQL (dimuon.sql, nanoaod.sql, jets.sql)
|
|
74
|
+
examples/ analysis.py (Marimo notebook), make_sample_nanoaod.py
|
|
75
|
+
tests/ pytest suite (reconstruction, round-trip, ADL)
|
|
76
|
+
data/ inputs (git-ignored) — see data/README.md
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
Development environment via **pixi** (builds Awkward from a local source
|
|
82
|
+
checkout; see *Awkward from source* below):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone --recursive https://github.com/scikit-hep/awkward.git
|
|
86
|
+
pixi install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or plain pip (uses released Awkward):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install -e ".[notebook,dev]"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The MonetDB **server** is separate (not on PyPI/conda): `brew install monetdb`
|
|
96
|
+
on macOS. The embedded engine (`pip install ".[embedded]"`, i.e. `monetdbe`) needs
|
|
97
|
+
Python ≤ 3.10.
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
awkward-monetizer ingest data/cms.root --dry-run # ROOT → flat tables
|
|
103
|
+
awkward-monetizer reconstruct data/cms.root # → Awkward NF2 + validate
|
|
104
|
+
awkward-monetizer make-nano # synthetic NanoAOD
|
|
105
|
+
awkward-monetizer adl # ADL Q1–Q8 on it
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
With pixi these are tasks: `pixi run ingest-dryrun`, `pixi run reconstruct-demo`,
|
|
109
|
+
`pixi run make-nano`, `pixi run adl`, `pixi run notebook`, `pixi run test`.
|
|
110
|
+
|
|
111
|
+
## Datasets
|
|
112
|
+
|
|
113
|
+
`event_id` is the join key across tables; `*_index` columns preserve each
|
|
114
|
+
object's position so nesting round-trips exactly. Two layouts (`--dataset`):
|
|
115
|
+
|
|
116
|
+
- **dimuon** (default; `data/cms.root`) — flat CMS dimuon ntuple: one row per
|
|
117
|
+
event, two muons as parallel columns (`pt1/pt2`, `Q1/Q2`, …), precomputed mass
|
|
118
|
+
`M`. Un-pivoted into a `muons` table. Schema: `schemas/dimuon.sql`.
|
|
119
|
+
- **nanoaod** — jagged per-event lists (`Jet_pt`, `Muon_pt`, …) exploded into
|
|
120
|
+
`jets`/`muons`, plus MET. Schema: `schemas/nanoaod.sql`. Matches real CMS
|
|
121
|
+
NanoAODv9 branch names/types; `event_id` is synthesized from a row index
|
|
122
|
+
(because `event` alone isn't unique in NanoAOD — the key is run+lumi+event,
|
|
123
|
+
kept as `run`/`lumi`/`event_number` columns).
|
|
124
|
+
|
|
125
|
+
## Live MonetDB round-trip
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
brew install monetdb
|
|
129
|
+
monetdbd create ~/hep-dbfarm && monetdbd start ~/hep-dbfarm
|
|
130
|
+
monetdb create hep && monetdb release hep
|
|
131
|
+
awkward-monetizer roundtrip --root-file data/cms.root # server backend
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`roundtrip` ingests, creates the schema, loads (`COPY INTO`), pushes a
|
|
135
|
+
`WHERE mass BETWEEN 60 AND 120` slice to SQL, reconstructs, and checks the
|
|
136
|
+
reconstructed dimuon mass against the stored `M`. `--backend embedded` runs the
|
|
137
|
+
same cycle in-process via `monetdbe` (loads with `INSERT`).
|
|
138
|
+
|
|
139
|
+
Round-trip mass validation supports only `dimuon`; unsupported datasets are
|
|
140
|
+
rejected before connecting to the database. If the SQL filter selects no events,
|
|
141
|
+
validation reports that it did not run and returns `passed=False`.
|
|
142
|
+
|
|
143
|
+
## ADL benchmark
|
|
144
|
+
|
|
145
|
+
`adl.py` implements the eight IRIS-HEP ADL benchmark queries (Q1 MET, Q2/Q3 jet
|
|
146
|
+
pT, Q4 ≥2 jets, Q5 OS dimuon, Q6 trijet, Q7 lepton-cleaned HT, Q8 MT of MET +
|
|
147
|
+
lead lepton) on reconstructed NanoAOD events using `vector`. No real NanoAOD file
|
|
148
|
+
ships here — `make-nano` writes a synthetic one; point `--from-root` at a real
|
|
149
|
+
CMS Open Data NanoAOD file to run on data. Scope: only muons are present, so
|
|
150
|
+
Q7/Q8 use muons as leptons; Q6 omits the b-tag term (no b-tag branch yet).
|
|
151
|
+
|
|
152
|
+
## Awkward from source
|
|
153
|
+
|
|
154
|
+
`pixi.toml` builds Awkward from a local `./awkward` clone (path dependencies:
|
|
155
|
+
`awkward-cpp` compiled, `awkward` editable) rather than a wheel — a `{ git=... }`
|
|
156
|
+
spec resolves to the released wheel. Verify with `pixi run check-awkward`: the
|
|
157
|
+
`file:` path should resolve into `./awkward/src/awkward/`. On macOS the source
|
|
158
|
+
build pins `MACOSX_DEPLOYMENT_TARGET` so the compiled wheel matches conda-forge's
|
|
159
|
+
Python; if a build is rejected as `macosx_15_0` incompatible, run
|
|
160
|
+
`MACOSX_DEPLOYMENT_TARGET=11.0 pixi install`.
|
|
161
|
+
|
|
162
|
+
## Benchmarks
|
|
163
|
+
|
|
164
|
+
`benchmark.py` compares backends on identical data for the dimuon Z→μμ selection
|
|
165
|
+
(opposite-charge pair, 60–120 GeV):
|
|
166
|
+
|
|
167
|
+
- **awkward** — pure Awkward (uproot read → reconstruct → analyze in memory)
|
|
168
|
+
- **hybrid** — MonetDB + Awkward: data pre-loaded, the event cut pushed to SQL,
|
|
169
|
+
survivors reconstructed and finished in Awkward
|
|
170
|
+
- **monetdb** — the *whole* selection in SQL via a `dimuon_mass` user-defined
|
|
171
|
+
function (`schemas/udf_dimuon.sql`): self-join the two muons per event, compute
|
|
172
|
+
the mass and filter in-database, return only the surviving masses — no Awkward
|
|
173
|
+
reconstruction. Runs on a server or embedded `monetdbe`.
|
|
174
|
+
- **rdataframe** — ROOT `RDataFrame` (gated: runs only if `import ROOT` works)
|
|
175
|
+
|
|
176
|
+
`--scale K` tiles the input into a K× larger physical ROOT file every backend
|
|
177
|
+
reads, so timings are meaningful; the query phase is timed (median of `--repeats`)
|
|
178
|
+
while one-time setup (reconstruction / DB ingest) is reported separately, since a
|
|
179
|
+
database amortizes load cost across many queries.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
awkward-monetizer bench --root-file data/cms.root --scale 50 \\
|
|
183
|
+
--backends awkward,hybrid,rdataframe --repeats 5
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The backends cross-check on selected-event count and mean mass (the harness prints
|
|
187
|
+
`agreement across backends: OK`). Pushing the physics into the database with the UDF (`monetdb` backend) returns
|
|
188
|
+
only the final selection, so it can beat the fetch-and-reconstruct `hybrid` path
|
|
189
|
+
by a wide margin on this query. The embedded hybrid backend loads via `INSERT`
|
|
190
|
+
(slow); use `--hybrid-backend server` (a real MonetDB server with `COPY INTO`) to
|
|
191
|
+
benchmark a realistic load path.
|
|
192
|
+
|
|
193
|
+
### When SQL wins vs when Awkward wins
|
|
194
|
+
|
|
195
|
+
The harness ships two analyses (`--analysis`) that show the crossover:
|
|
196
|
+
|
|
197
|
+
- `zmumu` (flat: two muons/event, a scalar mass) **favors the database** — the
|
|
198
|
+
in-DB `monetdb` backend runs the whole selection in SQL and beats
|
|
199
|
+
fetch-and-reconstruct.
|
|
200
|
+
- `trijet` (ADL Q6, jagged: every 3-jet combination, keep the mass closest to
|
|
201
|
+
172.5 GeV) **favors Awkward** — `ak.combinations(jets, 3)` is one vectorized
|
|
202
|
+
line, whereas the SQL equivalent (`schemas/udf_trijet.sql`) needs two UDFs, a
|
|
203
|
+
3-way self-join over jet triples, and a window function — and runs *slower* on
|
|
204
|
+
the same data.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
awkward-monetizer bench --analysis trijet --root-file data/nano_synth.root \\
|
|
208
|
+
--backends awkward,monetdb
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The relational store is the right tool for columnar filtering, materialization,
|
|
212
|
+
and scalar arithmetic; Awkward is the right tool for variable-length
|
|
213
|
+
combinatorics and nested (NF2) physics. The point of the hybrid design is to use
|
|
214
|
+
each where it wins, not to replace one with the other.
|
|
215
|
+
|
|
216
|
+
## Ingesting real CMS Open Data NanoAOD
|
|
217
|
+
|
|
218
|
+
The `nanoaod` dataset reads real CMS NanoAODv9 files directly (uproot pulls only
|
|
219
|
+
the branches we map, so a multi-GB file isn't fully read). Get a file — e.g. the
|
|
220
|
+
DoubleMuon sample from the [CMS NanoAOD guide](https://opendata.cern.ch/docs/cms-getting-started-nanoaod) —
|
|
221
|
+
via XRootD and stream it into a running MonetDB `hep` server in chunks:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
xrdcp root://eospublic.cern.ch//eos/opendata/cms/Run2016H/DoubleMuon/NANOAOD/UL2016_MiniAODv2_NanoAODv9-v1/2510000/127C2975-1B1C-A046-AABF-62B77E757A86.root .
|
|
225
|
+
awkward-monetizer ingest 127C2975-*.root --dataset nanoaod --database hep \\
|
|
226
|
+
--create-schema --step-size "100 MB" --entry-stop 200000
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`--step-size` streams with `uproot.iterate` (each chunk is offset so `event_id`
|
|
230
|
+
stays globally unique), keeping memory flat regardless of file size;
|
|
231
|
+
`--create-schema` (re)creates the tables first. Then the `nanoaod` ADL queries
|
|
232
|
+
and benchmarks run on real physics.
|
|
233
|
+
|
|
234
|
+
`--step-size 50000` sets an entry count instead of a memory size. Add `--dry-run`
|
|
235
|
+
to build and summarize chunks without connecting to MonetDB, or `--truncate`
|
|
236
|
+
to clear the dataset's tables once before loading the chunks.
|
|
237
|
+
|
|
238
|
+
## Testing
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pytest # or: pixi run test
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The ADL suite runs on generated synthetic data; the dimuon and round-trip tests
|
|
245
|
+
use `data/cms.root` and skip cleanly if it isn't present.
|
|
246
|
+
|
|
247
|
+
## Status
|
|
248
|
+
|
|
249
|
+
Ingestion, reconstruction, the live MonetDB round-trip, and ADL Q1–Q8 are
|
|
250
|
+
implemented and validated (round-trip verified on a real MonetDB server; the
|
|
251
|
+
reconstructed dimuon mass matches the stored `M` to ~1e-8). See `docs/roadmap.md`
|
|
252
|
+
for what's next (real NanoAOD data, ROOT/RDataFrame benchmarks, Arrow Flight).
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
BSD-3-Clause. Contributions welcome — see `CONTRIBUTING.md`.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# awkward-monetizer
|
|
2
|
+
|
|
3
|
+
A hybrid High-Energy Physics (HEP) analysis engine that combines:
|
|
4
|
+
|
|
5
|
+
- **[MonetDB](https://www.monetdb.org/)** — fast, columnar SQL over event-level data
|
|
6
|
+
- **[Awkward Array](https://awkward-array.org/)** — high-performance nested (NF2) physics analysis
|
|
7
|
+
- **[Apache Arrow](https://arrow.apache.org/)** — the bridge between the two
|
|
8
|
+
|
|
9
|
+
Interactive exploration is done in [Marimo](https://marimo.io/) notebooks; the aim
|
|
10
|
+
is a Python-native alternative to [ROOT/RDataFrame](https://root.cern/doc/master/classROOT_1_1RDataFrame.html).
|
|
11
|
+
|
|
12
|
+
The idea: flatten nested ROOT events into relational tables, push selections down
|
|
13
|
+
to SQL, then reconstruct the nested structure in Awkward for the physics. It's
|
|
14
|
+
motivated by *Evaluating Query Languages and Systems for High-Energy Physics
|
|
15
|
+
Data* (Graur et al., PVLDB vol. 15, 2022; [arXiv:2104.12615](https://arxiv.org/abs/2104.12615)),
|
|
16
|
+
which found SQL systems struggle with nested data while ROOT stays dominant
|
|
17
|
+
thanks to its NF2-friendly execution model.
|
|
18
|
+
|
|
19
|
+
## Pipeline
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ROOT ──uproot──▶ Awkward ──flatten──▶ pandas ──▶ MonetDB
|
|
23
|
+
│
|
|
24
|
+
Awkward (NF2) ◀──unflatten── pandas ◀──SQL slice──┘
|
|
25
|
+
│
|
|
26
|
+
└─▶ physics: invariant mass, ΔR, combinatorics ─▶ plots
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/awkward_monetizer/
|
|
33
|
+
datasets.py dataset registry (dimuon, nanoaod) + collection types
|
|
34
|
+
ingest.py ROOT → Awkward → flat tables → MonetDB
|
|
35
|
+
reconstruct.py flat tables → Awkward NF2 (ak.unflatten)
|
|
36
|
+
db.py MonetDB connections (server / embedded) + packaged schema
|
|
37
|
+
physics.py 4-vectors (scikit-hep vector), invariant mass, charge pairs
|
|
38
|
+
adl.py ADL Q1–Q8 benchmark queries
|
|
39
|
+
roundtrip.py full ingest → DB → slice → reconstruct → validate cycle
|
|
40
|
+
sampledata.py synthetic NanoAOD generator
|
|
41
|
+
cli.py `awkward-monetizer` command-line interface
|
|
42
|
+
schemas/ packaged SQL (dimuon.sql, nanoaod.sql, jets.sql)
|
|
43
|
+
examples/ analysis.py (Marimo notebook), make_sample_nanoaod.py
|
|
44
|
+
tests/ pytest suite (reconstruction, round-trip, ADL)
|
|
45
|
+
data/ inputs (git-ignored) — see data/README.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Development environment via **pixi** (builds Awkward from a local source
|
|
51
|
+
checkout; see *Awkward from source* below):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone --recursive https://github.com/scikit-hep/awkward.git
|
|
55
|
+
pixi install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or plain pip (uses released Awkward):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e ".[notebook,dev]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The MonetDB **server** is separate (not on PyPI/conda): `brew install monetdb`
|
|
65
|
+
on macOS. The embedded engine (`pip install ".[embedded]"`, i.e. `monetdbe`) needs
|
|
66
|
+
Python ≤ 3.10.
|
|
67
|
+
|
|
68
|
+
## Quickstart
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
awkward-monetizer ingest data/cms.root --dry-run # ROOT → flat tables
|
|
72
|
+
awkward-monetizer reconstruct data/cms.root # → Awkward NF2 + validate
|
|
73
|
+
awkward-monetizer make-nano # synthetic NanoAOD
|
|
74
|
+
awkward-monetizer adl # ADL Q1–Q8 on it
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
With pixi these are tasks: `pixi run ingest-dryrun`, `pixi run reconstruct-demo`,
|
|
78
|
+
`pixi run make-nano`, `pixi run adl`, `pixi run notebook`, `pixi run test`.
|
|
79
|
+
|
|
80
|
+
## Datasets
|
|
81
|
+
|
|
82
|
+
`event_id` is the join key across tables; `*_index` columns preserve each
|
|
83
|
+
object's position so nesting round-trips exactly. Two layouts (`--dataset`):
|
|
84
|
+
|
|
85
|
+
- **dimuon** (default; `data/cms.root`) — flat CMS dimuon ntuple: one row per
|
|
86
|
+
event, two muons as parallel columns (`pt1/pt2`, `Q1/Q2`, …), precomputed mass
|
|
87
|
+
`M`. Un-pivoted into a `muons` table. Schema: `schemas/dimuon.sql`.
|
|
88
|
+
- **nanoaod** — jagged per-event lists (`Jet_pt`, `Muon_pt`, …) exploded into
|
|
89
|
+
`jets`/`muons`, plus MET. Schema: `schemas/nanoaod.sql`. Matches real CMS
|
|
90
|
+
NanoAODv9 branch names/types; `event_id` is synthesized from a row index
|
|
91
|
+
(because `event` alone isn't unique in NanoAOD — the key is run+lumi+event,
|
|
92
|
+
kept as `run`/`lumi`/`event_number` columns).
|
|
93
|
+
|
|
94
|
+
## Live MonetDB round-trip
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
brew install monetdb
|
|
98
|
+
monetdbd create ~/hep-dbfarm && monetdbd start ~/hep-dbfarm
|
|
99
|
+
monetdb create hep && monetdb release hep
|
|
100
|
+
awkward-monetizer roundtrip --root-file data/cms.root # server backend
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`roundtrip` ingests, creates the schema, loads (`COPY INTO`), pushes a
|
|
104
|
+
`WHERE mass BETWEEN 60 AND 120` slice to SQL, reconstructs, and checks the
|
|
105
|
+
reconstructed dimuon mass against the stored `M`. `--backend embedded` runs the
|
|
106
|
+
same cycle in-process via `monetdbe` (loads with `INSERT`).
|
|
107
|
+
|
|
108
|
+
Round-trip mass validation supports only `dimuon`; unsupported datasets are
|
|
109
|
+
rejected before connecting to the database. If the SQL filter selects no events,
|
|
110
|
+
validation reports that it did not run and returns `passed=False`.
|
|
111
|
+
|
|
112
|
+
## ADL benchmark
|
|
113
|
+
|
|
114
|
+
`adl.py` implements the eight IRIS-HEP ADL benchmark queries (Q1 MET, Q2/Q3 jet
|
|
115
|
+
pT, Q4 ≥2 jets, Q5 OS dimuon, Q6 trijet, Q7 lepton-cleaned HT, Q8 MT of MET +
|
|
116
|
+
lead lepton) on reconstructed NanoAOD events using `vector`. No real NanoAOD file
|
|
117
|
+
ships here — `make-nano` writes a synthetic one; point `--from-root` at a real
|
|
118
|
+
CMS Open Data NanoAOD file to run on data. Scope: only muons are present, so
|
|
119
|
+
Q7/Q8 use muons as leptons; Q6 omits the b-tag term (no b-tag branch yet).
|
|
120
|
+
|
|
121
|
+
## Awkward from source
|
|
122
|
+
|
|
123
|
+
`pixi.toml` builds Awkward from a local `./awkward` clone (path dependencies:
|
|
124
|
+
`awkward-cpp` compiled, `awkward` editable) rather than a wheel — a `{ git=... }`
|
|
125
|
+
spec resolves to the released wheel. Verify with `pixi run check-awkward`: the
|
|
126
|
+
`file:` path should resolve into `./awkward/src/awkward/`. On macOS the source
|
|
127
|
+
build pins `MACOSX_DEPLOYMENT_TARGET` so the compiled wheel matches conda-forge's
|
|
128
|
+
Python; if a build is rejected as `macosx_15_0` incompatible, run
|
|
129
|
+
`MACOSX_DEPLOYMENT_TARGET=11.0 pixi install`.
|
|
130
|
+
|
|
131
|
+
## Benchmarks
|
|
132
|
+
|
|
133
|
+
`benchmark.py` compares backends on identical data for the dimuon Z→μμ selection
|
|
134
|
+
(opposite-charge pair, 60–120 GeV):
|
|
135
|
+
|
|
136
|
+
- **awkward** — pure Awkward (uproot read → reconstruct → analyze in memory)
|
|
137
|
+
- **hybrid** — MonetDB + Awkward: data pre-loaded, the event cut pushed to SQL,
|
|
138
|
+
survivors reconstructed and finished in Awkward
|
|
139
|
+
- **monetdb** — the *whole* selection in SQL via a `dimuon_mass` user-defined
|
|
140
|
+
function (`schemas/udf_dimuon.sql`): self-join the two muons per event, compute
|
|
141
|
+
the mass and filter in-database, return only the surviving masses — no Awkward
|
|
142
|
+
reconstruction. Runs on a server or embedded `monetdbe`.
|
|
143
|
+
- **rdataframe** — ROOT `RDataFrame` (gated: runs only if `import ROOT` works)
|
|
144
|
+
|
|
145
|
+
`--scale K` tiles the input into a K× larger physical ROOT file every backend
|
|
146
|
+
reads, so timings are meaningful; the query phase is timed (median of `--repeats`)
|
|
147
|
+
while one-time setup (reconstruction / DB ingest) is reported separately, since a
|
|
148
|
+
database amortizes load cost across many queries.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
awkward-monetizer bench --root-file data/cms.root --scale 50 \\
|
|
152
|
+
--backends awkward,hybrid,rdataframe --repeats 5
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The backends cross-check on selected-event count and mean mass (the harness prints
|
|
156
|
+
`agreement across backends: OK`). Pushing the physics into the database with the UDF (`monetdb` backend) returns
|
|
157
|
+
only the final selection, so it can beat the fetch-and-reconstruct `hybrid` path
|
|
158
|
+
by a wide margin on this query. The embedded hybrid backend loads via `INSERT`
|
|
159
|
+
(slow); use `--hybrid-backend server` (a real MonetDB server with `COPY INTO`) to
|
|
160
|
+
benchmark a realistic load path.
|
|
161
|
+
|
|
162
|
+
### When SQL wins vs when Awkward wins
|
|
163
|
+
|
|
164
|
+
The harness ships two analyses (`--analysis`) that show the crossover:
|
|
165
|
+
|
|
166
|
+
- `zmumu` (flat: two muons/event, a scalar mass) **favors the database** — the
|
|
167
|
+
in-DB `monetdb` backend runs the whole selection in SQL and beats
|
|
168
|
+
fetch-and-reconstruct.
|
|
169
|
+
- `trijet` (ADL Q6, jagged: every 3-jet combination, keep the mass closest to
|
|
170
|
+
172.5 GeV) **favors Awkward** — `ak.combinations(jets, 3)` is one vectorized
|
|
171
|
+
line, whereas the SQL equivalent (`schemas/udf_trijet.sql`) needs two UDFs, a
|
|
172
|
+
3-way self-join over jet triples, and a window function — and runs *slower* on
|
|
173
|
+
the same data.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
awkward-monetizer bench --analysis trijet --root-file data/nano_synth.root \\
|
|
177
|
+
--backends awkward,monetdb
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The relational store is the right tool for columnar filtering, materialization,
|
|
181
|
+
and scalar arithmetic; Awkward is the right tool for variable-length
|
|
182
|
+
combinatorics and nested (NF2) physics. The point of the hybrid design is to use
|
|
183
|
+
each where it wins, not to replace one with the other.
|
|
184
|
+
|
|
185
|
+
## Ingesting real CMS Open Data NanoAOD
|
|
186
|
+
|
|
187
|
+
The `nanoaod` dataset reads real CMS NanoAODv9 files directly (uproot pulls only
|
|
188
|
+
the branches we map, so a multi-GB file isn't fully read). Get a file — e.g. the
|
|
189
|
+
DoubleMuon sample from the [CMS NanoAOD guide](https://opendata.cern.ch/docs/cms-getting-started-nanoaod) —
|
|
190
|
+
via XRootD and stream it into a running MonetDB `hep` server in chunks:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
xrdcp root://eospublic.cern.ch//eos/opendata/cms/Run2016H/DoubleMuon/NANOAOD/UL2016_MiniAODv2_NanoAODv9-v1/2510000/127C2975-1B1C-A046-AABF-62B77E757A86.root .
|
|
194
|
+
awkward-monetizer ingest 127C2975-*.root --dataset nanoaod --database hep \\
|
|
195
|
+
--create-schema --step-size "100 MB" --entry-stop 200000
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`--step-size` streams with `uproot.iterate` (each chunk is offset so `event_id`
|
|
199
|
+
stays globally unique), keeping memory flat regardless of file size;
|
|
200
|
+
`--create-schema` (re)creates the tables first. Then the `nanoaod` ADL queries
|
|
201
|
+
and benchmarks run on real physics.
|
|
202
|
+
|
|
203
|
+
`--step-size 50000` sets an entry count instead of a memory size. Add `--dry-run`
|
|
204
|
+
to build and summarize chunks without connecting to MonetDB, or `--truncate`
|
|
205
|
+
to clear the dataset's tables once before loading the chunks.
|
|
206
|
+
|
|
207
|
+
## Testing
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pytest # or: pixi run test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The ADL suite runs on generated synthetic data; the dimuon and round-trip tests
|
|
214
|
+
use `data/cms.root` and skip cleanly if it isn't present.
|
|
215
|
+
|
|
216
|
+
## Status
|
|
217
|
+
|
|
218
|
+
Ingestion, reconstruction, the live MonetDB round-trip, and ADL Q1–Q8 are
|
|
219
|
+
implemented and validated (round-trip verified on a real MonetDB server; the
|
|
220
|
+
reconstructed dimuon mass matches the stored `M` to ~1e-8). See `docs/roadmap.md`
|
|
221
|
+
for what's next (real NanoAOD data, ROOT/RDataFrame benchmarks, Arrow Flight).
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
BSD-3-Clause. Contributions welcome — see `CONTRIBUTING.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# data/
|
|
2
|
+
|
|
3
|
+
Data files are **not** committed (see `.gitignore`). Put inputs here.
|
|
4
|
+
|
|
5
|
+
## `cms.root` — dimuon sample
|
|
6
|
+
The flat CMS dimuon teaching ntuple (tree `events`, 2,304 events; branches
|
|
7
|
+
`Run, Event, Type, M` and two muons as `pt1/pt2`, `Q1/Q2`, …). It's the default
|
|
8
|
+
input for the `dimuon` dataset. This is a well-known CMS open-data outreach file
|
|
9
|
+
(the "Dimuon" / DoubleMu sample); drop your copy here as `data/cms.root`.
|
|
10
|
+
|
|
11
|
+
## `nano_synth.root` — synthetic NanoAOD
|
|
12
|
+
Generated on demand for the NanoAOD path and the ADL benchmark:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
awkward-monetizer make-nano # writes data/nano_synth.root
|
|
16
|
+
# or: python examples/make_sample_nanoaod.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To run the ADL queries on **real** data, point `--from-root` at a CMS Open Data
|
|
20
|
+
NanoAOD file instead.
|