diff --git a/gstlal-inspiral/bin/Makefile.am b/gstlal-inspiral/bin/Makefile.am
index d53b492ef07411ac224f95711b8e5bcc570864f5..96670fafb33fded0057378f4985ad6c1de5963d5 100644
--- a/gstlal-inspiral/bin/Makefile.am
+++ b/gstlal-inspiral/bin/Makefile.am
@@ -11,7 +11,6 @@ dist_bin_SCRIPTS = \
 	gstlal_inspiral_create_p_of_ifos_given_horizon \
 	gstlal_inspiral_create_prior_diststats \
 	gstlal_inspiral_dlrs_diag \
-	gstlal_inspiral_fake_zerolag_counts \
 	gstlal_inspiral_iir_bank_pipe \
 	gstlal_inspiral_injection_snr \
 	gstlal_inspiral_lvalert_background_plotter \
diff --git a/gstlal-inspiral/bin/gstlal_inspiral b/gstlal-inspiral/bin/gstlal_inspiral
index 78581937e97c4b25dc88476994c83d111dc22cbf..1dc707fe69f8890e2716b486edee5ed346c22e1e 100755
--- a/gstlal-inspiral/bin/gstlal_inspiral
+++ b/gstlal-inspiral/bin/gstlal_inspiral
@@ -148,8 +148,8 @@
 #	+ `--job-tag`: Set the string to identify this job and register the resources it provides on a node.  Should be 4 digits of the form 0001, 0002, etc..
 #	+ `--ranking-stat-output` [filename]: Set the name of the file to which to write ranking statistic data collected from triggers (optional).  Can be given more than once.  If given, exactly as many must be provided as there are --svd-bank options and they will be writen to in order.
 #	+ `--ranking-stat-output-cache` [filename]: Provide a cache of ranking statistic output files.  This can be used instead of giving multiple --ranking-stat-output options.  Cannot be combined with --ranking-stat-output.
-#	+ `--ranking-stat-input` [filename]: Set the URL from which to load a ranking statistic definition.  When this is enabled, signal candidates will have ranking statistic values assigned on-the-fly, and the --min-log-L cut will be applied based on the assigned values.  Can only use with --data-source lvshm or framexmit;  must also set --likelihood-snapshot-interval.
-#	+ `--zerolag-rankingstatpdf-filename` [filename]: Record a histogram of the likelihood ratio ranking statistic values assigned to zero-lag candidates in this XML file, which must exist at start up and contain a RankingStatPDF object.  The counts will be added to the file.  Optional.  Can be given multiple times.
+#	+ `--ranking-stat-input` [filename]: Set the URL from which to load a ranking statistic definition.  When this is enabled, signal candidates will have ranking statistic values assigned on-the-fly, and the --min-log-L cut will be applied based on the assigned values.  Required when --data-source is lvshm or framexmit;  must also set --likelihood-snapshot-interval.
+#	+ `--zerolag-rankingstat-pdf` [url]: Record a histogram of the likelihood ratio ranking statistic values assigned to zero-lag candidates in this XML file.  This is used to construct the extinction model and set the overall false-alarm rate normalization during online running.  If it does not exist at start-up, a new file will be initialized, otherwise the counts will be added to the file's contents.  Required when --data-source is lvshm or framexmit;  optional otherwise.  If given, exactly as many must be provided as there are --svd-bank options and they will be used in order.
 #	+ `--likelihood-snapshot-interval` [seconds] (float): How often to snapshot candidate and ranking statistic data to disk when running online.
 #	+ `--ranking-stat-pdf` [url]: Set the URL from which to load the ranking statistic PDF.  This is used to compute false-alarm probabilities and false-alarm rates and is required for online operation (when --data-source is framexmit or lvshm).  It is forbidden for offline operation (all other data sources).
 #	+ `--gracedb-far-threshold` (float): False-alarm rate threshold for gracedb uploads in Hertz (default = do not upload to gracedb).
@@ -297,13 +297,13 @@ def parse_command_line():
 	group.add_option("--coincidence-threshold", metavar = "seconds", type = "float", default = 0.005, help = "Set the coincidence window in seconds (default = 0.005 s).  The light-travel time between instruments will be added automatically in the coincidence test.")
 	group.add_option("--min-instruments", metavar = "count", type = "int", default = 2, help = "Set the minimum number of instruments that must contribute triggers to form a candidate (default = 2).")
 	group.add_option("--min-log-L", metavar = "log likelihood ratio", type = "float", help = "Discard candidates that get assigned log likelihood ratios below this threshold (default = keep all).  When used without --ranking-stat-input, the cut is decided based on an internal approximate ranking statistic.")
-	group.add_option("--ranking-stat-input", metavar = "url", help = "Set the URL from which to load a ranking statistic definition.  When this is enabled, signal candidates will have ranking statistic values assigned on-the-fly, and the --min-log-L cut will be applied based on the assigned values.  Can only use with --data-source lvshm or framexmit;  must also set --likelihood-snapshot-interval.")
+	group.add_option("--ranking-stat-input", metavar = "url", help = "Set the URL from which to load a ranking statistic definition.  When this is enabled, signal candidates will have ranking statistic values assigned on-the-fly, and the --min-log-L cut will be applied based on the assigned values.  Required when --data-source is lvshm or framexmit;  must also set --likelihood-snapshot-interval.")
 	group.add_option("--ranking-stat-output", metavar = "filename", action = "append", default = [], help = "Set the name of the file to which to write ranking statistic data collected from triggers (optional).  Can be given more than once.  If given, exactly as many must be provided as there are --svd-bank options and they will be writen to in order.")
 	group.add_option("--ranking-stat-output-cache", metavar = "filename", help = "Provide a cache of ranking statistic output files.  This can be used instead of giving multiple --ranking-stat-output options.  Cannot be combined with --ranking-stat-output.")
 	group.add_option("--likelihood-snapshot-interval", type = "float", metavar = "seconds", help = "How often to snapshot candidate and ranking statistic data to disk when running online.")
 	group.add_option("--ranking-stat-pdf", metavar = "url", help = "Set the URL from which to load the ranking statistic PDF.  This is used to compute false-alarm probabilities and false-alarm rates and is required for online operation (when --data-source is framexmit or lvshm).  It is forbidden for offline operation (all other data sources)")
 	group.add_option("--time-slide-file", metavar = "filename", help = "Set the name of the xml file to get time slide offsets (required).")
