gstlal 1.13.0
Loading...
Searching...
No Matches
condition.py
1# Copyright (C) 2009--2013 Kipp Cannon, Chad Hanna, Drew Keppel
2#
3# This program is free software; you can redistribute it and/or modify it
4# under the terms of the GNU General Public License as published by the
5# Free Software Foundation; either version 2 of the License, or (at your
6# option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11# Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17
18#
19# =============================================================================
20#
21# Preamble
22#
23# =============================================================================
24#
25
26
27import sys
28import os
29import optparse
30import math
31import numpy
32
33import gi
34gi.require_version('Gst', '1.0')
35gi.require_version("GstAudio", "1.0")
36from gi.repository import GObject
37from gi.repository import Gst, GstAudio
38
39import lal
40from ligo import segments
41
42from gstlal import kernels
43from gstlal import pipeparts
44from gstlal.psd import interpolate_psd
45from gstlal import pipeio
46
47__doc__ = """
48
49**Review Status**
50
51+------------------------------------------------+------------------------------------------+------------+
52| Names | Hash | Date |
53+================================================+==========================================+============+
54| Florent, Sathya, Duncan Me, Jolien, Kipp, Chad | 8a6ea41398be79c00bdc27456ddeb1b590b0f68e | 2014-06-18 |
55+------------------------------------------------+------------------------------------------+------------+
56"""
57
58# a macro to switch between a conventional whitener and a fir whitener below
59try:
60 if int(os.environ["GSTLAL_FIR_WHITEN"]):
61 FIR_WHITENER = True
62 else:
63 FIR_WHITENER = False
64except KeyError as e:
65 print("You must set the environment variable GSTLAL_FIR_WHITEN to either 0 or 1. 1 enables causal whitening. 0 is the traditional acausal whitening filter", file=sys.stderr)
66 raise
67
68
69def mkcondition(pipeline, src, target_rate, instrument, psd = None, psd_fft_length = 32, ht_gate_threshold = float("inf"), idq_gate_threshold = float("inf"), veto_segments = None, nxydump_segment = None, track_psd = False, block_duration = Gst.SECOND // 4, zero_pad = None, width = 64, statevector = None, dqvector = None, idq_series = None, idq_channel_name = None, fir_whiten_reference_psd = None, track_latency = False):
70 """
71 Build pipeline stage to whiten and downsample h(t).
72
73 * pipeline: the gstreamer pipeline to add this to
74 * src: the gstreamer element that will be providing data to this
75 * target_rate: the requested sample rate.
76 * instrument: the instrument to process
77 * psd: a psd frequency series
78 * psd_fft_length: length of fft used for whitening
79 * ht_gate_threshold: gate h(t) if it crosses this value
80 * veto_segments: segments to mark as gaps after whitening
81 * track_psd: decide whether to dynamically track the spectrum or use the fixed spectrum provided
82 * width: type convert to either 32 or 64 bit float
83 * fir_whiten_reference_psd: when using FIR whitener, use this PSD to define desired desired phase response
84
85 **Gstreamer graph describing this function**
86
87 .. graphviz::
88
89 digraph mkbasicsrc {
90 rankdir = LR;
91 compound=true;
92 node [shape=record fontsize=10 fontname="Verdana"];
93 edge [fontsize=8 fontname="Verdana"];
94
95 capsfilter1 ;
96 audioresample ;
97 capsfilter2 ;
98 reblock ;
99 whiten ;
100 audioconvert ;
101 capsfilter3 ;
102 "segmentsrcgate()" [label="segmentsrcgate() \\n [iff veto segment list provided]", style=filled, color=lightgrey];
103 tee ;
104 audioamplifyr1 ;
105 capsfilterr1 ;
106 htgater1 [label="htgate() \\n [iff ht gate specified]", style=filled, color=lightgrey];
107 tee1 ;
108 audioamplifyr2 ;
109 capsfilterr2 ;
110 htgater2 [label="htgate() \\n [iff ht gate specified]", style=filled, color=lightgrey];
111 tee2 ;
112 audioamplify_rn ;
113 capsfilter_rn ;
114 htgate_rn [style=filled, color=lightgrey, label="htgate() \\n [iff ht gate specified]"];
115 tee ;
116
117 // nodes
118
119 "<src>" -> capsfilter1 -> audioresample;
120 audioresample -> capsfilter2;
121 capsfilter2 -> reblock;
122 reblock -> whiten;
123 whiten -> audioconvert;
124 audioconvert -> capsfilter3;
125 capsfilter3 -> "segmentsrcgate()";
126 "segmentsrcgate()" -> tee;
127
128 tee -> audioamplifyr1 [label="Rate 1"];
129 audioamplifyr1 -> capsfilterr1;
130 capsfilterr1 -> htgater1;
131 htgater1 -> tee1 -> "<return> 1";
132
133 tee -> audioamplifyr2 [label="Rate 2"];
134 audioamplifyr2 -> capsfilterr2;
135 capsfilterr2 -> htgater2;
136 htgater2 -> tee2 -> "<return> 2";
137
138 tee -> audioamplify_rn [label="Rate N"];
139 audioamplify_rn -> capsfilter_rn;
140 capsfilter_rn -> htgate_rn;
141 htgate_rn -> tee_n -> "<return> 3";
142 }
143
144 """
145 #
146 # input sanity checks
147 #
148
149 if psd is None and not track_psd:
150 raise ValueError("must enable track_psd when psd is None")
151 if int(psd_fft_length) != psd_fft_length:
152 raise ValueError("psd_fft_length must be an integer")
153 psd_fft_length = int(psd_fft_length)
154 if int(block_duration) != block_duration:
155 raise ValueError("block duration must be an integer")
156
157 #
158 # set default whitener zero-padding if needed
159 #
160
161 if zero_pad is None:
162 if FIR_WHITENER:
163 # in this configuration we are not asking the
164 # whitener to reassemble an output time series
165 # (that we care about) so we disable zero-padding
166 # to get the most information from the whitener's
167 # FFT blocks.
168 zero_pad = 0
169 elif psd_fft_length % 4:
170 raise ValueError("default whitener zero-padding requires psd_fft_length to be multiple of 4")
171 else:
172 zero_pad = psd_fft_length // 4
173
174 #
175 # down-sample to highest of target sample rates. we include a caps
176 # filter upstream of the resampler to ensure that this is, infact,
177 # *down*-sampling. if the source time series has a lower sample
178 # rate than the highest target sample rate the resampler will
179 # become an upsampler, and the result will likely interact poorly
180 # with the whitener as it tries to ampify the non-existant
181 # high-frequency components, possibly adding significant numerical
182 # noise to its output. if you see errors about being unable to
183 # negotiate a format from this stage in the pipeline, it is because
184 # you are asking for output sample rates that are higher than the
185 # sample rate of your data source.
186 #
187
188 head = pipeparts.mkcapsfilter(pipeline, src, f"audio/x-raw, rate=[{target_rate:d},MAX]")
189 head = pipeparts.mkinterpolator(pipeline, head)
190 head = pipeparts.mkaudioconvert(pipeline, head)
191 head = pipeparts.mkchecktimestamps(pipeline, head, f"{instrument}_timestamps_{target_rate:d}_hoft")
192
193 #
194 # construct whitener.
195 #
196
197 if FIR_WHITENER:
198 head = pipeparts.mktee(pipeline, head)
199 whiten = pipeparts.mkwhiten(pipeline, head, fft_length = psd_fft_length, zero_pad = zero_pad, average_samples = 64, median_samples = 7, expand_gaps = True, name = "lal_whiten_%s" % instrument)
200 pipeparts.mkfakesink(pipeline, whiten)
201
202 # high pass filter
203 kernel = kernels.one_second_highpass_kernel(max(rates), cutoff = 12)
204 block_stride = block_duration * target_rate // Gst.SECOND
205 assert len(kernel) % 2 == 1, "high-pass filter length is not odd"
206 head = pipeparts.mkfirbank(pipeline, head, fir_matrix = numpy.array(kernel, ndmin = 2), block_stride = block_stride, time_domain = False, latency = (len(kernel) - 1) // 2)
207
208 # FIR filter for whitening kernel
209 head = pipeparts.mktdwhiten(pipeline, head, kernel = numpy.zeros(1 + target_rate * psd_fft_length, dtype=numpy.float64), latency = 0)
210
211 # compute whitening kernel from PSD
212 def set_fir_psd(whiten, pspec, firelem, psd_fir_kernel):
213 psd_data = numpy.array(whiten.get_property("mean-psd"))
214 psd = lal.CreateREAL8FrequencySeries(
215 name = "psd",
216 epoch = lal.LIGOTimeGPS(0),
217 f0 = 0.0,
218 deltaF = whiten.get_property("delta-f"),
219 sampleUnits = lal.Unit(whiten.get_property("psd-units")),
220 length = len(psd_data)
221 )
222 psd.data.data = psd_data
223 kernel, latency, sample_rate = psd_fir_kernel.psd_to_linear_phase_whitening_fir_kernel(psd)
224 kernel, phase = psd_fir_kernel.linear_phase_fir_kernel_to_minimum_phase_whitening_fir_kernel(kernel, sample_rate)
225 firelem.set_property("kernel", pipeio.format_property(kernel))
226 firkernel = kernels.PSDFirKernel()
227 if fir_whiten_reference_psd is not None:
228 assert fir_whiten_reference_psd.f0 == 0.
229 # interpolate the reference phase PSD if its
230 # resolution doesn't match what we'll eventually
231 # require it to be.
232 if psd_fft_length != round(1. / fir_whiten_reference_psd.deltaF):
233 fir_whiten_reference_psd = interpolate_psd(fir_whiten_reference_psd, 1. / psd_fft_length)
234 # confirm that the reference phase PSD's Nyquist is
235 # sufficiently high, then reduce it to the required
236 # Nyquist if needed.
237 assert (psd_fft_length * target_rate) // 2 + 1 <= len(fir_whiten_reference_psd.data.data), "fir_whiten_reference_psd Nyquist too low"
238 if (psd_fft_length * target_rate) // 2 + 1 < len(fir_whiten_reference_psd.data.data):
239 fir_whiten_reference_psd = lal.CutREAL8FrequencySeries(fir_whiten_reference_psd, 0, (psd_fft_length * target_rate) // 2 + 1)
240 # set the reference phase PSD
241 firkernel.set_phase(fir_whiten_reference_psd)
242 whiten.connect_after("notify::mean-psd", set_fir_psd, head, firkernel)
243
244 # Gate after gaps. the queue sizes on the control inputs
245 # need only be large enough to hold the state vector
246 # streams until they are required. the streams will be
247 # consumed immediately when needed, so there is no risk
248 # that these queues add to the latency, so make them
249 # generously large.
250 # FIXME the -target_rate extra padding is for the high pass
251 # filter: NOTE it also needs to be big enough for the
252 # downsampling filter, but that is typically smaller than the
253 # HP filter (192 samples at Qual 9)
254 # FIXME: this first queue should not be needed. what is
255 # going on!?
256 if statevector is not None or dqvector is not None:
257 head = pipeparts.mkqueue(pipeline, head, max_size_buffers = 0, max_size_bytes = 0, max_size_time = Gst.SECOND * (psd_fft_length + 2))
258 if statevector is not None:
259 head = pipeparts.mkgate(pipeline, head, control = pipeparts.mkqueue(pipeline, statevector, max_size_buffers = 0, max_size_bytes = 0, max_size_time = 0), default_state = False, threshold = 1, hold_length = -target_rate, attack_length = -target_rate * (psd_fft_length + 1))
260 if dqvector is not None:
261 head = pipeparts.mkgate(pipeline, head, control = pipeparts.mkqueue(pipeline, dqvector, max_size_buffers = 0, max_size_bytes = 0, max_size_time = 0), default_state = False, threshold = 1, hold_length = -target_rate, attack_length = -target_rate * (psd_fft_length + 1))
262 head = pipeparts.mkchecktimestamps(pipeline, head, "%s_timestamps_fir" % instrument)
263 #head = pipeparts.mknxydumpsinktee(pipeline, head, filename = "after_mkfirbank.txt")
264 else:
265 # FIXME: we should require fir_whiten_reference_psd to be
266 # None in this code path for safety, but that's hard to do
267 # since the calling code would need to know what
268 # environment variable is being used to select the mode,
269 # and we don't want to be duplicating that code all over
270 # the place
271
272 #
273 # add a reblock element. the whitener's gap support isn't
274 # 100% yet and giving it smaller input buffers works around
275 # the remaining weaknesses (namely that when it sees a gap
276 # buffer large enough to drain its internal history, it
277 # doesn't know enough to produce a short non-gap buffer to
278 # drain its history followed by a gap buffer, it just
279 # produces one huge non-gap buffer that's mostly zeros).
280 # this is not required in the FIR-whitener case because
281 # there we don't use the whitener's output time series for
282 # anything.
283 #
284
285 head = pipeparts.mkreblock(pipeline, head, block_duration = block_duration)
286
287 head = whiten = pipeparts.mkwhiten(pipeline, head, fft_length = psd_fft_length, zero_pad = zero_pad, average_samples = 64, median_samples = 7, expand_gaps = True, name = "lal_whiten_%s" % instrument)
288 # make the buffers going downstream smaller, this can
289 # really help with RAM
290 head = pipeparts.mkreblock(pipeline, head, block_duration = block_duration)
291
292 #
293 # enable/disable PSD tracking
294 #
295
296 whiten.set_property("psd-mode", 0 if track_psd else 1)
297
298 #
299 # install signal handler to retrieve \Delta f and f_{Nyquist}
300 # whenever they are known and/or change, resample the user-supplied
301 # PSD, and install it into the whitener.
302 #
303
304 if psd is not None:
305 def psd_units_or_resolution_changed(elem, pspec, psd):
306 # make sure units are set, compute scale factor
307 units = lal.Unit(elem.get_property("psd-units"))
308 if units == lal.DimensionlessUnit:
309 return
310 scale = float(psd.sampleUnits / units)
311 # get frequency resolution and number of bins
312 delta_f = elem.get_property("delta-f")
313 n = int(round(elem.get_property("f-nyquist") / delta_f) + 1)
314 # interpolate, rescale, and install PSD
315 psd = interpolate_psd(psd, delta_f)
316 elem.set_property("mean-psd", pipeio.format_property(psd.data.data[:n] * scale))
317 whiten.connect_after("notify::f-nyquist", psd_units_or_resolution_changed, psd)
318 whiten.connect_after("notify::delta-f", psd_units_or_resolution_changed, psd)
319 whiten.connect_after("notify::psd-units", psd_units_or_resolution_changed, psd)
320
321 #
322 # convert to desired precision
323 #
324
325 head = pipeparts.mkaudioconvert(pipeline, head)
326 if width == 64:
327 head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d, format=%s" % (target_rate, GstAudio.AudioFormat.to_string(GstAudio.AudioFormat.F64)))
328 elif width == 32:
329 head = pipeparts.mkcapsfilter(pipeline, head, "audio/x-raw, rate=%d, format=%s" % (target_rate, GstAudio.AudioFormat.to_string(GstAudio.AudioFormat.F32)))
330 else:
331 raise ValueError("invalid width: %d" % width)
332 head = pipeparts.mkchecktimestamps(pipeline, head, "%s_timestamps_%d_whitehoft" % (instrument, target_rate))
333
334 #
335 # optionally add vetoes
336 #
337
338 if veto_segments is not None:
339 long_veto_segments = segments.segmentlist(veto_segments).protract(0.25).coalesce()
340 head = pipeparts.mkdeglitcher(pipeline, head, long_veto_segments)
341
342 #
343 # optional gate on whitened h(t) amplitude controlled by iDQ timeseries.
344 # Must come before the h(t) self gating b/c output of h(t) gating is
345 # used to define state of instruments in analysis.
346 # attack and hold are made to be 1/2 second or 1 sample, whichever is larger
347 #
348 idq_gate_window = max(target_rate // 4, 1) # samples
349 if idq_series is not None and idq_gate_threshold is not None:
350 if idq_channel_name:
351 _, channel_type = idq_channel_name.split('-')
352 invert_idq_control = channel_type.split('_')[0] != 'FAP'
353 #control = pipeparts.mkintegrate(pipeline, idq_series, 1.0, name='gstlalintegrate_%s'%instrument)
354 control = pipeparts.mkqueue(pipeline, idq_series, max_size_time = 12 * Gst.SECOND, max_size_bytes = 0, max_size_buffers = 0)
355 head = mkhtgate(pipeline, head, control = control, threshold = idq_gate_threshold, hold_length = idq_gate_window, attack_length = idq_gate_window, name = "%s_idq_gate" % instrument, invert_control = invert_idq_control)
356
357 #
358 # optional gate on whitened h(t) amplitude. attack and hold are
359 # made to be 1/2 second or 1 sample, whichever is larger
360 #
361
362 # FIXME: this could be omitted if ht_gate_threshold is None, but
363 # we need to collect whitened h(t) segments, however something
364 # could be done to collect those if these gates aren't here.
365 ht_gate_window = max(target_rate // 2, 1) # samples
366 head = mkhtgate(pipeline, head, threshold = ht_gate_threshold if ht_gate_threshold is not None else float("+inf"), hold_length = ht_gate_window, attack_length = ht_gate_window, name = "%s_ht_gate" % instrument)
367 # emit signals so that a user can latch on to them
368 head.set_property("emit-signals", True)
369
370 if track_latency:
371 head = pipeparts.mklatency(pipeline, head, name = "%s_whitening_latency" % instrument, silent = True)
372
373 return head
374
375
376def mkmultiband(pipeline, head, rates, instrument = None, unit_normalize = True):
377 """
378 Build pipeline stage to multiband a stream.
379
380 * rates: a list of the requested sample rates, e.g., [512,1024].
381
382 """
383 #
384 # tee for highest sample rate stream
385 #
386
387 head = {max(rates): pipeparts.mktee(pipeline, head)}
388
389 #
390 # down-sample whitened time series to remaining target sample rates
391 # while applying an amplitude correction to adjust for low-pass
392 # filter roll-off. we also scale by \sqrt{original rate / new
393 # rate}. this is done to preserve the square magnitude of the time
394 # series --- the inner product of the time series with itself.
395 # really what we want is for
396 #
397 # \int v_{1}(t) v_{2}(t) \diff t
398 # \approx \sum v_{1}(t) v_{2}(t) \Delta t
399 #
400 # to be preserved across different sample rates, i.e. for different
401 # \Delta t. what we do is rescale the time series and ignore
402 # \Delta t, so we put 1/2 factor of the ratio of the \Delta t's
403 # into the h(t) time series here, and, later, another 1/2 factor
404 # into the template when it gets downsampled.
405 #
406 # by design, the output of the whitener is a unit-variance time
407 # series. however, downsampling it reduces the variance due to the
408 # removal of some frequency components. we require the input to
409 # the orthogonal filter banks to be unit variance, therefore a
410 # correction factor is applied via an audio amplify element to
411 # adjust for the reduction in variance due to the downsampler.
412 #
413
414 for rate in sorted(set(rates))[:-1]:
415 # downsample. make sure each output stream is unit
416 # normalized, otherwise the audio resampler removes power
417 # according to the rate difference and filter rolloff
418 if unit_normalize:
419 # NOTE the interpolator is about as good as the
420 # audioresampler at quality 10, hence the 10.
421 head[rate] = pipeparts.mkaudioamplify(pipeline, head[max(rates)], 1. / math.sqrt(pipeparts.audioresample_variance_gain(10, max(rates), rate)))
422 else:
423 head[rate] = head[max(rates)]
424 head[rate] = pipeparts.mkcapsfilter(pipeline, pipeparts.mkinterpolator(pipeline, head[rate]), caps = "audio/x-raw, rate=%d" % rate)
425 head[rate] = pipeparts.mkchecktimestamps(pipeline, head[rate], "%s_timestamps_%d_whitehoft" % (instrument, rate))
426
427 head[rate] = pipeparts.mktee(pipeline, head[rate])
428
429 #
430 # done. return value is a dictionary of tee elements indexed by
431 # sample rate
432 #
433
434 return head
435
436
437def mkhtgate(pipeline, src, control = None, threshold = 8.0, attack_length = 128, hold_length = 128, invert_control = True, **kwargs):
438 """
439 A convenience function to provide thresholds on input data. This can
440 be used to remove large spikes / glitches etc. Of course you can use it for
441 other stuff by plugging whatever you want as input and ouput
442
443 NOTE: the queues constructed by this code assume the attack and
444 hold lengths combined are less than 1 second in duration.
445
446 **Gstreamer Graph**
447
448 .. graphviz::
449
450 digraph G {
451 compound=true;
452 node [shape=record fontsize=10 fontname="Verdana"];
453 rankdir=LR;
454 tee ;
455 inputqueue ;
456 lal_gate ;
457 in [label="<src>"];
458 out [label="<return>"];
459 in -> tee -> inputqueue -> lal_gate -> out;
460 tee -> lal_gate;
461 }
462
463 """
464 # FIXME someday explore a good bandpass filter
465 # src = pipeparts.mkaudiochebband(pipeline, src, low_frequency, high_frequency)
466 if control is None:
467 control = src = pipeparts.mktee(pipeline, src)
468 src = pipeparts.mkqueue(pipeline, src, max_size_time = Gst.SECOND, max_size_bytes = 0, max_size_buffers = 0)
469 return pipeparts.mkgate(pipeline, src, control = control, threshold = threshold, attack_length = -attack_length, hold_length = -hold_length, invert_control = invert_control, **kwargs)
470