aptapy 0.2.0__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 +68 -14
- aptapy/modeling.py +556 -76
- aptapy/plotting.py +9 -2
- aptapy/strip.py +92 -0
- {aptapy-0.2.0.dist-info → aptapy-0.3.0.dist-info}/METADATA +10 -2
- aptapy-0.3.0.dist-info/RECORD +12 -0
- aptapy-0.2.0.dist-info/RECORD +0 -11
- {aptapy-0.2.0.dist-info → aptapy-0.3.0.dist-info}/WHEEL +0 -0
- {aptapy-0.2.0.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
CHANGED
@@ -34,13 +34,13 @@ class AbstractHistogram(ABC):
|
|
34
34
|
edges : n-dimensional sequence of arrays
|
35
35
|
the bin edges on the different axes.
|
36
36
|
|
37
|
-
|
38
|
-
the text labels for the different axes.
|
37
|
+
axis_labels : sequence of strings
|
38
|
+
the text labels for the different histogram axes.
|
39
39
|
"""
|
40
40
|
|
41
41
|
DEFAULT_PLOT_OPTIONS = {}
|
42
42
|
|
43
|
-
def __init__(self, edges: Sequence[np.ndarray],
|
43
|
+
def __init__(self, edges: Sequence[np.ndarray], label: str, axis_labels: List[str]) -> None:
|
44
44
|
"""Constructor.
|
45
45
|
"""
|
46
46
|
# Edges are fixed once and forever, so we create a copy. Also, no matter
|
@@ -56,14 +56,15 @@ class AbstractHistogram(ABC):
|
|
56
56
|
raise ValueError(f"Bin edges {item} have less than 2 entries.")
|
57
57
|
if np.any(np.diff(item) <= 0):
|
58
58
|
raise ValueError(f"Bin edges {item} not strictly increasing.")
|
59
|
-
if
|
60
|
-
raise ValueError(f"Too many labels {
|
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
61
|
|
62
62
|
# Go ahead and create all the necessary data structures.
|
63
63
|
self._shape = tuple(item.size - 1 for item in self._edges)
|
64
64
|
self._sumw = np.zeros(self._shape, dtype=float)
|
65
65
|
self._sumw2 = np.zeros(self._shape, dtype=float)
|
66
|
-
self.
|
66
|
+
self.label = label
|
67
|
+
self.axis_labels = axis_labels
|
67
68
|
|
68
69
|
@property
|
69
70
|
def content(self) -> np.ndarray:
|
@@ -115,7 +116,7 @@ class AbstractHistogram(ABC):
|
|
115
116
|
# Note we really need the * in the constructor, here, as the abstract
|
116
117
|
# base class is never instantiated, and the arguments are unpacked in the
|
117
118
|
# constructors of all the derived classes.
|
118
|
-
histogram = self.__class__(*self._edges, *self.
|
119
|
+
histogram = self.__class__(*self._edges, self.label, *self.axis_labels)
|
119
120
|
histogram._sumw = self._sumw.copy()
|
120
121
|
histogram._sumw2 = self._sumw2.copy()
|
121
122
|
return histogram
|
@@ -185,33 +186,86 @@ class AbstractHistogram(ABC):
|
|
185
186
|
class Histogram1d(AbstractHistogram):
|
186
187
|
|
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").
|
188
204
|
"""
|
189
205
|
|
190
206
|
DEFAULT_PLOT_OPTIONS = dict(linewidth=1.25, alpha=0.4, histtype="stepfilled")
|
191
207
|
|
192
|
-
def __init__(self, xedges: np.ndarray,
|
208
|
+
def __init__(self, xedges: np.ndarray, label: str = None, xlabel: str = None,
|
209
|
+
ylabel: str = "Entries/bin") -> None:
|
193
210
|
"""Constructor.
|
194
211
|
"""
|
195
|
-
super().__init__((xedges, ), [xlabel, ylabel])
|
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()
|
196
226
|
|
197
227
|
def _do_plot(self, axes: matplotlib.axes._axes.Axes, **kwargs) -> None:
|
198
228
|
"""Overloaded make_plot() method.
|
199
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)
|
200
233
|
axes.hist(self.bin_centers(0), self._edges[0], weights=self.content, **kwargs)
|
201
|
-
setup_axes(axes, xlabel=self.
|
234
|
+
setup_axes(axes, xlabel=self.axis_labels[0], ylabel=self.axis_labels[1])
|
202
235
|
|
203
236
|
|
204
237
|
class Histogram2d(AbstractHistogram):
|
205
238
|
|
206
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").
|
207
260
|
"""
|
208
261
|
|
209
262
|
DEFAULT_PLOT_OPTIONS = dict(cmap=plt.get_cmap('hot'))
|
210
263
|
|
211
|
-
def __init__(self, xedges, yedges,
|
264
|
+
def __init__(self, xedges, yedges, label: str = None, xlabel: str = None,
|
265
|
+
ylabel: str = None, zlabel: str = 'Entries/bin') -> None:
|
212
266
|
"""Constructor.
|
213
267
|
"""
|
214
|
-
super().__init__((xedges, yedges), [xlabel, ylabel, zlabel])
|
268
|
+
super().__init__((xedges, yedges), label, [xlabel, ylabel, zlabel])
|
215
269
|
|
216
270
|
def _do_plot(self, axes: matplotlib.axes._axes.Axes, logz: bool = False, **kwargs) -> None:
|
217
271
|
"""Overloaded make_plot() method.
|
@@ -222,5 +276,5 @@ class Histogram2d(AbstractHistogram):
|
|
222
276
|
vmax = kwargs.pop('vmax', None)
|
223
277
|
kwargs.setdefault('norm', matplotlib.colors.LogNorm(vmin, vmax))
|
224
278
|
mappable = axes.pcolormesh(*self._edges, self.content.T, **kwargs)
|
225
|
-
plt.colorbar(mappable, ax=axes, label=self.
|
226
|
-
setup_axes(axes, xlabel=self.
|
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])
|
aptapy/modeling.py
CHANGED
@@ -21,15 +21,18 @@ import functools
|
|
21
21
|
import inspect
|
22
22
|
from abc import ABC, abstractmethod
|
23
23
|
from dataclasses import dataclass
|
24
|
+
from itertools import chain
|
24
25
|
from numbers import Number
|
25
|
-
from typing import Iterator, Tuple
|
26
|
+
from typing import Callable, Iterator, Sequence, Tuple
|
26
27
|
|
27
28
|
import matplotlib.pyplot as plt
|
28
29
|
import numpy as np
|
29
30
|
import uncertainties
|
30
31
|
from scipy.optimize import curve_fit
|
32
|
+
from scipy.stats import chi2
|
31
33
|
|
32
|
-
from
|
34
|
+
from .hist import Histogram1d
|
35
|
+
from .typing_ import ArrayLike
|
33
36
|
|
34
37
|
|
35
38
|
class Format(str, enum.Enum):
|
@@ -63,6 +66,11 @@ class FitParameter:
|
|
63
66
|
|
64
67
|
We are wrapping this into a property because, arguably, the parameter name is
|
65
68
|
the only thing we never, ever want to change after the fact.
|
69
|
+
|
70
|
+
Returns
|
71
|
+
-------
|
72
|
+
name : str
|
73
|
+
The parameter name.
|
66
74
|
"""
|
67
75
|
return self._name
|
68
76
|
|
@@ -72,11 +80,21 @@ class FitParameter:
|
|
72
80
|
|
73
81
|
We are wrapping this into a property because we interact with this member
|
74
82
|
via the freeze() and thaw() methods.
|
83
|
+
|
84
|
+
Returns
|
85
|
+
-------
|
86
|
+
frozen : bool
|
87
|
+
True if the parameter is frozen.
|
75
88
|
"""
|
76
89
|
return self._frozen
|
77
90
|
|
78
91
|
def is_bound(self) -> bool:
|
79
92
|
"""Return True if the parameter is bounded.
|
93
|
+
|
94
|
+
Returns
|
95
|
+
-------
|
96
|
+
bounded : bool
|
97
|
+
True if the parameter is bounded.
|
80
98
|
"""
|
81
99
|
return not np.isinf(self.minimum) or not np.isinf(self.maximum)
|
82
100
|
|
@@ -94,7 +112,12 @@ class FitParameter:
|
|
94
112
|
Arguments
|
95
113
|
---------
|
96
114
|
name : str
|
97
|
-
The name for the new FitParameter object.
|
115
|
+
The name for the new :class:`FitParameter` object.
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
parameter : FitParameter
|
120
|
+
The new :class:`FitParameter` object.
|
98
121
|
"""
|
99
122
|
return self.__class__(self.value, name, minimum=self.minimum, maximum=self.maximum)
|
100
123
|
|
@@ -132,11 +155,70 @@ class FitParameter:
|
|
132
155
|
|
133
156
|
def ufloat(self) -> uncertainties.ufloat:
|
134
157
|
"""Return the parameter value and error as a ufloat object.
|
158
|
+
|
159
|
+
Returns
|
160
|
+
-------
|
161
|
+
ufloat : uncertainties.ufloat
|
162
|
+
The parameter value and error as a ufloat object.
|
135
163
|
"""
|
136
164
|
return uncertainties.ufloat(self.value, self.error)
|
137
165
|
|
166
|
+
def pull(self, expected: float) -> float:
|
167
|
+
"""Calculate the pull of the parameter with respect to a given expected value.
|
168
|
+
|
169
|
+
Arguments
|
170
|
+
---------
|
171
|
+
expected : float
|
172
|
+
The expected value for the parameter.
|
173
|
+
|
174
|
+
Returns
|
175
|
+
-------
|
176
|
+
pull : float
|
177
|
+
The pull of the parameter with respect to the expected value, defined as
|
178
|
+
(value - expected) / error.
|
179
|
+
|
180
|
+
Raises
|
181
|
+
------
|
182
|
+
RuntimeError
|
183
|
+
If the parameter has no error associated to it.
|
184
|
+
"""
|
185
|
+
if self.error is None or self.error <= 0.:
|
186
|
+
raise RuntimeError(f"Cannot calculate pull for parameter {self.name} "
|
187
|
+
"with no error")
|
188
|
+
return (self.value - expected) / self.error
|
189
|
+
|
190
|
+
def compatible_with(self, expected: float, num_sigma: float = 3.) -> bool:
|
191
|
+
"""Check if the parameter is compatible with an expected value within
|
192
|
+
n_sigma.
|
193
|
+
|
194
|
+
Arguments
|
195
|
+
---------
|
196
|
+
expected : float
|
197
|
+
The expected value for the parameter.
|
198
|
+
|
199
|
+
num_sigma : float, optional
|
200
|
+
The number of sigmas to use for the compatibility check (default 3).
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
compatible : bool
|
205
|
+
True if the parameter is compatible with the expected value within
|
206
|
+
num_sigma.
|
207
|
+
"""
|
208
|
+
return abs(self.pull(expected)) <= num_sigma
|
209
|
+
|
138
210
|
def __format__(self, spec: str) -> str:
|
139
211
|
"""String formatting.
|
212
|
+
|
213
|
+
Arguments
|
214
|
+
---------
|
215
|
+
spec : str
|
216
|
+
The format specification.
|
217
|
+
|
218
|
+
Returns
|
219
|
+
-------
|
220
|
+
text : str
|
221
|
+
The formatted string.
|
140
222
|
"""
|
141
223
|
# Keep in mind Python passes an empty string explicitly when you call
|
142
224
|
# f"{parameter}", so we can't really assign a default value to spec.
|
@@ -145,8 +227,10 @@ class FitParameter:
|
|
145
227
|
if spec.endswith(Format.LATEX):
|
146
228
|
param = f"${param}$"
|
147
229
|
else:
|
148
|
-
spec
|
149
|
-
|
230
|
+
# Note in this case we are not passing the format spec to format(), as
|
231
|
+
# the only thing we can do in absence of an error is to use the
|
232
|
+
# Python default formatting.
|
233
|
+
param = format(self.value, "g")
|
150
234
|
text = f"{self._name.title()}: {param}"
|
151
235
|
info = []
|
152
236
|
if self._frozen:
|
@@ -165,6 +249,11 @@ class FitParameter:
|
|
165
249
|
This is meant to provide a more human-readable version of the parameter formatting
|
166
250
|
than the default ``__repr__`` implementation from the dataclass decorator, and it
|
167
251
|
is what is used in the actual printout of the fit parameters from a fit.
|
252
|
+
|
253
|
+
Returns
|
254
|
+
-------
|
255
|
+
text : str
|
256
|
+
The formatted string.
|
168
257
|
"""
|
169
258
|
return format(self, Format.PRETTY)
|
170
259
|
|
@@ -177,7 +266,7 @@ class FitStatus:
|
|
177
266
|
|
178
267
|
chisquare: float = None
|
179
268
|
dof: int = None
|
180
|
-
|
269
|
+
pvalue: float = None
|
181
270
|
fit_range: Tuple[float, float] = None
|
182
271
|
|
183
272
|
def reset(self) -> None:
|
@@ -185,10 +274,44 @@ class FitStatus:
|
|
185
274
|
"""
|
186
275
|
self.chisquare = None
|
187
276
|
self.dof = None
|
277
|
+
self.pvalue = None
|
188
278
|
self.fit_range = None
|
189
279
|
|
280
|
+
def update(self, chisquare: float, dof: int = None) -> None:
|
281
|
+
"""Update the fit status, i.e., set the chisquare and calculate the
|
282
|
+
corresponding p-value.
|
283
|
+
|
284
|
+
Arguments
|
285
|
+
---------
|
286
|
+
chisquare : float
|
287
|
+
The chisquare of the fit.
|
288
|
+
|
289
|
+
dof : int, optional
|
290
|
+
The number of degrees of freedom of the fit.
|
291
|
+
"""
|
292
|
+
self.chisquare = chisquare
|
293
|
+
if dof is not None:
|
294
|
+
self.dof = dof
|
295
|
+
self.pvalue = chi2.sf(self.chisquare, self.dof)
|
296
|
+
# chi2.sf() returns the survival function, i.e., 1 - cdf. If the survival
|
297
|
+
# function is > 0.5, we flip it around, so that we always report the smallest
|
298
|
+
# tail, and the pvalue is the probability of obtaining a chisquare value more
|
299
|
+
# `extreme` of the one we got.
|
300
|
+
if self.pvalue > 0.5:
|
301
|
+
self.pvalue = 1. - self.pvalue
|
302
|
+
|
190
303
|
def __format__(self, spec: str) -> str:
|
191
304
|
"""String formatting.
|
305
|
+
|
306
|
+
Arguments
|
307
|
+
---------
|
308
|
+
spec : str
|
309
|
+
The format specification.
|
310
|
+
|
311
|
+
Returns
|
312
|
+
-------
|
313
|
+
text : str
|
314
|
+
The formatted string.
|
192
315
|
"""
|
193
316
|
if self.chisquare is None:
|
194
317
|
return "N/A"
|
@@ -200,119 +323,227 @@ class FitStatus:
|
|
200
323
|
|
201
324
|
def __str__(self) -> str:
|
202
325
|
"""String formatting.
|
326
|
+
|
327
|
+
Returns
|
328
|
+
-------
|
329
|
+
text : str
|
330
|
+
The formatted string.
|
203
331
|
"""
|
204
332
|
return format(self, Format.PRETTY)
|
205
333
|
|
206
334
|
|
207
|
-
class
|
335
|
+
class AbstractFitModelBase(ABC):
|
208
336
|
|
209
|
-
"""Abstract base class for
|
337
|
+
"""Abstract base class for all the fit classes.
|
338
|
+
|
339
|
+
This is a acting a base class for both simple fit models and for composite models
|
340
|
+
(e.g., sums of simple ones).
|
210
341
|
"""
|
211
342
|
|
212
343
|
def __init__(self) -> None:
|
213
344
|
"""Constructor.
|
214
|
-
|
215
|
-
Here we loop over the FitParameter objects defined at the class level, and
|
216
|
-
create copies that are attached to the instance, so that the latter has its
|
217
|
-
own state.
|
218
345
|
"""
|
219
|
-
self._parameters = []
|
220
|
-
for name, value in self.__class__.__dict__.items():
|
221
|
-
if isinstance(value, FitParameter):
|
222
|
-
parameter = value.copy(name)
|
223
|
-
# Note we also set one instance attribute for each parameter so
|
224
|
-
# that we can use the notation model.parameter
|
225
|
-
setattr(self, name, parameter)
|
226
|
-
self._parameters.append(parameter)
|
227
|
-
# Fit status object holding all the additional information from the fit.
|
228
346
|
self.status = FitStatus()
|
229
347
|
|
230
|
-
|
231
|
-
"""Return the model name.
|
232
|
-
"""
|
233
|
-
return self.__class__.__name__
|
234
|
-
|
348
|
+
@abstractmethod
|
235
349
|
def __len__(self) -> int:
|
236
|
-
"""
|
237
|
-
|
238
|
-
return len(self._parameters)
|
350
|
+
"""Delegated to concrete classes: this should return the `total` number of
|
351
|
+
fit parameters (not only the free ones) in the model.
|
239
352
|
|
240
|
-
|
241
|
-
"""Iteration protocol.
|
242
|
-
"""
|
243
|
-
return iter(self._parameters)
|
353
|
+
.. note::
|
244
354
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
355
|
+
I still have mixed feelings about this method, as it is not clear whether
|
356
|
+
we are returning the number of parameters, or the number of free parameters,
|
357
|
+
but I think it is fine, as long as we document it. Also note that, while
|
358
|
+
the number of parameters is fixed once and for all for simple models,
|
359
|
+
it can change at runtime for composite models.
|
249
360
|
|
250
|
-
|
251
|
-
|
361
|
+
Returns
|
362
|
+
-------
|
363
|
+
n : int
|
364
|
+
The total number of fit parameters in the model.
|
252
365
|
"""
|
253
|
-
return tuple(parameter for parameter in self if not parameter.frozen)
|
254
366
|
|
255
|
-
|
256
|
-
|
367
|
+
@abstractmethod
|
368
|
+
def __iter__(self) -> Iterator[FitParameter]:
|
369
|
+
"""Delegated to concrete classes: this should return an iterator over `all`
|
370
|
+
the fit parameters in the model.
|
371
|
+
|
372
|
+
Returns
|
373
|
+
-------
|
374
|
+
iterator : Iterator[FitParameter]
|
375
|
+
An iterator over all the fit parameters in the model.
|
257
376
|
"""
|
258
|
-
return tuple(parameter.value for parameter in self.free_parameters())
|
259
377
|
|
260
378
|
@staticmethod
|
261
379
|
@abstractmethod
|
262
|
-
def evaluate(x: ArrayLike, *parameter_values:
|
263
|
-
"""Evaluate the model at a given
|
264
|
-
for a given set of model parameters.
|
380
|
+
def evaluate(x: ArrayLike, *parameter_values: Sequence[float]) -> ArrayLike:
|
381
|
+
"""Evaluate the model at a given set of parameter values.
|
265
382
|
|
266
383
|
Arguments
|
267
384
|
---------
|
268
385
|
x : array_like
|
269
386
|
The value(s) of the independent variable.
|
270
387
|
|
271
|
-
|
388
|
+
parameter_values : sequence of float
|
272
389
|
The value of the model parameters.
|
390
|
+
|
391
|
+
Returns
|
392
|
+
-------
|
393
|
+
y : array_like
|
394
|
+
The value(s) of the model at the given value(s) of the independent variable
|
395
|
+
for a given set of parameter values.
|
273
396
|
"""
|
274
397
|
|
398
|
+
def name(self) -> str:
|
399
|
+
"""Return the model name, e.g., for legends.
|
400
|
+
|
401
|
+
Note this can be reimplemented in concrete subclasses, but it should provide
|
402
|
+
a sensible default value in most circumstances.
|
403
|
+
|
404
|
+
Returns
|
405
|
+
-------
|
406
|
+
name : str
|
407
|
+
The model name.
|
408
|
+
"""
|
409
|
+
return self.__class__.__name__
|
410
|
+
|
275
411
|
def __call__(self, x: ArrayLike) -> ArrayLike:
|
276
412
|
"""Evaluate the model at the current value of the parameters.
|
413
|
+
|
414
|
+
Arguments
|
415
|
+
---------
|
416
|
+
x : array_like
|
417
|
+
The value(s) of the independent variable.
|
418
|
+
|
419
|
+
Returns
|
420
|
+
-------
|
421
|
+
y : array_like
|
422
|
+
The value(s) of the model at the given value(s) of the independent variable
|
423
|
+
for the current set of parameter values.
|
277
424
|
"""
|
278
425
|
return self.evaluate(x, *self.parameter_values())
|
279
426
|
|
280
|
-
def
|
281
|
-
"""
|
282
|
-
|
283
|
-
|
284
|
-
|
427
|
+
def init_parameters(self, xdata: ArrayLike, ydata: ArrayLike, sigma: ArrayLike) -> None:
|
428
|
+
"""Optional hook to change the current parameter values of the model, prior
|
429
|
+
to a fit, based on the input data.
|
430
|
+
|
431
|
+
Arguments
|
432
|
+
---------
|
433
|
+
xdata : array_like
|
434
|
+
The input values of the independent variable.
|
435
|
+
|
436
|
+
ydata : array_like
|
437
|
+
The input values of the dependent variable.
|
285
438
|
|
286
|
-
|
287
|
-
|
439
|
+
sigma : array_like
|
440
|
+
The input uncertainties on the dependent variable.
|
288
441
|
"""
|
289
442
|
# pylint: disable=unused-argument
|
290
443
|
return
|
291
444
|
|
292
|
-
def
|
293
|
-
"""
|
445
|
+
def parameter_values(self) -> Tuple[float]:
|
446
|
+
"""Return the current parameter values.
|
447
|
+
|
448
|
+
Note this only relies on the __iter__() method, so it works both for simple
|
449
|
+
and composite models.
|
450
|
+
|
451
|
+
Returns
|
452
|
+
-------
|
453
|
+
values : tuple of float
|
454
|
+
The current parameter values.
|
294
455
|
"""
|
295
|
-
|
296
|
-
|
297
|
-
|
456
|
+
return tuple(parameter.value for parameter in self)
|
457
|
+
|
458
|
+
def free_parameters(self) -> Tuple[FitParameter]:
|
459
|
+
"""Return the list of free parameters.
|
460
|
+
|
461
|
+
Note this only relies on the __iter__() method, so it works both for simple
|
462
|
+
and composite models.
|
463
|
+
|
464
|
+
Returns
|
465
|
+
-------
|
466
|
+
parameters : tuple of FitParameter
|
467
|
+
The list of free parameters.
|
468
|
+
"""
|
469
|
+
return tuple(parameter for parameter in self if not parameter.frozen)
|
470
|
+
|
471
|
+
def free_parameter_values(self) -> Tuple[float]:
|
472
|
+
"""Return the current parameter values.
|
473
|
+
|
474
|
+
Returns
|
475
|
+
-------
|
476
|
+
values : tuple of float
|
477
|
+
The current parameter values.
|
478
|
+
"""
|
479
|
+
return tuple(parameter.value for parameter in self.free_parameters())
|
298
480
|
|
299
481
|
def bounds(self) -> Tuple[ArrayLike, ArrayLike]:
|
300
482
|
"""Return the bounds on the fit parameters in a form that can be use by the
|
301
483
|
fitting method.
|
484
|
+
|
485
|
+
Returns
|
486
|
+
-------
|
487
|
+
bounds : 2-tuple of array_like
|
488
|
+
The lower and upper bounds on the (free) fit parameters.
|
302
489
|
"""
|
303
490
|
free_parameters = self.free_parameters()
|
304
491
|
return (tuple(parameter.minimum for parameter in free_parameters),
|
305
492
|
tuple(parameter.maximum for parameter in free_parameters))
|
306
493
|
|
307
|
-
def
|
494
|
+
def update_parameters(self, popt: np.ndarray, pcov: np.ndarray) -> None:
|
495
|
+
"""Update the model parameters based on the output of the ``curve_fit()`` call.
|
496
|
+
|
497
|
+
Arguments
|
498
|
+
---------
|
499
|
+
popt : array_like
|
500
|
+
The optimal values for the fit parameters.
|
501
|
+
|
502
|
+
pcov : array_like
|
503
|
+
The covariance matrix for the fit parameters.
|
504
|
+
"""
|
505
|
+
for parameter, value, error in zip(self.free_parameters(), popt, np.sqrt(pcov.diagonal())):
|
506
|
+
parameter.value = value
|
507
|
+
parameter.error = error
|
508
|
+
|
509
|
+
def calculate_chisquare(self, xdata: np.ndarray, ydata: np.ndarray, sigma) -> float:
|
308
510
|
"""Calculate the chisquare of the fit to some input data with the current
|
309
511
|
model parameters.
|
512
|
+
|
513
|
+
Arguments
|
514
|
+
---------
|
515
|
+
xdata : array_like
|
516
|
+
The input values of the independent variable.
|
517
|
+
|
518
|
+
ydata : array_like
|
519
|
+
The input values of the dependent variable.
|
520
|
+
|
521
|
+
sigma : array_like
|
522
|
+
The input uncertainties on the dependent variable.
|
523
|
+
|
524
|
+
Returns
|
525
|
+
-------
|
526
|
+
chisquare : float
|
527
|
+
The chisquare of the fit.
|
310
528
|
"""
|
311
529
|
return float((((ydata - self(xdata)) / sigma)**2.).sum())
|
312
530
|
|
313
531
|
@staticmethod
|
314
|
-
def freeze(model_function, **constraints):
|
532
|
+
def freeze(model_function, **constraints) -> Callable:
|
315
533
|
"""Freeze a subset of the model parameters.
|
534
|
+
|
535
|
+
Arguments
|
536
|
+
---------
|
537
|
+
model_function : callable
|
538
|
+
The model function to freeze parameters for.
|
539
|
+
|
540
|
+
constraints : dict
|
541
|
+
The parameters to freeze, as keyword arguments.
|
542
|
+
|
543
|
+
Returns
|
544
|
+
-------
|
545
|
+
wrapper : callable
|
546
|
+
A wrapper around the model function with the given parameters frozen.
|
316
547
|
"""
|
317
548
|
if not constraints:
|
318
549
|
return model_function
|
@@ -363,8 +594,36 @@ class AbstractFitModel(ABC):
|
|
363
594
|
|
364
595
|
def fit(self, xdata: ArrayLike, ydata: ArrayLike, p0: ArrayLike = None,
|
365
596
|
sigma: ArrayLike = 1., absolute_sigma: bool = False, xmin: float = -np.inf,
|
366
|
-
xmax: float = np.inf, **kwargs) ->
|
597
|
+
xmax: float = np.inf, **kwargs) -> FitStatus:
|
367
598
|
"""Fit a series of points.
|
599
|
+
|
600
|
+
Arguments
|
601
|
+
---------
|
602
|
+
xdata : array_like
|
603
|
+
The input values of the independent variable.
|
604
|
+
|
605
|
+
ydata : array_like
|
606
|
+
The input values of the dependent variable.
|
607
|
+
|
608
|
+
p0 : array_like, optional
|
609
|
+
The initial values for the fit parameters.
|
610
|
+
|
611
|
+
sigma : array_like
|
612
|
+
The input uncertainties on the dependent variable.
|
613
|
+
|
614
|
+
absolute_sigma : bool, optional (default False)
|
615
|
+
See the `curve_fit()` documentation for details.
|
616
|
+
|
617
|
+
xmin : float, optional (default -inf)
|
618
|
+
The minimum value of the independent variable to fit.
|
619
|
+
|
620
|
+
xmax : float, optional (default inf)
|
621
|
+
The maximum value of the independent variable to fit.
|
622
|
+
|
623
|
+
Returns
|
624
|
+
-------
|
625
|
+
status : FitStatus
|
626
|
+
The status of the fit.
|
368
627
|
"""
|
369
628
|
# Reset the fit status.
|
370
629
|
self.status.reset()
|
@@ -374,16 +633,21 @@ class AbstractFitModel(ABC):
|
|
374
633
|
# the broadcast facilities.
|
375
634
|
xdata = np.asarray(xdata)
|
376
635
|
ydata = np.asarray(ydata)
|
636
|
+
if isinstance(sigma, Number):
|
637
|
+
sigma = np.full(ydata.shape, sigma)
|
638
|
+
sigma = np.asarray(sigma)
|
377
639
|
# If we are fitting over a subrange, filter the input data.
|
378
640
|
mask = np.logical_and(xdata >= xmin, xdata <= xmax)
|
641
|
+
# Also, filter out any points with non-positive uncertainties.
|
642
|
+
mask = np.logical_and(mask, sigma > 0.)
|
379
643
|
# (And, since we are at it, make sure we have enough degrees of freedom.)
|
380
|
-
self.status.dof = int(mask.sum() - len(self))
|
644
|
+
self.status.dof = int(mask.sum() - len(self.free_parameters()))
|
381
645
|
if self.status.dof < 0:
|
382
646
|
raise RuntimeError(f"{self.name()} has no degrees of freedom")
|
383
647
|
xdata = xdata[mask]
|
384
648
|
ydata = ydata[mask]
|
385
|
-
|
386
|
-
|
649
|
+
sigma = sigma[mask]
|
650
|
+
|
387
651
|
# Cache the fit range for later use.
|
388
652
|
self.status.fit_range = (xdata.min(), xdata.max())
|
389
653
|
|
@@ -399,16 +663,38 @@ class AbstractFitModel(ABC):
|
|
399
663
|
model = self.freeze(self.evaluate, **constraints)
|
400
664
|
args = model, xdata, ydata, p0, sigma, absolute_sigma, True, self.bounds()
|
401
665
|
popt, pcov = curve_fit(*args, **kwargs)
|
402
|
-
self.
|
403
|
-
self.status.
|
666
|
+
self.update_parameters(popt, pcov)
|
667
|
+
self.status.update(self.calculate_chisquare(xdata, ydata, sigma))
|
404
668
|
return self.status
|
405
669
|
|
670
|
+
def fit_histogram(self, histogram: Histogram1d, p0: ArrayLike = None, **kwargs) -> None:
|
671
|
+
"""Convenience function for fitting a 1-dimensional histogram.
|
672
|
+
|
673
|
+
Arguments
|
674
|
+
---------
|
675
|
+
histogram : Histogram1d
|
676
|
+
The histogram to fit.
|
677
|
+
|
678
|
+
p0 : array_like, optional
|
679
|
+
The initial values for the fit parameters.
|
680
|
+
|
681
|
+
kwargs : dict, optional
|
682
|
+
Additional keyword arguments passed to `fit()`.
|
683
|
+
"""
|
684
|
+
args = histogram.bin_centers(), histogram.content, p0, histogram.errors
|
685
|
+
return self.fit(*args, **kwargs)
|
686
|
+
|
406
687
|
def default_plotting_range(self) -> Tuple[float, float]:
|
407
688
|
"""Return the default plotting range for the model.
|
408
689
|
|
409
|
-
This can be
|
690
|
+
This can be reimplemented in concrete models, and can be parameter-dependent
|
410
691
|
(e.g., for a gaussian we might want to plot within 5 sigma from the mean by
|
411
|
-
|
692
|
+
default).
|
693
|
+
|
694
|
+
Returns
|
695
|
+
-------
|
696
|
+
Tuple[float, float]
|
697
|
+
The default plotting range for the model.
|
412
698
|
"""
|
413
699
|
return (0., 1.)
|
414
700
|
|
@@ -416,6 +702,22 @@ class AbstractFitModel(ABC):
|
|
416
702
|
fit_padding: float = 0.) -> Tuple[float, float]:
|
417
703
|
"""Convenience function trying to come up with the most sensible plot range
|
418
704
|
for the model.
|
705
|
+
|
706
|
+
Arguments
|
707
|
+
---------
|
708
|
+
xmin : float, optional
|
709
|
+
The minimum value of the independent variable to plot.
|
710
|
+
|
711
|
+
xmax : float, optional
|
712
|
+
The maximum value of the independent variable to plot.
|
713
|
+
|
714
|
+
fit_padding : float, optional
|
715
|
+
The amount of padding to add to the fit range.
|
716
|
+
|
717
|
+
Returns
|
718
|
+
-------
|
719
|
+
Tuple[float, float]
|
720
|
+
The plotting range for the model.
|
419
721
|
"""
|
420
722
|
# If we have fitted the model to some data, we take the fit range and pad it
|
421
723
|
# a little bit.
|
@@ -434,27 +736,191 @@ class AbstractFitModel(ABC):
|
|
434
736
|
_xmax = xmax
|
435
737
|
return (_xmin, _xmax)
|
436
738
|
|
437
|
-
def plot(self, xmin: float = None, xmax: float = None, num_points: int = 200) ->
|
739
|
+
def plot(self, xmin: float = None, xmax: float = None, num_points: int = 200) -> np.ndarray:
|
438
740
|
"""Plot the model.
|
741
|
+
|
742
|
+
Arguments
|
743
|
+
---------
|
744
|
+
xmin : float, optional
|
745
|
+
The minimum value of the independent variable to plot.
|
746
|
+
|
747
|
+
xmax : float, optional
|
748
|
+
The maximum value of the independent variable to plot.
|
749
|
+
|
750
|
+
num_points : int, optional
|
751
|
+
The number of points to use for the plot.
|
752
|
+
|
753
|
+
Returns
|
754
|
+
-------
|
755
|
+
x : np.ndarray
|
756
|
+
The x values used for the plot, that can be used downstream to add
|
757
|
+
artists on the plot itself (e.g., composite models can use the same
|
758
|
+
grid to draw the components).
|
439
759
|
"""
|
440
760
|
x = np.linspace(*self._plotting_range(xmin, xmax), num_points)
|
441
761
|
y = self(x)
|
442
762
|
plt.plot(x, y, label=format(self, Format.LATEX))
|
763
|
+
return x
|
443
764
|
|
444
765
|
def __format__(self, spec: str) -> str:
|
445
766
|
"""String formatting.
|
446
|
-
|
447
|
-
|
448
|
-
|
767
|
+
|
768
|
+
Arguments
|
769
|
+
---------
|
770
|
+
spec : str
|
771
|
+
The format specification.
|
772
|
+
|
773
|
+
Returns
|
774
|
+
-------
|
775
|
+
text : str
|
776
|
+
The formatted string.
|
777
|
+
"""
|
778
|
+
text = f"{self.name()}\n"
|
779
|
+
if self.status is not None:
|
780
|
+
text = f"{text}{format(self.status, spec)}\n"
|
781
|
+
for parameter in self:
|
449
782
|
text = f"{text}{format(parameter, spec)}\n"
|
450
783
|
return text.strip("\n")
|
451
784
|
|
452
785
|
def __str__(self):
|
453
786
|
"""String formatting.
|
787
|
+
|
788
|
+
Returns
|
789
|
+
-------
|
790
|
+
text : str
|
791
|
+
The formatted string.
|
454
792
|
"""
|
455
793
|
return format(self, Format.PRETTY)
|
456
794
|
|
457
795
|
|
796
|
+
class AbstractFitModel(AbstractFitModelBase):
|
797
|
+
|
798
|
+
"""Abstract base class for a fit model.
|
799
|
+
"""
|
800
|
+
|
801
|
+
def __init__(self) -> None:
|
802
|
+
"""Constructor.
|
803
|
+
|
804
|
+
Here we loop over the FitParameter objects defined at the class level, and
|
805
|
+
create copies that are attached to the instance, so that the latter has its
|
806
|
+
own state.
|
807
|
+
"""
|
808
|
+
super().__init__()
|
809
|
+
self._parameters = []
|
810
|
+
for name, value in self.__class__.__dict__.items():
|
811
|
+
if isinstance(value, FitParameter):
|
812
|
+
parameter = value.copy(name)
|
813
|
+
# Note we also set one instance attribute for each parameter so
|
814
|
+
# that we can use the notation model.parameter
|
815
|
+
setattr(self, name, parameter)
|
816
|
+
self._parameters.append(parameter)
|
817
|
+
|
818
|
+
def __len__(self) -> int:
|
819
|
+
"""Return the `total` number of fit parameters in the model.
|
820
|
+
"""
|
821
|
+
return len(self._parameters)
|
822
|
+
|
823
|
+
def __iter__(self) -> Iterator[FitParameter]:
|
824
|
+
"""Iterate over `all` the model parameters.
|
825
|
+
"""
|
826
|
+
return iter(self._parameters)
|
827
|
+
|
828
|
+
def __add__(self, other):
|
829
|
+
"""Model sum.
|
830
|
+
"""
|
831
|
+
if not isinstance(other, AbstractFitModel):
|
832
|
+
raise TypeError(f"{other} is not a fit model")
|
833
|
+
return FitModelSum(self, other)
|
834
|
+
|
835
|
+
|
836
|
+
class FitModelSum(AbstractFitModelBase):
|
837
|
+
|
838
|
+
"""Composite model representing the sum of an arbitrary number of simple models.
|
839
|
+
|
840
|
+
Arguments
|
841
|
+
---------
|
842
|
+
components : sequence of AbstractFitModel
|
843
|
+
The components of the composite model.
|
844
|
+
"""
|
845
|
+
|
846
|
+
def __init__(self, *components: AbstractFitModel) -> None:
|
847
|
+
"""Constructor.
|
848
|
+
"""
|
849
|
+
super().__init__()
|
850
|
+
self._components = components
|
851
|
+
|
852
|
+
def name(self) -> str:
|
853
|
+
"""Return the model name.
|
854
|
+
"""
|
855
|
+
return " + ".join(component.name() for component in self._components)
|
856
|
+
|
857
|
+
def __len__(self) -> int:
|
858
|
+
"""Return the sum of `all` the fit parameters in the underlying models.
|
859
|
+
"""
|
860
|
+
return sum(len(component) for component in self._components)
|
861
|
+
|
862
|
+
def __iter__(self) -> Iterator[FitParameter]:
|
863
|
+
"""Iterate over `all` the parameters of the underlying components.
|
864
|
+
"""
|
865
|
+
return chain(*self._components)
|
866
|
+
|
867
|
+
def evaluate(self, x: ArrayLike, *parameter_values) -> ArrayLike:
|
868
|
+
"""Overloaded method for evaluating the model.
|
869
|
+
|
870
|
+
Note this is not a static method, as we need to access the list of components
|
871
|
+
to sum over.
|
872
|
+
"""
|
873
|
+
# pylint: disable=arguments-differ
|
874
|
+
cursor = 0
|
875
|
+
value = np.zeros(x.shape)
|
876
|
+
for component in self._components:
|
877
|
+
value += component.evaluate(x, *parameter_values[cursor:cursor + len(component)])
|
878
|
+
cursor += len(component)
|
879
|
+
return value
|
880
|
+
|
881
|
+
def plot(self, xmin: float = None, xmax: float = None, num_points: int = 200) -> None:
|
882
|
+
"""Overloaded method for plotting the model.
|
883
|
+
"""
|
884
|
+
x = super().plot(xmin, xmax, num_points)
|
885
|
+
color = plt.gca().lines[-1].get_color()
|
886
|
+
for component in self._components:
|
887
|
+
y = component(x)
|
888
|
+
plt.plot(x, y, label=None, ls="--", color=color)
|
889
|
+
|
890
|
+
def __format__(self, spec: str) -> str:
|
891
|
+
"""String formatting.
|
892
|
+
|
893
|
+
Arguments
|
894
|
+
---------
|
895
|
+
spec : str
|
896
|
+
The format specification.
|
897
|
+
|
898
|
+
Returns
|
899
|
+
-------
|
900
|
+
text : str
|
901
|
+
The formatted string.
|
902
|
+
"""
|
903
|
+
text = f"{self.name()}\n"
|
904
|
+
if self.status is not None:
|
905
|
+
text = f"{text}{format(self.status, spec)}\n"
|
906
|
+
for component in self._components:
|
907
|
+
text = f"{text}[{component.name()}]\n"
|
908
|
+
for parameter in component:
|
909
|
+
text = f"{text}{format(parameter, spec)}\n"
|
910
|
+
return text.strip("\n")
|
911
|
+
|
912
|
+
def __add__(self, other: AbstractFitModel) -> "FitModelSum":
|
913
|
+
"""Implementation of the model sum (i.e., using the `+` operator).
|
914
|
+
|
915
|
+
Note that, in the spirit of keeping the interfaces as simple as possible,
|
916
|
+
we are not implementing in-place addition (i.e., `+=`), and we only
|
917
|
+
allow ``AbstractFitModel`` objects (not ``FitModelSum``) on the right
|
918
|
+
hand side, which is all is needed to support the sum of an arbitrary
|
919
|
+
number of models.
|
920
|
+
"""
|
921
|
+
return self.__class__(*self._components, other)
|
922
|
+
|
923
|
+
|
458
924
|
class Constant(AbstractFitModel):
|
459
925
|
|
460
926
|
"""Constant model.
|
@@ -465,7 +931,7 @@ class Constant(AbstractFitModel):
|
|
465
931
|
@staticmethod
|
466
932
|
def evaluate(x: ArrayLike, value: float) -> ArrayLike:
|
467
933
|
# pylint: disable=arguments-differ
|
468
|
-
return np.full(
|
934
|
+
return np.full(x.shape, value)
|
469
935
|
|
470
936
|
|
471
937
|
class Line(AbstractFitModel):
|
@@ -503,13 +969,27 @@ class Gaussian(AbstractFitModel):
|
|
503
969
|
|
504
970
|
prefactor = FitParameter(1.)
|
505
971
|
mean = FitParameter(0.)
|
506
|
-
sigma = FitParameter(1.)
|
972
|
+
sigma = FitParameter(1., minimum=0.)
|
973
|
+
|
974
|
+
_NORM_CONSTANT = 1. / np.sqrt(2. * np.pi)
|
975
|
+
_SIGMA_TO_FWHM = 2. * np.sqrt(2. * np.log(2.))
|
507
976
|
|
508
977
|
@staticmethod
|
509
978
|
def evaluate(x: ArrayLike, prefactor: float, mean: float, sigma: float) -> ArrayLike:
|
510
979
|
# pylint: disable=arguments-differ
|
511
|
-
|
980
|
+
z = (x - mean) / sigma
|
981
|
+
return prefactor * Gaussian._NORM_CONSTANT / sigma * np.exp(-0.5 * z**2.)
|
512
982
|
|
513
983
|
def default_plotting_range(self, num_sigma: int = 5) -> Tuple[float, float]:
|
514
984
|
mean, half_width = self.mean.value, num_sigma * self.sigma.value
|
515
985
|
return (mean - half_width, mean + half_width)
|
986
|
+
|
987
|
+
def fwhm(self) -> uncertainties.ufloat:
|
988
|
+
"""Return the full-width at half-maximum (FWHM) of the gaussian.
|
989
|
+
|
990
|
+
Returns
|
991
|
+
-------
|
992
|
+
fwhm : uncertainties.ufloat
|
993
|
+
The FWHM of the gaussian.
|
994
|
+
"""
|
995
|
+
return self.sigma.ufloat() * self._SIGMA_TO_FWHM
|
aptapy/plotting.py
CHANGED
@@ -84,11 +84,18 @@ def _set(key: str, value: Any):
|
|
84
84
|
logger.warning(f"{exception}, skipping...")
|
85
85
|
|
86
86
|
|
87
|
-
def configure() -> None:
|
87
|
+
def configure(*args) -> None:
|
88
88
|
"""See https://matplotlib.org/stable/users/explain/customizing.html for more
|
89
89
|
information.
|
90
|
+
|
91
|
+
.. note::
|
92
|
+
|
93
|
+
Note that this function can be used as a hook by Sphinx Gallery to
|
94
|
+
configure the plotting environment for each example, so that the matplotlib
|
95
|
+
configuration is consistent across all examples and is not reset each time.
|
96
|
+
This is the reason why the function signature includes unused arguments.
|
90
97
|
"""
|
91
|
-
# pylint:disable=too-many-statements
|
98
|
+
# pylint:disable=too-many-statements, unused-argument
|
92
99
|
|
93
100
|
# Backends
|
94
101
|
_set("interactive", False)
|
aptapy/strip.py
ADDED
@@ -0,0 +1,92 @@
|
|
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
|
+
"""Strip charts.
|
17
|
+
"""
|
18
|
+
|
19
|
+
import collections
|
20
|
+
from typing import Sequence
|
21
|
+
|
22
|
+
import numpy as np
|
23
|
+
|
24
|
+
from .plotting import plt, setup_axes
|
25
|
+
|
26
|
+
|
27
|
+
class StripChart:
|
28
|
+
|
29
|
+
"""Class describing a sliding strip chart, that is, a scatter plot where the
|
30
|
+
number of points is limited to a maximum, so that the thing acts essentially
|
31
|
+
as a sliding window, typically in time.
|
32
|
+
|
33
|
+
Arguments
|
34
|
+
---------
|
35
|
+
max_length : int, optional
|
36
|
+
the maximum number of points to keep in the strip chart. If None (the default),
|
37
|
+
the number of points is unlimited.
|
38
|
+
|
39
|
+
label : str, optional
|
40
|
+
a text label for the data series (default is None).
|
41
|
+
|
42
|
+
xlabel : str, optional
|
43
|
+
the label for the x axis.
|
44
|
+
|
45
|
+
ylabel : str, optional
|
46
|
+
the label for the y axis.
|
47
|
+
|
48
|
+
datetime : bool, optional
|
49
|
+
if True, the x values are treated as POSIX timestamps and converted to
|
50
|
+
datetime objects for plotting purposes (default is False).
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, max_length: int = None, label: str = '', xlabel: str = None,
|
54
|
+
ylabel: str = None, datetime: bool = False) -> None:
|
55
|
+
"""Constructor.
|
56
|
+
"""
|
57
|
+
self.label = label
|
58
|
+
self.xlabel = xlabel
|
59
|
+
self.ylabel = ylabel
|
60
|
+
self._datetime = datetime
|
61
|
+
self.x = collections.deque(maxlen=max_length)
|
62
|
+
self.y = collections.deque(maxlen=max_length)
|
63
|
+
|
64
|
+
def clear(self) -> None:
|
65
|
+
"""Reset the strip chart.
|
66
|
+
"""
|
67
|
+
self.x.clear()
|
68
|
+
self.y.clear()
|
69
|
+
|
70
|
+
def append(self, x: float, y: float) -> None:
|
71
|
+
"""Append a data point to the strip chart.
|
72
|
+
"""
|
73
|
+
self.x.append(x)
|
74
|
+
self.y.append(y)
|
75
|
+
|
76
|
+
def extend(self, x: Sequence[float], y: Sequence[float]) -> None:
|
77
|
+
"""Append multiple data points to the strip chart.
|
78
|
+
"""
|
79
|
+
if len(x) != len(y):
|
80
|
+
raise ValueError("x and y must have the same length")
|
81
|
+
self.x.extend(x)
|
82
|
+
self.y.extend(y)
|
83
|
+
|
84
|
+
def plot(self, axes=None, **kwargs) -> None:
|
85
|
+
"""Plot the strip chart.
|
86
|
+
"""
|
87
|
+
kwargs.setdefault("label", self.label)
|
88
|
+
if axes is None:
|
89
|
+
axes = plt.gca()
|
90
|
+
x = np.array(self.x).astype('datetime64[s]') if self._datetime else self.x
|
91
|
+
axes.plot(x, self.y, **kwargs)
|
92
|
+
setup_axes(axes, xlabel=self.xlabel, ylabel=self.ylabel, grids=True)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: aptapy
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Statistical tools for online monitoring and analysis
|
5
5
|
Project-URL: Homepage, https://github.com/lucabaldini/aptapy
|
6
6
|
Project-URL: Issues, https://github.com/lucabaldini/aptapy/issues
|
@@ -694,9 +694,17 @@ Requires-Dist: pytest; extra == 'dev'
|
|
694
694
|
Requires-Dist: ruff; extra == 'dev'
|
695
695
|
Provides-Extra: docs
|
696
696
|
Requires-Dist: sphinx; extra == 'docs'
|
697
|
+
Requires-Dist: sphinx-gallery; extra == 'docs'
|
697
698
|
Requires-Dist: sphinxawesome-theme; extra == 'docs'
|
698
699
|
Description-Content-Type: text/markdown
|
699
700
|
|
700
701
|
<img src="docs/_static/logo.png" alt="logo" width="175"/>
|
701
702
|
|
702
|
-
|
703
|
+

|
704
|
+
[](https://github.com/lucabaldini/aptapy/actions/workflows/ci.yml)
|
705
|
+
[](https://github.com/lucabaldini/aptapy/actions/workflows/docs.yml)
|
706
|
+
[](https://lucabaldini.github.io/aptapy/)
|
707
|
+
[](https://techforpalestine.org/learn-more)
|
708
|
+
|
709
|
+
Statistical tools for online monitoring and analysis.
|
710
|
+
[Read more](https://lucabaldini.github.io/aptapy/).
|
@@ -0,0 +1,12 @@
|
|
1
|
+
aptapy/__init__.py,sha256=a7Au1ukdeJbjiIZ-UL-qZE1xk-d2WnKKkoqjg_0SzqA,1707
|
2
|
+
aptapy/_version.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
3
|
+
aptapy/hist.py,sha256=jvHULR2lW_gyoemNaRI-vpqhFqmLLCB319CaKjwU69E,9752
|
4
|
+
aptapy/modeling.py,sha256=mHHHFMYmMuXTRQD2yR1XLM6E4KaBk8md7Z7dDp5ReD8,32566
|
5
|
+
aptapy/plotting.py,sha256=p9YNdrcFcTimRCtoXcV3zORaEd4EfMtsDd4ETxGKKHM,27483
|
6
|
+
aptapy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
aptapy/strip.py,sha256=qGsVXWp-Dz-lz7KQxktMifUTNkSxh8ZQwYme8_bealQ,3026
|
8
|
+
aptapy/typing_.py,sha256=JIbEqKI8kn_fd90yDt0JmI1AojjmLhAEB_1RfMFxLx4,807
|
9
|
+
aptapy-0.3.0.dist-info/METADATA,sha256=ae7Q-HEWZCiZW8QjbAMNqCwZQVTFv52oH2n4J5tMRfg,42131
|
10
|
+
aptapy-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
+
aptapy-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
12
|
+
aptapy-0.3.0.dist-info/RECORD,,
|
aptapy-0.2.0.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
aptapy/__init__.py,sha256=a7Au1ukdeJbjiIZ-UL-qZE1xk-d2WnKKkoqjg_0SzqA,1707
|
2
|
-
aptapy/_version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
3
|
-
aptapy/hist.py,sha256=5fiaYEnSQ7b_jSsZdRvXrIGr2vTMlW_55jkFpE8L3Sw,8158
|
4
|
-
aptapy/modeling.py,sha256=V8DVfmwFyUfy9-YZXd9Hz5rlyGEy12xoyn-oJ4ss3W0,18236
|
5
|
-
aptapy/plotting.py,sha256=ZixAVF83qIuITjzQJBUvMNCK-REilzyAt0vxgcMbCOk,27125
|
6
|
-
aptapy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
aptapy/typing_.py,sha256=JIbEqKI8kn_fd90yDt0JmI1AojjmLhAEB_1RfMFxLx4,807
|
8
|
-
aptapy-0.2.0.dist-info/METADATA,sha256=yvpj0nK8DKrbXcccDPqKeiruxJYAGEK0FYaIKa4UpS0,41456
|
9
|
-
aptapy-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
-
aptapy-0.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
11
|
-
aptapy-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|