using System; using System.Collections.Generic; using MultiWheelC.Fleet; using MyParking.Shared; namespace MultiWheelC.Tests { internal static class FleetMemberCommandCorrectorTests { private const double Tolerance = 1e-9; public static void Run() { VerifyZeroErrorsPreserveBaseCommands(); VerifyRelativePositionErrorProducesOpposingCorrection(); VerifyCommonTranslationIsRemoved(); VerifyCommonRotationIsRemoved(); VerifyDeadbandSuppressesSmallErrors(); VerifyCorrectionLimits(); Console.WriteLine( "FleetMemberCommandCorrector测试通过,共6个场景。"); } private static void VerifyZeroErrorsPreserveBaseCommands() { var layout = CreateLayout(); var baseCommands = FleetKinematics.Decompose( layout, new FleetMotionCommand( Point2D.Zero, new Twist2D(0.4, 0.1, 0.05))); var corrected = CreateCorrector().Correct( layout, baseCommands, CreateErrors(Pose2D.Identity, Pose2D.Identity)); AssertCommandsEqual( corrected, baseCommands, "零布局误差"); } private static void VerifyRelativePositionErrorProducesOpposingCorrection() { var layout = CreateLayout(); var corrected = CreateCorrector().Correct( layout, CreateStopCommands(layout), CreateErrors( new Pose2D(0.02, 0.0, 0.0), new Pose2D(0.02, 0.0, 0.0))); AssertTwist( FindCommand(corrected, 1).TwistInVehicleBody, -0.02, 0.0, 0.0, "车辆1相对位置纠偏"); AssertTwist( FindCommand(corrected, 2).TwistInVehicleBody, -0.02, 0.0, 0.0, "车辆2相对位置纠偏"); } private static void VerifyCommonTranslationIsRemoved() { var layout = CreateLayout(); var corrected = CreateCorrector().Correct( layout, CreateStopCommands(layout), CreateErrors( new Pose2D(0.02, 0.0, 0.0), new Pose2D(-0.02, 0.0, 0.0))); AssertAllStopped( corrected, "共同平移不应成为成员相对纠偏"); } private static void VerifyCommonRotationIsRemoved() { const double fleetYawErrorRadians = 0.02; var layout = CreateLayout(); var commonRotationError = new Pose2D( 0.0, -fleetYawErrorRadians, fleetYawErrorRadians); var corrected = CreateCorrector().Correct( layout, CreateStopCommands(layout), CreateErrors( commonRotationError, commonRotationError)); AssertAllStopped( corrected, "共同旋转不应成为成员相对纠偏"); } private static void VerifyDeadbandSuppressesSmallErrors() { var layout = CreateLayout(); var corrector = new FleetMemberCommandCorrector( longitudinalPositionGainPerSecond: 1.0, lateralPositionGainPerSecond: 1.0, yawGainPerSecond: 1.0, positionErrorDeadbandMeters: 0.005, yawErrorDeadbandRadians: 0.02, maximumLinearCorrectionMetersPerSecond: 1.0, maximumAngularCorrectionRadiansPerSecond: 1.0); var corrected = corrector.Correct( layout, CreateStopCommands(layout), CreateErrors( new Pose2D(0.004, 0.003, 0.01), new Pose2D(0.004, -0.003, -0.01))); AssertAllStopped(corrected, "布局误差死区"); } private static void VerifyCorrectionLimits() { var layout = CreateLayout(); var corrector = new FleetMemberCommandCorrector( longitudinalPositionGainPerSecond: 1.0, lateralPositionGainPerSecond: 1.0, yawGainPerSecond: 1.0, positionErrorDeadbandMeters: 0.0, yawErrorDeadbandRadians: 0.0, maximumLinearCorrectionMetersPerSecond: 0.03, maximumAngularCorrectionRadiansPerSecond: 0.05); var corrected = corrector.Correct( layout, CreateStopCommands(layout), CreateErrors( new Pose2D(0.2, 0.0, 0.2), new Pose2D(0.2, 0.0, -0.2))); for (var index = 0; index < corrected.Count; index++) { var twist = corrected[index].TwistInVehicleBody; var linearMagnitude = Math.Sqrt( twist.VxMetersPerSecond * twist.VxMetersPerSecond + twist.VyMetersPerSecond * twist.VyMetersPerSecond); AssertNear( linearMagnitude, 0.03, "线速度纠偏限幅"); AssertNear( Math.Abs(twist.OmegaRadiansPerSecond), 0.05, "角速度纠偏限幅"); } } private static FleetMemberCommandCorrector CreateCorrector() { return new FleetMemberCommandCorrector( longitudinalPositionGainPerSecond: 1.0, lateralPositionGainPerSecond: 1.0, yawGainPerSecond: 1.0, positionErrorDeadbandMeters: 0.0, yawErrorDeadbandRadians: 0.0, maximumLinearCorrectionMetersPerSecond: 1.0, maximumAngularCorrectionRadiansPerSecond: 1.0); } private static FleetLayout CreateLayout() { return new FleetLayout( new[] { new VehicleLayout( 1, new Pose2D(-1.0, 0.0, 0.0)), new VehicleLayout( 2, new Pose2D(1.0, 0.0, Math.PI)) }); } private static IReadOnlyList CreateStopCommands(FleetLayout layout) { return FleetKinematics.Decompose( layout, FleetMotionCommand.Stop()); } private static FleetMemberLayoutError[] CreateErrors( Pose2D vehicle1Error, Pose2D vehicle2Error) { return new[] { new FleetMemberLayoutError(1, vehicle1Error), new FleetMemberLayoutError(2, vehicle2Error) }; } private static FleetMemberCommand FindCommand( IReadOnlyList commands, int vehicleId) { for (var index = 0; index < commands.Count; index++) { if (commands[index].VehicleId == vehicleId) { return commands[index]; } } throw new InvalidOperationException( $"没有找到车辆{vehicleId}的成员命令。"); } private static void AssertCommandsEqual( IReadOnlyList actual, IReadOnlyList expected, string scenario) { if (actual.Count != expected.Count) { throw new InvalidOperationException( $"{scenario}的命令数量不一致。"); } for (var index = 0; index < expected.Count; index++) { var expectedCommand = expected[index]; var actualCommand = FindCommand( actual, expectedCommand.VehicleId); AssertTwist( actualCommand.TwistInVehicleBody, expectedCommand.TwistInVehicleBody .VxMetersPerSecond, expectedCommand.TwistInVehicleBody .VyMetersPerSecond, expectedCommand.TwistInVehicleBody .OmegaRadiansPerSecond, scenario); } } private static void AssertAllStopped( IReadOnlyList commands, string scenario) { for (var index = 0; index < commands.Count; index++) { AssertTwist( commands[index].TwistInVehicleBody, 0.0, 0.0, 0.0, scenario); } } private static void AssertTwist( Twist2D actual, double expectedVx, double expectedVy, double expectedOmega, string scenario) { AssertNear( actual.VxMetersPerSecond, expectedVx, scenario + " Vx"); AssertNear( actual.VyMetersPerSecond, expectedVy, scenario + " Vy"); AssertNear( actual.OmegaRadiansPerSecond, expectedOmega, scenario + " Omega"); } private static void AssertNear( double actual, double expected, string valueName) { if (Math.Abs(actual - expected) > Tolerance) { throw new InvalidOperationException( $"{valueName}错误:" + $"actual={actual:F9}, expected={expected:F9}。"); } } } }