Пример #1
0
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.set(time, time)
    rdef.start.set(time, time)
    rdef.end.set(time, time)
    rdef.level = {lead: 127 for lead in leads}
    rdef.level[leads[selected.chan]] = 127 - selected.num
Пример #2
0
def _prev_rhythm_gconst(pattern, rhythm):
    """General constraints of a cardiac rhythm with the preceden one."""
    #We only accept the concatenation of the same rhythm for asystoles.
    verify(type(pattern.hypothesis) is o.Asystole
                                   or type(pattern.hypothesis) != type(rhythm))
    verify(rhythm.earlystart != pattern.hypothesis.earlystart)
    #An extrasystole does not modify the reference RR.
    pattern.hypothesis.meas = copy.copy(rhythm.meas)
Пример #3
0
def _qrs_ext_gconst_npause(pattern, qrs):
    """
    General constraints to verify that the extrasystole QRS is advanced wrt the
    environment rhythm. In addition, if there is no compensatory pause, the
    advanced QRS must have different origin than the surrounding complexes.
    """
    _qrs_ext_gconst(pattern, qrs)
    beats = pattern.evidence[o.QRS]
    if len(beats) > 1:
        verify(signal_unmatch(beats[-2].shape, qrs.shape))
Пример #4
0
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
Пример #5
0
 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
Пример #6
0
def _extrasyst_gconst(pattern, _):
    """
    General constraints of the pattern that are checked when the last T wave
    has been observed.
    """
    beats = pattern.evidence[o.QRS]
    if pattern.istate == 0:
        #We ensure that there are no missed beats.
        _check_missed_beats(pattern)
        #We must ensure that the first two beats and the last one have the
        #same shape.
        verify((beats[-3].paced and beats[-1].paced) or
                                signal_match(beats[-3].shape, beats[-1].shape))
Пример #7
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)
Пример #8
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 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
Пример #9
0
def _prev_rhythm_nreg_gconst(pattern, rhythm):
    _prev_rhythm_gconst(pattern, rhythm)
    verify(not isinstance(rhythm, o.RegularCardiacRhythm))
Пример #10
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()
Пример #11
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
Пример #12
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 xrange(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
Пример #13
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 xrange(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
Пример #14
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)