~ubuntu-branches/ubuntu/natty/python-cogent/natty

« back to all changes in this revision

Viewing changes to cogent/util/progress_display.py

  • Committer: Bazaar Package Importer
  • Author(s): Steffen Moeller
  • Date: 2010-12-04 22:30:35 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20101204223035-j11kinhcrrdgg2p2
Tags: 1.5-1
* Bumped standard to 3.9.1, no changes required.
* New upstream version.
  - major additions to Cookbook
  - added AlleleFreqs attribute to ensembl Variation objects.
  - added getGeneByStableId method to genome objects.
  - added Introns attribute to Transcript objects and an Intron class.
  - added Mann-Whitney test and a Monte-Carlo version
  - exploratory and confirmatory period estimation techniques (suitable for
    symbolic and continuous data)
  - Information theoretic measures (AIC and BIC) added
  - drawing of trees with collapsed nodes
  - progress display indicator support for terminal and GUI apps
  - added parser for illumina HiSeq2000 and GAiix sequence files as 
    cogent.parse.illumina_sequence.MinimalIlluminaSequenceParser.
  - added parser to FASTQ files, one of the output options for illumina's
    workflow, also added cookbook demo.
  - added functionality for parsing of SFF files without the Roche tools in
    cogent.parse.binary_sff
  - thousand fold performance improvement to nmds
  - >10-fold performance improvements to some Table operations

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
"""The hook for new user interfaces to take control of progress bar and status
 
4
display is pass setupRootUiContext an instance of their UI context class, with
 
5
the same methods as the _Context class defined here.
 
6
 
 
7
Long-running functions can be decorated with @display_wrap, and will then be
 
8
given the extra argument 'ui'.  'ui' is a ProgressContext instance with methods
 
9
.series(), .imap(), .map() and .display(), any one of which will cause a 
 
10
progress-bar to be displayed.
 
11
 
 
12
@display_wrap
 
13
def long_running_function(..., ui)
 
14
    ui.display(msg, progress)  # progress is between 0.0 and 1.0
 
15
  or
 
16
    for item in ui.map(items, function)
 
17
  or
 
18
    for item in ui.imap(items, function)
 
19
  or
 
20
    for item in ui.series(items)
 
21
"""
 
22
 
 
23
from __future__ import with_statement, division
 
24
import sys, time, contextlib, functools, warnings
 
25
import os, atexit
 
26
import threading
 
27
from cogent.util import parallel, terminal
 
28
 
 
29
__author__ = "Peter Maxwell"
 
30
__copyright__ = "Copyright 2009, The Cogent Project"
 
31
__credits__ = ["Peter Maxwell"]
 
32
__license__ = "GPL"
 
33
__version__ = "1.5.0"
 
34
 
 
35
try:
 
36
    curses_terminal = terminal.CursesOutput()
 
37
except terminal.TerminalUnavailableError:
 
38
    curses_terminal = None
 
39
else:
 
40
    CODES = curses_terminal.getCodes()
 
41
    bar_template = CODES['GREEN'] + '%s' + CODES['NORMAL'] + '%s'
 
42
    BOL = CODES['BOL']
 
43
    CLEAR = CODES['UP'] + BOL + CODES['CLEAR_EOL']
 
44
    
 
45
 
 
46
class TextBuffer(object):
 
47
    """A file-like object which accumulates written text.  Specialised for 
 
48
    output to a curses terminal in that it uses CLEAR and re-writing to extend
 
49
    incomplete lines instead of just outputting or buffering them.  That
 
50
    allows the output to always end at a newline, ready for a progress bar
 
51
    to be shown, without postponing output of any incomplete last line."""
 
52
    
 
53
    def __init__(self):
 
54
        self.chunks = []
 
55
        self.pending_eol = False
 
56
        
 
57
    def write(self, text):
 
58
        self.chunks.append(text)
 
59
        
 
60
    def regurgitate(self, out):
 
61
        if self.chunks:
 
62
            text = ''.join(self.chunks)
 
63
            if self.pending_eol:
 
64
                out.write(CLEAR)
 
65
            #out.write(CODES['YELLOW'])
 
66
            out.write(text)
 
67
            if text.endswith('\n'):
 
