Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# (c) Stefan Countryman, September 2, 2016 

2 

3""" 

4Utility functions for LLAMA with *no dependencies* on other LLAMA tooling. 

5""" 

6 

7import sys 

8import warnings 

9import tempfile 

10import tarfile 

11import logging 

12import gzip 

13import os 

14import subprocess 

15import importlib 

16from pathlib import Path 

17from pkgutil import iter_modules 

18from types import FunctionType 

19from stat import S_ISREG, ST_MTIME, ST_MODE 

20from subprocess import check_call 

21from collections import namedtuple 

22from textwrap import wrap 

23from pygments import highlight 

24from pygments.lexers import get_lexer_by_name 

25from pygments.formatters import Terminal256Formatter 

26from pygments.util import ClassNotFound 

27import six 

28from six import string_types 

29import functools 

30from llama.classes import ( 

31 COLOR, 

32 optional_env_var, 

33 GenerationError, # gets imported from utils elsewhere TODO move to classes 

34 AbstractFileHandler, 

35) 

36 

37_MAINPROC = [True] 

38 

39 

40def label_worker_proc(): 

41 """ 

42 Label the current process as a worker process (not the main process). You 

43 should only call this as the initializer to an executor. 

44 """ 

45 _MAINPROC[0] = False 

46 

47 

48def is_main_process(): 

49 """ 

50 Return whether this is the main process (as opposed to a worker process). 

51 """ 

52 return _MAINPROC[0] 

53 

54 

55def color(fg=None, bg=None): 

56 """ 

57 Return full-color escape codes for changing foreground and background in 

58 terminals with RGB color support. 

59 

60 Parameters 

61 ---------- 

62 fg : str or list, optional 

63 The foreground (text) color. A 3-item list containing integer values 

64 for red, green, and blue *or* a hexadecimal representation of the color 

65 prefixed by an octothorpe (like in CSS). Background will not be set if 

66 not specified. 

67 bg : str or list, optional 

68 The background color. Same type as ``fg``. 

69 

70 Returns 

71 ------- 

72 esc : str 

73 The teriminal escape code to use. 

74 

75 Raises 

76 ------ 

77 ValueError 

78 If ``fg`` or ``bg`` are not as specified. 

79 """ 

80 if fg is None and bg is None: 

81 return '' 

82 args = [fg, bg] 

83 bases = ['38;2;', '48;2;'] 

84 fmt_part = '{};{};{}' 

85 parts = [] 

86 for i, a in enumerate(args): 

87 if a is not None: 

88 if not hasattr(a, '__len__'): 

89 raise ValueError("Arguments must be string, tuple, or None. " 

90 f"Got fg = {fg} and bg = {bg}") 

91 if len(a) == 7: 

92 a = args[i] = [int(a[1+2*j:3+2*j], base=16) for j in range(3)] 

93 elif len(a) != 3: 

94 raise ValueError(f"Arg {i} should be hex or (r, g, b)") 

95 parts.append(bases[i]+fmt_part.format(*a)) 

96 return '\033['+';'.join(parts)+'m' 

97 

98 

99class ColorFormatter(logging.Formatter): 

100 """A formatter that colors log output for easier reading.""" 

101 

102 COLORS = { 

103 'WARNING': COLOR.MAGENTA, 

104 'INFO': COLOR.GREEN, 

105 'DEBUG': COLOR.BLUE, 

106 'CRITICAL': COLOR.RED, 

107 'ERROR': COLOR.YELLOW, 

108 } 

109 

110 def format(self, record): 

111 levelname = record.levelname 

112 if levelname in self.COLORS: 

113 lname = record.levelname 

114 record.levelname = (COLOR.BOLD + self.COLORS[lname] + 

115 f"{lname:<8}" + COLOR.CLEAR + COLOR.UNDERLINE) 

116 return logging.Formatter.format(self, record) 

117 

118 

119LOGGER = logging.getLogger(__name__) 

120# use this formatter throughout the library 

121LOG_FORMATTER = ColorFormatter( 

122 COLOR.UNDERLINE + ( 

123 '|%(levelname)s|%(asctime)-19s|%(processName)16s:%(process)-6s|' 

124 '%(threadName)-14s|%(name)-18s|%(filename)14s:%(lineno)-4s|' 

125 '%(funcName)14s|' 

126 ) + COLOR.CLEAR + ' %(message)s', 

127 datefmt='%Y-%m-%d %H:%M:%S' 

128) 

129# define some other important directories and make sure they exist 

130DEFAULT_OUTPUT_DIR = os.path.join( 

131 os.environ.get( 

132 'XDG_DATA_HOME', 

133 os.path.join(os.path.expanduser('~'), '.local', 'share') 

134 ), 

135 'llama', 

136) 

137OUTPUT_DIR = optional_env_var( 

138 ["LLAMA_OUTPUT_DIR"], 

139 "Specify where LLAMA data, triggers, and results should be saved locally.", 

140 [DEFAULT_OUTPUT_DIR], 

141)[0] 

142DATADIR = os.path.join(OUTPUT_DIR, 'inputs') 

143TEST_DATA = os.path.join(DATADIR, 'tests') 

144TEST_EVENT_FILES = os.path.join(TEST_DATA, 'event_files') 

145DEFAULT_CACHEDIR = os.path.join( 

146 os.environ.get( 

147 'XDG_CACHE_HOME', 

148 os.path.join(os.path.expanduser('~'), '.cache') 

149 ), 

150 'llama', 

151) 

152CACHEDIR = optional_env_var( 

153 ["LLAMA_CACHEDIR"], 

154 "Specify where LLAMA data should be cached.", 

155 [DEFAULT_CACHEDIR], 

156)[0] 

157OBJECT_DIR = os.path.join(CACHEDIR, 'objects') 

158SINGLE_DETECTOR_TRIGGER_DIR = os.path.join(OUTPUT_DIR, 

159 'single_detector_triggers') 

160DEFAULT_RUN_DIR = os.path.join(OUTPUT_DIR, 'current_run/') 

161PAST_RUNS_DIR = os.path.join(OUTPUT_DIR, 'past_runs') 

162LOGDIR = os.path.join(OUTPUT_DIR, 'logs') 

163LOGFILE = os.path.join(LOGDIR, 'llama.log') 

164for default_dir in (OUTPUT_DIR, DATADIR, DEFAULT_RUN_DIR, PAST_RUNS_DIR, 

165 OBJECT_DIR, CACHEDIR, LOGDIR, SINGLE_DETECTOR_TRIGGER_DIR): 

