DESIGNING TO FACILITATE CHANGE
WITH OBJECT-ORIENTED FRAMEWORKS
BY
BRIAN FOOTE
B.S.,
THESIS
Submitted in partial fulfillment of the requirements
for the degree of Master of Science in Computer Science
in
the
DESIGNING TO FACILITATE CHANGE
WITH OBJECT-ORIENTED FRAMEWORKS
Brian Foote
Department of Computer Science
Ralph E. Johnson, Advisor
Application domains that are characterized by rapidly changing software requirements pose challenges to the software designer that are distinctly different from those that must be addressed in more conventional application domains. This project assesses the impact of applying object-oriented programming tools and techniques to problems drawn from one such domain: that of realtime laboratory application programming. The project shows how a class inheritance hierarchy can promote the emergence of application specific frameworks as a family of applications evolves. Such frameworks offer significant advantages over conventional skeleton program-based approaches to the problems of managing families of applications. Particular attention is given to design issues that arise both during the initial design and implementation of such applications, and during later stages of their lifecycles, when these applications become the focus of reuse efforts. The project also addresses the impact of object-oriented approaches on the simulation of realtime systems and system components.
To Henry Hudson
Acknowledgements
This project has a longer history than most masters projects. This one is based on a larger system upon which I worked at the University of Illinois Department of Psychology's Cognitive Psychophysiology Laboratory (CPL). I am indebted to Emanuel Donchin, the CPL's Director, and to Earle F. Heffley III, the CPL's Technical Director, both for their willingness to allow me to explore new programming techniques with the CPL battery, which laid the foundation for this project, and for their support for my efforts to juggle school and work, which made this project possible.
I am grateful as well to Michael Faiman, Walter Schneider, and Arthur Kramer, who helped to convince me of the wisdom of pursuing an advanced degree.
Roy Campbell's advanced software engineering course had a major impact on my thinking about object-oriented framework lifecycle issues.
I'd like to thank Dan Ingalls, Kent Beck, and Barry Haynes of Apple Computer for providing me with up-to-date tools and assistance with the Macintosh Smalltalk that I used for the battery simulation.
Without the tireless help of my writing coach, Audrey Wells, this document would be in much sorrier shape than it is. The responsibility for those warts that remain, is of course, entirely mine.
Finally, I'd like to gratefully acknowledge the assistance, encouragement, enthusiasm and patience of my thesis advisor, Ralph Johnson. Without his willingness to embark on an open-end investigation rooted in an application domain somewhat foreign to day-to-day computer science, this effort (such as it is) would simply not have been possible.
Table of Contents
Chapter I -- Introduction................................................................................................... 1
The Structure of this Document.............................................................................. 4
Chapter II -- Background.................................................................................................. 5
The Realtime Laboratory Application Domain........................................................ 5
The CPL
A Tour of the CPL
Why a Smalltalk
Chapter III -- Anatomy of
the
Battery-Items........................................................................................................ 21
BatteryItem............................................................................................... 22
SternbergTask........................................................................................... 48
ToneOddball............................................................................................. 54
WordOddball............................................................................................ 58
Battery-Parameters................................................................................................ 63
BatteryParameters..................................................................................... 64
SternbergTaskParameters.......................................................................... 68
ToneOddballParameters............................................................................ 71
WordOddballParameters........................................................................... 73
Stimulus-Generators.............................................................................................. 74
StimulusGenerator..................................................................................... 76
SternbergDisplayGenerator........................................................................ 78
ToneGenerator.......................................................................................... 80
WordGenerator......................................................................................... 83
Stimulus-Support................................................................................................... 85
Stimulus..................................................................................................... 86
SternbergStimulus...................................................................................... 87
ToneStimulus............................................................................................. 88
WordStimulus............................................................................................ 89
Sequence-Support................................................................................................. 90
WeightedCollection................................................................................... 91
SequenceGenerator................................................................................... 94
Response-Support................................................................................................. 97
ButtonBox................................................................................................. 98
ButtonBoxResponse.................................................................................. 101
Data-Management................................................................................................. 102
BatteryBlock............................................................................................. 103
BatteryTrial............................................................................................... 104
DataDictionary.......................................................................................... 106
DataDictionaryEntry.................................................................................. 107
Interface-Battery................................................................................................... 109
ListHolder................................................................................................. 110
ItemListController...................................................................................... 112
ParameterListController............................................................................. 115
BatteryCodeController.............................................................................. 117
WaveformController.................................................................................. 119
BatteryBrowser......................................................................................... 121
ItemListView............................................................................................. 127
ParameterListView.................................................................................... 128
BatteryView.............................................................................................. 130
BatteryCodeView...................................................................................... 133
WaveformView......................................................................................... 134
Chapter IV -- Anatomy of
the
Realtime-Support.................................................................................................. 145
Timebase................................................................................................... 146
Timebase................................................................................................... 147
Realtime-Devices................................................................................................... 149
Device....................................................................................................... 150
ClockedDevice.......................................................................................... 152
Clock........................................................................................................ 154
Digitizer..................................................................................................... 161
BufferedDigitizer........................................................................................ 164
StreamedDigitizer...................................................................................... 166
InputBit..................................................................................................... 168
OutputBit.................................................................................................. 173
Waveform-Support............................................................................................... 175
Averager................................................................................................... 176
Waveform................................................................................................. 180
WaveformCollection.................................................................................. 183
Tally.......................................................................................................... 185
Random-Support................................................................................................... 189
IntegerGenerator....................................................................................... 190
IntegerStream............................................................................................ 191
RandomStream.......................................................................................... 193
SampledStream......................................................................................... 197
Plumbing-Support.................................................................................................. 199
Filter......................................................................................................... 200
Pump........................................................................................................ 203
Tee........................................................................................................... 207
ValueFilter................................................................................................. 208
Valve........................................................................................................ 210
ValueSupply.............................................................................................. 212
Accessible-Objects............................................................................................... 214
AccessibleObject...................................................................................... 215
AccessibleDictionary................................................................................. 223
Chapter V -- A Tour of the
Chapter VI -- Discussion................................................................................................... 229
Object-Oriented Frameworks................................................................................ 229
Environments......................................................................................................... 235
Getting Skeletons Back in the Closet...................................................................... 235
Why Software Design is So Difficult....................................................................... 241
Designing in the Presence of Volatile Requirements................................................. 246
Designing to Facilitate Change................................................................................ 251
Specificity vs. Generality........................................................................................ 252
Objects, Evolution, and Maintenance..................................................................... 254
Reuse vs. Reinvention............................................................................................ 257
Frameworks and the Software Lifecycle................................................................. 259
Programming in the Smalltalk-80 Environment........................................................ 263
The Learnability Gap............................................................................................. 264
Smalltalk and Realtime Systems............................................................................. 266
Starting with a "Real" System................................................................................. 269
Lisp, Simula, and Uniform Access.......................................................................... 270
O2 Programming is Easy. O2 Design is Hard........................................................ 274
Chapter VII -- Conclusion................................................................................................. 277
References........................................................................................................................ 279
Chapter I -- Introduction
A good software designer knows that a job is not done when the requirements for the project at hand have been met. Well-designed systems will lay the foundations for solving related problems as well. Good designers will always keep one eye open to opportunities to produce code and components that are usable beyond the scope of the current problem. A common result of such efforts is the accumulation of a library of broadly applicable utility components and routines. Another result can be the construction of robust, easily extensible applications that facilitate the evolution of a given application or component as the demands made of it change.
Not all programming
systems and methodologies are equally effective in supporting the graceful
evolution of applications and systems.
Certainly high-level languages such as Algol-68 and Pascal, and approaches
such as stepwise refinement and structured programming have done much to ease
the burden of the programmer designing new applications. The notions of encapsulation and information
hiding, which have been embodied in languages like
This document describes a project (the battery simulation) that assesses the value of bringing object-oriented tools and techniques to bear on problems drawn from the domain of realtime laboratory programming. The project was based upon a large laboratory system (the CPL battery, or the battery) developed by the author using more traditional tools and approaches. The project involved the reimplementation of substantial portions of the CPL battery using Smalltalk-80. The principal question that motivated this effort was: How might the use of object-oriented tools and techniques affect the design, implementation, and evolution of programs in this application domain?
The problems of realtime laboratory programming are quite distinct from those in areas that have been more comprehensively studied by computer scientists, such as compiler construction or operating system design. Realtime laboratory data acquisition and experimental control applications must flourish in a research environment that is (by the very nature of research itself) characterized by rapidly changing requirements. These applications must also operate under severe timing and performance constraints, and must be designed to facilitate graceful evolution.
The applications which provided the basis for this project (the CPL battery) resulted from a fairly ambitious attempt to address some of the requirements stated above using traditional programming tools. A number of the approaches taken in the design of the CPL battery were inspired by object-oriented techniques. This project (the battery simulation) represented an attempt to ascertain what impact the use of a full-blown, bona-fide object-oriented programming environment (Smalltalk-80) might have on a redesign and reimplementation of representative portions of the CPL battery.
This Smalltalk-80 reimplementation had, from the onset, an exploratory character. An important aim of the project was to examine how the Smalltalk language and system might affect the sort of code produced to solve laboratory programming problems. The "plumbing" data stream classes for data analysis and the "accessible object" record/dictionary classes are two of the more interesting results. Another goal was to assess the utility of Smalltalk's user interface construction tools in constructing these applications. The waveform and parameter browsing tools incorporated into the project resulted from this effort. The plumbing data stream classes, accessible objects, and the battery browsing tools are presented in detail in Chapters III and IV of this document.
The overriding focus of this effort, however, was not as much to see how object-oriented techniques might aid in the construction of any given application as it was to assess the ways in which these techniques might be used to avoid the sort of wasteful duplication that is conventionally seen in these sorts of application domains as requirements change.
One way in which object-oriented schemes help to meet this goal is by encouraging the design of general, application independent libraries of reusable components. More conventional programming environments do this too. The information hiding capabilities present in object-oriented languages and systems are of great benefit in promoting the development of reusable libraries.
Another way in which object-oriented approaches facilitate graceful component evolution is via the ability of an object-oriented system to support the customization of a general kernel of components through the specialization ability provided by inheritance. The specialization and reuse capabilities provided by object-oriented inheritance and polymorphism increase the potential applicability of both preexisting and user generated system components. Hence, effort spent making a component more general is likely to pay off sooner than it might in a conventional system.
The Smalltalk battery simulation uses a class inheritance hierarchy to help manage a set of related, evolving applications as they diverge from a common ancestor. The emergence of an application specific framework in the face of volatile requirements is perhaps the most interesting consequence of the use of object-oriented techniques.
Traditional tools and approaches in the laboratory domain encourage a programming style built around a library of context independent reusable subroutines and disposable custom programs built (perhaps) from simple skeletons. An object-oriented approach allows a broad middle ground between these two extremes: the application framework.
The existence of a mechanism that allows the graceful evolution of program components from the specific to the general is a valuable asset during the design of any system, but the value of such a capability takes on an additional dimension of importance when such systems must evolve in the face of highly dynamic requirements. Thus, designing to facilitate change takes in lifecycle issues that normally are addressed under the rubrics of reuse, maintenance and evolution.
The Structure of this Document
This document is organized as follows:
Chapter II gives the history and background of this project, including a detailed description of the system upon which the project was based (the CPL battery), and discusses Smalltalk and object-oriented programming in general.
Chapters III and IV give the detailed anatomy of the Smalltalk-80 battery simulation framework and library.
Chapter V gives an illustrated tour of the battery simulation.
Chapter VI contains a discussion of a number of general questions and points raised by this research.
Chapter VII summarizes the project's results and conclusions.
Chapter II -- Background
This chapter
describes the realtime laboratory application domain and the CPL battery, and
discusses how the CPL battery spawned the Smalltalk battery simulation.
The Realtime Laboratory Application Domain
The Smalltalk battery simulation that is the focus of the effort described in this document was based on another system (the CPL battery) developed by the author while employed (as a systems programmer/analyst) at the University of Illinois Department of Psychology's Cognitive Psychophysiology Laboratory (CPL).
A basic psychophysiological paradigm employed by researchers at the CPL is the so-called "oddball" paradigm [Donchin 81]. In an oddball experiment, a subject (typically human) is presented with a Bernoulli series of one of two alternative stimuli, such as high or low tones. Each such presentation constitutes a single trial. A complete series of such trials is called a block. In the simplest case, the subject's task is to count each occurrence of one type of stimuli, and ignore all occurrences of the other.
During this procedure, EEG is collected from the subject via electrodes affixed to the subject's scalp. This EEG is amplified and and fed into a computer-driven analog-to-digital converter. The phenomena of interest are low amplitude signals that are difficult to detect in the wash of noise that is typically present in the EEG collected for any given single trial. Thus, signal averaging is necessary to produce aggregate responses across collections of many trials.
All aspects of data collection and experimental control are under the realtime control of a laboratory computer system. This system must concurrently generate and present stimuli to the subject, digitize and store collected data on some secondary storage medium, and conduct an interactive dialog with the experimenter, which may include the generation of realtime waveform displays and the presentation of statistical information.
A single trial might last for a period of 1 to 2 seconds. Data are typically digitized at 100 to 200 points per second. Data from 2 to 32 channels (electrode sites) are recorded for each trial, and stored in real time to magtape or cartridge tape. In addition to the analog data, digital input data reporting a subject response to a given stimulus, together with a response time (RT) may be collected for each trial.
The definition of what constitutes adequate realtime performance varies enormously from one application domain to the next. Certain applications in high energy physics may require that microsecond or better accuracy be present in the relative timing among experimental events. Many process control applications (and so-called realtime operating systems) effectively define realtime as being within a line clock "tick" (1/50th or 1/60th of a second). Psychophysiological applications usually require that experimental timing be accurate to within a millisecond or so.
The computer system used to run these experiments is one designed and constructed at the CPL. These systems, called Pearl II systems [Heffley 85] are built around DEC LSI-11/73 processors, and contain from 256k to 4M bytes of memory. Among the more noteworthy attributes of these systems is the set of 6 custom PC cards that provide 6 realtime clocks, a 16/32 channel 12 bit DMA clocked A/D system, a 4 channel clocked 12 bit DMA system, and a custom DMA cartridge interface.
Programs are developed and run under the DEC RT11 operating system. Application programs are usually written in Fortran IV, with the assistance of a structured preprocessor called FLECS [Beyer 75] that provides facilities such as advanced control structures, constant definition, and internal procedures that are not (or were not) present in the Fortran 66-based DEC Fortran IV implementation.
Application development is supported by a large library of Fortran and PDP11 assembly language (Macro-11) subroutines called LABPAK [Donchin 75], [Foote 85]. This library provides fast implementations of operations that are time or performance critical, or beyond what can easily be accomplished in Fortran (such as device drivers).
The LABPAK library hides much of the detail and complexity of dealing with realtime device programming and high performance data manipulation from the application programmer. Hence, laboratory application development can be undertaken by researchers themselves.
Naturally, there is a great deal of variation among the levels of programming skills exhibited by researchers in this environment. A typical simple laboratory application program might be a few hundred lines long. Usually, such applications are written by a single individual to solve some problem immediately at hand. (Often, the programs generated by researchers are variations on the oddball theme discussed above.) Once written, such an application program may take on a life of its own. This is because research, by its nature, ensures that no single application stays current for very long. The problems presented by moving target requirements will be addressed in detail in Chapter VI.
The CPL Battery
The CPL Battery was developed in response to requirements in several research contracts that called for the development of a battery of electrophysiological tests for the assessment of toxic exposure (for the EPA) and man-machine operator workload (for the Air Force). This package is discussed in a paper by Heffley, Foote, Mui and Donchin [Heffley 85]. The specifications for these tests called for the development of a battery of related applications that together would constitute a turn-key package with which a relatively unskilled operator could administer these tests. The specifications called as well for a great deal of latitude and flexibility in the configuration of these battery items. At the same time, each of the specified items was, in many respects, a variation on the oddball theme discussed previously.
It seemed obvious at the outset of this project that using the conventional approach of developing and maintaining a single separate application program for each required battery task was not feasible. The burden of maintaining such a large collection of programs appeared to have the potential of becoming overwhelming. The commonalities in the underlying structures of these applications ensured that large portions of each application would be duplicated across many or all of them. What's more, the high level of flexibility required by each of the specifications would have required that several versions of each item be developed if each had a scope similar to that of one of the applications we were used to.
Even when surface differences could be otherwise be reconciled, there was a need for variations in the user interfaces of the programs to tailor them to the needs of the various contractors, particularly with respect to the operator interfaces.
It was to meet
these requirements that we developed the CPL
Typical CPL laboratory application programs have always had a brief list of parameters that could be modified by the experimenter using a simple table editing scheme. This parameter dialog might allow an experimenter to alter between 10 and 50 experimental parameters. In order to allow the large number of variations called for in the specification, a much larger number parameters seemed necessary.
To support this capability, we developed a stand-alone parameter editor, which could accommodate up to 512 parameters per battery item. This editor is table driven, and provides both type and subrange checking. Integer, Option (Boolean) and String types are supported by the battery editor. The editor provided a full screen, arrow and key-letter driven interface (which was still a novelty during the early 1980s) as well as a per parameter help capability (which is still more of a novelty than it should be.) The editor also supports the realtime interpretation of symbolic arithmetic expressions, and included a primitive constraint resolution mechanism. It also provided facilities for storing and retrieving sets of parameter values.
The tables that drive the parameter editor are also the basis for the battery data management scheme. By retaining symbolic information describing the contents of battery data records, any battery program or other utility may access data generated by any other utility. These tables are generated by a preprocessor that takes a textual description of each parameter, along with its help information, and generates binary tables, help files, and FLECS common definition files. These include files provide an efficient mechanism for establishing a correspondence between symbolic names in the parameter editor and variables in the application programs themselves.
The development of support for a multi-page parameter dialog permitted the design of a handful of general applications that could, by virtue of the high degree of configurability made possible by the large number of parameters present, take the place of a large number of single purpose programs.
To attempt to exploit the structural similarities between battery items, a decision was made to use the internal procedure and conditional compilation capabilities of the FLECS preprocessor to attempt to mimic an object-oriented inheritance hierarchy. This attempt proved quite successful. Each of the eight or so battery items is derived from a single common FLECS framework. The sharing of large amounts of structural code among all the battery items permits one to lavish this common core with additional attention and features, with the knowledge that this effort will benefit all the applications derived from it.
This attempt to employ object-oriented principles was motivated by a keen interest on the part of the author in object-oriented programming, and Smalltalk in particular. It was this interest, combined with the battery effort, that motivated the Smalltalk battery simulation described herein.
A Tour of the CPL Battery
This chapter
illustrates how an experimenter interacts with the CPL

