001/* An actor that outputs a random sequence with a Gaussian distribution.
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.continuous.kernel;
029
030import ptolemy.actor.Director;
031import ptolemy.actor.lib.Gaussian;
032import ptolemy.actor.util.Time;
033import ptolemy.data.BooleanToken;
034import ptolemy.data.expr.Parameter;
035import ptolemy.data.type.BaseType;
036import ptolemy.kernel.CompositeEntity;
037import ptolemy.kernel.util.IllegalActionException;
038import ptolemy.kernel.util.NameDuplicationException;
039
040///////////////////////////////////////////////////////////////////
041//// Noise
042
043/**
044This actor generates continuous-time noise with a Gaussian distribution.
045It provides two approximations to a white noise process, depending
046on the value of the <i>linearlyInterpolate</i> parameter. Specifically,
047if this parameter is true (the default), then the output signal is
048a continuous signal that linearly interpolates between
049Gaussian random numbers generated at each sample time chosen by
050the solver. If the solver step size is constant or increasing, then these Gaussian
051random numbers will be independent. However, if the solver finds itself having
052to reduce the step size after performing a speculative execution into the
053future, then the random number produced at the end of the reduced
054integration step will be correlated with that produced at the beginning
055of the integration step. (FIXME: Need a figure to illustrate this.)
056<p>
057If <i>linearlyInterpolate</i> is set to false, then this actor will
058hold the value of its output constant for the duration of an integration
059step. Thus, the output signal is piecewise constant. At each time step
060chosen by the solver, the value is given by a new independent Gaussian
061random number.  This method has the advantage that samples at all
062times chosen by the solver are uncorrelated.
063<p>
064In both cases, whether <i>linearlyInterpolate</i> is true or false, if the
065solver holds its step size constant, then the resulting signal is
066statistically equivalent to filtered white noise. If <i>linearlyInterpolate</i>
067is true, then the power spectrum has the shape of a sinc squared.
068If it is false, then it has the shape of the absolute value of a sinc
069function. In the latter case, the power is infinite, so the approximation
070is not physically realizable. In the former case, the power is finite.
071In both cases, sampling the process at the rate of one over the step
072size yields a discrete-time white noise process.
073<p>
074It is worth explaining why we must approximate white noise.
075In general, it is not possible in any discretized approximation of a continuous
076random process to exactly simulate a white noise process. By definition, a
077white noise process is one where any two values at distinct times are
078uncorrelated. A naive attempt to simulate this might simply generate
079a new random number at each sample time at which the solver chooses
080to fire the actor. However, this cannot work in general.
081Specifically, the semantics of the continuous domain assumes
082that signals are piecewise continuous. The signal resulting from
083the above strategy will not be piecewise continuous. If the solver
084refines a step size and chooses a point close to a previously calculated
085point, the new value produced by such an actor would not be close to the
086previously value previously produced. This can result in the solver
087assuming that its step size is too large and reducing it until it can
088reduce it no more.
089<p>
090To demonstrate this effect, try connecting a GaussianActor to a
091LevelCrossingDetector actor under a ContinuousDirector. An execution
092of the model will immediately trigger an exception with a message
093like "The refined step size is less than the time resolution, at time..."
094Conceptually, with a true white noise process, the level crossing
095occurs <i>at all times</i>, and therefore the exception is,
096in fact, the correct response.
097<p>
098If you modify the above example by sending the output of the
099Gaussian actor directly to an Integrator, and then the output
100of the Integrator to the LevelCrossingDetector, then the exception
101disappears. The Integrator ensures that the signal is piecewise
102continuous. This might seem like a reasonable approximation to a
103Weiner process, but in fact it is problematic. In particular,
104at the times that the LevelCrossingDetector triggers, the
105Gaussian actor will actually produce two distinct random numbers
106at the same time (at different microsteps). This changes the
107statistics of the output in a very subtle way.
108
109 @author Edward A. Lee
110 @version $Id$
111 @since Ptolemy II 8.0
112 @Pt.ProposedRating Yellow (eal)
113 @Pt.AcceptedRating Red (eal)
114 */
115public class Noise extends Gaussian {
116    /** Construct an actor with the given container and name.
117     *  @param container The container.
118     *  @param name The name of this actor.
119     *  @exception IllegalActionException If the actor cannot be contained
120     *   by the proposed container.
121     *  @exception NameDuplicationException If the container already has an
122     *   actor with this name.
123     */
124    public Noise(CompositeEntity container, String name)
125            throws NameDuplicationException, IllegalActionException {
126        super(container, name);
127
128        linearlyInterpolate = new Parameter(this, "linearlyInterpolate");
129        linearlyInterpolate.setTypeEquals(BaseType.BOOLEAN);
130        linearlyInterpolate.setExpression("true");
131    }
132
133    ///////////////////////////////////////////////////////////////////
134    ////                         parameters                        ////
135
136    /** If true, linearly between random number for multistep solvers,
137     *  and otherwise, perform zero-order hold. This is a boolean that
138     *  defaults to true.
139     */
140    public Parameter linearlyInterpolate;
141
142    ///////////////////////////////////////////////////////////////////
143    ////                         public methods                    ////
144
145    /** Produce a number that is linearly interpolated within the
146     *  current integration step, if <i>linearlyInterpolate</i> is true, or
147     *  the random number for the beginning of the integration
148     *  step otherwise.
149     *  @exception IllegalActionException If the superclass throws it.
150     */
151    @Override
152    public void fire() throws IllegalActionException {
153        if (_needNewGenerator) {
154            _createGenerator();
155        }
156        if (_needNew) {
157            // Generate a random number for the _end_ of the current
158            // integration period.
159            _generateRandomNumber();
160            _needNew = false;
161        }
162        // Get required information from the director.
163        Director director = getDirector();
164        double stepSize = ((ContinuousDirector) director)._getCurrentStepSize();
165        boolean interpolating = ((BooleanToken) linearlyInterpolate.getToken())
166                .booleanValue();
167        if (!interpolating) {
168            // Doing zero-order hold.
169            // Output value should be the value at the beginning
170            // of the integration period.
171            _current = _valueAtStart;
172        } else if (stepSize == 0.0) {
173            // Interpolating and step size is zero.
174            // If the step size is zero then any newly
175            // generated random number is ignored.
176            _current = _valueAtStart;
177        } else {
178            // Interpolating and step size is greater than zero.
179            Time iterationBeginTime = ((ContinuousDirector) director)._iterationBeginTime;
180            Time currentTime = ((ContinuousDirector) director).getModelTime();
181            double interval = currentTime.subtract(iterationBeginTime)
182                    .getDoubleValue();
183            if (interval == 0.0) {
184                _current = _valueAtStart;
185            } else {
186                double timeGapBetweenValues = _timeOfValueAtEnd
187                        .subtract(iterationBeginTime).getDoubleValue();
188                _current = _valueAtStart + (_valueAtEnd - _valueAtStart)
189                        * interval / timeGapBetweenValues;
190            }
191        }
192        // The superclass produces on the output the _current value.
193        // Since above we set _needNew to false if it was true,
194        // the superclass will never produce a new random number.
195        super.fire();
196    }
197
198    /** Initialize the random number generator with the seed, if it
199     *  has been given.  A seed of zero is interpreted to mean that no
200     *  seed is specified.  In such cases, a seed based on the current
201     *  time and this instance of a RandomSource is used to be fairly
202     *  sure that two identical sequences will not be returned.
203     *  @exception IllegalActionException If the parent class throws it.
204     */
205    @Override
206    public void initialize() throws IllegalActionException {
207        super.initialize();
208        // Generate the first random number.
209        super._generateRandomNumber();
210        // The first step size will be zero, so the value at
211        // the start and end of the integration period will be
212        // the same, namely the random number generated above.
213        _valueAtStart = _current;
214        _valueAtEnd = _current;
215        _timeOfValueAtEnd = getDirector().getModelTime();
216        // Suppress generation of the first random number in the
217        // next invocation of fire().
218        _needNew = false;
219    }
220
221    /** Set a flag to cause a new random number to be generated the next
222     *  time fire() is called.
223     *  @exception IllegalActionException If the base class throws it.
224     *  @return True if it is OK to continue.
225     */
226    @Override
227    public boolean postfire() throws IllegalActionException {
228        // The next line will set _needNew = true.
229        boolean result = super.postfire();
230
231        Director director = getDirector();
232        // The cast is safe because it was checked in fire().
233        double stepSize = ((ContinuousDirector) director)._getCurrentStepSize();
234
235        // If we are not interpolating, then request a refiring at the current time
236        // so that the new value is then produced at the current time.
237        boolean interpolating = ((BooleanToken) linearlyInterpolate.getToken())
238                .booleanValue();
239        if (!interpolating) {
240            // Request a refiring at the current time in order to get a zero-order
241            // hold effect, but only if the step size is non-zero.
242            // If the step size is zero, the new random number will be
243            // the same as the old so there is no need to refire.
244            if (stepSize != 0.0) {
245                // Request a refiring at the current time in order to get
246                // the zero-order hold effect.
247                director.fireAtCurrentTime(this);
248            }
249        }
250        return result;
251    }
252
253    ///////////////////////////////////////////////////////////////////
254    ////                         protected methods                 ////
255
256    /** Generate a new random number. This gets called in initialize()
257     *  and in the first fire() method of an iteration. It produces a number
258     *  that is to be the random number at the <i>end</i> of the current
259     *  iteration.
260     *  @exception IllegalActionException If parameter values are incorrect.
261     */
262    @Override
263    protected void _generateRandomNumber() throws IllegalActionException {
264        Director director = getDirector();
265        if (!(director instanceof ContinuousDirector)) {
266            throw new IllegalActionException(this, director,
267                    "WhiteNoise actor is designed to work with ContinuousDirector,"
268                            + " but the director is " + director.getClass());
269        }
270        double stepSize = ((ContinuousDirector) director)._getCurrentStepSize();
271        boolean interpolating = ((BooleanToken) linearlyInterpolate.getToken())
272                .booleanValue();
273        if (!interpolating) {
274            // Doing zero-order hold.
275            _valueAtStart = _valueAtEnd;
276            _timeOfValueAtEnd = director.getModelTime();
277            // Generate a new random number only if the step size is
278            // non-zero.
279            if (stepSize != 0.0) {
280                super._generateRandomNumber();
281                _valueAtEnd = _current;
282            }
283        } else {
284            // Interpolating.
285            // The value at the start of the new period should be equal
286            // to the interpolated value at the current time.
287            // Presumably, this is the most recently produced output.
288            _valueAtStart = _current;
289            _timeOfValueAtEnd = director.getModelTime().add(stepSize);
290            // If the step size is non-zero, generate a new random number
291            // for the end of the period. Otherwise, use the current value.
292            if (stepSize != 0.0) {
293                super._generateRandomNumber();
294                _valueAtEnd = _current;
295            } else {
296                _valueAtEnd = _valueAtStart;
297            }
298        }
299    }
300
301    ///////////////////////////////////////////////////////////////////
302    ////                         private variables                 ////
303
304    /** Time assigned to a new random number. This will be the integration end
305     *  time for the first firing with a non-zero step size after the random
306     *  number is generated.
307     */
308    private Time _timeOfValueAtEnd;
309
310    /** The random number at the start of the current integration period. */
311    private double _valueAtStart;
312
313    /** The random number at the end of the current integration period. */
314    private double _valueAtEnd;
315}