166 if not os.path.isdir(default_dir): 

167 try: 

168 os.makedirs(default_dir) 

169 except OSError as err: 

170 LOGGER.error("Could not create default directory %s. Error: %s", 

171 default_dir, err) 

172 warnings.warn(("Could not create default directory expected by " 

173 "llama: {} Error: {}").format(default_dir, err)) 

174 

175TB_LEXER = get_lexer_by_name('py3tb') # lexer 

176try: 

177 TB_FORMATTER = Terminal256Formatter(style='native') 

178except ClassNotFound: 

179 LOGGER.debug("Can't load 'native' pygments style, using default.") 

180 TB_FORMATTER = Terminal256Formatter() 

181SIDEREAL_DAY = 0.997269566 

182MAX_GRID_SCALE_RAD = 1e-7 

183# graphviz .dot graph file format for visualization functions 

184# https://www.graphviz.org/doc/info/shapes.html 

185EDGEFMT = (r'"{depnum}" -> "{num}" [' 

186 r'arrowtail="none", ' 

187 r'color="{color}", ' 

188 # r'arrowhead="dot"' 

189 r'];') 

190DOTFMT = """digraph "{name}" {{ 

191charset="utf-8" 

192splines=ortho 

193bgcolor={bgcolor} 

194margin=0 

195rankdir=LR 

196{nodes} 

197{edges} 

198}} 

199""" 

200GRAPH_EXTENSIONS = ( 

201 "png", 

202 "pdf", 

203 "svg", 

204) 

205 

206 

207def setup_logger(logfile, loglevel='DEBUG'): 

208 """Set up a logger for ``llama`` and all submodules that logs to both 

209 ``logfile`` and to STDOUT. You will still need to actually make a logger 

210 for whatever module you are calling this from. 

211 

212 Parameters 

213 ---------- 

214 logfile : str 

215 The logfile to write all output to. If the path equals or resolves to 

216 ``/dev/null``, no logfile will be configured. 

217 loglevel : int, str, or NoneType, optional 

218 The loglevel to pass to ``setLevel`` for the ``StreamHandler`` writing 

219 to the terminal; the ``logfile`` handler always writes at maximum 

220 verbosity (``DEBUG``). 

221 """ 

222 logger_llama = logging.getLogger('llama') 

223 logger_llama.setLevel(logging.DEBUG) 

224 # set up stream handler for logger and format it properly 

225 if loglevel is not None: 

226 log_stream = logging.StreamHandler() 

227 log_stream.setFormatter(LOG_FORMATTER) 

228 log_stream.setLevel(loglevel) 

229 logger_llama.addHandler(log_stream) 

230 # set up file handler for logger and format it properly 

231 log = Path(logfile) 

232 if log == '/dev/null' or log.resolve() == Path('/dev/null').resolve(): 

233 return 

234 log_file = logging.FileHandler(logfile, 'a') 

235 log_file.setFormatter(LOG_FORMATTER) 

236 logger_llama.addHandler(log_file) 

237 

238 

239# TODO update this so that it dynamically downloads test files from some 

240# testing branch as needed to $XDG_DATA_HOME. 

241def get_test_file(filename, eventid): 

242 """Get the path to an example file as would be generated by LLAMA. These 

243 are stored in a single directory (equivalent to an event directory) and can 

244 be used for unit, integration, and doc tests. 

245 

246 Parameters 

247 ---------- 

248 filename : str 

249 The name of the file as it would appear in an event directory with no 

250 path information before it. 

251 eventid : str 

252 The name of the event directory we will be using for this unit test. 

253 Since different test cases apply to different file handlers, you'll 

254 need to make sure to avoid specifying an ``eventid`` that this 

255 filehandler is not meant to be tested on (whatever the reason). 

256 

257 Returns 

258 ------- 

259 test_filepath : str 

260 The full path to the file to be used for testing. 

261 """ 

262 test_filepath = os.path.join(TEST_EVENT_FILES, eventid, filename) 

263 assert os.path.isfile(test_filepath) 

264 return test_filepath 

265 

266 

267def bytes2str(bytes_or_str): 

268 """Convert an object that could be a bytes object (in python3) to a str 

269 object. If it is already a str object, leave it alone. Used for 

270 python2/python3 compatibility.""" 

271 if not isinstance(bytes_or_str, str): 

272 return bytes_or_str.decode("utf-8") 

273 return bytes_or_str 

274 

275 

276try: 

277 from llama.version import version as __version__ 

278except ModuleNotFoundError: 

279 LOGGER.critical("Could not determine version; llama.version not " 

280 "defined. Your installation is broken. If you are a " 

281 "developer, install the library with `pip install .` (or, " 

282 "if you know what you're doing, with `pip install -e .`, " 

283 "though this will break automatic version detection and " 

284 "necessitate a reinstall when you want to set " 

285 "llama.version to the correct value).") 

286 raise 

287 

288 

289def memoize(method=False): 

290 """A decorator that takes a method and memoizes it by storing the 

291 value in a class or instance variable. Useful when the same arguments are 

292 likely to keep recurring and the computation time is long. Data is 

293 stored in the function (for regular functions and staticmethods), the class 

294 (for classmethods) or in the instance (for properties and methods), this 

295 approach can't work for staticmethods nor bare 

296 functions. For this to work, all arguments (except ``self`` or ``cls``) 

297 must be hashable. 

298 

299 NOTE: all arguments must be hashable (with the exception of the class or 

300 instance when ``method`` is ``True``; see below). 

301 

302 Parameters 

303 ---------- 

304 method : bool, default=False 

305 If true, treat this function like a method, so that the first argument 

306 is a class or instance, and store the cache with that class or instance 

307 instead of storing it with the function. This lets you use function 

308 memoization on properties, classmethods, and instance methods even if 

309 the class or instance is not hashable. 

310 """ 

311 # pylint: disable=missing-docstring 

312 def decorator(func): 

313 #TODO remove this to reinstate memoization, or else permanently remove 

314 # memoization later. 

315 return func 

316 if not method: 

317 func_cache_closure = dict() 

318 # pylint: disable=missing-docstring 

319 @functools.wraps(func) 

320 def wrapper(*args, **kwargs): 

321 if method: 

322 if not hasattr(args[0], '_cache'): 

323 setattr(args[0], '_cache', dict()) 

324 # have a cache for all functions in this class/instance 

