Skip to content

Commit 7e23eb7

Browse files
henryiiiaryamanjeendgarpre-commit-ci[bot]
authored
feat: starting serialization (#997)
* feat: starting serialization Starting with @aryamanjeendgar's https://github.com/aryamanjeendgar/UHI-serialization with some changes. Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Aryaman Jeendgar <[email protected]> * refactor: pass file in Signed-off-by: Henry Schreiner <[email protected]> * chore: use f-strings more often Signed-off-by: Henry Schreiner <[email protected]> * ci: cleanup and simplify uv config Signed-off-by: Henry Schreiner <[email protected]> * fixup! chore: use f-strings more often * tests: parametrize round trip test Signed-off-by: Henry Schreiner <[email protected]> * feat: add generic version * feat: rework serialization into generic + hdf5 Signed-off-by: Henry Schreiner <[email protected]> * fix: some cleanup and a mistake fixed that Copilot noticed Signed-off-by: Henry Schreiner <[email protected]> * fix: support transform axes, more tests Signed-off-by: Henry Schreiner <[email protected]> * refactor: follow updated schema Signed-off-by: Henry Schreiner <[email protected]> * refactor: pull out hdf5 backend (in uhi now) Signed-off-by: Henry Schreiner <[email protected]> * fix: add writer_info Signed-off-by: Henry Schreiner <[email protected]> * refactor: simplify naming slightly Signed-off-by: Henry Schreiner <[email protected]> * feat: add overridable api Signed-off-by: Henry Schreiner <[email protected]> * style: pre-commit fixes * fix: adjust imports Signed-off-by: Henry Schreiner <[email protected]> * chore: fix pylint Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Aryaman Jeendgar <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0dd610d commit 7e23eb7

File tree

8 files changed

+512
-12
lines changed

8 files changed

+512
-12
lines changed

CMakeLists.txt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,10 @@ source_group(
118118
option(BOOST_HISTOGRAM_ERRORS "Make warnings errors (for CI mostly)")
119119

120120
# Adding warnings
121+
# Boost.Histogram doesn't pass sign -Wsign-conversion
121122
if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
122-
target_compile_options(
123-
_core
124-
PRIVATE -Wall
125-
-Wextra
126-
-pedantic-errors
127-
-Wconversion
128-
-Wsign-conversion
129-
-Wsign-compare
130-
-Wno-unused-value)
123+
target_compile_options(_core PRIVATE -Wall -Wextra -pedantic-errors -Wconversion -Wsign-compare
124+
-Wno-unused-value)
131125
if(BOOST_HISTOGRAM_ERRORS)
132126
target_compile_options(_core PRIVATE -Werror)
133127
endif()

noxfile.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ def tests(session: nox.Session) -> None:
2323
opts = (
2424
["--reinstall-package=boost-histogram"] if session.venv_backend == "uv" else []
2525
)
26-
args = session.posargs or ["-n", "auto"]
26+
args = session.posargs or ["-n", "auto", "--benchmark-disable"]
2727
pyproject = nox.project.load_toml("pyproject.toml")
2828
session.install(*nox.project.dependency_groups(pyproject, "test"))
29-
session.install("-v", ".", *opts, silent=False)
29+
session.install("-v", "-e.", *opts, silent=False)
3030
session.run("pytest", *args)
3131

3232

@@ -117,7 +117,7 @@ def pylint(session: nox.Session) -> None:
117117
"""
118118

119119
session.install("pylint==3.3.*")
120-
session.install("-e.")
120+
session.install(".")
121121
session.run("pylint", "boost_histogram", *session.posargs)
122122

123123

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ environment.UV_PRERELEASE = "allow"
220220
[tool.pylint]
221221
py-version = "3.9"
222222
ignore-patterns = ['.*\.pyi']
223+
ignore = "version.py"
223224
extension-pkg-allow-list = ["boost_histogram._core"]
224225
reports.output-format = "colorized"
225226
similarities.ignore-imports = "yes"

src/boost_histogram/histogram.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import boost_histogram
2727
from boost_histogram import _core
2828

29+
from . import serialization
2930
from ._compat.typing import Self
3031
from ._utils import cast, register
3132
from .axis import AxesTuple, Axis, Variable
@@ -402,6 +403,19 @@ def _generate_axes_(self) -> AxesTuple:
402403

403404
return AxesTuple(self._axis(i) for i in range(self.ndim))
404405

406+
def _to_uhi_(self) -> dict[str, Any]:
407+
"""
408+
Convert to a UHI histogram.
409+
"""
410+
return serialization.to_uhi(self)
411+
412+
@classmethod
413+
def _from_uhi_(cls, inp: dict[str, Any], /) -> Self:
414+
"""
415+
Convert from a UHI histogram.
416+
"""
417+
return cls(serialization.from_uhi(inp))
418+
405419
@property
406420
def ndim(self) -> int:
407421
"""
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
# pylint: disable-next=import-error
6+
from .. import histogram, version
7+
from ._axis import _axis_from_dict, _axis_to_dict
8+
from ._storage import _data_from_dict, _storage_from_dict, _storage_to_dict
9+
10+
__all__ = ["from_uhi", "to_uhi"]
11+
12+
13+
def __dir__() -> list[str]:
14+
return __all__
15+
16+
17+
def to_uhi(h: histogram.Histogram, /) -> dict[str, Any]:
18+
"""Convert an Histogram to a dictionary."""
19+
20+
# Convert the histogram to a dictionary
21+
data = {
22+
"writer_info": {"boost-histogram": {"version": version.version}},
23+
"axes": [_axis_to_dict(axis) for axis in h.axes],
24+
"storage": _storage_to_dict(h.storage_type(), h.view(flow=True)),
25+
}
26+
if h.metadata is not None:
27+
data["metadata"] = h.metadata
28+
29+
return data
30+
31+
32+
def from_uhi(data: dict[str, Any], /) -> histogram.Histogram:
33+
"""Convert a dictionary to an Histogram."""
34+
35+
h = histogram.Histogram(
36+
*(_axis_from_dict(ax) for ax in data["axes"]),
37+
storage=_storage_from_dict(data["storage"]),
38+
metadata=data.get("metadata"),
39+
)
40+
h[...] = _data_from_dict(data["storage"])
41+
return h
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
from typing import Any
5+
6+
from .. import axis
7+
8+
__all__ = ["_axis_from_dict", "_axis_to_dict"]
9+
10+
11+
def __dir__() -> list[str]:
12+
return __all__
13+
14+
15+
@functools.singledispatch
16+
def _axis_to_dict(ax: Any, /) -> dict[str, Any]:
17+
"""Convert an axis to a dictionary."""
18+
raise TypeError(f"Unsupported axis type: {type(ax)}")
19+
20+
21+
@_axis_to_dict.register(axis.Regular)
22+
@_axis_to_dict.register(axis.Integer)
23+
def _(ax: axis.Regular | axis.Integer, /) -> dict[str, Any]:
24+
"""Convert a Regular axis to a dictionary."""
25+
26+
# Special handling if the axis has a transform
27+
if isinstance(ax, axis.Regular) and ax.transform is not None:
28+
data = {
29+
"type": "variable",
30+
"edges": ax.edges,
31+
"underflow": ax.traits.underflow,
32+
"overflow": ax.traits.overflow,
33+
"circular": ax.traits.circular,
34+
}
35+
else:
36+
data = {
37+
"type": "regular",
38+
"lower": ax.edges[0],
39+
"upper": ax.edges[-1],
40+
"bins": ax.size,
41+
"underflow": ax.traits.underflow,
42+
"overflow": ax.traits.overflow,
43+
"circular": ax.traits.circular,
44+
}
45+
if isinstance(ax, axis.Integer):
46+
data["writer_info"] = {"boost-histogram": {"orig_type": "Integer"}}
47+
if ax.metadata is not None:
48+
data["metadata"] = ax.metadata
49+
50+
return data
51+
52+
53+
@_axis_to_dict.register
54+
def _(ax: axis.Variable, /) -> dict[str, Any]:
55+
"""Convert a Variable or Integer axis to a dictionary."""
56+
data = {
57+
"type": "variable",
58+
"edges": ax.edges,
59+
"underflow": ax.traits.underflow,
60+
"overflow": ax.traits.overflow,
61+
"circular": ax.traits.circular,
62+
}
63+
if ax.metadata is not None:
64+
data["metadata"] = ax.metadata
65+
66+
return data
67+
68+
69+
@_axis_to_dict.register
70+
def _(ax: axis.IntCategory, /) -> dict[str, Any]:
71+
"""Convert an IntCategory axis to a dictionary."""
72+
data = {
73+
"type": "category_int",
74+
"categories": list(ax),
75+
"flow": ax.traits.overflow,
76+
}
77+
if ax.metadata is not None:
78+
data["metadata"] = ax.metadata
79+
80+
return data
81+
82+
83+
@_axis_to_dict.register
84+
def _(ax: axis.StrCategory, /) -> dict[str, Any]:
85+
"""Convert a StrCategory axis to a dictionary."""
86+
data = {
87+
"type": "category_str",
88+
"categories": list(ax),
89+
"flow": ax.traits.overflow,
90+
}
91+
if ax.metadata is not None:
92+
data["metadata"] = ax.metadata
93+
94+
return data
95+
96+
97+
@_axis_to_dict.register
98+
def _(ax: axis.Boolean, /) -> dict[str, Any]:
99+
"""Convert a Boolean axis to a dictionary."""
100+
data = {
101+
"type": "boolean",
102+
}
103+
if ax.metadata is not None:
104+
data["metadata"] = ax.metadata
105+
106+
return data
107+
108+
109+
def _axis_from_dict(data: dict[str, Any], /) -> axis.Axis:
110+
hist_type = data["type"]
111+
if hist_type == "regular":
112+
return axis.Regular(
113+
data["bins"],
114+
data["lower"],
115+
data["upper"],
116+
underflow=data["underflow"],
117+
overflow=data["overflow"],
118+
circular=data["circular"],
119+
metadata=data.get("metadata"),
120+
)
121+
if hist_type == "variable":
122+
return axis.Variable(
123+
data["edges"],
124+
underflow=data["underflow"],
125+
overflow=data["overflow"],
126+
circular=data["circular"],
127+
metadata=data.get("metadata"),
128+
)
129+
if hist_type == "category_int":
130+
return axis.IntCategory(
131+
data["categories"],
132+
overflow=data["flow"],
133+
metadata=data.get("metadata"),
134+
)
135+
if hist_type == "category_str":
136+
return axis.StrCategory(
137+
data["categories"],
138+
overflow=data["flow"],
139+
metadata=data.get("metadata"),
140+
)
141+
if hist_type == "boolean":
142+
return axis.Boolean(metadata=data.get("metadata"))
143+
144+
raise TypeError(f"Unsupported axis type: {hist_type}")
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
from typing import Any
5+
6+
import numpy as np
7+
8+
from .. import storage
9+
10+
__all__ = ["_data_from_dict", "_storage_from_dict", "_storage_to_dict"]
11+
12+
13+
def __dir__() -> list[str]:
14+
return __all__
15+
16+
17+
@functools.singledispatch
18+
def _storage_to_dict(_storage: Any, /, data: Any) -> dict[str, Any]: # noqa: ARG001
19+
"""Convert a storage to a dictionary."""
20+
msg = f"Unsupported storage type: {_storage}"
21+
raise TypeError(msg)
22+
23+
24+
@_storage_to_dict.register(storage.AtomicInt64)
25+
@_storage_to_dict.register(storage.Double)
26+
@_storage_to_dict.register(storage.Int64)
27+
@_storage_to_dict.register(storage.Unlimited)
28+
def _(
29+
storage_: storage.AtomicInt64 | storage.Double | storage.Int64 | storage.Unlimited,
30+
/,
31+
data: Any,
32+
) -> dict[str, Any]:
33+
return {
34+
"writer_info": {"boost-histogram": {"orig_type": type(storage_).__name__}},
35+
"type": "int" if np.issubdtype(data.dtype, np.integer) else "double",
36+
"values": data,
37+
}
38+
39+
40+
@_storage_to_dict.register(storage.Weight)
41+
def _(_storage: storage.Weight, /, data: Any) -> dict[str, Any]:
42+
return {
43+
"type": "weighted",
44+
"values": data.value,
45+
"variances": data.variance,
46+
}
47+
48+
49+
@_storage_to_dict.register(storage.Mean)
50+
def _(_storage: storage.Mean, /, data: Any) -> dict[str, Any]:
51+
return {
52+
"type": "mean",
53+
"counts": data.count,
54+
"values": data.value,
55+
"variances": data.variance,
56+
}
57+
58+
59+
@_storage_to_dict.register(storage.WeightedMean)
60+
def _(_storage: storage.WeightedMean, /, data: Any) -> dict[str, Any]:
61+
return {
62+
"type": "weighted_mean",
63+
"sum_of_weights": data.sum_of_weights,
64+
"sum_of_weights_squared": data.sum_of_weights_squared,
65+
"values": data.value,
66+
"variances": data.variance,
67+
}
68+
69+
70+
def _storage_from_dict(data: dict[str, Any], /) -> storage.Storage:
71+
"""Convert a dictionary to a storage object."""
72+
storage_type = data["type"]
73+
74+
if storage_type == "int":
75+
return storage.Int64()
76+
if storage_type == "double":
77+
return storage.Double()
78+
if storage_type == "weighted":
79+
return storage.Weight()
80+
if storage_type == "mean":
81+
return storage.Mean()
82+
if storage_type == "weighted_mean":
83+
return storage.WeightedMean()
84+
85+
raise TypeError(f"Unsupported storage type: {storage_type}")
86+
87+
88+
def _data_from_dict(data: dict[str, Any], /) -> np.typing.NDArray[Any]:
89+
"""Convert a dictionary to data."""
90+
storage_type = data["type"]
91+
92+
if storage_type in {"int", "double"}:
93+
return data["values"]
94+
if storage_type == "weighted":
95+
return np.stack([data["values"], data["variances"]]).T
96+
if storage_type == "mean":
97+
return np.stack(
98+
[data["counts"], data["values"], data["variances"]],
99+
).T
100+
if storage_type == "weighted_mean":
101+
return np.stack(
102+
[
103+
data["sum_of_weights"],
104+
data["sum_of_weights_squared"],
105+
data["values"],
106+
data["variances"],
107+
],
108+
).T
109+
110+
raise TypeError(f"Unsupported storage type: {storage_type}")

0 commit comments

Comments
 (0)