/* An actor that implements a discrete PID controller.
Copyright (c) 1998-2005 The Regents of the University of California.
All rights reserved.
Permission is hereby granted, without written agreement and without
license or royalty fees, to use, copy, modify, and distribute this
software and its documentation for any purpose, provided that the above
copyright notice and the following two paragraphs appear in all copies
of this software.
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
ENHANCEMENTS, OR MODIFICATIONS.
PT_COPYRIGHT_VERSION_2
COPYRIGHTENDKEY
*/
package ptolemy.domains.de.lib;
import ptolemy.actor.TypedIOPort;
import ptolemy.actor.util.Time;
import ptolemy.actor.util.TimedEvent;
import ptolemy.data.DoubleToken;
import ptolemy.data.expr.Parameter;
import ptolemy.data.type.BaseType.DoubleType;
import ptolemy.kernel.CompositeEntity;
import ptolemy.kernel.util.Attribute;
import ptolemy.kernel.util.IllegalActionException;
import ptolemy.kernel.util.NameDuplicationException;
import ptolemy.kernel.util.Workspace;
//////////////////////////////////////////////////////////////////////////
//// PID
/**
Generate PID output for a given input. The output is the sum of a proportional
gain (P), discrete integration (I), and discrete derivative (D).
<p>
The proportional component of the output is immediately available, such that
yp[n]=Kp*x[n], where <i>yp</i> is the proportional component of the output,
<i>Kp</i> is the proportional gain, and <i>x</i> is the input value.
<p>
For integral gain, the output is available after two input symbols have been
received, such that yi[n]=Ki*(yi[n-1]+(x[n] + x[n-1]))*dt[n]/2, where <i>yi</i>
is the integral component of the output, <i>Ki</i> is the integral gain, and
<i>dt[n]</i> is the time differential between input events x[n] and x[n-1].
<p>
For derivative gain, the output is available after two input symbols have been
received, such that yd[n] = Kd*(x[n]-x[n-1])/dt, where <i>yd</i> is the
derivative component of the output, <i>Kd</i> is the derivative gain, and
<i>dt</i> is the time differential between input events events x[n] and x[n-1].
<p>
The output of this actor is constrained to be a double, and input must be castable
to a double. If the input signal is not left-continuous and the derivative constant
is nonzero, then this actor will throw an exception as the derivative will be either infinite
or undefined. If the derivative constant is zero, then this actor may recieve
discontinuous input.
<p>
y[0]=Kp*x[0]
<br>y[n] = yp[n] + yi[n] + yd[n]
<br>y[n] = Kp*x[n] + Ki*sum{x=1}{n}{(x[n]+x[n-1])/2*dt[n]} + Kd*(x[n]-x[n-1]/dt[n])
<p>
In postfire(), if an event is present on the <i>reset</i> port, this
actor resets to its initial state, where integral and derivative components
of output will not be present until two subsequent inputs have been consumed.
This is useful if the input signal is switched on and off, in which case the
time gap between events becomes large and would otherwise effect the value of
the derivative (for one sample) and the integral.
<p>
@author Jeff C. Jensen
@version $Id: PID.java 39805 2005-10-28 20:19:33Z cxh $
@since Ptolemy II 8.1
@see ptolemy.domains.de.lib.Integrator
@see ptolemy.domains.de.lib.Derivative
*/
public class PID extends DETransformer {
/** Construct an actor with the given container and name.
* @param container The container.
* @param name The name of this actor.
* @exception IllegalActionException If the actor cannot be contained
* by the proposed container.
* @exception NameDuplicationException If the container already has an
* actor with this name.
*/
public PID(CompositeEntity container, String name)
throws NameDuplicationException, IllegalActionException {
super(container, name);
reset = new TypedIOPort(this, "reset", true, false);
reset.setMultiport(true);
input.setTypeAtMost(DoubleType.DOUBLE);
output.setTypeEquals(DoubleType.DOUBLE);
Kp = new Parameter(this, "Kp");
Kp.setExpression("1.0");
Ki = new Parameter(this, "Ki");
Ki.setExpression("0.0");
Kd = new Parameter(this, "Kd");
Kd.setExpression("0.0");
}
///////////////////////////////////////////////////////////////////
//// public methods ////
/** Clone the actor into the specified workspace. This calls the
* base class and then sets the ports.
* @param workspace The workspace for the new object.
* @return A new actor.
* @exception CloneNotSupportedException If a derived class has
* has an attribute that cannot be cloned.
*/
public Object clone(Workspace workspace) throws CloneNotSupportedException {
PID newObject = (PID) super.clone(workspace);
newObject.input.setTypeAtMost(DoubleType.DOUBLE);
newObject.output.setTypeEquals(DoubleType.DOUBLE);
// This is not strictly needed (since it is always recreated
// in preinitialize) but it is safer.
newObject._lastInput = null;
newObject._currentInput = null;
newObject._accumulated = new DoubleToken(0.0);
return newObject;
}
/** If the attribute is <i>Kp</i>, <i>Ki</i>, or <i>Kd</i> then ensure
* that the value is numeric.
* @param attribute The attribute that changed.
* @exception IllegalActionException If the value is non-numeric.
*/
public void attributeChanged(Attribute attribute) throws IllegalActionException {
if (attribute == Kp || attribute == Ki || attribute == Kd) {
try {
Parameter value = (Parameter)attribute;
if(value.getToken() == null || ((DoubleToken)value.getToken()).isNil()){
throw new IllegalActionException(this, "Must have a numeric value for gains.");
}
} catch (ClassCastException e) {
throw new IllegalActionException(this, "Gain values must be castable to a double.");
}
} else {
super.attributeChanged(attribute);
}
}
/** Reset to indicate that no input has yet been seen.
* @exception IllegalActionException If the parent class throws it.
*/
public void initialize() throws IllegalActionException {
super.initialize();
_lastInput = null;
_accumulated = new DoubleToken(0.0);
}
/** Consume at most one token from the <i>input</i> port and output
* the PID control. If there has been no previous iteration, only
* proportional output is generated.
* If there is no input, then produce no output.
* @exception IllegalActionException If addition, multiplication,
* subtraction, or division is not supported by the supplied tokens.
*/
public void fire() throws IllegalActionException {
super.fire();
//Consume input, generate output only if input provided
if (input.hasToken(0)) {
Time currentTime = getDirector().getModelTime();
DoubleToken currentToken = (DoubleToken)input.get(0);
_currentInput = new TimedEvent(currentTime, currentToken);
//Add proportional component to controller output
DoubleToken currentOutput = (DoubleToken)currentToken.multiply(Kp.getToken());
//If a previous input was given, then add integral and derivative components
if(_lastInput != null){
DoubleToken lastToken = (DoubleToken)_lastInput.contents;
Time lastTime = _lastInput.timeStamp;
DoubleToken timeGap = new DoubleToken(currentTime.subtract(lastTime).getDoubleValue());
//If the timeGap is zero, then we have received a simultaneous event. If the
// value of the input has not changed, then we can ignore this input, as a control
// signal was already generated. However if the value has changed, then the signal
// is discontinuous and we should throw an exception unless derivative control
// is disabled (Kd=0).
if(timeGap.equals(0)){
if(!Kd.equals(0) && !currentToken.equals(lastToken)){
throw new IllegalActionException("PID controller recevied discontinuous input.");
}
}
// Otherwise, the signal is continuous and we add integral and derivative components
else{
if(!Ki.getExpression().equals(0)){
//Calculate integral component and accumulate
_accumulated = (DoubleToken) _accumulated.add(currentToken.add(lastToken)
.multiply(timeGap)
.multiply(new DoubleToken(0.5)));
//Add integral component to controller output
currentOutput = (DoubleToken) currentOutput.add(_accumulated.multiply(Ki.getToken()));
}
//Add derivative component to controller output
if(!Kd.equals(0)){
currentOutput = (DoubleToken) currentOutput.add(
currentToken.subtract(lastToken)
.divide(timeGap)
.multiply(Kd.getToken()));
}
}
}
output.broadcast(currentOutput);
}
}
/** Record the most recent input as the latest input. If a reset
* event has been received, process it here.
* @exception IllegalActionException If the base class throws it.
*/
public boolean postfire() throws IllegalActionException {
//If reset port is connected and has a token, reset state.
if(reset.getWidth() > 0){
if(reset.hasToken(0)){
//Consume reset token
reset.get(0);
//Reset the current input
_currentInput = null;
//Reset accumulation
_accumulated = new DoubleToken(0.0);
}
}
_lastInput = _currentInput;
return super.postfire();
}
///////////////////////////////////////////////////////////////////
//// ports and parameters ////
/** The reset port, which has undeclared type. If this port
* receives a token, this actor resets to its initial state,
* and no output is generated until two inputs have been received.
*/
public TypedIOPort reset;
/** Proportional gain of the controller. Default value is 1.0.
* */
public Parameter Kp;
/** Integral gain of the controller. Default value is 0.0,
* which disables integral control.
* */
public Parameter Ki;
/** Derivative gain of the controller. Default value is 0.0, which disables
* derivative control. If Kd=0.0, this actor can receive discontinuous
* signals as input; otherwise, if Kd is nonzero and a discontinuous signal
* is received, an exception will be thrown.
*/
public Parameter Kd;
///////////////////////////////////////////////////////////////////
//// private members ////
private TimedEvent _currentInput;
private TimedEvent _lastInput;
private DoubleToken _accumulated;
}