-	group.add_option("--zerolag-rankingstatpdf-filename", metavar = "filename", action = "append", help = "Record a histogram of the likelihood ratio ranking statistic values assigned to zero-lag candidates in this XML file, which must exist at start up and contain a RankingStatPDF object.  The counts will be added to the file.  Optional.  Can be given multiple times.")
+	group.add_option("--zerolag-rankingstat-pdf", metavar = "url", action = "append", help = "Record a histogram of the likelihood ratio ranking statistic values assigned to zero-lag candidates in this XML file.  This is used to construct the extinction model and set the overall false-alarm rate normalization during online running.  If it does not exist at start-up, a new file will be initialized, otherwise the counts will be added to the file's contents.  Required when --data-source is lvshm or framexmit;  optional otherwise.  If given, exactly as many must be provided as there are --svd-bank options and they will be used in order.")
 	parser.add_option_group(group)
 
 	group = OptionGroup(parser, "GracedB Options", "Adjust GracedB interaction")
@@ -429,7 +429,7 @@ def parse_command_line():
 
 	if options.data_source in ("lvshm", "framexmit"):
 		missing_options = []
-		for option in ["job_tag", "ranking_stat_pdf"]:
+		for option in ["job_tag", "ranking_stat_input", "ranking_stat_pdf", "zerolag_rankingstat_pdf"]:
 			if getattr(options, option) is None:
 				missing_options.append("--%s" %option.replace("_","-"))
 
@@ -473,10 +473,10 @@ def parse_command_line():
 	elif len(options.ht_gate_threshold) != len(svd_banks):
 		raise ValueError("must supply either none or exactly as many --ht-gate-threshold values options as --svd-bank")
 
-	if not options.zerolag_rankingstatpdf_filename:
-		options.zerolag_rankingstatpdf_filename = [None] * len(svd_banks)
-	elif len(options.zerolag_rankingstatpdf_filename) != len(svd_banks):
-		raise ValueError("must supply either none or exactly as many --zerolag-rankingstatpdf-filename options as --svd-bank")
+	if not options.zerolag_rankingstat_pdf:
+		options.zerolag_rankingstat_pdf = [None] * len(svd_banks)
+	elif len(options.zerolag_rankingstat_pdf) != len(svd_banks):
+		raise ValueError("must supply either none or exactly as many --zerolag-rankingstat-pdf options as --svd-bank")
 
 	if options.min_instruments < 1:
 		raise ValueError("--min-instruments must be >= 1")
@@ -625,7 +625,7 @@ else:
 #
 
 
-for output_file_number, (svd_bank_url_dict, output_url, ranking_stat_output_url, zerolag_rankingstatpdf_filename, ht_gate_threshold) in enumerate(zip(svd_banks, options.output, options.ranking_stat_output, options.zerolag_rankingstatpdf_filename, options.ht_gate_threshold)):
+for output_file_number, (svd_bank_url_dict, output_url, ranking_stat_output_url, zerolag_rankingstat_pdf, ht_gate_threshold) in enumerate(zip(svd_banks, options.output, options.ranking_stat_output, options.zerolag_rankingstat_pdf, options.ht_gate_threshold)):
 	#
 	# Checkpointing only supported for gzip files in offline analysis
 	# FIXME Implement a means by which to check for sqlite file
