/* file: nst.c G. Moody 8 December 1983 Last revised: 24 April 2020 ------------------------------------------------------------------------------- nst: Noise stress test Copyright (C) 1983-2009 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/). _______________________________________________________________________________ */ #include #include /* declarations of pow(), sqrt() */ #include #include #ifdef NOMKSTEMP #define mkstemp mktemp #endif /* Define the WFDB path component separator (OS-dependent). */ #ifdef MSDOS /* for MS-DOS, OS/2, etc. */ # define PSEP ';' #else #ifdef MAC /* for Macintosh */ # define PSEP ';' #else /* for UNIX, etc. */ # define PSEP ':' #endif #endif char *pname, *prog_name(); double *gn; int format = 16, nisig, nnsig, *nse, *vin, *vout, *z, *zz; WFDB_Siginfo *si; main(argc, argv) int argc; char *argv[]; { static char answer[20], buf[256], *wfdbp, irec[10], nrec[21], nnrec[20], orec[10], refaname[10], *protocol, tfname[20], *p, *s; double *anoise, *asig, *g, nsf = 0.0, nsr, *r, snr = -999999.0, ssf = 0.0; FILE *ifile; int i; WFDB_Time t, t0, tf, dt; WFDB_Anninfo ai; static WFDB_Annotation annot; void help(), nst(); pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { case 'a': /* reference annotator name follows */ if (i >= argc-1) { (void)fprintf(stderr, "%s: reference annotator name must follow -a\n", pname); exit(1); } (void)strncpy(refaname, argv[++i], 10); break; case 'F': /* format follows */ if (i >= argc-1) { (void)fprintf(stderr, "%s: signal file format must follow -F\n", pname); exit(1); } format = atoi(argv[++i]); break; case 'h': /* help requested */ help(); exit(0); break; case 'i': /* input record names follow */ if (i >= argc-2) { (void)fprintf(stderr, "%s: input ECG and noise record names must follow -i\n", pname); exit(1); } (void)strncpy(irec, argv[++i], 10); (void)strncpy(nrec+1, argv[++i], 10); nrec[0] = '+'; break; case 'o': /* output record name follows */ if (i >= argc-1) { (void)fprintf(stderr, "%s: output record name must follow -o\n", pname); exit(1); } (void)strncpy(orec, argv[++i], 10); break; case 'p': /* protocol annotator name follows */ if (i >= argc-1) { (void)fprintf(stderr, "%s: protocol annotator name must follow -p\n", pname); exit(1); } protocol = argv[++i]; break; case 's': /* signal-to-noise ratio follows */ if (i >= argc-1) { (void)fprintf(stderr, "%s: signal-to-noise ratio (in dB) must follow -s\n", pname); exit(1); } snr = atof(argv[++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); } } /* Make sure that the WFDB path begins with a '.' (current directory) component or an empty component (otherwise, nst may not be able to find the protocol file it generates). */ wfdbp = getwfdb(); if (*wfdbp != PSEP && !(*wfdbp == '.' && ((*wfdbp+1) == PSEP || *(wfdbp+1) == ' '))) { char *nwfdbp; if ((nwfdbp = (char *)malloc(strlen(wfdbp)+2)) == NULL) { fprintf(stderr, "%s: insufficient memory\n", pname); exit(1); } (void)sprintf(nwfdbp, "%c%s", PSEP, wfdbp); setwfdb(nwfdbp); } /* Generate a temporary file name for use below. */ (void)strcpy(tfname, "nsXXXXXX"); (void)mkstemp(tfname); /* Set the reference annotator name if it was not specified with -a. */ if (refaname[0] == '\0') (void)strcpy(refaname, "atr"); /* Get input record names if they were not specified with -i. */ if (irec[0] == '\0' || nrec[0] == '\0') { (void)fprintf(stderr, "Enter the name of the existing ECG record (or `?' for help): "); (void)fgets(irec, 10, stdin); irec[strlen(irec)-1] = '\0'; if (strcmp(irec, "?") == 0) { help(); exit(1); } ai.name = refaname; ai.stat = WFDB_READ; wfdbquiet(); while (annopen(irec, &ai, 1) < 0) { (void)fprintf(stderr, "Can't read annotator `%s' for record `%s'\n", ai.name, irec); (void)fprintf(stderr, "Enter the name of the reference annotator: "); (void)fgets(refaname, 10, stdin); refaname[strlen(refaname)-1] = '\0'; } wfdbquit(); wfdbverbose(); (void)fprintf(stderr, "Enter the name of the existing noise record: "); nrec[0] = '+'; (void)fgets(nrec+1, 10, stdin); nrec[strlen(nrec)-1] = '\0'; } /* Get the output record name if it was not specified using -o. */ if (orec[0] == '\0') { (void)fprintf(stderr, "Enter the name of the record to be created: "); (void)fgets(orec, 10, stdin); orec[strlen(orec)-1] = '\0'; } /* Count the input signals. */ if ((nisig = isigopen(irec, NULL, 0)) < 1) exit(2); wfdbquiet(); /* suppress warning if sampling frequencies don't match */ if ((nnsig = isigopen(nrec, NULL, 0)) < 1) { (void)fprintf(stderr, "%s: can't read record %s\n", pname, nrec+1); exit(2); } wfdbverbose(); /* Allocate storage. */ if ((si = malloc((nisig+nnsig) * sizeof(WFDB_Siginfo))) == NULL || (anoise = malloc(nnsig * sizeof(double))) == NULL || (asig = malloc(nisig * sizeof(double))) == NULL || (r = malloc(nisig * sizeof(double))) == NULL || (g = malloc(nisig * sizeof(double))) == NULL || (gn = malloc(nisig * sizeof(double))) == NULL || (nse = malloc(nisig * sizeof(int))) == NULL || (vin = malloc((nisig+nnsig) * sizeof(int))) == NULL || (vout = calloc(nisig, sizeof(int))) == NULL || (z = calloc(nisig, sizeof(int))) == NULL || (zz = calloc(nisig, sizeof(int))) == NULL) { (void)fprintf(stderr, "%s: insufficient memory\n", pname); exit(2); } for (i = 0; i < nisig; i++) g[i] = 0.; /* If the protocol was unspecified, generate one. */ if (protocol == (char*)NULL) { /* Get the SNR if it was not specified using -s. */ if (snr == -999999.0) { (void)fprintf(stderr, "Enter the desired signal-to-noise ratio in dB: "); (void)fgets(answer, 10, stdin); (void)sscanf(answer, "%lf", &snr); } /* Convert SNR in dB to nsr (RMS noise / p-p signal amplitude ratio) */ nsr = pow(10., snr/-20.)*sqrt(2.0)/4.0; (void)sprintf(buf, "sigamp -r %s >%s", nrec+1, tfname); (void)fprintf(stderr, "Checking RMS amplitude of record `%s' ...", nrec+1); if (system(buf) != 0) exit(1); (void)fprintf(stderr, " done\n"); if ((ifile = fopen(tfname, "r")) == NULL || fscanf(ifile, "%lf", &anoise[0]) < 1 || anoise[0] == 0.0) { (void)fprintf(stderr, "%s: can't read temporary file %s\n", pname, tfname); exit(1); } for (i = 1; fscanf(ifile, "%lf", &anoise[i]) == 1; i++) { if (anoise[i] == 0.0) { (void)fprintf(stderr, "%s: noise signal %d amplitude is zero -- can't normalize\n", pname, i); exit(2); } } (void)unlink(tfname); (void)sprintf(buf, "sigamp -a %s -r %s >%s", refaname, irec, tfname); (void)fprintf(stderr, "Checking normal QRS amplitude of record `%s' ...", irec); if (system(buf) != 0) exit(1); (void)fprintf(stderr, " done\n"); if ((ifile = fopen(tfname, "r")) == NULL || fscanf(ifile, "%lf", &asig[0]) < 1 || asig[0] == 0.0) { (void)fprintf(stderr, "%s: can't read temporary file %s\n", pname, tfname); exit(1); } for (i = 1; fscanf(ifile, "%lf", &asig[i]) == 1; i++) { if (asig[i] == 0.0) { (void)fprintf(stderr, "%s: ECG signal %d amplitude is zero -- can't normalize\n", pname, i); exit(2); } } (void)fclose(ifile); (void)unlink(tfname); /* Determine gains for noisy segments. */ for (i = 0; i < nisig; i++) { r[i] = nsr*asig[i]/anoise[i % nnsig]; (void)fprintf(stderr, "[rec %s, sig %d] = [rec %s, sig %d] + %g * [rec %s, sig %d]\n", orec, i, irec, i, r[i], nrec+1, i % nnsig); } /* Determine the length of the test. */ if (sampfreq(irec) <= 0.) { (void)setsampfreq(WFDB_DEFFREQ); (void)fprintf(stderr, "Enter length of test [30:00]: "); (void)fgets(buf, 20, stdin); if (sscanf(buf, "%s", answer) == 0) (void)strcpy(answer, "30:0"); } else (void)strncpy(answer, mstimstr(strtim("e")), sizeof(answer)); t0 = strtim("5:0"); dt = strtim("2:0"); tf = strtim(answer); if (tf <= 0L) tf = strtim("30:0"); (void)setsampfreq(0.); (void)fprintf(stderr, "Generating protocol annotation file ..."); #ifdef MSDOS for (p = tfname; *p; p++) if (*p == '.') { *p = '\0'; break; } #endif ai.name = protocol = tfname; ai.stat = WFDB_WRITE; if (annopen(nrec+1, &ai, 1) < 0) exit(3); annot.anntyp = NOTE; annot.aux = buf; for (t = t0; t < tf; t += dt) { buf[0] = '\0'; for (i = 0; i < nisig; i++) { /* toggle g[i] from r[i] to 0 or vice versa */ g[i] = r[i] - g[i]; (void)sprintf(buf+strlen(buf), " %g", g[i]); } /* replace initial space with byte count */ buf[0] = strlen(buf+1); annot.time = t; (void)putann(0, &annot); } buf[0] = '\0'; for (i = 0; i < nisig; i++) (void)sprintf(buf+strlen(buf), " 0"); buf[0] = strlen(buf+1); annot.time = tf; (void)putann(0, &annot); wfdbquit(); (void)fprintf(stderr, " done\n"); (void)fprintf(stderr, "The protocol annotation file is `%s'.\n", wfdbfile(tfname, nrec+1)); } /* Check sampling frequencies of input records. */ wfdbquiet(); if ((ssf = sampfreq(irec)) <= 0.) ssf = WFDB_DEFFREQ; (void)setsampfreq(0.); if ((nsf = sampfreq(nrec+1)) <= 0.) nsf = WFDB_DEFFREQ; (void)setsampfreq(0.); if (0.9*ssf > nsf || nsf > 1.1*ssf) { (void)fprintf(stderr, "Sampling frequencies of records `%s' and `%s' differ significantly\n", irec, nrec+1); p = nnrec; s = nrec+1; while (*s != '\0' && *s != '_') *p++ = *s++; (void)sprintf(p, "_%d", (int)ssf); nsf = sampfreq(nnrec); (void)setsampfreq(0.); if (0.9*ssf <= nsf && nsf <= 1.1*ssf) { (void)fprintf(stderr, " ... substituting record `%s' for `%s'\n", nnrec, nrec+1); ai.name = protocol; ai.stat = WFDB_WRITE; if (annopen(nnrec, &ai, 1) < 0) exit(2); ai.stat = WFDB_READ; if (annopen(nrec, &ai, 1) < 0) exit(2); while (getann(0, &annot) == 0) (void)putann(0, &annot); wfdbquit(); (void)unlink(wfdbfile(protocol, nrec+1)); (void)strcpy(nrec+1, nnrec); } else { if (isigopen(nrec+1, si, -nnsig) != nnsig) { (void)fprintf(stderr, "\n%s: can't read record %s\n", pname, nrec+1); exit(2); } (void)setsampfreq(ssf); (void)sprintf(buf, "%s.dat", nnrec); for (i = 0; i < nnsig; i++) si[i].fname = buf; (void)setheader(tfname, si, (unsigned)nnsig); wfdbquit(); (void)sprintf(buf, "xform -i %s -o %s -N %s -a %s", nrec+1, tfname, nnrec, protocol); (void)fprintf(stderr, " ... resampling record `%s' ...", nrec+1); if (system(buf) != 0) exit(1); (void)unlink(wfdbfile(protocol, nrec+1)); (void)unlink(wfdbfile("header", tfname)); (void)fprintf(stderr, "Resampled noise record `%s' has been generated.\n", nnrec); (void)strcpy(nrec+1, nnrec); } } wfdbverbose(); (void)fprintf(stderr, "Generating record `%s' ...", orec); nst(irec, nrec, protocol, orec, snr); (void)fprintf(stderr, " done\n"); ai.name = refaname; ai.stat = WFDB_READ; if (annopen(irec, &ai, 1) < 0) exit(2); ai.stat = WFDB_WRITE; (void)sprintf(buf, "+%s", orec); if (annopen(buf, &ai, 1) < 0) exit(2); (void)fprintf(stderr, "Copying reference annotations for record `%s' to `%s' ...", irec, orec); while (getann(0, &annot) == 0) (void)putann(0, &annot); wfdbquit(); (void)fprintf(stderr, " done\n"); exit(0); /*NOTREACHED*/ } void nst(irec, nrec, protocol, orec, snr) char *irec, *nrec, *protocol, *orec; double snr; { char buf[80], ofname[20], *p; int errct = 0, i; WFDB_Time nlen, nend, t = 0L, dt, next_tick; WFDB_Annotation annot; WFDB_Anninfo ai; /* Open ECG signals. */ if (isigopen(irec, si, nisig) != nisig) exit(2); /* Open the noise record. */ wfdbquiet(); if (isigopen(nrec, si+nisig, nnsig) != nnsig) { (void)fprintf(stderr, "%s: can't read record %s\n", pname, nrec+1); exit(2); } wfdbverbose(); /* Open the protocol annotation file. */ ai.name = protocol; ai.stat = WFDB_READ; if (annopen(nrec, &ai, 1) < 0) exit(2); /* Open the output signal file. */ (void)sprintf(ofname, "%s.dat", orec); /* name for output signal file */ for (i = 0; i < nisig; i++) { si[i].fname = ofname; si[i].group = 0; zz[i] = si[i].adczero; /* any offset will be removed */ si[i].adczero = 0; si[i].fmt = format; si[i].spf = 1; gn[i] = 0.0; nse[i] = nisig + (i % nnsig); /* signal number of noise signal to be added to ECG signal i */ } if (osigfopen(si, (unsigned)nisig) < nisig) exit(2); setsampfreq(sampfreq(NULL)); if ((nend = strtim("e")) <= 0L) nlen = nend = strtim("30:0"); else nlen = nend; next_tick = dt = strtim("1:0"); /* Output section. On each iteration of the outer loop, an annotation from the protocol annotation file is read. The time of that annotation indicates the time at which the program's state variables must be changed next. */ while (errct == 0 & getann(0, &annot) >= 0) { /* Skip any protocol annotations that aren't NOTE annotations, or that don't have `aux' fields. */ while (annot.anntyp != NOTE || annot.aux == NULL) if (getann(0, &annot) < 0) goto cleanup; /* The inner loop is executed once per sample until the time specified by the protocol annotation is reached. */ while (t < annot.time && errct == 0) { if (++t > next_tick) { next_tick += dt; (void)fprintf(stderr, "."); } /* If the end of the noise record has been reached, the input pointer(s) for the noise signals are reset so that the next noise samples to be read will be those from the beginning of the record. */ if (--nlen <= 0L) { nlen = nend; for (i = nse[0]; i < nse[0]+nnsig; i++) if (si[i].group != si[i-1].group && isgsettime(si[i].group, 0L) < 0) errct++; if (getvec(vin) < 0) errct++; /* Adjust offsets to avoid discontinuities. */ for (i = 0; i < nisig; i++) if (vin[i] != WFDB_INVALID_SAMPLE && vin[nse[i]] != WFDB_INVALID_SAMPLE && vout[i] != WFDB_INVALID_SAMPLE) z[i] = vin[i] + gn[i]*vin[nse[i]] - vout[i]; } else if (getvec(vin) < 0) errct++; for (i = 0; i < nisig; i++) { if (vin[i] != WFDB_INVALID_SAMPLE && vin[nse[i]] != WFDB_INVALID_SAMPLE && vout[i] != WFDB_INVALID_SAMPLE) { vin[i] -= zz[i]; /* remove any offset first */ vout[i] = vin[i] + gn[i]*vin[nse[i]] - z[i]; } else vout[i] = WFDB_INVALID_SAMPLE; } if (putvec(vout) < 0) errct++; } /* When the sample specified by the protocol annotation has been reached, reset the state variables. The `aux' string contains noise gains. */ for (i = 0, p = annot.aux+1; i < nisig && *p; i++) { (void)sscanf(p, "%lf", &gn[i]); z[i] = vin[i] + gn[i]*vin[nse[i]] - vout[i]; while (*p && *(p++) != ' ') ; } } /* Cleanup section. */ cleanup: (void)newheader(orec); if (snr == -999999.0) (void)sprintf(buf, " Created by `%s' from records %s and %s, using protocol %s", pname, irec, nrec+1, protocol); else (void)sprintf(buf, " Created by `%s' from records %s and %s (SNR = %g dB)", pname, irec, nrec+1, snr); (void)putinfo(buf); wfdbquit(); } 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 [OPTIONS ...]\n", "where OPTIONS may include any of:", " -a ANNOTATOR copy annotations for the specified ANNOTATOR from SREC to", " OREC (default: atr); unless -p is used, normal QRS", " annotations from this annotator are used in determining", " signal amplitudes (hence noise scale factors)", " -F N write output signals in format N (default: 16)", " -h print this usage summary", " -i SREC NREC read signals from record SREC, and noise from record NREC", " -o OREC generate output record OREC", " -p PROTOCOL use the specified PROTOCOL (in an annotation file with", " annotator name PROTOCOL and record name NREC); if this", " option is omitted, a protocol annotation file is generated", " using scale factors that may be set using -s", " -s SNR set scale factors for noise such that the signal-to-noise", " ratio during noisy segments is SNR (in dB); this option", " is ignored if a protocol is specified using -p", 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]); }