View Javadoc
1   package org.opentrafficsim.road.gtu.lane.tactical.toledo;
2   
3   import java.io.Serializable;
4   import java.util.Iterator;
5   import java.util.Random;
6   
7   import org.djunits.unit.AccelerationUnit;
8   import org.djunits.unit.LengthUnit;
9   import org.djunits.unit.LinearDensityUnit;
10  import org.djunits.value.vdouble.scalar.Acceleration;
11  import org.djunits.value.vdouble.scalar.Length;
12  import org.djunits.value.vdouble.scalar.LinearDensity;
13  import org.djunits.value.vdouble.scalar.Speed;
14  import org.djunits.value.vdouble.scalar.Time;
15  import org.djutils.draw.point.OrientedPoint2d;
16  import org.djutils.exceptions.Try;
17  import org.opentrafficsim.base.parameters.ParameterException;
18  import org.opentrafficsim.base.parameters.ParameterTypeLength;
19  import org.opentrafficsim.base.parameters.ParameterTypes;
20  import org.opentrafficsim.base.parameters.Parameters;
21  import org.opentrafficsim.core.gtu.GtuException;
22  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
23  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
24  import org.opentrafficsim.core.network.LateralDirectionality;
25  import org.opentrafficsim.core.network.NetworkException;
26  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
27  import org.opentrafficsim.road.gtu.lane.perception.CategoricalLanePerception;
28  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
29  import org.opentrafficsim.road.gtu.lane.perception.PerceptionCollectable;
30  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
31  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.DirectNeighborsPerception;
32  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.HeadwayGtuType;
33  import org.opentrafficsim.road.gtu.lane.perception.categories.neighbors.NeighborsPerception;
34  import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayGtu;
35  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneChange;
36  import org.opentrafficsim.road.gtu.lane.plan.operational.LaneOperationalPlanBuilder;
37  import org.opentrafficsim.road.gtu.lane.tactical.AbstractLaneBasedTacticalPlanner;
38  import org.opentrafficsim.road.gtu.lane.tactical.following.CarFollowingModel;
39  import org.opentrafficsim.road.network.LaneChangeInfo;
40  import org.opentrafficsim.road.network.speed.SpeedLimitInfo;
41  import org.opentrafficsim.road.network.speed.SpeedLimitProspect;
42  
43  /**
44   * Implementation of the model of Toledo (2003).<br>
45   * <br>
46   * Tomer Toledo (2003) "Integrated Driving Behavior Modeling", Massachusetts Institute of Technology.
47   * <p>
48   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
49   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
50   * </p>
51   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
52   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
53   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
54   */
55  
56  public class Toledo extends AbstractLaneBasedTacticalPlanner
57  {
58  
59      /** */
60      private static final long serialVersionUID = 20160711L;
61  
62      /** Look ahead parameter type. */
63      protected static final ParameterTypeLength LOOKAHEAD = ParameterTypes.LOOKAHEAD;
64  
65      /** Defines light vs heavy vehicles. */
66      static final Length MAX_LIGHT_VEHICLE_LENGTH = new Length(9.14, LengthUnit.METER);
67  
68      /** Distance defining tail gating. */
69      static final Length TAILGATE_LENGTH = new Length(10, LengthUnit.METER);
70  
71      /** Density for tail gating (Level Of Service (LOS) A, B or C). */
72      static final LinearDensity LOS_DENSITY = new LinearDensity(16, LinearDensityUnit.PER_KILOMETER);
73  
74      /** Random number generator. */
75      public static final Random RANDOM = new Random();
76  
77      /** Lane change status. */
78      private final LaneChange laneChange;
79  
80      /**
81       * Constructor.
82       * @param carFollowingModel Car-following model.
83       * @param gtu GTU
84       */
85      public Toledo(final CarFollowingModel carFollowingModel, final LaneBasedGtu gtu)
86      {
87          super(carFollowingModel, gtu, new CategoricalLanePerception(gtu));
88          this.laneChange = Try.assign(() -> new LaneChange(gtu), "Parameter LCDUR is required.", GtuException.class);
89          getPerception().addPerceptionCategory(new ToledoPerception(getPerception()));
90          getPerception().addPerceptionCategory(new DirectNeighborsPerception(getPerception(), HeadwayGtuType.WRAP));
91      }
92  
93      @Override
94      public final OperationalPlan generateOperationalPlan(final Time startTime, final OrientedPoint2d locationAtStartTime)
95              throws GtuException, NetworkException, ParameterException
96      {
97  
98          // obtain objects to get info
99          LanePerception perception = getPerception();
100         SpeedLimitProspect slp =
101                 perception.getPerceptionCategory(ToledoPerception.class).getSpeedLimitProspect(RelativeLane.CURRENT);
102         SpeedLimitInfo sli = slp.getSpeedLimitInfo(Length.ZERO);
103         Parameters params = getGtu().getParameters();
104 
105         Acceleration acceleration = null;
106 
107         // if (this.laneChangeDirectionality == null)
108         LateralDirectionality initiatedLaneChange;
109         NeighborsPerception neighbors = perception.getPerceptionCategory(NeighborsPerception.class);
110         if (!this.laneChange.isChangingLane())
111         {
112             // not changing lane
113 
114             // 3rd layer of model: Target gap model
115             // TODO vehicle not ahead and not backwards but completely adjacent
116             GapInfo gapFwdL = getGapInfo(params, perception, Gap.FORWARD, RelativeLane.LEFT);
117             GapInfo gapAdjL = getGapInfo(params, perception, Gap.ADJACENT, RelativeLane.LEFT);
118             GapInfo gapBckL = getGapInfo(params, perception, Gap.BACKWARD, RelativeLane.LEFT);
119             GapInfo gapFwdR = getGapInfo(params, perception, Gap.FORWARD, RelativeLane.RIGHT);
120             GapInfo gapAdjR = getGapInfo(params, perception, Gap.ADJACENT, RelativeLane.RIGHT);
121             GapInfo gapBckR = getGapInfo(params, perception, Gap.BACKWARD, RelativeLane.RIGHT);
122             double emuTgL =
123                     Math.log(Math.exp(gapFwdL.getUtility()) + Math.exp(gapAdjL.getUtility()) + Math.exp(gapBckL.getUtility()));
124             double emuTgR =
125                     Math.log(Math.exp(gapFwdR.getUtility()) + Math.exp(gapAdjR.getUtility()) + Math.exp(gapBckR.getUtility()));
126 
127             // 2nd layer of model: Gap-acceptance model
128             // gap acceptance random terms (variable over time, equal for left and right)
129             double eLead = RANDOM.nextGaussian() * Math.pow(params.getParameter(ToledoLaneChangeParameters.SIGMA_LEAD), 2);
130             double eLag = RANDOM.nextGaussian() * Math.pow(params.getParameter(ToledoLaneChangeParameters.SIGMA_LAG), 2);
131             GapAcceptanceInfo gapAcceptL =
132                     getGapAcceptanceInfo(getGtu(), params, perception, emuTgL, eLead, eLag, RelativeLane.LEFT);
133             GapAcceptanceInfo gapAcceptR =
134                     getGapAcceptanceInfo(getGtu(), params, perception, emuTgR, eLead, eLag, RelativeLane.RIGHT);
135 
136             // 1st layer of model: Target lane model
137             double vL = laneUtility(getGtu(), params, perception, gapAcceptL.getEmu(), sli, RelativeLane.LEFT);
138             double vC = laneUtility(getGtu(), params, perception, 0, sli, RelativeLane.CURRENT);
139             double vR = laneUtility(getGtu(), params, perception, gapAcceptR.getEmu(), sli, RelativeLane.RIGHT);
140 
141             // change lane?
142             if (vL > vR && vL > vC && gapAcceptL.isAcceptable())
143             {
144                 initiatedLaneChange = LateralDirectionality.LEFT;
145             }
146             else if (vR > vL && vR > vC && gapAcceptR.isAcceptable())
147             {
148                 initiatedLaneChange = LateralDirectionality.RIGHT;
149             }
150             else
151             {
152                 initiatedLaneChange = LateralDirectionality.NONE;
153             }
154 
155             // accelerate for gap selection
156             if (initiatedLaneChange.isNone())
157             {
158                 if ((vC > vR && vC > vL)
159                         || (!neighbors.getLeaders(RelativeLane.CURRENT).isEmpty() && neighbors.getLeaders(RelativeLane.CURRENT)
160                                 .first().getDistance().lt(getCarFollowingModel().desiredHeadway(params, getGtu().getSpeed()))))
161                 {
162                     acceleration = getCarFollowingModel().followingAcceleration(params, getGtu().getSpeed(), sli,
163                             neighbors.getLeaders(RelativeLane.CURRENT));
164                 }
165                 else
166                 {
167                     GapInfo gapAdj;
168                     GapInfo gapFwd;
169                     GapInfo gapBck;
170                     if (vL > vR && vL > vC)
171                     {
172                         gapAdj = gapAdjL;
173                         gapFwd = gapFwdL;
174                         gapBck = gapBckL;
175                     }
176                     else
177                     {
178                         gapAdj = gapAdjR;
179                         gapFwd = gapFwdR;
180                         gapBck = gapBckR;
181                     }
182                     if (gapAdj.getUtility() > gapFwd.getUtility() && gapAdj.getUtility() > gapBck.getUtility())
183                     {
184                         // adjacent gap selected
185                         double eadj = Toledo.RANDOM.nextGaussian() * params.getParameter(ToledoLaneChangeParameters.SIGMA_ADJ)
186                                 * params.getParameter(ToledoLaneChangeParameters.SIGMA_ADJ);
187                         acceleration =
188                                 new Acceleration(
189                                         params.getParameter(ToledoLaneChangeParameters.C_ADJ_ACC)
190                                                 * (params.getParameter(ToledoLaneChangeParameters.BETA_DP)
191                                                         * gapAdj.getLength().si - gapAdj.getDistance().si)
192                                                 + eadj,
193                                         AccelerationUnit.SI);
194                     }
195                     else if (gapFwd.getUtility() > gapAdj.getUtility() && gapFwd.getUtility() > gapBck.getUtility())
196                     {
197                         // forward gap selected
198                         Length desiredPosition = new Length(
199                                 gapFwd.getDistance().si
200                                         + params.getParameter(ToledoLaneChangeParameters.BETA_DP) * gapFwd.getLength().si,
201                                 LengthUnit.SI);
202                         double deltaV = gapFwd.getSpeed().si > getGtu().getSpeed().si
203                                 ? params.getParameter(ToledoLaneChangeParameters.LAMBDA_FWD_POS)
204                                         * (gapFwd.getSpeed().si - getGtu().getSpeed().si)
205                                 : params.getParameter(ToledoLaneChangeParameters.LAMBDA_FWD_NEG)
206                                         * (getGtu().getSpeed().si - gapFwd.getSpeed().si);
207                         double efwd = Toledo.RANDOM.nextGaussian() * params.getParameter(ToledoLaneChangeParameters.SIGMA_FWD)
208                                 * params.getParameter(ToledoLaneChangeParameters.SIGMA_FWD);
209                         acceleration = new Acceleration(params.getParameter(ToledoLaneChangeParameters.C_FWD_ACC)
210                                 * Math.pow(desiredPosition.si, params.getParameter(ToledoLaneChangeParameters.BETA_FWD))
211                                 * Math.exp(deltaV) + efwd, AccelerationUnit.SI);
212                     }
213                     else
214                     {
215                         // backward gap selected
216                         Length desiredPosition = new Length(
217                                 gapBck.getDistance().si
218                                         + (1 - params.getParameter(ToledoLaneChangeParameters.BETA_DP)) * gapBck.getLength().si,
219                                 LengthUnit.SI);
220                         double deltaV = gapBck.getSpeed().si > getGtu().getSpeed().si
221                                 ? params.getParameter(ToledoLaneChangeParameters.LAMBDA_BCK_POS)
222                                         * (gapBck.getSpeed().si - getGtu().getSpeed().si)
223                                 : params.getParameter(ToledoLaneChangeParameters.LAMBDA_BCK_NEG)
224                                         * (getGtu().getSpeed().si - gapBck.getSpeed().si);
225                         double ebck = Toledo.RANDOM.nextGaussian() * params.getParameter(ToledoLaneChangeParameters.SIGMA_BCK)
226                                 * params.getParameter(ToledoLaneChangeParameters.SIGMA_BCK);
227                         acceleration = new Acceleration(params.getParameter(ToledoLaneChangeParameters.C_BCK_ACC)
228                                 * Math.pow(desiredPosition.si, params.getParameter(ToledoLaneChangeParameters.BETA_BCK))
229                                 * Math.exp(deltaV) + ebck, AccelerationUnit.SI);
230                     }
231                 }
232             }
233         }
234         else
235         {
236             initiatedLaneChange = LateralDirectionality.NONE;
237         }
238 
239         if (initiatedLaneChange.isLeft())
240         {
241             // changing left
242             getCarFollowingModel().followingAcceleration(params, getGtu().getSpeed(), sli,
243                     neighbors.getLeaders(RelativeLane.LEFT));
244         }
245         else if (initiatedLaneChange.isRight())
246         {
247             // changing right
248             getCarFollowingModel().followingAcceleration(params, getGtu().getSpeed(), sli,
249                     neighbors.getLeaders(RelativeLane.RIGHT));
250         }
251 
252         if (acceleration == null)
253         {
254             throw new Error("Acceleration from toledo model is null.");
255         }
256 
257         // operational plan
258         if (initiatedLaneChange.isNone())
259         {
260             return LaneOperationalPlanBuilder.buildAccelerationPlan(getGtu(), startTime, getGtu().getSpeed(), acceleration,
261                     params.getParameter(ToledoLaneChangeParameters.DT), false);
262         }
263 
264         OperationalPlan plan = LaneOperationalPlanBuilder.buildAccelerationLaneChangePlan(getGtu(), initiatedLaneChange,
265                 getGtu().getLocation(), startTime, getGtu().getSpeed(), acceleration,
266                 params.getParameter(ToledoLaneChangeParameters.DT), this.laneChange);
267         return plan;
268     }
269 
270     /**
271      * Returns info regarding a gap.
272      * @param params parameters
273      * @param perception perception
274      * @param gap gap
275      * @param lane lane
276      * @return utility of gap
277      * @throws ParameterException if parameter is not given
278      * @throws OperationalPlanException perception exception
279      */
280     private GapInfo getGapInfo(final Parameters params, final LanePerception perception, final Gap gap, final RelativeLane lane)
281             throws ParameterException, OperationalPlanException
282     {
283 
284         // capture no leaders/follower cases for forward and backward gaps
285         NeighborsPerception neighbors = perception.getPerceptionCategory(NeighborsPerception.class);
286         if (gap.equals(Gap.FORWARD) && (neighbors.getLeaders(lane) == null || neighbors.getLeaders(lane).isEmpty()))
287         {
288             // no leaders
289             return new GapInfo(Double.NEGATIVE_INFINITY, null, null, null);
290         }
291         if (gap.equals(Gap.BACKWARD) && (neighbors.getFollowers(lane) == null || neighbors.getFollowers(lane).isEmpty()))
292         {
293             // no followers
294             return new GapInfo(Double.NEGATIVE_INFINITY, null, null, null);
295         }
296 
297         double constant; // utility function: constant
298         double alpha; // utility function: alpha parameter
299         Length distanceToGap; // utility function: distance to gap
300         int deltaFrontVehicle; // utility function: dummy whether the gap is limited by vehicle in front
301         Length putativeLength; // acceleration model: gap length, not affected by vehicle in front
302         Length putativeDistance; // acceleration model: distance to gap, different from 'distanceToGap' in utility function
303         Speed putativeSpeed; // acceleration model: speed of putative follower or leader
304         Length leaderDist; // leader of (effective) gap
305         Speed leaderSpeed;
306         Length followerDist; // follower of (effective) gap
307         Speed followerSpeed;
308 
309         if (gap.equals(Gap.FORWARD))
310         {
311             constant = params.getParameter(ToledoLaneChangeParameters.C_FWD_TG);
312             alpha = 0;
313             PerceptionCollectable<HeadwayGtu, LaneBasedGtu> itAble = neighbors.getLeaders(lane);
314             HeadwayGtu second = null;
315             if (!itAble.isEmpty())
316             {
317                 Iterator<HeadwayGtu> it = itAble.iterator();
318                 it.next();
319                 second = it.next();
320             }
321             if (second != null)
322             {
323                 // two leaders
324                 Iterator<HeadwayGtu> it = neighbors.getLeaders(lane).iterator();
325                 it.next();
326                 HeadwayGtu leader = it.next();
327                 leaderDist = leader.getDistance();
328                 leaderSpeed = leader.getSpeed();
329                 putativeLength = leaderDist.minus(neighbors.getLeaders(lane).first().getDistance())
330                         .minus(neighbors.getLeaders(lane).first().getLength());
331             }
332             else
333             {
334                 // TODO infinite -> some limited space, speed also
335                 // one leader
336                 leaderDist = Length.POSITIVE_INFINITY;
337                 leaderSpeed = Speed.POSITIVE_INFINITY;
338                 putativeLength = leaderDist;
339             }
340             // distance to nose, so add length
341             followerDist =
342                     neighbors.getLeaders(lane).first().getDistance().plus(neighbors.getLeaders(lane).first().getLength());
343             followerSpeed = neighbors.getLeaders(lane).first().getSpeed();
344             distanceToGap = followerDist; // same as distance to nose of first leader
345             putativeDistance = distanceToGap;
346             putativeSpeed = followerSpeed;
347         }
348         else if (gap.equals(Gap.ADJACENT))
349         {
350             constant = 0;
351             alpha = params.getParameter(ToledoLaneChangeParameters.ALPHA_ADJ);
352             distanceToGap = Length.ZERO;
353             if (neighbors.getLeaders(lane) != null && !neighbors.getLeaders(lane).isEmpty())
354             {
355                 leaderDist = neighbors.getLeaders(lane).first().getDistance();
356                 leaderSpeed = neighbors.getLeaders(lane).first().getSpeed();
357             }
358             else
359             {
360                 leaderDist = Length.POS_MAXVALUE;
361                 leaderSpeed = Speed.POS_MAXVALUE;
362             }
363             if (neighbors.getFollowers(lane) != null && !neighbors.getFollowers(lane).isEmpty())
364             {
365                 // plus own vehicle length for distance from nose of own vehicle (and then whole negative)
366                 followerDist = neighbors.getFollowers(lane).first().getDistance().plus(getGtu().getLength());
367                 followerSpeed = neighbors.getFollowers(lane).first().getSpeed();
368                 putativeDistance = followerDist;
369             }
370             else
371             {
372                 followerDist = Length.NEG_MAXVALUE;
373                 followerSpeed = Speed.NEG_MAXVALUE;
374                 putativeDistance = Length.POS_MAXVALUE;
375             }
376             putativeSpeed = null;
377             putativeLength = leaderDist.plus(followerDist).plus(getGtu().getLength());
378         }
379         else
380         {
381             constant = params.getParameter(ToledoLaneChangeParameters.C_BCK_TG);
382             alpha = params.getParameter(ToledoLaneChangeParameters.ALPHA_BCK);
383             deltaFrontVehicle = 0;
384             PerceptionCollectable<HeadwayGtu, LaneBasedGtu> itAble = neighbors.getFollowers(lane);
385             HeadwayGtu second = null;
386             if (!itAble.isEmpty())
387             {
388                 Iterator<HeadwayGtu> it = itAble.iterator();
389                 it.next();
390                 second = it.next();
391             }
392             if (second != null)
393             {
394                 // two followers
395                 Iterator<HeadwayGtu> it = neighbors.getFollowers(lane).iterator();
396                 it.next();
397                 HeadwayGtu follower = it.next();
398                 followerDist = follower.getDistance();
399                 followerSpeed = follower.getSpeed();
400                 putativeLength = followerDist.minus(neighbors.getFollowers(lane).first().getDistance())
401                         .minus(neighbors.getFollowers(lane).first().getLength());
402             }
403             else
404             {
405                 // one follower
406                 followerDist = Length.NEGATIVE_INFINITY;
407                 followerSpeed = Speed.NEGATIVE_INFINITY;
408                 putativeLength = followerDist;
409             }
410             // add vehicle length to get distance to tail of 1st follower (and then whole negative)
411             leaderDist =
412                     neighbors.getFollowers(lane).first().getDistance().plus(neighbors.getLeaders(lane).first().getLength());
413             leaderSpeed = neighbors.getFollowers(lane).first().getSpeed();
414             distanceToGap = neighbors.getFollowers(lane).first().getDistance().plus(getGtu().getLength()); // own nose to nose
415             putativeDistance = distanceToGap.plus(neighbors.getFollowers(lane).first().getLength());
416             putativeSpeed = leaderSpeed;
417         }
418 
419         // limit by leader in current lane
420         if (!gap.equals(Gap.BACKWARD) && !neighbors.getLeaders(RelativeLane.CURRENT).isEmpty()
421                 && neighbors.getLeaders(RelativeLane.CURRENT).first().getDistance().lt(leaderDist))
422         {
423             leaderDist = neighbors.getLeaders(RelativeLane.CURRENT).first().getDistance();
424             leaderSpeed = neighbors.getLeaders(RelativeLane.CURRENT).first().getSpeed();
425             deltaFrontVehicle = 1;
426         }
427         else
428         {
429             deltaFrontVehicle = 0;
430         }
431 
432         // calculate relative gap speed and effective gap
433         Speed dV = followerSpeed.minus(leaderSpeed);
434         Length effectiveGap = leaderDist.minus(followerDist);
435         // {@formatter:off}
436         // calculate utility 
437         double util = constant 
438                 + params.getParameter(ToledoLaneChangeParameters.BETA_DTG) * distanceToGap.si
439                 + params.getParameter(ToledoLaneChangeParameters.BETA_EG) * effectiveGap.si
440                 + params.getParameter(ToledoLaneChangeParameters.BETA_FV) * deltaFrontVehicle
441                 + params.getParameter(ToledoLaneChangeParameters.BETA_RGS) * dV.si
442                 + alpha * params.getParameter(ToledoLaneChangeParameters.ERROR_TERM);
443         // {@formatter:on}
444         return new GapInfo(util, putativeLength, putativeDistance, putativeSpeed);
445 
446     }
447 
448     /**
449      * Returns info regarding gap-acceptance.
450      * @param gtu GTU
451      * @param params parameters
452      * @param perception perception
453      * @param emuTg emu from target gap model
454      * @param eLead lead error
455      * @param eLag lag error
456      * @param lane lane to evaluate
457      * @return info regarding gap-acceptance
458      * @throws ParameterException if parameter is not defined
459      * @throws OperationalPlanException perception exception
460      */
461     private GapAcceptanceInfo getGapAcceptanceInfo(final LaneBasedGtu gtu, final Parameters params,
462             final LanePerception perception, final double emuTg, final double eLead, final double eLag, final RelativeLane lane)
463             throws ParameterException, OperationalPlanException
464     {
465 
466         // get lead and lag utility and acceptance
467         double vLead;
468         double vLag;
469         boolean acceptLead;
470         boolean acceptLag;
471 
472         NeighborsPerception neighbors = perception.getPerceptionCategory(NeighborsPerception.class);
473         if (neighbors.getLeaders(lane) != null && !neighbors.getLeaders(lane).isEmpty())
474         {
475             Speed dVLead = neighbors.getLeaders(lane).first().getSpeed().minus(gtu.getSpeed());
476             Length sLead = neighbors.getLeaders(lane).first().getDistance();
477             // critical gap
478             Length gLead =
479                     new Length(
480                             Math.exp(params.getParameter(ToledoLaneChangeParameters.C_LEAD)
481                                     + params.getParameter(ToledoLaneChangeParameters.BETA_POS_LEAD)
482                                             * Speed.max(dVLead, Speed.ZERO).si
483                                     + params.getParameter(ToledoLaneChangeParameters.BETA_NEG_LEAD)
484                                             * Speed.min(dVLead, Speed.ZERO).si
485                                     + params.getParameter(ToledoLaneChangeParameters.BETA_EMU_LEAD) * emuTg
486                                     + params.getParameter(ToledoLaneChangeParameters.ALPHA_TL_LEAD)
487                                             * params.getParameter(ToledoLaneChangeParameters.ERROR_TERM)
488                                     + eLead),
489                             LengthUnit.SI);
490             vLead = Math.log(gLead.si) - (params.getParameter(ToledoLaneChangeParameters.C_LEAD)
491                     + params.getParameter(ToledoLaneChangeParameters.BETA_POS_LEAD) * Speed.max(dVLead, Speed.ZERO).si
492                     + params.getParameter(ToledoLaneChangeParameters.BETA_NEG_LEAD) * Speed.min(dVLead, Speed.ZERO).si
493                     + params.getParameter(ToledoLaneChangeParameters.BETA_EMU_LEAD) * emuTg);
494             acceptLead = gLead.lt(sLead);
495         }
496         else
497         {
498             vLead = 0;
499             acceptLead = false;
500         }
501 
502         if (neighbors.getFollowers(lane) != null && !neighbors.getFollowers(lane).isEmpty())
503         {
504             Speed dVLag = neighbors.getFollowers(lane).first().getSpeed().minus(gtu.getSpeed());
505             Length sLag = neighbors.getFollowers(lane).first().getDistance();
506             // critical gap
507             Length gLag =
508                     new Length(
509                             Math.exp(params.getParameter(ToledoLaneChangeParameters.C_LAG)
510                                     + params.getParameter(ToledoLaneChangeParameters.BETA_POS_LAG)
511                                             * Speed.max(dVLag, Speed.ZERO).si
512                                     + params.getParameter(ToledoLaneChangeParameters.BETA_EMU_LAG) * emuTg
513                                     + params.getParameter(ToledoLaneChangeParameters.ALPHA_TL_LAG)
514                                             * params.getParameter(ToledoLaneChangeParameters.ERROR_TERM)
515                                     + eLag),
516                             LengthUnit.SI);
517             vLag = Math.log(gLag.si) - (params.getParameter(ToledoLaneChangeParameters.C_LAG)
518                     + params.getParameter(ToledoLaneChangeParameters.BETA_POS_LAG) * Speed.max(dVLag, Speed.ZERO).si
519                     + params.getParameter(ToledoLaneChangeParameters.BETA_EMU_LAG) * emuTg);
520             acceptLag = gLag.lt(sLag);
521         }
522         else
523         {
524             vLag = 0;
525             acceptLag = false;
526         }
527 
528         // gap acceptance emu
529         double vRatLead = vLead / params.getParameter(ToledoLaneChangeParameters.SIGMA_LEAD);
530         double vRatLag = vLag / params.getParameter(ToledoLaneChangeParameters.SIGMA_LAG);
531         double emuGa = vLead * cumNormDist(vRatLead)
532                 + params.getParameter(ToledoLaneChangeParameters.SIGMA_LEAD) * normDist(vRatLead) + vLag * cumNormDist(vRatLag)
533                 + params.getParameter(ToledoLaneChangeParameters.SIGMA_LAG) * normDist(vRatLag);
534 
535         // return info
536         return new GapAcceptanceInfo(emuGa, acceptLead && acceptLag);
537 
538     }
539 
540     /**
541      * Returns the utility of a lane.
542      * @param gtu GTU
543      * @param params parameters
544      * @param perception perception
545      * @param emuGa emu from gap acceptance model
546      * @param sli speed limit info
547      * @param lane lane to evaluate
548      * @return utility of lane
549      * @throws ParameterException if parameter is not defined
550      * @throws OperationalPlanException perception exception
551      */
552     private double laneUtility(final LaneBasedGtu gtu, final Parameters params, final LanePerception perception,
553             final double emuGa, final SpeedLimitInfo sli, final RelativeLane lane)
554             throws ParameterException, OperationalPlanException
555     {
556 
557         ToledoPerception toledo = perception.getPerceptionCategory(ToledoPerception.class);
558         if (!perception.getLaneStructure().exists(lane))
559         {
560             return 0.0;
561         }
562 
563         // get infrastructure info
564         boolean takeNextOffRamp = false;
565         // for (LaneChangeInfo info : toledo.getInfrastructureLaneChangeInfo(RelativeLane.CURRENT))
566         // {
567         // if (info.getSplitNumber() == 1)
568         // {
569         // takeNextOffRamp = true;
570         // }
571         // }
572         int deltaNextExit = takeNextOffRamp ? 1 : 0;
573 
574         Length dExit;
575         if (!toledo.getInfrastructureLaneChangeInfo(RelativeLane.CURRENT).isEmpty())
576         {
577             dExit = toledo.getInfrastructureLaneChangeInfo(RelativeLane.CURRENT).first().remainingDistance();
578         }
579         else
580         {
581             dExit = Length.POSITIVE_INFINITY;
582         }
583 
584         int[] delta = new int[3];
585         int deltaAdd = 0;
586         if (!toledo.getInfrastructureLaneChangeInfo(lane).isEmpty())
587         {
588             LaneChangeInfo ilciLef = toledo.getInfrastructureLaneChangeInfo(lane).first();
589             if (ilciLef.numberOfLaneChanges() > 1 && ilciLef.numberOfLaneChanges() < 5)
590             {
591                 deltaAdd = ilciLef.numberOfLaneChanges() - 2;
592                 delta[deltaAdd] = 1;
593             }
594         }
595 
596         // heavy neighbor
597         NeighborsPerception neighbors = perception.getPerceptionCategory(NeighborsPerception.class);
598         Length leaderLength = !lane.isCurrent() && !neighbors.getFirstLeaders(lane.getLateralDirectionality()).isEmpty()
599                 ? neighbors.getFirstLeaders(lane.getLateralDirectionality()).first().getLength() : Length.ZERO;
600         Length followerLength = !lane.isCurrent() && !neighbors.getFirstFollowers(lane.getLateralDirectionality()).isEmpty()
601                 ? neighbors.getFirstFollowers(lane.getLateralDirectionality()).first().getDistance() : Length.ZERO;
602         int deltaHeavy = leaderLength.gt(MAX_LIGHT_VEHICLE_LENGTH) || followerLength.gt(MAX_LIGHT_VEHICLE_LENGTH) ? 1 : 0;
603 
604         // get density
605         LinearDensity d = getDensityInLane(gtu, perception, lane);
606 
607         // tail gating
608         int deltaTailgate = 0;
609         if (lane.equals(RelativeLane.CURRENT) && !neighbors.getFollowers(RelativeLane.CURRENT).isEmpty()
610                 && neighbors.getFollowers(RelativeLane.CURRENT).first().getDistance().le(TAILGATE_LENGTH))
611         {
612             LinearDensity dL = getDensityInLane(gtu, perception, RelativeLane.LEFT);
613             LinearDensity dR = getDensityInLane(gtu, perception, RelativeLane.RIGHT);
614             LinearDensity dRoad = new LinearDensity((dL.si + d.si + dR.si) / 3, LinearDensityUnit.SI);
615             if (dRoad.le(LOS_DENSITY))
616             {
617                 deltaTailgate = 1;
618             }
619         }
620 
621         // right most
622         // TODO definition of 'right most lane' does not account for ramps and weaving sections
623         int deltaRightMost = 0;
624         if (lane.equals(RelativeLane.CURRENT) && !toledo.getCrossSection().contains(RelativeLane.RIGHT))
625         {
626             deltaRightMost = 1;
627         }
628         else if (lane.equals(RelativeLane.RIGHT) && toledo.getCrossSection().contains(RelativeLane.RIGHT)
629                 && !toledo.getCrossSection().contains(RelativeLane.SECOND_RIGHT))
630         {
631             deltaRightMost = 1;
632         }
633 
634         // current lane traffic
635         Speed vFront;
636         Length sFront;
637         if (lane.equals(RelativeLane.CURRENT))
638         {
639             if (!neighbors.getLeaders(RelativeLane.CURRENT).isEmpty())
640             {
641                 vFront = neighbors.getLeaders(RelativeLane.CURRENT).first().getSpeed();
642                 sFront = neighbors.getLeaders(RelativeLane.CURRENT).first().getDistance();
643             }
644             else
645             {
646                 vFront = getCarFollowingModel().desiredSpeed(params, sli);
647                 sFront = getCarFollowingModel().desiredHeadway(params, gtu.getSpeed());
648             }
649         }
650         else
651         {
652             vFront = Speed.ZERO;
653             sFront = Length.ZERO;
654         }
655 
656         // target lane utility
657         double constant = 0;
658         double error = 0;
659         if (lane.equals(RelativeLane.CURRENT))
660         {
661             constant = params.getParameter(ToledoLaneChangeParameters.C_CL);
662             error = params.getParameter(ToledoLaneChangeParameters.ALPHA_CL)
663                     * params.getParameter(ToledoLaneChangeParameters.ERROR_TERM);
664         }
665         else if (lane.equals(RelativeLane.RIGHT))
666         {
667             constant = params.getParameter(ToledoLaneChangeParameters.C_RL);
668             error = params.getParameter(ToledoLaneChangeParameters.ALPHA_RL)
669                     * params.getParameter(ToledoLaneChangeParameters.ERROR_TERM);
670         }
671         // {@formatter:off}
672         return constant
673                 + params.getParameter(ToledoLaneChangeParameters.BETA_RIGHT_MOST) * deltaRightMost // 0 for LEFT
674                 + params.getParameter(ToledoLaneChangeParameters.BETA_VFRONT) * vFront.si // 0 for LEFT/RIGHT
675                 + params.getParameter(ToledoLaneChangeParameters.BETA_SFRONT) * sFront.si // 0 for LEFT/RIGHT
676                 + params.getParameter(ToledoLaneChangeParameters.BETA_DENSITY) * d.getInUnit(LinearDensityUnit.PER_KILOMETER)
677                 + params.getParameter(ToledoLaneChangeParameters.BETA_HEAVY_NEIGHBOUR) * deltaHeavy
678                 + params.getParameter(ToledoLaneChangeParameters.BETA_TAILGATE) * deltaTailgate
679                 + Math.pow(dExit.getInUnit(LengthUnit.KILOMETER), params.getParameter(ToledoLaneChangeParameters.THETA_MLC)) 
680                 * (params.getParameter(ToledoLaneChangeParameters.BETA1) * delta[0] 
681                         + params.getParameter(ToledoLaneChangeParameters.BETA2) * delta[1] 
682                         + params.getParameter(ToledoLaneChangeParameters.BETA3) * delta[2])
683                 + params.getParameter(ToledoLaneChangeParameters.BETA_NEXT_EXIT) * deltaNextExit
684                 + params.getParameter(ToledoLaneChangeParameters.BETA_ADD) * deltaAdd
685                 + params.getParameter(ToledoLaneChangeParameters.BETA_EMU_GA) * emuGa // 0 for CURRENT (given correct input)
686                 + error; // 0 for LEFT
687         // {@formatter:on}
688     }
689 
690     /**
691      * Returns the density in the given lane based on the following and leading vehicles.
692      * @param gtu subject GTU
693      * @param perception perception
694      * @param lane lane to get density of
695      * @return density in the given lane based on the following and leading vehicles
696      * @throws OperationalPlanException perception exception
697      */
698     private LinearDensity getDensityInLane(final LaneBasedGtu gtu, final LanePerception perception, final RelativeLane lane)
699             throws OperationalPlanException
700     {
701         int nVehicles = 0;
702         NeighborsPerception neighbors = perception.getPerceptionCategory(NeighborsPerception.class);
703         Length up = Length.ZERO;
704         Length down = Length.ZERO;
705         for (HeadwayGtu neighbor : neighbors.getFollowers(lane))
706         {
707             nVehicles++;
708             down = neighbor.getDistance();
709         }
710         for (HeadwayGtu neighbor : neighbors.getLeaders(lane))
711         {
712             nVehicles++;
713             up = neighbor.getDistance();
714         }
715         if (nVehicles > 0)
716         {
717             return new LinearDensity(nVehicles / up.plus(down).getInUnit(LengthUnit.KILOMETER),
718                     LinearDensityUnit.PER_KILOMETER);
719         }
720         return LinearDensity.ZERO;
721     }
722 
723     /**
724      * Returns the cumulative density function (CDF) at given value of the standard normal distribution.
725      * @param x value
726      * @return cumulative density function (CDF) at given value of the standard normal distribution
727      */
728     private static double cumNormDist(final double x)
729     {
730         return .5 * (1 + erf(x / Math.sqrt(2)));
731     }
732 
733     /**
734      * Error function approximation using Horner's method.
735      * @param x value
736      * @return error function approximation
737      */
738     private static double erf(final double x)
739     {
740         double t = 1.0 / (1.0 + 0.5 * Math.abs(x));
741         // use Horner's method
742         // {@formatter:off}
743         double tau = t * Math.exp(-x * x - 1.26551223 + t * (1.00002368 + t * (0.37409196 
744                 + t * (0.09678418 + t * (0.18628806 + t * (0.27886807 + t * (-1.13520398 
745                         + t * (1.48851587 + t * (-0.82215223 + t * (0.17087277))))))))));
746         // {@formatter:on}
747         return x >= 0 ? 1 - tau : tau - 1;
748     }
749 
750     /**
751      * Returns the probability density function (PDF) at given value of the standard normal distribution.
752      * @param x value
753      * @return probability density function (PDF) at given value of the standard normal distribution
754      */
755     private static double normDist(final double x)
756     {
757         return Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
758     }
759 
760     /**
761      * Gap indicator in adjacent lane.
762      * <p>
763      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
764      * <br>
765      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
766      * </p>
767      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
768      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
769      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
770      */
771     private enum Gap
772     {
773         /** Gap in front of leader in adjacent lane. */
774         FORWARD,
775 
776         /** Gap between follower and leader in adjacent lane. */
777         ADJACENT,
778 
779         /** Gap behind follower in adjacent lane. */
780         BACKWARD;
781     }
782 
783     /**
784      * Contains info regarding an adjacent gap.
785      * <p>
786      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
787      * <br>
788      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
789      * </p>
790      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
791      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
792      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
793      */
794     private class GapInfo implements Serializable
795     {
796 
797         /** */
798         private static final long serialVersionUID = 20160811L;
799 
800         /** Utility of the gap. */
801         private final double utility;
802 
803         /** Length of the gap. */
804         private final Length length;
805 
806         /** Distance towards the gap. */
807         private final Length distance;
808 
809         /** Speed of the vehicle in front or behind the gap, always the closest. */
810         private final Speed speed;
811 
812         /**
813          * @param utility utility of gap
814          * @param length length of the gap
815          * @param distance distance towards the gap
816          * @param speed speed of the vehicle in front or behind the gap, always the closest
817          */
818         GapInfo(final double utility, final Length length, final Length distance, final Speed speed)
819         {
820             this.utility = utility;
821             this.length = length;
822             this.distance = distance;
823             this.speed = speed;
824         }
825 
826         /**
827          * @return utility
828          */
829         public final double getUtility()
830         {
831             return this.utility;
832         }
833 
834         /**
835          * @return length
836          */
837         public final Length getLength()
838         {
839             return this.length;
840         }
841 
842         /**
843          * @return distance
844          */
845         public final Length getDistance()
846         {
847             return this.distance;
848         }
849 
850         /**
851          * @return speed
852          */
853         public final Speed getSpeed()
854         {
855             return this.speed;
856         }
857 
858         @Override
859         public String toString()
860         {
861             return "GapAcceptanceInfo [utility = " + this.utility + ", length = " + this.length + ", distance = "
862                     + this.distance + ", speed = " + this.speed + "]";
863         }
864 
865     }
866 
867     /**
868      * Contains info regarding gap-acceptance.
869      * <p>
870      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
871      * <br>
872      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
873      * </p>
874      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
875      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
876      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
877      */
878     private class GapAcceptanceInfo implements Serializable
879     {
880 
881         /** */
882         private static final long serialVersionUID = 20160811L;
883 
884         /** Emu value of gap-acceptance. */
885         private final double emu;
886 
887         /** Whether gap is acceptable. */
888         private final boolean acceptable;
889 
890         /**
891          * @param emu emu
892          * @param acceptable whether gap is acceptable
893          */
894         GapAcceptanceInfo(final double emu, final boolean acceptable)
895         {
896             this.emu = emu;
897             this.acceptable = acceptable;
898         }
899 
900         /**
901          * @return emu
902          */
903         public final double getEmu()
904         {
905             return this.emu;
906         }
907 
908         /**
909          * @return acceptable
910          */
911         public final boolean isAcceptable()
912         {
913             return this.acceptable;
914         }
915 
916         @Override
917         public String toString()
918         {
919             return "GapAcceptanceInfo [emu = " + this.emu + ", acceptable = " + this.acceptable + "]";
920         }
921 
922     }
923 
924     @Override
925     public final String toString()
926     {
927         return "Toledo tactical planner.";
928     }
929 
930 }