@@ -766,22 +766,27 @@ for output_file_number, (svd_bank_url_dict, output_url, ranking_stat_output_url,
 	#
 
 
-	if options.data_source in ("lvshm", "framexmit"):
-		assert ranking_stat_output_url is not None
-		rankingstat, _ = far.parse_likelihood_control_doc(ligolw_utils.load_url(ranking_stat_output_url, verbose = options.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
-		if rankingstat is None:
-			raise ValueError("\"%s\" does not contain parameter distribution data" % ranking_stat_output_url)
-		if rankingstat.delta_t != options.coincidence_threshold:
-			raise ValueError("\"%s\" is for delta_t=%g, we need %g" % (ranking_stat_output_url, rankingstat.denominator.delta_t, options.coincidence_threshold))
-		if rankingstat.min_instruments != options.min_instruments:
-			raise ValueError("\"%s\" is for min instruments = %d but we need %d" % (ranking_stat_output_url, rankingstat.denominator.min_instruments, options.min_instruments))
-		if rankingstat.instruments != all_instruments:
-			raise ValueError("\"%s\" is for %s but we need %s" % (ranking_stat_output_url, ", ".join(sorted(rankingstat.instruments)), ", ".join(sorted(all_instruments))))
-		if rankingstat.template_ids is None:
-			rankingstat.template_ids = template_ids
-		elif rankingstat.template_ids != template_ids:
-			raise ValueError("\"%s\" is for the wrong templates")
-	else:
+	rankingstat = None
+	if options.ranking_stat_input:
+		try:
+			xmldoc = ligolw_utils.load_url(options.ranking_stat_input, verbose = options.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler)
+		except IOError:
+			print >>sys.stderr, "warning:  '%s' not found or cannot be loaded;  will fall back to newly-initialized ranking statistic" % options.ranking_stat_input
+		else:
+			rankingstat, _ = far.parse_likelihood_control_doc(xmldoc)
+			if rankingstat is None:
+				raise ValueError("\"%s\" does not contain parameter distribution data" % options.ranking_stat_input)
+			if rankingstat.delta_t != options.coincidence_threshold:
+				raise ValueError("\"%s\" is for delta_t=%g, we need %g" % (options.ranking_stat_input, rankingstat.denominator.delta_t, options.coincidence_threshold))
+			if rankingstat.min_instruments != options.min_instruments:
+				raise ValueError("\"%s\" is for min instruments = %d but we need %d" % (options.ranking_stat_input, rankingstat.denominator.min_instruments, options.min_instruments))
+			if rankingstat.instruments != all_instruments:
+				raise ValueError("\"%s\" is for %s but we need %s" % (options.ranking_stat_input, ", ".join(sorted(rankingstat.instruments)), ", ".join(sorted(all_instruments))))
+			if rankingstat.template_ids is None:
+				rankingstat.template_ids = template_ids
+			elif rankingstat.template_ids != template_ids:
+				raise ValueError("\"%s\" is for the wrong templates")
+	if rankingstat is None:
 		rankingstat = far.RankingStat(template_ids = template_ids, instruments = all_instruments, delta_t = options.coincidence_threshold, min_instruments = options.min_instruments)
 		rankingstat.numerator.add_signal_model(df = 40)
 
@@ -808,7 +813,7 @@ for output_file_number, (svd_bank_url_dict, output_url, ranking_stat_output_url,
 		),
 		pipeline = pipeline,
 		rankingstat = rankingstat,
-		zerolag_rankingstatpdf_filename = zerolag_rankingstatpdf_filename,
+		zerolag_rankingstatpdf_url = zerolag_rankingstat_pdf,
 		ranking_stat_input_url = options.ranking_stat_input,
 		ranking_stat_output_url = ranking_stat_output_url,
 		rankingstatpdf_url = options.ranking_stat_pdf,
diff --git a/gstlal-inspiral/bin/gstlal_inspiral_fake_zerolag_counts b/gstlal-inspiral/bin/gstlal_inspiral_fake_zerolag_counts
deleted file mode 100755
index 3d6e976bebb2a5d52bcbb4d2b7ec71cc339c104c..0000000000000000000000000000000000000000
--- a/gstlal-inspiral/bin/gstlal_inspiral_fake_zerolag_counts
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2017  Kipp Cannon, Chad Hanna
-#
-# 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.
-
-
-#
-# =============================================================================
-#
-#                                   Preamble
-#
-# =============================================================================
-#
-
-
-from optparse import OptionParser
-import sys
-
-
-from glue.ligolw import ligolw
-from glue.ligolw import utils as ligolw_utils
-from glue.ligolw.utils import process as ligolw_process
-from gstlal import far
-
-
-process_name = u"gstlal_inspiral_fake_zerolag_counts"
-
-
-__author__ = "Kipp Cannon <kipp.cannon@ligo.org>"
-__version__ = "git id %s" % ""	# FIXME
-__date__ = ""	# FIXME
-
-
-#
-# =============================================================================
-#
-#                                 Command Line
-#
-# =============================================================================
-#
-
-
-def parse_command_line():
-	parser = OptionParser(
-		version = "Name: %%prog\n%s" % "" # FIXME
-	)
-	parser.add_option("-i", "--input", metavar = "filename", help = "Set name of input file (default = stdin).")
-	parser.add_option("-o", "--output", metavar = "filename", help = "Set name of output file (default = stdout).  May be same as input file (input will be descructively overwritten).")
-	parser.add_option("-v", "--verbose", action = "store_true", help = "Be verbose.")
-	options, urls = parser.parse_args()
-
-	paramdict = options.__dict__.copy()
-
-	required_options = []
-	missing_options = [opt for opt in required_options if getattr(options, opt) is None]
-	if missing_options:
-		raise ValueError("missing required option(s) %s" % ", ".join("--%s" % opt for opt in missing_options))
-
-	if urls:
-		raise ValueError("unrecognized options \"%s\"" % " ".join(urls))
-
-	return options, paramdict, urls
-
-
-#
-# =============================================================================
-#
-#                                     Main
-#
-# =============================================================================
-#
-
-
-#
-# command line
-#
-
-
-options, paramdict, urls = parse_command_line()
-
-
-#
-# load parameter distribution data
-#
-
-
-_, rankingstatpdf = far.parse_likelihood_control_doc(ligolw_utils.load_filename(options.input, contenthandler = far.RankingStat.LIGOLWContentHandler, verbose = options.verbose))
-if rankingstatpdf is None:
-	raise ValueError("'%s' does not contain a RankingStatPDF object" % options.input)
-if options.verbose:
-	print >>sys.stderr, "total livetime:  %s s" % str(abs(rankingstatpdf.segments))
-
-#
-# copy background histograms to zero-lag histograms, and zero the
-# background histograms
-#
-
-
-# FIXME 100.0 is a rough coincidence fraction loss, can probably do better.
-rankingstatpdf.zero_lag_lr_lnpdf.array[:] = rankingstatpdf.noise_lr_lnpdf.array / 100.0
-rankingstatpdf.zero_lag_lr_lnpdf.normalize()
-rankingstatpdf.noise_lr_lnpdf.array[:] = 0.
-rankingstatpdf.noise_lr_lnpdf.normalize()
-
-
-#
-# write result
-#
-
-
-xmldoc = ligolw.Document()
-xmldoc.appendChild(ligolw.LIGO_LW())
-process = ligolw_process.register_to_xmldoc(xmldoc, process_name, paramdict)
-far.gen_likelihood_control_doc(xmldoc, None, rankingstatpdf)
-ligolw_utils.write_filename(xmldoc, options.output, gz = (options.output or "stdout").endswith(".gz"), verbose = options.verbose)
diff --git a/gstlal-inspiral/bin/gstlal_inspiral_marginalize_likelihoods_online b/gstlal-inspiral/bin/gstlal_inspiral_marginalize_likelihoods_online
index 40e6fc5061394abde41269d15c425f0fc76201ee..8e43fd2861259a28997aa053cc9a6dbfad5f32c6 100755
--- a/gstlal-inspiral/bin/gstlal_inspiral_marginalize_likelihoods_online
+++ b/gstlal-inspiral/bin/gstlal_inspiral_marginalize_likelihoods_online
@@ -63,8 +63,8 @@ shift
 # paths to data objects on each job's web management interface
 #
 
-LIKELIHOOD_PATH="likelihood.xml"
-ZEROLAG_COUNTS_PATH="zero_lag_ranking_stats.xml"
+LIKELIHOOD_PATH="rankingstat.xml"
+ZEROLAG_COUNTS_PATH="zerolag_rankingstatpdf.xml"
 
 #
 # pause for each iteration (seconds)
@@ -98,7 +98,7 @@ while true ; do
 	# sum the noise and signal model ranking statistic histograms
 	# across jobs
 	date +"%H:%M:%S" >&2
-	gstlal_inspiral_marginalize_likelihood --verbose --ranking-stat-pdf --output ${BACKGROUND_COUNTS} ${RANKING_PDF_FILES} || break
+	gstlal_inspiral_marginalize_likelihood --verbose --marginalize ranking-stat-pdf --output ${BACKGROUND_COUNTS} ${RANKING_PDF_FILES} || break
 	rm -vf ${RANKING_PDF_FILES}
 
 	# collect and sum the current observed zero-lag ranking statistic
@@ -114,7 +114,7 @@ while true ; do
 		ZEROLAG_COUNTS_URLS="${ZEROLAG_COUNTS_URLS} ${SERVER}${ZEROLAG_COUNTS_PATH}"
 	done || break
 	date +"%H:%M:%S" >&2
-	gstlal_inspiral_marginalize_likelihood --verbose --ranking-stat-pdf --output ${OUTPUT}.next.gz ${BACKGROUND_COUNTS} ${ZEROLAG_COUNTS_URLS} || break
+	gstlal_inspiral_marginalize_likelihood --verbose --marginzlize ranking-stat-pdf --output ${OUTPUT}.next.gz ${BACKGROUND_COUNTS} ${ZEROLAG_COUNTS_URLS} || break
 	mv -f ${OUTPUT}.next.gz ${OUTPUT} || break
 done
 rm -vf ${BACKGROUND_COUNTS}
diff --git a/gstlal-inspiral/bin/gstlal_ll_inspiral_pipe b/gstlal-inspiral/bin/gstlal_ll_inspiral_pipe
index cf4e8717a856f41474ee89df72352c61f6566e62..de3d291e50a1c935c041bcf5089836428c3b0587 100755
--- a/gstlal-inspiral/bin/gstlal_ll_inspiral_pipe
+++ b/gstlal-inspiral/bin/gstlal_ll_inspiral_pipe
@@ -248,6 +248,8 @@ def parse_command_line():
 
 	if options.injection_file:
 		inj_name_dict = datasource.injection_dict_from_channel_list_with_node_range(options.injection_file)
+	else:
+		inj_name_dict = {}
 
 	#FIXME add consistency check?
 	bankcache = inspiral_pipe.parse_cache_str(options.bank_cache)
@@ -421,13 +423,17 @@ for num_insp_nodes, (svd_banks, likefile, zerolikefile) in enumerate(zip(bank_gr
 			"min-instruments":options.min_instruments,
 			"min-log-L":options.min_log_L,
 			"time-slide-file":options.time_slide_file
-			},
-		input_files = {"ranking-stat-pdf":options.marginalized_likelihood_file},
-		output_files = {"output":"not_used.xml.gz",
-				"ranking-stat-output":likefile,
-				"zerolag-rankingstatpdf-filename":zerolikefile,
-			}
-		)
+		},
+		input_files = {
+			"ranking-stat-input":[likefile],
+			"ranking-stat-pdf":options.marginalized_likelihood_file
+		},
+		output_files = {
+			"output":"/dev/null",
+			"ranking-stat-output":likefile,
+			"zerolag-rankingstat-pdf":zerolikefile
+		}
+	)
 
 	if str("%04d" %num_insp_nodes) in inj_channel_dict:
 		# FIXME The node number for injection jobs currently follows the same
@@ -467,12 +473,15 @@ for num_insp_nodes, (svd_banks, likefile, zerolikefile) in enumerate(zip(bank_gr
 				"min-instruments":options.min_instruments,
 				"min-log-L":options.min_log_L,
 				"time-slide-file":options.time_slide_file
-				},
-			input_files = {"ranking-stat-pdf":options.marginalized_likelihood_file,
-			"ranking-stat-input":[likefile]},
-			output_files = {"output":"not_used.xml.gz",
-				}
-			)
+			},
+			input_files = {
+				"ranking-stat-input":[likefile],
+				"ranking-stat-pdf":options.marginalized_likelihood_file
+			},
+			output_files = {
+				"output":"/dev/null"
+			}
+		)
 
 def groups(l, n):
 	for i in xrange(0, len(l), n):
