View Javadoc
1   package org.opentrafficsim.road.gtu.tactical.util.lmrs;
2   
3   import java.util.Optional;
4   
5   import org.djunits.value.vdouble.scalar.Acceleration;
6   import org.djunits.value.vdouble.scalar.Duration;
7   import org.djunits.value.vdouble.scalar.Length;
8   import org.djunits.value.vdouble.scalar.Speed;
9   import org.opentrafficsim.base.TimeStampedObject;
10  import org.opentrafficsim.base.parameters.ParameterException;
11  import org.opentrafficsim.base.parameters.ParameterTypeAcceleration;
12  import org.opentrafficsim.base.parameters.ParameterTypeDuration;
13  import org.opentrafficsim.base.parameters.ParameterTypes;
14  import org.opentrafficsim.base.parameters.Parameters;
15  import org.opentrafficsim.core.gtu.GtuException;
16  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
17  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
18  import org.opentrafficsim.core.network.LateralDirectionality;
19  import org.opentrafficsim.core.network.NetworkException;
20  import org.opentrafficsim.road.gtu.LaneBasedGtu;
21  import org.opentrafficsim.road.gtu.operational.SimpleOperationalPlan;
22  import org.opentrafficsim.road.gtu.perception.PerceptionCollectable;
23  import org.opentrafficsim.road.gtu.perception.RelativeLane;
24  import org.opentrafficsim.road.gtu.perception.categories.InfrastructurePerception;
25  import org.opentrafficsim.road.gtu.perception.categories.IntersectionPerception;
26  import org.opentrafficsim.road.gtu.perception.categories.neighbors.NeighborsPerception;
27  import org.opentrafficsim.road.gtu.perception.object.PerceivedConflict;
28  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtu;
29  import org.opentrafficsim.road.gtu.perception.object.PerceivedTrafficLight;
30  import org.opentrafficsim.road.gtu.tactical.Synchronizable;
31  import org.opentrafficsim.road.gtu.tactical.TacticalContext;
32  import org.opentrafficsim.road.gtu.tactical.TacticalContextEgo;
33  import org.opentrafficsim.road.gtu.tactical.lmrs.AbstractIncentivesTacticalPlanner;
34  import org.opentrafficsim.road.gtu.tactical.util.CarFollowingUtil;
35  import org.opentrafficsim.road.gtu.tactical.util.ConflictUtil;
36  import org.opentrafficsim.road.gtu.tactical.util.ConflictUtil.ConflictPlans;
37  import org.opentrafficsim.road.gtu.tactical.util.TrafficLightUtil;
38  import org.opentrafficsim.road.network.conflict.Conflict;
39  
40  /**
41   * <p>
42   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
43   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
44   * </p>
45   * @author Alexander Verbraeck
46   * @author Peter Knoppers
47   * @author Wouter Schakel
48   */
49  public final class LmrsUtil implements LmrsParameters
50  {
51  
52      /** Fixed model time step. */
53      public static final ParameterTypeDuration DT = ParameterTypes.DT;
54  
55      /** Minimum car-following headway. */
56      public static final ParameterTypeDuration TMIN = ParameterTypes.TMIN;
57  
58      /** Current car-following headway. */
59      public static final ParameterTypeDuration T = ParameterTypes.T;
60  
61      /** Maximum car-following headway. */
62      public static final ParameterTypeDuration TMAX = ParameterTypes.TMAX;
63  
64      /** Headway relaxation time. */
65      public static final ParameterTypeDuration TAU = ParameterTypes.TAU;
66  
67      /** Maximum critical deceleration, e.g. stop/go at traffic light. */
68      public static final ParameterTypeAcceleration BCRIT = ParameterTypes.BCRIT;
69  
70      /** Parameter key. */
71      private static final Object PARAMETER_KEY = new Object()
72      {
73          @Override
74          public String toString()
75          {
76              return "LmrsUtil.PARAMETER_KEY";
77          }
78      };
79  
80      /** Parameter key for T. This value might e.g. be set by relaxation, its initialization, and tailgating. */
81      public static final Object T_KEY = new Object()
82      {
83          @Override
84          public String toString()
85          {
86              return "LmrsUtil.T_KEY";
87          }
88      };
89  
90      /**
91       * Do not instantiate.
92       */
93      private LmrsUtil()
94      {
95          //
96      }
97  
98      /**
99       * Determines a simple representation of an operational plan.
100      * @param context tactical information such as parameters and car-following model
101      * @param lmrsData LMRS data
102      * @param incentives planner with set of incentives
103      * @return simple operational plan
104      * @throws GtuException gtu exception
105      * @throws NetworkException network exception
106      * @throws ParameterException parameter exception
107      * @throws OperationalPlanException operational plan exception
108      */
109     @SuppressWarnings("checkstyle:methodlength")
110     public static SimpleOperationalPlan determinePlan(final TacticalContextEgo context, final LmrsData lmrsData,
111             final AbstractIncentivesTacticalPlanner incentives) throws GtuException, NetworkException, ParameterException
112     {
113         lmrsData.initStep();
114 
115         // obtain objects to get info
116         NeighborsPerception neighbors = context.getPerception().getPerceptionCategory(NeighborsPerception.class);
117         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> leaders = neighbors.getLeaders(RelativeLane.CURRENT);
118 
119         // regular car-following
120         Acceleration a;
121         if (lmrsData.isHumanLongitudinalControl())
122         {
123             lmrsData.getTailgating().tailgate(context);
124             if (!leaders.isEmpty() && lmrsData.isNewLeader(leaders.first()))
125             {
126                 initHeadwayRelaxation(context.getParameters(), leaders.first());
127             }
128             a = context.getCarFollowingAcceleration();
129         }
130         else
131         {
132             a = Acceleration.POS_MAXVALUE;
133         }
134         lmrsData.setDesiredSpeed(context.getDesiredSpeed());
135 
136         // determine lane change desire based on incentives
137         Desire desire = getLaneChangeDesire(context, incentives);
138 
139         // lane change decision
140         LateralDirectionality initiatedOrContinuedLaneChange;
141         TurnIndicatorStatus turnIndicatorStatus = null;
142         double dFree = context.getParameters().getParameter(DFREE);
143         initiatedOrContinuedLaneChange = LateralDirectionality.NONE;
144         if (desire.leftIsLargerOrEqual() && desire.left() >= dFree)
145         {
146             if (acceptLaneChange(context, desire.left(), LateralDirectionality.LEFT, lmrsData.getGapAcceptance()))
147             {
148                 // change left
149                 initiatedOrContinuedLaneChange = LateralDirectionality.LEFT;
150                 turnIndicatorStatus = TurnIndicatorStatus.LEFT;
151                 context.getParameters().setClaimedParameter(DLC, desire.left(), PARAMETER_KEY);
152                 setDesiredHeadway(context.getParameters(), desire.left(), false);
153                 leaders = neighbors.getLeaders(RelativeLane.LEFT);
154                 if (!leaders.isEmpty())
155                 {
156                     // don't respond on its lane change desire, but remember it such that it isn't a new leader in the next
157                     // step
158                     lmrsData.isNewLeader(leaders.first());
159                 }
160                 a = Acceleration.min(a,
161                         context.getCarFollowingModel().followingAcceleration(context.getParameters(), context.getSpeed(),
162                                 context.getSpeedLimits(), context.getMaximumSpeed(), neighbors.getLeaders(RelativeLane.LEFT)));
163             }
164         }
165         else if (!desire.leftIsLargerOrEqual() && desire.right() >= dFree)
166         {
167             if (acceptLaneChange(context, desire.right(), LateralDirectionality.RIGHT, lmrsData.getGapAcceptance()))
168             {
169                 // change right
170                 initiatedOrContinuedLaneChange = LateralDirectionality.RIGHT;
171                 turnIndicatorStatus = TurnIndicatorStatus.RIGHT;
172                 context.getParameters().setClaimedParameter(DLC, desire.right(), PARAMETER_KEY);
173                 setDesiredHeadway(context.getParameters(), desire.right(), false);
174                 leaders = neighbors.getLeaders(RelativeLane.RIGHT);
175                 if (!leaders.isEmpty())
176                 {
177                     // don't respond on its lane change desire, but remember it such that it isn't a new leader in the next step
178                     lmrsData.isNewLeader(leaders.first());
179                 }
180                 a = Acceleration.min(a,
181                         context.getCarFollowingModel().followingAcceleration(context.getParameters(), context.getSpeed(),
182                                 context.getSpeedLimits(), context.getMaximumSpeed(), neighbors.getLeaders(RelativeLane.RIGHT)));
183             }
184         }
185 
186         context.getParameters().setClaimedParameter(DLEFT, desire.left(), PARAMETER_KEY);
187         context.getParameters().setClaimedParameter(DRIGHT, desire.right(), PARAMETER_KEY);
188         if (initiatedOrContinuedLaneChange.isNone())
189         {
190             // take action if we cannot change lane
191             Acceleration aSync;
192 
193             // synchronize
194             double dSync = context.getParameters().getParameter(DSYNC);
195             lmrsData.setSynchronizationState(Synchronizable.State.NONE);
196             if (desire.leftIsLargerOrEqual() && desire.left() >= dSync)
197             {
198                 if (desire.left() >= context.getParameters().getParameter(DCOOP))
199                 {
200                     // switch on left indicator
201                     turnIndicatorStatus = TurnIndicatorStatus.LEFT;
202                     lmrsData.setSynchronizationState(Synchronizable.State.INDICATING);
203                 }
204                 else
205                 {
206                     lmrsData.setSynchronizationState(Synchronizable.State.SYNCHRONIZING);
207                 }
208                 aSync = lmrsData.getSynchronization().synchronize(context, desire.left(), LateralDirectionality.LEFT, lmrsData,
209                         initiatedOrContinuedLaneChange);
210                 a = applyAcceleration(a, aSync);
211             }
212             else if (!desire.leftIsLargerOrEqual() && desire.right() >= dSync)
213             {
214                 if (desire.right() >= context.getParameters().getParameter(DCOOP))
215                 {
216                     // switch on right indicator
217                     turnIndicatorStatus = TurnIndicatorStatus.RIGHT;
218                     lmrsData.setSynchronizationState(Synchronizable.State.INDICATING);
219                 }
220                 else
221                 {
222                     lmrsData.setSynchronizationState(Synchronizable.State.SYNCHRONIZING);
223                 }
224                 aSync = lmrsData.getSynchronization().synchronize(context, desire.right(), LateralDirectionality.RIGHT,
225                         lmrsData, initiatedOrContinuedLaneChange);
226                 a = applyAcceleration(a, aSync);
227             }
228 
229             // cooperate
230             aSync = lmrsData.getCooperation().cooperate(context, LateralDirectionality.LEFT, lmrsData, desire);
231             a = applyAcceleration(a, aSync);
232             aSync = lmrsData.getCooperation().cooperate(context, LateralDirectionality.RIGHT, lmrsData, desire);
233             a = applyAcceleration(a, aSync);
234 
235             // relaxation
236             exponentialHeadwayRelaxation(context.getParameters());
237         }
238 
239         SimpleOperationalPlan simplePlan =
240                 new SimpleOperationalPlan(a, context.getParameters().getParameter(DT), initiatedOrContinuedLaneChange);
241 
242         // set turn indicator
243         if (turnIndicatorStatus != null)
244         {
245             context.addIntent(turnIndicatorStatus, Length.ZERO);
246         }
247         else
248         {
249             // lingering indicator during lane change
250             TimeStampedObject<TurnIndicatorStatus> lingeringLaneChangeIndicator = lmrsData.getInitiatedLaneChange();
251             if (lingeringLaneChangeIndicator != null)
252             {
253                 if (lingeringLaneChangeIndicator.timestamp().si > context.getTime().si
254                         - context.getParameters().getParameter(ParameterTypes.LCDUR).si)
255                 {
256                     context.addIntent(lingeringLaneChangeIndicator.object(), Length.ZERO);
257                 }
258                 else
259                 {
260                     lmrsData.setInitiatedLaneChange(null);
261                 }
262             }
263         }
264         if (simplePlan.isLaneChange())
265         {
266             turnIndicatorStatus =
267                     simplePlan.getLaneChangeDirection().isLeft() ? TurnIndicatorStatus.LEFT : TurnIndicatorStatus.RIGHT;
268             lmrsData.setInitiatedLaneChange(new TimeStampedObject<TurnIndicatorStatus>(turnIndicatorStatus, context.getTime()));
269         }
270         return simplePlan;
271 
272     }
273 
274     /**
275      * Minimizes the acceleration and sets the synchronization state if applicable.
276      * @param a previous acceleration
277      * @param aNew new acceleration
278      * @return minimized acceleration
279      */
280     private static Acceleration applyAcceleration(final Acceleration a, final Acceleration aNew)
281     {
282         if (a.si < aNew.si)
283         {
284             return a;
285         }
286         return aNew;
287     }
288 
289     /**
290      * Sets the headway as a response to a new leader.
291      * @param params parameters
292      * @param leader leader
293      * @throws ParameterException if DLC is not present
294      */
295     private static void initHeadwayRelaxation(final Parameters params, final PerceivedGtu leader) throws ParameterException
296     {
297         Optional<Double> dlc = leader.getBehavior().getParameters().getOptionalParameter(DLC);
298         if (dlc.isPresent())
299         {
300             setDesiredHeadway(params, dlc.get(), false);
301         }
302         // else could not be perceived
303     }
304 
305     /**
306      * Updates the desired headway following an exponential shape approximated with fixed time step <code>DT</code>.
307      * @param params parameters
308      * @throws ParameterException in case of a parameter exception
309      */
310     private static void exponentialHeadwayRelaxation(final Parameters params) throws ParameterException
311     {
312         double ratio = params.getParameter(DT).si / params.getParameter(TAU).si;
313         params.setClaimedParameter(T,
314                 Duration.interpolate(params.getParameter(T), params.getParameter(TMAX), ratio <= 1.0 ? ratio : 1.0), T_KEY);
315     }
316 
317     /**
318      * Determines lane change desire for the given GtU. Mandatory desire is deduced as the maximum of a set of mandatory
319      * incentives, while voluntary desires are added. Depending on the level of mandatory lane change desire, voluntary desire
320      * may be included partially. If both are positive or negative, voluntary desire is fully included. Otherwise, voluntary
321      * desire is less considered within the range dSync &lt; |mandatory| &lt; dCoop. The absolute value is used as large
322      * negative mandatory desire may also dominate voluntary desire.
323      * @param context tactical information such as parameters and car-following model
324      * @param incentives planner with set of incentives
325      * @return lane change desire for gtu
326      * @throws ParameterException if a parameter is not defined
327      * @throws GtuException if there is no mandatory incentive, the model requires at least one
328      */
329     public static Desire getLaneChangeDesire(final TacticalContextEgo context,
330             final AbstractIncentivesTacticalPlanner incentives) throws ParameterException, GtuException
331     {
332         double dSync = context.getParameters().getParameter(DSYNC);
333         double dCoop = context.getParameters().getParameter(DCOOP);
334 
335         Desire mandatoryDesire = incentives.getMandatoryDesire(context);
336         Desire voluntaryDesire = incentives.getVoluntaryDesire(context);
337         double thetaA = context.getParameters().getParameter(LAMBDA_V);
338         double leftThetaV = getThetaV(mandatoryDesire.left(), voluntaryDesire.left(), dSync, dCoop);
339         double rightThetaV = getThetaV(mandatoryDesire.right(), voluntaryDesire.right(), dSync, dCoop);
340         return new Desire(mandatoryDesire.left() + thetaA * leftThetaV * voluntaryDesire.left(),
341                 mandatoryDesire.right() + thetaA * rightThetaV * voluntaryDesire.right());
342     }
343 
344     /**
345      * Obtains theta, which is the level by which voluntary incentives are considered, given the prevalence of mandatory desire.
346      * @param mandatoryDesire mandatory desire
347      * @param voluntaryDesire voluntary desire
348      * @param dSync synchronization threshold
349      * @param dCoop cooperation threshold
350      * @return theta
351      */
352     private static double getThetaV(final double mandatoryDesire, final double voluntaryDesire, final double dSync,
353             final double dCoop)
354     {
355         double leftThetaV = 0;
356         double dLeftMandatoryAbs = Math.abs(mandatoryDesire);
357 
358         if (dLeftMandatoryAbs <= dSync || mandatoryDesire * voluntaryDesire >= 0)
359         {
360             // low mandatory desire, or same sign
361             leftThetaV = 1;
362         }
363         else if (dSync < dLeftMandatoryAbs && dLeftMandatoryAbs < dCoop && mandatoryDesire * voluntaryDesire < 0)
364         {
365             // linear from 1 at dSync to 0 at dCoop
366             leftThetaV = (dCoop - dLeftMandatoryAbs) / (dCoop - dSync);
367         }
368         return leftThetaV;
369     }
370 
371     /**
372      * Determine whether a lane change is acceptable (gap, lane markings, etc.).
373      * @param context tactical information such as parameters and car-following model
374      * @param desire lane change desire
375      * @param lat lateral direction for synchronization
376      * @param gapAcceptance gap-acceptance model
377      * @return whether a gap is acceptable
378      * @throws ParameterException if a parameter is not defined
379      * @throws OperationalPlanException perception exception
380      */
381     static boolean acceptLaneChange(final TacticalContextEgo context, final double desire, final LateralDirectionality lat,
382             final GapAcceptance gapAcceptance) throws ParameterException, OperationalPlanException
383     {
384         // legal?
385         InfrastructurePerception infra = context.getPerception().getPerceptionCategory(InfrastructurePerception.class);
386         if (infra.getLegalLaneChangePossibility(RelativeLane.CURRENT, lat).si <= 0.0)
387         {
388             return false;
389         }
390 
391         // safe regarding neighbors?
392         double consideredDesire = context.getLaneChangeDirection().equals(lat) ? 1.0 : desire;
393         if (!gapAcceptance.acceptGap(context, consideredDesire, lat))
394         {
395             return false;
396         }
397 
398         // intersection causes for deceleration
399         Optional<IntersectionPerception> intersection =
400                 context.getPerception().getPerceptionCategoryOptional(IntersectionPerception.class);
401         if (intersection.isPresent())
402         {
403             RelativeLane lane = new RelativeLane(lat, 1);
404 
405             // // conflicts alongside?
406             // if ((lat.isLeft() && intersection.isAlongsideConflictLeft())
407             // || (lat.isRight() && intersection.isAlongsideConflictRight()))
408             // {
409             // return false;
410             // }
411             // if (quickIntersectionScan(params, sli, cfm, ownSpeed, lat, intersection).lt(params.getParameter(BCRIT).neg()))
412             // {
413             // return false;
414             // }
415 
416             // conflicts
417             PerceptionCollectable<PerceivedConflict, Conflict> conflicts = intersection.get().getConflicts(lane);
418             try
419             {
420                 Acceleration a = ConflictUtil.approachConflicts(context, new ConflictPlans(), lane, Length.ZERO, false);
421                 if (a.lt(context.getParameters().getParameter(ParameterTypes.BCRIT).neg()))
422                 {
423                     return false;
424                 }
425                 // gap-acceptance on merge conflicts
426                 // TODO: this approach is a hack
427                 for (PerceivedConflict conflict : conflicts)
428                 {
429                     if (conflict.isMerge() && conflict.getDistance().si < 10.0)
430                     {
431                         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> down = conflict.getDownstreamConflictingGtus();
432                         if (!down.isEmpty() && down.first().getKinematics().getOverlap().isParallel())
433                         {
434                             return false; // GTU on conflict
435                         }
436                         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> up = conflict.getUpstreamConflictingGtus();
437                         if (!up.isEmpty() && up.first().getKinematics().getOverlap().isParallel())
438                         {
439                             return false; // GTU on conflict
440                         }
441                     }
442                 }
443             }
444             catch (GtuException exception)
445             {
446                 throw new OperationalPlanException(exception);
447             }
448             conflicts = intersection.get().getConflicts(RelativeLane.CURRENT);
449             for (PerceivedConflict conflict : conflicts)
450             {
451                 if (conflict.getLane().getLink().equals(conflict.getConflictingLink()))
452                 {
453                     if (conflict.isMerge() && conflict.getDistance().le0()
454                             && conflict.getDistance().neg().gt(conflict.getLength()))
455                     {
456                         return false; // partially past the merge; adjacent lane might be ambiguous
457                     }
458                     else if (conflict.isSplit() && conflict.getDistance().le0()
459                             && conflict.getDistance().neg().lt(context.getLength()))
460                     {
461                         return false; // partially before the split; adjacent lane might be ambiguous
462                     }
463                 }
464             }
465 
466             // traffic lights
467             Iterable<PerceivedTrafficLight> trafficLights = intersection.get().getTrafficLights(lane);
468             for (PerceivedTrafficLight trafficLight : trafficLights)
469             {
470                 if (trafficLight.getTrafficLightColor().isRedOrYellow())
471                 {
472                     Acceleration a = TrafficLightUtil.respondToTrafficLight(context, trafficLight);
473                     if (a.lt(context.getParameters().getParameter(ParameterTypes.BCRIT).neg()))
474                     {
475                         return false;
476                     }
477                 }
478             }
479         }
480 
481         // cut-in vehicles from 2nd lane
482         RelativeLane lane = new RelativeLane(lat, 2);
483         Acceleration b = context.getParameters().getParameter(ParameterTypes.B).neg();
484         for (PerceivedGtu leader : context.getPerception().getPerceptionCategory(NeighborsPerception.class).getLeaders(lane))
485         {
486             if (leader.getManeuver().isChangingLane(lat.flip()) && CarFollowingUtil.followSingleLeader(context, leader).lt(b))
487             {
488                 return false;
489             }
490         }
491 
492         return true;
493 
494     }
495 
496     /**
497      * Returns a quickly determined acceleration to consider on an adjacent lane, following from conflicts and traffic lights.
498      * @param context tactical information such as parameters and car-following model
499      * @param lat lateral direction for synchronization
500      * @param intersection intersection perception
501      * @return a quickly determined acceleration to consider on an adjacent lane, following from conflicts and traffic lights
502      * @throws ParameterException if a parameter is not defined
503      */
504     /*-
505     private static Acceleration quickIntersectionScan(final TacticalContextEgo context, final LateralDirectionality lat,
506             final IntersectionPerception intersection) throws ParameterException
507     {
508         Acceleration a = Acceleration.POSITIVE_INFINITY;
509         if (intersection != null)
510         {
511             RelativeLane lane = lat.isRight() ? RelativeLane.RIGHT : RelativeLane.LEFT;
512             Iterable<PerceivedConflict> iterable = intersection.getConflicts(lane);
513             if (iterable != null)
514             {
515                 Iterator<PerceivedConflict> conflicts = iterable.iterator();
516                 if (conflicts.hasNext())
517                 {
518                     a = Acceleration.min(a,
519                             CarFollowingUtil.followSingleLeader(context, conflicts.next().getDistance(), Speed.ZERO));
520                 }
521                 Iterator<PerceivedTrafficLight> trafficLights = intersection.getTrafficLights(lane).iterator();
522                 if (trafficLights.hasNext())
523                 {
524                     PerceivedTrafficLight trafficLight = trafficLights.next();
525                     if (trafficLight.getTrafficLightColor().isRedOrYellow())
526                     {
527                         a = Acceleration.min(a,
528                                 CarFollowingUtil.followSingleLeader(context, trafficLight.getDistance(), Speed.ZERO));
529                     }
530                 }
531             }
532         }
533         return a;
534     }
535     */
536 
537     /**
538      * Sets value for T depending on level of lane change desire.
539      * @param params parameters
540      * @param desire lane change desire
541      * @param resettable whether the T value will be reset later (ignoring key), or regular claimed setting (with key)
542      * @throws ParameterException if T, TMIN or TMAX is not in the parameters
543      */
544     static void setDesiredHeadway(final Parameters params, final double desire, final boolean resettable)
545             throws ParameterException
546     {
547         double limitedDesire = desire < 0 ? 0 : desire > 1 ? 1 : desire;
548         double tDes = limitedDesire * params.getParameter(TMIN).si + (1 - limitedDesire) * params.getParameter(TMAX).si;
549         double tSi = params.getParameter(T).si;
550         Duration t = Duration.ofSI(tDes < tSi ? tDes : tSi);
551         if (resettable)
552         {
553             params.setParameterResettable(T, t);
554         }
555         else
556         {
557             params.setClaimedParameter(T, t, T_KEY);
558         }
559     }
560 
561     /**
562      * Resets value for T depending on level of lane change desire.
563      * @param params parameters
564      * @throws ParameterException if T is not in the parameters
565      */
566     static void resetDesiredHeadway(final Parameters params) throws ParameterException
567     {
568         params.resetParameter(T);
569     }
570 
571     /**
572      * Determine acceleration from car-following with desire-adjusted headway.
573      * @param context tactical information such as parameters and car-following model
574      * @param distance distance from follower to leader
575      * @param leaderSpeed speed of leader
576      * @param desire level of lane change desire
577      * @return acceleration from car-following
578      * @throws ParameterException if a parameter is not defined
579      */
580     public static Acceleration singleAcceleration(final TacticalContext context, final Length distance, final Speed leaderSpeed,
581             final double desire) throws ParameterException
582     {
583         // set T
584         setDesiredHeadway(context.getParameters(), desire, true);
585         // calculate acceleration
586         Acceleration a = CarFollowingUtil.followSingleLeader(context, distance, leaderSpeed);
587         // reset T
588         resetDesiredHeadway(context.getParameters());
589         return a;
590     }
591 
592 }