diff --git a/bin/rivet b/bin/rivet --- a/bin/rivet +++ b/bin/rivet @@ -1,707 +1,707 @@ #! /usr/bin/env python """\ Run Rivet analyses on HepMC events read from a file or Unix pipe Examples: %(prog)s [options] [ ...] or mkfifo fifo.hepmc my_generator -o fifo.hepmc & %(prog)s [options] fifo.hepmc ENVIRONMENT: * RIVET_ANALYSIS_PATH: list of paths to be searched for plugin analysis libraries at runtime * RIVET_DATA_PATH: list of paths to be searched for data files (defaults to use analysis path) * RIVET_WEIGHT_INDEX: the numerical weight-vector index to use in this run (default = 0; -1 = ignore weights) * See the documentation for more environment variables. """ from __future__ import print_function import os, sys ## Load the rivet module try: import rivet except: ## If rivet loading failed, try to bootstrap the Python path! try: # TODO: Is this a good idea? Maybe just notify the user that their PYTHONPATH is wrong? import commands modname = sys.modules[__name__].__file__ binpath = os.path.dirname(modname) rivetconfigpath = os.path.join(binpath, "rivet-config") rivetpypath = commands.getoutput(rivetconfigpath + " --pythonpath") sys.path.append(rivetpypath) import rivet except: sys.stderr.write("The rivet Python module could not be loaded: is your PYTHONPATH set correctly?\n") sys.exit(5) rivet.util.check_python_version() rivet.util.set_process_name("rivet") import time, datetime, logging, signal ## Parse command line options import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("ARGS", nargs="*") parser.add_argument("--version", dest="SHOW_VERSION", action="store_true", default=False, help="show Rivet version") anagroup = parser.add_argument_group("Analysis handling") anagroup.add_argument("-a", "--analysis", "--analyses", dest="ANALYSES", action="append", default=[], metavar="ANA", help="add an analysis (or comma-separated list of analyses) to the processing list.") anagroup.add_argument("--list-analyses", "--list", dest="LIST_ANALYSES", action="store_true", default=False, help="show the list of available analyses' names. With -v, it shows the descriptions, too") anagroup.add_argument("--list-keywords", "--keywords", dest="LIST_KEYWORDS", action="store_true", default=False, help="show the list of available keywords.") anagroup.add_argument("--list-used-analyses", action="store_true", dest="LIST_USED_ANALYSES", default=False, help="list the analyses used by this command (after subtraction of inappropriate ones)") anagroup.add_argument("--show-analysis", "--show-analyses", "--show", dest="SHOW_ANALYSES", action="append", default=[], help="show the details of an analysis") anagroup.add_argument("--show-bibtex", dest="SHOW_BIBTEX", action="store_true", default=False, help="show BibTeX entries for all used analyses") anagroup.add_argument("--analysis-path", dest="ANALYSIS_PATH", metavar="PATH", default=None, help="specify the analysis search path (cf. $RIVET_ANALYSIS_PATH).") # TODO: remove/deprecate the append? anagroup.add_argument("--analysis-path-append", dest="ANALYSIS_PATH_APPEND", metavar="PATH", default=None, help="append to the analysis search path (cf. $RIVET_ANALYSIS_PATH).") anagroup.add_argument("--pwd", dest="ANALYSIS_PATH_PWD", action="store_true", default=False, help="append the current directory (pwd) to the analysis/data search paths (cf. $RIVET_ANALYSIS_PATH).") extragroup = parser.add_argument_group("Extra run settings") extragroup.add_argument("-o", "-H", "--histo-file", dest="HISTOFILE", default="Rivet.yoda", help="specify the output histo file path (default = %(default)s)") extragroup.add_argument("-p", "--preload-file", dest="PRELOADFILE", default=None, help="specify and old yoda file to initialize (default = %(default)s)") extragroup.add_argument("--no-histo-file", dest="WRITE_DATA", action="store_false", default=True, help="don't write out any histogram file at the end of the run (default = write)") extragroup.add_argument("-x", "--cross-section", dest="CROSS_SECTION", default=None, metavar="XS", help="specify the signal process cross-section in pb") extragroup.add_argument("-n", "--nevts", dest="MAXEVTNUM", type=int, default=None, metavar="NUM", help="restrict the max number of events to process") extragroup.add_argument("--nskip", dest="EVTSKIPNUM", type=int, default=0, metavar="NUM", help="skip NUM events read from input before beginning processing") extragroup.add_argument("--skip-weights", dest="SKIP_WEIGHTS", action="store_true", default=False, help="only run on the nominal weight") extragroup.add_argument("--weight-cap", dest="WEIGHT_CAP", type=float, default=None, help="set maximum abs(weight) to be used for histogram fills") extragroup.add_argument("--runname", dest="RUN_NAME", default=None, metavar="NAME", help="give an optional run name, to be prepended as a 'top level directory' in histo paths") extragroup.add_argument("--ignore-beams", dest="IGNORE_BEAMS", action="store_true", default=False, help="ignore input event beams when checking analysis compatibility. " "WARNING: analyses may not work correctly, or at all, with inappropriate beams") extragroup.add_argument("-d", "--dump", "--histo-interval", dest="DUMP_PERIOD", type=int, default=1000, metavar="NUM", help="specify the number of events between histogram file updates, " "default = %(default)s. Set to 0 to only write out at the end of the run. " "Note that intermediate histograms will be those from the analyze step " "only, except for analyses explicitly declared Reentrant for which the " "finalize function is executed first.") timinggroup = parser.add_argument_group("Timeouts and periodic operations") timinggroup.add_argument("--event-timeout", dest="EVENT_TIMEOUT", type=int, default=21600, metavar="NSECS", help="max time in whole seconds to wait for an event to be generated from the specified source (default = %(default)s)") timinggroup.add_argument("--run-timeout", dest="RUN_TIMEOUT", type=int, default=None, metavar="NSECS", help="max time in whole seconds to wait for the run to finish. This can be useful on batch systems such " "as the LCG Grid where tokens expire on a fixed wall-clock and can render long Rivet runs unable to write " "out the final histogram file (default = unlimited)") verbgroup = parser.add_argument_group("Verbosity control") parser.add_argument("-l", dest="NATIVE_LOG_STRS", action="append", default=[], help="set a log level in the Rivet library") verbgroup.add_argument("-v", "--verbose", action="store_const", const=logging.DEBUG, dest="LOGLEVEL", default=logging.INFO, help="print debug (very verbose) messages") verbgroup.add_argument("-q", "--quiet", action="store_const", const=logging.WARNING, dest="LOGLEVEL", default=logging.INFO, help="be very quiet") args = parser.parse_args() ## Print the version and exit if args.SHOW_VERSION: print("rivet v%s" % rivet.version()) sys.exit(0) ## Override/modify analysis search path if args.ANALYSIS_PATH: rivet.setAnalysisLibPaths(args.ANALYSIS_PATH.split(":")) rivet.setAnalysisDataPaths(args.ANALYSIS_PATH.split(":")) if args.ANALYSIS_PATH_APPEND: for ap in args.ANALYSIS_PATH_APPEND.split(":"): rivet.addAnalysisLibPath(ap) rivet.addAnalysisDataPath(ap) if args.ANALYSIS_PATH_PWD: rivet.addAnalysisLibPath(os.path.abspath(".")) rivet.addAnalysisDataPath(os.path.abspath(".")) ## Configure logging logging.basicConfig(level=args.LOGLEVEL, format="%(message)s") for l in args.NATIVE_LOG_STRS: name, level = None, None try: name, level = l.split("=") except: name = "Rivet" level = l ## Fix name if name != "Rivet" and not name.startswith("Rivet."): name = "Rivet." + name try: ## Get right error type level = rivet.LEVELS.get(level.upper(), None) logging.debug("Setting log level: %s %d" % (name, level)) rivet.setLogLevel(name, level) except: logging.warning("Couldn't process logging string '%s'" % l) ############################ ## Listing available analyses/keywords def getAnalysesByKeyword(alist, kstring): add, veto, ret = [], [], [] bits = [i for i in kstring.replace("^@", "@^").split("@") if len(i) > 0] for b in bits: if b.startswith("^"): veto.append(b.strip("^")) else: add.append(b) add = set(add) veto = set(veto) for a in alist: kwds = set([i.lower() for i in rivet.AnalysisLoader.getAnalysis(a).keywords()]) if kwds.intersection(veto) and len(kwds.intersection(add)) == len(list(add)): ret.append(a) return ret ## List of analyses all_analyses = rivet.AnalysisLoader.analysisNames() if args.LIST_ANALYSES: ## Treat args as case-insensitive regexes if present regexes = None if args.ARGS: import re regexes = [re.compile(arg, re.I) for arg in args.ARGS] # import tempfile, subprocess # tf, tfpath = tempfile.mkstemp(prefix="rivet-list.") names = [] msg = [] for aname in all_analyses: if not regexes: toshow = True else: toshow = False for regex in regexes: if regex.search(aname): toshow = True break if toshow: names.append(aname) msg.append('') if args.LOGLEVEL <= logging.INFO: a = rivet.AnalysisLoader.getAnalysis(aname) st = "" if a.status() == "VALIDATED" else ("[%s] " % a.status()) # detex will very likely introduce some non-ASCII chars from # greek names in analysis titles. # The u"" prefix and explicit print encoding are necessary for # py2 to handle this properly msg[-1] = u"%s%s" % (st, a.summary()) if args.LOGLEVEL < logging.INFO: if a.keywords(): msg[-1] += u" [%s]" % " ".join(a.keywords()) if a.luminosityfb(): msg[-1] += u" [ \int L = %s fb^{-1} ]" % a.luminosityfb() msg = rivet.util.detex(msg) retlist = '\n'.join([ u"%-25s %s" % (a,m) for a,m in zip(names,msg) ]) - retlist = retlist.encode('utf-8') + #retlist = retlist.encode('utf-8') print(retlist) sys.exit(0) def getKeywords(alist): all_keywords = [] for a in alist: all_keywords.extend(rivet.AnalysisLoader.getAnalysis(a).keywords()) all_keywords = [i.lower() for i in all_keywords] return sorted(list(set(all_keywords))) ## List keywords if args.LIST_KEYWORDS: # a = rivet.AnalysisLoader.getAnalysis(aname) for k in getKeywords(all_analyses): print(k) sys.exit(0) ## Show analyses' details if len(args.SHOW_ANALYSES) > 0: toshow = [] for i, a in enumerate(args.SHOW_ANALYSES): a_up = a.upper() if a_up in all_analyses and a_up not in toshow: toshow.append(a_up) else: ## Treat as a case-insensitive regex import re regex = re.compile(a, re.I) for ana in all_analyses: if regex.search(ana) and a_up not in toshow: toshow.append(ana) msgs = [] for i, name in enumerate(sorted(toshow)): import textwrap ana = rivet.AnalysisLoader.getAnalysis(name) msg = "" msg += name + "\n" msg += (len(name) * "=") + "\n\n" msg += rivet.util.detex(ana.summary()) + "\n\n" msg += "Status: " + ana.status() + "\n\n" # TODO: reduce to only show Inspire in v3 if ana.inspireId(): msg += "Inspire ID: " + ana.inspireId() + "\n" msg += "Inspire URL: http://inspire-hep.net/record/" + ana.inspireId() + "\n" msg += "HepData URL: http://hepdata.net/record/ins" + ana.inspireId() + "\n" elif ana.spiresId(): msg += "Spires ID: " + ana.spiresId() + "\n" msg += "Inspire URL: http://inspire-hep.net/search?p=find+key+" + ana.spiresId() + "\n" msg += "HepData URL: http://hepdata.cedar.ac.uk/view/irn" + ana.spiresId() + "\n" if ana.year(): msg += "Year of publication: " + ana.year() + "\n" if ana.bibKey(): msg += "BibTeX key: " + ana.bibKey() + "\n" msg += "Authors:\n" for a in ana.authors(): msg += " " + a + "\n" msg += "\n" msg += "Description:\n" twrap = textwrap.TextWrapper(width=75, initial_indent=2*" ", subsequent_indent=2*" ") msg += twrap.fill(rivet.util.detex(ana.description())) + "\n\n" if ana.experiment(): msg += "Experiment: " + ana.experiment() if ana.collider(): msg += "(%s)" % ana.collider() msg += "\n" # TODO: move this formatting into Analysis or a helper function? if ana.requiredBeams(): def pid_to_str(pid): if pid == 11: return "e-" elif pid == -11: return "e+" elif pid == 2212: return "p+" elif pid == -2212: return "p-" elif pid == 1000010020: return "d" elif pid == 1000130270: return "Al" elif pid == 1000290630: return "Cu" elif pid == 1000541290: return "Xe" elif pid == 1000791970: return "Au" elif pid == 1000822080: return "Pb" elif pid == 1000922380: return "U" elif pid == 10000: return "*" else: return str(pid) beamstrs = [] for bp in ana.requiredBeams(): beamstrs.append(pid_to_str(bp[0]) + " " + pid_to_str(bp[1])) msg += "Beams: " + ", ".join(beamstrs) + "\n" if ana.requiredEnergies(): msg += "Beam energies: " + "; ".join(["(%0.1f, %0.1f) GeV\n" % (epair[0], epair[1]) for epair in ana.requiredEnergies()]) else: msg += "Beam energies: ANY\n" if ana.runInfo(): msg += "Run details:\n" twrap = textwrap.TextWrapper(width=75, initial_indent=2*" ", subsequent_indent=4*" ") for l in ana.runInfo().split("\n"): msg += twrap.fill(l) + "\n" if ana.luminosityfb(): msg+= "\nIntegrated data luminosity = %s inverse fb.\n"%ana.luminosityfb() if ana.keywords(): msg += "\nAnalysis keywords:" for k in ana.keywords(): msg += " %s"%k msg+= "\n\n" if ana.references(): msg += "\n" + "References:\n" for r in ana.references(): url = None if r.startswith("arXiv:"): code = r.split()[0].replace("arXiv:", "") url = "http://arxiv.org/abs/" + code elif r.startswith("doi:"): code = r.replace("doi:", "") url = "http://dx.doi.org/" + code if url is not None: r += " - " + url msg += " " + r + "\n" ## Add to the output msgs.append(msg) ## Write the combined messages to a temporary file and page it if msgs: try: import tempfile, subprocess tffd, tfpath = tempfile.mkstemp(prefix="rivet-show.") msgsum = u"\n\n".join(msgs) msgsum = msgsum.encode('utf-8') os.write(tffd, msgsum) if sys.stdout.isatty(): pager = subprocess.Popen(["less", "-FX", tfpath]) #, stdin=subprocess.PIPE) pager.communicate() else: f = open(tfpath) print(f.read()) f.close() finally: os.unlink(tfpath) #< always clean up sys.exit(0) ############################ ## Actual analysis runs ## We allow comma-separated lists of analysis names -- normalise the list here newanas = [] for a in args.ANALYSES: if "," in a: newanas += a.split(",") elif "@" in a: #< NB. this bans combination of ana lists and keywords in a single arg temp = getAnalysesByKeyword(all_analyses, a) for i in temp: newanas.append(i) else: newanas.append(a) args.ANALYSES = newanas ## Parse supplied cross-section if args.CROSS_SECTION is not None: xsstr = args.CROSS_SECTION try: args.CROSS_SECTION = float(xsstr) except: import re suffmatch = re.search(r"[^\d.]", xsstr) if not suffmatch: raise ValueError("Bad cross-section string: %s" % xsstr) factor = base = None suffstart = suffmatch.start() if suffstart != -1: base = xsstr[:suffstart] suffix = xsstr[suffstart:].lower() if suffix == "mb": factor = 1e+9 elif suffix == "mub": factor = 1e+6 elif suffix == "nb": factor = 1e+3 elif suffix == "pb": factor = 1 elif suffix == "fb": factor = 1e-3 elif suffix == "ab": factor = 1e-6 if factor is None or base is None: raise ValueError("Bad cross-section string: %s" % xsstr) xs = float(base) * factor args.CROSS_SECTION = xs ## Print the available CLI options! #if args.LIST_OPTIONS: # for o in parser.option_list: # print(o.get_opt_string()) # sys.exit(0) ## Set up signal handling RECVD_KILL_SIGNAL = None def handleKillSignal(signum, frame): "Declare us as having been signalled, and return to default handling behaviour" global RECVD_KILL_SIGNAL logging.critical("Signal handler called with signal " + str(signum)) RECVD_KILL_SIGNAL = signum signal.signal(signum, signal.SIG_DFL) ## Signals to handle signal.signal(signal.SIGTERM, handleKillSignal); signal.signal(signal.SIGHUP, handleKillSignal); signal.signal(signal.SIGINT, handleKillSignal); signal.signal(signal.SIGUSR1, handleKillSignal); signal.signal(signal.SIGUSR2, handleKillSignal); try: signal.signal(signal.SIGXCPU, handleKillSignal); except: pass ## Identify HepMC files/streams ## TODO: check readability, deal with stdin if len(args.ARGS) > 0: HEPMCFILES = args.ARGS else: HEPMCFILES = ["-"] ## Event number logging def logNEvt(n, starttime, maxevtnum): if n % 10000 == 0: nevtloglevel = logging.CRITICAL elif n % 1000 == 0: nevtloglevel = logging.WARNING elif n % 100 == 0: nevtloglevel = logging.INFO else: nevtloglevel = logging.DEBUG if logging.getLogger().getEffectiveLevel() > nevtloglevel: return currenttime = datetime.datetime.now().replace(microsecond=0) elapsedtime = currenttime - starttime msg = "Event {:d} ({} elapsed)".format(n, str(elapsedtime)) # if maxevtnum is None: # logging.log(nevtloglevel, "Event %d (%s elapsed)" % (n, str(elapsedtime))) # else: # remainingtime = (maxevtnum-n) * elapsedtime.total_seconds() / float(n) # eta = time.strftime("%a %b %d %H:%M", datetime.localtime(currenttime + remainingtime)) # logging.log(nevtloglevel, "Event %d (%d s elapsed / %d s left) -> ETA: %s" % # (n, elapsedtime, remainingtime, eta)) if sys.stdout.isatty() and nevtloglevel <= logging.INFO: print(msg, end="\r") sys.stdout.flush() else: logging.log(nevtloglevel, msg) ## Do some checks on output histo file, before we stat the event loop histo_parentdir = os.path.dirname(os.path.abspath(args.HISTOFILE)) if not os.path.exists(histo_parentdir): logging.error('Parent path of output histogram file does not exist: %s\nExiting.' % histo_parentdir) sys.exit(4) if not os.access(histo_parentdir,os.W_OK): logging.error('Insufficient permissions to write output histogram file to directory %s\nExiting.' % histo_parentdir) sys.exit(4) ## Set up analysis handler RUNNAME = args.RUN_NAME or "" ah = rivet.AnalysisHandler(RUNNAME) ah.setIgnoreBeams(args.IGNORE_BEAMS) ah.skipMultiWeights(args.SKIP_WEIGHTS) if args.WEIGHT_CAP != None: ah.setWeightCap(args.WEIGHT_CAP) for a in args.ANALYSES: ## Print warning message and exit if not a valid analysis name if not rivet.stripOptions(a) in all_analyses: logging.warning("'%s' is not a known Rivet analysis! Do you need to set RIVET_ANALYSIS_PATH or use the --pwd switch?\n" % a) # TODO: lay out more neatly, or even try for a "did you mean XXXX?" heuristic? logging.warning("There are %d currently available analyses:\n" % len(all_analyses) + ", ".join(all_analyses)) sys.exit(1) logging.debug("Adding analysis '%s'" % a) ah.addAnalysis(a) if args.PRELOADFILE is not None: ah.readData(args.PRELOADFILE) if args.DUMP_PERIOD: ah.dump(args.HISTOFILE, args.DUMP_PERIOD) if args.SHOW_BIBTEX: bibs = [] for aname in sorted(ah.analysisNames()): ana = rivet.AnalysisLoader.getAnalysis(aname) bibs.append("% " + aname + "\n" + ana.bibTeX()) if bibs: print("\nBibTeX for used Rivet analyses:\n") print("% --------------------------\n") print("\n\n".join(bibs) + "\n") print("% --------------------------\n") ## Read and process events run = rivet.Run(ah) if args.CROSS_SECTION is not None: logging.info("User-supplied cross-section = %e pb" % args.CROSS_SECTION) run.setCrossSection(args.CROSS_SECTION) if args.LIST_USED_ANALYSES is not None: run.setListAnalyses(args.LIST_USED_ANALYSES) ## Print platform type import platform starttime = datetime.datetime.now().replace(microsecond=0) logging.info("Rivet %s running on machine %s (%s) at %s" % \ (rivet.version(), platform.node(), platform.machine(), str(starttime))) def min_nonnull(a, b): "A version of min which considers None to always be greater than a real number" if a is None: return b if b is None: return a return min(a, b) ## Set up an event timeout handler class TimeoutException(Exception): pass if args.EVENT_TIMEOUT or args.RUN_TIMEOUT: def evttimeouthandler(signum, frame): logging.warn("It has taken more than %d secs to get an event! Is the input event stream working?" % min_nonnull(args.EVENT_TIMEOUT, args.RUN_TIMEOUT)) raise TimeoutException("Event timeout") signal.signal(signal.SIGALRM, evttimeouthandler) ## Init run based on one event hepmcfile = HEPMCFILES[0] ## Apply a file-level weight derived from the filename hepmcfileweight = 1.0 if ":" in hepmcfile: hepmcfile, hepmcfileweight = hepmcfile.rsplit(":", 1) hepmcfileweight = float(hepmcfileweight) try: if args.EVENT_TIMEOUT or args.RUN_TIMEOUT: signal.alarm(min_nonnull(args.EVENT_TIMEOUT, args.RUN_TIMEOUT)) init_ok = run.init(hepmcfile, hepmcfileweight) signal.alarm(0) if not init_ok: logging.error("Failed to initialise using event file '%s'... exiting" % hepmcfile) sys.exit(2) except TimeoutException as te: logging.error("Timeout in initialisation from event file '%s'... exiting" % hepmcfile) sys.exit(3) except Exception as ex: logging.warning("Could not read from '%s' (error=%s)" % (hepmcfile, str(ex))) sys.exit(3) ## Event loop evtnum = 0 for fileidx, hepmcfile in enumerate(HEPMCFILES): ## Apply a file-level weight derived from the filename hepmcfileweight = 1.0 if ":" in hepmcfile: hepmcfile, hepmcfileweight = hepmcfile.rsplit(":", 1) hepmcfileweight = float(hepmcfileweight) ## Open next HepMC file (NB. this doesn't apply to the first file: it was already used for the run init) if fileidx > 0: try: run.openFile(hepmcfile, hepmcfileweight) except Exception as ex: logging.warning("Could not read from '%s' (error=%s)" % (hepmcfile, ex)) continue if not run.readEvent(): logging.warning("Could not read events from '%s'" % hepmcfile) continue ## Announce new file print() msg = "Reading events from '%s'" % hepmcfile if hepmcfileweight != 1.0: msg += " (file weight = %e)" % hepmcfileweight logging.info(msg) ## The event loop while args.MAXEVTNUM is None or evtnum-args.EVTSKIPNUM < args.MAXEVTNUM: evtnum += 1 ## Optional event skipping if evtnum <= args.EVTSKIPNUM: logging.debug("Skipping event #%i" % evtnum) run.readEvent(); if evtnum % 1000 == 0: logging.info("Skipping event #%i" % evtnum) continue ## Only log the event number once we're actually processing logNEvt(evtnum, starttime, args.MAXEVTNUM) ## Process this event processed_ok = run.processEvent() if not processed_ok: logging.warn("Event processing failed for evt #%i!" % evtnum) break ## Set flag to exit event loop if run timeout exceeded if args.RUN_TIMEOUT and (time.time() - starttime) > args.RUN_TIMEOUT: logging.warning("Run timeout of %d secs exceeded... exiting gracefully" % args.RUN_TIMEOUT) RECVD_KILL_SIGNAL = True ## Exit the loop if signalled if RECVD_KILL_SIGNAL is not None: break ## Read next event (with timeout handling if requested) try: if args.EVENT_TIMEOUT: signal.alarm(args.EVENT_TIMEOUT) read_ok = run.readEvent() signal.alarm(0) if not read_ok: break except TimeoutException as te: logging.error("Timeout in reading event from '%s'... exiting" % hepmcfile) sys.exit(3) ## Print end-of-loop messages print("\n") loopendtime = datetime.datetime.now().replace(microsecond=0) logging.info("Finished event loop at %s" % str(loopendtime)) logging.info("Cross-section = %e pb" % ah.nominalCrossSection()) ## Finalize and write out data file run.finalize() if args.WRITE_DATA: ah.writeData(args.HISTOFILE) ## Print end-of-run messages print() endtime = datetime.datetime.now().replace(microsecond=0) logging.info("Rivet run completed at %s, time elapsed = %s" % (str(endtime), str(endtime-starttime))) logging.info("Histograms written to %s" % os.path.abspath(args.HISTOFILE))