68
                self.pending_eol = False
 
69
                self.chunks = []
 
70
            else:
 
71
                self.pending_eol = True
 
72
                self.chunks = [text.split('\n')[-1]]
 
73
                out.write('\n')
 
74
            #out.write(CODES['NORMAL'])
 
75
 
 
76
 
 
77
class ProgressContext(object):
 
78
    """The interface by which cogent algorithms can report progress to the
 
79
    user interface.  Calls self.progress_bar.set(progress, message)"""
 
80
    
 
81
    def __init__(self, progress_bar=None, prefix=None, base=0.0, segment=1.0, 
 
82
            parent=None, rate=1.0):
 
83
        self.progress_bar = progress_bar
 
84
        self.desc = ''
 
85
        self.base = base
 
86
        self.segment = segment
 
87
        self.progress = 0
 
88
        self.current = 1
 
89
        if parent is None:
 
90
            self.depth = 0
 
91
            self.parent = None
 
92
            self.t_last = 0
 
93
        else:
 
94
            assert progress_bar is parent.progress_bar
 
95
            self.depth = parent.depth + 1
 
96
            self.parent = parent
 
97
            self.t_last = parent.t_last
 
98
        self.msg = ''
 
99
        self.prefix = prefix or []
 
100
        self.message = self.prefix + [self.msg]
 
101
        self._max_text_len = 0
 
102
        self.max_depth = 2
 
103
        self.rate = rate
 
104
        
 
105
    def subcontext(self):
 
106
        """For any sub-task which may want to report its own progress, but should
 
107
        not get its own progress bar."""
 
108
        if self.depth == self.max_depth:
 
109
            return NullContext()
 
110
        return ProgressContext(
 
111
            progress_bar = self.progress_bar, 
 
112
            prefix = self.message,
 
113
            base = self.base+self.progress*self.segment,
 
114
            segment = self.current*self.segment,
 
115
            parent = self,
 
116
            rate = self.rate) 
 
117
    
 
118
    def display(self, msg=None, progress=None, current=0.0):
 
119
        """Inform the UI that we are are at 'progress' of the way through and 
 
120
        will be doing 'msg' until we reach and report at progress+current.
 
121
        """
 
122
        if self.depth > 0:
 
123
            msg = None
 
124
            
 
125
        updated = False
 
126
        if progress is not None:
 
127
            self.progress = min(progress, 1.0)
 
128
            updated = True
 
129
 
 
130
        if current is not None:
 
131
            self.current = current
 
132
            updated = True
 
133
 
 
134
        if msg is not None and msg != self.msg:
 
135
            self.msg = self.message[-1] = msg
 
136
            updated = True
 
137
 
 
138
        if updated and (
 
139
                (self.depth==0 and self.progress in [0.0, 1.0]) or 
 
140
                time.time() > self.t_last + self.rate):
 
141
            self.render()
 
142
 
 
143
    def render(self):
 
144
        self.progress_bar.set(self.base+self.progress*self.segment, self.message[0])
 
145
        self.t_last = time.time()
 
146
 
 
147
    def done(self):
 
148
        if self.depth == 0:
 
149
            self.progress_bar.done()
 
150
    
 
151
    # Not much point while cogent is still full of print statements, but
 
152
    # .info() (and maybe other logging analogues such as .warning()) would
 
153
    # avoid the need to capture stdout:
 
154
    
 
155
    #def info(self, text):
 
156
    #    """Display some information which may be more than fleetingly useful, 
 
157
    #    such as a summary of intermediate statistics or a very mild warning.  
 
158
    #    A GUI should make this information retrievable but not intrusive.
 
159
    #    For terminal UIs this is equivalent to printing"""
 
160
    #    raise NotImplementedError
 
161
        
 
162
    def series(self, items, noun='', labels=None, start=None, end=1.0):
 
163
        """Wrap a looped-over list with a progress bar"""
 
164
        if not hasattr(items, '__len__'):
 
165
            items = list(items)
 
166
        if start is None:
 
167
            start = 0.0
 
168
        step = (end-start) / len(items)
 
169
        if labels:
 
170
            assert len(labels) == len(items)
 
171
        elif len(items) == 1:
 
