View Javadoc
1   package org.opentrafficsim.core.distributions;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.List;
6   
7   import org.djutils.exceptions.Throw;
8   
9   import nl.tudelft.simulation.jstats.distributions.DistUniform;
10  import nl.tudelft.simulation.jstats.streams.StreamInterface;
11  
12  /**
13   * Generic implementation of a set of objects that have a draw method with corresponding probabilities / frequencies.
14   * <p>
15   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
17   * <p>
18   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Mar 1, 2016 <br>
19   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
20   * @param <O> Type of the object returned by the draw method
21   */
22  public class Distribution<O> implements Generator<O>, Serializable
23  {
24      /** */
25      private static final long serialVersionUID = 20160301L;
26  
27      /** The generators (with their probabilities or frequencies). */
28      private final List<FrequencyAndObject<O>> generators = new ArrayList<>();
29  
30      /** Sum of all probabilities or frequencies. */
31      private double cumulativeTotal;
32  
33      /** The uniform random generator used to select a Generator. */
34      private final DistUniform random;
35  
36      /**
37       * Construct a new Distribution.
38       * @param generators List&lt;FrequencyAndObject&lt;O&gt;&gt;; the generators and their frequencies (or probabilities)
39       * @param stream StreamInterface; source for randomness
40       * @throws ProbabilityException when a frequency (or probability) is negative, or when generators is null or stream is null
41       */
42      public Distribution(final List<FrequencyAndObject<O>> generators, final StreamInterface stream) throws ProbabilityException
43      {
44          this(stream);
45          Throw.when(null == generators, ProbabilityException.class, "generators may not be null");
46          // Store a defensive copy of the generator list (the generators are immutable; a list of them is not) and make sure it
47          // is a List that supports add, remove, etc.
48          this.generators.addAll(generators);
49          fixProbabilities();
50      }
51  
52      /**
53       * Construct a new Distribution with no generators.
54       * @param stream StreamInterface; source for randomness
55       * @throws ProbabilityException when a frequency (or probability) is negative, or when generators is null or stream is null
56       */
57      public Distribution(final StreamInterface stream) throws ProbabilityException
58      {
59          Throw.when(null == stream, ProbabilityException.class, "random stream may not be null");
60          this.random = new DistUniform(stream, 0, 1);
61      }
62  
63      /**
64       * Compute the cumulative frequencies of the storedGenerators.
65       * @throws ProbabilityException on negative frequency
66       */
67      private void fixProbabilities() throws ProbabilityException
68      {
69          if (0 == this.generators.size())
70          {
71              return;
72          }
73          this.cumulativeTotal = 0;
74          for (FrequencyAndObject<O> generator : this.generators)
75          {
76              double frequency = generator.getFrequency();
77              Throw.when(frequency < 0, ProbabilityException.class,
78                      "Negative frequency or probability is not allowed (got " + frequency + ")");
79              this.cumulativeTotal += frequency;
80          }
81      }
82  
83      /** {@inheritDoc} */
84      @Override
85      public final O draw() throws ProbabilityException
86      {
87          Throw.when(0 == this.generators.size(), ProbabilityException.class, "Cannot draw from empty collection");
88          Throw.when(0 == this.cumulativeTotal, ProbabilityException.class, "Sum of frequencies or probabilities must be > 0");
89  
90          double randomValue = this.random.draw() * this.cumulativeTotal;
91          for (FrequencyAndObject<O> fAndO : this.generators)
92          {
93              double frequency = fAndO.getFrequency();
94              if (frequency >= randomValue)
95              {
96                  return fAndO.getObject();
97              }
98              randomValue -= frequency;
99          }
100         // If we get here we missed the intended object by a few ULP; return the first object that has non-zero frequency
101         FrequencyAndObject<O> useThisOne = this.generators.get(0);
102         for (FrequencyAndObject<O> fAndO : this.generators)
103         {
104             if (fAndO.getFrequency() > 0)
105             {
106                 useThisOne = fAndO;
107                 break;
108             }
109         }
110         return useThisOne.getObject();
111     }
112 
113     /**
114      * Append a generator to the internally stored list.
115      * @param generator FrequencyAndObject&lt;O&gt;; the generator to add
116      * @return Distribution&lt;O&gt;; this
117      * @throws ProbabilityException when frequency less than zero
118      */
119     public final Distribution<O> add(final FrequencyAndObject<O> generator) throws ProbabilityException
120     {
121         return add(this.generators.size(), generator);
122     }
123 
124     /**
125      * Insert a generator at the specified position in the internally stored list.
126      * @param index int; position to store the generator
127      * @param generator FrequencyAndObject&lt;O&gt;; the generator to add
128      * @return Distribution&lt;O&gt;; this
129      * @throws ProbabilityException when frequency less than zero
130      */
131     public final Distribution<O> add(final int index, final FrequencyAndObject<O> generator) throws ProbabilityException
132     {
133         Throw.when(generator.getFrequency() < 0, ProbabilityException.class,
134                 "frequency (or probability) must be >= 0 (got " + generator.getFrequency() + ")");
135         this.generators.add(index, generator);
136         fixProbabilities();
137         return this;
138     }
139 
140     /**
141      * Remove the generator at the specified position from the internally stored list.
142      * @param index int; the position
143      * @return this
144      * @throws IndexOutOfBoundsException when index is &lt; 0 or &gt;= size
145      * @throws ProbabilityException if the sum of the remaining probabilities or frequencies adds up to 0
146      */
147     public final Distribution<O> remove(final int index) throws IndexOutOfBoundsException, ProbabilityException
148     {
149         this.generators.remove(index);
150         fixProbabilities();
151         return this;
152     }
153 
154     /**
155      * Replace the generator at the specified position.
156      * @param index int; the position of the generator that must be replaced
157      * @param generator FrequencyAndObject&lt;O&gt;; the new generator and the frequency (or probability)
158      * @return this
159      * @throws ProbabilityException when the frequency (or probability) &lt; 0, or when index is &lt; 0 or &gt;= size
160      */
161     public final Distribution<O> set(final int index, final FrequencyAndObject<O> generator) throws ProbabilityException
162     {
163         Throw.when(generator.getFrequency() < 0, ProbabilityException.class,
164                 "frequency (or probability) must be >= 0 (got " + generator.getFrequency() + ")");
165         try
166         {
167             this.generators.set(index, generator);
168         }
169         catch (IndexOutOfBoundsException ie)
170         {
171             throw new ProbabilityException("Index out of bounds for set operation, index=" + index);
172         }
173         fixProbabilities();
174         return this;
175     }
176 
177     /**
178      * Alter the frequency (or probability) of one of the stored generators.
179      * @param index int; index of the stored generator
180      * @param frequency double; new frequency (or probability)
181      * @return this
182      * @throws ProbabilityException when the frequency (or probability) &lt; 0, or when index is &lt; 0 or &gt;= size
183      */
184     public final Distribution<O> modifyFrequency(final int index, final double frequency) throws ProbabilityException
185     {
186         Throw.when(index < 0 || index >= this.size(), ProbabilityException.class, "Index %s out of range (0..%d)", index,
187                 this.size() - 1);
188         return set(index, new FrequencyAndObject<O>(frequency, this.generators.get(index).getObject()));
189     }
190 
191     /**
192      * Empty the internally stored list.
193      * @return this
194      */
195     public final Distribution<O> clear()
196     {
197         this.generators.clear();
198         return this;
199     }
200 
201     /**
202      * Retrieve one of the internally stored generators.
203      * @param index int; the index of the FrequencyAndObject to retrieve
204      * @return FrequencyAndObject&lt;O&gt;; the generator stored at position <cite>index</cite>
205      * @throws ProbabilityException when index &lt; 0 or &gt;= size()
206      */
207     public final FrequencyAndObject<O> get(final int index) throws ProbabilityException
208     {
209         try
210         {
211             return this.generators.get(index);
212         }
213         catch (IndexOutOfBoundsException ie)
214         {
215             throw new ProbabilityException("Index out of bounds for set operation, index=" + index);
216         }
217     }
218 
219     /**
220      * Report the number of generators.
221      * @return int; the number of generators
222      */
223     public final int size()
224     {
225         return this.generators.size();
226     }
227 
228     /** {@inheritDoc} */
229     @Override
230     public final int hashCode()
231     {
232         final int prime = 31;
233         int result = 1;
234         long temp;
235         temp = Double.doubleToLongBits(this.cumulativeTotal);
236         result = prime * result + (int) (temp ^ (temp >>> 32));
237         result = prime * result + ((this.generators == null) ? 0 : this.generators.hashCode());
238         result = prime * result + ((this.random == null) ? 0 : this.random.hashCode());
239         return result;
240     }
241 
242     /** {@inheritDoc} */
243     @Override
244     @SuppressWarnings("checkstyle:needbraces")
245     public final boolean equals(final Object obj)
246     {
247         if (this == obj)
248             return true;
249         if (obj == null)
250             return false;
251         if (getClass() != obj.getClass())
252             return false;
253         Distribution<?> other = (Distribution<?>) obj;
254         if (Double.doubleToLongBits(this.cumulativeTotal) != Double.doubleToLongBits(other.cumulativeTotal))
255             return false;
256         if (this.generators == null)
257         {
258             if (other.generators != null)
259                 return false;
260         }
261         else if (!this.generators.equals(other.generators))
262             return false;
263         if (this.random == null)
264         {
265             if (other.random != null)
266                 return false;
267         }
268         else if (!this.random.equals(other.random))
269             return false;
270         return true;
271     }
272 
273     /** {@inheritDoc} */
274     @Override
275     public final String toString()
276     {
277         StringBuilder result = new StringBuilder();
278         result.append("Distribution [");
279         String separator = "";
280         for (FrequencyAndObject<O> fAndO : this.generators)
281         {
282             result.append(separator + fAndO.getFrequency() + "->" + fAndO.getObject());
283             separator = ", ";
284         }
285         result.append(']');
286         return result.toString();
287     }
288 
289     /**
290      * Immutable storage for a frequency (or probability) plus a Generator.
291      * <p>
292      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands.<br>
293      * All rights reserved. <br>
294      * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
295      * <p>
296      * @version $Revision$, $LastChangedDate$, by $Author$, initial version Mar 1, 2016 <br>
297      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
298      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
299      * @param <O> Type of the object returned by the draw method
300      */
301     public static class FrequencyAndObject<O> implements Serializable
302     {
303         /** */
304         private static final long serialVersionUID = 20160301L;
305 
306         /** Frequency (or probability) of an object. */
307         private final double frequency;
308 
309         /** The object. */
310         private final O object;
311 
312         /**
313          * Construct a new FrequencyAndObject instance.
314          * @param frequency double; the (<b>not cumulative</b>) frequency (or probability) of the <cite>generatingObject</cite>
315          * @param object O; an object
316          */
317         public FrequencyAndObject(final double frequency, final O object)
318         {
319             this.frequency = frequency;
320             this.object = object;
321         }
322 
323         /**
324          * Retrieve the frequency (or probability) of this FrequencyAndObject.
325          * @return double; the frequency (or probability) of this FrequencyAndObject
326          */
327         public final double getFrequency()
328         {
329             return this.frequency;
330         }
331 
332         /**
333          * Call the draw method of the generatingObject and return its result.
334          * @return O; the result of a call to the draw method of the generatingObject
335          */
336         public final O getObject()
337         {
338             return this.object;
339         }
340 
341         /** {@inheritDoc} */
342         @Override
343         public final int hashCode()
344         {
345             final int prime = 31;
346             int result = 1;
347             long temp;
348             temp = Double.doubleToLongBits(this.frequency);
349             result = prime * result + (int) (temp ^ (temp >>> 32));
350             result = prime * result + ((this.object == null) ? 0 : this.object.hashCode());
351             return result;
352         }
353 
354         /** {@inheritDoc} */
355         @Override
356         @SuppressWarnings("checkstyle:needbraces")
357         public final boolean equals(final Object obj)
358         {
359             if (this == obj)
360                 return true;
361             if (obj == null)
362                 return false;
363             if (getClass() != obj.getClass())
364                 return false;
365             FrequencyAndObject<?> other = (FrequencyAndObject<?>) obj;
366             if (Double.doubleToLongBits(this.frequency) != Double.doubleToLongBits(other.frequency))
367                 return false;
368             if (this.object == null)
369             {
370                 if (other.object != null)
371                     return false;
372             }
373             else if (!this.object.equals(other.object))
374                 return false;
375             return true;
376         }
377 
378         /** {@inheritDoc} */
379         @Override
380         public final String toString()
381         {
382             return "FrequencyAndObject [frequency=" + this.frequency + ", object=" + this.object + "]";
383         }
384 
385     }
386 
387 }