def _t_gconst(pattern, defl): """ T Wave abstraction pattern general constraints, checked when all the evidence has been observed. """ twave = pattern.hypothesis if defl.earlystart != defl.latestart or not pattern.evidence[o.QRS]: return qrs = pattern.evidence[o.QRS][0] # Wave limits beg = int(twave.earlystart) end = int(twave.lateend) ls_lim = int(twave.latestart - beg) ee_lim = int(twave.earlyend - beg) # Start and end estimation. endpoints = {} for lead in sorted(qrs.shape, key=lambda l: qrs.shape[l].amplitude, reverse=True): baseline, _ = characterize_baseline(lead, beg, end) sig = sig_buf.get_signal_fragment(beg, end, lead=lead)[0] verify(len(sig) == end - beg + 1) ep = _delimit_t(sig, baseline, ls_lim, ee_lim, qrs.shape[lead]) if ep is not None: endpoints[lead] = ep verify(endpoints) limits = max(endpoints.iteritems(), key=lambda ep: ep[1][1])[1][0] # We verify that in all leads the maximum slope of the T wave fragment does # not exceed the threshold. for lead in endpoints: sig = sig_buf.get_signal_fragment(beg + limits.start, beg + limits.end, lead=lead)[0] verify(np.max(np.abs(np.diff(sig))) <= qrs.shape[lead].maxslope * C.TQRS_MAX_DIFFR) # Amplitude measure if lead in endpoints: mx, mn = np.amax(sig), np.amin(sig) pol = 1.0 if max(mx - sig[0], mx - sig[-1]) >= -min(mn - sig[0], mn - sig[1]) else -1.0 twave.amplitude[lead] = pol * np.ptp(sig) twave.start.value = Iv(beg + limits.start, beg + limits.start) twave.end.value = Iv(beg + limits.end, beg + limits.end) # The duration of the T Wave must be greater than the QRS # (with a security margin) verify(twave.earlyend - twave.latestart > qrs.earlyend - qrs.latestart - C.TMARGIN) # The overlapping between the energy interval and the T Wave must be at # least the half of the duration of the energy interval. verify( Iv(twave.earlystart, twave.lateend).intersection(Iv(defl.earlystart, defl.lateend)).length >= (defl.lateend - defl.earlystart) / 2.0 ) # If the Deflection is a R-Deflection, we require a margin before # the end of the twave. if isinstance(defl, o.RDeflection): verify(twave.lateend - defl.time.end > C.TW_RDEF_MIN_DIST)
def _characterize_signal(beg, end): """ Characterizes the available signal in a specific time interval. Parameters ---------- beg: Starting time point of the interval. end: Last time point of the interval. Returns ------- out: sortedlist with one entry by lead. Each entry is a 5-size tuple with the lead, the signal samples, the relevant points to represent the samples, the baseline level estimation for the fragment, and the quality of the fragment in that lead. """ siginfo = sortedcontainers.SortedList(key=lambda v: -v[4]) for lead in sig_buf.get_available_leads(): baseline, quality = characterize_baseline(lead, beg, end) sig = sig_buf.get_signal_fragment(beg, end, lead=lead)[0] if len(sig) == 0: return None # We build a signal simplification taking at most 9 points, and with # a minimum relevant deviation of 50 uV. points = DP.arrayRDP(sig, ph2dg(0.05), 9) siginfo.add((lead, sig, points, baseline, quality)) return siginfo
def characterize_baseline(lead, beg, end): """ Obtains the baseline estimation for a fragment delimited by two time points in a specific lead. It also obtains a quality estimator for the fragment. Parameters ---------- lead: Selected lead to obtain the baseline estimator. beg: Starting sample of the interval. end: Ending sample of the interval. Returns ------ out: (baseline, quality) Tuple with (baseline, quality) estimators. At the moment, the quality estimator is not yet numerically characterized, but we have strong evidence that the higher this value is, the higher the signal quality of the fragment where the baseline has been estimated. """ assert beg >= 0 and end >= beg # We need at least 1 second of signal to estimate the baseline and the # quality. MIN_LENGTH = ms2sp(1000) if end - beg < MIN_LENGTH: center = beg + (end - beg) / 2.0 beg = max(0, int(center - MIN_LENGTH / 2)) end = int(center + MIN_LENGTH / 2) signal = sig_buf.get_signal_fragment(beg, end, lead=lead)[0] return (sig_meas.mode(signal), sig_meas.kurtosis(signal))
def _vflut_gconst(pattern, _): """ General constraints of the pattern, checked every time a new observation is added to the evidence. These constraints simply state that the majority of the leads must show a positive detection of a ventricular flutter. """ if not pattern.evidence[o.Cardiac_Rhythm]: return hyp = pattern.hypothesis ################## beg = int(hyp.earlystart) if beg < 0: beg = 0 end = int(hyp.earlyend) verify(not _contains_qrs(pattern), 'QRS detected during flutter') lpos = 0.0 ltot = 0.0 for lead in sig_buf.get_available_leads(): if _is_VF(sig_buf.get_signal_fragment(beg, end, lead=lead)[0]): lpos += 1 ltot += 1 verify(lpos / ltot > 0.5) defls = pattern.evidence[o.Deflection] if len(defls) > 1: rrs = np.diff([defl.earlystart for defl in defls]) hyp.meas = o.CycleMeasurements((np.mean(rrs), np.std(rrs)), (0, 0), (0, 0))
def _contains_qrs(pattern): """ Checks if inside the flutter fragment there is a waveform "identical" to the first environment QRS complex. """ qrs = pattern.evidence[o.QRS][0] # We limit the duration of the QRS to check this condition. if qrs.lateend - qrs.earlystart not in C.NQRS_DUR: return False defls = pattern.evidence[o.Deflection] if len(defls) > 1: limit = defls[-3].lateend if len(defls) > 2 else qrs.lateend sig = {} # We take the signal fragment with maximum correlation with the QRS # signal in each lead, and we check if the two fragments can be # clustered as equal QRS complexes. qshape = {} corr = -np.Inf delay = 0 leads = sig_buf.get_available_leads() for lead in leads: qshape[lead] = o.QRSShape() sigfr = sig_buf.get_signal_fragment(qrs.earlystart, qrs.lateend + 1, lead=lead)[0] qshape[lead].sig = sigfr - sigfr[0] qshape[lead].amplitude = np.ptp(qshape[lead].sig) sig[lead] = sig_buf.get_signal_fragment(limit, defls[-1].earlystart, lead=lead)[0] if len(sig[lead]) > 0 and len(qshape[lead].sig) > 0: lcorr, ldelay = xcorr_valid(sig[lead], qshape[lead].sig) if lcorr > corr: corr, delay = lcorr, ldelay if 0 <= delay < len(sig[lead]): sshape = {} for lead in leads: sshape[lead] = o.QRSShape() sshape[lead].sig = sig[lead][ delay:delay + len(qshape[lead].sig)] - sig[lead][delay] sshape[lead].amplitude = np.ptp(sshape[lead].sig) return not signal_unmatch(sshape, qshape) return False
def _verify_atrial_activity(pattern): """ Checks if the atrial activity is consistent with the definition of atrial fibrillation (that is, absence of constant P Waves or flutter-like baseline activity.) """ beats = pattern.evidence[o.QRS][-5:] obseq = pattern.obs_seq atr_sig = {lead: [] for lead in sig_buf.get_available_leads()} pw_lims = [] idx = pattern.get_step(beats[0]) # First we get all the signal fragments between ventricular observations, # which are the only recognized by this pattern. In these fragments is where # atrial activity may be recognized. for i in range(idx + 1, len(obseq)): if isinstance(obseq[i], o.QRS): beg = next(obs for obs in reversed(obseq[:i]) if obs is not None).lateend end = obseq[i].earlystart if end - beg > ms2sp(200): beg = end - ms2sp(200) pw_lims.append((beg, end)) for i in range(len(beats) - 1): beg, end = beats[i].lateend, beats[i + 1].earlystart for lead in atr_sig: atr_sig[lead].append( sig_buf.get_signal_fragment(beg, end, lead=lead)[0] - characterize_baseline(lead, beg, end)[0]) # Flutter check (only for atrial activity) aflut = set() for lead in atr_sig: sigfr = np.concatenate(atr_sig[lead]) if len(sigfr) > 15 and _is_VF(sigfr): aflut.add(lead) # FIXME improve flutter check, now is quite poor. # aflut = frozenset() # P waveform check (only for leads where flutters were not found.) pwaves = [] for beg, end in pw_lims: pwsig = _get_pwave_sig(beg, end) if pwsig is not None: for lead in aflut: pwsig.pop(lead, None) if not pwsig: continue for wave in pwaves: verify( abs(wave.values()[0].pr - pwsig.values()[0].pr) > C.TMARGIN or not signal_match(wave, pwsig)) pwaves.append(pwsig)
def _get_pwave_sig(beg, end): """ Checks if before a QRS complex there is a waveform similar to a P Wave. In an atrial fibrillation context, there cannot be any recognizable atrial activity. Parameters: ---------- beg: Earliest point for the starting of the P Wave. This limit may be further constrained if the distance between the two parameters is excessive. end: Latest point for the ending of the P Wave. **It is assumed to be the starting point of the QRS complex associated to the P Wave**. Returns ------- out: Dictionary with a tuple for each lead in which a P-Wave can be recognized. The tuple contains the distance in samples from *end* to the beginning of the P-Wave, and the signal fragment containing the P-Wave. """ # If the result is cached, we use it result = PCACHE.get((beg, end), None) if result is not None: return result.copy() est = end - ms2sp(250) if end - beg > ms2sp(250) else beg lst = end - ms2sp(80) eend = est + ms2sp(40) ltnd = end - ms2sp(20) if est > lst or eend > end or eend > ltnd: # Inconsistency return None pwave = o.PWave() limits = delineate_pwave(est, lst, eend, ltnd, pwave) if limits is None: return None result = {} for lead in pwave.amplitude: sig = sig_buf.get_signal_fragment(est + limits.start, est + limits.end + 1, lead=lead)[0] result[lead] = PW_SIG(end - (est + limits.start), sig) # Result is cached PCACHE[(beg, end)] = result return result.copy()
def _check_missed_beats(pattern): """ Checks if a rhythm pattern has missed a QRS complex in the identification, by looking for a waveform "identical" to the last observed in the interval between the last two observations. """ qrs = pattern.evidence[o.QRS][-1] obseq = pattern.obs_seq idx = obseq.index(qrs) if idx > 0: prevobs = next( (obs for obs in reversed(obseq[:idx]) if obs is not None), None) if prevobs is not None: if isinstance(prevobs, o.QRS): limit = max(prevobs.lateend, prevobs.earlystart + qrs.lateend - qrs.earlystart) else: limit = prevobs.lateend else: limit = pattern.hypothesis.earlystart ulimit = qrs.earlystart - C.TACHY_RR.start if limit >= ulimit: return sig = {} # We take the signal fragment with maximum correlation with the QRS # signal in each lead, and we check if the two fragments can be # clustered as equal QRS complexes. qshape = {} corr = -np.Inf delay = 0 leads = sig_buf.get_available_leads() for lead in leads: qshape[lead] = o.QRSShape() sigfr = sig_buf.get_signal_fragment(qrs.earlystart, qrs.lateend + 1, lead=lead)[0] qshape[lead].sig = sigfr - sigfr[0] qshape[lead].amplitude = np.ptp(qshape[lead].sig) sig[lead] = sig_buf.get_signal_fragment(limit, ulimit, lead=lead)[0] lcorr, ldelay = xcorr_valid(sig[lead], qshape[lead].sig) if lcorr > corr: corr, delay = lcorr, ldelay if 0 <= delay < len(sig[lead]): sshape = {} for lead in leads: sshape[lead] = o.QRSShape() sshape[lead].sig = sig[lead][ delay:delay + len(qshape[lead].sig)] - sig[lead][delay] sshape[lead].amplitude = np.ptp(sshape[lead].sig) if isinstance(pattern.hypothesis, o.RegularCardiacRhythm): qref = pattern.evidence[o.QRS][-2] rr = float(qrs.earlystart - qref.earlystart) loc = (limit + delay - qref.earlystart) / rr # Check for one and two missed beats in regular positions if 0.45 <= loc <= 0.55: verify(signal_unmatch(sshape, qshape), 'Missed beat') elif 0.28 <= loc <= 0.38 and not signal_unmatch( sshape, qshape): corr = -np.Inf delay = 0 for lead in leads: sig[lead] = sig_buf.get_signal_fragment( int(qref.earlystart + 0.61 * rr), min( int(qref.earlystart + 0.71 * rr) + len(qshape[lead].sig), int(qrs.earlystart)), lead=lead, )[0] lcorr, ldelay = xcorr_valid(sig[lead], qshape[lead].sig) if lcorr > corr: corr, delay = lcorr, ldelay sshape = {} for lead in leads: sshape[lead] = o.QRSShape() sshape[lead].sig = ( sig[lead][delay:delay + len(qshape[lead].sig)] - sig[lead][delay]) sshape[lead].amplitude = np.ptp(sshape[lead].sig) verify(signal_unmatch(sshape, qshape), 'Two missed beats') elif 0.61 <= loc <= 0.71 and not signal_unmatch( sshape, qshape): corr = -np.Inf delay = 0 for lead in leads: sig[lead] = sig_buf.get_signal_fragment( int(qref.earlystart + 0.28 * rr), min( int(qref.earlystart + 0.38 * rr) + len(qshape[lead].sig), int(qrs.earlystart)), lead=lead, )[0] lcorr, ldelay = xcorr_valid(sig[lead], qshape[lead].sig) if lcorr > corr: corr, delay = lcorr, ldelay sshape = {} for lead in leads: sshape[lead] = o.QRSShape() sshape[lead].sig = ( sig[lead][delay:delay + len(qshape[lead].sig)] - sig[lead][delay]) sshape[lead].amplitude = np.ptp(sshape[lead].sig) verify(signal_unmatch(sshape, qshape), 'Two missed beats') else: verify(signal_unmatch(sshape, qshape), 'Missed beat')
def _guided_qrs_observation(hyp): """ Performs the delineation and checking of the general constraints of the QRS abstraction pattern when a reference shape for seaching is set as the hypothesis shape. The modification is done in-place. modifying the hypothesis shape. Parameters ---------- hyp: QRS observation that is the hypothesis of the pattern. """ if hyp.shape: # We perform the alignment in the lead with highest energy. rlead, rshape = max(hyp.shape.iteritems(), key=lambda s: s[1].energy) ref = rshape.sig newshape = {} start = np.inf beg, end = (int(hyp.earlystart), min(int(hyp.latestart) + len(ref), int(hyp.lateend))) if beg < 0: beg = 0 try: sig = sig_buf.get_signal_fragment(beg, end, lead=rlead)[0] verify(len(sig) == end - beg + 1) sig = sig - sig[0] _, idx = xcorr_full(sig, ref) verify(idx >= 0) sig = sig[idx : idx + len(ref)] - sig[idx] verify(len(sig) == len(ref)) bref = rshape.waves[0].l rshape.move(-bref) shape = _get_guided_qrs_shape(sig, rshape) rshape.move(bref) shape.move(bref) # We admit a 25% variation in the energy of the new signal. verify(0.75 <= shape.energy / rshape.energy <= 1.25) # Absolute reference for QRS start start = idx - shape.waves[0].l verify(start >= 0) newshape[rlead] = shape for lead in hyp.shape: if lead is not rlead: rshape = hyp.shape[lead] bref = rshape.waves[0].l sig = sig_buf.get_signal_fragment( beg + start + bref, beg + start + rshape.waves[-1].r + 1, lead=lead )[0] sig = sig - sig[0] rshape.move(-bref) shape = _get_guided_qrs_shape(sig, rshape) rshape.move(bref) shape.move(bref) newshape[lead] = shape verify(signal_match(hyp.shape, newshape)) hyp.shape = newshape # The detected shapes may constrain the delineation area. llim = min(hyp.shape[lead].waves[0].l for lead in hyp.shape) if llim > 0: start = start + llim for lead in hyp.shape: hyp.shape[lead].move(-llim) end = start + max(s.waves[-1].r for s in hyp.shape.itervalues()) peak = start + min(s.waves[_reference_wave(s)].m for s in hyp.shape.itervalues()) hyp.start.value = Iv(beg + start, beg + start) hyp.time.value = Iv(beg + peak, beg + peak) hyp.end.value = Iv(beg + end, beg + end) hyp.clustered = True except InconsistencyError: hyp.shape = {} hyp.paced = False