Top Up Prev Next Bottom Contents Index Search

2.4 Writing C++ code for stars


This section assumes a knowledge of the C++ language; no attempt will be made to teach the language. We recommend "C++ Primer, Second Edition", by Stanley Lippman (from Addison-Wesley) for those new to the language. Chapter 3, "Infrastructure for Star Writers", is also highly recommended reading for those who will be writing stars, since it explains some of the more generic and useful classes defined in the Ptolemy kernel. Many of these are useful in stars.

C++ code segments are an important part of any star definition. They can appear in the setup, begin, go, wrapup, constructor, destructor, exectime, header, code, and method directives in the Ptolemy preprocessor. These directives all include a body of arbitrary C++ code, enclosed by curly braces, "{" and "}". In all but the code and header directives, the C++ code between braces defines the body of a method of the star class. Methods can access any member of the class, including portholes (for input and output), states, and members defined with the public, protected, and private directives.

2.4.1 The structure of a Ptolemy star

In general, the task of a Ptolemy star is to receive input particles and/or produce output particles; in addition, there may be side effects (reading or writing files, displaying graphs, or even updating shared data structures). As for all C++ objects, the constructor is called when the star is created, and the destructor is called when it is destroyed. In addition, the setup and begin methods, if any, are called every time a new simulation run is started, the go method (which always exists except for stars like BlackHole and Null that do nothing) is called each time a star is executed, and the wrapup method is called after the simulation run completes without errors.

2.4.2 Reading inputs and writing outputs

The precise mechanism for references to input and output portholes depends somewhat on the domain. This is because stars in the domain XXX use objects of class InXXXPort and OutXXXPort (derived from PortHole) for input and output, respectively. The examples we use here are for the SDF domain. See the appropriate domain chapter for variations that apply to other domains.

PortHoles and Particles

In the SDF domain, normal inputs and outputs become members of type InSDFPort and OutSDFPort after the preprocessor is finished. These are derived from base class PortHole. For example, given the following directive in the defstar of an SDF star,


input {
name {in}
type {float}
}
a member named in, of type InSDFPort, will become part of the star.

We are not usually interested in directly accessing these porthole classes, but rather wish to read or write data through the portholes. All data passing through a porthole is derived from base class Particle. Each particle contains data of the type specified in the type subdirective of the input or output directive.

The operator "%" operating on a porthole returns a reference to a particle. Consider the following example:

go {
Particle& currentSample = in%0;
Particle& pastSample = in%1;
...
}
The right-hand argument to the "%" operator specifies the delay of the access. A zero always means the most recent particle. A one means the particle arriving just before the most recent particle. The same rules apply to outputs. Given an output named out, the same particles that are read from in can be written to out in the same order as follows:


go {
...
out%1 = pastSample;
out%0 = currentSample;
}
This works because out%n returns a reference to a particle, and hence can accept an assignment. The assignment operator for the class Particle is overloaded to make a copy of the data field of the particle.

Operating directly on class Particle, as in the above examples, is useful for writing stars that accept anytype of input. The operations need not concern themselves with the type of data contained by the particle. But it is far more common to operate numerically on the data carried by a particle. This can be done using a cast to a compatible type. For example, since in above is of type float, its data can be accessed as follows:


go {
Particle& currentSample = in%0;
double value = double(currentSample);
...
}
or more concisely,


go {
double value = double(in%0);
...
}
The expression double(in%0) can be used anywhere that a double can be used. In many contexts, where there is no ambiguity, the conversion operator can be omitted:


double value = in%0;
However, since conversion operators are defined to convert particles to several types, it is often necessary to indicate precisely which type conversion is desired.

To write data to an output porthole, note that the right-hand side of the assignment operator should be of type Particle, as shown in the above example. An operator << is defined for particle classes to make this more convenient. Consider the following example:

go {
float t;
t = some value to be sent to the output
out%0 << t;
}
Note the distinction between the << operator and the assignment operator; the latter operator copies Particles, the former operator loads data into particles. The type of the right-side operand of << may be int, float, double, Fix, Complex or Envelope; the appropriate type conversion will be performed. For more information on the Envelope and Message types, please see the chapter "Data Types" on page 4-1.

SDF PortHole parameters

In the above example, where in%1 was referenced, some special action is required to tell Ptolemy that past input particles are to be saved. Special action is also required to tell the SDF scheduler how many particles will be consumed at each input and produced at each output when a star fires. This information can be provided through a call to setSDFParams in the setup method. This has the syntax


setup {
name.setSDFParams(multiplicity, past)
}
where name is the name of the input or output porthole, multiplicity is the number of particles consumed or produced, and past is the maximum value that offset can take in any expression of the form name%offset. For example, if the go method references name%0 and name%1, then past would have to be at least one. It is zero by default.

Multiple PortHoles

