from __future__ import annotations
import awkward as ak
from collections import ChainMap
from analyzer.utils.pretty import progbar
from analyzer.core.exceptions import ResultIntegrityError
from analyzer.utils.file_tools import iterPaths
import numpy as np
import numbers
import pickle as pkl
import lz4.frame
import dask_awkward as dak
import functools as ft
from cattrs.strategies import include_subclasses, configure_tagged_union
from analyzer.core.event_collection import FileSet
from analyzer.core.serialization import converter
import hist
from analyzer.utils.structure_tools import globWithMeta, commonDict, getWithMeta
from attrs import define, field
import copy
import abc
from typing import Any, Literal, ClassVar
import logging
[docs]
logger = logging.getLogger("analyzer")
[docs]
def getArrayMem(array):
from dask.sizeof import sizeof
if isinstance(array, ak.highlevel.Array):
return array.nbytes
return sizeof(array)
@define
[docs]
class ResultBase(abc.ABC):
_metadata: dict[str, Any] = field(factory=dict, kw_only=True)
@property
@abc.abstractmethod
[docs]
def __iadd__(self, other) -> ResultBase:
pass
@abc.abstractmethod
[docs]
def iscale(self, value) -> ResultBase:
pass
@abc.abstractmethod
[docs]
def approxSize(self) -> int:
pass
@abc.abstractmethod
[docs]
def finalize(self) -> ResultBase: ...
[docs]
def summary(self):
return self
[docs]
def __add__(self, other):
ret = copy.deepcopy(self)
ret += other
return ret
[docs]
def scale(self, value):
ret = copy.deepcopy(self)
return ret.iscale(value)
@define
[docs]
class ResultGroup(ResultBase):
_MAGIC_ID: ClassVar[Literal[b"sstopresult"]] = b"sstopresult"
_HEADER_SIZE: ClassVar[Literal[4]] = 4
[docs]
results: dict[str, ResultBase] = field(factory=dict)
@classmethod
[docs]
def peekFile(cls, f):
maybe_magic = f.read(len(cls._MAGIC_ID))
if maybe_magic == cls._MAGIC_ID:
peek_size = int.from_bytes(f.read(cls._HEADER_SIZE), byteorder="big")
ret = converter.unstructure(pkl.loads(f.read(peek_size)), ResultGroup)
return ret
else:
return converter.structure(pkl.loads(maybe_magic + f.read())).summary()
@classmethod
[docs]
def peekBytes(cls, data: bytes):
if data[0 : len(cls._MAGIC_ID)] == cls._MAGIC_ID:
header_value = data[
len(cls._MAGIC_ID) : len(cls._MAGIC_ID) + cls._HEADER_SIZE
]
peek_size = int.from_bytes(header_value, byteorder="big")
peek = data[
len(cls._MAGIC_ID) + cls._HEADER_SIZE : len(cls._MAGIC_ID)
+ cls._HEADER_SIZE
+ peek_size
]
return converter.structure(pkl.loads(peek), ResultGroup)
else:
return converter.structure(pkl.loads(data)).summary()
@classmethod
[docs]
def fromBytes(cls, data: bytes):
if data[0 : len(cls._MAGIC_ID)] == cls._MAGIC_ID:
header_value = data[
len(cls._MAGIC_ID) : len(cls._MAGIC_ID) + cls._HEADER_SIZE
]
peek_size = int.from_bytes(header_value, byteorder="big")
data[
len(cls._MAGIC_ID) + cls._HEADER_SIZE : len(cls._MAGIC_ID)
+ cls._HEADER_SIZE
+ peek_size
]
core_data = lz4.frame.decompress(
data[len(cls._MAGIC_ID) + cls._HEADER_SIZE + peek_size :]
)
ret = converter.structure(pkl.loads(core_data), cls)
else:
ret = converter.structure(pkl.loads(data), cls)
return ret
[docs]
def toBytes(self, packed_mode=True) -> bytes:
if packed_mode:
peek = pkl.dumps(converter.unstructure(self.summary()))
core_data = lz4.frame.compress(pkl.dumps(converter.unstructure(self)))
pl = len(peek)
plb = (pl.bit_length() + 7) // 8
if plb > self._HEADER_SIZE:
raise RuntimeError
return (
self._MAGIC_ID
+ pl.to_bytes(self._HEADER_SIZE, byteorder="big")
+ peek
+ core_data
)
else:
return pkl.dumps(converter.unstructure(self))
[docs]
def summary(self):
return ResultGroup(
name=self.name,
results={x: y.summary() for x, y in self.results.items()},
metadata=self.metadata,
)
[docs]
def approxSize(self):
return sum(x.approxSize() for x in self.results.values())
[docs]
def addResult(self, res):
self.results[res.name] = res
# def __setitem__(self, key, value):
# self.results[key] = value
[docs]
def __getitem__(self, key):
return self.results[key]
[docs]
def __iter__(self):
return iter(self.results)
[docs]
def keys(self):
return self.results.keys()
[docs]
def checkOk(self, other):
if "_provenance" in self.results:
if "_provenance" not in other.results:
raise RuntimeError()
intersection = self["_provenance"].file_set.intersection(other["_provenance"].file_set)
if (
not intersection.empty
):
raise ResultIntegrityError(f"Overlapping Provenance.\n{intersection}")
[docs]
def __iadd__(self, other):
self.checkOk(other)
for k in other.results:
if k in self.results:
self.results[k] += other.results[k]
else:
self.addResult(other.results[k])
return self
[docs]
def iscale(self, value):
for k in self.results:
self.results[k].iscale(value)
return self
[docs]
def finalize(self, finalizer):
for result in self.results.values():
result.finalize(finalizer)
@define
[docs]
class ResultProvenance(ResultBase):
[docs]
def approxSize(self):
return 200 * len(self.file_set.files)
[docs]
def __iadd__(self, other):
self.file_set += other.file_set
return self
[docs]
def iscale(self, value):
return self
@property
[docs]
def chunked_events(self):
return self.file_set.chunked_events
[docs]
def finalize(self, finalizer):
pass
@define
[docs]
class Histogram(ResultBase):
@define
[docs]
class Summary(ResultBase):
_approx_size: int = 0
[docs]
def __iadd__(self, other):
return self
[docs]
def iscale(self, value):
return self
[docs]
def approxSize(self):
return self._approx_size
[docs]
def finalize(self, finalizer):
return self
[docs]
def summary(self):
return Histogram.Summary(
name=self.name, axes=self.axes, approx_size=self.approxSize()
)
[docs]
def approxSize(self):
from dask.sizeof import sizeof
return sizeof(self.histogram.view(flow=True))
[docs]
def __iadd__(self, other):
self.histogram += other.histogram
return self
[docs]
def iscale(self, value):
self.histogram *= value
return self
[docs]
def finalize(self, finalizer):
return self
@define
[docs]
class UnscaledHistogram(ResultBase):
@define
[docs]
class Summary(ResultBase):
_approx_size: int = 0
[docs]
def __iadd__(self, other):
return self
[docs]
def iscale(self, value):
return self
[docs]
def approxSize(self):
return self._approx_size
[docs]
def finalize(self, finalizer):
return self
[docs]
def summary(self):
return UnscaledHistogram.Summary(
name=self.name, axes=self.axes, approx_size=self.approxSize()
)
[docs]
def approxSize(self):
from dask.sizeof import sizeof
return sizeof(self.histogram.view(flow=True))
[docs]
def __iadd__(self, other):
self.histogram += other.histogram
return self
[docs]
def iscale(self, value):
return self
[docs]
def finalize(self, finalizer):
return self
[docs]
Array = ak.Array | dak.Array | np.ndarray
@define
[docs]
class BasicSummary(ResultBase):
_approx_size: int = 0
[docs]
def __iadd__(self, other):
return self
[docs]
def iscale(self, value):
return self
[docs]
def approxSize(self):
return self._approx_size
[docs]
def finalize(self, finalizer):
return self
@define
[docs]
class ScalableArray(ResultBase):
[docs]
array: ak.Array | dak.Array | np.ndarray
[docs]
def __iadd__(self, other):
if isinstance(self.array, np.ndarray):
self.array = np.concatenate([self.array, other.array], axis=0)
return self
[docs]
def summary(self):
return BasicSummary(name=self.name, approx_size=self.approxSize())
[docs]
def approxSize(self):
return getArrayMem(self.array)
[docs]
def iscale(self, value):
self.array *= value
return self
[docs]
def finalize(self, finalizer):
self.array = finalizer(self.array)
@define
[docs]
class RawArray(ResultBase):
[docs]
array: ak.Array | dak.Array | np.ndarray
[docs]
def __iadd__(self, other):
if isinstance(self.array, np.ndarray):
self.array = np.concatenate([self.array, other.array], axis=0)
return self
[docs]
def iscale(self, value):
return self
[docs]
def finalize(self, finalizer):
self.array = finalizer(self.array)
[docs]
def summary(self):
return BasicSummary(name=self.name, approx_size=self.approxSize())
[docs]
def approxSize(self):
return getArrayMem(self.array)
@define
[docs]
class SavedColumns(ResultBase):
[docs]
data: dict[str, ak.Array | dak.Array | np.ndarray]
[docs]
def __iadd__(self, other):
if set(self.data) != set(other.data):
raise RuntimeError()
for k in self.data:
self.data[k] = np.concatenate([self.data[k], other.data[k]], axis=0)
return self
[docs]
def iscale(self, value):
self.data["Scale"] = np.ones_like(next(iter(self.data.values()))) * value
return self
[docs]
def finalize(self, finalizer):
for k in self.data:
self.data[k] = finalizer(self.data[k])
return self
[docs]
def summary(self):
return BasicSummary(name=self.name, approx_size=self.approxSize())
[docs]
def approxSize(self):
return sum(getArrayMem(x) for x in self.data.values())
[docs]
Scalar = dak.Scalar | numbers.Real
@define
[docs]
class SelectionFlow(ResultBase):
[docs]
cutflow: dict[str, Scalar]
[docs]
n_minus_one: dict[str, Scalar] | None = None
[docs]
one_cut: dict[str, Scalar] | None = None
[docs]
def approxSize(self):
return 30 * len(self.cuts)
[docs]
def __iadd__(self, other):
if self.cuts != other.cuts:
raise RuntimeError()
for x in self.cutflow:
self.cutflow[x] = self.cutflow[x] + other.cutflow[x]
if self.n_minus_one is not None:
for x in self.n_minus_one:
self.n_minus_one[x] = self.n_minus_one[x] + other.n_minus_one[x]
if self.one_cut is not None:
for x in self.one_cut:
self.one_cut[x] = self.one_cut[x] + other.one_cut[x]
return self
[docs]
def iscale(self, value):
for x in self.cutflow:
self.cutflow[x] = value * self.cutflow[x]
if self.n_minus_one is not None:
for x in self.n_minus_one:
self.n_minus_one[x] = value * self.n_minus_one[x]
if self.one_cut is not None:
for x in self.one_cut:
self.one_cut[x] = value * self.one_cut[x]
return self
[docs]
def finalize(self, finalizer):
pass
@define
@define
[docs]
class SavedFiles(ResultBase):
[docs]
saved_files: list[SavedEventFile]
[docs]
def approxSize(self):
return 200 * len(self.saved_files)
[docs]
def __iadd__(self, other):
self.saved_files += other.saved_files
return self
[docs]
def iscale(self, value):
return self
[docs]
def finalize(self, finalizer):
pass
@define
[docs]
class RawEventCount(ResultBase):
[docs]
def __iadd__(self, other):
self.count += other.count
return self
[docs]
def approxSize(self):
return 8
[docs]
def iscale(self, value):
return self
[docs]
def finalize(self, finalizer):
pass
@define
[docs]
class ScaledEventCount(ResultBase):
[docs]
def approxSize(self):
return 8
[docs]
def __iadd__(self, other):
self.count += other.count
return self
[docs]
def iscale(self, value):
self.count *= value
return self
[docs]
def finalize(self, finalizer):
pass
@define
[docs]
class RawSelectionFlow(ResultBase):
[docs]
cutflow: dict[str, Scalar]
[docs]
n_minus_one: dict[str, Scalar]
[docs]
one_cut: dict[str, Scalar]
[docs]
def approxSize(self):
return 30 * len(self.cuts)
[docs]
def __iadd__(self, other):
if self.cuts != other.cuts:
raise RuntimeError()
for x in self.cutflow:
self.cutflow[x] = self.cutflow[x] + other.cutflow[x]
for x in self.n_minus_one:
self.n_minus_one[x] = self.n_minus_one[x] + other.n_minus_one[x]
for x in self.one_cut:
self.one_cut[x] = self.one_cut[x] + other.one_cut[x]
return self
[docs]
def iscale(self, value):
return self
[docs]
def finalize(self, finalizer):
pass
configureConverter(converter)
[docs]
def iFilterResultGroup(rg, keep_patterns, current_path=None):
from fnmatch import fnmatch
if current_path is None:
current_path = ()
new_results = {}
for k, v in rg.results.items():
sub_path = current_path + (k,)
if k.startswith("_"):
new_results[k] = v
continue
if isinstance(v, ResultGroup):
filtered_v = iFilterResultGroup(v, keep_patterns, sub_path)
if filtered_v.results:
new_results[k] = filtered_v
else:
keep = False
for pattern in keep_patterns:
if len(sub_path) == len(pattern) and all(
fnmatch(sp, p) for sp, p in zip(sub_path, pattern)
):
keep = True
break
if keep:
new_results[k] = v
rg.results = new_results
return rg
[docs]
def loadResults(paths, peek_only=False, keep_patterns=None, return_file_sizes=False):
all_paths = paths
ret = None
file_sizes = {}
func = ResultGroup.peekBytes if peek_only else ResultGroup.fromBytes
used_paths = set()
for p in progbar(iterPaths(all_paths)):
if p in used_paths:
continue
used_paths.add(p)
with open(p, "rb") as f:
result = func(f.read())
if keep_patterns is not None:
iFilterResultGroup(result, keep_patterns)
for r in result.results.values():
r.addMetadataRecursive({"source_file": str(p)})
if return_file_sizes:
file_sizes[str(p)] = result.approxSize()
if ret is None:
ret = result
else:
ret += result
if return_file_sizes:
return ret, file_sizes
else:
return ret
[docs]
def mergeAndScale(results, drop_sample_pattern=None):
for dataset, meta in globWithMeta(results, ["*"]):
total = None
for s in dataset:
if drop_sample_pattern is not None:
item, meta = getWithMeta(results, [dataset.name, s])
if drop_sample_pattern.match(meta):
logger.info(
f"Dropping sample {meta['dataset_name']}--{meta['sample_name']}"
)
continue
sample_data = dataset[s]
s_meta = sample_data.metadata
provenance = sample_data["_provenance"]
processed_events = provenance.chunked_events
# print(f"{s_meta['sample_name'] = }")
# print(f"{processed_events = }")
# print(f"{s_meta['n_events'] = }")
if s_meta["sample_type"] == "MC":
lumi = s_meta["era"]["lumi"]
xs = s_meta["x_sec"]
scale = lumi * xs / processed_events
sample_data.iscale(scale)
elif s_meta["sample_type"] == "Data":
expected_nevents = s_meta["n_events"]
sample_data.iscale(expected_nevents / processed_events)
if total is None:
total = sample_data
else:
total += sample_data
merged_metadata = commonDict(dataset[x] for x in dataset)
total.name = dataset.name
total._metadata = merged_metadata
results.addResult(total)
return results
@define
[docs]
class ResultStatus:
@property
[docs]
def frac_complete(self):
return self.events_found / self.events_expected
[docs]
def checkResults(paths):
results = loadResults(paths, peek_only=True)
ret = []
for prov, meta in globWithMeta(results, ["*", "*", "_provenance"]):
expected = meta["n_events"]
found = prov.chunked_events
dataset_name = meta["dataset_name"]
sample_name = meta["sample_name"]
ret.append(ResultStatus(dataset_name, sample_name, expected, found))
return ret