dcio 0.1.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.
- dcio/__init__.py +18 -0
- dcio/analysis/__init__.py +35 -0
- dcio/analysis/bursts.py +243 -0
- dcio/analysis/histogram.py +171 -0
- dcio/analysis/record.py +495 -0
- dcio/cli/__init__.py +1 -0
- dcio/cli/csv2ssd.py +195 -0
- dcio/cli/scn2txt.py +110 -0
- dcio/cli/txt2scn.py +136 -0
- dcio/formats/__init__.py +1 -0
- dcio/formats/scan_ini.py +519 -0
- dcio/formats/scn.py +606 -0
- dcio/formats/ssd.py +484 -0
- dcio-0.1.0.dist-info/METADATA +169 -0
- dcio-0.1.0.dist-info/RECORD +19 -0
- dcio-0.1.0.dist-info/WHEEL +5 -0
- dcio-0.1.0.dist-info/entry_points.txt +4 -0
- dcio-0.1.0.dist-info/licenses/LICENSE +21 -0
- dcio-0.1.0.dist-info/top_level.txt +1 -0
dcio/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dcio
|
|
3
|
+
======
|
|
4
|
+
I/O library for electrophysiology file formats used by the DCProgs suite.
|
|
5
|
+
|
|
6
|
+
Supported formats
|
|
7
|
+
-----------------
|
|
8
|
+
* **SCN** – SCAN idealised single-channel records (read + write).
|
|
9
|
+
|
|
10
|
+
Quick start
|
|
11
|
+
-----------
|
|
12
|
+
>>> from dcio.formats.scn import read, write
|
|
13
|
+
>>> rec = read("myfile.scn")
|
|
14
|
+
>>> print(rec)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__author__ = "Remis Lape"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Single-channel record analysis."""
|
|
2
|
+
|
|
3
|
+
from dcio.analysis.histogram import (
|
|
4
|
+
bins_per_decade,
|
|
5
|
+
log_bin_edges,
|
|
6
|
+
log_bin_histogram,
|
|
7
|
+
staircase,
|
|
8
|
+
)
|
|
9
|
+
from dcio.analysis.bursts import (
|
|
10
|
+
bursts_from_record,
|
|
11
|
+
extract_burst_intervals,
|
|
12
|
+
extract_bursts,
|
|
13
|
+
)
|
|
14
|
+
from dcio.analysis.record import (
|
|
15
|
+
Periods,
|
|
16
|
+
SingleChannelRecord,
|
|
17
|
+
from_scn,
|
|
18
|
+
impose_resolution,
|
|
19
|
+
set_periods,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Periods",
|
|
24
|
+
"SingleChannelRecord",
|
|
25
|
+
"from_scn",
|
|
26
|
+
"impose_resolution",
|
|
27
|
+
"set_periods",
|
|
28
|
+
"extract_bursts",
|
|
29
|
+
"extract_burst_intervals",
|
|
30
|
+
"bursts_from_record",
|
|
31
|
+
"bins_per_decade",
|
|
32
|
+
"log_bin_edges",
|
|
33
|
+
"log_bin_histogram",
|
|
34
|
+
"staircase",
|
|
35
|
+
]
|
dcio/analysis/bursts.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Burst segmentation of an idealised single-channel record.
|
|
2
|
+
|
|
3
|
+
A *burst* is a run of openings and short shuttings delimited by shut intervals
|
|
4
|
+
longer than a critical time ``tcrit``. Segmentation sits directly on top of
|
|
5
|
+
:mod:`dcio.analysis.record`: it takes the resolved intervals a dead time
|
|
6
|
+
produced and cuts them into bursts.
|
|
7
|
+
|
|
8
|
+
Typical workflow::
|
|
9
|
+
|
|
10
|
+
from dcio.formats.scn import read
|
|
11
|
+
from dcio.analysis.record import from_scn
|
|
12
|
+
from dcio.analysis.bursts import extract_bursts
|
|
13
|
+
|
|
14
|
+
scr = from_scn(read("file.scn"), tres=25e-6)
|
|
15
|
+
lengths, n_openings = extract_bursts(
|
|
16
|
+
scr.resolved_intervals, scr.resolved_amplitudes,
|
|
17
|
+
tcrit=4e-3, flags=scr.resolved_flags,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
or, equivalently, straight from the record::
|
|
21
|
+
|
|
22
|
+
from dcio.analysis.bursts import bursts_from_record
|
|
23
|
+
|
|
24
|
+
lengths, n_openings = bursts_from_record(scr, tcrit=4e-3)
|
|
25
|
+
|
|
26
|
+
Convention
|
|
27
|
+
----------
|
|
28
|
+
The convention is the one EKDIST states in ``Bursts.slice_bursts`` and dcpyps
|
|
29
|
+
followed:
|
|
30
|
+
|
|
31
|
+
1. no gap longer than ``tcrit`` is required before the first burst of a
|
|
32
|
+
record -- the first defined opening is a valid burst start;
|
|
33
|
+
2. an unusable interval is a valid end of burst.
|
|
34
|
+
|
|
35
|
+
Both matter. Time-course fitting in SCAN leaves the last interval of a record
|
|
36
|
+
with no defined length, flagged unusable; it still ends the burst before it. A
|
|
37
|
+
leading interval may likewise be bad, and is discarded, but the first defined
|
|
38
|
+
opening after it starts a real burst. Dropping the runs at both ends -- as
|
|
39
|
+
earlier versions of this code did -- loses two bursts from every record, which
|
|
40
|
+
at 30 uM in the Burzomato 2004 set is a third of the data.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
|
|
47
|
+
from dcio.formats.scn import FLAG_UNUSABLE
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"extract_bursts",
|
|
51
|
+
"extract_burst_intervals",
|
|
52
|
+
"bursts_from_record",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _burst_segments(intervals, amplitudes, tcrit, flags=None):
|
|
57
|
+
"""Split a record into bursts and return them as (interval, amplitude) pairs.
|
|
58
|
+
|
|
59
|
+
Shared by :func:`extract_bursts` and :func:`extract_burst_intervals`, which
|
|
60
|
+
differ only in what they report about one and the same segmentation.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
intervals, amplitudes : array_like
|
|
65
|
+
Alternating interval record (ideal or apparent). Durations in
|
|
66
|
+
seconds, amplitudes in pA with 0 meaning shut.
|
|
67
|
+
tcrit : float
|
|
68
|
+
Critical shut time separating within- from between-burst gaps [s].
|
|
69
|
+
Its magnitude only -- see :func:`bursts_from_record` for the sign
|
|
70
|
+
convention that HJCFIT places on the same number.
|
|
71
|
+
flags : array_like of int, optional
|
|
72
|
+
Per-interval SCN property flags. An interval is unusable when
|
|
73
|
+
``flags & FLAG_UNUSABLE`` is set. Without them no interval is treated
|
|
74
|
+
as unusable, which is right for a simulated record and wrong for an
|
|
75
|
+
experimental one.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
list of list of (float, float)
|
|
80
|
+
One list of ``(interval, amplitude)`` pairs per burst, each starting
|
|
81
|
+
and ending on an opening.
|
|
82
|
+
"""
|
|
83
|
+
intervals = np.asarray(intervals, float)
|
|
84
|
+
amplitudes = np.asarray(amplitudes, float)
|
|
85
|
+
if intervals.size == 0:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
if flags is None:
|
|
89
|
+
unusable = np.zeros(intervals.shape, dtype=bool)
|
|
90
|
+
else:
|
|
91
|
+
unusable = (np.asarray(flags, int) & FLAG_UNUSABLE) != 0
|
|
92
|
+
|
|
93
|
+
# A burst ends at a between-burst gap or at an interval of unknown length.
|
|
94
|
+
# An unusable interval has no measured duration, so it is never compared
|
|
95
|
+
# with tcrit.
|
|
96
|
+
# Strictly greater: tcrit is the time such that gaps *longer* than it are
|
|
97
|
+
# between-burst. EKDIST's Bursts.slice_bursts uses the same test, and a
|
|
98
|
+
# shut interval exactly equal to tcrit is within-burst by that reading.
|
|
99
|
+
separator = ((amplitudes == 0.0) & (intervals > tcrit) & ~unusable) | unusable
|
|
100
|
+
|
|
101
|
+
# Trim to the first and last defined opening, so the record begins and
|
|
102
|
+
# ends on one. What lies outside is a shut interval or an unusable one,
|
|
103
|
+
# and neither belongs to a burst.
|
|
104
|
+
opening = (amplitudes != 0.0) & ~unusable
|
|
105
|
+
if not opening.any():
|
|
106
|
+
return []
|
|
107
|
+
lo = int(np.argmax(opening))
|
|
108
|
+
hi = int(len(opening) - np.argmax(opening[::-1]))
|
|
109
|
+
|
|
110
|
+
raw, seg = [], []
|
|
111
|
+
for t, a, sep in zip(intervals[lo:hi], amplitudes[lo:hi], separator[lo:hi]):
|
|
112
|
+
if sep:
|
|
113
|
+
if seg:
|
|
114
|
+
raw.append(seg)
|
|
115
|
+
seg = []
|
|
116
|
+
else:
|
|
117
|
+
seg.append((t, a))
|
|
118
|
+
if seg:
|
|
119
|
+
raw.append(seg)
|
|
120
|
+
|
|
121
|
+
bursts = []
|
|
122
|
+
for seg in raw:
|
|
123
|
+
while seg and seg[0][1] == 0.0: # trim leading shut
|
|
124
|
+
seg = seg[1:]
|
|
125
|
+
while seg and seg[-1][1] == 0.0: # trim trailing shut
|
|
126
|
+
seg = seg[:-1]
|
|
127
|
+
if seg:
|
|
128
|
+
bursts.append(seg)
|
|
129
|
+
return bursts
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def extract_bursts(intervals, amplitudes, tcrit, flags=None):
|
|
133
|
+
"""Split a record into bursts at shut intervals longer than ``tcrit``.
|
|
134
|
+
|
|
135
|
+
Each burst is trimmed to start and end on an opening.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
intervals, amplitudes : array_like
|
|
140
|
+
Alternating interval record (ideal or apparent).
|
|
141
|
+
tcrit : float
|
|
142
|
+
Critical shut time separating within- from between-burst gaps [s].
|
|
143
|
+
flags : array_like of int, optional
|
|
144
|
+
Per-interval SCN property flags; see :func:`_burst_segments`.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
lengths : ndarray
|
|
149
|
+
Burst lengths [s] (first opening start to last opening end).
|
|
150
|
+
n_openings : ndarray of int
|
|
151
|
+
Number of (apparent) openings in each burst.
|
|
152
|
+
|
|
153
|
+
See Also
|
|
154
|
+
--------
|
|
155
|
+
extract_burst_intervals : the same bursts, as interval sequences.
|
|
156
|
+
"""
|
|
157
|
+
bursts = _burst_segments(intervals, amplitudes, tcrit, flags)
|
|
158
|
+
lengths = [sum(t for t, _ in seg) for seg in bursts]
|
|
159
|
+
nops = [sum(1 for _, a in seg if a != 0.0) for seg in bursts]
|
|
160
|
+
return np.array(lengths), np.array(nops, dtype=int)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def extract_burst_intervals(intervals, amplitudes, tcrit, flags=None):
|
|
164
|
+
"""Split a record into bursts and return the intervals of each.
|
|
165
|
+
|
|
166
|
+
The segmentation is that of :func:`extract_bursts`, which reduces each
|
|
167
|
+
burst to its length and its number of openings. Maximum-likelihood
|
|
168
|
+
fitting of missed-events mechanisms needs the interval sequences
|
|
169
|
+
themselves: the HJC likelihood is a product of matrices, one per interval,
|
|
170
|
+
so the order and the individual durations both matter.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
intervals, amplitudes : array_like
|
|
175
|
+
Alternating interval record (ideal or apparent).
|
|
176
|
+
tcrit : float
|
|
177
|
+
Critical shut time separating within- from between-burst gaps [s].
|
|
178
|
+
flags : array_like of int, optional
|
|
179
|
+
Per-interval SCN property flags; see :func:`_burst_segments`.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
list of ndarray
|
|
184
|
+
One array of interval durations [s] per burst, alternating open and
|
|
185
|
+
shut and both starting and ending with an opening -- so every array
|
|
186
|
+
has odd length, which is what the missed-events likelihood requires.
|
|
187
|
+
|
|
188
|
+
See Also
|
|
189
|
+
--------
|
|
190
|
+
extract_bursts : the same bursts, as lengths and opening counts.
|
|
191
|
+
"""
|
|
192
|
+
return [np.array([t for t, _ in seg], dtype=float)
|
|
193
|
+
for seg in _burst_segments(intervals, amplitudes, tcrit, flags)]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def bursts_from_record(record, tcrit, intervals_only=False):
|
|
197
|
+
"""Segment a :class:`~dcio.analysis.record.SingleChannelRecord` into bursts.
|
|
198
|
+
|
|
199
|
+
Convenience over :func:`extract_bursts` that takes the intervals,
|
|
200
|
+
amplitudes and flags from the record itself, so a caller can get neither
|
|
201
|
+
the flags nor the input series wrong.
|
|
202
|
+
|
|
203
|
+
Segmentation runs on the record's **periods**, not on its resolved
|
|
204
|
+
intervals, and that distinction is not cosmetic.
|
|
205
|
+
:func:`~dcio.analysis.record.impose_resolution` emits a fresh open
|
|
206
|
+
interval whenever the fitted amplitude changes, so a record idealised with
|
|
207
|
+
sub-conductance levels contains runs of consecutive open intervals: the
|
|
208
|
+
experimental example shipped with dcio has 1770 such adjacencies in 3232
|
|
209
|
+
resolved intervals. Segmenting those directly gives bursts that do not
|
|
210
|
+
alternate open/shut, and counts each sub-level as a separate opening.
|
|
211
|
+
:func:`~dcio.analysis.record.set_periods` merges them, which is what makes
|
|
212
|
+
the burst an alternating sequence the missed-events likelihood can consume.
|
|
213
|
+
|
|
214
|
+
Burst count and burst lengths are identical either way; the number of
|
|
215
|
+
openings per burst is not.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
record : SingleChannelRecord
|
|
220
|
+
A record with a dead time already applied.
|
|
221
|
+
tcrit : float
|
|
222
|
+
Critical shut time [s]. Only its magnitude is used. The sign carries
|
|
223
|
+
no meaning here, but it does downstream: HJCFIT reads a negative
|
|
224
|
+
``tcrit`` as a flag selecting equilibrium vectors (Colquhoun & Hawkes
|
|
225
|
+
1982) over CHS vectors (Colquhoun, Hawkes & Srodzinski 1996), and the
|
|
226
|
+
same number is passed to both. Taking the magnitude here means a
|
|
227
|
+
caller can hand the same value to either without thinking about it.
|
|
228
|
+
intervals_only : bool, default False
|
|
229
|
+
Return the per-burst interval sequences instead of lengths and
|
|
230
|
+
opening counts.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
lengths, n_openings : ndarray, ndarray
|
|
235
|
+
When *intervals_only* is False.
|
|
236
|
+
list of ndarray
|
|
237
|
+
When *intervals_only* is True.
|
|
238
|
+
"""
|
|
239
|
+
periods = record.periods
|
|
240
|
+
args = (periods.intervals, periods.amplitudes, abs(tcrit), periods.flags)
|
|
241
|
+
if intervals_only:
|
|
242
|
+
return extract_burst_intervals(*args)
|
|
243
|
+
return extract_bursts(*args)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Log-binned dwell-time histograms: the arithmetic, not the figure.
|
|
2
|
+
|
|
3
|
+
Dwell times span decades, so they are histogrammed on a logarithmic time axis:
|
|
4
|
+
each bin is a fixed ratio wider than the last, and the ordinate is plotted as
|
|
5
|
+
the square root of the count (Sigworth & Sine 1987), which makes an exponential
|
|
6
|
+
component a recognisable peak rather than a featureless decay.
|
|
7
|
+
|
|
8
|
+
This module carries only the numbers -- bin counts, bin edges, and the step
|
|
9
|
+
coordinates a staircase plot needs. Drawing stays with the caller: EKDIST
|
|
10
|
+
draws for a person, HJCFIT draws inside a notebook, SCALCS feeds a Qt canvas,
|
|
11
|
+
and those are legitimately different figures over the same bins.
|
|
12
|
+
|
|
13
|
+
from dcio.analysis.histogram import log_bin_histogram, staircase
|
|
14
|
+
|
|
15
|
+
counts, edges, nbdec = log_bin_histogram(open_periods, tres=25e-6)
|
|
16
|
+
x, y = staircase(edges, counts)
|
|
17
|
+
ax.semilogx(x, np.sqrt(y))
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"bins_per_decade",
|
|
26
|
+
"log_bin_edges",
|
|
27
|
+
"log_bin_histogram",
|
|
28
|
+
"staircase",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def bins_per_decade(n):
|
|
33
|
+
"""Bins per decade for a sample of *n* intervals.
|
|
34
|
+
|
|
35
|
+
The DCprogs convention: widen the bins for smaller samples so the counts
|
|
36
|
+
in them stay usable.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
n : int
|
|
41
|
+
Number of intervals in the sample.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
int
|
|
46
|
+
5 at 300 intervals or fewer, 8 to 1000, 10 to 3000, 12 above.
|
|
47
|
+
"""
|
|
48
|
+
if n <= 300:
|
|
49
|
+
return 5
|
|
50
|
+
if n <= 1000:
|
|
51
|
+
return 8
|
|
52
|
+
if n <= 3000:
|
|
53
|
+
return 10
|
|
54
|
+
return 12
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def log_bin_edges(intervals, tres, nbdec=None):
|
|
58
|
+
"""Geometric bin edges for a dwell-time histogram.
|
|
59
|
+
|
|
60
|
+
Bins start at the resolution and each is ``10 ** (1 / nbdec)`` times wider
|
|
61
|
+
than the last, so *nbdec* of them span a decade. The last edge is at or
|
|
62
|
+
above the longest interval, rounded up to a whole decade.
|
|
63
|
+
|
|
64
|
+
That rounding is the part worth stating. Earlier implementations in this
|
|
65
|
+
stack wrote the decade round-up as ``exp(ceil(log(max)))``, which rounds up
|
|
66
|
+
to the next power of *e* rather than of ten. Because it can land below the
|
|
67
|
+
longest interval, ``numpy.histogram`` then drops the tail of the
|
|
68
|
+
distribution without saying so -- in randomly drawn exponential samples
|
|
69
|
+
that happens about three times in ten.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
intervals : array_like
|
|
74
|
+
Observed dwell times, in seconds. Must contain a positive value.
|
|
75
|
+
tres : float
|
|
76
|
+
Resolution, in seconds. The histogram starts here.
|
|
77
|
+
nbdec : int, optional
|
|
78
|
+
Bins per decade. Chosen from the sample size when omitted; see
|
|
79
|
+
:func:`bins_per_decade`.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
edges : ndarray
|
|
84
|
+
Bin edges, in seconds, ascending from *tres*.
|
|
85
|
+
nbdec : int
|
|
86
|
+
The number of bins per decade actually used.
|
|
87
|
+
|
|
88
|
+
Raises
|
|
89
|
+
------
|
|
90
|
+
ValueError
|
|
91
|
+
If *tres* is not positive, or no interval is positive.
|
|
92
|
+
"""
|
|
93
|
+
intervals = np.asarray(intervals, dtype=float)
|
|
94
|
+
if tres <= 0.0:
|
|
95
|
+
raise ValueError(f"tres must be positive, got {tres!r}")
|
|
96
|
+
|
|
97
|
+
positive = intervals[intervals > 0.0]
|
|
98
|
+
if positive.size == 0:
|
|
99
|
+
raise ValueError("no positive interval to histogram")
|
|
100
|
+
|
|
101
|
+
if nbdec is None:
|
|
102
|
+
nbdec = bins_per_decade(len(intervals))
|
|
103
|
+
|
|
104
|
+
ratio = 10.0 ** (1.0 / nbdec)
|
|
105
|
+
tmax = 10.0 ** np.ceil(np.log10(positive.max()))
|
|
106
|
+
nbin = int(np.log(tmax / tres) / np.log(ratio)) + 1
|
|
107
|
+
return tres * ratio ** np.arange(nbin + 1), nbdec
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def log_bin_histogram(intervals, tres, nbdec=None):
|
|
111
|
+
"""Bin dwell times logarithmically.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
intervals : array_like
|
|
116
|
+
Observed dwell times, in seconds.
|
|
117
|
+
tres : float
|
|
118
|
+
Resolution, in seconds.
|
|
119
|
+
nbdec : int, optional
|
|
120
|
+
Bins per decade; see :func:`bins_per_decade`.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
counts : ndarray of int
|
|
125
|
+
Number of intervals in each bin.
|
|
126
|
+
edges : ndarray
|
|
127
|
+
Bin edges, in seconds.
|
|
128
|
+
nbdec : int
|
|
129
|
+
Bins per decade used.
|
|
130
|
+
|
|
131
|
+
Notes
|
|
132
|
+
-----
|
|
133
|
+
Intervals shorter than *tres* fall below the first edge and are not
|
|
134
|
+
counted; by construction of the record they should not exist. No interval
|
|
135
|
+
falls above the last edge -- see :func:`log_bin_edges`.
|
|
136
|
+
"""
|
|
137
|
+
intervals = np.asarray(intervals, dtype=float)
|
|
138
|
+
edges, nbdec = log_bin_edges(intervals, tres, nbdec)
|
|
139
|
+
counts, _ = np.histogram(intervals, bins=edges)
|
|
140
|
+
return counts, edges, nbdec
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def staircase(edges, counts):
|
|
144
|
+
"""Step-plot coordinates for a binned histogram.
|
|
145
|
+
|
|
146
|
+
Turns *n* counts and *n + 1* edges into the vertex sequence a line plot
|
|
147
|
+
needs to render as a staircase, closed to zero at both ends.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
edges : array_like
|
|
152
|
+
Bin edges, length ``n + 1``.
|
|
153
|
+
counts : array_like
|
|
154
|
+
Bin counts, length ``n``.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
x, y : ndarray
|
|
159
|
+
Vertices, each of length ``2 * (n + 1)``. Plot them directly; take
|
|
160
|
+
``sqrt(y)`` for the conventional square-root ordinate.
|
|
161
|
+
"""
|
|
162
|
+
edges = np.asarray(edges, dtype=float)
|
|
163
|
+
counts = np.asarray(counts)
|
|
164
|
+
if len(edges) != len(counts) + 1:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"expected {len(counts) + 1} edges for {len(counts)} counts, "
|
|
167
|
+
f"got {len(edges)}"
|
|
168
|
+
)
|
|
169
|
+
x = np.repeat(edges, 2)
|
|
170
|
+
y = np.concatenate(([0], np.repeat(counts, 2), [0]))
|
|
171
|
+
return x, y
|