Skip to content

Commit 1bf5c16

Browse files
authored
Merge branch 'main' into hergQC-docstring
2 parents 1acc6a9 + c397bff commit 1bf5c16

7 files changed

Lines changed: 100 additions & 98 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This repository contains a python package and scripts for handling time-series d
55
The package has been tested with data from a SyncroPatch 384, but may be adapted to work with data in other formats.
66
It can also be used to perform quality control (QC) as described in [Lei et al. (2019)](https://doi.org/10.1016%2Fj.bpj.2019.07.029).
77

8-
This package is tested on Ubuntu with Python 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13.
8+
This package is tested on Ubuntu with Python 3.10, 3.11, 3.12, 3.13 and 3.14.
99

1010
## Getting Started
1111

pcpostprocess/leak_correct.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def fit_linear_leak(current, voltage, times, ramp_start_index, ramp_end_index,
149149
ax4.plot(times, I_obs, label=r'$I_\mathrm{obs}$')
150150
ax4.plot(times, I_leak, linestyle='--', label=r'$I_\mathrm{L}$')
151151
ax4.plot(times, I_obs - I_leak,
152-
linestyle='--', alpha=0.5, label=r'$I_\mathrm{obs} - I_\mathrm{L}$')
152+
alpha=0.5, label=r'$I_\mathrm{obs} - I_\mathrm{L}$')
153153
ax4.legend(frameon=False)
154154

155155
if not os.path.exists(output_dir):

pcpostprocess/scripts/run_herg_qc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def main():
5151
parser.add_argument('--reversal_spread_threshold', type=float, default=10)
5252
parser.add_argument('--export_failed', action='store_true')
5353
parser.add_argument('--selection_file')
54-
parser.add_argument('--figsize', nargs=2, type=int, default=[5, 8])
54+
parser.add_argument('--figsize', nargs=2, type=int, default=[16, 18])
5555
parser.add_argument('--debug', action='store_true')
5656
parser.add_argument('--log_level', default='INFO')
5757
parser.add_argument('--Erev', default=-90.71, type=float)
@@ -517,7 +517,6 @@ def extract_protocol(readname, savename, time_strs, selected_wells, args):
517517
#  Find start of leak section
518518
desc = voltage_protocol.get_all_sections()
519519
ramp_bounds = detect_ramp_bounds(times, desc)
520-
tstart, tend = ramp_bounds
521520

522521
nsweeps_before = before_trace.NofSweeps = 2
523522
nsweeps_after = after_trace.NofSweeps = 2

pcpostprocess/subtraction_plots.py

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66

77
def setup_subtraction_grid(fig, nsweeps):
8-
# Use 5 x 2 grid when there are 2 sweeps
9-
gs = GridSpec(6, nsweeps, figure=fig)
10-
8+
# Use 6 x 2 grid when there are 2 sweeps
9+
gs = GridSpec(6, nsweeps, figure=fig, height_ratios=[0.7, 2, 2, 2, 0.7, 3])
10+
# gs.subplots(sharey='row')
1111
# plot protocol at the top
1212
protocol_axs = [fig.add_subplot(gs[0, i]) for i in range(nsweeps)]
1313

@@ -20,18 +20,20 @@ def setup_subtraction_grid(fig, nsweeps):
2020
# Leak corrected traces
2121
corrected_axs = [fig.add_subplot(gs[3, i]) for i in range(nsweeps)]
2222

23-
# Subtracted traces on one axis
24-
subtracted_ax = fig.add_subplot(gs[4, :])
25-
2623
# Long axis for protocol on the bottom (full width)
27-
long_protocol_ax = fig.add_subplot(gs[5, :])
24+
long_protocol_ax = fig.add_subplot(gs[4, :])
25+
26+
# Subtracted traces on one axis
27+
subtracted_ax = fig.add_subplot(gs[5, :])
2828

2929
for ax, cap in zip(list(protocol_axs) + list(before_axs)
3030
+ list(after_axs) + list(corrected_axs)
31-
+ [subtracted_ax] + [long_protocol_ax],
31+
+ [long_protocol_ax] + [subtracted_ax],
3232
'abcdefghijklm'):
3333
ax.spines[['top', 'right']].set_visible(False)
3434
ax.set_title(cap, loc='left', fontweight='bold')
35+
if cap != 'a':
36+
ax.sharex(protocol_axs[0])
3537

3638
return protocol_axs, before_axs, after_axs, corrected_axs, subtracted_ax, long_protocol_ax
3739

@@ -46,100 +48,91 @@ def do_subtraction_plot(fig, times, sweeps, before_currents, after_currents,
4648
protocol_axs, before_axs, after_axs, corrected_axs, \
4749
subtracted_ax, long_protocol_ax = axs
4850

49-
for ax in protocol_axs:
51+
for i, ax in enumerate(protocol_axs):
5052
ax.plot(times*1e-3, voltages, color='black')
51-
ax.set_xlabel('time (s)')
52-
ax.set_ylabel(r'$V_\mathrm{cmd}$ (mV)')
53+
ax.set_title(f'Well {well}, sweep {sweeps[i]}', fontweight='bold')
54+
ax.tick_params(axis='x', labelbottom=False)
55+
56+
protocol_axs[0].set_ylabel(r'$V_\mathrm{cmd}$ (mV)', fontsize=16)
5357

5458
all_leak_params_before = []
5559
all_leak_params_after = []
56-
for i in range(len(sweeps)):
57-
before_params, _ = fit_linear_leak(before_currents, voltages, times,
58-
*ramp_bounds)
59-
all_leak_params_before.append(before_params)
6060

61-
after_params, _ = fit_linear_leak(after_currents, voltages, times,
62-
*ramp_bounds)
63-
all_leak_params_after.append(after_params)
61+
alpha_of_zero = 0.2
62+
style_of_zero = '-'
63+
range_of_zero = [times[0]*1e-3, times[-1]*1e-3]
6464

6565
# Compute and store leak currents
6666
before_leak_currents = np.full((nsweeps, voltages.shape[0]),
6767
np.nan)
6868
after_leak_currents = np.full((nsweeps, voltages.shape[0]),
6969
np.nan)
70-
for i, sweep in enumerate(sweeps):
71-
72-
b0, b1 = all_leak_params_before[i]
73-
gleak = b1
74-
Eleak = -b1/b0
75-
before_leak_currents[i, :] = gleak * (voltages - Eleak)
7670

77-
b0, b1 = all_leak_params_after[i]
78-
gleak = b1
79-
Eleak = -b1/b0
71+
for i in range(len(sweeps)):
72+
before_params, before_leak_current = fit_linear_leak(before_currents[i], voltages, times,
73+
*ramp_bounds)
74+
before_leak_currents[i, :] = before_leak_current
75+
all_leak_params_before.append(before_params)
8076

81-
after_leak_currents[i, :] = gleak * (voltages - Eleak)
77+
after_params, after_leak_current = fit_linear_leak(after_currents[i], voltages, times,
78+
*ramp_bounds)
79+
all_leak_params_after.append(after_params)
80+
after_leak_currents[i, :] = after_leak_current
8281

8382
for i, (sweep, ax) in enumerate(zip(sweeps, before_axs)):
84-
gleak, Eleak = all_leak_params_before[i]
85-
ax.plot(times*1e-3, before_currents[i, :], label=f"pre-drug raw, sweep {sweep}")
83+
b0, b1 = all_leak_params_before[i]
84+
gleak = b1
85+
Eleak = -b0/b1
86+
87+
ax.plot(times*1e-3, before_currents[i, :], label="Pre-drug raw")
8688
ax.plot(times*1e-3, before_leak_currents[i, :],
87-
label=r'$I_\mathrm{L}$.' f"g={gleak:1E}, E={Eleak:.1e}")
88-
# ax.legend()
89+
label=f"Fitted leak g={gleak:7.5g}, E={Eleak:7.4g} mV")
90+
ax.plot(range_of_zero, [0, 0], color='black', linestyle=style_of_zero, alpha=alpha_of_zero)
91+
ax.legend()
92+
ax.tick_params(axis='x', labelbottom=False)
8993

90-
if ax.get_legend():
91-
ax.get_legend().remove()
92-
ax.set_xlabel('time (s)')
93-
ax.set_ylabel(r'pre-drug trace')
94-
# ax.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e'))
95-
# ax.tick_params(axis='y', rotation=90)
94+
before_axs[0].set_ylabel(r'Pre-drug trace', fontsize=16)
9695

9796
for i, (sweep, ax) in enumerate(zip(sweeps, after_axs)):
98-
gleak, Eleak = all_leak_params_before[i]
99-
ax.plot(times*1e-3, after_currents[i, :], label=f"post-drug raw, sweep {sweep}")
97+
b0, b1 = all_leak_params_after[i]
98+
gleak = b1
99+
Eleak = -b0/b1
100+
101+
ax.plot(times*1e-3, after_currents[i, :], label="Post-drug raw")
100102
ax.plot(times*1e-3, after_leak_currents[i, :],
101-
label=r"$I_\mathrm{L}$." f"g={gleak:1E}, E={Eleak:.1e}")
102-
# ax.legend()
103-
if ax.get_legend():
104-
ax.get_legend().remove()
105-
ax.set_xlabel('$t$ (s)')
106-
ax.set_ylabel(r'post-drug trace')
107-
# ax.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e'))
108-
# ax.tick_params(axis='y', rotation=90)
103+
label=f"Fitted leak g={gleak:7.5g}, E={Eleak:7.4g} mV")
104+
ax.plot(range_of_zero, [0, 0], color='black', linestyle=style_of_zero, alpha=alpha_of_zero)
105+
ax.legend()
106+
ax.tick_params(axis='x', labelbottom=False)
107+
after_axs[0].set_ylabel(r'Post-drug trace', fontsize=16)
109108

