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}