dcio 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- dcio-0.1.0/PKG-INFO +169 -0
- dcio-0.1.0/README.md +142 -0
- dcio-0.1.0/dcio/__init__.py +18 -0
- dcio-0.1.0/dcio/analysis/__init__.py +35 -0
- dcio-0.1.0/dcio/analysis/bursts.py +243 -0
- dcio-0.1.0/dcio/analysis/histogram.py +171 -0
- dcio-0.1.0/dcio/analysis/record.py +495 -0
- dcio-0.1.0/dcio/cli/__init__.py +1 -0
- dcio-0.1.0/dcio/cli/csv2ssd.py +195 -0
- dcio-0.1.0/dcio/cli/scn2txt.py +110 -0
- dcio-0.1.0/dcio/cli/txt2scn.py +136 -0
- dcio-0.1.0/dcio/formats/__init__.py +1 -0
- dcio-0.1.0/dcio/formats/scan_ini.py +519 -0
- dcio-0.1.0/dcio/formats/scn.py +606 -0
- dcio-0.1.0/dcio/formats/ssd.py +484 -0
- dcio-0.1.0/dcio.egg-info/PKG-INFO +169 -0
- dcio-0.1.0/dcio.egg-info/SOURCES.txt +29 -0
- dcio-0.1.0/dcio.egg-info/dependency_links.txt +1 -0
- dcio-0.1.0/dcio.egg-info/entry_points.txt +4 -0
- dcio-0.1.0/dcio.egg-info/requires.txt +5 -0
- dcio-0.1.0/dcio.egg-info/top_level.txt +1 -0
- dcio-0.1.0/pyproject.toml +56 -0
- dcio-0.1.0/setup.cfg +4 -0
- dcio-0.1.0/tests/test_bursts.py +212 -0
- dcio-0.1.0/tests/test_cli.py +363 -0
- dcio-0.1.0/tests/test_histogram.py +131 -0
- dcio-0.1.0/tests/test_record.py +432 -0
- dcio-0.1.0/tests/test_scan_ini.py +333 -0
- dcio-0.1.0/tests/test_scn.py +297 -0
- dcio-0.1.0/tests/test_ssd.py +304 -0
dcio-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Remis Lape
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dcio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dcio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: I/O and record layer for electrophysiology file formats used by the DCPROGS suite
|
|
5
|
+
Author: Remis Lape
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DCPROGS/dcio
|
|
8
|
+
Project-URL: Repository, https://github.com/DCPROGS/dcio
|
|
9
|
+
Project-URL: Issues, https://github.com/DCPROGS/dcio/issues
|
|
10
|
+
Keywords: electrophysiology,single-channel,ion-channel,patch-clamp,scn,dcprogs
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=2.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# dcio
|
|
29
|
+
|
|
30
|
+
I/O library for electrophysiology file formats used by the
|
|
31
|
+
[DCProgs](http://www.ucl.ac.uk/Pharmacology/dcpr95.html) single-channel
|
|
32
|
+
analysis suite.
|
|
33
|
+
|
|
34
|
+
## Supported formats
|
|
35
|
+
|
|
36
|
+
| Format | Extension | Read | Write | Description |
|
|
37
|
+
|--------|-----------|------|-------|-------------|
|
|
38
|
+
| SCAN | `.scn` | ✓ | ✓ | Idealised single-channel records |
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python ≥ 3.10 and NumPy ≥ 1.24.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dcio.formats.scn import read, write
|
|
52
|
+
import numpy as np
|
|
53
|
+
|
|
54
|
+
# --- Load an existing file ---
|
|
55
|
+
rec = read("myrecording.scn")
|
|
56
|
+
print(rec)
|
|
57
|
+
# SCNRecord('myrecording.scn')
|
|
58
|
+
# version : -103 (simulated)
|
|
59
|
+
# total : 1842 intervals
|
|
60
|
+
# usable : 1841
|
|
61
|
+
# open / shut : 921 / 920
|
|
62
|
+
# mean open : 2.3147 ms
|
|
63
|
+
# mean shut : 8.0412 ms
|
|
64
|
+
# calfac2 : 1 pA/ADC
|
|
65
|
+
# filter : 3000 Hz
|
|
66
|
+
|
|
67
|
+
# Intervals are in seconds; amplitudes in pA
|
|
68
|
+
print(rec.intervals[:5]) # array([0.01031, 0.00198, 0.02047, ...])
|
|
69
|
+
print(rec.amplitudes[:5]) # array([0., -48., 0., -48., 0.])
|
|
70
|
+
|
|
71
|
+
# Filter out unusable intervals
|
|
72
|
+
from dcio.formats.scn import FLAG_UNUSABLE
|
|
73
|
+
good = rec.usable_mask
|
|
74
|
+
open_durations_ms = rec.intervals[rec.open_mask & good] * 1e3
|
|
75
|
+
|
|
76
|
+
# --- Write a new file ---
|
|
77
|
+
intervals_s = np.array([0.010, 0.002, 0.020, 0.001, 0.050])
|
|
78
|
+
amplitudes = np.array([0, -48, 0, -48, 0 ], dtype=float)
|
|
79
|
+
flags = np.zeros(5, dtype=np.int8)
|
|
80
|
+
|
|
81
|
+
write("output.scn", intervals_s, amplitudes, flags,
|
|
82
|
+
title="My simulated record", ffilt=3000.0)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## SCNRecord attributes
|
|
86
|
+
|
|
87
|
+
| Attribute | Type | Description |
|
|
88
|
+
|-----------|------|-------------|
|
|
89
|
+
| `intervals` | `ndarray[float64]` | Interval durations in **seconds** |
|
|
90
|
+
| `amplitudes` | `ndarray[float64]` | Amplitudes in **pA** (0 = shut) |
|
|
91
|
+
| `flags` | `ndarray[int8]` | Property flags; `& 8 != 0` → unusable |
|
|
92
|
+
| `header` | `SCNHeader` | Parsed file metadata |
|
|
93
|
+
| `path` | `Path` | Source file path |
|
|
94
|
+
| `open_mask` | `ndarray[bool]` | True for open intervals |
|
|
95
|
+
| `shut_mask` | `ndarray[bool]` | True for shut intervals |
|
|
96
|
+
| `usable_mask` | `ndarray[bool]` | True when `flags & 8 == 0` |
|
|
97
|
+
|
|
98
|
+
## Record analysis
|
|
99
|
+
|
|
100
|
+
Beyond file I/O, `dcio.analysis` carries the record layer shared by the rest of
|
|
101
|
+
the DCPROGS stack: applying a dead time, grouping the result into open and shut
|
|
102
|
+
periods, and cutting it into bursts.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from dcio.formats.scn import read
|
|
106
|
+
from dcio.analysis import from_scn, bursts_from_record
|
|
107
|
+
|
|
108
|
+
record = from_scn(read("myrecording.scn"), tres=25e-6) # 25 us dead time
|
|
109
|
+
lengths, n_openings = bursts_from_record(record, tcrit=4e-3)
|
|
110
|
+
|
|
111
|
+
print(len(lengths), "bursts,", n_openings.mean(), "openings each")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`bursts_from_record` segments the record's **periods**, not its resolved
|
|
115
|
+
intervals. `impose_resolution` emits a fresh open interval at every change of
|
|
116
|
+
fitted amplitude, so a record idealised with sub-conductance levels contains
|
|
117
|
+
runs of consecutive openings; segmenting those directly gives bursts that do
|
|
118
|
+
not alternate open/shut. Burst count and lengths are the same either way, the
|
|
119
|
+
number of openings per burst is not.
|
|
120
|
+
|
|
121
|
+
Two conventions are worth stating, because implementations in this stack have
|
|
122
|
+
differed on them:
|
|
123
|
+
|
|
124
|
+
- no gap longer than `tcrit` is required before the first burst -- the first
|
|
125
|
+
defined opening starts one;
|
|
126
|
+
- an unusable interval ends the burst before it, and its (meaningless)
|
|
127
|
+
duration is never compared with `tcrit`.
|
|
128
|
+
|
|
129
|
+
Both follow SCAN's time-course fitting, which leaves the final interval of a
|
|
130
|
+
record with no defined length. Dropping the runs at each end instead loses two
|
|
131
|
+
bursts from every record.
|
|
132
|
+
|
|
133
|
+
### Dwell-time histograms
|
|
134
|
+
|
|
135
|
+
`dcio.analysis.histogram` carries the log-binning arithmetic -- bin counts, bin
|
|
136
|
+
edges, and staircase coordinates -- and nothing else. Drawing stays with the
|
|
137
|
+
caller, because EKDIST, HJCFIT and SCALCS legitimately want different figures
|
|
138
|
+
over the same bins.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import numpy as np
|
|
142
|
+
from dcio.analysis.histogram import log_bin_histogram, staircase
|
|
143
|
+
|
|
144
|
+
counts, edges, nbdec = log_bin_histogram(record.periods.open_intervals, tres=25e-6)
|
|
145
|
+
x, y = staircase(edges, counts)
|
|
146
|
+
ax.semilogx(x, np.sqrt(y)) # Sigworth-Sine square-root ordinate
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Bins start at the resolution and each is `10 ** (1/nbdec)` wider than the last,
|
|
150
|
+
with `nbdec` chosen from the sample size (5 / 8 / 10 / 12). The last edge is
|
|
151
|
+
rounded up to a whole decade, so no interval falls outside the bins -- writing
|
|
152
|
+
that round-up with a natural log instead, as earlier code in this stack did,
|
|
153
|
+
gives a power of e and lets `numpy.histogram` drop the tail of the distribution
|
|
154
|
+
without saying so.
|
|
155
|
+
|
|
156
|
+
## Running tests
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cd dcio # from the dcprogs root
|
|
160
|
+
pytest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Tests cover write validation, round-trip fidelity, flag handling, edge cases,
|
|
164
|
+
and optional smoke tests against legacy SCN files in `examples/scn/`.
|
|
165
|
+
|
|
166
|
+
## Documentation
|
|
167
|
+
|
|
168
|
+
See [`docs/scn_format.md`](docs/scn_format.md) for a complete description of
|
|
169
|
+
the binary file format, header fields, and unit conventions.
|
dcio-0.1.0/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# dcio
|
|
2
|
+
|
|
3
|
+
I/O library for electrophysiology file formats used by the
|
|
4
|
+
[DCProgs](http://www.ucl.ac.uk/Pharmacology/dcpr95.html) single-channel
|
|
5
|
+
analysis suite.
|
|
6
|
+
|
|
7
|
+
## Supported formats
|
|
8
|
+
|
|
9
|
+
| Format | Extension | Read | Write | Description |
|
|
10
|
+
|--------|-----------|------|-------|-------------|
|
|
11
|
+
| SCAN | `.scn` | ✓ | ✓ | Idealised single-channel records |
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e ".[dev]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python ≥ 3.10 and NumPy ≥ 1.24.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from dcio.formats.scn import read, write
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
# --- Load an existing file ---
|
|
28
|
+
rec = read("myrecording.scn")
|
|
29
|
+
print(rec)
|
|
30
|
+
# SCNRecord('myrecording.scn')
|
|
31
|
+
# version : -103 (simulated)
|
|
32
|
+
# total : 1842 intervals
|
|
33
|
+
# usable : 1841
|
|
34
|
+
# open / shut : 921 / 920
|
|
35
|
+
# mean open : 2.3147 ms
|
|
36
|
+
# mean shut : 8.0412 ms
|
|
37
|
+
# calfac2 : 1 pA/ADC
|
|
38
|
+
# filter : 3000 Hz
|
|
39
|
+
|
|
40
|
+
# Intervals are in seconds; amplitudes in pA
|
|
41
|
+
print(rec.intervals[:5]) # array([0.01031, 0.00198, 0.02047, ...])
|
|
42
|
+
print(rec.amplitudes[:5]) # array([0., -48., 0., -48., 0.])
|
|
43
|
+
|
|
44
|
+
# Filter out unusable intervals
|
|
45
|
+
from dcio.formats.scn import FLAG_UNUSABLE
|
|
46
|
+
good = rec.usable_mask
|
|
47
|
+
open_durations_ms = rec.intervals[rec.open_mask & good] * 1e3
|
|
48
|
+
|
|
49
|
+
# --- Write a new file ---
|
|
50
|
+
intervals_s = np.array([0.010, 0.002, 0.020, 0.001, 0.050])
|
|
51
|
+
amplitudes = np.array([0, -48, 0, -48, 0 ], dtype=float)
|
|
52
|
+
flags = np.zeros(5, dtype=np.int8)
|
|
53
|
+
|
|
54
|
+
write("output.scn", intervals_s, amplitudes, flags,
|
|
55
|
+
title="My simulated record", ffilt=3000.0)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## SCNRecord attributes
|
|
59
|
+
|
|
60
|
+
| Attribute | Type | Description |
|
|
61
|
+
|-----------|------|-------------|
|
|
62
|
+
| `intervals` | `ndarray[float64]` | Interval durations in **seconds** |
|
|
63
|
+
| `amplitudes` | `ndarray[float64]` | Amplitudes in **pA** (0 = shut) |
|
|
64
|
+
| `flags` | `ndarray[int8]` | Property flags; `& 8 != 0` → unusable |
|
|
65
|
+
| `header` | `SCNHeader` | Parsed file metadata |
|
|
66
|
+
| `path` | `Path` | Source file path |
|
|
67
|
+
| `open_mask` | `ndarray[bool]` | True for open intervals |
|
|
68
|
+
| `shut_mask` | `ndarray[bool]` | True for shut intervals |
|
|
69
|
+
| `usable_mask` | `ndarray[bool]` | True when `flags & 8 == 0` |
|
|
70
|
+
|
|
71
|
+
## Record analysis
|
|
72
|
+
|
|
73
|
+
Beyond file I/O, `dcio.analysis` carries the record layer shared by the rest of
|
|
74
|
+
the DCPROGS stack: applying a dead time, grouping the result into open and shut
|
|
75
|
+
periods, and cutting it into bursts.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from dcio.formats.scn import read
|
|
79
|
+
from dcio.analysis import from_scn, bursts_from_record
|
|
80
|
+
|
|
81
|
+
record = from_scn(read("myrecording.scn"), tres=25e-6) # 25 us dead time
|
|
82
|
+
lengths, n_openings = bursts_from_record(record, tcrit=4e-3)
|
|
83
|
+
|
|
84
|
+
print(len(lengths), "bursts,", n_openings.mean(), "openings each")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`bursts_from_record` segments the record's **periods**, not its resolved
|
|
88
|
+
intervals. `impose_resolution` emits a fresh open interval at every change of
|
|
89
|
+
fitted amplitude, so a record idealised with sub-conductance levels contains
|
|
90
|
+
runs of consecutive openings; segmenting those directly gives bursts that do
|
|
91
|
+
not alternate open/shut. Burst count and lengths are the same either way, the
|
|
92
|
+
number of openings per burst is not.
|
|
93
|
+
|
|
94
|
+
Two conventions are worth stating, because implementations in this stack have
|
|
95
|
+
differed on them:
|
|
96
|
+
|
|
97
|
+
- no gap longer than `tcrit` is required before the first burst -- the first
|
|
98
|
+
defined opening starts one;
|
|
99
|
+
- an unusable interval ends the burst before it, and its (meaningless)
|
|
100
|
+
duration is never compared with `tcrit`.
|
|
101
|
+
|
|
102
|
+
Both follow SCAN's time-course fitting, which leaves the final interval of a
|
|
103
|
+
record with no defined length. Dropping the runs at each end instead loses two
|
|
104
|
+
bursts from every record.
|
|
105
|
+
|
|
106
|
+
### Dwell-time histograms
|
|
107
|
+
|
|
108
|
+
`dcio.analysis.histogram` carries the log-binning arithmetic -- bin counts, bin
|
|
109
|
+
edges, and staircase coordinates -- and nothing else. Drawing stays with the
|
|
110
|
+
caller, because EKDIST, HJCFIT and SCALCS legitimately want different figures
|
|
111
|
+
over the same bins.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import numpy as np
|
|
115
|
+
from dcio.analysis.histogram import log_bin_histogram, staircase
|
|
116
|
+
|
|
117
|
+
counts, edges, nbdec = log_bin_histogram(record.periods.open_intervals, tres=25e-6)
|
|
118
|
+
x, y = staircase(edges, counts)
|
|
119
|
+
ax.semilogx(x, np.sqrt(y)) # Sigworth-Sine square-root ordinate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Bins start at the resolution and each is `10 ** (1/nbdec)` wider than the last,
|
|
123
|
+
with `nbdec` chosen from the sample size (5 / 8 / 10 / 12). The last edge is
|
|
124
|
+
rounded up to a whole decade, so no interval falls outside the bins -- writing
|
|
125
|
+
that round-up with a natural log instead, as earlier code in this stack did,
|
|
126
|
+
gives a power of e and lets `numpy.histogram` drop the tail of the distribution
|
|
127
|
+
without saying so.
|
|
128
|
+
|
|
129
|
+
## Running tests
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd dcio # from the dcprogs root
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Tests cover write validation, round-trip fidelity, flag handling, edge cases,
|
|
137
|
+
and optional smoke tests against legacy SCN files in `examples/scn/`.
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
See [`docs/scn_format.md`](docs/scn_format.md) for a complete description of
|
|
142
|
+
the binary file format, header fields, and unit conventions.
|
|
@@ -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)
|