001/* Generate discrete events at prespecified time instants.
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.actor.lib;
029
030import ptolemy.actor.Director;
031import ptolemy.actor.SuperdenseTimeDirector;
032import ptolemy.actor.TypedIOPort;
033import ptolemy.actor.parameters.PortParameter;
034import ptolemy.actor.util.Time;
035import ptolemy.data.ArrayToken;
036import ptolemy.data.BooleanToken;
037import ptolemy.data.DoubleToken;
038import ptolemy.data.Token;
039import ptolemy.data.expr.Parameter;
040import ptolemy.data.expr.SingletonParameter;
041import ptolemy.data.type.ArrayType;
042import ptolemy.data.type.BaseType;
043import ptolemy.kernel.CompositeEntity;
044import ptolemy.kernel.util.Attribute;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.NameDuplicationException;
047import ptolemy.kernel.util.StringAttribute;
048import ptolemy.kernel.util.Workspace;
049
050///////////////////////////////////////////////////////////////////
051//// DiscreteClock
052
053/**
054 This actor produces a periodic signal, a sequence of events at
055 regularly spaced intervals.
056 At the beginning of each time interval of length given by <i>period</i>,
057 starting from the time at which initialize() is invoked,
058 this actor initiates a sequence of output events with values given by
059 <i>values</i> and offset into the period given by <i>offsets</i>.
060 These parameters contain arrays, which are required to have the same length.
061 The <i>offsets</i> array contains doubles, which
062 must be nondecreasing and nonnegative,
063 or an exception will be thrown when it is set.
064 If any entry is greater than the <i>period</i>
065 then the corresponding output will never be produced.
066 To get a finite sequence of events that is not periodic,
067 just set <i>period</i> to Infinity.
068 Alternatively, you can provide
069 a finite <i>stopTime</i>. Upon reaching that stop time,
070 postfire() returns false, which requests that the director
071 not fire this actor again.
072 The clock can also be started and stopped repeatedly
073 during an execution. A token at the <i>start</i> input will start the clock
074 at the beginning of a period. A token
075 at the <i>stop</i> input will stop the clock, if it is still running.
076 If both <i>start</i> and <i>stop</i> are received simultaneously, then
077 the clock will be stopped.
078 <p>
079 The <i>values</i> parameter by default
080 contains the array {1}.  The default
081 <i>offsets</i> array is {0.0}.
082 The default period is 1.0.
083 <p>
084 The type of the output can be any token type. This type is inferred
085 from the element type of the <i>values</i> parameter.
086 <p>
087 For example, if <i>values</i> = {1, 2, 3},
088 <i>offsets</i> = {0.1, 0.2, 0.3},
089 <i>period</i> = 1.0,
090 and the actor is initialized at time 0.0, then
091 it will produce outputs with value 1 at all times
092 <i>n</i> + 0.1, outputs with value 2 at all times
093 <i>n</i> + 0.2, and outputs with value 3 at all times
094 <i>n</i> + 0.3, for all non-negative integers <i>n</i>.
095 <p>
096 If the actor is not fired by the enclosing director at the time
097 of the next expected output, then it will stop producing outputs.
098 This should not occur. If it does, it is a bug in the director.
099 <p>
100 If the director that this is used with supports superdense time
101 (like DE, Continuous), then the outputs are normally produced at microstep
102 index 1. The reason for producing outputs at index 1
103 is to maintain continuity in continuous-time models.
104 Specifically, if the signal is absent prior to an output time,
105 then it should be absent at index 0 of the time at which it will
106 produce the next output. There are two exceptions. If
107 two or more offsets have the same value, then each output
108 at the same time is produced at superdense time index one greater
109 than the previous output. Also, if an expected output has not been
110 produced by the expected index, then it will be produced at the
111 next available index. E.g., the very first output may be produced
112 at a superdense index greater than zero if the director's index is
113 greater than zero when this actor is initialized. This can happen,
114 for example, if this clock is in a refinement in a modal model,
115 and the modal model enters that mode with a reset transition.
116 <p>
117 If the <i>period</i> is changed at any time, either by
118 provided by an input or by changing the parameter, then the
119 new period will take effect immediately if the new period
120 is provided at the same time (including the
121 microstep) that the current cycle starts,
122 or after the current cycle completes otherwise.
123 <p>
124 If the <i>trigger</i> input is connected, then an output will only
125 be produced if a trigger input has been received since the last output
126 or if the trigger input coincides with the time when an output should
127 be produced. If a trigger input has not been received, then the
128 output will be skipped, moving on to the the next phase.
129 The only exception is the first output, which is produced
130 whether a trigger is provided or not. This is because the
131 trigger input is typically useful
132 in a feedback situation, where the output of the clock
133 eventually results in a trigger input. If the time-stamp
134 of that trigger input is less than the time between clock
135 events, then the clock will behave as if there were no
136 trigger input. Otherwise, it will "skip beats."
137 <p>
138 This actor is a timed source; the untimed version is Pulse.
139
140 @author Edward A. Lee
141 @version $Id$
142 @since Ptolemy II 8.0
143 @Pt.ProposedRating Yellow (eal)
144 @Pt.AcceptedRating Red (hyzheng)
145 */
146public class DiscreteClock extends TimedSource {
147
148    /** Construct an actor in the specified container with the specified
149     *  name.  The name must be unique within the container or an exception
150     *  is thrown. The container argument must not be null, or a
151     *  NullPointerException will be thrown.
152     *  @param container The container.
153     *  @param name The actor's name
154     *  @exception IllegalActionException If the entity cannot be contained
155     *   by the proposed container.
156     *  @exception NameDuplicationException If name coincides with
157     *   an entity already in the container.
158     */
159    public DiscreteClock(CompositeEntity container, String name)
160            throws IllegalActionException, NameDuplicationException {
161        super(container, name);
162
163        period = new PortParameter(this, "period");
164        period.setExpression("1.0");
165        period.setTypeEquals(BaseType.DOUBLE);
166        new SingletonParameter(period.getPort(), "_showName")
167                .setToken(BooleanToken.TRUE);
168
169        offsets = new Parameter(this, "offsets");
170        offsets.setExpression("{0.0}");
171        offsets.setTypeEquals(new ArrayType(BaseType.DOUBLE));
172
173        // Call this so that we don't have to copy its code here...
174        _updateOffsets(offsets);
175
176        // Set the values parameter.
177        values = new Parameter(this, "values");
178        values.setExpression("{1}");
179
180        // Set type constraint on the output.
181        output.setTypeAtLeast(ArrayType.elementType(values));
182
183        // Don't call attributeChanged here, see _updateOffsets for
184        // details.
185        //attributeChanged(values);
186
187        start = new TypedIOPort(this, "start");
188        start.setInput(true);
189        new StringAttribute(start, "_cardinal").setExpression("SOUTH");
190        new Parameter(start, "_showName").setExpression("true");
191
192        stop = new TypedIOPort(this, "stop");
193        stop.setInput(true);
194        new StringAttribute(stop, "_cardinal").setExpression("SOUTH");
195        new Parameter(stop, "_showName").setExpression("true");
196    }
197
198    ///////////////////////////////////////////////////////////////////
199    ////                     ports and parameters                  ////
200
201    /** The offsets at which the specified values will be produced.
202     *  This parameter must contain an array of doubles, and it defaults
203     *  to {0.0}.
204     */
205    public Parameter offsets;
206
207    /** The period of the output waveform.
208     *  This is a double that defaults to 1.0.
209     */
210    public PortParameter period;
211
212    /** A port that, if connected, is used to specify when the clock
213     *  starts. This port accepts any type. The arrival of an event
214     *  is what starts the clock. Upon arrival of such an event,
215     *  the clock starts as if just initialized. The clock will not
216     *  start until such an event is provided, unless the port is
217     *  left unconnected, in which case the actor starts immediately.
218     *  Note that when the clock starts, the period will be set to
219     *  its initial value. If an input period arrives before a
220     *  start input, then that arrived value will be ignored.
221     */
222    public TypedIOPort start;
223
224    /** A port that, if connected, is used to specify when the clock
225     *  stops. This port accepts any type. The arrival of an event
226     *  is what stops the clock.
227     */
228    public TypedIOPort stop;
229
230    /** The values that will be produced at the specified offsets.
231     *  This parameter must contain an ArrayToken, and it defaults to
232     *  {1}
233     */
234    public Parameter values;
235
236    ///////////////////////////////////////////////////////////////////
237    ////                         public methods                    ////
238
239    /** If the argument is the <i>offsets</i> parameter, check that the
240     *  array is nondecreasing and has the right dimension; if the
241     *  argument is <i>period</i>, check that it is positive. Other
242     *  sanity checks with <i>period</i> and <i>values</i> are done in
243     *  the fire() method.
244     *  @param attribute The attribute that changed.
245     *  @exception IllegalActionException If the offsets array is not
246     *   nondecreasing and nonnegative.
247     */
248    @Override
249    public void attributeChanged(Attribute attribute)
250            throws IllegalActionException {
251        if (attribute == offsets) {
252            _updateOffsets(attribute);
253        } else if (attribute == period) {
254            _updatePeriod(attribute);
255        } else {
256            super.attributeChanged(attribute);
257        }
258    }
259
260    /** Clone the actor into the specified workspace.
261     *  @param workspace The workspace for the new object.
262     *  @return A new actor.
263     *  @exception CloneNotSupportedException If a derived class contains
264     *   an attribute that cannot be cloned.
265     */
266    @Override
267    public Object clone(Workspace workspace) throws CloneNotSupportedException {
268        DiscreteClock newObject = (DiscreteClock) super.clone(workspace);
269        try {
270            ArrayToken offsetsValue = (ArrayToken) offsets.getToken();
271            newObject._offsets = new double[offsetsValue.length()];
272            System.arraycopy(_offsets, 0, newObject._offsets, 0,
273                    _offsets.length);
274            newObject.output
275                    .setTypeAtLeast(ArrayType.elementType(newObject.values));
276        } catch (IllegalActionException ex) {
277            // CloneNotSupportedException does not have a constructor
278            // that takes a cause argument, so we use initCause
279            CloneNotSupportedException throwable = new CloneNotSupportedException();
280            throwable.initCause(ex);
281            throw throwable;
282        }
283        return newObject;
284    }
285
286    /** Output the current value of the clock if the clock is currently
287     *  enabled and, if the trigger input is connected, a trigger has been
288     *  received. This method is expected to be called only at the right
289     *  time to produce the next output, since otherwise prefire() will
290     *  return false.
291     *  @exception IllegalActionException If
292     *   the value in the offsets parameter is encountered that is greater
293     *   than the period, or if there is no director.
294     */
295    @Override
296    public void fire() throws IllegalActionException {
297        super.fire();
298        // Check the start input, to see whether everything needs to
299        // be reinitialized.
300        if (start.numberOfSources() > 0) {
301            if (start.hasToken(0)) {
302                if (_debugging) {
303                    _debug("Received a start input.");
304                }
305                start.get(0);
306                // Restart everything.
307                initialize();
308                _enabled = true;
309            }
310        }
311        // Check stop
312        if (stop.numberOfSources() > 0) {
313            if (stop.hasToken(0)) {
314                if (_debugging) {
315                    _debug("Received a stop input.");
316                }
317                stop.get(0);
318                _enabled = false;
319            }
320        }
321
322        // Update the period from the port parameter, if appropriate.
323        period.update();
324
325        //         // Check for a trigger input.
326        //         // Have to consume all trigger inputs.
327        //         if (trigger.numberOfSources() > 0) {
328        //             // Have to consume all trigger inputs.
329        //             for (int i = 0; i < trigger.getWidth(); i++) {
330        //                 if (trigger.isKnown(i) && trigger.hasToken(i)) {
331        //                     trigger.get(i);
332        //                     _triggered = true;
333        //                     if (_debugging) {
334        //                         _debug("Received a trigger input. Enabling an output.");
335        //                     }
336        //                 }
337        //             }
338        //         }
339
340        // See whether it is time to produce an output.
341        Director director = getDirector();
342        Time currentTime = director.getModelTime();
343        int currentIndex = 0;
344        if (director instanceof SuperdenseTimeDirector) {
345            currentIndex = ((SuperdenseTimeDirector) director).getIndex();
346        }
347        if (_debugging) {
348            _debug("Called fire() at time (" + currentTime + ", " + currentIndex
349                    + ")");
350        }
351        if (!_enabled) {
352            if (_debugging) {
353                _debug("Not sending output because start input has not arrived.");
354            }
355            output.sendClear(0);
356            return;
357        }
358
359        int comparison = _nextOutputTime.compareTo(currentTime);
360        if (comparison > 0) {
361            // If it is too early to produce an output.
362            // This is safe because we have made a fireAt() call for
363            // the next output time.
364            _produceIntermediateOutput();
365            return;
366        } else if (comparison == 0) {
367            // It is the right time to produce an output. Check
368            // the index.
369            if (director instanceof SuperdenseTimeDirector) {
370                if (_nextOutputIndex > currentIndex) {
371                    // We have not yet reached the requisite index.
372                    // Request another firing at the current time.
373                    _fireAt(currentTime);
374                    _produceIntermediateOutput();
375                    return;
376                }
377            }
378            // At this point, the time matches the next output, and
379            // the index either matches or exceeds the index for the next output,
380            // or the director does not support superdense time.
381            if (!_triggered) {
382                if (_debugging) {
383                    _debug("No trigger yet. Skipping phase.");
384                }
385                // Pretend we produced an output so that posfire() will
386                // skip to the next phase.
387                _outputProduced = true;
388                output.sendClear(0);
389                return;
390            }
391            // Ready to fire.
392            if (_enabled) {
393                if (_debugging) {
394                    _debug("Sending output data: " + _getValue(_phase));
395                }
396                output.send(0, _getValue(_phase));
397                _outputProduced = true;
398            }
399            return;
400        }
401        // If we get here, then current time has passed our
402        // expected next firing time.  This should not occur.
403        throw new IllegalActionException(this, getDirector(),
404                "Director failed to fire this actor at the requested time "
405                        + _nextOutputTime + " Current time is " + currentTime
406                        + ". Perhaps the director is incompatible with DiscreteClock?");
407    }
408
409    /** Override the base class to initialize the index.
410     *  @exception IllegalActionException If the parent class throws it,
411     *   or if the <i>values</i> parameter is not a row vector, or if the
412     *   fireAt() method of the director throws it, or if the director does not
413     *   agree to fire the actor at the specified time.
414     */
415    @Override
416    public synchronized void initialize() throws IllegalActionException {
417        super.initialize();
418
419        // Start cycles at the current time.
420        // This is important in modal models that reinitialize the actor.
421        Time currentTime = getDirector().getModelTime();
422        _cycleStartTime = currentTime;
423        _cycleCount = 0;
424        _phase = 0;
425        _nextOutputTime = _cycleStartTime.add(_offsets[_phase]);
426        _nextOutputIndex = 1;
427        _enabled = true;
428        _outputProduced = false;
429
430        // Enable without a trigger input on the first firing.
431        _triggered = true;
432
433        if (_debugging) {
434            _debug("In initialize, requesting firing at time "
435                    + _nextOutputTime);
436            _debug("Requesting a refiring at " + _nextOutputTime
437                    + ", with index " + _nextOutputIndex);
438        }
439        _fireAt(_nextOutputTime);
440
441        // If the start port is connected, then start disabled.
442        if (start.isOutsideConnected()) {
443            _enabled = false;
444        }
445    }
446
447    /** Update the time and index of the next expected output.
448     *  @return False if the specified number of cycles has been reached,
449     *   and true otherwise.
450     *  @exception IllegalActionException If the director throws it when
451     *   scheduling the next firing, or if an offset value exceeds the period.
452     */
453    @Override
454    public boolean postfire() throws IllegalActionException {
455        boolean result = super.postfire();
456        if (_outputProduced) {
457            _skipToNextPhase();
458            _outputProduced = false;
459            if (_debugging) {
460                _debug("Postfiring. Requesting refiring at (" + _nextOutputTime
461                        + ", " + _nextOutputIndex + ")");
462            }
463            if (trigger.numberOfSources() > 0) {
464                _triggered = false;
465                if (_debugging) {
466                    _debug("Trigger input is connected. Wait for the next trigger before producing an output.");
467                }
468            }
469        } else if (_debugging) {
470            _debug("Postfiring, but not requesting a firing since we've already requested it.");
471        }
472        return result;
473    }
474
475    /** Return true if current time has not exceeded the
476     *  stopTime.
477     *  Check that the length of the <i>values</i> and
478     *  <i>offsets</i> parameters are the same and return true
479     *  if it is time to produce an output.
480     *  @return True if current time is less than or equal to the
481     *   stop time.
482     *  @exception IllegalActionException If the <i>values</i> and
483     *   <i>offsets</i> parameters do not have the same length.
484     */
485    @Override
486    public boolean prefire() throws IllegalActionException {
487        // FIXME: This comment is not correct:
488        // Cannot call super.prefire() because it consumes trigger
489        // inputs.
490
491        // super.prefire() longer consumes trigger inputs.
492        // However, if we call super.prefire() then some of
493        // the DiscreteClock tests fail.
494
495        // Check the length of the values and offsets arrays.
496        // This is done here because it cannot be done in
497        // attributeChanged(), since the two parameters are set
498        // separately, and checking in initialize() is not really
499        // sufficient, since the values of these parameters can
500        // change at run time.
501        ArrayToken val = (ArrayToken) values.getToken();
502        if (_offsets.length != val.length()) {
503            throw new IllegalActionException(this,
504                    "Values and offsets vectors do not have the same length.");
505        }
506
507        // Start of portion of prefire() from TimedSource
508        Time currentTime;
509        boolean localTime = ((BooleanToken) stopTimeIsLocal.getToken())
510                .booleanValue();
511        if (localTime) {
512            currentTime = getDirector().getModelTime();
513        } else {
514            currentTime = getDirector().getGlobalTime();
515        }
516        if (currentTime.compareTo(getModelStopTime()) > 0) {
517            if (_debugging) {
518                _debug("Called prefire, which returns false because time exceeds stopTime.");
519            }
520            return false;
521        }
522        // End of portion of prefire() from TimedSource
523
524        if (_debugging) {
525            _debug("Called prefire, which returns true.");
526        }
527
528        return true;
529    }
530
531    ///////////////////////////////////////////////////////////////////
532    ////                         protected methods                 ////
533
534    /** Get the specified output value, checking the form of the values
535     *  parameter.
536     *  @param index The index of the output values.
537     *  @return A token that contains the output value.
538     *  @exception IllegalActionException If the index is out of the range of
539     *  the values parameter.
540     */
541    protected Token _getValue(int index) throws IllegalActionException {
542        ArrayToken val = (ArrayToken) values.getToken();
543        if (val == null || val.length() <= index) {
544            throw new IllegalActionException(this,
545                    "Index out of range of the values parameter.");
546        }
547        return val.getElement(index);
548    }
549
550    /** Produce the output required at times between the specified times.
551     *  This base class makes the output absent, but subclasses may
552     *  interpolate the values.
553     *  @exception IllegalActionException If sending the output fails.
554     */
555    protected void _produceIntermediateOutput() throws IllegalActionException {
556        if (_debugging) {
557            _debug("Too early to produce output.");
558        }
559        output.sendClear(0);
560    }
561
562    /** Skip the current firing phase and request a refiring at the
563     *  time of the next one.
564     *  @exception IllegalActionException If the period cannot be evaluated, or
565     *   if an offset is encountered that is greater than the period.
566     */
567    protected void _skipToNextPhase() throws IllegalActionException {
568        _phase++;
569        if (_phase >= _offsets.length) {
570            double periodValue = ((DoubleToken) period.getToken())
571                    .doubleValue();
572            _phase = 0;
573            _cycleStartTime = _cycleStartTime.add(periodValue);
574        }
575        double periodValue = ((DoubleToken) period.getToken()).doubleValue();
576        if (_offsets[_phase] > periodValue) {
577            throw new IllegalActionException(this,
578                    "Offset of " + _offsets[_phase]
579                            + " is greater than the period " + periodValue);
580        }
581        Time nextOutputTime = _cycleStartTime.add(_offsets[_phase]);
582        if (_nextOutputTime.equals(nextOutputTime)) {
583            _nextOutputIndex++;
584        } else {
585            _nextOutputTime = nextOutputTime;
586            _nextOutputIndex = 1;
587        }
588        _fireAt(_nextOutputTime);
589    }
590
591    ///////////////////////////////////////////////////////////////////
592    ////                         protected variables               ////
593
594    /** The count of cycles executed so far, or 0 before the start. */
595    protected transient int _cycleCount;
596
597    /** The most recent cycle start time. */
598    protected transient Time _cycleStartTime;
599
600    /** Indicator of whether the specified number of cycles have
601     *  been completed. Also used in derived classes to turn on
602     *  and off the clock.
603     */
604    protected transient boolean _enabled;
605
606    /** Indicator of whether the first output has been produced. */
607    protected transient boolean _firstOutputProduced = false;
608
609    /** The time for the next output. */
610    protected transient Time _nextOutputTime;
611
612    /** The index of when the output should be emitted. */
613    protected transient int _nextOutputIndex;
614
615    /** Cache of offsets array value. */
616    protected transient double[] _offsets;
617
618    /** The phase of the next output. */
619    protected transient int _phase;
620
621    /** Update the offsets attribute.
622     *  @param attribute The attribute that changed.
623     *  @exception IllegalActionException If the offsets array is not
624     *  nondecreasing and nonnegative.
625     */
626    private void _updateOffsets(Attribute attribute)
627            throws IllegalActionException {
628        // This method is necessary because FindBugs reports
629        // "Unitialized read of field in method called from constructor"
630        // if this constructor calls attributeChanged() and this
631        // class has a subclass.  The workaround is to define
632        // a separate method.
633        ArrayToken offsetsValue = (ArrayToken) offsets.getToken();
634        _offsets = new double[offsetsValue.length()];
635
636        double previous = 0.0;
637        for (int i = 0; i < offsetsValue.length(); i++) {
638            _offsets[i] = ((DoubleToken) offsetsValue.getElement(i))
639                    .doubleValue();
640            // Check nondecreasing property.
641            if (_offsets[i] < previous) {
642                throw new IllegalActionException(this,
643                        "Value of offsets is not nondecreasing "
644                                + "and nonnegative.");
645            }
646            previous = _offsets[i];
647        }
648    }
649
650    /** Update the period attribute.
651     *  @param attribute The attribute that changed.
652     *  @exception IllegalActionException If the period is not
653     *  positive.
654     */
655    private void _updatePeriod(Attribute attribute)
656            throws IllegalActionException {
657        // This method is necessary because FindBugs reports
658        // "Unitialized read of filed in method called from constructor"
659        // if this constructor calls attributeChanged() and this
660        // class has a subclass.  The workaround is to define
661        // a separate method.
662        double periodValue = ((DoubleToken) period.getToken()).doubleValue();
663        if (_debugging) {
664            _debug("Setting period to " + periodValue);
665        }
666        if (periodValue <= 0.0) {
667            throw new IllegalActionException(this,
668                    "Period is required to be positive.  " + "Period given: "
669                            + periodValue);
670        }
671    }
672
673    ///////////////////////////////////////////////////////////////////
674    ////                         private variables                 ////
675
676    /** Indicator that an output was produced and hence we should
677     *  skip to the next phase in postfire.
678     */
679    private boolean _outputProduced;
680}