
¿Qué es Awkward Array?¶
La lección anterior incluía una rebanada complicada:
corte = muones["nMuon"] == 2
pt0 = muones["Muon_pt", corte, 0]Las tres partes de la rebanada muones["Muon_pt", corte, 0]
seleccionan el campo
"Muon_pt"de todos los registros del array,aplican
corte, un array booleano, para seleccionar solo los eventos con dos muones,seleccionan el primer (
0) muón de cada uno de esos pares. De manera similar para los segundos (1) muones.
NumPy no sería capaz de realizar una rebanada así, ni siquiera de representar un array de listas de longitud variable sin recurrir a arrays de objetos.
import numpy as np
# genera un ValueError
np.array([[0.0, 1.1, 2.2], [], [3.3, 4.4], [5.5], [6.6, 7.7, 8.8, 9.9]])---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[1], line 4
1 import numpy as np
2
3 # genera un ValueError
----> 4 np.array([[0.0, 1.1, 2.2], [], [3.3, 4.4], [5.5], [6.6, 7.7, 8.8, 9.9]])
ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (5,) + inhomogeneous part.Awkward Array está diseñado para llenar este vacío:
import awkward as ak
ak.Array([[0.0, 1.1, 2.2], [], [3.3, 4.4], [5.5], [6.6, 7.7, 8.8, 9.9]])A los arrays como este se les llama a veces “arrays irregulares” o “desiguales” (en inglés, “jagged arrays” o “ragged arrays”).
Rebanadas en Awkward Array¶
Las rebanadas básicas son una generalización de las de NumPy: lo que NumPy haría si tuviera listas de longitud variable.
array = ak.Array([[0.0, 1.1, 2.2], [], [3.3, 4.4], [5.5], [6.6, 7.7, 8.8, 9.9]])
array.tolist()[[0.0, 1.1, 2.2], [], [3.3, 4.4], [5.5], [6.6, 7.7, 8.8, 9.9]]array[2]array[-1, 1]np.float64(7.7)array[2:, 0]array[2:, 1:]array[:, 0]---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[8], line 1
----> 1 array[:, 0]
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/highlevel.py:1118, in Array.__getitem__(self, where)
689 def __getitem__(self, where):
690 """
691 Args:
692 where (many types supported; see below): Index of positions to
(...) 1116 have the same dimension as the array being indexed.
1117 """
-> 1118 with ak._errors.SlicingErrorContext(self, where):
1119 # Handle named axis
1120 (_, ndim) = self._layout.minmax_depth
1121 named_axis = _get_named_axis(self)
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/_errors.py:79, in ErrorContext.__exit__(self, exception_type, exception_value, traceback)
77 self._slate.__dict__.clear()
78 # Handle caught exception
---> 79 raise self.decorate_exception(exception_type, exception_value)
80 else:
81 # Step out of the way so that another ErrorContext can become primary.
82 if self.primary() is self:
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/highlevel.py:1126, in Array.__getitem__(self, where)
1122 where = _normalize_named_slice(named_axis, where, ndim)
1124 NamedAxis.mapping = named_axis
-> 1126 indexed_layout = prepare_layout(self._layout._getitem(where, NamedAxis))
1128 if NamedAxis.mapping:
1129 return ak.operations.ak_with_named_axis._impl(
1130 indexed_layout,
1131 named_axis=NamedAxis.mapping,
(...) 1134 attrs=self._attrs,
1135 )
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/contents/content.py:651, in Content._getitem(self, where, named_axis)
642 named_axis.mapping = _named_axis
644 next = ak.contents.RegularArray(
645 this,
646 this.length,
647 1,
648 parameters=None,
649 )
--> 651 out = next._getitem_next(nextwhere[0], nextwhere[1:], None)
653 if out.length is not unknown_length and out.length == 0:
654 return out._getitem_nothing()
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/contents/regulararray.py:595, in RegularArray._getitem_next(self, head, tail, advanced)
589 nextcontent = self._content._carry(nextcarry, True)
591 if advanced is None or (
592 advanced.length is not unknown_length and advanced.length == 0
593 ):
594 return RegularArray(
--> 595 nextcontent._getitem_next(nexthead, nexttail, advanced),
596 nextsize,
597 self.length,
598 parameters=self._parameters,
599 )
600 else:
601 nextadvanced = ak.index.Index64.empty(nextcarry.length, nplike)
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/contents/listarray.py:764, in ListArray._getitem_next(self, head, tail, advanced)
758 head = ak._slicing.normalize_integer_like(head)
759 assert (
760 nextcarry.nplike is self._backend.nplike
761 and self._starts.nplike is self._backend.nplike
762 and self._stops.nplike is self._backend.nplike
763 )
--> 764 self._maybe_index_error(
765 self._backend[
766 "awkward_ListArray_getitem_next_at",
767 nextcarry.dtype.type,
768 self._starts.dtype.type,
769 self._stops.dtype.type,
770 ](
771 nextcarry.data,
772 self._starts.data,
773 self._stops.data,
774 lenstarts,
775 head,
776 ),
777 slicer=head,
778 )
779 nextcontent = self._content._carry(nextcarry, True)
780 return nextcontent._getitem_next(nexthead, nexttail, advanced)
File ~/miniconda3/envs/skhep-tutorial/lib/python3.12/site-packages/awkward/contents/content.py:297, in Content._maybe_index_error(self, error, slicer)
295 else:
296 message = self._backend.format_kernel_error(error)
--> 297 raise ak._errors.index_error(self, slicer, message)
IndexError: cannot slice ListArray (of length 5) with array(0): index out of range while attempting to get index 0 (in compiled code: https://github.com/scikit-hep/awkward/blob/awkward-cpp-56/awkward-cpp/src/cpu-kernels/awkward_ListArray_getitem_next_at.cpp#L21)
This error occurred while attempting to slice
<Array [[0, 1.1, 2.2], ..., [6.6, 7.7, ..., 9.9]] type='5 * var * float64'>
with
(:, 0)Pregunta rápida: ¿por qué la última genera un error?
Las rebanadas con booleanos y con enteros también funcionan:
array[[True, False, True, False, True]]array[[2, 3, 3, 1]]Igual que en NumPy, se pueden calcular arrays booleanos para las rebanadas, y funciones como ak.num son útiles para eso.
ak.num(array)ak.num(array) > 0array[ak.num(array) > 0, 0]array[ak.num(array) > 1, 1]Ahora considera esto (similar a un ejemplo de la primera lección):
corte = array * 10 % 2 == 0
array[corte]Este array, corte, no es solo un array de booleanos. Es un array irregular de booleanos. Todas sus listas anidadas encajan en las listas anidadas de array, por lo que puede seleccionar números en profundidad, en lugar de seleccionar listas.
Aplicación: seleccionar partículas, en lugar de eventos¶
Volviendo al TTree grande de la lección anterior,
import uproot
url_archivo = "root://eospublic.cern.ch//eos/opendata/cms/derived-data/AOD2NanoAODOutreachTool/Run2012BC_DoubleMuParked_Muons.root"
# Si estás en Windows o no tienes XRootD instalado, puedes usar esta url en su lugar
# url_archivo = "https://root.cern/files/rootbench/Run2012BC_DoubleMuParked_Muons.root"
archivo = uproot.open(url_archivo)
tree = archivo["Events"]
muon_pt = tree["Muon_pt"].array(entry_stop=10)Este array irregular de booleanos selecciona todos los muones con más de 20 GeV:
corte_particula = muon_pt > 20
muon_pt[corte_particula]y este array de booleanos no irregular (hecho con ak.any) selecciona todos los eventos que tienen un muón con más de 20 GeV:
corte_evento = ak.any(muon_pt > 20, axis=1)
muon_pt[corte_evento]Pregunta rápida: construye exactamente el mismo corte_evento usando ak.max.
Pregunta rápida: aplica ambos cortes; es decir, selecciona los muones con más de 20 GeV de los eventos que los tienen.
Sugerencia: vas a querer construir un
depurados = muon_pt[corte_particula]intermedio, y no puedes usar la variable corte_evento tal como está.
Sugerencia: el resultado final debería ser un array irregular, igual que muon_pt, pero con menos listas y menos elementos en esas listas.
Solución (¡no hagas trampa!)
depurados = muon_pt[corte_particula]
resultado_final = depurados[corte_evento]Combinatoria en Awkward Array¶
Las listas de longitud variable presentan más problemas que solo el rebanado y el cálculo de fórmulas array por array. A menudo queremos combinar partículas en todos los pares posibles (dentro de cada evento) para buscar cadenas de desintegración.
Pares a partir de dos arrays, pares a partir de un solo array¶
Awkward Array tiene funciones que generan estas combinaciones. Por ejemplo, ak.cartesian toma un producto cartesiano por evento (cuando axis=1, el valor predeterminado).

numeros = ak.Array([[1, 2, 3], [], [5, 7], [11]])
letras = ak.Array([["a", "b"], ["c"], ["d"], ["e", "f"]])
pares = ak.cartesian((numeros, letras))Estos pares son 2-tuplas, que se parecen a registros en la forma en que se rebanan de un array: usando cadenas.
pares["0"]pares["1"]izquierda, derecha = ak.unzip(pares)
izquierdaderechaTen en cuenta que estos izquierda y derecha no son los numeros y letras originales: han sido duplicados y tienen la misma forma.
El producto cartesiano es equivalente a este bucle for de C++ sobre dos colecciones:
for (int i = 0; i < numeros.size(); i++) {
for (int j = 0; j < letras.size(); j++) {
// calcular la fórmula con numeros[i] y letras[j]
}
}A veces, sin embargo, queremos encontrar todos los pares dentro de una sola colección, sin repetición. Eso sería equivalente a este bucle for de C++:
for (int i = 0; i < numeros.size(); i++) {
for (int j = i + 1; j < numeros.size(); j++) {
// calcular la fórmula con numeros[i] y numeros[j]
}
}La función de Awkward para este caso es ak.combinations.

pares = ak.combinations(numeros, 2)
pares
izquierda, derecha = ak.unzip(pares)
izquierda * derecha # se alinean, así que podemos calcular fórmulasAplicación a los dimuones¶
La búsqueda de dimuones de la lección anterior fue un poco ingenua, en el sentido de que exigíamos que existieran exactamente dos muones en cada evento y solo calculábamos la masa de esa combinación. Si hubiera un tercer muón presente porque se trata de una desintegración electrodébil compleja o porque algo se midió mal, no veríamos los otros dos muones. Podrían ser dimuones reales.
Un mejor procedimiento sería buscar todos los pares de muones en un evento y aplicar algún criterio para seleccionarlos.
En este ejemplo, juntaremos con ak.zip las variables de los muones en registros.
import uproot
import awkward as ak
url_archivo = "root://eospublic.cern.ch//eos/opendata/cms/derived-data/AOD2NanoAODOutreachTool/Run2012BC_DoubleMuParked_Muons.root"
# Si estás en Windows o no tienes XRootD instalado, puedes usar esta url en su lugar
# url_archivo = "https://root.cern/files/rootbench/Run2012BC_DoubleMuParked_Muons.root"
archivo = uproot.open(url_archivo)
tree = archivo["Events"]
arrays = tree.arrays(filter_name="/Muon_(pt|eta|phi|charge)/", entry_stop=10000)
muones = ak.zip(
{
"pt": arrays["Muon_pt"],
"eta": arrays["Muon_eta"],
"phi": arrays["Muon_phi"],
"charge": arrays["Muon_charge"],
}
)arrays.typeArrayType(RecordType([ListType(NumpyType('float32')), ListType(NumpyType('float32')), ListType(NumpyType('float32')), ListType(NumpyType('int32'))], ['Muon_pt', 'Muon_eta', 'Muon_phi', 'Muon_charge']), 10000, None)muones.typeArrayType(ListType(RecordType([NumpyType('float32'), NumpyType('float32'), NumpyType('float32'), NumpyType('int32')], ['pt', 'eta', 'phi', 'charge'])), 10000, None)La diferencia entre arrays y muones es que arrays contiene listas separadas de "Muon_pt", "Muon_eta", "Muon_phi", "Muon_charge", mientras que muones contiene listas de registros con los campos "pt", "eta", "phi", "charge".
Ahora podemos calcular pares de objetos muón
pares = ak.combinations(muones, 2)
pares.typeArrayType(ListType(RecordType([RecordType([NumpyType('float32'), NumpyType('float32'), NumpyType('float32'), NumpyType('int32')], ['pt', 'eta', 'phi', 'charge']), RecordType([NumpyType('float32'), NumpyType('float32'), NumpyType('float32'), NumpyType('int32')], ['pt', 'eta', 'phi', 'charge'])], None)), 10000, None)y separarlos en arrays del primer muón y del segundo muón de cada par.
mu1, mu2 = ak.unzip(pares)Pregunta rápida: ¿cómo garantizarías que todas las listas de registros en mu1 y mu2 tengan las mismas longitudes? Sugerencia: consulta ak.num y ak.all.
Dado que sí tienen las mismas longitudes, podemos usarlos en una fórmula.
import numpy as np
masa = np.sqrt(
2 * mu1.pt * mu2.pt * (np.cosh(mu1.eta - mu2.eta) - np.cos(mu1.phi - mu2.phi))
)Pregunta rápida: ¿cuántas masas tenemos en cada evento? ¿Cómo se compara esto con muones, mu1 y mu2?
Graficar el array irregular¶
Dado que esta masa es un array irregular, no se puede histogramar directamente. Los histogramas toman un conjunto de números como entrada, pero este array contiene listas.
Suponiendo que solo quieres graficar los números de las listas, puedes usar ak.flatten para aplanar un nivel de listas, o ak.ravel para aplanar todos los niveles.
import hist
hist.Hist(hist.axis.Regular(120, 0, 120, label="masa [GeV]")).fill(
ak.ravel(masa)
).plot();
Alternativamente, supongamos que quieres graficar la masa candidata máxima de cada evento, sesgándola hacia los bosones Z. ak.max es una función diferente que selecciona un elemento de cada lista, cuando se usa con axis=1.
ak.max(masa, axis=1)Algunos valores son None porque no hay máximo de una lista vacía. Llamar a ak.flatten con axis=0 elimina estos valores faltantes,
ak.flatten(ak.max(masa, axis=1), axis=0)pero eliminar de entrada las listas vacías consigue lo mismo.
ak.max(masa[ak.num(masa) > 0], axis=1)Ten en cuenta que aquí ak.ravel no es intercambiable con ak.flatten: aplana todos los niveles de anidamiento, pero conserva los valores faltantes, así que ak.ravel(ak.max(masa, axis=1)) todavía contiene None. Para eliminarlos, usa ak.drop_none. Vas a necesitar esto en el Ejercicio 3.