feat: select safe EM trajectory handoffs
This commit is contained in:
@@ -16,6 +16,7 @@ internal static class CoordinatorChecks
|
||||
VerifiesCompletedCycleDoesNotPoisonNextCancellationSource();
|
||||
VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults();
|
||||
VerifiesSinkExceptionsAreIsolatedIntoCycleDiagnostics();
|
||||
VerifiesSafePreviousTrajectoryHandoffs();
|
||||
}
|
||||
|
||||
private static void VerifiesCallerSuppliedSchedulingDecision()
|
||||
@@ -96,6 +97,134 @@ internal static class CoordinatorChecks
|
||||
"sink failure is reported in diagnostic");
|
||||
}
|
||||
|
||||
private static void VerifiesSafePreviousTrajectoryHandoffs()
|
||||
{
|
||||
DateTimeOffset effectiveAt = DateTimeOffset.UnixEpoch.AddSeconds(300d);
|
||||
EmPlannerConfiguration configuration = CreateHandoffConfiguration();
|
||||
var selector = new TrajectoryHandoffSelector();
|
||||
EmTrajectory forward = CreateHandoffTrajectory("handoff-forward", TravelDirection.Forward, 2, effectiveAt,
|
||||
EmBoundaryType.RollingSafetyStop, false);
|
||||
VehicleMotionState forwardMeasured = CreateMeasuredState(0.01d, 0d, 3.1666666666666665d, 0.10d,
|
||||
effectiveAt.AddSeconds(0.10d), 40L);
|
||||
|
||||
TrajectoryHandoffSelection accepted = selector.Select(forward, forwardMeasured, 2, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.10d), configuration);
|
||||
Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, accepted.Source, "forward handoff source");
|
||||
Verification.Equal(TrajectoryHandoffRejectionReason.None, accepted.RejectionReason, "forward handoff reason");
|
||||
Verification.True(object.ReferenceEquals(forward, accepted.PreviousTrajectory), "forward handoff seed");
|
||||
Verification.NearlyEqual(0.04d, accepted.StartState.Pose.X, "forward handoff position interpolation");
|
||||
Verification.NearlyEqual(3.3666666666666667d, accepted.StartState.Pose.Heading,
|
||||
"forward handoff yaw remains unwrapped");
|
||||
Verification.NearlyEqual(0.10d, accepted.StartState.SignedLongitudinalSpeedMetersPerSecond,
|
||||
"forward handoff signed speed interpolation");
|
||||
Verification.Equal(40L, accepted.StartState.SequenceId, "handoff retains measured-state identity");
|
||||
|
||||
EmTrajectory reverse = CreateHandoffTrajectory("handoff-reverse", TravelDirection.Reverse, 2, effectiveAt,
|
||||
EmBoundaryType.RollingSafetyStop, false);
|
||||
VehicleMotionState reverseMeasured = CreateMeasuredState(-0.01d, 0d, 3.1666666666666665d, -0.10d,
|
||||
effectiveAt.AddSeconds(0.10d), 41L);
|
||||
TrajectoryHandoffSelection reverseAccepted = selector.Select(reverse, reverseMeasured, 2, TravelDirection.Reverse,
|
||||
effectiveAt.AddSeconds(0.10d), configuration);
|
||||
Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, reverseAccepted.Source,
|
||||
"reverse same-segment handoff source");
|
||||
Verification.NearlyEqual(-0.04d, reverseAccepted.StartState.Pose.X, "reverse handoff position interpolation");
|
||||
Verification.NearlyEqual(-0.10d, reverseAccepted.StartState.SignedLongitudinalSpeedMetersPerSecond,
|
||||
"reverse handoff signed speed interpolation");
|
||||
|
||||
AssertHandoffRejected(selector, forward, forwardMeasured, 2, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.21d), configuration, TrajectoryHandoffRejectionReason.TrajectoryTooOld,
|
||||
"stale trajectory handoff");
|
||||
configuration.Scheduling.MaximumVehicleStateAgeSeconds = 0.60d;
|
||||
AssertHandoffRejected(selector, forward,
|
||||
CreateMeasuredState(1d, 0d, 3.1666666666666665d, 0.10d, effectiveAt.AddSeconds(0.10d), 42L), 2,
|
||||
TravelDirection.Forward, effectiveAt.AddSeconds(0.10d), configuration,
|
||||
TrajectoryHandoffRejectionReason.TrackingErrorExceeded, "large tracking-error handoff");
|
||||
VehicleMotionState terminalMeasured = CreateMeasuredState(0.03d, 0d, 3.30d, 0.10d,
|
||||
effectiveAt.AddSeconds(0.30d), 43L);
|
||||
AssertHandoffRejected(selector, forward, terminalMeasured, 2, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.30d), configuration, TrajectoryHandoffRejectionReason.TerminalBoundary,
|
||||
"terminal-proximity handoff");
|
||||
AssertHandoffRejected(selector, forward, forwardMeasured, 3, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.SegmentMismatch,
|
||||
"segment-mismatch handoff");
|
||||
AssertHandoffRejected(selector, forward, forwardMeasured, 2, TravelDirection.Reverse,
|
||||
effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.DirectionMismatch,
|
||||
"direction-mismatch handoff");
|
||||
VehicleMotionState beyondMeasured = CreateMeasuredState(0.031d, 0d, 3.3066666666666666d, 0.10d,
|
||||
effectiveAt.AddSeconds(0.31d), 44L);
|
||||
AssertHandoffRejected(selector, forward, beyondMeasured, 2, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.31d), configuration, TrajectoryHandoffRejectionReason.HandoffBeyondTrajectory,
|
||||
"beyond-trajectory handoff");
|
||||
|
||||
EmTrajectory gearBoundary = CreateHandoffTrajectory("handoff-gear", TravelDirection.Forward, 2, effectiveAt,
|
||||
EmBoundaryType.RollingSafetyStop, true);
|
||||
AssertHandoffRejected(selector, gearBoundary, forwardMeasured, 2, TravelDirection.Forward,
|
||||
effectiveAt.AddSeconds(0.10d), configuration, TrajectoryHandoffRejectionReason.GearBoundary,
|
||||
"gear-boundary handoff");
|
||||
Verification.True(!new TrajectorySampler().TrySample(gearBoundary, 0.15d, out _),
|
||||
"sampler never interpolates across different boundary types");
|
||||
|
||||
var service = new ControlledPlanningService();
|
||||
var coordinator = new EmPlanningCoordinator(service);
|
||||
PlanningCycleInput coordinatorInput = CreateInput(CreateMap(22), "handoff-reference", 40L, "handoff-forward",
|
||||
2, "handoff-publish", effectiveAt.AddSeconds(0.10d), configuration, forwardMeasured);
|
||||
Task<PlanningCycleResult> publication = coordinator.PlanLatestAsync(coordinatorInput, CancellationToken.None);
|
||||
service.WaitUntilStarted(coordinatorInput.Request.OutputTrajectoryId);
|
||||
service.Complete(coordinatorInput.Request.OutputTrajectoryId, forward);
|
||||
publication.GetAwaiter().GetResult();
|
||||
TrajectoryHandoffSelection coordinatorSelection = coordinator.SelectHandoff(coordinatorInput, TravelDirection.Forward);
|
||||
Verification.Equal(TrajectoryHandoffSource.PreviousTrajectory, coordinatorSelection.Source,
|
||||
"coordinator consumes only its published immutable trajectory");
|
||||
}
|
||||
|
||||
private static void AssertHandoffRejected(TrajectoryHandoffSelector selector, EmTrajectory trajectory,
|
||||
VehicleMotionState measuredState, int segmentIndex, TravelDirection direction, DateTimeOffset now,
|
||||
EmPlannerConfiguration configuration, TrajectoryHandoffRejectionReason reason, string name)
|
||||
{
|
||||
TrajectoryHandoffSelection selection = selector.Select(trajectory, measuredState, segmentIndex, direction, now,
|
||||
configuration);
|
||||
Verification.Equal(TrajectoryHandoffSource.MeasuredState, selection.Source, name + " source");
|
||||
Verification.Equal(reason, selection.RejectionReason, name + " reason");
|
||||
Verification.True(selection.PreviousTrajectory == null, name + " has no seed");
|
||||
Verification.True(object.ReferenceEquals(measuredState, selection.StartState), name + " returns measured state");
|
||||
}
|
||||
|
||||
private static EmPlannerConfiguration CreateHandoffConfiguration()
|
||||
{
|
||||
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
||||
configuration.Validation.SpatialToleranceMeters = 0.05d;
|
||||
configuration.Validation.KinematicTolerance = 0.05d;
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private static VehicleMotionState CreateMeasuredState(double x, double y, double yaw, double signedSpeed,
|
||||
DateTimeOffset capturedAt, long sequenceId)
|
||||
{
|
||||
return new VehicleMotionState(new Pose2D(x, y, yaw), signedSpeed, 0d, capturedAt, sequenceId);
|
||||
}
|
||||
|
||||
private static EmTrajectory CreateHandoffTrajectory(string trajectoryId, TravelDirection direction, int segmentIndex,
|
||||
DateTimeOffset effectiveAt, EmBoundaryType terminalBoundary, bool includeGearBoundary)
|
||||
{
|
||||
double sign = direction == TravelDirection.Forward ? 1d : -1d;
|
||||
EmBoundaryType middleBoundary = includeGearBoundary ? EmBoundaryType.GearSwitchApproach : EmBoundaryType.None;
|
||||
var metadata = new EmTrajectoryMetadata(trajectoryId, effectiveAt, effectiveAt, 55L, "handoff-reference", 39L,
|
||||
string.Empty, segmentIndex, direction, EmTerminalType.RollingSafetyStop);
|
||||
return new EmTrajectory(metadata, new[]
|
||||
{
|
||||
new EmTrajectoryPoint(0d, 0d, 3.10d, sign * 0.10d, 0d, 0.25d, segmentIndex, 0d, 0d,
|
||||
direction, EmBoundaryType.None, 0d, 0d),
|
||||
new EmTrajectoryPoint(sign * 0.01d, 0d, 3.1666666666666665d, sign * 0.10d, 0.10d, 0.25d,
|
||||
segmentIndex, 0.01d, 0.01d, direction, EmBoundaryType.None, 0d, 0d),
|
||||
new EmTrajectoryPoint(sign * 0.03d, 0d, 3.30d, sign * 0.10d, 0.30d, 0.25d, segmentIndex, 0.03d,
|
||||
0.03d, direction, middleBoundary, 0d, 0d),
|
||||
new EmTrajectoryPoint(sign * 0.055d, 0d, 3.4666666666666668d, sign * 0.10d, 0.55d, 0.25d,
|
||||
segmentIndex, 0.055d, 0.055d, direction, EmBoundaryType.None, 0d, 0d),
|
||||
new EmTrajectoryPoint(sign * 0.06d, 0d, 3.50d, 0d, 0.60d, 0.25d, segmentIndex, 0.06d, 0.06d,
|
||||
direction, terminalBoundary, 0d, 0d),
|
||||
});
|
||||
}
|
||||
|
||||
private static void VerifySuperseded(string name, PlanningCycleInput older, PlanningCycleInput newer)
|
||||
{
|
||||
var service = new ControlledPlanningService();
|
||||
@@ -121,10 +250,11 @@ internal static class CoordinatorChecks
|
||||
}
|
||||
|
||||
private static PlanningCycleInput CreateInput(PlanningGridMap map, string referencePathId, long stateSequenceId,
|
||||
string previousTrajectoryId, int segmentIndex, string outputTrajectoryId, DateTimeOffset now)
|
||||
string previousTrajectoryId, int segmentIndex, string outputTrajectoryId, DateTimeOffset now,
|
||||
EmPlannerConfiguration? configuration = null, VehicleMotionState? state = null)
|
||||
{
|
||||
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
||||
var state = new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, 0d, now, stateSequenceId);
|
||||
configuration ??= EmPlannerConfiguration.CreateDefault();
|
||||
state ??= new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, 0d, now, stateSequenceId);
|
||||
var request = new EmPlanningRequest(null, map, null, state, configuration, segmentIndex, null, now, now,
|
||||
outputTrajectoryId, referencePathId, previousTrajectoryId, EmMotionModel.NonholonomicForwardReverse);
|
||||
return new PlanningCycleInput(request, now);
|
||||
@@ -179,12 +309,15 @@ internal static class CoordinatorChecks
|
||||
return pending[outputTrajectoryId].CancellationToken.IsCancellationRequested;
|
||||
}
|
||||
|
||||
public void Complete(string outputTrajectoryId)
|
||||
public void Complete(string outputTrajectoryId, EmTrajectory? trajectory = null)
|
||||
{
|
||||
PendingCycle cycle;
|
||||
lock (gate)
|
||||
cycle = pending[outputTrajectoryId];
|
||||
cycle.Completion.TrySetResult(CreateSuccess(cycle.Request));
|
||||
EmPlanningResult result = trajectory == null
|
||||
? CreateSuccess(cycle.Request)
|
||||
: new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty);
|
||||
cycle.Completion.TrySetResult(result);
|
||||
}
|
||||
|
||||
private static EmPlanningResult CreateSuccess(EmPlanningRequest request)
|
||||
|
||||
Reference in New Issue
Block a user