From 47dc7ebacfb2beb78106bf6c1f8eacd412c475dd Mon Sep 17 00:00:00 2001 From: mrli Date: Fri, 9 May 2025 15:05:21 +0800 Subject: [PATCH 01/13] Add a draft of `custom-interpretation` --- .../AsCustom/__init__.py | 988 ++++++++++++++++++ .../AsCustom/should_be_cpp.py | 427 ++++++++ dev/custom-interpretation/README.md | 446 ++++++++ dev/custom-interpretation/bes3/__init__.py | 133 +++ .../bes3/should_be_cpp.py | 93 ++ dev/custom-interpretation/example.py | 14 + .../tiny_reader/__init__.py | 93 ++ .../tiny_reader/gen-data/CMakeLists.txt | 32 + .../tiny_reader/gen-data/example.root | Bin 0 -> 57712 bytes .../tiny_reader/gen-data/include/LinkDef.h | 10 + .../tiny_reader/gen-data/include/TMyObject.hh | 20 + .../gen-data/include/TMySubObject.hh | 57 + .../tiny_reader/gen-data/src/TMyObject.cc | 10 + .../tiny_reader/gen-data/src/TMySubObject.cc | 64 ++ .../tiny_reader/gen-data/src/main.cc | 21 + .../tiny_reader/should_be_cpp.py | 48 + src/uproot/__init__.py | 2 + src/uproot/interpretation/custom.py | 173 +++ src/uproot/interpretation/identify.py | 34 + 19 files changed, 2665 insertions(+) create mode 100644 dev/custom-interpretation/AsCustom/__init__.py create mode 100644 dev/custom-interpretation/AsCustom/should_be_cpp.py create mode 100644 dev/custom-interpretation/README.md create mode 100644 dev/custom-interpretation/bes3/__init__.py create mode 100644 dev/custom-interpretation/bes3/should_be_cpp.py create mode 100644 dev/custom-interpretation/example.py create mode 100644 dev/custom-interpretation/tiny_reader/__init__.py create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/example.root create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc create mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/main.cc create mode 100644 dev/custom-interpretation/tiny_reader/should_be_cpp.py create mode 100644 src/uproot/interpretation/custom.py diff --git a/dev/custom-interpretation/AsCustom/__init__.py b/dev/custom-interpretation/AsCustom/__init__.py new file mode 100644 index 000000000..e530aac37 --- /dev/null +++ b/dev/custom-interpretation/AsCustom/__init__.py @@ -0,0 +1,988 @@ +from __future__ import annotations + +import re +from enum import Enum +from typing import Literal + +import awkward as ak +import numpy as np + +import uproot + +from .should_be_cpp import ( + Cpp_BaseObjectReader, + Cpp_BinaryParser, + Cpp_CArrayReader, + Cpp_CtypeReader, + Cpp_EmptyReader, + Cpp_ObjectHeaderReader, + Cpp_STLMapReader, + Cpp_STLSequenceReader, + Cpp_STLStringReader, + Cpp_TArrayReader, + Cpp_TObjectReader, + Cpp_TStringReader, +) + +type_np2array = { + "u1": "B", + "u2": "H", + "u4": "I", + "u8": "Q", + "i1": "b", + "i2": "h", + "i4": "i", + "i8": "q", + "f": "f", + "d": "d", +} + +num_typenames = { + "bool": "i1", + "char": "i1", + "short": "i2", + "int": "i4", + "long": "i8", + "unsigned char": "u1", + "unsigned short": "u2", + "unsigned int": "u4", + "unsigned long": "u8", + "float": "f", + "double": "d", + # cstdint + "int8_t": "i1", + "int16_t": "i2", + "int32_t": "i4", + "int64_t": "i8", + "uint8_t": "u1", + "uint16_t": "u2", + "uint32_t": "u4", + "uint64_t": "u8", + # ROOT types + "Bool_t": "i1", + "Char_t": "i1", + "Short_t": "i2", + "Int_t": "i4", + "Long_t": "i8", + "UChar_t": "u1", + "UShort_t": "u2", + "UInt_t": "u4", + "ULong_t": "u8", + "Float_t": "f", + "Double_t": "d", +} + +stl_typenames = { + "vector", + "array", + "map", + "unordered_map", + "string", +} + + +tarray_typenames = { + "TArrayC": "i1", + "TArrayS": "i2", + "TArrayI": "i4", + "TArrayL": "i8", + "TArrayF": "f", + "TArrayD": "d", +} + + +ctype_hints = Literal["bool", "i1", "i2", "i4", "i8", "u1", "u2", "u4", "u8", "f", "d"] + + +class ReaderType(Enum): + CType = "CType" + STLSequence = "STLSequence" + STLMap = "STLMap" + STLString = "STLString" + TArray = "TArray" + TString = "TString" + TObject = "TObject" + CArray = "CArray" + BaseObject = "BaseObject" + ObjectHeader = "ObjectHeader" + Empty = "Empty" + + +def get_top_type_name(type_name: str) -> str: + if type_name.endswith("*"): + type_name = type_name[:-1].strip() + type_name = type_name.replace("std::", "").strip() + return type_name.split("<")[0] + + +def gen_tree_config( + cls_streamer_info: dict, + all_streamer_info: dict, + item_path: str = "", +) -> dict: + """ + Generate reader configuration for a class streamer information. + + The content it returns should be: + + ```python + { + "reader": ReaderType, + "name": str, + "ctype": str, # for CTypeReader, TArrayReader + "element_reader": dict, # reader config of the element, for STLVectorReader, SimpleCArrayReader, TObjectCArrayReader + "flat_size": int, # for SimpleCArrayReader, TObjectCArrayReader + "fMaxIndex": list[int], # for SimpleCArrayReader, TObjectCArrayReader + "fArrayDim": int, # for SimpleCArrayReader, TObjectCArrayReader + "key_reader": dict, # reader config of the key, for STLMapReader + "val_reader": dict, # reader config of the value, for STLMapReader + "sub_readers": list[dict], # for BaseObjectReader, ObjectHeaderReader + "is_top_level": bool, # for STLVectorReader, STLMapReader, STLStringReader + } + ``` + + Args: + cls_streamer_info (dict): Class streamer information. + all_streamer_info (dict): All streamer information. + item_path (str): Path to the item. + + Returns: + dict: Reader configuration. + """ + fName = cls_streamer_info["fName"] + item_path = fName if item_path == "" else f"{item_path}.{fName}" + + for reader in sorted(readers, key=lambda x: x.priority(), reverse=True): + top_type_name = get_top_type_name(cls_streamer_info["fTypeName"]) + tree_config = reader.gen_tree_config( + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ) + if tree_config is not None: + return tree_config + + raise ValueError(f"Unknown type: {cls_streamer_info['fTypeName']} for {item_path}") + + +def get_reader_instance(tree_config: dict): + for cls_reader in sorted(readers, key=lambda x: x.priority(), reverse=True): + reader = cls_reader.get_reader_instance(tree_config) + if reader is not None: + return reader + + raise ValueError( + f"Unknown reader type: {tree_config['reader']} for {tree_config['name']}" + ) + + +def reconstruct_array( + raw_data: np.ndarray | tuple | list | None, + tree_config: dict, +) -> ak.Array | None: + for reader in sorted(readers, key=lambda x: x.priority(), reverse=True): + data = reader.reconstruct_array(raw_data, tree_config) + if data is not None: + return data + + raise ValueError( + f"Unknown reader type: {tree_config['reader']} for {tree_config['name']}" + ) + + +def gen_tree_config_from_type_name( + type_name: str, + all_streamer_info: dict, + item_path: str = "", +): + return gen_tree_config( + { + "fName": type_name, + "fTypeName": type_name, + }, + all_streamer_info, + item_path, + ) + + +def regularize_object_path(object_path: str) -> str: + return re.sub(r";[0-9]+", r"", object_path) + + +class BaseReader: + """ + Base class for all readers. + """ + + @classmethod + def priority(cls) -> int: + """ + The priority of the reader. Higher priority means the reader will be + used first. + """ + return 20 + + @classmethod + def gen_tree_config( + cls, + top_type_name: str, + cls_streamer_info: dict, + all_streamer_info: dict, + item_path: str = "", + ) -> dict: + raise NotImplementedError("This method should be implemented in subclasses.") + + @classmethod + def get_reader_instance(cls, tree_config: dict): + """ + Args: + tree_config (dict): The configuration dictionary for the reader. + + Returns: + An instance of the appropriate reader class. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + @classmethod + def reconstruct_array( + cls, + raw_data: np.ndarray | tuple | list | None, + tree_config: dict, + ) -> ak.Array | None: + """ + Args: + raw_data (Union[np.ndarray, tuple, list, None]): The raw data to be + recovered. + tree_config (dict): The configuration dictionary for the reader. + + Returns: + awkward.Array: The recovered data as an awkward array. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + +readers: set[BaseReader] = set() + + +class CTypeReader(BaseReader): + """ + This class reads C++ primitive types from a binary parser. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name in num_typenames: + ctype = num_typenames[top_type_name] + return { + "reader": ReaderType.CType, + "name": cls_streamer_info["fName"], + "ctype": ctype, + } + else: + return None + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.CType: + return None + + ctype = tree_config["ctype"] + return Cpp_CtypeReader(tree_config["name"], ctype) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.CType: + return None + + return ak.Array(raw_data) + + +class STLSequenceReader(BaseReader): + """ + This class reads STL sequence (vector, array) from a binary parser. + """ + + @staticmethod + def get_sequence_element_typename(type_name: str) -> str: + """ + Get the element type name of a vector type. + + e.g. vector> -> vector + """ + type_name = ( + type_name.replace("std::", "").replace("< ", "<").replace(" >", ">").strip() + ) + return re.match(r"^(vector|array)<(.*)>$", type_name).group(2) + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name not in ["vector", "array"]: + return None + + fName = cls_streamer_info["fName"] + fTypeName = cls_streamer_info["fTypeName"] + element_type = cls.get_sequence_element_typename(fTypeName) + element_info = { + "fName": fName, + "fTypeName": element_type, + } + + element_tree_config = gen_tree_config( + element_info, + all_streamer_info, + item_path, + ) + + top_element_type = get_top_type_name(element_type) + if top_element_type in stl_typenames: + element_tree_config["is_top"] = False + + return { + "reader": ReaderType.STLSequence, + "name": fName, + "element_reader": element_tree_config, + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.STLSequence: + return None + + element_reader = get_reader_instance(tree_config["element_reader"]) + is_top = tree_config.get("is_top", True) + return Cpp_STLSequenceReader(tree_config["name"], is_top, element_reader) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.STLSequence: + return None + + counts, element_raw_data = raw_data + element_data = reconstruct_array( + element_raw_data, + tree_config["element_reader"], + ) + return ak.unflatten(element_data, counts) + + +class STLMapReader(BaseReader): + """ + This class reads std::map from a binary parser. + """ + + @staticmethod + def get_map_key_val_typenames(type_name: str) -> tuple[str, str]: + """ + Get the key and value type names of a map type. + + e.g. map> -> (int, vector) + """ + type_name = ( + type_name.replace("std::", "").replace("< ", "<").replace(" >", ">").strip() + ) + return re.match( + r"^(map|unordered_map|multimap)<(.*),(.*)>$", type_name + ).groups()[1:3] + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name not in ["map", "unordered_map", "multimap"]: + return None + + fTypeName = cls_streamer_info["fTypeName"] + key_type_name, val_type_name = cls.get_map_key_val_typenames(fTypeName) + + fName = cls_streamer_info["fName"] + key_info = { + "fName": "key", + "fTypeName": key_type_name, + } + + val_info = { + "fName": "val", + "fTypeName": val_type_name, + } + + key_tree_config = gen_tree_config(key_info, all_streamer_info, item_path) + if get_top_type_name(key_type_name) in stl_typenames: + key_tree_config["is_top"] = False + + val_tree_config = gen_tree_config(val_info, all_streamer_info, item_path) + if get_top_type_name(val_type_name) in stl_typenames: + val_tree_config["is_top"] = False + + return { + "reader": ReaderType.STLMap, + "name": fName, + "key_reader": key_tree_config, + "val_reader": val_tree_config, + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.STLMap: + return None + + key_cpp_reader = get_reader_instance(tree_config["key_reader"]) + val_cpp_reader = get_reader_instance(tree_config["val_reader"]) + is_top = tree_config.get("is_top", True) + return Cpp_STLMapReader( + tree_config["name"], + is_top, + key_cpp_reader, + val_cpp_reader, + ) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.STLMap: + return None + + key_tree_config = tree_config["key_reader"] + val_tree_config = tree_config["val_reader"] + counts, key_raw_data, val_raw_data = raw_data + key_data = reconstruct_array(key_raw_data, key_tree_config) + val_data = reconstruct_array(val_raw_data, val_tree_config) + + return ak.unflatten( + ak.zip( + { + key_tree_config["name"]: key_data, + val_tree_config["name"]: val_data, + }, + with_name="pair", + ), + counts, + ) + + +class STLStringReader(BaseReader): + """ + This class reads std::string from a binary parser. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name != "string": + return None + + return { + "reader": ReaderType.STLString, + "name": cls_streamer_info["fName"], + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.STLString: + return None + + return Cpp_STLStringReader( + tree_config["name"], + tree_config.get("is_top", True), + ) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.STLString: + return None + + counts, data = raw_data + return ak.enforce_type(ak.unflatten(data, counts), "string") + + +class TArrayReader(BaseReader): + """ + This class reads TArray from a binary paerser. + + TArray includes TArrayC, TArrayS, TArrayI, TArrayL, TArrayF, and TArrayD. + Corresponding ctype is u1, u2, i4, i8, f, and d. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name not in tarray_typenames: + return None + + ctype = tarray_typenames[top_type_name] + return { + "reader": ReaderType.TArray, + "name": cls_streamer_info["fName"], + "ctype": ctype, + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.TArray: + return None + + return Cpp_TArrayReader(tree_config["name"], tree_config["ctype"]) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.TArray: + return None + + counts, data = raw_data + return ak.unflatten(data, counts) + + +class TStringReader(BaseReader): + """ + This class reads TString from a binary parser. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name != "TString": + return None + + return { + "reader": ReaderType.TString, + "name": cls_streamer_info["fName"], + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.TString: + return None + + return Cpp_TStringReader(tree_config["name"]) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.TString: + return None + + counts, data = raw_data + return ak.enforce_type(ak.unflatten(data, counts), "string") + + +class TObjectReader(BaseReader): + """ + This class reads TObject from a binary parser. + + It will not record any data. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name != "BASE": + return None + + fType = cls_streamer_info["fType"] + if fType != 66: + return None + + return { + "reader": ReaderType.TObject, + "name": cls_streamer_info["fName"], + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.TObject: + return None + + return Cpp_TObjectReader(tree_config["name"]) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + return None + + +class CArrayReader(BaseReader): + """ + This class reads a C-array from a binary parser. + """ + + @classmethod + def priority(cls): + return 100 # This reader should be called first + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if cls_streamer_info.get("fArrayDim", 0) == 0: + return None + + fName = cls_streamer_info["fName"] + fTypeName = cls_streamer_info["fTypeName"] + fArrayDim = cls_streamer_info["fArrayDim"] + fMaxIndex = cls_streamer_info["fMaxIndex"] + + element_streamer_info = cls_streamer_info.copy() + element_streamer_info["fArrayDim"] = 0 + + element_tree_config = gen_tree_config( + element_streamer_info, + all_streamer_info, + ) + + flat_size = np.prod(fMaxIndex[:fArrayDim]) + assert ( + flat_size > 0 + ), f"flatten_size should be greater than 0, but got {flat_size}" + + # c-type number or TArray + if top_type_name in num_typenames or top_type_name in tarray_typenames: + return { + "reader": ReaderType.CArray, + "name": fName, + "is_obj": False, + "element_reader": element_tree_config, + "flat_size": flat_size, + "fMaxIndex": fMaxIndex, + "fArrayDim": fArrayDim, + } + + # TSTring + elif top_type_name == "TString": + return { + "reader": ReaderType.CArray, + "name": fName, + "is_obj": True, + "element_reader": element_tree_config, + "flat_size": flat_size, + "fMaxIndex": fMaxIndex, + "fArrayDim": fArrayDim, + } + + # STL + elif top_type_name in stl_typenames: + element_tree_config["is_top"] = False + return { + "reader": ReaderType.CArray, + "name": fName, + "is_obj": True, + "flat_size": flat_size, + "element_reader": element_tree_config, + "fMaxIndex": fMaxIndex, + "fArrayDim": fArrayDim, + } + + else: + raise ValueError(f"Unknown type: {top_type_name} for C-array: {fTypeName}") + + @classmethod + def get_reader_instance(cls, tree_config: dict): + reader_type = tree_config["reader"] + if reader_type != ReaderType.CArray: + return None + + element_reader = get_reader_instance(tree_config["element_reader"]) + + return Cpp_CArrayReader( + tree_config["name"], + tree_config["is_obj"], + tree_config["flat_size"], + element_reader, + ) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.CArray: + return None + + element_tree_config = tree_config["element_reader"] + fMaxIndex = tree_config["fMaxIndex"] + fArrayDim = tree_config["fArrayDim"] + shape = [fMaxIndex[i] for i in range(fArrayDim)] + + element_data = reconstruct_array( + raw_data, + element_tree_config, + ) + + for s in shape[::-1]: + element_data = ak.unflatten(element_data, s) + + return element_data + + +class BaseObjectReader(BaseReader): + """ + Base class is what a custom class inherits from. + It has fNBytes(uint32), fVersion(uint16) at the beginning. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + if top_type_name != "BASE": + return None + + fType = cls_streamer_info["fType"] + if fType != 0: + return None + + fName = cls_streamer_info["fName"] + sub_streamers: list = all_streamer_info[fName] + + sub_tree_configs = [ + gen_tree_config(s, all_streamer_info, item_path) for s in sub_streamers + ] + + return { + "reader": ReaderType.BaseObject, + "name": fName, + "sub_readers": sub_tree_configs, + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.BaseObject: + return None + + sub_readers = [get_reader_instance(s) for s in tree_config["sub_readers"]] + return Cpp_BaseObjectReader(tree_config["name"], sub_readers) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.BaseObject: + return None + + sub_tree_configs = tree_config["sub_readers"] + + arr_dict = {} + for s_cfg, s_data in zip(sub_tree_configs, raw_data): + s_name = s_cfg["name"] + s_reader_type = s_cfg["reader"] + + if s_reader_type == ReaderType.TObject: + continue + + arr_dict[s_name] = reconstruct_array(s_data, s_cfg) + + return ak.Array(arr_dict) + + +class ObjectHeaderReader(BaseReader): + """ + This class read an object starting with an object header. + """ + + @classmethod + def priority(cls): + return 0 # should be called last + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + sub_streamers: list = all_streamer_info[top_type_name] + sub_tree_configs = [ + gen_tree_config(s, all_streamer_info, item_path) for s in sub_streamers + ] + return { + "reader": ReaderType.ObjectHeader, + "name": top_type_name, + "sub_readers": sub_tree_configs, + } + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.ObjectHeader: + return None + + sub_readers = [get_reader_instance(s) for s in tree_config["sub_readers"]] + return Cpp_ObjectHeaderReader(tree_config["name"], sub_readers) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.ObjectHeader: + return None + + sub_tree_configs = tree_config["sub_readers"] + + arr_dict = {} + for s_cfg, s_data in zip(sub_tree_configs, raw_data): + s_name = s_cfg["name"] + s_reader_type = s_cfg["reader"] + + if s_reader_type == ReaderType.TObject: + continue + + arr_dict[s_name] = reconstruct_array(s_data, s_cfg) + + return ak.Array(arr_dict) + + +class EmptyReader(BaseReader): + """ + This class does nothing. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ): + return None + + @classmethod + def get_reader_instance(cls, tree_config: dict): + if tree_config["reader"] != ReaderType.Empty: + return None + + return Cpp_EmptyReader(tree_config["name"]) + + @classmethod + def reconstruct_array(cls, raw_data, tree_config): + if tree_config["reader"] != ReaderType.Empty: + return None + + return np.empty(shape=(0,)) + + +readers |= { + CTypeReader, + STLSequenceReader, + STLMapReader, + STLStringReader, + TArrayReader, + TStringReader, + TObjectReader, + CArrayReader, + BaseObjectReader, + ObjectHeaderReader, + EmptyReader, +} + + +class AsCustom(uproot.CustomInterpretation): + target_branches: set[str] = set() + + def __init__(self, branch, context, simplify): + super().__init__(branch, context, simplify) + + # simplify streamer information + self.all_streamer_info: dict[str, list[dict]] = {} + for k, v in branch.file.streamers.items(): + cur_infos = [ + i.all_members for i in next(iter(v.values())).member("fElements") + ] + self.all_streamer_info[k] = cur_infos + + @classmethod + def match_branch( + cls, + branch: uproot.behaviors.TBranch.TBranch, + context: dict, + simplify: bool, + ) -> bool: + """ + Args: + branch (:doc:`uproot.behaviors.TBranch.TBranch`): The ``TBranch`` to + interpret as an array. + context (dict): Auxiliary data used in deserialization. + simplify (bool): If True, call + :ref:`uproot.interpretation.objects.AsObjects.simplify` on any + :doc:`uproot.interpretation.objects.AsObjects` to try to get a + more efficient interpretation. + + Accept arguments from `uproot.interpretation.identify.interpretation_of`, + determine whether this interpretation can be applied to the given branch. + """ + full_path = regularize_object_path(branch.object_path) + return full_path in cls.target_branches + + def __repr__(self) -> str: + """ + The string representation of the interpretation. + """ + return f"AsCustom({self.typename})" + + def basket_array( + self, + data, + byte_offsets, + basket, + branch, + context, + cursor_offset, + library, + interp_options, + ): + assert library.name == "ak", "Only awkward arrays are supported" + + full_branch_path = regularize_object_path(branch.object_path) + + # generate reader config + tree_config = gen_tree_config_from_type_name( + branch.streamer.typename, self.all_streamer_info, full_branch_path + ) + + # get reader + reader = get_reader_instance(tree_config) + + # read data + parser = Cpp_BinaryParser(data, byte_offsets) + for _ in range(parser.n_entries): + reader.read(parser) + + # recover raw data and return + raw_data = reader.get_data() + return reconstruct_array(raw_data, tree_config) + + +uproot.register_interpretation(AsCustom) diff --git a/dev/custom-interpretation/AsCustom/should_be_cpp.py b/dev/custom-interpretation/AsCustom/should_be_cpp.py new file mode 100644 index 000000000..aec4e7e94 --- /dev/null +++ b/dev/custom-interpretation/AsCustom/should_be_cpp.py @@ -0,0 +1,427 @@ +from __future__ import annotations + +from array import array +from typing import Literal + +import numpy as np + +ctype_hints = Literal["i1", "i2", "i4", "i8", "u1", "u2", "u4", "u8", "f", "d"] +type_np2array = { + "u1": "B", + "u2": "H", + "u4": "I", + "u8": "Q", + "i1": "b", + "i2": "h", + "i4": "i", + "i8": "q", + "f": "f", + "d": "d", +} + + +class Cpp_BinaryParser: + nbytes_dict = { + "u1": 1, + "u2": 2, + "u4": 4, + "u8": 8, + "i1": 1, + "i2": 2, + "i4": 4, + "i8": 8, + "f": 4, + "d": 8, + } + + def __init__( + self, + data: np.ndarray[np.uint8], + offsets: np.ndarray, + ): + """ + Args: + data (np.ndarray): The binary data to parse. + """ + self.data = data + self.offsets = offsets + self.cursor = 0 + + @property + def n_entries(self) -> int: + """ + Returns: + The number of entries in the binary data. + """ + return len(self.offsets) - 1 + + def read_number( + self, + ctype: Literal["u1", "u2", "u4", "u8", "i1", "i2", "i4", "i8", "f", "d"], + ) -> np.number: + nbytes = self.nbytes_dict[ctype] + value = self.data[self.cursor : self.cursor + nbytes].view(f">{ctype}")[0] + self.cursor += nbytes + return value + + def read_fNBytes(self) -> np.uint32: + nbytes = self.read_number("u4") + assert nbytes & 0x40000000 != 0, f"Invalid fNBytes: {nbytes:#x}" + return nbytes & ~np.uint32(0x40000000) + + def read_fVersion(self) -> np.uint16: + return self.read_number("u2") + + def read_null_terminated_string(self) -> str: + """ + Reads a null-terminated string from the binary data. + + Returns: + The null-terminated string. + """ + start = self.cursor + while self.data[self.cursor] != 0: + self.cursor += 1 + end = self.cursor + self.cursor += 1 + + return self.data[start:end].tobytes().decode("utf-8") + + def __repr__(self): + cur_data_str = str(self.data[self.cursor :]) + return f"BinaryParser({cur_data_str})" + + +class Cpp_BaseReader: + def read(self, parser: Cpp_BinaryParser) -> None: + raise AssertionError + + def get_data(self) -> np.ndarray | tuple: + """ + Returns: + The data read by the reader. + """ + raise AssertionError + + +class Cpp_CtypeReader(Cpp_BaseReader): + """ + This class reads C++ primitive types from a binary parser. + """ + + def __init__( + self, + name: str, + ctype: ctype_hints, + ): + self.name = name + self.ctype = ctype + self.data = array(type_np2array[ctype]) + + def read(self, parser: Cpp_BinaryParser): + self.data.append(parser.read_number(self.ctype)) + + def get_data(self): + return np.array(self.data, dtype=self.ctype, copy=True) + + +class Cpp_STLSequenceReader(Cpp_BaseReader): + """ + This class reads STL sequence (vector, array) from a binary parser. + """ + + def __init__(self, name: str, is_top: bool, element_reader: Cpp_BaseReader): + self.name = name + self.element_reader = element_reader + self.counts = array("Q") + self.is_top = is_top + + def read(self, parser): + # Read fNBytes and fVersion when it is top level + if self.is_top: + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + + # Read data + fSize = parser.read_number("u4") + for _ in range(fSize): + self.element_reader.read(parser) + + # Update counts + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + self.element_reader.get_data(), + ) + + +class Cpp_STLMapReader(Cpp_BaseReader): + """ + This class reads std::map, unordered_map, multimap from a binary parser. + """ + + def __init__( + self, + name: str, + is_top: bool, + key_reader: Cpp_BaseReader, + val_reader: Cpp_BaseReader, + ): + self.name = name + self.key_reader = key_reader + self.val_reader = val_reader + self.counts = array("Q") + self.is_top = is_top + + def read(self, parser): + # Read fNBytes and fVersion when it is top level + if self.is_top: + _ = parser.read_fNBytes() + _ = parser.read_number("u8") # I don't know what this is + + # Read data + fSize = parser.read_number("u4") + + if self.is_top: + for _ in range(fSize): + self.key_reader.read(parser) + for _ in range(fSize): + self.val_reader.read(parser) + else: + for _ in range(fSize): + self.key_reader.read(parser) + self.val_reader.read(parser) + + # Update counts + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + self.key_reader.get_data(), + self.val_reader.get_data(), + ) + + +class Cpp_STLStringReader(Cpp_BaseReader): + """ + This class reads std::string from a binary parser. + """ + + def __init__(self, name: str, is_top: bool): + self.name = name + self.data = array("B") + self.counts = array("Q") + self.is_top = is_top + + def read(self, parser): + # Read fNBytes and fVersion when it is top level + if self.is_top: + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + + # Get length of std::string + fSize = parser.read_number("u1") + if fSize == 255: + fSize = parser.read_number("u4") + + # Read data + for _ in range(fSize): + self.data.append(parser.read_number("u1")) + + # Update counts + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + np.asarray(self.data, dtype="B"), + ) + + +class Cpp_TArrayReader(Cpp_BaseReader): + """ + This class reads TArray from a binary paerser. + + TArray includes TArrayC, TArrayS, TArrayI, TArrayL, TArrayF, and TArrayD. + Corresponding ctype is u1, u2, i4, i8, f, and d. + """ + + def __init__( + self, + name: str, + ctype: Literal["i1", "i2", "i4", "i8", "f", "d"], + ): + self.name = name + self.ctype = ctype + self.data = array(type_np2array[ctype]) + self.counts = array("Q") + + def read(self, parser): + # Read data + fSize = parser.read_number("u4") + for _ in range(fSize): + self.data.append(parser.read_number(self.ctype)) + + # Update counts + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + np.asarray(self.data, dtype=self.ctype), + ) + + +class Cpp_TStringReader(Cpp_BaseReader): + """ + This class reads TString from a binary parser. + """ + + def __init__(self, name: str): + self.name = name + self.data = array("B") + self.counts = array("Q") + + def read(self, parser): + # Get length of TString + fSize = parser.read_number("u1") + if fSize == 255: + fSize = parser.read_number("u4") + + # Read data + for _ in range(fSize): + self.data.append(parser.read_number("u1")) + + # Update counts + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + np.asarray(self.data, dtype="B"), + ) + + +class Cpp_TObjectReader(Cpp_BaseReader): + """ + This class reads TObject from a binary parser. + + It will not record any data. + """ + + def __init__(self, name: str): + self.name = name + + def read(self, parser): + _ = parser.read_fVersion() # fVersion + _ = parser.read_number("u4") # fUniqueID + _ = parser.read_number("u4") # fBits + + def get_data(self): + return None # should I return anything? + + +class Cpp_CArrayReader(Cpp_BaseReader): + """ + This class reads a C-array from a binary parser. + """ + + def __init__( + self, + name: str, + is_obj: bool, + flat_size: int, + element_reader: Cpp_BaseReader, + ): + self.name = name + self.is_obj = is_obj + self.flat_size = flat_size + self.element_reader = element_reader + + def read(self, parser): + if self.is_obj: + # Read fNBytes and fVersion + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + + # Read data + for _ in range(self.flat_size): + self.element_reader.read(parser) + + def get_data(self): + return self.element_reader.get_data() + + +class Cpp_BaseObjectReader(Cpp_BaseReader): + """ + Base class is what a custom class inherits from. + It has fNBytes(uint32), fVersion(uint16) at the beginning. + """ + + def __init__(self, name: str, sub_readers: list[Cpp_BaseReader]): + self.name = name + self.sub_readers = sub_readers + + def read(self, parser): + # Read fNBytes and fVersion + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + + # Read data + for sub_reader in self.sub_readers: + sub_reader.read(parser) + + def get_data(self): + data = [] + for sub_reader in self.sub_readers: + data.append(sub_reader.get_data()) + return data + + +class Cpp_ObjectHeaderReader(Cpp_BaseReader): + """ + This class read an object starting with an object header. + """ + + def __init__(self, name: str, sub_readers: list[Cpp_BaseReader]): + """ + Args: + sub_readers (list[BaseReader]): The readers for the elements in the object. + """ + self.name = name + self.sub_readers = sub_readers + + def read(self, parser): + # read object header + _ = parser.read_fNBytes() + fTag = parser.read_number("i4") + _ = parser.read_null_terminated_string() if fTag == -1 else "" # fClassName + + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + for sub_reader in self.sub_readers: + sub_reader.read(parser) + + def get_data(self): + data = [] + for sub_reader in self.sub_readers: + data.append(sub_reader.get_data()) + return data + + +class Cpp_EmptyReader(Cpp_BaseReader): + """ + This class does nothing. + """ + + def __init__(self, name: str): + self.name = name + + def read(self, parser): + pass + + def get_data(self): + return None diff --git a/dev/custom-interpretation/README.md b/dev/custom-interpretation/README.md new file mode 100644 index 000000000..caffa08c0 --- /dev/null +++ b/dev/custom-interpretation/README.md @@ -0,0 +1,446 @@ +# Draft of "Custom Interpretation" + +## 1 Overview + +To read BES3 data via `uproot`, I implemented a custom interpretation and successfully converted BES3 custom classes to `awkward.Array` ([`pybes3`](https://pybes3.readthedocs.io/en/latest/user-manual/bes3-data-reading)). During this work, I found that it seems possible for `uproot` to provide a more general interface for user-defined interpretation, and even provide a general custom-class reading mechanism. + +In this draft, I illustrated some ideas and designs I have in mind, which may be useful for `uproot` to improve the functionality of custom-class reading. + +In [Custom-Interpretation Interface](#2-custom-interpretation-interface), I simply illustrated how `uproot` provides a custom interpretation interface. In [Reader Interface](#3-reader-interface), I introduced a new interface called `Reader`, which can save users a lot of work when implementing custom interpretation. + +For users, one can choose different entry points to read their custom classes: + +* Directly use pre-defined interpretation `AsCustom`: `AsCustom` uses pre-defined `Reader` classes to read custom classes. No extra work is needed for users. See [AsCustom](#31-pre-defined-interpretation-ascustom). +* Define custom `Reader`: This is a relatively simple way to implement custom reading rules. There is no need for users to implement a new interpretation, but only need to implement a few custom `Reader` classes. +* Define custom `Interpretation`: This is the most general way, but it is also the most complex way. + +```mermaid +flowchart LR + subgraph Users Entry Points + B[User-Defined] + C[Pre-Defined: AsCustom] + end + + subgraph Users Entry Points + E[User-Defined] + end + + A((Custom Interpretation Interface)) --> B + A --> C + C --> D((Reader Interface)) + D --> E + D --> F[Pre-Defined] +``` + +## 2 Custom-Interpretation Interface + +A base class for custom interpretation is defined in `uproot.interpretation.custom.CustomInterpretation`. Several methods are defined in this class (`cache_key`, `typename`, `final_array`, etc), but there are still some methods that need to be implemented by users (`match_branch`, `basket_array`, etc). + +### 2.1 Identification + +There is only one extra method is defined in `CustomInterpretation`: `match_branch`. This method tells `uproot` whether a branch should be applied with custom interpretation or not. `match_branch` receives the arguments given to `uproot.interpretation.identify.interpretation_of`, and should return `True` if the branch matches the custom interpretation, otherwise return `False`. + +When opening a new branch, method `uproot.interpretation.identify.interpretation_of` will loop each registered interpretation class and call its `match_branch` method. If `match_branch` returns `True`, this interpretation will be instanciated and applied to the branch. + +### 2.2 Pre-defined Interpretation "AsBinary" + +This interpretation is designed to directly read out the raw binary data for developing and debugging. It can be used as: + +```python +binary_data = my_branch.array(interpretation=AsBinary()) +``` + +See `uproot.interpretation.custom.AsBinary` for more details. + +## 3 Reader Interface + +### 3.1 Pre-defined Interpretation "AsCustom" + +Even though `uproot` can provide a general interface for user-defined interpretation, it is still difficult for users to convert custom classes to `awkward.Array` from zero. In my consideration, `uproot` should provide more functionality beyond just an interface. During the implementation of `pybes3`, I found that there are only a few things that need to be customized for users. Most of the time, the reading process is still following the standard ROOT parsing rules. + +Therefore, I designed an interpretation class `AsCustom`, and a new layer of interface, the `Reader`, to help users convert custom classes to `awkward.Array`. With `uproot` provided `AsCustom` interpretation and a series of pre-defined `Reader` classes, users can focus on implementing the custom rules for their own data format, instead of handling the whole reading process. + +`AsCustom` has a class property `target_branches: set[str]` to specify which branches should be applied with custom interpretation. Users can set this property to a set of full branch paths. + +A method `regularize_object_path` is defined (though there is another method `regularize_object_path` in `uproot/_util.py`) to remove suffix `;N` of the TTree object path. This method can help users to get the correct object path when using `AsCustom` interpretation. + +### 3.2 Usage Scenario + +For custom classes, data is stored in a tree structure, all streaming information can be accessed via `some_branch.file.streamers` in `uproot`. When some custom rules are applied to the tree structure, they are actually applied on only a few nodes of the tree most of the time. This means that for user, it is sufficient to implement the custom rules only for those nodes, while the rest of the tree can be left unchanged. + +For example, when a custom class is defined as: + +```c++ +class TMySubClass : public TObject { + int m_index; + float m_x; +}; + +class TMyClass : public TObject { + double m_energy; + std::vector m_daughters; +}; +``` + +The data tree for `TMyClass` could be: + +```mermaid +graph TD + A([TMyClass]) --> B(double m_energy) + A --> C(std::vector<TMySubClass> m_daughters) + C --> D([TMySubClass]) + D --> E(int m_index) + D --> F(float m_x) +``` + +While when `std::vector` is replaced with `TObjArray`, but still storing `TMySubClass` objects (`TObjArray` is desined for storing any kinds of objects), the definition and data tree would be: + +```c++ +class TMyClass : public TObject { + double m_double; + TObjArray m_obj_array; // still stores TMySubClass +}; +``` + +```mermaid +graph TD + A([TMyClass]) + A --> B(double m_energy) + A --> C(TObjArray m_daughters) + C --> D([TMySubClass]) + D --> E(int m_index) + D --> F(float m_x) + + style C stroke:red +``` + +In this case, the user would only need to implement the custom rule for that `TObjArray` node, leaving other nodes unchanged. + +### 3.3 Reader + +The structure of "data tree" is so similar to that of `awkward` array, that we can just keep unflattening and zipping data from the leaf nodes to the root node. Basing on this, I defined `Reader` to act as a "data tree" node. + +To read out a "data tree", we can loop over all nodes on the tree, select a proper `Reader` class for each node, and combine readers into a "reader tree". + +The leaves of the reader tree should always be basic types readers, such as `CTypeReader` where `T` is a basic type like `int`, `float`, etc. The internal and root nodes of the reader tree should be a kind of wrapper, like `STLSequenceReader`, `STLMapReader`, `ObjectReader`, which possess and handle some children readers. + +Basic types readers alway return a 1D array of the data they read. For wrapper readers, while wrapper readers mostly control their children readers to read data in a correct order, and sometimes record information about the array structure, such as the number of elements in a `std::vector` for `STLSequenceReader`. + +Basing on this consideration, I designed 5 procedures using `Reader` to convert custom classes to `awkward.Array`: +1. **Tree Configuration Generation**: Generate a tree configuration that describes the tree structure of the data. +2. **Reader Tree Building (In Python)**: Build a reader tree according to the tree configuration. +3. **Data Reading (In C++)**: Read data from the binary data stream using the reader tree. +4. **Raw Data Retrieval (From C++ to Python)**: Retrieve raw data from the reader tree. +5. **Array Reconstruction (In Python)**: Reconstruct the raw data into `awkward.Array` using the tree configuration. + +```mermaid +--- +title: How Reader Works +--- + +flowchart TD + Z[(All Streamers Information)] -.-> A[Tree Configuration Generating] + A --> B[(Tree Configuration)] + A --> C[Reader Tree Building] + B -.-> C + C --> D[Data Reading] + D --> E[Get Raw Data] + E --> F[Array Reconstruction] + E --> G[(Raw Data)] + F -.-> H[(awkward.Array)] + G -.-> F + B -.-> F +``` + +Below I will illustrate how `Reader` works in detail. + +#### 3.3.1 Tree Configuration Generation (In Python) + +Since reading process should be implemented in C++, the tree configuration is a bridge between the building reader tree and reconstructing data arrays. It is a nested `dict`, generated by looping streamer information recursively. In the tree configuration, each node is represented as a dictionary, which contains: + +* `reader`: Which `Reader` class to use for this node. +* `name`: The name of the node. +* Other information needed for constructing `Reader` class, such as: + * `ctype` (for `CTypeReader`): The value type to be read, for example, `int`, `float`, `double`, etc. + * `element_reader` (for `STLSequenceReader`): The configuration dictionary of the element reader. + * `sub_readers` (for `ObjectReader`): A list of configuration dictionaries of the sub-readers. + * ... + +```mermaid +--- +title: Tree Configuration Generation +--- + +sequenceDiagram + participant AC as Interpretation + participant R as Root Node + participant E as ... + participant L as Leaf Nodes + + AC->>+R: gen_tree_config(**kwargs) + loop Children Data + R->>+E: gen_tree_config(**kwargs) + loop Children Data + E->>+L: gen_tree_config(**kwargs) + L->>-E: tree_config + end + E->>-R: tree_config + end + R->>-AC: tree_config +``` + +An example of tree configuration is: + +```python +{ + "reader": "ObjectReader", + "name": "TMyClass", + "sub_readers": [ + { + "reader": "CTypeReader", + "name": "m_double", + "ctype": "double" + }, + { + "reader": "STLSequenceReader", + "name": "m_data", + "element_reader": { + "reader": "CTypeReader", + "name": "element" + "ctype": "int", + } + } + ] +} +``` + +#### 3.3.2 Reader Tree Building (In Python) + +After generating a tree configuration, we can build a reader tree. All readers are instanciated according to nodes in the tree configuration. Children readers are instanciated first, and then passed to their parent reader as constructor argument. This procedure can combine all readers into a tree structure either in Python or C++. + + +```mermaid +--- +title: Reader Tree Building +--- + +sequenceDiagram + participant AC as Interpretation + participant R as Root Node + participant E as ... + participant L as Leaf Nodes + + AC->>+R: get_reader(tree_config) + loop Children Readers + R->>+E: get_reader(tree_config) + loop Children Readers + E->>+L: get_reader(tree_config) + L->>-E: reader instance + end + E->>-R: reader instance + end + R->>-AC: reader instance +``` + +#### 3.3.3 Data Reading & Raw Data Retrieval (In C++) + +Once the reader tree is built, a `BinaryParser` object will be instanciated with the binary data and byte-offsets. Then, the `read` method of the root reader will be called `n-evt` times to read data from the binary data stream, where `n-evt` is the number of events in the binary data. The root reader will call its children readers recursively to read data. + +Once the reading process finished, `get_data` method of the root reader will be called, recursively calling the `get_data` method of its children readers to retrieve data they read. Readers at the leaves of the reader tree returns an numpy array of what they read, while readers at the internal nodes return a tuple packaging their children readers' data and their own data together (e.g. `STLSequenceReader` returns `(counts, element_raw_data)` where `counts` is the number of elements in each call of `read` and `element_raw_data` is the raw data read by its children reader). + +I don't directly return an `awkward.Array` from the `get_data` method, since the reading process should be implemented in C++. Though directly returning `awkward.Array` in C++ is possible, for users who want to do more complex things, Python is more flexible. + +```mermaid +--- +title: Data Reading +--- +sequenceDiagram + participant AC as Interpretation + participant R as Root Reader + participant E as ... + participant L as Leaf Readers + + loop Every Event + AC->>R: read(parser) + loop Children Readers + R->>E: read(parser) + loop Children Readers + E->>L: read(parser) + end + end + end +``` + +```mermaid +--- +title: Raw Data Retrieval +--- + +sequenceDiagram + participant AC as Interpretation + participant R as Root Reader + participant E as ... + participant L as Leaf Readers + + AC->>+R: get_data() + loop Children Readers + R->>+E: get_data() + loop Children Readers + E->>+L: get_data() + L->>-E: raw_data + end + E->>-R: raw_data + end + R->>-AC: raw_data +``` + +#### 3.3.4 Array Reconstruction (In Python) + +By calling `get_data` method of the root reader, we can get a nested raw data only containing `tuple` and numpy arrays. The next step is to reconstruct those numpy arrays into `awkward.Array`. This process is irrelevant to the reader tree instance, since they work in C++, and this is why we need "tree configuration" to bridge the reader tree and the reconstruction process. + +Array reconstruction is done by calling `reconstruct_array` method. There are mostly 3 behaviors in this method: + +* Directly return a numpy array (e.g. `CTypeReader`) +* Call `awkward.unflatten` to "unflatten" an array coming from children readers (e.g. `STLSequenceReader`) +* Call `awkwar.zip` to combine arrays coming from children readers (e.g. `ObjectReader`) + +In expectation, the root reader will return an `awkward.Array` containing the whole data. + +```mermaid +sequenceDiagram + participant AC as Interpretation + participant R as Root Node + participant E as ... + participant L as Leaf Nodes + + AC->>+R: reconstruct_array(tree_config, raw_data) + loop Children Readers + R->>+E: reconstruct_array(tree_config, raw_data) + loop Children Readers + E->>+L: reconstruct_array(tree_config, raw_data) + L->>-E: awkward array + end + E->>-R: awkward array + end + R->>-AC: awkward array +``` + +### 3.4 Registration Mechanism of Readers + +A registration mechanism is designed to allow users to register their custom readers: `AsCustom` maintains a set of registered reader classes, and will loop over them in procedure [tree-configuration-generation](#331-tree-configuration-generation-in-python), [reader-tree-building](#332-reader-tree-building-in-python) and [array-reconstruction](#334-array-reconstruction-in-python). + +Take "tree-configuration-generation" as an example, the main `gen_tree_config` function loops over each registered reader and call its `gen_tree_config` method. Once any reader returns a non-`None` value, the loop will break and return that configuration. The `Reader.priority` method is used to sort the registered readers, so that specific reader can be called first. + +Here is a simplified version of `gen_tree_config`: + +```python +def gen_tree_config( + cls_streamer_info: dict, + all_streamer_info: dict, + item_path: str = "", +) -> dict: + # Some helper variables to identify node location + ... + + # Loop over all registered readers, and check if they can handle this node + for reader in sorted(readers, key=lambda x: x.priority(), reverse=True): + top_type_name = get_top_type_name(cls_streamer_info["fTypeName"]) + tree_config = reader.gen_tree_config( + top_type_name, + cls_streamer_info, + all_streamer_info, + item_path, + ) + if tree_config is not None: + _gen_tree_config_depth -= 1 + return tree_config + + raise ValueError(f"Unknown type: {cls_streamer_info['fTypeName']} for {item_path}") +``` + +It is the same for the main `get_reader` and `reconstruct_array` methods. + +### 3.5 Split Reader into Python/C++ + +Here is a relationship diagram of `Reader` in Python/C++, where `PyXXXReader` is a Python class and `CppXXXReader` is a C++ class. `PyXXXReader` returns an instance of `CppXXXReader` in its `get_reader_instance` method. + +```mermaid +classDiagram + direction BT + class CppBaseReader{ + +void read(BinaryParser) + +py::object get_data() + } + + class CppConcreteReader{ + +CppConcreteReader(...) # constructor + } + + class PyBaseReader{ + +@classmethod priority() -> int + +@classmethod gen_tree_config(...) -> dict + +@classmethod get_reader_instance(...) -> CppBaseReader + +@classmethod reconstruct_array(...) -> ak.Array + } + + CppConcreteReader --|> CppBaseReader : Inherit + PyConcreteReader --|> PyBaseReader : Inherit + CppConcreteReader <|.. PyConcreteReader : Instanciate +``` + + + +### 3.6 BinaryParser + +`BinaryParser` is a utility class to parse binary data stream. It is designed to: + +* Receive binary data and byte-offsets as input in constructor. +* Be passed to `Reader::read` method. +* Implemented in C++. + + +### 3.7 Pre-defined Reader & Custom Reader + +As [Usage Scenario](#32-usage-scenario) illustrated, most of the time, user only needs to implement the custom rules for a few nodes of the tree, i.e. user only needs to implement a few custom `Reader` classes, instead of implementing a new custom interpretation. + +Therefore, to simplify user's work, `uproot` may provide a set of pre-defined `Reader` classes for common data types, such as `CTypeReader`, `STLSequenceReader`, `STLMapReader`, etc. User can implement his own custom `Reader` classes inheriting from a `BaseReader` (defined in `dev/custom-interpretation/AsCustom`). + +There is no difference between pre-defined `Reader` classes and custom `Reader` classes. They all need to be registered in `uproot` to be used. The only difference is that pre-defined `Reader` classes are already registered in `uproot`, while custom `Reader` classes need to be registered by user. + +## 4 Implemention + +### 4.1 Custom Interpretation + +I made these modification for the custom interpretation interface: + +* Add the registration mechanism to `src/uproot/interpretation/identify.py` +* Add `CustomInterpretation` base class in `src/uproot/interpretation/custom.py` + +### 4.2 AsCustom and Pre-Defined Reader + +I have implemented the `AsCustom` interpretation and some readers in `/dev/custom-interpretation/AsCustom`. Classes in `should_be_cpp.py` are the ones that should be implemented in C++, but for simplicity I implemented them in Python. + +#### 4.2.1 Tiny Example + +I wrote a tiny example in `dev/custom-interpretation`. 2 custom classes, `TMyObject` and `TMySubObject`, are defined in `dev/custom-interpretation/gen-data`. The data is generated in `dev/custom-interpretation/tiny_reader/gen-data/example.root`. + +In this example, I used `TobjArray` to store `TMySubObject` objects. Since I know that there will be only one type of object in `TobjArray`, I implemented my custom `Reader` in `dev/custom-interpretation/tiny_reader` to optimize reading process and convert `TobjArray` to `awkward.Array`. + +#### 4.2.2 BES3 Example + +In BES3, storing custom classes in `TObjArray` is very common (That's why I take this as an example). And I implemented a `Bes3TObjArrayReader` as another example. + +## 5 Further Discussion and Problems + +### 5.1 `uproot-cpp` ? + +In the draft, `uproot` needs to: + +* Provide a C++ base class of `Reader` to users. +* Implemented a series of pre-defined `Reader` classes in C++. + +So I think it is better to set up a new repository `uproot-cpp` to store C++ code, as `awkward` did. + +### 5.2 Improve Pre-defined Readers + +Since I only know little about ROOT streaming rules, those pre-defined `Reader` classes are very rough and may not work in some cases. They need to be improved to cover more cases. diff --git a/dev/custom-interpretation/bes3/__init__.py b/dev/custom-interpretation/bes3/__init__.py new file mode 100644 index 000000000..296e46a5b --- /dev/null +++ b/dev/custom-interpretation/bes3/__init__.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import awkward as ak +from AsCustom import ( + AsCustom, + BaseReader, + ReaderType, + gen_reader_config, + get_reader_instance, + readers, + reconstruct_array, +) + +from .should_be_cpp import Cpp_Bes3TObjArrayReader + +bes3_branch2types = { + "/Event:TMcEvent/m_mdcMcHitCol": "TMdcMc", + "/Event:TMcEvent/m_cgemMcHitCol": "TCgemMc", + "/Event:TMcEvent/m_emcMcHitCol": "TEmcMc", + "/Event:TMcEvent/m_tofMcHitCol": "TTofMc", + "/Event:TMcEvent/m_mucMcHitCol": "TMucMc", + "/Event:TMcEvent/m_mcParticleCol": "TMcParticle", + "/Event:TDigiEvent/m_mdcDigiCol": "TMdcDigi", + "/Event:TDigiEvent/m_cgemDigiCol": "TCgemDigi", + "/Event:TDigiEvent/m_emcDigiCol": "TEmcDigi", + "/Event:TDigiEvent/m_tofDigiCol": "TTofDigi", + "/Event:TDigiEvent/m_mucDigiCol": "TMucDigi", + "/Event:TDigiEvent/m_lumiDigiCol": "TLumiDigi", + "/Event:TDstEvent/m_mdcTrackCol": "TMdcTrack", + "/Event:TDstEvent/m_emcTrackCol": "TEmcTrack", + "/Event:TDstEvent/m_tofTrackCol": "TTofTrack", + "/Event:TDstEvent/m_mucTrackCol": "TMucTrack", + "/Event:TDstEvent/m_mdcDedxCol": "TMdcDedx", + "/Event:TDstEvent/m_extTrackCol": "TExtTrack", + "/Event:TDstEvent/m_mdcKalTrackCol": "TMdcKalTrack", + "/Event:TRecEvent/m_recMdcTrackCol": "TRecMdcTrack", + "/Event:TRecEvent/m_recMdcHitCol": "TRecMdcHit", + "/Event:TRecEvent/m_recEmcHitCol": "TRecEmcHit", + "/Event:TRecEvent/m_recEmcClusterCol": "TRecEmcCluster", + "/Event:TRecEvent/m_recEmcShowerCol": "TRecEmcShower", + "/Event:TRecEvent/m_recTofTrackCol": "TRecTofTrack", + "/Event:TRecEvent/m_recMucTrackCol": "TRecMucTrack", + "/Event:TRecEvent/m_recMdcDedxCol": "TRecMdcDedx", + "/Event:TRecEvent/m_recMdcDedxHitCol": "TRecMdcDedxHit", + "/Event:TRecEvent/m_recExtTrackCol": "TRecExtTrack", + "/Event:TRecEvent/m_recMdcKalTrackCol": "TRecMdcKalTrack", + "/Event:TRecEvent/m_recMdcKalHelixSegCol": "TRecMdcKalHelixSeg", + "/Event:TRecEvent/m_recEvTimeCol": "TRecEvTime", + "/Event:TRecEvent/m_recZddChannelCol": "TRecZddChannel", + "/Event:TEvtRecObject/m_evtRecTrackCol": "TEvtRecTrack", + "/Event:TEvtRecObject/m_evtRecVeeVertexCol": "TEvtRecVeeVertex", + "/Event:TEvtRecObject/m_evtRecPi0Col": "TEvtRecPi0", + "/Event:TEvtRecObject/m_evtRecEtaToGGCol": "TEvtRecEtaToGG", + "/Event:TEvtRecObject/m_evtRecDTagCol": "TEvtRecDTag", + "/Event:THltEvent/m_hltRawCol": "THltRaw", +} + + +class Bes3TObjArrayReader(BaseReader): + @classmethod + def gen_reader_config( + cls, + top_type_name: str, + cls_streamer_info: dict, + all_streamer_info: dict, + item_path: str = "", + ): + if top_type_name != "TObjArray": + return None + + obj_typename = bes3_branch2types.get(item_path.replace(".TObjArray*", "")) + if obj_typename is None: + return None + + if obj_typename not in all_streamer_info: + return { + "reader": "MyTObjArrayReader", + "name": cls_streamer_info["fName"], + "element_reader": { + "reader": ReaderType.Empty, + "name": obj_typename, + }, + } + + sub_reader_config = [] + for s in all_streamer_info[obj_typename]: + sub_reader_config.append( + gen_reader_config(s, all_streamer_info, item_path + f".{obj_typename}") + ) + + return { + "reader": "MyTObjArrayReader", + "name": cls_streamer_info["fName"], + "element_reader": { + "reader": ReaderType.ObjectHeader, + "name": obj_typename, + "sub_readers": sub_reader_config, + }, + } + + @staticmethod + def get_reader_instance(reader_config: dict): + if reader_config["reader"] != "MyTObjArrayReader": + return None + + element_reader_config = reader_config["element_reader"] + element_reader = get_reader_instance(element_reader_config) + + return Cpp_Bes3TObjArrayReader(reader_config["name"], element_reader) + + @staticmethod + def reconstruct_array(raw_data, reader_config: dict): + if reader_config["reader"] != "MyTObjArrayReader": + return None + + counts, element_raw_data = raw_data + element_reader_config = reader_config["element_reader"] + element_data = reconstruct_array( + element_raw_data, + element_reader_config, + ) + + return ak.unflatten(element_data, counts) + + +def register(): + readers.add(Bes3TObjArrayReader) + AsCustom.target_branches |= set(bes3_branch2types.keys()) | { + "/Event:EventNavigator/m_mcMdcMcHits", + "/Event:EventNavigator/m_mcMdcTracks", + "/Event:EventNavigator/m_mcEmcMcHits", + "/Event:EventNavigator/m_mcEmcRecShowers", + } diff --git a/dev/custom-interpretation/bes3/should_be_cpp.py b/dev/custom-interpretation/bes3/should_be_cpp.py new file mode 100644 index 000000000..83fa03959 --- /dev/null +++ b/dev/custom-interpretation/bes3/should_be_cpp.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from array import array + +import numpy as np +from AsCustom import ( + ObjectHeaderReader, +) +from AsCustom.should_be_cpp import Cpp_BaseReader + +bes3_branch2types = { + "/Event:TMcEvent/m_mdcMcHitCol": "TMdcMc", + "/Event:TMcEvent/m_cgemMcHitCol": "TCgemMc", + "/Event:TMcEvent/m_emcMcHitCol": "TEmcMc", + "/Event:TMcEvent/m_tofMcHitCol": "TTofMc", + "/Event:TMcEvent/m_mucMcHitCol": "TMucMc", + "/Event:TMcEvent/m_mcParticleCol": "TMcParticle", + "/Event:TDigiEvent/m_mdcDigiCol": "TMdcDigi", + "/Event:TDigiEvent/m_cgemDigiCol": "TCgemDigi", + "/Event:TDigiEvent/m_emcDigiCol": "TEmcDigi", + "/Event:TDigiEvent/m_tofDigiCol": "TTofDigi", + "/Event:TDigiEvent/m_mucDigiCol": "TMucDigi", + "/Event:TDigiEvent/m_lumiDigiCol": "TLumiDigi", + "/Event:TDstEvent/m_mdcTrackCol": "TMdcTrack", + "/Event:TDstEvent/m_emcTrackCol": "TEmcTrack", + "/Event:TDstEvent/m_tofTrackCol": "TTofTrack", + "/Event:TDstEvent/m_mucTrackCol": "TMucTrack", + "/Event:TDstEvent/m_mdcDedxCol": "TMdcDedx", + "/Event:TDstEvent/m_extTrackCol": "TExtTrack", + "/Event:TDstEvent/m_mdcKalTrackCol": "TMdcKalTrack", + "/Event:TRecEvent/m_recMdcTrackCol": "TRecMdcTrack", + "/Event:TRecEvent/m_recMdcHitCol": "TRecMdcHit", + "/Event:TRecEvent/m_recEmcHitCol": "TRecEmcHit", + "/Event:TRecEvent/m_recEmcClusterCol": "TRecEmcCluster", + "/Event:TRecEvent/m_recEmcShowerCol": "TRecEmcShower", + "/Event:TRecEvent/m_recTofTrackCol": "TRecTofTrack", + "/Event:TRecEvent/m_recMucTrackCol": "TRecMucTrack", + "/Event:TRecEvent/m_recMdcDedxCol": "TRecMdcDedx", + "/Event:TRecEvent/m_recMdcDedxHitCol": "TRecMdcDedxHit", + "/Event:TRecEvent/m_recExtTrackCol": "TRecExtTrack", + "/Event:TRecEvent/m_recMdcKalTrackCol": "TRecMdcKalTrack", + "/Event:TRecEvent/m_recMdcKalHelixSegCol": "TRecMdcKalHelixSeg", + "/Event:TRecEvent/m_recEvTimeCol": "TRecEvTime", + "/Event:TRecEvent/m_recZddChannelCol": "TRecZddChannel", + "/Event:TEvtRecObject/m_evtRecTrackCol": "TEvtRecTrack", + "/Event:TEvtRecObject/m_evtRecVeeVertexCol": "TEvtRecVeeVertex", + "/Event:TEvtRecObject/m_evtRecPi0Col": "TEvtRecPi0", + "/Event:TEvtRecObject/m_evtRecEtaToGGCol": "TEvtRecEtaToGG", + "/Event:TEvtRecObject/m_evtRecDTagCol": "TEvtRecDTag", + "/Event:THltEvent/m_hltRawCol": "THltRaw", +} + + +class Cpp_Bes3TObjArrayReader(Cpp_BaseReader): + """ + This class reads a TObjArray from a binary parser. + + I know that there is only 1 kind of class in the TObjArray I will read, + so I can use only 1 reader to read all elements in TObjArray. + """ + + def __init__(self, name: str, element_reader: ObjectHeaderReader): + """ + Args: + element_reader (BaseReader): The reader for the elements in the array. + """ + self.name = name + self.element_reader = element_reader + self.counts = array("Q") + + def read(self, parser): + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + _ = parser.read_fVersion() + _ = parser.read_number("u4") # fUniqueID + _ = parser.read_number("u4") # fBits + + # Just directly read data + _ = parser.read_number("u1") # fName + fSize = parser.read_number("u4") + _ = parser.read_number("u4") # fLowerBound + + for _ in range(fSize): + self.element_reader.read(parser) + + # Update offsets + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + self.element_reader.get_data(), + ) diff --git a/dev/custom-interpretation/example.py b/dev/custom-interpretation/example.py new file mode 100644 index 000000000..afccf4e20 --- /dev/null +++ b/dev/custom-interpretation/example.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +import tiny_reader + +import uproot + +tiny_reader.register() + +f = uproot.open("tiny_reader/gen-data/example.root") +f["my_tree"].show() + +print("==================================") +arr = f["my_tree"].arrays() +arr.show(all=True) diff --git a/dev/custom-interpretation/tiny_reader/__init__.py b/dev/custom-interpretation/tiny_reader/__init__.py new file mode 100644 index 000000000..748f49222 --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/__init__.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import awkward as ak +from AsCustom import ( + AsCustom, + BaseReader, + ReaderType, + gen_tree_config, + get_reader_instance, + readers, + reconstruct_array, +) + +from .should_be_cpp import Cpp_MyTObjArrayReader + + +class MyTObjArrayReader(BaseReader): + """ + This class reads a TObjArray from a binary parser. + + I know that there is only 1 kind of class in the TObjArray I will read, + so I can use only 1 reader to read all elements in TObjArray. + """ + + @classmethod + def gen_tree_config( + cls, + top_type_name: str, + cls_streamer_info: dict, + all_streamer_info: dict, + item_path: str = "", + ): + if top_type_name != "TObjArray": + return None + + obj_typename = "TMySubObject" + + if obj_typename not in all_streamer_info: + return { + "reader": "MyTObjArrayReader", + "name": cls_streamer_info["fName"], + "element_reader": { + "reader": ReaderType.Empty, + "name": obj_typename, + }, + } + + sub_reader_config = [] + for s in all_streamer_info[obj_typename]: + sub_reader_config.append( + gen_tree_config(s, all_streamer_info, item_path + f".{obj_typename}") + ) + + return { + "reader": "MyTObjArrayReader", + "name": cls_streamer_info["fName"], + "element_reader": { + "reader": ReaderType.ObjectHeader, + "name": obj_typename, + "sub_readers": sub_reader_config, + }, + } + + @staticmethod + def get_reader_instance( + reader_config: dict, + ): + if reader_config["reader"] != "MyTObjArrayReader": + return None + + element_reader_config = reader_config["element_reader"] + element_reader = get_reader_instance(element_reader_config) + + return Cpp_MyTObjArrayReader(reader_config["name"], element_reader) + + @staticmethod + def reconstruct_array(raw_data, reader_config: dict): + if reader_config["reader"] != "MyTObjArrayReader": + return None + + counts, element_raw_data = raw_data + element_reader_config = reader_config["element_reader"] + element_data = reconstruct_array( + element_raw_data, + element_reader_config, + ) + + return ak.unflatten(element_data, counts) + + +def register(): + readers.add(MyTObjArrayReader) + AsCustom.target_branches.add("/my_tree:my_obj/m_obj_array") diff --git a/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt b/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt new file mode 100644 index 000000000..cb2225af3 --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.20) + +project(gen-data LANGUAGES CXX) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_BUILD_TYPE Debug) + +find_package(ROOT REQUIRED COMPONENTS Core RIO Tree) + +add_executable(gen-data + src/main.cc + src/TMyObject.cc + src/TMySubObject.cc +) + +target_include_directories(gen-data + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include +) + +target_link_libraries(gen-data + PRIVATE + ROOT::Core + ROOT::RIO + ROOT::Tree +) + +ROOT_GENERATE_DICTIONARY(MyRootDict + include/TMyObject.hh + include/TMySubObject.hh + LINKDEF include/LinkDef.h + MODULE gen-data +) diff --git a/dev/custom-interpretation/tiny_reader/gen-data/example.root b/dev/custom-interpretation/tiny_reader/gen-data/example.root new file mode 100644 index 0000000000000000000000000000000000000000..5e4d06bee6177e2785a5bd2a7845695e9a33c5a8 GIT binary patch literal 57712 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 Ug)sY}HFmRsCO<4FH=V)&0OiwI$^ZZW literal 0 HcmV?d00001 diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h b/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h new file mode 100644 index 000000000..0c75b079c --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h @@ -0,0 +1,10 @@ +#ifdef __CLING__ + +// # pragma link off all globals; +// # pragma link off all classes; +// # pragma link off all functions; + +# pragma link C++ class TMyObject + ; +# pragma link C++ class TMySubObject + ; + +#endif diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh b/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh new file mode 100644 index 000000000..6889a266c --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +using std::vector; + +class TMyObject : public TObject { +public: + TMyObject(int counter); + +private: + int m_counter = 0; + + TObjArray m_obj_array; + + ClassDef(TMyObject, 1) +}; diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh b/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh new file mode 100644 index 000000000..f138ae0e6 --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::map; +using std::string; +using std::vector; + +class TMySubObject : public TObject { +public: + TMySubObject(); + TMySubObject(int &counter); + + int set_data(int counter = 0); + +private: + // ------------ single elements ------------ // + // ctypes + int m_int; + int16_t m_int16; + ULong_t m_ulong; + + // STL + vector m_vec_int; + map m_map_int_double; + string m_stdstring; + + vector> m_vec_vec_int; + vector> m_vec_map_int_double; + + // ROOT types + TString m_tstring; + TArrayF m_tarrayf; + + // ------------ C-Arrays ------------ // + // 1d arrays + int m_carr_int[3]; + vector m_carr_vec_int[3]; + TString m_carr_tstring[3]; + TArrayF m_carr_tarrayf[3]; + + // 2d arrays + int m_carr2d_int[2][3]; + vector m_carr2d_vec_int[2][3]; + TString m_carr2d_tstring[2][3]; + TArrayF m_carr2d_tarrayf[2][3]; + + ClassDef(TMySubObject, 1) +}; diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc new file mode 100644 index 000000000..6da604361 --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc @@ -0,0 +1,10 @@ +#include "TMyObject.hh" +#include "TMySubObject.hh" + +ClassImp(TMyObject); + +TMyObject::TMyObject(int counter) : m_counter(counter) { + for (int i = 0; i < 3; i++) { + m_obj_array.Add(new TMySubObject(m_counter)); + } +} diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc new file mode 100644 index 000000000..9ad40629d --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc @@ -0,0 +1,64 @@ +#include "TMySubObject.hh" +#include + +ClassImp(TMySubObject); + +TMySubObject::TMySubObject() { set_data(); } +TMySubObject::TMySubObject(int &counter) { counter = set_data(counter); } + +int TMySubObject::set_data(int counter) { + // ------------ single elements ------------ // + m_int = counter++; + m_int16 = counter++; + m_ulong = counter++; + + m_vec_int = {counter++, counter++, counter++}; + m_map_int_double = {{counter++, (double)counter++}, + {counter++, (double)counter++}}; + m_stdstring = "I'm std::string " + std::to_string(counter++) + "!"; + for (int i = 0; i < 20; i++) { + m_stdstring += ("I'm std::string " + std::to_string(counter++) + "!"); + } + + m_vec_vec_int = {{counter++, counter++, counter++}, {counter++, counter++}}; + m_vec_map_int_double = vector>(); + for (int i = 0; i < 4; i++) { + map m; + for (int j = 0; j < 3; j++) { + m[counter++] = (double)counter++; + } + m_vec_map_int_double.push_back(m); + } + + m_tstring = Form("I'm TString %d", counter++); + + m_tarrayf = TArrayF(3); + m_tarrayf[0] = (float)counter++; + m_tarrayf[1] = (float)counter++; + m_tarrayf[2] = (float)counter++; + + // ------------ C-Arrays ------------ // + for (int i = 0, counter = 29; i < 3; i++) { + // 1d arrays + m_carr_int[i] = counter++; + m_carr_vec_int[i] = {counter++, counter++, counter++}; + m_carr_tstring[i] = Form("I'm TString %d", counter++); + m_carr_tarrayf[i] = TArrayF(3); + m_carr_tarrayf[i][0] = counter++; + m_carr_tarrayf[i][1] = counter++; + m_carr_tarrayf[i][2] = counter++; + + // 2d arrays + for (int j = 0; j < 2; j++) { + m_carr2d_int[j][i] = counter++; + m_carr2d_vec_int[j][i] = {counter++, counter++, counter++}; + m_carr2d_tstring[j][i] = Form("I'm TString %d", counter++); + m_carr2d_tarrayf[j][i] = TArrayF(3); + m_carr2d_tarrayf[j][i][0] = counter++; + m_carr2d_tarrayf[j][i][1] = counter++; + m_carr2d_tarrayf[j][i][2] = counter++; + } + } + + return counter; +} diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc new file mode 100644 index 000000000..5e61ba850 --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc @@ -0,0 +1,21 @@ +#include +#include + +#include "TMyObject.hh" + +int main() { + TFile f("test.root", "RECREATE"); + TTree t("my_tree", "tree"); + + TMyObject my_obj(0); + + t.Branch("my_obj", &my_obj); + + for (int i = 0; i < 100; i++) { + my_obj = TMyObject(i); + t.Fill(); + } + + t.Write(); + f.Close(); +} diff --git a/dev/custom-interpretation/tiny_reader/should_be_cpp.py b/dev/custom-interpretation/tiny_reader/should_be_cpp.py new file mode 100644 index 000000000..8c7677abd --- /dev/null +++ b/dev/custom-interpretation/tiny_reader/should_be_cpp.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from array import array + +import numpy as np +from AsCustom.should_be_cpp import Cpp_BaseReader, Cpp_ObjectHeaderReader + + +class Cpp_MyTObjArrayReader(Cpp_BaseReader): + """ + This class reads a TObjArray from a binary parser. + + I know that there is only 1 kind of class in the TObjArray I will read, + so I can use only 1 reader to read all elements in TObjArray. + """ + + def __init__(self, name: str, element_reader: Cpp_ObjectHeaderReader): + """ + Args: + element_reader (BaseReader): The reader for the elements in the array. + """ + self.name = name + self.element_reader = element_reader + self.counts = array("Q") + + def read(self, parser): + _ = parser.read_fNBytes() + _ = parser.read_fVersion() + _ = parser.read_fVersion() + _ = parser.read_number("u4") # fUniqueID + _ = parser.read_number("u4") # fBits + + # Just directly read data + _ = parser.read_number("u1") # fName + fSize = parser.read_number("u4") + _ = parser.read_number("u4") # fLowerBound + + for _ in range(fSize): + self.element_reader.read(parser) + + # Update offsets + self.counts.append(fSize) + + def get_data(self): + return ( + np.asarray(self.counts, dtype="Q"), + self.element_reader.get_data(), + ) diff --git a/src/uproot/__init__.py b/src/uproot/__init__.py index 91fa76be3..d3e8395de 100644 --- a/src/uproot/__init__.py +++ b/src/uproot/__init__.py @@ -170,6 +170,8 @@ from uproot.interpretation.objects import AsObjects from uproot.interpretation.objects import AsStridedObjects from uproot.interpretation.grouped import AsGrouped +from uproot.interpretation.custom import CustomInterpretation +from uproot.interpretation.identify import register_interpretation from uproot.containers import AsString from uproot.containers import AsPointer from uproot.containers import AsArray diff --git a/src/uproot/interpretation/custom.py b/src/uproot/interpretation/custom.py new file mode 100644 index 000000000..b158c9542 --- /dev/null +++ b/src/uproot/interpretation/custom.py @@ -0,0 +1,173 @@ +# BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE + +""" +This module defines an :doc:`uproot.interpretation.Interpretation` as +a base class for all user-defined custom interpretations, and provides +a :doc:`uproot.interpretation.Interpretation` for extracting binary data +from ``TBasket`` objects. +""" +from __future__ import annotations + +import numpy + +import uproot +import uproot.behaviors.TBranch +import uproot.extras +import uproot.interpretation + +awkward = uproot.extras.awkward() + + +class CustomInterpretation(uproot.interpretation.Interpretation): + def __init__( + self, + branch: uproot.behaviors.TBranch.TBranch, + context: dict, + simplify: bool, + ): + """ + Args: + branch (:doc:`uproot.behaviors.TBranch.TBranch`): The ``TBranch`` to + interpret as an array. + context (dict): Auxiliary data used in deserialization. + simplify (bool): If True, call + :ref:`uproot.interpretation.objects.AsObjects.simplify` on any + :doc:`uproot.interpretation.objects.AsObjects` to try to get a + more efficient interpretation. + + Accept arguments from `uproot.interpretation.identify.interpretation_of`. + """ + self._branch = branch + self._context = context + self._simplify = simplify + + def match_branch( + self, + branch: uproot.behaviors.TBranch.TBranch, + context: dict, + simplify: bool, + ) -> bool: + """ + Args: + branch (:doc:`uproot.behaviors.TBranch.TBranch`): The ``TBranch`` to + interpret as an array. + context (dict): Auxiliary data used in deserialization. + simplify (bool): If True, call + :ref:`uproot.interpretation.objects.AsObjects.simplify` on any + :doc:`uproot.interpretation.objects.AsObjects` to try to get a + more efficient interpretation. + + Accept arguments from `uproot.interpretation.identify.interpretation_of`, + determine whether this interpretation can be applied to the given branch. + """ + raise NotImplementedError + + @property + def typename(self) -> str: + """ + The name of the type of the interpretation. + """ + return self._branch.streamer.typename + + @property + def cache_key(self) -> str: + """ + The cache key of the interpretation. + """ + return id(self) + + def __repr__(self) -> str: + """ + The string representation of the interpretation. + """ + return self.__class__.__name__ + + def final_array( + self, + basket_arrays, + entry_start, + entry_stop, + entry_offsets, + library, + branch, + options, + ): + """ + Concatenate the arrays from the baskets and return the final array. + """ + + awkward = uproot.extras.awkward() + + basket_entry_starts = numpy.array(entry_offsets[:-1]) + basket_entry_stops = numpy.array(entry_offsets[1:]) + + basket_start_idx = numpy.where(basket_entry_starts <= entry_start)[0].max() + basket_end_idx = numpy.where(basket_entry_stops >= entry_stop)[0].min() + + arr_to_concat = [ + basket_arrays[i] for i in range(basket_start_idx, basket_end_idx + 1) + ] + tot_array = awkward.concatenate(arr_to_concat) + + relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] + relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] + + return tot_array[relative_entry_start:relative_entry_stop] + + +class AsBinary(uproot.interpretation.Interpretation): + """ + Return binary data of the ``TBasket``. Pass an instance of this class + to :ref:`uproot.behaviors.TBranch.TBranch.array` like this: + + .. code-block:: python + binary_data = branch.array(interpretation=AsBinary()) + + """ + + @property + def cache_key(self) -> str: + return id(self) + + def basket_array( + self, + data, + byte_offsets, + basket, + branch, + context, + cursor_offset, + library, + interp_options, + ): + counts = byte_offsets[1:] - byte_offsets[:-1] + awkward = uproot.extras.awkward() + return awkward.unflatten(data, counts) + + def final_array( + self, + basket_arrays, + entry_start, + entry_stop, + entry_offsets, + library, + branch, + options, + ): + basket_entry_starts = numpy.array(entry_offsets[:-1]) + basket_entry_stops = numpy.array(entry_offsets[1:]) + + basket_start_idx = numpy.where(basket_entry_starts <= entry_start)[0].max() + basket_end_idx = numpy.where(basket_entry_stops >= entry_stop)[0].min() + + arr_to_concat = [ + basket_arrays[i] for i in range(basket_start_idx, basket_end_idx + 1) + ] + + awkward = uproot.extras.awkward() + tot_array = awkward.concatenate(arr_to_concat) + + relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] + relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] + + return tot_array[relative_entry_start:relative_entry_stop] diff --git a/src/uproot/interpretation/identify.py b/src/uproot/interpretation/identify.py index 20c3c5b92..7dc619e2e 100644 --- a/src/uproot/interpretation/identify.py +++ b/src/uproot/interpretation/identify.py @@ -16,11 +16,30 @@ import ast import numbers import re +import warnings import numpy import uproot +_custom_interpretations = {} + + +def register_interpretation(cls): + """ + Register a custom interpretation class. + + Args: + cls (type): A subclass of :doc:`uproot.interpretation.custom.CustomInterpretation`. + + This method registers a custom interpretation class to be used in + :doc:`uproot.interpretation.identify.interpretation_of`. + """ + key = cls.__name__ + if key in _custom_interpretations: + warnings.warn(f"Overwriting existing custom interpretation{key}", stacklevel=2) + _custom_interpretations[key] = cls + def _normalize_ftype(fType): if fType is not None and uproot.const.kOffsetL < fType < uproot.const.kOffsetP: @@ -312,6 +331,21 @@ def interpretation_of(branch, context, simplify=True): If no interpretation can be found, it raises :doc:`uproot.interpretation.identify.UnknownInterpretation`. """ + # Match custom interpretations + matched_custom_interpretations = [ + cls + for cls in _custom_interpretations.values() + if cls.match_branch(branch, context, simplify) + ] + + assert ( + len(matched_custom_interpretations) <= 1 + ), "Multiple custom interpretations matched, uproot cannot determine which one to use!" + + if len(matched_custom_interpretations) == 1: + return matched_custom_interpretations[0](branch, context, simplify) + + # Original interpretation logic if len(branch.branches) != 0: if branch.top_level and branch.has_member("fClassName"): typename = str(branch.member("fClassName")) From a46f3cd90c20cc4f7b613a834857488375a93543 Mon Sep 17 00:00:00 2001 From: mrli Date: Sun, 15 Jun 2025 17:48:35 +0800 Subject: [PATCH 02/13] fix: wrong method in bes3 example of custom-interpretation --- dev/custom-interpretation/bes3/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/custom-interpretation/bes3/__init__.py b/dev/custom-interpretation/bes3/__init__.py index 296e46a5b..fb8679735 100644 --- a/dev/custom-interpretation/bes3/__init__.py +++ b/dev/custom-interpretation/bes3/__init__.py @@ -5,7 +5,7 @@ AsCustom, BaseReader, ReaderType, - gen_reader_config, + gen_tree_config, get_reader_instance, readers, reconstruct_array, @@ -58,7 +58,7 @@ class Bes3TObjArrayReader(BaseReader): @classmethod - def gen_reader_config( + def gen_tree_config( cls, top_type_name: str, cls_streamer_info: dict, @@ -85,7 +85,7 @@ def gen_reader_config( sub_reader_config = [] for s in all_streamer_info[obj_typename]: sub_reader_config.append( - gen_reader_config(s, all_streamer_info, item_path + f".{obj_typename}") + gen_tree_config(s, all_streamer_info, item_path + f".{obj_typename}") ) return { From 6d17a36d532c28be067f68193b1b4dad2bcefffb Mon Sep 17 00:00:00 2001 From: mrli Date: Wed, 30 Jul 2025 23:35:17 +0800 Subject: [PATCH 03/13] remove the draft of mechanism of reading custom classes --- .../AsCustom/__init__.py | 988 ------------------ .../AsCustom/should_be_cpp.py | 427 -------- dev/custom-interpretation/bes3/__init__.py | 133 --- .../bes3/should_be_cpp.py | 93 -- dev/custom-interpretation/example.py | 14 - .../tiny_reader/__init__.py | 93 -- .../tiny_reader/gen-data/CMakeLists.txt | 32 - .../tiny_reader/gen-data/example.root | Bin 57712 -> 0 bytes .../tiny_reader/gen-data/include/LinkDef.h | 10 - .../tiny_reader/gen-data/include/TMyObject.hh | 20 - .../gen-data/include/TMySubObject.hh | 57 - .../tiny_reader/gen-data/src/TMyObject.cc | 10 - .../tiny_reader/gen-data/src/TMySubObject.cc | 64 -- .../tiny_reader/gen-data/src/main.cc | 21 - .../tiny_reader/should_be_cpp.py | 48 - 15 files changed, 2010 deletions(-) delete mode 100644 dev/custom-interpretation/AsCustom/__init__.py delete mode 100644 dev/custom-interpretation/AsCustom/should_be_cpp.py delete mode 100644 dev/custom-interpretation/bes3/__init__.py delete mode 100644 dev/custom-interpretation/bes3/should_be_cpp.py delete mode 100644 dev/custom-interpretation/example.py delete mode 100644 dev/custom-interpretation/tiny_reader/__init__.py delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/example.root delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc delete mode 100644 dev/custom-interpretation/tiny_reader/gen-data/src/main.cc delete mode 100644 dev/custom-interpretation/tiny_reader/should_be_cpp.py diff --git a/dev/custom-interpretation/AsCustom/__init__.py b/dev/custom-interpretation/AsCustom/__init__.py deleted file mode 100644 index e530aac37..000000000 --- a/dev/custom-interpretation/AsCustom/__init__.py +++ /dev/null @@ -1,988 +0,0 @@ -from __future__ import annotations - -import re -from enum import Enum -from typing import Literal - -import awkward as ak -import numpy as np - -import uproot - -from .should_be_cpp import ( - Cpp_BaseObjectReader, - Cpp_BinaryParser, - Cpp_CArrayReader, - Cpp_CtypeReader, - Cpp_EmptyReader, - Cpp_ObjectHeaderReader, - Cpp_STLMapReader, - Cpp_STLSequenceReader, - Cpp_STLStringReader, - Cpp_TArrayReader, - Cpp_TObjectReader, - Cpp_TStringReader, -) - -type_np2array = { - "u1": "B", - "u2": "H", - "u4": "I", - "u8": "Q", - "i1": "b", - "i2": "h", - "i4": "i", - "i8": "q", - "f": "f", - "d": "d", -} - -num_typenames = { - "bool": "i1", - "char": "i1", - "short": "i2", - "int": "i4", - "long": "i8", - "unsigned char": "u1", - "unsigned short": "u2", - "unsigned int": "u4", - "unsigned long": "u8", - "float": "f", - "double": "d", - # cstdint - "int8_t": "i1", - "int16_t": "i2", - "int32_t": "i4", - "int64_t": "i8", - "uint8_t": "u1", - "uint16_t": "u2", - "uint32_t": "u4", - "uint64_t": "u8", - # ROOT types - "Bool_t": "i1", - "Char_t": "i1", - "Short_t": "i2", - "Int_t": "i4", - "Long_t": "i8", - "UChar_t": "u1", - "UShort_t": "u2", - "UInt_t": "u4", - "ULong_t": "u8", - "Float_t": "f", - "Double_t": "d", -} - -stl_typenames = { - "vector", - "array", - "map", - "unordered_map", - "string", -} - - -tarray_typenames = { - "TArrayC": "i1", - "TArrayS": "i2", - "TArrayI": "i4", - "TArrayL": "i8", - "TArrayF": "f", - "TArrayD": "d", -} - - -ctype_hints = Literal["bool", "i1", "i2", "i4", "i8", "u1", "u2", "u4", "u8", "f", "d"] - - -class ReaderType(Enum): - CType = "CType" - STLSequence = "STLSequence" - STLMap = "STLMap" - STLString = "STLString" - TArray = "TArray" - TString = "TString" - TObject = "TObject" - CArray = "CArray" - BaseObject = "BaseObject" - ObjectHeader = "ObjectHeader" - Empty = "Empty" - - -def get_top_type_name(type_name: str) -> str: - if type_name.endswith("*"): - type_name = type_name[:-1].strip() - type_name = type_name.replace("std::", "").strip() - return type_name.split("<")[0] - - -def gen_tree_config( - cls_streamer_info: dict, - all_streamer_info: dict, - item_path: str = "", -) -> dict: - """ - Generate reader configuration for a class streamer information. - - The content it returns should be: - - ```python - { - "reader": ReaderType, - "name": str, - "ctype": str, # for CTypeReader, TArrayReader - "element_reader": dict, # reader config of the element, for STLVectorReader, SimpleCArrayReader, TObjectCArrayReader - "flat_size": int, # for SimpleCArrayReader, TObjectCArrayReader - "fMaxIndex": list[int], # for SimpleCArrayReader, TObjectCArrayReader - "fArrayDim": int, # for SimpleCArrayReader, TObjectCArrayReader - "key_reader": dict, # reader config of the key, for STLMapReader - "val_reader": dict, # reader config of the value, for STLMapReader - "sub_readers": list[dict], # for BaseObjectReader, ObjectHeaderReader - "is_top_level": bool, # for STLVectorReader, STLMapReader, STLStringReader - } - ``` - - Args: - cls_streamer_info (dict): Class streamer information. - all_streamer_info (dict): All streamer information. - item_path (str): Path to the item. - - Returns: - dict: Reader configuration. - """ - fName = cls_streamer_info["fName"] - item_path = fName if item_path == "" else f"{item_path}.{fName}" - - for reader in sorted(readers, key=lambda x: x.priority(), reverse=True): - top_type_name = get_top_type_name(cls_streamer_info["fTypeName"]) - tree_config = reader.gen_tree_config( - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ) - if tree_config is not None: - return tree_config - - raise ValueError(f"Unknown type: {cls_streamer_info['fTypeName']} for {item_path}") - - -def get_reader_instance(tree_config: dict): - for cls_reader in sorted(readers, key=lambda x: x.priority(), reverse=True): - reader = cls_reader.get_reader_instance(tree_config) - if reader is not None: - return reader - - raise ValueError( - f"Unknown reader type: {tree_config['reader']} for {tree_config['name']}" - ) - - -def reconstruct_array( - raw_data: np.ndarray | tuple | list | None, - tree_config: dict, -) -> ak.Array | None: - for reader in sorted(readers, key=lambda x: x.priority(), reverse=True): - data = reader.reconstruct_array(raw_data, tree_config) - if data is not None: - return data - - raise ValueError( - f"Unknown reader type: {tree_config['reader']} for {tree_config['name']}" - ) - - -def gen_tree_config_from_type_name( - type_name: str, - all_streamer_info: dict, - item_path: str = "", -): - return gen_tree_config( - { - "fName": type_name, - "fTypeName": type_name, - }, - all_streamer_info, - item_path, - ) - - -def regularize_object_path(object_path: str) -> str: - return re.sub(r";[0-9]+", r"", object_path) - - -class BaseReader: - """ - Base class for all readers. - """ - - @classmethod - def priority(cls) -> int: - """ - The priority of the reader. Higher priority means the reader will be - used first. - """ - return 20 - - @classmethod - def gen_tree_config( - cls, - top_type_name: str, - cls_streamer_info: dict, - all_streamer_info: dict, - item_path: str = "", - ) -> dict: - raise NotImplementedError("This method should be implemented in subclasses.") - - @classmethod - def get_reader_instance(cls, tree_config: dict): - """ - Args: - tree_config (dict): The configuration dictionary for the reader. - - Returns: - An instance of the appropriate reader class. - """ - raise NotImplementedError("This method should be implemented in subclasses.") - - @classmethod - def reconstruct_array( - cls, - raw_data: np.ndarray | tuple | list | None, - tree_config: dict, - ) -> ak.Array | None: - """ - Args: - raw_data (Union[np.ndarray, tuple, list, None]): The raw data to be - recovered. - tree_config (dict): The configuration dictionary for the reader. - - Returns: - awkward.Array: The recovered data as an awkward array. - """ - raise NotImplementedError("This method should be implemented in subclasses.") - - -readers: set[BaseReader] = set() - - -class CTypeReader(BaseReader): - """ - This class reads C++ primitive types from a binary parser. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name in num_typenames: - ctype = num_typenames[top_type_name] - return { - "reader": ReaderType.CType, - "name": cls_streamer_info["fName"], - "ctype": ctype, - } - else: - return None - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.CType: - return None - - ctype = tree_config["ctype"] - return Cpp_CtypeReader(tree_config["name"], ctype) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.CType: - return None - - return ak.Array(raw_data) - - -class STLSequenceReader(BaseReader): - """ - This class reads STL sequence (vector, array) from a binary parser. - """ - - @staticmethod - def get_sequence_element_typename(type_name: str) -> str: - """ - Get the element type name of a vector type. - - e.g. vector> -> vector - """ - type_name = ( - type_name.replace("std::", "").replace("< ", "<").replace(" >", ">").strip() - ) - return re.match(r"^(vector|array)<(.*)>$", type_name).group(2) - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name not in ["vector", "array"]: - return None - - fName = cls_streamer_info["fName"] - fTypeName = cls_streamer_info["fTypeName"] - element_type = cls.get_sequence_element_typename(fTypeName) - element_info = { - "fName": fName, - "fTypeName": element_type, - } - - element_tree_config = gen_tree_config( - element_info, - all_streamer_info, - item_path, - ) - - top_element_type = get_top_type_name(element_type) - if top_element_type in stl_typenames: - element_tree_config["is_top"] = False - - return { - "reader": ReaderType.STLSequence, - "name": fName, - "element_reader": element_tree_config, - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.STLSequence: - return None - - element_reader = get_reader_instance(tree_config["element_reader"]) - is_top = tree_config.get("is_top", True) - return Cpp_STLSequenceReader(tree_config["name"], is_top, element_reader) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.STLSequence: - return None - - counts, element_raw_data = raw_data - element_data = reconstruct_array( - element_raw_data, - tree_config["element_reader"], - ) - return ak.unflatten(element_data, counts) - - -class STLMapReader(BaseReader): - """ - This class reads std::map from a binary parser. - """ - - @staticmethod - def get_map_key_val_typenames(type_name: str) -> tuple[str, str]: - """ - Get the key and value type names of a map type. - - e.g. map> -> (int, vector) - """ - type_name = ( - type_name.replace("std::", "").replace("< ", "<").replace(" >", ">").strip() - ) - return re.match( - r"^(map|unordered_map|multimap)<(.*),(.*)>$", type_name - ).groups()[1:3] - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name not in ["map", "unordered_map", "multimap"]: - return None - - fTypeName = cls_streamer_info["fTypeName"] - key_type_name, val_type_name = cls.get_map_key_val_typenames(fTypeName) - - fName = cls_streamer_info["fName"] - key_info = { - "fName": "key", - "fTypeName": key_type_name, - } - - val_info = { - "fName": "val", - "fTypeName": val_type_name, - } - - key_tree_config = gen_tree_config(key_info, all_streamer_info, item_path) - if get_top_type_name(key_type_name) in stl_typenames: - key_tree_config["is_top"] = False - - val_tree_config = gen_tree_config(val_info, all_streamer_info, item_path) - if get_top_type_name(val_type_name) in stl_typenames: - val_tree_config["is_top"] = False - - return { - "reader": ReaderType.STLMap, - "name": fName, - "key_reader": key_tree_config, - "val_reader": val_tree_config, - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.STLMap: - return None - - key_cpp_reader = get_reader_instance(tree_config["key_reader"]) - val_cpp_reader = get_reader_instance(tree_config["val_reader"]) - is_top = tree_config.get("is_top", True) - return Cpp_STLMapReader( - tree_config["name"], - is_top, - key_cpp_reader, - val_cpp_reader, - ) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.STLMap: - return None - - key_tree_config = tree_config["key_reader"] - val_tree_config = tree_config["val_reader"] - counts, key_raw_data, val_raw_data = raw_data - key_data = reconstruct_array(key_raw_data, key_tree_config) - val_data = reconstruct_array(val_raw_data, val_tree_config) - - return ak.unflatten( - ak.zip( - { - key_tree_config["name"]: key_data, - val_tree_config["name"]: val_data, - }, - with_name="pair", - ), - counts, - ) - - -class STLStringReader(BaseReader): - """ - This class reads std::string from a binary parser. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name != "string": - return None - - return { - "reader": ReaderType.STLString, - "name": cls_streamer_info["fName"], - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.STLString: - return None - - return Cpp_STLStringReader( - tree_config["name"], - tree_config.get("is_top", True), - ) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.STLString: - return None - - counts, data = raw_data - return ak.enforce_type(ak.unflatten(data, counts), "string") - - -class TArrayReader(BaseReader): - """ - This class reads TArray from a binary paerser. - - TArray includes TArrayC, TArrayS, TArrayI, TArrayL, TArrayF, and TArrayD. - Corresponding ctype is u1, u2, i4, i8, f, and d. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name not in tarray_typenames: - return None - - ctype = tarray_typenames[top_type_name] - return { - "reader": ReaderType.TArray, - "name": cls_streamer_info["fName"], - "ctype": ctype, - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.TArray: - return None - - return Cpp_TArrayReader(tree_config["name"], tree_config["ctype"]) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.TArray: - return None - - counts, data = raw_data - return ak.unflatten(data, counts) - - -class TStringReader(BaseReader): - """ - This class reads TString from a binary parser. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name != "TString": - return None - - return { - "reader": ReaderType.TString, - "name": cls_streamer_info["fName"], - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.TString: - return None - - return Cpp_TStringReader(tree_config["name"]) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.TString: - return None - - counts, data = raw_data - return ak.enforce_type(ak.unflatten(data, counts), "string") - - -class TObjectReader(BaseReader): - """ - This class reads TObject from a binary parser. - - It will not record any data. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name != "BASE": - return None - - fType = cls_streamer_info["fType"] - if fType != 66: - return None - - return { - "reader": ReaderType.TObject, - "name": cls_streamer_info["fName"], - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.TObject: - return None - - return Cpp_TObjectReader(tree_config["name"]) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - return None - - -class CArrayReader(BaseReader): - """ - This class reads a C-array from a binary parser. - """ - - @classmethod - def priority(cls): - return 100 # This reader should be called first - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if cls_streamer_info.get("fArrayDim", 0) == 0: - return None - - fName = cls_streamer_info["fName"] - fTypeName = cls_streamer_info["fTypeName"] - fArrayDim = cls_streamer_info["fArrayDim"] - fMaxIndex = cls_streamer_info["fMaxIndex"] - - element_streamer_info = cls_streamer_info.copy() - element_streamer_info["fArrayDim"] = 0 - - element_tree_config = gen_tree_config( - element_streamer_info, - all_streamer_info, - ) - - flat_size = np.prod(fMaxIndex[:fArrayDim]) - assert ( - flat_size > 0 - ), f"flatten_size should be greater than 0, but got {flat_size}" - - # c-type number or TArray - if top_type_name in num_typenames or top_type_name in tarray_typenames: - return { - "reader": ReaderType.CArray, - "name": fName, - "is_obj": False, - "element_reader": element_tree_config, - "flat_size": flat_size, - "fMaxIndex": fMaxIndex, - "fArrayDim": fArrayDim, - } - - # TSTring - elif top_type_name == "TString": - return { - "reader": ReaderType.CArray, - "name": fName, - "is_obj": True, - "element_reader": element_tree_config, - "flat_size": flat_size, - "fMaxIndex": fMaxIndex, - "fArrayDim": fArrayDim, - } - - # STL - elif top_type_name in stl_typenames: - element_tree_config["is_top"] = False - return { - "reader": ReaderType.CArray, - "name": fName, - "is_obj": True, - "flat_size": flat_size, - "element_reader": element_tree_config, - "fMaxIndex": fMaxIndex, - "fArrayDim": fArrayDim, - } - - else: - raise ValueError(f"Unknown type: {top_type_name} for C-array: {fTypeName}") - - @classmethod - def get_reader_instance(cls, tree_config: dict): - reader_type = tree_config["reader"] - if reader_type != ReaderType.CArray: - return None - - element_reader = get_reader_instance(tree_config["element_reader"]) - - return Cpp_CArrayReader( - tree_config["name"], - tree_config["is_obj"], - tree_config["flat_size"], - element_reader, - ) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.CArray: - return None - - element_tree_config = tree_config["element_reader"] - fMaxIndex = tree_config["fMaxIndex"] - fArrayDim = tree_config["fArrayDim"] - shape = [fMaxIndex[i] for i in range(fArrayDim)] - - element_data = reconstruct_array( - raw_data, - element_tree_config, - ) - - for s in shape[::-1]: - element_data = ak.unflatten(element_data, s) - - return element_data - - -class BaseObjectReader(BaseReader): - """ - Base class is what a custom class inherits from. - It has fNBytes(uint32), fVersion(uint16) at the beginning. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - if top_type_name != "BASE": - return None - - fType = cls_streamer_info["fType"] - if fType != 0: - return None - - fName = cls_streamer_info["fName"] - sub_streamers: list = all_streamer_info[fName] - - sub_tree_configs = [ - gen_tree_config(s, all_streamer_info, item_path) for s in sub_streamers - ] - - return { - "reader": ReaderType.BaseObject, - "name": fName, - "sub_readers": sub_tree_configs, - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.BaseObject: - return None - - sub_readers = [get_reader_instance(s) for s in tree_config["sub_readers"]] - return Cpp_BaseObjectReader(tree_config["name"], sub_readers) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.BaseObject: - return None - - sub_tree_configs = tree_config["sub_readers"] - - arr_dict = {} - for s_cfg, s_data in zip(sub_tree_configs, raw_data): - s_name = s_cfg["name"] - s_reader_type = s_cfg["reader"] - - if s_reader_type == ReaderType.TObject: - continue - - arr_dict[s_name] = reconstruct_array(s_data, s_cfg) - - return ak.Array(arr_dict) - - -class ObjectHeaderReader(BaseReader): - """ - This class read an object starting with an object header. - """ - - @classmethod - def priority(cls): - return 0 # should be called last - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - sub_streamers: list = all_streamer_info[top_type_name] - sub_tree_configs = [ - gen_tree_config(s, all_streamer_info, item_path) for s in sub_streamers - ] - return { - "reader": ReaderType.ObjectHeader, - "name": top_type_name, - "sub_readers": sub_tree_configs, - } - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.ObjectHeader: - return None - - sub_readers = [get_reader_instance(s) for s in tree_config["sub_readers"]] - return Cpp_ObjectHeaderReader(tree_config["name"], sub_readers) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.ObjectHeader: - return None - - sub_tree_configs = tree_config["sub_readers"] - - arr_dict = {} - for s_cfg, s_data in zip(sub_tree_configs, raw_data): - s_name = s_cfg["name"] - s_reader_type = s_cfg["reader"] - - if s_reader_type == ReaderType.TObject: - continue - - arr_dict[s_name] = reconstruct_array(s_data, s_cfg) - - return ak.Array(arr_dict) - - -class EmptyReader(BaseReader): - """ - This class does nothing. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name, - cls_streamer_info, - all_streamer_info, - item_path, - ): - return None - - @classmethod - def get_reader_instance(cls, tree_config: dict): - if tree_config["reader"] != ReaderType.Empty: - return None - - return Cpp_EmptyReader(tree_config["name"]) - - @classmethod - def reconstruct_array(cls, raw_data, tree_config): - if tree_config["reader"] != ReaderType.Empty: - return None - - return np.empty(shape=(0,)) - - -readers |= { - CTypeReader, - STLSequenceReader, - STLMapReader, - STLStringReader, - TArrayReader, - TStringReader, - TObjectReader, - CArrayReader, - BaseObjectReader, - ObjectHeaderReader, - EmptyReader, -} - - -class AsCustom(uproot.CustomInterpretation): - target_branches: set[str] = set() - - def __init__(self, branch, context, simplify): - super().__init__(branch, context, simplify) - - # simplify streamer information - self.all_streamer_info: dict[str, list[dict]] = {} - for k, v in branch.file.streamers.items(): - cur_infos = [ - i.all_members for i in next(iter(v.values())).member("fElements") - ] - self.all_streamer_info[k] = cur_infos - - @classmethod - def match_branch( - cls, - branch: uproot.behaviors.TBranch.TBranch, - context: dict, - simplify: bool, - ) -> bool: - """ - Args: - branch (:doc:`uproot.behaviors.TBranch.TBranch`): The ``TBranch`` to - interpret as an array. - context (dict): Auxiliary data used in deserialization. - simplify (bool): If True, call - :ref:`uproot.interpretation.objects.AsObjects.simplify` on any - :doc:`uproot.interpretation.objects.AsObjects` to try to get a - more efficient interpretation. - - Accept arguments from `uproot.interpretation.identify.interpretation_of`, - determine whether this interpretation can be applied to the given branch. - """ - full_path = regularize_object_path(branch.object_path) - return full_path in cls.target_branches - - def __repr__(self) -> str: - """ - The string representation of the interpretation. - """ - return f"AsCustom({self.typename})" - - def basket_array( - self, - data, - byte_offsets, - basket, - branch, - context, - cursor_offset, - library, - interp_options, - ): - assert library.name == "ak", "Only awkward arrays are supported" - - full_branch_path = regularize_object_path(branch.object_path) - - # generate reader config - tree_config = gen_tree_config_from_type_name( - branch.streamer.typename, self.all_streamer_info, full_branch_path - ) - - # get reader - reader = get_reader_instance(tree_config) - - # read data - parser = Cpp_BinaryParser(data, byte_offsets) - for _ in range(parser.n_entries): - reader.read(parser) - - # recover raw data and return - raw_data = reader.get_data() - return reconstruct_array(raw_data, tree_config) - - -uproot.register_interpretation(AsCustom) diff --git a/dev/custom-interpretation/AsCustom/should_be_cpp.py b/dev/custom-interpretation/AsCustom/should_be_cpp.py deleted file mode 100644 index aec4e7e94..000000000 --- a/dev/custom-interpretation/AsCustom/should_be_cpp.py +++ /dev/null @@ -1,427 +0,0 @@ -from __future__ import annotations - -from array import array -from typing import Literal - -import numpy as np - -ctype_hints = Literal["i1", "i2", "i4", "i8", "u1", "u2", "u4", "u8", "f", "d"] -type_np2array = { - "u1": "B", - "u2": "H", - "u4": "I", - "u8": "Q", - "i1": "b", - "i2": "h", - "i4": "i", - "i8": "q", - "f": "f", - "d": "d", -} - - -class Cpp_BinaryParser: - nbytes_dict = { - "u1": 1, - "u2": 2, - "u4": 4, - "u8": 8, - "i1": 1, - "i2": 2, - "i4": 4, - "i8": 8, - "f": 4, - "d": 8, - } - - def __init__( - self, - data: np.ndarray[np.uint8], - offsets: np.ndarray, - ): - """ - Args: - data (np.ndarray): The binary data to parse. - """ - self.data = data - self.offsets = offsets - self.cursor = 0 - - @property - def n_entries(self) -> int: - """ - Returns: - The number of entries in the binary data. - """ - return len(self.offsets) - 1 - - def read_number( - self, - ctype: Literal["u1", "u2", "u4", "u8", "i1", "i2", "i4", "i8", "f", "d"], - ) -> np.number: - nbytes = self.nbytes_dict[ctype] - value = self.data[self.cursor : self.cursor + nbytes].view(f">{ctype}")[0] - self.cursor += nbytes - return value - - def read_fNBytes(self) -> np.uint32: - nbytes = self.read_number("u4") - assert nbytes & 0x40000000 != 0, f"Invalid fNBytes: {nbytes:#x}" - return nbytes & ~np.uint32(0x40000000) - - def read_fVersion(self) -> np.uint16: - return self.read_number("u2") - - def read_null_terminated_string(self) -> str: - """ - Reads a null-terminated string from the binary data. - - Returns: - The null-terminated string. - """ - start = self.cursor - while self.data[self.cursor] != 0: - self.cursor += 1 - end = self.cursor - self.cursor += 1 - - return self.data[start:end].tobytes().decode("utf-8") - - def __repr__(self): - cur_data_str = str(self.data[self.cursor :]) - return f"BinaryParser({cur_data_str})" - - -class Cpp_BaseReader: - def read(self, parser: Cpp_BinaryParser) -> None: - raise AssertionError - - def get_data(self) -> np.ndarray | tuple: - """ - Returns: - The data read by the reader. - """ - raise AssertionError - - -class Cpp_CtypeReader(Cpp_BaseReader): - """ - This class reads C++ primitive types from a binary parser. - """ - - def __init__( - self, - name: str, - ctype: ctype_hints, - ): - self.name = name - self.ctype = ctype - self.data = array(type_np2array[ctype]) - - def read(self, parser: Cpp_BinaryParser): - self.data.append(parser.read_number(self.ctype)) - - def get_data(self): - return np.array(self.data, dtype=self.ctype, copy=True) - - -class Cpp_STLSequenceReader(Cpp_BaseReader): - """ - This class reads STL sequence (vector, array) from a binary parser. - """ - - def __init__(self, name: str, is_top: bool, element_reader: Cpp_BaseReader): - self.name = name - self.element_reader = element_reader - self.counts = array("Q") - self.is_top = is_top - - def read(self, parser): - # Read fNBytes and fVersion when it is top level - if self.is_top: - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - - # Read data - fSize = parser.read_number("u4") - for _ in range(fSize): - self.element_reader.read(parser) - - # Update counts - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - self.element_reader.get_data(), - ) - - -class Cpp_STLMapReader(Cpp_BaseReader): - """ - This class reads std::map, unordered_map, multimap from a binary parser. - """ - - def __init__( - self, - name: str, - is_top: bool, - key_reader: Cpp_BaseReader, - val_reader: Cpp_BaseReader, - ): - self.name = name - self.key_reader = key_reader - self.val_reader = val_reader - self.counts = array("Q") - self.is_top = is_top - - def read(self, parser): - # Read fNBytes and fVersion when it is top level - if self.is_top: - _ = parser.read_fNBytes() - _ = parser.read_number("u8") # I don't know what this is - - # Read data - fSize = parser.read_number("u4") - - if self.is_top: - for _ in range(fSize): - self.key_reader.read(parser) - for _ in range(fSize): - self.val_reader.read(parser) - else: - for _ in range(fSize): - self.key_reader.read(parser) - self.val_reader.read(parser) - - # Update counts - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - self.key_reader.get_data(), - self.val_reader.get_data(), - ) - - -class Cpp_STLStringReader(Cpp_BaseReader): - """ - This class reads std::string from a binary parser. - """ - - def __init__(self, name: str, is_top: bool): - self.name = name - self.data = array("B") - self.counts = array("Q") - self.is_top = is_top - - def read(self, parser): - # Read fNBytes and fVersion when it is top level - if self.is_top: - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - - # Get length of std::string - fSize = parser.read_number("u1") - if fSize == 255: - fSize = parser.read_number("u4") - - # Read data - for _ in range(fSize): - self.data.append(parser.read_number("u1")) - - # Update counts - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - np.asarray(self.data, dtype="B"), - ) - - -class Cpp_TArrayReader(Cpp_BaseReader): - """ - This class reads TArray from a binary paerser. - - TArray includes TArrayC, TArrayS, TArrayI, TArrayL, TArrayF, and TArrayD. - Corresponding ctype is u1, u2, i4, i8, f, and d. - """ - - def __init__( - self, - name: str, - ctype: Literal["i1", "i2", "i4", "i8", "f", "d"], - ): - self.name = name - self.ctype = ctype - self.data = array(type_np2array[ctype]) - self.counts = array("Q") - - def read(self, parser): - # Read data - fSize = parser.read_number("u4") - for _ in range(fSize): - self.data.append(parser.read_number(self.ctype)) - - # Update counts - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - np.asarray(self.data, dtype=self.ctype), - ) - - -class Cpp_TStringReader(Cpp_BaseReader): - """ - This class reads TString from a binary parser. - """ - - def __init__(self, name: str): - self.name = name - self.data = array("B") - self.counts = array("Q") - - def read(self, parser): - # Get length of TString - fSize = parser.read_number("u1") - if fSize == 255: - fSize = parser.read_number("u4") - - # Read data - for _ in range(fSize): - self.data.append(parser.read_number("u1")) - - # Update counts - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - np.asarray(self.data, dtype="B"), - ) - - -class Cpp_TObjectReader(Cpp_BaseReader): - """ - This class reads TObject from a binary parser. - - It will not record any data. - """ - - def __init__(self, name: str): - self.name = name - - def read(self, parser): - _ = parser.read_fVersion() # fVersion - _ = parser.read_number("u4") # fUniqueID - _ = parser.read_number("u4") # fBits - - def get_data(self): - return None # should I return anything? - - -class Cpp_CArrayReader(Cpp_BaseReader): - """ - This class reads a C-array from a binary parser. - """ - - def __init__( - self, - name: str, - is_obj: bool, - flat_size: int, - element_reader: Cpp_BaseReader, - ): - self.name = name - self.is_obj = is_obj - self.flat_size = flat_size - self.element_reader = element_reader - - def read(self, parser): - if self.is_obj: - # Read fNBytes and fVersion - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - - # Read data - for _ in range(self.flat_size): - self.element_reader.read(parser) - - def get_data(self): - return self.element_reader.get_data() - - -class Cpp_BaseObjectReader(Cpp_BaseReader): - """ - Base class is what a custom class inherits from. - It has fNBytes(uint32), fVersion(uint16) at the beginning. - """ - - def __init__(self, name: str, sub_readers: list[Cpp_BaseReader]): - self.name = name - self.sub_readers = sub_readers - - def read(self, parser): - # Read fNBytes and fVersion - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - - # Read data - for sub_reader in self.sub_readers: - sub_reader.read(parser) - - def get_data(self): - data = [] - for sub_reader in self.sub_readers: - data.append(sub_reader.get_data()) - return data - - -class Cpp_ObjectHeaderReader(Cpp_BaseReader): - """ - This class read an object starting with an object header. - """ - - def __init__(self, name: str, sub_readers: list[Cpp_BaseReader]): - """ - Args: - sub_readers (list[BaseReader]): The readers for the elements in the object. - """ - self.name = name - self.sub_readers = sub_readers - - def read(self, parser): - # read object header - _ = parser.read_fNBytes() - fTag = parser.read_number("i4") - _ = parser.read_null_terminated_string() if fTag == -1 else "" # fClassName - - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - for sub_reader in self.sub_readers: - sub_reader.read(parser) - - def get_data(self): - data = [] - for sub_reader in self.sub_readers: - data.append(sub_reader.get_data()) - return data - - -class Cpp_EmptyReader(Cpp_BaseReader): - """ - This class does nothing. - """ - - def __init__(self, name: str): - self.name = name - - def read(self, parser): - pass - - def get_data(self): - return None diff --git a/dev/custom-interpretation/bes3/__init__.py b/dev/custom-interpretation/bes3/__init__.py deleted file mode 100644 index fb8679735..000000000 --- a/dev/custom-interpretation/bes3/__init__.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import annotations - -import awkward as ak -from AsCustom import ( - AsCustom, - BaseReader, - ReaderType, - gen_tree_config, - get_reader_instance, - readers, - reconstruct_array, -) - -from .should_be_cpp import Cpp_Bes3TObjArrayReader - -bes3_branch2types = { - "/Event:TMcEvent/m_mdcMcHitCol": "TMdcMc", - "/Event:TMcEvent/m_cgemMcHitCol": "TCgemMc", - "/Event:TMcEvent/m_emcMcHitCol": "TEmcMc", - "/Event:TMcEvent/m_tofMcHitCol": "TTofMc", - "/Event:TMcEvent/m_mucMcHitCol": "TMucMc", - "/Event:TMcEvent/m_mcParticleCol": "TMcParticle", - "/Event:TDigiEvent/m_mdcDigiCol": "TMdcDigi", - "/Event:TDigiEvent/m_cgemDigiCol": "TCgemDigi", - "/Event:TDigiEvent/m_emcDigiCol": "TEmcDigi", - "/Event:TDigiEvent/m_tofDigiCol": "TTofDigi", - "/Event:TDigiEvent/m_mucDigiCol": "TMucDigi", - "/Event:TDigiEvent/m_lumiDigiCol": "TLumiDigi", - "/Event:TDstEvent/m_mdcTrackCol": "TMdcTrack", - "/Event:TDstEvent/m_emcTrackCol": "TEmcTrack", - "/Event:TDstEvent/m_tofTrackCol": "TTofTrack", - "/Event:TDstEvent/m_mucTrackCol": "TMucTrack", - "/Event:TDstEvent/m_mdcDedxCol": "TMdcDedx", - "/Event:TDstEvent/m_extTrackCol": "TExtTrack", - "/Event:TDstEvent/m_mdcKalTrackCol": "TMdcKalTrack", - "/Event:TRecEvent/m_recMdcTrackCol": "TRecMdcTrack", - "/Event:TRecEvent/m_recMdcHitCol": "TRecMdcHit", - "/Event:TRecEvent/m_recEmcHitCol": "TRecEmcHit", - "/Event:TRecEvent/m_recEmcClusterCol": "TRecEmcCluster", - "/Event:TRecEvent/m_recEmcShowerCol": "TRecEmcShower", - "/Event:TRecEvent/m_recTofTrackCol": "TRecTofTrack", - "/Event:TRecEvent/m_recMucTrackCol": "TRecMucTrack", - "/Event:TRecEvent/m_recMdcDedxCol": "TRecMdcDedx", - "/Event:TRecEvent/m_recMdcDedxHitCol": "TRecMdcDedxHit", - "/Event:TRecEvent/m_recExtTrackCol": "TRecExtTrack", - "/Event:TRecEvent/m_recMdcKalTrackCol": "TRecMdcKalTrack", - "/Event:TRecEvent/m_recMdcKalHelixSegCol": "TRecMdcKalHelixSeg", - "/Event:TRecEvent/m_recEvTimeCol": "TRecEvTime", - "/Event:TRecEvent/m_recZddChannelCol": "TRecZddChannel", - "/Event:TEvtRecObject/m_evtRecTrackCol": "TEvtRecTrack", - "/Event:TEvtRecObject/m_evtRecVeeVertexCol": "TEvtRecVeeVertex", - "/Event:TEvtRecObject/m_evtRecPi0Col": "TEvtRecPi0", - "/Event:TEvtRecObject/m_evtRecEtaToGGCol": "TEvtRecEtaToGG", - "/Event:TEvtRecObject/m_evtRecDTagCol": "TEvtRecDTag", - "/Event:THltEvent/m_hltRawCol": "THltRaw", -} - - -class Bes3TObjArrayReader(BaseReader): - @classmethod - def gen_tree_config( - cls, - top_type_name: str, - cls_streamer_info: dict, - all_streamer_info: dict, - item_path: str = "", - ): - if top_type_name != "TObjArray": - return None - - obj_typename = bes3_branch2types.get(item_path.replace(".TObjArray*", "")) - if obj_typename is None: - return None - - if obj_typename not in all_streamer_info: - return { - "reader": "MyTObjArrayReader", - "name": cls_streamer_info["fName"], - "element_reader": { - "reader": ReaderType.Empty, - "name": obj_typename, - }, - } - - sub_reader_config = [] - for s in all_streamer_info[obj_typename]: - sub_reader_config.append( - gen_tree_config(s, all_streamer_info, item_path + f".{obj_typename}") - ) - - return { - "reader": "MyTObjArrayReader", - "name": cls_streamer_info["fName"], - "element_reader": { - "reader": ReaderType.ObjectHeader, - "name": obj_typename, - "sub_readers": sub_reader_config, - }, - } - - @staticmethod - def get_reader_instance(reader_config: dict): - if reader_config["reader"] != "MyTObjArrayReader": - return None - - element_reader_config = reader_config["element_reader"] - element_reader = get_reader_instance(element_reader_config) - - return Cpp_Bes3TObjArrayReader(reader_config["name"], element_reader) - - @staticmethod - def reconstruct_array(raw_data, reader_config: dict): - if reader_config["reader"] != "MyTObjArrayReader": - return None - - counts, element_raw_data = raw_data - element_reader_config = reader_config["element_reader"] - element_data = reconstruct_array( - element_raw_data, - element_reader_config, - ) - - return ak.unflatten(element_data, counts) - - -def register(): - readers.add(Bes3TObjArrayReader) - AsCustom.target_branches |= set(bes3_branch2types.keys()) | { - "/Event:EventNavigator/m_mcMdcMcHits", - "/Event:EventNavigator/m_mcMdcTracks", - "/Event:EventNavigator/m_mcEmcMcHits", - "/Event:EventNavigator/m_mcEmcRecShowers", - } diff --git a/dev/custom-interpretation/bes3/should_be_cpp.py b/dev/custom-interpretation/bes3/should_be_cpp.py deleted file mode 100644 index 83fa03959..000000000 --- a/dev/custom-interpretation/bes3/should_be_cpp.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -from array import array - -import numpy as np -from AsCustom import ( - ObjectHeaderReader, -) -from AsCustom.should_be_cpp import Cpp_BaseReader - -bes3_branch2types = { - "/Event:TMcEvent/m_mdcMcHitCol": "TMdcMc", - "/Event:TMcEvent/m_cgemMcHitCol": "TCgemMc", - "/Event:TMcEvent/m_emcMcHitCol": "TEmcMc", - "/Event:TMcEvent/m_tofMcHitCol": "TTofMc", - "/Event:TMcEvent/m_mucMcHitCol": "TMucMc", - "/Event:TMcEvent/m_mcParticleCol": "TMcParticle", - "/Event:TDigiEvent/m_mdcDigiCol": "TMdcDigi", - "/Event:TDigiEvent/m_cgemDigiCol": "TCgemDigi", - "/Event:TDigiEvent/m_emcDigiCol": "TEmcDigi", - "/Event:TDigiEvent/m_tofDigiCol": "TTofDigi", - "/Event:TDigiEvent/m_mucDigiCol": "TMucDigi", - "/Event:TDigiEvent/m_lumiDigiCol": "TLumiDigi", - "/Event:TDstEvent/m_mdcTrackCol": "TMdcTrack", - "/Event:TDstEvent/m_emcTrackCol": "TEmcTrack", - "/Event:TDstEvent/m_tofTrackCol": "TTofTrack", - "/Event:TDstEvent/m_mucTrackCol": "TMucTrack", - "/Event:TDstEvent/m_mdcDedxCol": "TMdcDedx", - "/Event:TDstEvent/m_extTrackCol": "TExtTrack", - "/Event:TDstEvent/m_mdcKalTrackCol": "TMdcKalTrack", - "/Event:TRecEvent/m_recMdcTrackCol": "TRecMdcTrack", - "/Event:TRecEvent/m_recMdcHitCol": "TRecMdcHit", - "/Event:TRecEvent/m_recEmcHitCol": "TRecEmcHit", - "/Event:TRecEvent/m_recEmcClusterCol": "TRecEmcCluster", - "/Event:TRecEvent/m_recEmcShowerCol": "TRecEmcShower", - "/Event:TRecEvent/m_recTofTrackCol": "TRecTofTrack", - "/Event:TRecEvent/m_recMucTrackCol": "TRecMucTrack", - "/Event:TRecEvent/m_recMdcDedxCol": "TRecMdcDedx", - "/Event:TRecEvent/m_recMdcDedxHitCol": "TRecMdcDedxHit", - "/Event:TRecEvent/m_recExtTrackCol": "TRecExtTrack", - "/Event:TRecEvent/m_recMdcKalTrackCol": "TRecMdcKalTrack", - "/Event:TRecEvent/m_recMdcKalHelixSegCol": "TRecMdcKalHelixSeg", - "/Event:TRecEvent/m_recEvTimeCol": "TRecEvTime", - "/Event:TRecEvent/m_recZddChannelCol": "TRecZddChannel", - "/Event:TEvtRecObject/m_evtRecTrackCol": "TEvtRecTrack", - "/Event:TEvtRecObject/m_evtRecVeeVertexCol": "TEvtRecVeeVertex", - "/Event:TEvtRecObject/m_evtRecPi0Col": "TEvtRecPi0", - "/Event:TEvtRecObject/m_evtRecEtaToGGCol": "TEvtRecEtaToGG", - "/Event:TEvtRecObject/m_evtRecDTagCol": "TEvtRecDTag", - "/Event:THltEvent/m_hltRawCol": "THltRaw", -} - - -class Cpp_Bes3TObjArrayReader(Cpp_BaseReader): - """ - This class reads a TObjArray from a binary parser. - - I know that there is only 1 kind of class in the TObjArray I will read, - so I can use only 1 reader to read all elements in TObjArray. - """ - - def __init__(self, name: str, element_reader: ObjectHeaderReader): - """ - Args: - element_reader (BaseReader): The reader for the elements in the array. - """ - self.name = name - self.element_reader = element_reader - self.counts = array("Q") - - def read(self, parser): - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - _ = parser.read_fVersion() - _ = parser.read_number("u4") # fUniqueID - _ = parser.read_number("u4") # fBits - - # Just directly read data - _ = parser.read_number("u1") # fName - fSize = parser.read_number("u4") - _ = parser.read_number("u4") # fLowerBound - - for _ in range(fSize): - self.element_reader.read(parser) - - # Update offsets - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - self.element_reader.get_data(), - ) diff --git a/dev/custom-interpretation/example.py b/dev/custom-interpretation/example.py deleted file mode 100644 index afccf4e20..000000000 --- a/dev/custom-interpretation/example.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import tiny_reader - -import uproot - -tiny_reader.register() - -f = uproot.open("tiny_reader/gen-data/example.root") -f["my_tree"].show() - -print("==================================") -arr = f["my_tree"].arrays() -arr.show(all=True) diff --git a/dev/custom-interpretation/tiny_reader/__init__.py b/dev/custom-interpretation/tiny_reader/__init__.py deleted file mode 100644 index 748f49222..000000000 --- a/dev/custom-interpretation/tiny_reader/__init__.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import awkward as ak -from AsCustom import ( - AsCustom, - BaseReader, - ReaderType, - gen_tree_config, - get_reader_instance, - readers, - reconstruct_array, -) - -from .should_be_cpp import Cpp_MyTObjArrayReader - - -class MyTObjArrayReader(BaseReader): - """ - This class reads a TObjArray from a binary parser. - - I know that there is only 1 kind of class in the TObjArray I will read, - so I can use only 1 reader to read all elements in TObjArray. - """ - - @classmethod - def gen_tree_config( - cls, - top_type_name: str, - cls_streamer_info: dict, - all_streamer_info: dict, - item_path: str = "", - ): - if top_type_name != "TObjArray": - return None - - obj_typename = "TMySubObject" - - if obj_typename not in all_streamer_info: - return { - "reader": "MyTObjArrayReader", - "name": cls_streamer_info["fName"], - "element_reader": { - "reader": ReaderType.Empty, - "name": obj_typename, - }, - } - - sub_reader_config = [] - for s in all_streamer_info[obj_typename]: - sub_reader_config.append( - gen_tree_config(s, all_streamer_info, item_path + f".{obj_typename}") - ) - - return { - "reader": "MyTObjArrayReader", - "name": cls_streamer_info["fName"], - "element_reader": { - "reader": ReaderType.ObjectHeader, - "name": obj_typename, - "sub_readers": sub_reader_config, - }, - } - - @staticmethod - def get_reader_instance( - reader_config: dict, - ): - if reader_config["reader"] != "MyTObjArrayReader": - return None - - element_reader_config = reader_config["element_reader"] - element_reader = get_reader_instance(element_reader_config) - - return Cpp_MyTObjArrayReader(reader_config["name"], element_reader) - - @staticmethod - def reconstruct_array(raw_data, reader_config: dict): - if reader_config["reader"] != "MyTObjArrayReader": - return None - - counts, element_raw_data = raw_data - element_reader_config = reader_config["element_reader"] - element_data = reconstruct_array( - element_raw_data, - element_reader_config, - ) - - return ak.unflatten(element_data, counts) - - -def register(): - readers.add(MyTObjArrayReader) - AsCustom.target_branches.add("/my_tree:my_obj/m_obj_array") diff --git a/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt b/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt deleted file mode 100644 index cb2225af3..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -project(gen-data LANGUAGES CXX) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_BUILD_TYPE Debug) - -find_package(ROOT REQUIRED COMPONENTS Core RIO Tree) - -add_executable(gen-data - src/main.cc - src/TMyObject.cc - src/TMySubObject.cc -) - -target_include_directories(gen-data - PUBLIC - ${CMAKE_CURRENT_LIST_DIR}/include -) - -target_link_libraries(gen-data - PRIVATE - ROOT::Core - ROOT::RIO - ROOT::Tree -) - -ROOT_GENERATE_DICTIONARY(MyRootDict - include/TMyObject.hh - include/TMySubObject.hh - LINKDEF include/LinkDef.h - MODULE gen-data -) diff --git a/dev/custom-interpretation/tiny_reader/gen-data/example.root b/dev/custom-interpretation/tiny_reader/gen-data/example.root deleted file mode 100644 index 5e4d06bee6177e2785a5bd2a7845695e9a33c5a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57712 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 Ug)sY}HFmRsCO<4FH=V)&0OiwI$^ZZW diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h b/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h deleted file mode 100644 index 0c75b079c..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/include/LinkDef.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifdef __CLING__ - -// # pragma link off all globals; -// # pragma link off all classes; -// # pragma link off all functions; - -# pragma link C++ class TMyObject + ; -# pragma link C++ class TMySubObject + ; - -#endif diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh b/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh deleted file mode 100644 index 6889a266c..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/include/TMyObject.hh +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -using std::vector; - -class TMyObject : public TObject { -public: - TMyObject(int counter); - -private: - int m_counter = 0; - - TObjArray m_obj_array; - - ClassDef(TMyObject, 1) -}; diff --git a/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh b/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh deleted file mode 100644 index f138ae0e6..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/include/TMySubObject.hh +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using std::map; -using std::string; -using std::vector; - -class TMySubObject : public TObject { -public: - TMySubObject(); - TMySubObject(int &counter); - - int set_data(int counter = 0); - -private: - // ------------ single elements ------------ // - // ctypes - int m_int; - int16_t m_int16; - ULong_t m_ulong; - - // STL - vector m_vec_int; - map m_map_int_double; - string m_stdstring; - - vector> m_vec_vec_int; - vector> m_vec_map_int_double; - - // ROOT types - TString m_tstring; - TArrayF m_tarrayf; - - // ------------ C-Arrays ------------ // - // 1d arrays - int m_carr_int[3]; - vector m_carr_vec_int[3]; - TString m_carr_tstring[3]; - TArrayF m_carr_tarrayf[3]; - - // 2d arrays - int m_carr2d_int[2][3]; - vector m_carr2d_vec_int[2][3]; - TString m_carr2d_tstring[2][3]; - TArrayF m_carr2d_tarrayf[2][3]; - - ClassDef(TMySubObject, 1) -}; diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc deleted file mode 100644 index 6da604361..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/src/TMyObject.cc +++ /dev/null @@ -1,10 +0,0 @@ -#include "TMyObject.hh" -#include "TMySubObject.hh" - -ClassImp(TMyObject); - -TMyObject::TMyObject(int counter) : m_counter(counter) { - for (int i = 0; i < 3; i++) { - m_obj_array.Add(new TMySubObject(m_counter)); - } -} diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc deleted file mode 100644 index 9ad40629d..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/src/TMySubObject.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "TMySubObject.hh" -#include - -ClassImp(TMySubObject); - -TMySubObject::TMySubObject() { set_data(); } -TMySubObject::TMySubObject(int &counter) { counter = set_data(counter); } - -int TMySubObject::set_data(int counter) { - // ------------ single elements ------------ // - m_int = counter++; - m_int16 = counter++; - m_ulong = counter++; - - m_vec_int = {counter++, counter++, counter++}; - m_map_int_double = {{counter++, (double)counter++}, - {counter++, (double)counter++}}; - m_stdstring = "I'm std::string " + std::to_string(counter++) + "!"; - for (int i = 0; i < 20; i++) { - m_stdstring += ("I'm std::string " + std::to_string(counter++) + "!"); - } - - m_vec_vec_int = {{counter++, counter++, counter++}, {counter++, counter++}}; - m_vec_map_int_double = vector>(); - for (int i = 0; i < 4; i++) { - map m; - for (int j = 0; j < 3; j++) { - m[counter++] = (double)counter++; - } - m_vec_map_int_double.push_back(m); - } - - m_tstring = Form("I'm TString %d", counter++); - - m_tarrayf = TArrayF(3); - m_tarrayf[0] = (float)counter++; - m_tarrayf[1] = (float)counter++; - m_tarrayf[2] = (float)counter++; - - // ------------ C-Arrays ------------ // - for (int i = 0, counter = 29; i < 3; i++) { - // 1d arrays - m_carr_int[i] = counter++; - m_carr_vec_int[i] = {counter++, counter++, counter++}; - m_carr_tstring[i] = Form("I'm TString %d", counter++); - m_carr_tarrayf[i] = TArrayF(3); - m_carr_tarrayf[i][0] = counter++; - m_carr_tarrayf[i][1] = counter++; - m_carr_tarrayf[i][2] = counter++; - - // 2d arrays - for (int j = 0; j < 2; j++) { - m_carr2d_int[j][i] = counter++; - m_carr2d_vec_int[j][i] = {counter++, counter++, counter++}; - m_carr2d_tstring[j][i] = Form("I'm TString %d", counter++); - m_carr2d_tarrayf[j][i] = TArrayF(3); - m_carr2d_tarrayf[j][i][0] = counter++; - m_carr2d_tarrayf[j][i][1] = counter++; - m_carr2d_tarrayf[j][i][2] = counter++; - } - } - - return counter; -} diff --git a/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc b/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc deleted file mode 100644 index 5e61ba850..000000000 --- a/dev/custom-interpretation/tiny_reader/gen-data/src/main.cc +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include - -#include "TMyObject.hh" - -int main() { - TFile f("test.root", "RECREATE"); - TTree t("my_tree", "tree"); - - TMyObject my_obj(0); - - t.Branch("my_obj", &my_obj); - - for (int i = 0; i < 100; i++) { - my_obj = TMyObject(i); - t.Fill(); - } - - t.Write(); - f.Close(); -} diff --git a/dev/custom-interpretation/tiny_reader/should_be_cpp.py b/dev/custom-interpretation/tiny_reader/should_be_cpp.py deleted file mode 100644 index 8c7677abd..000000000 --- a/dev/custom-interpretation/tiny_reader/should_be_cpp.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -from array import array - -import numpy as np -from AsCustom.should_be_cpp import Cpp_BaseReader, Cpp_ObjectHeaderReader - - -class Cpp_MyTObjArrayReader(Cpp_BaseReader): - """ - This class reads a TObjArray from a binary parser. - - I know that there is only 1 kind of class in the TObjArray I will read, - so I can use only 1 reader to read all elements in TObjArray. - """ - - def __init__(self, name: str, element_reader: Cpp_ObjectHeaderReader): - """ - Args: - element_reader (BaseReader): The reader for the elements in the array. - """ - self.name = name - self.element_reader = element_reader - self.counts = array("Q") - - def read(self, parser): - _ = parser.read_fNBytes() - _ = parser.read_fVersion() - _ = parser.read_fVersion() - _ = parser.read_number("u4") # fUniqueID - _ = parser.read_number("u4") # fBits - - # Just directly read data - _ = parser.read_number("u1") # fName - fSize = parser.read_number("u4") - _ = parser.read_number("u4") # fLowerBound - - for _ in range(fSize): - self.element_reader.read(parser) - - # Update offsets - self.counts.append(fSize) - - def get_data(self): - return ( - np.asarray(self.counts, dtype="Q"), - self.element_reader.get_data(), - ) From ce86bc56cc0e8170bd9d19b4156995dbdc0fe895 Mon Sep 17 00:00:00 2001 From: mrli Date: Wed, 30 Jul 2025 23:37:57 +0800 Subject: [PATCH 04/13] use `set()` as registered interpretations container --- src/uproot/interpretation/identify.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/uproot/interpretation/identify.py b/src/uproot/interpretation/identify.py index 7dc619e2e..e88bf612f 100644 --- a/src/uproot/interpretation/identify.py +++ b/src/uproot/interpretation/identify.py @@ -22,7 +22,7 @@ import uproot -_custom_interpretations = {} +_registered_interpretations = set() def register_interpretation(cls): @@ -35,10 +35,9 @@ def register_interpretation(cls): This method registers a custom interpretation class to be used in :doc:`uproot.interpretation.identify.interpretation_of`. """ - key = cls.__name__ - if key in _custom_interpretations: - warnings.warn(f"Overwriting existing custom interpretation{key}", stacklevel=2) - _custom_interpretations[key] = cls + if cls in _registered_interpretations: + warnings.warn(f"Overwriting existing custom interpretation {cls}", stacklevel=2) + _registered_interpretations.add(cls) def _normalize_ftype(fType): @@ -334,13 +333,14 @@ def interpretation_of(branch, context, simplify=True): # Match custom interpretations matched_custom_interpretations = [ cls - for cls in _custom_interpretations.values() + for cls in _registered_interpretations if cls.match_branch(branch, context, simplify) ] assert ( len(matched_custom_interpretations) <= 1 - ), "Multiple custom interpretations matched, uproot cannot determine which one to use!" + ), "Multiple custom interpretations matched, " + f"uproot cannot determine which one to use: {matched_custom_interpretations}" if len(matched_custom_interpretations) == 1: return matched_custom_interpretations[0](branch, context, simplify) From 95c5b614a8aeda00a4157c95307596a509a7506d Mon Sep 17 00:00:00 2001 From: mrli Date: Wed, 30 Jul 2025 23:39:07 +0800 Subject: [PATCH 05/13] avoid directly exposing `CustomInterpretation` --- src/uproot/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uproot/__init__.py b/src/uproot/__init__.py index d3e8395de..fc77496f8 100644 --- a/src/uproot/__init__.py +++ b/src/uproot/__init__.py @@ -170,7 +170,6 @@ from uproot.interpretation.objects import AsObjects from uproot.interpretation.objects import AsStridedObjects from uproot.interpretation.grouped import AsGrouped -from uproot.interpretation.custom import CustomInterpretation from uproot.interpretation.identify import register_interpretation from uproot.containers import AsString from uproot.containers import AsPointer From 4ea9dcbd9cec106f9d1ac0d016e1efcc6eeaf5f5 Mon Sep 17 00:00:00 2001 From: mrli Date: Wed, 30 Jul 2025 23:39:45 +0800 Subject: [PATCH 06/13] Only keep `CustomInterpretation` as base class --- src/uproot/interpretation/custom.py | 75 ++++++----------------------- 1 file changed, 15 insertions(+), 60 deletions(-) diff --git a/src/uproot/interpretation/custom.py b/src/uproot/interpretation/custom.py index b158c9542..1418d5ed2 100644 --- a/src/uproot/interpretation/custom.py +++ b/src/uproot/interpretation/custom.py @@ -16,6 +16,7 @@ import uproot.interpretation awkward = uproot.extras.awkward() +pandas = uproot.extras.pandas() class CustomInterpretation(uproot.interpretation.Interpretation): @@ -107,67 +108,21 @@ def final_array( arr_to_concat = [ basket_arrays[i] for i in range(basket_start_idx, basket_end_idx + 1) ] - tot_array = awkward.concatenate(arr_to_concat) relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] - return tot_array[relative_entry_start:relative_entry_stop] - - -class AsBinary(uproot.interpretation.Interpretation): - """ - Return binary data of the ``TBasket``. Pass an instance of this class - to :ref:`uproot.behaviors.TBranch.TBranch.array` like this: - - .. code-block:: python - binary_data = branch.array(interpretation=AsBinary()) - - """ - - @property - def cache_key(self) -> str: - return id(self) - - def basket_array( - self, - data, - byte_offsets, - basket, - branch, - context, - cursor_offset, - library, - interp_options, - ): - counts = byte_offsets[1:] - byte_offsets[:-1] - awkward = uproot.extras.awkward() - return awkward.unflatten(data, counts) - - def final_array( - self, - basket_arrays, - entry_start, - entry_stop, - entry_offsets, - library, - branch, - options, - ): - basket_entry_starts = numpy.array(entry_offsets[:-1]) - basket_entry_stops = numpy.array(entry_offsets[1:]) - - basket_start_idx = numpy.where(basket_entry_starts <= entry_start)[0].max() - basket_end_idx = numpy.where(basket_entry_stops >= entry_stop)[0].min() - - arr_to_concat = [ - basket_arrays[i] for i in range(basket_start_idx, basket_end_idx + 1) - ] - - awkward = uproot.extras.awkward() - tot_array = awkward.concatenate(arr_to_concat) - - relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] - relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] - - return tot_array[relative_entry_start:relative_entry_stop] + if isinstance(arr_to_concat, awkward.Array): + tot_array = awkward.concatenate(arr_to_concat) + return tot_array[relative_entry_start:relative_entry_stop] + elif isinstance(arr_to_concat, numpy.ndarray): + tot_array = numpy.concatenate(arr_to_concat) + return tot_array[relative_entry_start:relative_entry_stop] + elif isinstance(arr_to_concat, pandas.DataFrame): + tot_array = pandas.concat(arr_to_concat, ignore_index=True) + return tot_array.iloc[relative_entry_start:relative_entry_stop] + else: + raise TypeError( + f"Unsupported array type: {type(arr_to_concat)}. " + "Expected an Awkward Array, NumPy array, or Pandas DataFrame." + ) From d90391ca6a185955c033ebb331605b23eff6562e Mon Sep 17 00:00:00 2001 From: mrli Date: Thu, 31 Jul 2025 00:50:15 +0800 Subject: [PATCH 07/13] add unit test for custom interpretation --- src/uproot/interpretation/custom.py | 9 ++-- tests/test_1477_custom_interpretation.py | 64 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 tests/test_1477_custom_interpretation.py diff --git a/src/uproot/interpretation/custom.py b/src/uproot/interpretation/custom.py index 1418d5ed2..1a9bb1f32 100644 --- a/src/uproot/interpretation/custom.py +++ b/src/uproot/interpretation/custom.py @@ -42,8 +42,9 @@ def __init__( self._context = context self._simplify = simplify + @classmethod def match_branch( - self, + cls, branch: uproot.behaviors.TBranch.TBranch, context: dict, simplify: bool, @@ -112,13 +113,13 @@ def final_array( relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] - if isinstance(arr_to_concat, awkward.Array): + if isinstance(arr_to_concat[0], awkward.Array): tot_array = awkward.concatenate(arr_to_concat) return tot_array[relative_entry_start:relative_entry_stop] - elif isinstance(arr_to_concat, numpy.ndarray): + elif isinstance(arr_to_concat[0], numpy.ndarray): tot_array = numpy.concatenate(arr_to_concat) return tot_array[relative_entry_start:relative_entry_stop] - elif isinstance(arr_to_concat, pandas.DataFrame): + elif isinstance(arr_to_concat[0], pandas.DataFrame): tot_array = pandas.concat(arr_to_concat, ignore_index=True) return tot_array.iloc[relative_entry_start:relative_entry_stop] else: diff --git a/tests/test_1477_custom_interpretation.py b/tests/test_1477_custom_interpretation.py new file mode 100644 index 000000000..f0706f2f6 --- /dev/null +++ b/tests/test_1477_custom_interpretation.py @@ -0,0 +1,64 @@ +# BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE + +import numpy +import pytest +import skhep_testdata + +import uproot +import uproot.interpretation.custom + + +class AsBinary(uproot.interpretation.custom.CustomInterpretation): + @classmethod + def match_branch(cls, branch, context, simplify): + if branch.object_path == "/Events;1:Info/runNum": + return True + + def basket_array( + self, + data, + byte_offsets, + basket, + branch, + context, + cursor_offset, + library, + interp_options, + ): + return data.view(">u4") + + +uproot.register_interpretation(AsBinary) + + +def test_custom_interpretation(): + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + runNo = f["Events/Info/runNum"].array() + + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + br = f["Events/Info/runNum"] + assert isinstance(br.interpretation, AsBinary) + + runNo_custom = br.array() + assert numpy.all(runNo_custom == runNo) + + +def test_custom_interpretation_entry_range(): + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + br = f["Events/Info/runNum"] + runNo_custom = br.array(entry_start=0, entry_stop=5) + assert len(runNo_custom) == 5 + + runNo_custom = br.array(entry_start=5, entry_stop=10) + assert len(runNo_custom) == 5 + + runNo_custom = br.array(entry_start=0, entry_stop=10) + assert len(runNo_custom) == 10 + + +def test_repeated_register(): + with pytest.warns( + UserWarning, + match="Overwriting existing custom interpretation ", + ): + uproot.register_interpretation(AsBinary) From 069ac9c42b4a0878f0b2c8f0e70b88f0e3e07ad7 Mon Sep 17 00:00:00 2001 From: mrli Date: Thu, 31 Jul 2025 00:50:23 +0800 Subject: [PATCH 08/13] add docs for custom interpretation --- docs-sphinx/basic.rst | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs-sphinx/basic.rst b/docs-sphinx/basic.rst index c70779e23..b4cf3b9a6 100644 --- a/docs-sphinx/basic.rst +++ b/docs-sphinx/basic.rst @@ -1311,3 +1311,67 @@ Here is an example of writing an RNTuple. Since TTree is still the default forma >>> data = {"my_int": [1,2], "my_vector": [[1,2], [3,4,5]]} >>> rntuple = file.mkrntuple("my_rntuple", data) >>> rntuple.extend(data) # Can be extended, just like TTrees + +Using your own interpretation +-------------------------------- + +Interpretations manages how data are read from files and converted into arrays. There are many built-in interpretations, but you can also write your own. This is useful if you want to read your custom classes that are not supported by Uproot. + +To write your own interpretation, you need to subclass :doc:`uproot.interpretation.custom.CustomInterpretation` and implement some methods: + +.. code-block:: python + + import uproot + from uproot.interpretation.custom import CustomInterpretation + + class MyCustomInterpretation(CustomInterpretation): + def __init__( + self, + branch: uproot.behaviors.TBranch.TBranch, + context: dict, + simplify: bool, + ): + super().__init__(branch, context, simplify) + # Initialize your interpretation here + + @classmethod + def match_branch( + self, + branch: uproot.behaviors.TBranch.TBranch, + context: dict, + simplify: bool, + ) -> bool: + # Return True if this interpretation can handle the branch + # You can also declare this method as a class method + + def basket_array( + self, + data, + byte_offsets, + basket, + branch, + context, + cursor_offset, + library, + interp_options, + ): + # Convert the data from the basket into an array + # `data` is the raw binary data from the basket as `np.ndarray[np.uint8]`, + # `byte_offsets` is a `uint32` numpy array with the byte offsets of each entry. + +The ``match_branch`` method tells Uproot whether this interpretation can handle a specific branch. It is called for each branch in the file, and if it returns ``True``, Uproot will instantiate this interpretation with the same arguments passed to ``match_branch``. + +The ``basket_array`` method is where you convert the raw binary data from the basket into an array. The ``data`` argument is a NumPy array of type ``np.uint8``, which contains the raw bytes of the basket. The ``byte_offsets`` argument is a NumPy array of type ``np.uint32``, which contains the byte offsets of each entry in ``data``. You should return the converted data in this method. + +.. note:: + + If ``basket_array`` does not return an ``awkward``, ``numpy`` array or ``pandas`` dataframe, you need to reimplement the ``final_array`` method to concatenate the results. + +To use your custom interpretation, register it with Uproot like: + +.. code-block:: python + + import uproot + uproot.register_interpretation(MyCustomInterpretation) + +Then you can use it to read data as described in the previous sections. From b75f4dc939b45caa681f2cfe8ea724a2d1cd6a77 Mon Sep 17 00:00:00 2001 From: mrli Date: Thu, 31 Jul 2025 16:29:10 +0800 Subject: [PATCH 09/13] import `pandas` only when needed --- src/uproot/interpretation/custom.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/uproot/interpretation/custom.py b/src/uproot/interpretation/custom.py index 1a9bb1f32..647240f54 100644 --- a/src/uproot/interpretation/custom.py +++ b/src/uproot/interpretation/custom.py @@ -15,9 +15,6 @@ import uproot.extras import uproot.interpretation -awkward = uproot.extras.awkward() -pandas = uproot.extras.pandas() - class CustomInterpretation(uproot.interpretation.Interpretation): def __init__( @@ -97,9 +94,6 @@ def final_array( """ Concatenate the arrays from the baskets and return the final array. """ - - awkward = uproot.extras.awkward() - basket_entry_starts = numpy.array(entry_offsets[:-1]) basket_entry_stops = numpy.array(entry_offsets[1:]) @@ -113,17 +107,21 @@ def final_array( relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] + if isinstance(arr_to_concat[0], numpy.ndarray): + tot_array = numpy.concatenate(arr_to_concat) + return tot_array[relative_entry_start:relative_entry_stop] + + awkward = uproot.extras.awkward() if isinstance(arr_to_concat[0], awkward.Array): tot_array = awkward.concatenate(arr_to_concat) return tot_array[relative_entry_start:relative_entry_stop] - elif isinstance(arr_to_concat[0], numpy.ndarray): - tot_array = numpy.concatenate(arr_to_concat) - return tot_array[relative_entry_start:relative_entry_stop] - elif isinstance(arr_to_concat[0], pandas.DataFrame): + + pandas = uproot.extras.pandas() + if isinstance(arr_to_concat[0], pandas.Series): tot_array = pandas.concat(arr_to_concat, ignore_index=True) return tot_array.iloc[relative_entry_start:relative_entry_stop] - else: - raise TypeError( - f"Unsupported array type: {type(arr_to_concat)}. " - "Expected an Awkward Array, NumPy array, or Pandas DataFrame." - ) + + raise TypeError( + f"Unsupported array type: {type(arr_to_concat)}. " + "Expected an Awkward Array, NumPy array, or Pandas DataFrame." + ) From 8e36949693ce12c3927328ce5dca4f3a0f523ab6 Mon Sep 17 00:00:00 2001 From: mrli Date: Thu, 31 Jul 2025 16:29:45 +0800 Subject: [PATCH 10/13] improve the test --- tests/test_1477_custom_interpretation.py | 80 +++++++++++++++++------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/tests/test_1477_custom_interpretation.py b/tests/test_1477_custom_interpretation.py index f0706f2f6..8b100a117 100644 --- a/tests/test_1477_custom_interpretation.py +++ b/tests/test_1477_custom_interpretation.py @@ -1,5 +1,7 @@ # BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE +import re + import numpy import pytest import skhep_testdata @@ -7,11 +9,14 @@ import uproot import uproot.interpretation.custom +awkward = uproot.extras.awkward() +pandas = uproot.extras.pandas() + -class AsBinary(uproot.interpretation.custom.CustomInterpretation): +class AsUint32(uproot.interpretation.custom.CustomInterpretation): @classmethod def match_branch(cls, branch, context, simplify): - if branch.object_path == "/Events;1:Info/runNum": + if branch.object_path == "/Events;1:Info/evtNum": return True def basket_array( @@ -25,40 +30,67 @@ def basket_array( library, interp_options, ): - return data.view(">u4") + np_data = data.view(">u4").astype("uint32") + if library.name == "np": + return np_data + elif library.name == "ak": + return awkward.from_numpy(np_data) + elif library.name == "pd": + return pandas.Series(np_data) + else: + raise ValueError(f"Unsupported library: {library.name}") -uproot.register_interpretation(AsBinary) +# During the test, registration can only be done once, since the +# uproot will not be reloaded across tests. +uproot.register_interpretation(AsUint32) -def test_custom_interpretation(): - with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: - runNo = f["Events/Info/runNum"].array() +def test_registration_and_use(): with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: - br = f["Events/Info/runNum"] - assert isinstance(br.interpretation, AsBinary) + br2 = f["Events/Info/evtNum"] + assert isinstance(br2.interpretation, AsUint32) + + evtNum_ak = br2.array() + assert evtNum_ak.tolist() == [ + 135353219, + 135353222, + 135353225, + 135353230, + 135353239, + 135353242, + 135353244, + 135353247, + 135353252, + 135353256, + ] + + evtNum_np = br2.array(library="np") + assert isinstance(evtNum_np, numpy.ndarray) + assert numpy.all(evtNum_np == evtNum_ak) + + evtNum_pd = br2.array(library="pd") + assert isinstance(evtNum_pd, pandas.Series) + assert numpy.all(evtNum_pd.values == evtNum_np) - runNo_custom = br.array() - assert numpy.all(runNo_custom == runNo) +def test_repeated_register(): + with pytest.warns( + UserWarning, + match="Overwriting existing custom interpretation ", + ): + uproot.register_interpretation(AsUint32) -def test_custom_interpretation_entry_range(): + +def test_entry_range(): with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: br = f["Events/Info/runNum"] - runNo_custom = br.array(entry_start=0, entry_stop=5) - assert len(runNo_custom) == 5 + runNo_custom = br.array(entry_start=0, entry_stop=6) + assert len(runNo_custom) == 6 - runNo_custom = br.array(entry_start=5, entry_stop=10) - assert len(runNo_custom) == 5 + runNo_custom = br.array(entry_start=6, entry_stop=10) + assert len(runNo_custom) == 4 runNo_custom = br.array(entry_start=0, entry_stop=10) assert len(runNo_custom) == 10 - - -def test_repeated_register(): - with pytest.warns( - UserWarning, - match="Overwriting existing custom interpretation ", - ): - uproot.register_interpretation(AsBinary) From 2732d721ba70464ae55a543c2e67ce5c426589d9 Mon Sep 17 00:00:00 2001 From: Mingrun Li <74824770+mrzimu@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:52:04 +0800 Subject: [PATCH 11/13] Update tests/test_1477_custom_interpretation.py Co-authored-by: Andres Rios Tascon --- tests/test_1477_custom_interpretation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_1477_custom_interpretation.py b/tests/test_1477_custom_interpretation.py index 8b100a117..3c6c1aa8b 100644 --- a/tests/test_1477_custom_interpretation.py +++ b/tests/test_1477_custom_interpretation.py @@ -9,8 +9,8 @@ import uproot import uproot.interpretation.custom -awkward = uproot.extras.awkward() -pandas = uproot.extras.pandas() +awkward = pytest.importorskip("awkward") +pandas = pytest.importorskip("pandas") class AsUint32(uproot.interpretation.custom.CustomInterpretation): From 23aeb17adc2d3b099d4301b88ca77ea0a0df5ade Mon Sep 17 00:00:00 2001 From: mrli Date: Fri, 1 Aug 2025 00:30:00 +0800 Subject: [PATCH 12/13] add interpretation unregistration method --- src/uproot/__init__.py | 1 + src/uproot/interpretation/identify.py | 13 ++++++++++++ tests/test_1477_custom_interpretation.py | 25 ++++++++++++++++++------ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/uproot/__init__.py b/src/uproot/__init__.py index fc77496f8..a324f4664 100644 --- a/src/uproot/__init__.py +++ b/src/uproot/__init__.py @@ -171,6 +171,7 @@ from uproot.interpretation.objects import AsStridedObjects from uproot.interpretation.grouped import AsGrouped from uproot.interpretation.identify import register_interpretation +from uproot.interpretation.identify import unregister_interpretation from uproot.containers import AsString from uproot.containers import AsPointer from uproot.containers import AsArray diff --git a/src/uproot/interpretation/identify.py b/src/uproot/interpretation/identify.py index e88bf612f..27356ff72 100644 --- a/src/uproot/interpretation/identify.py +++ b/src/uproot/interpretation/identify.py @@ -40,6 +40,19 @@ def register_interpretation(cls): _registered_interpretations.add(cls) +def unregister_interpretation(cls): + """ + Unregister a custom interpretation class. + + Args: + cls (type): A subclass of :doc:`uproot.interpretation.custom.CustomInterpretation`. + + This method removes a custom interpretation class from the registry. + """ + if cls in _registered_interpretations: + _registered_interpretations.remove(cls) + + def _normalize_ftype(fType): if fType is not None and uproot.const.kOffsetL < fType < uproot.const.kOffsetP: return fType - uproot.const.kOffsetL diff --git a/tests/test_1477_custom_interpretation.py b/tests/test_1477_custom_interpretation.py index 3c6c1aa8b..4c278c62a 100644 --- a/tests/test_1477_custom_interpretation.py +++ b/tests/test_1477_custom_interpretation.py @@ -49,10 +49,10 @@ def basket_array( def test_registration_and_use(): with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: - br2 = f["Events/Info/evtNum"] - assert isinstance(br2.interpretation, AsUint32) + br = f["Events/Info/evtNum"] + assert isinstance(br.interpretation, AsUint32) - evtNum_ak = br2.array() + evtNum_ak = br.array() assert evtNum_ak.tolist() == [ 135353219, 135353222, @@ -66,11 +66,11 @@ def test_registration_and_use(): 135353256, ] - evtNum_np = br2.array(library="np") + evtNum_np = br.array(library="np") assert isinstance(evtNum_np, numpy.ndarray) assert numpy.all(evtNum_np == evtNum_ak) - evtNum_pd = br2.array(library="pd") + evtNum_pd = br.array(library="pd") assert isinstance(evtNum_pd, pandas.Series) assert numpy.all(evtNum_pd.values == evtNum_np) @@ -85,7 +85,9 @@ def test_repeated_register(): def test_entry_range(): with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: - br = f["Events/Info/runNum"] + br = f["Events/Info/evtNum"] + assert isinstance(br.interpretation, AsUint32) + runNo_custom = br.array(entry_start=0, entry_stop=6) assert len(runNo_custom) == 6 @@ -94,3 +96,14 @@ def test_entry_range(): runNo_custom = br.array(entry_start=0, entry_stop=10) assert len(runNo_custom) == 10 + + +def test_unregister(): + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + br = f["Events/Info/evtNum"] + assert isinstance(br.interpretation, AsUint32) + + uproot.unregister_interpretation(AsUint32) + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + br = f["Events/Info/evtNum"] + assert not isinstance(br.interpretation, AsUint32) From dc70012a45a4f28891c9109860407ce6736807ee Mon Sep 17 00:00:00 2001 From: mrli Date: Fri, 1 Aug 2025 00:30:38 +0800 Subject: [PATCH 13/13] add `AsBinary` interpretation --- src/uproot/interpretation/custom.py | 95 ++++++++++++++++++++++++ tests/test_1477_custom_interpretation.py | 14 ++++ 2 files changed, 109 insertions(+) diff --git a/src/uproot/interpretation/custom.py b/src/uproot/interpretation/custom.py index 647240f54..ded74fcb5 100644 --- a/src/uproot/interpretation/custom.py +++ b/src/uproot/interpretation/custom.py @@ -125,3 +125,98 @@ def final_array( f"Unsupported array type: {type(arr_to_concat)}. " "Expected an Awkward Array, NumPy array, or Pandas DataFrame." ) + + +class AsBinary(uproot.interpretation.Interpretation): + """ + Return binary data of the ``TBasket``. Pass an instance of this class + to :ref:`uproot.behaviors.TBranch.TBranch.array` like this: + + .. code-block:: python + binary_data = branch.array(interpretation=AsBinary()) + + """ + + @property + def cache_key(self) -> str: + return id(self) + + def basket_array( + self, + data, + byte_offsets, + basket, + branch, + context, + cursor_offset, + library, + interp_options, + ): + if byte_offsets is not None: + counts = byte_offsets[1:] - byte_offsets[:-1] + else: + counts = None + + if library.name == "ak": + awkward = uproot.extras.awkward() + if counts is not None: + return awkward.unflatten(data, counts) + else: + fSize = branch.streamer.member("fSize") + return awkward.from_numpy(data.reshape(-1, fSize)) + + elif library.name == "np": + if counts is not None: + assert ( + numpy.unique(counts[1:] - counts[:-1]).size == 1 + ), "The byte offsets must be uniform for NumPy arrays." + + bytes_per_event = counts[0] + return data.reshape(-1, bytes_per_event) + else: + fSize = branch.streamer.member("fSize") + return data.reshape(-1, fSize).view(">u1") + else: + raise ValueError( + f"Unsupported library: {library.name}, can only use 'ak' or 'np'." + ) + + def final_array( + self, + basket_arrays, + entry_start, + entry_stop, + entry_offsets, + library, + branch, + options, + ): + + basket_entry_starts = numpy.array(entry_offsets[:-1]) + basket_entry_stops = numpy.array(entry_offsets[1:]) + + basket_start_idx = numpy.where(basket_entry_starts <= entry_start)[0].max() + basket_end_idx = numpy.where(basket_entry_stops >= entry_stop)[0].min() + + arr_to_concat = [ + basket_arrays[i] for i in range(basket_start_idx, basket_end_idx + 1) + ] + + relative_entry_start = entry_start - basket_entry_starts[basket_start_idx] + relative_entry_stop = entry_stop - basket_entry_starts[basket_start_idx] + + if library.name == "ak": + awkward = uproot.extras.awkward() + return awkward.concatenate(arr_to_concat)[ + relative_entry_start:relative_entry_stop + ] + + elif library.name == "np": + return numpy.concatenate(arr_to_concat)[ + relative_entry_start:relative_entry_stop + ] + + else: + raise ValueError( + f"Unsupported library: {library.name}, can only use 'ak' or 'np'." + ) diff --git a/tests/test_1477_custom_interpretation.py b/tests/test_1477_custom_interpretation.py index 4c278c62a..b41408dc9 100644 --- a/tests/test_1477_custom_interpretation.py +++ b/tests/test_1477_custom_interpretation.py @@ -107,3 +107,17 @@ def test_unregister(): with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: br = f["Events/Info/evtNum"] assert not isinstance(br.interpretation, AsUint32) + + +def test_AsBinary(): + from uproot.interpretation.custom import AsBinary + + with uproot.open(skhep_testdata.data_path("uproot-mc10events.root")) as f: + br = f["Events/Info/evtNum"] + + data_np_arr = br.array(library="np") + bin_ak_arr = br.array(interpretation=AsBinary()) + bin_np_arr = br.array(interpretation=AsBinary(), library="np") + + assert numpy.all(data_np_arr == bin_np_arr.view(">u4").flatten()) + assert numpy.all(data_np_arr == bin_ak_arr.to_numpy().view(">u4").flatten())