Files
ParkingRobot/MultiWheelC.Tests/FleetCoordinatorTests.cs
T

655 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using MultiWheelC.Control.Abstractions;
using MultiWheelC.Control.Allocation;
using MultiWheelC.Fleet;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetCoordinatorTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyCommandCycle();
VerifySmallLayoutErrorIsCorrected();
VerifyWarningRangeScalesAllCommands();
VerifyUnavailableStateStopsAndRecovers();
VerifyUnsafeLayoutFaultsAndLatches();
VerifyCompletionStops();
VerifyTrackingFaultStops();
VerifyCancelReturnsInactive();
Console.WriteLine(
"FleetCoordinator车队协调测试通过。共8个场景。");
}
private static void VerifyCommandCycle()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"正常协调周期");
if (!output.State.HasValue)
{
throw new InvalidOperationException(
"正常协调周期没有返回车队状态。");
}
AssertNear(
output.State.Value.FleetPoseInWorld.XMeters,
0.5,
"正常协调周期中心X");
AssertNear(
output.SpeedScale,
1.0,
"正常协调周期速度比例");
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.4,
0.0,
0.0,
"正常协调周期车队命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.4,
0.0,
0.0,
"正常协调周期车辆1");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.4,
0.0,
0.0,
"正常协调周期车辆2");
}
private static void VerifySmallLayoutErrorIsCorrected()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
new[]
{
CreateMemberState(
1,
new Pose2D(-0.99, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.99, 0.0, Math.PI),
1.0)
},
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"小范围布局误差纠偏");
AssertNear(
output.SpeedScale,
1.0,
"小范围布局误差速度比例");
AssertTwist(
FindCommand(output.BaseMemberCommands, 1)
.TwistInVehicleBody,
0.4,
0.0,
0.0,
"车辆1基础命令");
AssertTwist(
FindCommand(output.BaseMemberCommands, 2)
.TwistInVehicleBody,
-0.4,
0.0,
0.0,
"车辆2基础命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.395,
0.0,
0.0,
"车辆1纠偏后命令");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.405,
0.0,
0.0,
"车辆2纠偏后命令");
}
private static void VerifyWarningRangeScalesAllCommands()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
new[]
{
CreateMemberState(
1,
new Pose2D(-0.965, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.965, 0.0, Math.PI),
1.0)
},
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"警告区间统一缩放");
AssertNear(
output.SpeedScale,
0.5,
"警告区间速度比例");
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.2,
0.0,
0.0,
"警告区间车队命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.2,
0.0,
0.0,
"警告区间车辆1");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.2,
0.0,
0.0,
"警告区间车辆2");
if (string.IsNullOrWhiteSpace(output.Reason))
{
throw new InvalidOperationException(
"警告区间缩放没有返回限制原因。");
}
}
private static void VerifyUnavailableStateStopsAndRecovers()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var unavailableStates = CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.0);
unavailableStates[1] = new FleetMemberStateSample(
unavailableStates[1].VehicleId,
unavailableStates[1].SampleTimestampSeconds,
unavailableStates[1].PoseInWorld,
unavailableStates[1].TwistAtVehicleOriginInWorld,
isStateAvailable: false,
hasValidVelocityEstimate: true);
var waitingResult = coordinator.ExecuteCycle(
unavailableStates,
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var waitingOutput);
AssertResult(
waitingResult,
FleetCoordinationCycleResult.WaitingForState,
"状态暂时不可用");
AssertStop(waitingOutput, layout.VehicleCount);
if (!coordinator.IsActive)
{
throw new InvalidOperationException(
"状态暂时不可用不应取消车队控制器。");
}
var recoveredResult = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.02),
targetTimestampSeconds: 1.02,
deltaTimeSeconds: 0.02,
out _);
AssertResult(
recoveredResult,
FleetCoordinationCycleResult.CommandGenerated,
"状态恢复");
}
private static void VerifyUnsafeLayoutFaultsAndLatches()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var deformedStates = new[]
{
CreateMemberState(
1,
new Pose2D(-0.9, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.9, 0.0, Math.PI),
1.0)
};
var result = coordinator.ExecuteCycle(
deformedStates,
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Faulted,
"布局误差超限");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsFaulted ||
string.IsNullOrWhiteSpace(
coordinator.LastFailureReason))
{
throw new InvalidOperationException(
"布局误差故障没有被锁存。");
}
var latchedResult = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.02),
targetTimestampSeconds: 1.02,
deltaTimeSeconds: 0.02,
out var latchedOutput);
AssertResult(
latchedResult,
FleetCoordinationCycleResult.Faulted,
"布局误差故障锁存");
AssertStop(latchedOutput, layout.VehicleCount);
}
private static void VerifyCompletionStops()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(1.0, 0.0, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Completed,
"车队轨迹完成");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsCompleted)
{
throw new InvalidOperationException(
"车队轨迹完成状态没有被保存。");
}
}
private static void VerifyTrackingFaultStops()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.5, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Faulted,
"车队中心跟踪故障");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsFaulted)
{
throw new InvalidOperationException(
"车队中心跟踪故障没有传递到协调器。");
}
}
private static void VerifyCancelReturnsInactive()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
coordinator.Cancel();
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Inactive,
"取消车队协调");
AssertStop(output, layout.VehicleCount);
}
private static FleetCoordinator CreateCoordinator()
{
var estimator = new FleetStateEstimator(
maximumMemberStateAgeSeconds: 0.25,
maximumPositionDisagreementMeters: 0.5,
maximumYawDisagreementRadians:
AngleMath.DegreesToRadians(10.0));
var controller = new FleetController(
new StraightLateralController(),
new ReferenceLongitudinalController(),
new GcpCommandAllocator(Math.PI / 4.0),
virtualControlPointRadiusMeters: 0.5);
var commandCorrector =
new FleetMemberCommandCorrector(
longitudinalPositionGainPerSecond: 1.0,
lateralPositionGainPerSecond: 1.0,
yawGainPerSecond: 1.0,
positionErrorDeadbandMeters: 0.005,
yawErrorDeadbandRadians:
AngleMath.DegreesToRadians(0.5),
maximumLinearCorrectionMetersPerSecond:
0.03,
maximumAngularCorrectionRadiansPerSecond:
AngleMath.DegreesToRadians(2.0));
return new FleetCoordinator(
estimator,
controller,
commandCorrector,
memberPositionErrorWarningMeters: 0.02,
maximumMemberPositionErrorMeters: 0.05,
memberYawErrorWarningRadians:
AngleMath.DegreesToRadians(1.0),
maximumMemberYawErrorRadians:
AngleMath.DegreesToRadians(3.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 Trajectory2D CreateTrajectory()
{
return new Trajectory2D(
new[]
{
new TrajectoryPoint(
0.0,
Pose2D.Identity,
0.0,
0.4),
new TrajectoryPoint(
1.0,
new Pose2D(1.0, 0.0, 0.0),
0.0,
0.4)
});
}
private static FleetMemberStateSample[]
CreateRigidMemberStates(
FleetLayout layout,
Pose2D fleetPoseInWorld,
Twist2D twistAtFleetOriginInWorld,
double timestampSeconds)
{
var states =
new FleetMemberStateSample[layout.VehicleCount];
for (var index = 0;
index < layout.Vehicles.Count;
index++)
{
var vehicle = layout.Vehicles[index];
var poseInWorld = FrameTransform2D.Compose(
fleetPoseInWorld,
vehicle.PoseInFleet);
var offsetX =
poseInWorld.XMeters -
fleetPoseInWorld.XMeters;
var offsetY =
poseInWorld.YMeters -
fleetPoseInWorld.YMeters;
var twistInWorld = new Twist2D(
twistAtFleetOriginInWorld.VxMetersPerSecond -
twistAtFleetOriginInWorld.OmegaRadiansPerSecond *
offsetY,
twistAtFleetOriginInWorld.VyMetersPerSecond +
twistAtFleetOriginInWorld.OmegaRadiansPerSecond *
offsetX,
twistAtFleetOriginInWorld.OmegaRadiansPerSecond);
states[index] = new FleetMemberStateSample(
vehicle.VehicleId,
timestampSeconds,
poseInWorld,
twistInWorld,
isStateAvailable: true,
hasValidVelocityEstimate: true);
}
return states;
}
private static FleetMemberStateSample CreateMemberState(
int vehicleId,
Pose2D poseInWorld,
double timestampSeconds)
{
return new FleetMemberStateSample(
vehicleId,
timestampSeconds,
poseInWorld,
Twist2D.Zero,
isStateAvailable: true,
hasValidVelocityEstimate: true);
}
private static FleetMemberCommand FindCommand(
IReadOnlyList<FleetMemberCommand> 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 AssertStop(
FleetCoordinationCycleOutput output,
int expectedMemberCount)
{
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.0,
0.0,
0.0,
"车队停止命令");
if (output.MemberCommands.Count != expectedMemberCount)
{
throw new InvalidOperationException(
"停止输出的成员命令数量错误。");
}
if (output.BaseMemberCommands.Count != expectedMemberCount)
{
throw new InvalidOperationException(
"停止输出的成员基础命令数量错误。");
}
for (var index = 0;
index < output.MemberCommands.Count;
index++)
{
AssertTwist(
output.BaseMemberCommands[index]
.TwistInVehicleBody,
0.0,
0.0,
0.0,
"成员基础停止命令");
AssertTwist(
output.MemberCommands[index].TwistInVehicleBody,
0.0,
0.0,
0.0,
"成员停止命令");
}
}
private static void AssertResult(
FleetCoordinationCycleResult actual,
FleetCoordinationCycleResult expected,
string scenario)
{
if (actual != expected)
{
throw new InvalidOperationException(
$"{scenario}结果错误:" +
$"actual={actual}, expected={expected}。");
}
}
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}。");
}
}
private sealed class StraightLateralController :
ILateralController
{
public LateralControlCommand Compute(
PathTrackingContext context)
{
return LateralControlCommand.Straight;
}
public void Reset()
{
}
}
private sealed class ReferenceLongitudinalController :
ILongitudinalController
{
public double ComputeSpeedMetersPerSecond(
PathTrackingContext context)
{
return context.ControlReferenceSpeedMetersPerSecond;
}
public void Reset()
{
}
}
}
}