\n"
+ ],
+ "text/plain": [
+ "\u001b[1mCorrectionSet\u001b[0m (\u001b[3mschema v2\u001b[0m) \n",
+ "my custom corrections \n",
+ "📂 \n",
+ "└── 📈 \u001b[1mgen2_to_gen1\u001b[0m (v0) \n",
+ " Reweights gen2 to agree with gen1 \n",
+ " Node counts: \u001b[1mMultiBinning\u001b[0m: 1 \n",
+ " ╭──────────── ▶ input ─────────────╮ ╭──────────── ▶ input ────────────╮ \n",
+ " │ \u001b[1mpt\u001b[0m (real) │ │ \u001b[1meta\u001b[0m (real) │ \n",
+ " │ pt │ │ eta │ \n",
+ " │ Range: [0.0, 100.0), overflow ok │ │ Range: [-3.0, 3.0), overflow ok │ \n",
+ " ╰──────────────────────────────────╯ ╰─────────────────────────────────╯ \n",
+ " ╭─── ◀ output ───╮ \n",
+ " │ \u001b[1mout\u001b[0m (real) │ \n",
+ " │ \u001b[3mNo description\u001b[0m │ \n",
+ " ╰────────────────╯ \n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import correctionlib, rich\n",
+ "import correctionlib.convert\n",
+ "\n",
+ "# without a name, the resulting object will fail validation\n",
+ "sfhist.name = \"gen2_to_gen1\"\n",
+ "sfhist.label = \"out\"\n",
+ "clibcorr = correctionlib.convert.from_histogram(sfhist)\n",
+ "clibcorr.description = \"Reweights gen2 to agree with gen1\"\n",
+ "# set overflow bins behavior (default is to raise an error when out of bounds)\n",
+ "clibcorr.data.flow = \"clamp\"\n",
+ "\n",
+ "cset = correctionlib.schemav2.CorrectionSet(\n",
+ " schema_version=2,\n",
+ " description=\"my custom corrections\",\n",
+ " corrections=[clibcorr],\n",
+ ")\n",
+ "rich.print(cset)\n",
+ "\n",
+ "with open(\"data/mycorrections.json\", \"w\") as fout:\n",
+ " fout.write(cset.json(exclude_unset=True))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can now use this new correction in a similar way to the original `corr()` object:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([1.55691586, 1.36319225, 1.36319225, ..., 1.55691586, 0.64304079,\n",
+ " 1.02863368])"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ceval = cset.to_evaluator()\n",
+ "\n",
+ "ceval[\"gen2_to_gen1\"].evaluate(ptvals, etavals)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "At the time of writing, correctionlib does not support jagged arrays. But we can work around this using awkward utilities `flatten` and `unflatten`, as shown below"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def myJetSF(jets):\n",
+ " j, nj = ak.flatten(jets), ak.num(jets)\n",
+ " sf = ceval[\"gen2_to_gen1\"].evaluate(np.array(j.pt), np.array(j.eta))\n",
+ " return ak.unflatten(sf, nj)\n",
+ "\n",
+ "myJetSF(events.Jet)"
+ ]
+ }
+ ],
+ "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.9.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/_sources/notebooks/histograms.ipynb.txt b/_sources/notebooks/histograms.ipynb.txt
new file mode 100644
index 000000000..d14ddbe6d
--- /dev/null
+++ b/_sources/notebooks/histograms.ipynb.txt
@@ -0,0 +1,1190 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Coffea Histograms (deprecated)\n",
+ "\n",
+ "_This feature is deprecated in favor of [hist](https://hist.readthedocs.io/en/latest/) and [mplhep](https://mplhep.readthedocs.io/en/latest/). A migration guide can be found in discussion [CoffeaTeam/coffea#705](https://github.com/CoffeaTeam/coffea/discussions/705)_\n",
+ "\n",
+ "This is a rendered copy of [histograms.ipynb](https://github.com/CoffeaTeam/coffea/blob/master/binder/histograms.ipynb). You can optionally run it interactively on [binder at this link](https://mybinder.org/v2/gh/coffeateam/coffea/master?filepath=binder%2Fhistograms.ipynb)\n",
+ "\n",
+ "In scientific python, histograms seem to be considered as a plot style, on equal footing with, e.g. scatter plots.\n",
+ "It may well be that HEP is the only place where users need to plot *pre-binned* data, and thus must use histograms as persistent objects representing reduced data. In Coffea, the [hist](https://coffeateam.github.io/coffea/modules/coffea.hist.html) subpackage provides a persistable mergable histogram object. This notebook will discuss a few ways that such objects can be manipulated.\n",
+ "\n",
+ "A histogram object roughly goes through three stages in its life:\n",
+ "\n",
+ " - Filling\n",
+ " - Transformation (projection, rebinning, integrating)\n",
+ " - Plotting\n",
+ "\n",
+ "We'll go over examples of each stage in this notebook, and conclude with some styling examples."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Filling\n",
+ "Let's start with filling. We'll use a random distribution [near and dear](https://en.wikipedia.org/wiki/ARGUS_distribution) to of b and c factory physicists, and have the numpy builtin [histogram function](https://numpy.org/doc/stable/reference/generated/numpy.histogram.html) do the work for us:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(array([ 16, 47, 66, 95, 125, 113, 137, 167, 143, 91]), array([0.002894 , 0.10257766, 0.20226132, 0.30194498, 0.40162865,\n",
+ " 0.50131231, 0.60099597, 0.70067963, 0.80036329, 0.90004695,\n",
+ " 0.99973061]))\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from scipy.stats import argus\n",
+ "\n",
+ "vals = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "hist = np.histogram(vals)\n",
+ "print(hist)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So we're done, right?\n",
+ "Probably not: we have more than 1000 events, and probably need to use some map-reduce paradigm to fill the histogram because we can't keep all 1 billion `vals` in memory. So we need two things: a binning, so that all histograms that were independently created can be added, and the ability to add two histograms."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "def add_histos(h1, h2):\n",
+ " h1sumw, h1binning = h1\n",
+ " h2sumw, h2binning = h2\n",
+ " if h1binning.shape == h2binning.shape and np.all(h1binning==h2binning):\n",
+ " return h1sumw+h2sumw, h1binning\n",
+ " else:\n",
+ " raise ValueError(\"The histograms have inconsistent binning\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(array([ 2, 6, 6, 6, 12, 18, 20, 16, 14, 28, 22, 26, 28, 42, 18, 34, 28,\n",
+ " 48, 40, 42, 42, 60, 46, 62, 46, 52, 46, 38, 52, 54, 50, 58, 72, 54,\n",
+ " 50, 74, 80, 58, 70, 70, 42, 64, 68, 52, 60, 32, 44, 30, 18]), array([0. , 0.02040816, 0.04081633, 0.06122449, 0.08163265,\n",
+ " 0.10204082, 0.12244898, 0.14285714, 0.16326531, 0.18367347,\n",
+ " 0.20408163, 0.2244898 , 0.24489796, 0.26530612, 0.28571429,\n",
+ " 0.30612245, 0.32653061, 0.34693878, 0.36734694, 0.3877551 ,\n",
+ " 0.40816327, 0.42857143, 0.44897959, 0.46938776, 0.48979592,\n",
+ " 0.51020408, 0.53061224, 0.55102041, 0.57142857, 0.59183673,\n",
+ " 0.6122449 , 0.63265306, 0.65306122, 0.67346939, 0.69387755,\n",
+ " 0.71428571, 0.73469388, 0.75510204, 0.7755102 , 0.79591837,\n",
+ " 0.81632653, 0.83673469, 0.85714286, 0.87755102, 0.89795918,\n",
+ " 0.91836735, 0.93877551, 0.95918367, 0.97959184, 1. ]))\n"
+ ]
+ }
+ ],
+ "source": [
+ "vals2 = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "hist1 = np.histogram(vals, bins=binning)\n",
+ "hist2 = np.histogram(vals, bins=binning)\n",
+ "\n",
+ "hist = add_histos(hist1, hist2)\n",
+ "print(hist)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So now we have everything we need to make our own equivalent to ROOT TH1, from a filling perspective:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class myTH1:\n",
+ " def __init__(self, binning):\n",
+ " self._binning = binning\n",
+ " self._sumw = np.zeros(binning.size - 1)\n",
+ " \n",
+ " def fill(self, values, weights=None):\n",
+ " sumw, _ = np.histogram(values, bins=self._binning, weights=weights)\n",
+ " self._sumw += sumw\n",
+ " \n",
+ " def __add__(self, other):\n",
+ " if not isinstance(other, myTH1):\n",
+ " raise ValueError\n",
+ " if not np.array_equal(other._binning, self._binning):\n",
+ " raise ValueError(\"The histograms have inconsistent binning\")\n",
+ " out = myTH1(self._binning)\n",
+ " out._sumw = self._sumw + other._sumw\n",
+ " return out"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[ 3. 3. 7. 5. 9. 18. 16. 17. 15. 24. 26. 27. 34. 37. 23. 38. 29. 46.\n",
+ " 40. 40. 36. 50. 47. 49. 57. 49. 50. 44. 59. 69. 55. 52. 68. 54. 48. 72.\n",
+ " 72. 64. 55. 74. 57. 64. 63. 55. 49. 39. 39. 31. 22.]\n"
+ ]
+ }
+ ],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "h1 = myTH1(binning)\n",
+ "h1.fill(vals)\n",
+ "\n",
+ "h2 = myTH1(binning)\n",
+ "h2.fill(vals2)\n",
+ "\n",
+ "h = h1 + h2\n",
+ "print(h._sumw)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Homework: add `sumw2` support.\n",
+ "\n",
+ "Of course, we might want multidimensional histograms. There is `np.histogramdd`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xyz = np.random.multivariate_normal(mean=[1, 3, 7], cov=np.eye(3), size=10000)\n",
+ "\n",
+ "xbins = np.linspace(-10, 10, 20)\n",
+ "ybins = np.linspace(-10, 10, 20)\n",
+ "zbins = np.linspace(-10, 10, 20)\n",
+ "hnumpy = np.histogramdd(xyz, bins=(xbins, ybins, zbins))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "but we are becoming challenged by book-keeping of the variables.\n",
+ "The [histogram class](https://coffeateam.github.io/coffea/api/coffea.hist.Hist.html#coffea.hist.Hist) in Coffea is designed to simplify this operation, and the eventual successor (for filling purposes) [boost-histogram](https://github.com/scikit-hep/boost-histogram#usage) has similar syntax.\n",
+ "\n",
+ "In the constructor you specify each axis, either as a numeric `Bin` axis or a categorical `Cat` axis. Each axis constructor takes arguments similar to ROOT TH1 constructors. One can pass an array to the `Bin` axis for non-uniform binning. Then the fill call is as simple as passing the respective arrays to `histo.fill`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "import coffea.hist as hist\n",
+ "\n",
+ "histo = hist.Hist(\"Counts\",\n",
+ " hist.Cat(\"sample\", \"sample name\"),\n",
+ " hist.Bin(\"x\", \"x value\", 20, -10, 10),\n",
+ " hist.Bin(\"y\", \"y value\", 20, -10, 10),\n",
+ " hist.Bin(\"z\", \"z value\", 20, -10, 10),\n",
+ " )\n",
+ "\n",
+ "histo.fill(sample=\"sample 1\", x=xyz[:,0], y=xyz[:,1], z=xyz[:,2])\n",
+ "\n",
+ "# suppose we have another sample of xyz values\n",
+ "xyz_sample2 = np.random.multivariate_normal(mean=[1, 3, 7], cov=np.eye(3), size=10000)\n",
+ "\n",
+ "# additionally, lets assume entries in sample 2 have some non-uniform weight equal to atan(distance from origin)\n",
+ "weight = np.arctan(np.sqrt(np.power(xyz_sample2, 2).sum(axis=1)))\n",
+ "\n",
+ "# weight is a reserved keyword in Hist, and can be added to any fill() call\n",
+ "histo.fill(sample=\"sample 2\", x=xyz_sample2[:,0], y=xyz_sample2[:,1], z=xyz_sample2[:,2], weight=weight)\n",
+ "\n",
+ "print(histo)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# For more details, look at:\n",
+ "# help(hist.Hist)\n",
+ "# help(hist.Bin)\n",
+ "# help(hist.Cat)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Transformation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here are a few examples of transformations on multidimensional histograms in Coffea. For each, the docstring (`help(function)` or shift+tab in Jupyter) provides useful info."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# sum all x bins within nominal range (-10, 10)\n",
+ "histo.sum(\"x\", overflow='none')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There is some analog to fancy array slicing for histogram objects, which is supported (with reasonable consistency) in Coffea, where the slice boundaries are physical axis values, rather than bin indices. All values outside the slice range are merged into overflow bins.\n",
+ "\n",
+ "For a lengthy discussion on possible slicing syntax for the future, see [boost-histogram#35](https://github.com/scikit-hep/boost-histogram/issues/35)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "sliced = histo[:,0:,4:,0:]\n",
+ "display(sliced)\n",
+ "display(sliced.identifiers(\"y\", overflow='all'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# integrate y bins from -2 to +10\n",
+ "histo.integrate(\"y\", slice(0, 10))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# rebin z axis by providing a new axis definition\n",
+ "histo.rebin(\"z\", hist.Bin(\"znew\", \"rebinned z value\", [-10, -6, 6, 10]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# merge categorical axes\n",
+ "mapping = {\n",
+ " 'all samples': ['sample 1', 'sample 2'],\n",
+ " 'just sample 1': ['sample 1'],\n",
+ "}\n",
+ "histo.group(\"sample\", hist.Cat(\"cat\", \"new category\"), mapping)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# scale entire histogram by 3 (in-place)\n",
+ "histo.scale(3.)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# scale samples by different values (also in-place)\n",
+ "scales = {\n",
+ " 'sample 1': 1.2,\n",
+ " 'sample 2': 0.2,\n",
+ "}\n",
+ "histo.scale(scales, axis='sample')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# useful debugging tool: print bins, aka 'identifiers'\n",
+ "display(histo.identifiers('sample'))\n",
+ "display(histo.identifiers('x'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{('sample 1',): array([0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00,\n",
+ " 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00,\n",
+ " 0.00000e+00, 0.00000e+00, 3.60000e+00, 5.40000e+01, 7.23600e+02,\n",
+ " 4.83120e+03, 1.24164e+04, 1.24344e+04, 4.68720e+03, 8.13600e+02]),\n",
+ " ('sample 2',): array([0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 7.67352230e-01, 1.23975181e+01, 1.91084135e+02, 1.16950599e+03,\n",
+ " 3.00209800e+03, 2.88614286e+03, 1.20687727e+03, 1.68492970e+02])}"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# bin contents are accessed using values\n",
+ "histo.sum('x', 'y').values(sumw2=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/lagray/miniconda3/envs/coffea-work/lib/python3.7/site-packages/uproot3/__init__.py:138: FutureWarning: Consider switching from 'uproot3' to 'uproot', since the new interface became the default in 2020.\n",
+ "\n",
+ " pip install -U uproot\n",
+ "\n",
+ "In Python:\n",
+ "\n",
+ " >>> import uproot\n",
+ " >>> with uproot.open(...) as file:\n",
+ " ...\n",
+ "\n",
+ " FutureWarning\n"
+ ]
+ }
+ ],
+ "source": [
+ "# data can be exported to ROOT via uproot3 (soon uproot v4 as well), but only 1D\n",
+ "import uproot3\n",
+ "import os\n",
+ "\n",
+ "if os.path.exists(\"output.root\"):\n",
+ " os.remove(\"output.root\")\n",
+ "\n",
+ "outputfile = uproot3.create(\"output.root\")\n",
+ "h = histo.sum('x', 'y')\n",
+ "for sample in h.identifiers('sample'):\n",
+ " outputfile[sample.name] = hist.export1d(h.integrate('sample', sample))\n",
+ "outputfile.close()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Plotting\n",
+ "The most integrated plotting utility in the scientific python ecosystem, by far, is [matplotlib](https://matplotlib.org/). However, as we will see, it is not tailored to HEP needs.\n",
+ "\n",
+ "Let's start by looking at basic mpl histogramming."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib inline\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "vals = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "# notice the semicolon, which prevents display of the return values\n",
+ "plt.hist(vals);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Suppose we want to plot pre-binned data, for example from our earlier `np.histogram` usage. Here we start running into the edge of typical mpl usage. As mentioned before, apparently HEP is the only regular user of pre-binned histograms."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "h1vals, h1bins = np.histogram(vals, bins=binning)\n",
+ "plt.step(x=h1bins[:-1], y=h1vals, where='post');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To facilitate these operations, there is a package called [mplhep](https://github.com/scikit-hep/mplhep). This package is available standlaone, but it is also used internally by the `coffea.hist` subpackage to provide several convenience functions to aid in plotting `Hist` objects:\n",
+ "\n",
+ " * [plot1d](https://coffeateam.github.io/coffea/api/coffea.hist.plot1d.html#coffea.hist.plot1d): Create a 1D plot from a 1D or 2D Hist object\n",
+ "\n",
+ " * [plotratio](https://coffeateam.github.io/coffea/api/coffea.hist.plotratio.html#coffea.hist.plotratio): Create a ratio plot, dividing two compatible histograms\n",
+ "\n",
+ " * [plot2d](https://coffeateam.github.io/coffea/api/coffea.hist.plot2d.html#coffea.hist.plot2d): Create a 2D plot from a 2D Hist object\n",
+ "\n",
+ " * [plotgrid](https://coffeateam.github.io/coffea/api/coffea.hist.plotgrid.html#coffea.hist.plotgrid): Create a grid of plots, enumerating identifiers on up to 3 axes\n",
+ " \n",
+ "Below are some simple examples of using each function on our `histo` object."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZwAAAEGCAYAAABRvCMcAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAh6ElEQVR4nO3de3xU1b338c+Pa+oFAUkBCR5AQQkXKYZb0VNOqYCiYi3eqpWiFVultbYi+OjLW/Vp9ZxHqz5V5CiKfXlBqT5QpSIottYCGiyCXCxRUYOgFBSKHEH09/yxV+IQZpJJMrNnknzfr9e8smfttff+zc5kfll7r1nL3B0REZFsa5brAEREpGlQwhERkVgo4YiISCyUcEREJBZKOCIiEosWuQ4gbh06dPBu3brlOgwRkQZl+fLl/3T3wvrso8klnG7dulFaWprrMEREGhQze7e++9AlNRERiYUSjoiIxEIJR0REYtHk7uEk8/nnn1NeXs5nn32W61AapIKCAoqKimjZsmWuQxGRPKaEA5SXl3PwwQfTrVs3zCzX4TQo7s7WrVspLy+ne/fuuQ5HRPKYLqkBn332GYceeqiSTR2YGYceeqhahyJSIyWcQMmm7nTuRCQdSjgiIhILJZw8M2LECH0xVUQaJSUcEZE6OOveJZx175Kc76MhUcKpwaeffsrYsWM55phj6Nu3L7NnzwbgxhtvZNCgQfTt25dJkyZRMXPqiBEjuPzyyykpKaF37968+uqrnH766fTs2ZNrrrkGgA0bNnD00Udz7rnn0rt3b8aPH8+uXbv2O/Zzzz3HsGHDGDhwIGeccQY7d+7cr86IESOYOnUqgwcPplevXrz00kuVxzj++OMZOHAgAwcO5G9/+xsAL774It/61rcYN24cPXr0YNq0aTz88MMMHjyYfv368dZbbwGwZcsWvve97zFo0CAGDRrEyy+/nPmTKyJNirpF1+DZZ5/lsMMO45lnngFg+/btAEyePJlrr70WgB/84Ac8/fTTnHLKKQC0atWK0tJS7rjjDsaNG8fy5ctp3749RxxxBJdffjkAb775Jvfffz/Dhw/nggsu4O677+aKK66oPO4///lPbrrpJhYtWsSBBx7ILbfcwm233VZ5zER79+7llVdeYf78+dxwww0sWrSIr3/96yxcuJCCggLWr1/POeecU3mp7vXXX2ft2rW0b9+eHj168KMf/YhXXnmFO+64g7vuuovf/va3XHbZZVx++eUcd9xxvPfee4wePZq1a9dm70SLNDKPLHuPuSs2VltnzaYdANW2csYN6ML3hxye0dhyRQmnBv369eOXv/wlU6dO5eSTT+b4448HYPHixdx6663s2rWLbdu20adPn8qEc+qpp1Zu26dPHzp37gxAjx49eP/992nbti1du3Zl+PDhAJx33nnceeed+yScpUuXsmbNmso6e/bsYdiwYUljPP300wE49thj2bBhAxB9mXXy5MmsWLGC5s2b849//KOy/qBBgypjOuKIIxg1alRlvIsXLwZg0aJFrFmzpnKbHTt2sHPnTg466KC6nkqRBqWmhFFTslj2zjYAhnRvX+cYKo6hhNNE9OrVi9dee4358+dzzTXXMHLkSK688kouueQSSktL6dq1K9dff/0+30Np3bo1AM2aNatcrni+d+9eYP+uxFWfuzsnnHACjz76aI0xVhyjefPmlfu//fbb6dixI6+//jpffvklBQUF+9WvGmNifF9++SVLly7dZzuRpmTuio2s2bSD4s5t6rT9kO7ta2ydVCSr2Rcn/2eysd3fUcKpwQcffED79u0577zzaNu2Lffdd19lcunQoQM7d+5kzpw5jB8/vlb7fe+991iyZAnDhg3jkUce4bjjjttn/dChQ7n00kspKyvjyCOP5NNPP2Xjxo306tUrrf1v376doqIimjVrxqxZs/jiiy9qFd+oUaO46667mDJlCgArVqxgwIABtdqHSENX3LlNjckg1XrZnzoN1GDVqlUMHjyYAQMGcMMNN3DNNdfQtm1bLrroIvr27cvo0aMZNGhQrfd71FFH8bvf/Y7evXvz8ccf85Of/GSf9YWFhTz44IOcc8459O/fn2HDhrFu3bq093/JJZcwa9YsjjnmGNatW8eBBx5Yq/juvPNOSktL6d+/P8XFxUyfPr1W24uIVGUVvasyvmOzmcDJwEfu3jeU/SdwCrAHeAuY6O6fhHVXARcCXwA/c/cFoXwMcAfQHLjP3X8TyrsDjwGHAsuBH7j7npriKikp8arfc1m7di29e/eu70tO24YNGzj55JN54403YjtmtsV9DkWyLR9aMPkQQwUzW+7uJfXZRzZbOA8CY6qULQT6unt/4B/AVQBmVgycDfQJ29xtZs3NrDnwO+BEoBg4J9QFuAW43d2P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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Let's stack them, after defining some nice styling\n",
+ "stack_fill_opts = {\n",
+ " 'alpha': 0.8,\n",
+ " 'edgecolor':(0,0,0,.5)\n",
+ "}\n",
+ "stack_error_opts = {\n",
+ " 'label':'Stat. Unc.',\n",
+ " 'hatch':'///',\n",
+ " 'facecolor':'none',\n",
+ " 'edgecolor':(0,0,0,.5),\n",
+ " 'linewidth': 0\n",
+ "}\n",
+ "# maybe we want to compare different eta regions\n",
+ "# plotgrid accepts row and column axes, and creates a grid of 1d plots as appropriate\n",
+ "ax = hist.plotgrid(\n",
+ " lepton_kinematics,\n",
+ " row=\"eta\",\n",
+ " overlay=\"flavor\",\n",
+ " stack=True,\n",
+ " fill_opts=stack_fill_opts,\n",
+ " error_opts=stack_error_opts,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Here we create some pseudodata for the pt histogram so we can make a nice data/mc plot\n",
+ "pthist = lepton_kinematics.sum('eta')\n",
+ "bin_values = pthist.axis('pt').centers()\n",
+ "poisson_means = pthist.sum('flavor').values()[()]\n",
+ "values = np.repeat(bin_values, np.random.poisson(poisson_means))\n",
+ "pthist.fill(flavor='pseudodata', pt=values)\n",
+ "\n",
+ "# Set nicer labels, by accessing the string bins' label property\n",
+ "pthist.axis('flavor').index('electron').label = 'e Flavor'\n",
+ "pthist.axis('flavor').index('muon').label = r'$\\mu$ Flavor'\n",
+ "pthist.axis('flavor').index('pseudodata').label = r'Pseudodata from e/$\\mu$'\n",
+ "\n",
+ "# using regular expressions on flavor name to select just the data\n",
+ "# another method would be to fill a separate data histogram\n",
+ "import re\n",
+ "notdata = re.compile('(?!pseudodata)')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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Q25uLqtXr/a3P/zww7Rv377KAB5jxozhjDPOAGDXrl1MnDiR9u3b06xZMwYNGsQ777zjf+wJJ5zA+PHjmTRpEu3ateOYY44J6echyUVhKiktmEO3wQatRFdhYSHdu3enZcuW/mUrV64kPT2d3r17R+Q5SkpKmDhxIsuWLeOtt94iKyuL4cOH+/cezznnHLZv387rr7/uX6e4uJiXX36Z888/H4Crr76a5557jscee4yVK1fSr18/hg4dysaN/zsn/fTTT+OcY8mSJTz55JMRqV0Sm8JUUlawh24b4jnURFRYWMiAAQMqLFu5ciW9e/emSZMmNa63cOFCMjMz/bfAS5wqGzlyJCNHjqR79+4cdthhPP7446xfv55ly5YBsM8++3DaaafxzDPP+NeZO3cujRs35owzzqCkpIQHHniA2267jdNPP51evXrx4IMP0r59e2bMmOFf5+CDD+bOO++kZ8+e9OrVK9QfiSQRdUCSlFSfQ7cK0sRQWFjI2WefXWHZihUr6uxINHjwYB5++GH//drGJl63bh3XX389BQUFbNmyhb1797J3716+/vpr/2POP/98LrzwQkpLS2nRogXPPPMMI0eOpFmzZqxatYrdu3dXOHSblpbG0UcfzWeffeZfdsQRRwT7siVFaM9UUlI4h24VpLG3d+9ePv744yp7psuXL68zTFu0aEG3bt38t06dOtX42GHDhrFlyxYeeughCgoKWLlyJY0bN67QSej000+ncePGvPzyy2zevJl///vf/kO8tQkcA7byCFuS+rRnKikp2EO3U6ZMqbVdYmPt2rWUlpay//77+5d9/PHHrF27NmKXuGzdupU1a9Zw//33c+KJJwKePd/KM4o0bdqUc845h2eeeYYffviBDh06cMIJJwDQtWtX0tPTeffdd+natSvgmYVo6dKljBkzJiJ1SnJSmEpKCvbQ7dSpU2ttl9jwTTJw3333ceWVV1JUVMTEiRMBT+/ZSNhnn31o27YtjzzyCJ07d2bDhg1Mnjy52p6/559/PkOGDGH9+vWMHj2aRo08B/EyMjIYP34811xzDW3btuXggw/m7rvvZtOmTVx++eURqVOSk8JUUtJxx5/ItoAJgcvKdrF92zayWrfmqWdnV3l8MO37tduPJYvfjGrd0dJp/44xG0jB93z1UVhYyCmnnMK3335L37596dGjBzfccAPj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gwbxt7/9rdb1ysvLueWWWzjssMPYd999yczM5MUXX+Trr2N/Tvqwww7z/3///T2To2/evBnwfGk99thj/UEqqUdh2gDU9kGXl5fHYYcd5v8jrzz8X13rQ/J3xmkIEjlIfZo1a8Ypp5zCDTfcwHvvvcfFF1/M1KlTKSsrq3GdO+64gzvvvJPJkyfzxhtvUFhYyIgRI2pdJ1itWrUCYLt3zOhA27ZtIysrq8KywDl3fed09+7dG3YdkhwUpimurg+6b775huHDh3P//fcDFYf/C2b9ZDjHKcH16k6EXr2BevfuzZ49e/znUdPT06ucY3znnXcYPnw4F1xwAdnZ2XTt2pW1a9dWeEx16wWje/fuNGrUiA8//LDC8i+//JLt27dz6KGHBr2t/v37884770Qk5CUxKUxTXLDzhV588cWAZ/i/WAepznFGXyIH6datWznppJN4+umnWbVqFevXr+f5559n+vTpDBkyxL+H2KVLF5YsWcKGDRv44YcfAM95xzfeeIN33nmHNWvWMGHCBNavX1/ltS9btoyioiJ++OEH/95iz549a732tGXLllxyySVMnjyZl19+mfXr1/P2228zduxYBg0axHHHHRf0a7z88sspLi7m3HPP5YMPPuCLL75g1qxZFBYWAvDSSy/Rs2dPNmzYUJ8fnSQQhWmKi9YHpQZ9T35dunTh6quvrjEIw20PVmZmJoMGDeKee+7h+OOPp0+fPvzlL39hzJgx/gkTAG688Ua++eYbunbtSrt27QC47rrrOOqoo8jLy2Pw4MFkZGQwduzYCtufNGkS6enp9O7dm3bt2vnPp37++ef+UK7JPffcw29/+1uuvfZa+vTpw4UXXki/fv2YP3++/1BuMDp16sTbb79NWVkZJ554Iv379+fee++lcWNPH9Dt27fz+eefs3v37qC3KYnFnHPxriFmcnJy3PLly+NdRtzVFJRmhnOuziA9tHefoKZBU5DWbtKoPD4uXBHRba5evZpevXpFdJsiya62vwsz+9A5lxPuc+jSmBQXOOk2RG7i7dquk6zrOsp4XmcpIhINCtMU55t0G+reI4z3xNsiIslK50wbiESfeFtEJJkpTBuA+lyeonOcIiL1p8O8KS7YQemPyzuDnTt31NquIBURqZ72TFNcsIPSd+h0YIXh/6prFxGR6ilMU1ywg9LXt11ERP5HYZrigh2U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lPg7Ofkek56ezrvvvutfVl5eztKlS+ndu3dQ29i6dStr1qzhL3/5CyeffDK9evXil19+Yc+ePRVqS09Pr/C7Dna9UNX12iIl2N/Xyy+/XKEX79q1ayktLWX//ff3L/v4449Zu3at9kxFGrraDt3m5+ezdOlSSktLAU8vUV9HIN+5y8rr19azsz7S0tL8zxHL86Q1Wb9+PQ899BBnnHEGnTp14ssvv2TVqlWMHz+eli1bMmnSJCZNmoRzjsGDB1NcXOzv+XnppZcCcNJJJzFx4kTmzZvHoYceykMPPcQ333zj/7lnZmZy8cUXc80119CuXTv2339/brzxxgqBlpGRwfjx47nmmmto27YtBx98MHfffTebNm3i8ssvD2ob++yzD23btuWRRx6hc+fObNiwgcmTJ/s72fh06dKFZcuWUVRURGZmJm3atAlqvVDV9doiJZjf15YtW3j//ff517/+5V+vsLAQgPvuu48rr7ySoqIi//Wnu3btilh9oVKYisRJWdmuWs+B/uc///EHqU9gR6BYdEZKFC1atGDt2rWcc845/PDDD7Rv356xY8dyzTXXAHDTTTfRvn177rjjDsaPH0+rVq3Izs7m6quv9m/jt7/9LatWreK3v/0tAFdccQVnnXUWP/zwg/8xd9xxByUlJZx11lm0aNGC//f//l+Vzli33XYbABdddBHbtm2jf//+LFy4kI4dOwa1jUaNGvHcc8/xhz/8gb59+9KtWzfuvPPOKp2LJk2axIUXXkjv3r3ZsWMH69evD2q9cNT12iKlrt/X/PnzOfLII2nfvr1/ncLCQk455RS+/fZb+vbtS48ePbjhhhsYP3489957L6eeempEa6wvq3wOIZXl5OS45cuXx7uMmOqXPYA7Zsf+kgap24UnDeD9996t8RzotGnTmDNnToVAzczMZNasWfTt27faIF29ejW9evWKWI1mVuU8o0i0nXnmmRxzzDEVvgwNHTqU/v37c+utt9Z7e7X9XZjZh865nJCL9dI5U5E4qes60+uuu46jjz6azMxMAH9HoF69esVsj3TKlClR3b5IdY455hj/oCE+hYWFFXoSJ5q4HuY1s6eBIUAG8D0w3Tn3qLdtCDADOBAoAMY5577ytjUFHgB+DZR617sr9q9AJHQtW+9Lv+yK18yVle1i+7ZtZLVuzVPPzsY5R5t2+1FcXEybdvvx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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# make a nice ratio plot, adjusting some font sizes\n",
+ "plt.rcParams.update({\n",
+ " 'font.size': 14,\n",
+ " 'axes.titlesize': 18,\n",
+ " 'axes.labelsize': 18,\n",
+ " 'xtick.labelsize': 12,\n",
+ " 'ytick.labelsize': 12\n",
+ "})\n",
+ "fig, (ax, rax) = plt.subplots(\n",
+ " nrows=2,\n",
+ " ncols=1,\n",
+ " figsize=(7,7),\n",
+ " gridspec_kw={\"height_ratios\": (3, 1)},\n",
+ " sharex=True\n",
+ ")\n",
+ "fig.subplots_adjust(hspace=.07)\n",
+ "\n",
+ "# Here is an example of setting up a color cycler to color the various fill patches\n",
+ "# We get the colors from this useful utility: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=6\n",
+ "from cycler import cycler\n",
+ "colors = ['#a6cee3','#1f78b4','#b2df8a','#33a02c','#fb9a99','#e31a1c']\n",
+ "ax.set_prop_cycle(cycler(color=colors))\n",
+ "\n",
+ "fill_opts = {\n",
+ " 'edgecolor': (0,0,0,0.3),\n",
+ " 'alpha': 0.8\n",
+ "}\n",
+ "error_opts = {\n",
+ " 'label': 'Stat. Unc.',\n",
+ " 'hatch': '///',\n",
+ " 'facecolor': 'none',\n",
+ " 'edgecolor': (0,0,0,.5),\n",
+ " 'linewidth': 0\n",
+ "}\n",
+ "data_err_opts = {\n",
+ " 'linestyle': 'none',\n",
+ " 'marker': '.',\n",
+ " 'markersize': 10.,\n",
+ " 'color': 'k',\n",
+ " 'elinewidth': 1,\n",
+ "}\n",
+ "\n",
+ "# plot the MC first\n",
+ "hist.plot1d(\n",
+ " pthist[notdata], \n",
+ " overlay=\"flavor\", \n",
+ " ax=ax,\n",
+ " clear=False,\n",
+ " stack=True, \n",
+ " line_opts=None,\n",
+ " fill_opts=fill_opts,\n",
+ " error_opts=error_opts\n",
+ ")\n",
+ "# now the pseudodata, setting clear=False to avoid overwriting the previous plot\n",
+ "hist.plot1d(\n",
+ " pthist['pseudodata'],\n",
+ " overlay=\"flavor\",\n",
+ " ax=ax,\n",
+ " clear=False,\n",
+ " error_opts=data_err_opts\n",
+ ")\n",
+ "\n",
+ "ax.autoscale(axis='x', tight=True)\n",
+ "ax.set_ylim(0, None)\n",
+ "ax.set_xlabel(None)\n",
+ "leg = ax.legend()\n",
+ "\n",
+ "# now we build the ratio plot\n",
+ "hist.plotratio(\n",
+ " num=pthist['pseudodata'].sum(\"flavor\"),\n",
+ " denom=pthist[notdata].sum(\"flavor\"), \n",
+ " ax=rax,\n",
+ " error_opts=data_err_opts, \n",
+ " denom_fill_opts={},\n",
+ " guide_opts={},\n",
+ " unc='num'\n",
+ ")\n",
+ "rax.set_ylabel('Ratio')\n",
+ "rax.set_ylim(0,2)\n",
+ "\n",
+ "# add some labels\n",
+ "coffee = plt.text(0., 1., u\"☕\",\n",
+ " fontsize=28, \n",
+ " horizontalalignment='left', \n",
+ " verticalalignment='bottom', \n",
+ " transform=ax.transAxes\n",
+ " )\n",
+ "lumi = plt.text(1., 1., r\"1 fb$^{-1}$ (?? TeV)\",\n",
+ " fontsize=16, \n",
+ " horizontalalignment='right', \n",
+ " verticalalignment='bottom', \n",
+ " transform=ax.transAxes\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Some further styling tools are available through the `mplhep` package. In particular, there are several stylesheets that update `plt.rcParams` to conform with experiment style recommendations regarding font face, font sizes, tick mark styles, and other such things. Below is an example application."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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OteppDQidomIn0bEIxGxc4v9/wn8GvhP4EJgfmmfJEnqpL/+9a9AliSVJwhtbMCAARx88MEArSYuARxzzDEMGjSoYt13vvOdQHY1dfHixZuM56GHHgKyBKutpyQNGzaMf//3fweomGABHHrooRX3b7nllpuMoSu8/vWv54QTTuAtb3nLBvvXrl3L7373O/7rv/6rYr3Bgwfzrne9C4D//d//3eDYz3/+c1JKjB07lokTJzbvL39mRxxxRMU/KgAOPvhgNt98c6DyZ7brrruy/fbbd/Dd1b+a1ymNiB2Ae4HtNtq/E/D50rd/BF4FDgC+GhH/l1K6r9Y+JUlSNkMdYOnSpR1a7ui5555rtW+PPfZos/wOO+zAlltuyapVq3j44YcZN25cu+2XE6xNPbazfLytpHT06PyfybN+/Xp++9vfcuONN/LAAw/w0EMPsWTJEl599dV2651wwgn84Ac/4KabbmLFihUMHz4cgNmzZwPwgQ98YIMR5I58ZuUVDh588MGKn9lOO+1U5burbzUnpcCngBHAE8AFwDOl/ccBmwP3pJT2B4iIRuA04KOlrSRJqtHatWurKl/psnely9MtbbZZliK89NJLm2w/u+1w08ptvvLKKxWPbyqm7vbUU0/xzne+s/nWiM0335x/+7d/4+1vfzsTJ05khx12YPLkyRXrHnbYYWy//fY8/fTTzJ07l/e973089thj3HbbbQwYMICTTjppg/Jd8ZkNGTKkmrdX9zpz+f5tZDe+npZSakopvVzaf0xp/w9alL2ktJ2IJEnqlL322guA173udR2aQFJOslp65JFH2mz/mWee4fnnn9+gr/bsueeeAM3rgralfCvA6173uk22mYcPfvCD3Hnnney5557ceOONvPDCC9xxxx1cdtllnHLKKbzhDW9os+5mm23G1KlTgX9dwr/mmmsAOProo9l55503KN+Rz6zl7RP1+pl1pc4kpeUx9lvLOyIblz649G3Lu5jLY867daI/SZLEvxLFhx9+mBdffLHNcn/7299YsGABTzzxRKtjN998c5ujdeVnxsO/kqf2jBkzBoAHH3ywzQXkywley/jryerVq/nVr34FwHe/+12OPvro5lHKsvIl97accMIJANxwww2sXr26+dJ9pYlM5c/spptuavPWgPnz57N69WqgPj+zrtaZpLR87aDlGdsX2BZYmlJq+SfYZhXKSpKkGowZM4a99tqL9evXt/mUokcffZR9992XN7/5zfz9739vdfzPf/4zP/vZz1rtX7FiBRdeeCEAU6ZMYauttmpVZuNk9uCDD2brrbdm3bp1zJw5s2Ky++Uvf5lnn32WAQMGcOSRR3bkbfaolrdElCcXtfTyyy/z9a9/vfn7Su/xkEMOYeedd2bNmjU0NjZy5513Mnz48OaJYy0dc8wxADzwwAP86Ec/anX8lVdeaV4F4XWve11zEtuXdSYpLU/HO6jFvhml7cZrPexb2j7Wif4kSeqzVqxYwZIlS9p9PfZY9t/ogAED+OIXvwjA17/+dU455RTuueceXnzxRZYvX861117LW97yFlavXs0BBxzQajZ52UknncRXvvIVHnroIZ555hmKxSL7778/ixcv3qCPjf3lL39h7dq1zYnZ1ltvzbnnngtkjyCdNm0ad955Jy+88AJ/+tOf+I//+A++8IUvAPDhD394kxOi8rDttts2T/76+Mc/zoIFC1izZg3Lli3jl7/8JW9961v59a9/3Vz+F7/4Rav7PAcMGMB73/tegObP44QTTqiY5B566KEcffTRQHbbwAUXXMCiRYt4/vnn+cMf/sCkSZOYN28eAF/4whc6NKGt16t1gVOyx4auJ7s0Pwk4FlhJNtv+2Bbldudfs/Bnd/fCq73hhYvnqwUXz5f6t/IC7x15bbzY+5e+9KV2y48bNy499dRTG9QpL+R+5plntvlITyBddtllrWL94Q9/uMHjNvfaa6/mY+vWrUunnnpqu/Ece+yxafXq1Ru02XLx/LaUF4t/61vfWtNn29HF8+fMmdNm7Jtttln61re+lcaMGdO8b8qUKa36nD9//gb17rjjjjbje+qpp9LEiRPb/cw+97nPddnnUasW56duF8+/GHga2AP4DTAH2Ar4EzAXICJOJxtRnVCq841O9CdJklr41Kc+xV133cWHPvQh9t9/f7bcckt23nln3vKWt9DU1MQ999zT5jqWEydOZOHChZx22mnsvPPODBkyhDe+8Y3MmDGDO++8kzPPPLNVnRNPPJHTTz+d7bbbjgEDBmywJNHAgQP5/ve/T7FY5KSTTuLf/u3f2GKLLXjDG97AtGnTuOqqq5gzZw5Dhw7tts+js975zndy6623cswxx7DrrrsyePDg5ocM3H///Zx11ln89Kc/ZezYsey4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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import mplhep\n",
+ "plt.style.use(mplhep.style.ROOT)\n",
+ "\n",
+ "# Compare this to the style of the plot drawn previously\n",
+ "ax = hist.plot1d(lepton_pt, overlay=\"flavor\", density=True)"
+ ]
+ }
+ ],
+ "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.9.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/_sources/notebooks/nanoevents.ipynb.txt b/_sources/notebooks/nanoevents.ipynb.txt
new file mode 100644
index 000000000..87bf187dc
--- /dev/null
+++ b/_sources/notebooks/nanoevents.ipynb.txt
@@ -0,0 +1,639 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Reading data with coffea NanoEvents\n",
+ "\n",
+ "This is a rendered copy of [nanoevents.ipynb](https://github.com/CoffeaTeam/coffea/blob/master/binder/nanoevents.ipynb). You can optionally run it interactively on [binder at this link](https://mybinder.org/v2/gh/coffeateam/coffea/master?filepath=binder%2Fnanoevents.ipynb)\n",
+ "\n",
+ "NanoEvents is a Coffea utility to wrap flat nTuple structures (such as the CMS [NanoAOD](https://www.epj-conferences.org/articles/epjconf/pdf/2019/19/epjconf_chep2018_06021.pdf) format) into a single awkward array with appropriate object methods (such as Lorentz vector methods$^*$), cross references, and nested objects, all lazily accessed$^\\dagger$ from the source ROOT TTree via uproot. The interpretation of the TTree data is configurable via [schema objects](https://coffeateam.github.io/coffea/modules/coffea.nanoevents.html#classes), which are community-supplied for various source file types. These schema objects allow a richer interpretation of the file contents than the [uproot.lazy](https://uproot4.readthedocs.io/en/latest/uproot4.behaviors.TBranch.lazy.html) methods. Currently available schemas include:\n",
+ "\n",
+ " - `BaseSchema`, which provides a simple representation of the input TTree, where each branch is available verbatim as `events.branch_name`, effectively the same behavior as `uproot.lazy`. Any branches that uproot supports at \"full speed\" (i.e. that are fully split and either flat or single-jagged) can be read by this schema;\n",
+ " - `NanoAODSchema`, which is optimized to provide all methods and cross-references in CMS NanoAOD format;\n",
+ " - `PFNanoAODSchema`, which builds a double-jagged particle flow candidate colllection `events.jet.constituents` from compatible PFNanoAOD input files;\n",
+ " - `TreeMakerSchema` which is designed to read TTrees made by [TreeMaker](https://github.com/TreeMaker/TreeMaker), an alternative CMS nTuplization format;\n",
+ " - `PHYSLITESchema`, for the ATLAS DAOD_PHYSLITE derivation, a compact centrally-produced data format similar to CMS NanoAOD; and\n",
+ " - `DelphesSchema`, for reading Delphes fast simulation [nTuples](https://cp3.irmp.ucl.ac.be/projects/delphes/wiki/WorkBook/RootTreeDescription).\n",
+ "\n",
+ "We welcome contributions for new schemas, and can assist with the design of them.\n",
+ "\n",
+ "$^*$ Vector methods are currently made possible via the [coffea vector](https://coffeateam.github.io/coffea/modules/coffea.nanoevents.methods.vector.html) methods mixin class structure. In a future version of coffea, they will instead be provided by the dedicated scikit-hep [vector](https://vector.readthedocs.io/en/latest/) library, which provides a more rich feature set. The coffea vector methods predate the release of the vector library.\n",
+ "\n",
+ "$^\\dagger$ _Lazy_ access refers to only fetching the needed data from the (possibly remote) file when a sub-array is first accessed. The sub-array is then _materialized_ and subsequent access of the sub-array uses a cached value in memory. As such, fully materializing a `NanoEvents` object may require a significant amount of memory.\n",
+ "\n",
+ "\n",
+ "In this demo, we will use NanoEvents to read a small CMS NanoAOD sample. The events object can be instantiated as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import awkward as ak\n",
+ "from coffea.nanoevents import NanoEventsFactory, NanoAODSchema\n",
+ "\n",
+ "fname = \"https://raw.githubusercontent.com/CoffeaTeam/coffea/master/tests/samples/nano_dy.root\"\n",
+ "events = NanoEventsFactory.from_root(\n",
+ " fname,\n",
+ " schemaclass=NanoAODSchema.v6,\n",
+ " metadata={\"dataset\": \"DYJets\"},\n",
+ ").events()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the factory constructor, we also pass the desired schema version (the latest version of NanoAOD can be built with `schemaclass=NanoAODSchema`) for this file and some extra metadata that we can later access with `events.metadata`. In a later example, we will show how to set up this metadata in coffea processors where the `events` object is pre-created for you. Consider looking at the [from_root](https://coffeateam.github.io/coffea/api/coffea.nanoevents.NanoEventsFactory.html#coffea.nanoevents.NanoEventsFactory.from_root) class method to see all optional arguments.\n",
+ "\n",
+ "The `events` object is an awkward array, which at its top level is a record array with one record for each \"collection\", where a collection is a grouping of fields (TBranches) based on the naming conventions of [NanoAODSchema](https://coffeateam.github.io/coffea/api/coffea.nanoevents.NanoAODSchema.html). For example, in the file we opened, the branches:\n",
+ "```\n",
+ "Generator_binvar\n",
+ "Generator_scalePDF\n",
+ "Generator_weight\n",
+ "Generator_x1\n",
+ "Generator_x2\n",
+ "Generator_xpdf1\n",
+ "Generator_xpdf2\n",
+ "Generator_id1\n",
+ "Generator_id2\n",
+ "```\n",
+ "are grouped into one sub-record named `Generator` which can be accessed using either getitem or getattr syntax, i.e. `events[\"Generator\"]` or `events.Generator`. e.g."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.Generator.id1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['binvar', 'scalePDF', 'weight', 'x1', 'x2', 'xpdf1', 'xpdf2', 'id1', 'id2']"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# all names can be listed with:\n",
+ "events.Generator.fields"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In CMS NanoAOD, each TBranch has a self-documenting help string embedded in the title field, which is carried into the NanoEvents, e.g. executing the following cell should produce a help pop-up:\n",
+ "```\n",
+ "Type: Array\n",
+ "String form: [1, -1, -1, 21, 21, 4, 2, -2, 2, 1, 3, 1, ... -1, -1, 1, -2, 2, 1, 2, -2, -1, 2, 1]\n",
+ "Length: 40\n",
+ "File: ~/src/awkward-1.0/awkward1/highlevel.py\n",
+ "Docstring: id of first parton\n",
+ "Class docstring: ...\n",
+ "```\n",
+ "where the `Docstring` shows information about the content of this array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "events.Generator.id1?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Based on a collection's name or contents, some collections acquire additional _methods_, which are extra features exposed by the code in the mixin classes of the `coffea.nanoevents.methods` modules. For example, although `events.GenJet` has the fields:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['eta', 'mass', 'phi', 'pt', 'partonFlavour', 'hadronFlavour']"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.GenJet.fields"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "we can access additional attributes associated to each generated jet by virtue of the fact that they can be interpreted as [Lorentz vectors](https://coffeateam.github.io/coffea/api/coffea.nanoevents.methods.vector.LorentzVector.html#coffea.nanoevents.methods.vector.LorentzVector):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.GenJet.energy"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can call more complex methods, like computing the distance $\\Delta R = \\sqrt{\\Delta \\eta^2 + \\Delta \\phi ^2}$ between two LorentzVector objects:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# find distance between leading jet and all electrons in each event\n",
+ "dr = events.Jet[:, 0].delta_r(events.Electron)\n",
+ "dr"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# find minimum distance\n",
+ "ak.min(dr, axis=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# a convenience method for this operation on all jets is available\n",
+ "events.Jet.nearest(events.Electron)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The assignment of methods classes to collections is done inside the schema object during the initial creation of the array, governed by the awkward array's `__record__` parameter and the associated behavior. See [ak.behavior](https://awkward-array.readthedocs.io/en/latest/ak.behavior.html) for a more detailed explanation of array behaviors.\n",
+ "\n",
+ "Additional methods provide convenience functions for interpreting some branches, e.g. CMS NanoAOD packs several jet identification flag bits into a single integer, `jetId`. By implementing the bit-twiddling in the [Jet mixin](https://github.com/CoffeaTeam/coffea/blob/7045c06b9448d2be4315e65d432e6d8bd117d6d7/coffea/nanoevents/methods/nanoaod.py#L279-L282), the analsyis code becomes more clear:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[6, 6, 6, 6, 6], [6, 2, 6, 6, 6, 6, 6, 0], ... 6], [6], [6, 6, 0, 6, 6, 6], [6, 6]]\n",
+ "[[True, True, True, True, True], [True, ... False, True, True, True], [True, True]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(events.Jet.jetId)\n",
+ "print(events.Jet.isTight)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also define convenience functions to unpack and apply some mask to a set of flags, e.g. for generated particles:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Raw status flags: [[10625, 27009, 4481, 22913, 257, 257, ... 13884, 13884, 12876, 12876, 12876, 12876]]\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "print(f\"Raw status flags: {events.GenPart.statusFlags}\")\n",
+ "events.GenPart.hasFlags(['isPrompt', 'isLastCopy'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "CMS NanoAOD also contains pre-computed cross-references for some types of collections. For example, there is a TBranch `Electron_genPartIdx` which indexes the `GenPart` collection per event to give the matched generated particle, and `-1` if no match is found. NanoEvents transforms these indices into an awkward _indexed array_ pointing to the collection, so that one can directly access the matched particle using getattr syntax:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.Electron.matched_gen.pdgId"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.Muon[ak.num(events.Muon)>0].matched_jet.pt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For generated particles, the parent index is similarly mapped:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.GenPart.parent.pdgId"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In addition, using the parent index, a helper method computes the inverse mapping, namely, `children`. As such, one can find particle siblings with:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.GenPart.parent.children.pdgId\n",
+ "# notice this is a doubly-jagged array"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Since often one wants to shortcut repeated particles in a decay sequence, a helper method `distinctParent` is also available. Here we use it to find the parent particle ID for all prompt electrons:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "events.GenPart[\n",
+ " (abs(events.GenPart.pdgId) == 11)\n",
+ " & events.GenPart.hasFlags(['isPrompt', 'isLastCopy'])\n",
+ "].distinctParent.pdgId"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Events can be filtered like any other awkward array using boolean fancy-indexing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "mmevents = events[ak.num(events.Muon) == 2]\n",
+ "zmm = mmevents.Muon[:, 0] + mmevents.Muon[:, 1]\n",
+ "zmm.mass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# a convenience method is available to sum vectors along an axis:\n",
+ "mmevents.Muon.sum(axis=1).mass"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As expected for this sample, most of the dimuon events have a pair invariant mass close to that of a Z boson. But what about the last event? Let's take a look at the generator information:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[-13, 13]\n",
+ "[False, False]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(mmevents[-1].Muon.matched_gen.pdgId)\n",
+ "print(mmevents[-1].Muon.matched_gen.hasFlags([\"isPrompt\"]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So they are real generated muons, but they are not prompt (i.e. from the initial decay of a heavy resonance)\n",
+ "\n",
+ "Let's look at their parent particles:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 20,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "mmevents[-1].Muon.matched_gen.parent.pdgId"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "aha! They are muons coming from tau lepton decays, and hence a fair amount of the Z mass is carried away by the neutrinos:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "31.265271292167853\n",
+ "91.68363205830444\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(mmevents[-1].Muon.matched_gen.sum().mass)\n",
+ "print(mmevents[-1].Muon.matched_gen.parent.sum().mass)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One can assign new variables to the arrays, with some caveats:\n",
+ "\n",
+ " * Assignment must use setitem (`events[\"path\", \"to\", \"name\"] = value`)\n",
+ " * Assignment to a sliced `events` won't be accessible from the original variable\n",
+ " * New variables are not visible from cross-references"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "mmevents[\"Electron\", \"myvariable\"] = mmevents.Electron.pt + zmm.mass\n",
+ "mmevents.Electron.myvariable"
+ ]
+ }
+ ],
+ "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.9.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/_sources/notebooks/processor.ipynb.txt b/_sources/notebooks/processor.ipynb.txt
new file mode 100644
index 000000000..92a2e0ce9
--- /dev/null
+++ b/_sources/notebooks/processor.ipynb.txt
@@ -0,0 +1,947 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Coffea Processors\n",
+ "This is a rendered copy of [processor.ipynb](https://github.com/CoffeaTeam/coffea/blob/master/binder/processor.ipynb). You can optionally run it interactively on [binder at this link](https://mybinder.org/v2/gh/coffeateam/coffea/master?filepath=binder%2Fprocessor.ipynb)\n",
+ "\n",
+ "Coffea relies mainly on [uproot](https://github.com/scikit-hep/uproot) to provide access to ROOT files for analysis.\n",
+ "As a usual analysis will involve processing tens to thousands of files, totalling gigabytes to terabytes of data, there is a certain amount of work to be done to build a parallelized framework to process the data in a reasonable amount of time. Of course, one can work directly within uproot to achieve this, as we'll show in the beginning, but coffea provides the `coffea.processor` module, which allows users to worry just about the actual analysis code and not about how to implement efficient parallelization, assuming that the parallization is a trivial map-reduce operation (e.g. filling histograms and adding them together). The module provides the following key features:\n",
+ "\n",
+ " * A `ProcessorABC` abstract base class that can be derived from to implement the analysis code;\n",
+ " * A [NanoEvents](https://coffeateam.github.io/coffea/notebooks/nanoevents.html) interface to the arrays being read from the TTree as inputs;\n",
+ " * A generic `accumulate()` utility to reduce the outputs to a single result, as showin in the accumulators notebook tutorial; and\n",
+ " * A set of parallel executors to access multicore processing or distributed computing systems such as [Dask](https://distributed.dask.org/en/latest/), [Parsl](http://parsl-project.org/), [Spark](https://spark.apache.org/), [WorkQueue](https://cctools.readthedocs.io/en/latest/work_queue/), and others.\n",
+ "\n",
+ "Let's start by writing a simple processor class that reads some CMS open data and plots a dimuon mass spectrum.\n",
+ "We'll start by copying the [ProcessorABC](https://coffeateam.github.io/coffea/api/coffea.processor.ProcessorABC.html#coffea.processor.ProcessorABC) skeleton and filling in some details:\n",
+ "\n",
+ " * Remove `flag`, as we won't use it\n",
+ " * Adding a new histogram for $m_{\\mu \\mu}$\n",
+ " * Building a [Candidate](https://coffeateam.github.io/coffea/api/coffea.nanoevents.methods.candidate.PtEtaPhiMCandidate.html#coffea.nanoevents.methods.candidate.PtEtaPhiMCandidate) record for muons, since we will read it with `BaseSchema` interpretation (the files used here could be read with `NanoAODSchema` but we want to show how to build vector objects from other TTree formats) \n",
+ " * Calculating the dimuon invariant mass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import awkward as ak\n",
+ "from coffea import processor\n",
+ "from coffea.nanoevents.methods import candidate\n",
+ "import hist\n",
+ "\n",
+ "class MyProcessor(processor.ProcessorABC):\n",
+ " def __init__(self):\n",
+ " pass\n",
+ "\n",
+ " def process(self, events):\n",
+ " dataset = events.metadata['dataset']\n",
+ " muons = ak.zip(\n",
+ " {\n",
+ " \"pt\": events.Muon_pt,\n",
+ " \"eta\": events.Muon_eta,\n",
+ " \"phi\": events.Muon_phi,\n",
+ " \"mass\": events.Muon_mass,\n",
+ " \"charge\": events.Muon_charge,\n",
+ " },\n",
+ " with_name=\"PtEtaPhiMCandidate\",\n",
+ " behavior=candidate.behavior,\n",
+ " )\n",
+ "\n",
+ " h_mass = (\n",
+ " hist.Hist.new\n",
+ " .StrCat([\"opposite\", \"same\"], name=\"sign\")\n",
+ " .Log(1000, 0.2, 200., name=\"mass\", label=\"$m_{\\mu\\mu}$ [GeV]\")\n",
+ " .Int64()\n",
+ " )\n",
+ "\n",
+ " cut = (ak.num(muons) == 2) & (ak.sum(muons.charge, axis=1) == 0)\n",
+ " # add first and second muon in every event together\n",
+ " dimuon = muons[cut][:, 0] + muons[cut][:, 1]\n",
+ " h_mass.fill(sign=\"opposite\", mass=dimuon.mass)\n",
+ "\n",
+ " cut = (ak.num(muons) == 2) & (ak.sum(muons.charge, axis=1) != 0)\n",
+ " dimuon = muons[cut][:, 0] + muons[cut][:, 1]\n",
+ " h_mass.fill(sign=\"same\", mass=dimuon.mass)\n",
+ "\n",
+ " return {\n",
+ " dataset: {\n",
+ " \"entries\": len(events),\n",
+ " \"mass\": h_mass,\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " def postprocess(self, accumulator):\n",
+ " pass"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we were to just use bare uproot to execute this processor, we could do that with the following example, which:\n",
+ "\n",
+ " * Opens a CMS open data file\n",
+ " * Creates a NanoEvents object using `BaseSchema` (roughly equivalent to the output of `uproot.lazy`)\n",
+ " * Creates a `MyProcessor` instance\n",
+ " * Runs the `process()` function, which returns our accumulators\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'DoubleMuon': {'entries': 10000,\n",
+ " 'mass': Hist(\n",
+ " StrCategory(['opposite', 'same'], name='sign', label='sign'),\n",
+ " Regular(1000, 0.2, 200, transform=log, name='mass', label='$m_{\\\\mu\\\\mu}$ [GeV]'),\n",
+ " storage=Int64()) # Sum: 4939.0 (4951.0 with flow)}}"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import uproot\n",
+ "from coffea.nanoevents import NanoEventsFactory, BaseSchema\n",
+ "\n",
+ "filename = \"root://eospublic.cern.ch//eos/root-eos/cms_opendata_2012_nanoaod/Run2012B_DoubleMuParked.root\"\n",
+ "file = uproot.open(filename)\n",
+ "events = NanoEventsFactory.from_root(\n",
+ " file,\n",
+ " entry_stop=10000,\n",
+ " metadata={\"dataset\": \"DoubleMuon\"},\n",
+ " schemaclass=BaseSchema,\n",
+ ").events()\n",
+ "p = MyProcessor()\n",
+ "out = p.process(events)\n",
+ "out"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAEPCAYAAAC5sYRSAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAsDElEQVR4nO3de5hU5Znv/e/d1Seag8rJoBAaZxQFGohpQJQQdphtzKuJqImSiQ6eYsyWxODWiBkjnZgxvpm8OfDqRHGMshO3kHhITHR0J86QaIJIE4kKeIgIQkRogUBDH6vq3n/Uoau7q89dXV2rfp/r6qtrne+1Vvddz3rWs55l7o6IiARLQbYDEBGR/qfkLiISQEruIiIBpOQuIhJASu4iIgGk5C4iEkCF2Q4AYPTo0V5eXp7tMEREcsqmTZved/cx6aYNiuReXl5OdXV1tsMQEckpZrazo2mqlhERCSAldxGRAFJyFxEJoC7r3M3sx8B5wD53nxYfNxJYC5QDO4CL3f1gfNotwFVABPiyuz/Tm8Cam5vZvXs3DQ0NvVlc2igtLWX8+PEUFRVlOxQRGQDduaH6IHAX8L9Sxi0HnnX3O81seXz4ZjObAiwGpgInAL81s1PcPdLTwHbv3s3w4cMpLy/HzHq6uKRwd/bv38/u3buZNGlStsMRkQHQZbWMu/8eONBm9PnA6vjn1cCilPFr3L3R3d8G/gLM7k1gDQ0NjBo1Som9H5gZo0aN0lWQSB7pbZ378e6+ByD+e2x8/InArpT5dsfH9YoSe//RsRTJL/19QzVdBknbYbyZXWNm1WZWXVNT062Vh0IhZs6cydSpU5kxYwbf+973iEajAFRXV/PlL3+514FnWlVVFd/97nezHYaI5InePsS018zGufseMxsH7IuP3w1MSJlvPPBuuhW4+ypgFUBlZWW33hgyZMgQNm/eDMC+ffv4x3/8Rw4dOsQ3vvENKisrqays7N3e5IBIJEIoFMp2GCKSI3pbcn8CWBL/vAT4Zcr4xWZWYmaTgJOBF/sWYnpjx45l1apV3HXXXbg769at47zzzgNipeQlS5Zw9tlnU15ezmOPPcZXv/pVKioqOOecc2hubgZiT8a+//77QKzkv2DBAgAOHDjAokWLmD59OmeccQYvv/xycr1XXnklCxYs4KSTTmLlypVpY3v66ac5/fTTmTFjBgsXLkyO37p1a9plFy1axIc//GGmTp3KqlWrkuOHDRvGbbfdxpw5c1i/fj33338/p5xyCgsWLODzn/88S5cuBaCmpoaLLrqIWbNmMWvWLP7whz/001EWCZ5L7l3PJfeuz3YYmefunf4ADwN7gGZiJfOrgFHAs8Cb8d8jU+b/Z+At4HXgE12t39358Ic/7G1t3bq13bihQ4e2G3fsscf6e++95//1X//l5557rru7r1ixws866yxvamryzZs3+5AhQ/ypp55yd/dFixb5448/7u7uEydO9JqaGnd337hxo3/0ox91d/elS5d6VVWVu7s/++yzPmPGjOR6586d6w0NDV5TU+MjR470pqamVvHs27fPx48f79u3b3d39/3793e5bGKeuro6nzp1qr///vvusYPpa9eudXf3v/71rz5x4kTfv3+/NzU1+bx58/y6665zd/fPfvaz/txzz7m7+86dO/3UU09td5w6OqYi+ebie/7oF9/zx2yH0S+Aau8gr3ZZLePun+1g0sJ0I939X4B/6f7XS994B++A/cQnPkFRUREVFRVEIhHOOeccACoqKtixY0en63z++ed59NFHAfjYxz7G/v37OXToEADnnnsuJSUllJSUMHbsWPbu3cv48eOTy77wwgvMnz8/2eRw5MiRyWkdLbty5Uoef/xxAHbt2sWbb77JqFGjCIVCXHTRRQC8+OKLfPSjH02u7zOf+QxvvPEGAL/97W/ZunVrcjuHDx+mtraW4cOHd+8gikjgDIqOw3pr+/bthEIhxo4dy7Zt21pNKykpAaCgoICioqJka5GCggLC4TAAhYWFyRuyqc0E031hJJZPrBdiN3gT60pdtqOWKemWXbduHb/97W9Zv349ZWVlLFiwIBlLaWlpsp69oy8xgGg0yvr16xkyZEiH84hIfsnZ7gdqamq49tprWbp0aa+b+ZWXl7Np0yaAZEkdYP78+Tz00EMArFu3jtGjRzNixIhurXPu3Ln87ne/4+233wZi9fedOXToEMcddxxlZWW89tprvPDCC2nnmz17Nr/73e84ePAg4XC4Vbxnn302d911V3I4cdNZRNrbuucwW/ccznYYGZdTJff6+npmzpxJc3MzhYWFXHbZZdxwww29Xt+KFSu46qqruOOOO5gzZ05yfFVVFVdccQXTp0+nrKyM1atXd7KW1saMGcOqVau48MILiUajjB07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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "out[\"DoubleMuon\"][\"mass\"].plot1d(ax=ax)\n",
+ "ax.set_xscale(\"log\")\n",
+ "ax.legend(title=\"Dimuon charge\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One could expand on this code to run over several chunks of the file, setting `entry_start` and `entry_stop` as appropriate. Then, several datasets could be processed by iterating over several files. However, the processor [Runner](https://coffeateam.github.io/coffea/api/coffea.processor.Runner.html) can help with this! One lists the datasets and corresponding files, the processor they want to run, and which executor they want to use. Available executors derive from `ExecutorBase` and are listed [here](https://coffeateam.github.io/coffea/modules/coffea.processor.html#classes). Since these files are very large, we limit to just reading the first few chunks of events from each dataset with `maxchunks`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e4fb80c50d72479a8b3906c885982cd4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Output()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "
This class can store several boolean arrays in a memory-efficient mannner
+and evaluate arbitrary combinations of boolean requirements in an CPU-efficient way.
+Supported inputs are 1D numpy or awkward arrays.
+
+
Parameters:
+
dtype (numpy.dtype or str) – internal bitwidth of the packed array, which governs the maximum
+number of selections storable in this object. The default value
+is uint32, which allows up to 32 booleans to be stored, but
+if a smaller or larger number of selections needs to be stored,
+one can choose uint16 or uint64 instead.
selection (numpy.ndarray or awkward.Array) – a flat array of type bool or ?bool.
+If this is not the first selection added, it must also have
+the same shape as previously added selections. If the array
+is option-type, null entries will be filled with fill_value.
+
fill_value (bool, optional) – All masked entries will be filled as specified (default: False)
Container for event weights and associated systematic shifts
+
This container keeps track of correction factors and systematic
+effects that can be encoded as multiplicative modifiers to the event weight.
+All weights are stored in vector form.
+
+
Parameters:
+
+
size (int) – size of the weight arrays to be handled (i.e. the number of events / instances).
+
storeIndividual (bool, optional) – store not only the total weight + variations, but also each individual weight.
+Default is false.
weight (numpy.ndarray) – the nominal event weight associated with the correction
+
weightUp (numpy.ndarray, optional) – weight with correction uncertainty shifted up (if available)
+
weightDown (numpy.ndarray, optional) – weight with correction uncertainty shifted down. If weightUp is supplied, and
+the correction uncertainty is symmetric, this can be set to None to auto-calculate
+the down shift as 1/weightUp.
+
shift (bool, optional) – if True, interpret weightUp and weightDown as a realtive difference (additive) to the
+nominal value
+
+
+
+
+
Note
+
weightUp and weightDown are assumed to be rvalue-like and may be modified in-place by this function
weight (numpy.ndarray) – the nominal event weight associated with the correction
+
modifierNames (list of str) – list of modifiers for each set of weights variation
+
weightsUp (list of numpy.ndarray) – weight with correction uncertainty shifted up (if available)
+
weightsDown (list of numpy.ndarray) – weight with correction uncertainty shifted down. If weightUp is supplied, and
+the correction uncertainty is symmetric, this can be set to None to auto-calculate
+the down shift as 1/weightUp.
+
shift (bool, optional) – if True, interpret weightUp and weightDown as a realtive difference (additive) to the
+nominal value
+
+
+
+
+
Note
+
weightUp and weightDown are assumed to be rvalue-like and may be modified in-place by this function
Return a partial weight by multiplying a subset of all weights.
+Can be operated either by specifying weights to include or
+weights to exclude, but not both at the same time. The method
+can only be used if the individual weights are stored via the
+storeIndividual argument in the Weights initializer.
+
+
Parameters:
+
+
include (list) – Weight names to include, defaults to []
+
exclude (list) – Weight names to exclude, defaults to []
+
+
+
Returns:
+
weight – The weight vector, corresponding to only the effect of the
+corrections specified.
modifier (str, optional) – if supplied, provide event weight corresponding to a particular
+systematic uncertainty shift, of form str(name+'Up') or (Down)
+
+
Returns:
+
weight – The weight vector, possibly modified by the effect of a given systematic variation.
+__call__(systematic, flavor, eta, pt, discr=None, ignore_missing=False)[source]
+
Call self as a function.
+
+
+
+
+eval(systematic, flavor, eta, pt, discr=None, ignore_missing=False)[source]
+
Evaluate this scale factor as a function of jet properties
+
+
Parameters:
+
+
systematic (str) – Which systematic to evaluate. Nominal correction is ‘central’, the options depend
+on the scale factor and method
+
flavor (numpy.ndarray or awkward.Array) – The generated jet hadron flavor, following the enumeration:
+0: uds quark or gluon, 4: charm quark, 5: bottom quark
+
eta (numpy.ndarray or awkward.Array) – The jet pseudorapitiy
+
pt (numpy.ndarray or awkward.Array) – The jet transverse momentum
+
discr (numpy.ndarray or awkward.Array, optional) – The jet tagging discriminant value (default None), optional for all scale factors except
+the reshaping scale factor
+
ignore_missing (bool, optional) – If set true, any values that have no correction will return 1. instead of throwing
+an exception. Out-of-bounds values are always clipped to the nearest bin.
+
+
+
Returns:
+
out – An array with shape matching pt, containing the per-jet scale factor
identifier (float or Interval or numpy.ndarray) – The identifier(s) to lookup. Supports vectorized
+calls when a numpy 1D array of numbers is passed.
+
+
+
Returns an integer corresponding to the index in the axis where the histogram would be filled.
+The integer range includes flow bins: 0=underflow,n+1=overflow,n+2=nanflow
name (str) – is used as a keyword in histogram filling, immutable
+
label (str) – describes the meaning of the axis, can be changed
+
sorting ({'identifier', 'placement', 'integral'}, optional) – Axis sorting when listing identifiers. Default ‘placement’
+Changing this setting can effect the order of stack plotting
+in hist.plot1d.
+
+
+
+
The number of categories is arbitrary, and can be filled sparsely
+Identifiers are strings
identifier (str or StringBin) – The identifier to lookup
+
+
+
Returns a StringBin corresponding to the given argument (trival in the case
+where a StringBin was passed) and saves a reference internally in the case where
+the identifier was not seen before by this axis.
**values – Keyword arguments, one for each axis name, of either flat numpy arrays
+(for dense dimensions) or literals (for sparse dimensions) which will
+be used to fill bins at the corresponding indices.
+
+
+
+
Note
+
The reserved keyword weight, if specified, will increment sum of weights
+by the given column values, which must be broadcastable to the same dimension as all other
+columns. Upon first use, this will trigger the storage of the sum of squared weights.
Group a set of slices on old axes into a single new axis
+
+
Parameters:
+
+
old_axes – Axis or tuple of axes which are being grouped
+
new_axis – A new sparse dimension definition, e.g. a Cat instance
+
mapping (dict) – A mapping {'new_bin':(slice,...),...} where each
+slice is on the axes being re-binned. In the case of
+a single axis for old_axes, {'new_bin':slice,...}
+is admissible.
+
overflow (str) – See sum description for meaning of allowed values
+Default is to not include overflow bins
*axes (str or Axis) – Positional list of axes to project on to
+
overflow (str) – Controls behavior of integration over remaining axes.
+See sum description for meaning of allowed values
+Default is to not include overflow bins
This function will construct the mapping from old to new axis, and
+constructs a new histogram, rebinning the sum of weights along that dimension.
+
+
Note
+
No interpolation is performed, so the user must be sure the old
+and new axes have compatible bin boundaries, e.g. that they evenly
+divide each other.
new_axis (str or Axis or int) – A DenseAxis object defining the new axis (e.g. a Bin instance).
+If a number N is supplied, the old axis edges are downsampled by N,
+resulting in a histogram with old_nbins//N bins.
Integrates out a set of axes, producing a new histogram
+
+
Parameters:
+
+
*axes – Positional list of axes to integrate out (either a string or an Axis object)
+
overflow ({'none', 'under', 'over', 'all', 'allnan'}, optional) – How to treat the overflow bins in the sum. Only applies to dense axes.
+‘all’ includes both under- and over-flow but not nan-flow bins.
+Default is ‘none’.
Extract the sum of weights arrays from this histogram
+
+
Parameters:
+
+
sumw2 (bool) – If True, frequencies is a tuple of arrays (sum weights, sum squared weights)
+
overflow – See sum description for meaning of allowed values
+
+
+
+
Returns a mapping {(sparseidentifier,...):numpy.array(...),...}
+where each array has dimension dense_dim and shape matching
+the number of bins per axis, plus 0-3 overflow bins depending
+on the overflow argument.
name (str) – Name of the bin, as used in Hist.fill calls
+
label (str) – The str representation of this bin can be overriden by
+a custom label, which will be used preferentially in legends
+produced by hist.plot1d, etc.
hist (Hist) – Histogram with maximum of two dimensions
+
ax (matplotlib.axes.Axes, optional) – Axes object (if None, one is created)
+
clear (bool, optional) – Whether to clear Axes before drawing (if passed); if False, this function will skip drawing the legend
+
overlay (str, optional) – In the case that hist is 2D, specify the axis of hist to overlay (remaining axis will be x axis)
+
stack (bool, optional) – Whether to stack or overlay non-axis dimension (if it exists)
+
order (list, optional) – How to order when stacking. Take a list of identifiers.
+
overflow (str, optional) – If overflow behavior is not ‘none’, extra bins will be drawn on either end of the nominal
+axis range, to represent the contents of the overflow bins. See Hist.sum documentation
+for a description of the options.
+
line_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.step call
+internal to this function. Leave blank for defaults.
+
fill_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.fill_between call
+internal to this function. Leave blank for defaults.
+
error_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.errorbar call
+internal to this function. Leave blank for defaults. Some special options are interpreted by
+this function and not passed to matplotlib: ‘emarker’ (default: ‘’) specifies the marker type
+to place at cap of the errorbar.
+
legend_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.legend call
+internal to this fuction. Leave blank for defaults.
+
overlay_overflow (str, optional) – If overflow behavior is not ‘none’, extra bins in the overlay axis will be overlayed or stacked,
+to represent the contents of the overflow bins. See Hist.sum documentation for a description of the options.
+
density (bool, optional) – If true, convert sum weights to probability density (i.e. integrates to 1 over domain of axis)
+(Note: this option conflicts with binwnorm)
+
binwnorm (float, optional) – If true, convert sum weights to bin-width-normalized, with unit equal to supplied value (usually you want to specify 1.)
xaxis (str or Axis) – Which of the two dimensions to use as an x axis
+
ax (matplotlib.axes.Axes, optional) – Axes object (if None, one is created)
+
clear (bool, optional) – Whether to clear Axes before drawing (if passed); if False, this function will skip drawing the legend
+
xoverflow (str, optional) – If overflow behavior is not ‘none’, extra bins will be drawn on either end of the nominal x
+axis range, to represent the contents of the overflow bins. See Hist.sum documentation
+for a description of the options.
patch_opts (dict, optional) – Options passed to the matplotlib pcolormesh
+call internal to this function, to plot a rectangular grid of patches colored according to the bin values.
+Leave empty for defaults.
+
text_opts (dict, optional) – Options passed to the matplotlib text
+call internal to this function, to place a text label at each bin center with the bin value. Special
+options interpreted by this function and not passed to matplotlib: ‘format’: printf-style float format
+, default ‘%.2g’.
+
density (bool, optional) – If true, convert sum weights to probability density (i.e. integrates to 1 over domain of axis)
+(Note: this option conflicts with binwnorm)
+
binwnorm (float, optional) – If true, convert sum weights to bin-width-normalized, with unit equal to supplied value (usually you want to specify 1.)
row_overflow (str, optional) – If overflow behavior is not ‘none’, extra bins will be drawn on either end of the nominal x
+axis range, to represent the contents of the overflow bins. See Hist.sum documentation
+for a description of the options.
+
col_overflow (str, optional) – Similar to row_overflow
+
**plot_opts (kwargs) – The remaining axis of the histogram, after removing any of row,col,overlay specified,
+will be the plot axis, with plot_opts passed to the plot1d call.
denom (Hist) – Denominator, a single-axis histogram
+
ax (matplotlib.axes.Axes, optional) – Axes object (if None, one is created)
+
clear (bool, optional) – Whether to clear Axes before drawing (if passed); if False, this function will skip drawing the legend
+
overflow (str, optional) – If overflow behavior is not ‘none’, extra bins will be drawn on either end of the nominal
+axis range, to represent the contents of the overflow bins. See Hist.sum documentation
+for a description of the options.
+
xerr (bool, optional) – If true, then error bars are drawn for x-axis to indicate the size of the bin.
+
error_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.errorbar call
+internal to this function. Leave blank for defaults. Some special options are interpreted by
+this function and not passed to matplotlib: ‘emarker’ (default: ‘’) specifies the marker type
+to place at cap of the errorbar.
+
denom_fill_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.fill_between call
+internal to this function, filling the denominator uncertainty band. Leave blank for defaults.
+
guide_opts (dict, optional) – A dictionary of options to pass to the matplotlib
+ax.axhline call
+internal to this function, to plot a horizontal guide line at ratio of 1. Leave blank for defaults.
+
unc (str, optional) – Uncertainty calculation option: ‘clopper-pearson’ interval for efficiencies; ‘poisson-ratio’ interval
+for ratio of poisson distributions; ‘num’ poisson interval of numerator scaled by denominator value
+(common for data/mc, for better or worse).
+
label (str, optional) – Associate a label to this entry (note: y axis label set by num.label)
coverage (float, optional) – Central coverage interval, defaults to 68%
+
+
+
+
Calculates the so-called ‘Garwood’ interval,
+c.f. https://www.ine.pt/revstat/pdf/rs120203.pdf or
+http://ms.mcmaster.ca/peter/s743/poissonalpha.html
+For weighted data, this approximates the observed count by sumw**2/sumw2, which
+effectively scales the unweighted poisson interval by the average weight.
+This may not be the optimal solution: see https://arxiv.org/pdf/1309.1287.pdf for a proper treatment.
+When a bin is zero, the scale of the nearest nonzero bin is substituted to scale the nominal upper bound.
+If all bins zero, a warning is generated and interval is set to sumw.
This class is a columnar implementation of the FactorizedJetCorrector tool in
+CMSSW and FWLite. It applies a series of JECs in ascending order as defined by
+‘_level_order’, and checks for the consistency of input corrections.
+
It implements the jet energy correction definition specified in the JEC TWiki.
This class is a columnar implementation of the JetCorrectionUncertainty tool in
+CMSSW and FWLite. It calculates the jet energy scale uncertainty for a corrected jet
+in a given binning.
+
It implements the jet energy correction definition specified in the JES Uncertainty TWiki.
Returns the set of uncertainties for all input jets for all the levels (== sources)
+
Use it like:
+
juncs=uncertainty.getUncertainty(JetProperty1=jet.property1,...)
+#'juncs' will be formatted like [('SourceName', [[up_val down_val]_jet1 ... ]), ...]
+#in a zip iterator
+
This class is a columnar implementation of the JetResolution tool in
+CMSSW and FWLite. It calculates the jet energy resolution for a corrected jet
+in a given binning.
+
It implements the jet energy correction definition specified in the JER TWiki.
This class is a columnar implementation of the JetResolutionScaleFactor tool in
+CMSSW and FWLite. It calculates the jet energy resolution scale factor for a
+corrected jet in a given binning.
+
It implements the jet energy scale factor definition specified in the JER TWiki.
The evaluator class serves as a single point of extry for
+looking up values of histograms and other functions read in
+with the extractor class. Stored look ups can be indexed by
+name and then called through an overloaded __call__ function.
+
Example:
+
#assuming 'eta' and 'pt' are array like objects
+wgts="testSF2d scalefactors_Tight_Electron tests/samples/testSF2d.histo.root"
+extractor.add_weight_sets([wgts])
+extractor.finalize(reduce_list=['testSF2d'])
+evaluator=extractor.make_evaluator()
+out=evaluator["testSF2d"](eta,pt)
+
+
+
The returned value has the same shape as the input arguments.
+
lookup_types is a map of possible contructors for extracted data
expects a list of text lines to be formatted as '<local name> <name> <weights file>' allows * * <file> and <prefix> * <file> to do easy imports of whole file
expects a list of text lines to be formatted as ‘<local name> <name> <weights file>’
+allows * * <file> and <prefix> * <file> to do easy imports of whole file
The basic schema is essentially unchanged from the original ROOT file.
+A top-level base.NanoEvents object is returned, where each original branch
+form is accessible as a direct descendant.
The Delphes schema is built from all branches found in the supplied file, based on
+the naming pattern of the branches. The following additional arrays are constructed:
+
+
Any branches named {name}_size are assumed to be counts branches and converted to offsets o{name}
The NanoAOD schema is built from all branches found in the supplied file, based on
+the naming pattern of the branches. The following additional arrays are constructed:
+
+
Any branches named n{name} are assumed to be counts branches and converted to offsets o{name}
+
Any local index branches with names matching {source}_{target}Idx* are converted to global indexes for the event chunk (postfix G)
+
Any nested_items are constructed, if the necessary branches are available
+
Any special_items are constructed, if the necessary branches are available
+
+
From those arrays, NanoAOD collections are formed as collections of branches grouped by name, where:
+
+
one branch exists named name and no branches start with name_, interpreted as a single flat array;
+
one branch exists named name, one named n{name}, and no branches start with name_, interpreted as a single jagged array;
+
no branch exists named {name} and many branches start with name_*, interpreted as a flat table; or
+
one branch exists named n{name} and many branches start with name_*, interpreted as a jagged table.
+
+
Collections are assigned mixin types according to the mixins mapping.
+All collections are then zipped into one base.NanoEvents record and returned.
+
There is a class-level variable warn_missing_crossrefs which will alter the behavior of
+NanoAODSchema. If warn_missing_crossrefs is true then when a missing global index cross-ref
+target is encountered a warning will be issued. Regardless, the cross-reference is dropped.
Cross-references, where an index is to be interpreted with respect to another collection
+
Each such cross-reference will be converted to a global indexer, so that arbitrarily sliced events
+can still resolve the indirection back the parent events
file (str, pathlib.Path, pyarrow.NativeFile, or python file-like) – The filename or already opened file using e.g. uproot.open()
+
treepath (str, optional) – Name of the tree to read in the file
+
entry_start (int, optional) – Start at this entry offset in the tree (default 0)
+
entry_stop (int, optional) – Stop at this entry offset in the tree (default end of tree)
+
runtime_cache (dict, optional) – A dict-like interface to a cache object. This cache is expected to last the
+duration of the program only, and will be used to hold references to materialized
+awkward arrays, etc.
+
persistent_cache (dict, optional) – A dict-like interface to a cache object. Only bare numpy arrays will be placed in this cache,
+using globally-unique keys.
+
schemaclass (BaseSchema) – A schema class deriving from BaseSchema and implementing the desired view of the file
+
metadata (dict, optional) – Arbitrary metadata to add to the base.NanoEvents object
+
parquet_options (dict, optional) – Any options to pass to pyarrow.parquet.ParquetFile
+
access_log (list, optional) – Pass a list instance to record which branches were lazily accessed by this instance
Quickly build NanoEvents from a pre-loaded array source
+
+
Parameters:
+
+
array_source (Mapping[str, awkward.Array]) – A mapping of names to awkward arrays, it must have a metadata attribute with uuid,
+num_rows, and path sub-items.
+
entry_start (int, optional) – Start at this entry offset in the tree (default 0)
+
entry_stop (int, optional) – Stop at this entry offset in the tree (default end of tree)
+
runtime_cache (dict, optional) – A dict-like interface to a cache object. This cache is expected to last the
+duration of the program only, and will be used to hold references to materialized
+awkward arrays, etc.
+
persistent_cache (dict, optional) – A dict-like interface to a cache object. Only bare numpy arrays will be placed in this cache,
+using globally-unique keys.
+
schemaclass (BaseSchema) – A schema class deriving from BaseSchema and implementing the desired view of the file
+
metadata (dict, optional) – Arbitrary metadata to add to the base.NanoEvents object
+
access_log (list, optional) – Pass a list instance to record which branches were lazily accessed by this instance
file (str or uproot.reading.ReadOnlyDirectory) – The filename or already opened file using e.g. uproot.open()
+
treepath (str, optional) – Name of the tree to read in the file
+
entry_start (int, optional) – Start at this entry offset in the tree (default 0)
+
entry_stop (int, optional) – Stop at this entry offset in the tree (default end of tree)
+
runtime_cache (dict, optional) – A dict-like interface to a cache object. This cache is expected to last the
+duration of the program only, and will be used to hold references to materialized
+awkward arrays, etc.
+
persistent_cache (dict, optional) – A dict-like interface to a cache object. Only bare numpy arrays will be placed in this cache,
+using globally-unique keys.
+
schemaclass (BaseSchema) – A schema class deriving from BaseSchema and implementing the desired view of the file
+
metadata (dict, optional) – Arbitrary metadata to add to the base.NanoEvents object
+
uproot_options (dict, optional) – Any options to pass to uproot.open
+
access_log (list, optional) – Pass a list instance to record which branches were lazily accessed by this instance
Cross-references, where an index is to be interpreted with respect to another collection
+
Each such cross-reference will be converted to a global indexer, so that arbitrarily sliced events
+can still resolve the indirection back the parent events
This is a schema for the ATLAS DAOD_PHYSLITE derivation.
+Closely following schemas.nanoaod.NanoAODSchema, it is mainly build from
+naming patterns where the “Analysis” prefix has been removed, so the
+collections will be named Electrons, Muons, instead of AnalysisElectrons,
+AnalysisMunos, etc. The collection fields correspond to the “Aux” and
+“AuxDyn” columns.
+
Collections are assigned mixin types according to the mixins mapping.
+All collections are then zipped into one base.NanoEvents record and returned.
+
Cross references are build from ElementLink columns. Global indices are
+created dynamically, using an _eventindex field that is attached to
+each collection.
The TreeMaker schema is built from all branches found in the supplied file,
+based on the naming pattern of the branches. There are two steps of to the
+generation of array collections:
+
+
Objects with vector-like quantities (momentum, coordinate points) in the
+TreeMaker n-tuples are stored using ROOT PtEtaPhiEVectors and XYZPoint
+classes with maximum TTree splitting. These variable branches are grouped
+into a single collection with the original object name, with the
+corresponding coordinate variables names mapped to the standard variable
+names for coffea.nanoevents.methods.vector behaviors. For example:
+- The “Jets” branch in a TreeMaker n-tuple branch stores ‘PtEtaPhiEVector’s
+
+
corresponding to the momentum of AK4 jets. The resulting collection after
+this first step would contain the vector variables in the form of
+Jets.pt, Jets.eta, Jets.phi, Jets.energy, and addition vector quantities
+(px) can be accessed via the usual vector behavior methods.
+
+
+
The “PrimaryVertices” branch in a TreeMaker n-tuple branch stores
+‘XYZPoint’s corresponding to the coordinates of the primary vertices, The
+resulting collection after this first step wold contain the coordinate
+variables in the form of PrimaryVertices.x, PrimaryVertices.y,
+PrimaryVertices.z.
+
+
+
Extended quantities of physic objects are stored in the format
+<Object>_<variable>, such as “Jets_jecFactor”. Such variables will be
+merged into the collection <Object>, so the branch “Jets_jetFactor” will be
+access to in the array format as “Jets.jecFactor”. An exception to the
+
+
All collections are then zipped into one base.NanoEvents record and
+returned.
Converting a TreeMakerSchema event into something that is uproot
+writeable. Based off the discussion thread here [1], but added specific
+cased to handled the nested structures define for TreeMaker n-tuples.
+[1] https://github.com/CoffeaTeam/coffea/discussions/735
name: str, name of the systematic variation / uncertainty source kind: str, the name of the kind of systematic variation what: Union[str, List[str], Tuple[str]], name what gets varied, this could be a list or tuple of column names varying_function: Union[function, bound method], a function that describes how 'what' is varied, it must close over all non-event-data arguments.
name: str, name of the systematic variation / uncertainty source
+kind: str, the name of the kind of systematic variation
+what: Union[str, List[str], Tuple[str]], name what gets varied,
+
+
this could be a list or tuple of column names
+
+
varying_function: Union[function, bound method], a function that describes how ‘what’ is varied, it must close over all non-event-data arguments.
This describes how a systematic uncertainty needs to be evaluated in the context of other systematic uncertainties.
+i.e. Do you iterate over this keeping all others fixed or do you need to have correlations with other (subsets of) systematics.
NanoAOD generator-level particle object, including parent and child self-references
+
Parent and child self-references are constructed from the genPartIdxMother column, where
+for each entry, the mother entry index is recorded, or -1 if no mother exists.
flags (str or list) – A list of flags that are required to be set true. If the first argument
+is a list, it is expanded and subsequent arguments ignored.
+Possible flags are enumerated in the FLAGS attribute
A heavy emphasis towards a momentum vector interpretation is assumed.
+(+, -, -, -) metric
+This mixin class requires the parent class to provide items x, y, z, and t.
Apply a Lorentz boost given by the ThreeVectorother and return it
+
Note that this follows the convention that, for example in order to boost
+a vector into its own rest frame, one needs to use the negative of its boostvec
Return a list of a metric evaluated between this object and another.
+
The two arrays should be broadcast-compatible on all axes other than the specified
+axis, which will be used to form a cartesian product. If axis=None, broadcast arrays directly.
+The return shape will be that of self with a new axis with shape of other appended
+at the specified axis depths.
+
+
Parameters:
+
+
other (awkward.Array) – Another array with same shape in all but axis
+
axis (int, optional) – The axis to form the cartesian product (default 1). If None, the metric
+is directly evaluated on the input arrays (i.e. they should broadcast)
+
metric (callable) – A function of two arguments, returning a scalar. The default metric is delta_r.
+
return_combinations (bool) – If True return the combinations of inputs as well as an unzipped tuple
Finds item in other satisfying min(metric(self,other)).
+The two arrays should be broadcast-compatible on all axes other than the specified
+axis, which will be used to form a cartesian product. If axis=None, broadcast arrays directly.
+The return shape will be that of self.
+
+
Parameters:
+
+
other (awkward.Array) – Another array with same shape in all but axis
+
axis (int, optional) – The axis to form the cartesian product (default 1). If None, the metric
+is directly evaluated on the input arrays (i.e. they should broadcast)
+
metric (callable) – A function of two arguments, returning a scalar. The default metric is delta_r.
+
return_metric (bool, optional) – If true, return both the closest object and its metric (default false)
+
threshold (Number, optional) – If set, any objects with metric>threshold will be masked from the result
Accumulators are abstract objects that enable the reduce stage of the typical map-reduce
+scaleout that we do in Coffea. One concrete example is a histogram. The idea is that an
+accumulator definition holds enough information to be able to create an empty accumulator
+(the identity() method) and add two compatible accumulators together (the add() method).
+The former is not strictly necessary, but helps with book-keeping. Here we show an example usage
+of a few accumulator types. An arbitrary-depth nesting of dictionary accumulators is supported, much
+like the behavior of directories in ROOT hadd.
retries (int, optional) – Number of retries for failed tasks (default: 3)
+
heavy_input (serializable, optional) – Any value placed here will be broadcast to workers and joined to input
+items in a tuple (item, heavy_input) that is passed to function.
+
function_name (str, optional) – Name of the function being passed
function (callable) – A function to be called on each input, which returns an accumulator instance
+
accumulator (Accumulatable) – An accumulator to collect the output of the function
+
pool (concurrent.futures.Executor class or instance, optional) – The type of futures executor to use, defaults to ProcessPoolExecutor.
+You can pass an instance instead of a class to re-use an executor
+
workers (int, optional) – Number of parallel processes for futures (default 1)
+
status (bool, optional) – If true (default), enable progress bar
+
desc (str, optional) – Label of progress description (default: ‘Processing’)
+
unit (str, optional) – Label of progress bar bar unit (default: ‘items’)
+
compression (int, optional) – Compress accumulator outputs in flight with LZ4, at level specified (default 1)
+Set to None for no compression.
+
recoverable (bool, optional) – Instead of raising Exception right away, the exception is captured and returned
+up for custom parsing. Already completed items will be returned as well.
merging (bool | tuple(int, int, int), optional) – Enables submitting intermediate merge jobs to the executor. Format is
+(n_batches, min_batch_size, max_batch_size). Passing True will use default: (5, 4, 100),
+aka as they are returned try to split completed jobs into 5 batches, but of at least 4 and at most 100 items.
+Default is False - results get merged as they finish in the main process.
+
nparts (int, optional) – Number of merge jobs to create at a time. Also pass via ``merging(X, …, …)’’
+
minred (int, optional) – Minimum number of items to merge in one job. Also pass via ``merging(…, X, …)’’
+
maxred (int, optional) – maximum number of items to merge in one job. Also pass via ``merging(…, …, X)’’
+
mergepool (concurrent.futures.Executor class or instance | int, optional) – Supply an additional executor to process merge jobs indepedently.
+An int will be interpretted as ProcessPoolExecutor(max_workers=int).
+
tailtimeout (int, optional) – Timeout requirement on job tails. Cancel all remaining jobs if none have finished
+in the timeout window.
One can access branches either through df["bname"] or df.bname, although
+the latter is restricted to branches that do not start with a leading underscore.
+Keeps track of values accessed, in the materialized attribute.
+
+
Parameters:
+
+
tree (uproot.TTree) – Tree to read
+
entrystart (int, optional) – First entry to read, default: 0
+
entrystop (int, optional) – Last entry to read, default None (read to end)
+
preload_items (iterable) – Force preloading of a set of columns from the tree
+
metadata (Mapping) – Additional metadata for the dataframe
The NanoAOD schema is built from all branches found in the supplied file, based on
+the naming pattern of the branches. The following additional arrays are constructed:
+
+
Any branches named n{name} are assumed to be counts branches and converted to offsets o{name}
+
Any local index branches with names matching {source}_{target}Idx* are converted to global indexes for the event chunk (postfix G)
+
Any nested_items are constructed, if the necessary branches are available
+
Any special_items are constructed, if the necessary branches are available
+
+
From those arrays, NanoAOD collections are formed as collections of branches grouped by name, where:
+
+
one branch exists named name and no branches start with name_, interpreted as a single flat array;
+
one branch exists named name, one named n{name}, and no branches start with name_, interpreted as a single jagged array;
+
no branch exists named {name} and many branches start with name_*, interpreted as a flat table; or
+
one branch exists named n{name} and many branches start with name_*, interpreted as a jagged table.
+
+
Collections are assigned mixin types according to the mixins mapping.
+All collections are then zipped into one base.NanoEvents record and returned.
+
There is a class-level variable warn_missing_crossrefs which will alter the behavior of
+NanoAODSchema. If warn_missing_crossrefs is true then when a missing global index cross-ref
+target is encountered a warning will be issued. Regardless, the cross-reference is dropped.
Cross-references, where an index is to be interpreted with respect to another collection
+
Each such cross-reference will be converted to a global indexer, so that arbitrarily sliced events
+can still resolve the indirection back the parent events
This class can store several boolean masks (cuts, selections) and
+evaluate arbitrary combinations of the requirements in an CPU-efficient way
+
+
Parameters:
+
dtype (str) – internal bitwidth of mask vector, which governs the maximum
+number of boolean masks storable in this object.
+By default, up to 64 masks can be stored, but smaller values
+for the numpy.dtype may be more efficient.
compression (int, optional) – Compress accumulator outputs in flight with LZ4, at level specified (default 1)
+Set to None for no compression.
+
recoverable (bool, optional) – Instead of raising Exception right away, the exception is captured and returned
+up for custom parsing. Already completed items will be returned as well.
+
merging (bool | tuple(int, int, int), optional) – Enables submitting intermediate merge jobs to the executor. Format is
+(n_batches, min_batch_size, max_batch_size). Passing True will use default: (5, 4, 100),
+aka as they are returned try to split completed jobs into 5 batches, but of at least 4 and at most 100 items.
+Default is False - results get merged as they finish in the main process.
+
jobs_executors (list | "all" optional) – Labels of the executors (from dfk.config.executors) that will process main jobs.
+Default is ‘all’. Recommended is ['jobs'], while passing label='jobs' to the primary executor.
+
merges_executors (list | "all" optional) – Labels of the executors (from dfk.config.executors) that will process main jobs.
+Default is ‘all’. Recommended is ['merges'], while passing label='merges' to the executor dedicated towards merge jobs.
+
tailtimeout (int, optional) – Timeout requirement on job tails. Cancel all remaining jobs if none have finished
+in the timeout window.
The various data delivery mechanisms (spark, striped, uproot, uproot+futures, condor, …)
+receive such an object and the appropriate metadata to deliver NanoEvents to it.
+It is expected that the entire processor object can be serializable (check with save)
+No attempt should be made to track state inside an instance of ProcessorABC, it is to be
+treated simply as a bundle of methods.
A tool to run a processor using uproot for data delivery
+
A convenience wrapper to submit jobs for a file set, which is a
+dictionary of dataset: [file list] entries. Supports only uproot TTree
+reading, via NanoEvents or LazyDataFrame. For more customized processing,
+e.g. to read other objects from the files and pass them into data frames,
+one can write a similar function in their user code.
+
+
Parameters:
+
+
executor (ExecutorBase instance) – Executor, which implements a callable with inputs: items, function, accumulator
+and performs some action equivalent to:
+foriteminitems:accumulator+=function(item)
+
pre_executor (ExecutorBase instance) – Executor, used to calculate fileset metadata
+Defaults to executor
+
chunksize (int, optional) – Maximum number of entries to process at a time in the data frame, default: 100k
+
maxchunks (int, optional) – Maximum number of chunks to process per dataset
+Defaults to processing the whole dataset
+
metadata_cache (mapping, optional) – A dict-like object to use as a cache for (file, tree) metadata that is used to
+determine chunking. Defaults to a in-memory LRU cache that holds 100k entries
+(about 1MB depending on the length of filenames, etc.) If you edit an input file
+(please don’t) during a session, the session can be restarted to clear the cache.
+
dynamic_chunksize (dict, optional) – Whether to adapt the chunksize for units of work to run in the targets given.
+Currently supported are ‘wall_time’ (in seconds), and ‘memory’ (in MB).
+E.g., with {“wall_time”: 120, “memory”: 2048}, the chunksize will
+be dynamically adapted so that processing jobs each run in about
+two minutes, using two GB of memory. (Currently only for the WorkQueueExecutor.)
fileset (dict) – A dictionary {dataset:[file,file],}
+Optionally, if some files’ tree name differ, the dictionary can be specified:
+{dataset:{'treename':'name','files':[file,file]},}
+
treename (str) – name of tree inside each root file, can be None;
+treename can also be defined in fileset, which will override the passed treename
+
processor_instance (ProcessorABC) – An instance of a class deriving from ProcessorABC
fileset (dict) – A dictionary {dataset:[file,file],}
+Optionally, if some files’ tree name differ, the dictionary can be specified:
+{dataset:{'treename':'name','files':[file,file]},}
+
treename (str) – name of tree inside each root file, can be None;
+treename can also be defined in fileset, which will override the passed treename
A dictionary {dataset:[file,file],}
+Optionally, if some files’ tree name differ, the dictionary can be specified:
+{dataset:{'treename':'name','files':[file,file]},}
+
A single file name
+
File chunks for self.preprocess()
+
Chunk generator
+
+
+
treename (str, optional) – name of tree inside each root file, can be None;
+treename can also be defined in fileset, which will override the passed treename
+Not needed if processing premade chunks
+
processor_instance (ProcessorABC) – An instance of a class deriving from ProcessorABC
The TreeMaker schema is built from all branches found in the supplied file,
+based on the naming pattern of the branches. There are two steps of to the
+generation of array collections:
+
+
Objects with vector-like quantities (momentum, coordinate points) in the
+TreeMaker n-tuples are stored using ROOT PtEtaPhiEVectors and XYZPoint
+classes with maximum TTree splitting. These variable branches are grouped
+into a single collection with the original object name, with the
+corresponding coordinate variables names mapped to the standard variable
+names for coffea.nanoevents.methods.vector behaviors. For example:
+- The “Jets” branch in a TreeMaker n-tuple branch stores ‘PtEtaPhiEVector’s
+
+
corresponding to the momentum of AK4 jets. The resulting collection after
+this first step would contain the vector variables in the form of
+Jets.pt, Jets.eta, Jets.phi, Jets.energy, and addition vector quantities
+(px) can be accessed via the usual vector behavior methods.
+
+
+
The “PrimaryVertices” branch in a TreeMaker n-tuple branch stores
+‘XYZPoint’s corresponding to the coordinates of the primary vertices, The
+resulting collection after this first step wold contain the coordinate
+variables in the form of PrimaryVertices.x, PrimaryVertices.y,
+PrimaryVertices.z.
+
+
+
Extended quantities of physic objects are stored in the format
+<Object>_<variable>, such as “Jets_jecFactor”. Such variables will be
+merged into the collection <Object>, so the branch “Jets_jetFactor” will be
+access to in the array format as “Jets.jecFactor”. An exception to the
+
+
All collections are then zipped into one base.NanoEvents record and
+returned.
Converting a TreeMakerSchema event into something that is uproot
+writeable. Based off the discussion thread here [1], but added specific
+cased to handled the nested structures define for TreeMaker n-tuples.
+[1] https://github.com/CoffeaTeam/coffea/discussions/735
Container for event weights and associated systematic shifts
+
This container keeps track of correction factors and systematic
+effects that can be encoded as multiplicative modifiers to the event weight.
+All weights are stored in vector form.
+
+
Parameters:
+
+
size (int) – size of the weight arrays to be handled (i.e. the number of events / instances).
+
storeIndividual (bool, optional) – store not only the total weight + variations, but also each individual weight.
+Default is false.
weight (numpy.ndarray) – the nominal event weight associated with the correction
+
weightUp (numpy.ndarray, optional) – weight with correction uncertainty shifted up (if available)
+
weightDown (numpy.ndarray, optional) – weight with correction uncertainty shifted down. If weightUp is supplied, and
+the correction uncertainty is symmetric, this can be set to None to auto-calculate
+the down shift as 1/weightUp.
+
shift (bool, optional) – if True, interpret weightUp and weightDown as a realtive difference (additive) to the
+nominal value
+
+
+
+
+
Note
+
weightUp and weightDown are assumed to be rvalue-like and may be modified in-place by this function
Return a partial weight by multiplying a subset of all weights.
+Can be operated either by specifying weights to include or
+weights to exclude, but not both at the same time. The method
+can only be used if the individual weights are stored via the
+storeIndividual argument in the Weights initializer.
+
+
Parameters:
+
+
include (list) – Weight names to include, defaults to []
+
exclude (list) – Weight names to exclude, defaults to []
+
+
+
Returns:
+
weight – The weight vector, corresponding to only the effect of the
+corrections specified.
modifier (str, optional) – if supplied, provide event weight corresponding to a particular
+systematic uncertainty shift, of form str(name+'Up') or (Down)
+
+
Returns:
+
weight – The weight vector, possibly modified by the effect of a given systematic variation.
one of ‘fixed’, ‘max-seen’, or ‘max-throughput’. Default is ‘max-seen’.
+Sets the strategy to automatically allocate resources to tasks.
+- ‘fixed’: allocate cores, memory, and disk specified for each task.
+- ‘max-seen’ or ‘auto’: use the cores, memory, and disk given as maximum values to allocate,
+
+
but first try each task by allocating the maximum values seen. Leads
+to a good compromise between parallelism and number of retries.
+
+
+
+
’max-throughput’: Like max-seen, but first tries the task with an
allocation that maximizes overall throughput.
+
+
+
+
+
If resources_mode is other than ‘fixed’, preprocessing and
+accumulation tasks always use the ‘max-seen’ strategy, as the
+former tasks always use the same resources, the latter has a
+distribution of resources that increases over time.
+
+
split_on_exhaustion (bool) – Whether to split a processing task in half according to its chunksize when it exhausts its
+the cores, memory, or disk allocated to it. If False, a task that exhausts resources
+permanently fails. Default is True.
+
fast_terminate_workers (int) – Terminate workers on which tasks have been running longer than average.
+The time limit is computed by multiplying the average runtime of tasks
+by the value of ‘fast_terminate_workers’. Since there are
+legitimately slow tasks, no task may trigger fast termination in
+two distinct workers. Less than 1 disables it.
+
manager_name (str) – Name to refer to this work queue manager.
+Sets port to 0 (any available port) if port not given.
+
port (int or tuple(int, int)) – Port number or range (inclusive of ports )for work queue manager program.
+Defaults to 9123 if manager_name not given.
+
password_file (str) – Location of a file containing a password used to authenticate workers.
+
ssl (bool or tuple(str, str)) – Enable ssl encryption between manager and workers. If a tuple, then it
+should be of the form (key, cert), where key and cert are paths to the files
+containing the key and certificate in pem format. If True, auto-signed temporary
+key and cert are generated for the session.
+
extra_input_files (list) – A list of files in the current working directory to send along with each task.
+Useful for small custom libraries and configuration files needed by the processor.
+
x509_proxy (str) – Path to the X509 user proxy. If None (the default), use the value of the
+environment variable X509_USER_PROXY, or fallback to the file /tmp/x509up_u${UID} if
+exists. If False, disables the default behavior and no proxy is sent.
+
environment_file (optional, str) – Conda python environment tarball to use. If not given, assume that
+the python environment is already setup at the execution site.
+
wrapper (str) – Wrapper script to run/open python environment tarball. Defaults to python_package_run found in PATH.
+
treereduction (int) – Number of processed chunks per accumulation task. Defaults is 20.
+
verbose (bool) – If true, emit a message on each task submission and completion.
+Default is false.
+
print_stdout (bool) – If true (default), print the standard output of work queue task on completion.
stats_log (str) – Filename for tasks statistics output
+
transactions_log (str) – Filename for tasks lifetime reports output
+
tasks_accum_log (str) – Filename for the log of tasks that have been processed and accumulated.
+
filepath (str) – Path to the parent directory where to create the staging directory.
+Default is “.” (current working directory).
+
custom_init (function, optional) – A function that takes as an argument the queue’s WorkQueue object.
+The function is called just before the first work unit is submitted
+to the queue.
value (numpy.ndarray) – The identity value array, which should be an empty ndarray
+with the desired row shape. The column dimension will correspond to
+the first index of value shape.
A convenience wrapper to submit jobs for spark datasets, which is a
+dictionary of dataset: [file list] entries. Presently supports reading of
+parquet files converted from root. For more customized processing,
+e.g. to read other objects from the files and pass them into data frames,
+one can write a similar function in their user code.
The processor instance must define all the columns in data and MC that it reads as .columns
+
+
+
executor –
anything that inherits from SparkExecutor like spark_executor
+
In general, a function that takes 3 arguments: items, function accumulator
+and performs some action equivalent to:
+for item in items: accumulator += function(item)
+
+
executor_args – arguments to send to the creation of a spark session
+
spark –
an optional already created spark instance
+
if None then we create an ephemeral spark instance using a config
+
+
partitionsize – partition size to try to aim for (coalescese only, repartition too expensive)
+
thread_workers – how many spark jobs to let fly in parallel during processing steps
This page explains concepts and terminology used within the coffea package.
+It is intended to provide a high-level overview, while details can be found in other sections of the documentation.
Columnar analysis is a paradigm that describes the way the user writes the analysis application that is best described
+in contrast to the the traditional paradigm in high-energy particle physics (HEP) of using an event loop. In an event loop, the analysis operates row-wise
+on the input data (in HEP, one row usually corresponds to one reconstructed particle collision event.) Each row
+is a structure containing several fields, such as the properties of the visible outgoing particles
+that were reconstructed in a collision event. The analysis code manipulates this structure to either output derived
+quantities or summary statistics in the form of histograms. In contrast, columnar analysis operates on individual
+columns of data spanning a chunk (partition, batch) of rows using array programming
+primitives in turn, to compute derived quantities and summary statistics. Array programming is widely used within
+the scientific python ecosystem, supported by the numpy library.
+However, although the existing scientific python stack is fully capable of analyzing rectangular arrays (i.e.
+no variable-length array dimensions), HEP data is very irregular, and manipulating it can become awkward without
+first generalizing array structure a bit. The awkward package does this,
+extending array programming capabilities to the complexity of HEP data.
In almost all HEP analyses, each row corresponds to an independent event, and it is exceptionally rare
+to need to compute inter-row derived quantites. Due to this, horizontal scale-out is almost trivial:
+each chunk of rows can be operated on independently. Further, if the output of an analysis is restricted
+to reducible accumulators such as histograms (abstracted by AccumulatorABC), then outputs can even be merged via tree reduction.
+The ProcessorABC class is an abstraction to encapsulate analysis code so that it can be easily scaled out, leaving
+the delivery of input columns and reduction of output accumulators to the coffea framework.
Often, the computation requirements of a HEP data analysis exceed the resources of a single thread of execution.
+To facilitate parallelization and allow the user to access more compute resources, coffea employs various executors
+to ease the transition between a local analysis on a small set of test data to a full-scale analysis.
+The executors roughly fall into two categories: local and distributed.
Currently, two local executors exist: iterative_executor and futures_executor.
+The iterative executor simply processes each chunk of an input dataset in turn, using the current
+python thread. The futures executor employs python multiprocessing to spawn multiple python processes
+that process chunks in parallel on the machine. Processes are used rather than threads to avoid
+performance limitations due to the CPython global interpreter lock.
The following pages are rendered jupyter notebooks that provide an overview and example usage of Coffea features.
+Each notebook builds on the previous one so it is recommended to go through them in order.
+
+
+
+
\ No newline at end of file
diff --git a/genindex.html b/genindex.html
new file mode 100644
index 000000000..85f6810d5
--- /dev/null
+++ b/genindex.html
@@ -0,0 +1,1661 @@
+
+
+
+
+
+
+
+ Index — coffea 0.7.23 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index
+
+
+
+
+
+
+
+
+
+
Index
+
+
+ _
+ | A
+ | B
+ | C
+ | D
+ | E
+ | F
+ | G
+ | H
+ | I
+ | J
+ | K
+ | L
+ | M
+ | N
+ | P
+ | R
+ | S
+ | T
+ | U
+ | V
+ | W
+ | X
+ | Y
+ | Z
+
+
Basic tools and wrappers for enabling not-too-alien syntax when running columnar Collider HEP analysis.
+
coffea is a prototype package for pulling together all the typical needs
+of a high-energy collider physics (HEP) experiment analysis using the scientific
+python ecosystem. It makes use of uproot
+and awkward-array to provide an
+array-based syntax for manipulating HEP event data in an efficient and numpythonic
+way. There are sub-packages that implement histogramming, plotting, and look-up
+table functionalities that are needed to convey scientific insight, apply transformations
+to data, and correct for discrepancies in Monte Carlo simulations compared to data.
+
coffea also supplies facilities for horizontally scaling an analysis in order to reduce
+time-to-insight in a way that is largely independent of the resource the analysis
+is being executed on. By making use of modern big-data technologies like
+Apache Spark, parsl,
+Dask , and Work Queue,
+it is possible with coffea to scale a HEP analysis from a testing
+on a laptop to: a large multi-core server, computing clusters, and super-computers without
+the need to alter or otherwise adapt the analysis code itself.
+
coffea is a HEP community project collaborating with iris-hep
+and is currently a prototype. We welcome input to improve its quality as we progress towards
+a sensible refactorization into the scientific python ecosystem and a first release. Please
+feel free to contribute at our github repo!
Coffea is a python package distributed via PyPI. A python installation is required to use coffea.
+Python version 3.6 or newer is required.
+
All functional features in each supported python version are routinely tested.
+You can see the python version you have installed by typing the following at the command prompt:
+
>>> python--version
+
+
+
or, in some cases, if both python 2 and 3 are available, you can find the python 3 version via:
+
>>> python3--version
+
+
+
coffea core functionality is routinely tested on Windows, Linux and MacOS.
+All Local executors are tested against all three platforms,
+however the Distributed executors are not routinely tested on Windows.
+
Coffea starts from v0.5.0 in the PyPI repository since before v0.5.0 it was hosted as fnal-column-analysis-tools. If you are still using fnal-column-analysis-tools, please move to coffea!
To update a previously installed coffea to a newer version, use: pipinstall--upgradecoffea
+Although not required, it is recommended to also install Jupyter, as it provides a more interactive development environment.
+The installation procedure is essentially identical as above: pipinstalljupyter. (If you use conda, condainstalljupyter is a better option.)
+
In rare cases, you may find that the pip executable in your path does not correspond to the same python installation as the python executable. This is a sign of a broken python environment. However, this can be bypassed by using the syntax python-mpip... in place of pip....
Coffea supports several optional components that require additional package installations.
+In particular, all of the Distributed executors require additional packages.
+The necessary dependencies can be installed easily via pip using the setuptools extras facility:
Virtual environments are a good way to isolate python environments, and ensure no hidden dependencies.
+You can find more information at https://docs.python.org/3/library/venv.html
Although the local installation can work anywhere, if the base environment does not already have most of the coffea dependencies, then the user-local package directory can become quite bloated.
+An option to avoid this bloat is to use a base python environment provided via CERN LCG, which is available on any system that has the cvmfs directory /cvmfs/sft.cern.ch/ mounted.
+Simply source a LCG release (shown here: 98python3) and install:
+
# check your platform: CC7 shown below, for SL6 it would be "x86_64-slc6-gcc8-opt"
+source/cvmfs/sft.cern.ch/lcg/views/LCG_98python3/x86_64-centos7-gcc9-opt/setup.sh# or .csh, etc.
+pipinstall--usercoffea
+
+
+
This method can be fragile, since the LCG-distributed packages may conflict with the coffea dependencies. In general it is better to define your own environment or use an image.
In some instances, it may be useful to have a self-contained environment that can be relocated.
+One use case is for users of coffea that do not have access to a distributed compute cluster that is compatible with
+one of the coffea distributed executors. Here, a fallback solution can be found by creating traditional batch jobs (e.g. condor)
+which then use coffea local executors, possibly multi-threaded. In this case, often the user-local python package directory
+is not available from batch workers, so a portable python enviroment needs to be created.
+Annoyingly, python virtual environments are not portable by default due to several hardcoded paths in specific locations, however
+there are two workarounds presented below. In both cases, we make a virtual environment that starts from a non-system base
+python environment to lower the amount of needed installations in the virtual environment. One can always start a venv from scratch,
+but the number of coffea dependencies makes the installation rather large, up to a few hundred MB.
If we start from one of the singularity containers from the Pre-built images section, we don’t have to install nearly as much
+software in our virtual environment, letting the container image take care of the majority of the codebase. For example, the following
+code starts from the coffea-dask image and adds a special python module that is not included in the base image:
This creates a virtual environmennt myenv and a directory with the same name where the extra python module h5py will be
+installed. At this point, the terminal prompt will look like (myenv)Singularity>, indicating you are inside a singularity
+image and have myenv activated. Next time you log in, only lines 1, 2, and 4 need to be re-executed.
+
If using HTCondor for job submission, you can create a tarball of the virtual environment directory and then submit condor
+jobs using the +SingularityImageHTCondor option.
+Note that this option is not enabled by default in HTCondor installations, so you may need to talk to your site administrator to be
+able to use this option. You will also need to create a small wrapper script to re-source the environment to have the job use the
+same environment as your interactive container.
+A complete example that runs at FNAL LPC is shown in this gist.
There are not many locations to edit to make a venv portable, and some sed hacks can save the day.
+Here is an example of a bash script that installs coffea on top of the LCG 98python3 software stack inside a portable virtual environment,
+with the caveat that cvmfs must be visible from batch workers:
+
#!/usr/bin/env bash
+NAME=coffeaenv
+LCG=/cvmfs/sft.cern.ch/lcg/views/LCG_98python3/x86_64-centos7-gcc9-opt
+
+source$LCG/setup.sh
+# following https://aarongorka.com/blog/portable-virtualenv/, an alternative is https://github.com/pantsbuild/pex
+python-mvenv--copies$NAME
+source$NAME/bin/activate
+LOCALPATH=$NAME$(python-c'import sys; print(f"/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages")')
+exportPYTHONPATH=${LOCALPATH}:$PYTHONPATH
+python-mpipinstallsetuptoolspipwheel--upgrade
+python-mpipinstallcoffea
+sed-i'1s/#!.*python$/#!\/usr\/bin\/env python/'$NAME/bin/*
+sed-i'40s/.*/VIRTUAL_ENV="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}" )")" \&\& pwd)"/'$NAME/bin/activate
+sed-i"2a source ${LCG}/setup.sh"$NAME/bin/activate
+sed-i"3a export PYTHONPATH=${LOCALPATH}:\$PYTHONPATH"$NAME/bin/activate
+tar-zcf${NAME}.tar.gz${NAME}
+
+
+
The resulting tarball size is about 60 MB.
+An example batch job wrapper script is:
Note that this environment only functions from the working directory of the wrapper script due to having relative paths.
+Unless you install jupyter into this environment (which may bloat the tarball–LCG98 jupyter is reasonably recent), it is not visible inside the LCG jupyter server. From a shell with the virtual environment activated, you can execute:
+
python-mipykernelinstall--user--name=coffeaenv
+
+
+
to make a new kernel available that uses this environment.
coffea.hist is a histogram filling, transformation, and plotting sub-package, utilizing
+numpy arrays for storage and matplotlib plotting routines for visualization.
+
Features found in this package are similar to those found in
+packages such as histbook (deprecated),
+boost-histogram (in development),
+physt, and built-in numpy
+histogram utilities.
These tools are currently tailored to the CMS experiment
+data formats, however they could be generalized and/or compartmentalized
+into a standalone package.
This provides just a Lorentz vector with charge, but maybe
+in the future it will provide some sort of composite candiate building tool
+that automatically resolves duplicates in the chain.
These mixins will eventually be superceded by the vector library,
+which will hopefully be feature-compatible. The 2D vector provides cartesian and polar coordinate attributes,
+where r represents the polar distance from the origin.. The 3D vector provides cartesian and spherical coordinates,
+where rho represents the 3D distance from the origin and r is the axial distance from the z axis, so that it can
+subclass the 2D vector. The Lorentz vector also subclasses the 3D vector, adding t as the fourth
+cartesian coordinate. Aliases typical of momentum vectors are also provided.
Now that we know how to access data with NanoEvents and apply corrections, let’s go through some useful columnar analysis tools and idioms for building accumulators, namely, the eventual output of a coffea processor that has natural “merge” semantics. The most familiar type of accumulator is the histogram, but other types are useful in certain contexts.
+
We’ll use our small sample file to demonstrate the utilities, although it won’t be very interesting to analyze
To generate some mock systematics, we’ll use one of the scale factors from the applying_corrections notebook (note you will have to at least execute the cell that downloads test data in that notebook for this to work)
This is a container for event weights and associated systematic shifts, which helps track the product of the weights (i.e. the total event weight to be used for filling histograms) as well as systematic variations to that product. Here we demo its use by constructing an event weight consisting of the generator weight, the \(\alpha_s\) uncertainty variation, and the electron ID scale factor with its associated systematic.
+
+
[3]:
+
+
+
fromcoffea.analysis_toolsimportWeights
+
+weights=Weights(len(events))
+
+weights.add("genWeight",events.genWeight)
+
+weights.add(
+ "alphaS",
+ # in NanoAOD, the generator weights are already stored with respect to nominal
+ weight=np.ones(len(events)),
+ # 31 => alphas(MZ)=0.1165 central value; 32 => alphas(MZ)=0.1195
+ # per https://lhapdfsets.web.cern.ch/current/PDF4LHC15_nnlo_30_pdfas/PDF4LHC15_nnlo_30_pdfas.info
+ # which was found by looking up the LHA ID in events.LHEPdfWeight.__doc__
+ weightUp=events.LHEPdfWeight[:,32],
+ weightDown=events.LHEPdfWeight[:,31],
+)
+
+eleSF=evaluator["scalefactors_Tight_Electron"](events.Electron.eta,events.Electron.pt)
+eleSFerror=evaluator["scalefactors_Tight_Electron_error"](events.Electron.eta,events.Electron.pt)
+weights.add(
+ "eleSF",
+ # the event weight is the product of the per-electron weights
+ # note, in a real analysis we would first have to select electrons of interest
+ weight=ak.prod(eleSF,axis=1),
+ weightUp=ak.prod(eleSF+eleSFerror,axis=1),
+)
+
+
+
+
A WeightStatistics object tracks the smallest and largest weights seen per type, as well as some other summary statistics. It is kept internally and can be accessed via weights.weightStatistics. This object is addable, so it can be used in an accumulator.
This class can store several boolean arrays in a memory-efficient mannner and evaluate arbitrary combinations of boolean requirements in an CPU-efficient way. Supported inputs include 1D numpy or awkward arrays. This makes it a good tool to form analysis signal and control regions, and to implement cutflow or “N-1” plots.
+
Below we create a packed selection with some typical selections for a Z+jets study, to be used later to form same-sign and opposite-sign \(ee\) and \(\mu\mu\) event categories/regions.
+
+
[8]:
+
+
+
fromcoffea.analysis_toolsimportPackedSelection
+
+selection=PackedSelection()
+
+selection.add("twoElectron",ak.num(events.Electron)==2)
+selection.add("eleOppSign",ak.sum(events.Electron.charge,axis=1)==0)
+selection.add("noElectron",ak.num(events.Electron)==0)
+
+selection.add("twoMuon",ak.num(events.Muon)==2)
+selection.add("muOppSign",ak.sum(events.Muon.charge,axis=1)==0)
+selection.add("noMuon",ak.num(events.Muon)==0)
+
+
+selection.add(
+ "leadPt20",
+ # assuming one of `twoElectron` or `twoMuon` is imposed, this implies at least one is above threshold
+ ak.any(events.Electron.pt>=20.0,axis=1)|ak.any(events.Muon.pt>=20.0,axis=1)
+)
+
+print(selection.names)
+
To evaluate a boolean mask (e.g. to filter events) we can use the selection.all(*names) function, which will compute the AND of all listed boolean selections
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
Prior to coffea 0.7.2, accumulators were more explicit in that they inherited from AccumulatorABC. Such accumulators are still supported, but now the requirements for an acuumulator output of a processor have been relaxed to the protocol:
Let’s build an output accumulator that stores, per dataset: - the sum of weights for the events processed, to use for later luminosity-normalizing the yields; - a histogram of the dilepton invariant mass, with category axes for various selection regions of interest and systematics; and - the weight statistics, for debugging purposes
The cell below demonstrates that indeed this output is accumulatable. So if we were to return such a result from a coffea processor, we would be all ready to process thousands of files!
Here we will show how to apply corrections to columnar data using:
+
+
the coffea.lookup_tools package, which is designed to read in ROOT histograms and a variety of data file formats popular within CMS into a standardized lookup table format;
+
CMS-specific extensions to the above, for jet corrections (coffea.jetmet_tools) and b-tagging efficiencies/uncertainties (coffea.btag_tools);
+
the correctionlib package, which provides a experiment-agnostic serializable data format for common correction functions.
+
+
Test data: We’ll use NanoEvents to construct some test data.
Opening a root file and using it as a lookup table
+
In tests/samples, there is an example file with a TH2F histogram named scalefactors_Tight_Electron. The following code reads that histogram into an evaluator instance, under the key testSF2d and applies it to some electrons.
+
+
[4]:
+
+
+
ext=extractor()
+# several histograms can be imported at once using wildcards (*)
+ext.add_weight_sets(["testSF2d scalefactors_Tight_Electron data/testSF2d.histo.root"])
+ext.finalize()
+
+evaluator=ext.make_evaluator()
+
+print("available evaluator keys:")
+forkeyinevaluator.keys():
+ print("\t",key)
+print("testSF2d:",evaluator['testSF2d'])
+print("type of testSF2d:",type(evaluator['testSF2d']))
+
Building and using your own correction from a histogram
+
To use a histogram or ratio of histograms to build your own correction, you can use lookup_tools to simplify the implementation. Here we create some mock data for two slightly different pt and eta spectra (say, from two different generators) and derive a correction to reweight one sample to the other.
Now we derive a correction as a function of \(p_T\) and \(\eta\) to gen2 such that it agrees with gen1. We’ll set it to 1 anywhere we run out of statistics for the correction, to avoid divide by zero issues
+
+
[7]:
+
+
+
fromcoffea.lookup_tools.dense_lookupimportdense_lookup
+
+num=dists["gen1",:,:].values()
+den=dists["gen2",:,:].values()
+sf=np.where(
+ (num>0)&(den>0),
+ num/np.maximum(den,1)*den.sum()/num.sum(),
+ 1.0,
+)
+
+corr=dense_lookup(sf,[ax.edgesforaxindists.axes[1:]])
+print(corr)
+
+# a quick way to plot the scale factor is to steal the axis definitions from the input histograms:
+sfhist=hist.Hist(*dists.axes[1:],data=sf)
+sfhist.plot2d()
+
+ColormeshArtists(pcolormesh=<matplotlib.collections.QuadMesh object at 0x134eac6a0>, cbar=<matplotlib.colorbar.Colorbar object at 0x134eb87c0>, text=[])
+
+
+
+
+
+
+
+
+
+
Now we generate some new mock data as if it was drawn from gen2 and reweight it with our corr to match gen1
Applying energy scale transformations with jetmet_tools
+
The coffea.jetmet_tools package provides a convenience class JetTransformer which applies specified corrections and computes uncertainties in one call. First we build the desired jet correction stack to apply. This will usually be some set of the various JEC and JER correction text files that depends on the jet cone size (AK4, AK8) and the pileup mitigation algorithm, as
+well as the data-taking year they are associated with.
+
+
[9]:
+
+
+
fromcoffea.jetmet_toolsimportFactorizedJetCorrector,JetCorrectionUncertainty
+fromcoffea.jetmet_toolsimportJECStack,CorrectedJetsFactory
+importawkwardasak
+importnumpyasnp
+
+ext=extractor()
+ext.add_weight_sets([
+ "* * data/Fall17_17Nov2017_V32_MC_L2Relative_AK4PFPuppi.jec.txt",
+ "* * data/Fall17_17Nov2017_V32_MC_Uncertainty_AK4PFPuppi.junc.txt",
+])
+ext.finalize()
+
+jec_stack_names=[
+ "Fall17_17Nov2017_V32_MC_L2Relative_AK4PFPuppi",
+ "Fall17_17Nov2017_V32_MC_Uncertainty_AK4PFPuppi"
+]
+
+evaluator=ext.make_evaluator()
+
+jec_inputs={name:evaluator[name]fornameinjec_stack_names}
+jec_stack=JECStack(jec_inputs)
+### more possibilities are available if you send in more pieces of the JEC stack
+# mc2016_ak8_jxform = JECStack(["more", "names", "of", "JEC parts"])
+
+print(dir(evaluator))
+
Now we prepare some auxilary variables that are used to parameterize the jet energy corrections, such as jet area, mass, and event \(\rho\) (mean pileup energy density), and pass all of these into the CorrectedJetsFactory:
Applying CMS b-tagging corrections with btag_tools
+
The coffea.btag_tools module provides the high-level utility BTagScaleFactor which calculates per-jet weights for b-tagging as well as light flavor mis-tagging efficiencies. Uncertainties can be calculated as well.
For the most part, using correctionlib is straightforward. We’ll show here how to convert the custom correction we derived earlier (corr) into a correctionlib object, and save it in the json format:
+
+
[13]:
+
+
+
importcorrectionlib,rich
+importcorrectionlib.convert
+
+# without a name, the resulting object will fail validation
+sfhist.name="gen2_to_gen1"
+sfhist.label="out"
+clibcorr=correctionlib.convert.from_histogram(sfhist)
+clibcorr.description="Reweights gen2 to agree with gen1"
+# set overflow bins behavior (default is to raise an error when out of bounds)
+clibcorr.data.flow="clamp"
+
+cset=correctionlib.schemav2.CorrectionSet(
+ schema_version=2,
+ description="my custom corrections",
+ corrections=[clibcorr],
+)
+rich.print(cset)
+
+withopen("data/mycorrections.json","w")asfout:
+ fout.write(cset.json(exclude_unset=True))
+
At the time of writing, correctionlib does not support jagged arrays. But we can work around this using awkward utilities flatten and unflatten, as shown below
In scientific python, histograms seem to be considered as a plot style, on equal footing with, e.g. scatter plots. It may well be that HEP is the only place where users need to plot pre-binned data, and thus must use histograms as persistent objects representing reduced data. In Coffea, the hist subpackage provides a persistable mergable histogram object. This notebook will discuss a few ways that such objects can be
+manipulated.
+
A histogram object roughly goes through three stages in its life:
Let’s start with filling. We’ll use a random distribution near and dear to of b and c factory physicists, and have the numpy builtin histogram function do the work for us:
So we’re done, right? Probably not: we have more than 1000 events, and probably need to use some map-reduce paradigm to fill the histogram because we can’t keep all 1 billion vals in memory. So we need two things: a binning, so that all histograms that were independently created can be added, and the ability to add two histograms.
but we are becoming challenged by book-keeping of the variables. The histogram class in Coffea is designed to simplify this operation, and the eventual successor (for filling purposes) boost-histogram has similar syntax.
+
In the constructor you specify each axis, either as a numeric Bin axis or a categorical Cat axis. Each axis constructor takes arguments similar to ROOT TH1 constructors. One can pass an array to the Bin axis for non-uniform binning. Then the fill call is as simple as passing the respective arrays to histo.fill.
+
+
[7]:
+
+
+
importcoffea.histashist
+
+histo=hist.Hist("Counts",
+ hist.Cat("sample","sample name"),
+ hist.Bin("x","x value",20,-10,10),
+ hist.Bin("y","y value",20,-10,10),
+ hist.Bin("z","z value",20,-10,10),
+ )
+
+histo.fill(sample="sample 1",x=xyz[:,0],y=xyz[:,1],z=xyz[:,2])
+
+# suppose we have another sample of xyz values
+xyz_sample2=np.random.multivariate_normal(mean=[1,3,7],cov=np.eye(3),size=10000)
+
+# additionally, lets assume entries in sample 2 have some non-uniform weight equal to atan(distance from origin)
+weight=np.arctan(np.sqrt(np.power(xyz_sample2,2).sum(axis=1)))
+
+# weight is a reserved keyword in Hist, and can be added to any fill() call
+histo.fill(sample="sample 2",x=xyz_sample2[:,0],y=xyz_sample2[:,1],z=xyz_sample2[:,2],weight=weight)
+
+print(histo)
+
+
+
+
+
+
+
+
+<Hist (sample,x,y,z) instance at 0x7fa15cf61390>
+
+
+
+
[8]:
+
+
+
# For more details, look at:
+# help(hist.Hist)
+# help(hist.Bin)
+# help(hist.Cat)
+
Here are a few examples of transformations on multidimensional histograms in Coffea. For each, the docstring (help(function) or shift+tab in Jupyter) provides useful info.
+
+
[9]:
+
+
+
# sum all x bins within nominal range (-10, 10)
+histo.sum("x",overflow='none')
+
+
+
+
+
[9]:
+
+
+
+
+<Hist (sample,y,z) instance at 0x7fa15ba1af10>
+
+
+
There is some analog to fancy array slicing for histogram objects, which is supported (with reasonable consistency) in Coffea, where the slice boundaries are physical axis values, rather than bin indices. All values outside the slice range are merged into overflow bins.
+
For a lengthy discussion on possible slicing syntax for the future, see boost-histogram#35.
# data can be exported to ROOT via uproot3 (soon uproot v4 as well), but only 1D
+importuproot3
+importos
+
+ifos.path.exists("output.root"):
+ os.remove("output.root")
+
+outputfile=uproot3.create("output.root")
+h=histo.sum('x','y')
+forsampleinh.identifiers('sample'):
+ outputfile[sample.name]=hist.export1d(h.integrate('sample',sample))
+outputfile.close()
+
+
+
+
+
+
+
+
+/Users/lagray/miniconda3/envs/coffea-work/lib/python3.7/site-packages/uproot3/__init__.py:138: FutureWarning: Consider switching from 'uproot3' to 'uproot', since the new interface became the default in 2020.
+
+ pip install -U uproot
+
+In Python:
+
+ >>> import uproot
+ >>> with uproot.open(...) as file:
+ ...
+
+ FutureWarning
+
The most integrated plotting utility in the scientific python ecosystem, by far, is matplotlib. However, as we will see, it is not tailored to HEP needs.
+
Let’s start by looking at basic mpl histogramming.
vals=argus(chi=.5).rvs(size=1000)
+
+# notice the semicolon, which prevents display of the return values
+plt.hist(vals);
+
+
+
+
+
+
+
+
+
+
+
Suppose we want to plot pre-binned data, for example from our earlier np.histogram usage. Here we start running into the edge of typical mpl usage. As mentioned before, apparently HEP is the only regular user of pre-binned histograms.
To facilitate these operations, there is a package called mplhep. This package is available standlaone, but it is also used internally by the coffea.hist subpackage to provide several convenience functions to aid in plotting Hist objects:
+
+
plot1d: Create a 1D plot from a 1D or 2D Hist object
+
plotratio: Create a ratio plot, dividing two compatible histograms
# make coarse binned hist and look at several distributions
+hnew=(
+ histo
+ .rebin("y",hist.Bin("ynew","rebinned y value",[0,3,5]))
+ .rebin("z",hist.Bin("znew","rebinned z value",[5,8,10]))
+)
+
+hist.plotgrid(hnew,row='ynew',col='znew',overlay='sample');
+
We’ve covered the basics of plotting, now let’s go over some styling options. To make things more interesting, we’ll load some electron and muon Lorentz vectors from simulated \(H\rightarrow ZZ^{*}\) events into awkward arrays and then plot some kinematic quantities for them, making liberal use of the matplotlib styling options which are exposed through the coffea plotting utilities.
+ % Total % Received % Xferd Average Speed Time Time Time Current
+ Dload Upload Total Spent Left Speed
+100 212k 100 212k 0 0 7601k 0 --:--:-- --:--:-- --:--:-- 7601k
+
+
+
+
[28]:
+
+
+
importuproot
+importawkwardasak
+fromcoffea.nanoevents.methodsimportvector
+ak.behavior.update(vector.behavior)
+
+fin=uproot.open("HZZ.root")
+tree=fin["events"]
+
+# let's build the lepton arrays back into objects
+# in the future, some of this verbosity can be reduced
+arrays={k.replace('Electron_',''):vfork,vintree.arrays(filter_name="Electron_*",how=dict).items()}
+electrons=ak.zip({'x':arrays.pop('Px'),
+ 'y':arrays.pop('Py'),
+ 'z':arrays.pop("Pz"),
+ 't':arrays.pop("E"),
+ },
+ with_name="LorentzVector"
+)
+
+
+arrays={k.replace('Muon_',''):vfork,vintree.arrays(filter_name="Muon_*",how=dict).items()}
+muons=ak.zip({'x':arrays.pop('Px'),
+ 'y':arrays.pop('Py'),
+ 'z':arrays.pop("Pz"),
+ 't':arrays.pop("E"),
+ },
+ with_name="LorentzVector"
+)
+
+print("Avg. electrons/event:",ak.sum(ak.num(electrons))/tree.num_entries)
+print("Avg. muons/event:",ak.sum(ak.num(muons))/tree.num_entries)
+
lepton_kinematics=hist.Hist(
+ "Events",
+ hist.Cat("flavor","Lepton flavor"),
+ hist.Bin("pt","$p_{T}$",19,10,100),
+ hist.Bin("eta","$\eta$",[-2.5,-1.4,0,1.4,2.5]),
+)
+
+# Pass keyword arguments to fill, all arrays must be flat numpy arrays
+# User is responsible for ensuring all arrays have same jagged structure!
+lepton_kinematics.fill(
+ flavor="electron",
+ pt=ak.flatten(electrons.pt),
+ eta=ak.flatten(electrons.eta)
+)
+lepton_kinematics.fill(
+ flavor="muon",
+ pt=ak.flatten(muons.pt),
+ eta=ak.flatten(muons.eta)
+)
+
+
+
+
+
[30]:
+
+
+
# Now we can start to manipulate this single histogram to plot different views of the data
+# here we look at lepton pt for all eta
+lepton_pt=lepton_kinematics.integrate("eta")
+
+ax=hist.plot1d(
+ lepton_pt,
+ overlay="flavor",
+ stack=True,
+ fill_opts={'alpha':.5,'edgecolor':(0,0,0,0.3)}
+)
+# all plot calls return the matplotlib axes object, from which
+# you can edit features afterwards using matplotlib object-oriented syntax
+# e.g. maybe you really miss '90s graphics...
+ax.get_legend().shadow=True
+
+
+
+
+
+
+
+
+
+
+
+
[31]:
+
+
+
# Clearly the yields are much different, are the shapes similar? We can check by setting `density=True`
+lepton_pt.label="Density"
+ax=hist.plot1d(lepton_pt,overlay="flavor",density=True)
+
+
+
+
+
+
+
+
+
+
+
+
[32]:
+
+
+
# Let's stack them, after defining some nice styling
+stack_fill_opts={
+ 'alpha':0.8,
+ 'edgecolor':(0,0,0,.5)
+}
+stack_error_opts={
+ 'label':'Stat. Unc.',
+ 'hatch':'///',
+ 'facecolor':'none',
+ 'edgecolor':(0,0,0,.5),
+ 'linewidth':0
+}
+# maybe we want to compare different eta regions
+# plotgrid accepts row and column axes, and creates a grid of 1d plots as appropriate
+ax=hist.plotgrid(
+ lepton_kinematics,
+ row="eta",
+ overlay="flavor",
+ stack=True,
+ fill_opts=stack_fill_opts,
+ error_opts=stack_error_opts,
+)
+
+
+
+
+
+
+
+
+
+
+
+
[33]:
+
+
+
# Here we create some pseudodata for the pt histogram so we can make a nice data/mc plot
+pthist=lepton_kinematics.sum('eta')
+bin_values=pthist.axis('pt').centers()
+poisson_means=pthist.sum('flavor').values()[()]
+values=np.repeat(bin_values,np.random.poisson(poisson_means))
+pthist.fill(flavor='pseudodata',pt=values)
+
+# Set nicer labels, by accessing the string bins' label property
+pthist.axis('flavor').index('electron').label='e Flavor'
+pthist.axis('flavor').index('muon').label=r'$\mu$ Flavor'
+pthist.axis('flavor').index('pseudodata').label=r'Pseudodata from e/$\mu$'
+
+# using regular expressions on flavor name to select just the data
+# another method would be to fill a separate data histogram
+importre
+notdata=re.compile('(?!pseudodata)')
+
+
+
+
+
[34]:
+
+
+
# make a nice ratio plot, adjusting some font sizes
+plt.rcParams.update({
+ 'font.size':14,
+ 'axes.titlesize':18,
+ 'axes.labelsize':18,
+ 'xtick.labelsize':12,
+ 'ytick.labelsize':12
+})
+fig,(ax,rax)=plt.subplots(
+ nrows=2,
+ ncols=1,
+ figsize=(7,7),
+ gridspec_kw={"height_ratios":(3,1)},
+ sharex=True
+)
+fig.subplots_adjust(hspace=.07)
+
+# Here is an example of setting up a color cycler to color the various fill patches
+# We get the colors from this useful utility: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=6
+fromcyclerimportcycler
+colors=['#a6cee3','#1f78b4','#b2df8a','#33a02c','#fb9a99','#e31a1c']
+ax.set_prop_cycle(cycler(color=colors))
+
+fill_opts={
+ 'edgecolor':(0,0,0,0.3),
+ 'alpha':0.8
+}
+error_opts={
+ 'label':'Stat. Unc.',
+ 'hatch':'///',
+ 'facecolor':'none',
+ 'edgecolor':(0,0,0,.5),
+ 'linewidth':0
+}
+data_err_opts={
+ 'linestyle':'none',
+ 'marker':'.',
+ 'markersize':10.,
+ 'color':'k',
+ 'elinewidth':1,
+}
+
+# plot the MC first
+hist.plot1d(
+ pthist[notdata],
+ overlay="flavor",
+ ax=ax,
+ clear=False,
+ stack=True,
+ line_opts=None,
+ fill_opts=fill_opts,
+ error_opts=error_opts
+)
+# now the pseudodata, setting clear=False to avoid overwriting the previous plot
+hist.plot1d(
+ pthist['pseudodata'],
+ overlay="flavor",
+ ax=ax,
+ clear=False,
+ error_opts=data_err_opts
+)
+
+ax.autoscale(axis='x',tight=True)
+ax.set_ylim(0,None)
+ax.set_xlabel(None)
+leg=ax.legend()
+
+# now we build the ratio plot
+hist.plotratio(
+ num=pthist['pseudodata'].sum("flavor"),
+ denom=pthist[notdata].sum("flavor"),
+ ax=rax,
+ error_opts=data_err_opts,
+ denom_fill_opts={},
+ guide_opts={},
+ unc='num'
+)
+rax.set_ylabel('Ratio')
+rax.set_ylim(0,2)
+
+# add some labels
+coffee=plt.text(0.,1.,u"☕",
+ fontsize=28,
+ horizontalalignment='left',
+ verticalalignment='bottom',
+ transform=ax.transAxes
+ )
+lumi=plt.text(1.,1.,r"1 fb$^{-1}$ (?? TeV)",
+ fontsize=16,
+ horizontalalignment='right',
+ verticalalignment='bottom',
+ transform=ax.transAxes
+ )
+
+
+
+
+
+
+
+
+
+
+
Some further styling tools are available through the mplhep package. In particular, there are several stylesheets that update plt.rcParams to conform with experiment style recommendations regarding font face, font sizes, tick mark styles, and other such things. Below is an example application.
+
+
[35]:
+
+
+
importmplhep
+plt.style.use(mplhep.style.ROOT)
+
+# Compare this to the style of the plot drawn previously
+ax=hist.plot1d(lepton_pt,overlay="flavor",density=True)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/notebooks/histograms.ipynb b/notebooks/histograms.ipynb
new file mode 100644
index 000000000..d14ddbe6d
--- /dev/null
+++ b/notebooks/histograms.ipynb
@@ -0,0 +1,1190 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Coffea Histograms (deprecated)\n",
+ "\n",
+ "_This feature is deprecated in favor of [hist](https://hist.readthedocs.io/en/latest/) and [mplhep](https://mplhep.readthedocs.io/en/latest/). A migration guide can be found in discussion [CoffeaTeam/coffea#705](https://github.com/CoffeaTeam/coffea/discussions/705)_\n",
+ "\n",
+ "This is a rendered copy of [histograms.ipynb](https://github.com/CoffeaTeam/coffea/blob/master/binder/histograms.ipynb). You can optionally run it interactively on [binder at this link](https://mybinder.org/v2/gh/coffeateam/coffea/master?filepath=binder%2Fhistograms.ipynb)\n",
+ "\n",
+ "In scientific python, histograms seem to be considered as a plot style, on equal footing with, e.g. scatter plots.\n",
+ "It may well be that HEP is the only place where users need to plot *pre-binned* data, and thus must use histograms as persistent objects representing reduced data. In Coffea, the [hist](https://coffeateam.github.io/coffea/modules/coffea.hist.html) subpackage provides a persistable mergable histogram object. This notebook will discuss a few ways that such objects can be manipulated.\n",
+ "\n",
+ "A histogram object roughly goes through three stages in its life:\n",
+ "\n",
+ " - Filling\n",
+ " - Transformation (projection, rebinning, integrating)\n",
+ " - Plotting\n",
+ "\n",
+ "We'll go over examples of each stage in this notebook, and conclude with some styling examples."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Filling\n",
+ "Let's start with filling. We'll use a random distribution [near and dear](https://en.wikipedia.org/wiki/ARGUS_distribution) to of b and c factory physicists, and have the numpy builtin [histogram function](https://numpy.org/doc/stable/reference/generated/numpy.histogram.html) do the work for us:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(array([ 16, 47, 66, 95, 125, 113, 137, 167, 143, 91]), array([0.002894 , 0.10257766, 0.20226132, 0.30194498, 0.40162865,\n",
+ " 0.50131231, 0.60099597, 0.70067963, 0.80036329, 0.90004695,\n",
+ " 0.99973061]))\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from scipy.stats import argus\n",
+ "\n",
+ "vals = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "hist = np.histogram(vals)\n",
+ "print(hist)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So we're done, right?\n",
+ "Probably not: we have more than 1000 events, and probably need to use some map-reduce paradigm to fill the histogram because we can't keep all 1 billion `vals` in memory. So we need two things: a binning, so that all histograms that were independently created can be added, and the ability to add two histograms."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "def add_histos(h1, h2):\n",
+ " h1sumw, h1binning = h1\n",
+ " h2sumw, h2binning = h2\n",
+ " if h1binning.shape == h2binning.shape and np.all(h1binning==h2binning):\n",
+ " return h1sumw+h2sumw, h1binning\n",
+ " else:\n",
+ " raise ValueError(\"The histograms have inconsistent binning\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(array([ 2, 6, 6, 6, 12, 18, 20, 16, 14, 28, 22, 26, 28, 42, 18, 34, 28,\n",
+ " 48, 40, 42, 42, 60, 46, 62, 46, 52, 46, 38, 52, 54, 50, 58, 72, 54,\n",
+ " 50, 74, 80, 58, 70, 70, 42, 64, 68, 52, 60, 32, 44, 30, 18]), array([0. , 0.02040816, 0.04081633, 0.06122449, 0.08163265,\n",
+ " 0.10204082, 0.12244898, 0.14285714, 0.16326531, 0.18367347,\n",
+ " 0.20408163, 0.2244898 , 0.24489796, 0.26530612, 0.28571429,\n",
+ " 0.30612245, 0.32653061, 0.34693878, 0.36734694, 0.3877551 ,\n",
+ " 0.40816327, 0.42857143, 0.44897959, 0.46938776, 0.48979592,\n",
+ " 0.51020408, 0.53061224, 0.55102041, 0.57142857, 0.59183673,\n",
+ " 0.6122449 , 0.63265306, 0.65306122, 0.67346939, 0.69387755,\n",
+ " 0.71428571, 0.73469388, 0.75510204, 0.7755102 , 0.79591837,\n",
+ " 0.81632653, 0.83673469, 0.85714286, 0.87755102, 0.89795918,\n",
+ " 0.91836735, 0.93877551, 0.95918367, 0.97959184, 1. ]))\n"
+ ]
+ }
+ ],
+ "source": [
+ "vals2 = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "hist1 = np.histogram(vals, bins=binning)\n",
+ "hist2 = np.histogram(vals, bins=binning)\n",
+ "\n",
+ "hist = add_histos(hist1, hist2)\n",
+ "print(hist)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So now we have everything we need to make our own equivalent to ROOT TH1, from a filling perspective:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class myTH1:\n",
+ " def __init__(self, binning):\n",
+ " self._binning = binning\n",
+ " self._sumw = np.zeros(binning.size - 1)\n",
+ " \n",
+ " def fill(self, values, weights=None):\n",
+ " sumw, _ = np.histogram(values, bins=self._binning, weights=weights)\n",
+ " self._sumw += sumw\n",
+ " \n",
+ " def __add__(self, other):\n",
+ " if not isinstance(other, myTH1):\n",
+ " raise ValueError\n",
+ " if not np.array_equal(other._binning, self._binning):\n",
+ " raise ValueError(\"The histograms have inconsistent binning\")\n",
+ " out = myTH1(self._binning)\n",
+ " out._sumw = self._sumw + other._sumw\n",
+ " return out"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[ 3. 3. 7. 5. 9. 18. 16. 17. 15. 24. 26. 27. 34. 37. 23. 38. 29. 46.\n",
+ " 40. 40. 36. 50. 47. 49. 57. 49. 50. 44. 59. 69. 55. 52. 68. 54. 48. 72.\n",
+ " 72. 64. 55. 74. 57. 64. 63. 55. 49. 39. 39. 31. 22.]\n"
+ ]
+ }
+ ],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "h1 = myTH1(binning)\n",
+ "h1.fill(vals)\n",
+ "\n",
+ "h2 = myTH1(binning)\n",
+ "h2.fill(vals2)\n",
+ "\n",
+ "h = h1 + h2\n",
+ "print(h._sumw)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Homework: add `sumw2` support.\n",
+ "\n",
+ "Of course, we might want multidimensional histograms. There is `np.histogramdd`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xyz = np.random.multivariate_normal(mean=[1, 3, 7], cov=np.eye(3), size=10000)\n",
+ "\n",
+ "xbins = np.linspace(-10, 10, 20)\n",
+ "ybins = np.linspace(-10, 10, 20)\n",
+ "zbins = np.linspace(-10, 10, 20)\n",
+ "hnumpy = np.histogramdd(xyz, bins=(xbins, ybins, zbins))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "but we are becoming challenged by book-keeping of the variables.\n",
+ "The [histogram class](https://coffeateam.github.io/coffea/api/coffea.hist.Hist.html#coffea.hist.Hist) in Coffea is designed to simplify this operation, and the eventual successor (for filling purposes) [boost-histogram](https://github.com/scikit-hep/boost-histogram#usage) has similar syntax.\n",
+ "\n",
+ "In the constructor you specify each axis, either as a numeric `Bin` axis or a categorical `Cat` axis. Each axis constructor takes arguments similar to ROOT TH1 constructors. One can pass an array to the `Bin` axis for non-uniform binning. Then the fill call is as simple as passing the respective arrays to `histo.fill`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "import coffea.hist as hist\n",
+ "\n",
+ "histo = hist.Hist(\"Counts\",\n",
+ " hist.Cat(\"sample\", \"sample name\"),\n",
+ " hist.Bin(\"x\", \"x value\", 20, -10, 10),\n",
+ " hist.Bin(\"y\", \"y value\", 20, -10, 10),\n",
+ " hist.Bin(\"z\", \"z value\", 20, -10, 10),\n",
+ " )\n",
+ "\n",
+ "histo.fill(sample=\"sample 1\", x=xyz[:,0], y=xyz[:,1], z=xyz[:,2])\n",
+ "\n",
+ "# suppose we have another sample of xyz values\n",
+ "xyz_sample2 = np.random.multivariate_normal(mean=[1, 3, 7], cov=np.eye(3), size=10000)\n",
+ "\n",
+ "# additionally, lets assume entries in sample 2 have some non-uniform weight equal to atan(distance from origin)\n",
+ "weight = np.arctan(np.sqrt(np.power(xyz_sample2, 2).sum(axis=1)))\n",
+ "\n",
+ "# weight is a reserved keyword in Hist, and can be added to any fill() call\n",
+ "histo.fill(sample=\"sample 2\", x=xyz_sample2[:,0], y=xyz_sample2[:,1], z=xyz_sample2[:,2], weight=weight)\n",
+ "\n",
+ "print(histo)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# For more details, look at:\n",
+ "# help(hist.Hist)\n",
+ "# help(hist.Bin)\n",
+ "# help(hist.Cat)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Transformation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here are a few examples of transformations on multidimensional histograms in Coffea. For each, the docstring (`help(function)` or shift+tab in Jupyter) provides useful info."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# sum all x bins within nominal range (-10, 10)\n",
+ "histo.sum(\"x\", overflow='none')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There is some analog to fancy array slicing for histogram objects, which is supported (with reasonable consistency) in Coffea, where the slice boundaries are physical axis values, rather than bin indices. All values outside the slice range are merged into overflow bins.\n",
+ "\n",
+ "For a lengthy discussion on possible slicing syntax for the future, see [boost-histogram#35](https://github.com/scikit-hep/boost-histogram/issues/35)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "sliced = histo[:,0:,4:,0:]\n",
+ "display(sliced)\n",
+ "display(sliced.identifiers(\"y\", overflow='all'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# integrate y bins from -2 to +10\n",
+ "histo.integrate(\"y\", slice(0, 10))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# rebin z axis by providing a new axis definition\n",
+ "histo.rebin(\"z\", hist.Bin(\"znew\", \"rebinned z value\", [-10, -6, 6, 10]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# merge categorical axes\n",
+ "mapping = {\n",
+ " 'all samples': ['sample 1', 'sample 2'],\n",
+ " 'just sample 1': ['sample 1'],\n",
+ "}\n",
+ "histo.group(\"sample\", hist.Cat(\"cat\", \"new category\"), mapping)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# scale entire histogram by 3 (in-place)\n",
+ "histo.scale(3.)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# scale samples by different values (also in-place)\n",
+ "scales = {\n",
+ " 'sample 1': 1.2,\n",
+ " 'sample 2': 0.2,\n",
+ "}\n",
+ "histo.scale(scales, axis='sample')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# useful debugging tool: print bins, aka 'identifiers'\n",
+ "display(histo.identifiers('sample'))\n",
+ "display(histo.identifiers('x'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{('sample 1',): array([0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00,\n",
+ " 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00, 0.00000e+00,\n",
+ " 0.00000e+00, 0.00000e+00, 3.60000e+00, 5.40000e+01, 7.23600e+02,\n",
+ " 4.83120e+03, 1.24164e+04, 1.24344e+04, 4.68720e+03, 8.13600e+02]),\n",
+ " ('sample 2',): array([0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n",
+ " 7.67352230e-01, 1.23975181e+01, 1.91084135e+02, 1.16950599e+03,\n",
+ " 3.00209800e+03, 2.88614286e+03, 1.20687727e+03, 1.68492970e+02])}"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# bin contents are accessed using values\n",
+ "histo.sum('x', 'y').values(sumw2=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/lagray/miniconda3/envs/coffea-work/lib/python3.7/site-packages/uproot3/__init__.py:138: FutureWarning: Consider switching from 'uproot3' to 'uproot', since the new interface became the default in 2020.\n",
+ "\n",
+ " pip install -U uproot\n",
+ "\n",
+ "In Python:\n",
+ "\n",
+ " >>> import uproot\n",
+ " >>> with uproot.open(...) as file:\n",
+ " ...\n",
+ "\n",
+ " FutureWarning\n"
+ ]
+ }
+ ],
+ "source": [
+ "# data can be exported to ROOT via uproot3 (soon uproot v4 as well), but only 1D\n",
+ "import uproot3\n",
+ "import os\n",
+ "\n",
+ "if os.path.exists(\"output.root\"):\n",
+ " os.remove(\"output.root\")\n",
+ "\n",
+ "outputfile = uproot3.create(\"output.root\")\n",
+ "h = histo.sum('x', 'y')\n",
+ "for sample in h.identifiers('sample'):\n",
+ " outputfile[sample.name] = hist.export1d(h.integrate('sample', sample))\n",
+ "outputfile.close()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Plotting\n",
+ "The most integrated plotting utility in the scientific python ecosystem, by far, is [matplotlib](https://matplotlib.org/). However, as we will see, it is not tailored to HEP needs.\n",
+ "\n",
+ "Let's start by looking at basic mpl histogramming."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib inline\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "vals = argus(chi=.5).rvs(size=1000)\n",
+ "\n",
+ "# notice the semicolon, which prevents display of the return values\n",
+ "plt.hist(vals);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Suppose we want to plot pre-binned data, for example from our earlier `np.histogram` usage. Here we start running into the edge of typical mpl usage. As mentioned before, apparently HEP is the only regular user of pre-binned histograms."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "binning = np.linspace(0, 1, 50)\n",
+ "\n",
+ "h1vals, h1bins = np.histogram(vals, bins=binning)\n",
+ "plt.step(x=h1bins[:-1], y=h1vals, where='post');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To facilitate these operations, there is a package called [mplhep](https://github.com/scikit-hep/mplhep). This package is available standlaone, but it is also used internally by the `coffea.hist` subpackage to provide several convenience functions to aid in plotting `Hist` objects:\n",
+ "\n",
+ " * [plot1d](https://coffeateam.github.io/coffea/api/coffea.hist.plot1d.html#coffea.hist.plot1d): Create a 1D plot from a 1D or 2D Hist object\n",
+ "\n",
+ " * [plotratio](https://coffeateam.github.io/coffea/api/coffea.hist.plotratio.html#coffea.hist.plotratio): Create a ratio plot, dividing two compatible histograms\n",
+ "\n",
+ " * [plot2d](https://coffeateam.github.io/coffea/api/coffea.hist.plot2d.html#coffea.hist.plot2d): Create a 2D plot from a 2D Hist object\n",
+ "\n",
+ " * [plotgrid](https://coffeateam.github.io/coffea/api/coffea.hist.plotgrid.html#coffea.hist.plotgrid): Create a grid of plots, enumerating identifiers on up to 3 axes\n",
+ " \n",
+ "Below are some simple examples of using each function on our `histo` object."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZwAAAEGCAYAAABRvCMcAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAh6ElEQVR4nO3de3xU1b338c+Pa+oFAUkBCR5AQQkXKYZb0VNOqYCiYi3eqpWiFVultbYi+OjLW/Vp9ZxHqz5V5CiKfXlBqT5QpSIottYCGiyCXCxRUYOgFBSKHEH09/yxV+IQZpJJMrNnknzfr9e8smfttff+zc5kfll7r1nL3B0REZFsa5brAEREpGlQwhERkVgo4YiISCyUcEREJBZKOCIiEosWuQ4gbh06dPBu3brlOgwRkQZl+fLl/3T3wvrso8klnG7dulFaWprrMEREGhQze7e++9AlNRERiYUSjoiIxEIJR0REYtHk7uEk8/nnn1NeXs5nn32W61AapIKCAoqKimjZsmWuQxGRPKaEA5SXl3PwwQfTrVs3zCzX4TQo7s7WrVspLy+ne/fuuQ5HRPKYLqkBn332GYceeqiSTR2YGYceeqhahyJSIyWcQMmm7nTuRCQdSjgiIhILJZw8M2LECH0xVUQaJSUcEZE6OOveJZx175Kc76MhUcKpwaeffsrYsWM55phj6Nu3L7NnzwbgxhtvZNCgQfTt25dJkyZRMXPqiBEjuPzyyykpKaF37968+uqrnH766fTs2ZNrrrkGgA0bNnD00Udz7rnn0rt3b8aPH8+uXbv2O/Zzzz3HsGHDGDhwIGeccQY7d+7cr86IESOYOnUqgwcPplevXrz00kuVxzj++OMZOHAgAwcO5G9/+xsAL774It/61rcYN24cPXr0YNq0aTz88MMMHjyYfv368dZbbwGwZcsWvve97zFo0CAGDRrEyy+/nPmTKyJNirpF1+DZZ5/lsMMO45lnngFg+/btAEyePJlrr70WgB/84Ac8/fTTnHLKKQC0atWK0tJS7rjjDsaNG8fy5ctp3749RxxxBJdffjkAb775Jvfffz/Dhw/nggsu4O677+aKK66oPO4///lPbrrpJhYtWsSBBx7ILbfcwm233VZ5zER79+7llVdeYf78+dxwww0sWrSIr3/96yxcuJCCggLWr1/POeecU3mp7vXXX2ft2rW0b9+eHj168KMf/YhXXnmFO+64g7vuuovf/va3XHbZZVx++eUcd9xxvPfee4wePZq1a9dm70SLNDKPLHuPuSs2VltnzaYdANW2csYN6ML3hxye0dhyRQmnBv369eOXv/wlU6dO5eSTT+b4448HYPHixdx6663s2rWLbdu20adPn8qEc+qpp1Zu26dPHzp37gxAjx49eP/992nbti1du3Zl+PDhAJx33nnceeed+yScpUuXsmbNmso6e/bsYdiwYUljPP300wE49thj2bBhAxB9mXXy5MmsWLGC5s2b849//KOy/qBBgypjOuKIIxg1alRlvIsXLwZg0aJFrFmzpnKbHTt2sHPnTg466KC6nkqRBqWmhFFTslj2zjYAhnRvX+cYKo6hhNNE9OrVi9dee4358+dzzTXXMHLkSK688kouueQSSktL6dq1K9dff/0+30Np3bo1AM2aNatcrni+d+9eYP+uxFWfuzsnnHACjz76aI0xVhyjefPmlfu//fbb6dixI6+//jpffvklBQUF+9WvGmNifF9++SVLly7dZzuRpmTuio2s2bSD4s5t6rT9kO7ta2ydVCSr2Rcn/2eysd3fUcKpwQcffED79u0577zzaNu2Lffdd19lcunQoQM7d+5kzpw5jB8/vlb7fe+991iyZAnDhg3jkUce4bjjjttn/dChQ7n00kspKyvjyCOP5NNPP2Xjxo306tUrrf1v376doqIimjVrxqxZs/jiiy9qFd+oUaO46667mDJlCgArVqxgwIABtdqHSENX3LlNjckg1XrZnzoN1GDVqlUMHjyYAQMGcMMNN3DNNdfQtm1bLrroIvr27cvo0aMZNGhQrfd71FFH8bvf/Y7evXvz8ccf85Of/GSf9YWFhTz44IOcc8459O/fn2HDhrFu3bq093/JJZcwa9YsjjnmGNatW8eBBx5Yq/juvPNOSktL6d+/P8XFxUyfPr1W24uIVGUVvasyvmOzmcDJwEfu3jeU/SdwCrAHeAuY6O6fhHVXARcCXwA/c/cFoXwMcAfQHLjP3X8TyrsDjwGHAsuBH7j7npriKikp8arfc1m7di29e/eu70tO24YNGzj55JN54403YjtmtsV9DkWyLR9aMPkQQwUzW+7uJfXZRzZbOA8CY6qULQT6unt/4B/AVQBmVgycDfQJ29xtZs3NrDnwO+BEoBg4J9QFuAW43d2P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\n",
+ "text/plain": [
+ "
NanoEvents is a Coffea utility to wrap flat nTuple structures (such as the CMS NanoAOD format) into a single awkward array with appropriate object methods (such as Lorentz vector methods\(^*\)), cross references, and nested objects, all lazily accessed\(^\dagger\) from the source ROOT TTree via uproot. The interpretation of the TTree data is configurable via schema
+objects, which are community-supplied for various source file types. These schema objects allow a richer interpretation of the file contents than the uproot.lazy methods. Currently available schemas include:
+
+
BaseSchema, which provides a simple representation of the input TTree, where each branch is available verbatim as events.branch_name, effectively the same behavior as uproot.lazy. Any branches that uproot supports at “full speed” (i.e. that are fully split and either flat or single-jagged) can be read by this schema;
+
NanoAODSchema, which is optimized to provide all methods and cross-references in CMS NanoAOD format;
+
PFNanoAODSchema, which builds a double-jagged particle flow candidate colllection events.jet.constituents from compatible PFNanoAOD input files;
+
TreeMakerSchema which is designed to read TTrees made by TreeMaker, an alternative CMS nTuplization format;
+
PHYSLITESchema, for the ATLAS DAOD_PHYSLITE derivation, a compact centrally-produced data format similar to CMS NanoAOD; and
+
DelphesSchema, for reading Delphes fast simulation nTuples.
+
+
We welcome contributions for new schemas, and can assist with the design of them.
+
\(^*\) Vector methods are currently made possible via the coffea vector methods mixin class structure. In a future version of coffea, they will instead be provided by the dedicated scikit-hep vector library, which provides a more rich feature set. The coffea vector methods predate the release of the vector library.
+
\(^\dagger\)Lazy access refers to only fetching the needed data from the (possibly remote) file when a sub-array is first accessed. The sub-array is then materialized and subsequent access of the sub-array uses a cached value in memory. As such, fully materializing a NanoEvents object may require a significant amount of memory.
+
In this demo, we will use NanoEvents to read a small CMS NanoAOD sample. The events object can be instantiated as follows:
In the factory constructor, we also pass the desired schema version (the latest version of NanoAOD can be built with schemaclass=NanoAODSchema) for this file and some extra metadata that we can later access with events.metadata. In a later example, we will show how to set up this metadata in coffea processors where the events object is pre-created for you. Consider looking at the
+from_root class method to see all optional arguments.
+
The events object is an awkward array, which at its top level is a record array with one record for each “collection”, where a collection is a grouping of fields (TBranches) based on the naming conventions of NanoAODSchema. For example, in the file we opened, the branches:
are grouped into one sub-record named Generator which can be accessed using either getitem or getattr syntax, i.e. events["Generator"] or events.Generator. e.g.
In CMS NanoAOD, each TBranch has a self-documenting help string embedded in the title field, which is carried into the NanoEvents, e.g. executing the following cell should produce a help pop-up:
where the Docstring shows information about the content of this array.
+
+
[4]:
+
+
+
events.Generator.id1?
+
+
+
+
Based on a collection’s name or contents, some collections acquire additional methods, which are extra features exposed by the code in the mixin classes of the coffea.nanoevents.methods modules. For example, although events.GenJet has the fields:
The assignment of methods classes to collections is done inside the schema object during the initial creation of the array, governed by the awkward array’s __record__ parameter and the associated behavior. See ak.behavior for a more detailed explanation of array behaviors.
+
Additional methods provide convenience functions for interpreting some branches, e.g. CMS NanoAOD packs several jet identification flag bits into a single integer, jetId. By implementing the bit-twiddling in the Jet mixin, the analsyis code becomes more clear:
CMS NanoAOD also contains pre-computed cross-references for some types of collections. For example, there is a TBranch Electron_genPartIdx which indexes the GenPart collection per event to give the matched generated particle, and -1 if no match is found. NanoEvents transforms these indices into an awkward indexed array pointing to the collection, so that one can directly access the matched particle using getattr syntax:
Since often one wants to shortcut repeated particles in a decay sequence, a helper method distinctParent is also available. Here we use it to find the parent particle ID for all prompt electrons:
As expected for this sample, most of the dimuon events have a pair invariant mass close to that of a Z boson. But what about the last event? Let’s take a look at the generator information:
Coffea relies mainly on uproot to provide access to ROOT files for analysis. As a usual analysis will involve processing tens to thousands of files, totalling gigabytes to terabytes of data, there is a certain amount of work to be done to build a parallelized framework to process the data in a reasonable amount of time. Of course, one can work directly within uproot to achieve this, as we’ll show in the beginning, but coffea provides the
+coffea.processor module, which allows users to worry just about the actual analysis code and not about how to implement efficient parallelization, assuming that the parallization is a trivial map-reduce operation (e.g. filling histograms and adding them together). The module provides the following key features:
+
+
A ProcessorABC abstract base class that can be derived from to implement the analysis code;
+
A NanoEvents interface to the arrays being read from the TTree as inputs;
+
A generic accumulate() utility to reduce the outputs to a single result, as showin in the accumulators notebook tutorial; and
+
A set of parallel executors to access multicore processing or distributed computing systems such as Dask, Parsl, Spark, WorkQueue, and others.
+
+
Let’s start by writing a simple processor class that reads some CMS open data and plots a dimuon mass spectrum. We’ll start by copying the ProcessorABC skeleton and filling in some details:
+
+
Remove flag, as we won’t use it
+
Adding a new histogram for \(m_{\mu \mu}\)
+
Building a Candidate record for muons, since we will read it with BaseSchema interpretation (the files used here could be read with NanoAODSchema but we want to show how to build vector objects from other TTree formats)
One could expand on this code to run over several chunks of the file, setting entry_start and entry_stop as appropriate. Then, several datasets could be processed by iterating over several files. However, the processor Runner can help with this! One lists the datasets and corresponding files, the processor they want to run, and which executor they want to use. Available executors derive from ExecutorBase and
+are listed here. Since these files are very large, we limit to just reading the first few chunks of events from each dataset with maxchunks.
/opt/conda/lib/python3.8/site-packages/awkward/_connect/_numpy.py:195: RuntimeWarning:
+invalid value encountered in sqrt
+ result = getattr(ufunc, method)(
+
Now, if we want to use more than a single core on our machine, we simply change IterativeExecutor for FuturesExecutor, which uses the python concurrent.futures standard library. We can then set the most interesting argument to the FuturesExecutor: the number of
+cores to use (2):
Hopefully this ran faster than the previous cell, but that may depend on how many cores are available on the machine you are running this notebook and your connection to eospublic.cern.ch. At least the output will be prettier now:
+
+
+
+
\ No newline at end of file
diff --git a/notebooks/processor.ipynb b/notebooks/processor.ipynb
new file mode 100644
index 000000000..92a2e0ce9
--- /dev/null
+++ b/notebooks/processor.ipynb
@@ -0,0 +1,947 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Coffea Processors\n",
+ "This is a rendered copy of [processor.ipynb](https://github.com/CoffeaTeam/coffea/blob/master/binder/processor.ipynb). You can optionally run it interactively on [binder at this link](https://mybinder.org/v2/gh/coffeateam/coffea/master?filepath=binder%2Fprocessor.ipynb)\n",
+ "\n",
+ "Coffea relies mainly on [uproot](https://github.com/scikit-hep/uproot) to provide access to ROOT files for analysis.\n",
+ "As a usual analysis will involve processing tens to thousands of files, totalling gigabytes to terabytes of data, there is a certain amount of work to be done to build a parallelized framework to process the data in a reasonable amount of time. Of course, one can work directly within uproot to achieve this, as we'll show in the beginning, but coffea provides the `coffea.processor` module, which allows users to worry just about the actual analysis code and not about how to implement efficient parallelization, assuming that the parallization is a trivial map-reduce operation (e.g. filling histograms and adding them together). The module provides the following key features:\n",
+ "\n",
+ " * A `ProcessorABC` abstract base class that can be derived from to implement the analysis code;\n",
+ " * A [NanoEvents](https://coffeateam.github.io/coffea/notebooks/nanoevents.html) interface to the arrays being read from the TTree as inputs;\n",
+ " * A generic `accumulate()` utility to reduce the outputs to a single result, as showin in the accumulators notebook tutorial; and\n",
+ " * A set of parallel executors to access multicore processing or distributed computing systems such as [Dask](https://distributed.dask.org/en/latest/), [Parsl](http://parsl-project.org/), [Spark](https://spark.apache.org/), [WorkQueue](https://cctools.readthedocs.io/en/latest/work_queue/), and others.\n",
+ "\n",
+ "Let's start by writing a simple processor class that reads some CMS open data and plots a dimuon mass spectrum.\n",
+ "We'll start by copying the [ProcessorABC](https://coffeateam.github.io/coffea/api/coffea.processor.ProcessorABC.html#coffea.processor.ProcessorABC) skeleton and filling in some details:\n",
+ "\n",
+ " * Remove `flag`, as we won't use it\n",
+ " * Adding a new histogram for $m_{\\mu \\mu}$\n",
+ " * Building a [Candidate](https://coffeateam.github.io/coffea/api/coffea.nanoevents.methods.candidate.PtEtaPhiMCandidate.html#coffea.nanoevents.methods.candidate.PtEtaPhiMCandidate) record for muons, since we will read it with `BaseSchema` interpretation (the files used here could be read with `NanoAODSchema` but we want to show how to build vector objects from other TTree formats) \n",
+ " * Calculating the dimuon invariant mass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import awkward as ak\n",
+ "from coffea import processor\n",
+ "from coffea.nanoevents.methods import candidate\n",
+ "import hist\n",
+ "\n",
+ "class MyProcessor(processor.ProcessorABC):\n",
+ " def __init__(self):\n",
+ " pass\n",
+ "\n",
+ " def process(self, events):\n",
+ " dataset = events.metadata['dataset']\n",
+ " muons = ak.zip(\n",
+ " {\n",
+ " \"pt\": events.Muon_pt,\n",
+ " \"eta\": events.Muon_eta,\n",
+ " \"phi\": events.Muon_phi,\n",
+ " \"mass\": events.Muon_mass,\n",
+ " \"charge\": events.Muon_charge,\n",
+ " },\n",
+ " with_name=\"PtEtaPhiMCandidate\",\n",
+ " behavior=candidate.behavior,\n",
+ " )\n",
+ "\n",
+ " h_mass = (\n",
+ " hist.Hist.new\n",
+ " .StrCat([\"opposite\", \"same\"], name=\"sign\")\n",
+ " .Log(1000, 0.2, 200., name=\"mass\", label=\"$m_{\\mu\\mu}$ [GeV]\")\n",
+ " .Int64()\n",
+ " )\n",
+ "\n",
+ " cut = (ak.num(muons) == 2) & (ak.sum(muons.charge, axis=1) == 0)\n",
+ " # add first and second muon in every event together\n",
+ " dimuon = muons[cut][:, 0] + muons[cut][:, 1]\n",
+ " h_mass.fill(sign=\"opposite\", mass=dimuon.mass)\n",
+ "\n",
+ " cut = (ak.num(muons) == 2) & (ak.sum(muons.charge, axis=1) != 0)\n",
+ " dimuon = muons[cut][:, 0] + muons[cut][:, 1]\n",
+ " h_mass.fill(sign=\"same\", mass=dimuon.mass)\n",
+ "\n",
+ " return {\n",
+ " dataset: {\n",
+ " \"entries\": len(events),\n",
+ " \"mass\": h_mass,\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " def postprocess(self, accumulator):\n",
+ " pass"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we were to just use bare uproot to execute this processor, we could do that with the following example, which:\n",
+ "\n",
+ " * Opens a CMS open data file\n",
+ " * Creates a NanoEvents object using `BaseSchema` (roughly equivalent to the output of `uproot.lazy`)\n",
+ " * Creates a `MyProcessor` instance\n",
+ " * Runs the `process()` function, which returns our accumulators\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'DoubleMuon': {'entries': 10000,\n",
+ " 'mass': Hist(\n",
+ " StrCategory(['opposite', 'same'], name='sign', label='sign'),\n",
+ " Regular(1000, 0.2, 200, transform=log, name='mass', label='$m_{\\\\mu\\\\mu}$ [GeV]'),\n",
+ " storage=Int64()) # Sum: 4939.0 (4951.0 with flow)}}"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import uproot\n",
+ "from coffea.nanoevents import NanoEventsFactory, BaseSchema\n",
+ "\n",
+ "filename = \"root://eospublic.cern.ch//eos/root-eos/cms_opendata_2012_nanoaod/Run2012B_DoubleMuParked.root\"\n",
+ "file = uproot.open(filename)\n",
+ "events = NanoEventsFactory.from_root(\n",
+ " file,\n",
+ " entry_stop=10000,\n",
+ " metadata={\"dataset\": \"DoubleMuon\"},\n",
+ " schemaclass=BaseSchema,\n",
+ ").events()\n",
+ "p = MyProcessor()\n",
+ "out = p.process(events)\n",
+ "out"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAEPCAYAAAC5sYRSAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAsDElEQVR4nO3de5hU5Znv/e/d1Seag8rJoBAaZxQFGohpQJQQdphtzKuJqImSiQ6eYsyWxODWiBkjnZgxvpm8OfDqRHGMshO3kHhITHR0J86QaIJIE4kKeIgIQkRogUBDH6vq3n/Uoau7q89dXV2rfp/r6qtrne+1Vvddz3rWs55l7o6IiARLQbYDEBGR/qfkLiISQEruIiIBpOQuIhJASu4iIgGk5C4iEkCF2Q4AYPTo0V5eXp7tMEREcsqmTZved/cx6aYNiuReXl5OdXV1tsMQEckpZrazo2mqlhERCSAldxGRAFJyFxEJoC7r3M3sx8B5wD53nxYfNxJYC5QDO4CL3f1gfNotwFVABPiyuz/Tm8Cam5vZvXs3DQ0NvVlc2igtLWX8+PEUFRVlOxQRGQDduaH6IHAX8L9Sxi0HnnX3O81seXz4ZjObAiwGpgInAL81s1PcPdLTwHbv3s3w4cMpLy/HzHq6uKRwd/bv38/u3buZNGlStsMRkQHQZbWMu/8eONBm9PnA6vjn1cCilPFr3L3R3d8G/gLM7k1gDQ0NjBo1Som9H5gZo0aN0lWQSB7pbZ378e6+ByD+e2x8/InArpT5dsfH9YoSe//RsRTJL/19QzVdBknbYbyZXWNm1WZWXVNT062Vh0IhZs6cydSpU5kxYwbf+973iEajAFRXV/PlL3+514FnWlVVFd/97nezHYaI5InePsS018zGufseMxsH7IuP3w1MSJlvPPBuuhW4+ypgFUBlZWW33hgyZMgQNm/eDMC+ffv4x3/8Rw4dOsQ3vvENKisrqays7N3e5IBIJEIoFMp2GCKSI3pbcn8CWBL/vAT4Zcr4xWZWYmaTgJOBF/sWYnpjx45l1apV3HXXXbg769at47zzzgNipeQlS5Zw9tlnU15ezmOPPcZXv/pVKioqOOecc2hubgZiT8a+//77QKzkv2DBAgAOHDjAokWLmD59OmeccQYvv/xycr1XXnklCxYs4KSTTmLlypVpY3v66ac5/fTTmTFjBgsXLkyO37p1a9plFy1axIc//GGmTp3KqlWrkuOHDRvGbbfdxpw5c1i/fj33338/p5xyCgsWLODzn/88S5cuBaCmpoaLLrqIWbNmMWvWLP7whz/001EWCZ5L7l3PJfeuz3YYmefunf4ADwN7gGZiJfOrgFHAs8Cb8d8jU+b/Z+At4HXgE12t39358Ic/7G1t3bq13bihQ4e2G3fsscf6e++95//1X//l5557rru7r1ixws866yxvamryzZs3+5AhQ/ypp55yd/dFixb5448/7u7uEydO9JqaGnd337hxo3/0ox91d/elS5d6VVWVu7s/++yzPmPGjOR6586d6w0NDV5TU+MjR470pqamVvHs27fPx48f79u3b3d39/3793e5bGKeuro6nzp1qr///vvusYPpa9eudXf3v/71rz5x4kTfv3+/NzU1+bx58/y6665zd/fPfvaz/txzz7m7+86dO/3UU09td5w6OqYi+ebie/7oF9/zx2yH0S+Aau8gr3ZZLePun+1g0sJ0I939X4B/6f7XS994B++A/cQnPkFRUREVFRVEIhHOOeccACoqKtixY0en63z++ed59NFHAfjYxz7G/v37OXToEADnnnsuJSUllJSUMHbsWPbu3cv48eOTy77wwgvMnz8/2eRw5MiRyWkdLbty5Uoef/xxAHbt2sWbb77JqFGjCIVCXHTRRQC8+OKLfPSjH02u7zOf+QxvvPEGAL/97W/ZunVrcjuHDx+mtraW4cOHd+8gikjgDIqOw3pr+/bthEIhxo4dy7Zt21pNKykpAaCgoICioqJka5GCggLC4TAAhYWFyRuyqc0E031hJJZPrBdiN3gT60pdtqOWKemWXbduHb/97W9Zv349ZWVlLFiwIBlLaWlpsp69oy8xgGg0yvr16xkyZEiH84hIfsnZ7gdqamq49tprWbp0aa+b+ZWXl7Np0yaAZEkdYP78+Tz00EMArFu3jtGjRzNixIhurXPu3Ln87ne/4+233wZi9fedOXToEMcddxxlZWW89tprvPDCC2nnmz17Nr/73e84ePAg4XC4Vbxnn302d911V3I4cdNZRNrbuucwW/ccznYYGZdTJff6+npmzpxJc3MzhYWFXHbZZdxwww29Xt+KFSu46qqruOOOO5gzZ05yfFVVFVdccQXTp0+nrKyM1atXd7KW1saMGcOqVau48MILiUajjB07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\n",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "out[\"DoubleMuon\"][\"mass\"].plot1d(ax=ax)\n",
+ "ax.set_xscale(\"log\")\n",
+ "ax.legend(title=\"Dimuon charge\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One could expand on this code to run over several chunks of the file, setting `entry_start` and `entry_stop` as appropriate. Then, several datasets could be processed by iterating over several files. However, the processor [Runner](https://coffeateam.github.io/coffea/api/coffea.processor.Runner.html) can help with this! One lists the datasets and corresponding files, the processor they want to run, and which executor they want to use. Available executors derive from `ExecutorBase` and are listed [here](https://coffeateam.github.io/coffea/modules/coffea.processor.html#classes). Since these files are very large, we limit to just reading the first few chunks of events from each dataset with `maxchunks`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e4fb80c50d72479a8b3906c885982cd4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Output()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "
Coffea: a column object framework for effective analysis.
+
When executing
+
>>> importcoffea
+
+
+
a subset of the full coffea package is imported into the python environment.
+Some packages must be imported explicitly, so as to avoid importing unnecessary
+and/or heavy dependencies. Below lists the packages available in the coffea namespace.
Work Queue is a
+distributed computing framework used to build large scale manager-worker
+applications, developed by the Cooperative Computing Lab
+(CCL) at the University of Notre Dame. This executor functions as the
+manager program which divides up a Coffea data analysis workload into
+discrete tasks. A large number of worker processes running on
+cluster or cloud systems will execute the tasks.
+
To set up Coffea and Work Queue together, you may need to
+create a Conda environment, install the software, and then
+create a tarball containing the environment. The tarball is
+sent to each worker in order to provide the same environment
+as the manager machine.
+
# Create a new environment
+condacreate--yes--namecoffea-env-cconda-forgepythoncoffeaxrootdndcctoolscondaconda-pack
+condaactivatecoffea-env
+
+# Pack the environment into a portable tarball.
+conda-pack--outputcoffea-env.tar.gz
+
+
+
To run an analysis, you must set up a work queue executor
+with appropriate arguments. Here is a complete example:
+
###############################################################################
+# Example of Coffea with the Work Queue executor.
+#
+# To execute, start this application, and then start workers that will connect
+# to it and execute tasks.
+#
+# Note that, as written, this only processes 4 data chunks and should complete
+# in a short time. For a real run, change maxchunks=None in the main program
+# below.
+#
+# For simple testing this script will automatically use one local worker. To
+# scale this up, see the wq.Factory configuration below to change to your
+# favorite batch system.
+###############################################################################
+
+###############################################################################
+# Sample processor class given in the Coffea manual.
+###############################################################################
+importwork_queueaswq
+
+fromcoffea.processorimportRunner
+fromcoffea.processorimportWorkQueueExecutor
+
+###############################################################################
+# Collect and display setup info.
+###############################################################################
+
+print("------------------------------------------------")
+print("Example Coffea Analysis with Work Queue Executor")
+print("------------------------------------------------")
+
+importgetpass
+
+wq_manager_name="coffea-wq-{}".format(getpass.getuser())
+wq_port=9123
+
+print("Manager Name: -M "+wq_manager_name)
+print("------------------------------------------------")
+
+
+###############################################################################
+# Define a custom Coffea processor
+###############################################################################
+
+fromcoffeaimportprocessor
+fromcoffea.nanoevents.methodsimportcandidate
+importhist
+fromcollectionsimportdefaultdict
+importawkwardasak
+
+# register our candidate behaviors
+ak.behavior.update(candidate.behavior)
+
+
+classMyProcessor(processor.ProcessorABC):
+ @property
+ defaccumulator(self):
+ return{
+ "sumw":defaultdict(float),
+ "mass":hist.Hist(
+ hist.axis.StrCategory([],name="dataset",label="Dataset"),
+ hist.axis.Regular(
+ 60,60,120,name="mass",label=r"$m_{\mu\mu}$ [GeV]"
+ ),
+ name="Events",
+ ),
+ }
+
+ defprocess(self,events):
+ # Note: This is required to ensure that behaviors are registered
+ # when running this code in a remote task.
+ ak.behavior.update(candidate.behavior)
+
+ output=self.accumulator
+
+ dataset=events.metadata["dataset"]
+ muons=ak.zip(
+ {
+ "pt":events.Muon_pt,
+ "eta":events.Muon_eta,
+ "phi":events.Muon_phi,
+ "mass":events.Muon_mass,
+ "charge":events.Muon_charge,
+ },
+ with_name="PtEtaPhiMCandidate",
+ )
+
+ cut=(ak.num(muons)==2)&(ak.sum(muons.charge)==0)
+ # add first and second muon in every event together
+ dimuon=muons[cut][:,0]+muons[cut][:,1]
+
+ output["sumw"][dataset]+=len(events)
+ output["mass"].fill(
+ dataset=dataset,
+ mass=dimuon.mass,
+ )
+
+ returnoutput
+
+ defpostprocess(self,accumulator):
+ returnaccumulator
+
+
+###############################################################################
+# Sample data sources come from CERN opendata.
+###############################################################################
+
+fileset={
+ "DoubleMuon":[
+ "root://eospublic.cern.ch//eos/root-eos/cms_opendata_2012_nanoaod/Run2012B_DoubleMuParked.root",
+ "root://eospublic.cern.ch//eos/root-eos/cms_opendata_2012_nanoaod/Run2012C_DoubleMuParked.root",
+ ],
+}
+
+
+###############################################################################
+# Configuration of the Work Queue Executor
+###############################################################################
+
+# secret passed between manager and workers for authentication
+my_password_file="password.txt"
+withopen(my_password_file,"w")asf:
+ f.write("my_secret_password")
+
+work_queue_executor_args={
+ # Automatically allocate cores, memory and disk to tasks. Adjusts to
+ # maximum values measured. Initially, tasks use whole workers.
+ "resources_mode":"auto",
+ # Split a processing task in half according to its chunksize when it
+ # exhausts the resources allocated to it.
+ "split_on_exhaustion":True,
+ # Options to control how workers find this manager.
+ "master_name":wq_manager_name,
+ # Port for manager to listen on: if zero, will choose automatically.
+ "port":wq_port,
+ # Secret passed between manager and workers
+ "password_file":my_password_file,
+ # The named conda environment tarball will be transferred to each worker,
+ # and activated. This is useful when coffea is not installed in the remote
+ # machines. conda enviroments are created with conda-pack, and should at
+ # least include coffea, ndcctools (both from conda-forge channel)
+ # and their dependencies.
+ #
+ # "environment_file": "coffea-env.tar.gz",
+ # Debugging: Display notes about each task submitted/complete.
+ "verbose":True,
+ # Debugging: Display output of task if not empty.
+ "print_stdout":False,
+ # Debugging: Produce a lot at the manager side of things.
+ "debug_log":"coffea-wq.log",
+}
+
+executor=WorkQueueExecutor(**work_queue_executor_args)
+
+
+###############################################################################
+# Run the analysis using local Work Queue workers
+###############################################################################
+
+importtime
+
+tstart=time.time()
+
+workers=wq.Factory(
+ # local runs:
+ batch_type="local",
+ manager_host_port="localhost:{}".format(wq_port)
+ # with a batch system, e.g., condor.
+ # (If coffea not at the installation site, then a conda
+ # environment_file should be defined in the work_queue_executor_args.)
+ # batch_type="condor", manager_name=wq_manager_name
+)
+
+workers.max_workers=2
+workers.min_workers=1
+workers.cores=2
+workers.memory=1000# MB
+workers.disk=2000# MB
+workers.password=my_password_file
+
+# Instead of declaring the python environment per task, you can set it in
+# the factory directly. This is useful if you are going to run a workflow
+# several times using the same set of workers. It also ensures that the worker
+# itself executes in a friendly environment.
+# workers.python_package = "coffea-env.tar.gz"
+#
+# The factory tries to write temporary files to $TMPDIR (usually /tmp). When
+# this is not available, or causes errors, this scracth directory can be
+# manually set.
+# workers.scratch_dir = "./my-scratch-dir"
+
+withworkers:
+ # define the Runner instance
+ run_fn=Runner(
+ executor=executor,
+ chunksize=100000,
+ maxchunks=4,# change this to None for a large run
+ )
+ # execute the analysis on the given dataset
+ hists=run_fn(fileset,"Events",MyProcessor())
+
+elapsed=time.time()-tstart
+
+
+print(hists)
+print(hists["mass"])
+
+# (assert only valid when using maxchunks=4)
+asserthists["sumw"]["DoubleMuon"]==400224
+
+
+
When executing this example,
+you should see that Coffea begins to run, and a progress bar
+shows the creation of tasks. Workers are created locally using the factory
+declared.
+
You can also launch workers outside python. For testing purposes, you can start
+a single worker on the same machine, and direct it to connect to your manager
+process, like this:
With a single worker, the process will be gradual as it completes
+one task (or a few tasks) at a time. The output will be similar to this:
+
------------------------------------------------
+Example Coffea Analysis with Work Queue Executor
+------------------------------------------------
+Manager Name: -M coffea-wq-btovar
+------------------------------------------------
+Listening for work queue workers on port 9123.
+submitted preprocessing task id 1 item pre_0, with 1 file
+submitted preprocessing task id 2 item pre_1, with 1 file
+preprocessing task id 2 item pre_1 with 1 events on localhost. return code 0 (success)
+allocated cores: 2.0, memory: 1000 MB, disk 2000 MB, gpus: 0.0
+measured cores: 0.3, memory: 120 MB, disk 6 MB, gpus: 0.0, runtime 3.1 s
+preprocessing task id 1 item pre_0 with 1 events on localhost. return code 0 (success)
+allocated cores: 2.0, memory: 1000 MB, disk 2000 MB, gpus: 0.0
+measured cores: 0.3, memory: 120 MB, disk 6 MB, gpus: 0.0, runtime 2.9 s
+submitted processing task id 3 item p_2, with 100056 event
+submitted processing task id 4 item p_3, with 100056 event
+submitted processing task id 5 item p_4, with 100056 event
+submitted processing task id 6 item p_5, with 100056 event
+Preprocessing 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2/2 [ 0:00:06 < 0:00:00 | 0.3 file/s ]
+ Submitted 0% 0/400224 [ 0:00:00 < -:--:-- | ? event/s ]
+ Processed 0% 0/400224 [ 0:00:00 < -:--:-- | ? event/s ]
+ Accumulated 0% 0/1 [ 0:00:00 < -:--:-- | ? tasks/s ]
+
+
+
To run at larger scale, you will need to run a large number
+of workers on a cluster or other infrastructure. For example,
+to submit 32 workers to an HTCondor pool: