using System; using System.Collections.Generic; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Independently recomputes and verifies lateral world geometry before a path may be published. public sealed class LateralSolutionValidator { public bool TryValidate(LateralPlanningInput input, LateralCandidate candidate, LateralPath path, out LateralPath validatedPath, out string failureReason) { validatedPath = null; failureReason = string.Empty; if (input == null || candidate == null || path == null) { failureReason = "Lateral input, candidate, and reconstructed path are required."; return false; } if (path.Points.Count != input.ReferenceStations.Count || candidate.ReferenceStations.Count != path.Points.Count) { failureReason = "Lateral path point count does not match the candidate stations."; return false; } try { double spatialTolerance = RequireNonnegative(input.Configuration.Validation.SpatialToleranceMeters, "spatial tolerance"); double kinematicTolerance = RequireNonnegative(input.Configuration.Validation.KinematicTolerance, "kinematic tolerance"); double residualTolerance = RequireNonnegative(input.Configuration.Solver.StrictResidualTolerance, "strict residual tolerance"); if (!ValidateCandidateConstraints(input, candidate, spatialTolerance, kinematicTolerance, residualTolerance, out failureReason)) { return false; } List expected = ReconstructIndependently(input, candidate, out failureReason); if (expected == null) return false; if (!ComparePath(path, candidate, input.ReferenceStations, expected, spatialTolerance, kinematicTolerance, out failureReason)) { return false; } validatedPath = new LateralPath(path.Points, true); return true; } catch (ArgumentException exception) { failureReason = exception.Message; return false; } } private static bool ValidateCandidateConstraints(LateralPlanningInput input, LateralCandidate candidate, double spatialTolerance, double kinematicTolerance, double residualTolerance, out string failureReason) { failureReason = string.Empty; if (candidate.ReferenceStations.Count != input.ReferenceStations.Count) { failureReason = "Candidate station count does not match the input."; return false; } if (!candidate.SatisfiesExactDiscreteDynamics(residualTolerance)) { failureReason = "Candidate violates exact lateral dynamics."; return false; } LateralConfiguration lateral = input.Configuration.Lateral; double maximumCurvature = GetMaximumVehicleCurvature(input.Vehicle); for (int index = 0; index < input.ReferenceStations.Count; index++) { if (Math.Abs(candidate.ReferenceStations[index] - input.ReferenceStations[index]) > spatialTolerance) { failureReason = "Candidate reference-S does not match the input at station " + index + "."; return false; } LateralInterval corridor = input.Corridor.Stations[index]; double l = candidate.L[index]; double dl = candidate.DL[index]; double ddl = candidate.DDL[index]; if (!IsFinite(l) || !IsFinite(dl) || !IsFinite(ddl) || l < corridor.MinimumL - spatialTolerance || l > corridor.MaximumL + spatialTolerance || Math.Abs(l) > input.Configuration.Corridor.MaximumLateralOffsetMeters + spatialTolerance || Math.Abs(dl) > lateral.MaximumLateralSlope + kinematicTolerance || Math.Abs(ddl) > lateral.MaximumLateralSecondDerivativePerMeter + kinematicTolerance) { failureReason = "Candidate violates lateral corridor or derivative limits at station " + index + "."; return false; } FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, input.ReferenceStations[index]); double denominator = 1d - reference.GeometricCurvature * l; if (!IsFinite(denominator) || denominator < input.Configuration.Frenet.MinimumFrenetDenominator - kinematicTolerance) { failureReason = "Candidate violates the Frenet denominator at station " + index + "."; return false; } double geometricCurvature = CalculateGeometricCurvature(reference, l, dl, ddl, (input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d) * reference.VehicleCurvatureDerivative); double vehicleCurvature = (input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d) * geometricCurvature; if (!IsFinite(geometricCurvature) || !IsFinite(vehicleCurvature) || Math.Abs(vehicleCurvature) > maximumCurvature + kinematicTolerance) { failureReason = "Candidate violates vehicle curvature at station " + index + "."; return false; } } for (int index = 0; index < candidate.DDDL.Count; index++) { if (!IsFinite(candidate.DDDL[index]) || Math.Abs(candidate.DDDL[index]) > lateral.MaximumLateralThirdDerivativePerSquareMeter + kinematicTolerance) { failureReason = "Candidate violates third-derivative limits at interval " + index + "."; return false; } } double startDenominator = 1d - input.StartProjection.ReferencePoint.GeometricCurvature * input.StartProjection.LateralOffset; double expectedStartDL = startDenominator * Math.Tan(input.StartProjection.HeadingError); if (!IsFinite(expectedStartDL) || Math.Abs(candidate.L[0] - input.StartProjection.LateralOffset) > spatialTolerance || Math.Abs(candidate.DL[0] - expectedStartDL) > kinematicTolerance) { failureReason = "Candidate violates the lateral start state."; return false; } if (input.TerminalType != EmTerminalType.RollingSafetyStop && (Math.Abs(candidate.L[candidate.L.Count - 1]) > spatialTolerance || Math.Abs(candidate.DL[candidate.DL.Count - 1]) > kinematicTolerance)) { failureReason = "Candidate violates the exact terminal lateral state."; return false; } return true; } private static List ReconstructIndependently(LateralPlanningInput input, LateralCandidate candidate, out string failureReason) { failureReason = string.Empty; double minimumDenominator = input.Configuration.Frenet.MinimumFrenetDenominator; double directionSign = input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d; var samples = new List(candidate.ReferenceStations.Count); for (int index = 0; index < candidate.ReferenceStations.Count; index++) { FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, candidate.ReferenceStations[index]); double l = candidate.L[index]; double dl = candidate.DL[index]; double ddl = candidate.DDL[index]; double denominator = 1d - reference.GeometricCurvature * l; if (!IsFinite(denominator) || denominator < minimumDenominator) { failureReason = "Independent reconstruction found a Frenet denominator violation at station " + index + "."; return null; } double travelYaw = reference.TravelYaw + Math.Atan2(dl, denominator); double vehicleYaw = input.ReferenceSegment.Direction == TravelDirection.Forward ? AngleMath.NormalizeRadians(travelYaw) : AngleMath.NormalizeRadians(travelYaw + Math.PI); double geometricCurvature = CalculateGeometricCurvature(reference, l, dl, ddl, directionSign * reference.VehicleCurvatureDerivative); double vehicleCurvature = directionSign * geometricCurvature; double x = reference.X - l * Math.Sin(reference.TravelYaw); double y = reference.Y + l * Math.Cos(reference.TravelYaw); if (!IsFinite(x) || !IsFinite(y) || !IsFinite(travelYaw) || !IsFinite(vehicleYaw) || !IsFinite(geometricCurvature) || !IsFinite(vehicleCurvature)) { failureReason = "Independent reconstruction produced non-finite geometry at station " + index + "."; return null; } samples.Add(new ExpectedSample(candidate.ReferenceStations[index], x, y, vehicleYaw, geometricCurvature, vehicleCurvature)); } samples[0].PathS = 0d; for (int index = 1; index < samples.Count; index++) { double dx = samples[index].X - samples[index - 1].X; double dy = samples[index].Y - samples[index - 1].Y; double chord = Math.Sqrt(dx * dx + dy * dy); if (!IsFinite(chord) || chord <= 0d) { failureReason = "Independent reconstruction found non-increasing PathS at station " + index + "."; return null; } samples[index].PathS = samples[index - 1].PathS + chord; } for (int index = 0; index < samples.Count; index++) { int lower = index == 0 ? 0 : index - 1; int upper = index == samples.Count - 1 ? samples.Count - 1 : index + 1; double span = samples[upper].PathS - samples[lower].PathS; if (!IsFinite(span) || span <= 0d) { failureReason = "Independent curvature derivative span is invalid at station " + index + "."; return null; } samples[index].VehicleCurvatureDerivative = (samples[upper].VehicleCurvature - samples[lower].VehicleCurvature) / span; } return samples; } private static bool ComparePath(LateralPath path, LateralCandidate candidate, IReadOnlyList stations, IReadOnlyList expected, double spatialTolerance, double kinematicTolerance, out string failureReason) { failureReason = string.Empty; for (int index = 0; index < path.Points.Count; index++) { LateralPathPoint actual = path.Points[index]; ExpectedSample sample = expected[index]; double dddl = candidate.DDDL[Math.Min(index, candidate.DDDL.Count - 1)]; if (!AreClose(actual.ReferenceS, stations[index], spatialTolerance) || !AreClose(actual.PathS, sample.PathS, spatialTolerance) || !AreClose(actual.L, candidate.L[index], spatialTolerance) || !AreClose(actual.DL, candidate.DL[index], kinematicTolerance) || !AreClose(actual.DDL, candidate.DDL[index], kinematicTolerance) || !AreClose(actual.DDDL, dddl, kinematicTolerance) || !AreClose(actual.X, sample.X, spatialTolerance) || !AreClose(actual.Y, sample.Y, spatialTolerance) || Math.Abs(AngleMath.NormalizeRadians(actual.VehicleYaw - sample.VehicleYaw)) > kinematicTolerance || !AreClose(actual.GeometricCurvature, sample.GeometricCurvature, kinematicTolerance) || !AreClose(actual.VehicleCurvature, sample.VehicleCurvature, kinematicTolerance) || !AreClose(actual.VehicleCurvatureDerivative, sample.VehicleCurvatureDerivative, kinematicTolerance)) { failureReason = "Independent lateral geometry validation failed at station " + index + "."; return false; } if (index == 0 && Math.Abs(actual.PathS) > spatialTolerance) { failureReason = "PathS must start at zero."; return false; } if (index > 0 && actual.PathS <= path.Points[index - 1].PathS + spatialTolerance) { failureReason = "PathS must be strictly increasing."; return false; } } return true; } private static double CalculateGeometricCurvature(FrenetReferencePoint reference, double l, double dl, double ddl, double referenceCurvatureDerivative) { double a = 1d - reference.GeometricCurvature * l; double denominatorSquared = a * a + dl * dl; double numerator = a * a * reference.GeometricCurvature + a * ddl + referenceCurvatureDerivative * l * dl + 2d * reference.GeometricCurvature * dl * dl; return numerator / (denominatorSquared * Math.Sqrt(denominatorSquared)); } private static double GetMaximumVehicleCurvature(VehicleParameters vehicle) { if (vehicle == null) throw new ArgumentNullException(nameof(vehicle)); if (vehicle.MaximumCurvaturePerMeter.HasValue) return RequirePositive(vehicle.MaximumCurvaturePerMeter.Value, "vehicle maximum curvature"); if (vehicle.MinimumTurningRadiusMeters.HasValue) return 1d / RequirePositive(vehicle.MinimumTurningRadiusMeters.Value, "vehicle minimum turning radius"); throw new ArgumentException("Vehicle maximum curvature is required.", nameof(vehicle)); } private static bool AreClose(double actual, double expected, double tolerance) { return IsFinite(actual) && IsFinite(expected) && Math.Abs(actual - expected) <= tolerance; } private static double RequirePositive(double value, string name) { if (!IsFinite(value) || value <= 0d) throw new ArgumentOutOfRangeException(name); return value; } private static double RequireNonnegative(double value, string name) { if (!IsFinite(value) || value < 0d) throw new ArgumentOutOfRangeException(name); return value; } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } private sealed class ExpectedSample { public ExpectedSample(double referenceS, double x, double y, double vehicleYaw, double geometricCurvature, double vehicleCurvature) { ReferenceS = referenceS; X = x; Y = y; VehicleYaw = vehicleYaw; GeometricCurvature = geometricCurvature; VehicleCurvature = vehicleCurvature; } public double ReferenceS { get; } public double X { get; } public double Y { get; } public double VehicleYaw { get; } public double GeometricCurvature { get; } public double VehicleCurvature { get; } public double PathS { get; set; } public double VehicleCurvatureDerivative { get; set; } } }