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