1 """
2 Object classes for recording and plotting time-series data.
3
4 This module defines a set of DataRecorder object types for recording
5 time-series data, a set of Trace object types for
6 specifying ways of generating 1D-vs-time traces from recorded data,
7 and a TraceGroup object that will plot a set of traces on stacked,
8 aligned axes.
9
10 $Id: trace.py 11304 2010-07-27 16:35:22Z ceball $
11 """
12 __version__ = '$Revision: 11304 $'
13
14 import os
15 import bisect
16 from itertools import izip
17
18 from numpy import asarray
19 import ImageDraw
20
21 import param
22 from param import normalize_path
23
24 from topo.base.simulation import EventProcessor
25 from topo.plotting.bitmap import RGBBitmap, MontageBitmap, TITLE_FONT
26 from topo.misc.util import Struct
27
28
29
31 """
32 Record time-series data from a simulation.
33
34 A DataRecorder instance stores a set of named time-series
35 variables, consisting of a sequence of recorded data items of any
36 type, along with the times at which they were recorded.
37
38 DataRecorder is an abstract class for which different
39 implementations may exist for different means of storing recorded
40 data. For example, the subclass InMemoryRecorder stores all the
41 data in memory.
42
43 A DataRecorder instance can operate either as an event processor, or in a
44 stand-alone mode. Both usage modes can be used on the same
45 instance in the same simulation.
46
47 STAND-ALONE USAGE:
48
49 A DataRecorder instance is used as follows:
50
51 - Method .add_variable adds a named time series variable.
52 - Method .record_data records a new data item and timestamp.
53 - Method .get_data gets a time-delimited sequence of data from a variable
54
55 EVENTPROCESSOR USAGE:
56
57 A DataRecorder can also be connected to a simulation as an event
58 processor, forming a kind of virtual recording equipment. An
59 output port from any event processor in a simulation can be
60 connected to a DataRecorder; the recorder will automaticall create
61 a variable with the same name as the connection, and record any
62 incoming data on that variable with the time it was received. For
63 example::
64
65 topo.sim['Recorder'] = InMemoryRecorder()
66 topo.sim.connect('V1','Recorder',name='V1 Activity')
67
68 This script snippet will create a new DataRecorder and
69 automatically record all activity sent from the sheet 'V1'.
70 """
71
72 __abstract = True
73
74
78
79
81 raise NotImplementedError
82
83
87
88
91
92
94 """
95 Create a new time-series variable with the given name.
96 """
97 raise NotImplementedError
98
99
101 """
102 Record the given data item with the given timestamp in the
103 named timeseries.
104 """
105 raise NotImplementedError
106
107
108 - def get_data(self,varname,times=(None,None),fill_range=False):
109 """
110 Get the named timeseries between the given times
111 (inclusive). If fill_range is true, the returned data will
112 have timepoints exactly at the start and end of
113 the given timerange. The data values at these timepoints will
114 be those of the next-earlier datapoint in the series.
115
116 (NOTE: fill_range can fail to create a beginning timepoint if
117 the start of the time range is earlier than the first recorded datapoint.]
118 """
119 raise NotImplementedError
120
121
123 """
124 Get all the timestamps for a given variable.
125 """
126 raise NotImplementedError
127
128
130 """
131 For the named variable, get the start and end indices suitable
132 for slicing the data to include all times t::
133
134 start_time <= t <= end_time.
135
136 A start_ or end_time of None is interpreted to mean the
137 earliest or latest available time, respectively.
138 """
139
140 times = self.get_times(varname)
141 if start_time is None:
142 start = 0
143 else:
144 start = bisect.bisect_left(times,start_time)
145 if start >= len(times):
146 start = len(times)-1
147 elif times[start] > start_time:
148 start -= 1
149
150 if end_time is None:
151 end = None
152 else:
153 end = bisect.bisect_right(times,end_time)
154
155 return start,end
156
157
158
159
161 """
162 A data recorder that stores all recorded data in memory.
163 """
164
168
169
172
173
188
195
196 - def get_data(self,name,times=(None,None),fill_range=False):
197 tstart,tend = times
198 start,end = self.get_time_indices(name,tstart,tend)
199 var = self._vars[name]
200
201 if start >= len(var.data):
202
203 if fill_range:
204 time = times
205 data = [var.data[-1]]*2
206 else:
207 time,data = [],[]
208 else:
209 time,data = var.time[start:end],var.data[start:end]
210 if fill_range:
211 if time[0] > tstart and start > 0:
212 time.insert(0,tstart)
213 data.insert(0,var.data[start-1])
214 if time[-1] < tend:
215 time.append(tend)
216 data.append(data[-1])
217
218 return time,data
219
220
222 return self._vars[varname].time
223
224
225
226
227 -class Trace(param.Parameterized):
228 """
229 A specification for generating 1D traces of data from recorded
230 timeseries.
231
232 A Trace object is a callable object that encapsulates
233 a method for generating a 1-dimensional trace from possibly
234 multidimensional timeseries data, along with a specification for
235 how to plot that data, including Y-axis boundaries and plotting arguments.
236
237 Trace is an abstract class. Subclasses implement
238 the __call__ method to define how to extract a 1D trace from a
239 sequence of data.
240 """
241
242 __abstract = True
243
244 data_name = param.String(default=None,doc="""
245 Name of the timeseries from which the trace is generated.
246 E.g. the connection name into a DataRecorder object.""")
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261 ybounds = param.Parameter(default=(None,None),doc="""
262 The (min,max) boundaries for y axis. If either is None, then
263 the bound min or max of the data given, respectively.""")
264
265 ymargin = param.Number(default=0.1,doc="""
266 The fraction of the difference ymax-ymin to add to the
267 top of the plot as padding.""")
268
269 plotkw = param.Dict(default=dict(linestyle='steps'),doc="""
270 Contains the keyword arguments to pass to the plot command
271 when plotting the trace.""")
272
273
275 raise NotImplementedError
276
277
278
280 ymin,ymax = self.ybounds
281 if ymax is None:
282 ymax = max(ydata)
283
284 if ymin is None:
285 ymin = min(ydata)
286
287 ymax += (ymax-ymin)*self.ymargin
288
289 return ymin,ymax
290
291
293 """
294 A Trace that returns the data, unmodified.
295 """
298
299
300
302 """
303 A Trace that assumes that each data item is a sequence that can be
304 indexed with a single integer, and traces the value of one indexed element.
305 """
306
307 index = param.Integer(default=0,doc="""
308 The index into the data to be traced.""")
309
312
313
314
316 """
317 A trace that assumes that the data are sheet activity matrices,
318 and traces the value of a given (x,y) position on the sheet.
319 """
320
321 x = param.Number(default=0.0,doc="""
322 The x sheet-coordinate of the position to be traced.""")
323
324 y = param.Number(default=0.0,doc="""
325 The y sheet-coordinate of the position to be traced.""")
326
327 position = param.Composite(attribs=['x','y'],doc="""
328 The sheet coordinates of the position to be traced.""")
329
330
331
332
333 coordframe = param.Parameter(default=None,doc="""
334 The SheetCoordinateSystem to use to convert the position
335 into matrix coordinates.""")
336
340
341
342
344 """
345 A group of data traces to be plotted together.
346
347 A TraceGroup defines a set of associated data traces and allows
348 them to be plotted on stacked, aligned axes. The constructor
349 takes a DataRecorder object as a data source, and a list of
350 Trace objects that indicate the traces to plot. The
351 trace specifications are stored in the attribute self.traces,
352 which can be modified at any time.
353 """
354
355
356 hspace = param.Number(default=0.6,doc="""
357 Height spacing adjustment between plots. Larger values
358 produce more space.""")
359
360 time_axis_relative = param.Boolean(default=False,doc="""
361 Whether to plot the time-axis tic values relative to the start
362 of the plotted time range, or in absolute values.""")
363
364
365 - def __init__(self,recorder,traces=[],**params):
369
370
371 - def plot(self,times=(None,None)):
372 """
373 Plot the traces.
374
375 Requires MatPlotLib (aka pylab).
376
377 Plots the traces specified in self.traces, over the timespan
378 specified by times. times = (start_time,end_time); if either
379 start_time or end_time is None, it is assumed to extend to the
380 beginning or end of the timeseries, respectively.
381 """
382
383 import pylab
384 rows = len(self.traces)
385 tstart,tend = times
386
387 pylab.subplots_adjust(hspace=self.hspace)
388 for i,trace in enumerate(self.traces):
389
390
391
392
393 pylab.subplot(rows,1,i+1)
394 pylab.title(trace.name)
395 time,data = self.recorder.get_data(trace.data_name,times=times,fill_range=True)
396 y = trace(data)
397 if self.time_axis_relative:
398 time = asarray(time) - time[0]
399 pylab.plot(time,y,**trace.plotkw)
400 ymin,ymax = trace.get_ybounds(y)
401 pylab.axis(xmin=time[0],xmax=time[-1],ymin=ymin,ymax=ymax)
402
403
404
405
406
407 -def get_images(name,times,recorder,overlays=(0,0,0)):
408 """
409 Get a time-sequence of matrix data from a DataRecorder variable
410 and convert it to a sequence of images stored in Bitmap objects.
411
412 Parameters: name is the name of the variable to be queried. times
413 is a sequence of timepoints at which to query the
414 variable. recorder is the data recorder. overlays is a tuple of
415 matrices or scalars to be added to the red, green, and blue
416 channels of the bitmaps respectively.
417 """
418 result = []
419
420 for t in times:
421 d = recorder.get_datum(name,t)
422 im = RGBBitmap(d+overlays[0],d+overlays[1],d+overlays[2])
423 result.append(im)
424 return result
425
426
427
428
430 """
431 An object encapsulating a series of movie frames displaying the
432 value of one or more matrix-valued time-series contained in a
433 DataRecorder object.
434
435 An ActivityMovie takes a DataRecorder object, a list of names of
436 variables in that recorder and a sequence of timepoints at which
437 to sample those variables. It uses that information to compose a
438 sequence of MontageBitmap objects displaying the stored values of
439 each variable at each timepoint. These bitmaps can then be saved
440 to sequentially-named files that can be composited into a movie by
441 external software.
442
443 Parameters are available to control the layout of the montage,
444 adding timecodes to the frames, and the names of the frame files.
445 """
446
447
448 variables = param.List(class_=str, doc="""
449 A list of variable names in a DataRecorder object containing
450 matrix-valued time series data.""")
451
452 overlays = param.Dict(default={}, doc="""
453 A dictionary indicating overlays for the variable bitmaps. The
454 for each key in the dict matching the name of a variable, there
455 should be associated a triple of matrices to be overlayed on
456 the red, green, and blue channels of the corresponding bitmap
457 in each frame.""")
458
459 frame_times = param.List(default=[0,1], doc="""
460 A list of the times of the frames in the movie.""")
461
462 montage_params = param.Dict(default={},doc="""
463 A dictionary containing parameters to be used when
464 instantiating the MontageBitmap objects representing each frame.""",
465 instantiate=False)
466
467 recorder = param.ClassSelector(class_=DataRecorder, doc="""
468 The DataRecorder storing the timeseries.""")
469
470 filename_fmt = param.String(default='%n_%t.%T',doc="""
471 The format for the filenames used to store the frames. The following
472 substitutions are possible:
473
474 %n: The name of this ActivityMovie object.
475 %t: The frame time, as formatted by the filename_time_fmt parameter
476 %T: The filetype given by the filetype parameter. """)
477
478 filename_time_fmt = param.String(default='%05.0f', doc="""
479 The format of the frame time, using Python string substitution for
480 a floating-point number.""")
481
482 filetype = param.String(default='tif',doc="""
483 The filetype to use when writing frames. Can be any filetype understood
484 by the Python Imaging Library.""")
485
486 filename_prefix = param.String(default='', doc="""
487 A prefix to prepend to the filename of each frame when saving;
488 can include directories. If the filename contains a path, any
489 non-existent directories in the path will be created when the
490 movie is saved.""")
491
492 add_timecode = param.Boolean(default=False, doc="""
493 Whether to add a visible timecode indicator to each frame.""")
494
495 timecode_options = param.Dict(default={},instantiate=False,doc="""
496 A dictionary of keyword options to be passed to the PIL ImageDraw.text method
497 when drawing the timecode on the frame. Valid options include font,
498 an ImageFont object indicating the text font, and fill a PIL color
499 specification indicating the text color. If unspecified, color defaults to
500 the PIL default of black. Font defaults to topo.plotting.bitmap.TITLE_FONT.""")
501
502 timecode_fmt = param.String(default='%05.0f',doc="""
503 The format of the timecode displayed in the movie frames, using
504 Python string substitution for a floating-point number.""")
505
506 timecode_offset = param.Number(default=0,doc="""
507 A value to be added to each timecode before formatting for display.""")
508
509
511 super(ActivityMovie,self).__init__(**params)
512
513 bitmaps = [get_images(var,self.frame_times,self.recorder,
514 overlays=self.overlays.get(var,(0,0,0)))
515 for var in self.variables]
516
517 self.frames = [MontageBitmap(bitmaps=list(bms),**self.montage_params)
518 for bms in izip(*bitmaps)]
519 if self.add_timecode:
520 for t,f in izip(self.frame_times,self.frames):
521 draw = ImageDraw.Draw(f.image)
522 timecode = self.timecode_fmt % (t+self.timecode_offset)
523 tw,th = draw.textsize(timecode,font=self.timecode_options.setdefault('font',TITLE_FONT))
524 w,h = f.image.size
525
526 draw.text((w-tw-f.margin-1,h-th-1),timecode,**self.timecode_options)
527
528
546