using System; using System.Collections.Generic; using EMPlannerVerificationHost; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; internal static class LateralModelChecks { public static void Run() { VerifiesDeterministicVariableLayout(); VerifiesExactDiscreteDynamicsForUnequalStations(); VerifiesPlanningInputBoundariesAndDefensiveCopies(); VerifiesLateralResultPublicationContract(); } private static void VerifiesDeterministicVariableLayout() { LateralVariableLayout layout = CreateLayout(4); Verification.Equal(15, layout.VariableCount, "layout variable count"); for (int index = 0; index < 4; index++) { Verification.Equal(index, layout.L(index), "l index " + index); Verification.Equal(4 + index, layout.DL(index), "dl index " + index); Verification.Equal(8 + index, layout.DDL(index), "ddl index " + index); } for (int index = 0; index < 3; index++) Verification.Equal(12 + index, layout.DDDL(index), "dddl index " + index); ExpectArgumentException(() => CreateLayout(1), "layout rejects fewer than two stations"); ExpectArgumentException(() => layout.L(4), "l index bounds check"); ExpectArgumentException(() => layout.DL(-1), "dl index bounds check"); ExpectArgumentException(() => layout.DDL(4), "ddl index bounds check"); ExpectArgumentException(() => layout.DDDL(3), "dddl index bounds check"); } private static void VerifiesExactDiscreteDynamicsForUnequalStations() { double[] stations = { 0d, 0.4d, 1.25d, 2.5d }; double[] jerks = { 0.5d, -0.3d, 0.2d }; LateralCandidate candidate = LateralCandidate.Integrate(stations, 0.1d, -0.2d, 0.3d, jerks); for (int index = 0; index < jerks.Length; index++) { double ds = stations[index + 1] - stations[index]; Verification.NearlyEqual(candidate.DDL[index] + ds * candidate.DDDL[index], candidate.DDL[index + 1], "exact ddl integration " + index); Verification.NearlyEqual(candidate.DL[index] + ds * candidate.DDL[index] + 0.5d * ds * ds * candidate.DDDL[index], candidate.DL[index + 1], "exact dl integration " + index); Verification.NearlyEqual(candidate.L[index] + ds * candidate.DL[index] + 0.5d * ds * ds * candidate.DDL[index] + ds * ds * ds * candidate.DDDL[index] / 6d, candidate.L[index + 1], "exact l integration " + index); } Verification.True(candidate.SatisfiesExactDiscreteDynamics(1e-12d), "integrated candidate validates exact dynamics"); LateralCandidate inconsistent = new LateralCandidate(new[] { 0d, 1d }, new[] { 0d, 1d }, new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d }); Verification.True(!inconsistent.SatisfiesExactDiscreteDynamics(1e-12d), "candidate detects inconsistent dynamics"); ExpectArgumentException(() => new LateralCandidate(new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d, 0d }, new[] { 0d }), "candidate rejects non-increasing stations"); } private static void VerifiesPlanningInputBoundariesAndDefensiveCopies() { DirectionSegmentView segment = CreateStraightSegment(); var corridorStations = new[] { new LateralInterval(0d, -0.3d, 0.3d, 0d), new LateralInterval(1d, -0.3d, 0.3d, 0d), new LateralInterval(2d, -0.3d, 0.3d, 0d), }; var seeds = new[] { new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0d, 0d, 0d), }; LateralPlanningInput input = new LateralPlanningInput(segment, new StaticCorridor(corridorStations), seeds[0], EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), seeds); corridorStations[1] = new LateralInterval(1d, -0.1d, 0.1d, 0d); seeds[0] = new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0.2d, 0d, 0d); Verification.NearlyEqual(-0.3d, input.Corridor.Stations[1].MinimumL, "input copies corridor stations"); Verification.NearlyEqual(0d, input.PreviousTrajectorySeed[0].LateralOffset, "input copies seed list"); ExpectArgumentException(() => new LateralPlanningInput(segment, new StaticCorridor(new[] { new LateralInterval(0d, -0.3d, 0.3d, 0d) }), input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), "input rejects fewer than two stations"); ExpectArgumentException(() => new LateralPlanningInput(segment, new StaticCorridor(new[] { new LateralInterval(0d, -0.3d, 0.3d, 0d), new LateralInterval(0d, -0.3d, 0.3d, 0d), }), input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), "input rejects non-increasing corridor stations"); ExpectArgumentException(() => new LateralPlanningInput(segment, new StaticCorridor(new[] { new LateralInterval(0.1d, -0.3d, 0.3d, 0d), new LateralInterval(2d, -0.3d, 0.3d, 0d), }), input.StartProjection, EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), "input rejects start-corridor station mismatch"); ExpectArgumentException(() => new LateralPlanningInput(segment, new StaticCorridor(new[] { new LateralInterval(0d, -0.1d, 0.1d, 0d), new LateralInterval(2d, -0.1d, 0.1d, 0d), }), new FrenetProjection(ReferencePathInterpolator.Interpolate(segment, 0d), 0.2d, 0d, 0d), EmTerminalType.Goal, CreateVehicle(), EmPlannerConfiguration.CreateDefault(), Array.Empty()), "input rejects start projection outside first hard interval"); } private static void VerifiesLateralResultPublicationContract() { LateralPath unvalidated = new LateralPath(new[] { CreatePathPoint(0d) }, false); LateralPath validated = new LateralPath(new[] { CreatePathPoint(0d), CreatePathPoint(1d) }, true); ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.Success, unvalidated, string.Empty), "success requires an independently validated path"); ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.SuccessWithFallback, new LateralPath(Array.Empty(), true), string.Empty), "fallback requires a non-empty path"); ExpectArgumentException(() => new LateralPlanningResult(EmPlanningStatus.LateralInfeasible, validated, string.Empty), "failed result has no candidate"); LateralPlanningResult result = new LateralPlanningResult(EmPlanningStatus.SuccessWithFallback, validated, "fallback"); Verification.Equal(validated, result.Path, "fallback path is preserved"); } private static DirectionSegmentView CreateStraightSegment() { var points = new List { Point(0d, 0d), Point(1d, 1d), Point(2d, 2d), }; return new DirectionSegmentView(0, TravelDirection.Forward, points, new ReferenceBoundary(0, 0d, EmBoundaryType.None, 0d), new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 2d), 0d); } private static SmoothedPathPoint Point(double x, double s) { return new SmoothedPathPoint(x, 0d, 0d, 0d, s, TravelDirection.Forward, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor); } private static VehicleParameters CreateVehicle() { return new VehicleParameters { LengthMeters = 0.1d, WidthMeters = 0.1d, SafetyMarginMeters = 0d, MaximumCurvaturePerMeter = 1d, }; } private static LateralPathPoint CreatePathPoint(double referenceS) { return new LateralPathPoint(referenceS, referenceS, 0d, 0d, 0d, 0d, referenceS, 0d, 0d, 0d, 0d, 0d); } private static LateralVariableLayout CreateLayout(int stationCount) { return new LateralVariableLayout(stationCount); } private static void ExpectArgumentException(Action action, string name) { try { action(); } catch (ArgumentException) { return; } throw new InvalidOperationException(name + " did not throw ArgumentException."); } }