110109
for i, (sweep, ax) in enumerate(zip(sweeps, corrected_axs)):
111110
corrected_before_currents = before_currents[i, :] - before_leak_currents[i, :]
112111
corrected_after_currents = after_currents[i, :] - after_leak_currents[i, :]
113112
ax.plot(times*1e-3, corrected_before_currents,
114-
label=f"leak-corrected pre-drug trace, sweep {sweep}")
113+
label="Leak-corrected pre-drug trace")
115114
ax.plot(times*1e-3, corrected_after_currents,
116-
label=f"leak-corrected post-drug trace, sweep {sweep}")
117-
ax.set_xlabel(r'$t$ (s)')
118-
ax.set_ylabel(r'leak-corrected traces')
119-
# ax.tick_params(axis='y', rotation=90)
120-
# ax.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.1e'))
115+
label="Leak-corrected post-drug trace")
116+
ax.plot(range_of_zero, [0, 0], color='black', linestyle=style_of_zero, alpha=alpha_of_zero)
117+
ax.set_xlabel(r'Time (s)')
118+
ax.legend()
119+
corrected_axs[0].set_ylabel(r'Leak-corrected traces', fontsize=16)
121120

122-
ax = subtracted_ax
123121
for i, sweep in enumerate(sweeps):
124-
before_trace = before_currents[i, :].flatten()
125-
after_trace = after_currents[i, :].flatten()
126-
before_params, before_leak = fit_linear_leak(before_trace, voltages, times,
127-
*ramp_bounds)
128-
after_params, after_leak = fit_linear_leak(after_trace, voltages, times,
129-
*ramp_bounds)
130-
131122
subtracted_currents = before_currents[i, :] - before_leak_currents[i, :] - \
132123
(after_currents[i, :] - after_leak_currents[i, :])
133-
ax.plot(times*1e-3, subtracted_currents, label=f"sweep {sweep}", alpha=.5)
134124

