gstlal  1.4.1
gstlal_fake_frames
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2011 Kipp Cannon, Chad Hanna, Drew Keppel
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 
19 import numpy
20 from optparse import OptionParser
21 import os
22 import sys
23 
24 import gi
25 gi.require_version('Gst', '1.0')
26 from gi.repository import GObject, Gst
27 GObject.threads_init()
28 Gst.init(None)
29 
30 import lal
31 
32 from gstlal import pipeparts
33 from gstlal import reference_psd
34 from gstlal import simplehandler
35 from gstlal import datasource
36 from gstlal import multirate_datasource
37 from glue.ligolw import utils as ligolw_utils
38 
39 ### This program will make fake data in a variety of ways. Its input is anything
40 ### supported by datasource.py. You can additionally whiten the data or apply a
41 ### frequency domain coloring filter. It writes its output to frame files.
42 ###
43 ### Overview graph of the pipeline
44 ### ------------------------------
45 ###
46 ### Gray boxes are optional and depend on the command line given
47 ###
48 ### .. graphviz::
49 ###
50 ### digraph G {
51 ###
52 ### // graph properties
53 ###
54 ### rankdir=LR;
55 ### compound=true;
56 ### node [shape=record fontsize=10 fontname="Verdana"];
57 ### edge [fontsize=8 fontname="Verdana"];
58 ###
59 ### // nodes
60 ###
61 ### "mkbasicsrc()" [URL="\ref datasource.mkbasicsrc()"];
62 ### "whitened_multirate_src()" [label="whitened_multirate_src()", URL="\ref multirate_datasource.mkwhitened_multirate_src()", style=filled, color=lightgrey];
63 ### capsfilter [style=filled, color=lightgrey, URL="\ref pipeparts.mkcapsfilter()"];
64 ### resample [style=filled, color=lightgrey, URL="\ref pipeparts.mkresample()"];
65 ### audioamplify [style=filled, color=lightgrey, URL="\ref pipeparts.mkaudioamplify()", label="audioamplify \n if --data-source=white \n in order to have unit variance \n after resampling"];
66 ### lal_shift[style=filled, color=lightgrey, URL="\ref pipeparts.mkshift()", label="lal_shift \n [iff --shift provided]"];
67 ### progressreport1 [URL="\ref pipeparts.mkprogressreport()", label="progressreport \n [iff --verbose provided]"];
68 ### progressreport2 [style=filled, color=lightgrey, URL="\ref pipeparts.mkprogressreport()", label="progressreport \n [iff --verbose provided]"];
69 ### lal_firbank [style=filled, color=lightgrey, URL="\ref pipeparts.mkfirbank()", label="lal_firbank \n [iff --color-psd provided]"];
70 ### taginject [URL="\ref pipeparts.mktaginject()"];
71 ### lal_simulation [style=filled, color=lightgrey, URL="\ref pipeparts.mkinjections()", label="lal_simulation \n [iff --injections provided]"];
72 ### framecppchannelmux [URL="\ref pipeparts.mkframecppchannelmux()"];
73 ### framecppfilesink [URL="\ref pipeparts.mkframecppfilesink()"];
74 ###
75 ### // connect it up
76 ###
77 ### "mkbasicsrc()" -> progressreport1-> lal_shift -> progressreport2;
78 ### progressreport2 -> "whitened_multirate_src()" [label="if --whiten given"];
79 ### "whitened_multirate_src()" -> lal_firbank;
80 ### progressreport2 -> resample [label="if --whiten not given"];
81 ### resample -> capsfilter;
82 ### capsfilter -> audioamplify
83 ### audioamplify -> lal_firbank;
84 ### lal_firbank -> taginject -> lal_simulation -> framecppchannelmux -> framecppfilesink;
85 ### }
86 ###
87 ###
88 ### Usage cases
89 ### -----------
90 ###
91 ### 1. Making initial LIGO colored noise, Advanced LIGO noise, etc for different
92 ### instruments. See datasource.append_options() for other choices::
93 ###
94 ### $ gstlal_fake_frames --data-source=LIGO --channel-name=H1=FAKE-STRAIN \
95 ### --frame-type=H1_FAKE --gps-start-time=900000000 --gps-end-time=900005000 \
96 ### --output-path=testframes --verbose
97 ###
98 ### $ gstlal_fake_frames --data-source=AdvLIGO --channel-name=L1=FAKE-STRAIN \
99 ### --frame-type=L1_FAKE --gps-start-time=900000000 --gps-end-time=900005000 \
100 ### --output-path=testframes --verbose
101 ###
102 ###
103 ### 2. Custom colored noise.
104 ###
105 ### Obviously it is not practical to code up every possible noise curve to
106 ### simulate as a custom data source. However, it is possible to provide your
107 ### own custom noise curve as an ASCII file with frequency in one column and
108 ### strain/Hz in the second. e.g., early advanced ligo noise curve
109 ### https://git.ligo.org/lscsoft/gstlal/raw/master/gstlal/share/early_aligo_asd.txt
110 ### You will need to convert ASD text files to PSD xml files using
111 ### gstlal_psd_xml_from_asd_txt first.::
112 ###
113 ### $ gstlal_fake_frames --data-source=white --color-psd v1psd.xml.gz \
114 ### --channel-name=V1=FAKE-STRAIN --frame-type=V1_FAKE --gps-start-time=900000000 \
115 ### --gps-end-time=900005000 --output-path=testframes --verbose
116 ###
117 ###
118 ### 3. Recoloring existing frame data
119 ###
120 ### This procedure is very similar to the above except that instead of
121 ### using white noise to drive the fake frame generation, we start with
122 ### real frame data and whiten it. Recoloring can be thought of as first
123 ### whitening data and then applying a coloring filter. You must first
124 ### make a reference PSD of the data with e.g. gstlal_reference_psd. You
125 ### will need to make a frame cache of the frames to recolor.::
126 ###
127 ### $ gstlal_fake_frames --whiten-reference-psd reference_psd.xml.gz \
128 ### --color-psd recolor_psd.xml.gz --data-source frames \
129 ### --output-path /path/to/output --output-channel-name h_16384Hz \
130 ### --gps-start-time 966384031 --frame-type T1300121_V1_EARLY_RECOLORED_V2 \
131 ### --gps-end-time 966389031 --frame-duration 16 --frames-per-file 256 \
132 ### --frame-cache frame.cache --channel-name=V1=h_16384Hz --verbose
133 ###
134 ###
135 ### 4. Creating injections into silence (assumes you have an injection xml file from e.g. lalapps_inspinj)::
136 ###
137 ### $ gstlal_fake_frames --data-source silence --output-path /path/to/output \
138 ### --gps-start-time 966384031 --frame-type V1_INJECTIONS \
139 ### --gps-end-time 966389031 --frame-duration 16 --frames-per-file 256 \
140 ### --verbose --channel-name=V1=INJECTIONS --injections injections.xml
141 ###
142 ###
143 ### 5. Other things are certainly possible, please add some!
144 ###
145 ### Debug
146 ### -----
147 ###
148 ### It is possible to check the pipeline graph by interupting the running process
149 ### with ctrl+c if you have the GST_DEBUG_DUMP_DOT_DIR evironment set. A dot
150 ### graph will be written to gstlal_fake_frames. Here is an example::
151 ###
152 ### $ GST_DEBUG_DUMP_DOT_DIR="." gstlal_fake_frames --data-source=LIGO \
153 ### --channel-name=H1=FAKE-STRAIN --frame-type=H1_FAKE \
154 ### --gps-start-time=900000000 --gps-end-time=900005000 \
155 ### --output-path=testframes --verbose``
156 ###
157 ### You should see::
158 ###
159 ### Wrote pipeline to ./gstlal_fake_frames_PLAYING.dot
160 ###
161 ### After Ctrl+c you should see::
162 ###
163 ### ^C*** SIG 2 attempting graceful shutdown (this might take several minutes) ... ***
164 ### Wrote pipeline to ./gstlal_fake_frames.dot
165 ###
166 ### You can then turn the pipeline graph into an image with dot, e.g.,::
167 ###
168 ### $ dot -Tpng gstlal_fake_frames_PLAYING.dot > test.png
169 ###
170 ### Review
171 ### ------
172 ###
173 
174 
175 ##
176 # Extract and validate the command line options
177 #
178 def parse_command_line():
179  parser = OptionParser(description = __doc__)
180 
181  #
182  # Append data source options
183  #
184 
185  datasource.append_options(parser)
186 
187  #
188  # Append program specific options
189  #
190 
191  parser.add_option("--shift", metavar = "ns", help = "Number of nanoseconds to delay (negative) or advance (positive) the time stream", type = "int")
192  parser.add_option("--sample-rate", metavar = "Hz", default = 16384, type = "int", help = "Sample rate at which to generate the data, should be less than or equal to the sample rate of the measured psds provided, default = 16384 Hz, max 16384 Hz")
193  parser.add_option("--whiten-reference-psd", metavar = "name", help = "Set the name of psd xml file to whiten the data with")
194  parser.add_option("--whiten-track-psd", action = "store_true", help = "Calculate PSD from input data and track with time. Always enabled if --whiten-reference-psd is not given.")
195  parser.add_option("--color-psd", metavar = "name", help = "Set the name of psd xml file to color the data with")
196  parser.add_option("--output-path", metavar = "name", default = ".", help = "Path to output frame files (default = \".\").")
197  parser.add_option("--output-channel-name", metavar = "name", help = "The name of the channel in the output frames. The default is the same as the channel name")
198  parser.add_option("--frame-type", metavar = "name", help = "Frame type, required")
199  parser.add_option("--frame-duration", metavar = "s", default = 16, type = "int", help = "Set the duration of the output frames. The duration of the frame file will be multiplied by --frames-per-file. Default: 16s")
200  parser.add_option("--frames-per-file", metavar = "n", default = 256, type = "int", help = "Set the number of frames per file. Default: 256")
201  parser.add_option("-v", "--verbose", action = "store_true", help = "Be verbose (optional).")
202 
203  #
204  # Parse options
205  #
206 
207  options, filenames = parser.parse_args()
208 
209  if options.sample_rate > 16384:
210  raise ValueError("--sample-rate must be <= 16384")
211 
212  if options.frame_type is None:
213  raise ValueError("--frame-type is required")
214 
215  if options.whiten_reference_psd is None:
216  options.whiten_track_psd = True
217 
218  return options, filenames
219 
220 
221 #
222 # Main
223 #
224 
225 options, filenames = parse_command_line()
226 
227 ## Parse the command line options into a python.datasource.GWDataSourceInfo class instance
228 gw_data_source = datasource.GWDataSourceInfo(options)
229 
230 ## Assume instrument is the first and only key of the python.datasource.GWDataSourceInfo.channel_dict
231 instrument = gw_data_source.channel_dict.keys()[0]
232 
233 # disable progress reports if not verbose
234 if not options.verbose:
235  pipeparts.mkprogressreport = lambda pipeline, src, *args: src
236 
237 # set default output channel if not set by user
238 if options.output_channel_name is None:
239  options.output_channel_name = gw_data_source.channel_dict[instrument]
240 
241 # do not do injections in datasource.mkbasicsrc(), we will do them later
242 injections, gw_data_source.injection_filename = options.injections, None
243 
244 ## Setup the pipeline
245 pipeline = Gst.Pipeline(name=os.path.split(sys.argv[0])[1])
246 
247 ## Main loop
248 mainloop = GObject.MainLoop()
249 
250 ## An instance of the python.simplehandler.Handler class
251 handler = simplehandler.Handler(mainloop, pipeline)
252 
253 ## 1) Set the pipeline head to basic input from datasource.mkbasicsrc()
254 # FIXME: fake source causes problems when making large buffers, so block_size needs to be overwritten
255 gw_data_source.block_size = 8 * options.sample_rate
256 head, _, _ = datasource.mkbasicsrc(pipeline, gw_data_source, instrument, verbose = options.verbose)
257 
258 ## 2) Set the pipeline head to be verbose with pipeparts.mkprogressreport()
259 head = pipeparts.mkprogressreport(pipeline, head, "frames")
260 
261 if options.shift is not None:
262  ## 3) Set the pipeline head to apply a time shift if requested with a pipeparts.mkshift() element
263  head = pipeparts.mkshift(pipeline, head, shift = options.shift)
264 
265  ## 4) Set the pipeline head to be verbose with a pipeparts.mkprogressreport() element
266  head = pipeparts.mkprogressreport(pipeline, head, "frames_shifted")
267 
268 if options.whiten_reference_psd:
269  ## If whitening read the PSD
270  wpsd = lal.series.read_psd_xmldoc(ligolw_utils.load_filename(options.whiten_reference_psd, verbose = options.verbose, contenthandler = lal.series.PSDContentHandler))[instrument]
271 else:
272  ## else set wpsd to None
273  wpsd = None
274 
275 if options.whiten_reference_psd or options.whiten_track_psd:
276  ## 5) Set the pipeline head to a whitened data stream if requested using a multirate_datasource.mkwhitened_multirate_src()
277  head = multirate_datasource.mkwhitened_multirate_src(pipeline, head, [options.sample_rate], instrument, psd = wpsd, track_psd = options.whiten_track_psd)[options.sample_rate]
278 else:
279  ## 6) Otherwise simply add a pipeparts.mkcapsfilter() and pipeparts.mkresample()
280  head = pipeparts.mkcapsfilter(pipeline, pipeparts.mkresample(pipeline, head, quality = 9), "audio/x-raw, rate=%d" % options.sample_rate)
281  # FIXME this is a bit hacky, should datasource.mkbasicsrc be patched to change the sample rate?
282  # FIXME don't hardcode sample rate (though this is what datasource generates for all fake data, period
283  # Renormalize if datasource is "white"
284  if options.data_source == "white":
285  head = pipeparts.mkaudioamplify(pipeline, head, 1.0 / (pipeparts.audioresample_variance_gain(9, 16384, options.sample_rate))**.5)
286 
287 # Apply a coloring filter
288 if options.color_psd:
289 
290  ## read coloring psd file and convert to an FIR filter
291  rpsd = lal.series.read_psd_xmldoc(ligolw_utils.load_filename(options.color_psd, verbose = options.verbose, contenthandler = lal.series.PSDContentHandler))[instrument]
292 
293  ## Calculate the maximum sample rate
294  max_sample = int(round(1.0 / rpsd.deltaF * options.sample_rate / 2.0)) + 1
295 
296  # Truncate to requested output sample rate, if it is higher than the psd provides an assert will fail later
297  rpsd_data = numpy.array(rpsd.data.data[:max_sample])
298  rpsd = lal.CreateCOMPLEX16FrequencySeries(name = rpsd.name, epoch = rpsd.epoch, f0 = rpsd.f0, deltaF = rpsd.deltaF, length = max_sample, sampleUnits = rpsd.sampleUnits)
299  rpsd.data.data = rpsd_data
300 
301  # create the coloring FIR kernel from reference_psd.psd_to_fir_kernel()
302  FIRKernel = reference_psd.PSDFirKernel()
303  fir_matrix, latency, measured_sample_rate = FIRKernel.psd_to_linear_phase_whitening_fir_kernel(rpsd, invert = False)
304 
305  ## 7) Set the pipeline head to a pipeparts.mkfirbank() recoloring filter
306  head = pipeparts.mkfirbank(pipeline, head, latency = latency, fir_matrix = [fir_matrix], block_stride = 1 * options.sample_rate)
307 
308 
309 ## Set the tags for the output data
310 tagstr = "units=strain,channel-name=%s,instrument=%s" % (options.output_channel_name, instrument)
311 
312 ## 8) Put the units back to strain before writing to frames. Additionally, override the output channel name if provided from the command line
313 head = pipeparts.mktaginject(pipeline, head, tagstr)
314 
315 
316 if injections is not None:
317  ## 9) Do injections into the final fake data
318  head = pipeparts.mkinjections(pipeline, head, injections)
319 
320 if not os.path.isdir(options.output_path):
321  try:
322  os.makedirs(options.output_path)
323  except:
324  print >> sys.stderr, "Unable to make directory ", options.output_path
325  pass
326 else:
327  print "Target output directory already exists."
328 
329 ## 10) create frames
330 head = pipeparts.mkframecppchannelmux(pipeline, {"%s:%s" % (instrument, options.output_channel_name): head}, frame_duration = options.frame_duration, frames_per_file = options.frames_per_file)
331 
332 ## 11) Write the frames to disk
333 head = pipeparts.mkframecppfilesink(pipeline, head, frame_type = options.frame_type)
334 
335 # Put O(100000 s) frames in each directory
336 head.connect("notify::timestamp", pipeparts.framecpp_filesink_ldas_path_handler, (options.output_path, 5))
337 
338 # Run it
339 if pipeline.set_state(Gst.State.READY) == Gst.StateChangeReturn.FAILURE:
340  raise RuntimeError("pipeline failed to enter READY state")
341 datasource.pipeline_seek_for_gps(pipeline, gw_data_source.seg[0], gw_data_source.seg[1])
342 if pipeline.set_state(Gst.State.PLAYING) == Gst.StateChangeReturn.FAILURE:
343  raise RuntimeError("pipeline failed to enter PLAYING state")
344 
345 ## Debugging output
346 if "GST_DEBUG_DUMP_DOT_DIR" in os.environ:
347  pipeparts.write_dump_dot(pipeline, "%s_PLAYING" % pipeline.get_name(), verbose = True)
348 
349  ## Setup a signal handler to intercept SIGINT in order to write the pipeline graph at ctrl+C before cleanly shutting down
350  class SigHandler(simplehandler.OneTimeSignalHandler):
351  def do_on_call(self, signum, frame):
352  pipeparts.write_dump_dot(pipeline, "%s_SIGINT" % pipeline.get_name(), verbose = True)
353 
354  sighandler = SigHandler(pipeline)
355 
356 mainloop.run()