f9columnar 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
f9columnar/__init__.py ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore
f9columnar/arrays.py ADDED
@@ -0,0 +1,286 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from dataclasses import field as dataclass_field
4
+ from typing import List
5
+
6
+ import awkward as ak
7
+ import numpy as np
8
+
9
+ from f9columnar.processors import Processor
10
+ from f9columnar.utils.ak_helpers import check_list_type, check_numpy_type
11
+
12
+
13
+ @dataclass
14
+ class BaseArrays:
15
+ array: ak.Array
16
+ fields: list = dataclass_field(default_factory=list)
17
+
18
+ def __post_init__(self):
19
+ self.fields = self.array.fields
20
+
21
+ @property
22
+ def shape(self):
23
+ return (len(self.array), len(self.fields))
24
+
25
+ def mask(self, mask, inplace=True):
26
+ if inplace:
27
+ self.array = self.array[mask]
28
+ return self
29
+ else:
30
+ return self.array[mask]
31
+
32
+ def _check_type(self, array):
33
+ raise NotImplementedError
34
+
35
+ def __setitem__(self, field, new_array):
36
+ if not self._check_type(new_array):
37
+ raise ValueError(f"Field {field} is not of the correct type!")
38
+
39
+ self.array[field] = new_array
40
+ self.fields.append(field)
41
+
42
+ return self
43
+
44
+ def __getitem__(self, value):
45
+ if type(value) is str:
46
+ return self.array[value]
47
+ elif type(value) is int:
48
+ return self.array[value]
49
+ elif type(value) is ak.Array or type(value) is np.ndarray:
50
+ return self.mask(value)
51
+ elif type(value) is slice:
52
+ return self.array[value]
53
+ else:
54
+ raise ValueError("Value must be a field name or an array mask!")
55
+
56
+ def __len__(self):
57
+ return len(self.array)
58
+
59
+ def __delitem__(self, field):
60
+ self.array = ak.without_field(self.array, field)
61
+ self.fields.remove(field)
62
+ return self
63
+
64
+
65
+ @dataclass
66
+ class FlatArrays(BaseArrays):
67
+ def _check_type(self, array):
68
+ return check_numpy_type(array)
69
+
70
+ def __str__(self):
71
+ return f"{self.__class__.__name__}(shape={self.shape})"
72
+
73
+
74
+ @dataclass
75
+ class JaggedArrays(BaseArrays):
76
+ def _check_type(self, array):
77
+ return check_list_type(array)
78
+
79
+ def _get_non_empty_mask(self):
80
+ return ak.num(self.array[self.fields[0]]) > 0
81
+
82
+ def remove_empty(self):
83
+ mask = self._get_non_empty_mask()
84
+ self.mask(mask, inplace=True)
85
+ return self
86
+
87
+ def __str__(self):
88
+ return f"{self.__class__.__name__}(shape={self.shape})"
89
+
90
+
91
+ @dataclass
92
+ class Arrays:
93
+ """This class is used to store flat and jagged arrays separately. It is used to store awkward arrays in a more
94
+ structured way. It is initialized in the `ArrayProcessor` to separate flat and jagged fields. The funconality is
95
+ similar to `ak.Array` but with seperate handling of flat and jagged arrays. It is intended to make it easier to
96
+ work with ntuples that contain both flat (e.g. MC weights) and jagged fields (e.g. pt).
97
+
98
+ Parameters
99
+ ----------
100
+ flat_arrays : FlatArrays
101
+ Flat arrays.
102
+ jagged_arrays : JaggedArrays
103
+ Jagged arrays.
104
+
105
+ Other Parameters
106
+ ----------------
107
+ flat_fields : list
108
+ List of flat fields.
109
+ jagged_fields : list
110
+ List of jagged fields.
111
+ fields : list
112
+ List of all fields.
113
+
114
+ Attributes
115
+ ----------
116
+ shape : tuple
117
+ Shape of the arrays (number of events, number of flat array, number of jagged arrays).
118
+
119
+ Methods
120
+ -------
121
+ update_fields()
122
+ Update flat and jagged fields on (or after) initialization.
123
+ mask_flat(mask, inplace=True)
124
+ Mask flat arrays.
125
+ mask_jagged(mask, inplace=True)
126
+ Mask jagged arrays.
127
+ remove_empty(inplace=True, return_mask=False)
128
+ Remove empty sub arrays.
129
+
130
+ Warning
131
+ -------
132
+ Masking is done inplace by default using `__setitem__`. To return a masked array, set `inplace=False` and use
133
+ `mask_flat` or `mask_jagged` methods.
134
+
135
+ Note
136
+ ----
137
+ To delete a field, use `del arrays[field]` syntax.
138
+
139
+ """
140
+
141
+ flat_arrays: FlatArrays
142
+ jagged_arrays: JaggedArrays
143
+
144
+ flat_fields: List[str] = dataclass_field(default_factory=list)
145
+ jagged_fields: List[str] = dataclass_field(default_factory=list)
146
+ fields: List[str] = dataclass_field(default_factory=list)
147
+
148
+ def __post_init__(self):
149
+ self.update_fields()
150
+
151
+ def update_fields(self):
152
+ self.flat_fields = self.flat_arrays.fields
153
+ self.jagged_fields = self.jagged_arrays.fields
154
+ self.fields = self.flat_fields + self.jagged_fields
155
+ return self
156
+
157
+ @property
158
+ def shape(self):
159
+ return (len(self), len(self.flat_fields), len(self.jagged_fields))
160
+
161
+ def mask_flat(self, mask, inplace=True):
162
+ if inplace:
163
+ self.flat_arrays.mask(mask, inplace=True)
164
+ self.jagged_arrays.mask(mask, inplace=True)
165
+ return self
166
+ else:
167
+ return self.flat_arrays.mask(mask, inplace=False), self.jagged_arrays.mask(mask, inplace=False)
168
+
169
+ def mask_jagged(self, mask, inplace=True):
170
+ if inplace:
171
+ self.jagged_arrays.mask(mask, inplace=True)
172
+ return self
173
+ else:
174
+ return self.jagged_arrays.mask(mask, inplace=False)
175
+
176
+ def remove_empty(self, inplace=True, return_mask=False):
177
+ non_empty_mask = self.jagged_arrays._get_non_empty_mask()
178
+ if return_mask:
179
+ return self.mask_flat(non_empty_mask, inplace=inplace), non_empty_mask
180
+ else:
181
+ return self.mask_flat(non_empty_mask, inplace=inplace)
182
+
183
+ def __setitem__(self, field, new_array):
184
+ if check_numpy_type(new_array):
185
+ self.flat_arrays[field] = new_array
186
+ elif check_list_type(new_array):
187
+ self.jagged_arrays[field] = new_array
188
+ else:
189
+ raise ValueError("Array is not of the correct type!")
190
+
191
+ self.update_fields()
192
+
193
+ return self
194
+
195
+ def _get_by_field(self, field):
196
+ if field in self.flat_fields:
197
+ return self.flat_arrays[field]
198
+ elif field in self.jagged_fields:
199
+ return self.jagged_arrays[field]
200
+ else:
201
+ raise KeyError(f"Field {field} not found in flat or jagged fields!")
202
+
203
+ def _get_by_slice(self, slice_idx):
204
+ return self.flat_arrays[slice_idx], self.jagged_arrays[slice_idx]
205
+
206
+ def _get_by_mask(self, mask):
207
+ if check_numpy_type(mask):
208
+ self.mask_flat(mask, inplace=True)
209
+ elif check_list_type(mask):
210
+ self.mask_jagged(mask, inplace=True)
211
+ else:
212
+ raise ValueError("Mask is not of the correct type!")
213
+
214
+ return self
215
+
216
+ def __getitem__(self, value):
217
+ if type(value) is str:
218
+ return self._get_by_field(value)
219
+ elif type(value) is int:
220
+ return self._get_by_slice(value)
221
+ elif type(value) is ak.Array or type(value) is np.ndarray:
222
+ return self._get_by_mask(value)
223
+ elif type(value) is slice:
224
+ return self._get_by_slice(value)
225
+ else:
226
+ raise ValueError("Value must be a field name, int, array mask or slice!")
227
+
228
+ def __len__(self):
229
+ len_flat, len_jagged = len(self.flat_arrays), len(self.jagged_arrays)
230
+ assert len_flat == len_jagged, "Flat and jagged arrays have different lengths!"
231
+ return len_flat
232
+
233
+ def __delitem__(self, field):
234
+ if field in self.flat_fields:
235
+ del self.flat_arrays[field]
236
+ elif field in self.jagged_fields:
237
+ del self.jagged_arrays[field]
238
+ else:
239
+ raise KeyError(f"Field {field} not found in flat or jagged fields!")
240
+
241
+ self.fields.remove(field)
242
+
243
+ return self
244
+
245
+ def __str__(self):
246
+ return f"{self.__class__.__name__}(shape={self.shape})"
247
+
248
+
249
+ class ArrayProcessor(Processor):
250
+ def __init__(self):
251
+ """Processor to separate flat and jagged fields into two arrays from an awkward array.
252
+
253
+ Note
254
+ ----
255
+ This will modify the input arrays. It is intended to be used as the first processor in the analysis chain in
256
+ the case of ntuples that contain both flat and jagged fields.
257
+
258
+ """
259
+ super().__init__(name="arrayProcessor")
260
+ self.flat_fields, self.jagged_fields, self.other_fields = [], [], []
261
+
262
+ def _get_fields(self, arrays):
263
+ arrays_contents = arrays.type.content.contents
264
+ arrays_fields = arrays.type.content.fields
265
+
266
+ for content, array_field in zip(arrays_contents, arrays_fields):
267
+ if isinstance(content, ak.types.NumpyType):
268
+ self.flat_fields.append(array_field)
269
+ elif isinstance(content, ak.types.ListType):
270
+ self.jagged_fields.append(array_field)
271
+ else:
272
+ self.other_fields.append(array_field)
273
+
274
+ if len(self.other_fields) != 0:
275
+ logging.warning(f"Fields {self.other_fields} are not flat or jagged! Will not be processed.")
276
+
277
+ return self.flat_fields, self.jagged_fields
278
+
279
+ def run(self, arrays):
280
+ self._get_fields(arrays)
281
+
282
+ flat_arrays, jagged_arrays = arrays[self.flat_fields], arrays[self.jagged_fields]
283
+
284
+ arrays = Arrays(FlatArrays(flat_arrays), JaggedArrays(jagged_arrays))
285
+
286
+ return {"arrays": arrays}
@@ -0,0 +1,165 @@
1
+ import copy
2
+ import logging
3
+ import time
4
+ from abc import abstractmethod
5
+ from collections import OrderedDict
6
+
7
+ import awkward as ak
8
+
9
+ from f9columnar.processors import Processor
10
+ from f9columnar.utils.helpers import load_json
11
+
12
+
13
+ class ObjectCollection:
14
+ def __init__(self, collection_name, *objects, init=False):
15
+ """Collection of objects.
16
+
17
+ Parameters
18
+ ----------
19
+ collection_name : str
20
+ Name of this collection.
21
+ objects : *args
22
+ obj objects to store.
23
+ init : bool
24
+ Whether to initialize the objects or not.
25
+ """
26
+ self.name = collection_name
27
+ self.init = init
28
+ self.objects = OrderedDict()
29
+
30
+ for obj in objects:
31
+ if init:
32
+ obj = obj()
33
+ self.objects[obj.name] = obj
34
+ else:
35
+ self.objects[obj.name] = obj
36
+
37
+ self.branch_names = self._get_branch_names()
38
+
39
+ def __getitem__(self, name):
40
+ return self.objects[name]
41
+
42
+ def __add__(self, obj):
43
+ if isinstance(obj, ObjectCollection):
44
+ for v in obj.objects.values():
45
+ if v in self.objects:
46
+ raise ValueError(f"Object {v.name} already exists in the {self.collection_name} collection!")
47
+ else:
48
+ self.objects[v.name] = v
49
+ else:
50
+ if obj in self.objects:
51
+ raise ValueError(f"Object {obj.name} already exists in the {self.collection_name} collection!")
52
+ else:
53
+ if self.init:
54
+ obj = obj()
55
+ self.objects[obj.name] = obj
56
+ else:
57
+ self.objects[obj.name] = obj
58
+
59
+ self.branch_names = self._get_branch_names()
60
+
61
+ return self
62
+
63
+ def _get_branch_names(self):
64
+ branch_names = []
65
+ for v in self.objects.values():
66
+ if not hasattr(v, "branch_name"):
67
+ continue
68
+
69
+ branch_name = v.branch_name
70
+
71
+ if branch_name is None:
72
+ continue
73
+ elif type(branch_name) is list:
74
+ branch_names += [br for br in branch_name if br is not None]
75
+ else:
76
+ branch_names.append(v.branch_name)
77
+
78
+ return branch_names
79
+
80
+ def branch_name_filter(self, branch):
81
+ if branch.name in self.branch_names:
82
+ return True
83
+ else:
84
+ return False
85
+
86
+ def as_list(self):
87
+ return list(self.objects.values())
88
+
89
+ def __str__(self):
90
+ str_output = f"Object Collection\n{17 * '-'}\n"
91
+
92
+ for name, obj in self.objects.items():
93
+ str_output += f"{name}: {str(obj)}\n"
94
+
95
+ return str_output[:-1]
96
+
97
+
98
+ class Variable(Processor):
99
+ name = None
100
+ branch_name = None
101
+
102
+ def __init__(self):
103
+ super().__init__(self.name)
104
+
105
+ @abstractmethod
106
+ def run(self):
107
+ pass
108
+
109
+
110
+ class VariableCollection(ObjectCollection):
111
+ def __init__(self, *variables, init=False):
112
+ super().__init__("Variables", *variables, init=init)
113
+
114
+
115
+ class Cut(Processor):
116
+ name = None
117
+ branch_name = None
118
+
119
+ def __init__(self):
120
+ super().__init__(self.name)
121
+ self.start_n, self.end_n = None, None
122
+
123
+ def _run(self, arrays, **kwargs):
124
+ self.start_n, start_time = len(arrays), time.time()
125
+ self._results = copy.deepcopy(self.run(arrays, **kwargs))
126
+ self.end_n, self.delta_time = len(self._results["arrays"]), time.time() - start_time
127
+ return self
128
+
129
+ @abstractmethod
130
+ def run(self):
131
+ pass
132
+
133
+
134
+ class CutCollection(ObjectCollection):
135
+ def __init__(self, *cuts, init=False):
136
+ super().__init__("Cuts", *cuts, init=init)
137
+
138
+
139
+ class Weight(Processor):
140
+ name = None
141
+ branch_name = None
142
+
143
+ def __init__(self):
144
+ """MC weights processor.
145
+
146
+ References
147
+ ----------
148
+ [1] - https://ipnp.cz/scheirich/?page_id=292
149
+
150
+ """
151
+ super().__init__(self.name)
152
+
153
+ @abstractmethod
154
+ def run(self):
155
+ pass
156
+
157
+
158
+ class WeightCollection(ObjectCollection):
159
+ def __init__(self, *weights, init=False):
160
+ super().__init__("Weights", *weights, init=init)
161
+
162
+
163
+ class HistogramCollection(ObjectCollection):
164
+ def __init__(self, *histograms, init=False):
165
+ super().__init__("Histograms", *histograms, init=init)