View Javadoc
1   package trafficcontrol;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertTrue;
5   import static org.junit.jupiter.api.Assertions.fail;
6   
7   import java.io.ByteArrayOutputStream;
8   import java.io.PrintStream;
9   import java.util.LinkedHashMap;
10  import java.util.LinkedHashSet;
11  import java.util.Map;
12  import java.util.Set;
13  
14  import javax.naming.NamingException;
15  
16  import org.djunits.value.vdouble.scalar.Duration;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djutils.immutablecollections.ImmutableSet;
19  import org.mockito.ArgumentMatchers;
20  import org.mockito.Mockito;
21  import org.mockito.invocation.InvocationOnMock;
22  import org.mockito.stubbing.Answer;
23  import org.opentrafficsim.core.dsol.OtsModelInterface;
24  import org.opentrafficsim.core.dsol.OtsSimulator;
25  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
26  import org.opentrafficsim.core.network.Network;
27  import org.opentrafficsim.core.network.NetworkException;
28  import org.opentrafficsim.core.perception.HistoryManagerDevs;
29  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLight;
30  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;
31  import org.opentrafficsim.trafficcontrol.FixedTimeController;
32  import org.opentrafficsim.trafficcontrol.FixedTimeController.SignalGroup;
33  
34  import nl.tudelft.simulation.dsol.SimRuntimeException;
35  
36  /**
37   * Test the fixed time traffic controller class.
38   * <p>
39   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
40   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
41   * </p>
42   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
43   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
44   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
45   */
46  public final class TestFixedTimeController
47  {
48  
49      /** */
50      private TestFixedTimeController()
51      {
52          // do not instantiate test class
53      }
54  
55      /**
56       * Test the constructors and initializers of the signal group and fixed time controller classes.
57       * @throws SimRuntimeException if that happens uncaught; this test has failed
58       * @throws NamingException if that happens uncaught; this test has failed
59       * @throws NetworkException on exception
60       */
61      // TODO: @Test
62      public void testConstructors() throws SimRuntimeException, NamingException, NetworkException
63      {
64          String signalGroupId = "sgId";
65          Set<String> trafficLightIds = new LinkedHashSet<>();
66          String trafficLightId = "08.1";
67          trafficLightIds.add(trafficLightId);
68          Duration signalGroupOffset = Duration.ofSI(5);
69          Duration preGreen = Duration.ofSI(2);
70          Duration green = Duration.ofSI(10);
71          Duration yellow = Duration.ofSI(3.5);
72          try
73          {
74              new SignalGroup(null, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
75              fail("Null pointer for signalGroupId should have thrown a null pointer exception");
76          }
77          catch (NullPointerException npe)
78          {
79              // Ignore expected exception
80          }
81          try
82          {
83              new SignalGroup(signalGroupId, null, signalGroupOffset, preGreen, green, yellow);
84              fail("Null pointer for trafficLightIds should have thrown a null pointer exception");
85          }
86          catch (NullPointerException npe)
87          {
88              // Ignore expected exception
89          }
90          try
91          {
92              new SignalGroup(signalGroupId, trafficLightIds, null, preGreen, green, yellow);
93              fail("Null pointer for signalGroupOffset should have thrown a null pointer exception");
94          }
95          catch (NullPointerException npe)
96          {
97              // Ignore expected exception
98          }
99          try
100         {
101             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, null, green, yellow);
102             fail("Null pointer for preGreen should have thrown a null pointer exception");
103         }
104         catch (NullPointerException npe)
105         {
106             // Ignore expected exception
107         }
108         try
109         {
110             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, null, yellow);
111             fail("Null pointer for green should have thrown a null pointer exception");
112         }
113         catch (NullPointerException npe)
114         {
115             // Ignore expected exception
116         }
117         try
118         {
119             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, null);
120             fail("Null pointer for yellow should have thrown a null pointer exception");
121         }
122         catch (NullPointerException npe)
123         {
124             // Ignore expected exception
125         }
126         try
127         {
128             new SignalGroup(signalGroupId, new LinkedHashSet<String>(), signalGroupOffset, preGreen, green, yellow);
129             fail("Empty list of traffic light ids should have thrown an illegal argument exception");
130         }
131         catch (IllegalArgumentException iae)
132         {
133             // Ignore expected exception
134         }
135         // Test the controller that adds default for pre green time
136         SignalGroup sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, green, yellow);
137         assertEquals(0, sg.getPreGreen().si, 0, "default for pre green");
138         assertEquals(green.si, sg.getGreen().si, 0, "green");
139         assertEquals(yellow.si, sg.getYellow().si, 0, "yellow");
140         // Now that we've tested all ways that the constructor should have told us to go to hell, create a signal group
141         sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
142         assertEquals(signalGroupId, sg.getId(), "group id");
143         assertTrue(sg.toString().startsWith("SignalGroup ["), "toString returns something descriptive");
144 
145         String ftcId = "FTCid";
146         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
147         simulator.initialize(Time.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
148                 HistoryManagerDevs.noHistory(simulator));
149         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
150         String networkId = "networkID";
151         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
152         Network network = new Network(networkId, simulator);
153         network.addObject(trafficLightMap.get(trafficLightId));
154 
155         Duration cycleTime = Duration.ofSI(90);
156         Duration offset = Duration.ofSI(20);
157         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
158         ImmutableSet<String> ids = sg.getTrafficLightIds();
159         for (String tlId : ids)
160         {
161             assertTrue(trafficLightMap.containsKey(tlId), "returned id is in provided set");
162         }
163         for (String tlId : trafficLightMap.keySet())
164         {
165             assertTrue(ids.contains(tlId), "provided id is returned");
166         }
167         signalGroups.add(sg);
168         try
169         {
170             new FixedTimeController(null, simulator, network, cycleTime, offset, signalGroups);
171             fail("Null pointer for controller id should have thrown an exception");
172         }
173         catch (NullPointerException npe)
174         {
175             // Ignore
176         }
177         try
178         {
179             new FixedTimeController(ftcId, null, network, cycleTime, offset, signalGroups);
180             fail("Null pointer for simulator should have thrown an exception");
181         }
182         catch (NullPointerException npe)
183         {
184             // Ignore
185         }
186         try
187         {
188             new FixedTimeController(ftcId, simulator, null, cycleTime, offset, signalGroups);
189             fail("Null pointer for network should have thrown an exception");
190         }
191         catch (NullPointerException npe)
192         {
193             // Ignore
194         }
195         try
196         {
197             new FixedTimeController(ftcId, simulator, network, null, offset, signalGroups);
198             fail("Null pointer for cycle time should have thrown an exception");
199         }
200         catch (NullPointerException npe)
201         {
202             // Ignore
203         }
204         try
205         {
206             new FixedTimeController(ftcId, simulator, network, cycleTime, null, signalGroups);
207             fail("Null pointer for offset should have thrown an exception");
208         }
209         catch (NullPointerException npe)
210         {
211             // Ignore
212         }
213         try
214         {
215             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, null);
216             fail("Null pointer for signal groups should have thrown an exception");
217         }
218         catch (NullPointerException npe)
219         {
220             // Ignore
221         }
222         try
223         {
224             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, new LinkedHashSet<SignalGroup>());
225             fail("Empty signal groups should have thrown an exception");
226         }
227         catch (IllegalArgumentException iae)
228         {
229             // Ignore
230         }
231         try
232         {
233             new FixedTimeController(ftcId, simulator, network, Duration.ofSI(0), offset, signalGroups);
234             fail("Illegal cycle time should hav thrown an exception");
235         }
236         catch (IllegalArgumentException iae)
237         {
238             // Ignore
239         }
240         try
241         {
242             new FixedTimeController(ftcId, simulator, network, Duration.ofSI(-10), offset, signalGroups);
243             fail("Illegal cycle time should hav thrown an exception");
244         }
245         catch (IllegalArgumentException iae)
246         {
247             // Ignore
248         }
249         // Not testing check for identical signal groups; yet
250         // Now that we've tested all ways that the constructor should have told us to go to hell, create a controller
251         FixedTimeController ftc = new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
252         assertEquals(ftcId, ftc.getId(), "FTC id");
253         assertTrue(ftc.toString().startsWith("FixedTimeController ["), "toString returns something descriptive");
254 
255         simulator.runUpTo(Duration.ONE);
256         while (simulator.isStartingOrRunning())
257         {
258             try
259             {
260                 Thread.sleep(100);
261             }
262             catch (InterruptedException exception)
263             {
264                 exception.printStackTrace();
265             }
266         }
267         for (TrafficLight tl : sg.getTrafficLights())
268         {
269             assertTrue(trafficLightMap.containsKey(tl.getId()), "acquired traffic light is in the proved set");
270         }
271         assertEquals(cycleTime.minus(preGreen).minus(green).minus(yellow).si, sg.getRed().si, 0.0001,
272                 "red time makes up remainder of cycle time");
273     }
274 
275     /**
276      * Test detection of non-disjoint sets of traffic lights.
277      * @throws NamingException on exception
278      * @throws SimRuntimeException on exception
279      * @throws NetworkException on exception
280      */
281     // TODO: @Test
282     public void testDisjoint() throws SimRuntimeException, NamingException, NetworkException
283     {
284         String signalGroupId = "sgId1";
285         Set<String> trafficLightIds1 = new LinkedHashSet<>();
286         String trafficLightId = "08.1";
287         trafficLightIds1.add(trafficLightId);
288         Duration signalGroupOffset = Duration.ofSI(5);
289         Duration preGreen = Duration.ofSI(2);
290         Duration green = Duration.ofSI(10);
291         Duration yellow = Duration.ofSI(3.5);
292         SignalGroup sg1 = new SignalGroup(signalGroupId, trafficLightIds1, signalGroupOffset, preGreen, green, yellow);
293         String signalGroupId2 = "sgId2";
294         Set<String> trafficLightIds2 = new LinkedHashSet<>();
295         trafficLightIds2.add(trafficLightId);
296         SignalGroup sg2 = new SignalGroup(signalGroupId2, trafficLightIds2, signalGroupOffset, preGreen, green, yellow);
297 
298         String ftcId = "FTCid";
299         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
300         simulator.initialize(Time.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
301                 HistoryManagerDevs.noHistory(simulator));
302         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
303         String networkId = "networkID";
304         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
305         Network network = new Network(networkId, simulator);
306         network.addObject(trafficLightMap.get(trafficLightId));
307 
308         Duration cycleTime = Duration.ofSI(90);
309         Duration offset = Duration.ofSI(20);
310         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
311         signalGroups.add(sg1);
312         signalGroups.add(sg2);
313         try
314         {
315             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
316             fail("Same traffic light in different signal groups should have thrown an IllegalArgumnentException");
317         }
318         catch (IllegalArgumentException iae)
319         {
320             // Ignore
321         }
322     }
323 
324     /**
325      * Test timing of fixed time controller.
326      * @throws SimRuntimeException if that happens uncaught; this test has failed
327      * @throws NamingException if that happens uncaught; this test has failed
328      * @throws NetworkException on exception
329      */
330     // TODO: @Test
331     public void testTimings() throws SimRuntimeException, NamingException, NetworkException
332     {
333         String signalGroupId = "sgId";
334         Set<String> trafficLightIds = new LinkedHashSet<>();
335         String trafficLightId = "08.1";
336         trafficLightIds.add(trafficLightId);
337         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
338         for (int cycleTime : new int[] {60, 90})
339         {
340             Duration cycle = Duration.ofSI(cycleTime);
341             for (int ftcOffsetTime : new int[] {-100, -10, 0, 10, 100})
342             {
343                 Duration ftcOffset = Duration.ofSI(ftcOffsetTime);
344                 for (int sgOffsetTime : new int[] {-99, -9, 0, 9, 99})
345                 {
346                     Duration sgOffset = Duration.ofSI(sgOffsetTime);
347                     for (int preGreenTime : new int[] {0, 3})
348                     {
349                         Duration preGreen = Duration.ofSI(preGreenTime);
350                         for (int greenTime : new int[] {5, 15, 100})
351                         {
352                             Duration green = Duration.ofSI(greenTime);
353                             for (double yellowTime : new double[] {0, 3.5, 4.5})
354                             {
355                                 Duration yellow = Duration.ofSI(yellowTime);
356                                 double minimumCycleTime = preGreenTime + greenTime + yellowTime;
357                                 SignalGroup sg =
358                                         new SignalGroup(signalGroupId, trafficLightIds, sgOffset, preGreen, green, yellow);
359                                 signalGroups.clear();
360                                 signalGroups.add(sg);
361                                 String ftcId = "FTCid";
362                                 OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
363                                 simulator.initialize(Time.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
364                                         HistoryManagerDevs.noHistory(simulator));
365                                 Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
366                                 String networkId = "networkID";
367                                 trafficLightMap.put(trafficLightId,
368                                         createTrafficLightMock(trafficLightId, networkId, simulator));
369                                 Network network = new Network(networkId, simulator);
370                                 network.addObject(trafficLightMap.get(trafficLightId));
371                                 // System.out.println(cycle);
372                                 FixedTimeController ftc =
373                                         new FixedTimeController(ftcId, simulator, network, cycle, ftcOffset, signalGroups);
374                                 // System.out.print(ftc);
375                                 if (cycleTime < minimumCycleTime)
376                                 {
377                                     PrintStream originalError = System.err;
378                                     boolean exceptionThrown = false;
379                                     try
380                                     {
381                                         while (simulator.getSimulatorTime().si <= 0)
382                                         {
383                                             System.setErr(new PrintStream(new ByteArrayOutputStream()));
384                                             try
385                                             {
386                                                 simulator.step();
387                                             }
388                                             finally
389                                             {
390                                                 System.setErr(originalError);
391                                             }
392                                         }
393                                     }
394                                     catch (SimRuntimeException exception)
395                                     {
396                                         exceptionThrown = true;
397                                         assertTrue(exception.getCause().getCause().getMessage().contains("Cycle time shorter "),
398                                                 "exception explains cycle time problem");
399                                     }
400                                     assertTrue(exceptionThrown,
401                                             "Too short cycle time should have thrown a SimRuntimeException");
402                                 }
403                                 else
404                                 {
405                                     // All transitions are at multiples of 0.5 seconds; check the state at 0.25 and 0.75 in each
406                                     // second
407                                     for (int second = 0; second <= 300; second++)
408                                     {
409                                         simulator.scheduleEventAbs(Duration.ofSI(second + 0.25),
410                                                 () -> checkState(simulator, ftc, true));
411                                         simulator.scheduleEventAbs(Duration.ofSI(second + 0.75),
412                                                 () -> checkState(simulator, ftc, true));
413                                     }
414                                     Duration stopTime = Duration.ofSI(300.0);
415                                     simulator.runUpTo(stopTime);
416                                     while (simulator.isStartingOrRunning())
417                                     {
418                                         try
419                                         {
420                                             Thread.sleep(1);
421                                         }
422                                         catch (InterruptedException exception)
423                                         {
424                                             exception.printStackTrace();
425                                         }
426                                     }
427                                     if (simulator.getSimulatorTime().lt(stopTime))
428                                     {
429                                         // something went wrong; call checkState with stopSimulatorOnError set to false
430                                         checkState(simulator, ftc, Boolean.FALSE);
431                                         fail("checkState should have thrown an assert error");
432                                     }
433                                 }
434                             }
435                         }
436                     }
437                 }
438             }
439         }
440     }
441 
442     /**
443      * Check that the current state of a fixed time traffic light controller matches the design.
444      * @param simulator the simulator
445      * @param ftc the fixed time traffic light controller
446      * @param stopSimulatorOnError if true; stop the simulator on error; if false; execute the failing assert on error
447      */
448     public void checkState(final OtsSimulatorInterface simulator, final FixedTimeController ftc,
449             final boolean stopSimulatorOnError)
450     {
451         double cycleTime = ftc.getCycleTime().si;
452         double time = simulator.getSimulatorTime().si;
453         double mainOffset = ftc.getOffset().si;
454         for (SignalGroup sg : ftc.getSignalGroups())
455         {
456             double phaseOffset = sg.getOffset().si + mainOffset;
457             double phase = time + phaseOffset;
458             while (phase < 0)
459             {
460                 phase += cycleTime;
461             }
462             phase %= cycleTime;
463             TrafficLightColor expectedColor = null;
464             if (phase < sg.getPreGreen().si)
465             {
466                 expectedColor = TrafficLightColor.PREGREEN;
467             }
468             else if (phase < sg.getPreGreen().plus(sg.getGreen()).si)
469             {
470                 expectedColor = TrafficLightColor.GREEN;
471             }
472             else if (phase < sg.getPreGreen().plus(sg.getGreen()).plus(sg.getYellow()).si)
473             {
474                 expectedColor = TrafficLightColor.YELLOW;
475             }
476             else
477             {
478                 expectedColor = TrafficLightColor.RED;
479             }
480             // Verify the color of all traffic lights
481             for (TrafficLight tl : sg.getTrafficLights())
482             {
483                 if (!expectedColor.equals(tl.getTrafficLightColor()))
484                 {
485                     if (stopSimulatorOnError)
486                     {
487                         try
488                         {
489                             simulator.stop();
490                         }
491                         catch (SimRuntimeException exception)
492                         {
493                             exception.printStackTrace();
494                         }
495                     }
496                     else
497                     {
498                         assertEquals(expectedColor + " which is in phase " + phase + " of cycle time " + cycleTime,
499                                 tl.getTrafficLightColor(), "Traffic light color mismatch at simulator time "
500                                         + simulator.getSimulatorTime() + " of signal group " + sg);
501                     }
502                 }
503             }
504         }
505     }
506 
507     /**
508      * Create a mocked OtsModelInterface.
509      * @return OtsModelInterface
510      */
511     public OtsModelInterface createModelMock()
512     {
513         return Mockito.mock(OtsModelInterface.class);
514     }
515 
516     /** Remember current state of all mocked traffic lights. */
517     private Map<String, TrafficLightColor> currentTrafficLightColors = new LinkedHashMap<>();
518 
519     /**
520      * Mock a traffic light.
521      * @param id value that will be returned by the getId method
522      * @param networkId name of network (prepended to id for result of getFullId method)
523      * @param simulator TODO
524      * @return TrafficLight
525      */
526     public TrafficLight createTrafficLightMock(final String id, final String networkId, final OtsSimulatorInterface simulator)
527     {
528         TrafficLight result = Mockito.mock(TrafficLight.class);
529         Mockito.when(result.getId()).thenReturn(id);
530         Mockito.when(result.getFullId()).thenReturn(networkId + "." + id);
531         Mockito.when(result.getTrafficLightColor()).thenAnswer(new Answer<TrafficLightColor>()
532         {
533             @Override
534             public TrafficLightColor answer(final InvocationOnMock invocation) throws Throwable
535             {
536                 return TestFixedTimeController.this.currentTrafficLightColors.get(result.getFullId());
537             }
538         });
539         Mockito.doAnswer((Answer<Void>) invocation ->
540         {
541             TrafficLightColor tlc = invocation.getArgument(0);
542             // System.out.println(simulator.getSimulatorTime() + " changing color of " + result.getFullId() + " from "
543             // + this.currentTrafficLightColors.get(result.getFullId()) + " to " + tlc);
544             this.currentTrafficLightColors.put(result.getFullId(), tlc);
545             return null;
546         }).when(result).setTrafficLightColor(ArgumentMatchers.any(TrafficLightColor.class));
547         this.currentTrafficLightColors.put(result.getFullId(), TrafficLightColor.BLACK);
548         return result;
549     }
550 
551 }