def _cycle_finished_gconst(pattern, _): """ General constraints to be added when a trigeminy cycle is finished, this is, with the normal beat following an ectopy. """ #We check that there are no missed beats. _check_missed_beats(pattern) #We update the measurements of the rhythm taking the measures of the #regular cycles. rrs, pqs, rts = _get_measures(pattern, False) if len(pqs) == 0: pqm, pqst = 0, 0 elif len(pqs) == 1: #TODO use specific deviations for PQ rather than QT pqm, pqst = pqs[0], C.QT_ERR_STD else: pqm, pqst = np.mean(pqs), max(np.std(pqs), C.MIN_QT_STD) if len(rts) == 0: rtm, rtst = 0, 0 elif len(rts) == 1: rtm, rtst = rts[0], C.QT_ERR_STD else: rtm, rtst = np.mean(rts), max(np.std(rts), C.MIN_QT_STD) pattern.hypothesis.meas = o.CycleMeasurements((np.mean(rrs), np.std(rrs)), (rtm, rtst), (pqm, pqst))
def _pair_gconst(pattern, _): """ General constraints to be satisfied when a regular rhythm consists of only two beats. """ if pattern.evidence[o.Cardiac_Rhythm]: _check_missed_beats(pattern) prhythm = pattern.evidence[o.Cardiac_Rhythm][0] rhythm = pattern.hypothesis #Previous rhythm cannot be a regular rhythm. verify(not isinstance(prhythm, o.RegularCardiacRhythm)) mrr, stdrr = prhythm.meas.rr beats = pattern.evidence[o.QRS] rr = beats[-1].time.start - beats[0].time.start verify(rr in rr_bounds) #Avoid duplicate hypotheses with overlapping rhythms. if pattern.automata is SINUS_PATTERN: verify(C.TACHY_RR.end < rr < C.BRADY_RR.start) maxvar = max(C.TMARGIN, min(C.RR_MAX_DIFF, 2.5 * stdrr)) verify(rr in Iv(mrr - maxvar, mrr + maxvar)) #Besides being in rhythm, the two beats must share the morphology. verify(signal_match(beats[0].shape, beats[1].shape)) #The amplitude difference is also constrained for lead in beats[0].shape: if lead in beats[1].shape: samp, qamp = (beats[0].shape[lead].amplitude, beats[1].shape[lead].amplitude) verify( min(samp, qamp) / max(samp, qamp) >= C.MISSED_QRS_MAX_DIFF) rhythm.meas = o.CycleMeasurements( (rr, stdrr), (prhythm.meas.rt[0], C.QT_ERR_STD), (prhythm.meas.pq[0], C.QT_ERR_STD))
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. ltot = 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 _firstbeat_gconst(pattern, twave): """General constraints for the first beat, to obtain the first measure""" if twave is None: rt = 0.0 else: rt = twave.earlyend - pattern.obs_seq[0].time.start pattern.hypothesis.meas = o.CycleMeasurements((0.0, 0.0), (rt, QT_ERR_STD), (0.0, 0.0))
def get_features(interpretation): """ Obtains the relevant classification features for every QRS in the interpretation. """ result = collections.OrderedDict() rhythms = interpretation.get_observations(o.Cardiac_Rhythm) beats = sortedcontainers.SortedList(interpretation.get_observations(o.QRS)) rrs = np.diff([b.time.start for b in beats]) beatiter = iter(beats) obs = interpretation.observations qrs = None for rh in rhythms: qidx0 = bidx = 0 if qrs is None: i = 0 qrs = next(beatiter) else: i = 1 while qrs.time.start <= rh.lateend: info = BeatInfo(qrs) info.rh = rh bidx = beats.index(qrs) qidx0 = qidx0 or bidx if bidx > 0: info.rr = rrs[bidx - 1] idx = obs.index(qrs) pw = None if idx > 0 and isinstance(obs[idx - 1], o.PWave): pw = obs[idx - 1] elif idx > 1 and isinstance(obs[idx - 2], o.PWave): pw = obs[idx - 2] info.pwave = pw.amplitude if pw is not None else {} if isinstance(rh, (o.Sinus_Rhythm, o.Bradycardia, o.Tachycardia)): info.pos = REGULAR elif isinstance(rh, o.Extrasystole): info.pos = ADVANCED if i == 1 else REGULAR elif isinstance(rh, o.Couplet): info.pos = ADVANCED if i in (1, 2) else REGULAR elif isinstance(rh, (o.RhythmBlock, o.Asystole)): info.pos = DELAYED elif isinstance(rh, o.Atrial_Fibrillation): info.pos = AFIB elif isinstance(rh, o.Bigeminy): info.pos = ADVANCED if i % 2 == 1 else REGULAR elif isinstance(rh, o.Trigeminy): info.pos = ADVANCED if i % 3 == 1 else REGULAR elif isinstance(rh, o.Ventricular_Flutter): info.pos = REGULAR result[qrs] = info qrs = next(beatiter, None) if qrs is None: break i += 1 meanrr = np.mean(rrs[qidx0:bidx]) if qidx0 < bidx else rrs[bidx - 1] rh.meas = o.CycleMeasurements((meanrr, 0), (0, 0), (0, 0)) return result
def _prev_rhythm_gconst(pattern, rhythm): """General constraints of a cardiac rhythm with the preceden one.""" #We only accept the concatenation of the same rhythm for asystoles. verify( isinstance(pattern.hypothesis, o.Asystole) or type(pattern.hypothesis) != type(rhythm)) #The rhythm measurements are initially taken from the previous rhythm. pattern.hypothesis.meas = o.CycleMeasurements( rhythm.meas.rr, (rhythm.meas.rt[0], C.QT_ERR_STD), (rhythm.meas.pq[0], C.QT_ERR_STD))
def _update_measures(pattern): """ Updates the cycle time measures of the pattern. """ #Maximum number of observations considered for the measures (to avoid #excessive influence of old observations) nobs = 30 beats = pattern.evidence[o.QRS][-nobs:] obseq = pattern.obs_seq #RR rrs = np.diff([b.time.start for b in beats]) #The RT (QT) measure is updated by a Kalman Filter strategy. #Belief values rtmean, rtstd = pattern.hypothesis.meas.rt #Current RR measure (bounded) qrs = beats[-1] rr = rrs[-1] rr = max(min(rr, C.QTC_RR_LIMITS.end), C.QTC_RR_LIMITS.start) #Kalman filter algorithm, as explained in "Probabilistic Robotics" sigma_tbar = rtstd**2 + C.KF_Q**2 twave = obseq[-1] if isinstance(twave, o.TWave): #rt and corrected rt measure in the current iteration rt = twave.earlyend - qrs.time.start rtc = ms2sp(1000.0 * sp2sc(rt) / np.cbrt(sp2sc(rr))) meas_err = rtc - rtmean #Abnormally QT intervals have associated higher uncertainty qt = twave.earlyend - qrs.earlystart qt_lims = C.QT_FROM_RR(Iv(rr, rr)) #Measure uncertainty, represented by the R matrix in the Kalman filter KF_R = meas_err if qt in qt_lims else ms2sp(120) k_t = sigma_tbar / (sigma_tbar + max(KF_R, C.MIN_QT_STD)**2) else: #No measure - 0 Kalman gain meas_err = 0 k_t = 0 if rtmean == 0: mu_t = meas_err sigma_t = C.QT_ERR_STD**2 else: mu_t = rtmean + k_t * meas_err sigma_t = (1.0 - k_t) * sigma_tbar #PQ pqs = [] for pwave in pattern.evidence[o.PWave][-nobs:]: i = pattern.get_step(pwave) qrs = obseq[i - 1] pqs.append(qrs.earlystart - pwave.earlystart) pattern.hypothesis.meas = o.CycleMeasurements((np.mean(rrs), np.std(rrs)), (mu_t, np.sqrt(sigma_t)), (np.mean(pqs), np.std(pqs)))
def _update_measures(pattern): """ Updates the cycle time measures of the pattern. """ #Maximum number of observations considered for the measures (to avoid #excessive influence of old observations) nobs = 30 beats = pattern.evidence[o.QRS][-nobs:] #RR rrs = np.diff([b.time.start for b in beats]) obseq = pattern.obs_seq #The RT (QT) measure is updated by a Kalman Filter strategy. #Belief values rtmean, rtstd = pattern.hypothesis.meas.rt if (len(obseq) > 1 and isinstance(obseq[-2], o.TWave) and obseq[-2] is not pattern.finding): twave = obseq[-2] #Current RR measure (bounded) qrs = next( (q for q in reversed(beats) if q.lateend <= twave.earlystart), None) rr = qrs.time.start - beats[beats.index(qrs) - 1].time.start rr = max(min(rr, C.QTC_RR_LIMITS.end), C.QTC_RR_LIMITS.start) #Kalman filter algorithm, as explained in "Probabilistic Robotics" sigma_tbar = rtstd**2 + C.KF_Q**2 #rt and corrected rt measure in the current iteration rt = twave.earlyend - qrs.time.start rtc = ms2sp(1000.0 * sp2sc(rt) / np.cbrt(sp2sc(rr))) meas_err = rtc - rtmean #Abnormally QT intervals have associated higher uncertainty qt = twave.earlyend - qrs.earlystart qt_lims = C.QT_FROM_RR(Iv(rr, rr)) #Measure uncertainty, represented by the R matrix in the Kalman filter KF_R = meas_err if qt in qt_lims else ms2sp(120) k_t = sigma_tbar / (sigma_tbar + max(KF_R, C.MIN_QT_STD)**2) if rtmean == 0: rtmean = meas_err rtstd = C.QT_ERR_STD else: rtmean = rtmean + k_t * meas_err rtstd = np.sqrt((1.0 - k_t) * sigma_tbar) pattern.hypothesis.meas = o.CycleMeasurements((np.mean(rrs), np.std(rrs)), (rtmean, rtstd), (0.0, 0.0))
def _cycle_gconst(pattern, twave): """ General constraints applied after all the evidence of a heartbeat has been observed. The Kalman filter update for the QT limits is performed in this function. """ #Belief values rtmean, rtstd = pattern.obs_seq[0].meas.rt #Current RR measure (bounded) qrs = pattern.obs_seq[2] rr = qrs.time.start - pattern.obs_seq[1].time.start rr = max(min(rr, RR_LIMITS.end), RR_LIMITS.start) #Kalman filter algorithm, as explained in "Probabilistic Robotics" sigma_tbar = rtstd**2 + KF_Q**2 if twave is not None: #rt and corrected rt measure in the current iteration rt = twave.earlyend - qrs.time.start rtc = msec2samples(1000.0 * sp2sg(rt) / np.cbrt(sp2sg(rr))) meas_err = rtc - rtmean #Abnormally QT intervals have associated higher uncertainty qt = twave.earlyend - qrs.earlystart qt_lims = C.QT_FROM_RR(Iv(rr, rr)) #Measure uncertainty, represented by the R matrix in the Kalman filter KF_R = meas_err if qt in qt_lims else msec2samples(120) k_t = sigma_tbar / (sigma_tbar + max(KF_R, MIN_QT_STD)**2) else: #No measure - 0 Kalman gain meas_err = QT_ERR_STD k_t = 0 mu_t = rtmean + k_t * meas_err sigma_t = (1.0 - k_t) * sigma_tbar #PR interval pr = 0.0 if isinstance(pattern.obs_seq[3], o.PWave): pr = qrs.time.start - pattern.obs_seq[3].earlystart prmean = pattern.obs_seq[0].meas.pq[0] pr = (pr + prmean) / 2 if prmean > 0 else pr pattern.hypothesis.meas = o.CycleMeasurements( (rr, 0.0), (mu_t, np.sqrt(sigma_t)), (pr, 0.0))
def _rhythmstart_gconst(pattern, _): """General constraints of the rhythm start pattern.""" #We assume an starting mean rhythm of 75ppm, but the range allows from 65 #to 85bpm pattern.hypothesis.meas = o.CycleMeasurements((ms2sp(800), ms2sp(200)), (0, 0), (0, 0))