The screen above shows the CPL battery's main menu page. Users select an item either by using the vertical arrow keys, or by using one of a set of single keystroke command synonyms.

The screen above shows the main menu of the Word Oddball battery item. User may run a block of trials, inspect and change parameter values, or manipulate and examine the contents of either a Pearl II 1/4 inch cartridge tape, or a 9 track magtape.

The screen above depicts one of the Word Oddball items parameter directory pages. This battery item has three such pages. Each entry in the directory refers to a parameter page that may in turn contain up to 16 parameters.

This screen shows one of the Word Oddball's parameter pages. This page contains parameters associated with data collection. The values of the A/D digitizing rate, the A/D digitizing time, and the number of data points per sweep may be traded off against one another. The results of doing so are immediately displayed. The single degree of freedom present in this calculation may be assigned using the "Calc" parameters. The effect of such manipulations on memory usage is reflected immediately in the "Bytes Free" parameter. Such calculations are conducted by special constraint satisfaction code built into the menu management program that drives this display.
The text at the bottom gives a brief description of the "A/D Gain" parameter. Such interactive help is available for all of a battery item's parameters. The user alters a parameter by moving the cursor to it using arrow keys, and typing return. The user may then type an expression, the value of which becomes the new parameter value, if the parameter's limit constraints are not violated. This expression may include most standard Fortran and C operators, as well as symbolic parameter names. Hence, "A/D Gain"*2+1 is a legal expression.
The

The screen above gives a list of the commands available to the user in the parameter editor.

Thescreen above shows the information given to the experimenter just before a block is begun.

