/* file: nguess.c G. Moody 9 June 1986 Last revised: 24 April 2020 ------------------------------------------------------------------------------- nguess: Guess the times of missing normal sinus beats in an annotation file Copyright (C) 1986-2008 George B. Moody This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . You may contact the author by e-mail (wfdb@physionet.org) or postal mail (MIT Room E25-505A, Cambridge, MA 02139 USA). For updates to this software, please visit PhysioNet (http://www.physionet.org/). _______________________________________________________________________________ This program copies its input (a WFDB annotation file containing beat annotations), removing annotations of events other than sinus beats, and interpolating additional Q (unknown beat) annotations at times when sinus beats are expected. Intervals between sinus beats are predicted using a predictor array as described by Paul Schluter ("The design and evaluation of a bedside cardiac arrhythmia monitor"; Ph.D. thesis, MIT Dept. of Electrical Engineering, 1981). When the predictions are inconsistent with the known sinus beats, as may occur in extreme noise or in highly irregular rhythms such as atrial fibrillation, no interpolations are made. It should be understood that, as the name of this program implies, the Q labels it generates represent, at best, good guesses about the times at which sinus beats may be expected. Ideally, one should avoid having to make such guesses, but some commonly-used techniques for study of heart rate variability (for example, conventional methods for power spectral density estimation in the frequency domain) require a uniformly sampled instantaneous heart rate signal, such as can be obtained using 'tach' to process the output of 'nguess'. Other techniques, such as the Lomb periodogram method implemented by 'lomb', can obtain frequency spectra from time series with missing and irregularly spaced values, such as can be produced from a beat annotation file using 'ihr' without the need to use 'nguess'. Use 'nguess' only when necessary and do not expect it to perform miracles; as a rule of thumb, if the number of guesses (Q annotations) exceeds one or two percent of the number of known sinus beats (N annotations), be exceedingly wary of the guesses and consider using techniques such as 'lomb' that do not require the use of 'nguess'. Also as a general rule, 'nguess' works best when it is guessing the locations of sinus beats obscured by noise, or those of sinus beats that were inhibited by isolated premature ventricular beats; the underlying hypothesis of a quasi-continuous sinus rhythm, the basis not only of 'nguess' but also of all other algorithms for reconstructing NN interval time series, is most suspect in the context of supraventricular ectopic beats (which may reset the SA node, thus interrupting the sinus rhythm) and consecutive ventricular ectopic beats. The predictor array method is based on the observation that most of the short-term variability in normal sinus inter-beat (NN) intervals is due to respiratory sinus arrhythmia (RSA, the quasi-periodic modulation of heart rate by respiration, which is most notable in young, healthy subjects and decreases with age). Since respiration rate is (in humans and smaller mammals) substantially slower than heart rate, it is possible to estimate the length of the respiratory cycle in terms of some number of NN intervals. If, for example, heart rate is around 60 beats per minute and respiration rate is around 10 breaths per minute, one might expect that 6 NN intervals would correspond to one breath, and that the current interval might be particularly well-approximated by the sixth previous interval. Since we don't know the ratio between heart and respiration rate a priori, we can observe how well each of the previous PBLEN intervals predicts the current interval on average. Thus we have PBLEN predictors for each interval, some of which may be much better on average than others. At any time, we know which predictor is (locally) the best, and we can use that predictor to make a best guess of the location of the next sinus beat. In subjects with significant RSA, the best predictor may be determined by the length of the respiratory cycle; in others, the previous beat may be a better predictor. For our purposes, it really doesn't matter which predictor is best, only that the mean error of the best predictor is small. If the next known sinus beat is at least 1.75 times as distant as the prediction, and if the predictions are reasonably good on average, 'nguess' asserts that a gap exists and fills it in with a Q annotation (or more than one, if the gap is sufficiently long). */ #include #include #include #include #define PBLEN 12 /* size of predictor array */ char *pname, *prog_name(); main(argc, argv) int argc; char *argv[]; { char *record = NULL; static double alpha = 0.75, bestpe, pe[PBLEN], rrmax, sps, worstpe; static int b, best, i, n, worst; static WFDB_Time rr[PBLEN], rrsum; static WFDB_Anninfo an[2]; static WFDB_Annotation in_ann, out_ann; static WFDB_Time from, from0, next, to; void help(); pname = prog_name(argv[0]); /* Interpret command-line options. */ for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { case 'a': /* annotator follows */ if (++i >= argc) { (void)fprintf(stderr, "%s: input annotator must follow -a\n", pname); exit(1); } an[0].name = argv[i]; break; case 'f': /* starting time follows */ if (++i >= argc) { (void)fprintf(stderr, "%s: starting time must follow -f\n", pname); exit(1); } from = i; /* to be converted to sample intervals below */ break; case 'h': /* print usage summary and quit */ help(); exit(0); break; case 'm': /* multiple of predicted interval needed for inserting Q */ if (++i >= argc || (alpha = atof(argv[i]) - 1.0) <= 0) { (void)fprintf(stderr, "%s: multiplier (> 1) must follow -m\n", pname); exit(1); } break; case 'o': /* output annotator follows */ if (++i >= argc) { (void)fprintf(stderr, "%s: output annotator must follow -o\n", pname); exit(1); } an[1].name = argv[i]; break; case 'r': /* input record name follows */ if (++i >= argc) { (void)fprintf(stderr, "%s: input record name must follow -r\n", pname); exit(1); } record = argv[i]; break; case 't': /* ending time follows */ if (++i >= argc) { (void)fprintf(stderr, "%s: end time must follow -t\n", pname); exit(1); } to = i; break; default: (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); } else { (void)fprintf(stderr, "%s: unrecognized argument %s\n", pname, argv[i]); exit(1); } } /* Check that required arguments are present. */ if (record == NULL || an[0].name == NULL) { help(); exit(1); } an[0].stat = WFDB_READ; if (an[1].name == NULL) an[1].name = "nguess"; an[1].stat = WFDB_WRITE; /* Open the input and output annotation files. */ wfdbquiet(); if ((sps = sampfreq(record)) < 0) { fprintf(stderr, "%s: can't read header for record %s, assuming %g Hz\n", pname, record, sps = WFDB_DEFFREQ); setsampfreq(sps); } wfdbverbose(); if (annopen(record, an, 2) < 0) exit(2); if (getiaorigfreq(0) > sps) { setifreq(sps = getiaorigfreq(0)); setiafreq(0, sps); } if (from) { from = strtim(argv[(int)from]); if (from < (WFDB_Time)0) from = -from; } if (to) { to = strtim(argv[(int)to]); if (to < (WFDB_Time)0) to = -to; } if (to < from && to > 0L) { WFDB_Time tt = from; from = to; to = tt; } rrmax = sps/2.0; /* Begin processing a minute before the specified start time, if possible. No output is produced until the specified start time, but the extra minute gives the program a chance to `learn' the rhythm. */ if ((from0 = from - strtim("1:0")) < 0L) from0 = 0L; if (from0 > 0L && iannsettime(from0) < 0) { wfdbquit(); exit(0); } /* Get the first normal beat annotation after from0. */ getnormal(&in_ann); out_ann.time = in_ann.time; /* Once past `from', emit an output annotation for each iteration of the loop below. */ while (to <= 0L || out_ann.time < to) { /* Get (in in_ann) the normal beat annotation that occurs nearest the prediction, and get the following normal beat annotation in in_ann. Once the predictor array is full, the prediction is updated on each iteration, and the following lines discard interpolated beats and false detections. The assumption is that the true sinus rate never doubles instantaneously. */ while (map2(in_ann.anntyp) != NORMAL || in_ann.time < next) /* get input annot if needed */ getnormal(&in_ann); /* From the previous RR intervals, choose the best predictor. */ for (i = n, bestpe = 99999., worst = 0.; i > 0; i--) { double error; /* Put an upper bound on the prediction error, to limit the influence of a single observation on pe[i]. */ if ((error = fabs(rr[i] - rr[0])) > sps/2) error = sps/2; if (rr[i] > 0 && (pe[i] += (error - pe[i])/20.) <= bestpe) { bestpe = pe[i]; best = i; } /* find best predicting interval */ if (pe[i] >= worstpe) { worstpe = pe[i]; worst = i; } rr[i] = rr[i-1]; /* shift RR buffer */ } /* Define the current RR interval as the difference between the time of the current normal beat and the time of the most recently written output (beat) annotation. */ rr[0] = in_ann.time - out_ann.time; /* If the predictor array is not yet full, just copy the input annotation to the output (if it falls after `from'). */ if (n < PBLEN-1) { n++; out_ann.anntyp = in_ann.anntyp; in_ann.anntyp = NOTQRS; if ((out_ann.time += rr[0]) > from) putann(0, &out_ann); continue; } /* If the interval is long enough, search for a gap (corresponding to a non-conducted P-wave, or, more likely, an undetected sinus beat). */ i = 0; /* expected number of RR intervals contained within rr[0] */ if (rr[0] > rrmax) { b = best; /* index of best predicting interval */ rrsum = 0; /* sum of i predicted intervals */ while (rr[0] > rrsum + rr[b]*alpha) { rrsum += rr[b]; if (--b <= 0) b = best; /* get next prediction index */ i++; } } if (i > 1) { /* gap found -- prepare to insert an annotation */ rr[0] = 0.5 + ((double)rr[0]*rr[best])/(double)rrsum; out_ann.anntyp = UNKNOWN; } else { /* no gap -- prepare to copy the input annotation */ out_ann.anntyp = in_ann.anntyp; in_ann.anntyp = NOTQRS; } if ((out_ann.time += rr[0]) > from) putann(0, &out_ann); /* emit an output annotation */ /* If rr[0] doesn't match the best predicting interval reasonably well, adjust it to a weighted sum of the predicted and observed intervals. The weighting heavily favors the prediction, on the assumption that the observation is in error (or is unrepresentative of sinus rhythm). */ if (fabs(rr[best] - rr[0]) > rr[best]/10) rr[0] = (9*rr[best] + rr[0])/10; /* Predict the time of the next sinus beat, if the best and worst predictions are in reasonable agreement. */ if (fabs(rr[best] - rr[worst]) < rr[best]/8) next = out_ann.time + (rr[best-1] < sps) ? (19*rr[best-1])/20 : 19*sps/20; } } getnormal(ap) WFDB_Annotation *ap; { do { if (getann(0, ap) < 0) { wfdbquit(); exit(0); } } while (map2(ap->anntyp) != NORMAL); } char *prog_name(s) char *s; { char *p = s + strlen(s); #ifdef MSDOS while (p >= s && *p != '\\' && *p != ':') { if (*p == '.') *p = '\0'; /* strip off extension */ if ('A' <= *p && *p <= 'Z') *p += 'a' - 'A'; /* convert to lower case */ p--; } #else while (p >= s && *p != '/') p--; #endif return (p+1); } static char *help_strings[] = { "usage: %s -r RECORD -a ANNOTATOR [OPTIONS ...]\n", "where RECORD and ANNOTATOR specify the input, and OPTIONS may include:", " -f TIME start at specified TIME", " -h print this usage summary", " -m M insert a Q if RR > M * prediction (M > 1; default: M = 1.75)", " -o OANN write output as annotator OANN (default: nguess)", " -t TIME stop at specified TIME", "The output contains copies of all N annotations, with additional Q", "annotations inserted at the inferred locations of missing N (sinus) beats.", NULL }; void help() { int i; (void)fprintf(stderr, help_strings[0], pname); for (i = 1; help_strings[i] != NULL; i++) { (void)fprintf(stderr, "%s\n", help_strings[i]); if (i % 23 == 0) { char b[5]; (void)fprintf(stderr, "--More--"); (void)fgets(b, 5, stdin); (void)fprintf(stderr, "\033[A\033[2K"); /* erase "--More--"; assumes ANSI terminal */ } } }