325 cache = args[0]._cache 

326 # this is the cache just for this function 

327 if func not in cache: 

328 cache[func] = dict() 

329 func_cache = cache[func] 

330 hash_args = args[1:] 

331 else: 

332 func_cache = func_cache_closure 

333 hash_args = args 

334 # force argument key to be immutable and hence hashable; fails if 

335 # this wouldn't work anyway (e.g. one of ``args`` is a list) 

336 key = str( 

337 hash(( 

338 tuple(hash_args), 

339 tuple((k, kwargs[k]) for k in sorted(kwargs)) 

340 )) 

341 ) 

342 if key not in func_cache: 

343 val = func(*args, **kwargs) 

344 try: 

345 func_cache[key] = val 

346 except TypeError as err: 

347 err.args = ((err.args[0] + (" Can't save result to HDF5: " 

348 "{}").format(val),) + 

349 err.args[1:]) 

350 raise err 

351 return func_cache[key] 

352 

353 wrapper.__doc__ = func.__doc__ 

354 return wrapper 

355 

356 return decorator 

357 

358 

359def write_gzip(infile, outfilename): 

360 """See whether a file is actually a valid gzip file. If not, zip it. Either 

361 way, write the result to ``outfilename``. 

362 

363 Parameters 

364 ---------- 

365 infile : file-like 

366 File-like object to be compressed to a gzip file. 

367 outfilename : str 

368 Path to the output compressed file. Will be overwritten if it already 

369 exists. 

370 

371 Returns 

372 ------- 

373 success : bool 

374 ``True`` if it was already a valid gzip file and ``False`` if we were 

375 forced to compress it. 

376 

377 Examples 

378 -------- 

379 Compress a string in a file-like ``BytesIO`` wrapper to some temporary 

380 ``.gz`` file: 

381 >>> from io import BytesIO 

382 >>> import tempfile 

383 >>> with tempfile.NamedTemporaryFile(suffix='.gz', delete=False) as f: 

384 ... outfilename = f.name 

385 >>> infile = BytesIO('this is obviously not a valid gzip file'.encode()) 

386 >>> assert not write_gzip(infile, outfilename) 

387 >>> with open(outfilename, 'rb') as actuallyzippedfile: 

388 ... assert write_gzip(actuallyzippedfile, outfilename) 

389 """ 

390 with open(outfilename, 'wb') as outfile: 

391 outfile.write(infile.read()) 

392 try: 

393 with gzip.open(outfilename) as gzfile: 

394 gzfile.read(1) # read 1 byte if this is really a gz file 

395 return True 

396 except IOError: 

397 infile.seek(0) 

398 with gzip.open(outfilename, 'wb') as gzfile: 

399 gzfile.write(infile.read()) 

400 return False 

401 

402 

403# "ra" is a perfectly clear contraction for "Right Ascension". 

404# pylint: disable=invalid-name 

405def rotate_angs2vec(ra, dec, yrot, zrot, degrees=True): 

406 """Take a list of angular sky positions (points on the sphere) and rotate 

407 them first through an angle ``yrot`` about the y-axis (right-handed) 

408 followed by an angle ``zrot`` about the z-axis. Return the new points as 

409 (x, y, z) coordinates of the rotated directions as vectors on the unit 

410 sphere. 

411 

412 Parameters 

413 ---------- 

414 ra : array-like 

415 A list or ``numpy.ndarray`` of Right Ascension values for each point on 

416 the sphere. 

417 dec : array-like 

418 A list or ``numpy.ndarray`` of Declination values for each point on the 

419 sphere. Must have same length as ``ra``. 

420 yrot : float 

421 The angle through which to do a right-handed rotation about the 

422 positive y-axis. This rotation is applied first. 

423 zrot : float 

424 The angle through which to do a right-handed rotation about the 

425 positive z-axis. This rotation is applied second. 

426 degrees : bool, optional 

427 (DEFAULT: True) If true, interpret all input angles as being measured 

428 in degrees. Otherwise, interpret them as being measured in radians. 

429 

430 Returns 

431 ------- 

432 x : array 

433 The x-coordinates of the rotated input vectors [cartesian]. 

434 y : array 

435 The y-coordinates of the rotated input vectors [cartesian]. 

436 z : array 

437 The z-coordinates of the rotated input vectors [cartesian]. 

438 

439 Examples 

440 -------- 

441 >>> import numpy as np 

442 >>> def eql(a, b, prec=1e-15): # compares floating point arrays 

443 ... return all(prec > np.array(a).flatten() - np.array(b).flatten()) 

444 >>> eql(rotate_angs2vec(0, 0, 0, 0), [1, 0, 0]) 

445 True 

446 >>> eql(rotate_angs2vec(270, 0, 0, 0), [0, -1, 0]) 

447 True 

448 >>> eql(rotate_angs2vec(270, 0, 0, 90), [1, 0, 0]) 

449 True 

450 >>> eql(rotate_angs2vec(180, 0, 90, 90), [0, 0, 1]) 

451 True 

452 >>> eql(rotate_angs2vec(0, 90, 90, 90), [0, 1, 0]) 

453 True 

454 >>> eql(rotate_angs2vec(np.pi/2, 0, 0, np.pi, degrees=False), [0, -1, 0]) 

455 True 

456 >>> eql(rotate_angs2vec(np.pi, 0, np.pi/2, 0, degrees=False), [0, 0, 1]) 

457 True 

458 """ 

459 import numpy as np 

460 twopi = 2 * np.pi 

461 if degrees: 

462 ra = np.radians(ra).reshape((-1,)) % twopi 

463 dec = np.radians(dec).reshape((-1,)) % twopi 

464 yrot = np.radians(yrot) % twopi 

465 zrot = np.radians(zrot) % twopi 

466 else: 

467 ra = np.array(ra).reshape((-1,)) % twopi 

468 dec = np.array(dec).reshape((-1,)) % twopi 

469 yrot = np.array(yrot) % twopi 

470 zrot = np.array(zrot) % twopi 

471 # create a rotation matrix to move everything into place 

472 cosy = np.cos(yrot) 

473 siny = np.sin(yrot) 

474 cosz = np.cos(zrot) 

475 sinz = np.sin(zrot) 

476 rotation_matrix = np.array([[cosz*cosy, -sinz, cosz*siny], 

477 [sinz*cosy, cosz, sinz*siny], 

478 [-siny, 0, cosy]]) 

479 # get the x, y, z coordinates of the points to be rotated 

