#!/usr/bin/env python3
#
# Copyright (C) 2010-2020  Jordi Burguet-Castell, Madeline Wade, Aaron Viets
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


"""
This pipeline produces h(t) given DARM_ERR and DARM_CTRL or given DELTAL_RESIDUAL and DELTAL_CTRL. It can be run online in real-time or offline on frame files.  It can write h(t) frames to frame files or to a shared memory partition.

The differential arm length resulting from external sources is

\Delta L_{ext} = ((f^2 + f_s^2 - i * f * f_s / Q) / f^2)
* ((1 + i * f / f_cc) / (\kappa_c C_res)) * d_{err}
+ (A_tst * \kappa_tst + A_pu * \kappa_pu) * d_{ctrl}

where C is the static portion of the sensing function, A_tst is the TST actuation function, A_pu is the PUM+UIM actuation, \kappa_c is the time-dependent gain of the sensing function, \kappa_tst is the time-dependent gain of TST actuation, and \kappa_pu is the time-dependent gain of the PUM/UIM actuation.  \Delta L_{ext} is divided by the average arm length (4000 m) to obtain h(t), the external strain in the detectors,

h(t) = \Delta L_{ext} / L.

The time-dependent gains (\kappa's) as well as the value for the coupled cavity pole f_cc and SRC detuning parameters f_s and Q are calcuated in this pipeline as well.

This pipeline will most often be run in a format where it picks up after part of the actuation and sensing functions have been applied to the appropriate channels.  In this mode, the input channels are \Delta L_{res} and \Delta L_{ctrl, i}.  This pipeline then applies further high frequency corrections to each of these channels, applies the appropriate time delay to each channel, adds the channels together, and divides by L.

h(t) = (((f^2 + f_s^2 - i * f * f_s / Q) / f^2)
* ((1 + i * f / f_cc) / \kappa_c) * corrections * \Delta L_{res}
+ \kappa_tst * \Delta L_{ctrl, TST}
+ \kappa_pu * (\Delta L_{ctrl, P} + \Delta L_{ctrl, U})) / L

Note: The actuation \kappa's are complex numbers.  Only the real part of the computed \kappa's are applied as time-dependent gain corrections.

Further documentation explaining the time domain calibration procedure can be found in LIGO DCC #T1400256 and #P1700236.

For a full list of example command lines that were used to create the O1 h(t) frames, see https://wiki.ligo.org/Calibration/GDSCalibrationConfigurationsO1.
For a full list of example command lines that were used to create the O2 h(t) frames, see https://wiki.ligo.org/Calibration/GDSCalibrationConfigurationsO2.

Type gstlal_compute_strain --help to see the full list of command line options.
"""

from __future__ import print_function

import os
import sys
import numpy
import time
import resource
import hashlib
import copy

from optparse import OptionParser, Option
import configparser

import gi
gi.require_version('Gst', '1.0')
from gi.repository import GLib, Gst
Gst.init(None)

import lal

from gstlal import pipeparts
from gstlalcalibration import calibration_parts
from gstlal import datasource
from gstlalcalibration import calibhandler
from gstlal import simplehandler

from ligo import segments

#
# Function definition for writing pipeline graph
#

def write_graph(demux):
	pipeparts.write_dump_dot(pipeline, "%s.%s" % (DebuggingConfigs["pipelinegraphfilename"], "PLAYING"), verbose = True)

#
# Make sure we have sufficient resources
# We allocate far more memory than we need, so this is okay
#

def setrlimit(res, lim):
	hard_lim = resource.getrlimit(res)[1]
	resource.setrlimit(res, (lim if lim is not None else hard_lim, hard_lim))

if sys.platform == "linux":
	# set the number of processes and total set size up to hard limit and
	# shrink the per-thread stack size (default is 10 MiB)
	setrlimit(resource.RLIMIT_NPROC, None)
	setrlimit(resource.RLIMIT_AS, None)
	setrlimit(resource.RLIMIT_RSS, None)
	setrlimit(resource.RLIMIT_STACK, 1024*1024)

#
# Function definition to obtain the current GPS time
#

def now():
	return lal.LIGOTimeGPS(lal.UTCToGPS(time.gmtime()), 0)
	
#
# Function definition to check a string to see if it is a number
#

def is_number(s):
	try:
		float(s)
		return True
	except ValueError:
		return False

#
# Function definition to get sha256 checksum of a file
#

def file_sha256(path):
	with open(path, 'rb') as f:
		hexsha = hashlib.sha256(f.read()).hexdigest()
	return hexsha

#############################################################################
##################### Program Command Line Options ##########################
#############################################################################

parser = OptionParser(description = __doc__)

# Append program specific options

# These options should be used whether the pipeline runs in full calibration mode or partial calibration mode
parser.add_option("--gps-start-time", metavar = "seconds", help = "Set the start time of the segment to analyze in GPS seconds. This is required iff DataSource is frames")
parser.add_option("--gps-end-time", metavar = "seconds", help = "Set the end time of the segment to analyze in GPS seconds. This is required iff DataSource is =frames")
parser.add_option("--frame-cache", metavar = "filename", help = "Set the name of the LAL cache listing the LIGO .gwf frame files (optional).  This is required iff DataSource is frames")
parser.add_option("--output-path", metavar = "name", default = ".", help = "Set the output path for writing frame files. (Default=Current)")
parser.add_option("--wings", metavar = "seconds", default = 0, type = "int", help = "Number of seconds to trim off of the beginning and end of the output. Should only be used if DataSource is frames.")
parser.add_option("--frame-duration", metavar = "seconds", type = "int", default = 4, help = "Set the number of seconds for each frame. (Default = 4)")
parser.add_option("--frames-per-file", metavar = "count", type = "int", default = 1, help = "Set the number of frames per frame file. (Default = 1)")
parser.add_option("--config-file", metavar = "name", default = None, help = "Full path to configuration file for running.")
parser.add_option("--filters-file-name", metavar = "name", default = None, help = "Filters file for running.  This should be either a full path or just the name of the file.")
parser.add_option("--version", action = "store_true", help = "Print version info for gstlal-calibration, configuration file, and filters file and exit program")

# Parse options
options, filenames = parser.parse_args()

gstlal_calibration_version = os.popen("gst-inspect-1.0 gstlalcalibration").read().split("Version")[1].split('\n')[0].replace(' ', '').replace('v', '').split('.')
print("gstlal-calibration-%s.%s.%s" % (gstlal_calibration_version[0], gstlal_calibration_version[1], gstlal_calibration_version[2]), file=sys.stderr)
if options.version and options.config_file is None:
	exit()

#############################################################################
######################### Config File Options ###############################
#############################################################################

def ConfigSectionMap(section):
	dict1 = {}
	options = Config.options(section)
	for option in options:
		try:
			dict1[option] = Config.get(section, option)
			if dict1[option] == -1:
				DebugPrint("skip: %s" % option)
		except:
			print("exception on %s!" % option, file=sys.stderr)
			dict[option] = None
	return dict1

Config = configparser.ConfigParser()
Config.read(options.config_file)

InputConfigs = ConfigSectionMap("InputConfigurations")
OutputConfigs = ConfigSectionMap("OutputConfigurations")
CalibrationConfigs = ConfigSectionMap("CalibrationConfigurations")
DebuggingConfigs = ConfigSectionMap("DebuggingConfigurations")
TDCFConfigs = ConfigSectionMap("TDCFConfigurations")
ChannelNames = ConfigSectionMap("ChannelNames")
SampleRates = ConfigSectionMap("SampleRates")
Bitmasks = ConfigSectionMap("Bitmasks")
PipelineConfigs = ConfigSectionMap("PipelineConfigurations")
DataCleaningConfigs = ConfigSectionMap("DataCleaningConfigurations")
try:
	MonitorConfigs = ConfigSectionMap("MonitoringConfigurations")
	kafka_server = MonitorConfigs["kafkaserver"]
except:
	kafka_server = None
try:
	latency_suffix = MonitorConfigs["latencysuffix"]
except:
	latency_suffix = None

# Find information on the configs
path2configs = options.config_file.split('/')
configs_dir = path2configs[0] if len(path2configs) > 1 else '.'
for dirname in path2configs[1:-1]:
	configs_dir += '/' + dirname

# Sanity checks for command line options
data_sources = set(("frames", "lvshm"))

if InputConfigs["datasource"] not in data_sources:
	raise ValueError("DataSource must be one of %s" % ",".join(data_sources))

if InputConfigs["datasource"] == "frames" and options.frame_cache is None:
	raise ValueError("FrameCache must be specified when using DataSource: frames")

if int(options.wings != 0) and InputConfigs["datasource"] != "frames":
	raise ValueError("Wings can only be set when DataSource: frames")

if CalibrationConfigs["ifo"] is None:
	raise ValueError("must specify IFO")

if InputConfigs["datasource"] == "frames" and (options.gps_start_time is None or options.gps_end_time is None):
	raise ValueError("must specify --gps-start-time and --gps-end-time when DataSource: frames")

if (CalibrationConfigs["calibrationmode"] != "Full") and (CalibrationConfigs["calibrationmode"] != "Partial"):
	raise ValueError("must specify either Full or Partial for CalibrationMode")

if int(SampleRates["recordfactorssr"]) > int(SampleRates["computefactorssr"]):
	raise ValueError("RecordFactorsSR must be less than or equal to ComputeFactorsSR")

if TDCFConfigs["factorsfromfiltersfile"] == "No" and (TDCFConfigs["computefs"] == "Yes" or TDCFConfigs["computesrcq"] == "Yes") and ((InputConfigs["datasource"] == "frames" and int(options.gps_start_time) < 1175954418)):
	raise ValueError("Cannot compute SRC detuning parameters as the needed EPICS channels are not in the frames until GPS time 1175954418. Set ComputeFs: No and ComputeSRCQ: No.")

if options.gps_start_time is not None:
	if options.gps_end_time is None:
		raise ValueError("must provide both --gps-start-time and --gps-end-time")
	if InputConfigs["datasource"] == "lvshm":
		raise ValueError("cannot set --gps-start-time or --gps-end-time with DataSource: lvshm")
	if int(options.gps_start_time) >= int(options.gps_end_time):
		raise ValueError("--gps-start-time must be < --gps-end-time: %s < %s" % (options.gps_start_time, options.gps_end_time))
elif options.gps_end_time is not None:
	raise ValueError("must provide both --gps-start-time and --gps-end-time")

###################################################################################################
######################################## Setup ####################################################
###################################################################################################

# Set up instrument and channel name info from command line options
instrument = CalibrationConfigs["ifo"]

# Set up short-cut names for each of the sample rates used throughout the pipeline and establish caps string shortcuts
hoft_sr = int(SampleRates["hoftsr"]) # Sample rate for h(t)
calibstate_sr = int(SampleRates["calibstatesr"]) # Sample rate for the CALIB_STATE_VECTOR
obsintent_sr = int(SampleRates["obsintentsr"]) if "obsintentsr" in SampleRates else int(SampleRates["odcsr"]) # Sample rate of the ODC channel that is read in
lownoise_sr = int(SampleRates["lownoisesr"]) if "lownoisesr" in SampleRates else int(SampleRates["odcsr"])
filterclock_sr = int(SampleRates["filterclocksr"]) if "filterclocksr" in SampleRates else int(SampleRates["filterclocksrlist"]) if "filterclocksrlist" in SampleRates else lownoise_sr
filterclockthreshold_sr = int(SampleRates["filterclockthresholdsr"]) if "filterclockthresholdsr" in SampleRates else filterclock_sr
hwinj_sr = int(SampleRates["hwinjsr"]) if "hwinjsr" in SampleRates else int(SampleRates["odcsr"])
undisturbedok_sr = int(SampleRates["undisturbedoksr"]) if "undisturbedoksr" in SampleRates else obsintent_sr
ctrl_sr = int(SampleRates["ctrlsr"]) # Sample rate of the control channel (such as DARM_CTRL or DELTAL_CTRL)
tst_exc_sr = int(SampleRates["tstexcsr"])
pum_exc_sr = int(SampleRates["pumexcsr"])
uim_exc_sr = int(SampleRates["uimexcsr"])
coh_sr = int(SampleRates["cohsr"]) # Sample rate for the coherence uncertainty channels
epics_sr = int(SampleRates["epicsrefsr"]) # Sample rate for EPICS records used for TDCFs
line_amplitude_sr = int(SampleRates["lineamplitudesr"]) if "lineamplitudesr" in SampleRates else epics_sr
compute_factors_sr = int(SampleRates["computefactorssr"]) # Sample rate for computing TDCFs
record_factors_sr = int(SampleRates["recordfactorssr"]) # Sample rate for recording TDCFs
metrics_sr = int(SampleRates["metricssr"]) if "metricssr" in SampleRates else 1 # Sample rate for recording metrics from pipeline
nonsens_subtraction_sr = int(SampleRates["nonsenssubtractionsr"]) if "nonsenssubtractionsr" in SampleRates else hoft_sr
hoft_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % hoft_sr
ctrl_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % ctrl_sr
latency_metrics_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % metrics_sr
calibstate_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % calibstate_sr
calibstate_metrics_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % metrics_sr
obsintent_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % obsintent_sr
lownoise_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % lownoise_sr
filterclock_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % filterclock_sr
filterclockthreshold_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % filterclockthreshold_sr
hwinj_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % hwinj_sr
undisturbedok_caps = "audio/x-raw, format=U32LE, rate=%d, channel-mask=(bitmask)0x0" % undisturbedok_sr
coh_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % coh_sr
ref_factors_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % epics_sr
compute_calib_factors_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0X0" % compute_factors_sr
compute_calib_factors_complex_caps = "audio/x-raw, format=Z128LE, rate=%d, channel-mask=(bitmask)0x0" % compute_factors_sr
nonsens_subtraction_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % nonsens_subtraction_sr
line_amplitude_caps = "audio/x-raw, format=F64LE, rate=%d, channel-mask=(bitmask)0x0" % line_amplitude_sr

# Short cut names for a few other items that appear numerous times
input_frame_duration = int(InputConfigs["inputframeduration"])
wait_time = float(InputConfigs["waittime"]) if ("waittime" in InputConfigs and InputConfigs["datasource"] == "lvshm") else 0.0
input_min = float(InputConfigs["inputmin"]) if "inputmin" in InputConfigs else 1e-35
input_max = float(InputConfigs["inputmax"]) if "inputmax" in InputConfigs else 1e35
filter_latency_factor = float(PipelineConfigs["filterlatency"])
demodulation_filter_time = int(TDCFConfigs["demodulationfiltertime"])
coherence_unc_threshold = float(TDCFConfigs["coherenceuncthreshold"])
actuation_filter_update_time = float(TDCFConfigs["actuationfilterupdatetime"])
actuation_filter_averaging_time = float(TDCFConfigs["actuationfilteraveragingtime"])
actuation_filter_taper_length = int(TDCFConfigs["actuationfiltertaperlength"])
sensing_filter_update_time = float(TDCFConfigs["sensingfilterupdatetime"])
sensing_filter_averaging_time = float(TDCFConfigs["sensingfilteraveragingtime"]) if "sensingfilteraveragingtime" in TDCFConfigs else 0
sensing_filter_taper_length = int(TDCFConfigs["sensingfiltertaperlength"])
pcal_sign = float(TDCFConfigs["pcalsign"]) if "pcalsign" in TDCFConfigs else 1.0
cleaning_check_rms_time = float(DataCleaningConfigs["cleaningcheckrmstime"])
cleaning_check_range_low_min = float(DataCleaningConfigs["cleaningcheckrangelowmin"])
cleaning_check_range_low_max = float(DataCleaningConfigs["cleaningcheckrangelowmax"])
cleaning_check_range_mid_min = float(DataCleaningConfigs["cleaningcheckrangemidmin"])
cleaning_check_range_mid_max = float(DataCleaningConfigs["cleaningcheckrangemidmax"])
nonsens_check_range_min = float(DataCleaningConfigs["nonsenscheckrangemin"]) if "nonsenscheckrangemin" in DataCleaningConfigs else 15
nonsens_check_range_max = float(DataCleaningConfigs["nonsenscheckrangemax"]) if "nonsenscheckrangemax" in DataCleaningConfigs else 30
witness_channel_fft_time = float(DataCleaningConfigs["witnesschannelffttime"])
num_witness_ffts = int(DataCleaningConfigs["numwitnessffts"])
min_witness_ffts = int(DataCleaningConfigs["minwitnessffts"])
witness_fir_length = DataCleaningConfigs["witnessfirlength"].split(";")
witness_frequency_resolution = float(DataCleaningConfigs["witnessfrequencyresolution"])
witness_tf_update_time = float(DataCleaningConfigs["witnesstfupdatetime"])
critical_lock_loss_time = float(DataCleaningConfigs["criticallocklosstime"])
witness_filter_taper_time = float(DataCleaningConfigs["witnessfiltertapertime"])
witness_tf_time_shift = float(DataCleaningConfigs["witnesstftimeshift"]) if "witnesstftimeshift" in DataCleaningConfigs else 0.0
witness_tf_filename = DataCleaningConfigs["witnesstffilename"] if DataCleaningConfigs["witnesstffilename"] != "None" else None
strain_channel_transition_time = float(DataCleaningConfigs["strainchanneltransitiontime"]) if "strainchanneltransitiontime" in DataCleaningConfigs else 1.0

expected_kappatst_real = float(TDCFConfigs["expectedkappatstreal"])
expected_kappatst_imag = float(TDCFConfigs["expectedkappatstimag"])
expected_kappapum_real = float(TDCFConfigs["expectedkappapumreal"])
expected_kappapum_imag = float(TDCFConfigs["expectedkappapumimag"])
expected_kappauim_real = float(TDCFConfigs["expectedkappauimreal"])
expected_kappauim_imag = float(TDCFConfigs["expectedkappauimimag"])
expected_kappac = float(TDCFConfigs["expectedkappac"])
expected_fcc = float(TDCFConfigs["expectedfcc"]) if "expectedfcc" in TDCFConfigs else 0.0
# Note: fs and Q are handled later due to the fact that they are handled as complex values
kappatst_real_var = float(TDCFConfigs["kappatstrealvar"])
kappatst_imag_var = float(TDCFConfigs["kappatstimagvar"])
kappapum_real_var = float(TDCFConfigs["kappapumrealvar"])
kappapum_imag_var = float(TDCFConfigs["kappapumimagvar"])
kappauim_real_var = float(TDCFConfigs["kappauimrealvar"])
kappauim_imag_var = float(TDCFConfigs["kappauimimagvar"])
kappac_var = float(TDCFConfigs["kappacvar"])
fcc_var = float(TDCFConfigs["fccvar"])
fs_squared_var = float(TDCFConfigs["fssquaredvar"]) if "fssquaredvar" in TDCFConfigs else pow(float(TDCFConfigs["fsvar"]), 2)
# Maximum tolerable sum of square error measured at Pcal lines in the calculation of sensing function TDCFs
exact_sensing_tdcfs_max_error = float(TDCFConfigs["exactsensingtdcfsmaxerror"]) if "exactsensingtdcfsmaxerror" in TDCFConfigs else 1e-5

# Set up smoothing, averaging and integration sample sizes for kappa calulations
median_smoothing_time = int(TDCFConfigs["mediansmoothingtime"])
factors_average_time = int(TDCFConfigs["tdcfaveragingtime"])
coherence_time = int(TDCFConfigs["coherencetime"])
integration_samples = demodulation_filter_time * compute_factors_sr
factors_average_samples = factors_average_time * compute_factors_sr
median_smoothing_samples = median_smoothing_time * compute_factors_sr
coherence_samples = coherence_time * compute_factors_sr
kappa_gate_attack_length = -integration_samples * (1.0 - filter_latency_factor)
kappa_gate_hold_length = -integration_samples * filter_latency_factor - (filter_latency_factor != 0) * coherence_time * compute_factors_sr
tdcf_coherence_length = int(numpy.ceil(integration_samples / 4))
tdcf_unc_samples = int(numpy.ceil((median_smoothing_samples + factors_average_samples) / tdcf_coherence_length))

# Set up string for the channels suffix and prefix as provided by the user
if OutputConfigs["chansuffix"] != "None":
	chan_suffix = OutputConfigs["chansuffix"]
else:
	chan_suffix = ""
chan_prefix = OutputConfigs["chanprefix"]

ci_latency_dir = DebuggingConfigs["cilatencydir"] if "cilatencydir" in DebuggingConfigs else None
if ci_latency_dir == "None":
	ci_latency_dir = None

# Set up shortcut variables for boolean options
# Booleans for TDCFs
compute_kappatst = Config.getboolean("TDCFConfigurations", "computekappatst")
compute_kappapum = Config.getboolean("TDCFConfigurations", "computekappapum")
compute_kappauim = Config.getboolean("TDCFConfigurations", "computekappauim")
compute_kappac = Config.getboolean("TDCFConfigurations", "computekappac")
compute_fcc = Config.getboolean("TDCFConfigurations", "computefcc")
compute_fs = Config.getboolean("TDCFConfigurations", "computefs")
compute_srcq = Config.getboolean("TDCFConfigurations", "computesrcq")
compute_any_tdcfs = compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq
apply_kappatst = Config.getboolean("TDCFConfigurations", "applykappatst")
apply_complex_kappatst = Config.getboolean("TDCFConfigurations", "applycomplexkappatst")
apply_kappapum = Config.getboolean("TDCFConfigurations", "applykappapum")
apply_complex_kappapum = Config.getboolean("TDCFConfigurations", "applycomplexkappapum")
apply_kappauim = Config.getboolean("TDCFConfigurations", "applykappauim")
apply_complex_kappauim = Config.getboolean("TDCFConfigurations", "applycomplexkappauim")
apply_kappac = Config.getboolean("TDCFConfigurations", "applykappac")
apply_fcc = Config.getboolean("TDCFConfigurations", "applyfcc")
apply_srcq = Config.getboolean("TDCFConfigurations", "applysrcq")
apply_fs = Config.getboolean("TDCFConfigurations", "applyfs")
minimize_adaptive_sensfilt = Config.getboolean("TDCFConfigurations", "minimizeadaptivesensingfilter") if "minimizeadaptivesensingfilter" in TDCFConfigs else False
use_coherence = Config.getboolean("TDCFConfigurations", "usecoherence")
tdcf_gate_with_excitation = Config.getboolean("TDCFConfigurations", "tdcfgatewithexcitation") if "tdcfgatewithexcitation" in TDCFConfigs else False
tdcf_default_to_median = Config.getboolean("TDCFConfigurations", "tdcfdefaulttomedian")
compute_exact_kappas = Config.getboolean("TDCFConfigurations", "computeexactkappas") if "computeexactkappas" in TDCFConfigs else False
TDCFs_use_C2_sensing_model = Config.getboolean("TDCFConfigurations", "tdcfsusec2sensingmodel") if "tdcfsusec2sensingmodel" in TDCFConfigs else False
compute_tdcf_uncertainty = Config.getboolean("TDCFConfigurations", "computetdcfuncertainty") if "computetdcfuncertainty" in TDCFConfigs else False
# Boolean for state vector computation
compute_calib_statevector = Config.getboolean("CalibrationConfigurations", "computecalibstatevector")
# Boolean for computing factors from filters file
factors_from_filters_file = Config.getboolean("TDCFConfigurations", "factorsfromfiltersfile")
# Boolean for removing calibration lines, power lines, DC component, using median in witness transfer functions
remove_dc = Config.getboolean("DataCleaningConfigurations", "removedc")
witness_tf_use_median = Config.getboolean("DataCleaningConfigurations", "witnesstfusemedian")
witness_tf_parallel_mode = Config.getboolean("DataCleaningConfigurations", "witnesstfparallelmode") if "witnesstfparallelmode" in DataCleaningConfigs else False
pick_cleanest_strain_channel = Config.getboolean("DataCleaningConfigurations", "pickcleaneststrainchannel") if "pickcleaneststrainchannel" in DataCleaningConfigs else False
noisesub_use_filter_clock = Config.getboolean("DataCleaningConfigurations", "noisesubusefilterclock") if "noisesubusefilterclock" in DataCleaningConfigs else False
# If td is true we will perform filtering in the time domain (direct convolution) in all FIR filtering routines below
td = not Config.getboolean("PipelineConfigurations", "frequencydomainfiltering")
# Boolean for verboseness
verbose = Config.getboolean("DebuggingConfigurations", "verbose")
# Boolean for file_check_sum
file_check_sum = Config.getboolean("InputConfigurations", "filechecksum")
skip_bad_files = Config.getboolean("InputConfigurations", "skipbadfiles")
# Should we write information about filters, configs and gstlal-calibration version in the frame headers?
write_config_info = Config.getboolean("OutputConfigurations", "writeconfiginfo") if "writeconfiginfo" in OutputConfigs else False
# Booleans for calibration tests
test_latency = Config.getboolean("DebuggingConfigurations", "testlatency") if "testlatency" in DebuggingConfigs else False
test_filters = Config.getboolean("DebuggingConfigurations", "testfilters") if "testfilters" in DebuggingConfigs else False
monitor_statevector = Config.getboolean("DebuggingConfigurations", "monitorstatevector") if "monitorstatevector" in DebuggingConfigs else False

# Silence latency metric output from stdout if not running in "test_latency" mode
if test_latency:
	silent = False
else:
	silent = True

#
# Load in the filters file that contains filter coefficients, etc.
#

# Search the directory tree for files with names matching the one we want.
filters_name = options.filters_file_name
if filters_name is None:
	filters_name = InputConfigs["filtersfilename"]
if filters_name.count('/') > 0:
	# Then the path to the filters file was given
	filters = numpy.load(filters_name)
else:
	# We need to search for the filters file
	filters_paths = []
	print("\nSearching for %s ..." % filters_name, file=sys.stderr)
	# Check the user's home directory
	for dirpath, dirs, files in os.walk(os.environ['HOME']):
		if filters_name in files:
			# We prefer filters that came directly from a GDSFilters directory of the calibration SVN
			if dirpath.count("GDSFilters") > 0:
				filters_paths.insert(0, os.path.join(dirpath, filters_name))
			else:
				filters_paths.append(os.path.join(dirpath, filters_name))
	# Check if there is a checkout of the entire calibration SVN
	for dirpath, dirs, files in os.walk('/ligo/svncommon/CalSVN/aligocalibration/trunk/Runs/'):
		if filters_name in files:
			# We prefer filters that came directly from a GDSFilters directory of the calibration SVN
			if dirpath.count("GDSFilters") > 0:
				filters_paths.insert(0, os.path.join(dirpath, filters_name))
			else:
				filters_paths.append(os.path.join(dirpath, filters_name))
	if not len(filters_paths):
		raise ValueError("Cannot find filters file %s in home directory %s or in /ligo/svncommon/CalSVN/aligocalibration/trunk/Runs/*/GDSFilters", (filters_name, os.environ['HOME']))
	print("Loading calibration filters from %s\n" % filters_paths[0], file=sys.stderr)
	filters = numpy.load(filters_paths[0])

	filters_name = filters_paths[0]

# Get information about the current version of configs, filters, and gstlal-calibration
if write_config_info or options.version:
	try:
		configs_url = os.popen("cd %s; git config --get remote.origin.url" % configs_dir).read().split('\n')[0].split('@')[-1]
	except:
		configs_url = ''
	try:
		configs_git_commit_hash = os.popen("cd %s; git rev-parse HEAD" % configs_dir).read().split('\n')[0]
		configs_git_commit_hash_int = int(configs_git_commit_hash[:7], 16)
	except:
		configs_git_commit_hash = 'Unavailable'
		configs_git_commit_hash_int = 0
	filters_sha256 = file_sha256(filters_name)
	filters_sha256_int = int(filters_sha256[:7], 16)
	filters_tagstr = "filters=%s" % filters_name
	# Additional info requested from the filters file
	filters_info = OutputConfigs["filtersinfo"] if "filtersinfo" in OutputConfigs else None
	if filters_info == "None":
		filters_info == None
	if filters_info is not None:
		filters_info = filters_info.split(',')
		for info in filters_info:
			if info in filters:
				filters_tagstr += ",%s=%s" % (info, str(filters[info]))

	for i in range(len(gstlal_calibration_version)):
		while len(gstlal_calibration_version[i]) < 2:
			gstlal_calibration_version[i] = '0' + gstlal_calibration_version[i]
	version = ''
	for xx in gstlal_calibration_version:
		version += xx
	gstlal_calibration_version = int(version[-8:])
	print("%s:%sCALIB_GSTLAL_CALIBRATION_VERSION_INT%s: %d" % (instrument, chan_prefix, chan_suffix, gstlal_calibration_version), file=sys.stderr)
	print("Configuration file: %s" % os.path.abspath(options.config_file), file=sys.stderr)
	print("Configuration file sha256 checksum: %s" % file_sha256(options.config_file), file=sys.stderr)
	print("%s:%sCALIB_GSTLAL_CONFIG_HASH_INT%s: %d" % (instrument, chan_prefix, chan_suffix, configs_git_commit_hash_int), file=sys.stderr)
	print("Filters file: %s" % os.path.abspath(filters_name), file=sys.stderr)
	print("Filters file sha256 checksum: %s" % filters_sha256, file=sys.stderr)
	print("%s:%sCALIB_REPORT_GDS_HASH_INT%s: %d" % (instrument, chan_prefix, chan_suffix, filters_sha256_int), file=sys.stderr)

	if options.version:
		exit()

# Load all of the calibration line frequencies and and pcal correction factors
try:
	act_pcal_line_freq = float(filters["ka_pcal_line_freq"])
	pcal_corr_at_act_freq_real = float(filters["ka_pcal_corr_re"])
	pcal_corr_at_act_freq_imag = float(filters["ka_pcal_corr_im"])
except:
	if compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq:
		raise ValueError("Cannot compute any time-dependent correction factors, as the ~30 Hz pcal line frequency is not in the filters file")
try:
	opt_gain_fcc_line_freq = float(filters["kc_pcal_line_freq"])
	pcal_corr_at_opt_gain_fcc_freq_real = float(filters["kc_pcal_corr_re"])
	pcal_corr_at_opt_gain_fcc_freq_imag = float(filters["kc_pcal_corr_im"])
except:
	if compute_kappac or compute_fcc or compute_fs or compute_srcq:
		raise ValueError("Cannot compute kappa_C, f_cc, f_s, or Q, as the ~300 Hz pcal line is not in the filters file")
try:
	esd_act_line_freq = float(filters["ktst_esd_line_freq"])
except:
	if compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq:
		raise ValueError("Cannot compute time-dependent correction factors, as the ESD line frequency is not in the filters file")
try:
	pum_act_line_freq = float(filters["pum_act_line_freq"])
except:
	if compute_kappapum or (compute_kappac or compute_fcc or compute_fs or compute_srcq):
		raise ValueError("Cannot compute kappa_PUM, as the specified filters file does not contain the needed calibration line frequency")
try:
	uim_act_line_freq = float(filters["uim_act_line_freq"])
except:
	if compute_kappauim or (compute_kappac or compute_fcc or compute_fs or compute_srcq):
		raise ValueError("Cannot compute kappa_UIM using the UIM actuation line, as the specified filters file does not contain that calibration line frequency")
try:
	high_pcal_line_freq = float(filters["high_pcal_line_freq"])
	pcal_corr_at_high_line_freq_real = float(filters["high_pcal_corr_re"])
	pcal_corr_at_high_line_freq_imag = float(filters["high_pcal_corr_im"])
except Exception:
	pass
try:
	roaming_pcal_line_freq = float(filters["roaming_pcal_line_freq"])
	pcal_corr_at_roaming_line_real = float(filters["roaming_pcal_corr_re"])
	pcal_corr_at_roaming_line_imag = float(filters["roaming_pcal_corr_im"])
except Exception:
	pass
try:
	fcc_default = float(filters["fcc"])
except:
	fcc_default = expected_fcc
	if fcc_default == 0.0:
		print("Warning: Could not find expected fcc in filters file or config file.  Setting to zero.", file=sys.stderr)
if "fs_squared" in filters:
	fs_squared_default = float(filters["fs_squared"])
elif "expectedfssquared" in TDCFConfigs:
	fs_squared_default = float(TDCFConfigs["expectedfssquared"])