@@ -538,13 +547,15 @@ dag.write_cache()
 #
 
 
-shutil.copy2(dagparts.which('gstlalcbcsummary'), os.path.expanduser("~/public_html/cgi-bin"))
-shutil.copy2(dagparts.which('gstlalcbcnode'), os.path.expanduser("~/public_html/cgi-bin"))
+cgibinpath = os.path.expanduser("~/public_html/cgi-bin")
+if not os.path.isdir(cgibinpath):
+	os.makedirs(cgibinpath)
+shutil.copy2(dagparts.which('gstlalcbcsummary'), cgibinpath)
+shutil.copy2(dagparts.which('gstlalcbcnode'), cgibinpath)
 query = "id=%s,%s&dir=%s&ifos=%s" % (jobTags[0], jobTags[-1], os.getcwd(), ",".join(sorted(bank_cache.keys())))
 # Write the analysis to a special file that the summary page can find by default
-webfile = open(os.path.join(os.path.expanduser("~/public_html/cgi-bin"), "gstlalcbc_analysis.txt"), "w")
-webfile.write(query)
-webfile.close()
+with open(os.path.join(cgibinpath, "gstlalcbc_analysis.txt"), "w") as webfile:
+	webfile.write(query)
 print >>sys.stderr, "\n\n NOTE! You can monitor the analysis at this url: %s/~%s/cgi-bin/gstlalcbcsummary?%s \n\n" % (inspiral_pipe.webserver_url(), os.environ['USER'], query)
 if inj_jobTags:
 	print >>sys.stderr, "\n\n NOTE! You can monitor the injection analysis at this url: %s/~%s/cgi-bin/gstlalcbcsummary?id=%s,%s&dir=%s&ifos=%s \n\n" % (inspiral_pipe.webserver_url(), os.environ['USER'], inj_jobTags[0], inj_jobTags[-1], os.getcwd(), ",".join(sorted(bank_cache.keys())))
diff --git a/gstlal-inspiral/python/far.py b/gstlal-inspiral/python/far.py
index ec307243ed1d0b0075b61620f4be3cac636387d7..ad739a7bb67690c3488a65ee6a125e40eec07423 100644
--- a/gstlal-inspiral/python/far.py
+++ b/gstlal-inspiral/python/far.py
@@ -215,6 +215,7 @@ class RankingStat(snglcoinc.LnLikelihoodRatioMixin):
 		self.numerator.finish()
 		self.denominator.finish()
 		self.zerolag.finish()
+		return self
 
 	@classmethod
 	def get_xml_root(cls, xml, name):
@@ -270,6 +271,35 @@ class DatalessRankingStat(RankingStat):
 		# no zero-lag
 		self.numerator.finish()
 		self.denominator.finish()