480 cosd = np.cos(dec) 

481 xyz = np.array([cosd*np.cos(ra), cosd*np.sin(ra), np.sin(dec)]) 

482 # in python3.5+, @ is the matrix product operator in numpy; we can replace 

483 # the matrix product function call with that infix operator once we drop 

484 # support for python 3.4-. 

485 prods = np.matmul(rotation_matrix, xyz) 

486 return tuple([np.array(prods[i, :]).flatten() for i in range(3)]) 

487 

488 

489# pylint: disable=C0103 

490def rotate_angs2angs(ra, dec, yrot, zrot, degrees=True): 

491 """Take a list of angular sky positions (points on the sphere) and rotate 

492 them first through an angle ``yrot`` about the y-axis (right-handed) 

493 followed by an angle ``zrot`` about the z-axis. Return the new points as 

494 (ra, dec) coordinates of the rotated vectors. 

495 

496 Parameters 

497 ---------- 

498 ra : array-like 

499 A list or ``numpy.ndarray`` of Right Ascension values for each point on 

500 the sphere. 

501 dec : array-like 

502 A list or ``numpy.ndarray`` of Declination values for each point on the 

503 sphere. Must have same length as ``ra``. 

504 yrot : float 

505 The angle through which to do a right-handed rotation about the 

506 positive y-axis. This rotation is applied first. 

507 zrot : float 

508 The angle through which to do a right-handed rotation about the 

509 positive z-axis. This rotation is applied second. 

510 degrees : bool, optional 

511 (DEFAULT: True) If true, interpret all input angles as being measured 

512 in degrees and return values in degrees. Otherwise, interpret them as 

513 being measured in radians and return values in radians. 

514 

515 Returns 

516 ------- 

517 outra : array 

518 The Right Ascensions of the rotated input vectors in the specified 

519 units. 

520 outdec : array 

521 The Declinations of the rotated input vectors in the specified units. 

522 

523 Examples 

524 -------- 

525 >>> import numpy as np 

526 >>> def eql(a, b, prec=1e-13, mod=360): # compares floating point arrays 

527 ... a = np.array(a).flatten() 

528 ... b = np.array(b).flatten() 

529 ... return all(prec > (a%mod - b%mod)) 

530 >>> ra = range(360) 

531 >>> dec = np.zeros(360) 

532 >>> eql(rotate_angs2angs(ra, dec, 0, 0), [ra, dec]) 

533 True 

534 """ 

535 import numpy as np 

536 x, y, z = rotate_angs2vec(ra, dec, yrot, zrot, degrees) 

537 r = np.sqrt(x**2 + y**2) 

538 outra = np.arctan2(y, x) 

539 outdec = np.arctan2(z, r) 

540 if degrees: 

541 outra = np.degrees(outra) 

542 outdec = np.degrees(outdec) 

543 return (outra, outdec) 

544 

545 

546# Notes on defining a circular sky region (unused for now): 

547# mask = cosd(sigma)-sind(cDEC)*sind(dec) <= cosd(cDEC)*cosd(dec).*cosd(ra - 

548# cRA); 

549# at circle: 

550# cosd(sigma)-sind(cDEC)*sind(dec) = cosd(cDEC)*cosd(dec).*cosd(ra - cRA); 

551# cosd(ra - cRA) = (cosd(sigma)-sind(cDEC)*sind(dec)) / (cosd(cDEC)*cosd(dec)) 

552# ra - cRA = acosd((cosd(sigma)-sind(cDEC)*sind(dec)) / (cosd(cDEC)*cosd(dec))) 

553# ra = acosd((cosd(sigma)-sind(cDEC)*sind(dec)) / (cosd(cDEC)*cosd(dec))) + cRA 

554# ramax = +ra % 360 

555# ramin = -ra % 360 

556 

557 

558# pylint: disable=C0103 

559def get_grid(ra, dec, radius, pixels=100, degrees=True): 

560 """Get a list of pixel positions arranged in a rectangular grid and filling 

561 a square with sizes of length ``radius`` centered at Right Ascension ``ra`` 

562 and Declination ``dec`` with ``pixels`` as the width and height in pixels 

563 of the square grid. 

564 

565 This technically only works properly if the radius is very small, so a 

566 ValueError will be raised for radii larger than ``MAX_GRID_SCALE_RAD``. 

567 For larger sky areas, HEALPix pixelizations should be used to assure equal 

568 area. 

569 

570 Parameters 

571 ---------- 

572 ra : float 

573 The Right Ascension of the center of the grid. 

574 dec : float 

575 The Declination of the center of the grid. 

576 radius : float 

577 The radius of the circle. Must be larger than ``MAX_GRID_SCALE_RAD`` 

578 (in radians) to keep pixel areas nearly constant. 

579 pixels : float, optional 

580 The diameter, in pixels, of the circle along the cardinal directions of 

581 the grid. Higher values imply higher resolutions for the grid. 

582 degrees : bool, optional 

583 If true, interpret all input and returned angles as being measured in 

584 degrees. Otherwise, interpret them all as being measured in radians. 

585 

586 Returns 

587 ------- 

588 ras : array 

589 Right Ascension values of the grid pixels in the specified units. 

590 decs : array 

591 Declination values of the grid pixels in the specified units. 

592 area : float 

593 the (approximate) solid-angle per-pixel in the specified units. 

594 

595 Raises 

596 ------ 

597 ValueError 

598 Raised when the ``radius`` is too large (larger than 

599 ``MAX_GRID_SCALE_RAD``, see note above). 

600 """ 

601 import numpy as np 

602 area = (float(radius) / pixels)**2 

603 if degrees: 

604 ra = np.radians(ra) 

605 dec = np.radians(dec) 

606 radius = np.radians(radius) 

607 if radius > MAX_GRID_SCALE_RAD: 

608 raise ValueError( 

609 'radius must be less than {}'.format(MAX_GRID_SCALE_RAD) 

610 ) 

611 grid_vals = np.linspace(-radius, radius, pixels).reshape((1, -1)) 

612 ras = (grid_vals * np.ones((pixels, 1))).flatten() 

613 decs = (grid_vals.transpose() * np.ones((1, pixels))).flatten() 

614 ras, decs = rotate_angs2angs(ras, decs, -dec, ra, degrees=False) 

615 if degrees: 

616 ras = np.degrees(ras) 

617 decs = np.degrees(decs) 

618 return (ras, decs, area) 

619 

620# convert between zenith/azumuth and right ascension/declination units, where 