elif "fs" in filters:
	fs_squared_default = pow(float(numpy.real(filters["fs"])), 2)
elif "expectedfs" in TDCFConfigs:
	fs_squared_default = pow(float(TDCFConfigs["expectedfs"]), 2)
else:
	print("Warning: Could not find expected fs in filters file or config file.  Setting to zero.", file=sys.stderr)
	fs_squared_default = 0

# fs_squared_default cannot be exactly zero, as it is later the deniminator of a division
if fs_squared_default == 0:
	fs_squared_default = 0.01

fs_default = numpy.sqrt(complex(fs_squared_default))

if compute_exact_kappas and TDCFs_use_C2_sensing_model:
	srcQ_default = fcc_default / (2.0 * abs(fs_default))
elif "srcQ" in filters:
	srcQ_default = complex(filters["srcQ"])
elif "expectedsrcq" in TDCFConfigs:
	srcQ_default = complex(TDCFConfigs["expectedsrcq"])
else:
	print("Warning: Could not find expected srcQ in filters file or config file.  Setting to zero.", file=sys.stderr)
	srcQ_default = 0 + 0j

if fs_squared_default < 0:
	srcQ_default = -1j * srcQ_default
fs_over_Q_default = numpy.real(fs_default / srcQ_default)

# If we're reading the reference model factors from the filters file, load them

if factors_from_filters_file or compute_exact_kappas:
	ATinvRratio_fT_re, ATinvRratio_fT_im, APinvRratio_fP_re, APinvRratio_fP_im, AUinvRratio_fU_re, AUinvRratio_fU_im, C0_f2_re, C0_f2_im, D_f2_re, D_f2_im, AT_f2_re, AT_f2_im, AP_f2_re, AP_f2_im, AU_f2_re, AU_f2_im, C0_f1_re, C0_f1_im, D_f1_re, D_f1_im, AT_f1_re, AT_f1_im, AP_f1_re, AP_f1_im, AU_f1_re, AU_f1_im, C0DAT_f1_re, C0DAT_f1_im, C0DAP_f1_re, C0DAP_f1_im, C0DAU_f1_re, C0DAU_f1_im, C0DAT_f2_re, C0DAT_f2_im, C0DAP_f2_re, C0DAP_f2_im, C0DAU_f2_re, C0DAU_f2_im, C0AT0_fT_re, C0AT0_fT_im, C0DAT_fT_re, C0DAT_fT_im, C0DAP_fT_re, C0DAP_fT_im, C0DAU_fT_re, C0DAU_fT_im, C0AP0_fP_re, C0AP0_fP_im, C0DAT_fP_re, C0DAT_fP_im, C0DAP_fP_re, C0DAP_fP_im, C0DAU_fP_re, C0DAU_fP_im, C0AU0_fU_re, C0AU0_fU_im, C0DAT_fU_re, C0DAT_fU_im, C0DAP_fU_re, C0DAP_fU_im, C0DAU_fU_re, C0DAU_fU_im, R_f1_re, R_f1_im, R_f2_re, R_f2_im, RAT0inv_fT_re, RAT0inv_fT_im, RAP0inv_fP_re, RAP0inv_fP_im, RAU0inv_fU_re, RAU0inv_fU_im, AT0_fT_re, AT0_fT_im, AP0_fP_re, AP0_fP_im, AU0_fU_re, AU0_fU_im = calibration_parts.get_epics(filters)

# Load the high-pass filters for h(t)
if "ctrl_highpass" in filters:
	act_highpass = filters["ctrl_highpass"]
	act_highpass_delay = int(filters['ctrl_highpass_delay'])
elif "actuation_highpass" in filters:
	act_highpass = filters["actuation_highpass"]
	act_highpass_delay = int(filters['actuation_highpass_delay'])
else:
	act_highpass = []
if "res_highpass" in filters:
	invsens_highpass = filters["res_highpass"]
	invsens_highpass_delay = int(filters['res_highpass_delay'])
	invsens_highpass_sr = int(filters['res_highpass_sr']) if 'res_highpass_sr' in filters else hoft_sr
elif "inv_sensing_highpass" in filters:
	invsens_highpass = filters["inv_sensing_highpass"]
	invsens_highpass_delay = int(filters['invsens_highpass_delay'])
	invsens_highpass_sr = int(filters['invsens_highpass_sr']) if 'invsens_highpass_sr' in filters else hoft_sr
else:
	invsens_highpass = []

# Load the nonsens subtraction filter if there is one
if "nonsens_firfilt" in filters:
	nonsens_firfilt = filters["nonsens_firfilt"]
	nonsens_firfilt_delay = int(filters["nonsens_firfilt_delay"])
else:
	nonsens_firfilt = []

# If we're performing partial calibration, load the deltal filters
if CalibrationConfigs["calibrationmode"] == "Partial":
	reschaindelay = int(filters["res_corr_delay"])
	if minimize_adaptive_sensfilt and apply_fcc and (apply_fs or apply_srcq):
		reschainfilt = filters["res_corr_nopole_filter"]
	elif minimize_adaptive_sensfilt and apply_fcc:
		reschainfilt = filters["res_corr_noccpole_filter"]
	else:
		reschainfilt = filters["res_corr_filter"]
	tstdelay = pumdelay = uimdelay = pumuimdelay = int(filters["ctrl_corr_delay"])
	tstfilt = filters["TST_corr_filter"] if "TST_corr_filter" in filters else filters["ctrl_corr_filter"]
	pumfilt = filters["PUM_corr_filter"] if "PUM_corr_filter" in filters else filters["ctrl_corr_filter"]
	uimfilt = filters["UIM_corr_filter"] if "UIM_corr_filter" in filters else filters["ctrl_corr_filter"]
	pumuimfilt = filters["ctrl_corr_filter"]
	tstchainsr = pumchainsr = uimchainsr = pumuimchainsr = int(filters["ctrl_corr_sr"])
	reschainfilt_model = filters["res_corr_filt_model"] if "res_corr_filt_model" in filters else None
	tstfilt_model = filters["TST_corr_filt_model"] if "TST_corr_filt_model" in filters else filters["ctrl_corr_filt_model"] if "ctrl_corr_filt_model" in filters else None
	pumfilt_model = filters["PUM_corr_filt_model"] if "PUM_corr_filt_model" in filters else filters["ctrl_corr_filt_model"] if "ctrl_corr_filt_model" in filters else None
	uimfilt_model = filters["UIM_corr_filt_model"] if "UIM_corr_filt_model" in filters else filters["ctrl_corr_filt_model"] if "ctrl_corr_filt_model" in filters else None
	pumuimfilt_model = filters["ctrl_corr_filt_model"] if "ctrl_corr_filt_model" in filters else None

# If we're performing full calibration, load the actuation, sensing filters
if CalibrationConfigs["calibrationmode"] == "Full":
	tstchainsr = int(filters["actuation_tst_sr"])
	tstdelay = int(filters["actuation_tst_delay"])
	tstfilt = filters["actuation_tst"]
	tstfilt_model = filters["tstfilt_model"] if "tstfilt_model" in filters else None
	try:
		pumchainsr = int(filters["actuation_pum_sr"])
		pumdelay = int(filters["actuation_pum_delay"])
		pumfilt = filters["actuation_pum"]
		pumfilt_model = filters["pumfilt_model"] if "pumfilt_model" in filters else None
		uimchainsr = int(filters["actuation_uim_sr"])
		uimdelay = int(filters["actuation_uim_delay"])
		uimfilt = filters["actuation_uim"]
		uimfilt_model = filters["uimfilt_model"] if "uimfilt_model" in filters else None
	except:
		if apply_kappapum or apply_kappauim or apply_complex_kappapum or apply_complex_kappauim:
			raise ValueError("PUM and UIM actuation filters are needed since the PUM and UIM stages are being split")
	try:
		pumuimchainsr = int(filters["actuation_pum_sr"])
		pumuimdelay = int(filters["actuation_pum_delay"])
		pumuimfilt = filters["actuation_pum"] + filters["actuation_uim"]
		pumuimfilt_model = filters["pumfilt_model"] + filters["uimfilt_model"] if ("pumfilt_model" in filters and "uimfilt_model" in filters) else None
	except:
		if not (apply_kappapum or apply_kappauim or apply_complex_kappapum or apply_complex_kappauim):
			raise ValueError("PUM/UIM actuation filter is needed since the PUM and UIM stages are not being split")
	reschaindelay = int(filters["inv_sens_delay"])
	if minimize_adaptive_sensfilt and apply_fcc and (apply_fs or apply_srcq):
		reschainfilt = filters["inv_sensing_nopole"]
	elif minimize_adaptive_sensfilt and apply_fcc:
		reschainfilt = filters["inv_sensing_noccpole"]
	else:
		reschainfilt = filters["inv_sensing"]
	reschainfilt_model = filters["invsensfilt_model"] if "invsensfilt_model" in filters else None

invsens_window_type = str(filters["invsens_window_type"]) if "invsens_window_type" in filters else 'dpss'
act_window_type = str(filters["act_window_type"]) if "act_window_type" in filters else 'dpss'
invsens_freq_res = float(filters["invsens_freq_res"]) if "invsens_freq_res" in filters else 3.0
act_freq_res = float(filters["act_freq_res"]) if "act_freq_res" in filters else 3.0


#
# Set up the appropriate channel list. In this section, we also fill a list called headkeys
# that will be the keys for the dictionary holding each pipeline branch name, and we set up
# a dictionary that will be populated with pipeline branch names based on the channel list.
#

head_dict = {}
channel_list = []
headkeys = []
replace_value_list = []

# If we are computing the CALIB_STATE_VECTOR, we need input DQ channel(s)
if compute_calib_statevector:
	obsintent_channel_name = ChannelNames["obsintentchannel"] if "obsintentchannel" in ChannelNames else ChannelNames["inputdqchannel"]
	obsintent_bitmask = int(Bitmasks["obsintentbitmask"])
	lownoise_channel_name = ChannelNames["lownoisestatechannel"] if "lownoisestatechannel" in ChannelNames else ChannelNames["inputdqchannel"]
	lownoise_bitmask = int(Bitmasks["lownoisebitmask"]) if "lownoisebitmask" in Bitmasks else int(Bitmasks["obsreadybitmask"])
	filterclock_channel_name = ChannelNames["filterclockchannel"] if "filterclockchannel" in ChannelNames else ChannelNames["filterclockchannellist"] if "filterclockchannellist" in ChannelNames else lownoise_channel_name
	filterclock_bitmask = int(Bitmasks["filterclockbitmask"]) if "filterclockbitmask" in Bitmasks else int(Bitmasks["filterclockbitmasklist"]) if "filterclockbitmasklist" in Bitmasks else lownoise_bitmask
	filterclock_threshold = (int(Bitmasks["filterclockthreshold"]) if Bitmasks["filterclockthreshold"] != "None" else None) if "filterclockthreshold" in Bitmasks else None
	filterclock_threshold_channel_name = (ChannelNames["filterclockthresholdchannel"] if ChannelNames["filterclockthresholdchannel"] != "None" else None) if "filterclockthresholdchannel" in ChannelNames else None
	hwinj_channel_name = ChannelNames["hwinjchannel"] if "hwinjchannel" in ChannelNames else ChannelNames["inputdqchannel"]
	cbchwinj_bitmask = int(Bitmasks["cbchwinjbitmask"]) if "cbchwinjbitmask" in Bitmasks else None
	cbchwinj_offbitmask = int(Bitmasks["cbchwinjoffbitmask"]) if "cbchwinjoffbitmask" in Bitmasks else None
	bursthwinj_bitmask = int(Bitmasks["bursthwinjbitmask"]) if "bursthwinjbitmask" in Bitmasks else None
	bursthwinj_offbitmask = int(Bitmasks["bursthwinjoffbitmask"]) if "bursthwinjoffbitmask" in Bitmasks else None
	detcharhwinj_bitmask = int(Bitmasks["detcharhwinjbitmask"]) if "detcharhwinjbitmask" in Bitmasks else None
	detcharhwinj_offbitmask = int(Bitmasks["detcharhwinjoffbitmask"]) if "detcharhwinjoffbitmask" in Bitmasks else None
	stochhwinj_bitmask = int(Bitmasks["stochhwinjbitmask"]) if "stochhwinjbitmask" in Bitmasks else None
	stochhwinj_offbitmask = int(Bitmasks["stochhwinjoffbitmask"]) if "stochhwinjoffbitmask" in Bitmasks else None
	undisturbed_ok_channel = (ChannelNames["undisturbedokchannel"] if ChannelNames["undisturbedokchannel"] != 'None' else None) if "undisturbedokchannel" in ChannelNames else None
	undisturbedok_bitmask = int(Bitmasks["undisturbedokbitmask"]) if "undisturbedokbitmask" in Bitmasks else 1

	channel_list.append((instrument, obsintent_channel_name))
	headkeys.append("obsintent")
	replace_value_list.append(0)
	if lownoise_channel_name != obsintent_channel_name:
		channel_list.append((instrument, lownoise_channel_name))
		headkeys.append("lownoise")
		replace_value_list.append(0)
	if hwinj_channel_name != obsintent_channel_name and hwinj_channel_name != lownoise_channel_name:
		channel_list.append((instrument, hwinj_channel_name))
		headkeys.append("hwinj")
		replace_value_list.append(0)
	if undisturbed_ok_channel is not None:
		channel_list.append((instrument, undisturbed_ok_channel))
		headkeys.append("undisturbedok")
		replace_value_list.append(0)

	if filterclock_channel_name != obsintent_channel_name and filterclock_channel_name != lownoise_channel_name:
		channel_list.append((instrument, filterclock_channel_name))
		headkeys.append(filterclock_channel_name)
		replace_value_list.append(0)

	if filterclock_threshold_channel_name != None:
		channel_list.append((instrument, filterclock_threshold_channel_name))
		headkeys.append(filterclock_threshold_channel_name)
		replace_value_list.append(0)

# If we are using the front-end EPICS records to either compute the TDCFs or the CALIB_STATE_VECTOR, we need to add those channels
# Needed for kappa_tst, kappa_pum, kappa_uim, kappa_pu, kappa_c, f_cc, f_s, and Q
if not factors_from_filters_file:
	# Needed for kappa_TST
	if (compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq) and not compute_exact_kappas:
		channel_list.extend(((instrument, ChannelNames["atinvrratioftrealchannel"]), (instrument, ChannelNames["atinvrratioftimagchannel"]))) if "atinvrratioftrealchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep1realchannel"]), (instrument, ChannelNames["ep1imagchannel"])))
		headkeys.extend(("ATinvRratio_fT_re", "ATinvRratio_fT_im"))
		replace_value_list.extend(("keep_last_good_value", "keep_last_good_value"))
	# These are needed for kappa_PUM
	if (compute_kappapum or (compute_kappac or compute_fcc or compute_fs or compute_srcq)) and not compute_exact_kappas:
		channel_list.extend(((instrument, ChannelNames["apinvrratiofprealchannel"]), (instrument, ChannelNames["apinvrratiofpimagchannel"]))) if "apinvrratiofprealchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep15realchannel"]), (instrument, ChannelNames["ep15imagchannel"])))
		headkeys.extend(("APinvRratio_fP_re", "APinvRratio_fP_im"))
		replace_value_list.extend(("keep_last_good_value", "keep_last_good_value"))
	# These are needed for kappa_UIM
	if (compute_kappauim or (compute_kappac or compute_fcc or compute_fs or compute_srcq)) and not compute_exact_kappas:
		channel_list.extend(((instrument, ChannelNames["auinvrratiofurealchannel"]), (instrument, ChannelNames["auinvrratiofuimagchannel"]))) if "auinvrratiofurealchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep22realchannel"]), (instrument, ChannelNames["ep22imagchannel"])))
		headkeys.extend(("AUinvRratio_fU_re", "AUinvRratio_fU_im"))
		replace_value_list.extend(("keep_last_good_value", "keep_last_good_value"))
	# These are needed for kappa_C and f_cc
	if (compute_kappac or compute_fcc or compute_fs or compute_srcq) and not compute_exact_kappas:
		channel_list.extend(((instrument, ChannelNames["c0f2realchannel"]), (instrument, ChannelNames["c0f2imagchannel"]), (instrument, ChannelNames["df2realchannel"]), (instrument, ChannelNames["df2imagchannel"]), (instrument, ChannelNames["atf2realchannel"]), (instrument, ChannelNames["atf2imagchannel"]), (instrument, ChannelNames["apf2realchannel"]), (instrument, ChannelNames["apf2imagchannel"]), (instrument, ChannelNames["auf2realchannel"]), (instrument, ChannelNames["auf2imagchannel"]))) if "c0f2realchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep6realchannel"]), (instrument, ChannelNames["ep6imagchannel"]), (instrument, ChannelNames["ep7realchannel"]), (instrument, ChannelNames["ep7imagchannel"]), (instrument, ChannelNames["ep8realchannel"]), (instrument, ChannelNames["ep8imagchannel"]), (instrument, ChannelNames["ep18realchannel"]), (instrument, ChannelNames["ep18imagchannel"]), (instrument, ChannelNames["ep19realchannel"]), (instrument, ChannelNames["ep19imagchannel"])))
		headkeys.extend(("C0_f2_re", "C0_f2_im", "D_f2_re", "D_f2_im", "AT_f2_re", "AT_f2_im", "AP_f2_re", "AP_f2_im", "AU_f2_re", "AU_f2_im"))
		replace_value_list.extend(("keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value"))

	# These are needed if we compute the optical spring frequency and/or Q-factor of the Signal Recycling Cavity (SRC)
	if (compute_fs or compute_srcq) and not compute_exact_kappas:
		channel_list.extend(((instrument, ChannelNames["c0f1realchannel"]), (instrument, ChannelNames["c0f1imagchannel"]), (instrument, ChannelNames["df1realchannel"]), (instrument, ChannelNames["df1imagchannel"]), (instrument, ChannelNames["atf1realchannel"]), (instrument, ChannelNames["atf1imagchannel"]), (instrument, ChannelNames["apf1realchannel"]), (instrument, ChannelNames["apf1imagchannel"]), (instrument, ChannelNames["auf1realchannel"]), (instrument, ChannelNames["auf1imagchannel"]))) if "c0f1realchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep11realchannel"]), (instrument, ChannelNames["ep11imagchannel"]), (instrument, ChannelNames["ep12realchannel"]), (instrument, ChannelNames["ep12imagchannel"]), (instrument, ChannelNames["ep13realchannel"]), (instrument, ChannelNames["ep13imagchannel"]), (instrument, ChannelNames["ep20realchannel"]), (instrument, ChannelNames["ep20imagchannel"]), (instrument, ChannelNames["ep21realchannel"]), (instrument, ChannelNames["ep21imagchannel"])))
		headkeys.extend(("C0_f1_re", "C0_f1_im", "D_f1_re", "D_f1_im", "AT_f1_re", "AT_f1_im", "AP_f1_re", "AP_f1_im", "AU_f1_re", "AU_f1_im"))
		replace_value_list.extend(("keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value"))

	if compute_exact_kappas:
		if "C0_f2_re" not in headkeys:
			channel_list.extend(((instrument, ChannelNames["c0f2realchannel"]), (instrument, ChannelNames["c0f2imagchannel"]))) if "c0f2realchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep6realchannel"]), (instrument, ChannelNames["ep6imagchannel"])))
			headkeys.extend(("C0_f2_re", "C0_f2_im"))
			replace_value_list.extend(("keep_last_good_value", "keep_last_good_value"))
		if "C0_f1_re" not in headkeys:
			channel_list.extend(((instrument, ChannelNames["c0f1realchannel"]), (instrument, ChannelNames["c0f1imagchannel"]))) if "c0f1realchannel" in ChannelNames else channel_list.extend(((instrument, ChannelNames["ep11realchannel"]), (instrument, ChannelNames["ep11imagchannel"])))
			headkeys.extend(("C0_f1_re", "C0_f1_im"))
			replace_value_list.extend(("keep_last_good_value", "keep_last_good_value"))
		if "c0datf1realchannel" in ChannelNames or "ep25realchannel" in ChannelNames:
			if "c0datf1realchannel" in ChannelNames:
				channel_list.extend(((instrument, ChannelNames["c0datf1realchannel"]), (instrument, ChannelNames["c0datf1imagchannel"]), (instrument, ChannelNames["c0dapf1realchannel"]), (instrument, ChannelNames["c0dapf1imagchannel"]), (instrument, ChannelNames["c0dauf1realchannel"]), (instrument, ChannelNames["c0dauf1imagchannel"]), (instrument, ChannelNames["c0datf2realchannel"]), (instrument, ChannelNames["c0datf2imagchannel"]), (instrument, ChannelNames["c0dapf2realchannel"]), (instrument, ChannelNames["c0dapf2imagchannel"]), (instrument, ChannelNames["c0dauf2realchannel"]), (instrument, ChannelNames["c0dauf2imagchannel"]), (instrument, ChannelNames["c0at0ftrealchannel"]), (instrument, ChannelNames["c0at0ftimagchannel"]), (instrument, ChannelNames["c0datftrealchannel"]), (instrument, ChannelNames["c0datftimagchannel"]), (instrument, ChannelNames["c0dapftrealchannel"]), (instrument, ChannelNames["c0dapftimagchannel"]), (instrument, ChannelNames["c0dauftrealchannel"]), (instrument, ChannelNames["c0dauftimagchannel"]), (instrument, ChannelNames["c0ap0fprealchannel"]), (instrument, ChannelNames["c0ap0fpimagchannel"]), (instrument, ChannelNames["c0datfprealchannel"]), (instrument, ChannelNames["c0datfpimagchannel"]), (instrument, ChannelNames["c0dapfprealchannel"]), (instrument, ChannelNames["c0dapfpimagchannel"]), (instrument, ChannelNames["c0daufprealchannel"]), (instrument, ChannelNames["c0daufpimagchannel"]), (instrument, ChannelNames["c0au0furealchannel"]), (instrument, ChannelNames["c0au0fuimagchannel"]), (instrument, ChannelNames["c0datfurealchannel"]), (instrument, ChannelNames["c0datfuimagchannel"]), (instrument, ChannelNames["c0dapfurealchannel"]), (instrument, ChannelNames["c0dapfuimagchannel"]), (instrument, ChannelNames["c0daufurealchannel"]), (instrument, ChannelNames["c0daufuimagchannel"])))
			else:
				channel_list.extend(((instrument, ChannelNames["ep25realchannel"]), (instrument, ChannelNames["ep25imagchannel"]), (instrument, ChannelNames["ep26realchannel"]), (instrument, ChannelNames["ep26imagchannel"]), (instrument, ChannelNames["ep27realchannel"]), (instrument, ChannelNames["ep27imagchannel"]), (instrument, ChannelNames["ep28realchannel"]), (instrument, ChannelNames["ep28imagchannel"]), (instrument, ChannelNames["ep29realchannel"]), (instrument, ChannelNames["ep29imagchannel"]), (instrument, ChannelNames["ep30realchannel"]), (instrument, ChannelNames["ep30imagchannel"]), (instrument, ChannelNames["ep31realchannel"]), (instrument, ChannelNames["ep31imagchannel"]), (instrument, ChannelNames["ep32realchannel"]), (instrument, ChannelNames["ep32imagchannel"]), (instrument, ChannelNames["ep33realchannel"]), (instrument, ChannelNames["ep33imagchannel"]), (instrument, ChannelNames["ep34realchannel"]), (instrument, ChannelNames["ep34imagchannel"]), (instrument, ChannelNames["ep35realchannel"]), (instrument, ChannelNames["ep35imagchannel"]), (instrument, ChannelNames["ep36realchannel"]), (instrument, ChannelNames["ep36imagchannel"]), (instrument, ChannelNames["ep37realchannel"]), (instrument, ChannelNames["ep37imagchannel"]), (instrument, ChannelNames["ep38realchannel"]), (instrument, ChannelNames["ep38imagchannel"]), (instrument, ChannelNames["ep39realchannel"]), (instrument, ChannelNames["ep39imagchannel"]), (instrument, ChannelNames["ep40realchannel"]), (instrument, ChannelNames["ep40imagchannel"]), (instrument, ChannelNames["ep41realchannel"]), (instrument, ChannelNames["ep41imagchannel"]), (instrument, ChannelNames["ep42realchannel"]), (instrument, ChannelNames["ep42imagchannel"])))
			headkeys.extend(("C0DAT_f1_re", "C0DAT_f1_im", "C0DAP_f1_re", "C0DAP_f1_im", "C0DAU_f1_re", "C0DAU_f1_im", "C0DAT_f2_re", "C0DAT_f2_im", "C0DAP_f2_re", "C0DAP_f2_im", "C0DAU_f2_re", "C0DAU_f2_im", "C0AT0_fT_re", "C0AT0_fT_im", "C0DAT_fT_re", "C0DAT_fT_im", "C0DAP_fT_re", "C0DAP_fT_im", "C0DAU_fT_re", "C0DAU_fT_im", "C0AP0_fP_re", "C0AP0_fP_im", "C0DAT_fP_re", "C0DAT_fP_im", "C0DAP_fP_re", "C0DAP_fP_im", "C0DAU_fP_re", "C0DAU_fP_im", "C0AU0_fU_re", "C0AU0_fU_im", "C0DAT_fU_re", "C0DAT_fU_im", "C0DAP_fU_re", "C0DAP_fU_im", "C0DAU_fU_re", "C0DAU_fU_im"))
			replace_value_list.extend(("keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value", "keep_last_good_value"))

	# If we are using pre-computed coherence to gate kappas
if use_coherence:
	if compute_any_tdcfs:
		channel_list.append((instrument, ChannelNames["cohuncpcalyline1channel"]))
		headkeys.append("pcaly_line1_coh")
		replace_value_list.append(1)
		if ChannelNames["pcalline1amplitudechannel"] != "None" if "pcalline1amplitudechannel" in ChannelNames else False:
			channel_list.append((instrument, ChannelNames["pcalline1amplitudechannel"]))
			headkeys.append("pcal_line1_amp")
			replace_value_list.append("keep_last_good_value")

	if compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		channel_list.append((instrument, ChannelNames["cohuncsusline3channel"]))
		headkeys.append("sus_line3_coh")
		replace_value_list.append(1)
		if ChannelNames["susline3amplitudechannel"] != "None" if "susline3amplitudechannel" in ChannelNames else False:
			channel_list.append((instrument, ChannelNames["susline3amplitudechannel"]))
			headkeys.append("sus_line3_amp")
			replace_value_list.append("keep_last_good_value")
	if compute_kappapum or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		channel_list.append((instrument, ChannelNames["cohuncsusline2channel"]))
		headkeys.append("sus_line2_coh")
		replace_value_list.append(1)
		if ChannelNames["susline2amplitudechannel"] != "None" if "susline2amplitudechannel" in ChannelNames else False:
			channel_list.append((instrument, ChannelNames["susline2amplitudechannel"]))
			headkeys.append("sus_line2_amp")
			replace_value_list.append("keep_last_good_value")
	if compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		channel_list.append((instrument, ChannelNames["cohuncsusline1channel"]))
		headkeys.append("sus_line1_coh")
		replace_value_list.append(1)
		if ChannelNames["susline1amplitudechannel"] != "None" if "susline1amplitudechannel" in ChannelNames else False:
			channel_list.append((instrument, ChannelNames["susline1amplitudechannel"]))
			headkeys.append("sus_line1_amp")
			replace_value_list.append("keep_last_good_value")
	if compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		channel_list.append((instrument, ChannelNames["cohuncpcalyline2channel"]))
		headkeys.append("pcaly_line2_coh")
		replace_value_list.append(1)
		if ChannelNames["pcalline2amplitudechannel"] != "None" if "pcalline2amplitudechannel" in ChannelNames else False:
			channel_list.append((instrument, ChannelNames["pcalline2amplitudechannel"]))
			headkeys.append("pcal_line2_amp")
			replace_value_list.append("keep_last_good_value")

# We also need excitation channels for computing kappas
if compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq:
	channel_list.append((instrument, ChannelNames["tstexcchannel"]))
	headkeys.append("tstexc")
	replace_value_list.append(input_min)
	# If reading EPICS from the frames, also read the line frequency from the frames if we can
	if not factors_from_filters_file and "tstexclinefreqchannel" in ChannelNames:
		if ChannelNames["tstexclinefreqchannel"] != "None":
			channel_list.append((instrument, ChannelNames["tstexclinefreqchannel"]))
			headkeys.append("tstexc_linefreq")
			replace_value_list.append("keep_last_good_value")
if compute_kappapum or (compute_kappac or compute_fcc or compute_fs or compute_srcq):
	channel_list.append((instrument, ChannelNames["pumexcchannel"]))
	headkeys.append("pumexc")
	replace_value_list.append(input_min)
	# If reading EPICS from the frames, also read the line frequency from the frames if we can
	if not factors_from_filters_file and "pumexclinefreqchannel" in ChannelNames:
		if ChannelNames["pumexclinefreqchannel"] != "None":
			channel_list.append((instrument, ChannelNames["pumexclinefreqchannel"]))
			headkeys.append("pumexc_linefreq")
			replace_value_list.append("keep_last_good_value")
if compute_kappauim or (compute_kappac or compute_fcc or compute_fs or compute_srcq):
	channel_list.append((instrument, ChannelNames["uimexcchannel"]))
	headkeys.append("uimexc")
	replace_value_list.append(input_min)
	# If reading EPICS from the frames, also read the line frequency from the frames if we can
	if not factors_from_filters_file and "uimexclinefreqchannel" in ChannelNames:
		if ChannelNames["uimexclinefreqchannel"] != "None":
			channel_list.append((instrument, ChannelNames["uimexclinefreqchannel"]))
			headkeys.append("uimexc_linefreq")
			replace_value_list.append("keep_last_good_value")

# We need the PCAL channel if we are computing \kappas or removing the calibration lines
if compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq:
	channel_list.append((instrument, ChannelNames["pcalchannel"]))
	headkeys.append("pcal")
	replace_value_list.append(input_min)
	# If reading EPICS from the frames, also read the line frequencies from the frames if we can.
	# We need the pcal correction factors as well if we are going to update pcal line frequencies.
	if not factors_from_filters_file and "pcalline1freqchannel" in ChannelNames and "pcalline1corrrealchannel" in ChannelNames and "pcalline1corrimagchannel" in ChannelNames:
		if ChannelNames["pcalline1freqchannel"] != "None" and ChannelNames["pcalline1corrrealchannel"] != "None" and ChannelNames["pcalline1corrimagchannel"] != "None":
			channel_list.append((instrument, ChannelNames["pcalline1freqchannel"]))
			headkeys.append("pcal1_linefreq")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline1corrrealchannel"]))
			headkeys.append("pcal1_line_corr_real")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline1corrimagchannel"]))
			headkeys.append("pcal1_line_corr_imag")
			replace_value_list.append("keep_last_good_value")
	if not factors_from_filters_file and "pcalline2freqchannel" in ChannelNames and "pcalline2corrrealchannel" in ChannelNames and "pcalline2corrimagchannel" in ChannelNames:
		if ChannelNames["pcalline2freqchannel"] != "None" and ChannelNames["pcalline2corrrealchannel"] != "None" and ChannelNames["pcalline2corrimagchannel"] != "None":
			channel_list.append((instrument, ChannelNames["pcalline2freqchannel"]))
			headkeys.append("pcal2_linefreq")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline2corrrealchannel"]))
			headkeys.append("pcal2_line_corr_real")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline2corrimagchannel"]))
			headkeys.append("pcal2_line_corr_imag")
			replace_value_list.append("keep_last_good_value")
	if not factors_from_filters_file and "pcalline3freqchannel" in ChannelNames and "pcalline3corrrealchannel" in ChannelNames and "pcalline3corrimagchannel" in ChannelNames:
		if ChannelNames["pcalline3freqchannel"] != "None" and ChannelNames["pcalline3corrrealchannel"] != "None" and ChannelNames["pcalline3corrimagchannel"] != "None":
			channel_list.append((instrument, ChannelNames["pcalline3freqchannel"]))
			headkeys.append("pcal3_linefreq")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline3corrrealchannel"]))
			headkeys.append("pcal3_line_corr_real")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline3corrimagchannel"]))
			headkeys.append("pcal3_line_corr_imag")
			replace_value_list.append("keep_last_good_value")
	if not factors_from_filters_file and "pcalline4freqchannel" in ChannelNames and "pcalline4corrrealchannel" in ChannelNames and "pcalline4corrimagchannel" in ChannelNames:
		if ChannelNames["pcalline4freqchannel"] != "None" and ChannelNames["pcalline4corrrealchannel"] != "None" and ChannelNames["pcalline4corrimagchannel"] != "None":
			channel_list.append((instrument, ChannelNames["pcalline4freqchannel"]))
			headkeys.append("pcal4_linefreq")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline4corrrealchannel"]))
			headkeys.append("pcal4_line_corr_real")
			replace_value_list.append("keep_last_good_value")
			channel_list.append((instrument, ChannelNames["pcalline4corrimagchannel"]))
			headkeys.append("pcal4_line_corr_imag")
			replace_value_list.append("keep_last_good_value")

