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.
Files changed (32) hide show
  1. awkward_monetizer-0.1.1/.gitignore +50 -0
  2. awkward_monetizer-0.1.1/LICENSE +28 -0
  3. awkward_monetizer-0.1.1/PKG-INFO +256 -0
  4. awkward_monetizer-0.1.1/README.md +225 -0
  5. awkward_monetizer-0.1.1/data/README.md +20 -0
  6. awkward_monetizer-0.1.1/examples/analysis.py +143 -0
  7. awkward_monetizer-0.1.1/examples/benchmark.py +31 -0
  8. awkward_monetizer-0.1.1/examples/make_sample_nanoaod.py +24 -0
  9. awkward_monetizer-0.1.1/pyproject.toml +59 -0
  10. awkward_monetizer-0.1.1/src/awkward_monetizer/__init__.py +35 -0
  11. awkward_monetizer-0.1.1/src/awkward_monetizer/adl.py +139 -0
  12. awkward_monetizer-0.1.1/src/awkward_monetizer/benchmark.py +408 -0
  13. awkward_monetizer-0.1.1/src/awkward_monetizer/cli.py +269 -0
  14. awkward_monetizer-0.1.1/src/awkward_monetizer/datasets.py +123 -0
  15. awkward_monetizer-0.1.1/src/awkward_monetizer/db.py +87 -0
  16. awkward_monetizer-0.1.1/src/awkward_monetizer/ingest.py +210 -0
  17. awkward_monetizer-0.1.1/src/awkward_monetizer/physics.py +49 -0
  18. awkward_monetizer-0.1.1/src/awkward_monetizer/reconstruct.py +135 -0
  19. awkward_monetizer-0.1.1/src/awkward_monetizer/roundtrip.py +88 -0
  20. awkward_monetizer-0.1.1/src/awkward_monetizer/sampledata.py +50 -0
  21. awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/dimuon.sql +26 -0
  22. awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/jets.sql +10 -0
  23. awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/nanoaod.sql +34 -0
  24. awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/udf_dimuon.sql +15 -0
  25. awkward_monetizer-0.1.1/src/awkward_monetizer/schemas/udf_trijet.sql +35 -0
  26. awkward_monetizer-0.1.1/tests/conftest.py +30 -0
  27. awkward_monetizer-0.1.1/tests/test_adl.py +58 -0
  28. awkward_monetizer-0.1.1/tests/test_benchmark.py +49 -0
  29. awkward_monetizer-0.1.1/tests/test_cli.py +60 -0
  30. awkward_monetizer-0.1.1/tests/test_ingest.py +33 -0
  31. awkward_monetizer-0.1.1/tests/test_reconstruct.py +25 -0
  32. 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.