+		return self
+
+
+class OnlineFrakensteinRankingStat(RankingStat):
+	"""
+	Version of RankingStat with horizon distance history and trigger
+	rate history spliced in from another instance.  Used to solve a
+	chicken-or-egg problem and assign ranking statistic values in an
+	aonline anlysis.  NOTE:  the donor data is not copied, instances of
+	this class hold references to the donor's data, so as it is
+	modified those modifications are immediately reflected here.
+
+	For safety's sake, instances cannot be written to or read from
+	files, cannot be marginalized together with other instances, nor
+	accept updates from new data.
+	"""
+	# NOTE:  .__iadd__(), .copy() and I/O are forbidden, but these
+	# operations will be blocked by the .numerator and .denominator
+	# instances, no need to add extra code here to prevent these
+	# operations
+	def __init__(self, src, donor):
+		self.numerator = inspiral_lr.OnlineFrakensteinLnSignalDensity.splice(src.numerator, donor.numerator)
+		self.denominator = inspiral_lr.OnlineFrakensteinLnNoiseDensity.splice(src.denominator, donor.denominator)
+
+	def finish(self):
+		# no zero-lag
+		self.numerator.finish()
+		self.denominator.finish()
+		return self
 
 
 #
@@ -361,6 +391,13 @@ class RankingStatPDF(object):
 			raise ValueError("cannot be initialized from a RankingStat that is not for a specific set of templates")
 		self.template_ids = rankingstat.template_ids
 
+		#
+		# bailout used by codes that want all-zeros histograms
+		#
+
+		if not nsamples:
+			return
+
 		#
 		# run importance-weighted random sampling to populate
 		# binnings.
diff --git a/gstlal-inspiral/python/inspiral.py b/gstlal-inspiral/python/inspiral.py
index e293ae27e7846c9f63e69f6103e8474837f89000..811d2e5bf96eeb4814abc1104d20f640a731bff9 100644
--- a/gstlal-inspiral/python/inspiral.py
+++ b/gstlal-inspiral/python/inspiral.py
@@ -470,7 +470,7 @@ class CoincsDocument(object):
 
 
 class Data(object):
-	def __init__(self, coincs_document, pipeline, rankingstat, zerolag_rankingstatpdf_filename = None, rankingstatpdf_url = None, ranking_stat_output_url = None, ranking_stat_input_url = None, likelihood_snapshot_interval = None, thinca_interval = 50.0, min_log_L = None, sngls_snr_threshold = None, gracedb_far_threshold = None, gracedb_min_instruments = None, gracedb_group = "Test", gracedb_search = "LowMass", gracedb_pipeline = "gstlal", gracedb_service_url = "https://gracedb.ligo.org/api/", upload_auxiliary_data_to_gracedb = True, verbose = False):
+	def __init__(self, coincs_document, pipeline, rankingstat, zerolag_rankingstatpdf_url = None, rankingstatpdf_url = None, ranking_stat_output_url = None, ranking_stat_input_url = None, likelihood_snapshot_interval = None, thinca_interval = 50.0, min_log_L = None, sngls_snr_threshold = None, gracedb_far_threshold = None, gracedb_min_instruments = None, gracedb_group = "Test", gracedb_search = "LowMass", gracedb_pipeline = "gstlal", gracedb_service_url = "https://gracedb.ligo.org/api/", upload_auxiliary_data_to_gracedb = True, verbose = False):
 		#
 		# initialize
 		#
@@ -509,8 +509,8 @@ class Data(object):
 		bottle.route("/likelihood_history.txt")(self.web_get_likelihood_history)
 		bottle.route("/far_history.txt")(self.web_get_far_history)
 		bottle.route("/ram_history.txt")(self.web_get_ram_history)
-		bottle.route("/likelihood.xml")(self.web_get_likelihood_file)
-		bottle.route("/zerolag_rankingstatpdf.xml")(self.web_get_zero_lag_ranking_stats_file)
+		bottle.route("/rankingstat.xml")(self.web_get_rankingstat)
+		bottle.route("/zerolag_rankingstatpdf.xml")(self.web_get_zerolag_rankingstatpdf)
 		bottle.route("/gracedb_far_threshold.txt", method = "GET")(self.web_get_gracedb_far_threshold)
 		bottle.route("/gracedb_far_threshold.txt", method = "POST")(self.web_set_gracedb_far_threshold)
 		bottle.route("/gracedb_min_instruments.txt", method = "GET")(self.web_get_gracedb_min_instruments)
@@ -551,20 +551,39 @@ class Data(object):
 		# statistics they've collected internally.
 		# ranking_stat_input_url is not used when running offline.
 		#
-		# ranking_stat_output_url provides the name of the file to which the
-		# internally-collected ranking statistic information is to
-		# be written whenever output is written to disk.  if set to
-		# None, then only the trigger file will be written, no
-		# ranking statistic information will be written.  normally
-		# it is set to a non-null value, but injection jobs might
-		# be configured to disable ranking statistic output since
-		# they produce nonsense.
+		# ranking_stat_output_url provides the name of the file to
+		# which the internally-collected ranking statistic
+		# information is to be written whenever output is written
+		# to disk.  if set to None, then only the trigger file will
+		# be written, no ranking statistic information will be
+		# written.  normally it is set to a non-null value, but
+		# injection jobs might be configured to disable ranking
+		# statistic output since they produce nonsense.
 		#
 
-		self.ranking_stat_output_url = ranking_stat_output_url
 		self.ranking_stat_input_url = ranking_stat_input_url
+		self.ranking_stat_output_url = ranking_stat_output_url
 		self.rankingstat = rankingstat
 
+		#
+		# if we have been supplied with external ranking statistic
+		# information then use it to enable ranking statistic
+		# assignment in streamthinca.  otherwise, if we have not
+		# been and yet we have been asked to apply the min log L
+		# cut anyway then enable ranking statistic assignment using
+		# the dataless ranking statistic variant
+		#
+
+		if self.ranking_stat_input_url is not None:
+			self.stream_thinca.rankingstat = far.OnlineFrakensteinRankingStat(self.rankingstat, self.rankingstat).finish()
+		elif min_log_L is not None:
+			self.stream_thinca.rankingstat = far.DatalessRankingStat(
+				template_ids = rankingstat.template_ids,
+				instruments = rankingstat.instruments,
+				min_instruments = rankingstat.min_instruments,
+				delta_t = rankingstat.delta_t
+			).finish()
+
 		#
 		# zero_lag_ranking_stats is a RankingStatPDF object that is
 		# used to accumulate a histogram of the likelihood ratio
@@ -572,14 +591,22 @@ class Data(object):
 		# to implement the extinction model for low-significance
 		# events during online running but otherwise is optional.
 		#
+		# FIXME:  if the file does not exist or is not readable,
+		# the code silently initializes a new, empty, histogram.
+		# it would be better to determine whether or not the file
+		# is required and fail when it is missing
+		#
 
