View Javadoc
1   package org.djutils.stats.summarizers.event;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.assertFalse;
5   import static org.junit.Assert.assertNotNull;
6   import static org.junit.Assert.assertNull;
7   import static org.junit.Assert.assertTrue;
8   import static org.junit.Assert.fail;
9   
10  import java.util.Random;
11  
12  import org.djutils.event.Event;
13  import org.djutils.event.EventInterface;
14  import org.djutils.event.EventListenerInterface;
15  import org.djutils.event.EventType;
16  import org.djutils.metadata.MetaData;
17  import org.djutils.stats.ConfidenceInterval;
18  import org.djutils.stats.DistNormalTable;
19  import org.djutils.stats.summarizers.quantileaccumulator.FullStorageAccumulator;
20  import org.djutils.stats.summarizers.quantileaccumulator.NoStorageAccumulator;
21  import org.junit.Test;
22  
23  /**
24   * The TallyTest test the tally.
25   * <p>
26   * Copyright (c) 2002-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
27   * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
28   * project is distributed under a three-clause BSD-style license, which can be found at
29   * <a href="https://simulation.tudelft.nl/dsol/3.0/license.html" target="_blank">
30   * https://simulation.tudelft.nl/dsol/3.0/license.html</a>. <br>
31   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
32   * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
33   */
34  public class EventBasedTallyTest
35  {
36      /** an event to fire. */
37      private static final EventType VALUE_EVENT = new EventType("VALUE_EVENT", MetaData.NO_META_DATA);
38  
39      /** Test the event based tally. */
40      @SuppressWarnings("checkstyle:methodlength")
41      @Test
42      public void testEventBasedTally()
43      {
44          String description = "THIS TALLY IS TESTED";
45          EventBasedTally tally = new EventBasedTally(description);
46  
47          // check the description
48          assertTrue(tally.toString().contains(description));
49          assertEquals(description, tally.getDescription());
50          assertTrue(tally.toString().startsWith("EventBasedTally"));
51  
52          // now we check the initial values
53          assertTrue(Double.valueOf(tally.getMin()).isNaN());
54          assertTrue(Double.valueOf(tally.getMax()).isNaN());
55          assertTrue(Double.valueOf(tally.getSampleMean()).isNaN());
56          assertTrue(Double.valueOf(tally.getSampleVariance()).isNaN());
57          assertTrue(Double.valueOf(tally.getPopulationVariance()).isNaN());
58          assertTrue(Double.valueOf(tally.getSampleStDev()).isNaN());
59          assertTrue(Double.valueOf(tally.getPopulationStDev()).isNaN());
60          assertTrue(Double.valueOf(tally.getSampleSkewness()).isNaN());
61          assertTrue(Double.valueOf(tally.getPopulationSkewness()).isNaN());
62          assertTrue(Double.valueOf(tally.getSampleKurtosis()).isNaN());
63          assertTrue(Double.valueOf(tally.getPopulationKurtosis()).isNaN());
64          assertTrue(Double.valueOf(tally.getSampleExcessKurtosis()).isNaN());
65          assertTrue(Double.valueOf(tally.getPopulationExcessKurtosis()).isNaN());
66          assertEquals(0, tally.getSum(), 0);
67          assertEquals(0L, tally.getN());
68          assertNull(tally.getConfidenceInterval(0.95));
69          assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.LEFT_SIDE_CONFIDENCE));
70          assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE));
71          assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE));
72  
73          // We first fire a wrong event
74          try
75          {
76              tally.notify(new Event(VALUE_EVENT, "ERROR", "ERROR"));
77              fail("tally should react on events.value !instanceOf Double");
78          }
79          catch (Exception exception)
80          {
81              assertNotNull(exception);
82          }
83  
84          // Now we fire some events
85          try
86          {
87              tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.1));
88              assertFalse("mean is now available", Double.isNaN(tally.getSampleMean()));
89              assertTrue("sample variance is not available", Double.isNaN(tally.getSampleVariance()));
90              assertFalse("variance is not available", Double.isNaN(tally.getPopulationVariance()));
91              assertTrue("skewness is not available", Double.isNaN(tally.getPopulationSkewness()));
92              tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.2));
93              assertFalse("sample variance is now available", Double.isNaN(tally.getSampleVariance()));
94              assertTrue("sample skewness is not available", Double.isNaN(tally.getSampleSkewness()));
95              assertFalse("skewness is available", Double.isNaN(tally.getPopulationSkewness()));
96              assertTrue("kurtosis is not available", Double.isNaN(tally.getPopulationKurtosis()));
97              tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.3));
98              assertFalse("skewness is now available", Double.isNaN(tally.getSampleSkewness()));
99              assertFalse("kurtosis is now available", Double.isNaN(tally.getPopulationKurtosis()));
100             assertTrue("sample kurtosis is not available", Double.isNaN(tally.getSampleKurtosis()));
101             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.4));
102             assertFalse("sample kurtosis is now available", Double.isNaN(tally.getSampleKurtosis()));
103             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.5));
104             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.6));
105             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.7));
106             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.8));
107             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.9));
108             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 2.0));
109             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.0));
110         }
111         catch (Exception exception)
112         {
113             fail(exception.getMessage());
114         }
115 
116         // Now we check the tally
117         assertEquals(2.0, tally.getMax(), 1.0E-6);
118         assertEquals(1.0, tally.getMin(), 1.0E-6);
119         assertEquals(11, tally.getN());
120         assertEquals(16.5, tally.getSum(), 1.0E-6);
121         assertEquals(1.5, tally.getSampleMean(), 1.0E-6);
122         assertEquals(0.110000, tally.getSampleVariance(), 1.0E-6);
123         assertEquals(0.331662, tally.getSampleStDev(), 1.0E-6);
124 
125         assertEquals(1.304003602, tally.getConfidenceInterval(0.05)[0], 1E-05);
126         assertEquals(1.695996398, tally.getConfidenceInterval(0.05)[1], 1E-05);
127         assertEquals(1.335514637, tally.getConfidenceInterval(0.10)[0], 1E-05);
128         assertEquals(1.664485363, tally.getConfidenceInterval(0.10)[1], 1E-05);
129         assertEquals(1.356046853, tally.getConfidenceInterval(0.15)[0], 1E-05);
130         assertEquals(1.643953147, tally.getConfidenceInterval(0.15)[1], 1E-05);
131         assertEquals(1.432551025, tally.getConfidenceInterval(0.50)[0], 1E-05);
132         assertEquals(1.567448975, tally.getConfidenceInterval(0.50)[1], 1E-05);
133         assertEquals(1.474665290, tally.getConfidenceInterval(0.80)[0], 1E-05);
134         assertEquals(1.525334710, tally.getConfidenceInterval(0.80)[1], 1E-05);
135         assertEquals(1.493729322, tally.getConfidenceInterval(0.95)[0], 1E-05);
136         assertEquals(1.506270678, tally.getConfidenceInterval(0.95)[1], 1E-05);
137 
138         assertEquals(1.304003602, tally.getConfidenceInterval(0.05, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
139         assertEquals(1.695996398, tally.getConfidenceInterval(0.05, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
140         assertEquals(1.432551025, tally.getConfidenceInterval(0.50, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
141         assertEquals(1.567448975, tally.getConfidenceInterval(0.50, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
142         assertEquals(1.493729322, tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
143         assertEquals(1.506270678, tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
144 
145         assertEquals(1.304003602, tally.getConfidenceInterval(0.025, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
146         assertEquals(1.500000000, tally.getConfidenceInterval(0.025, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
147         assertEquals(1.432551025, tally.getConfidenceInterval(0.25, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
148         assertEquals(1.500000000, tally.getConfidenceInterval(0.25, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
149         assertEquals(1.474665290, tally.getConfidenceInterval(0.40, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
150         assertEquals(1.500000000, tally.getConfidenceInterval(0.40, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
151 
152         assertEquals(1.500000000, tally.getConfidenceInterval(0.025, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
153         assertEquals(1.695996398, tally.getConfidenceInterval(0.025, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
154         assertEquals(1.500000000, tally.getConfidenceInterval(0.25, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
155         assertEquals(1.567448975, tally.getConfidenceInterval(0.25, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
156         assertEquals(1.500000000, tally.getConfidenceInterval(0.40, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
157         assertEquals(1.525334710, tally.getConfidenceInterval(0.40, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
158 
159         // we check the input of the confidence interval
160         try
161         {
162             tally.getConfidenceInterval(0.95, null);
163             fail("null is not defined as side of confidence level");
164         }
165         catch (Exception exception)
166         {
167             assertTrue(exception.getClass().equals(NullPointerException.class));
168         }
169         try
170         {
171             assertNull(tally.getConfidenceInterval(-0.95));
172             fail("should have reacted on wrong confidence level -0.95");
173         }
174         catch (Exception exception)
175         {
176             assertTrue(exception.getClass().equals(IllegalArgumentException.class));
177         }
178         try
179         {
180             assertNull(tally.getConfidenceInterval(1.14));
181             fail("should have reacted on wrong confidence level 1.14");
182         }
183         catch (Exception exception)
184         {
185             assertTrue(exception.getClass().equals(IllegalArgumentException.class));
186         }
187 
188         assertTrue(Math.abs(tally.getSampleMean() - 1.5) < 10E-6);
189 
190         // Let's compute the standard deviation
191         double varianceAccumulator = 0;
192         for (int i = 0; i < 11; i++)
193         {
194             varianceAccumulator = Math.pow(1.5 - (1.0 + i / 10.0), 2) + varianceAccumulator;
195         }
196 
197         assertEquals(varianceAccumulator / 10.0, tally.getSampleVariance(), 1.0E-6);
198         assertEquals(Math.sqrt(varianceAccumulator / 10.0), tally.getSampleStDev(), 1.0E-6);
199 
200         assertEquals(varianceAccumulator / 11.0, tally.getPopulationVariance(), 1.0E-6);
201         assertEquals(Math.sqrt(varianceAccumulator / 11.0), tally.getPopulationStDev(), 1.0E-6);
202     }
203 
204     /**
205      * Test produced events by EventBasedTally.
206      */
207     @Test
208     public void testTallyEventProduction()
209     {
210         EventBasedTally tally = new EventBasedTally("testTally");
211         assertEquals(tally, tally.getSourceId());
212         ObservationEventListener oel = new ObservationEventListener();
213         tally.addListener(oel, StatisticsEvents.OBSERVATION_ADDED_EVENT);
214         assertEquals(0, oel.getObservationEvents());
215 
216         EventType[] types = new EventType[] {StatisticsEvents.N_EVENT, StatisticsEvents.MIN_EVENT, StatisticsEvents.MAX_EVENT,
217                 StatisticsEvents.POPULATION_MEAN_EVENT, StatisticsEvents.POPULATION_VARIANCE_EVENT,
218                 StatisticsEvents.POPULATION_SKEWNESS_EVENT, StatisticsEvents.POPULATION_KURTOSIS_EVENT,
219                 StatisticsEvents.POPULATION_STDEV_EVENT, StatisticsEvents.SUM_EVENT, StatisticsEvents.SAMPLE_MEAN_EVENT,
220                 StatisticsEvents.SAMPLE_VARIANCE_EVENT, StatisticsEvents.SAMPLE_SKEWNESS_EVENT,
221                 StatisticsEvents.SAMPLE_KURTOSIS_EVENT, StatisticsEvents.SAMPLE_STDEV_EVENT};
222         LoggingEventListener[] listeners = new LoggingEventListener[types.length];
223         for (int i = 0; i < types.length; i++)
224         {
225             listeners[i] = new LoggingEventListener();
226             tally.addListener(listeners[i], types[i]);
227         }
228 
229         for (int i = 1; i <= 10; i++)
230         {
231             tally.register(10 * i);
232         }
233 
234         assertEquals(10, oel.getObservationEvents());
235         
236         // values from: https://atozmath.com/StatsUG.aspx
237         Object[] expectedValues =
238                 new Object[] {10L, 10.0, 100.0, 55.0, 825.0, 0.0, 1.7758, 28.7228, 550.0, 55.0, 916.6667, 0.0, 1.5982, 30.2765};
239         for (int i = 0; i < types.length; i++)
240         {
241             assertEquals("Number of events for listener " + types[i], 10, listeners[i].getNumberOfEvents());
242             assertEquals("Event sourceId for listener " + types[i], tally, listeners[i].getLastEvent().getSourceId());
243             assertEquals("Event type for listener " + types[i], types[i], listeners[i].getLastEvent().getType());
244             if (expectedValues[i] instanceof Long)
245             {
246                 assertEquals("Final value for listener " + types[i], expectedValues[i],
247                         listeners[i].getLastEvent().getContent());
248             }
249             else
250             {
251                 double e = ((Double) expectedValues[i]).doubleValue();
252                 double c = ((Double) listeners[i].getLastEvent().getContent()).doubleValue();
253                 assertEquals("Final value for listener " + types[i], e, c, 0.001);
254             }
255         }
256     }
257 
258     /**
259      * Test EventBasedTally with the NoStorageAccumulator.
260      */
261     @Test
262     public void testNoStorageAccumulator()
263     {
264         EventBasedTally tally = new EventBasedTally("test with the NoStorageAccumulator", new NoStorageAccumulator());
265         assertTrue("mean of no data is NaN", Double.isNaN(tally.getSampleMean()));
266         try
267         {
268             tally.getQuantile(0.5);
269             fail("getQuantile of no data should have resulted in an IllegalArgumentException");
270         }
271         catch (IllegalArgumentException iae)
272         {
273             // Ignore expected exception
274         }
275 
276         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 90.0));
277         assertEquals("mean of one value is that value", 90.0, tally.getSampleMean(), 0);
278         try
279         {
280             tally.getQuantile(0.5);
281             fail("getQuantile of one value should have resulted in an IllegalArgumentException");
282         }
283         catch (IllegalArgumentException iae)
284         {
285             // Ignore expected exception
286         }
287 
288         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 110.0));
289         assertEquals("mean of two value", 100.0, tally.getSampleMean(), 0);
290         assertEquals("50% quantile", 100.0, tally.getQuantile(0.5), 0);
291 
292         /*-
293         double sigma = tally.getSampleStDev();
294         double mu = tally.getSampleMean();
295         // For loop below makes painfully clear where the getQuantile method fails
296         // Values are from last table in https://en.wikipedia.org/wiki/Standard_normal_table
297         for (double probability : new double[] {1 - 5.00000E-1, 1 - 1.58655E-1, 1 - 2.27501E-2, 1 - 1.34990E-3, 1 - 3.16712E-5,
298                 1 - 2.86652E-7, 1 - 9.86588E-10, 1 - 1.27981E-12, 1 - 6.22096E-16, 1 - 1.12859E-19, 1 - 7.61985E-24})
299         {
300             double x = tally.getQuantile(probability);
301             System.out.println(String.format("probability=%19.16f 1-probability=%19.16f, x=%19.14f, sigmaCount=%19.16f",
302                     probability, 1 - probability, x, (x - mu) / sigma));
303         }
304         // Output shows that the inverse cumulative probability function works fine up to about 8 sigma
305          */
306 
307         assertEquals("84% is about one sigma", 1, DistNormalTable.getInverseCumulativeProbability(0, 1, 0.84), 0.01);
308         assertEquals("16% is about minus one sigma", -1, DistNormalTable.getInverseCumulativeProbability(0, 1, 0.16), 0.01);
309 
310         // Test for the problem that Peter Knoppers had in Tritapt where really small rounding errors caused sqrt(-1e-14).
311         double value = 166.0 / 25.0;
312         tally.initialize();
313         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
314         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
315         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
316         tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
317         tally.initialize();
318         // Throw a lot of pseudo-randomly normally distributed values in and see if the expected mean and stddev come out
319         double mean = 123.456;
320         double stddev = 234.567;
321         Random random = new Random(123456);
322         for (int sample = 0; sample < 10000; sample++)
323         {
324             value = generateGaussianNoise(mean, stddev, random);
325             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
326         }
327         assertEquals("mean should approximately match", mean, tally.getSampleMean(), stddev / 10);
328         assertEquals("stddev should approximately match", stddev, tally.getSampleStDev(), stddev / 10);
329     }
330 
331     /**
332      * Test the Event based tally with the FullStorageAccumulator.
333      */
334     @Test
335     public void testFullStorageAccumulator()
336     {
337         EventBasedTally tally =
338                 new EventBasedTally("EventBasedTally for FullStorageAccumulator test", new FullStorageAccumulator());
339         // Insert values from 0.0 .. 100.0 (step 1.0)
340         for (int step = 0; step <= 100; step++)
341         {
342             tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.0 * step));
343         }
344         for (double probability : new double[] {0.0, 0.01, 0.1, 0.49, 0.5, 0.51, 0.9, 0.99, 1.0})
345         {
346             double expected = 100 * probability;
347             double got = tally.getQuantile(probability);
348             assertEquals("quantile should match", expected, got, 0.00001);
349         }
350         try
351         {
352             tally.getQuantile(-0.01);
353             fail("negative probability should have thrown an exception");
354         }
355         catch (IllegalArgumentException iae)
356         {
357             // Ignore expected exception
358         }
359 
360         try
361         {
362             tally.getQuantile(1.01);
363             fail("Probability > 1 should have thrown an exception");
364         }
365         catch (IllegalArgumentException iae)
366         {
367             // Ignore expected exception
368         }
369 
370         assertTrue("toString returns something descriptive",
371                 new FullStorageAccumulator().toString().startsWith("FullStorageAccumulator"));
372     }
373 
374     /**
375      * Generate normally distributed values. Derived from https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
376      * @param mu double; mean
377      * @param sigma double; standard deviation
378      * @param random Random; entropy source
379      * @return double; one pseudo random value
380      */
381     double generateGaussianNoise(final double mu, final double sigma, final Random random)
382     {
383         final double epsilon = Double.MIN_VALUE;
384         final double twoPi = Math.PI * 2;
385 
386         double u1, u2;
387         do
388         {
389             u1 = random.nextDouble();
390             u2 = random.nextDouble();
391         }
392         while (u1 <= epsilon);
393 
394         return mu + sigma * Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(twoPi * u2);
395     }
396 
397     /** The listener that counts the OBSERVATION_ADDED_EVENT events and checks correctness. */
398     class ObservationEventListener implements EventListenerInterface
399     {
400         /** */
401         private static final long serialVersionUID = 1L;
402 
403         /** counter for the event. */
404         private int observationEvents = 0;
405 
406         @Override
407         public void notify(final EventInterface event)
408         {
409             assertTrue(event.getType().equals(StatisticsEvents.OBSERVATION_ADDED_EVENT));
410             assertTrue("Content of the event has a wrong type, not DOuble: " + event.getContent().getClass(),
411                     event.getContent() instanceof Double);
412             assertTrue("SourceId of the event has a wrong type, not EventBasedTally: " + event.getSourceId().getClass(),
413                     event.getSourceId() instanceof EventBasedTally);
414             this.observationEvents++;
415         }
416 
417         /**
418          * @return countEvents
419          */
420         public int getObservationEvents()
421         {
422             return this.observationEvents;
423         }
424     }
425 
426 }