135-
#  Cycle to next colour
136-
ax.plot([np.nan], [np.nan], label=f"sweep {sweep}", alpha=.5)
125+
subtracted_ax.plot(times*1e-3, subtracted_currents, label=f"sweep {sweep}", alpha=.5)
137126

138-
ax.set_ylabel(r'$I_\mathrm{obs} - I_\mathrm{L}$ (mV)')
139-
ax.set_xlabel('$t$ (s)')
127+
subtracted_ax.legend()
128+
subtracted_ax.plot(range_of_zero, [0, 0], color='black',
129+
linestyle=style_of_zero, alpha=alpha_of_zero)
130+
subtracted_ax.set_ylabel('Final subtracted traces', fontsize=16)
131+
subtracted_ax.set_xlabel('Time (s)', fontsize=16)
140132

141133
long_protocol_ax.plot(times*1e-3, voltages, color='black')
142-
long_protocol_ax.set_xlabel('time (s)')
143-
long_protocol_ax.set_ylabel(r'$V_\mathrm{cmd}$ (mV)')
144-
long_protocol_ax.tick_params(axis='y', rotation=90)
134+
long_protocol_ax.set_ylabel(r'$V_\mathrm{cmd}$ (mV)', fontsize=16)
135+
long_protocol_ax.tick_params(axis='x', labelbottom=False)
136+
137+
fig.align_ylabels()
145138

