diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3df4252ab77e13e8f56bf67bdfdd575083cf6b2..a26015a8a5fbe6919de6d351dc43c7b41d161e86 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ stages: - docker - docker-latest - test-gstlal - - test-gstlal-only-ugly + - test-gstlal-full-build - test-gstlal-ugly - test-burst - test-calibration @@ -332,19 +332,22 @@ test:gstlal:el7: # Run doctests - cd gstlal - - python3 -m pytest -c pytest.ini -m "not requires_gstlal_ugly" + - python3 -m pytest -c pytest.ini -m "not requires_full_build" only: - schedules - pushes allow_failure: true -test:gstlal-only-ugly:el7: +test:gstlal-full-build:el7: interruptible: true image: containers.ligo.org/alexander.pace/gstlal-dev/gstlal-dev:el7 - stage: test-gstlal-only-ugly + stage: test-gstlal-full-build needs: - level0:rpm:gstlal - level1:rpm:gstlal-ugly + - level2:rpm:gstlal-burst + - level2:rpm:gstlal-calibration + - level2:rpm:gstlal-inspiral script: # Install RPMs and set up the test environment: - if [ -d rpmbuild ]; then yum -y install rpmbuild/RPMS/x86_64/*.rpm; fi @@ -353,7 +356,7 @@ test:gstlal-only-ugly:el7: # Run doctests - cd gstlal - - python3 -m pytest -c pytest.ini -m "requires_gstlal_ugly" + - python3 -m pytest -c pytest.ini -m "requires_full_build" only: - schedules - pushes @@ -505,26 +508,34 @@ test:gstlal:conda: before_script: [ ] script: - cd gstlal - - python3 -m pytest -c pytest.ini -m "not requires_gstlal_ugly" + - python3 -m pytest -c pytest.ini -m "not requires_full_build" --junitxml=report.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report.xml -test:gstlal-only-ugly:conda: +test:gstlal-full-build:conda: interruptible: true image: $CI_REGISTRY_IMAGE/conda-dev:$CI_COMMIT_REF_NAME - stage: test-gstlal-only-ugly + stage: test-gstlal-full-build needs: - docker:conda:dev before_script: [ ] script: - cd gstlal - - python3 -m pytest -c pytest.ini -m "requires_gstlal_ugly" + - python3 -m pytest -c pytest.ini -m "requires_full_build" --junitxml=report-full-build.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report-full-build.xml test:gstlal-inspiral:conda: interruptible: true @@ -535,11 +546,15 @@ test:gstlal-inspiral:conda: before_script: [ ] script: - cd gstlal-inspiral - - python3 -m pytest -c pytest.ini + - python3 -m pytest -c pytest.ini --junitxml=report-inspiral.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report-inspiral.xml test:gstlal-ugly:conda: interruptible: true @@ -550,11 +565,15 @@ test:gstlal-ugly:conda: before_script: [ ] script: - cd gstlal-ugly - - python3 -m pytest -c pytest.ini + - python3 -m pytest -c pytest.ini --junitxml=report-ugly.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report-ugly.xml test:gstlal-burst:conda: interruptible: true @@ -565,11 +584,15 @@ test:gstlal-burst:conda: before_script: [ ] script: - cd gstlal-burst - - python3 -m pytest -c pytest.ini + - python3 -m pytest -c pytest.ini --junitxml=report-burst.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report-burst.xml test:gstlal-calibration:conda: interruptible: true @@ -580,11 +603,15 @@ test:gstlal-calibration:conda: before_script: [ ] script: - cd gstlal-calibration - - python3 -m pytest -c pytest.ini + - python3 -m pytest -c pytest.ini --junitxml=report-calibration.xml only: - schedules - pushes allow_failure: true + artifacts: + when: always + reports: + junit: gstlal/report-calibration.xml test:offline:conda: interruptible: true diff --git a/gstlal/pytest.ini b/gstlal/pytest.ini index 8e112eb9830fbce5ab2b275cf7ebe18122c99ecb..116585b1970341cd2f4f9045b48b87d7039a6d17 100644 --- a/gstlal/pytest.ini +++ b/gstlal/pytest.ini @@ -1,6 +1,6 @@ # Configuration file for pytest within gstlal [pytest] -norecursedirs = gst/python port-tools tests share tests_pytest/utils +norecursedirs = gst/python port-tools tests share tests_pytest/utils python/pipeparts testpaths = tests/tests_pytest python addopts = -v diff --git a/gstlal/python/Makefile.am b/gstlal/python/Makefile.am index 5d26f37556fbec64bd6954686ab3031ca811178c..bff3ef85719c1364750092b0d496e3fccf8f7496 100644 --- a/gstlal/python/Makefile.am +++ b/gstlal/python/Makefile.am @@ -17,6 +17,7 @@ pkgpython_PYTHON = \ datafind.py \ datasource.py \ elements.py \ + gsttools.py \ httpinterface.py \ kernels.py \ matplotlibhelper.py \ diff --git a/gstlal/python/gsttools.py b/gstlal/python/gsttools.py new file mode 100644 index 0000000000000000000000000000000000000000..7d936c78d826b82ad0e05888af05f63e26104b8d --- /dev/null +++ b/gstlal/python/gsttools.py @@ -0,0 +1,89 @@ +"""Miscellaneous utilities for working with Gstreamer + +References: + [1] 1.0 API: https://lazka.github.io/pgi-docs/Gst-1.0/index.html +""" +import functools +from typing import Any, Union + +import gi + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) + + +def is_element(x: Any) -> bool: + """Test whether an object is a Gst Element. TODO: do these belong somewhere more general? + + Args: + x: + Any, the object to test + + Returns: + bool, True if x is a Gst Element, false otherwise + """ + return isinstance(x, Gst.Element) + + +def is_pad(x: Any) -> bool: + """Test whether an object is a Gst Pad. TODO: do these belong somewhere more general? + + Args: + x: + Any, the object to test + + Returns: + bool, True if x is a Gst Pad, false otherwise + """ + return isinstance(x, Gst.Pad) + + +def to_caps(x: Union[str, Gst.Caps]) -> Gst.Caps: + """Create a Caps object from a string, or pass thru if already is Caps + + Args: + x: + str or Caps, if a string construct Caps instance from x, else pass thru as Caps + + Returns: + Caps + """ + if isinstance(x, str): + return Gst.Caps.from_string(x) + if not isinstance(x, Gst.Caps): + raise ValueError('Cannot coerce type {} to Caps: {}'.format(type(x), str(x))) + return x + + +def make_element(type_name: str, name: str = None, **properties: dict) -> Gst.Element: + """Create a new element of the type defined by the given element factory. If name is None, then the element will + receive a guaranteed unique name, consisting of the element factory name and a number. If name is given, it will + be given the name supplied. + + Args: + type_name: + str, the name of the type of element + elem_name: + str, default None, the name of the element instance + properties: + dict, keyword arguments to set as properties of the element + + References: + [1] ElementFactor.make: https://lazka.github.io/pgi-docs/Gst-1.0/classes/ElementFactory.html#Gst.ElementFactory.make + + Returns: + Gst.Element + """ + elem = Gst.ElementFactory.make(type_name, name) + if elem is None: + raise RuntimeError("Unknown failure creating {} element: confirm that the correct plugins are being loaded".format(type_name)) + + # Set element properties + for k, v in properties.items(): + elem.set_property(k, v) + + return elem diff --git a/gstlal/python/pipeparts/Makefile.am b/gstlal/python/pipeparts/Makefile.am index 5f447c76cb8abaa518fd0602b4e01f56827932ee..4c4495ff5119ba1b3bb6f2f352cc7aed4d46d0de 100644 --- a/gstlal/python/pipeparts/Makefile.am +++ b/gstlal/python/pipeparts/Makefile.am @@ -2,4 +2,14 @@ pkgpythondir = $(pkgpyexecdir) pipepartsdir = $(pkgpythondir)/pipeparts pipeparts_PYTHON = \ - __init__.py + __init__.py \ + encode.py \ + filters.py \ + mux.py \ + pipedot.py \ + pipetools.py \ + plot.py \ + sink.py \ + source.py \ + transform.py \ + trigger.py diff --git a/gstlal/python/pipeparts/__init__.py b/gstlal/python/pipeparts/__init__.py index 9bea15ca1b8d2ae2ef167b484497bfcd2cc12d09..fa59f519e6d0a0cf61b5fe1eac02fcd94c2aa248 100644 --- a/gstlal/python/pipeparts/__init__.py +++ b/gstlal/python/pipeparts/__init__.py @@ -14,1108 +14,113 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -# -# ============================================================================= -# -# Preamble -# -# ============================================================================= -# - - -import math -import os -import sys -import threading - -import numpy - -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GObject -from gi.repository import Gst -GObject.threads_init() -Gst.init(None) - - -from ligo import segments -from gstlal import pipeio -from lal import iterutils -from lal import LIGOTimeGPS -from lal.utils import CacheEntry - - -if sys.byteorder == "little": - BYTE_ORDER = "LE" -else: - BYTE_ORDER = "BE" - - __author__ = "Kipp Cannon <kipp.cannon@ligo.org>, Chad Hanna <chad.hanna@ligo.org>, Drew Keppel <drew.keppel@ligo.org>" __version__ = "FIXME" __date__ = "FIXME" -## -# @file -# -# A file that contains the pipeparts module code -# - -## -# @package python.pipeparts -# -# pipeparts module - - -# -# ============================================================================= -# -# Generic Constructors -# -# ============================================================================= -# - - -# -# Applications should use the element-specific wrappings that follow below. -# The generic constructors are only intended to simplify the writing of -# those wrappings, they are not meant to be how applications create -# elements in pipelines. -# - - -def mkgeneric(pipeline, src, elem_type_name, **properties): - if "name" in properties: - elem = Gst.ElementFactory.make(elem_type_name, properties.pop("name")) - else: - elem = Gst.ElementFactory.make(elem_type_name, None) - if elem is None: - raise RuntimeError("unknown failure creating \"%s\" element: confirm that the correct plugins are being loaded" % elem_type_name) - # handle properties with names that collide with reserved keywords - if "async_" in properties: - properties["async"] = properties.pop("async_") - for name, value in properties.items(): - elem.set_property(name.replace("_", "-"), value) - pipeline.add(elem) - if isinstance(src, Gst.Pad): - src.get_parent_element().link_pads(src, elem, None) - elif src is not None: - src.link(elem) - return elem - - -# -# deferred link helper -# - - -class src_deferred_link(object): - """! - A class that manages the task of watching for and connecting to new - source pads by name. The inputs are an element, the name of the - source pad to watch for on that element, and the sink pad (on a - different element) to which the source pad should be linked when it - appears. - - The "pad-added" signal of the element will be used to watch for new - pads, and if the "no-more-pads" signal is emitted by the element - before the requested pad has appeared ValueError is raised. - """ - def __init__(self, element, srcpadname, sinkpad): - no_more_pads_handler_id = element.connect("no-more-pads", self.no_more_pads, srcpadname) - assert no_more_pads_handler_id > 0 - pad_added_data = [srcpadname, sinkpad, no_more_pads_handler_id] - pad_added_handler_id = element.connect("pad-added", self.pad_added, pad_added_data) - assert pad_added_handler_id > 0 - pad_added_data.append(pad_added_handler_id) - - @staticmethod - def pad_added(element, pad, src_sink_ids): - srcpadname, sinkpad, no_more_pads_handler_id, pad_added_handler_id = src_sink_ids - if pad.get_name() == srcpadname: - element.handler_disconnect(no_more_pads_handler_id) - element.handler_disconnect(pad_added_handler_id) - pad.link(sinkpad) - - @staticmethod - def no_more_pads(element, srcpadname): - raise ValueError("<%s>: no pad named '%s'" % (element.get_name(), srcpadname)) - - -# -# framecpp channeldemux helpers -# - - -class framecpp_channeldemux_set_units(object): - def __init__(self, elem, units_dict): - """ - Connect a handler for the pad-added signal of the - framecpp_channeldemux element elem, and when a pad is added - to the element if the pad's name appears as a key in the - units_dict dictionary that pad's units property will be set - to the string value associated with that key in the - dictionary. - - Example: - - >>> framecpp_channeldemux_set_units(elem, {"H1:LSC-STRAIN": "strain"}) - - NOTE: this is a work-around to address the problem that - most (all?) frame files do not have units set on their - channel data, whereas downstream consumers of the data - might require information about the units. The demuxer - provides the units as part of a tag event, and - framecpp_channeldemux_set_units() can be used to override - the values, thereby correcting absent or incorrect units - information. - """ - self.elem = elem - self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, units_dict) - assert self.pad_added_handler_id > 0 - - @staticmethod - def pad_added(element, pad, units_dict): - name = pad.get_name() - if name in units_dict: - pad.set_property("units", units_dict[name]) - - -class framecpp_channeldemux_check_segments(object): - """ - Utility to watch for missing data. Pad probes are used to collect - the times spanned by buffers, these are compared to a segment list - defining the intervals of data the stream is required to have. If - any intervals of data are found to have been skipped or if EOS is - seen before the end of the segment list then a ValueError exception - is raised. - - There are two ways to use this tool. To directly install a segment - list monitor on a single pad use the .set_probe() class method. - For elements with dynamic pads, the class can be allowed to - automatically add monitors to pads as they become available by - using the element's pad-added signal. In this case initialize an - instance of the class with the element and a dictionary of segment - lists mapping source pad name to the segment list to check that - pad's output against. - - In both cases a jitter parameter sets the maximum size of a skipped - segment that will be ignored (for example, to accomodate round-off - error in element timestamp computations). The default is 1 ns. - """ - # FIXME: this code now has two conflicting mechanisms for removing - # probes from pads: one code path removes probes when pads get to - # EOS, while the othe removes a probe each time the pad for the - # probe appears a second or subsequent time on an element (and then - # re-installs the probe on the new pad). it's possible that these - # two could attempt to remove the same probe twice, which will - # cause a crash, although it should not happen in current use - # cases. the fix is to rework the probe tracking mechanism so that - # both code paths agree on what probes are installed - def __init__(self, elem, seglists, jitter = LIGOTimeGPS(0, 1)): - self.jitter = jitter - self.probe_handler_ids = {} - # make a copy of the segmentlistdict in case the calling - # code modifies it - self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, seglists.copy()) - assert self.pad_added_handler_id > 0 - - def pad_added(self, element, pad, seglists): - name = pad.get_name() - if name in self.probe_handler_ids: - pad.remove_probe(self.probe_handler_ids.pop(name)) - if name in seglists: - self.probe_handler_ids[name] = self.set_probe(pad, seglists[name], self.jitter) - assert self.probe_handler_ids[name] > 0 - - @classmethod - def set_probe(cls, pad, seglist, jitter = LIGOTimeGPS(0, 1)): - # use a copy of the segmentlist so the probe can modify it - seglist = segments.segmentlist(seglist) - # mutable object to carry data to probe - data = [seglist, jitter, None] - # install probe, save ID in data - probe_id = data[2] = pad.add_probe(Gst.PadProbeType.DATA_DOWNSTREAM, cls.probe, data) - return probe_id - - @staticmethod - def probe(pad, probeinfo, seg_jitter_id): - seglist, jitter, probe_id = seg_jitter_id - if probeinfo.type & Gst.PadProbeType.BUFFER: - obj = probeinfo.get_buffer() - if not obj.mini_object.flags & Gst.BufferFlags.GAP: - # remove the current buffer from the data - # we're expecting to see - seglist -= segments.segmentlist([segments.segment((LIGOTimeGPS(0, obj.pts), LIGOTimeGPS(0, obj.pts + obj.duration)))]) - # ignore missing data intervals unless - # they're bigger than the jitter - iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist) - # are we still expecting to see something that - # precedes the current buffer? - preceding = segments.segment((segments.NegInfinity, LIGOTimeGPS(0, obj.pts))) - if seglist.intersects_segment(preceding): - raise ValueError("%s: detected missing data: %s" % (pad.get_name(), seglist & segments.segmentlist([preceding]))) - elif probeinfo.type & Gst.PadProbeType.EVENT_DOWNSTREAM and probeinfo.get_event().type == Gst.EventType.EOS: - # detach probe at EOS - pad.remove_probe(probe_id) - # ignore missing data intervals unless they're - # bigger than the jitter - iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist) - if seglist: - raise ValueError("%s: at EOS detected missing data: %s" % (pad.get_name(), seglist)) - return True - - -# -# framecpp file sink helpers -# - - -def framecpp_filesink_ldas_path_handler(elem, pspec, path_digits): - """ - Example: - - >>> filesinkelem.connect("notify::timestamp", framecpp_filesink_ldas_path_handler, (".", 5)) - """ - outpath, dir_digits = path_digits - - # get timestamp and truncate to integer seconds - timestamp = elem.get_property("timestamp") // Gst.SECOND - - # extract leading digits - leading_digits = timestamp // 10**int(math.log10(timestamp) + 1 - dir_digits) - - # get other metadata - instrument = elem.get_property("instrument") - frame_type = elem.get_property("frame-type") - - # make target directory, and set path - path = os.path.join(outpath, "%s-%s-%d" % (instrument, frame_type, leading_digits)) - if not os.path.exists(path): - os.makedirs(path) - elem.set_property("path", path) - - -def framecpp_filesink_cache_entry_from_mfs_message(message): - """ - Translate an element message posted by the multifilesink element - inside a framecpp_filesink bin into a lal.utils.CacheEntry object - describing the file being written by the multifilesink element. - """ - # extract the segment spanned by the file from the message directly - start = LIGOTimeGPS(0, message.get_structure()["timestamp"]) - end = start + LIGOTimeGPS(0, message.get_structure()["duration"]) - - # retrieve the framecpp_filesink bin (for instrument/observatory - # and frame file type) - parent = message.src.get_parent() - - # construct and return a CacheEntry object - return CacheEntry(parent.get_property("instrument"), parent.get_property("frame-type"), segments.segment(start, end), "file://localhost%s" % os.path.abspath(message.get_structure()["filename"])) - - -# -# ============================================================================= -# -# Pipeline Parts -# -# ============================================================================= -# - - -def mkchannelgram(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_channelgram", **properties) - - -def mkspectrumplot(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_spectrumplot", **properties) - - -def mkhistogram(pipeline, src): - return mkgeneric(pipeline, src, "lal_histogramplot") - - -## Adds a <a href="@gstlalgtkdoc/GSTLALSegmentSrc.html">lal_segmentsrc</a> element to a pipeline with useful default properties -def mksegmentsrc(pipeline, segment_list, blocksize = 4096 * 1 * 1, invert_output = False): - # default blocksize is 4096 seconds of unsigned integers at - # 1 Hz, e.g. segments without nanoseconds - return mkgeneric(pipeline, None, "lal_segmentsrc", blocksize = blocksize, segment_list = segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list), invert_output = invert_output) - - -## Adds a <a href="@gstlalgtkdoc/GstLALCacheSrc.html">lal_cachesrc</a> element to a pipeline with useful default properties -def mklalcachesrc(pipeline, location, use_mmap = True, **properties): - return mkgeneric(pipeline, None, "lal_cachesrc", location = location, use_mmap = use_mmap, **properties) - - -def mklvshmsrc(pipeline, shm_name, **properties): - return mkgeneric(pipeline, None, "gds_lvshmsrc", shm_name = shm_name, **properties) - - -def mkframexmitsrc(pipeline, multicast_group, port, **properties): - return mkgeneric(pipeline, None, "gds_framexmitsrc", multicast_group = multicast_group, port = port, **properties) - - -def mkigwdparse(pipeline, src, **properties): - return mkgeneric(pipeline, src, "framecpp_igwdparse", **properties) - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-uridecodebin.html">uridecodebin</a> element to a pipeline with useful default properties -def mkuridecodebin(pipeline, uri, caps = "application/x-igwd-frame,framed=true", **properties): - return mkgeneric(pipeline, None, "uridecodebin", uri = uri, caps = None if caps is None else Gst.Caps.from_string(caps), **properties) - - -def mkframecppchanneldemux(pipeline, src, **properties): - return mkgeneric(pipeline, src, "framecpp_channeldemux", **properties) - - -def mkframecppchannelmux(pipeline, channel_src_map, units = None, seglists = None, **properties): - elem = mkgeneric(pipeline, None, "framecpp_channelmux", **properties) - if channel_src_map is not None: - for channel, src in channel_src_map.items(): - for srcpad in src.srcpads: - # FIXME FIXME FIXME. This should use the pad template from the element. - # FIXME once a newer version of some library is available, then we should be able to switch to this - # if srcpad.link(elem.get_request_pad(channel)) == Gst.PadLinkReturn.OK - # Instead. Right now it fails due to the - # underscore in channel names. When it fails - # it fails silently and returns None, which - # gives a cryptic error message - if srcpad.link(elem.request_pad(Gst.PadTemplate.new(channel, Gst.PadDirection.SINK, Gst.PadPresence.REQUEST, Gst.Caps("ANY")), channel)) == Gst.PadLinkReturn.OK: - break - if units is not None: - framecpp_channeldemux_set_units(elem, units) - if seglists is not None: - framecpp_channeldemux_check_segments(elem, seglists) - return elem - - -def mkframecppfilesink(pipeline, src, message_forward = True, **properties): - post_messages = properties.pop("post_messages", True) - elem = mkgeneric(pipeline, src, "framecpp_filesink", message_forward = message_forward, **properties) - # FIXME: there's supposed to be some sort of proxy mechanism for - # setting properties on child elements, but we can't seem to get - # anything to work - elem.get_by_name("multifilesink").set_property("post-messages", post_messages) - return elem - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-multifilesink.html">multifilesink</a> element to a pipeline with useful default properties -def mkmultifilesink(pipeline, src, next_file = 0, sync = False, async_ = False, **properties): - properties["async"] = async_ - return mkgeneric(pipeline, src, "multifilesink", next_file = next_file, sync = sync, **properties) - - -def mkndssrc(pipeline, host, instrument, channel_name, channel_type, blocksize = 16384 * 8 * 1, port = 31200): - # default blocksize is 1 second of double precision floats at - # 16384 Hz, e.g., LIGO h(t) - return mkgeneric(pipeline, None, "ndssrc", blocksize = blocksize, port = port, host = host, channel_name = "%s:%s" % (instrument, channel_name), channel_type = channel_type) - - -## Adds a <a href="@gstdoc/gstreamer-plugins-capsfilter.html">capsfilter</a> element to a pipeline with useful default properties -def mkcapsfilter(pipeline, src, caps, **properties): - return mkgeneric(pipeline, src, "capsfilter", caps = Gst.Caps.from_string(caps), **properties) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-capssetter.html">capssetter</a> element to a pipeline with useful default properties -def mkcapssetter(pipeline, src, caps, **properties): - return mkgeneric(pipeline, src, "capssetter", caps = Gst.Caps.from_string(caps), **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALStateVector.html">lal_statevector</a> element to a pipeline with useful default properties -def mkstatevector(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_statevector", **properties) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-taginject.html">taginject</a> element to a pipeline with useful default properties -def mktaginject(pipeline, src, tags): - return mkgeneric(pipeline, src, "taginject", tags = tags) - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiotestsrc.html">audiotestsrc</a> element to a pipeline with useful default properties -def mkaudiotestsrc(pipeline, **properties): - return mkgeneric(pipeline, None, "audiotestsrc", **properties) - - -## see documentation for mktaginject() mkcapsfilter() and mkaudiotestsrc() -def mkfakesrc(pipeline, instrument, channel_name, blocksize = None, volume = 1e-20, is_live = False, wave = 9, rate = 16384, **properties): - if blocksize is None: - # default blocksize is 1 second * rate samples/second * 8 - # bytes/sample (assume double-precision floats) - blocksize = 1 * rate * 8 - return mktaginject(pipeline, mkcapsfilter(pipeline, mkaudiotestsrc(pipeline, samplesperbuffer = blocksize / 8, wave = wave, volume = volume, is_live = is_live, **properties), "audio/x-raw, format=F64%s, rate=%d" % (BYTE_ORDER, rate)), "instrument=%s,channel-name=%s,units=strain" % (instrument, channel_name)) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiofirfilter.html">audiofirfilter</a> element to a pipeline with useful default properties -def mkfirfilter(pipeline, src, kernel, latency, **properties): - properties.update((name, val) for name, val in (("kernel", kernel), ("latency", latency)) if val is not None) - return mkgeneric(pipeline, src, "audiofirfilter", **properties) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioiirfilter.html">audioiirfilter</a> element to a pipeline with useful default properties -def mkiirfilter(pipeline, src, a, b): - # convention is z = \exp(-i 2 \pi f / f_{\rm sampling}) - # H(z) = (\sum_{j=0}^{N} a_j z^{-j}) / (\sum_{j=0}^{N} (-1)^{j} b_j z^{-j}) - return mkgeneric(pipeline, src, "audioiirfilter", a = a, b = b) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALShift.html">lal_shift</a> element to a pipeline with useful default properties -def mkshift(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_shift", **properties) - - -def mkfakeLIGOsrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1): - properties = {"blocksize": blocksize} - properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None) - return mkgeneric(pipeline, None, "lal_fakeligosrc", **properties) - - -def mkfakeadvLIGOsrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1): - properties = {"blocksize": blocksize} - properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None) - return mkgeneric(pipeline, None, "lal_fakeadvligosrc", **properties) - - -def mkfakeadvvirgosrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1): - properties = {"blocksize": blocksize} - if instrument is not None: - properties["instrument"] = instrument - if channel_name is not None: - properties["channel_name"] = channel_name - return mkgeneric(pipeline, None, "lal_fakeadvvirgosrc", **properties) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-progressreport.html">progress_report</a> element to a pipeline with useful default properties -def mkprogressreport(pipeline, src, name): - return mkgeneric(pipeline, src, "progressreport", do_query = False, name = name) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALSimulation.html">lal_simulation</a> element to a pipeline with useful default properties -def mkinjections(pipeline, src, filename): - return mkgeneric(pipeline, src, "lal_simulation", xml_location = filename) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiochebband.html">audiochebband</a> element to a pipeline with useful default properties -def mkaudiochebband(pipeline, src, lower_frequency, upper_frequency, poles = 8): - return mkgeneric(pipeline, src, "audiochebband", lower_frequency = lower_frequency, upper_frequency = upper_frequency, poles = poles) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiocheblimit.html">audiocheblimit</a> element to a pipeline with useful default properties -def mkaudiocheblimit(pipeline, src, cutoff, mode = 0, poles = 8, type = 1, ripple = 0.25): - return mkgeneric(pipeline, src, "audiocheblimit", cutoff = cutoff, mode = mode, poles = poles, type = type, ripple = ripple) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioamplify.html">audioamplify</a> element to a pipeline with useful default properties -def mkaudioamplify(pipeline, src, amplification): - return mkgeneric(pipeline, src, "audioamplify", clipping_method = 3, amplification = amplification) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALAudioUnderSample.html">lal_audioundersample</a> element to a pipeline with useful default properties -def mkaudioundersample(pipeline, src): - return mkgeneric(pipeline, src, "lal_audioundersample") - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioresample.html">audioresample</a> element to a pipeline with useful default properties -def mkresample(pipeline, src, **properties): - return mkgeneric(pipeline, src, "audioresample", **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALInterpolator.html">lal_interpolator</a> element to a pipeline with useful default properties -def mkinterpolator(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_interpolator", **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties -def mkwhiten(pipeline, src, psd_mode = 0, zero_pad = 0, fft_length = 8, average_samples = 64, median_samples = 7, **properties): - return mkgeneric(pipeline, src, "lal_whiten", psd_mode = psd_mode, zero_pad = zero_pad, fft_length = fft_length, average_samples = average_samples, median_samples = median_samples, **properties) - - -## Adds a <a href="@gstdoc/gstreamer-plugins-tee.html">tee</a> element to a pipeline with useful default properties -def mktee(pipeline, src): - return mkgeneric(pipeline, src, "tee") - - -## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "sum" mode mixing. -def mkadder(pipeline, srcs, sync = True, mix_mode = "sum", **properties): - elem = mkgeneric(pipeline, None, "lal_adder", sync = sync, mix_mode = mix_mode, **properties) - if srcs is not None: - for src in srcs: - src.link(elem) - return elem - - -## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "product" mode mixing. -def mkmultiplier(pipeline, srcs, sync = True, mix_mode = "product", **properties): - return mkadder(pipeline, srcs, sync = sync, mix_mode = mix_mode, **properties) - - -## Adds a <a href="@gstdoc/gstreamer-plugins-queue.html">queue</a> element to a pipeline with useful default properties -def mkqueue(pipeline, src, **properties): - return mkgeneric(pipeline, src, "queue", **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties -def mkdrop(pipeline, src, drop_samples = 0): - return mkgeneric(pipeline, src, "lal_drop", drop_samples = drop_samples) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALNoFakeDisconts.html">lal_nofakedisconts</a> element to a pipeline with useful default properties -def mknofakedisconts(pipeline, src, silent = True): - return mkgeneric(pipeline, src, "lal_nofakedisconts", silent = silent) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALFIRBank.html">lal_firbank</a> element to a pipeline with useful default properties -def mkfirbank(pipeline, src, latency = None, fir_matrix = None, time_domain = None, block_stride = None): - properties = dict((name, value) for name, value in zip(("latency", "fir_matrix", "time_domain", "block_stride"), (latency, fir_matrix, time_domain, block_stride)) if value is not None) - return mkgeneric(pipeline, src, "lal_firbank", **properties) - - -def mktdwhiten(pipeline, src, latency = None, kernel = None, taper_length = None): - # a taper length of 1/4 kernel length mimics the default - # configuration of the FFT whitener - if taper_length is None and kernel is not None: - taper_length = len(kernel) // 4 - properties = dict((name, value) for name, value in zip(("latency", "kernel", "taper_length"), (latency, kernel, taper_length)) if value is not None) - return mkgeneric(pipeline, src, "lal_tdwhiten", **properties) - - -def mktrim(pipeline, src, initial_offset = None, final_offset = None, inverse = None): - properties = dict((name, value) for name, value in zip(("initial-offset", "final-offset", "inverse"), (initial_offset,final_offset,inverse)) if value is not None) - return mkgeneric(pipeline, src, "lal_trim", **properties) - - -def mkmean(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_mean", **properties) - - -def mkabs(pipeline, src, **properties): - return mkgeneric(pipeline, src, "abs", **properties) - - -def mkpow(pipeline, src, **properties): - return mkgeneric(pipeline, src, "pow", **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALReblock.html">lal_reblock</a> element to a pipeline with useful default properties -def mkreblock(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_reblock", **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALSumSquares.html">lal_sumsquares</a> element to a pipeline with useful default properties -def mksumsquares(pipeline, src, weights = None): - if weights is not None: - return mkgeneric(pipeline, src, "lal_sumsquares", weights = weights) - else: - return mkgeneric(pipeline, src, "lal_sumsquares") - - -## Adds a <a href="@gstlalgtkdoc/GSTLALGate.html">lal_gate</a> element to a pipeline with useful default properties -def mkgate(pipeline, src, threshold = None, control = None, **properties): - if threshold is not None: - elem = mkgeneric(pipeline, None, "lal_gate", threshold = threshold, **properties) - else: - elem = mkgeneric(pipeline, None, "lal_gate", **properties) - for peer, padname in ((src, "sink"), (control, "control")): - if isinstance(peer, Gst.Pad): - peer.get_parent_element().link_pads(peer, elem, padname) - elif peer is not None: - peer.link_pads(None, elem, padname) - return elem - - -def mkbitvectorgen(pipeline, src, bit_vector, **properties): - return mkgeneric(pipeline, src, "lal_bitvectorgen", bit_vector = bit_vector, **properties) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALMatrixMixer.html">lal_matrixmixer</a> element to a pipeline with useful default properties -def mkmatrixmixer(pipeline, src, matrix = None): - if matrix is not None: - return mkgeneric(pipeline, src, "lal_matrixmixer", matrix = matrix) - else: - return mkgeneric(pipeline, src, "lal_matrixmixer") - - -## Adds a <a href="@gstlalgtkdoc/GSTLALToggleComplex.html">lal_togglecomplex</a> element to a pipeline with useful default properties -def mktogglecomplex(pipeline, src): - return mkgeneric(pipeline, src, "lal_togglecomplex") - - -## Adds a <a href="@gstlalgtkdoc/GSTLALAutoChiSq.html">lal_autochisq</a> element to a pipeline with useful default properties -def mkautochisq(pipeline, src, autocorrelation_matrix = None, mask_matrix = None, latency = 0, snr_thresh=0): - properties = {} - if autocorrelation_matrix is not None: - properties.update({ - "autocorrelation_matrix": pipeio.repack_complex_array_to_real(autocorrelation_matrix), - "latency": latency, - "snr_thresh": snr_thresh - }) - if mask_matrix is not None: - properties["autocorrelation_mask_matrix"] = mask_matrix - return mkgeneric(pipeline, src, "lal_autochisq", **properties) - - -## Adds a <a href="@gstdoc/gstreamer-plugins-fakesink.html">fakesink</a> element to a pipeline with useful default properties -def mkfakesink(pipeline, src): - return mkgeneric(pipeline, src, "fakesink", sync = False, **{"async": False}) - - -## Adds a <a href="@gstdoc/gstreamer-plugins-filesink.html">filesink</a> element to a pipeline with useful default properties -def mkfilesink(pipeline, src, filename, sync = False, async_ = False): - return mkgeneric(pipeline, src, "filesink", sync = sync, buffer_mode = 2, location = filename, **{"async": async_}) - - -## Adds a <a href="@gstlalgtkdoc/GstTSVEnc.html">lal_nxydump</a> element to a pipeline with useful default properties -def mknxydumpsink(pipeline, src, filename, segment = None): - if segment is not None: - elem = mkgeneric(pipeline, src, "lal_nxydump", start_time = segment[0].ns(), stop_time = segment[1].ns()) - else: - elem = mkgeneric(pipeline, src, "lal_nxydump") - return mkfilesink(pipeline, elem, filename) - - -def mknxydumpsinktee(pipeline, src, *args, **properties): - t = mktee(pipeline, src) - mknxydumpsink(pipeline, mkqueue(pipeline, t), *args, **properties) - return t - - -def mkblcbctriggergen(pipeline, snr, chisq, template_bank_filename, snr_threshold, sigmasq): - # snr is complex and chisq is real so the correct source and sink - # pads will be selected automatically - elem = mkgeneric(pipeline, snr, "lal_blcbctriggergen", bank_filename = template_bank_filename, snr_thresh = snr_threshold, sigmasq = sigmasq) - chisq.link(elem) - return elem - - -def mktriggergen(pipeline, snr, chisq, template_bank_filename, snr_threshold, sigmasq): - # snr is complex and chisq is real so the correct source and sink - # pads will be selected automatically - elem = mkgeneric(pipeline, snr, "lal_triggergen", bank_filename = template_bank_filename, snr_thresh = snr_threshold, sigmasq = sigmasq) - chisq.link(elem) - return elem - - -def mktriggerxmlwritersink(pipeline, src, filename): - return mkgeneric(pipeline, src, "lal_triggerxmlwriter", location = filename, sync = False, **{"async": False}) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-wavenc.html">wavenc</a> element to a pipeline with useful default properties -def mkwavenc(pipeline, src): - return mkgeneric(pipeline, src, "wavenc") - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-vorbisenc.html">vorbisenc</a> element to a pipeline with useful default properties -def mkvorbisenc(pipeline, src): - return mkgeneric(pipeline, src, "vorbisenc") - - -def mkcolorspace(pipeline, src): - return mkgeneric(pipeline, src, "ffmpegcolorspace") # MOD: Found ffmpegcolorspace in line: [ return mkgeneric(pipeline, src, "ffmpegcolorspace")] - - -def mktheoraenc(pipeline, src, **properties): - return mkgeneric(pipeline, src, "theoraenc", **properties) - - -def mkmpeg4enc(pipeline, src, **properties): - return mkgeneric(pipeline, src, "ffenc_mpeg4", **properties) - - -def mkoggmux(pipeline, src): - return mkgeneric(pipeline, src, "oggmux") - - -def mkavimux(pipeline, src): - return mkgeneric(pipeline, src, "avimux") - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioconvert.html">audioconvert</a> element to a pipeline with useful default properties -def mkaudioconvert(pipeline, src, caps_string = None): - elem = mkgeneric(pipeline, src, "audioconvert") - if caps_string is not None: - elem = mkcapsfilter(pipeline, elem, caps_string) - return elem - - -## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiorate.html">audiorate</a> element to a pipeline with useful default properties -def mkaudiorate(pipeline, src, **properties): - return mkgeneric(pipeline, src, "audiorate", **properties) - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-flacenc.html">flacenc</a> element to a pipeline with useful default properties -def mkflacenc(pipeline, src, quality = 0, **properties): - return mkgeneric(pipeline, src, "flacenc", quality = quality, **properties) - - -def mkogmvideosink(pipeline, videosrc, filename, audiosrc = None, verbose = False): - src = mkcolorspace(pipeline, videosrc) - src = mkcapsfilter(pipeline, src, "video/x-raw-yuv, format=(fourcc)I420") - src = mktheoraenc(pipeline, src, border = 2, quality = 48, quick = False) - src = mkoggmux(pipeline, src) - if audiosrc is not None: - mkflacenc(pipeline, mkcapsfilter(pipeline, mkaudioconvert(pipeline, audiosrc), "audio/x-raw, format=S24%s" % BYTE_ORDER)).link(src) - if verbose: - src = mkprogressreport(pipeline, src, filename) - mkfilesink(pipeline, src, filename) - - -def mkvideosink(pipeline, src): - return mkgeneric(pipeline, mkcolorspace(pipeline, src), "autovideosink") - - -## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-autoaudiosink.html">autoaudiosink</a> element to a pipeline with useful default properties -def mkautoaudiosink(pipeline, src): - return mkgeneric(pipeline, mkqueue(pipeline, src), "autoaudiosink") - - -def mkplaybacksink(pipeline, src, amplification = 0.1): - elems = ( - Gst.ElementFactory.make("audioconvert", None), - Gst.ElementFactory.make("capsfilter", None), - Gst.ElementFactory.make("audioamplify", None), - Gst.ElementFactory.make("audioconvert", None), - Gst.ElementFactory.make("queue", None), - Gst.ElementFactory.make("autoaudiosink", None) - ) - elems[1].set_property("caps", Gst.Caps.from_string("audio/x-raw, format=F32%s" % BYTE_ORDER)) - elems[2].set_property("amplification", amplification) - elems[4].set_property("max-size-time", 1 * Gst.SECOND) - pipeline.add(*elems) - Gst.element_link_many(src, *elems) # MOD: Error line [733]: element_link_many not yet implemented. See web page ** - - -def mkdeglitcher(pipeline, src, segment_list): - return mkgeneric(pipeline, src, "lal_deglitcher", segment_list = segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list)) - - -# FIXME no specific alias for this url since this library only has one element. -# DO NOT DOCUMENT OTHER CODES THIS WAY! Use @gstdoc @gstpluginsbasedoc etc. -## Adds a <a href="http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-libs/html/gstreamer-app.html">appsink</a> element to a pipeline with useful default properties -def mkappsink(pipeline, src, max_buffers = 1, drop = False, sync = False, async_ = False, **properties): - properties["async"] = async_ - return mkgeneric(pipeline, src, "appsink", sync = sync, emit_signals = True, max_buffers = max_buffers, drop = drop, **properties) - - -class AppSync(object): - def __init__(self, appsink_new_buffer, appsinks = []): - self.lock = threading.Lock() - # handler to invoke on availability of new time-ordered - # buffer - self.appsink_new_buffer = appsink_new_buffer - # element --> timestamp of current buffer or None if no - # buffer yet available - self.appsinks = {} - # set of sink elements that are currently at EOS - self.at_eos = set() - # attach handlers to appsink elements provided at this time - for elem in appsinks: - self.attach(elem) - - def add_sink(self, pipeline, src, drop = False, **properties): - return self.attach(mkappsink(pipeline, src, drop = drop, **properties)) - - def attach(self, appsink): - """ - connect this AppSync's signal handlers to the given appsink - element. the element's max-buffers property will be set to - 1 (required for AppSync to work). - """ - if appsink in self.appsinks: - raise ValueError("duplicate appsinks %s" % repr(appsink)) - appsink.set_property("max-buffers", 1) - handler_id = appsink.connect("new-preroll", self.new_preroll_handler) - assert handler_id > 0 - handler_id = appsink.connect("new-sample", self.new_sample_handler) - assert handler_id > 0 - handler_id = appsink.connect("eos", self.eos_handler) - assert handler_id > 0 - self.appsinks[appsink] = None - return appsink - - def new_preroll_handler(self, elem): - with self.lock: - # clear eos status - self.at_eos.discard(elem) - # ignore preroll buffers - elem.emit("pull-preroll") - return Gst.FlowReturn.OK - - def new_sample_handler(self, elem): - with self.lock: - # clear eos status, and retrieve buffer timestamp - self.at_eos.discard(elem) - assert self.appsinks[elem] is None - self.appsinks[elem] = elem.get_last_sample().get_buffer().pts - # pull available buffers from appsink elements - return self.pull_buffers(elem) - - def eos_handler(self, elem): - with self.lock: - # set eos status - self.at_eos.add(elem) - # pull available buffers from appsink elements - return self.pull_buffers(elem) - - def pull_buffers(self, elem): - """ - for internal use. must be called with lock held. - """ - # keep looping while we can process buffers - while 1: - # retrieve the timestamps of all elements that - # aren't at eos and all elements at eos that still - # have buffers in them - timestamps = [(t, e) for e, t in self.appsinks.items() if e not in self.at_eos or t is not None] - # if all elements are at eos and none have buffers, - # then we're at eos - if not timestamps: - return Gst.FlowReturn.EOS - # find the element with the oldest timestamp. None - # compares as less than everything, so we'll find - # any element (that isn't at eos) that doesn't yet - # have a buffer (elements at eos and that are - # without buffers aren't in the list) - timestamp, elem_with_oldest = min(timestamps, key=lambda x: x[0] if x[0] is not None else -numpy.inf) - # if there's an element without a buffer, quit for - # now --- we require all non-eos elements to have - # buffers before proceding - if timestamp is None: - return Gst.FlowReturn.OK - # clear timestamp and pass element to handler func. - # function call is done last so that all of our - # book-keeping has been taken care of in case an - # exception gets raised - self.appsinks[elem_with_oldest] = None - self.appsink_new_buffer(elem_with_oldest) - - -class connect_appsink_dump_dot(object): - """ - add a signal handler to write a pipeline graph upon receipt of the - first trigger buffer. the caps in the pipeline graph are not fully - negotiated until data comes out the end, so this version of the graph - shows the final formats on all links - """ - def __init__(self, pipeline, appsinks, basename, verbose = False): - self.pipeline = pipeline - self.filestem = "%s.%s" % (basename, "TRIGGERS") - self.verbose = verbose - # map element to handler ID - self.remaining_lock = threading.Lock() - self.remaining = {} - for sink in appsinks: - self.remaining[sink] = sink.connect_after("new-preroll", self.execute) - assert self.remaining[sink] > 0 - - def execute(self, elem): - with self.remaining_lock: - handler_id = self.remaining.pop(elem) - if not self.remaining: - write_dump_dot(self.pipeline, self.filestem, verbose = self.verbose) - elem.disconnect(handler_id) - return Gst.FlowReturn.OK - - -def mkchecktimestamps(pipeline, src, name = None, silent = True, timestamp_fuzz = 1): - return mkgeneric(pipeline, src, "lal_checktimestamps", name = name, silent = silent, timestamp_fuzz = timestamp_fuzz) - - -## Adds a <a href="@gstlalgtkdoc/GSTLALPeak.html">lal_peak</a> element to a pipeline with useful default properties -def mkpeak(pipeline, src, n): - return mkgeneric(pipeline, src, "lal_peak", n = n) - - -def mkdenoiser(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_denoiser", **properties) - - -def mkclean(pipeline, src, threshold=1.0): - return mkdenoiser(pipeline, src, stationary=True, threshold=threshold) - - -def mkitac(pipeline, src, n, bank, autocorrelation_matrix = None, mask_matrix = None, snr_thresh = 0, sigmasq = None): - properties = { - "n": n, - "bank_filename": bank, - "snr_thresh": snr_thresh - } - if autocorrelation_matrix is not None: - properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix) - if mask_matrix is not None: - properties["autocorrelation_mask"] = mask_matrix - if sigmasq is not None: - properties["sigmasq"] = sigmasq - return mkgeneric(pipeline, src, "lal_itac", **properties) - -def mktrigger(pipeline, src, n, autocorrelation_matrix = None, mask_matrix = None, snr_thresh = 0, sigmasq = None, max_snr = False): - properties = { - "n": n, - "snr_thresh": snr_thresh, - "max_snr": max_snr - } - if autocorrelation_matrix is not None: - properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix) - if mask_matrix is not None: - properties["autocorrelation_mask"] = mask_matrix - if sigmasq is not None: - properties["sigmasq"] = sigmasq - return mkgeneric(pipeline, src, "lal_trigger", **properties) - -def mklatency(pipeline, src, name = None, silent = False): - return mkgeneric(pipeline, src, "lal_latency", name = name, silent = silent) - -def mklhocoherentnull(pipeline, H1src, H2src, H1_impulse, H1_latency, H2_impulse, H2_latency, srate): - elem = mkgeneric(pipeline, None, "lal_lho_coherent_null", block_stride = srate, H1_impulse = H1_impulse, H2_impulse = H2_impulse, H1_latency = H1_latency, H2_latency = H2_latency) - for peer, padname in ((H1src, "H1sink"), (H2src, "H2sink")): - if isinstance(peer, Gst.Pad): - peer.get_parent_element().link_pads(peer, elem, padname) - elif peer is not None: - peer.link_pads(None, elem, padname) - return elem - -def mkcomputegamma(pipeline, dctrl, exc, cos, sin, **properties): - elem = mkgeneric(pipeline, None, "lal_compute_gamma", **properties) - for peer, padname in ((dctrl, "dctrl_sink"), (exc, "exc_sink"), (cos, "cos"), (sin, "sin")): - if isinstance(peer, Gst.Pad): - peer.get_parent_element().link_pads(peer, elem, padname) - elif peer is not None: - peer.link_pads(None, elem, padname) - return elem - -def mkbursttriggergen(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_bursttriggergen", **properties) - -def mkodctodqv(pipeline, src, **properties): - return mkgeneric(pipeline, src, "lal_odc_to_dqv", **properties) - -def mktcpserversink(pipeline, src, **properties): - # units_soft_max = 1 GB - # FIXME: are these sensible defaults? - return mkgeneric(pipeline, src, "tcpserversink", sync = True, sync_method = "latest-keyframe", recover_policy = "keyframe", unit_type = "bytes", units_soft_max = 1024**3, **properties) - - -def audioresample_variance_gain(quality, num, den): - """Calculate the output gain of GStreamer's stock audioresample element. - - The audioresample element has a frequency response of unity "almost" all the - way up the Nyquist frequency. However, for an input of unit variance - Gaussian noise, the output will have a variance very slighly less than 1. - The return value is the variance that the filter will produce for a given - "quality" setting and sample rate. - - @param den The denomenator of the ratio of the input and output sample rates - @param num The numerator of the ratio of the input and output sample rates - @return The variance of the output signal for unit variance input - - The following example shows how to apply the correction factor using an - audioamplify element. - - >>> from gstlal.pipeutil import * - >>> from gstlal.pipeparts import audioresample_variance_gain - >>> from gstlal import pipeio - >>> import numpy - >>> nsamples = 2 ** 17 - >>> num = 2 - >>> den = 1 - >>> def handoff_handler(element, buffer, pad, (quality, filt_len, num, den)): - ... out_latency = numpy.ceil(float(den) / num * filt_len) - ... buf = pipeio.array_from_audio_buffer(buffer).flatten() - ... std = numpy.std(buf[out_latency:-out_latency]) - ... print "quality=%2d, filt_len=%3d, num=%d, den=%d, stdev=%.2f" % ( - ... quality, filt_len, num, den, std) - ... - >>> for quality in range(11): - ... pipeline = Gst.Pipeline() - ... correction = 1/numpy.sqrt(audioresample_variance_gain(quality, num, den)) - ... elems = mkelems_in_bin(pipeline, - ... ('audiotestsrc', {'wave':'gaussian-noise','volume':1}), - ... ('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,format=F64LE,rate=%d' % num)}), - ... ('audioresample', {'quality':quality}), - ... ('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,width=F64LE,rate=%d' % den)}), - ... ('audioamplify', {'amplification':correction,'clipping-method':'none'}), - ... ('fakesink', {'signal-handoffs':True, 'num-buffers':1}) - ... ) - ... filt_len = elems[2].get_property('filter-length') - ... elems[0].set_property('samplesperbuffer', 2 * filt_len + nsamples) - ... if elems[-1].connect_after('handoff', handoff_handler, (quality, filt_len, num, den)) < 1: - ... raise RuntimeError - ... try: - ... if pipeline.set_state(Gst.State.PLAYING) is not Gst.State.CHANGE_ASYNC: - ... raise RuntimeError - ... if not pipeline.get_bus().poll(Gst.MessageType.EOS, -1): - ... raise RuntimeError - ... finally: - ... if pipeline.set_state(Gst.State.NULL) is not Gst.StateChangeReturn.SUCCESS: - ... raise RuntimeError - ... - quality= 0, filt_len= 8, num=2, den=1, stdev=1.00 - quality= 1, filt_len= 16, num=2, den=1, stdev=1.00 - quality= 2, filt_len= 32, num=2, den=1, stdev=1.00 - quality= 3, filt_len= 48, num=2, den=1, stdev=1.00 - quality= 4, filt_len= 64, num=2, den=1, stdev=1.00 - quality= 5, filt_len= 80, num=2, den=1, stdev=1.00 - quality= 6, filt_len= 96, num=2, den=1, stdev=1.00 - quality= 7, filt_len=128, num=2, den=1, stdev=1.00 - quality= 8, filt_len=160, num=2, den=1, stdev=1.00 - quality= 9, filt_len=192, num=2, den=1, stdev=1.00 - quality=10, filt_len=256, num=2, den=1, stdev=1.00 - """ - - # These constants were measured with 2**22 samples. - - if num > den: # downsampling - return den * ( - 0.7224862140943990596, - 0.7975021342935247892, - 0.8547537598970208483, - 0.8744072146753004704, - 0.9075294214410336568, - 0.9101523813406768859, - 0.9280549396020538744, - 0.9391809530012216189, - 0.9539276644089494939, - 0.9623083437067311285, - 0.9684700588501590213 - )[quality] / num - elif num < den: # upsampling - return ( - 0.7539740617648067467, - 0.8270076656536116122, - 0.8835072979478705291, - 0.8966758456219333651, - 0.9253434087537378838, - 0.9255866674042573239, - 0.9346487800036394900, - 0.9415331868209220190, - 0.9524608799160205752, - 0.9624372769883490220, - 0.9704505626409354324 - )[quality] - else: # no change in sample rate - return 1. - - -# -# ============================================================================= -# -# Debug utilities -# -# ============================================================================= -# - - -def write_dump_dot(pipeline, filestem, verbose = False): - """ - This function needs the environment variable GST_DEBUG_DUMP_DOT_DIR - to be set. The filename will be - - os.path.join($GST_DEBUG_DUMP_DOT_DIR, filestem + ".dot") - - If verbose is True, a message will be written to stderr. - """ - if "GST_DEBUG_DUMP_DOT_DIR" not in os.environ: - raise ValueError("cannot write pipeline, environment variable GST_DEBUG_DUMP_DOT_DIR is not set") - Gst.debug_bin_to_dot_file(pipeline, Gst.DebugGraphDetails.ALL, filestem) - if verbose: - print("Wrote pipeline to %s" % os.path.join(os.environ["GST_DEBUG_DUMP_DOT_DIR"], "%s.dot" % filestem), file=sys.stderr) +__doc__ = """The `pipeparts` module contains all of the *elements* used to construct gstreamer pipelines +for gravitational wave analysis. These elements are grouped into thematic submodules. +""" + + +# Flatten package to preserve import paths +from .sink import AppSync as AppSync +from .sink import BYTE_ORDER as BYTE_ORDER +from .transform import audioresample_variance_gain as audioresample_variance_gain +from .sink import ConnectAppsinkDumpDot as connect_appsink_dump_dot +from .mux import FrameCPPChannelDemuxCheckSegmentsHandler as framecpp_channeldemux_check_segments +from .mux import FrameCPPChannelDemuxSetUnitsHandler as framecpp_channeldemux_set_units +from .sink import framecpp_filesink_cache_entry_from_mfs_message as framecpp_filesink_cache_entry_from_mfs_message +from .sink import framecpp_filesink_ldas_path_handler as framecpp_filesink_ldas_path_handler +from .transform import abs_ as mkabs +from .transform import adder as mkadder +from .sink import app as mkappsink +from .transform import amplify as mkaudioamplify +from .filters import audio_cheb_band as mkaudiochebband +from .filters import audio_cheb_limit as mkaudiocheblimit +from .transform import audio_convert as mkaudioconvert +from .transform import audio_rate as mkaudiorate +from .source import audio_test as mkaudiotestsrc +from .transform import undersample as mkaudioundersample +from .sink import auto_audio as mkautoaudiosink +from .transform import auto_chisq as mkautochisq +from .mux import avi_mux as mkavimux +from .transform import bit_vector_gen as mkbitvectorgen +from .trigger import blcbc_trigger_gen as mkblcbctriggergen +from .trigger import burst_trigger_gen as mkbursttriggergen +from .filters import caps as mkcapsfilter +from .transform import set_caps as mkcapssetter +from .plot import channelgram as mkchannelgram +from .transform import check_timestamps as mkchecktimestamps +from .transform import clean as mkclean +from .transform import colorspace as mkcolorspace +from .transform import mkcomputegamma as mkcomputegamma +from .transform import deglitch as mkdeglitcher +from .transform import denoise as mkdenoiser +from .filters import drop as mkdrop +from .source import fake_ligo as mkfakeLIGOsrc +from .source import fake_aligo as mkfakeadvLIGOsrc +from .source import fake_avirgo as mkfakeadvvirgosrc +from .sink import fake as mkfakesink +from .source import fake as mkfakesrc +from .sink import file as mkfilesink +from .transform import fir_bank as mkfirbank +from .filters import fir as mkfirfilter +from .encode import flac as mkflacenc +from .mux import framecpp_channel_demux as mkframecppchanneldemux +from .mux import framecpp_channel_mux as mkframecppchannelmux +from .sink import gwf as mkframecppfilesink +from .source import framexmit as mkframexmitsrc +from .filters import gate as mkgate +from .pipetools import make_element_with_src as mkgeneric +from .plot import histogram as mkhistogram +from .encode import igwd_parse as mkigwdparse +from .filters import iir as mkiirfilter +from .filters import inject as mkinjections +from .transform import interpolator as mkinterpolator +from .trigger import itac as mkitac +from .source import cache as mklalcachesrc +from .transform import latency as mklatency +from .transform import lho_coherent_null as mklhocoherentnull +from .source import lvshm as mklvshmsrc +from .transform import matrix_mixer as mkmatrixmixer +from .transform import mean as mkmean +from .sink import multi_file as mkmultifilesink +from .transform import multiplier as mkmultiplier +from .source import nds as mkndssrc +from .filters import remove_fake_disconts as mknofakedisconts +from .sink import tsv as mknxydumpsink +from .sink import tsv_tee as mknxydumpsinktee +from .transform import mkodctodqv as mkodctodqv +from .mux import ogg_mux as mkoggmux +from .sink import ogm_video as mkogmvideosink +from .transform import peak as mkpeak +from .sink import playback as mkplaybacksink +from .transform import pow as mkpow +from .transform import progress_report as mkprogressreport +from .transform import queue as mkqueue +from .transform import reblock as mkreblock +from .transform import resample as mkresample +from .source import segment as mksegmentsrc +from .transform import shift as mkshift +from .plot import spectrum as mkspectrumplot +from .filters import state_vector as mkstatevector +from .transform import sum_squares as mksumsquares +from .transform import tag_inject as mktaginject +from .sink import tcp_server as mktcpserversink +from .transform import td_whiten as mktdwhiten +from .transform import tee as mktee +from .encode import theora as mktheoraenc +from .transform import toggle_complex as mktogglecomplex +from .trigger import trigger as mktrigger +from .trigger import trigger_gen as mktriggergen +from .sink import trigger_xml_writer as mktriggerxmlwritersink +from .transform import trim as mktrim +from .encode import uri_decode_bin as mkuridecodebin +from .sink import auto_video as mkvideosink +from .encode import vorbis as mkvorbisenc +from .encode import wav as mkwavenc +from .transform import whiten as mkwhiten +from .source import SrcDeferredLink as src_deferred_link +from .pipedot import to_file as write_dump_dot diff --git a/gstlal/python/pipeparts/encode.py b/gstlal/python/pipeparts/encode.py new file mode 100644 index 0000000000000000000000000000000000000000..dd2cec8dd64de8ca78c6f85e2900813989022f7c --- /dev/null +++ b/gstlal/python/pipeparts/encode.py @@ -0,0 +1,130 @@ +"""Module for encoding elements + +""" + +from gstlal.pipeparts import pipetools + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-wavenc.html">wavenc</a> element to a pipeline with useful default properties +def wav(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Format source into the wav format + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + + References: + [1] wavenc docs: https://gstreamer.freedesktop.org/documentation/wavenc/index.html?gi-language=python + + Returns: + Element, the src encoded as wav format + """ + return pipetools.make_element_with_src(pipeline, src, "wavenc") + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-vorbisenc.html">vorbisenc</a> element to a pipeline with useful default properties +def vorbis(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """This element encodes raw float audio into a Vorbis stream + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] vorbisenc docs: https://gstreamer.freedesktop.org/documentation/vorbis/vorbisenc.html?gi-language=python + + Returns: + Element, the source encoded as a Vorbis stream + """ + return pipetools.make_element_with_src(pipeline, src, "vorbisenc") + + +def theora(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element: + """This element encodes raw video into a Theora stream + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + dict, keyword arguments to be set as element properties + + References: + [1] theoraenc docs: https://gstreamer.freedesktop.org/documentation/theora/theoraenc.html?gi-language=python + + Returns: + Element, the source encoded as a Theora stream + """ + return pipetools.make_element_with_src(pipeline, src, "theoraenc", **properties) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-flacenc.html">flacenc</a> element to a pipeline with useful default properties +def flac(pipeline: pipetools.Pipeline, src: pipetools.Element, quality=0, **properties): + """Encoded sources as FLAC streams. FLAC is a Free Lossless Audio Codec. + FLAC audio can directly be written into a file, or embedded into containers such as oggmux or matroskamux. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + quality: + int + **properties: + dict, keyword arguments to be set as element properties + + References: + [1] https://gstreamer.freedesktop.org/documentation/flac/flacenc.html?gi-language=python + + Returns: + Element, the source encoded as a FLAC stream + """ + return pipetools.make_element_with_src(pipeline, src, "flacenc", quality=quality, **properties) + + +def igwd_parse(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Parse byte streams into whole IGWD frame files (https://dcc.ligo.org/cgi-bin/DocDB/ShowDocument?docid=329) + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + Implementation gstlal/gstlal-ugly/gst/framecpp/framecpp_igwdparse.cc + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "framecpp_igwdparse", **properties) + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-uridecodebin.html">uridecodebin</a> element to a pipeline with useful default properties +def uri_decode_bin(pipeline: pipetools.Pipeline, uri: str, caps: pipetools.Caps = "application/x-igwd-frame,framed=true", **properties) -> pipetools.Element: + """Decodes data from a URI into raw media. It selects a source element that can handle the + given scheme and connects it to a decodebin. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + uri: + str, URI to decode + caps: + Gst.Caps, The caps on which to stop decoding. (NULL = default) + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/playback/uridecodebin.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, None, "uridecodebin", uri=uri, caps=None if caps is None else pipetools.Gst.Caps.from_string(caps), **properties) diff --git a/gstlal/python/pipeparts/filters.py b/gstlal/python/pipeparts/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..5f4ba774052776e9f88ade45ff215ddeae44b941 --- /dev/null +++ b/gstlal/python/pipeparts/filters.py @@ -0,0 +1,297 @@ +"""Module for filter elements + +""" +from typing import Union + +import gi + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) + +from gstlal import gsttools +from gstlal.pipeparts import pipetools + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiochebband.html">audiochebband</a> element to a pipeline with useful default properties +def audio_cheb_band(pipeline: pipetools.Pipeline, src: pipetools.Element, lower_frequency: float, upper_frequency: float, poles: int = 8) -> pipetools.Element: + """Attenuates all frequencies outside (bandpass) or inside (bandreject) of a frequency band. The number + of poles and the ripple parameter control the rolloff. This element has the advantage over the windowed + sinc bandpass and bandreject filter that it is much faster and produces almost as good results. It's only + disadvantages are the highly non-linear phase and the slower rolloff compared to a windowed sinc filter + with a large kernel. For type 1 the ripple parameter specifies how much ripple in dB is allowed in the + passband, i.e. some frequencies in the passband will be amplified by that value. A higher ripple value + will allow a faster rolloff. For type 2 the ripple parameter specifies the stopband attenuation. In the + stopband the gain will be at most this value. A lower ripple value will allow a faster rolloff. As a + special case, a Chebyshev type 1 filter with no ripple is a Butterworth filter. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + lower_frequency: + float, Start frequency of the band (Hz) + upper_frequency: + float, Stop frequency of the band (Hz) + poles: + int, Number of poles to use, will be rounded up to the next multiple of four + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiofx/audiochebband.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "audiochebband", lower_frequency=lower_frequency, upper_frequency=upper_frequency, poles=poles) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiocheblimit.html">audiocheblimit</a> element to a pipeline with useful default properties +def audio_cheb_limit(pipeline: pipetools.Pipeline, src: pipetools.Element, cutoff: float, mode: int = 0, poles: int = 8, type: int = 1, ripple: float = 0.25) -> pipetools.Element: + """Attenuates all frequencies above the cutoff frequency (low-pass) or all frequencies below the cutoff frequency (high-pass). + The number of poles and the ripple parameter control the rolloff. This element has the advantage over the windowed sinc lowpass + and highpass filter that it is much faster and produces almost as good results. It's only disadvantages are the highly non-linear + phase and the slower rolloff compared to a windowed sinc filter with a large kernel. For type 1 the ripple parameter specifies + how much ripple in dB is allowed in the passband, i.e. some frequencies in the passband will be amplified by that value. A higher + ripple value will allow a faster rolloff. For type 2 the ripple parameter specifies the stopband attenuation. In the stopband the + gain will be at most this value. A lower ripple value will allow a faster rolloff. As a special case, a Chebyshev type 1 filter + with no ripple is a Butterworth filter. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + cutoff: + float, Cut off frequency (Hz) + mode: + int, default 0, 0 for low-pass or 1 for high-pass + poles: + int, Number of poles to use, will be rounded up to the next multiple of four + type: + int, default 1, Type of the chebychev filter + ripple: + float, default 0,25, Amount of ripple (dB) + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiofx/audiocheblimit.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "audiocheblimit", cutoff=cutoff, mode=mode, poles=poles, type=type, ripple=ripple) + + +## Adds a <a href="@gstdoc/gstreamer-plugins-capsfilter.html">capsfilter</a> element to a pipeline with useful default properties +def caps(pipeline: pipetools.Pipeline, src: pipetools.Element, caps: Union[str, pipetools.Caps], **properties: dict) -> pipetools.Element: + """The element does not modify data as such, but can enforce limitations on the data format. + Note: this element does *not* act as a filter on the data of the source, but rather as a filter on the + metadata (CAPS) of the source element. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + caps: + str or Gst.Caps, the capabilities specification to limit the source data format + **properties: + dict, keyword arguments to be set as element properties + + References: + [1] capsfilter docs: https://gstreamer.freedesktop.org/documentation/coreelements/capsfilter.html?gi-language=python + + Returns: + Element, the source element limited by the given caps (capabilities) + """ + return pipetools.make_element_with_src(pipeline, src, "capsfilter", caps=gsttools.to_caps(caps), **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties +def drop(pipeline: pipetools.Pipeline, src: pipetools.Element, drop_samples: int = 0) -> pipetools.Element: + """Drop samples from the start of a stream + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + srcs: + Iterable[Gst.Element], the source elements + drop_samples: + int, default 0, number of samples to drop from the beginning of a stream + + References: + Implementation: gstlal/gst/lal/gstlal_drop.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_drop", drop_samples=drop_samples) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiofirfilter.html">audiofirfilter</a> element to a pipeline with useful default properties +def fir(pipeline: pipetools.Pipeline, src: pipetools.Element, kernel: pipetools.GValueArray, latency, **properties: dict) -> pipetools.Element: + """Generic audio FIR filter. Before usage the "kernel" property has to be set to the filter kernel that should be + used and the "latency" property has to be set to the latency (in samples) that is introduced by the filter kernel. + Setting a latency of n samples will lead to the first n samples being dropped from the output and n samples added + to the end. + + The filter kernel describes the impulse response of the filter. To calculate the frequency response of the filter + you have to calculate the Fourier Transform of the impulse response. + + To change the filter kernel whenever the sampling rate changes the "rate-changed" signal can be used. This should + be done for most FIR filters as they're depending on the sampling rate. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + kernel: + Gst.GValueArray, filter kernel for the FIR filter + latency: + int, filter latency in samples + **properties: + dict, keyword arguments to be set as element properties + + References: + [1] audiofirfilter docs: https://gstreamer.freedesktop.org/documentation/audiofx/audiofirfilter.html?gi-language=python + + Returns: + Element, the FIR element + """ + properties.update((name, val) for name, val in (("kernel", kernel), ("latency", latency)) if val is not None) + return pipetools.make_element_with_src(pipeline, src, "audiofirfilter", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALGate.html">lal_gate</a> element to a pipeline with useful default properties +def gate(pipeline: pipetools.Pipeline, src: pipetools.Element, threshold: float = None, control: pipetools.Element = None, **properties) -> pipetools.Element: + """Flag buffers as gaps based on the value of a control input + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + threshold: + float, default None, uutput will be flagged as non-gap when magnitude of control input is >= this value. See also invert-control. + control: + Element, optional control element input + **properties: + emit_signals: + bool, Emit start and stop signals (rate-changed is always emited). The start and stop signals + are emited on gap-to-non-gap and non-gap-to-gap transitions in the output stream respectively. + + References: + Implementation: gstlal/gst/lal/gstlal_gate.c + + Returns: + Element + """ + if threshold is not None: + elem = pipetools.make_element_with_src(pipeline, None, "lal_gate", threshold=threshold, **properties) + else: + elem = pipetools.make_element_with_src(pipeline, None, "lal_gate", **properties) + for peer, padname in ((src, "sink"), (control, "control")): + if isinstance(peer, Gst.Pad): + peer.get_parent_element().link_pads(peer, elem, padname) + elif peer is not None: + peer.link_pads(None, elem, padname) + return elem + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioiirfilter.html">audioiirfilter</a> element to a pipeline with useful default properties +def iir(pipeline: pipetools.Pipeline, src: pipetools.Element, a: pipetools.ValueArray, b: pipetools.ValueArray) -> pipetools.Element: + """aGeneric audio IIR filter. Before usage the "a" and "b" properties have to be set to the filter coefficients + that should be used. + + The filter coefficients describe the numerator and denominator of the transfer function. + + To change the filter coefficients whenever the sampling rate changes the "rate-changed" signal can be used. + This should be done for most IIR filters as they're depending on the sampling rate. + + convention is z = \exp(-i 2 \pi f / f_{\rm sampling}) + H(z) = (\sum_{j=0}^{N} a_j z^{-j}) / (\sum_{j=0}^{N} (-1)^{j} b_j z^{-j}) + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + a: + ValueArray, Filter coefficients (denominator of transfer function) + b: + ValueArray, Filter coefficients (numerator of transfer function) + + References: + [1] audioiirfilter docs: https://gstreamer.freedesktop.org/documentation/audiofx/audioiirfilter.html?gi-language=python + + Returns: + Element, IIR of the sources + """ + # + return pipetools.make_element_with_src(pipeline, src, "audioiirfilter", a=a, b=b) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALNoFakeDisconts.html">lal_nofakedisconts</a> element to a pipeline with useful default properties +def remove_fake_disconts(pipeline: pipetools.Pipeline, src: pipetools.Element, silent: bool = True) -> pipetools.Element: + """Fix incorrectly-set discontinuity flags + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + srcs: + Iterable[Gst.Element], the source elements + silent: + bool, default True, if True Don't print a message when alterning the flags in a buffer. + + References: + Implementation: gstal/gst/lal/gstlal_nofakedisconts.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_nofakedisconts", silent=silent) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALStateVector.html">lal_statevector</a> element to a pipeline with useful default properties +def state_vector(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Converts a state vector stream into booleans, for example to drive a lal_gate element. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + Implementation: gstlal/gst/lal/gstlal_statevector.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_statevector", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALSimulation.html">lal_simulation</a> element to a pipeline with useful default properties +def inject(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str) -> pipetools.Element: + """An injection routine calling lalsimulation waveform generators + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + filename: + str, path to xml file Name of LIGO Light Weight XML file containing list(s) of software injections + + References: + Implementation: gstlal/gst/lal/gstlal_simulation.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_simulation", xml_location=filename) diff --git a/gstlal/python/pipeparts/mux.py b/gstlal/python/pipeparts/mux.py new file mode 100644 index 0000000000000000000000000000000000000000..3c4043e55ab61bd7af626e26070bd9304c6cdc12 --- /dev/null +++ b/gstlal/python/pipeparts/mux.py @@ -0,0 +1,230 @@ +"""Module for multiplexing (mux) and demultiplexing (demux) elements + +""" + +import gi + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) + +from ligo import segments +from lal import iterutils +from lal import LIGOTimeGPS +from gstlal.pipeparts import pipetools + + +class FrameCPPChannelDemuxSetUnitsHandler(object): + def __init__(self, elem, units_dict): + """ + Connect a handler for the pad-added signal of the + framecpp_channeldemux element elem, and when a pad is added + to the element if the pad's name appears as a key in the + units_dict dictionary that pad's units property will be set + to the string value associated with that key in the + dictionary. + + Example: + + >>> FrameCPPChannelDemuxSetUnitsHandler(elem, {"H1:LSC-STRAIN": "strain"}) # doctest: +SKIP + + NOTE: this is a work-around to address the problem that + most (all?) frame files do not have units set on their + channel data, whereas downstream consumers of the data + might require information about the units. The demuxer + provides the units as part of a tag event, and + framecpp_channeldemux_set_units() can be used to override + the values, thereby correcting absent or incorrect units + information. + """ + self.elem = elem + self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, units_dict) + assert self.pad_added_handler_id > 0 + + @staticmethod + def pad_added(element, pad, units_dict): + name = pad.get_name() + if name in units_dict: + pad.set_property("units", units_dict[name]) + + +class FrameCPPChannelDemuxCheckSegmentsHandler(object): + """ + Utility to watch for missing data. Pad probes are used to collect + the times spanned by buffers, these are compared to a segment list + defining the intervals of data the stream is required to have. If + any intervals of data are found to have been skipped or if EOS is + seen before the end of the segment list then a ValueError exception + is raised. + + There are two ways to use this tool. To directly install a segment + list monitor on a single pad use the .set_probe() class method. + For elements with dynamic pads, the class can be allowed to + automatically add monitors to pads as they become available by + using the element's pad-added signal. In this case initialize an + instance of the class with the element and a dictionary of segment + lists mapping source pad name to the segment list to check that + pad's output against. + + In both cases a jitter parameter sets the maximum size of a skipped + segment that will be ignored (for example, to accomodate round-off + error in element timestamp computations). The default is 1 ns. + """ + + # FIXME: this code now has two conflicting mechanisms for removing + # probes from pads: one code path removes probes when pads get to + # EOS, while the othe removes a probe each time the pad for the + # probe appears a second or subsequent time on an element (and then + # re-installs the probe on the new pad). it's possible that these + # two could attempt to remove the same probe twice, which will + # cause a crash, although it should not happen in current use + # cases. the fix is to rework the probe tracking mechanism so that + # both code paths agree on what probes are installed + def __init__(self, elem, seglists, jitter=LIGOTimeGPS(0, 1)): + self.jitter = jitter + self.probe_handler_ids = {} + # make a copy of the segmentlistdict in case the calling + # code modifies it + self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, seglists.copy()) + assert self.pad_added_handler_id > 0 + + def pad_added(self, element, pad, seglists): + name = pad.get_name() + if name in self.probe_handler_ids: + pad.remove_probe(self.probe_handler_ids.pop(name)) + if name in seglists: + self.probe_handler_ids[name] = self.set_probe(pad, seglists[name], self.jitter) + assert self.probe_handler_ids[name] > 0 + + @classmethod + def set_probe(cls, pad, seglist, jitter=LIGOTimeGPS(0, 1)): + # use a copy of the segmentlist so the probe can modify it + seglist = segments.segmentlist(seglist) + # mutable object to carry data to probe + data = [seglist, jitter, None] + # install probe, save ID in data + probe_id = data[2] = pad.add_probe(Gst.PadProbeType.DATA_DOWNSTREAM, cls.probe, data) + return probe_id + + @staticmethod + def probe(pad, probeinfo, seg_jitter_id): + seglist, jitter, probe_id = seg_jitter_id + if probeinfo.type & Gst.PadProbeType.BUFFER: + obj = probeinfo.get_buffer() + if not obj.mini_object.flags & Gst.BufferFlags.GAP: + # remove the current buffer from the data + # we're expecting to see + seglist -= segments.segmentlist([segments.segment((LIGOTimeGPS(0, obj.pts), LIGOTimeGPS(0, obj.pts + obj.duration)))]) + # ignore missing data intervals unless + # they're bigger than the jitter + iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist) + # are we still expecting to see something that + # precedes the current buffer? + preceding = segments.segment((segments.NegInfinity, LIGOTimeGPS(0, obj.pts))) + if seglist.intersects_segment(preceding): + raise ValueError("%s: detected missing data: %s" % (pad.get_name(), seglist & segments.segmentlist([preceding]))) + elif probeinfo.type & Gst.PadProbeType.EVENT_DOWNSTREAM and probeinfo.get_event().type == Gst.EventType.EOS: + # detach probe at EOS + pad.remove_probe(probe_id) + # ignore missing data intervals unless they're + # bigger than the jitter + iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist) + if seglist: + raise ValueError("%s: at EOS detected missing data: %s" % (pad.get_name(), seglist)) + return True + + +def framecpp_channel_demux(pipeline, src, **properties): + """Demux src using framecpp + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + dict, keyword arguments to be set as element properties + + References: + [1] framecppdemux implementation: gstlal/gstlal-ugly/gst/framecpp/framecpp_channeldemux.cc + + Returns: + Element, src demuxed using framecpp + """ + return pipetools.make_element_with_src(pipeline, src, "framecpp_channeldemux", **properties) + + +def framecpp_channel_mux(pipeline, channel_src_map, units=None, seglists=None, **properties): + """Mux a source using framecpp + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + channel_src_map: + dict, mapping a channel -> src element + units: + str, default None, if given set these units on source + seglists: + default None, if given create a segments handler for these segments + **properties: + + Returns: + Element, the muxed sources + """ + elem = pipetools.make_element_with_src(pipeline, None, "framecpp_channelmux", **properties) + if channel_src_map is not None: + for channel, src in channel_src_map.items(): + for srcpad in src.srcpads: + # FIXME FIXME FIXME. This should use the pad template from the element. + # FIXME once a newer version of some library is available, then we should be able to switch to this + # if srcpad.link(elem.get_request_pad(channel)) == Gst.PadLinkReturn.OK + # Instead. Right now it fails due to the + # underscore in channel names. When it fails + # it fails silently and returns None, which + # gives a cryptic error message + if srcpad.link(elem.request_pad(Gst.PadTemplate.new(channel, Gst.PadDirection.SINK, Gst.PadPresence.REQUEST, Gst.Caps("ANY")), channel)) == Gst.PadLinkReturn.OK: + break + if units is not None: + FrameCPPChannelDemuxSetUnitsHandler(elem, units) + if seglists is not None: + FrameCPPChannelDemuxCheckSegmentsHandler(elem, seglists) + return elem + + +def ogg_mux(pipeline, src): + """This element merges streams (audio and video) into ogg files. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] oggmux docs: https://gstreamer.freedesktop.org/documentation/ogg/oggmux.html?gi-language=python + + Returns: + Element, the source merged as ogg format + """ + return pipetools.make_element_with_src(pipeline, src, "oggmux") + + +def avi_mux(pipeline, src): + """Muxes raw or compressed audio and/or video streams into an AVI file. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] avimux docs: https://gstreamer.freedesktop.org/documentation/avi/avimux.html?gi-language=python + + Returns: + Element, the source merged as avi + """ + return pipetools.make_element_with_src(pipeline, src, "avimux") diff --git a/gstlal/python/pipeparts/pipedot.py b/gstlal/python/pipeparts/pipedot.py new file mode 100644 index 0000000000000000000000000000000000000000..1054c8033fbd47659cadcf59d5a4943525b7f577 --- /dev/null +++ b/gstlal/python/pipeparts/pipedot.py @@ -0,0 +1,77 @@ +"""Utilities for converting Gst pipelines into DOT graphs +""" +import os +import pathlib +import sys + +import gi + +from gstlal.pipeparts import pipetools + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) + +ENV_VAR_DUMP_DIR = 'GST_DEBUG_DUMP_DOT_DIR' + + +def to_file(pipeline: pipetools.Pipeline, filestem: str, verbose: bool = False, use_str_method: bool = False): + """Write a pipeline out to a dot file. This function needs the environment variable GST_DEBUG_DUMP_DOT_DIR + to be set. + + Args: + pipeline: + filestem: + str, name of file (not including extension) + verbose: + bool, default False, if True a message will be written to stderr. + use_str_method: + bool, default False, if True use the "to_str" method as an intermediate step. This is enabled due to + some bugs in Gst where no files are written out using the direct "debug_bin_to_dot_file". + + Notes: + File Output: + The filename will be os.path.join($GST_DEBUG_DUMP_DOT_DIR, filestem + ".dot") + + Raises: + ValueError: + If "GST_DEBUG_DUMP_DOT_DIR" env var not defined + + References: + [1] https://lazka.github.io/pgi-docs/Gst-1.0/functions.html#Gst.debug_bin_to_dot_file + + Returns: + None + """ + if ENV_VAR_DUMP_DIR not in os.environ: + raise ValueError("cannot write pipeline, environment variable GST_DEBUG_DUMP_DOT_DIR is not set") + + output_path = pathlib.Path(os.environ[ENV_VAR_DUMP_DIR]) / '{}.dot'.format(filestem) + + if use_str_method: + dot_str = to_str(pipeline) + with open(output_path.as_posix(), 'w') as fid: + fid.write(dot_str) + else: + Gst.debug_bin_to_dot_file(pipeline, Gst.DebugGraphDetails.ALL, filestem) + if verbose: + print("Wrote pipeline to {}".format(output_path.as_posix()), file=sys.stderr) + + +def to_str(pipeline: pipetools.Pipeline) -> str: + """Convert a pipeline to a DOT-formatted string directly (no out file intermediary) + + Args: + pipeline: + Pipeline, the pipeline to convert to DOT string + + References: + [1] https://gstreamer.freedesktop.org/documentation/gstreamer/debugutils.html?gi-language=python + + Returns: + str, the pipeline converted to DOT formatted string + """ + return Gst.debug_bin_to_dot_data(pipeline, Gst.DebugGraphDetails.ALL) diff --git a/gstlal/python/pipeparts/pipetools.py b/gstlal/python/pipeparts/pipetools.py new file mode 100644 index 0000000000000000000000000000000000000000..0e535ffb331021d804e91f3ec1b3df6c7af6359b --- /dev/null +++ b/gstlal/python/pipeparts/pipetools.py @@ -0,0 +1,94 @@ +"""Utilities for constructing pipeline elements + +""" +import functools +import inspect +from typing import Union, Optional + +import gi +import ligo.segments +from lal import LIGOTimeGPS + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +from gstlal import gsttools + +GObject.threads_init() +Gst.init(None) + +PROPERTY_BUILTIN_NAMES = ('async') + +# The below are used for type annotations +Pipeline = Gst.Pipeline +Element = Gst.Element +Caps = Gst.Caps +GValueArray = Gst.ValueArray # Verify this type +ValueArray = GObject.ValueArray +Segment = ligo.segments.segment +TimeGPS = LIGOTimeGPS + + +def clean_property_name(name: str): + """Utility function for cleaning a property name, handles two cases: + + 1. Underscores to hyphens: property_name -> property-name + 2. Removes trailing underscores originally used to avoid collisions with builtin: math_ -> math. + The specific list of builtin collisions detected is defined in PROPERTY_BUILTIN_NAMES + + Args: + name: + str, the python property name + + Returns: + str, the formatted gstreamer compatible element property name + """ + if name.endswith('_') and name[:-1] in PROPERTY_BUILTIN_NAMES: + return name[:-1] + return name.replace('_', '-') + + +def make_element_with_src(pipeline: Gst.Pipeline, src: Union[Gst.Element, Gst.Pad], elem_type_name: str, **properties): + """Create a new element of the type defined by the given element factory. If name is None, then the + element will receive a guaranteed unique name, consisting of the element factory name and a number. + If name is given, it will be given the name supplied. + + Args: + pipeline: + Pipeline, the existing pipeline to which the new element will be added + src: + Element or Pad, the element or pad to use as a source for the new element + elem_type_name: + str or None, name of new element, or None to automatically create a unique name + **properties: + dict, keyword arguments to be set as properties on the new element + + References: + [1] https://lazka.github.io/pgi-docs/Gst-1.0/classes/ElementFactory.html#Gst.ElementFactory.make + + Raises: + RuntimeError: + If any problem occurs creating the element. + + Returns: + Element, the new element + """ + # Determine element name + name = properties.pop('name') if 'name' in properties else None + + properties = {clean_property_name(key): value for key, value in properties.items()} + + # Make the element + elem = gsttools.make_element(elem_type_name, name, **properties) + + # Add element to pipeline + pipeline.add(elem) + + # Link src argument as input to element + if gsttools.is_pad(src): + src.get_parent_element().link_pads(src, elem, None) + elif src is not None: + src.link(elem) + + return elem diff --git a/gstlal/python/pipeparts/plot.py b/gstlal/python/pipeparts/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..983d55d6436363652cb640f0489fe3abc4ad83c2 --- /dev/null +++ b/gstlal/python/pipeparts/plot.py @@ -0,0 +1,75 @@ +"""Module for producing plotting elements + +""" + +from gstlal.pipeparts import pipetools + + +def channelgram(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element: + """Scrolling channel amplitude plot, generates video showing a scrolling plot of channel amplitudes. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + dict, keyword arguments to be set as element properties + + Notes: + Available Properties: + plot_width: + float, default 0.0, width of the plot in seconds. (0 = 1/framerate) + + References: + [1] ChannelGram Implementation: gstlal/gstlal/gst/python/lal_channelgram.py + + Returns: + Element, the plot element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_channelgram", **properties) + + +def spectrum(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element: + """Power spectrum plot, generates a video showing a power spectrum (e.g., as measured by lal_whiten). + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + dict, keyword arguments to be set as element properties + + Notes: + Available properties: + f_min: + float, default 10.0, Lower bound of plot in Hz + f_max: + float, default 4000.0, Upper bound of plot in Hz + + References: + [1] SpectrumPlot Implementation: gstlal/gstlal/gst/python/lal_spectrumplot.py + + Returns: + Element, the plot element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_spectrumplot", **properties) + + +def histogram(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Histogram plot, generates a video showing a histogram of the input time series. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] HistogramPlot Implementation: gstlal/gstlal/gst/python/lal_histogramplot.py + + Returns: + Element, the plot element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_histogramplot") diff --git a/gstlal/python/pipeparts/sink.py b/gstlal/python/pipeparts/sink.py new file mode 100644 index 0000000000000000000000000000000000000000..28d18c28f97a6790f85ebd667ca4c45dfdc5a356 --- /dev/null +++ b/gstlal/python/pipeparts/sink.py @@ -0,0 +1,512 @@ +""""Module for producing sink elements + +""" +import math +import os +import sys +import threading +from typing import Tuple + +import numpy +from lal import LIGOTimeGPS, CacheEntry +from ligo import segments + +import gi + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) + +from gstlal.pipeparts import pipetools, pipedot, mux, encode, filters, transform + +BYTE_ORDER = 'LE' if sys.byteorder == "little" else 'BE' + + +def framecpp_filesink_ldas_path_handler(elem: pipetools.Element, pspec, path_digits: Tuple[str, int]): + """Add path for file sink to element + + Args: + elem: + Element, the element to which to add a filesink path property + pspec: + Unknown + path_digits: + Tuple[str, int], a string outpath and a directory digits int + + Examples: + >>> filesinkelem.connect("notify::timestamp", framecpp_filesink_ldas_path_handler, (".", 5)) + + Returns: + Element, with the formatted outpath attached as the "path" property + """ + outpath, dir_digits = path_digits + + # get timestamp and truncate to integer seconds + timestamp = elem.get_property("timestamp") // Gst.SECOND + + # extract leading digits + leading_digits = timestamp // 10 ** int(math.log10(timestamp) + 1 - dir_digits) + + # get other metadata + instrument = elem.get_property("instrument") + frame_type = elem.get_property("frame-type") + + # make target directory, and set path + path = os.path.join(outpath, "%s-%s-%d" % (instrument, frame_type, leading_digits)) + if not os.path.exists(path): + os.makedirs(path) + elem.set_property("path", path) + + +def framecpp_filesink_cache_entry_from_mfs_message(message): + """Translate an element message posted by the multifilesink element + inside a framecpp_filesink bin into a lal.utils.CacheEntry object + describing the file being written by the multifilesink element. + """ + # extract the segment spanned by the file from the message directly + start = LIGOTimeGPS(0, message.get_structure()["timestamp"]) + end = start + LIGOTimeGPS(0, message.get_structure()["duration"]) + + # retrieve the framecpp_filesink bin (for instrument/observatory + # and frame file type) + parent = message.src.get_parent() + + # construct and return a CacheEntry object + return CacheEntry(parent.get_property("instrument"), parent.get_property("frame-type"), segments.segment(start, end), + "file://localhost%s" % os.path.abspath(message.get_structure()["filename"])) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-multifilesink.html">multifilesink</a> element to a pipeline with useful default properties +def multi_file(pipeline: pipetools.Pipeline, src: pipetools.Element, next_file: int = 0, sync: bool = False, async_: bool = False, **properties) -> pipetools.Element: + """Adds a sink element to a pipeline with useful default properties + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + next_file: + int, default 0 + sync: + bool, default False + async_: + bool, default False + **properties: + + Returns: + Element + """ + properties["async"] = async_ + return pipetools.make_element_with_src(pipeline, src, "multifilesink", next_file=next_file, sync=sync, **properties) + + +def gwf(pipeline: pipetools.Pipeline, src: pipetools.Element, message_forward: bool = True, **properties) -> pipetools.Element: + """Add a framecpp file sink element to pipeline, that will write out a GWF file + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + message_forward: + bool, default True + **properties: + + References: + Implementation: gstlal-ugly/gst/framecpp/framecpp_filesink.c + + Returns: + Element + """ + post_messages = properties.pop("post_messages", True) + elem = pipetools.make_element_with_src(pipeline, src, "framecpp_filesink", message_forward=message_forward, **properties) + # FIXME: there's supposed to be some sort of proxy mechanism for + # setting properties on child elements, but we can't seem to get + # anything to work + elem.get_by_name("multifilesink").set_property("post-messages", post_messages) + return elem + + +## Adds a <a href="@gstdoc/gstreamer-plugins-fakesink.html">fakesink</a> element to a pipeline with useful default properties +def fake(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Create a fake sink element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "fakesink", sync=False, **{"async": False}) + + +## Adds a <a href="@gstdoc/gstreamer-plugins-filesink.html">filesink</a> element to a pipeline with useful default properties +def file(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str, sync: bool = False, async_: bool = False) -> pipetools.Element: + """Add file sink to pipeline + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + filename: + str, the name of the output file + sync: + bool, default False + async_: + bool, default False + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "filesink", sync=sync, buffer_mode=2, location=filename, **{"async": async_}) + + +## Adds a <a href="@gstlalgtkdoc/GstTSVEnc.html">lal_nxydump</a> element to a pipeline with useful default properties +def tsv(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str, segment: pipetools.Segment = None) -> pipetools.Element: + """Converts audio time-series to tab-separated ascii text, a format compatible with most plotting utilities. + The output is multi-column tab-separated ASCII text. The first column is the time, the remaining columns are + the values of the channels in order. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + filename: + str, the filename of the output text file + segment: + Segment, default None, a ligo.segments.segment + + Returns: + Element + """ + if segment is not None: + elem = pipetools.make_element_with_src(pipeline, src, "lal_nxydump", start_time=segment[0].ns(), stop_time=segment[1].ns()) + else: + elem = pipetools.make_element_with_src(pipeline, src, "lal_nxydump") + return file(pipeline, elem, filename) + + +def ogm_video(pipeline: pipetools.Pipeline, videosrc: pipetools.Element, filename: str, audiosrc: pipetools.Element = None, verbose: bool = False): + """Make a ogm video sink element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + videosrc: + Gst.Element, the video source element + filename: + str, the name of the output video file + audiosrc: + Gst.Element, default None, the audio source element + verbose: + bool, default False + + Returns: + Element, the sink element + """ + src = transform.colorspace(pipeline, videosrc) + src = filters.caps(pipeline, src, "video/x-raw-yuv, format=(fourcc)I420") + src = encode.theora(pipeline, src, + # border=2, + quality=48, + # quick=False + ) + src = mux.ogg_mux(pipeline, src) + if audiosrc is not None: + encode.flac(pipeline, filters.caps(pipeline, transform.audio_convert(pipeline, audiosrc), "audio/x-raw, format=S24%s" % BYTE_ORDER)).link(src) + if verbose: + src = progress_report(pipeline, src, filename) + return file(pipeline, src, filename) + + +def auto_video(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Create a video sink that automatically detects an appropriate video sink to use. It does so by scanning the + registry for all elements that have "Sink" and "Video" in the class field of their element information, and + also have a non-zero autoplugging rank. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] https://gstreamer.freedesktop.org/documentation/autodetect/autovideosink.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, transform.colorspace(pipeline, src), "autovideosink") + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-autoaudiosink.html">autoaudiosink</a> element to a pipeline with useful default properties +def auto_audio(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Create an audio sink that automatically detects an appropriate audio sink to use. It does so by + scanning the registry for all elements that have "Sink" and "Audio" in the class field of their element + information, and also have a non-zero autoplugging rank. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] https://gstreamer.freedesktop.org/documentation/autodetect/autoaudiosink.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, transform.queue(pipeline, src), "autoaudiosink") + + +def playback(pipeline: pipetools.Pipeline, src: pipetools.Element, amplification: float = 0.1) -> pipetools.Element: + """Create a playback pipeline and add it to existing pipeline + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + amplification: + float, default 0.1 + + Returns: + Element + """ + elems = ( + Gst.ElementFactory.make("audioconvert", None), + Gst.ElementFactory.make("capsfilter", None), + Gst.ElementFactory.make("audioamplify", None), + Gst.ElementFactory.make("audioconvert", None), + Gst.ElementFactory.make("queue", None), + Gst.ElementFactory.make("autoaudiosink", None) + ) + elems[1].set_property("caps", Gst.Caps.from_string("audio/x-raw, format=F32%s" % BYTE_ORDER)) + elems[2].set_property("amplification", amplification) + elems[4].set_property("max-size-time", 1 * Gst.SECOND) + pipeline.add(*elems) + return Gst.element_link_many(src, *elems) # MOD: Error line [733]: element_link_many not yet implemented. See web page ** + + +def tsv_tee(pipeline: pipetools.Pipeline, src: pipetools.Element, *args, **properties) -> pipetools.Element: + """Split data from source to an nxy dump + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + *args: + **properties: + + Returns: + Element + """ + t = transform.tee(pipeline, src) + tsv(pipeline, transform.queue(pipeline, t), *args, **properties) + return t + + +def trigger_xml_writer(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str): + """Write xml file + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + filename: + str, output path for xml file + + References: + Implementation + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_triggerxmlwriter", location=filename, sync=False, **{"async": False}) + + +# FIXME no specific alias for this url since this library only has one element. +# DO NOT DOCUMENT OTHER CODES THIS WAY! Use @gstdoc @gstpluginsbasedoc etc. +## Adds a <a href="http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-libs/html/gstreamer-app.html">appsink</a> element to a pipeline with useful default properties +def app(pipeline: pipetools.Pipeline, src: pipetools.Element, max_buffers: int = 1, drop: bool = False, sync: bool = False, async_: bool = False, **properties): + """Create an app sink, Appsink is a sink plugin that supports many different methods for making the + application get a handle on the GStreamer data in a pipeline. Unlike most GStreamer elements, + Appsink provides external API functions. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + max_buffers: + int, default 1 + drop: + bool, default False + sync: + bool, default False + async_: + bool, default False + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/app/appsink.html?gi-language=python + + Returns: + Element + """ + properties["async"] = async_ + return pipetools.make_element_with_src(pipeline, src, "appsink", sync=sync, emit_signals=True, max_buffers=max_buffers, drop=drop, **properties) + + +class AppSync(object): + def __init__(self, appsink_new_buffer, appsinks=[]): + self.lock = threading.Lock() + # handler to invoke on availability of new time-ordered + # buffer + self.appsink_new_buffer = appsink_new_buffer + # element --> timestamp of current buffer or None if no + # buffer yet available + self.appsinks = {} + # set of sink elements that are currently at EOS + self.at_eos = set() + # attach handlers to appsink elements provided at this time + for elem in appsinks: + self.attach(elem) + + def add_sink(self, pipeline, src, drop=False, **properties): + return self.attach(app(pipeline, src, drop=drop, **properties)) + + def attach(self, appsink): + """ + connect this AppSync's signal handlers to the given appsink + element. the element's max-buffers property will be set to + 1 (required for AppSync to work). + """ + if appsink in self.appsinks: + raise ValueError("duplicate appsinks %s" % repr(appsink)) + appsink.set_property("max-buffers", 1) + handler_id = appsink.connect("new-preroll", self.new_preroll_handler) + assert handler_id > 0 + handler_id = appsink.connect("new-sample", self.new_sample_handler) + assert handler_id > 0 + handler_id = appsink.connect("eos", self.eos_handler) + assert handler_id > 0 + self.appsinks[appsink] = None + return appsink + + def new_preroll_handler(self, elem): + with self.lock: + # clear eos status + self.at_eos.discard(elem) + # ignore preroll buffers + elem.emit("pull-preroll") + return Gst.FlowReturn.OK + + def new_sample_handler(self, elem): + with self.lock: + # clear eos status, and retrieve buffer timestamp + self.at_eos.discard(elem) + assert self.appsinks[elem] is None + self.appsinks[elem] = elem.get_last_sample().get_buffer().pts + # pull available buffers from appsink elements + return self.pull_buffers(elem) + + def eos_handler(self, elem): + with self.lock: + # set eos status + self.at_eos.add(elem) + # pull available buffers from appsink elements + return self.pull_buffers(elem) + + def pull_buffers(self, elem): + """ + for internal use. must be called with lock held. + """ + # keep looping while we can process buffers + while 1: + # retrieve the timestamps of all elements that + # aren't at eos and all elements at eos that still + # have buffers in them + timestamps = [(t, e) for e, t in self.appsinks.items() if e not in self.at_eos or t is not None] + # if all elements are at eos and none have buffers, + # then we're at eos + if not timestamps: + return Gst.FlowReturn.EOS + # find the element with the oldest timestamp. None + # compares as less than everything, so we'll find + # any element (that isn't at eos) that doesn't yet + # have a buffer (elements at eos and that are + # without buffers aren't in the list) + timestamp, elem_with_oldest = min(timestamps, key=lambda x: x[0] if x[0] is not None else -numpy.inf) + # if there's an element without a buffer, quit for + # now --- we require all non-eos elements to have + # buffers before proceding + if timestamp is None: + return Gst.FlowReturn.OK + # clear timestamp and pass element to handler func. + # function call is done last so that all of our + # book-keeping has been taken care of in case an + # exception gets raised + self.appsinks[elem_with_oldest] = None + self.appsink_new_buffer(elem_with_oldest) + + +class ConnectAppsinkDumpDot(object): + """Add a signal handler to write a pipeline graph upon receipt of the + first trigger buffer. the caps in the pipeline graph are not fully + negotiated until data comes out the end, so this version of the graph + shows the final formats on all links + """ + + def __init__(self, pipeline, appsinks, basename, verbose=False): + self.pipeline = pipeline + self.filestem = "%s.%s" % (basename, "TRIGGERS") + self.verbose = verbose + # map element to handler ID + self.remaining_lock = threading.Lock() + self.remaining = {} + for sink in appsinks: + self.remaining[sink] = sink.connect_after("new-preroll", self.execute) + assert self.remaining[sink] > 0 + + def execute(self, elem): + with self.remaining_lock: + handler_id = self.remaining.pop(elem) + if not self.remaining: + pipedot.write_dump_dot(self.pipeline, self.filestem, verbose=self.verbose) + elem.disconnect(handler_id) + return Gst.FlowReturn.OK + + +def tcp_server(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Create a sink via TCP server + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/tcp/tcpserversink.html?gi-language=python + + Returns: + Element + """ + # units_soft_max = 1 GB + # FIXME: are these sensible defaults? + return pipetools.make_element_with_src(pipeline, src, "tcpserversink", sync=True, sync_method="latest-keyframe", recover_policy="keyframe", unit_type="bytes", + units_soft_max=1024 ** 3, + **properties) diff --git a/gstlal/python/pipeparts/source.py b/gstlal/python/pipeparts/source.py new file mode 100644 index 0000000000000000000000000000000000000000..71cf70cc334e0cb836288bc91c50786bec97c7ca --- /dev/null +++ b/gstlal/python/pipeparts/source.py @@ -0,0 +1,357 @@ +"""Module for producing source elements + +""" +import sys +from typing import List, Tuple + +from ligo import segments + +from gstlal.pipeparts import pipetools, transform, filters + +BYTE_ORDER = 'LE' if sys.byteorder == "little" else 'BE' + + +class AudioTestWaveform: + """Enumeration of test waveforms + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiotestsrc/index.html?gi-language=python#GstAudioTestSrcWave + """ + Sine = 0 + Square = 1 + Saw = 2 + Triangle = 3 + Silence = 4 + WhiteNoise = 5 + PinkNoise = 6 + SineTable = 7 + Ticks = 8 + GaussianNoise = 9 + RedNoise = 10 + BlueNoise = 11 + VioletNoise = 12 + + +class NDSChannelType: + """Enumeration of NDS channel types + + References: + Implementation: gstlal-ugly/gst/nds/ndssrc.c + """ + Unknown = 'unknown' + Online = 'online' + Raw = 'raw' + Reduced = 'reduced' + SecondTrend = 's-trend' + MinuteTrend = 'm-trend' + TestPoint = 'test-pt' + + +class SrcDeferredLink(object): + """A class that manages the task of watching for and connecting to new + source pads by name. The inputs are an element, the name of the + source pad to watch for on that element, and the sink pad (on a + different element) to which the source pad should be linked when it + appears. + + The "pad-added" signal of the element will be used to watch for new + pads, and if the "no-more-pads" signal is emitted by the element + before the requested pad has appeared ValueError is raised. + """ + + def __init__(self, element, srcpadname, sinkpad): + no_more_pads_handler_id = element.connect("no-more-pads", self.no_more_pads, srcpadname) + assert no_more_pads_handler_id > 0 + pad_added_data = [srcpadname, sinkpad, no_more_pads_handler_id] + pad_added_handler_id = element.connect("pad-added", self.pad_added, pad_added_data) + assert pad_added_handler_id > 0 + pad_added_data.append(pad_added_handler_id) + + @staticmethod + def pad_added(element, pad, src_sink_ids): + srcpadname, sinkpad, no_more_pads_handler_id, pad_added_handler_id = src_sink_ids + if pad.get_name() == srcpadname: + element.handler_disconnect(no_more_pads_handler_id) + element.handler_disconnect(pad_added_handler_id) + pad.link(sinkpad) + + @staticmethod + def no_more_pads(element, srcpadname): + raise ValueError("<%s>: no pad named '%s'" % (element.get_name(), srcpadname)) + + +def fake_ligo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element: + """Fake LIGO Source + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + instrument: + str, default None + channel_name: + str, default None + blocksize: + int, default 16384 * 8 * 1, Number of samples in each outgoing buffer + + References: + Implementation gstlal/gst/python/lal_fakeligosrc.py + + Returns: + Element + """ + properties = {"blocksize": blocksize} + properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None) + return pipetools.make_element_with_src(pipeline, None, "lal_fakeligosrc", **properties) + + +def fake_aligo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element: + """Fake Advanced LIGO Source + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + instrument: + str, default None + channel_name: + str, default None + blocksize: + int, default 16384 * 8 * 1, Number of samples in each outgoing buffer + + References: + Implementation gstlal/gst/python/lal_fakeadvligosrc.py + + Returns: + Element + """ + properties = {"blocksize": blocksize} + properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None) + return pipetools.make_element_with_src(pipeline, None, "lal_fakeadvligosrc", **properties) + + +def fake_avirgo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element: + """Fake Advanced Virgo Source + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + instrument: + str, default None + channel_name: + str, default None + blocksize: + int, default 16384 * 8 * 1, Number of samples in each outgoing buffer + + References: + Implementation gstlal/gst/python/lal_fakeadvvirgosrc.py + + Returns: + Element + """ + properties = {"blocksize": blocksize} + if instrument is not None: + properties["instrument"] = instrument + if channel_name is not None: + properties["channel_name"] = channel_name + return pipetools.make_element_with_src(pipeline, None, "lal_fakeadvvirgosrc", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALSegmentSrc.html">lal_segmentsrc</a> element to a pipeline with useful default properties +def segment(pipeline: pipetools.Pipeline, segment_list: List[Tuple[pipetools.TimeGPS, pipetools.TimeGPS]], blocksize: int = 4096 * 1 * 1, + invert_output: bool = False) -> pipetools.Element: + """The output is a buffer of boolean values specifying when a list of segments are on and off. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + segment_list: + Iterable[Tuple[TimeGPS, TimeGPS]], list of segment start / stop times + blocksize: + int, default blocksize is 4096 seconds of unsigned integers at 1 Hz, e.g. segments without nanoseconds + invert_output: + bool, default False, False = output is high in segments (default), True = output is low in segments + + References: + Implementation: gstlal/gst/lal/gstlal_segmentsrc.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, None, "lal_segmentsrc", blocksize=blocksize, + segment_list=segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list), + invert_output=invert_output) + + +## Adds a <a href="@gstlalgtkdoc/GstLALCacheSrc.html">lal_cachesrc</a> element to a pipeline with useful default properties +def cache(pipeline: pipetools.Pipeline, location: str, use_mmap: bool = True, **properties) -> pipetools.Element: + """Retrieve frame files from locations recorded in a LAL cache file. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + location: + str, Path to LAL cache file. + use_mmap: + bool, default True, if True Use mmap() instead of read() + **properties: + + References: + Implementation: gstlal/gst/lal/gstlal_cachesrc.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, None, "lal_cachesrc", location=location, use_mmap=use_mmap, **properties) + + +def lvshm(pipeline: pipetools.Pipeline, shm_name: str, **properties) -> pipetools.Element: + """LIGO-Virgo shared memory frame file source element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + shm_name: + str, Shared memory partition name. Suggestions: LHO_Data, LLO_Data, VIRGO_Data + **properties: + + References: + Implementation: gstlal-ugly/gst/gds/lvshmsrc.cc + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, None, "gds_lvshmsrc", shm_name=shm_name, **properties) + + +def framexmit(pipeline: pipetools.Pipeline, multicast_group: str = '0.0.0.0', port: int = 0, **properties) -> pipetools.Element: + """FrameXMIT based source element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + multicast_group: + str, default "0.0.0.0", The address of multicast group to join. If no multicast address is supplied, the receiver will listen for + UDP/IP broadcast transmissions at the specified port. + port: + int, default 0, The local port on which to receive broadcasts (0 = allocate). These ports can be reused by multiple applications. + **properties: + + References: + Implementation: gstlal-ugly/gst/gds/framexmitsrc.cc + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, None, "gds_framexmitsrc", multicast_group=multicast_group, port=port, **properties) + + +def nds(pipeline: pipetools.Pipeline, host: str, instrument: str, channel_name: str, channel_type: str, blocksize: int = 16384 * 8 * 1, port: int = 31200) -> pipetools.Element: + """NDS-based src element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + host: + str, NDS1 or NDS2 remote host name or IP address + instrument: + str, name of instrument + channel_name: + str, Name of the desired NDS channel. + channel_type: + str, Type of the desired NDS channel. + blocksize: + int, default 16384 * 8 * 1, blocksize + port: + int, NDS1 or NDS2 remote host port + + References: + Implementation: gstlal-ugly/gst/nds/ndssrc.c + + Returns: + Element + """ + # default blocksize is 1 second of double precision floats at + # 16384 Hz, e.g., LIGO h(t) + return pipetools.make_element_with_src(pipeline, None, "ndssrc", blocksize=blocksize, port=port, host=host, channel_name="%s:%s" % (instrument, channel_name), + channel_type=channel_type) + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiotestsrc.html">audiotestsrc</a> element to a pipeline with useful default properties +def audio_test(pipeline: pipetools.Pipeline, freq: float = 440, volume: float = 0.8, wave: int = AudioTestWaveform.Sine, samples_per_buffer: int = 1024, + **properties) -> pipetools.Element: + """AudioTestSrc can be used to generate basic audio signals. It support several different waveforms and + allows to set the base frequency and volume. Some waveforms might use additional properties. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + freq: + float, Frequency of test signal. The sample rate needs to be at least 2 times higher. + volume: + float, default 0.8, Volume of test signal + wave: + int, default 0, the type of waveform to produce. Options are: + sine (0) – Sine + square (1) – Square + saw (2) – Saw + triangle (3) – Triangle + silence (4) – Silence + white-noise (5) – White uniform noise + pink-noise (6) – Pink noise + sine-table (7) – Sine table + ticks (8) – Periodic Ticks + gaussian-noise (9) – White Gaussian noise + red-noise (10) – Red (brownian) noise + blue-noise (11) – Blue noise + violet-noise (12) – Violet noise + samples_per_buffer: + int, default 1024, Number of samples in each outgoing buffer. Must be at least twice 'freq' + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiotestsrc/index.html?gi-language=python + + Returns: + Element + """ + if 'samplesperbuffer' in properties: # support legacy argument name + samples_per_buffer = properties.pop('samplesperbuffer') + return pipetools.make_element_with_src(pipeline, None, "audiotestsrc", freq=freq, volume=volume, wave=wave, + samplesperbuffer=samples_per_buffer, **properties) + + +## see documentation for mktaginject() mkcapsfilter() and mkaudiotestsrc() +def fake(pipeline: pipetools.Pipeline, instrument: str, channel_name: str, blocksize: int = None, volume: float = 1e-20, + is_live: bool = False, wave: int = AudioTestWaveform.GaussianNoise, rate: int = 16384, **properties) -> pipetools.Element: + """Create an audio_test source with several additional, lal-specific caps specified + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + instrument: + str, name of instrument + channel_name: + str, name of input channel + blocksize: + int, default 1 second * rate samples/second * 8 + volume: + float, default 1e-20, the sample volume + is_live: + bool, default False, whether or not audio_test source will behave like live source + wave: + int, default 9 (Gaussian Noise), see AudioTestWaveform enum for options + rate: + int, default 16384, sample rate + **properties: + + Returns: + Element + """ + if blocksize is None: + # default blocksize is 1 second * rate samples/second * 8 + # bytes/sample (assume double-precision floats) + blocksize = 1 * rate * 8 + caps = filters.caps(pipeline, + audio_test(pipeline, samples_per_buffer=int(blocksize / 8), wave=wave, + volume=volume, is_live=is_live, **properties), + "audio/x-raw, format=F64%s, rate=%d".format(BYTE_ORDER, rate)) + return transform.tag_inject(pipeline, caps, "instrument=%s,channel-name=%s,units=strain".format(instrument, channel_name)) diff --git a/gstlal/python/pipeparts/transform.py b/gstlal/python/pipeparts/transform.py new file mode 100644 index 0000000000000000000000000000000000000000..7a88c537a2f5e0d0d1805726ac8f66e67c3a1a5e --- /dev/null +++ b/gstlal/python/pipeparts/transform.py @@ -0,0 +1,1035 @@ +"""Module for general transformation elements + +""" +from typing import Iterable, Tuple, List + +import gi +import numpy + +gi.require_version('Gst', '1.0') +from gi.repository import GObject +from gi.repository import Gst + +GObject.threads_init() +Gst.init(None) +from ligo import segments +from gstlal import pipeio +from gstlal.pipeparts import pipetools, filters + + +def mean(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Compute mean + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_mean.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_mean", **properties) + + +def abs_(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Compute absolute value + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "abs", **properties) + + +def pow(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Compute power + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "pow", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALSumSquares.html">lal_sumsquares</a> element to a pipeline with useful default properties +def sum_squares(pipeline: pipetools.Pipeline, src: pipetools.Element, weights: pipetools.ValueArray = None) -> pipetools.Element: + """Computes the weighted sum-of-squares of the input channels. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + weights: + ValueArray, default None, Vector of weights to use in sum. If no vector is provided weights of 1.0 are assumed, + otherwise the number of input channels must equal the vector length. The incoming channels are first multiplied + by the weights, then squared, then summed. + + References: + Implementation gstlal/gstlal/gst/lal/gstlal_sumsquares.c + + Returns: + Element + """ + if weights is not None: + return pipetools.make_element_with_src(pipeline, src, "lal_sumsquares", weights=weights) + else: + return pipetools.make_element_with_src(pipeline, src, "lal_sumsquares") + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-taginject.html">taginject</a> element to a pipeline with useful default properties +def tag_inject(pipeline: pipetools.Pipeline, src: pipetools.Element, tags: str) -> pipetools.Element: + """Element that injects new metadata tags, but passes incoming data through unmodified. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + tags: + str, List of tags to inject into the target file + + References: + [1] https://gstreamer.freedesktop.org/documentation/debug/taginject.html?gi-language=python + + Returns: + Element, unmodified data with new tags + """ + return pipetools.make_element_with_src(pipeline, src, "taginject", tags=tags) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALShift.html">lal_shift</a> element to a pipeline with useful default properties +def shift(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Adjust segment events by +shift + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + Implementation: gstlal/gst/lal/gstlal_shift.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_shift", **properties) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioamplify.html">audioamplify</a> element to a pipeline with useful default properties +def amplify(pipeline: pipetools.Pipeline, src: pipetools.Element, amplification: float) -> pipetools.Element: + """Amplifies an audio stream by a given factor and allows the selection of different clipping modes. The + difference between the clipping modes is best evaluated by testing. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + amplification: + float, Factor of amplification + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiofx/audioamplify.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "audioamplify", clipping_method=3, amplification=amplification) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALAudioUnderSample.html">lal_audioundersample</a> element to a pipeline with useful default properties +def undersample(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Undersamples an audio stream. Undersampling downsamples by taking every n-th sample, with no antialiasing or + low-pass filter. For data confined to a narrow frequency band, this transformation simultaneously downconverts + and downsamples the data (otherwise it does weird things). This element's output sample rate must be an integer + divisor of its input sample rate. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + Implementation: gstlal/gst/lal/gstlal_audioundersample.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_audioundersample") + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioresample.html">audioresample</a> element to a pipeline with useful default properties +def resample(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Resamples raw audio buffers to different sample rates using a configurable windowing function to enhance quality. By default, + the resampler uses a reduced sinc table, with cubic interpolation filling in the gaps. This ensures that the table does not + become too big. However, the interpolation increases the CPU usage considerably. As an alternative, a full sinc table can be + used. Doing so can drastically reduce CPU usage (4x faster with 44.1 -> 48 kHz conversions for example), at the cost of increased + memory consumption, plus the sinc table takes longer to initialize when the element is created. A third mode exists, which uses + the full table unless said table would become too large, in which case the interpolated one is used instead. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/audioresample/index.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "audioresample", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALInterpolator.html">lal_interpolator</a> element to a pipeline with useful default properties +def interpolator(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Interpolates multichannel audio data using BLAS + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_interpolator.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_interpolator", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties +def whiten(pipeline: pipetools.Pipeline, src: pipetools.Element, psd_mode: int = 0, zero_pad: int = 0, fft_length: int = 8, + average_samples: int = 64, median_samples: int = 7, **properties) -> pipetools.Element: + """A PSD estimator and time series whitener. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + psd_mode: + int, default 0, PSD estimation mode. Options are: + "GSTLAL_PSDMODE_RUNNING_AVERAGE", Use running average for PSD + "GSTLAL_PSDMODE_FIXED", Use fixed spectrum for PSD + zero_pad: + int, default 0, Length of the zero-padding to include on both sides of the FFT in seconds + fft_length: + int, default 8, Total length of the FFT convolution (including zero padding) in seconds + average_samples: + int, default 64, Number of FFTs to be used in PSD average + median_samples: + int, default 7, Number of FFTs to be used in PSD median history + **properties: + + References: + Implementation: gstlal/gst/lal/gstlal_whiten.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_whiten", psd_mode=psd_mode, zero_pad=zero_pad, fft_length=fft_length, average_samples=average_samples, + median_samples=median_samples, + **properties) + + +## Adds a <a href="@gstdoc/gstreamer-plugins-tee.html">tee</a> element to a pipeline with useful default properties +def tee(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Split data to multiple pads. Branching the data flow is useful when e.g. capturing a video where the video is shown on + the screen and also encoded and written to a file. Another example is playing music and hooking up a visualisation module. + One needs to use separate queue elements (or a multiqueue) in each branch to provide separate threads for each branch. + Otherwise a blocked dataflow in one branch would stall the other branches. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + [1] https://gstreamer.freedesktop.org/documentation/coreelements/tee.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "tee") + + +## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "sum" mode mixing. +def adder(pipeline: pipetools.Pipeline, srcs: Iterable[pipetools.Element], sync: bool = True, mix_mode: str = "sum", **properties) -> pipetools.Element: + """The adder allows to mix several streams into one by adding the data. Mixed data is clamped to the min/max + values of the data format. If the element's sync property is TRUE the streams are mixed with the timestamps + synchronized. If the sync property is FALSE (the default, to be compatible with older versions), then the + first samples from each stream are added to produce the first sample of the output, the second samples are + added to produce the second sample of the output, and so on. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + srcs: + Iterable[Gst.Element], the source elements + sync: + bool, default True, Align the time stamps of input streams + mix_mode: + str, default 'sum', Algorithm for mixing the input streams, options: "sum", "product" + **properties: + + References: + Implementation: gstlal/gst/lal/gstadder.c + + Returns: + Element + """ + elem = pipetools.make_element_with_src(pipeline, None, "lal_adder", sync=sync, mix_mode=mix_mode, **properties) + if srcs is not None: + for src in srcs: + src.link(elem) + return elem + + +## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "product" mode mixing. +def multiplier(pipeline: pipetools.Pipeline, srcs: Iterable[pipetools.Element], sync: bool = True, mix_mode: str = "product", **properties) -> pipetools.Element: + """Helper function around adder that defaults to a mix mode of "product" + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + srcs: + Iterable[Gst.Element], the source elements + sync: + bool, default True, Align the time stamps of input streams + mix_mode: + str, default 'product', Algorithm for mixing the input streams, options: "sum", "product" + **properties: + + References: + Implementation: gstlal/gst/lal/gstadder.c + + Returns: + Element + """ + return adder(pipeline, srcs, sync=sync, mix_mode=mix_mode, **properties) + + +## Adds a <a href="@gstdoc/gstreamer-plugins-queue.html">queue</a> element to a pipeline with useful default properties +def queue(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Data is queued until one of the limits specified by the , and/or properties has been reached. Any attempt to push + more buffers into the queue will block the pushing thread until more space becomes available. The queue will create a + new thread on the source pad to decouple the processing on sink and source pad. You can query how many buffers are + queued by reading the property. You can track changes by connecting to the notify::current-level-buffers signal (which + like all signals will be emitted from the streaming thread). The same applies to the and properties. The default queue + size limits are 200 buffers, 10MB of data, or one second worth of data, whichever is reached first. As said earlier, the + queue blocks by default when one of the specified maximums (bytes, time, buffers) has been reached. You can set the + property to specify that instead of blocking it should leak (drop) new or old buffers. The signal is emitted when the + queue has less data than the specified minimum thresholds require (by default: when the queue is empty). The signal is + emitted when the queue is filled up. Both signals are emitted from the context of the streaming thread. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/coreelements/queue.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "queue", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALFIRBank.html">lal_firbank</a> element to a pipeline with useful default properties +def fir_bank(pipeline: pipetools.Pipeline, src: pipetools.Element, latency: int = None, fir_matrix: numpy.ndarray = None, time_domain: bool = None, + block_stride: int = None) -> pipetools.Element: + """Projects a single audio channel onto a bank of FIR filters to produce a multi-channel output + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + latency: + int, default None, Filter latency in samples. + fir_matrix: + numpy.ndarray, default None, Array of impulse response vectors. Number of vectors (rows) in matrix sets number of output channels. All filters + must have the same length. + time_domain: + bool, default None, Set to true to use time-domain (a.k.a. direct) convolution, set to false to use FFT-based convolution. + For long filters FFT-based convolution is usually significantly faster than time-domain convolution but incurs a higher processing + latency and requires more RAM. + block_stride: + int, default None, When using FFT convolutions, this many samples will be produced from each block. Smaller values decrease latency + but increase computational cost. If very small values are desired, consider using time-domain convolution mode instead. + + References: + Implementation: gstlal/gst/lal/gstlal_firbank.c + + Returns: + Element + """ + properties = dict((name, value) for name, value in zip(("latency", "fir_matrix", "time_domain", "block_stride"), + (latency, fir_matrix, time_domain, block_stride)) if value is not None) + return pipetools.make_element_with_src(pipeline, src, "lal_firbank", **properties) + + +def td_whiten(pipeline: pipetools.Pipeline, src: pipetools.Element, latency: int = None, kernel: numpy.ndarray = None, taper_length: int = None): + """Generic audio FIR filter with custom filter kernel and smooth kernel updates + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + latency: + int, default None, Filter latency in samples. + kernel: + array, default None, The newest kernel. + taper_length: + int, default None, Number of samples for kernel transition. + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_tdwhiten.c + + Returns: + Element + """ + # a taper length of 1/4 kernel length mimics the default + # configuration of the FFT whitener + if taper_length is None and kernel is not None: + taper_length = len(kernel) // 4 + properties = dict((name, value) for name, value in zip(("latency", "kernel", "taper_length"), + (latency, kernel, taper_length)) if value is not None) + return pipetools.make_element_with_src(pipeline, src, "lal_tdwhiten", **properties) + + +def trim(pipeline: pipetools.Pipeline, src: pipetools.Element, initial_offset: int = None, final_offset: int = None, inverse: bool = None) -> pipetools.Element: + """Pass data only inside a region and mark everything else as gaps. The offsets are media-type specific. For audio + buffers, it's the number of samples produced so far. For video buffers, it's generally the frame number. For compressed + data, it could be the byte offset in a source or destination file. If inverse=true is set, only data *outside* of the + specified region will pass, and data in the inside will be marked as gaps. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + initial_offset: + int, default None, Only let data with offset bigger than this value pass. + final_offset: + int, default None, Only let data with offset smaller than this value pass + inverse: + bool, default None, If True only data *outside* the region will pass. + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_trim.c + + Returns: + Element + """ + properties = dict((name, value) for name, value in zip(("initial-offset", "final-offset", "inverse"), + (initial_offset, final_offset, inverse)) if value is not None) + return pipetools.make_element_with_src(pipeline, src, "lal_trim", **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALReblock.html">lal_reblock</a> element to a pipeline with useful default properties +def reblock(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Chop audio buffers into smaller pieces to enforce a maximum allowed buffer duration + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + block_duration: + int, Maximum output buffer duration in nanoseconds. Buffers may be smaller than this. + + References: + Implementation: gstlal/gst/lal/gstlal_reblock.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_reblock", **properties) + + +def bit_vector_gen(pipeline: pipetools.Pipeline, src: pipetools.Element, bit_vector: int, **properties) -> pipetools.Element: + """Generate a bit vector stream based on the value of a control input + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + bit_vector: + int, Value to generate when output is \"on\" (output is 0 otherwise). Only as many + low-order bits as are needed by the output word size will be used. + **properties: + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_bitvectorgen.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_bitvectorgen", bit_vector=bit_vector, **properties) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALMatrixMixer.html">lal_matrixmixer</a> element to a pipeline with useful default properties +def matrix_mixer(pipeline: pipetools.Pipeline, src: pipetools.Element, matrix: numpy.ndarray = None) -> pipetools.Element: + """A many-to-many mixer + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + matrix: + array, Matrix of mixing coefficients. Number of rows in matrix sets number of input channels, + number of columns sets number of output channels. + + References: + Implementation: gstlal/gst/lal/gstlal_matrixmixer.c + + Returns: + Element + """ + if matrix is not None: + return pipetools.make_element_with_src(pipeline, src, "lal_matrixmixer", matrix=matrix) + else: + return pipetools.make_element_with_src(pipeline, src, "lal_matrixmixer") + + +## Adds a <a href="@gstlalgtkdoc/GSTLALToggleComplex.html">lal_togglecomplex</a> element to a pipeline with useful default properties +def toggle_complex(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Replace float caps with complex (with half the channels), complex with float (with twice the channels). + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + Implementation: gstlal/gst/lal/gstlal_togglecomplex.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_togglecomplex") + + +## Adds a <a href="@gstlalgtkdoc/GSTLALAutoChiSq.html">lal_autochisq</a> element to a pipeline with useful default properties +def auto_chisq(pipeline: pipetools.Pipeline, src: pipetools.Element, autocorrelation_matrix: numpy.ndarray = None, mask_matrix=None, + latency: int = 0, snr_thresh: int = 0) -> pipetools.Element: + """Computes the chisquared time series from a filter's autocorrelation + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + autocorrelation_matrix: + array, default None, Array of complex autocorrelation vectors. Number of vectors (rows) in matrix sets + number of channels. All vectors must have the same length. + mask_matrix: + array, default None, Array of integer mask vectors. Matrix must be the same size as the autocorrelation + matrix. Only autocorrelation vector samples corresponding to non-zero samples in these vectors will be + used to construct the \\chi^{2} statistic. If this matrix is not supplied, all autocorrelation samples + are used. + latency: + int, default 0, Filter latency in samples. Must be in (-autocorrelation length, 0]. + snr_thresh: + float, default 0, SNR Threshold that determines a trigger. + + References: + Implementation: gstlal/gst/lal/gstlal_autochisq.c + + Returns: + Element + """ + properties = {} + if autocorrelation_matrix is not None: + properties.update({ + "autocorrelation_matrix": pipeio.repack_complex_array_to_real(autocorrelation_matrix), + "latency": latency, + "snr_thresh": snr_thresh + }) + if mask_matrix is not None: + properties["autocorrelation_mask_matrix"] = mask_matrix + return pipetools.make_element_with_src(pipeline, src, "lal_autochisq", **properties) + + +def colorspace(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element: + """Convert video frames between a great variety of video formats. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + + References: + Pre gstreamer-1.0 docs: (ffmpegcolorspace) https://www.freedesktop.org/software/gstreamer-sdk/data/docs/2012.5/gst-plugins-base-plugins-0.10/gst-plugins-base-plugins-ffmpegcolorspace.html + Post gstreamer-1.0 docs: (videoconvert) https://gstreamer.freedesktop.org/documentation/videoconvert/index.html?gi-language=python + Migration: https://gstreamer.freedesktop.org/documentation/application-development/appendix/porting-1-0.html?gi-language=c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "videoconvert") + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioconvert.html">audioconvert</a> element to a pipeline with useful default properties +def audio_convert(pipeline: pipetools.Pipeline, src: pipetools.Element, caps_string: str = None) -> pipetools.Element: + """Audioconvert converts raw audio buffers between various possible formats. It supports integer to float conversion, + width/depth conversion, signedness and endianness conversion and channel transformations (ie. upmixing and downmixing), + as well as dithering and noise-shaping. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + caps_string: + str, Caps string + + References: + [1] https://gstreamer.freedesktop.org/documentation/audioconvert/index.html?gi-language=python + + Returns: + Element + """ + elem = pipetools.make_element_with_src(pipeline, src, "audioconvert") + if caps_string is not None: + elem = filters.caps(pipeline, elem, caps_string) + return elem + + +## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiorate.html">audiorate</a> element to a pipeline with useful default properties +def audio_rate(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """This element takes an incoming stream of timestamped raw audio frames and produces a perfect stream by + inserting or dropping samples as needed. This operation may be of use to link to elements that require or + otherwise implicitly assume a perfect stream as they do not store timestamps, but derive this by some means + (e.g. bitrate for some AVI cases). The properties , , and can be read to obtain information about number of + input samples, output samples, dropped samples (i.e. the number of unused input samples) and inserted samples + (i.e. the number of samples added to stream). When the property is set to FALSE, a GObject property notification + will be emitted whenever one of the or values changes. This can potentially cause performance degradation. Note + that property notification will happen from the streaming thread, so applications should be prepared for this. If + the property is non-zero, and an incoming buffer's timestamp deviates less than the property indicates from what + would make a 'perfect time', then no samples will be added or dropped. Note that the output is still guaranteed to + be a perfect stream, which means that the incoming data is then simply shifted (by less than the indicated tolerance) + to a perfect time. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + [1] https://gstreamer.freedesktop.org/documentation/audiorate/index.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "audiorate", **properties) + + +def deglitch(pipeline: pipetools.Pipeline, src: pipetools.Element, segment_list: List[Tuple[pipetools.TimeGPS, pipetools.TimeGPS]]) -> pipetools.Element: + """Removes glitches based on a segment list. Must be coalesced. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + segment_list: + Iterable[Tuple[TimeGPS, TimeGPS]], list of segment start / stop times + + References: + Implementation: gstlal-ugly/gst/lal/gstlaldeglitchfilter.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_deglitcher", segment_list=segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list)) + + +def check_timestamps(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str = None, silent: bool = True, timestamp_fuzz: int = 1) -> pipetools.Element: + """Timestamp Checker Pass-Through Element + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + name: + str, name of check + silent: + bool, default True, Only report errors. + timestamp_fuzz: + int, Number of nanoseconds of timestamp<-->offset discrepancy to accept before reporting it. Timestamp<-->offset discrepancies + of 1/2 a sample or more are always reported. + + References: + Implementation: gstlal/gst/python/lal_checktimestamps.py + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_checktimestamps", name=name, silent=silent, timestamp_fuzz=timestamp_fuzz) + + +## Adds a <a href="@gstlalgtkdoc/GSTLALPeak.html">lal_peak</a> element to a pipeline with useful default properties +def peak(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int) -> pipetools.Element: + """Find peaks in a time series every n samples + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + n: + int, number of samples over which to identify peaks + + References: + Implementation: gstlal/gst/lal/gstlal_peak.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_peak", n=n) + + +def denoise(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element: + """Separate out stationary/non-stationary components from signals. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_denoiser.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_denoiser", **properties) + + +def clean(pipeline: pipetools.Pipeline, src: pipetools.Element, threshold: float = 1.0) -> pipetools.Element: + """Helper function for denoise that cleans for a stationary, threshold + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + threshold: + float, default 1.0, The threshold in which to allow non-stationary signals in stationary component + + Returns: + Element + """ + return denoise(pipeline, src, stationary=True, threshold=threshold) + + +def latency(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str = None, silent: bool = False) -> pipetools.Element: + """Outputs the current GPS time at time of data flow + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + name: + str, name + silent: + bool, if True run silent + + References: + Implementation: gstlal-ugly/gst/lal/gstlal_latency.c + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_latency", name=name, silent=silent) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-capssetter.html">capssetter</a> element to a pipeline with useful default properties +def set_caps(pipeline: pipetools.Pipeline, src: pipetools.Element, caps: pipetools.Caps, **properties) -> pipetools.Element: + """Sets or merges caps on a stream's buffers. That is, a buffer's caps are updated using (fields of) “capsâ€. Note that this + may contain multiple structures (though not likely recommended), but each of these must be fixed (or will otherwise be rejected). + + If “join†is TRUE, then the incoming caps' mime-type is compared to the mime-type(s) of provided caps and only matching + structure(s) are considered for updating. + + If “replace†is TRUE, then any caps update is preceded by clearing existing fields, making provided fields (as a whole) + replace incoming ones. Otherwise, no clearing is performed, in which case provided fields are added/merged onto incoming caps + + Although this element might mainly serve as debug helper, it can also practically be used to correct a faulty pixel-aspect-ratio, + or to modify a yuv fourcc value to effectively swap chroma components or such alike. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + caps: + Gst.Caps, the caps + **properties: + + References: + [1] https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good/html/gst-plugins-good-plugins-capssetter.html + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "capssetter", caps=Gst.Caps.from_string(caps), **properties) + + +## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-progressreport.html">progress_report</a> element to a pipeline with useful default properties +def progress_report(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str): + """The progressreport element can be put into a pipeline to report progress, which is done by doing upstream + duration and position queries in regular (real-time) intervals. Both the interval and the preferred query format + can be specified via the and the property. + + Element messages containing a "progress" structure are posted on the bus whenever progress has been queried + (since gst-plugins-good 0.10.6 only). + + Since the element was originally designed for debugging purposes, it will by default also print information + about the current progress to the terminal. This can be prevented by setting the property to True. + + This element is most useful in transcoding pipelines or other situations where just querying the pipeline might + not lead to the wanted result. For progress in TIME format, the element is best placed in a 'raw stream' section + of the pipeline (or after any demuxers/decoders/parsers). + + Three more things should be pointed out: + First, the element will only query progress when data flow happens. If data flow is stalled for some reason, no + progress messages will be posted. + + Second, there are other elements (like qtdemux, for example) that may also post "progress" element messages on the bus. + Applications should check the source of any element messages they receive, if needed. + + Third, applications should not take action on receiving notification of progress being 100%, they should only take action + when they receive an EOS message (since the progress reported is in reference to an internal point of a pipeline and + not the pipeline as a whole). + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + name: + str, the name + + References: + [1] https://gstreamer.freedesktop.org/documentation/debug/progressreport.html?gi-language=python + + Returns: + Element + """ + return pipetools.make_element_with_src(pipeline, src, "progressreport", do_query=False, name=name) + + +# TODO move to calibration specific module +def lho_coherent_null(pipeline: pipetools.Pipeline, H1src: pipetools.Element, H2src: pipetools.Element, H1_impulse, H1_latency, H2_impulse, H2_latency, + srate: int) -> pipetools.Element: + """LHO Coherent and Null Streams + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + H1src: + Element, h1 source + H2src: + Element, h2 source + H1_impulse: + impulse response for H1 + H1_latency: + latency for H1 + H2_impulse: + impulse response for H2 + H2_latency: + latency for H2 + srate: + int, block stride for fir bank + + References: + Implementation: gstlal/gst/python/lal_lho_coherent_null.py + + Returns: + Element + """ + elem = pipetools.make_element_with_src(pipeline, None, "lal_lho_coherent_null", block_stride=srate, H1_impulse=H1_impulse, H2_impulse=H2_impulse, H1_latency=H1_latency, + H2_latency=H2_latency) + for peer, padname in ((H1src, "H1sink"), (H2src, "H2sink")): + if isinstance(peer, Gst.Pad): + peer.get_parent_element().link_pads(peer, elem, padname) + elif peer is not None: + peer.link_pads(None, elem, padname) + return elem + + +# TODO move to calibration specific module +def mkcomputegamma(pipeline, dctrl, exc, cos, sin, **properties): + """Compute Gamma + + Args: + pipeline: + dctrl: + exc: + cos: + sin: + **properties: + + References: + Implementation: gstlal-calibration/gst/python/lal_compute_gamma.py + + Returns: + Element + """ + elem = pipetools.make_element_with_src(pipeline, None, "lal_compute_gamma", **properties) + for peer, padname in ((dctrl, "dctrl_sink"), (exc, "exc_sink"), (cos, "cos"), (sin, "sin")): + if isinstance(peer, Gst.Pad): + peer.get_parent_element().link_pads(peer, elem, padname) + elif peer is not None: + peer.link_pads(None, elem, padname) + return elem + + +# TODO find source for lal_odc_to_dqv or delete +def mkodctodqv(pipeline, src, **properties): + return pipetools.make_element_with_src(pipeline, src, "lal_odc_to_dqv", **properties) + + +# TODO move this somewhere else, it's not a transform element +def audioresample_variance_gain(quality: int, num: int, den: int) -> float: + """Calculate the output gain of GStreamer's stock audioresample element. + + The audioresample element has a frequency response of unity "almost" all the + way up the Nyquist frequency. However, for an input of unit variance + Gaussian noise, the output will have a variance very slighly less than 1. + The return value is the variance that the filter will produce for a given + "quality" setting and sample rate. + + @param den The denomenator of the ratio of the input and output sample rates + @param num The numerator of the ratio of the input and output sample rates + @return The variance of the output signal for unit variance input + + The following example shows how to apply the correction factor using an + audioamplify element. + + >>> from gstlal.pipeutil import * + >>> from gstlal.pipeparts import audioresample_variance_gain + >>> from gstlal import pipeio + >>> import numpy + >>> nsamples = 2 ** 17 + >>> num = 2 + >>> den = 1 + >>> def handoff_handler(element, buffer, pad, (quality, filt_len, num, den)): + ... out_latency = numpy.ceil(float(den) / num * filt_len) + ... buf = pipeio.array_from_audio_buffer(buffer).flatten() + ... std = numpy.std(buf[out_latency:-out_latency]) + ... print "quality=%2d, filt_len=%3d, num=%d, den=%d, stdev=%.2f" % ( + ... quality, filt_len, num, den, std) + ... + >>> for quality in range(11): + ... pipeline = Gst.Pipeline() + ... correction = 1/numpy.sqrt(audioresample_variance_gain(quality, num, den)) + ... elems = mkelems_in_bin(pipeline, + ... ('audiotestsrc', {'wave':'gaussian-noise','volume':1}), + ... ('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,format=F64LE,rate=%d' % num)}), + ... ('audioresample', {'quality':quality}), + ... ('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,width=F64LE,rate=%d' % den)}), + ... ('audioamplify', {'amplification':correction,'clipping-method':'none'}), + ... ('fakesink', {'signal-handoffs':True, 'num-buffers':1}) + ... ) + ... filt_len = elems[2].get_property('filter-length') + ... elems[0].set_property('samplesperbuffer', 2 * filt_len + nsamples) + ... if elems[-1].connect_after('handoff', handoff_handler, (quality, filt_len, num, den)) < 1: + ... raise RuntimeError + ... try: + ... if pipeline.set_state(Gst.State.PLAYING) is not Gst.State.CHANGE_ASYNC: + ... raise RuntimeError + ... if not pipeline.get_bus().poll(Gst.MessageType.EOS, -1): + ... raise RuntimeError + ... finally: + ... if pipeline.set_state(Gst.State.NULL) is not Gst.StateChangeReturn.SUCCESS: + ... raise RuntimeError + ... + quality= 0, filt_len= 8, num=2, den=1, stdev=1.00 + quality= 1, filt_len= 16, num=2, den=1, stdev=1.00 + quality= 2, filt_len= 32, num=2, den=1, stdev=1.00 + quality= 3, filt_len= 48, num=2, den=1, stdev=1.00 + quality= 4, filt_len= 64, num=2, den=1, stdev=1.00 + quality= 5, filt_len= 80, num=2, den=1, stdev=1.00 + quality= 6, filt_len= 96, num=2, den=1, stdev=1.00 + quality= 7, filt_len=128, num=2, den=1, stdev=1.00 + quality= 8, filt_len=160, num=2, den=1, stdev=1.00 + quality= 9, filt_len=192, num=2, den=1, stdev=1.00 + quality=10, filt_len=256, num=2, den=1, stdev=1.00 + """ + + # These constants were measured with 2**22 samples. + + if num > den: # downsampling + return den * ( + 0.7224862140943990596, + 0.7975021342935247892, + 0.8547537598970208483, + 0.8744072146753004704, + 0.9075294214410336568, + 0.9101523813406768859, + 0.9280549396020538744, + 0.9391809530012216189, + 0.9539276644089494939, + 0.9623083437067311285, + 0.9684700588501590213 + )[quality] / num + elif num < den: # upsampling + return ( + 0.7539740617648067467, + 0.8270076656536116122, + 0.8835072979478705291, + 0.8966758456219333651, + 0.9253434087537378838, + 0.9255866674042573239, + 0.9346487800036394900, + 0.9415331868209220190, + 0.9524608799160205752, + 0.9624372769883490220, + 0.9704505626409354324 + )[quality] + else: # no change in sample rate + return 1. diff --git a/gstlal/python/pipeparts/trigger.py b/gstlal/python/pipeparts/trigger.py new file mode 100644 index 0000000000000000000000000000000000000000..122074019a36fd42b6e972a4b60d08fb39b9c44d --- /dev/null +++ b/gstlal/python/pipeparts/trigger.py @@ -0,0 +1,190 @@ +"""Module for making trigger elements + +""" +import numpy + +from gstlal import pipeio +from gstlal.pipeparts import pipetools + + +def trigger(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int, autocorrelation_matrix: numpy.ndarray = None, mask_matrix: numpy.ndarray = None, + snr_thresh: float = 0, sigmasq: float = None, max_snr: bool = False): + """Generic trigger generator, find triggers in snr streams + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + n: + int, number of samples over which to identify triggers + autocorrelation_matrix: + ndarray, Array of complex autocorrelation vectors. Number of vectors (rows) in matrix sets number of channels. + All vectors must have the same length. + mask_matrix: + ndarray, Array of integer autocorrelation mask vectors. Number of vectors (rows) in mask sets number of channels. + All vectors must have the same length. The mask values are either 0 or 1 and indicate whether to use the corresponding + matrix entry in computing the autocorrelation chi-sq statistic. + snr_thresh: + float, default 0, SNR Threshold that determines a trigger + sigmasq: + float, default None + max_snr: + bool, default False, Set flag to return single loudest trigger from buffer + + References: + [1] GstLAL Trigger Implementation: gstlal/gstlal-burst/gst/lal/gstlal_trigger.c + + Returns: + Element, the trigger element + """ + properties = { + "n": n, + "snr_thresh": snr_thresh, + "max_snr": max_snr + } + if autocorrelation_matrix is not None: + properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix) + if mask_matrix is not None: + properties["autocorrelation_mask"] = mask_matrix + if sigmasq is not None: + properties["sigmasq"] = sigmasq + return pipetools.make_element_with_src(pipeline, src, "lal_trigger", **properties) + + +def burst_trigger_gen(pipeline, src, **properties): + """Burst Triggergen, find burst triggers in snr streams + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + + References: + [1] BurstTriggerGen Implementation: gstlal/gstlal-burst/gst/lal/gstlal_burst_triggergen.c + + Returns: + Element, the trigger gen element + """ + return pipetools.make_element_with_src(pipeline, src, "lal_bursttriggergen", **properties) + + +def blcbc_trigger_gen(pipeline: pipetools.Pipeline, snr: pipetools.Element, chisq: pipetools.Element, template_bank_filename: str, snr_threshold: float, + sigmasq: pipetools.ValueArray) -> pipetools.Element: + """ Produce sngl_inspiral records from SNR and chi squared. + A trigger is recorded for every instant at which the absolute value of the SNR + is greater than snr-thresh, and also greater than at all of the window seconds + of data that come before and after. snr-thresh and window are properties of + this element. This element has a bounded latency of ~ window and is suitable + for online applications, or applications where precise triggering behavior with + small chunks of data is required + + The maximum possible trigger rate is (1/window) Hz per template. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + snr: + Gst.Element, the source element + chisq: + Element + template_bank_filename: + str, Path to XML file used to generate the template bank. Setting this property resets sigmasq to a vector of 0s. + snr_threshold: + float, SNR Threshold that determines a trigger. + sigmasq: + Vector of \\sigma^{2} factors. + + References: + [1] gstlal/gstlal-inspiral/gst/lal/gstlal_blcbc_triggergen.c + + Returns: + Element, the blcbc trigger gen + """ + # snr is complex and chisq is real so the correct source and sink + # pads will be selected automatically + elem = pipetools.make_element_with_src(pipeline, snr, "lal_blcbctriggergen", bank_filename=template_bank_filename, snr_thresh=snr_threshold, sigmasq=sigmasq) + chisq.link(elem) + return elem + + +def trigger_gen(pipeline: pipetools.Pipeline, snr: pipetools.Element, chisq: pipetools.Element, template_bank_filename: str, snr_threshold: float, + sigmasq: pipetools.ValueArray) -> pipetools.Element: + """Produce sngl_inspiral records from SNR and chi squared. + A trigger is recorded for every instant at which the absolute value of the SNR + is greater than snr-thresh, and also greater than at all of the max_gap seconds + of data that come before and after. snr-thresh and max_gap are properties of + this element + + The maximum possible trigger rate is (1/max_gap) Hz per template. + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + snr: + Gst.Element, the source element + chisq: + Element + template_bank_filename: + str, Path to XML file used to generate the template bank. Setting this property resets sigmasq to a vector of 0s. + snr_threshold: + float, SNR Threshold that determines a trigger. + sigmasq: + Vector of \\sigma^{2} factors. + + References: + Implementation: gstlal-inspiral/gst/lal/gstlal_triggergen.c + + Returns: + Element + """ + # snr is complex and chisq is real so the correct source and sink + # pads will be selected automatically + elem = pipetools.make_element_with_src(pipeline, snr, "lal_triggergen", bank_filename=template_bank_filename, snr_thresh=snr_threshold, sigmasq=sigmasq) + chisq.link(elem) + return elem + + +def itac(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int, bank: str, autocorrelation_matrix: numpy.ndarray = None, + mask_matrix: numpy.ndarray = None, snr_thresh=0, sigmasq=None) -> pipetools.Element: + """Find inspiral triggers in snr streams + + Args: + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + n: + int, number of samples over which to identify itacs + bank: + str, Path to XML file used to generate the template bank. Setting this property resets sigmasq to a vector of 0s. + autocorrelation_matrix: + array, Array of complex autocorrelation vectors. Number of vectors (rows) in matrix sets number of channels. All vectors must have the same length. + mask_matrix: + array, Array of integer autocorrelation mask vectors. Number of vectors (rows) in mask sets number of channels. All vectors must have the same + length. The mask values are either 0 or 1 and indicate whether to use the corresponding matrix entry in computing the autocorrelation chi-sq statistic. + snr_thresh: + float, SNR Threshold that determines a trigger. + sigmasq: + array, Vector of \\sigma^{2} factors. The effective distance of a trigger is \\sqrt{sigma^{2}} / SNR. + + References: + Implementation: gstlal-inspiral/gst/lal/gstlal_itac.c + + Returns: + Element + """ + properties = { + "n": n, + "bank_filename": bank, + "snr_thresh": snr_thresh + } + if autocorrelation_matrix is not None: + properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix) + if mask_matrix is not None: + properties["autocorrelation_mask"] = mask_matrix + if sigmasq is not None: + properties["sigmasq"] = sigmasq + return pipetools.make_element_with_src(pipeline, src, "lal_itac", **properties) diff --git a/gstlal/python/utilities/testtools.py b/gstlal/python/utilities/testtools.py index 8caebc0de3b52270d5cba14002c2b6d0e71c3188..c3bd40b42d83e749c671b6ab4b3f0ab9391a3dda 100644 --- a/gstlal/python/utilities/testtools.py +++ b/gstlal/python/utilities/testtools.py @@ -1,5 +1,14 @@ """Test utilities. Common functions used across various GstLAL unittests """ + +import gi + +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +GObject.threads_init() +Gst.init(None) +import os import pathlib import string import sys @@ -13,59 +22,118 @@ import pytest PLATFORM = sys.platform DEFAULT_MOCK_PATCHES = ( - # Ordered mapping of (target, {kwarg: value}) for passing into unittest.mock.patch - ('gstlal.datafind.load_frame_cache', {'return_value': [1, 2, 3]}), + # Ordered mapping of (target, {kwarg: value}) for passing into unittest.mock.patch + ('gstlal.datafind.load_frame_cache', {'return_value': [1, 2, 3]}), ) CLEAN_TRANSLATION = {ord(c): None for c in string.whitespace} + def clean_str(c: str): - """Clean a copyright string before comparison""" - return c.translate(CLEAN_TRANSLATION) + """Clean a copyright string before comparison""" + return c.translate(CLEAN_TRANSLATION) + def is_osx(platform: str = PLATFORM): - """Check is OSX""" - return platform.lower() == 'darwin' + """Check is OSX""" + return platform.lower() == 'darwin' def skip_osx(f: types.FunctionType) -> types.FunctionType: - """Decorator wrapping pytest.skipif""" - return pytest.mark.skipif(is_osx(), reason='Test not supported on OSX')(f) + """Decorator wrapping pytest.skipif""" + return pytest.mark.skipif(is_osx(), reason='Test not supported on OSX')(f) -class GstLALTestManager: - """Context manager for GstLAL tests""" - - def __init__(self, patch_info: Tuple[Dict[str, Dict]] = DEFAULT_MOCK_PATCHES): - self.tmp_dir = tempfile.TemporaryDirectory() - self.tmp_path = pathlib.Path(self.tmp_dir.name) - self.patch_info = patch_info - self._patches = [] +def requires_full_build(f: types.FunctionType): + return pytest.mark.requires_full_build(f) - def __enter__(self): - """Enter the GstLAL testing context""" - # Create temporary directory - self.tmp_dir.__enter__() +def broken(reason: str): + def wrapper(f: types.FunctionType): + func = pytest.mark.skip(f, reason) + func = pytest.mark.broken(func) + return func - # Set all mocks - for target, kwargs in self.patch_info: - p = mock.patch(target, **kwargs) - self._patches.append(p) - p.__enter__() + return wrapper - return self - def __exit__(self, exc_type, exc_val, exc_tb): - """Exit GstLAL context""" +def impl_deprecated(f): + return broken('Underlying implementation not included in build')(f) - # Remove tmp dir - self.tmp_dir.__exit__(exc_type, exc_val, exc_tb) - # Unset all mocks - for p in self._patches: - p.__exit__(exc_type, exc_val, exc_tb) - - @property - def cache_path(self): - """Cache path""" - return (self.tmp_path / 'cache.txt').as_posix() +class GstLALTestManager: + """Context manager for GstLAL tests""" + + def __init__(self, patch_info: Tuple[Dict[str, Dict]] = DEFAULT_MOCK_PATCHES, env_overrides: dict = None, with_pipeline: bool = False): + self.tmp_dir = tempfile.TemporaryDirectory() + self.tmp_path = pathlib.Path(self.tmp_dir.name) + self.patch_info = patch_info + self._patches = [] + self._env_originals = {} + self._env_overrides = {} if env_overrides is None else env_overrides + self._with_pipeline = with_pipeline + + def __enter__(self): + """Enter the GstLAL testing context""" + + # Create temporary directory + self.tmp_dir.__enter__() + + # Set all mocks + for target, kwargs in self.patch_info: + p = mock.patch(target, **kwargs) + self._patches.append(p) + p.__enter__() + + # Set env overrides + keys = list(self._env_overrides.keys()) + for k in keys: + self.override_env_var(k, self._env_overrides[k]) + + # Set pipeline + self.set_pipeline() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit GstLAL context""" + + # Remove tmp dir + self.tmp_dir.__exit__(exc_type, exc_val, exc_tb) + + # Unset all mocks + for p in self._patches: + p.__exit__(exc_type, exc_val, exc_tb) + + # Undo all env overrides + keys = list(self._env_originals.keys()) + for k in keys: + self.reset_env_var(k) + + @property + def cache_path(self): + """Cache path""" + return (self.tmp_path / 'cache.txt').as_posix() + + def override_env_var(self, key: str, val: str): + if key in self._env_originals: + raise ValueError('That env var is already overridden') + self._env_originals[key] = os.environ.get(key, None) + self._env_overrides[key] = val + os.environ[key] = val + + def reset_env_var(self, key: str): + if key not in self._env_originals: + raise ValueError('Unable to reset var: {}'.format(key)) + val = self._env_originals.pop(key) + if val is None: + os.environ.pop(key) + else: + os.environ[key] = val + if key in self._env_overrides: + self._env_overrides.pop(key) + + def set_pipeline(self): + if self._with_pipeline: + self.pipeline = Gst.Pipeline(name=os.path.split(sys.argv[0])[1]) + else: + self.pipeline = None diff --git a/gstlal/share/conda/set_env.sh b/gstlal/share/conda/set_env.sh index 55f6a48b1d96f59da0f18ea87bd8fc89bdf2706f..9b9ec133fdefa1f7887e57886d570e717181f109 100755 --- a/gstlal/share/conda/set_env.sh +++ b/gstlal/share/conda/set_env.sh @@ -45,20 +45,26 @@ PARENT_PATH=$( cd .. pwd ) +#APP_PARENT_PATH=$( +# builtin cd $PARENT_PATH +# cd .. +# pwd +#) +APP_PARENT_PATH=$PARENT_PATH echo "Repository root discovered: $REPO_PATH" # Check if deps directory already exists -if [ -d "$PARENT_PATH/deps" ]; then +if [ -d "$APP_PARENT_PATH/deps" ]; then echo "Sibling directory already exists, no need to create." else echo "Sibling directory does not exist, creating." - mkdir "$PARENT_PATH/deps" + mkdir "$APP_PARENT_PATH/deps" fi # Set dependency-path variable for gstlal build dependencies if [[ -z "${DEPS_PATH}" ]]; then - echo "DEPS_PATH variable not set. Setting to: $PARENT_PATH/deps." - export DEPS_PATH=${PARENT_PATH}/deps + echo "DEPS_PATH variable not set. Setting to: $APP_PARENT_PATH/deps." + export DEPS_PATH=${APP_PARENT_PATH}/deps fi # Add dep-path to PATH variable diff --git a/gstlal/tests/tests_pytest/pipeparts/test_encode.py b/gstlal/tests/tests_pytest/pipeparts/test_encode.py new file mode 100644 index 0000000000000000000000000000000000000000..e79e412b896c822f17a2e77def554d461be537a0 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_encode.py @@ -0,0 +1,60 @@ +"""Unittests for encoding pipeparts""" +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import encode, pipetools +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestEncode: + """Group test for encoders""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_wav(self, pipeline, src): + """Test wav""" + elem = encode.wav(pipeline, src) + assert_elem_props(elem, 'GstWavEnc') + + @testtools.broken('vorbis not available in current version of Gst') + def test_vorbis(self, pipeline, src): + """Test vorbis""" + elem = encode.vorbis(pipeline, src) + assert_elem_props(elem, 'GstVorbisEnc') + + @testtools.broken('theora not available in current version of Gst') + def test_theora(self, pipeline, src): + """Test theora""" + elem = encode.theora(pipeline, src) + assert_elem_props(elem, 'GstTheoraEnc') + + @testtools.broken('flac not available in current version of Gst') + def test_flac(self, pipeline, src): + """Test flac""" + elem = encode.flac(pipeline, src) + assert_elem_props(elem, 'GstFlacEnc') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_igwd_parse(self, pipeline, src): + """Test igwd parse""" + elem = encode.igwd_parse(pipeline, src) + assert_elem_props(elem, 'GstFrameCPPIGWDParse') + + def test_uri_decode_bin(self, pipeline, src): + """Test uri_decode""" + elem = encode.uri_decode_bin(pipeline, src) + assert_elem_props(elem, 'GstURIDecodeBin') diff --git a/gstlal/tests/tests_pytest/pipeparts/test_filters.py b/gstlal/tests/tests_pytest/pipeparts/test_filters.py new file mode 100644 index 0000000000000000000000000000000000000000..7452f330827b3945a729242c9ccdb63e6690be2f --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_filters.py @@ -0,0 +1,75 @@ +"""Unittests for filter pipeparts""" +import numpy +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import filters, pipetools +from gstlal.pipeparts.pipetools import Gst, GObject + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestFilters: + """Test filter pipeparts""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_caps(self, pipeline, src): + """Test caps filter""" + elem = filters.caps(pipeline, src, caps="audio/x-raw, rate=1") + assert_elem_props(elem, "GstCapsFilter") + + def test_fir(self, pipeline, src): + """Test create a FIR filter""" + elem = filters.fir(pipeline, src, kernel=None, latency=None) + assert_elem_props(elem, "GstAudioFIRFilter") + + def test_iir(self, pipeline, src): + """Test create IIR filter""" + elem = filters.iir(pipeline, src, a=numpy.array([1, 2, 3]), b=numpy.array([1, 2, 3])) + assert_elem_props(elem, "GstAudioIIRFilter") + + def test_state_vector(self, pipeline, src): + """Test state_vector""" + elem = filters.state_vector(pipeline, src) + assert_elem_props(elem, "GSTLALStateVector") + + def test_inject(self, pipeline, src): + """Test injections""" + elem = filters.inject(pipeline, src, 'file') + assert_elem_props(elem, "GSTLALSimulation") + + def test_audio_cheb_band(self, pipeline, src): + """Test cheb""" + elem = filters.audio_cheb_band(pipeline, src, 10.0, 100.0) + assert_elem_props(elem, "GstAudioChebBand") + + def test_audio_cheb_limit(self, pipeline, src): + """Test cheb limit""" + elem = filters.audio_cheb_limit(pipeline, src, 10.0) + assert_elem_props(elem, "GstAudioChebLimit") + + def test_drop(self, pipeline, src): + """Test drop""" + elem = filters.drop(pipeline, src, 10) + assert_elem_props(elem, "GSTLALDrop") + + def test_remove_fake_disconts(self, pipeline, src): + """Test removefd""" + elem = filters.remove_fake_disconts(pipeline, src) + assert_elem_props(elem, "GSTLALNoFakeDisconts") + + def test_gate(self, pipeline, src): + """Test gate""" + elem = filters.gate(pipeline, src, control=src) + assert_elem_props(elem, "GSTLALGate") diff --git a/gstlal/tests/tests_pytest/pipeparts/test_mux.py b/gstlal/tests/tests_pytest/pipeparts/test_mux.py new file mode 100644 index 0000000000000000000000000000000000000000..73e2f82b54755d066938a474b271ec3691afb9f5 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_mux.py @@ -0,0 +1,43 @@ +"""Unittests for mux pipeparts""" +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import mux, pipetools +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestMux: + """Test mux pipeparts""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + @testtools.requires_full_build + def test_framecpp_demux(self, pipeline, src): + """Test framecpp demux""" + elem = mux.framecpp_channel_demux(pipeline, src) + assert_elem_props(elem, "GstFrameCPPChannelDemux") + + @testtools.requires_full_build + def test_framecpp_mux(self, pipeline, src): + """Test framecpp demux""" + elem = mux.framecpp_channel_mux(pipeline, None) + assert_elem_props(elem, "GstFrameCPPChannelMux") + + @testtools.broken('oggmux not available in current version of Gst') + def test_ogg(self, pipeline, src): + """Test ogg mux""" + elem = mux.ogg_mux(pipeline, src) + assert_elem_props(elem, "GstOggMux") diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py b/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py new file mode 100644 index 0000000000000000000000000000000000000000..b5fc963eb8df1fd9b913ede3a36cffb4e4b8ee33 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py @@ -0,0 +1,125 @@ +"""Unittests for pipedot pipeparts""" +import re + +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, pipedot +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + +# 'fakesrc0_0x7fae93190120_src_0x7fae93194080' +MEMORY_ADDR_PATTERN_OSX = '[0-9]?_0x[0-9a-fA-F]{12}' +MEMORY_ADDR_PATTERN_LINUX = '[0-9]?_0x[0-9a-fA-F]{7}' +FAKE_SRC_PATTERN = 'fakesrc[0-9]{1}' + +CLEAN_PIPELINE_DOT_OSX = ( + 'digraph pipeline {\n rankdir=LR;\n fontname="sans";\n fontsize="10";\n labelloc=t;\n nodesep=.1;\n ranksep=.2;\n label="<GstPipeline>\\nTestEncode\\n[0]";\n' + ' node [style="filled,rounded", shape=box, fontsize="9", fontname="sans", margin="0.0,0.0"];\n edge [labelfontsize="6", fontsize="9", fontname="monospace"];\n \n' + ' legend [\n pos="0,0!",\n margin="0.05,0.05",\n style="filled",\n label="Legend\\lElement-States: [~] void-pending, [0] null, [-] ready, [=] paused, [>]' + ' playing\\lPad-Activation: [-] none, [>] push, [<] pull\\lPad-Flags: [b]locked, [f]lushing, [b]locking, [E]OS; upper-case is set\\lPad-Task: [T] has started task, [t]' + ' has paused task\\l",\n ];\n subgraph cluster_fakesrc {\n fontname="Bitstream Vera Sans";\n fontsize="8";\n style="filled,rounded";\n color=black;\n ' + 'label="GstFakeSrc\\nfakesrc\\n[0]\\nfilltype=nothing";\n subgraph cluster_fakesrc_src {\n label="";\n style="invis";\n fakesrc_src [color=black, ' + 'fillcolor="#ffaaaa", label="src\\n[-][bFb]", height="0.2", style="filled,solid"];\n }\n\n fillcolor="#ffaaaa";\n }\n\n}\n') + +CLEAN_PIPELINE_DOT_LINUX = ( + 'digraph pipeline {\n rankdir=LR;\n fontname="sans";\n fontsize="10";\n labelloc=t;\n nodesep=.1;\n ranksep=.2;\n label="<GstPipeline>\\nTestEncode\\n[0]";\n' + ' node [style="filled,rounded", shape=box, fontsize="9", fontname="sans", margin="0.0,0.0"];\n edge [labelfontsize="6", fontsize="9", fontname="monospace"];\n \n' + ' legend [\n pos="0,0!",\n margin="0.05,0.05",\n style="filled",\n label="Legend\\lElement-States: [~] void-pending, [0] null, [-] ready, [=] paused, [>]' + ' playing\\lPad-Activation: [-] none, [>] push, [<] pull\\lPad-Flags: [b]locked, [f]lushing, [b]locking, [E]OS; upper-case is set\\lPad-Task: [T] has started task, [t]' + ' has paused task\\l",\n ];\n subgraph cluster_fakesrc {\n fontname="Bitstream Vera Sans";\n fontsize="8";\n style="filled,rounded";\n color=black;\n ' + 'label="GstFakeSrc\\nfakesrc\\n[0]\\nparent=(GstPipeline) TestEncode\\nfilltype=nothing";\n subgraph cluster_fakesrc_src {\n label="";\n style="invis";\n fakesrc_src [color=black, ' + 'fillcolor="#ffaaaa", label="src\\n[-][bFb]", height="0.2", style="filled,solid"];\n }\n\n fillcolor="#ffaaaa";\n }\n\n}\n') + +SANITIZE_PATTERNS = [ + (MEMORY_ADDR_PATTERN_OSX, ''), + (MEMORY_ADDR_PATTERN_LINUX, ''), + (FAKE_SRC_PATTERN, 'fakesrc') +] + +if testtools.is_osx(): + CLEAN_PIPELINE_DOT = CLEAN_PIPELINE_DOT_OSX +else: + CLEAN_PIPELINE_DOT = CLEAN_PIPELINE_DOT_LINUX + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +def sanitize_dot_str(s: str) -> str: + """Utility for making a dot str stable across sessions""" + for pat, rep in SANITIZE_PATTERNS: + s = re.sub(pat, rep, s) + return s + + +class TestSanitize: + """Test group for sanitizing dot strings""" + + def test_memory_addr_pattern(self): + """Test memory address pattern""" + sample_text = 'asdljkhads asd fakesrc0_0x7fae93190120_src_0x7fae93194080 asldkj' + matches = re.findall(re.compile(MEMORY_ADDR_PATTERN_OSX), sample_text) + assert matches == ['0_0x7fae93190120', '_0x7fae93194080'] + + sample_text = 'cluster_fakesrc_0x2cc08c0' + matches = re.findall(re.compile(MEMORY_ADDR_PATTERN_LINUX), sample_text) + assert matches == ['_0x2cc08c0'] + + def test_sanitize_dot_str(self): + """Test sanitizer""" + sample_text = 'asdljkhads asd fakesrc0_0x7fae93190120_src_0x7fae93194080 asldkj' + sanitized = sanitize_dot_str(sample_text) + assert sanitized == 'asdljkhads asd fakesrc_src asldkj' + + sample_text = 'cluster_fakesrc_0x2cc08c0' + sanitized = sanitize_dot_str(sample_text) + assert sanitized == 'cluster_fakesrc' + + +class TestDot: + """Test dot pipeparts""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + @testtools.broken('Unable to redirect dump dot dir env var sufficiently') + def test_to_file(self, pipeline, src): + """Test write dot file""" + + with testtools.GstLALTestManager() as tm: + tm.override_env_var('GST_DEBUG_DUMP_DOT_DIR', tm.tmp_path.as_posix()) + + pipedot.to_file(pipeline, 'graph') + + output_path = tm.tmp_path / 'graph.dot' + assert output_path.exists() + + tm.reset_env_var('GST_DEBUG_DUMP_DOT_DIR') + + def test_to_file_use_str(self, pipeline, src): + """Test write dot file using str intermediary""" + + with testtools.GstLALTestManager() as tm: + tm.override_env_var('GST_DEBUG_DUMP_DOT_DIR', tm.tmp_path.as_posix()) + + pipedot.to_file(pipeline, 'graph', use_str_method=True) + + output_path = tm.tmp_path / 'graph.dot' + assert output_path.exists() + + def test_to_str(self, pipeline, src): + """Test convert pipeline to str""" + + with testtools.GstLALTestManager() as tm: + res = pipedot.to_str(pipeline) + assert isinstance(res, str) + assert sanitize_dot_str(res)[:10] == CLEAN_PIPELINE_DOT[:10] # TODO reverse kneecap diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py b/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py new file mode 100644 index 0000000000000000000000000000000000000000..7a52441d50c26a498bb2b555dc4a57c5ffabade4 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py @@ -0,0 +1,118 @@ +"""Unit tests for pipeparts subpackage + +These tests are only for the __init__ file and other subpackage-wide +tests, module specific tests should follow the test_module.py pattern +""" +import pytest +from gstlal import pipeparts + +LEGACY_ATTRS = [ + 'AppSync', + 'BYTE_ORDER', + 'audioresample_variance_gain', + 'connect_appsink_dump_dot', + 'framecpp_channeldemux_check_segments', + 'framecpp_channeldemux_set_units', + 'framecpp_filesink_cache_entry_from_mfs_message', + 'framecpp_filesink_ldas_path_handler', + 'mkabs', + 'mkadder', + 'mkappsink', + 'mkaudioamplify', + 'mkaudiochebband', + 'mkaudiocheblimit', + 'mkaudioconvert', + 'mkaudiorate', + 'mkaudiotestsrc', + 'mkaudioundersample', + 'mkautoaudiosink', + 'mkautochisq', + 'mkavimux', + 'mkbitvectorgen', + 'mkblcbctriggergen', + 'mkbursttriggergen', + 'mkcapsfilter', + 'mkcapssetter', + 'mkchannelgram', + 'mkchecktimestamps', + 'mkclean', + 'mkcolorspace', + 'mkcomputegamma', + 'mkdeglitcher', + 'mkdenoiser', + 'mkdrop', + 'mkfakeLIGOsrc', + 'mkfakeadvLIGOsrc', + 'mkfakeadvvirgosrc', + 'mkfakesink', + 'mkfakesrc', + 'mkfilesink', + 'mkfirbank', + 'mkfirfilter', + 'mkflacenc', + 'mkframecppchanneldemux', + 'mkframecppchannelmux', + 'mkframecppfilesink', + 'mkframexmitsrc', + 'mkgate', + 'mkgeneric', + 'mkhistogram', + 'mkigwdparse', + 'mkiirfilter', + 'mkinjections', + 'mkinterpolator', + 'mkitac', + 'mklalcachesrc', + 'mklatency', + 'mklhocoherentnull', + 'mklvshmsrc', + 'mkmatrixmixer', + 'mkmean', + 'mkmultifilesink', + 'mkmultiplier', + 'mkndssrc', + 'mknofakedisconts', + 'mknxydumpsink', + 'mknxydumpsinktee', + 'mkodctodqv', + 'mkoggmux', + 'mkogmvideosink', + 'mkpeak', + 'mkplaybacksink', + 'mkpow', + 'mkprogressreport', + 'mkqueue', + 'mkreblock', + 'mkresample', + 'mksegmentsrc', + 'mkshift', + 'mkspectrumplot', + 'mkstatevector', + 'mksumsquares', + 'mktaginject', + 'mktcpserversink', + 'mktdwhiten', + 'mktee', + 'mktheoraenc', + 'mktogglecomplex', + 'mktrigger', + 'mktriggergen', + 'mktriggerxmlwritersink', + 'mktrim', + 'mkuridecodebin', + 'mkvideosink', + 'mkvorbisenc', + 'mkwavenc', + 'mkwhiten', + 'src_deferred_link', + 'write_dump_dot', +] + + +class TestLegacyImportPaths: + """Test class for legacy import paths testing""" + + @pytest.mark.parametrize("attribute", LEGACY_ATTRS) + def test_attributes_exist(self, attribute): + """Test that all expected attributes of pipeparts exist""" + assert hasattr(pipeparts, attribute) diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py b/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py new file mode 100644 index 0000000000000000000000000000000000000000..dd21db64719504f99d5fefa5843281922810af32 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py @@ -0,0 +1,35 @@ +"""Unit tests for pipetools module""" +import inspect + +from gstlal import gsttools +from gstlal.pipeparts import pipetools +from gstlal.utilities import testtools + + +# Will reuse a million times: +""" + + pipeline: + Gst.Pipeline, the pipeline to which the new element will be added + src: + Gst.Element, the source element + **properties: + dict, keyword arguments to be set as element properties + +""" + + +class TestPipetools: + """Test class group for pipetools""" + + def test_clean_property_name(self): + """Test cleaning of property names""" + assert pipetools.clean_property_name('name_with_underscores') == 'name-with-underscores' + assert pipetools.clean_property_name('async_') == 'async' + + def test_make_element_with_src(self): + """Test make element""" + with testtools.GstLALTestManager(with_pipeline=True) as tm: + elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc') + assert gsttools.is_element(elem) + diff --git a/gstlal/tests/tests_pytest/pipeparts/test_plot.py b/gstlal/tests/tests_pytest/pipeparts/test_plot.py new file mode 100644 index 0000000000000000000000000000000000000000..a30b32132d4d954eb3da91fa60a6d697befafef2 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_plot.py @@ -0,0 +1,43 @@ +"""Unittests for plotting pipeparts""" +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, plot +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestPlot: + """Group test for encoders""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + @testtools.broken('Python gst plugins are broken') + def test_channelgram(self, pipeline, src): + """Test channelgram""" + elem = plot.channelgram(pipeline, src) + assert_elem_props(elem, 'GstWavEnc') + + @testtools.broken('Python gst plugins are broken') + def test_spectrum(self, pipeline, src): + """Test spectrum""" + elem = plot.spectrum(pipeline, src) + assert_elem_props(elem, 'GstWavEnc') + + @testtools.broken('Python gst plugins are broken') + def test_histogram(self, pipeline, src): + """Test histogram""" + elem = plot.histogram(pipeline, src) + assert_elem_props(elem, 'GstWavEnc') diff --git a/gstlal/tests/tests_pytest/pipeparts/test_sink.py b/gstlal/tests/tests_pytest/pipeparts/test_sink.py new file mode 100644 index 0000000000000000000000000000000000000000..5b6fa9d8ac19fce44960168221485c6485eb062a --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_sink.py @@ -0,0 +1,95 @@ +"""Unittests for sink pipeparts""" +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, sink +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestSink: + """Group test for encoders""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_multi_file(self, pipeline, src): + """Test test multi file""" + elem = sink.multi_file(pipeline, src) + assert_elem_props(elem, 'GstMultiFileSink') + + @testtools.requires_full_build + def test_gwf(self, pipeline, src): + """Test test framecpp file""" + elem = sink.gwf(pipeline, src) + assert_elem_props(elem, 'FRAMECPPFilesink') + + def test_fake(self, pipeline, src): + """Test test fake""" + elem = sink.fake(pipeline, src) + assert_elem_props(elem, 'GstFakeSink') + + def test_file(self, pipeline, src): + """Test test file""" + elem = sink.file(pipeline, src, 'file') + assert_elem_props(elem, 'GstFileSink') + + def test_nxy_dump(self, pipeline, src): + """Test nxy dump""" + elem = sink.tsv(pipeline, src, 'file') + assert_elem_props(elem, 'GstFileSink') + + @testtools.broken('ogmvideo not available in current version of Gst') + def test_ogm_video(self, pipeline, src): + """Test ogm_video""" + elem = sink.ogm_video(pipeline, src, 'file') + assert_elem_props(elem, 'GstFileSink') + + def test_auto_video(self, pipeline, src): + """Test auto_video""" + elem = sink.auto_video(pipeline, src) + assert_elem_props(elem, 'GstAutoVideoSink') + + def test_auto_audio(self, pipeline, src): + """Test auto_audio""" + elem = sink.auto_audio(pipeline, src) + assert_elem_props(elem, 'GstAutoAudioSink') + + @testtools.broken('element_link_many not available in current version of Gst') + def test_playback(self, pipeline, src): + """Test playback""" + elem = sink.playback(pipeline, src) + assert_elem_props(elem, 'GstAutoAudioSink') + + def test_nxy_dump_tee(self, pipeline, src): + """Test nxydumptee""" + elem = sink.tsv_tee(pipeline, src, 'file') + assert_elem_props(elem, 'GstTee') + + @testtools.broken('Unable to find implementation of lal_triggerxmlwriter') + def test_trigger_xml_writer(self, pipeline, src): + """Test trigger xml writer""" + elem = sink.trigger_xml_writer(pipeline, src, 'file') + assert_elem_props(elem, 'GstAutoAudioSink') + + def test_app(self, pipeline, src): + """Test app""" + elem = sink.app(pipeline, src) + assert_elem_props(elem, 'GstAppSink') + + @testtools.broken('Implementation bug, invalid property unit_type for tcpserversink element') + def test_tcp_server(self, pipeline, src): + """Test tcp_server""" + elem = sink.tcp_server(pipeline, src) + assert_elem_props(elem, 'GstTcpServerSink') diff --git a/gstlal/tests/tests_pytest/pipeparts/test_source.py b/gstlal/tests/tests_pytest/pipeparts/test_source.py new file mode 100644 index 0000000000000000000000000000000000000000..437925d7ee433df8fcc026965d13a5c1d85e2134 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_source.py @@ -0,0 +1,84 @@ +"""Unittests for source pipeparts""" +import pytest +from lal import LIGOTimeGPS + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, source +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestSource: + """Group test for sources""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_cache(self, pipeline, src): + """Test cache source""" + elem = source.cache(pipeline, 'cache-location.txt') + assert_elem_props(elem, 'GstLALCacheSrc') + + @testtools.requires_full_build + def test_lvshm(self, pipeline, src): + """Test lvshm""" + elem = source.lvshm(pipeline, 'LHO_Data') + assert_elem_props(elem, 'GDSLVSHMSrc') + + @testtools.requires_full_build + def test_frame_x_mit(self, pipeline, src): + """Test frame_x_mit""" + elem = source.framexmit(pipeline) + assert_elem_props(elem, 'GstGDSFramexmitSrc') + + @testtools.requires_full_build + def test_nds(self, pipeline, src): + """Test nds source""" + elem = source.nds(pipeline, 'host', 'inst', 'channel', + channel_type=source.NDSChannelType.TestPoint) + assert_elem_props(elem, 'GSTLALNDSSrc') + + def test_audio_test(self, pipeline, src): + """Test audio test source""" + elem = source.audio_test(pipeline) + assert_elem_props(elem, 'GstAudioTestSrc') + + def test_fake(self, pipeline, src): + """Test fake source""" + elem = source.fake(pipeline, 'H1', 'channel') + assert_elem_props(elem, 'GstTagInject') + + def test_segment(self, pipeline, src): + """Test segment source""" + elem = source.segment(pipeline, [(LIGOTimeGPS(123456), LIGOTimeGPS(123457))]) + assert_elem_props(elem, 'GSTLALSegmentSrc') + + @testtools.impl_deprecated + def test_fake_ligo(self, pipeline, src): + """Test fake ligo source""" + elem = source.fake_ligo(pipeline) + assert_elem_props(elem, 'lal_fakeligosrc') + + @testtools.impl_deprecated + def test_fake_aligo(self, pipeline, src): + """Test fake aligo source""" + elem = source.fake_aligo(pipeline) + assert_elem_props(elem, 'lal_fakeadvligosrc') + + @testtools.impl_deprecated + def test_fake_avirgo(self, pipeline, src): + """Test fake avirgo source""" + elem = source.fake_avirgo(pipeline) + assert_elem_props(elem, 'lal_fakeadvvirgosrc') + diff --git a/gstlal/tests/tests_pytest/pipeparts/test_transform.py b/gstlal/tests/tests_pytest/pipeparts/test_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..ae028e8c3e3afa86b217e08798937cd62409d00e --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_transform.py @@ -0,0 +1,236 @@ +"""Unittests for transform pipeparts""" +import pytest +from lal import LIGOTimeGPS + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, transform +from gstlal.pipeparts.pipetools import Gst +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestTransform: + """Group test for transform elements""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_sum_squares(self, pipeline, src): + """Test sumsquares""" + elem = transform.sum_squares(pipeline, src) + assert_elem_props(elem, 'GSTLALSumSquares') + + def test_tag_inject(self, pipeline, src): + """Test sumsquares""" + elem = transform.tag_inject(pipeline, src, 'a,b,c') + assert_elem_props(elem, 'GstTagInject') + + def test_shift(self, pipeline, src): + """Test shift""" + elem = transform.shift(pipeline, src) + assert_elem_props(elem, 'GSTLALShift') + + def test_audio_amplify(self, pipeline, src): + """Test audio amplify""" + elem = transform.amplify(pipeline, src, 0.1) + assert_elem_props(elem, 'GstAudioAmplify') + + def test_audio_undersample(self, pipeline, src): + """Test audio amplify""" + elem = transform.undersample(pipeline, src) + assert_elem_props(elem, 'GSTLALAudioUnderSample') + + def test_audio_resample(self, pipeline, src): + """Test audio resample""" + elem = transform.resample(pipeline, src) + assert_elem_props(elem, 'GstAudioResample') + + def test_whiten(self, pipeline, src): + """Test whiten""" + elem = transform.whiten(pipeline, src, "GSTLAL_PSDMODE_FIXED") + assert_elem_props(elem, 'GSTLALWhiten') + + def test_tee(self, pipeline, src): + """Test tee""" + elem = transform.tee(pipeline, src) + assert_elem_props(elem, 'GstTee') + + def test_adder(self, pipeline, src): + """Test adder""" + elem = transform.adder(pipeline, [src, src]) + assert_elem_props(elem, 'GstLALAdder') + + def test_multiplier(self, pipeline, src): + """Test adder""" + elem = transform.adder(pipeline, [src, src]) + assert_elem_props(elem, 'GstLALAdder') + + def test_queue(self, pipeline, src): + """Test queue""" + elem = transform.queue(pipeline, src) + assert_elem_props(elem, 'GstQueue') + + def test_fir_bank(self, pipeline, src): + """Test fir_bank""" + elem = transform.fir_bank(pipeline, src) + assert_elem_props(elem, 'GSTLALFIRBank') + + def test_reblock(self, pipeline, src): + """Test reblock""" + elem = transform.reblock(pipeline, src) + assert_elem_props(elem, 'GSTLALReblock') + + def test_matrixmixer(self, pipeline, src): + """Test matrixmixer""" + elem = transform.matrix_mixer(pipeline, src) + assert_elem_props(elem, 'GSTLALMatrixMixer') + + def test_toggle_complex(self, pipeline, src): + """Test matrixmixer""" + elem = transform.toggle_complex(pipeline, src) + assert_elem_props(elem, 'GSTLALToggleComplex') + + def test_auto_chisq(self, pipeline, src): + """Test """ + elem = transform.auto_chisq(pipeline, src) + assert_elem_props(elem, 'GSTLALAutoChiSq') + + def test_colorspace(self, pipeline, src): + """Test """ + elem = transform.colorspace(pipeline, src) + assert_elem_props(elem, 'GstVideoConvert') + + def test_audioconvert(self, pipeline, src): + """Test """ + elem = transform.audio_convert(pipeline, src) + assert_elem_props(elem, 'GstAudioConvert') + + def test_audiorate(self, pipeline, src): + """Test """ + elem = transform.audio_rate(pipeline, src) + assert_elem_props(elem, 'GstAudioRate') + + def test_peak(self, pipeline, src): + """Test """ + elem = transform.peak(pipeline, src, 10) + assert_elem_props(elem, 'GSTLALPeak') + + def test_set_caps(self, pipeline, src): + """Test caps setter""" + elem = transform.set_caps(pipeline, src, caps="audio/x-raw, rate=1") + assert_elem_props(elem, "GstCapsSetter") + + def test_progress_report(self, pipeline, src): + """Test progress report""" + elem = transform.progress_report(pipeline, src, 'name') + assert_elem_props(elem, 'GstProgressReport') + + +@testtools.requires_full_build +class TestTransformFullBuild: + """Group test for transform elements""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + return Gst.Pipeline('TestTransformFullBuild') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + def test_bitvectorgen(self, pipeline, src): + """Test bvg""" + elem = transform.bit_vector_gen(pipeline, src, 1) + assert_elem_props(elem, 'GSTLALBitVectorGen') + + def test_deglitch(self, pipeline, src): + """Test """ + elem = transform.deglitch(pipeline, src, [(LIGOTimeGPS(123456), LIGOTimeGPS(123457))]) + assert_elem_props(elem, 'GstLALDeglitchFilter') + + def test_denoise(self, pipeline, src): + """Test """ + elem = transform.denoise(pipeline, src) + assert_elem_props(elem, 'GSTLALDenoiser') + + def test_clean(self, pipeline, src): + """Test """ + elem = transform.clean(pipeline, src) + assert_elem_props(elem, 'GSTLALDenoiser') + + def test_latency(self, pipeline, src): + """Test """ + elem = transform.latency(pipeline, src) + assert_elem_props(elem, 'GSTLALLatency') + + +class TestTransformBroken: + """Broken tests""" + + @testtools.broken('Causes seg fault') + def test_td_whiten(self, pipeline, src): + """Test fir_bank""" + elem = transform.td_whiten(pipeline, src) + assert_elem_props(elem, 'GSTLALTDwhiten') + + @testtools.broken('Unable to find abs') + def test_abs(self, pipeline, src): + """Test abs""" + elem = transform.abs_(pipeline, src) + assert_elem_props(elem, 'GstMultiFileSink') + + @testtools.broken('Unable to find pow') + def test_pow(self, pipeline, src): + """Test pow""" + elem = transform.pow(pipeline, src) + assert_elem_props(elem, 'GstMultiFileSink') + + @testtools.broken('Crashed test suite on mac') + def test_check_timestamps(self, pipeline, src): + """Test """ + elem = transform.check_timestamps(pipeline, src) + assert_elem_props(elem, 'GstLALDeglitchFilter') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_mean(self, pipeline, src): + """Test mean""" + elem = transform.mean(pipeline, src) + assert_elem_props(elem, 'GstMultiFileSink') + + @testtools.broken('Python plugins are broken') + @testtools.requires_full_build + def test_coherentnull(self, pipeline, src): + """Test """ + elem = transform.lho_coherent_null(pipeline, src, src, None, None, None, None, 1) + assert_elem_props(elem, 'GSTLALDenoiser') + + @testtools.broken('Crashed test suite on mac') + @testtools.requires_full_build + def test_check_timestamps(self, pipeline, src): + """Test """ + elem = transform.check_timestamps(pipeline, src) + assert_elem_props(elem, 'GstLALDeglitchFilter') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_trim(self, pipeline, src): + """Test trim""" + elem = transform.trim(pipeline, src) + assert_elem_props(elem, 'GSTLALTrim') + + @testtools.broken('Causes seg fault') + @testtools.requires_full_build + def test_interpolator(self, pipeline, src): + """Test interpolator""" + elem = transform.interpolator(pipeline, src) + assert_elem_props(elem, 'GSTLALInterpolator') diff --git a/gstlal/tests/tests_pytest/pipeparts/test_trigger.py b/gstlal/tests/tests_pytest/pipeparts/test_trigger.py new file mode 100644 index 0000000000000000000000000000000000000000..c2c2e9f587ab23b421c11ac19634a12a375d8d82 --- /dev/null +++ b/gstlal/tests/tests_pytest/pipeparts/test_trigger.py @@ -0,0 +1,59 @@ +"""Unittests for trigger pipeparts""" +import pytest + +from gstlal import gsttools +from gstlal.pipeparts import pipetools, trigger +from gstlal.pipeparts.pipetools import Gst, GObject +from gstlal.utilities import testtools + + +def assert_elem_props(elem, name): + assert gsttools.is_element(elem) + assert type(elem).__name__ == name + + +class TestTrigger: + """Group test for trigger elements""" + + @pytest.fixture(scope='class', autouse=True) + def pipeline(self): + GObject.MainLoop() + return Gst.Pipeline('TestEncode') + + @pytest.fixture(scope='class', autouse=True) + def src(self, pipeline): + return pipetools.make_element_with_src(pipeline, None, 'fakesrc') + + @testtools.requires_full_build + def test_trigger(self, pipeline, src): + """Test test multi file""" + elem = trigger.trigger(pipeline, src, 1) + assert_elem_props(elem, 'GSTLALTrigger') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_burst_trigger_gen(self, pipeline, src): + """Test burst gen""" + elem = trigger.burst_trigger_gen(pipeline, src) + assert_elem_props(elem, 'GstTrigger') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_blcbc_trigger_gen(self, pipeline, src): + """Test blcbc gen""" + elem = trigger.blcbc_trigger_gen(pipeline, src, 0.0, 'file', 0.0, 0.0) + assert_elem_props(elem, 'GstTrigger') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_trigger_gen(self, pipeline, src): + """Test gen""" + elem = trigger.trigger_gen(pipeline, src, 0.0, 'file', 0.0, 0.0) + assert_elem_props(elem, 'GstTrigger') + + @testtools.impl_deprecated + @testtools.requires_full_build + def test_itac(self, pipeline, src): + """Test """ + elem = trigger.itac(pipeline, src, 1, 'bank') + assert_elem_props(elem, 'GSTLALDenoiser') diff --git a/gstlal/tests/tests_pytest/test_datasource.py b/gstlal/tests/tests_pytest/test_datasource.py index eb58534d80d141b83fedfe3bd444e1e85b27ff6a..e769f79bb5aefa9b9a4d28670125f3da42f5f576 100644 --- a/gstlal/tests/tests_pytest/test_datasource.py +++ b/gstlal/tests/tests_pytest/test_datasource.py @@ -7,6 +7,8 @@ import pathlib import gi import pytest +from gstlal.utilities import testtools + gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst @@ -348,7 +350,7 @@ class TestDataSourceInfoBasicSource: assert_info_mkbasicsrc(info, Detector.H1.value) -@pytest.mark.requires_gstlal_ugly +@testtools.requires_full_build class TestDataSourceInfoBasicSourceUgly: """Test class for new DataSourceInfo api, these tests require gstlal-ugly to be build first so they are grouped separately @@ -398,7 +400,7 @@ class TestDataSourceInfoBasicSourceOptParse: These tests are the specific drop-in replacements for the old api usage in scripts """ - @pytest.mark.skip('Fake sources not currently working with Gst 1.0 api') + @testtools.broken('Fake sources not currently working with Gst 1.0 api') def test_mkbasicsrc_aligo(self): """Test that new api works with mkbasicsrc, datasource: AdvLIGO""" with GstLALTestManager() as gsttm: @@ -415,7 +417,7 @@ class TestDataSourceInfoBasicSourceOptParse: info = datasource.DataSourceInfo.from_optparse(options) assert_info_mkbasicsrc(info, Detector.H1.value) - @pytest.mark.skip('Fake sources not currently working with Gst 1.0 api') + @testtools.broken('Fake sources not currently working with Gst 1.0 api') def test_mkbasicsrc_avirgo(self): """Test that new api works with mkbasicsrc, datasource: AdvVirgo""" with GstLALTestManager() as gsttm: @@ -432,7 +434,7 @@ class TestDataSourceInfoBasicSourceOptParse: info = datasource.DataSourceInfo.from_optparse(options) assert_info_mkbasicsrc(info, Detector.H1.value) - @pytest.mark.skip('Fake sources not currently working with Gst 1.0 api') + @testtools.broken('Fake sources not currently working with Gst 1.0 api') def test_mkbasicsrc_ligo(self): """Test that new api works with mkbasicsrc""" with GstLALTestManager() as gsttm: @@ -482,7 +484,7 @@ class TestDataSourceInfoBasicSourceOptParse: assert_info_mkbasicsrc(info, Detector.H1.value) -@pytest.mark.requires_gstlal_ugly +@testtools.requires_full_build class TestDataSourceInfoBasicSourceOptParseUgly: """Test class wrapper for testing new datasource api optparse integration These tests are the specific drop-in replacements for the old api usage in scripts diff --git a/gstlal/tests/tests_pytest/test_gsttools.py b/gstlal/tests/tests_pytest/test_gsttools.py new file mode 100644 index 0000000000000000000000000000000000000000..9df946cf436fb2afd1d221ea7c7aaa52d89d3e2a --- /dev/null +++ b/gstlal/tests/tests_pytest/test_gsttools.py @@ -0,0 +1,24 @@ +"""Unit tests for pipetools module""" + +from gstlal import gsttools +from gstlal.pipeparts import pipetools +from gstlal.utilities import testtools + + +class TestGsttools: + """Test class group for gsttools""" + + def test_is_element(self): + """Test is_element utility""" + assert not gsttools.is_element(1) + with testtools.GstLALTestManager(with_pipeline=True) as tm: + elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc') + assert gsttools.is_element(elem) + + def test_is_pad(self): + """Test is_pad utility""" + assert not gsttools.is_element(1) + with testtools.GstLALTestManager(with_pipeline=True) as tm: + elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc') + assert not gsttools.is_pad(elem) + assert gsttools.is_pad(elem.pads[0])