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 _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 _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 save_video(interp, fname, last=None, interval=50): """ Saves a video with the sequence of interpretations as they were generated by the interpretation process, beginning with *interp* and using *last* as last frame. Video is stored in *fname* path. Duration of each frame is controlled by *interval*, in milliseconds. """ interplist = [] queue = deque([interp]) while queue: head = queue.popleft() interplist.append(head) queue.extend(head.child) interplist.sort(key=lambda i:int(str(i))) if last is not None: interplist.append(last) sig = sig_buf.get_signal(sig_buf.get_available_leads()[0]) fig = figure(figsize=(18, 3), dpi=100, tight_layout=True) def update_figure(idx): print(str(idx) + '/' + str(len(interplist))) intplt = interplist[idx] plot_observations(sig, intplt, fig, False) fig.gca().set_xbound(lower=0, upper=len(sig)) return fig ani = animation.FuncAnimation(fig, update_figure, len(interplist), interval=interval, repeat=False) ani.save(fname, bitrate=2048)
def get_combined_energy(start, end, max_level, group=ms2sp(80)): """ This function obtains the energy intervals between two time points combined in a multilead fashion. And grouping by a distance criteria. Parameters ---------- start: Start time point to get the observations with respect to the signal buffer. end: Finish time point to get the observations wrt the signal buffer. max_level: Maximum level to search for energy intervals. See the description of the level in the *get_energy_intervals* function. group: Distance used to group close observations. Returns ------- out: Sorte list of *EnergyInterval* observations. """ #Dictionaries to store the energy intervals for each lead dicts = {} for lead in sig_buf.get_available_leads(): dicts[lead] = {} for i in range(max_level + 1): dicts[lead][i] = [] #Energy intervals detection and combination idx = start while idx < end: wfs = {} for lead in dicts: wfs[lead] = get_deflection_observations(start + idx, start + idx + TWINDOW, lead=lead, max_level=max_level, group=group) for i in range(max_level + 1): if dicts[lead][i] and wfs[lead][i]: if (wfs[lead][i][0].earlystart - dicts[lead][i][-1].lateend <= group): dicts[lead][i][-1].end.cpy(wfs[lead][i][0].start) wfs[lead][i].pop(0) dicts[lead][i].extend(wfs[lead][i]) idx += TWINDOW #Remove overlapping intervals combine_energy_intervals(list(dicts.values())) #Now we flatten the dictionaries, putting all the intervals in a sequence #sorted by the earlystart value. return SortedList(w for w in it.chain.from_iterable( it.chain.from_iterable(dic.values() for dic in dicts.values())))
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 xrange(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 xrange(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 __onclick(self, event): """Manager to the click event on the figure.""" #Left click if event.button in (1, 2, 3) and event.inaxes: trans = event.inaxes.transData #Distance from nodes (in pixels) for node in self.drnodes.copy(): posit = self.pos[node] xn, yn = trans.transform(posit) dx, dy = (event.x - xn, event.y - yn) dist = sqrt(dx * dx + dy * dy) #Nodes have a 10-pixel radius if dist < 10: #The clicked button will define the action #Button 1: plot #If the branch is already plot, we just show it if event.button == 1: if node in self._subfigs: mgr = Gcf.get_fig_manager( self._subfigs[node].fig.number) mgr.window.activateWindow() mgr.window.raise_() else: #Else plot the observations signal = sig_buf.get_signal( sig_buf.get_available_leads()[0]) #We have to keep a reference to the object to avoid #garbage collection and loosing the event manager #see http://matplotlib.org/users/event_handling.html obsview = ObservationVisualizer(signal, node) mgr = Gcf.get_fig_manager(obsview.fig.number) mgr.window.move(0, 0) obsview.fig.canvas.set_window_title(str(node)) self._subfigs[node] = obsview obsview.draw() #Button 2: Add child nodes of the selected one to the plot. elif event.button == 2: pyperclip.copy(str(node)) stack = [node] while stack: n = stack.pop() self.drnodes.add(n) if n is node or not n.is_firm: stack.extend(self.graph[n].keys()) self.redraw() #Button 3: Copy the branch name to the clipboard elif event.button == 3: pyperclip.copy(str(node))
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 _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
print('Finished in {0:.3f} seconds'.format(time.time() - t0)) print('Created {0} interpretations ({1} kept alive)'.format( interp.counter, interp.ndescendants)) #plotter.save_video(interp, '/tmp/vid.mp4', cntr.best.node) #Best explanation print(cntr.best) be = cntr.best.node be.recover_all() #print('List of resulting observations:') #pp(list(be.get_observations())) #Drawing of the best explanation brview = plotter.plot_observations( sig_buf.get_signal(sig_buf.get_available_leads()[0]), be) if args.o is not None: if args.video: plotter.save_video(interp, args.o, last=be) else: brview.fig.set_size_inches((12, 6)) brview.fig.gca().set_xbound( lower=0, upper=len(sig_buf.get_signal(sig_buf.get_available_leads()[0]))) brview.fig.savefig(args.o) #Drawing of the search tree #label_fncs = {} ##label_fncs['n'] = lambda br: str(br) #label_fncs['e'] = lambda br: '' #brview = plotter.plot_branch(interp, label_funcs=label_fncs, target=be, # full_tree=True)
def _qrs_gconst(pattern, rdef): """ Checks the general constraints of the QRS pattern transition. """ #We ensure that the abstracted evidence has been observed. if rdef.earlystart != rdef.lateend: return #The energy level of the observed interval must be low hyp = pattern.hypothesis #First we try a guided QRS observation _guided_qrs_observation(hyp) if hyp.shape: hyp.freeze() return #Hypothesis initial limits beg = int(hyp.earlystart) if beg < 0: beg = 0 end = int(hyp.lateend) #1. Signal characterization. siginfo = _characterize_signal(beg, end) verify(siginfo is not None) #2. Peak point estimation. peak = _find_peak(rdef, siginfo, beg, hyp.time) verify(peak is not None) #3. QRS start and end estimation #For each lead, we first check if it is a paced beat, whose #delineation process is different. In case of failure, we perform #common delineation. limits = OrderedDict() for lead, sig, points, baseline, _ in siginfo: endpoints = _paced_qrs_delineation(sig, points, peak, baseline) if endpoints is None: endpoints = _qrs_delineation(sig, points, peak) if endpoints is None: continue limits[lead] = (False, endpoints) else: limits[lead] = (True, endpoints) #Now we combine the limits in all leads. start, end = _combine_limits(limits, siginfo, peak) verify(start is not None and end > start) #4. QRS waveform extraction for each lead. for lead, sig, points, baseline, _ in siginfo: #We constrain the area delineated so far. sig = sig[start:end + 1] points = points[np.logical_and(points >= start, points <= end)] - start if len(points) == 0: continue if points[0] != 0: points = np.insert(points, 0, 0) if points[-1] != len(sig) - 1: points = np.append(points, len(sig) - 1) if len(points) < 3: continue #We define a distance function to evaluate the peaks dist = (lambda p: 1.0 + 2.0 * abs(beg + start + p - rdef.earlystart) / ms2sp(150)) dist = np.vectorize(dist) #We get the peak for this lead pks = points[sig_meas.get_peaks(sig[points])] if len(pks) == 0: continue peakscore = abs(sig[pks] - baseline) / dist(pks) peak = pks[peakscore.argmax()] #Now we get the shape of the QRS complex in this lead. shape = None #If there is a pace detection in this lead if lead in limits and limits[lead][0]: endpoints = limits[lead][1] shape = _get_paced_qrs_shape(sig, points, endpoints.start - start, min(endpoints.end - start, len(sig))) if shape is None: limits[lead] = (False, endpoints) if shape is None: shape = _get_qrs_shape(sig, points, peak, baseline) if shape is None: continue hyp.shape[lead] = shape #There must be a recognizable QRS waveform in at least one lead. verify(hyp.shape) #5. 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) ulim = max(hyp.shape[lead].waves[-1].r for lead in hyp.shape) if ulim < end - start: end = start + ulim #6. The definitive peak is assigned to the first relevant wave #(each QRS shapeform has a specific peak point.) peak = start + min(s.waves[_reference_wave(s)].m for s in hyp.shape.itervalues()) #7. Segmentation points set hyp.paced = any(v[0] for v in limits.itervalues()) hyp.time.value = Iv(beg + peak, beg + peak) hyp.start.value = Iv(beg + start, beg + start) hyp.end.value = Iv(beg + end, beg + end) ################################################################### #Amplitude conditions (between 0.5mV and 6.5 mV in at least one #lead or an identified pattern in most leads). ################################################################### verify( len(hyp.shape) > len(sig_buf.get_available_leads()) / 2.0 or ph2dg(0.5) <= max(s.amplitude for s in hyp.shape.itervalues()) <= ph2dg(6.5)) hyp.freeze()
ltime = (pekbfs.last_time, t0) while pekbfs.best is None: IN.get_more_evidence() acq_time = IN.get_acquisition_point() #HINT debug code fstr = 'Int: {0:05d} ' for i in range(int(sp2ms(acq_time - pekbfs.last_time) / 1000.0)): fstr += '-' fstr += ' Acq: {1}' print(fstr.format(int(pekbfs.last_time), acq_time)) #End of debug code pekbfs.step() if pekbfs.last_time > ltime[0]: ltime = (pekbfs.last_time, time.time()) if ms2sp((time.time() - ltime[1]) * 1000.0) > MAX_DELAY: print('Pruning search') if pekbfs.open: prevopen = pekbfs.open pekbfs.prune() print('Finished in {0:.3f} seconds'.format(time.time() - t0)) print('Created {0} interpretations'.format(interp.counter)) be = pekbfs.best brview = plotter.plot_observations( sig_buf.get_signal(sig_buf.get_available_leads()[0]), pekbfs.best) #Branches draw label_fncs = {} label_fncs['n'] = lambda br: str(br) label_fncs['e'] = lambda br: '' #brview = plotter.plot_branch(interp, label_funcs=label_fncs, target=pekbfs.best)
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')
cntr.step(filt) if cntr.last_time > ltime[0]: ltime = (cntr.last_time, time.time()) #If the distance between acquisition time and interpretation time is #excessive, the search tree is pruned. if ms2sp((time.time() - ltime[1]) * 1000.0) * TFACTOR > MAX_DELAY: print('Pruning search') cntr.prune() print('Finished in {0:.3f} seconds'.format(time.time() - t0)) print('Created {0} interpretations ({1} kept alive)'.format( interp.counter, interp.ndescendants)) #plotter.save_video(interp, '/tmp/vid.mp4', cntr.best.node) #Best explanation print(cntr.best) be = cntr.best.node be.recover_all() #print('List of resulting observations:') #pp(list(be.get_observations())) #Drawing of the best explanation brview = plotter.plot_observations( sig_buf.get_signal(sig_buf.get_available_leads()[0]), be) #Drawing of the search tree #label_fncs = {} ##label_fncs['n'] = lambda br: str(br) #label_fncs['e'] = lambda br: '' #brview = plotter.plot_branch(interp, label_funcs=label_fncs, target=be, # full_tree=True)