# For full calibration we need DARM_ERR and DARM_CTRL as our input channels
if CalibrationConfigs["calibrationmode"] == "Full":
	channel_list.extend(((instrument, ChannelNames["darmerrchannel"]), (instrument, ChannelNames["darmctrlchannel"])))
	headkeys.extend(("res", "ctrl"))
	replace_value_list.extend((0, 0))
# For partial calibration we need DELTAL_TST, DELTAL_PUM, DELTAL_UIM, and DELTAL_RES
elif CalibrationConfigs["calibrationmode"] == "Partial":
	channel_list.extend(((instrument, ChannelNames["deltalreschannel"]), (instrument, ChannelNames["deltaltstchannel"]), (instrument, ChannelNames["deltalpumchannel"]), (instrument, ChannelNames["deltaluimchannel"])))
	headkeys.extend(("res", "tst", "pum", "uim"))
	replace_value_list.extend((0, 0, 0, 0))
	if test_filters:
		# Then we need DARM_ERR and DARM_CTRL to test filters
		channel_list.extend(((instrument, ChannelNames["darmerrchannel"]), (instrument, ChannelNames["darmctrlchannel"])))
		headkeys.extend(("darm_err", "ctrl"))
		replace_value_list.extend((0, 0))
	elif compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq:
		# We also need DARM_ERR if we are computing the kappas
		channel_list.append((instrument, ChannelNames["darmerrchannel"]))
		headkeys.append("darm_err")
		replace_value_list.append(0)

# If we are removing lines using witness channels (e.g., 60 Hz lines and harmonics), add the witness channels
line_witness_channel_list = []
line_witness_freqs = []
line_witness_freq_vars = []
line_subtraction_filter_time = None
line_witness_tf_median_time = None
line_witness_tf_averaging_time = None
if (ChannelNames["linewitnesschannellist"] != "None") if "linewitnesschannellist" in ChannelNames else False:
	line_witness_channel_list = ChannelNames["linewitnesschannellist"].split(';')
	line_witness_freqs = DataCleaningConfigs["linewitnessfreqs"].split(';')
	line_witness_freq_vars = DataCleaningConfigs["linewitnessfreqvars"].split(';')
	line_subtraction_filter_time = int(DataCleaningConfigs["linesubtractionfiltertime"]) if "linesubtractionfiltertime" in DataCleaningConfigs else 20
	line_witness_tf_median_time = float(DataCleaningConfigs["linewitnesstfmediantime"])
	line_witness_tf_averaging_time = float(DataCleaningConfigs["linewitnesstfaveragingtime"])
	line_witness_tf_min_time = float(DataCleaningConfigs["linewitnesstfmintime"]) if "linewitnesstfmintime" in DataCleaningConfigs else 0
	line_witness_tf_secular_change_threshold = float(DataCleaningConfigs["linewitnesstfsecularchangethreshold"]) if "linewitnesstfsecularchangethreshold" in DataCleaningConfigs else 1.3
	for i in range(len(line_witness_channel_list)):
		line_witness_channel_list[i] = line_witness_channel_list[i].split(',')
		for j in range(len(line_witness_channel_list[i])):
			if (instrument, line_witness_channel_list[i][j]) not in channel_list:
				channel_list.append((instrument, line_witness_channel_list[i][j]))
				headkeys.append(line_witness_channel_list[i][j])
				replace_value_list.append(input_min)
	for i in range(len(line_witness_freqs)):
		line_witness_freqs[i] = line_witness_freqs[i].split(',')
		for j in range(len(line_witness_freqs[i])):
			line_witness_freqs[i][j] = float(line_witness_freqs[i][j])
	for i in range(len(line_witness_freq_vars)):
		line_witness_freq_vars[i] = float(line_witness_freq_vars[i])
	# Channels that contain frequencies of lines to be removed
	if ChannelNames["linewitnessfrequencychannellist"] != "None" if "linewitnessfrequencychannellist" in ChannelNames else False:
		line_witness_frequency_channel_list = ChannelNames["linewitnessfrequencychannellist"].split(';')
		for i in range(len(line_witness_frequency_channel_list)):
			line_witness_frequency_channel_list[i] = line_witness_frequency_channel_list[i].split(',')
			for j in range(len(line_witness_frequency_channel_list[i])):
				if line_witness_frequency_channel_list[i][j] == "None":
					line_witness_frequency_channel_list[i][j] = None
				elif is_number(line_witness_frequency_channel_list[i][j]):
					line_witness_frequency_channel_list[i][j] = float(line_witness_frequency_channel_list[i][j])
				elif (instrument, line_witness_frequency_channel_list[i][j]) not in channel_list:
					channel_list.append((instrument, line_witness_frequency_channel_list[i][j]))
					headkeys.append(line_witness_frequency_channel_list[i][j])
					replace_value_list.append("keep_last_good_value")
				else:
					line_witness_frequency_channel_list[i][j] = headkeys[channel_list.index((instrument, line_witness_frequency_channel_list[i][j]))]
	else:
		line_witness_frequency_channel_list = []
		for i in range(len(line_witness_freqs)):
			line_witness_frequency_channel_list.append([])
			for j in range(len(line_witness_freqs[i])):
				line_witness_frequency_channel_list[i].append(None)
	if ChannelNames["lineamplitudelist"] != "None" if "lineamplitudelist" in ChannelNames else False:
		line_amplitude_list = ChannelNames["lineamplitudelist"].split(';')
		for i in range(len(line_amplitude_list)):
			line_amplitude_list[i] = line_amplitude_list[i].split(',')
			for j in range(len(line_amplitude_list[i])):
				if line_amplitude_list[i][j] == "None":
					line_amplitude_list[i][j] = None
				elif (instrument, line_amplitude_list[i][j]) not in channel_list:
					channel_list.append((instrument, line_amplitude_list[i][j]))
					headkeys.append(line_amplitude_list[i][j])
					replace_value_list.append("keep_last_good_value")
				else:
					line_amplitude_list[i][j] = headkeys[channel_list.index((instrument, line_amplitude_list[i][j]))]
	else:
		line_amplitude_list = []
		for i in range(len(line_witness_freqs)):
			line_amplitude_list.append([])
			for j in range(len(line_witness_freqs[i])):
				line_amplitude_list[i].append(None)
else:
	line_witness_frequency_channel_list = []
	line_amplitude_list = []
line_amplitude_names = copy.deepcopy(line_amplitude_list)

# If we are using witness channels to clean h(t), add those to the channel list
witness_channel_list = []
if ChannelNames["witnesschannellist"] != "None":
	witness_channel_list = ChannelNames["witnesschannellist"].split(';')
	witness_notch_frequencies = DataCleaningConfigs["witnessnotchfrequencies"].split(';')
	witness_rates = SampleRates["witnesschannelsr"].split(';')
	if 'noisesubfftwindowtypes' in DataCleaningConfigs:
		noisesub_fft_window_types = DataCleaningConfigs['noisesubfftwindowtypes'].split(';')
	else:
		noisesub_fft_window_types = []
		for i in range(len(witness_channel_list)):
			noisesub_fft_window_types.append('dpss')
	if 'noisesubfirwindowtypes' in DataCleaningConfigs:
		noisesub_fir_window_types = DataCleaningConfigs['noisesubfirwindowtypes'].split(';')
	else:
		noisesub_fir_window_types = []
		for i in range(len(witness_channel_list)):
			noisesub_fir_window_types.append('dpss')
	witness_highpasses = DataCleaningConfigs['witnesshighpasses'].split(';') if "witnesshighpasses" in DataCleaningConfigs else list(15 * numpy.ones(len(witness_channel_list)))
	if len(witness_channel_list) != len(witness_notch_frequencies) or len(witness_channel_list) != len(witness_rates) or len(witness_channel_list) != len(noisesub_fft_window_types) or len(witness_channel_list) != len(noisesub_fir_window_types):
		raise ValueError("WitnessChannelList, WitnessChannelSR, WitnessNotchFrequencies, NoiseSubFFTWindowTypes, and NoiseSubFIRWindowTypes must all be the same length, i.e., they must all have the same number of semicolons (;)")
	for i in range(len(witness_channel_list)):
		witness_channel_list[i] = witness_channel_list[i].split(',')
		for j in range(len(witness_channel_list[i])):
			channel_list.append((instrument, witness_channel_list[i][j]))
			headkeys.append(witness_channel_list[i][j])
			replace_value_list.append(input_min)
		witness_rates[i] = int(witness_rates[i])
		witness_highpasses[i] = float(witness_highpasses[i])
		witness_notch_frequencies[i] = witness_notch_frequencies[i].split(',')
		if len(witness_notch_frequencies[i]) <= 1:
			witness_notch_frequencies[i] = []
		else:
			for j in range(len(witness_notch_frequencies[i])):
				witness_notch_frequencies[i][j] = float(witness_notch_frequencies[i][j])

# If we are gating the noise or 60 Hz line subtraction with something other than the ODC state vector, add that channel
if compute_calib_statevector:
	noisesub_gate_channel = ChannelNames["noisesubgatechannel"] if "noisesubgatechannel" in ChannelNames else lownoise_channel_name 
	noisesub_gate_bitmask = int(Bitmasks["noisesubgatebitmask"]) if "noisesubgatebitmask" in Bitmasks else lownoise_bitmask
	noisesub_gate_threshold = (int(Bitmasks["noisesubgatethreshold"]) if Bitmasks["noisesubgatethreshold"] != "None" else None) if "noisesubgatethreshold" in Bitmasks else None
	noisesub_gate_value = (int(Bitmasks["noisesubgatevalue"]) if Bitmasks["noisesubgatevalue"] != "None" else None) if "noisesubgatevalue" in Bitmasks else None
if compute_calib_statevector and (any(line_witness_channel_list) or any(witness_channel_list)) and noisesub_gate_channel != obsintent_channel_name and noisesub_gate_channel != lownoise_channel_name and noisesub_gate_channel != hwinj_channel_name and noisesub_gate_channel != filterclock_channel_name and noisesub_gate_channel != undisturbed_ok_channel and (noisesub_gate_bitmask > 0 or noisesub_gate_threshold is not None or noisesub_gate_value is not None) and noisesub_gate_channel != "None" and not noisesub_use_filter_clock:
	channel_list.append((instrument, noisesub_gate_channel))
	headkeys.append("noisesubgatechannel")
	replace_value_list.append(0)

nonsenssubtractionchannel = ChannelNames["nonsenssubtractionchannel"] if "nonsenssubtractionchannel" in ChannelNames else None
if nonsenssubtractionchannel == "None":
	nonsenssubtractionchannel = None
if nonsenssubtractionchannel is not None:
	channel_list.append((instrument, nonsenssubtractionchannel))
	headkeys.append("nonsenssubtractionchannel")
	replace_value_list.append(0)

####################################################################################################
####################################### Main Pipeline ##############################################
####################################################################################################

# Get information about start time and, if applicable, end time.
if options.gps_start_time is not None and options.gps_end_time is not None:
	gps_start_time = int(options.gps_start_time)
	gps_end_time = int(options.gps_end_time)

if InputConfigs["datasource"] == "frames":
	start = gps_start_time
elif InputConfigs["datasource"] == "lvshm":
	tm = time.gmtime()
	start = int(lal.UTCToGPS(tm))

# Gst Pipeline
pipeline = Gst.Pipeline(name="gstlal_compute_strain")
mainloop = GLib.MainLoop.new(None, True)
if kafka_server is not None:
	handler = calibhandler.Handler(mainloop, pipeline, kafka_server = kafka_server, verbose = verbose, latency_suffix = latency_suffix)
else:
	print("Using simple handler", file=sys.stderr)
	handler = simplehandler.Handler(mainloop, pipeline)

# 
# Turn off verboseness if requested
#

if not verbose:
	pipeparts.mkprogressreport = lambda pipeline, src, *args: src

#
# Set the length in seconds that data can be held in queues
#

queue_length = float(PipelineConfigs["maxqueuelength"]) if "maxqueuelength" in PipelineConfigs else 0.0

#
# Read in data from frames or shared memory
#

if InputConfigs["datasource"] == "lvshm": # Data is to be read from shared memory; "low-latency" mode
	src = pipeparts.mklvshmsrc(pipeline, shm_name = InputConfigs["shmpartition"], assumed_duration = input_frame_duration)
elif InputConfigs["datasource"] == "frames": # Data is to be read from frame files; "offline" mode
	src = pipeparts.mklalcachesrc(pipeline, location = options.frame_cache, cache_dsc_regex = instrument, use_mmap = True)
	if ci_latency_dir is not None:
		src = pipeparts.mkgeneric(pipeline, calibration_parts.mkqueue(pipeline, src), "lal_pacer")

if(test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None):
	src = pipeparts.mktee(pipeline, src)
	src_latency = pipeparts.mklatency(pipeline, src, name = "%s_src" % OutputConfigs["frametype"], silent = silent)
	if kafka_server is not None:
		src_latency.connect("notify::current-latency", handler.latency_new_buffer)
	pipeparts.mkfakesink(pipeline, src_latency)

if ci_latency_dir is not None:
	src = pipeparts.mkgeneric(pipeline, src, "splitcounter", filename = "%s/input_unix_timestamps.txt" % ci_latency_dir)

#
# Hook up the relevant channels to the demuxer
#

demux = pipeparts.mkframecppchanneldemux(pipeline, src, do_file_checksum = file_check_sum, skip_bad_files = skip_bad_files, channel_list = list(map("%s:%s".__mod__, channel_list)))

# Write the pipeline graph after pads have been hooked up to the demuxer
if DebuggingConfigs["pipelinegraphfilename"] != "None":
	demux.connect("no-more-pads", write_graph)	

# Get everything hooked up and fill in discontinuities
for key, chan, replace_value in zip(headkeys, channel_list, replace_value_list):
	head_dict[key] = calibration_parts.hook_up(pipeline, demux, chan[1], instrument, float(PipelineConfigs["bufferlength"]), wait_time = wait_time, input_min = input_min, input_max = input_max, replace_value = replace_value)

# The DARM_ERR channel follows different paths if we're doing full vs. partial calibration
if CalibrationConfigs["calibrationmode"] == "Full":
	darm_err = calibration_parts.caps_and_progress(pipeline, head_dict["res"], hoft_caps, "darm_err")
	derrtee = pipeparts.mktee(pipeline, darm_err)
elif compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_srcq or compute_fs or test_filters:
	darm_err = calibration_parts.caps_and_progress(pipeline, head_dict["darm_err"], hoft_caps, "darm_err")
	derrtee = pipeparts.mktee(pipeline, darm_err)

# pcal excitation channel, which will be demodulated
if compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_srcq or compute_fs:
	pcal = calibration_parts.caps_and_progress(pipeline, head_dict["pcal"], hoft_caps, "pcal")
	pcaltee = pipeparts.mktee(pipeline, pcal)
	# Check if we are also using pcal as a witness for line subtraction
	for i in range(len(line_witness_channel_list)):
		for j in range(len(line_witness_channel_list[i])):
			if line_witness_channel_list[i][j] == ChannelNames["pcalchannel"]:
				line_witness_channel_list[i][j] = pcaltee

#
# TIME-VARYING FACTORS COMPUTATIONS
#

epics_dict = {}
for key in headkeys:
	if key.endswith("linefreq") or key in [item for sublist in line_witness_frequency_channel_list for item in sublist]:
		head_dict[key] = calibration_parts.caps_and_progress(pipeline, head_dict[key], ref_factors_caps, key)
		head_dict[key] = pipeparts.mkgeneric(pipeline, head_dict[key], "lal_property", update_when_change = True, name = "lal_property_%s" % key)
		for i in range(len(line_witness_frequency_channel_list)):
			if key in line_witness_frequency_channel_list[i]:
				j = line_witness_frequency_channel_list[i].index(key)
				line_witness_frequency_channel_list[i][j] = head_dict[key]
	elif key in [item for sublist in line_amplitude_list for item in sublist]:
		head_dict[key] = calibration_parts.caps_and_progress(pipeline, head_dict[key], line_amplitude_caps, key)
		head_dict[key] = pipeparts.mktee(pipeline, head_dict[key])
		for i in range(len(line_amplitude_list)):
			if key in line_amplitude_list[i]:
				j = line_amplitude_list[i].index(key)
				line_amplitude_list[i][j] = head_dict[key]
	elif key.endswith("line_corr_real") or key.endswith("line_corr_imag"):
		head_dict[key] = calibration_parts.caps_and_progress(pipeline, head_dict[key], ref_factors_caps, key)
		head_dict[key] = calibration_parts.multiply_constant(pipeline, head_dict[key], pcal_sign, 0.0)
		head_dict[key] = pipeparts.mkgeneric(pipeline, head_dict[key], "lal_property", update_when_change = True, name = "lal_property_%s" % key)
	elif len(key.split('_')) == 3 and key.split('_')[1][0] == 'f' and (key.split('_')[2] == 're' or key.split('_')[2] == 'im'):
		# It's an EPICS channel
		epics_dict[key] = calibration_parts.caps_and_progress(pipeline, head_dict[key], ref_factors_caps, key)
		epics_dict[key] = calibration_parts.mkresample(pipeline, epics_dict[key], 0, False, compute_calib_factors_caps)
		epics_dict[key] = (pipeparts.mktee(pipeline, epics_dict[key]), float(filters[key]) if key in filters else numpy.inf)

tdcfs_use_amp = False
if use_coherence:
	if compute_any_tdcfs:
		pcaly_line1_coh = calibration_parts.caps_and_progress(pipeline, head_dict["pcaly_line1_coh"], coh_caps, "pcaly_line1_coh")
		pcaly_line1_coh = calibration_parts.mkresample(pipeline, pcaly_line1_coh, 0, False, compute_calib_factors_caps)
		pcaly_line1_coh = pipeparts.mktee(pipeline, pcaly_line1_coh)
		if "pcal_line1_amp" in headkeys:
			tdcfs_use_amp = True
			# We may have already done this for the line subtraction.
			if "pcal_line1_amp" in [item for sublist in line_amplitude_names for item in sublist]:
				pcal_line1_amp = head_dict["pcal_line1_amp"]
			else:
				pcal_line1_amp = calibration_parts.caps_and_progress(pipeline, head_dict["pcal_line1_amp"], line_amplitude_caps, "pcal_line1_amp")
				pcal_line1_amp = pipeparts.mktee(pipeline, pcal_line1_amp)
	if compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		sus_line3_coh = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line3_coh"], coh_caps, "sus_line3_coh")
		sus_line3_coh = calibration_parts.mkresample(pipeline, sus_line3_coh, 0, False, compute_calib_factors_caps)
		sus_line3_coh = pipeparts.mktee(pipeline, sus_line3_coh)
		if "sus_line3_amp" in headkeys:
			tdcfs_use_amp = True
			# We may have already done this for the line subtraction.
			if "sus_line3_amp" in [item for sublist in line_amplitude_names for item in sublist]:
				sus_line3_amp = head_dict["sus_line3_amp"]
			else:
				sus_line3_amp = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line3_amp"], line_amplitude_caps, "sus_line3_amp")
				sus_line3_amp = pipeparts.mktee(pipeline, sus_line3_amp)
	if compute_kappapum or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		sus_line2_coh = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line2_coh"], coh_caps, "sus_line2_coh")
		sus_line2_coh = calibration_parts.mkresample(pipeline, sus_line2_coh, 0, False, compute_calib_factors_caps)
		sus_line2_coh = pipeparts.mktee(pipeline, sus_line2_coh)
		if "sus_line2_amp" in headkeys:
			tdcfs_use_amp = True
			# We may have already done this for the line subtraction.
			if "sus_line2_amp" in [item for sublist in line_amplitude_names for item in sublist]:
				sus_line2_amp = head_dict["sus_line2_amp"]
			else:
				sus_line2_amp = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line2_amp"], line_amplitude_caps, "sus_line2_amp")
				sus_line2_amp = pipeparts.mktee(pipeline, sus_line2_amp)
	if compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		sus_line1_coh = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line1_coh"], coh_caps, "sus_line1_coh")
		sus_line1_coh = calibration_parts.mkresample(pipeline, sus_line1_coh, 0, False, compute_calib_factors_caps)
		sus_line1_coh = pipeparts.mktee(pipeline, sus_line1_coh)
		if "sus_line1_amp" in headkeys:
			tdcfs_use_amp = True
			# We may have already done this for the line subtraction.
			if "sus_line1_amp" in [item for sublist in line_amplitude_names for item in sublist]:
				sus_line1_amp = head_dict["sus_line1_amp"]
			else:
				sus_line1_amp = calibration_parts.caps_and_progress(pipeline, head_dict["sus_line1_amp"], line_amplitude_caps, "sus_line1_amp")
				sus_line1_amp = pipeparts.mktee(pipeline, sus_line1_amp)
	if compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
		pcaly_line2_coh = calibration_parts.caps_and_progress(pipeline, head_dict["pcaly_line2_coh"], coh_caps, "pcaly_line2_coh")
		pcaly_line2_coh = calibration_parts.mkresample(pipeline, pcaly_line2_coh, 0, False, compute_calib_factors_caps)
		pcaly_line2_coh = pipeparts.mktee(pipeline, pcaly_line2_coh)
		if "pcal_line2_amp" in headkeys:
			tdcfs_use_amp = True
			# We may have already done this for the line subtraction.
			if "pcal_line2_amp" in [item for sublist in line_amplitude_names for item in sublist]:
				pcal_line2_amp = head_dict["pcal_line2_amp"]
			else:
				pcal_line2_amp = calibration_parts.caps_and_progress(pipeline, head_dict["pcal_line2_amp"], line_amplitude_caps, "pcal_line2_amp")
				pcal_line2_amp = pipeparts.mktee(pipeline, pcal_line2_amp)

if compute_kappatst or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
	tstexccaps = "audio/x-raw, format=F64LE, rate=%d" % tst_exc_sr
	tstexc = calibration_parts.caps_and_progress(pipeline, head_dict["tstexc"], tstexccaps, "tstexc")
	tstexc = pipeparts.mktee(pipeline, tstexc)
	# Check if we are also using the TST excitation channel as a witness for line subtraction
	for i in range(len(line_witness_channel_list)):
		for j in range(len(line_witness_channel_list[i])):
			if line_witness_channel_list[i][j] == ChannelNames["tstexcchannel"]:
				line_witness_channel_list[i][j] = tstexc
if compute_kappapum or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
	pumexccaps = "audio/x-raw, format=F64LE, rate=%d" % pum_exc_sr
	pumexc = calibration_parts.caps_and_progress(pipeline, head_dict["pumexc"], pumexccaps, "pumexc")
	pumexc = pipeparts.mktee(pipeline, pumexc)
	# Check if we are also using the PUM excitation channel as a witness for line subtraction
	for i in range(len(line_witness_channel_list)):
		for j in range(len(line_witness_channel_list[i])):
			if line_witness_channel_list[i][j] == ChannelNames["pumexcchannel"]:
				line_witness_channel_list[i][j] = pumexc
if compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq or (compute_any_tdcfs and compute_exact_kappas):
	uimexccaps = "audio/x-raw, format=F64LE, rate=%d" % uim_exc_sr
	uimexc = calibration_parts.caps_and_progress(pipeline, head_dict["uimexc"], uimexccaps, "uimexc")
	uimexc = pipeparts.mktee(pipeline, uimexc)
	# Check if we are also using the UIM excitation channel as a witness for line subtraction
	for i in range(len(line_witness_channel_list)):
		for j in range(len(line_witness_channel_list[i])):
			if line_witness_channel_list[i][j] == ChannelNames["uimexcchannel"]:
				line_witness_channel_list[i][j] = uimexc

