class HDFwrite(Block): ''' This block writes the incoming signals to a file in HDF_ format. The HDF format is organized as follows: :: / (root) /procgraph (group with name procgraph) /procgraph/signal1 (table) /procgraph/signal2 (table) ... Each table has the following fields: time (float64 timestamp) value (the datatype of the signal) If a signal changes datatype, then an exception is raised. ''' Block.alias('hdfwrite') Block.input_is_variable('Signals to be written', min=1) Block.config('file', 'HDF file to write') Block.config('compress', 'Whether to compress the hdf table.', 1) Block.config('complib', 'Compression library (zlib, bzip2, blosc, lzo).', default='zlib') Block.config('complevel', 'Compression level (0-9)', 9) def init(self): self.writer = PGHDFLogWriter(self.config.file, compress=self.config.compress, complevel=self.config.complevel, complib=self.config.complib) def update(self): signals = self.get_input_signals_names() for signal in signals: if self.input_update_available(signal): self.log_signal(signal) def log_signal(self, signal): timestamp = self.get_input_timestamp(signal) value = self.get_input(signal) # only do something if we have something if value is None: return assert timestamp is not None if not isinstance(value, numpy.ndarray): # TODO: try converting try: value = numpy.array(value) except: msg = 'I can only log numpy arrays, not %r' % value.__class__ raise BadInput(msg, self, signal) self.writer.log_signal(timestamp, signal, value) def finish(self): self.writer.finish()
class Blend(Block): ''' Blend two or more images. RGB images are interpreted as having full alpha (opaque). All images must have the same width and height. ''' Block.alias('blend') Block.input_is_variable('images to blend', min=2) Block.output('rgb', 'The output is a RGB image (no alpha)') def update(self): # TODO: check images result = None for signal in self.get_input_signals_names(): image = self.get_input(signal) if result is None: result = image continue result = blend(result, image) self.output.rgb = to_rgb(result)
class FPSDataLimit(Block): ''' This block limits the output update to a certain framerate. ''' Block.alias('fps_data_limit') Block.config('fps', 'Maximum framerate.') Block.input_is_variable('Signals to decimate.', min=1) Block.output_is_variable('Decimated signals.') def init(self): self.state.last_timestamp = None def update(self): should_update = False last = self.state.last_timestamp current = max(self.get_input_signals_timestamps()) if last is None: should_update = True self.state.last_timestamp = current else: fps = self.config.fps delta = 1.0 / fps difference = current - last if difference > delta: should_update = True self.state.last_timestamp = current if not should_update: return # Just copy the input to the output for i in range(self.num_input_signals()): self.set_output(i, self.get_input(i), self.get_input_timestamp(i))
class ASync(Block): ''' The first signal is the "master". Waits that all signals are perceived once. Then it creates one event every time the master arrives. ''' Block.alias('async') Block.input_is_variable( 'Signals to (a)synchronize. The first is the master.', min=2) Block.output_is_variable('Synchronized signals.') def init(self): self.state.last_sent_timestamp = None def update(self): self.state.last_timestamp = None # No signal until everybody is ready if not self.all_input_signals_ready(): return # No signal unless the master is ready master_timestamp = self.get_input_timestamp(0) if self.state.last_timestamp == master_timestamp: return self.state.last_timestamp = master_timestamp # Write all signals using master's timestamp for s in self.get_input_signals_names(): self.set_output(s, self.get_input(s), master_timestamp)
class Wait(Block): ''' This block waits a given number of updates before transmitting the output signal. ''' Block.alias('wait') Block.config('n', 'Number of updates to wait at the beginning.') Block.input_is_variable('Arbitrary signals.') Block.output_is_variable('Copy of the signals, minus the first ' '*n* updates.') def init(self): self.state.count = 0 def update(self): count = self.state.count # make something happen after we have waited enough if count >= self.config.n: # Just copy the input to the output for i in range(self.num_input_signals()): self.set_output(i, self.get_input(i), self.get_input_timestamp(i)) self.state.count = count + 1
class Identity(Block): ''' This block outputs the inputs, unchanged. ''' Block.alias('identity') Block.input_is_variable('Input signals.', min=1) Block.output_is_variable('Output signals, equal to input.') def update(self): # Just copy the input to the output for i in range(self.num_input_signals()): self.set_output(i, self.get_input(i), self.get_input_timestamp(i))
class BagWrite(Block): ''' This block writes the incoming signals to a ROS bag file. By default, the signals are organized as follows: :: / (root) /procgraph (group with name procgraph) /procgraph/signal1 (table) /procgraph/signal2 (table) ... Note that the input should be ROS messages; no conversion is done. ''' Block.alias('bagwrite') Block.input_is_variable('Signals to be written', min=1) Block.config('file', 'Bag file to write') def init(self): from ros import rosbag # @UnresolvedImport self.info('Writing to bag %r.' % self.config.file) make_sure_dir_exists(self.config.file) if os.path.exists(self.config.file): os.unlink(self.config.file) self.tmp_file = self.config.file + '.active' self.bag = rosbag.Bag(self.tmp_file, 'w', compression='bz2') self.signal2last_timestamp = {} def finish(self): self.bag.close() os.rename(self.tmp_file, self.config.file) def update(self): import rospy signals = self.get_input_signals_names() for signal in signals: msg = self.get_input(signal) timestamp = self.get_input_timestamp(signal) if ((signal in self.signal2last_timestamp) and (timestamp == self.signal2last_timestamp[signal])): continue self.signal2last_timestamp[signal] = timestamp ros_timestamp = rospy.Time.from_sec(timestamp) self.bag.write('/%s' % signal, msg, ros_timestamp)
class Print(Block): ''' Print a representation of the input values along with their timestamp. ''' Block.alias('print') Block.input_is_variable('Signals to print.', min=1) def update(self): for i in range(self.num_input_signals()): print('P %s %s %s' % (self.canonicalize_input(i), self.get_input_timestamp(i), self.get_input(i)))
class MakeTuple(Block): ''' Creates a tuple out of the input signals values. Often used for plotting two signals as (x,y); see :ref:`block:plot`. ''' Block.alias('make_tuple') Block.input_is_variable('Signals to unite in a tuple.') Block.output('tuple', 'Tuple containing signals.') def update(self): values = self.get_input_signals_values() self.output.tuple = tuple(values)
class Compose(Block): ''' Compose several images in the same canvas. You should probably use :ref:`block:grid` in many situations. Example configuration: :: compose.positions = {y: [0,0], ys: [320,20]} ''' Block.alias('compose') Block.config('width', 'Dimension in pixels.') Block.config('height', 'Dimension in pixels.') Block.config( 'positions', 'A structure giving the position of each signal in the canvas.') Block.input_is_variable('Images to compose.') Block.output('canvas', 'RGB image') def update(self): width = self.get_config('width') height = self.get_config('height') canvas = np.zeros((height, width, 3), dtype='uint8') positions = self.get_config('positions') if not isinstance(positions, dict): raise Exception('I expected a dict, not "%s"' % positions) for signal, position in positions.items(): if not self.is_valid_input_name(signal): raise Exception('Unknown input "%s" in %s.' % (signal, self)) rgb = self.get_input(signal) # TODO check if rgb is not None: assert_rgb_image(rgb, 'input %s to compose block' % signal) place_at(canvas, rgb, position[0], position[1]) #print "Writing image %s" % signal else: print "Ignoring image %s because not ready.\n" % signal self.set_output(0, canvas)
class AllReady(Block): ''' This block outputs the inputs, unchanged. ''' Block.alias('all_ready') Block.input_is_variable('Input signals.', min=1) Block.output_is_variable('Output signals, equal to input.') def update(self): if not self.all_input_signals_ready(): return for i in range(self.num_input_signals()): if self.input_update_available(i): t, value = self.get_input_ts_and_value(i) self.set_output(i, value, t)
class Info(Block): ''' Prints more compact information about the inputs than :ref:`block:print`. For numpy arrays it prints their shape and dtype instead of their values. ''' Block.alias('info') Block.input_is_variable('Signals to describe.', min=1) def init(self): self.first = {} self.counter = {} def update(self): # Just copy the input to the output for i in range(self.num_input_signals()): name = self.canonicalize_input(i) val = self.get_input(i) ts = self.get_input_timestamp(i) if ts is None: continue if not i in self.first: self.first[i] = ts self.counter[i] = 0 friendly = ts - self.first[i] # if isinstance(val, numpy.ndarray): # s = "%s %s" % (str(val.shape), str(val.dtype)) # else: s = str(val) if len(s) > 40: s = s[:40] s = s.replace('\n', '|') date = datetime.fromtimestamp(ts).isoformat(' ')[:-4] ts = "%.2f" % ts self.debug('%s (%8.2fs) %12s %5d %s' % (date, friendly, name, self.counter[i], s)) self.counter[i] += 1
class Any(Block): ''' Joins the stream of multiple signals onto one output signal. ''' Block.alias('any') Block.input_is_variable('Signals to be put on the same stream.', min=1) Block.output('stream', 'Unified stream.') def init(self): self.last_ts = {} self.buffer = [] def update(self): # DO NOT USE NAMES # TODO: check other blocks for this bug nsignals = self.num_input_signals() for i in range(nsignals): value = self.get_input(i) ts = self.get_input_timestamp(i) if value is None: continue if not i in self.last_ts or ts != self.last_ts[i]: self.buffer.append((ts, value)) self.last_ts[i] = ts # t = [x[0] for x in self.buffer] # self.debug(' after1: %s' % t) # self.buffer = sorted(self.buffer, key=lambda x: x[0]) # t = [x[0] for x in self.buffer] # self.debug(' sort: %s' % t) # Make sure we saw every signal before outputing one # if len(self.last_ts) == nsignals: # FIXME: bug we will send one sample of the last stream if self.buffer: ts, value = self.buffer.pop(0) self.set_output(0, value, ts)
class FPSPrint(Block): ''' Prints the fps count for the input signals. ''' Block.alias('fps_print') Block.input_is_variable('Any signal.', min=1) def init(self): self.state.last_timestamp = None def update(self): current = max(self.get_input_signals_timestamps()) last = self.state.last_timestamp if last is not None: difference = current - last fps = 1.0 / difference self.info("FPS %s %.1f" % (self.canonicalize_input(0), fps)) self.state.last_timestamp = current
class ImageGrid(Block): ''' A block that creates a larger image by arranging them in a grid. The output is rgb, uint8. Inputs are passed through the "torgb" function. ''' Block.alias('grid') Block.config('cols', 'Columns in the grid.', default=None) Block.config('bgcolor', 'Background color.', default=[0, 0, 0]) Block.config('pad', 'Padding for each cell', default=0) Block.input_is_variable('Images to arrange in a grid.', min=1) Block.output('grid', 'Images arranged in a grid.') def update(self): if not self.all_input_signals_ready(): return n = self.num_input_signals() for i in range(n): input_check_convertible_to_rgb(self, i) cols = self.config.cols if cols is not None and not isinstance(cols, int): raise BadConfig('Expected an integer.', self, 'cols') images = [self.get_input(i) for i in range(n)] images = map(torgb, images) canvas = make_images_grid(images, cols=self.config.cols, pad=self.config.pad, bgcolor=self.config.bgcolor) self.set_output(0, canvas)
class AsJSON(Block): ''' Converts the input into a JSON string. TODO: add example ''' Block.alias('as_json') Block.input_is_variable('Inputs to transcribe as JSON.') Block.output('json', 'JSON string.') def update(self): data = {} data['timestamp'] = max(self.get_input_signals_timestamps()) for i in range(self.num_input_signals()): name = self.canonicalize_input(i) value = self.input[name] data[name] = value self.output.json = json.dumps(data)
class FPSLimit(Block): ''' This block limits the output update to a certain *realtime* framerate. Note that this uses realtime wall clock time -- not the data time! This is mean for real-time applications, such as visualization.''' Block.alias('fps_limit') Block.config('fps', 'Realtime fps limit.') Block.input_is_variable('Arbitrary signals.') Block.output_is_variable('Arbitrary signals with limited framerate.') def init(self): self.state.last_timestamp = None def update(self): should_update = False last = self.state.last_timestamp current = time.time() if last is None: should_update = True self.state.last_timestamp = current else: fps = self.config.fps delta = 1.0 / fps difference = current - last #print "difference: %s ~ %s" % (difference, delta) if difference > delta: should_update = True self.state.last_timestamp = current if not should_update: return # Just copy the input to the output for i in range(self.num_input_signals()): self.set_output(i, self.get_input(i), self.get_input_timestamp(i))
class Join(Block): ''' This block joins multiple signals into one. ''' Block.alias('join') Block.input_is_variable('Signals to be joined together.') Block.output('joined', 'Joined signals.') def init(self): sizes = {} names = self.get_input_signals_names() for signal in names: sizes[signal] = None self.state.sizes = sizes def update(self): sizes = self.state.sizes result = [] for name in self.get_input_signals_names(): value = self.get_input(name) # workaround for scalar values value = np.reshape(value, np.size(value)) size = len(value) if value is None: return if sizes[name] is None: sizes[name] = size else: if size != sizes[name]: raise Exception('Signal %s changed size from %s to %s.' % (name, sizes[name], size)) result.extend(value) self.output[0] = np.array(result)
class PickleGroup(Block): ''' Dumps the input as a :py:mod:`pickle` file, in the form of a dictionary signal name -> value. ''' Block.alias('pickle_group') Block.config('file', 'File to write to.') Block.input_is_variable('Any number of pickable signals.') def write(self, x, filename): make_sure_dir_exists(filename) with open(filename, 'wb') as f: pickle.dump(x, f, protocol=pickle.HIGHEST_PROTOCOL) def get_dict(self): data = {} for signal in self.get_input_signals_names(): data[signal] = self.get_input(signal) return data def update(self): self.write(self.get_dict(), self.config.file + '.part') def finish(self): self.write(self.get_dict(), self.config.file)
class MyBlockC(Block): Block.input_is_variable()
class Sync(Generator): ''' This block synchronizes a set of streams to the first stream (the master). The first signal is called the "master" signal. The other (N-1) are slaves. We guarantee that: - if the slaves are faster than the master, then we output exactly the same. Example diagrams: :: Master * * * * * Slave ++++++++++++++++ Master * * * * * output? v v x v Slave + + + output? v v v ''' Block.alias('sync') Block.input_is_variable('Signals to synchronize. The first is the master.', min=2) Block.output_is_variable('Synchronized signals.') def init(self): # output signals get the same name as the inputs names = self.get_input_signals_names() # create a state for each signal: it is an array # of tuples (timestamp, tuple) queues = {} for signal in names: queues[signal] = [] # note in all queues, # [(t0,...), (t1,...), ... , (tn,...) ] # and we have t0 > tn # The chronologically first (oldest) is queue[-1] # You get one out using queue.pop() # You insert one with queue.insert(0,...) self.set_state('queues', queues) self.state.already_seen = {} self.set_state('master', names[0]) self.set_state('slaves', names[1:]) # The output is an array of tuple (timestamp, values) # [ # (timestamp1, [value1,value2,value3,...]), # (timestamp1, [value1,value2,value3,...]) # ]# self.set_state('output', []) def update(self): def debug(s): if False: # XXX: use facilities print('sync %s %s' % (self.name, s)) output = self.get_state('output') queues = self.get_state('queues') names = self.get_input_signals_names() # for each input signal, put its value in the queues # if it is not already present for i, name in enumerate(names): # Commenting this; we clarified 0 = eternity; None = no signal yet if not self.input_signal_ready(i): debug('Ignoring signal %r because timestamp is None.' % name) continue current_timestamp = self.get_input_timestamp(i) current_value = self.get_input(i) if (name in self.state.already_seen and self.state.already_seen[name] == current_timestamp): continue else: self.state.already_seen[name] = current_timestamp queue = queues[name] # if there is nothing in the queue # or this is a new sample if ((len(queue) == 0) or newest(queue).timestamp != current_timestamp): # new sample debug("Inserting signal '%s' ts %s (queue len: %d)" % (name, current_timestamp, len(queue))) # debug('Before the queue is: %s' % queue) add_last( queue, Sample(timestamp=current_timestamp, value=current_value)) # debug('Now the queue is: %s' % queue) master = self.get_state('master') master_queue = queues[master] slaves = self.get_state('slaves') # if there is more than one value in each slave if len(master_queue) > 1: val = master_queue.pop() debug('DROPPING master (%s) ts =%s' % (master, val.timestamp)) # Now check whether all slaves signals are >= the master # If so, output a synchronized sample. if master_queue: all_ready = True master_timestamp = master_queue[-1].timestamp # print "Master timestamp: %s" % (master_timestamp) for slave in slaves: slave_queue = queues[slave] # remove oldest while (len(slave_queue) > 1 and oldest(slave_queue).timestamp < master_timestamp and oldest(slave_queue).timestamp != ETERNITY): debug("DROP one from %s" % slave) slave_queue.pop() if not slave_queue: # or (master_timestamp > slave_queue[-1].timestamp): all_ready = False # its_ts = map(lambda x:x.timestamp, slave_queue) # print "Slave %s not ready: %s" %(slave, its_ts) break if all_ready: debug("**** Ready: master timestamp %s " % (master_timestamp)) master_value = master_queue.pop().value output_values = [master_value] for slave in slaves: # get the freshest still after the master slave_queue = queues[slave] slave_timestamp, slave_value = slave_queue.pop() # print('Slave, %s %s ' % (slave, slave_timestamp)) if slave_timestamp == ETERNITY: slave_queue.append((slave_timestamp, slave_value)) # # not true anymore, assert slave_timestamp >= # master_timestamp # difference = slave_timestamp - master_timestamp # debug(" - %s timestamp %s diff %s" % # (slave, slave_timestamp, difference)) output_values.append(slave_value) output.insert(0, (master_timestamp, output_values)) # XXX XXX not really sure here # if master has more than one sample, then drop the first one # if we have something to output, do it if output: timestamp, values = output.pop() # FIXME: had to remove this for Vehicles simulation # assert timestamp > 0 debug("---------------- @ %s" % timestamp) for i in range(self.num_output_signals()): self.set_output(i, values[i], timestamp) def next_data_status(self): output = self.get_state('output') if not output: # no output ready return (False, None) else: timestamp = self.output[-1][0] return (True, timestamp)
class Plot(Block): ''' Plots the inputs using matplotlib. This block accepts an arbitrary number of signals. Each signals is treated independently and plot separately. Each signal can either be: 1. A tuple of length 2. It is interpreted as a tuple ``(x,y)``, and we plot ``x`` versus ``y`` (see also :ref:`block:make_tuple`). 2. A list of numbers, or a 1-dimensional numpy array of length N. In this case, it is interpreted as the y values, and we set ``x = 1:N``. ''' Block.alias('plot') Block.config('width', 'Image dimension', default=320) Block.config('height', 'Image dimension', default=240) Block.config('xlabel', 'X label for the plot.', default=None) Block.config('ylabel', 'Y label for the plot.', default=None) Block.config('legend', 'List of strings to use as legend handles.', default=None) Block.config('title', 'If None, use the signal name. Set to ``""`` to disable.', default=None) Block.config('format', 'Line format ("-",".","x-",etc.)', default='-') Block.config('symmetric', 'An alternative to y_min, y_max.' ' Makes sure the plot is symmetric for y. ', default=False) Block.config('x_min', 'If set, force the X axis to have this minimum.', default=None) Block.config('x_max', 'If set, force the X axis to have this maximum.', default=None) Block.config('y_min', 'If set, force the Y axis to have this minimum.', default=None) Block.config('y_max', 'If set, force the Y axis to have this maximum.', default=None) Block.config('keep', 'If True, tries to reuse the figure, without closing.' ' (buggy on some backends)', default=False) Block.config('transparent', 'If true, outputs a RGBA image instead of RGB.', default=False) Block.config( 'tight', 'Uses "tight" option for creating png (Matplotlib>=1.1).', default=False, ) Block.config('fancy_styles', 'A list of fancy styles to apply (%s).' % fancy_styles.keys(), default=[]) Block.input_is_variable('Data to plot.') Block.output('rgb', 'Resulting image.') def init(self): self.line = None # figure gets initialized in update() on the first execution self.figure = None self.warned = False def init_figure(self): width = self.config.width height = self.config.height # TODO: remove from here pylab.rc('xtick', labelsize=8) pylab.rc('ytick', labelsize=8) ''' Creates figure object and axes ''' self.figure = pylab.figure(frameon=False, figsize=(width / 100.0, height / 100.0)) # TODO: is there a better way? # left, bottom, right, top # borders = [0.15, 0.15, 0.03, 0.05] # w = 1 - borders[0] - borders[2] # h = 1 - borders[1] - borders[3] # self.axes = pylab.axes([borders[0], borders[1], w, h]) self.axes = pylab.axes() self.figure.add_axes(self.axes) pylab.draw_if_interactive = lambda: None pylab.figure(self.figure.number) if self.config.title is not None: if self.config.title != "": self.axes.set_title(self.config.title, fontsize=10) else: # We don't have a title --- t = ", ".join(self.get_input_signals_names()) self.axes.set_title(t, fontsize=10) if self.config.xlabel: self.axes.set_xlabel(self.config.xlabel) if self.config.ylabel: self.axes.set_ylabel(self.config.ylabel) self.legend_handle = None self.lines = {} self.lengths = {} def apply_styles(self, pylab): # TODO: check type for style in self.config.fancy_styles: if not style in fancy_styles: error = ('Cannot find style %r in %s' % (style, fancy_styles.keys())) raise BadConfig(error, self, 'style') function = fancy_styles[style] function(pylab) def plot_one(self, id, x, y, format): # @ReservedAssignment assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert len(x.shape) <= 1 assert len(y.shape) <= 1 assert len(x) == len(y) if id in self.lengths: if self.lengths[id] != len(x): redraw = True self.axes.lines.remove(self.lines[id]) else: redraw = False else: redraw = True if redraw: res = self.axes.plot(x, y, format) line = res[0] self.lines[id] = line else: self.lines[id].set_ydata(y) self.lines[id].set_xdata(x) self.lengths[id] = len(x) if self.limits is None: self.limits = np.array([min(x), max(x), min(y), max(y)]) else: self.limits[0] = min(self.limits[0], min(x)) self.limits[1] = max(self.limits[1], max(x)) self.limits[2] = min(self.limits[2], min(y)) self.limits[3] = max(self.limits[3], max(y)) # self.limits = map(float, self.limits) def update(self): self.limits = None start = time.clock() if self.figure is None: self.init_figure() pylab.figure(self.figure.number) self.apply_styles(pylab) for i in range(self.num_input_signals()): value = self.input[i] if value is None: raise BadInput('Input is None (did you forget a |sync|?)', self, i) elif isinstance(value, tuple): if len(value) != 2: raise BadInput( 'Expected tuple of length 2 instead of %d.' % len(value), self, i) xo = value[0] yo = value[1] if xo is None or yo is None: raise BadInput('Invalid members of tuple', self, i) x = np.array(xo) y = np.array(yo) # X must be one-dimensional if len(x.shape) > 1: raise BadInput('Bad x vector w/shape %s.' % str(x.shape), self, i) # y should be of dimensions ...? if len(y.shape) > 2: raise BadInput('Bad shape for y vector %s.' % str(y.shape), self, i) if len(x) != y.shape[0]: raise BadInput('Incompatible dimensions x: %s, y: %s' % (str(x.shape), str(y.shape))) # TODO: check x unidimensional (T) # TODO: check y compatible dimensions (T x N) else: y = np.array(value) if len(y.shape) > 2: raise BadInput('Bad shape for y vector %s.' % str(y.shape), self, i) if len(y.shape) == 1: x = np.array(range(len(y))) else: assert (len(y.shape) == 2) x = np.array(range(y.shape[1])) if len(x) <= 1: continue if len(y.shape) == 2: y = y.transpose() if len(y.shape) == 1: pid = self.canonicalize_input(i) self.plot_one(pid, x, y, self.config.format) else: assert (len(y.shape) == 2) num_lines = y.shape[0] for k in range(num_lines): pid = "%s-%d" % (self.canonicalize_input(i), k) yk = y[k, :] self.plot_one(pid, x, yk, self.config.format) # TODO: check that if one has time vector, also others have it if self.limits is not None: if self.config.x_min is not None: self.limits[0] = self.config.x_min if self.config.x_max is not None: self.limits[1] = self.config.x_max if self.config.y_min is not None: self.limits[2] = self.config.y_min if self.config.y_max is not None: self.limits[3] = self.config.y_max if self.config.symmetric: if self.config.y_min is not None or \ self.config.y_max is not None: raise BadConfig( 'Cannot specify symmetric together with' 'y_min or y_max.', self, 'symmetric') M = max(abs(self.limits[2:4])) if 'dottedzero' in self.config.fancy_styles: a = pylab.axis() pylab.plot([a[0], a[1]], [0, 0], 'k--') self.limits[2] = -M self.limits[3] = M # leave some space above and below self.limits[2] = self.limits[2] * 1.1 self.limits[3] = self.limits[3] * 1.1 self.axes.axis(self.limits) if self.legend_handle is None: legend = self.config.legend if legend: self.legend_handle = self.axes.legend(*legend, loc='upper right', handlelength=1.5, markerscale=2, labelspacing=0.03, borderpad=0, handletextpad=0.03, borderaxespad=1) # http://matplotlib.sourceforge.net/users/tight_layout_guide.html try: pylab.tight_layout() except Exception as e: msg = ('Could not call tight_layout(); available only on ' 'Matplotlib >=1.1 (%s)' % e) if not self.warned: self.warning(msg) self.warned = True plotting = time.clock() - start start = time.clock() # There is a bug that makes the image smaller than desired # if tight is True pixel_data = pylab2rgb(transparent=self.config.transparent, tight=self.config.tight) # So we check and compensate shape = pixel_data.shape[0:2] shape_expected = (self.config.height, self.config.width) from procgraph_images import image_pad # need here, otherwise circular if shape != shape_expected: msg = ('pylab2rgb() returned size %s instead of %s.' % (shape, shape_expected)) msg += ' I will pad the image with white.' self.warning(msg) pixel_data = image_pad(pixel_data, shape_expected, bgcolor=[1, 1, 1]) assert_equal(pixel_data.shape[0:2], shape_expected) reading = time.clock() - start if False: # 30 49 30 before print("plotting: %dms reading: %dms" % (plotting * 1000, reading * 1000)) self.output.rgb = pixel_data if not self.config.keep: pylab.close(self.figure.number) self.figure = None
class MyBlock(Block): Block.input('x') Block.input_is_variable()
class MyBlockA(Block): Block.input_is_variable() Block.output_is_variable()
class MyBlockB(Block): Block.input_is_variable() Block.output('only one')