Esempio n. 1
0
def _qrs_ext_gconst(pattern, qrs):
    """
    General constraints to verify that the extrasystole qrs is advanced wrt
    the environment rhythm.
    """
    if pattern.evidence[o.Cardiac_Rhythm]:
        mrr, stdrr = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr
        if mrr > 0:
            beats = pattern.evidence[o.QRS]
            idx = beats.index(qrs)
            rr = qrs.time.start - beats[idx - 1].time.start
            mshort = min(stdrr, 0.1 * mrr, C.TMARGIN)
            verify(rr <= mrr - mshort)
Esempio n. 2
0
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 range(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
Esempio n. 3
0
def _prev_rhythm_nreg_gconst(pattern, rhythm):
    _prev_rhythm_gconst(pattern, rhythm)
    verify(not isinstance(rhythm, o.RegularCardiacRhythm))
Esempio n. 4
0
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()
Esempio n. 5
0
def _guided_qrs_observation(hyp):
    """
    Performs the delineation and checking of the general constraints of the
    QRS abstraction pattern when a reference shape for seaching is set as the
    hypothesis shape. The modification is done in-place. modifying the
    hypothesis shape.

    Parameters
    ----------
    hyp:
        QRS observation that is the hypothesis of the pattern.
    """
    if hyp.shape:
        # We perform the alignment in the lead with highest energy.
        rlead, rshape = max(hyp.shape.iteritems(), key=lambda s: s[1].energy)
        ref = rshape.sig
        newshape = {}
        start = np.inf
        beg, end = (int(hyp.earlystart), min(int(hyp.latestart) + len(ref), int(hyp.lateend)))
        if beg < 0:
            beg = 0
        try:
            sig = sig_buf.get_signal_fragment(beg, end, lead=rlead)[0]
            verify(len(sig) == end - beg + 1)
            sig = sig - sig[0]
            _, idx = xcorr_full(sig, ref)
            verify(idx >= 0)
            sig = sig[idx : idx + len(ref)] - sig[idx]
            verify(len(sig) == len(ref))
            bref = rshape.waves[0].l
            rshape.move(-bref)
            shape = _get_guided_qrs_shape(sig, rshape)
            rshape.move(bref)
            shape.move(bref)
            # We admit a 25% variation in the energy of the new signal.
            verify(0.75 <= shape.energy / rshape.energy <= 1.25)
            # Absolute reference for QRS start
            start = idx - shape.waves[0].l
            verify(start >= 0)
            newshape[rlead] = shape
            for lead in hyp.shape:
                if lead is not rlead:
                    rshape = hyp.shape[lead]
                    bref = rshape.waves[0].l
                    sig = sig_buf.get_signal_fragment(
                        beg + start + bref, beg + start + rshape.waves[-1].r + 1, lead=lead
                    )[0]
                    sig = sig - sig[0]
                    rshape.move(-bref)
                    shape = _get_guided_qrs_shape(sig, rshape)
                    rshape.move(bref)
                    shape.move(bref)
                    newshape[lead] = shape
            verify(signal_match(hyp.shape, newshape))
            hyp.shape = newshape
            # 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)
            end = start + max(s.waves[-1].r for s in hyp.shape.itervalues())
            peak = start + min(s.waves[_reference_wave(s)].m for s in hyp.shape.itervalues())
            hyp.start.value = Iv(beg + start, beg + start)
            hyp.time.value = Iv(beg + peak, beg + peak)
            hyp.end.value = Iv(beg + end, beg + end)
            hyp.clustered = True
        except InconsistencyError:
            hyp.shape = {}
            hyp.paced = False
Esempio n. 6
0
def _paced_qrs_delineation(signal, points, peak, baseline):
    """
    Checks if a sequence of waves is a paced heartbeat. The main criteria is
    the presence of a spike at the beginning of the beat, followed by at least
    one significant wave.
    """
    try:
        # Gets the slope between two points.
        slope = lambda a, b: abs(dg2mm((signal[b] - signal[a]) / sp2mm(b - a)))
        # First we search for the spike.
        spike = _find_spike(signal, points)
        verify(spike)
        if not spike[-1] in points:
            points = np.insert(points, bisect.bisect(points, spike[-1]), spike[-1])
        # Now we get relevant points, checking some related constraints.
        bpts = points[points <= spike[0]]
        apts = points[points >= spike[-1]]
        verify(len(apts) >= 2)
        # Before and after the spike there must be a significant slope change.
        verify(slope(spike[0], spike[1]) > 2.0 * slope(bpts[-2], bpts[-1]))
        verify(slope(spike[1], spike[-1]) > 2.0 * slope(apts[0], apts[1]))
        # Now we look for the end of the QRS complex, by applying the same
        # clustering strategy than regular QRS, but only for the end.
        slopes = (signal[apts][1:] - signal[apts][:-1]) / (apts[1:] - apts[:-1])
        features = []
        for i in range(len(slopes)):
            # The features are the slope in logarithmic scale and the distance to
            # the peak.
            features.append([math.log(abs(slopes[i]) + 1.0), abs(apts[i + 1] - peak)])
        features = whiten(features)
        # We initialize the centroids in the extremes (considering what is
        # interesting of each feature for us)
        fmin = np.min(features, 0)
        fmax = np.max(features, 0)
        valid = np.where(
            kmeans2(features, np.array([[fmin[0], fmax[1]], [fmax[0], fmin[1]]]), minit='matrix')[1]
        )[0]
        verify(np.any(valid))
        end = apts[valid[-1] + 1]
        # The duration of the QRS complex after the spike must be more than 2
        # times the duration of the spike.
        verify((end - apts[0]) > 2.0 * (spike[-1] - spike[0]))
        # The amplitude of the qrs complex must higher than 0.5 the amplitude
        # of the spike.
        sgspike = signal[spike[0] : spike[-1] + 1]
        sgqrs = signal[apts[0] : end + 1]
        verify(np.ptp(sgqrs) > ph2dg(0.5))
        verify(np.ptp(sgqrs) > 0.5 * np.ptp(sgspike))
        # There must be at least one peak in the QRS fragment.
        qrspt = signal[apts[apts <= end]]
        verify(len(qrspt) >= 3)
        verify(abs(signal[end] - signal[spike[0]]) <= ph2dg(0.3) or len(get_peaks(qrspt)) > 0)
        # The area of the rest of the QRS complex must be higher than the spike.
        verify(np.sum(np.abs(sgspike - sgspike[0])) < np.sum(np.abs(sgqrs - sgspike[0])))
        # The distance between the beginning of the spike and the baseline
        # cannot be more than the 30% of the amplitude of the complex.
        verify(abs(signal[spike[0]] - baseline) < 0.3 * np.ptp(signal[spike[0] : end + 1]))
        # At last, we have found the paced QRS limits.
        return Iv(spike[0], end)
    except InconsistencyError:
        return None
Esempio n. 7
0
def _qrs_delineation(signal, points, peak):
    """
    Returns the interval points of a possible QRS complex in a signal fragment.

    Parameters
    ----------
    signal:
        Array containing a signal fragment with a possible QRS inside its limits
    points:
        Representative points candidates to be the limits..
    peak:
        Point of the determined QRS peak.

    Returns
    -------
    out:
        The interval of the QRS.
    """
    try:
        verify(len(points) >= 3)
        # We get the slope of each segment determined by the relevant points
        slopes = (signal[points][1:] - signal[points][:-1]) / (points[1:] - points[:-1])
        # We also get the peaks determined by the signal simplification.
        pks = points[sig_meas.get_peaks(signal[points])]
        verify(len(pks) > 0)
        # Now we perform a clustering operation over each slope, with a certain
        # set of features.
        features = []
        for i in range(len(slopes)):
            # We obtain the midpoint of the segment, and its difference with
            # respect to the peak, applying a temporal margin.
            # We get as representative point of the segment the starting point
            # if the segment is prior to the peak, and the ending point
            # otherwise.
            point = points[i] if points[i] < peak else points[i + 1]
            # The features are the slope in logarithmic scale and the distance to
            # the peak.
            dist = abs(point - peak)
            features.append([math.log(abs(slopes[i]) + 1.0), dist])
        # We perform a clustering operation on the extracted features
        features = whiten(features)
        # We initialize the centroids in the extremes (considering what is
        # interesting of each feature for us)
        fmin = np.min(features, 0)
        fmax = np.max(features, 0)
        tags = kmeans2(features, np.array([[fmin[0], fmax[1]], [fmax[0], fmin[1]]]), minit='matrix')[1]
        valid = np.where(tags)[0]
        verify(np.any(valid))
        start = points[valid[0]]
        end = points[valid[-1] + 1]
        # If the relation between not valid and valid exceeds 0.5, we take the
        # highest valid interval containing the peak.
        if _invalidtime_rate(points, valid) > 0.5:
            # We get the last valid segment before the peak, and the first valid
            # segment after the peak. We expand them with consecutive valid
            # segments.
            try:
                start = max(v for v in valid if points[v] <= peak)
                while start - 1 in valid:
                    start -= 1
                end = min(v for v in valid if points[v + 1] >= peak)
                while end + 1 in valid:
                    end += 1
                start, end = points[start], points[end + 1]
            except ValueError:
                return None
        # We ensure there is a peak between the limits.
        verify(np.any(np.logical_and(pks > start, pks < end)))
        # If there are no peaks, we don't accept the delineation
        return Iv(start, end)
    except InconsistencyError:
        return None
Esempio n. 8
0
def _combine_limits(limits, siginfo, peak):
    """
    Combines the QRS limits detected in a set of leads, applying ad-hoc rules
    for the situation in which a paced beat is detected. This function raises
    an *InconsistencyError* exception if the limits cannot be properly combined.

    Parameters
    ----------
    limits:
        Dictionary, indexed by lead, with a tuple in each one indicating if a
        paced beat was detected in that lead, and an Interval instance with
        the delineation result.
    siginfo:
        List with the information about the signal we are dealing with. It is
        the result of the *_characterize_signal* function.
    peak:
        Situation of the QRS peak point.

    Returns
    -------
    (start, end):
        Absolute endpoints of the QRS complex obtained from the combination of
        the limits in all leads.
    """
    start = end = None
    if any(v[0] for v in limits.itervalues()):
        # There is a pacing detection, we will check if the information of
        # all leads is consistent with detection.
        # First, all spikes must start within a 40ms margin.
        try:
            spkstart = [v[1].start for v in limits.itervalues() if v[0]]
            verify(max(spkstart) - min(spkstart) <= C.TMARGIN)
            # Second, all non-paced leads must start their QRS complex in the
            # 40 ms after the first spike has appeared.
            spkstart = min(spkstart)
            verify(
                all(
                    -C.TMARGIN <= v[1].start - spkstart <= C.TMARGIN for v in limits.itervalues() if not v[0]
                )
            )
            # We have confirmed the beat is a paced beat, we set the limits
            start = spkstart
            end = max(v[1].end for v in limits.itervalues() if v[0])
            for _, endpoints in limits.itervalues():
                if 0 < endpoints.end - end <= C.TMARGIN and endpoints.end - start <= C.QRS_DUR.end:
                    end = endpoints.end
        except InconsistencyError:
            # We set the non-paced delineation for previously detected paced
            # leads.
            for lead in (k for k, v in limits.iteritems() if v[0]):
                _, sig, points, _, _ = [info for info in siginfo if info[0] == lead][0]
                endpoints = _qrs_delineation(sig, points, peak)
                if endpoints is not None:
                    limits[lead] = (False, endpoints)
                else:
                    limits.pop(lead)
    # If we have discarded all limits, we raise an exception.
    verify(limits)
    # If there is no a paced beat, we join the limits estimation of every
    # lead, by order of quality.
    if start is None:
        start, end = limits.values()[0][1].start, limits.values()[0][1].end
        for _, endpoints in limits.itervalues():
            if 0 < start - endpoints.start <= C.TMARGIN and end - endpoints.start <= C.QRS_DUR.end:
                start = endpoints.start
            if 0 < endpoints.end - end <= C.TMARGIN and endpoints.end - start <= C.QRS_DUR.end:
                end = endpoints.end
    return (start, end)