2026-08-24 18:39:41 +08:00
|
|
|
|
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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|