Ptolemy is about component-based design. The domains define the semantics of the interaction between components. This chapter explains the common, domain-independent principles in the design of components that are actors. Actors are components with input and output that at least conceptually operate concurrently with other actors.
As explained in the previous chapter, some actors are designed to be domain polymorphic, meaning that they can operate in various domains. Others are domain specific. Refer to the domain chapter in part 3 for domain-specific information relevant to the design of actors. This chapter explains how to design actors so that they are maximally domain polymorphic. This minimizes the amount of duplicated code between domains.
As also explained in the previous chapter, many actors are also data polymorphic. This means that they can operate on a wide variety of token types. This chapter explains how to design actors so that they are maximally data polymorphic. This minimizes the amount of duplicated code in actor libraries.
Code duplication can be also be avoided using object-oriented inheritance. Inheritance can also be used to enforce consistency across a set of classes. Figure 3.1, which shows a UML static-structure diagram for an actor library, shows three base classes, Source, Sink, and Transformer, which exist to ensure consistent naming of ports and to avoid duplicating code associated with those ports (although there is not much code there). The fact that these three classes are the base classes for many of the actors in the library means that a user of the library can guess that an input port is named "input" and an output port is named "output," and he will probably be right. It is unlikely that an input port would be named "in" or "inputSignal" or something else. This sort of consistency helps to promote re-use of actors because it makes them easier to use. Thus, we recommend using a reasonably deep class hierarchy to promote consistency.
The basic structure of an actor is shown in figure 4.1. In that figure, keywords in bold are features of Ptolemy II that are briefly described here and described in more detail in the chapters of part 2. Italics is used to indicate text that would be substituted with something else in an actual actor definition.
We will go over this structure in detail in this chapter. The source code for existing Ptolemy II actors, located mostly in $PTII/ptolemy/actor/lib, should also be viewed as a key resource.
By convention, ports are public members of actors. They mediate input and output. Figure 4.1 shows a single port portName that is an instance of TypedIOPort, declared in the line
public TypedIOPort portName;
Most ports in actors are instances of TypedIOPort, unless they require domain-specific services, in which case they may be instances of a domain-specific subclass such as DEIOPort. The port is actually created in the constructor by the line
portName = new TypedIOPort(this, "portName", true, false);
The first argument to the constructor is the container of the port, this actor. The second is the name of the port, which can be any string, but by convention, is the same as the as the name of the public member. The third argument specifies whether the port is an input (it is in this example), and the fourth argument specifies whether it is an output (it is not in this example). There is no difficulty with having a port that is both an input and an output, but it is rarely useful to have one that is neither.
A port can be a single port or a multiport. By default, it is a single port. It can be declared to be a multiport with a statement like
portName.setMultiport(true);
All ports have a width. If the port is not connected, the width is zero. If the port is a single port, the width can be zero or one. If the port is a multiport, the width can be larger than one. A multiport mediates communication over any number of channels.
Data (encapsulated in a token) can be sent to a particular channel of an output multiport with the syntax
portName.send(channelNumber, token);
where channelNumber is the number of the channel (beginning with 0 for the first channel). The width of the port, the number of channels, can be obtained with the syntax
int width = portName.getWidth();
A token can be sent to all channels (or none if there are none) with the syntax
portName.broadcast(token);
A token can be read from a channel with the syntax
Token token = portName.get(channelNumber);
You can query an input port to see whether such a get() will succeed (whether a token is available or can be made available) with the syntax
boolean tokenAvailable = portName.hasToken(channelNumber);
You can also query an output port to see whether a send() will succeed using
boolean spaceAvailable = portName.hasRoom(channelNumber);
although with most current domains, the answer is always true.
Ptolemy II includes a sophisticated type system, described fully in the Type System chapter. This type system supports specification of type constraints in the form of inequalities between types. These inequalities can be easily understood as representing the possibility of lossless conversion. Type a is less than type b if an instance of a can be losslessly converted to an instance of b. For example, IntToken is less than DoubleToken, which is less than ComplexToken. However, LongToken is not less than DoubleToken, and DoubleToken is not less than LongToken, so these two types are said to be incomparable.
Suppose that you wish to ensure that the type of an output is greater than or equal to the type of a parameter. You can do so by putting the following statement in the constructor:
portName.setTypeAtLeast(parameterName);
This is called a relative type constraint because it constrains the type of one object relative to the type of another. Another form of relative type constraint forces two objects to have the same type, but without specifying what that type should be:
portName.setTypeSameAs(parameterName);
These constraints could be specified in the other order,
parameterName.setTypeSameAs(portName);
which obviously means the same thing, or
parameterName.setTypeAtLeast(portName);
Another common type constraint is an absolute type constraint, which fixes the type of the port (i.e. making it unimorphic rather than polymorphic),
portName.setTypeEquals(DoubleToken.class);
The above line declares that the port can only handle doubles. Another form of absolute type constraint imposes an upper bound on the type,
portName.setTypeAtMost(ComplexToken.class);
which declares that any type that can be losslessly converted to ComplexToken is acceptable.
If no type constraints are given for any ports of an actor, then by default, the output ports are constrained to have at least the type(s) of the input ports. If any type constraints are given for any ports in the actor, then this default is not applied for any of the other ports. Thus, if you specify any type constraints, you should specify all of them. For full details of the type system, see the Type System chapter.
To be concrete, consider first the code segment shown in figure 4.2, from the Transformer class in the ptolemy.actor.lib package. This actor is a base class for actors with one input and one output. The code shows two ports, one that is an input and one that is an output. By convention, the Javadoc1 comments indicate type constraints on the ports, if any. If the ports are multiports, then the Javadoc comment will indicate that. Otherwise, they are assumed to be single ports. Derived classes may change this, making the ports into multiports, in which case they should document this fact in the class comment. Derived classes may also change the type constraints of a port.
An extension of Transformer is shown in figure 4.3, the Scale actor. This actor produces an output token on each firing with a value that is equal to a scaled version of the input. The actor is polymorphic in that it can support any token type that supports multiplication by the gain parameter. In the constructor, the output type is constrained to be at least as general as both the input and the gain parameter.
Notice in figure 4.3 how the fire() method uses hasToken() to ensure that no output is produced if there is no input. This is generally the behavior of domain-polymorphic actors. Notice also how it uses the multiply() method of the Token class. This method is polymorphic. Thus, this gain actor can operate on any token type that supports multiplication, including all the numeric types and matrices.
Like ports, by convention, parameters are public members of actors. Figure 4.3 shows a parameter gain that is an instance of Parameter, declared in the line
public Parameter gain;
gain = new Parameter(this, "gain", new IntToken(1));
The third argument to the constructor, which is optional, is a default value for the parameter. In this example, the gain parameter defaults to the integer one.
As with ports, you can specify type constraints on parameters. The most common type constraint is to fix the type, using
parameterName.setTypeEquals(DoubleToken.class);
In fact, exactly the same relative or absolute type constraints that one can specify for ports can be specified for parameters as well. But in addition, arbitrary constraints on parameter values are possible, not just type constraints. An actor is notified when a parameter value changes by having its attributeChanged() method called. Consider the example shown in figure 4.4, taken from the Poisson actor. This actor generates timed events according to a Poisson process. One of its parameters is meanTime, which specifies the mean time between events. This must be a double, as asserted in the constructor.
The attributeChanged() method is passed the parameter that changed. If this is meanTime, then this method checks to make sure that the specified value is positive, and if not, it throws an exception.
A change in a parameter value sometimes has broader repercussions than just the local actor. It may, for example, impact the schedule of execution of actors. An actor can call the invalidateSchedule() method of the director, which informs the director that any statically computed schedule (if there is one) is no longer valid. This would be used, for example, if the parameter affects the number of tokens produced or consumed when an actor fires.
Note: It may be tempting to override attributeChanged() to read the new parameter value and store it in a local (private) variable for more convenient access by other methods. This is ill-advised, however, at least at the time of this writing. A parameter may have a value that is given as an expression that references other parameter values. For example, parameter a might have expression "b + c." The expression language in Ptolemy II is lazy, meaning that the expression is evaluated only when the value is requested. Thus, if you read the parameter value in attributeChanged(), the expression will be evaluated using the current values of b and c. However, if b changes, the value of a will be updated only when next read. Only at that point will the attributeChanged() method be called. Thus, the actor will likely never see the change in b.2
By default, actors do not allow type changes on parameters. Once a parameter is given a value, then the value can change but not the type. Thus, the statement
meanTime.setTypeEquals(DoubleToken.class);
in figure 4.4 is actually redundant, since the type was fixed in the previous line,
meanTime = new Parameter(this, "meanTime", new DoubleToken(1.0));
However, some actors may wish to allow type changes in their parameters. Consider again the Scale actor, which is data polymorphic. The gain parameter can be of any type that supports multiplication, so type changes should be allowed.
To allow type changes in an actor, you must override the attributeTypeChanged() method. This method is defined in the based class for actors NamedObj, and by default, throws an exception. The method is called whenever the type of a parameter is changed. Consider figure 4.5, taken from the Scale actor. The first task of this method is to not throw an exception. This means that type changes are allowed. However, recall from figure 4.3 that a type constraint was specified that relates the input type to the gain. If the type changes, then the resolved type of the input may no longer be valid. The code in figure 4.5 notifies the director of this fact by calling its invalidateResolvedTypes() method.
We have seen already that the major task of the constructor is to create and configure ports and parameters. In addition, you may have noticed that it calls
super(container, name);
and that it declares that it throws NameDuplicationException and IllegalActionException. The latter is the most widely used exception, with most methods of actors having the possibility of throwing it. The former is thrown if the specified container already contains an actor with the specified name. For more details about exceptions, see the Kernel chapter.
All actors have the possibility of being cloned. A user interface, for example, may support "copy and paste" of actors, so the clone() method is provided to make this easy. A clone of an actor needs to be a new instance of the same class, with the same parameter values, but without any connections to other actors.
Consider the clone() method in figure 4.6, taken from the Scale actor. This method begins with
Scale newobj = (Scale)super.clone(ws);
The convention in Ptolemy II is that each clone method begins the same way, so that cloning works its way up the inheritance tree until it ultimately uses the clone() method of the Java Object class. That method performs what is called a "shallow copy," which is not sufficient for our purposes. In particular, members of the class that are references to other objects, including public members such as ports and parameters, are copied by copying the references. So for example, the public member gain in an instance of Scale is copied into a clone of this instance, but the member in the new actor will reference the same instance of Parameter as the original actor. This is obviously not what we want. It would mean, for example, that if you change the gain of the clone, then the gain of the original actor will also change.
Actors are derived from a base class called NamedObj (see "The Kernel" chapter), which has a clone() method that clones all the parameters (Parameter is derived from Attribute, and instance of NamedObj can contain any number of instances of Attribute). Thus, a clone of an instance of Scale contains its own gain parameter, but the public member gain still does not reference it. This situation is corrected using the following line:
newobj.gain = (Parameter)newobj.getAttribute("gain");
This looks up the parameter by name in the new object and sets the public member to refer to the correct parameter instance.
How type constraints are handled by clone() is a little bit subtle. Absolute type constraints on ports and parameters are carried automatically into the clone. Relative type constraints are not, however, because of the difficulty of ensuring that the other object being referred to in the relative constraint is the intended one. Thus, in figure 4.6, the clone() method repeats the relative type constraints that were specified in the constructor:
newobj.output.setTypeAtLeast(newobj.input);
newobj.output.setTypeAtLeast(newobj.gain);
Note that at no time during cloning is any constructor invoked, so it is necessary to repeat in the clone() method any initialization that will be automatically cloned.
Notice that in figure 4.6 the clone() method does nothing about the ports. This is because the clone method of the superclass, Transformer, takes care of this. Its clone method is:
public Object clone(Workspace ws) throws CloneNotSupportedException {
Transformer newobj = (Transformer)super.clone(ws);
newobj.input = (TypedIOPort)newobj.getPort("input");
newobj.output = (TypedIOPort)newobj.getPort("output");
return newobj;
}
Figure 4.1 shows a set of public methods called the action methods because they specify the action performed by the actor. By convention, these are given in alphabetical order in Ptolemy II Java files, but we will discuss them here in the order that they are invoked. The first to be invoked is the initialize() method, which is invoked exactly once before any other action method is invoked. Typically, this is used to initialize state variables in the actor.
After the initialize() method, the actor experiences some number of iterations, where an iteration is defined to be exactly one invocation of prefire(), some number of invocations of fire(), and at most one invocation of postfire().
The initialize() method of the Average actor is shown in figure
4.7. This data and domain-polymorphic actor computes the average of tokens that have arrived. To do so, it keeps a running sum in a private variable _sum
, and a running count of the number of tokens it has seen in a private variable _count
. Both of these variables are initialized in the initialize() method. Notice that the actor also calls super.initialize(), allowing the base class to perform any initialization it expects to perform. This is essential because one of the base classes initializes the ports. The actor will fail to run if you forget this step.
Occasionally, an actor wishes to produce an initial output token once at the beginning of an execution of a model. It is tempting to do that in the initialize() method. However, currently, this cannot be done because type resolution is done after the initialize() methods of all the actors have been run. This permits actors to take action in the initialize() method that may affect type resolution3. To get the same effect, we recommend the following pattern:
private boolean firstIteration;
public void initialize() throws IllegalActionException {
super.initialize();
firstIteration = true;
...
}
public boolean prefire() throws IllegalActionException {
if (firstIteration) output.send(0, token);
return super.prefire();
}
This seems tedious, but actors that need to do this are relatively rare. Nonetheless, we are contemplating extensions to the type system design to allow initial tokens to be produced in the initialize() method in the future (the type of the token will become a type constraint on the port).
The prefire() method is invoked exactly once per iteration. It returns a boolean that indicates to the director whether the actor wishes for firing to proceed. Thus, there are two possible uses for prefire(). First, if you need a method that is guaranteed to be invoked once per iteration of the actor in all domains, this is it. Some domains invoke the fire() method only once per iteration, but others will invoke it multiple times (searching for global convergence to a solution, for example). Second, if you wish to test an input to see whether you are ready to fire, then you can do that here.
Consider for example an actor that reads from trueInput if a private boolean variable _state
is true, and otherwise reads from falseInput. The prefire() method might look like this:
public boolean prefire() throws IllegalActionException {
if(_state) {
if(trueInput.hasToken()) return true;
} else {
if(falseInput.hasToken()) return true;
}
return false;
}
It is good practice to check the superclass in case it has some reason to decline to be fired. The above example becomes:
public boolean prefire() throws IllegalActionException {
if(_state) {
if(trueInput.hasToken()) return super.prefire();
} else {
if(falseInput.hasToken()) return super.prefire();
}
return false;
}
Figure 4.3 shows a typical fire() method. The fire() method reads inputs and produces outputs. It may also read the current parameter values, and the output may depend on them. Things to remember when writing fire() methods are:
The postfire() method has two tasks:
Consider the fire() and postfire() methods of the Average actor in figure 4.8. Notice that the persistent state variables _sum and _count are not updated in fire(). Instead, they are shadowed by _latestSum and _latestCount, and updated in postfire().
The return value of postfire() is a boolean that indicates to the director whether execution of the actor is complete. By convention, the director should avoid iterating further an actor that returns false. Consider the two examples shown in figure 4.10. These are base classes for source actors (those with no input ports).
SequenceSource is a base class for actors that output sequences. Its key feature is a parameter firingCountLimit, which specifies a limit on the number of iterations of the actor. When this limit is reached, the postfire() method returns false. Thus, this parameter can be used to define sources of finite sequences.
TimedSource is similar, except that instead of specifying a limit on the number of iterations, it specifies a limit on the current model time. When that limit is reached, the postfire() method returns false.
The wrapup() method is invoked exactly once at the end of an execution, unless an exception occurs during execution. It is used typically for displaying final results.
Actors whose behavior depends on current model time should implement the TimedActor interface. This is a marker interface (with no methods). Implementing this interface alerts the director that the actor depends on time. Domains that have no meaningful notion of time can reject such actors.
An actor can access current model time with the syntax
double currentTime = getDirector().getCurrentTime();
Notice that although the director has a public method setCurrentTime(), an actor should never use it. An actor does not have sufficiently global visibility to be able to do this. Typically, only another enclosing director will call this method.
An actor can request an invocation at a future time using the fireAt() method of the director. This method returns immediately (for a correctly implemented director). It takes two arguments, an actor and a time. The director is responsible for iterating the specified actor at the specified time. This method can be used to get a source actor started, and to keep it operating. In its initialize() method, it can call fireAt() with a zero time. Then in each invocation of postfire(), it calls fireAt() again. Notice that the call should be in postfire() not in fire() because a request for a future firing is persistent state.
Ptolemy software follows fairly rigorous conventions for code formatting. Although many of these conventions are arbitrary, the resulting consistency makes reading the code much easier, once you get used to the conventions. We recommend that if you extend Ptolemy II in any way, that you follow these conventions. To be included in future versions of Ptolemy II, the code must follow the conventions.
Nested statements should be indented 4 characters, as in:
if (container != null) {
Manager manager = container.getManager();
if (manager != null) {
manager.requestChange(change);
}
}
Closing brackets should be on a line by themselves, aligned with the beginning of the line that contains the open bracket. Tabs are 8 space characters, not a Tab character. The reason for this is that code becomes unreadable when the Tab character is interpreted differently by different viewers. Do not override this in your text editor. Long lines should be broken up into many small lines. Long strings can be broken up using the + operator in Java. Continuation lines are indented by 8 characters, as in the throws clause of the constructor in figure 4.1.
Right: foo(a, b);
Wrong: foo(a,b);
Use spaces around operators such as plus, minus, multiply, divide or equals signs:
Right: a = b + 1;
Wrong: a=b+1;
Right: for (i = 0; i < 10; i += 2)
Wrong: for (i=0; i<10; i+=2)
Comments should be complete sentences and complete thoughts, capitalized at the beginning and with a period at the end. Spelling and grammar should be correct. Comments should include honest information about the limitations of the object definition.
Class names, and only class names, are capitalized, and also have internal capitalization at word boundaries, as in AtomicActor. Method and member names are not capitalized, except at internal word boundaries, as in getContainer(). Protected or private members and methods are preceded by a leading underscore "_" as in _protectedMethod().
Static final constants should be in uppercase, with words separated by underscores, as in INFINITE_CAPACITY. A leading underscore should be used if the constant is protected or private.
Package names should be short and not capitalized, as in "de" for the discrete-event domain.
In Java, there is no limit to name sizes (as it should be). Do not hesitate to use long names.
A number of exceptions are provided in the ptolemy.kernel.util package. Use these exceptions when possible because they provide convenient arguments of type Nameable that identify the source of the exception by name in a consistent way.
A key decision you need to make is whether to use a compile-time exception or a run-time exception. A run-time exception is one that implements the RuntimeException interface. Run-time exceptions are more convenient in that they do not need to be explicitly declared by methods that throw them. However, this can have the effect of masking problems in the code.
The convention we follow is that a run-time exception is acceptable only if the cause of the exception can be tested for prior to calling the method. This is called a testable precondition. For example, if a particular method will fail if the argument is negative, and this fact is documented, then the method can throw a run-time exception if the argument is negative. On the other hand, consider a method that takes a string argument and evaluates it as an expression. The expression may be malformed, in which case an exception will be thrown. Can this be a run-time exception? No, because to determine whether the expression is malformed, you really need to invoke the evaluator. Making this a compile-time exception forces the caller to explicitly deal with the exception, or to declare that it too throws the same exception.
When throwing an exception, the detail message should be a complete sentence that includes a string that fully describes what caused the exception. For example
throw IllegalActionException(this,
"Cannot append an object of type: "
+ obj.getClass().getName() + ".");
There is no need to include in the message an identification of the "this" object passed as the first argument to the exception constructor. That object will be identified when the exception is reported to the user.
Javadoc is a program distributed with Java that generates HTML documentation files from Java source code files. Javadoc comments begin with "/**
" and end with "*/
". The comment immediately preceding a method, member, or class documents that member, method, or class. Ptolemy II classes include Javadoc documentation for all classes and all public and protected members and methods. Private members and methods need not be documented. Documentation can include embedded HTML formatting. For example, by convention, in actor documentation, we set in italics the names of the ports and parameters using the syntax
/** In this actor, inputs are read from the <i>input</i> port ... */
By convention, method names are set in the default font, but followed by empty parentheses, as in
/** The fire() method is called when ... */
The parentheses are empty even if the method takes arguments. The arguments are not shown. If the method is overloaded (has several versions with different argument sets), then the text of the documentation needs to distinguish which version is being used.
It is common in the Java community to use the following style for documenting methods:
/** Sets the expression of this variable.
* @param expr The expression for this variable.
*/
public void setExpression(String expr) {
...
}
We use instead the imperative tense, as in
/** Set the expression of this variable.
* @param expr The expression for this variable.
*/
public void setExpression(String expr) {
...
}
The reason we do this is that our sentence is a well-formed, grammatical English sentence, while the usual convention is not (it is missing the subject). Moreover, calling a method is a command "do this," so it seems reasonable that the documentation say "Do this."
The annotation for the arguments (the @param statement) is not a complete sentence, since it is usually presented in tabular format. However, we do capitalize it and end it with a period.
Exceptions that are thrown by a method need to be identified in the Javadoc comment. An @exception tag should read like this:
* @exception MyException If such and such occurs.
Notice that the body always starts with "If", not "Thrown if", or anything else. Just look at the Javadoc output to see why this occurs. In the case of an interface or base class that does not throw the exception, use the following:
* @exception MyException Not thrown in this base class. Derived
* classes may throw it if such and such happens.
The exception still has to be declared so that derived classes can throw it, so it needs to be documented as well.
The Javadoc program gives extensive diagnostics when run on a source file. Except for a huge number of rather meaningless messages that it insists on giving about serialization of objects that cannot be serialized, our policy is to format the comments until there are no Javadoc warnings.
The basic file structure that we use follows the outline in figure 4.1, preceded by a one-line description of the file and a copyright notice. The key points to note about this organization are:
Javadoc is a program that generates HTML documentation from Java files based on comments enclosed in "/** ... */".
2This design is debatable. Lazy evaluation has the key advantage that expressions are not immediately evaluated, and thus can be set in any order. In other words, a parameter can be given an expression value before the parameters it depends on are defined. This greatly simplifies storing and retrieving models in files, for examples. In principle, it is possible to notify all parameters whose values are expressions when the value of the expression might change, but this has the side effect of causing expressions to be evaluated immediately most of the time.
3The need for this is relatively rare, but important. Examples include higher-order functions, which are actors that replace themselves with other subsystems, and certain actors whose ports are not created at the time they are constructed, such as the Expression actor, but rather are added later.
ptII at eecs berkeley edu Copyright © 1998-1999, The Regents of the University of California. All rights reserved.