Files
ParkingRobot/MultiWheelC/Fleet/FleetStateEstimator.cs
T

580 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MyParking.Shared;
namespace MultiWheelC.Fleet
{
// 一辆成员车在主车统一时间轴上的状态样本。
public readonly struct FleetMemberStateSample
{
public FleetMemberStateSample(
int vehicleId,
double sampleTimestampSeconds,
Pose2D poseInWorld,
Twist2D twistAtVehicleOriginInWorld,
bool isStateAvailable,
bool hasValidVelocityEstimate)
{
if (vehicleId <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(vehicleId),
"编队成员车号必须大于零。");
}
NumericGuard.EnsureFiniteNonNegative(
sampleTimestampSeconds,
nameof(sampleTimestampSeconds));
NumericGuard.EnsureFinite(
poseInWorld,
nameof(poseInWorld));
NumericGuard.EnsureFinite(
twistAtVehicleOriginInWorld,
nameof(twistAtVehicleOriginInWorld));
VehicleId = vehicleId;
SampleTimestampSeconds = sampleTimestampSeconds;
PoseInWorld = new Pose2D(
poseInWorld.XMeters,
poseInWorld.YMeters,
AngleMath.NormalizeRadians(
poseInWorld.YawRadians));
TwistAtVehicleOriginInWorld =
twistAtVehicleOriginInWorld;
IsStateAvailable = isStateAvailable;
HasValidVelocityEstimate =
hasValidVelocityEstimate;
}
public int VehicleId { get; }
// 该时间戳必须已经换算到主车/协调器的单调时间轴。
public double SampleTimestampSeconds { get; }
public Pose2D PoseInWorld { get; }
// 成员车体中心处的实际速度,在世界坐标系中表达。
public Twist2D TwistAtVehicleOriginInWorld { get; }
public bool IsStateAvailable { get; }
public bool HasValidVelocityEstimate { get; }
}
// 成员实际位姿相对固定布局目标位姿的误差。
public readonly struct FleetMemberLayoutError
{
public FleetMemberLayoutError(
int vehicleId,
Pose2D actualPoseInExpectedVehicleFrame)
{
if (vehicleId <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(vehicleId),
"编队成员车号必须大于零。");
}
NumericGuard.EnsureFinite(
actualPoseInExpectedVehicleFrame,
nameof(actualPoseInExpectedVehicleFrame));
VehicleId = vehicleId;
ActualPoseInExpectedVehicleFrame =
new Pose2D(
actualPoseInExpectedVehicleFrame.XMeters,
actualPoseInExpectedVehicleFrame.YMeters,
AngleMath.NormalizeRadians(
actualPoseInExpectedVehicleFrame
.YawRadians));
}
public int VehicleId { get; }
// 期望成员车体系中表达的实际成员位姿;理想刚体布局时为Identity。
public Pose2D ActualPoseInExpectedVehicleFrame { get; }
}
// 一次车队状态估计的结果;不可用时不提供FleetState。
public sealed class FleetStateEstimateResult
{
private static readonly IReadOnlyList<FleetMemberLayoutError>
EmptyMemberErrors = Array.AsReadOnly(
Array.Empty<FleetMemberLayoutError>());
private FleetStateEstimateResult(
bool isAvailable,
FleetState? state,
IReadOnlyList<FleetMemberLayoutError> memberErrors,
string unavailableReason)
{
IsAvailable = isAvailable;
State = state;
MemberErrors = memberErrors;
UnavailableReason = unavailableReason;
}
public bool IsAvailable { get; }
public FleetState? State { get; }
public IReadOnlyList<FleetMemberLayoutError> MemberErrors { get; }
public string UnavailableReason { get; }
internal static FleetStateEstimateResult Available(
FleetState state,
FleetMemberLayoutError[] memberErrors)
{
return new FleetStateEstimateResult(
true,
state,
Array.AsReadOnly(memberErrors),
string.Empty);
}
internal static FleetStateEstimateResult Unavailable(
string reason)
{
return new FleetStateEstimateResult(
false,
null,
EmptyMemberErrors,
reason ?? string.Empty);
}
}
// 从各成员状态反算并融合车队虚拟中心状态。
public sealed class FleetStateEstimator
{
private const double TimestampToleranceSeconds = 1e-9;
private const double MinimumCircularMeanMagnitude = 1e-12;
private readonly double _maximumMemberStateAgeSeconds;
private readonly double _maximumPositionDisagreementMeters;
private readonly double _maximumYawDisagreementRadians;
public FleetStateEstimator(
double maximumMemberStateAgeSeconds,
double maximumPositionDisagreementMeters,
double maximumYawDisagreementRadians)
{
NumericGuard.EnsureFinitePositive(
maximumMemberStateAgeSeconds,
nameof(maximumMemberStateAgeSeconds));
NumericGuard.EnsureFinitePositive(
maximumPositionDisagreementMeters,
nameof(maximumPositionDisagreementMeters));
NumericGuard.EnsureFinitePositive(
maximumYawDisagreementRadians,
nameof(maximumYawDisagreementRadians));
if (maximumYawDisagreementRadians > Math.PI)
{
throw new ArgumentOutOfRangeException(
nameof(maximumYawDisagreementRadians),
"车队候选航向差阈值不能大于π。");
}
_maximumMemberStateAgeSeconds =
maximumMemberStateAgeSeconds;
_maximumPositionDisagreementMeters =
maximumPositionDisagreementMeters;
_maximumYawDisagreementRadians =
maximumYawDisagreementRadians;
}
public FleetStateEstimateResult Estimate(
FleetLayout layout,
IReadOnlyList<FleetMemberStateSample> memberStates,
double targetTimestampSeconds)
{
if (layout == null)
{
throw new ArgumentNullException(nameof(layout));
}
if (memberStates == null)
{
throw new ArgumentNullException(nameof(memberStates));
}
NumericGuard.EnsureFiniteNonNegative(
targetTimestampSeconds,
nameof(targetTimestampSeconds));
if (memberStates.Count != layout.VehicleCount)
{
return FleetStateEstimateResult.Unavailable(
$"成员状态数量{memberStates.Count}与布局数量" +
$"{layout.VehicleCount}不一致。");
}
var statesByVehicleId =
new Dictionary<int, FleetMemberStateSample>(
memberStates.Count);
for (var index = 0;
index < memberStates.Count;
index++)
{
var memberState = memberStates[index];
if (memberState.VehicleId <= 0)
{
return FleetStateEstimateResult.Unavailable(
$"第{index}个成员状态的车号无效。");
}
if (!layout.TryGetVehicle(
memberState.VehicleId,
out _))
{
return FleetStateEstimateResult.Unavailable(
$"成员状态包含布局外车辆" +
$"{memberState.VehicleId}。");
}
if (statesByVehicleId.ContainsKey(
memberState.VehicleId))
{
return FleetStateEstimateResult.Unavailable(
$"成员状态包含重复车号" +
$"{memberState.VehicleId}。");
}
statesByVehicleId.Add(
memberState.VehicleId,
memberState);
}
var alignedMembers =
new AlignedMemberState[layout.VehicleCount];
for (var index = 0;
index < layout.Vehicles.Count;
index++)
{
var vehicleLayout = layout.Vehicles[index];
if (!statesByVehicleId.TryGetValue(
vehicleLayout.VehicleId,
out var memberState))
{
return FleetStateEstimateResult.Unavailable(
$"缺少车辆{vehicleLayout.VehicleId}的状态。");
}
var alignmentResult = AlignMemberState(
memberState,
targetTimestampSeconds,
out var alignedPoseInWorld);
if (alignmentResult != null)
{
return FleetStateEstimateResult.Unavailable(
alignmentResult);
}
var candidateFleetPoseInWorld =
FrameTransform2D.Compose(
alignedPoseInWorld,
FrameTransform2D.Inverse(
vehicleLayout.PoseInFleet));
alignedMembers[index] =
new AlignedMemberState(
vehicleLayout,
memberState,
alignedPoseInWorld,
candidateFleetPoseInWorld);
}
var disagreementReason =
FindCandidateDisagreement(alignedMembers);
if (disagreementReason != null)
{
return FleetStateEstimateResult.Unavailable(
disagreementReason);
}
if (!TryAverageCandidateFleetPose(
alignedMembers,
out var fleetPoseInWorld))
{
return FleetStateEstimateResult.Unavailable(
"成员候选航向无法形成唯一的车队平均航向。");
}
var hasValidVelocityEstimate =
TryAverageFleetOriginTwist(
alignedMembers,
fleetPoseInWorld,
out var twistAtFleetOriginInWorld);
var fleetState = new FleetState(
targetTimestampSeconds,
fleetPoseInWorld,
twistAtFleetOriginInWorld,
hasValidVelocityEstimate);
var memberErrors = CalculateMemberErrors(
alignedMembers,
fleetPoseInWorld);
return FleetStateEstimateResult.Available(
fleetState,
memberErrors);
}
private string AlignMemberState(
FleetMemberStateSample memberState,
double targetTimestampSeconds,
out Pose2D alignedPoseInWorld)
{
alignedPoseInWorld = memberState.PoseInWorld;
if (!memberState.IsStateAvailable)
{
return $"车辆{memberState.VehicleId}状态不可用。";
}
var ageSeconds =
targetTimestampSeconds -
memberState.SampleTimestampSeconds;
if (ageSeconds < -TimestampToleranceSeconds)
{
return $"车辆{memberState.VehicleId}的状态时间晚于" +
"本次估计目标时间。";
}
if (ageSeconds > _maximumMemberStateAgeSeconds)
{
return $"车辆{memberState.VehicleId}的状态已过期:" +
$"{ageSeconds:F3}s。";
}
if (ageSeconds <= TimestampToleranceSeconds)
{
return null;
}
if (!memberState.HasValidVelocityEstimate)
{
return $"车辆{memberState.VehicleId}缺少时间对齐所需的" +
"有效速度。";
}
var twist = memberState.TwistAtVehicleOriginInWorld;
alignedPoseInWorld = new Pose2D(
memberState.PoseInWorld.XMeters +
twist.VxMetersPerSecond * ageSeconds,
memberState.PoseInWorld.YMeters +
twist.VyMetersPerSecond * ageSeconds,
AngleMath.NormalizeRadians(
memberState.PoseInWorld.YawRadians +
twist.OmegaRadiansPerSecond * ageSeconds));
return null;
}
private string FindCandidateDisagreement(
IReadOnlyList<AlignedMemberState> alignedMembers)
{
for (var firstIndex = 0;
firstIndex < alignedMembers.Count;
firstIndex++)
{
var first = alignedMembers[firstIndex];
for (var secondIndex = firstIndex + 1;
secondIndex < alignedMembers.Count;
secondIndex++)
{
var second = alignedMembers[secondIndex];
var dx =
first.CandidateFleetPoseInWorld.XMeters -
second.CandidateFleetPoseInWorld.XMeters;
var dy =
first.CandidateFleetPoseInWorld.YMeters -
second.CandidateFleetPoseInWorld.YMeters;
var positionDifferenceMeters =
Math.Sqrt(dx * dx + dy * dy);
var yawDifferenceRadians = Math.Abs(
AngleMath.ShortestDifferenceRadians(
first.CandidateFleetPoseInWorld
.YawRadians,
second.CandidateFleetPoseInWorld
.YawRadians));
if (positionDifferenceMeters >
_maximumPositionDisagreementMeters)
{
return $"车辆{first.VehicleLayout.VehicleId}与" +
$"车辆{second.VehicleLayout.VehicleId}反算的" +
$"车队中心相差{positionDifferenceMeters:F3}m" +
"超过允许值。";
}
if (yawDifferenceRadians >
_maximumYawDisagreementRadians)
{
return $"车辆{first.VehicleLayout.VehicleId}与" +
$"车辆{second.VehicleLayout.VehicleId}反算的" +
$"车队航向相差" +
$"{AngleMath.RadiansToDegrees(yawDifferenceRadians):F2}°," +
"超过允许值。";
}
}
}
return null;
}
private static bool TryAverageCandidateFleetPose(
IReadOnlyList<AlignedMemberState> alignedMembers,
out Pose2D fleetPoseInWorld)
{
var xMeters = 0.0;
var yMeters = 0.0;
var yawCosineSum = 0.0;
var yawSineSum = 0.0;
for (var index = 0;
index < alignedMembers.Count;
index++)
{
var candidate =
alignedMembers[index]
.CandidateFleetPoseInWorld;
xMeters += candidate.XMeters;
yMeters += candidate.YMeters;
yawCosineSum += Math.Cos(candidate.YawRadians);
yawSineSum += Math.Sin(candidate.YawRadians);
}
var count = alignedMembers.Count;
var circularMeanMagnitude = Math.Sqrt(
yawCosineSum * yawCosineSum +
yawSineSum * yawSineSum);
if (circularMeanMagnitude <
MinimumCircularMeanMagnitude)
{
fleetPoseInWorld = Pose2D.Identity;
return false;
}
fleetPoseInWorld = new Pose2D(
xMeters / count,
yMeters / count,
Math.Atan2(yawSineSum, yawCosineSum));
return true;
}
private static bool TryAverageFleetOriginTwist(
IReadOnlyList<AlignedMemberState> alignedMembers,
Pose2D fleetPoseInWorld,
out Twist2D twistAtFleetOriginInWorld)
{
for (var index = 0;
index < alignedMembers.Count;
index++)
{
if (!alignedMembers[index]
.MemberState
.HasValidVelocityEstimate)
{
twistAtFleetOriginInWorld = Twist2D.Zero;
return false;
}
}
var vxMetersPerSecond = 0.0;
var vyMetersPerSecond = 0.0;
var omegaRadiansPerSecond = 0.0;
for (var index = 0;
index < alignedMembers.Count;
index++)
{
var member = alignedMembers[index];
var twist = member.MemberState
.TwistAtVehicleOriginInWorld;
var memberXFromFleetOrigin =
member.AlignedPoseInWorld.XMeters -
fleetPoseInWorld.XMeters;
var memberYFromFleetOrigin =
member.AlignedPoseInWorld.YMeters -
fleetPoseInWorld.YMeters;
vxMetersPerSecond +=
twist.VxMetersPerSecond +
twist.OmegaRadiansPerSecond *
memberYFromFleetOrigin;
vyMetersPerSecond +=
twist.VyMetersPerSecond -
twist.OmegaRadiansPerSecond *
memberXFromFleetOrigin;
omegaRadiansPerSecond +=
twist.OmegaRadiansPerSecond;
}
var count = alignedMembers.Count;
twistAtFleetOriginInWorld = new Twist2D(
vxMetersPerSecond / count,
vyMetersPerSecond / count,
omegaRadiansPerSecond / count);
return true;
}
private static FleetMemberLayoutError[] CalculateMemberErrors(
IReadOnlyList<AlignedMemberState> alignedMembers,
Pose2D fleetPoseInWorld)
{
var errors =
new FleetMemberLayoutError[alignedMembers.Count];
for (var index = 0;
index < alignedMembers.Count;
index++)
{
var member = alignedMembers[index];
var expectedPoseInWorld =
FrameTransform2D.Compose(
fleetPoseInWorld,
member.VehicleLayout.PoseInFleet);
var actualPoseInExpectedVehicleFrame =
FrameTransform2D.Compose(
FrameTransform2D.Inverse(
expectedPoseInWorld),
member.AlignedPoseInWorld);
errors[index] = new FleetMemberLayoutError(
member.VehicleLayout.VehicleId,
actualPoseInExpectedVehicleFrame);
}
return errors;
}
private readonly struct AlignedMemberState
{
public AlignedMemberState(
VehicleLayout vehicleLayout,
FleetMemberStateSample memberState,
Pose2D alignedPoseInWorld,
Pose2D candidateFleetPoseInWorld)
{
VehicleLayout = vehicleLayout;
MemberState = memberState;
AlignedPoseInWorld = alignedPoseInWorld;
CandidateFleetPoseInWorld =
candidateFleetPoseInWorld;
}
public VehicleLayout VehicleLayout { get; }
public FleetMemberStateSample MemberState { get; }
public Pose2D AlignedPoseInWorld { get; }
public Pose2D CandidateFleetPoseInWorld { get; }
}
}
}