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 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
+ ]
@@ -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