from __future__ import annotations
import copy
import itertools as it
import functools as ft
import operator as op
from analyzer.postprocessing.style import Style
from analyzer.core.results import Histogram
import numpy as np
from analyzer.utils.structure_tools import dictToDot, dotFormat
import hist
from collections import OrderedDict
from analyzer.utils.querying import BasePattern
from analyzer.utils.structure_tools import ItemWithMeta, commonDict, addChain
from attrs import define, field, asdict
from .registry import TransformHistogram
@define
[docs]
class SelectAxesValues(TransformHistogram):
[docs]
select_axes_values: dict[str, list[int] | list[str] | list[float]]
[docs]
def __call__(self, items: list[ItemWithMeta]):
ret = []
for item, meta in items:
h = item.histogram
keys_vals = list(self.select_axes_values.items())
keys, vals = list(zip(*keys_vals))
# new_axes = [x for x in item.axes if x.name not in select_axes_values]
for p in it.product(*vals):
u = dict(zip(keys, p))
new_meta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), u)},
)
u = dict(zip(keys, [hist.loc(x) for x in p]))
ret.append(
ItemWithMeta(
Histogram(name=item.name, axes=[], histogram=h[u]), new_meta
)
)
return ret
@define
[docs]
class MergeAxes(TransformHistogram):
[docs]
merge_axis_names: list[str | int]
[docs]
def __call__(self, items):
ret = []
for item, meta in items:
h = item.histogram
merging = {x: slice(0, len(h.axes[x]), sum) for x in self.merge_axis_names}
h = h[merging]
ret.append(
ItemWithMeta(Histogram(name=item.name, axes=[], histogram=h), meta)
)
return ret
@define
[docs]
class SplitAxes(TransformHistogram):
[docs]
split_axis_names: list[str]
[docs]
limit_pattern: BasePattern | None = None
[docs]
def __call__(self, items):
import hist
ret = []
for ph, meta in items:
h = ph.histogram
split_axes = [h.axes[a] for a in self.split_axis_names]
def passedPattern(name, val):
if self.limit_pattern is not None:
if isinstance(self.limit_pattern, dict):
return self.limit_pattern[name].match(val)
else:
return self.limit_pattern.match(val)
return True
possible_values = OrderedDict(
{
(x.name): [y for y in x if passedPattern(x.name, y)]
for x in split_axes
}
)
labels = [(x.name or x.label) for x in split_axes]
all_hists = {
x: h[dict(zip(possible_values.keys(), map(hist.loc, x)))]
for x in it.product(*possible_values.values())
}
for values, split_hist in all_hists.items():
axis_values = dict(zip(labels, values))
newmeta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), axis_values)},
)
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=None, histogram=split_hist),
newmeta,
)
)
return ret
@define
[docs]
class SumHistograms(TransformHistogram):
[docs]
sum_match_pattern: BasePattern
[docs]
def __call__(self, items):
to_sum = []
ret = []
for itemmeta in items:
if self.sum_match_pattern.match(itemmeta.metadata):
to_sum.append(itemmeta)
else:
ret.append(itemmeta)
if to_sum:
total_hist = ft.reduce(op.add, [x.item.histogram for x in to_sum])
new_meta = commonDict(to_sum)
new_meta = addChain(new_meta, self.new_meta_fields)
ret.append(
ItemWithMeta(
Histogram(
name=new_meta["name"],
axes=to_sum[0].item.axes,
histogram=total_hist,
),
new_meta,
)
)
return ret
@define
[docs]
class StatMaker(TransformHistogram):
[docs]
def __call__(self, items: list[ItemWithMeta]):
import scipy.stats
ret = []
for item, meta in items:
h = item.histogram
distribution = scipy.stats.rv_histogram(h.to_numpy())
stats = {
"integral": h.sum().value,
"integral_flow": h.sum(flow=True).value,
"mean": distribution.mean(),
"median": distribution.median(),
"std": distribution.std(),
"var": distribution.var(),
}
stats = {x: f"{y:.2g}" for x, y in stats.items()}
new_meta = addChain(meta, {"stats": stats})
ret.append(ItemWithMeta(item, new_meta))
return ret
@define
[docs]
class NormalizeSystematicByProjection(TransformHistogram):
[docs]
normalize_within: list[str]
# post_sf_name: str
[docs]
variation_axis: str = "variation"
[docs]
def __call__(self, items):
ret = []
for ph, meta in items:
h = ph.histogram.copy(deep=True)
v_idx = h.axes.name.index(self.variation_axis)
names = [x for x in h.axes[self.variation_axis] if x != self.pre_sf_name]
for post_sf_name in names:
pre_idx = h.axes[self.variation_axis].index(self.pre_sf_name)
post_idx = h.axes[self.variation_axis].index(post_sf_name)
view = h.view(flow=True)
slices_pre, slices_post = (
[slice(None)] * view.ndim,
[slice(None)] * view.ndim,
)
slices_pre[v_idx], slices_post[v_idx] = pre_idx, post_idx
pre_view, post_view = view[tuple(slices_pre)], view[tuple(slices_post)]
axes_to_sum = tuple(
i
for i, a in enumerate(
a for a in h.axes if a.name != self.variation_axis
)
if a.name not in self.normalize_within
)
if axes_to_sum:
pre_sum = pre_view["value"].sum(axis=axes_to_sum)
post_sum = post_view["value"].sum(axis=axes_to_sum)
else:
pre_sum = pre_view["value"]
post_sum = post_view["value"]
scale = np.divide(
pre_sum,
post_sum,
out=np.zeros_like(pre_sum, dtype=float),
where=post_sum != 0,
)
broadcast_shape = []
scale_idx = 0
for a in h.axes:
if a.name == self.variation_axis:
continue
if a.name in self.normalize_within:
broadcast_shape.append(scale.shape[scale_idx])
scale_idx += 1
else:
broadcast_shape.append(1)
scale = scale.reshape(broadcast_shape)
post_view["value"] *= scale
post_view["variance"] *= scale**2
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=ph.axes, histogram=h), metadata=meta
)
)
return ret
@define
[docs]
class OrBinaryAxes(TransformHistogram):
[docs]
or_axis_names: list[str]
[docs]
def __call__(self, items):
ret = []
for ph, meta in items:
h = ph.histogram
to_add = []
sor = set(self.or_axis_names)
for i in range(1, 1 + len(self.or_axis_names)):
for c in it.combinations(self.or_axis_names, i):
d = {x: 1 for x in c} | {x: 0 for x in sor - set(c)}
to_add.append(h[d])
h = sum(to_add)
axis_values = {x: "OR" for x in self.or_axis_names}
newmeta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), axis_values)},
)
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=ph.axes, histogram=h), metadata=newmeta
)
)
return ret
@define
[docs]
class RebinAxes(TransformHistogram):
[docs]
rebin: int | dict[str, int]
[docs]
def __call__(self, items):
ret = []
for ph, meta in items:
h = ph.histogram
if isinstance(self.rebin, dict):
rebins = {x: hist.rebin(y) for x, y in self.rebin.items()}
else:
rebins = {x.name: hist.rebin(self.rebin) for x in h.axes}
h = h[rebins]
newmeta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), rebins)},
)
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=ph.axes, histogram=h), metadata=newmeta
)
)
return ret
@define
[docs]
class SliceAxes(TransformHistogram):
[docs]
slices: dict[str | int, tuple[int | float | None, int | float | None]]
[docs]
def __call__(self, items):
ret = []
for ph, meta in items:
h = ph.histogram
slices = {
x: slice(*(hist.loc(y) if y else y for y in z))
for x, z in self.slices.items()
}
h = h[slices]
newmeta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), slices)},
)
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=ph.axes, histogram=h), metadata=newmeta
)
)
return ret
@define
[docs]
class MultiSliceAxes(TransformHistogram):
[docs]
multi_slices: dict[str | int, tuple[float, float, int]]
[docs]
def __call__(self, items):
ret = []
def makePairs(pair):
return np.stack([pair[:-1], pair[1:]], axis=1)
for ph, meta in items:
h = ph.histogram
chopping = OrderedDict(self.multi_slices)
names, vals = list(chopping.keys()), list(chopping.values())
pairs = [makePairs(np.arange(*x)) for x in vals]
for ranges in it.combinations(*pairs):
slices = {
x: slice(*(hist.loc(y) for y in z)) for x, z in zip(names, ranges)
}
hnew = h[slices]
newmeta = addChain(
meta,
{"axis_params": addChain(meta.get("axis_params", {}), slices)},
)
ret.append(
ItemWithMeta(
Histogram(name=ph.name, axes=ph.axes, histogram=hnew),
metadata=newmeta,
)
)
return ret
@define
@define
[docs]
class SetStyle(TransformHistogram):
[docs]
def __call__(self, histograms):
ret = []
for ph, meta in histograms:
meta = addChain(meta, {"style": asdict(self.style)})
ret.append(ItemWithMeta(ph, metadata=meta))
return ret
@define