-
Notifications
You must be signed in to change notification settings - Fork 29
/
core.py
2605 lines (2336 loc) · 98.6 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
""" The core tools used in pyfstat """
import getpass
import glob
import logging
import os
import socket
from datetime import datetime
from pprint import pformat
from weakref import finalize
import lal
import lalpulsar
import numpy as np
import scipy.optimize
import scipy.special
import pyfstat.tcw_fstat_map_funcs as tcw
import pyfstat.utils as utils
from ._version import get_versions
plt = utils.safe_X_less_plt()
logger = logging.getLogger(__name__)
detector_colors = {"h1": "C0", "l1": "C1"}
class BaseSearchClass:
"""The base class providing parent methods to other PyFstat classes.
This does not actually have any 'search' functionality,
which needs to be added by child classes
along with full initialization and any other custom methods.
"""
binary_keys = ["asini", "period", "ecc", "tp", "argp"]
"""List of extra parameters for sources in binaries."""
default_search_keys = [
"F0",
"F1",
"F2",
"Alpha",
"Delta",
]
"""Default order of the traditionally supported search parameter names.
FIXME: these are only used as fallbacks for the deprecated style
of passing keys one by one;
not needed when using the new parameters dictionary.
"""
tex_labels = {
# standard Doppler parameters
"F0": r"$f$",
"F1": r"$\dot{f}$",
"F2": r"$\ddot{f}$",
"F3": r"$\dddot{f}$",
"Alpha": r"$\alpha$",
"Delta": r"$\delta$",
# binary parameters
"asini": r"$\mathrm{asin}\,i$",
"period": r"$P$",
"ecc": r"$\mathrm{ecc}$",
"tp": r"$t_p$",
"argp": r"$\mathrm{argp}$",
# transient parameters
"transient_tstart": r"$t_\mathrm{start}$",
"transient_duration": r"$\Delta T$",
# glitch parameters
"delta_F0": r"$\delta f$",
"delta_F1": r"$\delta \dot{f}$",
"tglitch": r"$t_\mathrm{glitch}$",
# detection statistics
"twoF": r"$\widetilde{2\mathcal{F}}$",
"maxTwoF": r"$\max\widetilde{2\mathcal{F}}$",
"log10BSGL": r"$\log_{10}\mathcal{B}_{\mathrm{SGL}}$",
"lnBtSG": r"$\ln\mathcal{B}_{\mathrm{tS/G}}$",
}
"""Formatted labels used for plot annotations."""
unit_dictionary = dict(
# standard Doppler parameters
F0=r"Hz",
F1=r"Hz/s",
F2=r"Hz/s$^2$",
F3=r"Hz/s$^3$",
Alpha=r"rad",
Delta=r"rad",
# binary parameters
asini="",
period=r"s",
ecc="",
tp=r"s",
argp="",
# transient parameters
transient_tstart=r"s",
transient_duration=r"s",
# glitch parameters
delta_F0=r"Hz",
delta_F1=r"Hz/s",
tglitch=r"s",
)
"""Units for standard parameters."""
fmt_detstat = "%.9g"
"""Standard output precision for detection statistics."""
fmt_doppler = "%.16g"
"""Standard output precision for Doppler (frequency evolution) parameters."""
def __new__(cls, *args, **kwargs):
logger.info(f"Creating {cls.__name__} object...")
instance = super().__new__(cls)
return instance
def _get_list_of_matching_sfts(self):
"""Returns a list of sfts matching the attribute sftfilepattern"""
sftfilepatternlist = np.atleast_1d(self.sftfilepattern.split(";"))
matches = [glob.glob(p) for p in sftfilepatternlist]
matches = [item for sublist in matches for item in sublist]
if len(matches) > 0:
return matches
else: # pragma: no cover
raise IOError("No sfts found matching {}".format(self.sftfilepattern))
def tex_label0(self, key):
"""Formatted labels used for annotating central values in plots."""
label = self.tex_labels[key].strip("$")
return f"${label} - {label}_0$"
def set_ephemeris_files(self, earth_ephem=None, sun_ephem=None):
"""Set the ephemeris files to use for the Earth and Sun.
NOTE: If not given explicit arguments,
default values from utils.get_ephemeris_files()
are used.
Parameters
----------
earth_ephem, sun_ephem: str
Paths of the two files containing positions of Earth and Sun,
respectively at evenly spaced times, as passed to CreateFstatInput
"""
earth_ephem_default, sun_ephem_default = utils.get_ephemeris_files()
self.earth_ephem = earth_ephem or earth_ephem_default
self.sun_ephem = sun_ephem or sun_ephem_default
def _set_init_params_dict(self, argsdict):
"""Store the initial input arguments, e.g. for logging output."""
argsdict.pop("self")
self.init_params_dict = argsdict
def pprint_init_params_dict(self):
"""Pretty-print a parameters dictionary for output file headers.
Returns
-------
pretty_init_parameters: list
A list of lines to be printed,
including opening/closing "{" and "}",
consistent indentation,
as well as end-of-line commas,
but no comment markers at start of lines.
"""
pretty_init_parameters = pformat(
self.init_params_dict, indent=2, width=74
).split("\n")
pretty_init_parameters = (
["{"]
+ [pretty_init_parameters[0].replace("{", " ")]
+ pretty_init_parameters[1:-1]
+ [pretty_init_parameters[-1].rstrip("}")]
+ ["}"]
)
return pretty_init_parameters
def get_output_file_header(self):
"""Constructs a meta-information header for text output files.
This will include
PyFstat and LALSuite versioning,
information about when/where/how the code was run,
and input parameters of the instantiated class.
Returns
-------
header: list
A list of formatted header lines.
"""
header = [
"date: {}".format(str(datetime.now())),
"user: {}".format(getpass.getuser()),
"hostname: {}".format(socket.gethostname()),
"PyFstat: {}".format(get_versions()["version"]),
]
lalVCSinfo = lal.VCSInfoString(lalpulsar.PulsarVCSInfoList, 0, "")
header += filter(None, lalVCSinfo.split("\n"))
header += [
"search: {}".format(type(self).__name__),
"parameters: ",
]
header += self.pprint_init_params_dict()
return header
def read_par(
self, filename=None, label=None, outdir=None, suffix="par", raise_error=True
):
"""Read a `key=val` file and return a dictionary.
Parameters
----------
filename: str or None
Filename (path) containing rows of `key=val` data to read in.
label, outdir, suffix : str or None
If filename is None, form the file to read as `outdir/label.suffix`.
raise_error : bool
If True, raise an error for lines which are not comments,
but cannot be read.
Returns
-------
params_dict: dict
A dictionary of the parsed `key=val` pairs.
"""
params_dict = utils.read_par(
filename=filename,
label=label or getattr(self, "label", None),
outdir=outdir or getattr(self, "outdir", None),
suffix=suffix,
raise_error=raise_error,
)
return params_dict
@staticmethod
def translate_keys_to_lal(dictionary):
"""Convert input keys into lalpulsar convention.
In PyFstat's convention, input keys (search parameter names)
are F0, F1, F2, ...,
while lalpulsar functions prefer to use Freq, f1dot, f2dot, ....
Since lalpulsar keys are only used internally to call lalpulsar routines,
this function is provided so the keys can be translated on the fly.
Parameters
----------
dictionary: dict
Dictionary to translate. A copy will be made (and returned)
before translation takes place.
Returns
-------
translated_dict: dict
Copy of "dictionary" with new keys according to lalpulsar convention.
"""
translation = {
"F0": "Freq",
"F1": "f1dot",
"F2": "f2dot",
"phi": "phi0",
"tref": "refTime",
"asini": "orbitasini",
"period": "orbitPeriod",
"tp": "orbitTp",
"argp": "orbitArgp",
"ecc": "orbitEcc",
"transient_tstart": "transient-t0Epoch",
"transient_duration": "transient-tau",
}
keys_to_translate = [key for key in dictionary.keys() if key in translation]
translated_dict = dictionary.copy()
for key in keys_to_translate:
translated_dict[translation[key]] = translated_dict.pop(key)
return translated_dict
class ComputeFstat(BaseSearchClass):
"""Base search class providing an interface to `lalpulsar.ComputeFstat`.
In most cases, users should be using one of the higher-level search classes
from the grid_based_searches or mcmc_based_searches modules instead.
See the lalpulsar documentation at https://lscsoft.docs.ligo.org/lalsuite/lalpulsar/group___compute_fstat__h.html
and R. Prix, The F-statistic and its implementation in ComputeFstatistic_v2 ( https://dcc.ligo.org/T0900149/public )
for details of the lalpulsar module and the meaning of various technical concepts
as embodied by some of the class's parameters.
Normally this will read in existing data through the `sftfilepattern` argument,
but if that option is `None` and the necessary alternative arguments are used,
it can also generate simulated data (including noise and/or signals) on the fly.
NOTE that the detection statistics that can be computed from an instance of this class
depend on the `BSGL`, `BtSG` and `transientWindowType` arguments given at initialisation.
See `get_fullycoherent_detstat()` and `get_transient_detstats()` for details.
To change what you want to compute,
you may need to initialise a new instance with different options.
NOTE for GPU users (`tCWFstatMapVersion="pycuda"`):
This class tries to conveniently deal with GPU context management behind the scenes.
A known problematic case is if you try to instantiate it twice from the same
session/script. If you then get some messages like
`RuntimeError: make_default_context()`
and `invalid device context`,
that is because the GPU is still blocked from the first instance when
you try to initiate the second.
To avoid this problem, use context management::
with pyfstat.ComputeFstat(
[...],
tCWFstatMapVersion="pycuda",
) as search:
search.get_fullycoherent_detstat([...])
or manually call the `search.finalizer_()` method where needed.
"""
@utils.initializer
def __init__(
self,
tref,
sftfilepattern=None,
minStartTime=None,
maxStartTime=None,
Tsft=1800,
binary=False,
singleFstats=False,
BSGL=False,
BtSG=False,
transientWindowType=None,
t0Band=None,
tauBand=None,
tauMin=None,
dt0=None,
dtau=None,
detectors=None,
minCoverFreq=None,
maxCoverFreq=None,
search_ranges=None,
injectSources=None,
injectSqrtSX=None,
randSeed=None,
assumeSqrtSX=None,
SSBprec=None,
RngMedWindow=None,
tCWFstatMapVersion="lal",
cudaDeviceName=None,
computeAtoms=False,
earth_ephem=None,
sun_ephem=None,
allowedMismatchFromSFTLength=None,
):
"""
Parameters
----------
tref : int
GPS seconds of the reference time.
sftfilepattern : str
Pattern to match SFTs using wildcards (`*?`) and ranges [0-9];
multiple patterns can be given separated by colons.
minStartTime, maxStartTime : int
Only use SFTs with timestamps starting from within this range,
following the XLALCWGPSinRange convention:
half-open intervals [minStartTime,maxStartTime].
Tsft: int
SFT duration in seconds.
Only required if `sftfilepattern=None` and hence simulted data is
generated on the fly.
binary : bool
If true, search over binary parameters.
singleFstats : bool
If true, also compute the single-detector twoF values.
BSGL : bool
If true, compute the log10BSGL statistic rather than the twoF value.
For details, see Keitel et al (PRD 89, 064023, 2014):
https://arxiv.org/abs/1311.5738
Note this automatically sets `singleFstats=True` as well.
Tuning parameters are currently hardcoded:
* `Fstar0=15` for coherent searches.
* A p-value of 1e-6 and correspondingly recalculated Fstar0
for semicoherent searches.
* Uniform per-detector prior line-vs-Gaussian odds.
BtSG: bool
If true and `transientWindowType` is not `None`,
compute the transient
:math:`\\ln\\mathcal{B}_{\\mathrm{tS}/\\mathrm{G}}`
statistic from Prix, Giampanis & Messenger (PRD 84, 023007, 2011)
(tCWFstatMap marginalised over uniform t0, tau priors).
rather than the maxTwoF value.
transientWindowType: str
If `rect` or `exp`,
allow for the Fstat to be computed over a transient range.
(`none` instead of `None` explicitly calls the transient-window
function, but with the full range, for debugging.)
(If not None, will also force atoms regardless of computeAtoms option.)
t0Band, tauBand: int
Search ranges for transient start-time t0 and duration tau.
If >0, search t0 in (minStartTime,minStartTime+t0Band)
and tau in (tauMin,2*Tsft+tauBand).
If =0, only compute the continuous-wave Fstat with t0=minStartTime,
tau=maxStartTime-minStartTime.
tauMin: int
Minimum transient duration to cover,
defaults to 2*Tsft.
dt0: int
Grid resolution in transient start-time,
defaults to Tsft.
dtau: int
Grid resolution in transient duration,
defaults to Tsft.
detectors : str
Two-character references to the detectors for which to use data.
Specify `None` for no constraint.
For multiple detectors, separate by commas.
minCoverFreq, maxCoverFreq : float
The min and max cover frequency passed to lalpulsar.CreateFstatInput.
For negative values, these will be used as offsets from the min/max
frequency contained in the sftfilepattern.
If either is `None`, the search_ranges argument is used to estimate them.
If the automatic estimation fails and you do not have a good idea
what to set these two options to, setting both to -0.5 will
reproduce the default behaviour of PyFstat <=1.4 and may be a
reasonably safe fallback in many cases.
search_ranges: dict
Dictionary of ranges in all search parameters,
only used to estimate frequency band passed to lalpulsar.CreateFstatInput,
if minCoverFreq, maxCoverFreq are not specified (==`None`).
For actually running searches,
grids/points will have to be passed separately to the .run() method.
The entry for each parameter must be a list of length 1, 2 or 3:
[single_value], [min,max] or [min,max,step].
injectSources : dict or str
Either a dictionary of the signal parameters to inject,
or a string pointing to a .cff file defining a signal.
injectSqrtSX : float or list or str
Single-sided PSD values for generating fake Gaussian noise on the fly.
Single float or str value: use same for all IFOs.
List or comma-separated string: must match len(detectors)
and/or the data in sftfilepattern.
Detectors will be paired to list elements following alphabetical order.
randSeed : int or None
random seed for on-the-fly noise generation using `injectSqrtSX`.
Setting this to 0 or None is equivalent; both will randomise the seed,
following the behaviour of XLALAddGaussianNoise(),
while any number not equal to 0 will produce a reproducible noise realisation.
assumeSqrtSX : float or list or str
Don't estimate noise-floors but assume this (stationary) single-sided PSD.
Single float or str value: use same for all IFOs.
List or comma-separated string: must match len(detectors)
and/or the data in sftfilepattern.
Detectors will be paired to list elements following alphabetical order.
If working with signal-only data, please set assumeSqrtSX=1 .
SSBprec : int
Flag to set the Solar System Barycentring (SSB) calculation in lalpulsar:
0=Newtonian, 1=relativistic,
2=relativistic optimised, 3=DMoff, 4=NO_SPIN
RngMedWindow : int
Running-Median window size for F-statistic noise normalization
(number of SFT bins).
tCWFstatMapVersion: str
Choose between implementations of the transient F-statistic functionality:
standard `lal` implementation,
`pycuda` for GPU version,
and some others only for devel/debug.
cudaDeviceName: str
GPU name to be matched against drv.Device output,
only for `tCWFstatMapVersion=pycuda`.
computeAtoms: bool
Request calculation of 'F-statistic atoms' regardless of transientWindowType.
earth_ephem: str
Earth ephemeris file path.
If None, will check standard sources as per
utils.get_ephemeris_files().
sun_ephem: str
Sun ephemeris file path.
If None, will check standard sources as per
utils.get_ephemeris_files().
allowedMismatchFromSFTLength: float
Maximum allowed mismatch from SFTs being too long
[Default: what's hardcoded in XLALFstatMaximumSFTLength]
"""
self._setup_finalizer()
self._set_init_params_dict(locals())
self.set_ephemeris_files(earth_ephem, sun_ephem)
self.init_computefstatistic()
self.output_file_header = self.get_output_file_header()
self.get_det_stat = self.get_fullycoherent_detstat
self.allowedMismatchFromSFTLength = allowedMismatchFromSFTLength
def _setup_finalizer(self):
"""
Setup for proper cleanup at end of context in pycuda case.
Users should normally *not* have to call self._finalizer() manually:
the `finalize` call is enough to set up python garbage collection,
and we only store it as an attribute for debugging/testing purposes.
However, if one wants to initialise two or more of these objects from
a single script, one has to manually clean up after each one by either
using context management or calling the `.finalizer_()` method.
"""
if "cuda" in self.tCWFstatMapVersion:
logger.debug(
f"Setting up GPU context finalizer for {self.tCWFstatMapVersion} transient maps."
)
self._finalizer = finalize(self, self._finalize_gpu_context)
def _finalize_gpu_context(self):
"""Clean up at the end of context manager style usage."""
logger.debug("Leaving the ComputeFStat context...")
if hasattr(self, "gpu_context") and self.gpu_context:
logger.debug("Detaching GPU context...")
# this is needed because we use pyCuda without autoinit
self.gpu_context.detach()
def __enter__(self):
"""Enables context manager style calling."""
logger.debug("Entering the ComputeFstat context...")
return self
def __exit__(self, *args, **kwargs):
"""Clean up at the end of context manager style usage."""
logger.debug("Leaving the ComputeFStat context...")
if "cuda" in self.tCWFstatMapVersion:
self._finalizer()
def _get_SFTCatalog(self):
"""Load the SFTCatalog
If sftfilepattern is specified, load the data. If not, attempt to
create data on the fly.
"""
if hasattr(self, "SFTCatalog"):
logger.info("Already have SFTCatalog.")
return
if self.sftfilepattern is None:
logger.info("No sftfilepattern given, making fake SFTCatalog.")
for k in ["minStartTime", "maxStartTime", "detectors"]:
if getattr(self, k) is None:
raise ValueError(
"If sftfilepattern==None, you must provide" " '{}'.".format(k)
)
C1 = getattr(self, "injectSources", None) is None
C2 = getattr(self, "injectSqrtSX", None) is None
C3 = getattr(self, "Tsft", None) is None
if (C1 and C2) or C3:
raise ValueError(
"If sftfilepattern==None, you must specify Tsft and"
" either one of injectSources or injectSqrtSX."
)
SFTCatalog = lalpulsar.SFTCatalog()
Toverlap = 0
self.detector_names = self.detectors.split(",")
self.numDetectors = len(self.detector_names)
detNames = lal.CreateStringVector(*[d for d in self.detector_names])
# MakeMultiTimestamps follows the same [minStartTime,maxStartTime)
# convention as the SFT library, so we can pass Tspan like this
Tspan = self.maxStartTime - self.minStartTime
multiTimestamps = lalpulsar.MakeMultiTimestamps(
self.minStartTime, Tspan, self.Tsft, Toverlap, detNames.length
)
SFTCatalog = lalpulsar.MultiAddToFakeSFTCatalog(
SFTCatalog, detNames, multiTimestamps
)
self.SFTCatalog = SFTCatalog
return
logger.info("Initialising SFTCatalog from sftfilepattern.")
constraints = lalpulsar.SFTConstraints()
constr_str = []
if self.detectors:
if "," in self.detectors:
logger.warning(
"Multiple-detector constraints not available,"
" using all available data."
)
else:
constraints.detector = self.detectors
constr_str.append("detector=" + constraints.detector)
if self.minStartTime:
constraints.minStartTime = lal.LIGOTimeGPS(self.minStartTime)
constr_str.append("minStartTime={}".format(self.minStartTime))
if self.maxStartTime:
constraints.maxStartTime = lal.LIGOTimeGPS(self.maxStartTime)
constr_str.append("maxStartTime={}".format(self.maxStartTime))
logger.info(
"Loading data matching SFT file name pattern '{}'"
" with constraints {}.".format(self.sftfilepattern, ", ".join(constr_str))
)
self.SFTCatalog = lalpulsar.SFTdataFind(self.sftfilepattern, constraints)
if self.SFTCatalog.length == 0:
raise IOError("No SFTs found.")
Tsft_from_catalog = int(1.0 / self.SFTCatalog.data[0].header.deltaF)
if Tsft_from_catalog != self.Tsft:
logger.info(
"Overwriting pre-set Tsft={:d} with {:d} obtained from SFTs.".format(
self.Tsft, Tsft_from_catalog
)
)
self.Tsft = Tsft_from_catalog
# NOTE: in multi-IFO case, this will be a joint list of timestamps
# over all IFOs, probably sorted and not cleaned for uniqueness.
SFT_timestamps = [d.header.epoch for d in self.SFTCatalog.data]
self.SFT_timestamps = [float(s) for s in SFT_timestamps]
if len(SFT_timestamps) == 0:
raise ValueError("Failed to load any data")
dtstr1 = utils.gps_to_datestr_utc(int(SFT_timestamps[0]))
dtstr2 = utils.gps_to_datestr_utc(int(SFT_timestamps[-1]))
logger.info(
f"Data contains SFT timestamps from {SFT_timestamps[0]} ({dtstr1})"
f" to (including) {SFT_timestamps[-1]} ({dtstr2})."
)
if self.minStartTime is None:
self.minStartTime = int(SFT_timestamps[0])
if self.maxStartTime is None:
# XLALCWGPSinRange() convention: half-open intervals,
# maxStartTime must always be > last actual SFT timestamp
self.maxStartTime = int(SFT_timestamps[-1]) + self.Tsft
self.detector_names = list(set([d.header.name for d in self.SFTCatalog.data]))
self.numDetectors = len(self.detector_names)
if self.numDetectors == 0:
raise ValueError("No data loaded.")
logger.info(
"Loaded {} SFTs from {} detectors: {}".format(
len(SFT_timestamps), self.numDetectors, self.detector_names
)
)
def init_computefstatistic(self):
"""Initialization step for the F-stastic computation internals.
This sets up the special input and output structures the lalpulsar module needs,
the ephemerides,
optional on-the-fly signal injections,
and extra options for multi-detector consistency checks and transient searches.
All inputs are taken from the pre-initialized object,
so this function does not have additional arguments of its own.
"""
self._get_SFTCatalog()
# some sanity checks on user options
if self.BSGL: # pragma: no cover
if len(self.detector_names) < 2:
raise ValueError("Can't use BSGL with single detector data")
if getattr(self, "BtSG", False):
raise ValueError("Please choose only one of [BSGL,BtSG].")
logger.info("Initialising ephems")
ephems = lalpulsar.InitBarycenter(self.earth_ephem, self.sun_ephem)
logger.info("Initialising Fstat arguments")
dFreq = 0
self.whatToCompute = lalpulsar.FSTATQ_2F
if self.transientWindowType or self.computeAtoms:
self.whatToCompute += lalpulsar.FSTATQ_ATOMS_PER_DET
FstatOAs = lalpulsar.FstatOptionalArgs()
if self.SSBprec:
logger.info("Using SSBprec={}".format(self.SSBprec))
FstatOAs.SSBprec = self.SSBprec
else:
FstatOAs.SSBprec = lalpulsar.FstatOptionalArgsDefaults.SSBprec
FstatOAs.Dterms = lalpulsar.FstatOptionalArgsDefaults.Dterms
if self.RngMedWindow:
FstatOAs.runningMedianWindow = self.RngMedWindow
else:
FstatOAs.runningMedianWindow = (
lalpulsar.FstatOptionalArgsDefaults.runningMedianWindow
)
FstatOAs.FstatMethod = lalpulsar.FstatOptionalArgsDefaults.FstatMethod
if self.assumeSqrtSX is None:
FstatOAs.assumeSqrtSX = lalpulsar.FstatOptionalArgsDefaults.assumeSqrtSX
else:
mnf = lalpulsar.MultiNoiseFloor()
assumeSqrtSX = utils.parse_list_of_numbers(self.assumeSqrtSX)
mnf.sqrtSn[: len(assumeSqrtSX)] = assumeSqrtSX
mnf.length = len(assumeSqrtSX)
FstatOAs.assumeSqrtSX = mnf
FstatOAs.prevInput = lalpulsar.FstatOptionalArgsDefaults.prevInput
FstatOAs.collectTiming = lalpulsar.FstatOptionalArgsDefaults.collectTiming
if self.allowedMismatchFromSFTLength:
FstatOAs.allowedMismatchFromSFTLength = self.allowedMismatchFromSFTLength
if hasattr(self, "injectSources") and isinstance(self.injectSources, dict):
logger.info("Injecting source with params: {}".format(self.injectSources))
PPV = lalpulsar.CreatePulsarParamsVector(1)
PP = PPV.data[0]
h0 = self.injectSources["h0"]
cosi = self.injectSources["cosi"]
use_aPlus = "aPlus" in dir(PP.Amp)
if use_aPlus: # lalsuite interface changed in aff93c45
PP.Amp.aPlus = 0.5 * h0 * (1.0 + cosi**2)
PP.Amp.aCross = h0 * cosi
else:
PP.Amp.h0 = h0
PP.Amp.cosi = cosi
PP.Amp.phi0 = self.injectSources["phi"]
PP.Amp.psi = self.injectSources["psi"]
PP.Doppler.Alpha = self.injectSources["Alpha"]
PP.Doppler.Delta = self.injectSources["Delta"]
if "fkdot" in self.injectSources:
PP.Doppler.fkdot = np.array(self.injectSources["fkdot"])
else:
PP.Doppler.fkdot = np.zeros(lalpulsar.PULSAR_MAX_SPINS)
for i, key in enumerate(["F0", "F1", "F2"]):
PP.Doppler.fkdot[i] = self.injectSources[key]
PP.Doppler.refTime = self.tref
if "t0" not in self.injectSources:
PP.Transient.type = lalpulsar.TRANSIENT_NONE
FstatOAs.injectSources = PPV
elif hasattr(self, "injectSources") and isinstance(self.injectSources, str):
logger.info(
"Injecting source from param file: {}".format(self.injectSources)
)
PPV = lalpulsar.PulsarParamsFromFile(self.injectSources, self.tref)
FstatOAs.injectSources = PPV
else:
FstatOAs.injectSources = lalpulsar.FstatOptionalArgsDefaults.injectSources
if hasattr(self, "injectSqrtSX") and self.injectSqrtSX is not None:
self.injectSqrtSX = utils.parse_list_of_numbers(self.injectSqrtSX)
if len(self.injectSqrtSX) != len(self.detector_names):
raise ValueError(
"injectSqrtSX must be of same length as detector_names ({}!={})".format(
len(self.injectSqrtSX), len(self.detector_names)
)
)
FstatOAs.injectSqrtSX = lalpulsar.MultiNoiseFloor()
FstatOAs.injectSqrtSX.length = len(self.injectSqrtSX)
FstatOAs.injectSqrtSX.sqrtSn[: FstatOAs.injectSqrtSX.length] = (
self.injectSqrtSX
)
else:
FstatOAs.injectSqrtSX = lalpulsar.FstatOptionalArgsDefaults.injectSqrtSX
# Here we are treating 0 and None as equivalent
# (use default, which is 0 and means "randomise the seed").
# See XLALAddGaussianNoise().
FstatOAs.randSeed = (
getattr(self, "randSeed", None)
or lalpulsar.FstatOptionalArgsDefaults.randSeed
)
self._set_min_max_cover_freqs()
logger.info("Initialising FstatInput")
self.FstatInput = lalpulsar.CreateFstatInput(
self.SFTCatalog,
self.minCoverFreq,
self.maxCoverFreq,
dFreq,
ephems,
FstatOAs,
)
logger.info("Initialising PulsarDoplerParams")
PulsarDopplerParams = lalpulsar.PulsarDopplerParams()
PulsarDopplerParams.refTime = self.tref
PulsarDopplerParams.fkdot = np.zeros(lalpulsar.PULSAR_MAX_SPINS)
self.PulsarDopplerParams = PulsarDopplerParams
logger.info("Initialising FstatResults")
self.FstatResults = lalpulsar.FstatResults()
# always initialise the twoFX array,
# but only actually compute it if requested
self.twoF = 0
self.twoFX = np.zeros(lalpulsar.PULSAR_MAX_DETECTORS)
self.singleFstats = self.singleFstats or self.BSGL # BSGL implies twoFX
if self.singleFstats:
self.whatToCompute += lalpulsar.FSTATQ_2F_PER_DET
if self.BSGL:
logger.info("Initialising BSGL")
self.log10BSGL = np.nan
# Tuning parameters - to be reviewed
# We use a fixed Fstar0 for coherent searches,
# and recompute it from a fixed p-value for the semicoherent case.
nsegs_eff = max([getattr(self, "nsegs", 1), getattr(self, "nglitch", 1)])
if nsegs_eff > 1:
p_val_threshold = 1e-6
Fstar0s = np.linspace(0, 1000, 10000)
p_vals = scipy.special.gammaincc(2 * nsegs_eff, Fstar0s)
self.Fstar0 = Fstar0s[np.argmin(np.abs(p_vals - p_val_threshold))]
if self.Fstar0 == Fstar0s[-1]:
raise ValueError("Max Fstar0 exceeded")
else:
self.Fstar0 = 15.0
logger.info("Using Fstar0 of {:1.2f}".format(self.Fstar0))
# assume uniform per-detector prior line-vs-Gaussian odds
self.oLGX = np.zeros(lalpulsar.PULSAR_MAX_DETECTORS)
self.oLGX[: self.numDetectors] = 1.0 / self.numDetectors
self.BSGLSetup = lalpulsar.CreateBSGLSetup(
numDetectors=self.numDetectors,
Fstar0sc=self.Fstar0,
oLGX=self.oLGX,
useLogCorrection=True,
numSegments=getattr(self, "nsegs", 1),
)
if self.transientWindowType:
logger.info(
f"Initialising transient parameters for window type '{self.transientWindowType}'"
)
self.maxTwoF = 0
if getattr(self, "BtSG", False):
self.lnBtSG = np.nan
self.windowRange = lalpulsar.transientWindowRange_t()
transientWindowTypes = {
"none": lalpulsar.TRANSIENT_NONE,
"rect": lalpulsar.TRANSIENT_RECTANGULAR,
"exp": lalpulsar.TRANSIENT_EXPONENTIAL,
}
if self.transientWindowType in transientWindowTypes:
self.windowRange.type = transientWindowTypes[self.transientWindowType]
else:
raise ValueError(
"Unknown window-type ({}) passed as input, [{}] allows.".format(
self.transientWindowType, ", ".join(transientWindowTypes)
)
)
# default spacing
self.windowRange.dt0 = self.Tsft
self.windowRange.dtau = self.Tsft
# special treatment of window_type = none
# ==> replace by rectangular window spanning all the data
if self.windowRange.type == lalpulsar.TRANSIENT_NONE:
self.windowRange.t0 = int(self.minStartTime)
self.windowRange.t0Band = 0
self.windowRange.tau = int(self.maxStartTime - self.minStartTime)
self.windowRange.tauBand = 0
else: # user-set bands and spacings
if getattr(self, "t0Band", None) is None:
self.windowRange.t0Band = 0
else:
if not isinstance(self.t0Band, int):
logger.warning(
"Casting non-integer t0Band={} to int...".format(
self.t0Band
)
)
self.t0Band = int(self.t0Band)
self.windowRange.t0Band = self.t0Band
if self.dt0:
self.windowRange.dt0 = self.dt0
if getattr(self, "tauBand", None) is None:
self.windowRange.tauBand = 0
else:
if not isinstance(self.tauBand, int):
logger.warning(
"Casting non-integer tauBand={} to int...".format(
self.tauBand
)
)
self.tauBand = int(self.tauBand)
self.windowRange.tauBand = self.tauBand
if self.dtau:
self.windowRange.dtau = self.dtau
if self.tauMin is None:
self.windowRange.tau = int(2 * self.Tsft)
else:
if not isinstance(self.tauMin, int):
logger.warning(
"Casting non-integer tauMin={} to int...".format(
self.tauMin
)
)
self.tauMin = int(self.tauMin)
self.windowRange.tau = self.tauMin
logger.info("Initialising transient FstatMap features...")
(
self.tCWFstatMapFeatures,
self.gpu_context,
) = tcw.init_transient_fstat_map_features(
self.tCWFstatMapVersion, self.cudaDeviceName
)
if self.BSGL:
self.twoFXatMaxTwoF = np.zeros(lalpulsar.PULSAR_MAX_DETECTORS)
def _set_min_max_cover_freqs(self):
# decide on which minCoverFreq and maxCoverFreq to use:
# either from direct user input, estimate_min_max_CoverFreq(), or SFTs
if self.sftfilepattern is not None:
minFreq_SFTs, maxFreq_SFTs = self._get_min_max_freq_from_SFTCatalog()
if (self.minCoverFreq is None) != (self.maxCoverFreq is None):
raise ValueError(
"Please use either both or none of [minCoverFreq,maxCoverFreq]."
)
elif (
self.minCoverFreq is None
and self.maxCoverFreq is None
and self.search_ranges is None
):
raise ValueError(
"Please use either search_ranges or both of [minCoverFreq,maxCoverFreq]."
)
elif self.minCoverFreq is None or self.maxCoverFreq is None:
logger.info(
"[minCoverFreq,maxCoverFreq] not provided, trying to estimate"
" from search ranges."
)
self.estimate_min_max_CoverFreq()
elif (self.minCoverFreq < 0.0) or (self.maxCoverFreq < 0.0):
if self.sftfilepattern is None:
raise ValueError(
"If sftfilepattern==None, cannot use negative values for"
" minCoverFreq or maxCoverFreq (interpreted as offsets from"
" min/max SFT frequency)."
" Please use actual frequency values for both,"
" or set both to None (automated estimation)."
)
if self.minCoverFreq < 0.0:
logger.info(
"minCoverFreq={:f} provided, using as offset from min(SFTs).".format(
self.minCoverFreq
)
)
# to set *above* min, since minCoverFreq is negative: subtract it
self.minCoverFreq = minFreq_SFTs - self.minCoverFreq
if self.maxCoverFreq < 0.0:
logger.info(
"maxCoverFreq={:f} provided, using as offset from max(SFTs).".format(
self.maxCoverFreq
)
)
# to set *below* max, since minCoverFreq is negative: add it
self.maxCoverFreq = maxFreq_SFTs + self.maxCoverFreq
if (self.sftfilepattern is not None) and (
(self.minCoverFreq < minFreq_SFTs) or (self.maxCoverFreq > maxFreq_SFTs)
):
raise ValueError(
"[minCoverFreq,maxCoverFreq]=[{:f},{:f}] Hz incompatible with"
" SFT files content [{:f},{:f}] Hz".format(
self.minCoverFreq, self.maxCoverFreq, minFreq_SFTs, maxFreq_SFTs
)
)
logger.info(
"Using minCoverFreq={} and maxCoverFreq={}.".format(
self.minCoverFreq, self.maxCoverFreq
)
)
def _get_min_max_freq_from_SFTCatalog(self):
fAs = [d.header.f0 for d in self.SFTCatalog.data]
minFreq_SFTs = np.min(fAs)
fBs = [
d.header.f0 + (d.numBins - 1) * d.header.deltaF
for d in self.SFTCatalog.data
]
maxFreq_SFTs = np.max(fBs)
return minFreq_SFTs, maxFreq_SFTs
def estimate_min_max_CoverFreq(self):
"""Extract spanned spin-range at reference -time from the template bank.
To use this method, self.search_ranges must be a dictionary of lists per search parameter
which can be either [single_value], [min,max] or [min,max,step].
"""
if type(self.search_ranges) is not dict:
raise ValueError("Need a dictionary for search_ranges!")
range_keys = list(self.search_ranges.keys())
required_keys = ["Alpha", "Delta", "F0"]
if len(np.setdiff1d(required_keys, range_keys)) > 0:
raise ValueError(
"Required keys not found in search_ranges: {}".format(
np.setdiff1d(required_keys, range_keys)
)
)
for key in range_keys:
if (
type(self.search_ranges[key]) is not list
or len(self.search_ranges[key]) == 0
or len(self.search_ranges[key]) > 3
):
raise ValueError(
"search_ranges entry for {:s}"
" is not a list of a known format"
" (either [single_value], [min,max]"
" or [min,max,step]): {}".format(key, self.search_ranges[key])
)