# Set up computations for kappa_tst, kappa_pum, kappa_uim, kappa_pu,kappa_c, f_cc, f_s, and Q, if applicable
if compute_exact_kappas and (compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_srcq or compute_fs):
	# Use the "exact" algebraic solution of P1900052, Section 5.2.6.
	# Do all the demodulation up front.  We need DARM_ERR and either Pcal or an actuator
	# injection channel at each calibration line frequency used for the TDCFs.  We will
	# also take the ratios injection(f) / DARM_ERR(f) at all the calibration lines and
	# apply the gating and smoothing here, before computing the TDCFs.

	# Since every TDCF depends on all the lines, we will gate every line ratio with all the line coherences.
	if use_coherence:
		max_coh = calibration_parts.mkinterleave(pipeline, [pcaly_line1_coh, pcaly_line2_coh, sus_line1_coh, sus_line2_coh, sus_line3_coh])
		max_coh = pipeparts.mkgeneric(pipeline, max_coh, "lal_minmax", mode = 2)
		max_coh = pipeparts.mktee(pipeline, max_coh)
		# Gate with line amplitude channels if given
		if tdcfs_use_amp:
			amp_list = []
			if "pcal_line1_amp" in headkeys:
				amp_list.append(pcal_line1_amp)
			if "pcal_line2_amp" in headkeys:
				amp_list.append(pcal_line2_amp)
			if "sus_line1_amp" in headkeys:
				amp_list.append(sus_line1_amp)
			if "sus_line2_amp" in headkeys:
				amp_list.append(sus_line2_amp)
			if "sus_line3_amp" in headkeys:
				amp_list.append(sus_line3_amp)
			if len(amp_list) > 1:
				tdcfs_line_amp = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, amp_list))
			elif len(amp_list) == 1:
				tdcfs_line_amp = amp_list[0]
			else:
				raise ValueError("Code should not be reached.")
		if tdcf_gate_with_excitation:
			# Make sure d_err and all needed excitation channels are available, and gate if one or more are not
			derr_exists = calibration_parts.mkresample(pipeline, derrtee, 3, False, calibstate_sr)
			derr_exists = pipeparts.mkbitvectorgen(pipeline, derr_exists, threshold = 1.0001 * input_min, bit_vector = 1)
			derr_exists = pipeparts.mkcapsfilter(pipeline, derr_exists, calibstate_caps)
			pcal_exists = calibration_parts.mkresample(pipeline, pcaltee, 3, False, calibstate_sr)
			pcal_exists = pipeparts.mkbitvectorgen(pipeline, pcal_exists, threshold = 1.0001 * input_min, bit_vector = 1)
			pcal_exists = pipeparts.mkcapsfilter(pipeline, pcal_exists, calibstate_caps)
			tstexc_exists = calibration_parts.mkresample(pipeline, tstexc, 3, False, calibstate_sr)
			tstexc_exists = pipeparts.mkbitvectorgen(pipeline, tstexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
			tstexc_exists = pipeparts.mkcapsfilter(pipeline, tstexc_exists, calibstate_caps)
			pumexc_exists = calibration_parts.mkresample(pipeline, pumexc, 3, False, calibstate_sr)
			pumexc_exists = pipeparts.mkbitvectorgen(pipeline, pumexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
			pumexc_exists = pipeparts.mkcapsfilter(pipeline, pumexc_exists, calibstate_caps)
			uimexc_exists = calibration_parts.mkresample(pipeline, uimexc, 3, False, calibstate_sr)
			uimexc_exists = pipeparts.mkbitvectorgen(pipeline, uimexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
			uimexc_exists = pipeparts.mkcapsfilter(pipeline, uimexc_exists, calibstate_caps)
			tdcf_excitations_exist = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [derr_exists, pcal_exists, tstexc_exists, pumexc_exists, uimexc_exists]))


	# First, the actuation Pcal line, starting with DARM_ERR.
	derr_at_act_pcal_freq = calibration_parts.demodulate(pipeline, derrtee, act_pcal_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pcal1_linefreq"] if "pcal1_linefreq" in head_dict else None)
	# Now Pcal
	pcal_at_act_pcal_freq = calibration_parts.demodulate(pipeline, pcaltee, act_pcal_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, prefactor_real = pcal_sign * pcal_corr_at_act_freq_real, prefactor_imag = pcal_sign * pcal_corr_at_act_freq_imag, freq_update = [head_dict["pcal1_linefreq"], head_dict["pcal1_line_corr_real"], head_dict["pcal1_line_corr_imag"]] if "pcal1_linefreq" in head_dict else None)
	pcal_at_act_pcal_freq = pipeparts.mktee(pipeline, pcal_at_act_pcal_freq)

	# Take the ratio X1 = Pcal(f1) / DARM_ERR(f1)
	X1 = calibration_parts.complex_division(pipeline, pcal_at_act_pcal_freq, derr_at_act_pcal_freq)

	# Now the gating and smoothing
	if use_coherence:
		X1 = calibration_parts.mkgate(pipeline, X1, max_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'pcal_line1_gate', queue_length = queue_length)
		if tdcfs_use_amp:
			X1 = calibration_parts.mkgate(pipeline, X1, tdcfs_line_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'pcal_line1_amp_gate', queue_length = queue_length)
		if tdcf_gate_with_excitation:
			X1 = calibration_parts.mkgate(pipeline, X1, tdcf_excitations_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'pcal_line1_excitation_exists_gate', queue_length = queue_length)
		X1 = calibration_parts.smooth_complex_kappas(pipeline, X1, R_f1_re, R_f1_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
	else:
		var = 0.2 * abs(R_f1_re + 1j * R_f1_im)
		X1 = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, X1, var, var, R_f1_re, R_f1_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	# The sensing Pcal line (~400 Hz), starting with DARM_ERR.
	derr_at_opt_gain_freq = calibration_parts.demodulate(pipeline, derrtee, opt_gain_fcc_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pcal2_linefreq"] if "pcal2_linefreq" in head_dict else None)
	# Now Pcal
	pcal_at_opt_gain_freq = calibration_parts.demodulate(pipeline, pcaltee, opt_gain_fcc_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, prefactor_real = pcal_sign * pcal_corr_at_opt_gain_fcc_freq_real, prefactor_imag = pcal_sign * pcal_corr_at_opt_gain_fcc_freq_imag, freq_update = [head_dict["pcal2_linefreq"], head_dict["pcal2_line_corr_real"], head_dict["pcal2_line_corr_imag"]] if "pcal2_linefreq" in head_dict else None)
	pcal_at_opt_gain_freq = pipeparts.mktee(pipeline, pcal_at_opt_gain_freq)

	# Take the ratio X2 = Pcal(f2) / DARM_ERR(f2)
	X2 = calibration_parts.complex_division(pipeline, pcal_at_opt_gain_freq, derr_at_opt_gain_freq)

	# Now the gating and smoothing
	if use_coherence:
		X2 = calibration_parts.mkgate(pipeline, X2, max_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'pcal_line2_gate', queue_length = queue_length)
		if tdcfs_use_amp:
			X2 = calibration_parts.mkgate(pipeline, X2, tdcfs_line_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'pcal_line2_amp_gate', queue_length = queue_length)
		if tdcf_gate_with_excitation:
			X2 = calibration_parts.mkgate(pipeline, X2, tdcf_excitations_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'pcal_line2_excitation_exists_gate', queue_length = queue_length)
		X2 = calibration_parts.smooth_complex_kappas(pipeline, X2, R_f2_re, R_f2_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
	else:
		var = 0.2 * abs(R_f2_re + 1j * R_f2_im)
		X2 = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, X2, var, var, R_f2_re, R_f2_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	X2 = pipeparts.mktee(pipeline, X2)

	# Now the ESD/TST/L3 actuator line, starting with DARM_ERR.
	derr_at_esd_act_freq = calibration_parts.demodulate(pipeline, derrtee, esd_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["tstexc_linefreq"] if "tstexc_linefreq" in head_dict else None)
	# Now the TST excitation channel
	tstexc_at_esd_act_freq = calibration_parts.demodulate(pipeline, tstexc, esd_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["tstexc_linefreq"] if "tstexc_linefreq" in head_dict else None)
	tstexc_at_esd_act_freq = pipeparts.mktee(pipeline, tstexc_at_esd_act_freq)

	# Take the ratio X_T = TST_exc(f_T) / DARM_ERR(f_T)
	X_T = calibration_parts.complex_division(pipeline, tstexc_at_esd_act_freq, derr_at_esd_act_freq)

	# Now the gating and smoothing
	if use_coherence:
		X_T = calibration_parts.mkgate(pipeline, X_T, max_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'sus_line3_gate', queue_length = queue_length)
		if tdcfs_use_amp:
			X_T = calibration_parts.mkgate(pipeline, X_T, tdcfs_line_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line3_amp_gate', queue_length = queue_length)
		if tdcf_gate_with_excitation:
			X_T = calibration_parts.mkgate(pipeline, X_T, tdcf_excitations_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line3_excitation_exists_gate', queue_length = queue_length)
		X_T = calibration_parts.smooth_complex_kappas(pipeline, X_T, RAT0inv_fT_re, RAT0inv_fT_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
	else:
		var = 0.2 * abs(RAT0inv_fT_re + 1j * RAT0inv_fT_im)
		X_T = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, X_T, var, var, RAT0inv_fT_re, RAT0inv_fT_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	# Now the PUM/L2 actuator line, starting with DARM_ERR.
	derr_at_pum_act_freq = calibration_parts.demodulate(pipeline, derrtee, pum_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pumexc_linefreq"] if "pumexc_linefreq" in head_dict else None)
	# Now the PUM excitation channel
	pumexc_at_pum_act_freq = calibration_parts.demodulate(pipeline, pumexc, pum_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pumexc_linefreq"] if "pumexc_linefreq" in head_dict else None)
	pumexc_at_pum_act_freq = pipeparts.mktee(pipeline, pumexc_at_pum_act_freq)

	# Take the ratio X_P = PUM_exc(f_P) / DARM_ERR(f_P)
	X_P = calibration_parts.complex_division(pipeline, pumexc_at_pum_act_freq, derr_at_pum_act_freq)

	# Now the gating and smoothing
	if use_coherence:
		X_P = calibration_parts.mkgate(pipeline, X_P, max_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'sus_line2_gate', queue_length = queue_length)
		if tdcfs_use_amp:
			X_P = calibration_parts.mkgate(pipeline, X_P, tdcfs_line_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line2_amp_gate', queue_length = queue_length)
		if tdcf_gate_with_excitation:
			X_P = calibration_parts.mkgate(pipeline, X_P, tdcf_excitations_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line2_excitation_exists_gate', queue_length = queue_length)
		X_P = calibration_parts.smooth_complex_kappas(pipeline, X_P, RAP0inv_fP_re, RAP0inv_fP_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
	else:
		var = 0.2 * abs(RAP0inv_fP_re + 1j * RAP0inv_fP_im)
		X_P = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, X_P, var, var, RAP0inv_fP_re, RAP0inv_fP_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	# Now the UIM/L3 actuator line, starting with DARM_ERR.
	derr_at_uim_act_freq = calibration_parts.demodulate(pipeline, derrtee, uim_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["uimexc_linefreq"] if "uimexc_linefreq" in head_dict else None)
	# Now the UIM excitation channel
	uimexc_at_uim_act_freq = calibration_parts.demodulate(pipeline, uimexc, uim_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["uimexc_linefreq"] if "uimexc_linefreq" in head_dict else None)
	uimexc_at_uim_act_freq = pipeparts.mktee(pipeline, uimexc_at_uim_act_freq)

	# Take the ratio X_U = UIM_exc(f_U) / DARM_ERR(f_U)
	X_U = calibration_parts.complex_division(pipeline, uimexc_at_uim_act_freq, derr_at_uim_act_freq)

	# Now the gating and smoothing
	if use_coherence:
		X_U = calibration_parts.mkgate(pipeline, X_U, max_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'sus_line1_gate', queue_length = queue_length)
		if tdcfs_use_amp:
			X_U = calibration_parts.mkgate(pipeline, X_U, tdcfs_line_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line1_amp_gate', queue_length = queue_length)
		if tdcf_gate_with_excitation:
			X_U = calibration_parts.mkgate(pipeline, X_U, tdcf_excitations_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'sus_line1_excitation_exists_gate', queue_length = queue_length)
		X_U = calibration_parts.smooth_complex_kappas(pipeline, X_U, RAU0inv_fU_re, RAU0inv_fU_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
	else:
		var = 0.2 * abs(RAU0inv_fU_re + 1j * RAU0inv_fU_im)
		X_U = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, X_U, var, var, RAU0inv_fU_re, RAU0inv_fU_im, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	# Finally, compute the kappas
	X_list = [X1, X2, X_T, X_P, X_U]
	freq_list = [act_pcal_line_freq, opt_gain_fcc_line_freq, esd_act_line_freq, pum_act_line_freq, uim_act_line_freq]
	EPICS_list = [C0_f1_re, C0_f1_im, C0DAT_f1_re, C0DAT_f1_im, C0DAP_f1_re, C0DAP_f1_im, C0DAU_f1_re, C0DAU_f1_im, C0_f2_re, C0_f2_im, C0DAT_f2_re, C0DAT_f2_im, C0DAP_f2_re, C0DAP_f2_im, C0DAU_f2_re, C0DAU_f2_im, C0AT0_fT_re, C0AT0_fT_im, C0DAT_fT_re, C0DAT_fT_im, C0DAP_fT_re, C0DAP_fT_im, C0DAU_fT_re, C0DAU_fT_im, C0AP0_fP_re, C0AP0_fP_im, C0DAT_fP_re, C0DAT_fP_im, C0DAP_fP_re, C0DAP_fP_im, C0DAU_fP_re, C0DAU_fP_im, C0AU0_fU_re, C0AU0_fU_im, C0DAT_fU_re, C0DAT_fU_im, C0DAP_fU_re, C0DAP_fU_im, C0DAU_fU_re, C0DAU_fU_im]

	freq_channel_list = None if factors_from_filters_file else [head_dict["pcal1_linefreq"], head_dict["pcal2_linefreq"], head_dict["tstexc_linefreq"], head_dict["pumexc_linefreq"], head_dict["uimexc_linefreq"]]
	EPICS_channel_list = None if factors_from_filters_file else [epics_dict["C0_f1_re"][0], epics_dict["C0_f1_im"][0], epics_dict["C0DAT_f1_re"][0], epics_dict["C0DAT_f1_im"][0], epics_dict["C0DAP_f1_re"][0], epics_dict["C0DAP_f1_im"][0], epics_dict["C0DAU_f1_re"][0], epics_dict["C0DAU_f1_im"][0], epics_dict["C0_f2_re"][0], epics_dict["C0_f2_im"][0], epics_dict["C0DAT_f2_re"][0], epics_dict["C0DAT_f2_im"][0], epics_dict["C0DAP_f2_re"][0], epics_dict["C0DAP_f2_im"][0], epics_dict["C0DAU_f2_re"][0], epics_dict["C0DAU_f2_im"][0], epics_dict["C0AT0_fT_re"][0], epics_dict["C0AT0_fT_im"][0], epics_dict["C0DAT_fT_re"][0], epics_dict["C0DAT_fT_im"][0], epics_dict["C0DAP_fT_re"][0], epics_dict["C0DAP_fT_im"][0], epics_dict["C0DAU_fT_re"][0], epics_dict["C0DAU_fT_im"][0], epics_dict["C0AP0_fP_re"][0], epics_dict["C0AP0_fP_im"][0], epics_dict["C0DAT_fP_re"][0], epics_dict["C0DAT_fP_im"][0], epics_dict["C0DAP_fP_re"][0], epics_dict["C0DAP_fP_im"][0], epics_dict["C0DAU_fP_re"][0], epics_dict["C0DAU_fP_im"][0], epics_dict["C0AU0_fU_re"][0], epics_dict["C0AU0_fU_im"][0], epics_dict["C0DAT_fU_re"][0], epics_dict["C0DAT_fU_im"][0], epics_dict["C0DAP_fU_re"][0], epics_dict["C0DAP_fU_im"][0], epics_dict["C0DAU_fU_re"][0], epics_dict["C0DAU_fU_im"][0]]

	[ktst, kpum, kuim, tau_tst, tau_pum, tau_uim, kc, fcc, fs_squared, fs_over_Q] = calibration_parts.compute_exact_kappas(pipeline, X_list, freq_list, freq_channel_list, EPICS_list, EPICS_channel_list, compute_factors_sr, default_fcc = fcc_default, default_fs_squared = fs_squared_default, default_fs_over_Q = fs_over_Q_default, C2_model = TDCFs_use_C2_sensing_model, max_error = exact_sensing_tdcfs_max_error)

	if compute_tdcf_uncertainty:
		# Compute actuator kappa uncertainties.
		ktst_unc = calibration_parts.compute_act_stage_uncertainty(pipeline, pcaly_line1_coh, sus_line3_coh, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		ktst_unc = pipeparts.mktee(pipeline, ktst_unc)
		tau_tst_unc = calibration_parts.multiply_constant(pipeline, ktst_unc, 1.0 / (2.0 * numpy.pi * esd_act_line_freq), 0.0)
		kpum_unc = calibration_parts.compute_act_stage_uncertainty(pipeline, pcaly_line1_coh, sus_line2_coh, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		kpum_unc = pipeparts.mktee(pipeline, kpum_unc)
		tau_pum_unc = calibration_parts.multiply_constant(pipeline, kpum_unc, 1.0 / (2.0 * numpy.pi * esd_act_line_freq), 0.0)
		kuim_unc = calibration_parts.compute_act_stage_uncertainty(pipeline, pcaly_line1_coh, sus_line1_coh, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		kuim_unc = pipeparts.mktee(pipeline, kuim_unc)
		tau_uim_unc = calibration_parts.multiply_constant(pipeline, kuim_unc, 1.0 / (2.0 * numpy.pi * esd_act_line_freq), 0.0)

		# Compute sensing function uncertainties
		if factors_from_filters_file:
			[S_c, S_c_unc, fcc_unc] = calibration_parts.compute_S_c_uncertainty_from_filters_file(pipeline, C0_f2_re, C0_f2_im, D_f2_re, D_f2_im, opt_gain_fcc_line_freq, X2, pcaly_line2_coh, AT_f2_re, AT_f2_im, ktst, tau_tst, apply_complex_kappatst, ktst_unc, AP_f2_re, AP_f2_im, kpum, tau_pum, apply_complex_kappapum, kpum_unc, AU_f2_re, AU_f2_im, kuim, tau_uim, apply_complex_kappauim, kuim_unc, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		else:
			D_f2 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["D_f2_re"][0], epics_dict["D_f2_im"][0]))
			AT_f2 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AT_f2_re"][0], epics_dict["AT_f2_im"][0]))
			AP_f2 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AP_f2_re"][0], epics_dict["AP_f2_im"][0]))
			AU_f2 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AU_f2_re"][0], epics_dict["AU_f2_im"][0]))
			[S_c, S_c_unc, fcc_unc] = calibration_parts.compute_S_c_uncertainty(pipeline, C0_f2, D_f2, opt_gain_fcc_line_freq, X2, pcaly_line2_coh, AT_f2, ktst, tau_tst, apply_complex_kappatst, ktst_unc, AP_f2, kpum, tau_pum, apply_complex_kappapum, kpum_unc, AU_f2, kuim, tau_uim, apply_complex_kappauim, kuim_unc, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		kc_unc = S_c_unc
		fcc_unc = pipeparts.mktee(pipeline, fcc_unc)
		fcc_unc_abs = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, fcc_unc, fcc), queue_length = [queue_length])

		# Compute SRC uncertainties
		if factors_from_filters_file:
			[S_s_inv, fs_squared_unc_abs, fs_over_Q_unc_abs] = calibration_parts.compute_SRC_uncertainty_from_filters_file(pipeline, C0_f1_re, C0_f1_im, kc, kc_unc, fcc, fcc_unc, D_f1_re, D_f1_im, act_pcal_line_freq, X1, pcaly_line1_coh, AT_f1_re, AT_f1_im, ktst, tau_tst, apply_complex_kappatst, ktst_unc, AP_f1_re, AP_f1_im, kpum, tau_pum, apply_complex_kappapum, kpum_unc, AU_f1_re, AU_f1_im, kuim, tau_uim, apply_complex_kappauim, kuim_unc, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)
		else:
			D_f1 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["D_f1_re"][0], epics_dict["D_f1_im"][0]))
			AT_f1 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AT_f1_re"][0], epics_dict["AT_f1_im"][0]))
			AP_f1 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AP_f1_re"][0], epics_dict["AP_f1_im"][0]))
			AU_f1 = pipeparts.mktee(pipeline, calibration_parts.merge_into_complex(pipeline, epics_dict["AU_f1_re"][0], epics_dict["AU_f1_im"][0]))
			[S_s_inv, fs_squared_unc_abs, fs_over_Q_unc_abs] = calibration_parts.compute_SRC_uncertainty(pipeline, C0_f1, kc, kc_unc, fcc, fcc_unc, D_f1, act_pcal_line_freq, X1, pcaly_line1_coh, AT_f1, ktst, tau_tst, apply_complex_kappatst, ktst_unc, AP_f1, kpum, tau_pum, apply_complex_kappapum, kpum_unc, AU_f1, kuim, tau_uim, apply_complex_kappauim, kuim_unc, coherence_samples, integration_samples, median_smoothing_samples, factors_average_samples, coherence_unc_threshold)

	# A little bit of processing is necessary to ensure backwards compatibility
	if compute_kappatst:
		smooth_ktst_magnitude_tee = ktst
		smooth_ktsttee = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [calibration_parts.mktypecast(pipeline, ktst, "Z128LE"), pipeparts.mkgeneric(pipeline, calibration_parts.multiply_constant(pipeline, calibration_parts.mktypecast(pipeline, tau_tst, "Z128LE"), 0.0, 2.0 * numpy.pi * esd_act_line_freq), "cexp")], queue_length = [queue_length]))
		smooth_ktstR, smooth_ktstI = calibration_parts.split_into_real(pipeline, smooth_ktsttee)
		smooth_ktstRtee = pipeparts.mktee(pipeline, smooth_ktstR)
		smooth_ktstItee = pipeparts.mktee(pipeline, smooth_ktstI)
	else:
		pipeparts.mkfakesink(pipeline, ktst)
		pipeparts.mkfakesink(pipeline, tau_tst)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, tau_tst_unc)

	if compute_kappapum:
		smooth_kpum_magnitude_tee = kpum
		smooth_kpumtee = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [calibration_parts.mktypecast(pipeline, kpum, "Z128LE"), pipeparts.mkgeneric(pipeline, calibration_parts.multiply_constant(pipeline, calibration_parts.mktypecast(pipeline, tau_pum, "Z128LE"), 0.0, 2.0 * numpy.pi * pum_act_line_freq), "cexp")], queue_length = [queue_length]))
		smooth_kpumR, smooth_kpumI = calibration_parts.split_into_real(pipeline, smooth_kpumtee)
		smooth_kpumRtee = pipeparts.mktee(pipeline, smooth_kpumR)
		smooth_kpumItee = pipeparts.mktee(pipeline, smooth_kpumI)
	else:
		pipeparts.mkfakesink(pipeline, kpum)
		pipeparts.mkfakesink(pipeline, tau_pum)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, tau_pum_unc)

	if compute_kappauim:
		smooth_kuim_magnitude_tee = kuim
		smooth_kuimtee = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [calibration_parts.mktypecast(pipeline, kuim, "Z128LE"), pipeparts.mkgeneric(pipeline, calibration_parts.multiply_constant(pipeline, calibration_parts.mktypecast(pipeline, tau_uim, "Z128LE"), 0.0, 2.0 * numpy.pi * uim_act_line_freq), "cexp")], queue_length = [queue_length]))
		smooth_kuimR, smooth_kuimI = calibration_parts.split_into_real(pipeline, smooth_kuimtee)
		smooth_kuimRtee = pipeparts.mktee(pipeline, smooth_kuimR)
		smooth_kuimItee = pipeparts.mktee(pipeline, smooth_kuimI)
	else:
		pipeparts.mkfakesink(pipeline, kuim)
		pipeparts.mkfakesink(pipeline, tau_uim)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, tau_uim_unc)

	if compute_kappac:
		smooth_kctee = pipeparts.mktee(pipeline, kc)
	else:
		pipeparts.mkfakesink(pipeline, kc)

	if compute_fcc:
		smooth_fcctee = pipeparts.mktee(pipeline, fcc)
	else:
		pipeparts.mkfakesink(pipeline, fcc)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, fcc_unc_abs)

	if compute_fs:
		smooth_fs_squared = pipeparts.mktee(pipeline, fs_squared)
	else:
		pipeparts.mkfakesink(pipeline, fs_squared)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, fs_squared_unc_abs)

	if compute_srcq:
		# fs / Q is a real-valued quantity, but fs and Q are not necessarily, so we compute a complex-valued 1/Q.
		fs_over_Q = pipeparts.mktee(pipeline, fs_over_Q)
		smooth_srcQ_inv = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [calibration_parts.mktypecast(pipeline, fs_over_Q, "Z128LE"), calibration_parts.mkpow(pipeline, calibration_parts.mktypecast(pipeline, smooth_fs_squared, "Z128LE"), exponent = -0.5)], queue_length = [queue_length]))
		# We need a real-valued version of Q^(-1) to write the the frames.  If we have
		# an optical spring, the computed Q^(-1) is imaginary, and if we have an optical
		# antispring, the computed Q^(-1) is real.  To get a sensible real value either
		# way AND INCLUDE ANY MINUS SIGN, we use Re(Q^(-1)) + Im(Q^(-1)).
		smooth_srcQ_inv_real = pipeparts.mktee(pipeline, calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, pipeparts.mkgeneric(pipeline, smooth_srcQ_inv, "creal"), pipeparts.mkgeneric(pipeline, smooth_srcQ_inv, "cimag")), queue_length = [queue_length]))

	else:
		pipeparts.mkfakesink(pipeline, fs_over_Q)
		if compute_tdcf_uncertainty:
			pipeparts.mkfakesink(pipeline, fs_over_Q_unc_abs)

elif compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_srcq or compute_fs:
	# Use the solution in T1700106.
	# demodulate the PCAL channel and apply the PCAL correction factor at the actuation Pcal line frequency
	pcal_at_act_pcal_freq = calibration_parts.demodulate(pipeline, pcaltee, act_pcal_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, prefactor_real = pcal_sign * pcal_corr_at_act_freq_real, prefactor_imag = pcal_sign * pcal_corr_at_act_freq_imag, freq_update = [head_dict["pcal1_linefreq"], head_dict["pcal1_line_corr_real"], head_dict["pcal1_line_corr_imag"]] if "pcal1_linefreq" in head_dict else None)
	pcal_at_act_pcal_freq = pipeparts.mktee(pipeline, pcal_at_act_pcal_freq)

	# demodulate DARM_ERR at the actuation pcal line frequency
	derr_at_act_pcal_freq = calibration_parts.demodulate(pipeline, derrtee, act_pcal_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pcal1_linefreq"] if "pcal1_linefreq" in head_dict else None)
	derr_at_act_pcal_freq = pipeparts.mktee(pipeline, derr_at_act_pcal_freq)

	if tdcf_gate_with_excitation:
		derr_exists = calibration_parts.mkresample(pipeline, derrtee, 3, False, calibstate_sr)
		derr_exists = pipeparts.mkbitvectorgen(pipeline, derr_exists, threshold = 1.0001 * input_min, bit_vector = 1)
		derr_exists = pipeparts.mkcapsfilter(pipeline, derr_exists, calibstate_caps)
		pcal_exists = calibration_parts.mkresample(pipeline, pcaltee, 3, False, calibstate_sr)
		pcal_exists = pipeparts.mkbitvectorgen(pipeline, pcal_exists, threshold = 1.0001 * input_min, bit_vector = 1)
		pcal_exists = pipeparts.mkcapsfilter(pipeline, pcal_exists, calibstate_caps)
		derr_and_pcal_exist = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, [derr_exists, pcal_exists]))
		# Keep track of which of these has been computed
		excitations_exist = [derr_and_pcal_exist, None, None, None, None]

