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 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 eval_vflut(anns, _): """Evaluates the ventricular flutter presence""" lth, uth, dth = ms2sp( (4 * 60 + 45) * 1000), ms2sp(5 * 60 * 1000), ms2sp(3500) #We remove separations between consecutive flutter fragments i = 0 while i < len(anns): if anns[i].code is ECGCodes.VFOFF: onset = next((j for j in range(i, len(anns)) if anns[j].code is ECGCodes.VFON), None) if onset is not None and anns[i].time == anns[onset].time: anns.pop(onset) anns.pop(i) i -= 1 i += 1 vflim = (a for a in anns if a.code in (ECGCodes.VFON, ECGCodes.VFOFF)) vfluts = [] while True: try: beg = next(vflim) end = next(vflim) vfluts.append(Iv(beg.time, end.time)) except StopIteration: break #If the record shows many flutter fragments, we simply check some flutter #waves in the last 15 seconds. if sum(fl.length for fl in vfluts) > ms2sp(20000): vfw = [ a.time for a in anns if a.code is ECGCodes.FLWAV and lth <= a.time <= uth ] return len(vfw) > 5 interv = Iv(lth, uth) return any([interv.intersection(vflut).length > dth for vflut in vfluts])
def _reg_ae_tconst(pattern, qrs): """ Temporal constraints for regular beats coming after ectopic beats. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) assert _is_ectopic(idx - 1) tnet = pattern.tnet hyp = pattern.hypothesis 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) #The first regular beat takes the reference RR from the previous rhythm #and the subsequent take the reference from the proper trigeminy. if idx == 2: refrr = pattern.evidence[o.Cardiac_Rhythm][0].meas[0][0] else: refrr = beats[idx - 2].time.end - beats[idx - 3].time.start const = Iv(min(2 * refrr - C.RR_MAX_DIFF, refrr * C.COMPAUSE_MIN_F), max(2 * refrr + C.RR_MAX_DIFF, refrr * C.COMPAUSE_MAX_F)) tnet.add_constraint(beats[idx - 2].time, qrs.time, const) tnet.add_constraint(beats[idx - 2].start, qrs.start, const) tnet.add_constraint(beats[idx - 2].end, qrs.end, const) #Compensatory pause RR constraints minrr = beats[idx - 1].time.start - beats[idx - 2].time.start maxrr = beats[idx - 1].time.end - beats[idx - 2].time.start mincompause = max( C.COMPAUSE_MIN_DUR, min(minrr * C.COMPAUSE_RREXT_MIN_F, minrr + C.COMPAUSE_RREXT_MIN)) tnet.add_constraint(beats[idx - 1].time, qrs.time, Iv(mincompause, maxrr * C.COMPAUSE_RREXT_MAX_F)) #The morphology should be similar to the previous non-ectopic QRS qrs.shape = beats[idx - 2].shape qrs.paced = beats[idx - 2].paced
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 _t_tconst(pattern, twave): """ Temporal constraints of the T wave. """ beats = pattern.evidence[o.QRS] tnet = pattern.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) 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 xrange(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 _qrs_tconst(pattern, qrs): """ Temporal constraints to observe a new QRS complex. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) hyp = pattern.hypothesis tnet = pattern.tnet obseq = pattern.obs_seq oidx = pattern.get_step(qrs) #The environment complex sets the start of the rhythm observation. if pattern.get_evidence_type(qrs)[1] is ENVIRONMENT: tnet.set_equal(hyp.start, qrs.time) else: if idx > 0: prev = beats[idx - 1] tnet.add_constraint(prev.time, qrs.time, rr_bounds) if rr_bounds is not C.TACHY_RR: #Also bounding on begin and end, but with relaxed variation #margin. rlx_rrb = Iv(rr_bounds.start - C.TMARGIN, rr_bounds.end + C.TMARGIN) tnet.add_constraint(prev.start, qrs.start, rlx_rrb) tnet.add_constraint(prev.end, qrs.end, rlx_rrb) 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) ##RR evolution constraint. We combine the statistical limits #with a dynamic evolution. if idx > 1: prev2 = beats[idx - 2] rrev = prev.time.start - prev2.time.start if hyp.meas.rr[0] > 0: meanrr, stdrr = hyp.meas.rr const = Iv( min(0.8 * rrev, rrev - C.RR_MAX_DIFF, meanrr - 2 * stdrr), max(1.2 * rrev, rrev + C.RR_MAX_DIFF, meanrr + 2 * stdrr)) else: const = Iv(min(0.8 * rrev, rrev - C.RR_MAX_DIFF), max(1.2 * rrev, rrev + C.RR_MAX_DIFF)) tnet.add_constraint(prev.time, qrs.time, const) #TODO improve if not qrs.frozen and hyp.morph: nullsh = o.QRSShape() refbeat = next((b for b in reversed(beats[:idx]) if not b.clustered and all( b.shape.get(lead, nullsh).tag == hyp.morph[lead].tag for lead in hyp.morph)), None) if refbeat is not None: qrs.shape = refbeat.shape qrs.paced = refbeat.paced tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end)
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 _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 _def_gconst(pattern, _): """General constraints for the energy interval abstraction pattern""" verify(pattern.hypothesis.lateend < np.inf) #The margin to group consecutive fragments is 1 mm #Limits for the detection. beg = int(pattern.hypothesis.earlystart) end = int(pattern.hypothesis.lateend) #Now we get the energy accumulated in all leads. energy = None for lead in sig_buf.get_available_leads(): lenerg, fbeg, fend = sig_buf.get_energy_fragment( beg, end, TWINDOW, lead) energy = lenerg if energy is None else energy + lenerg if energy is None: return 0.0 #We get the already published fragments affecting our temporal support. conflictive = [] published = SortedList(obs_buf.get_observations(o.Deflection)) idx = published.bisect_left(pattern.hypothesis) if idx > 0 and published[idx - 1].lateend > beg: idx -= 1 while (idx < len(published) and Iv(beg, end).overlap( Iv(published[idx].earlystart, published[idx].lateend))): conflictive.append( Iv(published[idx].earlystart - beg + fbeg, published[idx].lateend - beg + fbeg)) idx += 1 #We obtain the relative limits of the energy interval wrt the fragment iv_start = Iv(fbeg, fbeg + int(pattern.hypothesis.latestart - beg)) iv_end = Iv(fend - int(end - pattern.hypothesis.earlyend), fend) #We look for the highest-level interval satisfying the limits. interval = None lev = 0 while interval is None and lev <= 20: areas = [ iv for iv in get_energy_intervals(energy, lev, group=TMARGIN) if iv.start in iv_start and iv.end in iv_end and all( not iv.overlapm(ein) for ein in conflictive) ] #We sort the areas by energy, with the highest energy first. areas.sort( key=lambda interv: np.sum(energy[interv.start:interv.end + 1]), reverse=True) #Now we take the element indicated by the index. if len(areas) > int_idx: interval = areas[int_idx] else: lev += 1 verify(interval is not None) pattern.hypothesis.start.set(interval.start + beg - fbeg, interval.start + beg - fbeg) pattern.hypothesis.end.set(interval.end + beg - fbeg, interval.end + beg - fbeg) for lead in sig_buf.get_available_leads(): pattern.hypothesis.level[lead] = lev
def _reg_qrs_tconst(pattern, qrs): """ Temporal constraints for regular beats, which appear after every ectopic beat. """ 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.NQRS_DUR) tnet.set_before(qrs.time, hyp.end) #Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs) #The environment QRS complex determines the beginning of the bigeminy. if pattern.get_evidence_type(qrs)[1] is ENV: tnet.set_equal(hyp.start, qrs.time) else: #The first regular beat takes the reference RR from the previous rhythm #and the subsequent take the reference from the proper bigeminy. if idx == 2: refrr, stdrr = pattern.evidence[o.Cardiac_Rhythm][0].meas[0] max_var = max(2 * C.RR_MAX_DIFF, 4 * stdrr) tnet.add_constraint( beats[0].time, qrs.time, Iv(min(2 * refrr - max_var, refrr * C.COMPAUSE_MIN_F), max(2 * refrr + max_var, refrr * C.COMPAUSE_MAX_F))) else: ref2rr = beats[idx - 2].time.end - beats[idx - 4].time.start mrr, srr = hyp.meas.rr const = Iv(min(ref2rr - 2 * C.RR_MAX_DIFF, 2 * mrr - 4 * srr), max(ref2rr + 2 * C.RR_MAX_DIFF, 2 * mrr + 4 * srr)) tnet.add_constraint(beats[idx - 2].time, qrs.time, const) tnet.add_constraint(beats[idx - 2].start, qrs.start, const) tnet.add_constraint(beats[idx - 2].end, qrs.end, const) #We guide the morphology search to be similar to the previous regular #QRS complex. qrs.shape = beats[idx - 2].shape qrs.paced = beats[idx - 2].paced #Compensatory pause RR minrr = beats[idx - 1].time.start - beats[idx - 2].time.end maxrr = beats[idx - 1].time.end - beats[idx - 2].time.start refcompause = (beats[idx - 2].time.start - beats[idx - 3].time.start if idx > 2 else maxrr * C.COMPAUSE_RREXT_MAX_F) mincompause = max( C.COMPAUSE_MIN_DUR, maxrr, min(minrr * C.COMPAUSE_RREXT_MIN_F, refcompause - C.TMARGIN, minrr + C.COMPAUSE_RREXT_MIN)) tnet.add_constraint(beats[idx - 1].time, qrs.time, Iv(mincompause, maxrr * C.COMPAUSE_RREXT_MAX_F)) #Beats cannot overlap tnet.add_constraint(beats[idx - 1].end, qrs.start, Iv(C.TQ_INTERVAL_MIN, np.Inf))
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.values()): for i in range(1, len(dicts)): conflictive = [] for lst in dicts[i].values(): 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.keys()) for w in conflictive)) - set( wint.level.keys()) for lead in alleads: wint.level[lead] = min( w.level.get(lead, np.Inf) for w in conflictive) for wconf in conflictive: dicts[i][next(wconf.level.values())].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 _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 _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 _delimit_p(signal, lead, es_lim, ls_lim, ee_lim): """ Performs the delimitation of a P wave in a signal fragment. If a waveform compatible with a P wave cannot be found, returns None, else return an Interval within signal length. """ #shape simplification (ignoring the environment signal) delta = ph2dg(0.02) points = DP.arrayRDP(signal[int(es_lim):], delta, 6) + int(es_lim) #If no relevant disturbances are detected, there is no a P wave if len(points) == 2: return None #Now we look for the shorter limits that satisfy the P-Wave classifier. cand = None i = next(k for k in range(len(points)-1, -1, -1) if points[k] <= ls_lim) while i >= 0: j = next(k for k in range(i+1, len(points)) if points[k] >= ee_lim) while j < len(points): sigfr = signal[points[i]:points[j]+1] #We consider a good P wave environment if the signal has no #amplitude variations beg = int(max(0, points[i]-C.PWAVE_ENV)) plainenv = not np.any(signal[beg:points[i]+1]-signal[beg]) #The minimum threshold varies with the environment quality ampthres = C.PWAVE_MIN_AMP if not plainenv else delta if (seems_pwave(sigfr, lead) and np.ptp(sigfr) >= ampthres): cand = (points[i], points[j]) break j += 1 if cand is not None: break i -= 1 return None if cand is None else Iv(int(cand[0]-es_lim), int(cand[1]-es_lim))
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 _qrs_tconst(pattern, qrs): """ Temporal constraints for the QRS complexes. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) hyp = pattern.hypothesis tnet = pattern.tnet obseq = pattern.obs_seq oidx = pattern.get_step(qrs) if idx > 0: prev = beats[idx-1] rr_bounds = Iv(C.TACHY_RR.start, C.BRADY_RR.end) 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) 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 _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 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 _prev_afib_tconst(pattern, afib): """ Temporal constraints of the fibrillation wrt a previous atrial fibrillation that will helps us to reduce the necessary evidence """ pattern.tnet.add_constraint(afib.end, pattern.hypothesis.start, Iv(0, C.AFIB_MAX_DELAY))
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 qidx % 2 == 0: 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 _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 _qrs_env_tconst(pattern, qrs): """Temporal constraints of the second environment QRS complex""" pattern.tnet.set_equal(pattern.hypothesis.start, qrs.time) if pattern.evidence[o.QRS].index(qrs) == 1: prev = pattern.evidence[o.QRS][0] pattern.tnet.add_constraint(prev.time, qrs.time, Iv(C.TACHY_RR.start, C.BRADY_RR.end))
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 _qrs0_tconst(pattern, qrs): """ Temporal constraints of the QRS complex that must be at the beginning of the flutter. """ pattern.tnet.set_equal(pattern.hypothesis.start, qrs.time) pattern.tnet.add_constraint(pattern.hypothesis.start, pattern.hypothesis.end, Iv(C.VFLUT_MIN_DUR, np.inf))
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)