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 +1 -1
- aptapy/hist.py +280 -0
- aptapy/modeling.py +576 -96
- aptapy/plotting.py +334 -290
- aptapy/strip.py +92 -0
- {aptapy-0.1.1.dist-info → aptapy-0.3.0.dist-info}/METADATA +10 -2
- aptapy-0.3.0.dist-info/RECORD +12 -0
- aptapy-0.1.1.dist-info/RECORD +0 -10
- {aptapy-0.1.1.dist-info → aptapy-0.3.0.dist-info}/WHEEL +0 -0
- {aptapy-0.1.1.dist-info → aptapy-0.3.0.dist-info}/licenses/LICENSE +0 -0
aptapy/_version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
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])
|