Sometimes a star should be defined with n input portholes or n output portholes, where n is variable. This is supported by the class MultiPortHole and its derived classes. An object of this class has a sequential list of PortHoles. For SDF, we have the specialized derived class MultiInSDFPort (which contains InSDFPorts) and MultiOutSDFPort (which contains OutSDFPorts).

Defining a multiple porthole is easy, as illustrated next:

defstar {
...
inmulti {
name {input_name}
type {input_type}
}
outmulti {
name {output_name}
type {output_type}
}
...
}
To successively access individual portholes in a MultiPortHole, the MPHIter iterator class should be used. Iterators are explained in more detail in "Iterators" on page 3-10. Consider the following code segment from the definition of the SDF Fork star:


input {
name{input}
type{ANYTYPE}
}
outmulti {
name{output}
type{= input}
}
go {
MPHIter nextp(output);
PortHole* p;
while ((p = nextp++) != 0)
(*p)%0 = input%0;
}
A single input porthole supplies a particle that gets copied to any number of output portholes. The type of the output MultiPortHole is inherited from the type of the input. The first line of the go method creates an MPHIter iterator called nextp, initialized to point to portholes in output. The "++" operator on the iterator returns a pointer to the next porthole in the list, until there are no more portholes, at which time it returns NULL. So the while construct steps through all output portholes, copying the input particle data to each one.

Consider another example, taken from the SDF Add star:


inmulti {
name {input}
type {float}
}
output {
name {output}
type {float}
}
go {
MPHIter nexti(input);
PortHole *p;
double sum = 0.0;
while ((p = nexti++) != 0)
sum += double((*p)%0);
output%0 << sum;
}
Again, an MPHIter iterator named nexti is created and used to access the inputs.

Upon occasion, the numberPorts method of class MultiPortHole, which returns the number of ports, is useful. This is called simply as portname.numberPorts(), and it returns an int.

Type conversion

The type conversion operators and "<<" operators are defined as virtual methods in the base class Particle. There are never really objects of class Particle in the system; instead, there are objects of class IntParticle, FloatParticle, ComplexParticle, and FixParticle, which hold data of type int, double (not float!), Complex, and Fix, respectively (there are also MessageParticle and a variety of matrix particles, described later). The conversion and loading operators are designed to "do the right thing" when an attempt is made to convert between mismatched types.

Clearly we can convert an int to a double or Complex, or a double to a Complex, with no loss of information. Attempts to convert in the opposite direction work as follows: conversion of a Complex to a double produces the magnitude of the complex number. Conversion of a double to an int produces the greatest integer that is less than or equal to the double value. There are also operators to convert to or from float and Fix.

Each particle also has a virtual print method, so a star that writes particles to a file can accept anytype.

2.4.3 States

A state is defined by the state directive. The star can use a state to store data values, remembering them from one invocation to another. They differ from ordinary members of the star, which are defined using the public, protected, and private directives, in that they have a name, and can be accessed from outside the star in systematic ways. For instance, the graphical interface pigi permits the user to set any state with the A_SETTABLE attribute to some value prior to a run, using the edit-params command. The interpreter provides similar functionality through the setstate command. The state attributes are set in the state directive. A state may be modified by the star code during a run. The attribute A_NONCONSTANT is used as a pragma to mark a state as one that gets modified during a run. There is currently no mechanism for checking the correctness of these attributes.

All states are derived from the base class State, defined in the Ptolemy kernel. The derived state classes currently defined in the kernel are FloatState, IntState, ComplexState, StringState, FloatArrayState, IntArrayState, ComplexArrayState, and StringArrayState.

A state can be used in a star method just like the corresponding predefined data types. As an example, suppose the star definition contains the following directive:

state {
name { myState }
type { float }
default { 1.0 }
descriptor { Gain parameter. }
}
This will define a member of class FloatState with default value 1.0. No attributes are defined, so A_CONSTANT and A_SETTABLE, the default attributes, are assumed. To use the value of a state, it should be cast to type double, either explicitly by the programmer or implicitly by the context. For example, the value of this state can be accessed in the go method as follows:


go {
output%0 << double(myState) * double(input%0);
}
The references to input and output are explained above. The reference to myState has an explicit cast to double; this cast is defined in FloatState class. Similarly, a cast to int is available for IntState, to Complex from ComplexState, and to const char* for Stringstate). In principle, it is possible to rely on the compiler to automatically invoke this cast. However:

Warning: some compilers (notably some versions of g++) may not choose the expected cast. In particular, g++ has been known to cast everything to Fix if the explicit cast is omitted in expressions similar to that above. The arithmetic is then performed using fixed-point point computations. This will be dramatically slower than double or integer arithmetic, and may yield unexpected results. It is best to explicitly cast states to the desired form. An exception is with simple assignment statements, like