# Check whether we need to compute kappa_tst
if not compute_exact_kappas and (compute_kappatst or compute_kappac or compute_fcc or compute_srcq or compute_fs):
	# demodulate the TST excitation channel at the ESD actuation line frequency
	tstexc_at_esd_act_freq = calibration_parts.demodulate(pipeline, tstexc, esd_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["tstexc_linefreq"] if "tstexc_linefreq" in head_dict else None)
	tstexc_at_esd_act_freq = pipeparts.mktee(pipeline, tstexc_at_esd_act_freq)

	# demodulate DARM_ERR at the ESD actuation line frequency
	derr_at_esd_act_freq = calibration_parts.demodulate(pipeline, derrtee, esd_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["tstexc_linefreq"] if "tstexc_linefreq" in head_dict else None)

	# compute kappa_tst, either using reference factors from the filters file or reading them from EPICS channels
	if not factors_from_filters_file:
		ATinvRratio_fT = calibration_parts.merge_into_complex(pipeline, epics_dict["ATinvRratio_fT_re"][0], epics_dict["ATinvRratio_fT_im"][0])
		ktst = calibration_parts.compute_kappatst(pipeline, derr_at_esd_act_freq, tstexc_at_esd_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, ATinvRratio_fT)
	elif factors_from_filters_file:
		ktst = calibration_parts.compute_kappatst_from_filters_file(pipeline, derr_at_esd_act_freq, tstexc_at_esd_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, ATinvRratio_fT_re, ATinvRratio_fT_im)

	ktst = pipeparts.mktee(pipeline, ktst)

	# Now apply the gating and smoothing to \kappa_tst
	if compute_kappatst:
		if use_coherence:
			# Gate kappa_tst with the coherence of the PCALY_line1 line
			ktst_gated = calibration_parts.mkgate(pipeline, ktst, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'ktstgate1', queue_length = queue_length)
			# Gate kappa_tst with the coherence of the TST (L3) suspension line
			ktst_gated = calibration_parts.mkgate(pipeline, ktst_gated, sus_line3_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'ktstgate2', queue_length = queue_length)
			# Gate kappa_tst with the amplitude channels of the lines used.
			if "pcal_line1_amp" in headkeys:
				ktst_gated = calibration_parts.mkgate(pipeline, ktst_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'ktstgate3', queue_length = queue_length)
			if "sus_line3_amp" in headkeys:
				ktst_gated = calibration_parts.mkgate(pipeline, ktst_gated, sus_line3_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'ktstgate4', queue_length = queue_length)
			# Use gating to require that the needed input channels are available and nonzero.
			if tdcf_gate_with_excitation:
				tstexc_exists = calibration_parts.mkresample(pipeline, tstexc, 3, False, calibstate_sr)
				tstexc_exists = pipeparts.mkbitvectorgen(pipeline, tstexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
				tstexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, tstexc_exists, calibstate_caps))
				excitations_exist[3] = tstexc_exists
				ktst_gated = calibration_parts.mkgate(pipeline, ktst_gated, derr_and_pcal_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'ktstgate5', queue_length = queue_length)
				ktst_gated = calibration_parts.mkgate(pipeline, ktst_gated, tstexc_exists, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'ktstgate6', queue_length = queue_length)
			ktst_gated = pipeparts.mktee(pipeline, ktst_gated)

			# Smooth kappa_tst
			smooth_ktst = calibration_parts.smooth_complex_kappas(pipeline, ktst_gated, expected_kappatst_real, expected_kappatst_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		else:
			# Smooth kappa_tst
			smooth_ktst = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, ktst, kappatst_real_var, kappatst_imag_var, expected_kappatst_real, expected_kappatst_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		smooth_ktsttee = pipeparts.mktee(pipeline, smooth_ktst)
		smooth_ktstR, smooth_ktstI = calibration_parts.split_into_real(pipeline, smooth_ktsttee)

		smooth_ktstRtee = pipeparts.mktee(pipeline, smooth_ktstR)
		smooth_ktstItee = pipeparts.mktee(pipeline, smooth_ktstI)

		if compute_kappac or compute_fcc or compute_fs or compute_srcq or apply_kappatst and not apply_complex_kappatst:
			smooth_ktst_magnitude_tee = pipeparts.mktee(pipeline, pipeparts.mkgeneric(pipeline, smooth_ktsttee, "cabs"))
		if compute_kappac or compute_fcc or compute_fs or compute_srcq:
			smooth_tau_tst_tee = pipeparts.mktee(pipeline, calibration_parts.multiply_constant(pipeline, pipeparts.mkgeneric(pipeline, smooth_ktsttee, "carg"), 1.0 / (2.0 * numpy.pi * esd_act_line_freq), 0.0))

		if compute_tdcf_uncertainty:
			ktst_unc = pipeparts.mkgeneric(pipeline, ktst_gated if use_coherence else ktst, "lal_stdev", mode = 1, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
			ktst_unc = calibration_parts.compute_uncertainty_reduction(pipeline, ktst_unc, integration_samples, median_smoothing_samples, factors_average_samples)
			ktst_unc = pipeparts.mktee(pipeline, ktst_unc)
			tau_tst_unc = calibration_parts.multiply_constant(pipeline, ktst_unc, 1.0 / (2.0 * numpy.pi * esd_act_line_freq), 0.0)

# Check if we need to compute kappa_pum
if not compute_exact_kappas and (compute_kappapum or (compute_kappac or compute_fcc or compute_fs or compute_srcq)):
	# demodulate the PUM excitation channel at the PUM actuation line frequency
	pumexc_at_pum_act_freq = calibration_parts.demodulate(pipeline, pumexc, pum_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pumexc_linefreq"] if "pumexc_linefreq" in head_dict else None)
	pumexc_at_pum_act_freq = pipeparts.mktee(pipeline, pumexc_at_pum_act_freq)

	# demodulate DARM_ERR at the PUM actuation line frequency
	derr_at_pum_act_freq = calibration_parts.demodulate(pipeline, derrtee, pum_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pumexc_linefreq"] if "pumexc_linefreq" in head_dict else None)

	# Compute kappa_pum, either using reference factors from the filters file or reading them from EPICS channels
	if not factors_from_filters_file:
		APinvRratio_fP = calibration_parts.merge_into_complex(pipeline, epics_dict["APinvRratio_fP_re"][0], epics_dict["APinvRratio_fP_im"][0])
		kpum = calibration_parts.compute_kappapum(pipeline, derr_at_pum_act_freq, pumexc_at_pum_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, APinvRratio_fP)
	else:
		kpum = calibration_parts.compute_kappapum_from_filters_file(pipeline, derr_at_pum_act_freq, pumexc_at_pum_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, APinvRratio_fP_re, APinvRratio_fP_im)

	kpum = pipeparts.mktee(pipeline, kpum)

	# Now apply the gating and smoothing to kappa_pum
	if compute_kappapum:
		if use_coherence:
			# Gate kappa_pum with the coherence of the PCALY_line1 line
			kpum_gated = calibration_parts.mkgate(pipeline, kpum, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kpumgate1', queue_length = queue_length)
			# Gate kappa_pum with the coherence of the PUM (L2) suspension line
			kpum_gated = calibration_parts.mkgate(pipeline, kpum_gated, sus_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kpumgate2', queue_length = queue_length)
			# Gate kappa_pum with the amplitude channels of the lines used.
			if "pcal_line1_amp" in headkeys:
				kpum_gated = calibration_parts.mkgate(pipeline, kpum_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kpumgate3', queue_length = queue_length)
			if "sus_line2_amp" in headkeys:
				kpum_gated = calibration_parts.mkgate(pipeline, kpum_gated, sus_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kpumgate4', queue_length = queue_length)
			# Use gating to require that the needed input channels are available and nonzero.
			if tdcf_gate_with_excitation:
				pumexc_exists = calibration_parts.mkresample(pipeline, pumexc, 3, False, calibstate_sr)
				pumexc_exists = pipeparts.mkbitvectorgen(pipeline, pumexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
				pumexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, pumexc_exists, calibstate_caps))
				excitations_exist[2] = pumexc_exists
				kpum_gated = calibration_parts.mkgate(pipeline, kpum_gated, derr_and_pcal_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kpumgate5', queue_length = queue_length)
				kpum_gated = calibration_parts.mkgate(pipeline, kpum_gated, pumexc_exists, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kpumgate6', queue_length = queue_length)
			kpum_gated = pipeparts.mktee(pipeline, kpum_gated)

			# Smooth kappa_pum
			smooth_kpum = calibration_parts.smooth_complex_kappas(pipeline, kpum_gated, expected_kappapum_real, expected_kappapum_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		else:
			# Smooth kappa_pum
			smooth_kpum = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, kpum, kappapum_real_var, kappapum_imag_var, expected_kappapum_real, expected_kappapum_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		smooth_kpumtee = pipeparts.mktee(pipeline, smooth_kpum)
		smooth_kpumR, smooth_kpumI = calibration_parts.split_into_real(pipeline, smooth_kpumtee)

		smooth_kpumRtee = pipeparts.mktee(pipeline, smooth_kpumR)
		smooth_kpumItee = pipeparts.mktee(pipeline, smooth_kpumI)

		if compute_kappac or compute_fcc or compute_fs or compute_srcq or apply_kappapum and not apply_complex_kappapum:
			smooth_kpum_magnitude_tee = pipeparts.mktee(pipeline, pipeparts.mkgeneric(pipeline, smooth_kpumtee, "cabs"))
		if compute_kappac or compute_fcc or compute_fs or compute_srcq:
			smooth_tau_pum_tee = pipeparts.mktee(pipeline, calibration_parts.multiply_constant(pipeline, pipeparts.mkgeneric(pipeline, smooth_kpumtee, "carg"), 1.0 / (2.0 * numpy.pi * pum_act_line_freq), 0.0))

		if compute_tdcf_uncertainty:
			kpum_unc = pipeparts.mkgeneric(pipeline, kpum_gated if use_coherence else kpum, "lal_stdev", mode = 1, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
			kpum_unc = calibration_parts.compute_uncertainty_reduction(pipeline, kpum_unc, integration_samples, median_smoothing_samples, factors_average_samples)
			kpum_unc = pipeparts.mktee(pipeline, kpum_unc)
			tau_pum_unc = calibration_parts.multiply_constant(pipeline, kpum_unc, 1.0 / (2.0 * numpy.pi * pum_act_line_freq), 0.0)

# Check if we need to compute kappa_uim
if not compute_exact_kappas and (compute_kappauim or (compute_kappac or compute_fcc or compute_fs or compute_srcq)):
	# Demodulate DARM_ERR and the UIM excitation channel at the UIM actuation line frequency
	derr_at_uim_act_freq = calibration_parts.demodulate(pipeline, derrtee, uim_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["uimexc_linefreq"] if "uimexc_linefreq" in head_dict else None)
	uimexc_at_uim_act_freq = calibration_parts.demodulate(pipeline, uimexc, uim_act_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["uimexc_linefreq"] if "uimexc_linefreq" in head_dict else None)
	uimexc_at_uim_act_freq = pipeparts.mktee(pipeline, uimexc_at_uim_act_freq)

	# Compute kappa_uim, either using reference factors from the filters file or reading them from EPICS channels
	if not factors_from_filters_file:
		AUinvRratio_fU = calibration_parts.merge_into_complex(pipeline, epics_dict["AUinvRratio_fU_re"][0], epics_dict["AUinvRratio_fU_im"][0])
		kuim = calibration_parts.compute_kappauim(pipeline, derr_at_uim_act_freq, uimexc_at_uim_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, AUinvRratio_fU)
	else:
		kuim = calibration_parts.compute_kappauim_from_filters_file(pipeline, derr_at_uim_act_freq, uimexc_at_uim_act_freq, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, AUinvRratio_fU_re, AUinvRratio_fU_im)

	kuim = pipeparts.mktee(pipeline, kuim)

	# Now apply the gating and smoothing to kappa_uim
	if compute_kappauim:
		if use_coherence:
			# Gate kappa_uim with the coherence of the PCALY_line1 line
			kuim_gated = calibration_parts.mkgate(pipeline, kuim, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kuimgate1', queue_length = queue_length)
			# Gate kappa_uim with the coherence of the UIM (L1) suspension line
			kuim_gated = calibration_parts.mkgate(pipeline, kuim_gated, sus_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kuimgate2', queue_length = queue_length)
			# Gate kappa_uim with the amplitude channels of the lines used.
			if "pcal_line1_amp" in headkeys:
				kuim_gated = calibration_parts.mkgate(pipeline, kuim_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kuimgate3', queue_length = queue_length)
			if "sus_line1_amp" in headkeys:
				kuim_gated = calibration_parts.mkgate(pipeline, kuim_gated, sus_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kuimgate4', queue_length = queue_length)
			# Use gating to require that the needed input channels are available and nonzero.
			if tdcf_gate_with_excitation:
				uimexc_exists = calibration_parts.mkresample(pipeline, uimexc, 3, False, calibstate_sr)
				uimexc_exists = pipeparts.mkbitvectorgen(pipeline, uimexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
				uimexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, uimexc_exists, calibstate_caps))
				excitations_exist[1] = uimexc_exists
				kuim_gated = calibration_parts.mkgate(pipeline, kuim_gated, derr_and_pcal_exist, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kuimgate5', queue_length = queue_length)
				kuim_gated = calibration_parts.mkgate(pipeline, kuim_gated, uimexc_exists, 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kuimgate6', queue_length = queue_length)
			kuim_gated = pipeparts.mktee(pipeline, kuim_gated)

			# Smooth kappa_uim
			smooth_kuim = calibration_parts.smooth_complex_kappas(pipeline, kuim_gated, expected_kappauim_real, expected_kappauim_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		else:
			# Smooth kappa_uim
			smooth_kuim = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, kuim, kappauim_real_var, kappauim_imag_var, expected_kappauim_real, expected_kappauim_imag, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		smooth_kuimtee = pipeparts.mktee(pipeline, smooth_kuim)
		smooth_kuimR, smooth_kuimI = calibration_parts.split_into_real(pipeline, smooth_kuimtee)

		smooth_kuimRtee = pipeparts.mktee(pipeline, smooth_kuimR)
		smooth_kuimItee = pipeparts.mktee(pipeline, smooth_kuimI)

		if compute_kappac or compute_fcc or compute_fs or compute_srcq or apply_kappauim and not apply_complex_kappauim:
			smooth_kuim_magnitude_tee = pipeparts.mktee(pipeline, pipeparts.mkgeneric(pipeline, smooth_kuimtee, "cabs"))
		if compute_kappac or compute_fcc or compute_fs or compute_srcq:
			smooth_tau_uim_tee = pipeparts.mktee(pipeline, calibration_parts.multiply_constant(pipeline, pipeparts.mkgeneric(pipeline, smooth_kuimtee, "carg"), 1.0 / (2.0 * numpy.pi * uim_act_line_freq), 0.0))

		if compute_tdcf_uncertainty:
			kuim_unc = pipeparts.mkgeneric(pipeline, kuim_gated if use_coherence else kuim, "lal_stdev", mode = 1, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
			kuim_unc = calibration_parts.compute_uncertainty_reduction(pipeline, kuim_unc, integration_samples, median_smoothing_samples, factors_average_samples)
			kuim_unc = pipeparts.mktee(pipeline, kuim_unc)
			tau_uim_unc = calibration_parts.multiply_constant(pipeline, kuim_unc, 1.0 / (2.0 * numpy.pi * uim_act_line_freq), 0.0)

# Compute \kappa_c and f_cc
if not compute_exact_kappas and (compute_kappac or compute_fcc or compute_fs or compute_srcq):
	# demodulate the PCAL channel and apply the PCAL correction factor at optical gain and f_cc line frequency
	pcal_at_opt_gain_freq = calibration_parts.demodulate(pipeline, pcaltee, opt_gain_fcc_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, prefactor_real = pcal_sign * pcal_corr_at_opt_gain_fcc_freq_real, prefactor_imag = pcal_sign * pcal_corr_at_opt_gain_fcc_freq_imag, freq_update = [head_dict["pcal2_linefreq"], head_dict["pcal2_line_corr_real"], head_dict["pcal2_line_corr_imag"]] if "pcal2_linefreq" in head_dict else None)
	pcal_at_opt_gain_freq = pipeparts.mktee(pipeline, pcal_at_opt_gain_freq)

	# demodulate DARM_ERR at optical gain and f_cc line frequency
	derr_at_opt_gain_freq = calibration_parts.demodulate(pipeline, derrtee, opt_gain_fcc_line_freq, td, compute_factors_sr, demodulation_filter_time, filter_latency_factor, freq_update = head_dict["pcal2_linefreq"] if "pcal2_linefreq" in head_dict else None)

	# Compute the factor S which will be used for the kappa_c and f_cc calculations
	if not factors_from_filters_file:
		C0_f2 = calibration_parts.merge_into_complex(pipeline, epics_dict["C0_f2_re"][0], epics_dict["C0_f2_im"][0])
		D_f2 = calibration_parts.merge_into_complex(pipeline, epics_dict["D_f2_re"][0], epics_dict["D_f2_im"][0])
		AT_f2 = calibration_parts.merge_into_complex(pipeline, epics_dict["AT_f2_re"][0], epics_dict["AT_f2_im"][0])
		AP_f2 = calibration_parts.merge_into_complex(pipeline, epics_dict["AP_f2_re"][0], epics_dict["AP_f2_im"][0])
		AU_f2 = calibration_parts.merge_into_complex(pipeline, epics_dict["AU_f2_re"][0], epics_dict["AU_f2_im"][0])
		S = calibration_parts.compute_S(pipeline, opt_gain_fcc_line_freq, C0_f2, pcal_at_opt_gain_freq, derr_at_opt_gain_freq, D_f2, esd_act_line_freq, ktst, apply_complex_kappatst, AT_f2, pum_act_line_freq, kpum, apply_complex_kappapum, AP_f2, uim_act_line_freq, kuim, apply_complex_kappauim, AU_f2)

	else:
		S = calibration_parts.compute_S_from_filters_file(pipeline, opt_gain_fcc_line_freq, C0_f2_re, C0_f2_im, pcal_at_opt_gain_freq, derr_at_opt_gain_freq, D_f2_re, D_f2_im, esd_act_line_freq, ktst, apply_complex_kappatst, AT_f2_re, AT_f2_im, pum_act_line_freq, kpum, apply_complex_kappapum, AP_f2_re, AP_f2_im, uim_act_line_freq, kuim, apply_complex_kappauim, AU_f2_re, AU_f2_im)

	S = pipeparts.mktee(pipeline, S)

	SR, SI = calibration_parts.split_into_real(pipeline, S)

	if compute_kappac and compute_fcc:
		SR = pipeparts.mktee(pipeline, SR)
		SI = pipeparts.mktee(pipeline, SI)

	# compute kappa_c
	if compute_kappac or compute_srcq or compute_fs:
		kc = calibration_parts.compute_kappac(pipeline, SR, SI)
	if compute_kappac:
		kc = pipeparts.mktee(pipeline, kc)
		if use_coherence:
			# Gate kappa_c with the coherence of all of the calibration lines used to compute it
			kc_gated = calibration_parts.mkgate(pipeline, kc, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kcgate1', queue_length = queue_length)
			kc_gated = calibration_parts.mkgate(pipeline, kc_gated, pcaly_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kcgate2', queue_length = queue_length)
			kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kcgate3', queue_length = queue_length)
			kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kcgate4', queue_length = queue_length)
			kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line3_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'kcgate5', queue_length = queue_length)
			# Gate kappa_c with the amplitude channels of the lines used.
			if "pcal_line1_amp" in headkeys:
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate6', queue_length = queue_length)
			if "pcal_line2_amp" in headkeys:
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, pcal_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate7', queue_length = queue_length)
			if "sus_line1_amp" in headkeys:
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate8', queue_length = queue_length)
			if "sus_line2_amp" in headkeys:
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate9', queue_length = queue_length)
			if "sus_line3_amp" in headkeys:
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, sus_line3_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate10', queue_length = queue_length)
			# Use gating to require that the needed input channels are available and nonzero.
			if tdcf_gate_with_excitation:
				if excitations_exist[1] == None:
					uimexc_exists = calibration_parts.mkresample(pipeline, uimexc, 3, False, calibstate_sr)
					uimexc_exists = pipeparts.mkbitvectorgen(pipeline, uimexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					uimexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, uimexc_exists, calibstate_caps))
					excitations_exist[1] = uimexc_exists
				if excitations_exist[2] == None:
					pumexc_exists = calibration_parts.mkresample(pipeline, pumexc, 3, False, calibstate_sr)
					pumexc_exists = pipeparts.mkbitvectorgen(pipeline, pumexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					pumexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, pumexc_exists, calibstate_caps))
					excitations_exist[2] = pumexc_exists
				if excitations_exist[3] == None:
					tstexc_exists = calibration_parts.mkresample(pipeline, tstexc, 3, False, calibstate_sr)
					tstexc_exists = pipeparts.mkbitvectorgen(pipeline, tstexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					tstexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, tstexc_exists, calibstate_caps))
					excitations_exist[3] = tstexc_exists
				excitations_exist[4] = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, excitations_exist[:4]))
				kc_gated = calibration_parts.mkgate(pipeline, kc_gated, excitations_exist[4], 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'kcgate11', queue_length = queue_length)
			kc_gated = pipeparts.mktee(pipeline, kc_gated)

			# Smooth kappa_c
			smooth_kc = calibration_parts.smooth_kappas(pipeline, kc_gated, expected_kappac, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		else:
			# Smooth kappa_c
			smooth_kc = calibration_parts.smooth_kappas_no_coherence(pipeline, kc, kappac_var, expected_kappac, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

		smooth_kctee = pipeparts.mktee(pipeline, smooth_kc)

		if compute_tdcf_uncertainty:
			kc_unc = pipeparts.mkgeneric(pipeline, kc_gated if use_coherence else kc, "lal_stdev", mode = 1, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
			kc_unc = calibration_parts.compute_uncertainty_reduction(pipeline, kc_unc, integration_samples, median_smoothing_samples, factors_average_samples)

	# compute f_cc
	if compute_fcc or compute_srcq or compute_fs:
		fcc = calibration_parts.compute_fcc(pipeline, SR, SI, opt_gain_fcc_line_freq, freq_update = head_dict["pcal2_linefreq"] if "pcal2_linefreq" in head_dict else None)
	if compute_fcc:
		fcc = pipeparts.mktee(pipeline, fcc)
		if use_coherence:
			# Gate f_cc with all four of the calibration lines
			fcc_gated = calibration_parts.mkgate(pipeline, fcc, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'fccgate1', queue_length = queue_length)
			fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, pcaly_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'fccgate2', queue_length = queue_length)
			fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'fccgate3', queue_length = queue_length)
			fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'fccgate4', queue_length = queue_length)
			fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line3_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'fccgate5', queue_length = queue_length)
			# Gate f_cc with the amplitude channels of the lines used.
			if "pcal_line1_amp" in headkeys:
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate6', queue_length = queue_length)
			if "pcal_line2_amp" in headkeys:
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, pcal_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate7', queue_length = queue_length)
			if "sus_line1_amp" in headkeys:
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate8', queue_length = queue_length)
			if "sus_line2_amp" in headkeys:
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate9', queue_length = queue_length)
			if "sus_line3_amp" in headkeys:
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, sus_line3_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate10', queue_length = queue_length)
			# Use gating to require that the needed input channels are available and nonzero.
			if tdcf_gate_with_excitation:
				if excitations_exist[4] == None:
					if excitations_exist[1] == None:
						uimexc_exists = calibration_parts.mkresample(pipeline, uimexc, 3, False, calibstate_sr)
						uimexc_exists = pipeparts.mkbitvectorgen(pipeline, uimexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
						uimexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, uimexc_exists, calibstate_caps))
						excitations_exist[1] = uimexc_exists
					if excitations_exist[2] == None:
						pumexc_exists = calibration_parts.mkresample(pipeline, pumexc, 3, False, calibstate_sr)
						pumexc_exists = pipeparts.mkbitvectorgen(pipeline, pumexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
						pumexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, pumexc_exists, calibstate_caps))
						excitations_exist[2] = pumexc_exists
					if excitations_exist[3] == None:
						tstexc_exists = calibration_parts.mkresample(pipeline, tstexc, 3, False, calibstate_sr)
						tstexc_exists = pipeparts.mkbitvectorgen(pipeline, tstexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
						tstexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, tstexc_exists, calibstate_caps))
						excitations_exist[3] = tstexc_exists
					excitations_exist[4] = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, excitations_exist[:4]))
				fcc_gated = calibration_parts.mkgate(pipeline, fcc_gated, excitations_exist[4], 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'fccgate11', queue_length = queue_length)
			fcc_gated = pipeparts.mktee(pipeline, fcc_gated)

			# Smooth f_cc
			smooth_fcc = calibration_parts.smooth_kappas(pipeline, fcc_gated, fcc_default, median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)
		else:
			# Smooth f_cc
			smooth_fcc = calibration_parts.smooth_kappas_no_coherence(pipeline, fcc, fcc_var, fcc_default, median_smoothing_samples, factors_average_samples, kappas_default_to_median, filter_latency_factor)

		smooth_fcctee = pipeparts.mktee(pipeline, smooth_fcc)

		if compute_tdcf_uncertainty:
			fcc_unc_abs = pipeparts.mkgeneric(pipeline, fcc_gated if use_coherence else fcc, "lal_stdev", mode = 0, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
			fcc_unc_abs = calibration_parts.compute_uncertainty_reduction(pipeline, fcc_unc_abs, integration_samples, median_smoothing_samples, factors_average_samples)

# compute f_s and Q
if not compute_exact_kappas and (compute_fs or compute_srcq):
	expected_Xi = complex((fs_squared_default - 1j * act_pcal_line_freq * fs_default / srcQ_default) / (act_pcal_line_freq * act_pcal_line_freq))
	Xi_var = fs_squared_var / pow(act_pcal_line_freq, 2)

	# Compute the factor Xi which will be used for the f_s and src_Q calculations
	if not factors_from_filters_file:
		C0_f1 = calibration_parts.merge_into_complex(pipeline, epics_dict["C0_f1_re"][0], epics_dict["C0_f1_im"][0])
		D_f1 = calibration_parts.merge_into_complex(pipeline, epics_dict["D_f1_re"][0], epics_dict["D_f1_im"][0])
		AT_f1 = calibration_parts.merge_into_complex(pipeline, epics_dict["AT_f1_re"][0], epics_dict["AT_f1_im"][0])
		AP_f1 = calibration_parts.merge_into_complex(pipeline, epics_dict["AP_f1_re"][0], epics_dict["AP_f1_im"][0])
		AU_f1 = calibration_parts.merge_into_complex(pipeline, epics_dict["AU_f1_re"][0], epics_dict["AU_f1_im"][0])
		Xi = calibration_parts.compute_Xi(pipeline, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, act_pcal_line_freq, C0_f1, D_f1, AT_f1, AP_f1, AU_f1, esd_act_line_freq, ktst, apply_complex_kappatst, pum_act_line_freq, kpum, apply_complex_kappapum, uim_act_line_freq, kuim, apply_complex_kappauim, kc, fcc)
	else:
		Xi = calibration_parts.compute_Xi_from_filters_file(pipeline, pcal_at_act_pcal_freq, derr_at_act_pcal_freq, act_pcal_line_freq, C0_f1_re, C0_f1_im, D_f1_re, D_f1_im, AT_f1_re, AT_f1_im, AP_f1_re, AP_f1_im, AU_f1_re, AU_f1_im, esd_act_line_freq, ktst, apply_complex_kappatst, pum_act_line_freq, kpum, apply_complex_kappapum, uim_act_line_freq, kuim, apply_complex_kappauim, kc, fcc)

	Xi = pipeparts.mktee(pipeline, Xi)

	if use_coherence:
		# Gate Xi with all coherences. We apply the gating and smoothing here since Q depends on the inverse of Im(Xi), which fluctuates about zero.
		Xi_gated = calibration_parts.mkgate(pipeline, Xi, pcaly_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'Xigate1', queue_length = queue_length)
		Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, pcaly_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'Xigate2', queue_length = queue_length)
		Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line3_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'Xigate3', queue_length = queue_length)
		Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line1_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'Xigate4', queue_length = queue_length)
		Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line2_coh, coherence_unc_threshold, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, invert_control = True, name = 'Xigate5', queue_length = queue_length)
		# Gate Xi with the amplitude channels of the lines used.
		if "pcal_line1_amp" in headkeys:
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, pcal_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate6', queue_length = queue_length)
		if "pcal_line2_amp" in headkeys:
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, pcal_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate7', queue_length = queue_length)
		if "sus_line1_amp" in headkeys:
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line1_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate8', queue_length = queue_length)
		if "sus_line2_amp" in headkeys:
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line2_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate9', queue_length = queue_length)
		if "sus_line3_amp" in headkeys:
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, sus_line3_amp, input_min, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate10', queue_length = queue_length)
		# Use gating to require that the needed input channels are available and nonzero.
		if tdcf_gate_with_excitation:
			if excitations_exist[4] == None:
				if excitations_exist[1] == None:
					uimexc_exists = calibration_parts.mkresample(pipeline, uimexc, 3, False, calibstate_sr)
					uimexc_exists = pipeparts.mkbitvectorgen(pipeline, uimexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					uimexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, uimexc_exists, calibstate_caps))
					excitations_exist[1] = uimexc_exists
				if excitations_exist[2] == None:
					pumexc_exists = calibration_parts.mkresample(pipeline, pumexc, 3, False, calibstate_sr)
					pumexc_exists = pipeparts.mkbitvectorgen(pipeline, pumexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					pumexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, pumexc_exists, calibstate_caps))
					excitations_exist[2] = pumexc_exists
				if excitations_exist[3] == None:
					tstexc_exists = calibration_parts.mkresample(pipeline, tstexc, 3, False, calibstate_sr)
					tstexc_exists = pipeparts.mkbitvectorgen(pipeline, tstexc_exists, threshold = 1.0001 * input_min, bit_vector = 1)
					tstexc_exists = pipeparts.mktee(pipeline, pipeparts.mkcapsfilter(pipeline, tstexc_exists, calibstate_caps))
					excitations_exist[3] = tstexc_exists
				excitations_exist[4] = pipeparts.mktee(pipeline, calibration_parts.mkmultiplier(pipeline, excitations_exist[:4]))
			Xi_gated = calibration_parts.mkgate(pipeline, Xi_gated, excitations_exist[4], 1, attack_length = kappa_gate_attack_length, hold_length = kappa_gate_hold_length, name = 'Xigate11', queue_length = queue_length)
		Xi_gated = pipeparts.mktee(pipeline, Xi_gated)

		smooth_Xi = calibration_parts.smooth_complex_kappas(pipeline, Xi_gated, float(numpy.real(expected_Xi)), float(numpy.imag(expected_Xi)), median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	else:
		smooth_Xi = calibration_parts.smooth_complex_kappas_no_coherence(pipeline, Xi, Xi_var, Xi_var, float(numpy.real(expected_Xi)), float(numpy.imag(expected_Xi)), median_smoothing_samples, factors_average_samples, tdcf_default_to_median, filter_latency_factor)

	if compute_tdcf_uncertainty:
		Xi_unc_abs = pipeparts.mkgeneric(pipeline, Xi_gated if use_coherence else Xi, "lal_stdev", mode = 0, coherence_length = tdcf_coherence_length, array_size = tdcf_unc_samples, filter_latency = filter_latency_factor)
		Xi_unc_abs = calibration_parts.compute_uncertainty_reduction(pipeline, Xi_unc_abs, integration_samples, 
median_smoothing_samples, factors_average_samples)
		fs_over_Q_unc_abs = calibration_parts.multiply_constant(pipeline, Xi_unc_abs, act_pcal_line_freq, 0.0)
		if "pcal1_linefreq" in head_dict:
			head_dict["pcal1_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, fs_over_Q_unc_abs, "timestamped_average", "value", 1)
		fs_over_Q_unc_abs = pipeparts.mktee(pipeline, fs_over_Q_unc_abs)

	if not compute_srcq:
		# the imaginary part is only used to compute Q
		smooth_XiR = pipeparts.mkgeneric(pipeline, smooth_Xi, "creal")
	else:
		smooth_XiR, smooth_XiI = calibration_parts.split_into_real(pipeline, smooth_Xi)
		smooth_XiI = pipeparts.mktee(pipeline, smooth_XiI)

	if compute_fs and compute_srcq:
		smooth_XiR = pipeparts.mktee(pipeline, smooth_XiR)

	# compute f_s
	if compute_fs:
		smooth_fs_squared_almost = calibration_parts.multiply_constant(pipeline, smooth_XiR, act_pcal_line_freq, 0.0)
		smooth_fs_squared = calibration_parts.multiply_constant(pipeline, smooth_fs_squared_almost, act_pcal_line_freq, 0.0)
		if "pcal1_linefreq" in head_dict:
			head_dict["pcal1_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, smooth_fs_squared_almost, "timestamped_average", "value", 1)
			head_dict["pcal1_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, smooth_fs_squared, "timestamped_average", "value", 1)

		smooth_fs_squared = pipeparts.mktee(pipeline, smooth_fs_squared)

		if compute_tdcf_uncertainty:
			fs_squared_unc_abs = calibration_parts.multiply_constant(pipeline, fs_over_Q_unc_abs, act_pcal_line_freq, 0.0)
			if "pcal1_linefreq" in head_dict:
				head_dict["pcal1_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, fs_squared_unc_abs, "timestamped_average", "value", 1)

	# compute SRC Q_inv
	if compute_srcq:
		smooth_sqrtXiR_inv = calibration_parts.mkpow(pipeline, calibration_parts.mktypecast(pipeline, smooth_XiR, "Z128LE"), exponent = -0.5)
		smooth_srcQ_inv = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, smooth_sqrtXiR_inv, calibration_parts.multiply_constant(pipeline, calibration_parts.mktypecast(pipeline, smooth_XiI, "Z128LE"), -1.0, 0.0)), queue_length = [queue_length])

		smooth_srcQ_inv = pipeparts.mktee(pipeline, smooth_srcQ_inv)

		# We need a real-valued version of Q^(-1) to write the the frames.  If we have
		# an optical spring, the computed Q^(-1) is imaginary, and if we have an optical
		# antispring, the computed Q^(-1) is real.  To get a sensible real value either
		# way AND INCLUDE ANY MINUS SIGN, we use Re(Q^(-1)) + Im(Q^(-1)).
		smooth_srcQ_inv_real = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, pipeparts.mkgeneric(pipeline, smooth_srcQ_inv, "creal"), pipeparts.mkgeneric(pipeline, smooth_srcQ_inv, "cimag")), queue_length = [queue_length])

		smooth_srcQ_inv_real = pipeparts.mktee(pipeline, smooth_srcQ_inv_real)

		if compute_calib_statevector:
			# We need the real value fs / Q.
			fs_over_Q = calibration_parts.multiply_constant(pipeline, smooth_XiI, -act_pcal_line_freq, 0.0)

#
# TIME-VARYING FACTORS COMPENSATIONS
#

if apply_complex_kappatst:
	# We will apply an adaptive FIR filter to the TST component of the actuation that includes time-dependence in the gain and computational time delay
	adaptive_tst_filter = calibration_parts.mkadaptivefirfilt(pipeline, smooth_ktsttee, static_model = tstfilt_model, static_filter = tstfilt, update_samples = int(actuation_filter_update_time * compute_factors_sr), average_samples = int(actuation_filter_averaging_time * compute_factors_sr), phase_measurement_frequency = esd_act_line_freq, adaptive_filter_length = len(tstfilt), frequency_resolution = act_freq_res, window_type = act_window_type, filter_sample_rate = tstchainsr, filter_timeshift = 1000000000 * actuation_filter_update_time, name = "adaptive_tst_filter")
	if "tstexc_linefreq" in head_dict:
		head_dict["tstexc_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, adaptive_tst_filter, "timestamped_average", "phase_measurement_frequency", 1)

if apply_complex_kappapum:
	# We will apply an adaptive FIR filter to the PUM component of the actuation that includes time-dependence in the gain and computational time delay
	adaptive_pum_filter = calibration_parts.mkadaptivefirfilt(pipeline, smooth_kpumtee, static_model = pumfilt_model, static_filter = pumfilt, update_samples = int(actuation_filter_update_time * compute_factors_sr), average_samples = int(actuation_filter_averaging_time * compute_factors_sr), phase_measurement_frequency = pum_act_line_freq, adaptive_filter_length = len(pumfilt), frequency_resolution = act_freq_res, window_type = act_window_type, filter_sample_rate = pumchainsr, filter_timeshift = 1000000000 * actuation_filter_update_time, name = "adaptive_pum_filter")
	if "pumexc_linefreq" in head_dict:
		head_dict["pumexc_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, adaptive_pum_filter, "timestamped_average", "phase_measurement_frequency", 1)

if apply_complex_kappauim:
	# We will apply an adaptive FIR filter to the UIM component of the actuation that includes time-dependence in the gain and computational time delay
	adaptive_uim_filter = calibration_parts.mkadaptivefirfilt(pipeline, smooth_kuimtee, static_model = uimfilt_model, static_filter = uimfilt, update_samples = int(actuation_filter_update_time * compute_factors_sr), average_samples = int(actuation_filter_averaging_time * compute_factors_sr), phase_measurement_frequency = uim_act_line_freq, adaptive_filter_length = len(uimfilt), frequency_resolution = act_freq_res, window_type = act_window_type, filter_sample_rate = uimchainsr, filter_timeshift = 1000000000 * actuation_filter_update_time, name = "adaptive_uim_filter")
	if "uimexc_linefreq" in head_dict:
		head_dict["uimexc_linefreq"].connect("notify::timestamped-average", calibration_parts.update_timestamped_property, adaptive_uim_filter, "timestamped_average", "phase_measurement_frequency", 1)

if apply_fcc or apply_fs or apply_srcq:
	# We will apply an adaptive FIR filter to DARM_ERR that allows corrections for poles, zeros, and gain
	# We need to track the number of time-dependent and static zeros and poles in the adaptive filter
	variable_invsens_zeros = 0
	static_invsens_poles = []
	tdep_zpk = []
	if apply_fcc:
		variable_invsens_zeros += 1
		# The real part of the pole is 0.0, and fcc_default is the imaginary part
		static_invsens_poles.extend([fcc_default, 0.0])

		# (1 + i * f / f_cc) is a zero in the variable inverse sensing filter
		complex_fcc = calibration_parts.mktypecast(pipeline, smooth_fcctee, "Z128LE")

		tdep_zpk.append(complex_fcc)

	# There are two zeros that depend on fs and Q, both of which depend on both fs and Q
	if apply_fs or apply_srcq:
		variable_invsens_zeros += 2
		SRC_pole1 = (fs_default / 2.0) * (pow(srcQ_default, -1.0) + pow(pow(srcQ_default, -2.0) + 4.0, 0.5))
		SRC_pole2 = (fs_default / 2.0) * (pow(srcQ_default, -1.0) - pow(pow(srcQ_default, -2.0) + 4.0, 0.5))
		static_invsens_poles.extend([numpy.real(SRC_pole1), numpy.imag(SRC_pole1)])
		static_invsens_poles.extend([numpy.real(SRC_pole2), numpy.imag(SRC_pole2)])

		smooth_fs = calibration_parts.mkpow(pipeline, calibration_parts.mktypecast(pipeline, smooth_fs_squared, "Z128LE"), exponent = 0.5)
		smooth_fs = pipeparts.mktee(pipeline, smooth_fs)

	if apply_fs and apply_srcq:
		# The variable zeros depend on the computed values of fs and Q
		Q_inv_squared = calibration_parts.mkpow(pipeline, smooth_srcQ_inv, exponent = 2.0)
		sqrt_Q_inv_squared_plus4 = calibration_parts.mkpow(pipeline, calibration_parts.add_constant(pipeline, Q_inv_squared, 4.0, 0.0), exponent = 0.5)
		sqrt_Q_inv_squared_plus4 = pipeparts.mktee(pipeline, sqrt_Q_inv_squared_plus4)

		SRC_zero1 = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, smooth_srcQ_inv, sqrt_Q_inv_squared_plus4), queue_length = [queue_length])
		SRC_zero1 = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, SRC_zero1, smooth_fs), queue_length = [queue_length])
		SRC_zero1 = calibration_parts.multiply_constant(pipeline, SRC_zero1, 0.5, 0.0)

		SRC_zero2 = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, smooth_srcQ_inv, calibration_parts.multiply_constant(pipeline, sqrt_Q_inv_squared_plus4, -1.0, 0.0)), queue_length = [queue_length])
		SRC_zero2 = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, SRC_zero2, smooth_fs), queue_length = [queue_length])
		SRC_zero2 = calibration_parts.multiply_constant(pipeline, SRC_zero2, 0.5, 0.0)

		tdep_zpk.extend([SRC_zero1, SRC_zero2])

		# This will be added into tdep_zpk at the end, as required by lal_adaptivefirfilt
		variable_invsens_gain = calibration_parts.multiply_constant(pipeline, smooth_fs_squared, pow(fs_squared_default, -1.0), 0.0)

	elif apply_fs:
		# The variable zeros depend on the computed value of fs and the model value of Q
		Q_factor1 = (pow(srcQ_default, -1.0) + pow(pow(srcQ_default, -2.0) + 4.0, 0.5)) / 2.0
		Q_factor2 = (pow(srcQ_default, -1.0) - pow(pow(srcQ_default, -2.0) + 4.0, 0.5)) / 2.0

		SRC_zero1 = calibration_parts.multiply_constant(pipeline, smooth_fs, numpy.real(Q_factor1), numpy.imag(Q_factor1))
		SRC_zero2 = calibration_parts.multiply_constant(pipeline, smooth_fs, numpy.real(Q_factor2), numpy.imag(Q_factor2))

		tdep_zpk.extend([SRC_zero1, SRC_zero2])

		# This will be added into tdep_zpk at the end, as required by lal_adaptivefirfilt
		variable_invsens_gain = calibration_parts.multiply_constant(pipeline, smooth_fs_squared, pow(fs_squared_default, -1.0), 0.0)

	elif apply_srcq:
		# The variable zeros depend on the model value of fs and the computed value of Q
		Q_inv_squared = calibration_parts.mkpow(pipeline, smooth_srcQ_inv, exponent = 2.0)
		sqrt_Q_inv_squared_plus4 = calibration_parts.mkpow(pipeline, calibration_parts.add_constant(pipeline, Q_inv_squared, 4.0, 0.0), exponent = 0.5)
		sqrt_Q_inv_squared_plus4 = pipeparts.mktee(pipeline, sqrt_Q_inv_squared_plus4)

		SRC_zero1 = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, smooth_srcQ_inv, sqrt_Q_inv_squared_plus4), queue_length = [queue_length])
		SRC_zero1 = calibration_parts.multiply_constant(pipeline, SRC_zero1, 0.5 * numpy.real(fs_default), 0.5 * numpy.imag(fs_default))

		SRC_zero2 = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, smooth_srcQ_inv, calibration_parts.multiply_constant(pipeline, sqrt_Q_inv_squared_plus4, -1.0, 0.0)), queue_length = [queue_length])
		SRC_zero2 = calibration_parts.multiply_constant(pipeline, SRC_zero2, 0.5 * numpy.real(fs_default), 0.5 * numpy.imag(fs_default))

		tdep_zpk.extend([SRC_zero1, SRC_zero2])

	if apply_kappac:
		# We divide the gain by kappa_c
		kappac_inv = calibration_parts.mkpow(pipeline, smooth_kctee, exponent = -1.0)
		if apply_fs:
			variable_invsens_gain = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, variable_invsens_gain, kappac_inv), queue_length = [queue_length])
		else:
			variable_invsens_gain = kappac_inv

	if apply_kappac or apply_fs:
		# Now add the gain into the list of corrections
		variable_invsens_gain = calibration_parts.mktypecast(pipeline, variable_invsens_gain, "Z128LE")
		tdep_zpk.append(variable_invsens_gain)

	# Now interleave the correction channels in tdep_zpk and feed them into lal_adaptivefirfilt to update the inverse sensing filter
	tdep_zpk = calibration_parts.mkinterleave(pipeline, tdep_zpk, complex_data = True)
	if minimize_adaptive_sensfilt:
		adaptive_invsens_filter = calibration_parts.mkadaptivefirfilt(pipeline, tdep_zpk, update_samples = int(sensing_filter_update_time * compute_factors_sr), average_samples = int(sensing_filter_averaging_time * compute_factors_sr), num_zeros = variable_invsens_zeros, num_poles = 0, window_type = None, filter_sample_rate = hoft_sr, filter_timeshift = 1000000000 * sensing_filter_update_time, minimize_filter_length = True, name = "adaptive_invsens_filter")
	else:
		adaptive_invsens_filter = calibration_parts.mkadaptivefirfilt(pipeline, tdep_zpk, static_model = reschainfilt_model, static_filter = reschainfilt, static_poles = static_invsens_poles, update_samples = int(sensing_filter_update_time * compute_factors_sr), average_samples = int(sensing_filter_averaging_time * compute_factors_sr), num_zeros = variable_invsens_zeros, num_poles = 0, adaptive_filter_length = len(reschainfilt), frequency_resolution = invsens_freq_res, window_type = invsens_window_type, filter_sample_rate = hoft_sr, filter_timeshift = 1000000000 * sensing_filter_update_time, minimize_filter_length = False, name = "adaptive_invsens_filter")

