using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using EMPlannerVerificationHost; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; internal static class EmPlanningServiceChecks { public static void Run() { VerifiesPreviousTrajectoryIsALongitudinalSoftReference(); VerifiesForwardReverseAndBoundarySuccessesAreDeterministic(); VerifiesServicePublishesRollingApproachAndExactStopModes(); VerifiesFullScopePublishesItsRequestScope(); VerifiesRequestAndStateFailuresPublishNoTrajectory(); VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory(); VerifiesNoProgressPublishesNoTrajectory(); VerifiesTimeoutFallbackAndCancellationSemantics(); VerifiesPublicationFailureAndDebugIsolation(); VerifiesPublicationGateStatusMappings(); } private static void VerifiesForwardReverseAndBoundarySuccessesAreDeterministic() { EmPlanningRequest forwardRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); var forwardService = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)); EmPlanningResult firstForward = forwardService.Plan(forwardRequest, CancellationToken.None); EmPlanningResult secondForward = forwardService.Plan(forwardRequest, CancellationToken.None); VerifySuccess(firstForward, forwardRequest, EmTerminalType.Goal, "forward"); VerifySuccess(secondForward, forwardRequest, EmTerminalType.Goal, "forward repeat"); VerifySameTrajectory(firstForward, secondForward, "forward deterministic result"); Verification.Equal(2, forwardRequest.ReferencePath.Path.Count, "request-owned reference list remains unchanged"); EmPlanningRequest reverseRequest = CreateRequest(TravelDirection.Reverse, -0.01d, false, false); EmPlanningResult reverse = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(reverseRequest, CancellationToken.None); VerifySuccess(reverse, reverseRequest, EmTerminalType.Goal, "reverse"); Verification.True(reverse.Trajectory.Points[1].SignedLongitudinalVelocity < 0d, "reverse service publishes negative signed velocity"); EmPlanningRequest gearRequest = CreateRequest(TravelDirection.Forward, 0d, true, false); EmPlanningResult gear = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(gearRequest, CancellationToken.None); VerifySuccess(gear, gearRequest, EmTerminalType.GearSwitch, "gear switch"); EmPlanningRequest rollingRequest = CreateRequest(TravelDirection.Forward, 0d, false, true); EmPlanningResult rolling = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(rollingRequest, CancellationToken.None); VerifySuccess(rolling, rollingRequest, EmTerminalType.RollingSafetyStop, "rolling stop"); } private static void VerifiesServicePublishesRollingApproachAndExactStopModes() { EmPlanningRequest rollingRequest = CreateRequest(TravelDirection.Forward, 0.10d, false, false, CreateReferencePath(TravelDirection.Forward, false, 10d), CreateMap(false, 12d)); ConfigureFiveMeterWindowAndTwoSecondHorizon(rollingRequest.Configuration); EmPlanningResult rolling = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( rollingRequest, CancellationToken.None); VerifySuccess(rolling, rollingRequest, EmTerminalType.RollingSafetyStop, "cycle 1 rolling"); Verification.Equal(EmLongitudinalMode.RollingContinuation, rolling.Trajectory.Metadata.LongitudinalMode, "cycle 1 rolls"); Verification.Equal(21, rolling.Trajectory.Points.Count, "rolling publishes the two-second ST knot count"); EmTrajectoryPoint rollingTerminal = rolling.Trajectory.Points[rolling.Trajectory.Points.Count - 1]; Verification.True(rollingTerminal.PathS < 5d, "two-second ST output remains inside the five-metre LS window"); Verification.True(rollingTerminal.SignedLongitudinalVelocity != 0d, "cycle 1 has nonzero terminal speed"); EmPlanningRequest approachRequest = CreateRequest(TravelDirection.Forward, 0.10d, false, false, CreateReferencePath(TravelDirection.Forward, false, 4d), CreateMap(false, 5d)); EmPlanningResult approach = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( approachRequest, CancellationToken.None); VerifySuccess(approach, approachRequest, EmTerminalType.Goal, "cycle 2 approach"); Verification.Equal(EmLongitudinalMode.ApproachStopBoundary, approach.Trajectory.Metadata.LongitudinalMode, "cycle 2 approaches"); Verification.True(approach.Trajectory.Points[approach.Trajectory.Points.Count - 1].SignedLongitudinalVelocity != 0d, "approach has no synthetic stop tail"); EmPlanningRequest exactRequest = CreateRequest(TravelDirection.Forward, 0.05d, false, false, CreateReferencePath(TravelDirection.Forward, false, 0.0075d)); ConfigureExactStopServiceScenario(exactRequest.Configuration); EmPlanningResult exact = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( exactRequest, CancellationToken.None); VerifySuccess(exact, exactRequest, EmTerminalType.Goal, "cycle 3 exact goal stop"); Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, exact.Trajectory.Metadata.LongitudinalMode, "cycle 3 stops"); AssertExactStopStabilization(exact.Trajectory, EmBoundaryType.Goal, "goal"); EmPlanningRequest gearRequest = CreateRequest(TravelDirection.Forward, 0.05d, false, false, EmFixtureFactory.CreateGearPairReferencePath(0.0075d)); ConfigureExactStopServiceScenario(gearRequest.Configuration); EmPlanningResult gear = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( gearRequest, CancellationToken.None); VerifySuccess(gear, gearRequest, EmTerminalType.GearSwitch, "gear-switch exact stop"); Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, gear.Trajectory.Metadata.LongitudinalMode, "gear switch stops exactly"); AssertExactStopStabilization(gear.Trajectory, EmBoundaryType.GearSwitchApproach, "gear switch"); for (int index = 0; index < gear.Trajectory.Points.Count; index++) { Verification.Equal(TravelDirection.Forward, gear.Trajectory.Points[index].Direction, "gear-switch publication excludes the next direction point " + index); } } private static void VerifiesFullScopePublishesItsRequestScope() { EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0.05d, false, false, CreateReferencePath(TravelDirection.Forward, false, 0.0075d), null, EmPlanningScope.FullDirectionSegment); ConfigureExactStopServiceScenario(request.Configuration); double[] strictFullPrimal = CreateStrictFullScopePrimal(request.Configuration); EmPlanningResult result = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success, null, strictFullPrimal)).Plan( request, CancellationToken.None); VerifySuccess(result, request, EmTerminalType.Goal, "full scope publication"); Verification.Equal(EmPlanningScope.FullDirectionSegment, result.Trajectory.Metadata.PlanningScope, "service metadata preserves full scope"); Verification.NearlyEqual(0d, result.Trajectory.Points[0].PathS, "full publication starts at projection"); Verification.NearlyEqual(0.0075d, result.Trajectory.Points[result.Trajectory.Points.Count - 1].PathS, "full publication reaches the real segment boundary"); } private static void ConfigureFiveMeterWindowAndTwoSecondHorizon(EmPlannerConfiguration configuration) { configuration.Scheduling.DistanceHorizonMeters = 5d; configuration.Scheduling.TimeHorizonSeconds = 2d; configuration.Scheduling.OutputTimeStepSeconds = 0.1d; configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.2d; } private static void ConfigureExactStopServiceScenario(EmPlannerConfiguration configuration) { configuration.Scheduling.TimeHorizonSeconds = 0.40d; configuration.Scheduling.OutputTimeStepSeconds = 0.10d; configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d; configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 20d; configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; } private static double[] CreateStrictFullScopePrimal(EmPlannerConfiguration configuration) { var path = new LateralPath(new[] { new LateralPathPoint(0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d), new LateralPathPoint(0.0075d, 0.0075d, 0d, 0d, 0d, 0d, 0.0075d, 0d, 0d, 0d, 0d, 0d), }, true); EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.05d, EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason); Verification.Equal(EmPlanningStatus.Success, status, "full scope test envelope: " + failureReason); status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.05d, 0d, configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration, out LongitudinalKnotSchedule schedule, out failureReason); Verification.Equal(EmPlanningStatus.Success, status, "full scope test schedule: " + failureReason); var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.05d, 0d, EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration, EmPlanningScope.FullDirectionSegment, schedule, Array.Empty(), Array.Empty()); int motionIntervalCount = schedule.TerminalHoldStartIndex; Verification.True(motionIntervalCount >= 3, "full scope test schedule has three motion intervals"); int terminalFirstInterval = motionIntervalCount - 3; var terminalTimes = new double[4]; for (int index = 1; index < terminalTimes.Length; index++) terminalTimes[index] = terminalTimes[index - 1] + schedule.KnotTimes[terminalFirstInterval + index] - schedule.KnotTimes[terminalFirstInterval + index - 1]; var motionTimes = new double[motionIntervalCount + 1]; for (int index = 0; index < motionTimes.Length; index++) motionTimes[index] = schedule.KnotTimes[index]; var influence = new double[3, 3]; for (int interval = 0; interval < 3; interval++) { var basis = new double[3]; basis[interval] = 1d; LongitudinalCandidate response = LongitudinalCandidate.Integrate(terminalTimes, 0d, 0d, 0d, basis); int terminalIndex = response.S.Count - 1; influence[0, interval] = response.A[terminalIndex]; influence[1, interval] = response.U[terminalIndex]; influence[2, interval] = response.S[terminalIndex]; } var validator = new LongitudinalSolutionValidator(); for (int firstJerkStep = -20; firstJerkStep <= 0; firstJerkStep++) { for (int secondJerkStep = terminalFirstInterval >= 2 ? -20 : 0; secondJerkStep <= (terminalFirstInterval >= 2 ? 20 : 0); secondJerkStep++) { for (int thirdJerkStep = terminalFirstInterval >= 3 ? -20 : 0; thirdJerkStep <= (terminalFirstInterval >= 3 ? 20 : 0); thirdJerkStep++) { var jerk = new double[motionIntervalCount]; jerk[0] = firstJerkStep; if (terminalFirstInterval >= 2) jerk[1] = secondJerkStep; if (terminalFirstInterval >= 3) jerk[2] = thirdJerkStep; LongitudinalCandidate baseline = LongitudinalCandidate.Integrate(motionTimes, 0d, 0.05d, 0d, jerk); double[] target = { -baseline.A[baseline.A.Count - 1], -baseline.U[baseline.U.Count - 1], 0.0075d - baseline.S[baseline.S.Count - 1], }; if (!TrySolveThreeByThree(influence, target, out double[] terminalJerk)) throw new InvalidOperationException("Full scope strict candidate terminal system is singular."); for (int interval = 0; interval < 3; interval++) jerk[terminalFirstInterval + interval] = terminalJerk[interval]; LongitudinalCandidate motion = LongitudinalCandidate.Integrate(motionTimes, 0d, 0.05d, 0d, jerk); LongitudinalCandidate candidate = AppendFullStopTail(schedule.KnotTimes, motionIntervalCount, motion); if (!validator.TryValidate(input, speedLimit, candidate, out LongitudinalCandidate strict, out _)) continue; return ToPrimal(strict); } } } throw new InvalidOperationException("Unable to construct a strict full-scope test candidate: hold=" + motionIntervalCount + ";times=" + string.Join(",", schedule.KnotTimes)); } private static LongitudinalCandidate AppendFullStopTail(IReadOnlyList times, int motionIntervalCount, LongitudinalCandidate motion) { var pathS = new double[times.Count]; var speed = new double[times.Count]; var acceleration = new double[times.Count]; var jerk = new double[times.Count - 1]; for (int index = 0; index <= motionIntervalCount; index++) { pathS[index] = index == motionIntervalCount ? 0.0075d : motion.S[index]; speed[index] = index == motionIntervalCount ? 0d : motion.U[index]; acceleration[index] = index == motionIntervalCount ? 0d : motion.A[index]; } for (int index = motionIntervalCount + 1; index < times.Count; index++) pathS[index] = 0.0075d; for (int index = 0; index < motion.J.Count; index++) jerk[index] = motion.J[index]; return new LongitudinalCandidate(times, pathS, speed, acceleration, jerk); } private static double[] ToPrimal(LongitudinalCandidate candidate) { var layout = new LongitudinalVariableLayout(candidate.S.Count); var primal = new double[layout.VariableCount]; for (int index = 0; index < candidate.S.Count; index++) { primal[layout.S(index)] = candidate.S[index]; primal[layout.U(index)] = candidate.U[index]; primal[layout.A(index)] = candidate.A[index]; } for (int index = 0; index < candidate.J.Count; index++) primal[layout.J(index)] = candidate.J[index]; return primal; } private static bool TrySolveThreeByThree(double[,] matrix, IReadOnlyList rightHandSide, out double[] solution) { var augmented = new double[3, 4]; for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) augmented[row, column] = matrix[row, column]; augmented[row, 3] = rightHandSide[row]; } for (int column = 0; column < 3; column++) { int pivot = column; for (int row = column + 1; row < 3; row++) { if (Math.Abs(augmented[row, column]) > Math.Abs(augmented[pivot, column])) pivot = row; } if (Math.Abs(augmented[pivot, column]) < 1e-12d) { solution = Array.Empty(); return false; } if (pivot != column) { for (int index = column; index < 4; index++) { double temporary = augmented[column, index]; augmented[column, index] = augmented[pivot, index]; augmented[pivot, index] = temporary; } } double divisor = augmented[column, column]; for (int index = column; index < 4; index++) augmented[column, index] /= divisor; for (int row = 0; row < 3; row++) { if (row == column) continue; double factor = augmented[row, column]; for (int index = column; index < 4; index++) augmented[row, index] -= factor * augmented[column, index]; } } solution = new[] { augmented[0, 3], augmented[1, 3], augmented[2, 3] }; return true; } private static void AssertExactStopStabilization(EmTrajectory trajectory, EmBoundaryType boundaryType, string name) { int exactAnchor = -1; for (int index = 0; index < trajectory.Points.Count; index++) { if (trajectory.Points[index].BoundaryType == boundaryType) { exactAnchor = index; break; } } Verification.True(exactAnchor >= 0, name + " has a real boundary anchor"); Verification.NearlyEqual(0d, trajectory.Points[exactAnchor].SignedLongitudinalVelocity, name + " speed is zero"); Verification.True(trajectory.Points.Count > exactAnchor + 1, name + " anchor is followed by a QP stabilization point"); Verification.NearlyEqual(trajectory.Points[exactAnchor].PathS, trajectory.Points[exactAnchor + 1].PathS, name + " stabilization keeps the stop position"); Verification.NearlyEqual(0d, trajectory.Points[exactAnchor + 1].SignedLongitudinalVelocity, name + " stabilization speed is zero"); } private static void VerifiesRequestAndStateFailuresPublishNoTrajectory() { EmPlanningRequest invalidSmoothing = CreateRequest(TravelDirection.Forward, 0d, false, false, PathSmoothingResult.Failure(PathSmoothingStatus.Failed, new PathSmoothingDiagnostics())); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(invalidSmoothing, CancellationToken.None), EmPlanningStatus.InvalidReferencePath, "invalid smoothing"); EmPlanningRequest stale = CreateRequest(TravelDirection.Forward, 0d, false, false); stale = ReplaceState(stale, new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0d, null, stale.RequestedAtUtc.AddSeconds(-1d), stale.VehicleState.SequenceId)); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(stale, CancellationToken.None), EmPlanningStatus.StaleVehicleState, "stale state"); EmPlanningRequest directionMismatch = CreateRequest(TravelDirection.Reverse, 0.02d, false, false); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(directionMismatch, CancellationToken.None), EmPlanningStatus.StateDirectionMismatch, "state direction mismatch"); } private static void VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory() { EmPlanningRequest projectionFailure = CreateRequest(TravelDirection.Forward, 0d, false, false); projectionFailure = ReplaceState(projectionFailure, new VehicleMotionState(new Pose2D(3d, 0d, 0d), 0d, null, projectionFailure.RequestedAtUtc, projectionFailure.VehicleState.SequenceId)); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(projectionFailure, CancellationToken.None), EmPlanningStatus.ProjectionFailed, "bounded projection failure"); EmPlanningRequest corridorFailure = CreateRequest(TravelDirection.Forward, 0d, false, false, referencePath: null, map: CreateMap(true)); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(corridorFailure, CancellationToken.None), EmPlanningStatus.CorridorInfeasible, "corridor infeasible"); EmPlanningRequest stoppingFailure = CreateRequest(TravelDirection.Forward, 0.20d, false, false, referencePath: CreateReferencePath(TravelDirection.Forward, false, 0.0055d)); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(stoppingFailure, CancellationToken.None), EmPlanningStatus.StoppingDistanceInsufficient, "stopping distance insufficient"); EmPlanningRequest regular = CreateRequest(TravelDirection.Forward, 0d, false, false); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.LateralInfeasible)).Plan(regular, CancellationToken.None), EmPlanningStatus.LateralInfeasible, "lateral infeasible"); EmPlanningResult longitudinalFallback = new EmPlanningService( new ScriptedPipelineSolver(PipelineSolverMode.LongitudinalInfeasible)).Plan(regular, CancellationToken.None); Verification.Equal(EmPlanningStatus.SuccessWithFallback, longitudinalFallback.Status, "longitudinal infeasible uses the validated fallback seed"); Verification.True(longitudinalFallback.Trajectory != null, "longitudinal fallback still publishes a complete trajectory"); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.SolverUnavailable)).Plan(regular, CancellationToken.None), EmPlanningStatus.SolverUnavailable, "solver unavailable"); } private static void VerifiesTimeoutFallbackAndCancellationSemantics() { EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.TimeoutWithoutFallback)).Plan(request, CancellationToken.None), EmPlanningStatus.SolverTimedOut, "timeout without fallback"); EmPlanningResult fallback = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.TimeoutWithFallback)).Plan( request, CancellationToken.None); Verification.Equal(EmPlanningStatus.SuccessWithFallback, fallback.Status, "timeout uses only strict fallback"); Verification.True(fallback.Trajectory != null && fallback.Trajectory.Points.Count > 0, "timeout fallback publishes a complete trajectory"); using (var cancellation = new CancellationTokenSource()) { cancellation.Cancel(); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan(request, cancellation.Token), EmPlanningStatus.Cancelled, "cancellation"); } } private static void VerifiesNoProgressPublishesNoTrajectory() { EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false, CreateReferencePath(TravelDirection.Forward, false, 0.10d), null, EmPlanningScope.FullDirectionSegment); request.Configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 1d; request.Configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 1d; EmPlanningResult result = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.NoProgress)).Plan( request, CancellationToken.None); VerifyFailure(result, EmPlanningStatus.NoProgress, "full nonterminal no progress"); Verification.True(result.FailureReason.IndexOf("NoProgress", StringComparison.Ordinal) >= 0, "no-progress failure preserves its diagnostic"); } private static void VerifiesPublicationFailureAndDebugIsolation() { EmPlanningRequest validationFailure = CreateRequest(TravelDirection.Forward, 0d, false, false); VerifyFailure(new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.PublicationValidationFailure, validationFailure.Map)).Plan(validationFailure, CancellationToken.None), EmPlanningStatus.ValidationFailed, "publication validation failure"); EmPlanningRequest debugRequest = CreateRequest(TravelDirection.Forward, 0d, false, false); debugRequest.Configuration.Solver.NativeVerbose = true; EmPlanningResult debugIsolated = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success), new ThrowingDebugSink()).Plan(debugRequest, CancellationToken.None); VerifySuccess(debugIsolated, debugRequest, EmTerminalType.Goal, "debug-sink isolation"); } private static void VerifiesPublicationGateStatusMappings() { Verification.Equal(EmPlanningStatus.TerminalPoseMismatch, EmPlanningService.MapPublicationFailure(EmTrajectoryValidationFailure.TerminalPoseMismatch), "terminal-pose validator failure has its precise service status"); EmPlanningRequest sampleLimited = CreateRequest(TravelDirection.Forward, 0d, false, false); sampleLimited.Configuration.Scheduling.MaximumPublishedSampleCount = 2; EmPlanningResult sampleLimitResult = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.Success)).Plan( sampleLimited, CancellationToken.None); VerifyFailure(sampleLimitResult, EmPlanningStatus.FullSegmentResourceLimitExceeded, "sample-limit publication failure"); } private static void VerifiesPreviousTrajectoryIsALongitudinalSoftReference() { EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false); var withoutPreviousSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); EmPlanningResult withoutPrevious = new EmPlanningService(withoutPreviousSolver).Plan(request, CancellationToken.None); VerifySuccess(withoutPrevious, request, EmTerminalType.Goal, "no previous longitudinal seed"); EmTrajectory validPrevious = CreateLongitudinalPreviousTrajectory(request.EffectiveAtUtc, TravelDirection.Forward, request.SegmentIndex); EmPlanningRequest withPreviousRequest = ReplacePreviousTrajectory(request, validPrevious); var withPreviousSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); EmPlanningResult withPrevious = new EmPlanningService(withPreviousSolver).Plan(withPreviousRequest, CancellationToken.None); VerifySuccess(withPrevious, withPreviousRequest, EmTerminalType.Goal, "valid previous longitudinal seed"); QuadraticProgram withoutPreviousProblem = withoutPreviousSolver.LastLongitudinalProblem ?? throw new InvalidOperationException("The no-seed longitudinal QP was not captured."); QuadraticProgram withPreviousProblem = withPreviousSolver.LastLongitudinalProblem ?? throw new InvalidOperationException("The seeded longitudinal QP was not captured."); var layout = new LongitudinalVariableLayout((withPreviousProblem.VariableCount + 1) / 4); Verification.True(MatrixValue(withPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)) > MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)), "valid previous seed adds a nonzero previous-S soft-reference Hessian term"); Verification.True(MatrixValue(withPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)) > MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)), "valid previous seed adds a nonzero previous-U soft-reference Hessian term"); Verification.True(Math.Abs(withPreviousProblem.LinearCost[layout.S(1)] - withoutPreviousProblem.LinearCost[layout.S(1)]) > 1e-12d, "valid previous seed adds a nonzero previous-S soft-reference linear term"); Verification.True(Math.Abs(withPreviousProblem.LinearCost[layout.U(1)] - withoutPreviousProblem.LinearCost[layout.U(1)]) > 1e-12d, "valid previous seed adds a nonzero previous-U soft-reference linear term"); EmPlanningRequest incompatibleRequest = ReplacePreviousTrajectory(request, CreateLongitudinalPreviousTrajectory(request.EffectiveAtUtc, TravelDirection.Reverse, request.SegmentIndex)); var incompatibleSolver = new ScriptedPipelineSolver(PipelineSolverMode.Success); EmPlanningResult incompatible = new EmPlanningService(incompatibleSolver).Plan(incompatibleRequest, CancellationToken.None); VerifySuccess(incompatible, incompatibleRequest, EmTerminalType.Goal, "incompatible previous seed"); QuadraticProgram incompatibleProblem = incompatibleSolver.LastLongitudinalProblem ?? throw new InvalidOperationException("The incompatible-seed longitudinal QP was not captured."); Verification.NearlyEqual(MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.S(1), layout.S(1)), MatrixValue(incompatibleProblem.UpperTriangularP, layout.S(1), layout.S(1)), "incompatible previous seed omits previous-S soft-reference term"); Verification.NearlyEqual(MatrixValue(withoutPreviousProblem.UpperTriangularP, layout.U(1), layout.U(1)), MatrixValue(incompatibleProblem.UpperTriangularP, layout.U(1), layout.U(1)), "incompatible previous seed omits previous-U soft-reference term"); } private static void VerifySuccess(EmPlanningResult result, EmPlanningRequest request, EmTerminalType terminalType, string name) { Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, name + " successful status: " + result.FailureReason); Verification.True(result.Trajectory != null && result.Trajectory.Metadata.TerminalType == terminalType, name + " terminal type"); string identifiers = "map=" + request.Map.SnapshotId + ";reference=" + request.ReferencePathId + ";state=" + request.VehicleState.SequenceId + ";previous=" + request.PreviousTrajectoryId + ";segment=" + request.SegmentIndex; Verification.True(result.FailureReason.IndexOf(identifiers, StringComparison.Ordinal) >= 0, name + " preserves request identifiers in deterministic diagnostics"); } private static void VerifyFailure(EmPlanningResult result, EmPlanningStatus expected, string name) { Verification.Equal(expected, result.Status, name + " status: " + result.FailureReason); Verification.True(result.Trajectory == null, name + " publishes no partial trajectory"); } private static void VerifySameTrajectory(EmPlanningResult left, EmPlanningResult right, string name) { Verification.Equal(left.Status, right.Status, name + " status"); Verification.Equal(left.Trajectory.Points.Count, right.Trajectory.Points.Count, name + " point count"); for (int index = 0; index < left.Trajectory.Points.Count; index++) { EmTrajectoryPoint first = left.Trajectory.Points[index]; EmTrajectoryPoint second = right.Trajectory.Points[index]; Verification.NearlyEqual(first.X, second.X, name + " X " + index); Verification.NearlyEqual(first.Y, second.Y, name + " Y " + index); Verification.NearlyEqual(first.TimeFromStart, second.TimeFromStart, name + " time " + index); Verification.NearlyEqual(first.SignedLongitudinalVelocity, second.SignedLongitudinalVelocity, name + " signed speed " + index); } } private static EmPlanningRequest CreateRequest(TravelDirection direction, double signedSpeed, bool endsAtGearSwitch, bool rolling, PathSmoothingResult? referencePath = null, PlanningGridMap? map = null, EmPlanningScope planningScope = EmPlanningScope.RollingHorizon) { DateTimeOffset requestedAtUtc = DateTimeOffset.UnixEpoch.AddSeconds(10d); EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); configuration.Solver.MaximumOuterIterations = 2; configuration.Scheduling.SolverTimeoutSeconds = 1d; configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 0.20d; configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 0.20d; configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond = 0.20d; configuration.Longitudinal.DesiredReverseSpeedMetersPerSecond = 0.20d; if (rolling) configuration.Scheduling.DistanceHorizonMeters = 0.30d; return new EmPlanningRequest(referencePath ?? CreateReferencePath(direction, endsAtGearSwitch), map ?? CreateMap(false), new VehicleParameters { LengthMeters = 0.10d, WidthMeters = 0.10d, SafetyMarginMeters = 0d, MaximumCurvaturePerMeter = 1d, }, new VehicleMotionState(new Pose2D(0d, 0d, 0d), signedSpeed, 0d, requestedAtUtc, 7L), configuration, 0, null, requestedAtUtc, requestedAtUtc, "output-trajectory", "reference-42", "prior-42", EmMotionModel.NonholonomicForwardReverse, planningScope); } private static EmPlanningRequest ReplaceState(EmPlanningRequest source, VehicleMotionState state) { return new EmPlanningRequest(source.ReferencePath, source.Map, source.Vehicle, state, source.Configuration, source.SegmentIndex, source.PreviousTrajectory, source.RequestedAtUtc, source.EffectiveAtUtc, source.OutputTrajectoryId, source.ReferencePathId, source.PreviousTrajectoryId, source.MotionModel, source.PlanningScope); } private static EmPlanningRequest ReplacePreviousTrajectory(EmPlanningRequest source, EmTrajectory previousTrajectory) { return new EmPlanningRequest(source.ReferencePath, source.Map, source.Vehicle, source.VehicleState, source.Configuration, source.SegmentIndex, previousTrajectory, source.RequestedAtUtc, source.EffectiveAtUtc, source.OutputTrajectoryId, source.ReferencePathId, source.PreviousTrajectoryId, source.MotionModel, source.PlanningScope); } private static EmTrajectory CreateLongitudinalPreviousTrajectory(DateTimeOffset effectiveAtUtc, TravelDirection direction, int segmentIndex) { var metadata = new EmTrajectoryMetadata("previous-service", effectiveAtUtc, effectiveAtUtc, 1L, "previous-reference", 1L, string.Empty, segmentIndex, direction, EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, EmPlanningScope.RollingHorizon); double sign = direction == TravelDirection.Forward ? 1d : -1d; return new EmTrajectory(metadata, new[] { new EmTrajectoryPoint(sign * 0.001d, 0d, 0d, sign * 0.01d, 0d, 0d, segmentIndex, 0.001d, 0.001d, direction, EmBoundaryType.None, 0d, 0d), new EmTrajectoryPoint(sign * 0.001d, 0d, 0d, sign * 0.01d, 6d, 0d, segmentIndex, 0.001d, 0.001d, direction, EmBoundaryType.None, 0d, 0d), }); } private static double MatrixValue(SparseCscMatrix matrix, int row, int column) { for (int index = matrix.ColumnPointers[column]; index < matrix.ColumnPointers[column + 1]; index++) { if (matrix.RowIndices[index] == row) return matrix.Values[index]; } return 0d; } private static PathSmoothingResult CreateReferencePath(TravelDirection direction, bool endsAtGearSwitch, double lengthMeters = 2d) { double endX = direction == TravelDirection.Forward ? lengthMeters : -lengthMeters; var points = new List { new SmoothedPathPoint(0d, 0d, 0d, 0d, 0d, direction, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor), new SmoothedPathPoint(endX, 0d, 0d, 0d, lengthMeters, direction, 0d, 0d, 0d, 1d, endsAtGearSwitch, endsAtGearSwitch ? SmoothedPathPointSource.GearSwitch : SmoothedPathPointSource.Anchor), }; var segments = new List { new SmoothedPathSegment(0, direction, 0, 1, false, endsAtGearSwitch), }; var metrics = new PathQualityMetrics(true, lengthMeters, 0d, 0d, 0d, 0d, 1d, 0d, 0d, 0d, 0d, 0d); return PathSmoothingResult.PublishLocalG2(PathSmoothingStatus.Complete, points, segments, new PathSmoothingDiagnostics(metrics, TimeSpan.Zero), new List()); } private static PlanningGridMap CreateMap(bool blockStart, double halfExtentMeters = 3d) { IMapObstacleSource[] sources = blockStart ? new IMapObstacleSource[] { new ManualObstacleSource("service-obstacle", 1L, true, new IMapObstacle[] { new AxisAlignedRectangleObstacle(-20f, 20f, -20f, 20f) }) } : Array.Empty(); PlanningMapBuildResult result = new PlanningMapFactory().Create(new PlanningMapRequest { Bounds = new MapBoundsMm((float)(-1000d * halfExtentMeters), (float)(1000d * halfExtentMeters), -1000f, 1000f), ResolutionMm = 20f, ObstacleSources = sources, AllowExplicitEmptyMap = !blockStart, }); Verification.True(result.Succeeded && result.Map != null && result.Map.PlanningReady, "service map builds: " + result.FailureReason); return result.Map!; } private enum PipelineSolverMode { Success, LateralInfeasible, LongitudinalInfeasible, SolverUnavailable, TimeoutWithoutFallback, TimeoutWithFallback, NoProgress, PublicationValidationFailure, } private sealed class ScriptedPipelineSolver : IQpSolver { private readonly PipelineSolverMode mode; private readonly PlanningGridMap? mapToCorrupt; private readonly IReadOnlyList? strictFullPrimal; private int longitudinalCallCount; public QuadraticProgram? LastLongitudinalProblem { get; private set; } public ScriptedPipelineSolver(PipelineSolverMode mode, PlanningGridMap? mapToCorrupt = null, IReadOnlyList? strictFullPrimal = null) { this.mode = mode; this.mapToCorrupt = mapToCorrupt; this.strictFullPrimal = strictFullPrimal; } public QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings, IReadOnlyList warmStart, CancellationToken cancellationToken) { bool longitudinal = IsLongitudinalProblem(problem); if (mode == PipelineSolverMode.SolverUnavailable) return Result(QpSolveStatus.SolverUnavailable, Array.Empty()); if (!longitudinal) { if (mode == PipelineSolverMode.LateralInfeasible) return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); if (mode == PipelineSolverMode.TimeoutWithoutFallback) return Result(QpSolveStatus.TimeLimit, Array.Empty()); return Result(QpSolveStatus.Solved, new double[problem.VariableCount]); } LastLongitudinalProblem = problem; if (mode == PipelineSolverMode.LongitudinalInfeasible) return Result(QpSolveStatus.PrimalInfeasible, Array.Empty()); if (mode == PipelineSolverMode.NoProgress) return Result(QpSolveStatus.Solved, new double[problem.VariableCount]); if (strictFullPrimal != null && strictFullPrimal.Count == problem.VariableCount) { longitudinalCallCount++; return Result(QpSolveStatus.Solved, strictFullPrimal); } if (mode == PipelineSolverMode.PublicationValidationFailure && longitudinalCallCount == 0) CorruptMapAtOrigin(mapToCorrupt); if (mode == PipelineSolverMode.TimeoutWithFallback && ++longitudinalCallCount > 1) return Result(QpSolveStatus.TimeLimit, Array.Empty()); longitudinalCallCount++; return Result(QpSolveStatus.Solved, TryCreateStrictExactStopPrimal(problem, out double[] strictPrimal) ? strictPrimal : warmStart); } private static void CorruptMapAtOrigin(PlanningGridMap? map) { if (map == null || !map.TryWorldToGrid(0d, 0d, out int row, out int column)) throw new InvalidOperationException("Unable to corrupt the publication test map."); FieldInfo occupiedField = typeof(PlanningGridMap).GetField("_occupied", BindingFlags.Instance | BindingFlags.NonPublic)! ?? throw new InvalidOperationException("Planning map occupancy storage was unavailable."); FieldInfo distanceField = typeof(PlanningGridMap).GetField("_conservativeDistances", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("Planning map distance storage was unavailable."); byte[] occupied = (byte[])occupiedField.GetValue(map)!; double[] distances = (double[])distanceField.GetValue(map)!; int index = row * map.Cols + column; occupied[index] = 1; distances[index] = 0d; } private static bool TryCreateStrictExactStopPrimal(QuadraticProgram problem, out double[] primal) { primal = Array.Empty(); int variableCount = problem.VariableCount; int knotCount = (variableCount + 1) / 4; var layout = new LongitudinalVariableLayout(knotCount); int stabilizationStart = FindExactStopTailStart(problem, layout); if (stabilizationStart < 3) return false; var times = new double[knotCount]; for (int index = 0; index < knotCount - 1; index++) { if (!TryReadDynamicsDuration(problem, layout, index, out double duration)) return false; times[index + 1] = times[index] + duration; } var motionTimes = new double[stabilizationStart + 1]; Array.Copy(times, motionTimes, motionTimes.Length); double initialPathS = ReadFixedVariable(problem, layout.S(0)); double initialSpeed = ReadFixedVariable(problem, layout.U(0)); double initialAcceleration = ReadFixedVariable(problem, layout.A(0)); double terminalPathS = ReadFixedVariable(problem, layout.S(stabilizationStart)); var preferredJerk = new double[stabilizationStart]; LongitudinalCandidate baseline = LongitudinalCandidate.Integrate(motionTimes, initialPathS, initialSpeed, initialAcceleration, preferredJerk); var influence = new double[3, stabilizationStart]; for (int interval = 0; interval < stabilizationStart; interval++) { var basis = new double[stabilizationStart]; basis[interval] = 1d; LongitudinalCandidate response = LongitudinalCandidate.Integrate(motionTimes, 0d, 0d, 0d, basis); int terminalIndex = response.S.Count - 1; influence[0, interval] = response.A[terminalIndex]; influence[1, interval] = response.U[terminalIndex]; influence[2, interval] = response.S[terminalIndex]; } double[] target = { -baseline.A[baseline.A.Count - 1], -baseline.U[baseline.U.Count - 1], terminalPathS - baseline.S[baseline.S.Count - 1], }; var jerk = new double[knotCount - 1]; if (stabilizationStart >= 4) { int terminalFirstInterval = stabilizationStart - 3; var terminalInfluence = new double[3, 3]; for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) terminalInfluence[row, column] = influence[row, terminalFirstInterval + column]; } if (!TrySolveThreeByThree(terminalInfluence, target, out double[] terminalJerk)) return false; for (int interval = 0; interval < 3; interval++) jerk[terminalFirstInterval + interval] = terminalJerk[interval]; } else { var gram = new double[3, 3]; for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) { for (int interval = 0; interval < stabilizationStart; interval++) gram[row, column] += influence[row, interval] * influence[column, interval]; } } if (!TrySolveThreeByThree(gram, target, out double[] multipliers)) return false; for (int interval = 0; interval < stabilizationStart; interval++) { jerk[interval] = preferredJerk[interval]; for (int row = 0; row < 3; row++) jerk[interval] += influence[row, interval] * multipliers[row]; } } var motionJerk = new double[stabilizationStart]; Array.Copy(jerk, motionJerk, motionJerk.Length); LongitudinalCandidate candidate = LongitudinalCandidate.Integrate(motionTimes, initialPathS, initialSpeed, initialAcceleration, motionJerk); primal = CreateExactStopPrimal(layout, knotCount, stabilizationStart, terminalPathS, candidate); return true; } private static double[] CreateExactStopPrimal(LongitudinalVariableLayout layout, int knotCount, int stabilizationStart, double terminalPathS, LongitudinalCandidate candidate) { var primal = new double[layout.VariableCount]; for (int index = 0; index < knotCount; index++) { bool isTerminalTail = index >= stabilizationStart; primal[layout.S(index)] = isTerminalTail ? terminalPathS : candidate.S[index]; primal[layout.U(index)] = isTerminalTail ? 0d : candidate.U[index]; primal[layout.A(index)] = isTerminalTail ? 0d : candidate.A[index]; } for (int index = 0; index < candidate.J.Count; index++) primal[layout.J(index)] = candidate.J[index]; return primal; } private static int FindExactStopTailStart(QuadraticProgram problem, LongitudinalVariableLayout layout) { for (int index = 1; index < layout.KnotCount; index++) { if (TryReadFixedVariable(problem, layout.S(index), out _) && TryReadFixedVariable(problem, layout.U(index), out _) && TryReadFixedVariable(problem, layout.A(index), out _)) { return index; } } return -1; } private static bool TryReadDynamicsDuration(QuadraticProgram problem, LongitudinalVariableLayout layout, int interval, out double duration) { duration = 0d; for (int row = 0; row < problem.ConstraintCount; row++) { if (Math.Abs(problem.LowerBounds[row]) > 1e-12d || Math.Abs(problem.UpperBounds[row]) > 1e-12d || CountRowEntries(problem, row) != 3 || Math.Abs(ReadCoefficient(problem, row, layout.A(interval + 1)) - 1d) > 1e-12d || Math.Abs(ReadCoefficient(problem, row, layout.A(interval)) + 1d) > 1e-12d) { continue; } double jerkCoefficient = ReadCoefficient(problem, row, layout.J(interval)); if (jerkCoefficient >= -1e-12d) continue; duration = -jerkCoefficient; return true; } return false; } private static int CountRowEntries(QuadraticProgram problem, int row) { int count = 0; for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) { for (int index = problem.ConstraintMatrix.ColumnPointers[column]; index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) { if (problem.ConstraintMatrix.RowIndices[index] == row) count++; } } return count; } private static double ReadCoefficient(QuadraticProgram problem, int row, int column) { for (int index = problem.ConstraintMatrix.ColumnPointers[column]; index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) { if (problem.ConstraintMatrix.RowIndices[index] == row) return problem.ConstraintMatrix.Values[index]; } return 0d; } private static bool TrySolveThreeByThree(double[,] matrix, IReadOnlyList rightHandSide, out double[] solution) { var augmented = new double[3, 4]; for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) augmented[row, column] = matrix[row, column]; augmented[row, 3] = rightHandSide[row]; } for (int column = 0; column < 3; column++) { int pivot = column; for (int row = column + 1; row < 3; row++) { if (Math.Abs(augmented[row, column]) > Math.Abs(augmented[pivot, column])) pivot = row; } if (Math.Abs(augmented[pivot, column]) < 1e-12d) { solution = Array.Empty(); return false; } if (pivot != column) { for (int index = column; index < 4; index++) { double temporary = augmented[column, index]; augmented[column, index] = augmented[pivot, index]; augmented[pivot, index] = temporary; } } double divisor = augmented[column, column]; for (int index = column; index < 4; index++) augmented[column, index] /= divisor; for (int row = 0; row < 3; row++) { if (row == column) continue; double factor = augmented[row, column]; for (int index = column; index < 4; index++) augmented[row, index] -= factor * augmented[column, index]; } } solution = new[] { augmented[0, 3], augmented[1, 3], augmented[2, 3] }; return true; } private static bool IsLongitudinalProblem(QuadraticProgram problem) { if (problem.VariableCount < 7 || (problem.VariableCount + 1) % 4 != 0) return false; int knotCount = (problem.VariableCount + 1) / 4; return problem.ConstraintCount >= 8 * knotCount - 2; } private static double ReadFixedVariable(QuadraticProgram problem, int variable) { if (TryReadFixedVariable(problem, variable, out double value)) return value; throw new InvalidOperationException("Expected a fixed ST variable constraint."); } private static bool TryReadFixedVariable(QuadraticProgram problem, int variable, out double value) { for (int row = 0; row < problem.ConstraintCount; row++) { int entryCount = 0; double coefficient = 0d; for (int column = 0; column < problem.ConstraintMatrix.ColumnCount; column++) { for (int index = problem.ConstraintMatrix.ColumnPointers[column]; index < problem.ConstraintMatrix.ColumnPointers[column + 1]; index++) { if (problem.ConstraintMatrix.RowIndices[index] != row) continue; entryCount++; if (column == variable) coefficient = problem.ConstraintMatrix.Values[index]; } } if (entryCount == 1 && Math.Abs(coefficient) > 1e-12d && Math.Abs(problem.LowerBounds[row] - problem.UpperBounds[row]) <= 1e-12d) { value = problem.LowerBounds[row] / coefficient; return true; } } value = 0d; return false; } private static QpSolveResult Result(QpSolveStatus status, IReadOnlyList primal) { return new QpSolveResult(status, primal, 0d, 0d, 0d, 1, TimeSpan.Zero, status.ToString(), string.Empty); } } private sealed class ThrowingDebugSink : IEmPlannerDebugSink { public void Write(string message) { throw new InvalidOperationException("debug sink failure"); } } }