def _get_paced_qrs_shape(signal, points, start, end): """ Obtains the QRSShape object corresponding to a paced QRS complex delimited inside a signal fragment. Parameters ---------- signal: Signal fragment containing a paced QRS complex. The limits of the signal should be the limits determined by the *_paced_qrs_delineation* function. points: Relevant points in the signal fragment. start: Start point of the pace spike wrt the start of the signal. end: Finish point of the paced QRS wrt the start of the signal. Returns ------- out: QRSShape object representing the paced beat. """ try: signal = signal[start:end + 1] points = points[np.logical_and(points >= start, points <= end)] - start verify(len(points) > 0) if points[0] != 0: points = np.insert(points, 0, 0) if points[-1] != len(signal) - 1: points = np.append(points, len(signal) - 1) verify(len(points) >= 3) #We assume the baseline level is the start signal value of the spike waves = extract_waves(signal, points, signal[points[0]]) verify(waves) total_energ = sum(w.e for w in waves) #We get the longest wave sequence with a valid QRS tag. i = 0 while i < len(waves) and _tag_qrs(waves[:i + 1]) in QRS_SHAPES: i += 1 tag = _tag_qrs(waves[:i]) verify(tag in QRS_SHAPES) shape = o.QRSShape() shape.waves = waves[:i] shape.energy = sum(w.e for w in shape.waves) shape.tag = tag shape.sig = (signal[shape.waves[0].l:shape.waves[-1].r + 1] - signal[shape.waves[0].l]) shape.maxslope = np.max(np.abs(np.diff(shape.sig))) shape.amplitude = np.ptp(shape.sig) shape.move(start) verify(shape.energy / total_energ > 0.5) return shape except (ValueError, InconsistencyError): return None
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 _get_qrs_shape(signal, points, peak, baseline): """ Obtains the QRSShape object that best fits a signal fragment, considering the simplification determined by points, and the peak and baseline estimations. The detected QRS shape must collect the majority of the total energy of the waves present in the signal fragment. """ try: waves = extract_waves(signal, points, baseline) verify(waves) total_energ = sum(w.e for w in waves) #We find the longest valid sequence of waves with the highest energy. sequences = [] for i in xrange(len(waves)): #Largest valid sequence starting in the i-th wave. seq = [waves[i]] j = i + 1 while j < len(waves) and _is_qrs_complex(waves[i:j + 1]): seq.append(waves[j]) j += 1 #We add the valid sequence and the acumulated energy (we require #the peak to actually be inside the sequence.) tag = _tag_qrs(seq) energ = sum(w.e for w in seq) if (tag in QRS_SHAPES and energ / total_energ > 0.5 and any(w.l <= peak <= w.r for w in seq)): sequences.append((seq, tag, energ)) #We get the sequence with the maximum value verify(sequences) seq, tag, energ = max(sequences, key=operator.itemgetter(2)) shape = o.QRSShape() shape.energy = energ shape.tag = tag shape.waves = seq shape.sig = signal[seq[0].l:seq[-1].r + 1] - signal[seq[0].l] shape.maxslope = np.max(np.abs(np.diff(shape.sig))) shape.amplitude = np.ptp(shape.sig) return shape except (ValueError, InconsistencyError): return None
def _update_morphology(pattern): """ Updates the reference morphology of the hypothesis of the pattern from the morphology of the beats that are part of the rhythm. """ beats = pattern.evidence[o.QRS] for lead in sig_buf.get_available_leads(): #We get the most common pattern as the reference. ctr = Counter(b.shape[lead].tag for b in beats if lead in b.shape) if ctr: mc = ctr.most_common(2) #If the most common is not unique, we move on if len(mc) == 2 and mc[0][1] == mc[1][1]: continue tag = mc[0][0] energy = np.mean([ b.shape[lead].energy for b in beats if lead in b.shape and b.shape[lead].tag == tag ]) if not lead in pattern.hypothesis.morph: pattern.hypothesis.morph[lead] = o.QRSShape() pattern.hypothesis.morph[lead].tag = tag pattern.hypothesis.morph[lead].energy = energy
def ann2interp(record, anns, fmt=False): """ Returns an interpretation containing the observations represented in a list of annotations associated to a loaded MIT record. Note that only the *observations* field is properly set. The optional parameter *fmt* allows to specify if the specific Construe format for annotation files can be assumed. This parameter is also inferred from the first annotation in the list. """ fmt = (fmt or len(anns) > 0 and anns[0].code is C.NOTE and anns[0].aux == FMT_STRING) interp = Interpretation() observations = [] RH_VALS = set(C.RHYTHM_AUX.values()) for i in range(len(anns)): ann = anns[i] if ann.code in (C.PWAVE, C.TWAVE): obs = o.PWave() if ann.code == C.PWAVE else o.TWave() if fmt: beg = next(a for a in reversed(anns[:i]) if a.time < ann.time and a.code == C.WFON and a.subtype == ann.code).time end = next(a for a in anns[i:] if a.time > ann.time and a.code == C.WFOFF and a.subtype == ann.code).time else: beg = next(a for a in reversed(anns[:i]) if a.time < ann.time and a.code == C.WFON).time end = next(a for a in anns[i:] if a.time > ann.time and a.code == C.WFOFF).time obs.start.set(beg, beg) obs.end.set(end, end) if fmt: amp = json.loads(ann.aux) for l in amp.keys(): if l not in record.leads: compatible = next((l2 for l2 in VALID_LEAD_NAMES if VALID_LEAD_NAMES[l2] == l), None) if compatible is None: raise ValueError('Unrecognized lead {0}'.format(l)) obs.amplitude[compatible] = amp.pop(l) else: leads = (record.leads if ann.code is C.TWAVE else set(K.PWAVE_LEADS) & set(record.leads)) leads = record.leads for lead in leads: sidx = record.leads.index(lead) s = record.signal[sidx][beg:end+1] mx, mn = np.amax(s), np.amin(s) pol = (1.0 if max(mx-s[0], mx-s[-1]) >= -min(mn-s[0],mn-s[1]) else -1.0) obs.amplitude[lead] = pol * np.ptp(s) observations.append(obs) elif MIT.is_qrs_annotation(ann): obs = o.QRS() obs.time.set(ann.time, ann.time) obs.tag = ann.code delin = json.loads(ann.aux) #QRS start and end is first tried to set according to delineation #info. If not present, it is done according to delineation #annotations. if delin: for l in delin.keys(): if l not in record.leads: compatible = next((l2 for l2 in VALID_LEAD_NAMES if VALID_LEAD_NAMES[l2] == l), None) if compatible is None: raise ValueError('Unrecognized lead {0}'.format(l)) delin[compatible] = delin.pop(l) beg = ann.time + min(d[0] for d in delin.values()) end = ann.time + max(d[-1] for d in delin.values()) else: def extra_cond(a): return a.subtype == C.SYSTOLE if fmt else True beg = next(a for a in reversed(anns[:i]) if a.code==C.WFON and extra_cond(a)).time end = next(a for a in anns[i:] if a.code == C.WFOFF and extra_cond(a)).time #Endpoints set obs.start.set(beg, beg) obs.end.set(end, end) for lead in delin: assert len(delin[lead]) % 3 == 0, 'Unrecognized delineation' sidx = record.leads.index(lead) beg = ann.time + delin[lead][0] end = ann.time + delin[lead][-1] obs.shape[lead] = o.QRSShape() sig = record.signal[sidx][beg:end+1] obs.shape[lead].sig = sig-sig[0] obs.shape[lead].amplitude = np.ptp(sig) obs.shape[lead].energy = np.sum(np.diff(sig)**2) obs.shape[lead].maxslope = np.max(np.abs(np.diff(sig))) waves = [] for i in range(0, len(delin[lead]), 3): wav = Wave() wav.pts = tuple(delin[lead][i:i+3]) wav.move(-delin[lead][0]) if wav.r >= len(sig): warnings.warn('Found delineation information after ' 'the end of the signal in annotation {0}'.format(ann)) break wav.amp = (np.sign(sig[wav.m]-sig[wav.l]) * np.ptp(sig[wav.l:wav.r+1])) wav.e = np.sum(np.diff(sig[wav.l:wav.r+1])**2) wav.move(delin[lead][0]) wav.move(ann.time-obs.earlystart) waves.append(wav) if not waves: obs.shape.pop(lead) else: obs.shape[lead].waves = tuple(waves) obs.shape[lead].tag = _tag_qrs(waves) observations.append(obs) elif ann.code is C.RHYTHM and ann.aux in RH_VALS: rhclazz = next(rh for rh in C.RHYTHM_AUX if C.RHYTHM_AUX[rh] == ann.aux) obs = rhclazz() obs.start.set(ann.time, ann.time) end = next((a.time for a in anns[i+1:] if a.code is C.RHYTHM), anns[-1].time) obs.end.set(end, end) observations.append(obs) elif ann.code is C.ARFCT: obs = o.RDeflection() obs.time.set(ann.time, ann.time) observations.append(obs) interp.observations = sortedcontainers.SortedList(observations) return interp
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.last_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.remove_constraint(hyp.end, prev.time) #We create a new temporal network for the cyclic observations tnet = ConstraintNetwork() 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) pattern.temporal_constraints.append(tnet) #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 BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end)
def _cycle_finished_gconst(pattern, _): """ General constraints to be added when a new cycle is observed, which currently coincides with the observation of the T waves or a QRS complex not followed by an observed T wave. """ #We update the measurements and the morphology of the rhythm. _update_measures(pattern) _update_morphology(pattern) #And check that there are no missed beat forms. _check_missed_beats(pattern) beats = pattern.evidence[o.QRS] rrs = np.diff([b.time.start for b in beats[-32:]]) #HINT with this check, we avoid overlapping between sinus rhythms and #tachycardias and bradycardias at the beginning of the pattern. if len(beats) == 3: if pattern.automata is SINUS_PATTERN: verify(np.any(rrs < C.BRADY_RR.start)) elif pattern.automata is BRADYCARDIA_PATTERN: if (pattern.evidence[o.Cardiac_Rhythm] and isinstance( pattern.evidence[o.Cardiac_Rhythm][0], o.Sinus_Rhythm)): verify(any([rr not in C.SINUS_RR for rr in rrs])) elif len(beats) == 4: if pattern.automata is SINUS_PATTERN: verify(np.any(rrs > C.TACHY_RR.end)) elif pattern.automata is TACHYCARDIA_PATTERN: if (pattern.evidence[o.Cardiac_Rhythm] and isinstance( pattern.evidence[o.Cardiac_Rhythm][0], o.Sinus_Rhythm)): verify(any([rr not in C.SINUS_RR for rr in rrs])) #We impose some constraints in the evolution of the RR interval and #of the amplitude #TODO remove these lines to enable full check ###################################################################### if len(beats) >= 3: #The coefficient of variation within a regular rhythm has to be low verify(np.std(rrs) / np.mean(rrs) <= C.RR_MAX_CV) #RR evolution meanrr, stdrr = pattern.hypothesis.meas.rr verify(meanrr - 2 * stdrr <= rrs[-1] <= meanrr + 2 * stdrr or abs(rrs[-1] - rrs[-2]) <= C.RR_MAX_DIFF or 0.8 * rrs[-2] <= rrs[-1] <= 1.2 * rrs[-2]) return ####################################################################### #Morphology check. We require the rhythm morphology to be matched #by the new beat in the sequence. ref = pattern.hypothesis.morph #We initialize the morphology with the first beat. if not ref: for lead in beats[0].shape: ref[lead] = o.QRSShape() ref[lead].tag = beats[0].shape[lead].tag ref[lead].energy = beats[0].shape[lead].energy beat = beats[-1] #The leads matching morphology should sum more energy than the #unmatching. menerg = 0.0 uenerg = 0.0 perfect_match = False for lead in beat.shape: if lead in ref: bshape = beat.shape[lead] #If there is a "perfect" match in one lead, we accept clustering if (bshape.tag == ref[lead].tag and 0.75 <= bshape.energy / ref[lead].energy <= 1.25): perfect_match = True break #If there are at least 10 beats in the sequence, we require #match from the beat to the rhythm, else we are ok in both #directions. match = bshape.tag in QRS_SHAPES[ref[lead].tag] if len(beats) < 10: match = bool(match or ref[lead].tag in QRS_SHAPES[bshape.tag]) if match: menerg += ref[lead].energy else: uenerg += ref[lead].energy #If the matched energy is lower than unmatched, the hypothesis is #refuted. verify(perfect_match or menerg > uenerg) _update_morphology(pattern) if len(beats) >= 3: #RR evolution rr_prev = beats[-2].time.start - beats[-3].time.start rr_act = beats[-1].time.start - beats[-2].time.start verify(abs(rr_act - rr_prev) <= C.RR_MAX_DIFF)
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')