#
# CONTROL BRANCH
#

# zero out filter settling samples
tst_filter_settle_time = 0.0
tst_filter_latency = 0.0
pum_filter_settle_time = 0.0
pum_filter_latency = 0.0
uim_filter_settle_time = 0.0
uim_filter_latency = 0.0
pumuim_filter_settle_time = 0.0
pumuim_filter_latency = 0.0

actsr = max(tstchainsr, pumchainsr, uimchainsr) if (apply_kappapum or apply_kappauim or apply_complex_kappapum or apply_complex_kappauim) else max (tstchainsr, pumuimchainsr)

pum_uim_separate = apply_kappapum or apply_kappauim or apply_complex_kappapum or apply_complex_kappauim or (CalibrationConfigs["calibrationmode"] == "Partial" and ("UIM_corr_filter" in filters or "PUM_corr_filter" in filters)) or CalibrationConfigs["calibrationmode"] == "Full"

# The reverse of the filters will be used in all filtering below due to the definition of the filtering procedure employed by lal_firbank
if CalibrationConfigs["calibrationmode"] == "Partial":
	# enforce caps on actuation channels and set up progress report if verbose is on
	tst = calibration_parts.caps_and_progress(pipeline, head_dict["tst"], ctrl_caps, "tst")
	tst = tsttee = pipeparts.mktee(pipeline, tst)
	pum = calibration_parts.caps_and_progress(pipeline, head_dict["pum"], ctrl_caps, "pum")
	pumtee = pipeparts.mktee(pipeline, pum)
	uim = calibration_parts.caps_and_progress(pipeline, head_dict["uim"], ctrl_caps, "uim")
	uimtee = pipeparts.mktee(pipeline, uim)

	# If processing the PUM and UIM actuation channels together, add them here
	if not pum_uim_separate:
		pumuim = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, pumtee, uimtee), queue_length = [queue_length])

if CalibrationConfigs["calibrationmode"] == "Full":
	# enforce caps on actuation channels and set up progress report, if verbose is on
	ctrl = calibration_parts.caps_and_progress(pipeline, head_dict["ctrl"], hoft_caps, "ctrl")
	ctrltee = darmctrltee = pipeparts.mktee(pipeline, ctrl)
	
	tst = ctrltee
	pumtee = ctrltee
	uimtee = ctrltee
	pumuim = ctrltee

elif test_filters:
	ctrl = calibration_parts.caps_and_progress(pipeline, head_dict["ctrl"], hoft_caps, "ctrl")
	ctrltee = pipeparts.mktee(pipeline, ctrl)	

# resample what will become the TST actuation chain to the TST FIR filter sample rate
tst = calibration_parts.mkresample(pipeline, tst, 5, False, "audio/x-raw, format=F64LE, rate=%d" % tstchainsr, window = 3, f_cut = 0.95)
# Remove any DC component
if remove_dc:
	tst = calibration_parts.removeDC(pipeline, tst, tstchainsr)
# High-pass filter the TST chain
if any(act_highpass):
	tst = calibration_parts.mkcomplexfirbank(pipeline, tst, latency = act_highpass_delay, fir_matrix = [act_highpass[::-1]], time_domain = td)
	tst_filter_settle_time += float(len(act_highpass)-act_highpass_delay)/tstchainsr
	tst_filter_latency += float(act_highpass_delay)/tstchainsr

if apply_complex_kappatst:
	# Apply an adaptive filter to include the time-dependence in the gain and the time delay.  Apply updates at fixed timestamps to ensure reproducibility.
	tst = calibration_parts.mktdepfirfilt(pipeline, calibration_parts.mkqueue(pipeline, tst, length = queue_length), kernel = tstfilt[::-1], latency = tstdelay, taper_length = actuation_filter_taper_length, kernel_endtime = 0, name = "TST_filter")
	# Hook up the adaptive filter from lal_adaptivefirfilt to lal_tdepfirfilt so that the filter gets updated
	adaptive_tst_filter.connect("notify::adaptive-filter", calibration_parts.update_filter, tst, "adaptive_filter", "kernel")
	adaptive_tst_filter.connect("notify::filter-endtime", calibration_parts.update_property_simple, tst, "filter_endtime", "kernel_endtime", 1)

else:
	# Filter the TST chain with the static TST actuation filter
	tst = calibration_parts.mkcomplexfirbank(pipeline, tst, latency = tstdelay, fir_matrix = [tstfilt[::-1]], time_domain = td)

tst_filter_settle_time += float(len(tstfilt)-tstdelay)/tstchainsr
tst_filter_latency += float(tstdelay)/tstchainsr

if apply_kappatst and not apply_complex_kappatst:
	# Apply only the real part of \kappa_tst as a correction to A_tst
	ktst_for_tst = calibration_parts.mkresample(pipeline, smooth_ktst_magnitude_tee, 3, False, tstchainsr)
	tst = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, ktst_for_tst, tst), queue_length = [queue_length])

# If we want, measure the transfer function applied by the TST filter(s)
if test_filters:
	tst = pipeparts.mktee(pipeline, tst)
	tst_tf = calibration_parts.mkinterleave(pipeline, [tst, calibration_parts.mkresample(pipeline, ctrltee, 5, False, tstchainsr, window = 3, f_cut = 0.95)])
	tst_tf_delay = tst_filter_settle_time + ((1.0 - filter_latency_factor) * (demodulation_filter_time + (median_smoothing_samples + factors_average_samples) / compute_factors_sr) + actuation_filter_update_time if apply_complex_kappatst else 0)
	tst_tf_start = start + tst_tf_delay
	tst_tf_dur = gps_end_time - tst_tf_start - tst_filter_latency - 10 if InputConfigs["datasource"] == "frames" else 300
	num_tst_ffts = int((tst_tf_dur - 56) / 8)
	tst_tf_dur = 56 + 8 * num_tst_ffts
	tst_tf = pipeparts.mkprogressreport(pipeline, tst_tf, "progress_tst_tf_%s" % instrument)
	calibration_parts.mktransferfunction(pipeline, tst_tf, fft_length = 64 * tstchainsr, fft_overlap = 56 * tstchainsr, num_ffts = num_tst_ffts, use_median = True, update_samples = 1e15, update_delay_samples = tst_tf_delay * tstchainsr, filename = "%s_tst_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), tst_tf_start, tst_tf_dur), name = "tst_filters_tf", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)

# resample the TST actuation chain if necessary
if tstchainsr < actsr:
	tst = calibration_parts.mkresample(pipeline, tst, 4, False, actsr, window = 3, f_cut = 0.95)

# Check whether we need to filter the PUM and UIM stages separately or together
if pum_uim_separate:
	# resample what will become the PUM and UIM actuation paths to the PUM and UIM FIR filter sample rates
	pum = calibration_parts.mkresample(pipeline, pumtee, 5, False, "audio/x-raw, format=F64LE, rate=%d" % pumchainsr, window = 3, f_cut = 0.95)
	uim = calibration_parts.mkresample(pipeline, uimtee, 5, False, "audio/x-raw, format=F64LE, rate=%d" % uimchainsr, window = 3, f_cut = 0.95)
	# Remove any DC component
	if remove_dc:
		pum = calibration_parts.removeDC(pipeline, pum, pumchainsr)
		uim = calibration_parts.removeDC(pipeline, uim, uimchainsr)
	# High-pass filter the PUM and UIM paths
	if any(act_highpass):
		pum = calibration_parts.mkcomplexfirbank(pipeline, pum, latency = act_highpass_delay, fir_matrix = [act_highpass[::-1]], time_domain = td)
		pum_filter_settle_time += float(len(act_highpass)-act_highpass_delay)/pumchainsr
		pum_filter_latency += float(act_highpass_delay)/pumchainsr
		uim = calibration_parts.mkcomplexfirbank(pipeline, uim, latency = act_highpass_delay, fir_matrix = [act_highpass[::-1]], time_domain = td) 
		uim_filter_settle_time += float(len(act_highpass)-act_highpass_delay)/uimchainsr
		uim_filter_latency += float(act_highpass_delay)/uimchainsr

	if apply_complex_kappapum:
		# Apply an adaptive filter to include the time-dependence in the gain and the time delay.  Apply updates at fixed timestamps to ensure reproducibility.
		pum = calibration_parts.mktdepfirfilt(pipeline, calibration_parts.mkqueue(pipeline, pum, length = queue_length), kernel = pumfilt[::-1], latency = pumdelay, taper_length = actuation_filter_taper_length, kernel_endtime = 0, name = "PUM_filter")
		# Hook up the adaptive filter from lal_adaptivefirfilt to lal_tdepfirfilt so that the filter gets updated
		adaptive_pum_filter.connect("notify::adaptive-filter", calibration_parts.update_filter, pum, "adaptive_filter", "kernel")
		adaptive_pum_filter.connect("notify::filter-endtime", calibration_parts.update_property_simple, pum, "filter_endtime", "kernel_endtime", 1)

	else:
		# Filter the PUM chain with the static PUM actuation filter
		pum = calibration_parts.mkcomplexfirbank(pipeline, pum, latency = pumdelay, fir_matrix = [pumfilt[::-1]], time_domain = td)

	pum_filter_settle_time += float(len(pumfilt)-pumdelay)/pumchainsr
	pum_filter_latency += float(pumdelay)/pumchainsr

	if apply_kappapum and not apply_complex_kappapum:
		# Apply only the real part of kappa_pum as a correction to A_pum
		kpum_for_pum = calibration_parts.mkresample(pipeline, smooth_kpum_magnitude_tee, 3, False, pumchainsr)
		pum = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, kpum_for_pum, pum), queue_length = [queue_length])

	# If we want, measure the transfer function applied by the PUM filter(s)
	if test_filters:
		pum = pipeparts.mktee(pipeline, pum)
		pum_tf = calibration_parts.mkinterleave(pipeline, [pum, calibration_parts.mkresample(pipeline, ctrltee, 5, False, pumchainsr, window = 3, f_cut = 0.95)])
		pum_tf_delay = pum_filter_settle_time + ((1.0 - filter_latency_factor) * (demodulation_filter_time + (median_smoothing_samples + factors_average_samples) / compute_factors_sr) + actuation_filter_update_time if apply_complex_kappapum else 0)
		pum_tf_start = start + pum_tf_delay
		pum_tf_dur = gps_end_time - pum_tf_start - pum_filter_latency - 10 if InputConfigs["datasource"] == "frames" else 300
		num_pum_ffts = int((pum_tf_dur - 56) / 8)
		pum_tf_dur = 56 + 8 * num_pum_ffts
		pum_tf = pipeparts.mkprogressreport(pipeline, pum_tf, "progress_pum_tf_%s" % instrument)
		calibration_parts.mktransferfunction(pipeline, pum_tf, fft_length = 64 * pumchainsr, fft_overlap = 56 * pumchainsr, num_ffts = num_pum_ffts, use_median = True, update_samples = 1e15, update_delay_samples = pum_tf_delay * pumchainsr, filename = "%s_pum_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), pum_tf_start, pum_tf_dur), name = "pum_filters_tf", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)

	if apply_complex_kappauim:
		# Apply an adaptive filter to include the time-dependence in the gain and the time delay.  Apply updates at fixed timestamps to ensure reproducibility.
		uim = calibration_parts.mktdepfirfilt(pipeline, calibration_parts.mkqueue(pipeline, uim, length = queue_length), kernel = uimfilt[::-1], latency = uimdelay, taper_length = actuation_filter_taper_length, kernel_endtime = 0, name = "UIM_filter")
		# Hook up the adaptive filter from lal_adaptivefirfilt to lal_tdepfirfilt so that the filter gets updated
		adaptive_uim_filter.connect("notify::adaptive-filter", calibration_parts.update_filter, uim, "adaptive_filter", "kernel")
		adaptive_uim_filter.connect("notify::filter-endtime", calibration_parts.update_property_simple, uim, "filter_endtime", "kernel_endtime", 1)

	else:
		# Filter the UIM chain with the static UIM actuation filter
		uim = calibration_parts.mkcomplexfirbank(pipeline, uim, latency = uimdelay, fir_matrix = [uimfilt[::-1]], time_domain = td)

	uim_filter_settle_time += float(len(uimfilt)-uimdelay)/uimchainsr
	uim_filter_latency += float(uimdelay)/uimchainsr

	if apply_kappauim and not apply_complex_kappauim:
		# Apply only the real part of kappa_uim as a correction to A_uim
		kuim_for_uim = calibration_parts.mkresample(pipeline, smooth_kuim_magnitude_tee, 3, False, uimchainsr)
		uim = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, kuim_for_uim, uim), queue_length = [queue_length])

	# If we want, measure the transfer function applied by the UIM filter(s)
	if test_filters:
		uim = pipeparts.mktee(pipeline, uim)
		uim_tf = calibration_parts.mkinterleave(pipeline, [uim, calibration_parts.mkresample(pipeline, ctrltee, 5, False, uimchainsr, window = 3, f_cut = 0.95)])
		uim_tf_delay = uim_filter_settle_time + ((1.0 - filter_latency_factor) * (demodulation_filter_time + (median_smoothing_samples + factors_average_samples) / compute_factors_sr) + actuation_filter_update_time if apply_complex_kappauim else 0)
		uim_tf_start = start + uim_tf_delay
		uim_tf_dur = gps_end_time - uim_tf_start - uim_filter_latency - 10 if InputConfigs["datasource"] == "frames" else 300
		num_uim_ffts = int((uim_tf_dur - 56) / 8)
		uim_tf_dur = 56 + 8 * num_uim_ffts
		uim_tf = pipeparts.mkprogressreport(pipeline, uim_tf, "progress_uim_tf_%s" % instrument)
		calibration_parts.mktransferfunction(pipeline, uim_tf, fft_length = 64 * uimchainsr, fft_overlap = 56 * uimchainsr, num_ffts = num_uim_ffts, use_median = True, update_samples = 1e15, update_delay_samples = uim_tf_delay * uimchainsr, filename = "%s_uim_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), uim_tf_start, uim_tf_dur), name = "uim_filters_tf", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)

	# resample the PUM actuation path if necessary
	if pumchainsr < actsr:
		pum = calibration_parts.mkresample(pipeline, pum, 4, False, actsr, window = 3, f_cut = 0.95)

	# resample the UIM actuation path if necessary
	if uimchainsr < actsr:
		uim = calibration_parts.mkresample(pipeline, uim, 4, False, actsr, window = 3, f_cut = 0.95)

	# Add the TST, PUM, and UIM paths together to form the full actuation path
	ctrl = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, tst, pum, uim), queue_length = [queue_length])
else:
	# resample what will become the PUM/UIM actuation chain to the PUM/UIM FIR filter sample rate
	pumuim = calibration_parts.mkresample(pipeline, pumuim, 5, False, "audio/x-raw, format=F64LE, rate=%d" % pumuimchainsr, window = 3, f_cut = 0.95)
	# Remove any DC component
	if remove_dc:
		pumuim = calibration_parts.removeDC(pipeline, pumuim, pumuimchainsr)
	# High-pass filter the PUM/UIM path
	if any(act_highpass):
		pumuim = calibration_parts.mkcomplexfirbank(pipeline, pumuim, latency = act_highpass_delay, fir_matrix = [act_highpass[::-1]], time_domain = td)
		pumuim_filter_settle_time += float(len(act_highpass)-act_highpass_delay)/pumuimchainsr
		pumuim_filter_latency += float(act_highpass_delay)/pumuimchainsr

	# Filter the PUM/UIM chain with the static PUM/UIM actuation filter
	pumuim = calibration_parts.mkcomplexfirbank(pipeline, pumuim, latency = pumuimdelay, fir_matrix = [pumuimfilt[::-1]], time_domain = td)

	pumuim_filter_settle_time += float(len(pumuimfilt)-pumuimdelay)/pumuimchainsr
	pumuim_filter_latency += float(pumuimdelay)/pumuimchainsr

	# If we want, measure the transfer function applied by the PUM/UIM filter(s)
	if test_filters:
		pumuim = pipeparts.mktee(pipeline, pumuim)
		pumuim_tf = calibration_parts.mkinterleave(pipeline, [pumuim, calibration_parts.mkresample(pipeline, ctrltee, 5, False, pumuimchainsr, window = 3, f_cut = 0.95)])
		pumuim_tf_delay = pumuim_filter_settle_time + ((1.0 - filter_latency_factor) * (demodulation_filter_time + (median_smoothing_samples + factors_average_samples) / compute_factors_sr))
		pumuim_tf_start = start + pumuim_tf_delay
		pumuim_tf_dur = gps_end_time - pumuim_tf_start - pumuim_filter_latency - 10 if InputConfigs["datasource"] == "frames" else 300
		num_pumuim_ffts = int((pumuim_tf_dur - 56) / 8)
		pumuim_tf_dur = 56 + 8 * num_pumuim_ffts
		pumuim_tf = pipeparts.mkprogressreport(pipeline, pumuim_tf, "progress_pumuim_tf_%s" % instrument)
		calibration_parts.mktransferfunction(pipeline, pumuim_tf, fft_length = 64 * pumuimchainsr, fft_overlap = 56 * pumuimchainsr, num_ffts = num_pumuim_ffts, use_median = True, update_samples = 1e15, update_delay_samples = pumuim_tf_delay * pumuimchainsr, filename = "%s_pumuim_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), pumuim_tf_start, pumuim_tf_dur), name = "pumuim_filters_tf", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)

	# resample the PUM/UIM actuation chain if necessary
	if pumuimchainsr < actsr:
		pumuim = calibration_parts.mkresample(pipeline, pumuim, 4, False, actsr, window = 3, f_cut = 0.95)

	# Add the TST and PUM/UIM chains together to form the full actuation chain
	ctrl = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, tst, pumuim), queue_length = [queue_length])

# Resample \DeltaL_ctrl to the full h(t) sample rate
if actsr != hoft_sr:
	ctrl = calibration_parts.mkresample(pipeline, ctrl, 4, False, hoft_caps, window = 3, f_cut = 0.95)

if (test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None):
	ctrl = pipeparts.mktee(pipeline, ctrl)
	ctrl_latency = calibration_parts.mkresample(pipeline, ctrl, 0, False, latency_metrics_caps) 
	ctrl_latency = pipeparts.mklatency(pipeline, ctrl_latency, name = "%s_ctrl" % OutputConfigs["frametype"], silent = silent)
	if kafka_server is not None:
		ctrl_latency.connect("notify::current-latency", handler.latency_new_buffer)
	pipeparts.mkfakesink(pipeline, ctrl_latency)

#
# RESIDUAL BRANCH
#

# zero out res filter settle time
res_filter_settle_time = 0.0
res_filter_latency = 0.0

# The reverse of the filters will be used in all filtering below due to the definition of the filtering procedure employed by lal_firbank

# enforce caps on the residual branch and hook up progress report if verbose is on
if CalibrationConfigs["calibrationmode"] == "Full":
	res = restee = derrtee
elif CalibrationConfigs["calibrationmode"] == "Partial":
	res = calibration_parts.caps_and_progress(pipeline, head_dict["res"], hoft_caps, "res")
	res = restee = pipeparts.mktee(pipeline, res)

# Remove any DC component
if remove_dc:
	res = calibration_parts.removeDC(pipeline, res, hoft_sr)

# High-pass filter the residual chain
if any(invsens_highpass):
	if invsens_highpass_sr != hoft_sr:
		# Magic trick to apply a high-pass filter to the inverse sensing path at a lower sample rate without losing information above the Nyquist frequency.
		res = pipeparts.mktee(pipeline, res)
		res_lowfreq = calibration_parts.mkresample(pipeline, res, 4, False, invsens_highpass_sr, frequency_resolution = (invsens_highpass_sr / 2.0 - 10.0) / 2)
		# Use spectral inversion to make a low-pass filter with a gain of -1.
		invsens_highpass[invsens_highpass_delay] = invsens_highpass[invsens_highpass_delay] - 1.0
		# Apply this filter to the inverse sensing path at a lower sample rate to get only the low frequency components
		res_lowfreq = calibration_parts.mkcomplexfirbank(pipeline, res_lowfreq, latency = invsens_highpass_delay, fir_matrix = [invsens_highpass[::-1]], time_domain = td)
		# Upsample
		res_lowfreq = calibration_parts.mkresample(pipeline, res_lowfreq, 4, False, hoft_sr, frequency_resolution = (invsens_highpass_sr / 2.0 - 10.0) / 2)
		# Add to the inverse sensing path to get rid of the low frequencies
		res = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, res, res_lowfreq), queue_length = [queue_length])
	else:
		# Apply the high-pass filter at the h(t) sample rate
		res = calibration_parts.mkcomplexfirbank(pipeline, res, latency = invsens_highpass_delay, fir_matrix = [invsens_highpass[::-1]], time_domain = td)
	res_filter_settle_time += float(len(invsens_highpass)-invsens_highpass_delay) / invsens_highpass_sr
	res_filter_latency += float(invsens_highpass_delay) / invsens_highpass_sr

if (apply_fcc or apply_fs or apply_srcq) and not minimize_adaptive_sensfilt:
	# Apply an adaptive filter to include the time-dependence of any sensing function parameters
	res = calibration_parts.mktdepfirfilt(pipeline, calibration_parts.mkqueue(pipeline, res, length = queue_length), kernel = reschainfilt[::-1], latency = reschaindelay, taper_length = sensing_filter_taper_length, kernel_endtime = 0, name = "RES_filter")
	# Hook up the adaptive filter from lal_adaptivefirfilt to lal_tdepfirfilt so that the filter gets updated
	adaptive_invsens_filter.connect("notify::adaptive-filter", calibration_parts.update_filter, res, "adaptive_filter", "kernel")
	adaptive_invsens_filter.connect("notify::filter-endtime", calibration_parts.update_property_simple, res, "filter_endtime", "kernel_endtime", 1)

else:
	# Apply the residual chain filter without time-dependence
	res = calibration_parts.mkcomplexfirbank(pipeline, res, latency = int(reschaindelay), fir_matrix = [reschainfilt[::-1]], time_domain = td)

# Account for filter latency and settle time for the CALIB_STATE_VECTOR
res_filter_settle_time += float(len(reschainfilt)-reschaindelay)/hoft_sr
res_filter_latency += float(reschaindelay)/hoft_sr

# Apply a short adaptive filter to include the time-dependence of the sensing function
if minimize_adaptive_sensfilt and (apply_fcc or apply_fs or apply_srcq):
	# Compute the reference model filter
	static_invsens_zero_filt = numpy.zeros(2)
	static_invsens_zero_filt[0] = 0.5 + hoft_sr / (2.0 * numpy.pi * static_invsens_poles[0])
	static_invsens_zero_filt[1] = 0.5 - hoft_sr / (2.0 * numpy.pi * static_invsens_poles[0])
	for i in range(1, len(static_invsens_poles) // 2):
		temp_filt = numpy.zeros(2)
		temp_filt[0] = 0.5 + hoft_sr / (2.0 * numpy.pi * static_invsens_poles[2 * i])
		temp_filt[1] = 0.5 - hoft_sr / (2.0 * numpy.pi * static_invsens_poles[2 * i])
		static_invsens_zero_filt = numpy.convolve(static_invsens_zero_filt, temp_filt)

	# Apply an adaptive filter and apply updates at predetermined times.
	res = calibration_parts.mktdepfirfilt(pipeline, calibration_parts.mkqueue(pipeline, res, length = queue_length), kernel = static_invsens_zero_filt[::-1], latency = 0, taper_length = sensing_filter_taper_length, kernel_endtime = 0, name = "RES_adaptive_filter")
	# Hook up the adaptive filter from lal_adaptivefirfilt to lal_tdepfirfilt so that the filter gets updated
	adaptive_invsens_filter.connect("notify::adaptive-filter", calibration_parts.update_filter, res, "adaptive_filter", "kernel")
	adaptive_invsens_filter.connect("notify::filter-endtime", calibration_parts.update_property_simple, res, "filter_endtime", "kernel_endtime", 1)

# Apply \kappa_c if we haven't already
if apply_kappac and not (apply_fcc or apply_fs or apply_srcq):
	kc_modify_res = calibration_parts.mkresample(pipeline, smooth_kctee, 3, False, hoft_caps)
	res = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, res, calibration_parts.mkpow(pipeline, kc_modify_res, exponent = -1.0)), queue_length = [queue_length])

if test_filters:
	res = pipeparts.mktee(pipeline, res)
	res_tf = calibration_parts.mkinterleave(pipeline, [res, derrtee])
	res_tf_delay = res_filter_settle_time + ((1.0 - filter_latency_factor) * (demodulation_filter_time + (median_smoothing_samples + factors_average_samples) / compute_factors_sr) + actuation_filter_update_time if (apply_fcc or apply_fs or apply_srcq) else 0)
	res_tf_start = start + res_tf_delay
	res_tf_dur = gps_end_time - res_tf_start - res_filter_latency - 10 if InputConfigs["datasource"] == "frames" else 300
	num_res_ffts = int((res_tf_dur - 56) / 8)
	res_tf_dur = 56 + 8 * num_res_ffts
	res_tf = pipeparts.mkprogressreport(pipeline, res_tf, "progress_res_tf_%s" % instrument)
	calibration_parts.mktransferfunction(pipeline, res_tf, fft_length = 64 * hoft_sr, fft_overlap =56 * hoft_sr, num_ffts = num_res_ffts, use_median = True, update_samples = 1e15, update_delay_samples = res_tf_delay * hoft_sr, filename = "%s_res_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), res_tf_start, res_tf_dur), name = "res_filters_tf", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)

if test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None:
	res = pipeparts.mktee(pipeline, res)
	res_latency = calibration_parts.mkresample(pipeline, res, 0, False, latency_metrics_caps) 
	res_latency = pipeparts.mklatency(pipeline, res_latency, name = "%s_res" % OutputConfigs["frametype"], silent = silent)
	if kafka_server is not None:
		res_latency.connect("notify::current-latency", handler.latency_new_buffer)
	pipeparts.mkfakesink(pipeline, res_latency)

filter_settle_time = max(res_filter_settle_time, tst_filter_settle_time, pumuim_filter_settle_time)
filter_latency = max(res_filter_latency, tst_filter_latency, pumuim_filter_latency)

#
# CONTROL + RESIDUAL = H(T)
#

# Add control and residual chains and divide by L to make h(t)
strain = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, res, ctrl), queue_length = [queue_length])
# The insertgap below just adds a group_id to the stream to prevent GST_DEBUG messages at stream start.
strain = calibration_parts.mkinsertgap(pipeline, strain, insert_gap = False, remove_gap = True)
if test_filters:
	strain = pipeparts.mktee(pipeline, strain)
	response_function = calibration_parts.mkinterleave(pipeline, [strain, derrtee])
	response_delay = max(res_tf_delay, tst_tf_delay)
	response_start = start + response_delay
	response_dur = gps_end_time - response_start - max(res_filter_latency, tst_filter_latency) - 10 if InputConfigs["datasource"] == "frames" else 300
	num_response_ffts = int((response_dur - 56) / 8)
	response_dur = 56 + 8 * num_response_ffts
	response_function = pipeparts.mkprogressreport(pipeline, response_function, "progress_response_function_%s" % instrument)
	calibration_parts.mktransferfunction(pipeline, response_function, fft_length = 64 * hoft_sr, fft_overlap = 56 * hoft_sr, num_ffts = num_response_ffts, use_median = True, update_samples = 1e15, update_delay_samples = response_delay * hoft_sr, filename = "%s_response_filters_transfer_function_%d-%d.txt" % (filters_name.split('/')[-1].replace('.', '_'), response_start, response_dur), name = "response_function", use_fir_fft = True, fft_window_type = 0, frequency_resolution = 0.25)
# Divide by L in a way that is compatible with old and new filters files, since old filter files don't recored "arm length"
try:
	strain = calibration_parts.multiply_constant(pipeline, strain, 1.0/float(filters["arm_length"]), 0.0)
except KeyError:
	strain = calibration_parts.multiply_constant(pipeline, strain, 1.0/3994.5, 0.0)

strain = pipeparts.mkprogressreport(pipeline, strain, "progress_hoft_%s" % instrument)

# Put the units back to strain before writing to frames
straintagstr = "units=strain,channel-name=%sCALIB_STRAIN%s,instrument=%s" % (chan_prefix, chan_suffix, instrument)
straintee = pipeparts.mktee(pipeline, strain)
if not pick_cleanest_strain_channel:
	strain = pipeparts.mktaginject(pipeline, straintee, straintagstr)
if test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None:
	strain = pipeparts.mktee(pipeline, strain)
	strain_latency = calibration_parts.mkresample(pipeline, strain, 0, False, latency_metrics_caps)
	strain_latency = pipeparts.mklatency(pipeline, strain_latency, name = "%s_hoft" % OutputConfigs["frametype"], silent = silent)
	if kafka_server is not None:
		strain_latency.connect("notify::current-latency", handler.latency_new_buffer)
	pipeparts.mkfakesink(pipeline, strain_latency)

#
# CALIB_STATE_VECTOR BRANCH
#

#FIXME: Add more comments!

# Bit definitions
hoft_ok_bitnum = 0
obs_intent_bitnum = 1
lownoise_bitnum = 2
filters_ok_bitnum = 3
no_gap_bitnum = 4
no_stoch_inj_bitnum = 5
no_cbc_inj_bitnum = 6
no_burst_inj_bitnum = 7
no_detchar_inj_bitnum = 8
undisturbed_ok_bitnum = 9
ktst_smooth_bitnum = 10
kpum_smooth_bitnum = 11
kuim_smooth_bitnum = 12
kc_smooth_bitnum = 13
fcc_smooth_bitnum = 14
fs_smooth_bitnum = 15
fs_over_Q_smooth_bitnum = 16
line_sub_bitnum = 17
noise_sub_bitnum = 18
noise_sub_gate_bitnum = 19
nonsens_sub_bitnum = 20

# We'll only want to check things when the OBSERVATION-INTENT and LOW-NOISE bits are set
check_state_bitmask = pow(2, obs_intent_bitnum) + pow(2, lownoise_bitnum)
TDCFs_valid_bitmask_list = []
if apply_kappatst:
	TDCFs_valid_bitmask_list.append(pow(2, ktst_smooth_bitnum))
if apply_kappapum:
	TDCFs_valid_bitmask_list.append(pow(2, kpum_smooth_bitnum))
if apply_kappauim:
	TDCFs_valid_bitmask_list.append(pow(2, kuim_smooth_bitnum))
if apply_kappac:
	TDCFs_valid_bitmask_list.append(pow(2, kc_smooth_bitnum))
if apply_fcc:
	TDCFs_valid_bitmask_list.append(pow(2, fcc_smooth_bitnum))