621# zenith/aziumuth are measured at ICECUBE's position (south pole) 

622 

623 

624def color_logger(loglevel=None, outfile=sys.stdout): 

625 """Return a function ``log`` that prints formatted strings to either 

626 ``sys.stdout`` or with the logger using a certain log level (if 

627 ``loglevel`` is specified). If ``loglevel`` is specified, then ``outfile`` 

628 is ignored.""" 

629 

630 def log(*args, **kwargs): 

631 """Print formatted strings to STDOUT, unless some log level is set, 

632 in which case log it. Optionall, specify a color from ``COLOR`` as 

633 ``col``.""" 

634 msg = ''.join([format(a) for a in args]) 

635 if 'col' in kwargs: 

636 msg = getattr(COLOR, kwargs['col']) + msg + COLOR.CLEAR 

637 if loglevel is None: 

638 print(msg, file=outfile) 

639 else: 

640 LOGGER.__getattribute__(loglevel)(msg) 

641 

642 return log 

643 

644 

645def archive_figs(plots, outfile, exts=('pdf', 'png'), fname_list=None, 

646 close_figs=False): 

647 """Take a list of figures and save them with filenames that look like 

648 ``{fname}.{suffix}`` to a ``.tar.gz`` archive named ``outfile``. The 

649 ``list_index`` will be the zero-padded index of the given plot in the 

650 ``plots`` list. Default filenames are generated if ``fname_list`` is not 

651 given. For instance, with no ``fname_list`` specified, the third plot in 

652 ``plots`` would have files named named ``plt_003.pdf`` and ``plt_003.png`` 

653 saved to ``outfile`` (assuming default ``kwargs`` values). 

654 

655 Parameters 

656 ---------- 

657 plots : list 

658 A list of ``matplotlib`` figures to save to file. 

659 outfile : str 

660 Filename of the output archive. Should end in ``.tar.gz`` since this will 

661 be a zipped tarball of figures. 

662 exts : list, optional 

663 List of filename extensions to use. Each extension becomes the 

664 ``suffix`` in the above format string. Allows multiple image formats to 

665 be saved for each plot. 

666 fname_list : list, optional 

667 A list of file names to use for each figure (sans file extensions). 

668 *NOTE:* If not provided, some default filenames are generated instead. 

669 These follow the filename pattern ``plt_{list_index}.{suffix}``. 

670 close_figs : bool, optional 

671 If ``True``, clear the figures in ``plots`` after plotting is finished. 

672 This will help reduce memory usage if the plots are not going to be 

673 reused. 

674 """ 

675 if fname_list is None: 

676 fname_list = ['plt_{:03d}.{}'.format(i, ext) 

677 for ext in exts 

678 for i in range(len(plots))] 

679 else: 

680 fname_list = [f + '.' + e for e in exts for f in fname_list] 

681 # the default arg value below is due to a scoping issue with lambda 

682 # closures in python; it explicitly establishes the scope of ``p`` within 

683 # the lambda rather than outside of it. for details, see: 

684 # stackoverflow.com/questions/2295290 

685 plot_writers = [lambda fname, plt=p: plt.savefig(fname) for p in plots] 

686 write_to_zip(fname_list, plot_writers * len(exts), outfile, 

687 suffix=sum([['.' + ext]*len(plots) for ext in exts], [])) 

688 if close_figs: 

689 for fig in plots: 

690 fig.clf() 

691 

692 

693def write_to_zip(fname_list, write_functions, outfilename, suffix=''): 

694 """Create a new ``.tar.gz`` tarfile archive names ``outfilename`` and fill 

695 it with filenames given in ``fname_list``. ``write_functions`` should 

696 be a list of functions that takes an input filename as its only argument 

697 and writes data to that filename. It should have the same length as 

698 ``fname_list``, since each element of ``write_functions`` will be used to 

699 generate the corresponding file in ``fname_list``. If the 

700 ``write_functions`` expect some sort of specific file extension, that can 

701 be provided by manually setting ``suffix`` (DEFAULT: '') to the appropriate 

702 extension. If each file requires its own specific filename extension, then 

703 ``suffix`` can be specified as a list of suffixes with length matching 

704 ``fname_list`` and ``write_functions``. 

705 

706 Examples 

707 -------- 

708 >>> import tempfile, tarfile 

709 >>> def foo(file): 

710 ... with open(file, 'w') as f: 

711 ... f.write('foo!') 

712 >>> write_functions = [foo] * 3 

713 >>> fname_list = ['bar.txt', 'baz.txt', 'quux.txt'] 

714 >>> outfile = tempfile.NamedTemporaryFile(delete=False) 

715 >>> outfilename = outfile.name 

716 >>> outfile.close() 

717 >>> write_to_zip(fname_list, write_functions, outfilename, suffix='.txt') 

718 >>> tar = tarfile.open(outfilename) 

719 >>> [f.name for f in tar.getmembers()] 

720 ['bar.txt', 'baz.txt', 'quux.txt'] 

721 >>> os.remove(outfilename) 

722 """ 

723 if not isinstance(suffix, list): 

724 suffix = len(fname_list) * [suffix] 

725 if not len(fname_list) == len(write_functions): 

726 raise ValueError('fname_list and write_functions must have same len') 

727 with tarfile.open(outfilename, 'w|gz') as tar: 

728 for i, fname in enumerate(fname_list): 

729 tmp = tempfile.NamedTemporaryFile(suffix=suffix[i], delete=False) 

730 tmpname = tmp.name 

731 tmp.close() 

732 write_functions[i](tmpname) 

733 tar.add(tmpname, arcname=fname) 

734 os.remove(tmpname) 

735 

736 

737# some common time conversions 

738def mjd2utc(mjd): 

739 """Convert MJD time to UTC string""" 

740 import astropy.time 

741 return astropy.time.Time(mjd, format='mjd').isot 

742 

743 

744def mjd2gps(mjd): 

745 """Convert MJD time to GPS seconds""" 

746 import astropy.time 

747 return astropy.time.Time(mjd, format='mjd').gps 

748 

749 

750def utc2mjd(utc): 

751 """Convert UTC time string to MJD time""" 

752 import astropy.time 

753 return astropy.time.Time(utc).mjd 

754 

755 

756def utc2gps(utc): 

757 """Convert UTC time string to GPS seconds""" 

758 import astropy.time 

759 return astropy.time.Time(utc).gps 

760 

761 

762def gps2mjd(gps): 

