Package topo :: Package misc :: Module trace
[hide private]
[frames] | no frames]

Source Code for Module topo.misc.trace

  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   
30 -class DataRecorder(EventProcessor):
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
75 - def __init__(self,**params):
76 super(DataRecorder,self).__init__(**params) 77 self._trace_groups = {}
78 79
80 - def _src_connect(self,conn):
81 raise NotImplementedError
82 83
84 - def _dest_connect(self,conn):
85 super(DataRecorder,self)._dest_connect(conn) 86 self.add_variable(conn.name)
87 88
89 - def input_event(self,conn,data):
90 self.record_data(conn.name,self.simulation.time(),data)
91 92
93 - def add_variable(self,name):
94 """ 95 Create a new time-series variable with the given name. 96 """ 97 raise NotImplementedError
98 99
100 - def record_data(self,varname,time,data):
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
122 - def get_times(self,var):
123 """ 124 Get all the timestamps for a given variable. 125 """ 126 raise NotImplementedError
127 128
129 - def get_time_indices(self,varname,start_time,end_time):
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
160 -class InMemoryRecorder(DataRecorder):
161 """ 162 A data recorder that stores all recorded data in memory. 163 """ 164
165 - def __init__(self,**params):
166 super(InMemoryRecorder,self).__init__(**params) 167 self._vars = {}
168 169
170 - def add_variable(self,name):
171 self._vars[name] = Struct(time=[],data=[])
172 173
174 - def record_data(self,varname,time,data):
175 var = self._vars[varname] 176 177 # add the data, maintaining it sorted by time 178 if not var.time or var.time[-1] <= time: 179 var.time.append(time) 180 var.data.append(data) 181 elif time < var.time[0]: 182 var.time.insert(0,time) 183 var.data.insert(0,data) 184 else: 185 idx = bisect.bisect_right(var.time,time) 186 var.time.insert(idx,time) 187 var.data.insert(idx,data)
188
189 - def get_datum(self,name,time):
190 idx,dummy = self.get_time_indices(name,time,time) 191 data = self._vars[name].data 192 if idx >= len(data): 193 idx -= 1 194 return data[idx]
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 # if the start index is out of bounds 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
221 - def get_times(self,varname):
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 # JPALERT: This should really be something like a NumericTuple, 249 # except that NumericTuple won't allow the use of None to indicate 250 # 'no default'. (Nor will Number.) 251 # JB: We could have that as an option, or as another Parameter 252 # type, but in many cases knowing that the parameter cannot be set 253 # to a non-numeric value is crucial, as it means we don't have to 254 # do special checks every time the value is used. So we should 255 # leave the default behavior as it is, but yes, it would be good 256 # to handle None for numeric types (and also for Boolean, to make 257 # it tri-state). Note that Bounds is a specific type that we 258 # should probably support in any case, because it not only needs 259 # to support None, it needs to specify whether the bounds are 260 # inclusive or exclusive. 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
274 - def __call__(self,data):
275 raise NotImplementedError
276 277 278 # JB: Needs docstring. Should this be a property instead?
279 - def get_ybounds(self,ydata):
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
292 -class IdentityTrace(Trace):
293 """ 294 A Trace that returns the data, unmodified. 295 """
296 - def __call__(self,data):
297 return data
298 299 300
301 -class IndexTrace(Trace):
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
310 - def __call__(self,data):
311 return [x[self.index] for x in data]
312 313 314
315 -class SheetPositionTrace(Trace):
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 # JPALERT: Would be nice to some way to set up the coordinate system 331 # automatically. The DataRecorder object already knows what Sheet 332 # the data came from. 333 coordframe = param.Parameter(default=None,doc=""" 334 The SheetCoordinateSystem to use to convert the position 335 into matrix coordinates.""") 336
337 - def __call__(self,data):
338 r,c = self.coordframe.sheet2matrixidx(self.x,self.y) 339 return [d[r,c] for d in data]
340 341 342
343 -class TraceGroup(param.Parameterized):
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):
366 super(TraceGroup,self).__init__(**params) 367 self.traces = traces 368 self.recorder = recorder
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 # JPALERT: The TraceGroup object should really create its 390 # own matplotlib.Figure object and always plot there 391 # (instead of in the frontmost plot), but I haven't 392 # figured out how to do that yet. 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 # JABALERT: Is there some reason it is called ActivityMovie in 428 # particular, if it can plot things other than Activity?
429 -class ActivityMovie(param.Parameterized):
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
510 - def __init__(self,**params):
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
529 - def save(self):
530 """Save the movie frames.""" 531 532 filename_pat = self.name.join(self.filename_fmt.split('%n')) 533 filename_pat = self.filename_time_fmt.join(filename_pat.split('%t')) 534 filename_pat = self.filetype.join(filename_pat.split('%T')) 535 536 filename_pat = normalize_path(filename_pat,prefix=self.filename_prefix) 537 dirname = os.path.dirname(filename_pat) 538 if not os.access(dirname,os.F_OK): 539 os.makedirs(dirname) 540 541 self.verbose('Writing',len(self.frames),'to files like "%s"'%filename_pat) 542 for t,f in zip(self.frame_times,self.frames): 543 filename = filename_pat% t 544 self.debug("Writing frame",repr(filename)) 545 f.image.save(filename)
546