ATLAS Offline Software
Loading...
Searching...
No Matches
pydraw.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3#
4# File: pydraw.py
5# Created: sss, a while ago. Added to ATLAS repository, Dec 2008
6# Purpose: Interactive python commands for plotting from a tuple-like object.
7#
8
9
10"""Interactive python commands for plotting from a tuple-like object.
11This module provides in python functionality similar to TTree::Draw
12and TTree::Scan. The major conceptual differences are:
13
14 - All expressions are written in python (and not the specialized
15 TTree::Draw language).
16 - This can be applied to any type of object providing a simple loop
17 interface (explained below), not just to TTree's. (A TTree will work;
18 there is also a wrapper for the Athena event loop.)
19
20In addition, this style of query may be somewhat
21easier to type interactively.
22
23
24Quick start
25===========
26
27Here are some examples of plotting commands to give you the flavor:
28
29 d tt.metx
30 d tt.ele_pt$i; 50 0 100*gev
31 d tt.metx:mety
32 d tt[100:1000].muo$i.pt() if abs(muo.eta)<2
33 d tt.(ele$i+ele$j).m() if $i<$j; same
34 scan tt.metx:mety
35
36To use these commands, you can pass them as strings to pydraw.cmd:
37
38 from pydraw import cmd
39 cmd('d tt.metx')
40
41Alternatively, if you execute the cmdhook function, you can type them
42directly:
43
44 from pydraw import cmdhook
45 cmdhook()
46 d tt.metx
47
48(Note that this second form doesn't work in general for code that's read
49from a file.)
50
51
52Draw syntax
53===========
54
55The general syntax for drawing a histogram looks like this:
56
57 d TUPLESPEC.[STMT@ ...]EXPR[:EXPR] [if EXPR] [; HISTSPEC]
58
59These pieces are explained in more detail below.
60
61
62Tuple specifications
63--------------------
64
65The object on which the draw operation is to be performed is given
66by TUPLESPEC. This should be the name of some object in the python
67global dictionary. Note that if the expression for the tuple
68itself contains a period or brackets, you'll need to enclose it in parentheses:
69
70 d (trees.tt[1])[:100].ele_pt$i
71
72Optionally, this can include a slice-like notation
73to restrict the rows to which the draw applies:
74
75 tuple[i:j] --- process rows i <= n < j
76 tuple[:j] --- process rows n < j
77 tuple[i:] --- process rows n >= i
78 tuple[i] --- process row i
79
80i and j can be expressions as well.
81
82The tuple object should contain a method like this:
83
84 def loop (self, func, looplo, loophi)
85
86This function should loop over rows [looplo, loophi) and
87call func on each. func will take two arguments, the first
88being the row number and the second being the event object
89(which may just be the tuple itself).
90
91If a method named loop is not available, but GetEntry and GetEntries
92methods are available, then those will be used instead.
93Similarly, if size and seekEvent are available, a wrapper
94appropriate for the Athena event loop will be used.
95
96Thus, you can make plots from within Athena like this:
97
98 d theApp.ElectronAODCollection$i.eta()
99
100
101Expressions
102-----------
103
104Once a tuple is specified, one then needs to specify what to plot.
105This is done with an arbitrary python expression, with some extensions.
106
107To plot a simple numeric variable from the tuple, just give its name.
108For example:
109
110 d tt.foo
111
112will plot the variable `foo' from each row of the tuple. You can of course
113also use more complicated expressions:
114
115 d tt.r*cos(phi)
116
117In the common case, though, the members of the tuple will be vectors
118of numbers of objects over which you want to iterate. You can iterate
119plotting over such a vector by specifying a dummy index. This is an
120identifier starting with a dollar sign:
121
122 d tt.pt_e$i
123
124(Making the dummy indices explicit was a deliberate change from the
125implicit iteration of root/paw. Such implicit iteration sometimes
126caused confusion about just what was being iterated over, and also
127when people needed to distinguish between accessing attributes
128of a container and attributes of the contained objects.)
129
130This will automatically step $i over the contents of the vector pt_e,
131plotting each one. The vector being iterated over may contain objects
132as well as numbers:
133
134 d tt.ele$i.pt()
135
136Note that the limits of the iteration are handled automatically.
137The index $i can also be used in contexts other than that of an index.
138For example, to limit the iteration in the above example to the first
139four items, one can use:
140
141 d tt.ele$i.pt() if $i<4
142
143(This uses a selection clause. This is to be described later, but the meaning
144should be obvious.)
145
146If a dummy index is used for more than one vector, the iteration limit
147will be taken from whichever vector is smaller. For example:
148
149 d tt.ele$i+muo$i
150
151will iterate min(len(ele),len(muo)) times.
152
153Multiple dummy indices may be used. In that case, all possible index
154combinations are used (a simple nested loop). If there is a symmetry
155involved, a selection clause may be used to eliminate duplicates.
156For example (given an appropriate ele definition), this will plot the
157invariant masses of all electron pairs:
158
159 d tt.(ele$i+ele$j).m() if $i>$j
160
161One can also reference individual elements of a vector using the
162notation $N. Here, the indexing is 1-based (so $1 is the first element).
163Example: to plot the pt of the leading electron
164
165 d tt.ele$1.pt()
166
167Note that one can do something similar just using standard python
168indexing directly:
169
170 d tt.ele[0].pt()
171
172This, however, will crash if ele is empty; if the $N form is used,
173an implicit selection is added to ensure that the index is in range.
174Thus, the proper equivalent of the $N-form example is:
175
176 d tt.ele[0].pt() if len(ele)>0
177
178You can also use $nCONT as an abbreviation for len(CONT):
179
180 d tt.ele[0].pt() if $nele>0
181
182Names used in expressions will be looked up first in the tuple object,
183then in the global dictionary. Note that the lookup in the tuple happens
184before looping starts, so all needed variables must already be present
185in the tuple. If you want to refer to the tuple object itself,
186you can use the special variable `_ev'. (This can be changed by assigning
187a string to pydraw._evtvar.)
188
189Multiple expressions may be given, separated by `:'. For drawing histograms,
190you can give two expressions, for the x and y axis expressions. For scanning
191tuples (see below), you give the list of expressions to be scanned.
192
193
194Selections
195----------
196
197You can select items to be filled using a boolean expression, set off
198with an `if' keyword:
199
200 d tt.ele$i.pt() if abs(ele$i.eta())<2
201
202All dummy variables are common between the variable expressions
203and the selection.
204
205
206Statements
207----------
208
209You can specify arbitrary Python expressions to be executed before looping
210starts. These come before the variables expressions, and are delimited
211with `@'. This is useful mostly for defining short aliases for tuple
212variables. For example:
213
214 d tt.ele=ElectronAODCollection@ele$i.eta() if ele$i.pt()>100*gev
215
216
217Histogram specifications
218------------------------
219
220Histogram specifications follow a semicolon. They can have the forms:
221
222 ! OPTIONS...
223 >>NAME OPTIONS...
224 NX XLO XHI OPTIONS...
225 NX XLO XHI NY YLO YHI OPTIONS...
226 OPTIONS...
227
228For the first form, specifying ! reuses the same histogram that was used
229for the last plot.
230
231For the second form, specifying >>NAME looks in the global dictionary
232for a histogram NAME and uses that.
233
234Otherwise, a new histogram is created. The binning for this histogram
235may be specified by NX XLO XHI and NY YLO YHI.
236
237Plotting options may also be given. The special option `prof' means
238to create a profile histogram. Otherwise, root drawing options may
239be used. This package uses PyAnalysisUtils.draw_obj to draw the
240histograms, so the extra options supported by that function may
241also be used. See draw_obj.py for details.
242
243The last histogram to have been drawn is available is pydraw.last_hist.
244
245
246Scan syntax
247===========
248
249The scan command is very similar to draw:
250
251 scan TUPLESPEC.[STMT@ ...]EXPR[:EXPR] [if EXPR] [; REDIRECT]
252
253Instead of drawing a histogram, scan prints out a table of the expression
254values.
255
256A semicolon may be followed by a redirection of the form >FNAME to write
257to file FNAME rather than printing, of >>FNAME to append to it.
258
259The formatting of the data printed by scan is currently pretty rudimentary.
260This should probably be improved.
261
262
263Loop syntax
264===========
265
266There is also a loop command:
267
268 loop TUPLESPEC.[STMT@ ...]EXPR[:EXPR] [if EXPR]
269
270Loop will evaluate the given expressions in the same manner as draw and scan,
271but the results of this are ignored. So it only makes sense to use loop
272to evaluate expressions for their side effects. This can be used,
273for example, to call a function that fills some large set of histograms.
274
275
276Running commands
277================
278
279The general interface to execute one of these commands is the `cmd' function,
280which takes the command as a string:
281
282 from pydraw import cmd
283 cmd ('d tt.foo')
284
285Each command is also implemented by a single function, which may be called
286directly. The command name should not be included in this case:
287
288 from pydraw import draw
289 draw ('tt.foo')
290
291Finally, if you call the function cmdhook(), then you can give the commands
292directly on the python command line:
293
294 from pydraw import cmdhook
295 cmdhook()
296 d tt.foo
297
298
299Bugs/stuff missing
300==================
301 - No way to specify an event weight when filling a histogram using
302 the draw command.
303
304 - Hoist selection code out of the dummy index loops, when they don't
305 depend on the index? For example,
306
307 d tt.foo$i if ht>100
308
309 gets implemented like:
310
311 for _it_foo in foo:
312 if _ev.ht>100:
313 Fill(_it_foo)
314
315 but it would be more efficient to pull the selection out of the loop.
316
317 - In an expr like d em.foo$i if em.bar$i>1
318 then foo always gets evaluated even if the condition is false.
319
320 - Scan formatting.
321
322
323"""
324
325
326import sys
327import string
328import tokenize
329import token
330import copy
331import ROOT
332import cppyy # noqa: F401
333from io import StringIO
334from PyAnalysisUtils.draw_obj import draw_obj, get_canvas
335
336
337try:
338 ScatterH2 = ROOT.RootUtils.ScatterH2
339except AttributeError: #pragma: NO COVER
340 ScatterH2 = ROOT.TH2F #pragma: NO COVER
341 print ("WARNING: RootUtils::ScatterH2 not available; using TH2F instead") #pragma: NO COVER
342
343
344try:
345 ROOT.TH1.kCanRebin
346 def _setCanRebin (h): #pragma: NO COVER
347 h.SetBit (ROOT.TH1.kCanRebin) #pragma: NO COVER
348 def _hasCanRebin (h): #pragma: NO COVER
349 return h.TestBit (ROOT.TH1.kCanRebin) #pragma: NO COVER
350except AttributeError: #pragma: NO COVER
351 def _setCanRebin (h): #pragma: NO COVER
352 h.GetXaxis().SetCanExtend(True) #pragma: NO COVER
353 def _hasCanRebin (h): #pragma: NO COVER
354 return h.GetXaxis().CanExtend() #pragma: NO COVER
355
356
357# The last histogram we made.
358last_hist = None
359
360
361# Dictionary in which to find global names.
362_globals = sys.modules['__main__'].__dict__
363
364
365# Characters that are legal in identifiers.
366_idchars = string.ascii_letters + string.digits + '_'
367
368# This is what's used as the event formal argument in the generated functions.
369_evtvar = '_ev'
370
371# Set this to true to dump out the generated function bodies.
372_debug = False
373
375 """Name a string safe to use as a histogram name.
376
377 Root does bad things if you put / in a histogram name, so we remove them.
378 Examples:
379 >>> print (_sanitize_hname('foo'))
380 foo
381 >>> print (_sanitize_hname('foo/bar'))
382 foo DIV bar
383 """
384 return s.replace ('/', ' DIV ')
385
386
387def _untokenize (tokens):
388 """Transform tokens back into Python source code.
389
390 Each element returned by the iterable must be a token sequence
391 with at least two elements, a token number and token value.
392
393 Unlike tokenize.untokenize(), this does not handle multiple lines.
394 It also tries not to add unneeded spaces.
395
396 Examples:
397 >>> from tokenize import generate_tokens, untokenize
398 >>> from io import StringIO
399 >>> def untokenize1(tt):
400 ... tt=list(tt)
401 ... if tt[-1][0]==0: tt=tt[:-1]
402 ... return untokenize(tt)
403 >>> untokenize1(generate_tokens(StringIO('1+1').readline))
404 '1+1'
405 >>> _untokenize(generate_tokens(StringIO('1+1').readline))
406 '1+1'
407 >>> untokenize1(generate_tokens(StringIO('foo$i>2*h').readline))
408 'foo$i>2*h'
409 >>> _untokenize(generate_tokens(StringIO('foo$i>2*h').readline))
410 'foo$i>2*h'
411 """
412 lastname = False
413 toks = []
414 toks_append = toks.append
415 for tok in tokens:
416 toknum, tokval = tok[:2]
417 tokval = tokval.strip()
418 if toknum in (token.NAME, token.NUMBER):
419 if lastname:
420 tokval = ' ' + tokval
421 lastname = True
422 else:
423 lastname = False
424 toks_append (tokval)
425 return ''.join(toks)
426
427
428def _find_outer (haystack, needle, ignore_delim = False):
429 """Look for NEEDLE in HAYSTACK (token-based. Return pair (HEAD, TAIL).
430
431 HAYSTACK and NEEDLE are both strings. Look for a token in HAYSTACK with
432 a value matching NEEDLE that is outside of any paired delimiters.
433 Also ignores things in strings.
434 If IGNORE_DELIM is True, then we do find things inside delimiters
435 (strings are still ignored).
436
437 Returns a pair (HEAD, TAIL) of the pieces of the string before and
438 after NEEDLE. If there is no match, returns (HAYSTACK, None).
439 Note that whitespace and formatting in HEAD and TAIL may differ
440 from the original string.
441
442 Examples:
443 >>> _find_outer ("head.tail1.tail2", ".")
444 ('head', 'tail1.tail2')
445 >>> _find_outer ("(head.tail1).tail2", ".")
446 ('(head.tail1)', 'tail2')
447 >>> _find_outer ("[a for a in foo if good(a)] if bar", "if")
448 ('[a for a in foo if good(a)]', 'bar')
449 >>> _find_outer ("(a [b {c . d } ] ) . e", ".")
450 ('(a [b {c . d } ] )', 'e')
451 >>> _find_outer ("a.b", ";")
452 ('a.b', None)
453 >>> _find_outer ("a '$' b", '$')
454 ("a '$' b", None)
455 >>> _find_outer ("a $ b", '$')
456 ('a', 'b')
457 >>> _find_outer ("(head.tail1).tail2", ".", True)
458 ('(head', 'tail1).tail2')
459 >>> _find_outer ('a; 1 -1 1', ';')
460 ('a', '1 -1 1')
461"""
462 tlist = tokenize.generate_tokens (StringIO(haystack).readline)
463 pend = []
464 head = []
465 for (i, (tnum, val, a, b, c)) in enumerate (tlist):
466 if tnum != token.STRING and not pend and val == needle:
467 col1 = a[1]
468 col2 = b[1]
469 return (haystack[:col1].strip(),
470 haystack[col2:].strip())
471 if not ignore_delim:
472 if val == '(':
473 pend.append (')')
474 elif val == '[':
475 pend.append (']')
476 elif val == '{':
477 pend.append ('}')
478 elif pend and val == pend[-1]:
479 pend.pop()
480 head.append ((tnum, val))
481 return (haystack, None)
482
483
484def _split_outer (haystack, needle):
485 """Split HAYSTACK at the delimiters NEEDLE, as in _find_outer.
486
487 Examples:
488 >>> _split_outer ("a,(b,c),d", ",")
489 ['a', '(b,c)', 'd']
490 >>> _split_outer ("a,,b", ",")
491 ['a', '', 'b']
492 >>> _split_outer ("a", ",")
493 ['a']
494 >>> #_split_outer ("", ",")
495 []
496"""
497 out = []
498 while True:
499 (head, tail) = _find_outer (haystack, needle)
500 head = head.strip()
501 out.append (head)
502 if tail is None:
503 break
504 else:
505 haystack = tail
506 return out
507
508
509class TreeLoopWrapper(object):
510 """Wrapper for TTree, supplying a loop method.
511
512 This class wraps a TTree class and provides a loop method
513 that will work with pydraw.
514"""
515
516 def __init__ (self, tree):
517 """Make a wrapper for a tree."""
518 self._tree = tree
519 return
520
521 def loop (self, f, looplo=0, loophi=sys.maxsize):
522 """Call f(i,tree) on rows [looplo, loophi)"""
523 tree = self._tree
524 loophi = min (loophi, tree.GetEntries())
525 getentry = tree.GetEntry
526 for i in range(looplo, loophi):
527 getentry(i)
528 f(i, tree)
529 return
530
531
532class AthenaLoopWrapper(object):
533 """Wrapper for the Athena event loop, supplying a loop method.
534
535 This class wraps an application manager object and provides a loop method
536 that will work with pydraw.
537"""
538 def __init__ (self, app=None):
539 from AthenaPython import PyAthena #pragma: NO COVER
540 if app is None: #pragma: NO COVER
541 from AthenaCommon.AppMgr import theApp #pragma: NO COVER
542 app = theApp #pragma: NO COVER
543 self._app = app #pragma: NO COVER
544 self._sg = PyAthena.py_svc('StoreGateSvc') #pragma: NO COVER
545 return #pragma: NO COVER
546
547
548 def loop (self, f, looplo=0, loophi=sys.maxsize):
549 """Call f(i,tree) on rows [looplo, loophi)"""
550 loophi = min (loophi, self._app.size()) #pragma: NO COVER
551 getentry = self._app.seekEvent #pragma: NO COVER
552 for i in range(looplo, loophi): #pragma: NO COVER
553 getentry(i) #pragma: NO COVER
554 f(i, self) #pragma: NO COVER
555 return #pragma: NO COVER
556
557
558 def __getattr__ (self, v):
559 if not v.startswith('_'): #pragma: NO COVER
560 return self._sg[v] #pragma: NO COVER
561 raise AttributeError() #pragma: NO COVER
562
563
564class _Loopvar(object):
565 """Holds information about a dummy loop variable.
566
567 Attributes:
568 name - The name of the dummy variable.
569 ids - Set of loop identifiers (`foo' in `foo$i') with which
570 this dummy has been used.
571 explicit - Set to true if this variable is ever used on its own
572 (just $i)
573 """
574
575 def __init__ (self, name):
576 """Initialize given the name."""
577 self.name = name
578 self.ids = set()
579 self.explicit = 0
580 return
581
582 def itname (self, id):
583 """Return the iterator variable for this dummy and loop identifier ID.
584 """
585 return "_it_%s_%s" % (self.name, id)
586
587 def dumname (self):
588 """Return the dummy variable name for this dummy."""
589 return "_dum_" + self.name
590
591 def add_id (self, id):
592 """Notice this this dummy is used with loop identifier ID.
593
594 Return the iterator variable.
595 """
596 self.ids.add (id)
597 return self.itname (id)
598
599 def get_ids (self):
600 """Return the list of loop identifiers with which we've been used."""
601 return list (self.ids)
602
603
604class Draw_Cmd(object):
605 """Holds information used to implement a draw/scan/loop command.
606
607 Pass the draw string to the constructor. See the file-level comments
608 for details on the syntax of this. This will define the
609 following attributes:
610
611 errstr - If set to a string, there was an error.
612 Should be None if everything's ok.
613 tuple - The name of the tuple object.
614 tuple_o - Tuple object.
615 lo - The lower bound for row iteration.
616 hi - The upper bound for row iteration.
617 stmts - List of additional statements.
618 exprs - List of draw expressions.
619 sel - Selection expression or None.
620 sel_orig - Untransformed selection expression or None.
621 expr_orig- Untransformed plotting expression.
622 histspec - The text following `;', split into space-separated words.
623
624 Other attributes:
625 _iddict - Map from loop identifiers (`foo' in `foo$i')
626 to temp variables used to reference
627 them in the loop function.
628 _limdict - Map from loop identifiers (`foo' in `foo$2')
629 to the largest explicit index seen.
630 _loopdict - Map of loop dummy variable names to _Loopvar instances.
631"""
632
633 def __init__ (self, s):
634 """Initialize from a draw string. See above for more details."""
635
636 # Assume no error.
637 self.errstr = None
638
639 self._iddict = {}
640 self._limdict = {}
641 self._loopdict = {}
642
643 try:
644 self._tupleparse (s)
645 except Exception as e:
646 import traceback
647 self.errstr = str(e)
648 self.excstr = traceback.format_exc()
649 return
650
651
652
653 def _tupleparse (self, s):
654 """Parse a draw string. See above for more details."""
655
656 # Split off the histspec.
657 (s, self.histspec) = _find_outer (s, ';')
658
659 # ??? Don't split at spaces in delimiters.
660 # _find_outer doesn't really work for this since it operates
661 # on the tokenized string, in which whitespace doesn't appear.
662 if self.histspec is None:
663 self.histspec = []
664 else:
665 self.histspec = self.histspec.split ()
666
667 # Gotta have something.
668 s = s.strip()
669 if not s:
670 self.errstr = "Empty draw string."
671 return
672
673 # Split off the tuple part --- before the first period.
674 (tuple, s) = _find_outer (s, '.')
675 if not s:
676 self.errstr = "Missing period in tuple specification."
677 return
678
679 # Handle a range specification on the sample.
680 self._parserange (tuple)
681
682 # Try to find the sample.
683 try:
684 self.tuple_o = eval (self.tuple, _globals)
685 except NameError:
686 self.tuple_o = None
687 if not self.tuple_o:
688 self.errstr = "Can't find sample " + self.tuple
689 return
690
691 # Look for additional statements.
692 self.stmts = _split_outer (s, '@')
693 s = self.stmts[-1]
694 del self.stmts[-1]
695 self.stmts = [self._mung_expr(x) for x in self.stmts]
696
697 # Split off the selection.
698 (self.expr_orig, self.sel_orig) = _find_outer (s, "if")
699 self.sel = self._mung_expr (self.sel_orig)
700
701 self.exprs = [self._mung_expr(x) for x in
702 _split_outer (self.expr_orig, ':')]
703
704 # Check the interface of the sample. If it doesn't have
705 # the loop interface but has the root tree interface,
706 # use a wrapper.
707 if hasattr (self.tuple_o, 'loop'):
708 # Ok --- has the loop interface.
709 pass
710 elif (hasattr (self.tuple_o, 'GetEntry') and
711 hasattr (self.tuple_o, 'GetEntries')):
712 # Has the TTree interface. Use a wrapper.
713 self.tuple_o = TreeLoopWrapper (self.tuple_o)
714 elif (hasattr (self.tuple_o, 'size') and
715 hasattr (self.tuple_o, 'seekEvent')): #pragma: NO COVER
716 # Has the appmgr interface. Use a wrapper.
717 self.tuple_o = AthenaLoopWrapper (self.tuple_o) #pragma: NO COVER
718 else:
719 # An error --- complain.
720 self.errstr = ("Sample " + self.tuple +
721 " doesn't have a correct interface.")
722 return
723
724 return
725
726
727
728 def _parserange (self, tuple):
729 """Parse the range part of a draw string.
730
731 See above for more details.
732 Fills self.tuple, self.lo, self.hi.
733 """
734 lo = 0
735 hi = sys.maxsize
736 (tuple, tail) = _find_outer (tuple, '[')
737 if tail:
738 g = copy.copy (_globals)
739
740 pos = tail.find (':')
741 pos2 = tail.find (']')
742 if pos2 < 0:
743 pos2 = len (tail) #pragma: NO COVER
744 if pos < 0:
745 slo = tail[:pos2].strip()
746 if len (slo) > 0:
747 lo = int (eval (slo, g))
748 hi = lo + 1
749 else:
750 slo = tail[:pos].strip()
751 if len (slo) > 0:
752 lo = int (eval (slo, g))
753 shi = tail[pos+1:pos2].strip()
754 if len (shi) > 0:
755 hi = int (eval (shi, g))
756
757 if tuple[0] == '(' and tuple[-1] == ')':
758 tuple = tuple[1:-1].strip()
759 self.tuple = tuple
760 self.lo = lo
761 self.hi = hi
762 return
763
764
765 def _mung_id (self, id):
766 """Given a loop identifier (`foo' in `foo$i'), return the identifier
767 used to reference it in loop functions.
768 """
769 out = self._iddict.get (id)
770 if not out:
771 out = '_e_' + id
772 self._iddict[id] = out
773 return out
774
775
776 def _mung_index (self, s1, s2):
777 """Handle an explicit index reference; i.e., `foo$2'.
778
779 S1 and S2 are pieces of the string before and after the `$'.
780 Returns the modified string.
781 """
782 pos2 = 0
783 while pos2 < len(s2) and s2[pos2] in string.digits:
784 pos2 += 1
785 if pos2 == 0:
786 self.errstr = "Bad index"
787 return ''
788 i = int (s2[:pos2])
789 if i < 1:
790 self.errstr = "Bad index"
791 return ''
792 s = ("[%d]" % (i-1)) + s2[pos2:]
793 pos2 = len(s1)-1
794 while pos2 >= 0 and s1[pos2] in _idchars:
795 pos2 -= 1
796 pos2 += 1
797 if pos2 == len(s1):
798 self.errstr = "Bad index"
799 return ''
800 id = s1[pos2:]
801 s = s1[:pos2] + self._mung_id (id) + s
802 self._limdict[id] = max (i, self._limdict.get(id, 0))
803 return s
804
805
806 def _mung_n (self, s1, s2):
807 """Handle a length reference; i.e., `$nfoo'.
808
809 S1 and S2 are pieces of the string before and after the `$'.
810 Returns the modified string.
811 """
812 pos2 = 1
813 while pos2 < len(s2) and s2[pos2] in _idchars:
814 pos2 += 1
815 id = s2[1:pos2]
816 s = s1 + (" len(%s)" % self._mung_id(id)) + s2[pos2:]
817 return s
818
819
820 def _mung_loop (self, s1, s2):
821 """Handle use of a dummy loop variable, such as foo$i.
822
823 S1 and S2 are pieces of the string before and after the `$'.
824 Returns the modified string.
825 """
826
827 # Scan after the $ to find the dummy variable.
828 pos2 = 0
829 while pos2 < len(s2) and s2[pos2] in _idchars:
830 pos2 += 1
831 if pos2 == 0:
832 self.errstr = "Bad loop var"
833 return ''
834 loopvar = s2[:pos2]
835
836 # Look it up. Make a new _Loopvar object if it's not in the map.
837 ll = self._loopdict.get (loopvar)
838 if not ll:
839 ll = _Loopvar(loopvar)
840 self._loopdict[loopvar] = ll
841
842 # Is the $ after an identifier?
843 if len(s1) > 0 and s1[-1] in _idchars:
844 # Yes --- find the identifier.
845 pos3 = len(s1)-1
846 while pos3 >= 0 and s1[pos3] in _idchars:
847 pos3 -= 1
848 pos3 += 1
849 assert (len(s1) - pos3 >= 1)
850 id = s1[pos3:]
851
852 # Replace with the iterator.
853 s = s1[:pos3] + ll.add_id(id) + s2[pos2:]
854 self._mung_id (id)
855 else:
856 # Explicit use of the dummy.
857 # Replace with the dummy name and note that it was used explicitly.
858 s = s1 + ("%s" % ll.dumname()) + s2[pos2:]
859 ll.explicit = 1
860 return s
861
862
863 def _mung_expr_dollar (self, s):
864 """Process $ constructions in string S.
865
866 Returns the modified string.
867 """
868 if not s:
869 return s
870 pos = 0
871 while 1:
872 (s1, s2) = _find_outer (s[pos:], '$', True)
873 if s2 is None:
874 break
875 snew = None
876 if len(s2) > 0:
877 if s2[0] in string.digits:
878 snew = self._mung_index (s1, s2)
879 elif (s2[0] == 'n' and
880 (not (len(s1) > 0 and s1[-1] in _idchars) or
881 s1.endswith (' and') or
882 s1.endswith (' or') or
883 s1.endswith ('not'))):
884 snew = self._mung_n (s1, s2)
885 elif s2[0] in string.ascii_letters:
886 snew = self._mung_loop (s1, s2)
887 s = s[:pos]
888 if snew is None:
889 snew = s1 + '$' + s2
890 pos = pos + len(s1)+1
891 s = s + snew
892 return s
893
894
895 def _mung_expr_ids (self, s):
896 """Perform id substitution in S.
897
898 For identifiers in S that are attributes of our tuple,
899 replace them with references to the tuple attribute
900 (using _mung_id).
901
902 Returns the modified string.
903 """
904 if not s:
905 return s
906
907 tlist = tokenize.generate_tokens (StringIO(s).readline)
908 out = []
909 afterDot = False
910 for tnum, val, a, b, c in tlist:
911 if tnum == token.NAME and not afterDot:
912 if hasattr (self.tuple_o, val):
913 val = self._mung_id (val)
914 #val = _evtvar + '.' + val
915 out.append ((tnum, val))
916 # Don't mung names after a period. We may have attributes
917 # with the same names as variables in the tuple.
918 if tnum == token.OP and val == '.':
919 afterDot = True
920 else:
921 afterDot = False
922 return _untokenize (out)
923
924
925 def _mung_expr (self, s):
926 """Process $ constructions and id substitution in string S.
927
928 Returns the modified string.
929 """
930 s = self._mung_expr_dollar (s)
931 return self._mung_expr_ids (s)
932
933
934 def _make_func (self, payload, extargs = ''):
935 """Create the text for the function to process this query.
936
937 PAYLOAD is the payload expression to plug in.
938 EXTARGS is an additional string to add to the end of the
939 function's argument list (to set default values, for example).
940 Returns the function definition as a string.
941 """
942
943 sel = self.sel
944 if self._limdict:
945 limsel = ' and '.join (["len(%s)>=%d" % (self._iddict[p[0]], p[1])
946 for p in self._limdict.items()])
947 if not sel:
948 sel = limsel
949 else:
950 sel = limsel + " and (" + sel + ")"
951
952 ftext = "def _loopfunc(_i, %s%s):\n" % (_evtvar, extargs)
953 for (id1, id2) in sorted(self._iddict.items()):
954 ftext += " %s = %s.%s\n" % (id2, _evtvar, id1)
955 indent = 2
956
957 for (i,l) in sorted(self._loopdict.items()):
958 ids = sorted(l.get_ids())
959 assert (not not ids)
960 if len(ids) == 1:
961 vars = l.itname (ids[0])
962 lists = self._iddict[ids[0]]
963 else:
964 vars = "(" + ','.join([l.itname (id) for id in ids]) + ")"
965 lists = ("zip(" + ','.join([self._iddict[id] for id in ids])
966 + ")")
967 if l.explicit:
968 vars = "(%s,%s)" % (l.dumname(), vars)
969 lists = "enumerate(%s)" % lists
970 ftext += ' '*indent + "for %s in %s:\n" % (vars, lists)
971 indent += 2
972
973 for s in self.stmts:
974 ftext += ' '*indent + s + '\n'
975
976 if sel and sel != '1':
977 ftext += ' '*indent + "if (%s):\n" % sel
978 indent += 2
979
980 ftext += ' '*indent + "%s\n" % payload
981
982 if _debug:
983 print (ftext)
984
985 return ftext
986
987
988class _Bins(object):
989 """Holds the results of _get_bins. Defined attributes:
990
991 nbins
992 lo
993 hi
994 rebin
995 """
996
997
998def _get_bins (args, ndim, axis):
999 """Parse bin specifications from split list of arguments ARGS.
1000 NDIM is 1 or 2, and AXIS is 0 or 1, for the x or y axis.
1001
1002 Examples:
1003 >>> from PyAnalysisUtils import pydraw
1004 >>> pydraw._globals = globals()
1005 >>> import ROOT
1006 >>> ROOT.gPad.Range(0, 1,2,3)
1007 >>> b = _get_bins (["50", "10", "100"], 1, 0)
1008 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1009 50 10.0 100.0 0
1010 >>> b = _get_bins ([], 1, 0)
1011 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1012 50 0 1 1
1013 >>> b = _get_bins (["!", "10"], 1, 0)
1014 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1015 50 10.0 11.0 1
1016 >>> b = _get_bins (["!", "!", "10"], 1, 0)
1017 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1018 50 0 10.0 0
1019 >>> scale = 10
1020 >>> b = _get_bins (["50", "0", "2*scale"], 1, 0)
1021 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1022 50 0.0 20.0 0
1023 >>> b = _get_bins ([], 2, 0)
1024 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1025 50 0.0 2.0 1
1026 >>> b = _get_bins ([], 2, 1)
1027 >>> print (b.nbins, b.lo, b.hi, b.rebin)
1028 50 1.0 3.0 1
1029 >>> b = _get_bins ([], 2, 2)
1030 Traceback (most recent call last):
1031 ...
1032 AssertionError
1033 """
1034
1035 g = copy.copy (_globals)
1036
1037 bins = _Bins()
1038
1039 bins.nbins = 0
1040 if len(args) >= 1 and args[0] != '!' and len(args[0]) > 0:
1041 bins.nbins = int (eval (args[0], g))
1042 if bins.nbins <= 0:
1043 bins.nbins = 50
1044
1045 bins.lo = 0
1046 if len(args) >= 2 and args[1] != '!' and len(args[1]) > 0:
1047 bins.lo = float (eval (args[1], g))
1048
1049 bins.hi = 0
1050 if len(args) >= 3 and args[2] != '!' and len(args[2]) > 0:
1051 bins.hi = float (eval (args[2], g))
1052
1053 bins.rebin = 0
1054 if bins.hi <= bins.lo:
1055 bins.rebin = 1
1056 if ndim == 1:
1057 bins.hi = bins.lo + 1
1058 elif axis == 0:
1059 bins.lo = ROOT.gPad.GetUxmin()
1060 bins.hi = ROOT.gPad.GetUxmax()
1061 elif axis == 1:
1062 bins.lo = ROOT.gPad.GetUymin()
1063 bins.hi = ROOT.gPad.GetUymax()
1064 else:
1065 assert 0
1066
1067 return bins
1068
1069
1070def _get_hist (ndim, args, hname, htitle):
1071 """Create a new histogram from options.
1072
1073 NDIM is the dimensionality of the histogram (1 or 2).
1074 ARGS is a list of the arguments given to specify the histogram.
1075 HNAME and HTITLE are the histogram name and title, respectively.
1076 """
1077 get_canvas()
1078
1079 # Get the x-axis bin specifications.
1080 xbins = _get_bins (args, ndim, 0)
1081 rebin = xbins.rebin
1082
1083 # Get the y-axis bin specifications.
1084 if ndim >= 2:
1085 ybins = _get_bins (args[3:], ndim, 1)
1086 rebin = rebin or ybins.rebin
1087
1088 profile = 0
1089 # Look for drawing options.
1090 options = ''
1091 for i in range (0, len(args)):
1092 if args[i][0] in string.ascii_letters:
1093 for j in range (i, len(args)):
1094 if ndim == 2 and args[j].lower() == "prof":
1095 profile = 1
1096 args[j] = ''
1097 options = ' '.join (args[i:])
1098 break
1099
1100 # Delete any old object of the same name.
1101 hold = ROOT.gROOT.FindObject (hname)
1102 if hold:
1103 ROOT.gROOT.Remove (hold)
1104
1105 # Create the histogram.
1106 if profile:
1107 hist = ROOT.TProfile (hname, htitle, xbins.nbins, xbins.lo, xbins.hi)
1108 if not ybins.rebin:
1109 hist.SetMinimum (ybins.lo)
1110 hist.SetMaximum (ybins.hi)
1111 elif ndim == 1:
1112 hist = ROOT.TH1F (hname, htitle, xbins.nbins, xbins.lo, xbins.hi)
1113 elif ndim == 2:
1114 hist = ScatterH2 (hname, htitle,
1115 xbins.nbins, xbins.lo, xbins.hi,
1116 ybins.nbins, ybins.lo, ybins.hi)
1117 if hasattr (hist, 'scatter'):
1118 hist.scatter (1)
1119
1120 # Automatic rebinning?
1121 if rebin:
1122 _setCanRebin (hist)
1123
1124 return (hist, options)
1125
1126
1127def draw (arg):
1128 """Process a draw command.
1129
1130 ARG is the command arguments (without the command word itself).
1131 See the header comments for the command syntax.
1132 """
1133
1134 global last_hist
1135
1136 # Initial parsing of the arguments.
1137 c = Draw_Cmd (arg)
1138 if c.errstr:
1139 print (c.errstr)
1140 return False
1141
1142 # Construct the expression to use to fill the histogram.
1143 if len (c.exprs) == 1:
1144 ndim = 1
1145 payload = "_hfill (%s)" % c.exprs[0]
1146 else:
1147 ndim = 2
1148 payload = "_hfill ((%s),(%s))" % (c.exprs[0], c.exprs[1])
1149
1150 # Construct the histogram title.
1151 htitle = "%s.%s" % (c.tuple, c.expr_orig)
1152 if c.sel_orig:
1153 htitle = htitle + '{%s}' % c.sel_orig
1154
1155 # Make the histogram.
1156 # If it's `!', then we just use the last one.
1157 if len(c.histspec) >= 1 and c.histspec[0] == "!" and last_hist is not None:
1158 hist = last_hist
1159 options = ' '.join (c.histspec[1:])
1160 elif len(c.histspec) >= 1 and c.histspec[0][:2] == '>>':
1161 hname = c.histspec[0][2:]
1162 hist = _globals.get (hname)
1163 options = ' '.join (c.histspec[1:])
1164 else:
1165 (hist, options) = _get_hist (ndim, c.histspec,
1166 _sanitize_hname(c.tuple+'.'+c.expr_orig), htitle)
1167
1168 # Remember it.
1169 last_hist = hist
1170
1171 # Generate the function.
1172 # It will be defined as _loopfunc in g.
1173 g = copy.copy (_globals)
1174 g['_hfill'] = hist.Fill
1175 ftext = c._make_func (payload, ', _hfill = _hfill')
1176 exec (ftext, g)
1177
1178 # Execute the loop over the data.
1179 c.tuple_o.loop (g['_loopfunc'], c.lo, c.hi)
1180
1181 # Adjust binning, if requested.
1182 if _hasCanRebin (hist):
1183 hist.LabelsDeflate ("X")
1184 if ndim > 1:
1185 hist.LabelsDeflate ("Y")
1186
1187 # Draw the histogram.
1188 draw_obj (hist, options)
1189 return True
1190
1191
1192def _scan_print (f, i, *args):
1193 """Helper to print out one row of a scan.
1194
1195 F is the file object to which to write, I is the row number,
1196 and ARGS is a tuple of the column values."""
1197
1198 s = '%6d' % i
1199 for a in args:
1200 if isinstance(a, int):
1201 s += ' %8d' % a
1202 elif isinstance(a, str):
1203 s += ' %8s' % a
1204 else:
1205 s += ' %8g' % a
1206 print (s, file=f)
1207 return
1208
1209
1210def scan (arg):
1211 """Process a scan command.
1212
1213 ARG is the command arguments (without the command word itself).
1214 See the header comments for the command syntax.
1215 """
1216
1217 # Initial parsing of the arguments.
1218 c = Draw_Cmd (arg)
1219 if c.errstr:
1220 print (c.errstr)
1221 return False
1222
1223 payload = "_print (_i, %s)" % \
1224 ','.join (['(%s)'%e for e in c.exprs])
1225
1226 # Output file handling
1227 fname = None
1228 append = False
1229 if len(c.histspec) > 0:
1230 if c.histspec[0].startswith ('>>'):
1231 append = True
1232 fname = c.histspec[0][2:]
1233 elif c.histspec[0].startswith ('>'):
1234 fname = c.histspec[0][1:]
1235 if fname == '' and len(c.histspec) >= 2:
1236 fname = c.histspec[1]
1237 if fname:
1238 fout = open (fname, 'a' if append else 'w')
1239 else:
1240 fout = sys.stdout
1241
1242 # Generate the function.
1243 # It will be defined as _loopfunc in g.
1244 g = copy.copy (_globals)
1245 g['_print'] = lambda i, *args: _scan_print (fout, i, *args)
1246 ftext = c._make_func (payload, ', _print = _print')
1247 exec (ftext, g)
1248
1249 # Execute the loop over the data.
1250 c.tuple_o.loop (g['_loopfunc'], c.lo, c.hi)
1251
1252 if fname:
1253 fout.close()
1254
1255 return True
1256
1257
1258def loop (arg):
1259 """Process a loop command.
1260
1261 ARG is the command arguments (without the command word itself).
1262 See the header comments for the command syntax.
1263 """
1264
1265 # Initial parsing of the arguments.
1266 c = Draw_Cmd (arg)
1267 if c.errstr:
1268 print (c.errstr)
1269 return False
1270
1271 payload = "(%s,)" % ','.join (c.exprs)
1272
1273 # Generate the function.
1274 # It will be defined as _loopfunc in g.
1275 g = copy.copy (_globals)
1276 ftext = c._make_func (payload)
1277 exec (ftext, g)
1278
1279 # Execute the loop over the data.
1280 c.tuple_o.loop (g['_loopfunc'], c.lo, c.hi)
1281
1282 return True
1283
1284
1285# Dictionary of command handlers.
1286# Should return True if cmd was handled.
1287_cmddict = {'d': draw,
1288 'draw' : draw,
1289 'scan' : scan,
1290 'loop' : loop,
1291 }
1292
1293
1294def cmd (s):
1295 """Process a command S.
1296
1297 Returns True if the command was handled, False otherwise.
1298 See the header comments for the command syntax.
1299 """
1300
1301 ssplit = s.split (None, 1)
1302
1303 if len(ssplit) < 2:
1304 return False
1305
1306 cmd, args = ssplit
1307
1308 func = _cmddict.get (cmd)
1309 if func:
1310 return func (args)
1311
1312 return False
1313
1314
1315
1316
1322
1323#
1324# Hook holding the original value of the exception hook.
1325# But be careful not to overwrite this if this file is reread.
1326#
1327if '_orig_ehook' not in globals():
1328 _orig_ehook = None
1329
1330
1331def _excepthook (exctype, value, traceb):
1332 """Exception hook used by pydraw to process drawing commands."""
1333
1334 # If it's a syntax error, try interpreting as a drawing command.
1335 if isinstance (value, SyntaxError):
1336 val = value.text
1337 if val[-1] == '\n':
1338 val = val[:-1] #pragma: NO COVER
1339 if cmd (val):
1340 # Success --- update root stuff and return.
1341 # (This will swallow the original syntax error.)
1342 ROOT.gInterpreter.EndOfLineAction()
1343 return
1344
1345 # No luck --- pass it on to the original exception handler.
1346 _orig_ehook (exctype, value, traceb)
1347
1348
1350 """Enable entering drawing commands directly at the python prompt."""
1351
1352 # Store the old value of the exception hook (only if we haven't
1353 # done so already).
1354 global _orig_ehook
1355 if _orig_ehook is None:
1356 _orig_ehook = sys.excepthook
1357
1358 # Install our handler.
1359 sys.excepthook = _excepthook
1360 return
1361
1362
1363
1364
1365# import ROOT
1366# import string
1367# import sys
1368# import exceptions
1369# import copy
1370# from PyAnalysisUtils.draw_obj import draw_obj, get_canvas
1371
1372# try:
1373# ScatterH2 = ROOT.RootUtils.ScatterH2
1374# except AttributeError:
1375# ScatterH2 = ROOT.TH2F
1376# print ("WARNING: RootUtils::ScatterH2 not available; using TH2F instead")
1377
1378# kCanRebin = ROOT.TH1.kCanRebin
1379
1380# _last_hist = None
1381# #_marker_style = 3
1382# #_marker_size = 0.5
1383
1384# _evtvar = 'e'
1385
1386
1387
1388
1389
1390
1391# from array import array
1392# class hist_filler:
1393# def __init__ (self, hist, nbuf=100):
1394# self.hist = hist
1395# self.filln = hist.FillN
1396# self.nbuf = nbuf
1397# self.xarr = array ('d', 100*[0.])
1398# self.warr = array ('d', 100*[1.])
1399# self.i = [0]
1400
1401# self.xlist = 100*[0.]
1402# return
1403# def get_filler (self):
1404# def fill (x,i=self.i,nbuf=self.nbuf,xarr=self.xarr,flush=self.flush):
1405# ii = i[0]
1406# if ii == nbuf:
1407# flush()
1408# ii = i[0]
1409# xarr[ii] = x
1410# i[0] = ii + 1
1411# return
1412# return fill
1413# def flush (self):
1414# #print (self.i, self.xarr)
1415# self.filln (self.i[0], self.xarr, self.warr)
1416# self.i[0] = 0
1417# return
1418
1419# ##############################################################################
1420# # Here are the command handlers.
loop(self, f, looplo=0, loophi=sys.maxsize)
Definition pydraw.py:548
_mung_n(self, s1, s2)
Definition pydraw.py:806
_mung_expr_dollar(self, s)
Definition pydraw.py:863
_mung_loop(self, s1, s2)
Definition pydraw.py:820
_mung_index(self, s1, s2)
Definition pydraw.py:776
_make_func(self, payload, extargs='')
Definition pydraw.py:934
_parserange(self, tuple)
Definition pydraw.py:728
_mung_expr_ids(self, s)
Definition pydraw.py:895
loop(self, f, looplo=0, loophi=sys.maxsize)
Definition pydraw.py:521
__init__(self, name)
Definition pydraw.py:575
STL class.
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:130
_get_bins(args, ndim, axis)
Definition pydraw.py:998
_hasCanRebin(h)
Definition pydraw.py:348
_get_hist(ndim, args, hname, htitle)
Definition pydraw.py:1070
_split_outer(haystack, needle)
Definition pydraw.py:484
_find_outer(haystack, needle, ignore_delim=False)
Definition pydraw.py:428
_excepthook(exctype, value, traceb)
Definition pydraw.py:1331
_scan_print(f, i, *args)
Definition pydraw.py:1192
_sanitize_hname(s)
Definition pydraw.py:374
_setCanRebin(h)
Definition pydraw.py:346
_untokenize(tokens)
Definition pydraw.py:387