setup.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@
1313
# Module name (lowercase)
1414
name='pcpostprocess',
1515
version='0.0.1',
16-
description='Export high-throughput patch-clamp data from the Nanion SyncroPatch',
16+
description='Post-process patch clamp recordings with the staircase protocol',
1717
long_description=readme,
1818
long_description_content_type="text/markdown",
19-
author='Frankie Patten-Elliot, Joseph Shuttleworth, Chon Lok Lei',
19+
author='Frankie Patten-Elliot, Joseph Shuttleworth, Chon Lok Lei, Michael Clerx',
2020
author_email='joseph.shuttleworth@nottingham.ac.uk',
2121
maintainer='Joseph Shuttleworth',
2222
maintainer_email='joseph.shuttleworth@nottingham.ac.uk',
23-
# url='https://github.com/CardiacModelling/markov-builder',
23+
url='https://github.com/CardiacModelling/pcpostprocess',
2424
classifiers=[
25-
"Programming Language :: Python :: 3",
26-
"License :: OSI Approved :: BSD License",
27-
"Operating System :: OS Independent",
25+
'Programming Language :: Python :: 3',
26+
'License :: OSI Approved :: BSD License',
27+
'Operating System :: OS Independent',
2828
],
2929

3030
# Packages to include
@@ -35,7 +35,7 @@
3535
include_package_data=True,
3636

3737
# Required Python version
38-
python_requires='>=3.7',
38+
python_requires='>=3.10',
3939

4040
# List of dependencies
4141
install_requires=[
@@ -44,25 +44,23 @@
4444
'matplotlib>=3.4',
4545
'pandas>=1.3',
4646
'regex>=2023.12.25',
47-
'openpyxl>=3.1.2',
48-
'jinja2>=3.1.0',
49-
'seaborn>=0.12.2'
47+
'seaborn>=0.12.2',
48+
'openpyxl>=3.1.2', # Used via pandas (to create excel doc)
49+
'jinja2>=3.1.0', # Used via pandas (to create latex doc)
5050
],
5151
extras_require={
5252
'test': [
5353
'pytest-cov>=2.10', # For coverage checking
5454
'pytest>=4.6', # For unit tests
5555
'flake8>=3', # For code style checking
5656
'isort',
57-
'mock>=3.0.5', # For mocking command line args etc.
5857
'codecov>=2.1.3',
5958
'syncropatch_export @ git+https://github.com/CardiacModelling/syncropatch_export.git'
6059
],
6160
},
6261
entry_points={
6362
'console_scripts': [
64-
'pcpostprocess='
65-
'pcpostprocess.scripts.__main__:main',
63+
'pcpostprocess=pcpostprocess.scripts.__main__:main',
6664
],
6765
},
6866
)

tests/test_leak_correct.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from syncropatch_export.trace import Trace
66

77
from pcpostprocess import leak_correct
8+
from pcpostprocess.detect_ramp_bounds import detect_ramp_bounds
89

910

