rootloader 1.3.1__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. rootloader-1.4.0/.claude/settings.local.json +14 -0
  2. rootloader-1.4.0/.coverage +0 -0
  3. rootloader-1.4.0/CLAUDE.md +31 -0
  4. {rootloader-1.3.1 → rootloader-1.4.0}/PKG-INFO +4 -1
  5. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/tdirectory.md +3 -3
  6. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/th2.md +1 -1
  7. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/ttree.md +30 -24
  8. {rootloader-1.3.1 → rootloader-1.4.0}/pyproject.toml +3 -0
  9. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/tdirectory.py +6 -1
  10. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/th2.py +3 -2
  11. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/tleaf.py +1 -1
  12. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/ttree.py +11 -9
  13. rootloader-1.4.0/rootloader/version.py +1 -0
  14. rootloader-1.4.0/tests/README.md +315 -0
  15. rootloader-1.4.0/tests/conftest.py +319 -0
  16. rootloader-1.4.0/tests/test_attrdict.py +159 -0
  17. rootloader-1.4.0/tests/test_package.py +47 -0
  18. rootloader-1.4.0/tests/test_tdirectory.py +368 -0
  19. rootloader-1.4.0/tests/test_tfile.py +278 -0
  20. rootloader-1.4.0/tests/test_th1.py +341 -0
  21. rootloader-1.4.0/tests/test_th2.py +301 -0
  22. rootloader-1.4.0/tests/test_tleaf.py +128 -0
  23. rootloader-1.4.0/tests/test_ttree.py +743 -0
  24. rootloader-1.3.1/rootloader/version.py +0 -1
  25. {rootloader-1.3.1 → rootloader-1.4.0}/.gitignore +0 -0
  26. {rootloader-1.3.1 → rootloader-1.4.0}/LICENSE +0 -0
  27. {rootloader-1.3.1 → rootloader-1.4.0}/README.md +0 -0
  28. {rootloader-1.3.1 → rootloader-1.4.0}/docs/README.md +0 -0
  29. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/attrdict.md +0 -0
  30. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/index.md +0 -0
  31. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/tfile.md +0 -0
  32. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/th1.md +0 -0
  33. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/tleaf.md +0 -0
  34. {rootloader-1.3.1 → rootloader-1.4.0}/docs/rootloader/version.md +0 -0
  35. {rootloader-1.3.1 → rootloader-1.4.0}/gen_documentation.bash +0 -0
  36. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/__init__.py +0 -0
  37. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/attrdict.py +0 -0
  38. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/tfile.py +0 -0
  39. {rootloader-1.3.1 → rootloader-1.4.0}/rootloader/th1.py +0 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python -m py_compile rootloader/th2.py rootloader/tleaf.py rootloader/ttree.py rootloader/tdirectory.py)",
5
+ "Bash(python3 -m py_compile rootloader/th2.py rootloader/tleaf.py rootloader/ttree.py rootloader/tdirectory.py)",
6
+ "Bash(python -m pytest tests/ -x --tb=short -q)",
7
+ "Bash(python3 -m pytest tests/ --tb=short -q)",
8
+ "Bash(python3 -m pytest tests/ --tb=no -q)",
9
+ "Bash(python3 -m pytest tests/test_ttree.py::test_stats_scalar_single_column_float tests/test_ttree.py::test_loc_slice_reduces_size --tb=long -q)",
10
+ "Bash(python3 -m pytest tests/ -v --tb=short -q)",
11
+ "Bash(python3 -m pytest tests/ -v --tb=short)"
12
+ ]
13
+ }
14
+ }
Binary file
@@ -0,0 +1,31 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ `rootloader` is a Python library that wraps PyROOT to load ROOT files into memory as Python-native objects (numpy arrays, pandas DataFrames). It targets physicists at TRIUMF who work with ROOT data but prefer Python workflows.
6
+
7
+ ## Architecture
8
+
9
+ The class hierarchy mirrors ROOT's object model:
10
+
11
+ ```
12
+ attrdict (dict subclass — attribute-style key access)
13
+ └── tdirectory — wraps ROOT.TDirectoryFile or ROOT.TFile; iterates keys and dispatches to typed wrappers
14
+ └── tfile — entry point; opens a ROOT.TFile and delegates to tdirectory
15
+
16
+ ttree — wraps ROOT.TTree via RDataFrame; supports lazy column selection, filters, and conversion to pandas/numpy
17
+ th1 — wraps ROOT.TH1; stores x/y/dy arrays and provides .plot() and .to_dataframe()
18
+ th2 — wraps ROOT.TH2; stores x/y/z/dz arrays and provides .plot() and .to_dataframe()
19
+ tleaf — thin wrapper around ROOT.TLeaf for direct leaf-level access
20
+ ```
21
+
22
+ **Key design decisions:**
23
+
24
+ - `attrdict` makes all dict keys accessible as attributes (`fid.tree1` == `fid['tree1']`). This is the base for `tdirectory`.
25
+ - `tdirectory.__init__` resolves ROOT key cycles (keeps highest cycle number) and dispatches each object to `ttree`, `th1`, `th2`, or a nested `tdirectory`.
26
+ - `ttree` is lazy: it holds a `ROOT.RDataFrame` and only materializes data when `.to_dataframe()`, `.to_dict()`, `.to_array()`, or a stats method (`.min()`, `.max()`, etc.) is called. Column selection via `__getitem__` returns a view (not a copy).
27
+ - Stats methods (`min`, `max`, `mean`, `sum`, `std`) use JIT-compiled C++ templates (`cpp_template` in `ttree.py`) to avoid memory creep from repeated ROOT JIT compilations. Each unique `(function, dtype)` pair is compiled once and cached on the ROOT module.
28
+ - `th1.to_dataframe()` and `th2.to_dataframe()` store reconstruction metadata in `df.attrs` so the original typed object can be restored via `tdirectory.from_dataframe()`.
29
+ - `ROOT.EnableImplicitMT()` is called at import time in `ttree.py` to enable multithreading for RDataFrame operations.
30
+ - `ROOT.gROOT.SetBatch(1)` is set at import in `__init__.py` to suppress graphical output.
31
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootloader
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: Read simple root files into memory
5
5
  Project-URL: Homepage, https://github.com/ucn-triumf/rootloader
