gstlal 1.13.0
Loading...
Searching...
No Matches
util.py
Go to the documentation of this file.
1# Copyright (C) 2010 Kipp Cannon (kipp.cannon@ligo.org)
2# Copyright (C) 2010 Chad Hanna (chad.hanna@ligo.org)
3# Copyright (C) 2020 Patrick Godwin (patrick.godwin@ligo.org)
4#
5# This program is free software; you can redistribute it and/or modify it under
6# the terms of the GNU General Public License as published by the Free Software
7# Foundation; either version 2 of the License, or (at your option) any later
8# version.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13# details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program; if not, write to the Free Software Foundation, Inc., 51
17# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19
20
21
22
23#
24# =============================================================================
25#
26# Preamble
27#
28# =============================================================================
29#
30
31
32"""
33DAG construction tools.
34"""
35
36
37import collections
38import doctest
39import itertools
40import math
41import os
42import sys
43import socket
44import subprocess
45import tempfile
46
47import numpy
48
49from ligo import segments
50
51from lal.utils import CacheEntry
52
53from gstlal import pipeline
54
55__author__ = "Kipp Cannon <kipp.cannon@ligo.org>, Chad Hanna <chad.hanna@ligo.org>"
56__date__ = "$Date$" #FIXME
57__version__ = "$Revision$" #FIXME
58
59
60#
61# =============================================================================
62#
63# Environment utilities
64#
65# =============================================================================
66#
67
68
69def which(prog):
70 which = subprocess.Popen(['which',prog], stdout=subprocess.PIPE)
71 out = which.stdout.read().strip().decode('utf-8')
72 if not out:
73 raise ValueError("could not find %s in your path, have you built the proper software and sourced the proper environment scripts?" % prog)
74 return out
75
76
77def condor_scratch_space():
78 """!
79 A way to standardize the condor scratch space even if it changes
80 >>> condor_scratch_space()
81 '_CONDOR_SCRATCH_DIR'
82 """
83 return "_CONDOR_SCRATCH_DIR"
84
85
86def log_path():
87 """!
88 The stupid pet tricks to find log space on the LDG.
89 Defaults to checking TMPDIR first.
90 """
91 host = socket.getfqdn()
92 try:
93 return os.environ['TMPDIR']
94 except KeyError:
95 print("\n\n!!!! $TMPDIR NOT SET !!!!\n\n\tPLEASE email your admin to tell them to set $TMPDIR to be the place where a users temporary files should be\n")
96 #FIXME add more hosts as you need them
97 if 'cit' in host or 'caltech.edu' in host:
98 tmp = '/usr1/' + os.environ['USER']
99 print(f"falling back to {tmp}")
100 return tmp
101 if 'phys.uwm.edu' in host:
102 tmp = '/localscratch/' + os.environ['USER']
103 print(f"falling back to {tmp}")
104 return tmp
105 if 'aei.uni-hannover.de' in host:
106 tmp = '/local/user/' + os.environ['USER']
107 print(f"falling back to {tmp}")
108 return tmp
109 if 'phy.syr.edu' in host:
110 tmp = '/usr1/' + os.environ['USER']
111 print(f"falling back to {tmp}")
112 return tmp
113
114 raise KeyError("$TMPDIR is not set and I don't recognize this environment")
115
116
117#
118# =============================================================================
119#
120# Condor DAG utilities
121#
122# =============================================================================
123#
124
125
127 """!
128 A thin subclass of pipeline.CondorDAG.
129
130 Extra features include an add_node() method and a cache writing method.
131 Also includes some standard setup, e.g., log file paths etc.
132 """
133 def __init__(self, name, logpath = log_path()):
134 self.basename = name.replace(".dag","")
135 tempfile.tempdir = logpath
136 tempfile.template = self.basename + '.dag.log.'
137 logfile = tempfile.mktemp()
138 fh = open( logfile, "w" )
139 fh.close()
140 pipeline.CondorDAG.__init__(self,logfile)
141 self.set_dag_file(self.basename)
142 self.jobsDict = {}
143 self.output_cache = []
144
145 def add_node(self, node, retry = 3):
146 node.set_retry(retry)
147 node.add_macro("macronodename", node.get_name())
148 pipeline.CondorDAG.add_node(self, node)
149
150 def write_cache(self):
151 out = self.basename + ".cache"
152 f = open(out,"w")
153 for c in self.output_cache:
154 f.write(str(c)+"\n")
155 f.close()
156
157
159 """!
160 A job class that subclasses pipeline.CondorDAGJob and adds some extra
161 boiler plate items for gstlal jobs which tends to do the "right" thing
162 when given just an executable name.
163 """
164 def __init__(self, executable, tag_base = None, universe = "vanilla", condor_commands = {}):
165 self.__executable = which(executable)
166 self.__universe = universe
167 if tag_base:
168 self.tag_base = tag_base
169 else:
170 self.tag_base = os.path.split(self.__executable)[1]
171 self.__prog__ = self.tag_base
172 pipeline.CondorDAGJob.__init__(self, self.__universe, self.__executable)
173 self.add_condor_cmd('getenv','True')
174 self.add_condor_cmd('environment',"GST_REGISTRY_UPDATE=no;")
175 self.set_sub_file(self.tag_base+'.sub')
176 self.set_stdout_file('logs/$(macronodename)-$(cluster)-$(process).out')
177 self.set_stderr_file('logs/$(macronodename)-$(cluster)-$(process).err')
178 self.number = 1
179 # make an output directory for files
180 self.output_path = self.tag_base
181 try:
182 os.mkdir(self.output_path)
183 except:
184 pass
185 for cmd, val in condor_commands.items():
186 self.add_condor_cmd(cmd, val)
187
188
190 """!
191 A node class that subclasses pipeline.CondorDAGNode that automates
192 adding the node to the dag, makes sensible names and allows a list of parent
193 nodes to be provided.
194
195 It tends to do the "right" thing when given a job, a dag, parent nodes, dictionary
196 options relevant to the job, a dictionary of options related to input files and a
197 dictionary of options related to output files.
198
199 NOTE and important and subtle behavior - You can specify an option with
200 an empty argument by setting it to "". However options set to None are simply
201 ignored.
202 """
203 def __init__(self, job, dag, parent_nodes, opts = {}, input_files = {}, output_files = {}, input_cache_files = {}, output_cache_files = {}, input_cache_file_name = None):
204 pipeline.CondorDAGNode.__init__(self, job)
205 for p in parent_nodes:
206 self.add_parent(p)
207 self.set_name("%s_%04X" % (job.tag_base, job.number))
208 job.number += 1
209 dag.add_node(self)
210
211 self.input_files = input_files.copy()
212 self.input_files.update(input_cache_files)
213 self.output_files = output_files.copy()
214 self.output_files.update(output_cache_files)
215
216 self.cache_inputs = {}
217 self.cache_outputs = {}
218
219 for opt, val in list(opts.items()) + list(output_files.items()) + list(input_files.items()):
220 if val is None:
221 continue # not the same as val = '' which is allowed
222 if isinstance(val, str) or not isinstance(val, collections.Iterable): # catches list like things but not strings
223 if opt == "":
224 self.add_var_arg(val)
225 else:
226 self.add_var_opt(opt, val)
227 # Must be an iterable
228 else:
229 if opt == "":
230 [self.add_var_arg(a) for a in val]
231 else:
232 self.add_var_opt(opt, pipeline_dot_py_append_opts_hack(opt, val))
233
234 # Create cache files for long command line arguments and store them in the job's subdirectory. NOTE the svd-bank string
235 # is handled by gstlal_inspiral_pipe directly
236
237 cache_dir = os.path.join(job.tag_base, 'cache')
238
239 for opt, val in input_cache_files.items():
240 if not os.path.isdir(cache_dir):
241 os.mkdir(cache_dir)
242 cache_entries = [CacheEntry.from_T050017("file://localhost%s" % os.path.abspath(filename)) for filename in val]
243 if input_cache_file_name is None:
244 cache_file_name = group_T050017_filename_from_T050017_files(cache_entries, '.cache', path = cache_dir)
245 else:
246 cache_file_name = os.path.join(cache_dir, input_cache_file_name)
247 open(cache_file_name, "w").write("\n".join(map(str, cache_entries)))
248 self.add_var_opt(opt, cache_file_name)
249 # Keep track of the cache files being created
250 self.cache_inputs.setdefault(opt, []).append(cache_file_name)
251
252 for opt, val in output_cache_files.items():
253 if not os.path.isdir(cache_dir):
254 os.mkdir(cache_dir)
255 cache_entries = [CacheEntry.from_T050017("file://localhost%s" % os.path.abspath(filename)) for filename in val]
256 cache_file_name = group_T050017_filename_from_T050017_files(cache_entries, '.cache', path = cache_dir)
257 open(cache_file_name, "w").write("\n".join(map(str, cache_entries)))
258 self.add_var_opt(opt, cache_file_name)
259 # Keep track of the cache files being created
260 self.cache_outputs.setdefault(opt, []).append(cache_file_name)
261
262
263def condor_command_dict_from_opts(opts, defaultdict = None):
264 """!
265 A function to turn a list of options into a dictionary of condor commands, e.g.,
266
267 >>> condor_command_dict_from_opts(["+Online_CBC_SVD=True", "TARGET.Online_CBC_SVD =?= True"])
268 {'+Online_CBC_SVD': 'True', 'TARGET.Online_CBC_SVD ': '?= True'}
269 >>> condor_command_dict_from_opts(["+Online_CBC_SVD=True", "TARGET.Online_CBC_SVD =?= True"], {"somecommand":"somevalue"})
270 {'somecommand': 'somevalue', '+Online_CBC_SVD': 'True', 'TARGET.Online_CBC_SVD ': '?= True'}
271 >>> condor_command_dict_from_opts(["+Online_CBC_SVD=True", "TARGET.Online_CBC_SVD =?= True"], {"+Online_CBC_SVD":"False"})
272 {'+Online_CBC_SVD': 'True', 'TARGET.Online_CBC_SVD ': '?= True'}
273 """
274
275 if defaultdict is None:
276 defaultdict = {}
277 for o in opts:
278 osplit = o.split("=")
279 k = osplit[0]
280 v = "=".join(osplit[1:])
281 defaultdict.update([(k, v)])
282 return defaultdict
283
284
285def pipeline_dot_py_append_opts_hack(opt, vals):
286 """!
287 A way to work around the dictionary nature of pipeline.py which can
288 only record options once.
289
290 >>> pipeline_dot_py_append_opts_hack("my-favorite-option", [1,2,3])
291 '1 --my-favorite-option 2 --my-favorite-option 3'
292 """
293 out = str(vals[0])
294 for v in vals[1:]:
295 out += " --%s %s" % (opt, str(v))
296 return out
297
298
299def format_ifo_args(ifos, args):
300 """
301 Given a set of instruments and arguments keyed by instruments, this
302 creates a list of strings in the form {ifo}={arg}. This is suitable
303 for command line options like --channel-name which expects this
304 particular format.
305 """
306 if isinstance(ifos, str):
307 ifos = [ifos]
308 return [f"{ifo}={args[ifo]}" for ifo in ifos]
309
310
311#
312# =============================================================================
313#
314# Segment utilities
315#
316# =============================================================================
317#
318
319
320def breakupseg(seg, maxextent, overlap):
321 if maxextent <= 0:
322 raise ValueError("maxextent must be positive, not %s" % repr(maxextent))
323
324 # Simple case of only one segment
325 if abs(seg) < maxextent:
326 return segments.segmentlist([seg])
327
328 # adjust maxextent so that segments are divided roughly equally
329 maxextent = max(int(abs(seg) / (int(abs(seg)) // int(maxextent) + 1)), overlap)
330 maxextent = int(math.ceil(abs(seg) / math.ceil(abs(seg) / maxextent)))
331 end = seg[1]
332
333 seglist = segments.segmentlist()
334
335
336 while abs(seg):
337 if (seg[0] + maxextent + overlap) < end:
338 seglist.append(segments.segment(seg[0], seg[0] + maxextent + overlap))
339 seg = segments.segment(seglist[-1][1] - overlap, seg[1])
340 else:
341 seglist.append(segments.segment(seg[0], end))
342 break
343
344 return seglist
345
346
347def breakupsegs(seglist, maxextent, overlap):
348 newseglist = segments.segmentlist()
349 for bigseg in seglist:
350 newseglist.extend(breakupseg(bigseg, maxextent, overlap))
351 return newseglist
352
353
354def breakupseglists(seglists, maxextent, overlap):
355 for instrument, seglist in seglists.iteritems():
356 newseglist = segments.segmentlist()
357 for bigseg in seglist:
358 newseglist.extend(breakupseg(bigseg, maxextent, overlap))
359 seglists[instrument] = newseglist
360
361
362def partition_by_time(span, segdict, ifos, min_ifos=1, max_livetime=14440, start_pad=512):
363 """!
364 Splits a time span roughly equally based on livetime.
365 """
366 # get segments for all ifo combinations requested and take union
367 segdict_by_combo = segments.segmentlistdict()
368 ifo_combos = flatten(itertools.combinations(ifos, n) for n in range(min_ifos, len(ifos)))
369 ifo_combos = [frozenset(ifo_combo) for ifo_combo in ifo_combos]
370 for ifo_combo in ifo_combos:
371 segdict_by_combo[ifo_combo] = segdict.intersection(ifo_combo)
372 all_segs = segdict_by_combo.union(ifo_combos) & segments.segmentlist([span])
373
374 # split equally into bins
375 num_bins = int(numpy.ceil(float(abs(all_segs) / max_livetime)))
376 time_bins = [segments.segmentlist() for i in range(num_bins)]
377
378 # calculate livetime for each bin_, ensuring
379 # start, end edges fall on integer boundaries
380 small_bin, remainder = divmod(float(abs(all_segs)), num_bins)
381 big_bin = small_bin + remainder
382 bin_livetime = [big_bin if n == 0 else small_bin for n in range(num_bins)]
383
384 # determine bins
385 bin_ = 0
386 for seg in all_segs:
387 # add entire segment to current bin_ if livetime doesn't spill over
388 current_livetime = abs(time_bins[bin_])
389 if current_livetime + abs(segments.segmentlist([seg])) <= bin_livetime[bin_]:
390 time_bins[bin_] |= segments.segmentlist([seg])
391
392 # otherwise, split segment and put spill-over into next bin(s)
393 else:
394 diff_livetime = bin_livetime[bin_] - current_livetime
395 needed_seg = segments.segmentlist([segments.segment(seg[0], seg[0] + diff_livetime)])
396 time_bins[bin_] |= needed_seg
397
398 # if segment is still too big, keep splitting until it isn't
399 remainder = segments.segmentlist([segments.segment(seg[0] + diff_livetime, seg[1])])
400 while abs(remainder) > bin_livetime[bin_]:
401 remainder_start = remainder[0][0]
402 remainder_mid = remainder[0][0] + bin_livetime[bin_]
403 time_bins[bin_+1] |= segments.segmentlist([segments.segment(remainder_start, remainder_mid)])
404 remainder = segments.segmentlist([segments.segment(remainder_mid, seg[1])])
405 bin_ += 1
406
407 # divvy up final piece
408 if bin_ < num_bins - 1:
409 bin_ += 1
410 time_bins[bin_] |= remainder
411
412 # calculate start/end times from each bin and pad accordingly
413 half_pad = start_pad / 2
414 return [segs.extent().protract(half_pad).shift(-half_pad) if i != 0 else segs.extent() for i, segs in enumerate(time_bins)]
415
416
417#
418# =============================================================================
419#
420# File utilities
421#
422# =============================================================================
423#
424
425
426def cache_to_instruments(cache):
427 """!
428 Given a cache, returns back a string containing all the IFOs that are
429 contained in each of its cache entries, sorted by IFO name.
430 """
431 observatories = set()
432 for cache_entry in cache:
433 observatories.update(groups(cache_entry.observatory, 2))
434 return ''.join(sorted(list(observatories)))
435
436
437def group_T050017_filename_from_T050017_files(cache_entries, extension, path = None):
438 """!
439 A function to return the name of a file created from multiple files following
440 the T050017 convention. In addition to the T050017 requirements, this assumes
441 that numbers relevant to organization schemes will be the first entry in the
442 description, e.g. 0_DIST_STATS, and that all files in a given cache file are
443 from the same group of ifos and either contain data from the same segment or
444 from the same background bin. Note, that each file doesn't have to be from
445 the same IFO, for example the template bank cache could contain template bank
446 files from H1 and template bank files from L1.
447 """
448 # Check that every file has same observatory.
449 observatories = cache_to_instruments(cache_entries)
450 split_description = cache_entries[0].description.split('_')
451 min_bin = [x for x in split_description[:2] if x.isdigit()]
452 max_bin = [x for x in cache_entries[-1].description.split('_')[:2] if x.isdigit()]
453 seg = segments.segmentlist(cache_entry.segment for cache_entry in cache_entries).extent()
454 if min_bin:
455 min_bin = min_bin[0]
456 if max_bin:
457 max_bin = max_bin[-1]
458 if min_bin and (min_bin == max_bin or not max_bin):
459 # All files from same bin, thus segments may be different.
460 # Note that this assumes that if the last file in the cache
461 # does not start with a number that every file in the cache is
462 # from the same bin, an example of this is the cache file
463 # generated for gstlal_inspiral_calc_likelihood, which contains
464 # all of the DIST_STATS files from a given background bin and
465 # then CREATE_PRIOR_DIST_STATS files which are not generated
466 # for specific bins
467 return T050017_filename(observatories, cache_entries[0].description, seg, extension, path = path)
468 elif min_bin and max_bin and min_bin != max_bin:
469 if split_description[1].isdigit():
470 description_base = split_description[2:]
471 else:
472 description_base = split_description[1:]
473 # Files from different bins, thus segments must be same
474 return T050017_filename(observatories, '_'.join([min_bin, max_bin] + description_base), seg, extension, path = path)
475 else:
476 print("ERROR: first and last file of cache file do not match known pattern, cannot name group file under T050017 convention. \nFile 1: %s\nFile 2: %s" % (cache_entries[0].path, cache_entries[-1].path), file=sys.stderr)
477 raise ValueError
478
479
480#
481# =============================================================================
482#
483# Misc utilities
484#
485# =============================================================================
486#
487
488
489def groups(l, n):
490 """!
491 Given a list, returns back sublists with a maximum size n.
492 """
493 for i in range(0, len(l), n):
494 yield l[i:i+n]
495
496
497def flatten(lst):
498 """!
499 Flatten a list by one level of nesting.
500 """
501 return list(itertools.chain.from_iterable(lst))
A job class that subclasses pipeline.CondorDAGJob and adds some extra boiler plate items for gstlal j...
Definition util.py:158
A node class that subclasses pipeline.CondorDAGNode that automates adding the node to the dag,...
Definition util.py:189
A thin subclass of pipeline.CondorDAG.
Definition util.py:126
add_node(self, node, retry=3)
Definition util.py:145
add_var_opt(self, opt, value, short=False)
Definition pipeline.py:919
add_var_arg(self, arg, quote=False)
Definition pipeline.py:946
set_stderr_file(self, path)
Definition pipeline.py:366
set_stdout_file(self, path)
Definition pipeline.py:379
add_condor_cmd(self, cmd, value)
Definition pipeline.py:184