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-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
17   * </p>
18   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
19   * @param <O> Type of the object returned by the draw method
20   */
21  public class Distribution<O> implements Generator<O>, Serializable
22  {
23      /** */
24      private static final long serialVersionUID = 20160301L;
25  
26      /** The generators (with their probabilities or frequencies). */
27      private final List<FrequencyAndObject<O>> generators = new ArrayList<>();
28  
29      /** Sum of all probabilities or frequencies. */
30      private double cumulativeTotal;
31  
32      /** The uniform random generator used to select a Generator. */
33      private final DistUniform random;
34  
35      /**
36       * Construct a new Distribution.
37       * @param generators the generators and their frequencies (or probabilities)
38       * @param stream source for randomness
39       * @throws NullPointerException when generators is null or stream is null
40       * @throws IllegalArgumentException when a frequency (or probability) is negative
41       */
42      public Distribution(final List<FrequencyAndObject<O>> generators, final StreamInterface stream)
43      {
44          this(stream);
45          Throw.whenNull(generators, "generators");
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 source for randomness
55       * @throws NullPointerException when generators is null or stream is null
56       */
57      public Distribution(final StreamInterface stream)
58      {
59          Throw.whenNull(stream, "stream");
60          this.random = new DistUniform(stream, 0, 1);
61      }
62  
63      /**
64       * Compute the cumulative frequencies of the storedGenerators.
65       * @throws IllegalArgumentException on negative frequency
66       */
67      private void fixProbabilities()
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.frequency();
77              this.cumulativeTotal += frequency;
78          }
79      }
80  
81      @Override
82      public final O draw()
83      {
84          Throw.when(0 == this.generators.size(), IllegalStateException.class, "Cannot draw from empty collection");
85          Throw.when(0 == this.cumulativeTotal, IllegalStateException.class, "Sum of frequencies or probabilities must be > 0");
86  
87          double randomValue = this.random.draw() * this.cumulativeTotal;
88          for (FrequencyAndObject<O> fAndO : this.generators)
89          {
90              double frequency = fAndO.frequency();
91              if (frequency >= randomValue)
92              {
93                  return fAndO.object();
94              }
95              randomValue -= frequency;
96          }
97          // If we get here we missed the intended object by a few ULP; return the first object that has non-zero frequency
98          FrequencyAndObject<O> useThisOne = this.generators.get(0);
99          for (FrequencyAndObject<O> fAndO : this.generators)
100         {
101             if (fAndO.frequency() > 0)
102             {
103                 useThisOne = fAndO;
104                 break;
105             }
106         }
107         return useThisOne.object();
108     }
109 
110     /**
111      * Append a generator to the internally stored list.
112      * @param generator the generator to add
113      * @return this
114      * @throws NullPointerException when generator is null
115      */
116     public final Distribution<O> add(final FrequencyAndObject<O> generator)
117     {
118         Throw.whenNull(generator, "generator");
119         return add(this.generators.size(), generator);
120     }
121 
122     /**
123      * Insert a generator at the specified position in the internally stored list.
124      * @param index position to store the generator
125      * @param generator the generator to add
126      * @return this
127      * @throws IndexOutOfBoundsException when index is &lt; 0 or &gt;= size
128      * @throws NullPointerException when generator is null
129      */
130     public final Distribution<O> add(final int index, final FrequencyAndObject<O> generator)
131     {
132         Throw.whenNull(generator, "generator");
133         this.generators.add(index, generator);
134         fixProbabilities();
135         return this;
136     }
137 
138     /**
139      * Remove the generator at the specified position from the internally stored list.
140      * @param index the position
141      * @return this
142      * @throws IndexOutOfBoundsException when index is &lt; 0 or &gt;= size
143      */
144     public final Distribution<O> remove(final int index)
145     {
146         this.generators.remove(index);
147         fixProbabilities();
148         return this;
149     }
150 
151     /**
152      * Replace the generator at the specified position.
153      * @param index the position of the generator that must be replaced
154      * @param generator the new generator and the frequency (or probability)
155      * @return this
156      * @throws IndexOutOfBoundsException when index is &lt; 0 or &gt;= size
157      * @throws NullPointerException when generator is null
158      */
159     public final Distribution<O> set(final int index, final FrequencyAndObject<O> generator)
160     {
161         Throw.whenNull(generator, "generator");
162         this.generators.set(index, generator);
163         fixProbabilities();
164         return this;
165     }
166 
167     /**
168      * Alter the frequency (or probability) of one of the stored generators.
169      * @param index index of the stored generator
170      * @param frequency new frequency (or probability)
171      * @return this
172      * @throws IndexOutOfBoundsException when index is &lt; 0 or &gt;= size
173      * @throws IllegalArgumentException when the frequency (or probability) &lt; 0
174      */
175     public final Distribution<O> modifyFrequency(final int index, final double frequency)
176     {
177         Throw.when(index < 0 || index >= this.size(), IndexOutOfBoundsException.class, "Index %s out of range (0..%d)", index,
178                 this.size() - 1);
179         return set(index, new FrequencyAndObject<O>(frequency, this.generators.get(index).object()));
180     }
181 
182     /**
183      * Empty the internally stored list.
184      * @return this
185      */
186     public final Distribution<O> clear()
187     {
188         this.generators.clear();
189         return this;
190     }
191 
192     /**
193      * Retrieve one of the internally stored generators.
194      * @param index the index of the FrequencyAndObject to retrieve
195      * @return the generator stored at position <cite>index</cite>
196      * @throws IndexOutOfBoundsException when index &lt; 0 or &gt;= size()
197      */
198     public final FrequencyAndObject<O> get(final int index)
199     {
200         return this.generators.get(index);
201     }
202 
203     /**
204      * Report the number of generators.
205      * @return the number of generators
206      */
207     public final int size()
208     {
209         return this.generators.size();
210     }
211 
212     @Override
213     public final int hashCode()
214     {
215         final int prime = 31;
216         int result = 1;
217         long temp;
218         temp = Double.doubleToLongBits(this.cumulativeTotal);
219         result = prime * result + (int) (temp ^ (temp >>> 32));
220         result = prime * result + ((this.generators == null) ? 0 : this.generators.hashCode());
221         result = prime * result + ((this.random == null) ? 0 : this.random.hashCode());
222         return result;
223     }
224 
225     @Override
226     @SuppressWarnings("checkstyle:needbraces")
227     public final boolean equals(final Object obj)
228     {
229         if (this == obj)
230             return true;
231         if (obj == null)
232             return false;
233         if (getClass() != obj.getClass())
234             return false;
235         Distribution<?> other = (Distribution<?>) obj;
236         if (Double.doubleToLongBits(this.cumulativeTotal) != Double.doubleToLongBits(other.cumulativeTotal))
237             return false;
238         if (this.generators == null)
239         {
240             if (other.generators != null)
241                 return false;
242         }
243         else if (!this.generators.equals(other.generators))
244             return false;
245         if (this.random == null)
246         {
247             if (other.random != null)
248                 return false;
249         }
250         else if (!this.random.equals(other.random))
251             return false;
252         return true;
253     }
254 
255     @Override
256     public final String toString()
257     {
258         StringBuilder result = new StringBuilder();
259         result.append("Distribution [");
260         String separator = "";
261         for (FrequencyAndObject<O> fAndO : this.generators)
262         {
263             result.append(separator + fAndO.frequency() + "->" + fAndO.object());
264             separator = ", ";
265         }
266         result.append(']');
267         return result.toString();
268     }
269 
270 }