763 """Convert GPS seconds to MJD time""" 

764 import astropy.time 

765 return astropy.time.Time(gps, format='gps', scale='utc').mjd 

766 

767 

768def gps2utc(gps): 

769 """Convert GPS seconds to UTC string""" 

770 import astropy.time 

771 return astropy.time.Time(gps, format='gps', scale='utc').isot 

772 

773 

774def get_voevent_param(voeventpath, param_name): 

775 """Get a 'What' parameter from a VOEvent XML file and return it as a string 

776 (no clever type inference is performed). For example, get the FAR 

777 (False Alarm Rate) for event G298048 (first BNS detection), which is 

778 included as a standard test case in 'llama/data/tests/voevents': 

779 

780 Examples 

781 -------- 

782 >>> get_voevent_param(get_test_file('lvc_gcn.xml', 

783 ... 'MS181101ab-2-Initial'), 'FAR') 

784 '9.11069936486e-14' 

785 

786 Note that the type of the returned data will (probably) be a unicode 

787 string, so be sure to convert as necessary.""" 

788 if not os.path.isfile(voeventpath): 

789 raise IOError(voeventpath + ': file does not exist.') 

790 import untangle 

791 event = untangle.parse(voeventpath) 

792 params = event.voe_VOEvent.What.Param 

793 # return filter(lambda a: a['name'] == param_name, params)[0]['value'] 

794 return [p for p in params if p['name'] == param_name][0]['value'] 

795 

796 

797def get_voevent_notice_time(voeventpath): 

798 """Get a unicode string with the date and time of the notification rather 

799 than the event itself. Is in ISO format.""" 

800 if not os.path.isfile(voeventpath): 

801 raise IOError(voeventpath + ': file does not exist.') 

802 import untangle 

803 event = untangle.parse(voeventpath) 

804 return event.voe_VOEvent.Who.Date.cdata 

805 

806 

807def get_voevent_time(voeventpath): 

808 """Get a unicode string with the ISO date and time of the event: 

809 

810 Examples 

811 -------- 

812 >>> get_voevent_time(get_test_file('lvc_gcn.xml', 

813 ... 'MS181101ab-2-Initial')) 

814 '2018-11-01T22:22:46.654437' 

815 """ 

816 if not os.path.isfile(voeventpath): 

817 raise IOError(voeventpath + ': file does not exist.') 

818 import untangle 

819 event = untangle.parse(voeventpath) 

820 ol = event.voe_VOEvent.WhereWhen.ObsDataLocation.ObservationLocation 

821 return ol.AstroCoords.Time.TimeInstant.ISOTime.cdata 

822 

823 

824def getVOEventGPSSeconds(voeventpath): 

825 """Get the GPS time of the event described in this VOEvent file 

826 to the nearest second.""" 

827 return int(utc2gps(get_voevent_time(voeventpath))) 

828 

829 

830def getVOEventGPSNanoseconds(voeventpath): 

831 """Get the number of nanoseconds past the start of the current 

832 GPS second of the event described in this VOEvent file. For example, 

833 if the current GPS time is 123456789.033, this method will return 

834 33000000.""" 

835 return (get_voevent_gps_time(voeventpath) - 

836 getVOEventGPSSeconds(voeventpath)) 

837 

838 

839def get_voevent_gps_time(voeventpath): 

840 """Get the GPS time of the event described in this VOEvent file. 

841 Maximum precision is in nanoseconds, though in practice it will 

842 not be this high.""" 

843 return utc2gps(get_voevent_time(voeventpath)) 

844 

845 

846def get_voevent_role(voeventpath): 

847 """Get the role of this VOEvent, test or observation.""" 

848 import untangle 

849 event = untangle.parse(voeventpath) 

850 return event.voe_VOEvent['role'] 

851 

852 

853def send_email(subject, recipients, body='', attachments=()): 

854 """Send an email using the default configured email address. No return 

855 value. 

856 

857 Parameters 

858 ---------- 

859 recipients : list 

860 A list of strings representing email addresses of the recipients. 

861 body : str or file-like, optional 

862 Send contents of this string or file-like object. 

863 attachments : str or list, optional 

864 A single filepath or a list of filepaths to files that should also be 

865 attached to this email. 

866 """ 

867 if not isinstance(recipients, list): 

868 raise ValueError('``recipients`` must be a list of strings.') 

869 if isinstance(body, six.string_types): 

870 tmp = tempfile.TemporaryFile(mode='w') 

871 tmp.write(body) 

872 tmp.seek(0) 

873 body = tmp 

874 # add attachments, if specified 

875 if isinstance(attachments, six.string_types): 

876 attachments = [attachments] 

877 mail_cmd = ['mail', '-s', subject] 

878 for attachment in attachments: 

879 mail_cmd.append('-a') 

880 mail_cmd.append(attachment) 

881 mail_cmd.append(','.join(recipients)) 

882 LOGGER.warning('running send_email command: %s\n', mail_cmd) 

883 # try sending the email, making sure to explicitly close the file 

884 # descriptor if it was opened within this function 

885 proc = subprocess.Popen(mail_cmd, stdin=body, stdout=subprocess.PIPE, 

886 stderr=subprocess.PIPE) 

887 res, err = proc.communicate() 

888 body.close() 

889 LOGGER.debug('STDOUT:\n%s\n', res) 

890 LOGGER.debug('STDERR:\n%s\n', err) 

891 if proc.returncode != 0: 

892 raise Exception( 

893 """ 

894 MAIL FAILED. 

895 

896 subject: {} 

897 dumping stdout: {} 

898 dumping stderr: {} 

899 """.format(subject, res, err) 

900 ) 

901 

902 

903def parameter_factory(classname, description, **kwargs): 

