From bc0a17c7fbdf99fd5dabb620cf344d0323d5aaf1 Mon Sep 17 00:00:00 2001
From: Aaron Viets <aaron.viets@ligo.org>
Date: Wed, 27 Oct 2021 12:49:23 -0700
Subject: [PATCH] lal_transferfunction:  Added ability to take sparse TFs and
 shift frequencies

---
 .../gst/lal/gstlal_transferfunction.c         | 282 +++++++++++-------
 .../gst/lal/gstlal_transferfunction.h         |   2 +
 gstlal-calibration/lib/gstlal_firtools.c      |   9 +-
 .../tests/check_calibration/Makefile          |  13 +-
 .../check_calibration/pcal2darm_timeseries.py |  10 +-
 .../plot_transfer_function.py                 |   2 +-
 6 files changed, 195 insertions(+), 123 deletions(-)

diff --git a/gstlal-calibration/gst/lal/gstlal_transferfunction.c b/gstlal-calibration/gst/lal/gstlal_transferfunction.c
index 772c4f32be..ff11416a28 100644
--- a/gstlal-calibration/gst/lal/gstlal_transferfunction.c
+++ b/gstlal-calibration/gst/lal/gstlal_transferfunction.c
@@ -227,7 +227,7 @@ DEFINE_MAXIMUM(32);
 DEFINE_MAXIMUM(64);
 
 