-		if zerolag_rankingstatpdf_filename is None:
-			self.zerolag_rankingstatpdf = None
-		else:
-			_, self.zerolag_rankingstatpdf = far.parse_likelihood_control_doc(ligolw_utils.load_filename(zerolag_rankingstatpdf_filename, verbose = verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
+		if zerolag_rankingstatpdf_url is not None and os.access(ligolw_utils.local_path_from_url(zerolag_rankingstatpdf_url), os.R_OK):
+			_, self.zerolag_rankingstatpdf = far.parse_likelihood_control_doc(ligolw_utils.load_url(zerolag_rankingstatpdf_url, verbose = verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
 			if self.zerolag_rankingstatpdf is None:
-				raise ValueError("\"%s\" does not contain ranking statistic PDF data" % zerolag_rankingstatpdf_filename)
-		self.zerolag_rankingstatpdf_filename = zerolag_rankingstatpdf_filename
+				raise ValueError("\"%s\" does not contain ranking statistic PDF data" % zerolag_rankingstatpdf_url)
+		elif zerolag_rankingstatpdf_url is not None:
+			# initialize an all-zeros set of PDFs
+			self.zerolag_rankingstatpdf = far.RankingStatPDF(rankingstat, nsamples = 0)
+		else:
+			self.zerolag_rankingstatpdf = None
+		self.zerolag_rankingstatpdf_url = zerolag_rankingstatpdf_url
 
 		#
 		# rankingstatpdf contains the RankingStatPDF object (loaded
@@ -593,26 +620,8 @@ class Data(object):
 		# for upload to gracedb, etc.
 		#
 
-		# None to disable
 		self.rankingstatpdf_url = rankingstatpdf_url
-		self.rankingstatpdf = None
-		self.fapfar = None
-
-		#
-		# if we are not in online mode but we need to compute LRs
-		# to apply an LR threshold, then enable LR assignment using
-		# the dataless ranking statistic variant
-		#
-
-		if self.likelihood_snapshot_interval is None and min_log_L is not None:
-			dataless_rankingstat = far.DatalessRankingStat(
-				template_ids = rankingstat.template_ids,
-				instruments = rankingstat.instruments,
-				min_instruments = rankingstat.min_instruments,
-				delta_t = rankingstat.delta_t
-			)
-			dataless_rankingstat.finish()
-			self.stream_thinca.rankingstat = dataless_rankingstat
+		self.load_rankingstat_pdf()
 
 		#
 		# Fun output stuff
@@ -626,6 +635,22 @@ class Data(object):
 		self.ram_history = deque(maxlen = 2)
 		self.ifo_snr_history = dict((instrument, deque(maxlen = 10000)) for instrument in rankingstat.instruments)
 
+	def load_rankingstat_pdf(self):
+		# FIXME:  if the file can't be accessed the code silently
+		# disables FAP/FAR assignment.  need to figure out when
+		# failure is OK and when it's not OK and put a better check
+		# here.
+		if self.rankingstatpdf_url is not None and os.access(ligolw_utils.local_path_from_url(self.rankingstatpdf_url), os.R_OK):
+			_, self.rankingstatpdf = far.parse_likelihood_control_doc(ligolw_utils.load_url(self.rankingstatpdf_url, verbose = self.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
+			if self.rankingstatpdf is None:
+				raise ValueError("\"%s\" does not contain ranking statistic PDFs" % url)
+			if not self.rankingstat.template_ids <= self.rankingstatpdf.template_ids:
+				raise ValueError("\"%s\" is for the wrong templates")
+			self.fapfar = far.FAPFAR(self.rankingstatpdf.new_with_extinction())
+		else:
+			self.rankingstatpdf = None
+			self.fapfar = None
+
 	def appsink_new_buffer(self, elem):
 		with self.lock:
 			# retrieve triggers from appsink element
@@ -697,21 +722,6 @@ class Data(object):
 			if self.likelihood_snapshot_interval is not None and (self.likelihood_snapshot_timestamp is None or buf_timestamp - self.likelihood_snapshot_timestamp >= self.likelihood_snapshot_interval):
 				self.likelihood_snapshot_timestamp = buf_timestamp
 
-				# if a ranking statistic source url is set,
-				# overwrite rankingstat with its contents.
-				# FIXME There is currently no guarantee
-				# that the reference_likelihood_file on
-				# disk will have updated since the last
-				# snapshot, but for our purpose it should
-				# not have that large of an effect. The
-				# data loaded should never be older than
-				# the snapshot before last
-				if self.ranking_stat_input_url is not None:
-					params_before = self.rankingstat.template_ids, self.rankingstat.instruments, self.rankingstat.min_instruments, self.rankingstat.delta_t
-					self.rankingstat, _ = far.parse_likelihood_control_doc(ligolw_utils.load_url(self.ranking_stat_input_url, verbose = self.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
-					if params_before != (self.rankingstat.template_ids, self.rankingstat.instruments, self.rankingstat.min_instruments, self.rankingstat.delta_t):
-						raise ValueError("'%s' contains incompatible ranking statistic configuration" % self.ranking_stat_input_url)
-
 				# post a checkpoint message.
 				# FIXME:  make sure this triggers
 				# self.snapshot_output_url() to be invoked.
@@ -729,39 +739,27 @@ class Data(object):
 				# overwritten.
 				self.pipeline.get_bus().post(message_new_checkpoint(self.pipeline, timestamp = buf_timestamp.ns()))
 
-				if self.rankingstatpdf_url is not None:
-					# enable streamthinca's likelihood
-					# ratio assignment using our own,
-					# local, parameter distribution
-					# data
-				# FIXME:  this won't work, because this
-				# object only has horizon distance and
-				# trigger rate information up to the
-				# current point in time.  it cannot be used
-				# to rank candidates collected after this
-				# time, which is exactly what we intend to
-				# do with it.  to fix this, I anticipate
-				# writing an "online" variant of the
-				# ranking statistic class that
-				# frankensteins together the snr and \chi^2
-				# densities from one rankingstat instance
-				# with the horizons and trigger rate
-				# densities from another
-					rankingstat = self.rankingstat.copy()
-					rankingstat.finish()
-					self.stream_thinca.rankingstat = rankingstat
-
-					# read the marginalized likelihood
-					# ratio distributions that have
-					# been updated asynchronously and
-					# initialize a FAP/FAR assignment
-					# machine from it.
-					_, self.rankingstatpdf = far.parse_likelihood_control_doc(ligolw_utils.load_url(self.rankingstatpdf_url, verbose = self.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
-					if self.rankingstatpdf is None:
-						raise ValueError("\"%s\" does not contain ranking statistic PDFs" % self.rankingstatpdf_url)
-					if not self.rankingstat.template_ids <= self.rankingstatpdf.template_ids:
-						raise ValueError("\"%s\" is for the wrong templates")
-					self.fapfar = far.FAPFAR(self.rankingstatpdf.new_with_extinction())
+				# if a ranking statistic source url is set
+				# and is not the same as the file to which
+				# we are writing our ranking statistic data
+				# then overwrite rankingstat with its
+				# contents.  the use case is online
+				# injection jobs that need to periodically
+				# grab new ranking statistic data from
+				# their corresponding non-injection partner
+				if self.ranking_stat_input_url is not None and self.ranking_stat_input_url != self.ranking_stat_output_url:
+					params_before = self.rankingstat.template_ids, self.rankingstat.instruments, self.rankingstat.min_instruments, self.rankingstat.delta_t
+					self.rankingstat, _ = far.parse_likelihood_control_doc(ligolw_utils.load_url(self.ranking_stat_input_url, verbose = self.verbose, contenthandler = far.RankingStat.LIGOLWContentHandler))
+					if params_before != (self.rankingstat.template_ids, self.rankingstat.instruments, self.rankingstat.min_instruments, self.rankingstat.delta_t):
+						raise ValueError("'%s' contains incompatible ranking statistic configuration" % self.ranking_stat_input_url)
+
+				# update streamthinca's ranking statistic
+				# data
+				self.stream_thinca.rankingstat = far.OnlineFrakensteinRankingStat(self.rankingstat, self.rankingstat).finish()
+
+				# optionally get updated ranking statistic
+				# PDF data and enable FAP/FAR assignment
+				self.load_rankingstat_pdf()
 
 			# add triggers to trigger rate record.  this needs
 			# to be done without any cuts on coincidence, etc.,
@@ -874,6 +872,9 @@ class Data(object):
 			# we encounter the first trigger whose SNR series
 			# might still be needed, save its index, and start
 			# the search from there next time
+			# FIXME:  could also trim segment and V data from
+			# ranking stat object if ranking_stat_output_url is
+			# not set because the info won't be used
 			discard_boundary = self.stream_thinca.discard_boundary
 			for self.snr_time_series_cleanup_index, event in enumerate(self.coincs_document.sngl_inspiral_table[self.snr_time_series_cleanup_index:], self.snr_time_series_cleanup_index):
 				if event.end >= discard_boundary:
@@ -885,7 +886,7 @@ class Data(object):
 		start, end = int(math.floor(start)), int(math.ceil(end))
 		return "%s-%s-%d-%d.%s" % ("".join(sorted(self.process.instruments)), description, start, end - start, extension)
 
-	def __get_likelihood_xmldoc(self):
+	def __get_rankingstat_xmldoc(self):
 		# generate a coinc parameter distribution document.  NOTE:
 		# likelihood ratio PDFs *are* included if they were present in
 		# the --likelihood-file that was loaded.
@@ -896,15 +897,15 @@ class Data(object):
 		ligolw_process.set_process_end_time(process)
 		return xmldoc
 
-	def web_get_likelihood_file(self):
+	def web_get_rankingstat(self):
 		with self.lock:
 			output = StringIO.StringIO()
-			ligolw_utils.write_fileobj(self.__get_likelihood_xmldoc(), output)
+			ligolw_utils.write_fileobj(self.__get_rankingstat_xmldoc(), output)
 			outstr = output.getvalue()
 			output.close()
 			return outstr
 
-	def __get_zero_lag_ranking_stats_xmldoc(self):
+	def __get_zerolag_rankingstatpdf_xmldoc(self):
 		xmldoc = ligolw.Document()
 		xmldoc.appendChild(ligolw.LIGO_LW())
 		process = ligolw_process.register_to_xmldoc(xmldoc, u"gstlal_inspiral", paramdict = {}, ifos = self.rankingstat.instruments)
@@ -912,10 +913,10 @@ class Data(object):
 		ligolw_process.set_process_end_time(process)
 		return xmldoc
 
-	def web_get_zero_lag_ranking_stats_file(self):
+	def web_get_zerolag_rankingstatpdf(self):
 		with self.lock:
 			output = StringIO.StringIO()
-			ligolw_utils.write_fileobj(self.__get_zero_lag_ranking_stats_xmldoc(), output)
+			ligolw_utils.write_fileobj(self.__get_zerolag_rankingstatpdf_xmldoc(), output)
 			outstr = output.getvalue()
 			output.close()
 			return outstr
@@ -998,6 +999,10 @@ class Data(object):
 			self.__flush()
 
 	def __do_gracedb_alerts(self, retries = 5, retry_delay = 5.):
+		# sanity check
+		if self.farpfar is None:
+			raise ValueError("gracedb alerts cannot be enabled without fap/far data")
+
 		# no-op short circuit
 		if not self.stream_thinca.last_coincs:
 			return
@@ -1120,7 +1125,7 @@ class Data(object):
 			if self.verbose:
 				print >>sys.stderr, "generating ranking_data.xml.gz ..."
 			fobj = StringIO.StringIO()
-			ligolw_utils.write_fileobj(self.__get_likelihood_xmldoc(), fobj, gz = True)
+			ligolw_utils.write_fileobj(self.__get_rankingstat_xmldoc(), fobj, gz = True)
 			message, filename, tag, contents = ("ranking statistic PDFs", "ranking_data.xml.gz", "ranking statistic", fobj.getvalue())
 			del fobj
 			self.__upload_gracedb_aux_data(message, filename, tag, contents, gracedb_ids, retries, gracedb_client)
@@ -1294,14 +1299,14 @@ class Data(object):
 
 	def __write_ranking_stat_url(self, url, description, snapshot = False, verbose = False):
 		# write the ranking statistic file.
-		ligolw_utils.write_url(self.__get_likelihood_xmldoc(), ligolw_utils.local_path_from_url(url), gz = (url or "stdout").endswith(".gz"), verbose = verbose, trap_signals = None)
+		ligolw_utils.write_url(self.__get_rankingstat_xmldoc(), ligolw_utils.local_path_from_url(url), gz = (url or "stdout").endswith(".gz"), verbose = verbose, trap_signals = None)
 		# Snapshots get their own custom file and path
 		if snapshot:
 			fname = self.T050017_filename(description + '_DISTSTATS', 'xml.gz')
 			shutil.copy(ligolw_utils.local_path_from_url(url), os.path.join(subdir_from_T050017_filename(fname), fname))
 
-	def __write_zero_lag_ranking_stats_file(self, filename, verbose = False):
-		ligolw_utils.write_filename(self.__get_zero_lag_ranking_stats_xmldoc(), filename, gz = (filename or "stdout").endswith(".gz"), verbose = verbose, trap_signals = None)
+	def __write_zero_lag_ranking_stat_url(self, url, verbose = False):
+		ligolw_utils.write_url(self.__get_zerolag_rankingstatpdf_xmldoc(), url, gz = (url or "stdout").endswith(".gz"), verbose = verbose, trap_signals = None)
 
 	def write_output_url(self, url = None, description = "", verbose = False):
 		with self.lock:
@@ -1321,6 +1326,6 @@ class Data(object):
 			if self.ranking_stat_output_url is not None:
 				self.__write_ranking_stat_url(self.ranking_stat_output_url, description, snapshot = True, verbose = verbose)
 			if self.zerolag_rankingstatpdf is not None:
-				self.__write_zero_lag_ranking_stats_file(self.zerolag_rankingstatpdf_filename, verbose = verbose)
+				self.__write_zero_lag_ranking_stat_url(self.zerolag_rankingstatpdf_url, verbose = verbose)
 			self.coincs_document = coincs_document
 			self.snr_time_series_cleanup_index = 0
diff --git a/gstlal-inspiral/python/inspiral_pipe.py b/gstlal-inspiral/python/inspiral_pipe.py
index d9e2f720552bcb81065e57ea33ab6fc21fd4d43b..6532068b335c31b17de7ea9e66db82d586b90547 100644
--- a/gstlal-inspiral/python/inspiral_pipe.py
+++ b/gstlal-inspiral/python/inspiral_pipe.py
@@ -116,7 +116,7 @@ def webserver_url():
 	#FIXME add more hosts as you need them
 	if "cit" in host or "ligo.caltech.edu" in host:
 		return "https://ldas-jobs.ligo.caltech.edu"
-	if "phys.uwm.edu" in host or "cgca.uwm.edu" in host:
+	if ".phys.uwm.edu" in host or ".cgca.uwm.edu" in host or ".nemo.uwm.edu" in host:
 		return "https://ldas-jobs.cgca.uwm.edu"
 
 	raise NotImplementedError("I don't know where the webserver is for this environment")
diff --git a/gstlal-inspiral/python/stats/inspiral_lr.py b/gstlal-inspiral/python/stats/inspiral_lr.py
index 7049ef230fbb16cc752932209d8f0a2ee679f77d..c4e43e7c7750c4dd795f40882972fe19c60cd538 100644
--- a/gstlal-inspiral/python/stats/inspiral_lr.py
+++ b/gstlal-inspiral/python/stats/inspiral_lr.py
@@ -532,6 +532,52 @@ class DatalessLnSignalDensity(LnSignalDensity):
 		raise NotImplementedError
 
 
+class OnlineFrakensteinLnSignalDensity(LnSignalDensity):
+	"""
+	Version of LnSignalDensity with horizon distance history spliced in
+	from another instance.  Used to solve a chicken-or-egg problem and
+	assign ranking statistic values in an aonline anlysis.  NOTE:  the
+	horizon history is not copied from the donor, instances of this
+	class hold a reference to the donor's data, so as it is modified
+	those modifications are immediately reflected here.
+
+	For safety's sake, instances cannot be written to or read from
+	files, cannot be marginalized together with other instances, nor
+	accept updates from new data.
+	"""
+	@classmethod
+	def splice(cls, src, Dh_donor):
+		self = cls(src.template_ids, src.instruments, src.delta_t, src.min_instruments)
+		for key, lnpdf in self.densities.items():
+			self.densities[key] = lnpdf.copy()
+		# NOTE:  not a copy.  we hold a reference to the donor's
+		# data so that as it is updated, we get the updates.
+		self.horizon_history = Dh_donor.horizon_history
+		return self
+
+	def __iadd__(self, other):
+		raise NotImplementedError
+
+	def increment(self, *args, **kwargs):
+		raise NotImplementedError
+
+	def copy(self):
+		raise NotImplementedError
+
+	def to_xml(self, name):
+		# I/O not permitted:  the on-disk version would be
+		# indistinguishable from a real ranking statistic and could
+		# lead to accidents
+		raise NotImplementedError
+
+	@classmethod
+	def from_xml(cls, xml, name):
+		# I/O not permitted:  the on-disk version would be
+		# indistinguishable from a real ranking statistic and could
+		# lead to accidents
+		raise NotImplementedError
+
+
 #
 # =============================================================================
 #
@@ -865,3 +911,49 @@ class DatalessLnNoiseDensity(LnNoiseDensity):
 		# indistinguishable from a real ranking statistic and could
 		# lead to accidents
 		raise NotImplementedError
+
+
+class OnlineFrakensteinLnNoiseDensity(LnNoiseDensity):
+	"""
+	Version of LnNoiseDensity with trigger rate data spliced in from
+	another instance.  Used to solve a chicken-or-egg problem and
+	assign ranking statistic values in an aonline anlysis.  NOTE:  the
+	trigger rate data is not copied from the donor, instances of this
+	class hold a reference to the donor's data, so as it is modified
+	those modifications are immediately reflected here.
+
+	For safety's sake, instances cannot be written to or read from
+	files, cannot be marginalized together with other instances, nor
+	accept updates from new data.
+	"""
+	@classmethod
+	def splice(cls, src, rates_donor):
+		self = cls(src.template_ids, src.instruments, src.delta_t, src.min_instruments)
+		for key, lnpdf in self.densities.items():
+			self.densities[key] = lnpdf.copy()
+		# NOTE:  not a copy.  we hold a reference to the donor's
+		# data so that as it is updated, we get the updates.
+		self.triggerrates = rates_donor.triggerrates
+		return self
+
+	def __iadd__(self, other):
+		raise NotImplementedError
+
+	def increment(self, *args, **kwargs):
+		raise NotImplementedError
+
+	def copy(self):
+		raise NotImplementedError
+
+	def to_xml(self, name):
+		# I/O not permitted:  the on-disk version would be
+		# indistinguishable from a real ranking statistic and could
+		# lead to accidents
+		raise NotImplementedError
+
+	@classmethod
+	def from_xml(cls, xml, name):
+		# I/O not permitted:  the on-disk version would be
+		# indistinguishable from a real ranking statistic and could
+		# lead to accidents
+		raise NotImplementedError