feat: add complete jerk-limited stopping math

This commit is contained in:
梁薄云
2026-08-05 17:07:54 +08:00
parent 6c2f4066c8
commit e56220fc29
5 changed files with 268 additions and 69 deletions
@@ -0,0 +1,203 @@
using System;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
public sealed class JerkLimitedStoppingProfile
{
internal JerkLimitedStoppingProfile(double distanceMeters, double durationSeconds,
double finalSpeedMetersPerSecond, double finalAccelerationMetersPerSecondSquared)
{
DistanceMeters = distanceMeters;
DurationSeconds = durationSeconds;
FinalSpeedMetersPerSecond = finalSpeedMetersPerSecond;
FinalAccelerationMetersPerSecondSquared = finalAccelerationMetersPerSecondSquared;
}
public double DistanceMeters { get; }
public double DurationSeconds { get; }
public double FinalSpeedMetersPerSecond { get; }
public double FinalAccelerationMetersPerSecondSquared { get; }
}
public static class JerkLimitedStoppingMath
{
private const double NumericTolerance = 1e-12d;
public static bool TryCalculate(double speed, double acceleration,
double maximumDeceleration, double maximumJerk,
out JerkLimitedStoppingProfile profile, out string failureReason)
{
profile = null;
failureReason = string.Empty;
if (!IsFinite(speed) || speed < 0d || !IsFinite(acceleration) ||
!IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
acceleration < -maximumDeceleration - NumericTolerance)
{
failureReason = "Stopping inputs are outside finite longitudinal bounds.";
return false;
}
if (speed <= NumericTolerance && Math.Abs(acceleration) <= NumericTolerance)
{
profile = new JerkLimitedStoppingProfile(0d, 0d, 0d, 0d);
return true;
}
double unavoidableReleaseLoss = acceleration < 0d
? acceleration * acceleration / (2d * maximumJerk)
: 0d;
if (speed + NumericTolerance < unavoidableReleaseLoss)
{
failureReason = "The current negative acceleration cannot be released before speed crosses zero.";
return false;
}
double peakDeceleration = Math.Sqrt(maximumJerk * speed +
0.5d * acceleration * acceleration);
double downDuration;
double plateauDuration;
double upDuration;
if (peakDeceleration <= maximumDeceleration + NumericTolerance)
{
peakDeceleration = Math.Min(peakDeceleration, maximumDeceleration);
downDuration = (acceleration + peakDeceleration) / maximumJerk;
plateauDuration = 0d;
upDuration = peakDeceleration / maximumJerk;
}
else
{
peakDeceleration = maximumDeceleration;
downDuration = (acceleration + peakDeceleration) / maximumJerk;
upDuration = peakDeceleration / maximumJerk;
double speedAfterDown = speed + acceleration * downDuration -
0.5d * maximumJerk * downDuration * downDuration;
double releaseLoss = peakDeceleration * peakDeceleration /
(2d * maximumJerk);
plateauDuration = (speedAfterDown - releaseLoss) / peakDeceleration;
}
if (downDuration < -NumericTolerance || plateauDuration < -NumericTolerance)
{
failureReason = "No monotone three-phase jerk-limited stop exists for the current state.";
return false;
}
downDuration = Math.Max(0d, downDuration);
plateauDuration = Math.Max(0d, plateauDuration);
double s = 0d;
double u = speed;
double a = acceleration;
Integrate(ref s, ref u, ref a, -maximumJerk, downDuration);
Integrate(ref s, ref u, ref a, 0d, plateauDuration);
Integrate(ref s, ref u, ref a, maximumJerk, upDuration);
if (Math.Abs(u) > 1e-9d || Math.Abs(a) > 1e-9d || s < -NumericTolerance)
{
failureReason = "The jerk-limited stop did not end at zero speed and acceleration.";
return false;
}
profile = new JerkLimitedStoppingProfile(Math.Max(0d, s),
downDuration + plateauDuration + upDuration, 0d, 0d);
return true;
}
public static double MaximumInitialSpeedForDistance(double availableDistance,
double conservativeInitialAcceleration, double maximumDeceleration,
double maximumJerk, double directionMaximumSpeed)
{
if (!IsFinite(availableDistance) || availableDistance < 0d ||
!IsFinite(conservativeInitialAcceleration) ||
!IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
!IsPositiveFinite(directionMaximumSpeed))
throw new ArgumentOutOfRangeException(nameof(availableDistance));
double lower = 0d;
double upper = directionMaximumSpeed;
for (int iteration = 0; iteration < 64; iteration++)
{
double candidate = 0.5d * (lower + upper);
bool fits = TryCalculate(candidate, conservativeInitialAcceleration,
maximumDeceleration, maximumJerk,
out JerkLimitedStoppingProfile stop, out _) &&
stop.DistanceMeters <= availableDistance + NumericTolerance;
if (fits)
lower = candidate;
else
upper = candidate;
}
return lower;
}
public static double CalculateMaximumStoppedDistance(double initialSpeed,
double initialAcceleration, double maximumSpeed, double maximumAcceleration,
double maximumDeceleration, double maximumJerk, double timeHorizon)
{
if (!IsFinite(initialSpeed) || initialSpeed < 0d ||
!IsFinite(initialAcceleration) || !IsPositiveFinite(maximumSpeed) ||
!IsPositiveFinite(maximumAcceleration) || !IsPositiveFinite(maximumDeceleration) ||
!IsPositiveFinite(maximumJerk) || !IsPositiveFinite(timeHorizon))
throw new ArgumentOutOfRangeException(nameof(timeHorizon));
double lower = 0d;
double upper = timeHorizon;
double bestDistance = 0d;
for (int iteration = 0; iteration < 64; iteration++)
{
double driveDuration = 0.5d * (lower + upper);
AdvanceTowardMaximumSpeed(initialSpeed, initialAcceleration,
maximumSpeed, maximumAcceleration, maximumJerk, driveDuration,
out double driveDistance, out double speed, out double acceleration);
bool fits = TryCalculate(speed, acceleration, maximumDeceleration,
maximumJerk, out JerkLimitedStoppingProfile stop, out _) &&
driveDuration + stop.DurationSeconds <= timeHorizon + NumericTolerance;
if (fits)
{
lower = driveDuration;
bestDistance = Math.Max(bestDistance, driveDistance + stop.DistanceMeters);
}
else
{
upper = driveDuration;
}
}
return bestDistance;
}
private static void AdvanceTowardMaximumSpeed(double initialSpeed, double initialAcceleration,
double maximumSpeed, double maximumAcceleration, double maximumJerk, double duration,
out double distance, out double speed, out double acceleration)
{
distance = 0d;
speed = initialSpeed;
acceleration = initialAcceleration;
double remaining = duration;
while (remaining > NumericTolerance)
{
double dt = Math.Min(0.001d, remaining);
double speedNeededToReleaseAcceleration = acceleration > 0d
? acceleration * acceleration / (2d * maximumJerk)
: 0d;
double jerk = speed + speedNeededToReleaseAcceleration >= maximumSpeed
? (acceleration > 0d ? -maximumJerk : 0d)
: (acceleration < maximumAcceleration ? maximumJerk : 0d);
Integrate(ref distance, ref speed, ref acceleration, jerk, dt);
if (speed > maximumSpeed && speed - maximumSpeed <= 1e-6d)
{
speed = maximumSpeed;
acceleration = 0d;
}
remaining -= dt;
}
}
private static void Integrate(ref double s, ref double u, ref double a,
double jerk, double duration)
{
s += u * duration + 0.5d * a * duration * duration +
jerk * duration * duration * duration / 6d;
u += a * duration + 0.5d * jerk * duration * duration;
a += jerk * duration;
}
private static bool IsPositiveFinite(double value) => IsFinite(value) && value > 0d;
private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value);
}
@@ -34,8 +34,12 @@ public sealed class PathSpeedLimitBuilder
return EmPlanningStatus.InvalidInput;
}
LongitudinalStoppingProfile stopProfile = LongitudinalStoppingMath.Calculate(input.InitialProgressSpeedMetersPerSecond,
input.InitialAccelerationMetersPerSecondSquared, maximumDeceleration, maximumJerk);
if (!JerkLimitedStoppingMath.TryCalculate(input.InitialProgressSpeedMetersPerSecond,
input.InitialAccelerationMetersPerSecondSquared, maximumDeceleration, maximumJerk,
out JerkLimitedStoppingProfile stopProfile, out failureReason))
{
return EmPlanningStatus.InvalidInput;
}
if (stopProfile.DistanceMeters + StopDistanceToleranceMeters > input.TerminalPathS)
{
failureReason = "The available actual PathS distance is insufficient for the jerk-limited stop.";
@@ -206,62 +210,3 @@ public sealed class PathSpeedLimitBuilder
return !double.IsNaN(value) && !double.IsInfinity(value);
}
}
internal sealed class LongitudinalStoppingProfile
{
public LongitudinalStoppingProfile(double distanceMeters, double durationSeconds)
{
DistanceMeters = distanceMeters;
DurationSeconds = durationSeconds;
}
public double DistanceMeters { get; }
public double DurationSeconds { get; }
}
internal static class LongitudinalStoppingMath
{
public static LongitudinalStoppingProfile Calculate(double speedMetersPerSecond, double accelerationMetersPerSecondSquared,
double maximumDecelerationMetersPerSecondSquared, double maximumJerkMetersPerSecondCubed)
{
if (!IsFinite(speedMetersPerSecond) || !IsFinite(accelerationMetersPerSecondSquared) ||
!IsPositiveFinite(maximumDecelerationMetersPerSecondSquared) || !IsPositiveFinite(maximumJerkMetersPerSecondCubed))
{
throw new ArgumentOutOfRangeException(nameof(speedMetersPerSecond));
}
if (speedMetersPerSecond <= 0d)
return new LongitudinalStoppingProfile(0d, 0d);
double acceleration = Math.Max(-maximumDecelerationMetersPerSecondSquared, accelerationMetersPerSecondSquared);
double rampDuration = (acceleration + maximumDecelerationMetersPerSecondSquared) / maximumJerkMetersPerSecondCubed;
double speedAfterRamp = speedMetersPerSecond + acceleration * rampDuration -
0.5d * maximumJerkMetersPerSecondCubed * rampDuration * rampDuration;
if (speedAfterRamp <= 0d)
{
double root = (acceleration + Math.Sqrt(acceleration * acceleration + 2d * maximumJerkMetersPerSecondCubed *
speedMetersPerSecond)) / maximumJerkMetersPerSecondCubed;
double distance = speedMetersPerSecond * root + 0.5d * acceleration * root * root -
maximumJerkMetersPerSecondCubed * root * root * root / 6d;
return new LongitudinalStoppingProfile(Math.Max(0d, distance), root);
}
double rampDistance = speedMetersPerSecond * rampDuration + 0.5d * acceleration * rampDuration * rampDuration -
maximumJerkMetersPerSecondCubed * rampDuration * rampDuration * rampDuration / 6d;
double constantDecelerationDuration = speedAfterRamp / maximumDecelerationMetersPerSecondSquared;
double constantDecelerationDistance = speedAfterRamp * speedAfterRamp /
(2d * maximumDecelerationMetersPerSecondSquared);
return new LongitudinalStoppingProfile(rampDistance + constantDecelerationDistance,
rampDuration + constantDecelerationDuration);
}
private static bool IsPositiveFinite(double value)
{
return IsFinite(value) && value > 0d;
}
private static bool IsFinite(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
}
@@ -205,9 +205,13 @@ public sealed class SequentialLongitudinalOptimizer
double horizon = times[knotCount - 1];
double requestedSpeed = Math.Min(input.DirectionMaximumSpeedMetersPerSecond,
Math.Max(0d, input.TerminalPathS / horizon));
LongitudinalStoppingProfile terminalStop = LongitudinalStoppingMath.Calculate(requestedSpeed, 0d,
if (!JerkLimitedStoppingMath.TryCalculate(requestedSpeed, 0d,
input.Configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared,
input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed);
input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed,
out JerkLimitedStoppingProfile terminalStop, out _))
{
throw new ArgumentOutOfRangeException(nameof(input));
}
double cruiseDistance = Math.Max(0d, input.TerminalPathS - terminalStop.DistanceMeters);
for (int index = 0; index < knotCount; index++)
{
@@ -60,9 +60,13 @@ public sealed class PlanningHorizonSelector
}
double remainingSegment = Math.Max(0d, segment.LengthMeters - currentSegmentReferenceS);
LongitudinalStoppingProfile initialStop = LongitudinalStoppingMath.Calculate(initialProgressSpeedMetersPerSecond,
if (!JerkLimitedStoppingMath.TryCalculate(initialProgressSpeedMetersPerSecond,
initialAccelerationMetersPerSecondSquared, longitudinal.MaximumDecelerationMetersPerSecondSquared,
longitudinal.MaximumJerkMetersPerSecondCubed);
longitudinal.MaximumJerkMetersPerSecondCubed, out JerkLimitedStoppingProfile initialStop,
out failureReason))
{
return EmPlanningStatus.InvalidInput;
}
if (initialStop.DistanceMeters + BoundaryTolerance > remainingSegment)
{
failureReason = "The current segment lacks the jerk-limited stopping distance.";
@@ -88,8 +92,13 @@ public sealed class PlanningHorizonSelector
private static double CalculateReachableDistance(double initialSpeed, double initialAcceleration, double maximumSpeed,
LongitudinalConfiguration configuration, double timeHorizonSeconds)
{
LongitudinalStoppingProfile stopAtMaximumSpeed = LongitudinalStoppingMath.Calculate(maximumSpeed, 0d,
configuration.MaximumDecelerationMetersPerSecondSquared, configuration.MaximumJerkMetersPerSecondCubed);
if (!JerkLimitedStoppingMath.TryCalculate(maximumSpeed, 0d,
configuration.MaximumDecelerationMetersPerSecondSquared,
configuration.MaximumJerkMetersPerSecondCubed,
out JerkLimitedStoppingProfile stopAtMaximumSpeed, out _))
{
throw new ArgumentOutOfRangeException(nameof(configuration));
}
double drivingDuration = timeHorizonSeconds - configuration.ZeroSpeedHoldSeconds - stopAtMaximumSpeed.DurationSeconds;
if (drivingDuration <= 0d)
return Math.Min(initialSpeed, maximumSpeed) * Math.Max(0d, timeHorizonSeconds - configuration.ZeroSpeedHoldSeconds);
@@ -10,6 +10,8 @@ internal static class LongitudinalModelChecks
{
public static void Run()
{
VerifiesJerkLimitedStoppingProfileEndsAtRest();
VerifiesStoppedReachabilityUsesTheSameJerkModel();
VerifiesFinitePathSIndexedSpeedEnvelope();
VerifiesStoppingEnvelopeIsRefinedOnActualPathS();
VerifiesStoppingEnvelopeUsesDiscreteTimeTailStations();
@@ -18,6 +20,42 @@ internal static class LongitudinalModelChecks
VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints();
}
private static void VerifiesJerkLimitedStoppingProfileEndsAtRest()
{
Verification.True(JerkLimitedStoppingMath.TryCalculate(
0.20d, 0d, 0.30d, 0.50d,
out JerkLimitedStoppingProfile profile, out string failure),
"jerk-limited stop builds: " + failure);
Verification.True(profile.DistanceMeters > 0d, "stop distance is positive");
Verification.True(profile.DurationSeconds > 0d, "stop duration is positive");
Verification.NearlyEqual(0d, profile.FinalSpeedMetersPerSecond,
"stop ends at zero speed");
Verification.NearlyEqual(0d, profile.FinalAccelerationMetersPerSecondSquared,
"stop releases acceleration to zero");
Verification.True(JerkLimitedStoppingMath.TryCalculate(
0.20d, 0.20d, 0.30d, 0.50d,
out JerkLimitedStoppingProfile accelerating, out failure),
"positive-acceleration stop builds: " + failure);
Verification.True(accelerating.DistanceMeters > profile.DistanceMeters,
"positive initial acceleration needs more stopping distance");
Verification.NearlyEqual(0d, accelerating.FinalAccelerationMetersPerSecondSquared,
"positive-acceleration stop also releases acceleration");
}
private static void VerifiesStoppedReachabilityUsesTheSameJerkModel()
{
double maximumDistance = JerkLimitedStoppingMath.CalculateMaximumStoppedDistance(
0d, 0d, 0.20d, 0.20d, 0.30d, 0.50d, 2d);
Verification.True(maximumDistance > 0d && maximumDistance < 0.40d,
"two-second stopped reach is finite and below unconstrained cruise distance");
double cap = JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(
maximumDistance, 0.20d, 0.30d, 0.50d, 0.20d);
Verification.True(cap >= 0d && cap <= 0.20d,
"distance inversion stays inside the direction speed range");
}
private static void VerifiesFinitePathSIndexedSpeedEnvelope()
{
LateralPath directionPath = CreatePath(new[]