-static void write_transfer_functions(complex double *tfs, char *element_name, double df, gint64 rows, int columns, double t_start, double t_finish, gboolean write_to_screen, char *filename, gboolean free_name) {
+static void write_transfer_functions(complex double *tfs, char *element_name, double f0, double df, gint64 rows, int columns, double t_start, double t_finish, gboolean write_to_screen, char *filename, gboolean free_name) {
 	gint64 i;
 	int j, j_stop;
 	if(write_to_screen) {
@@ -238,17 +238,17 @@ static void write_transfer_functions(complex double *tfs, char *element_name, do
 
 		j_stop = columns - 1;
 		for(i = 0; i < rows; i++) {
-			g_print("%10f\t", i * df);
+			g_print("%10f\t", f0 + i * df);
 			for(j = 0; j < j_stop; j++) {
 				if(cimag(tfs[i + j * rows]) < 0.0)
-					g_print("%10e - %10ei\t\t", creal(tfs[i + j * rows]), -cimag(tfs[i + j * rows]));
+					g_print("%1.15e - %1.15ei\t\t", creal(tfs[i + j * rows]), -cimag(tfs[i + j * rows]));
 				else
-					g_print("%10e + %10ei\t\t", creal(tfs[i + j * rows]), cimag(tfs[i + j * rows]));
+					g_print("%1.15e + %1.15ei\t\t", creal(tfs[i + j * rows]), cimag(tfs[i + j * rows]));
 			}
 			if(cimag(tfs[i + j_stop * rows]) < 0.0)
-				g_print("%10e - %10ei\n", creal(tfs[i + j_stop * rows]), -cimag(tfs[i + j_stop * rows]));
+				g_print("%1.15e - %1.15ei\n", creal(tfs[i + j_stop * rows]), -cimag(tfs[i + j_stop * rows]));
 			else
-				g_print("%10e + %10ei\n", creal(tfs[i + j_stop * rows]), cimag(tfs[i + j_stop * rows]));
+				g_print("%1.15e + %1.15ei\n", creal(tfs[i + j_stop * rows]), cimag(tfs[i + j_stop * rows]));
 		}
 		g_print("\n\n");
 	}
@@ -263,17 +263,17 @@ static void write_transfer_functions(complex double *tfs, char *element_name, do
 
 		j_stop = columns - 1;
 		for(i = 0; i < rows; i++) {
-			g_fprintf(fp, "%10f\t", i * df);
+			g_fprintf(fp, "%10f\t", f0 + i * df);
 			for(j = 0; j < j_stop; j++) {
 				if(cimag(tfs[i + j * rows]) < 0.0)
-					g_fprintf(fp, "%10e - %10ei\t\t", creal(tfs[i + j * rows]), -cimag(tfs[i + j * rows]));
+					g_fprintf(fp, "%1.15e - %1.15ei\t\t", creal(tfs[i + j * rows]), -cimag(tfs[i + j * rows]));
 				else
-					g_fprintf(fp, "%10e + %10ei\t\t", creal(tfs[i + j * rows]), cimag(tfs[i + j * rows]));
+					g_fprintf(fp, "%1.15e + %1.15ei\t\t", creal(tfs[i + j * rows]), cimag(tfs[i + j * rows]));
 			}
 			if(cimag(tfs[i + j_stop * rows]) < 0.0)
-				g_fprintf(fp, "%10e - %10ei\n", creal(tfs[i + j_stop * rows]), -cimag(tfs[i + j_stop * rows]));
+				g_fprintf(fp, "%1.15e - %1.15ei\n", creal(tfs[i + j_stop * rows]), -cimag(tfs[i + j_stop * rows]));
 			else
-				g_fprintf(fp, "%10e + %10ei\n", creal(tfs[i + j_stop * rows]), cimag(tfs[i + j_stop * rows]));
+				g_fprintf(fp, "%1.15e + %1.15ei\n", creal(tfs[i + j_stop * rows]), cimag(tfs[i + j_stop * rows]));
 		}
 		g_fprintf(fp, "\n\n");
 		fclose(fp);
@@ -295,8 +295,8 @@ static void write_fir_filters(double *filters, char *element_name, gint64 rows,
 		j_stop = columns - 1;
 		for(i = 0; i < rows; i++) {
 			for(j = 0; j < j_stop; j++)
-				g_print("%10e\t", filters[i + j * rows]);
-			g_print("%10e\n", filters[i + j_stop * rows]);
+				g_print("%1.15e\t", filters[i + j * rows]);
+			g_print("%1.15e\n", filters[i + j_stop * rows]);
 		}
 		g_print("\n\n");
 	}
@@ -312,8 +312,8 @@ static void write_fir_filters(double *filters, char *element_name, gint64 rows,
 		j_stop = columns - 1;
 		for(i = 0; i < rows; i++) {
 			for(j = 0; j < j_stop; j++)
-				g_fprintf(fp, "%10e\t", filters[i + j * rows]);
-			g_fprintf(fp, "%10e\n", filters[i + j_stop * rows]);
+				g_fprintf(fp, "%1.15e\t", filters[i + j * rows]);
+			g_fprintf(fp, "%1.15e\n", filters[i + j_stop * rows]);
 		}
 		g_fprintf(fp, "\n\n");
 		fclose(fp);
@@ -402,9 +402,9 @@ static gboolean update_transfer_functions_ ## DTYPE(complex DTYPE *autocorrelati
 			 * First, apply the sinc filter to higher frequencies. We could hit the edge
 			 * of the sinc table or the Nyquist frequency of the transfer function.
 			 */ \
-			sinc0 = (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input; \
+			sinc0 = sinc_length / 2 + (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input; \
 			input0 = j + (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1) * elements_per_freq; \
-			k_stop = minimum64((sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input, fd_fft_length - (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1)); \
+			k_stop = minimum64((sinc_taps_per_input - 1 + sinc_length - sinc0) / sinc_taps_per_input, fd_fft_length - (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1)); \
 			for(k = 0; k < k_stop; k++) \
 				z += sinc_table[sinc0 + k * sinc_taps_per_input] * autocorrelation_matrix[input0 + k * elements_per_freq]; \
 			/*
@@ -413,27 +413,27 @@ static gboolean update_transfer_functions_ ## DTYPE(complex DTYPE *autocorrelati
 			 */ \
 			sinc0 += k_stop * sinc_taps_per_input; \
 			input0 += (k_stop - 2) * elements_per_freq; \
-			k_stop = (sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input; \
+			k_stop = (sinc_taps_per_input - 1 + sinc_length - sinc0) / sinc_taps_per_input; \
 			for(k = 0; k < k_stop; k++) \
 				z += sinc_table[sinc0 + k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 - k * elements_per_freq]); \
 			/*
 			 * Now, go back and apply the sinc filter to the lower frequencies. We could hit the edge
 			 * of the sinc table or the DC component of the transfer function.
 			 */ \
-			sinc0 = 1 + (sinc_taps_per_input + i * sinc_taps_per_output - 1) % sinc_taps_per_input; \
+			sinc0 = sinc_length / 2 + (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input - sinc_taps_per_input; \
 			input0 = j + (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1) * elements_per_freq; \
-			k_stop = minimum64((sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input, (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1)); \
+			k_stop = minimum64((sinc0 + sinc_taps_per_input) / sinc_taps_per_input, (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1)); \
 			for(k = 0; k < k_stop; k++) \
-				z += sinc_table[sinc0 + k * sinc_taps_per_input] * autocorrelation_matrix[input0 - k * elements_per_freq]; \
+				z += sinc_table[sinc0 - k * sinc_taps_per_input] * autocorrelation_matrix[input0 - k * elements_per_freq]; \
 			/*
 			 * If we hit the DC component of the transfer function but not the edge of the 
 			 * sinc table, turn around and keep going until we hit the edge of the sinc table.
 			 */ \
-			sinc0 += k_stop * sinc_taps_per_input; \
+			sinc0 -= k_stop * sinc_taps_per_input; \
 			input0 = j; \
-			k_stop = (sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input; \
+			k_stop = (sinc0 + sinc_taps_per_input) / sinc_taps_per_input; \
 			for(k = 0; k < k_stop; k++) \
-				z += sinc_table[sinc0 + k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 + k * elements_per_freq]); \
+				z += sinc_table[sinc0 - k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 + k * elements_per_freq]); \
  \
 			/* Set an element of the GSL vector transfer_functions_at_f */ \
 			gsl_vector_complex_set(transfer_functions_at_f, j, gsl_complex_rect(creal((complex double) z) / num_avg, cimag((complex double) z) / num_avg)); \
@@ -447,9 +447,9 @@ static gboolean update_transfer_functions_ ## DTYPE(complex DTYPE *autocorrelati
 			 * First, apply the sinc filter to higher frequencies. We could hit the edge
 			 * of the sinc table or the Nyquist frequency of the transfer function.
 			 */ \
-			sinc0 = (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input; \
+			sinc0 = sinc_length / 2 + (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input; \
 			input0 = j + num_tfs + (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1) * elements_per_freq; \
-			k_stop = minimum64((sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input, fd_fft_length - (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1)); \
+			k_stop = minimum64((sinc_taps_per_input - 1 + sinc_length - sinc0) / sinc_taps_per_input, fd_fft_length - (i * (fd_fft_length - 1) + fd_tf_length - 2) / (fd_tf_length - 1)); \
 			for(k = 0; k < k_stop; k++) \
 				z += sinc_table[sinc0 + k * sinc_taps_per_input] * autocorrelation_matrix[input0 + k * elements_per_freq]; \
 			/*
@@ -458,27 +458,27 @@ static gboolean update_transfer_functions_ ## DTYPE(complex DTYPE *autocorrelati
 			 */ \
 			sinc0 += k_stop * sinc_taps_per_input; \
 			input0 += (k_stop - 2) * elements_per_freq; \
-			k_stop = (sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input; \
+			k_stop = (sinc_taps_per_input - 1 + sinc_length - sinc0) / sinc_taps_per_input; \
 			for(k = 0; k < k_stop; k++) \
 				z += sinc_table[sinc0 + k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 - k * elements_per_freq]); \
 			/*
 			 * Now, go back and apply the sinc filter to the lower frequencies. We could hit the edge
 			 * of the sinc table or the DC component of the transfer function.
 			 */ \
-			sinc0 = 1 + (sinc_taps_per_input + i * sinc_taps_per_output - 1) % sinc_taps_per_input; \
+			sinc0 = sinc_length / 2 + (sinc_taps_per_input - i * sinc_taps_per_output % sinc_taps_per_input) % sinc_taps_per_input - sinc_taps_per_input; \
 			input0 = j + num_tfs + (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1) * elements_per_freq; \
-			k_stop = minimum64((sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input, (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1)); \
+			k_stop = minimum64((sinc0 + sinc_taps_per_input) / sinc_taps_per_input, (i * (fd_fft_length - 1) - 1) / (fd_tf_length - 1)); \
 			for(k = 0; k < k_stop; k++) \
-				z += sinc_table[sinc0 + k * sinc_taps_per_input] * autocorrelation_matrix[input0 - k * elements_per_freq]; \
+				z += sinc_table[sinc0 - k * sinc_taps_per_input] * autocorrelation_matrix[input0 - k * elements_per_freq]; \
 			/*
 			 * If we hit the DC component of the transfer function but not the edge of the 
 			 * sinc table, turn around and keep going until we hit the edge of the sinc table.
 			 */ \
-			sinc0 += k_stop * sinc_taps_per_input; \
+			sinc0 -= k_stop * sinc_taps_per_input; \
 			input0 = j + num_tfs; \
-			k_stop = (sinc_taps_per_input + sinc_length / 2 - sinc0) / sinc_taps_per_input; \
+			k_stop = (sinc0 + sinc_taps_per_input) / sinc_taps_per_input; \
 			for(k = 0; k < k_stop; k++) \
-				z += sinc_table[sinc0 + k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 + k * elements_per_freq]); \
+				z += sinc_table[sinc0 - k * sinc_taps_per_input] * conj ## F_OR_BLANK(autocorrelation_matrix[input0 + k * elements_per_freq]); \
  \
 			/* Set an element of the GSL matrix autocorrelation_matrix_at_f */ \
 			gsl_matrix_complex_set(autocorrelation_matrix_at_f, j / num_tfs, j % num_tfs, gsl_complex_rect(creal((complex double) z) / num_avg, cimag((complex double) z) / num_avg)); \
@@ -566,7 +566,10 @@ static gboolean find_transfer_functions_ ## DTYPE(GSTLALTransferFunction *elemen
 	src_size /= element->unit_size; \
  \
 	gint64 i, j, k, m, num_ffts, num_ffts_in_avg_if_nogap, k_start, k_stop, first_index, first_index2, fd_fft_length, fd_tf_length, stride, num_tfs, notch_start, notch_end; \
-	fd_fft_length = element->fft_length / 2 + 1; \
+	if(element->df <= (double) element->rate / element->fft_length) \
+		fd_fft_length = element->fft_length / 2 + 1; \
+	else \
+		fd_fft_length = (gint64) (element->b / 2 / element->a + 1); \
 	fd_tf_length = element->fir_length / 2 + 1; \
 	stride = element->fft_length - element->fft_overlap; \
 	num_tfs = element->channels - 1; \
@@ -600,7 +603,17 @@ static gboolean find_transfer_functions_ ## DTYPE(GSTLALTransferFunction *elemen
 				real_fft[k] = element->workspace.w ## S_OR_D ## pf.fft_window[k] * src[j + element->channels * (k - k_start)]; \
  \
 			/* Take an FFT */ \
-			if(element->use_fir_fft) { \
+			if(element->df > (double) element->rate / element->fft_length) { \
+				/* Use a sparse FFT algorithm to process a "comb" */ \
+				firfft = gstlal_rfft_comb_ ## DTYPE(real_fft, (guint) element->fft_length, element->a, element->b, (DTYPE) element->f0, FALSE, NULL); \
+				/* Copy FFT to the proper location */ \
+				first_index = j * fd_fft_length; \
+				for(k = 0; k < fd_fft_length; k++) \
+					element->workspace.w ## S_OR_D ## pf.ffts[first_index + k] = firfft[k]; \
+ \
+				g_free(firfft); \
+ \
+			} else if(element->use_fir_fft) { \
 				firfft = gstlal_rfft_ ## DTYPE(real_fft, (guint) element->fft_length, NULL, 0, NULL, FALSE, 0, NULL, 0, NULL, NULL, NULL, FALSE); \
  \
 				/* Copy FFT to the proper location */ \
@@ -735,7 +748,17 @@ static gboolean find_transfer_functions_ ## DTYPE(GSTLALTransferFunction *elemen
 				real_fft[k] = element->workspace.w ## S_OR_D ## pf.fft_window[k] * ptr[first_index + k * element->channels]; \
  \
 			/* Take an FFT */ \
-			if(element->use_fir_fft) { \
+			if(element->df > (double) element->rate / element->fft_length) { \
+				/* Use a sparse FFT algorithm to process a "comb" */ \
+				firfft = gstlal_rfft_comb_ ## DTYPE(real_fft, (guint) element->fft_length, element->a, element->b, (DTYPE) element->f0, FALSE, NULL); \
+				/* Copy FFT to the proper location */ \
+				first_index = j * fd_fft_length; \
+				for(k = 0; k < fd_fft_length; k++) \
+					element->workspace.w ## S_OR_D ## pf.ffts[first_index + k] = firfft[k]; \
+ \
+				g_free(firfft); \
+ \
+			} else if(element->use_fir_fft) { \
 				firfft = gstlal_rfft_ ## DTYPE(real_fft, (guint) element->fft_length, NULL, 0, NULL, FALSE, 0, NULL, 0, NULL, NULL, NULL, FALSE); \
  \
 				/* Copy FFT to the proper location */ \
@@ -927,8 +950,11 @@ static gboolean find_transfer_functions_ ## DTYPE(GSTLALTransferFunction *elemen
 			/* Let other elements know about the update */ \
 			g_object_notify_by_pspec(G_OBJECT(element), properties[ARG_TRANSFER_FUNCTIONS]); \
 			/* Write transfer functions to the screen or a file if we want */ \
-			if(element->write_to_screen || element->filename) \
-				write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->rate / 2.0 / (fd_tf_length - 1.0), fd_tf_length, num_tfs, element->t_start_tf, element->t_start_tf + (double) (num_ffts_in_avg_if_nogap * stride + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE); \
+			if(element->write_to_screen || element->filename) { \
+				gint64 rows = fd_tf_length; \
+				double df = element->rate / 2.0 / (fd_tf_length - 1.0); \
+				write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->make_fir_filters ? 0 : element->f0 * element->df, df, rows, num_tfs, element->t_start_tf, element->t_start_tf + (double) (num_ffts_in_avg_if_nogap * stride + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE); \
+			} \
 			/* If this is this first transfer function after a gap, we may wish to store it */ \
 			if(element->use_first_after_gap && !element->num_tfs_since_gap) { \
 				for(i = 0; i < num_tfs * fd_tf_length; i++) \
@@ -1031,8 +1057,8 @@ static gboolean start(GstBaseSink *sink) {
 		element->fft_overlap = element->fft_length - 1;
 	}
 	if(element->fir_length % 2) {
-		GST_WARNING_OBJECT(element, "The chosen fir-length must be even. Adding 1 to make it even.");
-		element->fir_length += 1;
+		GST_WARNING_OBJECT(element, "The chosen fir-length must be even. Removing 1 sample to make it even.");
+		element->fir_length -= 1;
 	}
 	if(element->make_fir_filters && element->frequency_resolution < (double) element->rate / element->fir_length)
 		GST_WARNING_OBJECT(element, "The specified frequency resolution is finer than 1/fir_length, which cannot be achieved. The actual frequency resolution will be reset to 1/MIN(fft_length, fir_length).");
@@ -1064,9 +1090,13 @@ static gboolean event(GstBaseSink *sink, GstEvent *event) {
 		if(element->data_type == GSTLAL_TRANSFERFUNCTION_F32) {
 			if(element->workspace.wspf.num_ffts_in_avg - element->workspace.wspf.num_ffts_dropped > element->min_ffts && !element->computed_full_tfs) {
 				/* Then we should use the transfer functions we have now, since they won't get any better */
-				gint64 fd_fft_length = element->fft_length / 2 + 1;
-				gint64 fd_tf_length = element->fir_length / 2 + 1;
-				gint64 num_tfs = element->channels - 1;
+				gint64 fd_fft_length, fd_tf_length, num_tfs;
+				if(element->df <= (double) element->rate / element->fft_length)
+					fd_fft_length = element->fft_length / 2 + 1;
+				else
+					fd_fft_length = (gint64) (element->b / 2 / element->a + 1);
+				fd_tf_length = element->fir_length / 2 + 1;
+				num_tfs = element->channels - 1;
 				if(element->use_median) {
 					/* Then we still need to fill the autocorrelation matrix with median values */
 					gint64 median_length = element->num_ffts / 2 + 1;
@@ -1119,7 +1149,7 @@ static gboolean event(GstBaseSink *sink, GstEvent *event) {
 					g_object_notify_by_pspec(G_OBJECT(element), properties[ARG_TRANSFER_FUNCTIONS]);
 					/* Write transfer functions to the screen or a file if we want */
 					if(element->write_to_screen || element->filename)
-						write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->rate / 2.0 / (fd_tf_length - 1.0), fd_tf_length, num_tfs, element->t_start_tf, element->t_start_tf + (double) (element->workspace.wspf.num_ffts_in_avg * (element->fft_length - element->fft_overlap) + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE);
+						write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->make_fir_filters ? 0 : element->f0 * element->df, element->rate / 2.0 / (fd_tf_length - 1.0), fd_tf_length, num_tfs, element->t_start_tf, element->t_start_tf + (double) (element->workspace.wspf.num_ffts_in_avg * (element->fft_length - element->fft_overlap) + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE);
 				} else
 					GST_WARNING_OBJECT(element, "Transfer function(s) computation failed. No transfer functions will be produced.");
 				/* Update FIR filters if we want */
@@ -1139,9 +1169,13 @@ static gboolean event(GstBaseSink *sink, GstEvent *event) {
 		} else {
 			if(element->workspace.wdpf.num_ffts_in_avg - element->workspace.wdpf.num_ffts_dropped > element->min_ffts && !element->computed_full_tfs) {
 				/* Then we should use the transfer functions we have now, since they won't get any better */
-				gint64 fd_fft_length = element->fft_length / 2 + 1;
-				gint64 fd_tf_length = element->fir_length / 2 + 1;
-				gint64 num_tfs = element->channels - 1;
+				gint64 fd_fft_length, fd_tf_length, num_tfs;
+				if(element->df <= (double) element->rate / element->fft_length) 
+					fd_fft_length = element->fft_length / 2 + 1; 
+				else 
+					fd_fft_length = (gint64) (element->b / 2 / element->a + 1); 
+				fd_tf_length = element->fir_length / 2 + 1;
+				num_tfs = element->channels - 1;
 				if(element->use_median) {
 					/* Then we still need to fill the autocorrelation matrix with median values */
 					gint64 median_length = element->num_ffts / 2 + 1;
@@ -1194,7 +1228,7 @@ static gboolean event(GstBaseSink *sink, GstEvent *event) {
 					g_object_notify_by_pspec(G_OBJECT(element), properties[ARG_TRANSFER_FUNCTIONS]);
 					/* Write transfer functions to the screen or a file if we want */
 					if(element->write_to_screen || element->filename)
-						write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->rate / 2.0 / (fd_tf_length - 1.0), fd_tf_length, num_tfs, element->t_start_tf, element->t_start_tf + (double) (element->workspace.wdpf.num_ffts_in_avg * (element->fft_length - element->fft_overlap) + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE);
+						write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->make_fir_filters ? 0 : element->f0 * element->df, element->rate / 2.0 / (fd_tf_length - 1.0), fd_tf_length, num_tfs, element->t_start_tf, element->t_start_tf + (double) (element->workspace.wdpf.num_ffts_in_avg * (element->fft_length - element->fft_overlap) + element->fft_overlap) / element->rate, element->write_to_screen, element->filename, TRUE);
 				} else
 					GST_WARNING_OBJECT(element, "Transfer function(s) computation failed. No transfer functions will be produced.");
 				/* Update FIR filters if we want */
@@ -1427,13 +1461,52 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 		goto done;
 	}
 
+	/*
+	 * We can compute a "sparse" transfer function if the desired frequency spacing of the
+	 * FFTs is greater than 1 / fft_length.  This is useful for measuring comb injections.
+	 */
+
+	gint64 fd_fft_length;
+
+	if(element->df <= (double) element->rate / element->fft_length) {
+		element->df = (double) element->rate / element->fft_length;
+		fd_fft_length = element->fft_length / 2 + 1;
+		element->f0 = 0.0;
+	} else {
+		/* Find integers a and b such that a/b = element->df / element->rate */
+		guint a, best_a;
+		double b, best_b, error;
+		best_a = a = 1;
+		best_b = b = a * element->rate / element->df;
+		error = fabs((int) (b + 0.5) - b);
+		while(error > 1e-10 && a < 1000) {
+			a += 1;
+			b = a * element->rate / element->df;
+			if(fabs((int) (b + 0.5) - b) < error) {
+				best_a = a;
+				best_b = b;
+				error = fabs((int) (b + 0.5) - b);
+			}
+			a++;
+		}
+		element->a = best_a;
+		element->b = (guint) best_b;
+		element->df = best_a * element->rate / best_b;
+		/* Unit conversion of f0 into units of df */
+		element->f0 /= element->df;
+		element->f0 -= (int) element->f0;
+		if(element->f0 < 0)
+			element->f0 += 1; \
+
+		fd_fft_length = (gint64) (element->b / 2 / element->a + 1); \
+	}
+
 	/*
 	 * Allocate any memory that depends on stream parameters
 	 */
 
-	gint64 fd_fft_length = element->fft_length / 2 + 1;
 	if(!element->fir_length)
-		element->fir_length = element->fft_length;
+		element->fir_length = 2 * fd_fft_length - 2;
 	gint64 fd_fir_length = element->fir_length / 2 + 1;
 	element->transfer_functions = g_malloc((element->channels - 1) * fd_fir_length * sizeof(*element->transfer_functions));
 	memset(element->transfer_functions, 0, (element->channels - 1) * fd_fir_length * sizeof(*element->transfer_functions));
@@ -1513,7 +1586,7 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 			 * Make a sinc table to resample and/or low-pass filter the transfer functions when we make FIR filters
 			 */
 
-			if(element->fir_length == element->fft_length) {
+			if(fd_fir_length == fd_fft_length && !(element->f0 && element->make_fir_filters)) {
 				element->workspace.wspf.sinc_length = 1;
 				element->workspace.wspf.sinc_table = g_malloc(sizeof(*element->workspace.wspf.sinc_table));
 				*element->workspace.wspf.sinc_table = 1.0;
@@ -1530,47 +1603,41 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 				while(common_denominator % short_length)
 					common_denominator += long_length;
 				element->workspace.wspf.sinc_taps_per_df = common_denominator / long_length;
+				/* Number of taps per frequency bin of sinc table at fft frequency resolution */
+				gint64 taps_per_fftdf = common_denominator / (fd_fft_length - 1);
 				/* taps_per_osc is the number of taps per half-oscillation in the sinc table */
-				gint64 taps_per_osc = element->workspace.wspf.sinc_taps_per_df * (gint64) (maximum(maximum(element->frequency_resolution * element->fir_length / element->rate, element->frequency_resolution * element->fft_length / element->rate), maximum((double) element->fft_length / element->fir_length, (double) element->fir_length / element->fft_length)) + 0.5);
+				gint64 taps_per_osc = element->workspace.wspf.sinc_taps_per_df * (gint64) (maximum(maximum(element->frequency_resolution * element->fir_length / element->rate, element->frequency_resolution * element->fft_length / element->rate), maximum((double) (fd_fft_length - 1) / (fd_fir_length - 1), (double) (fd_fir_length - 1) / (fd_fft_length - 1))) + 0.5);
 				element->workspace.wspf.sinc_length = minimum64(element->workspace.wspf.sinc_taps_per_df * maximum64(fd_fir_length / 2, fd_fft_length / 2) - 1, 1 + SINC_LENGTH * taps_per_osc);
+				element->workspace.wspf.sinc_length += (1 + element->workspace.wspf.sinc_length) % 2;
 
-				/* To save memory, we use symmetry and record only half of the sinc table */
-				element->workspace.wspf.sinc_table = g_malloc((1 + element->workspace.wspf.sinc_length / 2) * sizeof(*element->workspace.wspf.sinc_table));
-				*element->workspace.wspf.sinc_table = 1.0;
+				element->workspace.wspf.sinc_table = g_malloc(element->workspace.wspf.sinc_length * sizeof(*element->workspace.wspf.sinc_table));
 				gint64 j;
-				float sin_arg, normalization;
+				float sin_arg_up, sin_arg_down, normalization;
+				sin_arg_up = sin_arg_down = element->f0 * M_PI * taps_per_fftdf / taps_per_osc;
+				if(sin_arg_up)
+					element->workspace.wspf.sinc_table[element->workspace.wspf.sinc_length / 2] = sinf(sin_arg_up) / sin_arg_up;
+				else
+					element->workspace.wspf.sinc_table[element->workspace.wspf.sinc_length / 2] = 1.0;
 				for(i = 1; i <= element->workspace.wspf.sinc_length / 2; i++) {
-					sin_arg = M_PI * i / taps_per_osc;
-					element->workspace.wspf.sinc_table[i] = sinf(sin_arg) / sin_arg;
+					sin_arg_up += M_PI / taps_per_osc;
+					sin_arg_down -= M_PI / taps_per_osc;
+					element->workspace.wspf.sinc_table[element->workspace.wspf.sinc_length / 2 + i] = sinf(sin_arg_up) / sin_arg_up;
+					element->workspace.wspf.sinc_table[element->workspace.wspf.sinc_length / 2 - i] = sinf(sin_arg_down) / sin_arg_down;
 				}
 
 				/* Window the sinc table */
-				kaiser_float(element->workspace.wspf.sinc_length, 10.0, element->workspace.wspf.sinc_table, TRUE);
+				kaiser_float(element->workspace.wspf.sinc_length, 10.0, element->workspace.wspf.sinc_table, FALSE);
 
 				/* 
 				 * Normalize the sinc table to make the DC gain exactly 1. We need to account for the fact 
 				 * that the density of taps in the filter could be higher than the density of input samples.
 				 */
 				gint64 taps_per_input = fd_fir_length > fd_fft_length ? common_denominator / short_length : element->workspace.wspf.sinc_taps_per_df;
-				for(i = 0; i < (taps_per_input + 1) / 2; i++) {
+				for(i = 0; i < taps_per_input; i++) {
 					normalization = 0.0;
-					for(j = i; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
-						normalization += element->workspace.wspf.sinc_table[j];
-					for(j = taps_per_input - i; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
+					for(j = i; j < element->workspace.wspf.sinc_length; j += taps_per_input)
 						normalization += element->workspace.wspf.sinc_table[j];
-					for(j = i; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
-						element->workspace.wspf.sinc_table[j] /= normalization;
-					if(i) {
-						for(j = taps_per_input - i; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
-							element->workspace.wspf.sinc_table[j] /= normalization;
-					}
-				}
-				/* If taps_per_input is even, we need to account for one more normalization without "over-normalizing." */
-				if(!((taps_per_input) % 2)) {
-					normalization = 0.0;
-					for(j = taps_per_input / 2; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
-						normalization += 2 * element->workspace.wspf.sinc_table[j];
-					for(j = taps_per_input / 2; j <= element->workspace.wspf.sinc_length / 2; j += taps_per_input)
+					for(j = i; j < element->workspace.wspf.sinc_length; j += taps_per_input)
 						element->workspace.wspf.sinc_table[j] /= normalization;
 				}
 			}
@@ -1683,7 +1750,7 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 		GST_LOG_OBJECT(element, "starting FFTWF planning");
 
 		/* data that will be Fourier transformed into frequency domain */
-		element->workspace.wspf.fft = (complex float *) fftwf_malloc(fd_fft_length * sizeof(*element->workspace.wspf.fft));
+		element->workspace.wspf.fft = (complex float *) fftwf_malloc((element->fft_length / 2 + 1) * sizeof(*element->workspace.wspf.fft));
 		element->workspace.wspf.plan = fftwf_plan_dft_r2c_1d(element->fft_length, (float *) element->workspace.wspf.fft, element->workspace.wspf.fft, FFTW_ESTIMATE);
 
 		if(element->make_fir_filters && !element->workspace.wspf.fir_filter) {
@@ -1738,7 +1805,7 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 			 * Make a sinc table to resample and/or low-pass filter the transfer functions when we make FIR filters
 			 */
 
-			if(element->fir_length == element->fft_length) {
+			if(fd_fir_length == fd_fft_length && !(element->f0 && element->make_fir_filters)) {
 				element->workspace.wdpf.sinc_length = 1;
 				element->workspace.wdpf.sinc_table = g_malloc(sizeof(*element->workspace.wdpf.sinc_table));
 				*element->workspace.wdpf.sinc_table = 1.0;
@@ -1755,47 +1822,41 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 				while(common_denominator % short_length)
 					common_denominator += long_length;
 				element->workspace.wdpf.sinc_taps_per_df = common_denominator / long_length;
+				/* Number of taps per frequency bin of sinc table at fft frequency resolution */
+				gint64 taps_per_fftdf = common_denominator / (fd_fft_length - 1);
 				/* taps_per_osc is the number of taps per half-oscillation in the sinc table */
-				gint64 taps_per_osc = element->workspace.wdpf.sinc_taps_per_df * (gint64) (maximum(maximum(element->frequency_resolution * element->fir_length / element->rate, element->frequency_resolution * element->fft_length / element->rate), maximum((double) element->fft_length / element->fir_length, (double) element->fir_length / element->fft_length)) + 0.5);
+				gint64 taps_per_osc = element->workspace.wdpf.sinc_taps_per_df * (gint64) (maximum(maximum(element->frequency_resolution * element->fir_length / element->rate, element->frequency_resolution * element->fft_length / element->rate), maximum((double) (fd_fft_length - 1) / (fd_fir_length - 1), (double) (fd_fir_length - 1) / (fd_fft_length - 1))) + 0.5);
 				element->workspace.wdpf.sinc_length = minimum64(element->workspace.wdpf.sinc_taps_per_df * maximum64(fd_fir_length / 2, fd_fft_length / 2) - 1, 1 + SINC_LENGTH * taps_per_osc);
+				element->workspace.wdpf.sinc_length += (1 + element->workspace.wdpf.sinc_length) % 2;
 
-				/* To save memory, we use symmetry and record only half of the sinc table */
-				element->workspace.wdpf.sinc_table = g_malloc((1 + element->workspace.wdpf.sinc_length / 2) * sizeof(*element->workspace.wdpf.sinc_table));
-				*element->workspace.wdpf.sinc_table = 1.0;
+				element->workspace.wdpf.sinc_table = g_malloc(element->workspace.wdpf.sinc_length * sizeof(*element->workspace.wdpf.sinc_table));
 				gint64 j;
-				double sin_arg, normalization;
+				double sin_arg_up, sin_arg_down, normalization;
+				sin_arg_up = sin_arg_down = element->f0 * M_PI * taps_per_fftdf / taps_per_osc;
+				if(sin_arg_up)
+					element->workspace.wdpf.sinc_table[element->workspace.wdpf.sinc_length / 2] = sinf(sin_arg_up) / sin_arg_up;
+				else
+					element->workspace.wdpf.sinc_table[element->workspace.wdpf.sinc_length / 2] = 1.0;
 				for(i = 1; i <= element->workspace.wdpf.sinc_length / 2; i++) {
-					sin_arg = M_PI * i / taps_per_osc;
-					element->workspace.wdpf.sinc_table[i] = sin(sin_arg) / sin_arg;
+					sin_arg_up += M_PI / taps_per_osc;
+					sin_arg_down -= M_PI / taps_per_osc;
+					element->workspace.wdpf.sinc_table[element->workspace.wdpf.sinc_length / 2 + i] = sin(sin_arg_up) / sin_arg_up;
+					element->workspace.wdpf.sinc_table[element->workspace.wdpf.sinc_length / 2 - i] = sin(sin_arg_down) / sin_arg_down;
 				}
 
 				/* Window the sinc table */
-				kaiser_double(element->workspace.wdpf.sinc_length, 10.0, element->workspace.wdpf.sinc_table, TRUE);
+				kaiser_double(element->workspace.wdpf.sinc_length, 10.0, element->workspace.wdpf.sinc_table, FALSE);
 
 				/* 
 				 * Normalize the sinc table to make the DC gain exactly 1. We need to account for the fact 
 				 * that the density of taps in the filter could be higher than the density of input samples.
 				 */
 				gint64 taps_per_input = fd_fir_length > fd_fft_length ? common_denominator / short_length : element->workspace.wdpf.sinc_taps_per_df;
-				for(i = 0; i < (taps_per_input + 1) / 2; i++) {
+				for(i = 0; i < taps_per_input; i++) {
 					normalization = 0.0;
-					for(j = i; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
-						normalization += element->workspace.wdpf.sinc_table[j];
-					for(j = taps_per_input - i; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
+					for(j = i; j < element->workspace.wdpf.sinc_length; j += taps_per_input)
 						normalization += element->workspace.wdpf.sinc_table[j];
-					for(j = i; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
-						element->workspace.wdpf.sinc_table[j] /= normalization;
-					if(i) {
-						for(j = taps_per_input - i; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
-							element->workspace.wdpf.sinc_table[j] /= normalization;
-					}
-				}
-				/* If taps_per_input is even, we need to account for one more normalization without "over-normalizing." */
-				if(!((taps_per_input) % 2)) {
-					normalization = 0.0;
-					for(j = taps_per_input / 2; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
-						normalization += 2 * element->workspace.wdpf.sinc_table[j];
-					for(j = taps_per_input / 2; j <= element->workspace.wdpf.sinc_length / 2; j += taps_per_input)
+					for(j = i; j < element->workspace.wdpf.sinc_length; j += taps_per_input)
 						element->workspace.wdpf.sinc_table[j] /= normalization;
 				}
 			}
@@ -1908,7 +1969,7 @@ static gboolean set_caps(GstBaseSink *sink, GstCaps *caps) {
 		GST_LOG_OBJECT(element, "starting FFTW planning");
 
 		/* data that will be Fourier transformed into frequency domain */
-		element->workspace.wdpf.fft = (complex double *) fftw_malloc(fd_fft_length * sizeof(*element->workspace.wdpf.fft));
+		element->workspace.wdpf.fft = (complex double *) fftw_malloc((element->fft_length / 2 + 1) * sizeof(*element->workspace.wdpf.fft));
 		element->workspace.wdpf.plan = fftw_plan_dft_r2c_1d(element->fft_length, (double *) element->workspace.wdpf.fft, element->workspace.wdpf.fft, FFTW_ESTIMATE);
 
 		if(element->make_fir_filters && !element->workspace.wdpf.fir_filter) {
@@ -1995,7 +2056,7 @@ static GstFlowReturn render(GstBaseSink *sink, GstBuffer *buffer) {
 			g_object_notify_by_pspec(G_OBJECT(element), properties[ARG_TRANSFER_FUNCTIONS]);
 			/* Write transfer functions to the screen or a file if we want */
 			if(element->write_to_screen || element->filename)
-				write_transfer_functions(element->transfer_functions, gst_element_get_name(element), (double) element->rate / element->fir_length, element->fir_length / 2 + 1, element->channels - 1, 0, 0, element->write_to_screen, element->filename, TRUE);
+				write_transfer_functions(element->transfer_functions, gst_element_get_name(element), element->make_fir_filters ? 0 : element->f0 * element->df, (double) element->rate / element->fir_length, element->fir_length / 2 + 1, element->channels - 1, 0, 0, element->write_to_screen, element->filename, TRUE);
 			if(element->make_fir_filters) {
 				for(i = 0; i < (element->channels - 1) * element->fir_length; i++)
 					element->fir_filters[i] = element->post_gap_fir_filters[i];
@@ -2060,11 +2121,16 @@ static GstFlowReturn render(GstBaseSink *sink, GstBuffer *buffer) {
 
 	/* Check whether we need to do anything with this data */
 	if(element->sample_count > element->update_samples && mapinfo.size) {
+		gint64 fd_fft_length;
+		if(element->df <= (double) element->rate / element->fft_length) \
+			fd_fft_length = element->fft_length / 2 + 1; \
+		else \
+			fd_fft_length = (gint64) (element->b / 2 / element->a + 1); \
 		gboolean success;
 		if(element->data_type == GSTLAL_TRANSFERFUNCTION_F32) {
 			/* If we are just beginning to compute new transfer functions with this data, initialize memory that we will fill to zero */
 			if(!element->workspace.wspf.num_ffts_in_avg) {
-				memset(element->workspace.wspf.autocorrelation_matrix, 0, element->channels * (element->channels - 1) * (element->fft_length / 2 + 1) * sizeof(*element->workspace.wspf.autocorrelation_matrix));
+				memset(element->workspace.wspf.autocorrelation_matrix, 0, element->channels * (element->channels - 1) * fd_fft_length * sizeof(*element->workspace.wspf.autocorrelation_matrix));
 				element->t_start_tf = (double) (gst_util_uint64_scale_int_round(GST_BUFFER_PTS(buffer) + GST_BUFFER_DURATION(buffer), element->rate, GST_SECOND) - element->sample_count + element->update_samples) / element->rate;
 			}
 			/* Send the data to a function to compute fft's and transfer functions */
@@ -2072,7 +2138,7 @@ static GstFlowReturn render(GstBaseSink *sink, GstBuffer *buffer) {
 		} else {
 			/* If we are just beginning to compute new transfer functions with this data, initialize memory that we will fill to zero */
 			if(!element->workspace.wdpf.num_ffts_in_avg) {
-				memset(element->workspace.wdpf.autocorrelation_matrix, 0, element->channels * (element->channels - 1) * (element->fft_length / 2 + 1) * sizeof(*element->workspace.wdpf.autocorrelation_matrix));
+				memset(element->workspace.wdpf.autocorrelation_matrix, 0, element->channels * (element->channels - 1) * fd_fft_length * sizeof(*element->workspace.wdpf.autocorrelation_matrix));
 				element->t_start_tf = (double) (gst_util_uint64_scale_int_round(GST_BUFFER_PTS(buffer) + GST_BUFFER_DURATION(buffer), element->rate, GST_SECOND) - element->sample_count + element->update_samples) / element->rate;
 			}
 			/* Send the data to a function to compute fft's and transfer functions */
@@ -3032,6 +3098,8 @@ static void gstlal_transferfunction_init(GSTLALTransferFunction *element) {
 	element->gap_samples = 0;
 	element->num_tfs_since_gap = 0;
 	element->computed_full_tfs = FALSE;
+	element->a = 0;
+	element->b = 0;
 
 	gst_base_sink_set_sync(GST_BASE_SINK(element), FALSE);
 	gst_base_sink_set_async_enabled(GST_BASE_SINK(element), FALSE);
diff --git a/gstlal-calibration/gst/lal/gstlal_transferfunction.h b/gstlal-calibration/gst/lal/gstlal_transferfunction.h
index d3c62dd867..cb9531e2dd 100644
--- a/gstlal-calibration/gst/lal/gstlal_transferfunction.h
+++ b/gstlal-calibration/gst/lal/gstlal_transferfunction.h
@@ -202,6 +202,8 @@ struct _GSTLALTransferFunction {
 	gboolean use_fir_fft;
 	double df;
 	double f0;
+	guint a;
+	guint b;
 };
 
 
diff --git a/gstlal-calibration/lib/gstlal_firtools.c b/gstlal-calibration/lib/gstlal_firtools.c
index b610eb7cf6..0195aa8911 100644
--- a/gstlal-calibration/lib/gstlal_firtools.c
+++ b/gstlal-calibration/lib/gstlal_firtools.c
@@ -1139,6 +1139,8 @@ GSTLAL_IRFFT(long, double, l, );
 LONG complex DTYPE *gstlal_rfft_comb_ ## LONG ## DTYPE(LONG DTYPE *td_data, guint N, guint a, guint b, LONG DTYPE f0, gboolean free_input, guint *N_out) { \
  \
 	guint factor, N_blocks, *prime_factors, num_factors, i, j, M, M_min, M_num_factors, *M_prime_factors, M_out, M_in, rfft_N_out; \
+	if(N_out == NULL) \
+		N_out = g_malloc(sizeof(guint)); \
 	gboolean M_prime_factors_need_freed, exp_array_need_freed; \
 	long complex double *rot_array, *rot_td_data, *M_exp_array, *M_exp_array2, *exp_array, *long_fd_data, *mini_fd_data, *fd_data; \
 	LONG DTYPE rot_pow; \
@@ -1192,9 +1194,9 @@ LONG complex DTYPE *gstlal_rfft_comb_ ## LONG ## DTYPE(LONG DTYPE *td_data, guin
 	/* Find prime factors */ \
 	prime_factors = find_prime_factors(b, &num_factors); \
  \
+	rfft_N_out = b / 2 + 1; \
+	*N_out = (rfft_N_out + a - 1) / a; \
 	if(f0 > 0) { \
-		rfft_N_out = b / 2; \
-		*N_out = (guint) (rfft_N_out + a - 1 - f0 * a) / a; \
 		/* Apply phase rotation to shift the frequencies being measured. */ \
 		rot_array = find_exp_array(N, FALSE); \
 		rot_td_data = g_malloc(N * sizeof(long complex double)); \
@@ -1267,9 +1269,6 @@ LONG complex DTYPE *gstlal_rfft_comb_ ## LONG ## DTYPE(LONG DTYPE *td_data, guin
 		} \
  \
 	} else { \
-		rfft_N_out = b / 2 + 1; \
-		*N_out = (rfft_N_out + a - 1) / a; \
- \
 		/* Check if we will need to use gstlal_prime_rfft_() for this */ \
 		if(prime_factors[num_factors - 2] >= 607) { \
 			/* Find the first member greater than or equal to 607 */ \
diff --git a/gstlal-calibration/tests/check_calibration/Makefile b/gstlal-calibration/tests/check_calibration/Makefile
index 3f04a6026c..bccfc16add 100644
--- a/gstlal-calibration/tests/check_calibration/Makefile
+++ b/gstlal-calibration/tests/check_calibration/Makefile
@@ -8,12 +8,13 @@ IFO = H
 # determines where to look for filters files (e.g., O1, O2, O3, ER10, ER13, ER14, PreER10, PreER13, PreER14)
 OBSRUN = O3
 
-START = $(shell echo 1269090018 | bc)
+START = $(shell echo 1269114368 | bc)
 # 1237831461 start of O3A
 # 1256655618 start of O3B
 # 1238288418
 # 1269090018
-END = $(shell echo 1269091018 | bc)
+# 1269114645-21449
+END = $(shell echo 1269136384 | bc)
 # 1253977218 end of O3A
 # 1269367218 end of O3B
 # 1238338818
@@ -48,7 +49,9 @@ DCSTESTCONFIGS = Filters/O3/GDSFilters/H1DCS_C01_1252083618_AllCorrectionsTest.i
 GDSSHMCONFIGS = Filters/O3/GDSFilters/H1GDS_1258216456_testing.ini
 GDSOLDCONFIGS = Filters/ER14/GDSFilters/L1GDS_1235491416_old.ini
 
-all: $(IFO)1_hoft_GDS_SHM_frames
+all: TDCFs_pcal2darm_plots
+# TDCFs_pcal2darm_plots
+# pcal_DCS_transfer_functions
 #approxKappasErrorPlot
 #$(IFO)1_hoft_DCS_STATSUB_frames.cache $(IFO)1_hoft_DCS_NONSTATSUB_frames.cache
 
@@ -206,7 +209,7 @@ approxKappasErrorPlot: $(IFO)1_C01_frames.cache
 #1239472998,1251057623,1256655618,1278428752
 
 TDCFs_pcal2darm_plots: $(IFO)1_easy_raw_frames.cache $(IFO)1_SCALAR_CORR_frames.cache $(IFO)1_FCC_CORR_frames.cache $(IFO)1_FCCFS_CORR_frames.cache $(IFO)1_ALL_CORR_frames.cache
-	python3 pcal2darm_timeseries.py --gps-start-time $(PLOT_START) --gps-end-time $(PLOT_END) --ifo $(IFO)1 --raw-frame-cache $(IFO)1_easy_raw_frames.cache --gstlal-frame-cache-list '$(IFO)1_SCALAR_CORR_frames.cache,$(IFO)1_FCC_CORR_frames.cache,$(IFO)1_FCCFS_CORR_frames.cache,$(IFO)1_ALL_CORR_frames.cache' --config-file '$(ALL_CORR_CONFIGS)' --pcal-channel-name CAL-PCALY_RX_PD_OUT_DQ --gstlal-channel-list 'DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN' --labels '{\rm Scalars},+f_{\rm cc},+f_{\rm s}+Q,+\tau_i' --latex-labels --pcal-time-advance 0.00006103515625 --show-stats
+	python3 pcal2darm_timeseries.py --gps-start-time $(PLOT_START) --gps-end-time $(PLOT_END) --ifo $(IFO)1 --raw-frame-cache $(IFO)1_easy_raw_frames.cache --gstlal-frame-cache-list '$(IFO)1_SCALAR_CORR_frames.cache,$(IFO)1_FCC_CORR_frames.cache,$(IFO)1_FCCFS_CORR_frames.cache,$(IFO)1_ALL_CORR_frames.cache' --config-file-list '$(SCALAR_CORR_CONFIGS),$(FCC_CORR_CONFIGS),$(FCCFS_CORR_CONFIGS),$(ALL_CORR_CONFIGS)' --pcal-channel-name CAL-PCALY_RX_PD_OUT_DQ --gstlal-channel-list 'DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN' --labels '{\rm Multipliers},+f_{\rm cc},+f_{\rm s}+Q,+\tau_i' --latex-labels --pcal-time-advance 0.00006103515625 --show-stats
 
 detect_actuation_timing_change: $(IFO)1_C01_frames.cache
 	python3 detect_kappa_change.py --gps-start-time $(START) --gps-end-time $(END) --ifo $(IFO)1 --frame-cache $(IFO)1_C01_frames.cache --statevector-channel DCS-CALIB_STATE_VECTOR_C01 filename $(IFO)_actuation_timing_changes_$(START)-$(END).txt
@@ -219,7 +222,7 @@ pcal_GDS_transfer_functions: $(IFO)1_easy_raw_frames.cache $(IFO)1_hoft_GDS_fram
 	python3 plot_transfer_function.py --gps-start-time $(PLOT_START) --gps-end-time $(PLOT_END) --ifo $(IFO)1 --denominator-frame-cache $(IFO)1_easy_raw_frames.cache --denominator-channel-name CAL-PCALY_RX_PD_OUT_DQ --denominator-correction y_arm_pcal_corr --numerator-correction arm_length --frequency-max 400 --numerator-frame-cache-list $(IFO)1_hoft_GDS_frames.cache --numerator-channel-list GDS-CALIB_STRAIN --config-file $(GDSCONFIGS) --use-median --labels GDS_NO_TDCFs
 
 pcal_DCS_transfer_functions: $(IFO)1_SCALAR_CORR_frames.cache $(IFO)1_FCC_CORR_frames.cache $(IFO)1_FCCFS_CORR_frames.cache $(IFO)1_ALL_CORR_frames.cache
-	python3 plot_transfer_function.py --gps-start-time $(PLOT_START) --gps-end-time $(PLOT_END) --ifo $(IFO)1 --denominator-frame-cache $(IFO)1_easy_raw_frames.cache --denominator-channel-name CAL-PCALY_RX_PD_OUT_DQ --denominator-correction y_arm_pcal_corr --numerator-correction arm_length --frequency-max 400 --numerator-frame-cache-list $(IFO)1_SCALAR_CORR_frames.cache,$(IFO)1_FCC_CORR_frames.cache,$(IFO)1_FCCFS_CORR_frames.cache,$(IFO)1_ALL_CORR_frames.cache --numerator-channel-list DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN --config-file $(ALL_CORR_CONFIGS) --use-median --labels '{\rm Scalars},+f_{\rm cc},+f_{\rm s}+Q,+\tau_i' --latex-labels
+	python3 plot_transfer_function.py --gps-start-time 1269021699 --gps-end-time 1269021877 --ifo $(IFO)1 --denominator-frame-cache $(IFO)1_easy_raw_frames.cache --denominator-channel-name CAL-PCALY_RX_PD_OUT_DQ --denominator-correction y_arm_pcal_corr --numerator-correction arm_length --frequency-max 400 --numerator-frame-cache-list $(IFO)1_SCALAR_CORR_frames.cache,$(IFO)1_FCC_CORR_frames.cache,$(IFO)1_FCCFS_CORR_frames.cache,$(IFO)1_ALL_CORR_frames.cache --numerator-channel-list DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN,DCS-CALIB_STRAIN --config-file $(ALL_CORR_CONFIGS) --use-median --labels '{\rm Multipliers},+f_{\rm cc},+f_{\rm s}+Q,+\tau_i' --latex-labels
 
 pcal_DCS_EXACTKAPPAS_transfer_functions: $(IFO)1_easy_raw_frames.cache $(IFO)1_hoft_DCS_APPROXKAPPAS_frames.cache $(IFO)1_hoft_DCS_EXACTKAPPAS_frames.cache $(IFO)1_C01_frames.cache
 	python3 plot_transfer_function.py --gps-start-time $(PLOT_START) --gps-end-time $(PLOT_END) --ifo $(IFO)1 --denominator-frame-cache $(IFO)1_easy_raw_frames.cache --denominator-channel-name CAL-PCALY_RX_PD_OUT_DQ --denominator-correction y_arm_pcal_corr --numerator-correction arm_length --frequency-max 400 --numerator-frame-cache-list $(IFO)1_C01_frames.cache,$(IFO)1_hoft_DCS_APPROXKAPPAS_frames.cache,$(IFO)1_hoft_DCS_EXACTKAPPAS_frames.cache --numerator-channel-list DCS-CALIB_STRAIN_C01,DCS-CALIB_STRAINAPPROXKAPPAS,DCS-CALIB_STRAINEXACTKAPPAS --config-file $(DCSEXACTKAPPASCONFIGS) --labels 'C01,Approx,Exact' --use-median
diff --git a/gstlal-calibration/tests/check_calibration/pcal2darm_timeseries.py b/gstlal-calibration/tests/check_calibration/pcal2darm_timeseries.py
index d33790cfc2..2c84195df2 100644
--- a/gstlal-calibration/tests/check_calibration/pcal2darm_timeseries.py
+++ b/gstlal-calibration/tests/check_calibration/pcal2darm_timeseries.py
@@ -331,7 +331,7 @@ else:
 		plot_labels.append("{\\rm %s}" % label.replace(':', '{:}').replace('-', '\mbox{-}').replace('_', '\_').replace(' ', '\ '))
 
 # Read data from files and plot it
-colors = ['red', 'limegreen', 'mediumblue', 'gold', 'b', 'm'] # Hopefully the user will not want to plot more than six datasets on one plot.
+colors = ['tomato', 'green', 'mediumblue', 'gold', 'b', 'm'] # Hopefully the user will not want to plot more than six datasets on one plot.
 channels = calcs_channels
 channels.extend(gstlal_channels)
 for i in range(0, len(frequencies)):
@@ -374,14 +374,14 @@ for i in range(0, len(frequencies)):
 		plt.ylabel(r'${\rm Magnitude}$')
 	magnitude_range = options.magnitude_ranges.split(';')[i]
 	ticks_and_grid(plt.gca(), ymin = float(magnitude_range.split(',')[0]), ymax = float(magnitude_range.split(',')[1]))
-	leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'upper right')
+	leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'lower right')
 	leg.get_frame().set_alpha(0.8)
 	plt.subplot(2, len(frequencies), len(frequencies) + i + 1)
 	if options.show_stats:
 		plt.plot(times[0], phases[0], colors[0], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s \ [\mu_{1/2} = %0.3f^{\circ}, \sigma = %0.3f^{\circ}]$' % (plot_labels[0], numpy.median(phases[0]), stdmed(phases[0])))
 	else:
 		plt.plot(times[0], phases[0], colors[0], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s$' % (plot_labels[0]))
-	leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'upper right')
+	leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'lower right')
 	leg.get_frame().set_alpha(0.8)
 	if i == 0:
 		plt.ylabel(r'${\rm Phase \  [deg]}$')
@@ -404,14 +404,14 @@ for i in range(0, len(frequencies)):
 			plt.plot(times[j], magnitudes[j], colors[j % 6], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s \ [\mu_{1/2} = %0.4f, \sigma = %0.4f]$' % (plot_labels[j], numpy.median(magnitudes[j]), stdmed(magnitudes[j])))
 		else:
 			plt.plot(times[j], magnitudes[j], colors[j % 6], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s$' % (plot_labels[j]))
-		leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'upper right')
+		leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'lower right')
 		leg.get_frame().set_alpha(0.8)
 		plt.subplot(2, len(frequencies), len(frequencies) + i + 1)
 		if options.show_stats:
 			plt.plot(times[j], phases[j], colors[j % 6], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s \ [\mu_{1/2} = %0.3f^{\circ}, \sigma = %0.3f^{\circ}]$' % (plot_labels[j], numpy.median(phases[j]), stdmed(phases[j])))
 		else:
 			plt.plot(times[j], phases[j], colors[j % 6], linestyle = 'None', marker = '.', markersize = markersize, label = r'$%s$' % (plot_labels[j]))
-		leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'upper right')
+		leg = plt.legend(fancybox = True, markerscale = 16.0 / markersize, numpoints = 1, loc = 'lower right')
 		leg.get_frame().set_alpha(0.8)
 plt.savefig("%s_deltal_over_pcal%s_%d-%d.png" % (ifo, options.file_name_suffix, int(t_start), int(dur_in_seconds)))
 plt.savefig("%s_deltal_over_pcal%s_%d-%d.pdf" % (ifo, options.file_name_suffix, int(t_start), int(dur_in_seconds)))
diff --git a/gstlal-calibration/tests/check_calibration/plot_transfer_function.py b/gstlal-calibration/tests/check_calibration/plot_transfer_function.py
index 7ebf8be10a..c05e1fc0d5 100644
--- a/gstlal-calibration/tests/check_calibration/plot_transfer_function.py
+++ b/gstlal-calibration/tests/check_calibration/plot_transfer_function.py
@@ -340,7 +340,7 @@ for i in range(len(real_poles)):
 colors = []
 
 # Start with defaults to use if we don't recognize what we are plotting.
-default_colors = ['tomato', 'red', 'limegreen', 'gold', 'orange', 'aqua', 'magenta', 'blue']
+default_colors = ['tomato', 'green', 'blue', 'gold', 'orange', 'aqua', 'magenta', 'blue']
 
 # Use specific colors for known version of calibration
 C02_labels = ['C02', 'c02']
-- 
GitLab