The Macintosh screen dump above shows part of the Word Oddball items runtime display. Trial categories are distinguished by bold and dim text. The trial number is given in the leftmost column, followed by the stimulus. The next three columns give subject response information. Since no subject was present when this example was run, the reaction time column (RT) is filled with zeros, and the response columns are written in reverse video, indicating that the responses for this trial are in error.
Why a Smalltalk
A number of factors made simulating the CPL battery in Smalltalk seem like a potentially worthwhile enterprise. The CPL battery was constructed using techniques and approaches that were taken from Smalltalk. The simulated inheritance hierarchy used to manage related applications and the highly interactive user interface present in the CPL battery were both inspired by Smalltalk. Both were simulated at considerable expense in the CPL battery. It seemed natural to ask what advantage there might be in using Smalltalk itself to construct a system with similar objectives.
The inheritance mechanism in Smalltalk is much more powerful (and less coarse) than the one used in the CPL battery. As for the user interface, Smalltalk is (rightfully) well known for having originated the bit-mapped, windowed user interface style that is so popular today. The Smalltalk environment comes with an extensive array of classes that support this user interface.
It seemed interesting to ask as well what impact an object-oriented language like Smalltalk might have on low level coding. Object-oriented polymorphism seemed to have the potential to allow low level code to cope with the needs of a family of related applications in a tidier fashion than conventional languages (like Fortran) might allow. Also, object-oriented programming styles encourage the development of a collection of modular application specific abstractions. Assessing the impact of using such abstractions on the coding process seemed a worthwhile pursuit.
Above all, however, the battery simulation was inspired by a desire to investigate the impact of object-oriented techniques and environments on the design of a family of diverging applications. In particular, I wanted to determine how a Smalltalk implementation of a system like the battery would support the sort of extension and change present in a laboratory environment.
Certain other aspects of the Smalltalk system made it attractive as well. Under Smalltalk-80, all the objects known to a given copy of the system are resident in that system's image. It struck me that such an environment might be an interesting place to investigate new ideas about managing the large volumes of data that are produced by laboratory applications, particularly with respect the problem of identifying and labeling such data. In a resident environment, stored data could retain their original identities as objects, and respond to their full repertoire of messages. The combination of resident data and the Smalltalk user interface tools seemed to have considerable potential.
One last aspect of Smalltalk versus the CPL development environment seemed compelling as well. The Apple Lisa and Macintosh MC68000 systems that supported the Smalltalk battery simulation had up to 2 megabytes of directly addressable memory available to them. The original CPL battery ran (and still runs) on DEC PDP-11 family processors that can, for most purposes, address only 64kb of memory directly.
Chapter III -- Anatomy of the Battery Framework
The
This chapter, and
the one that follows it, give a detailed
exposition of all the code in the
Battery-Items The Sternberg, Word and Tone Oddball
Battery-Parameters Parameter carriers for the Battery Items
Stimulus-Generators Pluggable stimulus presentation devices
Stimulus-Support Stimulus descriptor objects
Sequence-Support Stimulus sequences generation support
Response-Support Subject response descriptor objects
Data-Management Data archive simulation objects
Interface-Battery Support for browsing battery items
Realtime-Support Simulated realtime timebase code
Realtime-Devices Clocks, digitizers, digital I/O devices
Waveform-Support Waveform collection and statistics
Random-Support Random integer and stream support
Plumbing-Support Objects for connecting streams together
Accessible-Objects Support for record/dict. transparency
The Battery-Items category contains the core of the battery simulation: the abstract class BatteryItem. BatteryItem is the principal component of the psychophysiological experimental application framework. This class is the superclass of the three concrete battery items presented here: WordOddball, ToneOddball, and SternbergTask. One of the central goals of this simulation was to demonstrate how a class inheritance hierarchy could be used allow a family of related application programs to share common code.
The Battery-Parameters category contains a set of record-like objects that carry the user modifiable parameter values for each battery item. These are organized in a hierarchy that mirrors the battery item hierarchy. The use of separate objects to embody the configurable state of battery items simplifies and isolates the design of the battery parameter viewing mechanism.
The Stimulus-Generator category embodies an attempt to construct interchangeable stimulus generation modules that could be fitted into any appropriate application.
The Stimulus-Support and Response-Support categories contain classes that function as descriptors that ferry information between the battery items themselves and the stimulus generation and response reporting systems.
The Battery-Parameters, Stimulus-Generator, Stimulus-Support, and Response-Support categories are examples of objects which have been factored out of the original battery item design as discrete components. An experimenter constructing a new experiment will extend BatteryItem, or some subclass thereof, along with members of these hierarchies. Because these elements are not subclasses of BatteryItem itself, these components can be mixed and matched as black boxes among BatteryItems.
The Sequence-Support category supports the generation of experimental stimulus sequences.
The Data-Management category contains a set of descriptor classes that provide the foundation for a dictionary-based data archiving scheme. Class BatteryTrial contains waveform data and descriptive scalar information for each single trial that is performed. A BatteryBlock contains all the trials collected during a given experimental run. Each completed BatteryBlock is entered into a DataDictionaryEntry descriptor and entered into a DataDictionary. Class BatteryItem contains an instance of DataDictionary that serves as the master repository for battery data.
The
Interface-Battery category supports the
The code for the battery independent categories is described in the Chapter IV.
The code for each system category is presented in turn. A general description of each category is given first, along with a list of the classes defined in that category. Each class in the category is then presented. Each class description begins with a table that resembles a standard Smalltalk class definition template. The class name and its superclass are given, first followed by lists of instance and class variables. Inherited instance variable names are given in italics. These lists are followed by the pool dictionary list, the category name, and a table showing the class hierarchy.
Battery-Items
The classes in the
Battery-Items category constitute the framework around which the
BatteryItem A generic battery item
SternbergTask A Sternberg memory task
ToneOddball An auditory oddball experiment
WordOddball A visual semantic oddball experiment
BatteryItem
Class BatteryItem is the central core of each of the three actual battery items implemented herein. BatteryItem is an abstract superclass, since it exists only to provide a repository for the common behaviors among the three concrete battery items.
A BatteryItem is a generic psychophysiological experiment. A BatteryItem object's job is to run a block of experimental single trials whenever it receives the doBlock message. A block is a collection of a designated number of single trials. The number of trials in a block is an experimental parameter.
A single trial, or trial, corresponds to the presentation of a stimulus or a sequence of stimuli to an experimental subject. For each trial, a BatteryItem must coordinate the timing and presentation of stimuli, the collection of A/D waveform data (using an analog-to-digital converter, or digitizer), the collection of discrete response time information, and the maintenance of cumulative statistics and experimenter displays. A BatteryItem must accurately orchestrate all these activities in (simulated) real time.
BatteryItem is a subclass of AccessibleObject, which enables each of its instance variables to be accessed as though a BatteryItem were a dictionary. AccessibleObjects allow new key/value pairs to be defined, which will then also respond to the conventional instance variable accessing protocol (item, item: value). See the discussion of category Accessible-Objects for more information on what AccessibleObjects can do. BatteryItems inherit the instance variable items from AccessibleObject. This instance variable helps to provide the soft instance variable facility mentioned above.
The class variable BatteryDataDictionary is an important component of the battery simulation's rudimentary data management scheme. Rather than writing battery data to some external medium, the battery simulation enters the data collected during each run of a battery item into a dictionary that is kept in BatteryDataDictionary.
class name BatteryItem
superclass AccessibleObject
instance variable names
items
trialClock
stimulusClock
digitizer
responseDevice
parameters
stimulusSequence
sequenceGenerator
stimulusGenerator
buffer
experimenterLog
waveformDisplay
block
blockNumber
nextTrial
currentTrial
currentTrialNumber
outputStream
averagePipeline
tallyPipeline
class variable names
BatteryDataDictionary
category Battery-Items
hierarchy
Object
AccessibleObject
BatteryItem
SternbergTask
ToneOddball
WordOddball
BatteryItem methods for: block control
The block control
methods control block preparations, the sequencing of single trials, and block
termination activities.
The fundamental operation that a BatteryItem can be called upon to perform is to doBlock, which defines the basic sequence of block activities. Any time consuming block preparations are made by prepareBlock. These include updating the block counter, preparing a stimulus sequence, preparing the data management system, and setting up the dataflow data processing fixtures. Then, time-critical block start-up actions are performed by startBlock, such as informing the stimulus generator object that the block is starting.
The division of labor among the components that constitute a battery item is such that each battery item works with a discrete stimulus generating object of some sort. For some applications, the internal mechanisms of this object might be quite complex, and hence there is a need to synchronize it with the block and trial sequencing done by the battery item.
The runBlock method is the heart of the block control mechanism. It zeros a trial counter, and proceeds to generate and execute trials. A trial is executed by sending the doTrial message to a battery item. The trial control code is discussed below.
When the block has completed, the finishBlock method is executed. This method tells the data management and stimulus generation components to perform any block termination activities that they might require.
An experiment that required that basic changes be made to a BatteryItem's block sequencing behavior might override these methods. For example, an experiment that required that a sequence of blocks be run in order with only minor, systematic changes to experimental parameters might override some of these methods.
BatteryItem methods for: block control
doBlock
"To run a block, we
must make adequate preparations, start per block
activity, run the blocks's
trials themselves, and finish up. The
fundamental service a
BatteryItem provides to the outside world is the
ability to do a block when
it is requested to."
self prepareBlock.
self startBlock.
self runBlock.
self finishBlock
finishBlock
"We are done. Clean up.
We perform whatever end-of-block activity
our data management scheme
may require, and tell our stimulus generator
that the block is
over."
self finishDataManagement.
self stimulusGenerator finishBlock
prepareBlock
"Block preparations are
those time-consuming tasks that need to be
performed before a block is
started. These are as opposed to actions
of an
essentially instantaneous
nature, which should be enumerated in
startBlock. "
self blockNumber: self blockNumber + 1.
self prepareStimulusSequence.
self prepareDataManagement.
self prepareProcessingFixtures
runBlock
"To actually run the
block, we have to generate a bunch of trials.
We
overlap the completion of
the current trial and the preparation of the
next in doTrial. Hence, we must prime this process here."
self currentTrialNumber: 0.
self prepareNextTrial.
(1 to: self trials)
do:
[:n |
self currentTrialNumber: n.
self currentTrial: self nextTrial.
self doTrial]
startBlock
"Tell our stimulus
generator we are starting up."
stimulusGenerator startBlock
BatteryItem methods for: trial control
The trial control
methods are templates for a BatteryItem's basic trial control sequencing.
The doTrial method conducts an experimental trial. The startTrial method starts the single trial timer (which is returned by the trialClock method) for an amount of time indicated by the trialDuration method. The runTrial method is the generic trial body. It calls collectData and processData.
The collectData method must be provided by BatteryItem's subclasses, and is, in this implementation of the battery, the primary locus of differences among BatteryItem's concrete subclasses.
The processData method takes the data deposited in the buffer object and per-trial data structures and performs per-trial processing on them.
The prepareNextTrial method constructs the per-trial data objects for the next trial. This method allows us to overlap the perhaps time consuming preparation of the next trial with the often relatively quiescent period at the end of the current trial.
The finishTrial method simply waits for the trial clock's time to elapse.
BatteryItem methods for: trial control
doTrial
"To do a trial, do the
trial start up, run the trial, prepare the next one,
and clean up. We prepare the next trial before waiting for
the time
allotted for the current
trial to elapse (which is done in finishTrial)."
self startTrial.
self runTrial.
self prepareNextTrial.
self finishTrial
finishTrial
"We'll always end by
waiting for the trial clock. In the
interest of tidy
timing, subclasses that
override this method might want to send super
finishTrial last."
trialClock wait
prepareNextTrial
"Do stimulus selection
and preparation here. We allocate a
BatteryTrial
object, and set it up."
self nextTrial: BatteryTrial new.
self nextTrial trialNumber: self currentTrialNumber + 1.
self currentTrialNumber < self trials
ifTrue:
[self selectNextStimulus.
self prepareNextStimulus]
runTrial
"This is our generic
trial body. Each trial first collects
some data,
then processes and stores
them."
self collectData.
self processData
startTrial
"We'll always start by
starting up the trial clock. In the
interest of tidy
timing, subclasses that
override this method might want to send super
startTrial first."
self trialClock startFor: self trialDuration
BatteryItem methods for: instance initialization
The methods in this category allocate battery item resources and set default values.
The methods in this category are all called in response to an initialize message to a BatteryItem. The initialize message zeros the block number instance variable, and sends an allocateResources message to the BatteryItem. This method then invokes the remainder of the resource allocation code. They are concerned primarily with the one-time creation or allocation of per-item resources.
Note that allocateParameters and allocateStimulusGenerator are responsibilities of the concrete subclasses of BatteryItem. None of the other initialization messages in this incarnation of the battery are overridden.
BatteryItem methods for: instance initialization
allocateBuffers
"Allocate the buffer
our A/D system will use. The A/D system
fills a
pre-existing buffer rather
than creating and returning every time it is
asked to collect data. The buffer: message stores this buffer
somewhere
until it is needed."
self buffer: (WaveformCollection channels: self channels points: self points)
allocateClocks
"We will use two
timers, a trial timer, and a stimulus presentation timer.
We allocate and store these
here."
self trialClock: Clock new.
self stimulusClock: Clock new
allocateDigitizer
"Let's use a buffered
digitizer for now. (There is a
StreamedDigitizer as
well.) Store this somewhere where we can get at
it."
self digitizer: BufferedDigitizer new
allocateParameters
"Let our subclasses
deal with this..."
self subclassResponsibility
allocateResources
"Create the resources
we will be employing to conduct this experiment,
such as parameter blocks,
clocks, buffers, digitizers, stimulus generators,
sequence generators, and
response devices."
self allocateParameters.
self allocateClocks.
self allocateBuffers.
self allocateDigitizer.
self allocateStimulusGenerator.
self allocateSequenceGenerator.
self allocateResponseDevice
allocateResponseDevice
"Build a button box and
save it somewhere."
self responseDevice:
(ButtonBox bitA: self inputBitA bitB: self inputBitB usingClock: self stimulusClock)
allocateSequenceGenerator
"Allocate a sequence
generator."
self sequenceGenerator: SequenceGenerator new
allocateStimulusGenerator
"Let our subclasses
worry about this..."
self subclassResponsibility
initialize
"Create our block
resources, and zero the block counter."
self blockNumber: 0.
self allocateResources
BatteryItem methods for: data collection
Data collection
strategies will usually differ sufficiently among different battery items so as
to make it necessary for this code to be implemented further down in the
inheritance hierarchy.
The collectData method must be implemented by each subclass of BatteryItem to orchestrate the basic data collection sequence for that item. This method will usually contain a sequence of experimental control and data acquisition method invocations that will together define the heart of the experiment implemented by a given battery item.
The startDigitizer method will often (always given the current framework) be called from collectData to start the A/D system.
BatteryItem methods for: data collection
collectData
"Data collection
requirements differ from item to item, so our subclasses
must implement this."
self subclassResponsibility
startDigitizer
"Start our A/D
converter. We ask ourself for the
various parameters we
need to start this
process. We care not from where these
values actually
come... "
self digitizer
collectChannels: self channels
points: self points
in: self buffer
every: self digitizingRate
thenDo: []
BatteryItem methods for: data processing
This protocol category contains a set of generic methods for setting up and processing collected data.
The prepareProcessingFixtures method prepares the data averaging and statistical data accumulation pipelines. The averaging fixture set up method, prepareAverageFixtures should be provided by subclasses of BatteryItem. Each subclass will typically call average:using:withLabel: for each average category it wishes to define.
The average:using:withLabel: method constructs a dataflow pipeline that evaluates a given source block after the data for each trial are collected, and forces the result of that block down a pipeline using the indicated valve (conditional) block. If this valve is open, this result can then flow into the Averager object for the pipeline. The pipeline is constructed using the pseudo-Stream objects defined in the system category Plumbing-Supplies.
The average:using:withLabel: method works as follows: First, an average buffer into which a running average will be accumulated is created. Next, this buffer (the WaveformCollection) is entered into the average dictionary in the current TrialBlock object using the given label as a key. Next, a Pump object is created, and a ValueSupply object using the given source block as its source is connected to the input (source) side of the Pump (using the << or connect input message). Then, the output side of the dataflow pipeline is connected (using >>) to the effluent (sink) side of the pump. The first stage of the output pipeline is the Valve object, and the final stage is an Averager on the average buffer we created. Finally, the fixture is entered into an averagePipeline dictionary kept by this instance of BatteryItem.
The tally:using:withLabel: method uses a similar scheme to construct pipelines for incrementally accumulating statistical information using Tally objects. Tally objects are objects into which pipelined data can be fed that monitor the data that pass through them and accumulate statistical information about those data. Tally objects can later be asked for statistical characterizations of the data that have passed through them. (For instance, they can be asked to give a count, or an average, of the data they have seen.) The nature of the data collected will of course depend on the nature of the Tally object itself. The pipelined scheme used here allows the nature of strategies for accumulating and processing statistical data to be implemented independently of the contexts in which they are employed.
The prepareStatsFixtures method constructs
a set of fixtures for collecting BatteryItem button box response time
data. First, the tally pipeline and
tally collection dictionaries are created.
The tally pipeline dictionary holds the root of each pipeline we have
built (i.e. its Pump object), so that when the time is right, each pipeline can
be told to move data from its ValueSupply block down (Value permitting) to its
effluent sink (the Tally object). The
TrialBlock's tally dictionary provides access to the Tally objects at the end
of the pipelines for posterity's sake.
The pipeline fixtures and tally objects are entered into these
dictionaries by tally:using:withLabel:. The real work in the prepareStatsFixtures method is performed by calls to tally:using:withLabel:.
For all three of the pipelines constructed by this method, the values are generated using a block that asks the current trial for its response time. The pipelines differ in the labels given them, and in the condition under which the pipeline's Valve object will permit the data returned as a result of evaluating the value block to be passed through to the Tally object. The first pipeline checks only that the value passed it is not nil. Each of the other two adds at test for whether one or the other of the response buttons was pressed in addition to the test for nil. Note that the block passed through to the ValueSuppy takes no arguments, while the block passed to the Valve object takes one argument. When a Valve object is passed a value, it will pass this value in turn to its test block as the block's argument.
The purpose of all of this is to simplify the process of adding new statistical categories to data collection programs. Using the pipelining scheme, all one would have to do to add a new statistical category is make a single additional call to tally:using:withLabel: to define the new category. All of the statistical characterizations of which the Tally objects are capable will then be available. It is interesting to contrast the relative ease of this process with the tangle of additional variables, declarations and code that is typically generated when an experimenter desires that new statistical categories be added to a program using a conventional programming language, such as Fortran. The pipeline approach encapsulates the details of the statistical collection process, such as the temporary variables, the category criteria, etc.
The processData method is called from runTrial. It outlines our generic mechanism for dealing with collected data. First, the subject's response, (if any) is evaluated. Next, the data for this trial are stored. Then the per-block averages and statistics are updated. Finally, the waveform and experimenter log displays are refreshed.
The system transcript is used as an experimenter log in this implementation of the battery. Single channel number one of the (single trial) data buffer is displayed in the BatteryItem's WaveformView.
The updateAverages and updateStatistics methods illustrate how data are extracted from the ValueSupply blocks of the plumbing fixtures and are flushed down into the effluent sides of the pipelines. Each pump object in the average and tally pipeline dictionaries is sent the message nextPut to accomplish this. The nextPut method moves one value from the intake to the effluent side of a Pump.
BatteryItem methods for: data processing
average: sourceBlock using: valveBlock withLabel: label
"Build a fixture
dictionary entry."
| fixture averageBuffer |
averageBuffer ¬ WaveformCollection channels: self channels points: self points.
self block averages at: label put: averageBuffer.
fixture ¬ Pump new.
fixture << (ValueSupply using: sourceBlock).
fixture >> (Valve using: valveBlock) >> (Averager on: averageBuffer).
self averagePipeline at: label put: fixture
prepareAverageFixtures
"Let our subclasses
deal with this..."
self subclassResponsibility
prepareProcessingFixtures
"Prepare the tally and
average pipelines."
self prepareAverageFixtures; prepareStatsFixtures
prepareStatsFixtures
"Prepare the tally
pipelines."
self tallyPipeline: (Dictionary new: 10).
self block tallies: (Dictionary new: 10).
self
tally: [self currentTrial responseTime]
using: [:rt | self currentTrial responseTime ~~ nil]
withLabel: 'RT Statistics (All trials)'.
self
tally: [self currentTrial responseTime]
using: [:rt | self currentTrial responseTime ~~ nil and:
[self currentTrial responseType = #buttonA]]
withLabel: 'RT Statistics (Button A)'.
self
tally: [self currentTrial responseTime]
using: [:rt | self currentTrial responseTime ~~ nil and:
[self currentTrial responseType = #buttonB]]
withLabel: 'RT Statistics (Button B)'
processData
"Data collection is
complete. We must now evaluate the
digitized data
and the subject's response
(if any), and update the experimenter's
waveform and log
displays."
self evaluateResponse.
self storeCurrentTrial.
self updateAverages.
self updateStatistics.
self updateWaveformDisplays.
self updateExperimentalLog
tally: sourceBlock using: valveBlock withLabel: label
"Build a fixture
dictionary entry."
| fixture tally |
tally ¬ Tally new.
self block tallies at: label put: tally.
fixture ¬ Pump new.
fixture << (ValueSupply using: sourceBlock).
fixture >> (Valve using: valveBlock) >> tally.
self tallyPipeline at: label put: fixture
updateAverages
"Give our pipeline
pumps a little push..."
self averagePipeline do: [:fixture | fixture nextPut]
updateExperimentalLog
"Write a message for
each trial. We use the System Transcript
as the log
medium. "
Transcript show: self trialNumber printString , ') '.
Transcript show: 'Stim: ' , self stimulus category printString , ' '.
Transcript show: 'Resp: ' , self responseType printString , ' '.
Transcript show: self responseTime printString , ' ticks'; cr.
Transcript endEntry
updateStatistics
"Give our pipeline
pumps a little push."
self tallyPipeline do: [:fixture | fixture nextPut]
updateWaveformDisplays
"Show a single trial
channel for now. FLECS battery items
typically
show several single trial
and average channels. First, ask our
parameter's object for the
battery browser object we are to use.
Then, ask
it for its
WaveformView. Change the model for this
view to our first
channel (an arbitrary
choice, I admit) and change the view title to
indicate the current trial
number. Then, send this view a update:
#waveform message"
| aBrowser aWaveformView |
aBrowser ¬ self parameters browser.
aWaveformView ¬ aBrowser waveformView.
aWaveformView model: (self buffer at: 1).
aWaveformView title: 'Trial ' , self trialNumber printString.
aWaveformView update: #waveform
BatteryItem methods for: data management
The methods in this category implement a generic data management scheme.
This implementation of the battery uses a class variable common to all instances of BatteryItem to store a global data dictionary. The dataDictionary method returns this dictionary.
Every time a battery item collects a block of data, it enters a DataDictionaryEntry object containing the TrialBlock object into the data dictionary under a name generated by the entryName method. The entryLabel method provides additional descriptive information. This label is stored in the DataDictionaryEntry object as well.
The prepareDataManagement method allocates a new BatteryBlock object, and fills it with a copy of the BatteryItem's own parameter object, and an empty trial array. It then creates a WriteStream over the trial array, and installs this as the BatteryItem's output stream. To the rest of the BatteryItem code, outputStream is merely a sink of some sort down which single trial objects can flushed.
The storeCurrentTrial method copies data from the digitizing buffer into the current trial object, and sends this object down our outputStream.
The finishDataManagment method is charged with creating the DataDictionaryEntry for a completed block, and storing this entry in the master data dictionary.
BatteryItem methods for: data management
dataDictionary
"Return our data
dictionary. This is a class variable
shared by all
BatteryItems into which the
result of individual battery runs are
entered. "
^BatteryDataDictionary
entryLabel
"Just use the item
label. Each entry we make into the
BatteryDataDictionary will
have an entryLabel and an entryName."
^self label
entryName
"Return the name
parameter with the block number appended.
Each
entry we make into the
BatteryDataDictionary will have an entryLabel
and an entryName."
^self name , ' #' , self blockNumber printString
finishDataManagement
"If we got this far,
we'll presume that it's okay to enter our data in
the data dictionary. We do so here. Note that we could just as easily be
closing a data file or a data stream, should
an alternate data
management strategy require
this. (Note that from the perspective of
the
rest of the BatteryItem code,
the data management scheme appears as one
in which single trial
objects are sent down a WriteStream-like sink.)"
| anEntry aName |
aName ¬ self entryName.
anEntry ¬ DataDictionaryEntry
entryNamed: aName
withLabel: self entryLabel
andData: self block.
BatteryDataDictionary at: aName put: anEntry
prepareDataManagement
"Allocate a block data
object, and set up a stream over it. To
the bulk of
our code, the data
management scheme will appear as one that requires
single trials to be sent
down some sort of outputStream. The code
will
care not whether this is a
file, a collection, or what have you..."
| trialArray |
block ¬ BatteryBlock new.
block parameters: self parameters shallowCopy.
trialArray ¬ Array new: self trials.
block singleTrials: trialArray.
self outputStream: (WriteStream on: trialArray)
storeCurrentTrial
"Copy the digitized
data, and set the current trial on its way down
whatever it is we are using
as an outputStream..."
self currentTrial data: self buffer deepCopy.
self outputStream nextPut: currentTrial
BatteryItem methods for: stimulus control
The battery simulation uses discrete stimulus presentation objects to generate stimulus sequences and present stimuli. The stimulus control protocol coordinates the various objects involved in stimulus generation.
The prepareStimulusSequence method sets up a stream from which individual stimuli can be read for each trial. It does this by first creating a sequence (using the sequenceGenerator) using a given collection of stimulus template objects, a trial count, and a collection of relative stimulus probabilities. We then set up a SampledStream over this sequence so that we may draw from it in a random order without replacement. The sequence generation scheme is interesting in that it is constructed using parts (WeightedCollections and SampledStreams) that were much more specific to this application domain earlier in the design process. The current design broke out these two potentially general components and employs the Unix philosophy of constructing special purpose components by composing a handful of general building blocks rather than building a single special purpose component from scratch.
The selectNextStimulus method merely copies the next stimulus from the stimulus stream into the current trial object. Note that we make a deepCopy of the stimulus object rather than passing a reference to it. This is because the stimulus sequence uses multiple references (via the WeightedCollection) rather than copies, and because there is no telling whether a subsequent analysis application might elect to make some destructive reference to one of these objects. (The issue of when to copy an object or when to pass a reference to it is one that arises frequently in languages like Smalltalk and Lisp. [Bobrow 86] describes an approach to this sort of problem-based on a lazy copy-upon-destructive-reference-scheme.)
The turnOnTheStimulus and turnOffTheStimulus methods pass the current stimulus object to this BatteryItem's stimulus generator.
BatteryItem methods for: stimulus control
prepareNextStimulus
"Tell our stimulus
generator to prepare the stimulus."
stimulusGenerator prepare: self nextTrial stimulus
prepareStimulusSequence
"Prepare a sequence,
and set up a stream over it."
| seq stims count probs |
stims ¬ self stimuli.
count ¬ self trials.
probs ¬ self probabilities.
seq ¬ self sequenceGenerator
generateSequenceOn: stims
withSize: count
andProbabilities: probs.
self stimulusSequence: (SampledStream on: seq)
selectNextStimulus
"Pull the next stimulus
from our stimulus sequence, and tell ourself
what it is."
self nextTrial stimulus: self stimulusSequence next deepCopy
turnOffTheStimulus
"Tell our stimulus
generator to turn off the stimulus."
self stimulusGenerator turnOff: self stimulus
turnOnTheStimulus
"Tell our stimulus
generator to turn on the stimulus."
self stimulusGenerator turnOn: self stimulus
BatteryItem methods for: response control
The battery simulation uses discrete objects for response reporting. The response control protocol coordinates these.
The response control protocol for battery items consists of two methods. The enableResponseReporting method passes an enable method on to the response device. The evaluateResponse method queries the response device, and loads the fields of the returned response into the current trial object. Note that these values are stored using message sends to self, so that a different data management scheme might easily be substituted.
BatteryItem methods for: response control
enableResponseReporting
"Turn on the response
device."
self responseDevice enable
evaluateResponse
"Ask the response
device for information pertaining to the current
response, and store
it."
| resp |
resp ¬ self responseDevice response.
self responseType: resp responseButton.
self responseTime: resp responseTime
BatteryItem methods for: parameter access
These methods translate queries to the battery item into queries to the BatteryItem's BatteryParameter object. Consult the Battery-Parameters category description for more information on these objects.
The use of message sends to self insulates the bulk of the code in BatteryItem from changes in the design of the data structures (which have indeed occurred during the evolution of this project).
BatteryItem methods for: parameter access
baseline
^self parameters baseline
channels
^self parameters channels
digitizingRate
^self parameters digitizingRate
inputBitA
^self parameters inputBitA
inputBitB
^self parameters inputBitB
label
^self parameters label
name
^self parameters name
outputBitA
^self parameters outputBitA
outputBitB
^self parameters outputBitB
points
^self parameters points
probabilities
^self parameters probabilities
responseMax
^self parameters responseMax
stimuli
^self parameters stimuli
stimulusDuration
^self parameters stimulusDuration
trialDuration
^self parameters trialDuration
trials
^self parameters trials
BatteryItem methods for: single trial access
These access methods translate sends to self into references to the current single trial data object. Consult the definition of class BatteryTrial in the Data-Management system category for more information.
The use of message sends to self helps to encapsulate the bulk of the battery item framework from the details of our single trial data management strategy. This strategy, like the parameter and stimulus management strategies, has evolved considerably since the start of this project.
BatteryItem methods for: single trial access
data
^self currentTrial data
data: anObject
^self currentTrial data: anObject
responseTime
^self currentTrial responseTime
responseTime: time
^self currentTrial responseTime: time
responseType
^self currentTrial responseType
responseType: type
^self currentTrial responseType: type
stimulus
^self currentTrial stimulus
stimulus: stim
^self currentTrial stimulus: stim
trialNumber
^self currentTrial trialNumber
trialNumber: n
^self currentTrial trialNumber: n
BatteryItem methods for: accessing
The methods in this category provide explicit read/write access to all of BatteryItem's instance variables.
Perhaps the most interesting thing about this protocol category is that it was generated automatically, using
the accessingSubclass: instanceVariableNames: classVariableNames: poolDictionaries: category: method found on page 289 of the Smalltalk Blue book [Goldberg 83].
BatteryItem methods for: accessing
averagePipeline
^averagePipeline
averagePipeline: argument
averagePipeline ¬ argument.
^argument
block
^block
block: argument
block ¬ argument.
^argument
blockNumber
^blockNumber
blockNumber: argument
blockNumber ¬ argument.
^argument
buffer
^buffer
buffer: argument
buffer ¬ argument.
^argument
currentTrial
^currentTrial
currentTrial: argument
currentTrial ¬ argument.
^argument
currentTrialNumber
^currentTrialNumber
currentTrialNumber: argument
currentTrialNumber ¬ argument.
^argument
digitizer
^digitizer
digitizer: argument
digitizer ¬ argument.
^argument
experimenterLog
^experimenterLog
experimenterLog: argument
experimenterLog ¬ argument.
^argument
nextTrial
^nextTrial
nextTrial: argument
nextTrial ¬ argument.
^argument
outputStream
^outputStream
outputStream: argument
outputStream ¬ argument.
^argument
parameters
^parameters
parameters: argument
parameters ¬ argument.
^argument
responseDevice
^responseDevice
responseDevice: argument
responseDevice ¬ argument.
^argument
sequenceGenerator
^sequenceGenerator
sequenceGenerator: argument
sequenceGenerator ¬ argument.
^argument
stimulusClock
^stimulusClock
stimulusClock: argument
stimulusClock ¬ argument.
^argument
stimulusGenerator
^stimulusGenerator
stimulusGenerator: argument
stimulusGenerator ¬ argument.
^argument
stimulusSequence
^stimulusSequence
stimulusSequence: argument
stimulusSequence ¬ argument.
^argument
tallyPipeline
^tallyPipeline
tallyPipeline: argument
tallyPipeline ¬ argument.
^argument
trialClock
^trialClock
trialClock: argument
trialClock ¬ argument.
^argument
waveformDisplay
^waveformDisplay
waveformDisplay: argument
waveformDisplay ¬ argument.
^argument
BatteryItem class
class BatteryItem class
superclass AccessibleObject class
BatteryItem class methods for: instance creation
This BatteryItem class category overrides new in order to make sure that an initialize message is sent, and the BatteryDataDictionary is initialized. See BatteryBrowser class for an example of a use of the defaultItemList method.
BatteryItem class methods for: instance creation
defaultItemList
"Return a default item
list..."
| list |
list ¬ OrderedCollection new: 10.
list add: SternbergTask new.
list add: ToneOddball new.
list add: WordOddball new.
^list asArray
new
"Create a new battery
item, send it an initialize message, and return
the result..."
| item |
BatteryDataDictionary isNil ifTrue: [self initialize].
item ¬ super new.
item initialize.
^item
BatteryItem class methods for: instance initialization
The initialize method fcreates a new BatteryDataDictionary. Thehe standard fileOut format [Krasner 83] generates a <Class> initialize message send for any metaclass that implements an initialize method.
BatteryItem class methods for: instance initialization
initialize
BatteryDataDictionary ¬ DataDictionary new: 100
BatteryItem class methods for: category management
This protocol category has served as a repository for a collection of methods I have been using to manage the Smalltalk code for this project. One can make a case that from the standpoint of design aesthetics, much of this code should reside elsewhere. Nevertheless, here it is. It is presented in the hope that some of it might prove of utility to someone entrusted with maintaining a large collection of classes such the one presented here.
The backUpEverything method first files out all the categories in the list returned by the batteryCategories method as separate <Category-Name>.st files using the fileOutCategories: method. It then files out these same categories into a single file named 'Project-Sources.st' using the fileOutCategories:on: method. Then it files out the changes set [Goldberg 85] to 'Project-Changes.st' using the fileOutChangesOn: method.
The fileOutBundledCategories:on: method creates a Unix shar archive file that can create a set of individual system category *.st files on a Unix system. (Use the Bourne shell). The comment at the beginning of this method givens an example of how one might use the matchSystemCategories: method.
The print out messages printOutBundledCategories:on:, printOutCategories and printOutCategories:on: are similar in function to the analogous file out messages, except that they create Unix troff ready output files. These may be processed along with a macro file named tmac.st using the command: rditroff -ms <name>. (The tmac.st file should be in the default directory when rditroff is run.) The Smalltalk code to generate troff ready output was derived from code from the Berkeley Smalltalk distribution kit [Krasner 83].
BatteryItem class methods for: category management
backUpEverything
"Update all our back up
files..." "BatteryItem
backUpEverything"
BatteryItem fileOutCategories: BatteryItem batteryCategories.
BatteryItem
fileOutCategories: BatteryItem batteryCategories
on: (FileStream fileNamed: 'Project-Sources.st').
BatteryItem
fileOutChangesOn: (FileStream fileNamed: 'Project-Changes.st')
batteryCategories
"Return all the battery
project categories..."
^#('Battery-Items' 'Battery-Parameters' 'Stimulus-Generators' 'Stimulus-Support' 'Sequence-Support' 'Response-Support' 'Data-Management' 'Realtime-Support' 'Realtime-Devices' 'Waveform-Support' 'Random-Support' 'Plumbing-Support' 'Linear-Algebra' 'Source-Hacking' 'Accessible-Objects' 'Interface-Battery' 'Interface-Protocol' )
fileOutBundledCategories: categories on: aFileStream
"BatteryItem
fileOutBundledCategories: (BatteryItem matchSystemCategories: 'Int*') on:
(FileStream fileNamed:
'shar.Interface-Stuff.st')"
| file line |
categories do:
[:category |
Transcript cr; show: '--> ' , category.
file ¬ 'troff.' , category.
file ¬ file copyReplaceAll: ' ' with: '_'.
line ¬ 'echo "--> "' , file.
aFileStream nextPutAll: line; cr.
line ¬ 'cat > ' , file , ' << ''!!Hehehehe...!!'''.
aFileStream nextPutAll: line; cr.
SystemOrganization fileOutCategory: category on: aFileStream.
aFileStream cr; nextPutAll: '!!Hehehehe...!!'; cr].
aFileStream shorten; close
fileOutCategories: categories
"BatteryItem
fileOutCategories: BatteryItem batteryCategories"
categories do:
[:category |
Transcript cr; show: '--> ' , category.
SystemOrganization fileOutCategory: category]
fileOutCategories: categories on: aFileStream
"BatteryItem
fileOutCategories: BatteryItem
batteryCategories on:
(FileStream fileNamed:
'Project-Sources.st')"
categories do:
[:category |
Transcript cr; show: '--> ' , category.
SystemOrganization fileOutCategory: category on: aFileStream.
aFileStream cr].
aFileStream shorten; close
fileOutChangesOn: aFileStream
"BatteryItem
fileOutChangesOn: (FileStream fileNamed: 'Project-Changes.st')"
"Remove old DoIt code,
and file out changes..."
Smalltalk allBehaviorsDo:
[:class | class removeSelector: #DoIt; removeSelector: #DoItIn:].
aFileStream fileOutChanges
fileOutComments: categories on: aFileStream
"BatteryItem
fileOutComments: BatteryItem
batteryCategories on:
(FileStream fileNamed:
'Project-Sources.comments')"
categories do:
[:category |
Transcript cr; show: '--> ' , category.
SystemOrganization fileOutComments: category on: aFileStream.
aFileStream cr].
aFileStream shorten; close
matchSystemCategories: pattern
"Return a collection of
category names that matches the given pattern..."
^SystemOrganization categories select: [:category | pattern match: category]
printOutBundledCategories: categories on: aFileStream
"BatteryItem
printOutBundledCategories: (BatteryItem matchSystemCategories: 'Int*') on:
(FileStream fileNamed:
'shar.troff.Interface-Stuff')"
| file line |
categories do:
[:category |
file ¬ 'troff.' , category.
file ¬ file copyReplaceAll: ' ' with: '_'.
line ¬ 'echo "--> "' , file.
aFileStream nextPutAll: line; cr.
line ¬ 'cat > ' , file , ' << ''!!Hehehehe...!!'''.
aFileStream nextPutAll: line; cr.
Object printOutStartUp: aFileStream.
Object printOutTimeStamp: aFileStream.
SystemOrganization printOutCategory: category on: aFileStream.
aFileStream cr; nextPutAll: '!!Hehehehe...!!'; cr].
aFileStream shorten; close
printOutCategories: categories
"Print out a list of
categories..."
"BatteryItem
printOutCategories: BatteryItem batteryCategories"
"BatteryItem
printOutCategories: (BatteryItem matchSystemCategories: 'Int*')"
categories do: [:category | SystemOrganization printOutCategory: category]
printOutCategories: categories on: aFileStream
"BatteryItem printOutCategories:
BatteryItem batteryCategories on: (FileStream
fileNamed:
'troff.Project-Sources')"
Object printOutStartUp: aFileStream.
Object printOutTimeStamp: aFileStream.
categories do:
[:category |
SystemOrganization printOutCategory: category on: aFileStream.
aFileStream cr].
aFileStream shorten; close
"Brian Foote 9/28/86 Added start up and time stamp calls..."
BatteryItem initialize
SternbergTask
Class SternbergTask implements a visual Sternberg memory task [Sternberg 69] on top of the ERP/oddball framework present in BatteryItem. The variation on the Sternberg paradigm presented here involves the presentation to a subject, for each trial, of a set of characters or digits called a memory set. The subject is instructed to remember the characters in the memory set. After the memory set is removed from the screen, the subject is presented with another character called a probe. The subject's task is to indicated whether the probe belongs to the memory set. A trial in which the probe was in fact drawn from the memory set is a positive trial. One in which the probe was not drawn from the memory set is anegative trial.
class name SternbergTask
superclass BatteryItem
instance variable names
items
trialClock
stimulusClock
digitizer
responseDevice
parameters
stimulusSequence
sequenceGenerator
stimulusGenerator
buffer
experimenterLog
waveformDisplay
block
blockNumber
nextTrial
currentTrial
currentTrialNumber
outputStream
averagePipeline
tallyPipeline
class variable names
BatteryDataDictionary
category Battery-Items
hierarchy
Object
AccessibleObject
BatteryItem
SternbergTask
ToneOddball
WordOddball
SternbergTask methods for: instance initialization
The allocateParameters and allocateStimulusGenerator methods implement item specific parameter and stimulus generator resource allocation code.
SternbergTask methods for: instance initialization
allocateParameters
"Allocate our parameter
object."
self parameters: SternbergTaskParameters new
allocateStimulusGenerator
"Build our stimulus
generator."
self stimulusGenerator: SternbergDisplayGenerator new
SternbergTask methods for: data collection
The collectData method defines the experimental control and data acquisition strategy for the Sternberg task battery item. The remainder of the methods in this protocol category are inherited from BatteryItem. First, the A/D system is started. Then, the memory set is prepared, and trial clock is instructed to wait until a baseline period (relative to the start of the trial) has elapsed. The memory set is then presented for an appropriate amount of time, and turned off.
The pattern for presenting the actual stimulus (i.e. the probe) is similar. First, a probe is prepared. The program then waits until the time at which the probe is to be presented arrives. Then, a timer is started so that there will be a timebase relative to the stimulus onset. Next, the probe is turned on, and the response reporting system is enabled. When the stimulus duration has elapsed, the stimulus is turned off.
When all the data have been collected, a check is made to see whether additional time is necessary for subject response data collection.
SternbergTask methods for: data collection
collectData
"Do Sternberg data
collection."
"First, start the A/D converter..."
self startDigitizer.
"Now, prepare the memory set, and present it..."
self prepareTheMemorySet.
self trialClock waitUntil: self memorySetBaseline.
self turnOnTheMemorySet.
self trialClock waitUntil: self memorySetBaseline + self memorySetDuration.
self turnOffTheMemorySet.
"Next, start the stimulus clock and turn on the stimulus.
Enable response reporting too. Then wait until the stimulus
duration has elapsed, and turn off the stimulus..."
self prepareTheProbe.
self trialClock waitUntil: self probeBaseline.
self stimulusClock startFor: self trialDuration - self probeBaseline.
self turnOnTheProbe.
self enableResponseReporting.
self stimulusClock waitUntil: self stimulusDuration.
self turnOffTheProbe.
"Finally, wait for the digitizer to finish.
if the RT wait time is greater than the digitizing time,
wait that out..."
self digitizer wait.
self stimulusClock waitUntil: self responseMax
SternbergTask methods for: stimulus control
The methods in this protocol category embody a substantial proportion of the code that makes the SternbergTask battery item distinct.
The prepareNextStimulus method selects both a memory set and a probe for the next trial. The way in which the memory set is selected shows a novel application of SampledStreams. The memory set should be a set of a given size drawn at random (without replacement) from a given vocabulary of characters.
This set is constructed as follows: First, a SampledStream is created over the BatteryItem's vocabulary string. Then an empty output string (the memory set) is created, and a WriteStream is defined over it (memorySetStream). The memory set is constructed by moving the appropriate number of characters from the input (sampled) stream to the output stream. The underlying collection of this output stream is the memory set, which can then be used.
The probe is selected and prepared as follows: If a positive trial is called for, a single character is selected at random, using a RandomStream (sample with replacement) over the memory set. If a negative probe is required, the next character in the vocabulary stream that was used to create the memory set, from which the memory set has already been drawn, is selected as the stimulus character.
The prepareTheMemorySet method simply forwards the current stimulus descriptor to the stimulus generator. The remaining methods in this protocol category similarly forward stimulus manipulation requests to the stimulus generator.
SternbergTask methods for: stimulus control
prepareNextStimulus
"Select a memory set
and a probe."
|vocabularyStream memorySet memorySetStream |
vocabularyStream ¬ SampledStream on: self vocabulary.
memorySet ¬ String new: self memorySetSize.
memorySetStream ¬ WriteStream on: memorySet.
self memorySetSize timesRepeat: [memorySetStream nextPut: vocabularyStream next].
self nextTrial stimulus memorySet: memorySet.
self nextTrial stimulus category = #positive
ifTrue: [ self nextTrial stimulus probe:
(RandomStream on: memorySet) next asSymbol asString ]
ifFalse: [self nextTrial stimulus probe:
vocabularyStream next asSymbol asString]
prepareTheMemorySet
"Tell our stimulus
generator to get the memory set ready."
self stimulusGenerator prepareMemorySet: self currentTrial stimulus
prepareTheProbe
"Tell our stimulus
generator to get the memory set ready."
self stimulusGenerator prepareProbe: self currentTrial stimulus
turnOffTheMemorySet
"Tell our stimulus
generator to turn off the memory set."
self stimulusGenerator turnOffMemorySet: self currentTrial stimulus
turnOffTheProbe
"Tell our stimulus
generator to turn off the probe."
self stimulusGenerator turnOffProbe: self currentTrial stimulus
turnOnTheMemorySet
"Tell our stimulus
generator to turn on the memory set."
self stimulusGenerator turnOnMemorySet: self currentTrial stimulus
turnOnTheProbe
"Tell our stimulus
generator to turn on the probe."
self stimulusGenerator turnOnProbe: self currentTrial stimulus
SternbergTask methods for: parameter access
The methods in this category add BatteryItem access methods for the fields that the SternbergTaskParameters object adds to the BatteryParameters object.
SternbergTask methods for: parameter access
memorySetBaseline
^self parameters memorySetBaseline
memorySetDuration
^self parameters memorySetDuration
memorySetSize
^self parameters memorySetSize
probeBaseline
^self parameters probeBaseline
probeDuration
^self parameters probeDuration
vocabulary
^self parameters vocabulary
SternbergTask methods for: data processing
Each concrete battery item provides its own prepareAverageFixtures method. This is necessary to take into account item specific stimulus characteristics and labeling requirements. A detailed discussion of the mechanics of fixture preparation can be found in the discussion of this protocol category given for class BatteryItem.
SternbergTask methods for: data processing
prepareAverageFixtures
"Prepare the average
pipelines."
self averagePipeline: (Dictionary new: 10).
self block averages: (Dictionary new: 10).
self
average: [self currentTrial data]
using: [:data | true]
withLabel: 'Average (All trials)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #positive]
withLabel: 'Average (Positive)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #negative]
withLabel: 'Average (Negative)'
SternbergTask class
class SternbergTask class
superclass BatteryItem class
SternbergTask class methods for: examples
The example1 method shows how one might execute a sample block of trials. Note that before a block can be run, the Timebase object must have been initialized. In practice, battery blocks will usually be run by pressing the run button after having selected an item in the battery browser.
SternbergTask class methods for: examples
example1
"SternbergTask
example1"
Timebase initialize.
SternbergTask new doBlock
ToneOddball
The ToneOddball class implements a simple auditory oddball experiment [Duncan-Johnson 77]. In this experiment, a subject is presented with one of two tones. The subject can be instructed to count one of the tones, and ignore the other. The subject can also be told to press a button in response to one or both stimulus categories. A frequent manipulation is to vary the relative frequency of the two stimulus categories.
class name ToneOddball
superclass BatteryItem
instance variable names
items
trialClock
stimulusClock
digitizer
responseDevice
parameters
stimulusSequence
sequenceGenerator
stimulusGenerator
buffer
experimenterLog
waveformDisplay
block
blockNumber
nextTrial
currentTrial
currentTrialNumber
outputStream
averagePipeline
tallyPipeline
class variable names
BatteryDataDictionary
pool dictionaries
category Battery-Items
hierarchy
Object
AccessibleObject
BatteryItem
SternbergTask
ToneOddball
WordOddball
ToneOddball methods for: instance initialization
The allocateParameters and allocateStimulusGenerator methods implement item specific parameter and stimulus generator resource allocation code.
ToneOddball methods for: instance initialization
allocateParameters
"Allocate our parameter
object."
self parameters: ToneOddballParameters new
allocateStimulusGenerator
"Build our stimulus
generator."
| bitA bitB |
bitA ¬ self outputBitA.
bitB ¬ self outputBitB.
self stimulusGenerator: (ToneGenerator bitA: bitA bitB: bitB)
ToneOddball methods for: parameter access
ToneOddball methods for: parameter access
outputBitA
^self parameters outputBitA
outputBitB
^self parameters outputBitB
ToneOddball methods for: data collection
The collectData method specifies the experimental control and data acquisition scheme used for this item. First, the digitizer is started. Then the method waits for a baseline period, relative to the start of this trial, to elapse. Next, the stimulus timer is started. Immediately thereafter, the stimulus is turned on and response reporting is enabled. When the stimulus presentation period has elapsed, the stimulus is turned off. The method then waits until data collection is complete, and the response window time has elapsed.
ToneOddball methods for: data collection
collectData
"Do tone oddball data
collection."
"First, start the A/D converter, and wait until the baseline
elapses..."
self startDigitizer.
self trialClock waitUntil: self baseline.
"Next, start the stimulus clock and turn on the stimulus.
Enable response reporting too.
Then wait until the stimulus
duration has elapsed, and turn off the stimulus..."
self stimulusClock startFor: self trialDuration - self baseline.
self turnOnTheStimulus.
self enableResponseReporting.
self stimulusClock waitUntil: self stimulusDuration.
self turnOffTheStimulus.
"Finally, wait for the digitizer to finish.
If the RT wait time is greater than the digitizing time,
wait that out..."
self digitizer wait.
self stimulusClock waitUntil: self responseMax
ToneOddball methods for: data processing
Each concrete battery item provides its own prepareAverageFixtures method. This is necessary to take into account item specific stimulus characteristics and labeling requirements. A detailed discussion of the mechanics of fixture preparation can be found in the discussion of this protocol category given for class BatteryItem.
ToneOddball methods for: data processing
prepareAverageFixtures
"Prepare the average
pipelines."
self averagePipeline: (Dictionary new: 10).
self block averages: (Dictionary new: 10).
self
average: [self currentTrial data]
using: [:data | true]
withLabel: 'Average (All trials)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #catA]
withLabel: 'Average (Category A)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #catB]
withLabel: 'Average (Category B)'
ToneOddball class
class ToneOddball class
superclass BatteryItem class
ToneOddball class methods for: examples
The example1 method shows how one might execute a sample block of trials. Note that before a block can be run, the Timebase object must have been initialized. In practice, battery blocks will usually be run by pressing the run button after having selected an item in the battery browser.
ToneOddball class methods for: examples
example1
"ToneOddball
example1"
Timebase initialize.
ToneOddball new doBlock
WordOddball
The WordOddball class implements a visual semantic oddball task [Donchin 81]. In this experiment, a subject is presented with a random word drawn from one of two categories. The subject must count or respond as with the ToneOddball.
It is instructive to point out that to implement a concrete BatteryItem, it is necessary for an experiment to implement just four methods: allocateParameters, allocateStimulusGenerator, collectData, and prepareAverageFixtures. To fully exploit the battery framework, an experimenter might also need to create new subclasses of BatteryParameter, StimulusGenerator, and Stimulus hierarchies.
class name WordOddball
superclass BatteryItem
instance variable names
items
trialClock
stimulusClock
digitizer
responseDevice
parameters
stimulusSequence
sequenceGenerator
stimulusGenerator
buffer
experimenterLog
waveformDisplay
block
blockNumber
nextTrial
currentTrial
currentTrialNumber
outputStream
averagePipeline
tallyPipeline
class variable names
BatteryDataDictionary
category Battery-Items
hierarchy
Object
AccessibleObject
BatteryItem
SternbergTask
ToneOddball
WordOddball
WordOddball methods for: instance initialization
Subclasses of BatteryItem are required to provide implementations of two standard resource allocation messages: allocateParameters, and allocateStimulusGenerator. (Both are defined as being subclass responsibilities in BatteryItem.) The allocateParameters method simply ensures that a parameter object belonging to the correct class is allocated. The allocateStimulusGenerator message's task is potentially more complicated. It must ensure that an instance of some sort of StimulusGenerator appropriate to the type of battery item being constructed is created and initialized.
The battery simulation has attempted to separate battery items and (simulated) stimulus generation equipment so that stimulus generators might be mixed and matched among several different battery items. Frequently, the major differences among a collection of experimental designs will be in sorts of stimuli they employ. By separating the stimulus generation code from the rest of the experimental code, the reusability of both should be enhanced.
The stimulus
generation and response reporting hierarchies are examples of places in this
battery simulation where tasks handled in the original CPL battery by
(simulated) inheritance are now handled by discrete components. The evolution of inheritance frameworks into
component frameworks is discussed in Chapter VI.
The allocateParameters and allocateStimulusGenerator methods implement item specific parameter and stimulus generator resource allocation code. Of interest here is the scheme we use in allocateStimulusGenerator for implementing our random word lists. A dictionary of streams is created which is keyed by the two stimulus categories. Each stream is a RandomStream over a vocabulary array. Since RandomStreams sample with replacement, they will provide, each time they are asked, a random word from their respective underlying vocabularies. This dictionary, once constructed, is given to a newly created WordGenerator (a subclass of StimulusGenerator).
WordOddball methods for: instance initialization
allocateParameters
"Allocate our parameter
object."
self parameters: WordOddballParameters new
allocateStimulusGenerator
"Build our stimulus
generator."
| streamDictionary animals vegetables |
streamDictionary ¬ Dictionary new: 2.
animals ¬ #(dog cat mouse ).
vegetables ¬ #(carrot potato tomato ).
streamDictionary at: #animal put: (RandomStream on: animals).
streamDictionary at: #vegetable put: (RandomStream on: vegetables).
self stimulusGenerator: (WordGenerator on: streamDictionary)
WordOddball methods for: data collection
Each subclass of
BatteryItem is required to implement a collectData
method. This method conducts the
basic per-trial data collection and experimental control activity for each
battery item.
The collectData method specifies experimental control and data acquisition scheme used for this item. First, the digitizer is started. Then the method waits for a baseline period, relative to the start of this trial, to elapse. Then, the stimulus timer is started. Immediately thereafter, the method turns on the stimulus and enable response reporting. When the stimulus presentation period has elapsed, the stimulus is turned off. The method then waits until data collection is complete, and the response window time has elapsed.
The perceptive reader will note that the collectData method given here is identical to the one given in ToneOddball. I could have either defined an intermediate abstract calls (say, SimpleOddball) between BatteryItem and these two classes to take advantage of this. Alternately, I could have made this implementation of collectData the default in BatteryItem itself. That I did neither is in recognition of the fact that in a somewhat fuller battery simulation, detail that would distinguish a larger set of applications from one another would quickly emerge at this level, just as it does in the SternbergTask battery item.
WordOddball methods for: data collection
collectData
"Do word oddball data
collection."
"First, start the A/D converter, and wait until the baseline
elapses..."
self startDigitizer.
self trialClock waitUntil: self baseline.
"Next, start the stimulus clock and turn on the stimulus.
Enable response reporting too.
Then wait until the stimulus
duration has elapsed, and turn off the stimulus..."
self stimulusClock startFor: self trialDuration - self baseline.
self turnOnTheStimulus.
self enableResponseReporting.
self stimulusClock waitUntil: self stimulusDuration.
self turnOffTheStimulus.
"Finally, wait for the digitizer to finish.
If the RT wait time is greater than the digitizing time,
wait that out..."
self digitizer wait.
self stimulusClock waitUntil: self responseMax
WordOddball methods for: data processing
Each concrete battery item must provide its own prepareAverageFixtures method. This is necessary to take into account item specific stimulus characteristics and labeling requirements. A detailed discussion of the mechanics of fixture preparation can be found in the discussion of this protocol category given for class BatteryItem.
WordOddball methods for: data processing
prepareAverageFixtures
"Prepare the average
pipelines."
self averagePipeline: (Dictionary new: 10).
self block averages: (Dictionary new: 10).
self
average: [self currentTrial data]
using: [:data | true]
withLabel: 'Average (All trials)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #catA]
withLabel: 'Average (Category A)'.
self
average: [self currentTrial data]
using: [:data | self currentTrial stimulus category = #catB]
withLabel: 'Average (Category B)'
WordOddball class
class WordOddball class
superclass BatteryItem class
WordOddball class methods for: examples
The example1 method shows how one might execute a sample block of trials. Note that before a block can be run, the Timebase object must have been initialized. In practice, battery blocks will usually be run by pressing the run button after having selected an item in the battery browser.
WordOddball class methods for: examples
example1
"WordOddball
example1"
Timebase initialize.
WordOddball new doBlock
Battery-Parameters
The Battery-Parameters system category contains the following classes:
BatteryParameters Houses common BatteryItem parameters
SternbergTaskParameters Adds SternbergTask specific parameters
ToneOddballParameters Adds ToneOddball specific parameters
WordOddballParameters Adds WordOddball specific parameters
These parameter objects are in effect records or dictionaries that contain those values that can be edited and modified by the experimenter. (In fact, since these objects are subclasses of AccessibleObject, they can be treated as records or dictionaries.)
Parameter objects are distinct objects rather than parts of the items themselves for several reasons. One is that, as separate objects, they can be copied and archived along with the collected data without recording the entire state of the item itself. This distinction between the parameter objects and the item objects could become more significant should an external data management scheme be implemented. The second reason for keeping parameter objects separate is to distinguish values that should be presented to the user for alteration via the battery user interface from the battery item's other instance variables. Parameter objects might also serve as a components in a more general (and yet to be designed) user parameter management/interface scheme.
BatteryParameters
class name BatteryParameters
superclass AccessibleObject
instance variable names
items
baseline
channels
points
digitizingRate
responseMax
stimulusDuration
trialDuration
trials
writeDataFlag
probabilities
stimuli
inputBitA
inputBitB
name
label
category Battery-Parameters
hierarchy
Object
AccessibleObject
BatteryParameters
SternbergTaskParameters
ToneOddballParameters
WordOddballParameters
BatteryParameters methods for: instance initialization
These two methods establish default values for each basic parameter. Note that by default data are written to the data dictionary, and that the probability and stimuli fields are Arrays.
BatteryParameters methods for: instance initialization
establishDefaults
"Set out our
defaults..."
baseline ¬ 1.
channels ¬ 1.
points ¬ 10.
digitizingRate ¬ 1.
responseMax ¬ 9.
stimulusDuration ¬ 2.
trialDuration ¬ 15.
trials ¬ 10.
writeDataFlag ¬ True.
probabilities ¬ #(0.5 0.5).
stimuli ¬ #(catA catB)
initialize
"Set defaults and
return ourself..."
self establishDefaults.
^self
BatteryParameters methods for: accessing
These methods allow explicit read/write access to all of this object's fields using the conventional record style protocol. (Note that since we are a subclass of AccessibleObject, all these messages would have functioned correctly anyway, albeit much less quickly.)
BatteryParameters methods for: accessing
baseline
^baseline
baseline: argument
baseline ¬ argument.
^argument
channels
^channels
channels: argument
channels ¬ argument.
^argument
digitizingRate
^digitizingRate
digitizingRate: argument
digitizingRate ¬ argument.
^argument
inputBitA
^inputBitA
inputBitA: argument
inputBitA ¬ argument.
^argument
inputBitB
^inputBitB
inputBitB: argument
inputBitB ¬ argument.
^argument
label
^label
label: argument
label ¬ argument.
^argument
name
^name
name: argument
name ¬ argument.
^argument
points
^points
points: argument
points ¬ argument.
^argument
probabilities
^probabilities
probabilities: argument
probabilities ¬ argument.
^argument
responseMax
^responseMax
responseMax: argument
responseMax ¬ argument.
^argument
stimuli
^stimuli
stimuli: argument
stimuli ¬ argument.
^argument
stimulusDuration
^stimulusDuration
stimulusDuration: argument
stimulusDuration ¬ argument.
^argument
trialDuration
^trialDuration
trialDuration: argument
trialDuration ¬ argument.
^argument
trials
^trials
trials: argument
trials ¬ argument.
^argument
writeDataFlag
^writeDataFlag
writeDataFlag: argument
writeDataFlag ¬ argument.
^argument
BatteryParameter class
class BatteryParameter class
superclass AccessibleObject class
BatteryParameters class methods for: instance creation
The new method is overridden here to enforce the convention that each new object must explicitly call an initialize method.
BatteryParameters class methods for: instance creation
new
"Create a new parameter
object and initialize it..."
^super new initialize
SternbergTaskParameters
class name SternbergTaskParameters
superclass BatteryParameters
instance variable names
items
baseline
channels
points
digitizingRate
responseMax
stimulusDuration
trialDuration
trials
writeDataFlag
probabilities
stimuli
inputBitA
inputBitB
name
label
memorySetBaseline
memorySetDuration
probeBaseline
probeDuration
memorySetSize
vocabulary
class variable names
pool dictionaries
category Battery-Parameters
hierarchy
Object
AccessibleObject
BatteryParameters
SternbergTaskParameters
ToneOddballParameters
WordOddballParameters
SternbergTaskParameters methods for: instance initialization
The establishDefaults method overrides the one given in BatteryParameters so that values can be established for item specific parameters. Note that its first official act is to initialize the common parameters via a call to its superclass.
SternbergTaskParameters methods for: instance initialization
establishDefaults
"Do Sternberg
defaults... "
| positive negative |
super establishDefaults.
memorySetBaseline ¬ 1.
memorySetDuration ¬ 1.
memorySetSize ¬ 3.
vocabulary ¬ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
probeBaseline ¬ 4.
probeDuration ¬ 1.
positive ¬ SternbergStimulus new.
positive category: #positive.
negative ¬ SternbergStimulus new.
negative category: #negative.
stimuli ¬ Array with: positive with: negative.
name ¬ 'Sternberg/ERP Task'.
label ¬ 'Sternberg/ERP Experiment'
SternbergTaskParameters methods for: accessing
Allow explicit read/write access to all of this object's fields using the conventional record style protocol. (Note that since SternbergTaskParameters is a subclass of AccessibleObject, all these messages would have functioned correctly anyway, albeit much less quickly.)
SternbergTaskParameters methods for: accessing
memorySetBaseline
^memorySetBaseline
memorySetBaseline: argument
memorySetBaseline ¬ argument.
^argument
memorySetDuration
^memorySetDuration
memorySetDuration: argument
memorySetDuration ¬ argument.
^argument
memorySetSize
^memorySetSize
memorySetSize: argument
memorySetSize ¬ argument.
^argument
probeBaseline
^probeBaseline
probeBaseline: argument
probeBaseline ¬ argument.
^argument
probeDuration
^probeDuration
probeDuration: argument
probeDuration ¬ argument.
^argument
vocabulary
^vocabulary
vocabulary: argument
vocabulary ¬ argument.
^argument
ToneOddballParameters
class name ToneOddballParameters
superclass BatteryParameters
instance variable names
items
baseline
channels
points
digitizingRate
responseMax
stimulusDuration
trialDuration
trials
writeDataFlag
probabilities
stimuli
inputBitA
inputBitB
name
label
outputBitA
outputBitB
class variable names
pool dictionaries
category Battery-Parameters
hierarchy
Object
AccessibleObject
BatteryParameters
SternbergTaskParameters
ToneOddballParameters
WordOddballParameters
ToneOddballParameters methods for: instance initialization
The establishDefaults method overrides the one given in BatteryParameters so that values can be established for of this object's item specific parameters. Note that its first official act is to initialize the common parameters via a call to its superclass.
ToneOddballParameters methods for: instance initialization
establishDefaults
"Do ToneOddball
defaults... "
| toneA toneB |
super establishDefaults.
toneA ¬ ToneStimulus new.
toneA primary: #toneA ; category: #catA.
toneB ¬ ToneStimulus new.
toneB primary: #toneB ; category: #catB.
stimuli ¬ Array with: toneA with: toneB.
outputBitA ¬ 0.
outputBitB ¬ 1.
name ¬ 'Tone Oddball'.
label ¬ 'Auditory Oddball Experiment'
ToneOddballParameters methods for: accessing
Allow explicit read/write access to all of this object's fields using the conventional record style protocol. (Note that since ToneOddballParameters is a subclass of AccessibleObject, all these messages would have functioned correctly anyway, albeit much less quickly.)
ToneOddballParameters methods for: accessing
outputBitA
^outputBitA
outputBitA: argument
outputBitA ¬ argument.
^argument
outputBitB
^outputBitB
outputBitB: argument
outputBitB ¬ argument.
^argument
WordOddballParameters
class name WordOddballParameters
superclass BatteryParameters
instance variable names
items
baseline
channels
points
digitizingRate
responseMax
stimulusDuration
trialDuration
trials
writeDataFlag
probabilities
stimuli
inputBitA
inputBitB
name
label
class variable names
pool dictionaries
category Battery-Parameters
hierarchy
Object
AccessibleObject
BatteryParameters
SternbergTaskParameters
ToneOddballParameters
WordOddballParameters
WordOddballParameters methods for: instance initialization
The establishDefaults method overrides the
one given in BatteryParameters so that values can be established for this
object's item specific parameters.
WordOddballParameters methods for: instance initialization
establishDefaults
"Do WordOddball
defaults... "
| animal vegetable |
super establishDefaults.
animal ¬ WordStimulus new.
animal primary: #animal ; category: #catA.
vegetable ¬ WordStimulus new.
vegetable primary: #vegetable ; category: #catB.
stimuli ¬ Array with: animal with: vegetable.
name ¬ 'Word Oddball'.
label ¬ 'Animal/vegetable Oddball Experiment'
Stimulus-Generators
These classes simulate the hardware and software one would normally use to generate experimental stimuli. These classes might provide a basis for a mix-and-match stimulus generation capability in a fuller battery simulation.
Stimulus generator objects are designed to be self-contained embodiments of specific stimulus generation schemes. Since these schemes themselves vary with experimental designs as well as with the basic hardware and software characteristics of the stimulus generators, the external protocols for these objects vary as well. Note that though the stimulus generator classes defined here exactly mirror the BatteryItem hierarchy, this might not necessarily be the case in a fuller simulation.
The StimulusGenerator hierarchy shows how a portion of the battery simulation that was formerly embedded in the BatteryItem hierarchy has, under Smalltalk, evolved into a separate part of the overall battery framework. The reuse of pluggable, black-box components is, all other things being equal, preferable to reuse via inheritance. The identification of components that may be factored out of an inheritance hierarchy in this fashion is usually possible only after several iterations through the design process. (This issue is discussed in detail in Chapter VI.)
Indeed, the StimulusGenerator hierarchy is in some respects still in transition between membership in the original battery hierarchy and clean isolation from it. The degree of coupling seen in the interface between each battery item and these classes is still somewhat high.
One very intriguing route by which this coupling might be reduced is via the introduction of parallelism. Under such an approach, stimulus generators would run in parallel with battery items, with both tied to the same underlying timebase. Such an approach would simplify both the battery item stimulus coordination code, and the stimulus sequencing code in this stimulus generators. Hence, the use of parallelism could greatly increase the generality and reusability of components of both hierarchies.
Realtime application programs frequently exhibit a structure in which the mainline code must explicitly schedule sequences of otherwise unrelated events. The use of parallel processes can free the designer from this temporal yoke.
The classes in this category are:
StimulusGenerator Abstract StimulusGenerator superclass
SternbergDisplayGenerator The SternbergTask stimulus generator
ToneGenerator The ToneOddball stimulus generator
WordGenerator The WordOddball stimulus generator
StimulusGenerator
The StimulusGenerator class is an abstract superclass for a family of stimulus generation objects. It defines a common, default protocol for stimulus generators.
class name StimulusGenerator
superclass Object
instance variable names
class variable names
pool dictionaries
category Stimulus-Generators
hierarchy
Object
StimulusGenerator
SternbergDisplayGenerator
ToneGenerator
WordGenerator
StimulusGenerator methods for: instance initialization
StimulusGenerator methods for: instance initialization
initialize
"Provide nothing as our
default behavior..."
^self
StimulusGenerator methods for: stimulus control
This protocol defines a default stimulus control protocol to which most clients of any subclass of StimulusGenerator will subscribe. Note that not all subclasses of StimulusGenerator will override all these methods. In these cases, the default action (given below) will be to do nothing. Some subclasses of StimulusGenerator will need to provide a superset of the protocol given below for stimulus control. Such classes will add additional methods to this protocol.
StimulusGenerator methods for: stimulus control
finishBlock
"Default shall be to do
nothing..."
prepare: aStimulus
"Default shall be to do
nothing..."
startBlock
"Default shall be to do
nothing..."
turnOff: aStimulus
"Default shall be to do
nothing..."
turnOn: aStimulus
"Default shall be to do
nothing..."
StimulusGenerator class
class StimulusGenerator class
superclass Object class
StimulusGenerator class methods for: instance creation
StimulusGenerator class methods for: instance creation
new
"Return an initialized
instance..."
^super new initialize
SternbergDisplayGenerator
The SternbergDisplayGenerator class simulates the hardware and software that one would employ to generate the stimuli for a Sternberg task.
class name SternbergDisplayGenerator
superclass StimulusGenerator
instance variable names
class variable names
pool dictionaries
category Stimulus-Generators
hierarchy
Object
StimulusGenerator
SternbergDisplayGenerator
ToneGenerator
WordGenerator
SternbergDisplayGenerator methods for: stimulus control
Since the stimulus control scheme for a Sternberg task is different from the generic oddball scheme outlined in StimulusGenerator, we override the default (do nothing) behaviors for the default StimulusGenerator protocol using shouldNotImplement. The display device itself is simulated by writing the stimulus to the Smalltalk system transcript. Other messages in the stimulus generation protocol simply do nothing what so ever in this simulation.
This class is probably misplaced in the StimulusGenerator hierarchy. The fact that it in effect removes nearly the entire StimulusGenerator protocol is evidence that this hierarchy needs reorganization. This class is an example of a stimulus generation scheme that uses multiple stimulus events. In the CPL Battery, two items exhibited this structure. The other items exhibited the simpler structure that is reflected in the standard protocol for stimulus generation given in StimulusGenerator.
I deferred a major reorganization of this hierarchy in recognition of the potential a process-based organization might bring to the battery framework.
SternbergDisplayGenerator methods for: stimulus control
prepare: aStimulus
"We distinguish between
probes and memory sets, so we don't do this..."
self shouldNotImplement
prepareMemorySet: aStimulus
"Do nothing here for
now..."
prepareProbe: aStimulus
"Do nothing here for
now..."
turnOff: aStimulus
"We distinguish between
probes and memory sets, so we don't do this..."
self shouldNotImplement
turnOffMemorySet: aStimulus
"Do nothing here for
now..."
turnOffProbe: aStimulus
"Do nothing here for
now..."
turnOn: aStimulus
"We distinguish between
probes and memory sets, so we don't do this..."
self shouldNotImplement
turnOnMemorySet: aStimulus
"Just write the memory
set to the transcript for now..."
Transcript show: '--> Memory Set: ' , aStimulus memorySet; cr; endEntry
turnOnProbe: aStimulus
"Just write the probe
to the transcript for now..."
Transcript show: '--> Probe: ' , aStimulus probe; cr; endEntry
ToneGenerator
The ToneGenerator class simulates a digital output driven two channel tone generator. Tones are generated by sending set and clear messages to instances of OutputBit.
class name ToneGenerator
superclass StimulusGenerator
instance variable names
bitA
bitB
class variable names
pool dictionaries
category Stimulus-Generators
hierarchy
Object
StimulusGenerator
SternbergDisplayGenerator
ToneGenerator
WordGenerator
ToneGenerator methods for: accessing
These methods provide read/write access to the instance variables that house the instances of OutputBit that activate the (simulated) tone generators.
ToneGenerator methods for: accessing
bitA
"Return the output bit
for the first tone..."
^bitA
bitA: aBit
"Set the output bit for
the first tone..."
^bitA ¬ aBit
bitB
"Return the output bit
for the second tone..."
^bitB
bitB: aBit
"Set the output bit for
the second tone..."
^bitB ¬ aBit
ToneGenerator methods for: stimulus control
The methods in this category simulate a digital output bit driven tone generation scheme.
When a block is begun, the startBlock method sends clear messages to both the output bits to ensure that they are off. The same thing occurs at the end of a block in finishBlock.
The turnOn: and turnOff: methods both accept a stimulus descriptor, and check its primary stimulus field. If this field is #toneA, the output bit A is affected, and if it is #toneB, output bit B is affected.
ToneGenerator methods for: stimulus control
finishBlock
"Make sure both our
bits are off..."
bitA clear.
bitB clear
startBlock
"Make sure both our
bits are off..."
bitA clear.
bitB clear
turnOff: aStimulus
"Turn off the bit
indicated as the primary stimulus by the Stimulus object
that was passed to
us..."
| primary |
primary ¬ aStimulus primary.
primary == #toneA ifTrue: [bitA clear. ^self].
primary == #toneB ifTrue: [bitB clear. ^self].
self error: 'Bad stimulus...'
turnOn: aStimulus
"Turn on the bit
indicated as the primary stimulus by the Stimulus object
that was passed to
us..."
| primary |
primary ¬ aStimulus primary.
primary == #toneA ifTrue: [bitA set. ^self].
primary == #toneB ifTrue: [bitB set. ^self].
self error: 'Bad stimulus...'
ToneGenerator class
class ToneGenerator class
superclass StimulusGenerator class
ToneGenerator class methods for: instance creation
The bitA:bitB: message provides a shorthand for creating a ToneGenerator and assigning it a pair of output bits. (Perhaps new should be overridden to make this form of initialization mandatory.)
ToneGenerator class methods for: instance creation
bitA: a bitB: b
"Create a tone
generator, and set up the given bits..."
| gen |
gen ¬ super new.
gen initialize.
gen bitA: (OutputBit onBit: a).
gen bitB: (OutputBit onBit: b).
^gen
WordGenerator
The WordGenerator class simulates the hardware and software one might employ for presenting word stimuli in a word oddball task. It is provided, upon creation, with a dictionary of stream objects that provide it with the word stimuli themselves. This dictionary is keyed using the primary field of the stimulus descriptor.
class name WordGenerator
superclass StimulusGenerator
instance variable names
streamDictionary
class variable names
pool dictionaries
category Stimulus-Generators
hierarchy
Object
StimulusGenerator
SternbergDisplayGenerator