diff --git a/binder/packedselection.ipynb b/binder/packedselection.ipynb new file mode 100644 index 000000000..b042a15b8 --- /dev/null +++ b/binder/packedselection.ipynb @@ -0,0 +1,1745 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0ef5ce86-6598-46ad-8fd4-c074b13c06d2", + "metadata": {}, + "source": [ + "# PackedSelection in Coffea 2023\n", + "\n", + "In `coffea`, `PackedSelection` is a class that can store several boolean arrays in a memory-efficient manner and evaluate arbitrary combinations of boolean requirements in an CPU-efficient way. Supported inputs include 1D numpy or awkward arrays and it has built-in functionalities to form analysis in signal and control regions, and to implement cutflow or \"N-1\" plots.\n", + "\n", + "Although `coffea` 2023 should be used in delayed mode (using `dask-awkward`), we will first present these functionalities eagerly (like in `coffea` 0.7) to showcase this better. Let's first read a sample file of 40 Drell-Yan events to demonstrate the utilities using our `NanoAODSchema` as our schema." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d144affc-9918-4642-940e-148335eed6b7", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iason/fun/coffea_dev/coffea/binder/coffea/nanoevents/schemas/nanoaod.py:215: RuntimeWarning: Missing cross-reference index for FatJet_genJetAK8Idx => GenJetAK8\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
[{FsrPhoton: [], Electron: [], SoftActivityJetHT5: 63.5, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 64, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [Electron, Electron], SoftActivityJetHT5: 130, ...},\n",
+       " {FsrPhoton: [], Electron: [Electron, Electron], SoftActivityJetHT5: 25.8, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 172, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 54.4, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 96.2, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 19, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 9.36, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 115, RawMET: ..., ...},\n",
+       " ...,\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 49.6, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 14.7, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 22.1, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 33.9, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 16.2, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 28.4, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 16.1, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 28.5, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 7, RawMET: {...}, ...}]\n",
+       "--------------------------------------------------------------------------------\n",
+       "type: 40 * event
" + ], + "text/plain": [ + ", ...] type='40 * event'>" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import awkward as ak\n", + "import numpy as np\n", + "from coffea.nanoevents import NanoEventsFactory, NanoAODSchema\n", + "from matplotlib import pyplot as plt\n", + "\n", + "\n", + "events = NanoEventsFactory.from_root(\n", + " {\"../tests/samples/nano_dy.root\": \"Events\"},\n", + " metadata={\"dataset\": \"nano_dy\"},\n", + " schemaclass=NanoAODSchema,\n", + " permit_dask=False,\n", + ").events()\n", + "\n", + "events" + ] + }, + { + "cell_type": "markdown", + "id": "919582c0-9dc0-40d0-8e25-076dc4a848cd", + "metadata": {}, + "source": [ + "Now let's import `PackedSelection`, and create an instance of it." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e4c80e4f-f4bd-4a92-b5fe-4c9faaf22bf7", + "metadata": {}, + "outputs": [], + "source": [ + "from coffea.analysis_tools import PackedSelection\n", + "\n", + "selection = PackedSelection()" + ] + }, + { + "cell_type": "markdown", + "id": "18124df0-d370-4c3e-9e25-a9500eb1daf1", + "metadata": {}, + "source": [ + "We can create a boolean mask and add this to our selection by using the `add` method. This adds the following \"cut\" to our selection and names it \"twoElectron\"." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b3d0e60b-48b6-4c8b-bb1a-7d8e295cd23b", + "metadata": {}, + "outputs": [], + "source": [ + "selection.add(\"twoElectron\", ak.num(events.Electron) == 2)" + ] + }, + { + "cell_type": "markdown", + "id": "324c3acb-eb43-440a-a23e-3e31dded970a", + "metadata": {}, + "source": [ + "We've added one \"cut\" to our selection. Now let's add a couple more." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0e45733b-36dc-43b5-94d2-6cc9f974e830", + "metadata": {}, + "outputs": [], + "source": [ + "selection.add(\"eleOppSign\", ak.sum(events.Electron.charge, axis=1) == 0)\n", + "selection.add(\"noElectron\", ak.num(events.Electron) == 0)" + ] + }, + { + "cell_type": "markdown", + "id": "71edfec0-537d-42a7-86df-f96423f69371", + "metadata": {}, + "source": [ + "To avoid repeating calling `add` multiple times, we can just use the `add_multiple` method which does just that." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f16c75b5-71bf-4957-835e-efabda6978cf", + "metadata": {}, + "outputs": [], + "source": [ + "selection.add_multiple(\n", + " {\n", + " \"twoMuon\": ak.num(events.Muon) == 2,\n", + " \"muOppSign\": ak.sum(events.Muon.charge, axis=1) == 0,\n", + " \"noMuon\": ak.num(events.Muon) == 0,\n", + " \"leadPt20\": ak.any(events.Electron.pt >= 20.0, axis=1)\n", + " | ak.any(events.Muon.pt >= 20.0, axis=1),\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d0e968df-afdd-413b-a7ab-29f05f1e25c4", + "metadata": {}, + "source": [ + "By viewing the `PackedSelection` instance, one can see the names of the added selections, whether it is operating in delayed mode or not, the number of added selections and the maximum supported number of selections." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "927d8de4-53b7-4250-b24c-17fa16c16526", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PackedSelection(selections=('twoElectron', 'eleOppSign', 'noElectron', 'twoMuon', 'muOppSign', 'noMuon', 'leadPt20'), delayed_mode=False, items=7, maxitems=32)\n" + ] + } + ], + "source": [ + "print(selection)" + ] + }, + { + "cell_type": "markdown", + "id": "434fa647-9e54-4678-adf5-c75e25cc032e", + "metadata": {}, + "source": [ + "To evaluate a boolean mask (e.g. to filter events) we can use the `selection.all(*names)` function, which will compute the logical AND of all listed boolean selections." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0055353a-735b-4ac7-a2f1-698d2d42008b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([False, False, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "selection.all(\"twoElectron\", \"noMuon\", \"leadPt20\")" + ] + }, + { + "cell_type": "markdown", + "id": "b81d7b70-29cd-46b1-8d23-8f538b109c54", + "metadata": {}, + "source": [ + "We can also be more specific and require that a specific set of selections have a given value (with the unspecified ones allowed to be either true or false) using `selection.require`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5e6a478e-e69b-4181-8f8a-398bd6810375", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([False, False, False, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "selection.require(twoElectron=True, noMuon=True, eleOppSign=False)" + ] + }, + { + "cell_type": "markdown", + "id": "52a11d5f-3d39-4146-b38d-f783039ccac9", + "metadata": {}, + "source": [ + "There exist also the `allfalse` and `any` methods where the first one is the opposite of `all` and the second one is a logical OR between all listed boolean selections." + ] + }, + { + "cell_type": "markdown", + "id": "9e36a33b-2fcc-4380-8a0c-9c3402315e61", + "metadata": {}, + "source": [ + "Using `PackedSelection`, we are now able to perform an N-1 style selection using the `nminusone(*names)` method. This will perform an N-1 style selection by using as \"N\" the provided names and will exclude each named cut one at a time in order. In the end it will also peform a selection using all N cuts." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "77bc1bdc-3449-425a-8853-e2622fc4ed94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "NminusOne(selections=('twoElectron', 'noMuon', 'leadPt20'))" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nminusone = selection.nminusone(\"twoElectron\", \"noMuon\", \"leadPt20\")\n", + "nminusone" + ] + }, + { + "cell_type": "markdown", + "id": "164c7fb8-f582-4892-8e78-a54e47820721", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "This returns an `NminusOne` object which has the following methods: `result()`, `print()`, `yieldhist()`, `to_npz()` and `plot_vars()`" + ] + }, + { + "cell_type": "markdown", + "id": "7945c67d-5488-426c-b81c-56e2d09cee96", + "metadata": {}, + "source": [ + "Let's look at the results of the N-1 selection." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "eef67293-1f22-4ea0-ad7a-ac95a424a14c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ('labels', 'nev', 'masks')\n" + ] + } + ], + "source": [ + "res = nminusone.result()\n", + "print(type(res), res._fields)" + ] + }, + { + "cell_type": "markdown", + "id": "7cdcfac7-ec59-447d-a285-609332042306", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "This is just a `namedtuple` with the attributes `labels`, `nev` and `masks`. So we can say:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5ee9d9e6-b496-4cc3-9d36-578ce096ff57", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(['initial', 'N - twoElectron', 'N - noMuon', 'N - leadPt20', 'N'],\n", + " [40, 10, 3, 5, 3],\n", + " [array([False, True, True, False, False, False, False, False, False,\n", + " True, False, False, False, False, False, True, True, False,\n", + " False, False, True, True, False, False, False, False, False,\n", + " True, False, True, False, False, False, True, False, False,\n", + " False, False, False, False]),\n", + " array([False, False, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False]),\n", + " array([False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " True, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False]),\n", + " array([False, False, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False])])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "labels, nev, masks = res\n", + "labels, nev, masks" + ] + }, + { + "cell_type": "markdown", + "id": "e321f2b4-3e35-423c-a0a2-7b878ae7a260", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "`labels` is a list of labels of each mask that is applied, `nev` is a list of the number of events that survive each mask, and `masks` is a list of boolean masks (arrays) of which events survive each selection.\n", + "You can also choose to print the statistics of your N-1 selection in a similar fashion to `RDataFrame`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "abe984e8-a4f1-4769-8d7c-60ce565b2d56", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N-1 selection stats:\n", + "Ignoring twoElectron : pass = 10 all = 40 -- eff = 25.0 %\n", + "Ignoring noMuon : pass = 3 all = 40 -- eff = 7.5 %\n", + "Ignoring leadPt20 : pass = 5 all = 40 -- eff = 12.5 %\n", + "All cuts : pass = 3 all = 40 -- eff = 7.5 %\n" + ] + } + ], + "source": [ + "nminusone.print()" + ] + }, + { + "cell_type": "markdown", + "id": "282ebb23-70f8-43e1-bb60-99770fcf3543", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Or get a histogram of your total event yields. This just returns a `hist.Hist` object and we can plot it with its backends to `mplhep`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "ef6b795a-3205-4850-a366-791bf625f094", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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1FQ1b95cq1atkpeXV95VDQAAXFauw0fLli1ljPnD7TabTaNGjdKoUaNuqTAAAFA4sbYLAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBUnoePN998UzabLcetTp06ef00AADARRXPjwetX7++vvrqq/89SfF8eRoAAOCC8iUVFC9eXAEBAfnx0ABgGWOMLl7JdHYZRU4J92Ky2WzOLgP5KF/Cx/79+1WpUiV5eXkpPDxcMTExCgoKumFfu90uu93uuJ+enp4fJQFArl28kql6I1Y7u4wiZ/eoCJX0YMS8MMvzOR9hYWGaO3euVq1apRkzZigxMVH33HOPfv311xv2j4mJka+vr+MWGBiY1yUBAIACJM+jZceOHR1fN2zYUGFhYapatao++eQTPfXUU9f1j46O1uDBgx3309PTCSAACpwfX2+rkh7FnF1GoXXhcqaavP3VX3dEoZDv41plypRRrVq1dODAgRtu9/T0lKenZ36XAQC3pKRHMQ4FAHkk36/zcf78eR08eFAVK1bM76cCAAAuIM/Dx0svvaQNGzbo8OHD+v7779WtWzcVK1ZMPXr0yOunAgAALijPxxCPHz+uHj166MyZM/L391fz5s21ZcsW+fv75/VTAQAAF5Tn4WPhwoV5/ZAAAKAQYW0XAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBSrJIEuLgLlzOdXUKhxb51Dva7NUq4F5PNZnPKcxM+ABfHMuQobPibtsbuURFOW6mZwy4AAMBSjHwALqiEezHtHhXh7DKKlBLuxZxdQqHG37Q1LlzOLBAjS4QPwAXZbDanDZcC+YG/6aKFwy4AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACA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PDzUtGlTxz575plnNGbMGI0ePVrbt29XyZIl1b17d3377bcsHXCTsk+n7dGjh1q3bq25c+cqIyND48eP1+TJk1WiRAktWLBAc+bM0UMPPaT27dsrISFBDRo0cHbpKOqcnX6KqosXLzq7hCItewQjNTXVGGNMcnKyGTBggAkLCzNvv/22o99LL71k3NzczMqVK825c+fML7/8YoYOHWr8/f3N7t27nVK7K/j222+Nu7u7Y0L1b//ea9WqZaZMmeKs0gqVQ4cOmdDQUDN16lRjjDHnz583FStWNCEhIaZhw4aOs7GAgoaRDyfx8vJydglFVlZWltzc3BQfH6/g4GDt379fFSpU0KuvvqqmTZtq+fLleuuttyRduxDTo48+qi5duqhJkybq1KmTPv74Y61evVp169Z18k/ifObaoVtJ1/ZrtubNm6tNmzZ64okndPLkSXl5eckYowsXLqhkyZLy9fV1VsmFym233abu3bura9euSkpKUuPGjdWtWzetWbNG7u7umjJliuNvGShIWNUWRUp28NixY4fuuecePf3005o4caJjpdqUlBSNGTNGW7ZsUZcuXfTqq69KklauXKkzZ87otttuU+PGjVW5cmUn/yQFQ/Z+W79+vT7//HP5+fkpIiJC//jHP7RlyxYNGzZM586d03vvvSdjjL788kvNnDlTW7duVbVq1ZxdfqGQlpYmX19fDRkyRMePH9d7770nX19fDRgwQJ9//rkaNGigOXPmsKIyChTCB4qM3waP8PBwDRo0SKNHj3Zs/+WXX+Tv76+UlBTFxMRo8+bNuv/++1lq/C+sXr1anTp1UufOnfXtt98qJCRETzzxhPr06aO4uDiNGjVKX331lQIDA1WsWDHNnz9fd955p7PLdjnZQS8xMVGpqany9PRUUFCQvL29JUldu3ZVqVKlNH/+fElS//79VaNGDT322GPy9/d3ZunAdQgfKFJ+/vlnNWrUSCNHjlR0dLSjffTo0Vq5cqVWrlyp0qVL69SpUxozZox+/PFHtWjRIsdEVPzP8ePH9c4776hu3brq27evjh8/rmHDhikxMVF9+vTR008/LUmKj49XmTJl5OPjwxvhTcgOHkuWLNHgwYPl6+urM2fO6J577lGfPn3Url07DRkyRNu3b1fz5s2Vnp6uefPmadu2bawvhAKJOR8oMjIzM/Xhhx/q6tWr6tSp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "h, labels = nminusone.yieldhist()\n", + "h.plot1d()\n", + "plt.xticks(plt.gca().get_xticks(), labels, rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0080510c-aa7c-4492-b1a1-28601d4e7a27", + "metadata": {}, + "source": [ + "You can also save the results of the N-1 selection to a `.npz` file for later use." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d6bb3514-403c-441a-bf85-d88304e556c4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "labels: ['initial' 'N - twoElectron' 'N - noMuon' 'N - leadPt20' 'N']\n", + "nev: [40 10 3 5 3]\n", + "masks: [[False True True False False False False False False True False False\n", + " False False False True True False False False True True False False\n", + " False False False True False True False False False True False False\n", + " False False False False]\n", + " [False False True False False False False False False False False False\n", + " False False False False False False False False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False False True True False False False False False False False False\n", + " False False False False False False True False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False False True False False False False False False False False False\n", + " False False False False False False False False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]]\n" + ] + } + ], + "source": [ + "nminusone.to_npz(\"nminusone_results.npz\")\n", + "\n", + "with np.load(\"nminusone_results.npz\") as f:\n", + " for i in f.files:\n", + " print(f\"{i}: {f[i]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "abfab631-e4bd-40b7-a3e8-faff7c87ed76", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Finally, we can ask from this object to create histograms of different variables, masking them with our N-1 selection.\n", + "What it will output is a list of histograms, one for each requested variable, where the x-axis is the distribution of the variable, and the y-axis is the mask that was applied.\n", + "It is essentially slices of how the variable distribution evolves as each N-1 or N selection is applied. It does also return a list of labels of the masks to keep track.\n", + "\n", + "Note that the variables are parsed using a dictonary of `name: array` pairs and that the arrays will of course be flattened to be histogrammed." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b21100ec-06e8-4e94-9966-925a512212b7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([Hist(\n", + " Regular(20, 5.81891, 60.0685, name='Ept'),\n", + " Integer(0, 5, name='N-1'),\n", + " storage=Double()) # Sum: 60.0,\n", + " Hist(\n", + " Regular(20, -2.93115, 3.11865, name='Ephi'),\n", + " Integer(0, 5, name='N-1'),\n", + " storage=Double()) # Sum: 60.0],\n", + " ['initial', 'N - twoElectron', 'N - noMuon', 'N - leadPt20', 'N'])" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hs, labels = nminusone.plot_vars(\n", + " {\"Ept\": events.Electron.pt, \"Ephi\": events.Electron.phi}\n", + ")\n", + "hs, labels" + ] + }, + { + "cell_type": "markdown", + "id": "ef70ef89-f1d8-4692-a2fd-044d20ffd706", + "metadata": {}, + "source": [ + "And we can actually plot those histograms using again the `mplhep` backend." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d7ba7d28-b862-4f59-97df-27a1df3361f5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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RikpKUm5ublBH6u2tlazZs1Sp06d1Lp1aw0ePFiFhYXe17/++mtNnDhRnTp1UqtWrdS/f/86wWJNTY0mT56s2NhYJScn67HHHqt3rFatWikpKUk9evRQTk6OevXqpbVr1yo/P19z587VRx995M2K5ufn67LLLtPLL7+ssWPH6pJLLtFVV12lBQsW6LXXXvNmNP/yl7/o008/1TPPPKO0tDSNGTNG8+fP1+OPP64TJ04Ebd0AAED4aZaBZ0REhBYuXKilS5fq4MGDQR3rjjvu0ObNm7Vy5Up9/PHHuuGGGzR69Gjt3r1bknT8+HENGjRIb7zxhnbs2KFf/vKXuvnmm7V161ZvH/fdd5+Kior06quv6i9/+YsKCwu1ffv2844dExOjEydOaMKECbr33nt16aWXqry8XOXl5ZowYUK976msrFR8fLwiI08/0GDz5s3q37+/OnTo4L0nKytLVVVV2rlzZ7191NbWqqqqyqcBAICGczrTScYzwK677jqlpaUpOzs7aGMcOHBAK1as0KpVqzRs2DBdcsklmjVrloYOHaoVK1ZIkjp16qRZs2YpLS1NPXr00K9+9SuNHj1aL774oiSpurpay5cv16OPPqqRI0eqf//+KigoOOceS7fbrWeeeUYff/yxrrrqKsXExCg2NlaRkZFKSkpSUlKSYmJi6rzvq6++0vz58/XLX/7Se+3w4cM+Qack78+HDx+ud/zc3FwlJCR4W0pKin8LBwBAmHM64CTwDIJFixapoKBAu3btOu+9t956q2JjY72tIT755BO53W717t3b571FRUXau3evpNNB4vz589W/f3+1a9dOsbGxWr9+vQ4cOCBJ2rt3r06cOKHBgwd7+23Xrp369OlTZ7zf//73io2NVUxMjKZPn667775bt912W4PmWlVVpWuvvVapqanKyclp0HvOZs6cOaqsrPS2srKyRvUHAADCQ7N+gPzw4cOVlZWlOXPmaOrUqee8d968eZo1a5Zf/VdXVysiIkLFxcWKiIjwee1M8PrII49o8eLF+t3vfqf+/furdevWmjlz5gXtn5w0aZIefPBBxcTEKDk5WS1aNOy/G44dO6bRo0crLi5Oq1evVsuWLb2vJSUl+ZT9JenLL7/0vlYfl8sll8vl9/wBAMBpTmUcvzsH05p14ClJeXl5SktLqzeD+G2JiYlKTEz0q+/LL79cbrdbFRUVGjZsWL33bNq0SePGjdNNN90kSfJ4PPrss8+UmpoqSbrkkkvUsmVLbdmyRV26dJEkHTlyRJ999plGjBjh01dCQoJ69uxZ7zhRUVFyu911rldVVSkrK0sul0tr165VdHS0z+sZGRlasGCBKioqvJ9/w4YNio+P984RAAAElm1bsh0OPJ0Yv1mX2iWpf//+mjRpkpYsWRLwvnv37q1JkyZp8uTJeuWVV7Rv3z5t3bpVubm5euONNyRJvXr10oYNG/Tee+9p165d+o//+A9vRlE6nRm95ZZbdN999+mtt97Sjh07NHXq1AZnM8/o1q2b9u3bp5KSEn311VfeA0BXX321ampqtHz5clVVVenw4cM6fPiwN0i9+uqrlZqaqptvvlkfffSR1q9fr1//+teaMWMGWU0AABBQzT7jKZ0uo7/wwgtB6XvFihX6z//8T91777364osvdPHFF+v//b//px/96EeSpF//+tf6/PPPlZWVpVatWumXv/ylxo8f7/MQ90ceeUTV1dUaO3as4uLidO+999Z5yPv5XH/99XrllVd05ZVX6ujRo1qxYoW6deumLVu2SFKdTOm+ffvUrVs3RURE6PXXX9dtt92mjIwMtW7dWlOmTNG8efMauTIAAOBszjzE3ek5mGbZtu3Eg+vRjFRVVSkhIUHDMrMVGRl9/jf44e+/qLt9IBB6TCwJSr8ILvfIQUHpN2JjcVD6PfTAD4LSb4dttUHpN1jrgOBrav82giUY63Dq1HH9rXCu93GEjXXmd+bgNXcqsrWzlcVTNbXaMn5JwD5bQzT7UjsAAABCQ1iU2gEAAEJJuB4uIvAEAAAwjMcpAQAAwIhwzXiyxxMAAABGkPEEAAAwzA6BUjt7PAEAAMKALcnpB1o6MTyldgAAABhBxhMAAMAwjyxZYfjNRQSeAAAAhnGqHQAAAAgiMp4AAACGeWxLFg+QBwAAQLDZdgicandgfErtAAAAMIKMJwAAgGHheriIwBMAAMAwAk8AAAAYEa6Hi9jjCQAAACPIeAIAABgWrqfaCTwBAAAMOx14Or3H0/yYlNoBAABgBBlPAAAAwzjVDgAAACPsfzen52AapXYAAAAYQcYTAADAMErtAAAAMCNMa+2U2gEAAEz7d8bTySY/Mp7Lli3TgAEDFB8fr/j4eGVkZOjNN9/0+2MTeAIAAOCcOnfurLy8PBUXF2vbtm266qqrNG7cOO3cudOvfii1AwAAGNbUvrlo7NixPj8vWLBAy5Yt0/vvv69LL720wf0QeAIAABgWSoeLqqqqfK67XC65XK6zvs/tdmvVqlWqqalRRkaGX2MSeCJgIgpLFGG1DGifPTYGtDsAAPAdKSkpPj9nZ2crJyenzn2ffPKJMjIydPz4ccXGxmr16tVKTU31aywCTwAAANP8PNwTtDlIKisrU3x8vPfy2bKdffr0UUlJiSorK/XSSy9pypQpKioq8iv4JPAEAAAwLJT2eJ45qX4+UVFR6tmzpyRp0KBB+uCDD7R48WI9+eSTDR6TU+0AAADwm8fjUW1trV/vIeMJAABgWhN7gPycOXM0ZswYdenSRceOHdNzzz2nwsJCrV+/3q8hCTwBAAAMC6VT7Q1RUVGhyZMnq7y8XAkJCRowYIDWr1+vH/7wh36NSeAJAACAc1q+fHlA+iHwBAAAcILTpXYHEHgCAAAY1tRK7YFC4AkAAGBaEztcFCg8TgkAAABGkPEEAAAwzvp3c3oOZhF4AgAAmEapHQAAAAgeMp4AAACmhWnGk8ATAADANNs63Zyeg2GU2gEAAGAEGU8AAADDbPt0c3oOphF4AgAAmBamezwptQMAAMAIMp4AAACmhenhIgJPAAAAwyz7dHN6DqYReAIAAJjGHk8AAAAgeMh4AgAAmMYeTwAAABhBqR0AAAAIHjKeAAAApoVpxpPAEwAAwLQwDTwDWmrftWuXevToEcguAQAA0EwENON54sQJ/f3vfw9klwAAAM0Pp9rP75577jnn6//4xz8aNRkAAIBwwDcXNcDixYuVlpam+Pj4el+vrq4OyKQAAADQ/Pi1x7Nnz566++679fbbb9fbnnrqqWDNs9GmTp0qy7KUl5fnc33NmjWyLIdT3f+Wn58vy7LUr1+/Oq+tWrVKlmWpW7du5icGAAACyw6RZphfgWd6erqKi4vP+rplWbJtp49onV10dLQWLVqkI0eOOD2Vs2rdurUqKiq0efNmn+vLly9Xly5dHJoVAABA4/kVeD722GOaOXPmWV8fOHCgPB5PY+cUNKNGjVJSUpJyc3MD3ndmZqbuvPNO3X///WrXrp2SkpKUk5Pjc8+BAwc0btw4xcbGKj4+XjfeeKO+/PJLn3siIyP1s5/9TH/84x+91w4ePKjCwkL97Gc/87l36tSpGj9+vM+1mTNnKjMz0/tzbW2t7rzzTiUmJio6OlpDhw7VBx984H29sLBQlmVp48aNSk9PV6tWrfSDH/xApaWljVsQAABwVpb+b5+nY82Bz+1X4JmUlKSuXbsGay5BFxERoYULF2rp0qU6ePBgwPsvKChQ69attWXLFj388MOaN2+eNmzYIEnyeDwaN26c/vnPf6qoqEgbNmzQ559/rgkTJtTpZ9q0aXrxxRf1zTffSDpdgh89erQ6dOjg95zuv/9+vfzyyyooKND27dvVs2dPZWVl6Z///KfPfQ8++KAee+wxbdu2TZGRkZo2bdpZ+6ytrVVVVZVPAwAAOJ9GP8fz9ttv11dffRWIuRhx3XXXKS0tTdnZ2QHve8CAAcrOzlavXr00efJkpaena+PGjZKkjRs36pNPPtFzzz2nQYMGafDgwXr66adVVFTkk4GUpMsvv1w9evTQSy+9JNu2lZ+ff85A8Gxqamq0bNkyPfLIIxozZoxSU1P11FNPKSYmRsuXL/e5d8GCBRoxYoRSU1M1e/Zsvffeezp+/Hi9/ebm5iohIcHbUlJS/J4bAABh7czjlJxuhjU68HzmmWeaXMZr0aJFKigo0K5du85776233qrY2FhvO5cBAwb4/JycnKyKigpJpx+un5KS4hOkpaamqk2bNvXOY9q0aVqxYoWKiopUU1Oja665piEfzcfevXt18uRJDRkyxHutZcuW+v73v19nzG/PPTk5WZK8c/+uOXPmqLKy0tvKysr8nhsAAGHN6UNFTeFwUX1C+TDR2QwfPlxZWVmaM2fOee+dN2+eSkpKvO1cWrZs6fOzZVkXvOd10qRJev/995WTk6Obb75ZkZF1n3zVokWLOut/8uTJCxrv23M/c8r/bHN3uVyKj4/3aQAAAOcT0K/MbEry8vL02muv1Tk9/l2JiYnq2bOnt12ofv36qayszCc7+Omnn+ro0aNKTU2tc3+7du304x//WEVFRWcts7dv317l5eU+174dHF9yySWKiorSpk2bvNdOnjypDz74oN4xAQCAIU5nOptqxvPYsWNN8vvZ+/fvr0mTJmnJkiVGxhs1apR3zO3bt2vr1q2aPHmyRowYofT09Hrfk5+fr6+++kp9+/at9/WrrrpK27Zt09NPP63du3crOztbO3bs8L7eunVr3Xbbbbrvvvu0bt06ffrpp5o+fbq++eYb3XLLLUH5nAAA4PwcP9FuO/PNRX4Fni1atFBERMQ5W30l4VA1b948Y49/sixLr776qtq2bavhw4dr1KhR6tGjh1544YWzvicmJkYXXXTRWV/PysrSQw89pPvvv19XXHGFjh07psmTJ/vck5eXp+uvv14333yzvve972nPnj1av3692rZtG7DPBgAA0BCW7ccmzVdfffWsr23evFlLliyRx+M562loNE9VVVVKSEhQpsYp0mp5/jcAF8g9clBQ+o3YePYvxmiMQw/8ICj9dthWG5R+g7UOCL6m9m8jWIKxDqdOHdffCueqsrIyIGcazvzO7PafC9QiOjoAM7xwnuPHtf/XDwbsszWEX+nJcePG1blWWlqq2bNn67XXXtOkSZM0b968gE0OAACgWXJoj2WdORh2wXs8Dx06pOnTp6t///46deqUSkpKVFBQ0KQfMA8AAIDg8TvwrKys1AMPPKCePXtq586d2rhxo1577TVddtllwZgfAABAs+P0oSKnDhf5VWp/+OGHtWjRIiUlJen555+vt/QOAACA83Dom4PqzMEwvwLP2bNnKyYmRj179lRBQYEKCgrqve+VV14JyOQAAACapTDd4+lX4Dl58mTvt9oAAAAA/vAr8MzPzw/SNAAAAMKHU3ssvzsH05rO094BAACaizAttYftd7UDAADALDKeAAAApoVAqT3kDxcBAAAgACi1AwAAAMFDxhMAAMC0MM14EngCAAAYFq6PU6LUDgAAACMIPAEAAGAEpXYAAADT2OMJAAAAE9jjCQAAAAQRGU8AAAAnOF1qdwCBJwAAgGlhuseTUjsAAACMIOMJAABgWLgeLiLwBAAAMI1SOwAAABA8ZDwBAAAMC9dSOxlPAAAA0+wQaQ2Um5urK664QnFxcUpMTNT48eNVWlrq98cm8AQAAMA5FRUVacaMGXr//fe1YcMGnTx5UldffbVqamr86odSOwAAgGlN7HDRunXrfH7Oz89XYmKiiouLNXz48Ab3Q+AJAABgWCjt8ayqqvK57nK55HK5zvneyspKSVK7du38GpPAE0DAuUcOCkq/X6af+/8IL1j6D4LTbxNz6IHgrEOHbbVB6TdiY3FQ+gWMCKGMZ0pKis/l7Oxs5eTknPVtHo9HM2fO1JAhQ3TZZZf5NSSBJwAAQBgrKytTfHy89+fzZTtnzJihHTt26N133/V7LAJPAAAA00Io4xkfH+8TeJ7LHXfcoddff13vvPOOOnfu7PeQBJ4AAACGhdIez4awbVu/+tWvtHr1ahUWFqp79+4XNCaBJwAAAM5pxowZeu655/Tqq68qLi5Ohw8fliQlJCQoJiamwf3wHE8AAADTnH5wvJ+l/mXLlqmyslKZmZlKTk72thdeeMGvj03GEwAAwLCmWGoPBDKeAAAAMIKMJwAAgGkhdKrdJAJPAAAA08I08KTUDgAAACPIeAIAABhm/bs5PQfTCDwBAABMC9NSO4EnAACAYU3tcUqBwh5PAAAAGEHGEwAAwDRK7QAAADDG6cDTAZTaAQAAYAQZTwAAAMPC9XARgScAAIBpYbrHk1I7AAAAjCDjCQAAYBildgAAAJhBqR0AAAAIHjKeAAAAhlFqBwAAgBlhWmon8AQAADAtTANP9ngCAADACDKeAAAAhrHHEwAAAGZQagcAAACCh4wnAACAYZZty7KdTXk6MT4ZTwOmTp2q8ePHOz0NAAAQKuwQaYY5GnhOnTpVlmUpLy/P5/qaNWtkWVaj+8/MzNTMmTMb3c+5FBYWyrKsetvhw4eDMiaBLAAAaIocz3hGR0dr0aJFOnLkiNNTaZTS0lKVl5f7tMTEREfndPLkSUfHBwAA9Ttzqt3pZprjgeeoUaOUlJSk3NzcgPY7depUFRUVafHixd4M5P79+5Wenq5HH33Ue9/48ePVsmVLVVdXS5IOHjwoy7K0Z88eSdKRI0c0efJktW3bVq1atdKYMWO0e/fuOuMlJiYqKSnJp7VoUf/yejwe5ebmqnv37oqJidHAgQP10ksv+dyzc+dO/ehHP1J8fLzi4uI0bNgw7d27Vzk5OSooKNCrr77q/VyFhYXav3+/LMvSCy+8oBEjRig6OlrPPvusPB6P5s2bp86dO8vlciktLU3r1q3zjnPmfa+88oquvPJKtWrVSgMHDtTmzZsb/b8BAAA4C6dL7OFYapekiIgILVy4UEuXLtXBgwcD1u/ixYuVkZGh6dOnezOQKSkpGjFihAoLCyVJtm3rb3/7m9q0aaN3331XklRUVKROnTqpZ8+ekk4HsNu2bdPatWu1efNm2bata665plHZxNzcXD399NN64okntHPnTt1999266aabVFRUJEn64osvNHz4cLlcLr311lsqLi7WtGnTdOrUKc2aNUs33nijRo8e7f1cP/jBD7x9z549W3fddZd27dqlrKwsLV68WI899pgeffRRffzxx8rKytKPf/zjOsHzgw8+qFmzZqmkpES9e/fWxIkTderUqXrnX1tbq6qqKp8GAABwPiFxqv26665TWlqasrOztXz58oD0mZCQoKioKLVq1UpJSUne65mZmVq+fLncbrd27NihqKgoTZgwQYWFhRo9erQKCws1YsQISdLu3bu1du1abdq0yRvcPfvss0pJSdGaNWt0ww03ePvt3Lmzz/hdu3bVzp0768yrtrZWCxcu1F//+ldlZGRIknr06KF3331XTz75pEaMGKHHH39cCQkJWrlypVq2bClJ6t27t7ePmJgY1dbW+nyuM2bOnKmf/OQn3p8fffRRPfDAA/rpT38qSVq0aJHefvtt/e53v9Pjjz/uvW/WrFm69tprJUlz587VpZdeqj179qhv3751xsjNzdXcuXPrXXcAAHB+4foAeccznmcsWrRIBQUF2rVr13nvvfXWWxUbG+tt/hg2bJiOHTumDz/8UEVFRRoxYoQyMzO9WdCioiJlZmZKknbt2qXIyEgNHjzY+/6LLrpIffr0qTPPv/3tbyopKfG2P//5z/WOv2fPHn3zzTf64Q9/6PMZnn76ae3du1eSVFJSomHDhnmDTn+kp6d7/1xVVaVDhw5pyJAhPvcMGTKkzvwHDBjg/XNycrIkqaKiot4x5syZo8rKSm8rKyvze54AAIQ1p0vsDpXaQyLjKUnDhw9XVlaW5syZo6lTp57z3nnz5mnWrFkXNE6bNm00cOBAFRYWavPmzfrhD3+o4cOHa8KECfrss8+0e/dub8bTH927d1ebNm3Oe9+ZvaRvvPGGOnXq5POay+WSdDqjeaFat259Qe/7dpB75okCHo+n3ntdLpd3rgAAwH/hmvEMmcBTkvLy8pSWlqY+ffqc877ExMQGnRiPioqS2+2uc33EiBF6++23tXXrVi1YsEDt2rVTv379tGDBAiUnJ3vL2v369dOpU6e0ZcsWb6n966+/VmlpqVJTUy/gE0qpqalyuVw6cODAWQPcAQMGqKCgQCdPnqw363m2z/Vd8fHx6tixozZt2uQz1qZNm/T973//guYPAABwoUKm1C5J/fv316RJk7RkyZKA9NetWzdt2bJF+/fv11dffeXN4GVmZmr9+vWKjIz07mHMzMzUs88+6xOg9erVS+PGjdP0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HaOiotSmTRufe1nHuj755BPFxsbK5XLp1ltv1erVq5Wamsoa+mHlypXavn27cnNz67zGOp7f4MGDlZ+fr3Xr1mnZsmXat2+fhg0bpmPHjrF+fvj888+1bNky9erVS+vXr9dtt92mO++8UwUFBZL4/RJIfGUmEMZmzJihHTt2+OwJQ8P16dNHJSUlqqys1EsvvaQpU6aoqKjI6Wk1GWVlZbrrrru0YcMGRUdHOz2dJmnMmDHePw8YMECDBw9W165d9eKLLyomJsbBmTUtHo9H6enpWrhwoSTp8ssv144dO/TEE09oypQpDs+ueSHjiWYjKSlJkuqc2Pzyyy+9r+H/3HHHHXr99df19ttvq3Pnzt7rSUlJOnHihI4ePepzP+tYV1RUlHr27KlBgwYpNzdXAwcO1OLFi1nDBiouLlZFRYW+973vKTIyUpGRkSoqKtKSJUsUGRmpDh06sI5+atOmjXr37q09e/bw99APycnJSk1N9bnWr18/77YFfr8EDoEnmo3u3bsrKSlJGzdu9F6rqqrSli1blJGR4eDMQott27rjjju0evVqvfXWW+revbvP64MGDVLLli191rG0tFQHDhxgHc/D4/GotraWNWygkSNH6pNPPlFJSYm3paena9KkSd4/s47+qa6u1t69e5WcnMzfQz8MGTKkzmPlPvvsM3Xt2lUSv18CyunTTYA/jh07Zn/44Yf2hx9+aEuyf/vb39offvih/fe//922bdvOy8uz27RpY7/66qv2xx9/bI8bN87u3r27/a9//cvhmYeO2267zU5ISLALCwvt8vJyb/vmm2+899x66612ly5d7Lfeesvetm2bnZGRYWdkZDg469Aze/Zsu6ioyN63b5/98ccf27Nnz7Yty7L/8pe/2LbNGl6ob59qt23W8Xzuvfdeu7Cw0N63b5+9adMme9SoUfbFF19sV1RU2LbN+jXU1q1b7cjISHvBggX27t277WeffdZu1aqV/cwzz3jv4fdLYBB4okl5++23bUl12pQpU2zbPv3Ii4ceesju0KGD7XK57JEjR9qlpaXOTjrE1Ld+kuwVK1Z47/nXv/5l33777Xbbtm3tVq1a2dddd51dXl7u3KRD0LRp0+yuXbvaUVFRdvv27e2RI0d6g07bZg0v1HcDT9bx3CZMmGAnJyfbUVFRdqdOnewJEybYe/bs8b7O+jXca6+9Zl922WW2y+Wy+/bta//hD3/weZ3fL4Fh2bZtO5NrBQAAQDhhjycAAACMIPAEAACAEQSeAAAAMILAEwAAAEYQeAIAAMAIAk8AAAAYQeAJAAAAIwg8AQAAYASBJwCEiKlTp8qyrDpt9OjRDXr//v37ZVmWSkpKgjtRALhAkU5PAADwf0aPHq0VK1b4XHO5XA7NBgACi4wnAIQQl8ulpKQkn9a2bVtJkmVZWrZsmcaMGaOYmBj16NFDL730kve93bt3lyRdfvnlsixLmZmZTnwEADgrAk8AaEIeeughXX/99froo480adIk/fSnP9WuXbskSVu3bpUk/fWvf1V5ebleeeUVJ6cKAHUQeAJACHn99dcVGxvr0xYuXOh9/YYbbtAvfvEL9e7dW/Pnz1d6erqWLl0qSWrfvr0k6aKLLlJSUpLatWvnyGcAgLNhjycAhJArr7xSy5Yt87n27QAyIyPD57WMjAwOEwFoMgg8ASCEtG7dWj179nR6GgAQFJTaAaAJef/99+v83K9fP0lSVFSUJMntdhufFwA0BBlPAAghtbW1Onz4sM+1yMhIXXzxxZKkVatWKT09XUOHDtWzzz6rrVu3avny5ZKkxMRExcTEaN26dercubOio6OVkJBg/DMAwNmQ8QSAELJu3TolJyf7tKFDh3pfnzt3rlauXKkBAwbo6aef1vPPP6/U1FRJpwPUJUuW6Mknn1THjh01btw4pz4GANTLsm3bdnoSAIDzsyxLq1ev1vjx452eCgBcEDKeAAAAMILAEwAAAEZwuAgAmgh2RgFo6sh4AgAAwAgCTwAAABhB4AkAAAAjCDwBAABgBIEnAAAAjCDwBAAAgBEEngAAADCCwBMAAABGEHgCAADAiP8P6Fcn6WrgLY8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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R4/WI488ovvvv9+rWEtKSjRo0CDFxcVpypQpXh1TlUmTJqm4uNg9CgoKajQfAABAVa7ohwL07dtXycnJmjRpktLS0qrdd+rUqRo/frxP85eWlqpevXravn276tWr5/HehYR39uzZWrBggebPn6+uXbuqcePGSk9Pv6x+0BEjRuiJJ55QaGioWrRooaAg7/5f49SpUxowYIDCw8O1evVqNWjQwP2e3W73aEmQpGPHjrnfq0xwcLCCg4N9jh8AAJxnhcqm2et764pOViVp5syZio+Pr7RSebGoqChFRUX5NPf1118vh8Oh48eP65Zbbql0n48//lhDhgzR7373O0mS0+nUl19+qbi4OEnSNddcowYNGmjLli1q06aNJOnkyZP68ssvlZiY6DFXZGSkYmJiKl2nYcOGcjgcFbaXlJQoOTlZwcHBeuuttxQSEuLxfkJCgqZPn67jx4+7P/+GDRsUERHhjhEAANQul8sml8nJotnre+uKbgOQpK5du2rEiBFauHBhrc/dqVMnjRgxQqmpqXrzzTd18OBBbd26VRkZGXr33XclSR07dtSGDRu0adMm7d27V//v//0/d+VSOl+BHTVqlCZMmKD3339fu3fvVlpamtdV0wvatWungwcPKi8vTydOnHBfBHX77berrKxMy5YtU0lJiYqKilRUVORObG+//XbFxcXprrvu0qeffqr169frySef1JgxY6ieAgAA013xlVXp/Ff8r776ql/mXr58uZ555hk9+uij+uabb3T11Vfrpptu0s9//nNJ0pNPPqmvv/5aycnJatSokX7/+98rJSXF48b8s2fPVmlpqQYPHqzw8HA9+uijFW7cfym/+tWv9Oabb6pfv376/vvvtXz5crVr105btmyRpAoV2YMHD6pdu3aqV6+e3nnnHd1///1KSEhQ48aNNXLkSE2dOrWGZwYAAFTlwo35zY4hENhcLlegPMAAFlVSUqLIyEglaYjq2xpc+oAr2IH5N/ll3mvSN/tlXn/54c7el97JQkJXbzE7BEvw19+bv86vv/69SVLLHP/8auRnzb/88TPhPH1ahx9/0n3rx5q68Duz95qHVL+xud9gnisr15aUhbX22fzlim8DAAAAQOCqE20AAAAAVsIFVt4jWQUAADAYt67yHskqAACAwaiseo+eVQAAAFgWlVUAAACDuSzQBhAolVWSVQAA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qsTERIWEhOiVV16R0+nU1KlT1bp1awUHBys+Pl7r1q1zr3PhuDfffFP9+vVTo0aN1L17d+Xm5tb47wAAAFTB7K//aQPwXr169TRjxgwtWrRIR44cqbV5FyxYoISEBI0ePdpd6YyOjlZiYqKys7MlSS6XS//3f/+nJk2a6KOPPpIk5eTkqFWrVoqJiZF0Pundtm2b3nrrLeXm5srlcumOO+6oUdUyIyNDL730kp5//nnt2bNHjzzyiH73u98pJydHkvTNN9+ob9++Cg4O1vvvv6/t27frnnvu0blz5zR+/Hj9+te/1oABA9yfq0+fPu65J06cqIcfflh79+5VcnKyFixYoLlz52rOnDn67LPPlJycrF/84hcVEu4nnnhC48ePV15enjp16qThw4fr3LlzlcZfXl6ukpISjwEAAOAPlrgbwJ133qn4+HhNnjxZy5Ytq5U5IyMj1bBhQzVq1Eh2u929PSkpScuWLZPD4dDu3bvVsGFDDRs2TNnZ2RowYICys7OVmJgoSfrqq6/01ltv6eOPP3YnhK+88oqio6O1Zs0aDR061D1v69atPdZv27at9uzZUyGu8vJyzZgxQ//617+UkJAgSerQoYM++ugjLV26VImJiVq8eLEiIyO1cuVKNWjQQJLUqVMn9xyhoaEqLy/3+FwXpKen65e//KX79Zw5c/T444/rN7/5jSRp1qxZ+uCDDzR//nwtXrzYvd/48eM1aNAgSdLTTz+ta6+9Vvv371fnzp0rrJGRkaGnn3660vMOAAAuzQpfw5u9vrdMr6xeMGvWLGVlZWnv3r2X3Pe+++5TWFiYe/jilltu0alTp7Rz507l5OQoMTFRSUlJ7mprTk6OkpKSJEl79+5V/fr11bt3b/fxP/nJTxQbG1shzv/7v/9TXl6ee7z33nuVrr9//3795z//0W233ebxGV566SUdOHBAkpSXl6dbbrnFnaj6olevXu4/l5SU6OjRo7r55ps99rn55psrxN+tWzf3n1u0aCFJOn78eKVrTJo0ScXFxe5RUFDgc5wAANRpZn/9H0BtAJaorEpS3759lZycrEmTJiktLa3afadOnarx48df1jpNmjRR9+7dlZ2drdzcXN12223q27evhg0bpi+//FJfffWVu7Lqi/bt26tJkyaX3O9Cb+y7776rVq1aebwXHBws6Xzl9HI1btz4so67ODG+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1L17dyUnJ+v48eNmh2YZZWVl6t69uxYvXmx2KJaUk5OjMWPGaPPmzdqwYYPOnj2r22+/XWVlZWaHZhmtW7fWzJkztX37dm3btk233nqrhgwZoj179pgdGirBrasAA82ePVtLlizR119/bXYolpOZman09HR9//33Zodiqt69e+uGG27Qs88+K+n8E2Kio6P14IMPauLEiSZHZz02m02rV69WSkqK2aFY1rfffquoqCjl5OSob9++ZodjWU2bNtXs2bM1atQos0PBj1BZBQxUXFyspk2bmh0GLOrMmTPavn27+vfv794WFBSk/v37Kzc318TIEMiKi4slif/2VMHhcGjlypUqKyvz+TGgMAZPsAIMsn//fi1atEhz5swxOxRY1IkTJ+RwONxPg7mgefPm+uKLL0yKCoHM6XQqPT1dN998s89PDbrS7dq1SwkJCTp9+rTCwsK0evVqxcXFmR0WKkFlFfDRxIkTZbPZqh0/Tiy++eYbDRgwQEOHDtXo0aNNitw4l3OOANS+MWPGaPfu3Vq5cqXZoVhObGys8vLytGXLFt1///0aOXKkPv/8c7PDQiWorAI+evTRR5WWllbtPh06dHD/+ejRo+rXr5/69OmjF154wc/RWYOv5wjnXX311apXr56OHTvmsf3YsWOy2+0mRYVANXbsWL3zzjv68MMP1bp1a7PDsZyGDRsqJiZGktSzZ0998sknWrBggZYuXWpyZPgxklXAR82aNVOzZs282vebb75Rv3791LNnTy1fvlxBQXXjywxfzhH+p2HDhurZs6c2btzovmDI6XRq48aNGjt2rLnBIWC4XC49+OCDWr16tbKzs9W+fXuzQwoITqdT5eXlZoeBSpCsAn7yzTffKCkpSW3bttWcOXP07bffut+jSvY/+fn5+u6775Sfny+Hw6G8vDxJUkxMjMLCwswNzgTjxo3TyJEj1atXL914442aP3++ysrKdPfdd5sdmmWUlpZq//797tcHDx5UXl6emjZtqjZt2pgYmTWMGTNGK1as0Nq1axUeHq6ioiJJUmRkpEJDQ02OzhomTZqkgQMHqk2bNjp16pRWrFih7OxsrV+/3uzQUAluXQX4SWZmZpUJBv/s/ictLU1ZWVkVtn/wwQdKSkoyPiALePbZZzV79mwVFRUpPj5eCxcuVO/evc0OyzKys7PVr1+/CttHjhypzMxM4wOyGJvNVun25cuXX7I9p64YNWqUNm7cqMLCQkVGRqpbt256/PHHddttt5kdGipBsgoAAADLqhsNdAAAAAhIJKsAAACwLJJVAAAAWBbJKgAAACyLZBUAAACWRbIKAAAAyyJZBQAAgGWRrAJAADt06JBsNpv7yV+VyczMVJMmTQyLCQBqE8kqAJgoLS1NNputwhgwYECtrTFs2DB9+eWXtTYfABipvtkBAEBdN2DAAC1fvtxjW3BwcK3NHxoayjPhAQQsKqsAYLLg4GDZ7XaPcdVVV0k6/5z3JUuWaODAgQoNDVWHDh30+uuvV5jj66+/Vr9+/dSoUSN1795dubm57vdoAwAQyEhWAcDinnrqKf3qV7/Sp59+qhEjRug3v/mN9u7d67HPE088ofHjxysvL0+dOnXS8OHDde7cOZMiBoDaQ7IKACZ75513FBYW5jFmzJjhfn/o0KG699571alTJ02bNk29evXSokWLPOYYP368Bg0apE6dOunpp5/W4cOHtX//fqM/CgDUOnpWAcBk/fr105IlSzy2NW3a1P3nhIQEj/cSEhIqXP3frVs3959btGghSTp+/Lg6d+5cy9ECgLFIVgHAZI0bN1ZMTEyN5mjQoIH7zzabTZLkdDprNCcAWAFtAABgcZs3b67wukuXLiZFAwDGorIKACYrLy9XUVGRx7b69evr6quvliStWrVKvXr10k9/+lO98sor2rp1q5YtW2ZGqABgOJJVADDZunXr3H2mF8TGxuqLL76QJD399NNauXKlHnjgAbVo0UJ///vfFRcXZ0aoAGA4m8vlcpkdBACgcjabTatXr1ZKSorZoQCAKehZBQAAgGWRrAIAAMCy6FkFAAujUwtAXUdlFQAAAJZFsgoAAADLIlkFAACAZZGsAgAAwLJIVgEAAGBZJKsAAACwLJJVAAAAWBbJKgAAACyLZBUAAACW9f8BKTUhIJBMVKYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for h in hs:\n", + " h.plot2d()\n", + " plt.yticks(plt.gca().get_yticks(), labels, rotation=0)\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f78249e8-8ebe-43b3-8dd5-b996db6cfa2d", + "metadata": {}, + "source": [ + "You can slice these histograms to view and plot the 1D histogram at each step of the selection. For example, if we want the $P_T$ of the electrons at the final step (index 4) of the selection, we can do the following." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2cda4d43-4933-4fe1-a19b-1db74a5b62d0", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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Cxfqww8/VHl5uf8rLi5OS5cu9f9v1rL7GhsbdfHiRY0bN44/l100d+7cdr9a5ZNPPtHEiRMl9YPvP71+CzkGlBs3bljnz5+3zp8/b0mytm7dap0/f9763//9X8uyLOvll1+2HnzwQeu9996zfv/731uPP/64NXnyZOvzzz8P8OT9zzPPPGOFhYVZv/nNb6wrV674v5qbm/37/PCHP7SioqKsEydOWKWlpVZiYqKVmJgYwKn7p3Xr1lknT560qqurrd///vfWunXrLJvNZh09etSyLNbxbnz103CWxVp2xXPPPWf95je/saqrq60zZ85YKSkp1ujRo62rV69alsVadkVJSYllt9utl156yfr000+tt99+2xo6dKj1n//5n/59Avn9h1hCG0VFRZakdl+ZmZmWZf3l45vr16+3IiIirNDQUGvhwoVWVVVVYIfupzpaR0nW7t27/ft8/vnn1o9+9CNr5MiR1tChQ62MjAzrypUrgRu6n3r66aetiRMnWiEhIdaYMWOshQsX+kPJsljHu/H1WGIt79zixYutcePGWSEhIZbT6bQWL15sXbhwwf88a9k1v/rVr6wZM2ZYoaGh1kMPPWS9+eabbZ4P5Pcfm2VZVu9fvwIAABiYuGcJAADAgFgCAAAwIJYAAAAMiCUAAAADYgkAAMCAWAIAADAglgAAAAyIJQAAAANiCQAAwIBYAnDfeuqpp2Sz2dp9ffe7372j1//P//yPbDabysvLe3dQAAFlD/QAABBI3/3ud7V79+4220JDQwM0DYD+iCtLAO5roaGhioyMbPM1cuRISZLNZlNOTo7cbreGDBmiKVOm6Be/+IX/tZMnT5YkzZ49WzabTQsWLAjEWwDQy4glADBYv369vve97+l3v/udli5dqu9///v66KOPJEklJSWSpOPHj+vKlSs6cOBAIEcF0EuIJQD3tUOHDmnYsGFtvjZt2uR//sknn9Tf//3f65vf/KZ+8pOfKC4uTq+//rokacyYMZKk8PBwRUZGatSoUQF5DwB6F/csAbivJSUlKScnp822r0ZPYmJim+cSExO5oRu4zxBLAO5rDzzwgL7xjW8EegwA/Rg/hgMAgw8++KDd42nTpkmSQkJCJEmtra19PheAvsOVJQD3tS+++EK1tbVtttntdo0ePVqS9O677youLk6PPPKI3n77bZWUlGjnzp2SpLFjx2rIkCEqKCjQ+PHjNXjwYIWFhfX5ewDQu7iyBOC+VlBQoHHjxrX5euSRR/zP/9u//Zv279+vmTNn6j/+4z/0s5/9TDExMZL+ElWvvfaa3njjDTkcDj3++OOBehsAepHNsiwr0EMAQH9ks9mUn5+vRYsWBXoUAAHElSUAAAADYgkAAMCAG7wBoBPcpQBA4soSAACAEbEEAABgQCwBAAAYEEsAAAAGxBIAAIABsQQAAGBALAEAABgQSwAAAAb/Dzx9urnAPgsTAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "hs[0][:, 4].plot1d(yerr=0)\n", + "plt.yscale(\"log\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "328c05ef-6ddf-478d-93f2-c260436b4b0e", + "metadata": {}, + "source": [ + "Because this automatic bining doesn't look great, for $P_T$ at least, the user has the ability to customize the axes or pass in their own axes objects." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "10c5faad-e4a0-458c-9097-26acee2bba12", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on method plot_vars in module coffea.analysis_tools:\n", + "\n", + "plot_vars(vars, axes=None, bins=None, start=None, stop=None, edges=None, transform=None) method of coffea.analysis_tools.NminusOne instance\n", + " Plot the histograms of variables for each step of the N-1 selection\n", + " \n", + " Parameters\n", + " ----------\n", + " vars : dict\n", + " A dictionary in the form ``{name: array}`` where ``name`` is the name of the variable,\n", + " and ``array`` is the corresponding array of values.\n", + " The arrays must be the same length as each mask of the N-1 selection.\n", + " axes : list of hist.axis objects, optional\n", + " The axes objects to histogram the variables on. This will override all the following arguments that define axes.\n", + " Must be the same length as ``vars``.\n", + " bins : iterable of integers or Nones, optional\n", + " The number of bins for each variable histogram. If not specified, it defaults to 20.\n", + " Must be the same length as ``vars``.\n", + " start : iterable of floats or integers or Nones, optional\n", + " The lower edge of the first bin for each variable histogram. If not specified, it defaults to the minimum value of the variable array.\n", + " Must be the same length as ``vars``.\n", + " stop : iterable of floats or integers or Nones, optional\n", + " The upper edge of the last bin for each variable histogram. If not specified, it defaults to the maximum value of the variable array.\n", + " Must be the same length as ``vars``.\n", + " edges : list of iterables of floats or integers, optional\n", + " The bin edges for each variable histogram. This overrides ``bins``, ``start``, and ``stop`` if specified.\n", + " Must be the same length as ``vars``.\n", + " transform : iterable of hist.axis.transform objects or Nones, optional\n", + " The transforms to apply to each variable histogram axis. If not specified, it defaults to None.\n", + " Must be the same length as ``vars``.\n", + " \n", + " Returns\n", + " -------\n", + " hists : list of hist.Hist or hist.dask.Hist objects\n", + " A list of 2D histograms of the variables for each step of the N-1 selection.\n", + " The first axis is the variable, the second axis is the N-1 selection step.\n", + " labels : list of strings\n", + " The bin labels of y axis of the histogram.\n", + "\n" + ] + } + ], + "source": [ + "help(nminusone.plot_vars)" + ] + }, + { + "cell_type": "markdown", + "id": "5c3cd75f-858b-41a3-8b0b-060361a51eba", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Cutflow is implemented in a similar manner to the N-1 selection. We just have to use the `cutflow(*names)` function which will return a `Cutflow` object" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a856a24d-487d-4308-8236-4f89bb5917ca", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Cutflow(selections=('noMuon', 'twoElectron', 'leadPt20'))" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cutflow = selection.cutflow(\"noMuon\", \"twoElectron\", \"leadPt20\")\n", + "cutflow" + ] + }, + { + "cell_type": "markdown", + "id": "ec84e2a3-b2ab-4352-b716-d568814f3300", + "metadata": {}, + "source": [ + "The methods of this object are similar to the `NminusOne` object. The only difference is that now we seperate things in either \"onecut\" or \"cutflow\". \"onecut\" represents results where each cut is applied alone, while \"cutflow\" represents results where the cuts are applied cumulatively in order." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "96692df8-d25b-4e57-9eea-59acdc1a7f78", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ('labels', 'nevonecut', 'nevcutflow', 'masksonecut', 'maskscutflow')\n" + ] + }, + { + "data": { + "text/plain": [ + "(['initial', 'noMuon', 'twoElectron', 'leadPt20'],\n", + " [40, 28, 5, 17],\n", + " [40, 28, 5, 3],\n", + " [array([ True, True, True, True, False, False, False, True, True,\n", + " True, False, True, True, True, False, True, True, True,\n", + " True, True, True, True, True, False, False, True, False,\n", + " True, False, True, False, False, True, True, False, True,\n", + " True, True, True, True]),\n", + " array([False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " True, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False]),\n", + " array([False, True, True, False, True, True, True, False, False,\n", + " True, False, False, False, False, False, True, True, False,\n", + " False, False, True, True, False, True, True, False, True,\n", + " True, False, True, False, True, False, True, False, False,\n", + " False, False, False, False])],\n", + " [array([ True, True, True, True, False, False, False, True, True,\n", + " True, False, True, True, True, False, True, True, True,\n", + " True, True, True, True, True, False, False, True, False,\n", + " True, False, True, False, False, True, True, False, True,\n", + " True, True, True, True]),\n", + " array([False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " True, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False]),\n", + " array([False, False, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False])])" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = cutflow.result()\n", + "print(type(res), res._fields)\n", + "labels, nevonecut, nevcutflow, masksonecut, maskscutflow = res\n", + "labels, nevonecut, nevcutflow, masksonecut, maskscutflow" + ] + }, + { + "cell_type": "markdown", + "id": "e2a06c5f-23d0-4ad0-ab36-dfec7796b8d4", + "metadata": {}, + "source": [ + "As you can see, again we have the same `labels`, `nev` and `masks` only now we have two \"versions\" of them since they've been split into \"onecut\" and \"cutflow\".\n", + "You can again print the statistics of the cutflow exactly like `RDataFrame`." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "4db657a5-f16f-4fef-9956-649269e4ae2c", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cutflow stats:\n", + "Cut noMuon : pass = 28 cumulative pass = 28 all = 40 -- eff = 70.0 % -- cumulative eff = 70.0 %\n", + "Cut twoElectron : pass = 5 cumulative pass = 5 all = 40 -- eff = 12.5 % -- cumulative eff = 12.5 %\n", + "Cut leadPt20 : pass = 17 cumulative pass = 3 all = 40 -- eff = 42.5 % -- cumulative eff = 7.5 %\n" + ] + } + ], + "source": [ + "cutflow.print()" + ] + }, + { + "cell_type": "markdown", + "id": "e6aa8edf-3953-410d-b46f-f7d54adda821", + "metadata": {}, + "source": [ + "Again, you can extract yield hists, only now there are two of them." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "e9e3db1b-8836-4a85-8e56-5819134b7343", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "image/png": "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nasrV65o7dq17H10MI75AG7C/v371b9/f1WsWFELFixQo0aN7OvWrl2rp59+Wo0aNdLrr78uT09PdvE6CWOMdu/erX79+qlx48Zq166dZs6cqXfeeUfdunXLtW1iYqJ8fX11+fJleXp6Omji4mPt2rXy8/NTYGCgjDE6e/as7rnnHvXu3VtjxozR5cuXVb16dT3wwAOaOXOmbDab0tPTtWvXLp09e1YtWrTgoo2FAPEB3KQDBw5o8ODBat26tUaPHp0rQD7//HPVr1+f83g4IWOMdu3apaFDh+rw4cNat26dunbtqqysLJUoUUKSNGHCBO3cuVMxMTGEhwWSkpIUEhKijh076oknntCtt96qS5cu6fbbb9fSpUvl5uam9u3bq3v37nr99dclSevWrVPDhg15a3shwzEfwE0KDAzUW2+9pT179mjOnDk6dOiQfV3Xrl0JDyfx+7/DbDabWrdurYULF6p27dp68cUXc4XH5MmTNXfuXM2cOZPwsEjlypX14Ycf6uDBg5o1a5YOHjyoEiVK6PLly9q8ebO6du2qsLAw+xlmf/jhBy1evNh+jA4KD+IDyAfNmzfXm2++qQMHDmjq1Kn6/vvvHT0S8uC3p+KOiorSY489ppiYGP36669q06aNlixZori4OPsp1adPn64ZM2Zo27ZtCg4OdvD0xUtQUJAWLFigffv2ac6cOTp//rxGjBihxx57TPXq1dMbb7xhD8Q33nhDBw8eVOPGjR08NX6Pl12AfLR792498cQTevfdd1WlShVHj4M8+OijjzRo0CC1a9dOFy5c0LfffquBAwfqscceU5MmTfT111/rvvvu008//SQXFxdt27ZNLVq0cPTYxdb+/fv10EMPqWXLlurfv7/WrVun2bNnKyoqSpJ08uRJLVmyRFu3blWzZs0cOyz+gPgA8hnX4XE+J0+eVNeuXTVx4kQNGzZMkvTuu+/qxRdf1G233aZp06bJx8dH27dv19SpU/Xvf/+bX2iFwP79+zVs2DC1bNlS4eHhiouL0/z58+Xl5aVatWrpX//6F3s9CiniA0Cxd+TIEXXt2lVLlixRu3bt7MuXLVumYcOG6fPPP1fbtm2VlZWlzMxM4rIQ2bdvnx555BE1a9ZMzz//vHx9fe3vcOF5Krw45gNAsZPzN1d6erqka6fivnDhgjIzMyVJly9fliTdf//9ql27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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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PFxPPvlkUdUOAACcUIHCx4IFC/50mZ+fn2JjY6+7IAAAULJxbxcAAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYKkChY958+YpMDBQXl5e8vLyUnBwsL744gv78szMTEVERKhy5cry9PRUeHi4UlJSCr1oAADgvAoUPmrXrq3nnntOO3bs0Pbt29WtWzf17dtXP/74oyRp3LhxWrVqlT766CPFxsbq5MmT6t+/f5EUDgAAnFO5gnTu06dPvufPPvus5s2bp61bt6p27dpasGCBlixZom7dukmS3n77bTVr1kxbt25Vhw4dCq9qAADgtK75nI+cnBwtXbpUFy5cUHBwsHbs2KHs7GyFhoba+zRt2lR16tTRli1bCqVYAADg/Ao08iFJu3fvVnBwsDIzM+Xp6anly5erefPmio+Pl6urqypVqpSvf/Xq1ZWcnPyn28vKylJWVpb9eXp6ekFLAgAATqTAIx9NmjRRfHy8tm3bpkceeURDhw7V3r17r7mAqKgoeXt72x9+fn7XvC0AAFD8FTh8uLq66sYbb1Tbtm0VFRWlli1bau7cufL19dWlS5eUmpqar39KSop8fX3/dHuRkZFKS0uzPxITEwv8QwAAAOdx3fN85ObmKisrS23btpWLi4vWr19vX5aQkKBjx44pODj4T9d3c3OzX7qb9wAAACVXgc75iIyMVK9evVSnTh2dO3dOS5Ys0caNG7V27Vp5e3tr+PDhGj9+vHx8fOTl5aXRo0crODiYK10AAIBdgcLHqVOnNGTIECUlJcnb21uBgYFau3atbr31VknS7NmzVaZMGYWHhysrK0thYWF69dVXi6RwAADgnAoUPhYsWPCXy93d3RUdHa3o6OjrKgoAAJRc3NsFAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiqQDOcAsVFxqUcR5eA6+DhUlY2m83RZQBw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BQv06quv6tKlS1q9ejWjjw7GOR/Addi5c6cGDRqkqlWrav78+fL397cvW716tZ544gn5+/vr9ddfl4eHB0O8TsIYo++//14DBgxQQECAOnXqpJkzZ+q9995Tz5498/VNTk6Wr6+vLl68KA8PDwdVXHqsXr1atWvXVmBgoIwxOn36tO68807dcccdGjt2rC5evKi6devqnnvu0cyZM2Wz2ZSZmalt27bp9OnTatu2LTdtLAYIH8B12rVrl4YOHar27dtrzJgx+QLIl19+qSZNmjCPhxMyxmjbtm0aPny49u3bpzVr1qhHjx7KyclR2bJlJUkTJ07U1q1bFRMTQ/CwQEpKioKDgxUSEqLHHntMzZo104ULF9S1a1ctXrxYrq6u6ty5s2677Ta9/vrrkqQ1a9aoefPmXNpezHDOB3CdAgMD9dZbb2n79u2aM2eO9u7da1/Wo0cPgoeT+P33MJvNpvbt22vBggVq2LChXnjhhXzBY8qUKXr11Vc1c+ZMgodFqlevro8//lh79uzRrFmztGfPHpUtW1YXL17Uxo0b1aNHD/Xq1cs+w+zRo0f1zjvv2M/RQfFB+AAKQevWrfXmm29q165deuaZZ/TTTz85uiQUwG+n4o6KitIjjzyimJgY/frrr+rQoYMWLVqkhIQE+5Tqzz77rGbMmKGvv/5aQUFBDq6+dGnTpo3mz5+vuLg4zZkzR2fPntXIkSP1yCOPqHHjxnrjjTfsAfGNN97Qnj17FBAQ4OCq8XscdgEK0ffff6/HHntM77//vmrUqOHoclAAn3zyiYYMGaJOnTrp3Llz2r17t+6991498sgjatGihb777jvdddddOnHihMqUKaOvv/5abdu2dXTZpdbOnTt1//33q127dho0aJDWrFmj2bNnKyoqSpJ05MgRLVq0SJs2bVKrVq0cWyyuQvgAChn34XE+R44cUY8ePTRp0iSNGDFCkvT+++/rhRde0M0336zp06fL29tb33zzjZ555hk9//zzfKAVAzt37tSIESPUrl07hYeHKyEhQa+99po8PT3VoEED/fvf/2bUo5gifAAo9fbv368ePXpo0aJF6tSpk719yZIlGjFihL788kt17NhROTk5ys7OJlwWI3FxcXrooYfUqlUrTZs2Tb6+vvYrXHidii/O+QBQ6uR958rMzJR0ZSruc+fOKTs7W5J08eJFSdLd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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "honecut, hcutflow, labels = cutflow.yieldhist()\n", + "\n", + "honecut.plot1d(yerr=0)\n", + "plt.xticks(plt.gca().get_xticks(), labels, rotation=45)\n", + "plt.show()\n", + "\n", + "hcutflow.plot1d(yerr=0)\n", + "plt.xticks(plt.gca().get_xticks(), labels, rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b7faec21-57c7-477d-9f7b-1a22c06c0754", + "metadata": {}, + "source": [ + "Saving to `.npz` files is again there." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ecc3be33-da27-4845-bba2-f163729670ce", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "labels: ['initial' 'noMuon' 'twoElectron' 'leadPt20']\n", + "nevonecut: [40 28 5 17]\n", + "nevcutflow: [40 28 5 3]\n", + "masksonecut: [[ True True True True False False False True True True False True\n", + " True True False True True True True True True True True False\n", + " False True False True False True False False True True False True\n", + " True True True True]\n", + " [False False True True False False False False False False False False\n", + " False False False False False False True False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False True True False True True True False False True False False\n", + " False False False True True False False False True True False True\n", + " True False True True False True False True False True False False\n", + " False False False False]]\n", + "maskscutflow: [[ True True True True False False False True True True False True\n", + " True True False True True True True True True True True False\n", + " False True False True False True False False True True False True\n", + " True True True True]\n", + " [False False True True False False False False False False False False\n", + " False False False False False False True False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False False True False False False False False False False False False\n", + " False False False False False False False False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]]\n" + ] + } + ], + "source": [ + "cutflow.to_npz(\"cutflow_results.npz\")\n", + "\n", + "with np.load(\"cutflow_results.npz\") as f:\n", + " for i in f.files:\n", + " print(f\"{i}: {f[i]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "423a72e7-79f6-4e6c-ab59-b2d709e3f9a0", + "metadata": {}, + "source": [ + "And finally, `plot_vars` is also there with the same axes customizability while now it returns two lists of histograms, one for \"onecut\" and one for \"cutflow\". Those can of course be plotted in a similar fashion." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "22454105-13f9-4f22-9450-9915cab46759", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "([Hist(\n", + " Regular(20, 5.81891, 60.0685, name='ept'),\n", + " Integer(0, 4, name='onecut'),\n", + " storage=Double()) # Sum: 73.0,\n", + " Hist(\n", + " Regular(20, -2.93115, 3.11865, name='ephi'),\n", + " Integer(0, 4, name='onecut'),\n", + " storage=Double()) # Sum: 73.0],\n", + " [Hist(\n", + " Regular(20, 5.81891, 60.0685, name='ept'),\n", + " Integer(0, 4, name='cutflow'),\n", + " storage=Double()) # Sum: 63.0,\n", + " Hist(\n", + " Regular(20, -2.93115, 3.11865, name='ephi'),\n", + " Integer(0, 4, name='cutflow'),\n", + " storage=Double()) # Sum: 63.0],\n", + " ['initial', 'noMuon', 'twoElectron', 'leadPt20'])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "h1, h2, labels = cutflow.plot_vars(\n", + " {\"ept\": events.Electron.pt, \"ephi\": events.Electron.phi}\n", + ")\n", + "h1, h2, labels" + ] + }, + { + "cell_type": "markdown", + "id": "a96edc79-1b3b-4ff9-8459-6ef28a99d629", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Now, in `coffea` 2023, everything happens in a delayed fashion. Therefore, `PackedSelection` can also operate in delayed or lazy mode and fully support `dask_awkward` arrays. Use is still the same, but everything now is\n", + "a delayed `dask` type object which can be computed whenever the user wants to. This can be done by either calling `.compute()` on the object or `dask.compute(*things)`.\n", + "\n", + "PackedSelection can be initialized to operate in delayed mode by adding a delayed `dask_awkward` array for the first time instead of a materialized `numpy` or `awkward` one.\n", + "I would like to note that we only support delayed `dask_awkward` arrays and not `dask.array` arrays. Please convert your `dask` arrays to `dask_awkward` via `dask_awkward.from_dask_array(array)`. I would also like to note that you cannot mix materialized and delayed arrays in the same `PackedSelection`. Let's now read the same events using dask and perform the exact same things." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1adf0374-eee9-471b-9582-9cbbf06d2dda", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iason/fun/coffea_dev/coffea/binder/coffea/nanoevents/schemas/nanoaod.py:215: RuntimeWarning: Missing cross-reference index for FatJet_genJetAK8Idx => GenJetAK8\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "dask.awkward" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import dask\n", + "import dask_awkward as dak\n", + "\n", + "dakevents = NanoEventsFactory.from_root(\n", + " {\"../tests/samples/nano_dy.root\": \"Events\"},\n", + " metadata={\"dataset\": \"nano_dy\"},\n", + " schemaclass=NanoAODSchema,\n", + " permit_dask=True,\n", + ").events()\n", + "\n", + "dakevents" + ] + }, + { + "cell_type": "markdown", + "id": "8cb3c5fc-8897-4860-8fb0-924655e81ef7", + "metadata": {}, + "source": [ + "Now `dakevents` is a delayed `dask_awkward` version of our events and if we compute it we get our normal events." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "a54d1fcc-13c6-4919-bbac-3d6c299f65ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[{FsrPhoton: [], Electron: [], SoftActivityJetHT5: 63.5, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 64, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [Electron, Electron], SoftActivityJetHT5: 130, ...},\n",
+       " {FsrPhoton: [], Electron: [Electron, Electron], SoftActivityJetHT5: 25.8, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 172, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 54.4, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 96.2, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 19, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 9.36, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 115, RawMET: ..., ...},\n",
+       " ...,\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 49.6, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 14.7, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 22.1, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 33.9, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 16.2, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 28.4, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [{...}], SoftActivityJetHT5: 16.1, RawMET: ..., ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 28.5, RawMET: {...}, ...},\n",
+       " {FsrPhoton: [], Electron: [], SoftActivityJetHT5: 7, RawMET: {...}, ...}]\n",
+       "--------------------------------------------------------------------------------\n",
+       "type: 40 * event
" + ], + "text/plain": [ + ", ...] type='40 * event'>" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dakevents.compute()" + ] + }, + { + "cell_type": "markdown", + "id": "bd16d282-ace9-4d86-9079-dd66a635e508", + "metadata": {}, + "source": [ + "Now we have to use `dask_awkward` instead of `awkward` and `dakevents` instead of `events` to do the same things. Let's add the same (now delayed) arrays to PackedSelection." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "dc25b728-7504-44fb-b920-051dab6c99d1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PackedSelection(selections=('twoElectron', 'eleOppSign', 'noElectron', 'twoMuon', 'muOppSign', 'noMuon', 'leadPt20'), delayed_mode=True, items=7, maxitems=32)\n" + ] + } + ], + "source": [ + "selection = PackedSelection()\n", + "\n", + "selection.add_multiple(\n", + " {\n", + " \"twoElectron\": dak.num(dakevents.Electron) == 2,\n", + " \"eleOppSign\": dak.sum(dakevents.Electron.charge, axis=1) == 0,\n", + " \"noElectron\": dak.num(dakevents.Electron) == 0,\n", + " \"twoMuon\": dak.num(dakevents.Muon) == 2,\n", + " \"muOppSign\": dak.sum(dakevents.Muon.charge, axis=1) == 0,\n", + " \"noMuon\": dak.num(dakevents.Muon) == 0,\n", + " \"leadPt20\": dak.any(dakevents.Electron.pt >= 20.0, axis=1)\n", + " | dak.any(dakevents.Muon.pt >= 20.0, axis=1),\n", + " }\n", + ")\n", + "\n", + "print(selection)" + ] + }, + { + "cell_type": "markdown", + "id": "49520c74-3bac-4355-b4b8-482ac38b4ae2", + "metadata": {}, + "source": [ + "Now, the same functions will return `dask_awkward` objects that have to be computed." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "74c43d65-824b-49e6-a6aa-aa7bef97e7d9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dask.awkward" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "selection.all(\"twoElectron\", \"noMuon\", \"leadPt20\")" + ] + }, + { + "cell_type": "markdown", + "id": "87ce9147-a37d-46ba-802c-f1531cd1cf3d", + "metadata": {}, + "source": [ + "When computing those arrays we should get the same arrays that we got when operating in eager mode." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "be4b8e7a-5ea1-45f4-b1d9-827e90331366", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[False, False, True, False, False, ..., False, False, False, False, False]\n", + "[False, False, False, True, False, ..., False, False, False, False, False]\n" + ] + } + ], + "source": [ + "print(selection.all(\"twoElectron\", \"noMuon\", \"leadPt20\").compute())\n", + "print(selection.require(twoElectron=True, noMuon=True, eleOppSign=False).compute())" + ] + }, + { + "cell_type": "markdown", + "id": "b32ece8e-90af-4808-8248-8dfe63c16a6c", + "metadata": {}, + "source": [ + "Now, N-1 and cutflow will just return only delayed objects that must be computed." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "4e79a6b3-55cc-4248-a1db-a076f4667278", + "metadata": { + "slideshow": { + "slide_type": "-" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "NminusOne(selections=('twoElectron', 'noMuon', 'leadPt20'))" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nminusone = selection.nminusone(\"twoElectron\", \"noMuon\", \"leadPt20\")\n", + "nminusone" + ] + }, + { + "cell_type": "markdown", + "id": "e661f0d1-f792-423c-b698-f031209a721f", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "It is again an `NminusOne` object which has the same methods." + ] + }, + { + "cell_type": "markdown", + "id": "8442cbf2-6c41-4174-a011-a0c451c2feb6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Let's look at the results of the N-1 selection in the same way" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "dee4c70f-d6fa-4b0a-9d04-f5555ec1856f", + "metadata": { + "slideshow": { + "slide_type": "-" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(['initial', 'N - twoElectron', 'N - noMuon', 'N - leadPt20', 'N'],\n", + " [dask.awkward,\n", + " dask.awkward,\n", + " dask.awkward,\n", + " dask.awkward,\n", + " dask.awkward],\n", + " [dask.awkward,\n", + " dask.awkward,\n", + " dask.awkward,\n", + " dask.awkward])" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "labels, nev, masks = nminusone.result()\n", + "labels, nev, masks" + ] + }, + { + "cell_type": "markdown", + "id": "fbd64ce9-fa7d-4ff6-847a-856dae236565", + "metadata": {}, + "source": [ + "Now however, you can see that everything is a dask awkward object (apart from the labels of course). If we compute them we should get the same things as before and indeed we do:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "f8fc01f5-3f07-441d-b7de-44aeeaa042ca", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "((40, 10, 3, 5, 3),\n", + " (,\n", + " ,\n", + " ,\n", + " ))" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dask.compute(*nev), dask.compute(*masks)" + ] + }, + { + "cell_type": "markdown", + "id": "1df3b553-70b9-4785-b5f3-f44cd25889a6", + "metadata": {}, + "source": [ + "We can again print the statistics, however for this to happen, the object must of course compute the delayed `nev` list." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "118ee230-eb4f-44da-9dba-05ce15e7951e", + "metadata": { + "slideshow": { + "slide_type": "-" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N-1 selection stats:\n", + "Ignoring twoElectron : pass = 10 all = 40 -- eff = 25.0 %\n", + "Ignoring noMuon : pass = 3 all = 40 -- eff = 7.5 %\n", + "Ignoring leadPt20 : pass = 5 all = 40 -- eff = 12.5 %\n", + "All cuts : pass = 3 all = 40 -- eff = 7.5 %\n" + ] + } + ], + "source": [ + "nminusone.print()" + ] + }, + { + "cell_type": "markdown", + "id": "e1146ee8-5fe8-4e05-8ebe-658014f613a3", + "metadata": {}, + "source": [ + "And now if we call `result()` again, the `nev` list is materialized." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "700e7669-0459-4e3d-b020-805562106cab", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[40, 10, 3, 5, 3]" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nminusone.result().nev" + ] + }, + { + "cell_type": "markdown", + "id": "b77aa859-7ae1-4c54-9fc4-2ab516e3a483", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "Again the histogram of your total event yields works. This time it is returns a `hist.dask.Hist` object." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "a2c4fb20-b241-4eeb-8312-ee5a645f30b6", + "metadata": { + "slideshow": { + "slide_type": "-" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "N-1\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Integer(0, 5, name='N-1')
\n", + "
\n", + "Double() Σ=0.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Integer(0, 5, name='N-1'), storage=Double()) # (has staged fills)" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "h, labels = nminusone.yieldhist()\n", + "h" + ] + }, + { + "cell_type": "markdown", + "id": "bc992bfd-cce3-4004-9e16-275d69044745", + "metadata": {}, + "source": [ + "It appears empty because it hasn't been computed yet. Let's do that." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "38a6d003-fc3c-46e5-ad3b-5d343e997dc5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "N-1\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Integer(0, 5, name='N-1')
\n", + "
\n", + "Double() Σ=61.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Integer(0, 5, name='N-1'), storage=Double()) # Sum: 61.0" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "h.compute()" + ] + }, + { + "cell_type": "markdown", + "id": "c36f391a-d2a6-4364-afcc-8da81d72ec90", + "metadata": {}, + "source": [ + "Notice that this doesn't happen in place as `h` is still not computed." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "226cd7fa-ac79-460d-aee5-856eb3362099", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "\n", + "5\n", + "\n", + "\n", + "N-1\n", + "\n", + "\n", + "\n", + "
\n", + "
\n", + "Integer(0, 5, name='N-1')
\n", + "
\n", + "Double() Σ=0.0\n", + "\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "Hist(Integer(0, 5, name='N-1'), storage=Double()) # (has staged fills)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "h" + ] + }, + { + "cell_type": "markdown", + "id": "6e496df3-8846-4678-a92a-deaf0f2cafbb", + "metadata": {}, + "source": [ + "We can again plot this histogram but we have to call plot on the computed one, otherwise it will just be empty." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "c96c48b2-53b3-460b-8019-e9f91473aef5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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1FQ1b95cq1atkpeXV95VDQAAXFauw0fLli1ljPnD7TabTaNGjdKoUaNuqTAAAFA4sbYLAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACApQgfAADAUoQPAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBShA8AAGApwgcAALBUnoePN998UzabLcetTp06ef00AADARRXPjwetX7++vvrqq/89SfF8eRoAAOCC8iUVFC9eXAEBAfnx0ABgGWOMLl7JdHYZRU4J92Ky2WzOLgP5KF/Cx/79+1WpUiV5eXkpPDxcMTExCgoKumFfu90uu93uuJ+enp4fJQFArl28kql6I1Y7u4wiZ/eoCJX0YMS8MMvzOR9hYWGaO3euVq1apRkzZigxMVH33HOPfv311xv2j4mJka+vr+MWGBiY1yUBAIACJM+jZceOHR1fN2zYUGFhYapatao++eQTPfXUU9f1j46O1uDBgx3309PTCSAACpwfX2+rkh7FnF1GoXXhcqaavP3VX3dEoZDv41plypRRrVq1dODAgRtu9/T0lKenZ36XAQC3pKRHMQ4FAHkk36/zcf78eR08eFAVK1bM76cCAAAuIM/Dx0svvaQNGzbo8OHD+v7779WtWzcVK1ZMPXr0yOunAgAALijPxxCPHz+uHj166MyZM/L391fz5s21ZcsW+fv75/VTAQAAF5Tn4WPhwoV5/ZAAAKAQYW0XAABgKcIHAACwFOEDAABYivABAAAsRfgAAACWInwAAABLET4AAIClCB8AAMBSrJIEuLgLlzOdXUKhxb51Dva7NUq4F5PNZnPKcxM+ABfHMuQobPibtsbuURFOW6mZwy4AAMBSjHwALqiEezHtHhXh7DKKlBLuxZxdQqHG37Q1LlzOLBAjS4QPwAXZbDanDZcC+YG/6aKFwy4AAMBShA8AAGApwgcAALAU4QMAAFiK8AEAACxF+AAAAJYifAAAAEsRPgAAgKUIHwAAwFKEDwAAYCnCBwAAsBThAwAAWIrwAQAALEX4AAAAliJ8AAAASxE+AACA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PDzUtGlTxz575plnNGbMGI0ePVrbt29XyZIl1b17d3377bcsHXCTsk+n7dGjh1q3bq25c+cqIyND48eP1+TJk1WiRAktWLBAc+bM0UMPPaT27dsrISFBDRo0cHbpKOqcnX6KqosXLzq7hCItewQjNTXVGGNMcnKyGTBggAkLCzNvv/22o99LL71k3NzczMqVK825c+fML7/8YoYOHWr8/f3N7t27nVK7K/j222+Nu7u7Y0L1b//ea9WqZaZMmeKs0gqVQ4cOmdDQUDN16lRjjDHnz583FStWNCEhIaZhw4aOs7GAgoaRDyfx8vJydglFVlZWltzc3BQfH6/g4GDt379fFSpU0KuvvqqmTZtq+fLleuuttyRduxDTo48+qi5duqhJkybq1KmTPv74Y61evVp169Z18k/ifObaoVtJ1/ZrtubNm6tNmzZ64okndPLkSXl5eckYowsXLqhkyZLy9fV1VsmFym233abu3bura9euSkpKUuPGjdWtWzetWbNG7u7umjJliuNvGShIWNUWRUp28NixY4fuuecePf3005o4caJjpdqUlBSNGTNGW7ZsUZcuXfTqq69KklauXKkzZ87otttuU+PGjVW5cmUn/yQFQ/Z+W79+vT7//HP5+fkpIiJC//jHP7RlyxYNGzZM586d03vvvSdjjL788kvNnDlTW7duVbVq1ZxdfqGQlpYmX19fDRkyRMePH9d7770nX19fDRgwQJ9//rkaNGigOXPmsKIyChTCB4qM3waP8PBwDRo0SKNHj3Zs/+WXX+Tv76+UlBTFxMRo8+bNuv/++1lq/C+sXr1anTp1UufOnfXtt98qJCRETzzxhPr06aO4uDiNGjVKX331lQIDA1WsWDHNnz9fd955p7PLdjnZQS8xMVGpqany9PRUUFCQvL29JUldu3ZVqVKlNH/+fElS//79VaNGDT322GPy9/d3ZunAdQgfKFJ+/vlnNWrUSCNHjlR0dLSjffTo0Vq5cqVWrlyp0qVL69SpUxozZox+/PFHtWjRIsdEVPzP8ePH9c4776hu3brq27evjh8/rmHDhikxMVF9+vTR008/LUmKj49XmTJl5OPjwxvhTcgOHkuWLNHgwYPl6+urM2fO6J577lGfPn3Url07DRkyRNu3b1fz5s2Vnp6uefPmadu2bawvhAKJOR8oMjIzM/Xhhx/q6tWr6tSp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "h.compute().plot1d()\n", + "plt.xticks(plt.gca().get_xticks(), labels, rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8ca4e1f0-f5a3-46b3-9bcb-848aa3e04a99", + "metadata": {}, + "source": [ + "And we got exactly the same thing. Saving to `.npz` files is still possible but the delayed arrays will be naturally materalized while saving." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "d0d57fbb-633b-4140-95b6-7ac551a4c271", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "labels: ['initial' 'N - twoElectron' 'N - noMuon' 'N - leadPt20' 'N']\n", + "nev: [40 10 3 5 3]\n", + "masks: [[False True True False False False False False False True False False\n", + " False False False True True False False False True True False False\n", + " False False False True False True False False False True False False\n", + " False False False False]\n", + " [False False True False False False False False False False False False\n", + " False False False False False False False False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False False True True False False False False False False False False\n", + " False False False False False False True False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]\n", + " [False False True False False False False False False False False False\n", + " False False False False False False False False True True False False\n", + " False False False False False False False False False False False False\n", + " False False False False]]\n" + ] + } + ], + "source": [ + "nminusone.to_npz(\"nminusone_results.npz\")\n", + "\n", + "with np.load(\"nminusone_results.npz\") as f:\n", + " for i in f.files:\n", + " print(f\"{i}: {f[i]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7dd71131-d0df-4930-b853-d126d7c0c2db", + "metadata": {}, + "source": [ + "Same logic applies to the `plot_vars` function. Remember to use `dakevents` now and not `events`." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "621cc56b-7bf1-4be8-b24c-04c89125e6b0", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "hs, labels = nminusone.plot_vars(\n", + " {\"Ept\": dakevents.Electron.pt, \"Ephi\": dakevents.Electron.phi}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8e94ed9c-dfd7-47d3-997a-4c7561f51147", + "metadata": {}, + "source": [ + "Those histograms are also delayed and have to be computed before plotting them." + ] + }, + { + "cell_type": "markdown", + "id": "0877deac-0e27-480d-b471-bafba2441328", + "metadata": {}, + "source": [ + "Exactly the same things apply to the cutflow in delayed mode." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 80c453052..a840f32b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ ] dependencies = [ "awkward>=2.2.3", - "uproot>=5.0.7", + "uproot>=5.0.9", "dask[array]>=2023.4.0", "dask-awkward>=2023.6.3", "dask-histogram>=2023.6.0", diff --git a/src/coffea/analysis_tools.py b/src/coffea/analysis_tools.py index 1e85eeed9..66b92fe2b 100644 --- a/src/coffea/analysis_tools.py +++ b/src/coffea/analysis_tools.py @@ -3,9 +3,17 @@ These helper classes were previously part of ``coffea.processor`` but have been migrated and updated to be compatible with awkward-array 1.0 """ +import warnings +from collections import namedtuple + import awkward +import dask.array import dask_awkward +import hist +import hist.dask import numpy +from dask_awkward.lib.core import compatible_partitions +from dask_awkward.utils import IncompatiblePartitions import coffea.processor import coffea.util @@ -409,6 +417,603 @@ def variations(self): return keys +class NminusOneToNpz: + """Object to be returned by NmiusOne.to_npz()""" + + def __init__(self, file, labels, nev, masks, saver): + self._file = file + self._labels = labels + self._nev = nev + self._masks = masks + self._saver = saver + + def __repr__(self): + return f"NminusOneToNpz(file={self._file}), labels={self._labels})" + + @property + def file(self): + return self._file + + @property + def labels(self): + return self._labels + + @property + def nev(self): + return self._nev + + @property + def masks(self): + return self._masks + + def compute(self): + self._nev = list(dask.compute(*self._nev)) + self._masks = list(dask.compute(*self._masks)) + self._saver(self._file, labels=self._labels, nev=self._nev, masks=self._masks) + + +class CutflowToNpz: + """Object to be returned by Cutflow.to_npz()""" + + def __init__( + self, file, labels, nevonecut, nevcutflow, masksonecut, maskscutflow, saver + ): + self._file = file + self._labels = labels + self._nevonecut = nevonecut + self._nevcutflow = nevcutflow + self._masksonecut = masksonecut + self._maskscutflow = maskscutflow + self._saver = saver + + def __repr__(self): + return f"CutflowToNpz(file={self._file}), labels={self._labels})" + + @property + def file(self): + return self._file + + @property + def labels(self): + return self._labels + + @property + def nevonecut(self): + return self._nevonecut + + @property + def nevcutflow(self): + return self._nevcutflow + + @property + def masksonecut(self): + return self._masksonecut + + @property + def maskscutflow(self): + return self._maskscutflow + + def compute(self): + self._nevonecut = list(dask.compute(*self._nevonecut)) + self._nevcutflow = list(dask.compute(*self._nevcutflow)) + self._masksonecut = list(dask.compute(*self._masksonecut)) + self._maskscutflow = list(dask.compute(*self._maskscutflow)) + numpy.savez( + self._file, + labels=self._labels, + nevonecut=self._nevonecut, + nevcutflow=self._nevcutflow, + masksonecut=self._masksonecut, + maskscutflow=self._maskscutflow, + ) + + +class NminusOne: + """Object to be returned by PackedSelection.nminusone()""" + + def __init__(self, names, nev, masks, delayed_mode): + self._names = names + self._nev = nev + self._masks = masks + self._delayed_mode = delayed_mode + + def __repr__(self): + return f"NminusOne(selections={self._names})" + + def result(self): + """Returns the results of the N-1 selection as a namedtuple + + Returns + ------- + result : NminusOneResult + A namedtuple with the following attributes: + + nev : list of integers or dask_awkward.lib.core.Scalar objects + The number of events in each step of the N-1 selection as a list of integers or delayed integers + masks : list of boolean numpy.ndarray or dask_awkward.lib.core.Array objects + The boolean mask vectors of which events pass the N-1 selection each time as a list of materialized or delayed boolean arrays + + """ + NminusOneResult = namedtuple("NminusOneResult", ["labels", "nev", "masks"]) + labels = ["initial"] + [f"N - {i}" for i in self._names] + ["N"] + return NminusOneResult(labels, self._nev, self._masks) + + def to_npz(self, file, compressed=False, compute=True): + """Saves the results of the N-1 selection to a .npz file + + Parameters + ---------- + file : str or file + Either the filename (string) or an open file (file-like object) + where the data will be saved. If file is a string or a Path, the + ``.npz`` extension will be appended to the filename if it is not + already there. + compressed : bool, optional + If True, the data will be compressed in the ``.npz`` file. + Default is False. + compute : bool, optional + Whether to immediately start writing or to return an object + that the user can choose when to start writing by calling compute(). + Default is True. + + Returns + ------- + out : NminusOneToNpz or None + If ``compute=True``, returns None. Otherwise, returns an object + that can be used to start writing the data by calling compute(). + """ + labels, nev, masks = self.result() + + if compressed: + saver = numpy.savez_compressed + else: + saver = numpy.savez + + out = NminusOneToNpz(file, labels, nev, masks, saver) + if compute: + out.compute() + return None + else: + return out + + def print(self): + """Prints the statistics of the N-1 selection""" + + if self._delayed_mode: + self._nev = list(dask.compute(*self._nev)) + nev = self._nev + print("N-1 selection stats:") + for i, name in enumerate(self._names): + print( + f"Ignoring {name:<20}: pass = {nev[i+1]:<20}\ + all = {nev[0]:<20}\ + -- eff = {nev[i+1]*100/nev[0]:.1f} %" + ) + + if True: + print( + f"All cuts {'':<20}: pass = {nev[-1]:<20}\ + all = {nev[0]:<20}\ + -- eff = {nev[-1]*100/nev[0]:.1f} %" + ) + + def yieldhist(self): + """Returns the N-1 selection yields as a ``hist.Hist`` object + + Returns + ------- + h : hist.Hist or hist.dask.Hist + Histogram of the number of events surviving the N-1 selection + labels : list of strings + The bin labels of the histogram + """ + labels = ["initial"] + [f"N - {i}" for i in self._names] + ["N"] + if not self._delayed_mode: + h = hist.Hist(hist.axis.Integer(0, len(labels), name="N-1")) + h.fill(numpy.arange(len(labels)), weight=self._nev) + + else: + h = hist.dask.Hist(hist.axis.Integer(0, len(labels), name="N-1")) + for i, weight in enumerate(self._masks, 1): + h.fill(dask_awkward.full_like(weight, i, dtype=int), weight=weight) + h.fill(dask_awkward.zeros_like(weight)) + + return h, labels + + def plot_vars( + self, + vars, + axes=None, + bins=None, + start=None, + stop=None, + edges=None, + transform=None, + ): + """Plot the histograms of variables for each step of the N-1 selection + + Parameters + ---------- + vars : dict + A dictionary in the form ``{name: array}`` where ``name`` is the name of the variable, + and ``array`` is the corresponding array of values. + The arrays must be the same length as each mask of the N-1 selection. + axes : list of hist.axis objects, optional + The axes objects to histogram the variables on. This will override all the following arguments that define axes. + Must be the same length as ``vars``. + bins : iterable of integers or Nones, optional + The number of bins for each variable histogram. If not specified, it defaults to 20. + Must be the same length as ``vars``. + start : iterable of floats or integers or Nones, optional + The lower edge of the first bin for each variable histogram. If not specified, it defaults to the minimum value of the variable array. + Must be the same length as ``vars``. + stop : iterable of floats or integers or Nones, optional + The upper edge of the last bin for each variable histogram. If not specified, it defaults to the maximum value of the variable array. + Must be the same length as ``vars``. + edges : list of iterables of floats or integers, optional + The bin edges for each variable histogram. This overrides ``bins``, ``start``, and ``stop`` if specified. + Must be the same length as ``vars``. + transform : iterable of hist.axis.transform objects or Nones, optional + The transforms to apply to each variable histogram axis. If not specified, it defaults to None. + Must be the same length as ``vars``. + + Returns + ------- + hists : list of hist.Hist or hist.dask.Hist objects + A list of 2D histograms of the variables for each step of the N-1 selection. + The first axis is the variable, the second axis is the N-1 selection step. + labels : list of strings + The bin labels of y axis of the histogram. + """ + if self._delayed_mode: + for name, var in vars.items(): + if not compatible_partitions(var, self._masks[0]): + raise IncompatiblePartitions("plot_vars", var, self._masks[0]) + else: + for name, var in vars.items(): + if len(var) != len(self._masks[0]): + raise ValueError( + f"The variable '{name}' has length '{len(var)}', but the masks have length '{len(self._masks[0])}'" + ) + + hists = [] + labels = ["initial"] + [f"N - {i}" for i in self._names] + ["N"] + + bins = [None] * len(vars) if bins is None else bins + start = [None] * len(vars) if start is None else start + stop = [None] * len(vars) if stop is None else stop + edges = [None] * len(vars) if edges is None else edges + transform = [None] * len(vars) if transform is None else transform + + if axes is not None: + axes = axes + else: + axes = [] + for (name, var), b, s1, s2, e, t in zip( + vars.items(), bins, start, stop, edges, transform + ): + ax = coffea.util._gethistogramaxis( + name, var, b, s1, s2, e, t, self._delayed_mode + ) + axes.append(ax) + + checklengths = [ + len(x) == len(vars) for x in (axes, bins, start, stop, edges, transform) + ] + if not all(checklengths): + raise ValueError( + "vars, axes, bins, start, stop, edges, and transform must be the same length" + ) + + if not self._delayed_mode: + for (name, var), axis in zip(vars.items(), axes): + h = hist.Hist( + axis, + hist.axis.Integer(0, len(labels), name="N-1"), + ) + arr = awkward.flatten(var) + h.fill(arr, awkward.zeros_like(arr)) + for i, mask in enumerate(self.result().masks, 1): + arr = awkward.flatten(var[mask]) + h.fill(arr, awkward.full_like(arr, i, dtype=int)) + hists.append(h) + + else: + for (name, var), axis in zip(vars.items(), axes): + h = hist.dask.Hist( + axis, + hist.axis.Integer(0, len(labels), name="N-1"), + ) + arr = dask_awkward.flatten(var) + h.fill(arr, dask_awkward.zeros_like(arr)) + for i, mask in enumerate(self.result().masks, 1): + arr = dask_awkward.flatten(var[mask]) + h.fill(arr, dask_awkward.full_like(arr, i, dtype=int)) + hists.append(h) + + return hists, labels + + +class Cutflow: + """Object to be returned by PackedSelection.cutflow()""" + + def __init__( + self, names, nevonecut, nevcutflow, masksonecut, maskscutflow, delayed_mode + ): + self._names = names + self._nevonecut = nevonecut + self._nevcutflow = nevcutflow + self._masksonecut = masksonecut + self._maskscutflow = maskscutflow + self._delayed_mode = delayed_mode + + def __repr__(self): + return f"Cutflow(selections={self._names})" + + def result(self): + """Returns the results of the cutflow as a namedtuple + + Returns + ------- + result : CutflowResult + A namedtuple with the following attributes: + + nevonecut : list of integers or dask_awkward.lib.core.Scalar objects + The number of events that survive each cut alone as a list of integers or delayed integers + nevcutflow : list of integers or dask_awkward.lib.core.Scalar objects + The number of events that survive the cumulative cutflow as a list of integers or delayed integers + masksonecut : list of boolean numpy.ndarray or dask_awkward.lib.core.Array objects + The boolean mask vectors of which events pass each cut alone as a list of materialized or delayed boolean arrays + maskscutflow : list of boolean numpy.ndarray or dask_awkward.lib.core.Array objects + The boolean mask vectors of which events pass the cumulative cutflow a list of materialized or delayed boolean arrays + """ + CutflowResult = namedtuple( + "CutflowResult", + ["labels", "nevonecut", "nevcutflow", "masksonecut", "maskscutflow"], + ) + labels = ["initial"] + list(self._names) + return CutflowResult( + labels, + self._nevonecut, + self._nevcutflow, + self._masksonecut, + self._maskscutflow, + ) + + def to_npz(self, file, compressed=False, compute=True): + """Saves the results of the cutflow to a .npz file + + Parameters + ---------- + file : str or file + Either the filename (string) or an open file (file-like object) + where the data will be saved. If file is a string or a Path, the + ``.npz`` extension will be appended to the filename if it is not + already there. + compressed : bool, optional + If True, the data will be compressed in the ``.npz`` file. + Default is False. + compute : bool, optional + Whether to immediately start writing or to return an object + that the user can choose when to start writing by calling compute(). + Default is True. + + Returns + ------- + out : CutflowToNpz or None + If ``compute=True``, returns None. Otherwise, returns an object + that can be used to start writing the data by calling compute(). + """ + labels, nevonecut, nevcutflow, masksonecut, maskscutflow = self.result() + + if compressed: + saver = numpy.savez_compressed + else: + saver = numpy.savez + + out = CutflowToNpz( + file, labels, nevonecut, nevcutflow, masksonecut, maskscutflow, saver + ) + if compute: + out.compute() + return None + else: + return out + + def print(self): + """Prints the statistics of the Cutflow""" + + if self._delayed_mode: + self._nevonecut = list(dask.compute(*self._nevonecut)) + self._nevcutflow = list(dask.compute(*self._nevcutflow)) + nevonecut = self._nevonecut + nevcutflow = self._nevcutflow + print("Cutflow stats:") + for i, name in enumerate(self._names): + print( + f"Cut {name:<20}: pass = {nevonecut[i+1]:<20}\ + cumulative pass = {nevcutflow[i+1]:<20}\ + all = {nevonecut[0]:<20}\ + -- eff = {nevonecut[i+1]*100/nevonecut[0]:.1f} %\ + -- cumulative eff = {nevcutflow[i+1]*100/nevcutflow[0]:.1f} %" + ) + + def yieldhist(self): + """Returns the cutflow yields as ``hist.Hist`` objects + + Returns + ------- + honecut : hist.Hist or hist.dask.Hist + Histogram of the number of events surviving each cut alone + hcutflow : hist.Hist or hist.dask.Hist + Histogram of the number of events surviving the cumulative cutflow + labels : list of strings + The bin labels of the histograms + """ + labels = ["initial"] + list(self._names) + + if not self._delayed_mode: + honecut = hist.Hist(hist.axis.Integer(0, len(labels), name="onecut")) + hcutflow = honecut.copy() + hcutflow.axes.name = ("cutflow",) + honecut.fill(numpy.arange(len(labels)), weight=self._nevonecut) + hcutflow.fill(numpy.arange(len(labels)), weight=self._nevcutflow) + + else: + honecut = hist.dask.Hist(hist.axis.Integer(0, len(labels), name="onecut")) + hcutflow = honecut.copy() + hcutflow.axes.name = ("cutflow",) + + for i, weight in enumerate(self._masksonecut, 1): + honecut.fill( + dask_awkward.full_like(weight, i, dtype=int), weight=weight + ) + honecut.fill(dask_awkward.zeros_like(weight)) + for i, weight in enumerate(self._maskscutflow, 1): + hcutflow.fill( + dask_awkward.full_like(weight, i, dtype=int), weight=weight + ) + hcutflow.fill(dask_awkward.zeros_like(weight)) + + return honecut, hcutflow, labels + + def plot_vars( + self, + vars, + axes=None, + bins=None, + start=None, + stop=None, + edges=None, + transform=None, + ): + """Plot the histograms of variables for each step of the N-1 selection + + Parameters + ---------- + vars : dict + A dictionary in the form ``{name: array}`` where ``name`` is the name of the variable, + and ``array`` is the corresponding array of values. + The arrays must be the same length as each mask of the cutflow. + axes : list of hist.axis objects, optional + The axes objects to histogram the variables on. This will override all the following arguments that define axes. + Must be the same length as ``vars``. + bins : iterable of integers or Nones, optional + The number of bins for each variable histogram. If not specified, it defaults to 20. + Must be the same length as ``vars``. + start : iterable of floats or integers or Nones, optional + The lower edge of the first bin for each variable histogram. If not specified, it defaults to the minimum value of the variable array. + Must be the same length as ``vars``. + stop : iterable of floats or integers or Nones, optional + The upper edge of the last bin for each variable histogram. If not specified, it defaults to the maximum value of the variable array. + Must be the same length as ``vars``. + edges : list of iterables of floats or integers, optional + The bin edges for each variable histogram. This overrides ``bins``, ``start``, and ``stop`` if specified. + Must be the same length as ``vars``. + transform : iterable of hist.axis.transform objects or Nones, optional + The transforms to apply to each variable histogram axis. If not specified, it defaults to None. + Must be the same length as ``vars``. + + Returns + ------- + histsonecut : list of hist.Hist or hist.dask.Hist objects + A list of 1D histograms of the variables of events surviving each cut alone. + The first axis is the variable, the second axis is the cuts. + histscutflow : list of hist.Hist or hist.dask.Hist objects + A list of 1D histograms of the variables of events surviving the cumulative cutflow. + The first axis is the variable, the second axis is the cuts. + labels : list of strings + The bin labels of the y axis of the histograms. + """ + if self._delayed_mode: + for name, var in vars.items(): + if not compatible_partitions(var, self._masksonecut[0]): + raise IncompatiblePartitions("plot_vars", var, self._masksonecut[0]) + else: + for name, var in vars.items(): + if len(var) != len(self._masksonecut[0]): + raise ValueError( + f"The variable '{name}' has length '{len(var)}', but the masks have length '{len(self._masksonecut[0])}'" + ) + + histsonecut, histscutflow = [], [] + labels = ["initial"] + list(self._names) + + bins = [None] * len(vars) if bins is None else bins + start = [None] * len(vars) if start is None else start + stop = [None] * len(vars) if stop is None else stop + edges = [None] * len(vars) if edges is None else edges + transform = [None] * len(vars) if transform is None else transform + + if axes is not None: + axes = axes + else: + axes = [] + for (name, var), b, s1, s2, e, t in zip( + vars.items(), bins, start, stop, edges, transform + ): + ax = coffea.util._gethistogramaxis( + name, var, b, s1, s2, e, t, self._delayed_mode + ) + axes.append(ax) + + checklengths = [ + len(x) == len(vars) for x in (axes, bins, start, stop, edges, transform) + ] + if not all(checklengths): + raise ValueError( + "vars, axes, bins, start, stop, edges, and transform must be the same length" + ) + + if not self._delayed_mode: + for (name, var), axis in zip(vars.items(), axes): + honecut = hist.Hist( + axis, + hist.axis.Integer(0, len(labels), name="onecut"), + ) + hcutflow = honecut.copy() + hcutflow.axes.name = name, "cutflow" + + arr = awkward.flatten(var) + honecut.fill(arr, awkward.zeros_like(arr)) + hcutflow.fill(arr, awkward.zeros_like(arr)) + + for i, mask in enumerate(self.result().masksonecut, 1): + arr = awkward.flatten(var[mask]) + honecut.fill(arr, awkward.full_like(arr, i, dtype=int)) + histsonecut.append(honecut) + + for i, mask in enumerate(self.result().maskscutflow, 1): + arr = awkward.flatten(var[mask]) + hcutflow.fill(arr, awkward.full_like(arr, i, dtype=int)) + histscutflow.append(hcutflow) + + else: + for (name, var), axis in zip(vars.items(), axes): + honecut = hist.dask.Hist( + axis, + hist.axis.Integer(0, len(labels), name="onecut"), + ) + hcutflow = honecut.copy() + hcutflow.axes.name = name, "cutflow" + + arr = dask_awkward.flatten(var) + honecut.fill(arr, dask_awkward.zeros_like(arr)) + hcutflow.fill(arr, dask_awkward.zeros_like(arr)) + + for i, mask in enumerate(self.result().masksonecut, 1): + arr = dask_awkward.flatten(var[mask]) + honecut.fill(arr, dask_awkward.full_like(arr, i, dtype=int)) + histsonecut.append(honecut) + + for i, mask in enumerate(self.result().maskscutflow, 1): + arr = dask_awkward.flatten(var[mask]) + hcutflow.fill(arr, dask_awkward.full_like(arr, i, dtype=int)) + histscutflow.append(hcutflow) + + return histsonecut, histscutflow, labels + + class PackedSelection: """Store several boolean arrays in a compact manner @@ -439,11 +1044,27 @@ def __init__(self, dtype="uint32"): self._names = [] self._data = None + def __repr__(self): + delayed_mode = None if self._data is None else self.delayed_mode + return f"PackedSelection(selections={tuple(self._names)}, delayed_mode={delayed_mode}, items={len(self._names)}, maxitems={self.maxitems})" + @property def names(self): """Current list of mask names available""" return self._names + @property + def delayed_mode(self): + if isinstance(self._data, dask_awkward.Array): + return True + elif isinstance(self._data, numpy.ndarray): + return False + else: + warnings.warn( + "PackedSelection hasn't been initialized with a boolean array yet!" + ) + return False + @property def maxitems(self): return PackedSelection._supported_types[self._dtype] @@ -456,18 +1077,16 @@ def __add_delayed(self, name, selection, fill_value): selection = dask_awkward.fill_none(selection, fill_value) sel_type = dask_awkward.type(selection) if sel_type.primitive != "bool": - raise ValueError(f"Expected a boolean array, received {selection.dtype}") + raise ValueError(f"Expected a boolean array, received {sel_type.primitive}") if len(self._names) == 0: self._data = dask_awkward.zeros_like(selection, dtype=self._dtype) - if isinstance(selection, dask_awkward.Array) and not isinstance( - self._data, dask_awkward.Array - ): + if isinstance(selection, dask_awkward.Array) and not self.delayed_mode: raise ValueError( f"New selection '{name}' is not eager while PackedSelection is!" ) elif len(self._names) == self.maxitems: raise RuntimeError( - f"Exhausted all slots in {self}, consider a larger dtype or fewer selections" + f"Exhausted all slots in PackedSelection: {self}, consider a larger dtype or fewer selections" ) elif not dask_awkward.lib.core.compatible_partitions(self._data, selection): raise ValueError( @@ -488,15 +1107,13 @@ def __add_eager(self, name, selection, fill_value): raise ValueError(f"Expected a boolean array, received {selection.dtype}") if len(self._names) == 0: self._data = numpy.zeros(len(selection), dtype=self._dtype) - if isinstance(selection, numpy.ndarray) and not isinstance( - self._data, numpy.ndarray - ): + if isinstance(selection, numpy.ndarray) and self.delayed_mode: raise ValueError( - f"New selection '{name}' is not eager while PackedSelection is!" + f"New selection '{name}' is not delayed while PackedSelection is!" ) elif len(self._names) == self.maxitems: raise RuntimeError( - f"Exhausted all slots in {self}, consider a larger dtype or fewer selections" + f"Exhausted all slots in PackedSelection: {self}, consider a larger dtype or fewer selections" ) elif self._data.shape != selection.shape: raise ValueError( @@ -525,12 +1142,29 @@ def add(self, name, selection, fill_value=False): fill_value : bool, optional All masked entries will be filled as specified (default: ``False``) """ + if isinstance(selection, dask.array.Array): + raise ValueError( + "Dask arrays are not supported, please convert them to dask_awkward.Array by using dask_awkward.from_dask_array()" + ) selection = coffea.util._ensure_flat(selection, allow_missing=True) if isinstance(selection, numpy.ndarray): self.__add_eager(name, selection, fill_value) elif isinstance(selection, dask_awkward.Array): self.__add_delayed(name, selection, fill_value) + def add_multiple(self, selections, fill_value=False): + """Add multiple boolean arrays at once, see ``add`` for details + + Parameters + ---------- + selections : dict + a dictionary of selections, in the form ``{name: selection}`` + fill_value : bool, optional + All masked entries will be filled as specified (default: ``False``) + """ + for name, selection in selections.items(): + self.add(name, selection, fill_value) + def require(self, **names): """Return a mask vector corresponding to specific requirements @@ -557,6 +1191,12 @@ def require(self, **names): returns a boolean array where an entry is True if the corresponding entries ``cut1 == True``, ``cut2 == False``, and ``cut3`` arbitrary. """ + for cut, v in names.items(): + if not isinstance(cut, str) or cut not in self._names: + raise ValueError( + "All arguments must be strings that refer to the names of existing selections" + ) + consider = 0 require = 0 for name, val in names.items(): @@ -564,11 +1204,23 @@ def require(self, **names): idx = self._names.index(name) consider |= 1 << idx require |= int(val) << idx - return (self._data & consider) == require + return (self._data & self._dtype.type(consider)) == require def all(self, *names): - """Shorthand for `require`, where all the values are True""" - return self.require(**{name: True for name in names}) + """Shorthand for `require`, where all the values are True. + If no arguments are given, all the added selections are required to be True. + """ + if names: + return self.require(**{name: True for name in names}) + return self.require(**{name: True for name in self._names}) + + def allfalse(self, *names): + """Shorthand for `require`, where all the values are False. + If no arguments are given, all the added selections are required to be False. + """ + if names: + return self.require(**{name: False for name in names}) + return self.require(**{name: False for name in self._names}) def any(self, *names): """Return a mask vector corresponding to an inclusive OR of requirements @@ -593,8 +1245,108 @@ def any(self, *names): returns a boolean array where an entry is True if the corresponding entries ``cut1 == True`` or ``cut2 == False``, and ``cut3`` arbitrary. """ + for cut in names: + if not isinstance(cut, str) or cut not in self._names: + raise ValueError( + "All arguments must be strings that refer to the names of existing selections" + ) consider = 0 for name in names: idx = self._names.index(name) consider |= 1 << idx - return (self._data & consider) != 0 + return (self._data & self._dtype.type(consider)) != 0 + + def nminusone(self, *names): + """Compute the "N-1" style selection for a set of selections + + The N-1 style selection for a set of selections, returns an object which can return a list of the number of events + that pass all the other selections ignoring one at a time. The first element of the returned list + is the total number of events before any selections are applied. + The last element is the final number of events that pass if all selections are applied. + It also returns a list of boolean mask vectors of which events pass the N-1 selection each time. + Can also return a histogram as a ``hist.Hist`` object where the bin heights are the number of events of the N-1 selection list. + If the PackedSelection is in delayed mode, the elements of those lists will be dask_awkward Arrays that can be computed whenever the user wants. + If the histogram is requested, the delayed arrays of the number of events list will be computed in the process in order to set the bin heights. + + Parameters + ---------- + ``*names`` : args + The named selections to use, need to be a subset of the selections already added + + Returns + ------- + res: coffea.analysis_tools.NminusOne + A wrapper class for the results, see the documentation for that class for more details + """ + for cut in names: + if not isinstance(cut, str) or cut not in self._names: + raise ValueError( + "All arguments must be strings that refer to the names of existing selections" + ) + + masks = [] + for i, cut in enumerate(names): + mask = self.all(*(names[:i] + names[i + 1 :])) + masks.append(mask) + mask = self.all(*names) + masks.append(mask) + + if not self.delayed_mode: + nev = [len(self._data)] + nev.extend(numpy.sum(masks, axis=1)) + + else: + nev = [dask_awkward.count(self._data, axis=0)] + nev.extend([dask_awkward.sum(mask) for mask in masks]) + + return NminusOne(names, nev, masks, self.delayed_mode) + + def cutflow(self, *names): + """Compute the cutflow for a set of selections + + Returns an object which can return a list of the number of events that pass all the previous selections including the current one + after each named selection is applied consecutively. The first element + of the returned list is the total number of events before any selections are applied. + The last element is the final number of events that pass after all the selections are applied. + Can also return a cutflow histogram as a ``hist.Hist`` object where the bin heights are the number of events of the cutflow list. + If the PackedSelection is in delayed mode, the elements of the list will be dask_awkward Arrays that can be computed whenever the user wants. + If the histogram is requested, those delayed arrays will be computed in the process in order to set the bin heights. + + Parameters + ---------- + ``*names`` : args + The named selections to use, need to be a subset of the selections already added + + Returns + ------- + res: coffea.analysis_tools.Cutflow + A wrapper class for the results, see the documentation for that class for more details + """ + for cut in names: + if not isinstance(cut, str) or cut not in self._names: + raise ValueError( + "All arguments must be strings that refer to the names of existing selections" + ) + + masksonecut, maskscutflow = [], [] + for i, cut in enumerate(names): + mask1 = self.any(cut) + mask2 = self.all(*(names[: i + 1])) + masksonecut.append(mask1) + maskscutflow.append(mask2) + + if not self.delayed_mode: + nevonecut = [len(self._data)] + nevcutflow = [len(self._data)] + nevonecut.extend(numpy.sum(masksonecut, axis=1)) + nevcutflow.extend(numpy.sum(maskscutflow, axis=1)) + + else: + nevonecut = [dask_awkward.count(self._data, axis=0)] + nevcutflow = [dask_awkward.count(self._data, axis=0)] + nevonecut.extend([dask_awkward.sum(mask1) for mask1 in masksonecut]) + nevcutflow.extend([dask_awkward.sum(mask2) for mask2 in maskscutflow]) + + return Cutflow( + names, nevonecut, nevcutflow, masksonecut, maskscutflow, self.delayed_mode + ) diff --git a/src/coffea/util.py b/src/coffea/util.py index 0c68e5d2b..dc591da26 100644 --- a/src/coffea/util.py +++ b/src/coffea/util.py @@ -6,6 +6,7 @@ import awkward import dask_awkward +import hist import numba import numpy from rich.progress import ( @@ -98,6 +99,25 @@ def _ensure_flat(array, allow_missing=False): return array +def _gethistogramaxis(name, var, bins, start, stop, edges, transform, delayed_mode): + "Get a hist axis for plot_vars in PackedSelection" + + if edges is not None: + return hist.axis.Variable(edges=edges, name=name) + + if not delayed_mode: + start = ak.min(var) - 1e-6 if start is None else start + stop = ak.max(var) + 1e-6 if stop is None else stop + elif delayed_mode: + start = dak.min(var).compute() - 1e-6 if start is None else start + stop = dak.max(var).compute() + 1e-6 if stop is None else stop + bins = 20 if bins is None else bins + + return hist.axis.Regular( + bins=bins, start=start, stop=stop, name=name, transform=transform + ) + + def _exception_chain(exc: BaseException) -> List[BaseException]: """Retrieves the entire exception chain as a list.""" ret = [] diff --git a/tests/test_analysis_tools.py b/tests/test_analysis_tools.py index 65d5c02cd..1e8c46ec1 100644 --- a/tests/test_analysis_tools.py +++ b/tests/test_analysis_tools.py @@ -1,7 +1,26 @@ +import os + import numpy as np import pytest +import uproot from dummy_distributions import dummy_jagged_eta_pt +from coffea.nanoevents import NanoAODSchema, NanoEventsFactory + +fname = "tests/samples/nano_dy.root" +eagerevents = NanoEventsFactory.from_root( + {os.path.abspath(fname): "Events"}, + schemaclass=NanoAODSchema.v6, + metadata={"dataset": "DYJets"}, +).events() +dakevents = NanoEventsFactory.from_root( + {os.path.abspath(fname): "Events"}, + schemaclass=NanoAODSchema, + metadata={"dataset": "DYJets"}, + permit_dask=True, +).events() +uprootevents = uproot.dask({fname: "Events"}) + def test_weights(): from coffea.analysis_tools import Weights @@ -58,58 +77,61 @@ def test_weights(): raise weight.partial_weight(exclude="test") -def test_weights_dak(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_weights_dak(optimization_enabled): + import dask import dask.array as da import dask_awkward as dak from coffea.analysis_tools import Weights - counts, test_eta, test_pt = dummy_jagged_eta_pt() - scale_central = dak.from_dask_array( - da.random.normal(loc=1.0, scale=0.01, size=counts.size) - ) - scale_up = scale_central * 1.10 - scale_down = scale_central * 0.95 - scale_up_shift = 0.10 * scale_central - scale_down_shift = 0.05 * scale_central + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() + scale_central = dak.from_dask_array( + da.random.normal(loc=1.0, scale=0.01, size=counts.size) + ) + scale_up = scale_central * 1.10 + scale_down = scale_central * 0.95 + scale_up_shift = 0.10 * scale_central + scale_down_shift = 0.05 * scale_central + + weight = Weights(None) + weight.add("test", scale_central, weightUp=scale_up, weightDown=scale_down) + weight.add( + "testShift", + scale_central, + weightUp=scale_up_shift, + weightDown=scale_down_shift, + shift=True, + ) - weight = Weights(None) - weight.add("test", scale_central, weightUp=scale_up, weightDown=scale_down) - weight.add( - "testShift", - scale_central, - weightUp=scale_up_shift, - weightDown=scale_down_shift, - shift=True, - ) + var_names = weight.variations + expected_names = ["testShiftUp", "testShiftDown", "testUp", "testDown"] + for name in expected_names: + assert name in var_names - var_names = weight.variations - expected_names = ["testShiftUp", "testShiftDown", "testUp", "testDown"] - for name in expected_names: - assert name in var_names + test_central = weight.weight() + exp_weight = scale_central * scale_central - test_central = weight.weight() - exp_weight = scale_central * scale_central + assert np.all(np.abs(test_central - (exp_weight)).compute() < 1e-6) - assert np.all(np.abs(test_central - (exp_weight)).compute() < 1e-6) + test_up = weight.weight("testUp") + exp_up = scale_central * scale_central * 1.10 - test_up = weight.weight("testUp") - exp_up = scale_central * scale_central * 1.10 + assert np.all(np.abs(test_up - (exp_up)).compute() < 1e-6) - assert np.all(np.abs(test_up - (exp_up)).compute() < 1e-6) + test_down = weight.weight("testDown") + exp_down = scale_central * scale_central * 0.95 - test_down = weight.weight("testDown") - exp_down = scale_central * scale_central * 0.95 + assert np.all(np.abs(test_down - (exp_down)).compute() < 1e-6) - assert np.all(np.abs(test_down - (exp_down)).compute() < 1e-6) + test_shift_up = weight.weight("testUp") - test_shift_up = weight.weight("testUp") + assert np.all(np.abs(test_shift_up - (exp_up)).compute() < 1e-6) - assert np.all(np.abs(test_shift_up - (exp_up)).compute() < 1e-6) + test_shift_down = weight.weight("testDown") - test_shift_down = weight.weight("testDown") - - assert np.all(np.abs(test_shift_down - (exp_down)).compute() < 1e-6) + assert np.all(np.abs(test_shift_down - (exp_down)).compute() < 1e-6) def test_weights_multivariation(): @@ -162,59 +184,62 @@ def test_weights_multivariation(): assert np.all(np.abs(test_down_2 - (exp_down)) < 1e-6) -def test_weights_multivariation_dak(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_weights_multivariation_dak(optimization_enabled): + import dask import dask.array as da import dask_awkward as dak from coffea.analysis_tools import Weights - counts, test_eta, test_pt = dummy_jagged_eta_pt() - scale_central = dak.from_dask_array( - da.random.normal(loc=1.0, scale=0.01, size=counts.size) - ) - scale_up = scale_central * 1.10 - scale_down = scale_central * 0.95 - scale_up_2 = scale_central * 1.2 - scale_down_2 = scale_central * 0.90 + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() + scale_central = dak.from_dask_array( + da.random.normal(loc=1.0, scale=0.01, size=counts.size) + ) + scale_up = scale_central * 1.10 + scale_down = scale_central * 0.95 + scale_up_2 = scale_central * 1.2 + scale_down_2 = scale_central * 0.90 + + weight = Weights(None) + weight.add_multivariation( + "test", + scale_central, + modifierNames=["A", "B"], + weightsUp=[scale_up, scale_up_2], + weightsDown=[scale_down, scale_down_2], + ) - weight = Weights(None) - weight.add_multivariation( - "test", - scale_central, - modifierNames=["A", "B"], - weightsUp=[scale_up, scale_up_2], - weightsDown=[scale_down, scale_down_2], - ) + var_names = weight.variations + expected_names = ["test_AUp", "test_ADown", "test_BUp", "test_BDown"] + for name in expected_names: + assert name in var_names - var_names = weight.variations - expected_names = ["test_AUp", "test_ADown", "test_BUp", "test_BDown"] - for name in expected_names: - assert name in var_names + test_central = weight.weight() + exp_weight = scale_central - test_central = weight.weight() - exp_weight = scale_central + assert np.all(np.abs(test_central - (exp_weight)).compute() < 1e-6) - assert np.all(np.abs(test_central - (exp_weight)).compute() < 1e-6) + test_up = weight.weight("test_AUp") + exp_up = scale_central * 1.10 - test_up = weight.weight("test_AUp") - exp_up = scale_central * 1.10 + assert np.all(np.abs(test_up - (exp_up)).compute() < 1e-6) - assert np.all(np.abs(test_up - (exp_up)).compute() < 1e-6) + test_down = weight.weight("test_ADown") + exp_down = scale_central * 0.95 - test_down = weight.weight("test_ADown") - exp_down = scale_central * 0.95 + assert np.all(np.abs(test_down - (exp_down)).compute() < 1e-6) - assert np.all(np.abs(test_down - (exp_down)).compute() < 1e-6) + test_up_2 = weight.weight("test_BUp") + exp_up = scale_central * 1.2 - test_up_2 = weight.weight("test_BUp") - exp_up = scale_central * 1.2 + assert np.all(np.abs(test_up_2 - (exp_up)).compute() < 1e-6) - assert np.all(np.abs(test_up_2 - (exp_up)).compute() < 1e-6) + test_down_2 = weight.weight("test_BDown") + exp_down = scale_central * 0.90 - test_down_2 = weight.weight("test_BDown") - exp_down = scale_central * 0.90 - - assert np.all(np.abs(test_down_2 - (exp_down)).compute() < 1e-6) + assert np.all(np.abs(test_down_2 - (exp_down)).compute() < 1e-6) def test_weights_partial(): @@ -275,71 +300,83 @@ def test_weights_partial(): assert error_raised -def test_weights_partial_dak(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_weights_partial_dak(optimization_enabled): + import dask import dask.array as da import dask_awkward as dak from coffea.analysis_tools import Weights - counts, _, _ = dummy_jagged_eta_pt() - w1 = dak.from_dask_array(da.random.normal(loc=1.0, scale=0.01, size=counts.size)) - w2 = dak.from_dask_array(da.random.normal(loc=1.3, scale=0.05, size=counts.size)) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, _, _ = dummy_jagged_eta_pt() + w1 = dak.from_dask_array( + da.random.normal(loc=1.0, scale=0.01, size=counts.size) + ) + w2 = dak.from_dask_array( + da.random.normal(loc=1.3, scale=0.05, size=counts.size) + ) - weights = Weights(None, storeIndividual=True) - weights.add("w1", w1) - weights.add("w2", w2) + weights = Weights(None, storeIndividual=True) + weights.add("w1", w1) + weights.add("w2", w2) - test_exclude_none = weights.weight() - assert np.all(np.abs(test_exclude_none - w1 * w2).compute() < 1e-6) + test_exclude_none = weights.weight() + assert np.all(np.abs(test_exclude_none - w1 * w2).compute() < 1e-6) - test_exclude1 = weights.partial_weight(exclude=["w1"]) - assert np.all(np.abs(test_exclude1 - w2).compute() < 1e-6) + test_exclude1 = weights.partial_weight(exclude=["w1"]) + assert np.all(np.abs(test_exclude1 - w2).compute() < 1e-6) - test_include1 = weights.partial_weight(include=["w1"]) - assert np.all(np.abs(test_include1 - w1).compute() < 1e-6) + test_include1 = weights.partial_weight(include=["w1"]) + assert np.all(np.abs(test_include1 - w1).compute() < 1e-6) - test_exclude2 = weights.partial_weight(exclude=["w2"]) - assert np.all(np.abs(test_exclude2 - w1).compute() < 1e-6) + test_exclude2 = weights.partial_weight(exclude=["w2"]) + assert np.all(np.abs(test_exclude2 - w1).compute() < 1e-6) - test_include2 = weights.partial_weight(include=["w2"]) - assert np.all(np.abs(test_include2 - w2).compute() < 1e-6) + test_include2 = weights.partial_weight(include=["w2"]) + assert np.all(np.abs(test_include2 - w2).compute() < 1e-6) - test_include_both = weights.partial_weight(include=["w1", "w2"]) - assert np.all(np.abs(test_include_both - w1 * w2).compute() < 1e-6) + test_include_both = weights.partial_weight(include=["w1", "w2"]) + assert np.all(np.abs(test_include_both - w1 * w2).compute() < 1e-6) - # Check that exception is thrown if arguments are incompatible - error_raised = False - try: - weights.partial_weight(exclude=["w1"], include=["w2"]) - except ValueError: - error_raised = True - assert error_raised + # Check that exception is thrown if arguments are incompatible + error_raised = False + try: + weights.partial_weight(exclude=["w1"], include=["w2"]) + except ValueError: + error_raised = True + assert error_raised - error_raised = False - try: - weights.partial_weight() - except ValueError: - error_raised = True - assert error_raised + error_raised = False + try: + weights.partial_weight() + except ValueError: + error_raised = True + assert error_raised - # Check that exception is thrown if individual weights - # are not saved from the start - weights = Weights(None, storeIndividual=False) - weights.add("w1", w1) - weights.add("w2", w2) + # Check that exception is thrown if individual weights + # are not saved from the start + weights = Weights(None, storeIndividual=False) + weights.add("w1", w1) + weights.add("w2", w2) - error_raised = False - try: - weights.partial_weight(exclude=["test"], include=["test"]) - except ValueError: - error_raised = True - assert error_raised + error_raised = False + try: + weights.partial_weight(exclude=["test"], include=["test"]) + except ValueError: + error_raised = True + assert error_raised -def test_packed_selection(): +@pytest.mark.parametrize("dtype", ["uint16", "uint32", "uint64"]) +def test_packed_selection_basic(dtype): + import awkward as ak + import dask.array as da + import dask_awkward as dak + from coffea.analysis_tools import PackedSelection - sel = PackedSelection() + sel = PackedSelection(dtype=dtype) shape = (10,) all_true = np.full(shape=shape, fill_value=True, dtype=bool) @@ -347,13 +384,39 @@ def test_packed_selection(): fizz = np.arange(shape[0]) % 3 == 0 buzz = np.arange(shape[0]) % 5 == 0 ones = np.ones(shape=shape, dtype=np.uint64) - wrong_shape = ones = np.ones(shape=(shape[0] - 5,), dtype=bool) + wrong_shape = np.ones(shape=(shape[0] - 5,), dtype=bool) + wrong_type = dak.from_awkward(ak.Array(np.arange(shape[0]) % 3 == 0), 1) + daskarray = da.arange(shape[0]) % 3 == 0 + + with pytest.warns( + UserWarning, + match="PackedSelection hasn't been initialized with a boolean array yet!", + ): + assert sel.delayed_mode is False - sel.add("all_true", all_true) - sel.add("all_false", all_false) sel.add("fizz", fizz) sel.add("buzz", buzz) + assert np.all( + sel.all() + == np.array( + [True, False, False, False, False, False, False, False, False, False] + ) + ) + assert np.all( + sel.allfalse() + == np.array([False, True, True, False, True, False, False, True, True, False]) + ) + + sel.add_multiple({"all_true": all_true, "all_false": all_false}) + + assert sel.delayed_mode is False + with pytest.raises( + ValueError, + match="New selection 'wrong_type' is not eager while PackedSelection is!", + ): + sel.add("wrong_type", wrong_type) + assert np.all(sel.require(all_true=True, all_false=False) == all_true) # allow truthy values assert np.all(sel.require(all_true=1, all_false=0) == all_true) @@ -365,75 +428,873 @@ def test_packed_selection(): [True, False, False, False, False, False, False, False, False, False] ) ) + assert np.all( + sel.allfalse("fizz", "buzz") + == np.array([False, True, True, False, True, False, False, True, True, False]) + ) assert np.all( sel.any("fizz", "buzz") == np.array([True, False, False, True, False, True, True, False, False, True]) ) - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=r"New selection 'wrong_shape' has a different shape than existing selections \(\(5,\) vs. \(10,\)\)", + ): sel.add("wrong_shape", wrong_shape) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Expected a boolean array, received uint64"): sel.add("ones", ones) with pytest.raises(RuntimeError): - overpack = PackedSelection() + overpack = PackedSelection(dtype=dtype) for i in range(65): overpack.add("sel_%d", all_true) + with pytest.raises( + ValueError, + match="Dask arrays are not supported, please convert them to dask_awkward.Array by using dask_awkward.from_dask_array()", + ): + sel.add("dask_array", daskarray) + -def test_packed_selection_dak(): +def test_packed_selection_nminusone(): import awkward as ak - import dask_awkward as dak from coffea.analysis_tools import PackedSelection - sel = PackedSelection() + events = eagerevents - shape = (10,) - all_true = dak.from_awkward( - ak.Array(np.full(shape=shape, fill_value=True, dtype=bool)), 1 - ) - all_false = dak.from_awkward( - ak.Array(np.full(shape=shape, fill_value=False, dtype=bool)), 1 + selection = PackedSelection() + + twoelectron = ak.num(events.Electron) == 2 + nomuon = ak.num(events.Muon) == 0 + leadpt20 = ak.any(events.Electron.pt >= 20.0, axis=1) | ak.any( + events.Muon.pt >= 20.0, axis=1 ) - fizz = dak.from_awkward(ak.Array(np.arange(shape[0]) % 3 == 0), 1) - buzz = dak.from_awkward(ak.Array(np.arange(shape[0]) % 5 == 0), 1) - ones = dak.from_awkward(ak.Array(np.ones(shape=shape, dtype=np.uint64)), 1) - wrong_shape = ones = dak.from_awkward( - ak.Array(np.ones(shape=(shape[0] - 5,), dtype=bool)), 1 + + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } ) - sel.add("all_true", all_true) - sel.add("all_false", all_false) - sel.add("fizz", fizz) - sel.add("buzz", buzz) + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] - assert np.all( - sel.require(all_true=True, all_false=False).compute() == all_true.compute() - ) - # allow truthy values - assert np.all(sel.require(all_true=1, all_false=0).compute() == all_true.compute()) - assert np.all(sel.all("all_true", "all_false").compute() == all_false.compute()) - assert np.all(sel.any("all_true", "all_false").compute() == all_true.compute()) - assert np.all( - sel.all("fizz", "buzz").compute() - == np.array( - [True, False, False, False, False, False, False, False, False, False] + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.nminusone("twoElectron", "nonexistent") + nminusone = selection.nminusone("twoElectron", "noMuon", "leadPt20") + + labels, nev, masks = nminusone.result() + + assert labels == ["initial", "N - twoElectron", "N - noMuon", "N - leadPt20", "N"] + + assert nev == [ + len(events), + len(events[nomuon & leadpt20]), + len(events[twoelectron & leadpt20]), + len(events[twoelectron & nomuon]), + len(events[twoelectron & nomuon & leadpt20]), + ] + + for mask, truth in zip( + masks, + [ + nomuon & leadpt20, + twoelectron & leadpt20, + twoelectron & nomuon, + twoelectron & nomuon & leadpt20, + ], + ): + assert np.all(mask == truth) + + nminusone.to_npz("nminusone.npz", compressed=False) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == nev) + assert np.all(file["masks"] == masks) + os.remove("nminusone.npz") + + nminusone.to_npz("nminusone.npz", compressed=True) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == nev) + assert np.all(file["masks"] == masks) + os.remove("nminusone.npz") + + h, hlabels = nminusone.yieldhist() + + assert hlabels == ["initial", "N - twoElectron", "N - noMuon", "N - leadPt20", "N"] + + assert np.all(h.axes["N-1"].edges == np.arange(0, 6)) + + assert np.all(h.counts() == nev) + + with pytest.raises(ValueError): + nminusone.plot_vars( + {"Ept": events.Electron.pt, "Ephi": events.Electron.phi[:20]} ) + hs, hslabels = nminusone.plot_vars( + {"Ept": events.Electron.pt, "Ephi": events.Electron.phi} ) - assert np.all( - sel.any("fizz", "buzz").compute() - == np.array([True, False, False, True, False, True, True, False, False, True]) + + assert hslabels == ["initial", "N - twoElectron", "N - noMuon", "N - leadPt20", "N"] + + for h, array in zip(hs, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon & leadpt20, + twoelectron & leadpt20, + twoelectron & nomuon, + twoelectron & nomuon & leadpt20, + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(ak.flatten(array[truth]), bins=edges) + assert np.all(counts == c) + + +def test_packed_selection_cutflow(): + import awkward as ak + + from coffea.analysis_tools import PackedSelection + + events = eagerevents + + selection = PackedSelection() + + twoelectron = ak.num(events.Electron) == 2 + nomuon = ak.num(events.Muon) == 0 + leadpt20 = ak.any(events.Electron.pt >= 20.0, axis=1) | ak.any( + events.Muon.pt >= 20.0, axis=1 ) - with pytest.raises(ValueError): - sel.add("wrong_shape", wrong_shape) + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } + ) + + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] + + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.cutflow("twoElectron", "nonexistent") + cutflow = selection.cutflow("noMuon", "twoElectron", "leadPt20") + + labels, nevonecut, nevcutflow, masksonecut, maskscutflow = cutflow.result() + + assert labels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert nevonecut == [ + len(events), + len(events[nomuon]), + len(events[twoelectron]), + len(events[leadpt20]), + ] + + assert nevcutflow == [ + len(events), + len(events[nomuon]), + len(events[nomuon & twoelectron]), + len(events[nomuon & twoelectron & leadpt20]), + ] + + for mask, truth in zip(masksonecut, [nomuon, twoelectron, leadpt20]): + assert np.all(mask == truth) + + for mask, truth in zip( + maskscutflow, [nomuon, nomuon & twoelectron, nomuon & twoelectron & leadpt20] + ): + assert np.all(mask == truth) + + cutflow.to_npz("cutflow.npz", compressed=False) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == nevonecut) + assert np.all(file["nevcutflow"] == nevcutflow) + assert np.all(file["masksonecut"] == masksonecut) + assert np.all(file["maskscutflow"] == maskscutflow) + os.remove("cutflow.npz") + + cutflow.to_npz("cutflow.npz", compressed=True) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == nevonecut) + assert np.all(file["nevcutflow"] == nevcutflow) + assert np.all(file["masksonecut"] == masksonecut) + assert np.all(file["maskscutflow"] == maskscutflow) + os.remove("cutflow.npz") + + honecut, hcutflow, hlabels = cutflow.yieldhist() + + assert hlabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert np.all(honecut.axes["onecut"].edges == np.arange(0, 5)) + assert np.all(hcutflow.axes["cutflow"].edges == np.arange(0, 5)) + + assert np.all(honecut.counts() == nevonecut) + assert np.all(hcutflow.counts() == nevcutflow) with pytest.raises(ValueError): - sel.add("ones", ones) + cutflow.plot_vars({"Ept": events.Electron.pt, "Ephi": events.Electron.phi[:20]}) + honecuts, hcutflows, hslabels = cutflow.plot_vars( + {"ept": events.Electron.pt, "ephi": events.Electron.phi} + ) - with pytest.raises(RuntimeError): - overpack = PackedSelection() - for i in range(65): - overpack.add("sel_%d", all_true) + assert hslabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + for h, array in zip(honecuts, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [np.ones(40, dtype=bool), nomuon, twoelectron, leadpt20] + ): + counts = h[:, i].counts() + c, e = np.histogram(ak.flatten(array[truth]), bins=edges) + assert np.all(counts == c) + + for h, array in zip(hcutflows, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon, + nomuon & twoelectron, + nomuon & twoelectron & leadpt20, + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(ak.flatten(array[truth]), bins=edges) + assert np.all(counts == c) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +@pytest.mark.parametrize("dtype", ["uint16", "uint32", "uint64"]) +def test_packed_selection_basic_dak(optimization_enabled, dtype): + import awkward as ak + import dask + import dask.array as da + import dask_awkward as dak + + from coffea.analysis_tools import PackedSelection + + sel = PackedSelection(dtype=dtype) + + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + shape = (10,) + all_true = dak.from_awkward( + ak.Array(np.full(shape=shape, fill_value=True, dtype=bool)), 1 + ) + all_false = dak.from_awkward( + ak.Array(np.full(shape=shape, fill_value=False, dtype=bool)), 1 + ) + fizz = dak.from_awkward(ak.Array(np.arange(shape[0]) % 3 == 0), 1) + buzz = dak.from_awkward(ak.Array(np.arange(shape[0]) % 5 == 0), 1) + ones = dak.from_awkward(ak.Array(np.ones(shape=shape, dtype=np.uint64)), 1) + wrong_shape = dak.from_awkward( + ak.Array(np.ones(shape=(shape[0] - 5,), dtype=bool)), 1 + ) + wrong_type = np.arange(shape[0]) % 3 == 0 + daskarray = da.arange(shape[0]) % 3 == 0 + + with pytest.warns( + UserWarning, + match="PackedSelection hasn't been initialized with a boolean array yet!", + ): + assert sel.delayed_mode is False + + sel.add("fizz", fizz) + sel.add("buzz", buzz) + + assert np.all( + sel.all().compute() + == np.array( + [True, False, False, False, False, False, False, False, False, False] + ) + ) + assert np.all( + sel.allfalse().compute() + == np.array( + [False, True, True, False, True, False, False, True, True, False] + ) + ) + + sel.add_multiple({"all_true": all_true, "all_false": all_false}) + + assert sel.delayed_mode is True + with pytest.raises( + ValueError, + match="New selection 'wrong_type' is not delayed while PackedSelection is!", + ): + sel.add("wrong_type", wrong_type) + + assert np.all( + sel.require(all_true=True, all_false=False).compute() == all_true.compute() + ) + # allow truthy values + assert np.all( + sel.require(all_true=1, all_false=0).compute() == all_true.compute() + ) + assert np.all(sel.all("all_true", "all_false").compute() == all_false.compute()) + assert np.all(sel.any("all_true", "all_false").compute() == all_true.compute()) + assert np.all( + sel.all("fizz", "buzz").compute() + == np.array( + [True, False, False, False, False, False, False, False, False, False] + ) + ) + assert np.all( + sel.allfalse("fizz", "buzz").compute() + == np.array( + [False, True, True, False, True, False, False, True, True, False] + ) + ) + assert np.all( + sel.any("fizz", "buzz").compute() + == np.array( + [True, False, False, True, False, True, True, False, False, True] + ) + ) + + with pytest.raises( + ValueError, + match="New selection 'wrong_shape' has a different partition structure than existing selections", + ): + sel.add("wrong_shape", wrong_shape) + + with pytest.raises( + ValueError, match="Expected a boolean array, received uint64" + ): + sel.add("ones", ones) + + with pytest.raises(RuntimeError): + overpack = PackedSelection(dtype=dtype) + for i in range(65): + overpack.add("sel_%d", all_true) + + with pytest.raises( + ValueError, + match="Dask arrays are not supported, please convert them to dask_awkward.Array by using dask_awkward.from_dask_array()", + ): + sel.add("dask_array", daskarray) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_packed_selection_nminusone_dak(optimization_enabled): + import dask + import dask_awkward as dak + + from coffea.analysis_tools import PackedSelection + + events = dakevents + + selection = PackedSelection() + + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + twoelectron = dak.num(events.Electron) == 2 + nomuon = dak.num(events.Muon) == 0 + leadpt20 = dak.any(events.Electron.pt >= 20.0, axis=1) | dak.any( + events.Muon.pt >= 20.0, axis=1 + ) + + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } + ) + + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] + + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.nminusone("twoElectron", "nonexistent") + nminusone = selection.nminusone("twoElectron", "noMuon", "leadPt20") + + labels, nev, masks = nminusone.result() + + assert labels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + assert list(dask.compute(*nev)) == [ + len(events), + len(events[nomuon & leadpt20]), + len(events[twoelectron & leadpt20]), + len(events[twoelectron & nomuon]), + len(events[twoelectron & nomuon & leadpt20]), + ] + + for mask, truth in zip( + masks, + [ + nomuon & leadpt20, + twoelectron & leadpt20, + twoelectron & nomuon, + twoelectron & nomuon & leadpt20, + ], + ): + assert np.all(mask.compute() == truth.compute()) + + nminusone.to_npz("nminusone.npz", compressed=False) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == list(dask.compute(*nev))) + assert np.all(file["masks"] == list(dask.compute(*masks))) + os.remove("nminusone.npz") + + nminusone.to_npz("nminusone.npz", compressed=True) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == list(dask.compute(*nev))) + assert np.all(file["masks"] == list(dask.compute(*masks))) + os.remove("nminusone.npz") + + h, hlabels = dask.compute(*nminusone.yieldhist()) + + assert hlabels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + assert np.all(h.axes["N-1"].edges == np.arange(0, 6)) + + assert np.all(h.counts() == list(dask.compute(*nev))) + + # with pytest.raises(IncompatiblePartitions): + # nminusone.plot_vars( + # {"Ept": events.Electron.pt, "Ephi": events[:20].Electron.phi} + # ) + hs, hslabels = dask.compute( + *nminusone.plot_vars( + {"Ept": events.Electron.pt, "Ephi": events.Electron.phi} + ) + ) + + assert hslabels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + for h, array in zip(hs, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute() & leadpt20.compute(), + twoelectron.compute() & leadpt20.compute(), + twoelectron.compute() & nomuon.compute(), + twoelectron.compute() & nomuon.compute() & leadpt20.compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_packed_selection_cutflow_dak(optimization_enabled): + import dask + import dask_awkward as dak + + from coffea.analysis_tools import PackedSelection + + events = dakevents + + selection = PackedSelection() + + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + twoelectron = dak.num(events.Electron) == 2 + nomuon = dak.num(events.Muon) == 0 + leadpt20 = dak.any(events.Electron.pt >= 20.0, axis=1) | dak.any( + events.Muon.pt >= 20.0, axis=1 + ) + + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } + ) + + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] + + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.cutflow("twoElectron", "nonexistent") + cutflow = selection.cutflow("noMuon", "twoElectron", "leadPt20") + + labels, nevonecut, nevcutflow, masksonecut, maskscutflow = cutflow.result() + + assert labels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert list(dask.compute(*nevonecut)) == [ + len(events), + len(events[nomuon]), + len(events[twoelectron]), + len(events[leadpt20]), + ] + + assert list(dask.compute(*nevcutflow)) == [ + len(events), + len(events[nomuon]), + len(events[nomuon & twoelectron]), + len(events[nomuon & twoelectron & leadpt20]), + ] + + for mask, truth in zip(masksonecut, [nomuon, twoelectron, leadpt20]): + assert np.all(mask.compute() == truth.compute()) + + for mask, truth in zip( + maskscutflow, + [nomuon, nomuon & twoelectron, nomuon & twoelectron & leadpt20], + ): + assert np.all(mask.compute() == truth.compute()) + + cutflow.to_npz("cutflow.npz", compressed=False) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == list(dask.compute(*nevonecut))) + assert np.all(file["nevcutflow"] == list(dask.compute(*nevcutflow))) + assert np.all(file["masksonecut"] == list(dask.compute(*masksonecut))) + assert np.all(file["maskscutflow"] == list(dask.compute(*maskscutflow))) + os.remove("cutflow.npz") + + cutflow.to_npz("cutflow.npz", compressed=True) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == list(dask.compute(*nevonecut))) + assert np.all(file["nevcutflow"] == list(dask.compute(*nevcutflow))) + assert np.all(file["masksonecut"] == list(dask.compute(*masksonecut))) + assert np.all(file["maskscutflow"] == list(dask.compute(*maskscutflow))) + os.remove("cutflow.npz") + + honecut, hcutflow, hlabels = dask.compute(*cutflow.yieldhist()) + + assert hlabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert np.all(honecut.axes["onecut"].edges == np.arange(0, 5)) + assert np.all(hcutflow.axes["cutflow"].edges == np.arange(0, 5)) + + assert np.all(honecut.counts() == list(dask.compute(*nevonecut))) + assert np.all(hcutflow.counts() == list(dask.compute(*nevcutflow))) + + # with pytest.raises(IncompatiblePartitions): + # cutflow.plot_vars( + # {"Ept": events.Electron.pt, "Ephi": events[:20].Electron.phi} + # ) + honecuts, hcutflows, hslabels = dask.compute( + *cutflow.plot_vars({"ept": events.Electron.pt, "ephi": events.Electron.phi}) + ) + + assert hslabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + for h, array in zip(honecuts, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute(), + twoelectron.compute(), + leadpt20.compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) + + for h, array in zip(hcutflows, [events.Electron.pt, events.Electron.phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute(), + (nomuon & twoelectron).compute(), + (nomuon & twoelectron & leadpt20).compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_packed_selection_nminusone_dak_uproot_only(optimization_enabled): + import dask + import dask_awkward as dak + + from coffea.analysis_tools import PackedSelection + + events = uprootevents + + selection = PackedSelection() + + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + twoelectron = dak.num(events.Electron_pt) == 2 + nomuon = dak.num(events.Muon_pt) == 0 + leadpt20 = dak.any(events.Electron_pt >= 20.0, axis=1) | dak.any( + events.Muon_pt >= 20.0, axis=1 + ) + + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } + ) + + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] + + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.nminusone("twoElectron", "nonexistent") + nminusone = selection.nminusone("twoElectron", "noMuon", "leadPt20") + + labels, nev, masks = nminusone.result() + + assert labels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + assert list(dask.compute(*nev)) == [ + len(events), + len(events[nomuon & leadpt20]), + len(events[twoelectron & leadpt20]), + len(events[twoelectron & nomuon]), + len(events[twoelectron & nomuon & leadpt20]), + ] + + for mask, truth in zip( + masks, + [ + nomuon & leadpt20, + twoelectron & leadpt20, + twoelectron & nomuon, + twoelectron & nomuon & leadpt20, + ], + ): + assert np.all(mask.compute() == truth.compute()) + + nminusone.to_npz("nminusone.npz", compressed=False) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == list(dask.compute(*nev))) + assert np.all(file["masks"] == list(dask.compute(*masks))) + os.remove("nminusone.npz") + + nminusone.to_npz("nminusone.npz", compressed=True) + with np.load("nminusone.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nev"] == list(dask.compute(*nev))) + assert np.all(file["masks"] == list(dask.compute(*masks))) + os.remove("nminusone.npz") + + h, hlabels = dask.compute(*nminusone.yieldhist()) + + assert hlabels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + assert np.all(h.axes["N-1"].edges == np.arange(0, 6)) + + assert np.all(h.counts() == list(dask.compute(*nev))) + + # with pytest.raises(IncompatiblePartitions): + # nminusone.plot_vars( + # {"Ept": events.Electron_pt, "Ephi": events[:20].Electron_phi} + # ) + hs, hslabels = dask.compute( + *nminusone.plot_vars( + {"Ept": events.Electron_pt, "Ephi": events.Electron_phi} + ) + ) + + assert hslabels == [ + "initial", + "N - twoElectron", + "N - noMuon", + "N - leadPt20", + "N", + ] + + for h, array in zip(hs, [events.Electron_pt, events.Electron_phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute() & leadpt20.compute(), + twoelectron.compute() & leadpt20.compute(), + twoelectron.compute() & nomuon.compute(), + twoelectron.compute() & nomuon.compute() & leadpt20.compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_packed_selection_cutflow_dak_uproot_only(optimization_enabled): + import dask + import dask_awkward as dak + + from coffea.analysis_tools import PackedSelection + + events = uprootevents + + selection = PackedSelection() + + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + twoelectron = dak.num(events.Electron_pt) == 2 + nomuon = dak.num(events.Muon_pt) == 0 + leadpt20 = dak.any(events.Electron_pt >= 20.0, axis=1) | dak.any( + events.Muon_pt >= 20.0, axis=1 + ) + + selection.add_multiple( + { + "twoElectron": twoelectron, + "noMuon": nomuon, + "leadPt20": leadpt20, + } + ) + + assert selection.names == ["twoElectron", "noMuon", "leadPt20"] + + with pytest.raises( + ValueError, + match="All arguments must be strings that refer to the names of existing selections", + ): + selection.cutflow("twoElectron", "nonexistent") + cutflow = selection.cutflow("noMuon", "twoElectron", "leadPt20") + + labels, nevonecut, nevcutflow, masksonecut, maskscutflow = cutflow.result() + + assert labels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert list(dask.compute(*nevonecut)) == [ + len(events), + len(events[nomuon]), + len(events[twoelectron]), + len(events[leadpt20]), + ] + + assert list(dask.compute(*nevcutflow)) == [ + len(events), + len(events[nomuon]), + len(events[nomuon & twoelectron]), + len(events[nomuon & twoelectron & leadpt20]), + ] + + for mask, truth in zip(masksonecut, [nomuon, twoelectron, leadpt20]): + assert np.all(mask.compute() == truth.compute()) + + for mask, truth in zip( + maskscutflow, + [nomuon, nomuon & twoelectron, nomuon & twoelectron & leadpt20], + ): + assert np.all(mask.compute() == truth.compute()) + + cutflow.to_npz("cutflow.npz", compressed=False) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == list(dask.compute(*nevonecut))) + assert np.all(file["nevcutflow"] == list(dask.compute(*nevcutflow))) + assert np.all(file["masksonecut"] == list(dask.compute(*masksonecut))) + assert np.all(file["maskscutflow"] == list(dask.compute(*maskscutflow))) + os.remove("cutflow.npz") + + cutflow.to_npz("cutflow.npz", compressed=True) + with np.load("cutflow.npz") as file: + assert np.all(file["labels"] == labels) + assert np.all(file["nevonecut"] == list(dask.compute(*nevonecut))) + assert np.all(file["nevcutflow"] == list(dask.compute(*nevcutflow))) + assert np.all(file["masksonecut"] == list(dask.compute(*masksonecut))) + assert np.all(file["maskscutflow"] == list(dask.compute(*maskscutflow))) + os.remove("cutflow.npz") + + honecut, hcutflow, hlabels = dask.compute(*cutflow.yieldhist()) + + assert hlabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + assert np.all(honecut.axes["onecut"].edges == np.arange(0, 5)) + assert np.all(hcutflow.axes["cutflow"].edges == np.arange(0, 5)) + + assert np.all(honecut.counts() == list(dask.compute(*nevonecut))) + assert np.all(hcutflow.counts() == list(dask.compute(*nevcutflow))) + + # with pytest.raises(IncompatiblePartitions): + # cutflow.plot_vars( + # {"Ept": events.Electron_pt, "Ephi": events[:20].Electron_phi} + # ) + honecuts, hcutflows, hslabels = dask.compute( + *cutflow.plot_vars({"ept": events.Electron_pt, "ephi": events.Electron_phi}) + ) + + assert hslabels == ["initial", "noMuon", "twoElectron", "leadPt20"] + + for h, array in zip(honecuts, [events.Electron_pt, events.Electron_phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute(), + twoelectron.compute(), + leadpt20.compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) + + for h, array in zip(hcutflows, [events.Electron_pt, events.Electron_phi]): + edges = h.axes[0].edges + for i, truth in enumerate( + [ + np.ones(40, dtype=bool), + nomuon.compute(), + (nomuon & twoelectron).compute(), + (nomuon & twoelectron & leadpt20).compute(), + ] + ): + counts = h[:, i].counts() + c, e = np.histogram(dak.flatten(array[truth]).compute(), bins=edges) + assert np.all(counts == c) diff --git a/tests/test_jetmet_tools.py b/tests/test_jetmet_tools.py index 96c5002df..a7ef91385 100644 --- a/tests/test_jetmet_tools.py +++ b/tests/test_jetmet_tools.py @@ -1,8 +1,10 @@ import time import awkward as ak +import dask import dask_awkward as dak import pyinstrument +import pytest from dummy_distributions import dummy_jagged_eta_pt from coffea.util import numpy as np @@ -38,869 +40,916 @@ def jetmet_evaluator(): evaluator = jetmet_evaluator() -def test_factorized_jet_corrector(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_factorized_jet_corrector(optimization_enabled): from coffea.jetmet_tools import FactorizedJetCorrector - counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_Rho = np.full_like(test_eta, 100.0) - test_A = np.full_like(test_eta, 5.0) - - # Check that the FactorizedJetCorrector is functional - jec_names = [ - "Summer16_23Sep2016V3_MC_L1FastJet_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L2Relative_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L2L3Residual_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L3Absolute_AK4PFPuppi", - ] - corrector = FactorizedJetCorrector(**{name: evaluator[name] for name in jec_names}) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() + test_Rho = np.full_like(test_eta, 100.0) + test_A = np.full_like(test_eta, 5.0) + + # Check that the FactorizedJetCorrector is functional + jec_names = [ + "Summer16_23Sep2016V3_MC_L1FastJet_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L2Relative_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L2L3Residual_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L3Absolute_AK4PFPuppi", + ] + corrector = FactorizedJetCorrector( + **{name: evaluator[name] for name in jec_names} + ) - print(corrector) + print(corrector) - pt_copy = np.copy(test_pt) + pt_copy = np.copy(test_pt) - # Check that the corrector can be evaluated for flattened arrays - corrs = corrector.getCorrection( - JetEta=test_eta, Rho=test_Rho, JetPt=test_pt, JetA=test_A - ) + # Check that the corrector can be evaluated for flattened arrays + corrs = corrector.getCorrection( + JetEta=test_eta, Rho=test_Rho, JetPt=test_pt, JetA=test_A + ) - assert (np.abs(pt_copy - test_pt) < 1e-6).all() + assert (np.abs(pt_copy - test_pt) < 1e-6).all() - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) - test_Rho_jag = ak.unflatten(test_Rho, counts) - test_A_jag = ak.unflatten(test_A, counts) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) + test_Rho_jag = ak.unflatten(test_Rho, counts) + test_A_jag = ak.unflatten(test_A, counts) - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) - test_Rho_dak = dak.from_awkward(test_Rho_jag, 1) - test_A_dak = dak.from_awkward(test_A_jag, 1) + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) + test_Rho_dak = dak.from_awkward(test_Rho_jag, 1) + test_A_dak = dak.from_awkward(test_A_jag, 1) - # Check that the corrector can be evaluated for jagged arrays - corrs_jag = corrector.getCorrection( - JetEta=test_eta_jag, - Rho=test_Rho_jag, - JetPt=test_pt_jag, - JetA=test_A_jag, - ) + # Check that the corrector can be evaluated for jagged arrays + corrs_jag = corrector.getCorrection( + JetEta=test_eta_jag, + Rho=test_Rho_jag, + JetPt=test_pt_jag, + JetA=test_A_jag, + ) - print(corrs_jag) + print(corrs_jag) - corrs_dak = corrector.getCorrection( - JetEta=test_eta_dak, - Rho=test_Rho_dak, - JetPt=test_pt_dak, - JetA=test_A_dak, - ) + corrs_dak = corrector.getCorrection( + JetEta=test_eta_dak, + Rho=test_Rho_dak, + JetPt=test_pt_dak, + JetA=test_A_dak, + ) - print(corrs_dak) - print(corrs_dak.dask) - - assert ak.all(np.abs(pt_copy - ak.flatten(test_pt_jag)) < 1e-6) - assert ak.all(np.abs(corrs - ak.flatten(corrs_jag)) < 1e-6) - assert ak.all(np.abs(corrs - ak.flatten(corrs_dak.compute())) < 1e-6) - - # Check that the corrector returns the correct answers for each level of correction - # Use a subset of the values so that we can check the corrections by hand - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - test_Rho_jag = test_Rho_jag[0:3] - test_A_jag = test_A_jag[0:3] - counts = counts[0:3] - test_pt_dak = test_pt_dak[0:3] - test_eta_dak = test_eta_dak[0:3] - test_Rho_dak = test_Rho_dak[0:3] - test_A_dak = test_A_dak[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag) - print("eta:", test_eta_jag) - print("rho:", test_Rho_jag) - print("area:", test_A_jag, "\n") - - # Start by checking the L1 corrections - corrs_L1_jag_ref = ak.full_like(test_pt_jag, 1.0) - corrector = FactorizedJetCorrector( - **{name: evaluator[name] for name in jec_names[0:1]} - ) - corrs_L1_jag = corrector.getCorrection( - JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag, JetA=test_A_jag - ) + print(corrs_dak) + print(corrs_dak.dask) + + assert ak.all(np.abs(pt_copy - ak.flatten(test_pt_jag)) < 1e-6) + assert ak.all(np.abs(corrs - ak.flatten(corrs_jag)) < 1e-6) + assert ak.all(np.abs(corrs - ak.flatten(corrs_dak.compute())) < 1e-6) + + # Check that the corrector returns the correct answers for each level of correction + # Use a subset of the values so that we can check the corrections by hand + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + test_Rho_jag = test_Rho_jag[0:3] + test_A_jag = test_A_jag[0:3] + counts = counts[0:3] + test_pt_dak = test_pt_dak[0:3] + test_eta_dak = test_eta_dak[0:3] + test_Rho_dak = test_Rho_dak[0:3] + test_A_dak = test_A_dak[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag) + print("eta:", test_eta_jag) + print("rho:", test_Rho_jag) + print("area:", test_A_jag, "\n") + + # Start by checking the L1 corrections + corrs_L1_jag_ref = ak.full_like(test_pt_jag, 1.0) + corrector = FactorizedJetCorrector( + **{name: evaluator[name] for name in jec_names[0:1]} + ) + corrs_L1_jag = corrector.getCorrection( + JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag, JetA=test_A_jag + ) - corrs_L1_dak = corrector.getCorrection( - JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak, JetA=test_A_dak - ) + corrs_L1_dak = corrector.getCorrection( + JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak, JetA=test_A_dak + ) - print(corrs_L1_dak) - print(corrs_L1_dak.dask) + print(corrs_L1_dak) + print(corrs_L1_dak.dask) - print("Reference L1 corrections:", corrs_L1_jag_ref) - print("Calculated L1 corrections:", corrs_L1_jag) - assert ak.all( - np.abs(ak.flatten(corrs_L1_jag_ref) - ak.flatten(corrs_L1_jag)) < 1e-6 - ) - assert ak.all( - np.abs(ak.flatten(corrs_L1_jag_ref) - ak.flatten(corrs_L1_dak.compute())) < 1e-6 - ) + print("Reference L1 corrections:", corrs_L1_jag_ref) + print("Calculated L1 corrections:", corrs_L1_jag) + assert ak.all( + np.abs(ak.flatten(corrs_L1_jag_ref) - ak.flatten(corrs_L1_jag)) < 1e-6 + ) + assert ak.all( + np.abs(ak.flatten(corrs_L1_jag_ref) - ak.flatten(corrs_L1_dak.compute())) + < 1e-6 + ) - # Apply the L1 corrections and save the result - test_ptL1_jag = test_pt_jag * corrs_L1_jag - print("L1 corrected pT values:", test_ptL1_jag, "\n") - assert ak.all(np.abs(ak.flatten(test_pt_jag) - ak.flatten(test_ptL1_jag)) < 1e-6) + # Apply the L1 corrections and save the result + test_ptL1_jag = test_pt_jag * corrs_L1_jag + print("L1 corrected pT values:", test_ptL1_jag, "\n") + assert ak.all( + np.abs(ak.flatten(test_pt_jag) - ak.flatten(test_ptL1_jag)) < 1e-6 + ) - test_ptL1_dak = test_pt_dak * corrs_L1_dak - print(test_ptL1_dak) - assert ak.all( - np.abs(ak.flatten(test_pt_jag) - ak.flatten(test_ptL1_dak.compute())) < 1e-6 - ) + test_ptL1_dak = test_pt_dak * corrs_L1_dak + print(test_ptL1_dak) + assert ak.all( + np.abs(ak.flatten(test_pt_jag) - ak.flatten(test_ptL1_dak.compute())) < 1e-6 + ) - # Check the L2 corrections on a subset of jets - # Look up the parameters for the L2 corrections by hand and calculate the corrections - # [(1.37906,35.8534,-0.00829227,7.96644e-05,5.18988e-06), - # (1.38034,17.9841,-0.00729638,-0.000127141,5.70889e-05), - # (1.74466,18.6372,-0.0367036,0.00310864,-0.000277062), - # (1.4759,24.8882,-0.0155333,0.0020836,-0.000198039), - # (1.14606,36.4215,-0.00174801,-1.76393e-05,1.91863e-06), - # (0.999657,4.02981,1.06597,-0.619679,-0.0494)], - # [(1.54524,23.9023,-0.0162807,0.000665243,-4.66608e-06), - # (1.48431,8.68725,0.00642424,0.0252104,-0.0335696)]]) - corrs_L2_jag_ref = ak.unflatten( - np.array( - [ - 1.37038741364, - 1.37710384514, - 1.65148641108, - 1.46840446827, - 1.1328319784, - 1.0, - 1.50762056349, - 1.48719866989, - ] - ), - counts, - ) - corrector = FactorizedJetCorrector( - **{name: evaluator[name] for name in jec_names[1:2]} - ) - corrs_L2_jag = corrector.getCorrection( - JetEta=test_eta_jag, JetPt=corrs_L1_jag * test_pt_jag - ) - corrs_L2_dak = corrector.getCorrection( - JetEta=test_eta_dak, JetPt=corrs_L1_dak * test_pt_dak - ) - print("Reference L2 corrections:", corrs_L2_jag_ref.tolist()) - print("Calculated L2 corrections:", corrs_L2_jag.tolist()) - assert ak.all( - np.abs(ak.flatten(corrs_L2_jag_ref) - ak.flatten(corrs_L2_jag)) < 1e-6 - ) - assert ak.all( - np.abs(ak.flatten(corrs_L2_jag_ref) - ak.flatten(corrs_L2_dak.compute())) < 1e-6 - ) + # Check the L2 corrections on a subset of jets + # Look up the parameters for the L2 corrections by hand and calculate the corrections + # [(1.37906,35.8534,-0.00829227,7.96644e-05,5.18988e-06), + # (1.38034,17.9841,-0.00729638,-0.000127141,5.70889e-05), + # (1.74466,18.6372,-0.0367036,0.00310864,-0.000277062), + # (1.4759,24.8882,-0.0155333,0.0020836,-0.000198039), + # (1.14606,36.4215,-0.00174801,-1.76393e-05,1.91863e-06), + # (0.999657,4.02981,1.06597,-0.619679,-0.0494)], + # [(1.54524,23.9023,-0.0162807,0.000665243,-4.66608e-06), + # (1.48431,8.68725,0.00642424,0.0252104,-0.0335696)]]) + corrs_L2_jag_ref = ak.unflatten( + np.array( + [ + 1.37038741364, + 1.37710384514, + 1.65148641108, + 1.46840446827, + 1.1328319784, + 1.0, + 1.50762056349, + 1.48719866989, + ] + ), + counts, + ) + corrector = FactorizedJetCorrector( + **{name: evaluator[name] for name in jec_names[1:2]} + ) + corrs_L2_jag = corrector.getCorrection( + JetEta=test_eta_jag, JetPt=corrs_L1_jag * test_pt_jag + ) + corrs_L2_dak = corrector.getCorrection( + JetEta=test_eta_dak, JetPt=corrs_L1_dak * test_pt_dak + ) + print("Reference L2 corrections:", corrs_L2_jag_ref.tolist()) + print("Calculated L2 corrections:", corrs_L2_jag.tolist()) + assert ak.all( + np.abs(ak.flatten(corrs_L2_jag_ref) - ak.flatten(corrs_L2_jag)) < 1e-6 + ) + assert ak.all( + np.abs(ak.flatten(corrs_L2_jag_ref) - ak.flatten(corrs_L2_dak.compute())) + < 1e-6 + ) - # Apply the L2 corrections and save the result - test_ptL1L2_jag = test_ptL1_jag * corrs_L2_jag - print("L1L2 corrected pT values:", test_ptL1L2_jag, "\n") - test_ptL1L2_dak = test_ptL1_dak * corrs_L2_dak - print("L1L2 corrected pT values:", test_ptL1L2_dak.compute(), "\n") - print(test_ptL1L2_dak) - print(test_ptL1L2_dak.dask) - - # Apply the L3 corrections and save the result - corrs_L3_jag = ak.full_like(test_pt_jag, 1.0) - test_ptL1L2L3_jag = test_ptL1L2_jag * corrs_L3_jag - print("L1L2L3 corrected pT values:", test_ptL1L2L3_jag, "\n") - - corrs_L3_dak = dak.ones_like(test_pt_dak) - test_ptL1L2L3_dak = test_ptL1L2_dak * corrs_L3_dak - print("L1L2L3 corrected pT values:", test_ptL1L2L3_dak.compute(), "\n") - print(test_ptL1L2L3_dak) - print(test_ptL1L2L3_dak.dask) - - # Check that the corrections can be chained together - corrs_L1L2L3_jag_ref = ak.unflatten( - np.array( - [ - 1.37038741364, - 1.37710384514, - 1.65148641108, - 1.46840446827, - 1.1328319784, - 1.0, - 1.50762056349, - 1.48719866989, - ] - ), - counts, - ) - corrector = FactorizedJetCorrector( - **{name: evaluator[name] for name in (jec_names[0:2] + jec_names[3:])} - ) - corrs_L1L2L3_jag = corrector.getCorrection( - JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag, JetA=test_A_jag - ) + # Apply the L2 corrections and save the result + test_ptL1L2_jag = test_ptL1_jag * corrs_L2_jag + print("L1L2 corrected pT values:", test_ptL1L2_jag, "\n") + test_ptL1L2_dak = test_ptL1_dak * corrs_L2_dak + print("L1L2 corrected pT values:", test_ptL1L2_dak.compute(), "\n") + print(test_ptL1L2_dak) + print(test_ptL1L2_dak.dask) + + # Apply the L3 corrections and save the result + corrs_L3_jag = ak.full_like(test_pt_jag, 1.0) + test_ptL1L2L3_jag = test_ptL1L2_jag * corrs_L3_jag + print("L1L2L3 corrected pT values:", test_ptL1L2L3_jag, "\n") + + corrs_L3_dak = dak.ones_like(test_pt_dak) + test_ptL1L2L3_dak = test_ptL1L2_dak * corrs_L3_dak + print("L1L2L3 corrected pT values:", test_ptL1L2L3_dak.compute(), "\n") + print(test_ptL1L2L3_dak) + print(test_ptL1L2L3_dak.dask) + + # Check that the corrections can be chained together + corrs_L1L2L3_jag_ref = ak.unflatten( + np.array( + [ + 1.37038741364, + 1.37710384514, + 1.65148641108, + 1.46840446827, + 1.1328319784, + 1.0, + 1.50762056349, + 1.48719866989, + ] + ), + counts, + ) + corrector = FactorizedJetCorrector( + **{name: evaluator[name] for name in (jec_names[0:2] + jec_names[3:])} + ) + corrs_L1L2L3_jag = corrector.getCorrection( + JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag, JetA=test_A_jag + ) - corrs_L1L2L3_dak = corrector.getCorrection( - JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak, JetA=test_A_dak - ) + corrs_L1L2L3_dak = corrector.getCorrection( + JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak, JetA=test_A_dak + ) - print("Reference L1L2L3 corrections:", corrs_L1L2L3_jag_ref) - print("Calculated L1L2L3 corrections:", corrs_L1L2L3_jag) - print("Calculated L1L2L3 corrections:", corrs_L1L2L3_dak.compute()) - assert ak.all( - np.abs(ak.flatten(corrs_L1L2L3_jag_ref) - ak.flatten(corrs_L1L2L3_jag)) < 1e-6 - ) - assert ak.all( - np.abs( - ak.flatten(corrs_L1L2L3_jag_ref) - ak.flatten(corrs_L1L2L3_dak.compute()) + print("Reference L1L2L3 corrections:", corrs_L1L2L3_jag_ref) + print("Calculated L1L2L3 corrections:", corrs_L1L2L3_jag) + print("Calculated L1L2L3 corrections:", corrs_L1L2L3_dak.compute()) + assert ak.all( + np.abs(ak.flatten(corrs_L1L2L3_jag_ref) - ak.flatten(corrs_L1L2L3_jag)) + < 1e-6 + ) + assert ak.all( + np.abs( + ak.flatten(corrs_L1L2L3_jag_ref) + - ak.flatten(corrs_L1L2L3_dak.compute()) + ) + < 1e-6 + ) + print(corrs_L1L2L3_dak) + print(corrs_L1L2L3_dak.dask) + + # Apply the L1L2L3 corrections and save the result + test_ptL1L2L3chain_jag = test_pt_jag * corrs_L1L2L3_jag + print("Chained L1L2L3 corrected pT values:", test_ptL1L2L3chain_jag, "\n") + assert ak.all( + np.abs(ak.flatten(test_ptL1L2L3_jag) - ak.flatten(test_ptL1L2L3chain_jag)) + < 1e-6 ) - < 1e-6 - ) - print(corrs_L1L2L3_dak) - print(corrs_L1L2L3_dak.dask) - - # Apply the L1L2L3 corrections and save the result - test_ptL1L2L3chain_jag = test_pt_jag * corrs_L1L2L3_jag - print("Chained L1L2L3 corrected pT values:", test_ptL1L2L3chain_jag, "\n") - assert ak.all( - np.abs(ak.flatten(test_ptL1L2L3_jag) - ak.flatten(test_ptL1L2L3chain_jag)) - < 1e-6 - ) - test_ptL1L2L3chain_dak = test_pt_dak * corrs_L1L2L3_dak - print("Chained L1L2L3 corrected pT values:", test_ptL1L2L3chain_dak.compute(), "\n") - assert ak.all( - np.abs( - ak.flatten(test_ptL1L2L3_jag) - ak.flatten(test_ptL1L2L3chain_dak.compute()) + test_ptL1L2L3chain_dak = test_pt_dak * corrs_L1L2L3_dak + print( + "Chained L1L2L3 corrected pT values:", + test_ptL1L2L3chain_dak.compute(), + "\n", ) - < 1e-6 - ) - print(test_ptL1L2L3chain_dak) - print(test_ptL1L2L3chain_dak.dask) + assert ak.all( + np.abs( + ak.flatten(test_ptL1L2L3_jag) + - ak.flatten(test_ptL1L2L3chain_dak.compute()) + ) + < 1e-6 + ) + print(test_ptL1L2L3chain_dak) + print(test_ptL1L2L3chain_dak.dask) -def test_jet_resolution(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_resolution(optimization_enabled): from coffea.jetmet_tools import JetResolution - counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_Rho = np.full_like(test_eta, 10.0) - - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) - test_Rho_jag = ak.unflatten(test_Rho, counts) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() + test_Rho = np.full_like(test_eta, 10.0) - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) - test_Rho_dak = dak.from_awkward(test_Rho_jag, 1) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) + test_Rho_jag = ak.unflatten(test_Rho, counts) - jer_names = ["Spring16_25nsV10_MC_PtResolution_AK4PFPuppi"] - reso = JetResolution(**{name: evaluator[name] for name in jer_names}) + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) + test_Rho_dak = dak.from_awkward(test_Rho_jag, 1) - print(reso) + jer_names = ["Spring16_25nsV10_MC_PtResolution_AK4PFPuppi"] + reso = JetResolution(**{name: evaluator[name] for name in jer_names}) - resos = reso.getResolution(JetEta=test_eta, Rho=test_Rho, JetPt=test_pt) - resos_jag = reso.getResolution( - JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag - ) - resos_dak = reso.getResolution( - JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak - ) - assert ak.all(np.abs(resos - ak.flatten(resos_jag)) < 1e-6) - assert ak.all(np.abs(resos - ak.flatten(resos_dak.compute())) < 1e-6) - print(resos_dak) - print(resos_dak.dask) - - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - test_Rho_jag = test_Rho_jag[0:3] - test_Rho_jag = ak.concatenate( - [test_Rho_jag[:-1], [ak.concatenate([test_Rho_jag[-1, :-1], 100.0])]] - ) - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag) - print("eta:", test_eta_jag) - print("rho:", test_Rho_jag, "\n") - - resos_jag_ref = ak.unflatten( - np.array( - [ - 0.21974642, - 0.32421591, - 0.33702479, - 0.27420327, - 0.13940689, - 0.48134521, - 0.26564994, - 1.0, - ] - ), - counts, - ) - resos_jag = reso.getResolution( - JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag - ) - print("Reference Resolution (jagged):", resos_jag_ref) - print("Resolution (jagged):", resos_jag) - # NB: 5e-4 tolerance was agreed upon by lgray and aperloff, if the differences get bigger over time - # we need to agree upon how these numbers are evaluated (double/float conversion is kinda random) - assert ak.all(np.abs(ak.flatten(resos_jag_ref) - ak.flatten(resos_jag)) < 5e-4) + print(reso) - -def test_jet_correction_uncertainty(): - from coffea.jetmet_tools import JetCorrectionUncertainty - - counts, test_eta, test_pt = dummy_jagged_eta_pt() - - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) - - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) - - junc_names = ["Summer16_23Sep2016V3_MC_Uncertainty_AK4PFPuppi"] - junc = JetCorrectionUncertainty(**{name: evaluator[name] for name in junc_names}) - - print(junc) - - juncs = junc.getUncertainty(JetEta=test_eta, JetPt=test_pt) - - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - - juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) - - for i, (level, corrs) in enumerate(juncs): - assert corrs.shape[0] == test_eta.shape[0] - assert ak.all(corrs == ak.flatten(juncs_jag[i][1])) - assert ak.all(corrs == ak.flatten(juncs_dak[i][1].compute())) - - zipped_dak = dak.zip({k: v for k, v in juncs_dak}) - print(zipped_dak) - print(zipped_dak.dask) - - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag.tolist()) - print("eta:", test_eta_jag.tolist(), "\n") - - juncs_jag_ref = ak.unflatten( - np.array( - [ - [1.053504214, 0.946495786], - [1.033343349, 0.966656651], - [1.065159157, 0.934840843], - [1.033140127, 0.966859873], - [1.016858652, 0.983141348], - [1.130199999, 0.869800001], - [1.039968468, 0.960031532], - [1.033100002, 0.966899998], - ] - ), - counts, - ) - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - - for i, (level, corrs) in enumerate(juncs_jag): - print("Index:", i) - print("Correction level:", level) - print("Reference Uncertainties (jagged):", juncs_jag_ref) - print("Uncertainties (jagged):", corrs) - assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) + resos = reso.getResolution(JetEta=test_eta, Rho=test_Rho, JetPt=test_pt) + resos_jag = reso.getResolution( + JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag + ) + resos_dak = reso.getResolution( + JetEta=test_eta_dak, Rho=test_Rho_dak, JetPt=test_pt_dak + ) + assert ak.all(np.abs(resos - ak.flatten(resos_jag)) < 1e-6) + assert ak.all(np.abs(resos - ak.flatten(resos_dak.compute())) < 1e-6) + print(resos_dak) + print(resos_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + test_Rho_jag = test_Rho_jag[0:3] + test_Rho_jag = ak.concatenate( + [test_Rho_jag[:-1], [ak.concatenate([test_Rho_jag[-1, :-1], 100.0])]] + ) + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag) + print("eta:", test_eta_jag) + print("rho:", test_Rho_jag, "\n") + + resos_jag_ref = ak.unflatten( + np.array( + [ + 0.21974642, + 0.32421591, + 0.33702479, + 0.27420327, + 0.13940689, + 0.48134521, + 0.26564994, + 1.0, + ] + ), + counts, + ) + resos_jag = reso.getResolution( + JetEta=test_eta_jag, Rho=test_Rho_jag, JetPt=test_pt_jag + ) + print("Reference Resolution (jagged):", resos_jag_ref) + print("Resolution (jagged):", resos_jag) + # NB: 5e-4 tolerance was agreed upon by lgray and aperloff, if the differences get bigger over time + # we need to agree upon how these numbers are evaluated (double/float conversion is kinda random) + assert ak.all(np.abs(ak.flatten(resos_jag_ref) - ak.flatten(resos_jag)) < 5e-4) -def test_jet_correction_uncertainty_sources(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_correction_uncertainty(optimization_enabled): from coffea.jetmet_tools import JetCorrectionUncertainty - counts, test_eta, test_pt = dummy_jagged_eta_pt() - - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) - - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) - - junc_names = [] - levels = [] - for name in dir(evaluator): - if "Summer16_23Sep2016V3_MC_UncertaintySources_AK4PFPuppi" in name: - junc_names.append(name) - levels.append(name.split("_")[-1]) - # test for underscore in dataera - if "Fall17_17Nov2017_V6_MC_UncertaintySources_AK4PFchs_AbsoluteFlavMap" in name: - junc_names.append(name) - levels.append(name.split("_")[-1]) - junc = JetCorrectionUncertainty(**{name: evaluator[name] for name in junc_names}) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() - print(junc) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) - juncs = junc.getUncertainty(JetEta=test_eta, JetPt=test_pt) + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - - juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) + junc_names = ["Summer16_23Sep2016V3_MC_Uncertainty_AK4PFPuppi"] + junc = JetCorrectionUncertainty( + **{name: evaluator[name] for name in junc_names} + ) - for i, (level, corrs) in enumerate(juncs): - assert level in levels - assert corrs.shape[0] == test_eta.shape[0] - assert ak.all(corrs == ak.flatten(juncs_jag[i][1])) - assert ak.all(corrs == ak.flatten(juncs_dak[i][1].compute())) + print(junc) + + juncs = junc.getUncertainty(JetEta=test_eta, JetPt=test_pt) + + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) + + juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) + + for i, (level, corrs) in enumerate(juncs): + assert corrs.shape[0] == test_eta.shape[0] + assert ak.all(corrs == ak.flatten(juncs_jag[i][1])) + assert ak.all(corrs == ak.flatten(juncs_dak[i][1].compute())) + + zipped_dak = dak.zip({k: v for k, v in juncs_dak}) + print(zipped_dak) + print(zipped_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag.tolist()) + print("eta:", test_eta_jag.tolist(), "\n") + + juncs_jag_ref = ak.unflatten( + np.array( + [ + [1.053504214, 0.946495786], + [1.033343349, 0.966656651], + [1.065159157, 0.934840843], + [1.033140127, 0.966859873], + [1.016858652, 0.983141348], + [1.130199999, 0.869800001], + [1.039968468, 0.960031532], + [1.033100002, 0.966899998], + ] + ), + counts, + ) + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - zipped_dak = dak.zip({k: v for k, v in juncs_dak}) - print(zipped_dak) - print(zipped_dak.dask) + for i, (level, corrs) in enumerate(juncs_jag): + print("Index:", i) + print("Correction level:", level) + print("Reference Uncertainties (jagged):", juncs_jag_ref) + print("Uncertainties (jagged):", corrs) + assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag.tolist()) - print("eta:", test_eta_jag.tolist(), "\n") - juncs_jag_ref = ak.unflatten( - np.array( - [ - [1.053504214, 0.946495786], - [1.033343349, 0.966656651], - [1.065159157, 0.934840843], - [1.033140127, 0.966859873], - [1.016858652, 0.983141348], - [1.130199999, 0.869800001], - [1.039968468, 0.960031532], - [1.033100002, 0.966899998], - ] - ), - counts, - ) - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - for i, (level, corrs) in enumerate(juncs_jag): - if level != "Total": - continue - print("Index:", i) - print("Correction level:", level) - print("Reference Uncertainties (jagged):", juncs_jag_ref) - print("Uncertainties (jagged):", corrs, "\n") - assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) - - -def test_jet_correction_regrouped_uncertainty_sources(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_correction_uncertainty_sources(optimization_enabled): from coffea.jetmet_tools import JetCorrectionUncertainty - counts, test_eta, test_pt = dummy_jagged_eta_pt() + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) - junc_names = [] - levels = [] - for name in dir(evaluator): - if "Regrouped_Fall17_17Nov2017_V32_MC_UncertaintySources_AK4PFchs" in name: - junc_names.append(name) - if len(name.split("_")) == 9: - levels.append("_".join(name.split("_")[-2:])) - else: + junc_names = [] + levels = [] + for name in dir(evaluator): + if "Summer16_23Sep2016V3_MC_UncertaintySources_AK4PFPuppi" in name: + junc_names.append(name) levels.append(name.split("_")[-1]) - junc = JetCorrectionUncertainty(**{name: evaluator[name] for name in junc_names}) - - print(junc) - - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - - juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) - - for i, tpl in enumerate(list(junc.getUncertainty(JetEta=test_eta, JetPt=test_pt))): - assert tpl[0] in levels - assert tpl[1].shape[0] == test_eta.shape[0] - assert ak.all(tpl[1] == ak.flatten(juncs_jag[i][1])) - assert ak.all(tpl[1] == ak.flatten(juncs_dak[i][1].compute())) + # test for underscore in dataera + if ( + "Fall17_17Nov2017_V6_MC_UncertaintySources_AK4PFchs_AbsoluteFlavMap" + in name + ): + junc_names.append(name) + levels.append(name.split("_")[-1]) + junc = JetCorrectionUncertainty( + **{name: evaluator[name] for name in junc_names} + ) - zipped_dak = dak.zip({k: v for k, v in juncs_dak}) - print(zipped_dak) - print(zipped_dak.dask) + print(junc) + + juncs = junc.getUncertainty(JetEta=test_eta, JetPt=test_pt) + + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) + + juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) + + for i, (level, corrs) in enumerate(juncs): + assert level in levels + assert corrs.shape[0] == test_eta.shape[0] + assert ak.all(corrs == ak.flatten(juncs_jag[i][1])) + assert ak.all(corrs == ak.flatten(juncs_dak[i][1].compute())) + + zipped_dak = dak.zip({k: v for k, v in juncs_dak}) + print(zipped_dak) + print(zipped_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag.tolist()) + print("eta:", test_eta_jag.tolist(), "\n") + + juncs_jag_ref = ak.unflatten( + np.array( + [ + [1.053504214, 0.946495786], + [1.033343349, 0.966656651], + [1.065159157, 0.934840843], + [1.033140127, 0.966859873], + [1.016858652, 0.983141348], + [1.130199999, 0.869800001], + [1.039968468, 0.960031532], + [1.033100002, 0.966899998], + ] + ), + counts, + ) + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) + for i, (level, corrs) in enumerate(juncs_jag): + if level != "Total": + continue + print("Index:", i) + print("Correction level:", level) + print("Reference Uncertainties (jagged):", juncs_jag_ref) + print("Uncertainties (jagged):", corrs, "\n") + assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_correction_regrouped_uncertainty_sources(optimization_enabled): + from coffea.jetmet_tools import JetCorrectionUncertainty - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag.tolist()) - print("eta:", test_eta_jag.tolist(), "\n") + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() + + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) + + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) + + junc_names = [] + levels = [] + for name in dir(evaluator): + if "Regrouped_Fall17_17Nov2017_V32_MC_UncertaintySources_AK4PFchs" in name: + junc_names.append(name) + if len(name.split("_")) == 9: + levels.append("_".join(name.split("_")[-2:])) + else: + levels.append(name.split("_")[-1]) + junc = JetCorrectionUncertainty( + **{name: evaluator[name] for name in junc_names} + ) - juncs_jag_ref = ak.unflatten( - np.array( - [ - [1.119159088, 0.880840912], - [1.027003404, 0.972996596], - [1.135201275, 0.864798725], - [1.039665259, 0.960334741], - [1.015064503, 0.984935497], - [1.149900004, 0.850099996], - [1.079960600, 0.920039400], - [1.041200001, 0.958799999], - ] - ), - counts, - ) - juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) - for i, (level, corrs) in enumerate(juncs_jag): - if level != "Total": - continue - print("Index:", i) - print("Correction level:", level) - print("Reference Uncertainties (jagged):", juncs_jag_ref) - print("Uncertainties (jagged):", corrs, "\n") - assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) - - -def test_jet_resolution_sf(): + print(junc) + + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) + + juncs_dak = list(junc.getUncertainty(JetEta=test_eta_dak, JetPt=test_pt_dak)) + + for i, tpl in enumerate( + list(junc.getUncertainty(JetEta=test_eta, JetPt=test_pt)) + ): + assert tpl[0] in levels + assert tpl[1].shape[0] == test_eta.shape[0] + assert ak.all(tpl[1] == ak.flatten(juncs_jag[i][1])) + assert ak.all(tpl[1] == ak.flatten(juncs_dak[i][1].compute())) + + zipped_dak = dak.zip({k: v for k, v in juncs_dak}) + print(zipped_dak) + print(zipped_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag.tolist()) + print("eta:", test_eta_jag.tolist(), "\n") + + juncs_jag_ref = ak.unflatten( + np.array( + [ + [1.119159088, 0.880840912], + [1.027003404, 0.972996596], + [1.135201275, 0.864798725], + [1.039665259, 0.960334741], + [1.015064503, 0.984935497], + [1.149900004, 0.850099996], + [1.079960600, 0.920039400], + [1.041200001, 0.958799999], + ] + ), + counts, + ) + juncs_jag = list(junc.getUncertainty(JetEta=test_eta_jag, JetPt=test_pt_jag)) + for i, (level, corrs) in enumerate(juncs_jag): + if level != "Total": + continue + print("Index:", i) + print("Correction level:", level) + print("Reference Uncertainties (jagged):", juncs_jag_ref) + print("Uncertainties (jagged):", corrs, "\n") + assert ak.all(np.abs(ak.flatten(juncs_jag_ref) - ak.flatten(corrs)) < 1e-6) + + +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_resolution_sf(optimization_enabled): from coffea.jetmet_tools import JetResolutionScaleFactor - counts, test_eta, test_pt = dummy_jagged_eta_pt() - - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_eta_dak = dak.from_awkward(test_eta_jag, 1) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) - jersf_names = ["Spring16_25nsV10_MC_SF_AK4PFPuppi"] - resosf = JetResolutionScaleFactor(**{name: evaluator[name] for name in jersf_names}) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) - print(resosf) - - # 0-jet compatibility - assert resosf.getScaleFactor(JetEta=test_eta[:0]).shape == (0, 3) - - resosfs = resosf.getScaleFactor(JetEta=test_eta) - resosfs_jag = resosf.getScaleFactor(JetEta=test_eta_jag) - resosfs_dak = resosf.getScaleFactor(JetEta=test_eta_dak) - assert ak.all(resosfs == ak.flatten(resosfs_jag)) - assert ak.all(resosfs == ak.flatten(resosfs_dak.compute())) - print(resosfs_dak) - print(resosfs_dak.dask) - - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag) - print("eta:", test_eta_jag, "\n") + jersf_names = ["Spring16_25nsV10_MC_SF_AK4PFPuppi"] + resosf = JetResolutionScaleFactor( + **{name: evaluator[name] for name in jersf_names} + ) - resosfs_jag_ref = ak.unflatten( - np.array( - [ - [1.857, 1.928, 1.786], - [1.084, 1.095, 1.073], - [1.364, 1.403, 1.325], - [1.177, 1.218, 1.136], - [1.138, 1.151, 1.125], - [1.364, 1.403, 1.325], - [1.177, 1.218, 1.136], - [1.082, 1.117, 1.047], - ] - ), - counts, - ) - resosfs_jag = resosf.getScaleFactor(JetEta=test_eta_jag) - print("Reference Resolution SF (jagged):", resosfs_jag_ref) - print("Resolution SF (jagged):", resosfs_jag) - assert ak.all(np.abs(ak.flatten(resosfs_jag_ref) - ak.flatten(resosfs_jag)) < 1e-6) + print(resosf) + + # 0-jet compatibility + assert resosf.getScaleFactor(JetEta=test_eta[:0]).shape == (0, 3) + + resosfs = resosf.getScaleFactor(JetEta=test_eta) + resosfs_jag = resosf.getScaleFactor(JetEta=test_eta_jag) + resosfs_dak = resosf.getScaleFactor(JetEta=test_eta_dak) + assert ak.all(resosfs == ak.flatten(resosfs_jag)) + assert ak.all(resosfs == ak.flatten(resosfs_dak.compute())) + print(resosfs_dak) + print(resosfs_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag) + print("eta:", test_eta_jag, "\n") + + resosfs_jag_ref = ak.unflatten( + np.array( + [ + [1.857, 1.928, 1.786], + [1.084, 1.095, 1.073], + [1.364, 1.403, 1.325], + [1.177, 1.218, 1.136], + [1.138, 1.151, 1.125], + [1.364, 1.403, 1.325], + [1.177, 1.218, 1.136], + [1.082, 1.117, 1.047], + ] + ), + counts, + ) + resosfs_jag = resosf.getScaleFactor(JetEta=test_eta_jag) + print("Reference Resolution SF (jagged):", resosfs_jag_ref) + print("Resolution SF (jagged):", resosfs_jag) + assert ak.all( + np.abs(ak.flatten(resosfs_jag_ref) - ak.flatten(resosfs_jag)) < 1e-6 + ) -def test_jet_resolution_sf_2d(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_jet_resolution_sf_2d(optimization_enabled): from coffea.jetmet_tools import JetResolutionScaleFactor - counts, test_eta, test_pt = dummy_jagged_eta_pt() - - test_pt_jag = ak.unflatten(test_pt, counts) - test_eta_jag = ak.unflatten(test_eta, counts) + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_pt_dak = dak.from_awkward(test_pt_jag, 1) - test_eta_dak = dak.from_awkward(test_eta_jag, 1) + test_pt_jag = ak.unflatten(test_pt, counts) + test_eta_jag = ak.unflatten(test_eta, counts) - resosf = JetResolutionScaleFactor( - **{name: evaluator[name] for name in ["Autumn18_V7_MC_SF_AK4PFchs"]} - ) - - print(resosf) + test_pt_dak = dak.from_awkward(test_pt_jag, 1) + test_eta_dak = dak.from_awkward(test_eta_jag, 1) - # 0-jet compatibility - assert resosf.getScaleFactor(JetPt=test_pt[:0], JetEta=test_eta[:0]).shape == (0, 3) + resosf = JetResolutionScaleFactor( + **{name: evaluator[name] for name in ["Autumn18_V7_MC_SF_AK4PFchs"]} + ) - resosfs = resosf.getScaleFactor(JetPt=test_pt, JetEta=test_eta) - resosfs_jag = resosf.getScaleFactor(JetPt=test_pt_jag, JetEta=test_eta_jag) - resosfs_dak = resosf.getScaleFactor(JetPt=test_pt_dak, JetEta=test_eta_dak) - assert ak.all(resosfs == ak.flatten(resosfs_jag)) - assert ak.all(resosfs == ak.flatten(resosfs_dak.compute())) - print(resosfs_dak) - print(resosfs_dak.dask) + print(resosf) - test_pt_jag = test_pt_jag[0:3] - test_eta_jag = test_eta_jag[0:3] - counts = counts[0:3] - print("Raw jet values:") - print("pT:", test_pt_jag) - print("eta:", test_eta_jag, "\n") + # 0-jet compatibility + assert resosf.getScaleFactor(JetPt=test_pt[:0], JetEta=test_eta[:0]).shape == ( + 0, + 3, + ) - resosfs_jag_ref = ak.unflatten( - np.array( - [ - [1.11904, 1.31904, 1.0], - [1.1432, 1.2093, 1.0771], - [1.16633, 1.36633, 1.0], - [1.17642, 1.37642, 1.0], - [1.1808, 1.1977, 1.1640], - [1.15965, 1.35965, 1.0], - [1.17661, 1.37661, 1.0], - [1.1175, 1.1571, 1.0778], - ] - ), - counts, - ) - resosfs_jag = resosf.getScaleFactor(JetPt=test_pt_jag, JetEta=test_eta_jag) - print("Reference Resolution SF (jagged):", resosfs_jag_ref) - print("Resolution SF (jagged):", resosfs_jag) - assert ak.all(np.abs(ak.flatten(resosfs_jag_ref) - ak.flatten(resosfs_jag)) < 1e-6) + resosfs = resosf.getScaleFactor(JetPt=test_pt, JetEta=test_eta) + resosfs_jag = resosf.getScaleFactor(JetPt=test_pt_jag, JetEta=test_eta_jag) + resosfs_dak = resosf.getScaleFactor(JetPt=test_pt_dak, JetEta=test_eta_dak) + assert ak.all(resosfs == ak.flatten(resosfs_jag)) + assert ak.all(resosfs == ak.flatten(resosfs_dak.compute())) + print(resosfs_dak) + print(resosfs_dak.dask) + + test_pt_jag = test_pt_jag[0:3] + test_eta_jag = test_eta_jag[0:3] + counts = counts[0:3] + print("Raw jet values:") + print("pT:", test_pt_jag) + print("eta:", test_eta_jag, "\n") + + resosfs_jag_ref = ak.unflatten( + np.array( + [ + [1.11904, 1.31904, 1.0], + [1.1432, 1.2093, 1.0771], + [1.16633, 1.36633, 1.0], + [1.17642, 1.37642, 1.0], + [1.1808, 1.1977, 1.1640], + [1.15965, 1.35965, 1.0], + [1.17661, 1.37661, 1.0], + [1.1175, 1.1571, 1.0778], + ] + ), + counts, + ) + resosfs_jag = resosf.getScaleFactor(JetPt=test_pt_jag, JetEta=test_eta_jag) + print("Reference Resolution SF (jagged):", resosfs_jag_ref) + print("Resolution SF (jagged):", resosfs_jag) + assert ak.all( + np.abs(ak.flatten(resosfs_jag_ref) - ak.flatten(resosfs_jag)) < 1e-6 + ) -def test_corrected_jets_factory(): +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_corrected_jets_factory(optimization_enabled): import os - import dask - from coffea.jetmet_tools import CorrectedJetsFactory, CorrectedMETFactory, JECStack events = None from coffea.nanoevents import NanoEventsFactory - events = NanoEventsFactory.from_root( - {os.path.abspath("tests/samples/nano_dy.root"): "Events"}, - metadata={}, - permit_dask=True, - ).events() - - jec_stack_names = [ - "Summer16_23Sep2016V3_MC_L1FastJet_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L2Relative_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L2L3Residual_AK4PFPuppi", - "Summer16_23Sep2016V3_MC_L3Absolute_AK4PFPuppi", - "Spring16_25nsV10_MC_PtResolution_AK4PFPuppi", - "Spring16_25nsV10_MC_SF_AK4PFPuppi", - ] - for key in evaluator.keys(): - if "Summer16_23Sep2016V3_MC_UncertaintySources_AK4PFPuppi" in key: - jec_stack_names.append(key) - - jec_inputs = {name: evaluator[name] for name in jec_stack_names} - jec_stack = JECStack(jec_inputs) - - name_map = jec_stack.blank_name_map - name_map["JetPt"] = "pt" - name_map["JetMass"] = "mass" - name_map["JetEta"] = "eta" - name_map["JetA"] = "area" - - jets = events.Jet - - jets["pt_raw"] = (1 - jets["rawFactor"]) * jets.pt - jets["mass_raw"] = (1 - jets["rawFactor"]) * jets.mass - jets["pt_gen"] = dak.fill_none(jets.matched_gen.pt, 0) - jets["rho"] = events.fixedGridRhoFastjetAll - name_map["ptGenJet"] = "pt_gen" - name_map["ptRaw"] = "pt_raw" - name_map["massRaw"] = "mass_raw" - name_map["Rho"] = "rho" - - print(name_map) - - tic = time.time() - jet_factory = CorrectedJetsFactory(name_map, jec_stack) - toc = time.time() - - print("setup corrected jets time =", toc - tic) - - tic = time.time() - prof = pyinstrument.Profiler() - prof.start() - corrected_jets = jet_factory.build(jets) - prof.stop() - toc = time.time() - - print("corrected_jets build time =", toc - tic) - - print(prof.output_text(unicode=True, color=True, show_all=True)) - - print(corrected_jets.dask) - - print("Generated jet pt:", corrected_jets.pt_gen.compute()) - print("Original jet pt:", corrected_jets.pt_orig.compute()) - print("Raw jet pt:", jets.pt_raw) - print("Corrected jet pt:", corrected_jets.pt.compute()) - print("Original jet mass:", corrected_jets.mass_orig.compute()) - print("Raw jet mass:", jets["mass_raw"]) - print("Corrected jet mass:", corrected_jets.mass.compute()) - print("jet eta:", jets.eta) - - tic = time.time() - prof = pyinstrument.Profiler() - prof.start() - - tocompute = { - unc: {"up": corrected_jets[unc].up.pt, "down": corrected_jets[unc].down.pt} - for unc in jet_factory.uncertainties() - } - computed_uncs = dask.compute(tocompute)[0] - - for unc in jet_factory.uncertainties(): - print(unc) - print(computed_uncs[unc]["up"]) - print(computed_uncs[unc]["down"]) - prof.stop() - toc = time.time() - - print(prof.output_text(unicode=True, color=True, show_all=True)) - - print("build all jet variations =", toc - tic) - - # Test that the corrections were applied correctly - from coffea.jetmet_tools import ( - FactorizedJetCorrector, - JetResolution, - JetResolutionScaleFactor, - ) + with dask.config.set({"awkward.optimization.enabled": True}): + events = NanoEventsFactory.from_root( + {os.path.abspath("tests/samples/nano_dy.root"): "Events"}, + metadata={}, + permit_dask=True, + ).events() + + jec_stack_names = [ + "Summer16_23Sep2016V3_MC_L1FastJet_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L2Relative_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L2L3Residual_AK4PFPuppi", + "Summer16_23Sep2016V3_MC_L3Absolute_AK4PFPuppi", + "Spring16_25nsV10_MC_PtResolution_AK4PFPuppi", + "Spring16_25nsV10_MC_SF_AK4PFPuppi", + ] + for key in evaluator.keys(): + if "Summer16_23Sep2016V3_MC_UncertaintySources_AK4PFPuppi" in key: + jec_stack_names.append(key) + + jec_inputs = {name: evaluator[name] for name in jec_stack_names} + jec_stack = JECStack(jec_inputs) + + name_map = jec_stack.blank_name_map + name_map["JetPt"] = "pt" + name_map["JetMass"] = "mass" + name_map["JetEta"] = "eta" + name_map["JetA"] = "area" + + jets = events.Jet + + jets["pt_raw"] = (1 - jets["rawFactor"]) * jets.pt + jets["mass_raw"] = (1 - jets["rawFactor"]) * jets.mass + jets["pt_gen"] = dak.fill_none(jets.matched_gen.pt, 0) + jets["rho"] = events.fixedGridRhoFastjetAll + name_map["ptGenJet"] = "pt_gen" + name_map["ptRaw"] = "pt_raw" + name_map["massRaw"] = "mass_raw" + name_map["Rho"] = "rho" + + print(name_map) + + tic = time.time() + jet_factory = CorrectedJetsFactory(name_map, jec_stack) + toc = time.time() + + print("setup corrected jets time =", toc - tic) + + tic = time.time() + prof = pyinstrument.Profiler() + prof.start() + corrected_jets = jet_factory.build(jets) + prof.stop() + toc = time.time() + + print("corrected_jets build time =", toc - tic) + + print(prof.output_text(unicode=True, color=True, show_all=True)) + + print(corrected_jets.dask) + + print("Generated jet pt:", corrected_jets.pt_gen.compute()) + print("Original jet pt:", corrected_jets.pt_orig.compute()) + print("Raw jet pt:", jets.pt_raw) + print("Corrected jet pt:", corrected_jets.pt.compute()) + print("Original jet mass:", corrected_jets.mass_orig.compute()) + print("Raw jet mass:", jets["mass_raw"]) + print("Corrected jet mass:", corrected_jets.mass.compute()) + print("jet eta:", jets.eta) + + tic = time.time() + prof = pyinstrument.Profiler() + prof.start() + + tocompute = { + unc: {"up": corrected_jets[unc].up.pt, "down": corrected_jets[unc].down.pt} + for unc in jet_factory.uncertainties() + } + computed_uncs = dask.compute(tocompute)[0] + + for unc in jet_factory.uncertainties(): + print(unc) + print(computed_uncs[unc]["up"]) + print(computed_uncs[unc]["down"]) + prof.stop() + toc = time.time() + + print(prof.output_text(unicode=True, color=True, show_all=True)) + + print("build all jet variations =", toc - tic) + + # Test that the corrections were applied correctly + from coffea.jetmet_tools import ( + FactorizedJetCorrector, + JetResolution, + JetResolutionScaleFactor, + ) - corrector = FactorizedJetCorrector( - **{name: evaluator[name] for name in jec_stack_names[0:4]} - ) + corrector = FactorizedJetCorrector( + **{name: evaluator[name] for name in jec_stack_names[0:4]} + ) - check_corrs = corrector.getCorrection( - JetEta=jets.eta, - Rho=jets.rho, - JetPt=jets.pt_raw, - JetA=jets.area, - ).compute() - reso = JetResolution(**{name: evaluator[name] for name in jec_stack_names[4:5]}) - check_resos = reso.getResolution( - JetEta=jets.eta, - Rho=jets.rho, - JetPt=jets.pt_raw, - ).compute() - resosf = JetResolutionScaleFactor( - **{name: evaluator[name] for name in jec_stack_names[5:6]} - ) + check_corrs = corrector.getCorrection( + JetEta=jets.eta, + Rho=jets.rho, + JetPt=jets.pt_raw, + JetA=jets.area, + ).compute() + reso = JetResolution(**{name: evaluator[name] for name in jec_stack_names[4:5]}) + check_resos = reso.getResolution( + JetEta=jets.eta, + Rho=jets.rho, + JetPt=jets.pt_raw, + ).compute() + resosf = JetResolutionScaleFactor( + **{name: evaluator[name] for name in jec_stack_names[5:6]} + ) - print(dak.necessary_columns(jets.eta)) - print( - dak.necessary_columns( - resosf.getScaleFactor( - JetEta=jets.eta, + print(dak.necessary_columns(jets.eta)) + print( + dak.necessary_columns( + resosf.getScaleFactor( + JetEta=jets.eta, + ) ) ) - ) - check_resosfs = resosf.getScaleFactor( - JetEta=events.Jet.eta, - ).compute() + check_resosfs = resosf.getScaleFactor( + JetEta=events.Jet.eta, + ).compute() - # Filter out the non-deterministic (no gen pt) jets - def smear_factor(jetPt, pt_gen, jersf): - return ( - ak.full_like(jetPt, 1.0) - + (jersf[:, 0] - ak.full_like(jetPt, 1.0)) * (jetPt - pt_gen) / jetPt - ) + # Filter out the non-deterministic (no gen pt) jets + def smear_factor(jetPt, pt_gen, jersf): + return ( + ak.full_like(jetPt, 1.0) + + (jersf[:, 0] - ak.full_like(jetPt, 1.0)) * (jetPt - pt_gen) / jetPt + ) - test_gen_pt = ak.concatenate( - [ - dak.fill_none(events.Jet.matched_gen.pt, 0).compute()[0, :-2], - dak.fill_none(events.Jet.matched_gen.pt, 0).compute()[-1, :-1], - ] - ) - test_raw_pt = ak.concatenate( - [ - ((1 - events.Jet.rawFactor) * events.Jet.pt).compute()[0, :-2], - ((1 - events.Jet.rawFactor) * events.Jet.pt).compute()[-1, :-1], - ] - ) - test_pt = ak.concatenate( - [corrected_jets.pt.compute()[0, :-2], corrected_jets.pt.compute()[-1, :-1]] - ) - test_eta = ak.concatenate( - [events.Jet.eta.compute()[0, :-2], events.Jet.eta.compute()[-1, :-1]] - ) - test_jer = ak.concatenate([check_resos[0, :-2], check_resos[-1, :-1]]) - test_jer_sf = ak.concatenate( - [ - check_resosfs[0, :-2], - check_resosfs[-1, :-1], - ] - ) - test_jec = ak.concatenate([check_corrs[0, :-2], check_corrs[-1, :-1]]) - test_corrected_pt = ak.concatenate( - [corrected_jets.pt.compute()[0, :-2], corrected_jets.pt.compute()[-1, :-1]] - ) - test_corr_pt = test_raw_pt * test_jec - test_pt_smear_corr = test_corr_pt * smear_factor( - test_corr_pt, test_gen_pt, test_jer_sf - ) + test_gen_pt = ak.concatenate( + [ + dak.fill_none(events.Jet.matched_gen.pt, 0).compute()[0, :-2], + dak.fill_none(events.Jet.matched_gen.pt, 0).compute()[-1, :-1], + ] + ) + test_raw_pt = ak.concatenate( + [ + ((1 - events.Jet.rawFactor) * events.Jet.pt).compute()[0, :-2], + ((1 - events.Jet.rawFactor) * events.Jet.pt).compute()[-1, :-1], + ] + ) + test_pt = ak.concatenate( + [corrected_jets.pt.compute()[0, :-2], corrected_jets.pt.compute()[-1, :-1]] + ) + test_eta = ak.concatenate( + [events.Jet.eta.compute()[0, :-2], events.Jet.eta.compute()[-1, :-1]] + ) + test_jer = ak.concatenate([check_resos[0, :-2], check_resos[-1, :-1]]) + test_jer_sf = ak.concatenate( + [ + check_resosfs[0, :-2], + check_resosfs[-1, :-1], + ] + ) + test_jec = ak.concatenate([check_corrs[0, :-2], check_corrs[-1, :-1]]) + test_corrected_pt = ak.concatenate( + [corrected_jets.pt.compute()[0, :-2], corrected_jets.pt.compute()[-1, :-1]] + ) + test_corr_pt = test_raw_pt * test_jec + test_pt_smear_corr = test_corr_pt * smear_factor( + test_corr_pt, test_gen_pt, test_jer_sf + ) - # Print the results of the "by-hand" calculations and confirm that the values match the expected values - print("\nConfirm the CorrectedJetsFactory values:") - print("Jet pt (gen)", test_gen_pt.tolist()) - print("Jet pt (raw)", test_raw_pt.tolist()) - print("Jet pt (nano):", test_pt.tolist()) - print("Jet eta:", test_eta.tolist()) - print("Jet energy resolution:", test_jer.tolist()) - print("Jet energy resolution sf:", test_jer_sf.tolist()) - print("Jet energy correction:", test_jec.tolist()) - print("Corrected jet pt (ref)", test_corr_pt.tolist()) - print("Corrected & smeared jet pt (ref):", test_pt_smear_corr.tolist()) - print("Corrected & smeared jet pt:", test_corrected_pt.tolist(), "\n") - assert ak.all(np.abs(test_pt_smear_corr - test_corrected_pt) < 1e-6) - - name_map["METpt"] = "pt" - name_map["METphi"] = "phi" - name_map["JetPhi"] = "phi" - name_map["UnClusteredEnergyDeltaX"] = "MetUnclustEnUpDeltaX" - name_map["UnClusteredEnergyDeltaY"] = "MetUnclustEnUpDeltaY" - - tic = time.time() - met_factory = CorrectedMETFactory(name_map) - toc = time.time() - - print("setup corrected MET time =", toc - tic) - - met = events.MET - tic = time.time() - # prof = pyinstrument.Profiler() - # prof.start() - corrected_met = met_factory.build(met, corrected_jets) - # prof.stop() - toc = time.time() - - # print(prof.output_text(unicode=True, color=True, show_all=True)) - - print("corrected_met build time =", toc - tic) - - print(corrected_met.dask) - - print(corrected_met.pt_orig.compute()) - print(corrected_met.pt.compute()) - tic = time.time() - prof = pyinstrument.Profiler() - prof.start() - - tocompute = { - unc: {"up": corrected_met[unc].up.pt, "down": corrected_met[unc].down.pt} - for unc in (jet_factory.uncertainties() + met_factory.uncertainties()) - } - computed_uncs = dask.compute(tocompute)[0] - - for unc in jet_factory.uncertainties() + met_factory.uncertainties(): - print(unc) - print(computed_uncs[unc]["up"]) - print(computed_uncs[unc]["down"]) - prof.stop() - toc = time.time() - - print("build all met variations =", toc - tic) - - print(prof.output_text(unicode=True, color=True, show_all=True)) + # Print the results of the "by-hand" calculations and confirm that the values match the expected values + print("\nConfirm the CorrectedJetsFactory values:") + print("Jet pt (gen)", test_gen_pt.tolist()) + print("Jet pt (raw)", test_raw_pt.tolist()) + print("Jet pt (nano):", test_pt.tolist()) + print("Jet eta:", test_eta.tolist()) + print("Jet energy resolution:", test_jer.tolist()) + print("Jet energy resolution sf:", test_jer_sf.tolist()) + print("Jet energy correction:", test_jec.tolist()) + print("Corrected jet pt (ref)", test_corr_pt.tolist()) + print("Corrected & smeared jet pt (ref):", test_pt_smear_corr.tolist()) + print("Corrected & smeared jet pt:", test_corrected_pt.tolist(), "\n") + assert ak.all(np.abs(test_pt_smear_corr - test_corrected_pt) < 1e-6) + + name_map["METpt"] = "pt" + name_map["METphi"] = "phi" + name_map["JetPhi"] = "phi" + name_map["UnClusteredEnergyDeltaX"] = "MetUnclustEnUpDeltaX" + name_map["UnClusteredEnergyDeltaY"] = "MetUnclustEnUpDeltaY" + + tic = time.time() + met_factory = CorrectedMETFactory(name_map) + toc = time.time() + + print("setup corrected MET time =", toc - tic) + + met = events.MET + tic = time.time() + # prof = pyinstrument.Profiler() + # prof.start() + corrected_met = met_factory.build(met, corrected_jets) + # prof.stop() + toc = time.time() + + # print(prof.output_text(unicode=True, color=True, show_all=True)) + + print("corrected_met build time =", toc - tic) + + print(corrected_met.dask) + + print(corrected_met.pt_orig.compute()) + print(corrected_met.pt.compute()) + tic = time.time() + prof = pyinstrument.Profiler() + prof.start() + + tocompute = { + unc: {"up": corrected_met[unc].up.pt, "down": corrected_met[unc].down.pt} + for unc in (jet_factory.uncertainties() + met_factory.uncertainties()) + } + computed_uncs = dask.compute(tocompute)[0] + + for unc in jet_factory.uncertainties() + met_factory.uncertainties(): + print(unc) + print(computed_uncs[unc]["up"]) + print(computed_uncs[unc]["down"]) + prof.stop() + toc = time.time() + + print("build all met variations =", toc - tic) + + print(prof.output_text(unicode=True, color=True, show_all=True)) diff --git a/tests/test_lookup_tools.py b/tests/test_lookup_tools.py index 85d7d5fac..f4d5a3b30 100644 --- a/tests/test_lookup_tools.py +++ b/tests/test_lookup_tools.py @@ -1,6 +1,7 @@ import os import awkward as ak +import dask import dask_awkward as dak import pytest from dummy_distributions import dummy_jagged_eta_pt @@ -142,97 +143,103 @@ def test_evaluate_noimpl(): pass -def test_correctionlib(): - extractor = lookup_tools.extractor() - extractor.add_weight_sets(["* * tests/samples/testSF2d.corr.json.gz"]) +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_correctionlib(optimization_enabled): + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + extractor = lookup_tools.extractor() + extractor.add_weight_sets(["* * tests/samples/testSF2d.corr.json.gz"]) - extractor.finalize() + extractor.finalize() - evaluator = extractor.make_evaluator() + evaluator = extractor.make_evaluator() - counts, test_eta, test_pt = dummy_jagged_eta_pt() + counts, test_eta, test_pt = dummy_jagged_eta_pt() - test_out = evaluator["scalefactors_Tight_Electron"](test_eta, test_pt) + test_out = evaluator["scalefactors_Tight_Electron"](test_eta, test_pt) - # print it - print(evaluator["scalefactors_Tight_Electron"]) + # print it + print(evaluator["scalefactors_Tight_Electron"]) - # test structured eval - test_eta_jagged = ak.unflatten(test_eta, counts) - test_pt_jagged = ak.unflatten(test_pt, counts) - test_out_jagged = evaluator["scalefactors_Tight_Electron"]( - test_eta_jagged, test_pt_jagged - ) + # test structured eval + test_eta_jagged = ak.unflatten(test_eta, counts) + test_pt_jagged = ak.unflatten(test_pt, counts) + test_out_jagged = evaluator["scalefactors_Tight_Electron"]( + test_eta_jagged, test_pt_jagged + ) - # test lazy eval - test_eta_dak = dak.from_awkward(test_eta_jagged, 1) - test_pt_dak = dak.from_awkward(test_pt_jagged, 1) - test_out_dak = evaluator["scalefactors_Tight_Electron"]( - test_eta_dak, test_pt_dak, dask_label="scalefactors_Tight_Electron" - ) + # test lazy eval + test_eta_dak = dak.from_awkward(test_eta_jagged, 1) + test_pt_dak = dak.from_awkward(test_pt_jagged, 1) + test_out_dak = evaluator["scalefactors_Tight_Electron"]( + test_eta_dak, test_pt_dak, dask_label="scalefactors_Tight_Electron" + ) - print(test_out_dak) + print(test_out_dak) - assert ak.all(ak.num(test_out_jagged) == counts) - assert ak.all(ak.flatten(test_out_jagged) == test_out) - assert ak.all(ak.flatten(test_out_dak.compute()) == test_out) + assert ak.all(ak.num(test_out_jagged) == counts) + assert ak.all(ak.flatten(test_out_jagged) == test_out) + assert ak.all(ak.flatten(test_out_dak.compute()) == test_out) - print(test_out) + print(test_out) - diff = np.abs(test_out - _testSF2d_expected_output) - print("Max diff: %.16f" % diff.max()) - print("Median diff: %.16f" % np.median(diff)) - print( - "Diff over threshold rate: %.1f %%" % (100 * (diff >= 1.0e-8).sum() / diff.size) - ) - assert (diff < 1.0e-8).all() + diff = np.abs(test_out - _testSF2d_expected_output) + print("Max diff: %.16f" % diff.max()) + print("Median diff: %.16f" % np.median(diff)) + print( + "Diff over threshold rate: %.1f %%" + % (100 * (diff >= 1.0e-8).sum() / diff.size) + ) + assert (diff < 1.0e-8).all() -def test_root_scalefactors(): - extractor = lookup_tools.extractor() - extractor.add_weight_sets( - ["testSF2d scalefactors_Tight_Electron tests/samples/testSF2d.histo.root"] - ) +@pytest.mark.parametrize("optimization_enabled", [True, False]) +def test_root_scalefactors(optimization_enabled): + with dask.config.set({"awkward.optimization.enabled": optimization_enabled}): + extractor = lookup_tools.extractor() + extractor.add_weight_sets( + ["testSF2d scalefactors_Tight_Electron tests/samples/testSF2d.histo.root"] + ) - extractor.finalize(reduce_list=["testSF2d"]) + extractor.finalize(reduce_list=["testSF2d"]) - evaluator = extractor.make_evaluator() + evaluator = extractor.make_evaluator() - counts, test_eta, test_pt = dummy_jagged_eta_pt() + counts, test_eta, test_pt = dummy_jagged_eta_pt() - # test flat eval - test_out = evaluator["testSF2d"](test_eta, test_pt) + # test flat eval + test_out = evaluator["testSF2d"](test_eta, test_pt) - # print it - print(evaluator["testSF2d"]) + # print it + print(evaluator["testSF2d"]) - # test structured eval - test_eta_jagged = ak.unflatten(test_eta, counts) - test_pt_jagged = ak.unflatten(test_pt, counts) - test_out_jagged = evaluator["testSF2d"](test_eta_jagged, test_pt_jagged) + # test structured eval + test_eta_jagged = ak.unflatten(test_eta, counts) + test_pt_jagged = ak.unflatten(test_pt, counts) + test_out_jagged = evaluator["testSF2d"](test_eta_jagged, test_pt_jagged) - # test lazy eval - test_eta_dak = dak.from_awkward(test_eta_jagged, 1) - test_pt_dak = dak.from_awkward(test_pt_jagged, 1) - test_out_dak = evaluator["testSF2d"]( - test_eta_dak, test_pt_dak, dask_label="testSF2d" - ) + # test lazy eval + test_eta_dak = dak.from_awkward(test_eta_jagged, 1) + test_pt_dak = dak.from_awkward(test_pt_jagged, 1) + test_out_dak = evaluator["testSF2d"]( + test_eta_dak, test_pt_dak, dask_label="testSF2d" + ) - print(test_out_dak) + print(test_out_dak) - assert ak.all(ak.num(test_out_jagged) == counts) - assert ak.all(ak.flatten(test_out_jagged) == test_out) - assert ak.all(ak.flatten(test_out_dak.compute()) == test_out) + assert ak.all(ak.num(test_out_jagged) == counts) + assert ak.all(ak.flatten(test_out_jagged) == test_out) + assert ak.all(ak.flatten(test_out_dak.compute()) == test_out) - print(test_out) + print(test_out) - diff = np.abs(test_out - _testSF2d_expected_output) - print("Max diff: %.16f" % diff.max()) - print("Median diff: %.16f" % np.median(diff)) - print( - "Diff over threshold rate: %.1f %%" % (100 * (diff >= 1.0e-8).sum() / diff.size) - ) - assert (diff < 1.0e-8).all() + diff = np.abs(test_out - _testSF2d_expected_output) + print("Max diff: %.16f" % diff.max()) + print("Median diff: %.16f" % np.median(diff)) + print( + "Diff over threshold rate: %.1f %%" + % (100 * (diff >= 1.0e-8).sum() / diff.size) + ) + assert (diff < 1.0e-8).all() def test_histo_json_scalefactors():