6
6
  Project-URL: Bug Tracker, https://github.com/ucn-triumf/rootloader/issues
@@ -687,6 +687,9 @@ Requires-Dist: matplotlib
687
687
  Requires-Dist: numpy
688
688
  Requires-Dist: pandas
689
689
  Requires-Dist: tqdm
690
+ Provides-Extra: test
691
+ Requires-Dist: pytest; extra == 'test'
692
+ Requires-Dist: pytest-cov; extra == 'test'
690
693
  Description-Content-Type: text/markdown
691
694
 
692
695
  # ROOTLOADER
@@ -41,7 +41,7 @@ class tdirectory(attrdict):
41
41
 
42
42
  ### tdirectory.copy
43
43
 
44
- [Show source in tdirectory.py:122](../../rootloader/tdirectory.py#L122)
44
+ [Show source in tdirectory.py:127](../../rootloader/tdirectory.py#L127)
45
45
 
46
46
  Make a copy of this object
47
47
 
@@ -53,7 +53,7 @@ def copy(self): ...
53
53
 
54
54
  ### tdirectory.from_dataframe
55
55
 
56
- [Show source in tdirectory.py:131](../../rootloader/tdirectory.py#L131)
56
+ [Show source in tdirectory.py:136](../../rootloader/tdirectory.py#L136)
57
57
 
58
58
  Convert all elements contained in self to original objects
59
59
 
@@ -65,7 +65,7 @@ def from_dataframe(self): ...
65
65
 
66
66
  ### tdirectory.to_dataframe
67
67
 
68
- [Show source in tdirectory.py:142](../../rootloader/tdirectory.py#L142)
68
+ [Show source in tdirectory.py:147](../../rootloader/tdirectory.py#L147)
69
69
 
70
70
  Convert all objects possible (th1, th2, and ttree) into pandas dataframes
71
71
 
@@ -75,7 +75,7 @@ def plot(self, ax=None, flat=True, **kwargs): ...
75
75
 
76
76
  ### th2.to_dataframe
77
77
 
78
- [Show source in th2.py:159](../../rootloader/th2.py#L159)
78
+ [Show source in th2.py:160](../../rootloader/th2.py#L160)
79
79
 
80
80
  Convert tree to pandas dataframe
81
81
 
@@ -4,6 +4,12 @@
4
4
 
5
5
  > Auto-generated documentation for [rootloader.ttree](../../rootloader/ttree.py) module.
6
6
 
7
+ #### Attributes
8
+
9
+ - `cpp_template` - template to pre-compile stats functions to avoid memory creep during JIT compilations
10
+ used in ttree._get_stat: '\n{TYPE2} RDF_{FNNAME}_{TYPE2}({DTYPE} df, string col){\n return df.{FNNAME}<{TYPE2}>(col).GetValue.\n}\n'
11
+
12
+
7
13
  - [ttree](#ttree)
8
14
  - [ttree](#ttree-1)
9
15
  - [ttree.__getitem__](#ttree__getitem__)
@@ -31,7 +37,7 @@
31
37
 
32
38
  ## ttree
33
39
 
34
- [Show source in ttree.py:103](../../rootloader/ttree.py#L103)
40
+ [Show source in ttree.py:22](../../rootloader/ttree.py#L22)
35
41
 
36
42
  Extract ROOT.TTree with lazy operation. Looks like a dataframe in most ways
37
43
 
@@ -45,12 +51,12 @@ Extract ROOT.TTree with lazy operation. Looks like a dataframe in most ways
45
51
 
46
52
  ```python
47
53
  class ttree(object):
48
- def __init__(self, tree): ...
54
+ def __init__(self, tree, filter_string=None, columns=None): ...
49
55
  ```
50
56
 
51
57
  ### ttree.__getitem__
52
58
 
53
- [Show source in ttree.py:165](../../rootloader/ttree.py#L165)
59
+ [Show source in ttree.py:90](../../rootloader/ttree.py#L90)
54
60
 
55
61
  Fetch a new dataframe with fewer 'columns', as a memory view
56
62
 
@@ -62,7 +68,7 @@ def __getitem__(self, key): ...
62
68
 
63
69
  ### ttree.columns
64
70
 
65
- [Show source in ttree.py:420](../../rootloader/ttree.py#L420)
71
+ [Show source in ttree.py:345](../../rootloader/ttree.py#L345)
66
72
 
67
73
  Return list of column (branch) names
68
74
 
@@ -75,7 +81,7 @@ def columns(self): ...
75
81
 
76
82
  ### ttree.filters
77
83
 
78
- [Show source in ttree.py:424](../../rootloader/ttree.py#L424)
84
+ [Show source in ttree.py:349](../../rootloader/ttree.py#L349)
79
85
 
80
86
  Return list of RDataFrame filters
81
87
 
@@ -88,7 +94,7 @@ def filters(self): ...
88
94
 
89
95
  ### ttree.hist1d
90
96
 
91
- [Show source in ttree.py:214](../../rootloader/ttree.py#L214)
97
+ [Show source in ttree.py:139](../../rootloader/ttree.py#L139)
92
98
 
93
99
  Return histogram of column
94
100
 
@@ -113,7 +119,7 @@ def hist1d(self, column=None, nbins=None, step=None, edges=None): ...
113
119
 
114
120
  ### ttree.hist2d
115
121
 
116
- [Show source in ttree.py:267](../../rootloader/ttree.py#L267)
122
+ [Show source in ttree.py:192](../../rootloader/ttree.py#L192)
117
123
 
118
124
  Return histogram of two columns
119
125
 
@@ -137,7 +143,7 @@ def hist2d(
137
143
 
138
144
  ### ttree.index
139
145
 
140
- [Show source in ttree.py:428](../../rootloader/ttree.py#L428)
146
+ [Show source in ttree.py:353](../../rootloader/ttree.py#L353)
141
147
 
142
148
  Return ttree of just the index data
143
149
 
@@ -150,7 +156,7 @@ def index(self): ...
150
156
 
151
157
  ### ttree.index_name
152
158
 
153
- [Show source in ttree.py:432](../../rootloader/ttree.py#L432)
159
+ [Show source in ttree.py:357](../../rootloader/ttree.py#L357)
154
160
 
155
161
  Return string of the name of the index branch
156
162
 
@@ -163,7 +169,7 @@ def index_name(self): ...
163
169
 
164
170
  ### ttree.loc
165
171
 
166
- [Show source in ttree.py:436](../../rootloader/ttree.py#L436)
172
+ [Show source in ttree.py:361](../../rootloader/ttree.py#L361)
167
173
 
168
174
  Return a ttree that can be indexed like a pandas dataframe
169
175
 
@@ -176,7 +182,7 @@ def loc(self): ...
176
182
 
177
183
  ### ttree.max
178
184
 
179
- [Show source in ttree.py:473](../../rootloader/ttree.py#L473)
185
+ [Show source in ttree.py:402](../../rootloader/ttree.py#L402)
180
186
 
181
187
  Return the max value of the tree, for each branch
182
188
 
@@ -188,7 +194,7 @@ def max(self): ...
188
194
 
189
195
  ### ttree.mean
190
196
 
191
- [Show source in ttree.py:476](../../rootloader/ttree.py#L476)
197
+ [Show source in ttree.py:405](../../rootloader/ttree.py#L405)
192
198
 
193
199
  Return the mean value of the tree, for each branch
194
200
 
@@ -200,7 +206,7 @@ def mean(self): ...
200
206
 
201
207
  ### ttree.min
202
208
 
203
- [Show source in ttree.py:470](../../rootloader/ttree.py#L470)
209
+ [Show source in ttree.py:399](../../rootloader/ttree.py#L399)
204
210
 
205
211
  Return the min value of the tree, for each branch
206
212
 
@@ -212,7 +218,7 @@ def min(self): ...
212
218
 
213
219
  ### ttree.reset
214
220
 
215
- [Show source in ttree.py:339](../../rootloader/ttree.py#L339)
221
+ [Show source in ttree.py:264](../../rootloader/ttree.py#L264)
216
222
 
217
223
  Make a new tree
218
224
 
@@ -224,7 +230,7 @@ def reset(self): ...
224
230
 
225
231
  ### ttree.reset_columns
226
232
 
227
- [Show source in ttree.py:343](../../rootloader/ttree.py#L343)
233
+ [Show source in ttree.py:268](../../rootloader/ttree.py#L268)
228
234
 
229
235
  Include all columns again
230
236
 
@@ -236,7 +242,7 @@ def reset_columns(self): ...
236
242
 
237
243
  ### ttree.set_filter
238
244
 
239
- [Show source in ttree.py:353](../../rootloader/ttree.py#L353)
245
+ [Show source in ttree.py:278](../../rootloader/ttree.py#L278)
240
246
 
241
247
  Set a filter on the dataframe to select a subset of the data
242
248
 
@@ -248,7 +254,7 @@ def set_filter(self, expression, inplace=False): ...
248
254
 
249
255
  ### ttree.set_index
250
256
 
251
- [Show source in ttree.py:347](../../rootloader/ttree.py#L347)
257
+ [Show source in ttree.py:272](../../rootloader/ttree.py#L272)
252
258
 
253
259
  Set the index column name
254
260
 
@@ -260,7 +266,7 @@ def set_index(self, column): ...
260
266
 
261
267
  ### ttree.size
262
268
 
263
- [Show source in ttree.py:440](../../rootloader/ttree.py#L440)
269
+ [Show source in ttree.py:365](../../rootloader/ttree.py#L365)
264
270
 
265
271
  Return the number of rows in the ttree
266
272
 
@@ -273,7 +279,7 @@ def size(self): ...
273
279
 
274
280
  ### ttree.std
275
281
 
276
- [Show source in ttree.py:482](../../rootloader/ttree.py#L482)
282
+ [Show source in ttree.py:411](../../rootloader/ttree.py#L411)
277
283
 
278
284
  Return the standard deviationif the of values the tree, for each branch
279
285
 
@@ -285,7 +291,7 @@ def std(self): ...
285
291
 
286
292
  ### ttree.sum
287
293
 
288
- [Show source in ttree.py:479](../../rootloader/ttree.py#L479)
294
+ [Show source in ttree.py:408](../../rootloader/ttree.py#L408)
289
295
 
290
296
  Return the sum of the values of the tree, for each branch
291
297
 
@@ -297,7 +303,7 @@ def sum(self): ...
297
303
 
298
304
  ### ttree.to_array
299
305
 
300
- [Show source in ttree.py:364](../../rootloader/ttree.py#L364)
306
+ [Show source in ttree.py:289](../../rootloader/ttree.py#L289)
301
307
 
302
308
  Return ttree data as 1D or 2D numpy array (depending on number of columns)
303
309
 
@@ -309,7 +315,7 @@ def to_array(self): ...
309
315
 
310
316
  ### ttree.to_dataframe
311
317
 
312
- [Show source in ttree.py:375](../../rootloader/ttree.py#L375)
318
+ [Show source in ttree.py:300](../../rootloader/ttree.py#L300)
313
319
 
314
320
  Return ttree data as pandas dataframe
315
321
 
@@ -321,7 +327,7 @@ def to_dataframe(self): ...
321
327
 
322
328
  ### ttree.to_dict
323
329
 
324
- [Show source in ttree.py:409](../../rootloader/ttree.py#L409)
330
+ [Show source in ttree.py:334](../../rootloader/ttree.py#L334)
325
331
 
326
332
  Return ttree data as dict of numpy arrays
327
333
 
@@ -333,7 +339,7 @@ def to_dict(self): ...
333
339
 
334
340
  ### ttree.values
335
341
 
336
- [Show source in ttree.py:444](../../rootloader/ttree.py#L444)
342
+ [Show source in ttree.py:369](../../rootloader/ttree.py#L369)
337
343
 
338
344
  Convert ttree 1D or 2D numpy array (depending on number of columns)
339
345
 
@@ -22,6 +22,9 @@ dynamic = ["version"]
22
22
  "Homepage" = "https://github.com/ucn-triumf/rootloader"
23
23
  "Bug Tracker" = "https://github.com/ucn-triumf/rootloader/issues"
24
24
 
25
+ [project.optional-dependencies]
26
+ test = ["pytest", "pytest-cov"]
27
+
25
28
  # set version
26
29
  [tool.hatch.version]
27
30
  path = "rootloader/version.py"
@@ -65,7 +65,12 @@ class tdirectory(attrdict):
65
65
  # TTree
66
66
  if 'TTree' == classname:
67
67
  if empty_ok or obj.GetEntries() > 0:
68
- self[name] = ttree(obj)
68
+ if name in tree_filter:
69
+ filter_string, columns = tree_filter[name]
70
+ self[name] = ttree(obj, filter_string=filter_string,
71
+ columns=columns)
72
+ else:
73
+ self[name] = ttree(obj)
69
74
  elif not quiet:
70
75
  tqdm.write(f'Skipped "{name}" due to lack of entries')
71
76
 
@@ -71,7 +71,7 @@ class th2(object):
71
71
  self.dz = self.dz.transpose()
72
72
 
73
73
  def __len__(self):
74
- return self.nbins
74
+ return self.nbinsx * self.nbinsy
75
75
 
76
76
  def __repr__(self):
77
77
  return f'{self.base_class}: "{self.name}", {self.entries} entries, sum = {self.sum}'
@@ -143,7 +143,8 @@ class th2(object):
143
143
 
144
144
  if ax is None:
145
145
  plt.figure()
146
- ax = plt.gca().add_subplot(projection='3d')
146
+ # add_subplot is a Figure method, not an Axes method
147
+ ax = plt.gcf().add_subplot(111, projection='3d')
147
148
 
148
149
  ax.plot_surface(xx, yy, self.z, **kwargs)
149
150
 
@@ -16,7 +16,7 @@ class tleaf(object):
16
16
 
17
17
  def copy(self):
18
18
  """Make a copy of this object"""
19
- return tleaf(self._leaf.copy())
19
+ return tleaf(self._leaf)
20
20
 
21
21
  def get_entry(self, i):
22
22
  self._leaf.GetBranch().GetEntry(i)
@@ -28,7 +28,7 @@ class ttree(object):
28
28
  columns (list|None): list of column names to include in fetch, if None, get all
29
29
  """
30
30
 
31
- def __init__(self, tree):
31
+ def __init__(self, tree, filter_string=None, columns=None):
32
32
 
33
33
  # copy
34
34
  if isinstance(tree, ttree):
@@ -71,6 +71,12 @@ class ttree(object):
71
71
  for filt in self._filters:
72
72
  self._rdf = self._rdf.Filter(filt, filt)
73
73
 
74
+ # apply filter and column selection passed to the constructor
75
+ if filter_string is not None:
76
+ self.set_filter(filter_string, inplace=True)
77
+ if columns is not None:
78
+ self._columns = tuple(columns)
79
+
74
80
  def __dir__(self):
75
81
  superdir = [d for d in super().__dir__() if d[0] != '_']
76
82
  return sorted(self._columns) + superdir
@@ -313,6 +319,8 @@ class ttree(object):
313
319
 
314
320
  # set index
315
321
  if self._index is not None:
322
+ if self._index not in df.columns:
323
+ df[self._index] = self._rdf.AsNumpy(columns=[self._index])
316
324
  df.set_index(self._index, inplace=True)
317
325
 
318
326
  # convert to series?
@@ -327,13 +335,7 @@ class ttree(object):
327
335
 
328
336
  def to_dict(self):
329
337
  """Return ttree data as dict of numpy arrays"""
330
- # ensure index is loaded
331
- if self._index not in self._columns and self._index is not None:
332
- columns = [*self._columns, self._index]
333
- else:
334
- columns = self._columns
335
-
336
- return self._rdf.AsNumpy(columns=columns)
338
+ return self._rdf.AsNumpy(columns=self._columns)
337
339
 
338
340
  # PROPERTIES ===========================
339
341
  @property
@@ -427,6 +429,6 @@ class _ttree_indexed(object):
427
429
  raise NotImplementedError('Slicing steps not implemented')
428
430
 
429
431
  elif isinstance(key, (int, float)):
430
- tr.set_filter(f'{self._index} == {key}', inplace=True)
432
+ tr.set_filter(f'{tr._index} == {key}', inplace=True)
431
433
 
432
434
  return tr
@@ -0,0 +1 @@
1
+ __version__ = '1.4.0'