diff --git a/debug/.ipynb_checkpoints/Counter-checkpoint.ipynb b/debug/.ipynb_checkpoints/Counter-checkpoint.ipynb new file mode 100644 index 0000000..363fcab --- /dev/null +++ b/debug/.ipynb_checkpoints/Counter-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/debug/Counter.ipynb b/debug/Counter.ipynb new file mode 100644 index 0000000..6b67a65 --- /dev/null +++ b/debug/Counter.ipynb @@ -0,0 +1,115 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "538a4798-95f5-4116-ab6f-f411f9db9fa7", + "metadata": {}, + "outputs": [], + "source": [ + "import yoda" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "698a0a2e-b833-405f-a0fc-b53cf1138653", + "metadata": {}, + "outputs": [], + "source": [ + "c = yoda.Counter(title=\"hi\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1a8a28c1-7f1b-4f67-8bd8-cbd93ff3a981", + "metadata": {}, + "outputs": [], + "source": [ + "yoda.write([c], \"test_counter_v2.yoda\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ee895f9f-c60e-4a79-bd2e-e4e474cb7ace", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'path': '/', 'title': 'hi', 'type': 'Counter'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c.annotationsDict()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "019e131e-b9ec-409d-aeb3-2800e3c01f77", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Path', 'Title', 'Type']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c.annotations()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "82a60bfd-dbae-4ea5-9983-418c9c38acf4", + "metadata": {}, + "outputs": [], + "source": [ + "c.annotation(\"path\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3a7db0c-4307-40ef-98dd-3c9683c8d99e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/debug/test_counter.yoda b/debug/test_counter.yoda new file mode 100644 index 0000000..c438d50 --- /dev/null +++ b/debug/test_counter.yoda @@ -0,0 +1,8 @@ +BEGIN YODA_COUNTER_V3 / +Path: / +Title: hi +Type: Counter +--- +# sumW sumW2 numEntries +0.000000e+00 0.000000e+00 0.000000e+00 +END YODA_COUNTER_V3 diff --git a/debug/test_counter_v2.yoda b/debug/test_counter_v2.yoda new file mode 100644 index 0000000..c438d50 --- /dev/null +++ b/debug/test_counter_v2.yoda @@ -0,0 +1,8 @@ +BEGIN YODA_COUNTER_V3 / +Path: / +Title: hi +Type: Counter +--- +# sumW sumW2 numEntries +0.000000e+00 0.000000e+00 0.000000e+00 +END YODA_COUNTER_V3 diff --git a/debug/test_counter_v3.yoda b/debug/test_counter_v3.yoda new file mode 100644 index 0000000..c438d50 --- /dev/null +++ b/debug/test_counter_v3.yoda @@ -0,0 +1,8 @@ +BEGIN YODA_COUNTER_V3 / +Path: / +Title: hi +Type: Counter +--- +# sumW sumW2 numEntries +0.000000e+00 0.000000e+00 0.000000e+00 +END YODA_COUNTER_V3 diff --git a/src/babyyoda/__init__.py b/src/babyyoda/__init__.py index 36e256e..bc7e2bf 100644 --- a/src/babyyoda/__init__.py +++ b/src/babyyoda/__init__.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT from ._version import version as __version__ +from .counter import UHICounter from .histo1d import UHIHisto1D from .histo2d import UHIHisto2D from .read import read, read_grogu, read_yoda @@ -11,6 +12,7 @@ __all__ = [ "__version__", + "UHICounter", "UHIHisto1D", "UHIHisto2D", "read", diff --git a/src/babyyoda/analysisobject.py b/src/babyyoda/analysisobject.py new file mode 100644 index 0000000..dc39190 --- /dev/null +++ b/src/babyyoda/analysisobject.py @@ -0,0 +1,3 @@ +class UHIAnalysisObject: + def key(self): + return self.path() diff --git a/src/babyyoda/counter.py b/src/babyyoda/counter.py new file mode 100644 index 0000000..5bc0047 --- /dev/null +++ b/src/babyyoda/counter.py @@ -0,0 +1,115 @@ +import contextlib + +from babyyoda.analysisobject import UHIAnalysisObject + + +def set_bin0d(target, source): + if hasattr(target, "set"): + target.set( + source.numEntries(), + [source.sumW()], + [source.sumW2()], + ) + else: + err = "YODA1 backend can not set bin values" + raise NotImplementedError(err) + + +def Counter(*args, **kwargs): + """ + Automatically select the correct version of the Histo1D class + """ + try: + from babyyoda import yoda + except ImportError: + import babyyoda.grogu as yoda + return yoda.Counter(*args, **kwargs) + + +# TODO make this implementation independent (no V2 or V3...) +class UHICounter(UHIAnalysisObject): + ###### + # BACKENDS + ###### + + def to_grogu_v2(self): + from babyyoda.grogu.counter_v2 import GROGU_COUNTER_V2 + + return GROGU_COUNTER_V2( + d_key=self.key(), + d_annotations=self.annotationsDict(), + d_bins=[ + GROGU_COUNTER_V2.Bin( + d_sumw=self.sumW(), + d_sumw2=self.sumW2(), + d_numentries=self.numEntries(), + ) + ], + ) + + def to_grogu_v3(self): + from babyyoda.grogu.counter_v3 import GROGU_COUNTER_V3 + + return GROGU_COUNTER_V3( + d_key=self.key(), + d_annotations=self.annotationsDict(), + d_bins=[ + GROGU_COUNTER_V3.Bin( + d_sumw=self.sumW(), + d_sumw2=self.sumW2(), + d_numentries=self.numEntries(), + ) + ], + ) + + def to_yoda_v3(self): + err = "Not implemented yet" + raise NotImplementedError(err) + + def to_string(self): + # Now we need to map YODA to grogu and then call to_string + # TODO do we want to hardcode v3 here? + return self.to_grogu_v3().to_string() + + ######################################################## + # YODA compatibility code (dropped legacy code?) + ######################################################## + + ######################################################## + # Generic UHI code + ######################################################## + + @property + def axes(self): + return [] + + @property + def kind(self): + # TODO reeavaluate this + return "COUNT" + + def counts(self): + return self.numEntries() + + def values(self): + return self.sumW() + + def variances(self): + return self.sumW2() + + def plot(self, *args, binwnorm=1.0, **kwargs): + import mplhep as hep + + hep.histplot( + self, + *args, + yerr=self.variances() ** 0.5, + w2method="sqrt", + binwnorm=binwnorm, + **kwargs, + ) + + def _ipython_display_(self): + with contextlib.suppress(ImportError): + self.plot() + return self diff --git a/src/babyyoda/grogu/__init__.py b/src/babyyoda/grogu/__init__.py index 3ad8d0a..96d0494 100644 --- a/src/babyyoda/grogu/__init__.py +++ b/src/babyyoda/grogu/__init__.py @@ -1,3 +1,5 @@ +from babyyoda.grogu.counter_v2 import GROGU_COUNTER_V2 +from babyyoda.grogu.counter_v3 import GROGU_COUNTER_V3 from babyyoda.grogu.histo1d_v3 import GROGU_HISTO1D_V3 from babyyoda.grogu.histo2d_v2 import GROGU_HISTO2D_V2 from babyyoda.grogu.histo2d_v3 import GROGU_HISTO2D_V3 @@ -9,6 +11,26 @@ __all__ = ["read", "write"] +def Counter(title=None, **kwargs): + return Counter_v3(title=title, **kwargs) + + +def Counter_v3(title=None, **kwargs): + return GROGU_COUNTER_V3( + d_bins=[GROGU_COUNTER_V3.Bin()], + d_annotations={"Title": title} if title else {}, + **kwargs, + ) + + +def Counter_v2(title=None, **kwargs): + return GROGU_COUNTER_V2( + d_bins=[GROGU_COUNTER_V2.Bin()], + d_annotations={"Title": title} if title else {}, + **kwargs, + ) + + def Histo1D(nbins: int, start: float, end: float, title=None, **kwargs): return Histo1D_v2(nbins=nbins, start=start, end=end, title=title, **kwargs) @@ -25,7 +47,7 @@ def Histo1D_v2(nbins: int, start: float, end: float, title=None, **kwargs): d_overflow=GROGU_HISTO1D_V2.Bin(), d_underflow=GROGU_HISTO1D_V2.Bin(), d_total=GROGU_HISTO1D_V2.Bin(), - d_title=title, + d_annotations={"Title": title} if title else {}, **kwargs, ) @@ -37,7 +59,7 @@ def Histo1D_v3(nbins: int, start: float, end: float, title=None, **kwargs): GROGU_HISTO1D_V3.Bin() for i in range(nbins + 2) # add overflow and underflow ], - d_title=title, + d_annotations={"Title": title} if title else {}, **kwargs, ) @@ -86,7 +108,7 @@ def Histo2D_v2( for j in range(nybins) ], d_total=GROGU_HISTO2D_V2.Bin(), - d_title=title, + d_annotations={"Title": title} if title else {}, **kwargs, ) @@ -110,6 +132,6 @@ def Histo2D_v3( GROGU_HISTO2D_V3.Bin() for _ in range((nxbins + 2) * (nybins + 2)) # add overflow and underflow ], - d_title=title, + d_annotations={"Title": title} if title else {}, **kwargs, ) diff --git a/src/babyyoda/grogu/analysis_object.py b/src/babyyoda/grogu/analysis_object.py index 203520f..89c47f8 100644 --- a/src/babyyoda/grogu/analysis_object.py +++ b/src/babyyoda/grogu/analysis_object.py @@ -1,18 +1,18 @@ -from dataclasses import dataclass -from typing import Optional +import re +from dataclasses import dataclass, field @dataclass class GROGU_ANALYSIS_OBJECT: + d_annotations: dict = field(default_factory=dict) + # TODO add anotations d_key: str = "" - # d_name: str = "" - d_type: str = "" - d_title: str = "" - d_path: str = "/" - d_scaled_by: Optional[float] = ( - 1.0 # TODO maybe we want to track ScaledBy in the future - ) - # TODO how do I access anotation in YODA python interface for same scaledby treatment + + def __post_init__(self): + if "Path" not in self.d_annotations: + self.d_annotations["Path"] = "/" + if "Title" not in self.d_annotations: + self.d_annotations["Title"] = "" ############################################ # YODA compatibilty code @@ -21,14 +21,61 @@ class GROGU_ANALYSIS_OBJECT: def key(self): return self.d_key - def path(self): - return self.d_path - def name(self): return self.path().split("/")[-1] + def path(self): + p = self.annotation("Path") + return p if p else "/" + def title(self): - return self.d_title + return self.annotation("Title") def type(self): - return self.d_type + return self.annotation("Type") + + def annotations(self): + return self.d_annotations.keys() + + def annotation(self, k: str, default=None) -> str: + return self.d_annotations.get(k, default) + + def setAnnotation(self, key: str, value: str): + self.d_annotations[key] = value + + def clearAnnotations(self): + self.d_annotations = {} + + def hasAnnotation(self, key: str) -> bool: + return key in self.d_annotations + + def annotationsDict(self): + return self.d_annotations + + @classmethod + def from_string(cls, file_content: str) -> "GROGU_ANALYSIS_OBJECT": + lines = file_content.strip().splitlines() + # Extract metadata (path, title) + annotations = {"Path": "/"} + pattern = re.compile(r"(\S+): (.+)") + for line in lines: + pattern_match = pattern.match(line) + if pattern_match: + annotations[pattern_match.group(1).strip()] = pattern_match.group( + 2 + ).strip() + elif line.startswith("---"): + break + + print(annotations) + return cls( + d_annotations=annotations, + d_key=annotations.get("Path", ""), + ) + + def to_string(self): + ret = "" + for k, v in self.d_annotations.items(): + ret += f"{k}: {v}\n" + print(f"ret: {ret}") + return ret diff --git a/src/babyyoda/grogu/counter_v2.py b/src/babyyoda/grogu/counter_v2.py new file mode 100644 index 0000000..2d6381e --- /dev/null +++ b/src/babyyoda/grogu/counter_v2.py @@ -0,0 +1,206 @@ +import re +from dataclasses import dataclass, field +from typing import Union + +from babyyoda.counter import UHICounter +from babyyoda.grogu.analysis_object import GROGU_ANALYSIS_OBJECT + + +@dataclass +class GROGU_COUNTER_V2(GROGU_ANALYSIS_OBJECT, UHICounter): + @dataclass + class Bin: + d_sumw: float = 0.0 + d_sumw2: float = 0.0 + d_numentries: float = 0.0 + + ######################################################## + # YODA compatibilty code + ######################################################## + + def clone(self): + return GROGU_COUNTER_V2.Bin( + d_sumw=self.d_sumw, + d_sumw2=self.d_sumw2, + d_numentries=self.d_numentries, + ) + + def fill(self, weight: float = 1.0, fraction: float = 1.0) -> bool: + sf = fraction * weight + self.d_sumw += sf + self.d_sumw2 += sf * weight + self.d_numentries += fraction + + def set_bin(self, bin): + self.d_sumw = bin.sumW() + self.d_sumw2 = bin.sumW2() + self.d_numentries = bin.numEntries() + + def set( + self, + numEntries: float, + sumW: Union[list[float], float], + sumW2: Union[list[float], float], + ): + if isinstance(sumW, float): + sumW = [sumW] + if isinstance(sumW2, float): + sumW2 = [sumW2] + assert len(sumW) == 1 + assert len(sumW2) == 1 + self.d_sumw = sumW[0] + self.d_sumw2 = sumW2[0] + self.d_numentries = numEntries + + def sumW(self): + return self.d_sumw + + def sumW2(self): + return self.d_sumw2 + + def variance(self): + if self.d_sumw**2 - self.d_sumw2 == 0: + return 0 + return abs( + (self.d_sumw2 * self.d_sumw - self.d_sumw**2) + / (self.d_sumw**2 - self.d_sumw2) + ) + # return self.d_sumw2/self.d_numentries - (self.d_sumw/self.d_numentries)**2 + + def errW(self): + return self.d_sumw2**0.5 + + def stdDev(self): + return self.variance() ** 0.5 + + def effNumEntries(self): + return self.sumW() ** 2 / self.sumW2() + + def stdErr(self): + return self.stdDev() / self.effNumEntries() ** 0.5 + + def numEntries(self): + return self.d_numentries + + def __eq__(self, other): + return ( + isinstance(other, GROGU_COUNTER_V2.Bin) + and self.d_sumw == other.d_sumw + and self.d_sumw2 == other.d_sumw2 + and self.d_numentries == other.d_numentries + ) + + def __add__(self, other): + assert isinstance(other, GROGU_COUNTER_V2.Bin) + return GROGU_COUNTER_V2.Bin( + self.d_sumw + other.d_sumw, + self.d_sumw2 + other.d_sumw2, + self.d_numentries + other.d_numentries, + ) + + def to_string(self) -> str: + """Convert a CounterBin object to a formatted string.""" + return f"{self.d_sumw:<12.6e}\t{self.d_sumw2:<12.6e}\t{self.d_numentries:<12.6e}".strip() + + @classmethod + def from_string(cls, string: str) -> "GROGU_COUNTER_V2.Bin": + values = re.split(r"\s+", string.strip()) + # Regular bin + sumw, sumw2, numEntries = map(float, values) + return cls(sumw, sumw2, numEntries) + + d_bins: list[Bin] = field(default_factory=list) + + def __post_init__(self): + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Counter") + assert len(self.d_bins) == 1 + + ############################################ + # YODA compatibilty code + ############################################ + + def sumW(self): + return self.d_bins[0].sumW() + + def sumW2(self): + return self.d_bins[0].sumW2() + + def numEntries(self): + return self.d_bins[0].numEntries() + + def clone(self): + return GROGU_COUNTER_V2( + d_key=self.d_key, + d_annotations=self.annotationsDict(), + d_bins=[b.clone() for b in self.d_bins], + ) + + def fill(self, weight=1.0, fraction=1.0): + for b in self.bins(): + b.fill(weight=weight, fraction=fraction) + + def set(self, *args, **kwargs): + self.d_bins[0].set(*args, **kwargs) + + def bins(self): + return self.d_bins + + @classmethod + def from_string(cls, file_content: str) -> "GROGU_COUNTER_V2": + lines = file_content.strip().splitlines() + key = "" + if find := re.search(r"BEGIN YODA_COUNTER_V2 (\S+)", lines[0]): + key = find.group(1) + + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations + + # Extract bins and overflow/underflow + bins = [] + # edges = [] + data_section_started = False + + for line in lines: + if line.startswith("BEGIN YODA_COUNTER_V2"): + continue + if line.startswith("END YODA_COUNTER_V2"): + break + if line.startswith("#") or line.isspace(): + continue + if line.startswith("---"): + data_section_started = True + continue + if not data_section_started: + continue + + bins.append(cls.Bin.from_string(line)) + + return cls( + d_key=key, + d_annotations=annotations, + d_bins=bins, + ) + + def to_string(self): + """Convert a YODA_COUNTER_V2 object to a formatted string.""" + header = ( + f"BEGIN YODA_COUNTER_V2 {self.d_key}\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(self)}" + "---\n" + ) + + # Add the sumw and other info (we assume it's present in the metadata but you could also compute) + stats = ( + "" # f"# Mean: {self.xMean():.6e}\n" f"# Integral: {self.integral():.6e}\n" + ) + + # listed = ", ".join(f"{float(val):.6e}" for val in self.d_edges) + # edges = f"Edges(A1): [{listed}]\n" + # Add the bin data + bin_data = "\n".join(GROGU_COUNTER_V2.Bin.to_string(b) for b in self.bins()) + + footer = "END YODA_COUNTER_V2" + + return f"{header}{stats}# sumW\t sumW2\t numEntries\n{bin_data}\n{footer}" diff --git a/src/babyyoda/grogu/counter_v3.py b/src/babyyoda/grogu/counter_v3.py new file mode 100644 index 0000000..a2e3e4f --- /dev/null +++ b/src/babyyoda/grogu/counter_v3.py @@ -0,0 +1,206 @@ +import re +from dataclasses import dataclass, field +from typing import Union + +from babyyoda.counter import UHICounter +from babyyoda.grogu.analysis_object import GROGU_ANALYSIS_OBJECT + + +@dataclass +class GROGU_COUNTER_V3(GROGU_ANALYSIS_OBJECT, UHICounter): + @dataclass + class Bin: + d_sumw: float = 0.0 + d_sumw2: float = 0.0 + d_numentries: float = 0.0 + + ######################################################## + # YODA compatibilty code + ######################################################## + + def clone(self): + return GROGU_COUNTER_V3.Bin( + d_sumw=self.d_sumw, + d_sumw2=self.d_sumw2, + d_numentries=self.d_numentries, + ) + + def fill(self, weight: float = 1.0, fraction: float = 1.0) -> bool: + sf = fraction * weight + self.d_sumw += sf + self.d_sumw2 += sf * weight + self.d_numentries += fraction + + def set_bin(self, bin): + self.d_sumw = bin.sumW() + self.d_sumw2 = bin.sumW2() + self.d_numentries = bin.numEntries() + + def set( + self, + numEntries: float, + sumW: Union[list[float], float], + sumW2: Union[list[float], float], + ): + if isinstance(sumW, float): + sumW = [sumW] + if isinstance(sumW2, float): + sumW2 = [sumW2] + assert len(sumW) == 1 + assert len(sumW2) == 1 + self.d_sumw = sumW[0] + self.d_sumw2 = sumW2[0] + self.d_numentries = numEntries + + def sumW(self): + return self.d_sumw + + def sumW2(self): + return self.d_sumw2 + + def variance(self): + if self.d_sumw**2 - self.d_sumw2 == 0: + return 0 + return abs( + (self.d_sumw2 * self.d_sumw - self.d_sumw**2) + / (self.d_sumw**2 - self.d_sumw2) + ) + # return self.d_sumw2/self.d_numentries - (self.d_sumw/self.d_numentries)**2 + + def errW(self): + return self.d_sumw2**0.5 + + def stdDev(self): + return self.variance() ** 0.5 + + def effNumEntries(self): + return self.sumW() ** 2 / self.sumW2() + + def stdErr(self): + return self.stdDev() / self.effNumEntries() ** 0.5 + + def numEntries(self): + return self.d_numentries + + def __eq__(self, other): + return ( + isinstance(other, GROGU_COUNTER_V3.Bin) + and self.d_sumw == other.d_sumw + and self.d_sumw2 == other.d_sumw2 + and self.d_numentries == other.d_numentries + ) + + def __add__(self, other): + assert isinstance(other, GROGU_COUNTER_V3.Bin) + return GROGU_COUNTER_V3.Bin( + self.d_sumw + other.d_sumw, + self.d_sumw2 + other.d_sumw2, + self.d_numentries + other.d_numentries, + ) + + def to_string(self) -> str: + """Convert a CounterBin object to a formatted string.""" + return f"{self.d_sumw:<13.6e}\t{self.d_sumw2:<13.6e}\t{self.d_numentries:<13.6e}".strip() + + @classmethod + def from_string(cls, string: str) -> "GROGU_COUNTER_V3.Bin": + values = re.split(r"\s+", string.strip()) + # Regular bin + sumw, sumw2, numEntries = map(float, values) + return cls(sumw, sumw2, numEntries) + + d_bins: list[Bin] = field(default_factory=list) + + def __post_init__(self): + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Counter") + assert len(self.d_bins) == 1 + + ############################################ + # YODA compatibilty code + ############################################ + + def sumW(self): + return self.d_bins[0].sumW() + + def sumW2(self): + return self.d_bins[0].sumW2() + + def numEntries(self): + return self.d_bins[0].numEntries() + + def clone(self): + return GROGU_COUNTER_V3( + d_key=self.d_key, + d_annotations=self.annotationsDict(), + d_bins=[b.clone() for b in self.d_bins], + ) + + def fill(self, weight=1.0, fraction=1.0): + for b in self.bins(): + b.fill(weight=weight, fraction=fraction) + + def set(self, *args, **kwargs): + self.d_bins[0].set(*args, **kwargs) + + def bins(self): + return self.d_bins + + @classmethod + def from_string(cls, file_content: str) -> "GROGU_COUNTER_V3": + lines = file_content.strip().splitlines() + key = "" + if find := re.search(r"BEGIN YODA_COUNTER_V3 (\S+)", lines[0]): + key = find.group(1) + + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations + + # Extract bins and overflow/underflow + bins = [] + # edges = [] + data_section_started = False + + for line in lines: + if line.startswith("BEGIN YODA_COUNTER_V3"): + continue + if line.startswith("END YODA_COUNTER_V3"): + break + if line.startswith("#") or line.isspace(): + continue + if line.startswith("---"): + data_section_started = True + continue + if not data_section_started: + continue + + bins.append(cls.Bin.from_string(line)) + + return cls( + d_key=key, + d_annotations=annotations, + d_bins=bins, + ) + + def to_string(self): + """Convert a YODA_COUNTER_V3 object to a formatted string.""" + header = ( + f"BEGIN YODA_COUNTER_V3 {self.d_key}\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(self)}" + "---\n" + ) + + # Add the sumw and other info (we assume it's present in the metadata but you could also compute) + stats = ( + "" # f"# Mean: {self.xMean():.6e}\n" f"# Integral: {self.integral():.6e}\n" + ) + + # listed = ", ".join(f"{float(val):.6e}" for val in self.d_edges) + # edges = f"Edges(A1): [{listed}]\n" + # Add the bin data + bin_data = "\n".join(GROGU_COUNTER_V3.Bin.to_string(b) for b in self.bins()) + + footer = "END YODA_COUNTER_V3" + + return f"{header}{stats}# sumW \tsumW2 \tnumEntries\n{bin_data}\n{footer}" diff --git a/src/babyyoda/grogu/histo1d_v2.py b/src/babyyoda/grogu/histo1d_v2.py index 8d0b00a..31c7147 100644 --- a/src/babyyoda/grogu/histo1d_v2.py +++ b/src/babyyoda/grogu/histo1d_v2.py @@ -187,7 +187,8 @@ def to_string(bin, label=None) -> str: d_total: Optional[Bin] = None def __post_init__(self): - self.d_type = "Histo1D" + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Histo1D") ############################################ # YODA compatibilty code @@ -196,9 +197,7 @@ def __post_init__(self): def clone(self): return GROGU_HISTO1D_V2( d_key=self.d_key, - d_path=self.d_path, - d_scaled_by=self.d_scaled_by, - d_title=self.d_title, + d_annotations=self.annotationsDict(), d_bins=[b.clone() for b in self.d_bins], d_underflow=self.d_underflow, d_overflow=self.d_overflow, @@ -272,15 +271,9 @@ def rebinXTo(self, edges: list[float]): def to_string(histo) -> str: """Convert a YODA_HISTO1D_V2 object to a formatted string.""" - scale = ( - "" if histo.d_scaled_by == 1.0 else f"ScaledBy: {histo.d_scaled_by:.17e}\n" - ) header = ( f"BEGIN YODA_HISTO1D_V2 {histo.d_key}\n" - f"Path: {histo.d_path}\n" - f"{scale}" - f"Title: {histo.d_title}\n" - f"Type: Histo1D\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(histo)}" "---\n" ) @@ -307,19 +300,9 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO1D_V2": if find := re.search(r"BEGIN YODA_HISTO1D_V2 (\S+)", lines[0]): key = find.group(1) - # Extract metadata (path, title) - path = "" - title = "" - scaled_by = 1.0 - for line in lines: - if line.startswith("Path:"): - path = line.split(":")[1].strip() - elif line.startswith("Title:"): - title = line.split(":")[1].strip() - elif line.startswith("ScaledBy:"): - scaled_by = float(line.split(":")[1].strip()) - elif line.startswith("---"): - break + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations # Extract bins and overflow/underflow bins = [] @@ -353,9 +336,7 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO1D_V2": # Create and return the YODA_HISTO1D_V2 object return cls( d_key=key, - d_path=path, - d_title=title, - d_scaled_by=scaled_by, + d_annotations=annotations, d_bins=bins, d_underflow=underflow, d_total=total, diff --git a/src/babyyoda/grogu/histo1d_v3.py b/src/babyyoda/grogu/histo1d_v3.py index e85d490..e0ebc44 100644 --- a/src/babyyoda/grogu/histo1d_v3.py +++ b/src/babyyoda/grogu/histo1d_v3.py @@ -95,9 +95,6 @@ def effNumEntries(self): def stdErr(self): return self.stdDev() / self.effNumEntries() ** 0.5 - def dVol(self): - return self.d_xmax - self.d_xmin - def xVariance(self): # return self.d_sumwx2/self.d_sumw - (self.d_sumwx/self.d_sumw)**2 if self.d_sumw**2 - self.d_sumw2 == 0: @@ -145,7 +142,8 @@ def from_string(cls, string: str) -> "GROGU_HISTO1D_V3.Bin": d_bins: list[Bin] = field(default_factory=list) def __post_init__(self): - self.d_type = "Histo1D" + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Histo1D") # one more edge than bins, subtract 2 for underflow and overflow assert ( len(self.d_edges) == len(self.d_bins) + 1 - 2 @@ -158,9 +156,7 @@ def __post_init__(self): def clone(self): return GROGU_HISTO1D_V3( d_key=self.d_key, - d_path=self.d_path, - d_scaled_by=self.d_scaled_by, - d_title=self.d_title, + d_annotations=self.annotationsDict(), d_edges=copy.deepcopy(self.d_edges), d_bins=[b.clone() for b in self.d_bins], ) @@ -239,19 +235,9 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO1D_V3": if find := re.search(r"BEGIN YODA_HISTO1D_V3 (\S+)", lines[0]): key = find.group(1) - # Extract metadata (path, title) - path = "" - title = "" - scaled_by = 1.0 - for line in lines: - if line.startswith("Path:"): - path = line.split(":")[1].strip() - elif line.startswith("Title:"): - title = line.split(":")[1].strip() - elif line.startswith("ScaledBy:"): - scaled_by = float(line.split(":")[1].strip()) - elif line.startswith("---"): - break + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations # Extract bins and overflow/underflow bins = [] @@ -281,25 +267,17 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO1D_V3": # Create and return the YODA_HISTO1D_V2 object return cls( + d_annotations=annotations, d_key=key, - d_path=path, - d_scaled_by=scaled_by, - d_title=title, d_bins=bins, d_edges=edges, ) def to_string(self): """Convert a YODA_HISTO1D_V3 object to a formatted string.""" - scale = ( - "" if self.d_scaled_by == 1.0 else f"ScaledBy: {self.d_scaled_by:.17e}\n" - ) header = ( f"BEGIN YODA_HISTO1D_V3 {self.d_key}\n" - f"Path: {self.d_path}\n" - f"{scale}" - f"Title: {self.d_title}\n" - f"Type: Histo1D\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(self)}" "---\n" ) diff --git a/src/babyyoda/grogu/histo2d_v2.py b/src/babyyoda/grogu/histo2d_v2.py index bb724a5..7dc4dfa 100644 --- a/src/babyyoda/grogu/histo2d_v2.py +++ b/src/babyyoda/grogu/histo2d_v2.py @@ -139,7 +139,8 @@ def to_string(self, label=None) -> str: d_total: Optional[Bin] = None def __post_init__(self): - self.d_type = "Histo2D" + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Histo2D") # # YODA compatibilty code @@ -148,9 +149,7 @@ def __post_init__(self): def clone(self): return GROGU_HISTO2D_V2( d_key=self.d_key, - d_path=self.d_path, - d_scaled_by=self.d_scaled_by, - d_title=self.d_title, + d_annotations=self.annotationsDict(), d_bins=[b.clone() for b in self.d_bins], d_total=self.d_total.clone(), ) @@ -214,15 +213,9 @@ def binAt(self, x, y): def to_string(self) -> str: """Convert a YODA_HISTO2D_V2 object to a formatted string.""" - scale = ( - "" if self.d_scaled_by == 1.0 else f"ScaledBy: {self.d_scaled_by:.17e}\n" - ) header = ( f"BEGIN YODA_HISTO2D_V2 {self.d_key}\n" - f"Path: {self.d_path}\n" - f"{scale}" - f"Title: {self.d_title}\n" - f"Type: {self.d_type}\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(self)}" f"---\n" ) @@ -250,19 +243,9 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO2D_V2": if find := re.search(r"BEGIN YODA_HISTO2D_V2 (\S+)", lines[0]): key = find.group(1) - # Extract metadata (path, title) - path = "" - title = "" - scaled_by = 1.0 - for line in lines: - if line.startswith("Path:"): - path = line.split(":")[1].strip() - elif line.startswith("Title:"): - title = line.split(":")[1].strip() - elif line.startswith("ScaledBy:"): - scaled_by = float(line.split(":")[1].strip()) - elif line.startswith("---"): - break + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations bins = [] total = None @@ -333,9 +316,7 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO2D_V2": return cls( d_key=key, - d_path=path, - d_scaled_by=scaled_by, - d_title=title, + d_annotations=annotations, d_bins=bins, d_total=total, ) diff --git a/src/babyyoda/grogu/histo2d_v3.py b/src/babyyoda/grogu/histo2d_v3.py index f7e0945..574f4ba 100644 --- a/src/babyyoda/grogu/histo2d_v3.py +++ b/src/babyyoda/grogu/histo2d_v3.py @@ -120,7 +120,8 @@ def from_string(cls, line: str) -> "GROGU_HISTO2D_V3.Bin": d_edges: list[list[float]] = field(default_factory=list) def __post_init__(self): - self.d_type = "Histo2D" + GROGU_ANALYSIS_OBJECT.__post_init__(self) + self.setAnnotation("Type", "Histo2D") # plus 1 for underflow and overflow assert len(self.d_bins) == (len(self.d_edges[0]) + 1) * ( @@ -134,9 +135,7 @@ def __post_init__(self): def clone(self): return GROGU_HISTO2D_V3( d_key=self.d_key, - d_path=self.d_path, - d_scaled_by=self.d_scaled_by, - d_title=self.d_title, + d_annotations=self.annotationsDict(), d_bins=[b.clone() for b in self.d_bins], d_edges=copy.deepcopy(self.d_edges), ) @@ -192,15 +191,9 @@ def bins(self, includeOverflows=False): def to_string(self) -> str: """Convert a YODA_HISTO2D_V3 object to a formatted string.""" - scale = ( - "" if self.d_scaled_by == 1.0 else f"ScaledBy: {self.d_scaled_by:.17e}\n" - ) header = ( f"BEGIN YODA_HISTO2D_V3 {self.d_key}\n" - f"Path: {self.d_path}\n" - f"{scale}" - f"Title: {self.d_title}\n" - f"Type: {self.d_type}\n" + f"{GROGU_ANALYSIS_OBJECT.to_string(self)}" f"---\n" ) @@ -227,19 +220,10 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO2D_V3": key = "" if find := re.search(r"BEGIN YODA_HISTO2D_V3 (\S+)", lines[0]): key = find.group(1) - # Extract metadata (path, title) - path = "" - title = "" - scaled_by = 1.0 - for line in lines: - if line.startswith("Path:"): - path = line.split(":")[1].strip() - elif line.startswith("Title:"): - title = line.split(":")[1].strip() - elif line.startswith("ScaledBy:"): - scaled_by = float(line.split(":")[1].strip()) - elif line.startswith("---"): - break + + annotations = GROGU_ANALYSIS_OBJECT.from_string( + file_content=file_content + ).d_annotations # Extract bins and overflow/underflow bins = [] @@ -270,9 +254,7 @@ def from_string(cls, file_content: str) -> "GROGU_HISTO2D_V3": # Create and return the YODA_HISTO1D_V2 object return cls( d_key=key, - d_path=path, - d_scaled_by=scaled_by, - d_title=title, + d_annotations=annotations, d_bins=bins, d_edges=edges, ) diff --git a/src/babyyoda/grogu/read.py b/src/babyyoda/grogu/read.py index 3d71745..ab88373 100644 --- a/src/babyyoda/grogu/read.py +++ b/src/babyyoda/grogu/read.py @@ -1,6 +1,8 @@ import gzip import re +from babyyoda.grogu.counter_v2 import GROGU_COUNTER_V2 +from babyyoda.grogu.counter_v3 import GROGU_COUNTER_V3 from babyyoda.grogu.histo1d_v2 import GROGU_HISTO1D_V2 from babyyoda.grogu.histo1d_v3 import GROGU_HISTO1D_V3 from babyyoda.grogu.histo2d_v2 import GROGU_HISTO2D_V2 @@ -41,8 +43,14 @@ def read(file_path: str): histograms = {} - for full_match, hist_type, name, body in matches: - if hist_type == "YODA_HISTO1D_V2": + for full_match, hist_type, name, _body in matches: + if hist_type == "YODA_COUNTER_V2": + hist = GROGU_COUNTER_V2.from_string(full_match) + histograms[name] = hist + elif hist_type == "YODA_COUNTER_V3": + hist = GROGU_COUNTER_V3.from_string(full_match) + histograms[name] = hist + elif hist_type == "YODA_HISTO1D_V2": hist = GROGU_HISTO1D_V2.from_string(full_match) histograms[name] = hist elif hist_type == "YODA_HISTO1D_V3": diff --git a/src/babyyoda/histo1d.py b/src/babyyoda/histo1d.py index 443fdd5..df5a1bb 100644 --- a/src/babyyoda/histo1d.py +++ b/src/babyyoda/histo1d.py @@ -3,6 +3,7 @@ import numpy as np +from babyyoda.analysisobject import UHIAnalysisObject from babyyoda.util import loc, overflow, rebin, underflow @@ -33,7 +34,7 @@ def Histo1D(*args, **kwargs): # TODO make this implementation independent (no V2 or V3...) -class UHIHisto1D: +class UHIHisto1D(UHIAnalysisObject): ###### # BACKENDS ###### @@ -43,8 +44,7 @@ def to_grogu_v2(self): return GROGU_HISTO1D_V2( d_key=self.key(), - d_path=self.path(), - d_title=self.title(), + d_annotations=self.annotationsDict(), d_bins=[ GROGU_HISTO1D_V2.Bin( d_xmin=self.xEdges()[i], @@ -82,8 +82,7 @@ def to_grogu_v3(self): return GROGU_HISTO1D_V3( d_key=self.key(), - d_path=self.path(), - d_title=self.title(), + d_annotations=self.annotationsDict(), d_edges=self.xEdges(), d_bins=[ GROGU_HISTO1D_V3.Bin( @@ -304,9 +303,6 @@ def __setitem__(self, slices, value): index = self.__get_index(slices) self.__set_by_index(index, value) - def key(self): - return self.path() - def plot(self, *args, binwnorm=1.0, **kwargs): import mplhep as hep diff --git a/src/babyyoda/histo2d.py b/src/babyyoda/histo2d.py index 4fc81f7..9dc9d98 100644 --- a/src/babyyoda/histo2d.py +++ b/src/babyyoda/histo2d.py @@ -3,6 +3,7 @@ import numpy as np +from babyyoda.analysisobject import UHIAnalysisObject from babyyoda.util import loc, overflow, rebin, underflow @@ -33,7 +34,7 @@ def Histo2D(*args, **kwargs): return yoda.Histo2D(*args, **kwargs) -class UHIHisto2D: +class UHIHisto2D(UHIAnalysisObject): ##### # BACKENDS ##### @@ -43,8 +44,7 @@ def to_grogu_v2(self): return GROGU_HISTO2D_V2( d_key=self.key(), - d_path=self.path(), - d_title=self.title(), + d_annotations=self.annotationsDict(), d_bins=[ GROGU_HISTO2D_V2.Bin( d_xmin=self.xEdges()[i % len(self.xEdges())], @@ -85,8 +85,7 @@ def to_grogu_v3(self): # Fill up with empty overflow bins return GROGU_HISTO2D_V3( d_key=self.key(), - d_path=self.path(), - d_title=self.title(), + d_annotations=self.annotationsDict(), d_edges=[self.xEdges(), self.yEdges()], d_bins=[ GROGU_HISTO2D_V3.Bin( @@ -301,11 +300,6 @@ def __getitem__(self, slices): err = "Invalid argument type" raise TypeError(err) - def key(self): - if hasattr(self.target, "key"): - return self.target.key() - return self.path() - def plot(self, *args, binwnorm=True, **kwargs): import mplhep as hep diff --git a/src/babyyoda/test.py b/src/babyyoda/test.py index 1ceb280..cca902a 100644 --- a/src/babyyoda/test.py +++ b/src/babyyoda/test.py @@ -62,7 +62,7 @@ def equal_histo1d(gh1, yh1): def assert_ao(g, y): assert g.name() == y.name() assert g.path() == y.path() - assert g.title() == y.title() + assert g.title() == y.title(), f"{g.title()} != {y.title()}" assert g.type() == y.type() @@ -72,6 +72,12 @@ def assert_bin1d(gb, yb): assert_value1d(gb, yb) +def assert_value0d(gb, yb): + assert gb.sumW() == yb.sumW(), f"{gb.sumW()} != {yb.sumW()}" + assert gb.sumW2() == yb.sumW2() + assert gb.numEntries() == yb.numEntries() + + def assert_value1d(gb, yb): assert gb.sumW() == yb.sumW() assert gb.sumW2() == yb.sumW2() diff --git a/src/babyyoda/write.py b/src/babyyoda/write.py index 1489c8a..9c8a20d 100644 --- a/src/babyyoda/write.py +++ b/src/babyyoda/write.py @@ -11,7 +11,7 @@ def write(anyhistograms, file_path: str, *args, **kwargs): from babyyoda import yoda for h in listhistograms: - if not (isinstance(h, (yoda.Histo1D, yoda.Histo2D))): + if not (isinstance(h, (yoda.Counter, yoda.Histo1D, yoda.Histo2D))): use_yoda = False break except ImportError: diff --git a/src/babyyoda/yoda/__init__.py b/src/babyyoda/yoda/__init__.py index 919a298..dfb5773 100644 --- a/src/babyyoda/yoda/__init__.py +++ b/src/babyyoda/yoda/__init__.py @@ -1,6 +1,7 @@ +from .counter import Counter from .histo1d import Histo1D from .histo2d import Histo2D from .read import read from .write import write -__all__ = ["Histo1D", "Histo2D", "read", "write"] +__all__ = ["Counter", "Histo1D", "Histo2D", "read", "write"] diff --git a/src/babyyoda/yoda/counter.py b/src/babyyoda/yoda/counter.py new file mode 100644 index 0000000..936d177 --- /dev/null +++ b/src/babyyoda/yoda/counter.py @@ -0,0 +1,66 @@ +import yoda + +import babyyoda +from babyyoda.util import has_own_method + + +class Counter(babyyoda.UHICounter): + def __init__(self, *args, **kwargs): + """ + target is either a yoda or grogu Counter + """ + + target = args[0] if len(args) == 1 else yoda.Counter(*args, **kwargs) + # unwrap target + while isinstance(target, Counter): + target = target.target + + super().__setattr__("target", target) + + ######################################################## + # Relay all attribute access to the target object + ######################################################## + + def __getattr__(self, name): + # if we overwrite it here, use that + if has_own_method(Counter, name): + return getattr(self, name) + # if the target has the attribute, use that + if hasattr(self.target, name): + return getattr(self.target, name) + # lastly use the inherited attribute + if hasattr(super(), name): + return getattr(super(), name) + err = f"'{type(self).__name__}' object and target have no attribute '{name}'" + raise AttributeError(err) + + def __setattr__(self, name, value): + if has_own_method(Counter, name): + setattr(self, name, value) + elif hasattr(self.target, name): + setattr(self.target, name, value) + elif hasattr(super(), name): + setattr(super(), name, value) + else: + err = f"Cannot set attribute '{name}'; it does not exist in target or Forwarder." + raise AttributeError(err) + + def __call__(self, *args, **kwargs): + # If the target is callable, forward the call, otherwise raise an error + if callable(self.target): + return self.target(*args, **kwargs) + err = f"'{type(self.target).__name__}' object is not callable" + raise TypeError(err) + + def bins(self, *args, **kwargs): + return self.target.bins(*args, **kwargs) + + def clone(self): + return Counter(self.target.clone()) + + # Fix https://gitlab.com/hepcedar/yoda/-/issues/101 + def annotationsDict(self): + d = {} + for k in self.target.annotations(): + d[k] = self.target.annotation(k) + return d diff --git a/src/babyyoda/yoda/histo1d.py b/src/babyyoda/yoda/histo1d.py index fbda1f9..63448d4 100644 --- a/src/babyyoda/yoda/histo1d.py +++ b/src/babyyoda/yoda/histo1d.py @@ -86,3 +86,10 @@ def __getitem__(self, slices): def clone(self): return Histo1D(self.target.clone()) + + # Fix https://gitlab.com/hepcedar/yoda/-/issues/101 + def annotationsDict(self): + d = {} + for k in self.target.annotations(): + d[k] = self.target.annotation(k) + return d diff --git a/src/babyyoda/yoda/histo2d.py b/src/babyyoda/yoda/histo2d.py index 8a35368..25bc327 100644 --- a/src/babyyoda/yoda/histo2d.py +++ b/src/babyyoda/yoda/histo2d.py @@ -73,3 +73,10 @@ def __getitem__(self, slices): def clone(self): return Histo2D(self.target.clone()) + + # Fix https://gitlab.com/hepcedar/yoda/-/issues/101 + def annotationsDict(self): + d = {} + for k in self.target.annotations(): + d[k] = self.target.annotation(k) + return d diff --git a/src/babyyoda/yoda/read.py b/src/babyyoda/yoda/read.py index 46a5119..45979db 100644 --- a/src/babyyoda/yoda/read.py +++ b/src/babyyoda/yoda/read.py @@ -1,5 +1,6 @@ import yoda as yd +from babyyoda.yoda.counter import Counter from babyyoda.yoda.histo1d import Histo1D from babyyoda.yoda.histo2d import Histo2D @@ -15,6 +16,8 @@ def read(file_path: str): ret[k] = Histo1D(v) elif isinstance(v, yd.Histo2D): ret[k] = Histo2D(v) + elif isinstance(v, yd.Counter): + ret[k] = Counter(v) else: ret[k] = v return ret diff --git a/tests/babyyoda/test_counter.py b/tests/babyyoda/test_counter.py new file mode 100644 index 0000000..987e7b4 --- /dev/null +++ b/tests/babyyoda/test_counter.py @@ -0,0 +1,34 @@ +import pytest + +from babyyoda import grogu +from babyyoda.test import assert_value0d, init_yoda + +yoda, yoda_available, yoda2 = init_yoda() + + +def create_histo(factory): + h = factory(title="test") + for i in range(12): + for _ in range(i): + h.fill(i) + return h + + +@pytest.mark.parametrize( + "factory", [grogu.Counter, grogu.Counter_v2, grogu.Counter_v3, yoda.Counter] +) +def test_create_histo(factory): + create_histo(factory) + + +@pytest.mark.parametrize( + "factory1", [grogu.Counter, grogu.Counter_v2, grogu.Counter_v3, yoda.Counter] +) +@pytest.mark.parametrize( + "factory2", [grogu.Counter, grogu.Counter_v2, grogu.Counter_v3, yoda.Counter] +) +def test_histos_equal(factory1, factory2): + h1 = create_histo(factory1) + h2 = create_histo(factory2) + + assert_value0d(h1, h2) diff --git a/tests/babyyoda/test_read.py b/tests/babyyoda/test_read.py index 5f08670..f1204da 100644 --- a/tests/babyyoda/test_read.py +++ b/tests/babyyoda/test_read.py @@ -60,3 +60,31 @@ def test_read_histo2d_v2(mod): def test_read_histo2d_v3(mod): hists = mod("tests/test_histo2d_v3.yoda") assert len(hists) == 1 + + +@pytest.mark.parametrize( + "mod", + [ + babyyoda.read, + grogu.read, + yoda.read, + ], +) +@pytest.mark.skipif(not yoda2, reason="yoda >= 2.0.0 is required") +def test_read_counter_v3(mod): + hists = mod("tests/test_counter_v3.yoda") + assert len(hists) == 1 + + +@pytest.mark.parametrize( + "mod", + [ + babyyoda.read, + grogu.read, + yoda.read, + ], +) +@pytest.mark.skipif(not yoda2, reason="yoda >= 2.0.0 is required") +def test_read_counter_v2(mod): + hists = mod("tests/test_counter_v2.yoda") + assert len(hists) == 1 diff --git a/tests/babyyoda/test_write.py b/tests/babyyoda/test_write.py index a1e4977..fa055c0 100644 --- a/tests/babyyoda/test_write.py +++ b/tests/babyyoda/test_write.py @@ -3,7 +3,7 @@ import babyyoda import babyyoda.read from babyyoda import grogu -from babyyoda.test import assert_histo1d, assert_histo2d, init_yoda +from babyyoda.test import assert_histo1d, assert_histo2d, assert_value0d, init_yoda from babyyoda.util import is_yoda, uses_yoda yoda, yoda_available, yoda2 = init_yoda() @@ -145,3 +145,64 @@ def test_write_histo2d_v3(read, write, reread, filename): h1 = next(iter(hs1.values())) h2 = next(iter(hs2.values())) assert_histo2d(h1, h2) + + +@pytest.mark.parametrize( + ("read", "write", "reread"), + [ + (yoda.read, yoda.write, yoda.read), + (babyyoda.read, babyyoda.write, babyyoda.read), + (babyyoda.read_grogu, babyyoda.write_grogu, babyyoda.read_grogu), + (grogu.read, grogu.write, grogu.read), + (babyyoda.read, babyyoda.write_grogu, babyyoda.read_grogu), + (babyyoda.read, babyyoda.write_grogu, babyyoda.read), + (yoda.read, yoda.write, babyyoda.read), + (babyyoda.read, babyyoda.write, yoda.read), + ], +) +@pytest.mark.parametrize( + "filename", + [ + "test.yoda", + "test.yoda.gz", + ], +) +@pytest.mark.skipif(not yoda2, reason="yoda >= 2.0.0 is required") +def test_write_counter_v3(read, write, reread, filename): + hs1 = read("tests/test_counter_v3.yoda") + write(hs1, filename) + hs2 = reread(filename) + + h1 = next(iter(hs1.values())) + h2 = next(iter(hs2.values())) + assert_value0d(h1, h2) + + +@pytest.mark.parametrize( + ("read", "write", "reread"), + [ + (yoda.read, yoda.write, yoda.read), + (babyyoda.read, babyyoda.write, babyyoda.read), + (babyyoda.read_grogu, babyyoda.write_grogu, babyyoda.read_grogu), + (grogu.read, grogu.write, grogu.read), + (babyyoda.read, babyyoda.write_grogu, babyyoda.read_grogu), + (babyyoda.read, babyyoda.write_grogu, babyyoda.read), + (yoda.read, yoda.write, babyyoda.read), + (babyyoda.read, babyyoda.write, yoda.read), + ], +) +@pytest.mark.parametrize( + "filename", + [ + "test.yoda", + "test.yoda.gz", + ], +) +def test_write_counter_v2(read, write, reread, filename): + hs1 = read("tests/test_counter_v2.yoda") + write(hs1, filename) + hs2 = reread(filename) + + h1 = next(iter(hs1.values())) + h2 = next(iter(hs2.values())) + assert_value0d(h1, h2) diff --git a/tests/grogu/test_gg_string.py b/tests/grogu/test_gg_string.py index 5f491b2..0c3dfe4 100644 --- a/tests/grogu/test_gg_string.py +++ b/tests/grogu/test_gg_string.py @@ -3,11 +3,13 @@ import pytest import babyyoda +from babyyoda.grogu.counter_v2 import GROGU_COUNTER_V2 +from babyyoda.grogu.counter_v3 import GROGU_COUNTER_V3 from babyyoda.grogu.histo1d_v2 import GROGU_HISTO1D_V2 from babyyoda.grogu.histo1d_v3 import GROGU_HISTO1D_V3 from babyyoda.grogu.histo2d_v2 import GROGU_HISTO2D_V2 from babyyoda.grogu.histo2d_v3 import GROGU_HISTO2D_V3 -from babyyoda.test import assert_bin1d, assert_histo1d, assert_histo2d +from babyyoda.test import assert_bin1d, assert_histo1d, assert_histo2d, assert_value0d @pytest.mark.parametrize( @@ -125,3 +127,31 @@ def test_gg_histo2d_v3_string(): h2 = GROGU_HISTO2D_V3.from_string(s) assert_histo2d(h1, h2, includeFlow=True) assert h1 == h2 + + +def test_gg_counter_v2_string(): + h1 = babyyoda.grogu.Counter_v2(title="test") + w = 0 + for _ in range(-10, 12): + for _ in range(-10, 12): + w += 1 + h1.fill(w) + s = h1.to_string() + print(s) + h2 = GROGU_COUNTER_V2.from_string(s) + assert_value0d(h1, h2) + assert h1 == h2 + + +def test_gg_counter_v3_string(): + h1 = babyyoda.grogu.Counter_v3(title="test") + w = 0 + for _ in range(-10, 12): + for _ in range(-10, 12): + w += 1 + h1.fill(w) + s = h1.to_string() + print(s) + h2 = GROGU_COUNTER_V3.from_string(s) + assert_value0d(h1, h2) + assert h1 == h2 diff --git a/tests/grogu/test_gg_write.py b/tests/grogu/test_gg_write.py index 8d2311e..5d76647 100644 --- a/tests/grogu/test_gg_write.py +++ b/tests/grogu/test_gg_write.py @@ -31,3 +31,19 @@ def test_gg_write_histo2d_v3(): with open("test.yoda") as f1, open("tests/test_histo2d_v3.yoda") as f2: assert f1.read() == f2.read() + + +def test_gg_write_counter_v2(): + hists = read("tests/test_counter_v2.yoda") + write(hists, "test.yoda") + + with open("test.yoda") as f1, open("tests/test_counter_v2.yoda") as f2: + assert f1.read() == f2.read() + + +def test_gg_write_counter_v3(): + hists = read("tests/test_counter_v3.yoda") + write(hists, "test.yoda") + + with open("test.yoda") as f1, open("tests/test_counter_v3.yoda") as f2: + assert f1.read() == f2.read() diff --git a/tests/test_counter_v2.yoda b/tests/test_counter_v2.yoda new file mode 100644 index 0000000..903f61d --- /dev/null +++ b/tests/test_counter_v2.yoda @@ -0,0 +1,8 @@ +BEGIN YODA_COUNTER_V2 / +Path: / +Title: hi +Type: Counter +--- +# sumW sumW2 numEntries +0.000000e+00 0.000000e+00 0.000000e+00 +END YODA_COUNTER_V2 diff --git a/tests/test_counter_v3.yoda b/tests/test_counter_v3.yoda new file mode 100644 index 0000000..c438d50 --- /dev/null +++ b/tests/test_counter_v3.yoda @@ -0,0 +1,8 @@ +BEGIN YODA_COUNTER_V3 / +Path: / +Title: hi +Type: Counter +--- +# sumW sumW2 numEntries +0.000000e+00 0.000000e+00 0.000000e+00 +END YODA_COUNTER_V3 diff --git a/tests/yoda/test_yoda_vs_grogu.py b/tests/yoda/test_yoda_vs_grogu.py index 8ca4de7..488f13e 100644 --- a/tests/yoda/test_yoda_vs_grogu.py +++ b/tests/yoda/test_yoda_vs_grogu.py @@ -113,7 +113,7 @@ def test_create_histo1d(): h = yoda.Histo1D(10, 0, 10, title="test") g = GROGU_HISTO1D_V2( - d_title="test", + d_annotations={"Title": "test"}, d_bins=[ GROGU_HISTO1D_V2.Bin(d_xmin=hb.xMin(), d_xmax=hb.xMax()) for hb in h.bins() ], @@ -137,10 +137,9 @@ def test_create_histo1d(): def test_create_histo2d(): import babyyoda.yoda as yd - h = yd.Histo2D(10, 0, 10, 10, 0, 10, title="test") + h = yd.Histo2D(10, 0, 10, 10, 0, 10) g = GROGU_HISTO2D_V2( - d_title="test", d_bins=[ GROGU_HISTO2D_V2.Bin( d_xmin=hb.xMin(), d_xmax=hb.xMax(), d_ymin=hb.yMin(), d_ymax=hb.yMax() diff --git a/tests/yoda/uhi/test_yd_histo1d_access.py b/tests/yoda/uhi/test_yd_histo1d_access.py index 8e08375..93614ad 100644 --- a/tests/yoda/uhi/test_yd_histo1d_access.py +++ b/tests/yoda/uhi/test_yd_histo1d_access.py @@ -13,7 +13,6 @@ def create_linear_histo1ds(): h = yoda.Histo1D(10, 0, 10, title="test") g = GROGU_HISTO1D_V2( - d_title="test", d_bins=[ GROGU_HISTO1D_V2.Bin(d_xmin=hb.xMin(), d_xmax=hb.xMax()) for hb in h.bins() ], diff --git a/tests/yoda/uhi/test_yd_histo2d_access.py b/tests/yoda/uhi/test_yd_histo2d_access.py index 5312c4e..c6773f2 100644 --- a/tests/yoda/uhi/test_yd_histo2d_access.py +++ b/tests/yoda/uhi/test_yd_histo2d_access.py @@ -13,7 +13,6 @@ def create_histo2d(): h = yd.Histo2D(10, 0, 10, 10, 0, 10, title="test") g = GROGU_HISTO2D_V2( - d_title="test", d_bins=[ GROGU_HISTO2D_V2.Bin( d_xmin=hb.xMin(), d_xmax=hb.xMax(), d_ymin=hb.yMin(), d_ymax=hb.yMax()