aptapy 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aptapy/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.3.0"
aptapy/hist.py ADDED
@@ -0,0 +1,280 @@
1
+ # Copyright (C) 2025 Luca Baldini (luca.baldini@pi.infn.it)
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ """Histogram facilities.
17
+ """
18
+
19
+ from abc import ABC, abstractmethod
20
+ from typing import List, Sequence
21
+
22
+ import numpy as np
23
+
24
+ from .plotting import matplotlib, plt, setup_axes
25
+ from .typing_ import ArrayLike
26
+
27
+
28
+ class AbstractHistogram(ABC):
29
+
30
+ """Abstract base class for an n-dimensional histogram.
31
+
32
+ Arguments
33
+ ---------
34
+ edges : n-dimensional sequence of arrays
35
+ the bin edges on the different axes.
36
+
37
+ axis_labels : sequence of strings
38
+ the text labels for the different histogram axes.
39
+ """
40
+
41
+ DEFAULT_PLOT_OPTIONS = {}
42
+
43
+ def __init__(self, edges: Sequence[np.ndarray], label: str, axis_labels: List[str]) -> None:
44
+ """Constructor.
45
+ """
46
+ # Edges are fixed once and forever, so we create a copy. Also, no matter
47
+ # which kind of sequence we are passing, we turn the thing into a tuple.
48
+ self._edges = tuple(np.asarray(item, dtype=float).copy() for item in edges)
49
+ self._num_axes = len(self._edges)
50
+
51
+ # And a few basic checks on the input arguments.
52
+ for item in self._edges:
53
+ if item.ndim != 1:
54
+ raise ValueError(f"Bin edges {item} are not a 1-dimensional array.")
55
+ if item.size < 2:
56
+ raise ValueError(f"Bin edges {item} have less than 2 entries.")
57
+ if np.any(np.diff(item) <= 0):
58
+ raise ValueError(f"Bin edges {item} not strictly increasing.")
59
+ if axis_labels is not None and len(axis_labels) > self._num_axes + 1:
60
+ raise ValueError(f"Too many axis labels {axis_labels} for {self._num_axes} axes.")
61
+
62
+ # Go ahead and create all the necessary data structures.
63
+ self._shape = tuple(item.size - 1 for item in self._edges)
64
+ self._sumw = np.zeros(self._shape, dtype=float)
65
+ self._sumw2 = np.zeros(self._shape, dtype=float)
66
+ self.label = label
67
+ self.axis_labels = axis_labels
68
+
69
+ @property
70
+ def content(self) -> np.ndarray:
71
+ """Return the bin contents.
72
+ """
73
+ return self._sumw
74
+
75
+ @property
76
+ def errors(self) -> np.ndarray:
77
+ """Return the bin errors.
78
+ """
79
+ return np.sqrt(self._sumw2)
80
+
81
+ def bin_edges(self, axis: int = 0) -> np.ndarray:
82
+ """Return a view on the binning for specific axis.
83
+ """
84
+ return self._edges[axis].view()
85
+
86
+ def bin_centers(self, axis: int = 0) -> np.ndarray:
87
+ """Return the bin centers for a specific axis.
88
+ """
89
+ return 0.5 * (self._edges[axis][1:] + self._edges[axis][:-1])
90
+
91
+ def bin_widths(self, axis: int = 0) -> np.ndarray:
92
+ """Return the bin widths for a specific axis.
93
+ """
94
+ return np.diff(self._edges[axis])
95
+
96
+ def fill(self, *values: ArrayLike, weights: ArrayLike = None) -> "AbstractHistogram":
97
+ """Fill the histogram from unbinned data.
98
+
99
+ Note this method is returning the histogram instance, so that the function
100
+ call can be chained.
101
+ """
102
+ values = np.vstack(values).T
103
+ sumw, _ = np.histogramdd(values, bins=self._edges, weights=weights)
104
+ if weights is None:
105
+ sumw2 = sumw
106
+ else:
107
+ sumw2, _ = np.histogramdd(values, bins=self._edges, weights=weights**2.)
108
+ self._sumw += sumw
109
+ self._sumw2 += sumw2
110
+ return self
111
+
112
+ def copy(self) -> "AbstractHistogram":
113
+ """Create a full copy of a histogram.
114
+ """
115
+ # pylint: disable=protected-access
116
+ # Note we really need the * in the constructor, here, as the abstract
117
+ # base class is never instantiated, and the arguments are unpacked in the
118
+ # constructors of all the derived classes.
119
+ histogram = self.__class__(*self._edges, self.label, *self.axis_labels)
120
+ histogram._sumw = self._sumw.copy()
121
+ histogram._sumw2 = self._sumw2.copy()
122
+ return histogram
123
+
124
+ def _check_compat(self, other: "AbstractHistogram") -> None:
125
+ """Check whether two histogram objects are compatible with each other,
126
+ meaning, e.g., that they can be summed or subtracted.
127
+ """
128
+ # pylint: disable=protected-access
129
+ if not isinstance(other, AbstractHistogram):
130
+ raise TypeError(f"{other} is not a histogram.")
131
+ if self._num_axes != other._num_axes or self._shape != other._shape:
132
+ raise ValueError("Histogram dimensionality/shape mismatch.")
133
+ for edges in zip(self._edges, other._edges):
134
+ if not np.allclose(*edges):
135
+ raise ValueError("Histogram bin edges differ.")
136
+
137
+ def __iadd__(self, other: "AbstractHistogram") -> "AbstractHistogram":
138
+ """Histogram addition (in place).
139
+ """
140
+ self._check_compat(other)
141
+ self._sumw += other._sumw
142
+ self._sumw2 += other._sumw2
143
+ return self
144
+
145
+ def __add__(self, other: "AbstractHistogram") -> "AbstractHistogram":
146
+ """Histogram addition.
147
+ """
148
+ histogram = self.copy()
149
+ histogram += other
150
+ return histogram
151
+
152
+ def __isub__(self, other: "AbstractHistogram") -> "AbstractHistogram":
153
+ """Histogram subtraction (in place).
154
+ """
155
+ self._check_compat(other)
156
+ self._sumw -= other._sumw
157
+ self._sumw2 += other._sumw2
158
+ return self
159
+
160
+ def __sub__(self, other: "AbstractHistogram") -> "AbstractHistogram":
161
+ """Histogram subtraction.
162
+ """
163
+ histogram = self.copy()
164
+ histogram -= other
165
+ return histogram
166
+
167
+ @abstractmethod
168
+ def _do_plot(self, axes, **kwargs) -> None:
169
+ pass
170
+
171
+ def plot(self, axes=None, **kwargs) -> None:
172
+ """Plot the histogram.
173
+ """
174
+ if axes is None:
175
+ axes = plt.gca()
176
+ for key, value in self.DEFAULT_PLOT_OPTIONS.items():
177
+ kwargs.setdefault(key, value)
178
+ self._do_plot(axes, **kwargs)
179
+
180
+ def __repr__(self) -> str:
181
+ """String representation of the histogram.
182
+ """
183
+ return f"{self.__class__.__name__}({self._num_axes} axes, shape={self._shape})"
184
+
185
+
186
+ class Histogram1d(AbstractHistogram):
187
+
188
+ """One-dimensional histogram.
189
+
190
+ Arguments
191
+ ---------
192
+ edges : 1-dimensional array
193
+ the bin edges.
194
+
195
+ label : str
196
+ overall label for the histogram (if defined, this will be used in the
197
+ legend by default).
198
+
199
+ xlabel : str
200
+ the text label for the x axis.
201
+
202
+ ylabel : str
203
+ the text label for the y axis (default: "Entries/bin").
204
+ """
205
+
206
+ DEFAULT_PLOT_OPTIONS = dict(linewidth=1.25, alpha=0.4, histtype="stepfilled")
207
+
208
+ def __init__(self, xedges: np.ndarray, label: str = None, xlabel: str = None,
209
+ ylabel: str = "Entries/bin") -> None:
210
+ """Constructor.
211
+ """
212
+ super().__init__((xedges, ), label, [xlabel, ylabel])
213
+
214
+ def area(self) -> float:
215
+ """Return the total area under the histogram.
216
+
217
+ This is potentially useful when fitting a model to the histogram, e.g.,
218
+ to freeze the prefactor of a gaussian to the histogram normalization.
219
+
220
+ Returns
221
+ -------
222
+ area : float
223
+ The total area under the histogram.
224
+ """
225
+ return (self.content * self.bin_widths()).sum()
226
+
227
+ def _do_plot(self, axes: matplotlib.axes._axes.Axes, **kwargs) -> None:
228
+ """Overloaded make_plot() method.
229
+ """
230
+ # If we are not explicitly providing a label at plotting time, use
231
+ # the one attached to the histogram, if any.
232
+ kwargs.setdefault('label', self.label)
233
+ axes.hist(self.bin_centers(0), self._edges[0], weights=self.content, **kwargs)
234
+ setup_axes(axes, xlabel=self.axis_labels[0], ylabel=self.axis_labels[1])
235
+
236
+
237
+ class Histogram2d(AbstractHistogram):
238
+
239
+ """Two-dimensional histogram.
240
+
241
+ Arguments
242
+ ---------
243
+ xedges : 1-dimensional array
244
+ the bin edges on the x axis.
245
+
246
+ yedges : 1-dimensional array
247
+ the bin edges on the y axis.
248
+
249
+ label : str
250
+ overall label for the histogram
251
+
252
+ xlabel : str
253
+ the text label for the x axis.
254
+
255
+ ylabel : str
256
+ the text label for the y axis.
257
+
258
+ zlabel : str
259
+ the text label for the z axis (default: "Entries/bin").
260
+ """
261
+
262
+ DEFAULT_PLOT_OPTIONS = dict(cmap=plt.get_cmap('hot'))
263
+
264
+ def __init__(self, xedges, yedges, label: str = None, xlabel: str = None,
265
+ ylabel: str = None, zlabel: str = 'Entries/bin') -> None:
266
+ """Constructor.
267
+ """
268
+ super().__init__((xedges, yedges), label, [xlabel, ylabel, zlabel])
269
+
270
+ def _do_plot(self, axes: matplotlib.axes._axes.Axes, logz: bool = False, **kwargs) -> None:
271
+ """Overloaded make_plot() method.
272
+ """
273
+ # pylint: disable=arguments-differ
274
+ if logz:
275
+ vmin = kwargs.pop('vmin', None)
276
+ vmax = kwargs.pop('vmax', None)
277
+ kwargs.setdefault('norm', matplotlib.colors.LogNorm(vmin, vmax))
278
+ mappable = axes.pcolormesh(*self._edges, self.content.T, **kwargs)
279
+ plt.colorbar(mappable, ax=axes, label=self.axis_labels[2])
280
+ setup_axes(axes, xlabel=self.axis_labels[0], ylabel=self.axis_labels[1])