Skip to content

Commit d26d12e

Browse files
committed
#21 add itemised trace to hERG QC
1 parent 3815c47 commit d26d12e

File tree

2 files changed

+117
-54
lines changed

2 files changed

+117
-54
lines changed

pcpostprocess/hergQC.py

+111-53
Original file line numberDiff line numberDiff line change
@@ -123,38 +123,57 @@ def run_qc(self, voltage_steps, times,
123123
before = self.filter_capacitive_spikes(before, times, voltage_steps)
124124
after = self.filter_capacitive_spikes(after, times, voltage_steps)
125125

126+
QC = {label: [(False, None)] for label in self.qc_labels}
127+
126128
if len(before) == 0 or len(after) == 0:
127-
return False, [False for lab in self.qc_labels]
129+
return QC
128130

129131
if (None in qc_vals_before) or (None in qc_vals_after):
130-
return False, False * self.no_QC
132+
return QC
131133

132134
qc1_1 = self.qc1(*qc_vals_before)
133135
qc1_2 = self.qc1(*qc_vals_after)
134-
qc1 = [i and j for i, j in zip(qc1_1, qc1_2)]
135136

136-
qc2_1 = True
137-
qc2_2 = True
137+
QC['qc1.rseal'] = [qc1_1[0], qc1_2[0]]
138+
QC['qc1.cm'] = [qc1_1[1], qc1_2[1]]
139+
QC['qc1.rseries'] = [qc1_1[2], qc1_2[2]]
140+
141+
QC['qc2.raw'] = []
142+
QC['qc2.subtracted'] = []
138143
for i in range(n_sweeps):
139-
qc2_1 = qc2_1 and self.qc2(before[i])
140-
qc2_2 = qc2_2 and self.qc2(before[i] - after[i])
144+
qc2_1 = self.qc2(before[i])
145+
qc2_2 = self.qc2(before[i] - after[i])
146+
147+
QC['qc2.raw'].append(qc2_1)
148+
QC['qc2.subtracted'].append(qc2_2)
141149

142150
qc3_1 = self.qc3(before[0, :], before[1, :])
143151
qc3_2 = self.qc3(after[0, :], after[1, :])
144152
qc3_3 = self.qc3(before[0, :] - after[0, :],
145153
before[1, :] - after[1, :])
146154

155+
QC['qc3.raw'] = [qc3_1]
156+
QC['qc3.E4031'] = [qc3_2]
157+
QC['qc3.subtracted'] = [qc3_3]
158+
147159
rseals = [qc_vals_before[0], qc_vals_after[0]]
148160
cms = [qc_vals_before[1], qc_vals_after[1]]
149161
rseriess = [qc_vals_before[2], qc_vals_after[2]]
150162
qc4 = self.qc4(rseals, cms, rseriess)
151163

164+
QC['qc4.rseal'] = [qc4[0]]
165+
QC['qc4.cm'] = [qc4[1]]
166+
QC['qc4.rseries'] = [qc4[2]]
167+
152168
# indices where hERG peaks
153169
qc5 = self.qc5(before[0, :], after[0, :],
154170
self.qc5_win)
155171

156172
qc5_1 = self.qc5_1(before[0, :], after[0, :], label='1')
157173

174+
QC['qc5.staircase'] = [qc5]
175+
QC['qc5.1.staircase'] = [qc5_1]
176+
158177
# Ensure thats the windows are correct by checking the voltage trace
159178
assert np.all(
160179
np.abs(self.voltage[self.qc6_win[0]: self.qc6_win[1]] - 40.0))\
@@ -166,14 +185,17 @@ def run_qc(self, voltage_steps, times,
166185
np.abs(self.voltage[self.qc6_2_win[0]: self.qc6_2_win[1]] - 40.0))\
167186
< 1e-8
168187

169-
qc6, qc6_1, qc6_2 = True, True, True
188+
QC['qc6.subtracted'] = []
189+
QC['qc6.1.subtracted'] = []
190+
QC['qc6.2.subtracted'] = []
170191
for i in range(before.shape[0]):
171-
qc6 = qc6 and self.qc6((before[i, :] - after[i, :]),
172-
self.qc6_win, label='0')
173-
qc6_1 = qc6_1 and self.qc6((before[i, :] - after[i, :]),
174-
self.qc6_1_win, label='1')
175-
qc6_2 = qc6_2 and self.qc6((before[i, :] - after[i, :]),
176-
self.qc6_2_win, label='2')
192+
qc6 = self.qc6((before[i, :] - after[i, :]), self.qc6_win, label="0")
193+
qc6_1 = self.qc6((before[i, :] - after[i, :]), self.qc6_1_win, label="1")
194+
qc6_2 = self.qc6((before[i, :] - after[i, :]), self.qc6_2_win, label="2")
195+
196+
QC['qc6.subtracted'].append(qc6)
197+
QC['qc6.1.subtracted'].append(qc6_1)
198+
QC['qc6.2.subtracted'].append(qc6_2)
177199

178200
if self._debug:
179201
fig = plt.figure(figsize=(8, 5))
@@ -199,37 +221,47 @@ def run_qc(self, voltage_steps, times,
199221
fig.savefig(os.path.join(self.plot_dir, 'qc_debug.png'))
200222
plt.close(fig)
201223

202-
# Make a flat list of all QC criteria (pass/fail bool)
203-
QC = np.hstack([qc1, [qc2_1, qc2_2, qc3_1, qc3_2, qc3_3],
204-
qc4, [qc5, qc5_1, qc6, qc6_1, qc6_2]]).flatten()
224+
# Check if all QC criteria passed
225+
passed = all([x for qc in QC.values() for x, _ in qc])
205226

206-
passed = np.all(QC)
207227
return passed, QC
208228

209229
def qc1(self, rseal, cm, rseries):
210-
211-
if any([x is None for x in (rseal, cm, rseries)]):
212-
return False, False, False
213-
214230
# Check R_seal, C_m, R_series within desired range
215-
if rseal < self.rsealc[0] or rseal > self.rsealc[1] \
216-
or not np.isfinite(rseal):
231+
if (
232+
rseal is None
233+
or rseal < self.rsealc[0]
234+
or rseal > self.rsealc[1]
235+
or not np.isfinite(rseal)
236+
):
217237
self.logger.debug(f"rseal: {rseal}")
218238
qc11 = False
219239
else:
220240
qc11 = True
221-
if cm < self.cmc[0] or cm > self.cmc[1] or not np.isfinite(cm):
241+
242+
if (
243+
cm is None
244+
or cm < self.cmc[0]
245+
or cm > self.cmc[1]
246+
or not np.isfinite(cm)
247+
):
222248
self.logger.debug(f"cm: {cm}")
223249
qc12 = False
224250
else:
225251
qc12 = True
226-
if rseries < self.rseriesc[0] or rseries > self.rseriesc[1] \
227-
or not np.isfinite(rseries):
252+
253+
if (
254+
rseries is None
255+
or rseries < self.rseriesc[0]
256+
or rseries > self.rseriesc[1]
257+
or not np.isfinite(rseries)
258+
):
228259
self.logger.debug(f"rseries: {rseries}")
229260
qc13 = False
230261
else:
231262
qc13 = True
232-
return [qc11, qc12, qc13]
263+
264+
return [(qc11, rseal), (qc12, cm), (qc13, rseries)]
233265

234266
def qc2(self, recording, method=3):
235267
# Check SNR is good
@@ -245,9 +277,11 @@ def qc2(self, recording, method=3):
245277

246278
if snr < self.snrc or not np.isfinite(snr):
247279
self.logger.debug(f"snr: {snr}")
248-
return False
280+
result = False
281+
else:
282+
result = True
249283

250-
return True
284+
return (result, snr)
251285

252286
def qc3(self, recording1, recording2, method=3):
253287
# Check 2 sweeps similar
@@ -270,36 +304,51 @@ def qc3(self, recording1, recording2, method=3):
270304
rmsd = np.sqrt(np.mean((recording1 - recording2) ** 2))
271305
if rmsd > rmsdc or not (np.isfinite(rmsd) and np.isfinite(rmsdc)):
272306
self.logger.debug(f"rmsd: {rmsd}, rmsdc: {rmsdc}")
273-
return False
274-
return True
275-
276-
def qc4(self, rseals, cms, rseriess):
307+
result = False
308+
else:
309+
result = True
277310

278-
if any([x is None for x in list(rseals) + list(cms) + list(rseriess)]):
279-
return [False, False, False]
311+
return (result, rmsd)
280312

313+
def qc4(self, rseals, cms, rseriess):
281314
# Check R_seal, C_m, R_series stability
282315
# Require std/mean < x%
283316
qc41 = True
284317
qc42 = True
285318
qc43 = True
286-
if np.std(rseals) / np.mean(rseals) > self.rsealsc or not (
287-
np.isfinite(np.mean(rseals)) and np.isfinite(np.std(rseals))):
288-
self.logger.debug(f"d_rseal: {np.std(rseals) / np.mean(rseals)}")
319+
320+
if None in list(rseals):
289321
qc41 = False
322+
d_rseal = None
323+
else:
324+
d_rseal = np.std(rseals) / np.mean(rseals)
325+
if d_rseal > self.rsealsc or not (
326+
np.isfinite(np.mean(rseals)) and np.isfinite(np.std(rseals))):
327+
self.logger.debug(f"d_rseal: {d_rseal}")
328+
qc41 = False
290329

291-
if np.std(cms) / np.mean(cms) > self.cmsc or not (
292-
np.isfinite(np.mean(cms)) and np.isfinite(np.std(cms))):
293-
self.logger.debug(f"d_cm: {np.std(cms) / np.mean(cms)}")
330+
if None in list(cms):
294331
qc42 = False
332+
d_cm = None
333+
else:
334+
d_cm = np.std(cms) / np.mean(cms)
335+
if d_cm > self.cmsc or not (
336+
np.isfinite(np.mean(cms)) and np.isfinite(np.std(cms))):
337+
self.logger.debug(f"d_cm: {d_cm}")
338+
qc42 = False
295339

296-
if np.std(rseriess) / np.mean(rseriess) > self.rseriessc or not (
297-
np.isfinite(np.mean(rseriess))
298-
and np.isfinite(np.std(rseriess))):
299-
self.logger.debug(f"d_rseries: {np.std(rseriess) / np.mean(rseriess)}")
340+
if None in list(rseriess):
300341
qc43 = False
342+
d_rseries = None
343+
else:
344+
d_rseries = np.std(rseriess) / np.mean(rseriess)
345+
if d_rseries > self.rseriessc or not (
346+
np.isfinite(np.mean(rseriess))
347+
and np.isfinite(np.std(rseriess))):
348+
self.logger.debug(f"d_rseries: {d_rseries}")
349+
qc43 = False
301350

302-
return [qc41, qc42, qc43]
351+
return [(qc41, d_rseal), (qc42, d_cm), (qc43, d_rseries)]
303352

304353
def qc5(self, recording1, recording2, win=None, label=''):
305354
# Check pharma peak value drops after E-4031 application
@@ -325,8 +374,11 @@ def qc5(self, recording1, recording2, win=None, label=''):
325374
if (max_diff < max_diffc) or not (np.isfinite(max_diff)
326375
and np.isfinite(max_diffc)):
327376
self.logger.debug(f"max_diff: {max_diff}, max_diffc: {max_diffc}")
328-
return False
329-
return True
377+
result = False
378+
else:
379+
result = True
380+
381+
return (result, max_diff)
330382

331383
def qc5_1(self, recording1, recording2, win=None, label=''):
332384
# Check RMSD_0 drops after E-4031 application
@@ -355,8 +407,11 @@ def qc5_1(self, recording1, recording2, win=None, label=''):
355407
if (rmsd0_diff < rmsd0_diffc) or not (np.isfinite(rmsd0_diff)
356408
and np.isfinite(rmsd0_diffc)):
357409
self.logger.debug(f"rmsd0_diff: {rmsd0_diff}, rmsd0c: {rmsd0_diffc}")
358-
return False
359-
return True
410+
result = False
411+
else:
412+
result = True
413+
414+
return (result, rmsd0_diff)
360415

361416
def qc6(self, recording1, win=None, label=''):
362417
# Check subtracted staircase +40mV step up is non negative
@@ -377,8 +432,11 @@ def qc6(self, recording1, win=None, label=''):
377432
if (val < valc) or not (np.isfinite(val)
378433
and np.isfinite(valc)):
379434
self.logger.debug(f"qc6_{label} val: {val}, valc: {valc}")
380-
return False
381-
return True
435+
result = False
436+
else:
437+
result = True
438+
439+
return (result, val)
382440

383441
def filter_capacitive_spikes(self, current, times, voltage_step_times):
384442
"""

tests/test_herg_qc.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ def test_run_qc(self):
8989
qc_vals_after_well, n_sweeps=2)
9090

9191
logging.debug(well, passed)
92-
self.assertTrue(passed)
92+
93+
trace = ""
94+
for label, results in qcs.items():
95+
if any([x == False for x, _ in results]):
96+
trace += f"{label}: {results}\n"
97+
self.assertTrue(passed, trace)

0 commit comments

Comments
 (0)