From 82b31b007a26dae2e755568eb3b6388164a9d11f Mon Sep 17 00:00:00 2001
From: ChiWai Chan <chiwai.chan@ligo.org>
Date: Wed, 29 Sep 2021 00:55:24 -0700
Subject: [PATCH] gstlal_inspiral/plots: add horizon.py module for plotting
 horizon distance.

---
 gstlal-inspiral/python/plots/Makefile.am |   1 +
 gstlal-inspiral/python/plots/horizon.py  | 183 +++++++++++++++++++++++
 2 files changed, 184 insertions(+)
 create mode 100644 gstlal-inspiral/python/plots/horizon.py

diff --git a/gstlal-inspiral/python/plots/Makefile.am b/gstlal-inspiral/python/plots/Makefile.am
index 1d12e3d65b..4fdc0fdcae 100644
--- a/gstlal-inspiral/python/plots/Makefile.am
+++ b/gstlal-inspiral/python/plots/Makefile.am
@@ -4,6 +4,7 @@ plotsdir = $(pkgpythondir)/plots
 plots_PYTHON = \
 	bank.py \
 	far.py \
+	horizon.py \
 	lloid.py \
 	segments.py \
 	sensitivity.py \
diff --git a/gstlal-inspiral/python/plots/horizon.py b/gstlal-inspiral/python/plots/horizon.py
new file mode 100644
index 0000000000..85257bb4bd
--- /dev/null
+++ b/gstlal-inspiral/python/plots/horizon.py
@@ -0,0 +1,183 @@
+# Copyright (C) 2021  ChiWai Chan
+#
+# 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.
+
+import sys
+import numpy
+import matplotlib
+matplotlib.use("Agg")
+
+from pathlib import Path
+from collections import defaultdict
+from matplotlib import pyplot
+
+from gstlal import far
+from gstlal.plots import util as plotutil
+from gstlal.psd import read_psd, HorizonDistance
+
+
+class UnknownExtensionError(Exception):
+	pass
+
+
+class HorizonDistance:
+	def __init__(self, horizon_history_dict, verbose=False):
+		"""Construct a HorizonDistance instance from a list of ranking
+		statistic files.
+
+		Args:
+			horizon_history_dict:
+				dict, a dictionary containing keyed by ifo,
+				whose value is a dictionary of horizon history
+				(see notes).
+			verbose:
+				bool, default is False, toggle error message.
+
+		Notes:
+			You should use the two class methods
+			(`from_rankingstats` and `from_psds`) instead of
+			directly calling the init method. In case you need to
+			construct an instance from the init method, you prepare
+			a horizon history dict containing horizon history keyed
+			by ifo. A horizon history is a dictionary of GPS time
+			and horizon distance pairs.
+
+		Returns:
+			An instance of HorizionDistance.
+		"""
+		self.verbose = verbose
+		self.horizon_history_dict = horizon_history_dict
+
+	@classmethod
+	def from_rankingstats(cls, sources, verbose=False):
+		"""Construct a HorizonDistance instance from a list of ranking
+		statistic files.
+
+		Args:
+			sources:
+				list of str, the data sources to be loaded.
+			verbose:
+				bool, default is False, toggle error message.
+
+		Returns:
+			An instance of HorizionDistance.
+		"""
+		loader = cls.datasource_loader(sources)
+		urls = loader(sources)
+		rankingstat = far.marginalize_pdf_urls(list(urls), "RankingStat", verbose = verbose)
+		horizon_history_dict = rankingstat.numerator.horizon_history
+		return cls(horizon_history_dict)
+
+	@classmethod
+	def from_psds(cls, sources, verbose=False):
+		"""Construct a HorizonDistance instance from a list of psds.
+
+		Args:
+			sources:
+				list of str, the data sources to be loaded.
+			verbose:
+				bool, default is False, toggle error message.
+
+		Returns:
+			An instance of HorizionDistance.
+		"""
+		loader = cls.datasource_loader(sources)
+		horizon_history_dict = defaultdict(dict)
+		for f in loader(sources):
+			psds = read_psd(f, verbose = verbose)
+			for ifo, psd in psds.items():
+				if psd is not None:
+					horizon_history_dict[ifo].update({int(psd.epoch): HorizonDistance(10., 2048., psd.deltaF, 1.4, 1.4)(psd, 8.)[0]})
+		return cls(horizon_history_dict)
+
+	@staticmethod
+	def datasource_loader(sources):
+		"""Determine how to load the data sources based on their extensions.
+
+		Args:
+			sources:
+				list of str, the data sources to be loaded.
+
+		Raises:
+			UnknownExtensionError:
+				If the sources do not ends with .gz or .cache.
+
+		Returns:
+			None
+		"""
+		assert type(sources) == list, "Please provides <class 'list'> as input data sources."
+		assert len(sources) >= 1, "Please provides at least one data source."
+
+		# default loader
+		extension = Path(sources[0]).suffix
+		if extension == ".gz":
+			return _load_files
+		if extension == ".cache":
+			return _load_caches
+
+		raise UnknownExtensionError("Cannot determine the extension of the input data sources, please prepare files with the following extension: ('.gz' or '.cache').")
+
+	def savefig(self, output, figsize=(12,4)):
+		"""Save the horizon distance plot to disk.
+
+		Args:
+			output:
+				str, the output file name.
+			figsize:
+				tuple of float, default (12,4), set the output
+				figure size in inch.
+
+		Returns:
+			None
+		"""
+		fig, ax = pyplot.subplots(1, 2, figsize = figsize)
+		pyplot.tight_layout(pad = 4, w_pad = 4, h_pad = 4)
+
+		mint = int(min([min(horizon_history.keys()) for _, horizon_history in self.horizon_history_dict.items()]))
+		for ifo, horizon_history in self.horizon_history_dict.items():
+			GPSTime = numpy.array(horizon_history.keys())
+			horizon_dist = horizon_history.values()
+
+			minh, maxh = (float("inf"), 0)
+			maxh = max(maxh, max(horizon_dist))
+			minh = min(minh, min(horizon_dist))
+			SinceGPSTime = (GPSTime - mint)/1000.
+			binvec = numpy.linspace(minh, maxh, 25)
+
+			ax[0].semilogy(SinceGPSTime, horizon_dist, "x", color = plotutil.colour_from_instruments([ifo]), label = ifo)
+			ax[1].hist(horizon_dist, binvec, alpha = 0.5, color = plotutil.colour_from_instruments([ifo]), label = ifo)
+
+		if self.verbose:
+			sys.stderr.write("plotting " + output)
+
+		ax[0].set_xlabel("Time (ks) from GPS {:d}".format(mint))
+		ax[0].set_ylabel("Mpc")
+		ax[0].legend(loc = "best")
+		ax[0].grid()
+
+		ax[1].set_xlabel("Mpc")
+		ax[1].set_ylabel("Count")
+		ax[1].legend(loc = "best")
+		fig.savefig(output)
+		pyplot.close()
+
+
+def _load_files(files):
+	return files
+
+def _load_cachces(caches):
+	urls = (CacheEntry(line).url for cache in caches for line in open(cache))
+	return sorted(urls, key = lambda x: CacheEntry.from_T050017(x).description)
+
-- 
GitLab