if apply_fs:
	TDCFs_valid_bitmask_list.append(pow(2, fs_smooth_bitnum))
if apply_srcq:
	TDCFs_valid_bitmask_list.append(pow(2, fs_over_Q_smooth_bitnum))
TDCFs_valid_bitmask = sum(TDCFs_valid_bitmask_list)
monitor_bitmask_dict = {'monitor_on': check_state_bitmask, 'TDCFs_valid': TDCFs_valid_bitmask}

if compute_calib_statevector:

	# 
	# OBSERVATION-INTENT BIT BRANCH
	#

	obsintentchannel = calibration_parts.caps_and_progress(pipeline, head_dict["obsintent"], obsintent_caps, "obs_intent_%s" % instrument)
	obsintentchanneltee = pipeparts.mktee(pipeline, obsintentchannel)
	obsintent = pipeparts.mkgeneric(pipeline, obsintentchanneltee, "lal_logicalundersample", required_on = obsintent_bitmask, status_out = pow(2,obs_intent_bitnum))
	obsintent = pipeparts.mkcapsfilter(pipeline, obsintent, calibstate_caps)
	obsintenttee = pipeparts.mktee(pipeline, obsintent)
	
	#
	# NOMINAL LOW-NOISE BIT BRANCH
	#

	lownoisechanneltee = obsintentchanneltee if lownoise_channel_name == obsintent_channel_name else pipeparts.mktee(pipeline, calibration_parts.caps_and_progress(pipeline, head_dict["lownoise"], lownoise_caps, "low_noise_state_%s" % instrument))
	lownoise = pipeparts.mkgeneric(pipeline, lownoisechanneltee, "lal_logicalundersample", required_on = lownoise_bitmask, status_out = pow(2,lownoise_bitnum))
	lownoise = pipeparts.mkcapsfilter(pipeline, lownoise, calibstate_caps)
	lownoisetee = pipeparts.mktee(pipeline, lownoise)

	#
	# FILTERS-OK BIT BRANCH
	#
	
	# Set the FILTERS-OK bit based on one of these conditions, it order of preference:
	# 1. GRD-ISC_LOCK_STATE_N >= GRD-ISC_LOCK_NOMINAL_N.
	# 2. GRD-ISC_LOCK_STATE_N > some static threshold from the config file
	# 3. Apply a bitmask from the config file to, e.g., GRD-ISC_LOCK_OK
	# This determines when to start the clock for filter settling time

	if filterclock_channel_name != obsintent_channel_name and filterclock_channel_name != lownoise_channel_name:
		filterclock_channel = calibration_parts.caps_and_progress(pipeline, head_dict[filterclock_channel_name], filterclock_caps, "filterclock_channel_%s" % instrument)
		if filterclock_channel_name == noisesub_gate_channel:
			filterclock_channel = noisesub_gate_tee = pipeparts.mktee(pipeline, filterclock_channel)
		if filterclock_threshold_channel_name is not None:
			filterclock_threshold_channel = calibration_parts.caps_and_progress(pipeline, head_dict[filterclock_threshold_channel_name], filterclockthreshold_caps, "filterclock_threshold_channel_%s" % instrument)
			# Convert to doubles, divide filterclock_channel / filterclock_threshold_channel, and require that the result >= 1.
			filterclock_channel = pipeparts.mkgeneric(pipeline, filterclock_channel, "lal_typecast")
			filterclock_channel = pipeparts.mkcapsfilter(pipeline, filterclock_channel, "audio/x-raw, format=F64LE, channels=1, channel-mask=(bitmask)0x0")
			filterclock_threshold_channel = pipeparts.mkgeneric(pipeline, filterclock_threshold_channel, "lal_typecast")
			filterclock_threshold_channel = pipeparts.mkcapsfilter(pipeline, filterclock_threshold_channel, "audio/x-raw, format=F64LE, channels=1, channel-mask=(bitmask)0x0")
			# In case of digital error
			filterclock_channel = calibration_parts.add_constant(pipeline, filterclock_channel, 0.5, 0.0)
			filtersok = calibration_parts.complex_division(pipeline, filterclock_channel, filterclock_threshold_channel)
			filtersok = pipeparts.mkbitvectorgen(pipeline, filtersok, bit_vector = pow(2,filters_ok_bitnum), threshold = 1.0)
			filtersok = pipeparts.mkgeneric(pipeline, filtersok, "lal_logicalundersample", required_on = pow(2,filters_ok_bitnum), status_out = pow(2,filters_ok_bitnum), name = "filtersok_undersample")

		elif filterclock_threshold is not None:
			filtersok = pipeparts.mkbitvectorgen(pipeline, filterclock_channel, bit_vector = pow(2,filters_ok_bitnum), threshold = filterclock_threshold)
			filtersok = pipeparts.mkgeneric(pipeline, filtersok, "lal_logicalundersample", required_on = pow(2,filters_ok_bitnum), status_out = pow(2,filters_ok_bitnum))

		else:
			filtersok = pipeparts.mkgeneric(pipeline, filterclock_channel, "lal_logicalundersample", required_on = filterclock_bitmask, status_out = pow(2,filters_ok_bitnum))

	elif filterclock_channel_name == obsintent_channel_name:
		filtersok = pipeparts.mkgeneric(pipeline, obsintenttee, "lal_logicalundersample", required_on = pow(2,obs_intent_bitnum), status_out = pow(2,filters_ok_bitnum))

	elif filterclock_channel_name == lownoise_channel_name:
		filtersok = pipeparts.mkgeneric(pipeline, lownoisetee, "lal_logicalundersample", required_on = pow(2,lownoise_bitnum), status_out = pow(2,filters_ok_bitnum))

	filtersok = pipeparts.mkcapsfilter(pipeline, filtersok, calibstate_caps)
	filtersoktee = pipeparts.mktee(pipeline, filtersok)
	filtersok = calibration_parts.mkgate(pipeline, filtersoktee, filtersoktee, pow(2,filters_ok_bitnum), attack_length = -int(filter_settle_time * calibstate_sr), hold_length = -int(filter_latency * calibstate_sr), queue_length = queue_length)
	filtersok = pipeparts.mktee(pipeline, filtersok)

	#
	# NO-INVALID-INPUT BRANCH
	#

	# Check if any of the input data channels had to be replaced by zeroes because they were < input_min
	resok = pipeparts.mkbitvectorgen(pipeline, restee, threshold=input_min, bit_vector=1)
	resok = pipeparts.mkcapsfilter(pipeline, resok, "audio/x-raw, format=U32LE, rate=%d" % hoft_sr)
	resok = pipeparts.mkgeneric(pipeline, resok, "lal_logicalundersample", required_on = 1, status_out = 1)
	resok = pipeparts.mkcapsfilter(pipeline, resok, calibstate_caps)
	if CalibrationConfigs["calibrationmode"] == "Partial":
		tstok = pipeparts.mkbitvectorgen(pipeline, tsttee, threshold=input_min, bit_vector=1)
		tstok = pipeparts.mkcapsfilter(pipeline, tstok, "audio/x-raw, format=U32LE, rate=%d" % ctrl_sr)
		tstok = pipeparts.mkgeneric(pipeline, tstok, "lal_logicalundersample", required_on = 1, status_out = 1)
		tstok = pipeparts.mkcapsfilter(pipeline, tstok, calibstate_caps)
		pumok = pipeparts.mkbitvectorgen(pipeline, pumtee, threshold=input_min, bit_vector=1)
		pumok = pipeparts.mkcapsfilter(pipeline, pumok, "audio/x-raw, format=U32LE, rate=%d" % ctrl_sr)
		pumok = pipeparts.mkgeneric(pipeline, pumok, "lal_logicalundersample", required_on = 1, status_out = 1)
		pumok = pipeparts.mkcapsfilter(pipeline, pumok, calibstate_caps)
		uimok = pipeparts.mkbitvectorgen(pipeline, uimtee, threshold=input_min, bit_vector=1)
		uimok = pipeparts.mkcapsfilter(pipeline, uimok, "audio/x-raw, format=U32LE, rate=%d" % ctrl_sr)
		uimok = pipeparts.mkgeneric(pipeline, uimok, "lal_logicalundersample", required_on = 1, status_out = 1)
		uimok = pipeparts.mkcapsfilter(pipeline, uimok, calibstate_caps)
		noinvalidinput = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, resok, tstok, pumok, uimok), queue_length = [queue_length])
		noinvalidinput = pipeparts.mkbitvectorgen(pipeline, noinvalidinput, threshold=4, bit_vector=pow(2,no_gap_bitnum))
	if CalibrationConfigs["calibrationmode"] == "Full":
		ctrlok = pipeparts.mkbitvectorgen(pipeline, darmctrltee, threshold=input_min, bit_vector=1)
		ctrlok = pipeparts.mkcapsfilter(pipeline, ctrlok, "audio/x-raw, format=U32LE, rate=%d" % ctrl_sr)
		ctrlok = pipeparts.mkgeneric(pipeline, ctrlok, "lal_logicalundersample", required_on = 1, status_out = 1)
		ctrlok = pipeparts.mkcapsfilter(pipeline, ctrlok, calibstate_caps)
		noinvalidinput = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, resok, ctrlok), queue_length = [queue_length])
		noinvalidinput = pipeparts.mkbitvectorgen(pipeline, noinvalidinput, threshold=2, bit_vector=pow(2,no_gap_bitnum))
	noinvalidinput = pipeparts.mkcapsfilter(pipeline, noinvalidinput, calibstate_caps)
	noinvalidinput = pipeparts.mktee(pipeline, noinvalidinput)
	# inputs that are replaced with zeros affect h(t) for a short time before and after the zeros, so we also must account for this corrupted time.
	noinvalidinput = calibration_parts.mkgate(pipeline, noinvalidinput, noinvalidinput, pow(2,no_gap_bitnum), attack_length = -int(filter_settle_time * calibstate_sr), hold_length = -int(filter_latency * calibstate_sr), queue_length = queue_length)

	#
	# HW INJECTION BITS
	#

	hwinjchanneltee = obsintentchanneltee if hwinj_channel_name == obsintent_channel_name else lownoisechanneltee if hwinj_channel_name == lownoise_channel_name else pipeparts.mktee(pipeline, calibration_parts.caps_and_progress(pipeline, head_dict["hwinj"], hwinj_caps, "HW_injections_%s" % instrument))

	if stochhwinj_bitmask is not None:
		hwinjstoch = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = stochhwinj_bitmask, status_out = pow(2,no_stoch_inj_bitnum))
	elif stochhwinj_offbitmask is not None:
		hwinjstoch = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = stochhwinj_offbitmask, invert_result = True, status_out = pow(2,no_stoch_inj_bitnum))
	else:
		raise ValueError("Must set either StochHWInjBitmask or StochHWInjOffBitmask")
	hwinjstoch = pipeparts.mkcapsfilter(pipeline, hwinjstoch, calibstate_caps)

	if cbchwinj_bitmask is not None:
		hwinjcbc = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = cbchwinj_bitmask, status_out = pow(2,no_cbc_inj_bitnum))
	elif cbchwinj_offbitmask is not None:
		hwinjcbc = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = cbchwinj_offbitmask, invert_result = True, status_out = pow(2,no_cbc_inj_bitnum))
	else:
		raise ValueError("Must set either CBCHWInjBitmask or CBCHWInjOffBitmask")
	hwinjcbc = pipeparts.mkcapsfilter(pipeline, hwinjcbc, calibstate_caps)

	if bursthwinj_bitmask is not None:
		hwinjburst = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = bursthwinj_bitmask, status_out = pow(2,no_burst_inj_bitnum))
	elif bursthwinj_offbitmask is not None:
		hwinjburst = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = bursthwinj_offbitmask, invert_result = True, status_out = pow(2,no_burst_inj_bitnum))
	else:
		raise ValueError("Must set either BurstHWInjBitmask or BurstHWInjOffBitmask")
	hwinjburst = pipeparts.mkcapsfilter(pipeline, hwinjburst, calibstate_caps)

	if detcharhwinj_bitmask is not None:
		hwinjdetchar = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = detcharhwinj_bitmask, status_out = pow(2,no_detchar_inj_bitnum))
	elif detcharhwinj_offbitmask is not None:
		hwinjdetchar = pipeparts.mkgeneric(pipeline, hwinjchanneltee, "lal_logicalundersample", required_on = detcharhwinj_offbitmask, invert_result = True, status_out = pow(2,no_detchar_inj_bitnum))
	else:
		raise ValueError("Must set either DetCharHWInjBitmask or DetcharHWInjOffBitmask")
	hwinjdetchar = pipeparts.mkcapsfilter(pipeline, hwinjdetchar, calibstate_caps)

	#
	# UNDISTURBED-OK BIT BRANCH
	#

	if "undisturbedok" in head_dict:
		undisturbedok = calibration_parts.caps_and_progress(pipeline, head_dict["undisturbedok"], undisturbedok_caps, "undisturbed_ok_%s" % instrument)
		undisturbedok = pipeparts.mktee(pipeline, undisturbedok)
		undisturbedok_processed = pipeparts.mkgeneric(pipeline, undisturbedok, "lal_logicalundersample", required_on = undisturbedok_bitmask, status_out = pow(2,undisturbed_ok_bitnum))
		undisturbedok_processed = pipeparts.mkcapsfilter(pipeline, undisturbedok_processed, calibstate_caps)
		undisturbedok_processed = pipeparts.mktee(pipeline, undisturbedok_processed)

	#
	# KAPPATST BITS BRANCH
	#
	if compute_kappatst:
		ktstSmoothInRange = calibration_parts.compute_kappa_bits(pipeline, smooth_ktsttee, expected_kappatst_real, expected_kappatst_imag, kappatst_real_var, kappatst_imag_var, median_smoothing_samples, factors_average_samples, status_out_smooth = pow(2,ktst_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# KAPPAPUM BITS BRANCH
	#
	if compute_kappapum:
		kpumSmoothInRange = calibration_parts.compute_kappa_bits(pipeline, smooth_kpumtee, expected_kappapum_real, expected_kappapum_imag, kappapum_real_var, kappapum_imag_var, median_smoothing_samples, factors_average_samples, status_out_smooth = pow(2,kpum_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# KAPPAUIM BITS BRANCH
	#
	if compute_kappauim:
		kuimSmoothInRange = calibration_parts.compute_kappa_bits(pipeline, smooth_kuimtee, expected_kappauim_real, expected_kappauim_imag, kappauim_real_var, kappauim_imag_var, median_smoothing_samples, factors_average_samples, status_out_smooth = pow(2,kuim_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# KAPPAC BITS BRANCH
	#
	if compute_kappac:
		kcSmoothInRange = calibration_parts.compute_kappa_bits_only_real(pipeline, smooth_kctee, expected_kappac, kappac_var, median_smoothing_samples, factors_average_samples, status_out_smooth = pow(2,kc_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# FCC BITS BRANCH
	#
	if compute_fcc:
		fccSmoothInRange = calibration_parts.compute_kappa_bits_only_real(pipeline, smooth_fcctee, fcc_default, fcc_var, median_smoothing_samples + int((sensing_filter_update_time + sensing_filter_averaging_time) * compute_factors_sr), factors_average_samples, status_out_smooth = pow(2,fcc_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# FS BITS BRANCH
	#

	# We are actually checking the value fs^2, as that is more indicative of significant change.
	if compute_fs:
		fsSmoothInRange = calibration_parts.compute_kappa_bits_only_real(pipeline, smooth_fs_squared, fs_squared_default, fs_squared_var, median_smoothing_samples + int((sensing_filter_update_time + sensing_filter_averaging_time) * compute_factors_sr), factors_average_samples, status_out_smooth = pow(2,fs_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# SRCQ BITS BRANCH
	#

	# We are actually checking the value fs / Q, as that is more indicative of significant change.
	if compute_srcq:
		srcQSmoothInRange = calibration_parts.compute_kappa_bits_only_real(pipeline, fs_over_Q, fs_over_Q_default, fs_squared_var / act_pcal_line_freq, median_smoothing_samples + int((sensing_filter_update_time + sensing_filter_averaging_time) * compute_factors_sr), factors_average_samples, status_out_smooth = pow(2,fs_over_Q_smooth_bitnum), starting_rate = compute_factors_sr, ending_rate = calibstate_sr)

	#
	# H(T)-OK BIT BRANCH
	#

	# First combine higher order bits to determine h(t)-OK
	higherbits_list = [filtersok, lownoisetee, noinvalidinput]
	htok_threshold = pow(2,lownoise_bitnum) + pow(2,filters_ok_bitnum) + pow(2,no_gap_bitnum)
	if apply_kappatst or apply_complex_kappatst:
		higherbits_list.append(ktstSmoothInRange)
		htok_threshold += pow(2,ktst_smooth_bitnum)
	if apply_kappapum or apply_complex_kappapum:
		higherbits_list.append(kpumSmoothInRange)
		htok_threshold += pow(2,kpum_smooth_bitnum)
	if apply_kappauim or apply_complex_kappauim:
		higherbits_list.append(kuimSmoothInRange)
		htok_threshold += pow(2,kuim_smooth_bitnum)
	if apply_kappac:
		higherbits_list.append(kcSmoothInRange)
		htok_threshold += pow(2,kc_smooth_bitnum)
	if apply_fcc:
		higherbits_list.append(fccSmoothInRange)
		htok_threshold += pow(2,fcc_smooth_bitnum)
	if apply_fs:
		higherbits_list.append(fsSmoothInRange)
		htok_threshold += pow(2,fs_smooth_bitnum)
	if apply_srcq:
		higherbits_list.append(srcQSmoothInRange)
		htok_threshold += pow(2,fs_over_Q_smooth_bitnum)
	higherbits = calibration_parts.mkadder(pipeline, tuple(higherbits_list), queue_length = [queue_length])
	higherbitstee = pipeparts.mktee(pipeline, higherbits)

	# Now calculate h(t)-OK bit
	htok = pipeparts.mkbitvectorgen(pipeline, higherbitstee, bit_vector = pow(2,hoft_ok_bitnum), threshold = htok_threshold)
	htok = pipeparts.mkcapsfilter(pipeline, htok, calibstate_caps)

	#
	# COMBINE ALL BITS TO MAKE GDS-CALIB_STATE_VECTOR
	#

	all_bits_list = [higherbitstee, obsintenttee, htok, hwinjcbc, hwinjburst, hwinjdetchar, hwinjstoch]
	if "undisturbedok" in head_dict:
		all_bits_list.append(undisturbedok_processed)
	if compute_kappatst and not (apply_kappatst or apply_complex_kappatst):
		all_bits_list.append(ktstSmoothInRange)
	if compute_kappapum and not (apply_kappapum or apply_complex_kappapum):
		all_bits_list.append(kpumSmoothInRange)
	if compute_kappauim and not (apply_kappauim or apply_complex_kappauim):
		all_bits_list.append(kuimSmoothInRange)
	if compute_kappac and not apply_kappac:
		all_bits_list.append(kcSmoothInRange)
	if compute_fcc and not apply_fcc:
		all_bits_list.append(fccSmoothInRange)
	if compute_fs and not apply_fs:
		all_bits_list.append(fsSmoothInRange)
	if compute_srcq and not apply_srcq:
		all_bits_list.append(srcQSmoothInRange)

	calibstatevector = calibration_parts.mkadder(pipeline, tuple(all_bits_list), queue_length = [queue_length])

#
# SUBTRACTION OF LINES AND NOISE
#

# Set up gating for the power mains and noise subtraction
if compute_calib_statevector and (any(line_witness_channel_list) or any(witness_channel_list)) and (((noisesub_gate_bitmask > 0 or noisesub_gate_threshold is not None or noisesub_gate_value is not None) and noisesub_gate_channel != "None") or noisesub_use_filter_clock):
	if noisesub_use_filter_clock:
		noisesubgate = filtersok
	elif noisesub_gate_channel == obsintent_channel_name:
		noisesubgate = obsintentchanneltee
	elif noisesub_gate_channel == lownoise_channel_name:
		noisesubgate = lownoisechanneltee
	elif noisesub_gate_channel == hwinj_channel_name:
		noisesubgate = hwinjchanneltee
	elif noisesub_gate_channel == filterclock_channel_name:
		noisesubgate = noisesub_gate_tee
	elif noisesub_gate_channel == undisturbed_ok_channel:
		noisesubgate = undisturbedok
	else:
		noisesubgate = calibration_parts.caps_and_progress(pipeline, head_dict["noisesubgatechannel"], "audio/x-raw, format=S32LE, channels=1, channel-mask=(bitmask)0x0", noisesub_gate_channel)
	if noisesub_use_filter_clock:
		noisesubgate = pipeparts.mkcapsfilter(pipeline, noisesubgate, calibstate_caps)
		noisesubgate = pipeparts.mkbitvectorgen(pipeline, noisesubgate, bit_vector = pow(2,noise_sub_gate_bitnum), threshold = pow(2,filters_ok_bitnum))
		noisesubgate = pipeparts.mkgeneric(pipeline, noisesubgate, "lal_logicalundersample", required_on = pow(2,noise_sub_gate_bitnum), status_out = pow(2,noise_sub_gate_bitnum), name = "noisesubgate_logicalundersample")
	elif noisesub_gate_value is not None:
		noisesubgate = calibration_parts.mkinsertgap(pipeline, noisesubgate, bad_data_intervals = [noisesub_gate_value, noisesub_gate_value], insert_gap = False, open_intervals = True, replace_value = 0)
		noisesubgate = pipeparts.mkbitvectorgen(pipeline, noisesubgate, bit_vector = pow(2,noise_sub_gate_bitnum), threshold = noisesub_gate_value)
		noisesubgate = pipeparts.mkgeneric(pipeline, noisesubgate, "lal_logicalundersample", required_on = pow(2,noise_sub_gate_bitnum), status_out = pow(2,noise_sub_gate_bitnum))
	elif noisesub_gate_threshold is not None:
		noisesubgate = pipeparts.mkbitvectorgen(pipeline, noisesubgate, bit_vector = pow(2,noise_sub_gate_bitnum), threshold = noisesub_gate_threshold)
		noisesubgate = pipeparts.mkgeneric(pipeline, noisesubgate, "lal_logicalundersample", required_on = pow(2,noise_sub_gate_bitnum), status_out = pow(2,noise_sub_gate_bitnum))
	else:
		noisesubgate = pipeparts.mkgeneric(pipeline, noisesubgate, "lal_logicalundersample", required_on = noisesub_gate_bitmask, status_out = pow(2,noise_sub_gate_bitnum))
	noisesubgate = pipeparts.mkcapsfilter(pipeline, pipeparts.mkgeneric(pipeline, noisesubgate, "lal_typecast"), calibstate_caps)
	noisesubgatetee = pipeparts.mktee(pipeline, noisesubgate)
else:
	noisesubgatetee = None

# Remove lines using witness channels, such as calibration liens, 60 Hz power lines and harmonics
if any(line_witness_channel_list):
	for i in range(len(line_witness_channel_list)):
		for j in range(len(line_witness_channel_list[i])):
			if type(line_witness_channel_list[i][j]) == str:
				line_witness_channel_list[i][j] = calibration_parts.caps_and_progress(pipeline, head_dict[line_witness_channel_list[i][j]], "audio/x-raw, format=F64LE, channels=1, channel-mask=(bitmask)0x0", line_witness_channel_list[i][j])

	nolines_strain = calibration_parts.remove_lines_with_witnesses(pipeline, straintee, line_witness_channel_list, line_witness_freqs, line_witness_freq_vars, line_witness_frequency_channel_list, line_amplitude_list, filter_latency = filter_latency_factor, compute_rate = compute_factors_sr, rate_out = hoft_sr, num_median = line_witness_tf_median_time * compute_factors_sr, num_avg = line_witness_tf_averaging_time * compute_factors_sr, noisesub_gate_bit = noisesubgatetee, filter_length = line_subtraction_filter_time, queue_length = queue_length, min_num_median_avg = line_witness_tf_min_time * compute_factors_sr, secular_change_threshold = line_witness_tf_secular_change_threshold, witness_absent_value = input_min, input_max = input_max)
	# The insertgap below just adds a group_id to the stream to prevent GST_DEBUG messages at stream start.
	nolines_strain = calibration_parts.mkinsertgap(pipeline, nolines_strain, insert_gap = False, remove_gap = True)
	nolines_strain = pipeparts.mktee(pipeline, nolines_strain)

# Remove excess noise using any provided witness channels
if any(witness_channel_list):
	# Remove initial data from computation of transfer functions and wait until the filters and kappas settle
	witness_delay_time = witness_tf_time_shift if witness_tf_parallel_mode else (filter_settle_time + (1.0 - filter_latency_factor) * (demodulation_filter_time + median_smoothing_samples / compute_factors_sr + factors_average_samples / compute_factors_sr))
	# How much does the "delay_time" need to increase per iteration of cleaning?
	witness_delay_increment = witness_filter_taper_time + witness_channel_fft_time / 2.0 * (num_witness_ffts + 1.0) if not filter_latency_factor else 0.0
	# If we haven't removed any lines, clean the regular h(t) data
	if not any(line_witness_channel_list):
		nolines_strain = straintee

	for i in range(len(witness_channel_list)):
		# Length of ffts used to compute FIR filters
		witness_fft_samples = int(witness_channel_fft_time * witness_rates[i])
		# Overlap of ffts is half of fft length. The data is Hann-windowed before taking ffts.
		witness_fft_overlap = int(witness_fft_samples / 2)
		# How many samples between filter updates (does not include the samples used to compute the FIR filters
		witness_tf_update_samples = int(witness_rates[i] * witness_tf_update_time)
		# Length of FIR filters
		witness_fir_samples = int(float(witness_fir_length[min(i, len(witness_fir_length) - 1)]) * witness_rates[i])
		# Over how many samples should new FIR filters be tapered in?
		witness_filter_taper_length = int(witness_rates[i] * witness_filter_taper_time)

		witnesses = []
		for key in headkeys:
			if key in witness_channel_list[i]:
				witnesses.append(calibration_parts.caps_and_progress(pipeline, head_dict[key], "audio/x-raw, format=F64LE, channels=1, channel-mask=(bitmask)0x0", key))
		if len(witnesses) != len(witness_channel_list[i]):
			print("WARNING: Not all requested witness channels are being used", file=sys.stderr)
		nolines_strain = calibration_parts.clean_data(pipeline, nolines_strain, hoft_sr, witnesses, witness_rates[i], witness_fft_samples, witness_fft_overlap, num_witness_ffts, min_witness_ffts, witness_tf_update_samples, witness_fir_samples, witness_frequency_resolution, witness_filter_taper_length, use_median = witness_tf_use_median, parallel_mode = witness_tf_parallel_mode, notch_frequencies = witness_notch_frequencies[i], high_pass = witness_highpasses[i], noisesub_gate_bit = noisesubgatetee, delay_time = witness_delay_time, critical_lock_loss_time = critical_lock_loss_time, filename = None if witness_tf_filename is None else "%s_%d.txt" % (witness_tf_filename, i), fft_window_type = noisesub_fft_window_types[i], fir_window_type = noisesub_fir_window_types[i])
		witness_delay_time += witness_delay_increment

if any(line_witness_channel_list) or any(witness_channel_list):
	nolines_strain = pipeparts.mkprogressreport(pipeline, nolines_strain, "progress_hoft_nolines_%s" % instrument)
	if test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None:
		nolines_strain = pipeparts.mktee(pipeline, nolines_strain)
		nolines_strain_latency = calibration_parts.mkresample(pipeline, nolines_strain, 0, False, latency_metrics_caps)
		nolines_strain_latency = pipeparts.mklatency(pipeline, nolines_strain_latency, name = "%s_hoft_nolines" % OutputConfigs["frametype"], silent = silent)
		if kafka_server is not None:
			nolines_strain_latency.connect("notify::current-latency", handler.latency_new_buffer)
		pipeparts.mkfakesink(pipeline, nolines_strain_latency)

	# The insertgap below just adds a group_id to the stream to prevent GST_DEBUG messages at stream start.
	nolines_strain = calibration_parts.mkinsertgap(pipeline, nolines_strain, insert_gap = False, remove_gap = True)
	nolines_strain = pipeparts.mktee(pipeline, nolines_strain)

#
# NONSENS NOISE SUBTRACTION
#

if nonsenssubtractionchannel is not None:
	if not (any(line_witness_channel_list) or any(witness_channel_list)):
		nolines_strain = straintee

	nonsens_subtraction = calibration_parts.caps_and_progress(pipeline, head_dict["nonsenssubtractionchannel"], nonsens_subtraction_caps, "nonsens_subtraction")
	# If we need to filter the nonsens subtraction channel, e.g., to highpass or re-timestamp, do it now.
	if any(nonsens_firfilt):
		nonsens_subtraction = calibration_parts.mkcomplexfirbank(pipeline, nonsens_subtraction, latency = nonsens_firfilt_delay, fir_matrix = [nonsens_firfilt[::-1]], time_domain = td)
	nonsens_subtraction = calibration_parts.mkresample(pipeline, nonsens_subtraction, 4, False, hoft_caps)
	clean_strain = calibration_parts.mkadder(pipeline, [nolines_strain, calibration_parts.multiply_constant(pipeline, nonsens_subtraction, -1.0, 0.0)], queue_length = [queue_length])
	# The insertgap below just adds a group_id to the stream to prevent GST_DEBUG messages at stream start.
	clean_strain = calibration_parts.mkinsertgap(pipeline, clean_strain, insert_gap = False, remove_gap = True)
	clean_strain = pipeparts.mkprogressreport(pipeline, clean_strain, "progress_hoft_clean_%s" % instrument)
	clean_strain = pipeparts.mktee(pipeline, clean_strain)

#
# CALIB_STATE_VECTOR: CALIB_STRAIN_NOLINES AND CALIB_STRAIN_CLEAN
#

if (compute_calib_statevector or pick_cleanest_strain_channel) and (any(line_witness_channel_list) or any(witness_channel_list)):
	# Start with "no lines" channel
	low_rms_rate = pow(2, int(numpy.log(2 * cleaning_check_range_low_max) / numpy.log(2) + 1.01))
	mid_rms_rate = pow(2, int(numpy.log(2 * cleaning_check_range_mid_max) / numpy.log(2) + 1.01))
	rms_filter_latency = min(filter_latency_factor, 0.5)

	# Compute the RMS of the uncleaned strain in a low-frequency range to test subtraction of actuation lines
	strain_rms_lowfreq = calibration_parts.compute_rms(pipeline, straintee, low_rms_rate, cleaning_check_rms_time, 1, f_min = cleaning_check_range_low_min, f_max = cleaning_check_range_low_max, filter_latency = rms_filter_latency, rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))

	# Compute the RMS of the "no lines" strain in a low-frequency range
	nolines_strain_rms_lowfreq = calibration_parts.compute_rms(pipeline, nolines_strain, low_rms_rate, cleaning_check_rms_time, 1, f_min = cleaning_check_range_low_min, f_max = cleaning_check_range_low_max, filter_latency = rms_filter_latency, rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))
	# Require that ratio RMS(strain) / RMS(nolines_strain) > 1.0
	nolines_hoft_ok_lowfreq = calibration_parts.complex_division(pipeline, strain_rms_lowfreq, nolines_strain_rms_lowfreq)

	nolines_hoft_ok_lowfreq = pipeparts.mkbitvectorgen(pipeline, nolines_hoft_ok_lowfreq, bit_vector = pow(2,line_sub_bitnum), threshold=1.0)
	nolines_hoft_ok_lowfreq = pipeparts.mkcapsfilter(pipeline, nolines_hoft_ok_lowfreq, calibstate_caps)
	nolines_hoft_ok_lowfreq = pipeparts.mktee(pipeline, nolines_hoft_ok_lowfreq)

	# Compute the RMS of the uncleaned strain in a mid-frequency range to test subtraction of noise and/or the ~300 Hz pcal line
	strain_rms_midfreq = calibration_parts.compute_rms(pipeline, straintee, mid_rms_rate, cleaning_check_rms_time, 1, f_min = cleaning_check_range_mid_min, f_max = cleaning_check_range_mid_max, filter_latency = min(filter_latency_factor, 0.5), rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))
	# Compute the RMS of the "no lines" strain in a mid-frequency range
	nolines_strain_rms_midfreq = calibration_parts.compute_rms(pipeline, nolines_strain, mid_rms_rate, cleaning_check_rms_time, 1, f_min = cleaning_check_range_mid_min, f_max = cleaning_check_range_mid_max, filter_latency = min(filter_latency_factor, 0.5), rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))
	# Require that ratio RMS(strain) / RMS(nolines_strain) > 1.0
	nolines_hoft_ok_midfreq = calibration_parts.complex_division(pipeline, strain_rms_midfreq, nolines_strain_rms_midfreq)

	nolines_hoft_ok_midfreq = pipeparts.mkbitvectorgen(pipeline, nolines_hoft_ok_midfreq, bit_vector = pow(2,noise_sub_bitnum), threshold=1.0)
	nolines_hoft_ok_midfreq = pipeparts.mkcapsfilter(pipeline, nolines_hoft_ok_midfreq, calibstate_caps)
	nolines_hoft_ok_midfreq = pipeparts.mktee(pipeline, nolines_hoft_ok_midfreq)

	if compute_calib_statevector:
		# Add these into the CALIB_STATE_VECTOR
		calibstatevector = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, calibstatevector, nolines_hoft_ok_lowfreq, nolines_hoft_ok_midfreq), queue_length = [queue_length])

	if pick_cleanest_strain_channel:
		noise_sub_ok_bitmask = pow(2,line_sub_bitnum) + pow(2,noise_sub_bitnum)
		noise_sub_state_vector = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, nolines_hoft_ok_lowfreq, nolines_hoft_ok_midfreq), queue_length = [queue_length])
		if InputConfigs["datasource"] == "lvshm":
			# We don't want to add extra latency
			noise_sub_state_vector = pipeparts.mkgeneric(pipeline, noise_sub_state_vector, "lal_shift", shift = int(1000000000 * strain_channel_transition_time))
		noise_sub_state_vector = pipeparts.mktee(pipeline, noise_sub_state_vector)
		strain_coefficient = pipeparts.mkgeneric(pipeline, noise_sub_state_vector, "lal_dqtukey", required_on = noise_sub_ok_bitmask, planck_taper = True, transition_samples = hoft_sr * strain_channel_transition_time, invert_window = True)
		nolines_strain_coefficient = pipeparts.mkgeneric(pipeline, noise_sub_state_vector, "lal_dqtukey", required_on = noise_sub_ok_bitmask, transition_samples = hoft_sr * strain_channel_transition_time, planck_taper = True)
		strain = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, straintee, strain_coefficient), queue_length = [queue_length])
		nolines_strain = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, nolines_strain, nolines_strain_coefficient), queue_length = [queue_length])
		strain = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, strain, nolines_strain), queue_length = [queue_length])
		nolines_strain = strain = pipeparts.mktee(pipeline, strain)

