Is that some kind of a game you are playing?
So far in this book we’ve covered the basics of programming in Python, reviewed some essential electronics, and explored the tip of the iceberg of control systems theory. We’ve covered a lot, to be sure, but there is still one major topic left before we take on the challenges of actually connecting a computer to an instrument or a control system and turning it loose: simulation.
In engineering, simulation can be applied to many things, from a simple device to an entire complex system. In electronics engineering, circuit simulations are used to explore and analyze analog and digital designs well before an IC is fabricated or a soldering iron comes into play. Systems engineers build complex simulations of industrial systems to evaluate various control strategies and process flow models long before the pipes are laid out and the conveyors are installed. Military and commercial pilots are trained in realistic aircraft simulators where procedures and techniques can be learned and practiced with no risk to an actual vehicle or the people in it (or on the ground).
The primary objective of this chapter is to equip you with extensible simulation tools that you can reuse in other projects later, as well as an understanding of when and where simulation is useful, and where it is not. To this end, we’ll examine a couple of complete simulators written entirely in Python. We’ll wrap up by looking at some ways to leverage commonly available (and free) software tools to create other simulators.
The first example we will consider is a simulation of a generic multifunction device with both analog and discrete I/O. The second simulator example is an eight-channel AC power controller. Although the simulators in this chapter will touch on topics such as data I/O, data capture, and user interfaces, we will defer in-depth discussions of those topics to later chapters. Chapter 11 will examine data I/O in detail, and in Chapter 12 we will look at some ways to load and save data using files. In Chapter 13 we’ll explore user interfaces in more detail, including the TkInter and wxPython GUIs. It is my hope that you will take the initiative to return to these simulators and extend their usefulness with the knowledge you gain later.
If you’ve ever played a video game, you’ve used a simulator. One of the first commercially successful video games, Atari’s Pong, was a simulation (albeit crude by today’s standards) of a ping-pong game. In fact, all video games are simulations of something—what they simulate might not actually exist in the real world, but they’re still simulations. By the same token, a simulation of a control system that doesn’t really exist allows us to try out different novel ideas, invent worst-case scenarios to evaluate system behavior, and explore various behavioral models, all without risking any hardware or jeopardizing personal safety.
The key concept of simulation is that all simulators are based on a model of some sort. Models can be simple, or they can be complex. A model may be event- or time-series-based (automated handling of luggage at a large airport), purely mathematical (optical performance of lenses and mirrors), or some combination of these and other factors. One way to think of the core model in a simulation is as a dynamic virtual system. If, for example, you have a high-fidelity simulator for some type of chemical processing system, there is, in essence, a virtual chemical processing system in the simulation software that will exhibit as many of the responses and characteristics of the real thing as the fidelity of the simulation will allow. A truly high-fidelity simulation might even produce a simulated chemical product.
Figure 10-1 shows how a simulation corresponds to its real-world counterpart. The instrumentation system (which would typically be what we are developing or testing in this book) uses a simulated interface to interact with a model. The model allows us to observe and analyze how the instrumentation software will behave when connected to a system (in this case, a virtual system). We can implement a crude simulation model with basic behavior, or we could build something that has a very high degree of fidelity. We can also inject simulated faults into the model and examine the response of the instrumentation system.
When implementing data acquisition and control systems, simulations of the devices connected to the control PC can be used to speed up the development process and provide a safe environment to test out ideas. Simulation can also provide some invaluable, and otherwise unattainable, insights into the behavior of the instrumentation software and the device or system being simulated. Whether it’s implemented because the instrumentation hardware just isn’t available yet or because the target system hardware is too valuable to risk damaging, a simulation is a good way to get the software running, test it, and have a high degree of confidence that it will work correctly in the real world.
When talking about simulation, one of the first considerations to come up is the issue of fidelity. The fidelity of a simulation defines how accurately it will model a real system. The cost and effort of implementing a simulation can rise significantly with each increase in the level of fidelity, so you’ll have to decide when it’s good enough and resist the temptation to polish it up too much.
A common error made when attempting to write a simulator for the first time is to throw everything into it. Even seasoned pros with access to lots of real data from a real system don’t usually do this. There are too many unknowns at the outset. Subtle behavioral interactions can be surprising and might never have been seen in a real system, and assumptions made about the more opaque parts of a system may contain all sorts of traps and pitfalls.
This is why simulations are typically built up incrementally. First, the software interface, or API, is defined. A generic interface may use different names for the API functions, but it should accept and return data that looks as much like real API data as possible. In other words, the simulation should support the basic algorithmic functionality of the control or instrumentation system you are building in terms of the data that needs to pass through the interface. Once the interface simulation is at a point where it can support initial testing and development, simulation fidelity can be improved as necessary.
That being said, sometimes a low-fidelity simulator consisting of just an API will work fine. For example, if all you really want to do is verify that a proportional control function is working correctly, a simple data source to drive the simulation and some way to capture and save the output may be all that’s necessary.
In addition to simulating the functionality of a working system, a simulation may also need to have the ability to simulate an error. In other words, it should be able to simulate being broken in some predefined way.
If you’ve done a failure analysis, as outlined in Chapter 8, you should have an idea of what types of errors might occur and how they will cause the system to fail. The ability to simulate those errors allows you to see how well your software will deal with them.
If you do want to include the ability to do fault injection in a simulation, a preliminary failure analysis is an essential first step.
In general terms, there are two primary classes of faults (not including bugs that may be lurking in the instrumentation code itself) that come into play: interface faults and system faults. The line between these two classes of faults isn’t always distinct, but in a simulated environment it’s usually possible to treat them as separate classes. This, in turn, makes it much easier to clearly distinguish cause and effect without the complexity and messiness of dealing with real physical components.
An interface fault results in an error that, as the name implies, occurs in the API layer between the system under development or in test, and the system simulation behind it. These types of faults might manifest as communications errors, such as corrupt data or no response. In other words, an interface fault occurs in between the instrumentation software and the simulated system. In the real system, errors in the interface might arise as a result of broken wires, corroded connections, a defective electronic component, or spurious noise.
Simulating interface faults typically involves things like disabling a communications channel or injecting random data into a channel. You can also simulate a fault in a piece of bus interface hardware (e.g., a plug-in I/O card), although this falls into that gray area I alluded to earlier and may be better dealt with as a system fault.
Figure 10-2 shows a diagram of an interface simulation with basic fault injection capabilities. Notice that the TxD (transmit) output from the instrumentation software to the instrument simulator doesn’t have a disable capability. This isn’t an oversight. If the instrument employs a command-response type of control interface, disabling the TxD link between it and the instrumentation software doesn’t really accomplish much. The instrument (or simulated instrument) should just sit and wait for a command, and it won’t have any way to detect whether the link is broken or not.
The path from the instrument to the instrumentation software is another matter. In this case, the instrumentation software should be able to detect when it has not received a response from the instrument and react in a predefined manner.
A reasonable question to ask at this point would be: why not just simulate the disabled communications in the instrument simulator, instead of using an interface simulation? The reason is that the instrument, or control system, may interact with some other component in the system that the instrumentation software can sense even if the primary link to the instrument is disabled. In other words, clever instrumentation software can sometimes get verification from another source (e.g., sensing a power state change) indicating that a correct command action has occurred, and thereby determine that the interface is the probable cause of the problem.
Figure 10-2 also indicates that both the TxD and RxD (receive) channels have noise injection capabilities. In the context of a communications channel that handles streams of ASCII characters, this could involve the injection of random characters to simulate line noise. Anyone who has ever had to use a modem to communicate with a remote computer system has mostly likely seen examples of line noise when the modem connects to or disconnects from the remote host.
Instrumentation software that is able to deal with corrupted data will most likely have the ability to retry an operation, or at least issue a query to determine whether a command was accepted correctly. It is also possible to determine where the noise occurred by examining the response from the instrument. If the instrument complains that it received an invalid command, the noise occurred on the TxD channel from the instrumentation software. If the response from the instrument is garbage, the noise is most likely in the RxD channel to the instrumentation software.
A system fault is something that arises from within the external system, or perhaps one of its subsystems (depending on the complexity of the system). The ability to simulate various system faults increases the value of the simulation, allowing you to exercise and verify the fault response of the instrumentation software while interfaced with the simulation.
A system fault can be any number of things, but in a simulator it usually comes down to a value in a table or the return code from a function. In other words, if the simulator would normally return an expected value in response to a particular input, simulating a fault is simply a matter of including some logic to return an invalid value or an error indication instead.
One approach is to directly inject the fault at the return point of a function or method in the simulation, as shown in Figure 10-3.
Assuming that ret_val
in
Figure 10-3 holds the nominal
value that the function or method would normally return, setting
simerr
to True
will result in the error return value
(err_val
) being passed back to the
caller instead. The error value could be set via an accessor method,
loaded from a fault table, or even entered manually from a user
interface of some sort.
Another approach is to establish a fault condition using a module global variable or an object variable, as shown in Figure 10-4.
In Figure 10-4,
the class definition for SimObject
has been designed to specifically expose key variables used by the
methods in the class. In a simplistic scenario these would be
read-only variables, set once at object instantiation and not modified
by the class methods. It is possible to implement a lockout capability
to prevent methods from modifying shared object variables, but that
usually entails a level of complexity that’s not always
necessary.
If you’re experienced with unit testing techniques, you might be thinking that all this sounds rather familiar, and you would be correct (we also discussed this in Chapter 8). In unit testing, a primary objective is to exercise all possible paths of execution to achieve complete statement coverage. If the unit under test has paths that can be executed only when an error occurs, the unit test environment must be able to set variables or coerce inputs to stimulate the appropriate error responses. A simulation that has fault injection capabilities does much the same thing, although for a somewhat different reason.
As a test environment, a simulator can be used to create fault scenarios that would otherwise be difficult (or even near impossible) to replicate with real hardware, at least not without the possibility of risking some serious (and expensive) physical damage. Simulation with fault injection allows you to observe the behavior of the control or data acquisition system you are building to see how it will respond to various error conditions and, if you have been following some requirements that define error responses, to determine whether it meets those requirements.
In this section we will examine two complete simulators written in Python: DevSim and the Simple Power Controller (SPC). Later, we’ll look at some other ways to achieve simulation using free and open source software. The sources for the simulators are located in the resource repository for this book, which can be accessed from the book’s web page. While these are fully functional simulators, they are also only examples, not production-grade tools. My hope is that you will use them as inspiration, or perhaps as starting points, to create your own simulator tools to meet your particular needs.
Python is well suited to creating simulators. It is easy to use, very flexible, and allows you to easily implement things such as plug-in modules. Working simulators can often be gotten up and running very quickly, and once in place they can be readily extended and revised as necessary. In addition, Python is capable of some impressive math tricks when add-on libraries such as SciPy or NumPy are installed, and as we’ll see shortly, generating graphical output is not that difficult.
On the other hand, Python is not as fast as code written in C or C++. That’s just the nature of the language and its underlying interpreter. If you need high-speed data generation and fast responses, you should probably consider another approach. Fortunately, most instrumentation applications have rather long time constants to begin with, so speed is usually not an issue.
The simulators we’ll examine in this chapter are DevSim and
SPC. Each can reside in its own package (subdirectory). This is how I’ve
arranged them, with SPC residing in the ACSim
directory. DevSim imports the FileUtils
module that we’ll see in Chapter 12 for reading and writing
ASCII data files. It also imports a module called RetCodes
, which contains a set of pseudoconstants for return code
values. It is intended to be a read-only shared file. Typically the
FileUtils
and RetCodes
modules would reside in a separate
package called SimLib
, as shown in Figure 10-5, along with any other modules that your simulators and
utilities might need to share.
If you have not already done so, now would be a good time to pause and download the source code for this book from its website. There’s more there than what you’ll see here, and it’s all already neatly organized with installation instructions.
Notice that the top-level package, PySims
, has a file called __init__.py
. This establishes PySims
as the “anchor” of the package
hierarchy; it may contain a docstring, package initialization code, or
nothing at all. When we look at gnuplot later,
we’ll see how one developer elected to use __init__.py
to handle package
initialization.
First up is a rather substantial simulator that is intended to stand in for a bus-based multifunction I/O card. Right out of the box it can be used with the PID control code we saw in Chapter 9 (and I’ll show you how to do that in just a bit). It also has the ability to incorporate user-defined functions as part of the response data processing, which is useful for simulating things like mechanical inertia or applying a filter function.
Although the DevSim API may look daunting, it’s mainly just a collection of accessor methods to set simulation parameters and return the current values. In its basic form, it doesn’t actually simulate any particular external device or system. That functionality is added by you, in the form of user-definable functions or additional software to meet project-specific needs.
Rather than list it all here (it’s almost 1,000 lines of code), I’m going to describe its internal structure, list the various methods available, show some highlights, and discuss how the simulator is used. You should have the source listing handy for reference, if possible.
Internally, DevSim is the most complex thing we’ve seen so far, but keep in mind that it’s mostly just data routing. Figure 10-6 is an IC data-sheet-style logic diagram for DevSim.
Data is buffered as it moves though DevSim, and each activity is synchronized so as to occur in a lock-step fashion. DevSim is also multithreaded: four threads handle cyclic functions (waveform generation), another four manage file input, and a ninth thread handles the simulator’s primary sequencing in a main loop.
The DevSim
class’s __init__()
method presets the internal
parameters to default values, and then calls the internal method
__run()
as its final action. The
code for __run()
follows:
#----------------------------------------------------------------- # Simulator launch #----------------------------------------------------------------- def __run(self): """ Simulator start. Instantiates the main loop thread, sets up the cyclic and file I/O threads, and then waits for the start flag to go True. When the start flag becomes True the main loop thread is started. """ # input thread object lists cycThread = [None, None, None, None] fileThread = [None, None, None, None] simloop = threading.Thread(target=self.__simLoop) # create the cyclic and file input handling threads, # four of each for inch in range(0,4): cycThread[inch] = threading.Thread(target=self.__cyclic, args=[inch]) fileThread[inch] = threading.Thread(target=self.__fileData, args=[inch]) # start the cyclic and file I/O threads just created cycThread[inch].start() fileThread[inch].start() # wait for start signal then start the main thread wait_start = True while wait_start: if self.startSim == True: wait_start = False else: time.sleep(0.1) # wait 100 ms simloop.start() # start it up
First, the primary thread, simloop()
, is created. Next, the eight input
threads are instantiated in a pair of lists. As each thread is
created, it is subsequently started. The __run()
method then waits for a start
signal, and finally starts the simloop()
main thread.
The simulator is designed to start running when a Boolean
variable (startSim
) is set to
True
. You can set the various
parameters before enabling the simulator, or they can be set “on the
fly” while it is running. The simulation is stopped by setting the
value of the Boolean object variable stopSim
to True
.
The four cyclic threads and the four file input threads each
load data into a buffer when an “event” occurs. In the case of DevSim,
an event is simply a flag variable that is set and reset to control
activity; it is not a true event in the sense of a low-level message
such as one would find in a GUI. Figure 10-7 shows how the threads are
used to obtain input data and place it into buffers for eventual
routing to the outputs. Notice that the __cyclic()
threads can each select from one
of five possible inputs: DC (constant value), sine wave, pulse (square
wave), ramp, and sawtooth. The cyclic data is updated at a rate set by
the parameter accessor setCyclicRate()
, and each cyclic input may
have a unique rate value. The cyclic inputs are very useful for
generating a fixed DC-like value or periodic waveforms that the instrumentation software connected to the
simulator can read and process.
Using threads for the cyclic inputs allows them to run at different rates than the simulator’s main thread, and this in turns allows them to create waveform data at specific frequencies. If they weren’t implemented as threads they would generate changes in the simulated waveforms at the main loop rate of the simulator, which wouldn’t be suitable for a lot of situations. Allowing the cyclic data generators to run asynchronously is an example of timing decoupling. The buffers are necessary to capture the asynchronous data as it is generated.
The __fileData()
threads are
used to obtain data values from a file and pass them through to the
instrument software under test. This feature allows you to replay a
set of input data over and over again. It is useful when evaluating
the instrumentation software under test against a known set of input
values, to observe its behavior.
The main loop thread, shown next, is where all the data is collected and routed to the output buffers:
def __simLoop(self): """ Simulator main loop. The main loop continuously checks for input data (either from an external caller or from a cyclic source) for each input channel. If data is available it starts the processing chain that will ultimately result in the data appearing in the output buffers. The main loop runs forever as a thread. All externally supplied data is buffered, and all output data is buffered. When running in cyclic mode the output buffers will be overwritten with new data as it becomes available. """ while self.stopSim != True: # scan through all four input channels, get available data for ichan in range(0, 4): if self.in_src[ichan] == DS.EXT_IN: indata = self.inbuffer[ichan] else: indata = __getCycData(ichan) # if a user-supplied function is defined, then apply # it to the data self.databuffer[ichan] = self.__doUserFunc(ichan, indata) # fetch file data (if configured to do so) self.__getFileData(ichan) # step through each output channel, move data as nesc for ochan in range(0,4): if (self.out_src[ochan] >= DS.INCHAN1) and (self.out_src[ochan] <= DS.INCHAN4): outdata = self.databuffer[ochan] else: outdata = self.filebuffer[ochan] randdata = self.__getRandom() oscaled = self.__scaleData(outdata, self.outscale[ochan]) rscaled = self.__scaleData(randdata, self.randscale[ochan]) self.outbuffer[ochan] = oscaled + rscaled self.outavail[ochan] = True time.sleep(self.simtime)
Reading through the code for the __simloop()
method is equivalent to
following the data flow through Figure 10-6 from left to right. The first step
is to get either externally supplied data (via the sendData()
method, described in the
following section), or from one of the cyclic data sources. Next the
data is passed to a user-defined function, if one exists. It is then
written to a buffer (databuffer
).
If the data input is a file, the data is read from the file and stored in another buffer. Note that although a method is called, this doesn’t mean that anything will actually happen. An input method might be quiescent and therefore skipped internally, depending on how the simulator is configured.
Once the input data is in one of the two input buffers, it is read out and passed to the output. Along the way random data is obtained (for noise simulation), and the data is optionally scaled before it is summed with scaled random data. Finally, the data ends up in the output buffers for each channel.
The DevSim
class contains 26 public methods and 15 internal
methods. Most of the public methods are devoted to setting or
retrieving internal parameter values. The public methods are described
in the following lists.
When used, the parameters inchan
and outchan
may be any channel number
from 0 to 3.
getCyclicLevel(
inchan
)
Returns a 2-tuple with either NO_ERR
and the current cyclic level of
the specified channel, or BAD_PARAM
and None
if
inchan
is out of range.
setCyclicLevel(
inchan
,
level
)
Sets the output value for a cyclic source in CYCNONE
mode for a specific input
channel. Returns NO_ERR
if
successful or BAD_PARAM
if
inchan
is out of range.
getCyclicOffset()
Returns the current cyclic offset value.
setCyclicOffset(
offset
)
Sets the current cyclic offset value for all cyclic data. The offset is the shift of the peak-to-peak range of the output relative to zero, otherwise known as the bias.
getCyclicRate(
inchan
)
Returns a 2-tuple with either NO_ERR
and the current cyclic rate of
the specified channel, or BAD_PARAM
and None
.
setCyclicRate(
inchan
,
rate
)
The parameter rate defines the cyclic rate in fractional
seconds for a specific input channel. Note that this is the
period of the cyclic data, not the frequency. The frequency is
the inverse of the rate. Returns NO_ERR
if successful or BAD_PARAM
if
inchan
is out of range.
getCyclicType(
inchan
)
Returns a 2-tuple with either NO_ERR
and the current cyclic wave
shape of the specified channel, or BAD_PARAM
and None
.
setCyclicType(
inchan
,
cyctype
)
Defines the output wave shape of a cyclic data source. The available wave shapes are sine, pulse, ramp, and sawtooth. A cyclic source may also be set to generate a constant output value, and the value may be changed at any time while the simulator is active. This, in effect, emulates a variable voltage source. Table 10-1 lists the five cyclic data types available.
Cyclic type | Data value | Description |
|
| Constant output level |
|
| Sine wave |
|
| 50% duty-cycle pulse (i.e., a square wave) |
|
| Ramp wave shape with leading slope |
|
| Sawtooth wave with symmetrical rise/fall |
Returns NO_ERR
if
successful or BAD_PARAM
if
cyctype
is invalid or
inchan
is out of range.
setDataFile(
infile
,
path
,
filename
,
recycle=True
)
Defines and opens a data file for input as a data source
specified by the infile
index. If
path
is not specified (empty string
or None
), the default path is
assumed to be the current working directory. The input file must
contain data in one of the four formats supported by the
ASCIIDataRead
class in the
module FileUtils
.
If the parameter recycle
is
True
, the file will be reset
to the start and reread when an EOF is encountered. The default
behavior is to recycle the data file. If a data source file is
already opened for a given input channel and this method is
called, the currently open file will be closed and the new file
will be opened.
Returns OPEN_ERR
if the
file open failed or BAD_PARAM
if infile
is invalid; otherwise,
returns NO_ERR
.
getDataScale(
outchan
)
Returns a 2-tuple with either NO_ERR
and the current data scaling
for a specific channel, or BAD_PARAM
and None
.
setDataScale(
outchan
,
scale
)
Sets the output channel’s input data scaling factor. Each
output channel may have an optional unique multiplicative
scaling factor applied. Returns NO_ERR
if successful or BAD_PARAM
if
inchan
is out of range.
getFunction(
inchan
)
Returns a 2-tuple with either NO_ERR
and the current function string
for the specified channel, or BAD_PARAM
and None
.
setFunction(
inchan
,
funcstr='
'
)
Applies a user-supplied function expression to the data stream of a specified input data channel using two predefined variables:
x0
Input data
x1
Previous (1/z) data
The function is a string. It may reference the x0
and x1
variables but may not contain an
equals sign (an assignment). The result is used as the data
input to the output channels. Passing None
or an empty string disables the
application of a function to the data. Returns NO_ERR
if successful or BAD_PARAM
if
inchan
is out of range.
getInputSrc(
inchan
)
Returns a 2-tuple with either NO_ERR
and the input source for a
specific channel, or BAD_PARAM
and None
.
setInputSrc(
inchan
,
source
)
Selects the data source for an input channel.
inchan
may be any valid input channel
number from 0
to 3
. The
source
parameter may be one of
EXT_IN
(default) or CYCLIC
. The input source may be
changed on the fly at any time. Returns NO_ERR
if successful or BAD_PARAM
if
inchan
is out of range or
source
is invalid.
getOutputDest(
outchan
)
Returns a 2-tuple with either NO_ERR
and the current output channel
data source, or BAD_PARAM
and
None
.
setOutputDest(
outchan
,
source
)
Selects the data source for an output channel.
outchan
may be any valid output
channel number between 0
and
3
. The
source
parameter may be one of
INCHAN1
, INCHAN2
, INCHAN3
, INCHAN4
, SRCFILE1
, SRCFILE2
, SRCFILE3
, or SRCFILE4
. Returns NO_ERR
if successful or BAD_PARAM
if
inchan
is out of range or
source
is invalid.
getRandScale(
outchan
)
Returns a 2-tuple with either NO_ERR
and the current random data
scaling multiplier, or BAD_PARAM
and None
.
setRandScale(
outchan
,
scale
)
Sets the output channel’s random data scaling factor. Each
output channel may have an optional multiplicative scaling
factor applied to the random data. If the scaling is set to
zero, no random values are summed into the data. Returns
NO_ERR
if successful or
BAD_PARAM
if
inchan
is out of range.
getSimTime()
Returns the current simulator cycle time.
setSimTime(
time
)
Sets the overall cycle time of the simulation. This is, in effect, the amount of time for which the main loop will be suspended between each loop iteration. The time is specified in fractional seconds. Returns nothing.
getTrigMode(
inchan
)
Returns a 2-tuple with either NO_ERR
and the current trigger mode
for the specified channel, or BAD_PARAM
and None
.
setTriggerMode(
inchan
,
mode
)
Sets the trigger mode for a particular channel. The
trigger mode may be one of NO_TRIG
(0
), EXT_TRIG
(1
), or INT_TRIG
(2
).
In NO_TRIG
mode, all
cyclic sources run continuously at the clock rate set by the
setCyclicClock()
method, and
data source file reads do not occur until an output channel is
accessed.
In EXT_TRIG
mode,
cyclic sources perform a single operation and file sources are
read once for each trigger occurrence.
In INT_TRIG
mode,
cyclic sources perform a single cycle and data source files are
read once each time an output channel is accessed.
Returns NO_ERR
if
successful or BAD_PARAM
if
inchan
is out of range or
mode
is invalid.
Simulator control and I/O methods:
genTrigger(
inchan
)
Generates a trigger event. Depending on the trigger mode,
a trigger event will result in one iteration of a cyclic data
source, or one record read from a data input file. Returns
NO_ERR
if successful or
BAD_PARAM
if
inchan
is out of range.
Input/output methods:
readData(
outchan
,
block=True
,
timeout=1.0
)
Returns the data available for the specified output channel from the output buffer. If blocking is enabled, this method will block the return to the caller until the data becomes available or the specified timeout period has elapsed.
Returns a 2-tuple consisting of the return code and the
data value from the output channel. Returns NO_ERR
if successful or BAD_PARAM
if
outchan
is out of range. If the
return code is anything other than NO_ERR
, the data value will be
zero.
sendData(
inchan
,
dataval
)
Writes caller-supplied data into the specified channel.
The data in the input buffer will be read on each cycle of the
simulator. Returns NO_ERR
if
successful or BAD_PARAM
if
inchan
is out of range.
For all its apparent complexity, the simulator really boils down
to just two main methods: readData()
and sendData()
. Everything else just sets the
stage for what will occur between the inputs and the outputs.
The following example code demonstrates the data flow from an input to an output in the simulator. It doesn’t use the optional user function, nor does it apply scaling or noise to the data:
#! /bin/python # TestDevSim1.py # # Echos data written into the simulator back to the output. # # Source code from the book "Real World Instrumentation with Python" # By J. M. Hughes, published by O'Reilly. from DevSim import DevSim import SimLib.RetCodes as RC import DevSimDefs as DS def testDevSim1(): simIO = DevSim.DevSim() # set up the simulated device simIO.setInputSrc(DS.INCHAN1, DS.EXT_IN) simIO.setOutputSrc(DS.OUTCHAN1, DS.INCHAN1) loopcount = 0 while loopcount < 10: simIO.sendData(DS.INCHAN1, (5.0 + loopcount)) print simIO.readData(DS.OUTCHAN1) loopcount += 1 simIO.stopSim = True # set the stop flag
Referring to the code and to Figure 10-6, the first points of interest are
the setInputSrc()
and setOutputSrc()
statements. In both method calls, the first parameter
identifies the input or output channel to use and the second parameter
specifies where the data will come from. The first call states that
the data for input channel 1 will be obtained from the channel 1
external input, which occurs when the sendData()
method is called. In other words,
it controls the “Input Channel Source Select” (the small two-input
trapezoid symbol in Figure 10-6). The
second method call (setOutputSrc()
)
determines where the data that will appear on output channel 1 will be
obtained. In this case, the selection parameter governs the behavior
of the eight-input source selector, and it specifies input channel 1.
Note that input channel 1 could mean either a direct input from an
external source, or one of the internal cyclic data sources. If we
follow the data path in Figure 10-6 we
see that it goes through the user function block in the input side,
and then through a scaling block and a summing junction for random
data (noise) on the output side. This example does not use any of
those features, so it’s effectively a straight connection between the
input and the output.
The second example uses a data file as the input to the simulator:
#! /bin/python # TestDevSim2.py # # Reads data from an input file and passes the data to the output. A # data file containing 10 values in ASCII form must exist in the # current directory. # # Source code from the book "Real World Instrumentation with Python" # By J. M. Hughes, published by O'Reilly. from DevSim import DevSim import SimLib.RetCodes as RC import DevSimDefs as DS def testDevSim2(): simIO = DevSim.DevSim() # define the source file to use rc = simIO.setDataFile(DS.SRCFILE1, None, "indata1.dat") if rc != RC.NO_ERR: print "Error opening input data file" else: # set up the simulated device simIO.setOutputSrc(DS.OUTCHAN1, DS.SRCFILE1) loopcount = 0 while loopcount < 10: # read and print data from file print simIO.readData(DS.OUTCHAN1) loopcount += 1 simIO.stopSim = True
The call to the method setDataFile()
defines the data file that will
be associated with the index SRCFILE1
. Note that setDataFile()
also opens the file, and will
return an error value of OPEN_ERR
if the open fails. In this simple example we’re not checking the value
of the error code, except to determine whether it’s anything other
than NO_ERR
. The call to setOutputSrc()
associates OUTCHAN1
with the data file. Whenever the
readData()
method is called, the file is
accessed and a single entry is read. The data is then passed through
the eight-input source selector (see Figure 10-6) to the output scaling and random
data summing stages, and finally appears on the output. Calling
readData()
is, in effect, a trigger
event for the internal data file read operation.
Since there is no scaling and no random data “noise” applied to the values from the input file, this is basically just a way to open and read a data file. However, it’s also a useful building block for creating a more complex simulation setup, as shown in Figure 10-8. Here, DevSim is used to read data from an input file, process the data by applying a user-defined function and scaling, and then drive some test code with the resulting stimulus. This is just one way that DevSim can be leveraged to provide enhanced test and simulation functionality.
DevSim incorporates the ability to insert a simple user-defined function
into the input data stream just after the input source selector. A
user-defined function is a string containing a valid Python statement.
It is processed internally using Python’s built-in eval()
method, and two predefined variables,
x0
and x1
, are provided. x0
is the current input data value, and
x1
is the last current data value
(a 1/z unit delay). You can actually do quite a
bit with just these two variables. The result of the statement
evaluation becomes the output to the eight-channel source selector. If
the function string is empty, it is ignored.
For example, the statement x0 *
2
will multiply the input data by 2; it’s a simple scaling
function. However, the statement (x0/(x0**2))+x1
is rather more interesting.
It is an exponential expression, and given a linear series of input
values (say, 0 to 10) it will generate an output that will produce a
graph like the one shown in Figure 10-9.
The main restrictions on what you can do with a user-defined function are as follows:
A user-defined function cannot refer to any variables other
than x0
and x1
.
The x0
and x1
variables are read-only (i.e.,
inputs).
A user-defined function cannot contain an assignment.
A user-defined function must be a single statement; multiple statements and conditionals are not allowed.
Even with these restrictions there is quite a lot that can be accomplished with user-defined functions, including the simulation of physical characteristics such as inertial lag or a simple digital filter.
The following example shows how a user-defined function is used:
#! /bin/python # TestDevSim3.py # # Demonstrates the use of a user-defined function string. # # Source code from the book "Real World Instrumentation with Python" # By J. M. Hughes, published by O'Reilly. from DevSim import DevSim import SimLib.RetCodes as RC import DevSimDefs as DS def testDevSim3(): simIO = SimDev.SimDev() # set up the simulated device simIO.setInputSrc(SimDev.INCHAN1, SimDev.EXT_IN) simIO.setOutputSrc(SimDev.OUTCHAN1, SimDev.INCHAN1) simIO.setFunction(SimDev.INCHAN1, "x0 * 2") loopcount = 0 while loopcount < 10: # send data to simulator simIO.sendData(SimDev.INCHAN1, loopcount) # acquire data input from simulator with function applied rc, simdata = simIO.readData(SimDev.OUTCHAN1) if rc != RC.NO_ERR: print "SimDev returned: %d on read" % rc break print "%d %f" % (loopcount, simdata) loopcount += 1 # loop back for more simIO.stopSim = True
This example is just the 2x scaling function mentioned earlier.
The cyclic functions of the simulator provide a convenient source of predictable cyclic data. You have complete control over the wave shape and the timing, so you can, for example, set up a simulation using the pulse mode to evaluate the response of a control algorithm. The other cyclic functions might be used to simulate the velocity feedback from a mechanism, to simulate temperature change over time, or as a source of data to check limit sensing.
Because the cyclic functions have the ability to run in an asynchronous mode, you need to make sure that you don’t end up with a situation where the cyclic data is aliased because the data read rate is too slow, as shown in Figure 10-10. In this case, either the sample rate needs to be increased or the cyclic rate needs to be decreased so that the sample data read rate is no less than four times the cyclic rate.
Lastly, there is the random data injection that occurs just before the output. The relative range, or level, of both the data and the random “noise” are controllable. This allows you to set the balance between the two. Note that the random data is summed into the data stream, not simply injected. It won’t allow for the simulation of discrete transient events, but it does simulate modulation noise, such as might be found in a noisy voltage source (or a corroded connector).
The next example we’ll look at is a simulation of an eight-channel AC power control unit of the type often found in large server installations, laboratories, and industrial facilities. This example is intended to show how a simple command-response-type instrument behaves, and also to provide some insight into instruments that employ a serial interface. The concepts introduced here can also be seen in devices such as laser controllers, electronic test equipment, temperature controllers, and motion control units. We’ll also look at how to communicate with a serial interface simulator without using a second computer and a physical cable.
This simulation models a hypothetical device called the Simple Power Controller (SPC). It doesn’t have some of the bells and whistles found on real units, such as password protection, controller unit ID assignment, and so on, mainly because these features aren’t really necessary to control power. It does, however, simulate the inclusion of electronic circuit breakers (ECBs) for each AC channel. These are similar to the power control devices found in aerospace applications. They protect each channel from an over-current condition and can be reset remotely if necessary.
A diagram of the hypothetical SPC controller is shown in Figure 10-11. As you can see, it’s rather simple electrically (most devices like this are, actually). The “smarts” of the unit are contained entirely within its microcontroller.
In Figure 10-11, the blocks labeled SSR are solid state relays, and the microcontroller could be any suitable device. Notice that the diagram does not show any front-panel controls, because the simulator won’t be concerned with them. The objective of the simulator is to emulate the functionality of the microcontroller in terms of the remote control interface. You are, of course, welcome to add a nice GUI if you wish (we’ll discuss user interfaces in Chapter 13).
Unlike the DevSim simulator with its API, the SPC simulator uses a serial interface. It’s a simple 9,800-baud N-8-1–type interface that is implemented using the pySerial library, which we will examine in detail in Chapter 11. For now we can assume that pySerial provides all the necessary functionality to open a serial port, set the serial port parameters, and read and write data.
There’s a catch to using a simulator that employs a serial interface, and that is interfacing with it. One way is to use two computers with a null-modem cable between them (as shown in Chapter 7), but a second computer is not always a viable option, and some machines don’t even have a serial port available (such as notebook and netbook PCs).
The solution is to use what are called “virtual ports” to create a link between two applications that utilize a serial interface. One such utility for the Windows environment is Vyacheslav Frolov’s com0com package. You can download it from http://com0com.sourceforge.net.
com0com works by creating a pair of virtual serial ports configured as a null-modem connection. You can assign standard names to the ports when they are created in order to accommodate applications that can only deal with names like COM1, COM2, COM3, and so on, and from an application’s perspective these ports will behave exactly like real physical interface ports. You can set the baud rate, query the status, and in general do just about anything that can be done with a normal serial port. Figure 10-12 shows how instrumentation software can communicate with a serial interface–type simulator using two virtual serial ports.
Linux users can instead use tty0tty, which is available from http://tty0tty.sourceforge.net. It’s rather minimal, and the documentation is somewhat thin, but the user-level version worked just fine for me on Ubuntu 10.04 after compilation.
There are basically two ways to interact with the SPC simulation: directly via a terminal emulator, or by way of instrumentation software. If you just want to manually exercise the simulator, you will need some way to send commands and view the responses. The tool of choice here is a terminal emulator. It doesn’t really matter what terminal emulator you use, so long as you can configure it to use the virtual serial ports that com0com (or tty0tty) creates.
On Windows 2000 and XP systems you can use the venerable Hyperterm or you may wish to check out Tera Term (which we’ll look at shortly), and on Linux the minicom emulator is available. If you are using Windows Vista or Windows 7, you will need to look around for a terminal emulator, but there are many available. When using a terminal emulator and virtual serial ports the setup will be basically the same as that shown in Figure 10-12, but with “Instrument Software” replaced by “Terminal Emulator.”
SPC utilizes 10 simple commands to control outputs, check status, and set configuration parameters. All commands are three characters in length, with no exceptions, and all commands take at least one parameter. The commands are listed in Table 10-2.
Command | Parameters | Description |
|
| Enables or disables all eight AC channels at once |
|
| Sets the power state of a specific channel to either On or Off |
|
| Starts either a power-up or a power-down sequence |
|
| Sets the delay time between sequence steps, in milliseconds |
|
| Defines the power-up and power-down sequence |
|
| Sets the sequence error-handling mode |
|
| Returns the on/off status of a specific channel or all channels |
|
| Returns the OK/error status of a specific ECB or all ECBs |
|
| Sets the ECB current limit for a specific channel |
|
| Resets the ECB for a specific channel |
Here are a few general notes on the SPC commands:
All commands require at least one parameter. There are no zero-parameter commands.
Channels are identified by ASCII digits between 1 and 8,
inclusive. The value 0
is used
to indicate all channels with the CHK
command.
The ASCII digits 1
and
0
are used to indicate On and
Off, respectively, for the ALL
and POW
commands, and startup
and shutdown for the SEQ
command.
The sequence mode command, SEM
, takes a single ASCII digit
(0
, 1
, or 2
) to indicate the mode
selection.
Time and current limit values used with the STM
and LIM
commands are whole integer values in
ASCII form, and are limited to two digits (0
to 99
) for current limits (in amps) and
three digits (0
to 999
) for time (in seconds).
The SOR
command takes a
comma-separated list of one to eight channel numbers.
All commands return a response; there are no silent
commands. Command responses are either 1
or 0
. A 1
indicates either On or OK, and a
0
indicates either Off or an
error.
Now, let’s take a closer look at the commands:
ALL
state
Enables or disables all eight AC channels in sequence order without a dwell delay.
The state
parameter may be
1
(On) or 0
(Off).
Responds with 1
(OK) if
successful, or 0
(error) if
the ECB is tripped for any channel at power-up. Returns
immediately (does not wait for command completion).
POW
ch
,
state
Sets the power state of a channel to either On or Off.
ch
is a channel number, and
state
may be 1
(On) or 0
(Off).
Responds with 1
if
successful, or 0
if the ECB
is tripped at power-up or some other error occurred. Waits for
command completion before returning.
SEQ
state
Commands the controller to start either a power-up
or power-down sequence. If no sequence order has been defined
using the SOR
command, the
startup order will be from lowest to highest and the shutdown
order will be the inverse.
The parameter state
may be
1
(startup) or 0
(shutdown).
Responds with 1
if
successful, or 0
if the ECB
is tripped for any channel at power-up. Returns immediately
(does not wait for command completion).
STM
time
Sets the amount of time to pause between each step in a power-up or power-down sequence. The default pause time is 1 second, and the time is specified as an integer value.
Responds with 1
if
successful, or 0
if the time
value is invalid.
SOR
ch
,
ch
,
ch
,
ch
,
ch
,
ch
,
ch
,
ch
Defines the startup and shutdown sequence order.
Shutdown is the inverse of startup. The list may contain from
one to eight channel ID entries. Any channel not in the list
will be excluded from sequencing, and unused list positions are
marked with a 0
.
Responds with 1
if
successful, or 0
if a
sequence parameter is invalid.
SEM
mode
Sets the error handling for power-up sequencing,
where mode
is defined as
follows:
0
Normal (default) operation. If the ECB for any channel trips, the controller will disable power to any channels that are already active, in reverse order.
1
Error hold mode. If the ECB for any channel trips, the controller will halt the startup sequence but will not disable any channels that are already active.
2
Error continue mode. If the ECB for any channel trips during a startup sequence, the controller will continue the sequence with the next channel in the sequence list.
Responds with 1
if
successful, or 0
if the mode
is invalid.
CHK
ch
|0
Returns the on/off/error status of channel
ch
as either 1 or 0. If
ch
is set to 0
, the statuses of all eight channels
are returned as a comma-separated list of channel states. Also
returns a 0
character for a
channel if that channel’s ECB is tripped. Use the ERR
command to check the ECB
state.
ECB
ch
|0
Returns the ECB status of channel
ch
as either 1
(OK) or 0
(error). If
n
is set to 0
, the ECB statuses of all eight
channels are returned as a comma-separated list of
states.
LIM
ch
|0,
amps
Sets the current limit of the ECB for channel
ch
. If 0
is given for the channel ID, all
channels will be assigned the limit value specified by
amps
.
Responds with 1
if
successful, or 0
if the
channel ID or current limit value is invalid.
RST
ch
Attempts to reset the ECB for channel
ch
.
Responds with 1
if
successful, or 0
if the ECB
could not be reset.
The SPC is a simple command-response-type device. It will never initiate communications with the host system. This means that it can be effectively simulated using a simple command recognizer. Also, since the SPC is a discrete state-based simulator, it needs a set of data to define the state of each of the power control channels. How and when a channel transitions from one mode to another is determined by the commands described in the previous section. Figure 10-13 shows the internal data that the SPC simulator needs in order to model a physical system.
The sequence control data objects apply to all channels, and
each channel has three attributes: power state (1
or 0
),
ECB state (1
or 0
), and a current limit for the channel’s
ECB.
Next up is the command recognizer, which is very simple:
def Dispatch(self, instr): cmdstrs = instr.split() if len(cmdstrs) >= 2: if len(cmdstrs[0]) == 3: if cmdstrs[0].upper() == "ALL": self.SetAll(cmdstrs) elif cmdstrs[0].upper() == "POW": self.SetPower(cmdstrs) elif cmdstrs[0].upper() == "SEQ": self.SetSeq(cmdstrs) elif cmdstrs[0].upper() == "STM": self.SetSTM(cmdstrs) elif cmdstrs[0].upper() == "SOR": self.SetOrder(cmdstrs) elif cmdstrs[0].upper() == "SEM": self.SetSEM(cmdstrs) elif cmdstrs[0].upper() == "CHK": self.ChkChan(cmdstrs) elif cmdstrs[0].upper() == "ECB": self.ChkECB(cmdstrs) elif cmdstrs[0].upper() == "LIM": self.SetLimit(cmdstrs) elif cmdstrs[0].upper() == "RST": self.RstChan(cmdstrs) else: SendResp("ER") else: SendResp("ER") else: SendResp("ER")
After a command is received, the method Dispatch()
is called. The incoming string
from the host system is split into a list, which should contain two or
more elements. The first element should be the command keyword. The
number of parameters after the keyword will vary depending on the
command, but no command has zero parameters.
When the command is decoded, one of 10 utility methods is called to write data into the internal data table, get data from the table, and invoke the channel control to perform the commanded action (if it’s not just a status query). Notice that there is a command utility method for each command.
Figure 10-14 shows the message sequence chart (MSC) for a typical command-response interaction with the SPC simulator.
There are two possible return paths from the SPC command processor back to the host (the terminal emulator or instrumentation software), depending on the command. Some commands provide an immediate response and do not wait for the channel control logic to complete an activity. Other commands will wait and then return an indication of success or failure.
The SPC simulator uses a configuration file, also known as an “INI” file, to hold various configuration parameters that are read in at startup. Here is what spc.ini might look like:
[SPC] SPORT=COM4 SBAUD=9600 SDATA=8 SPAR=N SSTOP=1 ECB1=2.5 ECB2=2.5 ECB3=5.0 SOR=[3,2,1,4,5,8,7,8,6] STM=2.0 SEM=0
The values from the INI file will be loaded into the internal data table, but they may be overwritten with new values using the SPC commands. Parameters not defined in the INI file will assume their default values.
When the SPC simulator is started, it will first attempt to
open the serial port defined in the INI file. If successful, it will
start its primary loop and wait for incoming commands. When the SPC
receives a carriage return (CR) and nothing else, it will return the
prompt character (>
). Here’s an
example session that enables power channel 1:
>CHK 0
[0, 0, 0, 0, 0, 0, 0, 0] >POW 1, 1
1 >CHK 0
[1, 0, 0, 0, 0, 0, 0, 0]
To use the SPC simulator with instrument software, the first
step is to get the SPC’s attention. Something like the following code
snippet should suffice (we’ll assume that a serial port is already
open and referenced by the sport
object):
gotprompt = False last_time = time.time() MAXWAIT = 5.0 while True: sport.write(" ") instr = sport.read(2) if instr=="> ": gotprompt = True break if time.time() - last_time > MAXWAIT: break time.sleep(0.5)
This snippet will send a carriage return character every 500 milliseconds until the SPC responds or five seconds has elapsed, whichever comes first. When the SPC responds with a prompt, it exits the loop, and the system is ready to communicate.
Using the SPC as a framework, you can create a simulator for just about any simple device or instrument with a serial control interface. The SPC simulator also shows how a simulation can be used to evaluate a device or instrument that does not (yet) exist in the real world. There is no substitute for working with a live system, be it real or simulated, to get a feeling for what it can, and cannot, do.
When working with instruments and subsystems that employ a serial interface, it is sometimes possible to repurpose some commonly available tools to create a perfectly usable simulator.
One such tool for Windows systems is called Tera Term; this is the tool I will focus on in this section. Originally written by T. Teranashi in the mid-1990s (and last updated in 1999, when version 2.3 was released), Tera Term supports Telnet logins as well as serial I/O, but the original release of 2.3 does not support SSH.
Although there is no longer a big demand for serial terminal emulators, and Tera Term is getting rather dated, it has something that makes it particularly interesting: a powerful scripting language. When combined with a tool such as com0com, it is possible to use Tera Term to create a respectable simulation of a serial I/O instrument and communicate with it from your instrumentation software during development and testing. Tera Term works well at the other end of the communications link as well, and I’ve used it as a functional test driver for an embedded imaging system and a laser interferometer system, among other applications. Of course, I have also used it many times as just a terminal emulator.
You can download Tera Term and get more information about it from http://hp.vector.co.jp/authors/VA002416/teraterm.html. The source code is freely available, and there are some add-ons available as well. Check the website for details.
Installing Tera Term is easy. After downloading the archive, unzip it into a temporary location. Then find and run the file setup.exe. This will install Tera Term in c:Program FilesTTERMPRO (unless you specify a different location). After the installation is complete, you can delete the contents of the temporary directory.
The installation will create a subsection in the Windows Start→Programs menu called “Tera Term Pro.” You can use the right mouse button to drag the program icon labeled “Tera Term Pro” out onto the desktop and create an icon. Tera Term is known to work with Windows 2000 and Windows XP.
Almost all Linux installations come with a serial terminal emulator tool called minicom, and there are other emulators available as well. Some like, minicom, are rather limited in terms of their scripting capability, whereas others are more feature-complete. I tend to view Tera Term as a model of what a free and open source serial terminal emulator should be able to do, so I will stick to that for the rest of this discussion. If you are using a Linux system, by all means explore the options available to you. Given the huge amount of software available for Linux, I’m sure there is something that will meet your needs. In any case, after seeing what Tera Term can do you should have some idea of what to look for.
The main focus of terminal emulator scripts is the conditional test. In other words: if something is this, then do that, else do another thing. Although a terminal emulator may have a lot of bells and whistles, a basic script to dial a number and announce a connection boils down to something like this:
:dial send string "555-1212" if busy then goto dial wait connected if connected print "CONNECTED" if not connected print "ERROR - COULD NOT CONNECT" exit
If we accept that the wait
statement is actually a form of IF-ELSE
statement, we can see that this simple
script is really just a sequence of conditional tests.
I’m making a point of this because once you understand the paradigm behind the scripting languages employed in terminal emulators it becomes much easier to leverage these tools into roles for which they were never originally intended.
The Tera Term scripting language (Tera Term Language, or TTL
as the author calls it) is a full-featured language that not only
provides the basic commands for handling IF-THEN
decisions in a communications
context, but also includes commands for generating dialog windows,
writing data to files, reading from files, string conversions, and
executing external applications. The language provides flow control
statements such as IF-THEN
(plus
ELSEIF
and ELSE
), FOR
,
and WHILE
. It does not have a
complete set of math functions, and it supports only two data types:
integers and strings. But, given what it was originally intended to do,
this makes perfect sense, and it really isn’t a major hurdle. You can
view the online documentation included with Tera Term by selecting the
Help menu item (there is no user manual). Note that the main help
display refers to the scripting facility as “MACRO.”
Here is the connection test for the SPC that we saw earlier in Python, translated into TTL:
waitcnt = 0 :connect ; check for max attempts if wantcnt > 9 then goto noconnect endif send "" recvln ; recvln puts its return into "inputstr" strcompare inputstr "> " ; strcompare sets "result" based on the comparison if result = 0 then goto start else ; pause only uses whole numbers pause 1 ; jump back and try it again goto connect endif :start ; skip over the error dialog display goto endconnect :noconnect messagebox "SPC not responding" "Error" :endconnect ; at this point the user can start entering commands
If you’ve ever worked with BASIC, or the so-called batch files on
an MS-DOS or Windows system, this should look familiar. Tera Term’s TTL
does support subroutines (CALL-RETURN
), and it has a perfectly usable
WHILE
statement, but I elected not to
use them in this example.
Like most terminal emulators, Tera Term handles only one external connection at a time, so it’s not really possible to employ Tera Term as the control logic in an instrumentation system (at least not easily—this can be done using external programs and data files). Where Tera Term is useful is in creating a simulation of an instrument or device for a Python instrumentation application to communicate with. One could, in fact, implement the SPC simulator entirely in TTL using Tera Term. Almost any other simple instrument with a serial interface could also be a candidate for simulation using Tera Term.
Tera Term is also useful for driving other systems for repetitive testing. In fact, during the development of the image acquisition and processing software for a space probe, Tera Term was used to push tens of thousands of test images through the image compression software and log the results of each test. It worked flawlessly, and generated a mountain of data to sift through.
A simulation can generate a lot of useful data, but just looking at a file with a list of numbers isn’t as intuitive as seeing a graph of the data. In this section I will show you how to use the data generated by a simulator to create interesting and useful graphical output in the form of data plots.
We’ll focus on gnuplot, a venerable tool that has been available for Unix and Linux systems for many years. There is also a Windows version available, and both can work with Python to display dynamically generated data. Later, in Chapter 13, we’ll look at user interfaces and more sophisticated ways to generate graphical output, but this is a good place to begin.
gnuplot is a powerful and well-established graphical plotting tool
that is capable of generating graphical output ranging from simple line
graphs to complex data visualizations. Although it was originally
developed for Unix, there is a Windows version available as well.
gnuplot has a serviceable built-in
command-line-style user interface and the ability to load plot command
and data files. It can also use so-called pipes for its command input, thus allowing
other applications to drive the plot display. This section briefly
describes two methods to allow Python programs to send data and commands
to gnuplot for display. The first is a simple
demonstration of Python’s popen()
method. While this method is straightforward and easy to implement, it
does nothing to assist you with gnuplot; it just
sends commands. Consequently, the programmer needs to have a good
understanding of the gnuplot application and its
various command and configuration options. The second method uses
Michael Haggerty’s gnuplot.py
package, which implements a wrapper object for
gnuplot that handles some of the details of the
command interface for the programmer. The documentation for
gnuplot is contained in a set of HTML pages
included with the distribution, and is also available online at http://gnuplot-py.sourceforge.net/doc/Gnuplot/index.html.
The first step is to install gnuplot, if it isn’t already installed on your system. If you’re running Linux, there’s a good chance it’s already available. If it isn’t, a quick session with a package manager (apt-get, rpm, synaptic, etc.) can be used to do the installation. If you’re running Windows, you’ll need to download the gnuplot installation package from SourceForge. If you are so inclined, you can also download the source code and build it from scratch (not recommended unless you really know what you’re doing and your system doesn’t have a package manager tool available). The current version of gnuplot (at the time of publication) is 4.4.2. It is available from http://gnuplot.sourceforge.net.
I will assume that Linux users will install gnuplot using a package manager, so I won’t describe the process here. This procedure applies only to Windows users.
If installing the support packages for the first time, perform the setup in the following order:
Unzip the archive file gp440win32.zip to your C: drive, or somewhere else where you would like it to live permanently. You might even put it under “Program Files” on the C: drive. A directory called gnuplot will be created when the archive is unzipped.
Open the environment variables dialog (on Windows 2000 and
XP) by using Settings→Control Panel→System→Advanced→Environment Variables, or right-clicking on the “My Computer” icon
on the desktop and selecting “Properties.” Set the GNUPLOT
environment variable (as a
system variable, not a user variable) to refer to the directory
binary in the gnuplot directory structure. See the
INSTALL file in the gnuplot directory for more information
about the various environment variables available.
Using the Environment Variables dialog, put the gnuplotinary directory into the
Windows search path (the PATH
environment variable). For example, if you put
gnuplot in the root of the C: drive, you would add C:gnuplotinary to the search path
string. Note that entries in the path string are separated by
semicolons.
Optionally, you can also create an icon on the desktop to launch gnuplot using the file wgnuplot.exe. Make sure that the “Start in” parameter refers to the gnuplotinary directory (right-click on the icon and select Properties from the menu that appears).
Look through the documentation found in the directory gnuplotdocs, and the file gnuplot-4.4.0.pdf in particular. Also read README.windows, located in the root directory of the gnuplot directory tree.
The gnuplot package for Windows contains the following executable files:
A Windows GUI version of gnuplot. Provides the same command-line console interface as the non-GUI version, but uses a GUI text-editor-type display for the command line and includes a menu bar and buttons to click for common operations.
Same as wgnuplot, but with the advantage of support for internal pipe specifications of the form:
plot `<awk -f change.awk data.dat`
The classic text (console) interface version of the gnuplot executable, with all the associated pipe functionality as found on other platforms. This means that this program can also accept commands on stdin (standard input) and print messages on stdout (standard output). This is the preferred executable to use when integrating gnuplot with other programs, such as Python applications.
A “helper” program that will accept commands on stdin (standard input) and pipe them to an active (or newly created) wgnuplot.exe console window. Command-line options are passed on to wgnuplot.
wgnuplot is what you would typically put on the Windows desktop as an icon, and gnuplot is what a Python program would open when creating a pipe.
As I stated earlier, we are going to look at two ways to
interface with gnuplot from a Python application.
The first is simple, but requires a solid grasp of the
gnuplot command set. The second method handles a
lot of the details for you, but it also hides some of those command
details from you, and it implements someone else’s notion of what a
Python-gnuplot interface should be. It’s up to you
to pick the path of least resistance to accomplish your objectives, and
you should at least skim through the available documentation for
gnuplot and gnuplot.py
before deciding which method makes
the most sense for your application.
If you want to be able to use a pipe to send commands to
gnuplot running under Windows, you must use the
gnuplot version, not wgnuplot. This is because Windows GUI
applications (such as wgnuplot) do not accept
input from stdin. You can tell Python to use
gnuplot when creating the pipe using popen()
. Alternatively, you can use
pgnuplot to achieve the same results with
wgnuplot. On a Linux system this is not an issue
(there are no wgnuplot or
pgnuplot binaries).
The following example, gptest.py
, will launch
gnuplot and display a series of plots:
#! /usr/bin/python # gptest.py import os import time f=os.popen('gnuplot', 'w') print >> f, 'set title "Simple plot demo" 1, 1 font "arial, 11"' print >> f, 'set key font "arial, 9"' print >> f, 'set tics font "arial, 8"' print >> f, "set yrange[-20:+20]" print >> f, "set xrange[-10:+10]" print >> f, 'set xlabel "Input" font "arial,11"' print >> f, 'set ylabel "Output" font "arial,11"' for n in range(100): # plot sine output with zero line (the 0 term) print >> f, 'plot sin(x * %i) * 10, 0' % (n) time.sleep(0.1) f.flush() # pause before exit time.sleep(2)
To run this example, just save the code to a file (gptest.py
, for example) or load the file
from the source code for this book. On a Windows machine, type in the
following at a command prompt:
python gptest.py
Under Linux you can just enter the script’s name, assuming that the file is marked as executable and the Python interpreter resides in /usr/bin:
% gptest.py
When it runs, you should see a sine wave that expands and
contracts several times. What is actually happening is that
gnuplot is regenerating the plot across the
x range of −10 to +10 each time the plot
command is called. The result appears
as an animated image, but in fact it is a series of plots presented in
rapid succession. Note that the gnuplot window
will close as soon as the script completes and Python
terminates.
The following lines from the Method 1 example are possible candidates for inclusion into the gnuplot.ini file, which should be located in the binary directory with the gnuplot executables:
set key font "arial, 9" set tics font "arial, 8"
Refer to the gnuplot documentation for more about the gnuplot.ini file and its uses.
The second method uses Michael Haggerty’s gnuplot.py
package (version 1.8), which is
available from http://gnuplot-py.sourceforge.net. You will also need
the NumPy package (version 1.4.1), available from
http://numpy.scipy.org. For Windows, you should
download and install numpy-1.4.1-win32-superpack-python26.exe
(just execute the file to start the installation).
To install gnuplot.py
, follow
these steps:
Unzip gnuplot-py-1.8.zip into a temporary directory. It will create a directory called gnuplot-py-1.8.
Open a command window, and from within the command window change to the directory gnuplot-py-1.8 in the temporary directory where gnuplot-py-1.8 was unpacked. You should see a file called setup.py.
At the command prompt, type:
python setup.py install
During the execution of the setup script, you should see many lines of output go by on the screen. If an error is encountered, the setup script will halt; otherwise, you should now be ready to go.
In the directory <python>Libsite-packagesGnuplot run the file demo.py, like so:
python demo.py
<python> is where you installed Python 2.6, and on a Windows system it may be something like C:Python2.6.
If all goes well, you’ll see a plot display. Pressing the Enter key from within the command window will cause a series of graphs to be displayed.
gnuplot.py
is somewhat
unusual in terms of how it is imported into your application. If you
look at demo.py, you’ll see that
it is importing Gnuplot and
Gnuplot.funcutils. But there is
no “Gnuplot.py” in the Gnuplot
directory where the package resides. What is happening here is that
the package initializer, __init__.py
, is imported. It, in turn,
imports the rest of the necessary modules. The __init__.py
module also contains a top-level
docstring with lots of information. If you examine __init__.py
you may also notice that it
contains the following code at the bottom of the file:
if __name__ == '__main__': import demo demo.demo()
What this means is that you can type in:
python __init__.py
from the Gnuplot directory, and the demo will execute.
This approach is interesting in that when the gnuplot.py
package is imported the __init__.py
module will be evaluated
immediately, and the necessary imports will be in place and available
from that point onward in your application.
To import gnuplot.py
into
your application, you must at least import the main module:
import Gnuplot
You can import additional modules using dot notation, like so:
import Gnuplot.funcutils
This next couple of examples will plot the contents of a data file containing a set of records with a single field (we will look at ASCII data files in more detail in Chapter 12).
We’ll use the PID class introduced in Chapter 9, and create a data file to graph the impulse response of the controller.
First, here’s how to do it using the pipe method:
#! /bin/python # PIDPlot.py # # Uses gnuplot to generate a graph of the PID function's output # # Source code from the book "Real World Instrumentation with Python" # By J. M. Hughes, published by O'Reilly. import time import os import PID def PIDPlot(Kp=1, Ki=0, Kd=0): pid = PID.PID() pid.SetKp(Kp) pid.SetKi(Ki) pid.SetKd(Kd) time.sleep(.1) f = open('pidplot.dat','w') sp = 0 fb = 0 outv = 0 print "Kp: %2.3f Ki: %2.3f Kd: %2.3f" % (pid.Kp, pid.Ki, pid.Kd) for i in range(1,51): # summing node err = sp - fb # PID block outv = pid.GenOut(err) # control feedback if sp > 0: fb += (outv - (1/i)) # start with sp = 0, simulate a step input at t(10) if i > 9: sp = 1 print >> f,"%d % 2.3f % 2.3f % 2.3f % 2.3f" % (i, sp, fb, err, outv) time.sleep(.05) f.close() gp=os.popen('gnuplot', 'w') print >> gp, "set yrange[-1:2]" for i in range(0, 10): kpval = 0.9 + (i * .1) PIDPlot(kpval) print >> gp, "plot 'pidplot.dat' using 1:2 with lines, 'pidplot.dat' using 1:3 with lines" raw_input('Press return to exit... ')
This will generate a series of graphs, with a Kp value ranging from 0.9 to 1.8. The last graph is shown until the user presses the Enter key, and when the program terminates gnuplot closes. The output for the last graph (Kp = 1.8) is shown in Figure 10-15.
As you may recall from Chapter 9, if the proportional gain is too high the system will exhibit instability in response to a sudden input change, and there it is in Figure 10-15.
Now here’s the gnuplot.py
way
to plot the data:
# PIDPlot.py # # Uses gnuplot to generate a graph of the PID function's output # # Source code from the book "Real World Instrumentation with Python" # By J. M. Hughes, published by O'Reilly. import Gnuplot, Gnuplot.funcutils import time import os import PID def PIDPlot(Kp=1, Ki=0, Kd=0): pid = PID.PID() pid.SetKp(Kp) pid.SetKi(Ki) pid.SetKd(Kd) time.sleep(.1) f = open('pidplot.dat','w') sp = 0 fb = 0 outv = 0 print "Kp: %2.3f Ki: %2.3f Kd: %2.3f" % (pid.Kp, pid.Ki, pid.Kd) for i in range(1,51): # summing node err = sp - fb # PID block outv = pid.GenOut(err) # control feedback if sp > 0: fb += (outv - (1/i)) # start with sp = 0, simulate a step input at t(10) if i > 9: sp = 1 print >> f,"%d % 2.3f % 2.3f % 2.3f % 2.3f" % (i, sp, fb, err, outv) time.sleep(.05) f.close() gp = Gnuplot.Gnuplot() gp.clear() gp.title('PID Response') gp.set_range('yrange', (-1, 2)) for i in range(0, 10): kpval = 0.9 + (i * .1) PIDPlot(kpval) gp.plot(Gnuplot.File('pidplot.dat', using=(1,2), with_='lines'), Gnuplot.File('pidplot.dat', using=(1,3), with_='lines')) raw_input('Press return to exit... ')
The output looks the same as before, except that now there is a
title above the graph. For some examples of the other things that
gnuplot.py
can do, see the demo.py
and test.py
files included with gnuplot.py
and located in the gnuplot directory where the package was
installed (usually python26/Lib/site-packages).
Now that we’ve touched on what simulators are and seen some ways they can be used, it’s time to consider what goes into creating such a thing. I’m a big fan of simulators, but I also try to temper my enthusiasm with some realism. It’s all too easy to eat up a big chunk of the time and budget for a project just fiddling with the simulation. So, before starting to build your own simulator, there are three key questions you should ask yourself:
Why do you want to use a simulator?
What do you want to simulate?
How much time and effort can you expend to create a simulator?
How you answer these questions will help you avoid spending time on something you don’t really need (even if it is fun to build and play with).
First and foremost, there must be a real need for a simulation. If it is really not possible to develop the instrumentation or control software without one, that is probably enough justification. As I stated at the start of this chapter, such a situation might arise when software needs to be written, but the hardware won’t arrive until some later date. Rather than wait for the hardware, and run the risk of running over schedule as a result, you can use a simulation to start at least building and testing the framework of the instrumentation software.
Another example would be where the hardware is something unique and special, and there is a definite risk that the software could make it do something that might damage it. Some motion control systems fall into this category (recall the story of the runaway PID servo controller in Chapter 9), as might systems that involve high temperatures or pressures.
When considering something like a simulator, it is essential to define the scope of the simulation before any code is written: in other words, what it will simulate, and how accurate the simulation needs to be in order to be useful.
Figure 10-16 shows one way to visualize the levels of detail and complexity that can go into a simulation. Starting at the bottommost level, I/O, the fidelity of the simulation increases as additional layers are added. However, the costs in terms of time and effort also increase.
In many cases, just having the I/O and command processing levels may be sufficient. The simulators we’ve already seen in this chapter really aren’t much more than this, with a little bit of functionality to handle setting and recalling states and parameters. As the size of the blocks in Figure 10-16 implies, the more capabilities you add, the more complex and costly the simulator becomes.
Although each simulator you might contemplate must be evaluated based on the specific needs of the project, it is generally safe to say that if you can accurately simulate the interface and the command processing, it’s probably good enough to get you started. You can always add details in the behavior level later if it turns out that they are really necessary.
Writing a simulator takes time and effort. Sometimes this can be significant, and it is not uncommon to have a situation where creating and verifying the simulator takes as much (or more) effort than that required to implement the system being developed. This might be justified if the system is something critical (like part of an experimental aircraft), if the simulator is generic enough to be reused on other projects, or if it can be used later in a production environment for product testing. If it’s just a one-time thing for a simple system, it shouldn’t be any more complicated than it really needs to be. It might even turn out that a simulator isn’t really necessary at all if a good debugger is available, and you’re willing to use simple files to capture data.
When used appropriately, simulation is a powerful tool that can help save effort and avoid costly mistakes and delays later on, when the software meets real hardware. It is also a potential time-sink and source of development delays, so deciding when and how to employ simulation is important.
If you’re interested in digging deeper into simulation, the following books are good places to start:
A collection of papers and essays on simulation topics with a focus on practical applications rather than on theoretical issues. Provides step-by-step procedures and covers problem analysis, model development, and data analysis.
One of the first books to deal with the subject of fault injection in depth. Although oriented toward high-end safety-critical and fault-tolerant systems, the techniques and concepts in this book can be applied to any software development project.
A thorough review of gnuplot, along with a wealth of ideas for how to use it to create useful and interesting data visualization displays.
There are also some general resources available on the Web, although there seems to be very little in the way of introductory material available. Here are a few URLs that may be of interest to you:
This link points to the PDF version of Computer Simulation Techniques: The Definitive Introduction!, by Harry Perros. While not quite as broad in scope as the title might suggest, this book introduces traditional Operational Research (OR)–type simulations and contains extensive discussions of randomness, sampling theory, and estimation techniques.
The companion website for the book Introduction to Computer Simulation Methods, by Harvey Gould, Jan Tobochnik, and Wolfgang Christian (Addison-Wesley). While the book itself is not available here, there are lots of useful notes, tutorials, and a couple of sample chapters in PDF format. The main focus of the book itself is on computational physics and the simulation of physical systems. I’ve recommended it here mainly for the examples it provides of ways to approach various simulation problems using mathematical techniques.
18.117.76.30