feat: coordinate rolling EM replans
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||
using MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||
using MultiWheelC.TrajectoryPlanning.Mapping;
|
||||
|
||||
namespace EMPlannerVerificationHost;
|
||||
|
||||
internal static class CoordinatorChecks
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
VerifiesCallerSuppliedSchedulingDecision();
|
||||
VerifiesCompletedCycleDoesNotPoisonNextCancellationSource();
|
||||
VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults();
|
||||
VerifiesSinkExceptionsAreIsolatedIntoCycleDiagnostics();
|
||||
}
|
||||
|
||||
private static void VerifiesCallerSuppliedSchedulingDecision()
|
||||
{
|
||||
var service = new ControlledPlanningService();
|
||||
var coordinator = new EmPlanningCoordinator(service);
|
||||
DateTimeOffset start = DateTimeOffset.UnixEpoch.AddSeconds(100d);
|
||||
PlanningCycleInput input = CreateInput(CreateMap(1), "schedule-reference", 7L, "prior", 0, "schedule", start);
|
||||
|
||||
Verification.True(coordinator.ShouldStartCycle(start), "first cycle is due");
|
||||
Task<PlanningCycleResult> cycle = coordinator.PlanLatestAsync(input, CancellationToken.None);
|
||||
service.WaitUntilStarted("schedule");
|
||||
service.Complete("schedule");
|
||||
Verification.Equal(EmPlanningStatus.Success, cycle.GetAwaiter().GetResult().Result.Status, "schedule setup result");
|
||||
|
||||
Verification.True(!coordinator.ShouldStartCycle(start.AddSeconds(0.199d)), "cycle remains deferred before period");
|
||||
Verification.True(coordinator.ShouldStartCycle(start.AddSeconds(0.20d)), "cycle becomes due at exact period");
|
||||
}
|
||||
|
||||
private static void VerifiesLatestCycleWinsAndEveryIdentityFieldSuppressesStaleResults()
|
||||
{
|
||||
PlanningGridMap firstMap = CreateMap(10);
|
||||
PlanningGridMap secondMap = CreateMap(11);
|
||||
DateTimeOffset now = DateTimeOffset.UnixEpoch.AddSeconds(200d);
|
||||
|
||||
VerifySuperseded("later-version", CreateInput(firstMap, "reference", 7L, "prior", 0, "later-version-A", now),
|
||||
CreateInput(firstMap, "reference", 7L, "prior", 0, "later-version-B", now.AddSeconds(0.20d)));
|
||||
VerifySuperseded("map", CreateInput(firstMap, "reference", 7L, "prior", 0, "map-A", now),
|
||||
CreateInput(secondMap, "reference", 7L, "prior", 0, "map-B", now.AddSeconds(0.20d)));
|
||||
VerifySuperseded("reference", CreateInput(firstMap, "reference-A", 7L, "prior", 0, "reference-A", now),
|
||||
CreateInput(firstMap, "reference-B", 7L, "prior", 0, "reference-B", now.AddSeconds(0.20d)));
|
||||
VerifySuperseded("state", CreateInput(firstMap, "reference", 7L, "prior", 0, "state-A", now),
|
||||
CreateInput(firstMap, "reference", 8L, "prior", 0, "state-B", now.AddSeconds(0.20d)));
|
||||
VerifySuperseded("segment", CreateInput(firstMap, "reference", 7L, "prior", 0, "segment-A", now),
|
||||
CreateInput(firstMap, "reference", 7L, "prior", 1, "segment-B", now.AddSeconds(0.20d)));
|
||||
VerifySuperseded("previous", CreateInput(firstMap, "reference", 7L, "prior-A", 0, "previous-A", now),
|
||||
CreateInput(firstMap, "reference", 7L, "prior-B", 0, "previous-B", now.AddSeconds(0.20d)));
|
||||
}
|
||||
|
||||
private static void VerifiesCompletedCycleDoesNotPoisonNextCancellationSource()
|
||||
{
|
||||
var service = new ControlledPlanningService();
|
||||
var coordinator = new EmPlanningCoordinator(service);
|
||||
DateTimeOffset now = DateTimeOffset.UnixEpoch.AddSeconds(150d);
|
||||
|
||||
PlanningCycleInput first = CreateInput(CreateMap(5), "completed-reference", 4L, "prior", 0,
|
||||
"completed-first", now);
|
||||
Task<PlanningCycleResult> firstCycle = coordinator.PlanLatestAsync(first, CancellationToken.None);
|
||||
service.WaitUntilStarted(first.Request.OutputTrajectoryId);
|
||||
service.Complete(first.Request.OutputTrajectoryId);
|
||||
Verification.Equal(EmPlanningStatus.Success, firstCycle.GetAwaiter().GetResult().Result.Status,
|
||||
"first completed cycle succeeds");
|
||||
|
||||
PlanningCycleInput second = CreateInput(CreateMap(6), "completed-reference", 5L, "completed-first", 0,
|
||||
"completed-second", now.AddSeconds(0.20d));
|
||||
Task<PlanningCycleResult> secondCycle = coordinator.PlanLatestAsync(second, CancellationToken.None);
|
||||
service.WaitUntilStarted(second.Request.OutputTrajectoryId);
|
||||
service.Complete(second.Request.OutputTrajectoryId);
|
||||
|
||||
PlanningCycleResult result = secondCycle.GetAwaiter().GetResult();
|
||||
Verification.Equal(EmPlanningStatus.Success, result.Result.Status, "next cycle after completion succeeds");
|
||||
Verification.True(result.Published, "next cycle after completion publishes");
|
||||
}
|
||||
|
||||
private static void VerifiesSinkExceptionsAreIsolatedIntoCycleDiagnostics()
|
||||
{
|
||||
var service = new ControlledPlanningService();
|
||||
var coordinator = new EmPlanningCoordinator(service, new ThrowingCycleSink());
|
||||
PlanningCycleInput input = CreateInput(CreateMap(20), "sink-reference", 9L, "prior", 0, "sink", DateTimeOffset.UnixEpoch);
|
||||
|
||||
Task<PlanningCycleResult> cycle = coordinator.PlanLatestAsync(input, CancellationToken.None);
|
||||
service.WaitUntilStarted("sink");
|
||||
service.Complete("sink");
|
||||
PlanningCycleResult result = cycle.GetAwaiter().GetResult();
|
||||
|
||||
Verification.Equal(EmPlanningStatus.Success, result.Result.Status, "sink failure preserves planning result");
|
||||
Verification.True(result.Diagnostic.IndexOf("cycle sink", StringComparison.OrdinalIgnoreCase) >= 0,
|
||||
"sink failure is reported in diagnostic");
|
||||
}
|
||||
|
||||
private static void VerifySuperseded(string name, PlanningCycleInput older, PlanningCycleInput newer)
|
||||
{
|
||||
var service = new ControlledPlanningService();
|
||||
var coordinator = new EmPlanningCoordinator(service);
|
||||
|
||||
Task<PlanningCycleResult> slow = coordinator.PlanLatestAsync(older, CancellationToken.None);
|
||||
service.WaitUntilStarted(older.Request.OutputTrajectoryId);
|
||||
Task<PlanningCycleResult> current = coordinator.PlanLatestAsync(newer, CancellationToken.None);
|
||||
service.WaitUntilStarted(newer.Request.OutputTrajectoryId);
|
||||
|
||||
Verification.True(service.WasCancelled(older.Request.OutputTrajectoryId), name + " cancels older cycle token");
|
||||
service.Complete(newer.Request.OutputTrajectoryId);
|
||||
PlanningCycleResult currentResult = current.GetAwaiter().GetResult();
|
||||
service.Complete(older.Request.OutputTrajectoryId);
|
||||
PlanningCycleResult staleResult = slow.GetAwaiter().GetResult();
|
||||
|
||||
Verification.Equal(EmPlanningStatus.Success, currentResult.Result.Status, name + " current result succeeds");
|
||||
Verification.True(currentResult.Published, name + " current result publishes");
|
||||
Verification.Equal(EmPlanningStatus.Superseded, staleResult.Result.Status, name + " old result is superseded");
|
||||
Verification.True(staleResult.Result.Trajectory == null, name + " stale result exposes no trajectory");
|
||||
Verification.Equal(newer.Request.OutputTrajectoryId, coordinator.PublishedTrajectory.Metadata.TrajectoryId,
|
||||
name + " only latest trajectory is published");
|
||||
}
|
||||
|
||||
private static PlanningCycleInput CreateInput(PlanningGridMap map, string referencePathId, long stateSequenceId,
|
||||
string previousTrajectoryId, int segmentIndex, string outputTrajectoryId, DateTimeOffset now)
|
||||
{
|
||||
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
||||
var 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);
|
||||
}
|
||||
|
||||
private static PlanningGridMap CreateMap(int widthOffset)
|
||||
{
|
||||
PlanningMapBuildResult build = new PlanningMapFactory().Create(new PlanningMapRequest
|
||||
{
|
||||
Bounds = new MapBoundsMm(-1000f, 1000f + widthOffset * 20f, -1000f, 1000f),
|
||||
ResolutionMm = 20f,
|
||||
ObstacleSources = Array.Empty<IMapObstacleSource>(),
|
||||
AllowExplicitEmptyMap = true,
|
||||
});
|
||||
Verification.True(build.Succeeded && build.Map != null && build.Map.PlanningReady, "coordinator map builds");
|
||||
return build.Map!;
|
||||
}
|
||||
|
||||
private sealed class ControlledPlanningService : IEmPlanningService
|
||||
{
|
||||
private readonly object gate = new object();
|
||||
private readonly Dictionary<string, PendingCycle> pending = new Dictionary<string, PendingCycle>();
|
||||
|
||||
public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var cycle = new PendingCycle(request, cancellationToken);
|
||||
lock (gate)
|
||||
{
|
||||
pending.Add(request.OutputTrajectoryId, cycle);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
return cycle.Completion.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void WaitUntilStarted(string outputTrajectoryId)
|
||||
{
|
||||
DateTimeOffset timeout = DateTimeOffset.UtcNow.AddSeconds(5d);
|
||||
lock (gate)
|
||||
{
|
||||
while (!pending.ContainsKey(outputTrajectoryId))
|
||||
{
|
||||
TimeSpan remaining = timeout - DateTimeOffset.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero || !Monitor.Wait(gate, remaining))
|
||||
throw new InvalidOperationException("Planner cycle did not start: " + outputTrajectoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool WasCancelled(string outputTrajectoryId)
|
||||
{
|
||||
lock (gate)
|
||||
return pending[outputTrajectoryId].CancellationToken.IsCancellationRequested;
|
||||
}
|
||||
|
||||
public void Complete(string outputTrajectoryId)
|
||||
{
|
||||
PendingCycle cycle;
|
||||
lock (gate)
|
||||
cycle = pending[outputTrajectoryId];
|
||||
cycle.Completion.TrySetResult(CreateSuccess(cycle.Request));
|
||||
}
|
||||
|
||||
private static EmPlanningResult CreateSuccess(EmPlanningRequest request)
|
||||
{
|
||||
var metadata = new EmTrajectoryMetadata(request.OutputTrajectoryId, request.RequestedAtUtc, request.EffectiveAtUtc,
|
||||
request.Map.SnapshotId, request.ReferencePathId, request.VehicleState.SequenceId, request.PreviousTrajectoryId,
|
||||
request.SegmentIndex, TravelDirection.Forward, EmTerminalType.RollingSafetyStop);
|
||||
var point = new EmTrajectoryPoint(0d, 0d, 0d, 0d, 0d, 0d, request.SegmentIndex, 0d, 0d,
|
||||
TravelDirection.Forward, EmBoundaryType.RollingSafetyStop, 0d, 0d);
|
||||
return new EmPlanningResult(EmPlanningStatus.Success, new EmTrajectory(metadata, new[] { point }), string.Empty);
|
||||
}
|
||||
|
||||
private sealed class PendingCycle
|
||||
{
|
||||
public PendingCycle(EmPlanningRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Request = request;
|
||||
CancellationToken = cancellationToken;
|
||||
Completion = new TaskCompletionSource<EmPlanningResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public EmPlanningRequest Request { get; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
public TaskCompletionSource<EmPlanningResult> Completion { get; }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThrowingCycleSink : IEmPlanningCycleSink
|
||||
{
|
||||
public void OnCycleCompleted(PlanningCycleResult result)
|
||||
{
|
||||
throw new InvalidOperationException("cycle sink failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ internal static class Program
|
||||
args[0] != "lateral-real-osqp" && args[0] != "lateral-real-osqp-probe" && args[0] != "lateral-all" &&
|
||||
args[0] != "longitudinal-model" && args[0] != "longitudinal-integration" &&
|
||||
args[0] != "longitudinal-real-osqp-probe" && args[0] != "trajectory" &&
|
||||
args[0] != "em-planning-service" && args[0] != "em-core-all"))
|
||||
args[0] != "em-planning-service" && args[0] != "em-core-all" && args[0] != "coordinator"))
|
||||
{
|
||||
Console.Error.WriteLine("Usage: EMPlannerVerificationHost foundation|segmentation|frenet|corridor|optimization|osqp|osqp-loader|all-foundation|lateral-model|lateral-integration|lateral-real-osqp|lateral-all|longitudinal-model|longitudinal-integration");
|
||||
return 2;
|
||||
@@ -108,6 +108,11 @@ internal static class Program
|
||||
MultiWheelC.TrajectoryPlanning.EMPlanner.EmPlanningServiceChecks.Run();
|
||||
Console.WriteLine("PASS em-planning-service");
|
||||
}
|
||||
if (args[0] == "coordinator")
|
||||
{
|
||||
CoordinatorChecks.Run();
|
||||
Console.WriteLine("PASS coordinator");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
||||
Reference in New Issue
Block a user