From 4678544d7df8ea1c61b44093422eb62d190336b3 Mon Sep 17 00:00:00 2001
From: Kipp Cannon <kipp.cannon@ligo.org>
Date: Tue, 17 Apr 2018 12:46:00 -0500
Subject: [PATCH] gstlal-inspiral: partially restore the online variant

---
 gstlal-inspiral/bin/Makefile.am               |   1 -
 gstlal-inspiral/bin/gstlal_inspiral           |  59 +++---
 .../bin/gstlal_inspiral_fake_zerolag_counts   | 128 ------------
 ...al_inspiral_marginalize_likelihoods_online |   8 +-
 gstlal-inspiral/bin/gstlal_ll_inspiral_pipe   |  47 +++--
 gstlal-inspiral/python/far.py                 |  37 ++++
 gstlal-inspiral/python/inspiral.py            | 197 +++++++++---------
 gstlal-inspiral/python/inspiral_pipe.py       |   2 +-
 gstlal-inspiral/python/stats/inspiral_lr.py   |  92 ++++++++
 9 files changed, 296 insertions(+), 275 deletions(-)
 delete mode 100755 gstlal-inspiral/bin/gstlal_inspiral_fake_zerolag_counts

diff --git a/gstlal-inspiral/bin/Makefile.am b/gstlal-inspiral/bin/Makefile.am
index d53b492ef0..96670fafb3 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 78581937e9..1dc707fe69 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 3d6e976beb..0000000000
--- 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 40e6fc5061..8e43fd2861 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 cf4e8717a8..de3d291e50 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 ec307243ed..ad739a7bb6 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 e293ae27e7..811d2e5bf9 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 d9e2f72055..6532068b33 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 7049ef230f..c4e43e7c77 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
-- 
GitLab