904 r"""Return a class that can be used as a namespace for a bunch of 

905 parameters related to a specific search. Very similar to defining a 

906 ``namedtuple`` except that each parameter is defined as a property with a 

907 docstring, making these objects suitable for interactive use without having 

908 to open the source code to see parameter descriptions. Parameter names are 

909 provided as keyword argument names (see below). 

910 

911 The main point of this factory is that it provides a compact way of 

912 defining a new hashable class with descriptive docstrings; these features 

913 are good for classes representing collections of parameters (hence the 

914 function name). 

915 

916 The reason to implement this as a factory function rather than an abstract 

917 superclass is that the returned class has static attributes with docstrings 

918 available in a way that ipython can parse, making for easier interactive 

919 use of the built-in documentation. 

920 

921 Parameters 

922 ---------- 

923 classname : str 

924 The name of the new class. 

925 description : str 

926 A description of what sort of analysis these parameters are to be used 

927 for. Becomes part of the docstring of the new class (along with the 

928 descriptions included in ``parameters``). 

929 kwargs : str or function 

930 The names and descriptions (or, for derived values, formulas) of 

931 parameters to be stored. The names of the arguments become the 

932 parameter names; these will be attributes of the returned class. The 

933 values of the arguments 

934 can be strings serving as descriptions of the parameter (for 

935 INDEPENDENT parameters provided by the user, in which case the 

936 descriptive string becomes the docstring for the parameter in the 

937 returned class) or functions that take ``self`` as 

938 their only argument where ``self`` is an instance of the new parameter 

939 class and some of the attributes of said instance are 

940 used to calculate the value of this (DEPENDENT) parameter; in these 

941 cases, the docstring of the function is reused for that parameter. 

942 These dependent parameters become properties of the new class and are 

943 calculated dynamically from the class's other values (possibly 

944 including other dependent parameters as long as there are no circular 

945 method calls). 

946 

947 Returns 

948 ------- 

949 new_class : type 

950 a class whose name is ``classname``, whose properties are all the 

951 parameter names, whose docstrings are their descriptions, whose 

952 docstring is the class ``description`` combined with the names and 

953 descriptions in the ``kwargs`` parameters, and whose ``__init__`` 

954 signature requires that the parameters be passed as kwargs for the sake 

955 of explicitness in instantiation. 

956 

957 Examples 

958 -------- 

959 >>> def buz(self): 

960 ... "bar+baz" 

961 ... return self.bar + self.baz 

962 >>> def boz(self): 

963 ... "double buz" 

964 ... return 2*self.buz 

965 >>> Foo = parameter_factory( 

966 ... "Foo", 

967 ... "A silly example set of parameters.", 

968 ... bar='a fake param', 

969 ... baz='another fake param', 

970 ... buz=buz, 

971 ... boz=boz, 

972 ... ) 

973 >>> Foo.bar.__doc__ 

974 '(independent)\na fake param' 

975 >>> Foo.baz.__doc__ 

976 '(independent)\nanother fake param' 

977 >>> Foo.buz.__doc__ 

978 '(dependent)\nbar+baz' 

979 >>> Foo.boz.__doc__ 

980 '(dependent)\ndouble buz' 

981 >>> quux = Foo(bar=1, baz=2) 

982 >>> quux.bar 

983 1 

984 >>> quux.baz 

985 2 

986 >>> quux.buz 

987 3 

988 >>> quux.boz 

989 6 

990 """ 

991 docstr = description + ("\n\nAll properties listed are REQUIRED and must " 

992 "be passed as kwargs.\n\nProperties\n----------\n") 

993 parameters = kwargs 

994 newclassdict = dict() 

995 

996 # create getters and docstrings for all parameters and a class docstring 

997 # that mentions all parameters together 

998 for parameter_name, value in parameters.items(): 

999 if callable(value): 

1000 docstr += parameter_name + " : (dependent)\n" 

1001 def newmethod(self, func=value): 

1002 return func(self) 

1003 doc = ("No description given." if value.__doc__ is None else 

1004 value.__doc__) 

1005 newmethod.__doc__ = "(dependent)\n" + doc 

1006 docstr += ' {}\n'.format(('\n ').join(wrap(doc, width=75))) 

1007 else: 

1008 docstr += parameter_name + " : (independent)\n" 

1009 def newmethod(self, pname=parameter_name): 

1010 return getattr(self, '_'+pname) 

1011 newmethod.__doc__ = "(independent)\n" + value 

1012 docstr += ' {}\n'.format(('\n ').join(wrap(value, width=75))) 

1013 # indent the description of each parameter 

1014 newmethod.__name__ = parameter_name 

1015 newclassdict[parameter_name] = property(newmethod) 

1016 newclassdict['__doc__'] = docstr 

1017 

1018 def init(self, **kwargs): 

1019 for parameter_name, value in kwargs.items(): 

1020 if (not callable(value) and parameter_name in parameters and 

1021 not callable(parameters[parameter_name])): 

1022 setattr(self, '_'+parameter_name, value) 

1023 

1024 newclassdict['__init__'] = init 

1025 

1026 def _repr(self): 

1027 return "{}({})".format( 

1028 type(self).__name__, 

1029 ", ".join( 

1030 "{}={}".format(n, getattr(self, n)) 

1031 for n in sorted(parameters) if not callable(parameters[n]) 

1032 ) 

1033 ) 

1034 

1035 newclassdict['__repr__'] = _repr 

1036 newclassdict['__str__'] = _repr 

1037 

1038 def _iter(self): 

1039 return ((p, getattr(self, p)) for p in sorted(parameters)) 

1040 

1041 newclassdict['__iter__'] = _iter 

1042 

1043 def _hash(self): 

1044 return hash( 

1045 tuple( 

1046 (n, getattr(self, n)) 

1047 for n in sorted(parameters) if not callable(parameters[n]) 

1048 ) 

1049 ) 

1050 

1051 newclassdict['__hash__'] = _hash 

1052 

1053 def _eq(self, other): 

1054 return hash(self) == hash(other) and type(self) == type(other) 

1055 

1056 newclassdict['__eq__'] = _eq 

1057 return type(classname, (object,), newclassdict) 

1058 

1059 

1060def vectypes(func, types): 

1061 """Implementation of ``vecstr`` and ``veccls``.""" 

1062 def wrapper(fh, query): 

1063 if any(isinstance(query, t) for t in types): 

1064 return func(fh, query) 

1065 return any(func(fh, q) for q in query) 

1066 return wrapper 

1067 

1068 

1069def vecstr(func): 

1070 """Given a function taking a ``FileHandler`` instance ``fh`` and a 

1071 ``query`` argument and returning a ``bool``, return a function with the 

1072 same signature that first checks whether ``query`` is a ``str`` before 

1073 calling ``func``. If ``query`` is a ``str``, return ``func(fh, query)``, 

1074 and if it is not, return ``any(func(fh, q) for q in query)``, i.e. check 

1075 whether ``func`` is true for any of the values given in ``query``. Note 

1076 that this means ``query`` can either be a check against a ``str`` or 

1077 against several possible values of ``str`` contained in an iterable. This 

1078 function is an implementation detail used for downselection checks.""" 