double stateValue = myName; Even g++ gets this right. Explicit casting should be used whenever a state is used in an expression. For example, from the setup method of the SDFChop star, in which use_past_inputs is an integer state,

if ( int(use_past_inputs) )
input.setSDFParams(int(nread),int(nread)+int(offset)-1);
else
input.setSDFParams(int(nread),int(nread)-1);

Note that the type Complex is not a fundamental part of C++. We have implemented a subset of the Complex class as defined by several library vendors; we use our own version for maximum portability. Using the ComplexState class will automatically ensure the inclusion of the appropriate header files. A member of the Complex class can be initialized and operated upon any number of ways. For details, see "The Complex data type" on page 4-1.

A state may be updated by ordinary assignment in C++, as in the following lines


double t = expression;
myState = t;
This works because the FloatState class definition has overloaded the assignment operator ("=") to set its value from a double. Similarly, an IntState can be set from an int and a StringState can be set from a char* or const char* .

2.4.4 Array States

The ArrayState classes ( FloatArrayState, IntArrayState and ComplexArrayState) are used to store arrays of data. For example,


state {
name { taps }
type { FloatArray }
default { "0.0 0.0 0.0 0.0" }
descriptor { An array of length four. }
}
defines an array of type double with dimension four, with each element initialized to zero. Quotes must surround the initial values. Alternatively, you can specify a file name with a prefix <. If you have a file named foo that contains the default values for an array state, you can write,

default { "< foo" }
If you expect others to be able to use your star, however, you should specify the default filename using a full path. For instance,

default { "< ~/user_name/directory/foo" } For default files installed in the Ptolemy directory tree, this should read:

default { "< $PTOLEMY/directory/foo" } The format of the file is also a sequence of data separated by spaces (or newlines, tabs, or commas). File input can be combined with direct data input as in


default { "< foo 2.0" }
default { "0.5 < foo < bar" }
A "repeat" notation is also supported for ArrayState objects: the two value strings


default { "1.0 [5]" }
default { "1.0 1.0 1.0 1.0 1.0" }
are equivalent. Any integer expression may appear inside the brackets []. The number of elements in an ArrayState can be determined by calling its size method. The size is not specified explicitly, but is calculated by scanning the default value.

As an example of how to access the elements of an ArrayState, suppose fState is a FloatState and aState is a FloatArrayState. The accesses, like those in the following lines, are routine:


fState = aState[1] + 0.5;
aState[1] = (double)fState * 10.0;
aState[0] = (double)fState * aState[2];
For a more complete example of the use of FloatArrayState, consider the FIR star defined below. Note that this is a simplified version of the SDF FIR star that does not permit interpolation or decimation.


defstar {
name {FIR}
domain {SDF}
desc {
A Finite Impulse Response (FIR) filter.
}
input {
name {signalIn}
type {float}
}
output {
name {signalOut}
type {float}
}
state {
name {taps}
type {floatarray}
default { "-.04 -.001 .17 .37 .37 .17 -.0018 -.04" }
desc { Filter tap values. }
}
setup {
// tell the PortHole the maximum delay we will use
signalIn.setSDFParams(1, taps.size() - 1);
}
go {
double out = 0.0;
for (int i = 0; i < taps.size(); i++)
out += taps[i] * double(signalIn%i);
signalOut%0 << out;
}
}
Notice the setup method; this is necessary to allocate a buffer in the input PortHole large enough to hold the particles that are accessed in the go method. Notice the use of the size method of the FloatArrayState.

We now illustrate an ptcl interpreter session using the above FIR star. Assume there is a galaxy called singen that generates a sine wave. you can use it with the FIR star, as in:

star foop singen
star fir FIR
star printer Printer
connect foop output fir signalIn
connect fir signalOut printer input
print fir
Star: mainGalaxy.fir
...
States in the star fir:
mainGalaxy.fir.taps type: FloatArray
initial value: -.040609 -.001628 .17853 .37665 .37665 .17853
-.001628 -.040609
current value:
0 -0.040609
1 -0.001628
2 .17853
3 .37665
4 .37665
5 .17853
6 -0.001628
7 -0.040609
Then you can redefine taps by reading them from a file "foo", which contains the data:


1.1
-2.2
3.3
-4.4
The resulting interpreter commands are:


setstate fir taps "<foo 5.5"
print fir
Star: mainGalaxy.fir
...
States in the star fir:
mainGalaxy.fir.taps type: FloatArray
initial value: <foo 5.5
current value:
0 1.1
1 -2.2
2 3.3
3 -4.4
4 5.5
PTOLEMY:
This illustrates that both the contents and the size of a FloatArrayState are changed by a setstate command. Also, notice that file values may be combined with string values; when

< filename
occurs in an initial value, it is processed exactly as if the whole file were substituted at that point.



Top Up Prev Next Bottom Contents Index Search

Copyright © 1990-1997, University of California. All rights reserved.