legend-pydataobj 1.5.1__py3-none-any.whl → 1.6.1__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.
Files changed (46) hide show
  1. {legend_pydataobj-1.5.1.dist-info → legend_pydataobj-1.6.1.dist-info}/METADATA +1 -1
  2. legend_pydataobj-1.6.1.dist-info/RECORD +54 -0
  3. {legend_pydataobj-1.5.1.dist-info → legend_pydataobj-1.6.1.dist-info}/WHEEL +1 -1
  4. {legend_pydataobj-1.5.1.dist-info → legend_pydataobj-1.6.1.dist-info}/entry_points.txt +1 -0
  5. lgdo/__init__.py +7 -4
  6. lgdo/_version.py +2 -2
  7. lgdo/cli.py +237 -12
  8. lgdo/compression/__init__.py +1 -0
  9. lgdo/lh5/__init__.py +9 -1
  10. lgdo/lh5/_serializers/__init__.py +43 -0
  11. lgdo/lh5/_serializers/read/__init__.py +0 -0
  12. lgdo/lh5/_serializers/read/array.py +34 -0
  13. lgdo/lh5/_serializers/read/composite.py +405 -0
  14. lgdo/lh5/_serializers/read/encoded.py +129 -0
  15. lgdo/lh5/_serializers/read/ndarray.py +104 -0
  16. lgdo/lh5/_serializers/read/scalar.py +34 -0
  17. lgdo/lh5/_serializers/read/utils.py +12 -0
  18. lgdo/lh5/_serializers/read/vector_of_vectors.py +201 -0
  19. lgdo/lh5/_serializers/write/__init__.py +0 -0
  20. lgdo/lh5/_serializers/write/array.py +92 -0
  21. lgdo/lh5/_serializers/write/composite.py +259 -0
  22. lgdo/lh5/_serializers/write/scalar.py +23 -0
  23. lgdo/lh5/_serializers/write/vector_of_vectors.py +95 -0
  24. lgdo/lh5/core.py +272 -0
  25. lgdo/lh5/datatype.py +46 -0
  26. lgdo/lh5/exceptions.py +34 -0
  27. lgdo/lh5/iterator.py +1 -1
  28. lgdo/lh5/store.py +69 -1160
  29. lgdo/lh5/tools.py +27 -53
  30. lgdo/lh5/utils.py +130 -27
  31. lgdo/lh5_store.py +11 -2
  32. lgdo/logging.py +1 -0
  33. lgdo/types/__init__.py +1 -0
  34. lgdo/types/array.py +1 -0
  35. lgdo/types/arrayofequalsizedarrays.py +1 -0
  36. lgdo/types/encoded.py +3 -8
  37. lgdo/types/fixedsizearray.py +1 -0
  38. lgdo/types/struct.py +1 -0
  39. lgdo/types/table.py +46 -5
  40. lgdo/types/vectorofvectors.py +314 -458
  41. lgdo/types/vovutils.py +320 -0
  42. lgdo/types/waveformtable.py +1 -0
  43. lgdo/utils.py +1 -32
  44. legend_pydataobj-1.5.1.dist-info/RECORD +0 -36
  45. {legend_pydataobj-1.5.1.dist-info → legend_pydataobj-1.6.1.dist-info}/LICENSE +0 -0
  46. {legend_pydataobj-1.5.1.dist-info → legend_pydataobj-1.6.1.dist-info}/top_level.txt +0 -0
lgdo/lh5/tools.py CHANGED
@@ -2,11 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import fnmatch
4
4
  import glob
5
- import inspect
6
5
  import logging
7
6
  import os
8
- from collections.abc import Iterable
9
- from typing import Any
7
+ from copy import copy
10
8
  from warnings import warn
11
9
 
12
10
  import h5py
@@ -20,7 +18,11 @@ from .store import LH5Store
20
18
  log = logging.getLogger(__name__)
21
19
 
22
20
 
