001/* An actor that delays the input by the specified amount. 002 003 Copyright (c) 2009-2015 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.continuous.lib; 029 030import ptolemy.actor.lib.Transformer; 031import ptolemy.actor.util.CalendarQueue; 032import ptolemy.actor.util.Time; 033import ptolemy.actor.util.TimedEvent; 034import ptolemy.data.AbsentToken; 035import ptolemy.data.DoubleToken; 036import ptolemy.data.Token; 037import ptolemy.data.expr.Parameter; 038import ptolemy.data.type.BaseType; 039import ptolemy.kernel.CompositeEntity; 040import ptolemy.kernel.util.Attribute; 041import ptolemy.kernel.util.IllegalActionException; 042import ptolemy.kernel.util.NameDuplicationException; 043import ptolemy.kernel.util.Workspace; 044 045/////////////////////////////////////////////////////////////////// 046//// ContinuousTimeDelay 047 048/** 049 Delay the input by a specified amount of time. 050 051 <p>This actor is designed 052 to be used in timed domains such as DE and Continuous. It can also be used 053 in other domains, such as SR and SDF, but this will only be useful if the 054 delay value is a multiple of the period of those directors. The amount 055 of the delay is required to be non-negative and has a default value 1.0. 056 The input and output types are unconstrained, except that the output type 057 must be the same as that of the input. 058 <p> 059 This actor keeps a local FIFO queue of all input events that may be requested 060 by the director; an event expires and is removed from this queue if its 061 timestamp is older than the current time less the delay. The behavior of this 062 actor on each firing is to read a token from the input port (if present) and 063 generates an output that is either equal to or an approximation of the delayed 064 input signal. Output is absent if and only if no initial value is given and 065 the actor is fired before input is received, or before the transient delay 066 period has passed (i.e. model time is less than delay time). 067 <p> 068 Output is generated by the fire() method, and inputs are processed in postFire(). 069 <p> 070 Occasionally, this actor is useful with the 071 delay parameter set to 0.0. The time stamp of the output will 072 equal that of the input, but there is a "microstep" delay. 073 The continuous domain in Ptolemy II has a "super dense" model 074 of time, meaning that a signal from one actor to another can 075 contain multiple events with the same time stamp. These events 076 are "simultaneous," but nonetheless 077 have a well-defined sequential ordering determined by the order 078 in which they are produced. 079 If \textit{delay} is 0.0, then the actor does not generate output 080 in the current time microstep, but rather on a refiring at the 081 the same physical time but incremented timestep. 082 <p> 083 A consequence of this strategy is that this actor is 084 able to produce an output (or assert that there is no output) before the 085 input with the same time is known. Hence, it can be used to break 086 causality loops in feedback systems. The Continuous director will leverage this when 087 determining the fixed point behavior. It is sometimes useful to think 088 of this zero-valued delay as an infinitesimal delay. 089 090 @author Edward A. Lee, Jeff C. Jensen 091 @version $Id$ 092 @since Ptolemy II 8.0 093 @Pt.ProposedRating Yellow (eal) 094 @Pt.AcceptedRating Red (eal) 095 */ 096public class ContinuousTimeDelay extends Transformer { 097 // FIXME: delay cannot change during a run (more 098 // precisely... ignored until next initialize()). 099 100 // FIXME: implement solver step size control to capture periods of 101 // fine granularity; without this, though input events were stored 102 // with the same resolution as the solver deemed necessary, these 103 // delayed input events may be sampled sparsely if no other actor 104 // requires increased resolution at output time. If this actor 105 // uses fireAt() to force the solver to sample with the same 106 // resolution at which the input was generated, then subsequent 107 // inputs to the delay actor will arrive with this frequency 108 // regardless of whether or not this resolution is necessary. 109 110 /** Construct an actor with the specified container and name. 111 * Constrain that the output type to be the same as the input type. 112 * @param container The composite entity to contain this one. 113 * @param name The name of this actor. 114 * @exception IllegalActionException If the entity cannot be contained 115 * by the proposed container. 116 * @exception NameDuplicationException If the container already has an 117 * actor with this name. 118 */ 119 public ContinuousTimeDelay(CompositeEntity container, String name) 120 throws NameDuplicationException, IllegalActionException { 121 super(container, name); 122 123 delay = new Parameter(this, "delay"); 124 delay.setTypeEquals(BaseType.DOUBLE); 125 delay.setExpression("1.0"); 126 _delay = 1.0; 127 128 output.setTypeSameAs(input); 129 130 initialOutput = new Parameter(this, "initialOutput"); 131 initialOutput.setExpression(null); 132 133 //FIXME - this causes an exception if initialDelay = NULL 134 //output.setTypeAtLeast(initialOutput); 135 } 136 137 /////////////////////////////////////////////////////////////////// 138 //// ports and parameters //// 139 140 /** The amount of delay. The default for this parameter is 1.0. This 141 * parameter must contain a DoubleToken with a non-negative value, or an 142 * exception will be thrown when it is set. 143 */ 144 public Parameter delay; 145 146 /** Initial output of the delay actor. The default for this parameter 147 * is null, indicating no output will be generated until after input 148 * has been received. 149 */ 150 public Parameter initialOutput; 151 152 /////////////////////////////////////////////////////////////////// 153 //// public methods //// 154 155 /** If the attribute is <i>delay</i>, then ensure that the value 156 * is non-negative. 157 * <p>NOTE: the newDelay may be 0.0, which may change the causality 158 * property of the model. We leave the model designers to decide 159 * whether the zero delay is really what they want. 160 * @param attribute The attribute that changed. 161 * @exception IllegalActionException If the delay is negative. 162 */ 163 @Override 164 public void attributeChanged(Attribute attribute) 165 throws IllegalActionException { 166 if (attribute == delay) { 167 double newDelay = ((DoubleToken) delay.getToken()).doubleValue(); 168 169 if (newDelay < 0.0) { 170 throw new IllegalActionException(this, 171 "Cannot have negative delay: " + newDelay); 172 } else { 173 _delay = newDelay; 174 } 175 } else { 176 super.attributeChanged(attribute); 177 } 178 } 179 180 /** Clone the actor into the specified workspace. Set a type 181 * constraint that the output type is the same as the that of input. 182 * @param workspace The workspace for the new object. 183 * @return A new actor. 184 * @exception CloneNotSupportedException If a derived class has 185 * has an attribute that cannot be cloned. 186 */ 187 @Override 188 public Object clone(Workspace workspace) throws CloneNotSupportedException { 189 ContinuousTimeDelay newObject = (ContinuousTimeDelay) super.clone( 190 workspace); 191 newObject.output.setTypeSameAs(newObject.input); 192 return newObject; 193 } 194 195 /** Declare that the <i>output</i> 196 * does not depend on the <i>input</i> in a firing. 197 * @exception IllegalActionException If the causality interface 198 * cannot be computed. 199 * @see #getCausalityInterface() 200 */ 201 @Override 202 public void declareDelayDependency() throws IllegalActionException { 203 _declareDelayDependency(input, output, _delay); 204 } 205 206 /* 207 * Consume input (if available) and produce output for this actor, either 208 * by using the initial value (during transient behavior), or finding or 209 * interpolating the delayed signal from input buffers. 210 * 211 * The goal is to determine the input signal at the current time less delay. 212 * We refer to this point as the center point. If the center point occurs 213 * during the transient period of this actor and an initial value is 214 * present, then output the initial value. Otherwise, if the input buffer 215 * contains the center point, we simply output the recorded token and 216 * discard it from the input buffer. 217 * 218 * If the center point was not recorded in the input buffer, then find the 219 * most recent event that occurred before the center point, and label it the 220 * left point. Similarly, we search for the nearest point of our input 221 * buffer that occurred after the center point, and label it the right 222 * point. 223 * 224 * By discarding expired tokens (tokens which will no longer be requested by 225 * the director), the center point falls between the most recently discarded 226 * event and the first element of the input buffer. Hence the left point is 227 * the most recently discarded event, and the right point is the first 228 * element of the input buffer. 229 * 230 * If the time of the center point is equal to the current time, then the 231 * input is delayed by a microstep. The right point retains the input value 232 * and is output on a refire at the same physical time. The left point is 233 * ignored. 234 * 235 * @exception IllegalActionException 236 */ 237 @Override 238 public void fire() throws IllegalActionException { 239 super.fire(); 240 241 Time currentTime = getDirector().getModelTime(); 242 Time centerTime = currentTime.subtract(_delay); 243 TimedEvent leftEvent = null; 244 TimedEvent rightEvent = null; 245 _currentOutput = null; 246 247 // Consume input; if input is absent, do not add an event to the output queue, 248 // as it will force the scheduler to fire this actor to produce an absent output. 249 // Otherwise, the solver step size is prevented from increasing as delayed 250 // absent events are present everywhere in the signal. This would result in 251 // monotonically decreasing solver step size that quickly converges to the minimum 252 // allowed step size, effectively bypassing the solver logic and slowing simulation. 253 if (input.isKnown(0) && input.hasToken(0)) { 254 Token inputToken = input.get(0); 255 if (!inputToken.equals(AbsentToken.ABSENT)) { 256 _inputBuffer.put(new TimedEvent(currentTime, inputToken)); 257 } else { 258 inputToken = null; 259 } 260 } 261 262 // Discard expired input events that will never be considered 263 // by the solver. These are events that have timestamps before 264 // the current time less delay. 265 while (_inputBuffer.size() > 0) { 266 Time earliestEventTime = ((TimedEvent) _inputBuffer 267 .get()).timeStamp; 268 269 //Expired event 270 if (earliestEventTime.compareTo(centerTime) < 0) { 271 _discarded = (TimedEvent) _inputBuffer.take(); 272 } 273 //Earliest event is valid, so stop searching 274 else { 275 break; 276 } 277 } 278 279 //Record the left event; this is the most recently discarded input token. 280 // Note that if we have not seen input, but we have an initial value, the 281 // value was put to the input queue in the initialize() method, so the left 282 // point will be the initial value at time 0. 283 leftEvent = _discarded; 284 285 //Record the right event; because expired events are discarded from the 286 // input queue, the right event is always the first element of the queue 287 if (_inputBuffer.size() > 0) { 288 rightEvent = (TimedEvent) _inputBuffer.get(); 289 } 290 291 // If the input signal was recorded at the center point, output it here, 292 // and remove the event from the input queue to prevent refiring 293 if (rightEvent != null && rightEvent.timeStamp.equals(centerTime)) { 294 _currentOutput = (Token) rightEvent.contents; 295 _discarded = (TimedEvent) _inputBuffer.take(); 296 } 297 // If the current time is less than the delay time, output the initial value 298 else if (currentTime.compareTo(new Time(getDirector(), _delay)) < 0) { 299 _currentOutput = initialOutput.getToken(); 300 } 301 // If the current time is equal to the center time (delay=0), but the event was 302 // not on the input queue, then we have not read the input. Output nothing now, 303 // and postFire() will read the input and request a refiring at the current time; 304 // this will force the director to increase its microstep. 305 else if (currentTime.equals(centerTime)) { 306 //Do nothing 307 } 308 // If we have a left point, we construct the center point here 309 else if (leftEvent != null) { 310 //If we have a right point, then we interpolate the center point 311 if (rightEvent != null) { 312 _currentOutput = linearInterpolate(leftEvent, rightEvent); 313 } 314 // If we have a left point but no right point, we assume the value has not changed. 315 else { 316 //FIXME: Is this the best solution? 317 _currentOutput = (Token) leftEvent.contents; 318 } 319 } 320 // Otherwise, we did not record the event at the center time, have not seen input, 321 // and do not have a left point (e.g. no initial value). We cannot generate output. 322 323 // Produce output 324 if (_currentOutput != null && !output.isKnown(0)) { 325 output.send(0, _currentOutput); 326 // In the case where delay is zero (currentTime = centerTime) we have refired in 327 // order to output a token with an increased microstep. After this token is sent, 328 // it needs to be removed from the input buffer. 329 if (currentTime.equals(centerTime)) { 330 _discarded = (TimedEvent) _inputBuffer.take(); 331 } 332 } 333 334 //FIXME: What happens if two events in input buffer 335 // have the same physical timestamp? We need to interpolate 336 // earlier points by using the matching timestamp, 337 // and then output both events at some point. 338 // How does this affect the left point in interpolation? 339 // 340 // To maintain continuity principles, the current implementation 341 // may be correct; the events in the input buffer are ordered 342 // by microstep index, so that in interpolation, the event with 343 // the earliest time index is always used. This will preserve 344 // left continuity of an input signal. 345 } 346 347 /** Initialize the states of this actor. Place initial output 348 * token on the input buffer. 349 * @exception IllegalActionException If a derived class throws it. 350 */ 351 @Override 352 public void initialize() throws IllegalActionException { 353 Token initialToken = initialOutput.getToken(); 354 super.initialize(); 355 _currentOutput = null; 356 _inputBuffer = new CalendarQueue(new TimedEvent.TimeComparator()); 357 _discarded = null; 358 _nextFireAt = new Time(getDirector(), 0); 359 360 //Place the initial token in the input buffer 361 if (initialToken != null) { 362 Time modelStartTime = getDirector().getModelStartTime(); 363 _inputBuffer.put(new TimedEvent(modelStartTime, initialToken)); 364 } 365 } 366 367 /** Schedule the next output event. 368 * @exception IllegalActionException If thrown by fireAt() or 369 * by the superclass. 370 */ 371 @Override 372 public boolean postfire() throws IllegalActionException { 373 // Schedule the next output event. This event may fire at the same 374 // physical time in the case of zero delay or simultaneous events. 375 if (_inputBuffer.size() > 0) { 376 TimedEvent nextEvent = (TimedEvent) _inputBuffer.get(); 377 Time nextOutputTime = nextEvent.timeStamp.add(_delay); 378 Time currentTime = getDirector().getModelTime(); 379 380 // If the next output time is now, then there are additional tokens to output at the 381 // current physical time, so a refire is requested. If the next output time is in the 382 // future, first ensure we have not already scheduled this actor to fire. 383 if (nextOutputTime.equals(currentTime) 384 || !nextOutputTime.equals(_nextFireAt)) { 385 getDirector().fireAt(this, nextOutputTime); 386 _nextFireAt = nextOutputTime; 387 } 388 } 389 390 return super.postfire(); 391 } 392 393 /** Override the base class to declare that the actor is nonstrict 394 * if it has an initial value token. 395 */ 396 @Override 397 public boolean isStrict() { 398 // //FIXME: Does strictness depend on presence of an initial value? 399 // return false; 400 try { 401 Token t = initialOutput.getToken(); 402 return t == null; 403 } catch (IllegalActionException e) { 404 return true; 405 } 406 } 407 408 /////////////////////////////////////////////////////////////////// 409 //// protected methods //// 410 411 /** 412 * Linear interpolate between previous and current input. 413 * 414 * <p>To interpolate, we determine the slope between the left and right 415 * interpolation points, and multiply this by time gap between the left 416 * point and the current time. This estimates the amount by which the input 417 * signal has changed between the left and center points. We add this change 418 * to the value of the left point to estimate the value of the center point. 419 * 420 * @param leftEvent The left event. 421 * @param rightEvent the right event. 422 * @return The linear interpolation. 423 * @exception IllegalActionException If thrown by arithmetic operations 424 * on the events 425 */ 426 protected Token linearInterpolate(TimedEvent leftEvent, 427 TimedEvent rightEvent) throws IllegalActionException { 428 Time centerTime = getDirector().getModelTime().subtract(_delay); 429 430 //time gap (run) between left and right events 431 Token slope = new DoubleToken(rightEvent.timeStamp 432 .subtract(leftEvent.timeStamp).getDoubleValue()); 433 434 //slope = rise / run 435 slope = ((Token) rightEvent.contents) 436 .subtract((Token) leftEvent.contents).divide(slope); 437 438 //leftEvent + estimated rise from leftEvent to centerEvent 439 return ((Token) leftEvent.contents).add(slope.multiply(new DoubleToken( 440 centerTime.subtract(leftEvent.timeStamp).getDoubleValue()))); 441 } 442 443 /////////////////////////////////////////////////////////////////// 444 //// protected variables //// 445 446 /** Current output. */ 447 protected Token _currentOutput; 448 449 /** The amount of delay. */ 450 protected double _delay; 451 452 /** A local event queue to store input tokens, sorted by input time. */ 453 protected CalendarQueue _inputBuffer; 454 455 /** Holds the most recently discarded event from the input buffer. */ 456 protected TimedEvent _discarded; 457 458 /** Records the next scheduled fireAt() call, so that we do not request more than 459 * one fireAt() call for a given input event. 460 */ 461 protected Time _nextFireAt; 462}