gstlal 1.13.0
Loading...
Searching...
No Matches
segments.py
1# Copyright (C) 2010--2020 Kipp Cannon, Patrick Godwin, Chad Hanna, Ryan Magee
2#
3# This program is free software; you can redistribute it and/or modify it under
4# the terms of the GNU General Public License as published by the Free Software
5# Foundation; either version 2 of the License, or (at your option) any later
6# version.
7#
8# This program is distributed in the hope that it will be useful, but WITHOUT
9# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
11# details.
12#
13# You should have received a copy of the GNU General Public License along with
14# this program; if not, write to the Free Software Foundation, Inc., 51
15# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17
18import itertools
19import math
20import os
21import ssl
22import urllib.request
23from typing import Iterable, Mapping, Union
24import warnings
25
26import dqsegdb2.query
27from ligo import segments
28from ligo.segments import utils as segutils
29from ligo.lw import ligolw
30from ligo.lw import lsctables
31from ligo.lw import utils as ligolw_utils
32from lal import LIGOTimeGPS
33
34
35DEFAULT_DQSEGDB_SERVER = os.environ.get("DEFAULT_SEGMENT_SERVER", "https://segments.ligo.org")
36
37
38@lsctables.use_in
39class LIGOLWContentHandler(ligolw.LIGOLWContentHandler):
40 pass
41
42
43def query_dqsegdb_segments(
44 instruments: Union[str, Iterable],
45 start: Union[int, LIGOTimeGPS],
46 end: Union[int, LIGOTimeGPS],
47 flags: Union[str, Mapping],
48 server: str = DEFAULT_DQSEGDB_SERVER,
49) -> segments.segmentlistdict:
50 """Query DQSegDB for science segments.
51
52 Args:
53 instruments:
54 Union[str, Iterable], the instruments to query segments for
55 start:
56 Union[int, LIGOTimeGPS], the GPS start time
57 end:
58 Union[int, LIGOTimeGPS], the GPS end time
59 flags:
60 Union[str, Mapping], the name of the DQ flags used to query
61 server:
62 str, defaults to main DQSegDB server, the server URL
63
64 Returns:
65 segmentlistdict, the queried segments
66
67 """
68 span = segments.segment(LIGOTimeGPS(start), LIGOTimeGPS(end))
69 if isinstance(flags, str):
70 if not isinstance(instruments, str):
71 raise ValueError("if flags is type str, then instruments must also be type str")
72 flags = {instruments: flags}
73 if isinstance(instruments, str):
74 instruments = [instruments]
75
76 segs = segments.segmentlistdict()
77 for ifo, flag in flags.items():
78 active = dqsegdb2.query.query_segments(flag, start, end, host=server, coalesce=True)["active"]
79 segs[ifo] = segments.segmentlist([span]) & active
80
81 return segs
82
83
84def query_dqsegdb_veto_segments(
85 instruments: Union[str, Iterable],
86 start: Union[int, LIGOTimeGPS],
87 end: Union[int, LIGOTimeGPS],
88 veto_definer_file: str,
89 category: str,
90 cumulative: bool = True,
91 server: str = DEFAULT_DQSEGDB_SERVER,
92) -> segments.segmentlistdict:
93 """Query DQSegDB for veto segments.
94
95 Args:
96 instruments:
97 Union[str, Iterable], the instruments to query segments for
98 start:
99 Union[int, LIGOTimeGPS], the GPS start time
100 end:
101 Union[int, LIGOTimeGPS], the GPS end time
102 veto_definer_file:
103 str, the veto definer file in which to query veto segments for
104 category:
105 the veto category to use for vetoes, one of CAT1, CAT2, CAT3
106 cumulative:
107 whether veto categories are cumulative, e.g. choosing CAT2
108 also includes CAT1 vetoes
109 server:
110 str, defaults to main DQSegDB server, the server URL
111
112 Returns:
113 segmentlistdict, the queried veto segments
114
115 """
116 if isinstance(instruments, str):
117 instruments = [instruments]
118
119 if category not in set(("CAT1", "CAT2", "CAT3")):
120 raise ValueError("not valid category")
121
122 # read in vetoes
123 xmldoc = ligolw_utils.load_filename(veto_definer_file, contenthandler=LIGOLWContentHandler)
124 vetoes = lsctables.VetoDefTable.get_table(xmldoc)
125
126 # filter vetoes by instruments
127 vetoes[:] = [v for v in vetoes if v.ifo in set(instruments)]
128
129 # filter vetoes by category
130 cat_level = int(category[-1])
131 if cumulative:
132 vetoes[:] = [v for v in vetoes if v.category <= cat_level]
133 else:
134 vetoes[:] = [v for v in vetoes if v.category == cat_level]
135
136 # retrieve segments corresponding to flags
137 segs = segments.segmentlistdict()
138 for instrument in instruments:
139 segs[instrument] = segments.segmentlist()
140 for veto in vetoes:
141 flag = f"{veto.ifo}:{veto.name}:{veto.version}"
142 segs[veto.ifo] |= dqsegdb2.query.query_segments(flag, start, end, host=server, coalesce=True)["active"]
143 segs.coalesce()
144
145 return segs
146
147
148def query_gwosc_segments(
149 instruments: Union[str, Iterable],
150 start: Union[int, LIGOTimeGPS],
151 end: Union[int, LIGOTimeGPS],
152 verify_certs: bool = True,
153) -> segments.segmentlistdict:
154 """Query GWOSC for science segments.
155
156 Args:
157 instruments:
158 Union[str, Iterable], the instruments to query segments for
159 start:
160 Union[int, LIGOTimeGPS], the GPS start time
161 end:
162 Union[int, LIGOTimeGPS], the GPS end time
163 verify_certs:
164 bool, default True, whether to verify SSL certificates when querying GWOSC.
165
166 Returns:
167 segmentlistdict, the queried segments
168
169 """
170 if isinstance(instruments, str):
171 instruments = [instruments]
172
173 # Set up SSL context
174 context = ssl.create_default_context()
175 if not verify_certs:
176 context.check_hostname = False
177 context.verify_mode = ssl.CERT_NONE
178
179 # Retrieve segments
180 segs = segments.segmentlistdict()
181 for instrument in instruments:
182 url = _gwosc_segment_url(start, end, f"{instrument}_DATA")
183 urldata = urllib.request.urlopen(url, context=context).read().decode('utf-8')
184 with warnings.catch_warnings():
185 warnings.filterwarnings("ignore", category=FutureWarning)
186 segs[instrument] = segutils.fromsegwizard(
187 urldata.splitlines(),
188 coltype=lsctables.LIGOTimeGPS,
189 )
190 segs.coalesce()
191
192 return segs
193
194
195def query_gwosc_veto_segments(
196 instruments: Union[str, Iterable],
197 start: Union[int, LIGOTimeGPS],
198 end: Union[int, LIGOTimeGPS],
199 category: str,
200 cumulative: bool = True,
201 verify_certs: bool = True,
202) -> segments.segmentlistdict:
203 """Query GWOSC for veto segments.
204
205 Args:
206 instruments:
207 Union[str, Iterable], the instruments to query segments for
208 start:
209 Union[int, LIGOTimeGPS], the GPS start time
210 end:
211 Union[int, LIGOTimeGPS], the GPS end time
212 category:
213 the veto category to use for vetoes, one of CAT1, CAT2, CAT3
214 cumulative:
215 whether veto categories are cumulative, e.g. choosing CAT2
216 also includes CAT1 vetoes
217 verify_certs:
218 bool, default True, whether to verify SSL certificates when querying GWOSC.
219
220 Returns:
221 segmentlistdict, the queried veto segments
222
223 """
224 span = segments.segment(LIGOTimeGPS(start), LIGOTimeGPS(end))
225 if isinstance(instruments, str):
226 instruments = [instruments]
227
228 if category not in set(("CAT1", "CAT2", "CAT3")):
229 raise ValueError("not valid category")
230
231 if cumulative:
232 flags = [f"CBC_CAT{i}" for i in range(1, int(category[-1]) + 1)]
233 else:
234 flags = [f"CBC_{category}"]
235
236 # hardware injections not in normal categories in GWOSC
237 # so we treat them all as CAT1 (except CW)
238 hw_inj_flags = [
239 "NO_BURST_HW_INJ",
240 "NO_CBC_HW_INJ",
241 "NO_DETCHAR_HW_INJ",
242 "NO_STOCH_HW_INJ",
243 ]
244 flags += hw_inj_flags
245
246 # set up SSL context
247 context = ssl.create_default_context()
248 if not verify_certs:
249 context.check_hostname = False
250 context.verify_mode = ssl.CERT_NONE
251
252 # retrieve segments corresponding to flags
253 segs = segments.segmentlistdict()
254 for instrument in instruments:
255 segs[instrument] = segments.segmentlist([span])
256 for flag in flags:
257 url = _gwosc_segment_url(start, end, f"{instrument}_{flag}")
258 urldata = urllib.request.urlopen(url, context=context).read().decode('utf-8')
259 with warnings.catch_warnings():
260 warnings.filterwarnings("ignore", category=FutureWarning)
261 segs[instrument] &= segutils.fromsegwizard(
262 urldata.splitlines(),
263 coltype=lsctables.LIGOTimeGPS,
264 )
265 segs.coalesce()
266
267 # invert segments to transform into vetoes
268 for instrument in instruments:
269 segs[instrument] = segments.segmentlist([span]) & ~segs[instrument]
270
271 return segs
272
273
274def analysis_segments(
275 ifos: Iterable[str],
276 allsegs: segments.segmentlistdict,
277 boundary_seg: segments.segment,
278 start_pad: float = 0.,
279 overlap: float = 0.,
280 min_instruments: int = 1,
281 one_ifo_length: float = (3600 * 8.),
282) -> segments.segmentlistdict:
283 """Generate all disjoint detector combination segments for analysis job boundaries.
284
285 """
286 ifos = set(ifos)
287 segsdict = segments.segmentlistdict()
288
289 # segment length dependent on the number of instruments
290 # so that longest job runtimes are similar
291 segment_length = lambda n_ifo: one_ifo_length / 2 ** (n_ifo - 1)
292
293 # generate analysis segments
294 for n in range(min_instruments, 1 + len(ifos)):
295 for ifo_combos in itertools.combinations(list(ifos), n):
296 ifo_key = frozenset(ifo_combos)
297 segsdict[ifo_key] = allsegs.intersection(ifo_combos) - allsegs.union(ifos - set(ifo_combos))
298 segsdict[ifo_key] = segsdict[ifo_key].protract(overlap)
299 segsdict[ifo_key] &= segments.segmentlist([boundary_seg])
300 segsdict[ifo_key] = split_segments(segsdict[ifo_key], segment_length(len(ifo_combos)), start_pad)
301 if not segsdict[ifo_key]:
302 del segsdict[ifo_key]
303
304 return segsdict
305
306
307def split_segments_by_lock(
308 ifos: Iterable,
309 seglistdicts: segments.segmentlistdict,
310 boundary_seg: segments.segment,
311 max_time: float = 10 * 24 * 3600.,
312) -> segments.segmentlist:
313 """Split segments into segments with maximum time and boundaries outside of lock stretches.
314
315 """
316 ifos = set(seglistdicts)
317
318 # create set of segments for each ifo when it was
319 # in coincidence with at least one other ifo
320 doublesegs = segments.segmentlistdict()
321 for ifo1 in ifos:
322 for ifo2 in ifos - set([ifo1]):
323 if ifo1 in doublesegs:
324 doublesegs[ifo1] |= seglistdicts.intersection((ifo1, ifo2))
325 else:
326 doublesegs[ifo1] = seglistdicts.intersection((ifo1, ifo2))
327
328 # This is the set of segments when at least two ifos were on
329 doublesegsunion = doublesegs.union(doublesegs.keys())
330
331 # This is the set of segments when at least one ifo was on
332 segs = seglistdicts.union(seglistdicts.keys())
333
334 # define when "enough time" has passed
335 def enoughtime(seglist, start, end):
336 return abs(seglist & segments.segmentlist([segments.segment(start, end)])) > 0.7 * max_time
337
338 # iterate through all the segment where at least one ifo was on and extract
339 # chunks where each ifo satisfies our coincidence requirement. A consequence is
340 # that we only define boundaries when one ifos is on
341 chunks = segments.segmentlist([boundary_seg])
342
343 # This places boundaries when only one ifo or less was on
344 for start, end in doublesegsunion:
345 if all([enoughtime(s, chunks[-1][0], end) for s in doublesegs.values()]):
346 chunks[-1] = segments.segment(chunks[-1][0], end)
347 chunks.append(segments.segment(end, boundary_seg[1]))
348
349 # check that last segment has enough livetime
350 # if not, merge it with the previous segment
351 if len(chunks) > 1 and abs(chunks[-1]) < 0.3 * max_time:
352 last_chunk = chunks.pop()
353 chunks[-1] = segments.segmentlist([chunks[-1], last_chunk]).coalesce().extent()
354
355 return chunks
356
357
358def split_segments(
359 seglist: segments.segmentlist,
360 maxextent: float,
361 overlap: float
362) -> segments.segmentlist:
363 """Split a segmentlist into segments of maximum extent.
364
365 """
366 newseglist = segments.segmentlist()
367 for bigseg in seglist:
368 newseglist.extend(split_segment(bigseg, maxextent, overlap))
369 return newseglist
370
371
372def split_segment(seg: segments.segment, maxextent: float, overlap: float) -> segments.segmentlist:
373 """Split a segment into segments of maximum extent.
374
375 """
376 if maxextent <= 0:
377 raise ValueError("maxextent must be positive, not %s" % repr(maxextent))
378
379 # Simple case of only one segment
380 if abs(seg) < maxextent:
381 return segments.segmentlist([seg])
382
383 # adjust maxextent so that segments are divided roughly equally
384 maxextent = max(int(abs(seg) / (int(abs(seg)) // int(maxextent) + 1)), overlap)
385 maxextent = int(math.ceil(abs(seg) / math.ceil(abs(seg) / maxextent)))
386 end = seg[1]
387
388 seglist = segments.segmentlist()
389
390 while abs(seg):
391 if (seg[0] + maxextent + overlap) < end:
392 seglist.append(segments.segment(seg[0], seg[0] + maxextent + overlap))
393 seg = segments.segment(seglist[-1][1] - overlap, seg[1])
394 else:
395 seglist.append(segments.segment(seg[0], end))
396 break
397
398 return seglist
399
400
401def _gwosc_segment_url(start, end, flag):
402 """Returns the GWOSC URL associated with segments.
403
404 """
405 span = segments.segment(LIGOTimeGPS(start), LIGOTimeGPS(end))
406
407 # determine GWOSC URL to query from
408 urlbase = "https://gw-openscience.org/timeline/segments"
409 if start in segments.segment(1126051217, 1137254417):
410 query_url = f"{urlbase}/O1"
411 elif start in segments.segment(1164556817, 1187733618):
412 query_url = f"{urlbase}/O2_16KHZ_R1"
413 elif start in segments.segment(1238166018, 1253977218):
414 query_url = f"{urlbase}/O3a_16KHZ_R1"
415 elif start in segments.segment(1256655618, 1269363618):
416 query_url = f"{urlbase}/O3b_16KHZ_R1"
417 else:
418 raise ValueError("GPS times requested not in GWOSC")
419
420 return f"{query_url}/{flag}/{span[0]}/{abs(span)}"