1011
class TestLeakCorrect(unittest.TestCase):
@@ -18,15 +19,20 @@ def setUp(self):
1819
if not os.path.exists(self.output_dir):
1920
os.makedirs(self.output_dir)
2021

21-
self.ramp_bounds = [1700, 2500]
2222
self.test_trace = Trace(test_data_dir, json_file)
2323

2424
# get currents and QC from trace object
2525
self.currents = self.test_trace.get_all_traces(leakcorrect=False)
2626
self.currents['times'] = self.test_trace.get_times()
2727
self.currents['voltages'] = self.test_trace.get_voltage()
28+
2829
self.QC = self.test_trace.get_onboard_QC_values()
2930

31+
# Find first times ahead of these times
32+
voltage_protocol = self.test_trace.get_voltage_protocol().get_all_sections()
33+
times = self.currents['times'].flatten()
34+
self.ramp_bound_indices = detect_ramp_bounds(times, voltage_protocol, ramp_no=0)
35+
3036
def test_plot_leak_fit(self):
3137
well = 'A01'
3238
sweep = 0
@@ -37,7 +43,7 @@ def test_plot_leak_fit(self):
3743
current = self.test_trace.get_trace_sweeps(sweeps=[sweep])[well][0, :]
3844

3945
leak_correct.fit_linear_leak(current, voltage, times,
40-
*self.ramp_bounds,
46+
*self.ramp_bound_indices,
4147
output_dir=self.output_dir,
4248
save_fname=f"{well}_sweep{sweep}_leak_correction")
4349

@@ -50,7 +56,8 @@ def test_get_leak_correct(self):
5056
times = trace.get_times()
5157

5258
current = currents[well][sweep, :]
53-
x = leak_correct.get_leak_corrected(current, voltage, times, *self.ramp_bounds)
59+
x = leak_correct.get_leak_corrected(current, voltage, times,
60+
*self.ramp_bound_indices)
5461
self.assertEqual(x.shape, (30784,))
5562

5663

tests/test_subtraction_plots.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,26 @@
1111

1212
class TestSubtractionPlots(unittest.TestCase):
1313
def setUp(self):
14-
test_data_dir = os.path.join('tests', 'test_data', '13112023_MW2_FF',
15-
"staircaseramp (2)_2kHz_15.01.07")
16-
json_file = "staircaseramp (2)_2kHz_15.01.07.json"
14+
test_data_dir_before = os.path.join('tests', 'test_data', '13112023_MW2_FF',
15+
"staircaseramp (2)_2kHz_15.01.07")
16+
17+
test_data_dir_after = os.path.join('tests', 'test_data', '13112023_MW2_FF',
18+
"staircaseramp (2)_2kHz_15.11.33")
19+
20+
json_file_before = "staircaseramp (2)_2kHz_15.01.07.json"
21+
json_file_after = "staircaseramp (2)_2kHz_15.11.33.json"
1722

1823
self.output_dir = os.path.join('test_output', 'test_trace_class')
1924

2025
if not os.path.exists(self.output_dir):
2126
os.makedirs(self.output_dir)
2227

23-
self.ramp_bounds = [1700, 2500]
24-
2528
# Use identical traces for purpose of the test
26-
self.before_trace = Trace(test_data_dir, json_file)
27-
self.after_trace = Trace(test_data_dir, json_file)
29+
self.before_trace = Trace(test_data_dir_before, json_file_before)
30+
self.after_trace = Trace(test_data_dir_after, json_file_after)
2831

2932
def test_do_subtraction_plot(self):
30-
fig = plt.figure(layout='constrained')
33+
fig = plt.figure(figsize=(16, 18), layout='constrained')
3134
times = self.before_trace.get_times()
3235

3336
well = 'A01'
@@ -44,6 +47,8 @@ def test_do_subtraction_plot(self):
4447
do_subtraction_plot(fig, times, sweeps, before_current, after_current,
4548
voltages, ramp_bounds, well=well)
4649

50+
fig.savefig(os.path.join(self.output_dir, f"subtraction_plot_{well}"))
51+
4752

4853
if __name__ == "__main__":
4954
pass

0 commit comments

Comments
 (0)