172
            labels = ['']
 
173
        else:
 
174
            if noun:
 
175
                noun += ' '
 
176
            goal = len(items)
 
177
            template = '%s%%%sd/%s' % (noun, len(str(goal)), goal)
 
178
            labels = [template % i for i in range(0, len(items))]
 
179
        for (i, item) in enumerate(items):
 
180
            self.display(msg=labels[i], progress=start+step*i, current=step)
 
181
            yield item
 
182
        self.display(progress=end, current=0)
 
183
        
 
184
    def imap(self, f, s, labels=None, **kw):
 
185
        """Like itertools.imap() but with a progress bar"""
 
186
        with parallel.mpi_split(len(s)) as comm:
 
187
            (size, rank) = (comm.Get_size(), comm.Get_rank())
 
188
            ordinals = range(0, len(s), size)
 
189
            labels = labels and labels[0::size]
 
190
            for start in self.series(ordinals, labels=labels, **kw):
 
191
                chunk = s[start:start+size]
 
192
                if rank < len(chunk):
 
193
                    local_result = f(chunk[rank])
 
194
                else:
 
195
                    local_result = None
 
196
                for result in comm.allgather(local_result)[:len(chunk)]:
 
197
                    yield result
 
198
    
 
199
    def eager_map(self, f, s, **kw):
 
200
        """Like regular Python2 map() but with a progress bar"""
 
201
        return list(self.imap(f,s, **kw))
 
202
    
 
203
    def map(self, f, s, **kw):
 
204
        """Synonym for eager_map, unlike in Python3"""
 
205
        return self.eager_map(f, s, **kw)
 
206
 
 
207
 
 
208
class NullContext(ProgressContext):
 
209
    """A UI context which discards all output.  Useful on secondary MPI cpus, 
 
210
    and other situations where all output is suppressed"""
 
211
    def subcontext(self, *args, **kw):
 
212
        return self
 
213
        
 
214
    def display(self, *args, **kw):
 
215
        pass
 
216
 
 
217
    def done(self):
 
218
        pass
 
219
 
 
220
 
 
221
class LogFileOutput(object):
 
222
    """A fake progress bar for when progress bars are impossible"""
 
223
    def __init__(self):
 
224
        self.t0 = time.time()
 
225
        self.lpad = ''
 
226
        self.output = sys.stdout # sys.stderr
 
227
    
 
228
    def done(self):
 
229
        pass
 
230
    
 
231
    def set(self, progress, message):        
 
232
        if message:
 
233
            delta = '+%s' % int(time.time() - self.t0)
 
234
            progress = int(100*progress+0.5)
 
235
            print >>self.output, "%s %5s %3i%% %s" % (
 
236
                    self.lpad, delta, progress, message)
 
237
            
 
238
 
 
239
class CursesTerminalProgressBar(object):
 
240
    """Wraps stdout and stderr, displaying a progress bar via simple 
 
241
    ascii/curses art and scheduling other output around its redraws."""
 
242
    def __init__(self):
 
243
        global curses_terminal
 
244
        assert curses_terminal is not None
 
245
        self.curses_terminal = curses_terminal
 
246
        self.stdout = sys.stdout
 
247
        self.stderr = sys.stderr
 
248
        self.stdout_log = TextBuffer()
 
249
        self.stderr_log = TextBuffer()
 
250
        self.lines = []
 
251
        self.chunks = []
 
252
        self.pending_eol = False
 
253
        self.line_count = 0
 
254
        (sys.stdout, sys.stderr, self._stdout, self._stderr) = (
 
255
                self.stdout_log, self.stderr_log, sys.stdout, sys.stderr)
 
256
    
 
257
    def done(self):
 
258
        self.set(None, None)
 
259
        (sys.stdout, sys.stderr) = (self._stdout, self._stderr)
 
260
    
 
261
    def set(self, progress, message):
 
262
        """Clear the existing progress bar, write out any accumulated
 
263
        stdout and stderr, then draw the updated progress bar."""
 
264
        cols = self.curses_terminal.getColumns()
 
265
        width = cols - 1
 
266
        if progress is not None:
 
267
            assert 0.0 <= progress <= 1.0, progress
 
268
            BLOCK = '█'
 
