feat: select safe EM trajectory handoffs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||
|
||||
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||
|
||||
@@ -12,6 +13,7 @@ public sealed class EmPlanningCoordinator
|
||||
{
|
||||
private readonly IEmPlanningService planningService;
|
||||
private readonly IEmPlanningCycleSink sink;
|
||||
private readonly TrajectoryHandoffSelector handoffSelector = new TrajectoryHandoffSelector();
|
||||
private readonly object publicationGate = new object();
|
||||
private long latestCycleVersion;
|
||||
private PlanningCycleIdentity latestIdentity;
|
||||
@@ -66,6 +68,19 @@ public sealed class EmPlanningCoordinator
|
||||
return Task.Run(() => CompleteCycle(version, input, cycleCancellation));
|
||||
}
|
||||
|
||||
/// <summary>Builds a handoff decision from the coordinator's immutable published trajectory snapshot.</summary>
|
||||
public TrajectoryHandoffSelection SelectHandoff(PlanningCycleInput input, TravelDirection expectedDirection)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
EmTrajectory trajectory;
|
||||
lock (publicationGate)
|
||||
trajectory = publishedTrajectory;
|
||||
return handoffSelector.Select(trajectory, input.Request.VehicleState, input.Identity.SegmentIndex, expectedDirection,
|
||||
input.Now, input.Request.Configuration);
|
||||
}
|
||||
|
||||
private PlanningCycleResult CompleteCycle(long version, PlanningCycleInput input,
|
||||
CancellationTokenSource cycleCancellation)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||
|
||||
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||
|
||||
public enum TrajectoryHandoffSource
|
||||
{
|
||||
MeasuredState,
|
||||
PreviousTrajectory,
|
||||
}
|
||||
|
||||
public enum TrajectoryHandoffRejectionReason
|
||||
{
|
||||
None,
|
||||
MissingTrajectory,
|
||||
InvalidConfiguration,
|
||||
TrajectoryNotYetEffective,
|
||||
TrajectoryTooOld,
|
||||
SegmentMismatch,
|
||||
DirectionMismatch,
|
||||
TrackingErrorExceeded,
|
||||
GearBoundary,
|
||||
TerminalBoundary,
|
||||
HandoffBeyondTrajectory,
|
||||
SampleUnavailable,
|
||||
}
|
||||
|
||||
/// <summary>Immutable handoff decision for the next one-shot planning request.</summary>
|
||||
public sealed class TrajectoryHandoffSelection
|
||||
{
|
||||
internal TrajectoryHandoffSelection(TrajectoryHandoffSource source, VehicleMotionState startState,
|
||||
EmTrajectory previousTrajectory, EmTrajectoryPoint sampledPoint, TrajectoryHandoffRejectionReason rejectionReason)
|
||||
{
|
||||
Source = source;
|
||||
StartState = startState ?? throw new ArgumentNullException(nameof(startState));
|
||||
PreviousTrajectory = previousTrajectory;
|
||||
SampledPoint = sampledPoint;
|
||||
RejectionReason = rejectionReason;
|
||||
}
|
||||
|
||||
public TrajectoryHandoffSource Source { get; }
|
||||
|
||||
public VehicleMotionState StartState { get; }
|
||||
|
||||
public EmTrajectory PreviousTrajectory { get; }
|
||||
|
||||
public EmTrajectoryPoint SampledPoint { get; }
|
||||
|
||||
public TrajectoryHandoffRejectionReason RejectionReason { get; }
|
||||
}
|
||||
|
||||
/// <summary>Selects a future same-segment seed or preserves the caller's measured state.</summary>
|
||||
public sealed class TrajectoryHandoffSelector
|
||||
{
|
||||
private const double TimeEpsilonSeconds = 1e-9d;
|
||||
private readonly TrajectorySampler sampler;
|
||||
|
||||
public TrajectoryHandoffSelector()
|
||||
: this(new TrajectorySampler())
|
||||
{
|
||||
}
|
||||
|
||||
internal TrajectoryHandoffSelector(TrajectorySampler sampler)
|
||||
{
|
||||
this.sampler = sampler ?? throw new ArgumentNullException(nameof(sampler));
|
||||
}
|
||||
|
||||
public TrajectoryHandoffSelection Select(EmTrajectory previousTrajectory, VehicleMotionState measuredState,
|
||||
int expectedSegmentIndex, TravelDirection expectedDirection, DateTimeOffset now,
|
||||
EmPlannerConfiguration configuration)
|
||||
{
|
||||
if (measuredState == null)
|
||||
throw new ArgumentNullException(nameof(measuredState));
|
||||
if (expectedSegmentIndex < 0 || !Enum.IsDefined(typeof(TravelDirection), expectedDirection))
|
||||
throw new ArgumentOutOfRangeException(nameof(expectedSegmentIndex));
|
||||
if (previousTrajectory == null)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.MissingTrajectory);
|
||||
if (!TryReadConfiguration(configuration, out double maximumAgeSeconds, out double lookaheadSeconds,
|
||||
out double spatialToleranceMeters, out double kinematicTolerance))
|
||||
{
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.InvalidConfiguration);
|
||||
}
|
||||
if (previousTrajectory.Metadata.SegmentIndex != expectedSegmentIndex)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.SegmentMismatch);
|
||||
if (previousTrajectory.Metadata.Direction != expectedDirection)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.DirectionMismatch);
|
||||
|
||||
double trajectoryAgeSeconds = (now - previousTrajectory.Metadata.EffectiveAtUtc).TotalSeconds;
|
||||
if (trajectoryAgeSeconds < -TimeEpsilonSeconds)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.TrajectoryNotYetEffective);
|
||||
if (trajectoryAgeSeconds > maximumAgeSeconds + TimeEpsilonSeconds)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.TrajectoryTooOld);
|
||||
if (!sampler.TrySample(previousTrajectory, trajectoryAgeSeconds, out EmTrajectoryPoint currentPoint))
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.SampleUnavailable);
|
||||
if (!Tracks(currentPoint, measuredState, spatialToleranceMeters, kinematicTolerance))
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.TrackingErrorExceeded);
|
||||
|
||||
double handoffTime = trajectoryAgeSeconds + lookaheadSeconds;
|
||||
double terminalTime = previousTrajectory.Points[previousTrajectory.Points.Count - 1].TimeFromStart;
|
||||
if (handoffTime > terminalTime + TimeEpsilonSeconds)
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.HandoffBeyondTrajectory);
|
||||
TrajectoryHandoffRejectionReason boundaryReason = BoundaryInInterval(previousTrajectory, trajectoryAgeSeconds,
|
||||
handoffTime);
|
||||
if (boundaryReason != TrajectoryHandoffRejectionReason.None)
|
||||
return Measured(measuredState, boundaryReason);
|
||||
if (!sampler.TrySample(previousTrajectory, handoffTime, out EmTrajectoryPoint handoffPoint))
|
||||
return Measured(measuredState, TrajectoryHandoffRejectionReason.SampleUnavailable);
|
||||
|
||||
var startState = new VehicleMotionState(new Pose2D(handoffPoint.X, handoffPoint.Y, handoffPoint.Yaw),
|
||||
handoffPoint.SignedLongitudinalVelocity, handoffPoint.LongitudinalAcceleration,
|
||||
previousTrajectory.Metadata.EffectiveAtUtc.AddSeconds(handoffTime), measuredState.SequenceId);
|
||||
return new TrajectoryHandoffSelection(TrajectoryHandoffSource.PreviousTrajectory, startState,
|
||||
previousTrajectory, handoffPoint, TrajectoryHandoffRejectionReason.None);
|
||||
}
|
||||
|
||||
private static TrajectoryHandoffSelection Measured(VehicleMotionState measuredState,
|
||||
TrajectoryHandoffRejectionReason rejectionReason)
|
||||
{
|
||||
return new TrajectoryHandoffSelection(TrajectoryHandoffSource.MeasuredState, measuredState, null, null,
|
||||
rejectionReason);
|
||||
}
|
||||
|
||||
private static bool TryReadConfiguration(EmPlannerConfiguration configuration, out double maximumAgeSeconds,
|
||||
out double lookaheadSeconds, out double spatialToleranceMeters, out double kinematicTolerance)
|
||||
{
|
||||
maximumAgeSeconds = configuration?.Scheduling?.MaximumVehicleStateAgeSeconds ?? double.NaN;
|
||||
lookaheadSeconds = configuration?.Scheduling?.HandoffLookaheadSeconds ?? double.NaN;
|
||||
spatialToleranceMeters = configuration?.Validation?.SpatialToleranceMeters ?? double.NaN;
|
||||
kinematicTolerance = configuration?.Validation?.KinematicTolerance ?? double.NaN;
|
||||
return IsNonNegativeFinite(maximumAgeSeconds) && IsNonNegativeFinite(lookaheadSeconds) &&
|
||||
IsNonNegativeFinite(spatialToleranceMeters) && IsNonNegativeFinite(kinematicTolerance);
|
||||
}
|
||||
|
||||
private static bool Tracks(EmTrajectoryPoint trajectoryPoint, VehicleMotionState measuredState,
|
||||
double spatialToleranceMeters, double kinematicTolerance)
|
||||
{
|
||||
double dx = trajectoryPoint.X - measuredState.Pose.X;
|
||||
double dy = trajectoryPoint.Y - measuredState.Pose.Y;
|
||||
if (Math.Sqrt(dx * dx + dy * dy) > spatialToleranceMeters)
|
||||
return false;
|
||||
double yawError = Math.Atan2(Math.Sin(trajectoryPoint.Yaw - measuredState.Pose.Heading),
|
||||
Math.Cos(trajectoryPoint.Yaw - measuredState.Pose.Heading));
|
||||
return Math.Abs(yawError) <= kinematicTolerance &&
|
||||
Math.Abs(trajectoryPoint.SignedLongitudinalVelocity -
|
||||
measuredState.SignedLongitudinalSpeedMetersPerSecond) <= kinematicTolerance;
|
||||
}
|
||||
|
||||
private static TrajectoryHandoffRejectionReason BoundaryInInterval(EmTrajectory trajectory, double startTime,
|
||||
double endTime)
|
||||
{
|
||||
for (int index = 0; index < trajectory.Points.Count; index++)
|
||||
{
|
||||
EmTrajectoryPoint point = trajectory.Points[index];
|
||||
if (point.TimeFromStart <= startTime + TimeEpsilonSeconds || point.TimeFromStart > endTime + TimeEpsilonSeconds)
|
||||
continue;
|
||||
if (point.BoundaryType == EmBoundaryType.GearSwitchApproach ||
|
||||
point.BoundaryType == EmBoundaryType.GearSwitchDeparture)
|
||||
{
|
||||
return TrajectoryHandoffRejectionReason.GearBoundary;
|
||||
}
|
||||
if (point.BoundaryType == EmBoundaryType.RollingSafetyStop || point.BoundaryType == EmBoundaryType.Goal)
|
||||
return TrajectoryHandoffRejectionReason.TerminalBoundary;
|
||||
}
|
||||
return TrajectoryHandoffRejectionReason.None;
|
||||
}
|
||||
|
||||
private static bool IsNonNegativeFinite(double value)
|
||||
{
|
||||
return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
|
||||
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||
|
||||
/// <summary>Interpolates an immutable trajectory only inside one homogeneous trajectory interval.</summary>
|
||||
public sealed class TrajectorySampler
|
||||
{
|
||||
private const double TimeEpsilonSeconds = 1e-9d;
|
||||
|
||||
public bool TrySample(EmTrajectory trajectory, double timeFromStart, out EmTrajectoryPoint sampledPoint)
|
||||
{
|
||||
sampledPoint = null;
|
||||
if (trajectory == null || double.IsNaN(timeFromStart) || double.IsInfinity(timeFromStart) ||
|
||||
trajectory.Points.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EmTrajectoryPoint first = trajectory.Points[0];
|
||||
EmTrajectoryPoint last = trajectory.Points[trajectory.Points.Count - 1];
|
||||
if (timeFromStart < first.TimeFromStart - TimeEpsilonSeconds ||
|
||||
timeFromStart > last.TimeFromStart + TimeEpsilonSeconds)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int lower = 0;
|
||||
int upper = trajectory.Points.Count - 1;
|
||||
while (upper - lower > 1)
|
||||
{
|
||||
int middle = lower + (upper - lower) / 2;
|
||||
EmTrajectoryPoint point = trajectory.Points[middle];
|
||||
if (Math.Abs(point.TimeFromStart - timeFromStart) <= TimeEpsilonSeconds)
|
||||
{
|
||||
sampledPoint = point;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (point.TimeFromStart < timeFromStart)
|
||||
lower = middle;
|
||||
else
|
||||
upper = middle;
|
||||
}
|
||||
|
||||
EmTrajectoryPoint left = trajectory.Points[lower];
|
||||
EmTrajectoryPoint right = trajectory.Points[upper];
|
||||
if (Math.Abs(left.TimeFromStart - timeFromStart) <= TimeEpsilonSeconds)
|
||||
{
|
||||
sampledPoint = left;
|
||||
return true;
|
||||
}
|
||||
if (Math.Abs(right.TimeFromStart - timeFromStart) <= TimeEpsilonSeconds)
|
||||
{
|
||||
sampledPoint = right;
|
||||
return true;
|
||||
}
|
||||
if (left.SegmentIndex != right.SegmentIndex || left.Direction != right.Direction ||
|
||||
left.BoundaryType != right.BoundaryType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double fraction = (timeFromStart - left.TimeFromStart) / (right.TimeFromStart - left.TimeFromStart);
|
||||
sampledPoint = new EmTrajectoryPoint(
|
||||
Interpolate(left.X, right.X, fraction),
|
||||
Interpolate(left.Y, right.Y, fraction),
|
||||
Interpolate(left.Yaw, right.Yaw, fraction),
|
||||
Interpolate(left.SignedLongitudinalVelocity, right.SignedLongitudinalVelocity, fraction),
|
||||
timeFromStart,
|
||||
Interpolate(left.VehicleCurvature, right.VehicleCurvature, fraction),
|
||||
left.SegmentIndex,
|
||||
Interpolate(left.SegmentLocalS, right.SegmentLocalS, fraction),
|
||||
Interpolate(left.PathS, right.PathS, fraction),
|
||||
left.Direction,
|
||||
left.BoundaryType,
|
||||
Interpolate(left.LongitudinalAcceleration, right.LongitudinalAcceleration, fraction),
|
||||
Interpolate(left.LongitudinalJerk, right.LongitudinalJerk, fraction));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static double Interpolate(double left, double right, double fraction)
|
||||
{
|
||||
return left + (right - left) * fraction;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user