23
- def ls(lh5_file: str | h5py.Group, lh5_group: str = "") -> list[str]:
21
+ def ls(
22
+ lh5_file: str | h5py.Group,
23
+ lh5_group: str = "",
24
+ recursive: bool = False,
25
+ ) -> list[str]:
24
26
  """Return a list of LH5 groups in the input file and group, similar
25
27
  to ``ls`` or ``h5ls``. Supports wildcards in group names.
26
28
 
@@ -32,6 +34,8 @@ def ls(lh5_file: str | h5py.Group, lh5_group: str = "") -> list[str]:
32
34
  lh5_group
33
35
  group to search. add a ``/`` to the end of the group name if you want to
34
36
  list all objects inside that group.
37
+ recursive
38
+ if ``True``, recurse into subgroups.
35
39
  """
36
40
 
37
41
  log.debug(
@@ -49,15 +53,30 @@ def ls(lh5_file: str | h5py.Group, lh5_group: str = "") -> list[str]:
49
53
  if lh5_group == "":
50
54
  lh5_group = "*"
51
55
 
56
+ # get the first group in the group path
52
57
  splitpath = lh5_group.split("/", 1)
58
+ # filter out objects that don't match lh5_group pattern
53
59
  matchingkeys = fnmatch.filter(lh5_file.keys(), splitpath[0])
54
60
 
61
+ ret = []
62
+ # if there were no "/" in lh5_group just return the result
55
63
  if len(splitpath) == 1:
56
- return matchingkeys
64
+ ret = matchingkeys
65
+
66
+ else:
67
+ for key in matchingkeys:
68
+ ret.extend([f"{key}/{path}" for path in ls(lh5_file[key], splitpath[1])])
69
+
70
+ if recursive:
71
+ rec_ret = copy(ret)
72
+ for obj in ret:
73
+ try:
74
+ rec_ret += ls(lh5_file, lh5_group=f"{obj}/", recursive=True)
75
+ except AttributeError:
76
+ continue
77
+
78
+ return rec_ret
57
79
 
58
- ret = []
59
- for key in matchingkeys:
60
- ret.extend([f"{key}/{path}" for path in ls(lh5_file[key], splitpath[1])])
61
80
  return ret
62
81
 
63
82
 
@@ -169,51 +188,6 @@ def show(
169
188
  key = k_new
170
189
 
171
190
 
172
- def read_as(
173
- name: str,
174
- lh5_file: str | h5py.File | Iterable[str | h5py.File],
175
- library: str,
176
- **kwargs,
177
- ) -> Any:
178
- """Read LH5 data from disk straight into a third-party data format view.
179
-
180
- This function is nothing more than a shortcut chained call to
181
- :meth:`.LH5Store.read` and to :meth:`.LGDO.view_as`.
182
-
183
- Parameters
184
- ----------
185
- name
186
- LH5 object name on disk.
187
- lh5_file
188
- LH5 file name.
189
- library
190
- string ID of the third-party data format library (``np``, ``pd``,
191
- ``ak``, etc).
192
-
193
- See Also
194
- --------
195
- .LH5Store.read, .LGDO.view_as
196
- """
197
- # determine which keyword arguments should be forwarded to read() and which
198
- # should be forwarded to view_as()
199
- read_kwargs = inspect.signature(LH5Store.read).parameters.keys()
200
-
201
- kwargs1 = {}
202
- kwargs2 = {}
203
- for k, v in kwargs.items():
204
- if k in read_kwargs:
205
- kwargs1[k] = v
206
- else:
207
- kwargs2[k] = v
208
-
209
- # read the LGDO from disk
210
- store = LH5Store()
211
- obj, _ = store.read(name, lh5_file, **kwargs1)
212
-
213
- # and finally return a view
214
- return obj.view_as(library, **kwargs2)
215
-
216
-
217
191
  def load_nda(
218
192
  f_list: str | list[str],
219
193
  par_list: list[str],
lgdo/lh5/utils.py CHANGED
@@ -1,46 +1,149 @@
1
1
  """Implements utilities for LEGEND Data Objects."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import glob
5
6
  import logging
6
7
  import os
7
8
  import string
9
+ from collections.abc import Mapping, Sequence
10
+ from typing import Any
8
11
 
9
- log = logging.getLogger(__name__)
12
+ import h5py
10
13
 
14
+ from .. import types
15
+ from . import _serializers, datatype
16
+ from .exceptions import LH5DecodeError
11
17
 
12
- def parse_datatype(datatype: str) -> tuple[str, tuple[int, ...], str | list[str]]:
13
- """Parse datatype string and return type, dimensions and elements.
18
+ log = logging.getLogger(__name__)
14
19
 
15
- Parameters
16
- ----------
17
- datatype
18
- a LGDO-formatted datatype string.
19
20
 
20
- Returns
21
- -------
22
- element_type
23
- the datatype name dims if not ``None``, a tuple of dimensions for the
24
- LGDO. Note this is not the same as the NumPy shape of the underlying
25
- data object. See the LGDO specification for more information. Also see
26
- :class:`~.types.ArrayOfEqualSizedArrays` and
27
- :meth:`.lh5_store.LH5Store.read` for example code elements for
28
- numeric objects, the element type for struct-like objects, the list of
29
- fields in the struct.
21
+ def get_buffer(
22
+ name: str,
23
+ lh5_file: str | h5py.File | Sequence[str | h5py.File],
24
+ size: int | None = None,
25
+ field_mask: Mapping[str, bool] | Sequence[str] | None = None,
26
+ ) -> types.LGDO:
27
+ """Returns an LGDO appropriate for use as a pre-allocated buffer.
28
+
29
+ Sets size to `size` if object has a size.
30
30
  """
31
- if "{" not in datatype:
32
- return "scalar", None, datatype
31
+ obj, n_rows = _serializers._h5_read_lgdo(
32
+ name, lh5_file, n_rows=0, field_mask=field_mask
33
+ )
33
34
 
34
- # for other datatypes, need to parse the datatype string
35
- from parse import parse
35
+ if hasattr(obj, "resize") and size is not None:
36
+ obj.resize(new_size=size)
36
37
 
37
- datatype, element_description = parse("{}{{{}}}", datatype)
38
- if datatype.endswith(">"):
39
- datatype, dims = parse("{}<{}>", datatype)
40
- dims = [int(i) for i in dims.split(",")]
41
- return datatype, tuple(dims), element_description
38
+ return obj
42
39
 
43
- return datatype, None, element_description.split(",")
40
+
41
+ def read_n_rows(name: str, h5f: str | h5py.File) -> int | None:
42
+ """Look up the number of rows in an Array-like LGDO object on disk.
43
+
44
+ Return ``None`` if `name` is a :class:`.Scalar` or a :class:`.Struct`.
45
+ """
46
+ if not isinstance(h5f, h5py.File):
47
+ h5f = h5py.File(h5f, "r")
48
+
49
+ try:
50
+ attrs = h5f[name].attrs
51
+ except KeyError as e:
52
+ msg = "not found"
53
+ raise LH5DecodeError(msg, h5f, name) from e
54
+ except AttributeError as e:
55
+ msg = "missing 'datatype' attribute"
56
+ raise LH5DecodeError(msg, h5f, name) from e
57
+
58
+ lgdotype = datatype.datatype(attrs["datatype"])
59
+
60
+ # scalars are dim-0 datasets
61
+ if lgdotype is types.Scalar:
62
+ return None
63
+
64
+ # structs don't have rows
65
+ if lgdotype is types.Struct:
66
+ return None
67
+
68
+ # tables should have elements with all the same length
69
+ if lgdotype is types.Table:
70
+ # read out each of the fields
71
+ rows_read = None
72
+ for field in datatype.get_struct_fields(attrs["datatype"]):
73
+ n_rows_read = read_n_rows(name + "/" + field, h5f)
74
+ if not rows_read:
75
+ rows_read = n_rows_read
76
+ elif rows_read != n_rows_read:
77
+ log.warning(
78
+ f"'{field}' field in table '{name}' has {rows_read} rows, "
79
+ f"{n_rows_read} was expected"
80
+ )
81
+ return rows_read
82
+
83
+ # length of vector of vectors is the length of its cumulative_length
84
+ if lgdotype is types.VectorOfVectors:
85
+ return read_n_rows(f"{name}/cumulative_length", h5f)
86
+
87
+ # length of vector of encoded vectors is the length of its decoded_size
88
+ if lgdotype in (types.VectorOfEncodedVectors, types.ArrayOfEncodedEqualSizedArrays):
89
+ return read_n_rows(f"{name}/encoded_data", h5f)
90
+
91
+ # return array length (without reading the array!)
92
+ if issubclass(lgdotype, types.Array):
93
+ # compute the number of rows to read
94
+ return h5f[name].shape[0]
95
+
96
+ msg = f"don't know how to read rows of LGDO {lgdotype.__name__}"
97
+ raise LH5DecodeError(msg, h5f, name)
98
+
99
+
100
+ def get_h5_group(
101
+ group: str | h5py.Group,
102
+ base_group: h5py.Group,
103
+ grp_attrs: Mapping[str, Any] | None = None,
104
+ overwrite: bool = False,
105
+ ) -> h5py.Group:
106
+ """
107
+ Returns an existing :mod:`h5py` group from a base group or creates a
108
+ new one. Can also set (or replace) group attributes.
109
+
110
+ Parameters
111
+ ----------
112
+ group
113
+ name of the HDF5 group.
114
+ base_group
115
+ HDF5 group to be used as a base.
116
+ grp_attrs
117
+ HDF5 group attributes.
118
+ overwrite
119
+ whether overwrite group attributes, ignored if `grp_attrs` is
120
+ ``None``.
121
+ """
122
+ if not isinstance(group, h5py.Group):
123
+ if group in base_group:
124
+ group = base_group[group]
125
+ else:
126
+ group = base_group.create_group(group)
127
+ if grp_attrs is not None:
128
+ group.attrs.update(grp_attrs)
129
+ return group
130
+ if (
131
+ grp_attrs is not None
132
+ and len(set(grp_attrs.items()) ^ set(group.attrs.items())) > 0
133
+ ):
134
+ if not overwrite:
135
+ msg = (
136
+ f"Provided {grp_attrs=} are different from "
137
+ f"existing ones {dict(group.attrs)=} but overwrite flag is not set"
138
+ )
139
+ raise RuntimeError(msg)
140
+
141
+ log.debug(f"overwriting {group}.attrs...")
142
+ for key in group.attrs:
143
+ group.attrs.pop(key)
144
+ group.attrs.update(grp_attrs)
145
+
146
+ return group
44
147
 
45
148
 
46
149
  def expand_vars(expr: str, substitute: dict[str, str] | None = None) -> str:
lgdo/lh5_store.py CHANGED
@@ -2,6 +2,7 @@
2
2
  .. warning::
3
3
  This subpackage is deprecated, use :mod:`lgdo.lh5`.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import sys
@@ -27,15 +28,14 @@ from .types import (
27
28
  WaveformTable, # noqa: F401
28
29
  )
29
30
 
30
- DEFAULT_HDF5_COMPRESSION = None
31
31
  LGDO = Union[Array, Scalar, Struct, VectorOfVectors]
32
- DEFAULT_HDF5_SETTINGS: dict[str, ...] = {"shuffle": True, "compression": "gzip"}
33
32
 
34
33
 
35
34
  class LH5Iterator(lh5.LH5Iterator):
36
35
  """
37
36
  .. warning::
38
37
  This class is deprecated, use :class:`lgdo.lh5.iterator.LH5Iterator`.
38
+
39
39
  """
40
40
 
41
41
  def __init__(
@@ -82,6 +82,7 @@ class LH5Iterator(lh5.LH5Iterator):
82
82
  """
83
83
  .. warning::
84
84
  This method is deprecated, use :meth:`lgdo.lh5.iterator.LH5Iterator.write`.
85
+
85
86
  """
86
87
  warn(
87
88
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5 containing LH5Iterator. "
@@ -118,6 +119,7 @@ class LH5Iterator(lh5.LH5Iterator):
118
119
  """
119
120
  .. warning::
120
121
  This method is deprecated, use :meth:`lgdo.lh5.iterator.LH5Iterator.read`.
122
+
121
123
  """
122
124
  warn(
123
125
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5 containing LH5Iterator. "
@@ -144,6 +146,7 @@ class LH5Store(lh5.LH5Store):
144
146
  """
145
147
  .. warning::
146
148
  This class is deprecated, use :class:`lgdo.lh5.iterator.LH5Store`.
149
+
147
150
  """
148
151
 
149
152
  def __init__(self, base_path: str = "", keep_open: bool = False):
@@ -165,6 +168,7 @@ class LH5Store(lh5.LH5Store):
165
168
  """
166
169
  .. warning::
167
170
  This method is deprecated, use :meth:`lgdo.lh5.store.LH5Store.read`.
171
+
168
172
  """
169
173
  warn(
170
174
  "LH5Store.read_object() has been renamed to LH5Store.read(), "
@@ -185,6 +189,7 @@ class LH5Store(lh5.LH5Store):
185
189
  """
186
190
  .. warning::
187
191
  This method is deprecated, use :meth:`lgdo.lh5.store.LH5Store.write`.
192
+
188
193
  """
189
194
  warn(
190
195
  "LH5Store.write_object() has been renamed to LH5Store.write(), "
@@ -206,6 +211,7 @@ def load_dfs(
206
211
  .. warning::
207
212
  This function is deprecated, use :meth:`lgdo.types.lgdo.LGDO.view_as` to
208
213
  view LGDO data as a Pandas data structure.
214
+
209
215
  """
210
216
  warn(
211
217
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5. "
@@ -227,6 +233,7 @@ def load_nda(
227
233
  .. warning::
228
234
  This function is deprecated, use :meth:`lgdo.types.lgdo.LGDO.view_as` to
229
235
  view LGDO data as a NumPy data structure.
236
+
230
237
  """
231
238
  warn(
232
239
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5. "
@@ -242,6 +249,7 @@ def ls(lh5_file: str | h5py.Group, lh5_group: str = "") -> list[str]:
242
249
  """
243
250
  .. warning::
244
251
  This function is deprecated, import :func:`lgdo.lh5.tools.ls`.
252
+
245
253
  """
246
254
  warn(
247
255
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5. "
@@ -263,6 +271,7 @@ def show(
263
271
  """
264
272
  .. warning::
265
273
  This function is deprecated, import :func:`lgdo.lh5.tools.show`.
274
+
266
275
  """
267
276
  warn(
268
277
  "lgdo.lh5_store has moved to a subfolder lgdo.lh5. "
lgdo/logging.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """This module implements some helpers for setting up logging."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
lgdo/types/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """LEGEND Data Objects (LGDO) types."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from .array import Array
lgdo/types/array.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Implements a LEGEND Data Object representing an n-dimensional array and
3
3
  corresponding utilities.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import logging
@@ -2,6 +2,7 @@
2
2
  Implements a LEGEND Data Object representing an array of equal-sized arrays and
3
3
  corresponding utilities.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from collections.abc import Iterator
lgdo/types/encoded.py CHANGED
@@ -179,8 +179,8 @@ class VectorOfEncodedVectors(LGDO):
179
179
 
180
180
  def __str__(self) -> str:
181
181
  string = ""
182
- pos = 0
183
- for vec, size in self:
182
+ for pos, res in enumerate(self):
183
+ vec, size = res[0], res[1]
184
184
  if pos != 0:
185
185
  string += " "
186
186
 
@@ -200,8 +200,6 @@ class VectorOfEncodedVectors(LGDO):
200
200
  if pos < len(self.encoded_data.cumulative_length):
201
201
  string += ",\n"
202
202
 
203
- pos += 1
204
-
205
203
  string = f"[{string}]"
206
204
 
207
205
  attrs = self.getattrs()
@@ -400,8 +398,7 @@ class ArrayOfEncodedEqualSizedArrays(LGDO):
400
398
 
401
399
  def __str__(self) -> str:
402
400
  string = ""
403
- pos = 0
404
- for vec in self:
401
+ for pos, vec in enumerate(self):
405
402
  if pos != 0:
406
403
  string += " "
407
404
 
@@ -418,8 +415,6 @@ class ArrayOfEncodedEqualSizedArrays(LGDO):
418
415
  if pos < len(self.encoded_data.cumulative_length):
419
416
  string += ",\n"
420
417
 
421
- pos += 1
422
-
423
418
  string = f"[{string}] decoded_size={self.decoded_size}"
424
419
 
425
420
  attrs = self.getattrs()
@@ -2,6 +2,7 @@
2
2
  Implements a LEGEND Data Object representing an n-dimensional array of fixed
3
3
  size and corresponding utilities.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from typing import Any
lgdo/types/struct.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Implements a LEGEND Data Object representing a struct and corresponding
3
3
  utilities.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import logging
lgdo/types/table.py CHANGED
@@ -2,6 +2,7 @@
2
2
  Implements a LEGEND Data Object representing a special struct of arrays of
3
3
  equal length and corresponding utilities.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import logging
@@ -229,6 +230,29 @@ class Table(Struct):
229
230
  )
230
231
  return self.view_as(library="pd", cols=cols, prefix=prefix)
231
232
 
233
+ def flatten(self, _prefix="") -> Table:
234
+ """Flatten the table, if nested.
235
+
236
+ Returns a new :class:`Table` (that references, not copies, the existing
237
+ columns) with columns in nested tables being moved to the first level
238
+ (and renamed appropriately).
239
+
240
+ Examples
241
+ --------
242
+ >>> repr(tbl)
243
+ "Table(dict={'a': Array([1 2 3], attrs={'datatype': 'array<1>{real}'}), 'tbl': Table(dict={'b': Array([4 5 6], attrs={'datatype': 'array<1>{real}'}), 'tbl1': Table(dict={'z': Array([9 9 9], attrs={'datatype': 'array<1>{real}'})}, attrs={'datatype': 'table{z}'})}, attrs={'datatype': 'table{b,tbl1}'})}, attrs={'datatype': 'table{a,tbl}'})"
244
+ >>> tbl.flatten().keys()
245
+ dict_keys(['a', 'tbl__b', 'tbl__tbl1__z'])
246
+ """
247
+ flat_table = Table(size=self.size)
248
+ for key, obj in self.items():
249
+ if isinstance(obj, Table):
250
+ flat_table.join(obj.flatten(_prefix=f"{_prefix}{key}__"))
251
+ else:
252
+ flat_table.add_column(_prefix + key, obj)
253
+
254
+ return flat_table
255
+
232
256
  def eval(
233
257
  self,
234
258
  expr: str,
@@ -242,6 +266,13 @@ class Table(Struct):
242
266
  columns are viewed as :class:`ak.Array` and the respective routines are
243
267
  therefore available.
244
268
 
269
+ To columns nested in subtables can be accessed by scoping with two
270
+ underscores (``__``). For example: ::
271
+
272
+ tbl.eval("a + tbl2__b")
273
+
274
+ computes the sum of column `a` and column `b` in the subtable `tbl2`.
275
+
245
276
  Parameters
246
277
  ----------
247
278
  expr
@@ -285,15 +316,19 @@ class Table(Struct):
285
316
 
286
317
  # make a dictionary of low-level objects (numpy or awkward)
287
318
  # for later computation
319
+ flat_self = self.flatten()
288
320
  self_unwrap = {}
289
321
  has_ak = False
290
322
  for obj in c.co_names:
291
- if obj in self.keys():
292
- if isinstance(self[obj], VectorOfVectors):
293
- self_unwrap[obj] = self[obj].view_as("ak", with_units=False)
323
+ if obj in flat_self:
324
+ if isinstance(flat_self[obj], VectorOfVectors):
325
+ self_unwrap[obj] = flat_self[obj].view_as("ak", with_units=False)
294
326
  has_ak = True
295
327
  else:
296
- self_unwrap[obj] = self[obj].view_as("np", with_units=False)
328
+ self_unwrap[obj] = flat_self[obj].view_as("np", with_units=False)
329
+
330
+ msg = f"evaluating {expr!r} with locals={(self_unwrap | parameters)} and {has_ak=}"
331
+ log.debug(msg)
297
332
 
298
333
  # use numexpr if we are only dealing with numpy data types
299
334
  if not has_ak:
@@ -302,6 +337,9 @@ class Table(Struct):
302
337
  local_dict=(self_unwrap | parameters),
303
338
  )
304
339
 
340
+ msg = f"...the result is {out_data!r}"
341
+ log.debug(msg)
342
+
305
343
  # need to convert back to LGDO
306
344
  # np.evaluate should always return a numpy thing?
307
345
  if out_data.ndim == 0:
@@ -319,7 +357,10 @@ class Table(Struct):
319
357
 
320
358
  # resort to good ol' eval()
321
359
  globs = {"ak": ak, "np": np}
322
- out_data = eval(expr, globs, (self_unwrap | parameters)) # noqa: PGH001
360
+ out_data = eval(expr, globs, (self_unwrap | parameters))
361
+
362
+ msg = f"...the result is {out_data!r}"
363
+ log.debug(msg)
323
364
 
324
365
  # need to convert back to LGDO
325
366
  if isinstance(out_data, ak.Array):