001/* A publisher that transparently tunnels messages to subscribers and saves its output for testing
002
003 Copyright (c) 2007-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 java.util.ArrayList;
031import java.util.List;
032
033import ptolemy.actor.parameters.SharedParameter;
034import ptolemy.data.ArrayToken;
035import ptolemy.data.BooleanToken;
036import ptolemy.data.DoubleToken;
037import ptolemy.data.Token;
038import ptolemy.data.expr.Parameter;
039import ptolemy.data.type.ArrayType;
040import ptolemy.data.type.BaseType;
041import ptolemy.kernel.CompositeEntity;
042import ptolemy.kernel.util.Attribute;
043import ptolemy.kernel.util.IllegalActionException;
044import ptolemy.kernel.util.NameDuplicationException;
045import ptolemy.util.MessageHandler;
046import ptolemy.util.StringUtilities;
047
048///////////////////////////////////////////////////////////////////
049//// PublisherNonStrictTest
050
051/**
052 This actor publishes input tokens on a named channel and compares
053 the inputs against the value specified by the <i>correctValues</i> parameter.
054
055 <p>This actor combines the {@link ptolemy.actor.lib.Publisher} actor
056 and the {@link ptolemy.actor.lib.NonStrictTest} actor.  Thus, it has quite
057 a bit of duplicated code from the NonStrictTest actor.
058
059 <p> Note that in the superclass (Publisher), the input is a multiport,
060 whereas in this class it is a regular non-multiport.  To use a multiport
061 input, use {@link ptolemy.actor.lib.PublisherTest}
062
063 @author Christopher Brooks, based on Test, which has Edward A. Lee and Jim Armstrong as authors
064 @version $Id$
065 @since Ptolemy II 6.1
066 @Pt.ProposedRating Red (cxh)
067 @Pt.AcceptedRating Red (cxh)
068 */
069public class PublisherNonStrictTest extends Publisher {
070
071    /** Construct a publisher with the specified container and name.
072     *  @param container The container actor.
073     *  @param name The name of the actor.
074     *  @exception IllegalActionException If the actor is not of an acceptable
075     *   class for the container.
076     *  @exception NameDuplicationException If the name coincides with
077     *   an actor already in the container.
078     */
079    public PublisherNonStrictTest(CompositeEntity container, String name)
080            throws IllegalActionException, NameDuplicationException {
081        super(container, name);
082
083        correctValues = new Parameter(this, "correctValues");
084        correctValues.setExpression("{true}");
085        correctValues.setTypeAtLeast(ArrayType.ARRAY_BOTTOM);
086
087        tolerance = new Parameter(this, "tolerance");
088        tolerance.setExpression("1.0");
089        tolerance.setTypeEquals(BaseType.DOUBLE);
090
091        trainingMode = new SharedParameter(this, "trainingMode", getClass(),
092                "false");
093        trainingMode.setTypeEquals(BaseType.BOOLEAN);
094
095        // Note that in Publisher, the input is a multiport.
096        input.setMultiport(false);
097    }
098
099    ///////////////////////////////////////////////////////////////////
100    ////                   ports and parameters                    ////
101
102    /** A matrix specifying what the input should be.
103     *  This defaults to a one-by-one array containing a boolean true.
104     */
105    public Parameter correctValues;
106
107    /** A double specifying how close the input has to be to the value
108     *  given by <i>correctValues</i>.  This is a DoubleToken, with default
109     *  value 10<sup>-9</sup>.  During training, if a correct value is
110     *  greater than 10 orders of magnitude than the tolerance, then the
111     *  tolerance is changed to a value 9 orders of magnitude less than
112     *  the correct value.  This helps avoid comparisons beyond the
113     *  precision of a Java double.
114     */
115    public Parameter tolerance;
116
117    /** If true, then do not check inputs, but rather collect them into
118     *  the <i>correctValues</i> array.  This parameter is a boolean,
119     *  and it defaults to false. It is a shared parameter, meaning
120     *  that changing it for any one instance in a model will change
121     *  it for all instances in the model.
122     */
123    public SharedParameter trainingMode;
124
125    ///////////////////////////////////////////////////////////////////
126    ////                         public methods                    ////
127
128    /** If the attribute is the channel, increment the workspace version
129     *  to force cached receiver lists to be updated, and invalidate
130     *  the schedule and resolved types of the director, if there is one.
131     *  @param attribute The attribute that changed.
132     *  @exception IllegalActionException If the change is not acceptable
133     *   to this container.
134     */
135    @Override
136    public void attributeChanged(Attribute attribute)
137            throws IllegalActionException {
138        if (attribute == tolerance) {
139            _tolerance = ((DoubleToken) tolerance.getToken()).doubleValue();
140        } else {
141            super.attributeChanged(attribute);
142        }
143    }
144
145    /** Read at most one input token from each
146     *  input channel and send it to the subscribers,
147     *  if any.
148     *  @exception IllegalActionException If there is no director.
149     */
150    @Override
151    public void fire() throws IllegalActionException {
152        // We don't call the super class because we want to
153        // have token production occur in postfire.
154
155        //super.fire();
156        if (_debugging) {
157            _debug("Called fire()");
158        }
159        _firedOnce = true;
160
161        //int width = input.getWidth();
162
163        //         // If we are in training mode, read the inputs and add to the
164        //         // training data.
165        //         boolean training = ((BooleanToken) trainingMode.getToken())
166        //                 .booleanValue();
167
168        //         if (training) {
169        //             if (_trainingTokens == null) {
170        //                 _trainingTokens = new ArrayList();
171        //             }
172
173        //             if (width == 1) {
174        //                 if (input.hasToken(0)) {
175        //                     Token token = input.get(0);
176        //                     output.send(0, token);
177        //                     if (token instanceof ArrayToken) {
178        //                         Token[] innerArrayToken = new Token[1];
179        //                         innerArrayToken[0] = token;
180        //                         _trainingTokens.add(innerArrayToken);
181        //                     } else {
182        //                         _trainingTokens.add(token);
183        //                     }
184        //                 }
185        //             } else {
186        //                 ArrayList arrayList = new ArrayList();
187
188        //                 for (int i = 0; i < width; i++) {
189        //                     if (input.hasToken(i)) {
190        //                         Token token = input.get(i);
191        //                         arrayList.add(token);
192        //                         output.send(i, token);
193        //                     }
194        //                 }
195
196        //                 _trainingTokens.add(arrayList);
197        //             }
198
199        //             return;
200        //         }
201
202        //         for (int i = 0; i < width; i++) {
203        //             if (input.hasToken(i)) {
204        //                 Token token = input.get(i);
205        //                 output.send(i, token);
206        //             }
207        //         }
208    }
209
210    /** Override the base class to set the iteration counter to zero.
211     *  @exception IllegalActionException If the base class throws it or
212     *  if we are running under the test suite and the trainingMode
213     *  parameter is set to true.
214     *  @see ptolemy.util.MessageHandler#isNonInteractive()
215     */
216    @Override
217    public void initialize() throws IllegalActionException {
218        super.initialize();
219        _numberOfInputTokensSeen = 0;
220        _iteration = 0;
221        _trainingTokens = null;
222        _firedOnce = false;
223        _initialized = true;
224
225        if (((BooleanToken) trainingMode.getToken()).booleanValue()) {
226            if (MessageHandler.isNonInteractive()) {
227                throw new IllegalActionException(this,
228                        NonStrictTest.TRAINING_MODE_ERROR_MESSAGE);
229            } else {
230                System.err.println("Warning: '" + getFullName()
231                        + "' is in training mode, set the trainingMode "
232                        + "parameter to false before checking in");
233            }
234        }
235    }
236
237    /** Override the base class to ensure that links to subscribers
238     *  have been updated.
239     *  @exception IllegalActionException If there is already a publisher
240     *   publishing on the same channel.
241     */
242    @Override
243    public void preinitialize() throws IllegalActionException {
244        super.preinitialize();
245    }
246
247    /** Read one token from each input channel and compare against
248     *  the value specified in <i>correctValues</i>.  If the token count
249     *  is larger than the length of <i>correctValues</i>, then return
250     *  immediately, indicating that the inputs correctly matched
251     *  the values in <i>correctValues</i> and that the test succeeded.
252     *
253     *  @exception IllegalActionException If an input does not match
254     *   the required value or if the width of the input is not 1.
255     */
256    @Override
257    public boolean postfire() throws IllegalActionException {
258        boolean superReturnValue = super.postfire();
259        if (input.getWidth() != 1) {
260            throw new IllegalActionException(this, "Width of input is "
261                    + input.getWidth()
262                    + " but PublisherNonStrictTest only supports a width of 1.");
263        }
264
265        boolean training = ((BooleanToken) trainingMode.getToken())
266                .booleanValue();
267
268        if (training) {
269            if (_trainingTokens == null) {
270                _trainingTokens = new ArrayList();
271            }
272
273            if (input.hasToken(0)) {
274                Token token = input.get(0);
275                _trainingTokens.add(token);
276                output.send(0, token);
277            }
278
279            return true && superReturnValue;
280        }
281
282        //          if (_numberOfInputTokensSeen >= ((ArrayToken) (correctValues.getToken()))
283        //                  .length()) {
284        //              // Consume and discard input values.  We are beyond the end
285        //              // of the correctValues array.
286        //              //if (input.hasToken(0)) {
287        //              //    input.get(0);
288
289        //              //}
290        //              return true;
291        //          }
292
293        Token referenceToken = ((ArrayToken) correctValues.getToken())
294                .getElement(_numberOfInputTokensSeen);
295
296        for (int i = 0; i < input.getWidth(); i++) {
297            if (input.hasToken(i)) {
298                Token token = input.get(i);
299
300                //if (input.hasToken(0)) {
301                //Token token = input.get(0);
302                // output.send(0, token);
303                _numberOfInputTokensSeen++;
304
305                // FIXME: If we get a nil token on the input, what should we do?
306                // Here, we require that the referenceToken also be nil.
307                // If the token is an ArrayToken and two corresponding elements
308                // are nil, then we consider them "close".
309                if (token.isCloseTo(referenceToken, _tolerance)
310                        .booleanValue() == false
311                        && !referenceToken.isNil()
312                        && !NonStrictTest._isCloseToIfNilArrayElement(token,
313                                referenceToken, _tolerance)
314                        && !NonStrictTest._isCloseToIfNilRecordElement(token,
315                                referenceToken, _tolerance)) {
316                    throw new IllegalActionException(this,
317                            "Test fails in iteration " + _iteration + ".\n"
318                                    + "Value was: " + token
319                                    + ". Should have been: " + referenceToken);
320                }
321                output.send(i, token);
322            }
323        }
324
325        _iteration++;
326        return true && superReturnValue;
327    }
328
329    /** If <i>trainingMode</i> is <i>true</i>, then take the collected
330     *  training tokens and store them as an array in <i>correctValues</i>.
331     *  @exception IllegalActionException If initialized() was called
332     *  and fire() was not called or if the number of inputs tokens seen
333     *  is not greater than or equal to the number of elements in the
334     *  <i>correctValues</i> array.
335     */
336    @Override
337    public void wrapup() throws IllegalActionException {
338        super.wrapup();
339
340        boolean training = ((BooleanToken) trainingMode.getToken())
341                .booleanValue();
342
343        if (!training && _initialized) {
344            if (!_firedOnce) {
345                String errorMessage = "The fire() method of this actor was never called. "
346                        + "Usually, this is an error indicating that "
347                        + "starvation is occurring.";
348                String fireCompatProperty = "ptolemy.actor.lib.NonStrictTest.fire.compat";
349
350                if (StringUtilities.getProperty(fireCompatProperty)
351                        .length() > 0) {
352                    System.err.println("Warning: '" + getFullName() + "' "
353                            + errorMessage
354                            + "\nThis error is being ignored because " + "the "
355                            + fireCompatProperty + "property was set.");
356                } else {
357                    throw new IllegalActionException(this, errorMessage);
358                }
359            }
360
361            if (_numberOfInputTokensSeen < ((ArrayToken) correctValues
362                    .getToken()).length()) {
363                String errorMessage = "The test produced only "
364                        + _numberOfInputTokensSeen
365                        + " tokens, yet the correctValues parameter was "
366                        + "expecting "
367                        + ((ArrayToken) correctValues.getToken()).length()
368                        + " tokens.";
369
370                System.err.println(
371                        "Warning: '" + getFullName() + "' " + errorMessage);
372            }
373        }
374
375        _initialized = false;
376
377        // Note that wrapup() might get called by the manager before
378        // we have any data...
379        if (training && _trainingTokens != null && _trainingTokens.size() > 0) {
380            Object[] newValues = _trainingTokens.toArray();
381
382            // NOTE: Support input multiport for the benefit of derived classes.
383            int width = input.getWidth();
384            Token[] newTokens = new Token[newValues.length];
385
386            if (width == 1) {
387                for (int i = 0; i < newValues.length; i++) {
388                    if (newValues[i] instanceof Token[]) {
389                        // Handle width of 1, ArrayToken
390                        newTokens[i] = new ArrayToken((Token[]) newValues[i]);
391                        for (int j = 0; j < ((Token[]) newValues[i]).length; i++) {
392                            _checkRangeOfTolerance(((Token[]) newValues[i])[j]);
393                        }
394                    } else {
395                        newTokens[i] = (Token) newValues[i];
396                        _checkRangeOfTolerance((Token) newValues[i]);
397                    }
398                }
399            } else {
400                for (int i = 0; i < newValues.length; i++) {
401                    ArrayList entry = (ArrayList) newValues[i];
402
403                    // Entry may be an empty array, in which case,
404                    // we cannot do the update, so we return.
405                    if (entry.size() < 1) {
406                        System.err.println("Warning: '" + getFullName()
407                                + "': Unable to train. "
408                                + "Zero tokens received in iteration " + i);
409                        return;
410                    }
411
412                    Object[] entries = entry.toArray();
413                    Token[] newEntry = new Token[entries.length];
414
415                    for (int j = 0; j < entries.length; j++) {
416                        newEntry[j] = (Token) entries[j];
417                        _checkRangeOfTolerance(newEntry[i]);
418                    }
419
420                    newTokens[i] = new ArrayToken(newEntry);
421                }
422            }
423
424            correctValues.setToken(new ArrayToken(newTokens));
425            correctValues.setPersistent(true);
426        }
427
428        if (training
429                && (_trainingTokens == null || _trainingTokens.size() == 0)) {
430            System.err.println("Warning: '" + getFullName()
431                    + "' The test produced 0 tokens.");
432        }
433    }
434
435    ///////////////////////////////////////////////////////////////////
436    ////                         protected variables               ////
437
438    /** Set to true if fire() is called once.  If fire() is not called at
439     *  least once, then throw an exception in wrapup().
440     */
441    protected boolean _firedOnce = false;
442
443    /** Set to true when initialized() is called.
444     */
445    protected boolean _initialized = false;
446
447    /** Count of iterations. */
448    protected int _iteration;
449
450    /** Number of input tokens seen by this actor in the fire method.*/
451    protected int _numberOfInputTokensSeen = 0;
452
453    /** A double that is read from the <i>tolerance</i> parameter
454     *        specifying how close the input has to be to the value
455     *  given by <i>correctValues</i>.  This is a double, with default
456     *  value 10<sup>-9</sup>.
457     */
458    protected double _tolerance;
459
460    /** List to store tokens for training mode. */
461    protected List _trainingTokens;
462
463    /** Check that the difference in exponents between the
464     *  input and the tolerance is not greater than the precision
465     *  of a Double.  If the exponent of newValue parameter is
466     *  different by from the exponent of the <i>tolerance</i>
467     *  parameter by more than 10, then adjust the <i>tolerance</i>
468     *  parameter.  This is useful for training large modesl
469     *  that have many PublisherTests.
470     *  @param newValue The token to be tested.  DoubleTokens
471     *  are tested, other tokens are ignored.
472     *  @exception IllegalActionException If thrown while reading the
473     *  <i>tolerance</i> parameter.
474     */
475    private void _checkRangeOfTolerance(Token newValue)
476            throws IllegalActionException {
477        if (newValue instanceof DoubleToken) {
478            Double value = ((DoubleToken) newValue).doubleValue();
479            if (value == 0.0) {
480                // The exponent of 0.0 is -Infinity, so skip it
481                return;
482            }
483            double log = Math.log10(((DoubleToken) newValue).doubleValue());
484            if (Math.abs(log - Math.log10(_tolerance)) > 10) {
485                // Set the tolerance to something closer to the input so that
486                // we don't set it many times.
487                double newTolerance = Math.pow(10, log - 9);
488                if (newTolerance > _tolerance) {
489                    // Only set the tolerance if it is greater than the old tolerance.
490                    tolerance.setToken(new DoubleToken(newTolerance));
491                    tolerance.setPersistent(true);
492                    attributeChanged(tolerance);
493                    System.out.println("PublisherNonStrictTest: "
494                            + getFullName() + ": exponent of " + newValue
495                            + " is " + log
496                            + ", which cannot be compared with the previous tolerance."
497                            + " The new tolerance is "
498                            + tolerance.getExpression() + ".");
499                }
500            }
501        }
502    }
503}