001/* An actor that implements a discrete PID controller.
002
003 Copyright (c) 1998-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.domains.de.lib;
029
030import ptolemy.actor.TypedIOPort;
031import ptolemy.actor.util.Time;
032import ptolemy.actor.util.TimedEvent;
033import ptolemy.data.DoubleToken;
034import ptolemy.data.expr.Parameter;
035import ptolemy.data.type.BaseType;
036import ptolemy.kernel.CompositeEntity;
037import ptolemy.kernel.util.Attribute;
038import ptolemy.kernel.util.IllegalActionException;
039import ptolemy.kernel.util.NameDuplicationException;
040import ptolemy.kernel.util.Workspace;
041import ptolemy.math.Complex;
042
043///////////////////////////////////////////////////////////////////
044//// PID
045
046/**
047 Generate PID output for a given input. The output is the sum of a proportional
048 gain (P), discrete integration (I), and discrete derivative (D).
049 <p>
050 The proportional component of the output is immediately available, such that
051 yp[n]=Kp*x[n], where <i>yp</i> is the proportional component of the output,
052 <i>Kp</i> is the proportional gain, and <i>x</i> is the input value.
053 <p>
054 For integral gain, the output is available after two input symbols have been
055 received, such that yi[n]=Ki*(yi[n-1]+(x[n] + x[n-1])*dt[n]/2), where <i>yi</i>
056 is the integral component of the output, <i>Ki</i> is the integral gain, and
057 <i>dt[n]</i> is the time differential between input events x[n] and x[n-1].
058 <p>
059 For derivative gain, the output is available after two input symbols have been
060 received, such that yd[n] = Kd*(x[n]-x[n-1])/dt, where <i>yd</i> is the
061 derivative component of the output, <i>Kd</i> is the derivative gain, and
062 <i>dt</i> is the time differential between input events events x[n] and x[n-1].
063 <p>
064 The output of this actor is constrained to be a double, and input must be castable
065 to a double. If the input signal is not left-continuous and the derivative constant
066 is nonzero, then this actor will throw an exception as the derivative will be either infinite
067 or undefined. If the derivative constant is zero, then this actor may receive
068 discontinuous input.
069 <p>
070 y[0]=Kp*x[0]
071 <br>y[n] = yp[n] + yi[n] + yd[n]
072 <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])
073 <p>
074 In postfire(), if an event is present on the <i>reset</i> port, this
075 actor resets to its initial state, where integral and derivative components
076 of output will not be present until two subsequent inputs have been consumed.
077 This is useful if the input signal is switched on and off, in which case the
078 time gap between events becomes large and would otherwise effect the value of
079 the derivative (for one sample) and the integral.
080 <p>
081 @author Jeff C. Jensen
082 @version $Id$
083 @since Ptolemy II 8.0
084 @see ptolemy.domains.de.lib.Integrator
085 @see ptolemy.domains.de.lib.Derivative
086 */
087public class PID extends DETransformer {
088
089    /** Construct an actor with the given container and name.
090     *  @param container The container.
091     *  @param name The name of this actor.
092     *  @exception IllegalActionException If the actor cannot be contained
093     *   by the proposed container.
094     *  @exception NameDuplicationException If the container already has an
095     *   actor with this name.
096     */
097    public PID(CompositeEntity container, String name)
098            throws NameDuplicationException, IllegalActionException {
099        super(container, name);
100        reset = new TypedIOPort(this, "reset", true, false);
101        reset.setMultiport(true);
102        input.setTypeAtMost(BaseType.DOUBLE);
103        output.setTypeEquals(BaseType.DOUBLE);
104        Kp = new Parameter(this, "Kp");
105        Kp.setExpression("1.0");
106        Ki = new Parameter(this, "Ki");
107        Ki.setExpression("0.0");
108        Kd = new Parameter(this, "Kd");
109        Kd.setExpression("0.0");
110    }
111
112    ///////////////////////////////////////////////////////////////////
113    ////                     ports and parameters                  ////
114
115    /** The reset port, which has undeclared type. If this port
116     *  receives a token, this actor resets to its initial state,
117     *  and no output is generated until two inputs have been received.
118     */
119    public TypedIOPort reset;
120
121    /** Proportional gain of the controller. Default value is 1.0.
122     * */
123    public Parameter Kp;
124
125    /** Integral gain of the controller. Default value is 0.0,
126     *  which disables integral control.
127     * */
128    public Parameter Ki;
129
130    /** Derivative gain of the controller. Default value is 0.0, which disables
131     *  derivative control. If Kd=0.0, this actor can receive discontinuous
132     *  signals as input; otherwise, if Kd is nonzero and a discontinuous signal
133     *  is received, an exception will be thrown.
134     */
135    public Parameter Kd;
136
137    ///////////////////////////////////////////////////////////////////
138    ////                         public methods                    ////
139    /** Clone the actor into the specified workspace. This calls the
140     *  base class and then sets the ports.
141     *  @param workspace The workspace for the new object.
142     *  @return A new actor.
143     *  @exception CloneNotSupportedException If a derived class has
144     *   has an attribute that cannot be cloned.
145     */
146    @Override
147    public Object clone(Workspace workspace) throws CloneNotSupportedException {
148        PID newObject = (PID) super.clone(workspace);
149
150        newObject.input.setTypeAtMost(BaseType.DOUBLE);
151        newObject.output.setTypeEquals(BaseType.DOUBLE);
152
153        // This is not strictly needed (since it is always recreated
154        // in preinitialize) but it is safer.
155        newObject._lastInput = null;
156        newObject._currentInput = null;
157        newObject._accumulated = new DoubleToken(0.0);
158
159        return newObject;
160    }
161
162    /** If the attribute is <i>Kp</i>, <i>Ki</i>, or <i>Kd</i> then ensure
163     *  that the value is numeric.
164     *  @param attribute The attribute that changed.
165     *  @exception IllegalActionException If the value is non-numeric.
166     */
167    @Override
168    public void attributeChanged(Attribute attribute)
169            throws IllegalActionException {
170        if (attribute == Kp || attribute == Ki || attribute == Kd) {
171            try {
172                Parameter value = (Parameter) attribute;
173                if (value.getToken() == null
174                        || ((DoubleToken) value.getToken()).isNil()) {
175                    throw new IllegalActionException(this,
176                            "Must have a numeric value for gains.");
177                }
178            } catch (ClassCastException e) {
179                throw new IllegalActionException(this,
180                        "Gain values must be castable to a double.");
181            }
182        } else {
183            super.attributeChanged(attribute);
184        }
185    }
186
187    /** Reset to indicate that no input has yet been seen.
188     *  @exception IllegalActionException If the parent class throws it.
189     */
190    @Override
191    public void initialize() throws IllegalActionException {
192        super.initialize();
193        _lastInput = null;
194        _accumulated = new DoubleToken(0.0);
195    }
196
197    /** Consume at most one token from the <i>input</i> port and output
198     *  the PID control. If there has been no previous iteration, only
199     *  proportional output is generated.
200     *  If there is no input, then produce no output.
201     *  @exception IllegalActionException If addition, multiplication,
202     *  subtraction, or division is not supported by the supplied tokens.
203     */
204    @Override
205    public void fire() throws IllegalActionException {
206        super.fire();
207
208        // Consume input, generate output only if input provided.
209        if (input.hasToken(0)) {
210            Time currentTime = getDirector().getModelTime();
211            DoubleToken currentToken = (DoubleToken) input.get(0);
212            _currentInput = new TimedEvent(currentTime, currentToken);
213
214            // Add proportional component to controller output.
215            DoubleToken currentOutput = (DoubleToken) currentToken
216                    .multiply(Kp.getToken());
217
218            // If a previous input was given, then add integral and
219            // derivative components.
220            if (_lastInput != null) {
221                DoubleToken lastToken = (DoubleToken) _lastInput.contents;
222                Time lastTime = _lastInput.timeStamp;
223                DoubleToken timeGap = new DoubleToken(
224                        currentTime.subtract(lastTime).getDoubleValue());
225
226                //If the timeGap is zero, then we have received a
227                // simultaneous event. If the value of the input has
228                // not changed, then we can ignore this input, as a
229                // control signal was already generated. However if
230                // the value has changed, then the signal is
231                // discontinuous and we should throw an exception
232                // unless derivative control is disabled (Kd=0).
233
234                if (timeGap.isCloseTo(DoubleToken.ZERO, Complex.EPSILON)
235                        .booleanValue()) {
236                    if (!((DoubleToken) Kd.getToken())
237                            .isCloseTo(DoubleToken.ZERO, Complex.EPSILON)
238                            .booleanValue()
239                            && !currentToken.equals(lastToken)) {
240                        throw new IllegalActionException(this,
241                                "PID controller recevied discontinuous input.");
242                    }
243                }
244                // Otherwise, the signal is continuous and we add
245                // integral and derivative components.
246                else {
247                    if (!((DoubleToken) Ki.getToken())
248                            .isCloseTo(DoubleToken.ZERO, Complex.EPSILON)
249                            .booleanValue()) {
250                        //Calculate integral component and accumulate
251                        _accumulated = (DoubleToken) _accumulated.add(
252                                currentToken.add(lastToken).multiply(timeGap)
253                                        .multiply(new DoubleToken(0.5)));
254                        // Add integral component to controller output.
255                        currentOutput = (DoubleToken) currentOutput
256                                .add(_accumulated.multiply(Ki.getToken()));
257                    }
258
259                    // Add derivative component to controller output.
260                    if (!((DoubleToken) Kd.getToken())
261                            .isCloseTo(DoubleToken.ZERO, Complex.EPSILON)
262                            .booleanValue()) {
263                        currentOutput = (DoubleToken) currentOutput.add(
264                                currentToken.subtract(lastToken).divide(timeGap)
265                                        .multiply(Kd.getToken()));
266                    }
267                }
268            }
269
270            output.broadcast(currentOutput);
271        }
272    }
273
274    /** Record the most recent input as the latest input. If a reset
275     *  event has been received, process it here.
276     *  @exception IllegalActionException If the base class throws it.
277     */
278    @Override
279    public boolean postfire() throws IllegalActionException {
280        //If reset port is connected and has a token, reset state.
281        if (reset.getWidth() > 0) {
282            if (reset.hasToken(0)) {
283                // Consume reset token.
284                reset.get(0);
285
286                // Reset the current input.
287                _currentInput = null;
288
289                // Reset accumulation.
290                _accumulated = new DoubleToken(0.0);
291            }
292        }
293        _lastInput = _currentInput;
294        return super.postfire();
295    }
296
297    ///////////////////////////////////////////////////////////////////
298    ////                         private members                   ////
299    private TimedEvent _currentInput;
300
301    private TimedEvent _lastInput;
302
303    private DoubleToken _accumulated;
304}