20 Generate strain time series of a continuous-wave signal in the detector frame,
21 given a function which computes signal phase and amplitudes as functions of time.
23 The given function should compute quantities \f$\Delta\phi(t)\f$, \f$a_+(t)\f$,
24 and \f$a_\times(t)\f$ such that the plus and cross gravitational-wave
25 polarisations \f$h_+(t)\f$ and \f$h_\times(t)\f$ are given by:
27 h_+(t) & = & a_+(t) \cos[\phi_0 + \Delta\phi(t)] \; , \\
28 h_\times(t) & = & a_\times(t)\sin[\phi_0 + \Delta\phi(t)] \; .
31 This module provides a class CWSimulator() to generate the strain time series.
32 CWSimulator() can also directly write frame files and SFT files. Example usage:
36 from lalpulsar import simulateCW
38 def waveform(h0, cosi, freq, f1dot):
40 dphi = lal.TWOPI * (freq * dt + f1dot * 0.5 * dt**2)
41 ap = h0 * (1.0 + cosi**2) / 2.0
60 wf = waveform(h0, cosi, freq, f1dot)
61 S = simulateCW.CWSimulator(tref, tstart, Tdata, wf, dt_wf, phi0, psi, alpha, delta, detector)
64 for file, i, N in S.write_sft_files(fmax=32, Tsft=1800, comment="simCW"):
65 print('Generated SFT file %s (%i of %i)' % (file, i+1, N))
67 # To write frame files
68 for file, i, N in S.write_frame_files(fs=1, Tframe=1800, comment="simCW"):
69 print('Generated frame file %s (%i of %i)' % (file, i+1, N))
75 from __future__
import division, print_function
85 from .
import git_version
87 __author__ =
"Karl Wette <karl.wette@ligo.org>"
88 __version__ = git_version.id
89 __date__ = git_version.date
105 earth_ephem_file="earth00-40-DE405.dat.gz",
106 sun_ephem_file="sun00-40-DE405.dat.gz",
111 Initialise a continuous-wave signal simulator.
113 @param tref: reference time of signal phase at Solar System barycentre, in GPS seconds
114 (but see @b tref_at_det)
115 @param tstart: start time of signal, in GPS seconds
116 @param Tdata: total duration of signal, in seconds
117 @param waveform: function which computes signal phase and amplitudes as functions of time:
118 @b dphi, @b aplus, @b across = @b waveform(dt), where:
119 @b dt = time since reference time @b tref;
120 @b dphi = phase of signal at time @b dt relative to reference time @b tref, in radians;
121 @b aplus = strain amplitude of plus polarisation at time @b dt;
122 @b across = strain amplitude of cross polarisation at time @b dt
123 @param dt_wf: sampling time of the function @c waveform; this need only be small enough to ensure
124 that @c dphi, @c aplus, and @c across are smoothly interpolated, and does not need to make
125 the desired sampling frequency of the output strain time series
126 @param phi0: initial phase of the gravitational-wave signal at @b tstart, in radians
127 @param psi: polarisation angle of the gravitational-wave source, in radians
128 @param alpha: right ascension of the gravitational-wave source, in radians
129 @param delta: declination of the gravitational-wave source, in radians
130 @param det_name: name of the gravitational-wave detector to simulate a response for;
131 e.g. @c "H1" for LIGO Hanford, @c "L1" for LIGO Livingston, @c "V1" for Virgo
132 @param earth_ephem_file: name of file to load Earth ephemeris from
133 @param sun_ephem_file: name of file to load Sun ephemeris from
134 @param tref_at_det: default False; if True, shift reference time @b tref so that @b dt = 0 is
135 @b tref in @e detector frame instead of Solar System barycentre frame, useful if e.g. one
136 wants to turn on signal only for @b dt > 0, one can use @b tref as the turn-on time
137 @param extra_comment: additional text to add to comment string in frame/SFT headers
138 (not filenames), e.g. for wrapper script commandlines
141 self.
__origin_str__origin_str =
"Generated by %s, %s-%s (%s)" % (
156 _, self.
__site_index__site_index = lalpulsar.FindCWDetector(det_name,
True)
159 raise ValueError(
"Invalid detector name det_name='%s'" % det_name)
163 self.
__ephemerides__ephemerides = lalpulsar.InitBarycenter(earth_ephem_file, sun_ephem_file)
167 bary_state = lalpulsar.EarthState()
168 bary_input = lalpulsar.BarycenterInput()
169 bary_emit = lalpulsar.EmissionTime()
170 bary_input.tgps = tref
171 bary_input.site = self.
__site__site
172 for i
in range(0, 3):
173 bary_input.site.location[i] /= lal.C_SI
174 bary_input.alpha = alpha
175 bary_input.delta = delta
176 bary_input.dInv = 0.0
177 lalpulsar.BarycenterEarth(bary_state, bary_input.tgps, self.
__ephemerides__ephemerides)
178 lalpulsar.Barycenter(bary_emit, bary_input, bary_state)
181 tref += bary_emit.deltaT
187 Tpad_wf = 2.0 * lal.AU_SI / lal.C_SI
188 tstart_wf = tstart - Tpad_wf
189 Nwf =
int(math.ceil(float(Tdata + 2 * Tpad_wf) / float(dt_wf)))
192 self.
__phi__phi = lal.CreateREAL8TimeSeries(
193 "phi", tstart_wf, 0, dt_wf, lal.DimensionlessUnit, Nwf
198 self.
__a__a = lal.REAL4TimeVectorSeries()
199 self.
__a__a.name =
"a+,ax"
200 self.
__a__a.epoch = tstart_wf
201 self.
__a__a.deltaT = dt_wf
203 self.
__a__a.sampleUnits = lal.StrainUnit
204 self.
__a__a.data = lal.CreateREAL4VectorSequence(Nwf, 2)
207 dt = float(tstart_wf - tref)
208 for i
in range(0, Nwf):
209 dphi, aplus, across = waveform(dt)
210 self.
__phi__phi.data.data[i] = phi0 + dphi
211 self.
__a__a.data.data[i][0] = aplus
212 self.
__a__a.data.data[i][1] = across
216 self.
__wf__wf = lalpulsar.PulsarCoherentGW()
217 self.
__wf__wf.position.system = lal.COORDINATESYSTEM_EQUATORIAL
218 self.
__wf__wf.position.longitude = alpha
219 self.
__wf__wf.position.latitude = delta
220 self.
__wf__wf.psi = psi
225 self.
__detector__detector = lalpulsar.PulsarDetectorResponse()
229 def _simulate_coherent_gw(self, h, noise_sqrt_Sh, noise_seed):
231 lalpulsar.PulsarSimulateCoherentGW(h, self.
__wf__wf, self.
__detector__detector)
234 if noise_sqrt_Sh > 0:
235 assert noise_seed
is not None
236 assert noise_seed > 0
237 noise_sigma = math.sqrt(0.5 / h.deltaT) * noise_sqrt_Sh
238 lalpulsar.AddGaussianNoise(h, noise_sigma, noise_seed)
240 def get_strain(self, fs, tmin=0, tmax=None, noise_sqrt_Sh=0, noise_seed=None):
242 Generate strain time series of a continuous-wave signal in the detector frame.
244 @param fs: sampling frequency of strain time series, in Hz
245 @param tmin: start time for strain time series, as offsets from self.__tstart
246 @param tmax: start time for strain time series, as offsets from self.__tstart
247 @param noise_sqrt_Sh: if >0, add Gaussian noise with square-root single-sided power
248 spectral density given by this value, in Hz^(-1/2)
249 @param noise_seed: use this seed for the random number generator used to create noise;
250 required if noise_sqrt_Sh >0
252 @return (@b t, @b h), where:
253 @b t = start of time strain time series, in GPS seconds;
254 @b h = strain time series
258 if (tmin < 0)
or (tmin >= self.
__Tdata__Tdata):
259 raise ValueError(
"tmin must be within [0,{}).".format(self.
__Tdata__Tdata))
262 elif (tmax <= 0)
or (tmax > self.
__Tdata__Tdata):
263 raise ValueError(
"tmax must be within (0,{}].".format(self.
__Tdata__Tdata))
268 h = lal.CreateREAL4TimeSeries(
269 "h", self.
__tstart__tstart + tmin, 0, 1.0 / fs, lal.DimensionlessUnit, Nh
274 return (h.epoch, h.data.data)
278 Generate strain time series of a continuous-wave signal in the detector frame, in contiguous blocks.
280 @param fs: sampling frequency of strain time series, in Hz
281 @param Tblock: length of each block, in seconds; should divide evenly into @b Tdata
282 @param noise_sqrt_Sh: if >0, add Gaussian noise with square-root single-sided power
283 spectral density given by this value, in Hz^(-1/2)
284 @param noise_seed: use this seed for the random number generator used to create noise;
285 if None, generate a random seed using os.urandom()
287 @return (@b t, @b h, @b i, @b N), where:
288 @b t = start of time strain time series, in GPS seconds;
289 @b h = strain time series;
290 @b i = block index, starting from zero;
291 @b N = number of blocks
293 This is a Python generator function and so should be called as follows:
296 for t, h, i, N in S.get_strain_blocks(...):
302 Nblock =
int(round(self.
__Tdata__Tdata / Tblock))
303 if Tblock * Nblock > self.
__Tdata__Tdata:
305 "Length of block Tblock=%g does not divide evenly into Tdata=%g"
306 % (Tblock, self.
__Tdata__Tdata)
312 while not (0 < noise_seed
and noise_seed < lal.LAL_INT4_MAX - Nblock):
313 noise_seed = int.from_bytes(os.urandom(4), sys.byteorder, signed=
False)
317 for iblock
in range(0, Nblock):
322 noise_sqrt_Sh=noise_sqrt_Sh,
323 noise_seed=noise_seed + iblock,
325 yield epoch, hoft, iblock, Nblock
329 self, fs, Tframe, comment="simCW", out_dir=".", noise_sqrt_Sh=0, noise_seed=None
332 Write frame files [1] containing strain time series of a continuous-wave signal.
334 The strain time series is written as double-precision post-processed data (ProcData) channel named
335 <tt><detector>:SIMCW-STRAIN</tt>,
336 where <tt><detector></tt> is the 2-character detector prefix (e.g. <tt>H1</tt> for LIGO Hanford,
337 <tt>L1</tt> for LIGO Livingston, <tt>V1</tt> for Virgo).
339 @param fs: sampling frequency of strain time series, in Hz
340 @param Tframe: length of each frame, in seconds; should divide evenly into @b Tdata
341 @param comment: frame file name comment, may only contain A-Z, a-z, 0-9, _, +, # characters
342 @param out_dir: output directory to write frame files into
343 @param noise_sqrt_Sh: if >0, add Gaussian noise with square-root single-sided power
344 spectral density given by this value, in Hz^(-1/2)
345 @param noise_seed: use this seed for the random number generator used to create noise;
346 if None, generate a random seed using os.urandom()
348 @return (@b file, @b i, @b N), where:
349 @b file = name of frame file just written;
350 @b i = frame file index, starting from zero;
351 @b N = number of frame files
353 This is a Python generator function and so should be called as follows:
356 for file, i, N in S.write_frame_files(...):
360 [1] https://dcc.ligo.org/LIGO-T970130/public
366 raise ImportError(
"SWIG wrappings of LALFrame cannot be imported")
369 valid_comment = re.compile(
r"^[A-Za-z0-9_+#]+$")
370 if not valid_comment.match(comment):
372 "Frame file comment='%s' may only contain A-Z, a-z, 0-9, _, +, # characters"
379 fs, Tframe, noise_sqrt_Sh=noise_sqrt_Sh, noise_seed=noise_seed
383 frame_h_channel =
"%s:SIMCW-STRAIN" % self.
__site__site.frDetector.prefix
384 frame_h = lal.CreateREAL8TimeSeries(
385 frame_h_channel, t, 0, 1.0 / fs, lal.DimensionlessUnit, len(h)
388 frame_h.data.data = h
391 frame_src = self.
__site__site.frDetector.prefix[0]
393 frame_t0 =
int(t.gpsSeconds)
394 frame_Tdata =
int(math.ceil(float(t + Tframe)) - math.floor(float(t)))
395 frame_name =
"%s-%s-%u-%u.gwf" % (
401 frame_path = os.path.join(out_dir, frame_name)
405 frame = lalframe.FrameNew(
406 t, Tframe, self.__class__.__name__, -1, i, frame_det_bits
410 lalframe.FrameAddREAL8TimeSeriesProcData(frame, frame_h)
413 lalframe.FrameAddFrHistory(frame,
"origin", self.
__origin_str__origin_str)
416 lalframe.FrameWrite(frame, frame_path)
419 yield frame_path, i, N
427 window="rectangular",
431 Generate SFTs [2] containing strain time series of a continuous-wave signal.
433 @param fmax: maximum SFT frequency, in Hz
434 @param Tsft: length of each SFT, in seconds; should divide evenly into @b Tdata
435 @param noise_sqrt_Sh: if >0, add Gaussian noise with square-root single-sided power
436 spectral density given by this value, in Hz^(-1/2)
437 @param noise_seed: use this seed for the random number generator used to create noise;
438 if None, generate a random seed using os.urandom()
439 @param window: if not None, window the time series before performing the FFT, using
440 the named window function; see XLALCreateNamedREAL8Window()
441 @param window_param: parameter for the window function given by @b window, if needed
443 @return (@b sft, @b i, @b N), where:
445 @b i = SFT file index, starting from zero;
446 @b N = number of SFTs
448 This is a Python generator function and so should be called as follows:
451 for sft, i, N in S.get_sfts(...):
455 [2] https://dcc.ligo.org/LIGO-T040164/public
459 sft_ts = lalpulsar.CreateTimestampVector(1)
466 sft_fs, Tsft, noise_sqrt_Sh=noise_sqrt_Sh, noise_seed=noise_seed
470 sft_name = self.
__site__site.frDetector.prefix
471 sft_h = lal.CreateREAL8TimeSeries(
472 sft_name, t, 0, 1.0 / sft_fs, lal.DimensionlessUnit, len(h)
479 sft_vect = lalpulsar.MakeSFTsFromREAL8TimeSeries(
480 sft_h, sft_ts, window, window_param
484 yield sft_vect.data[0], i, N
494 window="rectangular",
498 Write SFT files [2] containing strain time series of a continuous-wave signal.
500 @param fmax: maximum SFT frequency, in Hz
501 @param Tsft: length of each SFT, in seconds; should divide evenly into @b Tdata
502 @param comment: SFT file name comment, may only contain A-Z, a-z, 0-9 characters
503 @param out_dir: output directory to write SFT files into
504 @param noise_sqrt_Sh: if >0, add Gaussian noise with square-root single-sided power
505 spectral density given by this value, in Hz^(-1/2)
506 @param noise_seed: use this seed for the random number generator used to create noise;
507 if None, generate a random seed using os.urandom()
508 @param window: if not None, window the time series before performing the FFT, using
509 the named window function; see XLALCreateNamedREAL8Window()
510 @param window_param: parameter for the window function given by @b window, if needed
512 @return (@b file, @b i, @b N), where:
513 @b file = name of SFT file just written;
514 @b i = SFT file index, starting from zero;
515 @b N = number of SFT files
517 This is a Python generator function and so should be called as follows:
520 for file, i, N in S.write_sft_files(...):
524 [2] https://dcc.ligo.org/LIGO-T040164/public
528 valid_comment = re.compile(
r"^[A-Za-z0-9]+$")
529 if not valid_comment.match(comment):
531 "SFT file comment='%s' may only contain A-Z, a-z, 0-9 characters"
536 spec = lalpulsar.SFTFilenameSpec()
538 spec.window_type = window
539 spec.window_param = window_param
540 spec.privMisc = comment
543 for sft, i, N
in self.
get_sftsget_sfts(
546 noise_sqrt_Sh=noise_sqrt_Sh,
547 noise_seed=noise_seed,
549 window_param=window_param,
552 lalpulsar.WriteSFT2StandardFile(sft, spec, self.
__origin_str__origin_str)
555 sft_path = lalpulsar.BuildSFTFilenameFromSpec(spec)
def get_sfts(self, fmax, Tsft, noise_sqrt_Sh=0, noise_seed=None, window="rectangular", window_param=0)
Generate SFTs [2] containing strain time series of a continuous-wave signal.
def write_sft_files(self, fmax, Tsft, comment="simCW", out_dir=".", noise_sqrt_Sh=0, noise_seed=None, window="rectangular", window_param=0)
Write SFT files [2] containing strain time series of a continuous-wave signal.
def get_strain(self, fs, tmin=0, tmax=None, noise_sqrt_Sh=0, noise_seed=None)
Generate strain time series of a continuous-wave signal in the detector frame.
def write_frame_files(self, fs, Tframe, comment="simCW", out_dir=".", noise_sqrt_Sh=0, noise_seed=None)
Write frame files [1] containing strain time series of a continuous-wave signal.
def get_strain_blocks(self, fs, Tblock, noise_sqrt_Sh=0, noise_seed=None)
Generate strain time series of a continuous-wave signal in the detector frame, in contiguous blocks.
def _simulate_coherent_gw(self, h, noise_sqrt_Sh, noise_seed)
def __init__(self, tref, tstart, Tdata, waveform, dt_wf, phi0, psi, alpha, delta, det_name, earth_ephem_file="earth00-40-DE405.dat.gz", sun_ephem_file="sun00-40-DE405.dat.gz", tref_at_det=False, extra_comment=None)
Initialise a continuous-wave signal simulator.