def _t_tconst(pattern, twave): """ Temporal constraints of the T Waves wrt the corresponding QRS complex. """ BASIC_TCONST(pattern, twave) obseq = pattern.obs_seq idx = pattern.get_step(twave) try: tnet = pattern.last_tnet # We find the qrs observation precedent to this T wave. qrs = next(obseq[i] for i in range(idx - 1, -1, -1) if isinstance(obseq[i], o.QRS)) # If we have more than one QRS, it is possible to constrain even more # the location of the T-Wave, based on rhythm information. qidx = pattern.evidence[o.QRS].index(qrs) if qidx > 1: refrr = (qrs.time.end - pattern.evidence[o.QRS][qidx - 2].time.start) / 2.0 tnet.add_constraint(qrs.time, twave.end, Iv(0, refrr - C.TQ_INTERVAL_MIN)) if idx > 0 and isinstance(obseq[idx - 1], o.PWave): pwave = obseq[idx - 1] tnet.add_constraint( pwave.end, twave.start, Iv(C.ST_INTERVAL.start, C.PQ_INTERVAL.end + C.QRS_DUR.end)) # ST interval tnet.add_constraint(qrs.end, twave.start, C.ST_INTERVAL) # QT duration tnet.add_constraint(qrs.start, twave.end, C.N_QT_INTERVAL) except StopIteration: pass
def _qrs3_tconst(pattern, qrs): """Temporal constraints of the third QRS complex""" BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_before(qrs.time, pattern.hypothesis.end) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) beats = pattern.evidence[o.QRS] # If there is a previous QRS if beats.index(qrs) == 1: tnet.add_constraint( beats[0].time, qrs.time, Iv(C.TACHY_RR.start + C.RR_MAX_DIFF, C.BRADY_RR.end)) # If we have reached an initial state. if pattern.istate == 0: idx = beats.index(qrs) meanrr, stdrr = pattern.hypothesis.meas.rr minrr = beats[1].time.start - beats[ 0].time.end if idx == 2 else meanrr - stdrr tnet.add_constraint( beats[idx - 1].time, qrs.time, Iv(min(C.ASYSTOLE_RR.start, minrr + C.RR_MAX_DIFF), C.ASYSTOLE_RR.start), ) # The block time has to be higher than the mean RR plus the standard # deviation. if meanrr > 0: tnet.add_constraint( beats[idx - 1].time, qrs.time, Iv(meanrr + stdrr, max(meanrr + stdrr, C.ASYSTOLE_RR.start)))
def _rdef_gconst(pattern, _): """ General constraints of the R-Deflection pattern, that simply looks in the global list for an appropriate annotation. """ if ANNOTS is None: _load_annots() leads = IN.SIG.get_available_leads() # We find all the annotations in the given interval. rdef = pattern.hypothesis beg = min(int(rdef.earlystart), IN.get_acquisition_point()) + IN._OFFSET end = min(int(rdef.lateend), IN.get_acquisition_point()) + IN._OFFSET dummy = MITAnnotation() dummy.time = beg bidx = ANNOTS.bisect_left(dummy) dummy.time = end eidx = ANNOTS.bisect_right(dummy) verify(eidx > bidx) selected = max(ANNOTS[bidx:eidx], key=operator.attrgetter('num')) time = selected.time - IN._OFFSET rdef.time.value = Iv(time, time) rdef.start.value = Iv(time, time) rdef.end.value = Iv(time, time) rdef.level = {lead: 127 for lead in leads} rdef.level[leads[selected.chan]] = 127 - selected.num
def tconst(pattern, qrs): """ Defines the temporal constraints function for the ectopic beat in an extrasystole, depending on its ventricular nature or not. """ BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_before(qrs.end, pattern.hypothesis.end) if ventricular: tnet.add_constraint(qrs.start, qrs.end, C.VQRS_DUR) # It must be the third beat. beats = pattern.evidence[o.QRS] idx = beats.index(qrs) # If there is a previous beat if idx > 0: tnet.add_constraint(beats[idx - 1].time, qrs.time, Iv(C.TACHY_RR.start, 0.9 * C.BRADY_RR.end)) # If all the previous evidence has been observed if pattern.istate == 0: # Anticipation of at least the 10% of the reference RR, or 1mm. if idx == 2: refrr = beats[1].time.end - beats[0].time.start elif pattern.evidence[o.Cardiac_Rhythm][0] is not pattern.finding: refrr = pattern.hypothesis.meas.rr[0] else: refrr = None if refrr is not None: short = min(0.1 * refrr, C.TMARGIN) tnet.add_constraint( beats[idx - 1].time, qrs.time, Iv(C.TACHY_RR.start, max(C.TACHY_RR.start, refrr - short)))
def _t_tconst(pattern, twave): """ Temporal constraints of the T wave. """ BASIC_TCONST(pattern, twave) beats = pattern.evidence[o.QRS] tnet = pattern.last_tnet qidx = qrsidx + len(beats) if qrsidx < 0 else qrsidx qrs = beats[qidx] if qidx < len(beats) - 1: tnet.set_before(twave.end, beats[qidx + 1].start) if qidx > 0: refrr = qrs.time.end - pattern.evidence[o.QRS][qidx - 1].time.start refrr = max(min(refrr, C.QTC_RR_LIMITS.end), C.QTC_RR_LIMITS.start) rtc, rtstd = pattern.hypothesis.meas.rt if rtc > 0: # Expected QT value from the QT corrected value rtmean = ms2sp(1000.0 * sp2sc(rtc) * np.cbrt(sp2sc(refrr))) tnet.add_constraint( qrs.time, twave.end, Iv(rtmean - 2.5 * rtstd, rtmean + 2.5 * rtstd)) try: tnet.add_constraint(qrs.time, twave.end, Iv(0, refrr - C.TQ_INTERVAL_MIN)) except ValueError: pass tnet.add_constraint(qrs.start, twave.end, C.N_QT_INTERVAL) # ST interval tnet.add_constraint(qrs.end, twave.start, C.ST_INTERVAL)
def _t_tconst(pattern, twave): """ Temporal constraints of the T Waves wrt the corresponding QRS complex. """ BASIC_TCONST(pattern, twave) tnet = pattern.last_tnet obseq = pattern.obs_seq idx = pattern.get_step(twave) beats = pattern.evidence[o.QRS] qidx = qrsidx + len(beats) if qrsidx < 0 else qrsidx qrs = beats[qidx] if qidx > 1: refsq = beats[qidx - 1].earlystart - beats[qidx - 2].lateend tnet.add_constraint(qrs.time, twave.end, Iv(0, max(0, refsq - C.TQ_INTERVAL_MIN))) if idx > 0 and isinstance(obseq[idx - 1], o.PWave): pwave = obseq[idx - 1] tnet.add_constraint( pwave.end, twave.start, Iv(C.ST_INTERVAL.start, C.PQ_INTERVAL.end + C.QRS_DUR.end)) if qidx < len(beats) - 1: tnet.set_before(twave.end, beats[qidx + 1].start) # ST interval tnet.add_constraint(qrs.end, twave.start, C.ST_INTERVAL) # QT duration tnet.add_constraint(qrs.start, twave.end, C.N_QT_INTERVAL) # RT variation if qidx % 2 == 0: rtmean, rtstd = pattern.hypothesis.meas.rt # We also define a constraint on T wave end based on the last # distance between normal and ectopic QRS. if qidx > 0: tnet.add_constraint( qrs.end, twave.end, Iv(0, beats[qidx - 1].earlystart - beats[qidx - 2].lateend)) else: rts = _get_measures(pattern, 1)[2] rtmean, rtstd = np.mean(rts), np.std(rts) if rtmean > 0: # The mean and standard deviation of the PQ measurements will # influence the following observations. maxdiff = C.QT_ERR_STD if len( pattern.evidence[o.TWave]) < 10 else rtstd maxdiff = max(maxdiff, C.MIN_QT_STD) interv = Iv(int(rtmean - 2.5 * maxdiff), int(rtmean + 2.5 * maxdiff)) # We avoid possible inconsistencies with constraint introduced by # the rhythm information. try: existing = tnet.get_constraint(qrs.time, twave.end).constraint except KeyError: existing = Iv(-np.inf, np.inf) if interv.overlap(existing): tnet.add_constraint(qrs.time, twave.end, interv)
def _qrs_fin_pause_tconst(pattern, qrs): """ Temporal constraints for the compensatory pause of the last QRS of the extrasystole. """ BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.end, qrs.time) beats = pattern.evidence[o.QRS] idx = beats.index(qrs) # We need all the previous evidence. if pattern.istate == 0: step = pattern.get_step(qrs) if isinstance(pattern.trseq[step - 1][1], o.TWave): twave = pattern.trseq[step - 1][1] tnet.set_before(twave.end, qrs.start) # Reference RR minrr = beats[1].time.start - beats[0].time.end if len( beats) == 4 else pattern.hypothesis.meas.rr[0] maxrr = beats[1].time.end - beats[0].time.start if len( beats) == 4 else pattern.hypothesis.meas.rr[0] # Compensatory pause tnet.add_constraint( beats[-3].time, qrs.time, Iv(min(minrr + C.COMPAUSE_MIN_DUR, minrr * C.COMPAUSE_MIN_F), maxrr * C.COMPAUSE_MAX_F), ) # Advanced beat RR minrr = beats[-2].time.start - beats[-3].time.end maxrr = beats[-2].time.end - beats[-3].time.start tnet.add_constraint( beats[-2].time, qrs.time, Iv( min(minrr * C.COMPAUSE_RREXT_MIN_F, minrr + C.COMPAUSE_RREXT_MIN), maxrr * C.COMPAUSE_RREXT_MAX_F, ), ) # The last QRS should have the same morphology than the one before the # extrasystole. if not qrs.frozen: qrs.shape = beats[-3].shape qrs.paced = beats[-3].paced elif idx > 0: # We constraint the previous beat location. tnet.add_constraint( beats[idx - 1].time, qrs.time, Iv(C.TACHY_RR.start * C.COMPAUSE_MIN_F, C.BRADY_RR.start * C.COMPAUSE_RREXT_MAX_F), )
def _qrs_tconst(pattern, rdef): """ Adds the temporal constraints of the QRS abstraction pattern automata. """ tnet = pattern.last_tnet qrs = pattern.hypothesis # QRS complex duration constraint tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) # Constraints related to the peak of the complex. tnet.add_constraint(qrs.start, qrs.time, Iv(C.QRS_START_PK, C.QRS_RDEF_DMAX)) tnet.add_constraint(qrs.time, qrs.end, Iv(C.QRS_PK_END, np.inf)) # Constraints between QRS and R-Deflection tnet.add_constraint(rdef.time, qrs.start, Iv(-C.QRS_RDEF_DMAX, C.QRS_RDEF_DMAX)) tnet.add_constraint(rdef.time, qrs.end, C.QRS_DUR)
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 combine_energy_intervals(dicts, margin=ms2sp(20)): """ Combines the overlapping observations in several dicts in the result format of the get_deflection_observations() function. Parameters ---------- dicts: List of dictionaries. The combination is always performed to the first dictionary. score: Dictionary that stores the score for each observation. For overlapping observations, the result score is the sum of the overlapped observations. margin: Group margin. Intervals separated by less than this margin are removed. """ chain = it.chain.from_iterable dict1 = dicts[0] for wint in chain(dict1.itervalues()): for i in range(1, len(dicts)): conflictive = [] for lst in dicts[i].itervalues(): if not lst: continue idx = bisect.bisect_left(lst, wint) # We go to the first real index while idx > 0 and lst[ idx - 1].lateend + margin >= wint.earlystart - margin: idx -= 1 # Now we search for overlapping intervals while idx < len(lst) and lst[ idx].earlystart - margin <= wint.lateend + margin: w = lst[idx] if Iv(w.earlystart - margin, w.lateend + margin).overlap( Iv(wint.earlystart - margin, wint.lateend + margin)): conflictive.append(w) idx += 1 if conflictive: alleads = set.union(*(set(w.level.iterkeys()) for w in conflictive)) - set( wint.level.iterkeys()) for lead in alleads: wint.level[lead] = min( w.level.get(lead, np.Inf) for w in conflictive) for wconf in conflictive: dicts[i][wconf.level.values()[0]].remove(wconf)
def _ect_qrs_tconst(pattern, qrs): """ Temporal constraints for ectopic beats, which appear after every regular beat. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) tnet = pattern.last_tnet hyp = pattern.hypothesis if idx > 0: prev = beats[idx - 1] # After the second couplet, every ectopic beat introduces a new temporal # network in the pattern to make it easier the minimization. if idx > 3: tnet.remove_constraint(hyp.end, prev.time) # We create a new temporal network for the cyclic observations tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) # The duration of each couplet should not have high instantaneous # variations. refrr = beats[idx - 2].time.end - beats[idx - 3].time.start tnet.add_constraint( prev.time, qrs.time, Iv(refrr - C.RR_MAX_DIFF, refrr + C.RR_MAX_DIFF)) # We guide the morphology search to be similar to the previous # ectopic QRS complex. qrs.shape = beats[idx - 2].shape # The reference RR varies from an upper limit to the last measurement, # through the contextual previous rhythm. refrr = C.BRADY_RR.end stdrr = 0.1 * refrr if pattern.evidence[o.Cardiac_Rhythm] and idx == 1: mrr, srr = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr if mrr > 0: refrr, stdrr = mrr, srr elif idx > 1: refrr, stdrr = hyp.meas.rr # Ectopic beats must be advanced wrt the reference RR tnet.add_constraint( prev.time, qrs.time, Iv(C.TACHY_RR.start, max(C.TACHY_RR.start, refrr - stdrr))) # Beats cannot overlap tnet.add_constraint(prev.end, qrs.start, Iv(C.TQ_INTERVAL_MIN, np.Inf)) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end) # Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs)
def _prev_rhythm_tconst(pattern, rhythm): """Temporal constraints of the flutter with the precedent rhythm""" BASIC_TCONST(pattern, rhythm) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.start, rhythm.end) tnet.add_constraint(pattern.hypothesis.start, pattern.hypothesis.end, Iv(C.VFLUT_MIN_DUR, np.inf))
def _p_tconst(pattern, pwave): """P waves temporal constraints""" BASIC_TCONST(pattern, pwave) tnet = pattern.last_tnet tnet.add_constraint(pwave.start, pwave.end, C.PW_DURATION) # We find the associated QRS. beats = pattern.evidence[o.QRS] qidx = qrsidx + len(beats) if qrsidx < 0 else qrsidx qrs = beats[qidx] if qidx > 0: tnet.set_before(beats[qidx - 1].end, pwave.start) tnet.add_constraint(pwave.start, qrs.start, C.N_PR_INTERVAL) tnet.set_before(pwave.end, qrs.start) if len(pattern.evidence[o.PWave]) > 10: # The mean and standard deviation of the PQ measurements will # influence the following observations. if not _is_ectopic(qidx): pqmean, pqstd = pattern.hypothesis.meas.pq else: pqs = _get_measures(pattern, True)[2] pqmean, pqstd = np.mean(pqs), np.std(pqs) if not np.isnan(pqmean) and not np.isnan(pqstd): interv = Iv(int(pqmean - 2 * pqstd), int(pqmean + 2 * pqstd)) if interv.overlap(C.N_PR_INTERVAL): tnet.add_constraint(pwave.start, qrs.start, interv)
def QT_FROM_RR(rr): """ Returns the interval of acceptable QT durations with the given RR intervals. It applies a linear regression model with the coefficients obtained from the referenced study. """ return Iv(m2s(220) + 0.1 * rr.start, m2s(240) + 0.25 * rr.end)
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 _qrsn_tconst(pattern, qrs): """ Temporal constraints for the QRS complexes. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) hyp = pattern.hypothesis tnet = pattern.last_tnet obseq = pattern.obs_seq oidx = pattern.get_step(qrs) prev = beats[idx - 1] # In cyclic observations, we have to introduce more networks to simplify # the minimization operation. tnet.remove_constraint(hyp.end, prev.time) tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) meanrr, stdrr = pattern.hypothesis.meas.rr rr_bounds = Iv(min(C.ASYSTOLE_RR.start, meanrr - stdrr + C.RR_MAX_DIFF), C.ASYSTOLE_RR.start) tnet.add_constraint(prev.time, qrs.time, rr_bounds) tnet.add_constraint(prev.start, qrs.start, rr_bounds) tnet.add_constraint(prev.end, qrs.end, rr_bounds) tnet.set_before(prev.end, qrs.start) # If there is a prior T Wave, it must finish before the start # of the QRS complex. if isinstance(obseq[oidx - 1], o.TWave): prevt = obseq[oidx - 1] tnet.set_before(prevt.end, qrs.start) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end) # We can introduce constraints on the morphology of the new QRS complex. if hyp.morph and not qrs.frozen: qrs.shape = hyp.morph
def _prev_rhythm_tconst(pattern, rhythm): """Temporal constraints of a cardiac rhythm with the precedent one.""" BASIC_TCONST(pattern, rhythm) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.start, rhythm.end) tnet.add_constraint(pattern.hypothesis.start, pattern.hypothesis.end, Iv(2 * C.TACHY_RR.start, 3 * C.BRADY_RR.end))
def _n1_tconst(pattern, qrs): """Temporal constraints of the normal beat determining the couplet end""" beats = pattern.evidence[o.QRS] idx = beats.index(qrs) tnet = pattern.last_tnet hyp = pattern.hypothesis _common_qrs_constraints(pattern, qrs) _qrs_after_twave(pattern, qrs) # Compensatory pause RR minrr = min( beats[idx - 1].time.start - beats[idx - 2].time.start, beats[idx - 2].time.start - beats[idx - 3].time.start, ) maxrr = max( beats[idx - 1].time.end - beats[idx - 2].time.start, beats[idx - 2].time.end - beats[idx - 3].time.start, ) mincompause = max( C.COMPAUSE_MIN_DUR, min(minrr * C.ICOUPLET_MIN_RREXT_F, minrr + C.ICOUPLET_MIN_RREXT)) tnet.add_constraint(beats[idx - 1].time, qrs.time, Iv(mincompause, maxrr * C.COMPAUSE_RREXT_MAX_F)) tnet.set_equal(qrs.time, hyp.end) # The morphology of the first and last QRS complexes should be similar qrs.shape = beats[idx - 3].shape qrs.paced = beats[idx - 3].paced
def _qrs_fin_npause_tconst(pattern, qrs): """ Temporal constraints of the fourth beat in an extrasystole without compensatory pause. """ BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.end, qrs.time) beats = pattern.evidence[o.QRS] # We need all previous evidence if pattern.istate == 0: step = pattern.get_step(qrs) twave = pattern.trseq[step - 1][1] if isinstance(twave, o.TWave): tnet.set_before(twave.end, qrs.start) # Reference RR minrr = beats[1].time.start - beats[0].time.end if len( beats) == 4 else pattern.hypothesis.meas.rr[0] maxrr = beats[1].time.end - beats[0].time.start if len( beats) == 4 else pattern.hypothesis.meas.rr[0] tnet.add_constraint(beats[-3].time, qrs.time, Iv(minrr - C.RR_MAX_DIFF, maxrr + C.RR_MAX_DIFF)) # The last QRS should have the same morphology than the one before the # extrasystole. qrs.shape = beats[-3].shape
def _reg_nae_tconst(pattern, qrs): """ Temporal constraints for regular beats not coming after ectopic beats. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) assert not _is_ectopic(idx) hyp = pattern.hypothesis tnet = pattern.last_tnet prev = beats[idx - 1] if idx > 3: # We create a new temporal network for the new trigeminy cycle. tnet.remove_constraint(hyp.end, prev.time) tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) rrev = beats[idx - 3].time.start - beats[idx - 4].time.start ##RR evolution constraint. else: rrev = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr[0] tnet.add_constraint(prev.time, qrs.time, Iv(rrev - C.RR_MAX_DIFF, rrev + C.RR_MAX_DIFF)) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.NQRS_DUR) tnet.set_before(qrs.time, hyp.end) # Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs) # Morphology should be similar to the previous QRS, since both are normal qrs.shape = prev.shape qrs.paced = prev.paced
def _ect_qrs_tconst(pattern, qrs): """ Temporal constraints for ectopic beats, which appear after every pair of regular beats. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) tnet = pattern.last_tnet hyp = pattern.hypothesis BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end) # Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs) # This check is needed because there is an abduction point invoking this # function. if idx > 0: assert _is_ectopic(idx) prev = beats[idx - 1] # The interval between ectopic beats should also be stable. if idx > 6: refrr = beats[idx - 3].time.end - beats[idx - 4].time.start tnet.add_constraint( prev.time, qrs.time, Iv(refrr - C.RR_MAX_DIFF, refrr + C.RR_MAX_DIFF)) # The reference RR varies from an upper limit to the last measurement, # through the contextual previous rhythm. refrr = C.BRADY_RR.end stdrr = 0.1 * refrr if pattern.evidence[o.Cardiac_Rhythm] and idx == 1: mrr, srr = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr if mrr > 0: refrr, stdrr = mrr, srr elif idx > 1: refrr, stdrr = hyp.meas.rr # There must be an instantaneous shortening of the RR. prevrr = prev.time.end - beats[idx - 2].time.start tnet.add_constraint( prev.time, qrs.time, Iv(C.TACHY_RR.start, max(C.TACHY_RR.start, prevrr - C.TMARGIN))) # Ectopic beats must be advanced wrt the reference RR. tnet.add_constraint( prev.time, qrs.time, Iv(C.TACHY_RR.start, max(C.TACHY_RR.start, refrr - stdrr))) tnet.set_before(prev.end, qrs.start)
def _prev_rhythm_tconst(pattern, rhythm): """Temporal constraints of the fibrillation with the precedent rhythm""" BASIC_TCONST(pattern, rhythm) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.start, rhythm.end) # An atrial fibrillation needs at least 7 QRS complexes. tnet.add_constraint(pattern.hypothesis.start, pattern.hypothesis.end, Iv(7 * C.TACHY_RR.start, np.inf))
def _v1_tconst(pattern, qrs): """Temporal constraints of the second extrasystole in the couplet""" beats = pattern.evidence[o.QRS] idx = beats.index(qrs) tnet = pattern.last_tnet prev = beats[idx - 1] # The second extrasystole must be shorter or approximately equal to the # first one, so we give the standard margin for the RR increasing. refrr = prev.time.end - beats[idx - 2].time.start const = Iv(C.TACHY_RR.start, refrr + C.ICOUPLET_MAX_DIFF) tnet.add_constraint(prev.time, qrs.time, const) tnet.add_constraint(prev.end, qrs.end, const) tnet.add_constraint(prev.end, qrs.start, Iv(C.TQ_INTERVAL_MIN, np.Inf)) # The second extrasystole should include also the same RR shortening # constraints of the first one. _v0_tconst(pattern, qrs) _qrs_after_twave(pattern, qrs)
def _prev_afib_tconst(pattern, afib): """ Temporal constraints of the fibrillation wrt a previous atrial fibrillation that will helps us to reduce the necessary evidence """ BASIC_TCONST(pattern, afib) pattern.last_tnet.add_constraint(afib.end, pattern.hypothesis.start, Iv(0, C.AFIB_MAX_DELAY))
def _t_qrs_tconst(pattern, twave): """ Temporal constraints of the T waves with the corresponding QRS complex """ BASIC_TCONST(pattern, twave) obseq = pattern.obs_seq idx = pattern.get_step(twave) tnet = pattern.last_tnet # Beat end tnet.set_equal(twave.end, pattern.hypothesis.end) # We find the qrs observation precedent to this T wave. try: qrs = next(obseq[i] for i in range(idx - 1, -1, -1) if isinstance(obseq[i], o.QRS)) # If there is no P Wave, the beat start is the QRS start. if pattern.trseq[idx][0].istate in (1, 3): tnet.set_equal(qrs.start, pattern.hypothesis.start) if idx > 0 and isinstance(obseq[idx - 1], o.PWave): pwave = obseq[idx - 1] tnet.add_constraint( pwave.end, twave.start, Iv(C.ST_INTERVAL.start, C.PQ_INTERVAL.end + C.QRS_DUR.end)) # ST interval tnet.add_constraint(qrs.end, twave.start, C.ST_INTERVAL) # QT duration tnet.add_constraint(qrs.start, twave.end, C.N_QT_INTERVAL) # If we observed a previous QRS complex, we set a limit for the QT # interval according to the RR and to the previous measures. if isinstance(obseq[1], o.QRS): rr = qrs.time.start - obseq[1].time.start rr = max(min(rr, RR_LIMITS.end), RR_LIMITS.start) rtc, rtstd = obseq[0].meas.rt if rtc > 0: # Expected QT value from the QT corrected value rtmean = msec2samples(1000.0 * sp2sg(rtc) * np.cbrt(sp2sg(rr))) tnet.add_constraint( qrs.time, twave.end, Iv(rtmean - 2.5 * rtstd, rtmean + 2.5 * rtstd)) # The PR interval is also included to limit the T wave duration pr = obseq[0].meas.pq[0] try: tnet.add_constraint(qrs.time, twave.end, Iv(0, rr - pr)) except ValueError: pass except StopIteration: pass
def nobs_before(time): """ Obtains the number of observations in the observation buffer before a given time. """ dummy = EventObservable() dummy.time.value = Iv(time, time) return _OBS.bisect_right(dummy)
def _delimit_t(signal, baseline, ls_lim, ee_lim, qrs_shape): """ This function performs the delineation of a possible T Wave present in the fragment. To obtain the endpoint of the T Wave, it uses a method based on the work by Zhang: 'An algorithm for robust and efficient location of T-wave ends in electrocardiograms'. To get the beginning, it uses a probabilistic approach with some basic morphology constraints. All the processing is made to a simplification of the signal fragment with at most 7 points. """ try: # We exclude the areas in which the slope of the signal exceeds limit. maxtslope = qrs_shape.maxslope * C.TQRS_MAX_DIFFR lidx, uidx = 0, len(signal) if ls_lim > 0: idx = np.where(np.max(np.abs(np.diff(signal[: ls_lim + 1]))) > maxtslope)[0] + 1 lidx = max(idx) if len(idx) > 0 else 0 if ee_lim < len(signal) - 1: idx = np.where(np.max(np.abs(np.diff(signal[ee_lim:]))) > maxtslope)[0] + ee_lim uidx = min(idx) if len(idx) > 0 else len(signal) - 1 if uidx > 1 and abs(signal[uidx] - baseline) > C.TWEND_BASELINE_MAX_DIFF: dfsign = np.sign(np.diff(signal[: uidx + 1])) signchange = ((np.roll(dfsign, 1) - dfsign) != 0).astype(int) if np.any(signchange): uidx = np.where(signchange)[0][-1] verify(uidx >= lidx) signal = signal[lidx : uidx + 1] ls_lim -= lidx ee_lim -= lidx # Any T waveform should be representable with at most 7 points. points = DP.arrayRDP(signal, max(ph2dg(0.02), qrs_shape.amplitude / 20.0), 7) n = len(points) verify(n >= 3) # 1. Endpoint estimation epts = points[points >= ee_lim] verify(len(epts) > 0) Tend, dum = _zhang_tendpoint(signal, epts) # 2. Onset point estimation. bpts = points[np.logical_and(points < Tend, points <= ls_lim)] score = {} # Range to normalize differences in the signal values rang = max(baseline, signal.max()) - min(signal.min(), baseline) # There must be between one and 3 peaks in the T Wave. for i in range(len(bpts)): sigpt = signal[points[i : np.where(points == Tend)[0][0] + 1]] npks = len(get_peaks(sigpt)) if len(sigpt) >= 3 else 0 if npks < 1 or npks > 2 or np.ptp(sigpt) <= ph2dg(0.05): continue bl_dist = 1.0 - np.abs(signal[bpts[i]] - baseline) / rang tdur = sp2ms(Tend - bpts[i]) score[bpts[i]] = bl_dist * _check_histogram(_TDUR_HIST, tdur) verify(score) Tbeg = max(score, key=score.get) verify(score[Tbeg] > 0) verify(np.max(np.abs(np.diff(signal[Tbeg : Tend + 1]))) <= maxtslope) return (Iv(Tbeg + lidx, Tend + lidx), dum) except InconsistencyError: return None
def _qrs_env_tconst(pattern, qrs): """Temporal constraints of the second environment QRS complex""" BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.start, qrs.time) if pattern.evidence[o.QRS].index(qrs) == 1: prev = pattern.evidence[o.QRS][0] tnet.add_constraint(prev.time, qrs.time, Iv(C.TACHY_RR.start, C.BRADY_RR.end))
def _qrs0_tconst(pattern, qrs): """ Temporal constraints of the QRS complex that must be at the beginning of the flutter. """ BASIC_TCONST(pattern, qrs) tnet = pattern.last_tnet tnet.set_equal(pattern.hypothesis.start, qrs.time) tnet.add_constraint(pattern.hypothesis.start, pattern.hypothesis.end, Iv(5 * C.TACHY_RR.start, np.inf))
def _t_defl_tconst(pattern, defl): """ Temporal constraints wrt the abstracted energy interval. """ BASIC_TCONST(pattern, defl) qrs = pattern.evidence[o.QRS][0] if pattern.evidence[o.QRS] else None twave = pattern.hypothesis tc = pattern.last_tnet tc.add_constraint(defl.start, defl.end, Iv(0, C.TW_DURATION.end)) tc.add_constraint(twave.start, defl.start, Iv(-C.TW_DEF_OVER_MAX, C.TW_DEF_OVER_MIN)) tc.add_constraint(twave.end, defl.end, Iv(-C.TW_DEF_OVER_MIN, C.TW_DEF_ENDIFF)) tc.set_before(defl.start, twave.end) tc.set_before(twave.start, defl.end) if qrs is not None: qrsdur = qrs.earlyend - qrs.latestart if qrsdur - C.TMARGIN <= C.TW_DURATION.end: tc.add_constraint(twave.start, twave.end, Iv(qrsdur - C.TMARGIN, np.inf)) tc.add_constraint(qrs.start, defl.end, Iv(0, C.QT_INTERVAL.end)) tc.set_before(qrs.end, defl.start)