if (compute_calib_statevector or pick_cleanest_strain_channel) and nonsenssubtractionchannel is not None:
	# Now, compute a bit for CALIB_STRAIN_CLEAN
	rms_rate = pow(2, int(numpy.log(2 * nonsens_check_range_max) / numpy.log(2) + 1.01))
	rms_filter_latency = min(filter_latency_factor, 0.5)

	# Compute the RMS of the "no lines" strain in a specified range to test nonsens subtraction
	nolines_strain_rms = calibration_parts.compute_rms(pipeline, nolines_strain, rms_rate, cleaning_check_rms_time, 1, f_min = nonsens_check_range_min, f_max = nonsens_check_range_max, filter_latency = rms_filter_latency, rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))
	# Compute the RMS of the nonsens cleaned strain in the same frequency band
	clean_strain_rms = calibration_parts.compute_rms(pipeline, clean_strain, rms_rate, cleaning_check_rms_time, 1, f_min = nonsens_check_range_min, f_max = nonsens_check_range_max, filter_latency = rms_filter_latency, rate_out = calibstate_sr, td = td, gate = noisesubgatetee, threshold = pow(2,noise_sub_gate_bitnum))
	# Require that ratio RMS(nolines_strain) / RMS(clean_strain) > 1.0
	clean_hoft_ok = calibration_parts.complex_division(pipeline, nolines_strain_rms, clean_strain_rms)
	clean_hoft_ok = pipeparts.mkbitvectorgen(pipeline, clean_hoft_ok, bit_vector = pow(2,nonsens_sub_bitnum), threshold=1.0)
	clean_hoft_ok = pipeparts.mkcapsfilter(pipeline, clean_hoft_ok, calibstate_caps)
	clean_hoft_ok = pipeparts.mktee(pipeline, clean_hoft_ok)

	if compute_calib_statevector:
		# Add these into the CALIB_STATE_VECTOR
		calibstatevector = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, calibstatevector, clean_hoft_ok), queue_length = [queue_length])

	if pick_cleanest_strain_channel:
		nonsens_sub_ok_bitmask = pow(2,nonsens_sub_bitnum)
		nonsens_sub_state_vector = clean_hoft_ok
		if InputConfigs["datasource"] == "lvshm":
			# We don't want to add extra latency
			nonsens_sub_state_vector = pipeparts.mkgeneric(pipeline, nonsens_sub_state_vector, "lal_shift", shift = int(1000000000 * strain_channel_transition_time))
		nonsens_sub_state_vector = pipeparts.mktee(pipeline, nonsens_sub_state_vector)
		nolines_strain_coefficient = pipeparts.mkgeneric(pipeline, nonsens_sub_state_vector, "lal_dqtukey", required_on = nonsens_sub_ok_bitmask, transition_samples = hoft_sr * strain_channel_transition_time, planck_taper = True, invert_window = True)
		clean_strain_coefficient = pipeparts.mkgeneric(pipeline, nonsens_sub_state_vector, "lal_dqtukey", required_on = nonsens_sub_ok_bitmask, transition_samples = hoft_sr * strain_channel_transition_time, planck_taper = True)
		nolines_strain = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, nolines_strain, nolines_strain_coefficient), queue_length = [queue_length])
		clean_strain = calibration_parts.mkmultiplier(pipeline, calibration_parts.list_srcs(pipeline, clean_strain, clean_strain_coefficient), queue_length = [queue_length])
		strain = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, nolines_strain, clean_strain), queue_length = [queue_length])
		clean_strain = nolines_strain = strain = pipeparts.mktee(pipeline, strain)

#
# CONFIGURATION INFORMATION
#

if write_config_info and compute_calib_statevector:
	calibstatevector = pipeparts.mktee(pipeline, calibstatevector)
	calib_config_hash = calibration_parts.mkinsertgap(pipeline, calibstatevector, bad_data_intervals = [0.1, 0.2], insert_gap = False, remove_gap = True, replace_value = configs_git_commit_hash_int)
	calib_config_hash = pipeparts.mkprogressreport(pipeline, calib_config_hash, "progress_calib_config_hash_%s" % instrument)
	calib_config_hash = pipeparts.mktaginject(pipeline, calib_config_hash, "remote_repository_url=%s" % configs_url)
	filters_sha256_stream = calibration_parts.mkinsertgap(pipeline, calibstatevector, bad_data_intervals = [0.1, 0.2], insert_gap = False, remove_gap = True, replace_value = filters_sha256_int)
	filters_sha256_stream = pipeparts.mkprogressreport(pipeline, filters_sha256_stream, "progress_filters_sha256_stream_%s" % instrument)
	filters_sha256_stream = pipeparts.mktaginject(pipeline, filters_sha256_stream, filters_tagstr)
	gstlal_calibration_version = calibration_parts.mkinsertgap(pipeline, calibstatevector, bad_data_intervals = [0.1, 0.2], insert_gap = False, remove_gap = True, replace_value = gstlal_calibration_version)
	gstlal_calibration_version = pipeparts.mkprogressreport(pipeline, gstlal_calibration_version, "progress_gstlal_calibration_version_%s" % instrument)

#
# INJECT TAGS
#

if (any(line_witness_channel_list) or any(witness_channel_list) or nonsenssubtractionchannel is not None) and pick_cleanest_strain_channel:
	strain = pipeparts.mktaginject(pipeline, strain, straintagstr)

if (any(line_witness_channel_list) or any(witness_channel_list)) and not pick_cleanest_strain_channel:
	nolines_straintagstr = "units=strain,channel-name=%sCALIB_STRAIN_NOLINES%s,instrument=%s" % (chan_prefix, chan_suffix, instrument)
	nolines_strain = pipeparts.mktaginject(pipeline, nolines_strain, nolines_straintagstr)

if nonsenssubtractionchannel is not None and not pick_cleanest_strain_channel:
	clean_straintagstr = "units=strain,channel-name=%sCALIB_STRAIN_CLEAN%s,instrument=%s" % (chan_prefix, chan_suffix, instrument)
	clean_strain = pipeparts.mktaginject(pipeline, clean_strain, clean_straintagstr)

# Check if we gated the cleaning, and if so, add that to the CALIB_STATE_VECTOR
if noisesubgatetee is not None:
	calibstatevector = calibration_parts.mkadder(pipeline, calibration_parts.list_srcs(pipeline, calibstatevector, noisesubgatetee), queue_length = [queue_length])

if compute_calib_statevector:
	# The insertgap below just adds a group_id to the stream to prevent GST_DEBUG messages at stream start.
	calibstatevector = calibration_parts.mkinsertgap(pipeline, calibstatevector, insert_gap = False, remove_gap = True)
	calibstatevector = pipeparts.mkprogressreport(pipeline, calibstatevector, "progress_calibstatevec_%s" % instrument)
	if test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None:
		calibstatevector = pipeparts.mktee(pipeline, calibstatevector)
		calibstatevector_latency = pipeparts.mklatency(pipeline, calibstatevector, name = "%s_calibstatevec" % OutputConfigs["frametype"], silent = silent)
		if kafka_server is not None:
			# Dump latency info to kafka
			calibstatevector_latency.connect("notify::current-latency", handler.latency_new_buffer)
			if monitor_statevector:
				# Dump statevector metric info to kafka
				statevector_monitor = pipeparts.mkgeneric(pipeline, calibstatevector, "lal_nxydump")
				statevector_monitor = pipeparts.mkappsink(pipeline, statevector_monitor, max_buffers = 1)	
				statevector_monitor.connect("new-sample", handler.appsink_statevector_new_buffer, instrument, monitor_bitmask_dict)
		pipeparts.mkfakesink(pipeline, calibstatevector_latency)
	dqtagstr = "channel-name=%s:GDS-CALIB_STATE_VECTOR, instrument=%s" % (instrument, instrument)
	calibstatevector = pipeparts.mktaginject(pipeline, calibstatevector, dqtagstr)

#
# Produce time-dependent correction factors to be recorded in the frames
#

record_kappa_caps = "audio/x-raw, format=F32LE, rate=%d" % record_factors_sr

# Resample the \kappa_tst channels at the specified recording sample rate and change them to single precision channels
if compute_kappatst:

	ktstRout = pipeparts.mkgeneric(pipeline, smooth_ktstRtee, "lal_typecast")
	ktstRout = calibration_parts.mkresample(pipeline, ktstRout, 1, False, record_kappa_caps)
	ktstRout = pipeparts.mkprogressreport(pipeline, ktstRout, "progress_kappa_tst_real_%s" % instrument)
	if test_latency:
		ktstRout = pipeparts.mklatency(pipeline, ktstRout, name = "%s_kappa_tst_real" % OutputConfigs["frametype"])

	ktstIout = pipeparts.mkgeneric(pipeline, smooth_ktstItee, "lal_typecast")
	ktstIout = calibration_parts.mkresample(pipeline, ktstIout, 1, False, record_kappa_caps)
	ktstIout = pipeparts.mkprogressreport(pipeline, ktstIout, "progress_kappa_tst_imag_%s" % instrument)
	if test_latency:
		ktstIout = pipeparts.mklatency(pipeline, ktstIout, name = "%s_kappa_tst_imag" % OutputConfigs["frametype"])

	if compute_tdcf_uncertainty:
		ktst_unc = pipeparts.mkgeneric(pipeline, ktst_unc, "lal_typecast")
		ktst_unc = calibration_parts.mkresample(pipeline, ktst_unc, 1, False, record_kappa_caps)
		ktst_unc = pipeparts.mkprogressreport(pipeline, ktst_unc, "progress_kappa_tst_uncertainty_%s" % instrument)
		if test_latency:
			ktst_unc = pipeparts.mklatency(pipeline, ktst_unc, name = "%s_kappa_tst_uncertainty" % OutputConfigs["frametype"])
		tau_tst_unc = pipeparts.mkgeneric(pipeline, tau_tst_unc, "lal_typecast")
		tau_tst_unc = calibration_parts.mkresample(pipeline, tau_tst_unc, 1, False, record_kappa_caps)
		tau_tst_unc = pipeparts.mkprogressreport(pipeline, tau_tst_unc, "progress_tau_tst_uncertainty_%s" % instrument)
		if test_latency:
			tau_tst_unc = pipeparts.mklatency(pipeline, tau_tst_unc, name = "%s_tau_tst_uncertainty" % OutputConfigs["frametype"])

# Resample the \kappa_pum channels at the specified recording sample rate and change them to single precision channels
if compute_kappapum:

	kpumRout = pipeparts.mkgeneric(pipeline, smooth_kpumRtee, "lal_typecast")
	kpumRout = calibration_parts.mkresample(pipeline, kpumRout, 1, False, record_kappa_caps)
	kpumRout = pipeparts.mkprogressreport(pipeline, kpumRout, "progress_kappa_pum_real_%s" % instrument)
	if test_latency:
		kpumRout = pipeparts.mklatency(pipeline, kpumRout, name = "%s_kappa_pum_real" % OutputConfigs["frametype"])

	kpumIout = pipeparts.mkgeneric(pipeline, smooth_kpumItee, "lal_typecast")
	kpumIout = calibration_parts.mkresample(pipeline, kpumIout, 1, False, record_kappa_caps)
	kpumIout = pipeparts.mkprogressreport(pipeline, kpumIout, "progress_kappa_pum_imag_%s" % instrument)
	if test_latency:
		kpumIout = pipeparts.mklatency(pipeline, kpumIout, name = "%s_kappa_pum_imag" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		kpum_unc = pipeparts.mkgeneric(pipeline, kpum_unc, "lal_typecast")
		kpum_unc = calibration_parts.mkresample(pipeline, kpum_unc, 1, False, record_kappa_caps)
		kpum_unc = pipeparts.mkprogressreport(pipeline, kpum_unc, "progress_kappa_pum_uncertainty_%s" % instrument)
		if test_latency:
			kpum_unc = pipeparts.mklatency(pipeline, kpum_unc, name = "%s_kappa_pum_uncertainty" % OutputConfigs["frametype"])
		tau_pum_unc = pipeparts.mkgeneric(pipeline, tau_pum_unc, "lal_typecast")
		tau_pum_unc = calibration_parts.mkresample(pipeline, tau_pum_unc, 1, False, record_kappa_caps)
		tau_pum_unc = pipeparts.mkprogressreport(pipeline, tau_pum_unc, "progress_tau_pum_uncertainty_%s" % instrument)
		if test_latency:
			tau_pum_unc = pipeparts.mklatency(pipeline, tau_pum_unc, name = "%s_tau_pum_uncertainty" % OutputConfigs["frametype"])

# Resample the \kappa_uim channels at the specified recording sample rate and change them to single precision channels
if compute_kappauim:

	kuimRout = pipeparts.mkgeneric(pipeline, smooth_kuimRtee, "lal_typecast")
	kuimRout = calibration_parts.mkresample(pipeline, kuimRout, 1, False, record_kappa_caps)
	kuimRout = pipeparts.mkprogressreport(pipeline, kuimRout, "progress_kappa_uim_real_%s" % instrument)
	if test_latency:
		kuimRout = pipeparts.mklatency(pipeline, kuimRout, name = "%s_kappa_uim_real" % (OutputConfigs["frametype"]))

	kuimIout = pipeparts.mkgeneric(pipeline, smooth_kuimItee, "lal_typecast")
	kuimIout = calibration_parts.mkresample(pipeline, kuimIout, 1, False, record_kappa_caps)
	kuimIout = pipeparts.mkprogressreport(pipeline, kuimIout, "progress_kappa_uim_imag_%s" % instrument)
	if test_latency:
		kuimIout = pipeparts.mklatency(pipeline, kuimIout, name = "%s_kappa_uim_imag" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		kuim_unc = pipeparts.mkgeneric(pipeline, kuim_unc, "lal_typecast")
		kuim_unc = calibration_parts.mkresample(pipeline, kuim_unc, 1, False, record_kappa_caps)
		kuim_unc = pipeparts.mkprogressreport(pipeline, kuim_unc, "progress_kappa_uim_uncertainty_%s" % instrument)
		if test_latency:
			kuim_unc = pipeparts.mklatency(pipeline, kuim_unc, name = "%s_kappa_uim_uncertainty" % OutputConfigs["frametype"])
		tau_uim_unc = pipeparts.mkgeneric(pipeline, tau_uim_unc, "lal_typecast")
		tau_uim_unc = calibration_parts.mkresample(pipeline, tau_uim_unc, 1, False, record_kappa_caps)
		tau_uim_unc = pipeparts.mkprogressreport(pipeline, tau_uim_unc, "progress_tau_uim_uncertainty_%s" % instrument)
		if test_latency:
			tau_uim_unc = pipeparts.mklatency(pipeline, tau_uim_unc, name = "%s_tau_uim_uncertainty" % OutputConfigs["frametype"])

# Resample the \kappa_c channels at the specified recording sample rate and change it to a single precision channel
if compute_kappac:
	kcout = pipeparts.mkgeneric(pipeline, smooth_kctee, "lal_typecast")
	kcout = calibration_parts.mkresample(pipeline, kcout, 1, False, record_kappa_caps)
	kcout = pipeparts.mkprogressreport(pipeline, kcout, "progress_kappa_c_%s" % instrument)
	if test_latency:
		kcout = pipeparts.mklatency(pipeline, kcout, name = "%s_kappa_c_imag" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		kc_unc = pipeparts.mkgeneric(pipeline, kc_unc, "lal_typecast")
		kc_unc = calibration_parts.mkresample(pipeline, kc_unc, 1, False, record_kappa_caps)
		kc_unc = pipeparts.mkprogressreport(pipeline, kc_unc, "progress_kappa_c_uncertainty_%s" % instrument)
		if test_latency:
			kc_unc = pipeparts.mklatency(pipeline, kc_unc, name = "%s_kappa_c_uncertainty" % OutputConfigs["frametype"])

# Resample the f_cc channels at the specified recording sample rate and change it to a single precision channel
if compute_fcc:
	fccout = pipeparts.mkgeneric(pipeline, smooth_fcctee, "lal_typecast")
	fccout = calibration_parts.mkresample(pipeline, fccout, 1, False, record_kappa_caps)
	fccout = pipeparts.mkprogressreport(pipeline, fccout, "progress_f_cc_%s" % instrument)
	if test_latency:
		fccout = pipeparts.mklatency(pipeline, fccout, name = "%s_f_cc_imag" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		fcc_unc_abs = pipeparts.mkgeneric(pipeline, fcc_unc_abs, "lal_typecast")
		fcc_unc_abs = calibration_parts.mkresample(pipeline, fcc_unc_abs, 1, False, record_kappa_caps)
		fcc_unc_abs = pipeparts.mkprogressreport(pipeline, fcc_unc_abs, "progress_f_cc_uncertainty_%s" % instrument)
		if test_latency:
			fcc_unc_abs = pipeparts.mklatency(pipeline, fcc_unc_abs, name = "%s_f_cc_uncertainty" % OutputConfigs["frametype"])

# Resample the f_s channels at the specified recording sample rate and change it to a single precision channel
if compute_fs:
	fs_squared_out = pipeparts.mkgeneric(pipeline, smooth_fs_squared, "lal_typecast")
	fs_squared_out = calibration_parts.mkresample(pipeline, fs_squared_out, 1, False, record_kappa_caps)
	fs_squared_out = pipeparts.mkprogressreport(pipeline, fs_squared_out, "progress_f_s_squared_%s" % instrument)
	if test_latency:
		fs_squared_out = pipeparts.mklatency(pipeline, fs_squared_out, name = "%s_f_s_squared" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		fs_squared_unc_abs = pipeparts.mkgeneric(pipeline, fs_squared_unc_abs, "lal_typecast")
		fs_squared_unc_abs = calibration_parts.mkresample(pipeline, fs_squared_unc_abs, 1, False, record_kappa_caps)
		fs_squared_unc_abs = pipeparts.mkprogressreport(pipeline, fs_squared_unc_abs, "progress_f_s_squared_uncertainty_%s" % instrument)
		if test_latency:
			fs_squared_unc_abs = pipeparts.mklatency(pipeline, fs_squared_unc_abs, name = "%s_f_s_squared_uncertainty" % OutputConfigs["frametype"])

# Resample the SRC Q channels at the specified recording sample rate and change it to a single precision channel
if compute_srcq:
	srcQ_inv_out = pipeparts.mkgeneric(pipeline, smooth_srcQ_inv_real, "lal_typecast")
	srcQ_inv_out = calibration_parts.mkresample(pipeline, srcQ_inv_out, 1, False, record_kappa_caps)
	srcQ_inv_out = pipeparts.mkprogressreport(pipeline, srcQ_inv_out, "progress_SRC_Q_%s" % instrument)
	if test_latency:
		srcQ_inv_out = pipeparts.mklatency(pipeline, srcQ_inv_out, name = "%s_SRC_Q" % (OutputConfigs["frametype"]))

	if compute_tdcf_uncertainty:
		fs_over_Q_unc_abs = pipeparts.mkgeneric(pipeline, fs_over_Q_unc_abs, "lal_typecast")
		fs_over_Q_unc_abs = calibration_parts.mkresample(pipeline, fs_over_Q_unc_abs, 1, False, record_kappa_caps)
		fs_over_Q_unc_abs = pipeparts.mkprogressreport(pipeline, fs_over_Q_unc_abs, "progress_f_s_over_Q_uncertainty_%s" % instrument)
		if test_latency:
			fs_over_Q_unc_abs = pipeparts.mklatency(pipeline, fs_over_Q_unc_abs, name = "%s_f_s_over_Q_uncertainty" % OutputConfigs["frametype"])

#
# CREATE MUXER AND HOOK EVERYTHING UP TO IT
#

channelmux_input_dict = {}
# Link the output DQ vectors up to the muxer, if applicable
if compute_calib_statevector: 
	channelmux_input_dict["%s:%sCALIB_STATE_VECTOR%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, calibstatevector, length = queue_length)
# Link the config info channels to the muxer
if write_config_info and compute_calib_statevector:
	channelmux_input_dict["%s:%sCALIB_GSTLAL_CONFIG_HASH_INT%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, calib_config_hash, length = queue_length)
	channelmux_input_dict["%s:%sCALIB_REPORT_GDS_HASH_INT%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, filters_sha256_stream, length = queue_length)
	channelmux_input_dict["%s:%sCALIB_GSTLAL_CALIBRATION_VERSION_INT%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, gstlal_calibration_version, length = queue_length)
# Link the strain branch to the muxer
channelmux_input_dict["%s:%sCALIB_STRAIN%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, strain, length = queue_length)
# Link the "no lines" strain branch to the muxer
if (any(line_witness_channel_list) or any(witness_channel_list)) and not pick_cleanest_strain_channel:
	channelmux_input_dict["%s:%sCALIB_STRAIN_NOLINES%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, nolines_strain, length = queue_length)
# Link the nonsens cleaned strain branch to the muxer
if nonsenssubtractionchannel is not None and not pick_cleanest_strain_channel:
	channelmux_input_dict["%s:%sCALIB_STRAIN_CLEAN%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, clean_strain, length = queue_length)
# Link the real and imaginary parts of kappa_tst to the muxer
if compute_kappatst:
	channelmux_input_dict["%s:%sCALIB_KAPPA_TST_REAL%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, ktstRout, length = queue_length)
	channelmux_input_dict["%s:%sCALIB_KAPPA_TST_IMAGINARY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, ktstIout, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_KAPPA_TST_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, ktst_unc, length = queue_length)
		channelmux_input_dict["%s:%sCALIB_TAU_TST_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, tau_tst_unc, length = queue_length)
# Link the real and imaginary parts of kappa_pum to the muxer
if compute_kappapum:
	channelmux_input_dict["%s:%sCALIB_KAPPA_PUM_REAL%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kpumRout, length = queue_length)
	channelmux_input_dict["%s:%sCALIB_KAPPA_PUM_IMAGINARY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kpumIout, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_KAPPA_PUM_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kpum_unc, length = queue_length)
		channelmux_input_dict["%s:%sCALIB_TAU_PUM_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, tau_pum_unc, length = queue_length)
# Link the real and imaginary parts of kappa_uim to the muxer
if compute_kappauim:
	channelmux_input_dict["%s:%sCALIB_KAPPA_UIM_REAL%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kuimRout, length = queue_length)
	channelmux_input_dict["%s:%sCALIB_KAPPA_UIM_IMAGINARY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kuimIout, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_KAPPA_UIM_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kuim_unc, length = queue_length)
		channelmux_input_dict["%s:%sCALIB_TAU_UIM_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, tau_uim_unc, length = queue_length)
# Link the \kappa_c to the muxer
if compute_kappac:
	channelmux_input_dict["%s:%sCALIB_KAPPA_C%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kcout, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_KAPPA_C_UNCERTAINTY%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, kc_unc, length = queue_length)
# Link the f_cc to the muxer
if compute_fcc:
	channelmux_input_dict["%s:%sCALIB_F_CC%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, fccout, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_F_CC_UNCERTAINTY_HZ%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, fcc_unc_abs, length = queue_length)
# Link the f_s to the muxer
if compute_fs:
	channelmux_input_dict["%s:%sCALIB_F_S_SQUARED%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, fs_squared_out, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_F_S_SQUARED_UNCERTAINTY_HZ_SQUARED%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, fs_squared_unc_abs, length = queue_length)
# Link the src_Q to the muxer
if compute_srcq:
	channelmux_input_dict["%s:%sCALIB_SRC_Q_INVERSE%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, srcQ_inv_out, length = queue_length)
	if compute_tdcf_uncertainty:
		channelmux_input_dict["%s:%sCALIB_F_S_OVER_Q_UNCERTAINTY_HZ%s" % (instrument, chan_prefix, chan_suffix)] = calibration_parts.mkqueue(pipeline, fs_over_Q_unc_abs, length = queue_length)

mux = calibration_parts.mkframecppchannelmux(pipeline, channelmux_input_dict, frame_duration = options.frame_duration, frames_per_file = options.frames_per_file, compression_scheme = int(OutputConfigs["compressionscheme"]), compression_level = int(OutputConfigs["compressionlevel"]))

# Check that all frames are long enough, that they have all of the channels by requiring a certain amount of time from start-up, and that frames aren't written for times requested by the wings option
def check_complete_frames(pad, info, output_start, frame_duration, wings_start, wings_end):
	if verbose:
		print("Checking if frames are complete", file=sys.stderr)
	buf = info.get_buffer()
	startts = lal.LIGOTimeGPS(0, buf.pts)
	duration = lal.LIGOTimeGPS(0, buf.duration)
	if not (startts % frame_duration == 0):
		if verbose:
			print("Dropping frame because it is not an integer multiple of frame duration", file=sys.stderr)
		return Gst.PadProbeReturn.DROP
	if startts < output_start:
		if verbose:
			print("Dropping frame because start time %f is less than desired output start time %f" % (startts, output_start), file=sys.stderr)
		return Gst.PadProbeReturn.DROP
	if duration != frame_duration:
		if verbose:
			print("Dropping frame because the duration %d is not equal to the required frame duration %d" % (duration, frame_duration), file=sys.stderr)
		return Gst.PadProbeReturn.DROP
	if wings_start is not None and wings_end is not None:
		if startts < wings_start or (startts+duration) > wings_end:
			if verbose:
				print("Dropping frame because it lies outside of the wings time", file=sys.stderr)
			return Gst.PadProbeReturn.DROP
	return Gst.PadProbeReturn.OK

# start time of first frame file is the desired start time + either filter latency or kappa settling (if computing kappas), whichever is bigger
if compute_kappatst or compute_kappapum or compute_kappauim or compute_kappac or compute_fcc or compute_fs or compute_srcq:
	output_start = start + max(int(numpy.ceil(filter_settle_time)), int(numpy.ceil((1.0 - filter_latency_factor) * demodulation_filter_time)))
	# + int(TDCFConfigs["mediansmoothingtime"]) + int(TDCFConfigs["tdcfaveragingtime"]))))
else:
	output_start = start + int(numpy.ceil(filter_settle_time))

# If the wings option is set, need to also check that frames aren't written during the requested wing time
wings = int(options.wings)
if wings != 0:
	wings_start = gps_start_time + wings
	wings_end = gps_end_time - wings
	mux.get_static_pad("src").add_probe(Gst.PadProbeType.BUFFER, check_complete_frames, lal.LIGOTimeGPS(output_start,0), lal.LIGOTimeGPS(options.frame_duration*options.frames_per_file,0), lal.LIGOTimeGPS(wings_start, 0), lal.LIGOTimeGPS(wings_end, 0))
else:
	mux.get_static_pad("src").add_probe(Gst.PadProbeType.BUFFER, check_complete_frames, lal.LIGOTimeGPS(output_start,0), lal.LIGOTimeGPS(options.frame_duration*options.frames_per_file,0), None, None)

mux = pipeparts.mkprogressreport(pipeline, mux, "progress_sink_%s" % instrument)

if test_latency or InputConfigs["datasource"] == "lvshm" and kafka_server is not None:
	mux = pipeparts.mktee(pipeline, mux)
	mux_latency = pipeparts.mklatency(pipeline, mux, name = "%s_sink" % OutputConfigs["frametype"], silent = silent)
	if kafka_server is not None:
		mux_latency.connect("notify::current-latency", handler.latency_new_buffer)
	pipeparts.mkfakesink(pipeline, mux_latency)

if ci_latency_dir is not None:
	mux = pipeparts.mkgeneric(pipeline, mux, "splitcounter", filename = "%s/output_unix_timestamps.txt" % ci_latency_dir)

if OutputConfigs["datasink"] == "lvshm":
	pipeparts.mkgeneric(pipeline, mux, "gds_lvshmsink", sync=False, async_=False, shm_name = OutputConfigs["outputshmpartition"], num_buffers = int(OutputConfigs["numbuffers"]), blocksize = int(OutputConfigs["framesize"])*options.frame_duration*options.frames_per_file, buffer_mode = int(OutputConfigs["buffermode"]))
elif OutputConfigs["datasink"] == "frames":
	pipeparts.mkframecppfilesink(pipeline, mux, frame_type = OutputConfigs["frametype"], path = options.output_path, instrument = instrument) 

# Run pipeline

if DebuggingConfigs["pipelinegraphfilename"] != "None":
	pipeparts.write_dump_dot(pipeline, "%s.%s" %(DebuggingConfigs["pipelinegraphfilename"], "NULL"), verbose = verbose)

# Seek the pipeline when necessary
if InputConfigs["datasource"] == "frames":
	if verbose:
		print("seeking GPS start and stop times ...", file=sys.stderr)
	if pipeline.set_state(Gst.State.READY) != Gst.StateChangeReturn.SUCCESS:
		raise RuntimeError("pipeline failed to enter READY state")
	datasource.pipeline_seek_for_gps(pipeline, gps_start_time, gps_end_time)

if verbose:
	print("setting pipeline state to playing ...", file=sys.stderr)
if pipeline.set_state(Gst.State.PLAYING) != Gst.StateChangeReturn.SUCCESS:
	raise RuntimeError("pipeline failed to enter PLAYING state")
else:
	if verbose:
		print("set to playing successfully", file=sys.stderr)
if DebuggingConfigs["pipelinegraphfilename"] != "None":
	pipeparts.write_dump_dot(pipeline, "%s.%s" %(DebuggingConfigs["pipelinegraphfilename"], "PLAYING"), verbose = verbose)
	
if verbose:
	print("running pipeline ...", file=sys.stderr)

mainloop.run()

if pipeline.set_state(Gst.State.NULL) != Gst.StateChangeReturn.SUCCESS:
	raise RuntimeError("pipeline could not be set to NULL")