269
            dots = int(progress * width)
 
270
            bar = bar_template % (BLOCK * dots, BLOCK * (width-dots))
 
271
 
 
272
        if self.line_count:
 
273
            self.stderr.write(CLEAR * (self.line_count))
 
274
        else:
 
275
            self.stderr.write(BOL)
 
276
        self.stdout_log.regurgitate(self.stdout)
 
277
        self.stderr_log.regurgitate(self.stderr)
 
278
        
 
279
        if progress is not None:
 
280
            self.stderr.writelines([bar, '\n'])
 
281
        if message is not None:
 
282
            self.stderr.writelines([message[:width], '\n'])
 
283
        self.line_count = (progress is not None) + (message is not None)
 
284
 
 
285
 
 
286
NULL_CONTEXT = NullContext()
 
287
CURRENT = threading.local()
 
288
CURRENT.context = None
 
289
 
 
290
class RootProgressContext(object):
 
291
    """The context between long running jobs, when there is no progress bar"""
 
292
    
 
293
    def __init__(self, pbar_constructor, rate):
 
294
        self.pbar_constructor = pbar_constructor
 
295
        self.rate = rate
 
296
        
 
297
    def subcontext(self):
 
298
        pbar = self.pbar_constructor()
 
299
        return ProgressContext(pbar, rate=self.rate)
 
300
 
 
301
 
 
302
def setupRootUiContext(progressBarConstructor=None, rate=None):
 
303
    """Select a UI Context type depending on system environment"""
 
304
    if parallel.getCommunicator().Get_rank() != 0:
 
305
        klass = None
 
306
    elif progressBarConstructor is not None:
 
307
        klass = progressBarConstructor
 
308
    elif curses_terminal and sys.stdout.isatty():
 
309
        klass = CursesTerminalProgressBar
 
310
    elif isinstance(sys.stdout, file):
 
311
        klass = LogFileOutput
 
312
        if rate is None:
 
313
            rate = 5.0
 
314
    else:
 
315
        klass = None
 
316
    
 
317
    if klass is None:
 
318
        CURRENT.context = NULL_CONTEXT
 
319
    else:
 
320
        if rate is None:
 
321
            rate = 0.1
 
322
        CURRENT.context = RootProgressContext(klass, rate)
 
323
 
 
324
 
 
325
def display_wrap(slow_function):
 
326
    """Decorator which give the function its own UI context.
 
327
    The function will receive an extra argument, 'ui', 
 
328
    which is used to report progress etc."""
 
329
    @functools.wraps(slow_function)
 
330
    def f(*args, **kw):
 
331
        if getattr(CURRENT, 'context', None) is None:
 
332
            setupRootUiContext()
 
333
        parent = CURRENT.context
 
334
        show_progress = kw.pop('show_progress', None)
 
335
        if show_progress is False:
 
336
            # PendingDeprecationWarning?
 
337
            subcontext = NULL_CONTEXT
 
338
        else:
 
339
            subcontext = parent.subcontext()
 
340
        kw['ui'] = CURRENT.context = subcontext
 
341
        try:
 
342
            result = slow_function(*args, **kw)
 
343
        finally:
 
344
            CURRENT.context = parent
 
345
            subcontext.done()
 
346
        return result
 
347
    return f
 
348
 
 
349
@display_wrap
 
350
def subdemo(ui):
 
351
    for j in ui.series(range(10)):
 
352
        time.sleep(0.1)
 
353
    return
 
354
    
 
355
@display_wrap
 
356
def demo(ui):
 
357
    print "non-linebuffered output, tricky but look:",
 
358
    for i in ui.series(range(10)):
 
359
        time.sleep(.6)
 
360
        if i == 5:
 
361
            print '\nhalfway through, a new line: ',
 
362
        if i % 2:
 
363
            subdemo()
 
364
        print i, ".", 
 
365
    print "done"
 
366
 
 
367
if __name__ == '__main__':
 
368
    #setupRootUiContext(rate=0.2)
 
369
    demo()
 
370
    
 
371
# This messes up interactive shells a bit:
 
372
#CURRENT.start()
 
373
#atexit.register(CURRENT.done)
 
374
 
 
375
 
 
376