1079 return vectypes(func, string_types) 

1080 

1081 

1082def veccls(func): 

1083 """Like ``vecstr`` but for class arguments instead of string arguments.""" 

1084 return vectypes(func, [type]) 

1085 

1086 

1087def vecfh(func): 

1088 """Like ``vecstr`` but for ``FileHandler`` instances.""" 

1089 return vectypes(func, [AbstractFileHandler]) 

1090 

1091 

1092def plot_graphviz(dot, outfile): 

1093 """Plot a ``.dot`` graph (graph here used in the mathematical sense) and save 

1094 it to ``outfile``. Calls graphviz ``dot`` executable, used for plotting 

1095 ``.dot`` graphs, so graphviz must be installed on the system. 

1096 

1097 Parameters 

1098 ---------- 

1099 dot : str 

1100 Graphviz ``.dot`` format graph (the same contents you would expect in a 

1101 ``.dot`` file). 

1102 outfile : str 

1103 Output file to save graph to. File type is inferred from ``outfile``'s 

1104 extension. This file type must be one of the ones recognized in 

1105 ``GRAPH_EXTENSIONS``. 

1106 

1107 Raises 

1108 ------ 

1109 subprocess.CalledProcessError 

1110 If the ``dot`` process fails to generate the output file. This 

1111 exception object will have the nonzero return code in it. 

1112 """ 

1113 if outfile is not None: 

1114 if outfile.endswith('.dot'): 

1115 with open(outfile, "w") as out: 

1116 out.write(dot) 

1117 return 

1118 for ext in GRAPH_EXTENSIONS: 

1119 if outfile.lower().endswith(ext): 

1120 with tempfile.NamedTemporaryFile("w") as tempout: 

1121 tempout.write(dot) 

1122 tempout.flush() 

1123 ext = outfile.split(".")[-1] 

1124 check_call(['dot', '-T'+ext, tempout.name, '-o', outfile]) 

1125 return 

1126 raise ValueError(("Unrecognized file extension {}, pick from " 

1127 "{}").format(outfile, GRAPH_EXTENSIONS)) 

1128 

1129 

1130def sort_files_by_modification_time(paths): 

1131 """Take the list of ``path`` strings sorted by ascending modification 

1132 time. This is the last time the contents were modified, or, for a new file, 

1133 its creation time.""" 

1134 entries = ((os.stat(path), path) for path in paths) 

1135 regular_files = ((stat[ST_MTIME], path) 

1136 for stat, path in entries if S_ISREG(stat[ST_MODE])) 

1137 sorted_files = sorted(regular_files, key=lambda f: f[0]) 

1138 return [f[1] for f in sorted_files] 

1139 

1140 

1141def find_in_submodules(modulename: str, predicate: FunctionType): 

1142 """Return a dict of fully-qualified variable names mapping to all objects 

1143 in ``modulename`` and its submodules for which ``predicate`` is True.""" 

1144 print("searching in ", modulename) 

1145 matches = dict() 

1146 locs = importlib.util.find_spec(modulename).submodule_search_locations 

1147 for _importer, subname, ispkg in iter_modules(locs): 

1148 fullname = f"{modulename}.{subname}" 

1149 mod = importlib.import_module(fullname) 

1150 for varname in dir(mod): 

1151 item = getattr(mod, varname) 

1152 if predicate(item): 

1153 matches[f"{fullname}.{varname}"] = item 

1154 if ispkg: 

1155 matches.update(find_in_submodules(fullname, predicate)) 

1156 return matches 

1157 

1158 

1159RemoteFileCacherTuple = namedtuple("RemoteFileCacherTuple", 

1160 ("url", "localpath")) 

1161 

1162 

1163class RemoteFileCacher(RemoteFileCacherTuple): 

1164 """A simple loader for remotely-cached data accessible on public URLs. Use 

1165 this to download cloud-based resources and locally cache them in {objdir}. 

1166 Files will only be downloaded as-needed to save space and bandwidth. 

1167 

1168 Parameters 

1169 ---------- 

1170 url : str 

1171 Specify the URL of the remote resource. 

1172 localpath : str, optional 

1173 The (optional) local path at which to cache this resource. By default, 

1174 will just be ``{objdir}/filename`` where ``filename`` is actually 

1175 taken from the remote URL filename. 

1176 """ 

1177 

1178 __doc__ = __doc__.format(objdir=OBJECT_DIR) 

1179 

1180 def __new__(cls, url, localpath=None): 

1181 if localpath is None: 

1182 from urllib.parse import urlparse 

1183 localpath = Path(OBJECT_DIR) / Path(urlparse(url).path).parts[-1] 

1184 else: 

1185 localpath = Path(localpath) 

1186 return RemoteFileCacherTuple.__new__(cls, url, localpath) 

1187 

1188 def get(self, query=None): 

1189 """If the file is not available locally, download it and store it at 

1190 ``localpath`` (do nothing if present). Return ``localpath``. Optionally 

1191 append a query string ``query``.""" 

1192 if not self.localpath.exists(): 

1193 if query is not None: 

1194 url = self.url+"?"+query 

1195 else: 

1196 url = self.url 

1197 LOGGER.info("File not cached locally, downloading %s -> %s", 

1198 url, self.localpath) 

1199 from llama.com.dl import download 

1200 download(url, str(self.localpath.absolute())) 

1201 return self.localpath 

1202 

1203 

1204# https://stackoverflow.com/a/1094933/3601493 

1205def sizeof_fmt(num, suffix='B'): 

1206 """Format the size of a file in a human-readable way. ``num`` is the file 

1207 size, presumed to be in bytes. Specify ``suffix='b'`` to indicate that the 

1208 unit of ``num`` is bits.""" 

1209 for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: 

1210 if abs(num) < 1024.0: 

1211 return "%3.1f%s%s" % (num, unit, suffix) 

1212 num /= 1024.0 

1213 return "%.1f%s%s" % (num, 'Yi', suffix) 

1214 

1215 

1216def tbhighlight(tb): 

1217 """ 

1218 Syntax-highlight a traceback for a 256-color terminal. 

1219 

1220 Parameters 

1221 ---------- 

1222 tb : str 

1223 The traceback string (as provided by ``traceback.format_exc()``) 

1224 

1225 Returns 

1226 ------- 

1227 highlighted_tb : str 

1228 The same traceback string with syntax highlighting applied. 

1229 """ 

1230 return highlight(tb, TB_LEXER, TB_FORMATTER)